1
0
mirror of https://github.com/spacebarchat/server.git synced 2024-11-25 03:33:33 +01:00
This commit is contained in:
Madeline 2022-09-26 22:29:30 +10:00
parent fcc0884e36
commit 99ee7e9400
280 changed files with 6800 additions and 3908 deletions

View File

@ -1,11 +1,15 @@
## Notes ## Notes
## Additions ## Additions
- -
## Fixes ## Fixes
- -
## Download ## Download
- [Windows]() - [Windows]()
- [MacOS]() - [MacOS]()
- [Linux]() - [Linux]()

2
.prettierignore Normal file
View File

@ -0,0 +1,2 @@
assets
dist

11
.prettierrc.json Normal file
View File

@ -0,0 +1,11 @@
{
"trailingComma": "all",
"tabWidth": 4,
"semi": true,
"arrowParens": "always",
"bracketSameLine": false,
"bracketSpacing": true,
"quoteProps": "as-needed",
"useTabs": true,
"singleQuote": false
}

8
.vscode/launch.json vendored
View File

@ -8,18 +8,14 @@
"name": "Launch current file", "name": "Launch current file",
"program": "${relativeFile}", "program": "${relativeFile}",
"request": "launch", "request": "launch",
"skipFiles": [ "skipFiles": ["<node_internals>/**"],
"<node_internals>/**"
],
"type": "node" "type": "node"
}, },
{ {
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"name": "Bundle", "name": "Bundle",
"skipFiles": [ "skipFiles": ["<node_internals>/**"],
"<node_internals>/**"
],
"program": "${workspaceFolder}/src/bundle/start.ts", "program": "${workspaceFolder}/src/bundle/start.ts",
"outFiles": ["${workspaceFolder}/dist/**/*.js"], "outFiles": ["${workspaceFolder}/dist/**/*.js"],
"preLaunchTask": "tsc: build - tsconfig.json" "preLaunchTask": "tsc: build - tsconfig.json"

22
package-lock.json generated
View File

@ -60,6 +60,7 @@
"@types/sharp": "^0.31.0", "@types/sharp": "^0.31.0",
"@types/ws": "^8.5.3", "@types/ws": "^8.5.3",
"express": "^4.18.1", "express": "^4.18.1",
"prettier": "^2.7.1",
"typescript": "^4.8.3" "typescript": "^4.8.3"
}, },
"optionalDependencies": { "optionalDependencies": {
@ -4715,6 +4716,21 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/prettier": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz",
"integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==",
"dev": true,
"bin": {
"prettier": "bin-prettier.js"
},
"engines": {
"node": ">=10.13.0"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/process-nextick-args": { "node_modules/process-nextick-args": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@ -9846,6 +9862,12 @@
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
"integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==" "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w=="
}, },
"prettier": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz",
"integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==",
"dev": true
},
"process-nextick-args": { "process-nextick-args": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",

View File

@ -40,11 +40,16 @@
"@types/sharp": "^0.31.0", "@types/sharp": "^0.31.0",
"@types/ws": "^8.5.3", "@types/ws": "^8.5.3",
"express": "^4.18.1", "express": "^4.18.1",
"prettier": "^2.7.1",
"typescript": "^4.8.3" "typescript": "^4.8.3"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.178.0",
"@sentry/node": "^7.13.0",
"@sentry/tracing": "^7.13.0",
"ajv": "^8.6.2", "ajv": "^8.6.2",
"ajv-formats": "^2.1.1", "ajv-formats": "^2.1.1",
"amqplib": "^0.10.3",
"bcrypt": "^5.0.1", "bcrypt": "^5.0.1",
"cheerio": "^1.0.0-rc.12", "cheerio": "^1.0.0-rc.12",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
@ -72,12 +77,7 @@
"sqlite3": "^5.1.1", "sqlite3": "^5.1.1",
"typeorm": "^0.3.10", "typeorm": "^0.3.10",
"typescript-json-schema": "^0.50.1", "typescript-json-schema": "^0.50.1",
"ws": "^8.9.0", "ws": "^8.9.0"
"@aws-sdk/client-s3": "^3.178.0",
"@sentry/node": "^7.13.0",
"@sentry/tracing": "^7.13.0",
"amqplib": "^0.10.3"
}, },
"optionalDependencies": { "optionalDependencies": {
"@yukikaze-bot/erlpack": "^1.0.1" "@yukikaze-bot/erlpack": "^1.0.1"

View File

@ -48,7 +48,7 @@ function connect() {
token, token,
properties: {}, properties: {},
}, },
}) }),
); );
break; break;

View File

@ -17,12 +17,12 @@ const INDEX_SCRIPTS = [
const doPatch = (content) => { const doPatch = (content) => {
//remove nitro references //remove nitro references
content = content.replace(/Discord Nitro/g, "Fosscord Premium"); content = content.replace(/Discord Nitro/g, "Fosscord Premium");
content = content.replace(/"Nitro"/g, "\"Premium\""); content = content.replace(/"Nitro"/g, '"Premium"');
content = content.replace(/Nitro /g, "Premium "); content = content.replace(/Nitro /g, "Premium ");
content = content.replace(/ Nitro/g, " Premium"); content = content.replace(/ Nitro/g, " Premium");
content = content.replace(/\[Nitro\]/g, "[Premium]"); content = content.replace(/\[Nitro\]/g, "[Premium]");
content = content.replace(/\*Nitro\*/g, "*Premium*"); content = content.replace(/\*Nitro\*/g, "*Premium*");
content = content.replace(/\"Nitro \. /g, "\"Premium. "); content = content.replace(/\"Nitro \. /g, '"Premium. ');
//remove discord references //remove discord references
content = content.replace(/ Discord /g, " Fosscord "); content = content.replace(/ Discord /g, " Fosscord ");
@ -35,11 +35,11 @@ const doPatch = (content) => {
content = content.replace(/\*Discord\*/g, "*Fosscord*"); content = content.replace(/\*Discord\*/g, "*Fosscord*");
//server -> guild //server -> guild
content = content.replace(/"Server"/g, "\"Guild\""); content = content.replace(/"Server"/g, '"Guild"');
content.replaceAll("server.\"", "guild.\""); content.replaceAll('server."', 'guild."');
content.replaceAll(" server ", " guild "); content.replaceAll(" server ", " guild ");
content.replaceAll(" Server ", " Guild "); content.replaceAll(" Server ", " Guild ");
content.replaceAll("\"Server", "\"Guild"); content.replaceAll('"Server', '"Guild');
// //change some vars // //change some vars
// content = content.replace('dsn: "https://fa97a90475514c03a42f80cd36d147c4@sentry.io/140984"', "dsn: (/true/.test(localStorage.sentryOptIn)?'https://6bad92b0175d41a18a037a73d0cff282@sentry.thearcanebrony.net/12':'')"); // content = content.replace('dsn: "https://fa97a90475514c03a42f80cd36d147c4@sentry.io/140984"', "dsn: (/true/.test(localStorage.sentryOptIn)?'https://6bad92b0175d41a18a037a73d0cff282@sentry.thearcanebrony.net/12':'')");
@ -52,8 +52,14 @@ const doPatch = (content) => {
// content = content.replace('width: n, height: o, viewBox: "0 0 28 20"', 'width: 48, height: 48, viewBox: "0 0 48 48"'); // content = content.replace('width: n, height: o, viewBox: "0 0 28 20"', 'width: 48, height: 48, viewBox: "0 0 48 48"');
//save some time on load resolving asset urls... //save some time on load resolving asset urls...
content = content.replaceAll('e.exports = n.p + "', 'e.exports = "/assets/'); content = content.replaceAll(
content = content.replaceAll('e.exports = r.p + "', 'e.exports = "/assets/'); 'e.exports = n.p + "',
'e.exports = "/assets/',
);
content = content.replaceAll(
'e.exports = r.p + "',
'e.exports = "/assets/',
);
return content; return content;
}; };
@ -66,7 +72,7 @@ const processFile = async (name) => {
await fs.writeFile(path.join(CACHE_PATH, `${name}.js`), text); await fs.writeFile(path.join(CACHE_PATH, `${name}.js`), text);
return [...new Set(text.match((/[A-Fa-f0-9]{20}/g)))]; return [...new Set(text.match(/[A-Fa-f0-9]{20}/g))];
}; };
(async () => { (async () => {
@ -83,7 +89,9 @@ const processFile = async (name) => {
process.stdout.clearLine(0); process.stdout.clearLine(0);
process.stdout.cursorTo(0); process.stdout.cursorTo(0);
process.stdout.write(`Scraping asset ${asset}. Remaining: ${INDEX_SCRIPTS.length}`); process.stdout.write(
`Scraping asset ${asset}. Remaining: ${INDEX_SCRIPTS.length}`,
);
const newAssets = await processFile(asset); const newAssets = await processFile(asset);
assets.push(...newAssets); assets.push(...newAssets);
@ -103,15 +111,21 @@ const processFile = async (name) => {
} }
while (rates.length > 20) rates.shift(); while (rates.length > 20) rates.shift();
const averageRate = rates.length ? rates.reduce((prev, curr) => prev + curr) / rates.length : 1; const averageRate = rates.length
const finishTime = (averageRate * (assets.length - i)); ? rates.reduce((prev, curr) => prev + curr) / rates.length
: 1;
const finishTime = averageRate * (assets.length - i);
process.stdout.clearLine(0); process.stdout.clearLine(0);
process.stdout.cursorTo(0); process.stdout.cursorTo(0);
process.stdout.write( process.stdout.write(
`Caching asset ${asset}. ` + `Caching asset ${asset}. ` +
`${i}/${assets.length - 1} = ${Math.floor((i / (assets.length - 1)) * 100)}% ` + `${i}/${assets.length - 1} = ${Math.floor(
`Finish at: ${new Date(Date.now() + finishTime).toLocaleTimeString()}` (i / (assets.length - 1)) * 100,
)}% ` +
`Finish at: ${new Date(
Date.now() + finishTime,
).toLocaleTimeString()}`,
); );
await processFile(asset); await processFile(asset);

View File

@ -1,4 +1,4 @@
require('module-alias/register'); require("module-alias/register");
const { Rights } = require(".."); const { Rights } = require("..");
const allRights = new Rights(1).bitfield; const allRights = new Rights(1).bitfield;

View File

@ -11,10 +11,10 @@ const settings = {
excludePrivate: true, excludePrivate: true,
defaultNumberType: "integer", defaultNumberType: "integer",
noExtraProps: true, noExtraProps: true,
defaultProps: false defaultProps: false,
}; };
const compilerOptions = { const compilerOptions = {
strictNullChecks: true strictNullChecks: true,
}; };
const Excluded = [ const Excluded = [
"DefaultSchema", "DefaultSchema",
@ -47,11 +47,17 @@ function modify(obj) {
} }
function main() { function main() {
const program = TJS.programFromConfig("tsconfig.json") const program = TJS.programFromConfig("tsconfig.json");
const generator = TJS.buildGenerator(program, settings); const generator = TJS.buildGenerator(program, settings);
if (!generator || !program) return; if (!generator || !program) return;
let schemas = generator.getUserSymbols().filter((x) => (x.endsWith("Schema") || x.endsWith("Response")) && !Excluded.includes(x)); let schemas = generator
.getUserSymbols()
.filter(
(x) =>
(x.endsWith("Schema") || x.endsWith("Response")) &&
!Excluded.includes(x),
);
console.log(schemas); console.log(schemas);
var definitions = {}; var definitions = {};

View File

@ -6,12 +6,12 @@ async function login(account) {
var body = { var body = {
fingerprint: "805826570869932034.wR8vi8lGlFBJerErO9LG5NViJFw", fingerprint: "805826570869932034.wR8vi8lGlFBJerErO9LG5NViJFw",
login: account.email, login: account.email,
password: account.password password: account.password,
}; };
var x = await fetch(config.url + "/auth/login", { var x = await fetch(config.url + "/auth/login", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(body) body: JSON.stringify(body),
}); });
console.log(x); console.log(x);
x = await x.json(); x = await x.json();

View File

@ -6,16 +6,19 @@ async function sendMessage(account) {
var body = { var body = {
fingerprint: "805826570869932034.wR8vi8lGlFBJerErO9LG5NViJFw", fingerprint: "805826570869932034.wR8vi8lGlFBJerErO9LG5NViJFw",
content: "Test", content: "Test",
tts: false tts: false,
}; };
var x = await fetch(config.url + "/channels/" + config["text-channel"] + "/messages", { var x = await fetch(
config.url + "/channels/" + config["text-channel"] + "/messages",
{
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: account.token Authorization: account.token,
}, },
body: JSON.stringify(body) body: JSON.stringify(body),
}); },
);
console.log(x); console.log(x);
x = await x.json(); x = await x.json();
console.log(x); console.log(x);

View File

@ -4,7 +4,11 @@ var config = require("../../config.json");
module.exports = generate; module.exports = generate;
async function generate() { async function generate() {
var mail = (Math.random() + 10).toString(36).substring(2); var mail = (Math.random() + 10).toString(36).substring(2);
mail = mail + "." + (Math.random() + 10).toString(36).substring(2) + "@stresstest.com"; mail =
mail +
"." +
(Math.random() + 10).toString(36).substring(2) +
"@stresstest.com";
var password = var password =
(Math.random() * 69).toString(36).substring(-7) + (Math.random() * 69).toString(36).substring(-7) +
(Math.random() * 69).toString(36).substring(-7) + (Math.random() * 69).toString(36).substring(-7) +
@ -20,12 +24,12 @@ async function generate() {
consent: true, consent: true,
date_of_birth: "2000-04-04", date_of_birth: "2000-04-04",
gift_code_sku_id: null, gift_code_sku_id: null,
captcha_key: null captcha_key: null,
}; };
var x = await fetch(config.url + "/auth/register", { var x = await fetch(config.url + "/auth/register", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(body) body: JSON.stringify(body),
}); });
console.log(x); console.log(x);
x = await x.json(); x = await x.json();

View File

@ -4,12 +4,9 @@
"name": "Slowcord Bot", "name": "Slowcord Bot",
"program": "${workspaceFolder}/build/index.js", "program": "${workspaceFolder}/build/index.js",
"request": "launch", "request": "launch",
"skipFiles": [ "skipFiles": ["<node_internals>/**"],
"<node_internals>/**"
],
"type": "node", "type": "node",
"preLaunchTask": "npm: build" "preLaunchTask": "npm: build"
} }
] ]
} }

View File

@ -5,7 +5,7 @@ import { Command, getCommands } from "./commands/index.js";
export default class Bot { export default class Bot {
client: Client; client: Client;
commands: { [key: string]: Command; } = {}; commands: { [key: string]: Command } = {};
constructor(client: Client) { constructor(client: Client) {
this.client = client; this.client = client;
@ -17,10 +17,12 @@ export default class Bot {
console.log(`Logged in as ${this.client.user!.tag}`); console.log(`Logged in as ${this.client.user!.tag}`);
this.client.user!.setPresence({ this.client.user!.setPresence({
activities: [{ activities: [
{
name: "EVERYTHING", name: "EVERYTHING",
type: "WATCHING", type: "WATCHING",
}] },
],
}); });
}; };

View File

@ -2,11 +2,11 @@ import { Message, GuildMember, Guild, User } from "discord.js";
import fs from "fs"; import fs from "fs";
export type CommandContext = { export type CommandContext = {
user: User, user: User;
guild: Guild | null, guild: Guild | null;
member: GuildMember | null, member: GuildMember | null;
message: Message, message: Message;
args: string[], args: string[];
}; };
export type Command = { export type Command = {
@ -19,8 +19,7 @@ const walk = async (path: string) => {
const out = []; const out = [];
for (var file of files) { for (var file of files) {
if (fs.statSync(`${path}/${file}`).isDirectory()) continue; if (fs.statSync(`${path}/${file}`).isDirectory()) continue;
if (file.indexOf("index") !== -1) if (file.indexOf("index") !== -1) continue;
continue;
if (file.indexOf(".js") !== file.length - 3) continue; if (file.indexOf(".js") !== file.length - 3) continue;
var imported = (await import(`./${file}`)).default; var imported = (await import(`./${file}`)).default;
out.push(imported); out.push(imported);

View File

@ -1,7 +1,7 @@
import { Command } from "./index.js"; import { Command } from "./index.js";
import { User, Guild, Message } from "@fosscord/util"; import { User, Guild, Message } from "@fosscord/util";
const cache: { [key: string]: number; } = { const cache: { [key: string]: number } = {
users: 0, users: 0,
guilds: 0, guilds: 0,
messages: 0, messages: 0,
@ -11,7 +11,10 @@ const cache: { [key: string]: number; } = {
export default { export default {
name: "instance", name: "instance",
exec: async ({ message }) => { exec: async ({ message }) => {
if (Date.now() > cache.lastChecked + parseInt(process.env.CACHE_TTL as string)) { if (
Date.now() >
cache.lastChecked + parseInt(process.env.CACHE_TTL as string)
) {
cache.users = await User.count(); cache.users = await User.count();
cache.guilds = await Guild.count(); cache.guilds = await Guild.count();
cache.messages = await Message.count(); cache.messages = await Message.count();
@ -19,18 +22,35 @@ export default {
} }
return message.reply({ return message.reply({
embeds: [{ embeds: [
{
title: "Instance Stats", title: "Instance Stats",
description: "For more indepth information, check out https://grafana.understars.dev", description:
"For more indepth information, check out https://grafana.understars.dev",
footer: { footer: {
text: `Last checked: ${Math.floor((Date.now() - cache.lastChecked) / (1000 * 60))} minutes ago`, text: `Last checked: ${Math.floor(
(Date.now() - cache.lastChecked) / (1000 * 60),
)} minutes ago`,
}, },
fields: [ fields: [
{ inline: true, name: "Total Users", value: cache.users.toString() }, {
{ inline: true, name: "Total Guilds", value: cache.guilds.toString() }, inline: true,
{ inline: true, name: "Total Messages", value: cache.messages.toString() }, name: "Total Users",
] value: cache.users.toString(),
}] },
{
inline: true,
name: "Total Guilds",
value: cache.guilds.toString(),
},
{
inline: true,
name: "Total Messages",
value: cache.messages.toString(),
},
],
},
],
}); });
} },
} as Command; } as Command;

View File

@ -11,10 +11,12 @@
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */ /* Language and Environment */
"target": "ES6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ "target": "ES6" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"lib": ["ES2021"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ "lib": [
"ES2021"
] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
// "jsx": "preserve", /* Specify what JSX code is generated. */ // "jsx": "preserve", /* Specify what JSX code is generated. */
"experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ "experimentalDecorators": true /* Enable experimental support for TC39 stage 2 draft decorators. */,
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
@ -24,9 +26,9 @@
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
/* Modules */ /* Modules */
"module": "CommonJS", /* Specify what module code is generated. */ "module": "CommonJS" /* Specify what module code is generated. */,
// "rootDir": "./", /* Specify the root folder within your source files. */ // "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
@ -45,9 +47,9 @@
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */ // "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
"sourceMap": true, /* Create source map files for emitted JavaScript files. */ "sourceMap": true /* Create source map files for emitted JavaScript files. */,
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
"outDir": "./build", /* Specify an output folder for all emitted files. */ "outDir": "./build" /* Specify an output folder for all emitted files. */,
// "removeComments": true, /* Disable emitting comments. */ // "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */ // "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
@ -69,17 +71,17 @@
/* Interop Constraints */ /* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
/* Type Checking */ /* Type Checking */
"strict": true, /* Enable all strict type-checking options. */ "strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
// "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
"strictPropertyInitialization": false, /* Check for class properties that are declared but not set in the constructor. */ "strictPropertyInitialization": false /* Check for class properties that are declared but not set in the constructor. */,
// "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
// "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */

View File

@ -4,13 +4,13 @@ html {
--background-primary: rgb(22, 23, 25); --background-primary: rgb(22, 23, 25);
--background-secondary: rgb(15, 16, 18); --background-secondary: rgb(15, 16, 18);
--foreground-primary: rgb(200, 200, 200); --foreground-primary: rgb(200, 200, 200);
--background-login-discord: #5865F2; --background-login-discord: #5865f2;
background: url("https://slowcord.maddy.k.vu/assets/background.png"); background: url("https://slowcord.maddy.k.vu/assets/background.png");
background-size: 100% 100%; background-size: 100% 100%;
background-repeat: no-repeat; background-repeat: no-repeat;
font-family: 'Montserrat', sans-serif; font-family: "Montserrat", sans-serif;
color: var(--foreground-primary); color: var(--foreground-primary);
} }
@ -55,7 +55,8 @@ html {
text-align: center; text-align: center;
} }
.header-subtext a, .header-subtext p { .header-subtext a,
.header-subtext p {
display: inline-block; display: inline-block;
margin: 0 10px 0 10px; margin: 0 10px 0 10px;
} }

View File

@ -29,12 +29,11 @@ const handleSubmit = async (path, body) => {
} }
// Very fun error message here lol // Very fun error message here lol
const error = const error = json.errors
json.errors
? Object.values(json.errors)[0]._errors[0].message ? Object.values(json.errors)[0]._errors[0].message
: ( : json.captcha_key
json.captcha_key ? "Captcha required" : json.message ? "Captcha required"
); : json.message;
failureMessage.innerHTML = error; failureMessage.innerHTML = error;
failureMessage.style.display = "block"; failureMessage.style.display = "block";

View File

@ -1,16 +1,18 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Slowcord</title> <title>Slowcord</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Montserrat&display=swap" rel="stylesheet"> <link
href="https://fonts.googleapis.com/css2?family=Montserrat&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="./css/index.css"> <link rel="stylesheet" href="./css/index.css" />
<script src="js/handler.js"></script> <script src="js/handler.js"></script>
</head> </head>
@ -36,16 +38,31 @@
<input type="submit" value="Login" /> <input type="submit" value="Login" />
<a id="loginDiscord" class="oauth" <a
href="https://discord.com/api/oauth2/authorize?client_id=991688571415175198&redirect_uri=https%3A%2F%2Fslowcord.maddy.k.vu%2Foauth%2Fdiscord&response_type=code&scope=identify%20email"> id="loginDiscord"
class="oauth"
href="https://discord.com/api/oauth2/authorize?client_id=991688571415175198&redirect_uri=https%3A%2F%2Fslowcord.maddy.k.vu%2Foauth%2Fdiscord&response_type=code&scope=identify%20email"
>
Login with Discord Login with Discord
</a> </a>
<div class="h-captcha" data-sitekey="fa3163ea-79a7-4b7b-b752-b58c545906c8" data-theme="dark"></div> <div
<script src="https://js.hcaptcha.com/1/api.js" async defer></script> class="h-captcha"
data-sitekey="fa3163ea-79a7-4b7b-b752-b58c545906c8"
data-theme="dark"
></div>
<script
src="https://js.hcaptcha.com/1/api.js"
async
defer
></script>
</form> </form>
<form action="javascript:void(0);" name="2fa" style="display: none"> <form
action="javascript:void(0);"
name="2fa"
style="display: none"
>
<label for="code">2FA Code</label> <label for="code">2FA Code</label>
<input type="number" name="code" /> <input type="number" name="code" />
@ -58,15 +75,23 @@
<script> <script>
/* https://stackoverflow.com/questions/5639346/what-is-the-shortest-function-for-reading-a-cookie-by-name-in-javascript */ /* https://stackoverflow.com/questions/5639346/what-is-the-shortest-function-for-reading-a-cookie-by-name-in-javascript */
const getCookieValue = (name) => ( const getCookieValue = (name) =>
document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)')?.pop() || '' document.cookie
); .match("(^|;)\\s*" + name + "\\s*=\\s*([^;]+)")
?.pop() || "";
let token = getCookieValue("token"); let token = getCookieValue("token");
if (token.trim().length) { if (token.trim().length) {
/* https://stackoverflow.com/a/27374365 */ /* https://stackoverflow.com/a/27374365 */
// why is clearing cookies so weird? wtf // why is clearing cookies so weird? wtf
document.cookie.split(";").forEach(function (c) { document.cookie = c.replace(/^ +/, "").replace(/=.*/, "=;expires=" + new Date().toUTCString() + ";path=/"); }); document.cookie.split(";").forEach(function (c) {
document.cookie = c
.replace(/^ +/, "")
.replace(
/=.*/,
"=;expires=" + new Date().toUTCString() + ";path=/",
);
});
window.localStorage.setItem("token", `"${token}"`); window.localStorage.setItem("token", `"${token}"`);
window.location.href = "/app"; window.location.href = "/app";
} }
@ -85,7 +110,7 @@
password: password, password: password,
captcha_key: hcaptcha, captcha_key: hcaptcha,
}); });
}) });
document.forms["2fa"].addEventListener("submit", async (e) => { document.forms["2fa"].addEventListener("submit", async (e) => {
const data = new FormData(e.target); const data = new FormData(e.target);
@ -96,8 +121,7 @@
code: code, code: code,
ticket: ticket, ticket: ticket,
}); });
}) });
</script> </script>
</body> </body>
</html> </html>

View File

@ -1,16 +1,18 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Slowcord</title> <title>Slowcord</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Montserrat&display=swap" rel="stylesheet"> <link
href="https://fonts.googleapis.com/css2?family=Montserrat&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="./css/index.css"> <link rel="stylesheet" href="./css/index.css" />
<script src="js/handler.js"></script> <script src="js/handler.js"></script>
</head> </head>
@ -27,7 +29,6 @@
<p id="failure">Register failed</p> <p id="failure">Register failed</p>
</div> </div>
<form action="javascript:void(0);"> <form action="javascript:void(0);">
<label for="email">Email</label> <label for="email">Email</label>
<input type="email" name="email" /> <input type="email" name="email" />
@ -43,13 +44,23 @@
<input type="submit" value="Register" /> <input type="submit" value="Register" />
<a id="loginDiscord" class="oauth" <a
href="https://discord.com/api/oauth2/authorize?client_id=991688571415175198&redirect_uri=https%3A%2F%2Fslowcord.maddy.k.vu%2Foauth%2Fdiscord&response_type=code&scope=identify%20email"> id="loginDiscord"
class="oauth"
href="https://discord.com/api/oauth2/authorize?client_id=991688571415175198&redirect_uri=https%3A%2F%2Fslowcord.maddy.k.vu%2Foauth%2Fdiscord&response_type=code&scope=identify%20email"
>
Login with Discord Login with Discord
</a> </a>
<div class="h-captcha" data-sitekey="fa3163ea-79a7-4b7b-b752-b58c545906c8"></div> <div
<script src="https://js.hcaptcha.com/1/api.js" async defer></script> class="h-captcha"
data-sitekey="fa3163ea-79a7-4b7b-b752-b58c545906c8"
></div>
<script
src="https://js.hcaptcha.com/1/api.js"
async
defer
></script>
</form> </form>
</div> </div>
</div> </div>
@ -61,7 +72,7 @@
const username = data.get("username"); const username = data.get("username");
const password = data.get("password"); const password = data.get("password");
const dob = data.get("dob"); const dob = data.get("dob");
const hcaptcha = data.get("h-captcha-response") const hcaptcha = data.get("h-captcha-response");
await handleSubmit("/api/v9/auth/register", { await handleSubmit("/api/v9/auth/register", {
consent: true, consent: true,
@ -71,8 +82,7 @@
date_of_birth: dob, date_of_birth: dob,
captcha_key: hcaptcha, captcha_key: hcaptcha,
}); });
}) });
</script> </script>
</body> </body>
</html> </html>

View File

