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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +12 -1
- package/dist/package.json +1 -1
- package/dist/src/platforms/discord/client.d.ts +18 -1
- package/dist/src/platforms/discord/client.d.ts.map +1 -1
- package/dist/src/platforms/discord/client.js +56 -3
- package/dist/src/platforms/discord/client.js.map +1 -1
- package/dist/src/platforms/discord/commands/dm.d.ts +4 -0
- package/dist/src/platforms/discord/commands/dm.d.ts.map +1 -1
- package/dist/src/platforms/discord/commands/dm.js +56 -4
- package/dist/src/platforms/discord/commands/dm.js.map +1 -1
- package/dist/src/platforms/discord/commands/member.d.ts +3 -0
- package/dist/src/platforms/discord/commands/member.d.ts.map +1 -1
- package/dist/src/platforms/discord/commands/member.js +72 -1
- package/dist/src/platforms/discord/commands/member.js.map +1 -1
- package/dist/src/platforms/discord/index.d.ts +1 -1
- package/dist/src/platforms/discord/index.d.ts.map +1 -1
- package/dist/src/platforms/discord/index.js.map +1 -1
- package/dist/src/platforms/discord/listener.d.ts +4 -2
- package/dist/src/platforms/discord/listener.d.ts.map +1 -1
- package/dist/src/platforms/discord/listener.js +9 -2
- package/dist/src/platforms/discord/listener.js.map +1 -1
- package/dist/src/platforms/discord/types.d.ts +48 -0
- package/dist/src/platforms/discord/types.d.ts.map +1 -1
- package/dist/src/platforms/discord/types.js +5 -1
- package/dist/src/platforms/discord/types.js.map +1 -1
- package/docs/content/docs/cli/discord.mdx +25 -0
- package/docs/content/docs/sdk/discord.mdx +21 -0
- package/package.json +1 -1
- package/skills/agent-channeltalk/SKILL.md +1 -1
- package/skills/agent-channeltalkbot/SKILL.md +1 -1
- package/skills/agent-discord/SKILL.md +31 -1
- package/skills/agent-discordbot/SKILL.md +1 -1
- package/skills/agent-imessage/SKILL.md +1 -1
- package/skills/agent-instagram/SKILL.md +1 -1
- package/skills/agent-kakaotalk/SKILL.md +1 -1
- package/skills/agent-line/SKILL.md +1 -1
- package/skills/agent-slack/SKILL.md +1 -1
- package/skills/agent-slackbot/SKILL.md +1 -1
- package/skills/agent-teams/SKILL.md +1 -1
- package/skills/agent-telegram/SKILL.md +1 -1
- package/skills/agent-telegrambot/SKILL.md +1 -1
- package/skills/agent-webex/SKILL.md +1 -1
- package/skills/agent-webexbot/SKILL.md +1 -1
- package/skills/agent-wechatbot/SKILL.md +1 -1
- package/skills/agent-whatsapp/SKILL.md +1 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/platforms/discord/client.test.ts +204 -1
- package/src/platforms/discord/client.ts +85 -3
- package/src/platforms/discord/commands/dm.test.ts +126 -1
- package/src/platforms/discord/commands/dm.ts +63 -3
- package/src/platforms/discord/commands/member.test.ts +177 -0
- package/src/platforms/discord/commands/member.ts +82 -0
- package/src/platforms/discord/index.ts +6 -0
- package/src/platforms/discord/listener.test.ts +12 -3
- package/src/platforms/discord/listener.ts +17 -4
- 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 {
|
|
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):
|
|
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
|
-
|
|
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({
|
|
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 =
|