From e8c5c988bb9d0c41de46b2029eefe4faa7e0e068 Mon Sep 17 00:00:00 2001 From: Rory& Date: Sat, 11 Oct 2025 03:58:37 +0200 Subject: Update path variables --- .../applications/#application_id/bot/index.ts | 141 ++++++++++++++++ .../applications/#application_id/entitlements.ts | 40 +++++ .../routes/applications/#application_id/index.ts | 157 ++++++++++++++++++ .../routes/applications/#application_id/skus.ts | 38 +++++ src/api/routes/applications/#id/bot/index.ts | 141 ---------------- src/api/routes/applications/#id/entitlements.ts | 40 ----- src/api/routes/applications/#id/index.ts | 157 ------------------ src/api/routes/applications/#id/skus.ts | 38 ----- src/api/routes/guilds/templates/index.ts | 12 +- src/api/routes/invites/index.ts | 24 +-- .../store/published-listings/applications.ts | 2 +- src/api/routes/store/published-listings/skus.ts | 2 +- src/api/routes/users/#id/delete.ts | 66 -------- src/api/routes/users/#id/index.ts | 41 ----- src/api/routes/users/#id/messages.ts | 56 ------- src/api/routes/users/#id/profile.ts | 184 --------------------- src/api/routes/users/#id/relationships.ts | 70 -------- src/api/routes/users/#user_id/delete.ts | 66 ++++++++ src/api/routes/users/#user_id/index.ts | 41 +++++ src/api/routes/users/#user_id/messages.ts | 56 +++++++ src/api/routes/users/#user_id/profile.ts | 184 +++++++++++++++++++++ src/api/routes/users/#user_id/relationships.ts | 70 ++++++++ 22 files changed, 813 insertions(+), 813 deletions(-) create mode 100644 src/api/routes/applications/#application_id/bot/index.ts create mode 100644 src/api/routes/applications/#application_id/entitlements.ts create mode 100644 src/api/routes/applications/#application_id/index.ts create mode 100644 src/api/routes/applications/#application_id/skus.ts delete mode 100644 src/api/routes/applications/#id/bot/index.ts delete mode 100644 src/api/routes/applications/#id/entitlements.ts delete mode 100644 src/api/routes/applications/#id/index.ts delete mode 100644 src/api/routes/applications/#id/skus.ts delete mode 100644 src/api/routes/users/#id/delete.ts delete mode 100644 src/api/routes/users/#id/index.ts delete mode 100644 src/api/routes/users/#id/messages.ts delete mode 100644 src/api/routes/users/#id/profile.ts delete mode 100644 src/api/routes/users/#id/relationships.ts create mode 100644 src/api/routes/users/#user_id/delete.ts create mode 100644 src/api/routes/users/#user_id/index.ts create mode 100644 src/api/routes/users/#user_id/messages.ts create mode 100644 src/api/routes/users/#user_id/profile.ts create mode 100644 src/api/routes/users/#user_id/relationships.ts (limited to 'src') diff --git a/src/api/routes/applications/#application_id/bot/index.ts b/src/api/routes/applications/#application_id/bot/index.ts new file mode 100644 index 00000000..0928af37 --- /dev/null +++ b/src/api/routes/applications/#application_id/bot/index.ts @@ -0,0 +1,141 @@ +/* + 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 . +*/ + +import { route } from "@spacebar/api"; +import { + Application, + BotModifySchema, + 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({ mergeParams: true }); + +router.post( + "/", + route({ + responses: { + 204: { + body: "TokenOnlyResponse", + }, + 400: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const app = await Application.findOneOrFail({ + where: { id: req.params.application_id }, + relations: ["owner"], + }); + + if (app.owner.id != req.user_id) + throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION; + + const user = await createAppBotUser(app, req); + + res.send({ + token: await generateToken(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.application_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.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() }; + + await bot.save(); + + const token = await generateToken(bot.id); + + res.json({ token }).status(200); + }, +); + +router.patch( + "/", + 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; + + const app = await Application.findOneOrFail({ + where: { id: req.params.application_id }, + relations: ["bot", "owner"], + }); + + if (!app.bot) throw DiscordApiErrors.BOT_ONLY_ENDPOINT; + + if (app.owner.id != req.user_id) + throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION; + + if (body.avatar) + body.avatar = await handleFile( + `/avatars/${app.id}`, + body.avatar as string, + ); + + app.bot.assign(body); + + app.bot.save(); + + await app.save(); + res.json(app).status(200); + }, +); + +export default router; diff --git a/src/api/routes/applications/#application_id/entitlements.ts b/src/api/routes/applications/#application_id/entitlements.ts new file mode 100644 index 00000000..63a7e7b9 --- /dev/null +++ b/src/api/routes/applications/#application_id/entitlements.ts @@ -0,0 +1,40 @@ +/* + 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 . +*/ + +import { route } from "@spacebar/api"; +import { Request, Response, Router } from "express"; + +const router = Router({ mergeParams: true }); + +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/#application_id/index.ts b/src/api/routes/applications/#application_id/index.ts new file mode 100644 index 00000000..d6ba26fe --- /dev/null +++ b/src/api/routes/applications/#application_id/index.ts @@ -0,0 +1,157 @@ +/* + 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 . +*/ + +import { route } from "@spacebar/api"; +import { + Application, + ApplicationModifySchema, + DiscordApiErrors, + Guild, + handleFile, +} from "@spacebar/util"; +import { Request, Response, Router } from "express"; +import { HTTPError } from "lambert-server"; +import { verifyToken } from "node-2fa"; + +const router: Router = Router({ mergeParams: true }); + +router.get( + "/", + route({ + responses: { + 200: { + body: "Application", + }, + 400: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const app = await Application.findOneOrFail({ + where: { id: req.params.application_id }, + relations: ["owner", "bot"], + }); + if (app.owner.id != req.user_id) + throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION; + + return res.json(app); + }, +); + +router.patch( + "/", + route({ + requestBody: "ApplicationModifySchema", + responses: { + 200: { + body: "Application", + }, + 400: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const body = req.body as ApplicationModifySchema; + + const app = await Application.findOneOrFail({ + where: { id: req.params.application_id }, + relations: ["owner", "bot"], + }); + + 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 (body.icon) { + body.icon = await handleFile( + `/app-icons/${app.id}`, + body.icon as string, + ); + } + if (body.cover_image) { + body.cover_image = await handleFile( + `/app-icons/${app.id}`, + body.cover_image as string, + ); + } + + if (body.guild_id) { + const guild = await Guild.findOneOrFail({ + where: { id: body.guild_id }, + select: ["owner_id"], + }); + if (guild.owner_id != req.user_id) + throw new HTTPError( + "You must be the owner of the guild to link it to an application", + 400, + ); + } + + if (app.bot) { + app.bot.assign({ bio: body.description }); + await app.bot.save(); + } + + app.assign(body); + + await app.save(); + + return res.json(app); + }, +); + +router.post( + "/delete", + route({ + responses: { + 200: {}, + 400: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const app = await Application.findOneOrFail({ + where: { id: req.params.application_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); + + await Application.delete({ id: app.id }); + + res.send().status(200); + }, +); + +export default router; diff --git a/src/api/routes/applications/#application_id/skus.ts b/src/api/routes/applications/#application_id/skus.ts new file mode 100644 index 00000000..0877f4fb --- /dev/null +++ b/src/api/routes/applications/#application_id/skus.ts @@ -0,0 +1,38 @@ +/* + 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 . +*/ + +import { route } from "@spacebar/api"; +import { Request, Response, Router } from "express"; + +const router: Router = Router({ mergeParams: true }); + +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/#id/bot/index.ts b/src/api/routes/applications/#id/bot/index.ts deleted file mode 100644 index 88c348df..00000000 --- a/src/api/routes/applications/#id/bot/index.ts +++ /dev/null @@ -1,141 +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 . -*/ - -import { route } from "@spacebar/api"; -import { - Application, - BotModifySchema, - 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({ mergeParams: true }); - -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"], - }); - - if (app.owner.id != req.user_id) - throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION; - - const user = await createAppBotUser(app, req); - - res.send({ - token: await generateToken(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.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() }; - - await bot.save(); - - const token = await generateToken(bot.id); - - res.json({ token }).status(200); - }, -); - -router.patch( - "/", - 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; - - const app = await Application.findOneOrFail({ - where: { id: req.params.id }, - relations: ["bot", "owner"], - }); - - if (!app.bot) throw DiscordApiErrors.BOT_ONLY_ENDPOINT; - - if (app.owner.id != req.user_id) - throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION; - - if (body.avatar) - body.avatar = await handleFile( - `/avatars/${app.id}`, - body.avatar as string, - ); - - app.bot.assign(body); - - app.bot.save(); - - await app.save(); - res.json(app).status(200); - }, -); - -export default router; diff --git a/src/api/routes/applications/#id/entitlements.ts b/src/api/routes/applications/#id/entitlements.ts deleted file mode 100644 index 63a7e7b9..00000000 --- a/src/api/routes/applications/#id/entitlements.ts +++ /dev/null @@ -1,40 +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 . -*/ - -import { route } from "@spacebar/api"; -import { Request, Response, Router } from "express"; - -const router = Router({ mergeParams: true }); - -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 deleted file mode 100644 index efb9ecdd..00000000 --- a/src/api/routes/applications/#id/index.ts +++ /dev/null @@ -1,157 +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 . -*/ - -import { route } from "@spacebar/api"; -import { - Application, - ApplicationModifySchema, - DiscordApiErrors, - Guild, - handleFile, -} from "@spacebar/util"; -import { Request, Response, Router } from "express"; -import { HTTPError } from "lambert-server"; -import { verifyToken } from "node-2fa"; - -const router: Router = Router({ mergeParams: true }); - -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); - }, -); - -router.patch( - "/", - route({ - requestBody: "ApplicationModifySchema", - responses: { - 200: { - body: "Application", - }, - 400: { - body: "APIErrorResponse", - }, - }, - }), - async (req: Request, res: Response) => { - const body = req.body as ApplicationModifySchema; - - 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; - - 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 (body.icon) { - body.icon = await handleFile( - `/app-icons/${app.id}`, - body.icon as string, - ); - } - if (body.cover_image) { - body.cover_image = await handleFile( - `/app-icons/${app.id}`, - body.cover_image as string, - ); - } - - if (body.guild_id) { - const guild = await Guild.findOneOrFail({ - where: { id: body.guild_id }, - select: ["owner_id"], - }); - if (guild.owner_id != req.user_id) - throw new HTTPError( - "You must be the owner of the guild to link it to an application", - 400, - ); - } - - if (app.bot) { - app.bot.assign({ bio: body.description }); - await app.bot.save(); - } - - app.assign(body); - - await app.save(); - - return res.json(app); - }, -); - -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); - - await Application.delete({ id: app.id }); - - 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 deleted file mode 100644 index 0877f4fb..00000000 --- a/src/api/routes/applications/#id/skus.ts +++ /dev/null @@ -1,38 +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 . -*/ - -import { route } from "@spacebar/api"; -import { Request, Response, Router } from "express"; - -const router: Router = Router({ mergeParams: true }); - -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/guilds/templates/index.ts b/src/api/routes/guilds/templates/index.ts index e4a959f8..002347d3 100644 --- a/src/api/routes/guilds/templates/index.ts +++ b/src/api/routes/guilds/templates/index.ts @@ -25,7 +25,7 @@ import { HTTPError } from "lambert-server"; const router: Router = Router({ mergeParams: true }); router.get( - "/:code", + "/:template_code", route({ responses: { 200: { @@ -40,16 +40,16 @@ router.get( }, }), async (req: Request, res: Response) => { - const { code } = req.params; + const { template_code } = req.params; - const template = await getTemplate(code); + const template = await getTemplate(template_code); res.json(template); }, ); -router.post("/:code", route({ requestBody: "GuildTemplateCreateSchema" }), async (req: Request, res: Response) => { - const { code } = req.params; +router.post("/:template_code", route({ requestBody: "GuildTemplateCreateSchema" }), async (req: Request, res: Response) => { + const { template_code } = req.params; const body = req.body as GuildTemplateCreateSchema; const { maxGuilds } = Config.get().limits.user; @@ -57,7 +57,7 @@ router.post("/:code", route({ requestBody: "GuildTemplateCreateSchema" }), async const guild_count = await Member.count({ where: { id: req.user_id } }); if (guild_count >= maxGuilds) throw DiscordApiErrors.MAXIMUM_GUILDS.withParams(maxGuilds); - const template = await getTemplate(code) as Template; + const template = await getTemplate(template_code) as Template; const guild = await Guild.createGuild({ ...template.serialized_source_guild, diff --git a/src/api/routes/invites/index.ts b/src/api/routes/invites/index.ts index 22191881..b807fe21 100644 --- a/src/api/routes/invites/index.ts +++ b/src/api/routes/invites/index.ts @@ -33,7 +33,7 @@ import { HTTPError } from "lambert-server"; const router: Router = Router({ mergeParams: true }); router.get( - "/:code", + "/:invite_code", route({ responses: { "200": { @@ -45,10 +45,10 @@ router.get( }, }), async (req: Request, res: Response) => { - const { code } = req.params; + const { invite_code } = req.params; const invite = await Invite.findOneOrFail({ - where: { code }, + where: { code: invite_code }, relations: PublicInviteRelation, }); @@ -57,7 +57,7 @@ router.get( ); router.post( - "/:code", + "/:invite_code", route({ right: "USE_MASS_INVITES", responses: { @@ -78,9 +78,9 @@ router.post( async (req: Request, res: Response) => { if (req.user_bot) throw DiscordApiErrors.BOT_PROHIBITED_ENDPOINT; - const { code } = req.params; + const { invite_code } = req.params; const { guild_id } = await Invite.findOneOrFail({ - where: { code: code }, + where: { code: invite_code }, }); const { features } = await Guild.findOneOrFail({ where: { id: guild_id }, @@ -100,7 +100,7 @@ router.post( if (features.includes("INVITES_DISABLED")) throw new HTTPError("Sorry, this guild has joins closed.", 403); - const invite = await Invite.joinGuild(req.user_id, code); + const invite = await Invite.joinGuild(req.user_id, invite_code); res.json(invite); }, @@ -108,7 +108,7 @@ router.post( // * cant use permission of route() function because path doesn't have guild_id/channel_id router.delete( - "/:code", + "/:invite_code", route({ responses: { "200": { @@ -123,8 +123,8 @@ router.delete( }, }), async (req: Request, res: Response) => { - const { code } = req.params; - const invite = await Invite.findOneOrFail({ where: { code } }); + const { invite_code } = req.params; + const invite = await Invite.findOneOrFail({ where: { code: invite_code } }); const { guild_id, channel_id } = invite; const permission = await getPermission( @@ -143,14 +143,14 @@ router.delete( ); await Promise.all([ - Invite.delete({ code }), + Invite.delete({ code: invite_code }), emitEvent({ event: "INVITE_DELETE", guild_id: guild_id, data: { channel_id: channel_id, guild_id: guild_id, - code: code, + code: invite_code, }, } as InviteDeleteEvent), ]); diff --git a/src/api/routes/store/published-listings/applications.ts b/src/api/routes/store/published-listings/applications.ts index 1881d657..b5cabb93 100644 --- a/src/api/routes/store/published-listings/applications.ts +++ b/src/api/routes/store/published-listings/applications.ts @@ -21,7 +21,7 @@ import { route } from "@spacebar/api"; const router: Router = Router({ mergeParams: true }); -router.get("/:id", route({}), async (req: Request, res: Response) => { +router.get("/:application_id", route({}), async (req: Request, res: Response) => { //TODO // const id = req.params.id; res.json({ diff --git a/src/api/routes/store/published-listings/skus.ts b/src/api/routes/store/published-listings/skus.ts index 1881d657..a463b4e4 100644 --- a/src/api/routes/store/published-listings/skus.ts +++ b/src/api/routes/store/published-listings/skus.ts @@ -21,7 +21,7 @@ import { route } from "@spacebar/api"; const router: Router = Router({ mergeParams: true }); -router.get("/:id", route({}), async (req: Request, res: Response) => { +router.get("/:sku_id", route({}), async (req: Request, res: Response) => { //TODO // const id = req.params.id; res.json({ diff --git a/src/api/routes/users/#id/delete.ts b/src/api/routes/users/#id/delete.ts deleted file mode 100644 index 4c7747c9..00000000 --- a/src/api/routes/users/#id/delete.ts +++ /dev/null @@ -1,66 +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 . -*/ - -import { route } from "@spacebar/api"; -import { - emitEvent, - Member, - PrivateUserProjection, - User, - UserDeleteEvent, -} from "@spacebar/util"; -import { Request, Response, Router } from "express"; - -const router = Router({ mergeParams: true }); - -router.post( - "/", - 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 }, - select: [...PrivateUserProjection, "data"], - }); - await Promise.all([ - Member.delete({ id: req.params.id }), - User.delete({ id: req.params.id }), - ]); - - // TODO: respect intents as USER_DELETE has potential to cause privacy issues - await emitEvent({ - event: "USER_DELETE", - user_id: req.user_id, - data: { user_id: req.params.id }, - } as UserDeleteEvent); - - res.sendStatus(204); - }, -); - -export default router; diff --git a/src/api/routes/users/#id/index.ts b/src/api/routes/users/#id/index.ts deleted file mode 100644 index 882fa2b7..00000000 --- a/src/api/routes/users/#id/index.ts +++ /dev/null @@ -1,41 +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 . -*/ - -import { route } from "@spacebar/api"; -import { User } from "@spacebar/util"; -import { Request, Response, Router } from "express"; - -const router: Router = Router({ mergeParams: true }); - -router.get( - "/", - route({ - responses: { - 200: { - body: "APIPublicUser", - }, - }, - }), - async (req: Request, res: Response) => { - const { id } = req.params; - - res.json(await User.getPublicUser(id)); - }, -); - -export default router; diff --git a/src/api/routes/users/#id/messages.ts b/src/api/routes/users/#id/messages.ts deleted file mode 100644 index 4d724575..00000000 --- a/src/api/routes/users/#id/messages.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2025 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 . -*/ - -import { route } from "@spacebar/api"; -import { Config, DmMessagesResponseSchema, Message, User } from "@spacebar/util"; -import { Request, Response, Router } from "express"; -const router = Router({ mergeParams: true }); - -router.get( - "/", - route({ - responses: { - 200: { - body: "DmMessagesResponseSchema", - }, - 400: { - body: "APIErrorResponse", - }, - }, - }), - async (req: Request, res: Response) => { - const user = await User.findOneOrFail({ where: { id: req.params.id } }); - const channel = await user.getDmChannelWith(req.user_id); - - const messages = ( - await Message.find({ - where: { channel_id: channel?.id }, - order: { timestamp: "DESC" }, - take: Math.clamp(req.query.limit ? Number(req.query.limit) : 50, 1, Config.get().limits.message.maxPreloadCount), - }) - ).filter((x) => x !== null) as Message[]; - - const filteredMessages = messages.map((message) => message.toPartialMessage()) as DmMessagesResponseSchema; - - return res.status(200).send(filteredMessages); - }, -); - -// TODO: POST to send a message to the user - -export default router; diff --git a/src/api/routes/users/#id/profile.ts b/src/api/routes/users/#id/profile.ts deleted file mode 100644 index e2eaa036..00000000 --- a/src/api/routes/users/#id/profile.ts +++ /dev/null @@ -1,184 +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 . -*/ - -import { route } from "@spacebar/api"; -import { - Badge, - Config, - emitEvent, - FieldErrors, - handleFile, - Member, - PrivateUserProjection, - PublicUser, - PublicUserProjection, - Relationship, - RelationshipType, - User, - UserProfileModifySchema, - UserUpdateEvent, -} from "@spacebar/util"; -import { Request, Response, Router } from "express"; -import { In } from "typeorm"; - -const router: Router = Router({ mergeParams: true }); - -router.get("/", route({ responses: { 200: { body: "UserProfileResponse" } } }), async (req: Request, res: Response) => { - if (req.params.id === "@me") req.params.id = req.user_id; - - const { guild_id, with_mutual_guilds, with_mutual_friends, with_mutual_friends_count } = req.query; - - const user = await User.getPublicUser(req.params.id, { - relations: ["connected_accounts"], - }); - - const mutual_guilds: object[] = []; - let premium_guild_since; - - if (with_mutual_guilds == "true") { - const requested_member = await Member.find({ - where: { id: req.params.id }, - }); - const self_member = await Member.find({ - where: { id: req.user_id }, - }); - - for (const rmem of requested_member) { - if (rmem.premium_since) { - if (premium_guild_since) { - if (premium_guild_since > rmem.premium_since) { - premium_guild_since = rmem.premium_since; - } - } else { - premium_guild_since = rmem.premium_since; - } - } - for (const smem of self_member) { - if (smem.guild_id === rmem.guild_id) { - mutual_guilds.push({ - id: rmem.guild_id, - nick: rmem.nick, - }); - } - } - } - } - - const guild_member = - guild_id && typeof guild_id == "string" - ? await Member.findOneOrFail({ - where: { id: req.params.id, guild_id: guild_id }, - relations: ["roles"], - }) - : undefined; - - // TODO: make proper DTO's in util? - - const userProfile = { - bio: req.user_bot ? null : user.bio, - accent_color: user.accent_color, - banner: user.banner, - pronouns: user.pronouns, - theme_colors: user.theme_colors?.map((t) => Number(t)), // these are strings for some reason, they should be numbers - }; - - const guildMemberProfile = { - accent_color: null, - banner: guild_member?.banner || null, - bio: guild_member?.bio || "", - guild_id, - }; - - const badges = await Badge.find(); - - let mutual_friends: PublicUser[] = []; - let mutual_friends_count = 0; - - if (with_mutual_friends == "true" || with_mutual_friends_count == "true") { - const relationshipsSelf = await Relationship.find({ where: { from_id: req.user_id, type: RelationshipType.friends } }); - const relationshipsUser = await Relationship.find({ where: { from_id: req.params.id, type: RelationshipType.friends } }); - const relationshipsIntersection = relationshipsSelf.filter((r1) => relationshipsUser.some((r2) => r2.to_id === r1.to_id)); - if (with_mutual_friends_count) mutual_friends_count = relationshipsIntersection.length; - if (with_mutual_friends) { - const users = await User.find({ where: { id: In(relationshipsIntersection.map((r) => r.to_id)) }, select: PublicUserProjection }); - mutual_friends = users.map((u) => u.toPublicUser()); - } - } - - res.json({ - connected_accounts: user.connected_accounts.filter((x) => x.visibility != 0), - premium_guild_since: premium_guild_since, // TODO - premium_since: user.premium_since, // TODO - mutual_guilds: with_mutual_guilds ? mutual_guilds : undefined, // TODO {id: "", nick: null} when ?with_mutual_guilds=true - mutual_friends: with_mutual_friends ? mutual_friends : undefined, - mutual_friends_count: with_mutual_friends_count ? mutual_friends_count : undefined, - 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_member?.toPublicMember(), - guild_member_profile: guild_id && guildMemberProfile, - badges: badges.filter((x) => user.badge_ids?.includes(x.id)), - }); -}); - -router.patch("/", route({ requestBody: "UserProfileModifySchema" }), async (req: Request, res: Response) => { - const body = req.body as UserProfileModifySchema; - - if (body.banner) body.banner = await handleFile(`/banners/${req.user_id}`, body.banner as string); - const user = await User.findOneOrFail({ - where: { id: req.user_id }, - select: [...PrivateUserProjection, "data"], - }); - - if (body.bio) { - const { maxBio } = Config.get().limits.user; - if (body.bio.length > maxBio) { - throw FieldErrors({ - bio: { - code: "BIO_INVALID", - message: `Bio must be less than ${maxBio} in length`, - }, - }); - } - } - - user.assign(body); - await user.save(); - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - delete user.data; - - // TODO: send update member list event in gateway - await emitEvent({ - event: "USER_UPDATE", - user_id: req.user_id, - data: user, - } as UserUpdateEvent); - - res.json({ - accent_color: user.accent_color, - bio: user.bio, - banner: user.banner, - theme_colors: user.theme_colors, - pronouns: user.pronouns, - }); -}); - -export default router; diff --git a/src/api/routes/users/#id/relationships.ts b/src/api/routes/users/#id/relationships.ts deleted file mode 100644 index 4c92c789..00000000 --- a/src/api/routes/users/#id/relationships.ts +++ /dev/null @@ -1,70 +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 . -*/ - -import { route } from "@spacebar/api"; -import { User, UserRelationsResponse } from "@spacebar/util"; -import { Request, Response, Router } from "express"; - -const router: Router = Router({ mergeParams: true }); - -router.get( - "/", - route({ - responses: { - 200: { body: "UserRelationsResponse" }, - 404: { - body: "APIErrorResponse", - }, - }, - }), - async (req: Request, res: Response) => { - const mutual_relations: UserRelationsResponse = []; - - const requested_relations = await User.findOneOrFail({ - where: { id: req.params.id }, - relations: ["relationships"], - }); - const self_relations = await User.findOneOrFail({ - where: { id: req.user_id }, - relations: ["relationships"], - }); - - for (const rmem of requested_relations.relationships) { - for (const smem of self_relations.relationships) - if ( - rmem.to_id === smem.to_id && - rmem.type === 1 && - rmem.to_id !== req.user_id - ) { - const relation_user = await User.getPublicUser(rmem.to_id); - - mutual_relations.push({ - id: relation_user.id, - username: relation_user.username, - avatar: relation_user.avatar, - discriminator: relation_user.discriminator, - public_flags: relation_user.public_flags, - }); - } - } - - res.json(mutual_relations); - }, -); - -export default router; diff --git a/src/api/routes/users/#user_id/delete.ts b/src/api/routes/users/#user_id/delete.ts new file mode 100644 index 00000000..53af01eb --- /dev/null +++ b/src/api/routes/users/#user_id/delete.ts @@ -0,0 +1,66 @@ +/* + 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 . +*/ + +import { route } from "@spacebar/api"; +import { + emitEvent, + Member, + PrivateUserProjection, + User, + UserDeleteEvent, +} from "@spacebar/util"; +import { Request, Response, Router } from "express"; + +const router = Router({ mergeParams: true }); + +router.post( + "/", + route({ + right: "MANAGE_USERS", + responses: { + 204: {}, + 403: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + await User.findOneOrFail({ + where: { id: req.params.user_id }, + select: [...PrivateUserProjection, "data"], + }); + await Promise.all([ + Member.delete({ id: req.params.user_id }), + User.delete({ id: req.params.user_id }), + ]); + + // TODO: respect intents as USER_DELETE has potential to cause privacy issues + await emitEvent({ + event: "USER_DELETE", + user_id: req.user_id, + data: { user_id: req.params.user_id }, + } as UserDeleteEvent); + + res.sendStatus(204); + }, +); + +export default router; diff --git a/src/api/routes/users/#user_id/index.ts b/src/api/routes/users/#user_id/index.ts new file mode 100644 index 00000000..417f961d --- /dev/null +++ b/src/api/routes/users/#user_id/index.ts @@ -0,0 +1,41 @@ +/* + 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 . +*/ + +import { route } from "@spacebar/api"; +import { User } from "@spacebar/util"; +import { Request, Response, Router } from "express"; + +const router: Router = Router({ mergeParams: true }); + +router.get( + "/", + route({ + responses: { + 200: { + body: "APIPublicUser", + }, + }, + }), + async (req: Request, res: Response) => { + const { user_id } = req.params; + + res.json(await User.getPublicUser(user_id)); + }, +); + +export default router; diff --git a/src/api/routes/users/#user_id/messages.ts b/src/api/routes/users/#user_id/messages.ts new file mode 100644 index 00000000..04708cfd --- /dev/null +++ b/src/api/routes/users/#user_id/messages.ts @@ -0,0 +1,56 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2025 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 . +*/ + +import { route } from "@spacebar/api"; +import { Config, DmMessagesResponseSchema, Message, User } from "@spacebar/util"; +import { Request, Response, Router } from "express"; +const router = Router({ mergeParams: true }); + +router.get( + "/", + route({ + responses: { + 200: { + body: "DmMessagesResponseSchema", + }, + 400: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const user = await User.findOneOrFail({ where: { id: req.params.user_id } }); + const channel = await user.getDmChannelWith(req.user_id); + + const messages = ( + await Message.find({ + where: { channel_id: channel?.id }, + order: { timestamp: "DESC" }, + take: Math.clamp(req.query.limit ? Number(req.query.limit) : 50, 1, Config.get().limits.message.maxPreloadCount), + }) + ).filter((x) => x !== null) as Message[]; + + const filteredMessages = messages.map((message) => message.toPartialMessage()) as DmMessagesResponseSchema; + + return res.status(200).send(filteredMessages); + }, +); + +// TODO: POST to send a message to the user + +export default router; diff --git a/src/api/routes/users/#user_id/profile.ts b/src/api/routes/users/#user_id/profile.ts new file mode 100644 index 00000000..30b4fb01 --- /dev/null +++ b/src/api/routes/users/#user_id/profile.ts @@ -0,0 +1,184 @@ +/* + 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 . +*/ + +import { route } from "@spacebar/api"; +import { + Badge, + Config, + emitEvent, + FieldErrors, + handleFile, + Member, + PrivateUserProjection, + PublicUser, + PublicUserProjection, + Relationship, + RelationshipType, + User, + UserProfileModifySchema, + UserUpdateEvent, +} from "@spacebar/util"; +import { Request, Response, Router } from "express"; +import { In } from "typeorm"; + +const router: Router = Router({ mergeParams: true }); + +router.get("/", route({ responses: { 200: { body: "UserProfileResponse" } } }), async (req: Request, res: Response) => { + if (req.params.user_id === "@me") req.params.user_id = req.user_id; + + const { guild_id, with_mutual_guilds, with_mutual_friends, with_mutual_friends_count } = req.query; + + const user = await User.getPublicUser(req.params.user_id, { + relations: ["connected_accounts"], + }); + + const mutual_guilds: object[] = []; + let premium_guild_since; + + if (with_mutual_guilds == "true") { + const requested_member = await Member.find({ + where: { id: req.params.user_id }, + }); + const self_member = await Member.find({ + where: { id: req.user_id }, + }); + + for (const rmem of requested_member) { + if (rmem.premium_since) { + if (premium_guild_since) { + if (premium_guild_since > rmem.premium_since) { + premium_guild_since = rmem.premium_since; + } + } else { + premium_guild_since = rmem.premium_since; + } + } + for (const smem of self_member) { + if (smem.guild_id === rmem.guild_id) { + mutual_guilds.push({ + id: rmem.guild_id, + nick: rmem.nick, + }); + } + } + } + } + + const guild_member = + guild_id && typeof guild_id == "string" + ? await Member.findOneOrFail({ + where: { id: req.params.user_id, guild_id: guild_id }, + relations: ["roles"], + }) + : undefined; + + // TODO: make proper DTO's in util? + + const userProfile = { + bio: req.user_bot ? null : user.bio, + accent_color: user.accent_color, + banner: user.banner, + pronouns: user.pronouns, + theme_colors: user.theme_colors?.map((t) => Number(t)), // these are strings for some reason, they should be numbers + }; + + const guildMemberProfile = { + accent_color: null, + banner: guild_member?.banner || null, + bio: guild_member?.bio || "", + guild_id, + }; + + const badges = await Badge.find(); + + let mutual_friends: PublicUser[] = []; + let mutual_friends_count = 0; + + if (with_mutual_friends == "true" || with_mutual_friends_count == "true") { + const relationshipsSelf = await Relationship.find({ where: { from_id: req.user_id, type: RelationshipType.friends } }); + const relationshipsUser = await Relationship.find({ where: { from_id: req.params.user_id, type: RelationshipType.friends } }); + const relationshipsIntersection = relationshipsSelf.filter((r1) => relationshipsUser.some((r2) => r2.to_id === r1.to_id)); + if (with_mutual_friends_count) mutual_friends_count = relationshipsIntersection.length; + if (with_mutual_friends) { + const users = await User.find({ where: { id: In(relationshipsIntersection.map((r) => r.to_id)) }, select: PublicUserProjection }); + mutual_friends = users.map((u) => u.toPublicUser()); + } + } + + res.json({ + connected_accounts: user.connected_accounts.filter((x) => x.visibility != 0), + premium_guild_since: premium_guild_since, // TODO + premium_since: user.premium_since, // TODO + mutual_guilds: with_mutual_guilds ? mutual_guilds : undefined, // TODO {id: "", nick: null} when ?with_mutual_guilds=true + mutual_friends: with_mutual_friends ? mutual_friends : undefined, + mutual_friends_count: with_mutual_friends_count ? mutual_friends_count : undefined, + 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_member?.toPublicMember(), + guild_member_profile: guild_id && guildMemberProfile, + badges: badges.filter((x) => user.badge_ids?.includes(x.id)), + }); +}); + +router.patch("/", route({ requestBody: "UserProfileModifySchema" }), async (req: Request, res: Response) => { + const body = req.body as UserProfileModifySchema; + + if (body.banner) body.banner = await handleFile(`/banners/${req.user_id}`, body.banner as string); + const user = await User.findOneOrFail({ + where: { id: req.user_id }, + select: [...PrivateUserProjection, "data"], + }); + + if (body.bio) { + const { maxBio } = Config.get().limits.user; + if (body.bio.length > maxBio) { + throw FieldErrors({ + bio: { + code: "BIO_INVALID", + message: `Bio must be less than ${maxBio} in length`, + }, + }); + } + } + + user.assign(body); + await user.save(); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + delete user.data; + + // TODO: send update member list event in gateway + await emitEvent({ + event: "USER_UPDATE", + user_id: req.user_id, + data: user, + } as UserUpdateEvent); + + res.json({ + accent_color: user.accent_color, + bio: user.bio, + banner: user.banner, + theme_colors: user.theme_colors, + pronouns: user.pronouns, + }); +}); + +export default router; diff --git a/src/api/routes/users/#user_id/relationships.ts b/src/api/routes/users/#user_id/relationships.ts new file mode 100644 index 00000000..e4af351f --- /dev/null +++ b/src/api/routes/users/#user_id/relationships.ts @@ -0,0 +1,70 @@ +/* + 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 . +*/ + +import { route } from "@spacebar/api"; +import { User, UserRelationsResponse } from "@spacebar/util"; +import { Request, Response, Router } from "express"; + +const router: Router = Router({ mergeParams: true }); + +router.get( + "/", + route({ + responses: { + 200: { body: "UserRelationsResponse" }, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const mutual_relations: UserRelationsResponse = []; + + const requested_relations = await User.findOneOrFail({ + where: { id: req.params.user_id }, + relations: ["relationships"], + }); + const self_relations = await User.findOneOrFail({ + where: { id: req.user_id }, + relations: ["relationships"], + }); + + for (const rmem of requested_relations.relationships) { + for (const smem of self_relations.relationships) + if ( + rmem.to_id === smem.to_id && + rmem.type === 1 && + rmem.to_id !== req.user_id + ) { + const relation_user = await User.getPublicUser(rmem.to_id); + + mutual_relations.push({ + id: relation_user.id, + username: relation_user.username, + avatar: relation_user.avatar, + discriminator: relation_user.discriminator, + public_flags: relation_user.public_flags, + }); + } + } + + res.json(mutual_relations); + }, +); + +export default router; -- cgit 1.5.1