From 2f7c3eb0eebafc2c3264c1b71812d9be70f22ca7 Mon Sep 17 00:00:00 2001 From: Rory& Date: Wed, 1 Jul 2026 15:14:39 +0200 Subject: [PATCH] Add application emojis implementation based on the guilds one, models --- assets/openapi.json | Bin 982407 -> 993353 bytes assets/schemas.json | Bin 444741 -> 445902 bytes .../applications/#application_id/emojis.ts | 185 ++++++++++++++++++ src/schemas/api/guilds/Emoji.ts | 1 + .../uncategorised/EmojiModifySchema.ts | 4 + src/util/config/types/LimitConfigurations.ts | 3 +- .../limits/ApplicationLimits.ts | 21 ++ .../types/subconfigurations/limits/index.ts | 1 + 8 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 src/api/routes/applications/#application_id/emojis.ts create mode 100644 src/util/config/types/subconfigurations/limits/ApplicationLimits.ts diff --git a/assets/openapi.json b/assets/openapi.json index 65e3a8f6f36efb9f44cfc0212988ddc9bd2a1855..8be85c7b1fad05b96c7c85869ddbb4d7258fdea3 100644 GIT binary patch delta 386 zcmY+;F-QVo7zSYe^M3EnJCCjgq2Vw>$RP}BOOT?dprMH72$V3eE+nZTI0(G?IO?Nx zIB2OUqw?QGY09OW3nHvIk|3if$f5qMp@wJpz879zqd9o)L<_13Hw#|TQq2>vA5mme zH4W87L^YZHSH_ zj-5=Vl7K=eU}@|aG^!ki=QD2HDop%d!#ODgjZ;sD5ifHTPYINd5-ExDlXzKf_u<6% zG-A#PRb*+3=>6GBlwN6-A!SG2Ma8&~MXj-R6xJ>uMzuAtf7B-@I{^{nX0Vxe*x=vpez6Y delta 127 zcmX?k!KVGU^#-|#lOC})3r=bmoW!_Ya1ztv(CHh(S)I4nykPpyH2uOAR*vcCWmv_h zf4IXgJAHxyGkg2Z*G$`QzGhxx57gN1q{{-tAewEvlP-J7wDu22*nyY>h&h3n3y8Ua Vm)N&4Ju=?zn=rYB^vaXI5>WS;hG1 z|8#$AR>jE%yo%G;S23wf-_XHgvK`2ZW}5yWosoO`YbjQN=?-65#iuXGXJnnc{(!*b z00oZe_5Q5<)4W+&rkm&iCMVeIPY?J7 zRCC}PbN2Lu6PU#(3#?|DeBmO?^n@AAsnh*`F|$oCHfIb4JFQ)FA~O)P05R)!&53M@ sd|<<-Pguj|HGT3THrwd}<*ecCi4_pP6`C`KPye9H%(A_2KHGLp05nuy4FCWD delta 97 zcmX>%Tl(lE=?zn=nwM8^UtZ1l{QvX-GiH|Q7ou4VwrkWdMKet=c*(*(y+DjrV0wZx ztLWqa1&-;Gs*HT?59*k5ueg&BxhfS7fA+7z}lzUdp^)h BB`p8| diff --git a/src/api/routes/applications/#application_id/emojis.ts b/src/api/routes/applications/#application_id/emojis.ts new file mode 100644 index 000000000..d7973fca9 --- /dev/null +++ b/src/api/routes/applications/#application_id/emojis.ts @@ -0,0 +1,185 @@ +/* + 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 { Request, Response, Router } from "express"; +import { route } from "@spacebar/api/util/handlers/route"; +import { Emoji } from "@spacebar/database"; +import { Config, DiscordApiErrors, Snowflake, handleFile } from "@spacebar/util"; +import { ApplicationEmojiModifySchema, EmojiCreateSchema } from "@spacebar/schemas"; + +const router = Router({ mergeParams: true }); + +router.get( + "/", + route({ + responses: { + 200: { + body: "ApplicationsEmojisResponse", + }, + 403: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { application_id } = req.params as { [key: string]: string }; + + // TODO: is there *any* gating on this endpoint? + + const emojis = await Emoji.find({ + where: { application_id: application_id }, + relations: { user: true }, + }); + + return res.json({ items: emojis }); + }, +); + +router.get( + "/:emoji_id", + route({ + responses: { + 200: { + body: "Emoji", + }, + 403: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { application_id, emoji_id } = req.params as { [key: string]: string }; + + const emoji = await Emoji.findOneOrFail({ + where: { application_id: application_id, id: emoji_id }, + relations: { user: true }, + }); + + return res.json(emoji); + }, +); + +router.post( + "/", + route({ + requestBody: "EmojiCreateSchema", + responses: { + 201: { + body: "Emoji", + }, + 403: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { application_id } = req.params as { [key: string]: string }; + const body = req.body as EmojiCreateSchema; + + const id = Snowflake.generate(); + const emoji_count = await Emoji.count({ + where: { application_id: application_id }, + }); + const { maxEmojis } = Config.get().limits.application; + + if (emoji_count >= maxEmojis) throw DiscordApiErrors.MAXIMUM_NUMBER_OF_EMOJIS_REACHED.withParams(maxEmojis); + if (body.require_colons == null) body.require_colons = true; + if (body.name?.includes("-")) body.name = body.name?.replaceAll("-", ""); // Dashes are invalid apparently + + const user = req.user; + await handleFile(`/emojis/${id}`, body.image); + + const mimeType = body.image.split(":")[1].split(";")[0]; + const emoji = await Emoji.create({ + id: id, + application_id: application_id, + name: body.name, + require_colons: body.require_colons ?? undefined, // schema allows nulls, db does not + user: user, + managed: false, + animated: mimeType == "image/gif" || mimeType == "image/apng" || mimeType == "video/webm", + available: true, + roles: [], + }).save(); + + return res.status(201).json(emoji); + }, +); + +router.patch( + "/:emoji_id", + route({ + requestBody: "ApplicationEmojiModifySchema", + responses: { + 200: { + body: "Emoji", + }, + 403: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { emoji_id, application_id } = req.params as { [key: string]: string }; + const body = req.body as ApplicationEmojiModifySchema; + + if (body.name?.includes("-")) body.name = body.name?.replaceAll("-", ""); // Dashes are invalid apparently + + await Emoji.findOneOrFail({ + where: { id: emoji_id, application_id: application_id }, + }); + + const emoji = await Emoji.create({ + ...body, + id: emoji_id, + application_id: application_id, + }).save(); + + return res.json(emoji); + }, +); + +router.delete( + "/:emoji_id", + route({ + responses: { + 204: {}, + 403: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { emoji_id, application_id } = req.params as { [key: string]: string }; + + await Emoji.delete({ + id: emoji_id, + application_id: application_id, + }); + + res.sendStatus(204); + }, +); + +export default router; diff --git a/src/schemas/api/guilds/Emoji.ts b/src/schemas/api/guilds/Emoji.ts index 6543fda7d..b0b358fda 100644 --- a/src/schemas/api/guilds/Emoji.ts +++ b/src/schemas/api/guilds/Emoji.ts @@ -2,6 +2,7 @@ import { Snowflake } from "../../Identifiers"; import { PartialUser } from "../users"; export type EmojisResponse = EmojiResponse[]; +export type ApplicationsEmojisResponse = { items: EmojiResponse[] }; // why is almost everything optional? export interface EmojiResponse { diff --git a/src/schemas/uncategorised/EmojiModifySchema.ts b/src/schemas/uncategorised/EmojiModifySchema.ts index f4efcce37..97255363d 100644 --- a/src/schemas/uncategorised/EmojiModifySchema.ts +++ b/src/schemas/uncategorised/EmojiModifySchema.ts @@ -16,6 +16,10 @@ along with this program. If not, see . */ +export interface ApplicationEmojiModifySchema { + name?: string; +} + export interface EmojiModifySchema { name?: string; roles?: string[]; diff --git a/src/util/config/types/LimitConfigurations.ts b/src/util/config/types/LimitConfigurations.ts index d3cc19f74..539256d6c 100644 --- a/src/util/config/types/LimitConfigurations.ts +++ b/src/util/config/types/LimitConfigurations.ts @@ -16,9 +16,10 @@ along with this program. If not, see . */ -import { ChannelLimits, GlobalRateLimits, GuildLimits, MessageLimits, RateLimits, UserLimits } from "."; +import { ApplicationLimits, ChannelLimits, GlobalRateLimits, GuildLimits, MessageLimits, RateLimits, UserLimits } from "."; export class LimitsConfiguration { + application: ApplicationLimits = new ApplicationLimits(); user: UserLimits = new UserLimits(); guild: GuildLimits = new GuildLimits(); message: MessageLimits = new MessageLimits(); diff --git a/src/util/config/types/subconfigurations/limits/ApplicationLimits.ts b/src/util/config/types/subconfigurations/limits/ApplicationLimits.ts new file mode 100644 index 000000000..47e09386f --- /dev/null +++ b/src/util/config/types/subconfigurations/limits/ApplicationLimits.ts @@ -0,0 +1,21 @@ +/* + 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 . +*/ + +export class ApplicationLimits { + maxEmojis: number = 100; +} diff --git a/src/util/config/types/subconfigurations/limits/index.ts b/src/util/config/types/subconfigurations/limits/index.ts index 89435e13b..b95a8b3df 100644 --- a/src/util/config/types/subconfigurations/limits/index.ts +++ b/src/util/config/types/subconfigurations/limits/index.ts @@ -16,6 +16,7 @@ along with this program. If not, see . */ +export * from "./ApplicationLimits"; export * from "./ChannelLimits"; export * from "./GlobalRateLimits"; export * from "./GuildLimits";