agent-messenger 2.32.3 → 2.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +12 -1
  3. package/dist/package.json +1 -1
  4. package/dist/src/platforms/discord/client.d.ts +18 -1
  5. package/dist/src/platforms/discord/client.d.ts.map +1 -1
  6. package/dist/src/platforms/discord/client.js +56 -3
  7. package/dist/src/platforms/discord/client.js.map +1 -1
  8. package/dist/src/platforms/discord/commands/dm.d.ts +4 -0
  9. package/dist/src/platforms/discord/commands/dm.d.ts.map +1 -1
  10. package/dist/src/platforms/discord/commands/dm.js +56 -4
  11. package/dist/src/platforms/discord/commands/dm.js.map +1 -1
  12. package/dist/src/platforms/discord/commands/member.d.ts +3 -0
  13. package/dist/src/platforms/discord/commands/member.d.ts.map +1 -1
  14. package/dist/src/platforms/discord/commands/member.js +72 -1
  15. package/dist/src/platforms/discord/commands/member.js.map +1 -1
  16. package/dist/src/platforms/discord/index.d.ts +1 -1
  17. package/dist/src/platforms/discord/index.d.ts.map +1 -1
  18. package/dist/src/platforms/discord/index.js.map +1 -1
  19. package/dist/src/platforms/discord/listener.d.ts +4 -2
  20. package/dist/src/platforms/discord/listener.d.ts.map +1 -1
  21. package/dist/src/platforms/discord/listener.js +9 -2
  22. package/dist/src/platforms/discord/listener.js.map +1 -1
  23. package/dist/src/platforms/discord/types.d.ts +48 -0
  24. package/dist/src/platforms/discord/types.d.ts.map +1 -1
  25. package/dist/src/platforms/discord/types.js +5 -1
  26. package/dist/src/platforms/discord/types.js.map +1 -1
  27. package/docs/content/docs/cli/discord.mdx +25 -0
  28. package/docs/content/docs/sdk/discord.mdx +21 -0
  29. package/package.json +1 -1
  30. package/skills/agent-channeltalk/SKILL.md +1 -1
  31. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  32. package/skills/agent-discord/SKILL.md +31 -1
  33. package/skills/agent-discordbot/SKILL.md +1 -1
  34. package/skills/agent-imessage/SKILL.md +1 -1
  35. package/skills/agent-instagram/SKILL.md +1 -1
  36. package/skills/agent-kakaotalk/SKILL.md +1 -1
  37. package/skills/agent-line/SKILL.md +1 -1
  38. package/skills/agent-slack/SKILL.md +1 -1
  39. package/skills/agent-slackbot/SKILL.md +1 -1
  40. package/skills/agent-teams/SKILL.md +1 -1
  41. package/skills/agent-telegram/SKILL.md +1 -1
  42. package/skills/agent-telegrambot/SKILL.md +1 -1
  43. package/skills/agent-webex/SKILL.md +1 -1
  44. package/skills/agent-webexbot/SKILL.md +1 -1
  45. package/skills/agent-wechatbot/SKILL.md +1 -1
  46. package/skills/agent-whatsapp/SKILL.md +1 -1
  47. package/skills/agent-whatsappbot/SKILL.md +1 -1
  48. package/src/platforms/discord/client.test.ts +204 -1
  49. package/src/platforms/discord/client.ts +85 -3
  50. package/src/platforms/discord/commands/dm.test.ts +126 -1
  51. package/src/platforms/discord/commands/dm.ts +63 -3
  52. package/src/platforms/discord/commands/member.test.ts +177 -0
  53. package/src/platforms/discord/commands/member.ts +82 -0
  54. package/src/platforms/discord/index.ts +6 -0
  55. package/src/platforms/discord/listener.test.ts +12 -3
  56. package/src/platforms/discord/listener.ts +17 -4
  57. package/src/platforms/discord/types.ts +69 -1
@@ -2,11 +2,52 @@ import { afterEach, beforeEach, expect, spyOn, it } from 'bun:test'
2
2
 
3
3
  import { DiscordClient } from '../client'
4
4
  import { DiscordCredentialManager } from '../credential-manager'
5
+ import { meAction } from './member'
5
6
 
7
+ let clientGetMyGuildMemberSpy: ReturnType<typeof spyOn>
8
+ let clientListRolesSpy: ReturnType<typeof spyOn>
6
9
  let clientSearchMembersSpy: ReturnType<typeof spyOn>
