From 5d92525596a0193fe65626119bb040c2eb9e945a Mon Sep 17 00:00:00 2001 From: Danial Raza Date: Tue, 20 Aug 2024 11:33:25 +0200 Subject: [PATCH] feat: application emojis (#10399) * feat: application emojis * chore: requested changes --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- packages/core/src/api/applications.ts | 88 ++++++++++- packages/discord.js/src/index.js | 2 + .../src/managers/ApplicationEmojiManager.js | 142 ++++++++++++++++++ .../src/structures/ApplicationEmoji.js | 118 +++++++++++++++ .../src/structures/ClientApplication.js | 7 + packages/discord.js/src/structures/Emoji.js | 2 +- packages/discord.js/typings/index.d.ts | 37 +++++ packages/discord.js/typings/index.test-d.ts | 7 + packages/discord.js/typings/rawDataTypes.d.ts | 1 + 9 files changed, 402 insertions(+), 2 deletions(-) create mode 100644 packages/discord.js/src/managers/ApplicationEmojiManager.js create mode 100644 packages/discord.js/src/structures/ApplicationEmoji.js diff --git a/packages/core/src/api/applications.ts b/packages/core/src/api/applications.ts index 97445935d231..168d83f3886a 100644 --- a/packages/core/src/api/applications.ts +++ b/packages/core/src/api/applications.ts @@ -2,10 +2,17 @@ import type { RequestData, REST } from '@discordjs/rest'; import { + Routes, + type RESTGetAPIApplicationEmojiResult, + type RESTGetAPIApplicationEmojisResult, type RESTGetCurrentApplicationResult, + type RESTPatchAPIApplicationEmojiJSONBody, + type RESTPatchAPIApplicationEmojiResult, type RESTPatchCurrentApplicationJSONBody, type RESTPatchCurrentApplicationResult, - Routes, + type RESTPostAPIApplicationEmojiJSONBody, + type RESTPostAPIApplicationEmojiResult, + type Snowflake, } from 'discord-api-types/v10'; export class ApplicationsAPI { @@ -34,4 +41,83 @@ export class ApplicationsAPI { signal, }) as Promise; } + + /** + * Fetches all emojis of an application + * + * @see {@link https://discord.com/developers/docs/resources/emoji#list-application-emojis} + * @param applicationId - The id of the application to fetch the emojis of + * @param options - The options for fetching the emojis + */ + public async getEmojis(applicationId: Snowflake, { signal }: Pick = {}) { + return this.rest.get(Routes.applicationEmojis(applicationId), { + signal, + }) as Promise; + } + + /** + * Fetches an emoji of an application + * + * @see {@link https://discord.com/developers/docs/resources/emoji#get-application-emoji} + * @param applicationId - The id of the application to fetch the emoji of + * @param emojiId - The id of the emoji to fetch + * @param options - The options for fetching the emoji + */ + public async getEmoji(applicationId: Snowflake, emojiId: Snowflake, { signal }: Pick = {}) { + return this.rest.get(Routes.applicationEmoji(applicationId, emojiId), { + signal, + }) as Promise; + } + + /** + * Creates a new emoji of an application + * + * @see {@link https://discord.com/developers/docs/resources/emoji#create-application-emoji} + * @param applicationId - The id of the application to create the emoji of + * @param body - The data for creating the emoji + * @param options - The options for creating the emoji + */ + public async createEmoji( + applicationId: Snowflake, + body: RESTPostAPIApplicationEmojiJSONBody, + { signal }: Pick = {}, + ) { + return this.rest.post(Routes.applicationEmojis(applicationId), { + body, + signal, + }) as Promise; + } + + /** + * Edits an emoji of an application + * + * @see {@link https://discord.com/developers/docs/resources/emoji#modify-application-emoji} + * @param applicationId - The id of the application to edit the emoji of + * @param emojiId - The id of the emoji to edit + * @param body - The data for editing the emoji + * @param options - The options for editing the emoji + */ + public async editEmoji( + applicationId: Snowflake, + emojiId: Snowflake, + body: RESTPatchAPIApplicationEmojiJSONBody, + { signal }: Pick = {}, + ) { + return this.rest.patch(Routes.applicationEmoji(applicationId, emojiId), { + body, + signal, + }) as Promise; + } + + /** + * Deletes an emoji of an application + * + * @see {@link https://discord.com/developers/docs/resources/emoji#delete-application-emoji} + * @param applicationId - The id of the application to delete the emoji of + * @param emojiId - The id of the emoji to delete + * @param options - The options for deleting the emoji + */ + public async deleteEmoji(applicationId: Snowflake, emojiId: Snowflake, { signal }: Pick = {}) { + await this.rest.delete(Routes.applicationEmoji(applicationId, emojiId), { signal }); + } } diff --git a/packages/discord.js/src/index.js b/packages/discord.js/src/index.js index 5f3442cd7caa..5b30a29bcdd6 100644 --- a/packages/discord.js/src/index.js +++ b/packages/discord.js/src/index.js @@ -54,6 +54,7 @@ exports.version = require('../package.json').version; // Managers exports.ApplicationCommandManager = require('./managers/ApplicationCommandManager'); +exports.ApplicationEmojiManager = require('./managers/ApplicationEmojiManager'); exports.ApplicationCommandPermissionsManager = require('./managers/ApplicationCommandPermissionsManager'); exports.AutoModerationRuleManager = require('./managers/AutoModerationRuleManager'); exports.BaseGuildEmojiManager = require('./managers/BaseGuildEmojiManager'); @@ -98,6 +99,7 @@ exports.Activity = require('./structures/Presence').Activity; exports.AnonymousGuild = require('./structures/AnonymousGuild'); exports.Application = require('./structures/interfaces/Application'); exports.ApplicationCommand = require('./structures/ApplicationCommand'); +exports.ApplicationEmoji = require('./structures/ApplicationEmoji'); exports.ApplicationRoleConnectionMetadata = require('./structures/ApplicationRoleConnectionMetadata').ApplicationRoleConnectionMetadata; exports.AutocompleteInteraction = require('./structures/AutocompleteInteraction'); diff --git a/packages/discord.js/src/managers/ApplicationEmojiManager.js b/packages/discord.js/src/managers/ApplicationEmojiManager.js new file mode 100644 index 000000000000..22e1d7c1a7cb --- /dev/null +++ b/packages/discord.js/src/managers/ApplicationEmojiManager.js @@ -0,0 +1,142 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const { Routes } = require('discord-api-types/v10'); +const CachedManager = require('./CachedManager'); +const { DiscordjsTypeError, ErrorCodes } = require('../errors'); +const ApplicationEmoji = require('../structures/ApplicationEmoji'); +const { resolveImage } = require('../util/DataResolver'); + +/** + * Manages API methods for ApplicationEmojis and stores their cache. + * @extends {CachedManager} + */ +class ApplicationEmojiManager extends CachedManager { + constructor(application, iterable) { + super(application.client, ApplicationEmoji, iterable); + + /** + * The application this manager belongs to + * @type {ClientApplication} + */ + this.application = application; + } + + _add(data, cache) { + return super._add(data, cache, { extras: [this.application] }); + } + + /** + * Options used for creating an emoji of the application + * @typedef {Object} ApplicationEmojiCreateOptions + * @property {BufferResolvable|Base64Resolvable} attachment The image for the emoji + * @property {string} name The name for the emoji + */ + + /** + * Creates a new custom emoji of the application. + * @param {ApplicationEmojiCreateOptions} options Options for creating the emoji + * @returns {Promise} The created emoji + * @example + * // Create a new emoji from a URL + * application.emojis.create({ attachment: 'https://i.imgur.com/w3duR07.png', name: 'rip' }) + * .then(emoji => console.log(`Created new emoji with name ${emoji.name}!`)) + * .catch(console.error); + * @example + * // Create a new emoji from a file on your computer + * application.emojis.create({ attachment: './memes/banana.png', name: 'banana' }) + * .then(emoji => console.log(`Created new emoji with name ${emoji.name}!`)) + * .catch(console.error); + */ + async create({ attachment, name }) { + attachment = await resolveImage(attachment); + if (!attachment) throw new DiscordjsTypeError(ErrorCodes.ReqResourceType); + + const body = { image: attachment, name }; + + const emoji = await this.client.rest.post(Routes.applicationEmojis(this.application.id), { body }); + return this._add(emoji); + } + + /** + * Obtains one or more emojis from Discord, or the emoji cache if they're already available. + * @param {Snowflake} [id] The emoji's id + * @param {BaseFetchOptions} [options] Additional options for this fetch + * @returns {Promise>} + * @example + * // Fetch all emojis from the application + * message.application.emojis.fetch() + * .then(emojis => console.log(`There are ${emojis.size} emojis.`)) + * .catch(console.error); + * @example + * // Fetch a single emoji + * message.application.emojis.fetch('222078108977594368') + * .then(emoji => console.log(`The emoji name is: ${emoji.name}`)) + * .catch(console.error); + */ + async fetch(id, { cache = true, force = false } = {}) { + if (id) { + if (!force) { + const existing = this.cache.get(id); + if (existing) return existing; + } + const emoji = await this.client.rest.get(Routes.applicationEmoji(this.application.id, id)); + return this._add(emoji, cache); + } + + const { items: data } = await this.client.rest.get(Routes.applicationEmojis(this.application.id)); + const emojis = new Collection(); + for (const emoji of data) emojis.set(emoji.id, this._add(emoji, cache)); + return emojis; + } + + /** + * Deletes an emoji. + * @param {EmojiResolvable} emoji The Emoji resolvable to delete + * @returns {Promise} + */ + async delete(emoji) { + const id = this.resolveId(emoji); + if (!id) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'emoji', 'EmojiResolvable', true); + await this.client.rest.delete(Routes.applicationEmoji(this.application.id, id)); + } + + /** + * Edits an emoji. + * @param {EmojiResolvable} emoji The Emoji resolvable to edit + * @param {ApplicationEmojiEditOptions} options The options to provide + * @returns {Promise} + */ + async edit(emoji, options) { + const id = this.resolveId(emoji); + if (!id) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'emoji', 'EmojiResolvable', true); + + const newData = await this.client.rest.patch(Routes.applicationEmoji(this.application.id, id), { + body: { + name: options.name, + }, + }); + const existing = this.cache.get(id); + if (existing) { + existing._patch(newData); + return existing; + } + return this._add(newData); + } + + /** + * Fetches the author for this emoji + * @param {EmojiResolvable} emoji The emoji to fetch the author of + * @returns {Promise} + */ + async fetchAuthor(emoji) { + const id = this.resolveId(emoji); + if (!id) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'emoji', 'EmojiResolvable', true); + + const data = await this.client.rest.get(Routes.applicationEmoji(this.application.id, id)); + + return this._add(data).author; + } +} + +module.exports = ApplicationEmojiManager; diff --git a/packages/discord.js/src/structures/ApplicationEmoji.js b/packages/discord.js/src/structures/ApplicationEmoji.js new file mode 100644 index 000000000000..1688c6512243 --- /dev/null +++ b/packages/discord.js/src/structures/ApplicationEmoji.js @@ -0,0 +1,118 @@ +'use strict'; + +const { Emoji } = require('./Emoji'); + +/** + * Represents a custom emoji. + * @extends {Emoji} + */ +class ApplicationEmoji extends Emoji { + constructor(client, data, application) { + super(client, data); + + /** + * The application this emoji originates from + * @type {ClientApplication} + */ + this.application = application; + + /** + * The user who created this emoji + * @type {?User} + */ + this.author = null; + + this.managed = null; + this.requiresColons = null; + + this._patch(data); + } + + _patch(data) { + if ('name' in data) this.name = data.name; + if (data.user) this.author = this.client.users._add(data.user); + + if ('managed' in data) { + /** + * Whether this emoji is managed by an external service + * @type {?boolean} + */ + this.managed = data.managed; + } + + if ('require_colons' in data) { + /** + * Whether or not this emoji requires colons surrounding it + * @type {?boolean} + */ + this.requiresColons = data.require_colons; + } + } + + /** + * Fetches the author for this emoji + * @returns {Promise} + */ + fetchAuthor() { + return this.application.emojis.fetchAuthor(this); + } + + /** + * Data for editing an emoji. + * @typedef {Object} ApplicationEmojiEditOptions + * @property {string} [name] The name of the emoji + */ + + /** + * Edits the emoji. + * @param {ApplicationEmojiEditOptions} options The options to provide + * @returns {Promise} + * @example + * // Edit an emoji + * emoji.edit({ name: 'newemoji' }) + * .then(emoji => console.log(`Edited emoji ${emoji}`)) + * .catch(console.error); + */ + edit(options) { + return this.application.emojis.edit(this.id, options); + } + + /** + * Sets the name of the emoji. + * @param {string} name The new name for the emoji + * @returns {Promise} + */ + setName(name) { + return this.edit({ name }); + } + + /** + * Deletes the emoji. + * @returns {Promise} + */ + async delete() { + await this.application.emojis.delete(this.id); + return this; + } + + /** + * Whether this emoji is the same as another one. + * @param {ApplicationEmoji|APIEmoji} other The emoji to compare it to + * @returns {boolean} + */ + equals(other) { + if (other instanceof ApplicationEmoji) { + return ( + other.animated === this.animated && + other.id === this.id && + other.name === this.name && + other.managed === this.managed && + other.requiresColons === this.requiresColons + ); + } + + return other.id === this.id && other.name === this.name; + } +} + +module.exports = ApplicationEmoji; diff --git a/packages/discord.js/src/structures/ClientApplication.js b/packages/discord.js/src/structures/ClientApplication.js index 64762a1df6da..cd4271fa6416 100644 --- a/packages/discord.js/src/structures/ClientApplication.js +++ b/packages/discord.js/src/structures/ClientApplication.js @@ -7,6 +7,7 @@ const { SKU } = require('./SKU'); const Team = require('./Team'); const Application = require('./interfaces/Application'); const ApplicationCommandManager = require('../managers/ApplicationCommandManager'); +const ApplicationEmojiManager = require('../managers/ApplicationEmojiManager'); const { EntitlementManager } = require('../managers/EntitlementManager'); const ApplicationFlagsBitField = require('../util/ApplicationFlagsBitField'); const { resolveImage } = require('../util/DataResolver'); @@ -32,6 +33,12 @@ class ClientApplication extends Application { */ this.commands = new ApplicationCommandManager(this.client); + /** + * The application emoji manager for this application + * @type {ApplicationEmojiManager} + */ + this.emojis = new ApplicationEmojiManager(this); + /** * The entitlement manager for this application * @type {EntitlementManager} diff --git a/packages/discord.js/src/structures/Emoji.js b/packages/discord.js/src/structures/Emoji.js index f4c93e757c04..9451fb043b0b 100644 --- a/packages/discord.js/src/structures/Emoji.js +++ b/packages/discord.js/src/structures/Emoji.js @@ -8,7 +8,7 @@ const Base = require('./Base'); let deprecationEmittedForURL = false; /** - * Represents an emoji, see {@link GuildEmoji} and {@link ReactionEmoji}. + * Represents an emoji, see {@link ApplicationEmoji}, {@link GuildEmoji} and {@link ReactionEmoji}. * @extends {Base} */ class Emoji extends Base { diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 8a998c206526..2acb6c38df15 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -191,6 +191,7 @@ import { RawAnonymousGuildData, RawApplicationCommandData, RawApplicationData, + RawApplicationEmojiData, RawBaseGuildData, RawChannelData, RawClientApplicationData, @@ -1061,6 +1062,7 @@ export class ClientApplication extends Application { public botRequireCodeGrant: boolean | null; public bot: User | null; public commands: ApplicationCommandManager; + public emojis: ApplicationEmojiManager; public entitlements: EntitlementManager; public guildId: Snowflake | null; public get guild(): Guild | null; @@ -1342,6 +1344,41 @@ export class Emoji extends Base { public toString(): string; } +export interface ApplicationEmojiCreateOptions { + attachment: BufferResolvable | Base64Resolvable; + name: string; +} + +export interface ApplicationEmojiEditOptions { + name?: string; +} + +export class ApplicationEmoji extends Emoji { + private constructor(client: Client, data: RawApplicationEmojiData, application: ClientApplication); + + public application: ClientApplication; + public author: User | null; + public id: Snowflake; + public managed: boolean | null; + public requiresColons: boolean | null; + public delete(): Promise; + public edit(options: ApplicationEmojiEditOptions): Promise; + public equals(other: ApplicationEmoji | unknown): boolean; + public fetchAuthor(): Promise; + public setName(name: string): Promise; +} + +export class ApplicationEmojiManager extends CachedManager { + private constructor(application: ClientApplication, iterable?: Iterable); + public application: ClientApplication; + public create(options: ApplicationEmojiCreateOptions): Promise; + public fetch(id: Snowflake, options?: BaseFetchOptions): Promise; + public fetch(id?: undefined, options?: BaseFetchOptions): Promise>; + public fetchAuthor(emoji: EmojiResolvable): Promise; + public delete(emoji: EmojiResolvable): Promise; + public edit(emoji: EmojiResolvable, options: ApplicationEmojiEditOptions): Promise; +} + export class Entitlement extends Base { private constructor(client: Client, data: APIEntitlement); public id: Snowflake; diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts index 833252c9231d..a3b0e7be102f 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -206,6 +206,8 @@ import { ChannelSelectMenuComponent, MentionableSelectMenuComponent, Poll, + ApplicationEmoji, + ApplicationEmojiManager, } from '.'; import { expectAssignable, expectDeprecated, expectNotAssignable, expectNotType, expectType } from 'tsd'; import type { ContextMenuCommandBuilder, SlashCommandBuilder } from '@discordjs/builders'; @@ -1695,6 +1697,11 @@ expectType>>(guildEmojiManager.fetch() expectType>>(guildEmojiManager.fetch(undefined, {})); expectType>(guildEmojiManager.fetch('0')); +declare const applicationEmojiManager: ApplicationEmojiManager; +expectType>>(applicationEmojiManager.fetch()); +expectType>>(applicationEmojiManager.fetch(undefined, {})); +expectType>(applicationEmojiManager.fetch('0')); + declare const guildBanManager: GuildBanManager; { expectType>(guildBanManager.fetch('1234567890')); diff --git a/packages/discord.js/typings/rawDataTypes.d.ts b/packages/discord.js/typings/rawDataTypes.d.ts index bb54ad696f80..1113ee6883e6 100644 --- a/packages/discord.js/typings/rawDataTypes.d.ts +++ b/packages/discord.js/typings/rawDataTypes.d.ts @@ -102,6 +102,7 @@ export type RawEmojiData = | RawReactionEmojiData | GatewayActivityEmoji | Omit, 'animated'>; +export type RawApplicationEmojiData = APIEmoji; export type RawGuildEmojiData = APIEmoji; export type RawReactionEmojiData = APIEmoji | APIPartialEmoji;