diff --git a/src/Client.ts b/src/Client.ts index a1a3602d..86faa1e7 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -101,6 +101,7 @@ export type Events = { // ^ userRelationshipChanged: [user: User, previousRelationship: RelationshipStatus]; // ^ userPresenceChanged: [user: User, previousPresence: boolean]; userSettingsUpdate: [id: string, update: Record]; + entitlementsUpdate: [entitlements: ProtocolV1["types"]["entitlement"][]]; emojiCreate: [emoji: Emoji]; emojiDelete: [emoji: HydratedEmoji]; @@ -206,6 +207,14 @@ export class Client extends AsyncEventEmitter { readonly connectionFailureCount: Accessor; #setConnectionFailureCount: Setter; + + /** + * Entitlements the current user holds, as last sent by the server. + * Note that passing an expiration date will NOT remove its data, + * so you may need to keep track of expiration yourself. + */ + readonly entitlements: Accessor; + #setEntitlements: Setter; #reconnectTimeout: number | undefined; /** @@ -270,6 +279,12 @@ export class Client extends AsyncEventEmitter { this.connectionFailureCount = connectionFailureCount; this.#setConnectionFailureCount = setConnectionFailureCount; + const [entitlements, setEntitlements] = createSignal< + ProtocolV1["types"]["entitlement"][] + >([]); + this.entitlements = entitlements; + this.#setEntitlements = setEntitlements; + this.account = new AccountCollection(this); this.bots = new BotCollection(this); this.channels = new ChannelCollection(this); @@ -428,6 +443,15 @@ export class Client extends AsyncEventEmitter { this.events.removeAllListeners(); this.removeAllListeners(); this.events.disconnect(); + this.#setEntitlements([]); + } + + /** + * Replace the current user's entitlements, from the events socket + * @internal + */ + setEntitlements(entitlements: ProtocolV1["types"]["entitlement"][]): void { + this.#setEntitlements(entitlements); } /** diff --git a/src/events/v1.ts b/src/events/v1.ts index 1c4b9459..ca17fefc 100644 --- a/src/events/v1.ts +++ b/src/events/v1.ts @@ -35,6 +35,7 @@ export type ProtocolV1 = { types: { policyChange: PolicyChange; + entitlement: Entitlement; }; }; @@ -161,6 +162,7 @@ type ServerMessage = update: { [key: string]: [number, string] }; } | { type: "UserPlatformWipe"; user_id: string; flags: number } + | { type: "EntitlementsUpdate"; entitlements: Entitlement[] } | ({ type: "EmojiCreate" } & Emoji) | { type: "EmojiDelete"; id: string } | ({ @@ -210,6 +212,22 @@ type ServerMessage = slowmodes: UserSlowmodes[]; }; +/** + * Entitlement the user holds + */ +type Entitlement = { + /** Entitlement key, a ULID */ + key: string; + /** When it stops granting access; may be in the past */ + expires_at: string; + /** Entitlement data revision. Backend implementation detail. Only ever increases */ + revision: number; + /** Issuer that last set it. Opaque string */ + source: string; + /** Start of the holder's current streak of access to this entitlement */ + since: string; +}; + /** * Policy change type */ @@ -265,6 +283,7 @@ type ReadyData = { channel_unreads: ChannelUnread[]; policy_changes: PolicyChange[]; + entitlements: Entitlement[]; }; /** @@ -339,8 +358,12 @@ export async function handleEvent( client.emojis.getOrCreate(emoji._id, emoji); } } + + client.setEntitlements(event.entitlements ?? []); }); + client.emit("entitlementsUpdate", client.entitlements()); + if (client.options.syncUnreads) { await client.channelUnreads.sync(); } @@ -958,6 +981,11 @@ export async function handleEvent( ); break; } + case "EntitlementsUpdate": { + client.setEntitlements(event.entitlements); + client.emit("entitlementsUpdate", event.entitlements); + break; + } case "UserSettingsUpdate": { client.emit("userSettingsUpdate", event.id, event.update); break;