diff options
author | Flam3rboy <34555296+Flam3rboy@users.noreply.github.com> | 2021-09-12 23:32:55 +0200 |
---|---|---|
committer | Flam3rboy <34555296+Flam3rboy@users.noreply.github.com> | 2021-09-12 23:32:55 +0200 |
commit | f93bd1fd67f38cd9dc9250f328bb1b8e1214e37c (patch) | |
tree | 9e8b85cbad3dbf65e4e4a1162f68163c72b3aea2 /api/src | |
parent | Merge pull request #353 from AlTech98/dummy-routes (diff) | |
parent | :sparkles: #307 done (diff) | |
download | server-f93bd1fd67f38cd9dc9250f328bb1b8e1214e37c.tar.xz |
Merge branch 'typescript-interface-body-parser+autogenerate-unit-tests+documentation'
Diffstat (limited to 'api/src')
76 files changed, 1006 insertions, 1345 deletions
diff --git a/api/src/index.ts b/api/src/index.ts index fe59310f..adc7649c 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -1,12 +1,3 @@ export * from "./Server"; export * from "./middlewares/"; -export * from "./schema/Ban"; -export * from "./schema/Channel"; -export * from "./schema/Guild"; -export * from "./schema/Invite"; -export * from "./schema/Message"; -export * from "./util/instanceOf"; -export * from "./util/instanceOf"; -export * from "./util/RandomInviteID"; -export * from "./util/String"; -export { check as checkPassword } from "./util/passwordStrength"; +export * from "./util/"; diff --git a/api/src/middlewares/ErrorHandler.ts b/api/src/middlewares/ErrorHandler.ts index be2586cf..d288f3fb 100644 --- a/api/src/middlewares/ErrorHandler.ts +++ b/api/src/middlewares/ErrorHandler.ts @@ -1,7 +1,7 @@ import { NextFunction, Request, Response } from "express"; import { HTTPError } from "lambert-server"; import { EntityNotFoundError } from "typeorm"; -import { FieldError } from "../util/instanceOf"; +import { FieldError } from "@fosscord/api"; import { ApiError } from "@fosscord/util"; export function ErrorHandler(error: Error, req: Request, res: Response, next: NextFunction) { diff --git a/api/src/middlewares/RateLimit.ts b/api/src/middlewares/RateLimit.ts index dffbc0d9..d1fd072f 100644 --- a/api/src/middlewares/RateLimit.ts +++ b/api/src/middlewares/RateLimit.ts @@ -1,6 +1,6 @@ import { Config, listenEvent } from "@fosscord/util"; import { NextFunction, Request, Response, Router } from "express"; -import { getIpAdress } from "../util/ipAddress"; +import { getIpAdress } from "@fosscord/api"; import { API_PREFIX_TRAILING_SLASH } from "./Authentication"; // Docs: https://discord.com/developers/docs/topics/rate-limits diff --git a/api/src/routes/auth/login.ts b/api/src/routes/auth/login.ts index 7fd0f870..f672658a 100644 --- a/api/src/routes/auth/login.ts +++ b/api/src/routes/auth/login.ts @@ -1,5 +1,5 @@ import { Request, Response, Router } from "express"; -import { check, FieldErrors, Length } from "../../util/instanceOf"; +import { FieldErrors, route } from "@fosscord/api"; import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; import { Config, User } from "@fosscord/util"; @@ -8,67 +8,65 @@ import { adjustEmail } from "./register"; const router: Router = Router(); export default router; -router.post( - "/", - check({ - login: new Length(String, 2, 100), // email or telephone - password: new Length(String, 8, 72), - $undelete: Boolean, - $captcha_key: String, - $login_source: String, - $gift_code_sku_id: String - }), - async (req: Request, res: Response) => { - const { login, password, captcha_key, undelete } = req.body; - const email = adjustEmail(login); - console.log("login", email); - - const config = Config.get(); - - if (config.login.requireCaptcha && config.security.captcha.enabled) { - if (!captcha_key) { - const { sitekey, service } = config.security.captcha; - return res.status(400).json({ - captcha_key: ["captcha-required"], - captcha_sitekey: sitekey, - captcha_service: service - }); - } - - // TODO: check captcha - } +export interface LoginSchema { + login: string; + password: string; + undelete?: boolean; + captcha_key?: string; + login_source?: string; + gift_code_sku_id?: string; +} - const user = await User.findOneOrFail({ - where: [{ phone: login }, { email: login }], - select: ["data", "id", "disabled", "deleted", "settings"] - }).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 }); +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) { + if (!captcha_key) { + const { sitekey, service } = config.security.captcha; + 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" } }); - } + // TODO: check captcha + } - const token = await generateToken(user.id); + const user = await User.findOneOrFail({ + where: [{ phone: login }, { email: login }], + select: ["data", "id", "disabled", "deleted", "settings"] + }).catch((e) => { + throw FieldErrors({ login: { message: req.t("auth:login.INVALID_LOGIN"), code: "INVALID_LOGIN" } }); + }); - // 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 + 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 }); + } - res.json({ token, settings: user.settings }); + // 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 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 }); +}); export async function generateToken(id: string) { const iat = Math.floor(Date.now() / 1000); diff --git a/api/src/routes/auth/register.ts b/api/src/routes/auth/register.ts index a9518e91..e0af1d6d 100644 --- a/api/src/routes/auth/register.ts +++ b/api/src/routes/auth/register.ts @@ -1,217 +1,224 @@ import { Request, Response, Router } from "express"; import { trimSpecial, User, Snowflake, Config, defaultSettings } from "@fosscord/util"; import bcrypt from "bcrypt"; -import { check, Email, EMAIL_REGEX, FieldErrors, Length } from "../../util/instanceOf"; +import { EMAIL_REGEX, FieldErrors, route } from "@fosscord/api"; import "missing-native-js-functions"; import { generateToken } from "./login"; -import { getIpAdress, IPAnalysis, isProxy } from "../../util/ipAddress"; +import { getIpAdress, IPAnalysis, isProxy } from "@fosscord/api"; import { HTTPError } from "lambert-server"; import { In } from "typeorm"; const router: Router = Router(); -router.post( - "/", - check({ - username: new Length(String, 2, 32), - // TODO: check min password length in config - // prevent Denial of Service with max length of 72 chars - password: new Length(String, 8, 72), - consent: Boolean, - $email: new Length(Email, 5, 100), - $fingerprint: String, - $invite: String, - $date_of_birth: Date, // "2000-04-03" - $gift_code_sku_id: String, - $captcha_key: String - }), - async (req: Request, res: Response) => { - const { - email, - username, - password, - consent, - fingerprint, - invite, - date_of_birth, - gift_code_sku_id, // ? what is this - captcha_key - } = req.body; - - // get register Config - const { register, security } = Config.get(); - const ip = getIpAdress(req); - - if (register.blockProxies) { - if (isProxy(await IPAnalysis(ip))) { - console.log(`proxy ${ip} blocked from registration`); - throw new HTTPError("Your IP is blocked from registration"); - } +export interface RegisterSchema { + /** + * @minLength 2 + * @maxLength 32 + */ + username: string; + /** + * @minLength 1 + * @maxLength 72 + */ + password: string; // TODO: use password strength of config + consent: boolean; + /** + * @TJS-format email + */ + email?: string; + fingerprint?: string; + invite?: string; + date_of_birth?: Date; // "2000-04-03" + gift_code_sku_id?: string; + captcha_key?: string; +} + +router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Response) => { + const { + email, + username, + password, + consent, + fingerprint, + invite, + date_of_birth, + gift_code_sku_id, // ? what is this + captcha_key + } = req.body; + + // get register Config + const { register, security } = Config.get(); + const ip = getIpAdress(req); + + if (register.blockProxies) { + if (isProxy(await IPAnalysis(ip))) { + console.log(`proxy ${ip} blocked from registration`); + throw new HTTPError("Your IP is blocked from registration"); } + } - console.log("register", req.body.email, req.body.username, ip); - // TODO: automatically join invite - // TODO: gift_code_sku_id? - // TODO: check password strength + console.log("register", req.body.email, req.body.username, ip); + // TODO: automatically join invite + // TODO: gift_code_sku_id? + // TODO: check password strength - // adjusted_email will be slightly modified version of the user supplied email -> e.g. protection against GMail Trick - let adjusted_email = adjustEmail(email); + // adjusted_email will be slightly modified version of the user supplied email -> e.g. protection against GMail Trick + let adjusted_email = adjustEmail(email); - // adjusted_password will be the hash of the password - let adjusted_password = ""; + // adjusted_password will be the hash of the password + let adjusted_password = ""; - // trim special uf8 control characters -> Backspace, Newline, ... - let adjusted_username = trimSpecial(username); + // trim special uf8 control characters -> Backspace, Newline, ... + let adjusted_username = trimSpecial(username); - // discriminator will be randomly generated - let discriminator = ""; + // discriminator will be randomly generated + let discriminator = ""; - // check if registration is allowed - if (!register.allowNewRegistration) { - throw FieldErrors({ - email: { code: "REGISTRATION_DISABLED", message: req.t("auth:register.REGISTRATION_DISABLED") } - }); - } + // 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 (!consent) { - throw FieldErrors({ - consent: { code: "CONSENT_REQUIRED", message: req.t("auth:register.CONSENT_REQUIRED") } - }); - } + // check if the user agreed to the Terms of Service + if (!consent) { + throw FieldErrors({ + consent: { code: "CONSENT_REQUIRED", message: req.t("auth:register.CONSENT_REQUIRED") } + }); + } - // require invite to register -> e.g. for organizations to send invites to their employees - if (register.requireInvite && !invite) { - throw FieldErrors({ - email: { code: "INVITE_ONLY", message: req.t("auth:register.INVITE_ONLY") } - }); - } + // require invite to register -> e.g. for organizations to send invites to their employees + if (register.requireInvite && !invite) { + throw FieldErrors({ + email: { code: "INVITE_ONLY", message: req.t("auth:register.INVITE_ONLY") } + }); + } - if (email) { - // replace all dots and chars after +, if its a gmail.com email - if (!adjusted_email) throw FieldErrors({ email: { code: "INVALID_EMAIL", message: req.t("auth:register.INVALID_EMAIL") } }); + if (email) { + // replace all dots and chars after +, if its a gmail.com email + if (!adjusted_email) throw FieldErrors({ email: { code: "INVALID_EMAIL", message: req.t("auth:register.INVALID_EMAIL") } }); - // check if there is already an account with this email - const exists = await User.findOneOrFail({ email: adjusted_email }).catch((e) => {}); + // check if there is already an account with this email + const exists = await User.findOneOrFail({ email: adjusted_email }).catch((e) => {}); - if (exists) { - throw FieldErrors({ - email: { - code: "EMAIL_ALREADY_REGISTERED", - message: req.t("auth:register.EMAIL_ALREADY_REGISTERED") - } - }); - } - } else if (register.email.necessary) { + if (exists) { throw FieldErrors({ - email: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") } + email: { + code: "EMAIL_ALREADY_REGISTERED", + message: req.t("auth:register.EMAIL_ALREADY_REGISTERED") + } }); } + } else if (register.email.necessary) { + throw FieldErrors({ + email: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") } + }); + } + + if (register.dateOfBirth.necessary && !date_of_birth) { + throw FieldErrors({ + date_of_birth: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") } + }); + } else if (register.dateOfBirth.minimum) { + const minimum = new Date(); + minimum.setFullYear(minimum.getFullYear() - register.dateOfBirth.minimum); - if (register.dateOfBirth.necessary && !date_of_birth) { + // higher is younger + if (date_of_birth > minimum) { throw FieldErrors({ - date_of_birth: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") } + date_of_birth: { + code: "DATE_OF_BIRTH_UNDERAGE", + message: req.t("auth:register.DATE_OF_BIRTH_UNDERAGE", { years: register.dateOfBirth.minimum }) + } }); - } else if (register.dateOfBirth.minimum) { - const minimum = new Date(); - minimum.setFullYear(minimum.getFullYear() - register.dateOfBirth.minimum); - - // higher is younger - if (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 (!register.allowMultipleAccounts) { - // TODO: check if fingerprint was eligible generated - const exists = await User.findOne({ where: { fingerprints: In(fingerprint) } }); - - if (exists) { - throw FieldErrors({ - email: { - code: "EMAIL_ALREADY_REGISTERED", - message: req.t("auth:register.EMAIL_ALREADY_REGISTERED") - } - }); - } - } - - if (register.requireCaptcha && security.captcha.enabled) { - if (!captcha_key) { - const { sitekey, service } = security.captcha; - return res.status(400).json({ - captcha_key: ["captcha-required"], - captcha_sitekey: sitekey, - captcha_service: service - }); - } - - // TODO: check captcha } + } - // the salt is saved in the password refer to bcrypt docs - adjusted_password = await bcrypt.hash(password, 12); - - let exists; - // randomly generates a discriminator between 1 and 9999 and checks max five times if it already exists - // if it all five times already exists, abort with USERNAME_TOO_MANY_USERS error - // else just continue - // TODO: is there any better way to generate a random discriminator only once, without checking if it already exists in the mongodb database? - for (let tries = 0; tries < 5; tries++) { - discriminator = Math.randomIntBetween(1, 9999).toString().padStart(4, "0"); - exists = await User.findOne({ where: { discriminator, username: adjusted_username }, select: ["id"] }); - if (!exists) break; - } + if (!register.allowMultipleAccounts) { + // TODO: check if fingerprint was eligible generated + const exists = await User.findOne({ where: { fingerprints: In(fingerprint) } }); if (exists) { throw FieldErrors({ - username: { - code: "USERNAME_TOO_MANY_USERS", - message: req.t("auth:register.USERNAME_TOO_MANY_USERS") + email: { + code: "EMAIL_ALREADY_REGISTERED", + message: req.t("auth:register.EMAIL_ALREADY_REGISTERED") } }); } + } + + if (register.requireCaptcha && security.captcha.enabled) { + if (!captcha_key) { + const { sitekey, service } = security.captcha; + return res.status(400).json({ + captcha_key: ["captcha-required"], + captcha_sitekey: sitekey, + captcha_service: service + }); + } + + // TODO: check captcha + } - // TODO: save date_of_birth - // appearently discord doesn't save the date of birth and just calculate if nsfw is allowed - // if nsfw_allowed is null/undefined it'll require date_of_birth to set it to true/false - - const user = await new User({ - created_at: new Date(), - username: adjusted_username, - discriminator, - id: Snowflake.generate(), - bot: false, - system: false, - desktop: false, - mobile: false, - premium: true, - premium_type: 2, - bio: "", - mfa_enabled: false, - verified: false, - disabled: false, - deleted: false, - email: adjusted_email, - nsfw_allowed: true, // TODO: depending on age - public_flags: "0", - flags: "0", // TODO: generate - data: { - hash: adjusted_password, - valid_tokens_since: new Date() - }, - settings: { ...defaultSettings, locale: req.language || "en-US" }, - fingerprints: [] - }).save(); - - return res.json({ token: await generateToken(user.id) }); + // the salt is saved in the password refer to bcrypt docs + adjusted_password = await bcrypt.hash(password, 12); + + let exists; + // randomly generates a discriminator between 1 and 9999 and checks max five times if it already exists + // if it all five times already exists, abort with USERNAME_TOO_MANY_USERS error + // else just continue + // TODO: is there any better way to generate a random discriminator only once, without checking if it already exists in the mongodb database? + for (let tries = 0; tries < 5; tries++) { + discriminator = Math.randomIntBetween(1, 9999).toString().padStart(4, "0"); + exists = await User.findOne({ where: { discriminator, username: adjusted_username }, select: ["id"] }); + if (!exists) break; } -); + + if (exists) { + throw FieldErrors({ + username: { + code: "USERNAME_TOO_MANY_USERS", + message: req.t("auth:register.USERNAME_TOO_MANY_USERS") + } + }); + } + + // TODO: save date_of_birth + // appearently discord doesn't save the date of birth and just calculate if nsfw is allowed + // if nsfw_allowed is null/undefined it'll require date_of_birth to set it to true/false + + const user = await new User({ + created_at: new Date(), + username: adjusted_username, + discriminator, + id: Snowflake.generate(), + bot: false, + system: false, + desktop: false, + mobile: false, + premium: true, + premium_type: 2, + bio: "", + mfa_enabled: false, + verified: false, + disabled: false, + deleted: false, + email: adjusted_email, + nsfw_allowed: true, // TODO: depending on age + public_flags: "0", + flags: "0", // TODO: generate + data: { + hash: adjusted_password, + valid_tokens_since: new Date() + }, + settings: { ...defaultSettings, locale: req.language || "en-US" }, + fingerprints: [] + }).save(); + + return res.json({ token: await generateToken(user.id) }); +}); export function adjustEmail(email: string): string | undefined { // body parser already checked if it is a valid email diff --git a/api/src/routes/channels/#channel_id/index.ts b/api/src/routes/channels/#channel_id/index.ts index 4aa5a5b9..02ac9884 100644 --- a/api/src/routes/channels/#channel_id/index.ts +++ b/api/src/routes/channels/#channel_id/index.ts @@ -1,48 +1,59 @@ -import { ChannelDeleteEvent, Channel, ChannelUpdateEvent, emitEvent, getPermission } from "@fosscord/util"; +import { ChannelDeleteEvent, Channel, ChannelUpdateEvent, emitEvent, ChannelType, ChannelPermissionOverwriteType } from "@fosscord/util"; import { Router, Response, Request } from "express"; -import { HTTPError } from "lambert-server"; -import { ChannelModifySchema } from "../../../schema/Channel"; -import { check } from "../../../util/instanceOf"; +import { route } from "@fosscord/api"; const router: Router = Router(); // TODO: delete channel // TODO: Get channel -router.get("/", async (req: Request, res: Response) => { +router.get("/", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: Response) => { const { channel_id } = req.params; const channel = await Channel.findOneOrFail({ id: channel_id }); - const permission = await getPermission(req.user_id, channel.guild_id, channel_id); - permission.hasThrow("VIEW_CHANNEL"); - return res.send(channel); }); -router.delete("/", async (req: Request, res: Response) => { +router.delete("/", route({ permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => { const { channel_id } = req.params; const channel = await Channel.findOneOrFail({ id: channel_id }); - const permission = await getPermission(req.user_id, channel?.guild_id, channel_id); - permission.hasThrow("MANAGE_CHANNELS"); - // TODO: Dm channel "close" not delete const data = channel; - await emitEvent({ event: "CHANNEL_DELETE", data, channel_id } as ChannelDeleteEvent); - - await Channel.delete({ id: channel_id }); + await Promise.all([emitEvent({ event: "CHANNEL_DELETE", data, channel_id } as ChannelDeleteEvent), Channel.delete({ id: channel_id })]); res.send(data); }); -router.patch("/", check(ChannelModifySchema), async (req: Request, res: Response) => { +export interface ChannelModifySchema { + /** + * @maxLength 100 + */ + name: string; + type: ChannelType; + topic?: string; + bitrate?: number; + user_limit?: number; + rate_limit_per_user?: number; + position?: number; + permission_overwrites?: { + id: string; + type: ChannelPermissionOverwriteType; + allow: bigint; + deny: bigint; + }[]; + parent_id?: string; + id?: string; // is not used (only for guild create) + nsfw?: boolean; + rtc_region?: string; + default_auto_archive_duration?: number; +} + +router.patch("/", route({ body: "ChannelModifySchema", permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => { var payload = req.body as ChannelModifySchema; const { channel_id } = req.params; - const permission = await getPermission(req.user_id, undefined, channel_id); - permission.hasThrow("MANAGE_CHANNELS"); - const channel = await Channel.findOneOrFail({ id: channel_id }); channel.assign(payload); diff --git a/api/src/routes/channels/#channel_id/invites.ts b/api/src/routes/channels/#channel_id/invites.ts index fe22d3bc..39263185 100644 --- a/api/src/routes/channels/#channel_id/invites.ts +++ b/api/src/routes/channels/#channel_id/invites.ts @@ -1,14 +1,25 @@ import { Router, Request, Response } from "express"; import { HTTPError } from "lambert-server"; -import { check } from "../../../util/instanceOf"; -import { random } from "../../../util/RandomInviteID"; -import { InviteCreateSchema } from "../../../schema/Invite"; +import { route } from "@fosscord/api"; +import { random } from "@fosscord/api"; import { getPermission, Channel, Invite, InviteCreateEvent, emitEvent, User, Guild, PublicInviteRelation } from "@fosscord/util"; import { isTextChannel } from "./messages"; const router: Router = Router(); -router.post("/", check(InviteCreateSchema), async (req: Request, res: Response) => { +export interface InviteCreateSchema { + target_user_id?: string; + target_type?: string; + validate?: string; //? wtf is this + max_age?: number; + max_uses?: number; + temporary?: boolean; + unique?: boolean; + target_user?: string; + target_user_type?: number; +} + +router.post("/", route({ body: "InviteCreateSchema", permission: "CREATE_INSTANT_INVITE" }), 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"] }); @@ -19,23 +30,6 @@ router.post("/", check(InviteCreateSchema), async (req: Request, res: Response) } const { guild_id } = channel; - const permission = await getPermission(user_id, guild_id, undefined, { - guild_select: [ - "banner", - "description", - "features", - "icon", - "id", - "name", - "nsfw", - "nsfw_level", - "splash", - "vanity_url_code", - "verification_level" - ] as (keyof Guild)[] - }); - permission.hasThrow("CREATE_INSTANT_INVITE"); - const expires_at = new Date(req.body.max_age * 1000 + Date.now()); const invite = await new Invite({ @@ -52,14 +46,14 @@ router.post("/", check(InviteCreateSchema), async (req: Request, res: Response) }).save(); const data = invite.toJSON(); data.inviter = await User.getPublicUser(req.user_id); - data.guild = permission.cache.guild; + data.guild = await Guild.findOne({ id: guild_id }); data.channel = channel; await emitEvent({ event: "INVITE_CREATE", data, guild_id } as InviteCreateEvent); res.status(201).send(data); }); -router.get("/", async (req: Request, res: Response) => { +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({ id: channel_id }); @@ -68,8 +62,6 @@ router.get("/", async (req: Request, res: Response) => { throw new HTTPError("This channel doesn't exist", 404); } const { guild_id } = channel; - const permission = await getPermission(user_id, guild_id); - permission.hasThrow("MANAGE_CHANNELS"); const invites = await Invite.find({ where: { guild_id }, relations: PublicInviteRelation }); diff --git a/api/src/routes/channels/#channel_id/messages/#message_id/ack.ts b/api/src/routes/channels/#channel_id/messages/#message_id/ack.ts index 0fd5f2be..97d1d19e 100644 --- a/api/src/routes/channels/#channel_id/messages/#message_id/ack.ts +++ b/api/src/routes/channels/#channel_id/messages/#message_id/ack.ts @@ -1,14 +1,18 @@ import { emitEvent, getPermission, MessageAckEvent, ReadState } from "@fosscord/util"; import { Request, Response, Router } from "express"; - -import { check } from "../../../../../util/instanceOf"; +import { route } from "@fosscord/api"; const router = Router(); // TODO: check if message exists // TODO: send read state event to all channel members -router.post("/", check({ $manual: Boolean, $mention_count: Number }), async (req: Request, res: Response) => { +export interface MessageAcknowledgeSchema { + manual?: boolean; + mention_count?: number; +} + +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); diff --git a/api/src/routes/channels/#channel_id/messages/#message_id/index.ts b/api/src/routes/channels/#channel_id/messages/#message_id/index.ts index 7a00de43..d0f780db 100644 --- a/api/src/routes/channels/#channel_id/messages/#message_id/index.ts +++ b/api/src/routes/channels/#channel_id/messages/#message_id/index.ts @@ -1,12 +1,13 @@ import { Channel, emitEvent, getPermission, MessageDeleteEvent, Message, MessageUpdateEvent } from "@fosscord/util"; import { Router, Response, Request } from "express"; -import { MessageCreateSchema } from "../../../../../schema/Message"; -import { check } from "../../../../../util/instanceOf"; -import { handleMessage, postHandleMessage } from "../../../../../util/Message"; +import { route } from "@fosscord/api"; +import { handleMessage, postHandleMessage } from "@fosscord/api"; +import { MessageCreateSchema } from "../index"; const router = Router(); +// TODO: message content/embed string length limit -router.patch("/", check(MessageCreateSchema), async (req: Request, res: Response) => { +router.patch("/", route({ body: "MessageCreateSchema", permission: "SEND_MESSAGES" }), async (req: Request, res: Response) => { const { message_id, channel_id } = req.params; var body = req.body as MessageCreateSchema; @@ -47,14 +48,17 @@ router.patch("/", check(MessageCreateSchema), async (req: Request, res: Response // TODO: delete attachments in message -router.delete("/", async (req: Request, res: Response) => { +// permission check only if deletes messagr from other user +router.delete("/", route({}), async (req: Request, res: Response) => { const { message_id, channel_id } = req.params; const channel = await Channel.findOneOrFail({ id: channel_id }); const message = await Message.findOneOrFail({ id: message_id }); - const permission = await getPermission(req.user_id, channel.guild_id, channel_id); - if (message.author_id !== req.user_id) permission.hasThrow("MANAGE_MESSAGES"); + if (message.author_id !== req.user_id) { + const permission = await getPermission(req.user_id, channel.guild_id, channel_id); + permission.hasThrow("MANAGE_MESSAGES"); + } await Message.delete({ id: message_id }); diff --git a/api/src/routes/channels/#channel_id/messages/#message_id/reactions.ts b/api/src/routes/channels/#channel_id/messages/#message_id/reactions.ts index f60484b5..f2b83d40 100644 --- a/api/src/routes/channels/#channel_id/messages/#message_id/reactions.ts +++ b/api/src/routes/channels/#channel_id/messages/#message_id/reactions.ts @@ -13,6 +13,7 @@ import { PublicUserProjection, User } from "@fosscord/util"; +import { route } from "@fosscord/api"; import { Router, Response, Request } from "express"; import { HTTPError } from "lambert-server"; import { In } from "typeorm"; @@ -35,14 +36,11 @@ function getEmoji(emoji: string): PartialEmoji { }; } -router.delete("/", async (req: Request, res: Response) => { +router.delete("/", route({ permission: "MANAGE_MESSAGES" }), async (req: Request, res: Response) => { const { message_id, channel_id } = req.params; const channel = await Channel.findOneOrFail({ id: channel_id }); - const permissions = await getPermission(req.user_id, undefined, channel_id); - permissions.hasThrow("MANAGE_MESSAGES"); - await Message.update({ id: message_id, channel_id }, { reactions: [] }); await emitEvent({ @@ -58,13 +56,10 @@ router.delete("/", async (req: Request, res: Response) => { res.sendStatus(204); }); -router.delete("/:emoji", async (req: Request, res: Response) => { +router.delete("/:emoji", route({ permission: "MANAGE_MESSAGES" }), async (req: Request, res: Response) => { const { message_id, channel_id } = req.params; const emoji = getEmoji(req.params.emoji); - const permissions = await getPermission(req.user_id, undefined, channel_id); - permissions.hasThrow("MANAGE_MESSAGES"); - const message = await Message.findOneOrFail({ id: message_id, channel_id }); const already_added = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name); @@ -88,7 +83,7 @@ router.delete("/:emoji", async (req: Request, res: Response) => { res.sendStatus(204); }); -router.get("/:emoji", async (req: Request, res: Response) => { +router.get("/:emoji", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: Response) => { const { message_id, channel_id } = req.params; const emoji = getEmoji(req.params.emoji); @@ -96,9 +91,6 @@ router.get("/:emoji", async (req: Request, res: Response) => { 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 permissions = await getPermission(req.user_id, undefined, channel_id); - permissions.hasThrow("VIEW_CHANNEL"); - const users = await User.find({ where: { id: In(reaction.user_ids) @@ -109,7 +101,7 @@ router.get("/:emoji", async (req: Request, res: Response) => { res.json(users); }); -router.put("/:emoji/:user_id", async (req: Request, res: Response) => { +router.put("/:emoji/:user_id", route({ permission: "READ_MESSAGE_HISTORY" }), 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); @@ -118,13 +110,11 @@ router.put("/:emoji/:user_id", async (req: Request, res: Response) => { const message = await Message.findOneOrFail({ id: message_id, channel_id }); const already_added = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name); - const permissions = await getPermission(req.user_id, undefined, channel_id); - permissions.hasThrow("READ_MESSAGE_HISTORY"); - if (!already_added) permissions.hasThrow("ADD_REACTIONS"); + if (!already_added) req.permission!.hasThrow("ADD_REACTIONS"); if (emoji.id) { const external_emoji = await Emoji.findOneOrFail({ id: emoji.id }); - if (!already_added) permissions.hasThrow("USE_EXTERNAL_EMOJIS"); + if (!already_added) req.permission!.hasThrow("USE_EXTERNAL_EMOJIS"); emoji.animated = external_emoji.animated; emoji.name = external_emoji.name; } @@ -154,7 +144,7 @@ router.put("/:emoji/:user_id", async (req: Request, res: Response) => { res.sendStatus(204); }); -router.delete("/:emoji/:user_id", async (req: Request, res: Response) => { +router.delete("/:emoji/:user_id", route({}), async (req: Request, res: Response) => { var { message_id, channel_id, user_id } = req.params; const emoji = getEmoji(req.params.emoji); @@ -162,10 +152,11 @@ router.delete("/:emoji/:user_id", async (req: Request, res: Response) => { const channel = await Channel.findOneOrFail({ id: channel_id }); const message = await Message.findOneOrFail({ id: message_id, channel_id }); - const permissions = await getPermission(req.user_id, undefined, channel_id); - if (user_id === "@me") user_id = req.user_id; - else permissions.hasThrow("MANAGE_MESSAGES"); + 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); diff --git a/api/src/routes/channels/#channel_id/messages/bulk-delete.ts b/api/src/routes/channels/#channel_id/messages/bulk-delete.ts index 5c486676..a0fe7cc0 100644 --- a/api/src/routes/channels/#channel_id/messages/bulk-delete.ts +++ b/api/src/routes/channels/#channel_id/messages/bulk-delete.ts @@ -1,18 +1,21 @@ import { Router, Response, Request } from "express"; import { Channel, Config, emitEvent, getPermission, MessageDeleteBulkEvent, Message } from "@fosscord/util"; import { HTTPError } from "lambert-server"; - -import { check } from "../../../../util/instanceOf"; +import { route } from "@fosscord/api"; import { In } from "typeorm"; const router: Router = Router(); export default router; +export interface BulkDeleteSchema { + messages: string[]; +} + // TODO: should users be able to bulk delete messages or only bots? // TODO: should this request fail, if you provide messages older than 14 days/invalid ids? // https://discord.com/developers/docs/resources/channel#bulk-delete-messages -router.post("/", check({ messages: [String] }), async (req: Request, res: Response) => { +router.post("/", route({ body: "BulkDeleteSchema" }), async (req: Request, res: Response) => { const { channel_id } = req.params; const channel = await Channel.findOneOrFail({ id: channel_id }); if (!channel.guild_id) throw new HTTPError("Can't bulk delete dm channel messages", 400); diff --git a/api/src/routes/channels/#channel_id/messages/index.ts b/api/src/routes/channels/#channel_id/messages/index.ts index ad590d05..11334367 100644 --- a/api/src/routes/channels/#channel_id/messages/index.ts +++ b/api/src/routes/channels/#channel_id/messages/index.ts @@ -1,12 +1,10 @@ import { Router, Response, Request } from "express"; -import { Attachment, Channel, ChannelType, getPermission, Message } from "@fosscord/util"; +import { Attachment, Channel, ChannelType, Embed, getPermission, Message } from "@fosscord/util"; import { HTTPError } from "lambert-server"; -import { MessageCreateSchema } from "../../../../schema/Message"; -import { check, instanceOf, Length } from "../../../../util/instanceOf"; +import { instanceOf, Length, route } from "@fosscord/api"; import multer from "multer"; -import { Query } from "mongoose"; -import { sendMessage } from "../../../../util/Message"; -import { uploadFile } from "../../../../util/cdn"; +import { sendMessage } from "@fosscord/api"; +import { uploadFile } from "@fosscord/api"; import { FindManyOptions, LessThan, MoreThan } from "typeorm"; const router: Router = Router(); @@ -31,6 +29,30 @@ export function isTextChannel(type: ChannelType): boolean { } } +export interface MessageCreateSchema { + content?: string; + nonce?: string; + tts?: boolean; + flags?: string; + embeds?: Embed[]; + embed?: Embed; + // TODO: ^ embed is deprecated in favor of embeds (https://discord.com/developers/docs/resources/channel#message-object) + allowed_mentions?: { + parse?: string[]; + roles?: string[]; + users?: string[]; + replied_user?: boolean; + }; + message_reference?: { + message_id: string; + channel_id: string; + guild_id?: string; + fail_if_not_exists?: boolean; + }; + payload_json?: string; + file?: any; +} + // https://discord.com/developers/docs/resources/channel#create-message // get messages router.get("/", async (req: Request, res: Response) => { @@ -109,39 +131,44 @@ const messageUpload = multer({ // TODO: check allowed_mentions // Send message -router.post("/", messageUpload.single("file"), async (req: Request, res: Response) => { - const { channel_id } = req.params; - var body = req.body as MessageCreateSchema; - const attachments: Attachment[] = []; - - if (req.file) { - try { - const file = await uploadFile(`/attachments/${channel_id}`, req.file); - attachments.push({ ...file, proxy_url: file.url }); - } catch (error) { - return res.status(400).json(error); +router.post( + "/", + messageUpload.single("file"), + async (req, res, next) => { + if (req.body.payload_json) { + req.body = JSON.parse(req.body.payload_json); } - } - if (body.payload_json) { - body = JSON.parse(body.payload_json); - } + next(); + }, + route({ body: "MessageCreateSchema", permission: "SEND_MESSAGES" }), + async (req: Request, res: Response) => { + const { channel_id } = req.params; + var body = req.body as MessageCreateSchema; + const attachments: Attachment[] = []; + + if (req.file) { + try { + const file = await uploadFile(`/attachments/${req.params.channel_id}`, req.file); + attachments.push({ ...file, proxy_url: file.url }); + } catch (error) { + return res.status(400).json(error); + } + } - const errors = instanceOf(MessageCreateSchema, body, { req }); - if (errors !== true) throw errors; - - const embeds = []; - if (body.embed) embeds.push(body.embed); - const data = await sendMessage({ - ...body, - type: 0, - pinned: false, - author_id: req.user_id, - embeds, - channel_id, - attachments, - edited_timestamp: undefined - }); - - return res.json(data); -}); + const embeds = []; + if (body.embed) embeds.push(body.embed); + const data = await sendMessage({ + ...body, + type: 0, + pinned: false, + author_id: req.user_id, + embeds, + channel_id, + attachments, + edited_timestamp: undefined + }); + + return res.json(data); + } +); diff --git a/api/src/routes/channels/#channel_id/permissions.ts b/api/src/routes/channels/#channel_id/permissions.ts index 9c49542b..827e46f2 100644 --- a/api/src/routes/channels/#channel_id/permissions.ts +++ b/api/src/routes/channels/#channel_id/permissions.ts @@ -2,61 +2,61 @@ import { Channel, ChannelPermissionOverwrite, ChannelUpdateEvent, emitEvent, get import { Router, Response, Request } from "express"; import { HTTPError } from "lambert-server"; -import { check } from "../../../util/instanceOf"; +import { check, route } from "@fosscord/api"; 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) -router.put("/:overwrite_id", check({ allow: String, deny: String, type: Number, id: String }), async (req: Request, res: Response) => { - const { channel_id, overwrite_id } = req.params; - const body = req.body as { allow: bigint; deny: bigint; type: number; id: string }; +export interface ChannelPermissionOverwriteSchema extends ChannelPermissionOverwrite {} - var channel = await Channel.findOneOrFail({ id: channel_id }); - if (!channel.guild_id) throw new HTTPError("Channel not found", 404); +router.put( + "/:overwrite_id", + route({ body: "ChannelPermissionOverwriteSchema", permission: "MANAGE_ROLES" }), + async (req: Request, res: Response) => { + const { channel_id, overwrite_id } = req.params; + const body = req.body as { allow: bigint; deny: bigint; type: number; id: string }; - const permissions = await getPermission(req.user_id, channel.guild_id, channel_id); - permissions.hasThrow("MANAGE_ROLES"); + var channel = await Channel.findOneOrFail({ id: channel_id }); + if (!channel.guild_id) throw new HTTPError("Channel not found", 404); - if (body.type === 0) { - if (!(await Role.count({ id: overwrite_id }))) throw new HTTPError("role not found", 404); - } else if (body.type === 1) { - if (!(await Member.count({ id: overwrite_id }))) throw new HTTPError("user not found", 404); - } else throw new HTTPError("type not supported", 501); + if (body.type === 0) { + if (!(await Role.count({ id: overwrite_id }))) throw new HTTPError("role not found", 404); + } else if (body.type === 1) { + if (!(await Member.count({ 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); - if (!overwrite) { // @ts-ignore - overwrite = { - id: overwrite_id, - type: body.type, - allow: body.allow, - deny: body.deny - }; - channel.permission_overwrites.push(overwrite); + var overwrite: ChannelPermissionOverwrite = channel.permission_overwrites.find((x) => x.id === overwrite_id); + if (!overwrite) { + // @ts-ignore + overwrite = { + id: overwrite_id, + type: body.type, + allow: body.allow, + deny: body.deny + }; + channel.permission_overwrites.push(overwrite); + } + overwrite.allow = body.allow; + overwrite.deny = body.deny; + + await Promise.all([ + channel.save(), + emitEvent({ + event: "CHANNEL_UPDATE", + channel_id, + data: channel + } as ChannelUpdateEvent) + ]); + + return res.sendStatus(204); } - overwrite.allow = body.allow; - overwrite.deny = body.deny; - - // @ts-ignore - channel = await Channel.findOneOrFailAndUpdate({ id: channel_id }, channel, { new: true }); - - await emitEvent({ - event: "CHANNEL_UPDATE", - channel_id, - data: channel - } as ChannelUpdateEvent); - - return res.sendStatus(204); -}); +); // TODO: check permission hierarchy -router.delete("/:overwrite_id", async (req: Request, res: Response) => { +router.delete("/:overwrite_id", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { const { channel_id, overwrite_id } = req.params; - const permissions = await getPermission(req.user_id, undefined, channel_id); - permissions.hasThrow("MANAGE_ROLES"); - const channel = await Channel.findOneOrFail({ id: channel_id }); if (!channel.guild_id) throw new HTTPError("Channel not found", 404); diff --git a/api/src/routes/channels/#channel_id/pins.ts b/api/src/routes/channels/#channel_id/pins.ts index 33309c86..e71e659f 100644 --- a/api/src/routes/channels/#channel_id/pins.ts +++ b/api/src/routes/channels/#channel_id/pins.ts @@ -1,19 +1,26 @@ -import { Channel, ChannelPinsUpdateEvent, Config, emitEvent, getPermission, Message, MessageUpdateEvent } from "@fosscord/util"; +import { + Channel, + ChannelPinsUpdateEvent, + Config, + emitEvent, + getPermission, + Message, + MessageUpdateEvent, + DiscordApiErrors +} from "@fosscord/util"; import { Router, Request, Response } from "express"; import { HTTPError } from "lambert-server"; -import { DiscordApiErrors } from "@fosscord/util"; +import { route } from "@fosscord/api"; const router: Router = Router(); -router.put("/:message_id", async (req: Request, res: Response) => { +router.put("/:message_id", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: Response) => { const { channel_id, message_id } = req.params; const message = await Message.findOneOrFail({ id: message_id }); - const permission = await getPermission(req.user_id, message.guild_id, channel_id); - permission.hasThrow("VIEW_CHANNEL"); // * in dm channels anyone can pin messages -> only check for guilds - if (message.guild_id) permission.hasThrow("MANAGE_MESSAGES"); + if (message.guild_id) req.permission!.hasThrow("MANAGE_MESSAGES"); const pinned_count = await Message.count({ channel: { id: channel_id }, pinned: true }); const { maxPins } = Config.get().limits.channel; @@ -26,7 +33,6 @@ router.put("/:message_id", async (req: Request, res: Response) => { channel_id, data: message } as MessageUpdateEvent), - emitEvent({ event: "CHANNEL_PINS_UPDATE", channel_id, @@ -41,14 +47,11 @@ router.put("/:message_id", async (req: Request, res: Response) => { res.sendStatus(204); }); -router.delete("/:message_id", async (req: Request, res: Response) => { +router.delete("/:message_id", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: Response) => { const { channel_id, message_id } = req.params; const channel = await Channel.findOneOrFail({ id: channel_id }); - - const permission = await getPermission(req.user_id, channel.guild_id, channel_id); - permission.hasThrow("VIEW_CHANNEL"); - if (channel.guild_id) permission.hasThrow("MANAGE_MESSAGES"); + if (channel.guild_id) req.permission!.hasThrow("MANAGE_MESSAGES"); const message = await Message.findOneOrFail({ id: message_id }); message.pinned = false; @@ -76,13 +79,9 @@ router.delete("/:message_id", async (req: Request, res: Response) => { res.sendStatus(204); }); -router.get("/", async (req: Request, res: Response) => { +router.get("/", route({ permission: ["READ_MESSAGE_HISTORY"] }), async (req: Request, res: Response) => { const { channel_id } = req.params; - const channel = await Channel.findOneOrFail({ id: channel_id }); - const permission = await getPermission(req.user_id, channel.guild_id, channel_id); - permission.hasThrow("VIEW_CHANNEL"); - let pins = await Message.find({ channel_id: channel_id, pinned: true }); res.send(pins); diff --git a/api/src/routes/channels/#channel_id/typing.ts b/api/src/routes/channels/#channel_id/typing.ts index f1fb3c86..ad973bca 100644 --- a/api/src/routes/channels/#channel_id/typing.ts +++ b/api/src/routes/channels/#channel_id/typing.ts @@ -1,11 +1,10 @@ import { Channel, emitEvent, Member, TypingStartEvent } from "@fosscord/util"; +import { route } from "@fosscord/api"; import { Router, Request, Response } from "express"; -import { HTTPError } from "lambert-server"; - const router: Router = Router(); -router.post("/", async (req: Request, res: Response) => { +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(); @@ -24,6 +23,7 @@ router.post("/", async (req: Request, res: Response) => { guild_id: channel.guild_id } } as TypingStartEvent); + res.sendStatus(204); }); diff --git a/api/src/routes/channels/#channel_id/webhooks.ts b/api/src/routes/channels/#channel_id/webhooks.ts index e4125879..f84dfcc5 100644 --- a/api/src/routes/channels/#channel_id/webhooks.ts +++ b/api/src/routes/channels/#channel_id/webhooks.ts @@ -1,5 +1,5 @@ import { Router, Response, Request } from "express"; -import { check, Length } from "../../../util/instanceOf"; +import { check, Length, route } from "@fosscord/api"; import { Channel, Config, getPermission, trimSpecial, Webhook } from "@fosscord/util"; import { HTTPError } from "lambert-server"; import { isTextChannel } from "./messages/index"; @@ -7,9 +7,16 @@ import { DiscordApiErrors } from "@fosscord/util"; const router: Router = Router(); // TODO: webhooks +export interface WebhookCreateSchema { + /** + * @maxLength 80 + */ + name: string; + avatar: string; +} // TODO: use Image Data Type for avatar instead of String -router.post("/", check({ name: new Length(String, 1, 80), $avatar: String }), async (req: Request, res: Response) => { +router.post("/", route({ body: "WebhookCreateSchema", permission: "MANAGE_WEBHOOKS" }), async (req: Request, res: Response) => { const channel_id = req.params.channel_id; const channel = await Channel.findOneOrFail({ id: channel_id }); @@ -20,12 +27,11 @@ router.post("/", check({ name: new Length(String, 1, 80), $avatar: String }), as const { maxWebhooks } = Config.get().limits.channel; if (webhook_count > maxWebhooks) throw DiscordApiErrors.MAXIMUM_WEBHOOKS.withParams(maxWebhooks); - const permission = await getPermission(req.user_id, channel.guild_id); - permission.hasThrow("MANAGE_WEBHOOKS"); - 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/api/src/routes/discoverable-guilds.ts b/api/src/routes/discoverable-guilds.ts index 0808727f..f667eb2a 100644 --- a/api/src/routes/discoverable-guilds.ts +++ b/api/src/routes/discoverable-guilds.ts @@ -1,10 +1,10 @@ import { Guild } from "@fosscord/util"; import { Router, Request, Response } from "express"; -import { In } from "typeorm"; +import { route } from "@fosscord/api"; const router = Router(); -router.get("/", async (req: Request, res: Response) => { +router.get("/", route({}), async (req: Request, res: Response) => { const { limit } = req.params; // ! this only works using SQL querys diff --git a/api/src/routes/experiments.ts b/api/src/routes/experiments.ts index 3bdbed62..966ed99c 100644 --- a/api/src/routes/experiments.ts +++ b/api/src/routes/experiments.ts @@ -1,8 +1,9 @@ import { Router, Response, Request } from "express"; +import { route } from "@fosscord/api"; const router = Router(); -router.get("/", (req: Request, res: Response) => { +router.get("/", route({}), (req: Request, res: Response) => { // TODO: res.send({ fingerprint: "", assignments: [] }); }); diff --git a/api/src/routes/gateway.ts b/api/src/routes/gateway.ts index 3120718e..d4208341 100644 --- a/api/src/routes/gateway.ts +++ b/api/src/routes/gateway.ts @@ -1,9 +1,10 @@ import { Config } from "@fosscord/util"; import { Router, Response, Request } from "express"; +import { route } from "@fosscord/api"; const router = Router(); -router.get("/", (req: Request, res: Response) => { +router.get("/", route({}), (req: Request, res: Response) => { const { endpoint } = Config.get().gateway; res.json({ url: endpoint || process.env.GATEWAY || "ws://localhost:3002" }); }); diff --git a/api/src/routes/guilds/#guild_id/bans.ts b/api/src/routes/guilds/#guild_id/bans.ts index 31aa2385..c7fda9ad 100644 --- a/api/src/routes/guilds/#guild_id/bans.ts +++ b/api/src/routes/guilds/#guild_id/bans.ts @@ -1,20 +1,22 @@ import { Request, Response, Router } from "express"; import { emitEvent, getPermission, GuildBanAddEvent, GuildBanRemoveEvent, Guild, Ban, User, Member } from "@fosscord/util"; import { HTTPError } from "lambert-server"; -import { getIpAdress } from "../../../util/ipAddress"; -import { BanCreateSchema } from "../../../schema/Ban"; -import { check } from "../../../util/instanceOf"; +import { getIpAdress, check, route } from "@fosscord/api"; -const router: Router = Router(); +export interface BanCreateSchema { + delete_message_days?: string; + reason?: string; +} -router.get("/", async (req: Request, res: Response) => { +const router: Router = Router(); +router.get("/", route({ permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => { const { guild_id } = req.params; var bans = await Ban.find({ guild_id: guild_id }); return res.json(bans); }); -router.get("/:user", async (req: Request, res: Response) => { +router.get("/:user", route({ permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => { const { guild_id } = req.params; const user_id = req.params.ban; @@ -22,15 +24,14 @@ router.get("/:user", async (req: Request, res: Response) => { return res.json(ban); }); -router.put("/:user_id", check(BanCreateSchema), async (req: Request, res: Response) => { +router.put("/:user_id", route({ body: "BanCreateSchema", permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => { const { guild_id } = req.params; const banned_user_id = req.params.user_id; const banned_user = await User.getPublicUser(banned_user_id); - const perms = await getPermission(req.user_id, guild_id); - perms.hasThrow("BAN_MEMBERS"); + if (req.user_id === banned_user_id) throw new HTTPError("You can't ban yourself", 400); - if (perms.cache.guild?.owner_id === banned_user_id) throw new HTTPError("You can't ban the owner", 400); + if (req.permission!.cache.guild?.owner_id === banned_user_id) throw new HTTPError("You can't ban the owner", 400); const ban = new Ban({ user_id: banned_user_id, @@ -56,17 +57,14 @@ router.put("/:user_id", check(BanCreateSchema), async (req: Request, res: Respon return res.json(ban); }); -router.delete("/:user_id", async (req: Request, res: Response) => { - var { guild_id } = req.params; - var banned_user_id = req.params.user_id; +router.delete("/:user_id", route({ permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => { + const { guild_id, user_id } = req.params; - const banned_user = await User.getPublicUser(banned_user_id); - const perms = await getPermission(req.user_id, guild_id); - perms.hasThrow("BAN_MEMBERS"); + const banned_user = await User.getPublicUser(user_id); await Promise.all([ Ban.delete({ - user_id: banned_user_id, + user_id: user_id, guild_id }), diff --git a/api/src/routes/guilds/#guild_id/channels.ts b/api/src/routes/guilds/#guild_id/channels.ts index 5aa1d33d..e21327d1 100644 --- a/api/src/routes/guilds/#guild_id/channels.ts +++ b/api/src/routes/guilds/#guild_id/channels.ts @@ -1,9 +1,8 @@ import { Router, Response, Request } from "express"; import { Channel, ChannelUpdateEvent, getPermission, emitEvent } from "@fosscord/util"; import { HTTPError } from "lambert-server"; -import { ChannelModifySchema } from "../../../schema/Channel"; - -import { check } from "../../../util/instanceOf"; +import { check, route } from "@fosscord/api"; +import { ChannelModifySchema } from "../../channels/#channel_id"; const router = Router(); router.get("/", async (req: Request, res: Response) => { @@ -13,10 +12,7 @@ router.get("/", async (req: Request, res: Response) => { res.json(channels); }); -// TODO: check if channel type is permitted -// TODO: check if parent_id exists - -router.post("/", check(ChannelModifySchema), async (req: Request, res: Response) => { +router.post("/", route({ body: "ChannelModifySchema", permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => { // creates a new guild channel https://discord.com/developers/docs/resources/guild#create-guild-channel const { guild_id } = req.params; const body = req.body as ChannelModifySchema; @@ -26,45 +22,39 @@ router.post("/", check(ChannelModifySchema), async (req: Request, res: Response) res.status(201).json(channel); }); -// TODO: check if parent_id exists -router.patch( - "/", - check([{ id: String, $position: Number, $lock_permissions: Boolean, $parent_id: String }]), - async (req: Request, res: Response) => { - // changes guild channel position - const { guild_id } = req.params; - const body = req.body as { id: string; position?: number; lock_permissions?: boolean; parent_id?: string }[]; - - const permission = await getPermission(req.user_id, guild_id); - permission.hasThrow("MANAGE_CHANNELS"); - - await Promise.all([ - body.map(async (x) => { - if (!x.position && !x.parent_id) throw new HTTPError(`You need to at least specify position or parent_id`, 400); +export type ChannelReorderSchema = { id: string; position?: number; lock_permissions?: boolean; parent_id?: string }[]; - const opts: any = {}; - if (x.position) 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; - } +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 && !x.parent_id) throw new HTTPError(`You need to at least specify position or parent_id`, 400); + + const opts: any = {}; + if (x.position) 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; } + } - await Channel.update({ guild_id, id: x.id }, opts); - const channel = await Channel.findOneOrFail({ guild_id, id: x.id }); + await Channel.update({ guild_id, id: x.id }, opts); + const channel = await Channel.findOneOrFail({ 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/api/src/routes/guilds/#guild_id/delete.ts b/api/src/routes/guilds/#guild_id/delete.ts index bbbd1fa4..7c3c5530 100644 --- a/api/src/routes/guilds/#guild_id/delete.ts +++ b/api/src/routes/guilds/#guild_id/delete.ts @@ -1,12 +1,13 @@ 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"; const router = Router(); // discord prefixes this route with /delete instead of using the delete method // docs are wrong https://discord.com/developers/docs/resources/guild#delete-guild -router.post("/", async (req: Request, res: Response) => { +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"] }); diff --git a/api/src/routes/guilds/#guild_id/index.ts b/api/src/routes/guilds/#guild_id/index.ts index 9d302a48..690d4103 100644 --- a/api/src/routes/guilds/#guild_id/index.ts +++ b/api/src/routes/guilds/#guild_id/index.ts @@ -1,23 +1,36 @@ import { Request, Response, Router } from "express"; import { emitEvent, getPermission, Guild, GuildUpdateEvent, Member } from "@fosscord/util"; import { HTTPError } from "lambert-server"; -import { GuildUpdateSchema } from "../../../schema/Guild"; - -import { check } from "../../../util/instanceOf"; -import { handleFile } from "../../../util/cdn"; +import { check, route } from "@fosscord/api"; +import { handleFile } from "@fosscord/api"; import "missing-native-js-functions"; +import { GuildCreateSchema } from "../index"; const router = Router(); -router.get("/", async (req: Request, res: Response) => { +export interface GuildUpdateSchema extends Omit<GuildCreateSchema, "channels"> { + banner?: string; + splash?: string; + description?: string; + features?: string[]; + verification_level?: number; + default_message_notifications?: number; + system_channel_flags?: number; + explicit_content_filter?: number; + public_updates_channel_id?: string; + afk_timeout?: number; + afk_channel_id?: string; + preferred_locale?: string; +} + +router.get("/", route({}), async (req: Request, res: Response) => { const { guild_id } = req.params; - const [guild, member_count, member] = await Promise.all([ + const [guild, member] = await Promise.all([ Guild.findOneOrFail({ id: guild_id }), - Member.count({ guild_id: guild_id, id: req.user_id }), - Member.findOneOrFail({ id: req.user_id }) + Member.findOne({ guild_id: guild_id, id: req.user_id }) ]); - if (!member_count) 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; @@ -25,14 +38,11 @@ router.get("/", async (req: Request, res: Response) => { return res.json(guild); }); -router.patch("/", check(GuildUpdateSchema), async (req: Request, res: Response) => { +router.patch("/", route({ body: "GuildUpdateSchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { const body = req.body as GuildUpdateSchema; const { guild_id } = req.params; // TODO: guild update check image - const perms = await getPermission(req.user_id, guild_id); - perms.hasThrow("MANAGE_GUILD"); - 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); diff --git a/api/src/routes/guilds/#guild_id/invites.ts b/api/src/routes/guilds/#guild_id/invites.ts index 39a934ee..b7534e31 100644 --- a/api/src/routes/guilds/#guild_id/invites.ts +++ b/api/src/routes/guilds/#guild_id/invites.ts @@ -1,14 +1,12 @@ import { getPermission, Invite, PublicInviteRelation } from "@fosscord/util"; +import { route } from "@fosscord/api"; import { Request, Response, Router } from "express"; const router = Router(); -router.get("/", async (req: Request, res: Response) => { +router.get("/", route({ permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { const { guild_id } = req.params; - const permissions = await getPermission(req.user_id, guild_id); - permissions.hasThrow("MANAGE_GUILD"); - const invites = await Invite.find({ where: { guild_id }, relations: PublicInviteRelation }); return res.json(invites); diff --git a/api/src/routes/guilds/#guild_id/members/#member_id/index.ts b/api/src/routes/guilds/#guild_id/members/#member_id/index.ts index 0d62e555..1708b7eb 100644 --- a/api/src/routes/guilds/#guild_id/members/#member_id/index.ts +++ b/api/src/routes/guilds/#guild_id/members/#member_id/index.ts @@ -1,23 +1,15 @@ import { Request, Response, Router } from "express"; -import { - Guild, - Member, - User, - GuildMemberAddEvent, - getPermission, - PermissionResolvable, - Role, - GuildMemberUpdateEvent, - emitEvent -} from "@fosscord/util"; +import { Member, getPermission, Role, GuildMemberUpdateEvent, emitEvent } from "@fosscord/util"; import { HTTPError } from "lambert-server"; -import { check } from "../../../../../util/instanceOf"; -import { MemberChangeSchema } from "../../../../../schema/Member"; -import { In } from "typeorm"; +import { check, route } from "@fosscord/api"; const router = Router(); -router.get("/", async (req: Request, res: Response) => { +export interface MemberChangeSchema { + roles?: string[]; +} + +router.get("/", route({}), async (req: Request, res: Response) => { const { guild_id, member_id } = req.params; await Member.IsInGuildOrFail(req.user_id, guild_id); @@ -26,8 +18,9 @@ router.get("/", async (req: Request, res: Response) => { return res.json(member); }); -router.patch("/", check(MemberChangeSchema), async (req: Request, res: Response) => { - const { guild_id, member_id } = req.params; +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"] }); @@ -39,7 +32,7 @@ router.patch("/", check(MemberChangeSchema), async (req: Request, res: Response) } await member.save(); - // do not use promise.all as we have to first write to db before emitting the event + // 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, @@ -49,7 +42,7 @@ router.patch("/", check(MemberChangeSchema), async (req: Request, res: Response) res.json(member); }); -router.put("/", async (req: Request, res: Response) => { +router.put("/", route({}), async (req: Request, res: Response) => { let { guild_id, member_id } = req.params; if (member_id === "@me") member_id = req.user_id; @@ -59,12 +52,9 @@ router.put("/", async (req: Request, res: Response) => { res.sendStatus(204); }); -router.delete("/", async (req: Request, res: Response) => { +router.delete("/", route({ permission: "KICK_MEMBERS" }), async (req: Request, res: Response) => { const { guild_id, member_id } = req.params; - const perms = await getPermission(req.user_id, guild_id); - perms.hasThrow("KICK_MEMBERS"); - await Member.removeFromGuild(member_id, guild_id); res.sendStatus(204); }); diff --git a/api/src/routes/guilds/#guild_id/members/#member_id/nick.ts b/api/src/routes/guilds/#guild_id/members/#member_id/nick.ts index 3f2975e6..27f7f65d 100644 --- a/api/src/routes/guilds/#guild_id/members/#member_id/nick.ts +++ b/api/src/routes/guilds/#guild_id/members/#member_id/nick.ts @@ -1,11 +1,14 @@ import { getPermission, Member, PermissionResolvable } from "@fosscord/util"; +import { route } from "@fosscord/api"; import { Request, Response, Router } from "express"; -import { check } from "lambert-server"; -import { MemberNickChangeSchema } from "../../../../../schema/Member"; const router = Router(); -router.patch("/", check(MemberNickChangeSchema), async (req: Request, res: Response) => { +export interface MemberNickChangeSchema { + nick: string; +} + +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") { diff --git a/api/src/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts b/api/src/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts index cb9bad9a..ae10be82 100644 --- a/api/src/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts +++ b/api/src/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts @@ -1,24 +1,19 @@ import { getPermission, Member } from "@fosscord/util"; +import { route } from "@fosscord/api"; import { Request, Response, Router } from "express"; const router = Router(); -router.delete("/:member_id/roles/:role_id", async (req: Request, res: Response) => { +router.delete("/:member_id/roles/:role_id", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { const { guild_id, role_id, member_id } = req.params; - const perms = await getPermission(req.user_id, guild_id); - perms.hasThrow("MANAGE_ROLES"); - await Member.removeRole(member_id, guild_id, role_id); res.sendStatus(204); }); -router.put("/:member_id/roles/:role_id", async (req: Request, res: Response) => { +router.put("/:member_id/roles/:role_id", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { const { guild_id, role_id, member_id } = req.params; - const perms = await getPermission(req.user_id, guild_id); - perms.hasThrow("MANAGE_ROLES"); - await Member.addRole(member_id, guild_id, role_id); res.sendStatus(204); }); diff --git a/api/src/routes/guilds/#guild_id/members/index.ts b/api/src/routes/guilds/#guild_id/members/index.ts index 0bfd71cb..335f21c7 100644 --- a/api/src/routes/guilds/#guild_id/members/index.ts +++ b/api/src/routes/guilds/#guild_id/members/index.ts @@ -1,13 +1,15 @@ import { Request, Response, Router } from "express"; import { Guild, Member, PublicMemberProjection } from "@fosscord/util"; -import { instanceOf, Length } from "../../../../util/instanceOf"; +import { instanceOf, Length, route } from "@fosscord/api"; import { MoreThan } from "typeorm"; const router = Router(); // TODO: not allowed for user -> only allowed for bots with privileged intents // TODO: send over websocket -router.get("/", async (req: Request, res: Response) => { +// TODO: check for GUILD_MEMBERS intent + +router.get("/", route({}), async (req: Request, res: Response) => { const { guild_id } = req.params; const guild = await Guild.findOneOrFail({ id: guild_id }); await Member.IsInGuildOrFail(req.user_id, guild_id); diff --git a/api/src/routes/guilds/#guild_id/regions.ts b/api/src/routes/guilds/#guild_id/regions.ts index 212c9bcd..75d24fd1 100644 --- a/api/src/routes/guilds/#guild_id/regions.ts +++ b/api/src/routes/guilds/#guild_id/regions.ts @@ -1,11 +1,11 @@ -import {Config, Guild, Member} from "@fosscord/util"; +import { Config, Guild, Member } from "@fosscord/util"; import { Request, Response, Router } from "express"; -import {getVoiceRegions} from "../../../util/Voice"; -import {getIpAdress} from "../../../util/ipAddress"; +import { getVoiceRegions, route } from "@fosscord/api"; +import { getIpAdress } from "@fosscord/api"; const router = Router(); -router.get("/", async (req: Request, res: Response) => { +router.get("/", route({}), async (req: Request, res: Response) => { const { guild_id } = req.params; const guild = await Guild.findOneOrFail({ id: guild_id }); //TODO we should use an enum for guild's features and not hardcoded strings diff --git a/api/src/routes/guilds/#guild_id/roles.ts b/api/src/routes/guilds/#guild_id/roles.ts index 6a318688..6b2902d9 100644 --- a/api/src/routes/guilds/#guild_id/roles.ts +++ b/api/src/routes/guilds/#guild_id/roles.ts @@ -2,23 +2,34 @@ import { Request, Response, Router } from "express"; import { Role, getPermission, - Snowflake, Member, GuildRoleCreateEvent, GuildRoleUpdateEvent, GuildRoleDeleteEvent, emitEvent, - Config + Config, + DiscordApiErrors } from "@fosscord/util"; import { HTTPError } from "lambert-server"; - -import { check } from "../../../util/instanceOf"; -import { RoleModifySchema, RolePositionUpdateSchema } from "../../../schema/Roles"; -import { DiscordApiErrors } from "@fosscord/util"; +import { check, route } from "@fosscord/api"; import { In } from "typeorm"; const router: Router = Router(); +export interface RoleModifySchema { + name?: string; + permissions?: bigint; + color?: number; + hoist?: boolean; // whether the role should be displayed separately in the sidebar + mentionable?: boolean; // whether the role should be mentionable + position?: number; +} + +export type RolePositionUpdateSchema = { + id: string; + position: number; +}[]; + router.get("/", async (req: Request, res: Response) => { const guild_id = req.params.guild_id; @@ -29,13 +40,10 @@ router.get("/", async (req: Request, res: Response) => { return res.json(roles); }); -router.post("/", check(RoleModifySchema), async (req: Request, res: Response) => { +router.post("/", route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { const guild_id = req.params.guild_id; const body = req.body as RoleModifySchema; - const perms = await getPermission(req.user_id, guild_id); - perms.hasThrow("MANAGE_ROLES"); - const role_count = await Role.count({ guild_id }); const { maxRoles } = Config.get().limits.guild; @@ -50,7 +58,7 @@ router.post("/", check(RoleModifySchema), async (req: Request, res: Response) => ...body, guild_id: guild_id, managed: false, - permissions: String(perms.bitfield & (body.permissions || 0n)), + permissions: String(req.permission!.bitfield & (body.permissions || 0n)), tags: undefined }); @@ -69,14 +77,11 @@ router.post("/", check(RoleModifySchema), async (req: Request, res: Response) => res.json(role); }); -router.delete("/:role_id", async (req: Request, res: Response) => { +router.delete("/:role_id", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { const guild_id = req.params.guild_id; const { role_id } = req.params; if (role_id === guild_id) throw new HTTPError("You can't delete the @everyone role"); - const permissions = await getPermission(req.user_id, guild_id); - permissions.hasThrow("MANAGE_ROLES"); - await Promise.all([ Role.delete({ id: role_id, @@ -97,14 +102,11 @@ router.delete("/:role_id", async (req: Request, res: Response) => { // TODO: check role hierarchy -router.patch("/:role_id", check(RoleModifySchema), async (req: Request, res: Response) => { +router.patch("/:role_id", route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { const { role_id, guild_id } = req.params; const body = req.body as RoleModifySchema; - const perms = await getPermission(req.user_id, guild_id); - perms.hasThrow("MANAGE_ROLES"); - - const role = new Role({ ...body, id: role_id, guild_id, permissions: String(perms.bitfield & (body.permissions || 0n)) }); + const role = new Role({ ...body, id: role_id, guild_id, permissions: String(req.permission!.bitfield & (body.permissions || 0n)) }); await Promise.all([ role.save(), @@ -121,7 +123,7 @@ router.patch("/:role_id", check(RoleModifySchema), async (req: Request, res: Res res.json(role); }); -router.patch("/", check(RolePositionUpdateSchema), async (req: Request, res: Response) => { +router.patch("/", route({ body: "RolePositionUpdateSchema" }), async (req: Request, res: Response) => { const { guild_id } = req.params; const body = req.body as RolePositionUpdateSchema; diff --git a/api/src/routes/guilds/#guild_id/templates.ts b/api/src/routes/guilds/#guild_id/templates.ts index a7613abf..5179e761 100644 --- a/api/src/routes/guilds/#guild_id/templates.ts +++ b/api/src/routes/guilds/#guild_id/templates.ts @@ -1,9 +1,8 @@ import { Request, Response, Router } from "express"; -import { Guild, getPermission, Template } from "@fosscord/util"; +import { Guild, Template } from "@fosscord/util"; import { HTTPError } from "lambert-server"; -import { TemplateCreateSchema, TemplateModifySchema } from "../../../schema/Template"; -import { check } from "../../../util/instanceOf"; -import { generateCode } from "../../../util/String"; +import { route } from "@fosscord/api"; +import { generateCode } from "@fosscord/api"; const router: Router = Router(); @@ -24,7 +23,17 @@ const TemplateGuildProjection: (keyof Guild)[] = [ "icon" ]; -router.get("/", async (req: Request, res: Response) => { +export interface TemplateCreateSchema { + name: string; + description?: string; +} + +export interface TemplateModifySchema { + name: string; + description?: string; +} + +router.get("/", route({}), async (req: Request, res: Response) => { const { guild_id } = req.params; var templates = await Template.find({ source_guild_id: guild_id }); @@ -32,12 +41,9 @@ router.get("/", async (req: Request, res: Response) => { return res.json(templates); }); -router.post("/", check(TemplateCreateSchema), async (req: Request, res: Response) => { +router.post("/", route({ body: "TemplateCreateSchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { const { guild_id } = req.params; const guild = await Guild.findOneOrFail({ where: { id: guild_id }, select: TemplateGuildProjection }); - const perms = await getPermission(req.user_id, guild_id); - perms.hasThrow("MANAGE_GUILD"); - const exists = await Template.findOneOrFail({ id: guild_id }).catch((e) => {}); if (exists) throw new HTTPError("Template already exists", 400); @@ -54,44 +60,31 @@ router.post("/", check(TemplateCreateSchema), async (req: Request, res: Response res.json(template); }); -router.delete("/:code", async (req: Request, res: Response) => { - const guild_id = req.params.guild_id; - const { code } = req.params; - - const perms = await getPermission(req.user_id, guild_id); - perms.hasThrow("MANAGE_GUILD"); +router.delete("/:code", route({ permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { + const { code, guild_id } = req.params; const template = await Template.delete({ - code + code, + source_guild_id: guild_id }); res.json(template); }); -router.put("/:code", async (req: Request, res: Response) => { - // synchronizes the 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 perms = await getPermission(req.user_id, guild_id); - perms.hasThrow("MANAGE_GUILD"); - const template = await new Template({ code, serialized_source_guild: guild }).save(); res.json(template); }); -router.patch("/:code", check(TemplateModifySchema), async (req: Request, res: Response) => { - // updates the template description - const { guild_id } = req.params; - const { code } = req.params; +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 perms = await getPermission(req.user_id, guild_id); - perms.hasThrow("MANAGE_GUILD"); - - const template = await new Template({ code, name: name, description: description }).save(); + const template = await new Template({ code, name: name, description: description, source_guild_id: guild_id }).save(); res.json(template); }); diff --git a/api/src/routes/guilds/#guild_id/vanity-url.ts b/api/src/routes/guilds/#guild_id/vanity-url.ts index 58940b42..9c0989cc 100644 --- a/api/src/routes/guilds/#guild_id/vanity-url.ts +++ b/api/src/routes/guilds/#guild_id/vanity-url.ts @@ -1,35 +1,37 @@ import { Channel, ChannelType, getPermission, Guild, Invite, trimSpecial } from "@fosscord/util"; import { Router, Request, Response } from "express"; -import { HTTPError } from "lambert-server"; -import { check, Length } from "../../../util/instanceOf"; +import { check, Length, route } from "@fosscord/api"; const router = Router(); const InviteRegex = /\W/g; -router.get("/", async (req: Request, res: Response) => { +router.get("/", route({ permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { const { guild_id } = req.params; - const permission = await getPermission(req.user_id, guild_id); - permission.hasThrow("MANAGE_GUILD"); - const guild = await Guild.findOneOrFail({ where: { id: guild_id }, relations: ["vanity_url"] }); if (!guild.vanity_url) return res.json({ code: null }); return res.json({ code: guild.vanity_url_code, uses: guild.vanity_url.uses }); }); +export interface VanityUrlSchema { + /** + * @minLength 1 + * @maxLength 20 + */ + code?: string; +} + // TODO: check if guild is elgible for vanity url -router.patch("/", check({ code: new Length(String, 0, 20) }), async (req: Request, res: Response) => { +router.patch("/", route({ body: "VanityUrlSchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { const { guild_id } = req.params; - const code = req.body.code.replace(InviteRegex); + const body = req.body as VanityUrlSchema; + const code = body.code?.replace(InviteRegex, ""); await Invite.findOneOrFail({ code }); const guild = await Guild.findOneOrFail({ id: guild_id }); - const permission = await getPermission(req.user_id, guild_id); - permission.hasThrow("MANAGE_GUILD"); - const { id } = await Channel.findOneOrFail({ guild_id, type: ChannelType.GUILD_TEXT }); guild.vanity_url_code = code; diff --git a/api/src/routes/guilds/#guild_id/voice-states/#user_id/index.ts b/api/src/routes/guilds/#guild_id/voice-states/#user_id/index.ts index 02951f81..3d76938b 100644 --- a/api/src/routes/guilds/#guild_id/voice-states/#user_id/index.ts +++ b/api/src/routes/guilds/#guild_id/voice-states/#user_id/index.ts @@ -1,15 +1,60 @@ -import { check } from "../../../../../util/instanceOf"; -import { VoiceStateUpdateSchema } from "../../../../../schema"; +import { Channel, ChannelType, DiscordApiErrors, emitEvent, getPermission, VoiceState, VoiceStateUpdateEvent } from "@fosscord/util"; +import { check, route } from "@fosscord/api"; import { Request, Response, Router } from "express"; -import { updateVoiceState } from "../../../../../util/VoiceState"; const router = Router(); +//TODO need more testing when community guild and voice stage channel are working -router.patch("/", check(VoiceStateUpdateSchema), async (req: Request, res: Response) => { +export interface VoiceStateUpdateSchema { + channel_id: string; + guild_id?: string; + suppress?: boolean; + request_to_speak_timestamp?: Date; + self_mute?: boolean; + self_deaf?: boolean; + self_video?: boolean; +} + +router.patch("/", route({ body: "VoiceStateUpdateSchema" }), async (req: Request, res: Response) => { const body = req.body as VoiceStateUpdateSchema; - const { guild_id, user_id } = req.params; - await updateVoiceState(body, guild_id, req.user_id, user_id) + 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); + + /* + 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({ + 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({ 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); }); -export default router; \ No newline at end of file +export default router; diff --git a/api/src/routes/guilds/#guild_id/voice-states/@me/index.ts b/api/src/routes/guilds/#guild_id/voice-states/@me/index.ts deleted file mode 100644 index 42ba543e..00000000 --- a/api/src/routes/guilds/#guild_id/voice-states/@me/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { check } from "../../../../../util/instanceOf"; -import { VoiceStateUpdateSchema } from "../../../../../schema"; -import { Request, Response, Router } from "express"; -import { updateVoiceState } from "../../../../../util/VoiceState"; - -const router = Router(); - -router.patch("/", check(VoiceStateUpdateSchema), async (req: Request, res: Response) => { - const body = req.body as VoiceStateUpdateSchema; - const { guild_id } = req.params; - await updateVoiceState(body, guild_id, req.user_id) - return res.sendStatus(204); -}); - -export default router; \ No newline at end of file diff --git a/api/src/routes/guilds/#guild_id/welcome_screen.ts b/api/src/routes/guilds/#guild_id/welcome_screen.ts index defbcd40..7141f17e 100644 --- a/api/src/routes/guilds/#guild_id/welcome_screen.ts +++ b/api/src/routes/guilds/#guild_id/welcome_screen.ts @@ -1,31 +1,36 @@ import { Request, Response, Router } from "express"; import { Guild, getPermission, Snowflake, Member } from "@fosscord/util"; import { HTTPError } from "lambert-server"; - -import { check } from "../../../util/instanceOf"; -import { GuildUpdateWelcomeScreenSchema } from "../../../schema/Guild"; +import { route } from "@fosscord/api"; const router: Router = Router(); -router.get("/", async (req: Request, res: Response) => { +export interface GuildUpdateWelcomeScreenSchema { + welcome_channels?: { + channel_id: string; + description: string; + emoji_id?: string; + emoji_name: string; + }[]; + enabled?: boolean; + description?: string; +} + +router.get("/", route({}), async (req: Request, res: Response) => { const guild_id = req.params.guild_id; const guild = await Guild.findOneOrFail({ id: guild_id }); - await Member.IsInGuildOrFail(req.user_id, guild_id); res.json(guild.welcome_screen); }); -router.patch("/", check(GuildUpdateWelcomeScreenSchema), async (req: Request, res: Response) => { +router.patch("/", route({ body: "GuildUpdateWelcomeScreenSchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { const guild_id = req.params.guild_id; const body = req.body as GuildUpdateWelcomeScreenSchema; const guild = await Guild.findOneOrFail({ id: guild_id }); - const perms = await getPermission(req.user_id, guild_id); - perms.hasThrow("MANAGE_GUILD"); - 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; diff --git a/api/src/routes/guilds/#guild_id/widget.json.ts b/api/src/routes/guilds/#guild_id/widget.json.ts index 193ed095..c31519fa 100644 --- a/api/src/routes/guilds/#guild_id/widget.json.ts +++ b/api/src/routes/guilds/#guild_id/widget.json.ts @@ -1,7 +1,7 @@ import { Request, Response, Router } from "express"; import { Config, Permissions, Guild, Invite, Channel, Member } from "@fosscord/util"; import { HTTPError } from "lambert-server"; -import { random } from "../../../util/RandomInviteID"; +import { random, route } from "@fosscord/api"; const router: Router = Router(); @@ -14,7 +14,7 @@ const router: Router = Router(); // https://discord.com/developers/docs/resources/guild#get-guild-widget // TODO: Cache the response for a guild for 5 minutes regardless of response -router.get("/", async (req: Request, res: Response) => { +router.get("/", route({}), async (req: Request, res: Response) => { const { guild_id } = req.params; const guild = await Guild.findOneOrFail({ id: guild_id }); diff --git a/api/src/routes/guilds/#guild_id/widget.png.ts b/api/src/routes/guilds/#guild_id/widget.png.ts index 89b31153..4c82b740 100644 --- a/api/src/routes/guilds/#guild_id/widget.png.ts +++ b/api/src/routes/guilds/#guild_id/widget.png.ts @@ -1,6 +1,7 @@ import { Request, Response, Router } from "express"; import { Guild } from "@fosscord/util"; import { HTTPError } from "lambert-server"; +import { route } from "@fosscord/api"; import fs from "fs"; import path from "path"; @@ -10,7 +11,7 @@ const router: Router = Router(); // https://discord.com/developers/docs/resources/guild#get-guild-widget-image // TODO: Cache the response -router.get("/", async (req: Request, res: Response) => { +router.get("/", route({}), async (req: Request, res: Response) => { const { guild_id } = req.params; const guild = await Guild.findOneOrFail({ id: guild_id }); diff --git a/api/src/routes/guilds/#guild_id/widget.ts b/api/src/routes/guilds/#guild_id/widget.ts index fcf71402..c8caae14 100644 --- a/api/src/routes/guilds/#guild_id/widget.ts +++ b/api/src/routes/guilds/#guild_id/widget.ts @@ -1,31 +1,29 @@ import { Request, Response, Router } from "express"; import { getPermission, Guild } from "@fosscord/util"; import { HTTPError } from "lambert-server"; -import { check } from "../../../util/instanceOf"; -import { WidgetModifySchema } from "../../../schema/Widget"; +import { check, route } from "@fosscord/api"; + +export interface WidgetModifySchema { + enabled: boolean; // whether the widget is enabled + channel_id: string; // the widget channel id +} const router: Router = Router(); // https://discord.com/developers/docs/resources/guild#get-guild-widget-settings -router.get("/", async (req: Request, res: Response) => { +router.get("/", route({}), async (req: Request, res: Response) => { const { guild_id } = req.params; - const perms = await getPermission(req.user_id, guild_id); - perms.hasThrow("MANAGE_GUILD"); - const guild = await Guild.findOneOrFail({ id: guild_id }); 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("/", check(WidgetModifySchema), async (req: Request, res: Response) => { +router.patch("/", route({ body: "WidgetModifySchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { const body = req.body as WidgetModifySchema; const { guild_id } = req.params; - const perms = await getPermission(req.user_id, guild_id); - perms.hasThrow("MANAGE_GUILD"); - 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 diff --git a/api/src/routes/guilds/index.ts b/api/src/routes/guilds/index.ts index e5830647..ba951f96 100644 --- a/api/src/routes/guilds/index.ts +++ b/api/src/routes/guilds/index.ts @@ -1,15 +1,28 @@ import { Router, Request, Response } from "express"; import { Role, Guild, Snowflake, Config, User, Member, Channel } from "@fosscord/util"; import { HTTPError } from "lambert-server"; -import { check } from "./../../util/instanceOf"; -import { GuildCreateSchema } from "../../schema/Guild"; +import { check, route } from "@fosscord/api"; import { DiscordApiErrors } from "@fosscord/util"; +import { ChannelModifySchema } from "../channels/#channel_id"; const router: Router = Router(); +export interface GuildCreateSchema { + /** + * @maxLength 100 + */ + name: string; + region?: string; + icon?: string; + channels?: ChannelModifySchema[]; + guild_template_code?: string; + system_channel_id?: string; + rules_channel_id?: string; +} + //TODO: create default channel -router.post("/", check(GuildCreateSchema), async (req: Request, res: Response) => { +router.post("/", route({ body: "GuildCreateSchema" }), async (req: Request, res: Response) => { const body = req.body as GuildCreateSchema; const { maxGuilds } = Config.get().limits.user; diff --git a/api/src/routes/guilds/templates/index.ts b/api/src/routes/guilds/templates/index.ts index 58201f65..d7a42044 100644 --- a/api/src/routes/guilds/templates/index.ts +++ b/api/src/routes/guilds/templates/index.ts @@ -2,11 +2,15 @@ import { Request, Response, Router } from "express"; const router: Router = Router(); import { Template, Guild, Role, Snowflake, Config, User, Member } from "@fosscord/util"; import { HTTPError } from "lambert-server"; -import { GuildTemplateCreateSchema } from "../../../schema/Guild"; -import { check } from "../../../util/instanceOf"; +import { check, route } from "@fosscord/api"; import { DiscordApiErrors } from "@fosscord/util"; -router.get("/:code", async (req: Request, res: Response) => { +export interface GuildTemplateCreateSchema { + name: string; + avatar?: string; +} + +router.get("/:code", route({}), async (req: Request, res: Response) => { const { code } = req.params; const template = await Template.findOneOrFail({ code: code }); @@ -14,7 +18,7 @@ router.get("/:code", async (req: Request, res: Response) => { res.json(template); }); -router.post("/:code", check(GuildTemplateCreateSchema), async (req: Request, res: Response) => { +router.post("/:code", route({ body: "GuildTemplateCreateSchema" }), async (req: Request, res: Response) => { const { code } = req.params; const body = req.body as GuildTemplateCreateSchema; diff --git a/api/src/routes/invites/index.ts b/api/src/routes/invites/index.ts index b8c24c1f..6e77a853 100644 --- a/api/src/routes/invites/index.ts +++ b/api/src/routes/invites/index.ts @@ -1,9 +1,10 @@ import { Router, Request, Response } from "express"; import { getPermission, Guild, Invite, Member, PublicInviteRelation } from "@fosscord/util"; +import { route } from "@fosscord/api"; import { HTTPError } from "lambert-server"; const router: Router = Router(); -router.get("/:code", async (req: Request, res: Response) => { +router.get("/:code", route({}), async (req: Request, res: Response) => { const { code } = req.params; const invite = await Invite.findOneOrFail({ where: { code }, relations: PublicInviteRelation }); @@ -11,7 +12,7 @@ router.get("/:code", async (req: Request, res: Response) => { res.status(200).send(invite); }); -router.post("/:code", async (req: Request, res: Response) => { +router.post("/:code", route({}), async (req: Request, res: Response) => { const { code } = req.params; const invite = await Invite.findOneOrFail({ code }); @@ -23,7 +24,8 @@ router.post("/:code", async (req: Request, res: Response) => { res.status(200).send(invite); }); -router.delete("/:code", async (req: Request, res: Response) => { +// * cant use permission of route() function because path doesn't have guild_id/channel_id +router.delete("/:code", route({}), async (req: Request, res: Response) => { const { code } = req.params; const invite = await Invite.findOneOrFail({ code }); const { guild_id, channel_id } = invite; diff --git a/api/src/routes/ping.ts b/api/src/routes/ping.ts index 38daf81e..5cdea705 100644 --- a/api/src/routes/ping.ts +++ b/api/src/routes/ping.ts @@ -1,8 +1,9 @@ import { Router, Response, Request } from "express"; +import { route } from "@fosscord/api"; const router = Router(); -router.get("/", (req: Request, res: Response) => { +router.get("/", route({}), (req: Request, res: Response) => { res.send("pong"); }); diff --git a/api/src/routes/science.ts b/api/src/routes/science.ts index b16ef783..8556a3ad 100644 --- a/api/src/routes/science.ts +++ b/api/src/routes/science.ts @@ -1,8 +1,9 @@ import { Router, Response, Request } from "express"; +import { route } from "@fosscord/api"; const router = Router(); -router.post("/", (req: Request, res: Response) => { +router.post("/", route({}), (req: Request, res: Response) => { // TODO: res.sendStatus(204); }); diff --git a/api/src/routes/users/#id/index.ts b/api/src/routes/users/#id/index.ts index 3841756b..bdb1060f 100644 --- a/api/src/routes/users/#id/index.ts +++ b/api/src/routes/users/#id/index.ts @@ -1,9 +1,10 @@ import { Router, Request, Response } from "express"; -import { User } from "../../../../../util/dist"; +import { User } from "@fosscord/util"; +import { route } from "@fosscord/api"; const router: Router = Router(); -router.get("/", async (req: Request, res: Response) => { +router.get("/", route({}), async (req: Request, res: Response) => { const { id } = req.params; res.json(await User.getPublicUser(id)); diff --git a/api/src/routes/users/#id/profile.ts b/api/src/routes/users/#id/profile.ts index 8be03b47..d60c4f86 100644 --- a/api/src/routes/users/#id/profile.ts +++ b/api/src/routes/users/#id/profile.ts @@ -1,9 +1,17 @@ import { Router, Request, Response } from "express"; -import { PublicConnectedAccount, PublicUser, User, UserPublic } from "../../../../../util/dist"; +import { PublicConnectedAccount, PublicUser, User, UserPublic } from "@fosscord/util"; +import { route } from "@fosscord/api"; const router: Router = Router(); -router.get("/", async (req: Request, res: Response) => { +export interface UserProfileResponse { + user: UserPublic; + connected_accounts: PublicConnectedAccount; + premium_guild_since?: Date; + premium_since?: Date; +} + +router.get("/", route({ response: { body: "UserProfileResponse" } }), async (req: Request, res: Response) => { if (req.params.id === "@me") req.params.id = req.user_id; const user = await User.getPublicUser(req.params.id, { relations: ["connected_accounts"] }); @@ -25,11 +33,4 @@ router.get("/", async (req: Request, res: Response) => { }); }); -export interface UserProfileResponse { - user: UserPublic; - connected_accounts: PublicConnectedAccount; - premium_guild_since?: Date; - premium_since?: Date; -} - export default router; diff --git a/api/src/routes/users/@me/affinities/guilds.ts b/api/src/routes/users/@me/affinities/guilds.ts index fa6be0e7..8d744744 100644 --- a/api/src/routes/users/@me/affinities/guilds.ts +++ b/api/src/routes/users/@me/affinities/guilds.ts @@ -1,8 +1,9 @@ import { Router, Response, Request } from "express"; +import { route } from "@fosscord/api"; const router = Router(); -router.get("/", (req: Request, res: Response) => { +router.get("/", route({}), (req: Request, res: Response) => { // TODO: res.status(200).send({ guild_affinities: [] }); }); diff --git a/api/src/routes/users/@me/affinities/users.ts b/api/src/routes/users/@me/affinities/users.ts index db67ab71..6d4e4991 100644 --- a/api/src/routes/users/@me/affinities/users.ts +++ b/api/src/routes/users/@me/affinities/users.ts @@ -1,9 +1,10 @@ import { Router, Response, Request } from "express"; +import { route } from "@fosscord/api"; const router = Router(); -router.get("/", (req: Request, res: Response) => { - //TODO +router.get("/", route({}), (req: Request, res: Response) => { + // TODO: res.status(200).send({ user_affinities: [], inverse_user_affinities: [] }); }); diff --git a/api/src/routes/users/@me/channels.ts b/api/src/routes/users/@me/channels.ts index 6fd396b8..5515a217 100644 --- a/api/src/routes/users/@me/channels.ts +++ b/api/src/routes/users/@me/channels.ts @@ -1,21 +1,23 @@ import { Router, Request, Response } from "express"; -import { Channel, ChannelCreateEvent, ChannelType, Snowflake, trimSpecial, User, emitEvent } from "@fosscord/util"; +import { Channel, ChannelCreateEvent, ChannelType, Snowflake, trimSpecial, User, emitEvent, Recipient } from "@fosscord/util"; import { HTTPError } from "lambert-server"; - -import { DmChannelCreateSchema } from "../../../schema/Channel"; -import { check } from "../../../util/instanceOf"; +import { route } from "@fosscord/api"; import { In } from "typeorm"; -import { Recipient } from "../../../../../util/dist/entities/Recipient"; + +export interface DmChannelCreateSchema { + name?: string; + recipients: string[]; +} const router: Router = Router(); -router.get("/", async (req: Request, res: Response) => { +router.get("/", route({}), async (req: Request, res: Response) => { const recipients = await Recipient.find({ where: { user_id: req.user_id }, relations: ["channel"] }); res.json(recipients.map((x) => x.channel)); }); -router.post("/", check(DmChannelCreateSchema), async (req: Request, res: Response) => { +router.post("/", route({ body: "DmChannelCreateSchema" }), async (req: Request, res: Response) => { const body = req.body as DmChannelCreateSchema; body.recipients = body.recipients.filter((x) => x !== req.user_id).unique(); diff --git a/api/src/routes/users/@me/delete.ts b/api/src/routes/users/@me/delete.ts index e5fda948..39ceefd9 100644 --- a/api/src/routes/users/@me/delete.ts +++ b/api/src/routes/users/@me/delete.ts @@ -1,10 +1,12 @@ import { Router, Request, Response } from "express"; import { Guild, Member, User } from "@fosscord/util"; +import { route } from "@fosscord/api"; import bcrypt from "bcrypt"; import { HTTPError } from "lambert-server"; + const router = Router(); -router.post("/", async (req: Request, res: Response) => { +router.post("/", route({}), async (req: Request, res: Response) => { const user = await User.findOneOrFail({ id: req.user_id }); //User object let correctpass = true; diff --git a/api/src/routes/users/@me/devices.ts b/api/src/routes/users/@me/devices.ts index b16ef783..8556a3ad 100644 --- a/api/src/routes/users/@me/devices.ts +++ b/api/src/routes/users/@me/devices.ts @@ -1,8 +1,9 @@ import { Router, Response, Request } from "express"; +import { route } from "@fosscord/api"; const router = Router(); -router.post("/", (req: Request, res: Response) => { +router.post("/", route({}), (req: Request, res: Response) => { // TODO: res.sendStatus(204); }); diff --git a/api/src/routes/users/@me/disable.ts b/api/src/routes/users/@me/disable.ts index 7b8a130c..259ced96 100644 --- a/api/src/routes/users/@me/disable.ts +++ b/api/src/routes/users/@me/disable.ts @@ -1,10 +1,11 @@ import { User } from "@fosscord/util"; import { Router, Response, Request } from "express"; +import { route } from "@fosscord/api"; import bcrypt from "bcrypt"; const router = Router(); -router.post("/", async (req: Request, res: Response) => { +router.post("/", route({}), async (req: Request, res: Response) => { const user = await User.findOneOrFail({ id: req.user_id }); //User object let correctpass = true; diff --git a/api/src/routes/users/@me/guilds.ts b/api/src/routes/users/@me/guilds.ts index fb88281b..4ba03cec 100644 --- a/api/src/routes/users/@me/guilds.ts +++ b/api/src/routes/users/@me/guilds.ts @@ -1,18 +1,18 @@ import { Router, Request, Response } from "express"; import { Guild, Member, User, GuildDeleteEvent, GuildMemberRemoveEvent, emitEvent } from "@fosscord/util"; import { HTTPError } from "lambert-server"; -import { In } from "typeorm"; +import { route } from "@fosscord/api"; const router: Router = Router(); -router.get("/", async (req: Request, res: Response) => { +router.get("/", route({}), async (req: Request, res: Response) => { const members = await Member.find({ relations: ["guild"], where: { id: req.user_id } }); res.json(members.map((x) => x.guild)); }); // user send to leave a certain guild -router.delete("/:id", async (req: Request, res: Response) => { +router.delete("/:id", route({}), async (req: Request, res: Response) => { const guild_id = req.params.id; const guild = await Guild.findOneOrFail({ where: { id: guild_id }, select: ["owner_id"] }); diff --git a/api/src/routes/users/@me/index.ts b/api/src/routes/users/@me/index.ts index 68649215..68723374 100644 --- a/api/src/routes/users/@me/index.ts +++ b/api/src/routes/users/@me/index.ts @@ -1,16 +1,33 @@ import { Router, Request, Response } from "express"; import { User, PrivateUserProjection } from "@fosscord/util"; -import { UserModifySchema } from "../../../schema/User"; -import { check } from "../../../util/instanceOf"; -import { handleFile } from "../../../util/cdn"; +import { check, route } from "@fosscord/api"; +import { handleFile } from "@fosscord/api"; const router: Router = Router(); +export interface UserModifySchema { + /** + * @minLength 1 + * @maxLength 100 + */ + username?: string; + avatar?: string | null; + /** + * @maxLength 1024 + */ + bio?: string; + accent_color?: number | null; + banner?: string | null; + password?: string; + new_password?: string; + code?: string; +} + router.get("/", async (req: Request, res: Response) => { - res.json(await User.getPublicUser(req.user_id, { select: PrivateUserProjection })); + res.json(await User.findOne({ select: PrivateUserProjection, where: { id: req.user_id } })); }); -router.patch("/", check(UserModifySchema), async (req: Request, res: Response) => { +router.patch("/", route({ body: "UserModifySchema" }), async (req: Request, res: Response) => { const body = req.body as UserModifySchema; if (body.avatar) body.avatar = await handleFile(`/avatars/${req.user_id}`, body.avatar as string); diff --git a/api/src/routes/users/@me/library.ts b/api/src/routes/users/@me/library.ts index d771cb5e..7ac13bae 100644 --- a/api/src/routes/users/@me/library.ts +++ b/api/src/routes/users/@me/library.ts @@ -1,8 +1,9 @@ import { Router, Response, Request } from "express"; +import { route } from "@fosscord/api"; const router = Router(); -router.get("/", (req: Request, res: Response) => { +router.get("/", route({}), (req: Request, res: Response) => { // TODO: res.status(200).send([]); }); diff --git a/api/src/routes/users/@me/relationships.ts b/api/src/routes/users/@me/relationships.ts index 995b0244..cc264f3f 100644 --- a/api/src/routes/users/@me/relationships.ts +++ b/api/src/routes/users/@me/relationships.ts @@ -11,19 +11,95 @@ import { import { Router, Response, Request } from "express"; import { HTTPError } from "lambert-server"; import { DiscordApiErrors } from "@fosscord/util"; - -import { check, Length } from "../../../util/instanceOf"; +import { route } from "@fosscord/api"; const router = Router(); const userProjection: (keyof User)[] = ["relationships", ...PublicUserProjection]; -router.get("/", async (req: Request, res: Response) => { +router.get("/", route({}), async (req: Request, res: Response) => { const user = await User.findOneOrFail({ where: { id: req.user_id }, relations: ["relationships"] }); return res.json(user.relationships); }); +export interface RelationshipPutSchema { + type: RelationshipType; +} + +router.put("/:id", route({ body: "RelationshipPutSchema" }), async (req: Request, res: Response) => { + return await updateRelationship( + req, + res, + await User.findOneOrFail({ id: req.params.id }, { relations: ["relationships"], select: userProjection }), + req.body.type + ); +}); + +export interface RelationshipPostSchema { + discriminator: string; + username: string; +} + +router.post("/", route({ body: "RelationshipPostSchema" }), async (req: Request, res: Response) => { + return await updateRelationship( + req, + res, + await User.findOneOrFail({ + relations: ["relationships"], + select: userProjection, + where: req.body as { discriminator: string; username: string } + }), + 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"); + + const user = await User.findOneOrFail({ id: req.user_id }, { select: userProjection, relations: ["relationships"] }); + const friend = await User.findOneOrFail({ id: id }, { select: userProjection, relations: ["relationships"] }); + + const relationship = user.relationships.find((x) => x.id === id); + const friendRequest = friend.relationships.find((x) => x.id === req.user_id); + + if (relationship?.type === RelationshipType.blocked) { + // unblock user + user.relationships.remove(relationship); + + await Promise.all([ + user.save(), + emitEvent({ event: "RELATIONSHIP_REMOVE", user_id: req.user_id, data: relationship } as RelationshipRemoveEvent) + ]); + return res.sendStatus(204); + } + if (!relationship || !friendRequest) throw new HTTPError("You are not friends with the user", 404); + if (friendRequest.type === RelationshipType.blocked) throw new HTTPError("The user blocked you"); + + user.relationships.remove(relationship); + friend.relationships.remove(friendRequest); + + await Promise.all([ + user.save(), + friend.save(), + emitEvent({ + event: "RELATIONSHIP_REMOVE", + data: relationship, + user_id: req.user_id + } as RelationshipRemoveEvent), + emitEvent({ + event: "RELATIONSHIP_REMOVE", + data: friendRequest, + user_id: id + } as RelationshipRemoveEvent) + ]); + + return res.sendStatus(204); +}); + +export default router; + 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"); @@ -114,71 +190,3 @@ async function updateRelationship(req: Request, res: Response, friend: User, typ return res.sendStatus(204); } - -router.put("/:id", check({ $type: new Length(Number, 1, 4) }), async (req: Request, res: Response) => { - return await updateRelationship( - req, - res, - await User.findOneOrFail({ id: req.params.id }, { relations: ["relationships"], select: userProjection }), - req.body.type - ); -}); - -router.post("/", check({ discriminator: String, username: String }), async (req: Request, res: Response) => { - return await updateRelationship( - req, - res, - await User.findOneOrFail({ - relations: ["relationships"], - select: userProjection, - where: req.body as { discriminator: string; username: string } - }), - req.body.type - ); -}); - -router.delete("/:id", 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"); - - const user = await User.findOneOrFail({ id: req.user_id }, { select: userProjection, relations: ["relationships"] }); - const friend = await User.findOneOrFail({ id: id }, { select: userProjection, relations: ["relationships"] }); - - const relationship = user.relationships.find((x) => x.id === id); - const friendRequest = friend.relationships.find((x) => x.id === req.user_id); - - if (relationship?.type === RelationshipType.blocked) { - // unblock user - user.relationships.remove(relationship); - - await Promise.all([ - user.save(), - emitEvent({ event: "RELATIONSHIP_REMOVE", user_id: req.user_id, data: relationship } as RelationshipRemoveEvent) - ]); - return res.sendStatus(204); - } - if (!relationship || !friendRequest) throw new HTTPError("You are not friends with the user", 404); - if (friendRequest.type === RelationshipType.blocked) throw new HTTPError("The user blocked you"); - - user.relationships.remove(relationship); - friend.relationships.remove(friendRequest); - - await Promise.all([ - user.save(), - friend.save(), - emitEvent({ - event: "RELATIONSHIP_REMOVE", - data: relationship, - user_id: req.user_id - } as RelationshipRemoveEvent), - emitEvent({ - event: "RELATIONSHIP_REMOVE", - data: friendRequest, - user_id: id - } as RelationshipRemoveEvent) - ]); - - return res.sendStatus(204); -}); - -export default router; diff --git a/api/src/routes/users/@me/settings.ts b/api/src/routes/users/@me/settings.ts index 90ee6372..9d7a2545 100644 --- a/api/src/routes/users/@me/settings.ts +++ b/api/src/routes/users/@me/settings.ts @@ -1,11 +1,12 @@ import { Router, Response, Request } from "express"; import { User, UserSettings } from "@fosscord/util"; -import { check } from "../../../util/instanceOf"; -import { UserSettingsSchema } from "../../../schema/User"; +import { route } from "@fosscord/api"; const router = Router(); -router.patch("/", check(UserSettingsSchema), async (req: Request, res: Response) => { +export interface UserSettingsSchema extends UserSettings {} + +router.patch("/", route({ body: "UserSettingsSchema" }), async (req: Request, res: Response) => { const body = req.body as UserSettings; // only users can update user settings diff --git a/api/src/routes/voice/regions.ts b/api/src/routes/voice/regions.ts index 812aa8f6..4de304ee 100644 --- a/api/src/routes/voice/regions.ts +++ b/api/src/routes/voice/regions.ts @@ -1,11 +1,11 @@ import { Router, Request, Response } from "express"; -import {getIpAdress} from "../../util/ipAddress"; -import {getVoiceRegions} from "../../util/Voice"; +import { getIpAdress, route } from "@fosscord/api"; +import { getVoiceRegions } from "@fosscord/api"; const router: Router = Router(); -router.get("/", async (req: Request, res: Response) => { - res.json(await getVoiceRegions(getIpAdress(req), true))//vip true? +router.get("/", route({}), async (req: Request, res: Response) => { + res.json(await getVoiceRegions(getIpAdress(req), true)); //vip true? }); export default router; diff --git a/api/src/schema/Ban.ts b/api/src/schema/Ban.ts deleted file mode 100644 index 947a60ea..00000000 --- a/api/src/schema/Ban.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const BanCreateSchema = { - $delete_message_days: String, - $reason: String, -}; - -export interface BanCreateSchema { - delete_message_days?: string; - reason?: string; -} diff --git a/api/src/schema/Channel.ts b/api/src/schema/Channel.ts deleted file mode 100644 index cfbc7205..00000000 --- a/api/src/schema/Channel.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { ChannelType } from "@fosscord/util"; -import { Length } from "../util/instanceOf"; - -export const ChannelModifySchema = { - name: new Length(String, 2, 100), - type: new Length(Number, 0, 13), - $topic: new Length(String, 0, 1024), - $bitrate: Number, - $user_limit: Number, - $rate_limit_per_user: new Length(Number, 0, 21600), - $position: Number, - $permission_overwrites: [ - { - id: String, - type: new Length(Number, 0, 1), // either 0 (role) or 1 (member) - allow: BigInt, - deny: BigInt - } - ], - $parent_id: String, - $rtc_region: String, - $default_auto_archive_duration: Number, - $id: String, // kept for backwards compatibility does nothing (need for guild create) - $nsfw: Boolean -}; - -export const DmChannelCreateSchema = { - $name: String, - recipients: new Length([String], 1, 10) -}; - -export interface DmChannelCreateSchema { - name?: string; - recipients: string[]; -} - -export interface ChannelModifySchema { - name: string; - type: number; - topic?: string; - bitrate?: number; - user_limit?: number; - rate_limit_per_user?: number; - position?: number; - permission_overwrites?: { - id: string; - type: number; - allow: bigint; - deny: bigint; - }[]; - parent_id?: string; - id?: string; // is not used (only for guild create) - nsfw?: boolean; - rtc_region?: string; - default_auto_archive_duration?: number; -} - -export const ChannelGuildPositionUpdateSchema = [ - { - id: String, - $position: Number - } -]; - -export type ChannelGuildPositionUpdateSchema = { - id: string; - position?: number; -}[]; diff --git a/api/src/schema/Emoji.ts b/api/src/schema/Emoji.ts deleted file mode 100644 index 0406919c..00000000 --- a/api/src/schema/Emoji.ts +++ /dev/null @@ -1,13 +0,0 @@ -// https://discord.com/developers/docs/resources/emoji - -export const EmojiCreateSchema = { - name: String, //name of the emoji - image: String, // image data the 128x128 emoji image uri - $roles: Array //roles allowed to use this emoji -}; - -export interface EmojiCreateSchema { - name: string; // name of the emoji - image: string; // image data the 128x128 emoji image uri - roles?: string[]; //roles allowed to use this emoji -} diff --git a/api/src/schema/Guild.ts b/api/src/schema/Guild.ts deleted file mode 100644 index 29c78ab0..00000000 --- a/api/src/schema/Guild.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { Channel } from "@fosscord/util"; -import { Length } from "../util/instanceOf"; -import { ChannelModifySchema } from "./Channel"; - -export const GuildCreateSchema = { - name: new Length(String, 2, 100), - $region: String, // auto complete voice region of the user - $icon: String, - $channels: [ChannelModifySchema], - $guild_template_code: String, - $system_channel_id: String, - $rules_channel_id: String -}; - -export interface GuildCreateSchema { - name: string; - region?: string; - icon?: string; - channels?: ChannelModifySchema[]; - guild_template_code?: string; - system_channel_id?: string; - rules_channel_id?: string; -} - -export const GuildUpdateSchema = { - ...GuildCreateSchema, - name: undefined, - $name: new Length(String, 2, 100), - $banner: String, - $splash: String, - $description: String, - $features: [String], - $icon: String, - $verification_level: Number, - $default_message_notifications: Number, - $system_channel_flags: Number, - $system_channel_id: String, - $explicit_content_filter: Number, - $public_updates_channel_id: String, - $afk_timeout: Number, - $afk_channel_id: String, - $preferred_locale: String -}; -// @ts-ignore -delete GuildUpdateSchema.$channels; - -export interface GuildUpdateSchema extends Omit<GuildCreateSchema, "channels"> { - banner?: string; - splash?: string; - description?: string; - features?: string[]; - verification_level?: number; - default_message_notifications?: number; - system_channel_flags?: number; - explicit_content_filter?: number; - public_updates_channel_id?: string; - afk_timeout?: number; - afk_channel_id?: string; - preferred_locale?: string; -} - -export const GuildTemplateCreateSchema = { - name: String, - $avatar: String -}; - -export interface GuildTemplateCreateSchema { - name: string; - avatar?: string; -} - -export const GuildUpdateWelcomeScreenSchema = { - $welcome_channels: [ - { - channel_id: String, - description: String, - $emoji_id: String, - emoji_name: String - } - ], - $enabled: Boolean, - $description: new Length(String, 0, 140) -}; - -export interface GuildUpdateWelcomeScreenSchema { - welcome_channels?: { - channel_id: string; - description: string; - emoji_id?: string; - emoji_name: string; - }[]; - enabled?: boolean; - description?: string; -} - -export const VoiceStateUpdateSchema = { - channel_id: String, // Snowflake - $suppress: Boolean, - $request_to_speak_timestamp: String // ISO8601 timestamp -}; - -export interface VoiceStateUpdateSchema { - channel_id: string; // Snowflake - suppress?: boolean; - request_to_speak_timestamp?: string // ISO8601 timestamp -} diff --git a/api/src/schema/Invite.ts b/api/src/schema/Invite.ts deleted file mode 100644 index da6192bc..00000000 --- a/api/src/schema/Invite.ts +++ /dev/null @@ -1,22 +0,0 @@ -export const InviteCreateSchema = { - $target_user_id: String, - $target_type: String, - $validate: String, //? wtf is this - $max_age: Number, - $max_uses: Number, - $temporary: Boolean, - $unique: Boolean, - $target_user: String, - $target_user_type: Number -}; -export interface InviteCreateSchema { - target_user_id?: string; - target_type?: string; - validate?: string; //? wtf is this - max_age?: number; - max_uses?: number; - temporary?: boolean; - unique?: boolean; - target_user?: string; - target_user_type?: number; -} diff --git a/api/src/schema/Member.ts b/api/src/schema/Member.ts deleted file mode 100644 index 607d0a06..00000000 --- a/api/src/schema/Member.ts +++ /dev/null @@ -1,29 +0,0 @@ -export const MemberCreateSchema = { - id: String, - nick: String, - guild_id: String, - joined_at: Date -}; - -export interface MemberCreateSchema { - id: string; - nick: string; - guild_id: string; - joined_at: Date; -} - -export const MemberNickChangeSchema = { - nick: String -}; - -export interface MemberNickChangeSchema { - nick: string; -} - -export const MemberChangeSchema = { - $roles: [String] -}; - -export interface MemberChangeSchema { - roles?: string[]; -} diff --git a/api/src/schema/Message.ts b/api/src/schema/Message.ts deleted file mode 100644 index d39f685a..00000000 --- a/api/src/schema/Message.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { Embed } from "@fosscord/util"; -import { Length } from "../util/instanceOf"; - -export const EmbedImage = { - $url: String, - $width: Number, - $height: Number -}; - -const embed = { - $title: new Length(String, 0, 256), //title of embed - $type: String, // type of embed (always "rich" for webhook embeds) - $description: new Length(String, 0, 2048), // description of embed - $url: String, // url of embed - $timestamp: String, // ISO8601 timestamp - $color: Number, // color code of the embed - $footer: { - text: new Length(String, 0, 2048), - icon_url: String, - proxy_icon_url: String - }, // footer object footer information - $image: EmbedImage, // image object image information - $thumbnail: EmbedImage, // thumbnail object thumbnail information - $video: EmbedImage, // video object video information - $provider: { - name: String, - url: String - }, // provider object provider information - $author: { - name: new Length(String, 0, 256), - url: String, - icon_url: String, - proxy_icon_url: String - }, // author object author information - $fields: new Length( - [ - { - name: new Length(String, 0, 256), - value: new Length(String, 0, 1024), - $inline: Boolean - } - ], - 0, - 25 - ) -}; - -export const MessageCreateSchema = { - $content: new Length(String, 0, 2000), - $nonce: String, - $tts: Boolean, - $flags: String, - $embed: embed, - // TODO: ^ embed is deprecated in favor of embeds (https://discord.com/developers/docs/resources/channel#message-object) - // $embeds: [embed], - $allowed_mentions: { - $parse: [String], - $roles: [String], - $users: [String], - $replied_user: Boolean - }, - $message_reference: { - message_id: String, - channel_id: String, - $guild_id: String, - $fail_if_not_exists: Boolean - }, - $payload_json: String, - $file: Object -}; - -export interface MessageCreateSchema { - content?: string; - nonce?: string; - tts?: boolean; - flags?: string; - embed?: Embed & { timestamp?: string }; - allowed_mentions?: { - parse?: string[]; - roles?: string[]; - users?: string[]; - replied_user?: boolean; - }; - message_reference?: { - message_id: string; - channel_id: string; - guild_id?: string; - fail_if_not_exists?: boolean; - }; - payload_json?: string; - file?: any; -} diff --git a/api/src/schema/Roles.ts b/api/src/schema/Roles.ts deleted file mode 100644 index e1a34ae8..00000000 --- a/api/src/schema/Roles.ts +++ /dev/null @@ -1,29 +0,0 @@ -export const RoleModifySchema = { - $name: String, - $permissions: BigInt, - $color: Number, - $hoist: Boolean, // whether the role should be displayed separately in the sidebar - $mentionable: Boolean, // whether the role should be mentionable - $position: Number -}; - -export interface RoleModifySchema { - name?: string; - permissions?: bigint; - color?: number; - hoist?: boolean; // whether the role should be displayed separately in the sidebar - mentionable?: boolean; // whether the role should be mentionable - position?: number; -} - -export const RolePositionUpdateSchema = [ - { - id: String, - position: Number - } -]; - -export type RolePositionUpdateSchema = { - id: string; - position: number; -}[]; diff --git a/api/src/schema/Template.ts b/api/src/schema/Template.ts deleted file mode 100644 index 88e36c53..00000000 --- a/api/src/schema/Template.ts +++ /dev/null @@ -1,19 +0,0 @@ -export const TemplateCreateSchema = { - name: String, - $description: String, -}; - -export interface TemplateCreateSchema { - name: string; - description?: string; -} - -export const TemplateModifySchema = { - name: String, - $description: String, -}; - -export interface TemplateModifySchema { - name: string; - description?: string; -} diff --git a/api/src/schema/User.ts b/api/src/schema/User.ts deleted file mode 100644 index 0d094b9e..00000000 --- a/api/src/schema/User.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { UserSettings } from "../../../util/dist"; -import { Length } from "../util/instanceOf"; - -export const UserModifySchema = { - $username: new Length(String, 2, 32), - $avatar: String, - $bio: new Length(String, 0, 190), - $accent_color: Number, - $banner: String, - $password: String, - $new_password: String, - $code: String // 2fa code -}; - -export interface UserModifySchema { - username?: string; - avatar?: string | null; - bio?: string; - accent_color?: number | null; - banner?: string | null; - password?: string; - new_password?: string; - code?: string; -} - -export const UserSettingsSchema = { - $afk_timeout: Number, - $allow_accessibility_detection: Boolean, - $animate_emoji: Boolean, - $animate_stickers: Number, - $contact_sync_enabled: Boolean, - $convert_emoticons: Boolean, - $custom_status: { - $emoji_id: String, - $emoji_name: String, - $expires_at: Number, - $text: String - }, - $default_guilds_restricted: Boolean, - $detect_platform_accounts: Boolean, - $developer_mode: Boolean, - $disable_games_tab: Boolean, - $enable_tts_command: Boolean, - $explicit_content_filter: Number, - $friend_source_flags: { - all: Boolean - }, - $gateway_connected: Boolean, - $gif_auto_play: Boolean, - $guild_folders: [ - { - color: Number, - guild_ids: [String], - id: Number, - name: String - } - ], - $guild_positions: [String], - $inline_attachment_media: Boolean, - $inline_embed_media: Boolean, - $locale: String, - $message_display_compact: Boolean, - $native_phone_integration_enabled: Boolean, - $render_embeds: Boolean, - $render_reactions: Boolean, - $restricted_guilds: [String], - $show_current_game: Boolean, - $status: String, - $stream_notifications_enabled: Boolean, - $theme: String, - $timezone_offset: Number -}; - -export interface UserSettingsSchema extends UserSettings {} diff --git a/api/src/schema/Widget.ts b/api/src/schema/Widget.ts deleted file mode 100644 index d37a38de..00000000 --- a/api/src/schema/Widget.ts +++ /dev/null @@ -1,10 +0,0 @@ -// https://discord.com/developers/docs/resources/guild#guild-widget-object -export const WidgetModifySchema = { - enabled: Boolean, // whether the widget is enabled - channel_id: String // the widget channel id -}; - -export interface WidgetModifySchema { - enabled: boolean; // whether the widget is enabled - channel_id: string; // the widget channel id -} diff --git a/api/src/schema/index.ts b/api/src/schema/index.ts deleted file mode 100644 index b5f38a2f..00000000 --- a/api/src/schema/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export * from "./Ban"; -export * from "./Channel"; -export * from "./Emoji"; -export * from "./Guild"; -export * from "./Invite"; -export * from "./Member"; -export * from "./Message"; -export * from "./Roles"; -export * from "./Template"; -export * from "./User"; -export * from "./Widget"; diff --git a/api/src/test/password_test.ts b/api/src/test/password_test.ts index 59d36621..983b18ae 100644 --- a/api/src/test/password_test.ts +++ b/api/src/test/password_test.ts @@ -1,12 +1,12 @@ -import { check } from "./../util/passwordStrength"; +import { checkPassword } from "@fosscord/api"; -console.log(check("123456789012345")); +console.log(checkPassword("123456789012345")); // -> 0.25 -console.log(check("ABCDEFGHIJKLMOPQ")); +console.log(checkPassword("ABCDEFGHIJKLMOPQ")); // -> 0.25 -console.log(check("ABC123___...123")); +console.log(checkPassword("ABC123___...123")); // -> -console.log(check("")); +console.log(checkPassword("")); // -> -// console.log(check("")); +// console.log(checkPassword("")); // // -> diff --git a/api/src/util/Message.ts b/api/src/util/Message.ts index fea553bc..f8230124 100644 --- a/api/src/util/Message.ts +++ b/api/src/util/Message.ts @@ -22,7 +22,7 @@ import { import { HTTPError } from "lambert-server"; import fetch from "node-fetch"; import cheerio from "cheerio"; -import { MessageCreateSchema } from "../schema/Message"; +import { MessageCreateSchema } from "../routes/channels/#channel_id/messages"; // TODO: check webhook, application, system author diff --git a/api/src/util/Voice.ts b/api/src/util/Voice.ts index 087bdfa8..f06b1aaa 100644 --- a/api/src/util/Voice.ts +++ b/api/src/util/Voice.ts @@ -1,32 +1,32 @@ -import {Config} from "@fosscord/util"; -import {distanceBetweenLocations, IPAnalysis} from "./ipAddress"; +import { Config } from "@fosscord/util"; +import { distanceBetweenLocations, IPAnalysis } from "./ipAddress"; export async function getVoiceRegions(ipAddress: string, vip: boolean) { - const regions = Config.get().regions; - const availableRegions = regions.available.filter(ar => vip ? true : !ar.vip); - let optimalId = regions.default + const regions = Config.get().regions; + const availableRegions = regions.available.filter((ar) => (vip ? true : !ar.vip)); + let optimalId = regions.default; - if(!regions.useDefaultAsOptimal) { - const clientIpAnalysis = await IPAnalysis(ipAddress) + if (!regions.useDefaultAsOptimal) { + const clientIpAnalysis = await IPAnalysis(ipAddress); - let min = Number.POSITIVE_INFINITY + let min = Number.POSITIVE_INFINITY; - for (let ar of availableRegions) { - //TODO the endpoint location should be saved in the database if not already present to prevent IPAnalysis call - const dist = distanceBetweenLocations(clientIpAnalysis, ar.location || (await IPAnalysis(ar.endpoint))) + for (let ar of availableRegions) { + //TODO the endpoint location should be saved in the database if not already present to prevent IPAnalysis call + const dist = distanceBetweenLocations(clientIpAnalysis, ar.location || (await IPAnalysis(ar.endpoint))); - if(dist < min) { - min = dist - optimalId = ar.id - } - } - } + if (dist < min) { + min = dist; + optimalId = ar.id; + } + } + } - return availableRegions.map(ar => ({ - id: ar.id, - name: ar.name, - custom: ar.custom, - deprecated: ar.deprecated, - optimal: ar.id === optimalId - })) -} \ No newline at end of file + return availableRegions.map((ar) => ({ + id: ar.id, + name: ar.name, + custom: ar.custom, + deprecated: ar.deprecated, + optimal: ar.id === optimalId + })); +} diff --git a/api/src/util/VoiceState.ts b/api/src/util/VoiceState.ts deleted file mode 100644 index 07022ec9..00000000 --- a/api/src/util/VoiceState.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Channel, ChannelType, DiscordApiErrors, emitEvent, getPermission, VoiceState, VoiceStateUpdateEvent } from "@fosscord/util"; -import { VoiceStateUpdateSchema } from "../schema"; - - -//TODO need more testing when community guild and voice stage channel are working -export async function updateVoiceState(vsuSchema: VoiceStateUpdateSchema, guildId: string, userId: string, targetUserId?: string) { - const perms = await getPermission(userId, guildId, vsuSchema.channel_id); - - /* - From https://discord.com/developers/docs/resources/guild#modify-current-user-voice-state - You must have the MUTE_MEMBERS permission to unsuppress yourself. 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 (targetUserId !== undefined || (vsuSchema.suppress !== undefined && !vsuSchema.suppress)) { - perms.hasThrow("MUTE_MEMBERS"); - } - if (vsuSchema.request_to_speak_timestamp !== undefined && vsuSchema.request_to_speak_timestamp !== "") { - perms.hasThrow("REQUEST_TO_SPEAK") - } - - if (!targetUserId) { - targetUserId = userId; - } else { - if (vsuSchema.suppress !== undefined && vsuSchema.suppress) - vsuSchema.request_to_speak_timestamp = "" //Need to check if empty string is the right value - } - - //TODO assumed that empty string means clean, need to test if it's right - let voiceState - try { - voiceState = await VoiceState.findOneOrFail({ - guild_id: guildId, - channel_id: vsuSchema.channel_id, - user_id: targetUserId - }); - } catch (error) { - throw DiscordApiErrors.UNKNOWN_VOICE_STATE; - } - - voiceState.assign(vsuSchema); - const channel = await Channel.findOneOrFail({ guild_id: guildId, id: vsuSchema.channel_id }) - if (channel.type !== ChannelType.GUILD_STAGE_VOICE) { - throw DiscordApiErrors.CANNOT_EXECUTE_ON_THIS_CHANNEL_TYPE; - } - - await Promise.all([ - voiceState.save(), - emitEvent({ - event: "VOICE_STATE_UPDATE", - data: voiceState, - guild_id: guildId - } as VoiceStateUpdateEvent)]); - return; -} \ No newline at end of file diff --git a/api/src/util/index.ts b/api/src/util/index.ts new file mode 100644 index 00000000..c98784a4 --- /dev/null +++ b/api/src/util/index.ts @@ -0,0 +1,10 @@ +export * from "./Base64"; +export * from "./cdn"; +export * from "./instanceOf"; +export * from "./ipAddress"; +export * from "./Message"; +export * from "./passwordStrength"; +export * from "./RandomInviteID"; +export * from "./route"; +export * from "./String"; +export * from "./Voice"; diff --git a/api/src/util/passwordStrength.ts b/api/src/util/passwordStrength.ts index dfffa2c0..047df008 100644 --- a/api/src/util/passwordStrength.ts +++ b/api/src/util/passwordStrength.ts @@ -16,7 +16,7 @@ const blocklist: string[] = []; // TODO: update ones passwordblocklist is stored * * Returns: 0 > pw > 1 */ -export function check(password: string): number { +export function checkPassword(password: string): number { const { minLength, minNumbers, minUpperCase, minSymbols } = Config.get().register.password; var strength = 0; diff --git a/api/src/util/route.ts b/api/src/util/route.ts new file mode 100644 index 00000000..f618a630 --- /dev/null +++ b/api/src/util/route.ts @@ -0,0 +1,76 @@ +import { DiscordApiErrors, Event, EventData, getPermission, PermissionResolvable, Permissions } from "@fosscord/util"; +import { NextFunction, Request, Response } from "express"; +import fs from "fs"; +import path from "path"; +import Ajv from "ajv"; +import { AnyValidateFunction } from "ajv/dist/core"; +import { FieldErrors } from ".."; +import addFormats from "ajv-formats"; + +const SchemaPath = path.join(__dirname, "..", "..", "assets", "schemas.json"); +const schemas = JSON.parse(fs.readFileSync(SchemaPath, { encoding: "utf8" })); +export const ajv = new Ajv({ + allErrors: true, + parseDate: true, + allowDate: true, + schemas, + messages: true, + strict: true, + strictRequired: true +}); +addFormats(ajv); + +declare global { + namespace Express { + interface Request { + permission?: Permissions; + } + } +} + +export type RouteSchema = string; // typescript interface name +export type RouteResponse = { status?: number; body?: RouteSchema; headers?: Record<string, string> }; + +export interface RouteOptions { + permission?: PermissionResolvable; + body?: RouteSchema; + response?: RouteResponse; + example?: { + body?: any; + path?: string; + event?: EventData; + headers?: Record<string, string>; + }; +} + +export function route(opts: RouteOptions) { + var validate: AnyValidateFunction<any>; + if (opts.body) { + // @ts-ignore + validate = ajv.getSchema(opts.body); + if (!validate) throw new Error(`Body schema ${opts.body} not found`); + } + + return async (req: Request, res: Response, next: NextFunction) => { + if (opts.permission) { + const required = new Permissions(opts.permission); + const permission = await getPermission(req.user_id, req.params.guild_id, req.params.channel_id); + console.log(required.bitfield, permission.bitfield, permission.bitfield & required.bitfield); + + // bitfield comparison: check if user lacks certain permission + if (!permission.has(required)) { + throw DiscordApiErrors.MISSING_PERMISSIONS.withParams(opts.permission as string); + } + + if (validate) { + const valid = validate(req.body); + if (!valid) { + const fields: Record<string, { code?: string; message: string }> = {}; + validate.errors?.forEach((x) => (fields[x.instancePath] = { code: x.keyword, message: x.message || "" })); + throw FieldErrors(fields); + } + } + } + next(); + }; +} |