@ -1,7 +1,13 @@
import "dotenv/config"; import "dotenv/config";
import express, { Request, Response } from "express"; import express, { Request, Response } from "express";
import cookieParser from "cookie-parser"; import cookieParser from "cookie-parser";
import { initDatabase, generateToken, User, Config, handleFile } from "fosscord-server/src/util"; import {
initDatabase,
generateToken,
User,
Config,
handleFile,
} from "fosscord-server/src/util";
import path from "path"; import path from "path";
import fetch from "node-fetch"; import fetch from "node-fetch";
@ -16,7 +22,7 @@ app.use(cookieParser());
const port = process.env.PORT; const port = process.env.PORT;
// ip -> unix epoch that requests will be accepted again // ip -> unix epoch that requests will be accepted again
const rateLimits: { [ip: string]: number; } = {}; const rateLimits: { [ip: string]: number } = {};
const allowRequestsEveryMs = 0.5 * 1000; // every half second const allowRequestsEveryMs = 0.5 * 1000; // every half second
const allowedRequestsPerSecond = 50; const allowedRequestsPerSecond = 50;
@ -36,23 +42,25 @@ class Discord {
static getAccessToken = async (req: Request, res: Response) => { static getAccessToken = async (req: Request, res: Response) => {
const { code } = req.query; const { code } = req.query;
const body = new URLSearchParams(Object.entries({ const body = new URLSearchParams(
Object.entries({
client_id: process.env.DISCORD_CLIENT_ID as string, client_id: process.env.DISCORD_CLIENT_ID as string,
client_secret: process.env.DISCORD_SECRET as string, client_secret: process.env.DISCORD_SECRET as string,
redirect_uri: process.env.DISCORD_REDIRECT as string, redirect_uri: process.env.DISCORD_REDIRECT as string,
code: code as string, code: code as string,
grant_type: "authorization_code", grant_type: "authorization_code",
})).toString(); }),
).toString();
const resp = await fetch("https://discord.com/api/oauth2/token", { const resp = await fetch("https://discord.com/api/oauth2/token", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/x-www-form-urlencoded", "Content-Type": "application/x-www-form-urlencoded",
}, },
body: body body: body,
}); });
const json = await resp.json() as any; const json = (await resp.json()) as any;
if (json.error) return null; if (json.error) return null;
return { return {
@ -67,24 +75,26 @@ class Discord {
static getUserDetails = async (token: string) => { static getUserDetails = async (token: string) => {
const resp = await fetch("https://discord.com/api/users/@me", { const resp = await fetch("https://discord.com/api/users/@me", {
headers: { headers: {
"Authorization": `Bearer ${token}`, Authorization: `Bearer ${token}`,
} },
}); });
const json = await resp.json() as any; const json = (await resp.json()) as any;
if (!json.username || !json.email) return null; // eh, deal with bad code later if (!json.username || !json.email) return null; // eh, deal with bad code later
return { return {
id: json.id, id: json.id,
email: json.email, email: json.email,
username: json.username, username: json.username,
avatar_url: json.avatar ? `https://cdn.discordapp.com/avatars/${json.id}/${json.avatar}?size=2048` : null, avatar_url: json.avatar
? `https://cdn.discordapp.com/avatars/${json.id}/${json.avatar}?size=2048`
: null,
}; };
}; };
} }
const handlers: { [key: string]: any; } = { const handlers: { [key: string]: any } = {
"discord": Discord, discord: Discord,
}; };
app.get("/oauth/:type", async (req, res) => { app.get("/oauth/:type", async (req, res) => {
@ -92,17 +102,21 @@ app.get("/oauth/:type", async (req, res) => {
if (requestsThisSecond > allowedRequestsPerSecond) if (requestsThisSecond > allowedRequestsPerSecond)
return res.sendStatus(429); return res.sendStatus(429);
const ip = (req.headers["x-forwarded-for"] as string) || req.socket.remoteAddress as string; const ip =
(req.headers["x-forwarded-for"] as string) ||
(req.socket.remoteAddress as string);
console.log(`${ip}`); console.log(`${ip}`);
if (!rateLimits[ip]) { if (!rateLimits[ip]) {
rateLimits[ip] = Date.now() + allowRequestsEveryMs; rateLimits[ip] = Date.now() + allowRequestsEveryMs;
} } else if (rateLimits[ip] > Date.now()) {
else if (rateLimits[ip] > Date.now()) {
rateLimits[ip] += allowRequestsEveryMs; rateLimits[ip] += allowRequestsEveryMs;
console.log(`${new Date()} : user ${ip} was timed out for ${(rateLimits[ip] - Date.now()) / 1000}s`); console.log(
`${new Date()} : user ${ip} was timed out for ${
(rateLimits[ip] - Date.now()) / 1000
}s`,
);
return res.sendStatus(429); return res.sendStatus(429);
} } else {
else {
delete rateLimits[ip]; delete rateLimits[ip];
} }
@ -121,16 +135,18 @@ app.get("/oauth/:type", async (req, res) => {
user = await User.register({ user = await User.register({
email: details.email, email: details.email,
username: details.username, username: details.username,
req req,
}); });
if (details.avatar_url) { if (details.avatar_url) {
try { try {
const avatar = await handleFile(`/avatars/${user.id}`, await toDataURL(details.avatar_url) as string); const avatar = await handleFile(
`/avatars/${user.id}`,
(await toDataURL(details.avatar_url)) as string,
);
user.avatar = avatar; user.avatar = avatar;
await user.save(); await user.save();
} } catch (e) {
catch (e) {
console.error(e); console.error(e);
} }
} }

View File

@ -1,10 +1,6 @@
{ {
"exclude": [ "exclude": ["node_modules"],
"node_modules" "include": ["src/**/*.ts"],
],
"include": [
"src/**/*.ts"
],
"compilerOptions": { "compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */ /* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Projects */ /* Projects */
@ -15,10 +11,12 @@
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */ /* Language and Environment */
"target": "ES6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ "target": "ES6" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"lib": ["ES2021"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ "lib": [
"ES2021"
] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
// "jsx": "preserve", /* Specify what JSX code is generated. */ // "jsx": "preserve", /* Specify what JSX code is generated. */
"experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ "experimentalDecorators": true /* Enable experimental support for TC39 stage 2 draft decorators. */,
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
@ -27,14 +25,16 @@
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
/* Modules */ /* Modules */
"module": "ES2020", /* Specify what module code is generated. */ "module": "ES2020" /* Specify what module code is generated. */,
// "rootDir": "./", /* Specify the root folder within your source files. */ // "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
"types": ["node"], /* Specify type package names to be included without being referenced in a source file. */ "types": [
"node"
] /* Specify type package names to be included without being referenced in a source file. */,
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "resolveJsonModule": true, /* Enable importing .json files */ // "resolveJsonModule": true, /* Enable importing .json files */
// "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */ // "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */
@ -46,9 +46,9 @@
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */ // "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
"sourceMap": true, /* Create source map files for emitted JavaScript files. */ "sourceMap": true /* Create source map files for emitted JavaScript files. */,
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
"outDir": "./build", /* Specify an output folder for all emitted files. */ "outDir": "./build" /* Specify an output folder for all emitted files. */,
// "removeComments": true, /* Disable emitting comments. */ // "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */ // "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
@ -69,16 +69,16 @@
/* Interop Constraints */ /* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
/* Type Checking */ /* Type Checking */
"strict": true, /* Enable all strict type-checking options. */ "strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
// "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
"strictPropertyInitialization": false, /* Check for class properties that are declared but not set in the constructor. */ "strictPropertyInitialization": false /* Check for class properties that are declared but not set in the constructor. */,
// "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
// "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */

View File

@ -3,11 +3,12 @@
Slowcord is a heavily modded Fosscord instance. You can browse it's source here: https://github.com/MaddyUnderStars/fosscord-server/tree/slowcord Slowcord is a heavily modded Fosscord instance. You can browse it's source here: https://github.com/MaddyUnderStars/fosscord-server/tree/slowcord
## Here are some general instance-wide rules: ## Here are some general instance-wide rules:
* **Harassment, homophobia, transphobia, etc, violence, and hate speech are forbidden.**
* Behaviour that harms the service - be it malicious/intentional or not - is strictly forbidden. This may include API abuse/spam, exploits, etc. - **Harassment, homophobia, transphobia, etc, violence, and hate speech are forbidden.**
* * If you do discover an exploit/bug, it would be greatly appreciated if you could create an issue in the above repo, or DM @MaddyUnderStars#0000. - Behaviour that harms the service - be it malicious/intentional or not - is strictly forbidden. This may include API abuse/spam, exploits, etc.
* Any content that would be considered illegal in Australia is also forbidden. Additionally, if it is illegal in your own country, it shouldn't be here. - - If you do discover an exploit/bug, it would be greatly appreciated if you could create an issue in the above repo, or DM @MaddyUnderStars#0000.
* Bots/selfbots are allowed. If you would like an account to be given bot status, DM @MaddyUnderStars#0000. - Any content that would be considered illegal in Australia is also forbidden. Additionally, if it is illegal in your own country, it shouldn't be here.
- Bots/selfbots are allowed. If you would like an account to be given bot status, DM @MaddyUnderStars#0000.
These rules are non-exhaustive, but should give a good idea of what will be enforced. These rules are non-exhaustive, but should give a good idea of what will be enforced.
@ -16,5 +17,6 @@ Permanent Slowcord guild invite: https://slowcord.understars.dev/invite/slowcord
### If a message or user breaks these rules, you can report it here: https://forms.gle/sd6RkdM7gRgJLV368 ### If a message or user breaks these rules, you can report it here: https://forms.gle/sd6RkdM7gRgJLV368
#### Lastly ( and not rules ): #### Lastly ( and not rules ):
* If you use BetterDiscord or Powercord, and want an easier time accessing Slowcord and other Fosscord instances, check out https://github.com/maddyunderstars/fosscord-bd!
* Also, if you're on Android, you can download the mobile client at https://slowcord.understars.dev/assets/slowcord.apk - If you use BetterDiscord or Powercord, and want an easier time accessing Slowcord and other Fosscord instances, check out https://github.com/maddyunderstars/fosscord-bd!
- Also, if you're on Android, you can download the mobile client at https://slowcord.understars.dev/assets/slowcord.apk

View File

@ -5,14 +5,22 @@ import mysql from "mysql2";
import fetch from "node-fetch"; import fetch from "node-fetch";
const dbConn = mysql.createConnection(process.env.DATABASE as string); const dbConn = mysql.createConnection(process.env.DATABASE as string);
const executePromise = (sql: string, args: any[]) => new Promise((resolve, reject) => dbConn.execute(sql, args, (err, res) => { if (err) reject(err); else resolve(res); })); const executePromise = (sql: string, args: any[]) =>
new Promise((resolve, reject) =>
dbConn.execute(sql, args, (err, res) => {
if (err) reject(err);
else resolve(res);
}),
);
const savePerf = async (time: number, name: string, error?: string | Error) => { const savePerf = async (time: number, name: string, error?: string | Error) => {
if (error && typeof error != "string") error = error.message; if (error && typeof error != "string") error = error.message;
try { try {
await executePromise("INSERT INTO performance (value, endpoint, timestamp, error) VALUES (?, ?, ?, ?)", [time ?? 0, name, new Date(), error ?? null]); await executePromise(
"INSERT INTO performance (value, endpoint, timestamp, error) VALUES (?, ?, ?, ?)",
[time ?? 0, name, new Date(), error ?? null],
);
// await executePromise("DELETE FROM performance WHERE DATE(timestamp) < now() - interval ? DAY", [process.env.RETENTION_DAYS]); // await executePromise("DELETE FROM performance WHERE DATE(timestamp) < now() - interval ? DAY", [process.env.RETENTION_DAYS]);
} } catch (e) {
catch (e) {
console.error(e); console.error(e);
} }
}; };
@ -23,7 +31,11 @@ const doMeasurements = async (channel: Discord.TextChannel) => {
timestamp = Date.now(); timestamp = Date.now();
await channel.send("hello this is a special message kthxbye"); await channel.send("hello this is a special message kthxbye");
setTimeout(doMeasurements, parseInt(process.env.MEASURE_INTERVAL as string), channel); setTimeout(
doMeasurements,
parseInt(process.env.MEASURE_INTERVAL as string),
channel,
);
}; };
const instance = { const instance = {
@ -37,8 +49,8 @@ const client = new Fosscord.Client({
intents: [], intents: [],
http: { http: {
api: instance.api, api: instance.api,
cdn: instance.cdn cdn: instance.cdn,
} },
}); });
client.on("ready", async () => { client.on("ready", async () => {
@ -52,19 +64,24 @@ client.on("ready", async () => {
client.on("messageCreate", async (msg: Discord.Message) => { client.on("messageCreate", async (msg: Discord.Message) => {
if (!timestamp) return; if (!timestamp) return;
if (msg.author.id != "992745947417141682" if (
|| msg.channel.id != "1019955729054267764" msg.author.id != "992745947417141682" ||
|| msg.content != "hello this is a special message kthxbye") msg.channel.id != "1019955729054267764" ||
msg.content != "hello this is a special message kthxbye"
)
return; return;
await savePerf(Date.now() - timestamp, "messageCreate", undefined); await savePerf(Date.now() - timestamp, "messageCreate", undefined);
timestamp = undefined; timestamp = undefined;
await fetch(`${instance.api}/channels/1019955729054267764/messages/${msg.id}`, { await fetch(
`${instance.api}/channels/1019955729054267764/messages/${msg.id}`,
{
method: "DELETE", method: "DELETE",
headers: { headers: {
authorization: instance.token authorization: instance.token,
} },
}) },
);
}); });
client.on("error", (error: any) => { client.on("error", (error: any) => {

View File

@ -4,7 +4,13 @@ import mysql from "mysql2";
import fetch from "node-fetch"; import fetch from "node-fetch";
const dbConn = mysql.createConnection(process.env.DATABASE as string); const dbConn = mysql.createConnection(process.env.DATABASE as string);
const executePromise = (sql: string, args: any[]) => new Promise((resolve, reject) => dbConn.execute(sql, args, (err, res) => { if (err) reject(err); else resolve(res); })); const executePromise = (sql: string, args: any[]) =>
new Promise((resolve, reject) =>
dbConn.execute(sql, args, (err, res) => {
if (err) reject(err);
else resolve(res);
}),
);
const instance = { const instance = {
app: process.env.INSTANCE_WEB_APP as string, app: process.env.INSTANCE_WEB_APP as string,
@ -16,24 +22,35 @@ const instance = {
const savePerf = async (time: number, name: string, error?: string | Error) => { const savePerf = async (time: number, name: string, error?: string | Error) => {
if (error && typeof error != "string") error = error.message; if (error && typeof error != "string") error = error.message;
try { try {
await executePromise("INSERT INTO performance (value, endpoint, timestamp, error) VALUES (?, ?, ?, ?)", [time ?? 0, name, new Date(), error ?? null]); await executePromise(
"INSERT INTO performance (value, endpoint, timestamp, error) VALUES (?, ?, ?, ?)",
[time ?? 0, name, new Date(), error ?? null],
);
// await executePromise("DELETE FROM performance WHERE DATE(timestamp) < now() - interval ? DAY", [process.env.RETENTION_DAYS]); // await executePromise("DELETE FROM performance WHERE DATE(timestamp) < now() - interval ? DAY", [process.env.RETENTION_DAYS]);
} } catch (e) {
catch (e) {
console.error(e); console.error(e);
} }
}; };
const saveSystemUsage = async (load: number, procUptime: number, sysUptime: number, ram: number, sessions: number) => { const saveSystemUsage = async (
load: number,
procUptime: number,
sysUptime: number,
ram: number,
sessions: number,
) => {
try { try {
await executePromise("INSERT INTO monitor (time, cpu, procUp, sysUp, ram, sessions) VALUES (?, ?, ?, ?, ?, ?)", [new Date(), load, procUptime, sysUptime, ram, sessions]); await executePromise(
} "INSERT INTO monitor (time, cpu, procUp, sysUp, ram, sessions) VALUES (?, ?, ?, ?, ?, ?)",
catch (e) { [new Date(), load, procUptime, sysUptime, ram, sessions],
);
} catch (e) {
console.error(e); console.error(e);
} }
}; };
const makeTimedRequest = (path: string, body?: object): Promise<number> => new Promise((resolve, reject) => { const makeTimedRequest = (path: string, body?: object): Promise<number> =>
new Promise((resolve, reject) => {
const opts = { const opts = {
hostname: new URL(path).hostname, hostname: new URL(path).hostname,
port: 443, port: 443,
@ -41,19 +58,18 @@ const makeTimedRequest = (path: string, body?: object): Promise<number> => new P
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": instance.token, Authorization: instance.token,
}, },
timeout: 1000, timeout: 1000,
}; };
let start: number, end: number; let start: number, end: number;
const req = https.request(opts, res => { const req = https.request(opts, (res) => {
if (res.statusCode! < 200 || res.statusCode! > 300) { if (res.statusCode! < 200 || res.statusCode! > 300) {
return reject(`${res.statusCode} ${res.statusMessage}`); return reject(`${res.statusCode} ${res.statusMessage}`);
} }
res.on("data", (data) => { res.on("data", (data) => {});
});
res.on("end", () => { res.on("end", () => {
end = Date.now(); end = Date.now();
@ -74,15 +90,18 @@ const makeTimedRequest = (path: string, body?: object): Promise<number> => new P
}); });
const measureApi = async (name: string, path: string, body?: object) => { const measureApi = async (name: string, path: string, body?: object) => {
let error, time = -1; let error,
time = -1;
try { try {
time = await makeTimedRequest(path, body); time = await makeTimedRequest(path, body);
} } catch (e) {
catch (e) {
error = e as Error | string; error = e as Error | string;
} }
console.log(`${name} took ${time}ms ${(error ? "with error" : "")}`, error ?? ""); console.log(
`${name} took ${time}ms ${error ? "with error" : ""}`,
error ?? "",
);
await savePerf(time, name, error); await savePerf(time, name, error);
}; };
@ -100,7 +119,11 @@ const app = async () => {
console.log("Connected to db"); console.log("Connected to db");
// await client.login(instance.token); // await client.login(instance.token);
console.log(`Monitoring performance for instance at ${new URL(instance.api).hostname}`); console.log(
`Monitoring performance for instance at ${
new URL(instance.api).hostname
}`,
);
const doMeasurements = async () => { const doMeasurements = async () => {
await measureApi("ping", `${instance.api}/ping`); await measureApi("ping", `${instance.api}/ping`);
@ -112,15 +135,22 @@ const app = async () => {
const res = await fetch(`${instance.api}/-/monitorz`, { const res = await fetch(`${instance.api}/-/monitorz`, {
headers: { headers: {
Authorization: process.env.INSTANCE_TOKEN as string, Authorization: process.env.INSTANCE_TOKEN as string,
} },
}); });
const json = await res.json() as monitorzSchema; const json = (await res.json()) as monitorzSchema;
await saveSystemUsage(json.load[1], json.procUptime, json.sysUptime, json.memPercent, json.sessions); await saveSystemUsage(
} json.load[1],
catch (e) { json.procUptime,
} json.sysUptime,
json.memPercent,
json.sessions,
);
} catch (e) {}
setTimeout(doMeasurements, parseInt(process.env.MEASURE_INTERVAL as string)); setTimeout(
doMeasurements,
parseInt(process.env.MEASURE_INTERVAL as string),
);
}; };
doMeasurements(); doMeasurements();

View File

@ -1,10 +1,6 @@
{ {
"exclude": [ "exclude": ["node_modules"],
"node_modules" "include": ["src/**/*.ts"],
],
"include": [
"src/**/*.ts"
],
"compilerOptions": { "compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */ /* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Projects */ /* Projects */
@ -15,10 +11,12 @@
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */ /* Language and Environment */
"target": "ES6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ "target": "ES6" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"lib": ["ES2021"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ "lib": [
"ES2021"
] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
// "jsx": "preserve", /* Specify what JSX code is generated. */ // "jsx": "preserve", /* Specify what JSX code is generated. */
"experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ "experimentalDecorators": true /* Enable experimental support for TC39 stage 2 draft decorators. */,
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
@ -27,14 +25,16 @@
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
/* Modules */ /* Modules */
"module": "ES2020", /* Specify what module code is generated. */ "module": "ES2020" /* Specify what module code is generated. */,
// "rootDir": "./", /* Specify the root folder within your source files. */ // "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
"types": ["node"], /* Specify type package names to be included without being referenced in a source file. */ "types": [
"node"
] /* Specify type package names to be included without being referenced in a source file. */,
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "resolveJsonModule": true, /* Enable importing .json files */ // "resolveJsonModule": true, /* Enable importing .json files */
// "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */ // "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */
@ -46,9 +46,9 @@
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */ // "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
"sourceMap": true, /* Create source map files for emitted JavaScript files. */ "sourceMap": true /* Create source map files for emitted JavaScript files. */,
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
"outDir": "./build", /* Specify an output folder for all emitted files. */ "outDir": "./build" /* Specify an output folder for all emitted files. */,
// "removeComments": true, /* Disable emitting comments. */ // "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */ // "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
@ -69,16 +69,16 @@
/* Interop Constraints */ /* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
/* Type Checking */ /* Type Checking */
"strict": true, /* Enable all strict type-checking options. */ "strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
// "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
"strictPropertyInitialization": false, /* Check for class properties that are declared but not set in the constructor. */ "strictPropertyInitialization": false /* Check for class properties that are declared but not set in the constructor. */,
// "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
// "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */

View File

@ -12,7 +12,7 @@ import { initTranslation } from "./middlewares/Translation";
import morgan from "morgan"; import morgan from "morgan";
import { initInstance } from "./util/handlers/Instance"; import { initInstance } from "./util/handlers/Instance";
import { registerRoutes } from "@fosscord/util"; import { registerRoutes } from "@fosscord/util";
import { red } from "picocolors" import { red } from "picocolors";
export interface FosscordServerOptions extends ServerOptions {} export interface FosscordServerOptions extends ServerOptions {}
@ -44,13 +44,18 @@ export class FosscordServer extends Server {
this.app.use( this.app.use(
morgan("combined", { morgan("combined", {
skip: (req, res) => { skip: (req, res) => {
var skip = !(process.env["LOG_REQUESTS"]?.includes(res.statusCode.toString()) ?? false); var skip = !(
if (process.env["LOG_REQUESTS"]?.charAt(0) == "-") skip = !skip; process.env["LOG_REQUESTS"]?.includes(
return skip; res.statusCode.toString(),
} ) ?? false
})
); );
}; if (process.env["LOG_REQUESTS"]?.charAt(0) == "-")
skip = !skip;
return skip;
},
}),
);
}
this.app.use(CORS); this.app.use(CORS);
this.app.use(BodyParser({ inflate: true, limit: "10mb" })); this.app.use(BodyParser({ inflate: true, limit: "10mb" }));
@ -63,16 +68,22 @@ export class FosscordServer extends Server {
await initRateLimits(api); await initRateLimits(api);
await initTranslation(api); await initTranslation(api);
this.routes = await registerRoutes(this, path.join(__dirname, "routes", "/")); this.routes = await registerRoutes(
this,
path.join(__dirname, "routes", "/"),
);
api.use("*", (error: any, req: Request, res: Response, next: NextFunction) => { api.use(
"*",
(error: any, req: Request, res: Response, next: NextFunction) => {
if (error) return next(error); if (error) return next(error);
res.status(404).json({ res.status(404).json({
message: "404 endpoint not found", message: "404 endpoint not found",
code: 0 code: 0,
}); });
next(); next();
}); },
);
this.app = app; this.app = app;
@ -87,8 +98,13 @@ export class FosscordServer extends Server {
this.app.use(ErrorHandler); this.app.use(ErrorHandler);
TestClient(this.app); TestClient(this.app);
if (logRequests) console.log(red(`Warning: Request logging is enabled! This will spam your console!\nTo disable this, unset the 'LOG_REQUESTS' environment variable!`)); if (logRequests)
console.log(
red(
`Warning: Request logging is enabled! This will spam your console!\nTo disable this, unset the 'LOG_REQUESTS' environment variable!`,
),
);
return super.start(); return super.start();
} }
}; }

View File

@ -26,7 +26,7 @@ export const NO_AUTHORIZATION_ROUTES = [
// Public policy pages // Public policy pages
"/policies/instance", "/policies/instance",
// Asset delivery // Asset delivery
/\/guilds\/\d+\/widget\.(json|png)/ /\/guilds\/\d+\/widget\.(json|png)/,
]; ];
export const API_PREFIX = /^\/api(\/v\d+)?/; export const API_PREFIX = /^\/api(\/v\d+)?/;
@ -43,7 +43,11 @@ declare global {
} }
} }
export async function Authentication(req: Request, res: Response, next: NextFunction) { export async function Authentication(
req: Request,
res: Response,
next: NextFunction,
) {
if (req.method === "OPTIONS") return res.sendStatus(204); if (req.method === "OPTIONS") return res.sendStatus(204);
const url = req.url.replace(API_PREFIX, ""); const url = req.url.replace(API_PREFIX, "");
if (url.startsWith("/invites") && req.method === "GET") return next(); if (url.startsWith("/invites") && req.method === "GET") return next();
@ -54,12 +58,16 @@ export async function Authentication(req: Request, res: Response, next: NextFunc
}) })
) )
return next(); return next();
if (!req.headers.authorization) return next(new HTTPError("Missing Authorization Header", 401)); if (!req.headers.authorization)
return next(new HTTPError("Missing Authorization Header", 401));
try { try {
const { jwtSecret } = Config.get().security; const { jwtSecret } = Config.get().security;
const { decoded, user }: any = await checkToken(req.headers.authorization, jwtSecret); const { decoded, user }: any = await checkToken(
req.headers.authorization,
jwtSecret,
);
req.token = decoded; req.token = decoded;
req.user_id = decoded.id; req.user_id = decoded.id;

View File

@ -6,7 +6,8 @@ export function BodyParser(opts?: OptionsJson) {
const jsonParser = bodyParser.json(opts); const jsonParser = bodyParser.json(opts);
return (req: Request, res: Response, next: NextFunction) => { return (req: Request, res: Response, next: NextFunction) => {
if (!req.headers["content-type"]) req.headers["content-type"] = "application/json"; if (!req.headers["content-type"])
req.headers["content-type"] = "application/json";
jsonParser(req, res, (err) => { jsonParser(req, res, (err) => {
if (err) { if (err) {

View File

@ -7,10 +7,16 @@ export function CORS(req: Request, res: Response, next: NextFunction) {
// TODO: use better CSP // TODO: use better CSP
res.set( res.set(
"Content-security-policy", "Content-security-policy",
"default-src * data: blob: filesystem: about: ws: wss: 'unsafe-inline' 'unsafe-eval'; script-src * data: blob: 'unsafe-inline' 'unsafe-eval'; connect-src * data: blob: 'unsafe-inline'; img-src * data: blob: 'unsafe-inline'; frame-src * data: blob: ; style-src * data: blob: 'unsafe-inline'; font-src * data: blob: 'unsafe-inline';" "default-src * data: blob: filesystem: about: ws: wss: 'unsafe-inline' 'unsafe-eval'; script-src * data: blob: 'unsafe-inline' 'unsafe-eval'; connect-src * data: blob: 'unsafe-inline'; img-src * data: blob: 'unsafe-inline'; frame-src * data: blob: ; style-src * data: blob: 'unsafe-inline'; font-src * data: blob: 'unsafe-inline';",
);
res.set(
"Access-Control-Allow-Headers",
req.header("Access-Control-Request-Headers") || "*",
);
res.set(
"Access-Control-Allow-Methods",
req.header("Access-Control-Request-Methods") || "*",
); );
res.set("Access-Control-Allow-Headers", req.header("Access-Control-Request-Headers") || "*");
res.set("Access-Control-Allow-Methods", req.header("Access-Control-Request-Methods") || "*");
next(); next();
} }

View File

@ -3,7 +3,12 @@ import { HTTPError } from "lambert-server";
import { ApiError, FieldError } from "@fosscord/util"; import { ApiError, FieldError } from "@fosscord/util";
const EntityNotFoundErrorRegex = /"(\w+)"/; const EntityNotFoundErrorRegex = /"(\w+)"/;
export function ErrorHandler(error: Error, req: Request, res: Response, next: NextFunction) { export function ErrorHandler(
error: Error,
req: Request,
res: Response,
next: NextFunction,
) {
if (!error) return next(); if (!error) return next();
try { try {
@ -12,20 +17,28 @@ export function ErrorHandler(error: Error, req: Request, res: Response, next: Ne
let message = error?.toString(); let message = error?.toString();
let errors = undefined; let errors = undefined;
if (error instanceof HTTPError && error.code) code = httpcode = error.code; if (error instanceof HTTPError && error.code)
code = httpcode = error.code;
else if (error instanceof ApiError) { else if (error instanceof ApiError) {
code = error.code; code = error.code;
message = error.message; message = error.message;
httpcode = error.httpStatus; httpcode = error.httpStatus;
} else if (error.name === "EntityNotFoundError") { } else if (error.name === "EntityNotFoundError") {
message = `${error.message.match(EntityNotFoundErrorRegex)?.[1] || "Item"} could not be found`; message = `${
error.message.match(EntityNotFoundErrorRegex)?.[1] || "Item"
} could not be found`;
code = httpcode = 404; code = httpcode = 404;
} else if (error instanceof FieldError) { } else if (error instanceof FieldError) {
code = Number(error.code); code = Number(error.code);
message = error.message; message = error.message;
errors = error.errors; errors = error.errors;
} else { } else {
console.error(`[Error] ${code} ${req.url}\n`, errors || error, "\nbody:", req.body); console.error(
`[Error] ${code} ${req.url}\n`,
errors || error,
"\nbody:",
req.body,
);
if (req.server?.options?.production) { if (req.server?.options?.production) {
// don't expose internal errors to the user, instead human errors should be thrown as HTTPError // don't expose internal errors to the user, instead human errors should be thrown as HTTPError
@ -39,6 +52,8 @@ export function ErrorHandler(error: Error, req: Request, res: Response, next: Ne
res.status(httpcode).json({ code: code, message, errors }); res.status(httpcode).json({ code: code, message, errors });
} catch (error) { } catch (error) {
console.error(`[Internal Server Error] 500`, error); console.error(`[Internal Server Error] 500`, error);
return res.status(500).json({ code: 500, message: "Internal Server Error" }); return res
.status(500)
.json({ code: 500, message: "Internal Server Error" });
} }
} }

View File

@ -40,21 +40,32 @@ export default function rateLimit(opts: {
success?: boolean; success?: boolean;
onlyIp?: boolean; onlyIp?: boolean;
}): any { }): any {
return async (req: Request, res: Response, next: NextFunction): Promise<any> => { return async (
req: Request,
res: Response,
next: NextFunction,
): Promise<any> => {
// exempt user? if so, immediately short circuit // exempt user? if so, immediately short circuit
if (req.user_id) { if (req.user_id) {
const rights = await getRights(req.user_id); const rights = await getRights(req.user_id);
if (rights.has("BYPASS_RATE_LIMITS")) return next(); if (rights.has("BYPASS_RATE_LIMITS")) return next();
} }
const bucket_id = opts.bucket || req.originalUrl.replace(API_PREFIX_TRAILING_SLASH, ""); const bucket_id =
opts.bucket ||
req.originalUrl.replace(API_PREFIX_TRAILING_SLASH, "");
let executor_id = getIpAdress(req); let executor_id = getIpAdress(req);
if (!opts.onlyIp && req.user_id) executor_id = req.user_id; if (!opts.onlyIp && req.user_id) executor_id = req.user_id;
let max_hits = opts.count; let max_hits = opts.count;
if (opts.bot && req.user_bot) max_hits = opts.bot; if (opts.bot && req.user_bot) max_hits = opts.bot;
if (opts.GET && ["GET", "OPTIONS", "HEAD"].includes(req.method)) max_hits = opts.GET; if (opts.GET && ["GET", "OPTIONS", "HEAD"].includes(req.method))
else if (opts.MODIFY && ["POST", "DELETE", "PATCH", "PUT"].includes(req.method)) max_hits = opts.MODIFY; max_hits = opts.GET;
else if (
opts.MODIFY &&
["POST", "DELETE", "PATCH", "PUT"].includes(req.method)
)
max_hits = opts.MODIFY;
let offender = Cache.get(executor_id + bucket_id); let offender = Cache.get(executor_id + bucket_id);
@ -75,11 +86,15 @@ export default function rateLimit(opts: {
const global = bucket_id === "global"; const global = bucket_id === "global";
// each block violation pushes the expiry one full window further // each block violation pushes the expiry one full window further
reset += opts.window * 1000; reset += opts.window * 1000;
offender.expires_at = new Date(offender.expires_at.getTime() + opts.window * 1000); offender.expires_at = new Date(
offender.expires_at.getTime() + opts.window * 1000,
);
resetAfterMs = reset - Date.now(); resetAfterMs = reset - Date.now();
resetAfterSec = Math.ceil(resetAfterMs / 1000); resetAfterSec = Math.ceil(resetAfterMs / 1000);
console.log(`blocked bucket: ${bucket_id} ${executor_id}`, { resetAfterMs }); console.log(`blocked bucket: ${bucket_id} ${executor_id}`, {
resetAfterMs,
});
return ( return (
res res
.status(429) .status(429)
@ -91,20 +106,33 @@ export default function rateLimit(opts: {
.set("Retry-After", `${Math.ceil(resetAfterSec)}`) .set("Retry-After", `${Math.ceil(resetAfterSec)}`)
.set("X-RateLimit-Bucket", `${bucket_id}`) .set("X-RateLimit-Bucket", `${bucket_id}`)
// TODO: error rate limit message translation // TODO: error rate limit message translation
.send({ message: "You are being rate limited.", retry_after: resetAfterSec, global }) .send({
message: "You are being rate limited.",
retry_after: resetAfterSec,
global,
})
); );
} }
} }
next(); next();
const hitRouteOpts = { bucket_id, executor_id, max_hits, window: opts.window }; const hitRouteOpts = {
bucket_id,
executor_id,
max_hits,
window: opts.window,
};
if (opts.error || opts.success) { if (opts.error || opts.success) {
res.once("finish", () => { res.once("finish", () => {
// check if error and increment error rate limit // check if error and increment error rate limit
if (res.statusCode >= 400 && opts.error) { if (res.statusCode >= 400 && opts.error) {
return hitRoute(hitRouteOpts); return hitRoute(hitRouteOpts);
} else if (res.statusCode >= 200 && res.statusCode < 300 && opts.success) { } else if (
res.statusCode >= 200 &&
res.statusCode < 300 &&
opts.success
) {
return hitRoute(hitRouteOpts); return hitRoute(hitRouteOpts);
} }
}); });
@ -141,8 +169,8 @@ export async function initRateLimits(app: Router) {
rateLimit({ rateLimit({
bucket: "global", bucket: "global",
onlyIp: true, onlyIp: true,
...ip ...ip,
}) }),
); );
app.use(rateLimit({ bucket: "global", ...global })); app.use(rateLimit({ bucket: "global", ...global }));
app.use( app.use(
@ -150,17 +178,25 @@ export async function initRateLimits(app: Router) {
bucket: "error", bucket: "error",
error: true, error: true,
onlyIp: true, onlyIp: true,
...error ...error,
}) }),
); );
app.use("/guilds/:id", rateLimit(routes.guild)); app.use("/guilds/:id", rateLimit(routes.guild));
app.use("/webhooks/:id", rateLimit(routes.webhook)); app.use("/webhooks/:id", rateLimit(routes.webhook));
app.use("/channels/:id", rateLimit(routes.channel)); app.use("/channels/:id", rateLimit(routes.channel));
app.use("/auth/login", rateLimit(routes.auth.login)); app.use("/auth/login", rateLimit(routes.auth.login));
app.use("/auth/register", rateLimit({ onlyIp: true, success: true, ...routes.auth.register })); app.use(
"/auth/register",
rateLimit({ onlyIp: true, success: true, ...routes.auth.register }),
);
} }
async function hitRoute(opts: { executor_id: string; bucket_id: string; max_hits: number; window: number; }) { async function hitRoute(opts: {
executor_id: string;
bucket_id: string;
max_hits: number;
window: number;
}) {
const id = opts.executor_id + opts.bucket_id; const id = opts.executor_id + opts.bucket_id;
let limit = Cache.get(id); let limit = Cache.get(id);
if (!limit) { if (!limit) {
@ -169,7 +205,7 @@ async function hitRoute(opts: { executor_id: string; bucket_id: string; max_hits
executor_id: opts.executor_id, executor_id: opts.executor_id,
expires_at: new Date(Date.now() + opts.window * 1000), expires_at: new Date(Date.now() + opts.window * 1000),
hits: 0, hits: 0,
blocked: false blocked: false,
}; };
Cache.set(id, limit); Cache.set(id, limit);
} }

View File

@ -9,8 +9,12 @@ const ASSET_FOLDER_PATH = path.join(__dirname, "..", "..", "..", "assets");
export async function initTranslation(router: Router) { export async function initTranslation(router: Router) {
const languages = fs.readdirSync(path.join(ASSET_FOLDER_PATH, "locales")); const languages = fs.readdirSync(path.join(ASSET_FOLDER_PATH, "locales"));
const namespaces = fs.readdirSync(path.join(ASSET_FOLDER_PATH, "locales", "en")); const namespaces = fs.readdirSync(
const ns = namespaces.filter((x) => x.endsWith(".json")).map((x) => x.slice(0, x.length - 5)); path.join(ASSET_FOLDER_PATH, "locales", "en"),
);
const ns = namespaces
.filter((x) => x.endsWith(".json"))
.map((x) => x.slice(0, x.length - 5));
await i18next await i18next
.use(i18nextBackend) .use(i18nextBackend)
@ -21,9 +25,11 @@ export async function initTranslation(router: Router) {
fallbackLng: "en", fallbackLng: "en",
ns, ns,
backend: { backend: {
loadPath: path.join(ASSET_FOLDER_PATH, "locales") + "/{{lng}}/{{ns}}.json", loadPath:
path.join(ASSET_FOLDER_PATH, "locales") +
"/{{lng}}/{{ns}}.json",
}, },
load: "all" load: "all",
}); });
router.use(i18nextMiddleware.handle(i18next, {})); router.use(i18nextMiddleware.handle(i18next, {}));

View File

@ -5,14 +5,18 @@ import os from "os";
const router = Router(); const router = Router();
router.get("/", route({ right: "OPERATOR" }), async (req: Request, res: Response) => { router.get(
"/",
route({ right: "OPERATOR" }),
async (req: Request, res: Response) => {
return res.json({ return res.json({
load: os.loadavg(), load: os.loadavg(),
procUptime: process.uptime(), procUptime: process.uptime(),
sysUptime: os.uptime(), sysUptime: os.uptime(),
memPercent: 100 - ((os.freemem() / os.totalmem()) * 100), memPercent: 100 - (os.freemem() / os.totalmem()) * 100,
sessions: await Session.count(), sessions: await Session.count(),
}) });
}) },
);
export default router; export default router;

View File

@ -7,7 +7,11 @@ router.get("/",route({}), async (req: Request, res: Response) => {
//TODO //TODO
//Note: It's most likely related to legal. At the moment Discord hasn't finished this too //Note: It's most likely related to legal. At the moment Discord hasn't finished this too
const country_code = (await IPAnalysis(getIpAdress(req))).country_code; const country_code = (await IPAnalysis(getIpAdress(req))).country_code;
res.json({ consent_required: false, country_code: country_code, promotional_email_opt_in: { required: true, pre_checked: false}}); res.json({
consent_required: false,
country_code: country_code,
promotional_email_opt_in: { required: true, pre_checked: false },
});
}); });
export default router; export default router;

View File

@ -1,14 +1,25 @@
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
import { route, getIpAdress, verifyCaptcha } from "@fosscord/api"; import { route, getIpAdress, verifyCaptcha } from "@fosscord/api";
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import { Config, User, generateToken, adjustEmail, FieldErrors, LoginSchema } from "@fosscord/util"; import {
Config,
User,
generateToken,
adjustEmail,
FieldErrors,
LoginSchema,
} from "@fosscord/util";
import crypto from "crypto"; import crypto from "crypto";
const router: Router = Router(); const router: Router = Router();
export default router; export default router;
router.post("/", route({ body: "LoginSchema" }), async (req: Request, res: Response) => { router.post(
const { login, password, captcha_key, undelete } = req.body as LoginSchema; "/",
route({ body: "LoginSchema" }),
async (req: Request, res: Response) => {
const { login, password, captcha_key, undelete } =
req.body as LoginSchema;
const email = adjustEmail(login); const email = adjustEmail(login);
console.log("login", email); console.log("login", email);
@ -20,7 +31,7 @@ router.post("/", route({ body: "LoginSchema" }), async (req: Request, res: Respo
return res.status(400).json({ return res.status(400).json({
captcha_key: ["captcha-required"], captcha_key: ["captcha-required"],
captcha_sitekey: sitekey, captcha_sitekey: sitekey,
captcha_service: service captcha_service: service,
}); });
} }
@ -30,31 +41,62 @@ router.post("/", route({ body: "LoginSchema" }), async (req: Request, res: Respo
return res.status(400).json({ return res.status(400).json({
captcha_key: verify["error-codes"], captcha_key: verify["error-codes"],
captcha_sitekey: sitekey, captcha_sitekey: sitekey,
captcha_service: service captcha_service: service,
}); });
} }
} }
const user = await User.findOneOrFail({ const user = await User.findOneOrFail({
where: [{ phone: login }, { email: login }], where: [{ phone: login }, { email: login }],
select: ["data", "id", "disabled", "deleted", "settings", "totp_secret", "mfa_enabled"] select: [
"data",
"id",
"disabled",
"deleted",
"settings",
"totp_secret",
"mfa_enabled",
],
}).catch((e) => { }).catch((e) => {
throw FieldErrors({ login: { message: req.t("auth:login.INVALID_LOGIN"), code: "INVALID_LOGIN" } }); throw FieldErrors({
login: {
message: req.t("auth:login.INVALID_LOGIN"),
code: "INVALID_LOGIN",
},
});
}); });
if (undelete) { if (undelete) {
// undelete refers to un'disable' here // undelete refers to un'disable' here
if (user.disabled) await User.update({ id: user.id }, { disabled: false }); if (user.disabled)
if (user.deleted) await User.update({ id: user.id }, { deleted: false }); await User.update({ id: user.id }, { disabled: false });
if (user.deleted)
await User.update({ id: user.id }, { deleted: false });
} else { } else {
if (user.deleted) return res.status(400).json({ message: "This account is scheduled for deletion.", code: 20011 }); if (user.deleted)
if (user.disabled) return res.status(400).json({ message: req.t("auth:login.ACCOUNT_DISABLED"), code: 20013 }); return res.status(400).json({
message: "This account is scheduled for deletion.",
code: 20011,
});
if (user.disabled)
return res.status(400).json({
message: req.t("auth:login.ACCOUNT_DISABLED"),
code: 20013,
});
} }
// the salt is saved in the password refer to bcrypt docs // the salt is saved in the password refer to bcrypt docs
const same_password = await bcrypt.compare(password, user.data.hash || ""); const same_password = await bcrypt.compare(
password,
user.data.hash || "",
);
if (!same_password) { if (!same_password) {
throw FieldErrors({ password: { message: req.t("auth:login.INVALID_PASSWORD"), code: "INVALID_PASSWORD" } }); throw FieldErrors({
password: {
message: req.t("auth:login.INVALID_PASSWORD"),
code: "INVALID_PASSWORD",
},
});
} }
if (user.mfa_enabled) { if (user.mfa_enabled) {
@ -68,7 +110,7 @@ router.post("/", route({ body: "LoginSchema" }), async (req: Request, res: Respo
mfa: true, mfa: true,
sms: false, // TODO sms: false, // TODO
token: null, token: null,
}) });
} }
const token = await generateToken(user.id); const token = await generateToken(user.id);
@ -78,7 +120,8 @@ router.post("/", route({ body: "LoginSchema" }), async (req: Request, res: Respo
// https://user-images.githubusercontent.com/6506416/81051916-dd8c9900-8ec2-11ea-8794-daf12d6f31f0.png // https://user-images.githubusercontent.com/6506416/81051916-dd8c9900-8ec2-11ea-8794-daf12d6f31f0.png
res.json({ token, settings: user.settings }); res.json({ token, settings: user.settings });
}); },
);
/** /**
* POST /auth/login * POST /auth/login

View File

@ -10,7 +10,8 @@ router.post("/", route({}), async (req: Request, res: Response) => {
} else { } else {
delete req.body.provider; delete req.body.provider;
delete req.body.voip_provider; delete req.body.voip_provider;
if (Object.keys(req.body).length != 0) console.log(`[LOGOUT]: Extra fields sent in logout!`, req.body); if (Object.keys(req.body).length != 0)
console.log(`[LOGOUT]: Extra fields sent in logout!`, req.body);
} }
res.status(204).send(); res.status(204).send();
}); });

View File

@ -5,18 +5,18 @@ import { verifyToken } from "node-2fa";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
const router = Router(); const router = Router();
router.post("/", route({ body: "TotpSchema" }), async (req: Request, res: Response) => { router.post(
const { code, ticket, gift_code_sku_id, login_source } = req.body as TotpSchema; "/",
route({ body: "TotpSchema" }),
async (req: Request, res: Response) => {
const { code, ticket, gift_code_sku_id, login_source } =
req.body as TotpSchema;
const user = await User.findOneOrFail({ const user = await User.findOneOrFail({
where: { where: {
totp_last_ticket: ticket, totp_last_ticket: ticket,
}, },
select: [ select: ["id", "totp_secret", "settings"],
"id",
"totp_secret",
"settings",
],
}); });
const backup = await BackupCode.findOne({ const backup = await BackupCode.findOne({
@ -24,16 +24,18 @@ router.post("/", route({ body: "TotpSchema" }), async (req: Request, res: Respon
code: code, code: code,
expired: false, expired: false,
consumed: false, consumed: false,
user: { id: user.id } user: { id: user.id },
} },
}); });
if (!backup) { if (!backup) {
const ret = verifyToken(user.totp_secret!, code); const ret = verifyToken(user.totp_secret!, code);
if (!ret || ret.delta != 0) if (!ret || ret.delta != 0)
throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008); throw new HTTPError(
} req.t("auth:login.INVALID_TOTP_CODE"),
else { 60008,
);
} else {
backup.consumed = true; backup.consumed = true;
await backup.save(); await backup.save();
} }
@ -44,6 +46,7 @@ router.post("/", route({ body: "TotpSchema" }), async (req: Request, res: Respon
token: await generateToken(user.id), token: await generateToken(user.id),
user_settings: user.settings, user_settings: user.settings,
}); });
}); },
);
export default router; export default router;

View File

@ -1,12 +1,29 @@
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
import { Config, generateToken, Invite, FieldErrors, User, adjustEmail, RegisterSchema } from "@fosscord/util"; import {
import { route, getIpAdress, IPAnalysis, isProxy, verifyCaptcha } from "@fosscord/api"; Config,
generateToken,
Invite,
FieldErrors,
User,
adjustEmail,
RegisterSchema,
} from "@fosscord/util";
import {
route,
getIpAdress,
IPAnalysis,
isProxy,
verifyCaptcha,
} from "@fosscord/api";
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
const router: Router = Router(); const router: Router = Router();
router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Response) => { router.post(
"/",
route({ body: "RegisterSchema" }),
async (req: Request, res: Response) => {
const body = req.body as RegisterSchema; const body = req.body as RegisterSchema;
const { register, security } = Config.get(); const { register, security } = Config.get();
const ip = getIpAdress(req); const ip = getIpAdress(req);
@ -17,14 +34,20 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
// check if registration is allowed // check if registration is allowed
if (!register.allowNewRegistration) { if (!register.allowNewRegistration) {
throw FieldErrors({ throw FieldErrors({
email: { code: "REGISTRATION_DISABLED", message: req.t("auth:register.REGISTRATION_DISABLED") } email: {
code: "REGISTRATION_DISABLED",
message: req.t("auth:register.REGISTRATION_DISABLED"),
},
}); });
} }
// check if the user agreed to the Terms of Service // check if the user agreed to the Terms of Service
if (!body.consent) { if (!body.consent) {
throw FieldErrors({ throw FieldErrors({
consent: { code: "CONSENT_REQUIRED", message: req.t("auth:register.CONSENT_REQUIRED") } consent: {
code: "CONSENT_REQUIRED",
message: req.t("auth:register.CONSENT_REQUIRED"),
},
}); });
} }
@ -32,8 +55,8 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
throw FieldErrors({ throw FieldErrors({
email: { email: {
code: "DISABLED", code: "DISABLED",
message: "registration is disabled on this instance" message: "registration is disabled on this instance",
} },
}); });
} }
@ -43,7 +66,7 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
return res?.status(400).json({ return res?.status(400).json({
captcha_key: ["captcha-required"], captcha_key: ["captcha-required"],
captcha_sitekey: sitekey, captcha_sitekey: sitekey,
captcha_service: service captcha_service: service,
}); });
} }
@ -52,21 +75,26 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
return res.status(400).json({ return res.status(400).json({
captcha_key: verify["error-codes"], captcha_key: verify["error-codes"],
captcha_sitekey: sitekey, captcha_sitekey: sitekey,
captcha_service: service captcha_service: service,
}); });
} }
} }
if (!register.allowMultipleAccounts) { if (!register.allowMultipleAccounts) {
// TODO: check if fingerprint was eligible generated // TODO: check if fingerprint was eligible generated
const exists = await User.findOne({ where: { fingerprints: body.fingerprint }, select: ["id"] }); const exists = await User.findOne({
where: { fingerprints: body.fingerprint },
select: ["id"],
});
if (exists) { if (exists) {
throw FieldErrors({ throw FieldErrors({
email: { email: {
code: "EMAIL_ALREADY_REGISTERED", code: "EMAIL_ALREADY_REGISTERED",
message: req.t("auth:register.EMAIL_ALREADY_REGISTERED") message: req.t(
} "auth:register.EMAIL_ALREADY_REGISTERED",
),
},
}); });
} }
} }
@ -84,7 +112,12 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
if (email) { if (email) {
// replace all dots and chars after +, if its a gmail.com email // replace all dots and chars after +, if its a gmail.com email
if (!email) { if (!email) {
throw FieldErrors({ email: { code: "INVALID_EMAIL", message: req?.t("auth:register.INVALID_EMAIL") } }); throw FieldErrors({
email: {
code: "INVALID_EMAIL",
message: req?.t("auth:register.INVALID_EMAIL"),
},
});
} }
// check if there is already an account with this email // check if there is already an account with this email
@ -94,23 +127,36 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
throw FieldErrors({ throw FieldErrors({
email: { email: {
code: "EMAIL_ALREADY_REGISTERED", code: "EMAIL_ALREADY_REGISTERED",
message: req.t("auth:register.EMAIL_ALREADY_REGISTERED") message: req.t(
} "auth:register.EMAIL_ALREADY_REGISTERED",
),
},
}); });
} }
} else if (register.email.required) { } else if (register.email.required) {
throw FieldErrors({ throw FieldErrors({
email: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") } email: {
code: "BASE_TYPE_REQUIRED",
message: req.t("common:field.BASE_TYPE_REQUIRED"),
},
}); });
} }
if (register.dateOfBirth.required && !body.date_of_birth) { if (register.dateOfBirth.required && !body.date_of_birth) {
throw FieldErrors({ throw FieldErrors({
date_of_birth: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") } date_of_birth: {
code: "BASE_TYPE_REQUIRED",
message: req.t("common:field.BASE_TYPE_REQUIRED"),
},
}); });
} else if (register.dateOfBirth.required && register.dateOfBirth.minimum) { } else if (
register.dateOfBirth.required &&
register.dateOfBirth.minimum
) {
const minimum = new Date(); const minimum = new Date();
minimum.setFullYear(minimum.getFullYear() - register.dateOfBirth.minimum); minimum.setFullYear(
minimum.getFullYear() - register.dateOfBirth.minimum,
);
body.date_of_birth = new Date(body.date_of_birth as Date); body.date_of_birth = new Date(body.date_of_birth as Date);
// higher is younger // higher is younger
@ -118,8 +164,10 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
throw FieldErrors({ throw FieldErrors({
date_of_birth: { date_of_birth: {
code: "DATE_OF_BIRTH_UNDERAGE", code: "DATE_OF_BIRTH_UNDERAGE",
message: req.t("auth:register.DATE_OF_BIRTH_UNDERAGE", { years: register.dateOfBirth.minimum }) message: req.t("auth:register.DATE_OF_BIRTH_UNDERAGE", {
} years: register.dateOfBirth.minimum,
}),
},
}); });
} }
} }
@ -129,14 +177,24 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
body.password = await bcrypt.hash(body.password, 12); body.password = await bcrypt.hash(body.password, 12);
} else if (register.password.required) { } else if (register.password.required) {
throw FieldErrors({ throw FieldErrors({
password: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") } password: {
code: "BASE_TYPE_REQUIRED",
message: req.t("common:field.BASE_TYPE_REQUIRED"),
},
}); });
} }
if (!body.invite && (register.requireInvite || (register.guestsRequireInvite && !register.email))) { if (
!body.invite &&
(register.requireInvite ||
(register.guestsRequireInvite && !register.email))
) {
// require invite to register -> e.g. for organizations to send invites to their employees // require invite to register -> e.g. for organizations to send invites to their employees
throw FieldErrors({ throw FieldErrors({
email: { code: "INVITE_ONLY", message: req.t("auth:register.INVITE_ONLY") } email: {
code: "INVITE_ONLY",
message: req.t("auth:register.INVITE_ONLY"),
},
}); });
} }
@ -150,7 +208,8 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
console.log("register", body.email, body.username, ip); console.log("register", body.email, body.username, ip);
return res.json({ token: await generateToken(user.id) }); return res.json({ token: await generateToken(user.id) });
}); },
);
export default router; export default router;

View File

@ -4,19 +4,31 @@ import { FieldErrors, User, BackupCodesChallengeSchema } from "@fosscord/util";
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
const router = Router(); const router = Router();
router.post("/", route({ body: "BackupCodesChallengeSchema" }), async (req: Request, res: Response) => { router.post(
"/",
route({ body: "BackupCodesChallengeSchema" }),
async (req: Request, res: Response) => {
const { password } = req.body as BackupCodesChallengeSchema; const { password } = req.body as BackupCodesChallengeSchema;
const user = await User.findOneOrFail({ where: { id: req.user_id }, select: ["data"] }); const user = await User.findOneOrFail({
where: { id: req.user_id },
select: ["data"],
});
if (!await bcrypt.compare(password, user.data.hash || "")) { if (!(await bcrypt.compare(password, user.data.hash || ""))) {
throw FieldErrors({ password: { message: req.t("auth:login.INVALID_PASSWORD"), code: "INVALID_PASSWORD" } }); throw FieldErrors({
password: {
message: req.t("auth:login.INVALID_PASSWORD"),
code: "INVALID_PASSWORD",
},
});
} }
return res.json({ return res.json({
nonce: "NoncePlaceholder", nonce: "NoncePlaceholder",
regenerate_nonce: "RegenNoncePlaceholder", regenerate_nonce: "RegenNoncePlaceholder",
}); });
}); },
);
export default router; export default router;

View File

@ -6,7 +6,7 @@ import {
emitEvent, emitEvent,
Recipient, Recipient,
handleFile, handleFile,
ChannelModifySchema ChannelModifySchema,
} from "@fosscord/util"; } from "@fosscord/util";
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
import { route } from "@fosscord/api"; import { route } from "@fosscord/api";
@ -15,44 +15,76 @@ const router: Router = Router();
// TODO: delete channel // TODO: delete channel
// TODO: Get channel // TODO: Get channel
router.get("/", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: Response) => { router.get(
"/",
route({ permission: "VIEW_CHANNEL" }),
async (req: Request, res: Response) => {
const { channel_id } = req.params; const { channel_id } = req.params;
const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); const channel = await Channel.findOneOrFail({
where: { id: channel_id },
return res.send(channel);
}); });
router.delete("/", route({ permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => { return res.send(channel);
},
);
router.delete(
"/",
route({ permission: "MANAGE_CHANNELS" }),
async (req: Request, res: Response) => {
const { channel_id } = req.params; const { channel_id } = req.params;
const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients"] }); const channel = await Channel.findOneOrFail({
where: { id: channel_id },
relations: ["recipients"],
});
if (channel.type === ChannelType.DM) { if (channel.type === ChannelType.DM) {
const recipient = await Recipient.findOneOrFail({ where: { channel_id: channel_id, user_id: req.user_id } }); const recipient = await Recipient.findOneOrFail({
where: { channel_id: channel_id, user_id: req.user_id },
});
recipient.closed = true; recipient.closed = true;
await Promise.all([ await Promise.all([
recipient.save(), recipient.save(),
emitEvent({ event: "CHANNEL_DELETE", data: channel, user_id: req.user_id } as ChannelDeleteEvent) emitEvent({
event: "CHANNEL_DELETE",
data: channel,
user_id: req.user_id,
} as ChannelDeleteEvent),
]); ]);
} else if (channel.type === ChannelType.GROUP_DM) { } else if (channel.type === ChannelType.GROUP_DM) {
await Channel.removeRecipientFromChannel(channel, req.user_id); await Channel.removeRecipientFromChannel(channel, req.user_id);
} else { } else {
await Promise.all([ await Promise.all([
Channel.delete({ id: channel_id }), Channel.delete({ id: channel_id }),
emitEvent({ event: "CHANNEL_DELETE", data: channel, channel_id } as ChannelDeleteEvent) emitEvent({
event: "CHANNEL_DELETE",
data: channel,
channel_id,
} as ChannelDeleteEvent),
]); ]);
} }
res.send(channel); res.send(channel);
}); },
);
router.patch("/", route({ body: "ChannelModifySchema", permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => { router.patch(
"/",
route({ body: "ChannelModifySchema", permission: "MANAGE_CHANNELS" }),
async (req: Request, res: Response) => {
var payload = req.body as ChannelModifySchema; var payload = req.body as ChannelModifySchema;
const { channel_id } = req.params; const { channel_id } = req.params;
if (payload.icon) payload.icon = await handleFile(`/channel-icons/${channel_id}`, payload.icon); if (payload.icon)
payload.icon = await handleFile(
`/channel-icons/${channel_id}`,
payload.icon,
);
const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); const channel = await Channel.findOneOrFail({
where: { id: channel_id },
});
channel.assign(payload); channel.assign(payload);
await Promise.all([ await Promise.all([
@ -60,11 +92,12 @@ router.patch("/", route({ body: "ChannelModifySchema", permission: "MANAGE_CHANN
emitEvent({ emitEvent({
event: "CHANNEL_UPDATE", event: "CHANNEL_UPDATE",
data: channel, data: channel,
channel_id channel_id,
} as ChannelUpdateEvent) } as ChannelUpdateEvent),
]); ]);
res.send(channel); res.send(channel);
}); },
);
export default router; export default router;

View File

@ -2,16 +2,33 @@ import { Router, Request, Response } from "express";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { route } from "@fosscord/api"; import { route } from "@fosscord/api";
import { random } from "@fosscord/api"; import { random } from "@fosscord/api";
import { Channel, Invite, InviteCreateEvent, emitEvent, User, Guild, PublicInviteRelation } from "@fosscord/util"; import {
Channel,
Invite,
InviteCreateEvent,
emitEvent,
User,
Guild,
PublicInviteRelation,
} from "@fosscord/util";
import { isTextChannel } from "./messages"; import { isTextChannel } from "./messages";
const router: Router = Router(); const router: Router = Router();
router.post("/", route({ body: "InviteCreateSchema", permission: "CREATE_INSTANT_INVITE", right: "CREATE_INVITES" }), router.post(
"/",
route({
body: "InviteCreateSchema",
permission: "CREATE_INSTANT_INVITE",
right: "CREATE_INVITES",
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { user_id } = req; const { user_id } = req;
const { channel_id } = req.params; const { channel_id } = req.params;
const channel = await Channel.findOneOrFail({ where: { id: channel_id }, select: ["id", "name", "type", "guild_id"] }); const channel = await Channel.findOneOrFail({
where: { id: channel_id },
select: ["id", "name", "type", "guild_id"],
});
isTextChannel(channel.type); isTextChannel(channel.type);
if (!channel.guild_id) { if (!channel.guild_id) {
@ -31,30 +48,44 @@ router.post("/", route({ body: "InviteCreateSchema", permission: "CREATE_INSTANT
created_at: new Date(), created_at: new Date(),
guild_id, guild_id,
channel_id: channel_id, channel_id: channel_id,
inviter_id: user_id inviter_id: user_id,
}).save(); }).save();
const data = invite.toJSON(); const data = invite.toJSON();
data.inviter = await User.getPublicUser(req.user_id); data.inviter = await User.getPublicUser(req.user_id);
data.guild = await Guild.findOne({ where: { id: guild_id } }); data.guild = await Guild.findOne({ where: { id: guild_id } });
data.channel = channel; data.channel = channel;
await emitEvent({ event: "INVITE_CREATE", data, guild_id } as InviteCreateEvent); await emitEvent({
event: "INVITE_CREATE",
data,
guild_id,
} as InviteCreateEvent);
res.status(201).send(data); res.status(201).send(data);
}); },
);
router.get("/", route({ permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => { router.get(
"/",
route({ permission: "MANAGE_CHANNELS" }),
async (req: Request, res: Response) => {
const { user_id } = req; const { user_id } = req;
const { channel_id } = req.params; const { channel_id } = req.params;
const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); const channel = await Channel.findOneOrFail({
where: { id: channel_id },
});
if (!channel.guild_id) { if (!channel.guild_id) {
throw new HTTPError("This channel doesn't exist", 404); throw new HTTPError("This channel doesn't exist", 404);
} }
const { guild_id } = channel; const { guild_id } = channel;
const invites = await Invite.find({ where: { guild_id }, relations: PublicInviteRelation }); const invites = await Invite.find({
where: { guild_id },
res.status(200).send(invites); relations: PublicInviteRelation,
}); });
res.status(200).send(invites);
},
);
export default router; export default router;

View File

@ -1,4 +1,9 @@
import { emitEvent, getPermission, MessageAckEvent, ReadState } from "@fosscord/util"; import {
emitEvent,
getPermission,
MessageAckEvent,
ReadState,
} from "@fosscord/util";
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
import { route } from "@fosscord/api"; import { route } from "@fosscord/api";
@ -8,14 +13,24 @@ const router = Router();
// TODO: send read state event to all channel members // TODO: send read state event to all channel members
// TODO: advance-only notification cursor // TODO: advance-only notification cursor
router.post("/", route({ body: "MessageAcknowledgeSchema" }), async (req: Request, res: Response) => { router.post(
"/",
route({ body: "MessageAcknowledgeSchema" }),
async (req: Request, res: Response) => {
const { channel_id, message_id } = req.params; const { channel_id, message_id } = req.params;
const permission = await getPermission(req.user_id, undefined, channel_id); const permission = await getPermission(
req.user_id,
undefined,
channel_id,
);
permission.hasThrow("VIEW_CHANNEL"); permission.hasThrow("VIEW_CHANNEL");
let read_state = await ReadState.findOne({ where: { user_id: req.user_id, channel_id } }); let read_state = await ReadState.findOne({
if (!read_state) read_state = ReadState.create({ user_id: req.user_id, channel_id }); where: { user_id: req.user_id, channel_id },
});
if (!read_state)
read_state = ReadState.create({ user_id: req.user_id, channel_id });
read_state.last_message_id = message_id; read_state.last_message_id = message_id;
await read_state.save(); await read_state.save();
@ -26,11 +41,12 @@ router.post("/", route({ body: "MessageAcknowledgeSchema" }), async (req: Reques
data: { data: {
channel_id, channel_id,
message_id, message_id,
version: 3763 version: 3763,
} },
} as MessageAckEvent); } as MessageAckEvent);
res.json({ token: null }); res.json({ token: null });
}); },
);
export default router; export default router;

View File

@ -3,14 +3,23 @@ import { route } from "@fosscord/api";
const router = Router(); const router = Router();
router.post("/", route({ permission: "MANAGE_MESSAGES" }), (req: Request, res: Response) => { router.post(
"/",
route({ permission: "MANAGE_MESSAGES" }),
(req: Request, res: Response) => {
// TODO: // TODO:
res.json({ res.json({
id: "", id: "",
type: 0, type: 0,
content: "", content: "",
channel_id: "", channel_id: "",
author: { id: "", username: "", avatar: "", discriminator: "", public_flags: 64 }, author: {
id: "",
username: "",
avatar: "",
discriminator: "",
public_flags: 64,
},
attachments: [], attachments: [],
embeds: [], embeds: [],
mentions: [], mentions: [],
@ -21,8 +30,9 @@ router.post("/", route({ permission: "MANAGE_MESSAGES" }), (req: Request, res: R
timestamp: "", timestamp: "",
edited_timestamp: null, edited_timestamp: null,
flags: 1, flags: 1,
components: [] components: [],
}).status(200); }).status(200);
}); },
);
export default router; export default router;

View File

@ -26,22 +26,36 @@ const messageUpload = multer({
limits: { limits: {
fileSize: 1024 * 1024 * 100, fileSize: 1024 * 1024 * 100,
fields: 10, fields: 10,
files: 1 files: 1,
}, },
storage: multer.memoryStorage() storage: multer.memoryStorage(),
}); // max upload 50 mb }); // max upload 50 mb
router.patch("/", route({ body: "MessageCreateSchema", permission: "SEND_MESSAGES", right: "SEND_MESSAGES" }), async (req: Request, res: Response) => { router.patch(
"/",
route({
body: "MessageCreateSchema",
permission: "SEND_MESSAGES",
right: "SEND_MESSAGES",
}),
async (req: Request, res: Response) => {
const { message_id, channel_id } = req.params; const { message_id, channel_id } = req.params;
var body = req.body as MessageCreateSchema; var body = req.body as MessageCreateSchema;
const message = await Message.findOneOrFail({ where: { id: message_id, channel_id }, relations: ["attachments"] }); const message = await Message.findOneOrFail({
where: { id: message_id, channel_id },
relations: ["attachments"],
});
const permissions = await getPermission(req.user_id, undefined, channel_id); const permissions = await getPermission(
req.user_id,
undefined,
channel_id,
);
const rights = await getRights(req.user_id); const rights = await getRights(req.user_id);
if ((req.user_id !== message.author_id)) { if (req.user_id !== message.author_id) {
if (!rights.has("MANAGE_MESSAGES")) { if (!rights.has("MANAGE_MESSAGES")) {
permissions.hasThrow("MANAGE_MESSAGES"); permissions.hasThrow("MANAGE_MESSAGES");
body = { flags: body.flags }; body = { flags: body.flags };
@ -58,7 +72,7 @@ router.patch("/", route({ body: "MessageCreateSchema", permission: "SEND_MESSAGE
author_id: message.author_id, author_id: message.author_id,
channel_id, channel_id,
id: message_id, id: message_id,
edited_timestamp: new Date() edited_timestamp: new Date(),
}); });
await Promise.all([ await Promise.all([
@ -66,15 +80,15 @@ router.patch("/", route({ body: "MessageCreateSchema", permission: "SEND_MESSAGE
await emitEvent({ await emitEvent({
event: "MESSAGE_UPDATE", event: "MESSAGE_UPDATE",
channel_id, channel_id,
data: { ...new_message, nonce: undefined } data: { ...new_message, nonce: undefined },
} as MessageUpdateEvent) } as MessageUpdateEvent),
]); ]);
postHandleMessage(message); postHandleMessage(message);
return res.json(message); return res.json(message);
}); },
);
// Backfill message with specific timestamp // Backfill message with specific timestamp
router.put( router.put(
@ -87,7 +101,11 @@ router.put(
next(); next();
}, },
route({ body: "MessageCreateSchema", permission: "SEND_MESSAGES", right: "SEND_BACKDATED_EVENTS" }), route({
body: "MessageCreateSchema",
permission: "SEND_MESSAGES",
right: "SEND_BACKDATED_EVENTS",
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { channel_id, message_id } = req.params; const { channel_id, message_id } = req.params;
var body = req.body as MessageCreateSchema; var body = req.body as MessageCreateSchema;
@ -107,20 +125,30 @@ router.put(
throw FosscordApiErrors.CANNOT_BACKFILL_TO_THE_FUTURE; throw FosscordApiErrors.CANNOT_BACKFILL_TO_THE_FUTURE;
} }
const exists = await Message.findOne({ where: { id: message_id, channel_id: channel_id } }); const exists = await Message.findOne({
where: { id: message_id, channel_id: channel_id },
});
if (exists) { if (exists) {
throw FosscordApiErrors.CANNOT_REPLACE_BY_BACKFILL; throw FosscordApiErrors.CANNOT_REPLACE_BY_BACKFILL;
} }
if (req.file) { if (req.file) {
try { try {
const file = await uploadFile(`/attachments/${req.params.channel_id}`, req.file); const file = await uploadFile(
attachments.push(Attachment.create({ ...file, proxy_url: file.url })); `/attachments/${req.params.channel_id}`,
req.file,
);
attachments.push(
Attachment.create({ ...file, proxy_url: file.url }),
);
} catch (error) { } catch (error) {
return res.status(400).json(error); return res.status(400).json(error);
} }
} }
const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients", "recipients.user"] }); const channel = await Channel.findOneOrFail({
where: { id: channel_id },
relations: ["recipients", "recipients.user"],
});
const embeds = body.embeds || []; const embeds = body.embeds || [];
if (body.embed) embeds.push(body.embed); if (body.embed) embeds.push(body.embed);
@ -142,27 +170,43 @@ router.put(
await Promise.all([ await Promise.all([
message.save(), message.save(),
emitEvent({ event: "MESSAGE_CREATE", channel_id: channel_id, data: message } as MessageCreateEvent), emitEvent({
channel.save() event: "MESSAGE_CREATE",
channel_id: channel_id,
data: message,
} as MessageCreateEvent),
channel.save(),
]); ]);
postHandleMessage(message).catch((e) => {}); // no await as it shouldnt block the message send function and silently catch error postHandleMessage(message).catch((e) => {}); // no await as it shouldnt block the message send function and silently catch error
return res.json(message); return res.json(message);
} },
); );
router.get("/", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: Response) => { router.get(
"/",
route({ permission: "VIEW_CHANNEL" }),
async (req: Request, res: Response) => {
const { message_id, channel_id } = req.params; const { message_id, channel_id } = req.params;
const message = await Message.findOneOrFail({ where: { id: message_id, channel_id }, relations: ["attachments"] }); const message = await Message.findOneOrFail({
where: { id: message_id, channel_id },
relations: ["attachments"],
});
const permissions = await getPermission(req.user_id, undefined, channel_id); const permissions = await getPermission(
req.user_id,
undefined,
channel_id,
);
if (message.author_id !== req.user_id) permissions.hasThrow("READ_MESSAGE_HISTORY"); if (message.author_id !== req.user_id)
permissions.hasThrow("READ_MESSAGE_HISTORY");
return res.json(message); return res.json(message);
}); },
);
router.delete("/", route({}), async (req: Request, res: Response) => { router.delete("/", route({}), async (req: Request, res: Response) => {
const { message_id, channel_id } = req.params; const { message_id, channel_id } = req.params;
@ -172,9 +216,13 @@ router.delete("/", route({}), async (req: Request, res: Response) => {
const rights = await getRights(req.user_id); const rights = await getRights(req.user_id);
if ((message.author_id !== req.user_id)) { if (message.author_id !== req.user_id) {
if (!rights.has("MANAGE_MESSAGES")) { if (!rights.has("MANAGE_MESSAGES")) {
const permission = await getPermission(req.user_id, channel.guild_id, channel_id); const permission = await getPermission(
req.user_id,
channel.guild_id,
channel_id,
);
permission.hasThrow("MANAGE_MESSAGES"); permission.hasThrow("MANAGE_MESSAGES");
} }
} else rights.hasThrow("SELF_DELETE_MESSAGES"); } else rights.hasThrow("SELF_DELETE_MESSAGES");
@ -187,8 +235,8 @@ router.delete("/", route({}), async (req: Request, res: Response) => {
data: { data: {
id: message_id, id: message_id,
channel_id, channel_id,
guild_id: channel.guild_id guild_id: channel.guild_id,
} },
} as MessageDeleteEvent); } as MessageDeleteEvent);
res.sendStatus(204); res.sendStatus(204);

View File

@ -11,7 +11,7 @@ import {
MessageReactionRemoveEvent, MessageReactionRemoveEvent,
PartialEmoji, PartialEmoji,
PublicUserProjection, PublicUserProjection,
User User,
} from "@fosscord/util"; } from "@fosscord/util";
import { route } from "@fosscord/api"; import { route } from "@fosscord/api";
import { Router, Response, Request } from "express"; import { Router, Response, Request } from "express";
@ -27,19 +27,24 @@ function getEmoji(emoji: string): PartialEmoji {
if (parts) if (parts)
return { return {
name: parts[0], name: parts[0],
id: parts[1] id: parts[1],
}; };
return { return {
id: undefined, id: undefined,
name: emoji name: emoji,
}; };
} }
router.delete("/", route({ permission: "MANAGE_MESSAGES" }), async (req: Request, res: Response) => { router.delete(
"/",
route({ permission: "MANAGE_MESSAGES" }),
async (req: Request, res: Response) => {
const { message_id, channel_id } = req.params; const { message_id, channel_id } = req.params;
const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); const channel = await Channel.findOneOrFail({
where: { id: channel_id },
});
await Message.update({ id: message_id, channel_id }, { reactions: [] }); await Message.update({ id: message_id, channel_id }, { reactions: [] });
@ -49,20 +54,30 @@ router.delete("/", route({ permission: "MANAGE_MESSAGES" }), async (req: Request
data: { data: {
channel_id, channel_id,
message_id, message_id,
guild_id: channel.guild_id guild_id: channel.guild_id,
} },
} as MessageReactionRemoveAllEvent); } as MessageReactionRemoveAllEvent);
res.sendStatus(204); res.sendStatus(204);
}); },
);
router.delete("/:emoji", route({ permission: "MANAGE_MESSAGES" }), async (req: Request, res: Response) => { router.delete(
"/:emoji",
route({ permission: "MANAGE_MESSAGES" }),
async (req: Request, res: Response) => {
const { message_id, channel_id } = req.params; const { message_id, channel_id } = req.params;
const emoji = getEmoji(req.params.emoji); const emoji = getEmoji(req.params.emoji);
const message = await Message.findOneOrFail({ where: { id: message_id, channel_id } }); const message = await Message.findOneOrFail({
where: { id: message_id, channel_id },
});
const already_added = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name); const already_added = message.reactions.find(
(x) =>
(x.emoji.id === emoji.id && emoji.id) ||
x.emoji.name === emoji.name,
);
if (!already_added) throw new HTTPError("Reaction not found", 404); if (!already_added) throw new HTTPError("Reaction not found", 404);
message.reactions.remove(already_added); message.reactions.remove(already_added);
@ -75,58 +90,90 @@ router.delete("/:emoji", route({ permission: "MANAGE_MESSAGES" }), async (req: R
channel_id, channel_id,
message_id, message_id,
guild_id: message.guild_id, guild_id: message.guild_id,
emoji emoji,
} },
} as MessageReactionRemoveEmojiEvent) } as MessageReactionRemoveEmojiEvent),
]); ]);
res.sendStatus(204); res.sendStatus(204);
}); },
);
router.get("/:emoji", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: Response) => { router.get(
"/:emoji",
route({ permission: "VIEW_CHANNEL" }),
async (req: Request, res: Response) => {
const { message_id, channel_id } = req.params; const { message_id, channel_id } = req.params;
const emoji = getEmoji(req.params.emoji); const emoji = getEmoji(req.params.emoji);
const message = await Message.findOneOrFail({ where: { id: message_id, channel_id } }); const message = await Message.findOneOrFail({
const reaction = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name); where: { id: message_id, channel_id },
});
const reaction = message.reactions.find(
(x) =>
(x.emoji.id === emoji.id && emoji.id) ||
x.emoji.name === emoji.name,
);
if (!reaction) throw new HTTPError("Reaction not found", 404); if (!reaction) throw new HTTPError("Reaction not found", 404);
const users = await User.find({ const users = await User.find({
where: { where: {
id: In(reaction.user_ids) id: In(reaction.user_ids),
}, },
select: PublicUserProjection select: PublicUserProjection,
}); });
res.json(users); res.json(users);
}); },
);
router.put("/:emoji/:user_id", route({ permission: "READ_MESSAGE_HISTORY", right: "SELF_ADD_REACTIONS" }), async (req: Request, res: Response) => { router.put(
"/:emoji/:user_id",
route({ permission: "READ_MESSAGE_HISTORY", right: "SELF_ADD_REACTIONS" }),
async (req: Request, res: Response) => {
const { message_id, channel_id, user_id } = req.params; const { message_id, channel_id, user_id } = req.params;
if (user_id !== "@me") throw new HTTPError("Invalid user"); if (user_id !== "@me") throw new HTTPError("Invalid user");
const emoji = getEmoji(req.params.emoji); const emoji = getEmoji(req.params.emoji);
const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); const channel = await Channel.findOneOrFail({
const message = await Message.findOneOrFail({ where: { id: message_id, channel_id } }); where: { id: channel_id },
const already_added = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name); });
const message = await Message.findOneOrFail({
where: { id: message_id, channel_id },
});
const already_added = message.reactions.find(
(x) =>
(x.emoji.id === emoji.id && emoji.id) ||
x.emoji.name === emoji.name,
);
if (!already_added) req.permission!.hasThrow("ADD_REACTIONS"); if (!already_added) req.permission!.hasThrow("ADD_REACTIONS");
if (emoji.id) { if (emoji.id) {
const external_emoji = await Emoji.findOneOrFail({ where: { id: emoji.id } }); const external_emoji = await Emoji.findOneOrFail({
where: { id: emoji.id },
});
if (!already_added) req.permission!.hasThrow("USE_EXTERNAL_EMOJIS"); if (!already_added) req.permission!.hasThrow("USE_EXTERNAL_EMOJIS");
emoji.animated = external_emoji.animated; emoji.animated = external_emoji.animated;
emoji.name = external_emoji.name; emoji.name = external_emoji.name;
} }
if (already_added) { if (already_added) {
if (already_added.user_ids.includes(req.user_id)) return res.sendStatus(204); // Do not throw an error ¯\_(ツ)_/¯ as discord also doesn't throw any error if (already_added.user_ids.includes(req.user_id))
return res.sendStatus(204); // Do not throw an error ¯\_(ツ)_/¯ as discord also doesn't throw any error
already_added.count++; already_added.count++;
} else message.reactions.push({ count: 1, emoji, user_ids: [req.user_id] }); } else
message.reactions.push({
count: 1,
emoji,
user_ids: [req.user_id],
});
await message.save(); await message.save();
const member = channel.guild_id && (await Member.findOneOrFail({ where: { id: req.user_id } })); const member =
channel.guild_id &&
(await Member.findOneOrFail({ where: { id: req.user_id } }));
await emitEvent({ await emitEvent({
event: "MESSAGE_REACTION_ADD", event: "MESSAGE_REACTION_ADD",
@ -137,29 +184,46 @@ router.put("/:emoji/:user_id", route({ permission: "READ_MESSAGE_HISTORY", right
message_id, message_id,
guild_id: channel.guild_id, guild_id: channel.guild_id,
emoji, emoji,
member member,
} },
} as MessageReactionAddEvent); } as MessageReactionAddEvent);
res.sendStatus(204); res.sendStatus(204);
}); },
);
router.delete("/:emoji/:user_id", route({}), async (req: Request, res: Response) => { router.delete(
"/:emoji/:user_id",
route({}),
async (req: Request, res: Response) => {
var { message_id, channel_id, user_id } = req.params; var { message_id, channel_id, user_id } = req.params;
const emoji = getEmoji(req.params.emoji); const emoji = getEmoji(req.params.emoji);
const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); const channel = await Channel.findOneOrFail({
const message = await Message.findOneOrFail({ where: { id: message_id, channel_id } }); where: { id: channel_id },
});
const message = await Message.findOneOrFail({
where: { id: message_id, channel_id },
});
if (user_id === "@me") user_id = req.user_id; if (user_id === "@me") user_id = req.user_id;
else { else {
const permissions = await getPermission(req.user_id, undefined, channel_id); const permissions = await getPermission(
req.user_id,
undefined,
channel_id,
);
permissions.hasThrow("MANAGE_MESSAGES"); permissions.hasThrow("MANAGE_MESSAGES");
} }
const already_added = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name); const already_added = message.reactions.find(
if (!already_added || !already_added.user_ids.includes(user_id)) throw new HTTPError("Reaction not found", 404); (x) =>
(x.emoji.id === emoji.id && emoji.id) ||
x.emoji.name === emoji.name,
);
if (!already_added || !already_added.user_ids.includes(user_id))
throw new HTTPError("Reaction not found", 404);
already_added.count--; already_added.count--;
@ -175,11 +239,12 @@ router.delete("/:emoji/:user_id", route({}), async (req: Request, res: Response)
channel_id, channel_id,
message_id, message_id,
guild_id: channel.guild_id, guild_id: channel.guild_id,
emoji emoji,
} },
} as MessageReactionRemoveEvent); } as MessageReactionRemoveEvent);
res.sendStatus(204); res.sendStatus(204);
}); },
);
export default router; export default router;

View File

@ -1,5 +1,13 @@
import { Router, Response, Request } from "express"; import { Router, Response, Request } from "express";
import { Channel, Config, emitEvent, getPermission, getRights, MessageDeleteBulkEvent, Message } from "@fosscord/util"; import {
Channel,
Config,
emitEvent,
getPermission,
getRights,
MessageDeleteBulkEvent,
Message,
} from "@fosscord/util";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { route } from "@fosscord/api"; import { route } from "@fosscord/api";
@ -10,24 +18,38 @@ export default router;
// should users be able to bulk delete messages or only bots? ANSWER: all users // should users be able to bulk delete messages or only bots? ANSWER: all users
// should this request fail, if you provide messages older than 14 days/invalid ids? ANSWER: NO // should this request fail, if you provide messages older than 14 days/invalid ids? ANSWER: NO
// https://discord.com/developers/docs/resources/channel#bulk-delete-messages // https://discord.com/developers/docs/resources/channel#bulk-delete-messages
router.post("/", route({ body: "BulkDeleteSchema" }), async (req: Request, res: Response) => { router.post(
"/",
route({ body: "BulkDeleteSchema" }),
async (req: Request, res: Response) => {
const { channel_id } = req.params; const { channel_id } = req.params;
const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); const channel = await Channel.findOneOrFail({
if (!channel.guild_id) throw new HTTPError("Can't bulk delete dm channel messages", 400); where: { id: channel_id },
});
if (!channel.guild_id)
throw new HTTPError("Can't bulk delete dm channel messages", 400);
const rights = await getRights(req.user_id); const rights = await getRights(req.user_id);
rights.hasThrow("SELF_DELETE_MESSAGES"); rights.hasThrow("SELF_DELETE_MESSAGES");
let superuser = rights.has("MANAGE_MESSAGES"); let superuser = rights.has("MANAGE_MESSAGES");
const permission = await getPermission(req.user_id, channel?.guild_id, channel_id); const permission = await getPermission(
req.user_id,
channel?.guild_id,
channel_id,
);
const { maxBulkDelete } = Config.get().limits.message; const { maxBulkDelete } = Config.get().limits.message;
const { messages } = req.body as { messages: string[] }; const { messages } = req.body as { messages: string[] };
if (messages.length === 0) throw new HTTPError("You must specify messages to bulk delete"); if (messages.length === 0)
throw new HTTPError("You must specify messages to bulk delete");
if (!superuser) { if (!superuser) {
permission.hasThrow("MANAGE_MESSAGES"); permission.hasThrow("MANAGE_MESSAGES");
if (messages.length > maxBulkDelete) throw new HTTPError(`You cannot delete more than ${maxBulkDelete} messages`); if (messages.length > maxBulkDelete)
throw new HTTPError(
`You cannot delete more than ${maxBulkDelete} messages`,
);
} }
await Message.delete(messages); await Message.delete(messages);
@ -35,8 +57,9 @@ router.post("/", route({ body: "BulkDeleteSchema" }), async (req: Request, res:
await emitEvent({ await emitEvent({
event: "MESSAGE_DELETE_BULK", event: "MESSAGE_DELETE_BULK",
channel_id, channel_id,
data: { ids: messages, channel_id, guild_id: channel.guild_id } data: { ids: messages, channel_id, guild_id: channel.guild_id },
} as MessageDeleteBulkEvent); } as MessageDeleteBulkEvent);
res.sendStatus(204); res.sendStatus(204);
}); },
);

View File

@ -61,33 +61,47 @@ router.get("/", async (req: Request, res: Response) => {
const before = req.query.before ? `${req.query.before}` : undefined; const before = req.query.before ? `${req.query.before}` : undefined;
const after = req.query.after ? `${req.query.after}` : undefined; const after = req.query.after ? `${req.query.after}` : undefined;
const limit = Number(req.query.limit) || 50; const limit = Number(req.query.limit) || 50;
if (limit < 1 || limit > 100) throw new HTTPError("limit must be between 1 and 100", 422); if (limit < 1 || limit > 100)
throw new HTTPError("limit must be between 1 and 100", 422);
var halfLimit = Math.floor(limit / 2); var halfLimit = Math.floor(limit / 2);
const permissions = await getPermission(req.user_id, channel.guild_id, channel_id); const permissions = await getPermission(
req.user_id,
channel.guild_id,
channel_id,
);
permissions.hasThrow("VIEW_CHANNEL"); permissions.hasThrow("VIEW_CHANNEL");
if (!permissions.has("READ_MESSAGE_HISTORY")) return res.json([]); if (!permissions.has("READ_MESSAGE_HISTORY")) return res.json([]);
var query: FindManyOptions<Message> & { where: { id?: any; }; } = { var query: FindManyOptions<Message> & { where: { id?: any } } = {
order: { timestamp: "DESC" }, order: { timestamp: "DESC" },
take: limit, take: limit,
where: { channel_id }, where: { channel_id },
relations: ["author", "webhook", "application", "mentions", "mention_roles", "mention_channels", "sticker_items", "attachments"] relations: [
"author",
"webhook",
"application",
"mentions",
"mention_roles",
"mention_channels",
"sticker_items",
"attachments",
],
}; };
if (after) { if (after) {
if (BigInt(after) > BigInt(Snowflake.generate())) return res.status(422); if (BigInt(after) > BigInt(Snowflake.generate()))
return res.status(422);
query.where.id = MoreThan(after); query.where.id = MoreThan(after);
} } else if (before) {
else if (before) { if (BigInt(before) < BigInt(req.params.channel_id))
if (BigInt(before) < BigInt(req.params.channel_id)) return res.status(422); return res.status(422);
query.where.id = LessThan(before); query.where.id = LessThan(before);
} } else if (around) {
else if (around) {
query.where.id = [ query.where.id = [
MoreThan((BigInt(around) - BigInt(halfLimit)).toString()), MoreThan((BigInt(around) - BigInt(halfLimit)).toString()),
LessThan((BigInt(around) + BigInt(halfLimit)).toString()) LessThan((BigInt(around) + BigInt(halfLimit)).toString()),
]; ];
return res.json([]); // TODO: fix around return res.json([]); // TODO: fix around
@ -105,11 +119,22 @@ router.get("/", async (req: Request, res: Response) => {
delete x.user_ids; delete x.user_ids;
}); });
// @ts-ignore // @ts-ignore
if (!x.author) x.author = { id: "4", discriminator: "0000", username: "Fosscord Ghost", public_flags: "0", avatar: null }; if (!x.author)
x.author = {
id: "4",
discriminator: "0000",
username: "Fosscord Ghost",
public_flags: "0",
avatar: null,
};
x.attachments?.forEach((y: any) => { x.attachments?.forEach((y: any) => {
// dynamically set attachment proxy_url in case the endpoint changed // dynamically set attachment proxy_url in case the endpoint changed
const uri = y.proxy_url.startsWith("http") ? y.proxy_url : `https://example.org${y.proxy_url}`; const uri = y.proxy_url.startsWith("http")
y.proxy_url = `${endpoint == null ? "" : endpoint}${new URL(uri).pathname}`; ? y.proxy_url
: `https://example.org${y.proxy_url}`;
y.proxy_url = `${endpoint == null ? "" : endpoint}${
new URL(uri).pathname
}`;
}); });
/** /**
@ -123,7 +148,7 @@ router.get("/", async (req: Request, res: Response) => {
// } // }
return x; return x;
}) }),
); );
}); });
@ -134,7 +159,7 @@ const messageUpload = multer({
fields: 10, fields: 10,
// files: 1 // files: 1
}, },
storage: multer.memoryStorage() storage: multer.memoryStorage(),
}); // max upload 50 mb }); // max upload 50 mb
/** /**
TODO: dynamically change limit of MessageCreateSchema with config TODO: dynamically change limit of MessageCreateSchema with config
@ -155,24 +180,38 @@ router.post(
next(); next();
}, },
route({ body: "MessageCreateSchema", permission: "SEND_MESSAGES", right: "SEND_MESSAGES" }), route({
body: "MessageCreateSchema",
permission: "SEND_MESSAGES",
right: "SEND_MESSAGES",
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { channel_id } = req.params; const { channel_id } = req.params;
var body = req.body as MessageCreateSchema; var body = req.body as MessageCreateSchema;
const attachments: Attachment[] = []; const attachments: Attachment[] = [];
const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients", "recipients.user"] }); const channel = await Channel.findOneOrFail({
where: { id: channel_id },
relations: ["recipients", "recipients.user"],
});
if (!channel.isWritable()) { if (!channel.isWritable()) {
throw new HTTPError(`Cannot send messages to channel of type ${channel.type}`, 400); throw new HTTPError(
`Cannot send messages to channel of type ${channel.type}`,
400,
);
} }
const files = req.files as Express.Multer.File[] ?? []; const files = (req.files as Express.Multer.File[]) ?? [];
for (var currFile of files) { for (var currFile of files) {
try { try {
const file = await uploadFile(`/attachments/${channel.id}`, currFile); const file = await uploadFile(
attachments.push(Attachment.create({ ...file, proxy_url: file.url })); `/attachments/${channel.id}`,
} currFile,
catch (error) { );
attachments.push(
Attachment.create({ ...file, proxy_url: file.url }),
);
} catch (error) {
return res.status(400).json(error); return res.status(400).json(error);
} }
} }
@ -188,7 +227,7 @@ router.post(
channel_id, channel_id,
attachments, attachments,
edited_timestamp: undefined, edited_timestamp: undefined,
timestamp: new Date() timestamp: new Date(),
}); });
channel.last_message_id = message.id; channel.last_message_id = message.id;
@ -205,32 +244,47 @@ router.post(
recipient.save(), recipient.save(),
emitEvent({ emitEvent({
event: "CHANNEL_CREATE", event: "CHANNEL_CREATE",
data: channel_dto.excludedRecipients([recipient.user_id]), data: channel_dto.excludedRecipients([
user_id: recipient.user_id recipient.user_id,
}) ]),
user_id: recipient.user_id,
}),
]); ]);
} }
}) }),
); );
} }
const member = await Member.findOneOrFail({ where: { id: req.user_id }, relations: ["roles"] }); const member = await Member.findOneOrFail({
member.roles = member.roles.filter((role: Role) => { where: { id: req.user_id },
relations: ["roles"],
});
member.roles = member.roles
.filter((role: Role) => {
return role.id !== role.guild_id; return role.id !== role.guild_id;
}).map((role: Role) => { })
.map((role: Role) => {
return role.id; return role.id;
}) as any; }) as any;
await Promise.all([ await Promise.all([
message.save(), message.save(),
emitEvent({ event: "MESSAGE_CREATE", channel_id: channel_id, data: message } as MessageCreateEvent), emitEvent({
message.guild_id ? Member.update({ id: req.user_id, guild_id: message.guild_id }, { last_message_id: message.id }) : null, event: "MESSAGE_CREATE",
channel.save() channel_id: channel_id,
data: message,
} as MessageCreateEvent),
message.guild_id
? Member.update(
{ id: req.user_id, guild_id: message.guild_id },
{ last_message_id: message.id },
)
: null,
channel.save(),
]); ]);
postHandleMessage(message).catch((e) => {}); // no await as it shouldnt block the message send function and silently catch error postHandleMessage(message).catch((e) => {}); // no await as it shouldnt block the message send function and silently catch error
return res.json(message); return res.json(message);
} },
); );

View File

@ -6,7 +6,7 @@ import {
emitEvent, emitEvent,
getPermission, getPermission,
Member, Member,
Role Role,
} from "@fosscord/util"; } from "@fosscord/util";
import { Router, Response, Request } from "express"; import { Router, Response, Request } from "express";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
@ -16,69 +16,90 @@ const router: Router = Router();
// TODO: Only permissions your bot has in the guild or channel can be allowed/denied (unless your bot has a MANAGE_ROLES overwrite in the channel) // TODO: Only permissions your bot has in the guild or channel can be allowed/denied (unless your bot has a MANAGE_ROLES overwrite in the channel)
export interface ChannelPermissionOverwriteSchema extends ChannelPermissionOverwrite { } export interface ChannelPermissionOverwriteSchema
extends ChannelPermissionOverwrite {}
router.put( router.put(
"/:overwrite_id", "/:overwrite_id",
route({ body: "ChannelPermissionOverwriteSchema", permission: "MANAGE_ROLES" }), route({
body: "ChannelPermissionOverwriteSchema",
permission: "MANAGE_ROLES",
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { channel_id, overwrite_id } = req.params; const { channel_id, overwrite_id } = req.params;
const body = req.body as ChannelPermissionOverwriteSchema; const body = req.body as ChannelPermissionOverwriteSchema;
var channel = await Channel.findOneOrFail({ where: { id: channel_id } }); var channel = await Channel.findOneOrFail({
where: { id: channel_id },
});
if (!channel.guild_id) throw new HTTPError("Channel not found", 404); if (!channel.guild_id) throw new HTTPError("Channel not found", 404);
if (body.type === 0) { if (body.type === 0) {
if (!(await Role.count({ where: { id: overwrite_id } }))) throw new HTTPError("role not found", 404); if (!(await Role.count({ where: { id: overwrite_id } })))
throw new HTTPError("role not found", 404);
} else if (body.type === 1) { } else if (body.type === 1) {
if (!(await Member.count({ where: { id: overwrite_id } }))) throw new HTTPError("user not found", 404); if (!(await Member.count({ where: { id: overwrite_id } })))
throw new HTTPError("user not found", 404);
} else throw new HTTPError("type not supported", 501); } else throw new HTTPError("type not supported", 501);
//@ts-ignore //@ts-ignore
var overwrite: ChannelPermissionOverwrite = channel.permission_overwrites.find((x) => x.id === overwrite_id); var overwrite: ChannelPermissionOverwrite =
channel.permission_overwrites?.find((x) => x.id === overwrite_id);
if (!overwrite) { if (!overwrite) {
// @ts-ignore // @ts-ignore
overwrite = { overwrite = {
id: overwrite_id, id: overwrite_id,
type: body.type type: body.type,
}; };
channel.permission_overwrites!.push(overwrite); channel.permission_overwrites!.push(overwrite);
} }
overwrite.allow = String(req.permission!.bitfield & (BigInt(body.allow) || BigInt("0"))); overwrite.allow = String(
overwrite.deny = String(req.permission!.bitfield & (BigInt(body.deny) || BigInt("0"))); req.permission!.bitfield & (BigInt(body.allow) || BigInt("0")),
);
overwrite.deny = String(
req.permission!.bitfield & (BigInt(body.deny) || BigInt("0")),
);
await Promise.all([ await Promise.all([
channel.save(), channel.save(),
emitEvent({ emitEvent({
event: "CHANNEL_UPDATE", event: "CHANNEL_UPDATE",
channel_id, channel_id,
data: channel data: channel,
} as ChannelUpdateEvent) } as ChannelUpdateEvent),
]); ]);
return res.sendStatus(204); return res.sendStatus(204);
} },
); );
// TODO: check permission hierarchy // TODO: check permission hierarchy
router.delete("/:overwrite_id", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { router.delete(
"/:overwrite_id",
route({ permission: "MANAGE_ROLES" }),
async (req: Request, res: Response) => {
const { channel_id, overwrite_id } = req.params; const { channel_id, overwrite_id } = req.params;
const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); const channel = await Channel.findOneOrFail({
where: { id: channel_id },
});
if (!channel.guild_id) throw new HTTPError("Channel not found", 404); if (!channel.guild_id) throw new HTTPError("Channel not found", 404);
channel.permission_overwrites = channel.permission_overwrites!.filter((x) => x.id === overwrite_id); channel.permission_overwrites = channel.permission_overwrites!.filter(
(x) => x.id === overwrite_id,
);
await Promise.all([ await Promise.all([
channel.save(), channel.save(),
emitEvent({ emitEvent({
event: "CHANNEL_UPDATE", event: "CHANNEL_UPDATE",
channel_id, channel_id,
data: channel data: channel,
} as ChannelUpdateEvent) } as ChannelUpdateEvent),
]); ]);
return res.sendStatus(204); return res.sendStatus(204);
}); },
);
export default router; export default router;

View File

@ -6,7 +6,7 @@ import {
getPermission, getPermission,
Message, Message,
MessageUpdateEvent, MessageUpdateEvent,
DiscordApiErrors DiscordApiErrors,
} from "@fosscord/util"; } from "@fosscord/util";
import { Router, Request, Response } from "express"; import { Router, Request, Response } from "express";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
@ -14,24 +14,32 @@ import { route } from "@fosscord/api";
const router: Router = Router(); const router: Router = Router();
router.put("/:message_id", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: Response) => { router.put(
"/:message_id",
route({ permission: "VIEW_CHANNEL" }),
async (req: Request, res: Response) => {
const { channel_id, message_id } = req.params; const { channel_id, message_id } = req.params;
const message = await Message.findOneOrFail({ where: { id: message_id } }); const message = await Message.findOneOrFail({
where: { id: message_id },
});
// * in dm channels anyone can pin messages -> only check for guilds // * in dm channels anyone can pin messages -> only check for guilds
if (message.guild_id) req.permission!.hasThrow("MANAGE_MESSAGES"); if (message.guild_id) req.permission!.hasThrow("MANAGE_MESSAGES");
const pinned_count = await Message.count({ where: { channel: { id: channel_id }, pinned: true } }); const pinned_count = await Message.count({
where: { channel: { id: channel_id }, pinned: true },
});
const { maxPins } = Config.get().limits.channel; const { maxPins } = Config.get().limits.channel;
if (pinned_count >= maxPins) throw DiscordApiErrors.MAXIMUM_PINS.withParams(maxPins); if (pinned_count >= maxPins)
throw DiscordApiErrors.MAXIMUM_PINS.withParams(maxPins);
await Promise.all([ await Promise.all([
Message.update({ id: message_id }, { pinned: true }), Message.update({ id: message_id }, { pinned: true }),
emitEvent({ emitEvent({
event: "MESSAGE_UPDATE", event: "MESSAGE_UPDATE",
channel_id, channel_id,
data: message data: message,
} as MessageUpdateEvent), } as MessageUpdateEvent),
emitEvent({ emitEvent({
event: "CHANNEL_PINS_UPDATE", event: "CHANNEL_PINS_UPDATE",
@ -39,21 +47,29 @@ router.put("/:message_id", route({ permission: "VIEW_CHANNEL" }), async (req: Re
data: { data: {
channel_id, channel_id,
guild_id: message.guild_id, guild_id: message.guild_id,
last_pin_timestamp: undefined last_pin_timestamp: undefined,
} },
} as ChannelPinsUpdateEvent) } as ChannelPinsUpdateEvent),
]); ]);
res.sendStatus(204); res.sendStatus(204);
}); },
);
router.delete("/:message_id", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: Response) => { router.delete(
"/:message_id",
route({ permission: "VIEW_CHANNEL" }),
async (req: Request, res: Response) => {
const { channel_id, message_id } = req.params; const { channel_id, message_id } = req.params;
const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); const channel = await Channel.findOneOrFail({
where: { id: channel_id },
});
if (channel.guild_id) req.permission!.hasThrow("MANAGE_MESSAGES"); if (channel.guild_id) req.permission!.hasThrow("MANAGE_MESSAGES");
const message = await Message.findOneOrFail({ where: { id: message_id } }); const message = await Message.findOneOrFail({
where: { id: message_id },
});
message.pinned = false; message.pinned = false;
await Promise.all([ await Promise.all([
@ -62,7 +78,7 @@ router.delete("/:message_id", route({ permission: "VIEW_CHANNEL" }), async (req:
emitEvent({ emitEvent({
event: "MESSAGE_UPDATE", event: "MESSAGE_UPDATE",
channel_id, channel_id,
data: message data: message,
} as MessageUpdateEvent), } as MessageUpdateEvent),
emitEvent({ emitEvent({
@ -71,20 +87,27 @@ router.delete("/:message_id", route({ permission: "VIEW_CHANNEL" }), async (req:
data: { data: {
channel_id, channel_id,
guild_id: channel.guild_id, guild_id: channel.guild_id,
last_pin_timestamp: undefined last_pin_timestamp: undefined,
} },
} as ChannelPinsUpdateEvent) } as ChannelPinsUpdateEvent),
]); ]);
res.sendStatus(204); res.sendStatus(204);
}); },
);
router.get("/", route({ permission: ["READ_MESSAGE_HISTORY"] }), async (req: Request, res: Response) => { router.get(
"/",
route({ permission: ["READ_MESSAGE_HISTORY"] }),
async (req: Request, res: Response) => {
const { channel_id } = req.params; const { channel_id } = req.params;
let pins = await Message.find({ where: { channel_id: channel_id, pinned: true } }); let pins = await Message.find({
where: { channel_id: channel_id, pinned: true },
res.send(pins);
}); });
res.send(pins);
},
);
export default router; export default router;

View File

@ -21,16 +21,28 @@ export default router;
/** /**
TODO: apply the delete bit by bit to prevent client and database stress TODO: apply the delete bit by bit to prevent client and database stress
**/ **/
router.post("/", route({ /*body: "PurgeSchema",*/ }), async (req: Request, res: Response) => { router.post(
"/",
route({
/*body: "PurgeSchema",*/
}),
async (req: Request, res: Response) => {
const { channel_id } = req.params; const { channel_id } = req.params;
const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); const channel = await Channel.findOneOrFail({
where: { id: channel_id },
});
if (!channel.guild_id) throw new HTTPError("Can't purge dm channels", 400); if (!channel.guild_id)
throw new HTTPError("Can't purge dm channels", 400);
isTextChannel(channel.type); isTextChannel(channel.type);
const rights = await getRights(req.user_id); const rights = await getRights(req.user_id);
if (!rights.has("MANAGE_MESSAGES")) { if (!rights.has("MANAGE_MESSAGES")) {
const permissions = await getPermission(req.user_id, channel.guild_id, channel_id); const permissions = await getPermission(
req.user_id,
channel.guild_id,
channel_id,
);
permissions.hasThrow("MANAGE_MESSAGES"); permissions.hasThrow("MANAGE_MESSAGES");
permissions.hasThrow("MANAGE_CHANNELS"); permissions.hasThrow("MANAGE_CHANNELS");
} }
@ -39,19 +51,29 @@ router.post("/", route({ /*body: "PurgeSchema",*/ }), async (req: Request, res:
// TODO: send the deletion event bite-by-bite to prevent client stress // TODO: send the deletion event bite-by-bite to prevent client stress
var query: FindManyOptions<Message> & { where: { id?: any; }; } = { var query: FindManyOptions<Message> & { where: { id?: any } } = {
order: { id: "ASC" }, order: { id: "ASC" },
// take: limit, // take: limit,
where: { where: {
channel_id, channel_id,
id: Between(after, before), // the right way around id: Between(after, before), // the right way around
author_id: rights.has("SELF_DELETE_MESSAGES") ? undefined : Not(req.user_id) author_id: rights.has("SELF_DELETE_MESSAGES")
? undefined
: Not(req.user_id),
// if you lack the right of self-deletion, you can't delete your own messages, even in purges // if you lack the right of self-deletion, you can't delete your own messages, even in purges
}, },
relations: ["author", "webhook", "application", "mentions", "mention_roles", "mention_channels", "sticker_items", "attachments"] relations: [
"author",
"webhook",
"application",
"mentions",
"mention_roles",
"mention_channels",
"sticker_items",
"attachments",
],
}; };
const messages = await Message.find(query); const messages = await Message.find(query);
const endpoint = Config.get().cdn.endpointPublic; const endpoint = Config.get().cdn.endpointPublic;
@ -65,8 +87,13 @@ router.post("/", route({ /*body: "PurgeSchema",*/ }), async (req: Request, res:
await emitEvent({ await emitEvent({
event: "MESSAGE_DELETE_BULK", event: "MESSAGE_DELETE_BULK",
channel_id, channel_id,
data: { ids: messages.map(x => x.id), channel_id, guild_id: channel.guild_id } data: {
ids: messages.map((x) => x.id),
channel_id,
guild_id: channel.guild_id,
},
} as MessageDeleteBulkEvent); } as MessageDeleteBulkEvent);
res.sendStatus(204); res.sendStatus(204);
}); },
);

View File

@ -8,7 +8,7 @@ import {
emitEvent, emitEvent,
PublicUserProjection, PublicUserProjection,
Recipient, Recipient,
User User,
} from "@fosscord/util"; } from "@fosscord/util";
import { route } from "@fosscord/api"; import { route } from "@fosscord/api";
@ -16,34 +16,48 @@ const router: Router = Router();
router.put("/:user_id", route({}), async (req: Request, res: Response) => { router.put("/:user_id", route({}), async (req: Request, res: Response) => {
const { channel_id, user_id } = req.params; const { channel_id, user_id } = req.params;
const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients"] }); const channel = await Channel.findOneOrFail({
where: { id: channel_id },
relations: ["recipients"],
});
if (channel.type !== ChannelType.GROUP_DM) { if (channel.type !== ChannelType.GROUP_DM) {
const recipients = [...channel.recipients!.map((r) => r.user_id), user_id].unique(); const recipients = [
...channel.recipients!.map((r) => r.user_id),
user_id,
].unique();
const new_channel = await Channel.createDMChannel(recipients, req.user_id); const new_channel = await Channel.createDMChannel(
recipients,
req.user_id,
);
return res.status(201).json(new_channel); return res.status(201).json(new_channel);
} else { } else {
if (channel.recipients!.map((r) => r.user_id).includes(user_id)) { if (channel.recipients!.map((r) => r.user_id).includes(user_id)) {
throw DiscordApiErrors.INVALID_RECIPIENT; //TODO is this the right error? throw DiscordApiErrors.INVALID_RECIPIENT; //TODO is this the right error?
} }
channel.recipients!.push(Recipient.create({ channel_id: channel_id, user_id: user_id })); channel.recipients!.push(
Recipient.create({ channel_id: channel_id, user_id: user_id }),
);
await channel.save(); await channel.save();
await emitEvent({ await emitEvent({
event: "CHANNEL_CREATE", event: "CHANNEL_CREATE",
data: await DmChannelDTO.from(channel, [user_id]), data: await DmChannelDTO.from(channel, [user_id]),
user_id: user_id user_id: user_id,
}); });
await emitEvent({ await emitEvent({
event: "CHANNEL_RECIPIENT_ADD", event: "CHANNEL_RECIPIENT_ADD",
data: { data: {
channel_id: channel_id, channel_id: channel_id,
user: await User.findOneOrFail({ where: { id: user_id }, select: PublicUserProjection }) user: await User.findOneOrFail({
where: { id: user_id },
select: PublicUserProjection,
}),
}, },
channel_id: channel_id channel_id: channel_id,
} as ChannelRecipientAddEvent); } as ChannelRecipientAddEvent);
return res.sendStatus(204); return res.sendStatus(204);
} }
@ -51,8 +65,16 @@ router.put("/:user_id", route({}), async (req: Request, res: Response) => {
router.delete("/:user_id", route({}), async (req: Request, res: Response) => { router.delete("/:user_id", route({}), async (req: Request, res: Response) => {
const { channel_id, user_id } = req.params; const { channel_id, user_id } = req.params;
const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients"] }); const channel = await Channel.findOneOrFail({
if (!(channel.type === ChannelType.GROUP_DM && (channel.owner_id === req.user_id || user_id === req.user_id))) where: { id: channel_id },
relations: ["recipients"],
});
if (
!(
channel.type === ChannelType.GROUP_DM &&
(channel.owner_id === req.user_id || user_id === req.user_id)
)
)
throw DiscordApiErrors.MISSING_PERMISSIONS; throw DiscordApiErrors.MISSING_PERMISSIONS;
if (!channel.recipients!.map((r) => r.user_id).includes(user_id)) { if (!channel.recipients!.map((r) => r.user_id).includes(user_id)) {

View File

@ -4,26 +4,42 @@ import { Router, Request, Response } from "express";
const router: Router = Router(); const router: Router = Router();
router.post("/", route({ permission: "SEND_MESSAGES" }), async (req: Request, res: Response) => { router.post(
"/",
route({ permission: "SEND_MESSAGES" }),
async (req: Request, res: Response) => {
const { channel_id } = req.params; const { channel_id } = req.params;
const user_id = req.user_id; const user_id = req.user_id;
const timestamp = Date.now(); const timestamp = Date.now();
const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); const channel = await Channel.findOneOrFail({
const member = await Member.findOne({ where: { id: user_id, guild_id: channel.guild_id }, relations: ["roles", "user"] }); where: { id: channel_id },
});
const member = await Member.findOne({
where: { id: user_id, guild_id: channel.guild_id },
relations: ["roles", "user"],
});
await emitEvent({ await emitEvent({
event: "TYPING_START", event: "TYPING_START",
channel_id: channel_id, channel_id: channel_id,
data: { data: {
...(member ? { member: { ...member, roles: member?.roles?.map((x) => x.id) } } : null), ...(member
? {
member: {
...member,
roles: member?.roles?.map((x) => x.id),
},
}
: null),
channel_id, channel_id,
timestamp, timestamp,
user_id, user_id,
guild_id: channel.guild_id guild_id: channel.guild_id,
} },
} as TypingStartEvent); } as TypingStartEvent);
res.sendStatus(204); res.sendStatus(204);
}); },
);
export default router; export default router;

View File

@ -13,22 +13,29 @@ router.get("/", route({}), async (req: Request, res: Response) => {
}); });
// TODO: use Image Data Type for avatar instead of String // TODO: use Image Data Type for avatar instead of String
router.post("/", route({ body: "WebhookCreateSchema", permission: "MANAGE_WEBHOOKS" }), async (req: Request, res: Response) => { router.post(
"/",
route({ body: "WebhookCreateSchema", permission: "MANAGE_WEBHOOKS" }),
async (req: Request, res: Response) => {
const channel_id = req.params.channel_id; const channel_id = req.params.channel_id;
const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); const channel = await Channel.findOneOrFail({
where: { id: channel_id },
});
isTextChannel(channel.type); isTextChannel(channel.type);
if (!channel.guild_id) throw new HTTPError("Not a guild channel", 400); if (!channel.guild_id) throw new HTTPError("Not a guild channel", 400);
const webhook_count = await Webhook.count({ where: { channel_id } }); const webhook_count = await Webhook.count({ where: { channel_id } });
const { maxWebhooks } = Config.get().limits.channel; const { maxWebhooks } = Config.get().limits.channel;
if (webhook_count > maxWebhooks) throw DiscordApiErrors.MAXIMUM_WEBHOOKS.withParams(maxWebhooks); if (webhook_count > maxWebhooks)
throw DiscordApiErrors.MAXIMUM_WEBHOOKS.withParams(maxWebhooks);
var { avatar, name } = req.body as { name: string; avatar?: string }; var { avatar, name } = req.body as { name: string; avatar?: string };
name = trimSpecial(name); name = trimSpecial(name);
if (name === "clyde") throw new HTTPError("Invalid name", 400); if (name === "clyde") throw new HTTPError("Invalid name", 400);
// TODO: save webhook in database and send response // TODO: save webhook in database and send response
}); },
);
export default router; export default router;

View File

@ -17,19 +17,33 @@ router.get("/", route({}), async (req: Request, res: Response) => {
if (categories == undefined) { if (categories == undefined) {
guilds = showAllGuilds guilds = showAllGuilds
? await Guild.find({ take: Math.abs(Number(limit || configLimit)) }) ? await Guild.find({ take: Math.abs(Number(limit || configLimit)) })
: await Guild.find({ where: { features: Like(`%DISCOVERABLE%`) }, take: Math.abs(Number(limit || configLimit)) }); : await Guild.find({
where: { features: Like(`%DISCOVERABLE%`) },
take: Math.abs(Number(limit || configLimit)),
});
} else { } else {
guilds = showAllGuilds guilds = showAllGuilds
? await Guild.find({ where: { primary_category_id: categories.toString() }, take: Math.abs(Number(limit || configLimit)) }) ? await Guild.find({
where: { primary_category_id: categories.toString() },
take: Math.abs(Number(limit || configLimit)),
})
: await Guild.find({ : await Guild.find({
where: { primary_category_id: categories.toString(), features: Like("%DISCOVERABLE%") }, where: {
take: Math.abs(Number(limit || configLimit)) primary_category_id: categories.toString(),
features: Like("%DISCOVERABLE%"),
},
take: Math.abs(Number(limit || configLimit)),
}); });
} }
const total = guilds ? guilds.length : undefined; const total = guilds ? guilds.length : undefined;
res.send({ total: total, guilds: guilds, offset: Number(offset || Config.get().guild.discovery.offset), limit: Number(limit || configLimit) }); res.send({
total: total,
guilds: guilds,
offset: Number(offset || Config.get().guild.discovery.offset),
limit: Number(limit || configLimit),
});
}); });
export default router; export default router;

View File

@ -10,7 +10,9 @@ router.get("/categories", route({}), async (req: Request, res: Response) => {
const { locale, primary_only } = req.query; const { locale, primary_only } = req.query;
const out = primary_only ? await Categories.find() : await Categories.find({ where: { is_primary: true } }); const out = primary_only
? await Categories.find()
: await Categories.find({ where: { is_primary: true } });
res.send(out); res.send(out);
}); });

View File

@ -10,9 +10,12 @@ router.get("/:branch", route({}), async (req: Request, res: Response) => {
const { platform } = req.query; const { platform } = req.query;
//TODO //TODO
if (!platform || !["linux", "osx", "win"].includes(platform.toString())) return res.status(404); if (!platform || !["linux", "osx", "win"].includes(platform.toString()))
return res.status(404);
const release = await Release.findOneOrFail({ where: { name: client.releases.upstreamVersion } }); const release = await Release.findOneOrFail({
where: { name: client.releases.upstreamVersion },
});
res.redirect(release[`win_url`]); res.redirect(release[`win_url`]);
}); });

View File

@ -18,9 +18,9 @@ export interface GatewayBotResponse {
const options: RouteOptions = { const options: RouteOptions = {
test: { test: {
response: { response: {
body: "GatewayBotResponse" body: "GatewayBotResponse",
} },
} },
}; };
router.get("/", route(options), (req: Request, res: Response) => { router.get("/", route(options), (req: Request, res: Response) => {
@ -32,8 +32,8 @@ router.get("/", route(options), (req: Request, res: Response) => {
total: 1000, total: 1000,
remaining: 999, remaining: 999,
reset_after: 14400000, reset_after: 14400000,
max_concurrency: 1 max_concurrency: 1,
} },
}); });
}); });

View File

@ -11,14 +11,16 @@ export interface GatewayResponse {
const options: RouteOptions = { const options: RouteOptions = {
test: { test: {
response: { response: {
body: "GatewayResponse" body: "GatewayResponse",
} },
} },
}; };
router.get("/", route(options), (req: Request, res: Response) => { router.get("/", route(options), (req: Request, res: Response) => {
const { endpointPublic } = Config.get().gateway; const { endpointPublic } = Config.get().gateway;
res.json({ url: endpointPublic || process.env.GATEWAY || "ws://localhost:3002" }); res.json({
url: endpointPublic || process.env.GATEWAY || "ws://localhost:3002",
});
}); });
export default router; export default router;

View File

@ -1,6 +1,6 @@
import { Router, Response, Request } from "express"; import { Router, Response, Request } from "express";
import fetch from "node-fetch"; import fetch from "node-fetch";
import ProxyAgent from 'proxy-agent'; import ProxyAgent from "proxy-agent";
import { route } from "@fosscord/api"; import { route } from "@fosscord/api";
import { getGifApiKey, parseGifResult } from "./trending"; import { getGifApiKey, parseGifResult } from "./trending";
@ -14,13 +14,16 @@ router.get("/", route({}), async (req: Request, res: Response) => {
const agent = new ProxyAgent(); const agent = new ProxyAgent();
const response = await fetch(`https://g.tenor.com/v1/search?q=${q}&media_format=${media_format}&locale=${locale}&key=${apiKey}`, { const response = await fetch(
`https://g.tenor.com/v1/search?q=${q}&media_format=${media_format}&locale=${locale}&key=${apiKey}`,
{
agent, agent,
method: "get", method: "get",
headers: { "Content-Type": "application/json" } headers: { "Content-Type": "application/json" },
}); },
);
const { results } = await response.json() as any; // TODO: types const { results } = (await response.json()) as any; // TODO: types
res.json(results.map(parseGifResult)).status(200); res.json(results.map(parseGifResult)).status(200);
}); });

View File

@ -1,6 +1,6 @@
import { Router, Response, Request } from "express"; import { Router, Response, Request } from "express";
import fetch from "node-fetch"; import fetch from "node-fetch";
import ProxyAgent from 'proxy-agent'; import ProxyAgent from "proxy-agent";
import { route } from "@fosscord/api"; import { route } from "@fosscord/api";
import { getGifApiKey, parseGifResult } from "./trending"; import { getGifApiKey, parseGifResult } from "./trending";
@ -14,13 +14,16 @@ router.get("/", route({}), async (req: Request, res: Response) => {
const agent = new ProxyAgent(); const agent = new ProxyAgent();
const response = await fetch(`https://g.tenor.com/v1/trending?media_format=${media_format}&locale=${locale}&key=${apiKey}`, { const response = await fetch(
`https://g.tenor.com/v1/trending?media_format=${media_format}&locale=${locale}&key=${apiKey}`,
{
agent, agent,
method: "get", method: "get",
headers: { "Content-Type": "application/json" } headers: { "Content-Type": "application/json" },
}); },
);
const { results } = await response.json() as any; // TODO: types const { results } = (await response.json()) as any; // TODO: types
res.json(results.map(parseGifResult)).status(200); res.json(results.map(parseGifResult)).status(200);
}); });

View File

@ -1,6 +1,6 @@
import { Router, Response, Request } from "express"; import { Router, Response, Request } from "express";
import fetch from "node-fetch"; import fetch from "node-fetch";
import ProxyAgent from 'proxy-agent'; import ProxyAgent from "proxy-agent";
import { route } from "@fosscord/api"; import { route } from "@fosscord/api";
import { Config } from "@fosscord/util"; import { Config } from "@fosscord/util";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
@ -16,14 +16,15 @@ export function parseGifResult(result: any) {
gif_src: result.media[0].gif.url, gif_src: result.media[0].gif.url,
width: result.media[0].mp4.dims[0], width: result.media[0].mp4.dims[0],
height: result.media[0].mp4.dims[1], height: result.media[0].mp4.dims[1],
preview: result.media[0].mp4.preview preview: result.media[0].mp4.preview,
}; };
} }
export function getGifApiKey() { export function getGifApiKey() {
const { enabled, provider, apiKey } = Config.get().gif; const { enabled, provider, apiKey } = Config.get().gif;
if (!enabled) throw new HTTPError(`Gifs are disabled`); if (!enabled) throw new HTTPError(`Gifs are disabled`);
if (provider !== "tenor" || !apiKey) throw new HTTPError(`${provider} gif provider not supported`); if (provider !== "tenor" || !apiKey)
throw new HTTPError(`${provider} gif provider not supported`);
return apiKey; return apiKey;
} }
@ -38,24 +39,33 @@ router.get("/", route({}), async (req: Request, res: Response) => {
const agent = new ProxyAgent(); const agent = new ProxyAgent();
const [responseSource, trendGifSource] = await Promise.all([ const [responseSource, trendGifSource] = await Promise.all([
fetch(`https://g.tenor.com/v1/categories?locale=${locale}&key=${apiKey}`, { fetch(
`https://g.tenor.com/v1/categories?locale=${locale}&key=${apiKey}`,
{
agent, agent,
method: "get", method: "get",
headers: { "Content-Type": "application/json" } headers: { "Content-Type": "application/json" },
}), },
fetch(`https://g.tenor.com/v1/trending?locale=${locale}&key=${apiKey}`, { ),
fetch(
`https://g.tenor.com/v1/trending?locale=${locale}&key=${apiKey}`,
{
agent, agent,
method: "get", method: "get",
headers: { "Content-Type": "application/json" } headers: { "Content-Type": "application/json" },
}) },
),
]); ]);
const { tags } = await responseSource.json() as any; // TODO: types const { tags } = (await responseSource.json()) as any; // TODO: types
const { results } = await trendGifSource.json() as any; //TODO: types; const { results } = (await trendGifSource.json()) as any; //TODO: types;
res.json({ res.json({
categories: tags.map((x: any) => ({ name: x.searchterm, src: x.image })), categories: tags.map((x: any) => ({
gifs: [parseGifResult(results[0])] name: x.searchterm,
src: x.image,
})),
gifs: [parseGifResult(results[0])],
}).status(200); }).status(200);
}); });

View File

@ -13,12 +13,21 @@ router.get("/", route({}), async (req: Request, res: Response) => {
// TODO: implement this with default typeorm query // TODO: implement this with default typeorm query
// const guilds = await Guild.find({ where: { features: "DISCOVERABLE" } }); //, take: Math.abs(Number(limit)) }); // const guilds = await Guild.find({ where: { features: "DISCOVERABLE" } }); //, take: Math.abs(Number(limit)) });
const genLoadId = (size: Number) => [...Array(size)].map(() => Math.floor(Math.random() * 16).toString(16)).join(''); const genLoadId = (size: Number) =>
[...Array(size)]
.map(() => Math.floor(Math.random() * 16).toString(16))
.join("");
const guilds = showAllGuilds const guilds = showAllGuilds
? await Guild.find({ take: Math.abs(Number(limit || 24)) }) ? await Guild.find({ take: Math.abs(Number(limit || 24)) })
: await Guild.find({ where: { features: Like("%DISCOVERABLE%") }, take: Math.abs(Number(limit || 24)) }); : await Guild.find({
res.send({ recommended_guilds: guilds, load_id: `server_recs/${genLoadId(32)}` }).status(200); where: { features: Like("%DISCOVERABLE%") },
take: Math.abs(Number(limit || 24)),
});
res.send({
recommended_guilds: guilds,
load_id: `server_recs/${genLoadId(32)}`,
}).status(200);
}); });
export default router; export default router;

View File

@ -11,7 +11,7 @@ router.get("/", route({}), async (req: Request, res: Response) => {
webhooks: [], webhooks: [],
guild_scheduled_events: [], guild_scheduled_events: [],
threads: [], threads: [],
application_commands: [] application_commands: [],
}); });
}); });
export default router; export default router;

View File

@ -1,5 +1,15 @@
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
import { DiscordApiErrors, emitEvent, GuildBanAddEvent, GuildBanRemoveEvent, Ban, User, Member, BanRegistrySchema, BanModeratorSchema } from "@fosscord/util"; import {
DiscordApiErrors,
emitEvent,
GuildBanAddEvent,
GuildBanRemoveEvent,
Ban,
User,
Member,
BanRegistrySchema,
BanModeratorSchema,
} from "@fosscord/util";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { getIpAdress, route } from "@fosscord/api"; import { getIpAdress, route } from "@fosscord/api";
@ -7,7 +17,10 @@ const router: Router = Router();
/* TODO: Deleting the secrets is just a temporary go-around. Views should be implemented for both safety and better handling. */ /* TODO: Deleting the secrets is just a temporary go-around. Views should be implemented for both safety and better handling. */
router.get("/", route({ permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => { router.get(
"/",
route({ permission: "BAN_MEMBERS" }),
async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id } = req.params;
let bans = await Ban.find({ where: { guild_id: guild_id } }); let bans = await Ban.find({ where: { guild_id: guild_id } });
@ -31,19 +44,25 @@ router.get("/", route({ permission: "BAN_MEMBERS" }), async (req: Request, res:
discriminator: user.discriminator, discriminator: user.discriminator,
id: user.id, id: user.id,
avatar: user.avatar, avatar: user.avatar,
public_flags: user.public_flags public_flags: user.public_flags,
} },
}); });
}); });
return res.json(bansObj); return res.json(bansObj);
}); },
);
router.get("/:user", route({ permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => { router.get(
"/:user",
route({ permission: "BAN_MEMBERS" }),
async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id } = req.params;
const user_id = req.params.ban; const user_id = req.params.ban;
let ban = await Ban.findOneOrFail({ where: { guild_id: guild_id, user_id: user_id } }) as BanRegistrySchema; let ban = (await Ban.findOneOrFail({
where: { guild_id: guild_id, user_id: user_id },
})) as BanRegistrySchema;
if (ban.user_id === ban.executor_id) throw DiscordApiErrors.UNKNOWN_BAN; if (ban.user_id === ban.executor_id) throw DiscordApiErrors.UNKNOWN_BAN;
// pretend self-bans don't exist to prevent victim chasing // pretend self-bans don't exist to prevent victim chasing
@ -55,16 +74,27 @@ router.get("/:user", route({ permission: "BAN_MEMBERS" }), async (req: Request,
delete ban.ip; delete ban.ip;
return res.json(ban); return res.json(ban);
}); },
);
router.put("/:user_id", route({ body: "BanCreateSchema", permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => { router.put(
"/:user_id",
route({ body: "BanCreateSchema", permission: "BAN_MEMBERS" }),
async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id } = req.params;
const banned_user_id = req.params.user_id; const banned_user_id = req.params.user_id;
if ((req.user_id === banned_user_id) && (banned_user_id === req.permission!.cache.guild?.owner_id)) if (
throw new HTTPError("You are the guild owner, hence can't ban yourself", 403); req.user_id === banned_user_id &&
banned_user_id === req.permission!.cache.guild?.owner_id
)
throw new HTTPError(
"You are the guild owner, hence can't ban yourself",
403,
);
if (req.permission!.cache.guild?.owner_id === banned_user_id) throw new HTTPError("You can't ban the owner", 400); if (req.permission!.cache.guild?.owner_id === banned_user_id)
throw new HTTPError("You can't ban the owner", 400);
const banned_user = await User.getPublicUser(banned_user_id); const banned_user = await User.getPublicUser(banned_user_id);
@ -73,7 +103,7 @@ router.put("/:user_id", route({ body: "BanCreateSchema", permission: "BAN_MEMBER
guild_id: guild_id, guild_id: guild_id,
ip: getIpAdress(req), ip: getIpAdress(req),
executor_id: req.user_id, executor_id: req.user_id,
reason: req.body.reason // || otherwise empty reason: req.body.reason, // || otherwise empty
}); });
await Promise.all([ await Promise.all([
@ -83,29 +113,36 @@ router.put("/:user_id", route({ body: "BanCreateSchema", permission: "BAN_MEMBER
event: "GUILD_BAN_ADD", event: "GUILD_BAN_ADD",
data: { data: {
guild_id: guild_id, guild_id: guild_id,
user: banned_user user: banned_user,
}, },
guild_id: guild_id guild_id: guild_id,
} as GuildBanAddEvent) } as GuildBanAddEvent),
]); ]);
return res.json(ban); return res.json(ban);
}); },
);
router.put("/@me", route({ body: "BanCreateSchema" }), async (req: Request, res: Response) => { router.put(
"/@me",
route({ body: "BanCreateSchema" }),
async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id } = req.params;
const banned_user = await User.getPublicUser(req.params.user_id); const banned_user = await User.getPublicUser(req.params.user_id);
if (req.permission!.cache.guild?.owner_id === req.params.user_id) if (req.permission!.cache.guild?.owner_id === req.params.user_id)
throw new HTTPError("You are the guild owner, hence can't ban yourself", 403); throw new HTTPError(
"You are the guild owner, hence can't ban yourself",
403,
);
const ban = Ban.create({ const ban = Ban.create({
user_id: req.params.user_id, user_id: req.params.user_id,
guild_id: guild_id, guild_id: guild_id,
ip: getIpAdress(req), ip: getIpAdress(req),
executor_id: req.params.user_id, executor_id: req.params.user_id,
reason: req.body.reason // || otherwise empty reason: req.body.reason, // || otherwise empty
}); });
await Promise.all([ await Promise.all([
@ -115,19 +152,25 @@ router.put("/@me", route({ body: "BanCreateSchema" }), async (req: Request, res:
event: "GUILD_BAN_ADD", event: "GUILD_BAN_ADD",
data: { data: {
guild_id: guild_id, guild_id: guild_id,
user: banned_user user: banned_user,
}, },
guild_id: guild_id guild_id: guild_id,
} as GuildBanAddEvent) } as GuildBanAddEvent),
]); ]);
return res.json(ban); return res.json(ban);
}); },
);
router.delete("/:user_id", route({ permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => { router.delete(
"/:user_id",
route({ permission: "BAN_MEMBERS" }),
async (req: Request, res: Response) => {
const { guild_id, user_id } = req.params; const { guild_id, user_id } = req.params;
let ban = await Ban.findOneOrFail({ where: { guild_id: guild_id, user_id: user_id } }); let ban = await Ban.findOneOrFail({
where: { guild_id: guild_id, user_id: user_id },
});
if (ban.user_id === ban.executor_id) throw DiscordApiErrors.UNKNOWN_BAN; if (ban.user_id === ban.executor_id) throw DiscordApiErrors.UNKNOWN_BAN;
// make self-bans irreversible and hide them from view to avoid victim chasing // make self-bans irreversible and hide them from view to avoid victim chasing
@ -137,20 +180,21 @@ router.delete("/:user_id", route({ permission: "BAN_MEMBERS" }), async (req: Req
await Promise.all([ await Promise.all([
Ban.delete({ Ban.delete({
user_id: user_id, user_id: user_id,
guild_id guild_id,
}), }),
emitEvent({ emitEvent({
event: "GUILD_BAN_REMOVE", event: "GUILD_BAN_REMOVE",
data: { data: {
guild_id, guild_id,
user: banned_user user: banned_user,
}, },
guild_id guild_id,
} as GuildBanRemoveEvent) } as GuildBanRemoveEvent),
]); ]);
return res.status(204).send(); return res.status(204).send();
}); },
);
export default router; export default router;

View File

@ -1,5 +1,10 @@
import { Router, Response, Request } from "express"; import { Router, Response, Request } from "express";
import { Channel, ChannelUpdateEvent, emitEvent, ChannelModifySchema } from "@fosscord/util"; import {
Channel,
ChannelUpdateEvent,
emitEvent,
ChannelModifySchema,
} from "@fosscord/util";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { route } from "@fosscord/api"; import { route } from "@fosscord/api";
const router = Router(); const router = Router();
@ -11,26 +16,45 @@ router.get("/", route({}), async (req: Request, res: Response) => {
res.json(channels); res.json(channels);
}); });
router.post("/", route({ body: "ChannelModifySchema", permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => { router.post(
"/",
route({ body: "ChannelModifySchema", permission: "MANAGE_CHANNELS" }),
async (req: Request, res: Response) => {
// creates a new guild channel https://discord.com/developers/docs/resources/guild#create-guild-channel // creates a new guild channel https://discord.com/developers/docs/resources/guild#create-guild-channel
const { guild_id } = req.params; const { guild_id } = req.params;
const body = req.body as ChannelModifySchema; const body = req.body as ChannelModifySchema;
const channel = await Channel.createChannel({ ...body, guild_id }, req.user_id); const channel = await Channel.createChannel(
{ ...body, guild_id },
req.user_id,
);
res.status(201).json(channel); res.status(201).json(channel);
}); },
);
export type ChannelReorderSchema = { id: string; position?: number; lock_permissions?: boolean; parent_id?: string; }[]; export type ChannelReorderSchema = {
id: string;
position?: number;
lock_permissions?: boolean;
parent_id?: string;
}[];
router.patch("/", route({ body: "ChannelReorderSchema", permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => { router.patch(
"/",
route({ body: "ChannelReorderSchema", permission: "MANAGE_CHANNELS" }),
async (req: Request, res: Response) => {
// changes guild channel position // changes guild channel position
const { guild_id } = req.params; const { guild_id } = req.params;
const body = req.body as ChannelReorderSchema; const body = req.body as ChannelReorderSchema;
await Promise.all([ await Promise.all([
body.map(async (x) => { body.map(async (x) => {
if (x.position == null && !x.parent_id) throw new HTTPError(`You need to at least specify position or parent_id`, 400); if (x.position == null && !x.parent_id)
throw new HTTPError(
`You need to at least specify position or parent_id`,
400,
);
const opts: any = {}; const opts: any = {};
if (x.position != null) opts.position = x.position; if (x.position != null) opts.position = x.position;
@ -39,21 +63,30 @@ router.patch("/", route({ body: "ChannelReorderSchema", permission: "MANAGE_CHAN
opts.parent_id = x.parent_id; opts.parent_id = x.parent_id;
const parent_channel = await Channel.findOneOrFail({ const parent_channel = await Channel.findOneOrFail({
where: { id: x.parent_id, guild_id }, where: { id: x.parent_id, guild_id },
select: ["permission_overwrites"] select: ["permission_overwrites"],
}); });
if (x.lock_permissions) { if (x.lock_permissions) {
opts.permission_overwrites = parent_channel.permission_overwrites; opts.permission_overwrites =
parent_channel.permission_overwrites;
} }
} }
await Channel.update({ guild_id, id: x.id }, opts); await Channel.update({ guild_id, id: x.id }, opts);
const channel = await Channel.findOneOrFail({ where: { guild_id, id: x.id } }); const channel = await Channel.findOneOrFail({
where: { guild_id, id: x.id },
});
await emitEvent({ event: "CHANNEL_UPDATE", data: channel, channel_id: x.id, guild_id } as ChannelUpdateEvent); await emitEvent({
}) event: "CHANNEL_UPDATE",
data: channel,
channel_id: x.id,
guild_id,
} as ChannelUpdateEvent);
}),
]); ]);
res.sendStatus(204); res.sendStatus(204);
}); },
);
export default router; export default router;

View File

@ -1,4 +1,14 @@
import { Channel, emitEvent, GuildDeleteEvent, Guild, Member, Message, Role, Invite, Emoji } from "@fosscord/util"; import {
Channel,
emitEvent,
GuildDeleteEvent,
Guild,
Member,
Message,
Role,
Invite,
Emoji,
} from "@fosscord/util";
import { Router, Request, Response } from "express"; import { Router, Request, Response } from "express";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { route } from "@fosscord/api"; import { route } from "@fosscord/api";
@ -10,18 +20,22 @@ const router = Router();
router.post("/", route({}), async (req: Request, res: Response) => { router.post("/", route({}), async (req: Request, res: Response) => {
var { guild_id } = req.params; var { guild_id } = req.params;
const guild = await Guild.findOneOrFail({ where: { id: guild_id }, select: ["owner_id"] }); const guild = await Guild.findOneOrFail({
if (guild.owner_id !== req.user_id) throw new HTTPError("You are not the owner of this guild", 401); where: { id: guild_id },
select: ["owner_id"],
});
if (guild.owner_id !== req.user_id)
throw new HTTPError("You are not the owner of this guild", 401);
await Promise.all([ await Promise.all([
Guild.delete({ id: guild_id }), // this will also delete all guild related data Guild.delete({ id: guild_id }), // this will also delete all guild related data
emitEvent({ emitEvent({
event: "GUILD_DELETE", event: "GUILD_DELETE",
data: { data: {
id: guild_id id: guild_id,
}, },
guild_id: guild_id guild_id: guild_id,
} as GuildDeleteEvent) } as GuildDeleteEvent),
]); ]);
return res.sendStatus(204); return res.sendStatus(204);

View File

@ -30,9 +30,9 @@ router.get("/", route({}), async (req: Request, res: Response) => {
avg_nonnew_participators: 0, avg_nonnew_participators: 0,
avg_nonnew_communicators: 0, avg_nonnew_communicators: 0,
num_intentful_joiners: 0, num_intentful_joiners: 0,
perc_ret_w1_intentful: 0 perc_ret_w1_intentful: 0,
}, },
minimum_size: 0 minimum_size: 0,
}); });
}); });

View File

@ -1,5 +1,17 @@
import { Router, Request, Response } from "express"; import { Router, Request, Response } from "express";
import { Config, DiscordApiErrors, emitEvent, Emoji, GuildEmojisUpdateEvent, handleFile, Member, Snowflake, User, EmojiCreateSchema, EmojiModifySchema } from "@fosscord/util"; import {
Config,
DiscordApiErrors,
emitEvent,
Emoji,
GuildEmojisUpdateEvent,
handleFile,
Member,
Snowflake,
User,
EmojiCreateSchema,
EmojiModifySchema,
} from "@fosscord/util";
import { route } from "@fosscord/api"; import { route } from "@fosscord/api";
const router = Router(); const router = Router();
@ -9,7 +21,10 @@ router.get("/", route({}), async (req: Request, res: Response) => {
await Member.IsInGuildOrFail(req.user_id, guild_id); await Member.IsInGuildOrFail(req.user_id, guild_id);
const emojis = await Emoji.find({ where: { guild_id: guild_id }, relations: ["user"] }); const emojis = await Emoji.find({
where: { guild_id: guild_id },
relations: ["user"],
});
return res.json(emojis); return res.json(emojis);
}); });
@ -19,20 +34,34 @@ router.get("/:emoji_id", route({}), async (req: Request, res: Response) => {
await Member.IsInGuildOrFail(req.user_id, guild_id); await Member.IsInGuildOrFail(req.user_id, guild_id);
const emoji = await Emoji.findOneOrFail({ where: { guild_id: guild_id, id: emoji_id }, relations: ["user"] }); const emoji = await Emoji.findOneOrFail({
where: { guild_id: guild_id, id: emoji_id },
relations: ["user"],
});
return res.json(emoji); return res.json(emoji);
}); });
router.post("/", route({ body: "EmojiCreateSchema", permission: "MANAGE_EMOJIS_AND_STICKERS" }), async (req: Request, res: Response) => { router.post(
"/",
route({
body: "EmojiCreateSchema",
permission: "MANAGE_EMOJIS_AND_STICKERS",
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id } = req.params;
const body = req.body as EmojiCreateSchema; const body = req.body as EmojiCreateSchema;
const id = Snowflake.generate(); const id = Snowflake.generate();
const emoji_count = await Emoji.count({ where: { guild_id: guild_id } }); const emoji_count = await Emoji.count({
where: { guild_id: guild_id },
});
const { maxEmojis } = Config.get().limits.guild; const { maxEmojis } = Config.get().limits.guild;
if (emoji_count >= maxEmojis) throw DiscordApiErrors.MAXIMUM_NUMBER_OF_EMOJIS_REACHED.withParams(maxEmojis); if (emoji_count >= maxEmojis)
throw DiscordApiErrors.MAXIMUM_NUMBER_OF_EMOJIS_REACHED.withParams(
maxEmojis,
);
if (body.require_colons == null) body.require_colons = true; if (body.require_colons == null) body.require_colons = true;
const user = await User.findOneOrFail({ where: { id: req.user_id } }); const user = await User.findOneOrFail({ where: { id: req.user_id } });
@ -47,7 +76,7 @@ router.post("/", route({ body: "EmojiCreateSchema", permission: "MANAGE_EMOJIS_A
managed: false, managed: false,
animated: false, // TODO: Add support animated emojis animated: false, // TODO: Add support animated emojis
available: true, available: true,
roles: [] roles: [],
}).save(); }).save();
await emitEvent({ await emitEvent({
@ -55,41 +84,52 @@ router.post("/", route({ body: "EmojiCreateSchema", permission: "MANAGE_EMOJIS_A
guild_id: guild_id, guild_id: guild_id,
data: { data: {
guild_id: guild_id, guild_id: guild_id,
emojis: await Emoji.find({ where: { guild_id: guild_id } }) emojis: await Emoji.find({ where: { guild_id: guild_id } }),
} },
} as GuildEmojisUpdateEvent); } as GuildEmojisUpdateEvent);
return res.status(201).json(emoji); return res.status(201).json(emoji);
}); },
);
router.patch( router.patch(
"/:emoji_id", "/:emoji_id",
route({ body: "EmojiModifySchema", permission: "MANAGE_EMOJIS_AND_STICKERS" }), route({
body: "EmojiModifySchema",
permission: "MANAGE_EMOJIS_AND_STICKERS",
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { emoji_id, guild_id } = req.params; const { emoji_id, guild_id } = req.params;
const body = req.body as EmojiModifySchema; const body = req.body as EmojiModifySchema;
const emoji = await Emoji.create({ ...body, id: emoji_id, guild_id: guild_id }).save(); const emoji = await Emoji.create({
...body,
id: emoji_id,
guild_id: guild_id,
}).save();
await emitEvent({ await emitEvent({
event: "GUILD_EMOJIS_UPDATE", event: "GUILD_EMOJIS_UPDATE",
guild_id: guild_id, guild_id: guild_id,
data: { data: {
guild_id: guild_id, guild_id: guild_id,
emojis: await Emoji.find({ where: { guild_id: guild_id } }) emojis: await Emoji.find({ where: { guild_id: guild_id } }),
} },
} as GuildEmojisUpdateEvent); } as GuildEmojisUpdateEvent);
return res.json(emoji); return res.json(emoji);
} },
); );
router.delete("/:emoji_id", route({ permission: "MANAGE_EMOJIS_AND_STICKERS" }), async (req: Request, res: Response) => { router.delete(
"/:emoji_id",
route({ permission: "MANAGE_EMOJIS_AND_STICKERS" }),
async (req: Request, res: Response) => {
const { emoji_id, guild_id } = req.params; const { emoji_id, guild_id } = req.params;
await Emoji.delete({ await Emoji.delete({
id: emoji_id, id: emoji_id,
guild_id: guild_id guild_id: guild_id,
}); });
await emitEvent({ await emitEvent({
@ -97,11 +137,12 @@ router.delete("/:emoji_id", route({ permission: "MANAGE_EMOJIS_AND_STICKERS" }),
guild_id: guild_id, guild_id: guild_id,
data: { data: {
guild_id: guild_id, guild_id: guild_id,
emojis: await Emoji.find({ where: { guild_id: guild_id } }) emojis: await Emoji.find({ where: { guild_id: guild_id } }),
} },
} as GuildEmojisUpdateEvent); } as GuildEmojisUpdateEvent);
res.sendStatus(204); res.sendStatus(204);
}); },
);
export default router; export default router;

View File

@ -1,5 +1,15 @@
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
import { DiscordApiErrors, emitEvent, getPermission, getRights, Guild, GuildUpdateEvent, handleFile, Member, GuildCreateSchema } from "@fosscord/util"; import {
DiscordApiErrors,
emitEvent,
getPermission,
getRights,
Guild,
GuildUpdateEvent,
handleFile,
Member,
GuildCreateSchema,
} from "@fosscord/util";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { route } from "@fosscord/api"; import { route } from "@fosscord/api";
@ -26,9 +36,13 @@ router.get("/", route({}), async (req: Request, res: Response) => {
const [guild, member] = await Promise.all([ const [guild, member] = await Promise.all([
Guild.findOneOrFail({ where: { id: guild_id } }), Guild.findOneOrFail({ where: { id: guild_id } }),
Member.findOne({ where: { guild_id: guild_id, id: req.user_id } }) Member.findOne({ where: { guild_id: guild_id, id: req.user_id } }),
]); ]);
if (!member) throw new HTTPError("You are not a member of the guild you are trying to access", 401); if (!member)
throw new HTTPError(
"You are not a member of the guild you are trying to access",
401,
);
// @ts-ignore // @ts-ignore
guild.joined_at = member?.joined_at; guild.joined_at = member?.joined_at;
@ -36,26 +50,36 @@ router.get("/", route({}), async (req: Request, res: Response) => {
return res.send(guild); return res.send(guild);
}); });
router.patch("/", route({ body: "GuildUpdateSchema" }), async (req: Request, res: Response) => { router.patch(
"/",
route({ body: "GuildUpdateSchema" }),
async (req: Request, res: Response) => {
const body = req.body as GuildUpdateSchema; const body = req.body as GuildUpdateSchema;
const { guild_id } = req.params; const { guild_id } = req.params;
const rights = await getRights(req.user_id); const rights = await getRights(req.user_id);
const permission = await getPermission(req.user_id, guild_id); const permission = await getPermission(req.user_id, guild_id);
if (!rights.has("MANAGE_GUILDS") || !permission.has("MANAGE_GUILD")) if (!rights.has("MANAGE_GUILDS") || !permission.has("MANAGE_GUILD"))
throw DiscordApiErrors.MISSING_PERMISSIONS.withParams("MANAGE_GUILD"); throw DiscordApiErrors.MISSING_PERMISSIONS.withParams(
"MANAGE_GUILD",
);
// TODO: guild update check image // TODO: guild update check image
if (body.icon) body.icon = await handleFile(`/icons/${guild_id}`, body.icon); if (body.icon)
if (body.banner) body.banner = await handleFile(`/banners/${guild_id}`, body.banner); body.icon = await handleFile(`/icons/${guild_id}`, body.icon);
if (body.splash) body.splash = await handleFile(`/splashes/${guild_id}`, body.splash); if (body.banner)
body.banner = await handleFile(`/banners/${guild_id}`, body.banner);
if (body.splash)
body.splash = await handleFile(
`/splashes/${guild_id}`,
body.splash,
);
var guild = await Guild.findOneOrFail({ var guild = await Guild.findOneOrFail({
where: { id: guild_id }, where: { id: guild_id },
relations: ["emojis", "roles", "stickers"] relations: ["emojis", "roles", "stickers"],
}); });
// TODO: check if body ids are valid // TODO: check if body ids are valid
guild.assign(body); guild.assign(body);
@ -66,9 +90,17 @@ router.patch("/", route({ body: "GuildUpdateSchema" }), async (req: Request, res
delete data.vanity_url_code; delete data.vanity_url_code;
delete data.template_id; delete data.template_id;
await Promise.all([guild.save(), emitEvent({ event: "GUILD_UPDATE", data, guild_id } as GuildUpdateEvent)]); await Promise.all([
guild.save(),
emitEvent({
event: "GUILD_UPDATE",
data,
guild_id,
} as GuildUpdateEvent),
]);
return res.json(data); return res.json(data);
}); },
);
export default router; export default router;

View File

@ -4,12 +4,19 @@ import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
router.get("/", route({ permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { router.get(
"/",
route({ permission: "MANAGE_GUILD" }),
async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id } = req.params;
const invites = await Invite.find({ where: { guild_id }, relations: PublicInviteRelation }); const invites = await Invite.find({
where: { guild_id },
return res.json(invites); relations: PublicInviteRelation,
}); });
return res.json(invites);
},
);
export default router; export default router;

View File

@ -7,7 +7,7 @@ router.get("/",route({}), async (req: Request, res: Response) => {
res.status(404).json({ res.status(404).json({
message: "Unknown Guild Member Verification Form", message: "Unknown Guild Member Verification Form",
code: 10068 code: 10068,
}); });
}); });

View File

@ -1,5 +1,16 @@
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
import { Member, getPermission, getRights, Role, GuildMemberUpdateEvent, emitEvent, Sticker, Emoji, Guild, MemberChangeSchema } from "@fosscord/util"; import {
Member,
getPermission,
getRights,
Role,
GuildMemberUpdateEvent,
emitEvent,
Sticker,
Emoji,
Guild,
MemberChangeSchema,
} from "@fosscord/util";
import { route } from "@fosscord/api"; import { route } from "@fosscord/api";
const router = Router(); const router = Router();
@ -8,29 +19,44 @@ router.get("/", route({}), async (req: Request, res: Response) => {
const { guild_id, member_id } = req.params; const { guild_id, member_id } = req.params;
await Member.IsInGuildOrFail(req.user_id, guild_id); await Member.IsInGuildOrFail(req.user_id, guild_id);
const member = await Member.findOneOrFail({ where: { id: member_id, guild_id } }); const member = await Member.findOneOrFail({
where: { id: member_id, guild_id },
});
return res.json(member); return res.json(member);
}); });
router.patch("/", route({ body: "MemberChangeSchema" }), async (req: Request, res: Response) => { router.patch(
"/",
route({ body: "MemberChangeSchema" }),
async (req: Request, res: Response) => {
let { guild_id, member_id } = req.params; let { guild_id, member_id } = req.params;
if (member_id === "@me") member_id = req.user_id; if (member_id === "@me") member_id = req.user_id;
const body = req.body as MemberChangeSchema; const body = req.body as MemberChangeSchema;
const member = await Member.findOneOrFail({ where: { id: member_id, guild_id }, relations: ["roles", "user"] }); const member = await Member.findOneOrFail({
where: { id: member_id, guild_id },
relations: ["roles", "user"],
});
const permission = await getPermission(req.user_id, guild_id); const permission = await getPermission(req.user_id, guild_id);
const everyone = await Role.findOneOrFail({ where: { guild_id: guild_id, name: "@everyone", position: 0 } }); const everyone = await Role.findOneOrFail({
where: { guild_id: guild_id, name: "@everyone", position: 0 },
});
if (body.roles) { if (body.roles) {
permission.hasThrow("MANAGE_ROLES"); permission.hasThrow("MANAGE_ROLES");
if (body.roles.indexOf(everyone.id) === -1) body.roles.push(everyone.id); if (body.roles.indexOf(everyone.id) === -1)
body.roles.push(everyone.id);
member.roles = body.roles.map((x) => Role.create({ id: x })); // foreign key constraint will fail if role doesn't exist member.roles = body.roles.map((x) => Role.create({ id: x })); // foreign key constraint will fail if role doesn't exist
} }
if ('nick' in body) { if ("nick" in body) {
permission.hasThrow(req.user_id == member.user.id ? "CHANGE_NICKNAME" : "MANAGE_NICKNAMES"); permission.hasThrow(
req.user_id == member.user.id
? "CHANGE_NICKNAME"
: "MANAGE_NICKNAMES",
);
member.nick = body.nick?.trim() || undefined; member.nick = body.nick?.trim() || undefined;
} }
@ -42,14 +68,14 @@ router.patch("/", route({ body: "MemberChangeSchema" }), async (req: Request, re
await emitEvent({ await emitEvent({
event: "GUILD_MEMBER_UPDATE", event: "GUILD_MEMBER_UPDATE",
guild_id, guild_id,
data: { ...member, roles: member.roles.map((x) => x.id) } data: { ...member, roles: member.roles.map((x) => x.id) },
} as GuildMemberUpdateEvent); } as GuildMemberUpdateEvent);
res.json(member); res.json(member);
}); },
);
router.put("/", route({}), async (req: Request, res: Response) => { router.put("/", route({}), async (req: Request, res: Response) => {
// TODO: Lurker mode // TODO: Lurker mode
const rights = await getRights(req.user_id); const rights = await getRights(req.user_id);
@ -63,19 +89,19 @@ router.put("/", route({}), async (req: Request, res: Response) => {
} }
var guild = await Guild.findOneOrFail({ var guild = await Guild.findOneOrFail({
where: { id: guild_id } where: { id: guild_id },
}); });
var emoji = await Emoji.find({ var emoji = await Emoji.find({
where: { guild_id: guild_id } where: { guild_id: guild_id },
}); });
var roles = await Role.find({ var roles = await Role.find({
where: { guild_id: guild_id } where: { guild_id: guild_id },
}); });
var stickers = await Sticker.find({ var stickers = await Sticker.find({
where: { guild_id: guild_id } where: { guild_id: guild_id },
}); });
await Member.addToGuild(member_id, guild_id); await Member.addToGuild(member_id, guild_id);

View File

@ -4,7 +4,10 @@ import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
router.patch("/", route({ body: "MemberNickChangeSchema" }), async (req: Request, res: Response) => { router.patch(
"/",
route({ body: "MemberNickChangeSchema" }),
async (req: Request, res: Response) => {
var { guild_id, member_id } = req.params; var { guild_id, member_id } = req.params;
var permissionString: PermissionResolvable = "MANAGE_NICKNAMES"; var permissionString: PermissionResolvable = "MANAGE_NICKNAMES";
if (member_id === "@me") { if (member_id === "@me") {
@ -17,6 +20,7 @@ router.patch("/", route({ body: "MemberNickChangeSchema" }), async (req: Request
await Member.changeNickname(member_id, guild_id, req.body.nick); await Member.changeNickname(member_id, guild_id, req.body.nick);
res.status(200).send(); res.status(200).send();
}); },
);
export default router; export default router;

View File

@ -4,18 +4,26 @@ import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
router.delete("/", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { router.delete(
"/",
route({ permission: "MANAGE_ROLES" }),
async (req: Request, res: Response) => {
const { guild_id, role_id, member_id } = req.params; const { guild_id, role_id, member_id } = req.params;
await Member.removeRole(member_id, guild_id, role_id); await Member.removeRole(member_id, guild_id, role_id);
res.sendStatus(204); res.sendStatus(204);
}); },
);
router.put("/", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { router.put(
"/",
route({ permission: "MANAGE_ROLES" }),
async (req: Request, res: Response) => {
const { guild_id, role_id, member_id } = req.params; const { guild_id, role_id, member_id } = req.params;
await Member.addRole(member_id, guild_id, role_id); await Member.addRole(member_id, guild_id, role_id);
res.sendStatus(204); res.sendStatus(204);
}); },
);
export default router; export default router;

View File

@ -12,7 +12,8 @@ const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get("/", route({}), async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id } = req.params;
const limit = Number(req.query.limit) || 1; const limit = Number(req.query.limit) || 1;
if (limit > 1000 || limit < 1) throw new HTTPError("Limit must be between 1 and 1000"); if (limit > 1000 || limit < 1)
throw new HTTPError("Limit must be between 1 and 1000");
const after = `${req.query.after}`; const after = `${req.query.after}`;
const query = after ? { id: MoreThan(after) } : {}; const query = after ? { id: MoreThan(after) } : {};
@ -22,7 +23,7 @@ router.get("/", route({}), async (req: Request, res: Response) => {
where: { guild_id, ...query }, where: { guild_id, ...query },
select: PublicMemberProjection, select: PublicMemberProjection,
take: limit, take: limit,
order: { id: "ASC" } order: { id: "ASC" },
}); });
return res.json(members); return res.json(members);

View File

@ -19,27 +19,53 @@ router.get("/", route({}), async (req: Request, res: Response) => {
} = req.query; } = req.query;
const parsedLimit = Number(limit) || 50; const parsedLimit = Number(limit) || 50;
if (parsedLimit < 1 || parsedLimit > 100) throw new HTTPError("limit must be between 1 and 100", 422); if (parsedLimit < 1 || parsedLimit > 100)
throw new HTTPError("limit must be between 1 and 100", 422);
if (sort_order) { if (sort_order) {
if (typeof sort_order != "string" if (
|| ["desc", "asc"].indexOf(sort_order) == -1) typeof sort_order != "string" ||
throw FieldErrors({ sort_order: { message: "Value must be one of ('desc', 'asc').", code: "BASE_TYPE_CHOICES" } }); // todo this is wrong ["desc", "asc"].indexOf(sort_order) == -1
)
throw FieldErrors({
sort_order: {
message: "Value must be one of ('desc', 'asc').",
code: "BASE_TYPE_CHOICES",
},
}); // todo this is wrong
} }
const permissions = await getPermission(req.user_id, req.params.guild_id, channel_id as string); const permissions = await getPermission(
req.user_id,
req.params.guild_id,
channel_id as string,
);
permissions.hasThrow("VIEW_CHANNEL"); permissions.hasThrow("VIEW_CHANNEL");
if (!permissions.has("READ_MESSAGE_HISTORY")) return res.json({ messages: [], total_results: 0 }); if (!permissions.has("READ_MESSAGE_HISTORY"))
return res.json({ messages: [], total_results: 0 });
var query: FindManyOptions<Message> = { var query: FindManyOptions<Message> = {
order: { timestamp: sort_order ? sort_order.toUpperCase() as "ASC" | "DESC" : "DESC" }, order: {
timestamp: sort_order
? (sort_order.toUpperCase() as "ASC" | "DESC")
: "DESC",
},
take: parsedLimit || 0, take: parsedLimit || 0,
where: { where: {
guild: { guild: {
id: req.params.guild_id, id: req.params.guild_id,
}, },
}, },
relations: ["author", "webhook", "application", "mentions", "mention_roles", "mention_channels", "sticker_items", "attachments"], relations: [
"author",
"webhook",
"application",
"mentions",
"mention_roles",
"mention_channels",
"sticker_items",
"attachments",
],
skip: offset ? Number(offset) : 0, skip: offset ? Number(offset) : 0,
}; };
//@ts-ignore //@ts-ignore
@ -51,7 +77,8 @@ router.get("/", route({}), async (req: Request, res: Response) => {
const messages: Message[] = await Message.find(query); const messages: Message[] = await Message.find(query);
const messagesDto = messages.map(x => [{ const messagesDto = messages.map((x) => [
{
id: x.id, id: x.id,
type: x.type, type: x.type,
content: x.content, content: x.content,
@ -76,7 +103,8 @@ router.get("/", route({}), async (req: Request, res: Response) => {
flags: x.flags, flags: x.flags,
components: x.components, components: x.components,
hit: true, hit: true,
}]); },
]);
return res.json({ return res.json({
messages: messagesDto, messages: messagesDto,

View File

@ -5,7 +5,12 @@ import { route } from "@fosscord/api";
const router = Router(); const router = Router();
//Returns all inactive members, respecting role hierarchy //Returns all inactive members, respecting role hierarchy
export const inactiveMembers = async (guild_id: string, user_id: string, days: number, roles: string[] = []) => { export const inactiveMembers = async (
guild_id: string,
user_id: string,
days: number,
roles: string[] = [],
) => {
var date = new Date(); var date = new Date();
date.setDate(date.getDate() - days); date.setDate(date.getDate() - days);
//Snowflake should have `generateFromTime` method? Or similar? //Snowflake should have `generateFromTime` method? Or similar?
@ -19,21 +24,27 @@ export const inactiveMembers = async (guild_id: string, user_id: string, days: n
where: [ where: [
{ {
guild_id, guild_id,
last_message_id: LessThan(minId.toString()) last_message_id: LessThan(minId.toString()),
}, },
{ {
last_message_id: IsNull() last_message_id: IsNull(),
} },
], ],
relations: ["roles"] relations: ["roles"],
}); });
console.log(members); console.log(members);
if (!members.length) return []; if (!members.length) return [];
//I'm sure I can do this in the above db query ( and it would probably be better to do so ), but oh well. //I'm sure I can do this in the above db query ( and it would probably be better to do so ), but oh well.
if (roles.length && members.length) members = members.filter((user) => user.roles?.some((role) => roles.includes(role.id))); if (roles.length && members.length)
members = members.filter((user) =>
user.roles?.some((role) => roles.includes(role.id)),
);
const me = await Member.findOneOrFail({ where: { id: user_id, guild_id }, relations: ["roles"] }); const me = await Member.findOneOrFail({
where: { id: user_id, guild_id },
relations: ["roles"],
});
const myHighestRole = Math.max(...(me.roles?.map((x) => x.position) || [])); const myHighestRole = Math.max(...(me.roles?.map((x) => x.position) || []));
const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
@ -44,8 +55,8 @@ export const inactiveMembers = async (guild_id: string, user_id: string, days: n
member.roles?.some( member.roles?.some(
(role) => (role) =>
role.position < myHighestRole || //roles higher than me can't be kicked role.position < myHighestRole || //roles higher than me can't be kicked
me.id === guild.owner_id //owner can kick anyone me.id === guild.owner_id, //owner can kick anyone
) ),
); );
return members; return members;
@ -57,23 +68,39 @@ router.get("/", route({}), async (req: Request, res: Response) => {
var roles = req.query.include_roles; var roles = req.query.include_roles;
if (typeof roles === "string") roles = [roles]; //express will return array otherwise if (typeof roles === "string") roles = [roles]; //express will return array otherwise
const members = await inactiveMembers(req.params.guild_id, req.user_id, days, roles as string[]); const members = await inactiveMembers(
req.params.guild_id,
req.user_id,
days,
roles as string[],
);
res.send({ pruned: members.length }); res.send({ pruned: members.length });
}); });
router.post("/", route({ permission: "KICK_MEMBERS", right: "KICK_BAN_MEMBERS" }), async (req: Request, res: Response) => { router.post(
"/",
route({ permission: "KICK_MEMBERS", right: "KICK_BAN_MEMBERS" }),
async (req: Request, res: Response) => {
const days = parseInt(req.body.days); const days = parseInt(req.body.days);
var roles = req.query.include_roles; var roles = req.query.include_roles;
if (typeof roles === "string") roles = [roles]; if (typeof roles === "string") roles = [roles];
const { guild_id } = req.params; const { guild_id } = req.params;
const members = await inactiveMembers(guild_id, req.user_id, days, roles as string[]); const members = await inactiveMembers(
guild_id,
req.user_id,
days,
roles as string[],
);
await Promise.all(members.map((x) => Member.removeFromGuild(x.id, guild_id))); await Promise.all(
members.map((x) => Member.removeFromGuild(x.id, guild_id)),
);
res.send({ purged: members.length }); res.send({ purged: members.length });
}); },
);
export default router; export default router;

View File

@ -9,7 +9,12 @@ router.get("/", route({}), async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id } = req.params;
const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
//TODO we should use an enum for guild's features and not hardcoded strings //TODO we should use an enum for guild's features and not hardcoded strings
return res.json(await getVoiceRegions(getIpAdress(req), guild.features.includes("VIP_REGIONS"))); return res.json(
await getVoiceRegions(
getIpAdress(req),
guild.features.includes("VIP_REGIONS"),
),
);
}); });
export default router; export default router;

View File

@ -1,5 +1,13 @@
import { Router, Request, Response } from "express"; import { Router, Request, Response } from "express";
import { Role, Member, GuildRoleUpdateEvent, GuildRoleDeleteEvent, emitEvent, handleFile, RoleModifySchema } from "@fosscord/util"; import {
Role,
Member,
GuildRoleUpdateEvent,
GuildRoleDeleteEvent,
emitEvent,
handleFile,
RoleModifySchema,
} from "@fosscord/util";
import { route } from "@fosscord/api"; import { route } from "@fosscord/api";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
@ -12,42 +20,56 @@ router.get("/", route({}), async (req: Request, res: Response) => {
return res.json(role); return res.json(role);
}); });
router.delete("/", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { router.delete(
"/",
route({ permission: "MANAGE_ROLES" }),
async (req: Request, res: Response) => {
const { guild_id, role_id } = req.params; const { guild_id, role_id } = req.params;
if (role_id === guild_id) throw new HTTPError("You can't delete the @everyone role"); if (role_id === guild_id)
throw new HTTPError("You can't delete the @everyone role");
await Promise.all([ await Promise.all([
Role.delete({ Role.delete({
id: role_id, id: role_id,
guild_id: guild_id guild_id: guild_id,
}), }),
emitEvent({ emitEvent({
event: "GUILD_ROLE_DELETE", event: "GUILD_ROLE_DELETE",
guild_id, guild_id,
data: { data: {
guild_id, guild_id,
role_id role_id,
} },
} as GuildRoleDeleteEvent) } as GuildRoleDeleteEvent),
]); ]);
res.sendStatus(204); res.sendStatus(204);
}); },
);
// TODO: check role hierarchy // TODO: check role hierarchy
router.patch("/", route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { router.patch(
"/",
route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }),
async (req: Request, res: Response) => {
const { role_id, guild_id } = req.params; const { role_id, guild_id } = req.params;
const body = req.body as RoleModifySchema; const body = req.body as RoleModifySchema;
if (body.icon && body.icon.length) body.icon = await handleFile(`/role-icons/${role_id}`, body.icon as string); if (body.icon && body.icon.length)
body.icon = await handleFile(
`/role-icons/${role_id}`,
body.icon as string,
);
else body.icon = undefined; else body.icon = undefined;
const role = Role.create({ const role = Role.create({
...body, ...body,
id: role_id, id: role_id,
guild_id, guild_id,
permissions: String(req.permission!.bitfield & BigInt(body.permissions || "0")) permissions: String(
req.permission!.bitfield & BigInt(body.permissions || "0"),
),
}); });
await Promise.all([ await Promise.all([
@ -57,12 +79,13 @@ router.patch("/", route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }
guild_id, guild_id,
data: { data: {
guild_id, guild_id,
role role,
} },
} as GuildRoleUpdateEvent) } as GuildRoleUpdateEvent),
]); ]);
res.json(role); res.json(role);
}); },
);
export default router; export default router;

View File

@ -29,14 +29,18 @@ router.get("/", route({}), async (req: Request, res: Response) => {
return res.json(roles); return res.json(roles);
}); });
router.post("/", route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { router.post(
"/",
route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }),
async (req: Request, res: Response) => {
const guild_id = req.params.guild_id; const guild_id = req.params.guild_id;
const body = req.body as RoleModifySchema; const body = req.body as RoleModifySchema;
const role_count = await Role.count({ where: { guild_id } }); const role_count = await Role.count({ where: { guild_id } });
const { maxRoles } = Config.get().limits.guild; const { maxRoles } = Config.get().limits.guild;
if (role_count > maxRoles) throw DiscordApiErrors.MAXIMUM_ROLES.withParams(maxRoles); if (role_count > maxRoles)
throw DiscordApiErrors.MAXIMUM_ROLES.withParams(maxRoles);
const role = Role.create({ const role = Role.create({
// values before ...body are default and can be overriden // values before ...body are default and can be overriden
@ -47,10 +51,12 @@ router.post("/", route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" })
...body, ...body,
guild_id: guild_id, guild_id: guild_id,
managed: false, managed: false,
permissions: String(req.permission!.bitfield & BigInt(body.permissions || "0")), permissions: String(
req.permission!.bitfield & BigInt(body.permissions || "0"),
),
tags: undefined, tags: undefined,
icon: undefined, icon: undefined,
unicode_emoji: undefined unicode_emoji: undefined,
}); });
await Promise.all([ await Promise.all([
@ -60,24 +66,34 @@ router.post("/", route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" })
guild_id, guild_id,
data: { data: {
guild_id, guild_id,
role: role role: role,
} },
} as GuildRoleCreateEvent) } as GuildRoleCreateEvent),
]); ]);
res.json(role); res.json(role);
}); },
);
router.patch("/", route({ body: "RolePositionUpdateSchema" }), async (req: Request, res: Response) => { router.patch(
"/",
route({ body: "RolePositionUpdateSchema" }),
async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id } = req.params;
const body = req.body as RolePositionUpdateSchema; const body = req.body as RolePositionUpdateSchema;
const perms = await getPermission(req.user_id, guild_id); const perms = await getPermission(req.user_id, guild_id);
perms.hasThrow("MANAGE_ROLES"); perms.hasThrow("MANAGE_ROLES");
await Promise.all(body.map(async (x) => Role.update({ guild_id, id: x.id }, { position: x.position }))); await Promise.all(
body.map(async (x) =>
Role.update({ guild_id, id: x.id }, { position: x.position }),
),
);
const roles = await Role.find({ where: body.map((x) => ({ id: x.id, guild_id })) }); const roles = await Role.find({
where: body.map((x) => ({ id: x.id, guild_id })),
});
await Promise.all( await Promise.all(
roles.map((x) => roles.map((x) =>
@ -86,13 +102,14 @@ router.patch("/", route({ body: "RolePositionUpdateSchema" }), async (req: Reque
guild_id, guild_id,
data: { data: {
guild_id, guild_id,
role: x role: x,
} },
} as GuildRoleUpdateEvent) } as GuildRoleUpdateEvent),
) ),
); );
res.json(roles); res.json(roles);
}); },
);
export default router; export default router;

