diff --git a/packages/core/src/api/applications.ts b/packages/core/src/api/applications.ts index 97445935d231c..10cf4d9b39a94 100644 --- a/packages/core/src/api/applications.ts +++ b/packages/core/src/api/applications.ts @@ -2,15 +2,101 @@ 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 { public constructor(private readonly rest: REST) {} + /** + * 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 }); + } + /** * Fetches the application associated with the requesting bot user. * diff --git a/packages/discord.js/src/managers/ApplicationEmojiManager.js b/packages/discord.js/src/managers/ApplicationEmojiManager.js new file mode 100644 index 0000000000000..2cec959b7d0a6 --- /dev/null +++ b/packages/discord.js/src/managers/ApplicationEmojiManager.js @@ -0,0 +1,141 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const { Routes } = require('discord-api-types/v10'); +const BaseApplicationEmojiManager = require('./BaseApplicationEmojiManager'); +const { DiscordjsTypeError, ErrorCodes } = require('../errors'); +const { resolveImage } = require('../util/DataResolver'); + +/** + * Manages API methods for ApplicationEmojis and stores their cache. + * @extends {BaseApplicationEmojiManager} + */ +class ApplicationEmojiManager extends BaseApplicationEmojiManager { + constructor(application, iterable) { + super(application.client, 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 an 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 in 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) { + const clone = existing._clone(); + clone._patch(newData); + return clone; + } + 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 emojiId = typeof emoji === 'string' ? emoji : emoji.id; + + const data = await this.client.rest.get(Routes.applicationEmoji(this.application.id, emojiId)); + + return this._add(data).author; + } +} + +module.exports = ApplicationEmojiManager; diff --git a/packages/discord.js/src/managers/BaseApplicationEmojiManager.js b/packages/discord.js/src/managers/BaseApplicationEmojiManager.js new file mode 100644 index 0000000000000..cde9d845dd313 --- /dev/null +++ b/packages/discord.js/src/managers/BaseApplicationEmojiManager.js @@ -0,0 +1,80 @@ +'use strict'; + +const CachedManager = require('./CachedManager'); +const ApplicationEmoji = require('../structures/ApplicationEmoji'); +const ReactionEmoji = require('../structures/ReactionEmoji'); +const { parseEmoji } = require('../util/Util'); + +/** + * Holds methods to resolve ApplicationEmojis and stores their cache. + * @extends {CachedManager} + */ +class BaseApplicationEmojiManager extends CachedManager { + constructor(client, iterable) { + super(client, ApplicationEmoji, iterable); + } + + /** + * The cache of ApplicationEmojis + * @type {Collection} + * @name BaseApplicationEmojiManager#cache + */ + + /** + * Data that can be resolved into a ApplicationEmoji object. This can be: + * * A Snowflake + * * An ApplicationEmoji object + * * A ReactionEmoji object + * @typedef {Snowflake|ApplicationEmoji|ReactionEmoji} EmojiResolvable + */ + + /** + * Resolves an EmojiResolvable to an Emoji object. + * @param {EmojiResolvable} emoji The Emoji resolvable to identify + * @returns {?ApplicationEmoji} + */ + resolve(emoji) { + if (emoji instanceof ReactionEmoji) return super.resolve(emoji.id); + return super.resolve(emoji); + } + + /** + * Resolves an EmojiResolvable to an Emoji id string. + * @param {EmojiResolvable} emoji The Emoji resolvable to identify + * @returns {?Snowflake} + */ + resolveId(emoji) { + if (emoji instanceof ReactionEmoji) return emoji.id; + return super.resolveId(emoji); + } + + /** + * Data that can be resolved to give an emoji identifier. This can be: + * * An EmojiResolvable + * * The ``, `<:name:id>`, `a:name:id` or `name:id` emoji identifier string of an emoji + * * The Unicode representation of an emoji + * @typedef {string|EmojiResolvable} EmojiIdentifierResolvable + */ + + /** + * Resolves an EmojiResolvable to an emoji identifier. + * @param {EmojiIdentifierResolvable} emoji The emoji resolvable to resolve + * @returns {?string} + */ + resolveIdentifier(emoji) { + const emojiResolvable = this.resolve(emoji); + if (emojiResolvable) return emojiResolvable.identifier; + if (emoji instanceof ReactionEmoji) return emoji.identifier; + if (typeof emoji === 'string') { + const res = parseEmoji(emoji); + if (res?.name.length) { + emoji = `${res.animated ? 'a:' : ''}${res.name}${res.id ? `:${res.id}` : ''}`; + } + if (!emoji.includes('%')) return encodeURIComponent(emoji); + return emoji; + } + return null; + } +} + +module.exports = BaseApplicationEmojiManager; diff --git a/packages/discord.js/src/structures/ApplicationEmoji.js b/packages/discord.js/src/structures/ApplicationEmoji.js new file mode 100644 index 0000000000000..203ea8355f680 --- /dev/null +++ b/packages/discord.js/src/structures/ApplicationEmoji.js @@ -0,0 +1,105 @@ +'use strict'; + +const BaseApplicationEmoji = require('./BaseApplicationEmoji'); + +/** + * Represents a custom emoji. + * @extends {BaseApplicationEmoji} + */ +class ApplicationEmoji extends BaseApplicationEmoji { + constructor(client, data, application) { + super(client, data, application); + + /** + * The user who created this emoji + * @type {?User} + */ + this.author = null; + + this._patch(data); + } + + /** + * The application this emoji originates from + * @type {ClientApplication} + * @name ApplicationEmoji#application + */ + + _clone() { + const clone = super._clone(); + return clone; + } + + _patch(data) { + super._patch(data); + + if (data.user) this.author = this.client.users._add(data.user); + } + + /** + * 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.id === this.id && + other.name === this.name && + other.managed === this.managed && + other.available === this.available && + other.requiresColons === this.requiresColons + ); + } + + return other.id === this.id && other.name === this.name; + } +} + +module.exports = ApplicationEmoji; diff --git a/packages/discord.js/src/structures/BaseApplicationEmoji.js b/packages/discord.js/src/structures/BaseApplicationEmoji.js new file mode 100644 index 0000000000000..30bd34cd3ee8e --- /dev/null +++ b/packages/discord.js/src/structures/BaseApplicationEmoji.js @@ -0,0 +1,66 @@ +'use strict'; + +const { Emoji } = require('./Emoji'); + +/** + * Parent class for {@link ApplicationEmoji}. + * @extends {Emoji} + * @abstract + */ +class BaseApplicationEmoji extends Emoji { + constructor(client, data, application) { + super(client, data); + + /** + * The application this emoji originates from + * @type {ClientApplication} + */ + this.application = application; + + this.requiresColons = null; + this.managed = null; + + this._patch(data); + } + + _patch(data) { + if ('name' in data) this.name = data.name; + + if ('require_colons' in data) { + /** + * Whether or not this emoji requires colons surrounding it + * @type {?boolean} + */ + this.requiresColons = data.require_colons; + } + + if ('managed' in data) { + /** + * Whether this emoji is managed by an external service + * @type {?boolean} + */ + this.managed = data.managed; + } + } +} + +/** + * Returns a URL for the emoji. + * @method imageURL + * @memberof BaseApplicationEmoji + * @instance + * @param {BaseImageURLOptions} [options] Options for the image URL + * @returns {string} + */ + +/** + * Returns a URL for the emoji. + * @name url + * @memberof BaseApplicationEmoji + * @instance + * @type {string} + * @readonly + * @deprecated Use {@link BaseApplicationEmoji#imageURL} instead. + */ + +module.exports = BaseApplicationEmoji; diff --git a/packages/discord.js/src/structures/ClientApplication.js b/packages/discord.js/src/structures/ClientApplication.js index 64762a1df6dac..cd4271fa64162 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 f4c93e757c04c..9451fb043b0b3 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 b1071e1d35588..175312dd546f3 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,55 @@ export class Emoji extends Base { public toString(): string; } +export class BaseApplicationEmoji extends Emoji { + protected constructor(client: Client, data: RawApplicationEmojiData, application: ClientApplication); + public imageURL(options?: BaseImageURLOptions): string; + public get url(): string; + public get createdAt(): Date; + public get createdTimestamp(): number; + public application: ClientApplication; + public id: Snowflake; + public managed: boolean | null; + public requiresColons: boolean | null; +} + +export interface ApplicationEmojiCreateOptions { + attachment: BufferResolvable | Base64Resolvable; + name: string; +} + +export interface ApplicationEmojiEditOptions { + name?: string; +} + +export class ApplicationEmoji extends BaseApplicationEmoji { + private constructor(client: Client, data: RawApplicationEmojiData, application: ClientApplication); + + public application: ClientApplication; + public author: User | null; + public delete(reason?: string): Promise; + public edit(options: ApplicationEmojiEditOptions): Promise; + public equals(other: ApplicationEmoji | unknown): boolean; + public fetchAuthor(): Promise; + public setName(name: string): Promise; +} + +export class BaseApplicationEmojiManager extends CachedManager { + protected constructor(client: Client, iterable?: Iterable); + public resolveIdentifier(emoji: EmojiIdentifierResolvable): string | null; +} + +export class ApplicationEmojiManager extends BaseApplicationEmojiManager { + 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, reason?: string): 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 407f6023e8d26..e43dbf314f5db 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -205,6 +205,8 @@ import { ChannelSelectMenuComponent, MentionableSelectMenuComponent, Poll, + ApplicationEmoji, + ApplicationEmojiManager, } from '.'; import { expectAssignable, expectDeprecated, expectNotAssignable, expectNotType, expectType } from 'tsd'; import type { ContextMenuCommandBuilder, SlashCommandBuilder } from '@discordjs/builders'; @@ -1694,6 +1696,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 bb54ad696f80e..1113ee6883e62 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;