From 30185ce004a61ae6bdcbe088f5de012cff756da5 Mon Sep 17 00:00:00 2001 From: JandereDev Date: Mon, 14 Mar 2022 10:00:10 +0100 Subject: [PATCH] add ability to create and delete antispam rules --- api/package.json | 1 + api/src/routes/dash/server-automod.ts | 82 ++++++++++++++++++++++++++- api/src/routes/dash/server.ts | 4 +- api/src/utils.ts | 47 ++++++++++++++- api/yarn.lock | 5 ++ web/src/pages/ServerDashboard.tsx | 37 +++++++++++- 6 files changed, 171 insertions(+), 5 deletions(-) diff --git a/api/package.json b/api/package.json index 87ec9b3..072d65a 100644 --- a/api/package.json +++ b/api/package.json @@ -20,6 +20,7 @@ "express": "^4.17.2", "log75": "^2.2.0", "monk": "^7.3.4", + "ulid": "^2.3.0", "ws": "^8.4.2" }, "devDependencies": { diff --git a/api/src/routes/dash/server-automod.ts b/api/src/routes/dash/server-automod.ts index c0d7cde..b9386f0 100644 --- a/api/src/routes/dash/server-automod.ts +++ b/api/src/routes/dash/server-automod.ts @@ -1,8 +1,9 @@ import { app, db } from '../..'; import { Request, Response } from 'express'; -import { badRequest, isAuthenticated, requireAuth, unauthorized } from '../../utils'; +import { badRequest, ensureObjectStructure, isAuthenticated, requireAuth, unauthorized } from '../../utils'; import { botReq } from '../internal/ws'; import { FindOneResult } from 'monk'; +import { ulid } from 'ulid'; type AntispamRule = { id: string; @@ -80,3 +81,82 @@ app.patch('/dash/server/:server/automod/:ruleid', requireAuth({ permission: 2 }) return res.send({ success: true }); }); + +app.post('/dash/server/:server/automod', requireAuth({ permission: 2 }), async (req, res) => { + const user = await isAuthenticated(req, res, true); + if (!user) return; + + const { server } = req.params; + if (!server || typeof server != 'string') return badRequest(res); + + const response = await botReq('getUserServerDetails', { user, server }); + if (!response.success) { + return res.status(response.statusCode ?? 500).send({ error: response.error }); + } + + if (!response.server) return res.status(404).send({ error: 'Server not found' }); + + let rule: any; + try { + rule = ensureObjectStructure(req.body, { + max_msg: 'number', + timeframe: 'number', + action: 'number', + message: 'string', + }, true); + } catch(e) { return res.status(400).send(e) } + + if (rule.action != null && rule.action < 0 || rule.action > 4) return res.status(400).send('Invalid action'); + + const id = ulid(); + + await db.get('servers').update({ + id: server, + }, { + $push: { + "automodSettings.spam": { + id: id, + max_msg: rule.max_msg ?? 5, + timeframe: rule.timeframe ?? 3, + action: rule.action ?? 0, + message: rule.message ?? null, + } + } + }); + + res.status(200).send({ success: true, id: id }); +}); + +app.delete('/dash/server/:server/automod/:ruleid', requireAuth({ permission: 2 }), async (req, res) => { + const user = await isAuthenticated(req, res, true); + if (!user) return; + + const { server, ruleid } = req.params; + if (!server || typeof server != 'string' || !ruleid || typeof ruleid != 'string') return badRequest(res); + + const response = await botReq('getUserServerDetails', { user, server }); + if (!response.success) { + return res.status(response.statusCode ?? 500).send({ error: response.error }); + } + + if (!response.server) return res.status(404).send({ error: 'Server not found' }); + + // todo: fix this shit idk if it works + let queryRes; + try { + queryRes = await db.get('servers').update({ + id: server + }, { + $pull: { + "automodSettings.spam": { id: ruleid } + } + }); + } catch(e) { + console.error(e); + res.status(500).send({ error: e }); + return; + } + + if (queryRes.nModified > 0) res.status(200).send({ success: true }); + else res.status(404).send({ success: false, error: 'Rule not found' }); +}); diff --git a/api/src/routes/dash/server.ts b/api/src/routes/dash/server.ts index bd144ab..89e4c90 100644 --- a/api/src/routes/dash/server.ts +++ b/api/src/routes/dash/server.ts @@ -144,7 +144,7 @@ app.put('/dash/server/:server/:option', async (req: Request, res: Response) => { } }); -app.delete('/dash/server/:server/:option/:target', async (req: Request, res: Response) => { +app.delete('/dash/server/:server/:option/:target', async (req: Request, res: Response, next) => { const user = await isAuthenticated(req, res, true); if (!user) return unauthorized(res); @@ -190,6 +190,6 @@ app.delete('/dash/server/:server/:option/:target', async (req: Request, res: Res }); return; } - default: return badRequest(res); + default: next(); } }); diff --git a/api/src/utils.ts b/api/src/utils.ts index 98228b2..1199a28 100644 --- a/api/src/utils.ts +++ b/api/src/utils.ts @@ -69,4 +69,49 @@ function requireAuth(config: RequireAuthConfig): (req: Request, res: Response, n } } -export { isAuthenticated, getSessionInfo, badRequest, unauthorized, getPermissionLevel, requireAuth } +/** + * Strips the input object of unwanted fields and + * throws if a value has the wrong type + * @param obj + * @param structure + */ +function ensureObjectStructure(obj: any, structure: { [key: string]: 'string'|'number'|'float'|'strarray' }, allowEmpty?: boolean): any { + const returnObj: any = {} + + for (const key of Object.keys(obj)) { + const type = obj[key] == null ? 'null' : typeof obj[key]; + + if (allowEmpty && (type == 'undefined' || type == 'null')) continue; + + switch(structure[key]) { + case 'string': + case 'number': + case 'float': + if (type != structure[key]) throw `Property '${key}' was expected to be of type '${structure[key]}', got '${type}' instead`; + + if (structure[key] == 'number' && `${Math.round(obj[key])}` != `${obj[key]}`) + throw `Property '${key}' was expected to be of type '${structure[key]}', got 'float' instead`; + + returnObj[key] = obj[key]; + break; + case 'strarray': + if (!(obj[key] instanceof Array)) { + throw `Property '${key}' was expected to be of type 'string[]', got '${type}' instead`; + } + + for (const i in obj[key]) { + const item = obj[key][i]; + if (typeof item != 'string') throw `Property '${key}' was expected to be of type 'string[]', ` + + `found '${typeof item}' at index ${i}`; + } + + returnObj[key] = obj[key]; + break; + default: continue; + } + } + + return returnObj; +} + +export { isAuthenticated, getSessionInfo, badRequest, unauthorized, getPermissionLevel, requireAuth, ensureObjectStructure } diff --git a/api/yarn.lock b/api/yarn.lock index fb6ac4c..f32a48e 100644 --- a/api/yarn.lock +++ b/api/yarn.lock @@ -639,6 +639,11 @@ typescript@^4.5.5: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3" integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA== +ulid@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/ulid/-/ulid-2.3.0.tgz#93063522771a9774121a84d126ecd3eb9804071f" + integrity sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw== + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" diff --git a/web/src/pages/ServerDashboard.tsx b/web/src/pages/ServerDashboard.tsx index e4b9b8c..3f7c8b4 100644 --- a/web/src/pages/ServerDashboard.tsx +++ b/web/src/pages/ServerDashboard.tsx @@ -8,7 +8,6 @@ import { LineDivider } from '@revoltchat/ui/lib/components/atoms/layout/LineDivi import { H1 } from '@revoltchat/ui/lib/components/atoms/heading/H1'; import { H3 } from '@revoltchat/ui/lib/components/atoms/heading/H3'; import { H4 } from '@revoltchat/ui/lib/components/atoms/heading/H4'; -import { H5 } from '@revoltchat/ui/lib/components/atoms/heading/H5'; import { Icon } from '@mdi/react'; import { mdiCloseBox } from '@mdi/js'; import { API_URL } from "../App"; @@ -187,6 +186,34 @@ const ServerDashboard: FunctionComponent = () => { ? ( <> {automodSettings.antispam.map(r => )} + ) : ( @@ -441,6 +468,13 @@ const ServerDashboard: FunctionComponent = () => { setChannelsChanged(false); }, []); + const remove = useCallback(async () => { + if (confirm(`Do you want to irreversably delete rule ${props.rule.id}?`)) { + await axios.delete(`${API_URL}/dash/server/${serverid}/automod/${props.rule.id}`, { headers: await getAuthHeaders() }); + setAutomodSettings({ antispam: automodSettings!.antispam.filter(r => r.id != props.rule.id) }); + } + }, []); + const inputStyle: React.CSSProperties = { maxWidth: '100px', margin: '8px 8px 0px 8px', @@ -560,6 +594,7 @@ const ServerDashboard: FunctionComponent = () => { > +