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 =>