diff --git a/assets/openapi.json b/assets/openapi.json index 65e3a8f6f..8be85c7b1 100644 Binary files a/assets/openapi.json and b/assets/openapi.json differ diff --git a/assets/schemas.json b/assets/schemas.json index 57c0a6c20..3b82e3826 100644 Binary files a/assets/schemas.json and b/assets/schemas.json differ 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";