@zhin.js/adapter-discord 5.0.2 → 5.0.3
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 +41 -0
- package/README.md +46 -77
- package/adapters/discord.ts +46 -0
- package/agent/tools/add_role.ts +2 -2
- package/agent/tools/create_thread.ts +2 -2
- package/agent/tools/forum_post.ts +2 -2
- package/agent/tools/list_roles.ts +2 -2
- package/agent/tools/react.ts +2 -2
- package/agent/tools/remove_role.ts +2 -2
- package/agent/tools/send_embed.ts +2 -2
- package/lib/discord-agent-deps.d.ts +35 -0
- package/lib/discord-agent-deps.js +32 -0
- package/lib/endpoint.d.ts +71 -0
- package/lib/endpoint.js +355 -0
- package/lib/gateway.d.ts +122 -0
- package/lib/gateway.js +235 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +6 -0
- package/lib/platform-permit.d.ts +15 -0
- package/lib/{src/platform-permit.js → platform-permit.js} +1 -2
- package/lib/protocol.d.ts +135 -0
- package/lib/protocol.js +234 -0
- package/lib/webhook.d.ts +13 -0
- package/lib/webhook.js +86 -0
- package/package.json +43 -36
- package/plugin.ts +13 -0
- package/schema.json +63 -0
- package/src/discord-agent-deps.ts +68 -11
- package/src/endpoint.ts +385 -1167
- package/src/gateway.ts +337 -0
- package/src/index.ts +56 -157
- package/src/platform-permit.ts +1 -1
- package/src/protocol.ts +392 -0
- package/src/webhook.ts +121 -0
- package/client/Dashboard.tsx +0 -195
- package/client/index.tsx +0 -11
- package/client/tsconfig.json +0 -7
- package/client/utils/api.ts +0 -30
- package/dist/index.js +0 -29
- package/lib/agent/tools/add_role.js +0 -22
- package/lib/agent/tools/add_role.js.map +0 -1
- package/lib/agent/tools/create_thread.js +0 -23
- package/lib/agent/tools/create_thread.js.map +0 -1
- package/lib/agent/tools/forum_post.js +0 -22
- package/lib/agent/tools/forum_post.js.map +0 -1
- package/lib/agent/tools/list_roles.js +0 -20
- package/lib/agent/tools/list_roles.js.map +0 -1
- package/lib/agent/tools/react.js +0 -20
- package/lib/agent/tools/react.js.map +0 -1
- package/lib/agent/tools/remove_role.js +0 -22
- package/lib/agent/tools/remove_role.js.map +0 -1
- package/lib/agent/tools/send_embed.js +0 -40
- package/lib/agent/tools/send_embed.js.map +0 -1
- package/lib/src/adapter.js +0 -96
- package/lib/src/adapter.js.map +0 -1
- package/lib/src/discord-agent-deps.js +0 -10
- package/lib/src/discord-agent-deps.js.map +0 -1
- package/lib/src/endpoint-interactions.js +0 -284
- package/lib/src/endpoint-interactions.js.map +0 -1
- package/lib/src/endpoint.js +0 -1093
- package/lib/src/endpoint.js.map +0 -1
- package/lib/src/index.js +0 -159
- package/lib/src/index.js.map +0 -1
- package/lib/src/platform-permit.js.map +0 -1
- package/lib/src/segment-mapper.js +0 -2
- package/lib/src/segment-mapper.js.map +0 -1
- package/lib/src/types.js +0 -2
- package/lib/src/types.js.map +0 -1
- package/plugin.yml +0 -3
- package/src/adapter.ts +0 -108
- package/src/endpoint-interactions.ts +0 -354
- package/src/segment-mapper.ts +0 -1
- package/src/types.ts +0 -60
package/src/endpoint.ts
CHANGED
|
@@ -1,1239 +1,457 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* DiscordEndpoint — lifecycle, outbound, admit, gateway / interactions modes, agent tool surface.
|
|
3
3
|
*/
|
|
4
|
+
import { ChannelType } from 'discord.js';
|
|
5
|
+
import type { EndpointInstance } from '@zhin.js/adapter';
|
|
6
|
+
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
7
|
+
import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
|
|
8
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
9
|
+
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
10
|
+
import { registerDiscordAgentEndpoint } from './discord-agent-deps.js';
|
|
4
11
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
AttachmentBuilder,
|
|
14
|
-
MessageCreateOptions,
|
|
15
|
-
ChannelType,
|
|
16
|
-
REST,
|
|
17
|
-
Routes,
|
|
18
|
-
ApplicationCommandData,
|
|
19
|
-
ChatInputCommandInteraction,
|
|
20
|
-
ButtonInteraction,
|
|
21
|
-
InteractionType,
|
|
22
|
-
InteractionResponseType,
|
|
23
|
-
GuildMember,
|
|
24
|
-
ActionRowBuilder,
|
|
25
|
-
ButtonBuilder,
|
|
26
|
-
ButtonStyle,
|
|
27
|
-
PermissionFlagsBits,
|
|
28
|
-
type MessageEditOptions as DiscordEditMessageOptions,
|
|
29
|
-
} from 'discord.js';
|
|
12
|
+
connectDiscordGatewayClient,
|
|
13
|
+
defaultCreateClient,
|
|
14
|
+
DEFAULT_INTENTS,
|
|
15
|
+
resolveSenderRole,
|
|
16
|
+
toMessageCreateOptions,
|
|
17
|
+
type CreateDiscordClient,
|
|
18
|
+
type DiscordClientTransport,
|
|
19
|
+
} from './gateway.js';
|
|
30
20
|
import {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
type
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
import
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
get pluginLogger() {
|
|
58
|
-
return this.adapter.plugin.logger;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
get $id() {
|
|
62
|
-
return this.$config.name;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
constructor(public adapter: DiscordAdapter, public $config: DiscordGatewayConfig) {
|
|
66
|
-
const intents = $config.intents || [
|
|
67
|
-
GatewayIntentBits.Guilds,
|
|
68
|
-
GatewayIntentBits.GuildMessages,
|
|
69
|
-
GatewayIntentBits.MessageContent,
|
|
70
|
-
GatewayIntentBits.DirectMessages,
|
|
71
|
-
GatewayIntentBits.GuildMembers,
|
|
72
|
-
GatewayIntentBits.GuildMessageReactions,
|
|
73
|
-
];
|
|
74
|
-
|
|
75
|
-
super({ intents });
|
|
76
|
-
this.$connected = false;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
private async handleDiscordMessage(
|
|
80
|
-
msg: DiscordChannelMessage
|
|
81
|
-
): Promise<void> {
|
|
82
|
-
// 忽略机器人消息
|
|
83
|
-
if (msg.author.bot) return;
|
|
84
|
-
|
|
85
|
-
const message = this.$formatMessage(msg);
|
|
86
|
-
this.adapter.emit("message.receive", message);
|
|
87
|
-
this.pluginLogger.debug(
|
|
88
|
-
`${this.$config.name} recv ${message.$channel.type}(${message.$channel.id}): ${segment.raw(
|
|
89
|
-
message.$content
|
|
90
|
-
)}`
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
private async handleSlashCommand(
|
|
95
|
-
interaction: ChatInputCommandInteraction
|
|
96
|
-
): Promise<void> {
|
|
97
|
-
const commandName = interaction.commandName;
|
|
98
|
-
const handler = this.slashCommandHandlers.get(commandName);
|
|
21
|
+
formatButtonContent,
|
|
22
|
+
formatInboundContent,
|
|
23
|
+
formatOutboundBody,
|
|
24
|
+
senderDisplayName,
|
|
25
|
+
type DiscordButtonInbound,
|
|
26
|
+
type DiscordInboundMessage,
|
|
27
|
+
type DiscordOutboundBody,
|
|
28
|
+
type ResolvedDiscordGatewayConfig,
|
|
29
|
+
type ResolvedDiscordInteractionsConfig,
|
|
30
|
+
} from './protocol.js';
|
|
31
|
+
import { registerDiscordInteractionRoutes } from './webhook.js';
|
|
32
|
+
|
|
33
|
+
const DISCORD_API = 'https://discord.com/api/v10';
|
|
34
|
+
const logger = getLogger('discord');
|
|
35
|
+
|
|
36
|
+
export type {
|
|
37
|
+
CreateDiscordClient,
|
|
38
|
+
DiscordClientTransport,
|
|
39
|
+
} from './gateway.js';
|
|
40
|
+
|
|
41
|
+
export interface DiscordEndpointOptions {
|
|
42
|
+
readonly id: CapabilityId;
|
|
43
|
+
readonly gateway: MessageGateway;
|
|
44
|
+
readonly config: ResolvedDiscordGatewayConfig;
|
|
45
|
+
readonly createClient?: CreateDiscordClient;
|
|
46
|
+
}
|
|
99
47
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
`Error executing slash command /${commandName}:`,
|
|
109
|
-
error
|
|
110
|
-
);
|
|
48
|
+
export class DiscordGatewayEndpoint implements EndpointInstance {
|
|
49
|
+
readonly #options: DiscordEndpointOptions;
|
|
50
|
+
readonly #createClient: CreateDiscordClient;
|
|
51
|
+
#client: DiscordClientTransport | null = null;
|
|
52
|
+
#open = false;
|
|
53
|
+
#started = false;
|
|
54
|
+
#unregisterAgent?: () => void;
|
|
55
|
+
readonly #messageChannelMap = new Map<string, string>();
|
|
111
56
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
content: errorMessage,
|
|
116
|
-
ephemeral: true,
|
|
117
|
-
});
|
|
118
|
-
} else {
|
|
119
|
-
await interaction.reply({ content: errorMessage, ephemeral: true });
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
} else {
|
|
123
|
-
this.pluginLogger.warn(`Unknown slash command: /${commandName}`);
|
|
124
|
-
if (!interaction.replied) {
|
|
125
|
-
await interaction.reply({
|
|
126
|
-
content: "Unknown command.",
|
|
127
|
-
ephemeral: true,
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
}
|
|
57
|
+
constructor(options: DiscordEndpointOptions) {
|
|
58
|
+
this.#options = options;
|
|
59
|
+
this.#createClient = options.createClient ?? defaultCreateClient;
|
|
131
60
|
}
|
|
132
61
|
|
|
133
|
-
|
|
62
|
+
async start(): Promise<void> {
|
|
63
|
+
if (this.#started) return;
|
|
64
|
+
this.#started = true;
|
|
134
65
|
try {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
$id: interaction.id,
|
|
144
|
-
$adapter: "discord",
|
|
145
|
-
$endpoint: this.$config.name,
|
|
146
|
-
$sender: {
|
|
147
|
-
id: interaction.user.id,
|
|
148
|
-
name: interaction.user.username || interaction.user.displayName,
|
|
149
|
-
},
|
|
150
|
-
$channel: {
|
|
151
|
-
id: channel.id,
|
|
152
|
-
type: channelType,
|
|
153
|
-
},
|
|
154
|
-
$content: [{
|
|
155
|
-
type: "action",
|
|
156
|
-
data: {
|
|
157
|
-
id: interaction.customId,
|
|
158
|
-
payload: interaction.customId,
|
|
159
|
-
sourceMessageId: interaction.message?.id,
|
|
160
|
-
},
|
|
161
|
-
}],
|
|
162
|
-
$raw: interaction.customId,
|
|
163
|
-
$timestamp: Date.now(),
|
|
164
|
-
$recall: async () => {},
|
|
165
|
-
$reply: async (content: SendContent): Promise<string> => {
|
|
166
|
-
if (!interaction.channel?.isTextBased()) return "";
|
|
167
|
-
const result = await this.sendContentToChannel(interaction.channel as TextChannel, content);
|
|
168
|
-
return result.id;
|
|
169
|
-
},
|
|
170
|
-
});
|
|
171
|
-
this.adapter.emit("message.receive", message);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
async $connect(): Promise<void> {
|
|
175
|
-
return new Promise((resolve, reject) => {
|
|
176
|
-
// 监听消息事件
|
|
177
|
-
this.on("messageCreate", this.handleDiscordMessage.bind(this));
|
|
178
|
-
|
|
179
|
-
// 监听交互事件(Slash Commands + 按钮)
|
|
180
|
-
this.on("interactionCreate", async (interaction) => {
|
|
181
|
-
if (interaction.isChatInputCommand() && this.$config.enableSlashCommands) {
|
|
182
|
-
await this.handleSlashCommand(interaction);
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
if (interaction.isButton()) {
|
|
186
|
-
await this.handleButtonInteraction(interaction);
|
|
187
|
-
}
|
|
66
|
+
this.#unregisterAgent = registerDiscordAgentEndpoint(this.#options.config.name, this);
|
|
67
|
+
const intents = this.#options.config.intents?.length
|
|
68
|
+
? [...this.#options.config.intents]
|
|
69
|
+
: DEFAULT_INTENTS;
|
|
70
|
+
this.#client = this.#createClient(intents);
|
|
71
|
+
await connectDiscordGatewayClient(this.#client, this.#options.config, {
|
|
72
|
+
onMessage: (msg) => this.admit(msg),
|
|
73
|
+
onButton: (interaction) => this.admitButton(interaction),
|
|
188
74
|
});
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
this.
|
|
194
|
-
|
|
195
|
-
);
|
|
196
|
-
|
|
197
|
-
// 设置活动状态
|
|
198
|
-
if (this.$config.defaultActivity) {
|
|
199
|
-
this.user?.setActivity(this.$config.defaultActivity.name, {
|
|
200
|
-
type: this.getActivityType(this.$config.defaultActivity.type),
|
|
201
|
-
url: this.$config.defaultActivity.url,
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// 注册 Slash Commands
|
|
206
|
-
if (this.$config.enableSlashCommands && this.$config.slashCommands) {
|
|
207
|
-
await this.registerSlashCommands();
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
resolve();
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
// 监听错误事件
|
|
214
|
-
this.on("error", (error) => {
|
|
215
|
-
this.pluginLogger.error("Discord client error:", error);
|
|
216
|
-
this.$connected = false;
|
|
217
|
-
reject(error);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
// 登录
|
|
221
|
-
this.login(this.$config.token).catch((error) => {
|
|
222
|
-
this.pluginLogger.error("Failed to login to Discord:", error);
|
|
223
|
-
this.$connected = false;
|
|
224
|
-
reject(error);
|
|
225
|
-
});
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
async $disconnect(): Promise<void> {
|
|
230
|
-
try {
|
|
231
|
-
(this as unknown as import('node:events').EventEmitter).removeAllListeners();
|
|
232
|
-
await this.destroy();
|
|
233
|
-
this.$connected = false;
|
|
234
|
-
this.pluginLogger.info(`Discord endpoint ${this.$config.name} disconnected`);
|
|
75
|
+
logger.info(formatCompact({
|
|
76
|
+
op: 'connect',
|
|
77
|
+
endpoint: this.#options.config.name,
|
|
78
|
+
mode: 'gateway',
|
|
79
|
+
user: this.#client.user?.tag,
|
|
80
|
+
}));
|
|
235
81
|
} catch (error) {
|
|
236
|
-
this.
|
|
82
|
+
await this.stop();
|
|
83
|
+
logger.error('Failed to connect Discord gateway:', error);
|
|
237
84
|
throw error;
|
|
238
85
|
}
|
|
239
86
|
}
|
|
240
87
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
let channelId: string;
|
|
245
|
-
|
|
246
|
-
if (msg.channel.type === ChannelType.DM) {
|
|
247
|
-
channelType = "private";
|
|
248
|
-
channelId = msg.channel.id;
|
|
249
|
-
} else if (msg.channel.type === ChannelType.GroupDM) {
|
|
250
|
-
channelType = "group";
|
|
251
|
-
channelId = msg.channel.id;
|
|
252
|
-
} else {
|
|
253
|
-
channelType = "channel";
|
|
254
|
-
channelId = msg.channel.id;
|
|
255
|
-
}
|
|
88
|
+
open(): void {
|
|
89
|
+
this.#open = true;
|
|
90
|
+
}
|
|
256
91
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
92
|
+
close(): void {
|
|
93
|
+
this.#open = false;
|
|
94
|
+
}
|
|
260
95
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
[PermissionFlagsBits.Administrator, "ADMINISTRATOR"],
|
|
272
|
-
[PermissionFlagsBits.ManageRoles, "MANAGE_ROLES"],
|
|
273
|
-
[PermissionFlagsBits.ModerateMembers, "MODERATE_MEMBERS"],
|
|
274
|
-
[PermissionFlagsBits.ManageChannels, "MANAGE_CHANNELS"],
|
|
275
|
-
[PermissionFlagsBits.ManageGuild, "MANAGE_GUILD"],
|
|
276
|
-
];
|
|
277
|
-
for (const [bit, name] of permChecks) {
|
|
278
|
-
if (member.permissions.has(bit)) tokens.push(name);
|
|
279
|
-
}
|
|
280
|
-
if (guild.ownerId === msg.author.id) {
|
|
281
|
-
base.role = "owner";
|
|
282
|
-
tokens.push("guild_owner", "ADMINISTRATOR");
|
|
283
|
-
} else if (tokens.includes("ADMINISTRATOR") || tokens.includes("MODERATE_MEMBERS")) {
|
|
284
|
-
base.role = "admin";
|
|
285
|
-
} else {
|
|
286
|
-
base.role = "member";
|
|
287
|
-
}
|
|
288
|
-
base.permissions = tokens;
|
|
96
|
+
async stop(): Promise<void> {
|
|
97
|
+
this.#open = false;
|
|
98
|
+
this.#unregisterAgent?.();
|
|
99
|
+
this.#unregisterAgent = undefined;
|
|
100
|
+
if (this.#client) {
|
|
101
|
+
try {
|
|
102
|
+
this.#client.removeAllListeners();
|
|
103
|
+
await this.#client.destroy();
|
|
104
|
+
} catch {
|
|
105
|
+
/* ignore */
|
|
289
106
|
}
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
});
|
|
336
|
-
return res;
|
|
337
|
-
},
|
|
107
|
+
this.#client = null;
|
|
108
|
+
}
|
|
109
|
+
this.#started = false;
|
|
110
|
+
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
|
|
114
|
+
const body = formatOutboundBody(payload);
|
|
115
|
+
const messageId = await this.#sendBody(target, body);
|
|
116
|
+
this.#messageChannelMap.set(messageId, target);
|
|
117
|
+
logger.debug(formatCompact({
|
|
118
|
+
op: 'discord_send',
|
|
119
|
+
endpoint: this.#options.config.name,
|
|
120
|
+
target,
|
|
121
|
+
messageId,
|
|
122
|
+
}));
|
|
123
|
+
return messageId;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Test / internal: admit a message when open. */
|
|
127
|
+
admit(msg: DiscordInboundMessage): void {
|
|
128
|
+
if (!this.#open) return;
|
|
129
|
+
if (msg.authorBot) return;
|
|
130
|
+
this.#messageChannelMap.set(msg.id, msg.channelId);
|
|
131
|
+
void this.#options.gateway.receive({
|
|
132
|
+
adapter: this.#options.id,
|
|
133
|
+
target: msg.channelId,
|
|
134
|
+
content: formatInboundContent(msg),
|
|
135
|
+
sender: senderDisplayName(msg),
|
|
136
|
+
id: msg.id,
|
|
137
|
+
metadata: Object.freeze({
|
|
138
|
+
endpoint: this.#options.config.name,
|
|
139
|
+
channelKind: msg.channelKind,
|
|
140
|
+
userId: msg.authorId,
|
|
141
|
+
guildId: msg.guildId,
|
|
142
|
+
permissions: msg.permissionTokens,
|
|
143
|
+
role: resolveSenderRole(msg),
|
|
144
|
+
...(msg.mentionedBot ? { mentioned: true } : {}),
|
|
145
|
+
}),
|
|
146
|
+
}).catch((err) => {
|
|
147
|
+
logger.warn(formatCompact({
|
|
148
|
+
op: 'discord_gateway_receive_failed',
|
|
149
|
+
target: msg.channelId,
|
|
150
|
+
error: err instanceof Error ? err.message : String(err),
|
|
151
|
+
}));
|
|
338
152
|
});
|
|
339
|
-
|
|
340
|
-
this.messageChannelMap.set(result.$id, channelId);
|
|
341
|
-
|
|
342
|
-
return result;
|
|
343
153
|
}
|
|
344
154
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
for (const attachment of msg.attachments.values()) {
|
|
368
|
-
segments.push(...this.parseAttachment(attachment));
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// Embed 消息
|
|
372
|
-
for (const embed of msg.embeds) {
|
|
373
|
-
segments.push({
|
|
374
|
-
type: "embed",
|
|
375
|
-
data: {
|
|
376
|
-
title: embed.title,
|
|
377
|
-
description: embed.description,
|
|
378
|
-
color: embed.color,
|
|
379
|
-
url: embed.url,
|
|
380
|
-
thumbnail: embed.thumbnail,
|
|
381
|
-
image: embed.image,
|
|
382
|
-
author: embed.author,
|
|
383
|
-
footer: embed.footer,
|
|
384
|
-
fields: embed.fields,
|
|
385
|
-
timestamp: embed.timestamp,
|
|
386
|
-
},
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// 贴纸消息
|
|
391
|
-
for (const sticker of msg.stickers.values()) {
|
|
392
|
-
segments.push({
|
|
393
|
-
type: "sticker",
|
|
394
|
-
data: {
|
|
395
|
-
id: sticker.id,
|
|
396
|
-
name: sticker.name,
|
|
397
|
-
url: sticker.url,
|
|
398
|
-
format: sticker.format,
|
|
399
|
-
tags: sticker.tags,
|
|
400
|
-
},
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
return segments.length > 0
|
|
405
|
-
? segments
|
|
406
|
-
: [{ type: "text", data: { text: "" } }];
|
|
155
|
+
/** Test / internal: admit a button interaction when open. */
|
|
156
|
+
admitButton(interaction: DiscordButtonInbound): void {
|
|
157
|
+
if (!this.#open) return;
|
|
158
|
+
void this.#options.gateway.receive({
|
|
159
|
+
adapter: this.#options.id,
|
|
160
|
+
target: interaction.channelId,
|
|
161
|
+
content: formatButtonContent(interaction),
|
|
162
|
+
sender: interaction.userName,
|
|
163
|
+
id: interaction.id,
|
|
164
|
+
metadata: Object.freeze({
|
|
165
|
+
endpoint: this.#options.config.name,
|
|
166
|
+
eventType: 'button',
|
|
167
|
+
payload: interaction.customId,
|
|
168
|
+
sourceMessageId: interaction.sourceMessageId,
|
|
169
|
+
}),
|
|
170
|
+
}).catch((err) => {
|
|
171
|
+
logger.warn(formatCompact({
|
|
172
|
+
op: 'discord_gateway_receive_failed',
|
|
173
|
+
target: interaction.channelId,
|
|
174
|
+
error: err instanceof Error ? err.message : String(err),
|
|
175
|
+
}));
|
|
176
|
+
});
|
|
407
177
|
}
|
|
408
178
|
|
|
409
|
-
//
|
|
410
|
-
parseTextContent(
|
|
411
|
-
content: string,
|
|
412
|
-
msg: DiscordChannelMessage
|
|
413
|
-
): MessageSegment[] {
|
|
414
|
-
const segments: MessageSegment[] = [];
|
|
415
|
-
let lastIndex = 0;
|
|
416
|
-
|
|
417
|
-
// 匹配用户提及 <@!?用户ID>
|
|
418
|
-
const userMentionRegex = /<@!?(\d+)>/g;
|
|
419
|
-
// 匹配频道提及 <#频道ID>
|
|
420
|
-
const channelMentionRegex = /<#(\d+)>/g;
|
|
421
|
-
// 匹配角色提及 <@&角色ID>
|
|
422
|
-
const roleMentionRegex = /<@&(\d+)>/g;
|
|
423
|
-
// 匹配自定义表情 <:名称:ID> 或 <a:名称:ID>
|
|
424
|
-
const emojiRegex = /<a?:(\w+):(\d+)>/g;
|
|
425
|
-
|
|
426
|
-
const allMatches: Array<{
|
|
427
|
-
match: RegExpExecArray;
|
|
428
|
-
type: "user" | "channel" | "role" | "emoji";
|
|
429
|
-
}> = [];
|
|
430
|
-
|
|
431
|
-
// 收集所有匹配项
|
|
432
|
-
let match;
|
|
433
|
-
while ((match = userMentionRegex.exec(content)) !== null) {
|
|
434
|
-
allMatches.push({ match, type: "user" });
|
|
435
|
-
}
|
|
436
|
-
while ((match = channelMentionRegex.exec(content)) !== null) {
|
|
437
|
-
allMatches.push({ match, type: "channel" });
|
|
438
|
-
}
|
|
439
|
-
while ((match = roleMentionRegex.exec(content)) !== null) {
|
|
440
|
-
allMatches.push({ match, type: "role" });
|
|
441
|
-
}
|
|
442
|
-
while ((match = emojiRegex.exec(content)) !== null) {
|
|
443
|
-
allMatches.push({ match, type: "emoji" });
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// 按位置排序
|
|
447
|
-
allMatches.sort((a, b) => a.match.index! - b.match.index!);
|
|
448
|
-
|
|
449
|
-
// 处理每个匹配项
|
|
450
|
-
for (const { match, type } of allMatches) {
|
|
451
|
-
const matchStart = match.index!;
|
|
452
|
-
const matchEnd = matchStart + match[0].length;
|
|
453
|
-
|
|
454
|
-
// 添加匹配项前的文本
|
|
455
|
-
if (matchStart > lastIndex) {
|
|
456
|
-
const beforeText = content.slice(lastIndex, matchStart);
|
|
457
|
-
if (beforeText.trim()) {
|
|
458
|
-
segments.push({ type: "text", data: { text: beforeText } });
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// 添加特殊内容段
|
|
463
|
-
switch (type) {
|
|
464
|
-
case "user":
|
|
465
|
-
const userId = match[1];
|
|
466
|
-
const user = msg.mentions.users.get(userId);
|
|
467
|
-
segments.push({
|
|
468
|
-
type: "at",
|
|
469
|
-
data: {
|
|
470
|
-
id: userId,
|
|
471
|
-
name: user?.username || "Unknown",
|
|
472
|
-
text: match[0],
|
|
473
|
-
},
|
|
474
|
-
});
|
|
475
|
-
break;
|
|
476
|
-
|
|
477
|
-
case "channel":
|
|
478
|
-
const channelId = match[1];
|
|
479
|
-
const channel = msg.mentions.channels.get(channelId);
|
|
480
|
-
segments.push({
|
|
481
|
-
type: "channel_mention",
|
|
482
|
-
data: {
|
|
483
|
-
id: channelId,
|
|
484
|
-
name: (channel as any)?.name || "unknown-channel",
|
|
485
|
-
text: match[0],
|
|
486
|
-
},
|
|
487
|
-
});
|
|
488
|
-
break;
|
|
489
|
-
|
|
490
|
-
case "role":
|
|
491
|
-
const roleId = match[1];
|
|
492
|
-
const role = msg.mentions.roles.get(roleId);
|
|
493
|
-
segments.push({
|
|
494
|
-
type: "role_mention",
|
|
495
|
-
data: {
|
|
496
|
-
id: roleId,
|
|
497
|
-
name: role?.name || "unknown-role",
|
|
498
|
-
text: match[0],
|
|
499
|
-
},
|
|
500
|
-
});
|
|
501
|
-
break;
|
|
502
|
-
|
|
503
|
-
case "emoji":
|
|
504
|
-
const emojiName = match[1];
|
|
505
|
-
const emojiId = match[2];
|
|
506
|
-
const isAnimated = match[0].startsWith("<a:");
|
|
507
|
-
segments.push({
|
|
508
|
-
type: "emoji",
|
|
509
|
-
data: {
|
|
510
|
-
id: emojiId,
|
|
511
|
-
name: emojiName,
|
|
512
|
-
animated: isAnimated,
|
|
513
|
-
url: `https://cdn.discordapp.com/emojis/${emojiId}.${isAnimated ? "gif" : "png"
|
|
514
|
-
}`,
|
|
515
|
-
text: match[0],
|
|
516
|
-
},
|
|
517
|
-
});
|
|
518
|
-
break;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
lastIndex = matchEnd;
|
|
522
|
-
}
|
|
179
|
+
// ── Agent tool surface ──────────────────────────────────────────────
|
|
523
180
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
segments.push({ type: "text", data: { text: remainingText } });
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
return segments.length > 0
|
|
533
|
-
? segments
|
|
534
|
-
: [{ type: "text", data: { text: content } }];
|
|
181
|
+
async addRole(guildId: string, userId: string, roleId: string): Promise<boolean> {
|
|
182
|
+
const member = await this.#fetchMember(guildId, userId) as { roles: { add(id: string): Promise<unknown> } };
|
|
183
|
+
await member.roles.add(roleId);
|
|
184
|
+
return true;
|
|
535
185
|
}
|
|
536
186
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
content_type: attachment.contentType,
|
|
579
|
-
},
|
|
580
|
-
});
|
|
581
|
-
} else {
|
|
582
|
-
segments.push({
|
|
583
|
-
type: "file",
|
|
584
|
-
data: {
|
|
585
|
-
id: attachment.id,
|
|
586
|
-
name: attachment.name,
|
|
587
|
-
url: attachment.url,
|
|
588
|
-
proxy_url: attachment.proxyURL,
|
|
589
|
-
size: attachment.size,
|
|
590
|
-
content_type: attachment.contentType,
|
|
591
|
-
},
|
|
592
|
-
});
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
return segments;
|
|
187
|
+
async removeRole(guildId: string, userId: string, roleId: string): Promise<boolean> {
|
|
188
|
+
const member = await this.#fetchMember(guildId, userId) as { roles: { remove(id: string): Promise<unknown> } };
|
|
189
|
+
await member.roles.remove(roleId);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async getRoles(guildId: string): Promise<unknown[]> {
|
|
194
|
+
const guild = await this.#requireClient().guilds.fetch(guildId);
|
|
195
|
+
await guild.roles.fetch();
|
|
196
|
+
const cache = guild.roles.cache as Map<string, {
|
|
197
|
+
id: string;
|
|
198
|
+
name: string;
|
|
199
|
+
hexColor: string;
|
|
200
|
+
position: number;
|
|
201
|
+
permissions: { bitfield: bigint };
|
|
202
|
+
}>;
|
|
203
|
+
return [...cache.values()].map((role) => ({
|
|
204
|
+
id: role.id,
|
|
205
|
+
name: role.name,
|
|
206
|
+
color: role.hexColor,
|
|
207
|
+
position: role.position,
|
|
208
|
+
permissions: role.permissions.bitfield.toString(),
|
|
209
|
+
}));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async createThread(
|
|
213
|
+
channelId: string,
|
|
214
|
+
name: string,
|
|
215
|
+
messageId?: string,
|
|
216
|
+
autoArchiveDuration?: number,
|
|
217
|
+
): Promise<{ id: string }> {
|
|
218
|
+
const channel = await this.#requireClient().channels.fetch(channelId);
|
|
219
|
+
if (!channel || !('threads' in channel) || !channel.threads) {
|
|
220
|
+
throw new Error(`Channel ${channelId} 不支持创建帖子`);
|
|
221
|
+
}
|
|
222
|
+
const options: Record<string, unknown> = {
|
|
223
|
+
name,
|
|
224
|
+
autoArchiveDuration: autoArchiveDuration || 1440,
|
|
225
|
+
};
|
|
226
|
+
if (messageId) options.startMessage = messageId;
|
|
227
|
+
return channel.threads.create(options);
|
|
596
228
|
}
|
|
597
229
|
|
|
598
|
-
async
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
throw new Error(`Channel ${options.id} is not a text channel`);
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
const canonical = expandInteractiveSegmentsInContent(options.content);
|
|
606
|
-
const wire = fromCanonicalSegments(canonical);
|
|
607
|
-
const result = await this.sendContentToChannel(
|
|
608
|
-
channel as any,
|
|
609
|
-
wire
|
|
610
|
-
);
|
|
611
|
-
this.messageChannelMap.set(result.id, options.id);
|
|
612
|
-
this.pluginLogger.debug(
|
|
613
|
-
`${this.$config.name} send ${options.type}(${options.id}): ${segment.raw(options.content)}`
|
|
614
|
-
);
|
|
615
|
-
return result.id;
|
|
616
|
-
} catch (error) {
|
|
617
|
-
this.pluginLogger.error("Failed to send Discord message:", error);
|
|
618
|
-
throw error;
|
|
230
|
+
async addReaction(channelId: string, messageId: string, emoji: string): Promise<void> {
|
|
231
|
+
const channel = await this.#requireClient().channels.fetch(channelId);
|
|
232
|
+
if (!channel?.isTextBased() || !channel.messages) {
|
|
233
|
+
throw new Error(`Channel ${channelId} 不是文本频道`);
|
|
619
234
|
}
|
|
235
|
+
const message = await channel.messages.fetch(messageId);
|
|
236
|
+
await message.react(emoji);
|
|
620
237
|
}
|
|
621
238
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
const messageOptions: MessageCreateOptions = { ...extraOptions };
|
|
631
|
-
let textContent = "";
|
|
632
|
-
const embeds: EmbedBuilder[] = [];
|
|
633
|
-
const files: AttachmentBuilder[] = [];
|
|
634
|
-
|
|
635
|
-
for (const segment of content) {
|
|
636
|
-
if (typeof segment === "string") {
|
|
637
|
-
textContent += segment;
|
|
638
|
-
continue;
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
const { type, data } = segment;
|
|
642
|
-
|
|
643
|
-
switch (type) {
|
|
644
|
-
case "text":
|
|
645
|
-
textContent += data.text || "";
|
|
646
|
-
break;
|
|
647
|
-
|
|
648
|
-
case "at":
|
|
649
|
-
textContent += `<@${data.id}>`;
|
|
650
|
-
break;
|
|
651
|
-
|
|
652
|
-
case "channel_mention":
|
|
653
|
-
textContent += `<#${data.id}>`;
|
|
654
|
-
break;
|
|
655
|
-
|
|
656
|
-
case "role_mention":
|
|
657
|
-
textContent += `<@&${data.id}>`;
|
|
658
|
-
break;
|
|
659
|
-
|
|
660
|
-
case "emoji":
|
|
661
|
-
textContent += data.animated
|
|
662
|
-
? `<a:${data.name}:${data.id}>`
|
|
663
|
-
: `<:${data.name}:${data.id}>`;
|
|
664
|
-
break;
|
|
665
|
-
|
|
666
|
-
case "image":
|
|
667
|
-
case "audio":
|
|
668
|
-
case "video":
|
|
669
|
-
case "file":
|
|
670
|
-
await this.handleFileSegment(data, files, textContent);
|
|
671
|
-
break;
|
|
672
|
-
|
|
673
|
-
case "embed":
|
|
674
|
-
embeds.push(this.createEmbedFromData(data));
|
|
675
|
-
break;
|
|
676
|
-
|
|
677
|
-
case "keyboard": {
|
|
678
|
-
const components = (data.rows ?? []).map((row: Array<{ id: string; label: string; payload: string; disabled?: boolean; style?: string }>) =>
|
|
679
|
-
new ActionRowBuilder<ButtonBuilder>().addComponents(
|
|
680
|
-
...row.map((btn) => {
|
|
681
|
-
const b = new ButtonBuilder()
|
|
682
|
-
.setCustomId(String(btn.payload).slice(0, 100))
|
|
683
|
-
.setLabel(btn.label)
|
|
684
|
-
.setDisabled(!!btn.disabled);
|
|
685
|
-
if (btn.style === "danger") b.setStyle(ButtonStyle.Danger);
|
|
686
|
-
else if (btn.style === "primary") b.setStyle(ButtonStyle.Primary);
|
|
687
|
-
else b.setStyle(ButtonStyle.Secondary);
|
|
688
|
-
return b;
|
|
689
|
-
}),
|
|
690
|
-
),
|
|
691
|
-
);
|
|
692
|
-
messageOptions.components = components;
|
|
693
|
-
break;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
default:
|
|
697
|
-
// 未知类型作为文本处理
|
|
698
|
-
textContent += data.text || `[${type}]`;
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
// 设置消息内容
|
|
703
|
-
if (textContent.trim()) {
|
|
704
|
-
messageOptions.content = textContent.trim();
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
if (embeds.length > 0) {
|
|
708
|
-
messageOptions.embeds = embeds.slice(0, 10); // Discord 限制最多10个embed
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
if (files.length > 0) {
|
|
712
|
-
messageOptions.files = files;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
// 发送消息
|
|
716
|
-
return await channel.send(messageOptions);
|
|
239
|
+
async sendEmbed(
|
|
240
|
+
channelId: string,
|
|
241
|
+
embedData: Record<string, unknown>,
|
|
242
|
+
): Promise<{ id: string }> {
|
|
243
|
+
const body: DiscordOutboundBody = { embeds: [embedData] };
|
|
244
|
+
const id = await this.#sendBody(channelId, body);
|
|
245
|
+
return { id };
|
|
717
246
|
}
|
|
718
247
|
|
|
719
|
-
async
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
const
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
const b = new ButtonBuilder()
|
|
739
|
-
.setCustomId(String(btn.payload).slice(0, 100))
|
|
740
|
-
.setLabel(btn.label)
|
|
741
|
-
.setDisabled(!!btn.disabled);
|
|
742
|
-
if (btn.style === "danger") b.setStyle(ButtonStyle.Danger);
|
|
743
|
-
else if (btn.style === "primary") b.setStyle(ButtonStyle.Primary);
|
|
744
|
-
else b.setStyle(ButtonStyle.Secondary);
|
|
745
|
-
return b;
|
|
746
|
-
}),
|
|
747
|
-
),
|
|
748
|
-
);
|
|
749
|
-
}
|
|
248
|
+
async createForumPost(
|
|
249
|
+
channelId: string,
|
|
250
|
+
name: string,
|
|
251
|
+
content: string,
|
|
252
|
+
tags?: string[],
|
|
253
|
+
): Promise<{ id: string }> {
|
|
254
|
+
const channel = await this.#requireClient().channels.fetch(channelId);
|
|
255
|
+
if (!channel || channel.type !== ChannelType.GuildForum || !channel.threads) {
|
|
256
|
+
throw new Error(`Channel ${channelId} 不是论坛频道`);
|
|
257
|
+
}
|
|
258
|
+
const options: Record<string, unknown> = {
|
|
259
|
+
name,
|
|
260
|
+
message: { content },
|
|
261
|
+
};
|
|
262
|
+
if (tags?.length && channel.availableTags?.length) {
|
|
263
|
+
const tagIds = channel.availableTags
|
|
264
|
+
.filter((t) => tags.includes(t.name))
|
|
265
|
+
.map((t) => t.id);
|
|
266
|
+
if (tagIds.length) options.appliedTags = tagIds;
|
|
750
267
|
}
|
|
751
|
-
|
|
752
|
-
await msg.edit(messageOptions);
|
|
268
|
+
return channel.threads.create(options);
|
|
753
269
|
}
|
|
754
270
|
|
|
755
|
-
async $recallMessage(id: string): Promise<void> { }
|
|
756
|
-
|
|
757
|
-
// ==================== 服务器管理 API ====================
|
|
758
|
-
|
|
759
|
-
/**
|
|
760
|
-
* 踢出成员
|
|
761
|
-
* @param guildId 服务器 ID
|
|
762
|
-
* @param userId 用户 ID
|
|
763
|
-
* @param reason 原因
|
|
764
|
-
*/
|
|
765
271
|
async kickMember(guildId: string, userId: string, reason?: string): Promise<boolean> {
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
await member.kick(reason);
|
|
770
|
-
this.pluginLogger.info(`Discord Endpoint ${this.$id} 踢出成员 ${userId}(服务器 ${guildId})`);
|
|
771
|
-
return true;
|
|
772
|
-
} catch (error) {
|
|
773
|
-
this.pluginLogger.error(`Discord Endpoint ${this.$id} 踢出成员失败:`, error);
|
|
774
|
-
throw error;
|
|
775
|
-
}
|
|
272
|
+
const member = await this.#fetchMember(guildId, userId) as { kick(reason?: string): Promise<unknown> };
|
|
273
|
+
await member.kick(reason);
|
|
274
|
+
return true;
|
|
776
275
|
}
|
|
777
276
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
* @param reason 原因
|
|
783
|
-
* @param deleteMessageDays 删除消息天数
|
|
784
|
-
*/
|
|
785
|
-
async banMember(guildId: string, userId: string, reason?: string, deleteMessageDays?: number): Promise<boolean> {
|
|
786
|
-
try {
|
|
787
|
-
const guild = await this.guilds.fetch(guildId);
|
|
788
|
-
await guild.members.ban(userId, { reason, deleteMessageSeconds: deleteMessageDays ? deleteMessageDays * 86400 : undefined });
|
|
789
|
-
this.pluginLogger.info(`Discord Endpoint ${this.$id} 封禁成员 ${userId}(服务器 ${guildId})`);
|
|
790
|
-
return true;
|
|
791
|
-
} catch (error) {
|
|
792
|
-
this.pluginLogger.error(`Discord Endpoint ${this.$id} 封禁成员失败:`, error);
|
|
793
|
-
throw error;
|
|
794
|
-
}
|
|
277
|
+
async banMember(guildId: string, userId: string, reason?: string): Promise<boolean> {
|
|
278
|
+
const guild = await this.#requireClient().guilds.fetch(guildId);
|
|
279
|
+
await guild.members.ban(userId, { reason });
|
|
280
|
+
return true;
|
|
795
281
|
}
|
|
796
282
|
|
|
797
|
-
/**
|
|
798
|
-
* 解除封禁
|
|
799
|
-
* @param guildId 服务器 ID
|
|
800
|
-
* @param userId 用户 ID
|
|
801
|
-
* @param reason 原因
|
|
802
|
-
*/
|
|
803
283
|
async unbanMember(guildId: string, userId: string, reason?: string): Promise<boolean> {
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
* @param reason 原因
|
|
821
|
-
*/
|
|
822
|
-
async timeoutMember(guildId: string, userId: string, duration: number = 600, reason?: string): Promise<boolean> {
|
|
823
|
-
try {
|
|
824
|
-
const guild = await this.guilds.fetch(guildId);
|
|
825
|
-
const member = await guild.members.fetch(userId);
|
|
826
|
-
if (duration === 0) {
|
|
827
|
-
await member.timeout(null, reason);
|
|
828
|
-
this.pluginLogger.info(`Discord Endpoint ${this.$id} 取消成员 ${userId} 超时(服务器 ${guildId})`);
|
|
829
|
-
} else {
|
|
830
|
-
await member.timeout(duration * 1000, reason);
|
|
831
|
-
this.pluginLogger.info(`Discord Endpoint ${this.$id} 超时成员 ${userId} ${duration}秒(服务器 ${guildId})`);
|
|
832
|
-
}
|
|
833
|
-
return true;
|
|
834
|
-
} catch (error) {
|
|
835
|
-
this.pluginLogger.error(`Discord Endpoint ${this.$id} 超时操作失败:`, error);
|
|
836
|
-
throw error;
|
|
837
|
-
}
|
|
284
|
+
const guild = await this.#requireClient().guilds.fetch(guildId);
|
|
285
|
+
await guild.members.unban(userId, reason);
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async timeoutMember(
|
|
290
|
+
guildId: string,
|
|
291
|
+
userId: string,
|
|
292
|
+
duration = 600,
|
|
293
|
+
reason?: string,
|
|
294
|
+
): Promise<boolean> {
|
|
295
|
+
const member = await this.#fetchMember(guildId, userId) as {
|
|
296
|
+
timeout(ms: number | null, reason?: string): Promise<unknown>;
|
|
297
|
+
};
|
|
298
|
+
await member.timeout(duration === 0 ? null : duration * 1000, reason);
|
|
299
|
+
return true;
|
|
838
300
|
}
|
|
839
301
|
|
|
840
|
-
/**
|
|
841
|
-
* 修改成员昵称
|
|
842
|
-
* @param guildId 服务器 ID
|
|
843
|
-
* @param userId 用户 ID
|
|
844
|
-
* @param nickname 新昵称
|
|
845
|
-
*/
|
|
846
302
|
async setNickname(guildId: string, userId: string, nickname: string): Promise<boolean> {
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
* @param roleId 角色 ID
|
|
883
|
-
*/
|
|
884
|
-
async removeRole(guildId: string, userId: string, roleId: string): Promise<boolean> {
|
|
885
|
-
try {
|
|
886
|
-
const guild = await this.guilds.fetch(guildId);
|
|
887
|
-
const member = await guild.members.fetch(userId);
|
|
888
|
-
await member.roles.remove(roleId);
|
|
889
|
-
this.pluginLogger.info(`Discord Endpoint ${this.$id} 移除成员 ${userId} 的角色 ${roleId}(服务器 ${guildId})`);
|
|
890
|
-
return true;
|
|
891
|
-
} catch (error) {
|
|
892
|
-
this.pluginLogger.error(`Discord Endpoint ${this.$id} 移除角色失败:`, error);
|
|
893
|
-
throw error;
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
/**
|
|
898
|
-
* 获取服务器角色列表
|
|
899
|
-
* @param guildId 服务器 ID
|
|
900
|
-
*/
|
|
901
|
-
async getRoles(guildId: string): Promise<any[]> {
|
|
902
|
-
try {
|
|
903
|
-
const guild = await this.guilds.fetch(guildId);
|
|
904
|
-
await guild.roles.fetch();
|
|
905
|
-
return guild.roles.cache.map(role => ({
|
|
906
|
-
id: role.id,
|
|
907
|
-
name: role.name,
|
|
908
|
-
color: role.hexColor,
|
|
909
|
-
position: role.position,
|
|
910
|
-
permissions: role.permissions.bitfield.toString(),
|
|
911
|
-
}));
|
|
912
|
-
} catch (error) {
|
|
913
|
-
this.pluginLogger.error(`Discord Endpoint ${this.$id} 获取角色列表失败:`, error);
|
|
914
|
-
throw error;
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
/**
|
|
919
|
-
* 获取成员列表
|
|
920
|
-
* @param guildId 服务器 ID
|
|
921
|
-
* @param limit 数量限制
|
|
922
|
-
*/
|
|
923
|
-
async getMembers(guildId: string, limit: number = 100): Promise<any[]> {
|
|
924
|
-
try {
|
|
925
|
-
const guild = await this.guilds.fetch(guildId);
|
|
926
|
-
const members = await guild.members.fetch({ limit });
|
|
927
|
-
return Array.from(members.values()).map(member => ({
|
|
928
|
-
id: member.id,
|
|
929
|
-
username: member.user.username,
|
|
930
|
-
nickname: member.nickname,
|
|
931
|
-
roles: member.roles.cache.map(r => r.id),
|
|
932
|
-
joined_at: member.joinedAt?.toISOString(),
|
|
933
|
-
}));
|
|
934
|
-
} catch (error) {
|
|
935
|
-
this.pluginLogger.error(`Discord Endpoint ${this.$id} 获取成员列表失败:`, error);
|
|
936
|
-
throw error;
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
/**
|
|
941
|
-
* 获取服务器信息
|
|
942
|
-
* @param guildId 服务器 ID
|
|
943
|
-
*/
|
|
944
|
-
async getGuildInfo(guildId: string): Promise<any> {
|
|
945
|
-
try {
|
|
946
|
-
const guild = await this.guilds.fetch(guildId);
|
|
947
|
-
return {
|
|
948
|
-
id: guild.id,
|
|
949
|
-
name: guild.name,
|
|
950
|
-
icon: guild.iconURL(),
|
|
951
|
-
owner_id: guild.ownerId,
|
|
952
|
-
member_count: guild.memberCount,
|
|
953
|
-
created_at: guild.createdAt?.toISOString(),
|
|
954
|
-
};
|
|
955
|
-
} catch (error) {
|
|
956
|
-
this.pluginLogger.error(`Discord Endpoint ${this.$id} 获取服务器信息失败:`, error);
|
|
957
|
-
throw error;
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
async createThread(channelId: string, name: string, messageId?: string, autoArchiveDuration?: number): Promise<ThreadChannel> {
|
|
962
|
-
try {
|
|
963
|
-
const channel = await this.channels.fetch(channelId);
|
|
964
|
-
if (!channel || !('threads' in channel)) throw new Error(`Channel ${channelId} 不支持创建帖子`);
|
|
965
|
-
const options: any = { name, autoArchiveDuration: autoArchiveDuration || 1440 };
|
|
966
|
-
if (messageId) options.startMessage = messageId;
|
|
967
|
-
const thread = await (channel as TextChannel).threads.create(options);
|
|
968
|
-
this.pluginLogger.info(`Discord Endpoint ${this.$id} 创建帖子 "${name}" (channel ${channelId})`);
|
|
969
|
-
return thread;
|
|
970
|
-
} catch (error) {
|
|
971
|
-
this.pluginLogger.error(`Discord Endpoint ${this.$id} 创建帖子失败:`, error);
|
|
972
|
-
throw error;
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
async addReaction(channelId: string, messageId: string, emoji: string): Promise<void> {
|
|
977
|
-
try {
|
|
978
|
-
const channel = await this.channels.fetch(channelId);
|
|
979
|
-
if (!channel || !channel.isTextBased()) throw new Error(`Channel ${channelId} 不是文本频道`);
|
|
980
|
-
const message = await (channel as TextChannel).messages.fetch(messageId);
|
|
981
|
-
await message.react(emoji);
|
|
982
|
-
this.pluginLogger.info(`Discord Endpoint ${this.$id} 添加反应 ${emoji} (message ${messageId})`);
|
|
983
|
-
} catch (error) {
|
|
984
|
-
this.pluginLogger.error(`Discord Endpoint ${this.$id} 添加反应失败:`, error);
|
|
985
|
-
throw error;
|
|
986
|
-
}
|
|
303
|
+
const member = await this.#fetchMember(guildId, userId) as {
|
|
304
|
+
setNickname(nickname: string): Promise<unknown>;
|
|
305
|
+
};
|
|
306
|
+
await member.setNickname(nickname);
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async getMembers(guildId: string, limit = 100): Promise<unknown[]> {
|
|
311
|
+
const guild = await this.#requireClient().guilds.fetch(guildId);
|
|
312
|
+
const members = await guild.members.fetch({ limit }) as Map<string, {
|
|
313
|
+
id: string;
|
|
314
|
+
user: { username: string };
|
|
315
|
+
nickname: string | null;
|
|
316
|
+
roles: { cache: { map(fn: (r: { id: string }) => string): string[] } };
|
|
317
|
+
joinedAt?: Date | null;
|
|
318
|
+
}>;
|
|
319
|
+
return [...members.values()].map((member) => ({
|
|
320
|
+
id: member.id,
|
|
321
|
+
username: member.user.username,
|
|
322
|
+
nickname: member.nickname,
|
|
323
|
+
roles: member.roles.cache.map((r) => r.id),
|
|
324
|
+
joined_at: member.joinedAt?.toISOString(),
|
|
325
|
+
}));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async getGuildInfo(guildId: string): Promise<unknown> {
|
|
329
|
+
const guild = await this.#requireClient().guilds.fetch(guildId);
|
|
330
|
+
return {
|
|
331
|
+
id: guild.id,
|
|
332
|
+
name: guild.name,
|
|
333
|
+
icon: guild.iconURL?.(),
|
|
334
|
+
owner_id: guild.ownerId,
|
|
335
|
+
member_count: guild.memberCount,
|
|
336
|
+
created_at: guild.createdAt?.toISOString(),
|
|
337
|
+
};
|
|
987
338
|
}
|
|
988
339
|
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
this.messageChannelMap.set(msgId, channelId);
|
|
994
|
-
return { channelId, msgId };
|
|
995
|
-
}
|
|
340
|
+
async #sendBody(channelId: string, body: DiscordOutboundBody): Promise<string> {
|
|
341
|
+
const channel = await this.#requireClient().channels.fetch(channelId);
|
|
342
|
+
if (!channel || !channel.isTextBased() || !channel.send) {
|
|
343
|
+
throw new Error(`Channel ${channelId} is not a text channel`);
|
|
996
344
|
}
|
|
997
|
-
|
|
998
|
-
const
|
|
999
|
-
|
|
1000
|
-
return { channelId, msgId: messageId };
|
|
345
|
+
const options = await toMessageCreateOptions(body);
|
|
346
|
+
const result = await channel.send(options);
|
|
347
|
+
return result.id;
|
|
1001
348
|
}
|
|
1002
349
|
|
|
1003
|
-
async
|
|
1004
|
-
const
|
|
1005
|
-
|
|
1006
|
-
this.pluginLogger.warn(`Discord Endpoint ${this.$id} 无法根据 message_id=${messageId} 定位 channel_id,跳过 addReaction`);
|
|
1007
|
-
return null;
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
try {
|
|
1011
|
-
await this.addReaction(ref.channelId, ref.msgId, emoji);
|
|
1012
|
-
return emoji;
|
|
1013
|
-
} catch (error) {
|
|
1014
|
-
this.pluginLogger.error(`Discord Endpoint ${this.$id} 添加 reaction 失败:`, error);
|
|
1015
|
-
return null;
|
|
1016
|
-
}
|
|
350
|
+
async #fetchMember(guildId: string, userId: string): Promise<unknown> {
|
|
351
|
+
const guild = await this.#requireClient().guilds.fetch(guildId);
|
|
352
|
+
return guild.members.fetch(userId);
|
|
1017
353
|
}
|
|
1018
354
|
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
this.pluginLogger.warn(`Discord Endpoint ${this.$id} 无法根据 message_id=${messageId} 定位 channel_id,跳过 removeReaction`);
|
|
1023
|
-
return;
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
try {
|
|
1027
|
-
const channel = await this.channels.fetch(ref.channelId);
|
|
1028
|
-
if (!channel || !channel.isTextBased()) return;
|
|
1029
|
-
const message = await (channel as TextChannel).messages.fetch(ref.msgId);
|
|
1030
|
-
const targetReaction =
|
|
1031
|
-
message.reactions.resolve(reactionId as any) ||
|
|
1032
|
-
message.reactions.cache.find(
|
|
1033
|
-
(reaction) =>
|
|
1034
|
-
reaction.emoji.toString() === reactionId ||
|
|
1035
|
-
reaction.emoji.name === reactionId ||
|
|
1036
|
-
reaction.emoji.id === reactionId,
|
|
1037
|
-
);
|
|
1038
|
-
if (targetReaction && this.user?.id) {
|
|
1039
|
-
await targetReaction.users.remove(this.user.id);
|
|
1040
|
-
}
|
|
1041
|
-
} catch (error) {
|
|
1042
|
-
this.pluginLogger.error(`Discord Endpoint ${this.$id} 移除 reaction 失败:`, error);
|
|
1043
|
-
}
|
|
355
|
+
#requireClient(): DiscordClientTransport {
|
|
356
|
+
if (!this.#client) throw new Error('Discord client not connected');
|
|
357
|
+
return this.#client;
|
|
1044
358
|
}
|
|
359
|
+
}
|
|
1045
360
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
return msg;
|
|
1054
|
-
} catch (error) {
|
|
1055
|
-
this.pluginLogger.error(`Discord Endpoint ${this.$id} 发送 Embed 失败:`, error);
|
|
1056
|
-
throw error;
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
361
|
+
export interface DiscordInteractionsEndpointOptions {
|
|
362
|
+
readonly id: CapabilityId;
|
|
363
|
+
readonly gateway: MessageGateway;
|
|
364
|
+
readonly http: HttpHost;
|
|
365
|
+
readonly config: ResolvedDiscordInteractionsConfig;
|
|
366
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
367
|
+
}
|
|
1059
368
|
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
name,
|
|
1067
|
-
message: { content },
|
|
1068
|
-
};
|
|
1069
|
-
if (tags?.length && forumChannel.availableTags?.length) {
|
|
1070
|
-
const tagIds = forumChannel.availableTags
|
|
1071
|
-
.filter((t: any) => tags.includes(t.name))
|
|
1072
|
-
.map((t: any) => t.id);
|
|
1073
|
-
if (tagIds.length) options.appliedTags = tagIds;
|
|
1074
|
-
}
|
|
1075
|
-
const thread = await forumChannel.threads.create(options);
|
|
1076
|
-
this.pluginLogger.info(`Discord Endpoint ${this.$id} 创建论坛帖 "${name}" (channel ${channelId})`);
|
|
1077
|
-
return thread;
|
|
1078
|
-
} catch (error) {
|
|
1079
|
-
this.pluginLogger.error(`Discord Endpoint ${this.$id} 创建论坛帖失败:`, error);
|
|
1080
|
-
throw error;
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
369
|
+
export class DiscordInteractionsEndpoint implements EndpointInstance {
|
|
370
|
+
readonly #options: DiscordInteractionsEndpointOptions;
|
|
371
|
+
readonly #fetch: typeof globalThis.fetch;
|
|
372
|
+
#routeReleases: HttpRouteRegistration[] = [];
|
|
373
|
+
#open = false;
|
|
374
|
+
#started = false;
|
|
1083
375
|
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
files: AttachmentBuilder[],
|
|
1088
|
-
textContent: string
|
|
1089
|
-
): Promise<void> {
|
|
1090
|
-
if (data.file && (await this.fileExists(data.file))) {
|
|
1091
|
-
// 本地文件
|
|
1092
|
-
files.push(
|
|
1093
|
-
new AttachmentBuilder(createReadStream(data.file), {
|
|
1094
|
-
name: data.name || path.basename(data.file),
|
|
1095
|
-
})
|
|
1096
|
-
);
|
|
1097
|
-
} else if (data.url) {
|
|
1098
|
-
// URL 文件
|
|
1099
|
-
files.push(
|
|
1100
|
-
new AttachmentBuilder(data.url, {
|
|
1101
|
-
name: data.name || "attachment",
|
|
1102
|
-
})
|
|
1103
|
-
);
|
|
1104
|
-
} else if (data.buffer) {
|
|
1105
|
-
// Buffer 数据
|
|
1106
|
-
files.push(
|
|
1107
|
-
new AttachmentBuilder(data.buffer, {
|
|
1108
|
-
name: data.name || "attachment",
|
|
1109
|
-
})
|
|
1110
|
-
);
|
|
1111
|
-
}
|
|
376
|
+
constructor(options: DiscordInteractionsEndpointOptions) {
|
|
377
|
+
this.#options = options;
|
|
378
|
+
this.#fetch = options.fetch ?? globalThis.fetch;
|
|
1112
379
|
}
|
|
1113
380
|
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
const embed = new EmbedBuilder();
|
|
1117
|
-
|
|
1118
|
-
if (data.title) embed.setTitle(data.title);
|
|
1119
|
-
if (data.description) embed.setDescription(data.description);
|
|
1120
|
-
if (data.color) embed.setColor(data.color);
|
|
1121
|
-
if (data.url) embed.setURL(data.url);
|
|
1122
|
-
if (data.thumbnail?.url) embed.setThumbnail(data.thumbnail.url);
|
|
1123
|
-
if (data.image?.url) embed.setImage(data.image.url);
|
|
1124
|
-
if (data.author) embed.setAuthor(data.author);
|
|
1125
|
-
if (data.footer) embed.setFooter(data.footer);
|
|
1126
|
-
if (data.timestamp) embed.setTimestamp(new Date(data.timestamp));
|
|
1127
|
-
if (data.fields && Array.isArray(data.fields)) {
|
|
1128
|
-
embed.addFields(data.fields);
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
return embed;
|
|
381
|
+
get isOpen(): boolean {
|
|
382
|
+
return this.#open;
|
|
1132
383
|
}
|
|
1133
384
|
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
const activityTypes = {
|
|
1137
|
-
PLAYING: 0,
|
|
1138
|
-
STREAMING: 1,
|
|
1139
|
-
LISTENING: 2,
|
|
1140
|
-
WATCHING: 3,
|
|
1141
|
-
COMPETING: 5,
|
|
1142
|
-
};
|
|
1143
|
-
return activityTypes[type as keyof typeof activityTypes] || 0;
|
|
385
|
+
get config(): ResolvedDiscordInteractionsConfig {
|
|
386
|
+
return this.#options.config;
|
|
1144
387
|
}
|
|
1145
388
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
body: this.$config.slashCommands,
|
|
1157
|
-
});
|
|
1158
|
-
this.pluginLogger.info("Successfully registered global slash commands");
|
|
1159
|
-
} else {
|
|
1160
|
-
// 为每个服务器注册命令
|
|
1161
|
-
for (const guild of this.guilds.cache.values()) {
|
|
1162
|
-
await rest.put(
|
|
1163
|
-
Routes.applicationGuildCommands(this.user.id, guild.id),
|
|
1164
|
-
{ body: this.$config.slashCommands }
|
|
1165
|
-
);
|
|
1166
|
-
}
|
|
1167
|
-
this.pluginLogger.info("Successfully registered guild slash commands");
|
|
1168
|
-
}
|
|
1169
|
-
} catch (error) {
|
|
1170
|
-
this.pluginLogger.error("Failed to register slash commands:", error);
|
|
1171
|
-
}
|
|
389
|
+
async start(): Promise<void> {
|
|
390
|
+
if (this.#started) return;
|
|
391
|
+
this.#started = true;
|
|
392
|
+
this.#routeReleases.push(...registerDiscordInteractionRoutes(this.#options.http, this));
|
|
393
|
+
logger.info(formatCompact({
|
|
394
|
+
op: 'connect',
|
|
395
|
+
endpoint: this.#options.config.name,
|
|
396
|
+
mode: 'interactions',
|
|
397
|
+
path: this.#options.config.interactionsPath,
|
|
398
|
+
}));
|
|
1172
399
|
}
|
|
1173
400
|
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
commandName: string,
|
|
1177
|
-
handler: (interaction: ChatInputCommandInteraction) => Promise<void>
|
|
1178
|
-
) {
|
|
1179
|
-
this.slashCommandHandlers.set(commandName, handler);
|
|
401
|
+
open(): void {
|
|
402
|
+
this.#open = true;
|
|
1180
403
|
}
|
|
1181
404
|
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
return this.slashCommandHandlers.delete(commandName);
|
|
405
|
+
close(): void {
|
|
406
|
+
this.#open = false;
|
|
1185
407
|
}
|
|
1186
408
|
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
} catch {
|
|
1193
|
-
return false;
|
|
1194
|
-
}
|
|
409
|
+
async stop(): Promise<void> {
|
|
410
|
+
this.#open = false;
|
|
411
|
+
for (const release of this.#routeReleases.splice(0)) release();
|
|
412
|
+
this.#started = false;
|
|
413
|
+
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
|
|
1195
414
|
}
|
|
1196
415
|
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
})
|
|
1230
|
-
|
|
416
|
+
async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
|
|
417
|
+
const body = formatOutboundBody(payload);
|
|
418
|
+
const response = await this.#fetch(`${DISCORD_API}/channels/${target}/messages`, {
|
|
419
|
+
method: 'POST',
|
|
420
|
+
headers: {
|
|
421
|
+
Authorization: `Bot ${this.#options.config.token}`,
|
|
422
|
+
'Content-Type': 'application/json',
|
|
423
|
+
},
|
|
424
|
+
body: JSON.stringify(body),
|
|
425
|
+
});
|
|
426
|
+
const text = await response.text();
|
|
427
|
+
if (!response.ok) {
|
|
428
|
+
throw new Error(`Discord send failed (${response.status}): ${text.slice(0, 200)}`);
|
|
429
|
+
}
|
|
430
|
+
const data = JSON.parse(text) as { id?: string };
|
|
431
|
+
return data.id ?? '';
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
admit(msg: DiscordInboundMessage): void {
|
|
435
|
+
if (!this.#open) return;
|
|
436
|
+
void this.#options.gateway.receive({
|
|
437
|
+
adapter: this.#options.id,
|
|
438
|
+
target: msg.channelId,
|
|
439
|
+
content: formatInboundContent(msg),
|
|
440
|
+
sender: senderDisplayName(msg),
|
|
441
|
+
id: msg.id,
|
|
442
|
+
metadata: Object.freeze({
|
|
443
|
+
endpoint: this.#options.config.name,
|
|
444
|
+
channelKind: msg.channelKind,
|
|
445
|
+
userId: msg.authorId,
|
|
446
|
+
guildId: msg.guildId,
|
|
447
|
+
eventType: 'application_command',
|
|
448
|
+
}),
|
|
449
|
+
}).catch((err) => {
|
|
450
|
+
logger.warn(formatCompact({
|
|
451
|
+
op: 'discord_gateway_receive_failed',
|
|
452
|
+
target: msg.channelId,
|
|
453
|
+
error: err instanceof Error ? err.message : String(err),
|
|
454
|
+
}));
|
|
455
|
+
});
|
|
1231
456
|
}
|
|
1232
457
|
}
|
|
1233
|
-
|
|
1234
|
-
// ================================================================================================
|
|
1235
|
-
// DiscordInteractionsEndpoint 类(Interactions 端点模式)
|
|
1236
|
-
// ================================================================================================
|
|
1237
|
-
|
|
1238
|
-
import * as nacl from "tweetnacl";
|
|
1239
|
-
|