@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/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;
|