diff options
author | Madeline <46743919+MaddyUnderStars@users.noreply.github.com> | 2023-07-28 08:24:15 +1000 |
---|---|---|
committer | Madeline <46743919+MaddyUnderStars@users.noreply.github.com> | 2023-07-28 08:24:15 +1000 |
commit | 8a3989c29776ad7eba8077bf7cc9c56e28b9b8c3 (patch) | |
tree | e71d68052e6bf5ddfad64c643a8fb2d04c2e183c /src | |
parent | Merge branch 'master' into feat/refactorIdentify (diff) | |
parent | Merge pull request #1075 from SpecificProtagonist/get_messages_around (diff) | |
download | server-8a3989c29776ad7eba8077bf7cc9c56e28b9b8c3.tar.xz |
Merge branch 'master' into feat/refactorIdentify
Diffstat (limited to 'src')
172 files changed, 4671 insertions, 2175 deletions
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..f97045a6 100644 --- a/src/api/routes/auth/reset.ts +++ b/src/api/routes/auth/reset.ts @@ -31,9 +31,20 @@ 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; diff --git a/src/api/routes/auth/verify/index.ts b/src/api/routes/auth/verify/index.ts index c1afcde9..a98c17fa 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; 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..a15b462d 100644 --- a/src/api/routes/channels/#channel_id/messages/index.ts +++ b/src/api/routes/channels/#channel_id/messages/index.ts @@ -16,165 +16,172 @@ 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 - } +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 (after) { + if (BigInt(after) > BigInt(Snowflake.generate())) + return res.status(422); + query.where.id = MoreThan(after); + messages = await Message.find(query); + } else if (before) { + if (BigInt(before) < BigInt(req.params.channel_id)) + return res.status(422); + query.where.id = LessThan(before); + messages = await Message.find(query); + } else if (around) { + query.take = Math.floor(limit / 2); + query.where.id = LessThan(around); + const messages_before = await Message.find(query); + query.where.id = MoreThan(around); + const messages_after = await Message.find(query); + messages = messages_before.concat(messages_after); + } else { + throw new HTTPError("after, around or before must be present", 422); + } - const messages = await Message.find(query); - const endpoint = Config.get().cdn.endpointPublic; + const endpoint = Config.get().cdn.endpointPublic; - return res.json( - messages.map((x: Partial<Message>) => { - (x.reactions || []).forEach((y: Partial<Reaction>) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - if ((y.user_ids || []).includes(req.user_id)) y.me = true; - delete y.user_ids; - }); - if (!x.author) - x.author = User.create({ - id: "4", - discriminator: "0000", - username: "Spacebar Ghost", - public_flags: 0, + return res.json( + messages.map((x: Partial<Message>) => { + (x.reactions || []).forEach((y: Partial<Reaction>) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + if ((y.user_ids || []).includes(req.user_id)) y.me = true; + delete y.user_ids; + }); + if (!x.author) + x.author = User.create({ + id: "4", + discriminator: "0000", + username: "Spacebar Ghost", + public_flags: 0, + }); + x.attachments?.forEach((y: Attachment) => { + // dynamically set attachment proxy_url in case the endpoint changed + const uri = y.proxy_url.startsWith("http") + ? y.proxy_url + : `https://example.org${y.proxy_url}`; + y.proxy_url = `${endpoint == null ? "" : endpoint}${ + new URL(uri).pathname + }`; }); - x.attachments?.forEach((y: Attachment) => { - // dynamically set attachment proxy_url in case the endpoint changed - const uri = y.proxy_url.startsWith("http") - ? y.proxy_url - : `https://example.org${y.proxy_url}`; - y.proxy_url = `${endpoint == null ? "" : endpoint}${ - new URL(uri).pathname - }`; - }); - /** + /** Some clients ( discord.js ) only check if a property exists within the response, which causes errors when, say, the `application` property is `null`. **/ - // for (var curr in x) { - // if (x[curr] === null) - // delete x[curr]; - // } + // for (var curr in x) { + // if (x[curr] === null) + // delete x[curr]; + // } - return x; - }), - ); -}); + return x; + }), + ); + }, +); // TODO: config max upload size const messageUpload = multer({ @@ -205,9 +212,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; @@ -366,3 +383,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/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..f125a463 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 { route } from "@spacebar/api"; +import { TenorMediaTypes, getGifApiKey, parseGifResult } from "@spacebar/util"; +import { Request, Response, Router } from "express"; import fetch from "node-fetch"; import ProxyAgent from "proxy-agent"; -import { route } from "@spacebar/api"; -import { getGifApiKey, parseGifResult } from "./trending"; 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..d6fa89ac 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 { route } from "@spacebar/api"; +import { TenorMediaTypes, getGifApiKey, parseGifResult } from "@spacebar/util"; +import { Request, Response, Router } from "express"; import fetch from "node-fetch"; import ProxyAgent from "proxy-agent"; -import { route } from "@spacebar/api"; -import { getGifApiKey, parseGifResult } from "./trending"; 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..e3d6e974 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 { route } from "@spacebar/api"; +import { + TenorCategoriesResults, + TenorTrendingResults, + getGifApiKey, + parseGifResult, +} from "@spacebar/util"; +import { Request, Response, Router } 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"; 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..afe60614 100644 --- a/src/api/routes/guilds/#guild_id/index.ts +++ b/src/api/routes/guilds/#guild_id/index.ts @@ -16,46 +16,79 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; import { DiscordApiErrors, - emitEvent, - getPermission, - getRights, Guild, GuildUpdateEvent, - handleFile, - Member, GuildUpdateSchema, + Member, 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; 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..2a739683 100644 --- a/src/api/routes/guilds/#guild_id/welcome-screen.ts +++ b/src/api/routes/guilds/#guild_id/welcome-screen.ts @@ -16,27 +16,49 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ +import { route } from "@spacebar/api"; +import { Guild, GuildUpdateWelcomeScreenSchema, Member } from "@spacebar/util"; import { Request, Response, Router } from "express"; -import { Guild, Member, GuildUpdateWelcomeScreenSchema } from "@spacebar/util"; import { HTTPError } from "lambert-server"; -import { route } from "@spacebar/api"; 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; 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 6680e375..6de4d583 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/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/bundle/Server.ts b/src/bundle/Server.ts index 6ba74be6..d281120d 100644 --- a/src/bundle/Server.ts +++ b/src/bundle/Server.ts @@ -51,7 +51,9 @@ async function main() { await Config.init(); await Sentry.init(app); - server.listen(port); + await new Promise((resolve) => + server.listen({ port }, () => resolve(undefined)), + ); await Promise.all([api.start(), cdn.start(), gateway.start()]); Sentry.errorHandler(app); 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/LazyRequest.ts b/src/gateway/opcodes/LazyRequest.ts index 64e50d92..77e1a25a 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 @@ -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/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/entities/Channel.ts b/src/util/entities/Channel.ts index 72490028..19a7a41a 100644 --- a/src/util/entities/Channel.ts +++ b/src/util/entities/Channel.ts @@ -486,6 +486,29 @@ export enum ChannelPermissionOverwriteType { export interface DMChannel extends Omit<Channel, "type" | "recipients"> { type: ChannelType.DM | ChannelType.GROUP_DM; recipients: Recipient[]; +} - // TODO: probably more props +// 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/Guild.ts b/src/util/entities/Guild.ts index 64dc2420..4c2949a3 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 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/User.ts b/src/util/entities/User.ts index b30a7c58..68d7b5e8 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 @@ -390,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/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/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/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/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/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, |