@zhin.js/adapter-discord 1.1.18 → 1.1.20
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 +46 -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/gateway.d.ts
CHANGED
|
@@ -1,133 +1,15 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { EndpointEventSink } from 'zhin.js/adapter';
|
|
2
|
+
import { Client, GatewayIntentBits, type RESTOptions, type ClientOptions, type MessageCreateOptions } from 'discord.js';
|
|
2
3
|
import { type DiscordButtonInbound, type DiscordInboundMessage, type DiscordOutboundBody, type ResolvedDiscordGatewayConfig } from './protocol.js';
|
|
3
4
|
export declare const DEFAULT_INTENTS: GatewayIntentBits[];
|
|
4
|
-
/**
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
login(token: string): Promise<string>;
|
|
8
|
-
destroy(): Promise<void>;
|
|
9
|
-
on(event: string, listener: (...args: unknown[]) => void): void;
|
|
10
|
-
once(event: string, listener: (...args: unknown[]) => void): void;
|
|
11
|
-
removeAllListeners(): void;
|
|
12
|
-
readonly user?: {
|
|
13
|
-
readonly id: string;
|
|
14
|
-
readonly tag?: string;
|
|
15
|
-
setActivity?(name: string, options?: {
|
|
16
|
-
type?: number;
|
|
17
|
-
url?: string;
|
|
18
|
-
}): void;
|
|
19
|
-
} | null;
|
|
20
|
-
channels: {
|
|
21
|
-
fetch(id: string): Promise<{
|
|
22
|
-
id: string;
|
|
23
|
-
type: number;
|
|
24
|
-
isTextBased(): boolean;
|
|
25
|
-
send?(options: MessageCreateOptions): Promise<{
|
|
26
|
-
id: string;
|
|
27
|
-
}>;
|
|
28
|
-
messages?: {
|
|
29
|
-
fetch(id: string): Promise<{
|
|
30
|
-
delete(): Promise<unknown>;
|
|
31
|
-
react(emoji: string): Promise<unknown>;
|
|
32
|
-
reactions: {
|
|
33
|
-
resolve(emoji: unknown): {
|
|
34
|
-
users: {
|
|
35
|
-
remove(userId: string): Promise<unknown>;
|
|
36
|
-
};
|
|
37
|
-
} | null;
|
|
38
|
-
cache: {
|
|
39
|
-
find(fn: (r: {
|
|
40
|
-
emoji: {
|
|
41
|
-
toString(): string;
|
|
42
|
-
name?: string | null;
|
|
43
|
-
id?: string | null;
|
|
44
|
-
};
|
|
45
|
-
}) => boolean): {
|
|
46
|
-
users: {
|
|
47
|
-
remove(userId: string): Promise<unknown>;
|
|
48
|
-
};
|
|
49
|
-
} | undefined;
|
|
50
|
-
};
|
|
51
|
-
};
|
|
52
|
-
}>;
|
|
53
|
-
};
|
|
54
|
-
threads?: {
|
|
55
|
-
create(options: Record<string, unknown>): Promise<{
|
|
56
|
-
id: string;
|
|
57
|
-
}>;
|
|
58
|
-
};
|
|
59
|
-
availableTags?: Array<{
|
|
60
|
-
id: string;
|
|
61
|
-
name: string;
|
|
62
|
-
}>;
|
|
63
|
-
} | null>;
|
|
64
|
-
};
|
|
65
|
-
guilds: {
|
|
66
|
-
fetch(id: string): Promise<{
|
|
67
|
-
id: string;
|
|
68
|
-
name: string;
|
|
69
|
-
ownerId: string;
|
|
70
|
-
memberCount: number;
|
|
71
|
-
createdAt?: Date | null;
|
|
72
|
-
iconURL?(options?: {
|
|
73
|
-
size?: number;
|
|
74
|
-
}): string | null;
|
|
75
|
-
roles: {
|
|
76
|
-
fetch(): Promise<unknown>;
|
|
77
|
-
cache: Map<string, {
|
|
78
|
-
id: string;
|
|
79
|
-
name: string;
|
|
80
|
-
hexColor: string;
|
|
81
|
-
position: number;
|
|
82
|
-
permissions: {
|
|
83
|
-
bitfield: bigint;
|
|
84
|
-
};
|
|
85
|
-
}> | {
|
|
86
|
-
map(fn: (role: {
|
|
87
|
-
id: string;
|
|
88
|
-
name: string;
|
|
89
|
-
hexColor: string;
|
|
90
|
-
position: number;
|
|
91
|
-
permissions: {
|
|
92
|
-
bitfield: bigint;
|
|
93
|
-
};
|
|
94
|
-
}) => unknown): unknown[];
|
|
95
|
-
};
|
|
96
|
-
};
|
|
97
|
-
members: {
|
|
98
|
-
fetch(userId: string | {
|
|
99
|
-
limit?: number;
|
|
100
|
-
}): Promise<unknown>;
|
|
101
|
-
ban(userId: string, options?: {
|
|
102
|
-
reason?: string;
|
|
103
|
-
deleteMessageSeconds?: number;
|
|
104
|
-
}): Promise<unknown>;
|
|
105
|
-
unban(userId: string, reason?: string): Promise<unknown>;
|
|
106
|
-
};
|
|
107
|
-
}>;
|
|
108
|
-
cache: {
|
|
109
|
-
values(): IterableIterator<{
|
|
110
|
-
id: string;
|
|
111
|
-
name: string;
|
|
112
|
-
channels: {
|
|
113
|
-
cache: {
|
|
114
|
-
values(): IterableIterator<{
|
|
115
|
-
id: string;
|
|
116
|
-
name?: string;
|
|
117
|
-
type: number;
|
|
118
|
-
}>;
|
|
119
|
-
};
|
|
120
|
-
};
|
|
121
|
-
}>;
|
|
122
|
-
};
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
export type CreateDiscordClient = (intents: readonly number[], gatewayFaultProxyUrl?: string, restOptions?: Partial<RESTOptions>) => DiscordClientTransport;
|
|
126
|
-
export declare function defaultCreateClient(intents: readonly number[], gatewayFaultProxyUrl?: string, restOptions?: Partial<RESTOptions>): DiscordClientTransport;
|
|
5
|
+
/** Native SDK surface; tests inject transport below the Client rather than replacing its identity. */
|
|
6
|
+
export type DiscordClientTransport = Client;
|
|
7
|
+
export declare function discordClientOptions(intents: readonly number[], gatewayFaultProxyUrl?: string, restOptions?: Partial<RESTOptions>): ClientOptions;
|
|
127
8
|
export declare function resolveSenderRole(msg: DiscordInboundMessage): string | undefined;
|
|
128
9
|
export declare function normalizeDiscordMessage(raw: unknown): DiscordInboundMessage | null;
|
|
129
10
|
export declare function toMessageCreateOptions(body: DiscordOutboundBody): Promise<MessageCreateOptions>;
|
|
130
11
|
export interface DiscordGatewayConnectHandlers {
|
|
12
|
+
readonly receive: EndpointEventSink['run'];
|
|
131
13
|
onPlatformEvent(name: string, event: unknown): void;
|
|
132
14
|
onMessage(msg: DiscordInboundMessage): void;
|
|
133
15
|
onButton(interaction: DiscordButtonInbound): void;
|
package/lib/gateway.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DiscordFaultProxyStrategy } from './gateway-fault-proxy.js';
|
|
2
|
-
import {
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { Partials, GatewayIntentBits, EmbedBuilder, AttachmentBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, REST, Routes, PermissionFlagsBits, } from 'discord.js';
|
|
5
5
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
6
6
|
import { activityTypeCode, resolveChannelKind, } from './protocol.js';
|
|
7
7
|
const logger = getLogger('discord');
|
|
@@ -13,8 +13,8 @@ export const DEFAULT_INTENTS = [
|
|
|
13
13
|
GatewayIntentBits.GuildMembers,
|
|
14
14
|
GatewayIntentBits.GuildMessageReactions,
|
|
15
15
|
];
|
|
16
|
-
export function
|
|
17
|
-
return
|
|
16
|
+
export function discordClientOptions(intents, gatewayFaultProxyUrl, restOptions = {}) {
|
|
17
|
+
return { rest: { ...restOptions, retries: 0, rejectOnRateLimit: () => true }, intents: [...intents], partials: [Partials.Channel], ...(gatewayFaultProxyUrl ? { ws: { buildStrategy: manager => new DiscordFaultProxyStrategy(manager, gatewayFaultProxyUrl) } } : {}) };
|
|
18
18
|
}
|
|
19
19
|
export function resolveSenderRole(msg) {
|
|
20
20
|
if (msg.isGuildOwner)
|
|
@@ -55,6 +55,8 @@ export function normalizeDiscordMessage(raw) {
|
|
|
55
55
|
}
|
|
56
56
|
return {
|
|
57
57
|
id: msg.id,
|
|
58
|
+
event: raw,
|
|
59
|
+
platformSentAt: msg.createdTimestamp,
|
|
58
60
|
content: msg.content ?? '',
|
|
59
61
|
channelId: msg.channel.id,
|
|
60
62
|
channelKind: resolveChannelKind(msg.channel.type),
|
|
@@ -120,7 +122,7 @@ export async function toMessageCreateOptions(body) {
|
|
|
120
122
|
}));
|
|
121
123
|
}
|
|
122
124
|
else if (file.file && await fileExists(file.file)) {
|
|
123
|
-
files.push(new AttachmentBuilder(
|
|
125
|
+
files.push(new AttachmentBuilder(file.file, {
|
|
124
126
|
name: file.name || path.basename(file.file),
|
|
125
127
|
}));
|
|
126
128
|
}
|
|
@@ -211,70 +213,81 @@ export async function connectDiscordGatewayClient(client, config, handlers, sign
|
|
|
211
213
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
212
214
|
const releaseAbort = () => signal?.removeEventListener('abort', onAbort);
|
|
213
215
|
client.on('messageCreate', (raw) => {
|
|
214
|
-
handlers.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
216
|
+
void handlers.receive(() => {
|
|
217
|
+
handlers.onPlatformEvent('messageCreate', raw);
|
|
218
|
+
const msg = normalizeDiscordMessage(raw);
|
|
219
|
+
if (!msg)
|
|
220
|
+
return;
|
|
221
|
+
// clientReady 之后 client.user 一定可用;消息事件只会在此之后到达
|
|
222
|
+
const botId = client.user?.id;
|
|
223
|
+
const mentions = raw.mentions;
|
|
224
|
+
const mentionedBot = !!botId && mentions?.users?.has?.(botId) === true;
|
|
225
|
+
handlers.onMessage(mentionedBot ? { ...msg, mentionedBot: true } : msg);
|
|
226
|
+
}).catch(() => logger.debug('Native callback was not admitted'));
|
|
223
227
|
});
|
|
224
228
|
client.on('interactionCreate', (raw) => {
|
|
225
|
-
handlers.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
229
|
+
void handlers.receive(() => {
|
|
230
|
+
handlers.onPlatformEvent('interactionCreate', raw);
|
|
231
|
+
const interaction = raw;
|
|
232
|
+
const isButton = interaction.isButton?.() === true;
|
|
233
|
+
logger.info(formatCompact({ op: 'discord_gateway_interaction_received', button: isButton, cachedChannel: !!interaction.channel, hasChannelId: !!interaction.channelId }));
|
|
234
|
+
if (!isButton)
|
|
235
|
+
return;
|
|
236
|
+
const ack = interaction.deferUpdate?.().then(() => {
|
|
237
|
+
logger.info(formatCompact({ op: 'discord_gateway_button_ack', status: 'confirmed' }));
|
|
238
|
+
}).catch(() => {
|
|
239
|
+
// SDK errors may contain token-bearing callback URLs; log no raw error.
|
|
240
|
+
logger.warn(formatCompact({ op: 'discord_gateway_button_ack', status: 'failed' }));
|
|
241
|
+
});
|
|
242
|
+
const channelId = interaction.channelId ?? interaction.channel?.id;
|
|
243
|
+
if (!channelId) {
|
|
244
|
+
logger.warn(formatCompact({ op: 'discord_gateway_button_dropped', reason: 'missing_channel_id' }));
|
|
245
|
+
return ack ? awaitOwnedAck(ack, signal) : undefined;
|
|
246
|
+
}
|
|
247
|
+
handlers.onButton({
|
|
248
|
+
event: raw,
|
|
249
|
+
platformSentAt: raw.createdTimestamp,
|
|
250
|
+
id: interaction.id,
|
|
251
|
+
customId: interaction.customId,
|
|
252
|
+
channelId,
|
|
253
|
+
channelKind: interaction.channel ? resolveChannelKind(interaction.channel.type) : interaction.guildId ? 'channel' : 'private',
|
|
254
|
+
...(interaction.guildId ? { guildId: interaction.guildId } : {}),
|
|
255
|
+
userId: interaction.user.id,
|
|
256
|
+
userName: interaction.user.username || interaction.user.displayName || interaction.user.id,
|
|
257
|
+
sourceMessageId: interaction.message?.id,
|
|
258
|
+
});
|
|
259
|
+
return ack ? awaitOwnedAck(ack, signal) : undefined;
|
|
260
|
+
}).catch(() => logger.debug('Native callback was not admitted'));
|
|
252
261
|
});
|
|
253
262
|
client.on('guildMemberAdd', (raw) => {
|
|
254
|
-
handlers.
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
263
|
+
void handlers.receive(() => {
|
|
264
|
+
handlers.onPlatformEvent('guildMemberAdd', raw);
|
|
265
|
+
const member = raw;
|
|
266
|
+
const guildId = member.guild?.id;
|
|
267
|
+
const userId = member.user?.id;
|
|
268
|
+
if (!guildId || !userId)
|
|
269
|
+
return;
|
|
270
|
+
handlers.onGuildMemberAdd?.({
|
|
271
|
+
guildId,
|
|
272
|
+
userId,
|
|
273
|
+
userName: member.user?.username || member.user?.displayName,
|
|
274
|
+
});
|
|
275
|
+
}).catch(() => logger.debug('Native callback was not admitted'));
|
|
265
276
|
});
|
|
266
277
|
client.on('guildMemberRemove', (raw) => {
|
|
267
|
-
handlers.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
+
void handlers.receive(() => {
|
|
279
|
+
handlers.onPlatformEvent('guildMemberRemove', raw);
|
|
280
|
+
const member = raw;
|
|
281
|
+
const guildId = member.guild?.id;
|
|
282
|
+
const userId = member.user?.id;
|
|
283
|
+
if (!guildId || !userId)
|
|
284
|
+
return;
|
|
285
|
+
handlers.onGuildMemberRemove?.({
|
|
286
|
+
guildId,
|
|
287
|
+
userId,
|
|
288
|
+
userName: member.user?.username || member.user?.displayName,
|
|
289
|
+
});
|
|
290
|
+
}).catch(() => logger.debug('Native callback was not admitted'));
|
|
278
291
|
});
|
|
279
292
|
client.once('clientReady', () => {
|
|
280
293
|
if (signal?.aborted)
|
|
@@ -334,3 +347,15 @@ export async function connectDiscordGatewayClient(client, config, handlers, sign
|
|
|
334
347
|
});
|
|
335
348
|
});
|
|
336
349
|
}
|
|
350
|
+
/** Logical cancellation only; the native SDK still owns its already-started ACK. */
|
|
351
|
+
function awaitOwnedAck(operation, signal) {
|
|
352
|
+
if (!signal)
|
|
353
|
+
return operation;
|
|
354
|
+
return new Promise((resolve, reject) => {
|
|
355
|
+
const abort = () => reject(signal.reason);
|
|
356
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
357
|
+
void operation.then(resolve, reject).finally(() => signal.removeEventListener('abort', abort));
|
|
358
|
+
if (signal.aborted)
|
|
359
|
+
abort();
|
|
360
|
+
});
|
|
361
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { activityTypeCode, formatButtonContent, formatInboundContent, formatOutboundBody, resolveChannelKind, resolveDiscordConfig, senderDisplayName, type DiscordActivity, type DiscordEndpointConfig, type DiscordButtonInbound, type DiscordInboundAttachment, type DiscordInboundMessage, type DiscordOutboundBody, type DiscordWireSegment, type ResolvedDiscordConfig, type ResolvedDiscordGatewayConfig, type ResolvedDiscordInteractionsConfig, } from './protocol.js';
|
|
2
|
-
export {
|
|
2
|
+
export { requireDiscordGatewayClient, type DiscordClient, type DiscordClientEventMap, } from './client.js';
|
|
3
3
|
export { checkDiscordPlatformPermit, discordGroupPermitResolver, normalizeDiscordSenderForPermit, platformPermit, } from './platform-permit.js';
|
|
4
|
-
export { DiscordGatewayEndpoint, DiscordInteractionsEndpoint, type
|
|
5
|
-
export { connectDiscordGatewayClient,
|
|
4
|
+
export { DiscordGatewayEndpoint, DiscordInteractionsEndpoint, type DiscordClientTransport, type DiscordEndpointOptions, type DiscordInteractionsEndpointOptions, } from './endpoint.js';
|
|
5
|
+
export { connectDiscordGatewayClient, discordClientOptions, normalizeDiscordMessage, resolveSenderRole, toMessageCreateOptions, type DiscordGatewayConnectHandlers, } from './gateway.js';
|
|
6
6
|
export { handleDiscordInteractionRequest, registerDiscordInteractionRoutes, type DiscordInteractionsHandler, } from './webhook.js';
|
|
7
|
+
export { DiscordAdapter } from './adapter.js';
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { activityTypeCode, formatButtonContent, formatInboundContent, formatOutboundBody, resolveChannelKind, resolveDiscordConfig, senderDisplayName, } from './protocol.js';
|
|
2
|
-
export {
|
|
2
|
+
export { requireDiscordGatewayClient, } from './client.js';
|
|
3
3
|
export { checkDiscordPlatformPermit, discordGroupPermitResolver, normalizeDiscordSenderForPermit, platformPermit, } from './platform-permit.js';
|
|
4
4
|
export { DiscordGatewayEndpoint, DiscordInteractionsEndpoint, } from './endpoint.js';
|
|
5
|
-
export { connectDiscordGatewayClient,
|
|
5
|
+
export { connectDiscordGatewayClient, discordClientOptions, normalizeDiscordMessage, resolveSenderRole, toMessageCreateOptions, } from './gateway.js';
|
|
6
6
|
export { handleDiscordInteractionRequest, registerDiscordInteractionRoutes, } from './webhook.js';
|
|
7
|
+
export { DiscordAdapter } from './adapter.js';
|
package/lib/protocol.d.ts
CHANGED
|
@@ -56,6 +56,8 @@ export interface DiscordInboundAttachment {
|
|
|
56
56
|
readonly size?: number;
|
|
57
57
|
}
|
|
58
58
|
export interface DiscordInboundMessage {
|
|
59
|
+
readonly event?: unknown;
|
|
60
|
+
readonly platformSentAt?: number;
|
|
59
61
|
readonly id: string;
|
|
60
62
|
readonly content: string;
|
|
61
63
|
readonly channelId: string;
|
|
@@ -75,6 +77,8 @@ export interface DiscordInboundMessage {
|
|
|
75
77
|
readonly mentionedBot?: boolean;
|
|
76
78
|
}
|
|
77
79
|
export interface DiscordButtonInbound {
|
|
80
|
+
readonly event?: unknown;
|
|
81
|
+
readonly platformSentAt?: number;
|
|
78
82
|
readonly guildId?: string;
|
|
79
83
|
readonly id: string;
|
|
80
84
|
readonly customId: string;
|
package/lib/protocol.js
CHANGED
|
@@ -330,6 +330,7 @@ export function interactionToInboundMessage(interaction) {
|
|
|
330
330
|
const user = interaction.member?.user
|
|
331
331
|
?? interaction.user;
|
|
332
332
|
return {
|
|
333
|
+
event: interaction,
|
|
333
334
|
id: String(interaction.id),
|
|
334
335
|
content: formatSlashCommandContent(interaction),
|
|
335
336
|
channelId: String(interaction.channel_id ?? ''),
|
package/lib/webhook.d.ts
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
|
+
import { type Endpoint } from 'zhin.js/adapter';
|
|
1
2
|
/**
|
|
2
3
|
* Discord interactions HTTP: signature → parse → admit.
|
|
3
4
|
*/
|
|
4
5
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
5
|
-
import { type Locale } from 'zhin.js/i18n';
|
|
6
6
|
import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
|
|
7
7
|
import { type DiscordInboundMessage, type DiscordButtonInbound, type ResolvedDiscordInteractionsConfig } from './protocol.js';
|
|
8
|
-
export interface DiscordInteractionsHandler {
|
|
8
|
+
export interface DiscordInteractionsHandler extends Endpoint {
|
|
9
9
|
readonly config: ResolvedDiscordInteractionsConfig;
|
|
10
10
|
readonly isOpen: boolean;
|
|
11
11
|
admit(msg: DiscordInboundMessage): void;
|
|
12
|
-
resolveLocale?(senderId: string): Promise<Locale | undefined>;
|
|
13
12
|
admitButton(interaction: DiscordButtonInbound): void;
|
|
14
13
|
admitPlatform(event: Record<string, unknown>): void;
|
|
15
14
|
}
|
package/lib/webhook.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { endpointSpec } from 'zhin.js/adapter';
|
|
1
2
|
import { createMessages } from 'zhin.js/i18n';
|
|
2
3
|
import { getLogger } from '@zhin.js/logger';
|
|
3
4
|
import { interactionToInboundMessage, verifyDiscordInteractionSignature, } from './protocol.js';
|
|
@@ -35,45 +36,56 @@ export async function handleDiscordInteractionRequest(request, response, handler
|
|
|
35
36
|
return;
|
|
36
37
|
}
|
|
37
38
|
const interaction = JSON.parse(rawBody);
|
|
38
|
-
if (handler.
|
|
39
|
-
|
|
39
|
+
if (!interaction || interaction.application_id !== handler.config.applicationId) {
|
|
40
|
+
response.writeHead(401, { 'Content-Type': 'text/plain' });
|
|
41
|
+
response.end('Unauthorized');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// A verified protocol challenge is not a business message and keeps its immediate ACK.
|
|
40
45
|
if (interaction.type === INTERACTION_TYPE_PING) {
|
|
46
|
+
if (handler.isOpen)
|
|
47
|
+
void handler[endpointSpec].events.run(() => handler.admitPlatform(interaction))
|
|
48
|
+
.catch(() => logger.debug('Protocol event dispatch did not complete'));
|
|
41
49
|
writeJson(response, 200, { type: INTERACTION_RESPONSE_PONG });
|
|
42
50
|
return;
|
|
43
51
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
await handler[endpointSpec].events.run(async () => {
|
|
53
|
+
if (handler.isOpen)
|
|
54
|
+
handler.admitPlatform(interaction);
|
|
55
|
+
if (interaction.type === 3) {
|
|
56
|
+
const data = interaction.data;
|
|
57
|
+
if (data?.component_type === 2 && typeof data.custom_id === 'string') {
|
|
58
|
+
if (!handler.isOpen) {
|
|
59
|
+
response.writeHead(503);
|
|
60
|
+
response.end('Unavailable');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const message = interactionToInboundMessage(interaction);
|
|
64
|
+
handler.admitButton({ event: interaction, id: message.id, customId: data.custom_id, channelId: message.channelId, channelKind: message.channelKind, userId: message.authorId, userName: message.authorName, ...(message.guildId ? { guildId: message.guildId } : {}), sourceMessageId: interaction.message?.id });
|
|
65
|
+
writeJson(response, 200, { type: 6 });
|
|
50
66
|
return;
|
|
51
67
|
}
|
|
68
|
+
}
|
|
69
|
+
if (interaction.type === INTERACTION_TYPE_APPLICATION_COMMAND) {
|
|
52
70
|
const message = interactionToInboundMessage(interaction);
|
|
53
|
-
|
|
54
|
-
|
|
71
|
+
if (handler.isOpen) {
|
|
72
|
+
handler.admit(message);
|
|
73
|
+
}
|
|
74
|
+
const locale = await handler[endpointSpec].resolveLocale(message.authorId);
|
|
75
|
+
// 即时响应(type 4):defer(type 5) 需要 followup PATCH,未实现会让用户端一直转圈
|
|
76
|
+
const commandName = String(interaction.data?.name ?? '');
|
|
77
|
+
writeJson(response, 200, {
|
|
78
|
+
type: INTERACTION_RESPONSE_CHANNEL_MESSAGE_WITH_SOURCE,
|
|
79
|
+
data: {
|
|
80
|
+
content: messages(locale, 'processing', { command: commandName }),
|
|
81
|
+
flags: INTERACTION_FLAG_EPHEMERAL,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
55
84
|
return;
|
|
56
85
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (handler.isOpen) {
|
|
61
|
-
handler.admit(message);
|
|
62
|
-
}
|
|
63
|
-
const locale = await handler.resolveLocale?.(message.authorId);
|
|
64
|
-
// 即时响应(type 4):defer(type 5) 需要 followup PATCH,未实现会让用户端一直转圈
|
|
65
|
-
const commandName = String(interaction.data?.name ?? '');
|
|
66
|
-
writeJson(response, 200, {
|
|
67
|
-
type: INTERACTION_RESPONSE_CHANNEL_MESSAGE_WITH_SOURCE,
|
|
68
|
-
data: {
|
|
69
|
-
content: messages(locale, 'processing', { command: commandName }),
|
|
70
|
-
flags: INTERACTION_FLAG_EPHEMERAL,
|
|
71
|
-
},
|
|
72
|
-
});
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
response.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
76
|
-
response.end('Unsupported interaction type');
|
|
86
|
+
response.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
87
|
+
response.end('Unsupported interaction type');
|
|
88
|
+
});
|
|
77
89
|
}
|
|
78
90
|
catch (error) {
|
|
79
91
|
logger.error('Discord interactions error:', error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-discord",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.20",
|
|
4
4
|
"description": "Zhin.js Discord adapter for Plugin Runtime (Gateway WebSocket)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -35,26 +35,26 @@
|
|
|
35
35
|
"@discordjs/ws": "1.2.3",
|
|
36
36
|
"discord.js": "^14.27.0",
|
|
37
37
|
"undici": "6.29.1",
|
|
38
|
-
"@zhin.js/adapter": "1.1.
|
|
39
|
-
"@zhin.js/agent-feature": "1.1.
|
|
40
|
-
"@zhin.js/client": "1.1.
|
|
38
|
+
"@zhin.js/adapter": "1.1.26",
|
|
39
|
+
"@zhin.js/agent-feature": "1.1.11",
|
|
40
|
+
"@zhin.js/client": "1.1.22",
|
|
41
41
|
"@zhin.js/console-contract": "1.1.3",
|
|
42
|
-
"@zhin.js/core": "1.1.
|
|
43
|
-
"@zhin.js/feature-kit": "1.1.
|
|
44
|
-
"@zhin.js/host-http": "1.1.
|
|
42
|
+
"@zhin.js/core": "1.1.54",
|
|
43
|
+
"@zhin.js/feature-kit": "1.1.10",
|
|
44
|
+
"@zhin.js/host-http": "1.1.12",
|
|
45
45
|
"@zhin.js/im-contract": "1.1.9",
|
|
46
46
|
"@zhin.js/logger": "1.1.2",
|
|
47
|
-
"@zhin.js/page": "1.1.
|
|
48
|
-
"@zhin.js/permission": "1.1.
|
|
49
|
-
"@zhin.js/skill": "1.1.
|
|
50
|
-
"@zhin.js/tool": "1.1.
|
|
47
|
+
"@zhin.js/page": "1.1.12",
|
|
48
|
+
"@zhin.js/permission": "1.1.10",
|
|
49
|
+
"@zhin.js/skill": "1.1.11",
|
|
50
|
+
"@zhin.js/tool": "1.1.13"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
53
|
"zod": "^4.0.0",
|
|
54
|
-
"@zhin.js/adapter": "^1.1.
|
|
55
|
-
"@zhin.js/command": "^1.1.
|
|
56
|
-
"@zhin.js/core": "^1.1.
|
|
57
|
-
"zhin.js": "^1.1.
|
|
54
|
+
"@zhin.js/adapter": "^1.1.26",
|
|
55
|
+
"@zhin.js/command": "^1.1.12",
|
|
56
|
+
"@zhin.js/core": "^1.1.54",
|
|
57
|
+
"zhin.js": "^1.1.19"
|
|
58
58
|
},
|
|
59
59
|
"peerDependenciesMeta": {
|
|
60
60
|
"@zhin.js/command": {
|
|
@@ -72,8 +72,8 @@
|
|
|
72
72
|
"typescript": "^6.0.3",
|
|
73
73
|
"vitest": "^4.1.10",
|
|
74
74
|
"zod": "^4.4.3",
|
|
75
|
-
"@zhin.js/host-http": "1.1.
|
|
76
|
-
"zhin.js": "1.1.
|
|
75
|
+
"@zhin.js/host-http": "1.1.12",
|
|
76
|
+
"zhin.js": "1.1.19"
|
|
77
77
|
},
|
|
78
78
|
"files": [
|
|
79
79
|
"adapters",
|
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
"features": [
|
|
105
105
|
{
|
|
106
106
|
"package": "@zhin.js/adapter",
|
|
107
|
-
"api": "^
|
|
107
|
+
"api": "^2.0.0"
|
|
108
108
|
},
|
|
109
109
|
{
|
|
110
110
|
"package": "@zhin.js/command",
|
package/plugin.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
-
import { createEndpointRuntimeState } from 'zhin.js/adapter';
|
|
3
2
|
import { definePlugin, pluginConsoleRpcToken } from 'zhin.js';
|
|
4
3
|
import { permissionHostToken } from '@zhin.js/permission';
|
|
5
4
|
import { checkDiscordPlatformPermit } from "./lib/platform-permit.js";
|
|
6
|
-
import { discordRuntimeStateToken } from "./lib/discord-runtime-state.js";
|
|
7
5
|
import { createDiscordConsoleRpc } from "./lib/console.js";
|
|
8
6
|
export default definePlugin({
|
|
9
7
|
name: 'discord',
|
|
@@ -11,8 +9,7 @@ export default definePlugin({
|
|
|
11
9
|
displayName: 'Discord Gateway Adapter',
|
|
12
10
|
},
|
|
13
11
|
setup(context) {
|
|
14
|
-
context.resources.provide(
|
|
15
|
-
context.resources.provide(pluginConsoleRpcToken, createDiscordConsoleRpc(context.resources, () => context.config.get()));
|
|
12
|
+
context.resources.provide(pluginConsoleRpcToken, createDiscordConsoleRpc(() => context.config.get()));
|
|
16
13
|
if (context.resources.has(permissionHostToken)) {
|
|
17
14
|
const host = context.resources.use(permissionHostToken);
|
|
18
15
|
return host.registerPlatform('discord', checkDiscordPlatformPermit);
|
package/src/adapter.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Adapter, type AdapterContext, type EndpointCreationContext } from 'zhin.js/adapter';
|
|
2
|
+
import { httpHostToken } from '@zhin.js/host-http';
|
|
3
|
+
import { REST, Routes } from 'discord.js';
|
|
4
|
+
import { DiscordGatewayEndpoint, DiscordInteractionsEndpoint } from './endpoint.js';
|
|
5
|
+
import { createDiscordRestApiAgent } from './rest-api-proxy.js';
|
|
6
|
+
import { resolveDiscordConfig, type DiscordEndpointConfig } from './protocol.js';
|
|
7
|
+
import type { DiscordClientEventMap } from './client.js';
|
|
8
|
+
|
|
9
|
+
export class DiscordAdapter extends Adapter<DiscordGatewayEndpoint | DiscordInteractionsEndpoint, DiscordEndpointConfig, DiscordClientEventMap> {
|
|
10
|
+
constructor(private readonly context: AdapterContext<DiscordEndpointConfig>) { super(); }
|
|
11
|
+
override get configurationIdentityFields(): readonly string[] { return ['id', 'applicationId']; }
|
|
12
|
+
override get configurationIdentityProbeFields(): readonly string[] { return ['token', 'publicKey']; }
|
|
13
|
+
override get configurationUpdateFields(): readonly string[] { return ['token', 'connection', 'restApiProxy', 'restApiProxy.port',
|
|
14
|
+
'gatewayFaultProxyUrl', 'intents', 'defaultActivity', 'defaultActivity.name', 'defaultActivity.type', 'defaultActivity.url', 'publicKey', 'interactionsPath']; }
|
|
15
|
+
protected override validateConfiguration(input: DiscordEndpointConfig): void { resolveDiscordConfig(input); }
|
|
16
|
+
protected override async identifyConfiguration(input: DiscordEndpointConfig, signal: AbortSignal): Promise<string> {
|
|
17
|
+
const config = resolveDiscordConfig(input);
|
|
18
|
+
const agent = config.connection === 'gateway' && config.restApiProxy ? createDiscordRestApiAgent(config.restApiProxy) : undefined;
|
|
19
|
+
const rest = new REST({ version: '10', retries: 0, rejectOnRateLimit: () => true, ...(agent ? { agent } : {}) }).setToken(config.token);
|
|
20
|
+
try {
|
|
21
|
+
const application = await rest.get(Routes.currentApplication(), { signal }) as { id?: unknown; verify_key?: unknown };
|
|
22
|
+
if (typeof application?.id !== 'string' || !application.id.trim()) throw new Error('Discord identity response is incomplete');
|
|
23
|
+
if (config.connection === 'interactions' && application.id !== config.applicationId) throw new Error('Discord application identity configuration does not match');
|
|
24
|
+
if (config.connection === 'interactions' && (typeof application.verify_key !== 'string'
|
|
25
|
+
|| application.verify_key.toLowerCase() !== config.publicKey.toLowerCase())) {
|
|
26
|
+
throw new Error('Discord verification key does not match the application identity');
|
|
27
|
+
}
|
|
28
|
+
return JSON.stringify(['discord', application.id]);
|
|
29
|
+
} finally { rest.clearHashSweeper(); rest.clearHandlerSweeper(); await agent?.destroyProxy(); }
|
|
30
|
+
}
|
|
31
|
+
protected async createEndpoint(input: DiscordEndpointConfig, execution: EndpointCreationContext): Promise<DiscordGatewayEndpoint | DiscordInteractionsEndpoint> {
|
|
32
|
+
const config = resolveDiscordConfig(input);
|
|
33
|
+
if (config.connection === 'interactions') return new DiscordInteractionsEndpoint({ id: execution.canonicalId, config, http: this.context.use(httpHostToken) });
|
|
34
|
+
const restApiAgent = config.restApiProxy ? createDiscordRestApiAgent(config.restApiProxy) : undefined;
|
|
35
|
+
try { return new DiscordGatewayEndpoint({ id: execution.canonicalId, config, restApiAgent }); }
|
|
36
|
+
catch (error) { await restApiAgent?.destroyProxy(); throw error; }
|
|
37
|
+
}
|
|
38
|
+
}
|