agent-messenger 2.31.0 → 2.32.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 +14 -1
- package/dist/package.json +1 -1
- package/dist/src/platforms/discord/client.d.ts +11 -2
- package/dist/src/platforms/discord/client.d.ts.map +1 -1
- package/dist/src/platforms/discord/client.js +109 -4
- package/dist/src/platforms/discord/client.js.map +1 -1
- package/dist/src/platforms/discord/commands/mention.d.ts +5 -0
- package/dist/src/platforms/discord/commands/mention.d.ts.map +1 -1
- package/dist/src/platforms/discord/commands/mention.js +60 -4
- package/dist/src/platforms/discord/commands/mention.js.map +1 -1
- package/dist/src/platforms/discord/commands/reaction.js +1 -1
- package/dist/src/platforms/discord/commands/reaction.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 -1
- package/dist/src/platforms/discord/listener.d.ts.map +1 -1
- package/dist/src/platforms/discord/listener.js +28 -1
- package/dist/src/platforms/discord/listener.js.map +1 -1
- package/dist/src/platforms/discord/types.d.ts +43 -9
- package/dist/src/platforms/discord/types.d.ts.map +1 -1
- package/dist/src/platforms/discord/types.js +22 -7
- package/dist/src/platforms/discord/types.js.map +1 -1
- package/docs/content/docs/cli/discord.mdx +7 -0
- package/docs/content/docs/sdk/discord.mdx +38 -0
- package/e2e/README.md +2 -2
- package/e2e/discord.e2e.test.ts +16 -2
- 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 +19 -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 +230 -9
- package/src/platforms/discord/client.ts +122 -5
- package/src/platforms/discord/commands/mention.test.ts +84 -0
- package/src/platforms/discord/commands/mention.ts +78 -15
- package/src/platforms/discord/commands/reaction.test.ts +1 -1
- package/src/platforms/discord/commands/reaction.ts +1 -1
- package/src/platforms/discord/index.ts +3 -0
- package/src/platforms/discord/listener.test.ts +69 -2
- package/src/platforms/discord/listener.ts +31 -2
- package/src/platforms/discord/types.test.ts +11 -0
- package/src/platforms/discord/types.ts +50 -10
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterEach, describe, expect, mock, it } from 'bun:test'
|
|
2
2
|
|
|
3
|
-
import { DiscordListener } from '@/platforms/discord/listener'
|
|
3
|
+
import { DiscordListener, parseReadStates } from '@/platforms/discord/listener'
|
|
4
4
|
import type {
|
|
5
5
|
DiscordGatewayGenericEvent,
|
|
6
6
|
DiscordGatewayMessageCreateEvent,
|
|
@@ -68,7 +68,7 @@ class MockWs {
|
|
|
68
68
|
this.simulateMessage({ op: 10, d: { heartbeat_interval: interval } })
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
simulateReady(sessionId = 'session_123') {
|
|
71
|
+
simulateReady(sessionId = 'session_123', readState?: unknown) {
|
|
72
72
|
this.simulateMessage({
|
|
73
73
|
op: 0,
|
|
74
74
|
t: 'READY',
|
|
@@ -77,6 +77,7 @@ class MockWs {
|
|
|
77
77
|
session_id: sessionId,
|
|
78
78
|
resume_gateway_url: 'wss://resume.discord.gg',
|
|
79
79
|
user: { id: 'U_SELF', username: 'testbot' },
|
|
80
|
+
...(readState === undefined ? {} : { read_state: readState }),
|
|
80
81
|
},
|
|
81
82
|
})
|
|
82
83
|
}
|
|
@@ -135,6 +136,33 @@ async function startConnecting(listener: DiscordListener): Promise<MockWs> {
|
|
|
135
136
|
return ws
|
|
136
137
|
}
|
|
137
138
|
|
|
139
|
+
describe('parseReadStates', () => {
|
|
140
|
+
it('filters out non-channel read state types', () => {
|
|
141
|
+
const result = parseReadStates([
|
|
142
|
+
{ id: 'c1', last_message_id: '100', mention_count: 1 },
|
|
143
|
+
{ id: 'c2', read_state_type: 2, mention_count: 5 },
|
|
144
|
+
])
|
|
145
|
+
|
|
146
|
+
expect(result).toEqual([{ channelId: 'c1', lastMessageId: '100', mentionCount: 1 }])
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('normalizes missing last_message_id and mention_count', () => {
|
|
150
|
+
const result = parseReadStates([{ id: 'c1' }])
|
|
151
|
+
|
|
152
|
+
expect(result).toEqual([{ channelId: 'c1', lastMessageId: null, mentionCount: 0 }])
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('skips malformed entries but keeps valid ones', () => {
|
|
156
|
+
const result = parseReadStates([{ id: 'c1', mention_count: 1 }, { mention_count: 3 }])
|
|
157
|
+
|
|
158
|
+
expect(result).toEqual([{ channelId: 'c1', lastMessageId: null, mentionCount: 1 }])
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('throws on a non-array, non-versioned payload', () => {
|
|
162
|
+
expect(() => parseReadStates({ foo: 'bar' })).toThrow(/invalid read_state/)
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
|
|
138
166
|
describe('DiscordListener', () => {
|
|
139
167
|
let listener: DiscordListener
|
|
140
168
|
|
|
@@ -235,6 +263,45 @@ describe('DiscordListener', () => {
|
|
|
235
263
|
})
|
|
236
264
|
})
|
|
237
265
|
|
|
266
|
+
describe('read state', () => {
|
|
267
|
+
it('captures raw-array read_state from READY', async () => {
|
|
268
|
+
const client = createMockClient()
|
|
269
|
+
listener = new DiscordListener(client)
|
|
270
|
+
|
|
271
|
+
const started = listener.start()
|
|
272
|
+
const ws = await waitForSocket()
|
|
273
|
+
ws.simulateOpen()
|
|
274
|
+
ws.simulateHello()
|
|
275
|
+
ws.simulateReady('session_123', [{ id: 'c1', last_message_id: '100', mention_count: 2 }])
|
|
276
|
+
await started
|
|
277
|
+
|
|
278
|
+
expect(listener.getReadState()).toEqual([{ channelId: 'c1', lastMessageId: '100', mentionCount: 2 }])
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it('captures versioned { entries } read_state from READY', async () => {
|
|
282
|
+
const client = createMockClient()
|
|
283
|
+
listener = new DiscordListener(client)
|
|
284
|
+
|
|
285
|
+
const started = listener.start()
|
|
286
|
+
const ws = await waitForSocket()
|
|
287
|
+
ws.simulateOpen()
|
|
288
|
+
ws.simulateHello()
|
|
289
|
+
ws.simulateReady('session_123', { version: 7, entries: [{ id: 'c1', last_message_id: '100', mention_count: 1 }] })
|
|
290
|
+
await started
|
|
291
|
+
|
|
292
|
+
expect(listener.getReadState()).toEqual([{ channelId: 'c1', lastMessageId: '100', mentionCount: 1 }])
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it('defaults to an empty read state when READY omits read_state', async () => {
|
|
296
|
+
const client = createMockClient()
|
|
297
|
+
listener = new DiscordListener(client)
|
|
298
|
+
|
|
299
|
+
await startAndReady(listener)
|
|
300
|
+
|
|
301
|
+
expect(listener.getReadState()).toEqual([])
|
|
302
|
+
})
|
|
303
|
+
})
|
|
304
|
+
|
|
238
305
|
describe('identify', () => {
|
|
239
306
|
it('sends Identify after Hello', async () => {
|
|
240
307
|
const client = createMockClient()
|
|
@@ -3,8 +3,30 @@ import { EventEmitter } from 'events'
|
|
|
3
3
|
import WebSocket from 'ws'
|
|
4
4
|
|
|
5
5
|
import type { DiscordClient } from './client'
|
|
6
|
-
import type { DiscordListenerEventMap, DiscordGatewayGenericEvent } from './types'
|
|
7
|
-
import { DiscordGatewayOpcode } from './types'
|
|
6
|
+
import type { DiscordListenerEventMap, DiscordGatewayGenericEvent, DiscordReadState } from './types'
|
|
7
|
+
import { DiscordGatewayOpcode, DiscordRawReadStateSchema, DiscordReadyReadStateSchema } from './types'
|
|
8
|
+
|
|
9
|
+
export function parseReadStates(value: unknown): DiscordReadState[] {
|
|
10
|
+
const parsed = DiscordReadyReadStateSchema.safeParse(value)
|
|
11
|
+
if (!parsed.success) {
|
|
12
|
+
throw new Error('Discord READY contained an invalid read_state payload')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const entries = Array.isArray(parsed.data) ? parsed.data : parsed.data.entries
|
|
16
|
+
|
|
17
|
+
const readStates: DiscordReadState[] = []
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
const state = DiscordRawReadStateSchema.safeParse(entry)
|
|
20
|
+
if (!state.success) continue
|
|
21
|
+
if ((state.data.read_state_type ?? 0) !== 0) continue
|
|
22
|
+
readStates.push({
|
|
23
|
+
channelId: state.data.id,
|
|
24
|
+
lastMessageId: state.data.last_message_id ?? null,
|
|
25
|
+
mentionCount: state.data.mention_count ?? 0,
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
return readStates
|
|
29
|
+
}
|
|
8
30
|
|
|
9
31
|
const GATEWAY_URL = 'wss://gateway.discord.gg/?v=10&encoding=json'
|
|
10
32
|
const GATEWAY_QUERY = '?v=10&encoding=json'
|
|
@@ -57,6 +79,7 @@ export class DiscordListener {
|
|
|
57
79
|
private resumeGatewayUrl: string | null = null
|
|
58
80
|
private token: string | null = null
|
|
59
81
|
private cachedUser: { id: string; username: string } | null = null
|
|
82
|
+
private readState: DiscordReadState[] | null = null
|
|
60
83
|
private generation = 0
|
|
61
84
|
private startPromise: Promise<void> | null = null
|
|
62
85
|
private pendingStartReject: ((error: Error) => void) | null = null
|
|
@@ -151,6 +174,7 @@ export class DiscordListener {
|
|
|
151
174
|
this.resumeGatewayUrl = null
|
|
152
175
|
this.token = null
|
|
153
176
|
this.cachedUser = null
|
|
177
|
+
this.readState = null
|
|
154
178
|
rejectStart?.(new Error('Discord gateway start was stopped before becoming ready'))
|
|
155
179
|
}
|
|
156
180
|
|
|
@@ -174,6 +198,10 @@ export class DiscordListener {
|
|
|
174
198
|
return this
|
|
175
199
|
}
|
|
176
200
|
|
|
201
|
+
getReadState(): DiscordReadState[] | null {
|
|
202
|
+
return this.readState
|
|
203
|
+
}
|
|
204
|
+
|
|
177
205
|
private isCurrent(generation: number, ws?: WebSocket): boolean {
|
|
178
206
|
if (generation !== this.generation || !this.running) return false
|
|
179
207
|
if (ws !== undefined && this.ws !== ws) return false
|
|
@@ -298,6 +326,7 @@ export class DiscordListener {
|
|
|
298
326
|
this.sessionId = d.session_id
|
|
299
327
|
this.resumeGatewayUrl = d.resume_gateway_url
|
|
300
328
|
this.cachedUser = d.user
|
|
329
|
+
this.readState = d.read_state === undefined ? [] : parseReadStates(d.read_state)
|
|
301
330
|
this.reconnectAttempts = 0
|
|
302
331
|
this.emitter.emit('connected', { user: d.user, sessionId: d.session_id })
|
|
303
332
|
return
|
|
@@ -150,6 +150,17 @@ it('DiscordReactionSchema validates reaction with custom emoji', () => {
|
|
|
150
150
|
expect(result.success).toBe(true)
|
|
151
151
|
})
|
|
152
152
|
|
|
153
|
+
it('DiscordReactionSchema validates nullable emoji fields', () => {
|
|
154
|
+
const result = DiscordReactionSchema.safeParse({
|
|
155
|
+
emoji: {
|
|
156
|
+
id: null,
|
|
157
|
+
name: null,
|
|
158
|
+
},
|
|
159
|
+
count: 1,
|
|
160
|
+
})
|
|
161
|
+
expect(result.success).toBe(true)
|
|
162
|
+
})
|
|
163
|
+
|
|
153
164
|
it('DiscordReactionSchema rejects missing required fields', () => {
|
|
154
165
|
const result = DiscordReactionSchema.safeParse({
|
|
155
166
|
emoji: {
|
|
@@ -38,6 +38,7 @@ export interface DiscordMessage {
|
|
|
38
38
|
timestamp: string
|
|
39
39
|
edited_timestamp?: string
|
|
40
40
|
thread_id?: string
|
|
41
|
+
reactions?: DiscordReaction[]
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
export interface DiscordUser {
|
|
@@ -58,8 +59,8 @@ export interface DiscordDMChannel {
|
|
|
58
59
|
|
|
59
60
|
export interface DiscordReaction {
|
|
60
61
|
emoji: {
|
|
61
|
-
id?: string
|
|
62
|
-
name: string
|
|
62
|
+
id?: string | null
|
|
63
|
+
name: string | null
|
|
63
64
|
}
|
|
64
65
|
count: number
|
|
65
66
|
}
|
|
@@ -91,6 +92,26 @@ export interface DiscordUserNote {
|
|
|
91
92
|
note: string
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
export interface DiscordReadState {
|
|
96
|
+
channelId: string
|
|
97
|
+
lastMessageId: string | null
|
|
98
|
+
mentionCount: number
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface DiscordUnreadMention extends DiscordMention {
|
|
102
|
+
mention_count: number
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface DiscordUnreadMentionsResult {
|
|
106
|
+
mentions: DiscordUnreadMention[]
|
|
107
|
+
count: number
|
|
108
|
+
// Account-wide unread-mention badge total; not narrowed by guildId because
|
|
109
|
+
// READY read states carry no guild association.
|
|
110
|
+
badgeCount: number
|
|
111
|
+
complete: boolean
|
|
112
|
+
windowDays: number
|
|
113
|
+
}
|
|
114
|
+
|
|
94
115
|
export interface DiscordSearchResult {
|
|
95
116
|
id: string
|
|
96
117
|
channel_id: string
|
|
@@ -190,6 +211,14 @@ export const DiscordChannelSchema = z.object({
|
|
|
190
211
|
position: z.number().optional(),
|
|
191
212
|
})
|
|
192
213
|
|
|
214
|
+
export const DiscordReactionSchema = z.object({
|
|
215
|
+
emoji: z.object({
|
|
216
|
+
id: z.string().nullish(),
|
|
217
|
+
name: z.string().nullable(),
|
|
218
|
+
}),
|
|
219
|
+
count: z.number(),
|
|
220
|
+
})
|
|
221
|
+
|
|
193
222
|
export const DiscordMessageSchema = z.object({
|
|
194
223
|
id: z.string(),
|
|
195
224
|
channel_id: z.string(),
|
|
@@ -201,6 +230,7 @@ export const DiscordMessageSchema = z.object({
|
|
|
201
230
|
timestamp: z.string(),
|
|
202
231
|
edited_timestamp: z.string().optional(),
|
|
203
232
|
thread_id: z.string().optional(),
|
|
233
|
+
reactions: z.array(DiscordReactionSchema).optional(),
|
|
204
234
|
})
|
|
205
235
|
|
|
206
236
|
export const DiscordUserSchema = z.object({
|
|
@@ -219,14 +249,6 @@ export const DiscordDMChannelSchema = z.object({
|
|
|
219
249
|
name: z.string().optional(),
|
|
220
250
|
})
|
|
221
251
|
|
|
222
|
-
export const DiscordReactionSchema = z.object({
|
|
223
|
-
emoji: z.object({
|
|
224
|
-
id: z.string().optional(),
|
|
225
|
-
name: z.string(),
|
|
226
|
-
}),
|
|
227
|
-
count: z.number(),
|
|
228
|
-
})
|
|
229
|
-
|
|
230
252
|
export const DiscordFileSchema = z.object({
|
|
231
253
|
id: z.string(),
|
|
232
254
|
filename: z.string(),
|
|
@@ -402,6 +424,24 @@ export interface DiscordGatewayGenericEvent {
|
|
|
402
424
|
[key: string]: unknown
|
|
403
425
|
}
|
|
404
426
|
|
|
427
|
+
// read_state_type 0 = channel read state (carries last_message_id); non-zero types
|
|
428
|
+
// carry last_acked_id instead and must be filtered out for mention correlation.
|
|
429
|
+
export const DiscordRawReadStateSchema = z.object({
|
|
430
|
+
id: z.string(),
|
|
431
|
+
read_state_type: z.number().optional(),
|
|
432
|
+
last_message_id: z.string().nullish(),
|
|
433
|
+
mention_count: z.number().optional(),
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
export type DiscordRawReadState = z.infer<typeof DiscordRawReadStateSchema>
|
|
437
|
+
|
|
438
|
+
// READY sends read_state as a raw array, or as { entries, version } when the
|
|
439
|
+
// VERSIONED_READ_STATES gateway capability is negotiated.
|
|
440
|
+
export const DiscordReadyReadStateSchema = z.union([
|
|
441
|
+
z.array(z.unknown()),
|
|
442
|
+
z.object({ entries: z.array(z.unknown()), version: z.number().optional() }),
|
|
443
|
+
])
|
|
444
|
+
|
|
405
445
|
export type DiscordGatewayEvent =
|
|
406
446
|
| DiscordGatewayMessageCreateEvent
|
|
407
447
|
| DiscordGatewayMessageUpdateEvent
|