From e0839677d50abf8be381d3d902620482bc9dd32b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lobo=20Metal=C3=BArgico?= <43734867+LoboMetalurgico@users.noreply.github.com> Date: Fri, 8 Oct 2021 23:45:12 -0300 Subject: [PATCH 1/9] (api): inital emojis route implementation --- api/src/routes/guilds/#guild_id/emojis.ts | 78 +++++++++++++++++++++++ util/src/entities/Config.ts | 2 + 2 files changed, 80 insertions(+) create mode 100644 api/src/routes/guilds/#guild_id/emojis.ts diff --git a/api/src/routes/guilds/#guild_id/emojis.ts b/api/src/routes/guilds/#guild_id/emojis.ts new file mode 100644 index 00000000..f1e427e8 --- /dev/null +++ b/api/src/routes/guilds/#guild_id/emojis.ts @@ -0,0 +1,78 @@ +import { Router, Request, Response } from "express"; +import { + Config, + emitEvent, + Emoji, + GuildEmojiUpdateEvent, + Member, + User +} from "@fosscord/util"; +import { HTTPError } from "lambert-server"; +import { route } from "@fosscord/api"; + +const router = Router(); + +export interface EmojiCreateSchema { + name?: string; + image?: string; + roles?: string[]; +} + +router.get("/", route({}), async (req: Request, res: Response) => { + const guild_id = req.params.guild_id; + + await Member.IsInGuildOrFail(req.user_id, guild_id); + + const emojis = await Emoji.find({ guild_id: guild_id }); + + return res.json(emojis); +}); + +router.get("/:emoji_id", route({}), async (req: Request, res: Response) => { + const guild_id = req.params.guild_id; + const emoji_id = req.params.emoji_id; + + await Member.IsInGuildOrFail(req.user_id, guild_id); + + const emoji = await Emoji.findOneOrFail({ guild_id: guild_id, id: emoji_id }); + + return res.json(emoji); +}); + +/** WIP +router.post("/", route({ body: "EmojiCreateSchema", permission: "MANAGE_EMOJIS_AND_STICKERS" }), async (req: Request, res: Response) => { + const guild_id = req.params.guild_id; + const body = req.body as EmojiCreateSchema; + + const emoji_count = await Emoji.count({ guild_id: guild_id }); + const { maxEmojis } = Config.get().limits.guild; + + if (emoji_count >= maxEmojis) { + throw new HTTPError("Max emojis reached", 400); + } +}); +*/ + +router.delete("/:emoji_id", route({ permission: "MANAGE_EMOJIS_AND_STICKERS" }), async (req: Request, res: Response) => { + const guild_id = req.params.guild_id; + const { emoji_id } = req.params; + + await Promise.all([ + Emoji.delete({ + id: emoji_id, + guild_id: guild_id + }), + emitEvent({ + event: "GUILD_EMOJI_UPDATE", + guild_id: guild_id, + data: { + guild_id: guild_id, + emojis: await Emoji.find({ guild_id: guild_id }) + } + } as GuildEmojiUpdateEvent) + ]) + + res.sendStatus(204); +}); + +export default router; diff --git a/util/src/entities/Config.ts b/util/src/entities/Config.ts index 921a12c2..ed2bcab3 100644 --- a/util/src/entities/Config.ts +++ b/util/src/entities/Config.ts @@ -64,6 +64,7 @@ export interface ConfigValue { }; guild: { maxRoles: number; + maxEmojis: number; maxMembers: number; maxChannels: number; maxChannelsInCategory: number; @@ -187,6 +188,7 @@ export const DefaultConfigOptions: ConfigValue = { }, guild: { maxRoles: 250, + maxEmojis: 50, // TODO: max emojis per guild per nitro level maxMembers: 250000, maxChannels: 500, maxChannelsInCategory: 50, From 8bc98e5f68350a242418078cf705396511bbf86a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lobo=20Metal=C3=BArgico?= <43734867+LoboMetalurgico@users.noreply.github.com> Date: Fri, 8 Oct 2021 23:49:45 -0300 Subject: [PATCH 2/9] (api): fix route name --- api/src/routes/guilds/#guild_id/{emojis.ts => emoji.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename api/src/routes/guilds/#guild_id/{emojis.ts => emoji.ts} (100%) diff --git a/api/src/routes/guilds/#guild_id/emojis.ts b/api/src/routes/guilds/#guild_id/emoji.ts similarity index 100% rename from api/src/routes/guilds/#guild_id/emojis.ts rename to api/src/routes/guilds/#guild_id/emoji.ts From ec8b59c5e5aa20c4865c5cab0864178a185deb1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lobo=20Metal=C3=BArgico?= <43734867+LoboMetalurgico@users.noreply.github.com> Date: Sat, 9 Oct 2021 10:52:43 -0300 Subject: [PATCH 3/9] (api): add patch to emoji route --- api/src/routes/guilds/#guild_id/emoji.ts | 26 ++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/api/src/routes/guilds/#guild_id/emoji.ts b/api/src/routes/guilds/#guild_id/emoji.ts index f1e427e8..f14790ed 100644 --- a/api/src/routes/guilds/#guild_id/emoji.ts +++ b/api/src/routes/guilds/#guild_id/emoji.ts @@ -18,6 +18,11 @@ export interface EmojiCreateSchema { roles?: string[]; } +export interface EmojiModifySchema { + name?: string; + roles?: string[]; +} + router.get("/", route({}), async (req: Request, res: Response) => { const guild_id = req.params.guild_id; @@ -53,6 +58,27 @@ router.post("/", route({ body: "EmojiCreateSchema", permission: "MANAGE_EMOJIS_A }); */ +router.patch("/:emoji_id", route({ body: "EmojiModifySchema", permission: "MANAGE_EMOJIS_AND_STICKERS" }), async (req: Request, res: Response) => { + const { emoji_id, guild_id } = req.params; + const body = req.body as EmojiModifySchema; + + const emoji = new Emoji({ ...body, id: emoji_id, guild_id: guild_id }); + + await Promise.all([ + emoji.save(), + emitEvent({ + event: "GUILD_EMOJI_UPDATE", + guild_id: guild_id, + data: { + guild_id: guild_id, + emojis: await Emoji.find({ guild_id: guild_id }) + } + } as GuildEmojiUpdateEvent) + ]) + + return res.json(emoji); +}); + router.delete("/:emoji_id", route({ permission: "MANAGE_EMOJIS_AND_STICKERS" }), async (req: Request, res: Response) => { const guild_id = req.params.guild_id; const { emoji_id } = req.params; From e642b6194aae348e489777008a17a8b6e63b9d0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lobo=20Metal=C3=BArgico?= <43734867+LoboMetalurgico@users.noreply.github.com> Date: Sat, 9 Oct 2021 11:36:58 -0300 Subject: [PATCH 4/9] (api): implement post on emoji route --- api/src/routes/guilds/#guild_id/emoji.ts | 42 +++++++++++++++++++++--- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/api/src/routes/guilds/#guild_id/emoji.ts b/api/src/routes/guilds/#guild_id/emoji.ts index f14790ed..6b194f7b 100644 --- a/api/src/routes/guilds/#guild_id/emoji.ts +++ b/api/src/routes/guilds/#guild_id/emoji.ts @@ -1,10 +1,13 @@ import { Router, Request, Response } from "express"; import { Config, + DiscordApiErrors, emitEvent, Emoji, GuildEmojiUpdateEvent, + handleFile, Member, + Snowflake, User } from "@fosscord/util"; import { HTTPError } from "lambert-server"; @@ -15,6 +18,7 @@ const router = Router(); export interface EmojiCreateSchema { name?: string; image?: string; + require_colons?: boolean | null; roles?: string[]; } @@ -44,7 +48,6 @@ router.get("/:emoji_id", route({}), async (req: Request, res: Response) => { return res.json(emoji); }); -/** WIP router.post("/", route({ body: "EmojiCreateSchema", permission: "MANAGE_EMOJIS_AND_STICKERS" }), async (req: Request, res: Response) => { const guild_id = req.params.guild_id; const body = req.body as EmojiCreateSchema; @@ -52,11 +55,42 @@ router.post("/", route({ body: "EmojiCreateSchema", permission: "MANAGE_EMOJIS_A const emoji_count = await Emoji.count({ guild_id: guild_id }); const { maxEmojis } = Config.get().limits.guild; - if (emoji_count >= maxEmojis) { - throw new HTTPError("Max emojis reached", 400); + if (emoji_count >= maxEmojis) throw DiscordApiErrors.MAXIMUM_NUMBER_OF_EMOJIS_REACHED.withParams(maxEmojis); + + const id = Snowflake.generate(); + + if (!body.image) { + throw DiscordApiErrors.GENERAL_ERROR.withParams("No image provided"); } + + if (body.require_colons === null) body.require_colons = true; + + const user = await User.findOneOrFail({ id: req.user_id }); + + body.image = await handleFile(`/emojis/${id}`, body.image); + + const emoji = new Emoji({ + id: id, + guild_id: guild_id, + ...body, + user: user, + managed: false, + animated: false, // TODO: Add support animated emojis + available: true + }); + + await Promise.all([ + emoji.save(), + emitEvent({ + event: "GUILD_EMOJI_UPDATE", + guild_id: guild_id, + data: { + guild_id: guild_id, + emojis: await Emoji.find({ guild_id: guild_id }) + } + } as GuildEmojiUpdateEvent) + ]); }); -*/ router.patch("/:emoji_id", route({ body: "EmojiModifySchema", permission: "MANAGE_EMOJIS_AND_STICKERS" }), async (req: Request, res: Response) => { const { emoji_id, guild_id } = req.params; From 5c4774a6f9ab7fd05f43c1da556d1395ca2d0168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lobo=20Metal=C3=BArgico?= <43734867+LoboMetalurgico@users.noreply.github.com> Date: Sat, 9 Oct 2021 11:43:27 -0300 Subject: [PATCH 5/9] (api): fix some issues --- api/src/routes/guilds/#guild_id/emoji.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/api/src/routes/guilds/#guild_id/emoji.ts b/api/src/routes/guilds/#guild_id/emoji.ts index 6b194f7b..000c9949 100644 --- a/api/src/routes/guilds/#guild_id/emoji.ts +++ b/api/src/routes/guilds/#guild_id/emoji.ts @@ -10,7 +10,6 @@ import { Snowflake, User } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; import { route } from "@fosscord/api"; const router = Router(); @@ -75,7 +74,7 @@ router.post("/", route({ body: "EmojiCreateSchema", permission: "MANAGE_EMOJIS_A ...body, user: user, managed: false, - animated: false, // TODO: Add support animated emojis + animated: false, // TODO: Add support animated emojis available: true }); @@ -108,7 +107,7 @@ router.patch("/:emoji_id", route({ body: "EmojiModifySchema", permission: "MANAG emojis: await Emoji.find({ guild_id: guild_id }) } } as GuildEmojiUpdateEvent) - ]) + ]); return res.json(emoji); }); From 2908544b271dcdf5fb3716b48c27d9b7b9506bb0 Mon Sep 17 00:00:00 2001 From: Flam3rboy <34555296+Flam3rboy@users.noreply.github.com> Date: Sun, 10 Oct 2021 14:08:52 +0200 Subject: [PATCH 6/9] :bug: fix cdn --- util/src/util/cdn.ts | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/util/src/util/cdn.ts b/util/src/util/cdn.ts index 8d45f85f..4dd0078a 100644 --- a/util/src/util/cdn.ts +++ b/util/src/util/cdn.ts @@ -25,30 +25,15 @@ export async function uploadFile(path: string, file: Express.Multer.File) { return result; } -export async function handleFile( - path: string, - body?: string -): Promise< - | (string & { - id: string; - content_type: string; - size: number; - url: string; - }) - | undefined -> { +export async function handleFile(path: string, body?: string): Promise { if (!body || !body.startsWith("data:")) return undefined; try { const mimetype = body.split(":")[1].split(";")[0]; const buffer = Buffer.from(body.split(",")[1], "base64"); // @ts-ignore - const file = await uploadFile(path, { buffer, mimetype, originalname: "banner" }); - const obj = file.id; - for (const key in file) { - obj[key] = file[key]; - } - return obj; + const { id } = await uploadFile(path, { buffer, mimetype, originalname: "banner" }); + return id; } catch (error) { console.error(error); throw new HTTPError("Invalid " + path); From 63da3c0da4eaf8ca29db24ff14fffd771fd56314 Mon Sep 17 00:00:00 2001 From: Flam3rboy <34555296+Flam3rboy@users.noreply.github.com> Date: Sun, 10 Oct 2021 14:09:18 +0200 Subject: [PATCH 7/9] :bug: fix cdn route not working without hash --- cdn/src/routes/avatars.ts | 15 +++++++++++++++ cdn/src/util/FileStorage.ts | 14 +++++++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/cdn/src/routes/avatars.ts b/cdn/src/routes/avatars.ts index 3d5e7d77..2a4a0ffe 100644 --- a/cdn/src/routes/avatars.ts +++ b/cdn/src/routes/avatars.ts @@ -58,6 +58,21 @@ router.post( } ); +router.get("/:user_id", async (req: Request, res: Response) => { + var { user_id } = req.params; + user_id = user_id.split(".")[0]; // remove .file extension + const path = `avatars/${user_id}`; + + const file = await storage.get(path); + if (!file) throw new HTTPError("not found", 404); + const type = await FileType.fromBuffer(file); + + res.set("Content-Type", type?.mime); + res.set("Cache-Control", "public, max-age=31536000"); + + return res.send(file); +}); + router.get("/:user_id/:hash", async (req: Request, res: Response) => { var { user_id, hash } = req.params; hash = hash.split(".")[0]; // remove .file extension diff --git a/cdn/src/util/FileStorage.ts b/cdn/src/util/FileStorage.ts index e0b24a84..84ecf556 100644 --- a/cdn/src/util/FileStorage.ts +++ b/cdn/src/util/FileStorage.ts @@ -13,16 +13,24 @@ function getPath(path: string) { const root = process.env.STORAGE_LOCATION || "../"; var filename = join(root, path); - if (path.indexOf("\0") !== -1 || !filename.startsWith(root)) throw new Error("invalid path"); + if (path.indexOf("\0") !== -1 || !filename.startsWith(root)) + throw new Error("invalid path"); return filename; } export class FileStorage implements Storage { async get(path: string): Promise { + path = getPath(path); try { - return fs.readFileSync(getPath(path)); + return fs.readFileSync(path); } catch (error) { - return null; + try { + const files = fs.readdirSync(path); + if (!files.length) return null; + return fs.readFileSync(join(path, files[0])); + } catch (error) { + return null; + } } } From ebb0f567d5b010951bc0b9b35ffa002135528e1a Mon Sep 17 00:00:00 2001 From: Flam3rboy <34555296+Flam3rboy@users.noreply.github.com> Date: Sun, 10 Oct 2021 14:09:54 +0200 Subject: [PATCH 8/9] :art: emoji db migration --- gateway/src/listener/listener.ts | 2 +- util/ormconfig.json | 9 + util/package-lock.json | 168 +++++++++++++++++- util/package.json | 6 +- util/src/entities/Emoji.ts | 14 +- util/src/interfaces/Event.ts | 8 +- .../migrations/1633864260873-EmojiRoles.ts | 13 ++ .../src/migrations/1633864669243-EmojiUser.ts | 23 +++ 8 files changed, 234 insertions(+), 9 deletions(-) create mode 100644 util/ormconfig.json create mode 100644 util/src/migrations/1633864260873-EmojiRoles.ts create mode 100644 util/src/migrations/1633864669243-EmojiUser.ts diff --git a/gateway/src/listener/listener.ts b/gateway/src/listener/listener.ts index ee640f38..c5b1a576 100644 --- a/gateway/src/listener/listener.ts +++ b/gateway/src/listener/listener.ts @@ -178,7 +178,7 @@ async function consume(this: WebSocket, opts: EventOpts) { case "CHANNEL_CREATE": case "CHANNEL_DELETE": case "CHANNEL_UPDATE": - case "GUILD_EMOJI_UPDATE": + case "GUILD_EMOJIS_UPDATE": case "READY": // will be sent by the gateway case "USER_UPDATE": case "APPLICATION_COMMAND_CREATE": diff --git a/util/ormconfig.json b/util/ormconfig.json new file mode 100644 index 00000000..c5587b8e --- /dev/null +++ b/util/ormconfig.json @@ -0,0 +1,9 @@ +{ + "type": "sqlite", + "database": "../bundle/database.db", + "migrations": ["src/migrations/*.ts"], + "entities": ["src/entities/*.ts"], + "cli": { + "migrationsDir": "src/migrations" + } +} diff --git a/util/package-lock.json b/util/package-lock.json index fa4549c6..5f136dbc 100644 --- a/util/package-lock.json +++ b/util/package-lock.json @@ -31,7 +31,8 @@ "@types/multer": "^1.4.7", "@types/node": "^14.17.9", "@types/node-fetch": "^2.5.12", - "jest": "^27.0.6" + "jest": "^27.0.6", + "ts-node": "^10.2.1" } }, "node_modules/@babel/code-frame": { @@ -666,6 +667,27 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@cspotcode/source-map-consumer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", + "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.6.1.tgz", + "integrity": "sha512-DX3Z+T5dt1ockmPdobJS/FAsQPW4V4SrWEhD2iYQT2Cb2tQsiMnYxrcUH9By/Z3B+v0S5LMBkQtV/XOBbpLEOg==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-consumer": "0.8.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1003,6 +1025,30 @@ "node": ">= 6" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", + "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", + "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", + "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", + "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", + "dev": true + }, "node_modules/@types/amqplib": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/@types/amqplib/-/amqplib-0.8.2.tgz", @@ -6431,6 +6477,59 @@ "node": ">=8" } }, + "node_modules/ts-node": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.2.1.tgz", + "integrity": "sha512-hCnyOyuGmD5wHleOQX6NIjJtYVIO8bPP8F2acWkB4W06wdlkgyvJtubO/I9NkI88hCFECbsEgoLc0VNkYmcSfw==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "0.6.1", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", @@ -7733,6 +7832,21 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "@cspotcode/source-map-consumer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", + "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==", + "dev": true + }, + "@cspotcode/source-map-support": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.6.1.tgz", + "integrity": "sha512-DX3Z+T5dt1ockmPdobJS/FAsQPW4V4SrWEhD2iYQT2Cb2tQsiMnYxrcUH9By/Z3B+v0S5LMBkQtV/XOBbpLEOg==", + "dev": true, + "requires": { + "@cspotcode/source-map-consumer": "0.8.0" + } + }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -8003,6 +8117,30 @@ "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", "dev": true }, + "@tsconfig/node10": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", + "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==", + "dev": true + }, + "@tsconfig/node12": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", + "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", + "dev": true + }, + "@tsconfig/node14": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", + "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==", + "dev": true + }, + "@tsconfig/node16": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", + "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", + "dev": true + }, "@types/amqplib": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/@types/amqplib/-/amqplib-0.8.2.tgz", @@ -12301,6 +12439,34 @@ "punycode": "^2.1.1" } }, + "ts-node": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.2.1.tgz", + "integrity": "sha512-hCnyOyuGmD5wHleOQX6NIjJtYVIO8bPP8F2acWkB4W06wdlkgyvJtubO/I9NkI88hCFECbsEgoLc0VNkYmcSfw==", + "dev": true, + "requires": { + "@cspotcode/source-map-support": "0.6.1", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "yn": "3.1.1" + }, + "dependencies": { + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true + } + } + }, "tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", diff --git a/util/package.json b/util/package.json index 5efc16ae..e1003114 100644 --- a/util/package.json +++ b/util/package.json @@ -8,7 +8,8 @@ "start": "npm run build && node dist/", "test": "npm run build && jest", "postinstall": "npm run build", - "build": "npx tsc -p ." + "build": "npx tsc -p .", + "typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js" }, "repository": { "type": "git", @@ -33,7 +34,8 @@ "@types/multer": "^1.4.7", "@types/node": "^14.17.9", "@types/node-fetch": "^2.5.12", - "jest": "^27.0.6" + "jest": "^27.0.6", + "ts-node": "^10.2.1" }, "dependencies": { "amqplib": "^0.8.0", diff --git a/util/src/entities/Emoji.ts b/util/src/entities/Emoji.ts index a252d9f4..03218375 100644 --- a/util/src/entities/Emoji.ts +++ b/util/src/entities/Emoji.ts @@ -1,4 +1,5 @@ -import { Column, Entity, JoinColumn, ManyToOne } from "typeorm"; +import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import { User } from "."; import { BaseClass } from "./BaseClass"; import { Guild } from "./Guild"; import { Role } from "./Role"; @@ -20,6 +21,14 @@ export class Emoji extends BaseClass { }) guild: Guild; + @Column({ nullable: true }) + @RelationId((emoji: Emoji) => emoji.user) + user_id: string; + + @JoinColumn({ name: "user_id" }) + @ManyToOne(() => User) + user: User; + @Column() managed: boolean; @@ -28,4 +37,7 @@ export class Emoji extends BaseClass { @Column() require_colons: boolean; + + @Column({ type: "simple-array" }) + roles: string[]; // roles this emoji is whitelisted to (new discord feature?) } diff --git a/util/src/interfaces/Event.ts b/util/src/interfaces/Event.ts index 03099bbb..3c8ab8ab 100644 --- a/util/src/interfaces/Event.ts +++ b/util/src/interfaces/Event.ts @@ -185,8 +185,8 @@ export interface GuildBanRemoveEvent extends Event { }; } -export interface GuildEmojiUpdateEvent extends Event { - event: "GUILD_EMOJI_UPDATE"; +export interface GuildEmojisUpdateEvent extends Event { + event: "GUILD_EMOJIS_UPDATE"; data: { guild_id: string; emojis: Emoji[]; @@ -459,7 +459,7 @@ export type EventData = | GuildDeleteEvent | GuildBanAddEvent | GuildBanRemoveEvent - | GuildEmojiUpdateEvent + | GuildEmojisUpdateEvent | GuildIntegrationUpdateEvent | GuildMemberAddEvent | GuildMemberRemoveEvent @@ -552,7 +552,7 @@ export type EVENT = | "GUILD_DELETE" | "GUILD_BAN_ADD" | "GUILD_BAN_REMOVE" - | "GUILD_EMOJI_UPDATE" + | "GUILD_EMOJIS_UPDATE" | "GUILD_INTEGRATIONS_UPDATE" | "GUILD_MEMBER_ADD" | "GUILD_MEMBER_REMOVE" diff --git a/util/src/migrations/1633864260873-EmojiRoles.ts b/util/src/migrations/1633864260873-EmojiRoles.ts new file mode 100644 index 00000000..f0d709f2 --- /dev/null +++ b/util/src/migrations/1633864260873-EmojiRoles.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class EmojiRoles1633864260873 implements MigrationInterface { + name = "EmojiRoles1633864260873"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "emojis" ADD "roles" text NOT NULL DEFAULT ''`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "emojis" DROP COLUMN column_name "roles"`); + } +} diff --git a/util/src/migrations/1633864669243-EmojiUser.ts b/util/src/migrations/1633864669243-EmojiUser.ts new file mode 100644 index 00000000..982405d7 --- /dev/null +++ b/util/src/migrations/1633864669243-EmojiUser.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class EmojiUser1633864669243 implements MigrationInterface { + name = "EmojiUser1633864669243"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "emojis" ADD "user_id" varchar`); + try { + await queryRunner.query( + `ALTER TABLE "emojis" ADD CONSTRAINT FK_fa7ddd5f9a214e28ce596548421 FOREIGN KEY (user_id) REFERENCES users(id)` + ); + } catch (error) { + console.error( + "sqlite doesn't support altering foreign keys: https://stackoverflow.com/questions/1884818/how-do-i-add-a-foreign-key-to-an-existing-sqlite-table" + ); + } + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "emojis" DROP COLUMN column_name "user_id"`); + await queryRunner.query(`ALTER TABLE "emojis" DROP CONSTRAINT FK_fa7ddd5f9a214e28ce596548421`); + } +} From 4d4e94d8d754909d87e249578945ce8ff9ae6ba6 Mon Sep 17 00:00:00 2001 From: Flam3rboy <34555296+Flam3rboy@users.noreply.github.com> Date: Sun, 10 Oct 2021 14:10:27 +0200 Subject: [PATCH 9/9] :bug: fix emojis route --- api/assets/schemas.json | 346 +++++++++++++++++++--- api/src/routes/guilds/#guild_id/emoji.ts | 137 --------- api/src/routes/guilds/#guild_id/emojis.ts | 121 ++++++++ 3 files changed, 431 insertions(+), 173 deletions(-) delete mode 100644 api/src/routes/guilds/#guild_id/emoji.ts create mode 100644 api/src/routes/guilds/#guild_id/emojis.ts diff --git a/api/assets/schemas.json b/api/assets/schemas.json index c2149836..4f1ab9a8 100644 --- a/api/assets/schemas.json +++ b/api/assets/schemas.json @@ -2887,47 +2887,321 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "EmojiListResponse": { - "type": "array", - "items": { - "type": "object", - "properties": { - "animated": { - "type": "boolean" - }, - "available": { - "type": "boolean" - }, - "id": { + "EmojiCreateSchema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "image": { + "type": "string" + }, + "require_colons": { + "type": [ + "null", + "boolean" + ] + }, + "roles": { + "type": "array", + "items": { "type": "string" - }, - "managed": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "require_colons": { - "type": "boolean" - }, - "guild_id": { - "type": "string" - }, - "roles": { - "type": "array", - "items": { + } + } + }, + "definitions": { + "ChannelPermissionOverwriteType": { + "enum": [ + 0, + 1 + ], + "type": "number" + }, + "Embed": { + "type": "object", + "properties": { + "title": { "type": "string" + }, + "type": { + "enum": [ + "article", + "gifv", + "image", + "link", + "rich", + "video" + ], + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "color": { + "type": "integer" + }, + "footer": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "icon_url": { + "type": "string" + }, + "proxy_icon_url": { + "type": "string" + } + }, + "required": [ + "text" + ] + }, + "image": { + "$ref": "#/definitions/EmbedImage" + }, + "thumbnail": { + "$ref": "#/definitions/EmbedImage" + }, + "video": { + "$ref": "#/definitions/EmbedImage" + }, + "provider": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "author": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string" + }, + "icon_url": { + "type": "string" + }, + "proxy_icon_url": { + "type": "string" + } + } + }, + "fields": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + }, + "inline": { + "type": "boolean" + } + }, + "required": [ + "name", + "value" + ] + } } } }, - "required": [ - "animated", - "available", - "id", - "managed", - "name", - "require_colons" - ] + "EmbedImage": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "proxy_url": { + "type": "string" + }, + "height": { + "type": "integer" + }, + "width": { + "type": "integer" + } + } + }, + "ChannelModifySchema": { + "type": "object", + "properties": { + "name": { + "maxLength": 100, + "type": "string" + }, + "type": { + "enum": [ + 0, + 1, + 10, + 11, + 12, + 13, + 2, + 3, + 4, + 5, + 6 + ], + "type": "number" + }, + "topic": { + "type": "string" + }, + "icon": { + "type": [ + "null", + "string" + ] + }, + "bitrate": { + "type": "integer" + }, + "user_limit": { + "type": "integer" + }, + "rate_limit_per_user": { + "type": "integer" + }, + "position": { + "type": "integer" + }, + "permission_overwrites": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/ChannelPermissionOverwriteType" + }, + "allow": { + "type": "string" + }, + "deny": { + "type": "string" + } + }, + "required": [ + "allow", + "deny", + "id", + "type" + ] + } + }, + "parent_id": { + "type": "string" + }, + "id": { + "type": "string" + }, + "nsfw": { + "type": "boolean" + }, + "rtc_region": { + "type": "string" + }, + "default_auto_archive_duration": { + "type": "integer" + } + } + }, + "UserPublic": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "discriminator": { + "type": "string" + }, + "id": { + "type": "string" + }, + "public_flags": { + "type": "integer" + }, + "avatar": { + "type": "string" + }, + "accent_color": { + "type": "integer" + }, + "banner": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "bot": { + "type": "boolean" + } + }, + "required": [ + "bio", + "bot", + "discriminator", + "id", + "public_flags", + "username" + ] + }, + "PublicConnectedAccount": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "verifie": { + "type": "boolean" + } + }, + "required": [ + "name", + "type", + "verifie" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "EmojiModifySchema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "roles": { + "type": "array", + "items": { + "type": "string" + } + } }, "definitions": { "ChannelPermissionOverwriteType": { diff --git a/api/src/routes/guilds/#guild_id/emoji.ts b/api/src/routes/guilds/#guild_id/emoji.ts deleted file mode 100644 index 000c9949..00000000 --- a/api/src/routes/guilds/#guild_id/emoji.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { Router, Request, Response } from "express"; -import { - Config, - DiscordApiErrors, - emitEvent, - Emoji, - GuildEmojiUpdateEvent, - handleFile, - Member, - Snowflake, - User -} from "@fosscord/util"; -import { route } from "@fosscord/api"; - -const router = Router(); - -export interface EmojiCreateSchema { - name?: string; - image?: string; - require_colons?: boolean | null; - roles?: string[]; -} - -export interface EmojiModifySchema { - name?: string; - roles?: string[]; -} - -router.get("/", route({}), async (req: Request, res: Response) => { - const guild_id = req.params.guild_id; - - await Member.IsInGuildOrFail(req.user_id, guild_id); - - const emojis = await Emoji.find({ guild_id: guild_id }); - - return res.json(emojis); -}); - -router.get("/:emoji_id", route({}), async (req: Request, res: Response) => { - const guild_id = req.params.guild_id; - const emoji_id = req.params.emoji_id; - - await Member.IsInGuildOrFail(req.user_id, guild_id); - - const emoji = await Emoji.findOneOrFail({ guild_id: guild_id, id: emoji_id }); - - return res.json(emoji); -}); - -router.post("/", route({ body: "EmojiCreateSchema", permission: "MANAGE_EMOJIS_AND_STICKERS" }), async (req: Request, res: Response) => { - const guild_id = req.params.guild_id; - const body = req.body as EmojiCreateSchema; - - const emoji_count = await Emoji.count({ guild_id: guild_id }); - const { maxEmojis } = Config.get().limits.guild; - - if (emoji_count >= maxEmojis) throw DiscordApiErrors.MAXIMUM_NUMBER_OF_EMOJIS_REACHED.withParams(maxEmojis); - - const id = Snowflake.generate(); - - if (!body.image) { - throw DiscordApiErrors.GENERAL_ERROR.withParams("No image provided"); - } - - if (body.require_colons === null) body.require_colons = true; - - const user = await User.findOneOrFail({ id: req.user_id }); - - body.image = await handleFile(`/emojis/${id}`, body.image); - - const emoji = new Emoji({ - id: id, - guild_id: guild_id, - ...body, - user: user, - managed: false, - animated: false, // TODO: Add support animated emojis - available: true - }); - - await Promise.all([ - emoji.save(), - emitEvent({ - event: "GUILD_EMOJI_UPDATE", - guild_id: guild_id, - data: { - guild_id: guild_id, - emojis: await Emoji.find({ guild_id: guild_id }) - } - } as GuildEmojiUpdateEvent) - ]); -}); - -router.patch("/:emoji_id", route({ body: "EmojiModifySchema", permission: "MANAGE_EMOJIS_AND_STICKERS" }), async (req: Request, res: Response) => { - const { emoji_id, guild_id } = req.params; - const body = req.body as EmojiModifySchema; - - const emoji = new Emoji({ ...body, id: emoji_id, guild_id: guild_id }); - - await Promise.all([ - emoji.save(), - emitEvent({ - event: "GUILD_EMOJI_UPDATE", - guild_id: guild_id, - data: { - guild_id: guild_id, - emojis: await Emoji.find({ guild_id: guild_id }) - } - } as GuildEmojiUpdateEvent) - ]); - - return res.json(emoji); -}); - -router.delete("/:emoji_id", route({ permission: "MANAGE_EMOJIS_AND_STICKERS" }), async (req: Request, res: Response) => { - const guild_id = req.params.guild_id; - const { emoji_id } = req.params; - - await Promise.all([ - Emoji.delete({ - id: emoji_id, - guild_id: guild_id - }), - emitEvent({ - event: "GUILD_EMOJI_UPDATE", - guild_id: guild_id, - data: { - guild_id: guild_id, - emojis: await Emoji.find({ guild_id: guild_id }) - } - } as GuildEmojiUpdateEvent) - ]) - - res.sendStatus(204); -}); - -export default router; diff --git a/api/src/routes/guilds/#guild_id/emojis.ts b/api/src/routes/guilds/#guild_id/emojis.ts new file mode 100644 index 00000000..ff565cd4 --- /dev/null +++ b/api/src/routes/guilds/#guild_id/emojis.ts @@ -0,0 +1,121 @@ +import { Router, Request, Response } from "express"; +import { Config, DiscordApiErrors, emitEvent, Emoji, GuildEmojisUpdateEvent, handleFile, Member, Snowflake, User } from "@fosscord/util"; +import { route } from "@fosscord/api"; + +const router = Router(); + +export interface EmojiCreateSchema { + name?: string; + image: string; + require_colons?: boolean | null; + roles?: string[]; +} + +export interface EmojiModifySchema { + name?: string; + roles?: string[]; +} + +router.get("/", route({}), async (req: Request, res: Response) => { + const { guild_id } = req.params; + + await Member.IsInGuildOrFail(req.user_id, guild_id); + + const emojis = await Emoji.find({ where: { guild_id: guild_id }, relations: ["user"] }); + + return res.json(emojis); +}); + +router.get("/:emoji_id", route({}), async (req: Request, res: Response) => { + const { guild_id, emoji_id } = req.params; + + await Member.IsInGuildOrFail(req.user_id, guild_id); + + const emoji = await Emoji.findOneOrFail({ where: { guild_id: guild_id, id: emoji_id }, relations: ["user"] }); + + return res.json(emoji); +}); + +router.post("/", route({ body: "EmojiCreateSchema", permission: "MANAGE_EMOJIS_AND_STICKERS" }), async (req: Request, res: Response) => { + const { guild_id } = req.params; + const body = req.body as EmojiCreateSchema; + + const emoji_count = await Emoji.count({ guild_id: guild_id }); + const { maxEmojis } = Config.get().limits.guild; + + if (emoji_count >= maxEmojis) throw DiscordApiErrors.MAXIMUM_NUMBER_OF_EMOJIS_REACHED.withParams(maxEmojis); + + const id = Snowflake.generate(); + + if (body.require_colons == null) body.require_colons = true; + + const user = await User.findOneOrFail({ id: req.user_id }); + + body.image = (await handleFile(`/emojis/${id}`, body.image)) as string; + + const emoji = await new Emoji({ + id: id, + guild_id: guild_id, + ...body, + user: user, + managed: false, + animated: false, // TODO: Add support animated emojis + available: true, + roles: [] + }).save(); + + await emitEvent({ + event: "GUILD_EMOJIS_UPDATE", + guild_id: guild_id, + data: { + guild_id: guild_id, + emojis: await Emoji.find({ guild_id: guild_id }) + } + } as GuildEmojisUpdateEvent); + + return res.status(201).json(emoji); +}); + +router.patch( + "/:emoji_id", + route({ body: "EmojiModifySchema", permission: "MANAGE_EMOJIS_AND_STICKERS" }), + async (req: Request, res: Response) => { + const { emoji_id, guild_id } = req.params; + const body = req.body as EmojiModifySchema; + + const emoji = await new Emoji({ ...body, id: emoji_id, guild_id: guild_id }).save(); + + await emitEvent({ + event: "GUILD_EMOJIS_UPDATE", + guild_id: guild_id, + data: { + guild_id: guild_id, + emojis: await Emoji.find({ guild_id: guild_id }) + } + } as GuildEmojisUpdateEvent); + + return res.json(emoji); + } +); + +router.delete("/:emoji_id", route({ permission: "MANAGE_EMOJIS_AND_STICKERS" }), async (req: Request, res: Response) => { + const { emoji_id, guild_id } = req.params; + + await Emoji.delete({ + id: emoji_id, + guild_id: guild_id + }); + + await emitEvent({ + event: "GUILD_EMOJIS_UPDATE", + guild_id: guild_id, + data: { + guild_id: guild_id, + emojis: await Emoji.find({ guild_id: guild_id }) + } + } as GuildEmojisUpdateEvent); + + res.sendStatus(204); +}); + +export default router;