@zhin.js/adapter-discord 1.0.12 → 1.0.14
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 +15 -0
- package/package.json +21 -7
- package/src/index.ts +0 -1145
- package/tsconfig.json +0 -24
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @zhin.js/adapter-discord
|
|
2
2
|
|
|
3
|
+
## 1.0.14
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 547028f: fix: 优化包结构,优化客户端支持
|
|
8
|
+
- Updated dependencies [547028f]
|
|
9
|
+
- @zhin.js/types@1.0.5
|
|
10
|
+
- zhin.js@1.0.14
|
|
11
|
+
|
|
12
|
+
## 1.0.13
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- zhin.js@1.0.13
|
|
17
|
+
|
|
3
18
|
## 1.0.12
|
|
4
19
|
|
|
5
20
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-discord",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.14",
|
|
4
4
|
"description": "zhin adapter for discord",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -11,10 +11,17 @@
|
|
|
11
11
|
"import": "./lib/index.js"
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"zhin",
|
|
16
|
+
"adapter",
|
|
17
|
+
"discord"
|
|
18
|
+
],
|
|
19
|
+
"author": "lc-cn",
|
|
20
|
+
"license": "MIT",
|
|
14
21
|
"repository": {
|
|
15
|
-
"url": "git+https://github.com/
|
|
22
|
+
"url": "git+https://github.com/zhinjs/zhin.git",
|
|
16
23
|
"type": "git",
|
|
17
|
-
"directory": "adapters/discord"
|
|
24
|
+
"directory": "plugins/adapters/discord"
|
|
18
25
|
},
|
|
19
26
|
"dependencies": {
|
|
20
27
|
"discord.js": "^14.15.3",
|
|
@@ -26,16 +33,23 @@
|
|
|
26
33
|
"typescript": "^5.3.0"
|
|
27
34
|
},
|
|
28
35
|
"peerDependencies": {
|
|
29
|
-
"@zhin.js/types": "1.0.
|
|
30
|
-
"zhin.js": "1.0.
|
|
36
|
+
"@zhin.js/types": "1.0.5",
|
|
37
|
+
"zhin.js": "1.0.14"
|
|
31
38
|
},
|
|
32
39
|
"peerDependenciesMeta": {
|
|
33
40
|
"@zhin.js/types": {
|
|
34
41
|
"optional": true
|
|
35
42
|
}
|
|
36
43
|
},
|
|
44
|
+
"files": [
|
|
45
|
+
"lib",
|
|
46
|
+
"node",
|
|
47
|
+
"README.md",
|
|
48
|
+
"CHANGELOG.md"
|
|
49
|
+
],
|
|
37
50
|
"scripts": {
|
|
38
|
-
"build": "
|
|
39
|
-
"clean": "rm -rf lib"
|
|
51
|
+
"build": "pnpm build:node",
|
|
52
|
+
"clean": "rm -rf lib",
|
|
53
|
+
"build:node": "tsc"
|
|
40
54
|
}
|
|
41
55
|
}
|
package/src/index.ts
DELETED
|
@@ -1,1145 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Client,
|
|
3
|
-
GatewayIntentBits,
|
|
4
|
-
Message as DiscordMessage,
|
|
5
|
-
TextChannel,
|
|
6
|
-
DMChannel,
|
|
7
|
-
NewsChannel,
|
|
8
|
-
ThreadChannel,
|
|
9
|
-
EmbedBuilder,
|
|
10
|
-
AttachmentBuilder,
|
|
11
|
-
MessageCreateOptions,
|
|
12
|
-
ChannelType,
|
|
13
|
-
REST,
|
|
14
|
-
Routes,
|
|
15
|
-
ApplicationCommandData,
|
|
16
|
-
ChatInputCommandInteraction,
|
|
17
|
-
InteractionType,
|
|
18
|
-
InteractionResponseType,
|
|
19
|
-
} from "discord.js";
|
|
20
|
-
import {
|
|
21
|
-
Bot,
|
|
22
|
-
Adapter,
|
|
23
|
-
Plugin,
|
|
24
|
-
registerAdapter,
|
|
25
|
-
Message,
|
|
26
|
-
SendOptions,
|
|
27
|
-
SendContent,
|
|
28
|
-
MessageSegment,
|
|
29
|
-
segment,
|
|
30
|
-
useContext,
|
|
31
|
-
usePlugin,
|
|
32
|
-
} from "zhin.js";
|
|
33
|
-
import type { Context } from "koa";
|
|
34
|
-
import { createReadStream } from "fs";
|
|
35
|
-
import { promises as fs } from "fs";
|
|
36
|
-
import path from "path";
|
|
37
|
-
|
|
38
|
-
// 声明模块,注册 discord 适配器类型
|
|
39
|
-
declare module "@zhin.js/types" {
|
|
40
|
-
interface RegisteredAdapters {
|
|
41
|
-
discord: Adapter<DiscordBot>;
|
|
42
|
-
"discord-interactions": Adapter<DiscordInteractionsBot>;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Discord Gateway 模式配置
|
|
47
|
-
export type DiscordBotConfig = Bot.Config & {
|
|
48
|
-
context: "discord";
|
|
49
|
-
token: string;
|
|
50
|
-
name: string;
|
|
51
|
-
intents?: GatewayIntentBits[];
|
|
52
|
-
// Discord 特有配置
|
|
53
|
-
enableSlashCommands?: boolean;
|
|
54
|
-
globalCommands?: boolean;
|
|
55
|
-
defaultActivity?: {
|
|
56
|
-
name: string;
|
|
57
|
-
type: "PLAYING" | "STREAMING" | "LISTENING" | "WATCHING" | "COMPETING";
|
|
58
|
-
url?: string;
|
|
59
|
-
};
|
|
60
|
-
// Slash Commands 定义
|
|
61
|
-
slashCommands?: ApplicationCommandData[];
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
// Discord Interactions 模式配置
|
|
65
|
-
export interface DiscordInteractionsConfig extends Bot.Config {
|
|
66
|
-
context: "discord-interactions";
|
|
67
|
-
token: string;
|
|
68
|
-
name: string;
|
|
69
|
-
applicationId: string;
|
|
70
|
-
publicKey: string; // Discord 应用的 Public Key
|
|
71
|
-
interactionsPath: string; // 交互端点路径,如 '/discord/interactions'
|
|
72
|
-
// 是否同时使用 Gateway(默认 false)
|
|
73
|
-
useGateway?: boolean;
|
|
74
|
-
intents?: GatewayIntentBits[];
|
|
75
|
-
// Slash Commands 定义
|
|
76
|
-
slashCommands?: ApplicationCommandData[];
|
|
77
|
-
globalCommands?: boolean;
|
|
78
|
-
defaultActivity?: {
|
|
79
|
-
name: string;
|
|
80
|
-
type: "PLAYING" | "STREAMING" | "LISTENING" | "WATCHING" | "COMPETING";
|
|
81
|
-
url?: string;
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Bot 接口
|
|
86
|
-
export interface DiscordBot {
|
|
87
|
-
$config: DiscordBotConfig;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export interface DiscordInteractionsBot {
|
|
91
|
-
$config: DiscordInteractionsConfig;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Discord 消息类型
|
|
95
|
-
type DiscordChannelMessage = DiscordMessage<boolean>;
|
|
96
|
-
const plugin = usePlugin();
|
|
97
|
-
// 主要的 DiscordBot 类
|
|
98
|
-
export class DiscordBot
|
|
99
|
-
extends Client
|
|
100
|
-
implements Bot<DiscordChannelMessage, DiscordBotConfig>
|
|
101
|
-
{
|
|
102
|
-
$connected?: boolean;
|
|
103
|
-
private slashCommandHandlers: Map<
|
|
104
|
-
string,
|
|
105
|
-
(interaction: ChatInputCommandInteraction) => Promise<void>
|
|
106
|
-
> = new Map();
|
|
107
|
-
|
|
108
|
-
constructor(public $config: DiscordBotConfig) {
|
|
109
|
-
const intents = $config.intents || [
|
|
110
|
-
GatewayIntentBits.Guilds,
|
|
111
|
-
GatewayIntentBits.GuildMessages,
|
|
112
|
-
GatewayIntentBits.MessageContent,
|
|
113
|
-
GatewayIntentBits.DirectMessages,
|
|
114
|
-
GatewayIntentBits.GuildMembers,
|
|
115
|
-
GatewayIntentBits.GuildMessageReactions,
|
|
116
|
-
];
|
|
117
|
-
|
|
118
|
-
super({ intents });
|
|
119
|
-
this.$connected = false;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
private async handleDiscordMessage(
|
|
123
|
-
msg: DiscordChannelMessage
|
|
124
|
-
): Promise<void> {
|
|
125
|
-
// 忽略机器人消息
|
|
126
|
-
if (msg.author.bot) return;
|
|
127
|
-
|
|
128
|
-
const message = this.$formatMessage(msg);
|
|
129
|
-
plugin.dispatch("message.receive", message);
|
|
130
|
-
plugin.logger.info(
|
|
131
|
-
`${this.$config.name} recv ${message.$channel.type}(${message.$channel.id}): ${segment.raw(
|
|
132
|
-
message.$content
|
|
133
|
-
)}`
|
|
134
|
-
);
|
|
135
|
-
plugin.dispatch(`message.${message.$channel.type}.receive`, message);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
private async handleSlashCommand(
|
|
139
|
-
interaction: ChatInputCommandInteraction
|
|
140
|
-
): Promise<void> {
|
|
141
|
-
const commandName = interaction.commandName;
|
|
142
|
-
const handler = this.slashCommandHandlers.get(commandName);
|
|
143
|
-
|
|
144
|
-
if (handler) {
|
|
145
|
-
try {
|
|
146
|
-
await handler(interaction);
|
|
147
|
-
plugin.logger.info(
|
|
148
|
-
`Executed slash command: /${commandName} by ${interaction.user.tag}`
|
|
149
|
-
);
|
|
150
|
-
} catch (error) {
|
|
151
|
-
plugin.logger.error(
|
|
152
|
-
`Error executing slash command /${commandName}:`,
|
|
153
|
-
error
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
const errorMessage = "An error occurred while executing this command.";
|
|
157
|
-
if (interaction.replied || interaction.deferred) {
|
|
158
|
-
await interaction.followUp({
|
|
159
|
-
content: errorMessage,
|
|
160
|
-
ephemeral: true,
|
|
161
|
-
});
|
|
162
|
-
} else {
|
|
163
|
-
await interaction.reply({ content: errorMessage, ephemeral: true });
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
} else {
|
|
167
|
-
plugin.logger.warn(`Unknown slash command: /${commandName}`);
|
|
168
|
-
if (!interaction.replied) {
|
|
169
|
-
await interaction.reply({
|
|
170
|
-
content: "Unknown command.",
|
|
171
|
-
ephemeral: true,
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
async $connect(): Promise<void> {
|
|
178
|
-
return new Promise((resolve, reject) => {
|
|
179
|
-
// 监听消息事件
|
|
180
|
-
this.on("messageCreate", this.handleDiscordMessage.bind(this));
|
|
181
|
-
|
|
182
|
-
// 监听交互事件(Slash Commands)
|
|
183
|
-
if (this.$config.enableSlashCommands) {
|
|
184
|
-
this.on("interactionCreate", async (interaction) => {
|
|
185
|
-
if (interaction.isChatInputCommand()) {
|
|
186
|
-
await this.handleSlashCommand(interaction);
|
|
187
|
-
}
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// 监听就绪事件
|
|
192
|
-
this.once("clientReady", async () => {
|
|
193
|
-
this.$connected = true;
|
|
194
|
-
plugin.logger.info(
|
|
195
|
-
`Discord bot ${this.$config.name} connected successfully as ${this.user?.tag}`
|
|
196
|
-
);
|
|
197
|
-
|
|
198
|
-
// 设置活动状态
|
|
199
|
-
if (this.$config.defaultActivity) {
|
|
200
|
-
this.user?.setActivity(this.$config.defaultActivity.name, {
|
|
201
|
-
type: this.getActivityType(this.$config.defaultActivity.type),
|
|
202
|
-
url: this.$config.defaultActivity.url,
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// 注册 Slash Commands
|
|
207
|
-
if (this.$config.enableSlashCommands && this.$config.slashCommands) {
|
|
208
|
-
await this.registerSlashCommands();
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
resolve();
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
// 监听错误事件
|
|
215
|
-
this.on("error", (error) => {
|
|
216
|
-
plugin.logger.error("Discord client error:", error);
|
|
217
|
-
this.$connected = false;
|
|
218
|
-
reject(error);
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
// 登录
|
|
222
|
-
this.login(this.$config.token).catch((error) => {
|
|
223
|
-
plugin.logger.error("Failed to login to Discord:", error);
|
|
224
|
-
this.$connected = false;
|
|
225
|
-
reject(error);
|
|
226
|
-
});
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
async $disconnect(): Promise<void> {
|
|
231
|
-
try {
|
|
232
|
-
await this.destroy();
|
|
233
|
-
this.$connected = false;
|
|
234
|
-
plugin.logger.info(`Discord bot ${this.$config.name} disconnected`);
|
|
235
|
-
} catch (error) {
|
|
236
|
-
plugin.logger.error("Error disconnecting Discord bot:", error);
|
|
237
|
-
throw error;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
$formatMessage(msg: DiscordChannelMessage): Message<DiscordChannelMessage> {
|
|
242
|
-
// 确定聊天类型和ID
|
|
243
|
-
let channelType: "private" | "group" | "channel";
|
|
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
|
-
}
|
|
256
|
-
|
|
257
|
-
// 转换消息内容为 segment 格式
|
|
258
|
-
const content = this.parseMessageContent(msg);
|
|
259
|
-
|
|
260
|
-
const result = Message.from(msg, {
|
|
261
|
-
$id: msg.id,
|
|
262
|
-
$adapter: "discord",
|
|
263
|
-
$bot: this.$config.name,
|
|
264
|
-
$sender: {
|
|
265
|
-
id: msg.author.id,
|
|
266
|
-
name: msg.member?.displayName || msg.author.displayName,
|
|
267
|
-
},
|
|
268
|
-
$channel: {
|
|
269
|
-
id: channelId,
|
|
270
|
-
type: channelType,
|
|
271
|
-
},
|
|
272
|
-
$content: content,
|
|
273
|
-
$raw: msg.content,
|
|
274
|
-
$timestamp: msg.createdTimestamp,
|
|
275
|
-
$reply: async (
|
|
276
|
-
content: SendContent,
|
|
277
|
-
quote?: boolean | string
|
|
278
|
-
): Promise<string> => {
|
|
279
|
-
if (!Array.isArray(content)) content = [content];
|
|
280
|
-
|
|
281
|
-
const sendOptions: MessageCreateOptions = {};
|
|
282
|
-
|
|
283
|
-
// 处理回复消息
|
|
284
|
-
if (quote) {
|
|
285
|
-
const replyId = typeof quote === "boolean" ? result.$id : quote;
|
|
286
|
-
try {
|
|
287
|
-
const replyMessage = await msg.channel.messages.fetch(replyId);
|
|
288
|
-
sendOptions.reply = { messageReference: replyMessage };
|
|
289
|
-
} catch (error) {
|
|
290
|
-
plugin.logger.warn(
|
|
291
|
-
`Could not find message to reply to: ${replyId}`
|
|
292
|
-
);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const res = await this.sendContentToChannel(
|
|
297
|
-
msg.channel as any,
|
|
298
|
-
content,
|
|
299
|
-
sendOptions
|
|
300
|
-
);
|
|
301
|
-
return res.id;
|
|
302
|
-
},
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
return result;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// 解析 Discord 消息内容为 segment 格式
|
|
309
|
-
parseMessageContent(msg: DiscordChannelMessage): MessageSegment[] {
|
|
310
|
-
const segments: MessageSegment[] = [];
|
|
311
|
-
|
|
312
|
-
// 回复消息处理
|
|
313
|
-
if (msg.reference) {
|
|
314
|
-
segments.push({
|
|
315
|
-
type: "reply",
|
|
316
|
-
data: {
|
|
317
|
-
id: msg.reference.messageId,
|
|
318
|
-
channel_id: msg.reference.channelId,
|
|
319
|
-
guild_id: msg.reference.guildId,
|
|
320
|
-
},
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// 文本消息(包含提及、表情等)
|
|
325
|
-
if (msg.content) {
|
|
326
|
-
segments.push(...this.parseTextContent(msg.content, msg));
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// 附件消息
|
|
330
|
-
for (const attachment of msg.attachments.values()) {
|
|
331
|
-
segments.push(...this.parseAttachment(attachment));
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Embed 消息
|
|
335
|
-
for (const embed of msg.embeds) {
|
|
336
|
-
segments.push({
|
|
337
|
-
type: "embed",
|
|
338
|
-
data: {
|
|
339
|
-
title: embed.title,
|
|
340
|
-
description: embed.description,
|
|
341
|
-
color: embed.color,
|
|
342
|
-
url: embed.url,
|
|
343
|
-
thumbnail: embed.thumbnail,
|
|
344
|
-
image: embed.image,
|
|
345
|
-
author: embed.author,
|
|
346
|
-
footer: embed.footer,
|
|
347
|
-
fields: embed.fields,
|
|
348
|
-
timestamp: embed.timestamp,
|
|
349
|
-
},
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// 贴纸消息
|
|
354
|
-
for (const sticker of msg.stickers.values()) {
|
|
355
|
-
segments.push({
|
|
356
|
-
type: "sticker",
|
|
357
|
-
data: {
|
|
358
|
-
id: sticker.id,
|
|
359
|
-
name: sticker.name,
|
|
360
|
-
url: sticker.url,
|
|
361
|
-
format: sticker.format,
|
|
362
|
-
tags: sticker.tags,
|
|
363
|
-
},
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
return segments.length > 0
|
|
368
|
-
? segments
|
|
369
|
-
: [{ type: "text", data: { text: "" } }];
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// 解析文本内容,处理提及、频道引用、角色引用等
|
|
373
|
-
parseTextContent(
|
|
374
|
-
content: string,
|
|
375
|
-
msg: DiscordChannelMessage
|
|
376
|
-
): MessageSegment[] {
|
|
377
|
-
const segments: MessageSegment[] = [];
|
|
378
|
-
let lastIndex = 0;
|
|
379
|
-
|
|
380
|
-
// 匹配用户提及 <@!?用户ID>
|
|
381
|
-
const userMentionRegex = /<@!?(\d+)>/g;
|
|
382
|
-
// 匹配频道提及 <#频道ID>
|
|
383
|
-
const channelMentionRegex = /<#(\d+)>/g;
|
|
384
|
-
// 匹配角色提及 <@&角色ID>
|
|
385
|
-
const roleMentionRegex = /<@&(\d+)>/g;
|
|
386
|
-
// 匹配自定义表情 <:名称:ID> 或 <a:名称:ID>
|
|
387
|
-
const emojiRegex = /<a?:(\w+):(\d+)>/g;
|
|
388
|
-
|
|
389
|
-
const allMatches: Array<{
|
|
390
|
-
match: RegExpExecArray;
|
|
391
|
-
type: "user" | "channel" | "role" | "emoji";
|
|
392
|
-
}> = [];
|
|
393
|
-
|
|
394
|
-
// 收集所有匹配项
|
|
395
|
-
let match;
|
|
396
|
-
while ((match = userMentionRegex.exec(content)) !== null) {
|
|
397
|
-
allMatches.push({ match, type: "user" });
|
|
398
|
-
}
|
|
399
|
-
while ((match = channelMentionRegex.exec(content)) !== null) {
|
|
400
|
-
allMatches.push({ match, type: "channel" });
|
|
401
|
-
}
|
|
402
|
-
while ((match = roleMentionRegex.exec(content)) !== null) {
|
|
403
|
-
allMatches.push({ match, type: "role" });
|
|
404
|
-
}
|
|
405
|
-
while ((match = emojiRegex.exec(content)) !== null) {
|
|
406
|
-
allMatches.push({ match, type: "emoji" });
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// 按位置排序
|
|
410
|
-
allMatches.sort((a, b) => a.match.index! - b.match.index!);
|
|
411
|
-
|
|
412
|
-
// 处理每个匹配项
|
|
413
|
-
for (const { match, type } of allMatches) {
|
|
414
|
-
const matchStart = match.index!;
|
|
415
|
-
const matchEnd = matchStart + match[0].length;
|
|
416
|
-
|
|
417
|
-
// 添加匹配项前的文本
|
|
418
|
-
if (matchStart > lastIndex) {
|
|
419
|
-
const beforeText = content.slice(lastIndex, matchStart);
|
|
420
|
-
if (beforeText.trim()) {
|
|
421
|
-
segments.push({ type: "text", data: { text: beforeText } });
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// 添加特殊内容段
|
|
426
|
-
switch (type) {
|
|
427
|
-
case "user":
|
|
428
|
-
const userId = match[1];
|
|
429
|
-
const user = msg.mentions.users.get(userId);
|
|
430
|
-
segments.push({
|
|
431
|
-
type: "at",
|
|
432
|
-
data: {
|
|
433
|
-
id: userId,
|
|
434
|
-
name: user?.username || "Unknown",
|
|
435
|
-
text: match[0],
|
|
436
|
-
},
|
|
437
|
-
});
|
|
438
|
-
break;
|
|
439
|
-
|
|
440
|
-
case "channel":
|
|
441
|
-
const channelId = match[1];
|
|
442
|
-
const channel = msg.mentions.channels.get(channelId);
|
|
443
|
-
segments.push({
|
|
444
|
-
type: "channel_mention",
|
|
445
|
-
data: {
|
|
446
|
-
id: channelId,
|
|
447
|
-
name: (channel as any)?.name || "unknown-channel",
|
|
448
|
-
text: match[0],
|
|
449
|
-
},
|
|
450
|
-
});
|
|
451
|
-
break;
|
|
452
|
-
|
|
453
|
-
case "role":
|
|
454
|
-
const roleId = match[1];
|
|
455
|
-
const role = msg.mentions.roles.get(roleId);
|
|
456
|
-
segments.push({
|
|
457
|
-
type: "role_mention",
|
|
458
|
-
data: {
|
|
459
|
-
id: roleId,
|
|
460
|
-
name: role?.name || "unknown-role",
|
|
461
|
-
text: match[0],
|
|
462
|
-
},
|
|
463
|
-
});
|
|
464
|
-
break;
|
|
465
|
-
|
|
466
|
-
case "emoji":
|
|
467
|
-
const emojiName = match[1];
|
|
468
|
-
const emojiId = match[2];
|
|
469
|
-
const isAnimated = match[0].startsWith("<a:");
|
|
470
|
-
segments.push({
|
|
471
|
-
type: "emoji",
|
|
472
|
-
data: {
|
|
473
|
-
id: emojiId,
|
|
474
|
-
name: emojiName,
|
|
475
|
-
animated: isAnimated,
|
|
476
|
-
url: `https://cdn.discordapp.com/emojis/${emojiId}.${
|
|
477
|
-
isAnimated ? "gif" : "png"
|
|
478
|
-
}`,
|
|
479
|
-
text: match[0],
|
|
480
|
-
},
|
|
481
|
-
});
|
|
482
|
-
break;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
lastIndex = matchEnd;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// 添加最后剩余的文本
|
|
489
|
-
if (lastIndex < content.length) {
|
|
490
|
-
const remainingText = content.slice(lastIndex);
|
|
491
|
-
if (remainingText.trim()) {
|
|
492
|
-
segments.push({ type: "text", data: { text: remainingText } });
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
return segments.length > 0
|
|
497
|
-
? segments
|
|
498
|
-
: [{ type: "text", data: { text: content } }];
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// 解析附件
|
|
502
|
-
parseAttachment(attachment: any): MessageSegment[] {
|
|
503
|
-
const segments: MessageSegment[] = [];
|
|
504
|
-
|
|
505
|
-
if (attachment.contentType?.startsWith("image/")) {
|
|
506
|
-
segments.push({
|
|
507
|
-
type: "image",
|
|
508
|
-
data: {
|
|
509
|
-
id: attachment.id,
|
|
510
|
-
name: attachment.name,
|
|
511
|
-
url: attachment.url,
|
|
512
|
-
proxy_url: attachment.proxyURL,
|
|
513
|
-
size: attachment.size,
|
|
514
|
-
width: attachment.width,
|
|
515
|
-
height: attachment.height,
|
|
516
|
-
content_type: attachment.contentType,
|
|
517
|
-
},
|
|
518
|
-
});
|
|
519
|
-
} else if (attachment.contentType?.startsWith("audio/")) {
|
|
520
|
-
segments.push({
|
|
521
|
-
type: "audio",
|
|
522
|
-
data: {
|
|
523
|
-
id: attachment.id,
|
|
524
|
-
name: attachment.name,
|
|
525
|
-
url: attachment.url,
|
|
526
|
-
proxy_url: attachment.proxyURL,
|
|
527
|
-
size: attachment.size,
|
|
528
|
-
content_type: attachment.contentType,
|
|
529
|
-
},
|
|
530
|
-
});
|
|
531
|
-
} else if (attachment.contentType?.startsWith("video/")) {
|
|
532
|
-
segments.push({
|
|
533
|
-
type: "video",
|
|
534
|
-
data: {
|
|
535
|
-
id: attachment.id,
|
|
536
|
-
name: attachment.name,
|
|
537
|
-
url: attachment.url,
|
|
538
|
-
proxy_url: attachment.proxyURL,
|
|
539
|
-
size: attachment.size,
|
|
540
|
-
width: attachment.width,
|
|
541
|
-
height: attachment.height,
|
|
542
|
-
content_type: attachment.contentType,
|
|
543
|
-
},
|
|
544
|
-
});
|
|
545
|
-
} else {
|
|
546
|
-
segments.push({
|
|
547
|
-
type: "file",
|
|
548
|
-
data: {
|
|
549
|
-
id: attachment.id,
|
|
550
|
-
name: attachment.name,
|
|
551
|
-
url: attachment.url,
|
|
552
|
-
proxy_url: attachment.proxyURL,
|
|
553
|
-
size: attachment.size,
|
|
554
|
-
content_type: attachment.contentType,
|
|
555
|
-
},
|
|
556
|
-
});
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
return segments;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
async $sendMessage(options: SendOptions): Promise<string> {
|
|
563
|
-
options = await plugin.app.handleBeforeSend(options);
|
|
564
|
-
|
|
565
|
-
try {
|
|
566
|
-
const channel = await this.channels.fetch(options.id);
|
|
567
|
-
if (!channel || !channel.isTextBased()) {
|
|
568
|
-
throw new Error(`Channel ${options.id} is not a text channel`);
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
const result = await this.sendContentToChannel(
|
|
572
|
-
channel as any,
|
|
573
|
-
options.content
|
|
574
|
-
);
|
|
575
|
-
plugin.logger.info(
|
|
576
|
-
`${this.$config.name} send ${options.type}(${options.id}): ${segment.raw(options.content)}`
|
|
577
|
-
);
|
|
578
|
-
return result.id;
|
|
579
|
-
} catch (error) {
|
|
580
|
-
plugin.logger.error("Failed to send Discord message:", error);
|
|
581
|
-
throw error;
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// 发送内容到频道
|
|
586
|
-
async sendContentToChannel(
|
|
587
|
-
channel: TextChannel | DMChannel | NewsChannel | ThreadChannel,
|
|
588
|
-
content: SendContent,
|
|
589
|
-
extraOptions: MessageCreateOptions = {}
|
|
590
|
-
): Promise<DiscordMessage<boolean>> {
|
|
591
|
-
if (!Array.isArray(content)) content = [content];
|
|
592
|
-
|
|
593
|
-
const messageOptions: MessageCreateOptions = { ...extraOptions };
|
|
594
|
-
let textContent = "";
|
|
595
|
-
const embeds: EmbedBuilder[] = [];
|
|
596
|
-
const files: AttachmentBuilder[] = [];
|
|
597
|
-
|
|
598
|
-
for (const segment of content) {
|
|
599
|
-
if (typeof segment === "string") {
|
|
600
|
-
textContent += segment;
|
|
601
|
-
continue;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
const { type, data } = segment;
|
|
605
|
-
|
|
606
|
-
switch (type) {
|
|
607
|
-
case "text":
|
|
608
|
-
textContent += data.text || "";
|
|
609
|
-
break;
|
|
610
|
-
|
|
611
|
-
case "at":
|
|
612
|
-
textContent += `<@${data.id}>`;
|
|
613
|
-
break;
|
|
614
|
-
|
|
615
|
-
case "channel_mention":
|
|
616
|
-
textContent += `<#${data.id}>`;
|
|
617
|
-
break;
|
|
618
|
-
|
|
619
|
-
case "role_mention":
|
|
620
|
-
textContent += `<@&${data.id}>`;
|
|
621
|
-
break;
|
|
622
|
-
|
|
623
|
-
case "emoji":
|
|
624
|
-
textContent += data.animated
|
|
625
|
-
? `<a:${data.name}:${data.id}>`
|
|
626
|
-
: `<:${data.name}:${data.id}>`;
|
|
627
|
-
break;
|
|
628
|
-
|
|
629
|
-
case "image":
|
|
630
|
-
case "audio":
|
|
631
|
-
case "video":
|
|
632
|
-
case "file":
|
|
633
|
-
await this.handleFileSegment(data, files, textContent);
|
|
634
|
-
break;
|
|
635
|
-
|
|
636
|
-
case "embed":
|
|
637
|
-
embeds.push(this.createEmbedFromData(data));
|
|
638
|
-
break;
|
|
639
|
-
|
|
640
|
-
default:
|
|
641
|
-
// 未知类型作为文本处理
|
|
642
|
-
textContent += data.text || `[${type}]`;
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
// 设置消息内容
|
|
647
|
-
if (textContent.trim()) {
|
|
648
|
-
messageOptions.content = textContent.trim();
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
if (embeds.length > 0) {
|
|
652
|
-
messageOptions.embeds = embeds.slice(0, 10); // Discord 限制最多10个embed
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
if (files.length > 0) {
|
|
656
|
-
messageOptions.files = files;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
// 发送消息
|
|
660
|
-
return await channel.send(messageOptions);
|
|
661
|
-
}
|
|
662
|
-
async $recallMessage(id: string): Promise<void> {}
|
|
663
|
-
// 处理文件段
|
|
664
|
-
async handleFileSegment(
|
|
665
|
-
data: any,
|
|
666
|
-
files: AttachmentBuilder[],
|
|
667
|
-
textContent: string
|
|
668
|
-
): Promise<void> {
|
|
669
|
-
if (data.file && (await this.fileExists(data.file))) {
|
|
670
|
-
// 本地文件
|
|
671
|
-
files.push(
|
|
672
|
-
new AttachmentBuilder(createReadStream(data.file), {
|
|
673
|
-
name: data.name || path.basename(data.file),
|
|
674
|
-
})
|
|
675
|
-
);
|
|
676
|
-
} else if (data.url) {
|
|
677
|
-
// URL 文件
|
|
678
|
-
files.push(
|
|
679
|
-
new AttachmentBuilder(data.url, {
|
|
680
|
-
name: data.name || "attachment",
|
|
681
|
-
})
|
|
682
|
-
);
|
|
683
|
-
} else if (data.buffer) {
|
|
684
|
-
// Buffer 数据
|
|
685
|
-
files.push(
|
|
686
|
-
new AttachmentBuilder(data.buffer, {
|
|
687
|
-
name: data.name || "attachment",
|
|
688
|
-
})
|
|
689
|
-
);
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
// 从数据创建 Embed
|
|
694
|
-
createEmbedFromData(data: any): EmbedBuilder {
|
|
695
|
-
const embed = new EmbedBuilder();
|
|
696
|
-
|
|
697
|
-
if (data.title) embed.setTitle(data.title);
|
|
698
|
-
if (data.description) embed.setDescription(data.description);
|
|
699
|
-
if (data.color) embed.setColor(data.color);
|
|
700
|
-
if (data.url) embed.setURL(data.url);
|
|
701
|
-
if (data.thumbnail?.url) embed.setThumbnail(data.thumbnail.url);
|
|
702
|
-
if (data.image?.url) embed.setImage(data.image.url);
|
|
703
|
-
if (data.author) embed.setAuthor(data.author);
|
|
704
|
-
if (data.footer) embed.setFooter(data.footer);
|
|
705
|
-
if (data.timestamp) embed.setTimestamp(new Date(data.timestamp));
|
|
706
|
-
if (data.fields && Array.isArray(data.fields)) {
|
|
707
|
-
embed.addFields(data.fields);
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
return embed;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
// 工具方法:获取活动类型
|
|
714
|
-
private getActivityType(type: string) {
|
|
715
|
-
const activityTypes = {
|
|
716
|
-
PLAYING: 0,
|
|
717
|
-
STREAMING: 1,
|
|
718
|
-
LISTENING: 2,
|
|
719
|
-
WATCHING: 3,
|
|
720
|
-
COMPETING: 5,
|
|
721
|
-
};
|
|
722
|
-
return activityTypes[type as keyof typeof activityTypes] || 0;
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
// 注册 Slash Commands
|
|
726
|
-
private async registerSlashCommands(): Promise<void> {
|
|
727
|
-
if (!this.$config.slashCommands || !this.user) return;
|
|
728
|
-
|
|
729
|
-
try {
|
|
730
|
-
const rest = new REST({ version: "10" }).setToken(this.$config.token);
|
|
731
|
-
|
|
732
|
-
if (this.$config.globalCommands) {
|
|
733
|
-
// 注册全局命令
|
|
734
|
-
await rest.put(Routes.applicationCommands(this.user.id), {
|
|
735
|
-
body: this.$config.slashCommands,
|
|
736
|
-
});
|
|
737
|
-
plugin.logger.info("Successfully registered global slash commands");
|
|
738
|
-
} else {
|
|
739
|
-
// 为每个服务器注册命令
|
|
740
|
-
for (const guild of this.guilds.cache.values()) {
|
|
741
|
-
await rest.put(
|
|
742
|
-
Routes.applicationGuildCommands(this.user.id, guild.id),
|
|
743
|
-
{ body: this.$config.slashCommands }
|
|
744
|
-
);
|
|
745
|
-
}
|
|
746
|
-
plugin.logger.info("Successfully registered guild slash commands");
|
|
747
|
-
}
|
|
748
|
-
} catch (error) {
|
|
749
|
-
plugin.logger.error("Failed to register slash commands:", error);
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
// 添加 Slash Command 处理器
|
|
754
|
-
addSlashCommandHandler(
|
|
755
|
-
commandName: string,
|
|
756
|
-
handler: (interaction: ChatInputCommandInteraction) => Promise<void>
|
|
757
|
-
) {
|
|
758
|
-
this.slashCommandHandlers.set(commandName, handler);
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
// 移除 Slash Command 处理器
|
|
762
|
-
removeSlashCommandHandler(commandName: string): boolean {
|
|
763
|
-
return this.slashCommandHandlers.delete(commandName);
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
// 工具方法:检查文件是否存在
|
|
767
|
-
private async fileExists(filePath: string): Promise<boolean> {
|
|
768
|
-
try {
|
|
769
|
-
await fs.access(filePath);
|
|
770
|
-
return true;
|
|
771
|
-
} catch {
|
|
772
|
-
return false;
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
// 静态方法:格式化内容为文本(用于日志显示)
|
|
777
|
-
static formatContentToText(content: SendContent): string {
|
|
778
|
-
if (!Array.isArray(content)) content = [content];
|
|
779
|
-
|
|
780
|
-
return content
|
|
781
|
-
.map((segment) => {
|
|
782
|
-
if (typeof segment === "string") return segment;
|
|
783
|
-
|
|
784
|
-
switch (segment.type) {
|
|
785
|
-
case "text":
|
|
786
|
-
return segment.data.text || "";
|
|
787
|
-
case "at":
|
|
788
|
-
return `@${segment.data.name || segment.data.id}`;
|
|
789
|
-
case "channel_mention":
|
|
790
|
-
return `#${segment.data.name}`;
|
|
791
|
-
case "role_mention":
|
|
792
|
-
return `@${segment.data.name}`;
|
|
793
|
-
case "image":
|
|
794
|
-
return "[图片]";
|
|
795
|
-
case "audio":
|
|
796
|
-
return "[音频]";
|
|
797
|
-
case "video":
|
|
798
|
-
return "[视频]";
|
|
799
|
-
case "file":
|
|
800
|
-
return "[文件]";
|
|
801
|
-
case "embed":
|
|
802
|
-
return "[嵌入消息]";
|
|
803
|
-
case "emoji":
|
|
804
|
-
return `:${segment.data.name}:`;
|
|
805
|
-
default:
|
|
806
|
-
return `[${segment.type}]`;
|
|
807
|
-
}
|
|
808
|
-
})
|
|
809
|
-
.join("");
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
// ================================================================================================
|
|
814
|
-
// DiscordInteractionsBot 类(Interactions 端点模式)
|
|
815
|
-
// ================================================================================================
|
|
816
|
-
|
|
817
|
-
import * as nacl from "tweetnacl";
|
|
818
|
-
|
|
819
|
-
export class DiscordInteractionsBot
|
|
820
|
-
extends Client
|
|
821
|
-
implements Bot<any, DiscordInteractionsConfig>
|
|
822
|
-
{
|
|
823
|
-
$connected?: boolean;
|
|
824
|
-
private router: any;
|
|
825
|
-
private slashCommandHandlers: Map<
|
|
826
|
-
string,
|
|
827
|
-
(interaction: any) => Promise<void>
|
|
828
|
-
> = new Map();
|
|
829
|
-
|
|
830
|
-
constructor(router: any, public $config: DiscordInteractionsConfig) {
|
|
831
|
-
const intents = $config.intents || [
|
|
832
|
-
GatewayIntentBits.Guilds,
|
|
833
|
-
GatewayIntentBits.GuildMessages,
|
|
834
|
-
GatewayIntentBits.MessageContent,
|
|
835
|
-
];
|
|
836
|
-
|
|
837
|
-
super({ intents });
|
|
838
|
-
this.$connected = false;
|
|
839
|
-
this.router = router;
|
|
840
|
-
|
|
841
|
-
// 设置交互端点路由
|
|
842
|
-
this.setupInteractionsEndpoint();
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
private setupInteractionsEndpoint(): void {
|
|
846
|
-
// 设置路由处理 Discord Interactions
|
|
847
|
-
this.router.post(this.$config.interactionsPath, (ctx: Context) => {
|
|
848
|
-
this.handleInteraction(ctx);
|
|
849
|
-
});
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
private async handleInteraction(ctx: Context): Promise<void> {
|
|
853
|
-
try {
|
|
854
|
-
const signature = ctx.get("x-signature-ed25519");
|
|
855
|
-
const timestamp = ctx.get("x-signature-timestamp");
|
|
856
|
-
const bodyString = JSON.stringify((ctx.request as any).body);
|
|
857
|
-
|
|
858
|
-
// 验证请求签名
|
|
859
|
-
if (!this.verifyDiscordSignature(bodyString, signature, timestamp)) {
|
|
860
|
-
plugin.logger.warn("Invalid Discord signature");
|
|
861
|
-
ctx.status = 401;
|
|
862
|
-
ctx.body = "Unauthorized";
|
|
863
|
-
return;
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
const interaction = (ctx.request as any).body;
|
|
867
|
-
|
|
868
|
-
// 处理不同类型的交互
|
|
869
|
-
if (interaction.type === InteractionType.Ping) {
|
|
870
|
-
// PING - Discord 验证端点
|
|
871
|
-
ctx.body = { type: InteractionResponseType.Pong };
|
|
872
|
-
} else if (interaction.type === InteractionType.ApplicationCommand) {
|
|
873
|
-
// APPLICATION_COMMAND - 应用命令
|
|
874
|
-
const response = await this.handleApplicationCommand(interaction);
|
|
875
|
-
ctx.body = response;
|
|
876
|
-
} else {
|
|
877
|
-
// 其他交互类型
|
|
878
|
-
ctx.status = 400;
|
|
879
|
-
ctx.body = "Unsupported interaction type";
|
|
880
|
-
}
|
|
881
|
-
} catch (error) {
|
|
882
|
-
plugin.logger.error("Interactions error:", error);
|
|
883
|
-
ctx.status = 500;
|
|
884
|
-
ctx.body = "Internal Server Error";
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
private verifyDiscordSignature(
|
|
889
|
-
body: string,
|
|
890
|
-
signature: string,
|
|
891
|
-
timestamp: string
|
|
892
|
-
): boolean {
|
|
893
|
-
try {
|
|
894
|
-
const publicKey = Buffer.from(this.$config.publicKey, "hex");
|
|
895
|
-
const sig = Buffer.from(signature, "hex");
|
|
896
|
-
const message = Buffer.from(timestamp + body, "utf8");
|
|
897
|
-
|
|
898
|
-
return nacl.sign.detached.verify(message, sig, publicKey);
|
|
899
|
-
} catch (error) {
|
|
900
|
-
plugin.logger.error("Signature verification error:", error);
|
|
901
|
-
return false;
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
private async handleApplicationCommand(interaction: any): Promise<any> {
|
|
906
|
-
// 处理应用命令
|
|
907
|
-
const commandName = interaction.data.name;
|
|
908
|
-
|
|
909
|
-
// 转换为标准消息格式并分发
|
|
910
|
-
const message = this.formatInteractionAsMessage(interaction);
|
|
911
|
-
plugin.dispatch("message.receive", message);
|
|
912
|
-
|
|
913
|
-
// 查找自定义处理器
|
|
914
|
-
const handler = this.slashCommandHandlers.get(commandName);
|
|
915
|
-
if (handler) {
|
|
916
|
-
try {
|
|
917
|
-
await handler(interaction);
|
|
918
|
-
} catch (error) {
|
|
919
|
-
plugin.logger.error(
|
|
920
|
-
`Error in slash command handler for ${commandName}:`,
|
|
921
|
-
error
|
|
922
|
-
);
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
// 默认响应
|
|
927
|
-
return {
|
|
928
|
-
type: InteractionResponseType.ChannelMessageWithSource,
|
|
929
|
-
data: {
|
|
930
|
-
content: `处理命令: ${commandName}`,
|
|
931
|
-
flags: 64, // EPHEMERAL - 只有用户可见
|
|
932
|
-
},
|
|
933
|
-
};
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
private formatInteractionAsMessage(interaction: any): Message<any> {
|
|
937
|
-
const channelType = interaction.guild_id ? "channel" : "private";
|
|
938
|
-
const channelId = interaction.channel_id;
|
|
939
|
-
|
|
940
|
-
// 解析命令参数为内容
|
|
941
|
-
const options = interaction.data.options || [];
|
|
942
|
-
const content = [segment.text(`/${interaction.data.name}`)];
|
|
943
|
-
|
|
944
|
-
for (const option of options) {
|
|
945
|
-
content.push(segment.text(` ${option.name}:${option.value}`));
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
return Message.from(interaction, {
|
|
949
|
-
$id: interaction.id,
|
|
950
|
-
$adapter: "discord-interactions",
|
|
951
|
-
$bot: this.$config.name,
|
|
952
|
-
$sender: {
|
|
953
|
-
id: interaction.user?.id || interaction.member?.user?.id,
|
|
954
|
-
name: interaction.user?.username || interaction.member?.user?.username,
|
|
955
|
-
},
|
|
956
|
-
$channel: {
|
|
957
|
-
id: channelId,
|
|
958
|
-
type: channelType as any,
|
|
959
|
-
},
|
|
960
|
-
$raw: JSON.stringify(interaction),
|
|
961
|
-
$timestamp: Date.now(),
|
|
962
|
-
$content: content,
|
|
963
|
-
$reply: async (content: SendContent): Promise<string> => {
|
|
964
|
-
return this.$sendMessage({
|
|
965
|
-
...this.$formatMessage(interaction),
|
|
966
|
-
content: content,
|
|
967
|
-
});
|
|
968
|
-
},
|
|
969
|
-
});
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
private formatSendContent(content: SendContent): any {
|
|
973
|
-
if (typeof content === "string") {
|
|
974
|
-
return { content };
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
if (Array.isArray(content)) {
|
|
978
|
-
const textParts: string[] = [];
|
|
979
|
-
let embed: any = null;
|
|
980
|
-
|
|
981
|
-
for (const item of content) {
|
|
982
|
-
if (typeof item === "string") {
|
|
983
|
-
textParts.push(item);
|
|
984
|
-
} else {
|
|
985
|
-
const segment = item as MessageSegment;
|
|
986
|
-
switch (segment.type) {
|
|
987
|
-
case "text":
|
|
988
|
-
textParts.push(segment.data.text || segment.data.content || "");
|
|
989
|
-
break;
|
|
990
|
-
case "embed":
|
|
991
|
-
embed = segment.data;
|
|
992
|
-
break;
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
const result: any = {};
|
|
998
|
-
if (textParts.length > 0) {
|
|
999
|
-
result.content = textParts.join("");
|
|
1000
|
-
}
|
|
1001
|
-
if (embed) {
|
|
1002
|
-
result.embeds = [embed];
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
return result;
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
return { content: String(content) };
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
async $connect(): Promise<void> {
|
|
1012
|
-
try {
|
|
1013
|
-
// 注册 Slash Commands
|
|
1014
|
-
if (this.$config.slashCommands) {
|
|
1015
|
-
await this.registerSlashCommands();
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
// 如果启用 Gateway,连接 Discord Gateway
|
|
1019
|
-
if (this.$config.useGateway) {
|
|
1020
|
-
await this.login(this.$config.token);
|
|
1021
|
-
|
|
1022
|
-
// 设置活动状态
|
|
1023
|
-
if (this.$config.defaultActivity) {
|
|
1024
|
-
this.user?.setActivity(this.$config.defaultActivity.name, {
|
|
1025
|
-
type: this.getActivityType(this.$config.defaultActivity.type),
|
|
1026
|
-
url: this.$config.defaultActivity.url,
|
|
1027
|
-
});
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
this.$connected = true;
|
|
1032
|
-
plugin.logger.info(
|
|
1033
|
-
`Discord interactions bot connected: ${this.$config.name}`
|
|
1034
|
-
);
|
|
1035
|
-
plugin.logger.info(
|
|
1036
|
-
`Interactions endpoint: ${this.$config.interactionsPath}`
|
|
1037
|
-
);
|
|
1038
|
-
} catch (error) {
|
|
1039
|
-
plugin.logger.error("Failed to connect Discord interactions bot:", error);
|
|
1040
|
-
throw error;
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
async $disconnect(): Promise<void> {
|
|
1045
|
-
try {
|
|
1046
|
-
if (this.isReady()) {
|
|
1047
|
-
await this.destroy();
|
|
1048
|
-
}
|
|
1049
|
-
this.$connected = false;
|
|
1050
|
-
plugin.logger.info("Discord interactions bot disconnected");
|
|
1051
|
-
} catch (error) {
|
|
1052
|
-
plugin.logger.error(
|
|
1053
|
-
"Error disconnecting Discord interactions bot:",
|
|
1054
|
-
error
|
|
1055
|
-
);
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
// Slash Commands 管理
|
|
1060
|
-
private async registerSlashCommands(): Promise<void> {
|
|
1061
|
-
if (!this.$config.slashCommands) return;
|
|
1062
|
-
|
|
1063
|
-
try {
|
|
1064
|
-
const rest = new REST({ version: "10" }).setToken(this.$config.token);
|
|
1065
|
-
|
|
1066
|
-
if (this.$config.globalCommands) {
|
|
1067
|
-
await rest.put(Routes.applicationCommands(this.$config.applicationId), {
|
|
1068
|
-
body: this.$config.slashCommands,
|
|
1069
|
-
});
|
|
1070
|
-
plugin.logger.info("Successfully registered global slash commands");
|
|
1071
|
-
} else {
|
|
1072
|
-
plugin.logger.info(
|
|
1073
|
-
"Note: Guild commands registration requires connecting to Gateway first"
|
|
1074
|
-
);
|
|
1075
|
-
}
|
|
1076
|
-
} catch (error) {
|
|
1077
|
-
plugin.logger.error("Failed to register slash commands:", error);
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
// 添加 Slash Command 处理器
|
|
1082
|
-
addSlashCommandHandler(
|
|
1083
|
-
commandName: string,
|
|
1084
|
-
handler: (interaction: any) => Promise<void>
|
|
1085
|
-
): void {
|
|
1086
|
-
this.slashCommandHandlers.set(commandName, handler);
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
// 移除 Slash Command 处理器
|
|
1090
|
-
removeSlashCommandHandler(commandName: string): boolean {
|
|
1091
|
-
return this.slashCommandHandlers.delete(commandName);
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
// 工具方法
|
|
1095
|
-
private getActivityType(type: string): any {
|
|
1096
|
-
const activityTypes: any = {
|
|
1097
|
-
PLAYING: 0,
|
|
1098
|
-
STREAMING: 1,
|
|
1099
|
-
LISTENING: 2,
|
|
1100
|
-
WATCHING: 3,
|
|
1101
|
-
COMPETING: 5,
|
|
1102
|
-
};
|
|
1103
|
-
return activityTypes[type] || 0;
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
// 简化实现 - 只支持基本消息格式化和发送
|
|
1107
|
-
$formatMessage(msg: any): Message<any> {
|
|
1108
|
-
return this.formatInteractionAsMessage(msg);
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
async $sendMessage(options: SendOptions): Promise<string> {
|
|
1112
|
-
// 简化实现 - 通过 REST API 发送消息
|
|
1113
|
-
try {
|
|
1114
|
-
const rest = new REST({ version: "10" }).setToken(this.$config.token);
|
|
1115
|
-
const messageContent = this.formatSendContent(options.content);
|
|
1116
|
-
|
|
1117
|
-
await rest.post(Routes.channelMessages(options.id), {
|
|
1118
|
-
body: messageContent,
|
|
1119
|
-
});
|
|
1120
|
-
} catch (error) {
|
|
1121
|
-
plugin.logger.error("Failed to send message:", error);
|
|
1122
|
-
}
|
|
1123
|
-
return "";
|
|
1124
|
-
}
|
|
1125
|
-
async $recallMessage(id: string): Promise<void> {}
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
// 注册 Gateway 模式适配器
|
|
1129
|
-
registerAdapter(
|
|
1130
|
-
new Adapter(
|
|
1131
|
-
"discord",
|
|
1132
|
-
(config: any) => new DiscordBot(config as DiscordBotConfig)
|
|
1133
|
-
)
|
|
1134
|
-
);
|
|
1135
|
-
|
|
1136
|
-
// 注册 Interactions 端点模式适配器(需要 router)
|
|
1137
|
-
useContext("router", (router) => {
|
|
1138
|
-
registerAdapter(
|
|
1139
|
-
new Adapter(
|
|
1140
|
-
"discord-interactions",
|
|
1141
|
-
(config: any) =>
|
|
1142
|
-
new DiscordInteractionsBot(router, config as DiscordInteractionsConfig)
|
|
1143
|
-
)
|
|
1144
|
-
);
|
|
1145
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"outDir": "./lib",
|
|
7
|
-
"rootDir": "./src",
|
|
8
|
-
"strict": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"skipLibCheck": true,
|
|
11
|
-
"forceConsistentCasingInFileNames": true,
|
|
12
|
-
"resolveJsonModule": true,
|
|
13
|
-
"isolatedModules": true,
|
|
14
|
-
"allowSyntheticDefaultImports": true,
|
|
15
|
-
"experimentalDecorators": true,
|
|
16
|
-
"emitDecoratorMetadata": true,
|
|
17
|
-
"declaration": true,
|
|
18
|
-
"declarationMap": true,
|
|
19
|
-
"sourceMap": true,
|
|
20
|
-
"verbatimModuleSyntax": false
|
|
21
|
-
},
|
|
22
|
-
"include": ["src/**/*"],
|
|
23
|
-
"exclude": ["lib", "node_modules"]
|
|
24
|
-
}
|