diff options
Diffstat (limited to 'src/api/routes')
87 files changed, 3647 insertions, 2162 deletions
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; |