Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/gateway-application-emoji.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@wolfstar/plugin-gateway": minor
---

Add `ApplicationEmoji` and `ApplicationEmojiManager`, discord.js's emojis owned by the application rather than a guild, reachable as `client.application.emojis`. The manager is fetch-only (Discord sends no event for these emojis, so nothing is cached): `fetch()` lists them in a `Collection` and `fetch(id)` gets one, with `create`, `edit`, `delete` and `fetchAuthor`. An `ApplicationEmoji` extends `Emoji` with `author`, `application`, `edit`, `setName`, `delete`, `fetchAuthor` and `equals`, which compares only the ID, name and `animated`, the fields the API sends.
23 changes: 21 additions & 2 deletions packages/plugin-gateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1099,8 +1099,7 @@ await client.application.editRoleConnectionMetadataRecords(records);
```

`owner` is the `Team` (with its `members`, each a `TeamMember`) when a team owns the application, the owner `User`
otherwise. `application.emojis` will be added to `ClientApplication` by its own feature, there is no
placeholder for it.
otherwise.

### Application commands

Expand Down Expand Up @@ -1157,6 +1156,26 @@ await guild.commands.permissions.add({
});
```

#### Application emojis

`client.application.emojis` is an `ApplicationEmojiManager` for the emojis the application owns, usable by the bot in
any guild. Discord sends no event for them, so they are never cached: every call goes to the API, and `fetch()` without
an ID lists them all, in a `Collection`. Unlike `GuildEmoji`, an `ApplicationEmoji` has no `roles`, `managed`,
`available` nor `requiresColons`, the API does not send them, and so `equals()` compares only the ID, name, and
`animated`.

```ts
const emojis = await client.application.emojis.fetch(); // Collection<string, ApplicationEmoji>
const emoji = await client.application.emojis.create({
attachment: "data:image/png;base64,...",
name: "howl",
});

await emoji.setName("awoo");
const author = await emoji.fetchAuthor();
await emoji.delete();
```

### Monetization

`client.application.entitlements` is an `EntitlementManager`, a cached manager like the others: `ENTITLEMENT_CREATE`,
Expand Down
138 changes: 138 additions & 0 deletions packages/plugin-gateway/src/managers/ApplicationEmojiManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { Collection } from "@discordjs/collection";
import type {
APIApplicationEmoji,
RESTPatchAPIApplicationEmojiJSONBody,
RESTPostAPIApplicationEmojiJSONBody,
} from "discord-api-types/v10";
import type { GatewayClient } from "../GatewayClient.js";
import { ApplicationEmoji } from "../structures/emojis/ApplicationEmoji.js";
import { bindClient } from "../structures/Structure.js";
import type { User } from "../structures/users/User.js";
import { resolveImage, type ImageResolvable } from "../util/DataResolver.js";
import { BaseManager } from "./BaseManager.js";

/**
* An application emoji, or its ID.
*/
export type ApplicationEmojiResolvable = ApplicationEmoji | string;

/**
* The options to create an application emoji with.
*/
export interface ApplicationEmojiCreateOptions {
/**
* The image, up to 256 KiB: a data URI (`data:image/png;base64,...`), its contents, a path, a URL, a stream, or a
* blob.
*/
attachment: ImageResolvable;
name: string;
}

/**
* The options to edit an application emoji with.
*/
export interface ApplicationEmojiEditOptions {
name?: string;
}

