summary refs log tree commit diff
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2026-07-01 15:14:39 +0200
committerRory& <root@rory.gay>2026-07-01 19:46:31 +0200
commit59a81537e010ed9d7cc83d1a929e00dd91525674 (patch)
tree8215b5f08b56ec4c442a511c18562f0ca9d9863f
parentAssert that emoji being updated belongs to the requested guild (diff)
downloadserver-ts-59a81537e010ed9d7cc83d1a929e00dd91525674.tar.xz
Add application emojis implementation based on the guilds one, models
-rw-r--r--assets/openapi.json316
-rw-r--r--assets/schemas.json43
-rw-r--r--src/api/routes/applications/#application_id/emojis.ts185
-rw-r--r--src/schemas/api/guilds/Emoji.ts1
-rw-r--r--src/schemas/uncategorised/EmojiModifySchema.ts4
-rw-r--r--src/util/config/types/LimitConfigurations.ts3
-rw-r--r--src/util/config/types/subconfigurations/limits/ApplicationLimits.ts21
-rw-r--r--src/util/config/types/subconfigurations/limits/index.ts1
8 files changed, 573 insertions, 1 deletions
diff --git a/assets/openapi.json b/assets/openapi.json

index 65e3a8f6..8be85c7b 100644 --- a/assets/openapi.json +++ b/assets/openapi.json
@@ -1013,6 +1013,14 @@ "image" ] }, + "ApplicationEmojiModifySchema": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }, "EmojiModifySchema": { "type": "object", "properties": { @@ -5174,6 +5182,9 @@ "APILimitsConfiguration": { "type": "object", "properties": { + "application": { + "$ref": "#/components/schemas/ApplicationLimits" + }, "user": { "$ref": "#/components/schemas/UserLimits" }, @@ -5195,6 +5206,7 @@ }, "required": [ "absoluteRate", + "application", "channel", "guild", "message", @@ -7504,6 +7516,20 @@ "$ref": "#/components/schemas/EmojiResponse" } }, + "ApplicationsEmojisResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EmojiResponse" + } + } + }, + "required": [ + "items" + ] + }, "EmojiResponse": { "type": "object", "properties": { @@ -14295,6 +14321,17 @@ "name" ] }, + "ApplicationLimits": { + "type": "object", + "properties": { + "maxEmojis": { + "type": "integer" + } + }, + "required": [ + "maxEmojis" + ] + }, "UserLimits": { "type": "object", "properties": { @@ -27957,6 +27994,285 @@ ] } }, + "/applications/{application_id}/emojis/": { + "get": { + "security": [ + { + "bearer": [] + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApplicationsEmojisResponse" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIErrorResponse" + } + } + } + } + }, + "parameters": [ + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "application_id" + } + ], + "tags": [ + "applications" + ] + }, + "post": { + "security": [ + { + "bearer": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmojiCreateSchema" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Emoji" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIErrorResponse" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIErrorResponse" + } + } + } + } + }, + "parameters": [ + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "application_id" + } + ], + "tags": [ + "applications" + ] + } + }, + "/applications/{application_id}/emojis/{emoji_id}": { + "get": { + "security": [ + { + "bearer": [] + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Emoji" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIErrorResponse" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIErrorResponse" + } + } + } + } + }, + "parameters": [ + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "application_id" + }, + { + "name": "emoji_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "emoji_id" + } + ], + "tags": [ + "applications" + ] + }, + "patch": { + "security": [ + { + "bearer": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApplicationEmojiModifySchema" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Emoji" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIErrorResponse" + } + } + } + } + }, + "parameters": [ + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "application_id" + }, + { + "name": "emoji_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "emoji_id" + } + ], + "tags": [ + "applications" + ] + }, + "delete": { + "security": [ + { + "bearer": [] + } + ], + "responses": { + "204": { + "description": "No description available" + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIErrorResponse" + } + } + } + } + }, + "parameters": [ + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "application_id" + }, + { + "name": "emoji_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "emoji_id" + } + ], + "tags": [ + "applications" + ] + } + }, "/applications/{application_id}/commands/": { "get": { "security": [ diff --git a/assets/schemas.json b/assets/schemas.json
index 57c0a6c2..3b82e382 100644 --- a/assets/schemas.json +++ b/assets/schemas.json
@@ -1055,6 +1055,16 @@ ], "$schema": "http://json-schema.org/draft-07/schema#" }, + "ApplicationEmojiModifySchema": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + }, "EmojiModifySchema": { "type": "object", "properties": { @@ -5486,6 +5496,9 @@ "APILimitsConfiguration": { "type": "object", "properties": { + "application": { + "$ref": "#/definitions/ApplicationLimits" + }, "user": { "$ref": "#/definitions/UserLimits" }, @@ -5508,6 +5521,7 @@ "additionalProperties": false, "required": [ "absoluteRate", + "application", "channel", "guild", "message", @@ -7989,6 +8003,22 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, + "ApplicationsEmojisResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/EmojiResponse" + } + } + }, + "additionalProperties": false, + "required": [ + "items" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, "EmojiResponse": { "type": "object", "properties": { @@ -15294,6 +15324,19 @@ ], "$schema": "http://json-schema.org/draft-07/schema#" }, + "ApplicationLimits": { + "type": "object", + "properties": { + "maxEmojis": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": [ + "maxEmojis" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, "UserLimits": { "type": "object", "properties": { 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 00000000..d7973fca --- /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 <https://www.gnu.org/licenses/>. +*/ + +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 6543fda7..b0b358fd 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 f4efcce3..97255363 100644 --- a/src/schemas/uncategorised/EmojiModifySchema.ts +++ b/src/schemas/uncategorised/EmojiModifySchema.ts
@@ -16,6 +16,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ +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 d3cc19f7..539256d6 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 <https://www.gnu.org/licenses/>. */ -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 00000000..47e09386 --- /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 <https://www.gnu.org/licenses/>. +*/ + +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 89435e13..b95a8b3d 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 <https://www.gnu.org/licenses/>. */ +export * from "./ApplicationLimits"; export * from "./ChannelLimits"; export * from "./GlobalRateLimits"; export * from "./GuildLimits";