mirror of
https://gitlab.com/MrFry/mrfrys-node-server
synced 2025-04-01 20:24:18 +02:00
615 lines
18 KiB
TypeScript
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,
|
|
}
|