diff options
Diffstat (limited to 'src')
210 files changed, 5586 insertions, 2652 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 ); } diff --git a/src/connections/BattleNet/index.ts b/src/connections/BattleNet/index.ts index 7edc2e92..4fdfccb1 100644 --- a/src/connections/BattleNet/index.ts +++ b/src/connections/BattleNet/index.ts @@ -19,12 +19,12 @@ import { ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, + Connection, ConnectionCallbackSchema, ConnectionLoader, DiscordApiErrors, } from "@spacebar/util"; import wretch from "wretch"; -import Connection from "../../util/connections/Connection"; import { BattleNetSettings } from "./BattleNetSettings"; interface BattleNetConnectionUser { @@ -33,10 +33,10 @@ interface BattleNetConnectionUser { battletag: string; } -interface BattleNetErrorResponse { - error: string; - error_description: string; -} +// interface BattleNetErrorResponse { +// error: string; +// error_description: string; +// } export default class BattleNetConnection extends Connection { public readonly id = "battlenet"; @@ -47,17 +47,21 @@ export default class BattleNetConnection extends Connection { settings: BattleNetSettings = new BattleNetSettings(); init(): void { - this.settings = ConnectionLoader.getConnectionConfig( - this.id, - this.settings, - ) as BattleNetSettings; + const settings = + ConnectionLoader.getConnectionConfig<BattleNetSettings>( + this.id, + this.settings, + ); + + if (settings.enabled && (!settings.clientId || !settings.clientSecret)) + throw new Error(`Invalid settings for connection ${this.id}`); } getAuthorizationUrl(userId: string): string { const state = this.createState(userId); const url = new URL(this.authorizeUrl); - url.searchParams.append("client_id", this.settings.clientId!); + url.searchParams.append("client_id", this.settings.clientId as string); url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("scope", this.scopes.join(" ")); url.searchParams.append("state", state); @@ -85,8 +89,8 @@ export default class BattleNetConnection extends Connection { new URLSearchParams({ grant_type: "authorization_code", code: code, - client_id: this.settings.clientId!, - client_secret: this.settings.clientSecret!, + client_id: this.settings.clientId as string, + client_secret: this.settings.clientSecret as string, redirect_uri: this.getRedirectUri(), }), ) @@ -115,8 +119,11 @@ export default class BattleNetConnection extends Connection { async handleCallback( params: ConnectionCallbackSchema, ): Promise<ConnectedAccount | null> { - const userId = this.getUserId(params.state); - const tokenData = await this.exchangeCode(params.state, params.code!); + const { state, code } = params; + if (!code) throw new Error("No code provided"); + + const userId = this.getUserId(state); + const tokenData = await this.exchangeCode(state, code); const userInfo = await this.getUser(tokenData.access_token); const exists = await this.hasConnection(userId, userInfo.id.toString()); diff --git a/src/connections/Discord/index.ts b/src/connections/Discord/index.ts index 76de33be..731086f1 100644 --- a/src/connections/Discord/index.ts +++ b/src/connections/Discord/index.ts @@ -19,12 +19,12 @@ import { ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, + Connection, ConnectionCallbackSchema, ConnectionLoader, DiscordApiErrors, } from "@spacebar/util"; import wretch from "wretch"; -import Connection from "../../util/connections/Connection"; import { DiscordSettings } from "./DiscordSettings"; interface UserResponse { @@ -43,10 +43,13 @@ export default class DiscordConnection extends Connection { settings: DiscordSettings = new DiscordSettings(); init(): void { - this.settings = ConnectionLoader.getConnectionConfig( + const settings = ConnectionLoader.getConnectionConfig<DiscordSettings>( this.id, this.settings, - ) as DiscordSettings; + ); + + if (settings.enabled && (!settings.clientId || !settings.clientSecret)) + throw new Error(`Invalid settings for connection ${this.id}`); } getAuthorizationUrl(userId: string): string { @@ -54,7 +57,7 @@ export default class DiscordConnection extends Connection { const url = new URL(this.authorizeUrl); url.searchParams.append("state", state); - url.searchParams.append("client_id", this.settings.clientId!); + url.searchParams.append("client_id", this.settings.clientId as string); url.searchParams.append("scope", this.scopes.join(" ")); url.searchParams.append("response_type", "code"); // controls whether, on repeated authorizations, the consent screen is shown @@ -82,8 +85,8 @@ export default class DiscordConnection extends Connection { }) .body( new URLSearchParams({ - client_id: this.settings.clientId!, - client_secret: this.settings.clientSecret!, + client_id: this.settings.clientId as string, + client_secret: this.settings.clientSecret as string, grant_type: "authorization_code", code: code, redirect_uri: this.getRedirectUri(), @@ -114,8 +117,11 @@ export default class DiscordConnection extends Connection { async handleCallback( params: ConnectionCallbackSchema, ): Promise<ConnectedAccount | null> { - const userId = this.getUserId(params.state); - const tokenData = await this.exchangeCode(params.state, params.code!); + const { state, code } = params; + if (!code) throw new Error("No code provided"); + + const userId = this.getUserId(state); + const tokenData = await this.exchangeCode(state, code); const userInfo = await this.getUser(tokenData.access_token); const exists = await this.hasConnection(userId, userInfo.id); diff --git a/src/connections/EpicGames/index.ts b/src/connections/EpicGames/index.ts index bd7c7eef..e5b2d336 100644 --- a/src/connections/EpicGames/index.ts +++ b/src/connections/EpicGames/index.ts @@ -19,12 +19,12 @@ import { ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, + Connection, ConnectionCallbackSchema, ConnectionLoader, DiscordApiErrors, } from "@spacebar/util"; import wretch from "wretch"; -import Connection from "../../util/connections/Connection"; import { EpicGamesSettings } from "./EpicGamesSettings"; export interface UserResponse { @@ -53,17 +53,21 @@ export default class EpicGamesConnection extends Connection { settings: EpicGamesSettings = new EpicGamesSettings(); init(): void { - this.settings = ConnectionLoader.getConnectionConfig( - this.id, - this.settings, - ) as EpicGamesSettings; + const settings = + ConnectionLoader.getConnectionConfig<EpicGamesSettings>( + this.id, + this.settings, + ); + + if (settings.enabled && (!settings.clientId || !settings.clientSecret)) + throw new Error(`Invalid settings for connection ${this.id}`); } getAuthorizationUrl(userId: string): string { const state = this.createState(userId); const url = new URL(this.authorizeUrl); - url.searchParams.append("client_id", this.settings.clientId!); + url.searchParams.append("client_id", this.settings.clientId as string); url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("response_type", "code"); url.searchParams.append("scope", this.scopes.join(" ")); @@ -127,8 +131,11 @@ export default class EpicGamesConnection extends Connection { async handleCallback( params: ConnectionCallbackSchema, ): Promise<ConnectedAccount | null> { - const userId = this.getUserId(params.state); - const tokenData = await this.exchangeCode(params.state, params.code!); + const { state, code } = params; + if (!code) throw new Error("No code provided"); + + const userId = this.getUserId(state); + const tokenData = await this.exchangeCode(state, code); const userInfo = await this.getUser(tokenData.access_token); const exists = await this.hasConnection(userId, userInfo[0].accountId); diff --git a/src/connections/Facebook/index.ts b/src/connections/Facebook/index.ts index 6ce722dd..2bf26f34 100644 --- a/src/connections/Facebook/index.ts +++ b/src/connections/Facebook/index.ts @@ -19,12 +19,12 @@ import { ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, + Connection, ConnectionCallbackSchema, ConnectionLoader, DiscordApiErrors, } from "@spacebar/util"; import wretch from "wretch"; -import Connection from "../../util/connections/Connection"; import { FacebookSettings } from "./FacebookSettings"; export interface FacebookErrorResponse { @@ -52,17 +52,20 @@ export default class FacebookConnection extends Connection { settings: FacebookSettings = new FacebookSettings(); init(): void { - this.settings = ConnectionLoader.getConnectionConfig( + const settings = ConnectionLoader.getConnectionConfig<FacebookSettings>( this.id, this.settings, - ) as FacebookSettings; + ); + + if (settings.enabled && (!settings.clientId || !settings.clientSecret)) + throw new Error(`Invalid settings for connection ${this.id}`); } getAuthorizationUrl(userId: string): string { const state = this.createState(userId); const url = new URL(this.authorizeUrl); - url.searchParams.append("client_id", this.settings.clientId!); + url.searchParams.append("client_id", this.settings.clientId as string); url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("state", state); url.searchParams.append("response_type", "code"); @@ -73,8 +76,11 @@ export default class FacebookConnection extends Connection { getTokenUrl(code: string): string { const url = new URL(this.tokenUrl); - url.searchParams.append("client_id", this.settings.clientId!); - url.searchParams.append("client_secret", this.settings.clientSecret!); + url.searchParams.append("client_id", this.settings.clientId as string); + url.searchParams.append( + "client_secret", + this.settings.clientSecret as string, + ); url.searchParams.append("code", code); url.searchParams.append("redirect_uri", this.getRedirectUri()); return url.toString(); @@ -118,8 +124,11 @@ export default class FacebookConnection extends Connection { async handleCallback( params: ConnectionCallbackSchema, ): Promise<ConnectedAccount | null> { - const userId = this.getUserId(params.state); - const tokenData = await this.exchangeCode(params.state, params.code!); + const { state, code } = params; + if (!code) throw new Error("No code provided"); + + const userId = this.getUserId(state); + const tokenData = await this.exchangeCode(state, code); const userInfo = await this.getUser(tokenData.access_token); const exists = await this.hasConnection(userId, userInfo.id); diff --git a/src/connections/GitHub/index.ts b/src/connections/GitHub/index.ts index a675873f..25e5f89f 100644 --- a/src/connections/GitHub/index.ts +++ b/src/connections/GitHub/index.ts @@ -19,12 +19,12 @@ import { ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, + Connection, ConnectionCallbackSchema, ConnectionLoader, DiscordApiErrors, } from "@spacebar/util"; import wretch from "wretch"; -import Connection from "../../util/connections/Connection"; import { GitHubSettings } from "./GitHubSettings"; interface UserResponse { @@ -42,17 +42,20 @@ export default class GitHubConnection extends Connection { settings: GitHubSettings = new GitHubSettings(); init(): void { - this.settings = ConnectionLoader.getConnectionConfig( + const settings = ConnectionLoader.getConnectionConfig<GitHubSettings>( this.id, this.settings, - ) as GitHubSettings; + ); + + if (settings.enabled && (!settings.clientId || !settings.clientSecret)) + throw new Error(`Invalid settings for connection ${this.id}`); } getAuthorizationUrl(userId: string): string { const state = this.createState(userId); const url = new URL(this.authorizeUrl); - url.searchParams.append("client_id", this.settings.clientId!); + url.searchParams.append("client_id", this.settings.clientId as string); url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("scope", this.scopes.join(" ")); url.searchParams.append("state", state); @@ -61,8 +64,11 @@ export default class GitHubConnection extends Connection { getTokenUrl(code: string): string { const url = new URL(this.tokenUrl); - url.searchParams.append("client_id", this.settings.clientId!); - url.searchParams.append("client_secret", this.settings.clientSecret!); + url.searchParams.append("client_id", this.settings.clientId as string); + url.searchParams.append( + "client_secret", + this.settings.clientSecret as string, + ); url.searchParams.append("code", code); return url.toString(); } @@ -105,8 +111,11 @@ export default class GitHubConnection extends Connection { async handleCallback( params: ConnectionCallbackSchema, ): Promise<ConnectedAccount | null> { - const userId = this.getUserId(params.state); - const tokenData = await this.exchangeCode(params.state, params.code!); + const { state, code } = params; + if (!code) throw new Error("No code provided"); + + const userId = this.getUserId(state); + const tokenData = await this.exchangeCode(state, code); const userInfo = await this.getUser(tokenData.access_token); const exists = await this.hasConnection(userId, userInfo.id.toString()); diff --git a/src/connections/Reddit/index.ts b/src/connections/Reddit/index.ts index 191c6452..149cce02 100644 --- a/src/connections/Reddit/index.ts +++ b/src/connections/Reddit/index.ts @@ -19,12 +19,12 @@ import { ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, + Connection, ConnectionCallbackSchema, ConnectionLoader, DiscordApiErrors, } from "@spacebar/util"; import wretch from "wretch"; -import Connection from "../../util/connections/Connection"; import { RedditSettings } from "./RedditSettings"; export interface UserResponse { @@ -54,17 +54,20 @@ export default class RedditConnection extends Connection { settings: RedditSettings = new RedditSettings(); init(): void { - this.settings = ConnectionLoader.getConnectionConfig( + const settings = ConnectionLoader.getConnectionConfig<RedditSettings>( this.id, this.settings, - ) as RedditSettings; + ); + + if (settings.enabled && (!settings.clientId || !settings.clientSecret)) + throw new Error(`Invalid settings for connection ${this.id}`); } getAuthorizationUrl(userId: string): string { const state = this.createState(userId); const url = new URL(this.authorizeUrl); - url.searchParams.append("client_id", this.settings.clientId!); + url.searchParams.append("client_id", this.settings.clientId as string); url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("response_type", "code"); url.searchParams.append("scope", this.scopes.join(" ")); @@ -124,8 +127,11 @@ export default class RedditConnection extends Connection { async handleCallback( params: ConnectionCallbackSchema, ): Promise<ConnectedAccount | null> { - const userId = this.getUserId(params.state); - const tokenData = await this.exchangeCode(params.state, params.code!); + const { state, code } = params; + if (!code) throw new Error("No code provided"); + + const userId = this.getUserId(state); + const tokenData = await this.exchangeCode(state, code); const userInfo = await this.getUser(tokenData.access_token); const exists = await this.hasConnection(userId, userInfo.id.toString()); diff --git a/src/connections/Spotify/index.ts b/src/connections/Spotify/index.ts index 61b17366..ece404d8 100644 --- a/src/connections/Spotify/index.ts +++ b/src/connections/Spotify/index.ts @@ -22,9 +22,9 @@ import { ConnectionCallbackSchema, ConnectionLoader, DiscordApiErrors, + RefreshableConnection, } from "@spacebar/util"; import wretch from "wretch"; -import RefreshableConnection from "../../util/connections/RefreshableConnection"; import { SpotifySettings } from "./SpotifySettings"; export interface UserResponse { @@ -63,17 +63,20 @@ export default class SpotifyConnection extends RefreshableConnection { * So to prevent spamming the spotify api we disable the ability to refresh. */ this.refreshEnabled = false; - this.settings = ConnectionLoader.getConnectionConfig( + const settings = ConnectionLoader.getConnectionConfig<SpotifySettings>( this.id, this.settings, - ) as SpotifySettings; + ); + + if (settings.enabled && (!settings.clientId || !settings.clientSecret)) + throw new Error(`Invalid settings for connection ${this.id}`); } getAuthorizationUrl(userId: string): string { const state = this.createState(userId); const url = new URL(this.authorizeUrl); - url.searchParams.append("client_id", this.settings.clientId!); + url.searchParams.append("client_id", this.settings.clientId as string); url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("response_type", "code"); url.searchParams.append("scope", this.scopes.join(" ")); @@ -98,7 +101,9 @@ export default class SpotifyConnection extends RefreshableConnection { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded", Authorization: `Basic ${Buffer.from( - `${this.settings.clientId!}:${this.settings.clientSecret!}`, + `${this.settings.clientId as string}:${ + this.settings.clientSecret as string + }`, ).toString("base64")}`, }) .body( @@ -129,7 +134,9 @@ export default class SpotifyConnection extends RefreshableConnection { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded", Authorization: `Basic ${Buffer.from( - `${this.settings.clientId!}:${this.settings.clientSecret!}`, + `${this.settings.clientId as string}:${ + this.settings.clientSecret as string + }`, ).toString("base64")}`, }) .body( @@ -169,8 +176,11 @@ export default class SpotifyConnection extends RefreshableConnection { async handleCallback( params: ConnectionCallbackSchema, ): Promise<ConnectedAccount | null> { - const userId = this.getUserId(params.state); - const tokenData = await this.exchangeCode(params.state, params.code!); + const { state, code } = params; + if (!code) throw new Error("No code provided"); + + const userId = this.getUserId(state); + const tokenData = await this.exchangeCode(state, code); const userInfo = await this.getUser(tokenData.access_token); const exists = await this.hasConnection(userId, userInfo.id); diff --git a/src/connections/Twitch/index.ts b/src/connections/Twitch/index.ts index 6d679aa4..9a6cea35 100644 --- a/src/connections/Twitch/index.ts +++ b/src/connections/Twitch/index.ts @@ -22,9 +22,9 @@ import { ConnectionCallbackSchema, ConnectionLoader, DiscordApiErrors, + RefreshableConnection, } from "@spacebar/util"; import wretch from "wretch"; -import RefreshableConnection from "../../util/connections/RefreshableConnection"; import { TwitchSettings } from "./TwitchSettings"; interface TwitchConnectionUserResponse { @@ -55,17 +55,20 @@ export default class TwitchConnection extends RefreshableConnection { settings: TwitchSettings = new TwitchSettings(); init(): void { - this.settings = ConnectionLoader.getConnectionConfig( + const settings = ConnectionLoader.getConnectionConfig<TwitchSettings>( this.id, this.settings, - ) as TwitchSettings; + ); + + if (settings.enabled && (!settings.clientId || !settings.clientSecret)) + throw new Error(`Invalid settings for connection ${this.id}`); } getAuthorizationUrl(userId: string): string { const state = this.createState(userId); const url = new URL(this.authorizeUrl); - url.searchParams.append("client_id", this.settings.clientId!); + url.searchParams.append("client_id", this.settings.clientId as string); url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("response_type", "code"); url.searchParams.append("scope", this.scopes.join(" ")); @@ -94,8 +97,8 @@ export default class TwitchConnection extends RefreshableConnection { new URLSearchParams({ grant_type: "authorization_code", code: code, - client_id: this.settings.clientId!, - client_secret: this.settings.clientSecret!, + client_id: this.settings.clientId as string, + client_secret: this.settings.clientSecret as string, redirect_uri: this.getRedirectUri(), }), ) @@ -124,8 +127,8 @@ export default class TwitchConnection extends RefreshableConnection { .body( new URLSearchParams({ grant_type: "refresh_token", - client_id: this.settings.clientId!, - client_secret: this.settings.clientSecret!, + client_id: this.settings.clientId as string, + client_secret: this.settings.clientSecret as string, refresh_token: refresh_token, }), ) @@ -148,7 +151,7 @@ export default class TwitchConnection extends RefreshableConnection { return wretch(url.toString()) .headers({ Authorization: `Bearer ${token}`, - "Client-Id": this.settings.clientId!, + "Client-Id": this.settings.clientId as string, }) .get() .json<TwitchConnectionUserResponse>() @@ -161,8 +164,11 @@ export default class TwitchConnection extends RefreshableConnection { async handleCallback( params: ConnectionCallbackSchema, ): Promise<ConnectedAccount | null> { - const userId = this.getUserId(params.state); - const tokenData = await this.exchangeCode(params.state, params.code!); + const { state, code } = params; + if (!code) throw new Error("No code provided"); + + const userId = this.getUserId(state); + const tokenData = await this.exchangeCode(state, code); const userInfo = await this.getUser(tokenData.access_token); const exists = await this.hasConnection(userId, userInfo.data[0].id); diff --git a/src/connections/Twitter/index.ts b/src/connections/Twitter/index.ts index aa48ca12..62fd7da1 100644 --- a/src/connections/Twitter/index.ts +++ b/src/connections/Twitter/index.ts @@ -22,9 +22,9 @@ import { ConnectionCallbackSchema, ConnectionLoader, DiscordApiErrors, + RefreshableConnection, } from "@spacebar/util"; import wretch from "wretch"; -import RefreshableConnection from "../../util/connections/RefreshableConnection"; import { TwitterSettings } from "./TwitterSettings"; interface TwitterUserResponse { @@ -40,10 +40,10 @@ interface TwitterUserResponse { }; } -interface TwitterErrorResponse { - error: string; - error_description: string; -} +// interface TwitterErrorResponse { +// error: string; +// error_description: string; +// } export default class TwitterConnection extends RefreshableConnection { public readonly id = "twitter"; @@ -55,17 +55,20 @@ export default class TwitterConnection extends RefreshableConnection { settings: TwitterSettings = new TwitterSettings(); init(): void { - this.settings = ConnectionLoader.getConnectionConfig( + const settings = ConnectionLoader.getConnectionConfig<TwitterSettings>( this.id, this.settings, - ) as TwitterSettings; + ); + + if (settings.enabled && (!settings.clientId || !settings.clientSecret)) + throw new Error(`Invalid settings for connection ${this.id}`); } getAuthorizationUrl(userId: string): string { const state = this.createState(userId); const url = new URL(this.authorizeUrl); - url.searchParams.append("client_id", this.settings.clientId!); + url.searchParams.append("client_id", this.settings.clientId as string); url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("response_type", "code"); url.searchParams.append("scope", this.scopes.join(" ")); @@ -92,14 +95,16 @@ export default class TwitterConnection extends RefreshableConnection { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded", Authorization: `Basic ${Buffer.from( - `${this.settings.clientId!}:${this.settings.clientSecret!}`, + `${this.settings.clientId as string}:${ + this.settings.clientSecret as string + }`, ).toString("base64")}`, }) .body( new URLSearchParams({ grant_type: "authorization_code", code: code, - client_id: this.settings.clientId!, + client_id: this.settings.clientId as string, redirect_uri: this.getRedirectUri(), code_verifier: "challenge", // TODO: properly use PKCE challenge }), @@ -126,14 +131,16 @@ export default class TwitterConnection extends RefreshableConnection { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded", Authorization: `Basic ${Buffer.from( - `${this.settings.clientId!}:${this.settings.clientSecret!}`, + `${this.settings.clientId as string}:${ + this.settings.clientSecret as string + }`, ).toString("base64")}`, }) .body( new URLSearchParams({ grant_type: "refresh_token", refresh_token, - client_id: this.settings.clientId!, + client_id: this.settings.clientId as string, redirect_uri: this.getRedirectUri(), code_verifier: "challenge", // TODO: properly use PKCE challenge }), @@ -163,8 +170,11 @@ export default class TwitterConnection extends RefreshableConnection { async handleCallback( params: ConnectionCallbackSchema, ): Promise<ConnectedAccount | null> { - const userId = this.getUserId(params.state); - const tokenData = await this.exchangeCode(params.state, params.code!); + const { state, code } = params; + if (!code) throw new Error("No code provided"); + + const userId = this.getUserId(state); + const tokenData = await this.exchangeCode(state, code); const userInfo = await this.getUser(tokenData.access_token); const exists = await this.hasConnection(userId, userInfo.data.id); diff --git a/src/connections/Xbox/index.ts b/src/connections/Xbox/index.ts index c592fd0b..935ff7ab 100644 --- a/src/connections/Xbox/index.ts +++ b/src/connections/Xbox/index.ts @@ -19,12 +19,12 @@ import { ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, + Connection, ConnectionCallbackSchema, ConnectionLoader, DiscordApiErrors, } from "@spacebar/util"; import wretch from "wretch"; -import Connection from "../../util/connections/Connection"; import { XboxSettings } from "./XboxSettings"; interface XboxUserResponse { @@ -44,10 +44,10 @@ interface XboxUserResponse { }; } -interface XboxErrorResponse { - error: string; - error_description: string; -} +// interface XboxErrorResponse { +// error: string; +// error_description: string; +// } export default class XboxConnection extends Connection { public readonly id = "xbox"; @@ -62,17 +62,20 @@ export default class XboxConnection extends Connection { settings: XboxSettings = new XboxSettings(); init(): void { - this.settings = ConnectionLoader.getConnectionConfig( + const settings = ConnectionLoader.getConnectionConfig<XboxSettings>( this.id, this.settings, - ) as XboxSettings; + ); + + if (settings.enabled && (!settings.clientId || !settings.clientSecret)) + throw new Error(`Invalid settings for connection ${this.id}`); } getAuthorizationUrl(userId: string): string { const state = this.createState(userId); const url = new URL(this.authorizeUrl); - url.searchParams.append("client_id", this.settings.clientId!); + url.searchParams.append("client_id", this.settings.clientId as string); url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("response_type", "code"); url.searchParams.append("scope", this.scopes.join(" ")); @@ -124,14 +127,16 @@ export default class XboxConnection extends Connection { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded", Authorization: `Basic ${Buffer.from( - `${this.settings.clientId!}:${this.settings.clientSecret!}`, + `${this.settings.clientId as string}:${ + this.settings.clientSecret as string + }`, ).toString("base64")}`, }) .body( new URLSearchParams({ grant_type: "authorization_code", code: code, - client_id: this.settings.clientId!, + client_id: this.settings.clientId as string, redirect_uri: this.getRedirectUri(), scope: this.scopes.join(" "), }), @@ -174,8 +179,11 @@ export default class XboxConnection extends Connection { async handleCallback( params: ConnectionCallbackSchema, ): Promise<ConnectedAccount | null> { - const userId = this.getUserId(params.state); - const tokenData = await this.exchangeCode(params.state, params.code!); + const { state, code } = params; + if (!code) throw new Error("No code provided"); + + const userId = this.getUserId(state); + const tokenData = await this.exchangeCode(state, code); const userToken = await this.getUserToken(tokenData.access_token); const userInfo = await this.getUser(userToken); diff --git a/src/connections/Youtube/index.ts b/src/connections/Youtube/index.ts index f3a43fcc..844803cf 100644 --- a/src/connections/Youtube/index.ts +++ b/src/connections/Youtube/index.ts @@ -19,12 +19,12 @@ import { ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, + Connection, ConnectionCallbackSchema, ConnectionLoader, DiscordApiErrors, } from "@spacebar/util"; import wretch from "wretch"; -import Connection from "../../util/connections/Connection"; import { YoutubeSettings } from "./YoutubeSettings"; interface YouTubeConnectionChannelListResult { @@ -62,17 +62,20 @@ export default class YoutubeConnection extends Connection { settings: YoutubeSettings = new YoutubeSettings(); init(): void { - this.settings = ConnectionLoader.getConnectionConfig( + const settings = ConnectionLoader.getConnectionConfig<YoutubeSettings>( this.id, this.settings, - ) as YoutubeSettings; + ); + + if (settings.enabled && (!settings.clientId || !settings.clientSecret)) + throw new Error(`Invalid settings for connection ${this.id}`); } getAuthorizationUrl(userId: string): string { const state = this.createState(userId); const url = new URL(this.authorizeUrl); - url.searchParams.append("client_id", this.settings.clientId!); + url.searchParams.append("client_id", this.settings.clientId as string); url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("response_type", "code"); url.searchParams.append("scope", this.scopes.join(" ")); @@ -101,8 +104,8 @@ export default class YoutubeConnection extends Connection { new URLSearchParams({ grant_type: "authorization_code", code: code, - client_id: this.settings.clientId!, - client_secret: this.settings.clientSecret!, + client_id: this.settings.clientId as string, + client_secret: this.settings.clientSecret as string, redirect_uri: this.getRedirectUri(), }), ) @@ -131,8 +134,11 @@ export default class YoutubeConnection extends Connection { async handleCallback( params: ConnectionCallbackSchema, ): Promise<ConnectedAccount | null> { - const userId = this.getUserId(params.state); - const tokenData = await this.exchangeCode(params.state, params.code!); + const { state, code } = params; + if (!code) throw new Error("No code provided"); + + const userId = this.getUserId(state); + const tokenData = await this.exchangeCode(state, code); const userInfo = await this.getUser(tokenData.access_token); const exists = await this.hasConnection(userId, userInfo.items[0].id); diff --git a/src/gateway/events/Close.ts b/src/gateway/events/Close.ts index 572037af..16f6b188 100644 --- a/src/gateway/events/Close.ts +++ b/src/gateway/events/Close.ts @@ -54,11 +54,19 @@ export async function Close(this: WebSocket, code: number, reason: Buffer) { status: "offline", }; + // TODO + // If a user was deleted, they may still be connected to gateway, + // which will cause this to throw when they disconnect. + // just send the ID of the user instead of the full correct payload for now + const userOrId = await User.getPublicUser(this.user_id).catch(() => ({ + id: this.user_id, + })); + await emitEvent({ event: "PRESENCE_UPDATE", user_id: this.user_id, data: { - user: await User.getPublicUser(this.user_id), + user: userOrId, activities: session.activities, client_status: session?.client_info, status: session.status, diff --git a/src/gateway/events/Connection.ts b/src/gateway/events/Connection.ts index 68273ace..1991ebbe 100644 --- a/src/gateway/events/Connection.ts +++ b/src/gateway/events/Connection.ts @@ -45,7 +45,7 @@ export async function Connection( socket: WebSocket, request: IncomingMessage, ) { - const forwardedFor = Config.get().security.forwadedFor; + const forwardedFor = Config.get().security.forwardedFor; const ipAddress = forwardedFor ? (request.headers[forwardedFor] as string) : request.socket.remoteAddress; diff --git a/src/gateway/opcodes/Heartbeat.ts b/src/gateway/opcodes/Heartbeat.ts index 7866c3e9..b9b62be3 100644 --- a/src/gateway/opcodes/Heartbeat.ts +++ b/src/gateway/opcodes/Heartbeat.ts @@ -25,5 +25,5 @@ export async function onHeartbeat(this: WebSocket) { setHeartbeat(this); - await Send(this, { op: 11 }); + await Send(this, { op: 11, d: {} }); } diff --git a/src/gateway/opcodes/Identify.ts b/src/gateway/opcodes/Identify.ts index 98fae3ed..7610901a 100644 --- a/src/gateway/opcodes/Identify.ts +++ b/src/gateway/opcodes/Identify.ts @@ -16,17 +16,23 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { WebSocket, Payload } from "@spacebar/gateway"; +import { + WebSocket, + Payload, + setupListener, + Capabilities, + CLOSECODES, + OPCODES, + Send, +} from "@spacebar/gateway"; import { checkToken, Intents, Member, ReadyEventData, - User, Session, EVENTEnum, Config, - PublicMember, PublicUser, PrivateUserProjection, ReadState, @@ -36,310 +42,385 @@ import { PrivateSessionProjection, MemberPrivateProjection, PresenceUpdateEvent, - UserSettings, IdentifySchema, DefaultUserGuildSettings, - UserGuildSettings, ReadyGuildDTO, Guild, - UserTokenData, - ConnectedAccount, + PublicUserProjection, + ReadyUserGuildSettingsEntries, + UserSettings, + Permissions, + DMChannel, + GuildOrUnavailable, + Recipient, + OPCodes, } from "@spacebar/util"; -import { Send } from "../util/Send"; -import { CLOSECODES, OPCODES } from "../util/Constants"; -import { setupListener } from "../listener/listener"; -// import experiments from "./experiments.json"; -const experiments: unknown[] = []; import { check } from "./instanceOf"; -import { Recipient } from "@spacebar/util"; // TODO: user sharding // TODO: check privileged intents, if defined in the config -// TODO: check if already identified - -// TODO: Refactor identify ( and lazyrequest, tbh ) export async function onIdentify(this: WebSocket, data: Payload) { + if (this.user_id) { + // we've already identified + return this.close(CLOSECODES.Already_authenticated); + } + clearTimeout(this.readyTimeout); - // TODO: is this needed now that we use `json-bigint`? - if (typeof data.d?.client_state?.highest_last_message_id === "number") - data.d.client_state.highest_last_message_id += ""; - check.call(this, IdentifySchema, data.d); + // Check payload matches schema + check.call(this, IdentifySchema, data.d); const identify: IdentifySchema = data.d; - let decoded: UserTokenData["decoded"]; - try { - const { jwtSecret } = Config.get().security; - decoded = (await checkToken(identify.token, jwtSecret)).decoded; // will throw an error if invalid - } catch (error) { - console.error("invalid token", error); - return this.close(CLOSECODES.Authentication_failed); - } - this.user_id = decoded.id; - const session_id = this.session_id; - - const [ - user, - read_states, - members, - recipients, - session, - application, - connected_accounts, - ] = await Promise.all([ - User.findOneOrFail({ - where: { id: this.user_id }, - relations: ["relationships", "relationships.to", "settings"], - select: [...PrivateUserProjection, "relationships"], - }), - ReadState.find({ where: { user_id: this.user_id } }), - Member.find({ - where: { id: this.user_id }, - select: MemberPrivateProjection, - relations: [ - "guild", - "guild.channels", - "guild.emojis", - "guild.roles", - "guild.stickers", - "user", - "roles", - ], - }), - Recipient.find({ - where: { user_id: this.user_id, closed: false }, - relations: [ - "channel", - "channel.recipients", - "channel.recipients.user", - ], - // TODO: public user selection - }), - // save the session and delete it when the websocket is closed - Session.create({ - user_id: this.user_id, - session_id: session_id, - // TODO: check if status is only one of: online, dnd, offline, idle - status: identify.presence?.status || "offline", //does the session always start as online? - client_info: { - //TODO read from identity - client: "desktop", - os: identify.properties?.os, - version: 0, - }, - activities: [], - }).save(), - Application.findOne({ where: { id: this.user_id } }), - ConnectedAccount.find({ where: { user_id: this.user_id } }), - ]); + this.capabilities = new Capabilities(identify.capabilities || 0); + const { user } = await checkToken(identify.token, { + relations: ["relationships", "relationships.to", "settings"], + select: [...PrivateUserProjection, "relationships"], + }); if (!user) return this.close(CLOSECODES.Authentication_failed); - if (!user.settings) { - user.settings = new UserSettings(); - await user.settings.save(); - } + this.user_id = user.id; - if (!identify.intents) identify.intents = BigInt("0x6ffffffff"); + // Check intents + if (!identify.intents) identify.intents = 30064771071n; // TODO: what is this number? this.intents = new Intents(identify.intents); + + // TODO: actually do intent things. + + // Validate sharding if (identify.shard) { this.shard_id = identify.shard[0]; this.shard_count = identify.shard[1]; + if ( this.shard_count == null || this.shard_id == null || - this.shard_id >= this.shard_count || + this.shard_id > this.shard_count || this.shard_id < 0 || this.shard_count <= 0 ) { - console.log(identify.shard); + // TODO: why do we even care about this right now? + console.log( + `[Gateway] Invalid sharding from ${user.id}: ${identify.shard}`, + ); return this.close(CLOSECODES.Invalid_shard); } } - let users: PublicUser[] = []; - const merged_members = members.map((x: Member) => { + // Generate a new gateway session ( id is already made, just save it in db ) + const session = Session.create({ + user_id: this.user_id, + session_id: this.session_id, + status: identify.presence?.status || "online", + client_info: { + client: identify.properties?.$device, + os: identify.properties?.os, + version: 0, + }, + activities: identify.presence?.activities, // TODO: validation + }); + + // Get from database: + // * the users read states + // * guild members for this user + // * recipients ( dm channels ) + // * the bot application, if it exists + const [, application, read_states, members, recipients] = await Promise.all( + [ + session.save(), + + Application.findOne({ + where: { id: this.user_id }, + select: ["id", "flags"], + }), + + ReadState.find({ + where: { user_id: this.user_id }, + select: [ + "id", + "channel_id", + "last_message_id", + "last_pin_timestamp", + "mention_count", + ], + }), + + Member.find({ + where: { id: this.user_id }, + select: { + // We only want some member props + ...Object.fromEntries( + MemberPrivateProjection.map((x) => [x, true]), + ), + settings: true, // guild settings + roles: { id: true }, // the full role is fetched from the `guild` relation + + // TODO: we don't really need every property of + // guild channels, emoji, roles, stickers + // but we do want almost everything from guild. + // How do you do that without just enumerating the guild props? + guild: true, + }, + relations: [ + "guild", + "guild.channels", + "guild.emojis", + "guild.roles", + "guild.stickers", + "roles", + + // For these entities, `user` is always just the logged in user we fetched above + // "user", + ], + }), + + Recipient.find({ + where: { user_id: this.user_id, closed: false }, + relations: [ + "channel", + "channel.recipients", + "channel.recipients.user", + ], + select: { + channel: { + id: true, + flags: true, + // is_spam: true, // TODO + last_message_id: true, + last_pin_timestamp: true, + type: true, + icon: true, + name: true, + owner_id: true, + recipients: { + // we don't actually need this ID or any other information about the recipient info, + // but typeorm does not select anything from the users relation of recipients unless we select + // at least one column. + id: true, + // We only want public user data for each dm channel + user: Object.fromEntries( + PublicUserProjection.map((x) => [x, true]), + ), + }, + }, + }, + }), + ], + ); + + // We forgot to migrate user settings from the JSON column of `users` + // to the `user_settings` table theyre in now, + // so for instances that migrated, users may not have a `user_settings` row. + if (!user.settings) { + user.settings = new UserSettings(); + await user.settings.save(); + } + + // Generate merged_members + const merged_members = members.map((x) => { return [ { ...x, roles: x.roles.map((x) => x.id), + + // add back user, which we don't fetch from db + // TODO: For guild profiles, this may need to be changed. + // TODO: The only field required in the user prop is `id`, + // but our types are annoying so I didn't bother. + user: user.toPublicUser(), + + guild: { + id: x.guild.id, + }, settings: undefined, - guild: undefined, }, ]; - }) as PublicMember[][]; - // TODO: This type is bad. - let guilds: Partial<Guild>[] = members.map((x) => ({ - ...x.guild, - joined_at: x.joined_at, - })); + }); - const pending_guilds: typeof guilds = []; - if (user.bot) - guilds = guilds.map((guild) => { - pending_guilds.push(guild); - return { id: guild.id, unavailable: true }; + // Populated with guilds 'unavailable' currently + // Just for bots + const pending_guilds: Guild[] = []; + + // Generate guilds list ( make them unavailable if user is bot ) + const guilds: GuildOrUnavailable[] = members.map((member) => { + // filter guild channels we don't have permission to view + // TODO: check if this causes issues when the user is granted other roles? + member.guild.channels = member.guild.channels.filter((channel) => { + const perms = Permissions.finalPermission({ + user: { + id: member.id, + roles: member.roles.map((x) => x.id), + }, + guild: member.guild, + channel, + }); + + return perms.has("VIEW_CHANNEL"); }); - // TODO: Rewrite this. Perhaps a DTO? - const user_guild_settings_entries = members.map((x) => ({ - ...DefaultUserGuildSettings, - ...x.settings, - guild_id: x.guild.id, - channel_overrides: Object.entries( - x.settings.channel_overrides ?? {}, - ).map((y) => ({ - ...y[1], - channel_id: y[0], - })), - })) as unknown as UserGuildSettings[]; - - const channels = recipients.map((x) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - x.channel.recipients = x.channel.recipients.map((x) => - x.user.toPublicUser(), - ); - //TODO is this needed? check if users in group dm that are not friends are sent in the READY event - users = users.concat(x.channel.recipients as unknown as User[]); - if (x.channel.isDm()) { - x.channel.recipients = x.channel.recipients?.filter( - (x) => x.id !== this.user_id, - ); + if (user.bot) { + pending_guilds.push(member.guild); + return { id: member.guild.id, unavailable: true }; } - return x.channel; - }); - for (const relation of user.relationships) { - const related_user = relation.to; - const public_related_user = { - username: related_user.username, - discriminator: related_user.discriminator, - id: related_user.id, - public_flags: related_user.public_flags, - avatar: related_user.avatar, - bot: related_user.bot, - bio: related_user.bio, - premium_since: user.premium_since, - premium_type: user.premium_type, - accent_color: related_user.accent_color, + return { + ...member.guild.toJSON(), + joined_at: member.joined_at, + + threads: [], }; - users.push(public_related_user); - } + }); + + // Generate user_guild_settings + const user_guild_settings_entries: ReadyUserGuildSettingsEntries[] = + members.map((x) => ({ + ...DefaultUserGuildSettings, + ...x.settings, + guild_id: x.guild_id, + channel_overrides: Object.entries( + x.settings.channel_overrides ?? {}, + ).map((y) => ({ + ...y[1], + channel_id: y[0], + })), + })); + + // Popultaed with users from private channels, relationships. + // Uses a set to dedupe for us. + const users: Set<PublicUser> = new Set(); + + // Generate dm channels from recipients list. Append recipients to `users` list + const channels = recipients + .filter(({ channel }) => channel.isDm()) + .map((r) => { + // TODO: fix the types of Recipient + // Their channels are only ever private (I think) and thus are always DM channels + const channel = r.channel as DMChannel; + + // Remove ourself from the list of other users in dm channel + channel.recipients = channel.recipients.filter( + (recipient) => recipient.user.id !== this.user_id, + ); + + const channelUsers = channel.recipients?.map((recipient) => + recipient.user.toPublicUser(), + ); + + if (channelUsers && channelUsers.length > 0) + channelUsers.forEach((user) => users.add(user)); - setImmediate(async () => { - // run in seperate "promise context" because ready payload is not dependent on those events + return { + id: channel.id, + flags: channel.flags, + last_message_id: channel.last_message_id, + type: channel.type, + recipients: channelUsers || [], + is_spam: false, // TODO + }; + }); + + // From user relationships ( friends ), also append to `users` list + user.relationships.forEach((x) => users.add(x.to.toPublicUser())); + + // Send SESSIONS_REPLACE and PRESENCE_UPDATE + const allSessions = ( + await Session.find({ + where: { user_id: this.user_id }, + select: PrivateSessionProjection, + }) + ).map((x) => ({ + // TODO how is active determined? + // in our lazy request impl, we just pick the 'most relevant' session + active: x.session_id == session.session_id, + activities: x.activities, + client_info: x.client_info, + // TODO: what does all mean? + session_id: x.session_id == session.session_id ? "all" : x.session_id, + status: x.status, + })); + + Promise.all([ emitEvent({ event: "SESSIONS_REPLACE", user_id: this.user_id, - data: await Session.find({ - where: { user_id: this.user_id }, - select: PrivateSessionProjection, - }), - } as SessionsReplace); + data: allSessions, + } as SessionsReplace), emitEvent({ event: "PRESENCE_UPDATE", user_id: this.user_id, data: { - user: await User.getPublicUser(this.user_id), + user: user.toPublicUser(), activities: session.activities, - client_status: session?.client_info, + client_status: session.client_info, status: session.status, }, - } as PresenceUpdateEvent); - }); + } as PresenceUpdateEvent), + ]); - read_states.forEach((s: Partial<ReadState>) => { - s.id = s.channel_id; - delete s.user_id; - delete s.channel_id; - }); + // Build READY - const privateUser = { - avatar: user.avatar, - mobile: user.mobile, - desktop: user.desktop, - discriminator: user.discriminator, - email: user.email, - flags: user.flags, - id: user.id, - mfa_enabled: user.mfa_enabled, - nsfw_allowed: user.nsfw_allowed, - phone: user.phone, - premium: user.premium, - premium_type: user.premium_type, - public_flags: user.public_flags, - premium_usage_flags: user.premium_usage_flags, - purchased_flags: user.purchased_flags, - username: user.username, - verified: user.verified, - bot: user.bot, - accent_color: user.accent_color, - banner: user.banner, - bio: user.bio, - premium_since: user.premium_since, - }; + read_states.forEach((x) => { + x.id = x.channel_id; + }); const d: ReadyEventData = { v: 9, - application: { - id: application?.id ?? "", - flags: application?.flags ?? 0, - }, //TODO: check this code! - user: privateUser, + application: application + ? { id: application.id, flags: application.flags } + : undefined, + user: user.toPrivateUser(), user_settings: user.settings, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - guilds: guilds.map((x: Guild & { joined_at: Date }) => { - return { - ...new ReadyGuildDTO(x).toJSON(), - guild_hashes: {}, - joined_at: x.joined_at, - name: x.name, - icon: x.icon, - }; - }), - guild_experiments: [], // TODO - geo_ordered_rtc_regions: [], // TODO + guilds: this.capabilities.has(Capabilities.FLAGS.CLIENT_STATE_V2) + ? guilds.map((x) => new ReadyGuildDTO(x).toJSON()) + : guilds, relationships: user.relationships.map((x) => x.toPublicRelationship()), read_state: { entries: read_states, partial: false, - version: 304128, + version: 0, // TODO }, user_guild_settings: { entries: user_guild_settings_entries, - partial: false, // TODO partial - version: 642, + partial: false, + version: 0, // TODO }, private_channels: channels, - session_id: session_id, - analytics_token: "", // TODO - connected_accounts, - consents: { - personalization: { - consented: false, // TODO - }, - }, - country_code: user.settings.locale, - friend_suggestion_count: 0, // TODO - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - experiments: experiments, // TODO - guild_join_requests: [], // TODO what is this? - users: users.filter((x) => x).unique(), + session_id: this.session_id, + country_code: user.settings.locale, // TODO: do ip analysis instead + users: Array.from(users), merged_members: merged_members, - // shard // TODO: only for user sharding - sessions: [], // TODO: + sessions: allSessions, + + resume_gateway_url: + Config.get().gateway.endpointClient || + Config.get().gateway.endpointPublic || + "ws://127.0.0.1:3001", // lol hack whatever required_action: Config.get().login.requireVerification && !user.verified ? "REQUIRE_VERIFIED_EMAIL" : undefined, + + consents: { + personalization: { + consented: false, // TODO + }, + }, + experiments: [], + guild_join_requests: [], + connected_accounts: [], + guild_experiments: [], + geo_ordered_rtc_regions: [], + api_code_version: 1, + friend_suggestion_count: 0, + analytics_token: "", + tutorial: null, + session_type: "normal", // TODO + auth_session_id_hash: "", // TODO }; - // TODO: send real proper data structure + // Send READY await Send(this, { op: OPCODES.Dispatch, t: EVENTEnum.Ready, @@ -347,23 +428,41 @@ export async function onIdentify(this: WebSocket, data: Payload) { d, }); + // If we're a bot user, send GUILD_CREATE for each unavailable guild await Promise.all( - pending_guilds.map((guild) => + pending_guilds.map((x) => Send(this, { op: OPCODES.Dispatch, t: EVENTEnum.GuildCreate, s: this.sequence++, - d: guild, - })?.catch(console.error), + d: x, + })?.catch((e) => + console.error(`[Gateway] error when sending bot guilds`, e), + ), ), ); - //TODO send READY_SUPPLEMENTAL + // TODO: ready supplemental + await Send(this, { + op: OPCodes.DISPATCH, + t: EVENTEnum.ReadySupplemental, + s: this.sequence++, + d: { + merged_presences: { + guilds: [], + friends: [], + }, + // these merged members seem to be all users currently in vc in your guilds + merged_members: [], + lazy_private_channels: [], + guilds: [], // { voice_states: [], id: string, embedded_activities: [] } + // embedded_activities are users currently in an activity? + disclose: [], // Config.get().general.uniqueUsernames ? ["pomelo"] : [] + }, + }); + //TODO send GUILD_MEMBER_LIST_UPDATE - //TODO send SESSIONS_REPLACE //TODO send VOICE_STATE_UPDATE to let the client know if another device is already connected to a voice channel await setupListener.call(this); - - // console.log(`${this.ipAddress} identified as ${d.user.id}`); } diff --git a/src/gateway/opcodes/LazyRequest.ts b/src/gateway/opcodes/LazyRequest.ts index 64e50d92..4ad1ae7b 100644 --- a/src/gateway/opcodes/LazyRequest.ts +++ b/src/gateway/opcodes/LazyRequest.ts @@ -27,6 +27,8 @@ import { User, Presence, partition, + Channel, + Permissions, } from "@spacebar/util"; import { WebSocket, @@ -35,6 +37,7 @@ import { OPCODES, Send, } from "@spacebar/gateway"; +import murmur from "murmurhash-js/murmurhash3_gc"; import { check } from "./instanceOf"; // TODO: only show roles/members that have access to this channel @@ -92,7 +95,7 @@ async function getMembers(guild_id: string, range: [number, number]) { console.error(`LazyRequest`, e); } - if (!members) { + if (!members || !members.length) { return { items: [], groups: [], @@ -267,7 +270,31 @@ export async function onLazyRequest(this: WebSocket, { d }: Payload) { if (!Array.isArray(ranges)) throw new Error("Not a valid Array"); const member_count = await Member.count({ where: { guild_id } }); - const ops = await Promise.all(ranges.map((x) => getMembers(guild_id, x))); + const ops = await Promise.all( + ranges.map((x) => getMembers(guild_id, x as [number, number])), + ); + + let list_id = "everyone"; + + const channel = await Channel.findOneOrFail({ + where: { id: channel_id }, + }); + if (channel.permission_overwrites) { + const perms: string[] = []; + + channel.permission_overwrites.forEach((overwrite) => { + const { id, allow, deny } = overwrite; + + if (allow.toBigInt() & Permissions.FLAGS.VIEW_CHANNEL) + perms.push(`allow:${id}`); + else if (deny.toBigInt() & Permissions.FLAGS.VIEW_CHANNEL) + perms.push(`deny:${id}`); + }); + + if (perms.length > 0) { + list_id = murmur(perms.sort().join(",")).toString(); + } + } // TODO: unsubscribe member_events that are not in op.members @@ -297,7 +324,7 @@ export async function onLazyRequest(this: WebSocket, { d }: Payload) { member_count - (groups.find((x) => x.id == "offline")?.count ?? 0), member_count, - id: "everyone", + id: list_id, guild_id, groups, }, diff --git a/src/gateway/util/Capabilities.ts b/src/gateway/util/Capabilities.ts new file mode 100644 index 00000000..6c94bb45 --- /dev/null +++ b/src/gateway/util/Capabilities.ts @@ -0,0 +1,26 @@ +import { BitField, BitFieldResolvable, BitFlag } from "@spacebar/util"; + +export type CapabilityResolvable = BitFieldResolvable | CapabilityString; +type CapabilityString = keyof typeof Capabilities.FLAGS; + +export class Capabilities extends BitField { + static FLAGS = { + // Thanks, Opencord! + // https://github.com/MateriiApps/OpenCord/blob/master/app/src/main/java/com/xinto/opencord/gateway/io/Capabilities.kt + LAZY_USER_NOTES: BitFlag(0), + NO_AFFINE_USER_IDS: BitFlag(1), + VERSIONED_READ_STATES: BitFlag(2), + VERSIONED_USER_GUILD_SETTINGS: BitFlag(3), + DEDUPLICATE_USER_OBJECTS: BitFlag(4), + PRIORITIZED_READY_PAYLOAD: BitFlag(5), + MULTIPLE_GUILD_EXPERIMENT_POPULATIONS: BitFlag(6), + NON_CHANNEL_READ_STATES: BitFlag(7), + AUTH_TOKEN_REFRESH: BitFlag(8), + USER_SETTINGS_PROTO: BitFlag(9), + CLIENT_STATE_V2: BitFlag(10), + PASSIVE_GUILD_UPDATE: BitFlag(11), + }; + + any = (capability: CapabilityResolvable) => super.any(capability); + has = (capability: CapabilityResolvable) => super.has(capability); +} diff --git a/src/gateway/util/WebSocket.ts b/src/gateway/util/WebSocket.ts index 972129c7..833756ff 100644 --- a/src/gateway/util/WebSocket.ts +++ b/src/gateway/util/WebSocket.ts @@ -19,6 +19,7 @@ import { Intents, ListenEventOpts, Permissions } from "@spacebar/util"; import WS from "ws"; import { Deflate, Inflate } from "fast-zlib"; +import { Capabilities } from "./Capabilities"; // import { Client } from "@spacebar/webrtc"; export interface WebSocket extends WS { @@ -40,5 +41,6 @@ export interface WebSocket extends WS { events: Record<string, undefined | (() => unknown)>; member_events: Record<string, () => unknown>; listen_options: ListenEventOpts; + capabilities?: Capabilities; // client?: Client; } diff --git a/src/gateway/util/index.ts b/src/gateway/util/index.ts index 627f12b2..6ef694d9 100644 --- a/src/gateway/util/index.ts +++ b/src/gateway/util/index.ts @@ -21,3 +21,4 @@ export * from "./Send"; export * from "./SessionUtils"; export * from "./Heartbeat"; export * from "./WebSocket"; +export * from "./Capabilities"; diff --git a/src/util/config/types/GeneralConfiguration.ts b/src/util/config/types/GeneralConfiguration.ts index c20fe9a7..cff8c527 100644 --- a/src/util/config/types/GeneralConfiguration.ts +++ b/src/util/config/types/GeneralConfiguration.ts @@ -28,4 +28,5 @@ export class GeneralConfiguration { correspondenceUserID: string | null = null; image: string | null = null; instanceId: string = Snowflake.generate(); + autoCreateBotUsers: boolean = false; } diff --git a/src/util/config/types/SecurityConfiguration.ts b/src/util/config/types/SecurityConfiguration.ts index 5e971cfe..35776642 100644 --- a/src/util/config/types/SecurityConfiguration.ts +++ b/src/util/config/types/SecurityConfiguration.ts @@ -28,7 +28,7 @@ export class SecurityConfiguration { // header to get the real user ip address // X-Forwarded-For for nginx/reverse proxies // CF-Connecting-IP for cloudflare - forwadedFor: string | null = null; + forwardedFor: string | null = null; ipdataApiKey: string | null = "eca677b284b3bac29eb72f5e496aa9047f26543605efe99ff2ce35c9"; mfaBackupCodeCount: number = 10; diff --git a/src/util/config/types/subconfigurations/limits/RateLimits.ts b/src/util/config/types/subconfigurations/limits/RateLimits.ts index caba740b..0ce0827c 100644 --- a/src/util/config/types/subconfigurations/limits/RateLimits.ts +++ b/src/util/config/types/subconfigurations/limits/RateLimits.ts @@ -16,11 +16,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { RouteRateLimit, RateLimitOptions } from "."; +import { RateLimitOptions, RouteRateLimit } from "."; export class RateLimits { enabled: boolean = false; - ip: Omit<RateLimitOptions, "bot_count"> = { + ip: RateLimitOptions = { count: 500, window: 5, }; diff --git a/src/util/connections/Connection.ts b/src/util/connections/Connection.ts index becee589..5bdebd47 100644 --- a/src/util/connections/Connection.ts +++ b/src/util/connections/Connection.ts @@ -24,7 +24,7 @@ import { Config, DiscordApiErrors } from "../util"; /** * A connection that can be used to connect to an external service. */ -export default abstract class Connection { +export abstract class Connection { id: string; settings: { enabled: boolean }; states: Map<string, string> = new Map(); diff --git a/src/util/connections/ConnectionLoader.ts b/src/util/connections/ConnectionLoader.ts index 28f1a202..e9dc6973 100644 --- a/src/util/connections/ConnectionLoader.ts +++ b/src/util/connections/ConnectionLoader.ts @@ -16,9 +16,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ +import { Connection } from "@spacebar/util"; import fs from "fs"; import path from "path"; -import Connection from "./Connection"; import { ConnectionConfig } from "./ConnectionConfig"; import { ConnectionStore } from "./ConnectionStore"; @@ -48,8 +48,7 @@ export class ConnectionLoader { }); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public static getConnectionConfig(id: string, defaults?: any): any { + public static getConnectionConfig<T>(id: string, defaults?: unknown): T { let cfg = ConnectionConfig.get()[id]; if (defaults) { if (cfg) cfg = Object.assign({}, defaults, cfg); @@ -70,8 +69,7 @@ export class ConnectionLoader { public static async setConnectionConfig( id: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - config: Partial<any>, + config: Partial<unknown>, ): Promise<void> { if (!config) console.warn(`[Connections/WARN] ${id} tried to set config=null!`); diff --git a/src/util/connections/ConnectionStore.ts b/src/util/connections/ConnectionStore.ts index 39abfea6..95e54fd9 100644 --- a/src/util/connections/ConnectionStore.ts +++ b/src/util/connections/ConnectionStore.ts @@ -16,8 +16,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import Connection from "./Connection"; -import RefreshableConnection from "./RefreshableConnection"; +import { Connection } from "./Connection"; +import { RefreshableConnection } from "./RefreshableConnection"; export class ConnectionStore { public static connections: Map<string, Connection | RefreshableConnection> = diff --git a/src/util/connections/RefreshableConnection.ts b/src/util/connections/RefreshableConnection.ts index fd93adfa..88ad8dab 100644 --- a/src/util/connections/RefreshableConnection.ts +++ b/src/util/connections/RefreshableConnection.ts @@ -18,13 +18,14 @@ import { ConnectedAccount } from "../entities"; import { ConnectedAccountCommonOAuthTokenResponse } from "../interfaces"; -import Connection from "./Connection"; +import { Connection } from "./Connection"; /** * A connection that can refresh its token. */ -export default abstract class RefreshableConnection extends Connection { +export abstract class RefreshableConnection extends Connection { refreshEnabled = true; + /** * Refreshes the token for a connected account. * @param connectedAccount The connected account to refresh diff --git a/src/util/dtos/ConnectedAccountDTO.ts b/src/util/dtos/ConnectedAccountDTO.ts index 0a3604d5..f9efd980 100644 --- a/src/util/dtos/ConnectedAccountDTO.ts +++ b/src/util/dtos/ConnectedAccountDTO.ts @@ -30,7 +30,7 @@ export class ConnectedAccountDTO { verified?: boolean; visibility?: number; integrations?: string[]; - metadata_?: any; + metadata_?: unknown; metadata_visibility?: number; two_way_link?: boolean; diff --git a/src/util/dtos/ReadyGuildDTO.ts b/src/util/dtos/ReadyGuildDTO.ts index b21afe74..905ede74 100644 --- a/src/util/dtos/ReadyGuildDTO.ts +++ b/src/util/dtos/ReadyGuildDTO.ts @@ -18,13 +18,45 @@ import { Channel, + ChannelOverride, + ChannelType, Emoji, Guild, - PublicMember, + PublicUser, Role, Sticker, + UserGuildSettings, + PublicMember, } from "../entities"; +// TODO: this is not the best place for this type +export type ReadyUserGuildSettingsEntries = Omit< + UserGuildSettings, + "channel_overrides" +> & { + channel_overrides: (ChannelOverride & { channel_id: string })[]; +}; + +// TODO: probably should move somewhere else +export interface ReadyPrivateChannel { + id: string; + flags: number; + is_spam: boolean; + last_message_id?: string; + recipients: PublicUser[]; + type: ChannelType.DM | ChannelType.GROUP_DM; +} + +export type GuildOrUnavailable = + | { id: string; unavailable: boolean } + | (Guild & { joined_at?: Date; unavailable: undefined }); + +const guildIsAvailable = ( + guild: GuildOrUnavailable, +): guild is Guild & { joined_at: Date; unavailable: false } => { + return guild.unavailable != true; +}; + export interface IReadyGuildDTO { application_command_counts?: { 1: number; 2: number; 3: number }; // ???????????? channels: Channel[]; @@ -65,12 +97,21 @@ export interface IReadyGuildDTO { max_members: number | undefined; nsfw_level: number | undefined; hub_type?: unknown | null; // ???? + + home_header: null; // TODO + latest_onboarding_question_id: null; // TODO + safety_alerts_channel_id: null; // TODO + max_stage_video_channel_users: 50; // TODO + nsfw: boolean; + id: string; }; roles: Role[]; stage_instances: unknown[]; stickers: Sticker[]; threads: unknown[]; version: string; + guild_hashes: unknown; + unavailable: boolean; } export class ReadyGuildDTO implements IReadyGuildDTO { @@ -113,14 +154,30 @@ export class ReadyGuildDTO implements IReadyGuildDTO { max_members: number | undefined; nsfw_level: number | undefined; hub_type?: unknown | null; // ???? + + home_header: null; // TODO + latest_onboarding_question_id: null; // TODO + safety_alerts_channel_id: null; // TODO + max_stage_video_channel_users: 50; // TODO + nsfw: boolean; + id: string; }; roles: Role[]; stage_instances: unknown[]; stickers: Sticker[]; threads: unknown[]; version: string; + guild_hashes: unknown; + unavailable: boolean; + joined_at: Date; + + constructor(guild: GuildOrUnavailable) { + if (!guildIsAvailable(guild)) { + this.id = guild.id; + this.unavailable = true; + return; + } - constructor(guild: Guild) { this.application_command_counts = { 1: 5, 2: 2, @@ -164,12 +221,21 @@ export class ReadyGuildDTO implements IReadyGuildDTO { max_members: guild.max_members, nsfw_level: guild.nsfw_level, hub_type: null, + + home_header: null, + id: guild.id, + latest_onboarding_question_id: null, + max_stage_video_channel_users: 50, // TODO + nsfw: guild.nsfw, + safety_alerts_channel_id: null, }; this.roles = guild.roles; this.stage_instances = []; this.stickers = guild.stickers; this.threads = []; this.version = "1"; // ?????? + this.guild_hashes = {}; + this.joined_at = guild.joined_at; } toJSON() { diff --git a/src/util/entities/Channel.ts b/src/util/entities/Channel.ts index 9ce04848..19952bc2 100644 --- a/src/util/entities/Channel.ts +++ b/src/util/entities/Channel.ts @@ -16,6 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ +import { HTTPError } from "lambert-server"; import { Column, Entity, @@ -24,26 +25,25 @@ import { OneToMany, RelationId, } from "typeorm"; -import { BaseClass } from "./BaseClass"; -import { Guild } from "./Guild"; -import { PublicUserProjection, User } from "./User"; -import { HTTPError } from "lambert-server"; +import { DmChannelDTO } from "../dtos"; +import { ChannelCreateEvent, ChannelRecipientRemoveEvent } from "../interfaces"; import { + InvisibleCharacters, + Snowflake, containsAll, emitEvent, getPermission, - Snowflake, trimSpecial, - InvisibleCharacters, } from "../util"; -import { ChannelCreateEvent, ChannelRecipientRemoveEvent } from "../interfaces"; -import { Recipient } from "./Recipient"; +import { BaseClass } from "./BaseClass"; +import { Guild } from "./Guild"; +import { Invite } from "./Invite"; import { Message } from "./Message"; import { ReadState } from "./ReadState"; -import { Invite } from "./Invite"; +import { Recipient } from "./Recipient"; +import { PublicUserProjection, User } from "./User"; import { VoiceState } from "./VoiceState"; import { Webhook } from "./Webhook"; -import { DmChannelDTO } from "../dtos"; export enum ChannelType { GUILD_TEXT = 0, // a text channel within a guild @@ -302,8 +302,10 @@ export class Channel extends BaseClass { : channel.position) || 0, }; + const ret = Channel.create(channel); + await Promise.all([ - Channel.create(channel).save(), + ret.save(), !opts?.skipEventEmit ? emitEvent({ event: "CHANNEL_CREATE", @@ -313,7 +315,7 @@ export class Channel extends BaseClass { : Promise.resolve(), ]); - return channel; + return ret; } static async createDMChannel( @@ -468,6 +470,18 @@ export class Channel extends BaseClass { ]; return disallowedChannelTypes.indexOf(this.type) == -1; } + + toJSON() { + return { + ...this, + + // these fields are not returned depending on the type of channel + bitrate: this.bitrate || undefined, + user_limit: this.user_limit || undefined, + rate_limit_per_user: this.rate_limit_per_user || undefined, + owner_id: this.owner_id || undefined, + }; + } } export interface ChannelPermissionOverwrite { @@ -482,3 +496,33 @@ export enum ChannelPermissionOverwriteType { member = 1, group = 2, } + +export interface DMChannel extends Omit<Channel, "type" | "recipients"> { + type: ChannelType.DM | ChannelType.GROUP_DM; + recipients: Recipient[]; +} + +// TODO: probably more props +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); + } +} diff --git a/src/util/entities/ConnectedAccount.ts b/src/util/entities/ConnectedAccount.ts index 5dd21250..6e089de1 100644 --- a/src/util/entities/ConnectedAccount.ts +++ b/src/util/entities/ConnectedAccount.ts @@ -66,6 +66,7 @@ export class ConnectedAccount extends BaseClass { integrations?: string[] = []; @Column({ type: "simple-json", name: "metadata", nullable: true }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any metadata_?: any; @Column() diff --git a/src/util/entities/Guild.ts b/src/util/entities/Guild.ts index e8454986..e364ed98 100644 --- a/src/util/entities/Guild.ts +++ b/src/util/entities/Guild.ts @@ -24,7 +24,7 @@ import { OneToMany, RelationId, } from "typeorm"; -import { Config, handleFile, Snowflake } from ".."; +import { Config, GuildWelcomeScreen, handleFile, Snowflake } from ".."; import { Ban } from "./Ban"; import { BaseClass } from "./BaseClass"; import { Channel } from "./Channel"; @@ -77,7 +77,7 @@ export class Guild extends BaseClass { afk_channel?: Channel; @Column({ nullable: true }) - afk_timeout?: number = Config.get().defaults.guild.afkTimeout; + afk_timeout?: number; // * commented out -> use owner instead // application id of the guild creator if it is bot-created @@ -95,8 +95,7 @@ export class Guild extends BaseClass { banner?: string; @Column({ nullable: true }) - default_message_notifications?: number = - Config.get().defaults.guild.defaultMessageNotifications; + default_message_notifications?: number; @Column({ nullable: true }) description?: string; @@ -105,11 +104,10 @@ export class Guild extends BaseClass { discovery_splash?: string; @Column({ nullable: true }) - explicit_content_filter?: number = - Config.get().defaults.guild.explicitContentFilter; + explicit_content_filter?: number; @Column({ type: "simple-array" }) - features: string[] = Config.get().guild.defaultFeatures || []; //TODO use enum + features: string[] = []; //TODO use enum //TODO: https://discord.com/developers/docs/resources/guild#guild-object-guild-features @Column({ nullable: true }) @@ -122,14 +120,13 @@ export class Guild extends BaseClass { large?: boolean = false; @Column({ nullable: true }) - max_members?: number = Config.get().limits.guild.maxMembers; + max_members?: number; @Column({ nullable: true }) - max_presences?: number = Config.get().defaults.guild.maxPresences; + max_presences?: number; @Column({ nullable: true }) - max_video_channel_users?: number = - Config.get().defaults.guild.maxVideoChannelUsers; + max_video_channel_users?: number; @Column({ nullable: true }) member_count?: number; @@ -247,7 +244,7 @@ export class Guild extends BaseClass { rules_channel?: string; @Column({ nullable: true }) - region?: string = Config.get().regions.default; + region?: string; @Column({ nullable: true }) splash?: string; @@ -270,16 +267,7 @@ export class Guild extends BaseClass { verification_level?: number; @Column({ type: "simple-json" }) - welcome_screen: { - enabled: boolean; - description: string; - welcome_channels: { - description: string; - emoji_id?: string; - emoji_name?: string; - channel_id: string; - }[]; - }; + welcome_screen: GuildWelcomeScreen; @Column({ nullable: true }) @RelationId((guild: Guild) => guild.widget_channel) @@ -336,6 +324,18 @@ export class Guild extends BaseClass { description: "Fill in your description", welcome_channels: [], }, + + afk_timeout: Config.get().defaults.guild.afkTimeout, + default_message_notifications: + Config.get().defaults.guild.defaultMessageNotifications, + explicit_content_filter: + Config.get().defaults.guild.explicitContentFilter, + features: Config.get().guild.defaultFeatures, + max_members: Config.get().limits.guild.maxMembers, + max_presences: Config.get().defaults.guild.maxPresences, + max_video_channel_users: + Config.get().defaults.guild.maxVideoChannelUsers, + region: Config.get().regions.default, }).save(); // we have to create the role _after_ the guild because else we would get a "SQLITE_CONSTRAINT: FOREIGN KEY constraint failed" error @@ -353,6 +353,7 @@ export class Guild extends BaseClass { position: 0, icon: undefined, unicode_emoji: undefined, + flags: 0, // TODO? }).save(); if (!body.channels || !body.channels.length) @@ -389,4 +390,11 @@ export class Guild extends BaseClass { return guild; } + + toJSON() { + return { + ...this, + unavailable: this.unavailable == false ? undefined : true, + }; + } } diff --git a/src/util/entities/Member.ts b/src/util/entities/Member.ts index 8c208202..8be6eae1 100644 --- a/src/util/entities/Member.ts +++ b/src/util/entities/Member.ts @@ -344,11 +344,7 @@ export class Member extends BaseClassWithoutId { relations: ["user", "roles"], take: 10, }) - ).map((member) => ({ - ...member.toPublicMember(), - user: member.user.toPublicUser(), - roles: member.roles.map((x) => x.id), - })); + ).map((member) => member.toPublicMember()); if ( await Member.count({ @@ -455,6 +451,10 @@ export class Member extends BaseClassWithoutId { PublicMemberProjection.forEach((x) => { member[x] = this[x]; }); + + if (member.roles) member.roles = member.roles.map((x: Role) => x.id); + if (member.user) member.user = member.user.toPublicUser(); + return member as PublicMember; } } diff --git a/src/util/entities/Message.ts b/src/util/entities/Message.ts index 519c431e..3598d29f 100644 --- a/src/util/entities/Message.ts +++ b/src/util/entities/Message.ts @@ -193,7 +193,7 @@ export class Message extends BaseClass { }; @Column({ nullable: true }) - flags?: string; + flags?: number; @Column({ type: "simple-json", nullable: true }) message_reference?: { @@ -217,6 +217,29 @@ export class Message extends BaseClass { @Column({ type: "simple-json", nullable: true }) components?: MessageComponent[]; + + toJSON(): Message { + return { + ...this, + author_id: undefined, + member_id: undefined, + webhook_id: undefined, + application_id: undefined, + + nonce: this.nonce ?? undefined, + tts: this.tts ?? false, + guild: this.guild ?? undefined, + webhook: this.webhook ?? undefined, + interaction: this.interaction ?? undefined, + reactions: this.reactions ?? undefined, + sticker_items: this.sticker_items ?? undefined, + message_reference: this.message_reference ?? undefined, + author: this.author?.toPublicUser() ?? undefined, + activity: this.activity ?? undefined, + application: this.application ?? undefined, + components: this.components ?? undefined, + }; + } } export interface MessageComponent { diff --git a/src/util/entities/Role.ts b/src/util/entities/Role.ts index 85877c12..9a601f31 100644 --- a/src/util/entities/Role.ts +++ b/src/util/entities/Role.ts @@ -66,4 +66,7 @@ export class Role extends BaseClass { integration_id?: string; premium_subscriber?: boolean; }; + + @Column({ default: 0 }) + flags: number; } diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts index df9af328..3f1bda05 100644 --- a/src/util/entities/User.ts +++ b/src/util/entities/User.ts @@ -26,11 +26,11 @@ import { OneToOne, } from "typeorm"; import { - adjustEmail, Config, Email, FieldErrors, Snowflake, + adjustEmail, trimSpecial, } from ".."; import { BitField } from "../util/BitField"; @@ -86,8 +86,7 @@ export const PrivateUserProjection = [ // Private user data that should never get sent to the client export type PublicUser = Pick<User, PublicUserKeys>; - -export type UserPublic = Pick<User, PublicUserKeys>; +export type PrivateUser = Pick<User, PrivateUserKeys>; export interface UserPrivate extends Pick<User, PrivateUserKeys> { locale: string; @@ -110,8 +109,10 @@ export class User extends BaseClass { @Column({ nullable: true }) banner?: string; // hash of the user banner + // TODO: Separate `User` and `UserProfile` models + // puyo: changed from [number, number] because it breaks openapi @Column({ nullable: true, type: "simple-array" }) - theme_colors?: [number, number]; // TODO: Separate `User` and `UserProfile` models + theme_colors?: number[]; @Column({ nullable: true }) pronouns?: string; @@ -126,10 +127,10 @@ export class User extends BaseClass { mobile: boolean = false; // if the user has mobile app installed @Column() - premium: boolean = Config.get().defaults.user.premium ?? false; // if user bought individual premium + premium: boolean; // if user bought individual premium @Column() - premium_type: number = Config.get().defaults.user.premiumType ?? 0; // individual premium level + premium_type: number; // individual premium level @Column() bot: boolean = false; // if user is bot @@ -156,13 +157,13 @@ export class User extends BaseClass { totp_last_ticket?: string = ""; @Column() - created_at: Date = new Date(); // registration date + created_at: Date; // registration date @Column({ nullable: true }) premium_since: Date; // premium date @Column({ select: false }) - verified: boolean = Config.get().defaults.user.verified ?? true; // email is verified + verified: boolean; // email is verified @Column() disabled: boolean = false; // if the account is disabled @@ -174,7 +175,7 @@ export class User extends BaseClass { email?: string; // email of the user @Column() - flags: string = "0"; // UserFlags // TODO: generate + flags: number = 0; // UserFlags // TODO: generate @Column() public_flags: number = 0; @@ -280,6 +281,15 @@ export class User extends BaseClass { return user as PublicUser; } + toPrivateUser() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const user: any = {}; + PrivateUserProjection.forEach((x) => { + user[x] = this[x]; + }); + return user as UserPrivate; + } + static async getPublicUser(user_id: string, opts?: FindOneOptions<User>) { return await User.findOneOrFail({ where: { id: user_id }, @@ -381,11 +391,16 @@ export class User extends BaseClass { valid_tokens_since: new Date(), }, extended_settings: "{}", + settings: settings, + premium_since: Config.get().defaults.user.premium ? new Date() : undefined, - settings: settings, rights: Config.get().register.defaultRights, + premium: Config.get().defaults.user.premium ?? false, + premium_type: Config.get().defaults.user.premiumType ?? 0, + verified: Config.get().defaults.user.verified ?? true, + created_at: new Date(), }); user.validate(); diff --git a/src/util/interfaces/Activity.ts b/src/util/interfaces/Activity.ts index 7654ba90..0227f242 100644 --- a/src/util/interfaces/Activity.ts +++ b/src/util/interfaces/Activity.ts @@ -36,7 +36,7 @@ export interface Activity { }; party?: { id?: string; - size?: [number]; // used to show the party's current and maximum size // TODO: array length 2 + size?: number[]; // used to show the party's current and maximum size // TODO: array length 2 }; assets?: { large_image?: string; // the id for a large asset of the activity, usually a snowflake diff --git a/src/util/interfaces/Event.ts b/src/util/interfaces/Event.ts index 76a5f8d0..deb54428 100644 --- a/src/util/interfaces/Event.ts +++ b/src/util/interfaces/Event.ts @@ -28,7 +28,6 @@ import { Role, Emoji, PublicMember, - UserGuildSettings, Guild, Channel, PublicUser, @@ -40,6 +39,10 @@ import { UserSettings, IReadyGuildDTO, ReadState, + UserPrivate, + ReadyUserGuildSettingsEntries, + ReadyPrivateChannel, + GuildOrUnavailable, } from "@spacebar/util"; export interface Event { @@ -68,22 +71,10 @@ export interface PublicRelationship { export interface ReadyEventData { v: number; - user: PublicUser & { - mobile: boolean; - desktop: boolean; - email: string | undefined; - flags: string; - mfa_enabled: boolean; - nsfw_allowed: boolean; - phone: string | undefined; - premium: boolean; - premium_type: number; - verified: boolean; - bot: boolean; - }; - private_channels: Channel[]; // this will be empty for bots + user: UserPrivate; + private_channels: ReadyPrivateChannel[]; // this will be empty for bots session_id: string; // resuming - guilds: IReadyGuildDTO[]; + guilds: IReadyGuildDTO[] | GuildOrUnavailable[]; // depends on capability analytics_token?: string; connected_accounts?: ConnectedAccount[]; consents?: { @@ -115,7 +106,7 @@ export interface ReadyEventData { version: number; }; user_guild_settings?: { - entries: UserGuildSettings[]; + entries: ReadyUserGuildSettingsEntries[]; version: number; partial: boolean; }; @@ -127,6 +118,17 @@ export interface ReadyEventData { // probably all users who the user is in contact with users?: PublicUser[]; sessions: unknown[]; + api_code_version: number; + tutorial: number | null; + resume_gateway_url: string; + session_type: string; + auth_session_id_hash: string; + required_action?: + | "REQUIRE_VERIFIED_EMAIL" + | "REQUIRE_VERIFIED_PHONE" + | "REQUIRE_CAPTCHA" // TODO: allow these to be triggered + | "TOS_UPDATE_ACKNOWLEDGMENT" + | "AGREEMENTS"; } export interface ReadyEvent extends Event { @@ -581,6 +583,7 @@ export type EventData = export enum EVENTEnum { Ready = "READY", + ReadySupplemental = "READY_SUPPLEMENTAL", ChannelCreate = "CHANNEL_CREATE", ChannelUpdate = "CHANNEL_UPDATE", ChannelDelete = "CHANNEL_DELETE", diff --git a/src/util/interfaces/GuildWelcomeScreen.ts b/src/util/interfaces/GuildWelcomeScreen.ts new file mode 100644 index 00000000..38b6061b --- /dev/null +++ b/src/util/interfaces/GuildWelcomeScreen.ts @@ -0,0 +1,10 @@ +export interface GuildWelcomeScreen { + enabled: boolean; + description: string; + welcome_channels: { + description: string; + emoji_id?: string; + emoji_name?: string; + channel_id: string; + }[]; +} diff --git a/src/util/interfaces/index.ts b/src/util/interfaces/index.ts index c6a00458..6620ba32 100644 --- a/src/util/interfaces/index.ts +++ b/src/util/interfaces/index.ts @@ -19,6 +19,7 @@ export * from "./Activity"; export * from "./ConnectedAccount"; export * from "./Event"; +export * from "./GuildWelcomeScreen"; export * from "./Interaction"; export * from "./Presence"; export * from "./Status"; diff --git a/src/util/schemas/AckBulkSchema.ts b/src/util/schemas/AckBulkSchema.ts index cf6dc597..5604c2fc 100644 --- a/src/util/schemas/AckBulkSchema.ts +++ b/src/util/schemas/AckBulkSchema.ts @@ -17,11 +17,9 @@ */ export interface AckBulkSchema { - read_states: [ - { - channel_id: string; - message_id: string; - read_state_type: number; // WHat is this? - }, - ]; + read_states: { + channel_id: string; + message_id: string; + read_state_type: number; // WHat is this? + }[]; } diff --git a/src/util/schemas/ConnectedAccountSchema.ts b/src/util/schemas/ConnectedAccountSchema.ts index fe808a35..5fd05b71 100644 --- a/src/util/schemas/ConnectedAccountSchema.ts +++ b/src/util/schemas/ConnectedAccountSchema.ts @@ -30,7 +30,7 @@ export interface ConnectedAccountSchema { verified?: boolean; visibility?: number; integrations?: string[]; - metadata_?: any; + metadata_?: unknown; metadata_visibility?: number; two_way_link?: boolean; } diff --git a/src/util/schemas/ConnectionCallbackSchema.ts b/src/util/schemas/ConnectionCallbackSchema.ts index eb86c087..b66bfe20 100644 --- a/src/util/schemas/ConnectionCallbackSchema.ts +++ b/src/util/schemas/ConnectionCallbackSchema.ts @@ -21,5 +21,5 @@ export interface ConnectionCallbackSchema { state: string; insecure: boolean; friend_sync: boolean; - openid_params?: any; // TODO: types + openid_params?: unknown; // TODO: types } diff --git a/src/util/schemas/IdentifySchema.ts b/src/util/schemas/IdentifySchema.ts index fb48c2a4..0619dacc 100644 --- a/src/util/schemas/IdentifySchema.ts +++ b/src/util/schemas/IdentifySchema.ts @@ -66,6 +66,7 @@ export const IdentifySchema = { $private_channels_version: Number, $guild_versions: Object, $api_code_version: Number, + $initial_guild_id: String, }, $clientState: { $guildHashes: Object, @@ -75,6 +76,7 @@ export const IdentifySchema = { $userGuildSettingsVersion: undefined, $guildVersions: Object, $apiCodeVersion: Number, + $initialGuildId: String, }, $v: Number, $version: Number, @@ -109,7 +111,11 @@ export interface IdentifySchema { compress?: boolean; large_threshold?: number; largeThreshold?: number; - shard?: [bigint, bigint]; + /** + * @minItems 2 + * @maxItems 2 + */ + shard?: bigint[]; // puyo: changed from [bigint, bigint] because it breaks openapi guild_subscriptions?: boolean; capabilities?: number; client_state?: { @@ -122,6 +128,7 @@ export interface IdentifySchema { private_channels_version?: number; guild_versions?: unknown; api_code_version?: number; + initial_guild_id?: string; }; clientState?: { guildHashes?: unknown; @@ -131,6 +138,7 @@ export interface IdentifySchema { useruserGuildSettingsVersion?: number; guildVersions?: unknown; apiCodeVersion?: number; + initialGuildId?: string; }; v?: number; } diff --git a/src/util/schemas/LazyRequestSchema.ts b/src/util/schemas/LazyRequestSchema.ts index 63e67416..ee52d66c 100644 --- a/src/util/schemas/LazyRequestSchema.ts +++ b/src/util/schemas/LazyRequestSchema.ts @@ -18,7 +18,14 @@ export interface LazyRequestSchema { guild_id: string; - channels?: Record<string, [number, number][]>; + channels?: { + /** + * @items.type integer + * @minItems 2 + * @maxItems 2 + */ + [key: string]: number[][]; // puyo: changed from [number, number] because it breaks openapi + }; activities?: boolean; threads?: boolean; typing?: true; diff --git a/src/util/schemas/LoginResponse.ts b/src/util/schemas/LoginResponse.ts new file mode 100644 index 00000000..faf3f769 --- /dev/null +++ b/src/util/schemas/LoginResponse.ts @@ -0,0 +1,14 @@ +import { TokenResponse } from "./responses"; + +export interface MFAResponse { + ticket: string; + mfa: true; + sms: false; // TODO + token: null; +} + +export interface WebAuthnResponse extends MFAResponse { + webauthn: string; +} + +export type LoginResponse = TokenResponse | MFAResponse | WebAuthnResponse; diff --git a/src/util/schemas/MemberChangeProfileSchema.ts b/src/util/schemas/MemberChangeProfileSchema.ts index e955a0f1..d2d1481d 100644 --- a/src/util/schemas/MemberChangeProfileSchema.ts +++ b/src/util/schemas/MemberChangeProfileSchema.ts @@ -21,8 +21,7 @@ export interface MemberChangeProfileSchema { nick?: string; bio?: string; pronouns?: string; - - /* + /** * @items.type integer */ theme_colors?: [number, number]; diff --git a/src/util/schemas/MessageCreateSchema.ts b/src/util/schemas/MessageCreateSchema.ts index 45cd735e..7e130751 100644 --- a/src/util/schemas/MessageCreateSchema.ts +++ b/src/util/schemas/MessageCreateSchema.ts @@ -29,7 +29,7 @@ export interface MessageCreateSchema { nonce?: string; channel_id?: string; tts?: boolean; - flags?: string; + flags?: number; embeds?: Embed[]; embed?: Embed; // TODO: ^ embed is deprecated in favor of embeds (https://discord.com/developers/docs/resources/channel#message-object) diff --git a/src/util/schemas/RegisterSchema.ts b/src/util/schemas/RegisterSchema.ts index f6c99b18..7b7de9c7 100644 --- a/src/util/schemas/RegisterSchema.ts +++ b/src/util/schemas/RegisterSchema.ts @@ -42,4 +42,8 @@ export interface RegisterSchema { captcha_key?: string; promotional_email_opt_in?: boolean; + + // part of pomelo + unique_username_registration?: boolean; + global_name?: string; } diff --git a/src/util/schemas/UserGuildSettingsSchema.ts b/src/util/schemas/UserGuildSettingsSchema.ts index c295f767..82edae9c 100644 --- a/src/util/schemas/UserGuildSettingsSchema.ts +++ b/src/util/schemas/UserGuildSettingsSchema.ts @@ -16,12 +16,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { UserGuildSettings, ChannelOverride } from "@spacebar/util"; +import { ChannelOverride, UserGuildSettings } from "@spacebar/util"; // This sucks. I would use a DeepPartial, my own or typeorms, but they both generate inncorect schema export interface UserGuildSettingsSchema extends Partial<Omit<UserGuildSettings, "channel_overrides">> { channel_overrides?: { - [channel_id: string]: Partial<ChannelOverride>; + [channel_id: string]: ChannelOverride; }; } diff --git a/src/util/schemas/UserNoteUpdateSchema.ts b/src/util/schemas/UserNoteUpdateSchema.ts new file mode 100644 index 00000000..0a731279 --- /dev/null +++ b/src/util/schemas/UserNoteUpdateSchema.ts @@ -0,0 +1,3 @@ +export interface UserNoteUpdateSchema { + note: string; +} diff --git a/src/util/schemas/UserProfileModifySchema.ts b/src/util/schemas/UserProfileModifySchema.ts index d49fe326..3dea257a 100644 --- a/src/util/schemas/UserProfileModifySchema.ts +++ b/src/util/schemas/UserProfileModifySchema.ts @@ -21,8 +21,7 @@ export interface UserProfileModifySchema { accent_color?: number | null; banner?: string | null; pronouns?: string; - - /* + /** * @items.type integer */ theme_colors?: [number, number]; diff --git a/src/util/schemas/UserProfileResponse.ts b/src/util/schemas/UserProfileResponse.ts deleted file mode 100644 index 699d6a29..00000000 --- a/src/util/schemas/UserProfileResponse.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 Spacebar and Spacebar Contributors - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ - -import { PublicConnectedAccount, UserPublic } from ".."; - -export interface UserProfileResponse { - user: UserPublic; - connected_accounts: PublicConnectedAccount; - premium_guild_since?: Date; - premium_since?: Date; -} diff --git a/src/util/schemas/UserRelationsResponse.ts b/src/util/schemas/UserRelationsResponse.ts deleted file mode 100644 index 38507420..00000000 --- a/src/util/schemas/UserRelationsResponse.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 Spacebar and Spacebar Contributors - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ - -export interface UserRelationsResponse { - object: { - id?: string; - username?: string; - avatar?: string; - discriminator?: string; - public_flags?: number; - }; -} diff --git a/src/util/schemas/VoiceStateUpdateSchema.ts b/src/util/schemas/VoiceStateUpdateSchema.ts index a7d9f9d7..fda073e9 100644 --- a/src/util/schemas/VoiceStateUpdateSchema.ts +++ b/src/util/schemas/VoiceStateUpdateSchema.ts @@ -26,6 +26,7 @@ export interface VoiceStateUpdateSchema { preferred_region?: string; request_to_speak_timestamp?: Date; suppress?: boolean; + flags?: number; } export const VoiceStateUpdateSchema = { @@ -37,4 +38,5 @@ export const VoiceStateUpdateSchema = { $preferred_region: String, $request_to_speak_timestamp: Date, $suppress: Boolean, + $flags: Number, }; diff --git a/src/util/schemas/WebAuthnSchema.ts b/src/util/schemas/WebAuthnSchema.ts index 652cda34..3f5e0da7 100644 --- a/src/util/schemas/WebAuthnSchema.ts +++ b/src/util/schemas/WebAuthnSchema.ts @@ -28,9 +28,9 @@ export interface CreateWebAuthnCredentialSchema { ticket: string; } -export type WebAuthnPostSchema = Partial< - GenerateWebAuthnCredentialsSchema | CreateWebAuthnCredentialSchema ->; +export type WebAuthnPostSchema = + | GenerateWebAuthnCredentialsSchema + | CreateWebAuthnCredentialSchema; export interface WebAuthnTotpSchema { code: string; diff --git a/src/util/schemas/index.ts b/src/util/schemas/index.ts index 2d254752..44a504cd 100644 --- a/src/util/schemas/index.ts +++ b/src/util/schemas/index.ts @@ -69,6 +69,7 @@ export * from "./TotpSchema"; export * from "./UserDeleteSchema"; export * from "./UserGuildSettingsSchema"; export * from "./UserModifySchema"; +export * from "./UserNoteUpdateSchema"; export * from "./UserProfileModifySchema"; export * from "./UserSettingsSchema"; export * from "./Validator"; @@ -79,7 +80,4 @@ export * from "./VoiceVideoSchema"; export * from "./WebAuthnSchema"; export * from "./WebhookCreateSchema"; export * from "./WidgetModifySchema"; -export * from "./UserRelationsResponse"; -export * from "./GatewayResponse"; -export * from "./GatewayBotResponse"; -export * from "./UserProfileResponse"; +export * from "./responses"; diff --git a/src/util/schemas/responses/APIErrorOrCaptchaResponse.ts b/src/util/schemas/responses/APIErrorOrCaptchaResponse.ts new file mode 100644 index 00000000..c9a0e5be --- /dev/null +++ b/src/util/schemas/responses/APIErrorOrCaptchaResponse.ts @@ -0,0 +1,6 @@ +import { APIErrorResponse } from "./APIErrorResponse"; +import { CaptchaRequiredResponse } from "./CaptchaRequiredResponse"; + +export type APIErrorOrCaptchaResponse = + | CaptchaRequiredResponse + | APIErrorResponse; diff --git a/src/util/schemas/responses/APIErrorResponse.ts b/src/util/schemas/responses/APIErrorResponse.ts new file mode 100644 index 00000000..25bb9504 --- /dev/null +++ b/src/util/schemas/responses/APIErrorResponse.ts @@ -0,0 +1,12 @@ +export interface APIErrorResponse { + code: number; + message: string; + errors: { + [key: string]: { + _errors: { + message: string; + code: string; + }[]; + }; + }; +} diff --git a/src/util/schemas/responses/BackupCodesChallengeResponse.ts b/src/util/schemas/responses/BackupCodesChallengeResponse.ts new file mode 100644 index 00000000..5473ad1f --- /dev/null +++ b/src/util/schemas/responses/BackupCodesChallengeResponse.ts @@ -0,0 +1,4 @@ +export interface BackupCodesChallengeResponse { + nonce: string; + regenerate_nonce: string; +} diff --git a/src/util/schemas/responses/CaptchaRequiredResponse.ts b/src/util/schemas/responses/CaptchaRequiredResponse.ts new file mode 100644 index 00000000..9f7f02ff --- /dev/null +++ b/src/util/schemas/responses/CaptchaRequiredResponse.ts @@ -0,0 +1,5 @@ +export interface CaptchaRequiredResponse { + captcha_key: string; + captcha_sitekey: string; + captcha_service: string; +} diff --git a/src/util/schemas/responses/DiscoverableGuildsResponse.ts b/src/util/schemas/responses/DiscoverableGuildsResponse.ts new file mode 100644 index 00000000..2a9fb1bd --- /dev/null +++ b/src/util/schemas/responses/DiscoverableGuildsResponse.ts @@ -0,0 +1,8 @@ +import { Guild } from "../../entities"; + +export interface DiscoverableGuildsResponse { + total: number; + guilds: Guild[]; + offset: number; + limit: number; +} diff --git a/src/util/schemas/responses/GatewayBotResponse.ts b/src/util/schemas/responses/GatewayBotResponse.ts new file mode 100644 index 00000000..30f1f57f --- /dev/null +++ b/src/util/schemas/responses/GatewayBotResponse.ts @@ -0,0 +1,10 @@ +export interface GatewayBotResponse { + url: string; + shards: number; + session_start_limit: { + total: number; + remaining: number; + reset_after: number; + max_concurrency: number; + }; +} diff --git a/src/util/schemas/responses/GatewayResponse.ts b/src/util/schemas/responses/GatewayResponse.ts new file mode 100644 index 00000000..e909f7bd --- /dev/null +++ b/src/util/schemas/responses/GatewayResponse.ts @@ -0,0 +1,3 @@ +export interface GatewayResponse { + url: string; +} diff --git a/src/util/schemas/responses/GenerateRegistrationTokensResponse.ts b/src/util/schemas/responses/GenerateRegistrationTokensResponse.ts new file mode 100644 index 00000000..8816eabf --- /dev/null +++ b/src/util/schemas/responses/GenerateRegistrationTokensResponse.ts @@ -0,0 +1,3 @@ +export interface GenerateRegistrationTokensResponse { + tokens: string[]; +} diff --git a/src/util/schemas/responses/GuildBansResponse.ts b/src/util/schemas/responses/GuildBansResponse.ts new file mode 100644 index 00000000..876a4bc4 --- /dev/null +++ b/src/util/schemas/responses/GuildBansResponse.ts @@ -0,0 +1,10 @@ +export interface GuildBansResponse { + reason: string; + user: { + username: string; + discriminator: string; + id: string; + avatar: string | null; + public_flags: number; + }; +} diff --git a/src/util/schemas/responses/GuildCreateResponse.ts b/src/util/schemas/responses/GuildCreateResponse.ts new file mode 100644 index 00000000..8185cb86 --- /dev/null +++ b/src/util/schemas/responses/GuildCreateResponse.ts @@ -0,0 +1,3 @@ +export interface GuildCreateResponse { + id: string; +} diff --git a/src/util/schemas/responses/GuildDiscoveryRequirements.ts b/src/util/schemas/responses/GuildDiscoveryRequirements.ts new file mode 100644 index 00000000..731976f7 --- /dev/null +++ b/src/util/schemas/responses/GuildDiscoveryRequirements.ts @@ -0,0 +1,23 @@ +export interface GuildDiscoveryRequirementsResponse { + uild_id: string; + safe_environment: boolean; + healthy: boolean; + health_score_pending: boolean; + size: boolean; + nsfw_properties: unknown; + protected: boolean; + sufficient: boolean; + sufficient_without_grace_period: boolean; + valid_rules_channel: boolean; + retention_healthy: boolean; + engagement_healthy: boolean; + age: boolean; + minimum_age: number; + health_score: { + avg_nonnew_participators: number; + avg_nonnew_communicators: number; + num_intentful_joiners: number; + perc_ret_w1_intentful: number; + }; + minimum_size: number; +} diff --git a/src/util/schemas/responses/GuildMessagesSearchResponse.ts b/src/util/schemas/responses/GuildMessagesSearchResponse.ts new file mode 100644 index 00000000..0b6248b7 --- /dev/null +++ b/src/util/schemas/responses/GuildMessagesSearchResponse.ts @@ -0,0 +1,32 @@ +import { + Attachment, + Embed, + MessageType, + PublicUser, + Role, +} from "../../entities"; + +export interface GuildMessagesSearchMessage { + id: string; + type: MessageType; + content?: string; + channel_id: string; + author: PublicUser; + attachments: Attachment[]; + embeds: Embed[]; + mentions: PublicUser[]; + mention_roles: Role[]; + pinned: boolean; + mention_everyone?: boolean; + tts: boolean; + timestamp: string; + edited_timestamp: string | null; + flags: number; + components: unknown[]; + hit: true; +} + +export interface GuildMessagesSearchResponse { + messages: GuildMessagesSearchMessage[]; + total_results: number; +} diff --git a/src/util/schemas/responses/GuildPruneResponse.ts b/src/util/schemas/responses/GuildPruneResponse.ts new file mode 100644 index 00000000..fb1abb89 --- /dev/null +++ b/src/util/schemas/responses/GuildPruneResponse.ts @@ -0,0 +1,7 @@ +export interface GuildPruneResponse { + pruned: number; +} + +export interface GuildPurgeResponse { + purged: number; +} diff --git a/src/util/schemas/responses/GuildRecommendationsResponse.ts b/src/util/schemas/responses/GuildRecommendationsResponse.ts new file mode 100644 index 00000000..211670a6 --- /dev/null +++ b/src/util/schemas/responses/GuildRecommendationsResponse.ts @@ -0,0 +1,6 @@ +import { Guild } from "../../entities"; + +export interface GuildRecommendationsResponse { + recommended_guilds: Guild[]; + load_id: string; +} diff --git a/src/util/schemas/responses/GuildVanityUrl.ts b/src/util/schemas/responses/GuildVanityUrl.ts new file mode 100644 index 00000000..ff37bf4e --- /dev/null +++ b/src/util/schemas/responses/GuildVanityUrl.ts @@ -0,0 +1,17 @@ +export interface GuildVanityUrl { + code: string; + uses: number; +} + +export interface GuildVanityUrlNoInvite { + code: null; +} + +export type GuildVanityUrlResponse = + | GuildVanityUrl + | GuildVanityUrl[] + | GuildVanityUrlNoInvite; + +export interface GuildVanityUrlCreateResponse { + code: string; +} diff --git a/src/util/schemas/responses/GuildVoiceRegionsResponse.ts b/src/util/schemas/responses/GuildVoiceRegionsResponse.ts new file mode 100644 index 00000000..8190d5fd --- /dev/null +++ b/src/util/schemas/responses/GuildVoiceRegionsResponse.ts @@ -0,0 +1,7 @@ +export interface GuildVoiceRegion { + id: string; + name: string; + custom: boolean; + deprecated: boolean; + optimal: boolean; +} diff --git a/src/util/schemas/responses/GuildWidgetJsonResponse.ts b/src/util/schemas/responses/GuildWidgetJsonResponse.ts new file mode 100644 index 00000000..ef85dd08 --- /dev/null +++ b/src/util/schemas/responses/GuildWidgetJsonResponse.ts @@ -0,0 +1,21 @@ +import { ClientStatus } from "../../interfaces"; + +export interface GuildWidgetJsonResponse { + id: string; + name: string; + instant_invite: string; + channels: { + id: string; + name: string; + position: number; + }[]; + members: { + id: string; + username: string; + discriminator: string; + avatar: string | null; + status: ClientStatus; + avatar_url: string; + }[]; + presence_count: number; +} diff --git a/src/util/schemas/responses/GuildWidgetSettingsResponse.ts b/src/util/schemas/responses/GuildWidgetSettingsResponse.ts new file mode 100644 index 00000000..3c6b45ce --- /dev/null +++ b/src/util/schemas/responses/GuildWidgetSettingsResponse.ts @@ -0,0 +1,6 @@ +import { Snowflake } from "../../util"; + +export interface GuildWidgetSettingsResponse { + enabled: boolean; + channel_id: Snowflake | null; +} diff --git a/src/util/schemas/responses/InstanceDomainsResponse.ts b/src/util/schemas/responses/InstanceDomainsResponse.ts new file mode 100644 index 00000000..60367492 --- /dev/null +++ b/src/util/schemas/responses/InstanceDomainsResponse.ts @@ -0,0 +1,6 @@ +export interface InstanceDomainsResponse { + cdn: string; + gateway: string; + defaultApiVersion: string; + apiEndpoint: string; +} diff --git a/src/util/schemas/responses/InstancePingResponse.ts b/src/util/schemas/responses/InstancePingResponse.ts new file mode 100644 index 00000000..5f1a9488 --- /dev/null +++ b/src/util/schemas/responses/InstancePingResponse.ts @@ -0,0 +1,13 @@ +export interface InstancePingResponse { + ping: "pong!"; + instance: { + id: string; + name: string; + description: string | null; + image: string | null; + correspondenceEmail: string | null; + correspondenceUserID: string | null; + frontPage: string | null; + tosPage: string | null; + }; +} diff --git a/src/util/schemas/responses/InstanceStatsResponse.ts b/src/util/schemas/responses/InstanceStatsResponse.ts new file mode 100644 index 00000000..d24fd434 --- /dev/null +++ b/src/util/schemas/responses/InstanceStatsResponse.ts @@ -0,0 +1,8 @@ +export interface InstanceStatsResponse { + counts: { + user: number; + guild: number; + message: number; + members: number; + }; +} diff --git a/src/util/schemas/responses/LocationMetadataResponse.ts b/src/util/schemas/responses/LocationMetadataResponse.ts new file mode 100644 index 00000000..55337557 --- /dev/null +++ b/src/util/schemas/responses/LocationMetadataResponse.ts @@ -0,0 +1,5 @@ +export interface LocationMetadataResponse { + consent_required: boolean; + country_code: string; + promotional_email_opt_in: { required: true; pre_checked: false }; +} diff --git a/src/util/schemas/responses/MemberJoinGuildResponse.ts b/src/util/schemas/responses/MemberJoinGuildResponse.ts new file mode 100644 index 00000000..d7b39d10 --- /dev/null +++ b/src/util/schemas/responses/MemberJoinGuildResponse.ts @@ -0,0 +1,8 @@ +import { Emoji, Guild, Role, Sticker } from "../../entities"; + +export interface MemberJoinGuildResponse { + guild: Guild; + emojis: Emoji[]; + roles: Role[]; + stickers: Sticker[]; +} diff --git a/src/util/schemas/responses/OAuthAuthorizeResponse.ts b/src/util/schemas/responses/OAuthAuthorizeResponse.ts new file mode 100644 index 00000000..60d6d2e2 --- /dev/null +++ b/src/util/schemas/responses/OAuthAuthorizeResponse.ts @@ -0,0 +1,3 @@ +export interface OAuthAuthorizeResponse { + location: string; +} diff --git a/src/util/schemas/responses/Tenor.ts b/src/util/schemas/responses/Tenor.ts new file mode 100644 index 00000000..9dddf9d0 --- /dev/null +++ b/src/util/schemas/responses/Tenor.ts @@ -0,0 +1,72 @@ +export enum TenorMediaTypes { + gif, + mediumgif, + tinygif, + nanogif, + mp4, + loopedmp4, + tinymp4, + nanomp4, + webm, + tinywebm, + nanowebm, +} + +export type TenorMedia = { + preview: string; + url: string; + dims: number[]; + size: number; +}; + +export type TenorGif = { + created: number; + hasaudio: boolean; + id: string; + media: { [type in keyof typeof TenorMediaTypes]: TenorMedia }[]; + tags: string[]; + title: string; + itemurl: string; + hascaption: boolean; + url: string; +}; + +export type TenorCategory = { + searchterm: string; + path: string; + image: string; + name: string; +}; + +export type TenorCategoriesResults = { + tags: TenorCategory[]; +}; + +export type TenorTrendingResults = { + next: string; + results: TenorGif[]; + locale: string; +}; + +export type TenorSearchResults = { + next: string; + results: TenorGif[]; +}; + +export interface TenorGifResponse { + id: string; + title: string; + url: string; + src: string; + gif_src: string; + width: number; + height: number; + preview: string; +} + +export interface TenorTrendingResponse { + categories: TenorCategoriesResults; + gifs: TenorGifResponse[]; +} + +export type TenorGifsResponse = TenorGifResponse[]; diff --git a/src/util/schemas/responses/TokenResponse.ts b/src/util/schemas/responses/TokenResponse.ts new file mode 100644 index 00000000..7e93055a --- /dev/null +++ b/src/util/schemas/responses/TokenResponse.ts @@ -0,0 +1,15 @@ +import { BackupCode, UserSettings } from "../../entities"; + +export interface TokenResponse { + token: string; + settings: UserSettings; +} + +export interface TokenOnlyResponse { + token: string; +} + +export interface TokenWithBackupCodesResponse { + token: string; + backup_codes: BackupCode[]; +} diff --git a/src/util/schemas/responses/TypedResponses.ts b/src/util/schemas/responses/TypedResponses.ts new file mode 100644 index 00000000..4349b93c --- /dev/null +++ b/src/util/schemas/responses/TypedResponses.ts @@ -0,0 +1,88 @@ +import { GeneralConfiguration, LimitsConfiguration } from "../../config"; +import { DmChannelDTO } from "../../dtos"; +import { + Application, + BackupCode, + Categories, + Channel, + Emoji, + Guild, + Invite, + Member, + Message, + PrivateUser, + PublicMember, + PublicUser, + Role, + Sticker, + StickerPack, + Template, + Webhook, +} from "../../entities"; +import { GuildVoiceRegion } from "./GuildVoiceRegionsResponse"; + +// removes internal properties from the guild class +export type APIGuild = Omit< + Guild, + | "afk_channel" + | "template" + | "owner" + | "public_updates_channel" + | "rules_channel" + | "system_channel" + | "widget_channel" +>; + +export type APIPublicUser = PublicUser; +export type APIPrivateUser = PrivateUser; + +export type APIGuildArray = APIGuild[]; + +export type APIDMChannelArray = DmChannelDTO[]; + +export type APIBackupCodeArray = BackupCode[]; + +export interface UserUpdateResponse extends APIPrivateUser { + newToken?: string; +} + +export type ApplicationDetectableResponse = unknown[]; + +export type ApplicationEntitlementsResponse = unknown[]; + +export type ApplicationSkusResponse = unknown[]; + +export type APIApplicationArray = Application[]; + +export type APIInviteArray = Invite[]; + +export type APIMessageArray = Message[]; + +export type APIWebhookArray = Webhook[]; + +export type APIDiscoveryCategoryArray = Categories[]; + +export type APIGeneralConfiguration = GeneralConfiguration; + +export type APIChannelArray = Channel[]; + +export type APIEmojiArray = Emoji[]; + +export type APIMemberArray = Member[]; +export type APIPublicMember = PublicMember; + +export interface APIGuildWithJoinedAt extends Guild { + joined_at: string; +} + +export type APIRoleArray = Role[]; + +export type APIStickerArray = Sticker[]; + +export type APITemplateArray = Template[]; + +export type APIGuildVoiceRegion = GuildVoiceRegion[]; + +export type APILimitsConfiguration = LimitsConfiguration; + +export type APIStickerPackArray = StickerPack[]; diff --git a/src/util/schemas/responses/UpdatesResponse.ts b/src/util/schemas/responses/UpdatesResponse.ts new file mode 100644 index 00000000..6b8566f4 --- /dev/null +++ b/src/util/schemas/responses/UpdatesResponse.ts @@ -0,0 +1,6 @@ +export interface UpdatesResponse { + name: string; + pub_date: string; + url: string; + notes: string | null; +} diff --git a/src/util/schemas/responses/UserNoteResponse.ts b/src/util/schemas/responses/UserNoteResponse.ts new file mode 100644 index 00000000..b142811e --- /dev/null +++ b/src/util/schemas/responses/UserNoteResponse.ts @@ -0,0 +1,5 @@ +export interface UserNoteResponse { + note: string; + note_user_id: string; + user_id: string; +} diff --git a/src/util/schemas/responses/UserProfileResponse.ts b/src/util/schemas/responses/UserProfileResponse.ts new file mode 100644 index 00000000..eba7cbcc --- /dev/null +++ b/src/util/schemas/responses/UserProfileResponse.ts @@ -0,0 +1,37 @@ +import { + Member, + PublicConnectedAccount, + PublicMember, + PublicUser, + User, +} from "@spacebar/util"; + +export type MutualGuild = { + id: string; + nick?: string; +}; + +export type PublicMemberProfile = Pick< + Member, + "banner" | "bio" | "guild_id" +> & { + accent_color: null; // TODO +}; + +export type UserProfile = Pick< + User, + "bio" | "accent_color" | "banner" | "pronouns" | "theme_colors" +>; + +export interface UserProfileResponse { + user: PublicUser; + connected_accounts: PublicConnectedAccount; + premium_guild_since?: Date; + premium_since?: Date; + mutual_guilds: MutualGuild[]; + premium_type: number; + profile_themes_experiment_bucket: number; + user_profile: UserProfile; + guild_member?: PublicMember; + guild_member_profile?: PublicMemberProfile; +} diff --git a/src/util/schemas/responses/UserRelationsResponse.ts b/src/util/schemas/responses/UserRelationsResponse.ts new file mode 100644 index 00000000..e784cafb --- /dev/null +++ b/src/util/schemas/responses/UserRelationsResponse.ts @@ -0,0 +1,7 @@ +import { User } from "@spacebar/util"; + +export type UserRelationsResponse = (Pick<User, "id"> & + Pick<User, "username"> & + Pick<User, "discriminator"> & + Pick<User, "avatar"> & + Pick<User, "public_flags">)[]; diff --git a/src/util/schemas/responses/UserRelationshipsResponse.ts b/src/util/schemas/responses/UserRelationshipsResponse.ts new file mode 100644 index 00000000..dff2f118 --- /dev/null +++ b/src/util/schemas/responses/UserRelationshipsResponse.ts @@ -0,0 +1,8 @@ +import { PublicUser, RelationshipType } from "../../entities"; + +export interface UserRelationshipsResponse { + id: string; + type: RelationshipType; + nickname: null; + user: PublicUser; +} diff --git a/src/util/schemas/responses/WebAuthnCreateResponse.ts b/src/util/schemas/responses/WebAuthnCreateResponse.ts new file mode 100644 index 00000000..9aa9e206 --- /dev/null +++ b/src/util/schemas/responses/WebAuthnCreateResponse.ts @@ -0,0 +1,4 @@ +export interface WebAuthnCreateResponse { + name: string; + id: string; +} diff --git a/src/util/schemas/responses/WebhookCreateResponse.ts b/src/util/schemas/responses/WebhookCreateResponse.ts new file mode 100644 index 00000000..ae142632 --- /dev/null +++ b/src/util/schemas/responses/WebhookCreateResponse.ts @@ -0,0 +1,6 @@ +import { User, Webhook } from "../../entities"; + +export interface WebhookCreateResponse { + user: User; + hook: Webhook; +} diff --git a/src/util/schemas/responses/index.ts b/src/util/schemas/responses/index.ts new file mode 100644 index 00000000..d8b7fd57 --- /dev/null +++ b/src/util/schemas/responses/index.ts @@ -0,0 +1,34 @@ +export * from "./APIErrorOrCaptchaResponse"; +export * from "./APIErrorResponse"; +export * from "./BackupCodesChallengeResponse"; +export * from "./CaptchaRequiredResponse"; +export * from "./DiscoverableGuildsResponse"; +export * from "./GatewayBotResponse"; +export * from "./GatewayResponse"; +export * from "./GenerateRegistrationTokensResponse"; +export * from "./GuildBansResponse"; +export * from "./GuildCreateResponse"; +export * from "./GuildDiscoveryRequirements"; +export * from "./GuildMessagesSearchResponse"; +export * from "./GuildPruneResponse"; +export * from "./GuildRecommendationsResponse"; +export * from "./GuildVanityUrl"; +export * from "./GuildVoiceRegionsResponse"; +export * from "./GuildWidgetJsonResponse"; +export * from "./GuildWidgetSettingsResponse"; +export * from "./InstanceDomainsResponse"; +export * from "./InstancePingResponse"; +export * from "./InstanceStatsResponse"; +export * from "./LocationMetadataResponse"; +export * from "./MemberJoinGuildResponse"; +export * from "./OAuthAuthorizeResponse"; +export * from "./Tenor"; +export * from "./TokenResponse"; +export * from "./TypedResponses"; +export * from "./UpdatesResponse"; +export * from "./UserNoteResponse"; +export * from "./UserProfileResponse"; +export * from "./UserRelationshipsResponse"; +export * from "./UserRelationsResponse"; +export * from "./WebAuthnCreateResponse"; +export * from "./WebhookCreateResponse"; diff --git a/src/util/util/Application.ts b/src/util/util/Application.ts new file mode 100644 index 00000000..23019a7f --- /dev/null +++ b/src/util/util/Application.ts @@ -0,0 +1,24 @@ +import { Request } from "express"; +import { Application, User } from "../entities"; + +export async function createAppBotUser(app: Application, req: Request) { + 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(); + + // flags is NaN here? + app.assign({ bot: user, flags: app.flags || 0 }); + + await app.save(); + + return user; +} diff --git a/src/util/util/AutoUpdate.ts b/src/util/util/AutoUpdate.ts index 1f90a41e..2af5cf1c 100644 --- a/src/util/util/AutoUpdate.ts +++ b/src/util/util/AutoUpdate.ts @@ -18,7 +18,7 @@ import "missing-native-js-functions"; import fetch from "node-fetch"; -import ProxyAgent from "proxy-agent"; +import { ProxyAgent } from "proxy-agent"; import readline from "readline"; import fs from "fs/promises"; import path from "path"; diff --git a/src/util/util/Gifs.ts b/src/util/util/Gifs.ts new file mode 100644 index 00000000..a5a5e64c --- /dev/null +++ b/src/util/util/Gifs.ts @@ -0,0 +1,25 @@ +import { HTTPError } from "lambert-server"; +import { Config } from "./Config"; +import { TenorGif } from ".."; + +export function parseGifResult(result: TenorGif) { + 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; +} diff --git a/src/util/util/JSON.ts b/src/util/util/JSON.ts index 1c39b66e..c7dcf47e 100644 --- a/src/util/util/JSON.ts +++ b/src/util/util/JSON.ts @@ -27,6 +27,16 @@ const JSONReplacer = function ( return (this[key] as Date).toISOString().replace("Z", "+00:00"); } + // erlpack encoding doesn't call json.stringify, + // so our toJSON functions don't get called. + // manually call it here + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + if (this?.[key]?.toJSON) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + this[key] = this[key].toJSON(); + return value; }; diff --git a/src/util/util/Token.ts b/src/util/util/Token.ts index 90310176..97bdec74 100644 --- a/src/util/util/Token.ts +++ b/src/util/util/Token.ts @@ -19,94 +19,69 @@ import jwt, { VerifyOptions } from "jsonwebtoken"; import { Config } from "./Config"; import { User } from "../entities"; +// TODO: dont use deprecated APIs lol +import { + FindOptionsRelationByString, + FindOptionsSelectByString, +} from "typeorm"; export const JWTOptions: VerifyOptions = { algorithms: ["HS256"] }; export type UserTokenData = { user: User; - decoded: { id: string; iat: number }; + decoded: { id: string; iat: number; email?: string }; }; -async function checkEmailToken( - decoded: jwt.JwtPayload, -): Promise<UserTokenData> { - // eslint-disable-next-line no-async-promise-executor - return new Promise(async (res, rej) => { - if (!decoded.iat) return rej("Invalid Token"); // will never happen, just for typings. - - const user = await User.findOne({ - where: { - email: decoded.email, - }, - select: [ - "email", - "id", - "verified", - "deleted", - "disabled", - "username", - "data", - ], - }); - - if (!user) return rej("Invalid Token"); - - if (new Date().getTime() > decoded.iat * 1000 + 86400 * 1000) - return rej("Invalid Token"); - - // Using as here because we assert `id` and `iat` are in decoded. - // TS just doesn't want to assume its there, though. - return res({ decoded, user } as UserTokenData); - }); -} - -export function checkToken( +export const checkToken = ( token: string, - jwtSecret: string, - isEmailVerification = false, -): Promise<UserTokenData> { - return new Promise((res, rej) => { - token = token.replace("Bot ", ""); - token = token.replace("Bearer ", ""); - /** - in spacebar, even with instances that have bot distinction; we won't enforce "Bot" prefix, - as we don't really have separate pathways for bots - **/ - - jwt.verify(token, jwtSecret, JWTOptions, async (err, decoded) => { - if (err || !decoded) return rej("Invalid Token"); - if ( - typeof decoded == "string" || - !("id" in decoded) || - !decoded.iat - ) - return rej("Invalid Token"); // will never happen, just for typings. - - if (isEmailVerification) return res(checkEmailToken(decoded)); - - const user = await User.findOne({ - where: { id: decoded.id }, - select: ["data", "bot", "disabled", "deleted", "rights"], - }); - - if (!user) return rej("Invalid Token"); - - // we need to round it to seconds as it saved as seconds in jwt iat and valid_tokens_since is stored in milliseconds - if ( - decoded.iat * 1000 < - new Date(user.data.valid_tokens_since).setSeconds(0, 0) - ) - return rej("Invalid Token"); - - if (user.disabled) return rej("User disabled"); - if (user.deleted) return rej("User not found"); - - // Using as here because we assert `id` and `iat` are in decoded. - // TS just doesn't want to assume its there, though. - return res({ decoded, user } as UserTokenData); - }); + opts?: { + select?: FindOptionsSelectByString<User>; + relations?: FindOptionsRelationByString; + }, +): Promise<UserTokenData> => + new Promise((resolve, reject) => { + token = token.replace("Bot ", ""); // there is no bot distinction in sb + token = token.replace("Bearer ", ""); // allow bearer tokens + + jwt.verify( + token, + Config.get().security.jwtSecret, + JWTOptions, + async (err, out) => { + const decoded = out as UserTokenData["decoded"]; + if (err || !decoded) return reject("Invalid Token"); + + const user = await User.findOne({ + where: decoded.email + ? { email: decoded.email } + : { id: decoded.id }, + select: [ + ...(opts?.select || []), + "bot", + "disabled", + "deleted", + "rights", + "data", + ], + relations: opts?.relations, + }); + + if (!user) return reject("User not found"); + + // we need to round it to seconds as it saved as seconds in jwt iat and valid_tokens_since is stored in milliseconds + if ( + decoded.iat * 1000 < + new Date(user.data.valid_tokens_since).setSeconds(0, 0) + ) + return reject("Invalid Token"); + + if (user.disabled) return reject("User disabled"); + if (user.deleted) return reject("User not found"); + + return resolve({ decoded, user }); + }, + ); }); -} export async function generateToken(id: string, email?: string) { const iat = Math.floor(Date.now() / 1000); diff --git a/src/util/util/index.ts b/src/util/util/index.ts index 838239b7..10e09b5c 100644 --- a/src/util/util/index.ts +++ b/src/util/util/index.ts @@ -41,3 +41,5 @@ export * from "./String"; export * from "./Token"; export * from "./TraverseDirectory"; export * from "./WebAuthn"; +export * from "./Gifs"; +export * from "./Application"; diff --git a/src/webrtc/opcodes/SelectProtocol.ts b/src/webrtc/opcodes/SelectProtocol.ts index 6618d83b..0a06e722 100644 --- a/src/webrtc/opcodes/SelectProtocol.ts +++ b/src/webrtc/opcodes/SelectProtocol.ts @@ -18,7 +18,7 @@ import { Payload, Send, WebSocket } from "@spacebar/gateway"; import { SelectProtocolSchema, validateSchema } from "@spacebar/util"; -import { endpoint, PublicIP, VoiceOPCodes } from "@spacebar/webrtc"; +import { PublicIP, VoiceOPCodes, endpoint } from "@spacebar/webrtc"; import SemanticSDP, { MediaInfo, SDPInfo } from "semantic-sdp"; export async function onSelectProtocol(this: WebSocket, payload: Payload) { @@ -56,7 +56,7 @@ export async function onSelectProtocol(this: WebSocket, payload: Payload) { `a=candidate:1 1 ${candidate.getTransport()} ${candidate.getFoundation()} ${candidate.getAddress()} ${candidate.getPort()} typ host`; await Send(this, { - op: VoiceOPCodes.SELECT_PROTOCOL_ACK, + op: VoiceOPCodes.SESSION_DESCRIPTION, d: { video_codec: "H264", sdp: answer, diff --git a/src/webrtc/util/Constants.ts b/src/webrtc/util/Constants.ts index bd89c974..dba1c511 100644 --- a/src/webrtc/util/Constants.ts +++ b/src/webrtc/util/Constants.ts @@ -29,7 +29,7 @@ export enum VoiceOPCodes { SELECT_PROTOCOL = 1, READY = 2, HEARTBEAT = 3, - SELECT_PROTOCOL_ACK = 4, + SESSION_DESCRIPTION = 4, SPEAKING = 5, HEARTBEAT_ACK = 6, RESUME = 7, |