View File

@ -26,15 +26,18 @@ const bodyParser = multer({
limits: { limits: {
fileSize: 1024 * 1024 * 100, fileSize: 1024 * 1024 * 100,
fields: 10, fields: 10,
files: 1 files: 1,
}, },
storage: multer.memoryStorage() storage: multer.memoryStorage(),
}).single("file"); }).single("file");
router.post( router.post(
"/", "/",
bodyParser, bodyParser,
route({ permission: "MANAGE_EMOJIS_AND_STICKERS", body: "ModifyGuildStickerSchema" }), route({
permission: "MANAGE_EMOJIS_AND_STICKERS",
body: "ModifyGuildStickerSchema",
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
if (!req.file) throw new HTTPError("missing file"); if (!req.file) throw new HTTPError("missing file");
@ -49,15 +52,15 @@ router.post(
id, id,
type: StickerType.GUILD, type: StickerType.GUILD,
format_type: getStickerFormat(req.file.mimetype), format_type: getStickerFormat(req.file.mimetype),
available: true available: true,
}).save(), }).save(),
uploadFile(`/stickers/${id}`, req.file) uploadFile(`/stickers/${id}`, req.file),
]); ]);
await sendStickerUpdateEvent(guild_id); await sendStickerUpdateEvent(guild_id);
res.json(sticker); res.json(sticker);
} },
); );
export function getStickerFormat(mime_type: string) { export function getStickerFormat(mime_type: string) {
@ -71,7 +74,9 @@ export function getStickerFormat(mime_type: string) {
case "image/gif": case "image/gif":
return StickerFormatType.GIF; return StickerFormatType.GIF;
default: default:
throw new HTTPError("invalid sticker format: must be png, apng or lottie"); throw new HTTPError(
"invalid sticker format: must be png, apng or lottie",
);
} }
} }
@ -79,21 +84,30 @@ router.get("/:sticker_id", route({}), async (req: Request, res: Response) => {
const { guild_id, sticker_id } = req.params; const { guild_id, sticker_id } = req.params;
await Member.IsInGuildOrFail(req.user_id, guild_id); await Member.IsInGuildOrFail(req.user_id, guild_id);
res.json(await Sticker.findOneOrFail({ where: { guild_id, id: sticker_id } })); res.json(
await Sticker.findOneOrFail({ where: { guild_id, id: sticker_id } }),
);
}); });
router.patch( router.patch(
"/:sticker_id", "/:sticker_id",
route({ body: "ModifyGuildStickerSchema", permission: "MANAGE_EMOJIS_AND_STICKERS" }), route({
body: "ModifyGuildStickerSchema",
permission: "MANAGE_EMOJIS_AND_STICKERS",
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { guild_id, sticker_id } = req.params; const { guild_id, sticker_id } = req.params;
const body = req.body as ModifyGuildStickerSchema; const body = req.body as ModifyGuildStickerSchema;
const sticker = await Sticker.create({ ...body, guild_id, id: sticker_id }).save(); const sticker = await Sticker.create({
...body,
guild_id,
id: sticker_id,
}).save();
await sendStickerUpdateEvent(guild_id); await sendStickerUpdateEvent(guild_id);
return res.json(sticker); return res.json(sticker);
} },
); );
async function sendStickerUpdateEvent(guild_id: string) { async function sendStickerUpdateEvent(guild_id: string) {
@ -102,18 +116,22 @@ async function sendStickerUpdateEvent(guild_id: string) {
guild_id: guild_id, guild_id: guild_id,
data: { data: {
guild_id: guild_id, guild_id: guild_id,
stickers: await Sticker.find({ where: { guild_id: guild_id } }) stickers: await Sticker.find({ where: { guild_id: guild_id } }),
} },
} as GuildStickersUpdateEvent); } as GuildStickersUpdateEvent);
} }
router.delete("/:sticker_id", route({ permission: "MANAGE_EMOJIS_AND_STICKERS" }), async (req: Request, res: Response) => { router.delete(
"/:sticker_id",
route({ permission: "MANAGE_EMOJIS_AND_STICKERS" }),
async (req: Request, res: Response) => {
const { guild_id, sticker_id } = req.params; const { guild_id, sticker_id } = req.params;
await Sticker.delete({ guild_id, id: sticker_id }); await Sticker.delete({ guild_id, id: sticker_id });
await sendStickerUpdateEvent(guild_id); await sendStickerUpdateEvent(guild_id);
return res.sendStatus(204); return res.sendStatus(204);
}); },
);
export default router; export default router;

