@zhin.js/adapter-discord 1.1.18 → 1.1.19
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/CHANGELOG.md +39 -0
- package/README.md +10 -3
- package/adapters/discord/index.js +3 -23
- package/adapters/discord/index.ts +2 -27
- package/agents/discord/skills/discord/tools/send_embed/index.js +1 -1
- package/agents/discord/skills/discord/tools/send_embed/index.ts +1 -1
- package/agents/discord/skills/discord-guild/tools/create_thread/index.js +6 -2
- package/agents/discord/skills/discord-guild/tools/create_thread/index.ts +5 -2
- package/agents/discord/skills/discord-guild/tools/forum_post/index.js +1 -1
- package/agents/discord/skills/discord-guild/tools/forum_post/index.ts +1 -1
- package/commands/discord/endpoint/definition.js +0 -2
- package/commands/discord/endpoint/definition.ts +0 -2
- package/lib/adapter.d.ts +14 -0
- package/lib/adapter.js +55 -0
- package/lib/client.d.ts +9 -9
- package/lib/client.js +0 -2
- package/lib/console-spec.js +9 -0
- package/lib/console.d.ts +1 -7
- package/lib/console.js +2 -5
- package/lib/endpoint.d.ts +36 -32
- package/lib/endpoint.js +174 -170
- package/lib/gateway.d.ts +6 -124
- package/lib/gateway.js +88 -63
- package/lib/index.d.ts +4 -3
- package/lib/index.js +3 -2
- package/lib/protocol.d.ts +4 -0
- package/lib/protocol.js +1 -0
- package/lib/webhook.d.ts +2 -3
- package/lib/webhook.js +42 -30
- package/package.json +18 -18
- package/plugin.js +1 -4
- package/src/adapter.ts +38 -0
- package/src/client.ts +6 -12
- package/src/console-spec.ts +9 -0
- package/src/console.ts +2 -6
- package/src/endpoint.ts +197 -172
- package/src/gateway.ts +103 -151
- package/src/index.ts +2 -3
- package/src/protocol.ts +5 -0
- package/src/webhook.ts +42 -32
- package/lib/discord-runtime-state.d.ts +0 -1
- package/lib/discord-runtime-state.js +0 -6
- package/src/discord-runtime-state.ts +0 -7
package/lib/endpoint.js
CHANGED
|
@@ -1,124 +1,103 @@
|
|
|
1
|
-
import { createDiscordRestApiAgent } from './rest-api-proxy.js';
|
|
2
1
|
/**
|
|
3
2
|
* DiscordEndpoint — lifecycle, outbound, admit, gateway / interactions modes, agent tool surface.
|
|
4
3
|
*/
|
|
5
|
-
import { ChannelType } from 'discord.js';
|
|
6
|
-
import {
|
|
4
|
+
import { Client, ChannelType } from 'discord.js';
|
|
5
|
+
import { createEndpointSpecification, endpointSpec, createEndpointLifecycle, createEndpointOperation, } from 'zhin.js/adapter';
|
|
7
6
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
8
|
-
import { connectDiscordGatewayClient,
|
|
7
|
+
import { connectDiscordGatewayClient, discordClientOptions, DEFAULT_INTENTS, resolveSenderRole, toMessageCreateOptions, } from './gateway.js';
|
|
9
8
|
import { discordInboundConversation, formatButtonContent, formatButtonSegments, formatInboundContent, formatInboundSegments, formatOutboundBody, senderDisplayName, } from './protocol.js';
|
|
10
9
|
import { registerDiscordInteractionRoutes } from './webhook.js';
|
|
11
10
|
import { receiveDiscordGuildMemberSideEvent } from './side-event-dispatch.js';
|
|
12
11
|
const DISCORD_API = 'https://discord.com/api/v10';
|
|
13
12
|
/** 出站 HTTP 调用统一 30s 超时。 */
|
|
14
13
|
const OUTBOUND_TIMEOUT_MS = 30_000;
|
|
15
|
-
export class DiscordGatewayEndpoint extends
|
|
14
|
+
export class DiscordGatewayEndpoint extends Client {
|
|
15
|
+
[endpointSpec];
|
|
16
16
|
#logger;
|
|
17
17
|
#options;
|
|
18
|
-
#createClient;
|
|
19
18
|
#fetch;
|
|
20
|
-
#
|
|
19
|
+
#connection;
|
|
20
|
+
#sdkDisposal;
|
|
21
21
|
#open = false;
|
|
22
22
|
#lifecycle = createEndpointLifecycle({ name: 'discord', reconnect: false });
|
|
23
23
|
#cleanup;
|
|
24
24
|
#retiring;
|
|
25
|
-
management = createDiscordEndpointManagement(() => this.#requireClient());
|
|
26
|
-
control = Object.freeze({
|
|
27
|
-
recall:
|
|
28
|
-
addReaction:
|
|
29
|
-
const channelId = hint?.channelId ?? message.conversation.id;
|
|
30
|
-
if (!channelId || !message.id)
|
|
31
|
-
return null;
|
|
32
|
-
await this.#addReaction(channelId, message.id, emoji);
|
|
33
|
-
return emoji;
|
|
34
|
-
},
|
|
25
|
+
#management = createDiscordEndpointManagement(() => this.#requireClient());
|
|
26
|
+
#control = Object.freeze({
|
|
27
|
+
recall: request => this.#recallMessage(request),
|
|
28
|
+
addReaction: request => this.#addReaction(request),
|
|
35
29
|
});
|
|
36
|
-
content = Object.freeze({
|
|
30
|
+
#content = Object.freeze({
|
|
37
31
|
resolve: (reference, context) => resolveDiscordContent(this.#fetch, this.#options.config.token, reference, context),
|
|
38
32
|
});
|
|
39
33
|
constructor(options) {
|
|
40
|
-
super();
|
|
34
|
+
super(discordClientOptions(options.config.intents?.length ? options.config.intents : DEFAULT_INTENTS, options.config.gatewayFaultProxyUrl, options.restApiAgent ? { agent: options.restApiAgent } : {}));
|
|
41
35
|
this.#logger = getAdapterLogger('discord', options.config.id);
|
|
42
36
|
this.#options = options;
|
|
43
|
-
this.#createClient = options.createClient ?? defaultCreateClient;
|
|
44
37
|
this.#fetch = options.fetch ?? globalThis.fetch;
|
|
38
|
+
const endpoint = this;
|
|
39
|
+
this[endpointSpec] = createEndpointSpecification(this, {
|
|
40
|
+
start: signal => this.#startFramework(signal), open: () => this.#openFramework(), close: () => this.#closeFramework(),
|
|
41
|
+
stop: () => this.#stopFramework(), send: request => this.#sendFramework(request),
|
|
42
|
+
get name() { return options.config.id; }, get transportState() { return endpoint.#transportState; },
|
|
43
|
+
management: this.#management, control: this.#control, content: this.#content,
|
|
44
|
+
});
|
|
45
45
|
}
|
|
46
|
-
|
|
47
|
-
get client() {
|
|
48
|
-
return this.#requireClient();
|
|
49
|
-
}
|
|
50
|
-
get transportState() {
|
|
46
|
+
get #transportState() {
|
|
51
47
|
if (this.#lifecycle.state !== 'open')
|
|
52
48
|
return this.#lifecycle.state;
|
|
53
|
-
if (!this.#
|
|
49
|
+
if (!this.#connection)
|
|
54
50
|
return 'closed';
|
|
55
|
-
|
|
56
|
-
return this.#lifecycle.state;
|
|
57
|
-
return this.#client.isReady() ? 'open' : 'reconnecting';
|
|
51
|
+
return this.isReady() ? 'open' : 'reconnecting';
|
|
58
52
|
}
|
|
59
|
-
async
|
|
53
|
+
async #startFramework(executionSignal) {
|
|
60
54
|
await this.#retiring;
|
|
61
55
|
await this.#lifecycle.start(async (handle) => {
|
|
62
56
|
let cleanup;
|
|
63
57
|
let cancelled = false;
|
|
64
58
|
try {
|
|
65
|
-
const
|
|
66
|
-
? [...this.#options.config.intents]
|
|
67
|
-
: DEFAULT_INTENTS;
|
|
68
|
-
const restAgent = this.#options.config.restApiProxy ? createDiscordRestApiAgent(this.#options.config.restApiProxy) : undefined;
|
|
69
|
-
let client;
|
|
70
|
-
try {
|
|
71
|
-
client = this.#createClient(intents, this.#options.config.gatewayFaultProxyUrl, restAgent ? { agent: restAgent } : undefined);
|
|
72
|
-
}
|
|
73
|
-
catch (error) {
|
|
74
|
-
await restAgent?.destroyProxy();
|
|
75
|
-
throw error;
|
|
76
|
-
}
|
|
77
|
-
this.#client = client;
|
|
59
|
+
const client = this;
|
|
78
60
|
const abort = new AbortController();
|
|
79
61
|
let stopping;
|
|
62
|
+
this.#connection = abort;
|
|
80
63
|
cleanup = () => {
|
|
81
64
|
cancelled = true;
|
|
82
65
|
abort.abort();
|
|
83
|
-
if (this.#
|
|
84
|
-
this.#
|
|
85
|
-
return stopping ??= (
|
|
86
|
-
await client.destroy();
|
|
87
|
-
}
|
|
88
|
-
finally {
|
|
89
|
-
await restAgent?.destroyProxy();
|
|
90
|
-
} })();
|
|
66
|
+
if (this.#connection === abort)
|
|
67
|
+
this.#connection = undefined;
|
|
68
|
+
return stopping ??= this.#disposeSdk();
|
|
91
69
|
};
|
|
92
70
|
this.#cleanup = cleanup;
|
|
93
71
|
handle.onForceClose(() => { void cleanup?.().catch(() => { }); });
|
|
94
72
|
await connectDiscordGatewayClient(client, this.#options.config, {
|
|
73
|
+
receive: action => this[endpointSpec].events.run(action),
|
|
95
74
|
onPlatformEvent: (name, event) => {
|
|
96
|
-
if (this.#
|
|
75
|
+
if (this.#connection !== abort)
|
|
97
76
|
return;
|
|
98
77
|
void this.#emitPlatformEvent(name, event);
|
|
99
78
|
},
|
|
100
|
-
onMessage: (msg) => { if (this.#
|
|
79
|
+
onMessage: (msg) => { if (this.#connection === abort)
|
|
101
80
|
return this.admit(msg); },
|
|
102
|
-
onButton: (interaction) => { if (this.#
|
|
81
|
+
onButton: (interaction) => { if (this.#connection === abort)
|
|
103
82
|
return this.admitButton(interaction); },
|
|
104
83
|
onGuildMemberAdd: (member) => {
|
|
105
|
-
if (this.#
|
|
84
|
+
if (this.#connection !== abort)
|
|
106
85
|
return;
|
|
107
|
-
receiveDiscordGuildMemberSideEvent((name, payload) => this.emit(name, payload), this.#options.config.id, 'member_increase', member, this.#logger);
|
|
86
|
+
receiveDiscordGuildMemberSideEvent((name, payload) => this[endpointSpec].events.emit(name, payload), this.#options.config.id, 'member_increase', member, this.#logger);
|
|
108
87
|
},
|
|
109
88
|
onGuildMemberRemove: (member) => {
|
|
110
|
-
if (this.#
|
|
89
|
+
if (this.#connection !== abort)
|
|
111
90
|
return;
|
|
112
|
-
receiveDiscordGuildMemberSideEvent((name, payload) => this.emit(name, payload), this.#options.config.id, 'member_decrease', member, this.#logger);
|
|
91
|
+
receiveDiscordGuildMemberSideEvent((name, payload) => this[endpointSpec].events.emit(name, payload), this.#options.config.id, 'member_decrease', member, this.#logger);
|
|
113
92
|
},
|
|
114
|
-
}, abort.signal);
|
|
115
|
-
if (abort.signal.aborted || this.#
|
|
93
|
+
}, AbortSignal.any([abort.signal, executionSignal]));
|
|
94
|
+
if (abort.signal.aborted || this.#connection !== abort)
|
|
116
95
|
return;
|
|
117
96
|
this.#logger.info(formatCompact({
|
|
118
97
|
op: 'connect',
|
|
119
98
|
endpoint: this.#options.config.id,
|
|
120
99
|
mode: 'gateway',
|
|
121
|
-
user: this
|
|
100
|
+
user: this.user?.tag,
|
|
122
101
|
}));
|
|
123
102
|
}
|
|
124
103
|
catch (error) {
|
|
@@ -134,30 +113,27 @@ export class DiscordGatewayEndpoint extends Endpoint {
|
|
|
134
113
|
}
|
|
135
114
|
});
|
|
136
115
|
}
|
|
137
|
-
|
|
116
|
+
#openFramework() {
|
|
138
117
|
this.#open = true;
|
|
139
118
|
}
|
|
140
|
-
|
|
119
|
+
#closeFramework() {
|
|
141
120
|
this.#open = false;
|
|
142
121
|
}
|
|
143
|
-
async
|
|
122
|
+
async #stopFramework() {
|
|
144
123
|
this.#open = false;
|
|
145
124
|
const cleanup = this.#cleanup;
|
|
146
125
|
this.#cleanup = undefined;
|
|
147
126
|
this.#lifecycle.stop();
|
|
148
|
-
const retiring = this.#retiring ?? Promise.resolve().then(
|
|
149
|
-
await cleanup?.();
|
|
150
|
-
}
|
|
151
|
-
catch { /* best-effort SDK shutdown */ } });
|
|
127
|
+
const retiring = this.#retiring ?? Promise.resolve().then(() => cleanup ? cleanup() : this.#disposeSdk());
|
|
152
128
|
this.#retiring = retiring;
|
|
153
129
|
await retiring;
|
|
154
130
|
if (this.#retiring === retiring)
|
|
155
131
|
this.#retiring = undefined;
|
|
156
132
|
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
157
133
|
}
|
|
158
|
-
async
|
|
134
|
+
async #sendFramework({ conversation, payload, signal }) {
|
|
159
135
|
const body = formatOutboundBody(payload);
|
|
160
|
-
const snowflake = await this.#sendBody(conversation.id, body);
|
|
136
|
+
const snowflake = await this.#sendBody({ channelId: conversation.id, body, signal });
|
|
161
137
|
this.#logger.debug(formatCompact({
|
|
162
138
|
op: 'discord_send',
|
|
163
139
|
endpoint: this.#options.config.id,
|
|
@@ -166,14 +142,15 @@ export class DiscordGatewayEndpoint extends Endpoint {
|
|
|
166
142
|
}));
|
|
167
143
|
return snowflake;
|
|
168
144
|
}
|
|
169
|
-
async recallMessage(message) {
|
|
145
|
+
async #recallMessage({ message, signal }) {
|
|
170
146
|
if (!message.id)
|
|
171
147
|
return;
|
|
172
|
-
const
|
|
148
|
+
const operation = createEndpointOperation({ signal });
|
|
149
|
+
const channel = await operation.prepare(() => this.#requireClient().channels.fetch(message.conversation.id));
|
|
173
150
|
if (!channel?.isTextBased() || !channel.messages)
|
|
174
151
|
return;
|
|
175
|
-
const msg = await channel.messages.fetch(message.id);
|
|
176
|
-
await msg.delete();
|
|
152
|
+
const msg = await operation.prepare(() => channel.messages.fetch(message.id));
|
|
153
|
+
await operation.run(() => msg.delete());
|
|
177
154
|
}
|
|
178
155
|
/** Test / internal: admit a message when open. */
|
|
179
156
|
admit(msg) {
|
|
@@ -182,8 +159,10 @@ export class DiscordGatewayEndpoint extends Endpoint {
|
|
|
182
159
|
if (msg.authorBot)
|
|
183
160
|
return;
|
|
184
161
|
const conversation = discordInboundConversation(String(this.#options.id), msg);
|
|
185
|
-
void this.emit('message.receive', {
|
|
162
|
+
void this[endpointSpec].events.emit('message.receive', {
|
|
186
163
|
conversation,
|
|
164
|
+
event: msg.event,
|
|
165
|
+
platformSentAt: msg.platformSentAt,
|
|
187
166
|
message: { conversation, id: msg.id },
|
|
188
167
|
content: formatInboundContent(msg),
|
|
189
168
|
segments: formatInboundSegments(msg),
|
|
@@ -214,7 +193,9 @@ export class DiscordGatewayEndpoint extends Endpoint {
|
|
|
214
193
|
if (!this.#open)
|
|
215
194
|
return;
|
|
216
195
|
const conversation = discordInboundConversation(String(this.#options.id), interaction);
|
|
217
|
-
void this.emit('message.receive', {
|
|
196
|
+
void this[endpointSpec].events.emit('message.receive', {
|
|
197
|
+
event: interaction.event,
|
|
198
|
+
platformSentAt: interaction.platformSentAt,
|
|
218
199
|
conversation,
|
|
219
200
|
message: { conversation, id: interaction.id },
|
|
220
201
|
content: formatButtonContent(interaction),
|
|
@@ -234,16 +215,20 @@ export class DiscordGatewayEndpoint extends Endpoint {
|
|
|
234
215
|
}));
|
|
235
216
|
});
|
|
236
217
|
}
|
|
237
|
-
async #addReaction(
|
|
238
|
-
const
|
|
239
|
-
if (!
|
|
218
|
+
async #addReaction({ message, emoji, hint, signal }) {
|
|
219
|
+
const channelId = hint?.channelId ?? message.conversation.id;
|
|
220
|
+
if (!channelId || !message.id)
|
|
221
|
+
return null;
|
|
222
|
+
const operation = createEndpointOperation({ signal });
|
|
223
|
+
const channel = await operation.prepare(() => this.#requireClient().channels.fetch(channelId));
|
|
224
|
+
if (!channel?.isTextBased() || !channel.messages)
|
|
240
225
|
throw new Error(`Channel ${channelId} 不是文本频道`);
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
226
|
+
const native = await operation.prepare(() => channel.messages.fetch(message.id));
|
|
227
|
+
await operation.run(() => native.react(emoji));
|
|
228
|
+
return emoji;
|
|
244
229
|
}
|
|
245
230
|
async #emitPlatformEvent(name, event) {
|
|
246
|
-
await this.
|
|
231
|
+
await this[endpointSpec].events.platform(name, event).catch((error) => {
|
|
247
232
|
this.#logger.warn(formatCompact({
|
|
248
233
|
op: 'discord_platform_event_failed',
|
|
249
234
|
event: name,
|
|
@@ -251,17 +236,18 @@ export class DiscordGatewayEndpoint extends Endpoint {
|
|
|
251
236
|
}));
|
|
252
237
|
});
|
|
253
238
|
}
|
|
254
|
-
async #sendBody(channelId, body) {
|
|
239
|
+
async #sendBody({ channelId, body, signal }) {
|
|
240
|
+
const operation = createEndpointOperation({ signal });
|
|
255
241
|
let stage = 'fetch_channel';
|
|
256
242
|
try {
|
|
257
|
-
const channel = await this.#requireClient().channels.fetch(channelId);
|
|
258
|
-
if (!channel || !channel.
|
|
243
|
+
const channel = await operation.prepare(() => this.#requireClient().channels.fetch(channelId));
|
|
244
|
+
if (!channel || !channel.isSendable()) {
|
|
259
245
|
throw new Error(`Channel ${channelId} is not a text channel`);
|
|
260
246
|
}
|
|
261
247
|
stage = 'render';
|
|
262
|
-
const options = await toMessageCreateOptions(body);
|
|
248
|
+
const options = await operation.prepare(() => toMessageCreateOptions(body));
|
|
263
249
|
stage = 'send';
|
|
264
|
-
const result = await channel.send(options);
|
|
250
|
+
const result = await operation.run(async () => channel.send(options));
|
|
265
251
|
return result.id;
|
|
266
252
|
}
|
|
267
253
|
catch (error) {
|
|
@@ -273,10 +259,22 @@ export class DiscordGatewayEndpoint extends Endpoint {
|
|
|
273
259
|
throw error;
|
|
274
260
|
}
|
|
275
261
|
}
|
|
262
|
+
#disposeSdk() {
|
|
263
|
+
this.#sdkDisposal ??= Promise.resolve().then(async () => {
|
|
264
|
+
this.removeAllListeners();
|
|
265
|
+
try {
|
|
266
|
+
await super.destroy();
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
await this.#options.restApiAgent?.destroyProxy();
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
return this.#sdkDisposal;
|
|
273
|
+
}
|
|
276
274
|
#requireClient() {
|
|
277
|
-
if (!this.#
|
|
275
|
+
if (!this.#connection)
|
|
278
276
|
throw new Error('Discord client not connected');
|
|
279
|
-
return this
|
|
277
|
+
return this;
|
|
280
278
|
}
|
|
281
279
|
}
|
|
282
280
|
/** Minimal Discord REST client used when Gateway is intentionally disabled. */
|
|
@@ -287,49 +285,55 @@ export class DiscordRestClient {
|
|
|
287
285
|
this.token = token;
|
|
288
286
|
this.fetch = fetch;
|
|
289
287
|
}
|
|
290
|
-
async request(method, path, body) {
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
|
|
288
|
+
async request({ method, path, body, signal }) {
|
|
289
|
+
const deadline = AbortSignal.timeout(OUTBOUND_TIMEOUT_MS);
|
|
290
|
+
const operationSignal = signal ? AbortSignal.any([signal, deadline]) : deadline;
|
|
291
|
+
const operation = createEndpointOperation({ signal: operationSignal });
|
|
292
|
+
const execute = async () => {
|
|
293
|
+
const response = await this.fetch(`${DISCORD_API}${path}`, {
|
|
294
|
+
method,
|
|
295
|
+
headers: { Authorization: `Bot ${this.token}`, ...(body === undefined ? {} : { 'Content-Type': 'application/json' }) },
|
|
296
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
297
|
+
signal: operationSignal,
|
|
298
|
+
});
|
|
299
|
+
const text = await response.text();
|
|
300
|
+
if (!response.ok)
|
|
301
|
+
throw new Error(`Discord API ${method} ${path} failed (${response.status}): ${text.slice(0, 200)}`);
|
|
302
|
+
return (text ? JSON.parse(text) : undefined);
|
|
303
|
+
};
|
|
304
|
+
return operation.run(execute);
|
|
305
|
+
}
|
|
306
|
+
createMessage({ channelId, body, signal }) {
|
|
307
|
+
return this.request({ method: 'POST', path: `/channels/${channelId}/messages`, body, signal });
|
|
308
|
+
}
|
|
309
|
+
deleteMessage({ channelId, messageId, signal }) {
|
|
310
|
+
return this.request({ method: 'DELETE', path: `/channels/${channelId}/messages/${messageId}`, signal });
|
|
311
311
|
}
|
|
312
312
|
}
|
|
313
|
-
export class DiscordInteractionsEndpoint extends
|
|
314
|
-
|
|
313
|
+
export class DiscordInteractionsEndpoint extends DiscordRestClient {
|
|
314
|
+
[endpointSpec];
|
|
315
315
|
#logger;
|
|
316
316
|
#options;
|
|
317
317
|
#fetch;
|
|
318
318
|
#routeReleases = [];
|
|
319
319
|
#open = false;
|
|
320
320
|
#started = false;
|
|
321
|
-
control = Object.freeze({
|
|
322
|
-
recall:
|
|
321
|
+
#control = Object.freeze({
|
|
322
|
+
recall: request => this.#recallMessage(request),
|
|
323
323
|
});
|
|
324
|
-
content = Object.freeze({
|
|
324
|
+
#content = Object.freeze({
|
|
325
325
|
resolve: (reference, context) => resolveDiscordContent(this.#fetch, this.#options.config.token, reference, context),
|
|
326
326
|
});
|
|
327
327
|
constructor(options) {
|
|
328
|
-
super();
|
|
328
|
+
super(options.config.token, options.fetch ?? globalThis.fetch);
|
|
329
329
|
this.#logger = getAdapterLogger('discord', options.config.id);
|
|
330
330
|
this.#options = options;
|
|
331
331
|
this.#fetch = options.fetch ?? globalThis.fetch;
|
|
332
|
-
this
|
|
332
|
+
this[endpointSpec] = createEndpointSpecification(this, {
|
|
333
|
+
start: () => this.#startFramework(), open: () => this.#openFramework(), close: () => this.#closeFramework(),
|
|
334
|
+
stop: () => this.#stopFramework(), send: request => this.#sendFramework(request), name: options.config.id,
|
|
335
|
+
control: this.#control, content: this.#content,
|
|
336
|
+
});
|
|
333
337
|
}
|
|
334
338
|
get isOpen() {
|
|
335
339
|
return this.#open;
|
|
@@ -337,7 +341,7 @@ export class DiscordInteractionsEndpoint extends Endpoint {
|
|
|
337
341
|
get config() {
|
|
338
342
|
return this.#options.config;
|
|
339
343
|
}
|
|
340
|
-
async
|
|
344
|
+
async #startFramework() {
|
|
341
345
|
if (this.#started)
|
|
342
346
|
return;
|
|
343
347
|
this.#started = true;
|
|
@@ -349,35 +353,37 @@ export class DiscordInteractionsEndpoint extends Endpoint {
|
|
|
349
353
|
path: this.#options.config.interactionsPath,
|
|
350
354
|
}));
|
|
351
355
|
}
|
|
352
|
-
|
|
356
|
+
#openFramework() {
|
|
353
357
|
this.#open = true;
|
|
354
358
|
}
|
|
355
|
-
|
|
359
|
+
#closeFramework() {
|
|
356
360
|
this.#open = false;
|
|
357
361
|
}
|
|
358
|
-
async
|
|
362
|
+
async #stopFramework() {
|
|
359
363
|
this.#open = false;
|
|
360
364
|
for (const release of this.#routeReleases.splice(0))
|
|
361
365
|
release();
|
|
362
366
|
this.#started = false;
|
|
363
367
|
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
364
368
|
}
|
|
365
|
-
async
|
|
369
|
+
async #sendFramework({ conversation, payload, signal }) {
|
|
366
370
|
const body = formatOutboundBody(payload);
|
|
367
|
-
const data = await this.
|
|
371
|
+
const data = await this.createMessage({ channelId: conversation.id, body, signal });
|
|
368
372
|
return data.id ?? '';
|
|
369
373
|
}
|
|
370
|
-
async recallMessage(message) {
|
|
374
|
+
async #recallMessage({ message, signal }) {
|
|
371
375
|
if (!message.id)
|
|
372
376
|
return;
|
|
373
|
-
await this.
|
|
377
|
+
await this.deleteMessage({ channelId: message.conversation.id, messageId: message.id, signal });
|
|
374
378
|
}
|
|
375
379
|
admit(msg) {
|
|
376
380
|
if (!this.#open)
|
|
377
381
|
return;
|
|
378
382
|
const conversation = discordInboundConversation(String(this.#options.id), msg);
|
|
379
|
-
void this.emit('message.receive', {
|
|
383
|
+
void this[endpointSpec].events.emit('message.receive', {
|
|
380
384
|
conversation,
|
|
385
|
+
event: msg.event,
|
|
386
|
+
platformSentAt: msg.platformSentAt,
|
|
381
387
|
message: { conversation, id: msg.id },
|
|
382
388
|
content: formatInboundContent(msg),
|
|
383
389
|
segments: formatInboundSegments(msg),
|
|
@@ -406,7 +412,9 @@ export class DiscordInteractionsEndpoint extends Endpoint {
|
|
|
406
412
|
if (!this.#open)
|
|
407
413
|
return;
|
|
408
414
|
const conversation = discordInboundConversation(String(this.#options.id), interaction);
|
|
409
|
-
void this.emit('message.receive', {
|
|
415
|
+
void this[endpointSpec].events.emit('message.receive', {
|
|
416
|
+
event: interaction.event,
|
|
417
|
+
platformSentAt: interaction.platformSentAt,
|
|
410
418
|
conversation,
|
|
411
419
|
message: { conversation, id: interaction.id },
|
|
412
420
|
content: formatButtonContent(interaction),
|
|
@@ -430,7 +438,7 @@ export class DiscordInteractionsEndpoint extends Endpoint {
|
|
|
430
438
|
if (!this.#open)
|
|
431
439
|
return;
|
|
432
440
|
const type = typeof event.type === 'number' ? `interaction.${event.type}` : 'interaction';
|
|
433
|
-
void this.
|
|
441
|
+
void this[endpointSpec].events.platform(type, event).catch((error) => {
|
|
434
442
|
this.#logger.warn(formatCompact({
|
|
435
443
|
op: 'discord_platform_event_failed',
|
|
436
444
|
event: type,
|
|
@@ -488,57 +496,53 @@ async function resolveDiscordContent(fetch, token, reference, context) {
|
|
|
488
496
|
return Object.freeze({ status: 'failed', code: 'discord_content_resolution_failed', message: error instanceof Error ? error.message : String(error) });
|
|
489
497
|
}
|
|
490
498
|
}
|
|
491
|
-
/**
|
|
492
|
-
* Discord snowflake 是 64 位整数的字符串形式,超出 Number.MAX_SAFE_INTEGER,
|
|
493
|
-
* Number() 转换会丢精度。Console 社交面只把 group_id 当 JSON 值透传、
|
|
494
|
-
* 并以字符串回传给 listGroupMembers,因此这里保留原始字符串(仅按契约
|
|
495
|
-
* 类型声明强转),是全链路最不丢信息的方案。
|
|
496
|
-
*/
|
|
497
|
-
function toGroupId(id) {
|
|
498
|
-
return id;
|
|
499
|
-
}
|
|
500
499
|
/**
|
|
501
500
|
* DiscordGatewayEndpoint 的 EndpointManagement 语义端口(参照 qq 的工厂模式)。
|
|
502
501
|
* 数据源为 discord.js SDK 缓存:guilds.cache / guild.channels.cache / guild.members。
|
|
503
502
|
*/
|
|
504
503
|
export function createDiscordEndpointManagement(requireClient) {
|
|
505
504
|
return Object.freeze({
|
|
506
|
-
async listGroups() {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
groups.push({
|
|
512
|
-
group_id: toGroupId(String(guild.id)),
|
|
513
|
-
name: String(guild.name ?? guild.id),
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
return groups;
|
|
517
|
-
},
|
|
518
|
-
async listChannels() {
|
|
519
|
-
const channels = [];
|
|
520
|
-
for (const guild of requireClient().guilds.cache.values()) {
|
|
521
|
-
if (!guild?.id)
|
|
522
|
-
continue;
|
|
523
|
-
const guildId = String(guild.id);
|
|
524
|
-
const guildName = String(guild.name ?? guildId);
|
|
525
|
-
for (const channel of guild.channels?.cache?.values() ?? []) {
|
|
526
|
-
if (!channel?.id)
|
|
505
|
+
async listGroups(request = {}) {
|
|
506
|
+
return createEndpointOperation(request).run(() => {
|
|
507
|
+
const groups = [];
|
|
508
|
+
for (const guild of requireClient().guilds.cache.values()) {
|
|
509
|
+
if (!guild?.id)
|
|
527
510
|
continue;
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
id: String(channel.id),
|
|
532
|
-
name: channel.name ? String(channel.name) : undefined,
|
|
533
|
-
parent: { type: 'guild', id: guildId, name: guildName },
|
|
511
|
+
groups.push({
|
|
512
|
+
group_id: String(guild.id),
|
|
513
|
+
name: String(guild.name ?? guild.id),
|
|
534
514
|
});
|
|
535
515
|
}
|
|
536
|
-
|
|
537
|
-
|
|
516
|
+
return groups;
|
|
517
|
+
});
|
|
518
|
+
},
|
|
519
|
+
async listChannels(request = {}) {
|
|
520
|
+
return createEndpointOperation(request).run(() => {
|
|
521
|
+
const channels = [];
|
|
522
|
+
for (const guild of requireClient().guilds.cache.values()) {
|
|
523
|
+
if (!guild?.id)
|
|
524
|
+
continue;
|
|
525
|
+
const guildId = String(guild.id);
|
|
526
|
+
const guildName = String(guild.name ?? guildId);
|
|
527
|
+
for (const channel of guild.channels?.cache?.values() ?? []) {
|
|
528
|
+
if (!channel?.id)
|
|
529
|
+
continue;
|
|
530
|
+
if (channel.type !== ChannelType.GuildText)
|
|
531
|
+
continue;
|
|
532
|
+
channels.push({
|
|
533
|
+
id: String(channel.id),
|
|
534
|
+
name: channel.name ? String(channel.name) : undefined,
|
|
535
|
+
parent: { type: 'guild', id: guildId, name: guildName },
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return channels;
|
|
540
|
+
});
|
|
538
541
|
},
|
|
539
|
-
async listGroupMembers(groupId) {
|
|
540
|
-
const
|
|
541
|
-
const
|
|
542
|
+
async listGroupMembers({ groupId, signal }) {
|
|
543
|
+
const operation = createEndpointOperation({ signal });
|
|
544
|
+
const guild = await operation.run(() => requireClient().guilds.fetch(groupId));
|
|
545
|
+
const members = await operation.run(() => guild.members.fetch({ limit: 100 }));
|
|
542
546
|
return [...members.values()].map((member) => ({
|
|
543
547
|
id: member.id,
|
|
544
548
|
username: member.user.username,
|