@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 +26 -0
- package/dist/index.cjs +601 -0
- package/dist/index.d.cts +167 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +167 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +602 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +56 -0
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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"}
|