View File

@ -20,21 +20,31 @@ const TemplateGuildProjection: (keyof Guild)[] = [
"afk_channel_id", "afk_channel_id",
"system_channel_id", "system_channel_id",
"system_channel_flags", "system_channel_flags",
"icon" "icon",
]; ];
router.get("/", route({}), async (req: Request, res: Response) => { router.get("/", route({}), async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id } = req.params;
var templates = await Template.find({ where: { source_guild_id: guild_id } }); var templates = await Template.find({
where: { source_guild_id: guild_id },
});
return res.json(templates); return res.json(templates);
}); });
router.post("/", route({ body: "TemplateCreateSchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { router.post(
"/",
route({ body: "TemplateCreateSchema", permission: "MANAGE_GUILD" }),
async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id } = req.params;
const guild = await Guild.findOneOrFail({ where: { id: guild_id }, select: TemplateGuildProjection }); const guild = await Guild.findOneOrFail({
const exists = await Template.findOneOrFail({ where: { id: guild_id } }).catch((e) => { }); where: { id: guild_id },
select: TemplateGuildProjection,
});
const exists = await Template.findOneOrFail({
where: { id: guild_id },
}).catch((e) => {});
if (exists) throw new HTTPError("Template already exists", 400); if (exists) throw new HTTPError("Template already exists", 400);
const template = await Template.create({ const template = await Template.create({
@ -44,39 +54,63 @@ router.post("/", route({ body: "TemplateCreateSchema", permission: "MANAGE_GUILD
created_at: new Date(), created_at: new Date(),
updated_at: new Date(), updated_at: new Date(),
source_guild_id: guild_id, source_guild_id: guild_id,
serialized_source_guild: guild serialized_source_guild: guild,
}).save(); }).save();
res.json(template); res.json(template);
}); },
);
router.delete("/:code", route({ permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { router.delete(
"/:code",
route({ permission: "MANAGE_GUILD" }),
async (req: Request, res: Response) => {
const { code, guild_id } = req.params; const { code, guild_id } = req.params;
const template = await Template.delete({ const template = await Template.delete({
code, code,
source_guild_id: guild_id source_guild_id: guild_id,
}); });
res.json(template); res.json(template);
}); },
);
router.put("/:code", route({ permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { router.put(
"/:code",
route({ permission: "MANAGE_GUILD" }),
async (req: Request, res: Response) => {
const { code, guild_id } = req.params; const { code, guild_id } = req.params;
const guild = await Guild.findOneOrFail({ where: { id: guild_id }, select: TemplateGuildProjection }); const guild = await Guild.findOneOrFail({
where: { id: guild_id },
const template = await Template.create({ code, serialized_source_guild: guild }).save(); select: TemplateGuildProjection,
res.json(template);
}); });
router.patch("/:code", route({ body: "TemplateModifySchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { const template = await Template.create({
code,
serialized_source_guild: guild,
}).save();
res.json(template);
},
);
router.patch(
"/:code",
route({ body: "TemplateModifySchema", permission: "MANAGE_GUILD" }),
async (req: Request, res: Response) => {
const { code, guild_id } = req.params; const { code, guild_id } = req.params;
const { name, description } = req.body; const { name, description } = req.body;
const template = await Template.create({ code, name: name, description: description, source_guild_id: guild_id }).save(); const template = await Template.create({
code,
name: name,
description: description,
source_guild_id: guild_id,
}).save();
res.json(template); res.json(template);
}); },
);
export default router; export default router;

View File

@ -1,4 +1,10 @@
import { Channel, ChannelType, Guild, Invite, VanityUrlSchema } from "@fosscord/util"; import {
Channel,
ChannelType,
Guild,
Invite,
VanityUrlSchema,
} from "@fosscord/util";
import { Router, Request, Response } from "express"; import { Router, Request, Response } from "express";
import { route } from "@fosscord/api"; import { route } from "@fosscord/api";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
@ -7,37 +13,54 @@ const router = Router();
const InviteRegex = /\W/g; const InviteRegex = /\W/g;
router.get("/", route({ permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { router.get(
"/",
route({ permission: "MANAGE_GUILD" }),
async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id } = req.params;
const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
if (!guild.features.includes("ALIASABLE_NAMES")) { if (!guild.features.includes("ALIASABLE_NAMES")) {
const invite = await Invite.findOne({ where: { guild_id: guild_id, vanity_url: true } }); const invite = await Invite.findOne({
where: { guild_id: guild_id, vanity_url: true },
});
if (!invite) return res.json({ code: null }); if (!invite) return res.json({ code: null });
return res.json({ code: invite.code, uses: invite.uses }); return res.json({ code: invite.code, uses: invite.uses });
} else { } else {
const invite = await Invite.find({ where: { guild_id: guild_id, vanity_url: true } }); const invite = await Invite.find({
where: { guild_id: guild_id, vanity_url: true },
});
if (!invite || invite.length == 0) return res.json({ code: null }); if (!invite || invite.length == 0) return res.json({ code: null });
return res.json(invite.map((x) => ({ code: x.code, uses: x.uses }))); return res.json(
invite.map((x) => ({ code: x.code, uses: x.uses })),
);
} }
}); },
);
router.patch("/", route({ body: "VanityUrlSchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { router.patch(
"/",
route({ body: "VanityUrlSchema", permission: "MANAGE_GUILD" }),
async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id } = req.params;
const body = req.body as VanityUrlSchema; const body = req.body as VanityUrlSchema;
const code = body.code?.replace(InviteRegex, ""); const code = body.code?.replace(InviteRegex, "");
const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
if (!guild.features.includes("VANITY_URL")) throw new HTTPError("Your guild doesn't support vanity urls"); if (!guild.features.includes("VANITY_URL"))
throw new HTTPError("Your guild doesn't support vanity urls");
if (!code || code.length === 0) throw new HTTPError("Code cannot be null or empty"); if (!code || code.length === 0)
throw new HTTPError("Code cannot be null or empty");
const invite = await Invite.findOne({ where: { code } }); const invite = await Invite.findOne({ where: { code } });
if (invite) throw new HTTPError("Invite already exists"); if (invite) throw new HTTPError("Invite already exists");
const { id } = await Channel.findOneOrFail({ where: { guild_id, type: ChannelType.GUILD_TEXT } }); const { id } = await Channel.findOneOrFail({
where: { guild_id, type: ChannelType.GUILD_TEXT },
});
await Invite.create({ await Invite.create({
vanity_url: true, vanity_url: true,
@ -49,10 +72,11 @@ router.patch("/", route({ body: "VanityUrlSchema", permission: "MANAGE_GUILD" })
created_at: new Date(), created_at: new Date(),
expires_at: new Date(), expires_at: new Date(),
guild_id: guild_id, guild_id: guild_id,
channel_id: id channel_id: id,
}).save(); }).save();
return res.json({ code: code }); return res.json({ code: code });
}); },
);
export default router; export default router;

View File

@ -1,16 +1,32 @@
import { Channel, ChannelType, DiscordApiErrors, emitEvent, getPermission, VoiceState, VoiceStateUpdateEvent, VoiceStateUpdateSchema } from "@fosscord/util"; import {
Channel,
ChannelType,
DiscordApiErrors,
emitEvent,
getPermission,
VoiceState,
VoiceStateUpdateEvent,
VoiceStateUpdateSchema,
} from "@fosscord/util";
import { route } from "@fosscord/api"; import { route } from "@fosscord/api";
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
//TODO need more testing when community guild and voice stage channel are working //TODO need more testing when community guild and voice stage channel are working
router.patch("/", route({ body: "VoiceStateUpdateSchema" }), async (req: Request, res: Response) => { router.patch(
"/",
route({ body: "VoiceStateUpdateSchema" }),
async (req: Request, res: Response) => {
const body = req.body as VoiceStateUpdateSchema; const body = req.body as VoiceStateUpdateSchema;
var { guild_id, user_id } = req.params; var { guild_id, user_id } = req.params;
if (user_id === "@me") user_id = req.user_id; if (user_id === "@me") user_id = req.user_id;
const perms = await getPermission(req.user_id, guild_id, body.channel_id); const perms = await getPermission(
req.user_id,
guild_id,
body.channel_id,
);
/* /*
From https://discord.com/developers/docs/resources/guild#modify-current-user-voice-state From https://discord.com/developers/docs/resources/guild#modify-current-user-voice-state
@ -27,13 +43,15 @@ router.patch("/", route({ body: "VoiceStateUpdateSchema" }), async (req: Request
where: { where: {
guild_id, guild_id,
channel_id: body.channel_id, channel_id: body.channel_id,
user_id user_id,
} },
}); });
if (!voice_state) throw DiscordApiErrors.UNKNOWN_VOICE_STATE; if (!voice_state) throw DiscordApiErrors.UNKNOWN_VOICE_STATE;
voice_state.assign(body); voice_state.assign(body);
const channel = await Channel.findOneOrFail({ where: { guild_id, id: body.channel_id } }); const channel = await Channel.findOneOrFail({
where: { guild_id, id: body.channel_id },
});
if (channel.type !== ChannelType.GUILD_STAGE_VOICE) { if (channel.type !== ChannelType.GUILD_STAGE_VOICE) {
throw DiscordApiErrors.CANNOT_EXECUTE_ON_THIS_CHANNEL_TYPE; throw DiscordApiErrors.CANNOT_EXECUTE_ON_THIS_CHANNEL_TYPE;
} }
@ -43,10 +61,11 @@ router.patch("/", route({ body: "VoiceStateUpdateSchema" }), async (req: Request
emitEvent({ emitEvent({
event: "VOICE_STATE_UPDATE", event: "VOICE_STATE_UPDATE",
data: voice_state, data: voice_state,
guild_id guild_id,
} as VoiceStateUpdateEvent) } as VoiceStateUpdateEvent),
]); ]);
return res.sendStatus(204); return res.sendStatus(204);
}); },
);
export default router; export default router;

View File

@ -14,20 +14,30 @@ router.get("/", route({}), async (req: Request, res: Response) => {
res.json(guild.welcome_screen); res.json(guild.welcome_screen);
}); });
router.patch("/", route({ body: "GuildUpdateWelcomeScreenSchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { router.patch(
"/",
route({
body: "GuildUpdateWelcomeScreenSchema",
permission: "MANAGE_GUILD",
}),
async (req: Request, res: Response) => {
const guild_id = req.params.guild_id; const guild_id = req.params.guild_id;
const body = req.body as GuildUpdateWelcomeScreenSchema; const body = req.body as GuildUpdateWelcomeScreenSchema;
const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
if (!guild.welcome_screen.enabled) throw new HTTPError("Welcome screen disabled", 400); if (!guild.welcome_screen.enabled)
if (body.welcome_channels) guild.welcome_screen.welcome_channels = body.welcome_channels; // TODO: check if they exist and are valid throw new HTTPError("Welcome screen disabled", 400);
if (body.description) guild.welcome_screen.description = body.description; if (body.welcome_channels)
guild.welcome_screen.welcome_channels = body.welcome_channels; // TODO: check if they exist and are valid
if (body.description)
guild.welcome_screen.description = body.description;
if (body.enabled != null) guild.welcome_screen.enabled = body.enabled; if (body.enabled != null) guild.welcome_screen.enabled = body.enabled;
await guild.save(); await guild.save();
res.sendStatus(204); res.sendStatus(204);
}); },
);
export default router; export default router;

View File

@ -1,5 +1,12 @@
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
import { Config, Permissions, Guild, Invite, Channel, Member } from "@fosscord/util"; import {
Config,
Permissions,
Guild,
Invite,
Channel,
Member,
} from "@fosscord/util";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { random, route } from "@fosscord/api"; import { random, route } from "@fosscord/api";
@ -21,7 +28,9 @@ router.get("/", route({}), async (req: Request, res: Response) => {
if (!guild.widget_enabled) throw new HTTPError("Widget Disabled", 404); if (!guild.widget_enabled) throw new HTTPError("Widget Disabled", 404);
// Fetch existing widget invite for widget channel // Fetch existing widget invite for widget channel
var invite = await Invite.findOne({ where: { channel_id: guild.widget_channel_id } }); var invite = await Invite.findOne({
where: { channel_id: guild.widget_channel_id },
});
if (guild.widget_channel_id && !invite) { if (guild.widget_channel_id && !invite) {
// Create invite for channel if none exists // Create invite for channel if none exists
@ -45,16 +54,24 @@ router.get("/", route({}), async (req: Request, res: Response) => {
// Fetch voice channels, and the @everyone permissions object // Fetch voice channels, and the @everyone permissions object
const channels = [] as any[]; const channels = [] as any[];
(await Channel.find({ where: { guild_id: guild_id, type: 2 }, order: { position: "ASC" } })).filter((doc) => { (
await Channel.find({
where: { guild_id: guild_id, type: 2 },
order: { position: "ASC" },
})
).filter((doc) => {
// Only return channels where @everyone has the CONNECT permission // Only return channels where @everyone has the CONNECT permission
if ( if (
doc.permission_overwrites === undefined || doc.permission_overwrites === undefined ||
Permissions.channelPermission(doc.permission_overwrites, Permissions.FLAGS.CONNECT) === Permissions.FLAGS.CONNECT Permissions.channelPermission(
doc.permission_overwrites,
Permissions.FLAGS.CONNECT,
) === Permissions.FLAGS.CONNECT
) { ) {
channels.push({ channels.push({
id: doc.id, id: doc.id,
name: doc.name, name: doc.name,
position: doc.position position: doc.position,
}); });
} }
}); });
@ -70,7 +87,7 @@ router.get("/", route({}), async (req: Request, res: Response) => {
instant_invite: invite?.code, instant_invite: invite?.code,
channels: channels, channels: channels,
members: members, members: members,
presence_count: guild.presence_count presence_count: guild.presence_count,
}; };
res.set("Cache-Control", "public, max-age=300"); res.set("Cache-Control", "public, max-age=300");

View File

@ -24,8 +24,13 @@ router.get("/", route({}), async (req: Request, res: Response) => {
// Fetch parameter // Fetch parameter
const style = req.query.style?.toString() || "shield"; const style = req.query.style?.toString() || "shield";
if (!["shield", "banner1", "banner2", "banner3", "banner4"].includes(style)) { if (
throw new HTTPError("Value must be one of ('shield', 'banner1', 'banner2', 'banner3', 'banner4').", 400); !["shield", "banner1", "banner2", "banner3", "banner4"].includes(style)
) {
throw new HTTPError(
"Value must be one of ('shield', 'banner1', 'banner2', 'banner3', 'banner4').",
400,
);
} }
// Setup canvas // Setup canvas
@ -34,7 +39,17 @@ router.get("/", route({}), async (req: Request, res: Response) => {
const sizeOf = require("image-size"); const sizeOf = require("image-size");
// TODO: Widget style templates need Fosscord branding // TODO: Widget style templates need Fosscord branding
const source = path.join(__dirname, "..", "..", "..", "..", "..", "assets", "widget", `${style}.png`); const source = path.join(
__dirname,
"..",
"..",
"..",
"..",
"..",
"assets",
"widget",
`${style}.png`,
);
if (!fs.existsSync(source)) { if (!fs.existsSync(source)) {
throw new HTTPError("Widget template does not exist.", 400); throw new HTTPError("Widget template does not exist.", 400);
} }
@ -50,30 +65,68 @@ router.get("/", route({}), async (req: Request, res: Response) => {
switch (style) { switch (style) {
case "shield": case "shield":
ctx.textAlign = "center"; ctx.textAlign = "center";
await drawText(ctx, 73, 13, "#FFFFFF", "thin 10px Verdana", presence); await drawText(
ctx,
73,
13,
"#FFFFFF",
"thin 10px Verdana",
presence,
);
break; break;
case "banner1": case "banner1":
if (icon) await drawIcon(ctx, 20, 27, 50, icon); if (icon) await drawIcon(ctx, 20, 27, 50, icon);
await drawText(ctx, 83, 51, "#FFFFFF", "12px Verdana", name, 22); await drawText(ctx, 83, 51, "#FFFFFF", "12px Verdana", name, 22);
await drawText(ctx, 83, 66, "#C9D2F0FF", "thin 11px Verdana", presence); await drawText(
ctx,
83,
66,
"#C9D2F0FF",
"thin 11px Verdana",
presence,
);
break; break;
case "banner2": case "banner2":
if (icon) await drawIcon(ctx, 13, 19, 36, icon); if (icon) await drawIcon(ctx, 13, 19, 36, icon);
await drawText(ctx, 62, 34, "#FFFFFF", "12px Verdana", name, 15); await drawText(ctx, 62, 34, "#FFFFFF", "12px Verdana", name, 15);
await drawText(ctx, 62, 49, "#C9D2F0FF", "thin 11px Verdana", presence); await drawText(
ctx,
62,
49,
"#C9D2F0FF",
"thin 11px Verdana",
presence,
);
break; break;
case "banner3": case "banner3":
if (icon) await drawIcon(ctx, 20, 20, 50, icon); if (icon) await drawIcon(ctx, 20, 20, 50, icon);
await drawText(ctx, 83, 44, "#FFFFFF", "12px Verdana", name, 27); await drawText(ctx, 83, 44, "#FFFFFF", "12px Verdana", name, 27);
await drawText(ctx, 83, 58, "#C9D2F0FF", "thin 11px Verdana", presence); await drawText(
ctx,
83,
58,
"#C9D2F0FF",
"thin 11px Verdana",
presence,
);
break; break;
case "banner4": case "banner4":
if (icon) await drawIcon(ctx, 21, 136, 50, icon); if (icon) await drawIcon(ctx, 21, 136, 50, icon);
await drawText(ctx, 84, 156, "#FFFFFF", "13px Verdana", name, 27); await drawText(ctx, 84, 156, "#FFFFFF", "13px Verdana", name, 27);
await drawText(ctx, 84, 171, "#C9D2F0FF", "thin 12px Verdana", presence); await drawText(
ctx,
84,
171,
"#C9D2F0FF",
"thin 12px Verdana",
presence,
);
break; break;
default: default:
throw new HTTPError("Value must be one of ('shield', 'banner1', 'banner2', 'banner3', 'banner4').", 400); throw new HTTPError(
"Value must be one of ('shield', 'banner1', 'banner2', 'banner3', 'banner4').",
400,
);
} }
// Return final image // Return final image
@ -83,7 +136,13 @@ router.get("/", route({}), async (req: Request, res: Response) => {
return res.send(buffer); return res.send(buffer);
}); });
async function drawIcon(canvas: any, x: number, y: number, scale: number, icon: string) { async function drawIcon(
canvas: any,
x: number,
y: number,
scale: number,
icon: string,
) {
// @ts-ignore // @ts-ignore
const img = new require("canvas").Image(); const img = new require("canvas").Image();
img.src = icon; img.src = icon;
@ -101,10 +160,19 @@ async function drawIcon(canvas: any, x: number, y: number, scale: number, icon:
canvas.restore(); canvas.restore();
} }
async function drawText(canvas: any, x: number, y: number, color: string, font: string, text: string, maxcharacters?: number) { async function drawText(
canvas: any,
x: number,
y: number,
color: string,
font: string,
text: string,
maxcharacters?: number,
) {
canvas.fillStyle = color; canvas.fillStyle = color;
canvas.font = font; canvas.font = font;
if (text.length > (maxcharacters || 0) && maxcharacters) text = text.slice(0, maxcharacters) + "..."; if (text.length > (maxcharacters || 0) && maxcharacters)
text = text.slice(0, maxcharacters) + "...";
canvas.fillText(text, x, y); canvas.fillText(text, x, y);
} }

View File

@ -10,18 +10,31 @@ router.get("/", route({}), async (req: Request, res: Response) => {
const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
return res.json({ enabled: guild.widget_enabled || false, channel_id: guild.widget_channel_id || null }); return res.json({
enabled: guild.widget_enabled || false,
channel_id: guild.widget_channel_id || null,
});
}); });
// https://discord.com/developers/docs/resources/guild#modify-guild-widget // https://discord.com/developers/docs/resources/guild#modify-guild-widget
router.patch("/", route({ body: "WidgetModifySchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { router.patch(
"/",
route({ body: "WidgetModifySchema", permission: "MANAGE_GUILD" }),
async (req: Request, res: Response) => {
const body = req.body as WidgetModifySchema; const body = req.body as WidgetModifySchema;
const { guild_id } = req.params; const { guild_id } = req.params;
await Guild.update({ id: guild_id }, { widget_enabled: body.enabled, widget_channel_id: body.channel_id }); await Guild.update(
{ id: guild_id },
{
widget_enabled: body.enabled,
widget_channel_id: body.channel_id,
},
);
// Widget invite for the widget_channel_id gets created as part of the /guilds/{guild.id}/widget.json request // Widget invite for the widget_channel_id gets created as part of the /guilds/{guild.id}/widget.json request
return res.json(body); return res.json(body);
}); },
);
export default router; export default router;

View File

@ -1,22 +1,36 @@
import { Router, Request, Response } from "express"; import { Router, Request, Response } from "express";
import { Role, Guild, Config, getRights, Member, DiscordApiErrors, GuildCreateSchema } from "@fosscord/util"; import {
Role,
Guild,
Config,
getRights,
Member,
DiscordApiErrors,
GuildCreateSchema,
} from "@fosscord/util";
import { route } from "@fosscord/api"; import { route } from "@fosscord/api";
const router: Router = Router(); const router: Router = Router();
//TODO: create default channel //TODO: create default channel
router.post("/", route({ body: "GuildCreateSchema", right: "CREATE_GUILDS" }), async (req: Request, res: Response) => { router.post(
"/",
route({ body: "GuildCreateSchema", right: "CREATE_GUILDS" }),
async (req: Request, res: Response) => {
const body = req.body as GuildCreateSchema; const body = req.body as GuildCreateSchema;
const { maxGuilds } = Config.get().limits.user; const { maxGuilds } = Config.get().limits.user;
const guild_count = await Member.count({ where: { id: req.user_id } }); const guild_count = await Member.count({ where: { id: req.user_id } });
const rights = await getRights(req.user_id); const rights = await getRights(req.user_id);
if ((guild_count >= maxGuilds) && !rights.has("MANAGE_GUILDS")) { if (guild_count >= maxGuilds && !rights.has("MANAGE_GUILDS")) {
throw DiscordApiErrors.MAXIMUM_GUILDS.withParams(maxGuilds); throw DiscordApiErrors.MAXIMUM_GUILDS.withParams(maxGuilds);
} }
const guild = await Guild.createGuild({ ...body, owner_id: req.user_id }); const guild = await Guild.createGuild({
...body,
owner_id: req.user_id,
});
const { autoJoin } = Config.get().guild; const { autoJoin } = Config.get().guild;
if (autoJoin.enabled && !autoJoin.guilds?.length) { if (autoJoin.enabled && !autoJoin.guilds?.length) {
@ -27,6 +41,7 @@ router.post("/", route({ body: "GuildCreateSchema", right: "CREATE_GUILDS" }), a
await Member.addToGuild(req.user_id, guild.id); await Member.addToGuild(req.user_id, guild.id);
res.status(201).json({ id: guild.id }); res.status(201).json({ id: guild.id });
}); },
);
export default router; export default router;

View File

@ -1,29 +1,58 @@
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
import { Template, Guild, Role, Snowflake, Config, Member, GuildTemplateCreateSchema } from "@fosscord/util"; import {
Template,
Guild,
Role,
Snowflake,
Config,
Member,
GuildTemplateCreateSchema,
} from "@fosscord/util";
import { route } from "@fosscord/api"; import { route } from "@fosscord/api";
import { DiscordApiErrors } from "@fosscord/util"; import { DiscordApiErrors } from "@fosscord/util";
import fetch from "node-fetch"; import fetch from "node-fetch";
const router: Router = Router(); const router: Router = Router();
router.get("/:code", route({}), async (req: Request, res: Response) => { router.get("/:code", route({}), async (req: Request, res: Response) => {
const { allowDiscordTemplates, allowRaws, enabled } = Config.get().templates; const { allowDiscordTemplates, allowRaws, enabled } =
if (!enabled) res.json({ code: 403, message: "Template creation & usage is disabled on this instance." }).sendStatus(403); Config.get().templates;
if (!enabled)
res.json({
code: 403,
message: "Template creation & usage is disabled on this instance.",
}).sendStatus(403);
const { code } = req.params; const { code } = req.params;
if (code.startsWith("discord:")) { if (code.startsWith("discord:")) {
if (!allowDiscordTemplates) return res.json({ code: 403, message: "Discord templates cannot be used on this instance." }).sendStatus(403); if (!allowDiscordTemplates)
return res
.json({
code: 403,
message:
"Discord templates cannot be used on this instance.",
})
.sendStatus(403);
const discordTemplateID = code.split("discord:", 2)[1]; const discordTemplateID = code.split("discord:", 2)[1];
const discordTemplateData = await fetch(`https://discord.com/api/v9/guilds/templates/${discordTemplateID}`, { const discordTemplateData = await fetch(
`https://discord.com/api/v9/guilds/templates/${discordTemplateID}`,
{
method: "get", method: "get",
headers: { "Content-Type": "application/json" } headers: { "Content-Type": "application/json" },
}); },
);
return res.json(await discordTemplateData.json()); return res.json(await discordTemplateData.json());
} }
if (code.startsWith("external:")) { if (code.startsWith("external:")) {
if (!allowRaws) return res.json({ code: 403, message: "Importing raws is disabled on this instance." }).sendStatus(403); if (!allowRaws)
return res
.json({
code: 403,
message: "Importing raws is disabled on this instance.",
})
.sendStatus(403);
return res.json(code.split("external:", 2)[1]); return res.json(code.split("external:", 2)[1]);
} }
@ -32,10 +61,31 @@ router.get("/:code", route({}), async (req: Request, res: Response) => {
res.json(template); res.json(template);
}); });
router.post("/:code", route({ body: "GuildTemplateCreateSchema" }), async (req: Request, res: Response) => { router.post(
const { enabled, allowTemplateCreation, allowDiscordTemplates, allowRaws } = Config.get().templates; "/:code",
if (!enabled) return res.json({ code: 403, message: "Template creation & usage is disabled on this instance." }).sendStatus(403); route({ body: "GuildTemplateCreateSchema" }),
if (!allowTemplateCreation) return res.json({ code: 403, message: "Template creation is disabled on this instance." }).sendStatus(403); async (req: Request, res: Response) => {
const {
enabled,
allowTemplateCreation,
allowDiscordTemplates,
allowRaws,
} = Config.get().templates;
if (!enabled)
return res
.json({
code: 403,
message:
"Template creation & usage is disabled on this instance.",
})
.sendStatus(403);
if (!allowTemplateCreation)
return res
.json({
code: 403,
message: "Template creation is disabled on this instance.",
})
.sendStatus(403);
const { code } = req.params; const { code } = req.params;
const body = req.body as GuildTemplateCreateSchema; const body = req.body as GuildTemplateCreateSchema;
@ -47,7 +97,9 @@ router.post("/:code", route({ body: "GuildTemplateCreateSchema" }), async (req:
throw DiscordApiErrors.MAXIMUM_GUILDS.withParams(maxGuilds); throw DiscordApiErrors.MAXIMUM_GUILDS.withParams(maxGuilds);
} }
const template = await Template.findOneOrFail({ where: { code: code } }); const template = await Template.findOneOrFail({
where: { code: code },
});
const guild_id = Snowflake.generate(); const guild_id = Snowflake.generate();
@ -56,7 +108,7 @@ router.post("/:code", route({ body: "GuildTemplateCreateSchema" }), async (req:
...body, ...body,
...template.serialized_source_guild, ...template.serialized_source_guild,
id: guild_id, id: guild_id,
owner_id: req.user_id owner_id: req.user_id,
}).save(), }).save(),
Role.create({ Role.create({
id: guild_id, id: guild_id,
@ -68,12 +120,13 @@ router.post("/:code", route({ body: "GuildTemplateCreateSchema" }), async (req:
name: "@everyone", name: "@everyone",
permissions: BigInt("2251804225").toString(), // TODO: where did this come from? permissions: BigInt("2251804225").toString(), // TODO: where did this come from?
position: 0, position: 0,
}).save() }).save(),
]); ]);
await Member.addToGuild(req.user_id, guild_id); await Member.addToGuild(req.user_id, guild_id);
res.status(201).json({ id: guild.id }); res.status(201).json({ id: guild.id });
}); },
);
export default router; export default router;

View File

@ -1,5 +1,13 @@
import { Router, Request, Response } from "express"; import { Router, Request, Response } from "express";
import { emitEvent, getPermission, Guild, Invite, InviteDeleteEvent, User, PublicInviteRelation } from "@fosscord/util"; import {
emitEvent,
getPermission,
Guild,
Invite,
InviteDeleteEvent,
User,
PublicInviteRelation,
} from "@fosscord/util";
import { route } from "@fosscord/api"; import { route } from "@fosscord/api";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
@ -8,24 +16,45 @@ const router: Router = Router();
router.get("/:code", route({}), async (req: Request, res: Response) => { router.get("/:code", route({}), async (req: Request, res: Response) => {
const { code } = req.params; const { code } = req.params;
const invite = await Invite.findOneOrFail({ where: { code }, relations: PublicInviteRelation }); const invite = await Invite.findOneOrFail({
where: { code },
relations: PublicInviteRelation,
});
res.status(200).send(invite); res.status(200).send(invite);
}); });
router.post("/:code", route({ right: "USE_MASS_INVITES" }), async (req: Request, res: Response) => { router.post(
"/:code",
route({ right: "USE_MASS_INVITES" }),
async (req: Request, res: Response) => {
const { code } = req.params; const { code } = req.params;
const { guild_id } = await Invite.findOneOrFail({ where: { code: code } }); const { guild_id } = await Invite.findOneOrFail({
const { features } = await Guild.findOneOrFail({ where: { id: guild_id } }); where: { code: code },
const { public_flags } = await User.findOneOrFail({ where: { id: req.user_id } }); });
const { features } = await Guild.findOneOrFail({
where: { id: guild_id },
});
const { public_flags } = await User.findOneOrFail({
where: { id: req.user_id },
});
if (features.includes("INTERNAL_EMPLOYEE_ONLY") && (public_flags & 1) !== 1) throw new HTTPError("Only intended for the staff of this server.", 401); if (
if (features.includes("INVITES_CLOSED")) throw new HTTPError("Sorry, this guild has joins closed.", 403); features.includes("INTERNAL_EMPLOYEE_ONLY") &&
(public_flags & 1) !== 1
)
throw new HTTPError(
"Only intended for the staff of this server.",
401,
);
if (features.includes("INVITES_CLOSED"))
throw new HTTPError("Sorry, this guild has joins closed.", 403);
const invite = await Invite.joinGuild(req.user_id, code); const invite = await Invite.joinGuild(req.user_id, code);
res.json(invite); res.json(invite);
}); },
);
// * cant use permission of route() function because path doesn't have guild_id/channel_id // * cant use permission of route() function because path doesn't have guild_id/channel_id
router.delete("/:code", route({}), async (req: Request, res: Response) => { router.delete("/:code", route({}), async (req: Request, res: Response) => {
@ -36,7 +65,10 @@ router.delete("/:code", route({}), async (req: Request, res: Response) => {
const permission = await getPermission(req.user_id, guild_id, channel_id); const permission = await getPermission(req.user_id, guild_id, channel_id);
if (!permission.has("MANAGE_GUILD") && !permission.has("MANAGE_CHANNELS")) if (!permission.has("MANAGE_GUILD") && !permission.has("MANAGE_CHANNELS"))
throw new HTTPError("You missing the MANAGE_GUILD or MANAGE_CHANNELS permission", 401); throw new HTTPError(
"You missing the MANAGE_GUILD or MANAGE_CHANNELS permission",
401,
);
await Promise.all([ await Promise.all([
Invite.delete({ code }), Invite.delete({ code }),
@ -46,9 +78,9 @@ router.delete("/:code", route({}), async (req: Request, res: Response) => {
data: { data: {
channel_id: channel_id, channel_id: channel_id,
guild_id: guild_id, guild_id: guild_id,
code: code code: code,
} },
} as InviteDeleteEvent) } as InviteDeleteEvent),
]); ]);
res.json({ invite: invite }); res.json({ invite: invite });

View File

@ -1,4 +1,3 @@
import { Guild, Config } from "@fosscord/util"; import { Guild, Config } from "@fosscord/util";
import { Router, Request, Response } from "express"; import { Router, Request, Response } from "express";
@ -31,9 +30,9 @@ router.get("/", route({}), async (req: Request, res: Response) => {
avg_nonnew_participators: 0, avg_nonnew_participators: 0,
avg_nonnew_communicators: 0, avg_nonnew_communicators: 0,
num_intentful_joiners: 0, num_intentful_joiners: 0,
perc_ret_w1_intentful: 0 perc_ret_w1_intentful: 0,
}, },
minimum_size: 0 minimum_size: 0,
}); });
}); });

View File

@ -1,7 +1,7 @@
import { Router, Request, Response } from "express"; import { Router, Request, Response } from "express";
import { route } from "@fosscord/api"; import { route } from "@fosscord/api";
import { Config } from "@fosscord/util"; import { Config } from "@fosscord/util";
import { config } from "dotenv" import { config } from "dotenv";
const router = Router(); const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get("/", route({}), async (req: Request, res: Response) => {
@ -9,7 +9,10 @@ router.get("/",route({}), async (req: Request, res: Response) => {
const IdentityForm = { const IdentityForm = {
cdn: cdn.endpointPublic || process.env.CDN || "http://localhost:3001", cdn: cdn.endpointPublic || process.env.CDN || "http://localhost:3001",
gateway: gateway.endpointPublic || process.env.GATEWAY || "ws://localhost:3002" gateway:
gateway.endpointPublic ||
process.env.GATEWAY ||
"ws://localhost:3002",
}; };
res.json(IdentityForm); res.json(IdentityForm);

View File

@ -3,7 +3,6 @@ import { route } from "@fosscord/api";
import { Config } from "@fosscord/util"; import { Config } from "@fosscord/util";
const router = Router(); const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get("/", route({}), async (req: Request, res: Response) => {
const { general } = Config.get(); const { general } = Config.get();
res.json(general); res.json(general);

View File

@ -2,11 +2,15 @@ import { Router, Request, Response } from "express";
import { route } from "@fosscord/api"; import { route } from "@fosscord/api";
const router = Router(); const router = Router();
router.get("/scheduled-maintenances/upcoming.json",route({}), async (req: Request, res: Response) => { router.get(
"/scheduled-maintenances/upcoming.json",
route({}),
async (req: Request, res: Response) => {
res.json({ res.json({
"page": {}, page: {},
"scheduled_maintenances": {} scheduled_maintenances: {},
});
}); });
},
);
export default router; export default router;

View File

@ -6,17 +6,19 @@ const router: Router = Router();
router.post("/", route({}), async (req: Request, res: Response) => { router.post("/", route({}), async (req: Request, res: Response) => {
//EXPERIMENTAL: have an "OPERATOR" platform permission implemented for this API route //EXPERIMENTAL: have an "OPERATOR" platform permission implemented for this API route
const user = await User.findOneOrFail({ where: { id: req.user_id }, select: ["rights"] }); const user = await User.findOneOrFail({
where: { id: req.user_id },
select: ["rights"],
});
if ((Number(user.rights) << Number(0)) % Number(2) == Number(1)) { if ((Number(user.rights) << Number(0)) % Number(2) == Number(1)) {
console.log("user that POSTed to the API was ALLOWED"); console.log("user that POSTed to the API was ALLOWED");
console.log(user.rights); console.log(user.rights);
res.sendStatus(200) res.sendStatus(200);
process.kill(process.pid, 'SIGTERM') process.kill(process.pid, "SIGTERM");
} } else {
else {
console.log("operation failed"); console.log("operation failed");
console.log(user.rights); console.log(user.rights);
res.sendStatus(403) res.sendStatus(403);
} }
}); });

View File

@ -41,29 +41,29 @@ router.get("/:id", route({}), async (req: Request, res: Response) => {
publishers: [ publishers: [
{ {
id: "", id: "",
name: "" name: "",
} },
], ],
developers: [ developers: [
{ {
id: "", id: "",
name: "" name: "",
} },
], ],
system_requirements: {}, system_requirements: {},
show_age_gate: false, show_age_gate: false,
price: { price: {
amount: 0, amount: 0,
currency: "EUR" currency: "EUR",
}, },
locales: [] locales: [],
}, },
tagline: "", tagline: "",
description: "", description: "",
carousel_items: [ carousel_items: [
{ {
asset_id: "" asset_id: "",
} },
], ],
header_logo_dark_theme: {}, //{id: "", size: 4665, mime_type: "image/gif", width 160, height: 160} header_logo_dark_theme: {}, //{id: "", size: 4665, mime_type: "image/gif", width 160, height: 160}
header_logo_light_theme: {}, header_logo_light_theme: {},
@ -71,8 +71,8 @@ router.get("/:id", route({}), async (req: Request, res: Response) => {
thumbnail: {}, thumbnail: {},
header_background: {}, header_background: {},
hero_background: {}, hero_background: {},
assets: [] assets: [],
} },
}).status(200); }).status(200);
}); });

Some files were not shown because too many files have changed in this diff Show More