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