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