mrfrys-node-server/src/modules/api/submodules/forum.ts
2023-04-11 14:59:26 +02:00

615 lines
18 KiB
TypeScript

/* ----------------------------------------------------------------------------
Question Server
GitLab: <https://gitlab.com/MrFry/mrfrys-node-server>
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 <https://www.gnu.org/licenses/>.
------------------------------------------------------------------------- */
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,
}