add/remove bot managers and mods from dashboard

This commit is contained in:
JandereDev 2022-01-25 19:44:39 +01:00
parent 4675ed8bdd
commit 0e4667a298
Signed by: Lea
GPG key ID: 5D5E18ACB990F57A
13 changed files with 432 additions and 20 deletions

View file

@ -3,6 +3,7 @@ import { app, logger } from "..";
app.use('*', (req: Request, res: Response, next: () => void) => {
res.header('Access-Control-Allow-Origin', '*');
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, x-auth-user, x-auth-token");
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, x-auth-user, x-auth-token');
res.header('Access-Control-Allow-Methods', '*');
next();
});

View file

@ -1,16 +1,19 @@
import { app } from '../..';
import { app, db } from '../..';
import { Request, Response } from 'express';
import { badRequest, isAuthenticated, unauthorized } from '../../utils';
import { badRequest, getPermissionLevel, isAuthenticated, unauthorized } from '../../utils';
import { botReq } from '../internal/ws';
type User = { id: string, username?: string, avatarURL?: string }
type ServerDetails = {
id: string,
perms: 0|1|2,
perms: 0|1|2|3,
name: string,
description?: string,
iconURL?: string,
bannerURL?: string,
serverConfig: any,
users: User[],
}
app.get('/dash/server/:server', async (req: Request, res: Response) => {
@ -30,3 +33,122 @@ app.get('/dash/server/:server', async (req: Request, res: Response) => {
const s: ServerDetails = response.server;
res.send({ server: s });
});
app.put('/dash/server/:server/:option', async (req: Request, res: Response) => {
try {
const user = await isAuthenticated(req, res, true);
if (!user) return unauthorized(res);
const { server } = req.params;
const { item } = req.body;
if (!server || typeof server != 'string' || !item || typeof item != 'string') return badRequest(res);
const permissionLevelRes = await getPermissionLevel(user, server);
if (!permissionLevelRes.success)
return res.status(permissionLevelRes.statusCode || 500).send({ error: permissionLevelRes.error });
const servers = db.get('servers');
const permissionLevel: 0|1|2|3 = permissionLevelRes.level;
const settings = await servers.findOne({ id: server });
switch(req.params.option) {
case 'managers': {
if (permissionLevel < 3) return res.status(403).send({ error: 'You are not allowed to add other bot managers.' });
const userRes = await botReq('getUser', { user: item });
if (!userRes.success) {
return res.status(404).send({ error: 'User could not be found' });
}
if (settings.botManagers?.includes(userRes.user.id) === true) {
return res.status(400).send({ error: 'This user is already manager' });
}
const newManagers = [ ...(settings.botManagers ?? []), userRes.user.id ];
await servers.update({ id: server }, { $set: { botManagers: newManagers } });
res.send({
success: true,
managers: newManagers,
users: [ userRes.user ],
});
return;
}
case 'mods': {
if (permissionLevel < 2) return res.status(403).send({ error: 'You are not allowed to add other moderators.' });
const userRes = await botReq('getUser', { user: item });
if (!userRes.success) {
return res.status(404).send({ error: 'User could not be found' });
}
if (settings.moderators?.includes(userRes.user.id) === true) {
return res.status(400).send({ error: 'This user is already moderator' });
}
const newMods = [ ...(settings.moderators ?? []), userRes.user.id ];
await servers.update({ id: server }, { $set: { moderators: newMods } });
res.send({
success: true,
mods: newMods,
users: [ userRes.user ],
});
return;
}
default: return badRequest(res);
}
} catch(e: any) {
res.status(500).send({ error: e });
}
});
app.delete('/dash/server/:server/:option/:target', async (req: Request, res: Response) => {
const user = await isAuthenticated(req, res, true);
if (!user) return unauthorized(res);
const { server, target, option } = req.params;
if (!server || typeof server != 'string' || !target || typeof target != 'string') return badRequest(res);
const permissionLevelRes = await getPermissionLevel(user, server);
if (!permissionLevelRes.success)
return res.status(permissionLevelRes.statusCode || 500).send({ error: permissionLevelRes.error });
const servers = db.get('servers');
const permissionLevel: 0|1|2|3 = permissionLevelRes.level;
const settings = await servers.findOne({ id: server });
switch(option) {
case 'managers': {
if (permissionLevel < 3) return res.status(403).send({ error: 'You are not allowed to remove bot managers.' });
if (!settings.botManagers?.includes(target)) {
return res.status(400).send({ error: 'This user is not manager' });
}
const newManagers = (settings.botManagers ?? []).filter((i: string) => i != target);
await servers.update({ id: server }, { $set: { botManagers: newManagers } });
res.send({
success: true,
managers: newManagers,
});
return;
}
case 'mods': {
if (permissionLevel < 2) return res.status(403).send({ error: 'You are not allowed to remove moderators.' });
if (!settings.moderators?.includes(target)) {
return res.status(400).send({ error: 'This user is not moderator' });
}
const newMods = (settings.moderators ?? []).filter((i: string) => i != target);
await servers.update({ id: server }, { $set: { moderators: newMods } });
res.send({
success: true,
mods: newMods,
});
return;
}
default: return badRequest(res);
}
});

View file

@ -3,7 +3,7 @@ import { Request, Response } from 'express';
import { isAuthenticated, unauthorized } from '../../utils';
import { botReq } from '../internal/ws';
type Server = { id: string, perms: 0|1|2, name: string, iconURL?: string, bannerURL?: string }
type Server = { id: string, perms: 0|1|2|3, name: string, iconURL?: string, bannerURL?: string }
app.get('/dash/servers', async (req: Request, res: Response) => {
const user = await isAuthenticated(req, res, true);

View file

@ -1,6 +1,7 @@
import { Request, Response } from "express";
import { FindOneResult } from "monk";
import { db } from ".";
import { botReq } from "./routes/internal/ws";
class Session {
user: string;
@ -44,4 +45,8 @@ function unauthorized(res: Response) {
res.status(401).send(JSON.stringify({ "error": "Unauthorized" }, null, 4));
}
export { isAuthenticated, getSessionInfo, badRequest, unauthorized }
async function getPermissionLevel(user: string, server: string) {
return await botReq('getPermissionLevel', { user, server });
}
export { isAuthenticated, getSessionInfo, badRequest, unauthorized, getPermissionLevel }

View file

@ -1,21 +1,22 @@
import { Member } from "revolt.js/dist/maps/Members";
import { User } from "revolt.js/dist/maps/Users";
import { client } from "../../..";
import AutomodSettings from "../../../struct/antispam/AutomodSettings";
import ServerConfig from "../../../struct/ServerConfig";
import { getPermissionLevel } from "../../util";
import { wsEvents, WSResponse } from "../api_communication";
type ReqData = { user: string, server: string }
type APIUser = { id: string, username?: string, avatarURL?: string }
type ServerDetails = {
id: string,
perms: 0|1|2,
perms: 0|1|2|3,
name: string,
description?: string,
iconURL?: string,
bannerURL?: string,
serverConfig?: ServerConfig,
users: APIUser[],
}
wsEvents.on('req:getUserServerDetails', async (data: ReqData, cb: (data: WSResponse) => void) => {
@ -43,6 +44,20 @@ wsEvents.on('req:getUserServerDetails', async (data: ReqData, cb: (data: WSRespo
// todo: remove unwanted keys from server config
async function fetchUser(id: string) {
try {
return client.users.get(id) || await client.users.fetch(id);
} catch(e) {
throw id; // this is stupid but idc
}
}
const users = await Promise.allSettled([
...(serverConfig.botManagers?.map(u => fetchUser(u)) ?? []),
...(serverConfig.moderators?.map(u => fetchUser(u)) ?? []),
fetchUser(user._id),
]);
const response: ServerDetails = {
id: server._id,
name: server.name,
@ -51,6 +66,11 @@ wsEvents.on('req:getUserServerDetails', async (data: ReqData, cb: (data: WSRespo
bannerURL: server.generateBannerURL(),
iconURL: server.generateIconURL(),
serverConfig,
users: users.map(
u => u.status == 'fulfilled'
? { id: u.value._id, avatarURL: u.value.generateAvatarURL(), username: u.value.username }
: { id: u.reason }
),
}
cb({ success: true, server: response });
@ -59,3 +79,5 @@ wsEvents.on('req:getUserServerDetails', async (data: ReqData, cb: (data: WSRespo
cb({ success: false, error: `${e}` });
}
});
export { APIUser }

View file

@ -17,7 +17,7 @@ wsEvents.on('req:getUserServers', async (data: ReqData, cb: (data: WSResponse) =
const mutuals = await user.fetchMutual();
type ServerResponse = { id: string, perms: 0|1|2, name: string, iconURL?: string, bannerURL?: string }
type ServerResponse = { id: string, perms: 0|1|2|3, name: string, iconURL?: string, bannerURL?: string }
const promises: Promise<ServerResponse>[] = [];

View file

@ -0,0 +1,38 @@
import { User } from "revolt.js/dist/maps/Users";
import { client } from "../../..";
import { getPermissionLevel, parseUser } from "../../util";
import { wsEvents, WSResponse } from "../api_communication";
import { APIUser } from "./server_details";
wsEvents.on('req:getPermissionLevel', async (data: { user: string, server: string }, cb: (data: WSResponse) => void) => {
try {
const server = client.servers.get(data.server);
if (!server) return cb({ success: false, error: 'The requested server could not be found', statusCode: 404 });
let user: User;
try {
user = client.users.get(data.user) || await client.users.fetch(data.user);
} catch(e) {
cb({ success: false, error: 'The requested user could not be found', statusCode: 404 });
return;
}
return cb({ success: true, level: await getPermissionLevel(user, server) })
} catch(e) {
console.error(e);
cb({ success: false, error: `${e}` });
}
});
wsEvents.on('req:getUser', async (data: { user: string }, cb: (data: WSResponse) => void) => {
try {
const user = await parseUser(data.user);
if (!user)
cb({ success: false, statusCode: 404, error: 'User could not be found' });
else
cb({ success: true, user: { id: user._id, username: user.username, avatarURL: user.generateAvatarURL() } as APIUser });
} catch(e) {
console.error(e);
cb({ success: false, error: `${e}` });
}
});

View file

@ -135,3 +135,4 @@ export { wsEvents, wsSend, WSResponse }
import('./api/servers');
import('./api/server_details');
import('./api/users');

View file

@ -99,13 +99,13 @@ async function checkSudoPermission(message: Message): Promise<boolean> {
return true;
}
}
async function getPermissionLevel(user: User|Member, server: Server): Promise<0|1|2> {
async function getPermissionLevel(user: User|Member, server: Server): Promise<0|1|2|3> {
if (isSudo(user instanceof User ? user : (user.user || await client.users.fetch(user._id.user)))) return 2;
const member = user instanceof User ? await server.fetchMember(user) : user;
if (user instanceof Member) user = user.user!;
if (hasPerm(member, 'ManageServer')) return 2;
if (hasPerm(member, 'ManageServer')) return 3;
if (hasPerm(member, 'KickMembers')) return 1;
const config = (await client.db.get('servers').findOne({ id: server._id }) || {}) as ServerConfig;

View file

@ -7,6 +7,8 @@
"preview": "vite preview"
},
"dependencies": {
"@mdi/js": "^6.5.95",
"@mdi/react": "^1.5.0",
"@revoltchat/ui": "^1.0.24",
"@types/axios": "^0.14.0",
"@types/core-js": "^2.5.5",
@ -14,6 +16,7 @@
"axios": "^0.25.0",
"core-js": "^3.20.3",
"localforage": "^1.10.0",
"prop-types": "^15.8.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router-dom": "^6.2.1",

View file

@ -7,7 +7,7 @@ import { H2 } from '@revoltchat/ui/lib/components/atoms/heading/H2';
import { API_URL } from "../App";
import { getAuthHeaders } from "../utils";
type Server = { id: string, perms: 0|1|2, name: string, iconURL?: string, bannerURL?: string }
type Server = { id: string, perms: 0|1|2|3, name: string, iconURL?: string, bannerURL?: string }
function permissionName(p: number) {
switch(p) {

View file

@ -1,17 +1,30 @@
import localforage from "localforage";
import axios from 'axios';
import { FunctionComponent, useCallback, useEffect, useState } from "react";
import { Button } from '@revoltchat/ui/lib/components/atoms/inputs/Button';
import { InputBox } from '@revoltchat/ui/lib/components/atoms/inputs/InputBox';
import { Checkbox } from '@revoltchat/ui/lib/components/atoms/inputs/Checkbox';
import { LineDivider } from '@revoltchat/ui/lib/components/atoms/layout/LineDivider';
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 { Icon } from '@mdi/react';
import { mdiCloseBox } from '@mdi/js';
import { API_URL } from "../App";
import { getAuthHeaders } from "../utils";
import { useParams } from "react-router-dom";
type Server = { id?: string, perms?: 0|1|2, name?: string, description?: string, iconURL?: string, bannerURL?: string, serverConfig?: any }
type User = { id: string, username?: string, avatarURL?: string }
type Server = {
id?: string,
perms?: 0|1|2|3,
name?: string,
description?: string,
iconURL?: string,
bannerURL?: string,
serverConfig?: { [key: string]: any },
users: User[],
}
const ServerDashboard: FunctionComponent = () => {
const [serverInfo, setServerInfo] = useState({} as Server);
@ -19,6 +32,9 @@ const ServerDashboard: FunctionComponent = () => {
const [prefix, setPrefix] = useState('' as string|undefined);
const [prefixAllowSpace, setPrefixAllowSpace] = useState(false);
const [botManagers, setBotManagers] = useState([] as string[]);
const [moderators, setModerators] = useState([] as string[]);
const { serverid } = useParams();
@ -30,11 +46,15 @@ const ServerDashboard: FunctionComponent = () => {
try {
const res = await axios.get(`${API_URL}/dash/server/${serverid}`, { headers: await getAuthHeaders() });
console.log(res.data);
const server: Server = res.data.server;
setServerInfo(server);
setPrefix(server.serverConfig?.prefix || undefined);
setPrefix(server.serverConfig?.prefix || '');
setPrefixAllowSpace(!!server.serverConfig?.spaceAfterPrefix);
setBotManagers(server.serverConfig?.botManagers ?? []);
setModerators(server.serverConfig?.moderators ?? []);
} catch(e: any) {
console.error(e);
setStatus(`${e?.message ?? e}`);
@ -50,7 +70,7 @@ const ServerDashboard: FunctionComponent = () => {
<div hidden={Object.keys(serverInfo).length == 0}>
<H4>{serverInfo.description ?? <i>No server description set</i>}</H4>
<br/>
<div style={{ paddingLeft: '10px' }}>
<div style={{ paddingLeft: '10px', paddingRight: '10px' }}>
<H3>Prefix</H3>
<InputBox
style={{ width: '150px', }}
@ -61,7 +81,7 @@ const ServerDashboard: FunctionComponent = () => {
}}
/>
<Checkbox
style={{ width: '400px' }}
style={{ maxWidth: '400px' }}
value={prefixAllowSpace}
onChange={() => {
setPrefixAllowSpace(!prefixAllowSpace);
@ -73,10 +93,191 @@ const ServerDashboard: FunctionComponent = () => {
style={{ marginTop: "16px" }}
onClick={saveConfig}
>Save</Button>
<LineDivider />
<H3>Bot Managers</H3>
<H4>
Only users with "Manage Server" permission are allowed to add/remove other
bot managers and are automatically considered bot manager.
</H4>
<UserListTypeContainer>
<UserListContainer disabled={(serverInfo.perms ?? 0) < 3}>
{botManagers.map((uid: string) => {
const user = serverInfo.users.find(u => u.id == uid) || { id: uid }
return (
<UserListEntry type='MANAGER' user={user} key={uid} />
)})}
<UserListAddField type='MANAGER' />
</UserListContainer>
</UserListTypeContainer>
<H3>Moderators</H3>
<H4>
Only bot managers are allowed to add/remove moderators.
All bot managers are also moderators.
</H4>
<UserListTypeContainer>
<UserListContainer disabled={(serverInfo.perms ?? 0) < 2}>
{moderators.map((uid: string) => {
const user = serverInfo.users.find(u => u.id == uid) || { id: uid }
return (
<UserListEntry type='MOD' user={user} key={uid} />
)})}
<UserListAddField type='MOD' />
</UserListContainer>
</UserListTypeContainer>
</div>
</div>
</>
);
}
function UserListEntry(props: { user: User, type: 'MANAGER'|'MOD' }) {
return (
<div
key={props.user.id}
style={{
display: 'block',
margin: '4px 6px',
padding: '4px',
backgroundColor: 'var(--tertiary-background)',
borderRadius: '5px',
}}
>
<img
src={props.user.avatarURL ?? 'https://amogus.org/amogus.png'}
width={28}
height={28}
style={{
borderRadius: '50%',
verticalAlign: 'middle',
display: 'inline-block',
}}
/>
<span
style={{
color: 'var(--foreground)',
fontSize: '20px',
paddingLeft: '6px',
marginBottom: '2px',
verticalAlign: 'middle',
display: 'inline-block',
}}
>{props.user.username ?? 'Unknown'}</span>
<div
style={{
marginLeft: '4px',
verticalAlign: 'middle',
display: 'inline-block',
height: '30px',
}}
onClick={async () => {
const res = await axios.delete(
`${API_URL}/dash/server/${serverid}/${props.type == 'MANAGER' ? 'managers' : 'mods'}/${props.user.id}`,
{ headers: await getAuthHeaders() }
);
if (props.type == 'MANAGER') {
setBotManagers(res.data.managers);
}
else if (props.type == 'MOD') {
setModerators(res.data.mods);
}
}}
>
<Icon // todo: hover effect
path={mdiCloseBox}
color='var(--tertiary-foreground)'
size='30px'
/>
</div>
</div>
);
}
function UserListContainer(props: { disabled: boolean, children: any }) {
return (
<div
style={{
display: 'flex',
flexWrap: 'wrap',
...(props.disabled ? {
filter: 'grayscale(100%)',
pointerEvents: 'none',
} : {})
}}
>
{props.children}
</div>
);
}
function UserListTypeContainer(props: any) {
return (
<div
style={{
display: 'flex',
backgroundColor: 'var(--secondary-background)',
borderRadius: '10px',
marginTop: '15px',
paddingTop: '5px',
paddingBottom: '5px',
}}
>{props.children}</div>
);
}
function UserListAddField(props: { type: 'MANAGER'|'MOD' }) {
const [content, setContent] = useState('');
const onConfirm = useCallback(async () => {
if (content.length) {
const res = await axios.put(
`${API_URL}/dash/server/${serverid}/${props.type == 'MANAGER' ? 'managers' : 'mods'}`,
{ item: content },
{ headers: await getAuthHeaders() }
);
if (res.data.users?.length) {
res.data.users.forEach((user: User) => {
if (!serverInfo.users.find(u => u.id == user.id)) serverInfo.users.push(user);
});
}
if (props.type == 'MANAGER') {
setBotManagers(res.data.managers);
}
else if (props.type == 'MOD') {
setModerators(res.data.mods);
}
}
}, [content]);
return (
<div>
<InputBox
placeholder={`Add a ${props.type == 'MANAGER' ? 'bot manager' : 'moderator'}...`}
value={content}
onChange={e => setContent(e.currentTarget.value)}
style={{
float: 'left',
width: '180px',
height: '38px',
margin: '4px 8px',
}}
onKeyDown={e => e.key == 'Enter' && onConfirm()}
/>
<Button
style={{
float: 'left',
width: '40px',
height: '38px',
margin: '4px 8px',
}}
onClick={onConfirm}
>Ok</Button>
</div>
);
}
}
export default ServerDashboard;

View file

@ -265,6 +265,16 @@
resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed"
integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==
"@mdi/js@^6.5.95":
version "6.5.95"
resolved "https://registry.yarnpkg.com/@mdi/js/-/js-6.5.95.tgz#2d895b013408f213252b77c30e0fdaaba6dc8b4b"
integrity sha512-x/bwEoAGP+Mo10Dfk5audNIPi7Yz8ZBrILcbXLW3ShOI/njpgodzpgpC2WYK3D2ZSC392peRRemIFb/JsyzzYQ==
"@mdi/react@^1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@mdi/react/-/react-1.5.0.tgz#461d2064ba12d509723bffc95e2f7169a6ac884a"
integrity sha512-NztRgUxSYD+ImaKN94Tg66VVVqXj4SmlDGzZoz48H9riJ+Awha56sfXH2fegw819NWo7KI3oeS1Es0lNQqwr0w==
"@revoltchat/ui@^1.0.24":
version "1.0.24"
resolved "https://registry.yarnpkg.com/@revoltchat/ui/-/ui-1.0.24.tgz#ce55cc225ec92eb07dd5865b9255d1f160d06ce2"
@ -719,7 +729,7 @@ lodash@^4.17.11:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
loose-envify@^1.1.0:
loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@ -780,6 +790,15 @@ postcss@^8.4.5:
picocolors "^1.0.0"
source-map-js "^1.0.1"
prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
dependencies:
loose-envify "^1.4.0"
object-assign "^4.1.1"
react-is "^16.13.1"
react-dom@^17.0.2:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
@ -789,7 +808,7 @@ react-dom@^17.0.2:
object-assign "^4.1.1"
scheduler "^0.20.2"
react-is@^16.7.0:
react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==