diff options
Diffstat (limited to 'src')
247 files changed, 6137 insertions, 3427 deletions
diff --git a/src/api/Server.ts b/src/api/Server.ts index 4cf0917d..bd9bc4b9 100644 --- a/src/api/Server.ts +++ b/src/api/Server.ts @@ -12,7 +12,7 @@ import { initTranslation } from "./middlewares/Translation"; import morgan from "morgan"; import { initInstance } from "./util/handlers/Instance"; import { registerRoutes } from "@fosscord/util"; -import { red } from "picocolors" +import { red } from "picocolors"; export interface FosscordServerOptions extends ServerOptions {} @@ -44,13 +44,18 @@ export class FosscordServer extends Server { this.app.use( morgan("combined", { skip: (req, res) => { - var skip = !(process.env["LOG_REQUESTS"]?.includes(res.statusCode.toString()) ?? false); - if (process.env["LOG_REQUESTS"]?.charAt(0) == "-") skip = !skip; + var skip = !( + process.env["LOG_REQUESTS"]?.includes( + res.statusCode.toString(), + ) ?? false + ); + if (process.env["LOG_REQUESTS"]?.charAt(0) == "-") + skip = !skip; return skip; - } - }) + }, + }), ); - }; + } this.app.use(CORS); this.app.use(BodyParser({ inflate: true, limit: "10mb" })); @@ -63,16 +68,22 @@ export class FosscordServer extends Server { await initRateLimits(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) => { - if (error) return next(error); - res.status(404).json({ - message: "404 endpoint not found", - code: 0 - }); - next(); - }); + api.use( + "*", + (error: any, req: Request, res: Response, next: NextFunction) => { + if (error) return next(error); + res.status(404).json({ + message: "404 endpoint not found", + code: 0, + }); + next(); + }, + ); this.app = app; @@ -87,8 +98,13 @@ export class FosscordServer extends Server { this.app.use(ErrorHandler); 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(); } -}; \ No newline at end of file +} diff --git a/src/api/index.ts b/src/api/index.ts index 09663452..adc7649c 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,3 +1,3 @@ export * from "./Server"; export * from "./middlewares/"; -export * from "./util/"; \ No newline at end of file +export * from "./util/"; diff --git a/src/api/middlewares/Authentication.ts b/src/api/middlewares/Authentication.ts index 1df7911b..50048b12 100644 --- a/src/api/middlewares/Authentication.ts +++ b/src/api/middlewares/Authentication.ts @@ -10,7 +10,7 @@ export const NO_AUTHORIZATION_ROUTES = [ "/auth/mfa/totp", // Routes with a seperate auth system "/webhooks/", - // Public information endpoints + // Public information endpoints "/ping", "/gateway", "/experiments", @@ -26,7 +26,7 @@ export const NO_AUTHORIZATION_ROUTES = [ // Public policy pages "/policies/instance", // Asset delivery - /\/guilds\/\d+\/widget\.(json|png)/ + /\/guilds\/\d+\/widget\.(json|png)/, ]; 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); const url = req.url.replace(API_PREFIX, ""); 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(); - 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 { 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.user_id = decoded.id; diff --git a/src/api/middlewares/BodyParser.ts b/src/api/middlewares/BodyParser.ts index 4cb376bc..7741f1fd 100644 --- a/src/api/middlewares/BodyParser.ts +++ b/src/api/middlewares/BodyParser.ts @@ -6,7 +6,8 @@ export function BodyParser(opts?: OptionsJson) { const jsonParser = bodyParser.json(opts); 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) => { if (err) { diff --git a/src/api/middlewares/CORS.ts b/src/api/middlewares/CORS.ts index 20260cf9..2dce51c6 100644 --- a/src/api/middlewares/CORS.ts +++ b/src/api/middlewares/CORS.ts @@ -7,10 +7,16 @@ export function CORS(req: Request, res: Response, next: NextFunction) { // TODO: use better CSP res.set( "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(); } diff --git a/src/api/middlewares/ErrorHandler.ts b/src/api/middlewares/ErrorHandler.ts index 2012b91c..bf3b011a 100644 --- a/src/api/middlewares/ErrorHandler.ts +++ b/src/api/middlewares/ErrorHandler.ts @@ -3,7 +3,12 @@ import { HTTPError } from "lambert-server"; import { ApiError, FieldError } from "@fosscord/util"; 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(); try { @@ -12,20 +17,28 @@ export function ErrorHandler(error: Error, req: Request, res: Response, next: Ne let message = error?.toString(); 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) { code = error.code; message = error.message; httpcode = error.httpStatus; } 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; } else if (error instanceof FieldError) { code = Number(error.code); message = error.message; errors = error.errors; } 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) { // 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 }); } catch (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" }); } } diff --git a/src/api/middlewares/RateLimit.ts b/src/api/middlewares/RateLimit.ts index 57645c0b..b3976a16 100644 --- a/src/api/middlewares/RateLimit.ts +++ b/src/api/middlewares/RateLimit.ts @@ -40,21 +40,32 @@ export default function rateLimit(opts: { success?: boolean; onlyIp?: boolean; }): 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 if (req.user_id) { const rights = await getRights(req.user_id); 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); if (!opts.onlyIp && req.user_id) executor_id = req.user_id; let max_hits = opts.count; if (opts.bot && req.user_bot) max_hits = opts.bot; - if (opts.GET && ["GET", "OPTIONS", "HEAD"].includes(req.method)) max_hits = opts.GET; - else if (opts.MODIFY && ["POST", "DELETE", "PATCH", "PUT"].includes(req.method)) max_hits = opts.MODIFY; + if (opts.GET && ["GET", "OPTIONS", "HEAD"].includes(req.method)) + 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); @@ -75,11 +86,15 @@ export default function rateLimit(opts: { const global = bucket_id === "global"; // each block violation pushes the expiry one full window further 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(); resetAfterSec = Math.ceil(resetAfterMs / 1000); - console.log(`blocked bucket: ${bucket_id} ${executor_id}`, { resetAfterMs }); + console.log(`blocked bucket: ${bucket_id} ${executor_id}`, { + resetAfterMs, + }); return ( res .status(429) @@ -91,20 +106,33 @@ export default function rateLimit(opts: { .set("Retry-After", `${Math.ceil(resetAfterSec)}`) .set("X-RateLimit-Bucket", `${bucket_id}`) // 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(); - 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) { res.once("finish", () => { // check if error and increment error rate limit if (res.statusCode >= 400 && opts.error) { 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); } }); @@ -141,8 +169,8 @@ export async function initRateLimits(app: Router) { rateLimit({ bucket: "global", onlyIp: true, - ...ip - }) + ...ip, + }), ); app.use(rateLimit({ bucket: "global", ...global })); app.use( @@ -150,17 +178,25 @@ export async function initRateLimits(app: Router) { bucket: "error", error: true, onlyIp: true, - ...error - }) + ...error, + }), ); app.use("/guilds/:id", rateLimit(routes.guild)); app.use("/webhooks/:id", rateLimit(routes.webhook)); app.use("/channels/:id", rateLimit(routes.channel)); 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; let limit = Cache.get(id); if (!limit) { @@ -169,7 +205,7 @@ async function hitRoute(opts: { executor_id: string; bucket_id: string; max_hits executor_id: opts.executor_id, expires_at: new Date(Date.now() + opts.window * 1000), hits: 0, - blocked: false + blocked: false, }; Cache.set(id, limit); } @@ -205,4 +241,4 @@ async function hitRoute(opts: { executor_id: string; bucket_id: string; max_hits } await ratelimit.save(); */ -} \ No newline at end of file +} diff --git a/src/api/middlewares/Translation.ts b/src/api/middlewares/Translation.ts index c0b7a4b8..05038040 100644 --- a/src/api/middlewares/Translation.ts +++ b/src/api/middlewares/Translation.ts @@ -9,8 +9,12 @@ const ASSET_FOLDER_PATH = path.join(__dirname, "..", "..", "..", "assets"); export async function initTranslation(router: Router) { const languages = fs.readdirSync(path.join(ASSET_FOLDER_PATH, "locales")); - const namespaces = fs.readdirSync(path.join(ASSET_FOLDER_PATH, "locales", "en")); - const ns = namespaces.filter((x) => x.endsWith(".json")).map((x) => x.slice(0, x.length - 5)); + const namespaces = fs.readdirSync( + 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 .use(i18nextBackend) @@ -21,9 +25,11 @@ export async function initTranslation(router: Router) { fallbackLng: "en", ns, 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, {})); diff --git a/src/api/routes/-/monitorz.ts b/src/api/routes/-/monitorz.ts index f85cd099..630a832b 100644 --- a/src/api/routes/-/monitorz.ts +++ b/src/api/routes/-/monitorz.ts @@ -5,14 +5,18 @@ import os from "os"; const router = Router(); -router.get("/", route({ right: "OPERATOR" }), async (req: Request, res: Response) => { - return res.json({ - load: os.loadavg(), - procUptime: process.uptime(), - sysUptime: os.uptime(), - memPercent: 100 - ((os.freemem() / os.totalmem()) * 100), - sessions: await Session.count(), - }) -}) +router.get( + "/", + route({ right: "OPERATOR" }), + async (req: Request, res: Response) => { + return res.json({ + load: os.loadavg(), + procUptime: process.uptime(), + sysUptime: os.uptime(), + memPercent: 100 - (os.freemem() / os.totalmem()) * 100, + sessions: await Session.count(), + }); + }, +); -export default router; \ No newline at end of file +export default router; diff --git a/src/api/routes/auth/location-metadata.ts b/src/api/routes/auth/location-metadata.ts index f4c2bd16..0ae946ed 100644 --- a/src/api/routes/auth/location-metadata.ts +++ b/src/api/routes/auth/location-metadata.ts @@ -3,11 +3,15 @@ import { route } from "@fosscord/api"; import { getIpAdress, IPAnalysis } from "@fosscord/api"; const router = Router(); -router.get("/",route({}), async (req: Request, res: Response) => { - //TODO - //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; - res.json({ consent_required: false, country_code: country_code, promotional_email_opt_in: { required: true, pre_checked: false}}); +router.get("/", route({}), async (req: Request, res: Response) => { + //TODO + //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; + res.json({ + consent_required: false, + country_code: country_code, + promotional_email_opt_in: { required: true, pre_checked: false }, + }); }); export default router; diff --git a/src/api/routes/auth/login.ts b/src/api/routes/auth/login.ts index 9bed5aab..9ea2606c 100644 --- a/src/api/routes/auth/login.ts +++ b/src/api/routes/auth/login.ts @@ -1,84 +1,127 @@ import { Request, Response, Router } from "express"; import { route, getIpAdress, verifyCaptcha } from "@fosscord/api"; 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"; const router: Router = Router(); export default router; -router.post("/", route({ body: "LoginSchema" }), async (req: Request, res: Response) => { - const { login, password, captcha_key, undelete } = req.body as LoginSchema; - const email = adjustEmail(login); - console.log("login", email); +router.post( + "/", + route({ body: "LoginSchema" }), + async (req: Request, res: Response) => { + const { login, password, captcha_key, undelete } = + req.body as LoginSchema; + const email = adjustEmail(login); + console.log("login", email); + + const config = Config.get(); + + if (config.login.requireCaptcha && config.security.captcha.enabled) { + const { sitekey, service } = config.security.captcha; + if (!captcha_key) { + return res.status(400).json({ + captcha_key: ["captcha-required"], + captcha_sitekey: sitekey, + captcha_service: service, + }); + } + + const ip = getIpAdress(req); + const verify = await verifyCaptcha(captcha_key, ip); + if (!verify.success) { + return res.status(400).json({ + captcha_key: verify["error-codes"], + captcha_sitekey: sitekey, + captcha_service: service, + }); + } + } - const config = Config.get(); + const user = await User.findOneOrFail({ + where: [{ phone: login }, { email: login }], + select: [ + "data", + "id", + "disabled", + "deleted", + "settings", + "totp_secret", + "mfa_enabled", + ], + }).catch((e) => { + throw FieldErrors({ + login: { + message: req.t("auth:login.INVALID_LOGIN"), + code: "INVALID_LOGIN", + }, + }); + }); + + if (undelete) { + // undelete refers to un'disable' here + if (user.disabled) + await User.update({ id: user.id }, { disabled: false }); + if (user.deleted) + await User.update({ id: user.id }, { deleted: false }); + } else { + if (user.deleted) + 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, + }); + } - if (config.login.requireCaptcha && config.security.captcha.enabled) { - const { sitekey, service } = config.security.captcha; - if (!captcha_key) { - return res.status(400).json({ - captcha_key: ["captcha-required"], - captcha_sitekey: sitekey, - captcha_service: service + // the salt is saved in the password refer to bcrypt docs + const same_password = await bcrypt.compare( + password, + user.data.hash || "", + ); + if (!same_password) { + throw FieldErrors({ + password: { + message: req.t("auth:login.INVALID_PASSWORD"), + code: "INVALID_PASSWORD", + }, }); } - const ip = getIpAdress(req); - const verify = await verifyCaptcha(captcha_key, ip); - if (!verify.success) { - return res.status(400).json({ - captcha_key: verify["error-codes"], - captcha_sitekey: sitekey, - captcha_service: service + if (user.mfa_enabled) { + // TODO: This is not a discord.com ticket. I'm not sure what it is but I'm lazy + const ticket = crypto.randomBytes(40).toString("hex"); + + await User.update({ id: user.id }, { totp_last_ticket: ticket }); + + return res.json({ + ticket: ticket, + mfa: true, + sms: false, // TODO + token: null, }); } - } - - const user = await User.findOneOrFail({ - where: [{ phone: login }, { email: login }], - select: ["data", "id", "disabled", "deleted", "settings", "totp_secret", "mfa_enabled"] - }).catch((e) => { - throw FieldErrors({ login: { message: req.t("auth:login.INVALID_LOGIN"), code: "INVALID_LOGIN" } }); - }); - - if (undelete) { - // undelete refers to un'disable' here - if (user.disabled) await User.update({ id: user.id }, { disabled: false }); - if (user.deleted) await User.update({ id: user.id }, { deleted: false }); - } else { - if (user.deleted) 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 - const same_password = await bcrypt.compare(password, user.data.hash || ""); - if (!same_password) { - throw FieldErrors({ password: { message: req.t("auth:login.INVALID_PASSWORD"), code: "INVALID_PASSWORD" } }); - } - - if (user.mfa_enabled) { - // TODO: This is not a discord.com ticket. I'm not sure what it is but I'm lazy - const ticket = crypto.randomBytes(40).toString("hex"); - - await User.update({ id: user.id }, { totp_last_ticket: ticket }); - - return res.json({ - ticket: ticket, - mfa: true, - sms: false, // TODO - token: null, - }) - } - - const token = await generateToken(user.id); - - // Notice this will have a different token structure, than discord - // Discord header is just the user id as string, which is not possible with npm-jsonwebtoken package - // https://user-images.githubusercontent.com/6506416/81051916-dd8c9900-8ec2-11ea-8794-daf12d6f31f0.png - - res.json({ token, settings: user.settings }); -}); + + const token = await generateToken(user.id); + + // Notice this will have a different token structure, than discord + // Discord header is just the user id as string, which is not possible with npm-jsonwebtoken package + // https://user-images.githubusercontent.com/6506416/81051916-dd8c9900-8ec2-11ea-8794-daf12d6f31f0.png + + res.json({ token, settings: user.settings }); + }, +); /** * POST /auth/login diff --git a/src/api/routes/auth/logout.ts b/src/api/routes/auth/logout.ts index e806fed9..e1bdbea3 100644 --- a/src/api/routes/auth/logout.ts +++ b/src/api/routes/auth/logout.ts @@ -10,7 +10,8 @@ router.post("/", route({}), async (req: Request, res: Response) => { } else { delete req.body.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(); -}); \ No newline at end of file +}); diff --git a/src/api/routes/auth/mfa/totp.ts b/src/api/routes/auth/mfa/totp.ts index 96a48b66..83cf7648 100644 --- a/src/api/routes/auth/mfa/totp.ts +++ b/src/api/routes/auth/mfa/totp.ts @@ -5,45 +5,48 @@ import { verifyToken } from "node-2fa"; import { HTTPError } from "lambert-server"; const router = Router(); -router.post("/", route({ body: "TotpSchema" }), async (req: Request, res: Response) => { - const { code, ticket, gift_code_sku_id, login_source } = req.body as TotpSchema; +router.post( + "/", + 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({ - where: { - totp_last_ticket: ticket, - }, - select: [ - "id", - "totp_secret", - "settings", - ], - }); + const user = await User.findOneOrFail({ + where: { + totp_last_ticket: ticket, + }, + select: ["id", "totp_secret", "settings"], + }); - const backup = await BackupCode.findOne({ - where: { - code: code, - expired: false, - consumed: false, - user: { id: user.id } - } - }); + const backup = await BackupCode.findOne({ + where: { + code: code, + expired: false, + consumed: false, + user: { id: user.id }, + }, + }); - if (!backup) { - const ret = verifyToken(user.totp_secret!, code); - if (!ret || ret.delta != 0) - throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008); - } - else { - backup.consumed = true; - await backup.save(); - } + if (!backup) { + const ret = verifyToken(user.totp_secret!, code); + if (!ret || ret.delta != 0) + throw new HTTPError( + req.t("auth:login.INVALID_TOTP_CODE"), + 60008, + ); + } else { + backup.consumed = true; + await backup.save(); + } - await User.update({ id: user.id }, { totp_last_ticket: "" }); + await User.update({ id: user.id }, { totp_last_ticket: "" }); - return res.json({ - token: await generateToken(user.id), - user_settings: user.settings, - }); -}); + return res.json({ + token: await generateToken(user.id), + user_settings: user.settings, + }); + }, +); export default router; diff --git a/src/api/routes/auth/register.ts b/src/api/routes/auth/register.ts index 84f8f838..3479c4a0 100644 --- a/src/api/routes/auth/register.ts +++ b/src/api/routes/auth/register.ts @@ -1,156 +1,215 @@ import { Request, Response, Router } from "express"; -import { Config, generateToken, Invite, FieldErrors, User, adjustEmail, RegisterSchema } from "@fosscord/util"; -import { route, getIpAdress, IPAnalysis, isProxy, verifyCaptcha } from "@fosscord/api"; +import { + Config, + generateToken, + Invite, + FieldErrors, + User, + adjustEmail, + RegisterSchema, +} from "@fosscord/util"; +import { + route, + getIpAdress, + IPAnalysis, + isProxy, + verifyCaptcha, +} from "@fosscord/api"; import bcrypt from "bcrypt"; import { HTTPError } from "lambert-server"; const router: Router = Router(); -router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Response) => { - const body = req.body as RegisterSchema; - const { register, security } = Config.get(); - const ip = getIpAdress(req); - - // email will be slightly modified version of the user supplied email -> e.g. protection against GMail Trick - let email = adjustEmail(body.email); - - // check if registration is allowed - if (!register.allowNewRegistration) { - throw FieldErrors({ - email: { code: "REGISTRATION_DISABLED", message: req.t("auth:register.REGISTRATION_DISABLED") } - }); - } - - // check if the user agreed to the Terms of Service - if (!body.consent) { - throw FieldErrors({ - consent: { code: "CONSENT_REQUIRED", message: req.t("auth:register.CONSENT_REQUIRED") } - }); - } - - if (register.disabled) { - throw FieldErrors({ - email: { - code: "DISABLED", - message: "registration is disabled on this instance" - } - }); - } - - if (register.requireCaptcha && security.captcha.enabled) { - const { sitekey, service } = security.captcha; - if (!body.captcha_key) { - return res?.status(400).json({ - captcha_key: ["captcha-required"], - captcha_sitekey: sitekey, - captcha_service: service +router.post( + "/", + route({ body: "RegisterSchema" }), + async (req: Request, res: Response) => { + const body = req.body as RegisterSchema; + const { register, security } = Config.get(); + const ip = getIpAdress(req); + + // email will be slightly modified version of the user supplied email -> e.g. protection against GMail Trick + let email = adjustEmail(body.email); + + // check if registration is allowed + if (!register.allowNewRegistration) { + throw FieldErrors({ + email: { + code: "REGISTRATION_DISABLED", + message: req.t("auth:register.REGISTRATION_DISABLED"), + }, }); } - const verify = await verifyCaptcha(body.captcha_key, ip); - if (!verify.success) { - return res.status(400).json({ - captcha_key: verify["error-codes"], - captcha_sitekey: sitekey, - captcha_service: service + // check if the user agreed to the Terms of Service + if (!body.consent) { + throw FieldErrors({ + consent: { + code: "CONSENT_REQUIRED", + message: req.t("auth:register.CONSENT_REQUIRED"), + }, }); } - } - - if (!register.allowMultipleAccounts) { - // TODO: check if fingerprint was eligible generated - const exists = await User.findOne({ where: { fingerprints: body.fingerprint }, select: ["id"] }); - if (exists) { + if (register.disabled) { throw FieldErrors({ email: { - code: "EMAIL_ALREADY_REGISTERED", - message: req.t("auth:register.EMAIL_ALREADY_REGISTERED") - } + code: "DISABLED", + message: "registration is disabled on this instance", + }, }); } - } - if (register.blockProxies) { - if (isProxy(await IPAnalysis(ip))) { - console.log(`proxy ${ip} blocked from registration`); - throw new HTTPError("Your IP is blocked from registration"); + if (register.requireCaptcha && security.captcha.enabled) { + const { sitekey, service } = security.captcha; + if (!body.captcha_key) { + return res?.status(400).json({ + captcha_key: ["captcha-required"], + captcha_sitekey: sitekey, + captcha_service: service, + }); + } + + const verify = await verifyCaptcha(body.captcha_key, ip); + if (!verify.success) { + return res.status(400).json({ + captcha_key: verify["error-codes"], + captcha_sitekey: sitekey, + captcha_service: service, + }); + } } - } - // TODO: gift_code_sku_id? - // TODO: check password strength + if (!register.allowMultipleAccounts) { + // TODO: check if fingerprint was eligible generated + const exists = await User.findOne({ + where: { fingerprints: body.fingerprint }, + select: ["id"], + }); + + if (exists) { + throw FieldErrors({ + email: { + code: "EMAIL_ALREADY_REGISTERED", + message: req.t( + "auth:register.EMAIL_ALREADY_REGISTERED", + ), + }, + }); + } + } - if (email) { - // replace all dots and chars after +, if its a gmail.com email - if (!email) { - throw FieldErrors({ email: { code: "INVALID_EMAIL", message: req?.t("auth:register.INVALID_EMAIL") } }); + if (register.blockProxies) { + if (isProxy(await IPAnalysis(ip))) { + console.log(`proxy ${ip} blocked from registration`); + throw new HTTPError("Your IP is blocked from registration"); + } } - // check if there is already an account with this email - const exists = await User.findOne({ where: { email: email } }); + // TODO: gift_code_sku_id? + // TODO: check password strength + + if (email) { + // replace all dots and chars after +, if its a gmail.com email + if (!email) { + throw FieldErrors({ + email: { + code: "INVALID_EMAIL", + message: req?.t("auth:register.INVALID_EMAIL"), + }, + }); + } - if (exists) { + // check if there is already an account with this email + const exists = await User.findOne({ where: { email: email } }); + + if (exists) { + throw FieldErrors({ + email: { + code: "EMAIL_ALREADY_REGISTERED", + message: req.t( + "auth:register.EMAIL_ALREADY_REGISTERED", + ), + }, + }); + } + } else if (register.email.required) { throw FieldErrors({ email: { - code: "EMAIL_ALREADY_REGISTERED", - message: req.t("auth:register.EMAIL_ALREADY_REGISTERED") - } + code: "BASE_TYPE_REQUIRED", + message: req.t("common:field.BASE_TYPE_REQUIRED"), + }, }); } - } else if (register.email.required) { - throw FieldErrors({ - email: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") } - }); - } - - if (register.dateOfBirth.required && !body.date_of_birth) { - throw FieldErrors({ - date_of_birth: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") } - }); - } else if (register.dateOfBirth.required && register.dateOfBirth.minimum) { - const minimum = new Date(); - minimum.setFullYear(minimum.getFullYear() - register.dateOfBirth.minimum); - body.date_of_birth = new Date(body.date_of_birth as Date); - - // higher is younger - if (body.date_of_birth > minimum) { + + if (register.dateOfBirth.required && !body.date_of_birth) { throw FieldErrors({ date_of_birth: { - code: "DATE_OF_BIRTH_UNDERAGE", - message: req.t("auth:register.DATE_OF_BIRTH_UNDERAGE", { years: register.dateOfBirth.minimum }) - } + code: "BASE_TYPE_REQUIRED", + message: req.t("common:field.BASE_TYPE_REQUIRED"), + }, + }); + } else if ( + register.dateOfBirth.required && + register.dateOfBirth.minimum + ) { + const minimum = new Date(); + minimum.setFullYear( + minimum.getFullYear() - register.dateOfBirth.minimum, + ); + body.date_of_birth = new Date(body.date_of_birth as Date); + + // higher is younger + if (body.date_of_birth > minimum) { + throw FieldErrors({ + date_of_birth: { + code: "DATE_OF_BIRTH_UNDERAGE", + message: req.t("auth:register.DATE_OF_BIRTH_UNDERAGE", { + years: register.dateOfBirth.minimum, + }), + }, + }); + } + } + + if (body.password) { + // the salt is saved in the password refer to bcrypt docs + body.password = await bcrypt.hash(body.password, 12); + } else if (register.password.required) { + throw FieldErrors({ + password: { + code: "BASE_TYPE_REQUIRED", + message: req.t("common:field.BASE_TYPE_REQUIRED"), + }, + }); + } + + if ( + !body.invite && + (register.requireInvite || + (register.guestsRequireInvite && !register.email)) + ) { + // require invite to register -> e.g. for organizations to send invites to their employees + throw FieldErrors({ + email: { + code: "INVITE_ONLY", + message: req.t("auth:register.INVITE_ONLY"), + }, }); } - } - - if (body.password) { - // the salt is saved in the password refer to bcrypt docs - body.password = await bcrypt.hash(body.password, 12); - } else if (register.password.required) { - throw FieldErrors({ - password: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") } - }); - } - - if (!body.invite && (register.requireInvite || (register.guestsRequireInvite && !register.email))) { - // require invite to register -> e.g. for organizations to send invites to their employees - throw FieldErrors({ - email: { code: "INVITE_ONLY", message: req.t("auth:register.INVITE_ONLY") } - }); - } - - const user = await User.register({ ...body, req }); - - if (body.invite) { - // await to fail if the invite doesn't exist (necessary for requireInvite to work properly) (username only signups are possible) - await Invite.joinGuild(user.id, body.invite); - } - - console.log("register", body.email, body.username, ip); - - return res.json({ token: await generateToken(user.id) }); -}); + + const user = await User.register({ ...body, req }); + + if (body.invite) { + // await to fail if the invite doesn't exist (necessary for requireInvite to work properly) (username only signups are possible) + await Invite.joinGuild(user.id, body.invite); + } + + console.log("register", body.email, body.username, ip); + + return res.json({ token: await generateToken(user.id) }); + }, +); export default router; diff --git a/src/api/routes/auth/verify/view-backup-codes-challenge.ts b/src/api/routes/auth/verify/view-backup-codes-challenge.ts index 24de8ec5..65f0a57c 100644 --- a/src/api/routes/auth/verify/view-backup-codes-challenge.ts +++ b/src/api/routes/auth/verify/view-backup-codes-challenge.ts @@ -4,19 +4,31 @@ import { FieldErrors, User, BackupCodesChallengeSchema } from "@fosscord/util"; import bcrypt from "bcrypt"; const router = Router(); -router.post("/", route({ body: "BackupCodesChallengeSchema" }), async (req: Request, res: Response) => { - const { password } = req.body as BackupCodesChallengeSchema; +router.post( + "/", + route({ body: "BackupCodesChallengeSchema" }), + async (req: Request, res: Response) => { + 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 || "")) { - throw FieldErrors({ password: { message: req.t("auth:login.INVALID_PASSWORD"), code: "INVALID_PASSWORD" } }); - } + if (!(await bcrypt.compare(password, user.data.hash || ""))) { + throw FieldErrors({ + password: { + message: req.t("auth:login.INVALID_PASSWORD"), + code: "INVALID_PASSWORD", + }, + }); + } - return res.json({ - nonce: "NoncePlaceholder", - regenerate_nonce: "RegenNoncePlaceholder", - }); -}); + return res.json({ + nonce: "NoncePlaceholder", + regenerate_nonce: "RegenNoncePlaceholder", + }); + }, +); export default router; diff --git a/src/api/routes/channels/#channel_id/index.ts b/src/api/routes/channels/#channel_id/index.ts index 8dbefe1b..a164fff6 100644 --- a/src/api/routes/channels/#channel_id/index.ts +++ b/src/api/routes/channels/#channel_id/index.ts @@ -6,7 +6,7 @@ import { emitEvent, Recipient, handleFile, - ChannelModifySchema + ChannelModifySchema, } from "@fosscord/util"; import { Request, Response, Router } from "express"; import { route } from "@fosscord/api"; @@ -15,56 +15,89 @@ const router: Router = Router(); // TODO: delete channel // TODO: Get channel -router.get("/", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: Response) => { - const { channel_id } = req.params; +router.get( + "/", + route({ permission: "VIEW_CHANNEL" }), + async (req: Request, res: Response) => { + 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); -}); + return res.send(channel); + }, +); -router.delete("/", route({ permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => { - const { channel_id } = req.params; +router.delete( + "/", + route({ permission: "MANAGE_CHANNELS" }), + async (req: Request, res: Response) => { + 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) { - const recipient = await Recipient.findOneOrFail({ where: { channel_id: channel_id, user_id: req.user_id } }); - recipient.closed = true; - await Promise.all([ - recipient.save(), - emitEvent({ event: "CHANNEL_DELETE", data: channel, user_id: req.user_id } as ChannelDeleteEvent) - ]); - } else if (channel.type === ChannelType.GROUP_DM) { - await Channel.removeRecipientFromChannel(channel, req.user_id); - } else { - await Promise.all([ - Channel.delete({ id: channel_id }), - emitEvent({ event: "CHANNEL_DELETE", data: channel, channel_id } as ChannelDeleteEvent) - ]); - } + if (channel.type === ChannelType.DM) { + const recipient = await Recipient.findOneOrFail({ + where: { channel_id: channel_id, user_id: req.user_id }, + }); + recipient.closed = true; + await Promise.all([ + recipient.save(), + emitEvent({ + event: "CHANNEL_DELETE", + data: channel, + user_id: req.user_id, + } as ChannelDeleteEvent), + ]); + } else if (channel.type === ChannelType.GROUP_DM) { + await Channel.removeRecipientFromChannel(channel, req.user_id); + } else { + await Promise.all([ + Channel.delete({ id: channel_id }), + 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) => { - var payload = req.body as ChannelModifySchema; - const { channel_id } = req.params; - if (payload.icon) payload.icon = await handleFile(`/channel-icons/${channel_id}`, payload.icon); +router.patch( + "/", + route({ body: "ChannelModifySchema", permission: "MANAGE_CHANNELS" }), + async (req: Request, res: Response) => { + var payload = req.body as ChannelModifySchema; + const { channel_id } = req.params; + if (payload.icon) + payload.icon = await handleFile( + `/channel-icons/${channel_id}`, + payload.icon, + ); - const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); - channel.assign(payload); + const channel = await Channel.findOneOrFail({ + where: { id: channel_id }, + }); + channel.assign(payload); - await Promise.all([ - channel.save(), - emitEvent({ - event: "CHANNEL_UPDATE", - data: channel, - channel_id - } as ChannelUpdateEvent) - ]); + await Promise.all([ + channel.save(), + emitEvent({ + event: "CHANNEL_UPDATE", + data: channel, + channel_id, + } as ChannelUpdateEvent), + ]); - res.send(channel); -}); + res.send(channel); + }, +); export default router; diff --git a/src/api/routes/channels/#channel_id/invites.ts b/src/api/routes/channels/#channel_id/invites.ts index 246a2c69..afaabf47 100644 --- a/src/api/routes/channels/#channel_id/invites.ts +++ b/src/api/routes/channels/#channel_id/invites.ts @@ -2,16 +2,33 @@ import { Router, Request, Response } from "express"; import { HTTPError } from "lambert-server"; import { route } 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"; 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) => { const { user_id } = req; 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); if (!channel.guild_id) { @@ -31,30 +48,44 @@ router.post("/", route({ body: "InviteCreateSchema", permission: "CREATE_INSTANT created_at: new Date(), guild_id, channel_id: channel_id, - inviter_id: user_id + inviter_id: user_id, }).save(); const data = invite.toJSON(); data.inviter = await User.getPublicUser(req.user_id); data.guild = await Guild.findOne({ where: { id: guild_id } }); 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); - }); + }, +); -router.get("/", route({ permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => { - const { user_id } = req; - const { channel_id } = req.params; - const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); +router.get( + "/", + route({ permission: "MANAGE_CHANNELS" }), + async (req: Request, res: Response) => { + const { user_id } = req; + const { channel_id } = req.params; + const channel = await Channel.findOneOrFail({ + where: { id: channel_id }, + }); - if (!channel.guild_id) { - throw new HTTPError("This channel doesn't exist", 404); - } - const { guild_id } = channel; + if (!channel.guild_id) { + throw new HTTPError("This channel doesn't exist", 404); + } + const { guild_id } = channel; - const invites = await Invite.find({ where: { guild_id }, relations: PublicInviteRelation }); + const invites = await Invite.find({ + where: { guild_id }, + relations: PublicInviteRelation, + }); - res.status(200).send(invites); -}); + res.status(200).send(invites); + }, +); export default router; diff --git a/src/api/routes/channels/#channel_id/messages/#message_id/ack.ts b/src/api/routes/channels/#channel_id/messages/#message_id/ack.ts index bedd453c..1a30143f 100644 --- a/src/api/routes/channels/#channel_id/messages/#message_id/ack.ts +++ b/src/api/routes/channels/#channel_id/messages/#message_id/ack.ts @@ -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 { route } from "@fosscord/api"; @@ -8,29 +13,40 @@ const router = Router(); // TODO: send read state event to all channel members // TODO: advance-only notification cursor -router.post("/", route({ body: "MessageAcknowledgeSchema" }), async (req: Request, res: Response) => { - const { channel_id, message_id } = req.params; +router.post( + "/", + route({ body: "MessageAcknowledgeSchema" }), + async (req: Request, res: Response) => { + const { channel_id, message_id } = req.params; - const permission = await getPermission(req.user_id, undefined, channel_id); - permission.hasThrow("VIEW_CHANNEL"); - - let read_state = await ReadState.findOne({ 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; - - await read_state.save(); - - await emitEvent({ - event: "MESSAGE_ACK", - user_id: req.user_id, - data: { + const permission = await getPermission( + req.user_id, + undefined, channel_id, - message_id, - version: 3763 - } - } as MessageAckEvent); - - res.json({ token: null }); -}); + ); + permission.hasThrow("VIEW_CHANNEL"); + + let read_state = await ReadState.findOne({ + 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; + + await read_state.save(); + + await emitEvent({ + event: "MESSAGE_ACK", + user_id: req.user_id, + data: { + channel_id, + message_id, + version: 3763, + }, + } as MessageAckEvent); + + res.json({ token: null }); + }, +); export default router; diff --git a/src/api/routes/channels/#channel_id/messages/#message_id/crosspost.ts b/src/api/routes/channels/#channel_id/messages/#message_id/crosspost.ts index b2cb6763..d8b55ccd 100644 --- a/src/api/routes/channels/#channel_id/messages/#message_id/crosspost.ts +++ b/src/api/routes/channels/#channel_id/messages/#message_id/crosspost.ts @@ -3,26 +3,36 @@ import { route } from "@fosscord/api"; const router = Router(); -router.post("/", route({ permission: "MANAGE_MESSAGES" }), (req: Request, res: Response) => { - // TODO: - res.json({ - id: "", - type: 0, - content: "", - channel_id: "", - author: { id: "", username: "", avatar: "", discriminator: "", public_flags: 64 }, - attachments: [], - embeds: [], - mentions: [], - mention_roles: [], - pinned: false, - mention_everyone: false, - tts: false, - timestamp: "", - edited_timestamp: null, - flags: 1, - components: [] - }).status(200); -}); +router.post( + "/", + route({ permission: "MANAGE_MESSAGES" }), + (req: Request, res: Response) => { + // TODO: + res.json({ + id: "", + type: 0, + content: "", + channel_id: "", + author: { + id: "", + username: "", + avatar: "", + discriminator: "", + public_flags: 64, + }, + attachments: [], + embeds: [], + mentions: [], + mention_roles: [], + pinned: false, + mention_everyone: false, + tts: false, + timestamp: "", + edited_timestamp: null, + flags: 1, + components: [], + }).status(200); + }, +); export default router; diff --git a/src/api/routes/channels/#channel_id/messages/#message_id/index.ts b/src/api/routes/channels/#channel_id/messages/#message_id/index.ts index 46b0d6bd..3abfebe8 100644 --- a/src/api/routes/channels/#channel_id/messages/#message_id/index.ts +++ b/src/api/routes/channels/#channel_id/messages/#message_id/index.ts @@ -26,55 +26,69 @@ const messageUpload = multer({ limits: { fileSize: 1024 * 1024 * 100, fields: 10, - files: 1 + files: 1, }, - storage: multer.memoryStorage() + storage: multer.memoryStorage(), }); // max upload 50 mb -router.patch("/", route({ body: "MessageCreateSchema", permission: "SEND_MESSAGES", right: "SEND_MESSAGES" }), async (req: Request, res: Response) => { - const { message_id, channel_id } = req.params; - var body = req.body as MessageCreateSchema; +router.patch( + "/", + route({ + body: "MessageCreateSchema", + permission: "SEND_MESSAGES", + right: "SEND_MESSAGES", + }), + async (req: Request, res: Response) => { + const { message_id, channel_id } = req.params; + 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 (!rights.has("MANAGE_MESSAGES")) { - permissions.hasThrow("MANAGE_MESSAGES"); - body = { flags: body.flags }; - // guild admins can only suppress embeds of other messages, no such restriction imposed to instance-wide admins - } - } else rights.hasThrow("SELF_EDIT_MESSAGES"); - - const new_message = await handleMessage({ - ...message, - // TODO: should message_reference be overridable? - // @ts-ignore - message_reference: message.message_reference, - ...body, - author_id: message.author_id, - channel_id, - id: message_id, - edited_timestamp: new Date() - }); - - await Promise.all([ - new_message!.save(), - await emitEvent({ - event: "MESSAGE_UPDATE", + if (req.user_id !== message.author_id) { + if (!rights.has("MANAGE_MESSAGES")) { + permissions.hasThrow("MANAGE_MESSAGES"); + body = { flags: body.flags }; + // guild admins can only suppress embeds of other messages, no such restriction imposed to instance-wide admins + } + } else rights.hasThrow("SELF_EDIT_MESSAGES"); + + const new_message = await handleMessage({ + ...message, + // TODO: should message_reference be overridable? + // @ts-ignore + message_reference: message.message_reference, + ...body, + author_id: message.author_id, channel_id, - data: { ...new_message, nonce: undefined } - } as MessageUpdateEvent) - ]); + id: message_id, + edited_timestamp: new Date(), + }); - postHandleMessage(message); + await Promise.all([ + new_message!.save(), + await emitEvent({ + event: "MESSAGE_UPDATE", + channel_id, + data: { ...new_message, nonce: undefined }, + } as MessageUpdateEvent), + ]); - return res.json(message); -}); + postHandleMessage(message); + return res.json(message); + }, +); // Backfill message with specific timestamp router.put( @@ -87,7 +101,11 @@ router.put( 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) => { const { channel_id, message_id } = req.params; var body = req.body as MessageCreateSchema; @@ -107,20 +125,30 @@ router.put( 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) { throw FosscordApiErrors.CANNOT_REPLACE_BY_BACKFILL; } if (req.file) { try { - const file = await uploadFile(`/attachments/${req.params.channel_id}`, req.file); - attachments.push(Attachment.create({ ...file, proxy_url: file.url })); + const file = await uploadFile( + `/attachments/${req.params.channel_id}`, + req.file, + ); + attachments.push( + Attachment.create({ ...file, proxy_url: file.url }), + ); } catch (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 || []; if (body.embed) embeds.push(body.embed); @@ -142,27 +170,43 @@ router.put( await Promise.all([ message.save(), - emitEvent({ event: "MESSAGE_CREATE", channel_id: channel_id, data: message } as MessageCreateEvent), - channel.save() + emitEvent({ + 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); - } + }, ); -router.get("/", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: Response) => { - const { message_id, channel_id } = req.params; +router.get( + "/", + route({ permission: "VIEW_CHANNEL" }), + async (req: Request, res: Response) => { + 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) => { 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); - if ((message.author_id !== req.user_id)) { + if (message.author_id !== req.user_id) { 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"); } } else rights.hasThrow("SELF_DELETE_MESSAGES"); @@ -187,8 +235,8 @@ router.delete("/", route({}), async (req: Request, res: Response) => { data: { id: message_id, channel_id, - guild_id: channel.guild_id - } + guild_id: channel.guild_id, + }, } as MessageDeleteEvent); res.sendStatus(204); diff --git a/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts b/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts index c3cca05d..9f774682 100644 --- a/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts +++ b/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts @@ -11,7 +11,7 @@ import { MessageReactionRemoveEvent, PartialEmoji, PublicUserProjection, - User + User, } from "@fosscord/util"; import { route } from "@fosscord/api"; import { Router, Response, Request } from "express"; @@ -27,159 +27,224 @@ function getEmoji(emoji: string): PartialEmoji { if (parts) return { name: parts[0], - id: parts[1] + id: parts[1], }; return { id: undefined, - name: emoji + name: emoji, }; } -router.delete("/", route({ permission: "MANAGE_MESSAGES" }), async (req: Request, res: Response) => { - const { message_id, channel_id } = req.params; +router.delete( + "/", + route({ permission: "MANAGE_MESSAGES" }), + async (req: Request, res: Response) => { + 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: [] }); - await emitEvent({ - event: "MESSAGE_REACTION_REMOVE_ALL", - channel_id, - data: { + await emitEvent({ + event: "MESSAGE_REACTION_REMOVE_ALL", channel_id, - message_id, - guild_id: channel.guild_id + data: { + channel_id, + message_id, + guild_id: channel.guild_id, + }, + } as MessageReactionRemoveAllEvent); + + res.sendStatus(204); + }, +); + +router.delete( + "/:emoji", + route({ permission: "MANAGE_MESSAGES" }), + async (req: Request, res: Response) => { + const { message_id, channel_id } = req.params; + const emoji = getEmoji(req.params.emoji); + + 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) throw new HTTPError("Reaction not found", 404); + message.reactions.remove(already_added); + + await Promise.all([ + message.save(), + emitEvent({ + event: "MESSAGE_REACTION_REMOVE_EMOJI", + channel_id, + data: { + channel_id, + message_id, + guild_id: message.guild_id, + emoji, + }, + } as MessageReactionRemoveEmojiEvent), + ]); + + res.sendStatus(204); + }, +); + +router.get( + "/:emoji", + route({ permission: "VIEW_CHANNEL" }), + async (req: Request, res: Response) => { + const { message_id, channel_id } = req.params; + const emoji = getEmoji(req.params.emoji); + + const message = await Message.findOneOrFail({ + 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); + + const users = await User.find({ + where: { + id: In(reaction.user_ids), + }, + select: PublicUserProjection, + }); + + res.json(users); + }, +); + +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; + if (user_id !== "@me") throw new HTTPError("Invalid user"); + const emoji = getEmoji(req.params.emoji); + + const channel = await Channel.findOneOrFail({ + where: { 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, + ); + + if (!already_added) req.permission!.hasThrow("ADD_REACTIONS"); + + if (emoji.id) { + const external_emoji = await Emoji.findOneOrFail({ + where: { id: emoji.id }, + }); + if (!already_added) req.permission!.hasThrow("USE_EXTERNAL_EMOJIS"); + emoji.animated = external_emoji.animated; + emoji.name = external_emoji.name; } - } as MessageReactionRemoveAllEvent); - - res.sendStatus(204); -}); -router.delete("/:emoji", route({ permission: "MANAGE_MESSAGES" }), async (req: Request, res: Response) => { - const { message_id, channel_id } = req.params; - const emoji = getEmoji(req.params.emoji); - - 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) throw new HTTPError("Reaction not found", 404); - message.reactions.remove(already_added); - - await Promise.all([ - message.save(), - emitEvent({ - event: "MESSAGE_REACTION_REMOVE_EMOJI", + 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 + already_added.count++; + } else + message.reactions.push({ + count: 1, + emoji, + user_ids: [req.user_id], + }); + + await message.save(); + + const member = + channel.guild_id && + (await Member.findOneOrFail({ where: { id: req.user_id } })); + + await emitEvent({ + event: "MESSAGE_REACTION_ADD", channel_id, data: { + user_id: req.user_id, channel_id, message_id, - guild_id: message.guild_id, - emoji - } - } as MessageReactionRemoveEmojiEvent) - ]); - - res.sendStatus(204); -}); - -router.get("/:emoji", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: Response) => { - const { message_id, channel_id } = req.params; - const emoji = getEmoji(req.params.emoji); - - const message = await Message.findOneOrFail({ 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); - - const users = await User.find({ - where: { - id: In(reaction.user_ids) - }, - select: PublicUserProjection - }); - - res.json(users); -}); - -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; - if (user_id !== "@me") throw new HTTPError("Invalid user"); - const emoji = getEmoji(req.params.emoji); - - const channel = await Channel.findOneOrFail({ where: { 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); - - if (!already_added) req.permission!.hasThrow("ADD_REACTIONS"); - - if (emoji.id) { - const external_emoji = await Emoji.findOneOrFail({ where: { id: emoji.id } }); - if (!already_added) req.permission!.hasThrow("USE_EXTERNAL_EMOJIS"); - emoji.animated = external_emoji.animated; - emoji.name = external_emoji.name; - } - - 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 - already_added.count++; - } else message.reactions.push({ count: 1, emoji, user_ids: [req.user_id] }); - - await message.save(); - - const member = channel.guild_id && (await Member.findOneOrFail({ where: { id: req.user_id } })); - - await emitEvent({ - event: "MESSAGE_REACTION_ADD", - channel_id, - data: { - user_id: req.user_id, - channel_id, - message_id, - guild_id: channel.guild_id, - emoji, - member + guild_id: channel.guild_id, + emoji, + member, + }, + } as MessageReactionAddEvent); + + res.sendStatus(204); + }, +); + +router.delete( + "/:emoji/:user_id", + route({}), + async (req: Request, res: Response) => { + var { message_id, channel_id, user_id } = req.params; + + const emoji = getEmoji(req.params.emoji); + + const channel = await Channel.findOneOrFail({ + where: { id: channel_id }, + }); + const message = await Message.findOneOrFail({ + where: { id: message_id, channel_id }, + }); + + if (user_id === "@me") user_id = req.user_id; + else { + const permissions = await getPermission( + req.user_id, + undefined, + channel_id, + ); + permissions.hasThrow("MANAGE_MESSAGES"); } - } as MessageReactionAddEvent); - res.sendStatus(204); -}); + const already_added = message.reactions.find( + (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); -router.delete("/:emoji/:user_id", route({}), async (req: Request, res: Response) => { - var { message_id, channel_id, user_id } = req.params; + already_added.count--; - const emoji = getEmoji(req.params.emoji); + if (already_added.count <= 0) message.reactions.remove(already_added); - const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); - const message = await Message.findOneOrFail({ where: { id: message_id, channel_id } }); + await message.save(); - if (user_id === "@me") user_id = req.user_id; - else { - const permissions = await getPermission(req.user_id, undefined, channel_id); - permissions.hasThrow("MANAGE_MESSAGES"); - } - - const already_added = message.reactions.find((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--; - - if (already_added.count <= 0) message.reactions.remove(already_added); - - await message.save(); - - await emitEvent({ - event: "MESSAGE_REACTION_REMOVE", - channel_id, - data: { - user_id: req.user_id, + await emitEvent({ + event: "MESSAGE_REACTION_REMOVE", channel_id, - message_id, - guild_id: channel.guild_id, - emoji - } - } as MessageReactionRemoveEvent); - - res.sendStatus(204); -}); + data: { + user_id: req.user_id, + channel_id, + message_id, + guild_id: channel.guild_id, + emoji, + }, + } as MessageReactionRemoveEvent); + + res.sendStatus(204); + }, +); export default router; diff --git a/src/api/routes/channels/#channel_id/messages/bulk-delete.ts b/src/api/routes/channels/#channel_id/messages/bulk-delete.ts index 6493c16a..553ab17e 100644 --- a/src/api/routes/channels/#channel_id/messages/bulk-delete.ts +++ b/src/api/routes/channels/#channel_id/messages/bulk-delete.ts @@ -1,5 +1,13 @@ 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 { route } from "@fosscord/api"; @@ -10,33 +18,48 @@ export default router; // 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 // https://discord.com/developers/docs/resources/channel#bulk-delete-messages -router.post("/", route({ body: "BulkDeleteSchema" }), async (req: Request, res: Response) => { - const { channel_id } = req.params; - const channel = await Channel.findOneOrFail({ 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); - rights.hasThrow("SELF_DELETE_MESSAGES"); - - let superuser = rights.has("MANAGE_MESSAGES"); - const permission = await getPermission(req.user_id, channel?.guild_id, channel_id); - - const { maxBulkDelete } = Config.get().limits.message; - - const { messages } = req.body as { messages: string[] }; - if (messages.length === 0) throw new HTTPError("You must specify messages to bulk delete"); - if (!superuser) { - permission.hasThrow("MANAGE_MESSAGES"); - if (messages.length > maxBulkDelete) throw new HTTPError(`You cannot delete more than ${maxBulkDelete} messages`); - } - - await Message.delete(messages); - - await emitEvent({ - event: "MESSAGE_DELETE_BULK", - channel_id, - data: { ids: messages, channel_id, guild_id: channel.guild_id } - } as MessageDeleteBulkEvent); - - res.sendStatus(204); -}); +router.post( + "/", + route({ body: "BulkDeleteSchema" }), + async (req: Request, res: Response) => { + const { channel_id } = req.params; + const channel = await Channel.findOneOrFail({ + 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); + rights.hasThrow("SELF_DELETE_MESSAGES"); + + let superuser = rights.has("MANAGE_MESSAGES"); + const permission = await getPermission( + req.user_id, + channel?.guild_id, + channel_id, + ); + + const { maxBulkDelete } = Config.get().limits.message; + + const { messages } = req.body as { messages: string[] }; + if (messages.length === 0) + throw new HTTPError("You must specify messages to bulk delete"); + if (!superuser) { + permission.hasThrow("MANAGE_MESSAGES"); + if (messages.length > maxBulkDelete) + throw new HTTPError( + `You cannot delete more than ${maxBulkDelete} messages`, + ); + } + + await Message.delete(messages); + + await emitEvent({ + event: "MESSAGE_DELETE_BULK", + channel_id, + data: { ids: messages, channel_id, guild_id: channel.guild_id }, + } as MessageDeleteBulkEvent); + + res.sendStatus(204); + }, +); diff --git a/src/api/routes/channels/#channel_id/messages/index.ts b/src/api/routes/channels/#channel_id/messages/index.ts index bee93e80..631074c6 100644 --- a/src/api/routes/channels/#channel_id/messages/index.ts +++ b/src/api/routes/channels/#channel_id/messages/index.ts @@ -61,36 +61,50 @@ router.get("/", async (req: Request, res: Response) => { const before = req.query.before ? `${req.query.before}` : undefined; const after = req.query.after ? `${req.query.after}` : undefined; 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); - 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"); 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" }, take: limit, 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 (BigInt(after) > BigInt(Snowflake.generate())) return res.status(422); + if (BigInt(after) > BigInt(Snowflake.generate())) + return res.status(422); query.where.id = MoreThan(after); - } - else if (before) { - if (BigInt(before) < BigInt(req.params.channel_id)) return res.status(422); + } else if (before) { + if (BigInt(before) < BigInt(req.params.channel_id)) + return res.status(422); query.where.id = LessThan(before); - } - else if (around) { + } else if (around) { query.where.id = [ 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 } const messages = await Message.find(query); @@ -105,11 +119,22 @@ router.get("/", async (req: Request, res: Response) => { delete x.user_ids; }); // @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) => { // 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}`; - y.proxy_url = `${endpoint == null ? "" : endpoint}${new URL(uri).pathname}`; + const uri = y.proxy_url.startsWith("http") + ? 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; - }) + }), ); }); @@ -134,7 +159,7 @@ const messageUpload = multer({ fields: 10, // files: 1 }, - storage: multer.memoryStorage() + storage: multer.memoryStorage(), }); // max upload 50 mb /** TODO: dynamically change limit of MessageCreateSchema with config @@ -155,24 +180,38 @@ router.post( next(); }, - route({ body: "MessageCreateSchema", permission: "SEND_MESSAGES", right: "SEND_MESSAGES" }), + route({ + body: "MessageCreateSchema", + permission: "SEND_MESSAGES", + right: "SEND_MESSAGES", + }), async (req: Request, res: Response) => { const { channel_id } = req.params; var body = req.body as MessageCreateSchema; 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()) { - 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) { try { - const file = await uploadFile(`/attachments/${channel.id}`, currFile); - attachments.push(Attachment.create({ ...file, proxy_url: file.url })); - } - catch (error) { + const file = await uploadFile( + `/attachments/${channel.id}`, + currFile, + ); + attachments.push( + Attachment.create({ ...file, proxy_url: file.url }), + ); + } catch (error) { return res.status(400).json(error); } } @@ -188,7 +227,7 @@ router.post( channel_id, attachments, edited_timestamp: undefined, - timestamp: new Date() + timestamp: new Date(), }); channel.last_message_id = message.id; @@ -205,32 +244,47 @@ router.post( recipient.save(), emitEvent({ event: "CHANNEL_CREATE", - data: channel_dto.excludedRecipients([recipient.user_id]), - user_id: recipient.user_id - }) + data: channel_dto.excludedRecipients([ + recipient.user_id, + ]), + user_id: recipient.user_id, + }), ]); } - }) + }), ); } - const member = await Member.findOneOrFail({ where: { id: req.user_id }, relations: ["roles"] }); - member.roles = member.roles.filter((role: Role) => { - return role.id !== role.guild_id; - }).map((role: Role) => { - return role.id; - }) as any; + const member = await Member.findOneOrFail({ + where: { id: req.user_id }, + relations: ["roles"], + }); + member.roles = member.roles + .filter((role: Role) => { + return role.id !== role.guild_id; + }) + .map((role: Role) => { + return role.id; + }) as any; await Promise.all([ message.save(), - emitEvent({ event: "MESSAGE_CREATE", 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() + emitEvent({ + event: "MESSAGE_CREATE", + 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); - } + }, ); - diff --git a/src/api/routes/channels/#channel_id/permissions.ts b/src/api/routes/channels/#channel_id/permissions.ts index e74a0255..89be843f 100644 --- a/src/api/routes/channels/#channel_id/permissions.ts +++ b/src/api/routes/channels/#channel_id/permissions.ts @@ -6,7 +6,7 @@ import { emitEvent, getPermission, Member, - Role + Role, } from "@fosscord/util"; import { Router, Response, Request } from "express"; 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) -export interface ChannelPermissionOverwriteSchema extends ChannelPermissionOverwrite { } +export interface ChannelPermissionOverwriteSchema + extends ChannelPermissionOverwrite {} router.put( "/:overwrite_id", - route({ body: "ChannelPermissionOverwriteSchema", permission: "MANAGE_ROLES" }), + route({ + body: "ChannelPermissionOverwriteSchema", + permission: "MANAGE_ROLES", + }), async (req: Request, res: Response) => { const { channel_id, overwrite_id } = req.params; 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 (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) { - 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); - // @ts-ignore - var overwrite: ChannelPermissionOverwrite = channel.permission_overwrites.find((x) => x.id === overwrite_id); + //@ts-ignore + var overwrite: ChannelPermissionOverwrite = + channel.permission_overwrites?.find((x) => x.id === overwrite_id); if (!overwrite) { // @ts-ignore overwrite = { id: overwrite_id, - type: body.type + type: body.type, }; channel.permission_overwrites!.push(overwrite); } - overwrite.allow = String(req.permission!.bitfield & (BigInt(body.allow) || BigInt("0"))); - overwrite.deny = String(req.permission!.bitfield & (BigInt(body.deny) || BigInt("0"))); + overwrite.allow = String( + req.permission!.bitfield & (BigInt(body.allow) || BigInt("0")), + ); + overwrite.deny = String( + req.permission!.bitfield & (BigInt(body.deny) || BigInt("0")), + ); await Promise.all([ channel.save(), emitEvent({ event: "CHANNEL_UPDATE", channel_id, - data: channel - } as ChannelUpdateEvent) + data: channel, + } as ChannelUpdateEvent), ]); return res.sendStatus(204); - } + }, ); // TODO: check permission hierarchy -router.delete("/:overwrite_id", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { - const { channel_id, overwrite_id } = req.params; +router.delete( + "/:overwrite_id", + route({ permission: "MANAGE_ROLES" }), + async (req: Request, res: Response) => { + const { channel_id, overwrite_id } = req.params; - const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); - if (!channel.guild_id) throw new HTTPError("Channel not found", 404); + const channel = await Channel.findOneOrFail({ + where: { id: channel_id }, + }); + 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([ - channel.save(), - emitEvent({ - event: "CHANNEL_UPDATE", - channel_id, - data: channel - } as ChannelUpdateEvent) - ]); + await Promise.all([ + channel.save(), + emitEvent({ + event: "CHANNEL_UPDATE", + channel_id, + data: channel, + } as ChannelUpdateEvent), + ]); - return res.sendStatus(204); -}); + return res.sendStatus(204); + }, +); export default router; diff --git a/src/api/routes/channels/#channel_id/pins.ts b/src/api/routes/channels/#channel_id/pins.ts index 30507c71..d3f6960a 100644 --- a/src/api/routes/channels/#channel_id/pins.ts +++ b/src/api/routes/channels/#channel_id/pins.ts @@ -6,7 +6,7 @@ import { getPermission, Message, MessageUpdateEvent, - DiscordApiErrors + DiscordApiErrors, } from "@fosscord/util"; import { Router, Request, Response } from "express"; import { HTTPError } from "lambert-server"; @@ -14,77 +14,100 @@ import { route } from "@fosscord/api"; const router: Router = Router(); -router.put("/:message_id", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: Response) => { - const { channel_id, message_id } = req.params; - - const message = await Message.findOneOrFail({ where: { id: message_id } }); - - // * in dm channels anyone can pin messages -> only check for guilds - if (message.guild_id) req.permission!.hasThrow("MANAGE_MESSAGES"); - - const pinned_count = await Message.count({ where: { channel: { id: channel_id }, pinned: true } }); - const { maxPins } = Config.get().limits.channel; - if (pinned_count >= maxPins) throw DiscordApiErrors.MAXIMUM_PINS.withParams(maxPins); - - await Promise.all([ - Message.update({ id: message_id }, { pinned: true }), - emitEvent({ - event: "MESSAGE_UPDATE", - channel_id, - data: message - } as MessageUpdateEvent), - emitEvent({ - event: "CHANNEL_PINS_UPDATE", - channel_id, - data: { +router.put( + "/:message_id", + route({ permission: "VIEW_CHANNEL" }), + async (req: Request, res: Response) => { + const { channel_id, message_id } = req.params; + + const message = await Message.findOneOrFail({ + where: { id: message_id }, + }); + + // * in dm channels anyone can pin messages -> only check for guilds + if (message.guild_id) req.permission!.hasThrow("MANAGE_MESSAGES"); + + const pinned_count = await Message.count({ + where: { channel: { id: channel_id }, pinned: true }, + }); + const { maxPins } = Config.get().limits.channel; + if (pinned_count >= maxPins) + throw DiscordApiErrors.MAXIMUM_PINS.withParams(maxPins); + + await Promise.all([ + Message.update({ id: message_id }, { pinned: true }), + emitEvent({ + event: "MESSAGE_UPDATE", channel_id, - guild_id: message.guild_id, - last_pin_timestamp: undefined - } - } as ChannelPinsUpdateEvent) - ]); - - res.sendStatus(204); -}); - -router.delete("/:message_id", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: Response) => { - const { channel_id, message_id } = req.params; - - const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); - if (channel.guild_id) req.permission!.hasThrow("MANAGE_MESSAGES"); - - const message = await Message.findOneOrFail({ where: { id: message_id } }); - message.pinned = false; - - await Promise.all([ - message.save(), - - emitEvent({ - event: "MESSAGE_UPDATE", - channel_id, - data: message - } as MessageUpdateEvent), - - emitEvent({ - event: "CHANNEL_PINS_UPDATE", - channel_id, - data: { + data: message, + } as MessageUpdateEvent), + emitEvent({ + event: "CHANNEL_PINS_UPDATE", channel_id, - guild_id: channel.guild_id, - last_pin_timestamp: undefined - } - } as ChannelPinsUpdateEvent) - ]); - - res.sendStatus(204); -}); - -router.get("/", route({ permission: ["READ_MESSAGE_HISTORY"] }), async (req: Request, res: Response) => { - const { channel_id } = req.params; - - let pins = await Message.find({ where: { channel_id: channel_id, pinned: true } }); + data: { + channel_id, + guild_id: message.guild_id, + last_pin_timestamp: undefined, + }, + } as ChannelPinsUpdateEvent), + ]); + + res.sendStatus(204); + }, +); + +router.delete( + "/:message_id", + route({ permission: "VIEW_CHANNEL" }), + async (req: Request, res: Response) => { + const { channel_id, message_id } = req.params; + + const channel = await Channel.findOneOrFail({ + where: { id: channel_id }, + }); + if (channel.guild_id) req.permission!.hasThrow("MANAGE_MESSAGES"); + + const message = await Message.findOneOrFail({ + where: { id: message_id }, + }); + message.pinned = false; + + await Promise.all([ + message.save(), + + emitEvent({ + event: "MESSAGE_UPDATE", + channel_id, + data: message, + } as MessageUpdateEvent), - res.send(pins); -}); + emitEvent({ + event: "CHANNEL_PINS_UPDATE", + channel_id, + data: { + channel_id, + guild_id: channel.guild_id, + last_pin_timestamp: undefined, + }, + } as ChannelPinsUpdateEvent), + ]); + + res.sendStatus(204); + }, +); + +router.get( + "/", + route({ permission: ["READ_MESSAGE_HISTORY"] }), + async (req: Request, res: Response) => { + const { channel_id } = req.params; + + let pins = await Message.find({ + where: { channel_id: channel_id, pinned: true }, + }); + + res.send(pins); + }, +); export default router; diff --git a/src/api/routes/channels/#channel_id/purge.ts b/src/api/routes/channels/#channel_id/purge.ts index 9fe6b658..a9f88662 100644 --- a/src/api/routes/channels/#channel_id/purge.ts +++ b/src/api/routes/channels/#channel_id/purge.ts @@ -21,52 +21,79 @@ export default router; /** TODO: apply the delete bit by bit to prevent client and database stress **/ -router.post("/", route({ /*body: "PurgeSchema",*/ }), async (req: Request, res: Response) => { - const { channel_id } = req.params; - const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); +router.post( + "/", + route({ + /*body: "PurgeSchema",*/ + }), + async (req: Request, res: Response) => { + const { channel_id } = req.params; + const channel = await Channel.findOneOrFail({ + where: { id: channel_id }, + }); - if (!channel.guild_id) throw new HTTPError("Can't purge dm channels", 400); - isTextChannel(channel.type); + if (!channel.guild_id) + throw new HTTPError("Can't purge dm channels", 400); + isTextChannel(channel.type); - const rights = await getRights(req.user_id); - if (!rights.has("MANAGE_MESSAGES")) { - const permissions = await getPermission(req.user_id, channel.guild_id, channel_id); - permissions.hasThrow("MANAGE_MESSAGES"); - permissions.hasThrow("MANAGE_CHANNELS"); - } + const rights = await getRights(req.user_id); + if (!rights.has("MANAGE_MESSAGES")) { + const permissions = await getPermission( + req.user_id, + channel.guild_id, + channel_id, + ); + permissions.hasThrow("MANAGE_MESSAGES"); + permissions.hasThrow("MANAGE_CHANNELS"); + } - const { before, after } = req.body as PurgeSchema; + const { before, after } = req.body as PurgeSchema; - // TODO: send the deletion event bite-by-bite to prevent client stress - - var query: FindManyOptions<Message> & { where: { id?: any; }; } = { - order: { id: "ASC" }, - // take: limit, - where: { - channel_id, - id: Between(after, before), // the right way around - 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 - }, - relations: ["author", "webhook", "application", "mentions", "mention_roles", "mention_channels", "sticker_items", "attachments"] - }; + // TODO: send the deletion event bite-by-bite to prevent client stress + var query: FindManyOptions<Message> & { where: { id?: any } } = { + order: { id: "ASC" }, + // take: limit, + where: { + channel_id, + id: Between(after, before), // the right way around + 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 + }, + relations: [ + "author", + "webhook", + "application", + "mentions", + "mention_roles", + "mention_channels", + "sticker_items", + "attachments", + ], + }; - const messages = await Message.find(query); - const endpoint = Config.get().cdn.endpointPublic; + const messages = await Message.find(query); + const endpoint = Config.get().cdn.endpointPublic; - if (messages.length == 0) { - res.sendStatus(304); - return; - } + if (messages.length == 0) { + res.sendStatus(304); + return; + } - await Message.delete(messages.map((x) => x.id)); + await Message.delete(messages.map((x) => x.id)); - await emitEvent({ - event: "MESSAGE_DELETE_BULK", - channel_id, - data: { ids: messages.map(x => x.id), channel_id, guild_id: channel.guild_id } - } as MessageDeleteBulkEvent); + await emitEvent({ + event: "MESSAGE_DELETE_BULK", + channel_id, + data: { + ids: messages.map((x) => x.id), + channel_id, + guild_id: channel.guild_id, + }, + } as MessageDeleteBulkEvent); - res.sendStatus(204); -}); + res.sendStatus(204); + }, +); diff --git a/src/api/routes/channels/#channel_id/recipients.ts b/src/api/routes/channels/#channel_id/recipients.ts index 25854415..cc7e5756 100644 --- a/src/api/routes/channels/#channel_id/recipients.ts +++ b/src/api/routes/channels/#channel_id/recipients.ts @@ -8,7 +8,7 @@ import { emitEvent, PublicUserProjection, Recipient, - User + User, } from "@fosscord/util"; import { route } from "@fosscord/api"; @@ -16,34 +16,48 @@ const router: Router = Router(); router.put("/:user_id", route({}), async (req: Request, res: Response) => { 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) { - 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); } else { if (channel.recipients!.map((r) => r.user_id).includes(user_id)) { 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 emitEvent({ event: "CHANNEL_CREATE", data: await DmChannelDTO.from(channel, [user_id]), - user_id: user_id + user_id: user_id, }); await emitEvent({ event: "CHANNEL_RECIPIENT_ADD", data: { 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); 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) => { const { channel_id, user_id } = req.params; - const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients"] }); - if (!(channel.type === ChannelType.GROUP_DM && (channel.owner_id === req.user_id || user_id === req.user_id))) + const channel = await Channel.findOneOrFail({ + 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; if (!channel.recipients!.map((r) => r.user_id).includes(user_id)) { diff --git a/src/api/routes/channels/#channel_id/typing.ts b/src/api/routes/channels/#channel_id/typing.ts index 99460f6e..03f76205 100644 --- a/src/api/routes/channels/#channel_id/typing.ts +++ b/src/api/routes/channels/#channel_id/typing.ts @@ -4,26 +4,42 @@ import { Router, Request, Response } from "express"; const router: Router = Router(); -router.post("/", route({ permission: "SEND_MESSAGES" }), async (req: Request, res: Response) => { - const { channel_id } = req.params; - const user_id = req.user_id; - const timestamp = Date.now(); - const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); - const member = await Member.findOne({ where: { id: user_id, guild_id: channel.guild_id }, relations: ["roles", "user"] }); +router.post( + "/", + route({ permission: "SEND_MESSAGES" }), + async (req: Request, res: Response) => { + const { channel_id } = req.params; + const user_id = req.user_id; + const timestamp = Date.now(); + const channel = await Channel.findOneOrFail({ + where: { id: channel_id }, + }); + const member = await Member.findOne({ + where: { id: user_id, guild_id: channel.guild_id }, + relations: ["roles", "user"], + }); - await emitEvent({ - event: "TYPING_START", - channel_id: channel_id, - data: { - ...(member ? { member: { ...member, roles: member?.roles?.map((x) => x.id) } } : null), - channel_id, - timestamp, - user_id, - guild_id: channel.guild_id - } - } as TypingStartEvent); + await emitEvent({ + event: "TYPING_START", + channel_id: channel_id, + data: { + ...(member + ? { + member: { + ...member, + roles: member?.roles?.map((x) => x.id), + }, + } + : null), + channel_id, + timestamp, + user_id, + guild_id: channel.guild_id, + }, + } as TypingStartEvent); - res.sendStatus(204); -}); + res.sendStatus(204); + }, +); export default router; diff --git a/src/api/routes/channels/#channel_id/webhooks.ts b/src/api/routes/channels/#channel_id/webhooks.ts index 99c104ca..da8fe73c 100644 --- a/src/api/routes/channels/#channel_id/webhooks.ts +++ b/src/api/routes/channels/#channel_id/webhooks.ts @@ -13,22 +13,29 @@ router.get("/", route({}), async (req: Request, res: Response) => { }); // TODO: use Image Data Type for avatar instead of String -router.post("/", route({ body: "WebhookCreateSchema", permission: "MANAGE_WEBHOOKS" }), async (req: Request, res: Response) => { - const channel_id = req.params.channel_id; - const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); - - isTextChannel(channel.type); - if (!channel.guild_id) throw new HTTPError("Not a guild channel", 400); - - const webhook_count = await Webhook.count({ where: { channel_id } }); - const { maxWebhooks } = Config.get().limits.channel; - if (webhook_count > maxWebhooks) throw DiscordApiErrors.MAXIMUM_WEBHOOKS.withParams(maxWebhooks); - - var { avatar, name } = req.body as { name: string; avatar?: string }; - name = trimSpecial(name); - if (name === "clyde") throw new HTTPError("Invalid name", 400); - - // TODO: save webhook in database and send response -}); +router.post( + "/", + route({ body: "WebhookCreateSchema", permission: "MANAGE_WEBHOOKS" }), + async (req: Request, res: Response) => { + const channel_id = req.params.channel_id; + const channel = await Channel.findOneOrFail({ + where: { id: channel_id }, + }); + + isTextChannel(channel.type); + if (!channel.guild_id) throw new HTTPError("Not a guild channel", 400); + + const webhook_count = await Webhook.count({ where: { channel_id } }); + const { maxWebhooks } = Config.get().limits.channel; + if (webhook_count > maxWebhooks) + throw DiscordApiErrors.MAXIMUM_WEBHOOKS.withParams(maxWebhooks); + + var { avatar, name } = req.body as { name: string; avatar?: string }; + name = trimSpecial(name); + if (name === "clyde") throw new HTTPError("Invalid name", 400); + + // TODO: save webhook in database and send response + }, +); export default router; diff --git a/src/api/routes/discoverable-guilds.ts b/src/api/routes/discoverable-guilds.ts index 383e2b24..0e7cfbab 100644 --- a/src/api/routes/discoverable-guilds.ts +++ b/src/api/routes/discoverable-guilds.ts @@ -17,19 +17,33 @@ router.get("/", route({}), async (req: Request, res: Response) => { if (categories == undefined) { guilds = showAllGuilds ? 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 { 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({ - where: { primary_category_id: categories.toString(), features: Like("%DISCOVERABLE%") }, - take: Math.abs(Number(limit || configLimit)) - }); + where: { + primary_category_id: categories.toString(), + features: Like("%DISCOVERABLE%"), + }, + take: Math.abs(Number(limit || configLimit)), + }); } 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; diff --git a/src/api/routes/discovery.ts b/src/api/routes/discovery.ts index 6ab2cc13..90450035 100644 --- a/src/api/routes/discovery.ts +++ b/src/api/routes/discovery.ts @@ -10,7 +10,9 @@ router.get("/categories", route({}), async (req: Request, res: Response) => { 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); }); diff --git a/src/api/routes/downloads.ts b/src/api/routes/downloads.ts index df3df911..bc0750f7 100644 --- a/src/api/routes/downloads.ts +++ b/src/api/routes/downloads.ts @@ -10,9 +10,12 @@ router.get("/:branch", route({}), async (req: Request, res: Response) => { const { platform } = req.query; //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`]); }); diff --git a/src/api/routes/experiments.ts b/src/api/routes/experiments.ts index 7be86fb8..b2b7d724 100644 --- a/src/api/routes/experiments.ts +++ b/src/api/routes/experiments.ts @@ -5,7 +5,7 @@ const router = Router(); router.get("/", route({}), (req: Request, res: Response) => { // TODO: - res.send({ fingerprint: "", assignments: [], guild_experiments:[] }); + res.send({ fingerprint: "", assignments: [], guild_experiments: [] }); }); export default router; diff --git a/src/api/routes/gateway/bot.ts b/src/api/routes/gateway/bot.ts index f1dbb9df..2e26d019 100644 --- a/src/api/routes/gateway/bot.ts +++ b/src/api/routes/gateway/bot.ts @@ -18,9 +18,9 @@ export interface GatewayBotResponse { const options: RouteOptions = { test: { response: { - body: "GatewayBotResponse" - } - } + body: "GatewayBotResponse", + }, + }, }; router.get("/", route(options), (req: Request, res: Response) => { @@ -32,8 +32,8 @@ router.get("/", route(options), (req: Request, res: Response) => { total: 1000, remaining: 999, reset_after: 14400000, - max_concurrency: 1 - } + max_concurrency: 1, + }, }); }); diff --git a/src/api/routes/gateway/index.ts b/src/api/routes/gateway/index.ts index 9bad7478..a6ed9dc4 100644 --- a/src/api/routes/gateway/index.ts +++ b/src/api/routes/gateway/index.ts @@ -11,14 +11,16 @@ export interface GatewayResponse { const options: RouteOptions = { test: { response: { - body: "GatewayResponse" - } - } + body: "GatewayResponse", + }, + }, }; router.get("/", route(options), (req: Request, res: Response) => { 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; diff --git a/src/api/routes/gifs/search.ts b/src/api/routes/gifs/search.ts index c7468641..54352215 100644 --- a/src/api/routes/gifs/search.ts +++ b/src/api/routes/gifs/search.ts @@ -1,6 +1,6 @@ import { Router, Response, Request } from "express"; import fetch from "node-fetch"; -import ProxyAgent from 'proxy-agent'; +import ProxyAgent from "proxy-agent"; import { route } from "@fosscord/api"; import { getGifApiKey, parseGifResult } from "./trending"; @@ -11,16 +11,19 @@ router.get("/", route({}), async (req: Request, res: Response) => { const { q, media_format, locale } = req.query; const apiKey = getGifApiKey(); - + const agent = new ProxyAgent(); - const response = await fetch(`https://g.tenor.com/v1/search?q=${q}&media_format=${media_format}&locale=${locale}&key=${apiKey}`, { - agent, - method: "get", - headers: { "Content-Type": "application/json" } - }); + const response = await fetch( + `https://g.tenor.com/v1/search?q=${q}&media_format=${media_format}&locale=${locale}&key=${apiKey}`, + { + agent, + method: "get", + 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); }); diff --git a/src/api/routes/gifs/trending-gifs.ts b/src/api/routes/gifs/trending-gifs.ts index 52a8969d..e4b28e24 100644 --- a/src/api/routes/gifs/trending-gifs.ts +++ b/src/api/routes/gifs/trending-gifs.ts @@ -1,6 +1,6 @@ import { Router, Response, Request } from "express"; import fetch from "node-fetch"; -import ProxyAgent from 'proxy-agent'; +import ProxyAgent from "proxy-agent"; import { route } from "@fosscord/api"; import { getGifApiKey, parseGifResult } from "./trending"; @@ -11,16 +11,19 @@ router.get("/", route({}), async (req: Request, res: Response) => { const { media_format, locale } = req.query; const apiKey = getGifApiKey(); - + const agent = new ProxyAgent(); - const response = await fetch(`https://g.tenor.com/v1/trending?media_format=${media_format}&locale=${locale}&key=${apiKey}`, { - agent, - method: "get", - headers: { "Content-Type": "application/json" } - }); + const response = await fetch( + `https://g.tenor.com/v1/trending?media_format=${media_format}&locale=${locale}&key=${apiKey}`, + { + agent, + method: "get", + 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); }); diff --git a/src/api/routes/gifs/trending.ts b/src/api/routes/gifs/trending.ts index aa976c3f..58044ea5 100644 --- a/src/api/routes/gifs/trending.ts +++ b/src/api/routes/gifs/trending.ts @@ -1,6 +1,6 @@ import { Router, Response, Request } from "express"; import fetch from "node-fetch"; -import ProxyAgent from 'proxy-agent'; +import ProxyAgent from "proxy-agent"; import { route } from "@fosscord/api"; import { Config } from "@fosscord/util"; import { HTTPError } from "lambert-server"; @@ -16,14 +16,15 @@ export function parseGifResult(result: any) { gif_src: result.media[0].gif.url, width: result.media[0].mp4.dims[0], height: result.media[0].mp4.dims[1], - preview: result.media[0].mp4.preview + preview: result.media[0].mp4.preview, }; } export function getGifApiKey() { const { enabled, provider, apiKey } = Config.get().gif; 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; } @@ -34,28 +35,37 @@ router.get("/", route({}), async (req: Request, res: Response) => { const { media_format, locale } = req.query; const apiKey = getGifApiKey(); - + const agent = new ProxyAgent(); const [responseSource, trendGifSource] = await Promise.all([ - fetch(`https://g.tenor.com/v1/categories?locale=${locale}&key=${apiKey}`, { - agent, - method: "get", - headers: { "Content-Type": "application/json" } - }), - fetch(`https://g.tenor.com/v1/trending?locale=${locale}&key=${apiKey}`, { - agent, - method: "get", - headers: { "Content-Type": "application/json" } - }) + fetch( + `https://g.tenor.com/v1/categories?locale=${locale}&key=${apiKey}`, + { + agent, + method: "get", + headers: { "Content-Type": "application/json" }, + }, + ), + fetch( + `https://g.tenor.com/v1/trending?locale=${locale}&key=${apiKey}`, + { + agent, + method: "get", + headers: { "Content-Type": "application/json" }, + }, + ), ]); - const { tags } = await responseSource.json() as any; // TODO: types - const { results } = await trendGifSource.json() as any; //TODO: types; + const { tags } = (await responseSource.json()) as any; // TODO: types + const { results } = (await trendGifSource.json()) as any; //TODO: types; res.json({ - categories: tags.map((x: any) => ({ name: x.searchterm, src: x.image })), - gifs: [parseGifResult(results[0])] + categories: tags.map((x: any) => ({ + name: x.searchterm, + src: x.image, + })), + gifs: [parseGifResult(results[0])], }).status(200); }); diff --git a/src/api/routes/guild-recommendations.ts b/src/api/routes/guild-recommendations.ts index b851d710..bda37973 100644 --- a/src/api/routes/guild-recommendations.ts +++ b/src/api/routes/guild-recommendations.ts @@ -13,12 +13,21 @@ router.get("/", route({}), async (req: Request, res: Response) => { // TODO: implement this with default typeorm query // 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 ? await Guild.find({ take: Math.abs(Number(limit || 24)) }) - : await Guild.find({ where: { features: Like("%DISCOVERABLE%") }, take: Math.abs(Number(limit || 24)) }); - res.send({ recommended_guilds: guilds, load_id: `server_recs/${genLoadId(32)}` }).status(200); + : await Guild.find({ + 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; diff --git a/src/api/routes/guilds/#guild_id/audit-logs.ts b/src/api/routes/guilds/#guild_id/audit-logs.ts index b54835fc..76a11f6b 100644 --- a/src/api/routes/guilds/#guild_id/audit-logs.ts +++ b/src/api/routes/guilds/#guild_id/audit-logs.ts @@ -11,7 +11,7 @@ router.get("/", route({}), async (req: Request, res: Response) => { webhooks: [], guild_scheduled_events: [], threads: [], - application_commands: [] + application_commands: [], }); }); export default router; diff --git a/src/api/routes/guilds/#guild_id/bans.ts b/src/api/routes/guilds/#guild_id/bans.ts index ed00f9c0..930985d7 100644 --- a/src/api/routes/guilds/#guild_id/bans.ts +++ b/src/api/routes/guilds/#guild_id/bans.ts @@ -1,5 +1,15 @@ 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 { getIpAdress, route } from "@fosscord/api"; @@ -7,150 +17,184 @@ const router: Router = Router(); /* 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) => { - const { guild_id } = req.params; +router.get( + "/", + route({ permission: "BAN_MEMBERS" }), + async (req: Request, res: Response) => { + const { guild_id } = req.params; - let bans = await Ban.find({ where: { guild_id: guild_id } }); - let promisesToAwait: object[] = []; - const bansObj: object[] = []; + let bans = await Ban.find({ where: { guild_id: guild_id } }); + let promisesToAwait: object[] = []; + const bansObj: object[] = []; - bans.filter((ban) => ban.user_id !== ban.executor_id); // pretend self-bans don't exist to prevent victim chasing + bans.filter((ban) => ban.user_id !== ban.executor_id); // pretend self-bans don't exist to prevent victim chasing - bans.forEach((ban) => { - promisesToAwait.push(User.getPublicUser(ban.user_id)); - }); - - const bannedUsers: object[] = await Promise.all(promisesToAwait); - - bans.forEach((ban, index) => { - const user = bannedUsers[index] as User; - bansObj.push({ - reason: ban.reason, - user: { - username: user.username, - discriminator: user.discriminator, - id: user.id, - avatar: user.avatar, - public_flags: user.public_flags - } + bans.forEach((ban) => { + promisesToAwait.push(User.getPublicUser(ban.user_id)); }); - }); - - return res.json(bansObj); -}); - -router.get("/:user", route({ permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => { - const { guild_id } = req.params; - const user_id = req.params.ban; - - 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; - // pretend self-bans don't exist to prevent victim chasing - - /* Filter secret from registry. */ - - ban = ban as BanModeratorSchema; - - delete ban.ip; - - return res.json(ban); -}); - -router.put("/:user_id", route({ body: "BanCreateSchema", permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => { - const { guild_id } = req.params; - const banned_user_id = req.params.user_id; - if ((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); - - const banned_user = await User.getPublicUser(banned_user_id); + const bannedUsers: object[] = await Promise.all(promisesToAwait); + + bans.forEach((ban, index) => { + const user = bannedUsers[index] as User; + bansObj.push({ + reason: ban.reason, + user: { + username: user.username, + discriminator: user.discriminator, + id: user.id, + avatar: user.avatar, + public_flags: user.public_flags, + }, + }); + }); - const ban = Ban.create({ - user_id: banned_user_id, - guild_id: guild_id, - ip: getIpAdress(req), - executor_id: req.user_id, - reason: req.body.reason // || otherwise empty - }); + return res.json(bansObj); + }, +); + +router.get( + "/:user", + route({ permission: "BAN_MEMBERS" }), + async (req: Request, res: Response) => { + const { guild_id } = req.params; + const user_id = req.params.ban; + + 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; + // pretend self-bans don't exist to prevent victim chasing + + /* Filter secret from registry. */ + + ban = ban as BanModeratorSchema; + + delete ban.ip; + + return res.json(ban); + }, +); + +router.put( + "/:user_id", + route({ body: "BanCreateSchema", permission: "BAN_MEMBERS" }), + async (req: Request, res: Response) => { + const { guild_id } = req.params; + const banned_user_id = req.params.user_id; + + if ( + 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); + + const banned_user = await User.getPublicUser(banned_user_id); + + const ban = Ban.create({ + user_id: banned_user_id, + guild_id: guild_id, + ip: getIpAdress(req), + executor_id: req.user_id, + reason: req.body.reason, // || otherwise empty + }); - await Promise.all([ - Member.removeFromGuild(banned_user_id, guild_id), - ban.save(), - emitEvent({ - event: "GUILD_BAN_ADD", - data: { - guild_id: guild_id, - user: banned_user - }, - guild_id: guild_id - } as GuildBanAddEvent) - ]); - - return res.json(ban); -}); - -router.put("/@me", route({ body: "BanCreateSchema" }), async (req: Request, res: Response) => { - const { guild_id } = req.params; - - const banned_user = await User.getPublicUser(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); - - const ban = Ban.create({ - user_id: req.params.user_id, - guild_id: guild_id, - ip: getIpAdress(req), - executor_id: req.params.user_id, - reason: req.body.reason // || otherwise empty - }); - - await Promise.all([ - Member.removeFromGuild(req.user_id, guild_id), - ban.save(), - emitEvent({ - event: "GUILD_BAN_ADD", - data: { + await Promise.all([ + Member.removeFromGuild(banned_user_id, guild_id), + ban.save(), + emitEvent({ + event: "GUILD_BAN_ADD", + data: { + guild_id: guild_id, + user: banned_user, + }, guild_id: guild_id, - user: banned_user - }, - guild_id: guild_id - } as GuildBanAddEvent) - ]); + } as GuildBanAddEvent), + ]); + + return res.json(ban); + }, +); + +router.put( + "/@me", + route({ body: "BanCreateSchema" }), + async (req: Request, res: Response) => { + const { guild_id } = req.params; + + const banned_user = await User.getPublicUser(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, + ); + + const ban = Ban.create({ + user_id: req.params.user_id, + guild_id: guild_id, + ip: getIpAdress(req), + executor_id: req.params.user_id, + reason: req.body.reason, // || otherwise empty + }); - return res.json(ban); -}); + await Promise.all([ + Member.removeFromGuild(req.user_id, guild_id), + ban.save(), + emitEvent({ + event: "GUILD_BAN_ADD", + data: { + guild_id: guild_id, + user: banned_user, + }, + guild_id: guild_id, + } as GuildBanAddEvent), + ]); -router.delete("/:user_id", route({ permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => { - const { guild_id, user_id } = req.params; + return res.json(ban); + }, +); - let ban = await Ban.findOneOrFail({ where: { guild_id: guild_id, user_id: user_id } }); +router.delete( + "/:user_id", + route({ permission: "BAN_MEMBERS" }), + async (req: Request, res: Response) => { + const { guild_id, user_id } = req.params; - if (ban.user_id === ban.executor_id) throw DiscordApiErrors.UNKNOWN_BAN; - // make self-bans irreversible and hide them from view to avoid victim chasing + let ban = await Ban.findOneOrFail({ + where: { guild_id: guild_id, user_id: user_id }, + }); - const banned_user = await User.getPublicUser(user_id); + if (ban.user_id === ban.executor_id) throw DiscordApiErrors.UNKNOWN_BAN; + // make self-bans irreversible and hide them from view to avoid victim chasing - await Promise.all([ - Ban.delete({ - user_id: user_id, - guild_id - }), + const banned_user = await User.getPublicUser(user_id); - emitEvent({ - event: "GUILD_BAN_REMOVE", - data: { + await Promise.all([ + Ban.delete({ + user_id: user_id, guild_id, - user: banned_user - }, - guild_id - } as GuildBanRemoveEvent) - ]); - - return res.status(204).send(); -}); + }), + + emitEvent({ + event: "GUILD_BAN_REMOVE", + data: { + guild_id, + user: banned_user, + }, + guild_id, + } as GuildBanRemoveEvent), + ]); + + return res.status(204).send(); + }, +); export default router; diff --git a/src/api/routes/guilds/#guild_id/channels.ts b/src/api/routes/guilds/#guild_id/channels.ts index 7a5b50d1..af17465d 100644 --- a/src/api/routes/guilds/#guild_id/channels.ts +++ b/src/api/routes/guilds/#guild_id/channels.ts @@ -1,5 +1,10 @@ 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 { route } from "@fosscord/api"; const router = Router(); @@ -11,49 +16,77 @@ router.get("/", route({}), async (req: Request, res: Response) => { res.json(channels); }); -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 - const { guild_id } = req.params; - const body = req.body as ChannelModifySchema; +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 + const { guild_id } = req.params; + 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) => { - // changes guild channel position - const { guild_id } = req.params; - const body = req.body as ChannelReorderSchema; +router.patch( + "/", + route({ body: "ChannelReorderSchema", permission: "MANAGE_CHANNELS" }), + async (req: Request, res: Response) => { + // changes guild channel position + const { guild_id } = req.params; + const body = req.body as ChannelReorderSchema; - await Promise.all([ - 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); + await Promise.all([ + 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, + ); - const opts: any = {}; - if (x.position != null) opts.position = x.position; + const opts: any = {}; + if (x.position != null) opts.position = x.position; - if (x.parent_id) { - opts.parent_id = x.parent_id; - const parent_channel = await Channel.findOneOrFail({ - where: { id: x.parent_id, guild_id }, - select: ["permission_overwrites"] - }); - if (x.lock_permissions) { - opts.permission_overwrites = parent_channel.permission_overwrites; + if (x.parent_id) { + opts.parent_id = x.parent_id; + const parent_channel = await Channel.findOneOrFail({ + where: { id: x.parent_id, guild_id }, + select: ["permission_overwrites"], + }); + if (x.lock_permissions) { + opts.permission_overwrites = + parent_channel.permission_overwrites; + } } - } - await Channel.update({ guild_id, id: x.id }, opts); - const channel = await Channel.findOneOrFail({ where: { guild_id, id: x.id } }); + await Channel.update({ guild_id, id: x.id }, opts); + 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; diff --git a/src/api/routes/guilds/#guild_id/delete.ts b/src/api/routes/guilds/#guild_id/delete.ts index bd158c56..b951e4f4 100644 --- a/src/api/routes/guilds/#guild_id/delete.ts +++ b/src/api/routes/guilds/#guild_id/delete.ts @@ -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 { HTTPError } from "lambert-server"; import { route } from "@fosscord/api"; @@ -10,18 +20,22 @@ const router = Router(); router.post("/", route({}), async (req: Request, res: Response) => { var { guild_id } = req.params; - const guild = await Guild.findOneOrFail({ 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); + const guild = await Guild.findOneOrFail({ + 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([ Guild.delete({ id: guild_id }), // this will also delete all guild related data emitEvent({ event: "GUILD_DELETE", data: { - id: guild_id + id: guild_id, }, - guild_id: guild_id - } as GuildDeleteEvent) + guild_id: guild_id, + } as GuildDeleteEvent), ]); return res.sendStatus(204); diff --git a/src/api/routes/guilds/#guild_id/discovery-requirements.ts b/src/api/routes/guilds/#guild_id/discovery-requirements.ts index ad20633f..7e63c06b 100644 --- a/src/api/routes/guilds/#guild_id/discovery-requirements.ts +++ b/src/api/routes/guilds/#guild_id/discovery-requirements.ts @@ -6,33 +6,33 @@ import { route } from "@fosscord/api"; const router = Router(); router.get("/", route({}), async (req: Request, res: Response) => { - const { guild_id } = req.params; - // TODO: - // Load from database - // Admin control, but for now it allows anyone to be discoverable + const { guild_id } = req.params; + // TODO: + // Load from database + // Admin control, but for now it allows anyone to be discoverable res.send({ guild_id: guild_id, safe_environment: true, - healthy: true, - health_score_pending: false, - size: true, - nsfw_properties: {}, - protected: true, - sufficient: true, - sufficient_without_grace_period: true, - valid_rules_channel: true, - retention_healthy: true, - engagement_healthy: true, - age: true, - minimum_age: 0, - health_score: { - avg_nonnew_participators: 0, - avg_nonnew_communicators: 0, - num_intentful_joiners: 0, - perc_ret_w1_intentful: 0 - }, - minimum_size: 0 + healthy: true, + health_score_pending: false, + size: true, + nsfw_properties: {}, + protected: true, + sufficient: true, + sufficient_without_grace_period: true, + valid_rules_channel: true, + retention_healthy: true, + engagement_healthy: true, + age: true, + minimum_age: 0, + health_score: { + avg_nonnew_participators: 0, + avg_nonnew_communicators: 0, + num_intentful_joiners: 0, + perc_ret_w1_intentful: 0, + }, + minimum_size: 0, }); }); diff --git a/src/api/routes/guilds/#guild_id/emojis.ts b/src/api/routes/guilds/#guild_id/emojis.ts index cf9d742a..6e8570eb 100644 --- a/src/api/routes/guilds/#guild_id/emojis.ts +++ b/src/api/routes/guilds/#guild_id/emojis.ts @@ -1,5 +1,17 @@ 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"; const router = Router(); @@ -9,7 +21,10 @@ router.get("/", route({}), async (req: Request, res: Response) => { 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); }); @@ -19,89 +34,115 @@ router.get("/:emoji_id", route({}), async (req: Request, res: Response) => { 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); }); -router.post("/", route({ body: "EmojiCreateSchema", permission: "MANAGE_EMOJIS_AND_STICKERS" }), async (req: Request, res: Response) => { - const { guild_id } = req.params; - const body = req.body as EmojiCreateSchema; - - const id = Snowflake.generate(); - const emoji_count = await Emoji.count({ where: { guild_id: guild_id } }); - const { maxEmojis } = Config.get().limits.guild; - - if (emoji_count >= maxEmojis) throw DiscordApiErrors.MAXIMUM_NUMBER_OF_EMOJIS_REACHED.withParams(maxEmojis); - if (body.require_colons == null) body.require_colons = true; - - const user = await User.findOneOrFail({ where: { id: req.user_id } }); - body.image = (await handleFile(`/emojis/${id}`, body.image)) as string; - - const emoji = await Emoji.create({ - id: id, - guild_id: guild_id, - ...body, - require_colons: body.require_colons ?? undefined, // schema allows nulls, db does not - user: user, - managed: false, - animated: false, // TODO: Add support animated emojis - available: true, - roles: [] - }).save(); - - await emitEvent({ - event: "GUILD_EMOJIS_UPDATE", - guild_id: guild_id, - data: { +router.post( + "/", + route({ + body: "EmojiCreateSchema", + permission: "MANAGE_EMOJIS_AND_STICKERS", + }), + async (req: Request, res: Response) => { + const { guild_id } = req.params; + const body = req.body as EmojiCreateSchema; + + const id = Snowflake.generate(); + const emoji_count = await Emoji.count({ + where: { guild_id: guild_id }, + }); + const { maxEmojis } = Config.get().limits.guild; + + if (emoji_count >= maxEmojis) + throw DiscordApiErrors.MAXIMUM_NUMBER_OF_EMOJIS_REACHED.withParams( + maxEmojis, + ); + if (body.require_colons == null) body.require_colons = true; + + const user = await User.findOneOrFail({ where: { id: req.user_id } }); + body.image = (await handleFile(`/emojis/${id}`, body.image)) as string; + + const emoji = await Emoji.create({ + id: id, guild_id: guild_id, - emojis: await Emoji.find({ where: { guild_id: guild_id } }) - } - } as GuildEmojisUpdateEvent); + ...body, + require_colons: body.require_colons ?? undefined, // schema allows nulls, db does not + user: user, + managed: false, + animated: false, // TODO: Add support animated emojis + available: true, + roles: [], + }).save(); - return res.status(201).json(emoji); -}); + await emitEvent({ + event: "GUILD_EMOJIS_UPDATE", + guild_id: guild_id, + data: { + guild_id: guild_id, + emojis: await Emoji.find({ where: { guild_id: guild_id } }), + }, + } as GuildEmojisUpdateEvent); + + return res.status(201).json(emoji); + }, +); router.patch( "/:emoji_id", - route({ body: "EmojiModifySchema", permission: "MANAGE_EMOJIS_AND_STICKERS" }), + route({ + body: "EmojiModifySchema", + permission: "MANAGE_EMOJIS_AND_STICKERS", + }), async (req: Request, res: Response) => { const { emoji_id, guild_id } = req.params; 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({ event: "GUILD_EMOJIS_UPDATE", guild_id: guild_id, data: { guild_id: guild_id, - emojis: await Emoji.find({ where: { guild_id: guild_id } }) - } + emojis: await Emoji.find({ where: { guild_id: guild_id } }), + }, } as GuildEmojisUpdateEvent); return res.json(emoji); - } + }, ); -router.delete("/:emoji_id", route({ permission: "MANAGE_EMOJIS_AND_STICKERS" }), async (req: Request, res: Response) => { - const { emoji_id, guild_id } = req.params; +router.delete( + "/:emoji_id", + route({ permission: "MANAGE_EMOJIS_AND_STICKERS" }), + async (req: Request, res: Response) => { + const { emoji_id, guild_id } = req.params; - await Emoji.delete({ - id: emoji_id, - guild_id: guild_id - }); + await Emoji.delete({ + id: emoji_id, + guild_id: guild_id, + }); - await emitEvent({ - event: "GUILD_EMOJIS_UPDATE", - guild_id: guild_id, - data: { + await emitEvent({ + event: "GUILD_EMOJIS_UPDATE", guild_id: guild_id, - emojis: await Emoji.find({ where: { guild_id: guild_id } }) - } - } as GuildEmojisUpdateEvent); + data: { + guild_id: guild_id, + emojis: await Emoji.find({ where: { guild_id: guild_id } }), + }, + } as GuildEmojisUpdateEvent); - res.sendStatus(204); -}); + res.sendStatus(204); + }, +); export default router; diff --git a/src/api/routes/guilds/#guild_id/index.ts b/src/api/routes/guilds/#guild_id/index.ts index afeb0938..715a3835 100644 --- a/src/api/routes/guilds/#guild_id/index.ts +++ b/src/api/routes/guilds/#guild_id/index.ts @@ -1,5 +1,15 @@ 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 { route } from "@fosscord/api"; @@ -26,9 +36,13 @@ router.get("/", route({}), async (req: Request, res: Response) => { const [guild, member] = await Promise.all([ 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 guild.joined_at = member?.joined_at; @@ -36,39 +50,57 @@ router.get("/", route({}), async (req: Request, res: Response) => { return res.send(guild); }); -router.patch("/", route({ body: "GuildUpdateSchema" }), async (req: Request, res: Response) => { - const body = req.body as GuildUpdateSchema; - const { guild_id } = req.params; - - - const rights = await getRights(req.user_id); - const permission = await getPermission(req.user_id, guild_id); - - if (!rights.has("MANAGE_GUILDS") || !permission.has("MANAGE_GUILD")) - throw DiscordApiErrors.MISSING_PERMISSIONS.withParams("MANAGE_GUILD"); - - // TODO: guild update check image - - if (body.icon) body.icon = await handleFile(`/icons/${guild_id}`, body.icon); - 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({ - where: { id: guild_id }, - relations: ["emojis", "roles", "stickers"] - }); - // TODO: check if body ids are valid - guild.assign(body); - - const data = guild.toJSON(); - // TODO: guild hashes - // TODO: fix vanity_url_code, template_id - delete data.vanity_url_code; - delete data.template_id; - - await Promise.all([guild.save(), emitEvent({ event: "GUILD_UPDATE", data, guild_id } as GuildUpdateEvent)]); - - return res.json(data); -}); +router.patch( + "/", + route({ body: "GuildUpdateSchema" }), + async (req: Request, res: Response) => { + const body = req.body as GuildUpdateSchema; + const { guild_id } = req.params; + + const rights = await getRights(req.user_id); + const permission = await getPermission(req.user_id, guild_id); + + if (!rights.has("MANAGE_GUILDS") || !permission.has("MANAGE_GUILD")) + throw DiscordApiErrors.MISSING_PERMISSIONS.withParams( + "MANAGE_GUILD", + ); + + // TODO: guild update check image + + if (body.icon) + body.icon = await handleFile(`/icons/${guild_id}`, body.icon); + 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({ + where: { id: guild_id }, + relations: ["emojis", "roles", "stickers"], + }); + // TODO: check if body ids are valid + guild.assign(body); + + const data = guild.toJSON(); + // TODO: guild hashes + // TODO: fix vanity_url_code, template_id + delete data.vanity_url_code; + delete data.template_id; + + await Promise.all([ + guild.save(), + emitEvent({ + event: "GUILD_UPDATE", + data, + guild_id, + } as GuildUpdateEvent), + ]); + + return res.json(data); + }, +); export default router; diff --git a/src/api/routes/guilds/#guild_id/invites.ts b/src/api/routes/guilds/#guild_id/invites.ts index b7534e31..4d033e9c 100644 --- a/src/api/routes/guilds/#guild_id/invites.ts +++ b/src/api/routes/guilds/#guild_id/invites.ts @@ -4,12 +4,19 @@ import { Request, Response, Router } from "express"; const router = Router(); -router.get("/", route({ permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { - const { guild_id } = req.params; +router.get( + "/", + route({ permission: "MANAGE_GUILD" }), + async (req: Request, res: Response) => { + const { guild_id } = req.params; - const invites = await Invite.find({ where: { guild_id }, relations: PublicInviteRelation }); + const invites = await Invite.find({ + where: { guild_id }, + relations: PublicInviteRelation, + }); - return res.json(invites); -}); + return res.json(invites); + }, +); export default router; diff --git a/src/api/routes/guilds/#guild_id/member-verification.ts b/src/api/routes/guilds/#guild_id/member-verification.ts index 265a1b35..c2f946b2 100644 --- a/src/api/routes/guilds/#guild_id/member-verification.ts +++ b/src/api/routes/guilds/#guild_id/member-verification.ts @@ -2,12 +2,12 @@ import { Router, Request, Response } from "express"; import { route } from "@fosscord/api"; const router = Router(); -router.get("/",route({}), async (req: Request, res: Response) => { +router.get("/", route({}), async (req: Request, res: Response) => { // TODO: member verification res.status(404).json({ message: "Unknown Guild Member Verification Form", - code: 10068 + code: 10068, }); }); diff --git a/src/api/routes/guilds/#guild_id/members/#member_id/index.ts b/src/api/routes/guilds/#guild_id/members/#member_id/index.ts index 407619d3..2d867920 100644 --- a/src/api/routes/guilds/#guild_id/members/#member_id/index.ts +++ b/src/api/routes/guilds/#guild_id/members/#member_id/index.ts @@ -1,5 +1,16 @@ 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"; const router = Router(); @@ -8,48 +19,63 @@ router.get("/", route({}), async (req: Request, res: Response) => { const { guild_id, member_id } = req.params; 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); }); -router.patch("/", route({ body: "MemberChangeSchema" }), async (req: Request, res: Response) => { - let { guild_id, member_id } = req.params; - if (member_id === "@me") member_id = req.user_id; - const body = req.body as MemberChangeSchema; - - const member = await Member.findOneOrFail({ where: { id: member_id, guild_id }, relations: ["roles", "user"] }); - const permission = await getPermission(req.user_id, guild_id); - const everyone = await Role.findOneOrFail({ where: { guild_id: guild_id, name: "@everyone", position: 0 } }); - - if (body.roles) { - permission.hasThrow("MANAGE_ROLES"); - - 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 - } - - if ('nick' in body) { - permission.hasThrow(req.user_id == member.user.id ? "CHANGE_NICKNAME" : "MANAGE_NICKNAMES"); - member.nick = body.nick?.trim() || undefined; - } - - await member.save(); - - member.roles = member.roles.filter((x) => x.id !== everyone.id); - - // do not use promise.all as we have to first write to db before emitting the event to catch errors - await emitEvent({ - event: "GUILD_MEMBER_UPDATE", - guild_id, - data: { ...member, roles: member.roles.map((x) => x.id) } - } as GuildMemberUpdateEvent); - - res.json(member); -}); +router.patch( + "/", + route({ body: "MemberChangeSchema" }), + async (req: Request, res: Response) => { + let { guild_id, member_id } = req.params; + if (member_id === "@me") member_id = req.user_id; + const body = req.body as MemberChangeSchema; + + const member = await Member.findOneOrFail({ + where: { id: member_id, guild_id }, + relations: ["roles", "user"], + }); + const permission = await getPermission(req.user_id, guild_id); + const everyone = await Role.findOneOrFail({ + where: { guild_id: guild_id, name: "@everyone", position: 0 }, + }); + + if (body.roles) { + permission.hasThrow("MANAGE_ROLES"); + + 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 + } + + if ("nick" in body) { + permission.hasThrow( + req.user_id == member.user.id + ? "CHANGE_NICKNAME" + : "MANAGE_NICKNAMES", + ); + member.nick = body.nick?.trim() || undefined; + } + + await member.save(); + + member.roles = member.roles.filter((x) => x.id !== everyone.id); + + // do not use promise.all as we have to first write to db before emitting the event to catch errors + await emitEvent({ + event: "GUILD_MEMBER_UPDATE", + guild_id, + data: { ...member, roles: member.roles.map((x) => x.id) }, + } as GuildMemberUpdateEvent); + + res.json(member); + }, +); router.put("/", route({}), async (req: Request, res: Response) => { - // TODO: Lurker mode const rights = await getRights(req.user_id); @@ -59,23 +85,23 @@ router.put("/", route({}), async (req: Request, res: Response) => { member_id = req.user_id; rights.hasThrow("JOIN_GUILDS"); } else { - // TODO: join others by controller + // TODO: join others by controller } var guild = await Guild.findOneOrFail({ - where: { id: guild_id } + where: { id: guild_id }, }); var emoji = await Emoji.find({ - where: { guild_id: guild_id } + where: { guild_id: guild_id }, }); var roles = await Role.find({ - where: { guild_id: guild_id } + where: { guild_id: guild_id }, }); var stickers = await Sticker.find({ - where: { guild_id: guild_id } + where: { guild_id: guild_id }, }); await Member.addToGuild(member_id, guild_id); diff --git a/src/api/routes/guilds/#guild_id/members/#member_id/nick.ts b/src/api/routes/guilds/#guild_id/members/#member_id/nick.ts index edd47605..20443821 100644 --- a/src/api/routes/guilds/#guild_id/members/#member_id/nick.ts +++ b/src/api/routes/guilds/#guild_id/members/#member_id/nick.ts @@ -4,19 +4,23 @@ import { Request, Response, Router } from "express"; const router = Router(); -router.patch("/", route({ body: "MemberNickChangeSchema" }), async (req: Request, res: Response) => { - var { guild_id, member_id } = req.params; - var permissionString: PermissionResolvable = "MANAGE_NICKNAMES"; - if (member_id === "@me") { - member_id = req.user_id; - permissionString = "CHANGE_NICKNAME"; - } +router.patch( + "/", + route({ body: "MemberNickChangeSchema" }), + async (req: Request, res: Response) => { + var { guild_id, member_id } = req.params; + var permissionString: PermissionResolvable = "MANAGE_NICKNAMES"; + if (member_id === "@me") { + member_id = req.user_id; + permissionString = "CHANGE_NICKNAME"; + } - const perms = await getPermission(req.user_id, guild_id); - perms.hasThrow(permissionString); + const perms = await getPermission(req.user_id, guild_id); + perms.hasThrow(permissionString); - await Member.changeNickname(member_id, guild_id, req.body.nick); - res.status(200).send(); -}); + await Member.changeNickname(member_id, guild_id, req.body.nick); + res.status(200).send(); + }, +); export default router; diff --git a/src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts b/src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts index 8f5ca7ba..c0383912 100644 --- a/src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts +++ b/src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts @@ -4,18 +4,26 @@ import { Request, Response, Router } from "express"; const router = Router(); -router.delete("/", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { - const { guild_id, role_id, member_id } = req.params; +router.delete( + "/", + route({ permission: "MANAGE_ROLES" }), + async (req: Request, res: Response) => { + const { guild_id, role_id, member_id } = req.params; - await Member.removeRole(member_id, guild_id, role_id); - res.sendStatus(204); -}); + await Member.removeRole(member_id, guild_id, role_id); + res.sendStatus(204); + }, +); -router.put("/", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { - const { guild_id, role_id, member_id } = req.params; +router.put( + "/", + route({ permission: "MANAGE_ROLES" }), + async (req: Request, res: Response) => { + const { guild_id, role_id, member_id } = req.params; - await Member.addRole(member_id, guild_id, role_id); - res.sendStatus(204); -}); + await Member.addRole(member_id, guild_id, role_id); + res.sendStatus(204); + }, +); export default router; diff --git a/src/api/routes/guilds/#guild_id/members/index.ts b/src/api/routes/guilds/#guild_id/members/index.ts index b730a4e7..b516b9e9 100644 --- a/src/api/routes/guilds/#guild_id/members/index.ts +++ b/src/api/routes/guilds/#guild_id/members/index.ts @@ -12,7 +12,8 @@ const router = Router(); router.get("/", route({}), async (req: Request, res: Response) => { const { guild_id } = req.params; 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 query = after ? { id: MoreThan(after) } : {}; @@ -22,7 +23,7 @@ router.get("/", route({}), async (req: Request, res: Response) => { where: { guild_id, ...query }, select: PublicMemberProjection, take: limit, - order: { id: "ASC" } + order: { id: "ASC" }, }); return res.json(members); diff --git a/src/api/routes/guilds/#guild_id/messages/search.ts b/src/api/routes/guilds/#guild_id/messages/search.ts index a7516ebd..f2d8087e 100644 --- a/src/api/routes/guilds/#guild_id/messages/search.ts +++ b/src/api/routes/guilds/#guild_id/messages/search.ts @@ -10,36 +10,62 @@ router.get("/", route({}), async (req: Request, res: Response) => { const { channel_id, content, - include_nsfw, // TODO + include_nsfw, // TODO offset, sort_order, - sort_by, // TODO: Handle 'relevance' + sort_by, // TODO: Handle 'relevance' limit, author_id, } = req.query; 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 (typeof sort_order != "string" - || ["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 + if ( + typeof sort_order != "string" || + ["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"); - 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> = { - 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, where: { guild: { 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, }; //@ts-ignore @@ -51,32 +77,34 @@ router.get("/", route({}), async (req: Request, res: Response) => { const messages: Message[] = await Message.find(query); - const messagesDto = messages.map(x => [{ - id: x.id, - type: x.type, - content: x.content, - channel_id: x.channel_id, - author: { - id: x.author?.id, - username: x.author?.username, - avatar: x.author?.avatar, - avatar_decoration: null, - discriminator: x.author?.discriminator, - public_flags: x.author?.public_flags, + const messagesDto = messages.map((x) => [ + { + id: x.id, + type: x.type, + content: x.content, + channel_id: x.channel_id, + author: { + id: x.author?.id, + username: x.author?.username, + avatar: x.author?.avatar, + avatar_decoration: null, + discriminator: x.author?.discriminator, + public_flags: x.author?.public_flags, + }, + attachments: x.attachments, + embeds: x.embeds, + mentions: x.mentions, + mention_roles: x.mention_roles, + pinned: x.pinned, + mention_everyone: x.mention_everyone, + tts: x.tts, + timestamp: x.timestamp, + edited_timestamp: x.edited_timestamp, + flags: x.flags, + components: x.components, + hit: true, }, - attachments: x.attachments, - embeds: x.embeds, - mentions: x.mentions, - mention_roles: x.mention_roles, - pinned: x.pinned, - mention_everyone: x.mention_everyone, - tts: x.tts, - timestamp: x.timestamp, - edited_timestamp: x.edited_timestamp, - flags: x.flags, - components: x.components, - hit: true, - }]); + ]); return res.json({ messages: messagesDto, @@ -84,4 +112,4 @@ router.get("/", route({}), async (req: Request, res: Response) => { }); }); -export default router; \ No newline at end of file +export default router; diff --git a/src/api/routes/guilds/#guild_id/prune.ts b/src/api/routes/guilds/#guild_id/prune.ts index 2e674349..d11244b1 100644 --- a/src/api/routes/guilds/#guild_id/prune.ts +++ b/src/api/routes/guilds/#guild_id/prune.ts @@ -5,7 +5,12 @@ import { route } from "@fosscord/api"; const router = Router(); //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(); date.setDate(date.getDate() - days); //Snowflake should have `generateFromTime` method? Or similar? @@ -19,21 +24,27 @@ export const inactiveMembers = async (guild_id: string, user_id: string, days: n where: [ { 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); 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. - 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"] }); + 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 myHighestRole = Math.max(...(me.roles?.map((x) => x.position) || [])); 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( (role) => 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; @@ -57,23 +68,39 @@ router.get("/", route({}), async (req: Request, res: Response) => { var roles = req.query.include_roles; 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 }); }); -router.post("/", route({ permission: "KICK_MEMBERS", right: "KICK_BAN_MEMBERS" }), async (req: Request, res: Response) => { - const days = parseInt(req.body.days); - - var roles = req.query.include_roles; - if (typeof roles === "string") roles = [roles]; - - const { guild_id } = req.params; - 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))); - - res.send({ purged: members.length }); -}); +router.post( + "/", + route({ permission: "KICK_MEMBERS", right: "KICK_BAN_MEMBERS" }), + async (req: Request, res: Response) => { + const days = parseInt(req.body.days); + + var roles = req.query.include_roles; + if (typeof roles === "string") roles = [roles]; + + const { guild_id } = req.params; + 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)), + ); + + res.send({ purged: members.length }); + }, +); export default router; diff --git a/src/api/routes/guilds/#guild_id/regions.ts b/src/api/routes/guilds/#guild_id/regions.ts index 308d5ee5..0b275ea4 100644 --- a/src/api/routes/guilds/#guild_id/regions.ts +++ b/src/api/routes/guilds/#guild_id/regions.ts @@ -9,7 +9,12 @@ router.get("/", route({}), async (req: Request, res: Response) => { const { guild_id } = req.params; const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); //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; diff --git a/src/api/routes/guilds/#guild_id/roles/#role_id/index.ts b/src/api/routes/guilds/#guild_id/roles/#role_id/index.ts index 87cf5261..e274e3d0 100644 --- a/src/api/routes/guilds/#guild_id/roles/#role_id/index.ts +++ b/src/api/routes/guilds/#guild_id/roles/#role_id/index.ts @@ -1,5 +1,13 @@ 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 { HTTPError } from "lambert-server"; @@ -12,57 +20,72 @@ router.get("/", route({}), async (req: Request, res: Response) => { return res.json(role); }); -router.delete("/", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { - const { guild_id, role_id } = req.params; - if (role_id === guild_id) throw new HTTPError("You can't delete the @everyone role"); +router.delete( + "/", + route({ permission: "MANAGE_ROLES" }), + async (req: Request, res: Response) => { + const { guild_id, role_id } = req.params; + if (role_id === guild_id) + throw new HTTPError("You can't delete the @everyone role"); - await Promise.all([ - Role.delete({ - id: role_id, - guild_id: guild_id - }), - emitEvent({ - event: "GUILD_ROLE_DELETE", - guild_id, - data: { + await Promise.all([ + Role.delete({ + id: role_id, + guild_id: guild_id, + }), + emitEvent({ + event: "GUILD_ROLE_DELETE", guild_id, - role_id - } - } as GuildRoleDeleteEvent) - ]); + data: { + guild_id, + role_id, + }, + } as GuildRoleDeleteEvent), + ]); - res.sendStatus(204); -}); + res.sendStatus(204); + }, +); // TODO: check role hierarchy -router.patch("/", route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { - const { role_id, guild_id } = req.params; - const body = req.body as RoleModifySchema; +router.patch( + "/", + route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }), + async (req: Request, res: Response) => { + const { role_id, guild_id } = req.params; + const body = req.body as RoleModifySchema; - if (body.icon && body.icon.length) body.icon = await handleFile(`/role-icons/${role_id}`, body.icon as string); - else body.icon = undefined; + if (body.icon && body.icon.length) + body.icon = await handleFile( + `/role-icons/${role_id}`, + body.icon as string, + ); + else body.icon = undefined; - const role = Role.create({ - ...body, - id: role_id, - guild_id, - permissions: String(req.permission!.bitfield & BigInt(body.permissions || "0")) - }); - - await Promise.all([ - role.save(), - emitEvent({ - event: "GUILD_ROLE_UPDATE", + const role = Role.create({ + ...body, + id: role_id, guild_id, - data: { + permissions: String( + req.permission!.bitfield & BigInt(body.permissions || "0"), + ), + }); + + await Promise.all([ + role.save(), + emitEvent({ + event: "GUILD_ROLE_UPDATE", guild_id, - role - } - } as GuildRoleUpdateEvent) - ]); + data: { + guild_id, + role, + }, + } as GuildRoleUpdateEvent), + ]); - res.json(role); -}); + res.json(role); + }, +); export default router; diff --git a/src/api/routes/guilds/#guild_id/roles/index.ts b/src/api/routes/guilds/#guild_id/roles/index.ts index c5a86400..e3c7373e 100644 --- a/src/api/routes/guilds/#guild_id/roles/index.ts +++ b/src/api/routes/guilds/#guild_id/roles/index.ts @@ -29,70 +29,87 @@ router.get("/", route({}), async (req: Request, res: Response) => { return res.json(roles); }); -router.post("/", route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { - const guild_id = req.params.guild_id; - const body = req.body as RoleModifySchema; - - const role_count = await Role.count({ where: { guild_id } }); - const { maxRoles } = Config.get().limits.guild; - - if (role_count > maxRoles) throw DiscordApiErrors.MAXIMUM_ROLES.withParams(maxRoles); - - const role = Role.create({ - // values before ...body are default and can be overriden - position: 0, - hoist: false, - color: 0, - mentionable: false, - ...body, - guild_id: guild_id, - managed: false, - permissions: String(req.permission!.bitfield & BigInt(body.permissions || "0")), - tags: undefined, - icon: undefined, - unicode_emoji: undefined - }); - - await Promise.all([ - role.save(), - emitEvent({ - event: "GUILD_ROLE_CREATE", - guild_id, - data: { - guild_id, - role: role - } - } as GuildRoleCreateEvent) - ]); - - res.json(role); -}); - -router.patch("/", route({ body: "RolePositionUpdateSchema" }), async (req: Request, res: Response) => { - const { guild_id } = req.params; - const body = req.body as RolePositionUpdateSchema; - - const perms = await getPermission(req.user_id, guild_id); - perms.hasThrow("MANAGE_ROLES"); - - 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 })) }); - - await Promise.all( - roles.map((x) => +router.post( + "/", + route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }), + async (req: Request, res: Response) => { + const guild_id = req.params.guild_id; + const body = req.body as RoleModifySchema; + + const role_count = await Role.count({ where: { guild_id } }); + const { maxRoles } = Config.get().limits.guild; + + if (role_count > maxRoles) + throw DiscordApiErrors.MAXIMUM_ROLES.withParams(maxRoles); + + const role = Role.create({ + // values before ...body are default and can be overriden + position: 0, + hoist: false, + color: 0, + mentionable: false, + ...body, + guild_id: guild_id, + managed: false, + permissions: String( + req.permission!.bitfield & BigInt(body.permissions || "0"), + ), + tags: undefined, + icon: undefined, + unicode_emoji: undefined, + }); + + await Promise.all([ + role.save(), emitEvent({ - event: "GUILD_ROLE_UPDATE", + event: "GUILD_ROLE_CREATE", guild_id, data: { guild_id, - role: x - } - } as GuildRoleUpdateEvent) - ) - ); - - res.json(roles); -}); + role: role, + }, + } as GuildRoleCreateEvent), + ]); + + res.json(role); + }, +); + +router.patch( + "/", + route({ body: "RolePositionUpdateSchema" }), + async (req: Request, res: Response) => { + const { guild_id } = req.params; + const body = req.body as RolePositionUpdateSchema; + + const perms = await getPermission(req.user_id, guild_id); + perms.hasThrow("MANAGE_ROLES"); + + 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 })), + }); + + await Promise.all( + roles.map((x) => + emitEvent({ + event: "GUILD_ROLE_UPDATE", + guild_id, + data: { + guild_id, + role: x, + }, + } as GuildRoleUpdateEvent), + ), + ); + + res.json(roles); + }, +); export default router; diff --git a/src/api/routes/guilds/#guild_id/stickers.ts b/src/api/routes/guilds/#guild_id/stickers.ts index fc0f49ab..3b1f5f8e 100644 --- a/src/api/routes/guilds/#guild_id/stickers.ts +++ b/src/api/routes/guilds/#guild_id/stickers.ts @@ -26,15 +26,18 @@ const bodyParser = multer({ limits: { fileSize: 1024 * 1024 * 100, fields: 10, - files: 1 + files: 1, }, - storage: multer.memoryStorage() + storage: multer.memoryStorage(), }).single("file"); router.post( "/", bodyParser, - route({ permission: "MANAGE_EMOJIS_AND_STICKERS", body: "ModifyGuildStickerSchema" }), + route({ + permission: "MANAGE_EMOJIS_AND_STICKERS", + body: "ModifyGuildStickerSchema", + }), async (req: Request, res: Response) => { if (!req.file) throw new HTTPError("missing file"); @@ -49,15 +52,15 @@ router.post( id, type: StickerType.GUILD, format_type: getStickerFormat(req.file.mimetype), - available: true + available: true, }).save(), - uploadFile(`/stickers/${id}`, req.file) + uploadFile(`/stickers/${id}`, req.file), ]); await sendStickerUpdateEvent(guild_id); res.json(sticker); - } + }, ); export function getStickerFormat(mime_type: string) { @@ -71,7 +74,9 @@ export function getStickerFormat(mime_type: string) { case "image/gif": return StickerFormatType.GIF; 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; 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( "/:sticker_id", - route({ body: "ModifyGuildStickerSchema", permission: "MANAGE_EMOJIS_AND_STICKERS" }), + route({ + body: "ModifyGuildStickerSchema", + permission: "MANAGE_EMOJIS_AND_STICKERS", + }), async (req: Request, res: Response) => { const { guild_id, sticker_id } = req.params; 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); return res.json(sticker); - } + }, ); async function sendStickerUpdateEvent(guild_id: string) { @@ -102,18 +116,22 @@ async function sendStickerUpdateEvent(guild_id: string) { guild_id: guild_id, data: { guild_id: guild_id, - stickers: await Sticker.find({ where: { guild_id: guild_id } }) - } + stickers: await Sticker.find({ where: { guild_id: guild_id } }), + }, } as GuildStickersUpdateEvent); } -router.delete("/:sticker_id", route({ permission: "MANAGE_EMOJIS_AND_STICKERS" }), async (req: Request, res: Response) => { - const { guild_id, sticker_id } = req.params; +router.delete( + "/:sticker_id", + route({ permission: "MANAGE_EMOJIS_AND_STICKERS" }), + async (req: Request, res: Response) => { + const { guild_id, sticker_id } = req.params; - await Sticker.delete({ guild_id, id: sticker_id }); - await sendStickerUpdateEvent(guild_id); + await Sticker.delete({ guild_id, id: sticker_id }); + await sendStickerUpdateEvent(guild_id); - return res.sendStatus(204); -}); + return res.sendStatus(204); + }, +); export default router; diff --git a/src/api/routes/guilds/#guild_id/templates.ts b/src/api/routes/guilds/#guild_id/templates.ts index 628321f5..3b5eddaa 100644 --- a/src/api/routes/guilds/#guild_id/templates.ts +++ b/src/api/routes/guilds/#guild_id/templates.ts @@ -20,63 +20,97 @@ const TemplateGuildProjection: (keyof Guild)[] = [ "afk_channel_id", "system_channel_id", "system_channel_flags", - "icon" + "icon", ]; router.get("/", route({}), async (req: Request, res: Response) => { const { guild_id } = req.params; - var templates = await Template.find({ where: { source_guild_id: guild_id } }); - - return res.json(templates); -}); - -router.post("/", route({ body: "TemplateCreateSchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { - const { guild_id } = req.params; - const guild = await Guild.findOneOrFail({ 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); - - const template = await Template.create({ - ...req.body, - code: generateCode(), - creator_id: req.user_id, - created_at: new Date(), - updated_at: new Date(), - source_guild_id: guild_id, - serialized_source_guild: guild - }).save(); - - res.json(template); -}); - -router.delete("/:code", route({ permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { - const { code, guild_id } = req.params; - - const template = await Template.delete({ - code, - source_guild_id: guild_id + var templates = await Template.find({ + where: { source_guild_id: guild_id }, }); - res.json(template); -}); - -router.put("/:code", route({ permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { - const { code, guild_id } = req.params; - const guild = await Guild.findOneOrFail({ where: { id: guild_id }, select: TemplateGuildProjection }); - - const template = await Template.create({ code, serialized_source_guild: guild }).save(); - - res.json(template); + return res.json(templates); }); -router.patch("/:code", route({ body: "TemplateModifySchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { - const { code, guild_id } = req.params; - const { name, description } = req.body; - - const template = await Template.create({ code, name: name, description: description, source_guild_id: guild_id }).save(); - - res.json(template); -}); +router.post( + "/", + route({ body: "TemplateCreateSchema", permission: "MANAGE_GUILD" }), + async (req: Request, res: Response) => { + const { guild_id } = req.params; + const guild = await Guild.findOneOrFail({ + 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); + + const template = await Template.create({ + ...req.body, + code: generateCode(), + creator_id: req.user_id, + created_at: new Date(), + updated_at: new Date(), + source_guild_id: guild_id, + serialized_source_guild: guild, + }).save(); + + res.json(template); + }, +); + +router.delete( + "/:code", + route({ permission: "MANAGE_GUILD" }), + async (req: Request, res: Response) => { + const { code, guild_id } = req.params; + + const template = await Template.delete({ + code, + source_guild_id: guild_id, + }); + + res.json(template); + }, +); + +router.put( + "/:code", + route({ permission: "MANAGE_GUILD" }), + async (req: Request, res: Response) => { + const { code, guild_id } = req.params; + const guild = await Guild.findOneOrFail({ + where: { id: guild_id }, + select: TemplateGuildProjection, + }); + + 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 { name, description } = req.body; + + const template = await Template.create({ + code, + name: name, + description: description, + source_guild_id: guild_id, + }).save(); + + res.json(template); + }, +); export default router; diff --git a/src/api/routes/guilds/#guild_id/vanity-url.ts b/src/api/routes/guilds/#guild_id/vanity-url.ts index d1fe4726..9a96b066 100644 --- a/src/api/routes/guilds/#guild_id/vanity-url.ts +++ b/src/api/routes/guilds/#guild_id/vanity-url.ts @@ -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 { route } from "@fosscord/api"; import { HTTPError } from "lambert-server"; @@ -7,52 +13,70 @@ const router = Router(); const InviteRegex = /\W/g; -router.get("/", route({ permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { - const { guild_id } = req.params; - const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); - - if (!guild.features.includes("ALIASABLE_NAMES")) { - const invite = await Invite.findOne({ where: { guild_id: guild_id, vanity_url: true } }); - if (!invite) return res.json({ code: null }); - - return res.json({ code: invite.code, uses: invite.uses }); - } else { - const invite = await Invite.find({ where: { guild_id: guild_id, vanity_url: true } }); - if (!invite || invite.length == 0) return res.json({ code: null }); - - 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) => { - const { guild_id } = req.params; - const body = req.body as VanityUrlSchema; - const code = body.code?.replace(InviteRegex, ""); - - 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 (!code || code.length === 0) throw new HTTPError("Code cannot be null or empty"); - - const invite = await Invite.findOne({ where: { code } }); - if (invite) throw new HTTPError("Invite already exists"); - - const { id } = await Channel.findOneOrFail({ where: { guild_id, type: ChannelType.GUILD_TEXT } }); - - await Invite.create({ - vanity_url: true, - code: code, - temporary: false, - uses: 0, - max_uses: 0, - max_age: 0, - created_at: new Date(), - expires_at: new Date(), - guild_id: guild_id, - channel_id: id - }).save(); - - return res.json({ code: code }); -}); +router.get( + "/", + route({ permission: "MANAGE_GUILD" }), + async (req: Request, res: Response) => { + const { guild_id } = req.params; + const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); + + if (!guild.features.includes("ALIASABLE_NAMES")) { + const invite = await Invite.findOne({ + where: { guild_id: guild_id, vanity_url: true }, + }); + if (!invite) return res.json({ code: null }); + + return res.json({ code: invite.code, uses: invite.uses }); + } else { + const invite = await Invite.find({ + where: { guild_id: guild_id, vanity_url: true }, + }); + if (!invite || invite.length == 0) return res.json({ code: null }); + + 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) => { + const { guild_id } = req.params; + const body = req.body as VanityUrlSchema; + const code = body.code?.replace(InviteRegex, ""); + + 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 (!code || code.length === 0) + throw new HTTPError("Code cannot be null or empty"); + + const invite = await Invite.findOne({ where: { code } }); + if (invite) throw new HTTPError("Invite already exists"); + + const { id } = await Channel.findOneOrFail({ + where: { guild_id, type: ChannelType.GUILD_TEXT }, + }); + + await Invite.create({ + vanity_url: true, + code: code, + temporary: false, + uses: 0, + max_uses: 0, + max_age: 0, + created_at: new Date(), + expires_at: new Date(), + guild_id: guild_id, + channel_id: id, + }).save(); + + return res.json({ code: code }); + }, +); export default router; diff --git a/src/api/routes/guilds/#guild_id/voice-states/#user_id/index.ts b/src/api/routes/guilds/#guild_id/voice-states/#user_id/index.ts index 006e997f..af03a07e 100644 --- a/src/api/routes/guilds/#guild_id/voice-states/#user_id/index.ts +++ b/src/api/routes/guilds/#guild_id/voice-states/#user_id/index.ts @@ -1,52 +1,71 @@ -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 { Request, Response, Router } from "express"; const router = Router(); //TODO need more testing when community guild and voice stage channel are working -router.patch("/", route({ body: "VoiceStateUpdateSchema" }), async (req: Request, res: Response) => { - const body = req.body as VoiceStateUpdateSchema; - var { guild_id, user_id } = req.params; - if (user_id === "@me") user_id = req.user_id; +router.patch( + "/", + route({ body: "VoiceStateUpdateSchema" }), + async (req: Request, res: Response) => { + const body = req.body as VoiceStateUpdateSchema; + var { guild_id, user_id } = req.params; + 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 You must have the MUTE_MEMBERS permission to unsuppress others. You can always suppress yourself. You must have the REQUEST_TO_SPEAK permission to request to speak. You can always clear your own request to speak. */ - if (body.suppress && user_id !== req.user_id) { - perms.hasThrow("MUTE_MEMBERS"); - } - if (!body.suppress) body.request_to_speak_timestamp = new Date(); - if (body.request_to_speak_timestamp) perms.hasThrow("REQUEST_TO_SPEAK"); - - const voice_state = await VoiceState.findOne({ - where: { - guild_id, - channel_id: body.channel_id, - user_id + if (body.suppress && user_id !== req.user_id) { + perms.hasThrow("MUTE_MEMBERS"); + } + if (!body.suppress) body.request_to_speak_timestamp = new Date(); + if (body.request_to_speak_timestamp) perms.hasThrow("REQUEST_TO_SPEAK"); + + const voice_state = await VoiceState.findOne({ + where: { + guild_id, + channel_id: body.channel_id, + user_id, + }, + }); + if (!voice_state) throw DiscordApiErrors.UNKNOWN_VOICE_STATE; + + voice_state.assign(body); + const channel = await Channel.findOneOrFail({ + where: { guild_id, id: body.channel_id }, + }); + if (channel.type !== ChannelType.GUILD_STAGE_VOICE) { + throw DiscordApiErrors.CANNOT_EXECUTE_ON_THIS_CHANNEL_TYPE; } - }); - if (!voice_state) throw DiscordApiErrors.UNKNOWN_VOICE_STATE; - - voice_state.assign(body); - const channel = await Channel.findOneOrFail({ where: { guild_id, id: body.channel_id } }); - if (channel.type !== ChannelType.GUILD_STAGE_VOICE) { - throw DiscordApiErrors.CANNOT_EXECUTE_ON_THIS_CHANNEL_TYPE; - } - - await Promise.all([ - voice_state.save(), - emitEvent({ - event: "VOICE_STATE_UPDATE", - data: voice_state, - guild_id - } as VoiceStateUpdateEvent) - ]); - return res.sendStatus(204); -}); + + await Promise.all([ + voice_state.save(), + emitEvent({ + event: "VOICE_STATE_UPDATE", + data: voice_state, + guild_id, + } as VoiceStateUpdateEvent), + ]); + return res.sendStatus(204); + }, +); export default router; diff --git a/src/api/routes/guilds/#guild_id/welcome-screen.ts b/src/api/routes/guilds/#guild_id/welcome-screen.ts index 57da062d..80ab138b 100644 --- a/src/api/routes/guilds/#guild_id/welcome-screen.ts +++ b/src/api/routes/guilds/#guild_id/welcome-screen.ts @@ -14,20 +14,30 @@ router.get("/", route({}), async (req: Request, res: Response) => { res.json(guild.welcome_screen); }); -router.patch("/", route({ body: "GuildUpdateWelcomeScreenSchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { - const guild_id = req.params.guild_id; - const body = req.body as GuildUpdateWelcomeScreenSchema; - - const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); - - if (!guild.welcome_screen.enabled) throw new HTTPError("Welcome screen disabled", 400); - 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; - - await guild.save(); - - res.sendStatus(204); -}); +router.patch( + "/", + route({ + body: "GuildUpdateWelcomeScreenSchema", + permission: "MANAGE_GUILD", + }), + async (req: Request, res: Response) => { + const guild_id = req.params.guild_id; + const body = req.body as GuildUpdateWelcomeScreenSchema; + + const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); + + if (!guild.welcome_screen.enabled) + throw new HTTPError("Welcome screen disabled", 400); + 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; + + await guild.save(); + + res.sendStatus(204); + }, +); export default router; diff --git a/src/api/routes/guilds/#guild_id/widget.json.ts b/src/api/routes/guilds/#guild_id/widget.json.ts index be5bf23f..2c3124a2 100644 --- a/src/api/routes/guilds/#guild_id/widget.json.ts +++ b/src/api/routes/guilds/#guild_id/widget.json.ts @@ -1,5 +1,12 @@ 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 { 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); // 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) { // 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 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 if ( 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({ id: doc.id, 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, channels: channels, members: members, - presence_count: guild.presence_count + presence_count: guild.presence_count, }; res.set("Cache-Control", "public, max-age=300"); diff --git a/src/api/routes/guilds/#guild_id/widget.png.ts b/src/api/routes/guilds/#guild_id/widget.png.ts index c17d511e..eaec8f07 100644 --- a/src/api/routes/guilds/#guild_id/widget.png.ts +++ b/src/api/routes/guilds/#guild_id/widget.png.ts @@ -24,8 +24,13 @@ router.get("/", route({}), async (req: Request, res: Response) => { // Fetch parameter const style = req.query.style?.toString() || "shield"; - if (!["shield", "banner1", "banner2", "banner3", "banner4"].includes(style)) { - throw new HTTPError("Value must be one of ('shield', 'banner1', 'banner2', 'banner3', 'banner4').", 400); + if ( + !["shield", "banner1", "banner2", "banner3", "banner4"].includes(style) + ) { + throw new HTTPError( + "Value must be one of ('shield', 'banner1', 'banner2', 'banner3', 'banner4').", + 400, + ); } // Setup canvas @@ -34,7 +39,17 @@ router.get("/", route({}), async (req: Request, res: Response) => { const sizeOf = require("image-size"); // 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)) { throw new HTTPError("Widget template does not exist.", 400); } @@ -50,30 +65,68 @@ router.get("/", route({}), async (req: Request, res: Response) => { switch (style) { case "shield": ctx.textAlign = "center"; - await drawText(ctx, 73, 13, "#FFFFFF", "thin 10px Verdana", presence); + await drawText( + ctx, + 73, + 13, + "#FFFFFF", + "thin 10px Verdana", + presence, + ); break; case "banner1": if (icon) await drawIcon(ctx, 20, 27, 50, icon); 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; case "banner2": if (icon) await drawIcon(ctx, 13, 19, 36, icon); 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; case "banner3": if (icon) await drawIcon(ctx, 20, 20, 50, icon); 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; case "banner4": if (icon) await drawIcon(ctx, 21, 136, 50, icon); 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; 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 @@ -83,7 +136,13 @@ router.get("/", route({}), async (req: Request, res: Response) => { 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 const img = new require("canvas").Image(); img.src = icon; @@ -101,10 +160,19 @@ async function drawIcon(canvas: any, x: number, y: number, scale: number, icon: 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.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); } diff --git a/src/api/routes/guilds/#guild_id/widget.ts b/src/api/routes/guilds/#guild_id/widget.ts index dbb4cc0c..108339e1 100644 --- a/src/api/routes/guilds/#guild_id/widget.ts +++ b/src/api/routes/guilds/#guild_id/widget.ts @@ -10,18 +10,31 @@ router.get("/", route({}), async (req: Request, res: Response) => { 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 -router.patch("/", route({ body: "WidgetModifySchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { - const body = req.body as WidgetModifySchema; - const { guild_id } = req.params; - - 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 - - return res.json(body); -}); +router.patch( + "/", + route({ body: "WidgetModifySchema", permission: "MANAGE_GUILD" }), + async (req: Request, res: Response) => { + const body = req.body as WidgetModifySchema; + const { guild_id } = req.params; + + 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 + + return res.json(body); + }, +); export default router; diff --git a/src/api/routes/guilds/index.ts b/src/api/routes/guilds/index.ts index 0807cb96..69575aea 100644 --- a/src/api/routes/guilds/index.ts +++ b/src/api/routes/guilds/index.ts @@ -1,32 +1,47 @@ 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"; const router: Router = Router(); //TODO: create default channel -router.post("/", route({ body: "GuildCreateSchema", right: "CREATE_GUILDS" }), async (req: Request, res: Response) => { - const body = req.body as GuildCreateSchema; - - const { maxGuilds } = Config.get().limits.user; - const guild_count = await Member.count({ where: { id: req.user_id } }); - const rights = await getRights(req.user_id); - if ((guild_count >= maxGuilds) && !rights.has("MANAGE_GUILDS")) { - throw DiscordApiErrors.MAXIMUM_GUILDS.withParams(maxGuilds); - } - - const guild = await Guild.createGuild({ ...body, owner_id: req.user_id }); - - const { autoJoin } = Config.get().guild; - if (autoJoin.enabled && !autoJoin.guilds?.length) { - // @ts-ignore - await Config.set({ guild: { autoJoin: { guilds: [guild.id] } } }); - } - - await Member.addToGuild(req.user_id, guild.id); - - res.status(201).json({ id: guild.id }); -}); +router.post( + "/", + route({ body: "GuildCreateSchema", right: "CREATE_GUILDS" }), + async (req: Request, res: Response) => { + const body = req.body as GuildCreateSchema; + + const { maxGuilds } = Config.get().limits.user; + const guild_count = await Member.count({ where: { id: req.user_id } }); + const rights = await getRights(req.user_id); + if (guild_count >= maxGuilds && !rights.has("MANAGE_GUILDS")) { + throw DiscordApiErrors.MAXIMUM_GUILDS.withParams(maxGuilds); + } + + const guild = await Guild.createGuild({ + ...body, + owner_id: req.user_id, + }); + + const { autoJoin } = Config.get().guild; + if (autoJoin.enabled && !autoJoin.guilds?.length) { + // @ts-ignore + await Config.set({ guild: { autoJoin: { guilds: [guild.id] } } }); + } + + await Member.addToGuild(req.user_id, guild.id); + + res.status(201).json({ id: guild.id }); + }, +); export default router; diff --git a/src/api/routes/guilds/templates/index.ts b/src/api/routes/guilds/templates/index.ts index 4e7abcc5..240bf074 100644 --- a/src/api/routes/guilds/templates/index.ts +++ b/src/api/routes/guilds/templates/index.ts @@ -1,29 +1,58 @@ 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 { DiscordApiErrors } from "@fosscord/util"; import fetch from "node-fetch"; const router: Router = Router(); router.get("/:code", route({}), async (req: Request, res: Response) => { - const { allowDiscordTemplates, allowRaws, enabled } = Config.get().templates; - if (!enabled) res.json({ code: 403, message: "Template creation & usage is disabled on this instance." }).sendStatus(403); + const { allowDiscordTemplates, allowRaws, enabled } = + Config.get().templates; + if (!enabled) + res.json({ + code: 403, + message: "Template creation & usage is disabled on this instance.", + }).sendStatus(403); const { code } = req.params; 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 discordTemplateData = await fetch(`https://discord.com/api/v9/guilds/templates/${discordTemplateID}`, { - method: "get", - headers: { "Content-Type": "application/json" } - }); + const discordTemplateData = await fetch( + `https://discord.com/api/v9/guilds/templates/${discordTemplateID}`, + { + method: "get", + headers: { "Content-Type": "application/json" }, + }, + ); return res.json(await discordTemplateData.json()); } 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]); } @@ -32,48 +61,72 @@ router.get("/:code", route({}), async (req: Request, res: Response) => { res.json(template); }); -router.post("/:code", route({ body: "GuildTemplateCreateSchema" }), 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 body = req.body as GuildTemplateCreateSchema; - - const { maxGuilds } = Config.get().limits.user; - - const guild_count = await Member.count({ where: { id: req.user_id } }); - if (guild_count >= maxGuilds) { - throw DiscordApiErrors.MAXIMUM_GUILDS.withParams(maxGuilds); - } - - const template = await Template.findOneOrFail({ where: { code: code } }); +router.post( + "/:code", + route({ body: "GuildTemplateCreateSchema" }), + 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 body = req.body as GuildTemplateCreateSchema; + + const { maxGuilds } = Config.get().limits.user; + + const guild_count = await Member.count({ where: { id: req.user_id } }); + if (guild_count >= maxGuilds) { + throw DiscordApiErrors.MAXIMUM_GUILDS.withParams(maxGuilds); + } + + const template = await Template.findOneOrFail({ + where: { code: code }, + }); - const guild_id = Snowflake.generate(); - - const [guild, role] = await Promise.all([ - Guild.create({ - ...body, - ...template.serialized_source_guild, - id: guild_id, - owner_id: req.user_id - }).save(), - Role.create({ - id: guild_id, - guild_id: guild_id, - color: 0, - hoist: false, - managed: true, - mentionable: true, - name: "@everyone", - permissions: BigInt("2251804225").toString(), // TODO: where did this come from? - position: 0, - }).save() - ]); - - await Member.addToGuild(req.user_id, guild_id); - - res.status(201).json({ id: guild.id }); -}); + const guild_id = Snowflake.generate(); + + const [guild, role] = await Promise.all([ + Guild.create({ + ...body, + ...template.serialized_source_guild, + id: guild_id, + owner_id: req.user_id, + }).save(), + Role.create({ + id: guild_id, + guild_id: guild_id, + color: 0, + hoist: false, + managed: true, + mentionable: true, + name: "@everyone", + permissions: BigInt("2251804225").toString(), // TODO: where did this come from? + position: 0, + }).save(), + ]); + + await Member.addToGuild(req.user_id, guild_id); + + res.status(201).json({ id: guild.id }); + }, +); export default router; diff --git a/src/api/routes/invites/index.ts b/src/api/routes/invites/index.ts index c268085f..ce0ba982 100644 --- a/src/api/routes/invites/index.ts +++ b/src/api/routes/invites/index.ts @@ -1,5 +1,13 @@ 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 { HTTPError } from "lambert-server"; @@ -8,24 +16,45 @@ const router: Router = Router(); router.get("/:code", route({}), async (req: Request, res: Response) => { 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); }); -router.post("/:code", route({ right: "USE_MASS_INVITES" }), async (req: Request, res: Response) => { - const { code } = req.params; - const { guild_id } = await Invite.findOneOrFail({ where: { code: code } }); - const { features } = await Guild.findOneOrFail({ where: { id: guild_id } }); - const { public_flags } = await User.findOneOrFail({ where: { id: req.user_id } }); +router.post( + "/:code", + route({ right: "USE_MASS_INVITES" }), + async (req: Request, res: Response) => { + const { code } = req.params; + const { guild_id } = await Invite.findOneOrFail({ + where: { code: code }, + }); + 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 (features.includes("INVITES_CLOSED")) throw new HTTPError("Sorry, this guild has joins closed.", 403); + if ( + 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 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); 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([ Invite.delete({ code }), @@ -46,9 +78,9 @@ router.delete("/:code", route({}), async (req: Request, res: Response) => { data: { channel_id: channel_id, guild_id: guild_id, - code: code - } - } as InviteDeleteEvent) + code: code, + }, + } as InviteDeleteEvent), ]); res.json({ invite: invite }); diff --git a/src/api/routes/partners/#guild_id/requirements.ts b/src/api/routes/partners/#guild_id/requirements.ts index 545c5c78..7e63c06b 100644 --- a/src/api/routes/partners/#guild_id/requirements.ts +++ b/src/api/routes/partners/#guild_id/requirements.ts @@ -1,4 +1,3 @@ - import { Guild, Config } from "@fosscord/util"; import { Router, Request, Response } from "express"; @@ -7,33 +6,33 @@ import { route } from "@fosscord/api"; const router = Router(); router.get("/", route({}), async (req: Request, res: Response) => { - const { guild_id } = req.params; - // TODO: - // Load from database - // Admin control, but for now it allows anyone to be discoverable + const { guild_id } = req.params; + // TODO: + // Load from database + // Admin control, but for now it allows anyone to be discoverable res.send({ guild_id: guild_id, safe_environment: true, - healthy: true, - health_score_pending: false, - size: true, - nsfw_properties: {}, - protected: true, - sufficient: true, - sufficient_without_grace_period: true, - valid_rules_channel: true, - retention_healthy: true, - engagement_healthy: true, - age: true, - minimum_age: 0, - health_score: { - avg_nonnew_participators: 0, - avg_nonnew_communicators: 0, - num_intentful_joiners: 0, - perc_ret_w1_intentful: 0 - }, - minimum_size: 0 + healthy: true, + health_score_pending: false, + size: true, + nsfw_properties: {}, + protected: true, + sufficient: true, + sufficient_without_grace_period: true, + valid_rules_channel: true, + retention_healthy: true, + engagement_healthy: true, + age: true, + minimum_age: 0, + health_score: { + avg_nonnew_participators: 0, + avg_nonnew_communicators: 0, + num_intentful_joiners: 0, + perc_ret_w1_intentful: 0, + }, + minimum_size: 0, }); }); diff --git a/src/api/routes/policies/instance/domains.ts b/src/api/routes/policies/instance/domains.ts index 20cd07ba..f22eac17 100644 --- a/src/api/routes/policies/instance/domains.ts +++ b/src/api/routes/policies/instance/domains.ts @@ -1,16 +1,19 @@ import { Router, Request, Response } from "express"; import { route } from "@fosscord/api"; import { Config } from "@fosscord/util"; -import { config } from "dotenv" +import { config } from "dotenv"; const router = Router(); -router.get("/",route({}), async (req: Request, res: Response) => { - const { cdn, gateway } = Config.get(); - - const IdentityForm = { - cdn: cdn.endpointPublic || process.env.CDN || "http://localhost:3001", - gateway: gateway.endpointPublic || process.env.GATEWAY || "ws://localhost:3002" - }; +router.get("/", route({}), async (req: Request, res: Response) => { + const { cdn, gateway } = Config.get(); + + const IdentityForm = { + cdn: cdn.endpointPublic || process.env.CDN || "http://localhost:3001", + gateway: + gateway.endpointPublic || + process.env.GATEWAY || + "ws://localhost:3002", + }; res.json(IdentityForm); }); diff --git a/src/api/routes/policies/instance/index.ts b/src/api/routes/policies/instance/index.ts index e3da014f..1c1afa09 100644 --- a/src/api/routes/policies/instance/index.ts +++ b/src/api/routes/policies/instance/index.ts @@ -3,8 +3,7 @@ import { route } from "@fosscord/api"; import { Config } from "@fosscord/util"; const router = Router(); - -router.get("/",route({}), async (req: Request, res: Response) => { +router.get("/", route({}), async (req: Request, res: Response) => { const { general } = Config.get(); res.json(general); }); diff --git a/src/api/routes/policies/instance/limits.ts b/src/api/routes/policies/instance/limits.ts index 7de1476b..06f14f83 100644 --- a/src/api/routes/policies/instance/limits.ts +++ b/src/api/routes/policies/instance/limits.ts @@ -3,7 +3,7 @@ import { route } from "@fosscord/api"; import { Config } from "@fosscord/util"; const router = Router(); -router.get("/",route({}), async (req: Request, res: Response) => { +router.get("/", route({}), async (req: Request, res: Response) => { const { limits } = Config.get(); res.json(limits); }); diff --git a/src/api/routes/scheduled-maintenances/upcoming_json.ts b/src/api/routes/scheduled-maintenances/upcoming_json.ts index 83092e44..e42723a1 100644 --- a/src/api/routes/scheduled-maintenances/upcoming_json.ts +++ b/src/api/routes/scheduled-maintenances/upcoming_json.ts @@ -2,11 +2,15 @@ import { Router, Request, Response } from "express"; import { route } from "@fosscord/api"; const router = Router(); -router.get("/scheduled-maintenances/upcoming.json",route({}), async (req: Request, res: Response) => { - res.json({ - "page": {}, - "scheduled_maintenances": {} - }); -}); +router.get( + "/scheduled-maintenances/upcoming.json", + route({}), + async (req: Request, res: Response) => { + res.json({ + page: {}, + scheduled_maintenances: {}, + }); + }, +); export default router; diff --git a/src/api/routes/stop.ts b/src/api/routes/stop.ts index 7f8b78ba..78abb9d7 100644 --- a/src/api/routes/stop.ts +++ b/src/api/routes/stop.ts @@ -6,17 +6,19 @@ const router: Router = Router(); router.post("/", route({}), async (req: Request, res: Response) => { //EXPERIMENTAL: have an "OPERATOR" platform permission implemented for this API route - const user = await User.findOneOrFail({ where: { id: req.user_id }, select: ["rights"] }); - if((Number(user.rights) << Number(0))%Number(2)==Number(1)) { + const user = await User.findOneOrFail({ + where: { id: req.user_id }, + select: ["rights"], + }); + if ((Number(user.rights) << Number(0)) % Number(2) == Number(1)) { console.log("user that POSTed to the API was ALLOWED"); console.log(user.rights); - res.sendStatus(200) - process.kill(process.pid, 'SIGTERM') - } - else { + res.sendStatus(200); + process.kill(process.pid, "SIGTERM"); + } else { console.log("operation failed"); console.log(user.rights); - res.sendStatus(403) + res.sendStatus(403); } }); diff --git a/src/api/routes/store/published-listings/applications.ts b/src/api/routes/store/published-listings/applications.ts index 060a4c3d..6156f43e 100644 --- a/src/api/routes/store/published-listings/applications.ts +++ b/src/api/routes/store/published-listings/applications.ts @@ -41,29 +41,29 @@ router.get("/:id", route({}), async (req: Request, res: Response) => { publishers: [ { id: "", - name: "" - } + name: "", + }, ], developers: [ { id: "", - name: "" - } + name: "", + }, ], system_requirements: {}, show_age_gate: false, price: { amount: 0, - currency: "EUR" + currency: "EUR", }, - locales: [] + locales: [], }, tagline: "", description: "", carousel_items: [ { - asset_id: "" - } + asset_id: "", + }, ], header_logo_dark_theme: {}, //{id: "", size: 4665, mime_type: "image/gif", width 160, height: 160} header_logo_light_theme: {}, @@ -71,8 +71,8 @@ router.get("/:id", route({}), async (req: Request, res: Response) => { thumbnail: {}, header_background: {}, hero_background: {}, - assets: [] - } + assets: [], + }, }).status(200); }); diff --git a/src/api/routes/store/published-listings/applications/#id/subscription-plans.ts b/src/api/routes/store/published-listings/applications/#id/subscription-plans.ts index 54151ae5..845cdfe7 100644 --- a/src/api/routes/store/published-listings/applications/#id/subscription-plans.ts +++ b/src/api/routes/store/published-listings/applications/#id/subscription-plans.ts @@ -17,8 +17,8 @@ router.get("/", route({}), async (req: Request, res: Response) => { fallback_currency: "eur", currency: "eur", price: 4199, - price_tier: null - } + price_tier: null, + }, ]).status(200); }); diff --git a/src/api/routes/store/published-listings/skus.ts b/src/api/routes/store/published-listings/skus.ts index 060a4c3d..6156f43e 100644 --- a/src/api/routes/store/published-listings/skus.ts +++ b/src/api/routes/store/published-listings/skus.ts @@ -41,29 +41,29 @@ router.get("/:id", route({}), async (req: Request, res: Response) => { publishers: [ { id: "", - name: "" - } + name: "", + }, ], developers: [ { id: "", - name: "" - } + name: "", + }, ], system_requirements: {}, show_age_gate: false, price: { amount: 0, - currency: "EUR" + currency: "EUR", }, - locales: [] + locales: [], }, tagline: "", description: "", carousel_items: [ { - asset_id: "" - } + asset_id: "", + }, ], header_logo_dark_theme: {}, //{id: "", size: 4665, mime_type: "image/gif", width 160, height: 160} header_logo_light_theme: {}, @@ -71,8 +71,8 @@ router.get("/:id", route({}), async (req: Request, res: Response) => { thumbnail: {}, header_background: {}, hero_background: {}, - assets: [] - } + assets: [], + }, }).status(200); }); diff --git a/src/api/routes/store/published-listings/skus/#sku_id/subscription-plans.ts b/src/api/routes/store/published-listings/skus/#sku_id/subscription-plans.ts index 03162ec8..33151056 100644 --- a/src/api/routes/store/published-listings/skus/#sku_id/subscription-plans.ts +++ b/src/api/routes/store/published-listings/skus/#sku_id/subscription-plans.ts @@ -17,8 +17,8 @@ const skus = new Map([ currency: "usd", price: 0, price_tier: null, - } - ] + }, + ], ], [ "521842865731534868", @@ -32,7 +32,7 @@ const skus = new Map([ sku_id: "521842865731534868", currency: "usd", price: 0, - price_tier: null + price_tier: null, }, { id: "511651860671627264", @@ -43,9 +43,9 @@ const skus = new Map([ sku_id: "521842865731534868", currency: "usd", price: 0, - price_tier: null - } - ] + price_tier: null, + }, + ], ], [ "521846918637420545", @@ -59,7 +59,7 @@ const skus = new Map([ sku_id: "521846918637420545", currency: "usd", price: 0, - price_tier: null + price_tier: null, }, { id: "511651876987469824", @@ -70,9 +70,9 @@ const skus = new Map([ sku_id: "521846918637420545", currency: "usd", price: 0, - price_tier: null - } - ] + price_tier: null, + }, + ], ], [ "521847234246082599", @@ -86,7 +86,7 @@ const skus = new Map([ sku_id: "521847234246082599", currency: "usd", price: 0, - price_tier: null + price_tier: null, }, { id: "511651880837840896", @@ -97,7 +97,7 @@ const skus = new Map([ sku_id: "521847234246082599", currency: "usd", price: 0, - price_tier: null + price_tier: null, }, { id: "511651885459963904", @@ -108,9 +108,9 @@ const skus = new Map([ sku_id: "521847234246082599", currency: "usd", price: 0, - price_tier: null - } - ] + price_tier: null, + }, + ], ], [ "590663762298667008", @@ -125,7 +125,7 @@ const skus = new Map([ discount_price: 0, currency: "usd", price: 0, - price_tier: null + price_tier: null, }, { id: "590665538238152709", @@ -137,10 +137,10 @@ const skus = new Map([ discount_price: 0, currency: "usd", price: 0, - price_tier: null - } - ] - ] + price_tier: null, + }, + ], + ], ]); router.get("/", route({}), async (req: Request, res: Response) => { diff --git a/src/api/routes/updates.ts b/src/api/routes/updates.ts index 42f77323..8fe6fc2a 100644 --- a/src/api/routes/updates.ts +++ b/src/api/routes/updates.ts @@ -7,13 +7,15 @@ const router = Router(); router.get("/", route({}), async (req: Request, res: Response) => { const { client } = Config.get(); - const release = await Release.findOneOrFail({ where: { name: client.releases.upstreamVersion } }); + const release = await Release.findOneOrFail({ + where: { name: client.releases.upstreamVersion }, + }); res.json({ name: release.name, pub_date: release.pub_date, url: release.url, - notes: release.notes + notes: release.notes, }); }); diff --git a/src/api/routes/users/#id/profile.ts b/src/api/routes/users/#id/profile.ts index de96422a..ebea805b 100644 --- a/src/api/routes/users/#id/profile.ts +++ b/src/api/routes/users/#id/profile.ts @@ -1,5 +1,12 @@ import { Router, Request, Response } from "express"; -import { PublicConnectedAccount, PublicUser, User, UserPublic, Member, Guild } from "@fosscord/util"; +import { + PublicConnectedAccount, + PublicUser, + User, + UserPublic, + Member, + Guild, +} from "@fosscord/util"; import { route } from "@fosscord/api"; const router: Router = Router(); @@ -11,81 +18,102 @@ export interface UserProfileResponse { premium_since?: Date; } -router.get("/", route({ test: { response: { body: "UserProfileResponse" } } }), async (req: Request, res: Response) => { - if (req.params.id === "@me") req.params.id = req.user_id; +router.get( + "/", + route({ test: { response: { body: "UserProfileResponse" } } }), + async (req: Request, res: Response) => { + if (req.params.id === "@me") req.params.id = req.user_id; - const { guild_id, with_mutual_guilds } = req.query; + const { guild_id, with_mutual_guilds } = req.query; - const user = await User.getPublicUser(req.params.id, { relations: ["connected_accounts"] }); + const user = await User.getPublicUser(req.params.id, { + relations: ["connected_accounts"], + }); - var mutual_guilds: object[] = []; - var premium_guild_since; + var mutual_guilds: object[] = []; + var premium_guild_since; - if (with_mutual_guilds == "true") { - const requested_member = await Member.find({ where: { id: req.params.id } }); - const self_member = await Member.find({ where: { id: req.user_id } }); + if (with_mutual_guilds == "true") { + const requested_member = await Member.find({ + where: { id: req.params.id }, + }); + const self_member = await Member.find({ + where: { id: req.user_id }, + }); - for (const rmem of requested_member) { - if (rmem.premium_since) { - if (premium_guild_since) { - if (premium_guild_since > rmem.premium_since) { + for (const rmem of requested_member) { + if (rmem.premium_since) { + if (premium_guild_since) { + if (premium_guild_since > rmem.premium_since) { + premium_guild_since = rmem.premium_since; + } + } else { premium_guild_since = rmem.premium_since; } - } else { - premium_guild_since = rmem.premium_since; } - } - for (const smem of self_member) { - if (smem.guild_id === rmem.guild_id) { - mutual_guilds.push({ id: rmem.guild_id, nick: rmem.nick }); + for (const smem of self_member) { + if (smem.guild_id === rmem.guild_id) { + mutual_guilds.push({ + id: rmem.guild_id, + nick: rmem.nick, + }); + } } } } - } - const guild_member = guild_id && typeof guild_id == "string" - ? await Member.findOneOrFail({ where: { id: req.params.id, guild_id: guild_id }, relations: ["roles"] }) - : undefined; + const guild_member = + guild_id && typeof guild_id == "string" + ? await Member.findOneOrFail({ + where: { id: req.params.id, guild_id: guild_id }, + relations: ["roles"], + }) + : undefined; - // TODO: make proper DTO's in util? + // TODO: make proper DTO's in util? - const userDto = { - username: user.username, - discriminator: user.discriminator, - id: user.id, - public_flags: user.public_flags, - avatar: user.avatar, - accent_color: user.accent_color, - banner: user.banner, - bio: req.user_bot ? null : user.bio, - bot: user.bot - }; + const userDto = { + username: user.username, + discriminator: user.discriminator, + id: user.id, + public_flags: user.public_flags, + avatar: user.avatar, + accent_color: user.accent_color, + banner: user.banner, + bio: req.user_bot ? null : user.bio, + bot: user.bot, + }; - const guildMemberDto = guild_member ? { - avatar: user.avatar, // TODO - banner: user.banner, // TODO - bio: req.user_bot ? null : user.bio, // TODO - communication_disabled_until: null, // TODO - deaf: guild_member.deaf, - flags: user.flags, - is_pending: guild_member.pending, - pending: guild_member.pending, // why is this here twice, discord? - joined_at: guild_member.joined_at, - mute: guild_member.mute, - nick: guild_member.nick, - premium_since: guild_member.premium_since, - roles: guild_member.roles.map(x => x.id).filter(id => id != guild_id), - user: userDto - } : undefined; + const guildMemberDto = guild_member + ? { + avatar: user.avatar, // TODO + banner: user.banner, // TODO + bio: req.user_bot ? null : user.bio, // TODO + communication_disabled_until: null, // TODO + deaf: guild_member.deaf, + flags: user.flags, + is_pending: guild_member.pending, + pending: guild_member.pending, // why is this here twice, discord? + joined_at: guild_member.joined_at, + mute: guild_member.mute, + nick: guild_member.nick, + premium_since: guild_member.premium_since, + roles: guild_member.roles + .map((x) => x.id) + .filter((id) => id != guild_id), + user: userDto, + } + : undefined; - res.json({ - connected_accounts: user.connected_accounts, - premium_guild_since: premium_guild_since, // TODO - premium_since: user.premium_since, // TODO - mutual_guilds: mutual_guilds, // TODO {id: "", nick: null} when ?with_mutual_guilds=true - user: userDto, - guild_member: guildMemberDto, - }); -}); + res.json({ + connected_accounts: user.connected_accounts, + premium_guild_since: premium_guild_since, // TODO + premium_since: user.premium_since, // TODO + mutual_guilds: mutual_guilds, // TODO {id: "", nick: null} when ?with_mutual_guilds=true + user: userDto, + guild_member: guildMemberDto, + }); + }, +); export default router; diff --git a/src/api/routes/users/#id/relationships.ts b/src/api/routes/users/#id/relationships.ts index de7cb9d3..c6480567 100644 --- a/src/api/routes/users/#id/relationships.ts +++ b/src/api/routes/users/#id/relationships.ts @@ -6,36 +6,49 @@ const router: Router = Router(); export interface UserRelationsResponse { object: { - id?: string, - username?: string, - avatar?: string, - discriminator?: string, - public_flags?: number - } + id?: string; + username?: string; + avatar?: string; + discriminator?: string; + public_flags?: number; + }; } +router.get( + "/", + route({ test: { response: { body: "UserRelationsResponse" } } }), + async (req: Request, res: Response) => { + var mutual_relations: object[] = []; + const requested_relations = await User.findOneOrFail({ + where: { id: req.params.id }, + relations: ["relationships"], + }); + const self_relations = await User.findOneOrFail({ + where: { id: req.user_id }, + relations: ["relationships"], + }); -router.get("/", route({ test: { response: { body: "UserRelationsResponse" } } }), async (req: Request, res: Response) => { - var mutual_relations: object[] = []; - const requested_relations = await User.findOneOrFail({ - where: { id: req.params.id }, - relations: ["relationships"] - }); - const self_relations = await User.findOneOrFail({ - where: { id: req.user_id }, - relations: ["relationships"] - }); - - for(const rmem of requested_relations.relationships) { - for(const smem of self_relations.relationships) - if (rmem.to_id === smem.to_id && rmem.type === 1 && rmem.to_id !== req.user_id) { - var relation_user = await User.getPublicUser(rmem.to_id) + for (const rmem of requested_relations.relationships) { + for (const smem of self_relations.relationships) + if ( + rmem.to_id === smem.to_id && + rmem.type === 1 && + rmem.to_id !== req.user_id + ) { + var relation_user = await User.getPublicUser(rmem.to_id); - mutual_relations.push({id: relation_user.id, username: relation_user.username, avatar: relation_user.avatar, discriminator: relation_user.discriminator, public_flags: relation_user.public_flags}) + mutual_relations.push({ + id: relation_user.id, + username: relation_user.username, + avatar: relation_user.avatar, + discriminator: relation_user.discriminator, + public_flags: relation_user.public_flags, + }); + } } - } - res.json(mutual_relations) -}); + res.json(mutual_relations); + }, +); export default router; diff --git a/src/api/routes/users/@me/channels.ts b/src/api/routes/users/@me/channels.ts index ad483529..237be102 100644 --- a/src/api/routes/users/@me/channels.ts +++ b/src/api/routes/users/@me/channels.ts @@ -1,5 +1,10 @@ import { Request, Response, Router } from "express"; -import { Recipient, DmChannelDTO, Channel, DmChannelCreateSchema } from "@fosscord/util"; +import { + Recipient, + DmChannelDTO, + Channel, + DmChannelCreateSchema, +} from "@fosscord/util"; import { route } from "@fosscord/api"; const router: Router = Router(); @@ -7,14 +12,28 @@ const router: Router = Router(); router.get("/", route({}), async (req: Request, res: Response) => { const recipients = await Recipient.find({ where: { user_id: req.user_id, closed: false }, - relations: ["channel", "channel.recipients"] + relations: ["channel", "channel.recipients"], }); - res.json(await Promise.all(recipients.map((r) => DmChannelDTO.from(r.channel, [req.user_id])))); + res.json( + await Promise.all( + recipients.map((r) => DmChannelDTO.from(r.channel, [req.user_id])), + ), + ); }); -router.post("/", route({ body: "DmChannelCreateSchema" }), async (req: Request, res: Response) => { - const body = req.body as DmChannelCreateSchema; - res.json(await Channel.createDMChannel(body.recipients, req.user_id, body.name)); -}); +router.post( + "/", + route({ body: "DmChannelCreateSchema" }), + async (req: Request, res: Response) => { + const body = req.body as DmChannelCreateSchema; + res.json( + await Channel.createDMChannel( + body.recipients, + req.user_id, + body.name, + ), + ); + }, +); export default router; diff --git a/src/api/routes/users/@me/delete.ts b/src/api/routes/users/@me/delete.ts index c24c3f1e..a9f8167c 100644 --- a/src/api/routes/users/@me/delete.ts +++ b/src/api/routes/users/@me/delete.ts @@ -7,7 +7,10 @@ import { HTTPError } from "lambert-server"; const router = Router(); router.post("/", route({}), async (req: Request, res: Response) => { - const user = await User.findOneOrFail({ where: { id: req.user_id }, select: ["data"] }); //User object + const user = await User.findOneOrFail({ + where: { id: req.user_id }, + select: ["data"], + }); //User object let correctpass = true; if (user.data.hash) { @@ -21,7 +24,10 @@ router.post("/", route({}), async (req: Request, res: Response) => { // TODO: decrement guild member count if (correctpass) { - await Promise.all([User.delete({ id: req.user_id }), Member.delete({ id: req.user_id })]); + await Promise.all([ + User.delete({ id: req.user_id }), + Member.delete({ id: req.user_id }), + ]); res.sendStatus(204); } else { diff --git a/src/api/routes/users/@me/disable.ts b/src/api/routes/users/@me/disable.ts index 4aff3774..313a888f 100644 --- a/src/api/routes/users/@me/disable.ts +++ b/src/api/routes/users/@me/disable.ts @@ -6,7 +6,10 @@ import bcrypt from "bcrypt"; const router = Router(); router.post("/", route({}), async (req: Request, res: Response) => { - const user = await User.findOneOrFail({ where: { id: req.user_id }, select: ["data"] }); //User object + const user = await User.findOneOrFail({ + where: { id: req.user_id }, + select: ["data"], + }); //User object let correctpass = true; if (user.data.hash) { @@ -19,7 +22,10 @@ router.post("/", route({}), async (req: Request, res: Response) => { res.sendStatus(204); } else { - res.status(400).json({ message: "Password does not match", code: 50018 }); + res.status(400).json({ + message: "Password does not match", + code: 50018, + }); } }); diff --git a/src/api/routes/users/@me/email-settings.ts b/src/api/routes/users/@me/email-settings.ts index 3114984e..a2834b89 100644 --- a/src/api/routes/users/@me/email-settings.ts +++ b/src/api/routes/users/@me/email-settings.ts @@ -11,9 +11,9 @@ router.get("/", route({}), (req: Request, res: Response) => { communication: true, tips: false, updates_and_announcements: false, - recommendations_and_events: false + recommendations_and_events: false, }, - initialized: false + initialized: false, }).status(200); }); diff --git a/src/api/routes/users/@me/guilds.ts b/src/api/routes/users/@me/guilds.ts index 754a240e..e12bf258 100644 --- a/src/api/routes/users/@me/guilds.ts +++ b/src/api/routes/users/@me/guilds.ts @@ -1,12 +1,23 @@ import { Router, Request, Response } from "express"; -import { Guild, Member, User, GuildDeleteEvent, GuildMemberRemoveEvent, emitEvent, Config } from "@fosscord/util"; +import { + Guild, + Member, + User, + GuildDeleteEvent, + GuildMemberRemoveEvent, + emitEvent, + Config, +} from "@fosscord/util"; import { HTTPError } from "lambert-server"; import { route } from "@fosscord/api"; const router: Router = Router(); router.get("/", route({}), async (req: Request, res: Response) => { - const members = await Member.find({ relations: ["guild"], where: { id: req.user_id } }); + const members = await Member.find({ + relations: ["guild"], + where: { id: req.user_id }, + }); let guild = members.map((x) => x.guild); @@ -21,11 +32,19 @@ router.get("/", route({}), async (req: Request, res: Response) => { router.delete("/:guild_id", route({}), async (req: Request, res: Response) => { const { autoJoin } = Config.get().guild; const { guild_id } = req.params; - const guild = await Guild.findOneOrFail({ where: { id: guild_id }, select: ["owner_id"] }); + const guild = await Guild.findOneOrFail({ + where: { id: guild_id }, + select: ["owner_id"], + }); if (!guild) throw new HTTPError("Guild doesn't exist", 404); - if (guild.owner_id === req.user_id) throw new HTTPError("You can't leave your own guild", 400); - if (autoJoin.enabled && autoJoin.guilds.includes(guild_id) && !autoJoin.canLeave) { + if (guild.owner_id === req.user_id) + throw new HTTPError("You can't leave your own guild", 400); + if ( + autoJoin.enabled && + autoJoin.guilds.includes(guild_id) && + !autoJoin.canLeave + ) { throw new HTTPError("You can't leave instance auto join guilds", 400); } @@ -34,10 +53,10 @@ router.delete("/:guild_id", route({}), async (req: Request, res: Response) => { emitEvent({ event: "GUILD_DELETE", data: { - id: guild_id + id: guild_id, }, - user_id: req.user_id - } as GuildDeleteEvent) + user_id: req.user_id, + } as GuildDeleteEvent), ]); const user = await User.getPublicUser(req.user_id); @@ -46,9 +65,9 @@ router.delete("/:guild_id", route({}), async (req: Request, res: Response) => { event: "GUILD_MEMBER_REMOVE", data: { guild_id: guild_id, - user: user + user: user, }, - guild_id: guild_id + guild_id: guild_id, } as GuildMemberRemoveEvent); return res.sendStatus(204); diff --git a/src/api/routes/users/@me/guilds/#guild_id/settings.ts b/src/api/routes/users/@me/guilds/#guild_id/settings.ts index f09be25b..4b806cfb 100644 --- a/src/api/routes/users/@me/guilds/#guild_id/settings.ts +++ b/src/api/routes/users/@me/guilds/#guild_id/settings.ts @@ -1,39 +1,51 @@ import { Router, Response, Request } from "express"; -import { Channel, ChannelOverride, Member, UserGuildSettings } from "@fosscord/util"; +import { + Channel, + ChannelOverride, + Member, + UserGuildSettings, +} from "@fosscord/util"; import { route } from "@fosscord/api"; const router = Router(); // This sucks. I would use a DeepPartial, my own or typeorms, but they both generate inncorect schema -export interface UserGuildSettingsSchema extends Partial<Omit<UserGuildSettings, 'channel_overrides'>> { +export interface UserGuildSettingsSchema + extends Partial<Omit<UserGuildSettings, "channel_overrides">> { channel_overrides: { [channel_id: string]: Partial<ChannelOverride>; - }, + }; } // GET doesn't exist on discord.com router.get("/", route({}), async (req: Request, res: Response) => { const user = await Member.findOneOrFail({ where: { id: req.user_id, guild_id: req.params.guild_id }, - select: ["settings"] + select: ["settings"], }); return res.json(user.settings); }); -router.patch("/", route({ body: "UserGuildSettingsSchema" }), async (req: Request, res: Response) => { - const body = req.body as UserGuildSettings; +router.patch( + "/", + route({ body: "UserGuildSettingsSchema" }), + async (req: Request, res: Response) => { + const body = req.body as UserGuildSettings; - if (body.channel_overrides) { - for (var channel in body.channel_overrides) { - Channel.findOneOrFail({ where: { id: channel } }); + if (body.channel_overrides) { + for (var channel in body.channel_overrides) { + Channel.findOneOrFail({ where: { id: channel } }); + } } - } - const user = await Member.findOneOrFail({ where: { id: req.user_id, guild_id: req.params.guild_id } }); - user.settings = { ...user.settings, ...body }; - await user.save(); + const user = await Member.findOneOrFail({ + where: { id: req.user_id, guild_id: req.params.guild_id }, + }); + user.settings = { ...user.settings, ...body }; + await user.save(); - res.json(user.settings); -}); + res.json(user.settings); + }, +); export default router; diff --git a/src/api/routes/users/@me/index.ts b/src/api/routes/users/@me/index.ts index e849b72a..5eba4665 100644 --- a/src/api/routes/users/@me/index.ts +++ b/src/api/routes/users/@me/index.ts @@ -1,5 +1,15 @@ import { Router, Request, Response } from "express"; -import { User, PrivateUserProjection, emitEvent, UserUpdateEvent, handleFile, FieldErrors, adjustEmail, Config, UserModifySchema } from "@fosscord/util"; +import { + User, + PrivateUserProjection, + emitEvent, + UserUpdateEvent, + handleFile, + FieldErrors, + adjustEmail, + Config, + UserModifySchema, +} from "@fosscord/util"; import { route } from "@fosscord/api"; import bcrypt from "bcrypt"; import { HTTPError } from "lambert-server"; @@ -7,79 +17,134 @@ import { HTTPError } from "lambert-server"; const router: Router = Router(); router.get("/", route({}), async (req: Request, res: Response) => { - res.json(await User.findOne({ select: PrivateUserProjection, where: { id: req.user_id } })); + res.json( + await User.findOne({ + select: PrivateUserProjection, + where: { id: req.user_id }, + }), + ); }); -router.patch("/", route({ body: "UserModifySchema" }), async (req: Request, res: Response) => { - const body = req.body as UserModifySchema; - - const user = await User.findOneOrFail({ where: { id: req.user_id }, select: [...PrivateUserProjection, "data"] }); - - if (user.email == "demo@maddy.k.vu") throw new HTTPError("Demo user, sorry", 400); +router.patch( + "/", + route({ body: "UserModifySchema" }), + async (req: Request, res: Response) => { + const body = req.body as UserModifySchema; + + const user = await User.findOneOrFail({ + where: { id: req.user_id }, + select: [...PrivateUserProjection, "data"], + }); + + if (user.email == "demo@maddy.k.vu") + throw new HTTPError("Demo user, sorry", 400); + + if (body.avatar) + body.avatar = await handleFile( + `/avatars/${req.user_id}`, + body.avatar as string, + ); + if (body.banner) + body.banner = await handleFile( + `/banners/${req.user_id}`, + body.banner as string, + ); + + if (body.password) { + if (user.data?.hash) { + const same_password = await bcrypt.compare( + body.password, + user.data.hash || "", + ); + if (!same_password) { + throw FieldErrors({ + password: { + message: req.t("auth:login.INVALID_PASSWORD"), + code: "INVALID_PASSWORD", + }, + }); + } + } else { + user.data.hash = await bcrypt.hash(body.password, 12); + } + } - if (body.avatar) body.avatar = await handleFile(`/avatars/${req.user_id}`, body.avatar as string); - if (body.banner) body.banner = await handleFile(`/banners/${req.user_id}`, body.banner as string); + if (body.email) { + body.email = adjustEmail(body.email); + if (!body.email && Config.get().register.email.required) + throw FieldErrors({ + email: { + message: req.t("auth:register.EMAIL_INVALID"), + code: "EMAIL_INVALID", + }, + }); + if (!body.password) + throw FieldErrors({ + password: { + message: req.t("auth:register.INVALID_PASSWORD"), + code: "INVALID_PASSWORD", + }, + }); + } - if (body.password) { - if (user.data?.hash) { - const same_password = await bcrypt.compare(body.password, user.data.hash || ""); - if (!same_password) { - throw FieldErrors({ password: { message: req.t("auth:login.INVALID_PASSWORD"), code: "INVALID_PASSWORD" } }); + if (body.new_password) { + if (!body.password && !user.email) { + throw FieldErrors({ + password: { + code: "BASE_TYPE_REQUIRED", + message: req.t("common:field.BASE_TYPE_REQUIRED"), + }, + }); } - } else { - user.data.hash = await bcrypt.hash(body.password, 12); + user.data.hash = await bcrypt.hash(body.new_password, 12); } - } - - if (body.email) { - body.email = adjustEmail(body.email); - if (!body.email && Config.get().register.email.required) - throw FieldErrors({ email: { message: req.t("auth:register.EMAIL_INVALID"), code: "EMAIL_INVALID" } }); - if (!body.password) - throw FieldErrors({ password: { message: req.t("auth:register.INVALID_PASSWORD"), code: "INVALID_PASSWORD" } }); - } - - if (body.new_password) { - if (!body.password && !user.email) { - throw FieldErrors({ - password: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") } - }); - } - user.data.hash = await bcrypt.hash(body.new_password, 12); - } - - if (body.username) { - var check_username = body?.username?.replace(/\s/g, ''); - if (!check_username) { - throw FieldErrors({ - username: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") } - }); + + if (body.username) { + var check_username = body?.username?.replace(/\s/g, ""); + if (!check_username) { + throw FieldErrors({ + username: { + code: "BASE_TYPE_REQUIRED", + message: req.t("common:field.BASE_TYPE_REQUIRED"), + }, + }); + } } - } - if (body.discriminator) { - if (await User.findOne({ where: { discriminator: body.discriminator, username: body.username || user.username } })) { - throw FieldErrors({ - discriminator: { code: "INVALID_DISCRIMINATOR", message: "This discriminator is already in use." } - }); + if (body.discriminator) { + if ( + await User.findOne({ + where: { + discriminator: body.discriminator, + username: body.username || user.username, + }, + }) + ) { + throw FieldErrors({ + discriminator: { + code: "INVALID_DISCRIMINATOR", + message: "This discriminator is already in use.", + }, + }); + } } - } - user.assign(body); - await user.save(); + user.assign(body); + await user.save(); - // @ts-ignore - delete user.data; + // @ts-ignore + delete user.data; - // TODO: send update member list event in gateway - await emitEvent({ - event: "USER_UPDATE", - user_id: req.user_id, - data: user - } as UserUpdateEvent); + // TODO: send update member list event in gateway + await emitEvent({ + event: "USER_UPDATE", + user_id: req.user_id, + data: user, + } as UserUpdateEvent); - res.json(user); -}); + res.json(user); + }, +); export default router; // {"message": "Invalid two-factor code", "code": 60008} diff --git a/src/api/routes/users/@me/mfa/codes-verification.ts b/src/api/routes/users/@me/mfa/codes-verification.ts index 071c71fa..3411605b 100644 --- a/src/api/routes/users/@me/mfa/codes-verification.ts +++ b/src/api/routes/users/@me/mfa/codes-verification.ts @@ -1,41 +1,49 @@ import { Router, Request, Response } from "express"; import { route } from "@fosscord/api"; -import { BackupCode, generateMfaBackupCodes, User, CodesVerificationSchema } from "@fosscord/util"; +import { + BackupCode, + generateMfaBackupCodes, + User, + CodesVerificationSchema, +} from "@fosscord/util"; const router = Router(); -router.post("/", route({ body: "CodesVerificationSchema" }), async (req: Request, res: Response) => { - const { key, nonce, regenerate } = req.body as CodesVerificationSchema; - - // TODO: We don't have email/etc etc, so can't send a verification code. - // Once that's done, this route can verify `key` - - const user = await User.findOneOrFail({ where: { id: req.user_id } }); - - var codes: BackupCode[]; - if (regenerate) { - await BackupCode.update( - { user: { id: req.user_id } }, - { expired: true } - ); - - codes = generateMfaBackupCodes(req.user_id); - await Promise.all(codes.map(x => x.save())); - } - else { - codes = await BackupCode.find({ - where: { - user: { - id: req.user_id, +router.post( + "/", + route({ body: "CodesVerificationSchema" }), + async (req: Request, res: Response) => { + const { key, nonce, regenerate } = req.body as CodesVerificationSchema; + + // TODO: We don't have email/etc etc, so can't send a verification code. + // Once that's done, this route can verify `key` + + const user = await User.findOneOrFail({ where: { id: req.user_id } }); + + var codes: BackupCode[]; + if (regenerate) { + await BackupCode.update( + { user: { id: req.user_id } }, + { expired: true }, + ); + + codes = generateMfaBackupCodes(req.user_id); + await Promise.all(codes.map((x) => x.save())); + } else { + codes = await BackupCode.find({ + where: { + user: { + id: req.user_id, + }, + expired: false, }, - expired: false, - } - }); - } + }); + } - return res.json({ - backup_codes: codes.map(x => ({ ...x, expired: undefined })), - }); -}); + return res.json({ + backup_codes: codes.map((x) => ({ ...x, expired: undefined })), + }); + }, +); export default router; diff --git a/src/api/routes/users/@me/mfa/codes.ts b/src/api/routes/users/@me/mfa/codes.ts index 58466b9c..33053028 100644 --- a/src/api/routes/users/@me/mfa/codes.ts +++ b/src/api/routes/users/@me/mfa/codes.ts @@ -1,45 +1,62 @@ import { Router, Request, Response } from "express"; import { route } from "@fosscord/api"; -import { BackupCode, FieldErrors, generateMfaBackupCodes, User, MfaCodesSchema } from "@fosscord/util"; +import { + BackupCode, + FieldErrors, + generateMfaBackupCodes, + User, + MfaCodesSchema, +} from "@fosscord/util"; import bcrypt from "bcrypt"; const router = Router(); // TODO: This route is replaced with users/@me/mfa/codes-verification in newer clients -router.post("/", route({ body: "MfaCodesSchema" }), async (req: Request, res: Response) => { - const { password, regenerate } = req.body as MfaCodesSchema; - - const user = await User.findOneOrFail({ where: { id: req.user_id }, select: ["data"] }); - - if (!await bcrypt.compare(password, user.data.hash || "")) { - throw FieldErrors({ password: { message: req.t("auth:login.INVALID_PASSWORD"), code: "INVALID_PASSWORD" } }); - } - - var codes: BackupCode[]; - if (regenerate) { - await BackupCode.update( - { user: { id: req.user_id } }, - { expired: true } - ); - - codes = generateMfaBackupCodes(req.user_id); - await Promise.all(codes.map(x => x.save())); - } - else { - codes = await BackupCode.find({ - where: { - user: { - id: req.user_id, - }, - expired: false, - } +router.post( + "/", + route({ body: "MfaCodesSchema" }), + async (req: Request, res: Response) => { + const { password, regenerate } = req.body as MfaCodesSchema; + + const user = await User.findOneOrFail({ + where: { id: req.user_id }, + select: ["data"], }); - } - return res.json({ - backup_codes: codes.map(x => ({ ...x, expired: undefined })), - }); -}); + if (!(await bcrypt.compare(password, user.data.hash || ""))) { + throw FieldErrors({ + password: { + message: req.t("auth:login.INVALID_PASSWORD"), + code: "INVALID_PASSWORD", + }, + }); + } + + var codes: BackupCode[]; + if (regenerate) { + await BackupCode.update( + { user: { id: req.user_id } }, + { expired: true }, + ); + + codes = generateMfaBackupCodes(req.user_id); + await Promise.all(codes.map((x) => x.save())); + } else { + codes = await BackupCode.find({ + where: { + user: { + id: req.user_id, + }, + expired: false, + }, + }); + } + + return res.json({ + backup_codes: codes.map((x) => ({ ...x, expired: undefined })), + }); + }, +); export default router; diff --git a/src/api/routes/users/@me/mfa/totp/disable.ts b/src/api/routes/users/@me/mfa/totp/disable.ts index 2fe9355c..7916e598 100644 --- a/src/api/routes/users/@me/mfa/totp/disable.ts +++ b/src/api/routes/users/@me/mfa/totp/disable.ts @@ -1,41 +1,56 @@ import { Router, Request, Response } from "express"; import { route } from "@fosscord/api"; -import { verifyToken } from 'node-2fa'; +import { verifyToken } from "node-2fa"; import { HTTPError } from "lambert-server"; -import { User, generateToken, BackupCode, TotpDisableSchema } from "@fosscord/util"; +import { + User, + generateToken, + BackupCode, + TotpDisableSchema, +} from "@fosscord/util"; const router = Router(); -router.post("/", route({ body: "TotpDisableSchema" }), async (req: Request, res: Response) => { - const body = req.body as TotpDisableSchema; - - const user = await User.findOneOrFail({ where: { id: req.user_id }, select: ["totp_secret"] }); - - const backup = await BackupCode.findOne({ where: { code: body.code } }); - if (!backup) { - const ret = verifyToken(user.totp_secret!, body.code); - if (!ret || ret.delta != 0) - throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008); - } - - await User.update( - { id: req.user_id }, - { - mfa_enabled: false, - totp_secret: "", - }, - ); - - await BackupCode.update( - { user: { id: req.user_id } }, - { - expired: true, +router.post( + "/", + route({ body: "TotpDisableSchema" }), + async (req: Request, res: Response) => { + const body = req.body as TotpDisableSchema; + + const user = await User.findOneOrFail({ + where: { id: req.user_id }, + select: ["totp_secret"], + }); + + const backup = await BackupCode.findOne({ where: { code: body.code } }); + if (!backup) { + const ret = verifyToken(user.totp_secret!, body.code); + if (!ret || ret.delta != 0) + throw new HTTPError( + req.t("auth:login.INVALID_TOTP_CODE"), + 60008, + ); } - ); - return res.json({ - token: await generateToken(user.id), - }); -}); - -export default router; \ No newline at end of file + await User.update( + { id: req.user_id }, + { + mfa_enabled: false, + totp_secret: "", + }, + ); + + await BackupCode.update( + { user: { id: req.user_id } }, + { + expired: true, + }, + ); + + return res.json({ + token: await generateToken(user.id), + }); + }, +); + +export default router; diff --git a/src/api/routes/users/@me/mfa/totp/enable.ts b/src/api/routes/users/@me/mfa/totp/enable.ts index adafe180..75c64425 100644 --- a/src/api/routes/users/@me/mfa/totp/enable.ts +++ b/src/api/routes/users/@me/mfa/totp/enable.ts @@ -1,46 +1,62 @@ import { Router, Request, Response } from "express"; -import { User, generateToken, generateMfaBackupCodes, TotpEnableSchema } from "@fosscord/util"; +import { + User, + generateToken, + generateMfaBackupCodes, + TotpEnableSchema, +} from "@fosscord/util"; import { route } from "@fosscord/api"; import bcrypt from "bcrypt"; import { HTTPError } from "lambert-server"; -import { verifyToken } from 'node-2fa'; +import { verifyToken } from "node-2fa"; const router = Router(); -router.post("/", route({ body: "TotpEnableSchema" }), async (req: Request, res: Response) => { - const body = req.body as TotpEnableSchema; - - const user = await User.findOneOrFail({ where: { id: req.user_id }, select: ["data", "email"] }); - - if (user.email == "demo@maddy.k.vu") throw new HTTPError("Demo user, sorry", 400); - - // TODO: Are guests allowed to enable 2fa? - if (user.data.hash) { - if (!await bcrypt.compare(body.password, user.data.hash)) { - throw new HTTPError(req.t("auth:login.INVALID_PASSWORD")); +router.post( + "/", + route({ body: "TotpEnableSchema" }), + async (req: Request, res: Response) => { + const body = req.body as TotpEnableSchema; + + const user = await User.findOneOrFail({ + where: { id: req.user_id }, + select: ["data", "email"], + }); + + if (user.email == "demo@maddy.k.vu") + throw new HTTPError("Demo user, sorry", 400); + + // TODO: Are guests allowed to enable 2fa? + if (user.data.hash) { + if (!(await bcrypt.compare(body.password, user.data.hash))) { + throw new HTTPError(req.t("auth:login.INVALID_PASSWORD")); + } } - } - - if (!body.secret) - throw new HTTPError(req.t("auth:login.INVALID_TOTP_SECRET"), 60005); - - if (!body.code) - throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008); - - if (verifyToken(body.secret, body.code)?.delta != 0) - throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008); - - let backup_codes = generateMfaBackupCodes(req.user_id); - await Promise.all(backup_codes.map(x => x.save())); - await User.update( - { id: req.user_id }, - { mfa_enabled: true, totp_secret: body.secret } - ); - - res.send({ - token: await generateToken(user.id), - backup_codes: backup_codes.map(x => ({ ...x, expired: undefined })), - }); -}); -export default router; \ No newline at end of file + if (!body.secret) + throw new HTTPError(req.t("auth:login.INVALID_TOTP_SECRET"), 60005); + + if (!body.code) + throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008); + + if (verifyToken(body.secret, body.code)?.delta != 0) + throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008); + + let backup_codes = generateMfaBackupCodes(req.user_id); + await Promise.all(backup_codes.map((x) => x.save())); + await User.update( + { id: req.user_id }, + { mfa_enabled: true, totp_secret: body.secret }, + ); + + res.send({ + token: await generateToken(user.id), + backup_codes: backup_codes.map((x) => ({ + ...x, + expired: undefined, + })), + }); + }, +); + +export default router; diff --git a/src/api/routes/users/@me/notes.ts b/src/api/routes/users/@me/notes.ts index f938f088..e54eb897 100644 --- a/src/api/routes/users/@me/notes.ts +++ b/src/api/routes/users/@me/notes.ts @@ -11,7 +11,7 @@ router.get("/:id", route({}), async (req: Request, res: Response) => { where: { owner: { id: req.user_id }, target: { id: id }, - } + }, }); return res.json({ @@ -24,32 +24,40 @@ router.get("/:id", route({}), async (req: Request, res: Response) => { router.put("/:id", route({}), async (req: Request, res: Response) => { const { id } = req.params; const owner = await User.findOneOrFail({ where: { id: req.user_id } }); - const target = await User.findOneOrFail({ where: { id: id } }); //if noted user does not exist throw + const target = await User.findOneOrFail({ where: { id: id } }); //if noted user does not exist throw const { note } = req.body; if (note && note.length) { // upsert a note - if (await Note.findOne({ where: { owner: { id: owner.id }, target: { id: target.id } } })) { + if ( + await Note.findOne({ + where: { owner: { id: owner.id }, target: { id: target.id } }, + }) + ) { Note.update( { owner: { id: owner.id }, target: { id: target.id } }, - { owner, target, content: note } + { owner, target, content: note }, ); + } else { + Note.insert({ + id: Snowflake.generate(), + owner, + target, + content: note, + }); } - else { - Note.insert( - { id: Snowflake.generate(), owner, target, content: note } - ); - } - } - else { - await Note.delete({ owner: { id: owner.id }, target: { id: target.id } }); + } else { + await Note.delete({ + owner: { id: owner.id }, + target: { id: target.id }, + }); } await emitEvent({ event: "USER_NOTE_UPDATE", data: { note: note, - id: target.id + id: target.id, }, user_id: owner.id, }); diff --git a/src/api/routes/users/@me/relationships.ts b/src/api/routes/users/@me/relationships.ts index cd33704d..3eec704b 100644 --- a/src/api/routes/users/@me/relationships.ts +++ b/src/api/routes/users/@me/relationships.ts @@ -6,7 +6,7 @@ import { RelationshipRemoveEvent, emitEvent, Relationship, - Config + Config, } from "@fosscord/util"; import { Router, Response, Request } from "express"; import { HTTPError } from "lambert-server"; @@ -15,13 +15,16 @@ import { route } from "@fosscord/api"; const router = Router(); -const userProjection: (keyof User)[] = ["relationships", ...PublicUserProjection]; +const userProjection: (keyof User)[] = [ + "relationships", + ...PublicUserProjection, +]; router.get("/", route({}), async (req: Request, res: Response) => { const user = await User.findOneOrFail({ where: { id: req.user_id }, relations: ["relationships", "relationships.to"], - select: ["id", "relationships"] + select: ["id", "relationships"], }); //TODO DTO @@ -30,49 +33,76 @@ router.get("/", route({}), async (req: Request, res: Response) => { id: r.to.id, type: r.type, nickname: null, - user: r.to.toPublicUser() + user: r.to.toPublicUser(), }; }); return res.json(related_users); }); -router.put("/:id", route({ body: "RelationshipPutSchema" }), async (req: Request, res: Response) => { - return await updateRelationship( - req, - res, - await User.findOneOrFail({ where: { id: req.params.id }, relations: ["relationships", "relationships.to"], select: userProjection }), - req.body.type ?? RelationshipType.friends - ); -}); +router.put( + "/:id", + route({ body: "RelationshipPutSchema" }), + async (req: Request, res: Response) => { + return await updateRelationship( + req, + res, + await User.findOneOrFail({ + where: { id: req.params.id }, + relations: ["relationships", "relationships.to"], + select: userProjection, + }), + req.body.type ?? RelationshipType.friends, + ); + }, +); -router.post("/", route({ body: "RelationshipPostSchema" }), async (req: Request, res: Response) => { - return await updateRelationship( - req, - res, - await User.findOneOrFail({ - relations: ["relationships", "relationships.to"], - select: userProjection, - where: { - discriminator: String(req.body.discriminator).padStart(4, "0"), //Discord send the discriminator as integer, we need to add leading zeroes - username: req.body.username - } - }), - req.body.type - ); -}); +router.post( + "/", + route({ body: "RelationshipPostSchema" }), + async (req: Request, res: Response) => { + return await updateRelationship( + req, + res, + await User.findOneOrFail({ + relations: ["relationships", "relationships.to"], + select: userProjection, + where: { + discriminator: String(req.body.discriminator).padStart( + 4, + "0", + ), //Discord send the discriminator as integer, we need to add leading zeroes + username: req.body.username, + }, + }), + req.body.type, + ); + }, +); router.delete("/:id", route({}), async (req: Request, res: Response) => { const { id } = req.params; - if (id === req.user_id) throw new HTTPError("You can't remove yourself as a friend"); + if (id === req.user_id) + throw new HTTPError("You can't remove yourself as a friend"); - const user = await User.findOneOrFail({ where: { id: req.user_id }, select: userProjection, relations: ["relationships"] }); - const friend = await User.findOneOrFail({ where: { id: id }, select: userProjection, relations: ["relationships"] }); + const user = await User.findOneOrFail({ + where: { id: req.user_id }, + select: userProjection, + relations: ["relationships"], + }); + const friend = await User.findOneOrFail({ + where: { id: id }, + select: userProjection, + relations: ["relationships"], + }); const relationship = user.relationships.find((x) => x.to_id === id); - const friendRequest = friend.relationships.find((x) => x.to_id === req.user_id); + const friendRequest = friend.relationships.find( + (x) => x.to_id === req.user_id, + ); - if (!relationship) throw new HTTPError("You are not friends with the user", 404); + if (!relationship) + throw new HTTPError("You are not friends with the user", 404); if (relationship?.type === RelationshipType.blocked) { // unblock user @@ -81,8 +111,8 @@ router.delete("/:id", route({}), async (req: Request, res: Response) => { emitEvent({ event: "RELATIONSHIP_REMOVE", user_id: req.user_id, - data: relationship.toPublicRelationship() - } as RelationshipRemoveEvent) + data: relationship.toPublicRelationship(), + } as RelationshipRemoveEvent), ]); return res.sendStatus(204); } @@ -92,8 +122,8 @@ router.delete("/:id", route({}), async (req: Request, res: Response) => { await emitEvent({ event: "RELATIONSHIP_REMOVE", data: friendRequest.toPublicRelationship(), - user_id: id - } as RelationshipRemoveEvent) + user_id: id, + } as RelationshipRemoveEvent), ]); } @@ -102,8 +132,8 @@ router.delete("/:id", route({}), async (req: Request, res: Response) => { emitEvent({ event: "RELATIONSHIP_REMOVE", data: relationship.toPublicRelationship(), - user_id: req.user_id - } as RelationshipRemoveEvent) + user_id: req.user_id, + } as RelationshipRemoveEvent), ]); return res.sendStatus(204); @@ -111,26 +141,40 @@ router.delete("/:id", route({}), async (req: Request, res: Response) => { export default router; -async function updateRelationship(req: Request, res: Response, friend: User, type: RelationshipType) { +async function updateRelationship( + req: Request, + res: Response, + friend: User, + type: RelationshipType, +) { const id = friend.id; - if (id === req.user_id) throw new HTTPError("You can't add yourself as a friend"); + if (id === req.user_id) + throw new HTTPError("You can't add yourself as a friend"); const user = await User.findOneOrFail({ where: { id: req.user_id }, - relations: ["relationships", "relationships.to"], select: userProjection + relations: ["relationships", "relationships.to"], + select: userProjection, }); var relationship = user.relationships.find((x) => x.to_id === id); - const friendRequest = friend.relationships.find((x) => x.to_id === req.user_id); + const friendRequest = friend.relationships.find( + (x) => x.to_id === req.user_id, + ); // TODO: you can add infinitely many blocked users (should this be prevented?) if (type === RelationshipType.blocked) { if (relationship) { - if (relationship.type === RelationshipType.blocked) throw new HTTPError("You already blocked the user"); + if (relationship.type === RelationshipType.blocked) + throw new HTTPError("You already blocked the user"); relationship.type = RelationshipType.blocked; await relationship.save(); } else { - relationship = await Relationship.create({ to_id: id, type: RelationshipType.blocked, from_id: req.user_id }).save(); + relationship = await Relationship.create({ + to_id: id, + type: RelationshipType.blocked, + from_id: req.user_id, + }).save(); } if (friendRequest && friendRequest.type !== RelationshipType.blocked) { @@ -139,43 +183,56 @@ async function updateRelationship(req: Request, res: Response, friend: User, typ emitEvent({ event: "RELATIONSHIP_REMOVE", data: friendRequest.toPublicRelationship(), - user_id: id - } as RelationshipRemoveEvent) + user_id: id, + } as RelationshipRemoveEvent), ]); } await emitEvent({ event: "RELATIONSHIP_ADD", data: relationship.toPublicRelationship(), - user_id: req.user_id + user_id: req.user_id, } as RelationshipAddEvent); return res.sendStatus(204); } const { maxFriends } = Config.get().limits.user; - if (user.relationships.length >= maxFriends) throw DiscordApiErrors.MAXIMUM_FRIENDS.withParams(maxFriends); + if (user.relationships.length >= maxFriends) + throw DiscordApiErrors.MAXIMUM_FRIENDS.withParams(maxFriends); - var incoming_relationship = Relationship.create({ nickname: undefined, type: RelationshipType.incoming, to: user, from: friend }); + var incoming_relationship = Relationship.create({ + nickname: undefined, + type: RelationshipType.incoming, + to: user, + from: friend, + }); var outgoing_relationship = Relationship.create({ nickname: undefined, type: RelationshipType.outgoing, to: friend, - from: user + from: user, }); if (friendRequest) { - if (friendRequest.type === RelationshipType.blocked) throw new HTTPError("The user blocked you"); - if (friendRequest.type === RelationshipType.friends) throw new HTTPError("You are already friends with the user"); + if (friendRequest.type === RelationshipType.blocked) + throw new HTTPError("The user blocked you"); + if (friendRequest.type === RelationshipType.friends) + throw new HTTPError("You are already friends with the user"); // accept friend request incoming_relationship = friendRequest; incoming_relationship.type = RelationshipType.friends; } if (relationship) { - if (relationship.type === RelationshipType.outgoing) throw new HTTPError("You already sent a friend request"); - if (relationship.type === RelationshipType.blocked) throw new HTTPError("Unblock the user before sending a friend request"); - if (relationship.type === RelationshipType.friends) throw new HTTPError("You are already friends with the user"); + if (relationship.type === RelationshipType.outgoing) + throw new HTTPError("You already sent a friend request"); + if (relationship.type === RelationshipType.blocked) + throw new HTTPError( + "Unblock the user before sending a friend request", + ); + if (relationship.type === RelationshipType.friends) + throw new HTTPError("You are already friends with the user"); outgoing_relationship = relationship; outgoing_relationship.type = RelationshipType.friends; } @@ -186,16 +243,16 @@ async function updateRelationship(req: Request, res: Response, friend: User, typ emitEvent({ event: "RELATIONSHIP_ADD", data: outgoing_relationship.toPublicRelationship(), - user_id: req.user_id + user_id: req.user_id, } as RelationshipAddEvent), emitEvent({ event: "RELATIONSHIP_ADD", data: { ...incoming_relationship.toPublicRelationship(), - should_notify: true + should_notify: true, }, - user_id: id - } as RelationshipAddEvent) + user_id: id, + } as RelationshipAddEvent), ]); return res.sendStatus(204); diff --git a/src/api/routes/users/@me/settings.ts b/src/api/routes/users/@me/settings.ts index 9060baf7..30e5969c 100644 --- a/src/api/routes/users/@me/settings.ts +++ b/src/api/routes/users/@me/settings.ts @@ -4,25 +4,31 @@ import { route } from "@fosscord/api"; const router = Router(); -export interface UserSettingsSchema extends Partial<UserSettings> { } +export interface UserSettingsSchema extends Partial<UserSettings> {} router.get("/", route({}), async (req: Request, res: Response) => { const user = await User.findOneOrFail({ where: { id: req.user_id }, - select: ["settings"] + select: ["settings"], }); return res.json(user.settings); }); -router.patch("/", route({ body: "UserSettingsSchema" }), async (req: Request, res: Response) => { - const body = req.body as UserSettings; - if (body.locale === "en") body.locale = "en-US"; // fix discord client crash on unkown locale +router.patch( + "/", + route({ body: "UserSettingsSchema" }), + async (req: Request, res: Response) => { + const body = req.body as UserSettings; + if (body.locale === "en") body.locale = "en-US"; // fix discord client crash on unkown locale - const user = await User.findOneOrFail({ where: { id: req.user_id, bot: false } }); - user.settings = { ...user.settings, ...body }; - await user.save(); + const user = await User.findOneOrFail({ + where: { id: req.user_id, bot: false }, + }); + user.settings = { ...user.settings, ...body }; + await user.save(); - res.json(user.settings); -}); + res.json(user.settings); + }, +); export default router; diff --git a/src/api/start.ts b/src/api/start.ts index ccb4d108..fa120e59 100644 --- a/src/api/start.ts +++ b/src/api/start.ts @@ -11,7 +11,7 @@ var cores = 1; try { cores = Number(process.env.THREADS) || os.cpus().length; } catch { - console.log("[API] Failed to get thread count! Using 1...") + console.log("[API] Failed to get thread count! Using 1..."); } if (cluster.isMaster && process.env.NODE_ENV == "production") { diff --git a/src/api/util/handlers/Message.ts b/src/api/util/handlers/Message.ts index 0d29c2e6..d44b368f 100644 --- a/src/api/util/handlers/Message.ts +++ b/src/api/util/handlers/Message.ts @@ -32,24 +32,32 @@ const allow_empty = false; // TODO: check webhook, application, system author, stickers // TODO: embed gifs/videos/images -const LINK_REGEX = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g; +const LINK_REGEX = + /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g; const DEFAULT_FETCH_OPTIONS: any = { redirect: "follow", follow: 1, headers: { - "user-agent": "Mozilla/5.0 (compatible; Fosscord/1.0; +https://github.com/fosscord/fosscord)" + "user-agent": + "Mozilla/5.0 (compatible; Fosscord/1.0; +https://github.com/fosscord/fosscord)", }, // size: 1024 * 1024 * 5, // grabbed from config later compress: true, - method: "GET" + method: "GET", }; export async function handleMessage(opts: MessageOptions): Promise<Message> { - const channel = await Channel.findOneOrFail({ where: { id: opts.channel_id }, relations: ["recipients"] }); - if (!channel || !opts.channel_id) throw new HTTPError("Channel not found", 404); + const channel = await Channel.findOneOrFail({ + where: { id: opts.channel_id }, + relations: ["recipients"], + }); + if (!channel || !opts.channel_id) + throw new HTTPError("Channel not found", 404); - const stickers = opts.sticker_ids ? await Sticker.find({ where: { id: In(opts.sticker_ids) } }) : undefined; + const stickers = opts.sticker_ids + ? await Sticker.find({ where: { id: In(opts.sticker_ids) } }) + : undefined; const message = Message.create({ ...opts, id: Snowflake.generate(), @@ -58,11 +66,14 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> { channel_id: opts.channel_id, attachments: opts.attachments || [], embeds: opts.embeds || [], - reactions: /*opts.reactions ||*/[], + reactions: /*opts.reactions ||*/ [], type: opts.type ?? 0, }); - if (message.content && message.content.length > Config.get().limits.message.maxCharacters) { + if ( + message.content && + message.content.length > Config.get().limits.message.maxCharacters + ) { throw new HTTPError("Content length over max character limit"); } @@ -72,13 +83,21 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> { rights.hasThrow("SEND_MESSAGES"); } if (opts.application_id) { - message.application = await Application.findOneOrFail({ where: { id: opts.application_id } }); + message.application = await Application.findOneOrFail({ + where: { id: opts.application_id }, + }); } if (opts.webhook_id) { - message.webhook = await Webhook.findOneOrFail({ where: { id: opts.webhook_id } }); + message.webhook = await Webhook.findOneOrFail({ + where: { id: opts.webhook_id }, + }); } - const permission = await getPermission(opts.author_id, channel.guild_id, opts.channel_id); + const permission = await getPermission( + opts.author_id, + channel.guild_id, + opts.channel_id, + ); permission.hasThrow("SEND_MESSAGES"); if (permission.cache.member) { message.member = permission.cache.member; @@ -89,10 +108,18 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> { permission.hasThrow("READ_MESSAGE_HISTORY"); // code below has to be redone when we add custom message routing if (message.guild_id !== null) { - const guild = await Guild.findOneOrFail({ where: { id: channel.guild_id } }); + const guild = await Guild.findOneOrFail({ + where: { id: channel.guild_id }, + }); if (!guild.features.includes("CROSS_CHANNEL_REPLIES")) { - if (opts.message_reference.guild_id !== channel.guild_id) throw new HTTPError("You can only reference messages from this guild"); - if (opts.message_reference.channel_id !== opts.channel_id) throw new HTTPError("You can only reference messages from this channel"); + if (opts.message_reference.guild_id !== channel.guild_id) + throw new HTTPError( + "You can only reference messages from this guild", + ); + if (opts.message_reference.channel_id !== opts.channel_id) + throw new HTTPError( + "You can only reference messages from this channel", + ); } } /** Q: should be checked if the referenced message exists? ANSWER: NO @@ -102,7 +129,13 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> { } // TODO: stickers/activity - if (!allow_empty && (!opts.content && !opts.embeds?.length && !opts.attachments?.length && !opts.sticker_ids?.length)) { + if ( + !allow_empty && + !opts.content && + !opts.embeds?.length && + !opts.attachments?.length && + !opts.sticker_ids?.length + ) { throw new HTTPError("Empty messages are not allowed", 50006); } @@ -112,31 +145,42 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> { var mention_user_ids = [] as string[]; var mention_everyone = false; - if (content) { // TODO: explicit-only mentions + if (content) { + // TODO: explicit-only mentions message.content = content.trim(); for (const [_, mention] of content.matchAll(CHANNEL_MENTION)) { - if (!mention_channel_ids.includes(mention)) mention_channel_ids.push(mention); + if (!mention_channel_ids.includes(mention)) + mention_channel_ids.push(mention); } for (const [_, mention] of content.matchAll(USER_MENTION)) { - if (!mention_user_ids.includes(mention)) mention_user_ids.push(mention); + if (!mention_user_ids.includes(mention)) + mention_user_ids.push(mention); } await Promise.all( - Array.from(content.matchAll(ROLE_MENTION)).map(async ([_, mention]) => { - const role = await Role.findOneOrFail({ where: { id: mention, guild_id: channel.guild_id } }); - if (role.mentionable || permission.has("MANAGE_ROLES")) { - mention_role_ids.push(mention); - } - }) + Array.from(content.matchAll(ROLE_MENTION)).map( + async ([_, mention]) => { + const role = await Role.findOneOrFail({ + where: { id: mention, guild_id: channel.guild_id }, + }); + if (role.mentionable || permission.has("MANAGE_ROLES")) { + mention_role_ids.push(mention); + } + }, + ), ); if (permission.has("MENTION_EVERYONE")) { - mention_everyone = !!content.match(EVERYONE_MENTION) || !!content.match(HERE_MENTION); + mention_everyone = + !!content.match(EVERYONE_MENTION) || + !!content.match(HERE_MENTION); } } - message.mention_channels = mention_channel_ids.map((x) => Channel.create({ id: x })); + message.mention_channels = mention_channel_ids.map((x) => + Channel.create({ id: x }), + ); message.mention_roles = mention_role_ids.map((x) => Role.create({ id: x })); message.mentions = mention_user_ids.map((x) => User.create({ id: x })); message.mention_everyone = mention_everyone; @@ -156,7 +200,8 @@ export async function postHandleMessage(message: Message) { links = links.slice(0, 20) as RegExpMatchArray; // embed max 20 links — TODO: make this configurable with instance policies - const { endpointPublic, resizeWidthMax, resizeHeightMax } = Config.get().cdn; + const { endpointPublic, resizeWidthMax, resizeHeightMax } = + Config.get().cdn; for (const link of links) { try { @@ -176,45 +221,64 @@ export async function postHandleMessage(message: Message) { }, image: { // can't be bothered rn - proxy_url: `${endpointPublic}/external/resize/${encodeURIComponent(link)}?width=500&height=400`, + proxy_url: `${endpointPublic}/external/resize/${encodeURIComponent( + link, + )}?width=500&height=400`, url: link, width: 500, - height: 400 - } + height: 400, + }, }; data.embeds.push(embed); - } - else { + } else { const text = await request.text(); const $ = cheerio.load(text); const title = $('meta[property="og:title"]').attr("content"); const provider_name = $('meta[property="og:site_name"]').text(); - const author_name = $('meta[property="article:author"]').attr("content"); - const description = $('meta[property="og:description"]').attr("content") || $('meta[property="description"]').attr("content"); + const author_name = $('meta[property="article:author"]').attr( + "content", + ); + const description = + $('meta[property="og:description"]').attr("content") || + $('meta[property="description"]').attr("content"); const image = $('meta[property="og:image"]').attr("content"); - const width = parseInt($('meta[property="og:image:width"]').attr("content") || "") || undefined; - const height = parseInt($('meta[property="og:image:height"]').attr("content") || "") || undefined; + const width = + parseInt( + $('meta[property="og:image:width"]').attr("content") || + "", + ) || undefined; + const height = + parseInt( + $('meta[property="og:image:height"]').attr("content") || + "", + ) || undefined; const url = $('meta[property="og:url"]').attr("content"); // TODO: color embed = { provider: { url: link, - name: provider_name - } + name: provider_name, + }, }; const resizeWidth = Math.min(resizeWidthMax ?? 1, width ?? 100); - const resizeHeight = Math.min(resizeHeightMax ?? 1, height ?? 100); + const resizeHeight = Math.min( + resizeHeightMax ?? 1, + height ?? 100, + ); if (author_name) embed.author = { name: author_name }; - if (image) embed.thumbnail = { - proxy_url: `${endpointPublic}/external/resize/${encodeURIComponent(image)}?width=${resizeWidth}&height=${resizeHeight}`, - url: image, - width: width, - height: height - }; + if (image) + embed.thumbnail = { + proxy_url: `${endpointPublic}/external/resize/${encodeURIComponent( + image, + )}?width=${resizeWidth}&height=${resizeHeight}`, + url: image, + width: width, + height: height, + }; if (title) embed.title = title; if (url) embed.url = url; if (description) embed.description = description; @@ -227,18 +291,25 @@ export async function postHandleMessage(message: Message) { // very bad code below // don't care lol - if (embed?.thumbnail?.url && approvedProviders.indexOf(new URL(embed.thumbnail.url).hostname) !== -1) { + if ( + embed?.thumbnail?.url && + approvedProviders.indexOf( + new URL(embed.thumbnail.url).hostname, + ) !== -1 + ) { embed = { provider: { url: link, name: new URL(link).hostname, }, image: { - proxy_url: `${endpointPublic}/external/resize/${encodeURIComponent(image!)}?width=${resizeWidth}&height=${resizeHeight}`, + proxy_url: `${endpointPublic}/external/resize/${encodeURIComponent( + image!, + )}?width=${resizeWidth}&height=${resizeHeight}`, url: image, width: width, - height: height - } + height: height, + }, }; } @@ -246,16 +317,19 @@ export async function postHandleMessage(message: Message) { data.embeds.push(embed); } } - } catch (error) { } + } catch (error) {} } await Promise.all([ emitEvent({ event: "MESSAGE_UPDATE", channel_id: message.channel_id, - data + data, } as MessageUpdateEvent), - Message.update({ id: message.id, channel_id: message.channel_id }, { embeds: data.embeds }) + Message.update( + { id: message.id, channel_id: message.channel_id }, + { embeds: data.embeds }, + ), ]); } @@ -264,10 +338,14 @@ export async function sendMessage(opts: MessageOptions) { await Promise.all([ Message.insert(message), - emitEvent({ event: "MESSAGE_CREATE", channel_id: opts.channel_id, data: message.toJSON() } as MessageCreateEvent) + emitEvent({ + event: "MESSAGE_CREATE", + channel_id: opts.channel_id, + data: message.toJSON(), + } as MessageCreateEvent), ]); - postHandleMessage(message).catch((e) => { }); // no await as it should catch error non-blockingly + postHandleMessage(message).catch((e) => {}); // no await as it should catch error non-blockingly return message; } diff --git a/src/api/util/handlers/Voice.ts b/src/api/util/handlers/Voice.ts index 4d60eb91..88e266a1 100644 --- a/src/api/util/handlers/Voice.ts +++ b/src/api/util/handlers/Voice.ts @@ -3,7 +3,9 @@ import { distanceBetweenLocations, IPAnalysis } from "../utility/ipAddress"; export async function getVoiceRegions(ipAddress: string, vip: boolean) { const regions = Config.get().regions; - const availableRegions = regions.available.filter((ar) => (vip ? true : !ar.vip)); + const availableRegions = regions.available.filter((ar) => + vip ? true : !ar.vip, + ); let optimalId = regions.default; if (!regions.useDefaultAsOptimal) { @@ -13,7 +15,10 @@ export async function getVoiceRegions(ipAddress: string, vip: boolean) { for (let ar of availableRegions) { //TODO the endpoint location should be saved in the database if not already present to prevent IPAnalysis call - const dist = distanceBetweenLocations(clientIpAnalysis, ar.location || (await IPAnalysis(ar.endpoint))); + const dist = distanceBetweenLocations( + clientIpAnalysis, + ar.location || (await IPAnalysis(ar.endpoint)), + ); if (dist < min) { min = dist; @@ -27,6 +32,6 @@ export async function getVoiceRegions(ipAddress: string, vip: boolean) { name: ar.name, custom: ar.custom, deprecated: ar.deprecated, - optimal: ar.id === optimalId + optimal: ar.id === optimalId, })); } diff --git a/src/api/util/handlers/route.ts b/src/api/util/handlers/route.ts index c245b411..5dcae953 100644 --- a/src/api/util/handlers/route.ts +++ b/src/api/util/handlers/route.ts @@ -10,7 +10,7 @@ import { PermissionResolvable, Permissions, RightResolvable, - Rights + Rights, } from "@fosscord/util"; import { NextFunction, Request, Response } from "express"; import { AnyValidateFunction } from "ajv/dist/core"; @@ -23,7 +23,11 @@ declare global { } } -export type RouteResponse = { status?: number; body?: `${string}Response`; headers?: Record<string, string> }; +export type RouteResponse = { + status?: number; + body?: `${string}Response`; + headers?: Record<string, string>; +}; export interface RouteOptions { permission?: PermissionResolvable; @@ -48,11 +52,17 @@ export function route(opts: RouteOptions) { return async (req: Request, res: Response, next: NextFunction) => { if (opts.permission) { const required = new Permissions(opts.permission); - req.permission = await getPermission(req.user_id, req.params.guild_id, req.params.channel_id); + req.permission = await getPermission( + req.user_id, + req.params.guild_id, + req.params.channel_id, + ); // bitfield comparison: check if user lacks certain permission if (!req.permission.has(required)) { - throw DiscordApiErrors.MISSING_PERMISSIONS.withParams(opts.permission as string); + throw DiscordApiErrors.MISSING_PERMISSIONS.withParams( + opts.permission as string, + ); } } @@ -61,15 +71,26 @@ export function route(opts: RouteOptions) { req.rights = await getRights(req.user_id); if (!req.rights || !req.rights.has(required)) { - throw FosscordApiErrors.MISSING_RIGHTS.withParams(opts.right as string); + throw FosscordApiErrors.MISSING_RIGHTS.withParams( + opts.right as string, + ); } } if (validate) { const valid = validate(normalizeBody(req.body)); if (!valid) { - const fields: Record<string, { code?: string; message: string }> = {}; - validate.errors?.forEach((x) => (fields[x.instancePath.slice(1)] = { code: x.keyword, message: x.message || "" })); + const fields: Record< + string, + { code?: string; message: string } + > = {}; + validate.errors?.forEach( + (x) => + (fields[x.instancePath.slice(1)] = { + code: x.keyword, + message: x.message || "", + }), + ); throw FieldErrors(fields); } } diff --git a/src/api/util/index.ts b/src/api/util/index.ts index de6b6064..9f375f72 100644 --- a/src/api/util/index.ts +++ b/src/api/util/index.ts @@ -6,4 +6,4 @@ export * from "./utility/RandomInviteID"; export * from "./handlers/route"; export * from "./utility/String"; export * from "./handlers/Voice"; -export * from "./utility/captcha"; \ No newline at end of file +export * from "./utility/captcha"; diff --git a/src/api/util/utility/Base64.ts b/src/api/util/utility/Base64.ts index 46cff77a..c10176f2 100644 --- a/src/api/util/utility/Base64.ts +++ b/src/api/util/utility/Base64.ts @@ -1,4 +1,5 @@ -const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+"; +const alphabet = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+"; // binary to string lookup table const b2s = alphabet.split(""); diff --git a/src/api/util/utility/RandomInviteID.ts b/src/api/util/utility/RandomInviteID.ts index 7ea344e0..bfed65bb 100644 --- a/src/api/util/utility/RandomInviteID.ts +++ b/src/api/util/utility/RandomInviteID.ts @@ -2,7 +2,8 @@ import { Snowflake } from "@fosscord/util"; export function random(length = 6) { // Declare all characters - let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; // Pick characers randomly let str = ""; @@ -15,18 +16,18 @@ export function random(length = 6) { export function snowflakeBasedInvite() { // Declare all characters - let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; let base = BigInt(chars.length); let snowflake = Snowflake.generateWorkerProcess(); // snowflakes hold ~10.75 characters worth of entropy; // safe to generate a 8-char invite out of them let str = ""; - for (let i=0; i < 10; i++) { - + for (let i = 0; i < 10; i++) { str.concat(chars.charAt(Number(snowflake % base))); snowflake = snowflake / base; } - - return str.substr(3,8).split("").reverse().join(""); + + return str.substr(3, 8).split("").reverse().join(""); } diff --git a/src/api/util/utility/String.ts b/src/api/util/utility/String.ts index 982b7e11..0913013e 100644 --- a/src/api/util/utility/String.ts +++ b/src/api/util/utility/String.ts @@ -2,13 +2,21 @@ import { Request } from "express"; import { ntob } from "./Base64"; import { FieldErrors } from "@fosscord/util"; -export function checkLength(str: string, min: number, max: number, key: string, req: Request) { +export function checkLength( + str: string, + min: number, + max: number, + key: string, + req: Request, +) { if (str.length < min || str.length > max) { throw FieldErrors({ [key]: { code: "BASE_TYPE_BAD_LENGTH", - message: req.t("common:field.BASE_TYPE_BAD_LENGTH", { length: `${min} - ${max}` }) - } + message: req.t("common:field.BASE_TYPE_BAD_LENGTH", { + length: `${min} - ${max}`, + }), + }, }); } } diff --git a/src/api/util/utility/captcha.ts b/src/api/util/utility/captcha.ts index 739647d2..50e2c91a 100644 --- a/src/api/util/utility/captcha.ts +++ b/src/api/util/utility/captcha.ts @@ -7,8 +7,8 @@ export interface hcaptchaResponse { hostname: string; credit: boolean; "error-codes": string[]; - score: number; // enterprise only - score_reason: string[]; // enterprise only + score: number; // enterprise only + score_reason: string[]; // enterprise only } export interface recaptchaResponse { @@ -23,7 +23,7 @@ export interface recaptchaResponse { const verifyEndpoints = { hcaptcha: "https://hcaptcha.com/siteverify", recaptcha: "https://www.google.com/recaptcha/api/siteverify", -} +}; export async function verifyCaptcha(response: string, ip?: string) { const { security } = Config.get(); @@ -36,11 +36,12 @@ export async function verifyCaptcha(response: string, ip?: string) { headers: { "Content-Type": "application/x-www-form-urlencoded", }, - body: `response=${encodeURIComponent(response)}` - + `&secret=${encodeURIComponent(secret!)}` - + `&sitekey=${encodeURIComponent(sitekey!)}` - + (ip ? `&remoteip=${encodeURIComponent(ip!)}` : ""), + body: + `response=${encodeURIComponent(response)}` + + `&secret=${encodeURIComponent(secret!)}` + + `&sitekey=${encodeURIComponent(sitekey!)}` + + (ip ? `&remoteip=${encodeURIComponent(ip!)}` : ""), }); - return await res.json() as hcaptchaResponse | recaptchaResponse; -} \ No newline at end of file + return (await res.json()) as hcaptchaResponse | recaptchaResponse; +} diff --git a/src/api/util/utility/ipAddress.ts b/src/api/util/utility/ipAddress.ts index f17b145e..d166ebc5 100644 --- a/src/api/util/utility/ipAddress.ts +++ b/src/api/util/utility/ipAddress.ts @@ -25,27 +25,27 @@ const exampleData = { name: "", domain: "", route: "", - type: "isp" + type: "isp", }, languages: [ { name: "", - native: "" - } + native: "", + }, ], currency: { name: "", code: "", symbol: "", native: "", - plural: "" + plural: "", }, time_zone: { name: "", abbr: "", offset: "", is_dst: true, - current_time: "" + current_time: "", }, threat: { is_tor: false, @@ -54,10 +54,10 @@ const exampleData = { is_known_attacker: false, is_known_abuser: false, is_threat: false, - is_bogon: false + is_bogon: false, }, count: 0, - status: 200 + status: 200, }; //TODO add function that support both ip and domain names @@ -65,7 +65,9 @@ export async function IPAnalysis(ip: string): Promise<typeof exampleData> { const { ipdataApiKey } = Config.get().security; if (!ipdataApiKey) return { ...exampleData, ip }; - return (await fetch(`https://api.ipdata.co/${ip}?api-key=${ipdataApiKey}`)).json() as any; // TODO: types + return ( + await fetch(`https://api.ipdata.co/${ip}?api-key=${ipdataApiKey}`) + ).json() as any; // TODO: types } export function isProxy(data: typeof exampleData) { @@ -77,19 +79,35 @@ export function isProxy(data: typeof exampleData) { } export function getIpAdress(req: Request): string { - // @ts-ignore - return req.headers[Config.get().security.forwadedFor] || req.socket.remoteAddress; + return ( + // @ts-ignore + req.headers[Config.get().security.forwadedFor] || + req.socket.remoteAddress + ); } export function distanceBetweenLocations(loc1: any, loc2: any): number { - return distanceBetweenCoords(loc1.latitude, loc1.longitude, loc2.latitude, loc2.longitude); + return distanceBetweenCoords( + loc1.latitude, + loc1.longitude, + loc2.latitude, + loc2.longitude, + ); } //Haversine function -function distanceBetweenCoords(lat1: number, lon1: number, lat2: number, lon2: number) { +function distanceBetweenCoords( + lat1: number, + lon1: number, + lat2: number, + lon2: number, +) { const p = 0.017453292519943295; // Math.PI / 180 const c = Math.cos; - const a = 0.5 - c((lat2 - lat1) * p) / 2 + (c(lat1 * p) * c(lat2 * p) * (1 - c((lon2 - lon1) * p))) / 2; + const a = + 0.5 - + c((lat2 - lat1) * p) / 2 + + (c(lat1 * p) * c(lat2 * p) * (1 - c((lon2 - lon1) * p))) / 2; return 12742 * Math.asin(Math.sqrt(a)); // 2 * R; R = 6371 km } diff --git a/src/api/util/utility/passwordStrength.ts b/src/api/util/utility/passwordStrength.ts index 439700d0..35c55999 100644 --- a/src/api/util/utility/passwordStrength.ts +++ b/src/api/util/utility/passwordStrength.ts @@ -18,7 +18,8 @@ const blocklist: string[] = []; // TODO: update ones passwordblocklist is stored * Returns: 0 > pw > 1 */ export function checkPassword(password: string): number { - const { minLength, minNumbers, minUpperCase, minSymbols } = Config.get().register.password; + const { minLength, minNumbers, minUpperCase, minSymbols } = + Config.get().register.password; var strength = 0; // checks for total password len @@ -42,19 +43,24 @@ export function checkPassword(password: string): number { } // checks if password only consists of numbers or only consists of chars - if (password.length == password.count(reNUMBER) || password.length === password.count(reUPPERCASELETTER)) { + if ( + password.length == password.count(reNUMBER) || + password.length === password.count(reUPPERCASELETTER) + ) { strength = 0; } - + let entropyMap: { [key: string]: number } = {}; for (let i = 0; i < password.length; i++) { if (entropyMap[password[i]]) entropyMap[password[i]]++; else entropyMap[password[i]] = 1; } - + let entropies = Object.values(entropyMap); - - entropies.map(x => (x / entropyMap.length)); - strength += entropies.reduceRight((a: number, x: number) => a - (x * Math.log2(x))) / Math.log2(password.length); + + entropies.map((x) => x / entropyMap.length); + strength += + entropies.reduceRight((a: number, x: number) => a - x * Math.log2(x)) / + Math.log2(password.length); return strength; } diff --git a/src/bundle/Server.ts b/src/bundle/Server.ts index c85daf40..dd75e777 100644 --- a/src/bundle/Server.ts +++ b/src/bundle/Server.ts @@ -22,7 +22,7 @@ const cdn = new CDNServer({ server, port, production, app }); const gateway = new Gateway.Server({ server, port, production }); //this is what has been added for the /stop API route -process.on('SIGTERM', () => { +process.on("SIGTERM", () => { server.close(() => { console.log("Stop API has been successfully POSTed, SIGTERM sent"); }); @@ -66,7 +66,9 @@ async function main() { //Sentry if (Config.get().sentry.enabled) { console.log( - `[Bundle] ${yellow("You are using Sentry! This may slightly impact performance on large loads!")}` + `[Bundle] ${yellow( + "You are using Sentry! This may slightly impact performance on large loads!", + )}`, ); Sentry.init({ dsn: Config.get().sentry.endpoint, @@ -81,7 +83,10 @@ async function main() { Sentry.addGlobalEventProcessor((event, hint) => { if (event.transaction) { - event.transaction = event.transaction.split("/").map(x => !parseInt(x) ? x : ":id").join("/"); + event.transaction = event.transaction + .split("/") + .map((x) => (!parseInt(x) ? x : ":id")) + .join("/"); } delete event.request?.cookies; @@ -93,14 +98,20 @@ async function main() { } if (event.breadcrumbs) { - event.breadcrumbs = event.breadcrumbs.filter(x => { + event.breadcrumbs = event.breadcrumbs.filter((x) => { if (x.message?.includes("identified as")) return false; if (x.message?.includes("[WebSocket] closed")) return false; - if (x.message?.includes("Got Resume -> cancel not implemented")) return false; - if (x.message?.includes("[Gateway] New connection from")) return false; + if ( + x.message?.includes( + "Got Resume -> cancel not implemented", + ) + ) + return false; + if (x.message?.includes("[Gateway] New connection from")) + return false; return true; - }) + }); } return event; diff --git a/src/bundle/index.ts b/src/bundle/index.ts index 960d4dc0..45f084c5 100644 --- a/src/bundle/index.ts +++ b/src/bundle/index.ts @@ -1,4 +1,4 @@ export * from "@fosscord/api"; export * from "@fosscord/util"; export * from "@fosscord/gateway"; -export * from "@fosscord/cdn"; \ No newline at end of file +export * from "@fosscord/cdn"; diff --git a/src/bundle/start.ts b/src/bundle/start.ts index 2a1e6520..0c85b58d 100644 --- a/src/bundle/start.ts +++ b/src/bundle/start.ts @@ -1,5 +1,5 @@ // process.env.MONGOMS_DEBUG = "true"; -require('module-alias/register'); +require("module-alias/register"); import "reflect-metadata"; import cluster, { Worker } from "cluster"; import os from "os"; @@ -37,18 +37,20 @@ if (cluster.isMaster) { ╚═╝ ╚═════╝ ╚══════╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═════╝ fosscord-server | ${yellow( - `Pre-release (${commit !== null - ? commit.slice(0, 7) - : "Unknown (Git cannot be found)" - })` + `Pre-release (${ + commit !== null + ? commit.slice(0, 7) + : "Unknown (Git cannot be found)" + })`, )} -Commit Hash: ${commit !== null +Commit Hash: ${ + commit !== null ? `${cyan(commit)} (${yellow(commit.slice(0, 7))})` : "Unknown (Git cannot be found)" - } + } Cores: ${cyan(os.cpus().length)} (Using ${cores} thread(s).) -`) +`), ); if (commit == null) { @@ -85,8 +87,8 @@ Cores: ${cyan(os.cpus().length)} (Using ${cores} thread(s).) cluster.on("exit", (worker: any, code: any, signal: any) => { console.log( `[Worker] ${red( - `died with PID: ${worker.process.pid} , restarting ...` - )}` + `died with PID: ${worker.process.pid} , restarting ...`, + )}`, ); cluster.fork(); }); diff --git a/src/bundle/stats.ts b/src/bundle/stats.ts index 0234e0b4..3a9b2d85 100644 --- a/src/bundle/stats.ts +++ b/src/bundle/stats.ts @@ -6,18 +6,17 @@ export function initStats() { console.log(`[Path] running in ${__dirname}`); try { console.log(`[CPU] ${osu.cpu.model()} Cores x${osu.cpu.count()}`); + } catch { + console.log("[CPU] Failed to get cpu model!"); } - catch { - console.log('[CPU] Failed to get cpu model!') - } - + console.log(`[System] ${os.platform()} ${os.arch()}`); console.log(`[Process] running with PID: ${process.pid}`); if (process.getuid && process.getuid() === 0) { console.warn( red( - `[Process] Warning fosscord is running as root, this highly discouraged and might expose your system vulnerable to attackers. Please run fosscord as a user without root privileges.` - ) + `[Process] Warning fosscord is running as root, this highly discouraged and might expose your system vulnerable to attackers. Please run fosscord as a user without root privileges.`, + ), ); } diff --git a/src/cdn/Server.ts b/src/cdn/Server.ts index 5b395589..f7e6dbdc 100644 --- a/src/cdn/Server.ts +++ b/src/cdn/Server.ts @@ -5,7 +5,7 @@ import avatarsRoute from "./routes/avatars"; import iconsRoute from "./routes/role-icons"; import bodyParser from "body-parser"; -export interface CDNServerOptions extends ServerOptions { } +export interface CDNServerOptions extends ServerOptions {} export class CDNServer extends Server { public declare options: CDNServerOptions; @@ -22,15 +22,15 @@ export class CDNServer extends Server { // TODO: use better CSP policy res.set( "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") || "*" + req.header("Access-Control-Request-Headers") || "*", ); res.set( "Access-Control-Allow-Methods", - req.header("Access-Control-Request-Methods") || "*" + req.header("Access-Control-Request-Methods") || "*", ); next(); }); diff --git a/src/cdn/routes/attachments.ts b/src/cdn/routes/attachments.ts index ae50bc48..2a1b6f09 100644 --- a/src/cdn/routes/attachments.ts +++ b/src/cdn/routes/attachments.ts @@ -56,7 +56,7 @@ router.post( }; return res.json(file); - } + }, ); router.get( @@ -65,7 +65,7 @@ router.get( const { channel_id, id, filename } = req.params; const file = await storage.get( - `attachments/${channel_id}/${id}/${filename}` + `attachments/${channel_id}/${id}/${filename}`, ); if (!file) throw new HTTPError("File not found"); const type = await FileType.fromBuffer(file); @@ -79,7 +79,7 @@ router.get( res.set("Cache-Control", "public, max-age=31536000"); return res.send(file); - } + }, ); router.delete( @@ -94,7 +94,7 @@ router.delete( await storage.delete(path); return res.send({ success: true }); - } + }, ); export default router; diff --git a/src/cdn/routes/avatars.ts b/src/cdn/routes/avatars.ts index e5e25a4c..50a76d4b 100644 --- a/src/cdn/routes/avatars.ts +++ b/src/cdn/routes/avatars.ts @@ -55,7 +55,7 @@ router.post( size, url: `${endpoint}${req.baseUrl}/${user_id}/${hash}`, }); - } + }, ); router.get("/:user_id", async (req: Request, res: Response) => { @@ -86,7 +86,7 @@ export const getAvatar = async (req: Request, res: Response) => { res.set("Cache-Control", "public, max-age=31536000"); return res.send(file); -} +}; router.get("/:user_id/:hash", getAvatar); diff --git a/src/cdn/routes/external.ts b/src/cdn/routes/external.ts index cb17ff9b..405e665e 100644 --- a/src/cdn/routes/external.ts +++ b/src/cdn/routes/external.ts @@ -66,14 +66,14 @@ router.get("/resize/:url", async (req: Request, res: Response) => { const { resizeHeightMax, resizeWidthMax } = Config.get().cdn; const w = Math.min(parseInt(width as string), resizeWidthMax ?? 100); const h = Math.min(parseInt(height as string), resizeHeightMax ?? 100); - if (w < 1 || h < 1) throw new HTTPError("Width and height must be greater than 0"); + if (w < 1 || h < 1) + throw new HTTPError("Width and height must be greater than 0"); let buffer, response; try { response = await fetch(url, DEFAULT_FETCH_OPTIONS); buffer = await response.buffer(); - } - catch (e) { + } catch (e) { throw new HTTPError("Couldn't fetch website"); } @@ -84,7 +84,10 @@ router.get("/resize/:url", async (req: Request, res: Response) => { .toBuffer(); res.setHeader("Content-Disposition", "attachment"); - res.setHeader("Content-Type", response.headers.get("content-type") ?? "image/png"); + res.setHeader( + "Content-Type", + response.headers.get("content-type") ?? "image/png", + ); return res.end(resizedBuffer); }); diff --git a/src/cdn/routes/guilds.ts b/src/cdn/routes/guilds.ts index 3c4b646c..6f0719b6 100644 --- a/src/cdn/routes/guilds.ts +++ b/src/cdn/routes/guilds.ts @@ -7,4 +7,4 @@ const router = Router(); router.get("/:guild_id/users/:user_id/avatars/:hash", getAvatar); router.get("/:guild_id/users/:user_id/banners/:hash", getAvatar); -export default router; \ No newline at end of file +export default router; diff --git a/src/cdn/routes/role-icons.ts b/src/cdn/routes/role-icons.ts index 12aae8a4..bdfa0355 100644 --- a/src/cdn/routes/role-icons.ts +++ b/src/cdn/routes/role-icons.ts @@ -54,7 +54,7 @@ router.post( size, url: `${endpoint}${req.baseUrl}/${role_id}/${hash}`, }); - } + }, ); router.get("/:role_id", async (req: Request, res: Response) => { diff --git a/src/cdn/start.ts b/src/cdn/start.ts index 1fdea22e..c22984fa 100644 --- a/src/cdn/start.ts +++ b/src/cdn/start.ts @@ -1,4 +1,4 @@ -require('module-alias/register') +require("module-alias/register"); import dotenv from "dotenv"; dotenv.config(); diff --git a/src/cdn/util/S3Storage.ts b/src/cdn/util/S3Storage.ts index c4066817..33c11265 100644 --- a/src/cdn/util/S3Storage.ts +++ b/src/cdn/util/S3Storage.ts @@ -14,7 +14,7 @@ export class S3Storage implements Storage { public constructor( private client: S3, private bucket: string, - private basePath?: string + private basePath?: string, ) {} /** diff --git a/src/cdn/util/Storage.ts b/src/cdn/util/Storage.ts index d040f50b..d66cb2dc 100644 --- a/src/cdn/util/Storage.ts +++ b/src/cdn/util/Storage.ts @@ -33,14 +33,14 @@ if (process.env.STORAGE_PROVIDER === "file" || !process.env.STORAGE_PROVIDER) { if (!region) { console.error( - `[CDN] You must provide a region when using the S3 storage provider.` + `[CDN] You must provide a region when using the S3 storage provider.`, ); process.exit(1); } if (!bucket) { console.error( - `[CDN] You must provide a bucket when using the S3 storage provider.` + `[CDN] You must provide a bucket when using the S3 storage provider.`, ); process.exit(1); } @@ -50,7 +50,7 @@ if (process.env.STORAGE_PROVIDER === "file" || !process.env.STORAGE_PROVIDER) { if (!location) { console.warn( - `[CDN] STORAGE_LOCATION unconfigured for S3 provider, defaulting to the bucket root...` + `[CDN] STORAGE_LOCATION unconfigured for S3 provider, defaulting to the bucket root...`, ); location = undefined; } diff --git a/src/gateway/events/Connection.ts b/src/gateway/events/Connection.ts index bed3cf44..8747e3ad 100644 --- a/src/gateway/events/Connection.ts +++ b/src/gateway/events/Connection.ts @@ -21,10 +21,12 @@ try { export async function Connection( this: WS.Server, socket: WebSocket, - request: IncomingMessage + request: IncomingMessage, ) { const forwardedFor = Config.get().security.forwadedFor; - const ipAddress = forwardedFor ? request.headers[forwardedFor] as string : request.socket.remoteAddress; + const ipAddress = forwardedFor + ? (request.headers[forwardedFor] as string) + : request.socket.remoteAddress; socket.ipAddress = ipAddress; @@ -33,7 +35,9 @@ export async function Connection( socket.on("close", Close); // @ts-ignore socket.on("message", Message); - console.log(`[Gateway] New connection from ${socket.ipAddress}, total ${this.clients.size}`); + console.log( + `[Gateway] New connection from ${socket.ipAddress}, total ${this.clients.size}`, + ); const { searchParams } = new URL(`http://localhost${request.url}`); // @ts-ignore @@ -41,7 +45,7 @@ export async function Connection( if (!["json", "etf"].includes(socket.encoding)) { if (socket.encoding === "etf" && erlpack) { throw new Error( - "Erlpack is not installed: 'npm i @yukikaze-bot/erlpack'" + "Erlpack is not installed: 'npm i @yukikaze-bot/erlpack'", ); } return socket.close(CLOSECODES.Decode_error); diff --git a/src/gateway/events/Message.ts b/src/gateway/events/Message.ts index 4699f1af..603f68fa 100644 --- a/src/gateway/events/Message.ts +++ b/src/gateway/events/Message.ts @@ -3,7 +3,7 @@ import { WebSocket, Payload } from "@fosscord/gateway"; var erlpack: any; try { erlpack = require("@yukikaze-bot/erlpack"); -} catch (error) { } +} catch (error) {} import OPCodeHandlers from "../opcodes"; import { Tuple } from "lambert-server"; import { check } from "../opcodes/instanceOf"; @@ -34,11 +34,9 @@ export async function Message(this: WebSocket, buffer: WS.Data) { } } data = bigIntJson.parse(buffer as string); - } - else if (typeof buffer == "string") { + } else if (typeof buffer == "string") { data = bigIntJson.parse(buffer as string); - } - else return; + } else return; check.call(this, PayloadSchema, data); diff --git a/src/gateway/listener/listener.ts b/src/gateway/listener/listener.ts index 72dd9d5b..1d9caebb 100644 --- a/src/gateway/listener/listener.ts +++ b/src/gateway/listener/listener.ts @@ -26,7 +26,7 @@ import { Recipient } from "@fosscord/util"; export function handlePresenceUpdate( this: WebSocket, - { event, acknowledge, data }: EventOpts + { event, acknowledge, data }: EventOpts, ) { acknowledge?.(); if (event === EVENTEnum.PresenceUpdate) { @@ -54,14 +54,14 @@ export async function setupListener(this: WebSocket) { where: { from_id: this.user_id, type: RelationshipType.friends, - } + }, }), ]); const guilds = members.map((x) => x.guild); const dm_channels = recipients.map((x) => x.channel); - const opts: { acknowledge: boolean; channel?: AMQChannel; } = { + const opts: { acknowledge: boolean; channel?: AMQChannel } = { acknowledge: true, }; this.listen_options = opts; @@ -79,7 +79,7 @@ export async function setupListener(this: WebSocket) { this.events[relationship.to_id] = await listenEvent( relationship.to_id, handlePresenceUpdate.bind(this), - opts + opts, ); }); @@ -101,7 +101,7 @@ export async function setupListener(this: WebSocket) { this.events[channel.id] = await listenEvent( channel.id, consumer, - opts + opts, ); } }); @@ -137,7 +137,7 @@ async function consume(this: WebSocket, opts: EventOpts) { this.member_events[data.user.id] = await listenEvent( data.user.id, handlePresenceUpdate.bind(this), - this.listen_options + this.listen_options, ); break; case "GUILD_MEMBER_REMOVE": @@ -164,7 +164,7 @@ async function consume(this: WebSocket, opts: EventOpts) { this.events[data.user.id] = await listenEvent( data.user.id, handlePresenceUpdate.bind(this), - this.listen_options + this.listen_options, ); break; case "GUILD_CREATE": diff --git a/src/gateway/opcodes/Identify.ts b/src/gateway/opcodes/Identify.ts index b4b36075..c5c78f1a 100644 --- a/src/gateway/opcodes/Identify.ts +++ b/src/gateway/opcodes/Identify.ts @@ -158,11 +158,13 @@ export async function onIdentify(this: WebSocket, data: Payload) { ...x.settings, guild_id: x.guild.id, // disgusting - channel_overrides: Object.entries(x.settings.channel_overrides ?? {}).map(y => ({ + channel_overrides: Object.entries( + x.settings.channel_overrides ?? {}, + ).map((y) => ({ ...y[1], channel_id: y[0], - })) - })) as any as UserGuildSettings[]; // VERY disgusting. don't care. + })), + })) as any as UserGuildSettings[]; // VERY disgusting. don't care. const channels = recipients.map((x) => { // @ts-ignore @@ -171,7 +173,7 @@ export async function onIdentify(this: WebSocket, data: Payload) { users = users.concat(x.channel.recipients as unknown as User[]); if (x.channel.isDm()) { x.channel.recipients = x.channel.recipients!.filter( - (x) => x.id !== this.user_id + (x) => x.id !== this.user_id, ); } return x.channel; @@ -243,7 +245,7 @@ export async function onIdentify(this: WebSocket, data: Payload) { accent_color: user.accent_color, banner: user.banner, bio: user.bio, - premium_since: user.premium_since + premium_since: user.premium_since, }; const d: ReadyEventData = { diff --git a/src/gateway/opcodes/LazyRequest.ts b/src/gateway/opcodes/LazyRequest.ts index 0f21d087..f5bbad14 100644 --- a/src/gateway/opcodes/LazyRequest.ts +++ b/src/gateway/opcodes/LazyRequest.ts @@ -1,5 +1,19 @@ -import { getDatabase, getPermission, listenEvent, Member, Role, Session, LazyRequestSchema } from "@fosscord/util"; -import { WebSocket, Payload, handlePresenceUpdate, OPCODES, Send } from "@fosscord/gateway"; +import { + getDatabase, + getPermission, + listenEvent, + Member, + Role, + Session, + LazyRequestSchema, +} from "@fosscord/util"; +import { + WebSocket, + Payload, + handlePresenceUpdate, + OPCODES, + Send, +} from "@fosscord/gateway"; import { check } from "./instanceOf"; // TODO: only show roles/members that have access to this channel @@ -14,7 +28,8 @@ async function getMembers(guild_id: string, range: [number, number]) { let members: Member[] = []; try { - members = await getDatabase()!.getRepository(Member) + members = await getDatabase()! + .getRepository(Member) .createQueryBuilder("member") .where("member.guild_id = :guild_id", { guild_id }) .leftJoinAndSelect("member.roles", "role") @@ -23,7 +38,7 @@ async function getMembers(guild_id: string, range: [number, number]) { .addSelect("user.settings") .addSelect( "CASE WHEN session.status = 'offline' THEN 0 ELSE 1 END", - "_status" + "_status", ) .orderBy("role.position", "DESC") .addOrderBy("_status", "DESC") @@ -31,8 +46,7 @@ async function getMembers(guild_id: string, range: [number, number]) { .offset(Number(range[0]) || 0) .limit(Number(range[1]) || 100) .getMany(); - } - catch (e) { + } catch (e) { console.error(`LazyRequest`, e); } @@ -51,14 +65,20 @@ async function getMembers(guild_id: string, range: [number, number]) { .map((m) => m.roles) .flat() .unique((r: Role) => r.id); - member_roles.push(member_roles.splice(member_roles.findIndex(x => x.id === x.guild_id), 1)[0]); + member_roles.push( + member_roles.splice( + member_roles.findIndex((x) => x.id === x.guild_id), + 1, + )[0], + ); const offlineItems = []; for (const role of member_roles) { // @ts-ignore - const [role_members, other_members]: Member[][] = partition(members, (m: Member) => - m.roles.find((r) => r.id === role.id) + const [role_members, other_members]: Member[][] = partition( + members, + (m: Member) => m.roles.find((r) => r.id === role.id), ); const group = { count: role_members.length, @@ -74,15 +94,19 @@ async function getMembers(guild_id: string, range: [number, number]) { .map((x: Role) => x.id); const statusMap = { - "online": 0, - "idle": 1, - "dnd": 2, - "invisible": 3, - "offline": 4, + online: 0, + idle: 1, + dnd: 2, + invisible: 3, + offline: 4, }; // sort sessions by relevance const sessions = member.user.sessions.sort((a, b) => { - return (statusMap[a.status] - statusMap[b.status]) + ((a.activities.length - b.activities.length) * 2); + return ( + statusMap[a.status] - + statusMap[b.status] + + (a.activities.length - b.activities.length) * 2 + ); }); var session: Session | undefined = sessions.first(); @@ -103,7 +127,11 @@ async function getMembers(guild_id: string, range: [number, number]) { }, }; - if (!session || session.status == "invisible" || session.status == "offline") { + if ( + !session || + session.status == "invisible" || + session.status == "offline" + ) { item.member.presence.status = "offline"; offlineItems.push(item); group.count--; @@ -130,7 +158,9 @@ async function getMembers(guild_id: string, range: [number, number]) { items, groups, range, - members: items.map((x) => 'member' in x ? x.member : undefined).filter(x => !!x), + members: items + .map((x) => ("member" in x ? x.member : undefined)) + .filter((x) => !!x), }; } @@ -161,7 +191,7 @@ export async function onLazyRequest(this: WebSocket, { d }: Payload) { this.member_events[member.user.id] = await listenEvent( member.user.id, handlePresenceUpdate.bind(this), - this.listen_options + this.listen_options, ); }); }); @@ -181,7 +211,9 @@ export async function onLazyRequest(this: WebSocket, { d }: Payload) { op: "SYNC", range: x.range, })), - online_count: member_count - (groups.find(x => x.id == "offline")?.count ?? 0), + online_count: + member_count - + (groups.find((x) => x.id == "offline")?.count ?? 0), member_count, id: "everyone", guild_id, @@ -199,6 +231,6 @@ function partition<T>(array: T[], isValid: Function) { ? [[...pass, elem], fail] : [pass, [...fail, elem]]; }, - [[], []] + [[], []], ); } diff --git a/src/gateway/opcodes/PresenceUpdate.ts b/src/gateway/opcodes/PresenceUpdate.ts index d17b7dd7..37299213 100644 --- a/src/gateway/opcodes/PresenceUpdate.ts +++ b/src/gateway/opcodes/PresenceUpdate.ts @@ -1,5 +1,11 @@ import { WebSocket, Payload } from "@fosscord/gateway"; -import { emitEvent, PresenceUpdateEvent, Session, User, ActivitySchema } from "@fosscord/util"; +import { + emitEvent, + PresenceUpdateEvent, + Session, + User, + ActivitySchema, +} from "@fosscord/util"; import { check } from "./instanceOf"; export async function onPresenceUpdate(this: WebSocket, { d }: Payload) { @@ -8,7 +14,7 @@ export async function onPresenceUpdate(this: WebSocket, { d }: Payload) { await Session.update( { session_id: this.session_id }, - { status: presence.status, activities: presence.activities } + { status: presence.status, activities: presence.activities }, ); await emitEvent({ diff --git a/src/gateway/opcodes/VoiceStateUpdate.ts b/src/gateway/opcodes/VoiceStateUpdate.ts index 8e1585ec..17ed7e4f 100644 --- a/src/gateway/opcodes/VoiceStateUpdate.ts +++ b/src/gateway/opcodes/VoiceStateUpdate.ts @@ -87,16 +87,18 @@ export async function onVoiceStateUpdate(this: WebSocket, data: Payload) { //If it's null it means that we are leaving the channel and this event is not needed if (voiceState.channel_id !== null) { - const guild = await Guild.findOne({ where: { id: voiceState.guild_id } }); + const guild = await Guild.findOne({ + where: { id: voiceState.guild_id }, + }); const regions = Config.get().regions; let guildRegion: Region; if (guild && guild.region) { guildRegion = regions.available.filter( - (r) => r.id === guild.region + (r) => r.id === guild.region, )[0]; } else { guildRegion = regions.available.filter( - (r) => r.id === regions.default + (r) => r.id === regions.default, )[0]; } diff --git a/src/gateway/start.ts b/src/gateway/start.ts index 90d7f34e..84de674f 100644 --- a/src/gateway/start.ts +++ b/src/gateway/start.ts @@ -1,4 +1,4 @@ -require('module-alias/register'); +require("module-alias/register"); process.on("uncaughtException", console.error); process.on("unhandledRejection", console.error); diff --git a/src/gateway/util/Send.ts b/src/gateway/util/Send.ts index e1460846..1c0f33c3 100644 --- a/src/gateway/util/Send.ts +++ b/src/gateway/util/Send.ts @@ -2,7 +2,9 @@ var erlpack: any; try { erlpack = require("@yukikaze-bot/erlpack"); } catch (error) { - console.log("Missing @yukikaze-bot/erlpack, electron-based desktop clients designed for discord.com will not be able to connect!"); + console.log( + "Missing @yukikaze-bot/erlpack, electron-based desktop clients designed for discord.com will not be able to connect!", + ); } import { Payload, WebSocket } from "@fosscord/gateway"; diff --git a/src/util/dtos/DmChannelDTO.ts b/src/util/dtos/DmChannelDTO.ts index 226b2f9d..fcc91204 100644 --- a/src/util/dtos/DmChannelDTO.ts +++ b/src/util/dtos/DmChannelDTO.ts @@ -11,7 +11,11 @@ export class DmChannelDTO { recipients: MinimalPublicUserDTO[]; type: number; - static async from(channel: Channel, excluded_recipients: string[] = [], origin_channel_id?: string) { + static async from( + channel: Channel, + excluded_recipients: string[] = [], + origin_channel_id?: string, + ) { const obj = new DmChannelDTO(); obj.icon = channel.icon || null; obj.id = channel.id; @@ -23,10 +27,15 @@ export class DmChannelDTO { obj.recipients = ( await Promise.all( channel - .recipients!.filter((r) => !excluded_recipients.includes(r.user_id)) + .recipients!.filter( + (r) => !excluded_recipients.includes(r.user_id), + ) .map(async (r) => { - return await User.findOneOrFail({ where: { id: r.user_id }, select: PublicUserProjection }); - }) + return await User.findOneOrFail({ + where: { id: r.user_id }, + select: PublicUserProjection, + }); + }), ) ).map((u) => new MinimalPublicUserDTO(u)); return obj; @@ -35,7 +44,9 @@ export class DmChannelDTO { excludedRecipients(excluded_recipients: string[]): DmChannelDTO { return { ...this, - recipients: this.recipients.filter((r) => !excluded_recipients.includes(r.id)), + recipients: this.recipients.filter( + (r) => !excluded_recipients.includes(r.id), + ), }; } } diff --git a/src/util/entities/Attachment.ts b/src/util/entities/Attachment.ts index 7b4b17eb..055b6f4b 100644 --- a/src/util/entities/Attachment.ts +++ b/src/util/entities/Attachment.ts @@ -1,4 +1,11 @@ -import { BeforeRemove, Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import { + BeforeRemove, + Column, + Entity, + JoinColumn, + ManyToOne, + RelationId, +} from "typeorm"; import { URL } from "url"; import { deleteFile } from "../util/cdn"; import { BaseClass } from "./BaseClass"; @@ -31,9 +38,13 @@ export class Attachment extends BaseClass { message_id: string; @JoinColumn({ name: "message_id" }) - @ManyToOne(() => require("./Message").Message, (message: import("./Message").Message) => message.attachments, { - onDelete: "CASCADE", - }) + @ManyToOne( + () => require("./Message").Message, + (message: import("./Message").Message) => message.attachments, + { + onDelete: "CASCADE", + }, + ) message: import("./Message").Message; @BeforeRemove() diff --git a/src/util/entities/AuditLog.ts b/src/util/entities/AuditLog.ts index b003e7ba..9cc97742 100644 --- a/src/util/entities/AuditLog.ts +++ b/src/util/entities/AuditLog.ts @@ -5,24 +5,24 @@ import { User } from "./User"; export enum AuditLogEvents { // guild level - GUILD_UPDATE = 1, + GUILD_UPDATE = 1, GUILD_IMPORT = 2, GUILD_EXPORTED = 3, GUILD_ARCHIVE = 4, GUILD_UNARCHIVE = 5, // join-leave - USER_JOIN = 6, + USER_JOIN = 6, USER_LEAVE = 7, // channels - CHANNEL_CREATE = 10, + CHANNEL_CREATE = 10, CHANNEL_UPDATE = 11, CHANNEL_DELETE = 12, // permission overrides - CHANNEL_OVERWRITE_CREATE = 13, + CHANNEL_OVERWRITE_CREATE = 13, CHANNEL_OVERWRITE_UPDATE = 14, CHANNEL_OVERWRITE_DELETE = 15, // kick and ban - MEMBER_KICK = 20, + MEMBER_KICK = 20, MEMBER_PRUNE = 21, MEMBER_BAN_ADD = 22, MEMBER_BAN_REMOVE = 23, @@ -79,17 +79,17 @@ export enum AuditLogEvents { // application commands APPLICATION_COMMAND_PERMISSION_UPDATE = 121, // automod - POLICY_CREATE = 140, + POLICY_CREATE = 140, POLICY_UPDATE = 141, POLICY_DELETE = 142, - MESSAGE_BLOCKED_BY_POLICIES = 143, // in fosscord, blocked messages are stealth-dropped + MESSAGE_BLOCKED_BY_POLICIES = 143, // in fosscord, blocked messages are stealth-dropped // instance policies affecting the guild GUILD_AFFECTED_BY_POLICIES = 216, // message moves IN_GUILD_MESSAGE_MOVE = 223, CROSS_GUILD_MESSAGE_MOVE = 224, // message routing - ROUTE_CREATE = 225, + ROUTE_CREATE = 225, ROUTE_UPDATE = 226, } diff --git a/src/util/entities/BackupCodes.ts b/src/util/entities/BackupCodes.ts index d532a39a..81cdbb6d 100644 --- a/src/util/entities/BackupCodes.ts +++ b/src/util/entities/BackupCodes.ts @@ -24,7 +24,7 @@ export function generateMfaBackupCodes(user_id: string) { for (let i = 0; i < 10; i++) { const code = BackupCode.create({ user: { id: user_id }, - code: crypto.randomBytes(4).toString("hex"), // 8 characters + code: crypto.randomBytes(4).toString("hex"), // 8 characters consumed: false, expired: false, }); @@ -32,4 +32,4 @@ export function generateMfaBackupCodes(user_id: string) { } return backup_codes; -} \ No newline at end of file +} diff --git a/src/util/entities/BaseClass.ts b/src/util/entities/BaseClass.ts index d5a7c2bf..9942b60e 100644 --- a/src/util/entities/BaseClass.ts +++ b/src/util/entities/BaseClass.ts @@ -1,5 +1,12 @@ import "reflect-metadata"; -import { BaseEntity, BeforeInsert, BeforeUpdate, FindOptionsWhere, ObjectIdColumn, PrimaryColumn } from "typeorm"; +import { + BaseEntity, + BeforeInsert, + BeforeUpdate, + FindOptionsWhere, + ObjectIdColumn, + PrimaryColumn, +} from "typeorm"; import { Snowflake } from "../util/Snowflake"; import "missing-native-js-functions"; import { getDatabase } from ".."; @@ -22,23 +29,40 @@ export class BaseClassWithoutId extends BaseEntity { toJSON(): any { return Object.fromEntries( this.metadata!.columns // @ts-ignore - .map((x) => [x.propertyName, this[x.propertyName]]) // @ts-ignore - .concat(this.metadata.relations.map((x) => [x.propertyName, this[x.propertyName]])) + .map((x) => [x.propertyName, this[x.propertyName]]) + .concat( + // @ts-ignore + this.metadata.relations.map((x) => [ + x.propertyName, + // @ts-ignore + this[x.propertyName], + ]), + ), ); } - static increment<T extends BaseClass>(conditions: FindOptionsWhere<T>, propertyPath: string, value: number | string) { + static increment<T extends BaseClass>( + conditions: FindOptionsWhere<T>, + propertyPath: string, + value: number | string, + ) { const repository = this.getRepository(); return repository.increment(conditions, propertyPath, value); } - static decrement<T extends BaseClass>(conditions: FindOptionsWhere<T>, propertyPath: string, value: number | string) { + static decrement<T extends BaseClass>( + conditions: FindOptionsWhere<T>, + propertyPath: string, + value: number | string, + ) { const repository = this.getRepository(); return repository.decrement(conditions, propertyPath, value); } } -export const PrimaryIdColumn = process.env.DATABASE?.startsWith("mongodb") ? ObjectIdColumn : PrimaryColumn; +export const PrimaryIdColumn = process.env.DATABASE?.startsWith("mongodb") + ? ObjectIdColumn + : PrimaryColumn; export class BaseClass extends BaseClassWithoutId { @PrimaryIdColumn() diff --git a/src/util/entities/Categories.ts b/src/util/entities/Categories.ts index 81fbc303..f12b237d 100644 --- a/src/util/entities/Categories.ts +++ b/src/util/entities/Categories.ts @@ -1,4 +1,4 @@ -import { PrimaryColumn, Column, Entity} from "typeorm"; +import { PrimaryColumn, Column, Entity } from "typeorm"; import { BaseClassWithoutId } from "./BaseClass"; // TODO: categories: @@ -16,18 +16,18 @@ import { BaseClassWithoutId } from "./BaseClass"; // Also populate discord default categories @Entity("categories") -export class Categories extends BaseClassWithoutId { // Not using snowflake - - @PrimaryColumn() - id: number; +export class Categories extends BaseClassWithoutId { + // Not using snowflake - @Column({ nullable: true }) - name: string; + @PrimaryColumn() + id: number; - @Column({ type: "simple-json" }) - localizations: string; + @Column({ nullable: true }) + name: string; - @Column({ nullable: true }) - is_primary: boolean; + @Column({ type: "simple-json" }) + localizations: string; -} \ No newline at end of file + @Column({ nullable: true }) + is_primary: boolean; +} diff --git a/src/util/entities/Channel.ts b/src/util/entities/Channel.ts index 2200bfa3..14f36857 100644 --- a/src/util/entities/Channel.ts +++ b/src/util/entities/Channel.ts @@ -1,389 +1,457 @@ -import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm"; -import { BaseClass } from "./BaseClass"; -import { Guild } from "./Guild"; -import { PublicUserProjection, User } from "./User"; -import { HTTPError } from "lambert-server"; -import { containsAll, emitEvent, getPermission, Snowflake, trimSpecial, InvisibleCharacters, ChannelTypes } from "../util"; -import { ChannelCreateEvent, ChannelRecipientRemoveEvent } from "../interfaces"; -import { Recipient } from "./Recipient"; -import { Message } from "./Message"; -import { ReadState } from "./ReadState"; -import { Invite } from "./Invite"; -import { VoiceState } from "./VoiceState"; -import { Webhook } from "./Webhook"; -import { DmChannelDTO } from "../dtos"; - -export enum ChannelType { - GUILD_TEXT = 0, // a text channel within a guild - DM = 1, // a direct message between users - GUILD_VOICE = 2, // a voice channel within a guild - GROUP_DM = 3, // a direct message between multiple users - GUILD_CATEGORY = 4, // an organizational category that contains zero or more channels - GUILD_NEWS = 5, // a channel that users can follow and crosspost into a guild or route - GUILD_STORE = 6, // a channel in which game developers can sell their things - ENCRYPTED = 7, // end-to-end encrypted channel - ENCRYPTED_THREAD = 8, // end-to-end encrypted thread channel - TRANSACTIONAL = 9, // event chain style transactional channel - GUILD_NEWS_THREAD = 10, // a temporary sub-channel within a GUILD_NEWS channel - GUILD_PUBLIC_THREAD = 11, // a temporary sub-channel within a GUILD_TEXT channel - GUILD_PRIVATE_THREAD = 12, // a temporary sub-channel within a GUILD_TEXT channel that is only viewable by those invited and those with the MANAGE_THREADS permission - GUILD_STAGE_VOICE = 13, // a voice channel for hosting events with an audience - DIRECTORY = 14, // guild directory listing channel - GUILD_FORUM = 15, // forum composed of IM threads - TICKET_TRACKER = 33, // ticket tracker, individual ticket items shall have type 12 - KANBAN = 34, // confluence like kanban board - VOICELESS_WHITEBOARD = 35, // whiteboard but without voice (whiteboard + voice is the same as stage) - CUSTOM_START = 64, // start custom channel types from here - UNHANDLED = 255 // unhandled unowned pass-through channel type -} - -@Entity("channels") -export class Channel extends BaseClass { - @Column() - created_at: Date; - - @Column({ nullable: true }) - name?: string; - - @Column({ type: "text", nullable: true }) - icon?: string | null; - - @Column({ type: "int" }) - type: ChannelType; - - @OneToMany(() => Recipient, (recipient: Recipient) => recipient.channel, { - cascade: true, - orphanedRowAction: "delete", - }) - recipients?: Recipient[]; - - @Column({ nullable: true }) - last_message_id?: string; - - @Column({ nullable: true }) - @RelationId((channel: Channel) => channel.guild) - guild_id?: string; - - @JoinColumn({ name: "guild_id" }) - @ManyToOne(() => Guild, { - onDelete: "CASCADE", - }) - guild: Guild; - - @Column({ nullable: true }) - @RelationId((channel: Channel) => channel.parent) - parent_id: string; - - @JoinColumn({ name: "parent_id" }) - @ManyToOne(() => Channel) - parent?: Channel; - - // for group DMs and owned custom channel types - @Column({ nullable: true }) - @RelationId((channel: Channel) => channel.owner) - owner_id?: string; - - @JoinColumn({ name: "owner_id" }) - @ManyToOne(() => User) - owner: User; - - @Column({ nullable: true }) - last_pin_timestamp?: number; - - @Column({ nullable: true }) - default_auto_archive_duration?: number; - - @Column({ nullable: true }) - position?: number; - - @Column({ type: "simple-json", nullable: true }) - permission_overwrites?: ChannelPermissionOverwrite[]; - - @Column({ nullable: true }) - video_quality_mode?: number; - - @Column({ nullable: true }) - bitrate?: number; - - @Column({ nullable: true }) - user_limit?: number; - - @Column() - nsfw: boolean = false; - - @Column({ nullable: true }) - rate_limit_per_user?: number; - - @Column({ nullable: true }) - topic?: string; - - @OneToMany(() => Invite, (invite: Invite) => invite.channel, { - cascade: true, - orphanedRowAction: "delete", - }) - invites?: Invite[]; - - @Column({ nullable: true }) - retention_policy_id?: string; - - @OneToMany(() => Message, (message: Message) => message.channel, { - cascade: true, - orphanedRowAction: "delete", - }) - messages?: Message[]; - - @OneToMany(() => VoiceState, (voice_state: VoiceState) => voice_state.channel, { - cascade: true, - orphanedRowAction: "delete", - }) - voice_states?: VoiceState[]; - - @OneToMany(() => ReadState, (read_state: ReadState) => read_state.channel, { - cascade: true, - orphanedRowAction: "delete", - }) - read_states?: ReadState[]; - - @OneToMany(() => Webhook, (webhook: Webhook) => webhook.channel, { - cascade: true, - orphanedRowAction: "delete", - }) - webhooks?: Webhook[]; - - // TODO: DM channel - static async createChannel( - channel: Partial<Channel>, - user_id: string = "0", - opts?: { - keepId?: boolean; - skipExistsCheck?: boolean; - skipPermissionCheck?: boolean; - skipEventEmit?: boolean; - skipNameChecks?: boolean; - } - ) { - if (!opts?.skipPermissionCheck) { - // Always check if user has permission first - const permissions = await getPermission(user_id, channel.guild_id); - permissions.hasThrow("MANAGE_CHANNELS"); - } - - if (!opts?.skipNameChecks) { - const guild = await Guild.findOneOrFail({ where: { id: channel.guild_id } }); - if (!guild.features.includes("ALLOW_INVALID_CHANNEL_NAMES") && channel.name) { - for (var character of InvisibleCharacters) - if (channel.name.includes(character)) - throw new HTTPError("Channel name cannot include invalid characters", 403); - - // Categories skip these checks on discord.com - if (channel.type !== ChannelType.GUILD_CATEGORY) { - if (channel.name.includes(" ")) - throw new HTTPError("Channel name cannot include invalid characters", 403); - - if (channel.name.match(/\-\-+/g)) - throw new HTTPError("Channel name cannot include multiple adjacent dashes.", 403); - - if (channel.name.charAt(0) === "-" || - channel.name.charAt(channel.name.length - 1) === "-") - throw new HTTPError("Channel name cannot start/end with dash.", 403); - } - else - channel.name = channel.name.trim(); //category names are trimmed client side on discord.com - } - - if (!guild.features.includes("ALLOW_UNNAMED_CHANNELS")) { - if (!channel.name) - throw new HTTPError("Channel name cannot be empty.", 403); - } - } - - switch (channel.type) { - case ChannelType.GUILD_TEXT: - case ChannelType.GUILD_NEWS: - case ChannelType.GUILD_VOICE: - if (channel.parent_id && !opts?.skipExistsCheck) { - const exists = await Channel.findOneOrFail({ where: { id: channel.parent_id } }); - if (!exists) throw new HTTPError("Parent id channel doesn't exist", 400); - if (exists.guild_id !== channel.guild_id) - throw new HTTPError("The category channel needs to be in the guild"); - } - break; - case ChannelType.GUILD_CATEGORY: - case ChannelType.UNHANDLED: - break; - case ChannelType.DM: - case ChannelType.GROUP_DM: - throw new HTTPError("You can't create a dm channel in a guild"); - case ChannelType.GUILD_STORE: - default: - throw new HTTPError("Not yet supported"); - } - - if (!channel.permission_overwrites) channel.permission_overwrites = []; - // TODO: eagerly auto generate position of all guild channels - - channel = { - ...channel, - ...(!opts?.keepId && { id: Snowflake.generate() }), - created_at: new Date(), - position: (channel.type === ChannelType.UNHANDLED ? 0 : channel.position) || 0, - }; - - await Promise.all([ - Channel.create(channel).save(), - !opts?.skipEventEmit - ? emitEvent({ - event: "CHANNEL_CREATE", - data: channel, - guild_id: channel.guild_id, - } as ChannelCreateEvent) - : Promise.resolve(), - ]); - - return channel; - } - - static async createDMChannel(recipients: string[], creator_user_id: string, name?: string) { - recipients = recipients.unique().filter((x) => x !== creator_user_id); - // TODO: check config for max number of recipients - /** if you want to disallow note to self channels, uncomment the conditional below - - const otherRecipientsUsers = await User.find({ where: recipients.map((x) => ({ id: x })) }); - if (otherRecipientsUsers.length !== recipients.length) { - throw new HTTPError("Recipient/s not found"); - } - **/ - - const type = recipients.length > 1 ? ChannelType.GROUP_DM : ChannelType.DM; - - let channel = null; - - const channelRecipients = [...recipients, creator_user_id]; - - const userRecipients = await Recipient.find({ - where: { user_id: creator_user_id }, - relations: ["channel", "channel.recipients"], - }); - - for (let ur of userRecipients) { - let re = ur.channel.recipients!.map((r) => r.user_id); - if (re.length === channelRecipients.length) { - if (containsAll(re, channelRecipients)) { - if (channel == null) { - channel = ur.channel; - await ur.assign({ closed: false }).save(); - } - } - } - } - - if (channel == null) { - name = trimSpecial(name); - - channel = await Channel.create({ - name, - type, - owner_id: undefined, - created_at: new Date(), - last_message_id: undefined, - recipients: channelRecipients.map( - (x) => - Recipient.create({ user_id: x, closed: !(type === ChannelType.GROUP_DM || x === creator_user_id) }) - ), - nsfw: false, - }).save(); - } - - const channel_dto = await DmChannelDTO.from(channel); - - if (type === ChannelType.GROUP_DM) { - for (let recipient of channel.recipients!) { - await emitEvent({ - event: "CHANNEL_CREATE", - data: channel_dto.excludedRecipients([recipient.user_id]), - user_id: recipient.user_id, - }); - } - } else { - await emitEvent({ event: "CHANNEL_CREATE", data: channel_dto, user_id: creator_user_id }); - } - - if (recipients.length === 1) return channel_dto; - else return channel_dto.excludedRecipients([creator_user_id]); - } - - static async removeRecipientFromChannel(channel: Channel, user_id: string) { - await Recipient.delete({ channel_id: channel.id, user_id: user_id }); - channel.recipients = channel.recipients?.filter((r) => r.user_id !== user_id); - - if (channel.recipients?.length === 0) { - await Channel.deleteChannel(channel); - await emitEvent({ - event: "CHANNEL_DELETE", - data: await DmChannelDTO.from(channel, [user_id]), - user_id: user_id, - }); - return; - } - - await emitEvent({ - event: "CHANNEL_DELETE", - data: await DmChannelDTO.from(channel, [user_id]), - user_id: user_id, - }); - - //If the owner leave the server user is the new owner - if (channel.owner_id === user_id) { - channel.owner_id = "1"; // The channel is now owned by the server user - await emitEvent({ - event: "CHANNEL_UPDATE", - data: await DmChannelDTO.from(channel, [user_id]), - channel_id: channel.id, - }); - } - - await channel.save(); - - await emitEvent({ - event: "CHANNEL_RECIPIENT_REMOVE", - data: { - channel_id: channel.id, - user: await User.findOneOrFail({ where: { id: user_id }, select: PublicUserProjection }), - }, - channel_id: channel.id, - } as ChannelRecipientRemoveEvent); - } - - static async deleteChannel(channel: Channel) { - await Message.delete({ channel_id: channel.id }); //TODO we should also delete the attachments from the cdn but to do that we need to move cdn.ts in util - //TODO before deleting the channel we should check and delete other relations - await Channel.delete({ id: channel.id }); - } - - isDm() { - return this.type === ChannelType.DM || this.type === ChannelType.GROUP_DM; - } - - // Does the channel support sending messages ( eg categories do not ) - isWritable() { - const disallowedChannelTypes = [ - ChannelType.GUILD_CATEGORY, - ChannelType.GUILD_STAGE_VOICE, - ChannelType.VOICELESS_WHITEBOARD, - ]; - return disallowedChannelTypes.indexOf(this.type) == -1; - } -} - -export interface ChannelPermissionOverwrite { - allow: string; - deny: string; - id: string; - type: ChannelPermissionOverwriteType; -} - -export enum ChannelPermissionOverwriteType { - role = 0, - member = 1, - group = 2, -} +import { + Column, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + RelationId, +} from "typeorm"; +import { BaseClass } from "./BaseClass"; +import { Guild } from "./Guild"; +import { PublicUserProjection, User } from "./User"; +import { HTTPError } from "lambert-server"; +import { + containsAll, + emitEvent, + getPermission, + Snowflake, + trimSpecial, + InvisibleCharacters, + ChannelTypes, +} from "../util"; +import { ChannelCreateEvent, ChannelRecipientRemoveEvent } from "../interfaces"; +import { Recipient } from "./Recipient"; +import { Message } from "./Message"; +import { ReadState } from "./ReadState"; +import { Invite } from "./Invite"; +import { VoiceState } from "./VoiceState"; +import { Webhook } from "./Webhook"; +import { DmChannelDTO } from "../dtos"; + +export enum ChannelType { + GUILD_TEXT = 0, // a text channel within a guild + DM = 1, // a direct message between users + GUILD_VOICE = 2, // a voice channel within a guild + GROUP_DM = 3, // a direct message between multiple users + GUILD_CATEGORY = 4, // an organizational category that contains zero or more channels + GUILD_NEWS = 5, // a channel that users can follow and crosspost into a guild or route + GUILD_STORE = 6, // a channel in which game developers can sell their things + ENCRYPTED = 7, // end-to-end encrypted channel + ENCRYPTED_THREAD = 8, // end-to-end encrypted thread channel + TRANSACTIONAL = 9, // event chain style transactional channel + GUILD_NEWS_THREAD = 10, // a temporary sub-channel within a GUILD_NEWS channel + GUILD_PUBLIC_THREAD = 11, // a temporary sub-channel within a GUILD_TEXT channel + GUILD_PRIVATE_THREAD = 12, // a temporary sub-channel within a GUILD_TEXT channel that is only viewable by those invited and those with the MANAGE_THREADS permission + GUILD_STAGE_VOICE = 13, // a voice channel for hosting events with an audience + DIRECTORY = 14, // guild directory listing channel + GUILD_FORUM = 15, // forum composed of IM threads + TICKET_TRACKER = 33, // ticket tracker, individual ticket items shall have type 12 + KANBAN = 34, // confluence like kanban board + VOICELESS_WHITEBOARD = 35, // whiteboard but without voice (whiteboard + voice is the same as stage) + CUSTOM_START = 64, // start custom channel types from here + UNHANDLED = 255, // unhandled unowned pass-through channel type +} + +@Entity("channels") +export class Channel extends BaseClass { + @Column() + created_at: Date; + + @Column({ nullable: true }) + name?: string; + + @Column({ type: "text", nullable: true }) + icon?: string | null; + + @Column({ type: "int" }) + type: ChannelType; + + @OneToMany(() => Recipient, (recipient: Recipient) => recipient.channel, { + cascade: true, + orphanedRowAction: "delete", + }) + recipients?: Recipient[]; + + @Column({ nullable: true }) + last_message_id?: string; + + @Column({ nullable: true }) + @RelationId((channel: Channel) => channel.guild) + guild_id?: string; + + @JoinColumn({ name: "guild_id" }) + @ManyToOne(() => Guild, { + onDelete: "CASCADE", + }) + guild: Guild; + + @Column({ nullable: true }) + @RelationId((channel: Channel) => channel.parent) + parent_id: string; + + @JoinColumn({ name: "parent_id" }) + @ManyToOne(() => Channel) + parent?: Channel; + + // for group DMs and owned custom channel types + @Column({ nullable: true }) + @RelationId((channel: Channel) => channel.owner) + owner_id?: string; + + @JoinColumn({ name: "owner_id" }) + @ManyToOne(() => User) + owner: User; + + @Column({ nullable: true }) + last_pin_timestamp?: number; + + @Column({ nullable: true }) + default_auto_archive_duration?: number; + + @Column({ nullable: true }) + position?: number; + + @Column({ type: "simple-json", nullable: true }) + permission_overwrites?: ChannelPermissionOverwrite[]; + + @Column({ nullable: true }) + video_quality_mode?: number; + + @Column({ nullable: true }) + bitrate?: number; + + @Column({ nullable: true }) + user_limit?: number; + + @Column() + nsfw: boolean = false; + + @Column({ nullable: true }) + rate_limit_per_user?: number; + + @Column({ nullable: true }) + topic?: string; + + @OneToMany(() => Invite, (invite: Invite) => invite.channel, { + cascade: true, + orphanedRowAction: "delete", + }) + invites?: Invite[]; + + @Column({ nullable: true }) + retention_policy_id?: string; + + @OneToMany(() => Message, (message: Message) => message.channel, { + cascade: true, + orphanedRowAction: "delete", + }) + messages?: Message[]; + + @OneToMany( + () => VoiceState, + (voice_state: VoiceState) => voice_state.channel, + { + cascade: true, + orphanedRowAction: "delete", + }, + ) + voice_states?: VoiceState[]; + + @OneToMany(() => ReadState, (read_state: ReadState) => read_state.channel, { + cascade: true, + orphanedRowAction: "delete", + }) + read_states?: ReadState[]; + + @OneToMany(() => Webhook, (webhook: Webhook) => webhook.channel, { + cascade: true, + orphanedRowAction: "delete", + }) + webhooks?: Webhook[]; + + // TODO: DM channel + static async createChannel( + channel: Partial<Channel>, + user_id: string = "0", + opts?: { + keepId?: boolean; + skipExistsCheck?: boolean; + skipPermissionCheck?: boolean; + skipEventEmit?: boolean; + skipNameChecks?: boolean; + }, + ) { + if (!opts?.skipPermissionCheck) { + // Always check if user has permission first + const permissions = await getPermission(user_id, channel.guild_id); + permissions.hasThrow("MANAGE_CHANNELS"); + } + + if (!opts?.skipNameChecks) { + const guild = await Guild.findOneOrFail({ + where: { id: channel.guild_id }, + }); + if ( + !guild.features.includes("ALLOW_INVALID_CHANNEL_NAMES") && + channel.name + ) { + for (var character of InvisibleCharacters) + if (channel.name.includes(character)) + throw new HTTPError( + "Channel name cannot include invalid characters", + 403, + ); + + // Categories skip these checks on discord.com + if (channel.type !== ChannelType.GUILD_CATEGORY) { + if (channel.name.includes(" ")) + throw new HTTPError( + "Channel name cannot include invalid characters", + 403, + ); + + if (channel.name.match(/\-\-+/g)) + throw new HTTPError( + "Channel name cannot include multiple adjacent dashes.", + 403, + ); + + if ( + channel.name.charAt(0) === "-" || + channel.name.charAt(channel.name.length - 1) === "-" + ) + throw new HTTPError( + "Channel name cannot start/end with dash.", + 403, + ); + } else channel.name = channel.name.trim(); //category names are trimmed client side on discord.com + } + + if (!guild.features.includes("ALLOW_UNNAMED_CHANNELS")) { + if (!channel.name) + throw new HTTPError("Channel name cannot be empty.", 403); + } + } + + switch (channel.type) { + case ChannelType.GUILD_TEXT: + case ChannelType.GUILD_NEWS: + case ChannelType.GUILD_VOICE: + if (channel.parent_id && !opts?.skipExistsCheck) { + const exists = await Channel.findOneOrFail({ + where: { id: channel.parent_id }, + }); + if (!exists) + throw new HTTPError( + "Parent id channel doesn't exist", + 400, + ); + if (exists.guild_id !== channel.guild_id) + throw new HTTPError( + "The category channel needs to be in the guild", + ); + } + break; + case ChannelType.GUILD_CATEGORY: + case ChannelType.UNHANDLED: + break; + case ChannelType.DM: + case ChannelType.GROUP_DM: + throw new HTTPError("You can't create a dm channel in a guild"); + case ChannelType.GUILD_STORE: + default: + throw new HTTPError("Not yet supported"); + } + + if (!channel.permission_overwrites) channel.permission_overwrites = []; + // TODO: eagerly auto generate position of all guild channels + + channel = { + ...channel, + ...(!opts?.keepId && { id: Snowflake.generate() }), + created_at: new Date(), + position: + (channel.type === ChannelType.UNHANDLED + ? 0 + : channel.position) || 0, + }; + + await Promise.all([ + Channel.create(channel).save(), + !opts?.skipEventEmit + ? emitEvent({ + event: "CHANNEL_CREATE", + data: channel, + guild_id: channel.guild_id, + } as ChannelCreateEvent) + : Promise.resolve(), + ]); + + return channel; + } + + static async createDMChannel( + recipients: string[], + creator_user_id: string, + name?: string, + ) { + recipients = recipients.unique().filter((x) => x !== creator_user_id); + // TODO: check config for max number of recipients + /** if you want to disallow note to self channels, uncomment the conditional below + + const otherRecipientsUsers = await User.find({ where: recipients.map((x) => ({ id: x })) }); + if (otherRecipientsUsers.length !== recipients.length) { + throw new HTTPError("Recipient/s not found"); + } + **/ + + const type = + recipients.length > 1 ? ChannelType.GROUP_DM : ChannelType.DM; + + let channel = null; + + const channelRecipients = [...recipients, creator_user_id]; + + const userRecipients = await Recipient.find({ + where: { user_id: creator_user_id }, + relations: ["channel", "channel.recipients"], + }); + + for (let ur of userRecipients) { + let re = ur.channel.recipients!.map((r) => r.user_id); + if (re.length === channelRecipients.length) { + if (containsAll(re, channelRecipients)) { + if (channel == null) { + channel = ur.channel; + await ur.assign({ closed: false }).save(); + } + } + } + } + + if (channel == null) { + name = trimSpecial(name); + + channel = await Channel.create({ + name, + type, + owner_id: undefined, + created_at: new Date(), + last_message_id: undefined, + recipients: channelRecipients.map((x) => + Recipient.create({ + user_id: x, + closed: !( + type === ChannelType.GROUP_DM || + x === creator_user_id + ), + }), + ), + nsfw: false, + }).save(); + } + + const channel_dto = await DmChannelDTO.from(channel); + + if (type === ChannelType.GROUP_DM) { + for (let recipient of channel.recipients!) { + await emitEvent({ + event: "CHANNEL_CREATE", + data: channel_dto.excludedRecipients([recipient.user_id]), + user_id: recipient.user_id, + }); + } + } else { + await emitEvent({ + event: "CHANNEL_CREATE", + data: channel_dto, + user_id: creator_user_id, + }); + } + + if (recipients.length === 1) return channel_dto; + else return channel_dto.excludedRecipients([creator_user_id]); + } + + static async removeRecipientFromChannel(channel: Channel, user_id: string) { + await Recipient.delete({ channel_id: channel.id, user_id: user_id }); + channel.recipients = channel.recipients?.filter( + (r) => r.user_id !== user_id, + ); + + if (channel.recipients?.length === 0) { + await Channel.deleteChannel(channel); + await emitEvent({ + event: "CHANNEL_DELETE", + data: await DmChannelDTO.from(channel, [user_id]), + user_id: user_id, + }); + return; + } + + await emitEvent({ + event: "CHANNEL_DELETE", + data: await DmChannelDTO.from(channel, [user_id]), + user_id: user_id, + }); + + //If the owner leave the server user is the new owner + if (channel.owner_id === user_id) { + channel.owner_id = "1"; // The channel is now owned by the server user + await emitEvent({ + event: "CHANNEL_UPDATE", + data: await DmChannelDTO.from(channel, [user_id]), + channel_id: channel.id, + }); + } + + await channel.save(); + + await emitEvent({ + event: "CHANNEL_RECIPIENT_REMOVE", + data: { + channel_id: channel.id, + user: await User.findOneOrFail({ + where: { id: user_id }, + select: PublicUserProjection, + }), + }, + channel_id: channel.id, + } as ChannelRecipientRemoveEvent); + } + + static async deleteChannel(channel: Channel) { + await Message.delete({ channel_id: channel.id }); //TODO we should also delete the attachments from the cdn but to do that we need to move cdn.ts in util + //TODO before deleting the channel we should check and delete other relations + await Channel.delete({ id: channel.id }); + } + + isDm() { + return ( + this.type === ChannelType.DM || this.type === ChannelType.GROUP_DM + ); + } + + // Does the channel support sending messages ( eg categories do not ) + isWritable() { + const disallowedChannelTypes = [ + ChannelType.GUILD_CATEGORY, + ChannelType.GUILD_STAGE_VOICE, + ChannelType.VOICELESS_WHITEBOARD, + ]; + return disallowedChannelTypes.indexOf(this.type) == -1; + } +} + +export interface ChannelPermissionOverwrite { + allow: string; + deny: string; + id: string; + type: ChannelPermissionOverwriteType; +} + +export enum ChannelPermissionOverwriteType { + role = 0, + member = 1, + group = 2, +} diff --git a/src/util/entities/ClientRelease.ts b/src/util/entities/ClientRelease.ts index c5afd307..2723ab67 100644 --- a/src/util/entities/ClientRelease.ts +++ b/src/util/entities/ClientRelease.ts @@ -1,4 +1,4 @@ -import { Column, Entity} from "typeorm"; +import { Column, Entity } from "typeorm"; import { BaseClass } from "./BaseClass"; @Entity("client_release") diff --git a/src/util/entities/Config.ts b/src/util/entities/Config.ts index 9aabc1a8..cd7a6923 100644 --- a/src/util/entities/Config.ts +++ b/src/util/entities/Config.ts @@ -191,17 +191,17 @@ export interface ConfigValue { allowTemplateCreation: Boolean; allowDiscordTemplates: Boolean; allowRaws: Boolean; - }, + }; client: { useTestClient: Boolean; releases: { useLocalRelease: Boolean; //TODO upstreamVersion: string; }; - }, + }; metrics: { timeout: number; - }, + }; sentry: { enabled: boolean; endpoint: string; @@ -230,7 +230,8 @@ export const DefaultConfigOptions: ConfigValue = { }, general: { instanceName: "Fosscord Instance", - instanceDescription: "This is a Fosscord instance made in pre-release days", + instanceDescription: + "This is a Fosscord instance made in pre-release days", frontPage: null, tosPage: null, correspondenceEmail: "noreply@localhost.local", @@ -318,8 +319,9 @@ export const DefaultConfigOptions: ConfigValue = { sitekey: null, secret: null, }, - ipdataApiKey: "eca677b284b3bac29eb72f5e496aa9047f26543605efe99ff2ce35c9", - defaultRights: "30644591655936", // See util/scripts/rights.js + ipdataApiKey: + "eca677b284b3bac29eb72f5e496aa9047f26543605efe99ff2ce35c9", + defaultRights: "30644591655936", // See util/scripts/rights.js }, login: { requireCaptcha: false, @@ -395,22 +397,23 @@ export const DefaultConfigOptions: ConfigValue = { enabled: true, allowTemplateCreation: true, allowDiscordTemplates: true, - allowRaws: false + allowRaws: false, }, client: { useTestClient: true, releases: { useLocalRelease: true, - upstreamVersion: "0.0.264" - } + upstreamVersion: "0.0.264", + }, }, metrics: { - timeout: 30000 + timeout: 30000, }, sentry: { enabled: false, - endpoint: "https://05e8e3d005f34b7d97e920ae5870a5e5@sentry.thearcanebrony.net/6", + endpoint: + "https://05e8e3d005f34b7d97e920ae5870a5e5@sentry.thearcanebrony.net/6", traceSampleRate: 1.0, - environment: hostname() - } -}; \ No newline at end of file + environment: hostname(), + }, +}; diff --git a/src/util/entities/ConnectedAccount.ts b/src/util/entities/ConnectedAccount.ts index 09ae30ab..a893ff34 100644 --- a/src/util/entities/ConnectedAccount.ts +++ b/src/util/entities/ConnectedAccount.ts @@ -2,7 +2,8 @@ import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; import { BaseClass } from "./BaseClass"; import { User } from "./User"; -export interface PublicConnectedAccount extends Pick<ConnectedAccount, "name" | "type" | "verified"> {} +export interface PublicConnectedAccount + extends Pick<ConnectedAccount, "name" | "type" | "verified"> {} @Entity("connected_accounts") export class ConnectedAccount extends BaseClass { diff --git a/src/util/entities/Emoji.ts b/src/util/entities/Emoji.ts index a3615b7d..0aa640b5 100644 --- a/src/util/entities/Emoji.ts +++ b/src/util/entities/Emoji.ts @@ -40,7 +40,7 @@ export class Emoji extends BaseClass { @Column({ type: "simple-array" }) roles: string[]; // roles this emoji is whitelisted to (new discord feature?) - + @Column({ type: "simple-array", nullable: true }) groups: string[]; // user groups this emoji is whitelisted to (Fosscord extension) } diff --git a/src/util/entities/Encryption.ts b/src/util/entities/Encryption.ts index b597b90a..4c427b32 100644 --- a/src/util/entities/Encryption.ts +++ b/src/util/entities/Encryption.ts @@ -1,9 +1,23 @@ -import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm"; +import { + Column, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + RelationId, +} from "typeorm"; import { BaseClass } from "./BaseClass"; import { Guild } from "./Guild"; import { PublicUserProjection, User } from "./User"; import { HTTPError } from "lambert-server"; -import { containsAll, emitEvent, getPermission, Snowflake, trimSpecial, InvisibleCharacters } from "../util"; +import { + containsAll, + emitEvent, + getPermission, + Snowflake, + trimSpecial, + InvisibleCharacters, +} from "../util"; import { BitField, BitFieldResolvable, BitFlag } from "../util/BitField"; import { Recipient } from "./Recipient"; import { Message } from "./Message"; @@ -13,7 +27,6 @@ import { DmChannelDTO } from "../dtos"; @Entity("security_settings") export class SecuritySettings extends BaseClass { - @Column({ nullable: true }) guild_id: string; @@ -31,5 +44,4 @@ export class SecuritySettings extends BaseClass { @Column({ nullable: true }) used_since_message: string; - } diff --git a/src/util/entities/Guild.ts b/src/util/entities/Guild.ts index 2ce7c213..8854fec0 100644 --- a/src/util/entities/Guild.ts +++ b/src/util/entities/Guild.ts @@ -1,4 +1,13 @@ -import { Column, Entity, JoinColumn, ManyToMany, ManyToOne, OneToMany, OneToOne, RelationId } from "typeorm"; +import { + Column, + Entity, + JoinColumn, + ManyToMany, + ManyToOne, + OneToMany, + OneToOne, + RelationId, +} from "typeorm"; import { Config, handleFile, Snowflake } from ".."; import { Ban } from "./Ban"; import { BaseClass } from "./BaseClass"; @@ -86,7 +95,7 @@ export class Guild extends BaseClass { //TODO: https://discord.com/developers/docs/resources/guild#guild-object-guild-features @Column({ nullable: true }) - primary_category_id?: string; // TODO: this was number? + primary_category_id?: string; // TODO: this was number? @Column({ nullable: true }) icon?: string; @@ -269,7 +278,7 @@ export class Guild extends BaseClass { @Column() nsfw: boolean; - + // TODO: nested guilds @Column({ nullable: true }) parent?: string; @@ -332,10 +341,13 @@ export class Guild extends BaseClass { permissions: String("2251804225"), position: 0, icon: undefined, - unicode_emoji: undefined + unicode_emoji: undefined, }).save(); - if (!body.channels || !body.channels.length) body.channels = [{ id: "01", type: 0, name: "general", nsfw: false }]; + if (!body.channels || !body.channels.length) + body.channels = [ + { id: "01", type: 0, name: "general", nsfw: false }, + ]; const ids = new Map(); @@ -345,17 +357,23 @@ export class Guild extends BaseClass { } }); - for (const channel of body.channels?.sort((a, b) => (a.parent_id ? 1 : -1))) { + for (const channel of body.channels?.sort((a, b) => + a.parent_id ? 1 : -1, + )) { var id = ids.get(channel.id) || Snowflake.generate(); var parent_id = ids.get(channel.parent_id); - await Channel.createChannel({ ...channel, guild_id, id, parent_id }, body.owner_id, { - keepId: true, - skipExistsCheck: true, - skipPermissionCheck: true, - skipEventEmit: true, - }); + await Channel.createChannel( + { ...channel, guild_id, id, parent_id }, + body.owner_id, + { + keepId: true, + skipExistsCheck: true, + skipPermissionCheck: true, + skipEventEmit: true, + }, + ); } return guild; diff --git a/src/util/entities/Invite.ts b/src/util/entities/Invite.ts index 4f36f247..90dec92a 100644 --- a/src/util/entities/Invite.ts +++ b/src/util/entities/Invite.ts @@ -1,4 +1,11 @@ -import { Column, Entity, JoinColumn, ManyToOne, RelationId, PrimaryColumn } from "typeorm"; +import { + Column, + Entity, + JoinColumn, + ManyToOne, + RelationId, + PrimaryColumn, +} from "typeorm"; import { Member } from "./Member"; import { BaseClassWithoutId } from "./BaseClass"; import { Channel } from "./Channel"; @@ -76,7 +83,8 @@ export class Invite extends BaseClassWithoutId { static async joinGuild(user_id: string, code: string) { const invite = await Invite.findOneOrFail({ where: { code } }); - if (invite.uses++ >= invite.max_uses && invite.max_uses !== 0) await Invite.delete({ code }); + if (invite.uses++ >= invite.max_uses && invite.max_uses !== 0) + await Invite.delete({ code }); else await invite.save(); await Member.addToGuild(user_id, invite.guild_id); diff --git a/src/util/entities/Member.ts b/src/util/entities/Member.ts index 7d1346ba..f2762adc 100644 --- a/src/util/entities/Member.ts +++ b/src/util/entities/Member.ts @@ -22,7 +22,6 @@ import { GuildMemberRemoveEvent, GuildMemberUpdateEvent, MessageCreateEvent, - } from "../interfaces"; import { HTTPError } from "lambert-server"; import { Role } from "./Role"; @@ -126,19 +125,34 @@ export class Member extends BaseClassWithoutId { if (this.nick) { this.nick = this.nick.split("\n").join(""); this.nick = this.nick.split("\t").join(""); - if (BannedWords.find(this.nick)) throw FieldErrors({ nick: { message: "Bad nickname", code: "INVALID_NICKNAME" } }); + if (BannedWords.find(this.nick)) + throw FieldErrors({ + nick: { message: "Bad nickname", code: "INVALID_NICKNAME" }, + }); } } static async IsInGuildOrFail(user_id: string, guild_id: string) { - if (await Member.count({ where: { id: user_id, guild: { id: guild_id } } })) return true; + if ( + await Member.count({ + where: { id: user_id, guild: { id: guild_id } }, + }) + ) + return true; throw new HTTPError("You are not member of this guild", 403); } static async removeFromGuild(user_id: string, guild_id: string) { - const guild = await Guild.findOneOrFail({ select: ["owner_id"], where: { id: guild_id } }); - if (guild.owner_id === user_id) throw new Error("The owner cannot be removed of the guild"); - const member = await Member.findOneOrFail({ where: { id: user_id, guild_id }, relations: ["user"] }); + const guild = await Guild.findOneOrFail({ + select: ["owner_id"], + where: { id: guild_id }, + }); + if (guild.owner_id === user_id) + throw new Error("The owner cannot be removed of the guild"); + const member = await Member.findOneOrFail({ + where: { id: user_id, guild_id }, + relations: ["user"], + }); // use promise all to execute all promises at the same time -> save time return Promise.all([ @@ -169,9 +183,12 @@ export class Member extends BaseClassWithoutId { where: { id: user_id, guild_id }, relations: ["user", "roles"], // we don't want to load the role objects just the ids //@ts-ignore - select: ["index", "roles.id"], // TODO fix type + select: ["index", "roles.id"], // TODO fix type + }), + Role.findOneOrFail({ + where: { id: role_id, guild_id }, + select: ["id"], }), - Role.findOneOrFail({ where: { id: role_id, guild_id }, select: ["id"] }), ]); member.roles.push(Role.create({ id: role_id })); @@ -189,7 +206,11 @@ export class Member extends BaseClassWithoutId { ]); } - static async removeRole(user_id: string, guild_id: string, role_id: string) { + static async removeRole( + user_id: string, + guild_id: string, + role_id: string, + ) { const [member] = await Promise.all([ Member.findOneOrFail({ where: { id: user_id, guild_id }, @@ -215,7 +236,11 @@ export class Member extends BaseClassWithoutId { ]); } - static async changeNickname(user_id: string, guild_id: string, nickname: string) { + static async changeNickname( + user_id: string, + guild_id: string, + nickname: string, + ) { const member = await Member.findOneOrFail({ where: { id: user_id, @@ -249,7 +274,10 @@ export class Member extends BaseClassWithoutId { const { maxGuilds } = Config.get().limits.user; const guild_count = await Member.count({ where: { id: user_id } }); if (guild_count >= maxGuilds) { - throw new HTTPError(`You are at the ${maxGuilds} server limit.`, 403); + throw new HTTPError( + `You are at the ${maxGuilds} server limit.`, + 403, + ); } const guild = await Guild.findOneOrFail({ @@ -259,7 +287,11 @@ export class Member extends BaseClassWithoutId { relations: [...PublicGuildRelations, "system_channel"], }); - if (await Member.count({ where: { id: user.id, guild: { id: guild_id } } })) + if ( + await Member.count({ + where: { id: user.id, guild: { id: guild_id } }, + }) + ) throw new HTTPError("You are already a member of this guild", 400); const member = { @@ -268,7 +300,7 @@ export class Member extends BaseClassWithoutId { nick: undefined, roles: [guild_id], // @everyone role joined_at: new Date(), - premium_since: (new Date()).getTime(), + premium_since: new Date().getTime(), deaf: false, mute: false, pending: false, @@ -339,7 +371,11 @@ export class Member extends BaseClassWithoutId { }); await Promise.all([ message.save(), - emitEvent({ event: "MESSAGE_CREATE", channel_id: message.channel_id, data: message } as MessageCreateEvent) + emitEvent({ + event: "MESSAGE_CREATE", + channel_id: message.channel_id, + data: message, + } as MessageCreateEvent), ]); } } @@ -362,7 +398,7 @@ export interface UserGuildSettings { channel_overrides: { [channel_id: string]: ChannelOverride; - } | null, + } | null; message_notifications: number; mobile_push: boolean; mute_config: MuteConfig | null; @@ -389,7 +425,7 @@ export const DefaultUserGuildSettings: UserGuildSettings = { notify_highlights: 0, suppress_everyone: false, suppress_roles: false, - version: 453, // ? + version: 453, // ? guild_id: null, }; diff --git a/src/util/entities/Message.ts b/src/util/entities/Message.ts index be790502..a52b4785 100644 --- a/src/util/entities/Message.ts +++ b/src/util/entities/Message.ts @@ -51,7 +51,7 @@ export enum MessageType { SELF_COMMAND_SCRIPT = 43, // self command scripts ENCRYPTION = 50, CUSTOM_START = 63, - UNHANDLED = 255 + UNHANDLED = 255, } @Entity("messages") @@ -115,7 +115,10 @@ export class Message extends BaseClass { @ManyToOne(() => Application) application?: Application; - @Column({ nullable: true, type: process.env.PRODUCTION ? "longtext" : undefined }) + @Column({ + nullable: true, + type: process.env.PRODUCTION ? "longtext" : undefined, + }) content?: string; @Column() @@ -147,10 +150,14 @@ export class Message extends BaseClass { @ManyToMany(() => Sticker, { cascade: true, onDelete: "CASCADE" }) sticker_items?: Sticker[]; - @OneToMany(() => Attachment, (attachment: Attachment) => attachment.message, { - cascade: true, - orphanedRowAction: "delete", - }) + @OneToMany( + () => Attachment, + (attachment: Attachment) => attachment.message, + { + cascade: true, + orphanedRowAction: "delete", + }, + ) attachments?: Attachment[]; @Column({ type: "simple-json" }) @@ -176,7 +183,7 @@ export class Message extends BaseClass { @Column({ nullable: true }) flags?: string; - + @Column({ type: "simple-json", nullable: true }) message_reference?: { message_id: string; @@ -204,7 +211,11 @@ export class Message extends BaseClass { @BeforeInsert() validate() { if (this.content) { - if (BannedWords.find(this.content)) throw new HTTPError("Message was blocked by automatic moderation", 200000); + if (BannedWords.find(this.content)) + throw new HTTPError( + "Message was blocked by automatic moderation", + 200000, + ); } } } diff --git a/src/util/entities/Migration.ts b/src/util/entities/Migration.ts index 3f39ae72..f4e54eae 100644 --- a/src/util/entities/Migration.ts +++ b/src/util/entities/Migration.ts @@ -1,7 +1,14 @@ -import { Column, Entity, ObjectIdColumn, PrimaryGeneratedColumn } from "typeorm"; +import { + Column, + Entity, + ObjectIdColumn, + PrimaryGeneratedColumn, +} from "typeorm"; import { BaseClassWithoutId } from "."; -export const PrimaryIdAutoGenerated = process.env.DATABASE?.startsWith("mongodb") +export const PrimaryIdAutoGenerated = process.env.DATABASE?.startsWith( + "mongodb", +) ? ObjectIdColumn : PrimaryGeneratedColumn; diff --git a/src/util/entities/Note.ts b/src/util/entities/Note.ts index 36017c5e..b3ac45ee 100644 --- a/src/util/entities/Note.ts +++ b/src/util/entities/Note.ts @@ -15,4 +15,4 @@ export class Note extends BaseClass { @Column() content: string; -} \ No newline at end of file +} diff --git a/src/util/entities/ReadState.ts b/src/util/entities/ReadState.ts index b915573b..53ed5589 100644 --- a/src/util/entities/ReadState.ts +++ b/src/util/entities/ReadState.ts @@ -1,4 +1,11 @@ -import { Column, Entity, Index, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import { + Column, + Entity, + Index, + JoinColumn, + ManyToOne, + RelationId, +} from "typeorm"; import { BaseClass } from "./BaseClass"; import { Channel } from "./Channel"; import { Message } from "./Message"; @@ -33,8 +40,8 @@ export class ReadState extends BaseClass { // fully read marker @Column({ nullable: true }) - last_message_id: string; - + last_message_id: string; + // public read receipt @Column({ nullable: true }) public_ack: string; diff --git a/src/util/entities/Relationship.ts b/src/util/entities/Relationship.ts index c3592c76..25b52757 100644 --- a/src/util/entities/Relationship.ts +++ b/src/util/entities/Relationship.ts @@ -1,4 +1,11 @@ -import { Column, Entity, Index, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import { + Column, + Entity, + Index, + JoinColumn, + ManyToOne, + RelationId, +} from "typeorm"; import { BaseClass } from "./BaseClass"; import { User } from "./User"; diff --git a/src/util/entities/StickerPack.ts b/src/util/entities/StickerPack.ts index ec8c69a2..04d74bac 100644 --- a/src/util/entities/StickerPack.ts +++ b/src/util/entities/StickerPack.ts @@ -1,4 +1,12 @@ -import { Column, Entity, JoinColumn, ManyToOne, OneToMany, OneToOne, RelationId } from "typeorm"; +import { + Column, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + OneToOne, + RelationId, +} from "typeorm"; import { Sticker } from "."; import { BaseClass } from "./BaseClass"; diff --git a/src/util/entities/Team.ts b/src/util/entities/Team.ts index 22140b7f..8f410bb4 100644 --- a/src/util/entities/Team.ts +++ b/src/util/entities/Team.ts @@ -1,4 +1,12 @@ -import { Column, Entity, JoinColumn, ManyToMany, ManyToOne, OneToMany, RelationId } from "typeorm"; +import { + Column, + Entity, + JoinColumn, + ManyToMany, + ManyToOne, + OneToMany, + RelationId, +} from "typeorm"; import { BaseClass } from "./BaseClass"; import { TeamMember } from "./TeamMember"; import { User } from "./User"; diff --git a/src/util/entities/TeamMember.ts b/src/util/entities/TeamMember.ts index b726e1e8..3f4a0422 100644 --- a/src/util/entities/TeamMember.ts +++ b/src/util/entities/TeamMember.ts @@ -20,9 +20,13 @@ export class TeamMember extends BaseClass { team_id: string; @JoinColumn({ name: "team_id" }) - @ManyToOne(() => require("./Team").Team, (team: import("./Team").Team) => team.members, { - onDelete: "CASCADE", - }) + @ManyToOne( + () => require("./Team").Team, + (team: import("./Team").Team) => team.members, + { + onDelete: "CASCADE", + }, + ) team: import("./Team").Team; @Column({ nullable: true }) diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts index 84a8a674..1389a424 100644 --- a/src/util/entities/User.ts +++ b/src/util/entities/User.ts @@ -1,9 +1,24 @@ -import { BeforeInsert, BeforeUpdate, Column, Entity, FindOneOptions, JoinColumn, OneToMany } from "typeorm"; +import { + BeforeInsert, + BeforeUpdate, + Column, + Entity, + FindOneOptions, + JoinColumn, + OneToMany, +} from "typeorm"; import { BaseClass } from "./BaseClass"; import { BitField } from "../util/BitField"; import { Relationship } from "./Relationship"; import { ConnectedAccount } from "./ConnectedAccount"; -import { Config, FieldErrors, Snowflake, trimSpecial, BannedWords, adjustEmail } from ".."; +import { + Config, + FieldErrors, + Snowflake, + trimSpecial, + BannedWords, + adjustEmail, +} from ".."; import { Member, Session } from "."; export enum PublicUserEnum { @@ -38,7 +53,7 @@ export enum PrivateUserEnum { export type PrivateUserKeys = keyof typeof PrivateUserEnum | PublicUserKeys; export const PublicUserProjection = Object.values(PublicUserEnum).filter( - (x) => typeof x === "string" + (x) => typeof x === "string", ) as PublicUserKeys[]; export const PrivateUserProjection = [ ...PublicUserProjection, @@ -48,7 +63,7 @@ export const PrivateUserProjection = [ // Private user data that should never get sent to the client export type PublicUser = Pick<User, PublicUserKeys>; -export interface UserPublic extends Pick<User, PublicUserKeys> { } +export interface UserPublic extends Pick<User, PublicUserKeys> {} export interface UserPrivate extends Pick<User, PrivateUserKeys> { locale: string; @@ -144,17 +159,25 @@ export class User extends BaseClass { sessions: Session[]; @JoinColumn({ name: "relationship_ids" }) - @OneToMany(() => Relationship, (relationship: Relationship) => relationship.from, { - cascade: true, - orphanedRowAction: "delete", - }) + @OneToMany( + () => Relationship, + (relationship: Relationship) => relationship.from, + { + cascade: true, + orphanedRowAction: "delete", + }, + ) relationships: Relationship[]; @JoinColumn({ name: "connected_account_ids" }) - @OneToMany(() => ConnectedAccount, (account: ConnectedAccount) => account.user, { - cascade: true, - orphanedRowAction: "delete", - }) + @OneToMany( + () => ConnectedAccount, + (account: ConnectedAccount) => account.user, + { + cascade: true, + orphanedRowAction: "delete", + }, + ) connected_accounts: ConnectedAccount[]; @Column({ type: "simple-json", select: false }) @@ -177,16 +200,43 @@ export class User extends BaseClass { @BeforeInsert() validate() { this.email = adjustEmail(this.email); - if (!this.email) throw FieldErrors({ email: { message: "Invalid email", code: "EMAIL_INVALID" } }); - if (!this.email.match(/([a-z\d.-]{3,})@([a-z\d.-]+).([a-z]{2,})/g)) throw FieldErrors({ email: { message: "Invalid email", code: "EMAIL_INVALID" } }); + if (!this.email) + throw FieldErrors({ + email: { message: "Invalid email", code: "EMAIL_INVALID" }, + }); + if (!this.email.match(/([a-z\d.-]{3,})@([a-z\d.-]+).([a-z]{2,})/g)) + throw FieldErrors({ + email: { message: "Invalid email", code: "EMAIL_INVALID" }, + }); const discrim = Number(this.discriminator); - if (this.discriminator.length > 4) throw FieldErrors({ email: { message: "Discriminator cannot be more than 4 digits.", code: "DISCRIMINATOR_INVALID" } }); - if (isNaN(discrim)) throw FieldErrors({ email: { message: "Discriminator must be a number.", code: "DISCRIMINATOR_INVALID" } }); - if (discrim <= 0 || discrim >= 10000) throw FieldErrors({ email: { message: "Discriminator must be a number.", code: "DISCRIMINATOR_INVALID" } }); + if (this.discriminator.length > 4) + throw FieldErrors({ + email: { + message: "Discriminator cannot be more than 4 digits.", + code: "DISCRIMINATOR_INVALID", + }, + }); + if (isNaN(discrim)) + throw FieldErrors({ + email: { + message: "Discriminator must be a number.", + code: "DISCRIMINATOR_INVALID", + }, + }); + if (discrim <= 0 || discrim >= 10000) + throw FieldErrors({ + email: { + message: "Discriminator must be a number.", + code: "DISCRIMINATOR_INVALID", + }, + }); this.discriminator = discrim.toString().padStart(4, "0"); - if (BannedWords.find(this.username)) throw FieldErrors({ username: { message: "Bad username", code: "INVALID_USERNAME" } }); + if (BannedWords.find(this.username)) + throw FieldErrors({ + username: { message: "Bad username", code: "INVALID_USERNAME" }, + }); } toPublicUser() { @@ -202,17 +252,25 @@ export class User extends BaseClass { where: { id: user_id }, ...opts, //@ts-ignore - select: [...PublicUserProjection, ...(opts?.select || [])], // TODO: fix + select: [...PublicUserProjection, ...(opts?.select || [])], // TODO: fix }); } - private static async generateDiscriminator(username: string): Promise<string | undefined> { + private static async generateDiscriminator( + username: string, + ): Promise<string | undefined> { if (Config.get().register.incrementingDiscriminators) { // discriminator will be incrementally generated // First we need to figure out the currently highest discrimnator for the given username and then increment it - const users = await User.find({ where: { username }, select: ["discriminator"] }); - const highestDiscriminator = Math.max(0, ...users.map((u) => Number(u.discriminator))); + const users = await User.find({ + where: { username }, + select: ["discriminator"], + }); + const highestDiscriminator = Math.max( + 0, + ...users.map((u) => Number(u.discriminator)), + ); const discriminator = highestDiscriminator + 1; if (discriminator >= 10000) { @@ -226,8 +284,13 @@ export class User extends BaseClass { // randomly generates a discriminator between 1 and 9999 and checks max five times if it already exists // TODO: is there any better way to generate a random discriminator only once, without checking if it already exists in the database? for (let tries = 0; tries < 5; tries++) { - const discriminator = Math.randomIntBetween(1, 9999).toString().padStart(4, "0"); - const exists = await User.findOne({ where: { discriminator, username: username }, select: ["id"] }); + const discriminator = Math.randomIntBetween(1, 9999) + .toString() + .padStart(4, "0"); + const exists = await User.findOne({ + where: { discriminator, username: username }, + select: ["id"], + }); if (!exists) return discriminator; } @@ -265,7 +328,8 @@ export class User extends BaseClass { // TODO: save date_of_birth // appearently discord doesn't save the date of birth and just calculate if nsfw is allowed // if nsfw_allowed is null/undefined it'll require date_of_birth to set it to true/false - const language = req.language === "en" ? "en-US" : req.language || "en-US"; + const language = + req.language === "en" ? "en-US" : req.language || "en-US"; const user = User.create({ created_at: new Date(), @@ -295,8 +359,8 @@ export class User extends BaseClass { }, settings: { ...defaultSettings, locale: language }, purchased_flags: 5, // TODO: idk what the values for this are - premium_usage_flags: 2, // TODO: idk what the values for this are - extended_settings: "", // TODO: was {} + premium_usage_flags: 2, // TODO: idk what the values for this are + extended_settings: "", // TODO: was {} fingerprints: [], }); @@ -305,7 +369,7 @@ export class User extends BaseClass { setImmediate(async () => { if (Config.get().guild.autoJoin.enabled) { for (const guild of Config.get().guild.autoJoin.guilds || []) { - await Member.addToGuild(user.id, guild).catch((e) => { }); + await Member.addToGuild(user.id, guild).catch((e) => {}); } } }); @@ -372,7 +436,7 @@ export interface UserSettings { disable_games_tab: boolean; enable_tts_command: boolean; explicit_content_filter: number; - friend_source_flags: { all: boolean; }; + friend_source_flags: { all: boolean }; gateway_connected: boolean; gif_auto_play: boolean; // every top guild is displayed as a "folder" diff --git a/src/util/entities/index.ts b/src/util/entities/index.ts index 49793810..c439a4b7 100644 --- a/src/util/entities/index.ts +++ b/src/util/entities/index.ts @@ -29,4 +29,4 @@ export * from "./VoiceState"; export * from "./Webhook"; export * from "./ClientRelease"; export * from "./BackupCodes"; -export * from "./Note"; \ No newline at end of file +export * from "./Note"; diff --git a/src/util/imports/OrmUtils.ts b/src/util/imports/OrmUtils.ts index 68a1932c..26652db0 100644 --- a/src/util/imports/OrmUtils.ts +++ b/src/util/imports/OrmUtils.ts @@ -9,7 +9,12 @@ export class OrmUtils { return !item.constructor || item.constructor === Object; } - private static mergeArrayKey(target: any, key: number, value: any, memo: Map<any, any>) { + private static mergeArrayKey( + target: any, + key: number, + value: any, + memo: Map<any, any>, + ) { // Have we seen this before? Prevent infinite recursion. if (memo.has(value)) { target[key] = memo.get(value); @@ -38,7 +43,12 @@ export class OrmUtils { memo.delete(value); } - private static mergeObjectKey(target: any, key: string, value: any, memo: Map<any, any>) { + private static mergeObjectKey( + target: any, + key: string, + value: any, + memo: Map<any, any>, + ) { // Have we seen this before? Prevent infinite recursion. if (memo.has(value)) { Object.assign(target, { [key]: memo.get(value) }); @@ -67,7 +77,11 @@ export class OrmUtils { memo.delete(value); } - private static merge(target: any, source: any, memo: Map<any, any> = new Map()): any { + private static merge( + target: any, + source: any, + memo: Map<any, any> = new Map(), + ): any { if (Array.isArray(target) && Array.isArray(source)) { for (let key = 0; key < source.length; key++) { this.mergeArrayKey(target, key, source[key], memo); @@ -93,4 +107,4 @@ export class OrmUtils { return target; } -} \ No newline at end of file +} diff --git a/src/util/imports/index.ts b/src/util/imports/index.ts index 5d9bfb5f..823151d9 100644 --- a/src/util/imports/index.ts +++ b/src/util/imports/index.ts @@ -1 +1 @@ -export * from "./OrmUtils"; \ No newline at end of file +export * from "./OrmUtils"; diff --git a/src/util/index.ts b/src/util/index.ts index 385070a3..3773c275 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -5,4 +5,4 @@ export * from "./interfaces/index"; export * from "./entities/index"; export * from "./dtos/index"; export * from "./schemas"; -export * from "./imports"; \ No newline at end of file +export * from "./imports"; diff --git a/src/util/interfaces/Activity.ts b/src/util/interfaces/Activity.ts index 9912e197..279ee40f 100644 --- a/src/util/interfaces/Activity.ts +++ b/src/util/interfaces/Activity.ts @@ -36,7 +36,8 @@ export interface Activity { id?: string; sync_id?: string; - metadata?: { // spotify + metadata?: { + // spotify context_uri?: string; album_id: string; artist_ids: string[]; diff --git a/src/util/interfaces/Event.ts b/src/util/interfaces/Event.ts index 59f995db..472fd572 100644 --- a/src/util/interfaces/Event.ts +++ b/src/util/interfaces/Event.ts @@ -75,7 +75,7 @@ export interface ReadyEventData { number, [[number, { e: number; s: number }[]]], [number, [[number, [number, number]]]], - { b: number; k: bigint[] }[] + { b: number; k: bigint[] }[], ][]; guild_join_requests?: any[]; // ? what is this? this is new shard?: [number, number]; diff --git a/src/util/migrations/1633864260873-EmojiRoles.ts b/src/util/migrations/1633864260873-EmojiRoles.ts index f0d709f2..31ced96b 100644 --- a/src/util/migrations/1633864260873-EmojiRoles.ts +++ b/src/util/migrations/1633864260873-EmojiRoles.ts @@ -4,10 +4,14 @@ export class EmojiRoles1633864260873 implements MigrationInterface { name = "EmojiRoles1633864260873"; public async up(queryRunner: QueryRunner): Promise<void> { - await queryRunner.query(`ALTER TABLE "emojis" ADD "roles" text NOT NULL DEFAULT ''`); + await queryRunner.query( + `ALTER TABLE "emojis" ADD "roles" text NOT NULL DEFAULT ''`, + ); } public async down(queryRunner: QueryRunner): Promise<void> { - await queryRunner.query(`ALTER TABLE "emojis" DROP COLUMN column_name "roles"`); + await queryRunner.query( + `ALTER TABLE "emojis" DROP COLUMN column_name "roles"`, + ); } } diff --git a/src/util/migrations/1633864669243-EmojiUser.ts b/src/util/migrations/1633864669243-EmojiUser.ts index 982405d7..9610216b 100644 --- a/src/util/migrations/1633864669243-EmojiUser.ts +++ b/src/util/migrations/1633864669243-EmojiUser.ts @@ -7,17 +7,21 @@ export class EmojiUser1633864669243 implements MigrationInterface { await queryRunner.query(`ALTER TABLE "emojis" ADD "user_id" varchar`); try { await queryRunner.query( - `ALTER TABLE "emojis" ADD CONSTRAINT FK_fa7ddd5f9a214e28ce596548421 FOREIGN KEY (user_id) REFERENCES users(id)` + `ALTER TABLE "emojis" ADD CONSTRAINT FK_fa7ddd5f9a214e28ce596548421 FOREIGN KEY (user_id) REFERENCES users(id)`, ); } catch (error) { console.error( - "sqlite doesn't support altering foreign keys: https://stackoverflow.com/questions/1884818/how-do-i-add-a-foreign-key-to-an-existing-sqlite-table" + "sqlite doesn't support altering foreign keys: https://stackoverflow.com/questions/1884818/how-do-i-add-a-foreign-key-to-an-existing-sqlite-table", ); } } public async down(queryRunner: QueryRunner): Promise<void> { - await queryRunner.query(`ALTER TABLE "emojis" DROP COLUMN column_name "user_id"`); - await queryRunner.query(`ALTER TABLE "emojis" DROP CONSTRAINT FK_fa7ddd5f9a214e28ce596548421`); + await queryRunner.query( + `ALTER TABLE "emojis" DROP COLUMN column_name "user_id"`, + ); + await queryRunner.query( + `ALTER TABLE "emojis" DROP CONSTRAINT FK_fa7ddd5f9a214e28ce596548421`, + ); } } diff --git a/src/util/migrations/1633881705509-VanityInvite.ts b/src/util/migrations/1633881705509-VanityInvite.ts index 45485310..16072473 100644 --- a/src/util/migrations/1633881705509-VanityInvite.ts +++ b/src/util/migrations/1633881705509-VanityInvite.ts @@ -5,15 +5,21 @@ export class VanityInvite1633881705509 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise<void> { try { - await queryRunner.query(`ALTER TABLE "emojis" DROP COLUMN vanity_url_code`); - await queryRunner.query(`ALTER TABLE "emojis" DROP CONSTRAINT FK_c2c1809d79eb120ea0cb8d342ad`); + await queryRunner.query( + `ALTER TABLE "emojis" DROP COLUMN vanity_url_code`, + ); + await queryRunner.query( + `ALTER TABLE "emojis" DROP CONSTRAINT FK_c2c1809d79eb120ea0cb8d342ad`, + ); } catch (error) {} } public async down(queryRunner: QueryRunner): Promise<void> { - await queryRunner.query(`ALTER TABLE "emojis" ADD vanity_url_code varchar`); await queryRunner.query( - `ALTER TABLE "emojis" ADD CONSTRAINT FK_c2c1809d79eb120ea0cb8d342ad FOREIGN KEY ("vanity_url_code") REFERENCES "invites"("code") ON DELETE NO ACTION ON UPDATE NO ACTION` + `ALTER TABLE "emojis" ADD vanity_url_code varchar`, + ); + await queryRunner.query( + `ALTER TABLE "emojis" ADD CONSTRAINT FK_c2c1809d79eb120ea0cb8d342ad FOREIGN KEY ("vanity_url_code") REFERENCES "invites"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`, ); } } diff --git a/src/util/migrations/1634308884591-Stickers.ts b/src/util/migrations/1634308884591-Stickers.ts index fbc4649f..f7b83242 100644 --- a/src/util/migrations/1634308884591-Stickers.ts +++ b/src/util/migrations/1634308884591-Stickers.ts @@ -1,35 +1,64 @@ -import { MigrationInterface, QueryRunner, Table, TableColumn, TableForeignKey } from "typeorm"; +import { + MigrationInterface, + QueryRunner, + Table, + TableColumn, + TableForeignKey, +} from "typeorm"; export class Stickers1634308884591 implements MigrationInterface { name = "Stickers1634308884591"; public async up(queryRunner: QueryRunner): Promise<void> { - await queryRunner.dropForeignKey("read_states", "FK_6f255d873cfbfd7a93849b7ff74"); + await queryRunner.dropForeignKey( + "read_states", + "FK_6f255d873cfbfd7a93849b7ff74", + ); await queryRunner.changeColumn( "stickers", "tags", - new TableColumn({ name: "tags", type: "varchar", isNullable: true }) + new TableColumn({ + name: "tags", + type: "varchar", + isNullable: true, + }), ); await queryRunner.changeColumn( "stickers", "pack_id", - new TableColumn({ name: "pack_id", type: "varchar", isNullable: true }) + new TableColumn({ + name: "pack_id", + type: "varchar", + isNullable: true, + }), + ); + await queryRunner.changeColumn( + "stickers", + "type", + new TableColumn({ name: "type", type: "integer" }), ); - await queryRunner.changeColumn("stickers", "type", new TableColumn({ name: "type", type: "integer" })); await queryRunner.changeColumn( "stickers", "format_type", - new TableColumn({ name: "format_type", type: "integer" }) + new TableColumn({ name: "format_type", type: "integer" }), ); await queryRunner.changeColumn( "stickers", "available", - new TableColumn({ name: "available", type: "boolean", isNullable: true }) + new TableColumn({ + name: "available", + type: "boolean", + isNullable: true, + }), ); await queryRunner.changeColumn( "stickers", "user_id", - new TableColumn({ name: "user_id", type: "boolean", isNullable: true }) + new TableColumn({ + name: "user_id", + type: "boolean", + isNullable: true, + }), ); await queryRunner.createForeignKey( "stickers", @@ -39,17 +68,33 @@ export class Stickers1634308884591 implements MigrationInterface { referencedColumnNames: ["id"], referencedTableName: "users", onDelete: "CASCADE", - }) + }), ); await queryRunner.createTable( new Table({ name: "sticker_packs", columns: [ - new TableColumn({ name: "id", type: "varchar", isPrimary: true }), + new TableColumn({ + name: "id", + type: "varchar", + isPrimary: true, + }), new TableColumn({ name: "name", type: "varchar" }), - new TableColumn({ name: "description", type: "varchar", isNullable: true }), - new TableColumn({ name: "banner_asset_id", type: "varchar", isNullable: true }), - new TableColumn({ name: "cover_sticker_id", type: "varchar", isNullable: true }), + new TableColumn({ + name: "description", + type: "varchar", + isNullable: true, + }), + new TableColumn({ + name: "banner_asset_id", + type: "varchar", + isNullable: true, + }), + new TableColumn({ + name: "cover_sticker_id", + type: "varchar", + isNullable: true, + }), ], foreignKeys: [ new TableForeignKey({ @@ -58,7 +103,7 @@ export class Stickers1634308884591 implements MigrationInterface { referencedTableName: "stickers", }), ], - }) + }), ); } diff --git a/src/util/migrations/1634424361103-Presence.ts b/src/util/migrations/1634424361103-Presence.ts index 729955b8..a71cb253 100644 --- a/src/util/migrations/1634424361103-Presence.ts +++ b/src/util/migrations/1634424361103-Presence.ts @@ -4,7 +4,10 @@ export class Presence1634424361103 implements MigrationInterface { name = "Presence1634424361103"; public async up(queryRunner: QueryRunner): Promise<void> { - queryRunner.addColumn("sessions", new TableColumn({ name: "activites", type: "text" })); + queryRunner.addColumn( + "sessions", + new TableColumn({ name: "activites", type: "text" }), + ); } public async down(queryRunner: QueryRunner): Promise<void> {} diff --git a/src/util/migrations/1634426540271-MigrationTimestamp.ts b/src/util/migrations/1634426540271-MigrationTimestamp.ts index 3208b25b..fb596906 100644 --- a/src/util/migrations/1634426540271-MigrationTimestamp.ts +++ b/src/util/migrations/1634426540271-MigrationTimestamp.ts @@ -7,7 +7,11 @@ export class MigrationTimestamp1634426540271 implements MigrationInterface { await queryRunner.changeColumn( "migrations", "timestamp", - new TableColumn({ name: "timestampe", type: "bigint", isNullable: false }) + new TableColumn({ + name: "timestampe", + type: "bigint", + isNullable: false, + }), ); } diff --git a/src/util/migrations/1660678870706-opencordFixes.ts b/src/util/migrations/1660678870706-opencordFixes.ts index 1f10c212..53c561ce 100644 --- a/src/util/migrations/1660678870706-opencordFixes.ts +++ b/src/util/migrations/1660678870706-opencordFixes.ts @@ -1,53 +1,52 @@ import { MigrationInterface, QueryRunner } from "typeorm"; export class opencordFixes1660678870706 implements MigrationInterface { - name = 'opencordFixes1660678870706' + name = "opencordFixes1660678870706"; - public async up(queryRunner: QueryRunner): Promise<void> { - await queryRunner.query(` + public async up(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` ALTER TABLE \`users\` ADD \`purchased_flags\` int NOT NULL `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE \`users\` ADD \`premium_usage_flags\` int NOT NULL `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE \`user_settings\` ADD \`friend_discovery_flags\` int NOT NULL `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE \`user_settings\` ADD \`view_nsfw_guilds\` tinyint NOT NULL `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE \`user_settings\` ADD \`passwordless\` tinyint NOT NULL `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE \`users\` CHANGE \`mfa_enabled\` \`mfa_enabled\` tinyint NOT NULL `); - } + } - public async down(queryRunner: QueryRunner): Promise<void> { - await queryRunner.query(` + public async down(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` ALTER TABLE \`users\` CHANGE \`mfa_enabled\` \`mfa_enabled\` tinyint NULL `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE \`user_settings\` DROP COLUMN \`passwordless\` `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE \`user_settings\` DROP COLUMN \`view_nsfw_guilds\` `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE \`user_settings\` DROP COLUMN \`friend_discovery_flags\` `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE \`users\` DROP COLUMN \`premium_usage_flags\` `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE \`users\` DROP COLUMN \`purchased_flags\` `); - } - -} \ No newline at end of file + } +} diff --git a/src/util/migrations/1660689892073-mobileFixes2.ts b/src/util/migrations/1660689892073-mobileFixes2.ts index bd28694e..63e7e032 100644 --- a/src/util/migrations/1660689892073-mobileFixes2.ts +++ b/src/util/migrations/1660689892073-mobileFixes2.ts @@ -1,37 +1,36 @@ import { MigrationInterface, QueryRunner } from "typeorm"; export class mobileFixes21660689892073 implements MigrationInterface { - name = 'mobileFixes21660689892073' + name = "mobileFixes21660689892073"; - public async up(queryRunner: QueryRunner): Promise<void> { - await queryRunner.query(` + public async up(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` ALTER TABLE \`user_settings\` ADD \`banner_color\` varchar(255) NULL `); await queryRunner.query(` UPDATE \`channels\` SET \`nsfw\` = 0 WHERE \`nsfw\` = NULL `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE \`channels\` CHANGE \`nsfw\` \`nsfw\` tinyint NOT NULL `); await queryRunner.query(` UPDATE \`guilds\` SET \`nsfw\` = 0 WHERE \`nsfw\` = NULL `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE \`guilds\` CHANGE \`nsfw\` \`nsfw\` tinyint NOT NULL `); - } + } - public async down(queryRunner: QueryRunner): Promise<void> { - await queryRunner.query(` + public async down(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` ALTER TABLE \`guilds\` CHANGE \`nsfw\` \`nsfw\` tinyint NULL `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE \`channels\` CHANGE \`nsfw\` \`nsfw\` tinyint NULL `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE \`user_settings\` DROP COLUMN \`banner_color\` `); - } - -} \ No newline at end of file + } +} diff --git a/src/util/schemas/ActivitySchema.ts b/src/util/schemas/ActivitySchema.ts index d316420e..5a3d205b 100644 --- a/src/util/schemas/ActivitySchema.ts +++ b/src/util/schemas/ActivitySchema.ts @@ -41,7 +41,8 @@ export const ActivitySchema = { $id: String, $sync_id: String, - $metadata: { // spotify + $metadata: { + // spotify $context_uri: String, album_id: String, artist_ids: [String], @@ -57,4 +58,4 @@ export interface ActivitySchema { status: Status; activities?: Activity[]; since?: number; // unix time (in milliseconds) of when the client went idle, or null if the client is not idle -} \ No newline at end of file +} diff --git a/src/util/schemas/BackupCodesChallengeSchema.ts b/src/util/schemas/BackupCodesChallengeSchema.ts index d6b519b7..8e2f0649 100644 --- a/src/util/schemas/BackupCodesChallengeSchema.ts +++ b/src/util/schemas/BackupCodesChallengeSchema.ts @@ -1,3 +1,3 @@ export interface BackupCodesChallengeSchema { password: string; -} \ No newline at end of file +} diff --git a/src/util/schemas/BanCreateSchema.ts b/src/util/schemas/BanCreateSchema.ts index 876b2a89..834577dc 100644 --- a/src/util/schemas/BanCreateSchema.ts +++ b/src/util/schemas/BanCreateSchema.ts @@ -1,4 +1,4 @@ export interface BanCreateSchema { delete_message_days?: string; reason?: string; -}; \ No newline at end of file +} diff --git a/src/util/schemas/BanModeratorSchema.ts b/src/util/schemas/BanModeratorSchema.ts index 8efa2402..afb76433 100644 --- a/src/util/schemas/BanModeratorSchema.ts +++ b/src/util/schemas/BanModeratorSchema.ts @@ -4,4 +4,4 @@ export interface BanModeratorSchema { guild_id: string; executor_id: string; reason?: string | undefined; -}; \ No newline at end of file +} diff --git a/src/util/schemas/BanRegistrySchema.ts b/src/util/schemas/BanRegistrySchema.ts index 8680d3db..501f94dc 100644 --- a/src/util/schemas/BanRegistrySchema.ts +++ b/src/util/schemas/BanRegistrySchema.ts @@ -5,4 +5,4 @@ export interface BanRegistrySchema { executor_id: string; ip?: string; reason?: string | undefined; -}; \ No newline at end of file +} diff --git a/src/util/schemas/BulkDeleteSchema.ts b/src/util/schemas/BulkDeleteSchema.ts index 6a71e052..bfc4df65 100644 --- a/src/util/schemas/BulkDeleteSchema.ts +++ b/src/util/schemas/BulkDeleteSchema.ts @@ -1,3 +1,3 @@ export interface BulkDeleteSchema { messages: string[]; -} \ No newline at end of file +} diff --git a/src/util/schemas/ChannelModifySchema.ts b/src/util/schemas/ChannelModifySchema.ts index 835ea2d7..9a07f983 100644 --- a/src/util/schemas/ChannelModifySchema.ts +++ b/src/util/schemas/ChannelModifySchema.ts @@ -27,4 +27,4 @@ export interface ChannelModifySchema { flags?: number; default_thread_rate_limit_per_user?: number; video_quality_mode?: number; -} \ No newline at end of file +} diff --git a/src/util/schemas/CodesVerificationSchema.ts b/src/util/schemas/CodesVerificationSchema.ts index e8e2e7b4..73c371eb 100644 --- a/src/util/schemas/CodesVerificationSchema.ts +++ b/src/util/schemas/CodesVerificationSchema.ts @@ -2,4 +2,4 @@ export interface CodesVerificationSchema { key: string; nonce: string; regenerate?: boolean; -} \ No newline at end of file +} diff --git a/src/util/schemas/DmChannelCreateSchema.ts b/src/util/schemas/DmChannelCreateSchema.ts index 04b8ff69..1b0fe86d 100644 --- a/src/util/schemas/DmChannelCreateSchema.ts +++ b/src/util/schemas/DmChannelCreateSchema.ts @@ -1,4 +1,4 @@ export interface DmChannelCreateSchema { name?: string; recipients: string[]; -} \ No newline at end of file +} diff --git a/src/util/schemas/EmojiCreateSchema.ts b/src/util/schemas/EmojiCreateSchema.ts index 8e2a2307..34084713 100644 --- a/src/util/schemas/EmojiCreateSchema.ts +++ b/src/util/schemas/EmojiCreateSchema.ts @@ -3,4 +3,4 @@ export interface EmojiCreateSchema { image: string; require_colons?: boolean | null; roles?: string[]; -} \ No newline at end of file +} diff --git a/src/util/schemas/EmojiModifySchema.ts b/src/util/schemas/EmojiModifySchema.ts index cd5b7e3e..05d2d395 100644 --- a/src/util/schemas/EmojiModifySchema.ts +++ b/src/util/schemas/EmojiModifySchema.ts @@ -1,4 +1,4 @@ export interface EmojiModifySchema { name?: string; roles?: string[]; -} \ No newline at end of file +} diff --git a/src/util/schemas/GuildCreateSchema.ts b/src/util/schemas/GuildCreateSchema.ts index 9b5f7dc2..f3de7007 100644 --- a/src/util/schemas/GuildCreateSchema.ts +++ b/src/util/schemas/GuildCreateSchema.ts @@ -11,4 +11,4 @@ export interface GuildCreateSchema { guild_template_code?: string; system_channel_id?: string; rules_channel_id?: string; -} \ No newline at end of file +} diff --git a/src/util/schemas/GuildTemplateCreateSchema.ts b/src/util/schemas/GuildTemplateCreateSchema.ts index 7caefcb8..59db8428 100644 --- a/src/util/schemas/GuildTemplateCreateSchema.ts +++ b/src/util/schemas/GuildTemplateCreateSchema.ts @@ -1,4 +1,4 @@ export interface GuildTemplateCreateSchema { name: string; avatar?: string | null; -} \ No newline at end of file +} diff --git a/src/util/schemas/GuildUpdateWelcomeScreenSchema.ts b/src/util/schemas/GuildUpdateWelcomeScreenSchema.ts index 0022da6e..e271b83e 100644 --- a/src/util/schemas/GuildUpdateWelcomeScreenSchema.ts +++ b/src/util/schemas/GuildUpdateWelcomeScreenSchema.ts @@ -7,4 +7,4 @@ export interface GuildUpdateWelcomeScreenSchema { }[]; enabled?: boolean; description?: string; -} \ No newline at end of file +} diff --git a/src/util/schemas/InviteCreateSchema.ts b/src/util/schemas/InviteCreateSchema.ts index 83ae22dd..cac11147 100644 --- a/src/util/schemas/InviteCreateSchema.ts +++ b/src/util/schemas/InviteCreateSchema.ts @@ -8,4 +8,4 @@ export interface InviteCreateSchema { unique?: boolean; target_user?: string; target_user_type?: number; -} \ No newline at end of file +} diff --git a/src/util/schemas/LoginSchema.ts b/src/util/schemas/LoginSchema.ts index 543d236c..dc889d94 100644 --- a/src/util/schemas/LoginSchema.ts +++ b/src/util/schemas/LoginSchema.ts @@ -5,4 +5,4 @@ export interface LoginSchema { captcha_key?: string; login_source?: string; gift_code_sku_id?: string; -} \ No newline at end of file +} diff --git a/src/util/schemas/MemberChangeSchema.ts b/src/util/schemas/MemberChangeSchema.ts index 566d7e20..2367bef3 100644 --- a/src/util/schemas/MemberChangeSchema.ts +++ b/src/util/schemas/MemberChangeSchema.ts @@ -1,4 +1,4 @@ export interface MemberChangeSchema { roles?: string[]; nick?: string; -} \ No newline at end of file +} diff --git a/src/util/schemas/MemberNickChangeSchema.ts b/src/util/schemas/MemberNickChangeSchema.ts index ed9fdb7b..d863038c 100644 --- a/src/util/schemas/MemberNickChangeSchema.ts +++ b/src/util/schemas/MemberNickChangeSchema.ts @@ -1,3 +1,3 @@ export interface MemberNickChangeSchema { nick: string; -} \ No newline at end of file +} diff --git a/src/util/schemas/MessageAcknowledgeSchema.ts b/src/util/schemas/MessageAcknowledgeSchema.ts index 1e7fb80d..194bb4b4 100644 --- a/src/util/schemas/MessageAcknowledgeSchema.ts +++ b/src/util/schemas/MessageAcknowledgeSchema.ts @@ -1,4 +1,4 @@ export interface MessageAcknowledgeSchema { manual?: boolean; mention_count?: number; -} \ No newline at end of file +} diff --git a/src/util/schemas/MessageCreateSchema.ts b/src/util/schemas/MessageCreateSchema.ts index 9d77c485..bf3470bb 100644 --- a/src/util/schemas/MessageCreateSchema.ts +++ b/src/util/schemas/MessageCreateSchema.ts @@ -30,4 +30,4 @@ export interface MessageCreateSchema { **/ attachments?: any[]; sticker_ids?: string[]; -} \ No newline at end of file +} diff --git a/src/util/schemas/MfaCodesSchema.ts b/src/util/schemas/MfaCodesSchema.ts index 226c43f1..ac05b9a4 100644 --- a/src/util/schemas/MfaCodesSchema.ts +++ b/src/util/schemas/MfaCodesSchema.ts @@ -1,4 +1,4 @@ export interface MfaCodesSchema { password: string; regenerate?: boolean; -} \ No newline at end of file +} diff --git a/src/util/schemas/ModifyGuildStickerSchema.ts b/src/util/schemas/ModifyGuildStickerSchema.ts index 06bf4ffe..159cc44f 100644 --- a/src/util/schemas/ModifyGuildStickerSchema.ts +++ b/src/util/schemas/ModifyGuildStickerSchema.ts @@ -12,4 +12,4 @@ export interface ModifyGuildStickerSchema { * @maxLength 200 */ tags: string; -} \ No newline at end of file +} diff --git a/src/util/schemas/PruneSchema.ts b/src/util/schemas/PruneSchema.ts index 60601d81..bea5e2b4 100644 --- a/src/util/schemas/PruneSchema.ts +++ b/src/util/schemas/PruneSchema.ts @@ -3,4 +3,4 @@ export interface PruneSchema { * @min 0 */ days: number; -} \ No newline at end of file +} diff --git a/src/util/schemas/PurgeSchema.ts b/src/util/schemas/PurgeSchema.ts index 8916be92..f5ab0a20 100644 --- a/src/util/schemas/PurgeSchema.ts +++ b/src/util/schemas/PurgeSchema.ts @@ -1,4 +1,4 @@ export interface PurgeSchema { before: string; after: string; -} \ No newline at end of file +} diff --git a/src/util/schemas/RegisterSchema.ts b/src/util/schemas/RegisterSchema.ts index c0cc3805..865f55b3 100644 --- a/src/util/schemas/RegisterSchema.ts +++ b/src/util/schemas/RegisterSchema.ts @@ -24,4 +24,4 @@ export interface RegisterSchema { captcha_key?: string; promotional_email_opt_in?: boolean; -} \ No newline at end of file +} diff --git a/src/util/schemas/RelationshipPostSchema.ts b/src/util/schemas/RelationshipPostSchema.ts index 3ff6eade..774c67f6 100644 --- a/src/util/schemas/RelationshipPostSchema.ts +++ b/src/util/schemas/RelationshipPostSchema.ts @@ -1,4 +1,4 @@ export interface RelationshipPostSchema { discriminator: string; username: string; -} \ No newline at end of file +} diff --git a/src/util/schemas/RelationshipPutSchema.ts b/src/util/schemas/RelationshipPutSchema.ts index 455f854e..0a7f9720 100644 --- a/src/util/schemas/RelationshipPutSchema.ts +++ b/src/util/schemas/RelationshipPutSchema.ts @@ -2,4 +2,4 @@ import { RelationshipType } from "@fosscord/util"; export interface RelationshipPutSchema { type?: RelationshipType; -} \ No newline at end of file +} diff --git a/src/util/schemas/RoleModifySchema.ts b/src/util/schemas/RoleModifySchema.ts index adb0c1a6..f3f4a20e 100644 --- a/src/util/schemas/RoleModifySchema.ts +++ b/src/util/schemas/RoleModifySchema.ts @@ -7,4 +7,4 @@ export interface RoleModifySchema { position?: number; icon?: string; unicode_emoji?: string; -} \ No newline at end of file +} diff --git a/src/util/schemas/SelectProtocolSchema.ts b/src/util/schemas/SelectProtocolSchema.ts index 92958e97..0ba0c23b 100644 --- a/src/util/schemas/SelectProtocolSchema.ts +++ b/src/util/schemas/SelectProtocolSchema.ts @@ -1,12 +1,12 @@ export interface SelectProtocolSchema { protocol: "webrtc" | "udp"; data: - | string - | { - address: string; - port: number; - mode: string; - }; + | string + | { + address: string; + port: number; + mode: string; + }; sdp?: string; codecs?: { name: "opus" | "VP8" | "VP9" | "H264"; @@ -16,4 +16,4 @@ export interface SelectProtocolSchema { rtx_payload_type?: number | null; }[]; rtc_connection_id?: string; // uuid -} \ No newline at end of file +} diff --git a/src/util/schemas/TemplateCreateSchema.ts b/src/util/schemas/TemplateCreateSchema.ts index 3f98f692..160934f5 100644 --- a/src/util/schemas/TemplateCreateSchema.ts +++ b/src/util/schemas/TemplateCreateSchema.ts @@ -1,4 +1,4 @@ export interface TemplateCreateSchema { name: string; description?: string; -} \ No newline at end of file +} diff --git a/src/util/schemas/TemplateModifySchema.ts b/src/util/schemas/TemplateModifySchema.ts index 3e6efb74..f9c9d14b 100644 --- a/src/util/schemas/TemplateModifySchema.ts +++ b/src/util/schemas/TemplateModifySchema.ts @@ -1,4 +1,4 @@ export interface TemplateModifySchema { name: string; description?: string; -} \ No newline at end of file +} diff --git a/src/util/schemas/TotpDisableSchema.ts b/src/util/schemas/TotpDisableSchema.ts index 05192bfa..51446e1c 100644 --- a/src/util/schemas/TotpDisableSchema.ts +++ b/src/util/schemas/TotpDisableSchema.ts @@ -1,3 +1,3 @@ export interface TotpDisableSchema { code: string; -} \ No newline at end of file +} diff --git a/src/util/schemas/TotpEnableSchema.ts b/src/util/schemas/TotpEnableSchema.ts index 7f6fb5a9..4e3551d9 100644 --- a/src/util/schemas/TotpEnableSchema.ts +++ b/src/util/schemas/TotpEnableSchema.ts @@ -2,4 +2,4 @@ export interface TotpEnableSchema { password: string; code?: string; secret?: string; -} \ No newline at end of file +} diff --git a/src/util/schemas/TotpSchema.ts b/src/util/schemas/TotpSchema.ts index 889cb443..941a92ec 100644 --- a/src/util/schemas/TotpSchema.ts +++ b/src/util/schemas/TotpSchema.ts @@ -1,6 +1,6 @@ export interface TotpSchema { - code: string, - ticket: string, - gift_code_sku_id?: string | null, - login_source?: string | null, -} \ No newline at end of file + code: string; + ticket: string; + gift_code_sku_id?: string | null; + login_source?: string | null; +} diff --git a/src/util/schemas/UserModifySchema.ts b/src/util/schemas/UserModifySchema.ts index 34e0f135..5327e34b 100644 --- a/src/util/schemas/UserModifySchema.ts +++ b/src/util/schemas/UserModifySchema.ts @@ -16,4 +16,4 @@ export interface UserModifySchema { code?: string; email?: string; discriminator?: string; -} \ No newline at end of file +} diff --git a/src/util/schemas/Validator.ts b/src/util/schemas/Validator.ts index b71bf6a1..e85cdf7b 100644 --- a/src/util/schemas/Validator.ts +++ b/src/util/schemas/Validator.ts @@ -3,7 +3,14 @@ import addFormats from "ajv-formats"; import fs from "fs"; import path from "path"; -const SchemaPath = path.join(__dirname, "..", "..", "..", "assets", "schemas.json"); +const SchemaPath = path.join( + __dirname, + "..", + "..", + "..", + "assets", + "schemas.json", +); const schemas = JSON.parse(fs.readFileSync(SchemaPath, { encoding: "utf8" })); export const ajv = new Ajv({ @@ -14,7 +21,7 @@ export const ajv = new Ajv({ coerceTypes: true, messages: true, strict: true, - strictRequired: true + strictRequired: true, }); addFormats(ajv); @@ -41,7 +48,14 @@ export const normalizeBody = (body: any = {}) => { } else { for (const [key, value] of Object.entries(object)) { if (value == null) { - if (key === "icon" || key === "avatar" || key === "banner" || key === "splash" || key === "discovery_splash") continue; + if ( + key === "icon" || + key === "avatar" || + key === "banner" || + key === "splash" || + key === "discovery_splash" + ) + continue; delete object[key]; } else if (typeof value === "object") { normalizeObject(value); @@ -51,4 +65,4 @@ export const normalizeBody = (body: any = {}) => { }; normalizeObject(body); return body; -}; \ No newline at end of file +}; diff --git a/src/util/schemas/VanityUrlSchema.ts b/src/util/schemas/VanityUrlSchema.ts index 28bf7f2b..4dd9b9da 100644 --- a/src/util/schemas/VanityUrlSchema.ts +++ b/src/util/schemas/VanityUrlSchema.ts @@ -4,4 +4,4 @@ export interface VanityUrlSchema { * @maxLength 20 */ code?: string; -} \ No newline at end of file +} diff --git a/src/util/schemas/VoiceIdentifySchema.ts b/src/util/schemas/VoiceIdentifySchema.ts index d48de347..df023713 100644 --- a/src/util/schemas/VoiceIdentifySchema.ts +++ b/src/util/schemas/VoiceIdentifySchema.ts @@ -9,4 +9,4 @@ export interface VoiceIdentifySchema { rid: string; quality: number; }[]; -} \ No newline at end of file +} diff --git a/src/util/schemas/VoiceStateUpdateSchema.ts b/src/util/schemas/VoiceStateUpdateSchema.ts index 5f805f4d..79e93b07 100644 --- a/src/util/schemas/VoiceStateUpdateSchema.ts +++ b/src/util/schemas/VoiceStateUpdateSchema.ts @@ -15,8 +15,8 @@ export const VoiceStateUpdateSchema = { $channel_id: String, self_mute: Boolean, self_deaf: Boolean, - $self_video: Boolean, //required in docs but bots don't always send it + $self_video: Boolean, //required in docs but bots don't always send it $preferred_region: String, $request_to_speak_timestamp: Date, $suppress: Boolean, -}; \ No newline at end of file +}; diff --git a/src/util/schemas/VoiceVideoSchema.ts b/src/util/schemas/VoiceVideoSchema.ts index 837ee1e7..0ba519e1 100644 --- a/src/util/schemas/VoiceVideoSchema.ts +++ b/src/util/schemas/VoiceVideoSchema.ts @@ -12,6 +12,6 @@ export interface VoiceVideoSchema { rtx_ssrc: number; max_bitrate: number; max_framerate: number; - max_resolution: { type: string; width: number; height: number; }; + max_resolution: { type: string; width: number; height: number }; }[]; -} \ No newline at end of file +} diff --git a/src/util/schemas/WebhookCreateSchema.ts b/src/util/schemas/WebhookCreateSchema.ts index c32b642d..99f6a12f 100644 --- a/src/util/schemas/WebhookCreateSchema.ts +++ b/src/util/schemas/WebhookCreateSchema.ts @@ -5,4 +5,4 @@ export interface WebhookCreateSchema { */ name: string; avatar: string; -} \ No newline at end of file +} diff --git a/src/util/schemas/WidgetModifySchema.ts b/src/util/schemas/WidgetModifySchema.ts index 3c84b3a1..26d4504f 100644 --- a/src/util/schemas/WidgetModifySchema.ts +++ b/src/util/schemas/WidgetModifySchema.ts @@ -1,4 +1,4 @@ export interface WidgetModifySchema { enabled: boolean; // whether the widget is enabled channel_id: string; // the widget channel id -} \ No newline at end of file +} diff --git a/src/util/schemas/index.ts b/src/util/schemas/index.ts index f86552f3..ae80de71 100644 --- a/src/util/schemas/index.ts +++ b/src/util/schemas/index.ts @@ -38,4 +38,4 @@ export * from "./VoiceStateUpdateSchema"; export * from "./VoiceVideoSchema"; export * from "./IdentifySchema"; export * from "./ActivitySchema"; -export * from "./LazyRequestSchema"; \ No newline at end of file +export * from "./LazyRequestSchema"; diff --git a/src/util/util/ApiError.ts b/src/util/util/ApiError.ts index f1a9b4f6..0fce6882 100644 --- a/src/util/util/ApiError.ts +++ b/src/util/util/ApiError.ts @@ -3,23 +3,34 @@ export class ApiError extends Error { readonly message: string, public readonly code: number, public readonly httpStatus: number = 400, - public readonly defaultParams?: string[] + public readonly defaultParams?: string[], ) { super(message); } withDefaultParams(): ApiError { if (this.defaultParams) - return new ApiError(applyParamsToString(this.message, this.defaultParams), this.code, this.httpStatus); + return new ApiError( + applyParamsToString(this.message, this.defaultParams), + this.code, + this.httpStatus, + ); return this; } withParams(...params: (string | number)[]): ApiError { - return new ApiError(applyParamsToString(this.message, params), this.code, this.httpStatus); + return new ApiError( + applyParamsToString(this.message, params), + this.code, + this.httpStatus, + ); } } -export function applyParamsToString(s: string, params: (string | number)[]): string { +export function applyParamsToString( + s: string, + params: (string | number)[], +): string { let newString = s; params.forEach((a) => { newString = newString.replace("{}", "" + a); diff --git a/src/util/util/AutoUpdate.ts b/src/util/util/AutoUpdate.ts index fd65ecf5..769d959f 100644 --- a/src/util/util/AutoUpdate.ts +++ b/src/util/util/AutoUpdate.ts @@ -1,6 +1,6 @@ import "missing-native-js-functions"; import fetch from "node-fetch"; -import ProxyAgent from 'proxy-agent'; +import ProxyAgent from "proxy-agent"; import readline from "readline"; import fs from "fs/promises"; import path from "path"; @@ -19,14 +19,17 @@ export function enableAutoUpdate(opts: { }) { if (!opts.checkInterval) return; var interval = 1000 * 60 * 60 * 24; - if (typeof opts.checkInterval === "number") opts.checkInterval = 1000 * interval; + if (typeof opts.checkInterval === "number") + opts.checkInterval = 1000 * interval; const i = setInterval(async () => { const currentVersion = await getCurrentVersion(opts.path); const latestVersion = await getLatestVersion(opts.packageJsonLink); if (currentVersion !== latestVersion) { clearInterval(i); - console.log(`[Auto Update] Current version (${currentVersion}) is out of date, updating ...`); + console.log( + `[Auto Update] Current version (${currentVersion}) is out of date, updating ...`, + ); await download(opts.downloadUrl, opts.path); } }, interval); @@ -43,7 +46,7 @@ export function enableAutoUpdate(opts: { } else { console.log(`[Auto update] aborted`); } - } + }, ); } }); @@ -65,7 +68,9 @@ async function download(url: string, dir: string) { async function getCurrentVersion(dir: string) { try { - const content = await fs.readFile(path.join(dir, "package.json"), { encoding: "utf8" }); + const content = await fs.readFile(path.join(dir, "package.json"), { + encoding: "utf8", + }); return JSON.parse(content).version; } catch (error) { throw new Error("[Auto update] couldn't get current version in " + dir); @@ -76,7 +81,7 @@ async function getLatestVersion(url: string) { try { const agent = new ProxyAgent(); const response = await fetch(url, { agent }); - const content = await response.json() as any; // TODO: types + const content = (await response.json()) as any; // TODO: types return content.version; } catch (error) { throw new Error("[Auto update] check failed for " + url); diff --git a/src/util/util/BannedWords.ts b/src/util/util/BannedWords.ts index 891a5980..74a82efe 100644 --- a/src/util/util/BannedWords.ts +++ b/src/util/util/BannedWords.ts @@ -6,7 +6,9 @@ var words: string[]; export const BannedWords = { init: async function init() { if (words) return words; - const file = (await fs.readFile(path.join(process.cwd(), "bannedWords"))).toString(); + const file = ( + await fs.readFile(path.join(process.cwd(), "bannedWords")) + ).toString(); if (!file) { words = []; return []; @@ -18,6 +20,6 @@ export const BannedWords = { get: () => words, find: (val: string) => { - return words.some(x => val.indexOf(x) != -1); - } -}; \ No newline at end of file + return words.some((x) => val.indexOf(x) != -1); + }, +}; diff --git a/src/util/util/BitField.ts b/src/util/util/BitField.ts index fb887e05..4e606660 100644 --- a/src/util/util/BitField.ts +++ b/src/util/util/BitField.ts @@ -3,7 +3,12 @@ // https://github.com/discordjs/discord.js/blob/master/src/util/BitField.js // Apache License Version 2.0 Copyright 2015 - 2021 Amish Shah -export type BitFieldResolvable = number | BigInt | BitField | string | BitFieldResolvable[]; +export type BitFieldResolvable = + | number + | BigInt + | BitField + | string + | BitFieldResolvable[]; /** * Data structure that makes it easy to interact with a bitfield. @@ -91,7 +96,8 @@ export class BitField { */ serialize() { const serialized: Record<string, boolean> = {}; - for (const [flag, bit] of Object.entries(BitField.FLAGS)) serialized[flag] = this.has(bit); + for (const [flag, bit] of Object.entries(BitField.FLAGS)) + serialized[flag] = this.has(bit); return serialized; } @@ -130,14 +136,21 @@ export class BitField { static resolve(bit: BitFieldResolvable = BigInt(0)): bigint { // @ts-ignore const FLAGS = this.FLAGS || this.constructor?.FLAGS; - if ((typeof bit === "number" || typeof bit === "bigint") && bit >= BigInt(0)) return BigInt(bit); + if ( + (typeof bit === "number" || typeof bit === "bigint") && + bit >= BigInt(0) + ) + return BigInt(bit); if (bit instanceof BitField) return bit.bitfield; if (Array.isArray(bit)) { // @ts-ignore const resolve = this.constructor?.resolve || this.resolve; - return bit.map((p) => resolve.call(this, p)).reduce((prev, p) => BigInt(prev) | BigInt(p), BigInt(0)); + return bit + .map((p) => resolve.call(this, p)) + .reduce((prev, p) => BigInt(prev) | BigInt(p), BigInt(0)); } - if (typeof bit === "string" && typeof FLAGS[bit] !== "undefined") return FLAGS[bit]; + if (typeof bit === "string" && typeof FLAGS[bit] !== "undefined") + return FLAGS[bit]; throw new RangeError("BITFIELD_INVALID: " + bit); } } diff --git a/src/util/util/Categories.ts b/src/util/util/Categories.ts index a3c69da7..cd706a8a 100644 --- a/src/util/util/Categories.ts +++ b/src/util/util/Categories.ts @@ -1 +1 @@ -//TODO: populate default discord categories + init, get and set methods \ No newline at end of file +//TODO: populate default discord categories + init, get and set methods diff --git a/src/util/util/Config.ts b/src/util/util/Config.ts index 31b8d97c..7fab7f90 100644 --- a/src/util/util/Config.ts +++ b/src/util/util/Config.ts @@ -1,5 +1,9 @@ import "missing-native-js-functions"; -import { ConfigValue, ConfigEntity, DefaultConfigOptions } from "../entities/Config"; +import { + ConfigValue, + ConfigEntity, + DefaultConfigOptions, +} from "../entities/Config"; import path from "path"; import fs from "fs"; @@ -42,7 +46,11 @@ export const Config = { function applyConfig(val: ConfigValue) { async function apply(obj: any, key = ""): Promise<any> { if (typeof obj === "object" && obj !== null) - return Promise.all(Object.keys(obj).map((k) => apply(obj[k], key ? `${key}_${k}` : k))); + return Promise.all( + Object.keys(obj).map((k) => + apply(obj[k], key ? `${key}_${k}` : k), + ), + ); let pair = pairs.find((x) => x.key === key); if (!pair) pair = new ConfigEntity(); @@ -67,7 +75,8 @@ function pairsToConfig(pairs: ConfigEntity[]) { let i = 0; for (const key of keys) { - if (!isNaN(Number(key)) && !prevObj[prev]?.length) prevObj[prev] = obj = []; + if (!isNaN(Number(key)) && !prevObj[prev]?.length) + prevObj[prev] = obj = []; if (i++ === keys.length - 1) obj[key] = p.value; else if (!obj[key]) obj[key] = {}; diff --git a/src/util/util/Constants.ts b/src/util/util/Constants.ts index 81a7165d..7c5b7dcb 100644 --- a/src/util/util/Constants.ts +++ b/src/util/util/Constants.ts @@ -77,9 +77,9 @@ export const VoiceOPCodes = { RESUME: 7, HELLO: 8, RESUMED: 9, - CLIENT_CONNECT: 12, // incorrect, op 12 is probably used for video - CLIENT_DISCONNECT: 13, // incorrect - VERSION: 16, //not documented + CLIENT_CONNECT: 12, // incorrect, op 12 is probably used for video + CLIENT_DISCONNECT: 13, // incorrect + VERSION: 16, //not documented }; export const Events = { @@ -160,7 +160,13 @@ export const ShardEvents = { * sidebar for more information.</warn> * @typedef {string} PartialType */ -export const PartialTypes = keyMirror(["USER", "CHANNEL", "GUILD_MEMBER", "MESSAGE", "REACTION"]); +export const PartialTypes = keyMirror([ + "USER", + "CHANNEL", + "GUILD_MEMBER", + "MESSAGE", + "REACTION", +]); /** * The type of a websocket message event, e.g. `MESSAGE_CREATE`. Here are the available events: @@ -291,7 +297,7 @@ export const MessageTypes = [ * @typedef {string} SystemMessageType */ export const SystemMessageTypes = MessageTypes.filter( - (type: string | null) => type && type !== "DEFAULT" && type !== "REPLY" + (type: string | null) => type && type !== "DEFAULT" && type !== "REPLY", ); /** @@ -305,7 +311,14 @@ export const SystemMessageTypes = MessageTypes.filter( * * COMPETING * @typedef {string} ActivityType */ -export const ActivityTypes = ["PLAYING", "STREAMING", "LISTENING", "WATCHING", "CUSTOM_STATUS", "COMPETING"]; +export const ActivityTypes = [ + "PLAYING", + "STREAMING", + "LISTENING", + "WATCHING", + "CUSTOM_STATUS", + "COMPETING", +]; export const ChannelTypes = { TEXT: 0, @@ -361,7 +374,11 @@ export const Colors = { * * ALL_MEMBERS * @typedef {string} ExplicitContentFilterLevel */ -export const ExplicitContentFilterLevels = ["DISABLED", "MEMBERS_WITHOUT_ROLES", "ALL_MEMBERS"]; +export const ExplicitContentFilterLevels = [ + "DISABLED", + "MEMBERS_WITHOUT_ROLES", + "ALL_MEMBERS", +]; /** * The value set for the verification levels for a guild: @@ -372,7 +389,13 @@ export const ExplicitContentFilterLevels = ["DISABLED", "MEMBERS_WITHOUT_ROLES", * * VERY_HIGH * @typedef {string} VerificationLevel */ -export const VerificationLevels = ["NONE", "LOW", "MEDIUM", "HIGH", "VERY_HIGH"]; +export const VerificationLevels = [ + "NONE", + "LOW", + "MEDIUM", + "HIGH", + "VERY_HIGH", +]; /** * An error encountered while performing an API request. Here are the potential errors: @@ -517,7 +540,10 @@ export const VerificationLevels = ["NONE", "LOW", "MEDIUM", "HIGH", "VERY_HIGH"] */ export const DiscordApiErrors = { //https://discord.com/developers/docs/topics/opcodes-and-status-codes#json-json-error-codes - GENERAL_ERROR: new ApiError("General error (such as a malformed request body, amongst other things)", 0), + GENERAL_ERROR: new ApiError( + "General error (such as a malformed request body, amongst other things)", + 0, + ), UNKNOWN_ACCOUNT: new ApiError("Unknown account", 10001), UNKNOWN_APPLICATION: new ApiError("Unknown application", 10002), UNKNOWN_CHANNEL: new ApiError("Unknown channel", 10003), @@ -542,185 +568,410 @@ export const DiscordApiErrors = { UNKNOWN_BUILD: new ApiError("Unknown build", 10030), UNKNOWN_LOBBY: new ApiError("Unknown lobby", 10031), UNKNOWN_BRANCH: new ApiError("Unknown branch", 10032), - UNKNOWN_STORE_DIRECTORY_LAYOUT: new ApiError("Unknown store directory layout", 10033), + UNKNOWN_STORE_DIRECTORY_LAYOUT: new ApiError( + "Unknown store directory layout", + 10033, + ), UNKNOWN_REDISTRIBUTABLE: new ApiError("Unknown redistributable", 10036), UNKNOWN_GIFT_CODE: new ApiError("Unknown gift code", 10038), UNKNOWN_STREAM: new ApiError("Unknown stream", 10049), - UNKNOWN_PREMIUM_SERVER_SUBSCRIBE_COOLDOWN: new ApiError("Unknown premium server subscribe cooldown", 10050), + UNKNOWN_PREMIUM_SERVER_SUBSCRIBE_COOLDOWN: new ApiError( + "Unknown premium server subscribe cooldown", + 10050, + ), UNKNOWN_GUILD_TEMPLATE: new ApiError("Unknown guild template", 10057), - UNKNOWN_DISCOVERABLE_SERVER_CATEGORY: new ApiError("Unknown discoverable server category", 10059), + UNKNOWN_DISCOVERABLE_SERVER_CATEGORY: new ApiError( + "Unknown discoverable server category", + 10059, + ), UNKNOWN_STICKER: new ApiError("Unknown sticker", 10060), UNKNOWN_INTERACTION: new ApiError("Unknown interaction", 10062), - UNKNOWN_APPLICATION_COMMAND: new ApiError("Unknown application command", 10063), - UNKNOWN_APPLICATION_COMMAND_PERMISSIONS: new ApiError("Unknown application command permissions", 10066), + UNKNOWN_APPLICATION_COMMAND: new ApiError( + "Unknown application command", + 10063, + ), + UNKNOWN_APPLICATION_COMMAND_PERMISSIONS: new ApiError( + "Unknown application command permissions", + 10066, + ), UNKNOWN_STAGE_INSTANCE: new ApiError("Unknown Stage Instance", 10067), - UNKNOWN_GUILD_MEMBER_VERIFICATION_FORM: new ApiError("Unknown Guild Member Verification Form", 10068), - UNKNOWN_GUILD_WELCOME_SCREEN: new ApiError("Unknown Guild Welcome Screen", 10069), - UNKNOWN_GUILD_SCHEDULED_EVENT: new ApiError("Unknown Guild Scheduled Event", 10070), - UNKNOWN_GUILD_SCHEDULED_EVENT_USER: new ApiError("Unknown Guild Scheduled Event User", 10071), - BOT_PROHIBITED_ENDPOINT: new ApiError("Bots cannot use this endpoint", 20001), + UNKNOWN_GUILD_MEMBER_VERIFICATION_FORM: new ApiError( + "Unknown Guild Member Verification Form", + 10068, + ), + UNKNOWN_GUILD_WELCOME_SCREEN: new ApiError( + "Unknown Guild Welcome Screen", + 10069, + ), + UNKNOWN_GUILD_SCHEDULED_EVENT: new ApiError( + "Unknown Guild Scheduled Event", + 10070, + ), + UNKNOWN_GUILD_SCHEDULED_EVENT_USER: new ApiError( + "Unknown Guild Scheduled Event User", + 10071, + ), + BOT_PROHIBITED_ENDPOINT: new ApiError( + "Bots cannot use this endpoint", + 20001, + ), BOT_ONLY_ENDPOINT: new ApiError("Only bots can use this endpoint", 20002), EXPLICIT_CONTENT_CANNOT_BE_SENT_TO_RECIPIENT: new ApiError( "Explicit content cannot be sent to the desired recipient(s)", - 20009 + 20009, ), ACTION_NOT_AUTHORIZED_ON_APPLICATION: new ApiError( "You are not authorized to perform this action on this application", - 20012 + 20012, + ), + SLOWMODE_RATE_LIMIT: new ApiError( + "This action cannot be performed due to slowmode rate limit", + 20016, + ), + ONLY_OWNER: new ApiError( + "Only the owner of this account can perform this action", + 20018, + ), + ANNOUNCEMENT_RATE_LIMITS: new ApiError( + "This message cannot be edited due to announcement rate limits", + 20022, + ), + CHANNEL_WRITE_RATELIMIT: new ApiError( + "The channel you are writing has hit the write rate limit", + 20028, ), - SLOWMODE_RATE_LIMIT: new ApiError("This action cannot be performed due to slowmode rate limit", 20016), - ONLY_OWNER: new ApiError("Only the owner of this account can perform this action", 20018), - ANNOUNCEMENT_RATE_LIMITS: new ApiError("This message cannot be edited due to announcement rate limits", 20022), - CHANNEL_WRITE_RATELIMIT: new ApiError("The channel you are writing has hit the write rate limit", 20028), WORDS_NOT_ALLOWED: new ApiError( "Your Stage topic, server name, server description, or channel names contain words that are not allowed", - 20031 - ), - GUILD_PREMIUM_LEVEL_TOO_LOW: new ApiError("Guild premium subscription level too low", 20035), - MAXIMUM_GUILDS: new ApiError("Maximum number of guilds reached ({})", 30001, undefined, ["100"]), - MAXIMUM_FRIENDS: new ApiError("Maximum number of friends reached ({})", 30002, undefined, ["1000"]), - MAXIMUM_PINS: new ApiError("Maximum number of pins reached for the channel ({})", 30003, undefined, ["50"]), - MAXIMUM_NUMBER_OF_RECIPIENTS_REACHED: new ApiError("Maximum number of recipients reached ({})", 30004, undefined, [ - "10", - ]), - MAXIMUM_ROLES: new ApiError("Maximum number of guild roles reached ({})", 30005, undefined, ["250"]), - MAXIMUM_WEBHOOKS: new ApiError("Maximum number of webhooks reached ({})", 30007, undefined, ["10"]), - MAXIMUM_NUMBER_OF_EMOJIS_REACHED: new ApiError("Maximum number of emojis reached", 30008), - MAXIMUM_REACTIONS: new ApiError("Maximum number of reactions reached ({})", 30010, undefined, ["20"]), - MAXIMUM_CHANNELS: new ApiError("Maximum number of guild channels reached ({})", 30013, undefined, ["500"]), - MAXIMUM_ATTACHMENTS: new ApiError("Maximum number of attachments in a message reached ({})", 30015, undefined, [ - "10", - ]), - MAXIMUM_INVITES: new ApiError("Maximum number of invites reached ({})", 30016, undefined, ["1000"]), - MAXIMUM_ANIMATED_EMOJIS: new ApiError("Maximum number of animated emojis reached", 30018), - MAXIMUM_SERVER_MEMBERS: new ApiError("Maximum number of server members reached", 30019), + 20031, + ), + GUILD_PREMIUM_LEVEL_TOO_LOW: new ApiError( + "Guild premium subscription level too low", + 20035, + ), + MAXIMUM_GUILDS: new ApiError( + "Maximum number of guilds reached ({})", + 30001, + undefined, + ["100"], + ), + MAXIMUM_FRIENDS: new ApiError( + "Maximum number of friends reached ({})", + 30002, + undefined, + ["1000"], + ), + MAXIMUM_PINS: new ApiError( + "Maximum number of pins reached for the channel ({})", + 30003, + undefined, + ["50"], + ), + MAXIMUM_NUMBER_OF_RECIPIENTS_REACHED: new ApiError( + "Maximum number of recipients reached ({})", + 30004, + undefined, + ["10"], + ), + MAXIMUM_ROLES: new ApiError( + "Maximum number of guild roles reached ({})", + 30005, + undefined, + ["250"], + ), + MAXIMUM_WEBHOOKS: new ApiError( + "Maximum number of webhooks reached ({})", + 30007, + undefined, + ["10"], + ), + MAXIMUM_NUMBER_OF_EMOJIS_REACHED: new ApiError( + "Maximum number of emojis reached", + 30008, + ), + MAXIMUM_REACTIONS: new ApiError( + "Maximum number of reactions reached ({})", + 30010, + undefined, + ["20"], + ), + MAXIMUM_CHANNELS: new ApiError( + "Maximum number of guild channels reached ({})", + 30013, + undefined, + ["500"], + ), + MAXIMUM_ATTACHMENTS: new ApiError( + "Maximum number of attachments in a message reached ({})", + 30015, + undefined, + ["10"], + ), + MAXIMUM_INVITES: new ApiError( + "Maximum number of invites reached ({})", + 30016, + undefined, + ["1000"], + ), + MAXIMUM_ANIMATED_EMOJIS: new ApiError( + "Maximum number of animated emojis reached", + 30018, + ), + MAXIMUM_SERVER_MEMBERS: new ApiError( + "Maximum number of server members reached", + 30019, + ), MAXIMUM_SERVER_CATEGORIES: new ApiError( "Maximum number of server categories has been reached ({})", 30030, undefined, - ["5"] + ["5"], + ), + GUILD_ALREADY_HAS_TEMPLATE: new ApiError( + "Guild already has a template", + 30031, + ), + MAXIMUM_THREAD_PARTICIPANTS: new ApiError( + "Max number of thread participants has been reached", + 30033, ), - GUILD_ALREADY_HAS_TEMPLATE: new ApiError("Guild already has a template", 30031), - MAXIMUM_THREAD_PARTICIPANTS: new ApiError("Max number of thread participants has been reached", 30033), MAXIMUM_BANS_FOR_NON_GUILD_MEMBERS: new ApiError( "Maximum number of bans for non-guild members have been exceeded", - 30035 + 30035, + ), + MAXIMUM_BANS_FETCHES: new ApiError( + "Maximum number of bans fetches has been reached", + 30037, ), - MAXIMUM_BANS_FETCHES: new ApiError("Maximum number of bans fetches has been reached", 30037), MAXIMUM_STICKERS: new ApiError("Maximum number of stickers reached", 30039), - MAXIMUM_PRUNE_REQUESTS: new ApiError("Maximum number of prune requests has been reached. Try again later", 30040), - UNAUTHORIZED: new ApiError("Unauthorized. Provide a valid token and try again", 40001), + MAXIMUM_PRUNE_REQUESTS: new ApiError( + "Maximum number of prune requests has been reached. Try again later", + 30040, + ), + UNAUTHORIZED: new ApiError( + "Unauthorized. Provide a valid token and try again", + 40001, + ), ACCOUNT_VERIFICATION_REQUIRED: new ApiError( "You need to verify your account in order to perform this action", - 40002 + 40002, + ), + OPENING_DIRECT_MESSAGES_TOO_FAST: new ApiError( + "You are opening direct messages too fast", + 40003, + ), + REQUEST_ENTITY_TOO_LARGE: new ApiError( + "Request entity too large. Try sending something smaller in size", + 40005, + ), + FEATURE_TEMPORARILY_DISABLED: new ApiError( + "This feature has been temporarily disabled server-side", + 40006, ), - OPENING_DIRECT_MESSAGES_TOO_FAST: new ApiError("You are opening direct messages too fast", 40003), - REQUEST_ENTITY_TOO_LARGE: new ApiError("Request entity too large. Try sending something smaller in size", 40005), - FEATURE_TEMPORARILY_DISABLED: new ApiError("This feature has been temporarily disabled server-side", 40006), USER_BANNED: new ApiError("The user is banned from this guild", 40007), - TARGET_USER_IS_NOT_CONNECTED_TO_VOICE: new ApiError("Target user is not connected to voice", 40032), - ALREADY_CROSSPOSTED: new ApiError("This message has already been crossposted", 40033), - APPLICATION_COMMAND_ALREADY_EXISTS: new ApiError("An application command with that name already exists", 40041), + TARGET_USER_IS_NOT_CONNECTED_TO_VOICE: new ApiError( + "Target user is not connected to voice", + 40032, + ), + ALREADY_CROSSPOSTED: new ApiError( + "This message has already been crossposted", + 40033, + ), + APPLICATION_COMMAND_ALREADY_EXISTS: new ApiError( + "An application command with that name already exists", + 40041, + ), MISSING_ACCESS: new ApiError("Missing access", 50001), INVALID_ACCOUNT_TYPE: new ApiError("Invalid account type", 50002), - CANNOT_EXECUTE_ON_DM: new ApiError("Cannot execute action on a DM channel", 50003), + CANNOT_EXECUTE_ON_DM: new ApiError( + "Cannot execute action on a DM channel", + 50003, + ), EMBED_DISABLED: new ApiError("Guild widget disabled", 50004), - CANNOT_EDIT_MESSAGE_BY_OTHER: new ApiError("Cannot edit a message authored by another user", 50005), - CANNOT_SEND_EMPTY_MESSAGE: new ApiError("Cannot send an empty message", 50006), - CANNOT_MESSAGE_USER: new ApiError("Cannot send messages to this user", 50007), - CANNOT_SEND_MESSAGES_IN_VOICE_CHANNEL: new ApiError("Cannot send messages in a voice channel", 50008), + CANNOT_EDIT_MESSAGE_BY_OTHER: new ApiError( + "Cannot edit a message authored by another user", + 50005, + ), + CANNOT_SEND_EMPTY_MESSAGE: new ApiError( + "Cannot send an empty message", + 50006, + ), + CANNOT_MESSAGE_USER: new ApiError( + "Cannot send messages to this user", + 50007, + ), + CANNOT_SEND_MESSAGES_IN_VOICE_CHANNEL: new ApiError( + "Cannot send messages in a voice channel", + 50008, + ), CHANNEL_VERIFICATION_LEVEL_TOO_HIGH: new ApiError( "Channel verification level is too high for you to gain access", - 50009 + 50009, + ), + OAUTH2_APPLICATION_BOT_ABSENT: new ApiError( + "OAuth2 application does not have a bot", + 50010, + ), + MAXIMUM_OAUTH2_APPLICATIONS: new ApiError( + "OAuth2 application limit reached", + 50011, ), - OAUTH2_APPLICATION_BOT_ABSENT: new ApiError("OAuth2 application does not have a bot", 50010), - MAXIMUM_OAUTH2_APPLICATIONS: new ApiError("OAuth2 application limit reached", 50011), INVALID_OAUTH_STATE: new ApiError("Invalid OAuth2 state", 50012), - MISSING_PERMISSIONS: new ApiError("You lack permissions to perform that action ({})", 50013, undefined, [""]), - INVALID_AUTHENTICATION_TOKEN: new ApiError("Invalid authentication token provided", 50014), + MISSING_PERMISSIONS: new ApiError( + "You lack permissions to perform that action ({})", + 50013, + undefined, + [""], + ), + INVALID_AUTHENTICATION_TOKEN: new ApiError( + "Invalid authentication token provided", + 50014, + ), NOTE_TOO_LONG: new ApiError("Note was too long", 50015), INVALID_BULK_DELETE_QUANTITY: new ApiError( "Provided too few or too many messages to delete. Must provide at least {} and fewer than {} messages to delete", 50016, undefined, - ["2", "100"] + ["2", "100"], ), CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL: new ApiError( "A message can only be pinned to the channel it was sent in", - 50019 - ), - INVALID_OR_TAKEN_INVITE_CODE: new ApiError("Invite code was either invalid or taken", 50020), - CANNOT_EXECUTE_ON_SYSTEM_MESSAGE: new ApiError("Cannot execute action on a system message", 50021), - CANNOT_EXECUTE_ON_THIS_CHANNEL_TYPE: new ApiError("Cannot execute action on this channel type", 50024), - INVALID_OAUTH_TOKEN: new ApiError("Invalid OAuth2 access token provided", 50025), - MISSING_REQUIRED_OAUTH2_SCOPE: new ApiError("Missing required OAuth2 scope", 50026), - INVALID_WEBHOOK_TOKEN_PROVIDED: new ApiError("Invalid webhook token provided", 50027), + 50019, + ), + INVALID_OR_TAKEN_INVITE_CODE: new ApiError( + "Invite code was either invalid or taken", + 50020, + ), + CANNOT_EXECUTE_ON_SYSTEM_MESSAGE: new ApiError( + "Cannot execute action on a system message", + 50021, + ), + CANNOT_EXECUTE_ON_THIS_CHANNEL_TYPE: new ApiError( + "Cannot execute action on this channel type", + 50024, + ), + INVALID_OAUTH_TOKEN: new ApiError( + "Invalid OAuth2 access token provided", + 50025, + ), + MISSING_REQUIRED_OAUTH2_SCOPE: new ApiError( + "Missing required OAuth2 scope", + 50026, + ), + INVALID_WEBHOOK_TOKEN_PROVIDED: new ApiError( + "Invalid webhook token provided", + 50027, + ), INVALID_ROLE: new ApiError("Invalid role", 50028), INVALID_RECIPIENT: new ApiError("Invalid Recipient(s)", 50033), - BULK_DELETE_MESSAGE_TOO_OLD: new ApiError("A message provided was too old to bulk delete", 50034), + BULK_DELETE_MESSAGE_TOO_OLD: new ApiError( + "A message provided was too old to bulk delete", + 50034, + ), INVALID_FORM_BODY: new ApiError( "Invalid form body (returned for both application/json and multipart/form-data bodies), or invalid Content-Type provided", - 50035 + 50035, ), INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT: new ApiError( "An invite was accepted to a guild the application's bot is not in", - 50036 + 50036, ), INVALID_API_VERSION: new ApiError("Invalid API version provided", 50041), - FILE_EXCEEDS_MAXIMUM_SIZE: new ApiError("File uploaded exceeds the maximum size", 50045), + FILE_EXCEEDS_MAXIMUM_SIZE: new ApiError( + "File uploaded exceeds the maximum size", + 50045, + ), INVALID_FILE_UPLOADED: new ApiError("Invalid file uploaded", 50046), - CANNOT_SELF_REDEEM_GIFT: new ApiError("Cannot self-redeem this gift", 50054), - PAYMENT_SOURCE_REQUIRED: new ApiError("Payment source required to redeem gift", 50070), + CANNOT_SELF_REDEEM_GIFT: new ApiError( + "Cannot self-redeem this gift", + 50054, + ), + PAYMENT_SOURCE_REQUIRED: new ApiError( + "Payment source required to redeem gift", + 50070, + ), CANNOT_DELETE_COMMUNITY_REQUIRED_CHANNEL: new ApiError( "Cannot delete a channel required for Community guilds", - 50074 + 50074, ), INVALID_STICKER_SENT: new ApiError("Invalid sticker sent", 50081), CANNOT_EDIT_ARCHIVED_THREAD: new ApiError( "Tried to perform an operation on an archived thread, such as editing a message or adding a user to the thread", - 50083 + 50083, + ), + INVALID_THREAD_NOTIFICATION_SETTINGS: new ApiError( + "Invalid thread notification settings", + 50084, ), - INVALID_THREAD_NOTIFICATION_SETTINGS: new ApiError("Invalid thread notification settings", 50084), BEFORE_EARLIER_THAN_THREAD_CREATION_DATE: new ApiError( "before value is earlier than the thread creation date", - 50085 + 50085, + ), + SERVER_NOT_AVAILABLE_IN_YOUR_LOCATION: new ApiError( + "This server is not available in your location", + 50095, ), - SERVER_NOT_AVAILABLE_IN_YOUR_LOCATION: new ApiError("This server is not available in your location", 50095), SERVER_NEEDS_MONETIZATION_ENABLED: new ApiError( "This server needs monetization enabled in order to perform this action", - 50097 + 50097, + ), + TWO_FACTOR_REQUIRED: new ApiError( + "Two factor is required for this operation", + 60003, + ), + NO_USERS_WITH_DISCORDTAG_EXIST: new ApiError( + "No users with DiscordTag exist", + 80004, ), - TWO_FACTOR_REQUIRED: new ApiError("Two factor is required for this operation", 60003), - NO_USERS_WITH_DISCORDTAG_EXIST: new ApiError("No users with DiscordTag exist", 80004), REACTION_BLOCKED: new ApiError("Reaction was blocked", 90001), - RESOURCE_OVERLOADED: new ApiError("API resource is currently overloaded. Try again a little later", 130000), + RESOURCE_OVERLOADED: new ApiError( + "API resource is currently overloaded. Try again a little later", + 130000, + ), STAGE_ALREADY_OPEN: new ApiError("The Stage is already open", 150006), - THREAD_ALREADY_CREATED_FOR_THIS_MESSAGE: new ApiError("A thread has already been created for this message", 160004), + THREAD_ALREADY_CREATED_FOR_THIS_MESSAGE: new ApiError( + "A thread has already been created for this message", + 160004, + ), THREAD_IS_LOCKED: new ApiError("Thread is locked", 160005), - MAXIMUM_NUMBER_OF_ACTIVE_THREADS: new ApiError("Maximum number of active threads reached", 160006), + MAXIMUM_NUMBER_OF_ACTIVE_THREADS: new ApiError( + "Maximum number of active threads reached", + 160006, + ), MAXIMUM_NUMBER_OF_ACTIVE_ANNOUNCEMENT_THREADS: new ApiError( "Maximum number of active announcement threads reached", - 160007 + 160007, + ), + INVALID_JSON_FOR_UPLOADED_LOTTIE_FILE: new ApiError( + "Invalid JSON for uploaded Lottie file", + 170001, ), - INVALID_JSON_FOR_UPLOADED_LOTTIE_FILE: new ApiError("Invalid JSON for uploaded Lottie file", 170001), LOTTIES_CANNOT_CONTAIN_RASTERIZED_IMAGES: new ApiError( "Uploaded Lotties cannot contain rasterized images such as PNG or JPEG", - 170002 + 170002, + ), + STICKER_MAXIMUM_FRAMERATE: new ApiError( + "Sticker maximum framerate exceeded", + 170003, + ), + STICKER_MAXIMUM_FRAME_COUNT: new ApiError( + "Sticker frame count exceeds maximum of {} frames", + 170004, + undefined, + ["1000"], + ), + LOTTIE_ANIMATION_MAXIMUM_DIMENSIONS: new ApiError( + "Lottie animation maximum dimensions exceeded", + 170005, ), - STICKER_MAXIMUM_FRAMERATE: new ApiError("Sticker maximum framerate exceeded", 170003), - STICKER_MAXIMUM_FRAME_COUNT: new ApiError("Sticker frame count exceeds maximum of {} frames", 170004, undefined, [ - "1000", - ]), - LOTTIE_ANIMATION_MAXIMUM_DIMENSIONS: new ApiError("Lottie animation maximum dimensions exceeded", 170005), STICKER_FRAME_RATE_TOO_SMALL_OR_TOO_LARGE: new ApiError( "Sticker frame rate is either too small or too large", - 170006 + 170006, ), STICKER_ANIMATION_DURATION_MAXIMUM: new ApiError( "Sticker animation duration exceeds maximum of {} seconds", 170007, undefined, - ["5"] + ["5"], ), //Other errors @@ -731,26 +982,92 @@ export const DiscordApiErrors = { * An error encountered while performing an API request (Fosscord only). Here are the potential errors: */ export const FosscordApiErrors = { - MANUALLY_TRIGGERED_ERROR: new ApiError("This is an artificial error", 1, 500), - PREMIUM_DISABLED_FOR_GUILD: new ApiError("This guild cannot be boosted", 25001), - NO_FURTHER_PREMIUM: new ApiError("This guild does not receive further boosts", 25002), - GUILD_PREMIUM_DISABLED_FOR_YOU: new ApiError("This guild cannot be boosted by you", 25003, 403), + MANUALLY_TRIGGERED_ERROR: new ApiError( + "This is an artificial error", + 1, + 500, + ), + PREMIUM_DISABLED_FOR_GUILD: new ApiError( + "This guild cannot be boosted", + 25001, + ), + NO_FURTHER_PREMIUM: new ApiError( + "This guild does not receive further boosts", + 25002, + ), + GUILD_PREMIUM_DISABLED_FOR_YOU: new ApiError( + "This guild cannot be boosted by you", + 25003, + 403, + ), CANNOT_FRIEND_SELF: new ApiError("Cannot friend oneself", 25009), - USER_SPECIFIC_INVITE_WRONG_RECIPIENT: new ApiError("This invite is not meant for you", 25010), + USER_SPECIFIC_INVITE_WRONG_RECIPIENT: new ApiError( + "This invite is not meant for you", + 25010, + ), USER_SPECIFIC_INVITE_FAILED: new ApiError("Failed to invite user", 25011), - CANNOT_MODIFY_USER_GROUP: new ApiError("This user cannot manipulate this group", 25050, 403), - CANNOT_REMOVE_SELF_FROM_GROUP: new ApiError("This user cannot remove oneself from user group", 25051), - CANNOT_BAN_OPERATOR: new ApiError("Non-OPERATOR cannot ban OPERATOR from instance", 25052), - CANNOT_LEAVE_GUILD: new ApiError("You are not allowed to leave guilds that you joined by yourself", 25059, 403), - EDITS_DISABLED: new ApiError("You are not allowed to edit your own messages", 25060, 403), - DELETE_MESSAGE_DISABLED: new ApiError("You are not allowed to delete your own messages", 25061, 403), - FEATURE_PERMANENTLY_DISABLED: new ApiError("This feature has been disabled server-side", 45006, 501), - MISSING_RIGHTS: new ApiError("You lack rights to perform that action ({})", 50013, undefined, [""]), - CANNOT_REPLACE_BY_BACKFILL: new ApiError("Cannot backfill to message ID that already exists", 55002, 409), - CANNOT_BACKFILL_TO_THE_FUTURE: new ApiError("You cannot backfill messages in the future", 55003), - CANNOT_GRANT_PERMISSIONS_EXCEEDING_RIGHTS: new ApiError("You cannot grant permissions exceeding your own rights", 50050), - ROUTES_LOOPING: new ApiError("Loops in the route definition ({})", 50060, undefined, [""]), - CANNOT_REMOVE_ROUTE: new ApiError("Cannot remove message route while it is in effect and being used", 50061), + CANNOT_MODIFY_USER_GROUP: new ApiError( + "This user cannot manipulate this group", + 25050, + 403, + ), + CANNOT_REMOVE_SELF_FROM_GROUP: new ApiError( + "This user cannot remove oneself from user group", + 25051, + ), + CANNOT_BAN_OPERATOR: new ApiError( + "Non-OPERATOR cannot ban OPERATOR from instance", + 25052, + ), + CANNOT_LEAVE_GUILD: new ApiError( + "You are not allowed to leave guilds that you joined by yourself", + 25059, + 403, + ), + EDITS_DISABLED: new ApiError( + "You are not allowed to edit your own messages", + 25060, + 403, + ), + DELETE_MESSAGE_DISABLED: new ApiError( + "You are not allowed to delete your own messages", + 25061, + 403, + ), + FEATURE_PERMANENTLY_DISABLED: new ApiError( + "This feature has been disabled server-side", + 45006, + 501, + ), + MISSING_RIGHTS: new ApiError( + "You lack rights to perform that action ({})", + 50013, + undefined, + [""], + ), + CANNOT_REPLACE_BY_BACKFILL: new ApiError( + "Cannot backfill to message ID that already exists", + 55002, + 409, + ), + CANNOT_BACKFILL_TO_THE_FUTURE: new ApiError( + "You cannot backfill messages in the future", + 55003, + ), + CANNOT_GRANT_PERMISSIONS_EXCEEDING_RIGHTS: new ApiError( + "You cannot grant permissions exceeding your own rights", + 50050, + ), + ROUTES_LOOPING: new ApiError( + "Loops in the route definition ({})", + 50060, + undefined, + [""], + ), + CANNOT_REMOVE_ROUTE: new ApiError( + "Cannot remove message route while it is in effect and being used", + 50061, + ), }; /** @@ -769,11 +1086,7 @@ export const DefaultMessageNotifications = ["ALL", "MENTIONS", "MUTED"]; * * INSERTED (Fosscord extension) * @typedef {string} MembershipStates */ -export const MembershipStates = [ - "INSERTED", - "INVITED", - "ACCEPTED", -]; +export const MembershipStates = ["INSERTED", "INVITED", "ACCEPTED"]; /** * The value set for a webhook's type: @@ -782,15 +1095,10 @@ export const MembershipStates = [ * * Custom (Fosscord extension) * @typedef {string} WebhookTypes */ -export const WebhookTypes = [ - "Custom", - "Incoming", - "Channel Follower", -]; +export const WebhookTypes = ["Custom", "Incoming", "Channel Follower"]; function keyMirror(arr: string[]) { let tmp = Object.create(null); for (const value of arr) tmp[value] = value; return tmp; } - diff --git a/src/util/util/Database.ts b/src/util/util/Database.ts index ddbea57d..e96be6c4 100644 --- a/src/util/util/Database.ts +++ b/src/util/util/Database.ts @@ -9,7 +9,8 @@ import { yellow, green, red } from "picocolors"; // We want to generate all id's with Snowflakes that's why we have our own BaseEntity class var dbConnection: DataSource | undefined; -let dbConnectionString = process.env.DATABASE || path.join(process.cwd(), "database.db"); +let dbConnectionString = + process.env.DATABASE || path.join(process.cwd(), "database.db"); export function getDatabase(): DataSource | null { // if (!dbConnection) throw new Error("Tried to get database before it was initialised"); @@ -20,18 +21,24 @@ export function getDatabase(): DataSource | null { export async function initDatabase(): Promise<DataSource> { if (dbConnection) return dbConnection; - const type = dbConnectionString.includes("://") ? dbConnectionString.split(":")[0]?.replace("+srv", "") : "sqlite"; + const type = dbConnectionString.includes("://") + ? dbConnectionString.split(":")[0]?.replace("+srv", "") + : "sqlite"; const isSqlite = type.includes("sqlite"); console.log(`[Database] ${yellow(`connecting to ${type} db`)}`); if (isSqlite) { - console.log(`[Database] ${red(`You are running sqlite! Please keep in mind that we recommend setting up a dedicated database!`)}`); + console.log( + `[Database] ${red( + `You are running sqlite! Please keep in mind that we recommend setting up a dedicated database!`, + )}`, + ); } const dataSource = new DataSource({ //@ts-ignore type, - charset: 'utf8mb4', + charset: "utf8mb4", url: isSqlite ? undefined : dbConnectionString, database: isSqlite ? dbConnectionString : undefined, entities: ["dist/util/entities/*.js"], diff --git a/src/util/util/Email.ts b/src/util/util/Email.ts index 6885da33..c98ccff0 100644 --- a/src/util/util/Email.ts +++ b/src/util/util/Email.ts @@ -15,7 +15,7 @@ export function adjustEmail(email?: string): string | undefined { // replace .dots and +alternatives -> Gmail Dot Trick https://support.google.com/mail/answer/7436150 and https://generator.email/blog/gmail-generator let v = user.replace(/[.]|(\+.*)/g, "") + "@gmail.com"; } - + if (domain === "google.com") { // replace .dots and +alternatives -> Google Staff GMail Dot Trick let v = user.replace(/[.]|(\+.*)/g, "") + "@google.com"; diff --git a/src/util/util/Event.ts b/src/util/util/Event.ts index 20a638a0..c81de951 100644 --- a/src/util/util/Event.ts +++ b/src/util/util/Event.ts @@ -5,15 +5,27 @@ import { EVENT, Event } from "../interfaces"; export const events = new EventEmitter(); export async function emitEvent(payload: Omit<Event, "created_at">) { - const id = (payload.channel_id || payload.user_id || payload.guild_id) as string; + const id = (payload.channel_id || + payload.user_id || + payload.guild_id) as string; if (!id) return console.error("event doesn't contain any id", payload); if (RabbitMQ.connection) { - const data = typeof payload.data === "object" ? JSON.stringify(payload.data) : payload.data; // use rabbitmq for event transmission - await RabbitMQ.channel?.assertExchange(id, "fanout", { durable: false }); + const data = + typeof payload.data === "object" + ? JSON.stringify(payload.data) + : payload.data; // use rabbitmq for event transmission + await RabbitMQ.channel?.assertExchange(id, "fanout", { + durable: false, + }); // assertQueue isn't needed, because a queue will automatically created if it doesn't exist - const successful = RabbitMQ.channel?.publish(id, "", Buffer.from(`${data}`), { type: payload.event }); + const successful = RabbitMQ.channel?.publish( + id, + "", + Buffer.from(`${data}`), + { type: payload.event }, + ); if (!successful) throw new Error("failed to send event"); } else if (process.env.EVENT_TRANSMISSION === "process") { process.send?.({ type: "event", event: payload, id } as ProcessEvent); @@ -48,10 +60,19 @@ export interface ProcessEvent { id: string; } -export async function listenEvent(event: string, callback: (event: EventOpts) => any, opts?: ListenEventOpts) { +export async function listenEvent( + event: string, + callback: (event: EventOpts) => any, + opts?: ListenEventOpts, +) { if (RabbitMQ.connection) { - // @ts-ignore - return rabbitListen(opts?.channel || RabbitMQ.channel, event, callback, { acknowledge: opts?.acknowledge }); + return rabbitListen( + // @ts-ignore + opts?.channel || RabbitMQ.channel, + event, + callback, + { acknowledge: opts?.acknowledge }, + ); } else if (process.env.EVENT_TRANSMISSION === "process") { const cancel = () => { process.removeListener("message", listener); @@ -59,7 +80,9 @@ export async function listenEvent(event: string, callback: (event: EventOpts) => }; const listener = (msg: ProcessEvent) => { - msg.type === "event" && msg.id === event && callback({ ...msg.event, cancel }); + msg.type === "event" && + msg.id === event && + callback({ ...msg.event, cancel }); }; //@ts-ignore apparently theres no function addListener with this signature @@ -84,10 +107,13 @@ async function rabbitListen( channel: Channel, id: string, callback: (event: EventOpts) => any, - opts?: { acknowledge?: boolean } + opts?: { acknowledge?: boolean }, ) { await channel.assertExchange(id, "fanout", { durable: false }); - const q = await channel.assertQueue("", { exclusive: true, autoDelete: true }); + const q = await channel.assertQueue("", { + exclusive: true, + autoDelete: true, + }); const cancel = () => { channel.cancel(q.queue); @@ -116,7 +142,7 @@ async function rabbitListen( }, { noAck: !opts?.acknowledge, - } + }, ); return cancel; diff --git a/src/util/util/FieldError.ts b/src/util/util/FieldError.ts index 406b33e8..24818fed 100644 --- a/src/util/util/FieldError.ts +++ b/src/util/util/FieldError.ts @@ -1,6 +1,8 @@ import "missing-native-js-functions"; -export function FieldErrors(fields: Record<string, { code?: string; message: string }>) { +export function FieldErrors( + fields: Record<string, { code?: string; message: string }>, +) { return new FieldError( 50035, "Invalid Form Body", @@ -11,7 +13,7 @@ export function FieldErrors(fields: Record<string, { code?: string; message: str code: code || "BASE_TYPE_INVALID", }, ], - })) + })), ); } @@ -19,7 +21,11 @@ export function FieldErrors(fields: Record<string, { code?: string; message: str // Ensure you use the proper content type (image/jpeg, image/png, image/gif) that matches the image data being provided. export class FieldError extends Error { - constructor(public code: string | number, public message: string, public errors?: any) { + constructor( + public code: string | number, + public message: string, + public errors?: any, + ) { super(message); } } diff --git a/src/util/util/Intents.ts b/src/util/util/Intents.ts index 1e840b76..b9f4d65a 100644 --- a/src/util/util/Intents.ts +++ b/src/util/util/Intents.ts @@ -22,13 +22,12 @@ export class Intents extends BitField { GUILD_POLICY_EXECUTION: BigInt(1) << BigInt(21), // guild policy execution LIVE_MESSAGE_COMPOSITION: BigInt(1) << BigInt(32), // allow composing messages using the gateway GUILD_ROUTES: BigInt(1) << BigInt(41), // message routes affecting the guild - DIRECT_MESSAGES_THREADS: BigInt(1) << BigInt(42), // direct message threads + DIRECT_MESSAGES_THREADS: BigInt(1) << BigInt(42), // direct message threads JUMBO_EVENTS: BigInt(1) << BigInt(43), // jumbo events (size limits to be defined later) LOBBIES: BigInt(1) << BigInt(44), // lobbies - INSTANCE_ROUTES: BigInt(1) << BigInt(60), // all message route changes + INSTANCE_ROUTES: BigInt(1) << BigInt(60), // all message route changes INSTANCE_GUILD_CHANGES: BigInt(1) << BigInt(61), // all guild create, guild object patch, split, merge and delete events INSTANCE_POLICY_UPDATES: BigInt(1) << BigInt(62), // all instance policy updates - INSTANCE_USER_UPDATES: BigInt(1) << BigInt(63) // all instance user updates + INSTANCE_USER_UPDATES: BigInt(1) << BigInt(63), // all instance user updates }; } - diff --git a/src/util/util/InvisibleCharacters.ts b/src/util/util/InvisibleCharacters.ts index a48cfab0..da295d68 100644 --- a/src/util/util/InvisibleCharacters.ts +++ b/src/util/util/InvisibleCharacters.ts @@ -1,56 +1,56 @@ -// List from https://invisible-characters.com/ -export const InvisibleCharacters = [ - '\u{9}', //Tab - //'\u{20}', //Space //categories can have spaces in them - '\u{ad}', //Soft hyphen - '\u{34f}', //Combining grapheme joiner - '\u{61c}', //Arabic letter mark - '\u{115f}', //Hangul choseong filler - '\u{1160}', //Hangul jungseong filler - '\u{17b4}', //Khmer vowel inherent AQ - '\u{17b5}', //Khmer vowel inherent AA - '\u{180e}', //Mongolian vowel separator - '\u{2000}', //En quad - '\u{2001}', //Em quad - '\u{2002}', //En space - '\u{2003}', //Em space - '\u{2004}', //Three-per-em space - '\u{2005}', //Four-per-em space - '\u{2006}', //Six-per-em space - '\u{2007}', //Figure space - '\u{2008}', //Punctuation space - '\u{2009}', //Thin space - '\u{200a}', //Hair space - '\u{200b}', //Zero width space - '\u{200c}', //Zero width non-joiner - '\u{200d}', //Zero width joiner - '\u{200e}', //Left-to-right mark - '\u{200f}', //Right-to-left mark - '\u{202f}', //Narrow no-break space - '\u{205f}', //Medium mathematical space - '\u{2060}', //Word joiner - '\u{2061}', //Function application - '\u{2062}', //Invisible times - '\u{2063}', //Invisible separator - '\u{2064}', //Invisible plus - '\u{206a}', //Inhibit symmetric swapping - '\u{206b}', //Activate symmetric swapping - '\u{206c}', //Inhibit arabic form shaping - '\u{206d}', //Activate arabic form shaping - '\u{206e}', //National digit shapes - '\u{206f}', //Nominal digit shapes - '\u{3000}', //Ideographic space - '\u{2800}', //Braille pattern blank - '\u{3164}', //Hangul filler - '\u{feff}', //Zero width no-break space - '\u{ffa0}', //Haldwidth hangul filler - '\u{1d159}', //Musical symbol null notehead - '\u{1d173}', //Musical symbol begin beam - '\u{1d174}', //Musical symbol end beam - '\u{1d175}', //Musical symbol begin tie - '\u{1d176}', //Musical symbol end tie - '\u{1d177}', //Musical symbol begin slur - '\u{1d178}', //Musical symbol end slur - '\u{1d179}', //Musical symbol begin phrase - '\u{1d17a}' //Musical symbol end phrase -]; \ No newline at end of file +// List from https://invisible-characters.com/ +export const InvisibleCharacters = [ + "\u{9}", //Tab + //'\u{20}', //Space //categories can have spaces in them + "\u{ad}", //Soft hyphen + "\u{34f}", //Combining grapheme joiner + "\u{61c}", //Arabic letter mark + "\u{115f}", //Hangul choseong filler + "\u{1160}", //Hangul jungseong filler + "\u{17b4}", //Khmer vowel inherent AQ + "\u{17b5}", //Khmer vowel inherent AA + "\u{180e}", //Mongolian vowel separator + "\u{2000}", //En quad + "\u{2001}", //Em quad + "\u{2002}", //En space + "\u{2003}", //Em space + "\u{2004}", //Three-per-em space + "\u{2005}", //Four-per-em space + "\u{2006}", //Six-per-em space + "\u{2007}", //Figure space + "\u{2008}", //Punctuation space + "\u{2009}", //Thin space + "\u{200a}", //Hair space + "\u{200b}", //Zero width space + "\u{200c}", //Zero width non-joiner + "\u{200d}", //Zero width joiner + "\u{200e}", //Left-to-right mark + "\u{200f}", //Right-to-left mark + "\u{202f}", //Narrow no-break space + "\u{205f}", //Medium mathematical space + "\u{2060}", //Word joiner + "\u{2061}", //Function application + "\u{2062}", //Invisible times + "\u{2063}", //Invisible separator + "\u{2064}", //Invisible plus + "\u{206a}", //Inhibit symmetric swapping + "\u{206b}", //Activate symmetric swapping + "\u{206c}", //Inhibit arabic form shaping + "\u{206d}", //Activate arabic form shaping + "\u{206e}", //National digit shapes + "\u{206f}", //Nominal digit shapes + "\u{3000}", //Ideographic space + "\u{2800}", //Braille pattern blank + "\u{3164}", //Hangul filler + "\u{feff}", //Zero width no-break space + "\u{ffa0}", //Haldwidth hangul filler + "\u{1d159}", //Musical symbol null notehead + "\u{1d173}", //Musical symbol begin beam + "\u{1d174}", //Musical symbol end beam + "\u{1d175}", //Musical symbol begin tie + "\u{1d176}", //Musical symbol end tie + "\u{1d177}", //Musical symbol begin slur + "\u{1d178}", //Musical symbol end slur + "\u{1d179}", //Musical symbol begin phrase + "\u{1d17a}", //Musical symbol end phrase +]; diff --git a/src/util/util/Permissions.ts b/src/util/util/Permissions.ts index a432af76..0c12487e 100644 --- a/src/util/util/Permissions.ts +++ b/src/util/util/Permissions.ts @@ -1,6 +1,12 @@ // https://github.com/discordjs/discord.js/blob/master/src/util/Permissions.js // Apache License Version 2.0 Copyright 2015 - 2021 Amish Shah -import { Channel, ChannelPermissionOverwrite, Guild, Member, Role } from "../entities"; +import { + Channel, + ChannelPermissionOverwrite, + Guild, + Member, + Role, +} from "../entities"; import { BitField } from "./BitField"; import "missing-native-js-functions"; import { BitFieldResolvable, BitFlag } from "./BitField"; @@ -13,7 +19,12 @@ try { HTTPError = Error; } -export type PermissionResolvable = bigint | number | Permissions | PermissionResolvable[] | PermissionString; +export type PermissionResolvable = + | bigint + | number + | Permissions + | PermissionResolvable[] + | PermissionString; type PermissionString = keyof typeof Permissions.FLAGS; @@ -80,14 +91,20 @@ export class Permissions extends BitField { }; any(permission: PermissionResolvable, checkAdmin = true) { - return (checkAdmin && super.any(Permissions.FLAGS.ADMINISTRATOR)) || super.any(permission); + return ( + (checkAdmin && super.any(Permissions.FLAGS.ADMINISTRATOR)) || + super.any(permission) + ); } /** * Checks whether the bitfield has a permission, or multiple permissions. */ has(permission: PermissionResolvable, checkAdmin = true) { - return (checkAdmin && super.has(Permissions.FLAGS.ADMINISTRATOR)) || super.has(permission); + return ( + (checkAdmin && super.has(Permissions.FLAGS.ADMINISTRATOR)) || + super.has(permission) + ); } /** @@ -96,28 +113,39 @@ export class Permissions extends BitField { hasThrow(permission: PermissionResolvable) { if (this.has(permission) && this.has("VIEW_CHANNEL")) return true; // @ts-ignore - throw new HTTPError(`You are missing the following permissions ${permission}`, 403); + throw new HTTPError( + `You are missing the following permissions ${permission}`, + 403, + ); } overwriteChannel(overwrites: ChannelPermissionOverwrite[]) { if (!overwrites) return this; if (!this.cache) throw new Error("permission chache not available"); overwrites = overwrites.filter((x) => { - if (x.type === 0 && this.cache.roles?.some((r) => r.id === x.id)) return true; + if (x.type === 0 && this.cache.roles?.some((r) => r.id === x.id)) + return true; if (x.type === 1 && x.id == this.cache.user_id) return true; return false; }); - return new Permissions(Permissions.channelPermission(overwrites, this.bitfield)); + return new Permissions( + Permissions.channelPermission(overwrites, this.bitfield), + ); } - static channelPermission(overwrites: ChannelPermissionOverwrite[], init?: bigint) { + static channelPermission( + overwrites: ChannelPermissionOverwrite[], + init?: bigint, + ) { // TODO: do not deny any permissions if admin return overwrites.reduce((permission, overwrite) => { // apply disallowed permission // * permission: current calculated permission (e.g. 010) // * deny contains all denied permissions (e.g. 011) // * allow contains all explicitly allowed permisions (e.g. 100) - return (permission & ~BigInt(overwrite.deny)) | BigInt(overwrite.allow); + return ( + (permission & ~BigInt(overwrite.deny)) | BigInt(overwrite.allow) + ); // ~ operator inverts deny (e.g. 011 -> 100) // & operator only allows 1 for both ~deny and permission (e.g. 010 & 100 -> 000) // | operators adds both together (e.g. 000 + 100 -> 100) @@ -126,7 +154,10 @@ export class Permissions extends BitField { static rolePermission(roles: Role[]) { // adds all permissions of all roles together (Bit OR) - return roles.reduce((permission, role) => permission | BigInt(role.permissions), BigInt(0)); + return roles.reduce( + (permission, role) => permission | BigInt(role.permissions), + BigInt(0), + ); } static finalPermission({ @@ -157,7 +188,8 @@ export class Permissions extends BitField { } if (channel?.recipient_ids) { - if (channel?.owner_id === user.id) return new Permissions("ADMINISTRATOR"); + if (channel?.owner_id === user.id) + return new Permissions("ADMINISTRATOR"); if (channel.recipient_ids.includes(user.id)) { // Default dm permissions return new Permissions([ @@ -183,7 +215,10 @@ export class Permissions extends BitField { } } -const ALL_PERMISSIONS = Object.values(Permissions.FLAGS).reduce((total, val) => total | val, BigInt(0)); +const ALL_PERMISSIONS = Object.values(Permissions.FLAGS).reduce( + (total, val) => total | val, + BigInt(0), +); export type PermissionCache = { channel?: Channel | undefined; @@ -204,7 +239,7 @@ export async function getPermission( channel_relations?: string[]; member_select?: (keyof Member)[]; member_relations?: string[]; - } = {} + } = {}, ) { if (!user_id) throw new HTTPError("User not found"); var channel: Channel | undefined; @@ -239,16 +274,17 @@ export async function getPermission( ], relations: opts.guild_relations, }); - if (guild.owner_id === user_id) return new Permissions(Permissions.FLAGS.ADMINISTRATOR); + if (guild.owner_id === user_id) + return new Permissions(Permissions.FLAGS.ADMINISTRATOR); member = await Member.findOneOrFail({ where: { guild_id, id: user_id }, relations: ["roles", ...(opts.member_relations || [])], // select: [ - // "id", // TODO: Bug in typeorm? adding these selects breaks the query. - // "roles", - // @ts-ignore - // ...(opts.member_select || []), + // "id", // TODO: Bug in typeorm? adding these selects breaks the query. + // "roles", + // @ts-ignore + // ...(opts.member_select || []), // ], }); } diff --git a/src/util/util/RabbitMQ.ts b/src/util/util/RabbitMQ.ts index 0f5eb6aa..1bfb3f5c 100644 --- a/src/util/util/RabbitMQ.ts +++ b/src/util/util/RabbitMQ.ts @@ -1,7 +1,11 @@ import amqp, { Connection, Channel } from "amqplib"; // import Config from "./Config"; -export const RabbitMQ: { connection: Connection | null; channel: Channel | null; init: () => Promise<void> } = { +export const RabbitMQ: { + connection: Connection | null; + channel: Channel | null; + init: () => Promise<void>; +} = { connection: null, channel: null, init: async function () { diff --git a/src/util/util/Rights.ts b/src/util/util/Rights.ts index b28c75b7..659353a6 100644 --- a/src/util/util/Rights.ts +++ b/src/util/util/Rights.ts @@ -11,7 +11,12 @@ try { HTTPError = Error; } -export type RightResolvable = bigint | number | Rights | RightResolvable[] | RightString; +export type RightResolvable = + | bigint + | number + | Rights + | RightResolvable[] + | RightString; type RightString = keyof typeof Rights.FLAGS; // TODO: just like roles for members, users should have privilidges which combine multiple rights into one and make it easy to assign @@ -60,7 +65,7 @@ export class Rights extends BitField { CREDITABLE: BitFlag(32), // can receive money from monetisation related features KICK_BAN_MEMBERS: BitFlag(33), // can kick or ban guild or group DM members in the guilds/groups that they have KICK_MEMBERS, or BAN_MEMBERS - SELF_LEAVE_GROUPS: BitFlag(34), + SELF_LEAVE_GROUPS: BitFlag(34), // can leave the guilds or group DMs that they joined on their own (one can always leave a guild or group DMs they have been force-added) PRESENCE: BitFlag(35), // inverts the presence confidentiality default (OPERATOR's presence is not routed by default, others' are) for a given user @@ -72,31 +77,44 @@ export class Rights extends BitField { RESPOND_TO_INTERACTIONS: BitFlag(41), // can respond to interactions SEND_BACKDATED_EVENTS: BitFlag(42), // can send backdated events USE_MASS_INVITES: BitFlag(43), // added per @xnacly's request — can accept mass invites - ACCEPT_INVITES: BitFlag(44) // added per @xnacly's request — can accept user-specific invites and DM requests + ACCEPT_INVITES: BitFlag(44), // added per @xnacly's request — can accept user-specific invites and DM requests }; any(permission: RightResolvable, checkOperator = true) { - return (checkOperator && super.any(Rights.FLAGS.OPERATOR)) || super.any(permission); + return ( + (checkOperator && super.any(Rights.FLAGS.OPERATOR)) || + super.any(permission) + ); } has(permission: RightResolvable, checkOperator = true) { - return (checkOperator && super.has(Rights.FLAGS.OPERATOR)) || super.has(permission); + return ( + (checkOperator && super.has(Rights.FLAGS.OPERATOR)) || + super.has(permission) + ); } hasThrow(permission: RightResolvable) { if (this.has(permission)) return true; // @ts-ignore - throw new HTTPError(`You are missing the following rights ${permission}`, 403); + throw new HTTPError( + `You are missing the following rights ${permission}`, + 403, + ); } - } -const ALL_RIGHTS = Object.values(Rights.FLAGS).reduce((total, val) => total | val, BigInt(0)); +const ALL_RIGHTS = Object.values(Rights.FLAGS).reduce( + (total, val) => total | val, + BigInt(0), +); -export async function getRights( user_id: string +export async function getRights( + user_id: string, /**, opts: { in_behalf?: (keyof User)[]; - } = {} **/) { + } = {} **/ +) { let user = await User.findOneOrFail({ where: { id: user_id } }); return new Rights(user.rights); -} +} diff --git a/src/util/util/Snowflake.ts b/src/util/util/Snowflake.ts index 134d526e..69effb2e 100644 --- a/src/util/util/Snowflake.ts +++ b/src/util/util/Snowflake.ts @@ -17,7 +17,9 @@ export class Snowflake { static workerId = BigInt((cluster.worker?.id || 0) % 31); // max 31 constructor() { - throw new Error(`The ${this.constructor.name} class may not be instantiated.`); + throw new Error( + `The ${this.constructor.name} class may not be instantiated.`, + ); } /** @@ -83,14 +85,15 @@ export class Snowflake { return dec; } - static generateWorkerProcess() { // worker process - returns a number + static generateWorkerProcess() { + // worker process - returns a number var time = BigInt(Date.now() - Snowflake.EPOCH) << BigInt(22); var worker = Snowflake.workerId << 17n; var process = Snowflake.processId << 12n; var increment = Snowflake.INCREMENT++; return BigInt(time | worker | process | increment); } - + static generate() { return Snowflake.generateWorkerProcess().toString(); } @@ -111,7 +114,9 @@ export class Snowflake { * @returns {DeconstructedSnowflake} Deconstructed snowflake */ static deconstruct(snowflake) { - const BINARY = Snowflake.idToBinary(snowflake).toString(2).padStart(64, "0"); + const BINARY = Snowflake.idToBinary(snowflake) + .toString(2) + .padStart(64, "0"); const res = { timestamp: parseInt(BINARY.substring(0, 42), 2) + Snowflake.EPOCH, workerID: parseInt(BINARY.substring(42, 47), 2), diff --git a/src/util/util/Token.ts b/src/util/util/Token.ts index 5ba3e1ec..19e64f47 100644 --- a/src/util/util/Token.ts +++ b/src/util/util/Token.ts @@ -17,11 +17,14 @@ export function checkToken(token: string, jwtSecret: string): Promise<any> { const user = await User.findOne({ where: { id: decoded.id }, - select: ["data", "bot", "disabled", "deleted", "rights"] + select: ["data", "bot", "disabled", "deleted", "rights"], }); if (!user) return rej("Invalid Token"); // we need to round it to seconds as it saved as seconds in jwt iat and valid_tokens_since is stored in milliseconds - if (decoded.iat * 1000 < new Date(user.data.valid_tokens_since).setSeconds(0, 0)) + if ( + decoded.iat * 1000 < + new Date(user.data.valid_tokens_since).setSeconds(0, 0) + ) return rej("Invalid Token"); if (user.disabled) return rej("User disabled"); if (user.deleted) return rej("User not found"); @@ -45,7 +48,7 @@ export async function generateToken(id: string) { (err, token) => { if (err) return rej(err); return res(token); - } + }, ); }); } diff --git a/src/util/util/TraverseDirectory.ts b/src/util/util/TraverseDirectory.ts index 3d0d6279..3f5b0385 100644 --- a/src/util/util/TraverseDirectory.ts +++ b/src/util/util/TraverseDirectory.ts @@ -1,13 +1,14 @@ import { Server, traverseDirectory } from "lambert-server"; //if we're using ts-node, use ts files instead of js -const extension = Symbol.for("ts-node.register.instance") in process ? "ts" : "js" +const extension = + Symbol.for("ts-node.register.instance") in process ? "ts" : "js"; -const DEFAULT_FILTER = new RegExp("^([^\.].*)(?<!\.d)\.(" + extension + ")$"); +const DEFAULT_FILTER = new RegExp("^([^.].*)(?<!.d).(" + extension + ")$"); export function registerRoutes(server: Server, root: string) { return traverseDirectory( { dirname: root, recursive: true, filter: DEFAULT_FILTER }, - server.registerRoute.bind(server, root) + server.registerRoute.bind(server, root), ); } diff --git a/src/util/util/cdn.ts b/src/util/util/cdn.ts index 812a4e1d..6b2c8424 100644 --- a/src/util/util/cdn.ts +++ b/src/util/util/cdn.ts @@ -4,7 +4,10 @@ import fetch from "node-fetch"; import { Attachment } from "../entities"; import { Config } from "./Config"; -export async function uploadFile(path: string, file?: Express.Multer.File): Promise<Attachment> { +export async function uploadFile( + path: string, + file?: Express.Multer.File, +): Promise<Attachment> { if (!file?.buffer) throw new HTTPError("Missing file in body"); const form = new FormData(); @@ -13,28 +16,38 @@ export async function uploadFile(path: string, file?: Express.Multer.File): Prom filename: file.originalname, }); - const response = await fetch(`${Config.get().cdn.endpointPrivate || "http://localhost:3003"}${path}`, { - headers: { - signature: Config.get().security.requestSignature, - ...form.getHeaders(), + const response = await fetch( + `${Config.get().cdn.endpointPrivate || "http://localhost:3003"}${path}`, + { + headers: { + signature: Config.get().security.requestSignature, + ...form.getHeaders(), + }, + method: "POST", + body: form, }, - method: "POST", - body: form, - }); - const result = await response.json() as Attachment; + ); + const result = (await response.json()) as Attachment; if (response.status !== 200) throw result; return result; } -export async function handleFile(path: string, body?: string): Promise<string | undefined> { +export async function handleFile( + path: string, + body?: string, +): Promise<string | undefined> { if (!body || !body.startsWith("data:")) return undefined; try { const mimetype = body.split(":")[1].split(";")[0]; const buffer = Buffer.from(body.split(",")[1], "base64"); // @ts-ignore - const { id } = await uploadFile(path, { buffer, mimetype, originalname: "banner" }); + const { id } = await uploadFile(path, { + buffer, + mimetype, + originalname: "banner", + }); return id; } catch (error) { console.error(error); @@ -43,12 +56,15 @@ export async function handleFile(path: string, body?: string): Promise<string | } export async function deleteFile(path: string) { - const response = await fetch(`${Config.get().cdn.endpointPrivate || "http://localhost:3003"}${path}`, { - headers: { - signature: Config.get().security.requestSignature, + const response = await fetch( + `${Config.get().cdn.endpointPrivate || "http://localhost:3003"}${path}`, + { + headers: { + signature: Config.get().security.requestSignature, + }, + method: "DELETE", }, - method: "DELETE", - }); + ); const result = await response.json(); if (response.status !== 200) throw result; diff --git a/src/util/util/index.ts b/src/util/util/index.ts index b2bd6489..aa38e472 100644 --- a/src/util/util/index.ts +++ b/src/util/util/index.ts @@ -20,4 +20,4 @@ export * from "./String"; export * from "./Array"; export * from "./TraverseDirectory"; export * from "./InvisibleCharacters"; -export * from "./BannedWords"; \ No newline at end of file +export * from "./BannedWords"; diff --git a/src/webrtc/Server.ts b/src/webrtc/Server.ts index 32b795ea..d9a892a3 100644 --- a/src/webrtc/Server.ts +++ b/src/webrtc/Server.ts @@ -11,7 +11,15 @@ export class Server { public server: http.Server; public production: boolean; - constructor({ port, server, production }: { port: number; server?: http.Server; production?: boolean }) { + constructor({ + port, + server, + production, + }: { + port: number; + server?: http.Server; + production?: boolean; + }) { this.port = port; this.production = production || false; @@ -53,4 +61,4 @@ export class Server { closeDatabase(); this.server.close(); } -} \ No newline at end of file +} diff --git a/src/webrtc/events/Close.ts b/src/webrtc/events/Close.ts index 1c203653..4cf80bb2 100644 --- a/src/webrtc/events/Close.ts +++ b/src/webrtc/events/Close.ts @@ -6,4 +6,4 @@ export async function onClose(this: WebSocket, code: number, reason: string) { if (this.session_id) await Session.delete({ session_id: this.session_id }); this.removeAllListeners(); -} \ No newline at end of file +} diff --git a/src/webrtc/events/Connection.ts b/src/webrtc/events/Connection.ts index bf228d64..9300b6b2 100644 --- a/src/webrtc/events/Connection.ts +++ b/src/webrtc/events/Connection.ts @@ -14,7 +14,11 @@ try { // TODO: specify rate limit in config // TODO: check msg max size -export async function Connection(this: WS.Server, socket: WebSocket, request: IncomingMessage) { +export async function Connection( + this: WS.Server, + socket: WebSocket, + request: IncomingMessage, +) { try { socket.on("close", onClose.bind(socket)); socket.on("message", onMessage.bind(socket)); @@ -29,7 +33,7 @@ export async function Connection(this: WS.Server, socket: WebSocket, request: In "open", "ping", "pong", - "unexpected-response" + "unexpected-response", ].forEach((x) => { socket.on(x, (y) => console.log("[WebRTC]", x, y)); }); @@ -39,7 +43,8 @@ export async function Connection(this: WS.Server, socket: WebSocket, request: In socket.encoding = "json"; socket.version = Number(searchParams.get("v")) || 5; - if (socket.version < 3) return socket.close(CLOSECODES.Unknown_error, "invalid version"); + if (socket.version < 3) + return socket.close(CLOSECODES.Unknown_error, "invalid version"); setHeartbeat(socket); @@ -50,11 +55,11 @@ export async function Connection(this: WS.Server, socket: WebSocket, request: In await Send(socket, { op: VoiceOPCodes.HELLO, d: { - heartbeat_interval: 1000 * 30 - } + heartbeat_interval: 1000 * 30, + }, }); } catch (error) { console.error("[WebRTC]", error); return socket.close(CLOSECODES.Unknown_error); } -} \ No newline at end of file +} diff --git a/src/webrtc/events/Message.ts b/src/webrtc/events/Message.ts index 8f75a815..38676f6c 100644 --- a/src/webrtc/events/Message.ts +++ b/src/webrtc/events/Message.ts @@ -7,13 +7,14 @@ const PayloadSchema = { op: Number, $d: new Tuple(Object, Number), // or number for heartbeat sequence $s: Number, - $t: String + $t: String, }; export async function onMessage(this: WebSocket, buffer: Buffer) { try { var data: Payload = JSON.parse(buffer.toString()); - if (data.op !== VoiceOPCodes.IDENTIFY && !this.user_id) return this.close(CLOSECODES.Not_authenticated); + if (data.op !== VoiceOPCodes.IDENTIFY && !this.user_id) + return this.close(CLOSECODES.Not_authenticated); // @ts-ignore const OPCodeHandler = OPCodeHandlers[data.op]; @@ -25,7 +26,11 @@ export async function onMessage(this: WebSocket, buffer: Buffer) { return; } - if (![VoiceOPCodes.HEARTBEAT, VoiceOPCodes.SPEAKING].includes(data.op as VoiceOPCodes)) { + if ( + ![VoiceOPCodes.HEARTBEAT, VoiceOPCodes.SPEAKING].includes( + data.op as VoiceOPCodes, + ) + ) { // @ts-ignore console.log("[WebRTC] Opcode " + VoiceOPCodes[data.op]); } @@ -35,4 +40,4 @@ export async function onMessage(this: WebSocket, buffer: Buffer) { console.error("[WebRTC] error", error); // if (!this.CLOSED && this.CLOSING) return this.close(CloseCodes.Unknown_error); } -} \ No newline at end of file +} diff --git a/src/webrtc/index.ts b/src/webrtc/index.ts index 7cecc9b6..ccb088ac 100644 --- a/src/webrtc/index.ts +++ b/src/webrtc/index.ts @@ -1,2 +1,2 @@ export * from "./Server"; -export * from "./util/index"; \ No newline at end of file +export * from "./util/index"; diff --git a/src/webrtc/opcodes/BackendVersion.ts b/src/webrtc/opcodes/BackendVersion.ts index b4b61c7d..375dd0cc 100644 --- a/src/webrtc/opcodes/BackendVersion.ts +++ b/src/webrtc/opcodes/BackendVersion.ts @@ -2,5 +2,8 @@ import { Payload, Send, WebSocket } from "@fosscord/gateway"; import { VoiceOPCodes } from "../util"; export async function onBackendVersion(this: WebSocket, data: Payload) { - await Send(this, { op: VoiceOPCodes.VOICE_BACKEND_VERSION, d: { voice: "0.8.43", rtc_worker: "0.3.26" } }); -} \ No newline at end of file + await Send(this, { + op: VoiceOPCodes.VOICE_BACKEND_VERSION, + d: { voice: "0.8.43", rtc_worker: "0.3.26" }, + }); +} diff --git a/src/webrtc/opcodes/Heartbeat.ts b/src/webrtc/opcodes/Heartbeat.ts index 1b6c5bcd..932cd458 100644 --- a/src/webrtc/opcodes/Heartbeat.ts +++ b/src/webrtc/opcodes/Heartbeat.ts @@ -1,4 +1,10 @@ -import { CLOSECODES, Payload, Send, setHeartbeat, WebSocket } from "@fosscord/gateway"; +import { + CLOSECODES, + Payload, + Send, + setHeartbeat, + WebSocket, +} from "@fosscord/gateway"; import { VoiceOPCodes } from "../util"; export async function onHeartbeat(this: WebSocket, data: Payload) { @@ -6,4 +12,4 @@ export async function onHeartbeat(this: WebSocket, data: Payload) { if (isNaN(data.d)) return this.close(CLOSECODES.Decode_error); await Send(this, { op: VoiceOPCodes.HEARTBEAT_ACK, d: data.d }); -} \ No newline at end of file +} diff --git a/src/webrtc/opcodes/Identify.ts b/src/webrtc/opcodes/Identify.ts index 19a575ab..45ad6c0a 100644 --- a/src/webrtc/opcodes/Identify.ts +++ b/src/webrtc/opcodes/Identify.ts @@ -1,33 +1,46 @@ import { CLOSECODES, Payload, Send, WebSocket } from "@fosscord/gateway"; -import { validateSchema, VoiceIdentifySchema, VoiceState } from "@fosscord/util"; +import { + validateSchema, + VoiceIdentifySchema, + VoiceState, +} from "@fosscord/util"; import { endpoint, getClients, VoiceOPCodes, PublicIP } from "@fosscord/webrtc"; import SemanticSDP from "semantic-sdp"; const defaultSDP = require("./sdp.json"); export async function onIdentify(this: WebSocket, data: Payload) { clearTimeout(this.readyTimeout); - const { server_id, user_id, session_id, token, streams, video } = validateSchema("VoiceIdentifySchema", data.d) as VoiceIdentifySchema; + const { server_id, user_id, session_id, token, streams, video } = + validateSchema("VoiceIdentifySchema", data.d) as VoiceIdentifySchema; - const voiceState = await VoiceState.findOne({ where: { guild_id: server_id, user_id, token, session_id } }); + const voiceState = await VoiceState.findOne({ + where: { guild_id: server_id, user_id, token, session_id }, + }); if (!voiceState) return this.close(CLOSECODES.Authentication_failed); this.user_id = user_id; this.session_id = session_id; const sdp = SemanticSDP.SDPInfo.expand(defaultSDP); - sdp.setDTLS(SemanticSDP.DTLSInfo.expand({ setup: "actpass", hash: "sha-256", fingerprint: endpoint.getDTLSFingerprint() })); + sdp.setDTLS( + SemanticSDP.DTLSInfo.expand({ + setup: "actpass", + hash: "sha-256", + fingerprint: endpoint.getDTLSFingerprint(), + }), + ); this.client = { websocket: this, out: { - tracks: new Map() + tracks: new Map(), }, in: { audio_ssrc: 0, video_ssrc: 0, - rtx_ssrc: 0 + rtx_ssrc: 0, }, sdp, - channel_id: voiceState.channel_id + channel_id: voiceState.channel_id, }; const clients = getClients(voiceState.channel_id)!; @@ -51,10 +64,10 @@ export async function onIdentify(this: WebSocket, data: Payload) { "xsalsa20_poly1305_lite_rtpsize", "xsalsa20_poly1305_lite", "xsalsa20_poly1305_suffix", - "xsalsa20_poly1305" + "xsalsa20_poly1305", ], ip: PublicIP, - experiments: [] - } + experiments: [], + }, }); -} \ No newline at end of file +} diff --git a/src/webrtc/opcodes/SelectProtocol.ts b/src/webrtc/opcodes/SelectProtocol.ts index a3579b34..eadba283 100644 --- a/src/webrtc/opcodes/SelectProtocol.ts +++ b/src/webrtc/opcodes/SelectProtocol.ts @@ -6,7 +6,10 @@ import SemanticSDP, { MediaInfo, SDPInfo } from "semantic-sdp"; export async function onSelectProtocol(this: WebSocket, payload: Payload) { if (!this.client) return; - const data = validateSchema("SelectProtocolSchema", payload.d) as SelectProtocolSchema; + const data = validateSchema( + "SelectProtocolSchema", + payload.d, + ) as SelectProtocolSchema; const offer = SemanticSDP.SDPInfo.parse("m=audio\n" + data.sdp!); this.client.sdp!.setICE(offer.getICE()); @@ -25,14 +28,14 @@ export async function onSelectProtocol(this: WebSocket, payload: Payload) { const candidate = candidates[0]; const answer = - `m=audio ${port} ICE/SDP` - + `a=fingerprint:${fingerprint}` - + `c=IN IP4 ${PublicIP}` - + `a=rtcp:${port}` - + `a=ice-ufrag:${ice.getUfrag()}` - + `a=ice-pwd:${ice.getPwd()}` - + `a=fingerprint:${fingerprint}` - + `a=candidate:1 1 ${candidate.getTransport()} ${candidate.getFoundation()} ${candidate.getAddress()} ${candidate.getPort()} typ host`; + `m=audio ${port} ICE/SDP` + + `a=fingerprint:${fingerprint}` + + `c=IN IP4 ${PublicIP}` + + `a=rtcp:${port}` + + `a=ice-ufrag:${ice.getUfrag()}` + + `a=ice-pwd:${ice.getPwd()}` + + `a=fingerprint:${fingerprint}` + + `a=candidate:1 1 ${candidate.getTransport()} ${candidate.getFoundation()} ${candidate.getAddress()} ${candidate.getPort()} typ host`; await Send(this, { op: VoiceOPCodes.SELECT_PROTOCOL_ACK, @@ -40,7 +43,7 @@ export async function onSelectProtocol(this: WebSocket, payload: Payload) { video_codec: "H264", sdp: answer, media_session_id: this.session_id, - audio_codec: "opus" - } + audio_codec: "opus", + }, }); -} \ No newline at end of file +} diff --git a/src/webrtc/opcodes/Speaking.ts b/src/webrtc/opcodes/Speaking.ts index e2227040..8488acf8 100644 --- a/src/webrtc/opcodes/Speaking.ts +++ b/src/webrtc/opcodes/Speaking.ts @@ -15,8 +15,8 @@ export async function onSpeaking(this: WebSocket, data: Payload) { d: { user_id: client.websocket.user_id, speaking: data.d.speaking, - ssrc: ssrc?.audio_ssrc || 0 - } + ssrc: ssrc?.audio_ssrc || 0, + }, }); }); -} \ No newline at end of file +} diff --git a/src/webrtc/opcodes/Video.ts b/src/webrtc/opcodes/Video.ts index ff20d5a9..dcbc9aa0 100644 --- a/src/webrtc/opcodes/Video.ts +++ b/src/webrtc/opcodes/Video.ts @@ -21,8 +21,8 @@ export async function onVideo(this: WebSocket, payload: Payload) { SemanticSDP.StreamInfo.expand({ id, // @ts-ignore - tracks: [] - }) + tracks: [], + }), ); this.client.in.stream = stream; @@ -46,8 +46,8 @@ export async function onVideo(this: WebSocket, payload: Payload) { SemanticSDP.StreamInfo.expand({ id: "out" + this.user_id, // @ts-ignore - tracks: [] - }) + tracks: [], + }), ); this.client.out.stream = out; @@ -64,20 +64,35 @@ export async function onVideo(this: WebSocket, payload: Payload) { } if (d.audio_ssrc) { - handleSSRC.call(this, "audio", { media: d.audio_ssrc, rtx: d.audio_ssrc + 1 }); + handleSSRC.call(this, "audio", { + media: d.audio_ssrc, + rtx: d.audio_ssrc + 1, + }); } if (d.video_ssrc && d.rtx_ssrc) { - handleSSRC.call(this, "video", { media: d.video_ssrc, rtx: d.rtx_ssrc }); + handleSSRC.call(this, "video", { + media: d.video_ssrc, + rtx: d.rtx_ssrc, + }); } } -function attachTrack(this: WebSocket, track: IncomingStreamTrack, user_id: string) { +function attachTrack( + this: WebSocket, + track: IncomingStreamTrack, + user_id: string, +) { if (!this.client) return; - const outTrack = this.client.transport!.createOutgoingStreamTrack(track.getMedia()); + const outTrack = this.client.transport!.createOutgoingStreamTrack( + track.getMedia(), + ); outTrack.attachTo(track); this.client.out.stream!.addTrack(outTrack); var ssrcs = this.client.out.tracks.get(user_id)!; - if (!ssrcs) ssrcs = this.client.out.tracks.set(user_id, { audio_ssrc: 0, rtx_ssrc: 0, video_ssrc: 0 }).get(user_id)!; + if (!ssrcs) + ssrcs = this.client.out.tracks + .set(user_id, { audio_ssrc: 0, rtx_ssrc: 0, video_ssrc: 0 }) + .get(user_id)!; if (track.getMedia() === "audio") { ssrcs.audio_ssrc = outTrack.getSSRCs().media!; @@ -90,8 +105,8 @@ function attachTrack(this: WebSocket, track: IncomingStreamTrack, user_id: strin op: VoiceOPCodes.VIDEO, d: { user_id: user_id, - ...ssrcs - } as VoiceVideoSchema + ...ssrcs, + } as VoiceVideoSchema, }); } @@ -115,4 +130,4 @@ function handleSSRC(this: WebSocket, type: "audio" | "video", ssrcs: SSRCs) { attachTrack.call(this, track, client.websocket.user_id); }); } -} \ No newline at end of file +} diff --git a/src/webrtc/opcodes/index.ts b/src/webrtc/opcodes/index.ts index 8c664cce..86e39687 100644 --- a/src/webrtc/opcodes/index.ts +++ b/src/webrtc/opcodes/index.ts @@ -15,5 +15,5 @@ export default { [VoiceOPCodes.VOICE_BACKEND_VERSION]: onBackendVersion, [VoiceOPCodes.VIDEO]: onVideo, [VoiceOPCodes.SPEAKING]: onSpeaking, - [VoiceOPCodes.SELECT_PROTOCOL]: onSelectProtocol -}; \ No newline at end of file + [VoiceOPCodes.SELECT_PROTOCOL]: onSelectProtocol, +}; diff --git a/src/webrtc/opcodes/sdp.json b/src/webrtc/opcodes/sdp.json index 4867b9c7..5f7eba38 100644 --- a/src/webrtc/opcodes/sdp.json +++ b/src/webrtc/opcodes/sdp.json @@ -417,4 +417,4 @@ } ], "candidates": [] -} \ No newline at end of file +} diff --git a/src/webrtc/start.ts b/src/webrtc/start.ts index 9a5f38ee..57361909 100644 --- a/src/webrtc/start.ts +++ b/src/webrtc/start.ts @@ -8,6 +8,6 @@ config(); const port = Number(process.env.PORT) || 3004; const server = new Server({ - port + port, }); -server.start(); \ No newline at end of file +server.start(); diff --git a/src/webrtc/util/Constants.ts b/src/webrtc/util/Constants.ts index 64d78e22..d9f1ff60 100644 --- a/src/webrtc/util/Constants.ts +++ b/src/webrtc/util/Constants.ts @@ -3,7 +3,7 @@ export enum VoiceStatus { CONNECTING = 1, AUTHENTICATING = 2, RECONNECTING = 3, - DISCONNECTED = 4 + DISCONNECTED = 4, } export enum VoiceOPCodes { @@ -22,5 +22,5 @@ export enum VoiceOPCodes { SESSION_UPDATE = 14, MEDIA_SINK_WANTS = 15, VOICE_BACKEND_VERSION = 16, - CHANNEL_OPTIONS_UPDATE = 17 -} \ No newline at end of file + CHANNEL_OPTIONS_UPDATE = 17, +} diff --git a/src/webrtc/util/MediaServer.ts b/src/webrtc/util/MediaServer.ts index 93230c91..520b8682 100644 --- a/src/webrtc/util/MediaServer.ts +++ b/src/webrtc/util/MediaServer.ts @@ -1,5 +1,9 @@ import { WebSocket } from "@fosscord/gateway"; -import MediaServer, { IncomingStream, OutgoingStream, Transport } from "medooze-media-server"; +import MediaServer, { + IncomingStream, + OutgoingStream, + Transport, +} from "medooze-media-server"; import SemanticSDP from "semantic-sdp"; MediaServer.enableLog(true); @@ -13,7 +17,11 @@ try { MediaServer.setPortRange(min, max); } catch (error) { - console.error("Invalid env var: WEBRTC_PORT_RANGE", process.env.WEBRTC_PORT_RANGE, error); + console.error( + "Invalid env var: WEBRTC_PORT_RANGE", + process.env.WEBRTC_PORT_RANGE, + error, + ); process.exit(1); } @@ -48,4 +56,4 @@ export interface Client { export function getClients(channel_id: string) { if (!channels.has(channel_id)) channels.set(channel_id, new Set()); return channels.get(channel_id)!; -} \ No newline at end of file +} diff --git a/src/webrtc/util/index.ts b/src/webrtc/util/index.ts index 2e09bc48..f0d49049 100644 --- a/src/webrtc/util/index.ts +++ b/src/webrtc/util/index.ts @@ -1,2 +1,2 @@ export * from "./Constants"; -export * from "./MediaServer"; \ No newline at end of file +export * from "./MediaServer"; |