From a1c922ebcca1de7e9c99fd7d55cedb120b2a511e Mon Sep 17 00:00:00 2001 From: Rory& Date: Mon, 21 Sep 2026 03:38:18 +0200 Subject: [PATCH] Initial avatar decorations work --- assets/openapi.json | Bin 1096222 -> 1100978 bytes assets/schemas.json | Bin 512137 -> 515460 bytes .../api/v1/avatar_decorations/#id/index.ts | 65 ++++++++++++++++++ .../api/v1/avatar_decorations/index.ts | 64 +++++++++++++++++ src/database/entities/AvatarDecoration.ts | 11 +++ src/database/entities/index.ts | 1 + src/schemas/api/spacebar/AvatarDecorations.ts | 42 +++++++++++ 7 files changed, 183 insertions(+) create mode 100644 src/api/routes_toplevel/_spacebar/api/v1/avatar_decorations/#id/index.ts create mode 100644 src/api/routes_toplevel/_spacebar/api/v1/avatar_decorations/index.ts create mode 100644 src/schemas/api/spacebar/AvatarDecorations.ts diff --git a/assets/openapi.json b/assets/openapi.json index 9e951318550da9cc6fc5734195b3d62b102e054d..e9c892ee2725a41eff0c74d17543480abec5e78e 100644 GIT binary patch delta 624 zcmbRD#c9(_=M5(oPrtXCg-yV*EU_f9$R#y7zbLUJGe2+gIr~;_B*E!{icF^B0Y#ZW zb*V_=FvShiA5^lkVd!W+vAF%jVn!fl0%B$$W&vW>?I#wqS=B$dR zI5!_~uwk06pw7X{STmK=2SvKmKX@^*O+PHnsW82vh*@s>hJP#;)At(!#f;OL z)h1uaHJk45!^$z;!G+Cb@`dy6VExl4@o-2__w!-poV<6D^z;uM9I}%GXG(z0n#`CY zGX2167I^`5U%?$RdBS>U_SBU4(&E&j>4_X1C#E}?F>_1~YCbo8!d6xmkW^8APAXVx zbAtOnMv5H`HeYzA5~}$Rzq8ALt!4qc1k42m4?*uOxXvio*2Lb{#L?Eo+1A9>*2LY` z#M9Qq+t$R_*2KT9NnoiCf0?0vA~^cvQ$X=Q?H2Er=>lK5ghhd(nK_9`IjIT|c?FRC s^bH4?rM5q);FAIRb|sdiGrh2qr*?XS5|{J#ck2aM7`HE7Cs4@^0QX4jTL1t6 delta 86 zcmdlq(|O((rwu0-H#aP8Z&=C*#7scU48$xz%(}f{DVxQ;_L&pdftUk`If0l9h`E87 Z2Z(urm=B2gx6hm)uvBNezw?r#F0OViQCW&&Q*1U4C%wr07GcDW8lAZ7w$W*}zSF4w`j<2u+0JjkX^f0)abu>FBI z(=o>B7tXM;GL}tmWM*`ozP^f`eY)^FR`KZ*&N7NmZ%}1qn_i&Ds5Cu+iIr`-yA7+_ zbOC)vx#=;6tTxjdc$p(vit@`+rzcvlYE8f3#4a&C#EVq`Vv0}_d$=$b=Yp*En67Wm z#KBZrJl)|3Q}^@&QAUC32IfrclTD)Mff>w2`8m@ko@Sgmt&l~63hoSt7{8@X3d{hy sIwy7dhwW@4(+$qDu(GG7#FrMQ7C}Tn;lYH*Q|;H{*|uMcXWv`^0DcIl2LJ#7 delta 44 wcmZqKE#J9NeuG>`^OMf@C!LHy%ml>DK+LlJNhj-$>+KhF*tTEHVc%Q;0QFH8XaE2J 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 . +*/ + +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 . +*/ + +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; diff --git a/src/database/entities/AvatarDecoration.ts b/src/database/entities/AvatarDecoration.ts index dedc8f3e6..695de8850 100644 --- a/src/database/entities/AvatarDecoration.ts +++ b/src/database/entities/AvatarDecoration.ts @@ -18,6 +18,7 @@ import { Column, Entity, Index, JoinColumn, ManyToOne, RelationId } from "typeorm"; import { AvatarDecorationData } from "@spacebar/schemas"; +import { PublicAvatarDecoration } from "@spacebar/schemas/api/spacebar/AvatarDecorations"; import { BaseClass } from "./BaseClass"; import { User } from "./User"; @@ -60,4 +61,14 @@ export class AvatarDecorations extends BaseClass { expires_at: null, } satisfies AvatarDecorationData; } + + toPublicAvatarDecoration(opts?: {available: boolean}): PublicAvatarDecoration { + return { + id: this.id, + approved: this.approved, + uploader: this.uploader.toPartialUser(), + public: this.public, + available: opts?.available ?? this.public + } satisfies PublicAvatarDecoration; + } } diff --git a/src/database/entities/index.ts b/src/database/entities/index.ts index 79fceecca..429efb0a3 100644 --- a/src/database/entities/index.ts +++ b/src/database/entities/index.ts @@ -21,6 +21,7 @@ export * from "./ApplicationCommand"; export * from "./Attachment"; export * from "./AuditLog"; export * from "./AutomodRule"; +export * from "./AvatarDecoration"; export * from "./BackupCodes"; export * from "./Badge"; export * from "./Ban"; diff --git a/src/schemas/api/spacebar/AvatarDecorations.ts b/src/schemas/api/spacebar/AvatarDecorations.ts new file mode 100644 index 000000000..5c3992744 --- /dev/null +++ b/src/schemas/api/spacebar/AvatarDecorations.ts @@ -0,0 +1,42 @@ +/* + 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 . +*/ + +import { PartialUser, Snowflake } from "@spacebar/schemas"; + +export type PublicAvatarDecorationListResponse = PublicAvatarDecorationResponse[]; +export type PrivateAvatarDecorationListResponse = PrivateAvatarDecorationResponse[]; +export interface PublicAvatarDecorationResponse { + id: Snowflake; + approved: boolean; + uploader: PartialUser; + public: boolean; + available: boolean; +} + +export interface PrivateAvatarDecorationResponse extends PublicAvatarDecorationResponse { + allowed_user_ids: Snowflake[]; + allowed_guild_ids: Snowflake[]; + allowed_role_ids: Snowflake[]; +} + +export interface UpdateAvatarDecorationSchema { + public?: boolean; + allowed_user_ids?: { add?: Snowflake[]; remove?: Snowflake[] }; + allowed_guild_ids?: { add?: Snowflake[]; remove?: Snowflake[] }; + allowed_role_ids?: { add?: Snowflake[]; remove?: Snowflake[] }; +}