mirror of
https://github.com/spacebarchat/server.git
synced 2024-11-22 10:22:39 +01:00
Prettier
This commit is contained in:
parent
e4dbf27a6a
commit
dbaf39237a
4
.github/relase_body_template.md
vendored
4
.github/relase_body_template.md
vendored
@ -1,11 +1,15 @@
|
|||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
## Additions
|
## Additions
|
||||||
|
|
||||||
-
|
-
|
||||||
|
|
||||||
## Fixes
|
## Fixes
|
||||||
|
|
||||||
-
|
-
|
||||||
|
|
||||||
## Download
|
## Download
|
||||||
|
|
||||||
- [Windows]()
|
- [Windows]()
|
||||||
- [MacOS]()
|
- [MacOS]()
|
||||||
- [Linux]()
|
- [Linux]()
|
||||||
|
2
.prettierignore
Normal file
2
.prettierignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
assets
|
||||||
|
dist
|
11
.prettierrc.json
Normal file
11
.prettierrc.json
Normal 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
8
.vscode/launch.json
vendored
@ -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
22
package-lock.json
generated
@ -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",
|
||||||
|
12
package.json
12
package.json
@ -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"
|
||||||
|
@ -48,7 +48,7 @@ function connect() {
|
|||||||
token,
|
token,
|
||||||
properties: {},
|
properties: {},
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
@ -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 = {};
|
||||||
|
@ -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();
|
||||||
|
@ -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);
|
||||||
|
@ -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();
|
||||||
|
5
src-slowcord/bot/.vscode/launch.json
vendored
5
src-slowcord/bot/.vscode/launch.json
vendored
@ -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"
|
||||||
}
|
}
|
||||||
|
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -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",
|
||||||
}]
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
@ -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. */
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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";
|
||||||
|
@ -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>
|
@ -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>
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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. */
|
||||||
|
@ -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
|
||||||
|
@ -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) => {
|
||||||
|
@ -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();
|
||||||
|
@ -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. */
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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) {
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import express, { Request, Response, Application } from "express";
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fetch, { Response as FetchResponse } from "node-fetch";
|
import fetch, { Response as FetchResponse } from "node-fetch";
|
||||||
import ProxyAgent from 'proxy-agent';
|
import ProxyAgent from "proxy-agent";
|
||||||
import { Config } from "@fosscord/util";
|
import { Config } from "@fosscord/util";
|
||||||
|
|
||||||
const ASSET_FOLDER_PATH = path.join(__dirname, "..", "..", "..", "assets");
|
const ASSET_FOLDER_PATH = path.join(__dirname, "..", "..", "..", "assets");
|
||||||
@ -11,40 +11,67 @@ let hasWarnedAboutCache = false;
|
|||||||
|
|
||||||
export default function TestClient(app: Application) {
|
export default function TestClient(app: Application) {
|
||||||
const agent = new ProxyAgent();
|
const agent = new ProxyAgent();
|
||||||
const assetCache = new Map<string, { response: FetchResponse; buffer: Buffer; }>();
|
const assetCache = new Map<
|
||||||
const indexHTML = fs.readFileSync(path.join(ASSET_FOLDER_PATH, "client_test", "index.html"), { encoding: "utf8" });
|
string,
|
||||||
|
{ response: FetchResponse; buffer: Buffer }
|
||||||
|
>();
|
||||||
|
const indexHTML = fs.readFileSync(
|
||||||
|
path.join(ASSET_FOLDER_PATH, "client_test", "index.html"),
|
||||||
|
{ encoding: "utf8" },
|
||||||
|
);
|
||||||
|
|
||||||
var html = indexHTML;
|
var html = indexHTML;
|
||||||
const CDN_ENDPOINT = (Config.get().cdn.endpointClient || Config.get()?.cdn.endpointPublic || process.env.CDN || "").replace(
|
const CDN_ENDPOINT = (
|
||||||
/(https?)?(:\/\/?)/g,
|
Config.get().cdn.endpointClient ||
|
||||||
|
Config.get()?.cdn.endpointPublic ||
|
||||||
|
process.env.CDN ||
|
||||||
""
|
""
|
||||||
);
|
).replace(/(https?)?(:\/\/?)/g, "");
|
||||||
const GATEWAY_ENDPOINT = Config.get().gateway.endpointClient || Config.get()?.gateway.endpointPublic || process.env.GATEWAY || "";
|
const GATEWAY_ENDPOINT =
|
||||||
|
Config.get().gateway.endpointClient ||
|
||||||
|
Config.get()?.gateway.endpointPublic ||
|
||||||
|
process.env.GATEWAY ||
|
||||||
|
"";
|
||||||
|
|
||||||
if (CDN_ENDPOINT) {
|
if (CDN_ENDPOINT) {
|
||||||
html = html.replace(/CDN_HOST: .+/, `CDN_HOST: \`${CDN_ENDPOINT}\`,`);
|
html = html.replace(/CDN_HOST: .+/, `CDN_HOST: \`${CDN_ENDPOINT}\`,`);
|
||||||
}
|
}
|
||||||
if (GATEWAY_ENDPOINT) {
|
if (GATEWAY_ENDPOINT) {
|
||||||
html = html.replace(/GATEWAY_ENDPOINT: .+/, `GATEWAY_ENDPOINT: \`${GATEWAY_ENDPOINT}\`,`);
|
html = html.replace(
|
||||||
|
/GATEWAY_ENDPOINT: .+/,
|
||||||
|
`GATEWAY_ENDPOINT: \`${GATEWAY_ENDPOINT}\`,`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// inline plugins
|
// inline plugins
|
||||||
var files = fs.readdirSync(path.join(ASSET_FOLDER_PATH, "preload-plugins"));
|
var files = fs.readdirSync(path.join(ASSET_FOLDER_PATH, "preload-plugins"));
|
||||||
var plugins = "";
|
var plugins = "";
|
||||||
files.forEach(x => { if (x.endsWith(".js")) plugins += `<script>${fs.readFileSync(path.join(ASSET_FOLDER_PATH, "preload-plugins", x))}</script>\n`; });
|
files.forEach((x) => {
|
||||||
|
if (x.endsWith(".js"))
|
||||||
|
plugins += `<script>${fs.readFileSync(
|
||||||
|
path.join(ASSET_FOLDER_PATH, "preload-plugins", x),
|
||||||
|
)}</script>\n`;
|
||||||
|
});
|
||||||
html = html.replaceAll("<!-- preload plugin marker -->", plugins);
|
html = html.replaceAll("<!-- preload plugin marker -->", plugins);
|
||||||
|
|
||||||
// plugins
|
// plugins
|
||||||
files = fs.readdirSync(path.join(ASSET_FOLDER_PATH, "plugins"));
|
files = fs.readdirSync(path.join(ASSET_FOLDER_PATH, "plugins"));
|
||||||
plugins = "";
|
plugins = "";
|
||||||
files.forEach(x => { if (x.endsWith(".js")) plugins += `<script src='/assets/plugins/${x}'></script>\n`; });
|
files.forEach((x) => {
|
||||||
|
if (x.endsWith(".js"))
|
||||||
|
plugins += `<script src='/assets/plugins/${x}'></script>\n`;
|
||||||
|
});
|
||||||
html = html.replaceAll("<!-- plugin marker -->", plugins);
|
html = html.replaceAll("<!-- plugin marker -->", plugins);
|
||||||
//preload plugins
|
//preload plugins
|
||||||
files = fs.readdirSync(path.join(ASSET_FOLDER_PATH, "preload-plugins"));
|
files = fs.readdirSync(path.join(ASSET_FOLDER_PATH, "preload-plugins"));
|
||||||
plugins = "";
|
plugins = "";
|
||||||
files.forEach(x => { if (x.endsWith(".js")) plugins += `<script>${fs.readFileSync(path.join(ASSET_FOLDER_PATH, "preload-plugins", x))}</script>\n`; });
|
files.forEach((x) => {
|
||||||
|
if (x.endsWith(".js"))
|
||||||
|
plugins += `<script>${fs.readFileSync(
|
||||||
|
path.join(ASSET_FOLDER_PATH, "preload-plugins", x),
|
||||||
|
)}</script>\n`;
|
||||||
|
});
|
||||||
html = html.replaceAll("<!-- preload plugin marker -->", plugins);
|
html = html.replaceAll("<!-- preload plugin marker -->", plugins);
|
||||||
|
|
||||||
|
|
||||||
app.use("/assets", express.static(path.join(ASSET_FOLDER_PATH, "public")));
|
app.use("/assets", express.static(path.join(ASSET_FOLDER_PATH, "public")));
|
||||||
app.use("/assets", express.static(path.join(ASSET_FOLDER_PATH, "cache")));
|
app.use("/assets", express.static(path.join(ASSET_FOLDER_PATH, "cache")));
|
||||||
|
|
||||||
@ -52,7 +79,9 @@ export default function TestClient(app: Application) {
|
|||||||
if (!hasWarnedAboutCache) {
|
if (!hasWarnedAboutCache) {
|
||||||
hasWarnedAboutCache = true;
|
hasWarnedAboutCache = true;
|
||||||
if (req.params.file.includes(".js"))
|
if (req.params.file.includes(".js"))
|
||||||
console.warn(`[TestClient] Cache miss for file ${req.params.file}! Use 'npm run generate:client' to cache and patch.`);
|
console.warn(
|
||||||
|
`[TestClient] Cache miss for file ${req.params.file}! Use 'npm run generate:client' to cache and patch.`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
delete req.headers.host;
|
delete req.headers.host;
|
||||||
@ -60,13 +89,16 @@ export default function TestClient(app: Application) {
|
|||||||
var buffer: Buffer;
|
var buffer: Buffer;
|
||||||
const cache = assetCache.get(req.params.file);
|
const cache = assetCache.get(req.params.file);
|
||||||
if (!cache) {
|
if (!cache) {
|
||||||
response = await fetch(`https://discord.com/assets/${req.params.file}`, {
|
response = await fetch(
|
||||||
|
`https://discord.com/assets/${req.params.file}`,
|
||||||
|
{
|
||||||
agent,
|
agent,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
headers: {
|
headers: {
|
||||||
...req.headers
|
...req.headers,
|
||||||
}
|
},
|
||||||
});
|
},
|
||||||
|
);
|
||||||
buffer = await response.buffer();
|
buffer = await response.buffer();
|
||||||
} else {
|
} else {
|
||||||
response = cache.response;
|
response = cache.response;
|
||||||
@ -83,7 +115,7 @@ export default function TestClient(app: Application) {
|
|||||||
"transfer-encoding",
|
"transfer-encoding",
|
||||||
"expect-ct",
|
"expect-ct",
|
||||||
"access-control-allow-origin",
|
"access-control-allow-origin",
|
||||||
"content-encoding"
|
"content-encoding",
|
||||||
].includes(name.toLowerCase())
|
].includes(name.toLowerCase())
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@ -99,19 +131,34 @@ export default function TestClient(app: Application) {
|
|||||||
res.set("Cache-Control", "public, max-age=" + 60 * 60 * 24);
|
res.set("Cache-Control", "public, max-age=" + 60 * 60 * 24);
|
||||||
res.set("content-type", "text/html");
|
res.set("content-type", "text/html");
|
||||||
|
|
||||||
if (!useTestClient) return res.send("Test client is disabled on this instance. Use a stand-alone client to connect this instance.");
|
if (!useTestClient)
|
||||||
|
return res.send(
|
||||||
|
"Test client is disabled on this instance. Use a stand-alone client to connect this instance.",
|
||||||
|
);
|
||||||
|
|
||||||
res.send(fs.readFileSync(path.join(ASSET_FOLDER_PATH, "client_test", "developers.html"), { encoding: "utf8" }));
|
res.send(
|
||||||
|
fs.readFileSync(
|
||||||
|
path.join(ASSET_FOLDER_PATH, "client_test", "developers.html"),
|
||||||
|
{ encoding: "utf8" },
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
app.get("*", (req: Request, res: Response) => {
|
app.get("*", (req: Request, res: Response) => {
|
||||||
const { useTestClient } = Config.get().client;
|
const { useTestClient } = Config.get().client;
|
||||||
res.set("Cache-Control", "public, max-age=" + 60 * 60 * 24);
|
res.set("Cache-Control", "public, max-age=" + 60 * 60 * 24);
|
||||||
res.set("content-type", "text/html");
|
res.set("content-type", "text/html");
|
||||||
|
|
||||||
if (req.url.startsWith("/api") || req.url.startsWith("/__development")) return;
|
if (req.url.startsWith("/api") || req.url.startsWith("/__development"))
|
||||||
|
return;
|
||||||
|
|
||||||
if (!useTestClient) return res.send("Test client is disabled on this instance. Use a stand-alone client to connect this instance.");
|
if (!useTestClient)
|
||||||
if (req.url.startsWith("/invite")) return res.send(html.replace("9b2b7f0632acd0c5e781", "9f24f709a3de09b67c49"));
|
return res.send(
|
||||||
|
"Test client is disabled on this instance. Use a stand-alone client to connect this instance.",
|
||||||
|
);
|
||||||
|
if (req.url.startsWith("/invite"))
|
||||||
|
return res.send(
|
||||||
|
html.replace("9b2b7f0632acd0c5e781", "9f24f709a3de09b67c49"),
|
||||||
|
);
|
||||||
|
|
||||||
res.send(html);
|
res.send(html);
|
||||||
});
|
});
|
||||||
|
@ -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, {}));
|
||||||
|
@ -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;
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -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();
|
||||||
});
|
});
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
@ -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);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
@ -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)) {
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
|
@ -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`]);
|
||||||
});
|
});
|
||||||
|
@ -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,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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");
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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 });
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user