/**
* Manages the emojis of the application, `client.application.emojis`. Discord does not send them over the gateway, so
* they are never cached: every method calls the API.
*/
export class ApplicationEmojiManager extends BaseManager {
public constructor(client: GatewayClient) {
super(client);
}

/**
* The ID of the application the emojis belong to.
*/
public get applicationId(): string {
return this.client.application.id;
}

/**
* Fetches an emoji of the application, or every one of them.
*
* @param emojiId The ID of the emoji, omit it to fetch them all.
* @returns The emoji, or a `Collection` of every emoji by ID.
*/
public fetch(): Promise<Collection<string, ApplicationEmoji>>;
public fetch(emojiId: string): Promise<ApplicationEmoji>;
public async fetch(
emojiId?: string,
): Promise<ApplicationEmoji | Collection<string, ApplicationEmoji>> {
if (emojiId !== undefined) {
return this.hydrate(await this.client.api.applications.getEmoji(this.applicationId, emojiId));
}

const { items } = await this.client.api.applications.getEmojis(this.applicationId);
const emojis = await Promise.all(items.map((emoji) => this.hydrate(emoji)));
return new Collection(emojis.map((emoji) => [emoji.id, emoji]));
}

/**
* Uploads an emoji.
*
* @param options The image and name.
*/
public async create(options: ApplicationEmojiCreateOptions): Promise<ApplicationEmoji> {
const body: RESTPostAPIApplicationEmojiJSONBody = {
image: (await resolveImage(options.attachment))!,
name: options.name,
};
return this.hydrate(await this.client.api.applications.createEmoji(this.applicationId, body));
}

/**
* Edits an emoji.
*
* @param emoji The emoji, or its ID.
* @param options The changes to apply.
*/
public async edit(
emoji: ApplicationEmojiResolvable,
options: ApplicationEmojiEditOptions,
): Promise<ApplicationEmoji> {
const body: RESTPatchAPIApplicationEmojiJSONBody = { name: options.name };
const edited = await this.client.api.applications.editEmoji(
this.applicationId,
resolveEmojiId(emoji),
body,
);
return this.hydrate(edited);
}

/**
* Deletes an emoji.
*
* @param emoji The emoji, or its ID.
*/
public async delete(emoji: ApplicationEmojiResolvable): Promise<void> {
await this.client.api.applications.deleteEmoji(this.applicationId, resolveEmojiId(emoji));
}

/**
* Fetches the user who uploaded an emoji.
*
* @param emoji The emoji, or its ID.
*/
public async fetchAuthor(emoji: ApplicationEmojiResolvable): Promise<User> {
const fetched = await this.fetch(resolveEmojiId(emoji));
return fetched.author;
}

/**
* Builds the structure of a raw emoji, resolving its uploader from the cache.
*
* @param data The raw emoji.
*/
public async hydrate(data: APIApplicationEmoji): Promise<ApplicationEmoji> {
const author = await this.client.users._resolveData(data.user);
return bindClient(new ApplicationEmoji(data, { author }), this.client);
}
}

