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-client-application.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@wolfstar/plugin-gateway": minor
---

Add `client.application` to `@wolfstar/plugin-gateway`, a `ClientApplication` like discord.js's, with the `Application`, `Team` and `TeamMember` structures. It exists from the construction of the client with only the client ID known (`partial`), unlike discord.js where it is `null` until `READY`; `READY` patches the same instance with the application's flags, and `fetch()` (`GET /applications/@me`) with the rest. It also has `edit()`, `fetchRoleConnectionMetadataRecords()` and `editRoleConnectionMetadataRecords()`, and exposes the install params, integration types config, `owner` (a `Team` or a `User`), the event webhooks and the guild and user install counts.
28 changes: 26 additions & 2 deletions packages/plugin-gateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,8 @@ without touching the cache again.
- `client.replayDispatch({ t, d, s? }, shardId, state?)` emits `raw` and the matching event, with the same Structures
and `old` arguments the connected client emitted. It never reads or writes the cache, and never emits `dispatch`.
Dispatches of a guild replay in order, like on a connected client. `client.replayDispatchTypes` lists the types it
handles: `READY` and `INTERACTION_CREATE` are not replayed, so `client.user` stays `null` until something sets it,
handles: `READY` and `INTERACTION_CREATE` are not replayed, so `client.user` stays `null` until something sets it (and
`client.application` partial until `fetch()`),
and shard lifecycle events never fire. The promise rejects when a listener or the handler throws, which lets the
caller retry the dispatch; async listeners are awaited.

Expand Down Expand Up @@ -684,7 +685,7 @@ already ships (`User`, `Message`, `Attachment`, `Embed`, `Reaction`, `Poll`, `Em
cache, CDN URLs, and actions through the client. The ones it has no counterpart for yet (`Guild`,
`GuildMember`, `Role`, `ThreadMember`, `GuildScheduledEvent`, ...) extend its base `Structure`.
They live in one folder per domain, each with an `index.ts`, mirroring `@discordjs/structures`:
`automoderation/`, `channels/` (and `channels/mixins/`), `emojis/`, `guilds/`, `invites/`,
`applications/`, `automoderation/`, `channels/` (and `channels/mixins/`), `emojis/`, `guilds/`, `invites/`,
`messages/`, `polls/`, `presences/`, `soundboards/`, `stageInstances/`, `stickers/`, `users/`,
`voice/`, and `webhooks/`.

Expand Down Expand Up @@ -1029,6 +1030,29 @@ await webhook.editMessage(message.id, "Awoo!");
const fetched = await client.fetchWebhook(webhookId, token); // no bot authorization needed
```

### Application

`client.application` is a `ClientApplication`, the entry point for what the application owns rather than a guild.
Unlike discord.js, where it is `null` until `READY`, it is never `null`: the client builds it from `clientId`, so it
exists before `READY` and without a gateway connection (a client replaying another process's dispatches never sees
`READY`, and keeps it that way). Until then it is `partial`: only `id` is known, `READY` patches the very same
instance with the `flags`, and `fetch()` with everything else. It lives on the client, not in the cache.

```ts
client.application.partial; // true until fetch() or edit() resolves
await client.application.fetch(); // GET /applications/@me
client.application.name; // "Wolf"
client.application.owner; // a Team, a User, or null
client.application.iconURL();

await client.application.edit({ description: "Awoo", tags: ["wolf"] });
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.commands`, `application.emojis` and the entitlements will be added to `ClientApplication`
by their own features, there are no placeholders for them.

### Messages

`Message` follows discord.js: `attachments`, `embeds`, `mentions` (`MessageMentions`), `reactions`
Expand Down
14 changes: 14 additions & 0 deletions packages/plugin-gateway/src/GatewayClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import { ThreadManager } from "./managers/ThreadManager.js";
import { ThreadMemberManager } from "./managers/ThreadMemberManager.js";
import { UserManager } from "./managers/UserManager.js";
import type { BaseInvite } from "./structures/invites/BaseInvite.js";
import { ClientApplication } from "./structures/applications/ClientApplication.js";
import { ClientUser } from "./structures/users/ClientUser.js";
import { createInvite } from "./structures/invites/GroupDMInvite.js";
import { bindClient, type StructureMixin } from "./structures/Structure.js";
Expand Down Expand Up @@ -391,6 +392,18 @@ export class GatewayClient extends Client {
*/
public user: ClientUser | null = null;

/**
* The application of the bot, like discord.js's `Client#application`.
*
* @remarks
* Unlike discord.js, it is never `null`: it is built from {@link GatewayClientOptions.clientId} when the client is,
* so it exists before `READY` and without any gateway connection. It is then partial, see
* {@link ClientApplication.partial}: `READY` patches the same instance with the application's flags, and
* {@link ClientApplication.fetch} with the rest. It is kept on the client, not in the cache: it is a single small
* object, and every process gets the same `READY`.
*/
public readonly application: ClientApplication;

public readonly users: UserManager;
public readonly guilds: GuildManager;
public readonly channels: ChannelManager;
Expand Down Expand Up @@ -525,6 +538,7 @@ export class GatewayClient extends Client {
this.#intents = Number(options.intents);
// The base client validated it already, and scrubs it from `this.options`.
this.#token = (options.discordToken ?? process.env.DISCORD_TOKEN)!;
this.application = bindClient(new ClientApplication({ id: this.id }), this);
this.users = new UserManager(this);
this.guilds = new GuildManager(this);
this.channels = new ChannelManager(this);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import type { BaseImageURLOptions } from "@discordjs/rest";
import type { APIApplication } from "discord-api-types/v10";
import { cdn } from "../../util/cdn.js";
import { kData, snowflakeTimestamp, Structure } from "../Structure.js";

/**
* The raw fields of an application: its ID, and any of the others. `READY` only carries the ID and flags, so a
* partial application holds just these.
*/
export type ApplicationData = Pick<APIApplication, "id"> & Partial<Omit<APIApplication, "id">>;

/**
* The public attributes of an application, like discord.js's `Application`.
*
* @typeParam Data The raw application data this structure wraps.
*/
export class Application<Data extends ApplicationData = ApplicationData> extends Structure<Data> {
public get id(): string {
return this[kData].id;
}

/**
* The name of the application, or `null` while it is not known.
*/
public get name(): string | null {
return this[kData].name ?? null;
}

/**
* The description of the application, or `null` if it has none or is not known.
*/
public get description(): string | null {
return this[kData].description ?? null;
}

/**
* The hash of the application's icon, or `null` if it has none or is not known.
*/
public get icon(): string | null {
return this[kData].icon ?? null;
}

/**
* The hash of the application's cover image, or `null` if it has none or is not known.
*/
public get cover(): string | null {
return this[kData].cover_image ?? null;
}

public get createdTimestamp(): number {
return snowflakeTimestamp(this.id);
}

public get createdAt(): Date {
return new Date(this.createdTimestamp);
}

/**
* Gets the URL of the application's icon, or `null` if it has none.
*
* @param options The image options.
*/
public iconURL(options?: BaseImageURLOptions): string | null {
const { icon } = this[kData];
return icon ? cdn.appIcon(this.id, icon, options) : null;
}

/**
* Gets the URL of the application's cover image, or `null` if it has none.
*
* @param options The image options.
*/
public coverURL(options?: BaseImageURLOptions): string | null {
const { cover_image: cover } = this[kData];
return cover ? cdn.appIcon(this.id, cover, options) : null;
}

/**
* The name of the application, its ID while the name is not known.
*/
public toString(): string {
return this.name ?? this.id;
}
}
Loading
Loading