@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/LICENSE.md ADDED
@@ -0,0 +1,26 @@
1
+ # Proprietary License
2
+
3
+ Copyright (c) 2024-2025 ArcBlock, Inc. All Rights Reserved.
4
+
5
+ This software and associated documentation files (the "Software") are proprietary
6
+ and confidential. Unauthorized copying, modification, distribution, or use of
7
+ this Software, via any medium, is strictly prohibited.
8
+
9
+ The Software is provided for internal use only within ArcBlock, Inc. and its
10
+ authorized affiliates.
11
+
12
+ ## No License Granted
13
+
14
+ No license, express or implied, is granted to any party for any purpose.
15
+ All rights are reserved by ArcBlock, Inc.
16
+
17
+ ## Public Artifact Distribution
18
+
19
+ Portions of this Software may be released publicly under separate open-source
20
+ licenses (such as MIT License) through designated public repositories. Such
21
+ public releases are governed by their respective licenses and do not affect
22
+ the proprietary nature of this repository.
23
+
24
+ ## Contact
25
+
26
+ For licensing inquiries, contact: legal@arcblock.io
package/dist/index.cjs ADDED
@@ -0,0 +1,601 @@
1
+ let _aigne_afs_provider = require("@aigne/afs/provider");
2
+ let _aigne_afs_messaging = require("@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 _aigne_afs_messaging.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([(0, _aigne_afs_provider.Actions)("/")], AFSDiscord.prototype, "listRootActions", null);
596
+ __decorate([_aigne_afs_provider.Actions.Exec("/", "start")], AFSDiscord.prototype, "execStart", null);
597
+ __decorate([_aigne_afs_provider.Actions.Exec("/", "stop")], AFSDiscord.prototype, "execStop", null);
598
+ __decorate([_aigne_afs_provider.Actions.Exec("/", "process-event")], AFSDiscord.prototype, "execProcessEvent", null);
599
+
600
+ //#endregion
601
+ exports.AFSDiscord = AFSDiscord;