@zhin.js/adapter-discord 8.0.0 → 8.0.1
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 +22 -0
- package/README.md +10 -1
- package/adapters/discord.js +0 -7
- package/adapters/discord.ts +0 -7
- package/agent/tools/add_role.ts +10 -8
- package/agent/tools/create_thread.ts +12 -9
- package/agent/tools/forum_post.ts +18 -9
- package/agent/tools/list_roles.ts +21 -9
- package/agent/tools/react.ts +11 -9
- package/agent/tools/remove_role.ts +10 -8
- package/agent/tools/send_embed.ts +10 -9
- package/lib/client.d.ts +16 -0
- package/lib/client.js +8 -0
- package/lib/discord-endpoint-commands.d.ts +1 -1
- package/lib/endpoint.d.ts +20 -32
- package/lib/endpoint.js +88 -157
- package/lib/gateway.d.ts +1 -0
- package/lib/gateway.js +5 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/protocol.d.ts +1 -1
- package/lib/protocol.js +1 -1
- package/lib/side-event-dispatch.d.ts +2 -2
- package/lib/side-event-dispatch.js +3 -3
- package/lib/webhook.d.ts +1 -0
- package/lib/webhook.js +2 -0
- package/package.json +15 -14
- package/src/client.ts +24 -0
- package/src/endpoint.ts +105 -212
- package/src/gateway.ts +6 -0
- package/src/index.ts +4 -6
- package/src/protocol.ts +1 -1
- package/src/side-event-dispatch.ts +4 -4
- package/src/webhook.ts +2 -0
- package/lib/discord-agent-deps.d.ts +0 -35
- package/lib/discord-agent-deps.js +0 -32
- package/src/discord-agent-deps.ts +0 -79
package/src/endpoint.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Endpoint } from 'zhin.js/adapter';
|
|
1
2
|
/**
|
|
2
3
|
* DiscordEndpoint — lifecycle, outbound, admit, gateway / interactions modes, agent tool surface.
|
|
3
4
|
*/
|
|
@@ -8,11 +9,9 @@ import type {
|
|
|
8
9
|
EndpointContentResolveContext,
|
|
9
10
|
EndpointControl,
|
|
10
11
|
EndpointGroup,
|
|
11
|
-
EndpointInstance,
|
|
12
12
|
EndpointManagement,
|
|
13
13
|
EndpointSendRequest,
|
|
14
14
|
} from 'zhin.js/adapter';
|
|
15
|
-
import type { MessageGateway, SideEventGateway } from '@zhin.js/core/runtime';
|
|
16
15
|
import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
|
|
17
16
|
import {
|
|
18
17
|
type ConversationReference,
|
|
@@ -21,7 +20,6 @@ import {
|
|
|
21
20
|
} from '@zhin.js/im-contract';
|
|
22
21
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
23
22
|
import type { CapabilityId } from 'zhin.js';
|
|
24
|
-
import { registerDiscordAgentEndpoint } from './discord-agent-deps.js';
|
|
25
23
|
import {
|
|
26
24
|
connectDiscordGatewayClient,
|
|
27
25
|
defaultCreateClient,
|
|
@@ -58,14 +56,12 @@ export type {
|
|
|
58
56
|
|
|
59
57
|
export interface DiscordEndpointOptions {
|
|
60
58
|
readonly id: CapabilityId;
|
|
61
|
-
readonly gateway: MessageGateway;
|
|
62
|
-
readonly sideEvents?: SideEventGateway;
|
|
63
59
|
readonly config: ResolvedDiscordGatewayConfig;
|
|
64
60
|
readonly createClient?: CreateDiscordClient;
|
|
65
61
|
readonly fetch?: typeof globalThis.fetch;
|
|
66
62
|
}
|
|
67
63
|
|
|
68
|
-
export class DiscordGatewayEndpoint
|
|
64
|
+
export class DiscordGatewayEndpoint extends Endpoint<DiscordClientTransport> {
|
|
69
65
|
readonly #logger!: ReturnType<typeof getAdapterLogger>;
|
|
70
66
|
|
|
71
67
|
readonly #options: DiscordEndpointOptions;
|
|
@@ -74,11 +70,9 @@ export class DiscordGatewayEndpoint implements EndpointInstance {
|
|
|
74
70
|
#client: DiscordClientTransport | null = null;
|
|
75
71
|
#open = false;
|
|
76
72
|
#started = false;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
getMembers: (guildId) => this.getMembers(guildId),
|
|
81
|
-
});
|
|
73
|
+
readonly management: EndpointManagement = createDiscordEndpointManagement(
|
|
74
|
+
() => this.#requireClient(),
|
|
75
|
+
);
|
|
82
76
|
readonly control: EndpointControl = Object.freeze({
|
|
83
77
|
recall: (message: MessageRef) => this.recallMessage(message),
|
|
84
78
|
addReaction: async (
|
|
@@ -88,7 +82,7 @@ export class DiscordGatewayEndpoint implements EndpointInstance {
|
|
|
88
82
|
) => {
|
|
89
83
|
const channelId = hint?.channelId ?? message.conversation.id;
|
|
90
84
|
if (!channelId || !message.id) return null;
|
|
91
|
-
await this
|
|
85
|
+
await this.#addReaction(channelId, message.id, emoji);
|
|
92
86
|
return emoji;
|
|
93
87
|
},
|
|
94
88
|
});
|
|
@@ -98,27 +92,35 @@ export class DiscordGatewayEndpoint implements EndpointInstance {
|
|
|
98
92
|
});
|
|
99
93
|
|
|
100
94
|
constructor(options: DiscordEndpointOptions) {
|
|
95
|
+
super();
|
|
101
96
|
this.#logger = getAdapterLogger('discord', options.config.id);
|
|
102
97
|
this.#options = options;
|
|
103
98
|
this.#createClient = options.createClient ?? defaultCreateClient;
|
|
104
99
|
this.#fetch = options.fetch ?? globalThis.fetch;
|
|
105
100
|
}
|
|
106
101
|
|
|
102
|
+
/** The actual discord.js-compatible client used by this connection. */
|
|
103
|
+
get client(): DiscordClientTransport {
|
|
104
|
+
return this.#requireClient();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
107
|
async start(): Promise<void> {
|
|
108
108
|
if (this.#started) return;
|
|
109
109
|
this.#started = true;
|
|
110
110
|
try {
|
|
111
|
-
this.#unregisterAgent = registerDiscordAgentEndpoint(this.#options.config.id, this);
|
|
112
111
|
const intents = this.#options.config.intents?.length
|
|
113
112
|
? [...this.#options.config.intents]
|
|
114
113
|
: DEFAULT_INTENTS;
|
|
115
114
|
this.#client = this.#createClient(intents);
|
|
116
115
|
await connectDiscordGatewayClient(this.#client, this.#options.config, {
|
|
116
|
+
onPlatformEvent: (name, event) => {
|
|
117
|
+
void this.#emitPlatformEvent(name, event);
|
|
118
|
+
},
|
|
117
119
|
onMessage: (msg) => this.admit(msg),
|
|
118
120
|
onButton: (interaction) => this.admitButton(interaction),
|
|
119
121
|
onGuildMemberAdd: (member) => {
|
|
120
122
|
receiveDiscordGuildMemberSideEvent(
|
|
121
|
-
this
|
|
123
|
+
(name, payload) => this.emit(name, payload),
|
|
122
124
|
this.#options.config.id,
|
|
123
125
|
'member_increase',
|
|
124
126
|
member,
|
|
@@ -127,7 +129,7 @@ export class DiscordGatewayEndpoint implements EndpointInstance {
|
|
|
127
129
|
},
|
|
128
130
|
onGuildMemberRemove: (member) => {
|
|
129
131
|
receiveDiscordGuildMemberSideEvent(
|
|
130
|
-
this
|
|
132
|
+
(name, payload) => this.emit(name, payload),
|
|
131
133
|
this.#options.config.id,
|
|
132
134
|
'member_decrease',
|
|
133
135
|
member,
|
|
@@ -158,8 +160,6 @@ export class DiscordGatewayEndpoint implements EndpointInstance {
|
|
|
158
160
|
|
|
159
161
|
async stop(): Promise<void> {
|
|
160
162
|
this.#open = false;
|
|
161
|
-
this.#unregisterAgent?.();
|
|
162
|
-
this.#unregisterAgent = undefined;
|
|
163
163
|
if (this.#client) {
|
|
164
164
|
try {
|
|
165
165
|
this.#client.removeAllListeners();
|
|
@@ -198,7 +198,7 @@ export class DiscordGatewayEndpoint implements EndpointInstance {
|
|
|
198
198
|
if (!this.#open) return;
|
|
199
199
|
if (msg.authorBot) return;
|
|
200
200
|
const conversation = discordInboundConversation(String(this.#options.id), msg);
|
|
201
|
-
void this
|
|
201
|
+
void this.emit('message.receive', {
|
|
202
202
|
conversation,
|
|
203
203
|
message: { conversation, id: msg.id },
|
|
204
204
|
content: formatInboundContent(msg),
|
|
@@ -230,7 +230,7 @@ export class DiscordGatewayEndpoint implements EndpointInstance {
|
|
|
230
230
|
admitButton(interaction: DiscordButtonInbound): void {
|
|
231
231
|
if (!this.#open) return;
|
|
232
232
|
const conversation = discordInboundConversation(String(this.#options.id), interaction);
|
|
233
|
-
void this
|
|
233
|
+
void this.emit('message.receive', {
|
|
234
234
|
conversation,
|
|
235
235
|
message: { conversation, id: interaction.id },
|
|
236
236
|
content: formatButtonContent(interaction),
|
|
@@ -251,58 +251,7 @@ export class DiscordGatewayEndpoint implements EndpointInstance {
|
|
|
251
251
|
});
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
async addRole(guildId: string, userId: string, roleId: string): Promise<boolean> {
|
|
257
|
-
const member = await this.#fetchMember(guildId, userId) as { roles: { add(id: string): Promise<unknown> } };
|
|
258
|
-
await member.roles.add(roleId);
|
|
259
|
-
return true;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
async removeRole(guildId: string, userId: string, roleId: string): Promise<boolean> {
|
|
263
|
-
const member = await this.#fetchMember(guildId, userId) as { roles: { remove(id: string): Promise<unknown> } };
|
|
264
|
-
await member.roles.remove(roleId);
|
|
265
|
-
return true;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
async getRoles(guildId: string): Promise<unknown[]> {
|
|
269
|
-
const guild = await this.#requireClient().guilds.fetch(guildId);
|
|
270
|
-
await guild.roles.fetch();
|
|
271
|
-
const cache = guild.roles.cache as Map<string, {
|
|
272
|
-
id: string;
|
|
273
|
-
name: string;
|
|
274
|
-
hexColor: string;
|
|
275
|
-
position: number;
|
|
276
|
-
permissions: { bitfield: bigint };
|
|
277
|
-
}>;
|
|
278
|
-
return [...cache.values()].map((role) => ({
|
|
279
|
-
id: role.id,
|
|
280
|
-
name: role.name,
|
|
281
|
-
color: role.hexColor,
|
|
282
|
-
position: role.position,
|
|
283
|
-
permissions: role.permissions.bitfield.toString(),
|
|
284
|
-
}));
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
async createThread(
|
|
288
|
-
channelId: string,
|
|
289
|
-
name: string,
|
|
290
|
-
messageId?: string,
|
|
291
|
-
autoArchiveDuration?: number,
|
|
292
|
-
): Promise<{ id: string }> {
|
|
293
|
-
const channel = await this.#requireClient().channels.fetch(channelId);
|
|
294
|
-
if (!channel || !('threads' in channel) || !channel.threads) {
|
|
295
|
-
throw new Error(`Channel ${channelId} 不支持创建帖子`);
|
|
296
|
-
}
|
|
297
|
-
const options: Record<string, unknown> = {
|
|
298
|
-
name,
|
|
299
|
-
autoArchiveDuration: autoArchiveDuration || 1440,
|
|
300
|
-
};
|
|
301
|
-
if (messageId) options.startMessage = messageId;
|
|
302
|
-
return channel.threads.create(options);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
async addReaction(channelId: string, messageId: string, emoji: string): Promise<void> {
|
|
254
|
+
async #addReaction(channelId: string, messageId: string, emoji: string): Promise<void> {
|
|
306
255
|
const channel = await this.#requireClient().channels.fetch(channelId);
|
|
307
256
|
if (!channel?.isTextBased() || !channel.messages) {
|
|
308
257
|
throw new Error(`Channel ${channelId} 不是文本频道`);
|
|
@@ -311,105 +260,14 @@ export class DiscordGatewayEndpoint implements EndpointInstance {
|
|
|
311
260
|
await message.react(emoji);
|
|
312
261
|
}
|
|
313
262
|
|
|
314
|
-
async
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
async createForumPost(
|
|
324
|
-
channelId: string,
|
|
325
|
-
name: string,
|
|
326
|
-
content: string,
|
|
327
|
-
tags?: string[],
|
|
328
|
-
): Promise<{ id: string }> {
|
|
329
|
-
const channel = await this.#requireClient().channels.fetch(channelId);
|
|
330
|
-
if (!channel || channel.type !== ChannelType.GuildForum || !channel.threads) {
|
|
331
|
-
throw new Error(`Channel ${channelId} 不是论坛频道`);
|
|
332
|
-
}
|
|
333
|
-
const options: Record<string, unknown> = {
|
|
334
|
-
name,
|
|
335
|
-
message: { content },
|
|
336
|
-
};
|
|
337
|
-
if (tags?.length && channel.availableTags?.length) {
|
|
338
|
-
const tagIds = channel.availableTags
|
|
339
|
-
.filter((t) => tags.includes(t.name))
|
|
340
|
-
.map((t) => t.id);
|
|
341
|
-
if (tagIds.length) options.appliedTags = tagIds;
|
|
342
|
-
}
|
|
343
|
-
return channel.threads.create(options);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
async kickMember(guildId: string, userId: string, reason?: string): Promise<boolean> {
|
|
347
|
-
const member = await this.#fetchMember(guildId, userId) as { kick(reason?: string): Promise<unknown> };
|
|
348
|
-
await member.kick(reason);
|
|
349
|
-
return true;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
async banMember(guildId: string, userId: string, reason?: string): Promise<boolean> {
|
|
353
|
-
const guild = await this.#requireClient().guilds.fetch(guildId);
|
|
354
|
-
await guild.members.ban(userId, { reason });
|
|
355
|
-
return true;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
async unbanMember(guildId: string, userId: string, reason?: string): Promise<boolean> {
|
|
359
|
-
const guild = await this.#requireClient().guilds.fetch(guildId);
|
|
360
|
-
await guild.members.unban(userId, reason);
|
|
361
|
-
return true;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
async timeoutMember(
|
|
365
|
-
guildId: string,
|
|
366
|
-
userId: string,
|
|
367
|
-
duration = 600,
|
|
368
|
-
reason?: string,
|
|
369
|
-
): Promise<boolean> {
|
|
370
|
-
const member = await this.#fetchMember(guildId, userId) as {
|
|
371
|
-
timeout(ms: number | null, reason?: string): Promise<unknown>;
|
|
372
|
-
};
|
|
373
|
-
await member.timeout(duration === 0 ? null : duration * 1000, reason);
|
|
374
|
-
return true;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
async setNickname(guildId: string, userId: string, nickname: string): Promise<boolean> {
|
|
378
|
-
const member = await this.#fetchMember(guildId, userId) as {
|
|
379
|
-
setNickname(nickname: string): Promise<unknown>;
|
|
380
|
-
};
|
|
381
|
-
await member.setNickname(nickname);
|
|
382
|
-
return true;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
async getMembers(guildId: string, limit = 100): Promise<unknown[]> {
|
|
386
|
-
const guild = await this.#requireClient().guilds.fetch(guildId);
|
|
387
|
-
const members = await guild.members.fetch({ limit }) as Map<string, {
|
|
388
|
-
id: string;
|
|
389
|
-
user: { username: string };
|
|
390
|
-
nickname: string | null;
|
|
391
|
-
roles: { cache: { map(fn: (r: { id: string }) => string): string[] } };
|
|
392
|
-
joinedAt?: Date | null;
|
|
393
|
-
}>;
|
|
394
|
-
return [...members.values()].map((member) => ({
|
|
395
|
-
id: member.id,
|
|
396
|
-
username: member.user.username,
|
|
397
|
-
nickname: member.nickname,
|
|
398
|
-
roles: member.roles.cache.map((r) => r.id),
|
|
399
|
-
joined_at: member.joinedAt?.toISOString(),
|
|
400
|
-
}));
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
async getGuildInfo(guildId: string): Promise<unknown> {
|
|
404
|
-
const guild = await this.#requireClient().guilds.fetch(guildId);
|
|
405
|
-
return {
|
|
406
|
-
id: guild.id,
|
|
407
|
-
name: guild.name,
|
|
408
|
-
icon: guild.iconURL?.(),
|
|
409
|
-
owner_id: guild.ownerId,
|
|
410
|
-
member_count: guild.memberCount,
|
|
411
|
-
created_at: guild.createdAt?.toISOString(),
|
|
412
|
-
};
|
|
263
|
+
async #emitPlatformEvent(name: string, event: unknown): Promise<void> {
|
|
264
|
+
await this.emitPlatform(name, event).catch((error) => {
|
|
265
|
+
this.#logger.warn(formatCompact({
|
|
266
|
+
op: 'discord_platform_event_failed',
|
|
267
|
+
event: name,
|
|
268
|
+
error: error instanceof Error ? error.message : String(error),
|
|
269
|
+
}));
|
|
270
|
+
});
|
|
413
271
|
}
|
|
414
272
|
|
|
415
273
|
async #sendBody(channelId: string, body: DiscordOutboundBody): Promise<string> {
|
|
@@ -422,11 +280,6 @@ export class DiscordGatewayEndpoint implements EndpointInstance {
|
|
|
422
280
|
return result.id;
|
|
423
281
|
}
|
|
424
282
|
|
|
425
|
-
async #fetchMember(guildId: string, userId: string): Promise<unknown> {
|
|
426
|
-
const guild = await this.#requireClient().guilds.fetch(guildId);
|
|
427
|
-
return guild.members.fetch(userId);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
283
|
#requireClient(): DiscordClientTransport {
|
|
431
284
|
if (!this.#client) throw new Error('Discord client not connected');
|
|
432
285
|
return this.#client;
|
|
@@ -435,14 +288,50 @@ export class DiscordGatewayEndpoint implements EndpointInstance {
|
|
|
435
288
|
|
|
436
289
|
export interface DiscordInteractionsEndpointOptions {
|
|
437
290
|
readonly id: CapabilityId;
|
|
438
|
-
readonly gateway: MessageGateway;
|
|
439
|
-
readonly sideEvents?: SideEventGateway;
|
|
440
291
|
readonly http: HttpHost;
|
|
441
292
|
readonly config: ResolvedDiscordInteractionsConfig;
|
|
442
293
|
readonly fetch?: typeof globalThis.fetch;
|
|
443
294
|
}
|
|
444
295
|
|
|
445
|
-
|
|
296
|
+
/** Minimal Discord REST client used when Gateway is intentionally disabled. */
|
|
297
|
+
export class DiscordRestClient {
|
|
298
|
+
constructor(
|
|
299
|
+
readonly token: string,
|
|
300
|
+
readonly fetch: typeof globalThis.fetch = globalThis.fetch,
|
|
301
|
+
) {}
|
|
302
|
+
|
|
303
|
+
async request<T = unknown>(
|
|
304
|
+
method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE',
|
|
305
|
+
path: string,
|
|
306
|
+
body?: unknown,
|
|
307
|
+
): Promise<T> {
|
|
308
|
+
const response = await this.fetch(`${DISCORD_API}${path}`, {
|
|
309
|
+
method,
|
|
310
|
+
headers: {
|
|
311
|
+
Authorization: `Bot ${this.token}`,
|
|
312
|
+
...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
|
|
313
|
+
},
|
|
314
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
315
|
+
signal: AbortSignal.timeout(OUTBOUND_TIMEOUT_MS),
|
|
316
|
+
});
|
|
317
|
+
const text = await response.text();
|
|
318
|
+
if (!response.ok) {
|
|
319
|
+
throw new Error(`Discord API ${method} ${path} failed (${response.status}): ${text.slice(0, 200)}`);
|
|
320
|
+
}
|
|
321
|
+
return (text ? JSON.parse(text) : undefined) as T;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
createMessage(channelId: string, body: unknown): Promise<{ id?: string }> {
|
|
325
|
+
return this.request('POST', `/channels/${channelId}/messages`, body);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
deleteMessage(channelId: string, messageId: string): Promise<void> {
|
|
329
|
+
return this.request('DELETE', `/channels/${channelId}/messages/${messageId}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export class DiscordInteractionsEndpoint extends Endpoint<DiscordRestClient> {
|
|
334
|
+
readonly client: DiscordRestClient;
|
|
446
335
|
readonly #logger!: ReturnType<typeof getAdapterLogger>;
|
|
447
336
|
|
|
448
337
|
readonly #options: DiscordInteractionsEndpointOptions;
|
|
@@ -459,9 +348,11 @@ export class DiscordInteractionsEndpoint implements EndpointInstance {
|
|
|
459
348
|
});
|
|
460
349
|
|
|
461
350
|
constructor(options: DiscordInteractionsEndpointOptions) {
|
|
351
|
+
super();
|
|
462
352
|
this.#logger = getAdapterLogger('discord', options.config.id);
|
|
463
353
|
this.#options = options;
|
|
464
354
|
this.#fetch = options.fetch ?? globalThis.fetch;
|
|
355
|
+
this.client = new DiscordRestClient(options.config.token, this.#fetch);
|
|
465
356
|
}
|
|
466
357
|
|
|
467
358
|
get isOpen(): boolean {
|
|
@@ -501,42 +392,19 @@ export class DiscordInteractionsEndpoint implements EndpointInstance {
|
|
|
501
392
|
|
|
502
393
|
async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
|
|
503
394
|
const body = formatOutboundBody(payload);
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
method: 'POST',
|
|
507
|
-
headers: {
|
|
508
|
-
Authorization: `Bot ${this.#options.config.token}`,
|
|
509
|
-
'Content-Type': 'application/json',
|
|
510
|
-
},
|
|
511
|
-
body: JSON.stringify(body),
|
|
512
|
-
signal: AbortSignal.timeout(OUTBOUND_TIMEOUT_MS),
|
|
513
|
-
});
|
|
514
|
-
const text = await response.text();
|
|
515
|
-
if (!response.ok) {
|
|
516
|
-
throw new Error(`Discord send failed (${response.status}): ${text.slice(0, 200)}`);
|
|
517
|
-
}
|
|
518
|
-
const data = JSON.parse(text) as { id?: string };
|
|
519
|
-
const snowflake = data.id ?? '';
|
|
520
|
-
return snowflake;
|
|
395
|
+
const data = await this.client.createMessage(conversation.id, body);
|
|
396
|
+
return data.id ?? '';
|
|
521
397
|
}
|
|
522
398
|
|
|
523
399
|
async recallMessage(message: MessageRef): Promise<void> {
|
|
524
400
|
if (!message.id) return;
|
|
525
|
-
|
|
526
|
-
method: 'DELETE',
|
|
527
|
-
headers: { Authorization: `Bot ${this.#options.config.token}` },
|
|
528
|
-
signal: AbortSignal.timeout(OUTBOUND_TIMEOUT_MS),
|
|
529
|
-
});
|
|
530
|
-
if (!response.ok && response.status !== 404) {
|
|
531
|
-
const text = await response.text();
|
|
532
|
-
throw new Error(`Discord recall failed (${response.status}): ${text.slice(0, 200)}`);
|
|
533
|
-
}
|
|
401
|
+
await this.client.deleteMessage(message.conversation.id, message.id);
|
|
534
402
|
}
|
|
535
403
|
|
|
536
404
|
admit(msg: DiscordInboundMessage): void {
|
|
537
405
|
if (!this.#open) return;
|
|
538
406
|
const conversation = discordInboundConversation(String(this.#options.id), msg);
|
|
539
|
-
void this
|
|
407
|
+
void this.emit('message.receive', {
|
|
540
408
|
conversation,
|
|
541
409
|
message: { conversation, id: msg.id },
|
|
542
410
|
content: formatInboundContent(msg),
|
|
@@ -561,6 +429,18 @@ export class DiscordInteractionsEndpoint implements EndpointInstance {
|
|
|
561
429
|
}));
|
|
562
430
|
});
|
|
563
431
|
}
|
|
432
|
+
|
|
433
|
+
admitPlatform(event: Record<string, unknown>): void {
|
|
434
|
+
if (!this.#open) return;
|
|
435
|
+
const type = typeof event.type === 'number' ? `interaction.${event.type}` : 'interaction';
|
|
436
|
+
void this.emitPlatform(type, event).catch((error) => {
|
|
437
|
+
this.#logger.warn(formatCompact({
|
|
438
|
+
op: 'discord_platform_event_failed',
|
|
439
|
+
event: type,
|
|
440
|
+
error: error instanceof Error ? error.message : String(error),
|
|
441
|
+
}));
|
|
442
|
+
});
|
|
443
|
+
}
|
|
564
444
|
}
|
|
565
445
|
|
|
566
446
|
async function resolveDiscordContent(
|
|
@@ -628,14 +508,13 @@ function toGroupId(id: string): number {
|
|
|
628
508
|
* DiscordGatewayEndpoint 的 EndpointManagement 语义端口(参照 qq 的工厂模式)。
|
|
629
509
|
* 数据源为 discord.js SDK 缓存:guilds.cache / guild.channels.cache / guild.members。
|
|
630
510
|
*/
|
|
631
|
-
export function createDiscordEndpointManagement(
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
}): EndpointManagement {
|
|
511
|
+
export function createDiscordEndpointManagement(
|
|
512
|
+
requireClient: () => DiscordClientTransport,
|
|
513
|
+
): EndpointManagement {
|
|
635
514
|
return Object.freeze<EndpointManagement>({
|
|
636
515
|
async listGroups(): Promise<readonly EndpointGroup[]> {
|
|
637
516
|
const groups: EndpointGroup[] = [];
|
|
638
|
-
for (const guild of
|
|
517
|
+
for (const guild of requireClient().guilds.cache.values()) {
|
|
639
518
|
if (!guild?.id) continue;
|
|
640
519
|
groups.push({
|
|
641
520
|
group_id: toGroupId(String(guild.id)),
|
|
@@ -646,7 +525,7 @@ export function createDiscordEndpointManagement(endpoint: {
|
|
|
646
525
|
},
|
|
647
526
|
async listChannels(): Promise<readonly EndpointChannel[]> {
|
|
648
527
|
const channels: EndpointChannel[] = [];
|
|
649
|
-
for (const guild of
|
|
528
|
+
for (const guild of requireClient().guilds.cache.values()) {
|
|
650
529
|
if (!guild?.id) continue;
|
|
651
530
|
const guildId = String(guild.id);
|
|
652
531
|
const guildName = String(guild.name ?? guildId);
|
|
@@ -663,7 +542,21 @@ export function createDiscordEndpointManagement(endpoint: {
|
|
|
663
542
|
return channels;
|
|
664
543
|
},
|
|
665
544
|
async listGroupMembers(groupId: string): Promise<readonly unknown[]> {
|
|
666
|
-
|
|
545
|
+
const guild = await requireClient().guilds.fetch(groupId);
|
|
546
|
+
const members = await guild.members.fetch({ limit: 100 }) as Map<string, {
|
|
547
|
+
id: string;
|
|
548
|
+
user: { username: string };
|
|
549
|
+
nickname: string | null;
|
|
550
|
+
roles: { cache: { map(fn: (role: { id: string }) => string): string[] } };
|
|
551
|
+
joinedAt?: Date | null;
|
|
552
|
+
}>;
|
|
553
|
+
return [...members.values()].map((member) => ({
|
|
554
|
+
id: member.id,
|
|
555
|
+
username: member.user.username,
|
|
556
|
+
nickname: member.nickname,
|
|
557
|
+
roles: member.roles.cache.map((role) => role.id),
|
|
558
|
+
joined_at: member.joinedAt?.toISOString(),
|
|
559
|
+
}));
|
|
667
560
|
},
|
|
668
561
|
});
|
|
669
562
|
}
|
package/src/gateway.ts
CHANGED
|
@@ -285,6 +285,7 @@ function decodeBase64(value: string): Buffer {
|
|
|
285
285
|
}
|
|
286
286
|
|
|
287
287
|
export interface DiscordGatewayConnectHandlers {
|
|
288
|
+
onPlatformEvent(name: string, event: unknown): void;
|
|
288
289
|
onMessage(msg: DiscordInboundMessage): void;
|
|
289
290
|
onButton(interaction: DiscordButtonInbound): void;
|
|
290
291
|
onGuildMemberAdd?(member: { guildId: string; userId: string; userName?: string }): void;
|
|
@@ -300,6 +301,7 @@ export async function connectDiscordGatewayClient(
|
|
|
300
301
|
let settled = false;
|
|
301
302
|
|
|
302
303
|
client.on('messageCreate', (raw) => {
|
|
304
|
+
handlers.onPlatformEvent('messageCreate', raw);
|
|
303
305
|
const msg = normalizeDiscordMessage(raw);
|
|
304
306
|
if (!msg) return;
|
|
305
307
|
// clientReady 之后 client.user 一定可用;消息事件只会在此之后到达
|
|
@@ -310,6 +312,7 @@ export async function connectDiscordGatewayClient(
|
|
|
310
312
|
});
|
|
311
313
|
|
|
312
314
|
client.on('interactionCreate', (raw) => {
|
|
315
|
+
handlers.onPlatformEvent('interactionCreate', raw);
|
|
313
316
|
const interaction = raw as {
|
|
314
317
|
isButton?(): boolean;
|
|
315
318
|
deferUpdate?(): Promise<unknown>;
|
|
@@ -334,6 +337,7 @@ export async function connectDiscordGatewayClient(
|
|
|
334
337
|
});
|
|
335
338
|
|
|
336
339
|
client.on('guildMemberAdd', (raw) => {
|
|
340
|
+
handlers.onPlatformEvent('guildMemberAdd', raw);
|
|
337
341
|
const member = raw as {
|
|
338
342
|
guild?: { id?: string };
|
|
339
343
|
user?: { id?: string; username?: string; displayName?: string };
|
|
@@ -349,6 +353,7 @@ export async function connectDiscordGatewayClient(
|
|
|
349
353
|
});
|
|
350
354
|
|
|
351
355
|
client.on('guildMemberRemove', (raw) => {
|
|
356
|
+
handlers.onPlatformEvent('guildMemberRemove', raw);
|
|
352
357
|
const member = raw as {
|
|
353
358
|
guild?: { id?: string };
|
|
354
359
|
user?: { id?: string; username?: string; displayName?: string };
|
|
@@ -364,6 +369,7 @@ export async function connectDiscordGatewayClient(
|
|
|
364
369
|
});
|
|
365
370
|
|
|
366
371
|
client.once('clientReady', () => {
|
|
372
|
+
handlers.onPlatformEvent('clientReady', client.user);
|
|
367
373
|
void (async () => {
|
|
368
374
|
try {
|
|
369
375
|
if (config.defaultActivity && client.user?.setActivity) {
|
package/src/index.ts
CHANGED
|
@@ -18,12 +18,10 @@ export {
|
|
|
18
18
|
} from './protocol.js';
|
|
19
19
|
|
|
20
20
|
export {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
type DiscordAgentEndpoint,
|
|
26
|
-
} from './discord-agent-deps.js';
|
|
21
|
+
discordClient,
|
|
22
|
+
type DiscordClient,
|
|
23
|
+
type DiscordClientEventMap,
|
|
24
|
+
} from './client.js';
|
|
27
25
|
|
|
28
26
|
export {
|
|
29
27
|
checkDiscordPlatformPermit,
|
package/src/protocol.ts
CHANGED
|
@@ -227,7 +227,7 @@ export function senderDisplayName(msg: DiscordInboundMessage): string {
|
|
|
227
227
|
return msg.authorName || msg.authorId;
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
-
/** Build inbound text for
|
|
230
|
+
/** Build inbound text for OutboundMessageService.receive (gateway owns reply routing). */
|
|
231
231
|
export function formatInboundContent(msg: DiscordInboundMessage): string {
|
|
232
232
|
const parts: string[] = [];
|
|
233
233
|
if (msg.content?.trim()) parts.push(msg.content.trim());
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { buildNotice, senderFromId } from '@zhin.js/core';
|
|
2
|
-
import type {
|
|
2
|
+
import type { EndpointEventEmitter } from 'zhin.js/adapter';
|
|
3
3
|
import { formatCompact, type getAdapterLogger } from '@zhin.js/logger';
|
|
4
4
|
|
|
5
5
|
export interface DiscordGuildMemberSideEvent {
|
|
@@ -9,14 +9,14 @@ export interface DiscordGuildMemberSideEvent {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export function receiveDiscordGuildMemberSideEvent(
|
|
12
|
-
|
|
12
|
+
emit: EndpointEventEmitter,
|
|
13
13
|
configId: string,
|
|
14
14
|
kind: 'member_increase' | 'member_decrease',
|
|
15
15
|
event: DiscordGuildMemberSideEvent,
|
|
16
16
|
logger: ReturnType<typeof getAdapterLogger>,
|
|
17
17
|
): void {
|
|
18
|
-
if (!
|
|
19
|
-
void
|
|
18
|
+
if (!emit) return;
|
|
19
|
+
void emit('notice.receive', buildNotice(event, {
|
|
20
20
|
$id: `discord:guild_member:${kind}:${event.guildId}:${event.userId}:${Date.now()}`,
|
|
21
21
|
$adapter: 'discord' as never,
|
|
22
22
|
$endpoint: configId,
|
package/src/webhook.ts
CHANGED
|
@@ -24,6 +24,7 @@ export interface DiscordInteractionsHandler {
|
|
|
24
24
|
readonly config: ResolvedDiscordInteractionsConfig;
|
|
25
25
|
readonly isOpen: boolean;
|
|
26
26
|
admit(msg: DiscordInboundMessage): void;
|
|
27
|
+
admitPlatform(event: Record<string, unknown>): void;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export function registerDiscordInteractionRoutes(
|
|
@@ -63,6 +64,7 @@ export async function handleDiscordInteractionRequest(
|
|
|
63
64
|
return;
|
|
64
65
|
}
|
|
65
66
|
const interaction = JSON.parse(rawBody) as Record<string, unknown>;
|
|
67
|
+
if (handler.isOpen) handler.admitPlatform(interaction);
|
|
66
68
|
if (interaction.type === INTERACTION_TYPE_PING) {
|
|
67
69
|
writeJson(response, 200, { type: INTERACTION_RESPONSE_PONG });
|
|
68
70
|
return;
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Agent tool deps for discord.
|
|
3
|
-
* Endpoints register themselves on start; tools look up by config name / endpoint id.
|
|
4
|
-
*/
|
|
5
|
-
export interface DiscordAgentEndpoint {
|
|
6
|
-
addRole(guildId: string, userId: string, roleId: string): Promise<boolean>;
|
|
7
|
-
removeRole(guildId: string, userId: string, roleId: string): Promise<boolean>;
|
|
8
|
-
getRoles(guildId: string): Promise<unknown[]>;
|
|
9
|
-
createThread(channelId: string, name: string, messageId?: string, autoArchiveDuration?: number): Promise<{
|
|
10
|
-
id: string;
|
|
11
|
-
}>;
|
|
12
|
-
addReaction(channelId: string, messageId: string, emoji: string): Promise<void>;
|
|
13
|
-
sendEmbed(channelId: string, embedData: Record<string, unknown>): Promise<{
|
|
14
|
-
id: string;
|
|
15
|
-
}>;
|
|
16
|
-
createForumPost(channelId: string, name: string, content: string, tags?: string[]): Promise<{
|
|
17
|
-
id: string;
|
|
18
|
-
}>;
|
|
19
|
-
kickMember(guildId: string, userId: string, reason?: string): Promise<boolean>;
|
|
20
|
-
banMember(guildId: string, userId: string, reason?: string): Promise<boolean>;
|
|
21
|
-
unbanMember(guildId: string, userId: string, reason?: string): Promise<boolean>;
|
|
22
|
-
timeoutMember(guildId: string, userId: string, duration?: number, reason?: string): Promise<boolean>;
|
|
23
|
-
setNickname(guildId: string, userId: string, nickname: string): Promise<boolean>;
|
|
24
|
-
getMembers(guildId: string, limit?: number): Promise<unknown[]>;
|
|
25
|
-
getGuildInfo(guildId: string): Promise<unknown>;
|
|
26
|
-
}
|
|
27
|
-
export interface DiscordAgentDeps {
|
|
28
|
-
getEndpoint: (endpointKey: string) => DiscordAgentEndpoint;
|
|
29
|
-
/** Alias kept for existing agent/tools that call getGatewayEndpoint. */
|
|
30
|
-
getGatewayEndpoint: (endpointKey: string) => DiscordAgentEndpoint;
|
|
31
|
-
}
|
|
32
|
-
export declare function registerDiscordAgentEndpoint(endpointKey: string, endpoint: DiscordAgentEndpoint): () => void;
|
|
33
|
-
/** Optional override used by tests / transitional callers. Pass `null` to clear. */
|
|
34
|
-
export declare function setDiscordAgentDeps(deps: DiscordAgentDeps | null): void;
|
|
35
|
-
export declare function getDiscordAgentDeps(): DiscordAgentDeps;
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Agent tool deps for discord.
|
|
3
|
-
* Endpoints register themselves on start; tools look up by config name / endpoint id.
|
|
4
|
-
*/
|
|
5
|
-
const endpoints = new Map();
|
|
6
|
-
let override = null;
|
|
7
|
-
export function registerDiscordAgentEndpoint(endpointKey, endpoint) {
|
|
8
|
-
endpoints.set(endpointKey, endpoint);
|
|
9
|
-
return () => {
|
|
10
|
-
if (endpoints.get(endpointKey) === endpoint) {
|
|
11
|
-
endpoints.delete(endpointKey);
|
|
12
|
-
}
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
/** Optional override used by tests / transitional callers. Pass `null` to clear. */
|
|
16
|
-
export function setDiscordAgentDeps(deps) {
|
|
17
|
-
override = deps;
|
|
18
|
-
}
|
|
19
|
-
function lookup(endpointKey) {
|
|
20
|
-
const registered = endpoints.get(endpointKey);
|
|
21
|
-
if (!registered)
|
|
22
|
-
throw new Error(`Endpoint ${endpointKey} 不存在`);
|
|
23
|
-
return registered;
|
|
24
|
-
}
|
|
25
|
-
export function getDiscordAgentDeps() {
|
|
26
|
-
if (override)
|
|
27
|
-
return override;
|
|
28
|
-
return {
|
|
29
|
-
getEndpoint: lookup,
|
|
30
|
-
getGatewayEndpoint: lookup,
|
|
31
|
-
};
|
|
32
|
-
}
|