summary refs log tree commit diff
path: root/src/api
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2026-09-21 03:38:18 +0200
committerRory& <root@rory.gay>2026-09-21 03:38:18 +0200
commita1c922ebcca1de7e9c99fd7d55cedb120b2a511e (patch)
tree4c819d9687262bf21e9e34e22da491fca16a49f8 /src/api
parentInitial avatar decorations table layout (diff)
downloadserver-ts-a1c922ebcca1de7e9c99fd7d55cedb120b2a511e.tar.xz
Initial avatar decorations work
Diffstat (limited to 'src/api')
-rw-r--r--src/api/routes_toplevel/_spacebar/api/v1/avatar_decorations/#id/index.ts65
-rw-r--r--src/api/routes_toplevel/_spacebar/api/v1/avatar_decorations/index.ts64
2 files changed, 129 insertions, 0 deletions
diff --git a/src/api/routes_toplevel/_spacebar/api/v1/avatar_decorations/#id/index.ts b/src/api/routes_toplevel/_spacebar/api/v1/avatar_decorations/#id/index.ts
new file mode 100644

index 000000000..bf36ea792 --- /dev/null +++ b/src/api/routes_toplevel/_spacebar/api/v1/avatar_decorations/#id/index.ts
@@ -0,0 +1,65 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2026 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 { Router, Response, Request } from "express"; +import { route } from "@spacebar/api/middlewares"; +import { AvatarDecorations } from "@spacebar/database"; +import { PublicAvatarDecorationResponse, UpdateAvatarDecorationSchema } from "@spacebar/schemas/api/spacebar/AvatarDecorations"; +import { ApiError } from "@spacebar/util"; + +const router = Router({ mergeParams: true }); + +router.patch( + "/", + route({ + spacebarOnly: true, + description: "Get available avatar decorations", + requestBody: "UpdateAvatarDecorationSchema", + responses: { + 200: { + body: "PublicAvatarDecorationListResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const changes = req.body as UpdateAvatarDecorationSchema; + const deco = await AvatarDecorations.findOneOrFail({ where: { id: req.params.id as string } }); + + if (deco.uploader_id !== req.user_id) throw new ApiError("You do not have permission to update this avatar decoration", 0, 403); + + if (changes.public !== undefined) deco.public = changes.public; + if (changes.allowed_user_ids) { + if (changes.allowed_user_ids.add) deco.allowed_user_ids.push(...changes.allowed_user_ids.add); + if (changes.allowed_user_ids.remove) deco.allowed_user_ids = deco.allowed_user_ids.filter((x) => !changes.allowed_user_ids!.remove!.includes(x)); + } + if (changes.allowed_guild_ids) { + if (changes.allowed_guild_ids.add) deco.allowed_user_ids.push(...changes.allowed_guild_ids.add); + if (changes.allowed_guild_ids.remove) deco.allowed_user_ids = deco.allowed_user_ids.filter((x) => !changes.allowed_guild_ids!.remove!.includes(x)); + } + if (changes.allowed_role_ids) { + if (changes.allowed_role_ids.add) deco.allowed_user_ids.push(...changes.allowed_role_ids.add); + if (changes.allowed_role_ids.remove) deco.allowed_user_ids = deco.allowed_user_ids.filter((x) => !changes.allowed_role_ids!.remove!.includes(x)); + } + + // TODO: remove avatar from users that no longer have access to them + + res.json(deco.toPublicAvatarDecoration() satisfies PublicAvatarDecorationResponse); + }, +); + +export default router; diff --git a/src/api/routes_toplevel/_spacebar/api/v1/avatar_decorations/index.ts b/src/api/routes_toplevel/_spacebar/api/v1/avatar_decorations/index.ts new file mode 100644
index 000000000..e403ef6e9 --- /dev/null +++ b/src/api/routes_toplevel/_spacebar/api/v1/avatar_decorations/index.ts
@@ -0,0 +1,64 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2026 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 { Router, Response, Request } from "express"; +import { Raw } from "typeorm"; +import { route } from "@spacebar/api/middlewares"; +import { AvatarDecorations, Member } from "@spacebar/database"; +import { PublicAvatarDecorationListResponse } from "@spacebar/schemas/api/spacebar/AvatarDecorations"; +import { arrayDistinctBy } from "@spacebar/extensions"; + +const router = Router({ mergeParams: true }); + +router.get( + "/", + route({ + spacebarOnly: true, + description: "Get available avatar decorations", + responses: { + 200: { + body: "PublicAvatarDecorationListResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const memberships = await Member.find({ select: { guild_id: true, roles: { id: true } }, relations: { roles: true }, where: { id: req.user_id } }); + + const decos = ( + await AvatarDecorations.find({ + where: [ + { approved: true, public: true }, + { approved: true, uploader_id: req.user_id }, + { approved: true, allowed_user_ids: Raw((columnAlias) => `${columnAlias} && ARRAY[:req_uid]::int8[]`, { req_uid: req.user_id }) }, + { approved: true, allowed_guild_ids: Raw((columnAlias) => `${columnAlias} && :guild_ids`, { guild_ids: memberships.map((x) => x.guild_id) }) }, + { approved: true, allowed_role_ids: Raw((columnAlias) => `${columnAlias} && :role_ids`, { role_ids: memberships.flatMap((x) => x.roles.map((r) => r.id)) }) }, + ], + relations: { + uploader: true, + }, + order: { + id: "DESC", + }, + }) + ).map((x) => x.toPublicAvatarDecoration({ available: true })); + + res.json(arrayDistinctBy(decos, (d) => d.id) satisfies PublicAvatarDecorationListResponse); + }, +); + +export default router;