diff options
Diffstat (limited to 'src/api')
111 files changed, 4052 insertions, 2062 deletions
diff --git a/src/api/middlewares/Authentication.ts b/src/api/middlewares/Authentication.ts index d0e4d8a0..9e41b453 100644 --- a/src/api/middlewares/Authentication.ts +++ b/src/api/middlewares/Authentication.ts @@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { checkToken, Config, Rights } from "@spacebar/util"; +import { checkToken, Rights } from "@spacebar/util"; import * as Sentry from "@sentry/node"; import { NextFunction, Request, Response } from "express"; import { HTTPError } from "lambert-server"; @@ -92,12 +92,7 @@ export async function Authentication( Sentry.setUser({ id: req.user_id }); try { - const { jwtSecret } = Config.get().security; - - const { decoded, user } = await checkToken( - req.headers.authorization, - jwtSecret, - ); + const { decoded, user } = await checkToken(req.headers.authorization); req.token = decoded; req.user_id = decoded.id; diff --git a/src/api/middlewares/RateLimit.ts b/src/api/middlewares/RateLimit.ts index 0da292e9..f5bfbb4f 100644 --- a/src/api/middlewares/RateLimit.ts +++ b/src/api/middlewares/RateLimit.ts @@ -83,6 +83,13 @@ export default function rateLimit(opts: { const offender = Cache.get(executor_id + bucket_id); + res.set("X-RateLimit-Limit", `${max_hits}`) + .set("X-RateLimit-Remaining", `${max_hits - (offender?.hits || 0)}`) + .set("X-RateLimit-Bucket", `${bucket_id}`) + // assuming we aren't blocked, a new window will start after this request + .set("X-RateLimit-Reset", `${Date.now() + opts.window}`) + .set("X-RateLimit-Reset-After", `${opts.window}`); + if (offender) { let reset = offender.expires_at.getTime(); let resetAfterMs = reset - Date.now(); @@ -96,6 +103,12 @@ export default function rateLimit(opts: { Cache.delete(executor_id + bucket_id); } + res.set("X-RateLimit-Reset", `${reset}`); + res.set( + "X-RateLimit-Reset-After", + `${Math.max(0, Math.ceil(resetAfterSec))}`, + ); + if (offender.blocked) { const global = bucket_id === "global"; // each block violation pushes the expiry one full window further @@ -109,16 +122,17 @@ export default function rateLimit(opts: { console.log(`blocked bucket: ${bucket_id} ${executor_id}`, { resetAfterMs, }); + + if (global) res.set("X-RateLimit-Global", "true"); + return ( res .status(429) - .set("X-RateLimit-Limit", `${max_hits}`) .set("X-RateLimit-Remaining", "0") - .set("X-RateLimit-Reset", `${reset}`) - .set("X-RateLimit-Reset-After", `${resetAfterSec}`) - .set("X-RateLimit-Global", `${global}`) - .set("Retry-After", `${Math.ceil(resetAfterSec)}`) - .set("X-RateLimit-Bucket", `${bucket_id}`) + .set( + "Retry-After", + `${Math.max(0, Math.ceil(resetAfterSec))}`, + ) // TODO: error rate limit message translation .send({ message: "You are being rate limited.", diff --git a/src/api/middlewares/Translation.ts b/src/api/middlewares/Translation.ts index 60ff4ad7..f3a4c8df 100644 --- a/src/api/middlewares/Translation.ts +++ b/src/api/middlewares/Translation.ts @@ -20,7 +20,7 @@ import fs from "fs"; import path from "path"; import i18next from "i18next"; import i18nextMiddleware from "i18next-http-middleware"; -import i18nextBackend from "i18next-node-fs-backend"; +import i18nextBackend from "i18next-fs-backend"; import { Router } from "express"; const ASSET_FOLDER_PATH = path.join(__dirname, "..", "..", "..", "assets"); diff --git a/src/api/routes/applications/#id/bot/index.ts b/src/api/routes/applications/#id/bot/index.ts index e3f1832c..3c431e3d 100644 --- a/src/api/routes/applications/#id/bot/index.ts +++ b/src/api/routes/applications/#id/bot/index.ts @@ -16,78 +16,99 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Request, Response, Router } from "express"; import { route } from "@spacebar/api"; import { Application, - generateToken, - User, BotModifySchema, - handleFile, DiscordApiErrors, + User, + createAppBotUser, + generateToken, + handleFile, } from "@spacebar/util"; +import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; import { verifyToken } from "node-2fa"; const router: Router = Router(); -router.post("/", route({}), async (req: Request, res: Response) => { - const app = await Application.findOneOrFail({ - where: { id: req.params.id }, - relations: ["owner"], - }); - - if (app.owner.id != req.user_id) - throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION; - - const user = await User.register({ - username: app.name, - password: undefined, - id: app.id, - req, - }); - - user.id = app.id; - user.premium_since = new Date(); - user.bot = true; - - await user.save(); +router.post( + "/", + route({ + responses: { + 204: { + body: "TokenOnlyResponse", + }, + 400: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const app = await Application.findOneOrFail({ + where: { id: req.params.id }, + relations: ["owner"], + }); - // flags is NaN here? - app.assign({ bot: user, flags: app.flags || 0 }); + if (app.owner.id != req.user_id) + throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION; - await app.save(); + const user = await createAppBotUser(app, req); - res.send({ - token: await generateToken(user.id), - }).status(204); -}); + res.send({ + token: await generateToken(user.id), + }).status(204); + }, +); -router.post("/reset", route({}), async (req: Request, res: Response) => { - const bot = await User.findOneOrFail({ where: { id: req.params.id } }); - const owner = await User.findOneOrFail({ where: { id: req.user_id } }); +router.post( + "/reset", + route({ + responses: { + 200: { + body: "TokenResponse", + }, + 400: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const bot = await User.findOneOrFail({ where: { id: req.params.id } }); + const owner = await User.findOneOrFail({ where: { id: req.user_id } }); - if (owner.id != req.user_id) - throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION; + if (owner.id != req.user_id) + throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION; - if ( - owner.totp_secret && - (!req.body.code || verifyToken(owner.totp_secret, req.body.code)) - ) - throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008); + if ( + owner.totp_secret && + (!req.body.code || verifyToken(owner.totp_secret, req.body.code)) + ) + throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008); - bot.data = { hash: undefined, valid_tokens_since: new Date() }; + bot.data = { hash: undefined, valid_tokens_since: new Date() }; - await bot.save(); + await bot.save(); - const token = await generateToken(bot.id); + const token = await generateToken(bot.id); - res.json({ token }).status(200); -}); + res.json({ token }).status(200); + }, +); router.patch( "/", - route({ body: "BotModifySchema" }), + route({ + requestBody: "BotModifySchema", + responses: { + 200: { + body: "Application", + }, + 400: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const body = req.body as BotModifySchema; if (!body.avatar?.trim()) delete body.avatar; diff --git a/src/api/routes/applications/#id/entitlements.ts b/src/api/routes/applications/#id/entitlements.ts index e88fb7f7..6388e6b3 100644 --- a/src/api/routes/applications/#id/entitlements.ts +++ b/src/api/routes/applications/#id/entitlements.ts @@ -16,15 +16,25 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Response, Request } from "express"; import { route } from "@spacebar/api"; +import { Request, Response, Router } from "express"; const router = Router(); -router.get("/", route({}), (req: Request, res: Response) => { - // TODO: - //const { exclude_consumed } = req.query; - res.status(200).send([]); -}); +router.get( + "/", + route({ + responses: { + 200: { + body: "ApplicationEntitlementsResponse", + }, + }, + }), + (req: Request, res: Response) => { + // TODO: + //const { exclude_consumed } = req.query; + res.status(200).send([]); + }, +); export default router; diff --git a/src/api/routes/applications/#id/index.ts b/src/api/routes/applications/#id/index.ts index 067f5dad..c372869a 100644 --- a/src/api/routes/applications/#id/index.ts +++ b/src/api/routes/applications/#id/index.ts @@ -16,32 +16,55 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Request, Response, Router } from "express"; import { route } from "@spacebar/api"; import { Application, - DiscordApiErrors, ApplicationModifySchema, + DiscordApiErrors, } from "@spacebar/util"; -import { verifyToken } from "node-2fa"; +import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; +import { verifyToken } from "node-2fa"; const router: Router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - const app = await Application.findOneOrFail({ - where: { id: req.params.id }, - relations: ["owner", "bot"], - }); - if (app.owner.id != req.user_id) - throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION; +router.get( + "/", + route({ + responses: { + 200: { + body: "Application", + }, + 400: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const app = await Application.findOneOrFail({ + where: { id: req.params.id }, + relations: ["owner", "bot"], + }); + if (app.owner.id != req.user_id) + throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION; - return res.json(app); -}); + return res.json(app); + }, +); router.patch( "/", - route({ body: "ApplicationModifySchema" }), + route({ + requestBody: "ApplicationModifySchema", + responses: { + 200: { + body: "Application", + }, + 400: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const body = req.body as ApplicationModifySchema; @@ -73,23 +96,35 @@ router.patch( }, ); -router.post("/delete", route({}), async (req: Request, res: Response) => { - const app = await Application.findOneOrFail({ - where: { id: req.params.id }, - relations: ["bot", "owner"], - }); - if (app.owner.id != req.user_id) - throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION; +router.post( + "/delete", + route({ + responses: { + 200: {}, + 400: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const app = await Application.findOneOrFail({ + where: { id: req.params.id }, + relations: ["bot", "owner"], + }); + if (app.owner.id != req.user_id) + throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION; - if ( - app.owner.totp_secret && - (!req.body.code || verifyToken(app.owner.totp_secret, req.body.code)) - ) - throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008); + if ( + app.owner.totp_secret && + (!req.body.code || + verifyToken(app.owner.totp_secret, req.body.code)) + ) + throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008); - await Application.delete({ id: app.id }); + await Application.delete({ id: app.id }); - res.send().status(200); -}); + res.send().status(200); + }, +); export default router; diff --git a/src/api/routes/applications/#id/skus.ts b/src/api/routes/applications/#id/skus.ts index fcb75423..dc4fad23 100644 --- a/src/api/routes/applications/#id/skus.ts +++ b/src/api/routes/applications/#id/skus.ts @@ -16,13 +16,23 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Request, Response, Router } from "express"; import { route } from "@spacebar/api"; +import { Request, Response, Router } from "express"; const router: Router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - res.json([]).status(200); -}); +router.get( + "/", + route({ + responses: { + 200: { + body: "ApplicationSkusResponse", + }, + }, + }), + async (req: Request, res: Response) => { + res.json([]).status(200); + }, +); export default router; diff --git a/src/api/routes/applications/detectable.ts b/src/api/routes/applications/detectable.ts index a8e30894..5cf9d171 100644 --- a/src/api/routes/applications/detectable.ts +++ b/src/api/routes/applications/detectable.ts @@ -16,14 +16,24 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Request, Response, Router } from "express"; import { route } from "@spacebar/api"; +import { Request, Response, Router } from "express"; const router: Router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - //TODO - res.send([]).status(200); -}); +router.get( + "/", + route({ + responses: { + 200: { + body: "ApplicationDetectableResponse", + }, + }, + }), + async (req: Request, res: Response) => { + //TODO + res.send([]).status(200); + }, +); export default router; diff --git a/src/api/routes/applications/index.ts b/src/api/routes/applications/index.ts index 80a19aa8..5bba3338 100644 --- a/src/api/routes/applications/index.ts +++ b/src/api/routes/applications/index.ts @@ -16,28 +16,47 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Request, Response, Router } from "express"; import { route } from "@spacebar/api"; import { Application, ApplicationCreateSchema, - trimSpecial, + Config, User, + createAppBotUser, + trimSpecial, } from "@spacebar/util"; +import { Request, Response, Router } from "express"; const router: Router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - const results = await Application.find({ - where: { owner: { id: req.user_id } }, - relations: ["owner", "bot"], - }); - res.json(results).status(200); -}); +router.get( + "/", + route({ + responses: { + 200: { + body: "APIApplicationArray", + }, + }, + }), + async (req: Request, res: Response) => { + const results = await Application.find({ + where: { owner: { id: req.user_id } }, + relations: ["owner", "bot"], + }); + res.json(results).status(200); + }, +); router.post( "/", - route({ body: "ApplicationCreateSchema" }), + route({ + requestBody: "ApplicationCreateSchema", + responses: { + 200: { + body: "Application", + }, + }, + }), async (req: Request, res: Response) => { const body = req.body as ApplicationCreateSchema; const user = await User.findOneOrFail({ where: { id: req.user_id } }); @@ -51,7 +70,11 @@ router.post( flags: 0, }); - await app.save(); + // april 14, 2023: discord made bot users be automatically added to all new apps + const { autoCreateBotUsers } = Config.get().general; + if (autoCreateBotUsers) { + await createAppBotUser(app, req); + } else await app.save(); res.json(app); }, diff --git a/src/api/routes/auth/forgot.ts b/src/api/routes/auth/forgot.ts index e240dff2..6fa86021 100644 --- a/src/api/routes/auth/forgot.ts +++ b/src/api/routes/auth/forgot.ts @@ -30,7 +30,18 @@ const router = Router(); router.post( "/", - route({ body: "ForgotPasswordSchema" }), + route({ + requestBody: "ForgotPasswordSchema", + responses: { + 204: {}, + 400: { + body: "APIErrorOrCaptchaResponse", + }, + 500: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const { login, captcha_key } = req.body as ForgotPasswordSchema; diff --git a/src/api/routes/auth/generate-registration-tokens.ts b/src/api/routes/auth/generate-registration-tokens.ts index 723875f8..80fdaed1 100644 --- a/src/api/routes/auth/generate-registration-tokens.ts +++ b/src/api/routes/auth/generate-registration-tokens.ts @@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { route, random } from "@spacebar/api"; +import { random, route } from "@spacebar/api"; import { Config, ValidRegistrationToken } from "@spacebar/util"; import { Request, Response, Router } from "express"; @@ -25,7 +25,22 @@ export default router; router.get( "/", - route({ right: "OPERATOR" }), + route({ + query: { + count: { + type: "number", + description: + "The number of registration tokens to generate. Defaults to 1.", + }, + length: { + type: "number", + description: + "The length of each registration token. Defaults to 255.", + }, + }, + right: "OPERATOR", + responses: { 200: { body: "GenerateRegistrationTokensResponse" } }, + }), async (req: Request, res: Response) => { const count = req.query.count ? parseInt(req.query.count as string) : 1; const length = req.query.length diff --git a/src/api/routes/auth/location-metadata.ts b/src/api/routes/auth/location-metadata.ts index 52a45c67..28293e59 100644 --- a/src/api/routes/auth/location-metadata.ts +++ b/src/api/routes/auth/location-metadata.ts @@ -16,20 +16,29 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Request, Response } from "express"; -import { route } from "@spacebar/api"; -import { getIpAdress, IPAnalysis } from "@spacebar/api"; +import { IPAnalysis, getIpAdress, route } from "@spacebar/api"; +import { Request, Response, Router } from "express"; const router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - //TODO - //Note: It's most likely related to legal. At the moment Discord hasn't finished this too - const country_code = (await IPAnalysis(getIpAdress(req))).country_code; - res.json({ - consent_required: false, - country_code: country_code, - promotional_email_opt_in: { required: true, pre_checked: false }, - }); -}); +router.get( + "/", + route({ + responses: { + 200: { + body: "LocationMetadataResponse", + }, + }, + }), + async (req: Request, res: Response) => { + //TODO + //Note: It's most likely related to legal. At the moment Discord hasn't finished this too + const country_code = (await IPAnalysis(getIpAdress(req))).country_code; + res.json({ + consent_required: false, + country_code: country_code, + promotional_email_opt_in: { required: true, pre_checked: false }, + }); + }, +); export default router; diff --git a/src/api/routes/auth/login.ts b/src/api/routes/auth/login.ts index fe0b4f99..d3fc1fb4 100644 --- a/src/api/routes/auth/login.ts +++ b/src/api/routes/auth/login.ts @@ -36,7 +36,17 @@ export default router; router.post( "/", - route({ body: "LoginSchema" }), + route({ + requestBody: "LoginSchema", + responses: { + 200: { + body: "LoginResponse", + }, + 400: { + body: "APIErrorOrCaptchaResponse", + }, + }, + }), async (req: Request, res: Response) => { const { login, password, captcha_key, undelete } = req.body as LoginSchema; diff --git a/src/api/routes/auth/logout.ts b/src/api/routes/auth/logout.ts index 51909afa..94a3e474 100644 --- a/src/api/routes/auth/logout.ts +++ b/src/api/routes/auth/logout.ts @@ -22,14 +22,25 @@ import { Request, Response, Router } from "express"; const router: Router = Router(); export default router; -router.post("/", route({}), async (req: Request, res: Response) => { - if (req.body.provider != null || req.body.voip_provider != null) { - console.log(`[LOGOUT]: provider or voip provider not null!`, req.body); - } else { - delete req.body.provider; - delete req.body.voip_provider; - if (Object.keys(req.body).length != 0) - console.log(`[LOGOUT]: Extra fields sent in logout!`, req.body); - } - res.status(204).send(); -}); +router.post( + "/", + route({ + responses: { + 204: {}, + }, + }), + async (req: Request, res: Response) => { + if (req.body.provider != null || req.body.voip_provider != null) { + console.log( + `[LOGOUT]: provider or voip provider not null!`, + req.body, + ); + } else { + delete req.body.provider; + delete req.body.voip_provider; + if (Object.keys(req.body).length != 0) + console.log(`[LOGOUT]: Extra fields sent in logout!`, req.body); + } + res.status(204).send(); + }, +); diff --git a/src/api/routes/auth/mfa/totp.ts b/src/api/routes/auth/mfa/totp.ts index 2396443d..4df408f9 100644 --- a/src/api/routes/auth/mfa/totp.ts +++ b/src/api/routes/auth/mfa/totp.ts @@ -16,16 +16,26 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Request, Response } from "express"; import { route } from "@spacebar/api"; -import { BackupCode, generateToken, User, TotpSchema } from "@spacebar/util"; -import { verifyToken } from "node-2fa"; +import { BackupCode, TotpSchema, User, generateToken } from "@spacebar/util"; +import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; +import { verifyToken } from "node-2fa"; const router = Router(); router.post( "/", - route({ body: "TotpSchema" }), + route({ + requestBody: "TotpSchema", + responses: { + 200: { + body: "TokenResponse", + }, + 400: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { // const { code, ticket, gift_code_sku_id, login_source } = const { code, ticket } = req.body as TotpSchema; diff --git a/src/api/routes/auth/mfa/webauthn.ts b/src/api/routes/auth/mfa/webauthn.ts index 1b387411..b58d2944 100644 --- a/src/api/routes/auth/mfa/webauthn.ts +++ b/src/api/routes/auth/mfa/webauthn.ts @@ -41,7 +41,13 @@ function toArrayBuffer(buf: Buffer) { router.post( "/", - route({ body: "WebAuthnTotpSchema" }), + route({ + requestBody: "WebAuthnTotpSchema", + responses: { + 200: { body: "TokenResponse" }, + 400: { body: "APIErrorResponse" }, + }, + }), async (req: Request, res: Response) => { if (!WebAuthn.fido2) { // TODO: I did this for typescript and I can't use ! diff --git a/src/api/routes/auth/register.ts b/src/api/routes/auth/register.ts index 430c9532..14dc319a 100644 --- a/src/api/routes/auth/register.ts +++ b/src/api/routes/auth/register.ts @@ -16,25 +16,25 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Request, Response, Router } from "express"; +import { + IPAnalysis, + getIpAdress, + isProxy, + route, + verifyCaptcha, +} from "@spacebar/api"; import { Config, - generateToken, - Invite, FieldErrors, - User, - adjustEmail, + Invite, RegisterSchema, + User, ValidRegistrationToken, + adjustEmail, + generateToken, } from "@spacebar/util"; -import { - route, - getIpAdress, - IPAnalysis, - isProxy, - verifyCaptcha, -} from "@spacebar/api"; import bcrypt from "bcrypt"; +import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; import { MoreThan } from "typeorm"; @@ -42,7 +42,13 @@ const router: Router = Router(); router.post( "/", - route({ body: "RegisterSchema" }), + route({ + requestBody: "RegisterSchema", + responses: { + 200: { body: "TokenOnlyResponse" }, + 400: { body: "APIErrorOrCaptchaResponse" }, + }, + }), async (req: Request, res: Response) => { const body = req.body as RegisterSchema; const { register, security, limits } = Config.get(); @@ -219,6 +225,20 @@ router.post( } if (body.password) { + const min = register.password.minLength + ? register.password.minLength + : 8; + if (body.password.length < min) { + throw FieldErrors({ + password: { + code: "PASSWORD_REQUIREMENTS_MIN_LENGTH", + message: req.t( + "auth:register.PASSWORD_REQUIREMENTS_MIN_LENGTH", + { min: min }, + ), + }, + }); + } // the salt is saved in the password refer to bcrypt docs body.password = await bcrypt.hash(body.password, 12); } else if (register.password.required) { diff --git a/src/api/routes/auth/reset.ts b/src/api/routes/auth/reset.ts index 852a43c7..b3ca1e9e 100644 --- a/src/api/routes/auth/reset.ts +++ b/src/api/routes/auth/reset.ts @@ -19,7 +19,6 @@ import { route } from "@spacebar/api"; import { checkToken, - Config, Email, FieldErrors, generateToken, @@ -31,17 +30,26 @@ import { Request, Response, Router } from "express"; const router = Router(); +// TODO: the response interface also returns settings, but this route doesn't actually return that. router.post( "/", - route({ body: "PasswordResetSchema" }), + route({ + requestBody: "PasswordResetSchema", + responses: { + 200: { + body: "TokenOnlyResponse", + }, + 400: { + body: "APIErrorOrCaptchaResponse", + }, + }, + }), async (req: Request, res: Response) => { const { password, token } = req.body as PasswordResetSchema; - const { jwtSecret } = Config.get().security; - let user; try { - const userTokenData = await checkToken(token, jwtSecret, true); + const userTokenData = await checkToken(token); user = userTokenData.user; } catch { throw FieldErrors({ diff --git a/src/api/routes/auth/verify/index.ts b/src/api/routes/auth/verify/index.ts index c1afcde9..49f74277 100644 --- a/src/api/routes/auth/verify/index.ts +++ b/src/api/routes/auth/verify/index.ts @@ -37,9 +37,20 @@ async function getToken(user: User) { return { token }; } +// TODO: the response interface also returns settings, but this route doesn't actually return that. router.post( "/", - route({ body: "VerifyEmailSchema" }), + route({ + requestBody: "VerifyEmailSchema", + responses: { + 200: { + body: "TokenResponse", + }, + 400: { + body: "APIErrorOrCaptchaResponse", + }, + }, + }), async (req: Request, res: Response) => { const { captcha_key, token } = req.body; @@ -67,11 +78,10 @@ router.post( } } - const { jwtSecret } = Config.get().security; let user; try { - const userTokenData = await checkToken(token, jwtSecret, true); + const userTokenData = await checkToken(token); user = userTokenData.user; } catch { throw FieldErrors({ diff --git a/src/api/routes/auth/verify/resend.ts b/src/api/routes/auth/verify/resend.ts index f2727abd..701f0ea8 100644 --- a/src/api/routes/auth/verify/resend.ts +++ b/src/api/routes/auth/verify/resend.ts @@ -24,7 +24,18 @@ const router = Router(); router.post( "/", - route({ right: "RESEND_VERIFICATION_EMAIL" }), + route({ + right: "RESEND_VERIFICATION_EMAIL", + responses: { + 204: {}, + 400: { + body: "APIErrorResponse", + }, + 500: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const user = await User.findOneOrFail({ where: { id: req.user_id }, diff --git a/src/api/routes/auth/verify/view-backup-codes-challenge.ts b/src/api/routes/auth/verify/view-backup-codes-challenge.ts index b12719ff..5407de82 100644 --- a/src/api/routes/auth/verify/view-backup-codes-challenge.ts +++ b/src/api/routes/auth/verify/view-backup-codes-challenge.ts @@ -16,15 +16,21 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Request, Response } from "express"; import { route } from "@spacebar/api"; -import { FieldErrors, User, BackupCodesChallengeSchema } from "@spacebar/util"; +import { BackupCodesChallengeSchema, FieldErrors, User } from "@spacebar/util"; import bcrypt from "bcrypt"; +import { Request, Response, Router } from "express"; const router = Router(); router.post( "/", - route({ body: "BackupCodesChallengeSchema" }), + route({ + requestBody: "BackupCodesChallengeSchema", + responses: { + 200: { body: "BackupCodesChallengeResponse" }, + 400: { body: "APIErrorResponse" }, + }, + }), async (req: Request, res: Response) => { const { password } = req.body as BackupCodesChallengeSchema; diff --git a/src/api/routes/channels/#channel_id/index.ts b/src/api/routes/channels/#channel_id/index.ts index db0d4242..567c7c92 100644 --- a/src/api/routes/channels/#channel_id/index.ts +++ b/src/api/routes/channels/#channel_id/index.ts @@ -16,18 +16,18 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ +import { route } from "@spacebar/api"; import { Channel, ChannelDeleteEvent, + ChannelModifySchema, ChannelType, ChannelUpdateEvent, - emitEvent, Recipient, + emitEvent, handleFile, - ChannelModifySchema, } from "@spacebar/util"; import { Request, Response, Router } from "express"; -import { route } from "@spacebar/api"; const router: Router = Router(); // TODO: delete channel @@ -35,7 +35,15 @@ const router: Router = Router(); router.get( "/", - route({ permission: "VIEW_CHANNEL" }), + route({ + permission: "VIEW_CHANNEL", + responses: { + 200: { + body: "Channel", + }, + 404: {}, + }, + }), async (req: Request, res: Response) => { const { channel_id } = req.params; @@ -49,7 +57,15 @@ router.get( router.delete( "/", - route({ permission: "MANAGE_CHANNELS" }), + route({ + permission: "MANAGE_CHANNELS", + responses: { + 200: { + body: "Channel", + }, + 404: {}, + }, + }), async (req: Request, res: Response) => { const { channel_id } = req.params; @@ -90,7 +106,19 @@ router.delete( router.patch( "/", - route({ body: "ChannelModifySchema", permission: "MANAGE_CHANNELS" }), + route({ + requestBody: "ChannelModifySchema", + permission: "MANAGE_CHANNELS", + responses: { + 200: { + body: "Channel", + }, + 404: {}, + 400: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const payload = req.body as ChannelModifySchema; const { channel_id } = req.params; diff --git a/src/api/routes/channels/#channel_id/invites.ts b/src/api/routes/channels/#channel_id/invites.ts index 9f247fe8..b02f65d3 100644 --- a/src/api/routes/channels/#channel_id/invites.ts +++ b/src/api/routes/channels/#channel_id/invites.ts @@ -16,29 +16,37 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Request, Response } from "express"; -import { HTTPError } from "lambert-server"; -import { route } from "@spacebar/api"; -import { random } from "@spacebar/api"; +import { random, route } from "@spacebar/api"; import { Channel, + Guild, Invite, InviteCreateEvent, - emitEvent, - User, - Guild, PublicInviteRelation, + User, + emitEvent, + isTextChannel, } from "@spacebar/util"; -import { isTextChannel } from "./messages"; +import { Request, Response, Router } from "express"; +import { HTTPError } from "lambert-server"; const router: Router = Router(); router.post( "/", route({ - body: "InviteCreateSchema", + requestBody: "InviteCreateSchema", permission: "CREATE_INSTANT_INVITE", right: "CREATE_INVITES", + responses: { + 201: { + body: "Invite", + }, + 404: {}, + 400: { + body: "APIErrorResponse", + }, + }, }), async (req: Request, res: Response) => { const { user_id } = req; @@ -84,7 +92,15 @@ router.post( router.get( "/", - route({ permission: "MANAGE_CHANNELS" }), + route({ + permission: "MANAGE_CHANNELS", + responses: { + 200: { + body: "APIInviteArray", + }, + 404: {}, + }, + }), async (req: Request, res: Response) => { const { channel_id } = req.params; const channel = await Channel.findOneOrFail({ diff --git a/src/api/routes/channels/#channel_id/messages/#message_id/ack.ts b/src/api/routes/channels/#channel_id/messages/#message_id/ack.ts index f098fa8e..a6dcae6b 100644 --- a/src/api/routes/channels/#channel_id/messages/#message_id/ack.ts +++ b/src/api/routes/channels/#channel_id/messages/#message_id/ack.ts @@ -16,6 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ +import { route } from "@spacebar/api"; import { emitEvent, getPermission, @@ -23,7 +24,6 @@ import { ReadState, } from "@spacebar/util"; import { Request, Response, Router } from "express"; -import { route } from "@spacebar/api"; const router = Router(); @@ -33,7 +33,13 @@ const router = Router(); router.post( "/", - route({ body: "MessageAcknowledgeSchema" }), + route({ + requestBody: "MessageAcknowledgeSchema", + responses: { + 200: {}, + 403: {}, + }, + }), async (req: Request, res: Response) => { const { channel_id, message_id } = req.params; diff --git a/src/api/routes/channels/#channel_id/messages/#message_id/crosspost.ts b/src/api/routes/channels/#channel_id/messages/#message_id/crosspost.ts index 909a459e..5ca645c0 100644 --- a/src/api/routes/channels/#channel_id/messages/#message_id/crosspost.ts +++ b/src/api/routes/channels/#channel_id/messages/#message_id/crosspost.ts @@ -16,14 +16,21 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Response, Request } from "express"; import { route } from "@spacebar/api"; +import { Request, Response, Router } from "express"; const router = Router(); router.post( "/", - route({ permission: "MANAGE_MESSAGES" }), + route({ + permission: "MANAGE_MESSAGES", + responses: { + 200: { + body: "Message", + }, + }, + }), (req: Request, res: Response) => { // TODO: res.json({ diff --git a/src/api/routes/channels/#channel_id/messages/#message_id/index.ts b/src/api/routes/channels/#channel_id/messages/#message_id/index.ts index cd4b243e..6bc03f53 100644 --- a/src/api/routes/channels/#channel_id/messages/#message_id/index.ts +++ b/src/api/routes/channels/#channel_id/messages/#message_id/index.ts @@ -19,24 +19,23 @@ import { Attachment, Channel, - emitEvent, - SpacebarApiErrors, - getPermission, - getRights, Message, MessageCreateEvent, + MessageCreateSchema, MessageDeleteEvent, + MessageEditSchema, MessageUpdateEvent, Snowflake, + SpacebarApiErrors, + emitEvent, + getPermission, + getRights, uploadFile, - MessageCreateSchema, - MessageEditSchema, } from "@spacebar/util"; -import { Router, Response, Request } from "express"; -import multer from "multer"; -import { route } from "@spacebar/api"; -import { handleMessage, postHandleMessage } from "@spacebar/api"; +import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; +import multer from "multer"; +import { handleMessage, postHandleMessage, route } from "../../../../../util"; const router = Router(); // TODO: message content/embed string length limit @@ -53,9 +52,19 @@ const messageUpload = multer({ router.patch( "/", route({ - body: "MessageEditSchema", + requestBody: "MessageEditSchema", permission: "SEND_MESSAGES", right: "SEND_MESSAGES", + responses: { + 200: { + body: "Message", + }, + 400: { + body: "APIErrorResponse", + }, + 403: {}, + 404: {}, + }, }), async (req: Request, res: Response) => { const { message_id, channel_id } = req.params; @@ -143,9 +152,19 @@ router.put( next(); }, route({ - body: "MessageCreateSchema", + requestBody: "MessageCreateSchema", permission: "SEND_MESSAGES", right: "SEND_BACKDATED_EVENTS", + responses: { + 200: { + body: "Message", + }, + 400: { + body: "APIErrorResponse", + }, + 403: {}, + 404: {}, + }, }), async (req: Request, res: Response) => { const { channel_id, message_id } = req.params; @@ -230,7 +249,19 @@ router.put( router.get( "/", - route({ permission: "VIEW_CHANNEL" }), + route({ + permission: "VIEW_CHANNEL", + responses: { + 200: { + body: "Message", + }, + 400: { + body: "APIErrorResponse", + }, + 403: {}, + 404: {}, + }, + }), async (req: Request, res: Response) => { const { message_id, channel_id } = req.params; @@ -252,38 +283,54 @@ router.get( }, ); -router.delete("/", route({}), async (req: Request, res: Response) => { - const { message_id, channel_id } = req.params; +router.delete( + "/", + route({ + responses: { + 204: {}, + 400: { + body: "APIErrorResponse", + }, + 404: {}, + }, + }), + async (req: Request, res: Response) => { + const { message_id, channel_id } = req.params; - const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); - const message = await Message.findOneOrFail({ where: { id: message_id } }); + const channel = await Channel.findOneOrFail({ + where: { id: channel_id }, + }); + const message = await Message.findOneOrFail({ + where: { id: message_id }, + }); - const rights = await getRights(req.user_id); + const rights = await getRights(req.user_id); - if (message.author_id !== req.user_id) { - if (!rights.has("MANAGE_MESSAGES")) { - const permission = await getPermission( - req.user_id, - channel.guild_id, - channel_id, - ); - permission.hasThrow("MANAGE_MESSAGES"); - } - } else rights.hasThrow("SELF_DELETE_MESSAGES"); + if (message.author_id !== req.user_id) { + if (!rights.has("MANAGE_MESSAGES")) { + const permission = await getPermission( + req.user_id, + channel.guild_id, + channel_id, + ); + permission.hasThrow("MANAGE_MESSAGES"); + } + } else rights.hasThrow("SELF_DELETE_MESSAGES"); - await Message.delete({ id: message_id }); + await Message.delete({ id: message_id }); - await emitEvent({ - event: "MESSAGE_DELETE", - channel_id, - data: { - id: message_id, + await emitEvent({ + event: "MESSAGE_DELETE", channel_id, - guild_id: channel.guild_id, - }, - } as MessageDeleteEvent); + data: { + id: message_id, + channel_id, + guild_id: channel.guild_id, + }, + } as MessageDeleteEvent); - res.sendStatus(204); -}); + res.sendStatus(204); + }, +); export default router; diff --git a/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts b/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts index cb66cd64..5efa0f14 100644 --- a/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts +++ b/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts @@ -16,6 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ +import { route } from "@spacebar/api"; import { Channel, emitEvent, @@ -32,8 +33,7 @@ import { PublicUserProjection, User, } from "@spacebar/util"; -import { route } from "@spacebar/api"; -import { Router, Response, Request } from "express"; +import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; import { In } from "typeorm"; @@ -57,7 +57,17 @@ function getEmoji(emoji: string): PartialEmoji { router.delete( "/", - route({ permission: "MANAGE_MESSAGES" }), + route({ + permission: "MANAGE_MESSAGES", + responses: { + 204: {}, + 400: { + body: "APIErrorResponse", + }, + 404: {}, + 403: {}, + }, + }), async (req: Request, res: Response) => { const { message_id, channel_id } = req.params; @@ -83,7 +93,17 @@ router.delete( router.delete( "/:emoji", - route({ permission: "MANAGE_MESSAGES" }), + route({ + permission: "MANAGE_MESSAGES", + responses: { + 204: {}, + 400: { + body: "APIErrorResponse", + }, + 404: {}, + 403: {}, + }, + }), async (req: Request, res: Response) => { const { message_id, channel_id } = req.params; const emoji = getEmoji(req.params.emoji); @@ -120,7 +140,19 @@ router.delete( router.get( "/:emoji", - route({ permission: "VIEW_CHANNEL" }), + route({ + permission: "VIEW_CHANNEL", + responses: { + 200: { + body: "PublicUser", + }, + 400: { + body: "APIErrorResponse", + }, + 404: {}, + 403: {}, + }, + }), async (req: Request, res: Response) => { const { message_id, channel_id } = req.params; const emoji = getEmoji(req.params.emoji); @@ -148,7 +180,18 @@ router.get( router.put( "/:emoji/:user_id", - route({ permission: "READ_MESSAGE_HISTORY", right: "SELF_ADD_REACTIONS" }), + route({ + permission: "READ_MESSAGE_HISTORY", + right: "SELF_ADD_REACTIONS", + responses: { + 204: {}, + 400: { + body: "APIErrorResponse", + }, + 404: {}, + 403: {}, + }, + }), async (req: Request, res: Response) => { const { message_id, channel_id, user_id } = req.params; if (user_id !== "@me") throw new HTTPError("Invalid user"); @@ -219,7 +262,16 @@ router.put( router.delete( "/:emoji/:user_id", - route({}), + route({ + responses: { + 204: {}, + 400: { + body: "APIErrorResponse", + }, + 404: {}, + 403: {}, + }, + }), async (req: Request, res: Response) => { let { user_id } = req.params; const { message_id, channel_id } = req.params; diff --git a/src/api/routes/channels/#channel_id/messages/bulk-delete.ts b/src/api/routes/channels/#channel_id/messages/bulk-delete.ts index 18476d5c..9b607d59 100644 --- a/src/api/routes/channels/#channel_id/messages/bulk-delete.ts +++ b/src/api/routes/channels/#channel_id/messages/bulk-delete.ts @@ -16,18 +16,18 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Response, Request } from "express"; +import { route } from "@spacebar/api"; import { Channel, Config, emitEvent, getPermission, getRights, - MessageDeleteBulkEvent, Message, + MessageDeleteBulkEvent, } from "@spacebar/util"; +import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; -import { route } from "@spacebar/api"; const router: Router = Router(); @@ -38,7 +38,17 @@ export default router; // https://discord.com/developers/docs/resources/channel#bulk-delete-messages router.post( "/", - route({ body: "BulkDeleteSchema" }), + route({ + requestBody: "BulkDeleteSchema", + responses: { + 204: {}, + 400: { + body: "APIErrorResponse", + }, + 403: {}, + 404: {}, + }, + }), async (req: Request, res: Response) => { const { channel_id } = req.params; const channel = await Channel.findOneOrFail({ diff --git a/src/api/routes/channels/#channel_id/messages/index.ts b/src/api/routes/channels/#channel_id/messages/index.ts index 7f0c9fb5..c384a05b 100644 --- a/src/api/routes/channels/#channel_id/messages/index.ts +++ b/src/api/routes/channels/#channel_id/messages/index.ts @@ -16,128 +16,137 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Response, Request } from "express"; +import { handleMessage, postHandleMessage, route } from "@spacebar/api"; import { Attachment, Channel, - ChannelType, Config, DmChannelDTO, - emitEvent, FieldErrors, - getPermission, + Member, Message, MessageCreateEvent, - Snowflake, - uploadFile, - Member, MessageCreateSchema, + Reaction, ReadState, Rights, - Reaction, + Snowflake, User, + emitEvent, + getPermission, + isTextChannel, + uploadFile, } from "@spacebar/util"; +import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; -import { handleMessage, postHandleMessage, route } from "@spacebar/api"; import multer from "multer"; import { FindManyOptions, FindOperator, LessThan, MoreThan } from "typeorm"; import { URL } from "url"; const router: Router = Router(); -export default router; - -export function isTextChannel(type: ChannelType): boolean { - switch (type) { - case ChannelType.GUILD_STORE: - case ChannelType.GUILD_VOICE: - case ChannelType.GUILD_STAGE_VOICE: - case ChannelType.GUILD_CATEGORY: - case ChannelType.GUILD_FORUM: - case ChannelType.DIRECTORY: - throw new HTTPError("not a text channel", 400); - case ChannelType.DM: - case ChannelType.GROUP_DM: - case ChannelType.GUILD_NEWS: - case ChannelType.GUILD_NEWS_THREAD: - case ChannelType.GUILD_PUBLIC_THREAD: - case ChannelType.GUILD_PRIVATE_THREAD: - case ChannelType.GUILD_TEXT: - case ChannelType.ENCRYPTED: - case ChannelType.ENCRYPTED_THREAD: - return true; - default: - throw new HTTPError("unimplemented", 400); - } -} - // https://discord.com/developers/docs/resources/channel#create-message // get messages -router.get("/", route({}), async (req: Request, res: Response) => { - const channel_id = req.params.channel_id; - const channel = await Channel.findOneOrFail({ - where: { id: channel_id }, - }); - if (!channel) throw new HTTPError("Channel not found", 404); - - isTextChannel(channel.type); - const around = req.query.around ? `${req.query.around}` : undefined; - const before = req.query.before ? `${req.query.before}` : undefined; - const after = req.query.after ? `${req.query.after}` : undefined; - const limit = Number(req.query.limit) || 50; - if (limit < 1 || limit > 100) - throw new HTTPError("limit must be between 1 and 100", 422); - - const halfLimit = Math.floor(limit / 2); - - const permissions = await getPermission( - req.user_id, - channel.guild_id, - channel_id, - ); - permissions.hasThrow("VIEW_CHANNEL"); - if (!permissions.has("READ_MESSAGE_HISTORY")) return res.json([]); - - const query: FindManyOptions<Message> & { - where: { id?: FindOperator<string> | FindOperator<string>[] }; - } = { - order: { timestamp: "DESC" }, - take: limit, - where: { channel_id }, - relations: [ - "author", - "webhook", - "application", - "mentions", - "mention_roles", - "mention_channels", - "sticker_items", - "attachments", - ], - }; - - if (after) { - if (BigInt(after) > BigInt(Snowflake.generate())) - return res.status(422); - query.where.id = MoreThan(after); - } else if (before) { - if (BigInt(before) < BigInt(req.params.channel_id)) - return res.status(422); - query.where.id = LessThan(before); - } else if (around) { - query.where.id = [ - MoreThan((BigInt(around) - BigInt(halfLimit)).toString()), - LessThan((BigInt(around) + BigInt(halfLimit)).toString()), - ]; - - return res.json([]); // TODO: fix around - } - - const messages = await Message.find(query); - const endpoint = Config.get().cdn.endpointPublic; - - return res.json( - messages.map((x: Partial<Message>) => { +router.get( + "/", + route({ + query: { + around: { + type: "string", + }, + before: { + type: "string", + }, + after: { + type: "string", + }, + limit: { + type: "number", + description: + "max number of messages to return (1-100). defaults to 50", + }, + }, + responses: { + 200: { + body: "APIMessageArray", + }, + 400: { + body: "APIErrorResponse", + }, + 403: {}, + 404: {}, + }, + }), + async (req: Request, res: Response) => { + const channel_id = req.params.channel_id; + const channel = await Channel.findOneOrFail({ + where: { id: channel_id }, + }); + if (!channel) throw new HTTPError("Channel not found", 404); + + isTextChannel(channel.type); + const around = req.query.around ? `${req.query.around}` : undefined; + const before = req.query.before ? `${req.query.before}` : undefined; + const after = req.query.after ? `${req.query.after}` : undefined; + const limit = Number(req.query.limit) || 50; + if (limit < 1 || limit > 100) + throw new HTTPError("limit must be between 1 and 100", 422); + + const permissions = await getPermission( + req.user_id, + channel.guild_id, + channel_id, + ); + permissions.hasThrow("VIEW_CHANNEL"); + if (!permissions.has("READ_MESSAGE_HISTORY")) return res.json([]); + + const query: FindManyOptions<Message> & { + where: { id?: FindOperator<string> | FindOperator<string>[] }; + } = { + order: { timestamp: "DESC" }, + take: limit, + where: { channel_id }, + relations: [ + "author", + "webhook", + "application", + "mentions", + "mention_roles", + "mention_channels", + "sticker_items", + "attachments", + ], + }; + + let messages: Message[]; + + if (around) { + query.take = Math.floor(limit / 2); + const [right, left] = await Promise.all([ + Message.find({ ...query, where: { id: LessThan(around) } }), + Message.find({ ...query, where: { id: MoreThan(around) } }), + ]); + right.push(...left); + messages = right; + } else { + if (after) { + if (BigInt(after) > BigInt(Snowflake.generate())) + return res.status(422); + query.where.id = MoreThan(after); + } else if (before) { + if (BigInt(before) > BigInt(Snowflake.generate())) + return res.status(422); + query.where.id = LessThan(before); + } + + messages = await Message.find(query); + } + + const endpoint = Config.get().cdn.endpointPublic; + + const ret = messages.map((x: Message) => { + x = x.toJSON(); + (x.reactions || []).forEach((y: Partial<Reaction>) => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore @@ -172,9 +181,11 @@ router.get("/", route({}), async (req: Request, res: Response) => { // } return x; - }), - ); -}); + }); + + return res.json(ret); + }, +); // TODO: config max upload size const messageUpload = multer({ @@ -205,9 +216,19 @@ router.post( next(); }, route({ - body: "MessageCreateSchema", + requestBody: "MessageCreateSchema", permission: "SEND_MESSAGES", right: "SEND_MESSAGES", + responses: { + 200: { + body: "Message", + }, + 400: { + body: "APIErrorResponse", + }, + 403: {}, + 404: {}, + }, }), async (req: Request, res: Response) => { const { channel_id } = req.params; @@ -288,9 +309,11 @@ router.post( embeds, channel_id, attachments, - edited_timestamp: undefined, timestamp: new Date(), }); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore dont care2 + message.edited_timestamp = null; channel.last_message_id = message.id; @@ -366,3 +389,5 @@ router.post( return res.json(message); }, ); + +export default router; diff --git a/src/api/routes/channels/#channel_id/permissions.ts b/src/api/routes/channels/#channel_id/permissions.ts index 68dbc2f2..d3edb0fa 100644 --- a/src/api/routes/channels/#channel_id/permissions.ts +++ b/src/api/routes/channels/#channel_id/permissions.ts @@ -19,13 +19,13 @@ import { Channel, ChannelPermissionOverwrite, + ChannelPermissionOverwriteSchema, ChannelUpdateEvent, emitEvent, Member, Role, - ChannelPermissionOverwriteSchema, } from "@spacebar/util"; -import { Router, Response, Request } from "express"; +import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; import { route } from "@spacebar/api"; @@ -36,8 +36,14 @@ const router: Router = Router(); router.put( "/:overwrite_id", route({ - body: "ChannelPermissionOverwriteSchema", + requestBody: "ChannelPermissionOverwriteSchema", permission: "MANAGE_ROLES", + responses: { + 204: {}, + 404: {}, + 501: {}, + 400: { body: "APIErrorResponse" }, + }, }), async (req: Request, res: Response) => { const { channel_id, overwrite_id } = req.params; @@ -92,7 +98,7 @@ router.put( // TODO: check permission hierarchy router.delete( "/:overwrite_id", - route({ permission: "MANAGE_ROLES" }), + route({ permission: "MANAGE_ROLES", responses: { 204: {}, 404: {} } }), async (req: Request, res: Response) => { const { channel_id, overwrite_id } = req.params; diff --git a/src/api/routes/channels/#channel_id/pins.ts b/src/api/routes/channels/#channel_id/pins.ts index 32820916..724ebffd 100644 --- a/src/api/routes/channels/#channel_id/pins.ts +++ b/src/api/routes/channels/#channel_id/pins.ts @@ -16,23 +16,33 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ +import { route } from "@spacebar/api"; import { Channel, ChannelPinsUpdateEvent, Config, + DiscordApiErrors, emitEvent, Message, MessageUpdateEvent, - DiscordApiErrors, } from "@spacebar/util"; -import { Router, Request, Response } from "express"; -import { route } from "@spacebar/api"; +import { Request, Response, Router } from "express"; const router: Router = Router(); router.put( "/:message_id", - route({ permission: "VIEW_CHANNEL" }), + route({ + permission: "VIEW_CHANNEL", + responses: { + 204: {}, + 403: {}, + 404: {}, + 400: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const { channel_id, message_id } = req.params; @@ -74,7 +84,17 @@ router.put( router.delete( "/:message_id", - route({ permission: "VIEW_CHANNEL" }), + route({ + permission: "VIEW_CHANNEL", + responses: { + 204: {}, + 403: {}, + 404: {}, + 400: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const { channel_id, message_id } = req.params; @@ -114,7 +134,17 @@ router.delete( router.get( "/", - route({ permission: ["READ_MESSAGE_HISTORY"] }), + route({ + permission: ["READ_MESSAGE_HISTORY"], + responses: { + 200: { + body: "APIMessageArray", + }, + 400: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const { channel_id } = req.params; diff --git a/src/api/routes/channels/#channel_id/purge.ts b/src/api/routes/channels/#channel_id/purge.ts index c8da6760..012fec1c 100644 --- a/src/api/routes/channels/#channel_id/purge.ts +++ b/src/api/routes/channels/#channel_id/purge.ts @@ -16,20 +16,20 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { HTTPError } from "lambert-server"; import { route } from "@spacebar/api"; -import { isTextChannel } from "./messages"; -import { FindManyOptions, Between, Not, FindOperator } from "typeorm"; import { Channel, - emitEvent, - getPermission, - getRights, Message, MessageDeleteBulkEvent, PurgeSchema, + emitEvent, + getPermission, + getRights, + isTextChannel, } from "@spacebar/util"; -import { Router, Response, Request } from "express"; +import { Request, Response, Router } from "express"; +import { HTTPError } from "lambert-server"; +import { Between, FindManyOptions, FindOperator, Not } from "typeorm"; const router: Router = Router(); @@ -42,6 +42,14 @@ router.post( "/", route({ /*body: "PurgeSchema",*/ + responses: { + 204: {}, + 400: { + body: "APIErrorResponse", + }, + 404: {}, + 403: {}, + }, }), async (req: Request, res: Response) => { const { channel_id } = req.params; diff --git a/src/api/routes/channels/#channel_id/recipients.ts b/src/api/routes/channels/#channel_id/recipients.ts index f1fb48af..569bb5cd 100644 --- a/src/api/routes/channels/#channel_id/recipients.ts +++ b/src/api/routes/channels/#channel_id/recipients.ts @@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; import { Channel, ChannelRecipientAddEvent, @@ -28,80 +28,98 @@ import { Recipient, User, } from "@spacebar/util"; -import { route } from "@spacebar/api"; +import { Request, Response, Router } from "express"; const router: Router = Router(); -router.put("/:user_id", route({}), async (req: Request, res: Response) => { - const { channel_id, user_id } = req.params; - const channel = await Channel.findOneOrFail({ - where: { id: channel_id }, - relations: ["recipients"], - }); +router.put( + "/:user_id", + route({ + responses: { + 201: {}, + 404: {}, + }, + }), + async (req: Request, res: Response) => { + const { channel_id, user_id } = req.params; + const channel = await Channel.findOneOrFail({ + where: { id: channel_id }, + relations: ["recipients"], + }); - if (channel.type !== ChannelType.GROUP_DM) { - const recipients = [ - ...(channel.recipients?.map((r) => r.user_id) || []), - user_id, - ].unique(); + if (channel.type !== ChannelType.GROUP_DM) { + const recipients = [ + ...(channel.recipients?.map((r) => r.user_id) || []), + user_id, + ].unique(); - const new_channel = await Channel.createDMChannel( - recipients, - req.user_id, - ); - return res.status(201).json(new_channel); - } else { - if (channel.recipients?.map((r) => r.user_id).includes(user_id)) { - throw DiscordApiErrors.INVALID_RECIPIENT; //TODO is this the right error? - } + const new_channel = await Channel.createDMChannel( + recipients, + req.user_id, + ); + return res.status(201).json(new_channel); + } else { + if (channel.recipients?.map((r) => r.user_id).includes(user_id)) { + throw DiscordApiErrors.INVALID_RECIPIENT; //TODO is this the right error? + } - channel.recipients?.push( - Recipient.create({ channel_id: channel_id, user_id: user_id }), - ); - await channel.save(); + channel.recipients?.push( + Recipient.create({ channel_id: channel_id, user_id: user_id }), + ); + await channel.save(); - await emitEvent({ - event: "CHANNEL_CREATE", - data: await DmChannelDTO.from(channel, [user_id]), - user_id: user_id, - }); + await emitEvent({ + event: "CHANNEL_CREATE", + data: await DmChannelDTO.from(channel, [user_id]), + user_id: user_id, + }); - await emitEvent({ - event: "CHANNEL_RECIPIENT_ADD", - data: { + await emitEvent({ + event: "CHANNEL_RECIPIENT_ADD", + data: { + channel_id: channel_id, + user: await User.findOneOrFail({ + where: { id: user_id }, + select: PublicUserProjection, + }), + }, channel_id: channel_id, - user: await User.findOneOrFail({ - where: { id: user_id }, - select: PublicUserProjection, - }), - }, - channel_id: channel_id, - } as ChannelRecipientAddEvent); - return res.sendStatus(204); - } -}); + } as ChannelRecipientAddEvent); + return res.sendStatus(204); + } + }, +); -router.delete("/:user_id", route({}), async (req: Request, res: Response) => { - const { channel_id, user_id } = req.params; - const channel = await Channel.findOneOrFail({ - where: { id: channel_id }, - relations: ["recipients"], - }); - if ( - !( - channel.type === ChannelType.GROUP_DM && - (channel.owner_id === req.user_id || user_id === req.user_id) +router.delete( + "/:user_id", + route({ + responses: { + 204: {}, + 404: {}, + }, + }), + async (req: Request, res: Response) => { + const { channel_id, user_id } = req.params; + const channel = await Channel.findOneOrFail({ + where: { id: channel_id }, + relations: ["recipients"], + }); + if ( + !( + channel.type === ChannelType.GROUP_DM && + (channel.owner_id === req.user_id || user_id === req.user_id) + ) ) - ) - throw DiscordApiErrors.MISSING_PERMISSIONS; + throw DiscordApiErrors.MISSING_PERMISSIONS; - if (!channel.recipients?.map((r) => r.user_id).includes(user_id)) { - throw DiscordApiErrors.INVALID_RECIPIENT; //TODO is this the right error? - } + if (!channel.recipients?.map((r) => r.user_id).includes(user_id)) { + throw DiscordApiErrors.INVALID_RECIPIENT; //TODO is this the right error? + } - await Channel.removeRecipientFromChannel(channel, user_id); + await Channel.removeRecipientFromChannel(channel, user_id); - return res.sendStatus(204); -}); + return res.sendStatus(204); + }, +); export default router; diff --git a/src/api/routes/channels/#channel_id/typing.ts b/src/api/routes/channels/#channel_id/typing.ts index 6a2fef39..b5d61d74 100644 --- a/src/api/routes/channels/#channel_id/typing.ts +++ b/src/api/routes/channels/#channel_id/typing.ts @@ -16,15 +16,22 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Channel, emitEvent, Member, TypingStartEvent } from "@spacebar/util"; import { route } from "@spacebar/api"; -import { Router, Request, Response } from "express"; +import { Channel, emitEvent, Member, TypingStartEvent } from "@spacebar/util"; +import { Request, Response, Router } from "express"; const router: Router = Router(); router.post( "/", - route({ permission: "SEND_MESSAGES" }), + route({ + permission: "SEND_MESSAGES", + responses: { + 204: {}, + 404: {}, + 403: {}, + }, + }), async (req: Request, res: Response) => { const { channel_id } = req.params; const user_id = req.user_id; diff --git a/src/api/routes/channels/#channel_id/webhooks.ts b/src/api/routes/channels/#channel_id/webhooks.ts index 14791a1c..d54756a1 100644 --- a/src/api/routes/channels/#channel_id/webhooks.ts +++ b/src/api/routes/channels/#channel_id/webhooks.ts @@ -16,34 +16,56 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Response, Request } from "express"; import { route } from "@spacebar/api"; import { Channel, Config, - handleFile, - trimSpecial, + DiscordApiErrors, User, Webhook, WebhookCreateSchema, WebhookType, + handleFile, + trimSpecial, + isTextChannel, } from "@spacebar/util"; -import { HTTPError } from "lambert-server"; -import { isTextChannel } from "./messages/index"; -import { DiscordApiErrors } from "@spacebar/util"; import crypto from "crypto"; +import { Request, Response, Router } from "express"; +import { HTTPError } from "lambert-server"; const router: Router = Router(); //TODO: implement webhooks -router.get("/", route({}), async (req: Request, res: Response) => { - res.json([]); -}); +router.get( + "/", + route({ + responses: { + 200: { + body: "APIWebhookArray", + }, + }, + }), + async (req: Request, res: Response) => { + res.json([]); + }, +); // TODO: use Image Data Type for avatar instead of String router.post( "/", - route({ body: "WebhookCreateSchema", permission: "MANAGE_WEBHOOKS" }), + route({ + requestBody: "WebhookCreateSchema", + permission: "MANAGE_WEBHOOKS", + responses: { + 200: { + body: "WebhookCreateResponse", + }, + 400: { + body: "APIErrorResponse", + }, + 403: {}, + }, + }), async (req: Request, res: Response) => { const channel_id = req.params.channel_id; const channel = await Channel.findOneOrFail({ diff --git a/src/api/routes/connections/#connection_name/#connection_id/refresh.ts b/src/api/routes/connections/#connection_name/#connection_id/refresh.ts index 0d432c2b..d44cf314 100644 --- a/src/api/routes/connections/#connection_name/#connection_id/refresh.ts +++ b/src/api/routes/connections/#connection_name/#connection_id/refresh.ts @@ -22,7 +22,7 @@ const router = Router(); router.post("/", route({}), async (req: Request, res: Response) => { // TODO: - const { connection_name, connection_id } = req.params; + // const { connection_name, connection_id } = req.params; res.sendStatus(204); }); diff --git a/src/api/routes/connections/#connection_name/callback.ts b/src/api/routes/connections/#connection_name/callback.ts index bc9ba455..ee0db94a 100644 --- a/src/api/routes/connections/#connection_name/callback.ts +++ b/src/api/routes/connections/#connection_name/callback.ts @@ -29,7 +29,7 @@ const router = Router(); router.post( "/", - route({ body: "ConnectionCallbackSchema" }), + route({ requestBody: "ConnectionCallbackSchema" }), async (req: Request, res: Response) => { const { connection_name } = req.params; const connection = ConnectionStore.connections.get(connection_name); diff --git a/src/api/routes/discoverable-guilds.ts b/src/api/routes/discoverable-guilds.ts index 75eb6088..b8c6a386 100644 --- a/src/api/routes/discoverable-guilds.ts +++ b/src/api/routes/discoverable-guilds.ts @@ -16,49 +16,61 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Guild, Config } from "@spacebar/util"; +import { Config, Guild } from "@spacebar/util"; -import { Router, Request, Response } from "express"; import { route } from "@spacebar/api"; +import { Request, Response, Router } from "express"; import { Like } from "typeorm"; const router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - const { offset, limit, categories } = req.query; - const showAllGuilds = Config.get().guild.discovery.showAllGuilds; - const configLimit = Config.get().guild.discovery.limit; - let guilds; - if (categories == undefined) { - guilds = showAllGuilds - ? await Guild.find({ take: Math.abs(Number(limit || configLimit)) }) - : await Guild.find({ - where: { features: Like(`%DISCOVERABLE%`) }, - take: Math.abs(Number(limit || configLimit)), - }); - } else { - guilds = showAllGuilds - ? await Guild.find({ - where: { primary_category_id: categories.toString() }, - take: Math.abs(Number(limit || configLimit)), - }) - : await Guild.find({ - where: { - primary_category_id: categories.toString(), - features: Like("%DISCOVERABLE%"), - }, - take: Math.abs(Number(limit || configLimit)), - }); - } +router.get( + "/", + route({ + responses: { + 200: { + body: "DiscoverableGuildsResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { offset, limit, categories } = req.query; + const showAllGuilds = Config.get().guild.discovery.showAllGuilds; + const configLimit = Config.get().guild.discovery.limit; + let guilds; + if (categories == undefined) { + guilds = showAllGuilds + ? await Guild.find({ + take: Math.abs(Number(limit || configLimit)), + }) + : await Guild.find({ + where: { features: Like(`%DISCOVERABLE%`) }, + take: Math.abs(Number(limit || configLimit)), + }); + } else { + guilds = showAllGuilds + ? await Guild.find({ + where: { primary_category_id: categories.toString() }, + take: Math.abs(Number(limit || configLimit)), + }) + : await Guild.find({ + where: { + primary_category_id: categories.toString(), + features: Like("%DISCOVERABLE%"), + }, + take: Math.abs(Number(limit || configLimit)), + }); + } - const total = guilds ? guilds.length : undefined; + const total = guilds ? guilds.length : undefined; - res.send({ - total: total, - guilds: guilds, - offset: Number(offset || Config.get().guild.discovery.offset), - limit: Number(limit || configLimit), - }); -}); + res.send({ + total: total, + guilds: guilds, + offset: Number(offset || Config.get().guild.discovery.offset), + limit: Number(limit || configLimit), + }); + }, +); export default router; diff --git a/src/api/routes/discovery.ts b/src/api/routes/discovery.ts index 0c8089e4..a045c191 100644 --- a/src/api/routes/discovery.ts +++ b/src/api/routes/discovery.ts @@ -16,24 +16,34 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Categories } from "@spacebar/util"; -import { Router, Response, Request } from "express"; import { route } from "@spacebar/api"; +import { Categories } from "@spacebar/util"; +import { Request, Response, Router } from "express"; const router = Router(); -router.get("/categories", route({}), async (req: Request, res: Response) => { - // TODO: - // Get locale instead +router.get( + "/categories", + route({ + responses: { + 200: { + body: "APIDiscoveryCategoryArray", + }, + }, + }), + async (req: Request, res: Response) => { + // TODO: + // Get locale instead - // const { locale, primary_only } = req.query; - const { primary_only } = req.query; + // const { locale, primary_only } = req.query; + const { primary_only } = req.query; - const out = primary_only - ? await Categories.find() - : await Categories.find({ where: { is_primary: true } }); + const out = primary_only + ? await Categories.find() + : await Categories.find({ where: { is_primary: true } }); - res.send(out); -}); + res.send(out); + }, +); export default router; diff --git a/src/api/routes/download.ts b/src/api/routes/download.ts index c4eea8e8..85fb41be 100644 --- a/src/api/routes/download.ts +++ b/src/api/routes/download.ts @@ -16,32 +16,43 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Response, Request } from "express"; import { route } from "@spacebar/api"; import { FieldErrors, Release } from "@spacebar/util"; +import { Request, Response, Router } from "express"; const router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - const { platform } = req.query; +router.get( + "/", + route({ + responses: { + 302: {}, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { platform } = req.query; + + if (!platform) + throw FieldErrors({ + platform: { + code: "BASE_TYPE_REQUIRED", + message: req.t("common:field.BASE_TYPE_REQUIRED"), + }, + }); - if (!platform) - throw FieldErrors({ - platform: { - code: "BASE_TYPE_REQUIRED", - message: req.t("common:field.BASE_TYPE_REQUIRED"), + const release = await Release.findOneOrFail({ + where: { + enabled: true, + platform: platform as string, }, + order: { pub_date: "DESC" }, }); - const release = await Release.findOneOrFail({ - where: { - enabled: true, - platform: platform as string, - }, - order: { pub_date: "DESC" }, - }); - - res.redirect(release.url); -}); + res.redirect(release.url); + }, +); export default router; diff --git a/src/api/routes/gateway/bot.ts b/src/api/routes/gateway/bot.ts index 243159ec..d9101159 100644 --- a/src/api/routes/gateway/bot.ts +++ b/src/api/routes/gateway/bot.ts @@ -16,32 +16,34 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ +import { route } from "@spacebar/api"; import { Config } from "@spacebar/util"; -import { Router, Response, Request } from "express"; -import { route, RouteOptions } from "@spacebar/api"; +import { Request, Response, Router } from "express"; const router = Router(); -const options: RouteOptions = { - test: { - response: { - body: "GatewayBotResponse", +router.get( + "/", + route({ + responses: { + 200: { + body: "GatewayBotResponse", + }, }, + }), + (req: Request, res: Response) => { + const { endpointPublic } = Config.get().gateway; + res.json({ + url: endpointPublic || process.env.GATEWAY || "ws://localhost:3001", + shards: 1, + session_start_limit: { + total: 1000, + remaining: 999, + reset_after: 14400000, + max_concurrency: 1, + }, + }); }, -}; - -router.get("/", route(options), (req: Request, res: Response) => { - const { endpointPublic } = Config.get().gateway; - res.json({ - url: endpointPublic || process.env.GATEWAY || "ws://localhost:3001", - shards: 1, - session_start_limit: { - total: 1000, - remaining: 999, - reset_after: 14400000, - max_concurrency: 1, - }, - }); -}); +); export default router; diff --git a/src/api/routes/gateway/index.ts b/src/api/routes/gateway/index.ts index 12e96919..9100d5ee 100644 --- a/src/api/routes/gateway/index.ts +++ b/src/api/routes/gateway/index.ts @@ -16,25 +16,27 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ +import { route } from "@spacebar/api"; import { Config } from "@spacebar/util"; -import { Router, Response, Request } from "express"; -import { route, RouteOptions } from "@spacebar/api"; +import { Request, Response, Router } from "express"; const router = Router(); -const options: RouteOptions = { - test: { - response: { - body: "GatewayResponse", +router.get( + "/", + route({ + responses: { + 200: { + body: "GatewayResponse", + }, }, + }), + (req: Request, res: Response) => { + const { endpointPublic } = Config.get().gateway; + res.json({ + url: endpointPublic || process.env.GATEWAY || "ws://localhost:3001", + }); }, -}; - -router.get("/", route(options), (req: Request, res: Response) => { - const { endpointPublic } = Config.get().gateway; - res.json({ - url: endpointPublic || process.env.GATEWAY || "ws://localhost:3001", - }); -}); +); export default router; diff --git a/src/api/routes/gifs/search.ts b/src/api/routes/gifs/search.ts index fb99374b..305a2a48 100644 --- a/src/api/routes/gifs/search.ts +++ b/src/api/routes/gifs/search.ts @@ -16,34 +16,62 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Response, Request } from "express"; -import fetch from "node-fetch"; -import ProxyAgent from "proxy-agent"; import { route } from "@spacebar/api"; -import { getGifApiKey, parseGifResult } from "./trending"; +import { TenorMediaTypes, getGifApiKey, parseGifResult } from "@spacebar/util"; +import { Request, Response, Router } from "express"; +import fetch from "node-fetch"; +import { ProxyAgent } from "proxy-agent"; const router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - // TODO: Custom providers - const { q, media_format, locale } = req.query; +router.get( + "/", + route({ + query: { + q: { + type: "string", + required: true, + description: "Search query", + }, + media_format: { + type: "string", + description: "Media format", + values: Object.keys(TenorMediaTypes).filter((key) => + isNaN(Number(key)), + ), + }, + locale: { + type: "string", + description: "Locale", + }, + }, + responses: { + 200: { + body: "TenorGifsResponse", + }, + }, + }), + async (req: Request, res: Response) => { + // TODO: Custom providers + const { q, media_format, locale } = req.query; - const apiKey = getGifApiKey(); + const apiKey = getGifApiKey(); - const agent = new ProxyAgent(); + const agent = new ProxyAgent(); - const response = await fetch( - `https://g.tenor.com/v1/search?q=${q}&media_format=${media_format}&locale=${locale}&key=${apiKey}`, - { - agent, - method: "get", - headers: { "Content-Type": "application/json" }, - }, - ); + const response = await fetch( + `https://g.tenor.com/v1/search?q=${q}&media_format=${media_format}&locale=${locale}&key=${apiKey}`, + { + agent, + method: "get", + headers: { "Content-Type": "application/json" }, + }, + ); - const { results } = await response.json(); + const { results } = await response.json(); - res.json(results.map(parseGifResult)).status(200); -}); + res.json(results.map(parseGifResult)).status(200); + }, +); export default router; diff --git a/src/api/routes/gifs/trending-gifs.ts b/src/api/routes/gifs/trending-gifs.ts index 238a2abd..77a61efc 100644 --- a/src/api/routes/gifs/trending-gifs.ts +++ b/src/api/routes/gifs/trending-gifs.ts @@ -16,34 +16,57 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Response, Request } from "express"; -import fetch from "node-fetch"; -import ProxyAgent from "proxy-agent"; import { route } from "@spacebar/api"; -import { getGifApiKey, parseGifResult } from "./trending"; +import { TenorMediaTypes, getGifApiKey, parseGifResult } from "@spacebar/util"; +import { Request, Response, Router } from "express"; +import fetch from "node-fetch"; +import { ProxyAgent } from "proxy-agent"; const router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - // TODO: Custom providers - const { media_format, locale } = req.query; +router.get( + "/", + route({ + query: { + media_format: { + type: "string", + description: "Media format", + values: Object.keys(TenorMediaTypes).filter((key) => + isNaN(Number(key)), + ), + }, + locale: { + type: "string", + description: "Locale", + }, + }, + responses: { + 200: { + body: "TenorGifsResponse", + }, + }, + }), + async (req: Request, res: Response) => { + // TODO: Custom providers + const { media_format, locale } = req.query; - const apiKey = getGifApiKey(); + const apiKey = getGifApiKey(); - const agent = new ProxyAgent(); + const agent = new ProxyAgent(); - const response = await fetch( - `https://g.tenor.com/v1/trending?media_format=${media_format}&locale=${locale}&key=${apiKey}`, - { - agent, - method: "get", - headers: { "Content-Type": "application/json" }, - }, - ); + const response = await fetch( + `https://g.tenor.com/v1/trending?media_format=${media_format}&locale=${locale}&key=${apiKey}`, + { + agent, + method: "get", + headers: { "Content-Type": "application/json" }, + }, + ); - const { results } = await response.json(); + const { results } = await response.json(); - res.json(results.map(parseGifResult)).status(200); -}); + res.json(results.map(parseGifResult)).status(200); + }, +); export default router; diff --git a/src/api/routes/gifs/trending.ts b/src/api/routes/gifs/trending.ts index 5cccdb2d..fe726842 100644 --- a/src/api/routes/gifs/trending.ts +++ b/src/api/routes/gifs/trending.ts @@ -16,126 +16,76 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Response, Request } from "express"; -import fetch from "node-fetch"; -import ProxyAgent from "proxy-agent"; import { route } from "@spacebar/api"; -import { Config } from "@spacebar/util"; -import { HTTPError } from "lambert-server"; +import { + TenorCategoriesResults, + TenorTrendingResults, + getGifApiKey, + parseGifResult, +} from "@spacebar/util"; +import { Request, Response, Router } from "express"; +import fetch from "node-fetch"; +import { ProxyAgent } from "proxy-agent"; const router = Router(); -// TODO: Move somewhere else -enum TENOR_GIF_TYPES { - gif, - mediumgif, - tinygif, - nanogif, - mp4, - loopedmp4, - tinymp4, - nanomp4, - webm, - tinywebm, - nanowebm, -} - -type TENOR_MEDIA = { - preview: string; - url: string; - dims: number[]; - size: number; -}; - -type TENOR_GIF = { - created: number; - hasaudio: boolean; - id: string; - media: { [type in keyof typeof TENOR_GIF_TYPES]: TENOR_MEDIA }[]; - tags: string[]; - title: string; - itemurl: string; - hascaption: boolean; - url: string; -}; - -type TENOR_CATEGORY = { - searchterm: string; - path: string; - image: string; - name: string; -}; - -type TENOR_CATEGORIES_RESULTS = { - tags: TENOR_CATEGORY[]; -}; - -type TENOR_TRENDING_RESULTS = { - next: string; - results: TENOR_GIF[]; -}; - -export function parseGifResult(result: TENOR_GIF) { - return { - id: result.id, - title: result.title, - url: result.itemurl, - src: result.media[0].mp4.url, - gif_src: result.media[0].gif.url, - width: result.media[0].mp4.dims[0], - height: result.media[0].mp4.dims[1], - preview: result.media[0].mp4.preview, - }; -} - -export function getGifApiKey() { - const { enabled, provider, apiKey } = Config.get().gif; - if (!enabled) throw new HTTPError(`Gifs are disabled`); - if (provider !== "tenor" || !apiKey) - throw new HTTPError(`${provider} gif provider not supported`); - - return apiKey; -} - -router.get("/", route({}), async (req: Request, res: Response) => { - // TODO: Custom providers - // TODO: return gifs as mp4 - // const { media_format, locale } = req.query; - const { locale } = req.query; - - const apiKey = getGifApiKey(); - - const agent = new ProxyAgent(); - - const [responseSource, trendGifSource] = await Promise.all([ - fetch( - `https://g.tenor.com/v1/categories?locale=${locale}&key=${apiKey}`, - { - agent, - method: "get", - headers: { "Content-Type": "application/json" }, +router.get( + "/", + route({ + query: { + locale: { + type: "string", + description: "Locale", }, - ), - fetch( - `https://g.tenor.com/v1/trending?locale=${locale}&key=${apiKey}`, - { - agent, - method: "get", - headers: { "Content-Type": "application/json" }, + }, + responses: { + 200: { + body: "TenorTrendingResponse", }, - ), - ]); - - const { tags } = (await responseSource.json()) as TENOR_CATEGORIES_RESULTS; - const { results } = (await trendGifSource.json()) as TENOR_TRENDING_RESULTS; - - res.json({ - categories: tags.map((x) => ({ - name: x.searchterm, - src: x.image, - })), - gifs: [parseGifResult(results[0])], - }).status(200); -}); + }, + }), + async (req: Request, res: Response) => { + // TODO: Custom providers + // TODO: return gifs as mp4 + // const { media_format, locale } = req.query; + const { locale } = req.query; + + const apiKey = getGifApiKey(); + + const agent = new ProxyAgent(); + + const [responseSource, trendGifSource] = await Promise.all([ + fetch( + `https://g.tenor.com/v1/categories?locale=${locale}&key=${apiKey}`, + { + agent, + method: "get", + headers: { "Content-Type": "application/json" }, + }, + ), + fetch( + `https://g.tenor.com/v1/trending?locale=${locale}&key=${apiKey}`, + { + agent, + method: "get", + headers: { "Content-Type": "application/json" }, + }, + ), + ]); + + const { tags } = + (await responseSource.json()) as TenorCategoriesResults; + const { results } = + (await trendGifSource.json()) as TenorTrendingResults; + + res.json({ + categories: tags.map((x) => ({ + name: x.searchterm, + src: x.image, + })), + gifs: [parseGifResult(results[0])], + }).status(200); + }, +); export default router; diff --git a/src/api/routes/guild-recommendations.ts b/src/api/routes/guild-recommendations.ts index 67f43c14..876780df 100644 --- a/src/api/routes/guild-recommendations.ts +++ b/src/api/routes/guild-recommendations.ts @@ -16,34 +16,44 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Guild, Config } from "@spacebar/util"; +import { Config, Guild } from "@spacebar/util"; -import { Router, Request, Response } from "express"; import { route } from "@spacebar/api"; +import { Request, Response, Router } from "express"; import { Like } from "typeorm"; const router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - // const { limit, personalization_disabled } = req.query; - const { limit } = req.query; - const showAllGuilds = Config.get().guild.discovery.showAllGuilds; +router.get( + "/", + route({ + responses: { + 200: { + body: "GuildRecommendationsResponse", + }, + }, + }), + async (req: Request, res: Response) => { + // const { limit, personalization_disabled } = req.query; + const { limit } = req.query; + const showAllGuilds = Config.get().guild.discovery.showAllGuilds; - const genLoadId = (size: number) => - [...Array(size)] - .map(() => Math.floor(Math.random() * 16).toString(16)) - .join(""); + const genLoadId = (size: number) => + [...Array(size)] + .map(() => Math.floor(Math.random() * 16).toString(16)) + .join(""); - const guilds = showAllGuilds - ? await Guild.find({ take: Math.abs(Number(limit || 24)) }) - : await Guild.find({ - where: { features: Like("%DISCOVERABLE%") }, - take: Math.abs(Number(limit || 24)), - }); - res.send({ - recommended_guilds: guilds, - load_id: `server_recs/${genLoadId(32)}`, - }).status(200); -}); + const guilds = showAllGuilds + ? await Guild.find({ take: Math.abs(Number(limit || 24)) }) + : await Guild.find({ + where: { features: Like("%DISCOVERABLE%") }, + take: Math.abs(Number(limit || 24)), + }); + res.send({ + recommended_guilds: guilds, + load_id: `server_recs/${genLoadId(32)}`, + }).status(200); + }, +); export default router; diff --git a/src/api/routes/guilds/#guild_id/bans.ts b/src/api/routes/guilds/#guild_id/bans.ts index 31aed6b9..0776ab62 100644 --- a/src/api/routes/guilds/#guild_id/bans.ts +++ b/src/api/routes/guilds/#guild_id/bans.ts @@ -16,20 +16,20 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Request, Response, Router } from "express"; +import { getIpAdress, route } from "@spacebar/api"; import { + Ban, + BanModeratorSchema, + BanRegistrySchema, DiscordApiErrors, - emitEvent, GuildBanAddEvent, GuildBanRemoveEvent, - Ban, - User, Member, - BanRegistrySchema, - BanModeratorSchema, + User, + emitEvent, } from "@spacebar/util"; +import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; -import { getIpAdress, route } from "@spacebar/api"; const router: Router = Router(); @@ -37,7 +37,17 @@ const router: Router = Router(); router.get( "/", - route({ permission: "BAN_MEMBERS" }), + route({ + permission: "BAN_MEMBERS", + responses: { + 200: { + body: "GuildBansResponse", + }, + 403: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const { guild_id } = req.params; @@ -73,7 +83,20 @@ router.get( router.get( "/:user", - route({ permission: "BAN_MEMBERS" }), + route({ + permission: "BAN_MEMBERS", + responses: { + 200: { + body: "BanModeratorSchema", + }, + 403: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const { guild_id } = req.params; const user_id = req.params.ban; @@ -97,7 +120,21 @@ router.get( router.put( "/:user_id", - route({ body: "BanCreateSchema", permission: "BAN_MEMBERS" }), + route({ + requestBody: "BanCreateSchema", + permission: "BAN_MEMBERS", + responses: { + 200: { + body: "Ban", + }, + 400: { + body: "APIErrorResponse", + }, + 403: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const { guild_id } = req.params; const banned_user_id = req.params.user_id; @@ -143,7 +180,20 @@ router.put( router.put( "/@me", - route({ body: "BanCreateSchema" }), + route({ + requestBody: "BanCreateSchema", + responses: { + 200: { + body: "Ban", + }, + 400: { + body: "APIErrorResponse", + }, + 403: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const { guild_id } = req.params; @@ -182,7 +232,18 @@ router.put( router.delete( "/:user_id", - route({ permission: "BAN_MEMBERS" }), + route({ + permission: "BAN_MEMBERS", + responses: { + 204: {}, + 403: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const { guild_id, user_id } = req.params; diff --git a/src/api/routes/guilds/#guild_id/channels.ts b/src/api/routes/guilds/#guild_id/channels.ts index d74d9f84..1d5897a5 100644 --- a/src/api/routes/guilds/#guild_id/channels.ts +++ b/src/api/routes/guilds/#guild_id/channels.ts @@ -16,28 +16,52 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Response, Request } from "express"; +import { route } from "@spacebar/api"; import { Channel, - ChannelUpdateEvent, - emitEvent, ChannelModifySchema, ChannelReorderSchema, + ChannelUpdateEvent, + emitEvent, } from "@spacebar/util"; +import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; -import { route } from "@spacebar/api"; const router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - const { guild_id } = req.params; - const channels = await Channel.find({ where: { guild_id } }); +router.get( + "/", + route({ + responses: { + 201: { + body: "APIChannelArray", + }, + }, + }), + async (req: Request, res: Response) => { + const { guild_id } = req.params; + const channels = await Channel.find({ where: { guild_id } }); - res.json(channels); -}); + res.json(channels); + }, +); router.post( "/", - route({ body: "ChannelModifySchema", permission: "MANAGE_CHANNELS" }), + route({ + requestBody: "ChannelModifySchema", + permission: "MANAGE_CHANNELS", + responses: { + 201: { + body: "Channel", + }, + 400: { + body: "APIErrorResponse", + }, + 403: { + body: "APIErrorResponse", + }, + }, + }), 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; @@ -54,7 +78,19 @@ router.post( router.patch( "/", - route({ body: "ChannelReorderSchema", permission: "MANAGE_CHANNELS" }), + route({ + requestBody: "ChannelReorderSchema", + permission: "MANAGE_CHANNELS", + responses: { + 204: {}, + 400: { + body: "APIErrorResponse", + }, + 403: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { // changes guild channel position const { guild_id } = req.params; diff --git a/src/api/routes/guilds/#guild_id/delete.ts b/src/api/routes/guilds/#guild_id/delete.ts index ec72a4ae..dee52c81 100644 --- a/src/api/routes/guilds/#guild_id/delete.ts +++ b/src/api/routes/guilds/#guild_id/delete.ts @@ -16,37 +16,51 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { emitEvent, GuildDeleteEvent, Guild } from "@spacebar/util"; -import { Router, Request, Response } from "express"; -import { HTTPError } from "lambert-server"; import { route } from "@spacebar/api"; +import { Guild, GuildDeleteEvent, emitEvent } from "@spacebar/util"; +import { Request, Response, Router } from "express"; +import { HTTPError } from "lambert-server"; 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("/", route({}), async (req: Request, res: Response) => { - const { guild_id } = req.params; +router.post( + "/", + route({ + responses: { + 204: {}, + 401: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { guild_id } = req.params; - const guild = await Guild.findOneOrFail({ - where: { id: guild_id }, - select: ["owner_id"], - }); - if (guild.owner_id !== req.user_id) - throw new HTTPError("You are not the owner of this guild", 401); + const guild = await Guild.findOneOrFail({ + where: { id: guild_id }, + select: ["owner_id"], + }); + if (guild.owner_id !== req.user_id) + throw new HTTPError("You are not the owner of this guild", 401); - await Promise.all([ - Guild.delete({ id: guild_id }), // this will also delete all guild related data - emitEvent({ - event: "GUILD_DELETE", - data: { - id: guild_id, - }, - guild_id: guild_id, - } as GuildDeleteEvent), - ]); + await Promise.all([ + Guild.delete({ id: guild_id }), // this will also delete all guild related data + emitEvent({ + event: "GUILD_DELETE", + data: { + id: guild_id, + }, + guild_id: guild_id, + } as GuildDeleteEvent), + ]); - return res.sendStatus(204); -}); + return res.sendStatus(204); + }, +); export default router; diff --git a/src/api/routes/guilds/#guild_id/discovery-requirements.ts b/src/api/routes/guilds/#guild_id/discovery-requirements.ts index 5e15676a..741fa9b3 100644 --- a/src/api/routes/guilds/#guild_id/discovery-requirements.ts +++ b/src/api/routes/guilds/#guild_id/discovery-requirements.ts @@ -16,40 +16,50 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Request, Response } from "express"; import { route } from "@spacebar/api"; +import { Request, Response, Router } from "express"; const router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - const { guild_id } = req.params; - // TODO: - // Load from database - // Admin control, but for now it allows anyone to be discoverable - - res.send({ - guild_id: guild_id, - safe_environment: true, - healthy: true, - health_score_pending: false, - size: true, - nsfw_properties: {}, - protected: true, - sufficient: true, - sufficient_without_grace_period: true, - valid_rules_channel: true, - retention_healthy: true, - engagement_healthy: true, - age: true, - minimum_age: 0, - health_score: { - avg_nonnew_participators: 0, - avg_nonnew_communicators: 0, - num_intentful_joiners: 0, - perc_ret_w1_intentful: 0, +router.get( + "/", + route({ + responses: { + 200: { + body: "GuildDiscoveryRequirementsResponse", + }, }, - minimum_size: 0, - }); -}); + }), + async (req: Request, res: Response) => { + const { guild_id } = req.params; + // TODO: + // Load from database + // Admin control, but for now it allows anyone to be discoverable + + res.send({ + guild_id: guild_id, + safe_environment: true, + healthy: true, + health_score_pending: false, + size: true, + nsfw_properties: {}, + protected: true, + sufficient: true, + sufficient_without_grace_period: true, + valid_rules_channel: true, + retention_healthy: true, + engagement_healthy: true, + age: true, + minimum_age: 0, + health_score: { + avg_nonnew_participators: 0, + avg_nonnew_communicators: 0, + num_intentful_joiners: 0, + perc_ret_w1_intentful: 0, + }, + minimum_size: 0, + }); + }, +); export default router; diff --git a/src/api/routes/guilds/#guild_id/emojis.ts b/src/api/routes/guilds/#guild_id/emojis.ts index c661202e..ef28f989 100644 --- a/src/api/routes/guilds/#guild_id/emojis.ts +++ b/src/api/routes/guilds/#guild_id/emojis.ts @@ -16,55 +16,95 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Request, Response } from "express"; +import { route } from "@spacebar/api"; import { Config, DiscordApiErrors, - emitEvent, Emoji, + EmojiCreateSchema, + EmojiModifySchema, GuildEmojisUpdateEvent, - handleFile, Member, Snowflake, User, - EmojiCreateSchema, - EmojiModifySchema, + emitEvent, + handleFile, } from "@spacebar/util"; -import { route } from "@spacebar/api"; +import { Request, Response, Router } from "express"; const router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - const { guild_id } = req.params; +router.get( + "/", + route({ + responses: { + 200: { + body: "APIEmojiArray", + }, + 403: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { guild_id } = req.params; - await Member.IsInGuildOrFail(req.user_id, guild_id); + await Member.IsInGuildOrFail(req.user_id, guild_id); - const emojis = await Emoji.find({ - where: { guild_id: guild_id }, - relations: ["user"], - }); + const emojis = await Emoji.find({ + where: { guild_id: guild_id }, + relations: ["user"], + }); - return res.json(emojis); -}); + return res.json(emojis); + }, +); -router.get("/:emoji_id", route({}), async (req: Request, res: Response) => { - const { guild_id, emoji_id } = req.params; +router.get( + "/:emoji_id", + route({ + responses: { + 200: { + body: "Emoji", + }, + 403: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { guild_id, emoji_id } = req.params; - await Member.IsInGuildOrFail(req.user_id, guild_id); + await Member.IsInGuildOrFail(req.user_id, guild_id); - const emoji = await Emoji.findOneOrFail({ - where: { guild_id: guild_id, id: emoji_id }, - relations: ["user"], - }); + const emoji = await Emoji.findOneOrFail({ + where: { guild_id: guild_id, id: emoji_id }, + relations: ["user"], + }); - return res.json(emoji); -}); + return res.json(emoji); + }, +); router.post( "/", route({ - body: "EmojiCreateSchema", + requestBody: "EmojiCreateSchema", permission: "MANAGE_EMOJIS_AND_STICKERS", + responses: { + 201: { + body: "Emoji", + }, + 403: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, }), async (req: Request, res: Response) => { const { guild_id } = req.params; @@ -113,8 +153,16 @@ router.post( router.patch( "/:emoji_id", route({ - body: "EmojiModifySchema", + requestBody: "EmojiModifySchema", permission: "MANAGE_EMOJIS_AND_STICKERS", + responses: { + 200: { + body: "Emoji", + }, + 403: { + body: "APIErrorResponse", + }, + }, }), async (req: Request, res: Response) => { const { emoji_id, guild_id } = req.params; @@ -141,7 +189,15 @@ router.patch( router.delete( "/:emoji_id", - route({ permission: "MANAGE_EMOJIS_AND_STICKERS" }), + route({ + permission: "MANAGE_EMOJIS_AND_STICKERS", + responses: { + 204: {}, + 403: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const { emoji_id, guild_id } = req.params; diff --git a/src/api/routes/guilds/#guild_id/index.ts b/src/api/routes/guilds/#guild_id/index.ts index 672bc92e..6feb0a83 100644 --- a/src/api/routes/guilds/#guild_id/index.ts +++ b/src/api/routes/guilds/#guild_id/index.ts @@ -16,46 +16,81 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; import { + Channel, DiscordApiErrors, - emitEvent, - getPermission, - getRights, Guild, GuildUpdateEvent, - handleFile, - Member, GuildUpdateSchema, + Member, + Permissions, SpacebarApiErrors, + emitEvent, + getPermission, + getRights, + handleFile, } from "@spacebar/util"; +import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; -import { route } from "@spacebar/api"; const router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - const { guild_id } = req.params; - - const [guild, member] = await Promise.all([ - Guild.findOneOrFail({ where: { id: guild_id } }), - Member.findOne({ where: { guild_id: guild_id, id: req.user_id } }), - ]); - if (!member) - throw new HTTPError( - "You are not a member of the guild you are trying to access", - 401, - ); - - return res.send({ - ...guild, - joined_at: member?.joined_at, - }); -}); +router.get( + "/", + route({ + responses: { + "200": { + body: "APIGuildWithJoinedAt", + }, + 401: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { guild_id } = req.params; + + const [guild, member] = await Promise.all([ + Guild.findOneOrFail({ where: { id: guild_id } }), + Member.findOne({ where: { guild_id: guild_id, id: req.user_id } }), + ]); + if (!member) + throw new HTTPError( + "You are not a member of the guild you are trying to access", + 401, + ); + + return res.send({ + ...guild, + joined_at: member?.joined_at, + }); + }, +); router.patch( "/", - route({ body: "GuildUpdateSchema", permission: "MANAGE_GUILD" }), + route({ + requestBody: "GuildUpdateSchema", + permission: "MANAGE_GUILD", + responses: { + "200": { + body: "GuildUpdateSchema", + }, + 401: { + body: "APIErrorResponse", + }, + 403: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const body = req.body as GuildUpdateSchema; const { guild_id } = req.params; @@ -122,13 +157,83 @@ router.patch( guild.features = body.features; } + if (body.public_updates_channel_id == "1") { + // move all channels up 1 + await Channel.createQueryBuilder("channels") + .where({ guild: { id: guild_id } }) + .update({ position: () => "position + 1" }) + .execute(); + + // create an updates channel for them + await Channel.createChannel( + { + name: "moderator-only", + guild_id: guild.id, + position: 0, + type: 0, + permission_overwrites: [ + // remove SEND_MESSAGES from @everyone + { + id: guild.id, + allow: "0", + deny: Permissions.FLAGS.VIEW_CHANNEL.toString(), + type: 0, + }, + ], + }, + undefined, + { skipPermissionCheck: true }, + ); + } else if (body.public_updates_channel_id != undefined) { + // ensure channel exists in this guild + await Channel.findOneOrFail({ + where: { guild_id, id: body.public_updates_channel_id }, + select: { id: true }, + }); + } + + if (body.rules_channel_id == "1") { + // move all channels up 1 + await Channel.createQueryBuilder("channels") + .where({ guild: { id: guild_id } }) + .update({ position: () => "position + 1" }) + .execute(); + + // create a rules for them + await Channel.createChannel( + { + name: "rules", + guild_id: guild.id, + position: 0, + type: 0, + permission_overwrites: [ + // remove SEND_MESSAGES from @everyone + { + id: guild.id, + allow: "0", + deny: Permissions.FLAGS.SEND_MESSAGES.toString(), + type: 0, + }, + ], + }, + undefined, + { skipPermissionCheck: true }, + ); + } else if (body.rules_channel_id != undefined) { + // ensure channel exists in this guild + await Channel.findOneOrFail({ + where: { guild_id, id: body.rules_channel_id }, + select: { id: true }, + }); + } + // TODO: check if body ids are valid guild.assign(body); const data = guild.toJSON(); // TODO: guild hashes // TODO: fix vanity_url_code, template_id - delete data.vanity_url_code; + // delete data.vanity_url_code; delete data.template_id; await Promise.all([ diff --git a/src/api/routes/guilds/#guild_id/invites.ts b/src/api/routes/guilds/#guild_id/invites.ts index 9c446928..a0ffa3f4 100644 --- a/src/api/routes/guilds/#guild_id/invites.ts +++ b/src/api/routes/guilds/#guild_id/invites.ts @@ -16,15 +16,22 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Invite, PublicInviteRelation } from "@spacebar/util"; import { route } from "@spacebar/api"; +import { Invite, PublicInviteRelation } from "@spacebar/util"; import { Request, Response, Router } from "express"; const router = Router(); router.get( "/", - route({ permission: "MANAGE_GUILD" }), + route({ + permission: "MANAGE_GUILD", + responses: { + 200: { + body: "APIInviteArray", + }, + }, + }), async (req: Request, res: Response) => { const { guild_id } = req.params; diff --git a/src/api/routes/guilds/#guild_id/member-verification.ts b/src/api/routes/guilds/#guild_id/member-verification.ts index 242f3684..2c39093e 100644 --- a/src/api/routes/guilds/#guild_id/member-verification.ts +++ b/src/api/routes/guilds/#guild_id/member-verification.ts @@ -16,17 +16,27 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Request, Response } from "express"; import { route } from "@spacebar/api"; +import { Request, Response, Router } from "express"; const router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - // TODO: member verification +router.get( + "/", + route({ + responses: { + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + // TODO: member verification - res.status(404).json({ - message: "Unknown Guild Member Verification Form", - code: 10068, - }); -}); + res.status(404).json({ + message: "Unknown Guild Member Verification Form", + code: 10068, + }); + }, +); export default router; diff --git a/src/api/routes/guilds/#guild_id/members/#member_id/index.ts b/src/api/routes/guilds/#guild_id/members/#member_id/index.ts index a14691f2..cafb922e 100644 --- a/src/api/routes/guilds/#guild_id/members/#member_id/index.ts +++ b/src/api/routes/guilds/#guild_id/members/#member_id/index.ts @@ -16,38 +16,91 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; import { - Member, - getPermission, - getRights, - Role, - GuildMemberUpdateEvent, emitEvent, - Sticker, Emoji, + getPermission, + getRights, Guild, + GuildMemberUpdateEvent, handleFile, + Member, MemberChangeSchema, + PublicMemberProjection, + PublicUserProjection, + Role, + Sticker, } from "@spacebar/util"; -import { route } from "@spacebar/api"; +import { Request, Response, Router } from "express"; const router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - const { guild_id, member_id } = req.params; - await Member.IsInGuildOrFail(req.user_id, guild_id); +router.get( + "/", + route({ + responses: { + 200: { + body: "APIPublicMember", + }, + 403: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { guild_id, member_id } = req.params; + await Member.IsInGuildOrFail(req.user_id, guild_id); - const member = await Member.findOneOrFail({ - where: { id: member_id, guild_id }, - }); + const member = await Member.findOneOrFail({ + where: { id: member_id, guild_id }, + relations: ["roles", "user"], + select: { + index: true, + // only grab public member props + ...Object.fromEntries( + PublicMemberProjection.map((x) => [x, true]), + ), + // and public user props + user: Object.fromEntries( + PublicUserProjection.map((x) => [x, true]), + ), + roles: { + id: true, + }, + }, + }); - return res.json(member); -}); + return res.json({ + ...member.toPublicMember(), + user: member.user.toPublicUser(), + roles: member.roles.map((x) => x.id), + }); + }, +); router.patch( "/", - route({ body: "MemberChangeSchema" }), + route({ + requestBody: "MemberChangeSchema", + responses: { + 200: { + body: "Member", + }, + 400: { + body: "APIErrorResponse", + }, + 403: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const { guild_id } = req.params; const member_id = @@ -119,54 +172,81 @@ router.patch( }, ); -router.put("/", route({}), async (req: Request, res: Response) => { - // TODO: Lurker mode - - const rights = await getRights(req.user_id); - - const { guild_id } = req.params; - let { member_id } = req.params; - if (member_id === "@me") { - member_id = req.user_id; - rights.hasThrow("JOIN_GUILDS"); - } else { - // TODO: join others by controller - } - - const guild = await Guild.findOneOrFail({ - where: { id: guild_id }, - }); - - const emoji = await Emoji.find({ - where: { guild_id: guild_id }, - }); - - const roles = await Role.find({ - where: { guild_id: guild_id }, - }); - - const stickers = await Sticker.find({ - where: { guild_id: guild_id }, - }); - - await Member.addToGuild(member_id, guild_id); - res.send({ ...guild, emojis: emoji, roles: roles, stickers: stickers }); -}); - -router.delete("/", route({}), async (req: Request, res: Response) => { - const { guild_id, member_id } = req.params; - const permission = await getPermission(req.user_id, guild_id); - const rights = await getRights(req.user_id); - if (member_id === "@me" || member_id === req.user_id) { - // TODO: unless force-joined - rights.hasThrow("SELF_LEAVE_GROUPS"); - } else { - rights.hasThrow("KICK_BAN_MEMBERS"); - permission.hasThrow("KICK_MEMBERS"); - } - - await Member.removeFromGuild(member_id, guild_id); - res.sendStatus(204); -}); +router.put( + "/", + route({ + responses: { + 200: { + body: "MemberJoinGuildResponse", + }, + 403: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + // TODO: Lurker mode + + const rights = await getRights(req.user_id); + + const { guild_id } = req.params; + let { member_id } = req.params; + if (member_id === "@me") { + member_id = req.user_id; + rights.hasThrow("JOIN_GUILDS"); + } else { + // TODO: join others by controller + } + + const guild = await Guild.findOneOrFail({ + where: { id: guild_id }, + }); + + const emoji = await Emoji.find({ + where: { guild_id: guild_id }, + }); + + const roles = await Role.find({ + where: { guild_id: guild_id }, + }); + + const stickers = await Sticker.find({ + where: { guild_id: guild_id }, + }); + + await Member.addToGuild(member_id, guild_id); + res.send({ ...guild, emojis: emoji, roles: roles, stickers: stickers }); + }, +); + +router.delete( + "/", + route({ + responses: { + 204: {}, + 403: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { guild_id, member_id } = req.params; + const permission = await getPermission(req.user_id, guild_id); + const rights = await getRights(req.user_id); + if (member_id === "@me" || member_id === req.user_id) { + // TODO: unless force-joined + rights.hasThrow("SELF_LEAVE_GROUPS"); + } else { + rights.hasThrow("KICK_BAN_MEMBERS"); + permission.hasThrow("KICK_MEMBERS"); + } + + await Member.removeFromGuild(member_id, guild_id); + res.sendStatus(204); + }, +); export default router; diff --git a/src/api/routes/guilds/#guild_id/members/#member_id/nick.ts b/src/api/routes/guilds/#guild_id/members/#member_id/nick.ts index 14e7467f..7b8e44d3 100644 --- a/src/api/routes/guilds/#guild_id/members/#member_id/nick.ts +++ b/src/api/routes/guilds/#guild_id/members/#member_id/nick.ts @@ -16,15 +16,26 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { getPermission, Member, PermissionResolvable } from "@spacebar/util"; import { route } from "@spacebar/api"; +import { getPermission, Member, PermissionResolvable } from "@spacebar/util"; import { Request, Response, Router } from "express"; const router = Router(); router.patch( "/", - route({ body: "MemberNickChangeSchema" }), + route({ + requestBody: "MemberNickChangeSchema", + responses: { + 200: {}, + 400: { + body: "APIErrorResponse", + }, + 403: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const { guild_id } = req.params; let permissionString: PermissionResolvable = "MANAGE_NICKNAMES"; diff --git a/src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts b/src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts index 698df88f..46dd70bb 100644 --- a/src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts +++ b/src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts @@ -16,15 +16,23 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Member } from "@spacebar/util"; import { route } from "@spacebar/api"; +import { Member } from "@spacebar/util"; import { Request, Response, Router } from "express"; const router = Router(); router.delete( "/", - route({ permission: "MANAGE_ROLES" }), + route({ + permission: "MANAGE_ROLES", + responses: { + 204: {}, + 403: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const { guild_id, role_id, member_id } = req.params; @@ -35,7 +43,13 @@ router.delete( router.put( "/", - route({ permission: "MANAGE_ROLES" }), + route({ + permission: "MANAGE_ROLES", + responses: { + 204: {}, + 403: {}, + }, + }), async (req: Request, res: Response) => { const { guild_id, role_id, member_id } = req.params; diff --git a/src/api/routes/guilds/#guild_id/members/index.ts b/src/api/routes/guilds/#guild_id/members/index.ts index f7a55cf1..9260308d 100644 --- a/src/api/routes/guilds/#guild_id/members/index.ts +++ b/src/api/routes/guilds/#guild_id/members/index.ts @@ -16,35 +16,58 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Request, Response, Router } from "express"; -import { Member, PublicMemberProjection } from "@spacebar/util"; import { route } from "@spacebar/api"; -import { MoreThan } from "typeorm"; +import { Member, PublicMemberProjection } from "@spacebar/util"; +import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; +import { MoreThan } from "typeorm"; const router = Router(); // TODO: send over websocket // TODO: check for GUILD_MEMBERS intent -router.get("/", route({}), async (req: Request, res: Response) => { - const { guild_id } = req.params; - const limit = Number(req.query.limit) || 1; - if (limit > 1000 || limit < 1) - throw new HTTPError("Limit must be between 1 and 1000"); - const after = `${req.query.after}`; - const query = after ? { id: MoreThan(after) } : {}; - - await Member.IsInGuildOrFail(req.user_id, guild_id); - - const members = await Member.find({ - where: { guild_id, ...query }, - select: PublicMemberProjection, - take: limit, - order: { id: "ASC" }, - }); - - return res.json(members); -}); +router.get( + "/", + route({ + query: { + limit: { + type: "number", + description: + "max number of members to return (1-1000). default 1", + }, + after: { + type: "string", + }, + }, + responses: { + 200: { + body: "APIMemberArray", + }, + 403: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { guild_id } = req.params; + const limit = Number(req.query.limit) || 1; + if (limit > 1000 || limit < 1) + throw new HTTPError("Limit must be between 1 and 1000"); + const after = `${req.query.after}`; + const query = after ? { id: MoreThan(after) } : {}; + + await Member.IsInGuildOrFail(req.user_id, guild_id); + + const members = await Member.find({ + where: { guild_id, ...query }, + select: PublicMemberProjection, + take: limit, + order: { id: "ASC" }, + }); + + return res.json(members); + }, +); export default router; diff --git a/src/api/routes/guilds/#guild_id/messages/search.ts b/src/api/routes/guilds/#guild_id/messages/search.ts index bc5f1b6e..637d1e43 100644 --- a/src/api/routes/guilds/#guild_id/messages/search.ts +++ b/src/api/routes/guilds/#guild_id/messages/search.ts @@ -18,140 +18,159 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import { Request, Response, Router } from "express"; import { route } from "@spacebar/api"; -import { getPermission, FieldErrors, Message, Channel } from "@spacebar/util"; +import { Channel, FieldErrors, Message, getPermission } from "@spacebar/util"; +import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; import { FindManyOptions, In, Like } from "typeorm"; const router: Router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - const { - channel_id, - content, - // include_nsfw, // TODO - offset, - sort_order, - // sort_by, // TODO: Handle 'relevance' - limit, - author_id, - } = req.query; +router.get( + "/", + route({ + responses: { + 200: { + body: "GuildMessagesSearchResponse", + }, + 403: { + body: "APIErrorResponse", + }, + 422: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { + channel_id, + content, + // include_nsfw, // TODO + offset, + sort_order, + // sort_by, // TODO: Handle 'relevance' + limit, + author_id, + } = req.query; - const parsedLimit = Number(limit) || 50; - if (parsedLimit < 1 || parsedLimit > 100) - throw new HTTPError("limit must be between 1 and 100", 422); + const parsedLimit = Number(limit) || 50; + if (parsedLimit < 1 || parsedLimit > 100) + throw new HTTPError("limit must be between 1 and 100", 422); - if (sort_order) { - if ( - typeof sort_order != "string" || - ["desc", "asc"].indexOf(sort_order) == -1 - ) - throw FieldErrors({ - sort_order: { - message: "Value must be one of ('desc', 'asc').", - code: "BASE_TYPE_CHOICES", - }, - }); // todo this is wrong - } + if (sort_order) { + if ( + typeof sort_order != "string" || + ["desc", "asc"].indexOf(sort_order) == -1 + ) + throw FieldErrors({ + sort_order: { + message: "Value must be one of ('desc', 'asc').", + code: "BASE_TYPE_CHOICES", + }, + }); // todo this is wrong + } - const permissions = await getPermission( - req.user_id, - req.params.guild_id, - channel_id as string | undefined, - ); - permissions.hasThrow("VIEW_CHANNEL"); - if (!permissions.has("READ_MESSAGE_HISTORY")) - return res.json({ messages: [], total_results: 0 }); + const permissions = await getPermission( + req.user_id, + req.params.guild_id, + channel_id as string | undefined, + ); + permissions.hasThrow("VIEW_CHANNEL"); + if (!permissions.has("READ_MESSAGE_HISTORY")) + return res.json({ messages: [], total_results: 0 }); - const query: FindManyOptions<Message> = { - order: { - timestamp: sort_order - ? (sort_order.toUpperCase() as "ASC" | "DESC") - : "DESC", - }, - take: parsedLimit || 0, - where: { - guild: { - id: req.params.guild_id, + const query: FindManyOptions<Message> = { + order: { + timestamp: sort_order + ? (sort_order.toUpperCase() as "ASC" | "DESC") + : "DESC", }, - }, - relations: [ - "author", - "webhook", - "application", - "mentions", - "mention_roles", - "mention_channels", - "sticker_items", - "attachments", - ], - skip: offset ? Number(offset) : 0, - }; - //@ts-ignore - if (channel_id) query.where.channel = { id: channel_id }; - else { - // get all channel IDs that this user can access - const channels = await Channel.find({ - where: { guild_id: req.params.guild_id }, - select: ["id"], - }); - const ids = []; + take: parsedLimit || 0, + where: { + guild: { + id: req.params.guild_id, + }, + }, + relations: [ + "author", + "webhook", + "application", + "mentions", + "mention_roles", + "mention_channels", + "sticker_items", + "attachments", + ], + skip: offset ? Number(offset) : 0, + }; + //@ts-ignore + if (channel_id) query.where.channel = { id: channel_id }; + else { + // get all channel IDs that this user can access + const channels = await Channel.find({ + where: { guild_id: req.params.guild_id }, + select: ["id"], + }); + const ids = []; - for (const channel of channels) { - const perm = await getPermission( - req.user_id, - req.params.guild_id, - channel.id, - ); - if (!perm.has("VIEW_CHANNEL") || !perm.has("READ_MESSAGE_HISTORY")) - continue; - ids.push(channel.id); - } + for (const channel of channels) { + const perm = await getPermission( + req.user_id, + req.params.guild_id, + channel.id, + ); + if ( + !perm.has("VIEW_CHANNEL") || + !perm.has("READ_MESSAGE_HISTORY") + ) + continue; + ids.push(channel.id); + } + //@ts-ignore + query.where.channel = { id: In(ids) }; + } + //@ts-ignore + if (author_id) query.where.author = { id: author_id }; //@ts-ignore - query.where.channel = { id: In(ids) }; - } - //@ts-ignore - if (author_id) query.where.author = { id: author_id }; - //@ts-ignore - if (content) query.where.content = Like(`%${content}%`); + if (content) query.where.content = Like(`%${content}%`); - const messages: Message[] = await Message.find(query); + const messages: Message[] = await Message.find(query); - const messagesDto = messages.map((x) => [ - { - id: x.id, - type: x.type, - content: x.content, - channel_id: x.channel_id, - author: { - id: x.author?.id, - username: x.author?.username, - avatar: x.author?.avatar, - avatar_decoration: null, - discriminator: x.author?.discriminator, - public_flags: x.author?.public_flags, + const messagesDto = messages.map((x) => [ + { + id: x.id, + type: x.type, + content: x.content, + channel_id: x.channel_id, + author: { + id: x.author?.id, + username: x.author?.username, + avatar: x.author?.avatar, + avatar_decoration: null, + discriminator: x.author?.discriminator, + public_flags: x.author?.public_flags, + }, + attachments: x.attachments, + embeds: x.embeds, + mentions: x.mentions, + mention_roles: x.mention_roles, + pinned: x.pinned, + mention_everyone: x.mention_everyone, + tts: x.tts, + timestamp: x.timestamp, + edited_timestamp: x.edited_timestamp, + flags: x.flags, + components: x.components, + hit: true, }, - attachments: x.attachments, - embeds: x.embeds, - mentions: x.mentions, - mention_roles: x.mention_roles, - pinned: x.pinned, - mention_everyone: x.mention_everyone, - tts: x.tts, - timestamp: x.timestamp, - edited_timestamp: x.edited_timestamp, - flags: x.flags, - components: x.components, - hit: true, - }, - ]); + ]); - return res.json({ - messages: messagesDto, - total_results: messages.length, - }); -}); + return res.json({ + messages: messagesDto, + total_results: messages.length, + }); + }, +); export default router; diff --git a/src/api/routes/guilds/#guild_id/profile/index.ts b/src/api/routes/guilds/#guild_id/profile/index.ts index 8ec22ea4..60526259 100644 --- a/src/api/routes/guilds/#guild_id/profile/index.ts +++ b/src/api/routes/guilds/#guild_id/profile/index.ts @@ -31,7 +31,20 @@ const router = Router(); router.patch( "/:member_id", - route({ body: "MemberChangeProfileSchema" }), + route({ + requestBody: "MemberChangeProfileSchema", + responses: { + 200: { + body: "Member", + }, + 400: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const { guild_id } = req.params; // const member_id = diff --git a/src/api/routes/guilds/#guild_id/prune.ts b/src/api/routes/guilds/#guild_id/prune.ts index dbed546b..2c77340d 100644 --- a/src/api/routes/guilds/#guild_id/prune.ts +++ b/src/api/routes/guilds/#guild_id/prune.ts @@ -16,14 +16,14 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Request, Response } from "express"; -import { Guild, Member, Snowflake } from "@spacebar/util"; -import { LessThan, IsNull } from "typeorm"; import { route } from "@spacebar/api"; +import { Guild, Member, Snowflake } from "@spacebar/util"; +import { Request, Response, Router } from "express"; +import { IsNull, LessThan } from "typeorm"; const router = Router(); //Returns all inactive members, respecting role hierarchy -export const inactiveMembers = async ( +const inactiveMembers = async ( guild_id: string, user_id: string, days: number, @@ -80,25 +80,46 @@ export const inactiveMembers = async ( return members; }; -router.get("/", route({}), async (req: Request, res: Response) => { - const days = parseInt(req.query.days as string); +router.get( + "/", + route({ + responses: { + "200": { + body: "GuildPruneResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const days = parseInt(req.query.days as string); - let roles = req.query.include_roles; - if (typeof roles === "string") roles = [roles]; //express will return array otherwise + let roles = req.query.include_roles; + if (typeof roles === "string") roles = [roles]; //express will return array otherwise - const members = await inactiveMembers( - req.params.guild_id, - req.user_id, - days, - roles as string[], - ); + const members = await inactiveMembers( + req.params.guild_id, + req.user_id, + days, + roles as string[], + ); - res.send({ pruned: members.length }); -}); + res.send({ pruned: members.length }); + }, +); router.post( "/", - route({ permission: "KICK_MEMBERS", right: "KICK_BAN_MEMBERS" }), + route({ + permission: "KICK_MEMBERS", + right: "KICK_BAN_MEMBERS", + responses: { + 200: { + body: "GuildPurgeResponse", + }, + 403: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const days = parseInt(req.body.days); diff --git a/src/api/routes/guilds/#guild_id/regions.ts b/src/api/routes/guilds/#guild_id/regions.ts index de1e8769..b0ae0602 100644 --- a/src/api/routes/guilds/#guild_id/regions.ts +++ b/src/api/routes/guilds/#guild_id/regions.ts @@ -16,22 +16,35 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ +import { getIpAdress, getVoiceRegions, route } from "@spacebar/api"; import { Guild } from "@spacebar/util"; import { Request, Response, Router } from "express"; -import { getVoiceRegions, route, getIpAdress } from "@spacebar/api"; const router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - const { guild_id } = req.params; - const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); - //TODO we should use an enum for guild's features and not hardcoded strings - return res.json( - await getVoiceRegions( - getIpAdress(req), - guild.features.includes("VIP_REGIONS"), - ), - ); -}); +router.get( + "/", + route({ + responses: { + 200: { + body: "APIGuildVoiceRegion", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { guild_id } = req.params; + const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); + //TODO we should use an enum for guild's features and not hardcoded strings + return res.json( + await getVoiceRegions( + getIpAdress(req), + guild.features.includes("VIP_REGIONS"), + ), + ); + }, +); export default router; diff --git a/src/api/routes/guilds/#guild_id/roles/#role_id/index.ts b/src/api/routes/guilds/#guild_id/roles/#role_id/index.ts index de3fc35b..ea1a782a 100644 --- a/src/api/routes/guilds/#guild_id/roles/#role_id/index.ts +++ b/src/api/routes/guilds/#guild_id/roles/#role_id/index.ts @@ -16,31 +16,63 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Request, Response } from "express"; +import { route } from "@spacebar/api"; import { - Role, - Member, - GuildRoleUpdateEvent, - GuildRoleDeleteEvent, emitEvent, + GuildRoleDeleteEvent, + GuildRoleUpdateEvent, handleFile, + Member, + Role, RoleModifySchema, } from "@spacebar/util"; -import { route } from "@spacebar/api"; +import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; const router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - const { guild_id, role_id } = req.params; - await Member.IsInGuildOrFail(req.user_id, guild_id); - const role = await Role.findOneOrFail({ where: { guild_id, id: role_id } }); - return res.json(role); -}); +router.get( + "/", + route({ + responses: { + 200: { + body: "Role", + }, + 403: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { guild_id, role_id } = req.params; + await Member.IsInGuildOrFail(req.user_id, guild_id); + const role = await Role.findOneOrFail({ + where: { guild_id, id: role_id }, + }); + return res.json(role); + }, +); router.delete( "/", - route({ permission: "MANAGE_ROLES" }), + route({ + permission: "MANAGE_ROLES", + responses: { + 204: {}, + 400: { + body: "APIErrorResponse", + }, + 403: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const { guild_id, role_id } = req.params; if (role_id === guild_id) @@ -69,7 +101,24 @@ router.delete( router.patch( "/", - route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }), + route({ + requestBody: "RoleModifySchema", + permission: "MANAGE_ROLES", + responses: { + 200: { + body: "Role", + }, + 400: { + body: "APIErrorResponse", + }, + 403: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const { role_id, guild_id } = req.params; const body = req.body as RoleModifySchema; diff --git a/src/api/routes/guilds/#guild_id/roles/index.ts b/src/api/routes/guilds/#guild_id/roles/index.ts index f93e9385..e2c34e7f 100644 --- a/src/api/routes/guilds/#guild_id/roles/index.ts +++ b/src/api/routes/guilds/#guild_id/roles/index.ts @@ -16,21 +16,20 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; import { - Role, - getPermission, - Member, - GuildRoleCreateEvent, - GuildRoleUpdateEvent, - emitEvent, Config, DiscordApiErrors, + emitEvent, + GuildRoleCreateEvent, + GuildRoleUpdateEvent, + Member, + Role, RoleModifySchema, RolePositionUpdateSchema, Snowflake, } from "@spacebar/util"; -import { route } from "@spacebar/api"; +import { Request, Response, Router } from "express"; import { Not } from "typeorm"; const router: Router = Router(); @@ -47,7 +46,21 @@ router.get("/", route({}), async (req: Request, res: Response) => { router.post( "/", - route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }), + route({ + requestBody: "RoleModifySchema", + permission: "MANAGE_ROLES", + responses: { + 200: { + body: "Role", + }, + 400: { + body: "APIErrorResponse", + }, + 403: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const guild_id = req.params.guild_id; const body = req.body as RoleModifySchema; @@ -104,14 +117,25 @@ router.post( router.patch( "/", - route({ body: "RolePositionUpdateSchema" }), + route({ + requestBody: "RolePositionUpdateSchema", + permission: "MANAGE_ROLES", + responses: { + 200: { + body: "APIRoleArray", + }, + 400: { + body: "APIErrorResponse", + }, + 403: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const { guild_id } = req.params; const body = req.body as RolePositionUpdateSchema; - const perms = await getPermission(req.user_id, guild_id); - perms.hasThrow("MANAGE_ROLES"); - await Promise.all( body.map(async (x) => Role.update({ guild_id, id: x.id }, { position: x.position }), diff --git a/src/api/routes/guilds/#guild_id/stickers.ts b/src/api/routes/guilds/#guild_id/stickers.ts index 84a23670..88f9a40e 100644 --- a/src/api/routes/guilds/#guild_id/stickers.ts +++ b/src/api/routes/guilds/#guild_id/stickers.ts @@ -16,29 +16,42 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ +import { route } from "@spacebar/api"; import { - emitEvent, GuildStickersUpdateEvent, Member, + ModifyGuildStickerSchema, Snowflake, Sticker, StickerFormatType, StickerType, + emitEvent, uploadFile, - ModifyGuildStickerSchema, } from "@spacebar/util"; -import { Router, Request, Response } from "express"; -import { route } from "@spacebar/api"; -import multer from "multer"; +import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; +import multer from "multer"; const router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - const { guild_id } = req.params; - await Member.IsInGuildOrFail(req.user_id, guild_id); +router.get( + "/", + route({ + responses: { + 200: { + body: "APIStickerArray", + }, + 403: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { guild_id } = req.params; + await Member.IsInGuildOrFail(req.user_id, guild_id); - res.json(await Sticker.find({ where: { guild_id } })); -}); + res.json(await Sticker.find({ where: { guild_id } })); + }, +); const bodyParser = multer({ limits: { @@ -54,7 +67,18 @@ router.post( bodyParser, route({ permission: "MANAGE_EMOJIS_AND_STICKERS", - body: "ModifyGuildStickerSchema", + requestBody: "ModifyGuildStickerSchema", + responses: { + 200: { + body: "Sticker", + }, + 400: { + body: "APIErrorResponse", + }, + 403: { + body: "APIErrorResponse", + }, + }, }), async (req: Request, res: Response) => { if (!req.file) throw new HTTPError("missing file"); @@ -81,7 +105,7 @@ router.post( }, ); -export function getStickerFormat(mime_type: string) { +function getStickerFormat(mime_type: string) { switch (mime_type) { case "image/apng": return StickerFormatType.APNG; @@ -98,20 +122,46 @@ export function getStickerFormat(mime_type: string) { } } -router.get("/:sticker_id", route({}), async (req: Request, res: Response) => { - const { guild_id, sticker_id } = req.params; - await Member.IsInGuildOrFail(req.user_id, guild_id); +router.get( + "/:sticker_id", + route({ + responses: { + 200: { + body: "Sticker", + }, + 403: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { guild_id, sticker_id } = req.params; + await Member.IsInGuildOrFail(req.user_id, guild_id); - res.json( - await Sticker.findOneOrFail({ where: { guild_id, id: sticker_id } }), - ); -}); + res.json( + await Sticker.findOneOrFail({ + where: { guild_id, id: sticker_id }, + }), + ); + }, +); router.patch( "/:sticker_id", route({ - body: "ModifyGuildStickerSchema", + requestBody: "ModifyGuildStickerSchema", permission: "MANAGE_EMOJIS_AND_STICKERS", + responses: { + 200: { + body: "Sticker", + }, + 400: { + body: "APIErrorResponse", + }, + 403: { + body: "APIErrorResponse", + }, + }, }), async (req: Request, res: Response) => { const { guild_id, sticker_id } = req.params; @@ -141,7 +191,15 @@ async function sendStickerUpdateEvent(guild_id: string) { router.delete( "/:sticker_id", - route({ permission: "MANAGE_EMOJIS_AND_STICKERS" }), + route({ + permission: "MANAGE_EMOJIS_AND_STICKERS", + responses: { + 204: {}, + 403: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const { guild_id, sticker_id } = req.params; diff --git a/src/api/routes/guilds/#guild_id/templates.ts b/src/api/routes/guilds/#guild_id/templates.ts index 3bd28e05..85ae0ac9 100644 --- a/src/api/routes/guilds/#guild_id/templates.ts +++ b/src/api/routes/guilds/#guild_id/templates.ts @@ -16,11 +16,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Request, Response, Router } from "express"; +import { generateCode, route } from "@spacebar/api"; import { Guild, Template } from "@spacebar/util"; +import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; -import { route } from "@spacebar/api"; -import { generateCode } from "@spacebar/api"; const router: Router = Router(); @@ -41,19 +40,46 @@ const TemplateGuildProjection: (keyof Guild)[] = [ "icon", ]; -router.get("/", route({}), async (req: Request, res: Response) => { - const { guild_id } = req.params; +router.get( + "/", + route({ + responses: { + 200: { + body: "APITemplateArray", + }, + }, + }), + async (req: Request, res: Response) => { + const { guild_id } = req.params; - const templates = await Template.find({ - where: { source_guild_id: guild_id }, - }); + const templates = await Template.find({ + where: { source_guild_id: guild_id }, + }); - return res.json(templates); -}); + return res.json(templates); + }, +); router.post( "/", - route({ body: "TemplateCreateSchema", permission: "MANAGE_GUILD" }), + route({ + requestBody: "TemplateCreateSchema", + permission: "MANAGE_GUILD", + responses: { + 200: { + body: "Template", + }, + 400: { + body: "APIErrorResponse", + }, + 403: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const { guild_id } = req.params; const guild = await Guild.findOneOrFail({ @@ -81,7 +107,13 @@ router.post( router.delete( "/:code", - route({ permission: "MANAGE_GUILD" }), + route({ + permission: "MANAGE_GUILD", + responses: { + 200: { body: "Template" }, + 403: { body: "APIErrorResponse" }, + }, + }), async (req: Request, res: Response) => { const { code, guild_id } = req.params; @@ -96,7 +128,13 @@ router.delete( router.put( "/:code", - route({ permission: "MANAGE_GUILD" }), + route({ + permission: "MANAGE_GUILD", + responses: { + 200: { body: "Template" }, + 403: { body: "APIErrorResponse" }, + }, + }), async (req: Request, res: Response) => { const { code, guild_id } = req.params; const guild = await Guild.findOneOrFail({ @@ -115,7 +153,14 @@ router.put( router.patch( "/:code", - route({ body: "TemplateModifySchema", permission: "MANAGE_GUILD" }), + route({ + requestBody: "TemplateModifySchema", + permission: "MANAGE_GUILD", + responses: { + 200: { body: "Template" }, + 403: { body: "APIErrorResponse" }, + }, + }), async (req: Request, res: Response) => { const { code, guild_id } = req.params; const { name, description } = req.body; diff --git a/src/api/routes/guilds/#guild_id/vanity-url.ts b/src/api/routes/guilds/#guild_id/vanity-url.ts index c85c943f..a64ae2c9 100644 --- a/src/api/routes/guilds/#guild_id/vanity-url.ts +++ b/src/api/routes/guilds/#guild_id/vanity-url.ts @@ -16,6 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ +import { route } from "@spacebar/api"; import { Channel, ChannelType, @@ -23,8 +24,7 @@ import { Invite, VanityUrlSchema, } from "@spacebar/util"; -import { Router, Request, Response } from "express"; -import { route } from "@spacebar/api"; +import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; const router = Router(); @@ -33,7 +33,20 @@ const InviteRegex = /\W/g; router.get( "/", - route({ permission: "MANAGE_GUILD" }), + route({ + permission: "MANAGE_GUILD", + responses: { + 200: { + body: "GuildVanityUrlResponse", + }, + 403: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const { guild_id } = req.params; const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); @@ -60,7 +73,21 @@ router.get( router.patch( "/", - route({ body: "VanityUrlSchema", permission: "MANAGE_GUILD" }), + route({ + requestBody: "VanityUrlSchema", + permission: "MANAGE_GUILD", + responses: { + 200: { + body: "GuildVanityUrlCreateResponse", + }, + 403: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const { guild_id } = req.params; const body = req.body as VanityUrlSchema; @@ -80,6 +107,17 @@ router.patch( where: { guild_id, type: ChannelType.GUILD_TEXT }, }); + if (!guild.features.includes("ALIASABLE_NAMES")) { + await Invite.update( + { guild_id }, + { + code: code, + }, + ); + + return res.json({ code }); + } + await Invite.create({ vanity_url: true, code: code, diff --git a/src/api/routes/guilds/#guild_id/voice-states/#user_id/index.ts b/src/api/routes/guilds/#guild_id/voice-states/#user_id/index.ts index 791ac102..60c69075 100644 --- a/src/api/routes/guilds/#guild_id/voice-states/#user_id/index.ts +++ b/src/api/routes/guilds/#guild_id/voice-states/#user_id/index.ts @@ -16,6 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ +import { route } from "@spacebar/api"; import { Channel, ChannelType, @@ -26,7 +27,6 @@ import { VoiceStateUpdateEvent, VoiceStateUpdateSchema, } from "@spacebar/util"; -import { route } from "@spacebar/api"; import { Request, Response, Router } from "express"; const router = Router(); @@ -34,7 +34,21 @@ const router = Router(); router.patch( "/", - route({ body: "VoiceStateUpdateSchema" }), + route({ + requestBody: "VoiceStateUpdateSchema", + responses: { + 204: {}, + 400: { + body: "APIErrorResponse", + }, + 403: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const body = req.body as VoiceStateUpdateSchema; const { guild_id } = req.params; diff --git a/src/api/routes/guilds/#guild_id/welcome-screen.ts b/src/api/routes/guilds/#guild_id/welcome-screen.ts index 696e20db..81000b4b 100644 --- a/src/api/routes/guilds/#guild_id/welcome-screen.ts +++ b/src/api/routes/guilds/#guild_id/welcome-screen.ts @@ -16,27 +16,53 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Request, Response, Router } from "express"; -import { Guild, Member, GuildUpdateWelcomeScreenSchema } from "@spacebar/util"; -import { HTTPError } from "lambert-server"; import { route } from "@spacebar/api"; +import { + Channel, + Guild, + GuildUpdateWelcomeScreenSchema, + Member, +} from "@spacebar/util"; +import { Request, Response, Router } from "express"; const router: Router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - const guild_id = req.params.guild_id; +router.get( + "/", + route({ + responses: { + 200: { + body: "GuildWelcomeScreen", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const guild_id = req.params.guild_id; - const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); - await Member.IsInGuildOrFail(req.user_id, guild_id); + const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); + await Member.IsInGuildOrFail(req.user_id, guild_id); - res.json(guild.welcome_screen); -}); + res.json(guild.welcome_screen); + }, +); router.patch( "/", route({ - body: "GuildUpdateWelcomeScreenSchema", + requestBody: "GuildUpdateWelcomeScreenSchema", permission: "MANAGE_GUILD", + responses: { + 204: {}, + 400: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, }), async (req: Request, res: Response) => { const guild_id = req.params.guild_id; @@ -44,17 +70,28 @@ router.patch( const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); - if (!guild.welcome_screen.enabled) - throw new HTTPError("Welcome screen disabled", 400); - if (body.welcome_channels) - guild.welcome_screen.welcome_channels = body.welcome_channels; // TODO: check if they exist and are valid - if (body.description) + if (body.enabled != undefined) + guild.welcome_screen.enabled = body.enabled; + + if (body.description != undefined) guild.welcome_screen.description = body.description; - if (body.enabled != null) guild.welcome_screen.enabled = body.enabled; + + if (body.welcome_channels != undefined) { + // Ensure channels exist within the guild + await Promise.all( + body.welcome_channels?.map(({ channel_id }) => + Channel.findOneOrFail({ + where: { id: channel_id, guild_id }, + select: { id: true }, + }), + ) || [], + ); + guild.welcome_screen.welcome_channels = body.welcome_channels; + } await guild.save(); - res.sendStatus(204); + res.status(200).json(guild.welcome_screen); }, ); diff --git a/src/api/routes/guilds/#guild_id/widget.json.ts b/src/api/routes/guilds/#guild_id/widget.json.ts index 1799f0be..69b5d48c 100644 --- a/src/api/routes/guilds/#guild_id/widget.json.ts +++ b/src/api/routes/guilds/#guild_id/widget.json.ts @@ -16,10 +16,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ +import { random, route } from "@spacebar/api"; +import { Channel, Guild, Invite, Member, Permissions } from "@spacebar/util"; import { Request, Response, Router } from "express"; -import { Permissions, Guild, Invite, Channel, Member } from "@spacebar/util"; import { HTTPError } from "lambert-server"; -import { random, route } from "@spacebar/api"; const router: Router = Router(); @@ -32,77 +32,90 @@ 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("/", route({}), async (req: Request, res: Response) => { - const { guild_id } = req.params; +router.get( + "/", + route({ + responses: { + 200: { + body: "GuildWidgetJsonResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { guild_id } = req.params; - const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); - if (!guild.widget_enabled) throw new HTTPError("Widget Disabled", 404); + const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); + if (!guild.widget_enabled) throw new HTTPError("Widget Disabled", 404); - // Fetch existing widget invite for widget channel - let invite = await Invite.findOne({ - where: { channel_id: guild.widget_channel_id }, - }); + // Fetch existing widget invite for widget channel + let invite = await Invite.findOne({ + where: { channel_id: guild.widget_channel_id }, + }); - if (guild.widget_channel_id && !invite) { - // Create invite for channel if none exists - // TODO: Refactor invite create code to a shared function - const max_age = 86400; // 24 hours - const expires_at = new Date(max_age * 1000 + Date.now()); + if (guild.widget_channel_id && !invite) { + // Create invite for channel if none exists + // TODO: Refactor invite create code to a shared function + const max_age = 86400; // 24 hours + const expires_at = new Date(max_age * 1000 + Date.now()); - invite = await Invite.create({ - code: random(), - temporary: false, - uses: 0, - max_uses: 0, - max_age: max_age, - expires_at, - created_at: new Date(), - guild_id, - channel_id: guild.widget_channel_id, - }).save(); - } + invite = await Invite.create({ + code: random(), + temporary: false, + uses: 0, + max_uses: 0, + max_age: max_age, + expires_at, + created_at: new Date(), + guild_id, + channel_id: guild.widget_channel_id, + }).save(); + } - // Fetch voice channels, and the @everyone permissions object - const channels: { id: string; name: string; position: number }[] = []; + // Fetch voice channels, and the @everyone permissions object + const channels: { id: string; name: string; position: number }[] = []; - ( - await Channel.find({ - where: { guild_id: guild_id, type: 2 }, - order: { position: "ASC" }, - }) - ).filter((doc) => { - // Only return channels where @everyone has the CONNECT permission - if ( - doc.permission_overwrites === undefined || - Permissions.channelPermission( - doc.permission_overwrites, - Permissions.FLAGS.CONNECT, - ) === Permissions.FLAGS.CONNECT - ) { - channels.push({ - id: doc.id, - name: doc.name ?? "Unknown channel", - position: doc.position ?? 0, - }); - } - }); + ( + await Channel.find({ + where: { guild_id: guild_id, type: 2 }, + order: { position: "ASC" }, + }) + ).filter((doc) => { + // Only return channels where @everyone has the CONNECT permission + if ( + doc.permission_overwrites === undefined || + Permissions.channelPermission( + doc.permission_overwrites, + Permissions.FLAGS.CONNECT, + ) === Permissions.FLAGS.CONNECT + ) { + channels.push({ + id: doc.id, + name: doc.name ?? "Unknown channel", + position: doc.position ?? 0, + }); + } + }); - // Fetch members - // TODO: Understand how Discord's max 100 random member sample works, and apply to here (see top of this file) - const members = await Member.find({ where: { guild_id: guild_id } }); + // Fetch members + // TODO: Understand how Discord's max 100 random member sample works, and apply to here (see top of this file) + const members = await Member.find({ where: { guild_id: guild_id } }); - // Construct object to respond with - const data = { - id: guild_id, - name: guild.name, - instant_invite: invite?.code, - channels: channels, - members: members, - presence_count: guild.presence_count, - }; + // Construct object to respond with + const data = { + id: guild_id, + name: guild.name, + instant_invite: invite?.code, + channels: channels, + members: members, + presence_count: guild.presence_count, + }; - res.set("Cache-Control", "public, max-age=300"); - return res.json(data); -}); + res.set("Cache-Control", "public, max-age=300"); + return res.json(data); + }, +); export default router; diff --git a/src/api/routes/guilds/#guild_id/widget.png.ts b/src/api/routes/guilds/#guild_id/widget.png.ts index 4e975603..c9ba8afc 100644 --- a/src/api/routes/guilds/#guild_id/widget.png.ts +++ b/src/api/routes/guilds/#guild_id/widget.png.ts @@ -18,11 +18,11 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Request, Response, Router } from "express"; -import { Guild } from "@spacebar/util"; -import { HTTPError } from "lambert-server"; import { route } from "@spacebar/api"; +import { Guild } from "@spacebar/util"; +import { Request, Response, Router } from "express"; import fs from "fs"; +import { HTTPError } from "lambert-server"; import path from "path"; const router: Router = Router(); @@ -31,130 +31,178 @@ const router: Router = Router(); // https://discord.com/developers/docs/resources/guild#get-guild-widget-image // TODO: Cache the response -router.get("/", route({}), async (req: Request, res: Response) => { - const { guild_id } = req.params; - - const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); - if (!guild.widget_enabled) throw new HTTPError("Unknown Guild", 404); - - // Fetch guild information - const icon = guild.icon; - const name = guild.name; - const presence = guild.presence_count + " ONLINE"; - - // Fetch parameter - const style = req.query.style?.toString() || "shield"; - if ( - !["shield", "banner1", "banner2", "banner3", "banner4"].includes(style) - ) { - throw new HTTPError( - "Value must be one of ('shield', 'banner1', 'banner2', 'banner3', 'banner4').", - 400, - ); - } - - // Setup canvas - const { createCanvas } = require("canvas"); - const { loadImage } = require("canvas"); - const sizeOf = require("image-size"); - - // TODO: Widget style templates need Spacebar branding - const source = path.join( - __dirname, - "..", - "..", - "..", - "..", - "..", - "assets", - "widget", - `${style}.png`, - ); - if (!fs.existsSync(source)) { - throw new HTTPError("Widget template does not exist.", 400); - } - - // Create base template image for parameter - const { width, height } = await sizeOf(source); - const canvas = createCanvas(width, height); - const ctx = canvas.getContext("2d"); - const template = await loadImage(source); - ctx.drawImage(template, 0, 0); - - // Add the guild specific information to the template asset image - switch (style) { - case "shield": - ctx.textAlign = "center"; - await drawText( - ctx, - 73, - 13, - "#FFFFFF", - "thin 10px Verdana", - presence, - ); - break; - case "banner1": - if (icon) await drawIcon(ctx, 20, 27, 50, icon); - await drawText(ctx, 83, 51, "#FFFFFF", "12px Verdana", name, 22); - await drawText( - ctx, - 83, - 66, - "#C9D2F0FF", - "thin 11px Verdana", - presence, - ); - break; - case "banner2": - if (icon) await drawIcon(ctx, 13, 19, 36, icon); - await drawText(ctx, 62, 34, "#FFFFFF", "12px Verdana", name, 15); - await drawText( - ctx, - 62, - 49, - "#C9D2F0FF", - "thin 11px Verdana", - presence, - ); - break; - case "banner3": - if (icon) await drawIcon(ctx, 20, 20, 50, icon); - await drawText(ctx, 83, 44, "#FFFFFF", "12px Verdana", name, 27); - await drawText( - ctx, - 83, - 58, - "#C9D2F0FF", - "thin 11px Verdana", - presence, - ); - break; - case "banner4": - if (icon) await drawIcon(ctx, 21, 136, 50, icon); - await drawText(ctx, 84, 156, "#FFFFFF", "13px Verdana", name, 27); - await drawText( - ctx, - 84, - 171, - "#C9D2F0FF", - "thin 12px Verdana", - presence, - ); - break; - default: +router.get( + "/", + route({ + responses: { + 200: {}, + 400: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { guild_id } = req.params; + + const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); + if (!guild.widget_enabled) throw new HTTPError("Unknown Guild", 404); + + // Fetch guild information + const icon = guild.icon; + const name = guild.name; + const presence = guild.presence_count + " ONLINE"; + + // Fetch parameter + const style = req.query.style?.toString() || "shield"; + if ( + !["shield", "banner1", "banner2", "banner3", "banner4"].includes( + style, + ) + ) { throw new HTTPError( "Value must be one of ('shield', 'banner1', 'banner2', 'banner3', 'banner4').", 400, ); - } - - // Return final image - const buffer = canvas.toBuffer("image/png"); - res.set("Content-Type", "image/png"); - res.set("Cache-Control", "public, max-age=3600"); - return res.send(buffer); -}); + } + + // Setup canvas + const { createCanvas } = require("canvas"); + const { loadImage } = require("canvas"); + const sizeOf = require("image-size"); + + // TODO: Widget style templates need Spacebar branding + const source = path.join( + __dirname, + "..", + "..", + "..", + "..", + "..", + "assets", + "widget", + `${style}.png`, + ); + if (!fs.existsSync(source)) { + throw new HTTPError("Widget template does not exist.", 400); + } + + // Create base template image for parameter + const { width, height } = await sizeOf(source); + const canvas = createCanvas(width, height); + const ctx = canvas.getContext("2d"); + const template = await loadImage(source); + ctx.drawImage(template, 0, 0); + + // Add the guild specific information to the template asset image + switch (style) { + case "shield": + ctx.textAlign = "center"; + await drawText( + ctx, + 73, + 13, + "#FFFFFF", + "thin 10px Verdana", + presence, + ); + break; + case "banner1": + if (icon) await drawIcon(ctx, 20, 27, 50, icon); + await drawText( + ctx, + 83, + 51, + "#FFFFFF", + "12px Verdana", + name, + 22, + ); + await drawText( + ctx, + 83, + 66, + "#C9D2F0FF", + "thin 11px Verdana", + presence, + ); + break; + case "banner2": + if (icon) await drawIcon(ctx, 13, 19, 36, icon); + await drawText( + ctx, + 62, + 34, + "#FFFFFF", + "12px Verdana", + name, + 15, + ); + await drawText( + ctx, + 62, + 49, + "#C9D2F0FF", + "thin 11px Verdana", + presence, + ); + break; + case "banner3": + if (icon) await drawIcon(ctx, 20, 20, 50, icon); + await drawText( + ctx, + 83, + 44, + "#FFFFFF", + "12px Verdana", + name, + 27, + ); + await drawText( + ctx, + 83, + 58, + "#C9D2F0FF", + "thin 11px Verdana", + presence, + ); + break; + case "banner4": + if (icon) await drawIcon(ctx, 21, 136, 50, icon); + await drawText( + ctx, + 84, + 156, + "#FFFFFF", + "13px Verdana", + name, + 27, + ); + await drawText( + ctx, + 84, + 171, + "#C9D2F0FF", + "thin 12px Verdana", + presence, + ); + break; + default: + throw new HTTPError( + "Value must be one of ('shield', 'banner1', 'banner2', 'banner3', 'banner4').", + 400, + ); + } + + // Return final image + const buffer = canvas.toBuffer("image/png"); + res.set("Content-Type", "image/png"); + res.set("Cache-Control", "public, max-age=3600"); + return res.send(buffer); + }, +); async function drawIcon( canvas: any, diff --git a/src/api/routes/guilds/#guild_id/widget.ts b/src/api/routes/guilds/#guild_id/widget.ts index 77af25dc..cae0d6be 100644 --- a/src/api/routes/guilds/#guild_id/widget.ts +++ b/src/api/routes/guilds/#guild_id/widget.ts @@ -16,28 +16,55 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Request, Response, Router } from "express"; -import { Guild, WidgetModifySchema } from "@spacebar/util"; import { route } from "@spacebar/api"; +import { Guild, WidgetModifySchema } from "@spacebar/util"; +import { Request, Response, Router } from "express"; const router: Router = Router(); // https://discord.com/developers/docs/resources/guild#get-guild-widget-settings -router.get("/", route({}), async (req: Request, res: Response) => { - const { guild_id } = req.params; +router.get( + "/", + route({ + responses: { + 200: { + body: "GuildWidgetSettingsResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { guild_id } = req.params; - const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); + const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); - return res.json({ - enabled: guild.widget_enabled || false, - channel_id: guild.widget_channel_id || null, - }); -}); + return res.json({ + enabled: guild.widget_enabled || false, + channel_id: guild.widget_channel_id || null, + }); + }, +); // https://discord.com/developers/docs/resources/guild#modify-guild-widget router.patch( "/", - route({ body: "WidgetModifySchema", permission: "MANAGE_GUILD" }), + route({ + requestBody: "WidgetModifySchema", + permission: "MANAGE_GUILD", + responses: { + 200: { + body: "WidgetModifySchema", + }, + 400: { + body: "APIErrorResponse", + }, + 403: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const body = req.body as WidgetModifySchema; const { guild_id } = req.params; diff --git a/src/api/routes/guilds/index.ts b/src/api/routes/guilds/index.ts index c793d185..545beb18 100644 --- a/src/api/routes/guilds/index.ts +++ b/src/api/routes/guilds/index.ts @@ -16,16 +16,16 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Request, Response } from "express"; +import { route } from "@spacebar/api"; import { - Guild, Config, - getRights, - Member, DiscordApiErrors, + Guild, GuildCreateSchema, + Member, + getRights, } from "@spacebar/util"; -import { route } from "@spacebar/api"; +import { Request, Response, Router } from "express"; const router: Router = Router(); @@ -33,7 +33,21 @@ const router: Router = Router(); router.post( "/", - route({ body: "GuildCreateSchema", right: "CREATE_GUILDS" }), + route({ + requestBody: "GuildCreateSchema", + right: "CREATE_GUILDS", + responses: { + 201: { + body: "GuildCreateResponse", + }, + 400: { + body: "APIErrorResponse", + }, + 403: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const body = req.body as GuildCreateSchema; @@ -58,7 +72,7 @@ router.post( await Member.addToGuild(req.user_id, guild.id); - res.status(201).json({ id: guild.id }); + res.status(201).json(guild); }, ); diff --git a/src/api/routes/guilds/templates/index.ts b/src/api/routes/guilds/templates/index.ts index bfbb7d3b..8f718a21 100644 --- a/src/api/routes/guilds/templates/index.ts +++ b/src/api/routes/guilds/templates/index.ts @@ -16,72 +16,91 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; import { - Template, + Config, + DiscordApiErrors, Guild, + GuildTemplateCreateSchema, + Member, Role, Snowflake, - Config, - Member, - GuildTemplateCreateSchema, + Template, } from "@spacebar/util"; -import { route } from "@spacebar/api"; -import { DiscordApiErrors } from "@spacebar/util"; +import { Request, Response, Router } from "express"; import fetch from "node-fetch"; const router: Router = Router(); -router.get("/:code", route({}), async (req: Request, res: Response) => { - const { allowDiscordTemplates, allowRaws, enabled } = - Config.get().templates; - if (!enabled) - res.json({ - code: 403, - message: "Template creation & usage is disabled on this instance.", - }).sendStatus(403); - - const { code } = req.params; +router.get( + "/:code", + route({ + responses: { + 200: { + body: "Template", + }, + 403: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { allowDiscordTemplates, allowRaws, enabled } = + Config.get().templates; + if (!enabled) + res.json({ + code: 403, + message: + "Template creation & usage is disabled on this instance.", + }).sendStatus(403); - if (code.startsWith("discord:")) { - if (!allowDiscordTemplates) - return res - .json({ - code: 403, - message: - "Discord templates cannot be used on this instance.", - }) - .sendStatus(403); - const discordTemplateID = code.split("discord:", 2)[1]; + const { code } = req.params; - const discordTemplateData = await fetch( - `https://discord.com/api/v9/guilds/templates/${discordTemplateID}`, - { - method: "get", - headers: { "Content-Type": "application/json" }, - }, - ); - return res.json(await discordTemplateData.json()); - } + if (code.startsWith("discord:")) { + if (!allowDiscordTemplates) + return res + .json({ + code: 403, + message: + "Discord templates cannot be used on this instance.", + }) + .sendStatus(403); + const discordTemplateID = code.split("discord:", 2)[1]; + + const discordTemplateData = await fetch( + `https://discord.com/api/v9/guilds/templates/${discordTemplateID}`, + { + method: "get", + headers: { "Content-Type": "application/json" }, + }, + ); + return res.json(await discordTemplateData.json()); + } - if (code.startsWith("external:")) { - if (!allowRaws) - return res - .json({ - code: 403, - message: "Importing raws is disabled on this instance.", - }) - .sendStatus(403); + if (code.startsWith("external:")) { + if (!allowRaws) + return res + .json({ + code: 403, + message: "Importing raws is disabled on this instance.", + }) + .sendStatus(403); - return res.json(code.split("external:", 2)[1]); - } + return res.json(code.split("external:", 2)[1]); + } - const template = await Template.findOneOrFail({ where: { code: code } }); - res.json(template); -}); + const template = await Template.findOneOrFail({ + where: { code: code }, + }); + res.json(template); + }, +); router.post( "/:code", - route({ body: "GuildTemplateCreateSchema" }), + route({ requestBody: "GuildTemplateCreateSchema" }), async (req: Request, res: Response) => { const { enabled, diff --git a/src/api/routes/invites/index.ts b/src/api/routes/invites/index.ts index 8bff2200..28a3b429 100644 --- a/src/api/routes/invites/index.ts +++ b/src/api/routes/invites/index.ts @@ -16,36 +16,68 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Request, Response } from "express"; +import { route } from "@spacebar/api"; import { + DiscordApiErrors, emitEvent, getPermission, Guild, Invite, InviteDeleteEvent, - User, PublicInviteRelation, + User, } from "@spacebar/util"; -import { route } from "@spacebar/api"; +import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; const router: Router = Router(); -router.get("/:code", route({}), async (req: Request, res: Response) => { - const { code } = req.params; +router.get( + "/:code", + route({ + responses: { + "200": { + body: "Invite", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { code } = req.params; - const invite = await Invite.findOneOrFail({ - where: { code }, - relations: PublicInviteRelation, - }); + const invite = await Invite.findOneOrFail({ + where: { code }, + relations: PublicInviteRelation, + }); - res.status(200).send(invite); -}); + res.status(200).send(invite); + }, +); router.post( "/:code", - route({ right: "USE_MASS_INVITES" }), + route({ + right: "USE_MASS_INVITES", + responses: { + "200": { + body: "Invite", + }, + 401: { + body: "APIErrorResponse", + }, + 403: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { + if (req.user_bot) throw DiscordApiErrors.BOT_PROHIBITED_ENDPOINT; + const { code } = req.params; const { guild_id } = await Invite.findOneOrFail({ where: { code: code }, @@ -75,33 +107,56 @@ router.post( ); // * 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({ where: { code } }); - const { guild_id, channel_id } = invite; - - const permission = await getPermission(req.user_id, guild_id, channel_id); +router.delete( + "/:code", + route({ + responses: { + "200": { + body: "Invite", + }, + 401: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { code } = req.params; + const invite = await Invite.findOneOrFail({ where: { code } }); + const { guild_id, channel_id } = invite; - if (!permission.has("MANAGE_GUILD") && !permission.has("MANAGE_CHANNELS")) - throw new HTTPError( - "You missing the MANAGE_GUILD or MANAGE_CHANNELS permission", - 401, + const permission = await getPermission( + req.user_id, + guild_id, + channel_id, ); - await Promise.all([ - Invite.delete({ code }), - emitEvent({ - event: "INVITE_DELETE", - guild_id: guild_id, - data: { - channel_id: channel_id, + if ( + !permission.has("MANAGE_GUILD") && + !permission.has("MANAGE_CHANNELS") + ) + throw new HTTPError( + "You missing the MANAGE_GUILD or MANAGE_CHANNELS permission", + 401, + ); + + await Promise.all([ + Invite.delete({ code }), + emitEvent({ + event: "INVITE_DELETE", guild_id: guild_id, - code: code, - }, - } as InviteDeleteEvent), - ]); + data: { + channel_id: channel_id, + guild_id: guild_id, + code: code, + }, + } as InviteDeleteEvent), + ]); - res.json({ invite: invite }); -}); + res.json({ invite: invite }); + }, +); export default router; diff --git a/src/api/routes/oauth2/authorize.ts b/src/api/routes/oauth2/authorize.ts index c041f671..7ae6fa84 100644 --- a/src/api/routes/oauth2/authorize.ts +++ b/src/api/routes/oauth2/authorize.ts @@ -16,126 +16,168 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Request, Response } from "express"; import { route } from "@spacebar/api"; import { ApiError, Application, ApplicationAuthorizeSchema, - getPermission, DiscordApiErrors, Member, Permissions, User, + getPermission, } from "@spacebar/util"; +import { Request, Response, Router } from "express"; const router = Router(); // TODO: scopes, other oauth types -router.get("/", route({}), async (req: Request, res: Response) => { - // const { client_id, scope, response_type, redirect_url } = req.query; - const { client_id } = req.query; - - const app = await Application.findOne({ - where: { - id: client_id as string, +router.get( + "/", + route({ + responses: { + // TODO: I really didn't feel like typing all of it out + 200: {}, + 400: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, }, - relations: ["bot"], - }); + }), + async (req: Request, res: Response) => { + // const { client_id, scope, response_type, redirect_url } = req.query; + const { client_id } = req.query; - // TODO: use DiscordApiErrors - // findOneOrFail throws code 404 - if (!app) throw DiscordApiErrors.UNKNOWN_APPLICATION; - if (!app.bot) throw DiscordApiErrors.OAUTH2_APPLICATION_BOT_ABSENT; + const app = await Application.findOne({ + where: { + id: client_id as string, + }, + relations: ["bot"], + }); - const bot = app.bot; - delete app.bot; + // TODO: use DiscordApiErrors + // findOneOrFail throws code 404 + if (!app) throw DiscordApiErrors.UNKNOWN_APPLICATION; + if (!app.bot) throw DiscordApiErrors.OAUTH2_APPLICATION_BOT_ABSENT; - const user = await User.findOneOrFail({ - where: { - id: req.user_id, - bot: false, - }, - select: ["id", "username", "avatar", "discriminator", "public_flags"], - }); + const bot = app.bot; + delete app.bot; - const guilds = await Member.find({ - where: { - user: { + const user = await User.findOneOrFail({ + where: { id: req.user_id, + bot: false, }, - }, - relations: ["guild", "roles"], - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - // prettier-ignore - select: ["guild.id", "guild.name", "guild.icon", "guild.mfa_level", "guild.owner_id", "roles.id"], - }); - - const guildsWithPermissions = guilds.map((x) => { - const perms = - x.guild.owner_id === user.id - ? new Permissions(Permissions.FLAGS.ADMINISTRATOR) - : Permissions.finalPermission({ - user: { - id: user.id, - roles: x.roles?.map((x) => x.id) || [], - }, - guild: { - roles: x?.roles || [], - }, - }); - - return { - id: x.guild.id, - name: x.guild.name, - icon: x.guild.icon, - mfa_level: x.guild.mfa_level, - permissions: perms.bitfield.toString(), - }; - }); - - return res.json({ - guilds: guildsWithPermissions, - user: { - id: user.id, - username: user.username, - avatar: user.avatar, - avatar_decoration: null, // TODO - discriminator: user.discriminator, - public_flags: user.public_flags, - }, - application: { - id: app.id, - name: app.name, - icon: app.icon, - description: app.description, - summary: app.summary, - type: app.type, - hook: app.hook, - guild_id: null, // TODO support guilds - bot_public: app.bot_public, - bot_require_code_grant: app.bot_require_code_grant, - verify_key: app.verify_key, - flags: app.flags, - }, - bot: { - id: bot.id, - username: bot.username, - avatar: bot.avatar, - avatar_decoration: null, // TODO - discriminator: bot.discriminator, - public_flags: bot.public_flags, - bot: true, - approximated_guild_count: 0, // TODO - }, - authorized: false, - }); -}); + select: [ + "id", + "username", + "avatar", + "discriminator", + "public_flags", + ], + }); + + const guilds = await Member.find({ + where: { + user: { + id: req.user_id, + }, + }, + relations: ["guild", "roles"], + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + // prettier-ignore + select: ["guild.id", "guild.name", "guild.icon", "guild.mfa_level", "guild.owner_id", "roles.id"], + }); + + const guildsWithPermissions = guilds.map((x) => { + const perms = + x.guild.owner_id === user.id + ? new Permissions(Permissions.FLAGS.ADMINISTRATOR) + : Permissions.finalPermission({ + user: { + id: user.id, + roles: x.roles?.map((x) => x.id) || [], + }, + guild: { + roles: x?.roles || [], + }, + }); + + return { + id: x.guild.id, + name: x.guild.name, + icon: x.guild.icon, + mfa_level: x.guild.mfa_level, + permissions: perms.bitfield.toString(), + }; + }); + + return res.json({ + guilds: guildsWithPermissions, + user: { + id: user.id, + username: user.username, + avatar: user.avatar, + avatar_decoration: null, // TODO + discriminator: user.discriminator, + public_flags: user.public_flags, + }, + application: { + id: app.id, + name: app.name, + icon: app.icon, + description: app.description, + summary: app.summary, + type: app.type, + hook: app.hook, + guild_id: null, // TODO support guilds + bot_public: app.bot_public, + bot_require_code_grant: app.bot_require_code_grant, + verify_key: app.verify_key, + flags: app.flags, + }, + bot: { + id: bot.id, + username: bot.username, + avatar: bot.avatar, + avatar_decoration: null, // TODO + discriminator: bot.discriminator, + public_flags: bot.public_flags, + bot: true, + approximated_guild_count: 0, // TODO + }, + authorized: false, + }); + }, +); router.post( "/", - route({ body: "ApplicationAuthorizeSchema" }), + route({ + requestBody: "ApplicationAuthorizeSchema", + query: { + client_id: { + type: "string", + }, + }, + responses: { + 200: { + body: "OAuthAuthorizeResponse", + }, + 400: { + body: "APIErrorResponse", + }, + 403: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const body = req.body as ApplicationAuthorizeSchema; // const { client_id, scope, response_type, redirect_url } = req.query; diff --git a/src/api/routes/ping.ts b/src/api/routes/ping.ts index 0fb6d9d0..73330239 100644 --- a/src/api/routes/ping.ts +++ b/src/api/routes/ping.ts @@ -16,29 +16,39 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Response, Request } from "express"; import { route } from "@spacebar/api"; import { Config } from "@spacebar/util"; +import { Request, Response, Router } from "express"; const router = Router(); -router.get("/", route({}), (req: Request, res: Response) => { - const { general } = Config.get(); - res.send({ - ping: "pong!", - instance: { - id: general.instanceId, - name: general.instanceName, - description: general.instanceDescription, - image: general.image, +router.get( + "/", + route({ + responses: { + 200: { + body: "InstancePingResponse", + }, + }, + }), + (req: Request, res: Response) => { + const { general } = Config.get(); + res.send({ + ping: "pong!", + instance: { + id: general.instanceId, + name: general.instanceName, + description: general.instanceDescription, + image: general.image, - correspondenceEmail: general.correspondenceEmail, - correspondenceUserID: general.correspondenceUserID, + correspondenceEmail: general.correspondenceEmail, + correspondenceUserID: general.correspondenceUserID, - frontPage: general.frontPage, - tosPage: general.tosPage, - }, - }); -}); + frontPage: general.frontPage, + tosPage: general.tosPage, + }, + }); + }, +); export default router; diff --git a/src/api/routes/policies/instance/domains.ts b/src/api/routes/policies/instance/domains.ts index 696a8510..afeb0e85 100644 --- a/src/api/routes/policies/instance/domains.ts +++ b/src/api/routes/policies/instance/domains.ts @@ -16,25 +16,38 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Request, Response } from "express"; import { route } from "@spacebar/api"; import { Config } from "@spacebar/util"; +import { Request, Response, Router } from "express"; const router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - const { cdn, gateway, api } = Config.get(); +router.get( + "/", + route({ + responses: { + 200: { + body: "InstanceDomainsResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { cdn, gateway, api } = Config.get(); - const IdentityForm = { - cdn: cdn.endpointPublic || process.env.CDN || "http://localhost:3001", - gateway: - gateway.endpointPublic || - process.env.GATEWAY || - "ws://localhost:3001", - defaultApiVersion: api.defaultVersion ?? 9, - apiEndpoint: api.endpointPublic ?? "http://localhost:3001/api/", - }; + const IdentityForm = { + cdn: + cdn.endpointPublic || + process.env.CDN || + "http://localhost:3001", + gateway: + gateway.endpointPublic || + process.env.GATEWAY || + "ws://localhost:3001", + defaultApiVersion: api.defaultVersion ?? 9, + apiEndpoint: api.endpointPublic ?? "http://localhost:3001/api/", + }; - res.json(IdentityForm); -}); + res.json(IdentityForm); + }, +); export default router; diff --git a/src/api/routes/policies/instance/index.ts b/src/api/routes/policies/instance/index.ts index 68ce3b42..6e269a5c 100644 --- a/src/api/routes/policies/instance/index.ts +++ b/src/api/routes/policies/instance/index.ts @@ -16,14 +16,24 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Request, Response } from "express"; import { route } from "@spacebar/api"; import { Config } from "@spacebar/util"; +import { Request, Response, Router } from "express"; const router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - const { general } = Config.get(); - res.json(general); -}); +router.get( + "/", + route({ + responses: { + 200: { + body: "APIGeneralConfiguration", + }, + }, + }), + async (req: Request, res: Response) => { + const { general } = Config.get(); + res.json(general); + }, +); export default router; diff --git a/src/api/routes/policies/instance/limits.ts b/src/api/routes/policies/instance/limits.ts index a6f13170..9852459d 100644 --- a/src/api/routes/policies/instance/limits.ts +++ b/src/api/routes/policies/instance/limits.ts @@ -16,14 +16,24 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Request, Response } from "express"; import { route } from "@spacebar/api"; import { Config } from "@spacebar/util"; +import { Request, Response, Router } from "express"; const router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - const { limits } = Config.get(); - res.json(limits); -}); +router.get( + "/", + route({ + responses: { + 200: { + body: "APILimitsConfiguration", + }, + }, + }), + async (req: Request, res: Response) => { + const { limits } = Config.get(); + res.json(limits); + }, +); export default router; diff --git a/src/api/routes/policies/stats.ts b/src/api/routes/policies/stats.ts index 3939e1e8..b2cd3d5a 100644 --- a/src/api/routes/policies/stats.ts +++ b/src/api/routes/policies/stats.ts @@ -28,20 +28,33 @@ import { import { Request, Response, Router } from "express"; const router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - if (!Config.get().security.statsWorldReadable) { - const rights = await getRights(req.user_id); - rights.hasThrow("VIEW_SERVER_STATS"); - } - - res.json({ - counts: { - user: await User.count(), - guild: await Guild.count(), - message: await Message.count(), - members: await Member.count(), +router.get( + "/", + route({ + responses: { + 200: { + body: "InstanceStatsResponse", + }, + 403: { + body: "APIErrorResponse", + }, }, - }); -}); + }), + async (req: Request, res: Response) => { + if (!Config.get().security.statsWorldReadable) { + const rights = await getRights(req.user_id); + rights.hasThrow("VIEW_SERVER_STATS"); + } + + res.json({ + counts: { + user: await User.count(), + guild: await Guild.count(), + message: await Message.count(), + members: await Member.count(), + }, + }); + }, +); export default router; diff --git a/src/api/routes/read-states/ack-bulk.ts b/src/api/routes/read-states/ack-bulk.ts index 2c51893b..3ee25d1a 100644 --- a/src/api/routes/read-states/ack-bulk.ts +++ b/src/api/routes/read-states/ack-bulk.ts @@ -16,14 +16,22 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Request, Response } from "express"; import { route } from "@spacebar/api"; import { AckBulkSchema, ReadState } from "@spacebar/util"; +import { Request, Response, Router } from "express"; const router = Router(); router.post( "/", - route({ body: "AckBulkSchema" }), + route({ + requestBody: "AckBulkSchema", + responses: { + 204: {}, + 400: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const body = req.body as AckBulkSchema; diff --git a/src/api/routes/science.ts b/src/api/routes/science.ts index 099da18b..d5cdc173 100644 --- a/src/api/routes/science.ts +++ b/src/api/routes/science.ts @@ -16,14 +16,22 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Response, Request } from "express"; import { route } from "@spacebar/api"; +import { Request, Response, Router } from "express"; const router = Router(); -router.post("/", route({}), (req: Request, res: Response) => { - // TODO: - res.sendStatus(204); -}); +router.post( + "/", + route({ + responses: { + 204: {}, + }, + }), + (req: Request, res: Response) => { + // TODO: + res.sendStatus(204); + }, +); export default router; diff --git a/src/api/routes/sticker-packs/index.ts b/src/api/routes/sticker-packs/index.ts index 234e03c6..569d1104 100644 --- a/src/api/routes/sticker-packs/index.ts +++ b/src/api/routes/sticker-packs/index.ts @@ -16,16 +16,28 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Request, Response, Router } from "express"; import { route } from "@spacebar/api"; import { StickerPack } from "@spacebar/util"; +import { Request, Response, Router } from "express"; const router: Router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - const sticker_packs = await StickerPack.find({ relations: ["stickers"] }); +router.get( + "/", + route({ + responses: { + 200: { + body: "APIStickerPackArray", + }, + }, + }), + async (req: Request, res: Response) => { + const sticker_packs = await StickerPack.find({ + relations: ["stickers"], + }); - res.json({ sticker_packs }); -}); + res.json({ sticker_packs }); + }, +); export default router; diff --git a/src/api/routes/stickers/#sticker_id/index.ts b/src/api/routes/stickers/#sticker_id/index.ts index 360149b5..2ea81bf9 100644 --- a/src/api/routes/stickers/#sticker_id/index.ts +++ b/src/api/routes/stickers/#sticker_id/index.ts @@ -16,15 +16,25 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Sticker } from "@spacebar/util"; -import { Router, Request, Response } from "express"; import { route } from "@spacebar/api"; +import { Sticker } from "@spacebar/util"; +import { Request, Response, Router } from "express"; const router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - const { sticker_id } = req.params; +router.get( + "/", + route({ + responses: { + 200: { + body: "Sticker", + }, + }, + }), + async (req: Request, res: Response) => { + const { sticker_id } = req.params; - res.json(await Sticker.find({ where: { id: sticker_id } })); -}); + res.json(await Sticker.find({ where: { id: sticker_id } })); + }, +); export default router; diff --git a/src/api/routes/stop.ts b/src/api/routes/stop.ts index 6a6e6277..79e132d7 100644 --- a/src/api/routes/stop.ts +++ b/src/api/routes/stop.ts @@ -16,14 +16,22 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Request, Response } from "express"; import { route } from "@spacebar/api"; +import { Request, Response, Router } from "express"; const router: Router = Router(); router.post( "/", - route({ right: "OPERATOR" }), + route({ + right: "OPERATOR", + responses: { + 200: {}, + 403: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { console.log(`/stop was called by ${req.user_id} at ${new Date()}`); res.sendStatus(200); diff --git a/src/api/routes/updates.ts b/src/api/routes/updates.ts index f7403899..101bd3bc 100644 --- a/src/api/routes/updates.ts +++ b/src/api/routes/updates.ts @@ -16,37 +16,53 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Response, Request } from "express"; import { route } from "@spacebar/api"; import { FieldErrors, Release } from "@spacebar/util"; +import { Request, Response, Router } from "express"; const router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - const platform = req.query.platform; +router.get( + "/", + route({ + responses: { + 200: { + body: "UpdatesResponse", + }, + 400: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const platform = req.query.platform; - if (!platform) - throw FieldErrors({ - platform: { - code: "BASE_TYPE_REQUIRED", - message: req.t("common:field.BASE_TYPE_REQUIRED"), + if (!platform) + throw FieldErrors({ + platform: { + code: "BASE_TYPE_REQUIRED", + message: req.t("common:field.BASE_TYPE_REQUIRED"), + }, + }); + + const release = await Release.findOneOrFail({ + where: { + enabled: true, + platform: platform as string, }, + order: { pub_date: "DESC" }, }); - const release = await Release.findOneOrFail({ - where: { - enabled: true, - platform: platform as string, - }, - order: { pub_date: "DESC" }, - }); - - res.json({ - name: release.name, - pub_date: release.pub_date, - url: release.url, - notes: release.notes, - }); -}); + res.json({ + name: release.name, + pub_date: release.pub_date, + url: release.url, + notes: release.notes, + }); + }, +); export default router; diff --git a/src/api/routes/users/#id/delete.ts b/src/api/routes/users/#id/delete.ts index e36a35e6..5b1a682c 100644 --- a/src/api/routes/users/#id/delete.ts +++ b/src/api/routes/users/#id/delete.ts @@ -30,7 +30,18 @@ const router = Router(); router.post( "/", - route({ right: "MANAGE_USERS" }), + route({ + right: "MANAGE_USERS", + responses: { + 204: {}, + 403: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { await User.findOneOrFail({ where: { id: req.params.id }, diff --git a/src/api/routes/users/#id/index.ts b/src/api/routes/users/#id/index.ts index 0c7cfe37..1bd413d3 100644 --- a/src/api/routes/users/#id/index.ts +++ b/src/api/routes/users/#id/index.ts @@ -16,16 +16,26 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Request, Response } from "express"; -import { User } from "@spacebar/util"; import { route } from "@spacebar/api"; +import { User } from "@spacebar/util"; +import { Request, Response, Router } from "express"; const router: Router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - const { id } = req.params; +router.get( + "/", + route({ + responses: { + 200: { + body: "APIPublicUser", + }, + }, + }), + async (req: Request, res: Response) => { + const { id } = req.params; - res.json(await User.getPublicUser(id)); -}); + res.json(await User.getPublicUser(id)); + }, +); export default router; diff --git a/src/api/routes/users/#id/profile.ts b/src/api/routes/users/#id/profile.ts index 2836c563..eecec0f3 100644 --- a/src/api/routes/users/#id/profile.ts +++ b/src/api/routes/users/#id/profile.ts @@ -16,23 +16,23 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Request, Response } from "express"; +import { route } from "@spacebar/api"; import { - User, Member, - UserProfileModifySchema, - handleFile, PrivateUserProjection, - emitEvent, + User, + UserProfileModifySchema, UserUpdateEvent, + emitEvent, + handleFile, } from "@spacebar/util"; -import { route } from "@spacebar/api"; +import { Request, Response, Router } from "express"; const router: Router = Router(); router.get( "/", - route({ test: { response: { body: "UserProfileResponse" } } }), + route({ responses: { 200: { body: "UserProfileResponse" } } }), async (req: Request, res: Response) => { if (req.params.id === "@me") req.params.id = req.user_id; @@ -84,18 +84,6 @@ router.get( // TODO: make proper DTO's in util? - const userDto = { - username: user.username, - discriminator: user.discriminator, - id: user.id, - public_flags: user.public_flags, - avatar: user.avatar, - accent_color: user.accent_color, - banner: user.banner, - bio: req.user_bot ? null : user.bio, - bot: user.bot, - }; - const userProfile = { bio: req.user_bot ? null : user.bio, accent_color: user.accent_color, @@ -104,28 +92,6 @@ router.get( theme_colors: user.theme_colors, }; - const guildMemberDto = guild_member - ? { - avatar: guild_member.avatar, - banner: guild_member.banner, - bio: req.user_bot ? null : guild_member.bio, - communication_disabled_until: - guild_member.communication_disabled_until, - deaf: guild_member.deaf, - flags: user.flags, - is_pending: guild_member.pending, - pending: guild_member.pending, // why is this here twice, discord? - joined_at: guild_member.joined_at, - mute: guild_member.mute, - nick: guild_member.nick, - premium_since: guild_member.premium_since, - roles: guild_member.roles - .map((x) => x.id) - .filter((id) => id != guild_id), - user: userDto, - } - : undefined; - const guildMemberProfile = { accent_color: null, banner: guild_member?.banner || null, @@ -139,11 +105,11 @@ router.get( premium_guild_since: premium_guild_since, // TODO premium_since: user.premium_since, // TODO mutual_guilds: mutual_guilds, // TODO {id: "", nick: null} when ?with_mutual_guilds=true - user: userDto, + user: user.toPublicUser(), premium_type: user.premium_type, profile_themes_experiment_bucket: 4, // TODO: This doesn't make it available, for some reason? user_profile: userProfile, - guild_member: guild_id && guildMemberDto, + guild_member: guild_member?.toPublicMember(), guild_member_profile: guild_id && guildMemberProfile, }); }, @@ -151,7 +117,7 @@ router.get( router.patch( "/", - route({ body: "UserProfileModifySchema" }), + route({ requestBody: "UserProfileModifySchema" }), async (req: Request, res: Response) => { const body = req.body as UserProfileModifySchema; diff --git a/src/api/routes/users/#id/relationships.ts b/src/api/routes/users/#id/relationships.ts index dfe52a5e..3737ca00 100644 --- a/src/api/routes/users/#id/relationships.ts +++ b/src/api/routes/users/#id/relationships.ts @@ -16,17 +16,25 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Request, Response } from "express"; -import { User } from "@spacebar/util"; import { route } from "@spacebar/api"; +import { User, UserRelationsResponse } from "@spacebar/util"; +import { Request, Response, Router } from "express"; const router: Router = Router(); router.get( "/", - route({ test: { response: { body: "UserRelationsResponse" } } }), + route({ + responses: { + 200: { body: "UserRelationsResponse" }, + 404: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { - const mutual_relations: object[] = []; + const mutual_relations: UserRelationsResponse = []; + const requested_relations = await User.findOneOrFail({ where: { id: req.params.id }, relations: ["relationships"], diff --git a/src/api/routes/users/@me/channels.ts b/src/api/routes/users/@me/channels.ts index 04db4fe9..8a8fadd9 100644 --- a/src/api/routes/users/@me/channels.ts +++ b/src/api/routes/users/@me/channels.ts @@ -16,32 +16,51 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; import { - Recipient, - DmChannelDTO, Channel, DmChannelCreateSchema, + DmChannelDTO, + Recipient, } from "@spacebar/util"; -import { route } from "@spacebar/api"; +import { Request, Response, Router } from "express"; const router: Router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - const recipients = await Recipient.find({ - where: { user_id: req.user_id, closed: false }, - relations: ["channel", "channel.recipients"], - }); - res.json( - await Promise.all( - recipients.map((r) => DmChannelDTO.from(r.channel, [req.user_id])), - ), - ); -}); +router.get( + "/", + route({ + responses: { + 200: { + body: "APIDMChannelArray", + }, + }, + }), + async (req: Request, res: Response) => { + const recipients = await Recipient.find({ + where: { user_id: req.user_id, closed: false }, + relations: ["channel", "channel.recipients"], + }); + res.json( + await Promise.all( + recipients.map((r) => + DmChannelDTO.from(r.channel, [req.user_id]), + ), + ), + ); + }, +); router.post( "/", - route({ body: "DmChannelCreateSchema" }), + route({ + requestBody: "DmChannelCreateSchema", + responses: { + 200: { + body: "DmChannelDTO", + }, + }, + }), async (req: Request, res: Response) => { const body = req.body as DmChannelCreateSchema; res.json( diff --git a/src/api/routes/users/@me/connections/#connection_name/#connection_id/access-token.ts b/src/api/routes/users/@me/connections/#connection_name/#connection_id/access-token.ts index 9031f3c8..789a7878 100644 --- a/src/api/routes/users/@me/connections/#connection_name/#connection_id/access-token.ts +++ b/src/api/routes/users/@me/connections/#connection_name/#connection_id/access-token.ts @@ -23,9 +23,9 @@ import { ConnectionStore, DiscordApiErrors, FieldErrors, + RefreshableConnection, } from "@spacebar/util"; import { Request, Response, Router } from "express"; -import RefreshableConnection from "../../../../../../../util/connections/RefreshableConnection"; const router = Router(); // TODO: this route is only used for spotify, twitch, and youtube. (battlenet seems to be able to PUT, maybe others also) diff --git a/src/api/routes/users/@me/connections/#connection_name/#connection_id/index.ts b/src/api/routes/users/@me/connections/#connection_name/#connection_id/index.ts index 3a4e5e0a..351ec99a 100644 --- a/src/api/routes/users/@me/connections/#connection_name/#connection_id/index.ts +++ b/src/api/routes/users/@me/connections/#connection_name/#connection_id/index.ts @@ -29,7 +29,7 @@ const router = Router(); // TODO: connection update schema router.patch( "/", - route({ body: "ConnectionUpdateSchema" }), + route({ requestBody: "ConnectionUpdateSchema" }), async (req: Request, res: Response) => { const { connection_name, connection_id } = req.params; const body = req.body as ConnectionUpdateSchema; diff --git a/src/api/routes/users/@me/delete.ts b/src/api/routes/users/@me/delete.ts index dce737fc..e36a1e92 100644 --- a/src/api/routes/users/@me/delete.ts +++ b/src/api/routes/users/@me/delete.ts @@ -16,41 +16,58 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Request, Response } from "express"; -import { Member, User } from "@spacebar/util"; import { route } from "@spacebar/api"; +import { Member, User } from "@spacebar/util"; import bcrypt from "bcrypt"; +import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; const router = Router(); -router.post("/", route({}), async (req: Request, res: Response) => { - const user = await User.findOneOrFail({ - where: { id: req.user_id }, - select: ["data"], - }); //User object - let correctpass = true; - - if (user.data.hash) { - // guest accounts can delete accounts without password - correctpass = await bcrypt.compare(req.body.password, user.data.hash); - if (!correctpass) { - throw new HTTPError(req.t("auth:login.INVALID_PASSWORD")); +router.post( + "/", + route({ + responses: { + 204: {}, + 401: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const user = await User.findOneOrFail({ + where: { id: req.user_id }, + select: ["data"], + }); //User object + let correctpass = true; + + if (user.data.hash) { + // guest accounts can delete accounts without password + correctpass = await bcrypt.compare( + req.body.password, + user.data.hash, + ); + if (!correctpass) { + throw new HTTPError(req.t("auth:login.INVALID_PASSWORD")); + } } - } - // TODO: decrement guild member count + // TODO: decrement guild member count - if (correctpass) { - await Promise.all([ - User.delete({ id: req.user_id }), - Member.delete({ id: req.user_id }), - ]); + if (correctpass) { + await Promise.all([ + User.delete({ id: req.user_id }), + Member.delete({ id: req.user_id }), + ]); - res.sendStatus(204); - } else { - res.sendStatus(401); - } -}); + res.sendStatus(204); + } else { + res.sendStatus(401); + } + }, +); export default router; diff --git a/src/api/routes/users/@me/disable.ts b/src/api/routes/users/@me/disable.ts index d123a6a1..b4d03e62 100644 --- a/src/api/routes/users/@me/disable.ts +++ b/src/api/routes/users/@me/disable.ts @@ -16,35 +16,52 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { User } from "@spacebar/util"; -import { Router, Response, Request } from "express"; import { route } from "@spacebar/api"; +import { User } from "@spacebar/util"; import bcrypt from "bcrypt"; +import { Request, Response, Router } from "express"; const router = Router(); -router.post("/", route({}), async (req: Request, res: Response) => { - const user = await User.findOneOrFail({ - where: { id: req.user_id }, - select: ["data"], - }); //User object - let correctpass = true; +router.post( + "/", + route({ + responses: { + 204: {}, + 400: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const user = await User.findOneOrFail({ + where: { id: req.user_id }, + select: ["data"], + }); //User object + let correctpass = true; - if (user.data.hash) { - // guest accounts can delete accounts without password - correctpass = await bcrypt.compare(req.body.password, user.data.hash); //Not sure if user typed right password :/ - } + if (user.data.hash) { + // guest accounts can delete accounts without password + correctpass = await bcrypt.compare( + req.body.password, + user.data.hash, + ); //Not sure if user typed right password :/ + } - if (correctpass) { - await User.update({ id: req.user_id }, { disabled: true }); + if (correctpass) { + await User.update({ id: req.user_id }, { disabled: true }); - res.sendStatus(204); - } else { - res.status(400).json({ - message: "Password does not match", - code: 50018, - }); - } -}); + res.sendStatus(204); + } else { + res.status(400).json({ + message: "Password does not match", + code: 50018, + }); + } + }, +); export default router; diff --git a/src/api/routes/users/@me/guilds.ts b/src/api/routes/users/@me/guilds.ts index b16b909d..0bce432b 100644 --- a/src/api/routes/users/@me/guilds.ts +++ b/src/api/routes/users/@me/guilds.ts @@ -16,79 +16,106 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Request, Response } from "express"; +import { route } from "@spacebar/api"; import { + Config, Guild, - Member, - User, GuildDeleteEvent, GuildMemberRemoveEvent, + Member, + User, emitEvent, - Config, } from "@spacebar/util"; +import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; -import { route } from "@spacebar/api"; const router: Router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - const members = await Member.find({ - relations: ["guild"], - where: { id: req.user_id }, - }); +router.get( + "/", + route({ + responses: { + 200: { + body: "APIGuildArray", + }, + }, + }), + async (req: Request, res: Response) => { + const members = await Member.find({ + relations: ["guild"], + where: { id: req.user_id }, + }); - let guild = members.map((x) => x.guild); + let guild = members.map((x) => x.guild); - if ("with_counts" in req.query && req.query.with_counts == "true") { - guild = []; // TODO: Load guilds with user role permissions number - } + if ("with_counts" in req.query && req.query.with_counts == "true") { + guild = []; // TODO: Load guilds with user role permissions number + } - res.json(guild); -}); + res.json(guild); + }, +); // user send to leave a certain guild -router.delete("/:guild_id", route({}), async (req: Request, res: Response) => { - const { autoJoin } = Config.get().guild; - const { guild_id } = req.params; - const guild = await Guild.findOneOrFail({ - where: { id: guild_id }, - select: ["owner_id"], - }); +router.delete( + "/:guild_id", + route({ + responses: { + 204: {}, + 400: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { autoJoin } = Config.get().guild; + const { guild_id } = req.params; + const guild = await Guild.findOneOrFail({ + where: { id: guild_id }, + select: ["owner_id"], + }); - if (!guild) throw new HTTPError("Guild doesn't exist", 404); - if (guild.owner_id === req.user_id) - throw new HTTPError("You can't leave your own guild", 400); - if ( - autoJoin.enabled && - autoJoin.guilds.includes(guild_id) && - !autoJoin.canLeave - ) { - throw new HTTPError("You can't leave instance auto join guilds", 400); - } + if (!guild) throw new HTTPError("Guild doesn't exist", 404); + if (guild.owner_id === req.user_id) + throw new HTTPError("You can't leave your own guild", 400); + if ( + autoJoin.enabled && + autoJoin.guilds.includes(guild_id) && + !autoJoin.canLeave + ) { + throw new HTTPError( + "You can't leave instance auto join guilds", + 400, + ); + } - await Promise.all([ - Member.delete({ id: req.user_id, guild_id: guild_id }), - emitEvent({ - event: "GUILD_DELETE", - data: { - id: guild_id, - }, - user_id: req.user_id, - } as GuildDeleteEvent), - ]); + await Promise.all([ + Member.delete({ id: req.user_id, guild_id: guild_id }), + emitEvent({ + event: "GUILD_DELETE", + data: { + id: guild_id, + }, + user_id: req.user_id, + } as GuildDeleteEvent), + ]); - const user = await User.getPublicUser(req.user_id); + const user = await User.getPublicUser(req.user_id); - await emitEvent({ - event: "GUILD_MEMBER_REMOVE", - data: { + await emitEvent({ + event: "GUILD_MEMBER_REMOVE", + data: { + guild_id: guild_id, + user: user, + }, guild_id: guild_id, - user: user, - }, - guild_id: guild_id, - } as GuildMemberRemoveEvent); + } as GuildMemberRemoveEvent); - return res.sendStatus(204); -}); + return res.sendStatus(204); + }, +); export default router; diff --git a/src/api/routes/users/@me/guilds/#guild_id/settings.ts b/src/api/routes/users/@me/guilds/#guild_id/settings.ts index 7e9f2a08..ac6586ce 100644 --- a/src/api/routes/users/@me/guilds/#guild_id/settings.ts +++ b/src/api/routes/users/@me/guilds/#guild_id/settings.ts @@ -16,29 +16,49 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Response, Request } from "express"; +import { route } from "@spacebar/api"; import { Channel, Member, OrmUtils, UserGuildSettingsSchema, } from "@spacebar/util"; -import { route } from "@spacebar/api"; +import { Request, Response, Router } from "express"; const router = Router(); // GET doesn't exist on discord.com -router.get("/", route({}), async (req: Request, res: Response) => { - const user = await Member.findOneOrFail({ - where: { id: req.user_id, guild_id: req.params.guild_id }, - select: ["settings"], - }); - return res.json(user.settings); -}); +router.get( + "/", + route({ + responses: { + 200: {}, + 404: {}, + }, + }), + async (req: Request, res: Response) => { + const user = await Member.findOneOrFail({ + where: { id: req.user_id, guild_id: req.params.guild_id }, + select: ["settings"], + }); + return res.json(user.settings); + }, +); router.patch( "/", - route({ body: "UserGuildSettingsSchema" }), + route({ + requestBody: "UserGuildSettingsSchema", + responses: { + 200: {}, + 400: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const body = req.body as UserGuildSettingsSchema; diff --git a/src/api/routes/users/@me/index.ts b/src/api/routes/users/@me/index.ts index b3eeb964..8fe86265 100644 --- a/src/api/routes/users/@me/index.ts +++ b/src/api/routes/users/@me/index.ts @@ -16,36 +16,59 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Request, Response } from "express"; +import { route } from "@spacebar/api"; import { - User, - PrivateUserProjection, - emitEvent, - UserUpdateEvent, - handleFile, - FieldErrors, adjustEmail, Config, - UserModifySchema, + emitEvent, + FieldErrors, generateToken, + handleFile, + PrivateUserProjection, + User, + UserModifySchema, + UserUpdateEvent, } from "@spacebar/util"; -import { route } from "@spacebar/api"; import bcrypt from "bcrypt"; +import { Request, Response, Router } from "express"; const router: Router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - res.json( - await User.findOne({ - select: PrivateUserProjection, - where: { id: req.user_id }, - }), - ); -}); +router.get( + "/", + route({ + responses: { + 200: { + body: "APIPrivateUser", + }, + }, + }), + async (req: Request, res: Response) => { + res.json( + await User.findOne({ + select: PrivateUserProjection, + where: { id: req.user_id }, + }), + ); + }, +); router.patch( "/", - route({ body: "UserModifySchema" }), + route({ + requestBody: "UserModifySchema", + responses: { + 200: { + body: "UserUpdateResponse", + }, + 400: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const body = req.body as UserModifySchema; diff --git a/src/api/routes/users/@me/mfa/codes-verification.ts b/src/api/routes/users/@me/mfa/codes-verification.ts index 69d45e91..f71704a9 100644 --- a/src/api/routes/users/@me/mfa/codes-verification.ts +++ b/src/api/routes/users/@me/mfa/codes-verification.ts @@ -16,21 +16,34 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Request, Response } from "express"; import { route } from "@spacebar/api"; import { BackupCode, - generateMfaBackupCodes, - User, CodesVerificationSchema, DiscordApiErrors, + User, + generateMfaBackupCodes, } from "@spacebar/util"; +import { Request, Response, Router } from "express"; const router = Router(); router.post( "/", - route({ body: "CodesVerificationSchema" }), + route({ + requestBody: "CodesVerificationSchema", + responses: { + 200: { + body: "APIBackupCodeArray", + }, + 400: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { // const { key, nonce, regenerate } = req.body as CodesVerificationSchema; const { regenerate } = req.body as CodesVerificationSchema; diff --git a/src/api/routes/users/@me/mfa/codes.ts b/src/api/routes/users/@me/mfa/codes.ts index 4ddbf78e..f9cfc4c4 100644 --- a/src/api/routes/users/@me/mfa/codes.ts +++ b/src/api/routes/users/@me/mfa/codes.ts @@ -16,16 +16,16 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Request, Response } from "express"; import { route } from "@spacebar/api"; import { BackupCode, FieldErrors, generateMfaBackupCodes, - User, MfaCodesSchema, + User, } from "@spacebar/util"; import bcrypt from "bcrypt"; +import { Request, Response, Router } from "express"; const router = Router(); @@ -33,7 +33,23 @@ const router = Router(); router.post( "/", - route({ body: "MfaCodesSchema" }), + route({ + requestBody: "MfaCodesSchema", + deprecated: true, + description: + "This route is replaced with users/@me/mfa/codes-verification in newer clients", + responses: { + 200: { + body: "APIBackupCodeArray", + }, + 400: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const { password, regenerate } = req.body as MfaCodesSchema; diff --git a/src/api/routes/users/@me/mfa/totp/disable.ts b/src/api/routes/users/@me/mfa/totp/disable.ts index 9f406423..362152d7 100644 --- a/src/api/routes/users/@me/mfa/totp/disable.ts +++ b/src/api/routes/users/@me/mfa/totp/disable.ts @@ -16,22 +16,32 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Request, Response } from "express"; import { route } from "@spacebar/api"; -import { verifyToken } from "node-2fa"; -import { HTTPError } from "lambert-server"; import { - User, - generateToken, BackupCode, TotpDisableSchema, + User, + generateToken, } from "@spacebar/util"; +import { Request, Response, Router } from "express"; +import { HTTPError } from "lambert-server"; +import { verifyToken } from "node-2fa"; const router = Router(); router.post( "/", - route({ body: "TotpDisableSchema" }), + route({ + requestBody: "TotpDisableSchema", + responses: { + 200: { + body: "TokenOnlyResponse", + }, + 400: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const body = req.body as TotpDisableSchema; diff --git a/src/api/routes/users/@me/mfa/totp/enable.ts b/src/api/routes/users/@me/mfa/totp/enable.ts index 4d6b2763..19836e4d 100644 --- a/src/api/routes/users/@me/mfa/totp/enable.ts +++ b/src/api/routes/users/@me/mfa/totp/enable.ts @@ -16,15 +16,15 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Request, Response } from "express"; +import { route } from "@spacebar/api"; import { + TotpEnableSchema, User, - generateToken, generateMfaBackupCodes, - TotpEnableSchema, + generateToken, } from "@spacebar/util"; -import { route } from "@spacebar/api"; import bcrypt from "bcrypt"; +import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; import { verifyToken } from "node-2fa"; @@ -32,7 +32,20 @@ const router = Router(); router.post( "/", - route({ body: "TotpEnableSchema" }), + route({ + requestBody: "TotpEnableSchema", + responses: { + 200: { + body: "TokenWithBackupCodesResponse", + }, + 400: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const body = req.body as TotpEnableSchema; diff --git a/src/api/routes/users/@me/mfa/webauthn/credentials/#key_id/index.ts b/src/api/routes/users/@me/mfa/webauthn/credentials/#key_id/index.ts index 04aca7e4..9cf42def 100644 --- a/src/api/routes/users/@me/mfa/webauthn/credentials/#key_id/index.ts +++ b/src/api/routes/users/@me/mfa/webauthn/credentials/#key_id/index.ts @@ -21,21 +21,31 @@ import { SecurityKey, User } from "@spacebar/util"; import { Request, Response, Router } from "express"; const router = Router(); -router.delete("/", route({}), async (req: Request, res: Response) => { - const { key_id } = req.params; +router.delete( + "/", + route({ + responses: { + 204: {}, + }, + }), + async (req: Request, res: Response) => { + const { key_id } = req.params; - await SecurityKey.delete({ - id: key_id, - user_id: req.user_id, - }); + await SecurityKey.delete({ + id: key_id, + user_id: req.user_id, + }); - const keys = await SecurityKey.count({ where: { user_id: req.user_id } }); + const keys = await SecurityKey.count({ + where: { user_id: req.user_id }, + }); - // disable webauthn if there are no keys left - if (keys === 0) - await User.update({ id: req.user_id }, { webauthn_enabled: false }); + // disable webauthn if there are no keys left + if (keys === 0) + await User.update({ id: req.user_id }, { webauthn_enabled: false }); - res.sendStatus(204); -}); + res.sendStatus(204); + }, +); export default router; diff --git a/src/api/routes/users/@me/mfa/webauthn/credentials/index.ts b/src/api/routes/users/@me/mfa/webauthn/credentials/index.ts index 29dbb7cf..f383ffb7 100644 --- a/src/api/routes/users/@me/mfa/webauthn/credentials/index.ts +++ b/src/api/routes/users/@me/mfa/webauthn/credentials/index.ts @@ -73,7 +73,17 @@ router.get("/", route({}), async (req: Request, res: Response) => { router.post( "/", - route({ body: "WebAuthnPostSchema" }), + route({ + requestBody: "WebAuthnPostSchema", + responses: { + 200: { + body: "WebAuthnCreateResponse", + }, + 400: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { if (!WebAuthn.fido2) { // TODO: I did this for typescript and I can't use ! diff --git a/src/api/routes/users/@me/notes.ts b/src/api/routes/users/@me/notes.ts index d05c799c..248e61f9 100644 --- a/src/api/routes/users/@me/notes.ts +++ b/src/api/routes/users/@me/notes.ts @@ -16,71 +16,99 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Request, Response, Router } from "express"; import { route } from "@spacebar/api"; -import { User, Note, emitEvent, Snowflake } from "@spacebar/util"; +import { Note, Snowflake, User, emitEvent } from "@spacebar/util"; +import { Request, Response, Router } from "express"; const router: Router = Router(); -router.get("/:id", route({}), async (req: Request, res: Response) => { - const { id } = req.params; - - const note = await Note.findOneOrFail({ - where: { - owner: { id: req.user_id }, - target: { id: id }, +router.get( + "/:id", + route({ + responses: { + 200: { + body: "UserNoteResponse", + }, + 404: { + body: "APIErrorResponse", + }, }, - }); + }), + async (req: Request, res: Response) => { + const { id } = req.params; + + const note = await Note.findOneOrFail({ + where: { + owner: { id: req.user_id }, + target: { id: id }, + }, + }); - return res.json({ - note: note?.content, - note_user_id: id, - user_id: req.user_id, - }); -}); + return res.json({ + note: note?.content, + note_user_id: id, + user_id: req.user_id, + }); + }, +); -router.put("/:id", route({}), async (req: Request, res: Response) => { - const { id } = req.params; - const owner = await User.findOneOrFail({ where: { id: req.user_id } }); - const target = await User.findOneOrFail({ where: { id: id } }); //if noted user does not exist throw - const { note } = req.body; +router.put( + "/:id", + route({ + requestBody: "UserNoteUpdateSchema", + responses: { + 204: {}, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { id } = req.params; + const owner = await User.findOneOrFail({ where: { id: req.user_id } }); + const target = await User.findOneOrFail({ where: { id: id } }); //if noted user does not exist throw + const { note } = req.body; - if (note && note.length) { - // upsert a note - if ( - await Note.findOne({ - where: { owner: { id: owner.id }, target: { id: target.id } }, - }) - ) { - Note.update( - { owner: { id: owner.id }, target: { id: target.id } }, - { owner, target, content: note }, - ); + if (note && note.length) { + // upsert a note + if ( + await Note.findOne({ + where: { + owner: { id: owner.id }, + target: { id: target.id }, + }, + }) + ) { + Note.update( + { owner: { id: owner.id }, target: { id: target.id } }, + { owner, target, content: note }, + ); + } else { + Note.insert({ + id: Snowflake.generate(), + owner, + target, + content: note, + }); + } } else { - Note.insert({ - id: Snowflake.generate(), - owner, - target, - content: note, + await Note.delete({ + owner: { id: owner.id }, + target: { id: target.id }, }); } - } else { - await Note.delete({ - owner: { id: owner.id }, - target: { id: target.id }, - }); - } - await emitEvent({ - event: "USER_NOTE_UPDATE", - data: { - note: note, - id: target.id, - }, - user_id: owner.id, - }); + await emitEvent({ + event: "USER_NOTE_UPDATE", + data: { + note: note, + id: target.id, + }, + user_id: owner.id, + }); - return res.status(204); -}); + return res.status(204); + }, +); export default router; diff --git a/src/api/routes/users/@me/relationships.ts b/src/api/routes/users/@me/relationships.ts index e9ea47e6..bce0a654 100644 --- a/src/api/routes/users/@me/relationships.ts +++ b/src/api/routes/users/@me/relationships.ts @@ -16,20 +16,20 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ +import { route } from "@spacebar/api"; import { - RelationshipAddEvent, - User, + Config, + DiscordApiErrors, PublicUserProjection, - RelationshipType, + Relationship, + RelationshipAddEvent, RelationshipRemoveEvent, + RelationshipType, + User, emitEvent, - Relationship, - Config, } from "@spacebar/util"; -import { Router, Response, Request } from "express"; +import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; -import { DiscordApiErrors } from "@spacebar/util"; -import { route } from "@spacebar/api"; const router = Router(); @@ -38,29 +38,53 @@ const userProjection: (keyof User)[] = [ ...PublicUserProjection, ]; -router.get("/", route({}), async (req: Request, res: Response) => { - const user = await User.findOneOrFail({ - where: { id: req.user_id }, - relations: ["relationships", "relationships.to"], - select: ["id", "relationships"], - }); +router.get( + "/", + route({ + responses: { + 200: { + body: "UserRelationshipsResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const user = await User.findOneOrFail({ + where: { id: req.user_id }, + relations: ["relationships", "relationships.to"], + select: ["id", "relationships"], + }); - //TODO DTO - const related_users = user.relationships.map((r) => { - return { - id: r.to.id, - type: r.type, - nickname: null, - user: r.to.toPublicUser(), - }; - }); + //TODO DTO + const related_users = user.relationships.map((r) => { + return { + id: r.to.id, + type: r.type, + nickname: null, + user: r.to.toPublicUser(), + }; + }); - return res.json(related_users); -}); + return res.json(related_users); + }, +); router.put( "/:id", - route({ body: "RelationshipPutSchema" }), + route({ + requestBody: "RelationshipPutSchema", + responses: { + 204: {}, + 400: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { return await updateRelationship( req, @@ -77,7 +101,18 @@ router.put( router.post( "/", - route({ body: "RelationshipPostSchema" }), + route({ + requestBody: "RelationshipPostSchema", + responses: { + 204: {}, + 400: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { return await updateRelationship( req, @@ -98,64 +133,78 @@ router.post( }, ); -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"); +router.delete( + "/:id", + route({ + responses: { + 204: {}, + 400: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), + 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({ - where: { id: req.user_id }, - select: userProjection, - relations: ["relationships"], - }); - const friend = await User.findOneOrFail({ - where: { id: id }, - select: userProjection, - relations: ["relationships"], - }); + const user = await User.findOneOrFail({ + where: { id: req.user_id }, + select: userProjection, + relations: ["relationships"], + }); + const friend = await User.findOneOrFail({ + where: { id: id }, + select: userProjection, + relations: ["relationships"], + }); - const relationship = user.relationships.find((x) => x.to_id === id); - const friendRequest = friend.relationships.find( - (x) => x.to_id === req.user_id, - ); + const relationship = user.relationships.find((x) => x.to_id === id); + const friendRequest = friend.relationships.find( + (x) => x.to_id === req.user_id, + ); - if (!relationship) - throw new HTTPError("You are not friends with the user", 404); - if (relationship?.type === RelationshipType.blocked) { - // unblock user + if (!relationship) + throw new HTTPError("You are not friends with the user", 404); + if (relationship?.type === RelationshipType.blocked) { + // unblock user + + await Promise.all([ + Relationship.delete({ id: relationship.id }), + emitEvent({ + event: "RELATIONSHIP_REMOVE", + user_id: req.user_id, + data: relationship.toPublicRelationship(), + } as RelationshipRemoveEvent), + ]); + return res.sendStatus(204); + } + if (friendRequest && friendRequest.type !== RelationshipType.blocked) { + await Promise.all([ + Relationship.delete({ id: friendRequest.id }), + await emitEvent({ + event: "RELATIONSHIP_REMOVE", + data: friendRequest.toPublicRelationship(), + user_id: id, + } as RelationshipRemoveEvent), + ]); + } await Promise.all([ Relationship.delete({ id: relationship.id }), emitEvent({ event: "RELATIONSHIP_REMOVE", - user_id: req.user_id, data: relationship.toPublicRelationship(), + user_id: req.user_id, } as RelationshipRemoveEvent), ]); - return res.sendStatus(204); - } - if (friendRequest && friendRequest.type !== RelationshipType.blocked) { - await Promise.all([ - Relationship.delete({ id: friendRequest.id }), - await emitEvent({ - event: "RELATIONSHIP_REMOVE", - data: friendRequest.toPublicRelationship(), - user_id: id, - } as RelationshipRemoveEvent), - ]); - } - await Promise.all([ - Relationship.delete({ id: relationship.id }), - emitEvent({ - event: "RELATIONSHIP_REMOVE", - data: relationship.toPublicRelationship(), - user_id: req.user_id, - } as RelationshipRemoveEvent), - ]); - - return res.sendStatus(204); -}); + return res.sendStatus(204); + }, +); export default router; diff --git a/src/api/routes/users/@me/settings.ts b/src/api/routes/users/@me/settings.ts index 62cfe904..d22d6de1 100644 --- a/src/api/routes/users/@me/settings.ts +++ b/src/api/routes/users/@me/settings.ts @@ -16,23 +16,49 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Response, Request } from "express"; -import { User, UserSettingsSchema } from "@spacebar/util"; import { route } from "@spacebar/api"; +import { User, UserSettingsSchema } from "@spacebar/util"; +import { Request, Response, Router } from "express"; const router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - const user = await User.findOneOrFail({ - where: { id: req.user_id }, - relations: ["settings"], - }); - return res.json(user.settings); -}); +router.get( + "/", + route({ + responses: { + 200: { + body: "UserSettings", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const user = await User.findOneOrFail({ + where: { id: req.user_id }, + relations: ["settings"], + }); + return res.json(user.settings); + }, +); router.patch( "/", - route({ body: "UserSettingsSchema" }), + route({ + requestBody: "UserSettingsSchema", + responses: { + 200: { + body: "UserSettings", + }, + 400: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const body = req.body as UserSettingsSchema; if (body.locale === "en") body.locale = "en-US"; // fix discord client crash on unkown locale diff --git a/src/api/routes/voice/regions.ts b/src/api/routes/voice/regions.ts index 59bac07f..10a8b21d 100644 --- a/src/api/routes/voice/regions.ts +++ b/src/api/routes/voice/regions.ts @@ -16,14 +16,23 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Router, Request, Response } from "express"; -import { getIpAdress, route } from "@spacebar/api"; -import { getVoiceRegions } from "@spacebar/api"; +import { getIpAdress, getVoiceRegions, route } from "@spacebar/api"; +import { Request, Response, Router } from "express"; const router: Router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - res.json(await getVoiceRegions(getIpAdress(req), true)); //vip true? -}); +router.get( + "/", + route({ + responses: { + 200: { + body: "APIGuildVoiceRegion", + }, + }, + }), + async (req: Request, res: Response) => { + res.json(await getVoiceRegions(getIpAdress(req), true)); //vip true? + }, +); export default router; diff --git a/src/api/util/handlers/route.ts b/src/api/util/handlers/route.ts index 604df4e9..5a0b48e6 100644 --- a/src/api/util/handlers/route.ts +++ b/src/api/util/handlers/route.ts @@ -17,21 +17,21 @@ */ import { - ajv, DiscordApiErrors, EVENT, FieldErrors, - SpacebarApiErrors, - getPermission, - getRights, - normalizeBody, PermissionResolvable, Permissions, RightResolvable, Rights, + SpacebarApiErrors, + ajv, + getPermission, + getRights, + normalizeBody, } from "@spacebar/util"; -import { NextFunction, Request, Response } from "express"; import { AnyValidateFunction } from "ajv/dist/core"; +import { NextFunction, Request, Response } from "express"; declare global { // TODO: fix this @@ -52,21 +52,40 @@ export type RouteResponse = { export interface RouteOptions { permission?: PermissionResolvable; right?: RightResolvable; - body?: `${string}Schema`; // typescript interface name - test?: { - response?: RouteResponse; - body?: unknown; - path?: string; - event?: EVENT | EVENT[]; - headers?: Record<string, string>; + requestBody?: `${string}Schema`; // typescript interface name + responses?: { + [status: number]: { + // body?: `${string}Response`; + body?: string; + }; + }; + event?: EVENT | EVENT[]; + summary?: string; + description?: string; + query?: { + [key: string]: { + type: string; + required?: boolean; + description?: string; + values?: string[]; + }; }; + deprecated?: boolean; + // test?: { + // response?: RouteResponse; + // body?: unknown; + // path?: string; + // event?: EVENT | EVENT[]; + // headers?: Record<string, string>; + // }; } export function route(opts: RouteOptions) { let validate: AnyValidateFunction | undefined; - if (opts.body) { - validate = ajv.getSchema(opts.body); - if (!validate) throw new Error(`Body schema ${opts.body} not found`); + if (opts.requestBody) { + validate = ajv.getSchema(opts.requestBody); + if (!validate) + throw new Error(`Body schema ${opts.requestBody} not found`); } return async (req: Request, res: Response, next: NextFunction) => { diff --git a/src/api/util/utility/ipAddress.ts b/src/api/util/utility/ipAddress.ts index 172e9604..c51daf6c 100644 --- a/src/api/util/utility/ipAddress.ts +++ b/src/api/util/utility/ipAddress.ts @@ -102,7 +102,7 @@ export function getIpAdress(req: Request): string { return ( // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - req.headers[Config.get().security.forwadedFor] || + req.headers[Config.get().security.forwardedFor] || req.socket.remoteAddress ); } |