@aigne/afs-discord 1.11.0-beta.12

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/dist/index.mjs ADDED
@@ -0,0 +1,602 @@
1
+ import { Actions } from "@aigne/afs/provider";
2
+ import { BaseMessageProvider } from "@aigne/afs-messaging";
3
+
4
+ //#region src/client.ts
5
+ /**
6
+ * DiscordClient — pure Discord REST API wrapper.
7
+ *
8
+ * Zero external dependencies (uses native fetch).
9
+ * No AFS or application-specific concepts.
10
+ * Gateway WebSocket is handled by the provider, not the client.
11
+ */
12
+ const DEFAULT_API_BASE = "https://discord.com/api/v10";
13
+ const DEFAULT_TIMEOUT = 3e4;
14
+ var DiscordClient = class {
15
+ _token;
16
+ _apiBase;
17
+ _timeout;
18
+ constructor(options) {
19
+ if (!options.token) throw new Error("DiscordClient requires a token");
20
+ this._token = options.token;
21
+ this._apiBase = options.apiBase ?? DEFAULT_API_BASE;
22
+ this._timeout = options.timeout ?? DEFAULT_TIMEOUT;
23
+ }
24
+ async sendMessage(channelId, text, options) {
25
+ const body = { content: text };
26
+ if (options?.replyTo) body.message_reference = { message_id: options.replyTo };
27
+ if (options?.embeds) body.embeds = options.embeds;
28
+ return this._call("POST", `/channels/${channelId}/messages`, body);
29
+ }
30
+ async triggerTyping(channelId) {
31
+ await this._call("POST", `/channels/${channelId}/typing`);
32
+ }
33
+ async getGatewayUrl() {
34
+ return (await this._call("GET", "/gateway/bot")).url;
35
+ }
36
+ async getCurrentUser() {
37
+ return this._call("GET", "/users/@me");
38
+ }
39
+ static escapeMarkdown(text) {
40
+ return text.replace(/([*_~`|\\])/g, "\\$1");
41
+ }
42
+ async _call(method, path, body) {
43
+ const headers = { Authorization: `Bot ${this._token}` };
44
+ const init = {
45
+ method,
46
+ headers,
47
+ signal: AbortSignal.timeout(this._timeout)
48
+ };
49
+ if (body) {
50
+ headers["Content-Type"] = "application/json";
51
+ init.body = JSON.stringify(body);
52
+ }
53
+ const response = await fetch(`${this._apiBase}${path}`, init);
54
+ if (!response.ok) {
55
+ const desc = (await response.text().catch(() => "")).replace(this._token, "***");
56
+ throw new Error(`Discord API error ${response.status}: ${desc}`);
57
+ }
58
+ if (response.status === 204) return void 0;
59
+ return response.json();
60
+ }
61
+ };
62
+
63
+ //#endregion
64
+ //#region \0@oxc-project+runtime@0.108.0/helpers/decorate.js
65
+ function __decorate(decorators, target, key, desc) {
66
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
67
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
68
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
69
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
70
+ }
71
+
72
+ //#endregion
73
+ //#region src/index.ts
74
+ const GatewayOp = {
75
+ DISPATCH: 0,
76
+ HEARTBEAT: 1,
77
+ IDENTIFY: 2,
78
+ RESUME: 6,
79
+ RECONNECT: 7,
80
+ INVALID_SESSION: 9,
81
+ HELLO: 10,
82
+ HEARTBEAT_ACK: 11
83
+ };
84
+ const DEFAULT_INTENTS = 37377;
85
+ var AFSDiscord = class extends BaseMessageProvider {
86
+ static manifest() {
87
+ return {
88
+ name: "discord",
89
+ description: "Discord Bot API — bidirectional messaging via Gateway WebSocket.\n- Real-time events via Discord Gateway with heartbeat and resume\n- Multi-bot support with per-bot channel monitoring\n- Path: /:bot/conversations/:channelId/messages/:msgId",
90
+ uriTemplate: "discord://{token}",
91
+ category: "messaging",
92
+ schema: {
93
+ type: "object",
94
+ properties: {
95
+ token: {
96
+ type: "string",
97
+ description: "Discord Bot token",
98
+ sensitive: true
99
+ },
100
+ channels: {
101
+ type: "array",
102
+ items: { type: "string" },
103
+ description: "Channel IDs to monitor"
104
+ }
105
+ },
106
+ required: ["token"]
107
+ },
108
+ tags: [
109
+ "discord",
110
+ "messaging",
111
+ "chat",
112
+ "bot"
113
+ ],
114
+ capabilityTags: [
115
+ "read-write",
116
+ "crud",
117
+ "search",
118
+ "auth:token",
119
+ "remote",
120
+ "http",
121
+ "real-time",
122
+ "rate-limited"
123
+ ],
124
+ security: {
125
+ riskLevel: "external",
126
+ resourceAccess: ["internet"],
127
+ notes: ["Connects to Discord API + Gateway — requires bot token with MESSAGE_CONTENT intent"]
128
+ },
129
+ capabilities: { network: {
130
+ egress: true,
131
+ allowedDomains: ["discord.com", "gateway.discord.gg"]
132
+ } }
133
+ };
134
+ }
135
+ static treeSchema() {
136
+ return {
137
+ operations: [
138
+ "list",
139
+ "read",
140
+ "exec",
141
+ "stat",
142
+ "explain"
143
+ ],
144
+ tree: {
145
+ "/": {
146
+ kind: "messaging:root",
147
+ operations: ["list", "exec"],
148
+ actions: [
149
+ "add-bot",
150
+ "remove-bot",
151
+ "start",
152
+ "stop",
153
+ "process-event"
154
+ ]
155
+ },
156
+ "/{bot}": {
157
+ kind: "messaging:bot",
158
+ operations: ["list", "read"]
159
+ },
160
+ "/{bot}/ctl": {
161
+ kind: "messaging:status",
162
+ operations: ["read"]
163
+ },
164
+ "/{bot}/conversations": {
165
+ kind: "messaging:conversations",
166
+ operations: ["list"]
167
+ },
168
+ "/{bot}/conversations/{convId}": {
169
+ kind: "messaging:conversation",
170
+ operations: ["list"]
171
+ },
172
+ "/{bot}/conversations/{convId}/messages": {
173
+ kind: "messaging:messages",
174
+ operations: ["list", "exec"],
175
+ actions: ["send"]
176
+ },
177
+ "/{bot}/conversations/{convId}/messages/{msgId}": {
178
+ kind: "messaging:message",
179
+ operations: ["read"]
180
+ }
181
+ },
182
+ auth: {
183
+ type: "token",
184
+ env: ["DISCORD_BOT_TOKEN"]
185
+ },
186
+ bestFor: [
187
+ "community chat",
188
+ "bot commands",
189
+ "server notifications"
190
+ ],
191
+ notFor: ["file storage", "database queries"]
192
+ };
193
+ }
194
+ providerName = "discord";
195
+ eventPrefix = "discord";
196
+ _gatewayStates = /* @__PURE__ */ new Map();
197
+ constructor(options) {
198
+ let bots;
199
+ if (options.bots) {
200
+ bots = options.bots;
201
+ for (const bot of bots) if (!bot.token) throw new Error(`AFSDiscord bot "${bot.name}" requires a token`);
202
+ } else {
203
+ if (!options.token) throw new Error("AFSDiscord requires a token");
204
+ bots = [{
205
+ name: "default",
206
+ token: options.token,
207
+ conversations: options.channels ?? [],
208
+ apiBase: options.apiBase,
209
+ intents: options.intents
210
+ }];
211
+ }
212
+ super({
213
+ bots,
214
+ bufferSize: options.bufferSize
215
+ });
216
+ for (const bot of bots) this._gatewayStates.set(bot.name, {
217
+ ws: null,
218
+ connected: false,
219
+ disposed: false,
220
+ sessionId: null,
221
+ resumeUrl: null,
222
+ seq: null,
223
+ heartbeatTimer: null,
224
+ heartbeatAcked: true,
225
+ backoff: 1e3,
226
+ intents: bot.intents ?? DEFAULT_INTENTS
227
+ });
228
+ }
229
+ getMessageCapabilities() {
230
+ return {
231
+ formats: {
232
+ send: ["text", "markdown"],
233
+ receive: ["text"]
234
+ },
235
+ maxMessageLength: 2e3,
236
+ features: {
237
+ edit: false,
238
+ delete: false,
239
+ reply: true,
240
+ thread: true,
241
+ reaction: true,
242
+ inlineKeyboard: false
243
+ },
244
+ limits: { messagesPerSecond: 5 }
245
+ };
246
+ }
247
+ createBotClient(config) {
248
+ return new DiscordClient({
249
+ token: config.token,
250
+ apiBase: config.apiBase
251
+ });
252
+ }
253
+ async sendMessage(client, convId, text, _opts) {
254
+ return { messageId: (await client.sendMessage(convId, text)).id };
255
+ }
256
+ async sendTypingIndicator(client, convId) {
257
+ await client.triggerTyping(convId);
258
+ }
259
+ normalizeMessage(raw) {
260
+ const msg = raw;
261
+ return {
262
+ id: msg.id ?? "0",
263
+ text: String(msg.content ?? ""),
264
+ from: this.normalizeSender(msg.author),
265
+ timestamp: msg.timestamp ? Math.floor(new Date(msg.timestamp).getTime() / 1e3) : Math.floor(Date.now() / 1e3),
266
+ conversationId: String(msg.channel_id ?? ""),
267
+ platform: { guild_id: msg.guild_id }
268
+ };
269
+ }
270
+ normalizeSender(raw) {
271
+ return {
272
+ id: String(raw.id ?? "0"),
273
+ name: String(raw.username ?? raw.name ?? ""),
274
+ isBot: raw.bot
275
+ };
276
+ }
277
+ start(botName) {
278
+ const names = botName ? [botName] : [...this._gatewayStates.keys()];
279
+ for (const name of names) {
280
+ const state = this._gatewayStates.get(name);
281
+ if (!state || state.connected || state.disposed) continue;
282
+ this._connectGateway(name);
283
+ }
284
+ }
285
+ stop(botName) {
286
+ const names = botName ? [botName] : [...this._gatewayStates.keys()];
287
+ for (const name of names) this._disconnectGateway(name);
288
+ }
289
+ dispose() {
290
+ for (const name of this._gatewayStates.keys()) {
291
+ const state = this._gatewayStates.get(name);
292
+ state.disposed = true;
293
+ this._disconnectGateway(name);
294
+ }
295
+ }
296
+ onBotAdded(name) {
297
+ if (!this._gatewayStates.has(name)) this._gatewayStates.set(name, {
298
+ ws: null,
299
+ connected: false,
300
+ disposed: false,
301
+ sessionId: null,
302
+ resumeUrl: null,
303
+ seq: null,
304
+ heartbeatTimer: null,
305
+ heartbeatAcked: true,
306
+ backoff: 1e3,
307
+ intents: DEFAULT_INTENTS
308
+ });
309
+ }
310
+ onBotRemoved(name) {
311
+ this._disconnectGateway(name);
312
+ this._gatewayStates.delete(name);
313
+ }
314
+ async listRootActions(_ctx) {
315
+ return { data: [
316
+ this.buildEntry("/.actions/add-bot", { meta: { description: "Add a bot instance at runtime" } }),
317
+ this.buildEntry("/.actions/remove-bot", { meta: { description: "Remove a bot instance" } }),
318
+ this.buildEntry("/.actions/start", { meta: { description: "Connect to Discord Gateway" } }),
319
+ this.buildEntry("/.actions/stop", { meta: { description: "Disconnect from Gateway" } }),
320
+ this.buildEntry("/.actions/process-event", { meta: { description: "Inject a Discord event externally" } })
321
+ ] };
322
+ }
323
+ async execStart(_ctx) {
324
+ this.start();
325
+ return {
326
+ success: true,
327
+ data: {
328
+ ok: true,
329
+ gateway: true
330
+ }
331
+ };
332
+ }
333
+ async execStop(_ctx) {
334
+ this.stop();
335
+ return {
336
+ success: true,
337
+ data: {
338
+ ok: true,
339
+ gateway: false
340
+ }
341
+ };
342
+ }
343
+ async execProcessEvent(_ctx, args) {
344
+ this._processEvent(args);
345
+ return {
346
+ success: true,
347
+ data: { ok: true }
348
+ };
349
+ }
350
+ async _connectGateway(botName) {
351
+ const state = this._gatewayStates.get(botName);
352
+ const client = this._getClient(botName);
353
+ if (!state || !client || state.disposed) return;
354
+ try {
355
+ let gatewayUrl = state.resumeUrl;
356
+ if (!gatewayUrl) gatewayUrl = await client.getGatewayUrl();
357
+ const wsUrl = `${gatewayUrl}/?v=10&encoding=json`;
358
+ const ws = new WebSocket(wsUrl);
359
+ state.ws = ws;
360
+ ws.onmessage = (event) => {
361
+ try {
362
+ const payload = JSON.parse(String(event.data));
363
+ this._handleGatewayPayload(botName, payload);
364
+ } catch {}
365
+ };
366
+ ws.onclose = () => {
367
+ this._cleanupGateway(botName);
368
+ if (!state.disposed) {
369
+ setTimeout(() => this._connectGateway(botName), state.backoff);
370
+ state.backoff = Math.min(state.backoff * 2, 6e4);
371
+ }
372
+ };
373
+ ws.onerror = () => {};
374
+ } catch {
375
+ if (!state.disposed) {
376
+ setTimeout(() => this._connectGateway(botName), state.backoff);
377
+ state.backoff = Math.min(state.backoff * 2, 6e4);
378
+ }
379
+ }
380
+ }
381
+ _handleGatewayPayload(botName, payload) {
382
+ const state = this._gatewayStates.get(botName);
383
+ if (!state) return;
384
+ if (payload.s !== null) state.seq = payload.s;
385
+ switch (payload.op) {
386
+ case GatewayOp.HELLO: {
387
+ const interval = payload.d?.heartbeat_interval ?? 41250;
388
+ this._startHeartbeat(botName, interval);
389
+ if (state.sessionId) this._sendResume(botName);
390
+ else this._sendIdentify(botName);
391
+ break;
392
+ }
393
+ case GatewayOp.HEARTBEAT_ACK:
394
+ state.heartbeatAcked = true;
395
+ break;
396
+ case GatewayOp.DISPATCH:
397
+ this._handleDispatch(botName, payload.t, payload.d);
398
+ break;
399
+ case GatewayOp.RECONNECT:
400
+ state.ws?.close();
401
+ break;
402
+ case GatewayOp.INVALID_SESSION:
403
+ if (!(payload.d === true)) {
404
+ state.sessionId = null;
405
+ state.seq = null;
406
+ }
407
+ setTimeout(() => {
408
+ state.ws?.close();
409
+ }, 1e3 + Math.random() * 4e3);
410
+ break;
411
+ }
412
+ }
413
+ _handleDispatch(botName, eventType, data) {
414
+ const state = this._gatewayStates.get(botName);
415
+ if (!state) return;
416
+ if (eventType === "READY") {
417
+ state.connected = true;
418
+ state.sessionId = data.session_id;
419
+ state.resumeUrl = data.resume_gateway_url ?? null;
420
+ state.backoff = 1e3;
421
+ return;
422
+ }
423
+ if (eventType === "RESUMED") {
424
+ state.connected = true;
425
+ state.backoff = 1e3;
426
+ return;
427
+ }
428
+ if (eventType === "MESSAGE_CREATE") {
429
+ if (data.author?.bot) return;
430
+ const channelId = String(data.channel_id ?? "");
431
+ const text = String(data.content ?? "").trim();
432
+ if (!text) return;
433
+ this.emitMessageReceived(botName, {
434
+ id: String(data.id),
435
+ text,
436
+ from: this.normalizeSender({
437
+ id: data.author?.id,
438
+ username: data.author?.username,
439
+ bot: data.author?.bot
440
+ }),
441
+ timestamp: data.timestamp ? Math.floor(new Date(data.timestamp).getTime() / 1e3) : Math.floor(Date.now() / 1e3),
442
+ conversationId: channelId,
443
+ platform: { guild_id: data.guild_id }
444
+ });
445
+ }
446
+ }
447
+ _sendIdentify(botName) {
448
+ const state = this._gatewayStates.get(botName);
449
+ if (!state?.ws) return;
450
+ const token = (this._pendingBots?.find((b) => b.name === botName))?.token;
451
+ if (!token) return;
452
+ state.ws.send(JSON.stringify({
453
+ op: GatewayOp.IDENTIFY,
454
+ d: {
455
+ token,
456
+ intents: state.intents,
457
+ properties: {
458
+ os: "linux",
459
+ browser: "afs-discord",
460
+ device: "afs-discord"
461
+ }
462
+ }
463
+ }));
464
+ }
465
+ _sendResume(botName) {
466
+ const state = this._gatewayStates.get(botName);
467
+ if (!state?.ws) return;
468
+ const token = (this._pendingBots?.find((b) => b.name === botName))?.token;
469
+ if (!token) return;
470
+ state.ws.send(JSON.stringify({
471
+ op: GatewayOp.RESUME,
472
+ d: {
473
+ token,
474
+ session_id: state.sessionId,
475
+ seq: state.seq
476
+ }
477
+ }));
478
+ }
479
+ _startHeartbeat(botName, intervalMs) {
480
+ const state = this._gatewayStates.get(botName);
481
+ if (!state) return;
482
+ this._stopHeartbeat(botName);
483
+ state.heartbeatAcked = true;
484
+ state.heartbeatTimer = setInterval(() => {
485
+ if (!state.heartbeatAcked) {
486
+ state.ws?.close();
487
+ return;
488
+ }
489
+ state.heartbeatAcked = false;
490
+ state.ws?.send(JSON.stringify({
491
+ op: GatewayOp.HEARTBEAT,
492
+ d: state.seq
493
+ }));
494
+ }, intervalMs);
495
+ }
496
+ _stopHeartbeat(botName) {
497
+ const state = this._gatewayStates.get(botName);
498
+ if (state?.heartbeatTimer) {
499
+ clearInterval(state.heartbeatTimer);
500
+ state.heartbeatTimer = null;
501
+ }
502
+ }
503
+ _disconnectGateway(botName) {
504
+ const state = this._gatewayStates.get(botName);
505
+ if (!state) return;
506
+ this._stopHeartbeat(botName);
507
+ if (state.ws) {
508
+ state.ws.onclose = null;
509
+ state.ws.onmessage = null;
510
+ state.ws.onerror = null;
511
+ state.ws.close();
512
+ state.ws = null;
513
+ }
514
+ state.connected = false;
515
+ }
516
+ _cleanupGateway(botName) {
517
+ const state = this._gatewayStates.get(botName);
518
+ if (!state) return;
519
+ this._stopHeartbeat(botName);
520
+ state.ws = null;
521
+ state.connected = false;
522
+ }
523
+ /** Process an externally-provided Discord event (e.g., from interaction endpoint). Public for testing. */
524
+ _processEvent(payload) {
525
+ const botName = payload.botName ?? this._botOrder[0] ?? "default";
526
+ const type = payload.t;
527
+ const data = payload.d ?? payload;
528
+ if (type === "MESSAGE_CREATE" || data.content !== void 0) {
529
+ if (data.author?.bot) return;
530
+ const channelId = String(data.channel_id ?? "");
531
+ const text = String(data.content ?? "").trim();
532
+ if (!text) return;
533
+ this.emitMessageReceived(botName, {
534
+ id: String(data.id ?? Date.now()),
535
+ text,
536
+ from: this.normalizeSender({
537
+ id: data.author?.id ?? "0",
538
+ username: data.author?.username ?? "",
539
+ bot: data.author?.bot
540
+ }),
541
+ timestamp: data.timestamp ? Math.floor(new Date(data.timestamp).getTime() / 1e3) : Math.floor(Date.now() / 1e3),
542
+ conversationId: channelId,
543
+ platform: { guild_id: data.guild_id }
544
+ });
545
+ }
546
+ }
547
+ /** Add a channel to a bot. Defaults to first bot. */
548
+ addChannel(channelId, botName) {
549
+ const name = botName ?? this._botOrder[0];
550
+ if (!name) return;
551
+ const convs = this._botConversations.get(name);
552
+ if (!convs || convs.has(channelId)) return;
553
+ convs.add(channelId);
554
+ const buffers = this._botBuffers.get(name);
555
+ if (buffers && !buffers.has(channelId)) buffers.set(channelId, []);
556
+ }
557
+ /** Remove a channel from a bot. */
558
+ removeChannel(channelId, botName) {
559
+ const name = botName ?? this._botOrder[0];
560
+ if (!name) return;
561
+ const convs = this._botConversations.get(name);
562
+ if (convs) convs.delete(channelId);
563
+ const buffers = this._botBuffers.get(name);
564
+ if (buffers) buffers.delete(channelId);
565
+ }
566
+ /** Add a message to the ring buffer directly. Public for testing/conformance. */
567
+ _addToBuffer(channelId, msg) {
568
+ const botName = this._botOrder[0] ?? "default";
569
+ const convs = this._botConversations.get(botName);
570
+ if (convs && !convs.has(channelId)) convs.add(channelId);
571
+ let botBuffers = this._botBuffers.get(botName);
572
+ if (!botBuffers) {
573
+ botBuffers = /* @__PURE__ */ new Map();
574
+ this._botBuffers.set(botName, botBuffers);
575
+ }
576
+ let buffer = botBuffers.get(channelId);
577
+ if (!buffer) {
578
+ buffer = [];
579
+ botBuffers.set(channelId, buffer);
580
+ }
581
+ buffer.push({
582
+ id: msg.id,
583
+ text: msg.content,
584
+ from: {
585
+ id: msg.author.id,
586
+ name: msg.author.username
587
+ },
588
+ timestamp: msg.timestamp ? Math.floor(new Date(msg.timestamp).getTime() / 1e3) : Math.floor(Date.now() / 1e3),
589
+ conversationId: channelId,
590
+ platform: {}
591
+ });
592
+ while (buffer.length > this._bufferSize) buffer.shift();
593
+ }
594
+ };
595
+ __decorate([Actions("/")], AFSDiscord.prototype, "listRootActions", null);
596
+ __decorate([Actions.Exec("/", "start")], AFSDiscord.prototype, "execStart", null);
597
+ __decorate([Actions.Exec("/", "stop")], AFSDiscord.prototype, "execStop", null);
598
+ __decorate([Actions.Exec("/", "process-event")], AFSDiscord.prototype, "execProcessEvent", null);
599
+
600
+ //#endregion
601
+ export { AFSDiscord };
602
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/client.ts","../src/index.ts"],"sourcesContent":["/**\n * DiscordClient — pure Discord REST API wrapper.\n *\n * Zero external dependencies (uses native fetch).\n * No AFS or application-specific concepts.\n * Gateway WebSocket is handled by the provider, not the client.\n */\n\nconst DEFAULT_API_BASE = \"https://discord.com/api/v10\";\nconst DEFAULT_TIMEOUT = 30_000;\n\nexport interface DiscordClientOptions {\n token: string;\n apiBase?: string;\n timeout?: number;\n}\n\nexport interface SendMessageOptions {\n /** Reply to a specific message */\n replyTo?: string;\n /** Embed objects */\n embeds?: unknown[];\n}\n\n/** Discord message object (subset). */\nexport interface DiscordMessage {\n id: string;\n channel_id: string;\n guild_id?: string;\n author: DiscordUser;\n content: string;\n timestamp: string;\n}\n\n/** Discord user object (subset). */\nexport interface DiscordUser {\n id: string;\n username: string;\n discriminator?: string;\n bot?: boolean;\n}\n\n/** Discord Gateway event envelope. */\nexport interface GatewayPayload {\n op: number;\n d: any;\n s: number | null;\n t: string | null;\n}\n\nexport class DiscordClient {\n private readonly _token: string;\n private readonly _apiBase: string;\n private readonly _timeout: number;\n\n constructor(options: DiscordClientOptions) {\n if (!options.token) throw new Error(\"DiscordClient requires a token\");\n this._token = options.token;\n this._apiBase = options.apiBase ?? DEFAULT_API_BASE;\n this._timeout = options.timeout ?? DEFAULT_TIMEOUT;\n }\n\n // ─── API Methods ─────────────────────────────────────────\n\n async sendMessage(\n channelId: string,\n text: string,\n options?: SendMessageOptions,\n ): Promise<DiscordMessage> {\n const body: Record<string, unknown> = { content: text };\n if (options?.replyTo) {\n body.message_reference = { message_id: options.replyTo };\n }\n if (options?.embeds) body.embeds = options.embeds;\n return this._call(\"POST\", `/channels/${channelId}/messages`, body);\n }\n\n async triggerTyping(channelId: string): Promise<void> {\n await this._call(\"POST\", `/channels/${channelId}/typing`);\n }\n\n async getGatewayUrl(): Promise<string> {\n const data = await this._call(\"GET\", \"/gateway/bot\");\n return data.url;\n }\n\n async getCurrentUser(): Promise<DiscordUser> {\n return this._call(\"GET\", \"/users/@me\");\n }\n\n // ─── Helpers ─────────────────────────────────────────────\n\n static escapeMarkdown(text: string): string {\n return text.replace(/([*_~`|\\\\])/g, \"\\\\$1\");\n }\n\n // ─── Internal ────────────────────────────────────────────\n\n private async _call(method: string, path: string, body?: Record<string, unknown>): Promise<any> {\n const headers: Record<string, string> = {\n Authorization: `Bot ${this._token}`,\n };\n const init: RequestInit = {\n method,\n headers,\n signal: AbortSignal.timeout(this._timeout),\n };\n\n if (body) {\n headers[\"Content-Type\"] = \"application/json\";\n init.body = JSON.stringify(body);\n }\n\n const response = await fetch(`${this._apiBase}${path}`, init);\n\n if (!response.ok) {\n const text = await response.text().catch(() => \"\");\n const desc = text.replace(this._token, \"***\");\n throw new Error(`Discord API error ${response.status}: ${desc}`);\n }\n\n // 204 No Content (e.g., typing indicator)\n if (response.status === 204) return undefined;\n\n return response.json();\n }\n}\n","/**\n * AFSDiscord — AFS provider for Discord Bot API.\n *\n * Extends BaseMessageProvider (Plan 9 model).\n * Path structure (from base):\n * / → List bots\n * /:botName/ctl → Bot status\n * /:botName/conversations/:convId/messages → List messages\n * /:botName/conversations/:convId/messages/:msgId → Read message\n *\n * Additional Discord actions:\n * /.actions/start → Connect to Discord Gateway\n * /.actions/stop → Disconnect from Gateway\n * /.actions/process-event → Inject event externally (webhook/interaction endpoint)\n *\n * Standard events (from base):\n * messaging:message → Inbound text message\n * messaging:command → Inbound /command (or messages starting with configurable prefix)\n *\n * Discord Gateway:\n * Requires MESSAGE_CONTENT privileged intent enabled in Discord Developer Portal.\n * Default intents: GUILDS | GUILD_MESSAGES | MESSAGE_CONTENT | DIRECT_MESSAGES\n */\n\nimport type { AFSExecResult, AFSListResult, ProviderTreeSchema } from \"@aigne/afs\";\nimport { Actions, type RouteContext } from \"@aigne/afs/provider\";\nimport type {\n BotConfig,\n BufferedMessage,\n MessageCapabilities,\n MessageSender,\n SendOptions,\n} from \"@aigne/afs-messaging\";\nimport { BaseMessageProvider } from \"@aigne/afs-messaging\";\nimport { DiscordClient, type DiscordMessage, type GatewayPayload } from \"./client.js\";\n\nexport type { DiscordClient, DiscordMessage, DiscordUser, GatewayPayload } from \"./client.js\";\n\nexport interface AFSDiscordOptions {\n /** Multi-bot config */\n bots?: Array<{\n name: string;\n token: string;\n conversations?: string[];\n apiBase?: string;\n /** Gateway intents bitmask (default: GUILDS | GUILD_MESSAGES | MESSAGE_CONTENT | DIRECT_MESSAGES = 37377) */\n intents?: number;\n }>;\n /** Single-bot shorthand: token */\n token?: string;\n /** Single-bot shorthand: channel IDs to monitor */\n channels?: string[];\n apiBase?: string;\n /** Gateway intents bitmask */\n intents?: number;\n bufferSize?: number;\n}\n\n// Discord Gateway opcodes\nconst GatewayOp = {\n DISPATCH: 0,\n HEARTBEAT: 1,\n IDENTIFY: 2,\n RESUME: 6,\n RECONNECT: 7,\n INVALID_SESSION: 9,\n HELLO: 10,\n HEARTBEAT_ACK: 11,\n} as const;\n\n// Default intents: GUILDS | GUILD_MESSAGES | MESSAGE_CONTENT | DIRECT_MESSAGES\nconst DEFAULT_INTENTS = (1 << 0) | (1 << 9) | (1 << 15) | (1 << 12); // 37377\n\ninterface GatewayState {\n ws: WebSocket | null;\n connected: boolean;\n disposed: boolean;\n sessionId: string | null;\n resumeUrl: string | null;\n seq: number | null;\n heartbeatTimer: ReturnType<typeof setInterval> | null;\n heartbeatAcked: boolean;\n backoff: number;\n intents: number;\n}\n\nexport class AFSDiscord extends BaseMessageProvider {\n static manifest() {\n return {\n name: \"discord\",\n description:\n \"Discord Bot API — bidirectional messaging via Gateway WebSocket.\\n- Real-time events via Discord Gateway with heartbeat and resume\\n- Multi-bot support with per-bot channel monitoring\\n- Path: /:bot/conversations/:channelId/messages/:msgId\",\n uriTemplate: \"discord://{token}\",\n category: \"messaging\",\n schema: {\n type: \"object\",\n properties: {\n token: { type: \"string\", description: \"Discord Bot token\", sensitive: true },\n channels: {\n type: \"array\",\n items: { type: \"string\" },\n description: \"Channel IDs to monitor\",\n },\n },\n required: [\"token\"],\n },\n tags: [\"discord\", \"messaging\", \"chat\", \"bot\"],\n capabilityTags: [\n \"read-write\",\n \"crud\",\n \"search\",\n \"auth:token\",\n \"remote\",\n \"http\",\n \"real-time\",\n \"rate-limited\",\n ],\n security: {\n riskLevel: \"external\",\n resourceAccess: [\"internet\"],\n notes: [\n \"Connects to Discord API + Gateway — requires bot token with MESSAGE_CONTENT intent\",\n ],\n },\n capabilities: {\n network: { egress: true, allowedDomains: [\"discord.com\", \"gateway.discord.gg\"] },\n },\n };\n }\n\n static treeSchema(): ProviderTreeSchema {\n return {\n operations: [\"list\", \"read\", \"exec\", \"stat\", \"explain\"],\n tree: {\n \"/\": {\n kind: \"messaging:root\",\n operations: [\"list\", \"exec\"],\n actions: [\"add-bot\", \"remove-bot\", \"start\", \"stop\", \"process-event\"],\n },\n \"/{bot}\": { kind: \"messaging:bot\", operations: [\"list\", \"read\"] },\n \"/{bot}/ctl\": { kind: \"messaging:status\", operations: [\"read\"] },\n \"/{bot}/conversations\": { kind: \"messaging:conversations\", operations: [\"list\"] },\n \"/{bot}/conversations/{convId}\": { kind: \"messaging:conversation\", operations: [\"list\"] },\n \"/{bot}/conversations/{convId}/messages\": {\n kind: \"messaging:messages\",\n operations: [\"list\", \"exec\"],\n actions: [\"send\"],\n },\n \"/{bot}/conversations/{convId}/messages/{msgId}\": {\n kind: \"messaging:message\",\n operations: [\"read\"],\n },\n },\n auth: { type: \"token\", env: [\"DISCORD_BOT_TOKEN\"] },\n bestFor: [\"community chat\", \"bot commands\", \"server notifications\"],\n notFor: [\"file storage\", \"database queries\"],\n };\n }\n\n readonly providerName = \"discord\";\n readonly eventPrefix = \"discord\";\n\n private readonly _gatewayStates = new Map<string, GatewayState>();\n\n constructor(options: AFSDiscordOptions) {\n let bots: BotConfig[];\n if (options.bots) {\n bots = options.bots;\n for (const bot of bots) {\n if (!bot.token) throw new Error(`AFSDiscord bot \"${bot.name}\" requires a token`);\n }\n } else {\n if (!options.token) throw new Error(\"AFSDiscord requires a token\");\n bots = [\n {\n name: \"default\",\n token: options.token,\n conversations: options.channels ?? [],\n apiBase: options.apiBase,\n intents: options.intents,\n },\n ];\n }\n\n super({ bots, bufferSize: options.bufferSize });\n\n for (const bot of bots) {\n this._gatewayStates.set(bot.name, {\n ws: null,\n connected: false,\n disposed: false,\n sessionId: null,\n resumeUrl: null,\n seq: null,\n heartbeatTimer: null,\n heartbeatAcked: true,\n backoff: 1000,\n intents: (bot.intents as number) ?? DEFAULT_INTENTS,\n });\n }\n }\n\n // ─── Abstract Implementation ─────────────────────────\n\n getMessageCapabilities(): MessageCapabilities {\n return {\n formats: {\n send: [\"text\", \"markdown\"],\n receive: [\"text\"],\n },\n maxMessageLength: 2000,\n features: {\n edit: false,\n delete: false,\n reply: true,\n thread: true,\n reaction: true,\n inlineKeyboard: false,\n },\n limits: {\n messagesPerSecond: 5,\n },\n };\n }\n\n createBotClient(config: BotConfig): DiscordClient {\n return new DiscordClient({\n token: config.token as string,\n apiBase: config.apiBase as string | undefined,\n });\n }\n\n async sendMessage(\n client: unknown,\n convId: string,\n text: string,\n _opts?: SendOptions,\n ): Promise<{ messageId: string }> {\n const dc = client as DiscordClient;\n const result = await dc.sendMessage(convId, text);\n return { messageId: result.id };\n }\n\n async sendTypingIndicator(client: unknown, convId: string): Promise<void> {\n const dc = client as DiscordClient;\n await dc.triggerTyping(convId);\n }\n\n normalizeMessage(raw: Record<string, unknown>): BufferedMessage {\n const msg = raw as unknown as DiscordMessage;\n return {\n id: msg.id ?? \"0\",\n text: String(msg.content ?? \"\"),\n from: this.normalizeSender(msg.author as unknown as Record<string, unknown>),\n timestamp: msg.timestamp\n ? Math.floor(new Date(msg.timestamp).getTime() / 1000)\n : Math.floor(Date.now() / 1000),\n conversationId: String(msg.channel_id ?? \"\"),\n platform: { guild_id: msg.guild_id },\n };\n }\n\n normalizeSender(raw: Record<string, unknown>): MessageSender {\n return {\n id: String(raw.id ?? \"0\"),\n name: String(raw.username ?? raw.name ?? \"\"),\n isBot: raw.bot as boolean | undefined,\n };\n }\n\n // ─── Lifecycle ─────────────────────────────────────────\n\n start(botName?: string): void {\n const names = botName ? [botName] : [...this._gatewayStates.keys()];\n for (const name of names) {\n const state = this._gatewayStates.get(name);\n if (!state || state.connected || state.disposed) continue;\n this._connectGateway(name);\n }\n }\n\n stop(botName?: string): void {\n const names = botName ? [botName] : [...this._gatewayStates.keys()];\n for (const name of names) {\n this._disconnectGateway(name);\n }\n }\n\n dispose(): void {\n for (const name of this._gatewayStates.keys()) {\n const state = this._gatewayStates.get(name)!;\n state.disposed = true;\n this._disconnectGateway(name);\n }\n }\n\n protected onBotAdded(name: string): void {\n if (!this._gatewayStates.has(name)) {\n this._gatewayStates.set(name, {\n ws: null,\n connected: false,\n disposed: false,\n sessionId: null,\n resumeUrl: null,\n seq: null,\n heartbeatTimer: null,\n heartbeatAcked: true,\n backoff: 1000,\n intents: DEFAULT_INTENTS,\n });\n }\n }\n\n protected onBotRemoved(name: string): void {\n this._disconnectGateway(name);\n this._gatewayStates.delete(name);\n }\n\n // ─── Discord-specific Actions ─────────────────────────\n\n @Actions(\"/\")\n async listRootActions(_ctx: RouteContext): Promise<AFSListResult> {\n return {\n data: [\n this.buildEntry(\"/.actions/add-bot\", {\n meta: { description: \"Add a bot instance at runtime\" },\n }),\n this.buildEntry(\"/.actions/remove-bot\", {\n meta: { description: \"Remove a bot instance\" },\n }),\n this.buildEntry(\"/.actions/start\", {\n meta: { description: \"Connect to Discord Gateway\" },\n }),\n this.buildEntry(\"/.actions/stop\", {\n meta: { description: \"Disconnect from Gateway\" },\n }),\n this.buildEntry(\"/.actions/process-event\", {\n meta: { description: \"Inject a Discord event externally\" },\n }),\n ],\n };\n }\n\n @Actions.Exec(\"/\", \"start\")\n async execStart(_ctx: RouteContext): Promise<AFSExecResult> {\n this.start();\n return { success: true, data: { ok: true, gateway: true } };\n }\n\n @Actions.Exec(\"/\", \"stop\")\n async execStop(_ctx: RouteContext): Promise<AFSExecResult> {\n this.stop();\n return { success: true, data: { ok: true, gateway: false } };\n }\n\n @Actions.Exec(\"/\", \"process-event\")\n async execProcessEvent(\n _ctx: RouteContext,\n args: Record<string, unknown>,\n ): Promise<AFSExecResult> {\n this._processEvent(args);\n return { success: true, data: { ok: true } };\n }\n\n // ─── Gateway Connection ──────────────────────────────\n\n private async _connectGateway(botName: string): Promise<void> {\n const state = this._gatewayStates.get(botName);\n const client = this._getClient(botName) as DiscordClient | undefined;\n if (!state || !client || state.disposed) return;\n\n try {\n // Get Gateway URL (use resume URL if available, otherwise fetch fresh)\n let gatewayUrl = state.resumeUrl;\n if (!gatewayUrl) {\n gatewayUrl = await client.getGatewayUrl();\n }\n\n const wsUrl = `${gatewayUrl}/?v=10&encoding=json`;\n const ws = new WebSocket(wsUrl);\n state.ws = ws;\n\n ws.onmessage = (event) => {\n try {\n const payload: GatewayPayload = JSON.parse(String(event.data));\n this._handleGatewayPayload(botName, payload);\n } catch {\n // Ignore malformed payloads\n }\n };\n\n ws.onclose = () => {\n this._cleanupGateway(botName);\n if (!state.disposed) {\n // Reconnect with backoff\n setTimeout(() => this._connectGateway(botName), state.backoff);\n state.backoff = Math.min(state.backoff * 2, 60_000);\n }\n };\n\n ws.onerror = () => {\n // onclose will fire after onerror\n };\n } catch {\n if (!state.disposed) {\n setTimeout(() => this._connectGateway(botName), state.backoff);\n state.backoff = Math.min(state.backoff * 2, 60_000);\n }\n }\n }\n\n private _handleGatewayPayload(botName: string, payload: GatewayPayload): void {\n const state = this._gatewayStates.get(botName);\n if (!state) return;\n\n if (payload.s !== null) {\n state.seq = payload.s;\n }\n\n switch (payload.op) {\n case GatewayOp.HELLO: {\n const interval = payload.d?.heartbeat_interval ?? 41250;\n this._startHeartbeat(botName, interval);\n // Identify or Resume\n if (state.sessionId) {\n this._sendResume(botName);\n } else {\n this._sendIdentify(botName);\n }\n break;\n }\n\n case GatewayOp.HEARTBEAT_ACK:\n state.heartbeatAcked = true;\n break;\n\n case GatewayOp.DISPATCH:\n this._handleDispatch(botName, payload.t!, payload.d);\n break;\n\n case GatewayOp.RECONNECT:\n // Server requests reconnect\n state.ws?.close();\n break;\n\n case GatewayOp.INVALID_SESSION: {\n const resumable = payload.d === true;\n if (!resumable) {\n state.sessionId = null;\n state.seq = null;\n }\n // Wait 1-5s then reconnect (per Discord docs)\n setTimeout(\n () => {\n state.ws?.close();\n },\n 1000 + Math.random() * 4000,\n );\n break;\n }\n }\n }\n\n private _handleDispatch(botName: string, eventType: string, data: any): void {\n const state = this._gatewayStates.get(botName);\n if (!state) return;\n\n if (eventType === \"READY\") {\n state.connected = true;\n state.sessionId = data.session_id;\n state.resumeUrl = data.resume_gateway_url ?? null;\n state.backoff = 1000;\n return;\n }\n\n if (eventType === \"RESUMED\") {\n state.connected = true;\n state.backoff = 1000;\n return;\n }\n\n if (eventType === \"MESSAGE_CREATE\") {\n // Ignore bot messages\n if (data.author?.bot) return;\n\n const channelId = String(data.channel_id ?? \"\");\n const text = String(data.content ?? \"\").trim();\n if (!text) return;\n\n this.emitMessageReceived(botName, {\n id: String(data.id),\n text,\n from: this.normalizeSender({\n id: data.author?.id,\n username: data.author?.username,\n bot: data.author?.bot,\n }),\n timestamp: data.timestamp\n ? Math.floor(new Date(data.timestamp).getTime() / 1000)\n : Math.floor(Date.now() / 1000),\n conversationId: channelId,\n platform: { guild_id: data.guild_id },\n });\n }\n }\n\n private _sendIdentify(botName: string): void {\n const state = this._gatewayStates.get(botName);\n if (!state?.ws) return;\n\n const botConfig = this._pendingBots?.find((b: BotConfig) => b.name === botName);\n const token = botConfig?.token as string;\n if (!token) return;\n\n state.ws.send(\n JSON.stringify({\n op: GatewayOp.IDENTIFY,\n d: {\n token,\n intents: state.intents,\n properties: {\n os: \"linux\",\n browser: \"afs-discord\",\n device: \"afs-discord\",\n },\n },\n }),\n );\n }\n\n private _sendResume(botName: string): void {\n const state = this._gatewayStates.get(botName);\n if (!state?.ws) return;\n\n const botConfig = this._pendingBots?.find((b: BotConfig) => b.name === botName);\n const token = botConfig?.token as string;\n if (!token) return;\n\n state.ws.send(\n JSON.stringify({\n op: GatewayOp.RESUME,\n d: {\n token,\n session_id: state.sessionId,\n seq: state.seq,\n },\n }),\n );\n }\n\n private _startHeartbeat(botName: string, intervalMs: number): void {\n const state = this._gatewayStates.get(botName);\n if (!state) return;\n\n this._stopHeartbeat(botName);\n state.heartbeatAcked = true;\n\n state.heartbeatTimer = setInterval(() => {\n if (!state.heartbeatAcked) {\n // Missed ACK — zombied connection, reconnect\n state.ws?.close();\n return;\n }\n state.heartbeatAcked = false;\n state.ws?.send(JSON.stringify({ op: GatewayOp.HEARTBEAT, d: state.seq }));\n }, intervalMs);\n }\n\n private _stopHeartbeat(botName: string): void {\n const state = this._gatewayStates.get(botName);\n if (state?.heartbeatTimer) {\n clearInterval(state.heartbeatTimer);\n state.heartbeatTimer = null;\n }\n }\n\n private _disconnectGateway(botName: string): void {\n const state = this._gatewayStates.get(botName);\n if (!state) return;\n this._stopHeartbeat(botName);\n if (state.ws) {\n state.ws.onclose = null;\n state.ws.onmessage = null;\n state.ws.onerror = null;\n state.ws.close();\n state.ws = null;\n }\n state.connected = false;\n }\n\n private _cleanupGateway(botName: string): void {\n const state = this._gatewayStates.get(botName);\n if (!state) return;\n this._stopHeartbeat(botName);\n state.ws = null;\n state.connected = false;\n }\n\n // ─── External Event Injection ──────────────────────────\n\n /** Process an externally-provided Discord event (e.g., from interaction endpoint). Public for testing. */\n _processEvent(payload: Record<string, unknown>): void {\n const botName = (payload.botName as string) ?? this._botOrder[0] ?? \"default\";\n const type = payload.t as string;\n const data = (payload.d ?? payload) as any;\n\n if (type === \"MESSAGE_CREATE\" || data.content !== undefined) {\n if (data.author?.bot) return;\n const channelId = String(data.channel_id ?? \"\");\n const text = String(data.content ?? \"\").trim();\n if (!text) return;\n\n this.emitMessageReceived(botName, {\n id: String(data.id ?? Date.now()),\n text,\n from: this.normalizeSender({\n id: data.author?.id ?? \"0\",\n username: data.author?.username ?? \"\",\n bot: data.author?.bot,\n }),\n timestamp: data.timestamp\n ? Math.floor(new Date(data.timestamp).getTime() / 1000)\n : Math.floor(Date.now() / 1000),\n conversationId: channelId,\n platform: { guild_id: data.guild_id },\n });\n }\n }\n\n // ─── Convenience Methods (test helpers) ─────────────\n\n /** Add a channel to a bot. Defaults to first bot. */\n addChannel(channelId: string, botName?: string): void {\n const name = botName ?? this._botOrder[0];\n if (!name) return;\n const convs = this._botConversations.get(name);\n if (!convs || convs.has(channelId)) return;\n convs.add(channelId);\n const buffers = this._botBuffers.get(name);\n if (buffers && !buffers.has(channelId)) {\n buffers.set(channelId, []);\n }\n }\n\n /** Remove a channel from a bot. */\n removeChannel(channelId: string, botName?: string): void {\n const name = botName ?? this._botOrder[0];\n if (!name) return;\n const convs = this._botConversations.get(name);\n if (convs) convs.delete(channelId);\n const buffers = this._botBuffers.get(name);\n if (buffers) buffers.delete(channelId);\n }\n\n /** Add a message to the ring buffer directly. Public for testing/conformance. */\n _addToBuffer(\n channelId: string,\n msg: {\n id: string;\n content: string;\n author: { id: string; username: string };\n timestamp?: string;\n },\n ): void {\n const botName = this._botOrder[0] ?? \"default\";\n\n const convs = this._botConversations.get(botName);\n if (convs && !convs.has(channelId)) {\n convs.add(channelId);\n }\n\n let botBuffers = this._botBuffers.get(botName);\n if (!botBuffers) {\n botBuffers = new Map();\n this._botBuffers.set(botName, botBuffers);\n }\n let buffer = botBuffers.get(channelId);\n if (!buffer) {\n buffer = [];\n botBuffers.set(channelId, buffer);\n }\n buffer.push({\n id: msg.id,\n text: msg.content,\n from: { id: msg.author.id, name: msg.author.username },\n timestamp: msg.timestamp\n ? Math.floor(new Date(msg.timestamp).getTime() / 1000)\n : Math.floor(Date.now() / 1000),\n conversationId: channelId,\n platform: {},\n });\n while (buffer.length > this._bufferSize) {\n buffer.shift();\n }\n }\n}\n"],"mappings":"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"}