web/back/UserPage.ts
import ourbigbook from 'ourbigbook'
import { getLoggedInUser } from 'back'
import { getServerSidePropsArticleHoc } from 'back/ArticlePage'
import { articleLimit } from 'front/config'
import { getList, getLocked, getOrderAndPage, idToSlug, uidTopicIdToId } from 'front/js'
import { MyGetServerSideProps } from 'front/types'
import { UserPageProps } from 'front/UserPage'
import { cant } from 'front/cant'
export const getServerSidePropsUserHoc = (what): MyGetServerSideProps => {
return async (context) => {
const { params: { uid, parentTopicId }, query, req, res } = context
const parentTopicIdString = parentTopicId === undefined ? undefined : (
// Not sure how this can be a string which is the reason for the cast.
parentTopicId as string[]
).join(ourbigbook.Macro.HEADER_SCOPE_SEPARATOR)
if (
typeof uid === 'string'
) {
const sequelize = req.sequelize
const { Article, Comment, Issue, Ref, SignupBlacklistIp, Upload, UploadDirectory, User } = sequelize.models
const [loggedInUser, user] = await Promise.all([
getLoggedInUser(req, res),
User.findOne({
where: { username: uid },
}),
])
if (!user) {
return {
notFound: true
}
}
const list = getList(req, res)
const locked = getLocked(req, res)
let author, articlesFollowedBy, likedBy, following, followedBy, itemType
let allowedSorts, allowedSortsExtra, defaultOrder, parentFromTo, parentId, parentType
switch (what) {
case 'user-files':
case 'user-files-tree':
itemType = 'file'
break
case 'follows':
followedBy = uid
itemType = 'user'
break
case 'followed':
following = uid
itemType = 'user'
break
case 'home':
itemType = null
break
case 'liked':
author = uid
itemType = 'like'
break
case 'liked-discussions':
// TODO https://github.com/ourbigbook/ourbigbook/issues/313
author = uid
itemType = 'discussion-like'
break
case 'likes':
likedBy = uid
itemType = 'article'
break
case 'likes-discussions':
likedBy = uid
itemType = 'discussion'
break
case 'followed-articles':
articlesFollowedBy = uid
itemType = 'article'
break
case 'followed-discussions':
articlesFollowedBy = uid
itemType = 'discussion'
break
case 'user-articles':
author = uid
itemType = 'article'
break
case 'user-child-articles':
author = uid
// TODO would be more efficient here with Ref.to_id_index.
// But that requires generalizing the Article.getArticles interface a bit,
// not in the mood right now.
defaultOrder = 'nestedSetIndex',
itemType = 'article'
parentId = uidTopicIdToId(uid, parentTopicIdString)
parentType = Ref.Types[ourbigbook.REFS_TABLE_PARENT]
break
case 'user-incoming-articles':
author = uid
defaultOrder = 'slug',
itemType = 'article'
parentId = uidTopicIdToId(uid, parentTopicIdString)
parentFromTo = 'from'
parentType = Ref.Types[ourbigbook.REFS_TABLE_X]
break
case 'user-tagged-articles':
author = uid
defaultOrder = 'slug',
itemType = 'article'
parentId = uidTopicIdToId(uid, parentTopicIdString)
parentType = Ref.Types[ourbigbook.REFS_TABLE_X_CHILD]
break
case 'user-comments':
author = uid
itemType = 'comment'
break
case 'user-issues':
author = uid
itemType = 'discussion'
break
default:
throw new Error(`Unknown search: ${what}`)
}
switch (what) {
case 'user-files':
allowedSortsExtra = { size: 'size', path: 'path' }
break
case 'user-articles':
allowedSortsExtra = Article.ALLOWED_SORTS_EXTRA
break
case 'user-comments':
allowedSortsExtra = Comment.ALLOWED_SORTS_EXTRA
break
case 'user-issues':
allowedSortsExtra = Issue.ALLOWED_SORTS_EXTRA
break
default:
allowedSorts = undefined
// It is harder to do the rest efficiently as they would require indices across tables.
allowedSortsExtra = {}
}
const getOrderAndPageOpts: any = { allowedSortsExtra }
if (allowedSorts) {
getOrderAndPageOpts.allowedSorts = allowedSorts
}
const { ascDesc, err, order, page } = getOrderAndPage(req, query.page, {
allowedSorts,
allowedSortsExtra,
defaultOrder,
})
if (err) { res.statusCode = 422 }
const offset = page * articleLimit
const getArticlesOpts = {
author,
followedBy: articlesFollowedBy,
likedBy,
limit: articleLimit,
list,
offset,
order,
orderAscDesc: ascDesc,
parentFromTo,
parentId,
parentType,
topicIdSearch: query.search,
sequelize,
}
const articlesPromise =
itemType === 'article' ? Article.getArticles(getArticlesOpts) :
itemType === 'discussion' ? Issue.getIssues({
author,
likedBy,
followedBy: articlesFollowedBy,
includeArticle: true,
limit: articleLimit,
list,
offset,
order,
orderAscDesc: ascDesc,
sequelize,
}) :
[]
const likesPromise =
itemType === 'like' ? User.findAndCountArticleLikesReceived(user.id, {
offset, order, orderAscDesc: ascDesc }) :
itemType === 'discussion-like' ? User.findAndCountDiscussionLikesReceived(user.id, {
offset, order, orderAscDesc: ascDesc }) :
[]
const usersPromise = itemType === 'user' ? User.getUsers({
following,
followedBy,
locked,
limit: articleLimit,
offset,
order,
orderAscDesc: ascDesc,
sequelize,
}) : []
const lockedUsersPromise = itemType === 'user' ? User.getUsers({
following,
followedBy,
limit: 1,
locked: true,
sequelize,
}) : { count: 0 }
const updateNewScoreLastCheckPromise = (what === 'liked' && loggedInUser && user.id === loggedInUser.id) ?
user.update({ newScoreLastCheck: Date.now() }) : null
const [
articles,
comments,
userJson,
hasListedContent,
lockedUsers,
loggedInUserJson,
likes,
unlistedArticles,
parentArticleJson,
totalArticlesByUser,
totalCommentsByUser,
totalDiscussionsByUser,
users,
signupIpIsBlacklisted,
fileIndex,
rootDirectory,
] = await Promise.all([
// articles
articlesPromise,
// comments
itemType === 'comment'
? Comment.getComments({ authorId: user.id, limit: articleLimit, list, offset })
: {}
,
// userJson
user.toJson(loggedInUser),
// hasListedContent
cant.updateSiteSettings(loggedInUser) ? false : Promise.all([
Article.findOne({ where: { authorId: user.id, list: true }, attributes: ['id'] }),
Issue.findOne({ where: { authorId: user.id, list: true }, attributes: ['id'] }),
Comment.findOne({ where: { authorId: user.id, list: true }, attributes: ['id'] }),
Upload.findOne({ where: { ...Upload.fileIndexWhere(user.id), list: true }, attributes: ['id'] }),
]).then(items => items.some(item => !!item)),
// lockedUsers
lockedUsersPromise,
// loggedInUserJson
loggedInUser ? loggedInUser.toJson() : undefined,
// likes
likesPromise,
// unlisted content
itemType === 'article'
? Article.getArticles(Object.assign({}, getArticlesOpts, { list: false, rows: false }))
: itemType === 'discussion'
? Issue.getIssues({
author,
followedBy: articlesFollowedBy,
likedBy,
limit: 1,
list: false,
sequelize,
})
: itemType === 'file'
? Upload.count({ where: { ...Upload.fileIndexWhere(user.id), list: false } }).then(count => ({ count }))
: itemType === 'comment'
? Comment.getComments({ authorId: user.id, limit: 1, list: false })
: {}
,
// parentArticleJson
(parentId !== undefined)
? Article.getArticle({ slug: idToSlug(parentId), sequelize }).then(article => article ? article.toJson(loggedInUser) : null)
: null
,
// totalArticlesByUser
Article.count({ where: { authorId: user.id, list: true } }),
// totalCommentsByUser
user.commentCount,
// totalDiscussionsByUser
user.discussionCount,
// users
usersPromise,
// signupIpIsBlacklisted
cant.updateSiteSettings(loggedInUser) ? false : SignupBlacklistIp.findOne({ where: { ip: user.ip } }).then(ip => !!ip),
what === 'user-files' ? Upload.getFileIndex({ authorId: user.id, list, limit: articleLimit, offset, order, orderAscDesc: ascDesc }) : null,
what === 'user-files-tree' ? Upload.getDirectory({ authorId: user.id, list }) : null,
updateNewScoreLastCheckPromise,
])
if (parentTopicIdString && !parentArticleJson) {
return {
notFound: true
}
}
const props: UserPageProps = {
clearScoreDelta: !!updateNewScoreLastCheckPromise,
hasLocked: !!lockedUsers.count,
hasUnlisted: !!unlistedArticles.count,
itemType,
list: list === undefined ? null : list,
locked: locked === undefined ? null : locked,
order,
orderAscDesc: ascDesc,
page,
signupIpIsBlacklisted,
totalArticlesByUser,
totalCommentsByUser,
totalDiscussionsByUser,
user: userJson,
what,
}
if (!cant.updateSiteSettings(loggedInUser)) {
props.hasListedContent = !!hasListedContent
}
if (loggedInUser) {
props.loggedInUser = loggedInUserJson
}
if (parentArticleJson) {
props.parentArticle = parentArticleJson
}
if (itemType === 'file') {
if (what === 'user-files') {
props.files = fileIndex.files
props.filesCount = fileIndex.count
} else {
// An existing user always has an empty file root, even before their
// first upload (or after deleting their last file).
props.childDirectories = (rootDirectory?.childDirectories || [])
.sort((a, b) => a.path.localeCompare(b.path)).map(directory => directory.toEntryJson())
props.childFiles = (rootDirectory?.childFiles || [])
.sort((a, b) => a.path.localeCompare(b.path)).map(file => file.toEntryJson())
}
} else if (itemType === 'user') {
props.users = await Promise.all(users.rows.map(user => user.toJson(loggedInUser)))
props.usersCount = users.count
} else if (itemType === 'article' || itemType === 'discussion') {
props.articles = await Promise.all(articles.rows.map(article => article.toJson(loggedInUser)))
props.articlesCount = articles.count
} else if (itemType === 'like' || itemType === 'discussion-like') {
const articles = []
for (const like of likes.rows) {
const article = like.article
article.likedBy = like.user
article.likedByDate = like.createdAt
articles.push(article)
}
props.articles = await Promise.all(articles.map(article => article.toJson(loggedInUser)))
props.articlesCount = likes.count
} else if (itemType === 'comment') {
props.comments = await Promise.all(comments.rows.map(comment => comment.toJson(loggedInUser)))
props.commentsCount = comments.count
} else {
const articleContext = Object.assign({}, context, { params: { slug: [ uid ] } })
const articleProps = await (getServerSidePropsArticleHoc({
includeIssues: true,
loggedInUserCache: loggedInUser,
})(articleContext))
if ('props' in articleProps) {
Object.assign(props, articleProps.props)
}
}
return { props }
} else {
return { notFound: true }
}
}
}