function resolveEmojiId(emoji: ApplicationEmojiResolvable): string {
return typeof emoji === "string" ? emoji : emoji.id;
}
1 change: 1 addition & 0 deletions packages/plugin-gateway/src/managers/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from "./ApplicationCommandManager.js";
export * from "./ApplicationCommandPermissionsManager.js";
export * from "./ApplicationEmojiManager.js";
export * from "./AutoModerationRuleManager.js";
export * from "./BaseManager.js";
export * from "./CachedManager.js";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
} from "discord-api-types/v10";
import { GatewayTypeError } from "../../errors/GatewayError.js";
import { ApplicationCommandManager } from "../../managers/ApplicationCommandManager.js";
import { ApplicationEmojiManager } from "../../managers/ApplicationEmojiManager.js";
import { EntitlementManager } from "../../managers/EntitlementManager.js";
import { SubscriptionManager } from "../../managers/SubscriptionManager.js";
import type { SKUResolvable } from "../../types.js";
Expand Down Expand Up @@ -134,6 +135,7 @@ export class ClientApplication extends Application {

// The owner is built once per payload, so that it stays the same object between reads.
#owner: { team: unknown; user: unknown; value: Team | User | null } | null = null;
#emojis: ApplicationEmojiManager | null = null;

// Built on first use: it needs the client, which binds the application after constructing it.
#entitlements: EntitlementManager | null = null;
Expand Down Expand Up @@ -297,6 +299,13 @@ export class ClientApplication extends Application {
return types ? [...types] : null;
}

/**
* The emojis of the application, fetched from the API: they are not cached.
*/
public get emojis(): ApplicationEmojiManager {
return (this.#emojis ??= new ApplicationEmojiManager(this.client));
}

/**
* The entitlements of the application: the access users and guilds were granted to its SKUs. Cached from the
* gateway, like discord.js's `ClientApplication#entitlements`.
Expand Down
115 changes: 115 additions & 0 deletions packages/plugin-gateway/src/structures/emojis/ApplicationEmoji.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import type { APIApplicationEmoji } from "discord-api-types/v10";
import type { ApplicationEmojiEditOptions } from "../../managers/ApplicationEmojiManager.js";
import type { ClientApplication } from "../applications/ClientApplication.js";
import { kData, kPatch, kRelations } from "../Structure.js";
import { User } from "../users/User.js";
import { Emoji } from "./Emoji.js";

/**
* The relations of an {@link ApplicationEmoji}, resolved from the cache by `client.application.emojis`.
*/
export interface ApplicationEmojiRelations {
author?: User;
}

/**
* A custom emoji owned by the application rather than by a guild, usable by the bot everywhere. Like discord.js's
* `ApplicationEmoji`.
*
* @remarks
* Discord sends no event for application emojis, so they are never cached: `client.application.emojis` fetches them
* from the API. The payload also carries no `managed`, `requires_colons`, `available` nor `roles`.
*/
export class ApplicationEmoji extends Emoji<APIApplicationEmoji, ApplicationEmojiRelations> {
/**
* @param data The raw emoji.
* @param relations The uploader as resolved from the cache, by `client.application.emojis`.
*/
public constructor(data: APIApplicationEmoji, relations: ApplicationEmojiRelations = {}) {
super(data, relations);
}

public override [kPatch](data: Readonly<Partial<APIApplicationEmoji>>): this {
if (data.user) this.dropRelations("author");
return super[kPatch](data);
}

public override get id(): string {
return this[kData].id;
}

public override get name(): string {
return this[kData].name;
}

/**
* The application that owns the emoji: the application of the client.
*/
public get application(): ClientApplication {
return this.client.application;
}

/**
* The user who uploaded the emoji. The API always sends it for application emojis.
*/
public get author(): User {
return this[kRelations].author ?? new User(this[kData].user);
}

/**
* Fetches the user who uploaded the emoji, from the API.
*/
public fetchAuthor(): Promise<User> {
return this.client.application.emojis.fetchAuthor(this.id);
}

/**
* Edits the emoji, and patches this structure with the result.
*
* @param options The changes to apply.
* @returns This emoji.
*/
public async edit(options: ApplicationEmojiEditOptions): Promise<this> {
const emoji = await this.client.application.emojis.edit(this.id, options);
return this[kPatch](emoji.toJSON() as APIApplicationEmoji);
}

/**
* Sets the name of the emoji.
*
* @param name The new name.
*/
public setName(name: string): Promise<this> {
return this.edit({ name });
}

/**
* Deletes the emoji.
*
* @returns This emoji.
*/
public async delete(): Promise<this> {
await this.client.application.emojis.delete(this.id);
return this;
}

/**
* Whether this emoji has the same data as another one, like discord.js's `ApplicationEmoji#equals`. Against an
* application emoji or a raw one, it compares the ID, name, and `animated`: the only fields the API sends, so unlike
* `GuildEmoji#equals` there is no `managed`, `available`, `requiresColons` nor roles to compare. `false` for
* anything else, including another kind of emoji.
*
* @param other The application emoji, or raw one, to compare with.
*/
public equals(other: unknown): boolean {
if (other instanceof ApplicationEmoji) {
return other.id === this.id && other.name === this.name && other.animated === this.animated;
}

if (typeof other !== "object" || other === null || other instanceof Emoji) return false;
const raw = other as Partial<APIApplicationEmoji>;
return (
raw.id === this.id && raw.name === this.name && (raw.animated ?? false) === this.animated
);
}
}
1 change: 1 addition & 0 deletions packages/plugin-gateway/src/structures/emojis/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./ApplicationEmoji.js";
export * from "./Emoji.js";
export * from "./GuildEmoji.js";
export * from "./ReactionEmoji.js";
Loading
Loading