/* ---------------------------------------------------------------------------- Question Server GitLab: This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ------------------------------------------------------------------------- */ import { v4 as uuidv4 } from 'uuid' import logger from '../../../utils/logger' import utils from '../../../utils/utils' import { Request, SubmoduleData, User } from '../../../types/basicTypes' interface Comment { date: string user: number content: string admin: boolean subComments?: Comment[] reacts?: { [key: string]: number[] } } interface ForumEntry { date: string user: number title: string content: string admin: boolean comments: Comment[] mediaPath?: string reacts: { [key: string]: number[] } } interface ForumContents { user: number title: string admin: boolean commentCount: number } const adminUsersFile = 'data/admins.json' const forumContentsFileName = '.contents.json' function countComments(comments: Comment[]) { return comments.reduce((acc, comment) => { if (comment.subComments) { acc += countComments(comment.subComments) + 1 } else { acc += 1 } return acc }, 0) } function addComment(obj: Comment[], path: number[], comment: Comment) { if (path.length === 0) { obj.push(comment) } else { const i = path.pop() if (!obj[i].subComments) { obj[i].subComments = [] } addComment(obj[i].subComments, path, comment) } } function deleteComment( obj: Comment[], path: number[], userid: number ): boolean { if (path.length === 1) { if (obj[path[0]].user === userid) { obj.splice(path[0], 1) return true } else { return false } } else { const i = path.pop() deleteComment(obj[i].subComments, path, userid) return true } } function addReaction( obj: Comment[], path: number[], { reaction, isDelete, uid, }: { reaction: string; isDelete: boolean; uid: number } ) { if (path.length === 1) { const index = path[0] if (!obj[index].reacts) { obj[index].reacts = {} } if (isDelete) { if (obj[index].reacts[reaction]) { obj[index].reacts[reaction] = obj[index].reacts[ reaction ].filter((currUid: number) => { return uid !== currUid }) if (obj[index].reacts[reaction].length === 0) { delete obj[index].reacts[reaction] } } } else { if (!obj[index].reacts[reaction]) { obj[index].reacts[reaction] = [uid] } else { if (!obj[index].reacts[reaction].includes(uid)) { obj[index].reacts[reaction].push(uid) } } } } else { const i = path.pop() addReaction(obj[i].subComments, path, { reaction: reaction, isDelete: isDelete, uid: uid, }) } } function getForumData( forumName: string, forumDir: string ): { forumPath: string; contentFilePath: string; contents: ForumContents[] } { const safeForumName = forumName.replace(/\./g, '').replace(/\/+/g, '') const forumPath = forumDir + '/' + safeForumName const contentFilePath = forumPath + '/' + forumContentsFileName if (!utils.FileExists(forumPath)) { utils.CreatePath(forumPath, true) } if (!utils.FileExists(contentFilePath)) { utils.WriteFile('{}', contentFilePath) } const contents: ForumContents[] = utils.ReadJSON(contentFilePath) return { forumPath: forumPath, contentFilePath: contentFilePath, contents: contents, } } function getPostData( postKey: string, contents: ForumContents[], forumPath: string ): { postData: ForumEntry postPath: string } { const safePostKey = postKey.replace(/\./g, '').replace(/\/+/g, '') const postPath = forumPath + '/' + safePostKey if (!contents[postKey] || !utils.FileExists(postPath)) { return null } return { postData: utils.ReadJSON(postPath), postPath: postPath } } function setup(data: SubmoduleData): void { const { app, publicdirs } = data const publicDir = publicdirs[0] const forumDir = publicDir + 'forum' const forumFiles = publicDir + 'forumFiles' if (!utils.FileExists(forumDir)) { utils.CreatePath(forumDir, true) } app.get('/forumEntries', (req: Request, res) => { logger.LogReq(req) const forumName: string = req.query.forumName if (!forumName) { res.json({ success: false, msg: `Forum name is not specified!`, }) return } const { forumPath, contents } = getForumData(forumName, forumDir) const from = req.query.from || Object.keys(contents).reverse()[0] const count = parseInt( req.query.count ? req.query.count.toString() : '5' ) const getContent = req.query.getContent let entries = {} let i = 0 let passed = false let lastKey = undefined Object.keys(contents) .reverse() .some((key) => { const entry = getContent ? utils.ReadJSON(forumPath + '/' + key) : contents[key] if (key === from) { passed = true } if (i < count && passed) { entries = { ...entries, [key]: entry, } } if (i === count) { lastKey = key return true } if (passed) { i++ } return false }) res.json({ entries: entries, nextKey: lastKey, }) }) app.get('/forumEntry', (req: Request, res) => { logger.LogReq(req) const forumName: string = req.query.forumName const postKey: string = req.query.postKey if (!forumName || !postKey) { res.json({ success: false, msg: `ForumName or postKey is not specified!`, }) return } const { forumPath } = getForumData(forumName, forumDir) res.json({ entry: utils.ReadJSON( forumPath + '/' + postKey.replace(/\./g, '').replace(/\/+/g, '') ), success: true, }) }) app.get('/forumRanklist', (req: Request, res) => { const forumName: string = req.query.forumName if (!forumName) { res.json({ success: false, msg: 'forumName required' }) return } const { forumPath, contents } = getForumData(forumName, forumDir) const forumEntries = Object.keys(contents).map((key) => { const entry = utils.ReadJSON(forumPath + '/' + key) return entry }) const leaderBoard = forumEntries.reduce((acc, forumEntry) => { const { user, reacts } = forumEntry const ups = reacts?.['thumbs up']?.length || 0 const downs = reacts?.['thumbs down']?.length || 0 if (!acc[user]) { acc[user] = { up: ups, down: downs, } } else { acc[user] = { up: acc[user].up + ups, down: acc[user].down + downs, } } return acc }, {}) res.json({ success: true, leaderBoard: Object.keys(leaderBoard) .map((key) => { const val = leaderBoard[key] return { ...val, user: key, sum: val.up - val.down, } }) .sort((a, b) => { return b.sum - a.sum }), }) }) app.post('/postMeme', (req: Request, res) => { utils .uploadFile(req, forumFiles) .then(() => { res.json({ success: true }) }) .catch(() => { res.json({ success: false, msg: 'error during uploading' }) return }) }) app.post( '/addPost', ( req: Request<{ forumName: string content: string title: string image?: string }>, res ) => { logger.LogReq(req) const { title, content, forumName, image } = req.body if (!forumName) { res.json({ success: false, msg: `Forum name is not specified!`, }) return } const { forumPath, contents, contentFilePath } = getForumData( forumName, forumDir ) const user: User = req.session.user const admins: string[] = utils.FileExists(adminUsersFile) ? utils.ReadJSON(adminUsersFile) : [] const newPostKey = uuidv4() const postData = { date: utils.GetDateString(), user: user.id, title: title, admin: admins.includes(user.id.toString()), commentCount: 0, mediaPath: image, } contents[newPostKey] = postData utils.WriteFile(JSON.stringify(contents, null, 2), contentFilePath) utils.WriteFile( JSON.stringify({ ...postData, content: content }, null, 2), forumPath + '/' + newPostKey ) res.json({ success: true, newPostKey: newPostKey, newEntry: { ...postData, content: content }, }) } ) app.post( '/rmPost', ( req: Request<{ postKey: string forumName: string }>, res ) => { logger.LogReq(req) const { postKey } = req.body const forumName = req.body.forumName if (!forumName) { res.json({ success: false, msg: `Forum name is not specified!`, }) return } const { forumPath, contentFilePath, contents } = getForumData( forumName, forumDir ) const user: User = req.session.user if (contents[postKey] && contents[postKey].user === user.id) { utils.deleteFile(forumPath + '/' + postKey) delete contents[postKey] utils.WriteFile( JSON.stringify(contents, null, 2), contentFilePath ) res.json({ success: true, deletedId: postKey }) } else { res.json({ success: false, msg: 'Entry does not exists, or you are not authorized to delete this post', }) return } } ) app.post( '/comment', ( req: Request<{ type: string path: number[] postKey: string forumName: string content: string }>, res ) => { logger.LogReq(req) const forumName = req.body.forumName if (!forumName) { res.json({ success: false, msg: `Forum name is not specified!`, }) return } const { forumPath, contentFilePath, contents } = getForumData( forumName, forumDir ) const user: User = req.session.user const admins: string[] = utils.FileExists(adminUsersFile) ? utils.ReadJSON(adminUsersFile) : [] const { type, path, postKey } = req.body if (!postKey || !type || !path) { res.json({ success: false, msg: 'type or path or postKey is undefined', }) return } const { postPath, postData } = getPostData( postKey, contents, forumPath ) if (!postData) { res.json({ success: false, msg: `Post entry '${postKey}' from forum '${forumName}' does not exits!`, }) } if (type === 'add') { const { content } = req.body const comment = { date: utils.GetDateString(), user: user.id, content: content, admin: admins.includes(user.id.toString()), } if (!postData.comments) { postData.comments = [] } addComment(postData.comments, path, comment) } else if (type === 'delete') { if (postData.comments) { const success = deleteComment( postData.comments, path, user.id ) if (!success) { res.json({ success: false, msg: 'you cant delete other users comments', postData: postData, }) return } } } else { res.json({ success: false, msg: 'no such type' }) return } contents[postKey].commentCount = countComments(postData.comments) utils.WriteFile(JSON.stringify(postData, null, 2), postPath) utils.WriteFile(JSON.stringify(contents, null, 2), contentFilePath) res.json({ success: true, postKey: postKey, postData: postData }) } ) app.post( '/react', ( req: Request<{ forumName: string postKey: string path: number[] reaction: string isDelete: boolean }>, res ) => { logger.LogReq(req) const forumName = req.body.forumName if (!forumName) { res.json({ success: false, msg: `Forum name is not specified!`, }) return } const { forumPath, contents } = getForumData(forumName, forumDir) const user: User = req.session.user const { postKey, path, reaction, isDelete } = req.body if (!postKey || !reaction) { res.json({ success: false, msg: 'no postkey or reaction' }) return } const { postPath, postData } = getPostData( postKey, contents, forumPath ) if (!postData) { res.json({ success: false, msg: `Post entry '${postKey}' from forum '${forumName}' does not exits!`, }) } if (!path || path.length === 0) { if (postData) { if (isDelete) { if (postData.reacts) { postData.reacts[reaction] = postData.reacts[ reaction ].filter((uid) => { return uid !== user.id }) if (postData.reacts[reaction].length === 0) { delete postData.reacts[reaction] } } } else { if (!postData.reacts) { postData.reacts = { [reaction]: [user.id] } } else { if (Array.isArray(postData.reacts[reaction])) { if ( !postData.reacts[reaction].includes(user.id) ) { postData.reacts[reaction].push(user.id) } } else { postData.reacts[reaction] = [user.id] } } } } } else { addReaction(postData.comments, path, { reaction: reaction, isDelete: isDelete, uid: user.id, }) } utils.WriteFile(JSON.stringify(postData, null, 2), postPath) res.json({ success: true, postData: postData }) } ) } export default { setup: setup, }