10
+ let consoleLogSpy: ReturnType<typeof spyOn>
7
11
  let credManagerLoadSpy: ReturnType<typeof spyOn>
8
12
 
9
13
  beforeEach(() => {
14
+ clientGetMyGuildMemberSpy = spyOn(DiscordClient.prototype, 'getMyGuildMember').mockResolvedValue({
15
+ user: {
16
+ id: 'user-1',
17
+ username: 'alice',
18
+ global_name: 'Alice Smith',
19
+ avatar: 'avatar-hash-1',
20
+ bot: false,
21
+ },
22
+ nick: 'AliceNick',
23
+ roles: ['role-low', 'role-missing', 'role-high'],
24
+ joined_at: '2024-01-15T10:00:00Z',
25
+ deaf: false,
26
+ mute: false,
27
+ flags: 0,
28
+ })
29
+ clientListRolesSpy = spyOn(DiscordClient.prototype, 'listRoles').mockResolvedValue([
30
+ {
31
+ id: 'role-low',
32
+ name: 'Member',
33
+ color: 0,
34
+ hoist: false,
35
+ position: 1,
36
+ permissions: '1024',
37
+ managed: false,
38
+ mentionable: false,
39
+ },
40
+ {
41
+ id: 'role-high',
42
+ name: 'Admin',
43
+ color: 16711680,
44
+ hoist: true,
45
+ position: 10,
46
+ permissions: '8',
47
+ managed: false,
48
+ mentionable: true,
49
+ },
50
+ ])
10
51
  clientSearchMembersSpy = spyOn(DiscordClient.prototype, 'searchMembers').mockResolvedValue([
11
52
  {
12
53
  user: {
@@ -47,10 +88,14 @@ beforeEach(() => {
47
88
  'guild-1': { server_id: 'guild-1', server_name: 'Test Guild' },
48
89
  },
49
90
  })
91
+ consoleLogSpy = spyOn(console, 'log').mockImplementation(() => {})
50
92
  })
51
93
 
52
94
  afterEach(() => {
95
+ clientGetMyGuildMemberSpy?.mockRestore()
96
+ clientListRolesSpy?.mockRestore()
53
97
  clientSearchMembersSpy?.mockRestore()
98
+ consoleLogSpy?.mockRestore()
54
99
  credManagerLoadSpy?.mockRestore()
55
100
  })
56
101
 
@@ -97,3 +142,135 @@ it('search: uses default limit of 10', async () => {
97
142
 
98
143
  expect(members).toHaveLength(2)
99
144
  })
145
+
146
+ it("me: returns the current user's member object", async () => {
147
+ await meAction(undefined, {})
148
+
149
+ const output = JSON.parse(consoleLogSpy.mock.calls.at(-1)?.[0] as string)
150
+ expect(output).toMatchObject({
151
+ user: {
152
+ id: 'user-1',
153
+ username: 'alice',
154
+ global_name: 'Alice Smith',
155
+ avatar: 'avatar-hash-1',
156
+ bot: false,
157
+ },
158
+ nick: 'AliceNick',
159
+ joined_at: '2024-01-15T10:00:00Z',
160
+ deaf: false,
161
+ mute: false,
162
+ flags: 0,
163
+ })
164
+ expect(clientGetMyGuildMemberSpy).toHaveBeenCalledWith('guild-1')
165
+ expect(clientListRolesSpy).toHaveBeenCalledWith('guild-1')
166
+ })
167
+
168
+ it('me: returns roles resolved to names (not raw IDs)', async () => {
169
+ await meAction('guild-2', {})
170
+
171
+ const output = JSON.parse(consoleLogSpy.mock.calls.at(-1)?.[0] as string)
172
+ expect(output.roles.map((role: { name: string }) => role.name)).toEqual(['Admin', 'Member'])
173
+ expect(output.roles).not.toContain('role-high')
174
+ expect(clientGetMyGuildMemberSpy).toHaveBeenCalledWith('guild-2')
175
+ })
176
+
177
+ it('me: roles are sorted by position descending', async () => {
178
+ await meAction(undefined, {})
179
+
180
+ const output = JSON.parse(consoleLogSpy.mock.calls.at(-1)?.[0] as string)
181
+ expect(output.roles.map((role: { position: number }) => role.position)).toEqual([10, 1])
182
+ })
183
+
184
+ it('me: role IDs with no matching role definition are dropped', async () => {
185
+ await meAction(undefined, {})
186
+
187
+ const output = JSON.parse(consoleLogSpy.mock.calls.at(-1)?.[0] as string)
188
+ expect(output.roles).toHaveLength(2)
189
+ expect(output.roles.map((role: { id: string }) => role.id)).not.toContain('role-missing')
190
+ })
191
+
192
+ it('me: breaks equal-position ties by snowflake, ranking the smaller ID higher', async () => {
193
+ // given two roles sharing a position whose IDs collide once coerced to Number
194
+ clientGetMyGuildMemberSpy.mockResolvedValue({
195
+ user: { id: 'user-1', username: 'alice', global_name: 'Alice Smith', avatar: 'avatar-hash-1', bot: false },
196
+ nick: null,
197
+ roles: ['1234567890123456789', '1234567890123456788'],
198
+ joined_at: '2024-01-15T10:00:00Z',
199
+ deaf: false,
200
+ mute: false,
201
+ flags: 0,
202
+ })
203
+ clientListRolesSpy.mockResolvedValue([
204
+ {
205
+ id: '1234567890123456789',
206
+ name: 'Newer',
207
+ color: 0,
208
+ position: 5,
209
+ hoist: false,
210
+ managed: false,
211
+ mentionable: false,
212
+ permissions: '0',
213
+ },
214
+ {
215
+ id: '1234567890123456788',
216
+ name: 'Older',
217
+ color: 0,
218
+ position: 5,
219
+ hoist: false,
220
+ managed: false,
221
+ mentionable: false,
222
+ permissions: '0',
223
+ },
224
+ ])
225
+
226
+ await meAction(undefined, {})
227
+
228
+ const output = JSON.parse(consoleLogSpy.mock.calls.at(-1)?.[0] as string)
229
+ expect(output.roles.map((role: { name: string }) => role.name)).toEqual(['Older', 'Newer'])
230
+ })
231
+
232
+ it('me: ranks by position before falling back to the snowflake', async () => {
233
+ clientGetMyGuildMemberSpy.mockResolvedValue({
234
+ user: { id: 'user-1', username: 'alice', global_name: 'Alice Smith', avatar: 'avatar-hash-1', bot: false },
235
+ nick: null,
236
+ roles: ['100', '200'],
237
+ joined_at: '2024-01-15T10:00:00Z',
238
+ deaf: false,
239
+ mute: false,
240
+ flags: 0,
241
+ })
242
+ clientListRolesSpy.mockResolvedValue([
243
+ {
244
+ id: '100',
245
+ name: 'Low',
246
+ color: 0,
247
+ position: 1,
248
+ hoist: false,
249
+ managed: false,
250
+ mentionable: false,
251
+ permissions: '0',
252
+ },
253
+ {
254
+ id: '200',
255
+ name: 'High',
256
+ color: 0,
257
+ position: 9,
258
+ hoist: false,
259
+ managed: false,
260
+ mentionable: false,
261
+ permissions: '0',
262
+ },
263
+ ])
264
+
265
+ await meAction(undefined, {})
266
+
267
+ const output = JSON.parse(consoleLogSpy.mock.calls.at(-1)?.[0] as string)
268
+ expect(output.roles.map((role: { name: string }) => role.name)).toEqual(['High', 'Low'])
269
+ })
270
+
271
+ it('me: preserves the raw role IDs even when a definition is unresolvable', async () => {
272
+ await meAction(undefined, {})
273
+
274
+ const output = JSON.parse(consoleLogSpy.mock.calls.at(-1)?.[0] as string)
275
+ expect(output.role_ids).toEqual(['role-low', 'role-missing', 'role-high'])
276
+ })
@@ -5,6 +5,7 @@ import { formatOutput } from '@/shared/utils/output'
5
5
 
6
6
  import { DiscordClient } from '../client'
7
7
  import { DiscordCredentialManager } from '../credential-manager'
8
+ import type { DiscordRole } from '../types'
8
9
 
9
10
  async function searchAction(
10
11
  guildId: string,
@@ -46,6 +47,80 @@ async function searchAction(
46
47
  }
47
48
  }
48
49
 
50
+ export async function meAction(guildId: string | undefined, options: { pretty?: boolean }): Promise<void> {
51
+ try {
52
+ const credManager = new DiscordCredentialManager()
53
+ const config = await credManager.load()
54
+
55
+ if (!config.token) {
56
+ console.log(formatOutput({ error: 'Not authenticated. Run "auth extract" first.' }, options.pretty))
57
+ process.exit(1)
58
+ }
59
+
60
+ const targetGuildId = guildId ?? config.current_server
61
+ if (!targetGuildId) {
62
+ console.log(formatOutput({ error: 'No current server set. Run "server switch <id>" first.' }, options.pretty))
63
+ process.exit(1)
64
+ }
65
+
66
+ const client = await new DiscordClient().login({ token: config.token })
67
+ const [member, roles] = await Promise.all([client.getMyGuildMember(targetGuildId), client.listRoles(targetGuildId)])
68
+ const rolesById = new Map<string, DiscordRole>(roles.map((role) => [role.id, role]))
69
+ const resolvedRoles = member.roles
70
+ .map((roleId) => rolesById.get(roleId))
71
+ .filter((role): role is DiscordRole => role !== undefined)
72
+ .sort(compareRolesByHierarchy)
73
+ .map((role) => ({
74
+ id: role.id,
75
+ name: role.name,
76
+ color: role.color,
77
+ position: role.position,
78
+ hoist: role.hoist,
79
+ managed: role.managed,
80
+ mentionable: role.mentionable,
81
+ permissions: role.permissions,
82
+ }))
83
+
84
+ const output = {
85
+ user: {
86
+ id: member.user.id,
87
+ username: member.user.username,
88
+ global_name: member.user.global_name,
89
+ avatar: member.user.avatar,
90
+ bot: member.user.bot,
91
+ },
92
+ nick: member.nick,
93
+ role_ids: member.roles,
94
+ roles: resolvedRoles,
95
+ joined_at: member.joined_at,
96
+ deaf: member.deaf,
97
+ mute: member.mute,
98
+ flags: member.flags,
99
+ }
100
+
101
+ console.log(formatOutput(output, options.pretty))
102
+ } catch (error) {
103
+ handleError(error as Error)
104
+ }
105
+ }
106
+
107
+ // Discord ranks roles by descending position and breaks ties on the snowflake,
108
+ // where the smaller ID is the higher role. Snowflakes exceed Number.MAX_SAFE_INTEGER,
109
+ // so they are compared as BigInt rather than subtracted.
110
+ function compareRolesByHierarchy(left: DiscordRole, right: DiscordRole): number {
111
+ if (left.position !== right.position) {
112
+ return right.position - left.position
113
+ }
114
+
115
+ const leftId = BigInt(left.id)
116
+ const rightId = BigInt(right.id)
117
+ if (leftId === rightId) {
118
+ return 0
119
+ }
120
+
121
+ return leftId < rightId ? -1 : 1
122
+ }
123
+
49
124
  export const memberCommand = new Command('member')
50
125
  .description('Member commands')
51
126
  .addCommand(
@@ -57,3 +132,10 @@ export const memberCommand = new Command('member')
57
132
  .option('--pretty', 'Pretty print JSON output')
58
133
  .action(searchAction),
59
134
  )
135
+ .addCommand(
136
+ new Command('me')
137
+ .description('Get your own membership and roles in a guild')
138
+ .argument('[guild-id]', 'Guild ID (defaults to current server)')
139
+ .option('--pretty', 'Pretty print JSON output')
140
+ .action(meAction),
141
+ )
@@ -26,10 +26,16 @@ export type {
26
26
  DiscordMessage,
27
27
  DiscordReaction,
28
28
  DiscordReadState,
29
+ DiscordReadStateSnapshot,
29
30
  DiscordRelationship,
31
+ DiscordRole,
32
+ DiscordRoleColors,
33
+ DiscordRoleTags,
30
34
  DiscordSearchOptions,
31
35
  DiscordSearchResponse,
32
36
  DiscordSearchResult,
37
+ DiscordUnreadDM,
38
+ DiscordUnreadDMsResult,
33
39
  DiscordUnreadMention,
34
40
  DiscordUnreadMentionsResult,
35
41
  DiscordUser,
@@ -143,19 +143,28 @@ describe('parseReadStates', () => {
143
143
  { id: 'c2', read_state_type: 2, mention_count: 5 },
144
144
  ])
145
145
 
146
- expect(result).toEqual([{ channelId: 'c1', lastMessageId: '100', mentionCount: 1 }])
146
+ expect(result.states).toEqual([{ channelId: 'c1', lastMessageId: '100', mentionCount: 1 }])
147
147
  })
148
148
 
149
149
  it('normalizes missing last_message_id and mention_count', () => {
150
150
  const result = parseReadStates([{ id: 'c1' }])
151
151
 
152
- expect(result).toEqual([{ channelId: 'c1', lastMessageId: null, mentionCount: 0 }])
152
+ expect(result.states).toEqual([{ channelId: 'c1', lastMessageId: null, mentionCount: 0 }])
153
153
  })
154
154
 
155
155
  it('skips malformed entries but keeps valid ones', () => {
156
156
  const result = parseReadStates([{ id: 'c1', mention_count: 1 }, { mention_count: 3 }])
157
157
 
158
- expect(result).toEqual([{ channelId: 'c1', lastMessageId: null, mentionCount: 1 }])
158
+ expect(result.states).toEqual([{ channelId: 'c1', lastMessageId: null, mentionCount: 1 }])
159
+ })
160
+
161
+ it('treats a bare array payload as complete', () => {
162
+ expect(parseReadStates([{ id: 'c1' }]).partial).toBe(false)
163
+ })
164
+
165
+ it('reads the partial flag from a versioned payload', () => {
166
+ expect(parseReadStates({ entries: [{ id: 'c1' }], version: 7, partial: true }).partial).toBe(true)
167
+ expect(parseReadStates({ entries: [{ id: 'c1' }], version: 7 }).partial).toBe(false)
159
168
  })
160
169
 
161
170
  it('throws on a non-array, non-versioned payload', () => {
@@ -3,16 +3,22 @@ import { EventEmitter } from 'events'
3
3
  import WebSocket from 'ws'
4
4
 
5
5
  import type { DiscordClient } from './client'
6
- import type { DiscordListenerEventMap, DiscordGatewayGenericEvent, DiscordReadState } from './types'
6
+ import type {
7
+ DiscordListenerEventMap,
8
+ DiscordGatewayGenericEvent,
9
+ DiscordReadState,
10
+ DiscordReadStateSnapshot,
11
+ } from './types'
7
12
  import { DiscordGatewayOpcode, DiscordRawReadStateSchema, DiscordReadyReadStateSchema } from './types'
8
13
 
9
- export function parseReadStates(value: unknown): DiscordReadState[] {
14
+ export function parseReadStates(value: unknown): DiscordReadStateSnapshot {
10
15
  const parsed = DiscordReadyReadStateSchema.safeParse(value)
11
16
  if (!parsed.success) {
12
17
  throw new Error('Discord READY contained an invalid read_state payload')
13
18
  }
14
19
 
15
20
  const entries = Array.isArray(parsed.data) ? parsed.data : parsed.data.entries
21
+ const partial = Array.isArray(parsed.data) ? false : (parsed.data.partial ?? false)
16
22
 
17
23
  const readStates: DiscordReadState[] = []
18
24
  for (const entry of entries) {
@@ -25,7 +31,7 @@ export function parseReadStates(value: unknown): DiscordReadState[] {
25
31
  mentionCount: state.data.mention_count ?? 0,
26
32
  })
27
33
  }
28
- return readStates
34
+ return { states: readStates, partial }
29
35
  }
30
36
 
31
37
  const GATEWAY_URL = 'wss://gateway.discord.gg/?v=10&encoding=json'
@@ -80,6 +86,7 @@ export class DiscordListener {
80
86
  private token: string | null = null
81
87
  private cachedUser: { id: string; username: string } | null = null
82
88
  private readState: DiscordReadState[] | null = null
89
+ private readStatePartial = false
83
90
  private generation = 0
84
91
  private startPromise: Promise<void> | null = null
85
92
  private pendingStartReject: ((error: Error) => void) | null = null
@@ -202,6 +209,10 @@ export class DiscordListener {
202
209
  return this.readState
203
210
  }
204
211
 
212
+ getReadStateSnapshot(): DiscordReadStateSnapshot | null {
213
+ return this.readState === null ? null : { states: this.readState, partial: this.readStatePartial }
214
+ }
215
+
205
216
  private isCurrent(generation: number, ws?: WebSocket): boolean {
206
217
  if (generation !== this.generation || !this.running) return false
207
218
  if (ws !== undefined && this.ws !== ws) return false
@@ -326,7 +337,9 @@ export class DiscordListener {
326
337
  this.sessionId = d.session_id
327
338
  this.resumeGatewayUrl = d.resume_gateway_url
328
339
  this.cachedUser = d.user
329
- this.readState = d.read_state === undefined ? [] : parseReadStates(d.read_state)
340
+ const snapshot = d.read_state === undefined ? null : parseReadStates(d.read_state)
341
+ this.readState = snapshot === null ? [] : snapshot.states
342
+ this.readStatePartial = snapshot?.partial ?? false
330
343
  this.reconnectAttempts = 0
331
344
  this.emitter.emit('connected', { user: d.user, sessionId: d.session_id })
332
345
  return
@@ -139,6 +139,13 @@ export interface DiscordReadState {
139
139
  mentionCount: number
140
140
  }
141
141
 
142
+ // When partial is true the entries are an incomplete slice, so an absent channel
143
+ // carries no information: it may simply have been left out of this payload.
144
+ export interface DiscordReadStateSnapshot {
145
+ states: DiscordReadState[]
146
+ partial: boolean
147
+ }
148
+
142
149
  export interface DiscordUnreadMention extends DiscordMention {
143
150
  mention_count: number
144
151
  }
@@ -153,6 +160,31 @@ export interface DiscordUnreadMentionsResult {
153
160
  windowDays: number
154
161
  }
155
162
 
163
+ export interface DiscordUnreadDM {
164
+ id: string
165
+ type: number
166
+ name: string | null
167
+ recipients: DiscordUser[]
168
+ lastMessageId: string
169
+ // Discord's own badge counter, which counts every message in a DM rather than
170
+ // only mentions. null when the channel has no READY read-state entry (count
171
+ // unknown); 0 when the DM is muted, since muting stops the counter but still
172
+ // leaves the channel unread.
173
+ unreadCount: number | null
174
+ }
175
+
176
+ // count and totalUnread describe every unread DM found, so they stay truthful when
177
+ // `channels` is capped by a limit. totalUnread sums known counts only, meaning
178
+ // channels with an unknown count contribute nothing to it. complete is false when
179
+ // Discord delivered a partial read state, in which case never-opened DMs may be
180
+ // missing from the result.
181
+ export interface DiscordUnreadDMsResult {
182
+ channels: DiscordUnreadDM[]
183
+ count: number
184
+ totalUnread: number
185
+ complete: boolean
186
+ }
187
+
156
188
  export interface DiscordSearchResult {
157
189
  id: string
158
190
  channel_id: string
@@ -207,6 +239,38 @@ export interface DiscordGuildMember {
207
239
  flags: number
208
240
  }
209
241
 
242
+ export interface DiscordRoleColors {
243
+ primary_color: number
244
+ secondary_color: number | null
245
+ tertiary_color: number | null
246
+ }
247
+
248
+ export interface DiscordRoleTags {
249
+ bot_id?: string
250
+ integration_id?: string
251
+ premium_subscriber?: null
252
+ subscription_listing_id?: string
253
+ available_for_purchase?: null
254
+ guild_connections?: null
255
+ }
256
+
257
+ export interface DiscordRole {
258
+ id: string
259
+ name: string
260
+ /** @deprecated Discord still returns this; prefer {@link DiscordRoleColors.primary_color}. */
261
+ color: number
262
+ colors: DiscordRoleColors
263
+ hoist: boolean
264
+ position: number
265
+ permissions: string
266
+ managed: boolean
267
+ mentionable: boolean
268
+ flags: number
269
+ icon?: string | null
270
+ unicode_emoji?: string | null
271
+ tags?: DiscordRoleTags
272
+ }
273
+
210
274
  export interface DiscordUserProfile {
211
275
  user: DiscordUser & { bio?: string }
212
276
  connected_accounts: Array<{
@@ -480,7 +544,11 @@ export type DiscordRawReadState = z.infer<typeof DiscordRawReadStateSchema>
480
544
  // VERSIONED_READ_STATES gateway capability is negotiated.
481
545
  export const DiscordReadyReadStateSchema = z.union([
482
546
  z.array(z.unknown()),
483
- z.object({ entries: z.array(z.unknown()), version: z.number().optional() }),
547
+ z.object({
548
+ entries: z.array(z.unknown()),
549
+ version: z.number().optional(),
550
+ partial: z.boolean().optional(),
551
+ }),
484
552
  ])
485
553
 
486
554
  export type DiscordGatewayEvent =