@quadslab.io/discord-mcp 1.0.0
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/LICENSE +21 -0
- package/README.md +474 -0
- package/dist/discord-client.d.ts +57 -0
- package/dist/discord-client.js +188 -0
- package/dist/discord-client.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +135 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-server.d.ts +15 -0
- package/dist/mcp-server.js +65 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/tools/automod.d.ts +6 -0
- package/dist/tools/automod.js +376 -0
- package/dist/tools/automod.js.map +1 -0
- package/dist/tools/channels.d.ts +6 -0
- package/dist/tools/channels.js +840 -0
- package/dist/tools/channels.js.map +1 -0
- package/dist/tools/emojis.d.ts +6 -0
- package/dist/tools/emojis.js +295 -0
- package/dist/tools/emojis.js.map +1 -0
- package/dist/tools/events.d.ts +6 -0
- package/dist/tools/events.js +304 -0
- package/dist/tools/events.js.map +1 -0
- package/dist/tools/forums.d.ts +6 -0
- package/dist/tools/forums.js +276 -0
- package/dist/tools/forums.js.map +1 -0
- package/dist/tools/guild.d.ts +6 -0
- package/dist/tools/guild.js +88 -0
- package/dist/tools/guild.js.map +1 -0
- package/dist/tools/index.d.ts +13 -0
- package/dist/tools/index.js +142 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/members.d.ts +6 -0
- package/dist/tools/members.js +628 -0
- package/dist/tools/members.js.map +1 -0
- package/dist/tools/messages.d.ts +6 -0
- package/dist/tools/messages.js +688 -0
- package/dist/tools/messages.js.map +1 -0
- package/dist/tools/reactions.d.ts +6 -0
- package/dist/tools/reactions.js +137 -0
- package/dist/tools/reactions.js.map +1 -0
- package/dist/tools/roles.d.ts +6 -0
- package/dist/tools/roles.js +469 -0
- package/dist/tools/roles.js.map +1 -0
- package/dist/tools/server.d.ts +6 -0
- package/dist/tools/server.js +607 -0
- package/dist/tools/server.js.map +1 -0
- package/dist/tools/stage.d.ts +6 -0
- package/dist/tools/stage.js +153 -0
- package/dist/tools/stage.js.map +1 -0
- package/dist/tools/threads.d.ts +6 -0
- package/dist/tools/threads.js +331 -0
- package/dist/tools/threads.js.map +1 -0
- package/dist/tools/utils.d.ts +21 -0
- package/dist/tools/utils.js +315 -0
- package/dist/tools/utils.js.map +1 -0
- package/dist/tools/webhooks.d.ts +6 -0
- package/dist/tools/webhooks.js +195 -0
- package/dist/tools/webhooks.js.map +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
import { EmbedBuilder, ChannelType } from 'discord.js';
|
|
2
|
+
import { smartFindTextChannel } from './utils.js';
|
|
3
|
+
import { getGuild } from '../discord-client.js';
|
|
4
|
+
/**
|
|
5
|
+
* Messaging tools - with smart fuzzy matching for channel names
|
|
6
|
+
*/
|
|
7
|
+
export const messageTools = [
|
|
8
|
+
{
|
|
9
|
+
name: 'get_message',
|
|
10
|
+
description: 'Get a specific message by its ID from a channel.',
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
channel: {
|
|
15
|
+
type: 'string',
|
|
16
|
+
description: 'The channel name or ID (fuzzy matched)',
|
|
17
|
+
},
|
|
18
|
+
messageId: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
description: 'The ID of the message to fetch',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
required: ['channel', 'messageId'],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'edit_message',
|
|
28
|
+
description: 'Edit a message that was sent by the bot. Can only edit the bot\'s own messages.',
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: {
|
|
32
|
+
channel: {
|
|
33
|
+
type: 'string',
|
|
34
|
+
description: 'The channel name or ID (fuzzy matched)',
|
|
35
|
+
},
|
|
36
|
+
messageId: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
description: 'The ID of the message to edit',
|
|
39
|
+
},
|
|
40
|
+
content: {
|
|
41
|
+
type: 'string',
|
|
42
|
+
description: 'The new content for the message',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
required: ['channel', 'messageId', 'content'],
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'crosspost_message',
|
|
50
|
+
description: 'Publish (crosspost) a message from an announcement channel to all servers following it.',
|
|
51
|
+
inputSchema: {
|
|
52
|
+
type: 'object',
|
|
53
|
+
properties: {
|
|
54
|
+
channel: {
|
|
55
|
+
type: 'string',
|
|
56
|
+
description: 'The announcement channel name or ID (fuzzy matched)',
|
|
57
|
+
},
|
|
58
|
+
messageId: {
|
|
59
|
+
type: 'string',
|
|
60
|
+
description: 'The ID of the message to publish',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
required: ['channel', 'messageId'],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'get_messages',
|
|
68
|
+
description: 'Get recent messages from a text channel OR voice channel text chat. Supports both regular text channels and voice channel text chats. Channel name is fuzzy-matched.',
|
|
69
|
+
inputSchema: {
|
|
70
|
+
type: 'object',
|
|
71
|
+
properties: {
|
|
72
|
+
channel: {
|
|
73
|
+
type: 'string',
|
|
74
|
+
description: 'The channel name or ID (fuzzy matched - spaces, hyphens, case don\'t matter)',
|
|
75
|
+
},
|
|
76
|
+
limit: {
|
|
77
|
+
type: 'number',
|
|
78
|
+
description: 'Number of messages to retrieve (default: 10, max: 100)',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
required: ['channel'],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'send_message',
|
|
86
|
+
description: 'Send a text message to a text channel OR a voice channel (voice channels have built-in text chat). Supports both regular text channels and voice channel text chats. Channel name is fuzzy-matched. IMPORTANT: Before calling this tool, use list_channels to find the exact channel name.',
|
|
87
|
+
inputSchema: {
|
|
88
|
+
type: 'object',
|
|
89
|
+
properties: {
|
|
90
|
+
channel: {
|
|
91
|
+
type: 'string',
|
|
92
|
+
description: 'The EXACT channel name (get from list_channels first) or channel ID. While fuzzy matching works, using exact names ensures clarity in approval prompts.',
|
|
93
|
+
},
|
|
94
|
+
content: {
|
|
95
|
+
type: 'string',
|
|
96
|
+
description: 'The message content to send',
|
|
97
|
+
},
|
|
98
|
+
replyTo: {
|
|
99
|
+
type: 'string',
|
|
100
|
+
description: 'Message ID to reply to (optional)',
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
required: ['channel', 'content'],
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'send_embed',
|
|
108
|
+
description: 'Send a rich embed message to a channel. IMPORTANT: Before calling this tool, use list_channels to find the exact channel name, then use that exact name so the user sees exactly where the embed will go.',
|
|
109
|
+
inputSchema: {
|
|
110
|
+
type: 'object',
|
|
111
|
+
properties: {
|
|
112
|
+
channel: {
|
|
113
|
+
type: 'string',
|
|
114
|
+
description: 'The EXACT channel name (get from list_channels first) or channel ID',
|
|
115
|
+
},
|
|
116
|
+
title: {
|
|
117
|
+
type: 'string',
|
|
118
|
+
description: 'The title of the embed',
|
|
119
|
+
},
|
|
120
|
+
description: {
|
|
121
|
+
type: 'string',
|
|
122
|
+
description: 'The description/main content of the embed',
|
|
123
|
+
},
|
|
124
|
+
color: {
|
|
125
|
+
type: 'string',
|
|
126
|
+
description: 'The color of the embed sidebar in hex format (e.g., "#FF0000")',
|
|
127
|
+
},
|
|
128
|
+
fields: {
|
|
129
|
+
type: 'array',
|
|
130
|
+
description: 'Array of fields to add to the embed',
|
|
131
|
+
items: {
|
|
132
|
+
type: 'object',
|
|
133
|
+
properties: {
|
|
134
|
+
name: {
|
|
135
|
+
type: 'string',
|
|
136
|
+
description: 'Field title',
|
|
137
|
+
},
|
|
138
|
+
value: {
|
|
139
|
+
type: 'string',
|
|
140
|
+
description: 'Field content',
|
|
141
|
+
},
|
|
142
|
+
inline: {
|
|
143
|
+
type: 'boolean',
|
|
144
|
+
description: 'Whether to display inline with other fields',
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
required: ['name', 'value'],
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
footer: {
|
|
151
|
+
type: 'string',
|
|
152
|
+
description: 'Footer text for the embed',
|
|
153
|
+
},
|
|
154
|
+
thumbnail: {
|
|
155
|
+
type: 'string',
|
|
156
|
+
description: 'URL of thumbnail image (small image on the right)',
|
|
157
|
+
},
|
|
158
|
+
image: {
|
|
159
|
+
type: 'string',
|
|
160
|
+
description: 'URL of main image (large image at bottom)',
|
|
161
|
+
},
|
|
162
|
+
url: {
|
|
163
|
+
type: 'string',
|
|
164
|
+
description: 'URL to link the title to',
|
|
165
|
+
},
|
|
166
|
+
content: {
|
|
167
|
+
type: 'string',
|
|
168
|
+
description: 'Optional text content to send alongside the embed',
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
required: ['channel'],
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: 'bulk_delete_messages',
|
|
176
|
+
description: 'Delete multiple messages from a channel at once (2-100 messages). Only works on messages less than 14 days old.',
|
|
177
|
+
inputSchema: {
|
|
178
|
+
type: 'object',
|
|
179
|
+
properties: {
|
|
180
|
+
channel: {
|
|
181
|
+
type: 'string',
|
|
182
|
+
description: 'The channel name or ID (fuzzy matched)',
|
|
183
|
+
},
|
|
184
|
+
count: {
|
|
185
|
+
type: 'number',
|
|
186
|
+
description: 'Number of messages to delete (2-100)',
|
|
187
|
+
},
|
|
188
|
+
userId: {
|
|
189
|
+
type: 'string',
|
|
190
|
+
description: 'Optional: only delete messages from this user ID',
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
required: ['channel', 'count'],
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: 'pin_message',
|
|
198
|
+
description: 'Pin a message in a channel.',
|
|
199
|
+
inputSchema: {
|
|
200
|
+
type: 'object',
|
|
201
|
+
properties: {
|
|
202
|
+
channel: {
|
|
203
|
+
type: 'string',
|
|
204
|
+
description: 'The channel name or ID (fuzzy matched)',
|
|
205
|
+
},
|
|
206
|
+
messageId: {
|
|
207
|
+
type: 'string',
|
|
208
|
+
description: 'The ID of the message to pin',
|
|
209
|
+
},
|
|
210
|
+
reason: {
|
|
211
|
+
type: 'string',
|
|
212
|
+
description: 'The reason for pinning (shown in audit log)',
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
required: ['channel', 'messageId'],
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: 'unpin_message',
|
|
220
|
+
description: 'Unpin a message in a channel.',
|
|
221
|
+
inputSchema: {
|
|
222
|
+
type: 'object',
|
|
223
|
+
properties: {
|
|
224
|
+
channel: {
|
|
225
|
+
type: 'string',
|
|
226
|
+
description: 'The channel name or ID (fuzzy matched)',
|
|
227
|
+
},
|
|
228
|
+
messageId: {
|
|
229
|
+
type: 'string',
|
|
230
|
+
description: 'The ID of the message to unpin',
|
|
231
|
+
},
|
|
232
|
+
reason: {
|
|
233
|
+
type: 'string',
|
|
234
|
+
description: 'The reason for unpinning (shown in audit log)',
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
required: ['channel', 'messageId'],
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: 'list_pinned_messages',
|
|
242
|
+
description: 'Get all pinned messages in a channel.',
|
|
243
|
+
inputSchema: {
|
|
244
|
+
type: 'object',
|
|
245
|
+
properties: {
|
|
246
|
+
channel: {
|
|
247
|
+
type: 'string',
|
|
248
|
+
description: 'The channel name or ID (fuzzy matched)',
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
required: ['channel'],
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
name: 'add_reaction',
|
|
256
|
+
description: 'Add a reaction (from the bot) to a message.',
|
|
257
|
+
inputSchema: {
|
|
258
|
+
type: 'object',
|
|
259
|
+
properties: {
|
|
260
|
+
channel: {
|
|
261
|
+
type: 'string',
|
|
262
|
+
description: 'The channel name or ID (fuzzy matched)',
|
|
263
|
+
},
|
|
264
|
+
messageId: {
|
|
265
|
+
type: 'string',
|
|
266
|
+
description: 'The ID of the message to react to',
|
|
267
|
+
},
|
|
268
|
+
emoji: {
|
|
269
|
+
type: 'string',
|
|
270
|
+
description: 'The emoji to react with (e.g., "👍" or a custom emoji name)',
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
required: ['channel', 'messageId', 'emoji'],
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
name: 'remove_reaction',
|
|
278
|
+
description: 'Remove the bot\'s reaction from a message.',
|
|
279
|
+
inputSchema: {
|
|
280
|
+
type: 'object',
|
|
281
|
+
properties: {
|
|
282
|
+
channel: {
|
|
283
|
+
type: 'string',
|
|
284
|
+
description: 'The channel name or ID (fuzzy matched)',
|
|
285
|
+
},
|
|
286
|
+
messageId: {
|
|
287
|
+
type: 'string',
|
|
288
|
+
description: 'The ID of the message to remove the reaction from',
|
|
289
|
+
},
|
|
290
|
+
emoji: {
|
|
291
|
+
type: 'string',
|
|
292
|
+
description: 'The emoji to remove (e.g., "👍" or a custom emoji name)',
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
required: ['channel', 'messageId', 'emoji'],
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
name: 'delete_message',
|
|
300
|
+
description: 'Delete a message from a channel by its message ID.',
|
|
301
|
+
inputSchema: {
|
|
302
|
+
type: 'object',
|
|
303
|
+
properties: {
|
|
304
|
+
channel: {
|
|
305
|
+
type: 'string',
|
|
306
|
+
description: 'The channel name or ID (fuzzy matched)',
|
|
307
|
+
},
|
|
308
|
+
messageId: {
|
|
309
|
+
type: 'string',
|
|
310
|
+
description: 'The ID of the message to delete',
|
|
311
|
+
},
|
|
312
|
+
reason: {
|
|
313
|
+
type: 'string',
|
|
314
|
+
description: 'The reason for deleting the message (for audit purposes)',
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
required: ['channel', 'messageId'],
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
];
|
|
321
|
+
export async function executeMessageTool(name, args) {
|
|
322
|
+
switch (name) {
|
|
323
|
+
case 'get_message':
|
|
324
|
+
return await getMessage(args);
|
|
325
|
+
case 'edit_message':
|
|
326
|
+
return await editMessage(args);
|
|
327
|
+
case 'crosspost_message':
|
|
328
|
+
return await crosspostMessage(args);
|
|
329
|
+
case 'get_messages':
|
|
330
|
+
return await getMessages(args);
|
|
331
|
+
case 'send_message':
|
|
332
|
+
return await sendMessage(args);
|
|
333
|
+
case 'send_embed':
|
|
334
|
+
return await sendEmbed(args);
|
|
335
|
+
case 'delete_message':
|
|
336
|
+
return await deleteMessage(args);
|
|
337
|
+
case 'bulk_delete_messages':
|
|
338
|
+
return await bulkDeleteMessages(args);
|
|
339
|
+
case 'pin_message':
|
|
340
|
+
return await pinMessage(args);
|
|
341
|
+
case 'unpin_message':
|
|
342
|
+
return await unpinMessage(args);
|
|
343
|
+
case 'list_pinned_messages':
|
|
344
|
+
return await listPinnedMessages(args);
|
|
345
|
+
case 'add_reaction':
|
|
346
|
+
return await addReaction(args);
|
|
347
|
+
case 'remove_reaction':
|
|
348
|
+
return await removeReaction(args);
|
|
349
|
+
default:
|
|
350
|
+
throw new Error(`Unknown message tool: ${name}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async function getMessages(args) {
|
|
354
|
+
const channelIdentifier = args['channel'];
|
|
355
|
+
const limit = Math.min(args['limit'] || 10, 100);
|
|
356
|
+
const channel = await smartFindTextChannel(channelIdentifier);
|
|
357
|
+
const messages = await channel.messages.fetch({ limit });
|
|
358
|
+
const isVoice = channel.type === ChannelType.GuildVoice || channel.type === ChannelType.GuildStageVoice;
|
|
359
|
+
const messageList = messages
|
|
360
|
+
.sort((a, b) => a.createdTimestamp - b.createdTimestamp)
|
|
361
|
+
.map(msg => ({
|
|
362
|
+
id: msg.id,
|
|
363
|
+
content: msg.content || '(no text content)',
|
|
364
|
+
author: {
|
|
365
|
+
id: msg.author.id,
|
|
366
|
+
username: msg.author.username,
|
|
367
|
+
isBot: msg.author.bot,
|
|
368
|
+
},
|
|
369
|
+
createdAt: msg.createdAt.toISOString(),
|
|
370
|
+
editedAt: msg.editedAt?.toISOString() ?? null,
|
|
371
|
+
hasEmbeds: msg.embeds.length > 0,
|
|
372
|
+
hasAttachments: msg.attachments.size > 0,
|
|
373
|
+
replyTo: msg.reference?.messageId ?? null,
|
|
374
|
+
}));
|
|
375
|
+
return JSON.stringify({
|
|
376
|
+
channel: {
|
|
377
|
+
id: channel.id,
|
|
378
|
+
name: channel.name,
|
|
379
|
+
type: isVoice ? 'voice' : 'text',
|
|
380
|
+
},
|
|
381
|
+
messageCount: messageList.length,
|
|
382
|
+
messages: messageList,
|
|
383
|
+
}, null, 2);
|
|
384
|
+
}
|
|
385
|
+
async function sendMessage(args) {
|
|
386
|
+
const channelIdentifier = args['channel'];
|
|
387
|
+
const content = args['content'];
|
|
388
|
+
const replyTo = args['replyTo'];
|
|
389
|
+
const channel = await smartFindTextChannel(channelIdentifier);
|
|
390
|
+
const messageOptions = { content };
|
|
391
|
+
if (replyTo) {
|
|
392
|
+
messageOptions.reply = { messageReference: replyTo };
|
|
393
|
+
}
|
|
394
|
+
const message = await channel.send(messageOptions);
|
|
395
|
+
const isVoice = channel.type === ChannelType.GuildVoice || channel.type === ChannelType.GuildStageVoice;
|
|
396
|
+
const channelPrefix = isVoice ? '🔊' : '#';
|
|
397
|
+
return JSON.stringify({
|
|
398
|
+
success: true,
|
|
399
|
+
message: `Message sent to ${channelPrefix}${channel.name}`,
|
|
400
|
+
sentMessage: {
|
|
401
|
+
id: message.id,
|
|
402
|
+
content: message.content,
|
|
403
|
+
channel: {
|
|
404
|
+
id: channel.id,
|
|
405
|
+
name: channel.name,
|
|
406
|
+
type: isVoice ? 'voice' : 'text',
|
|
407
|
+
},
|
|
408
|
+
createdAt: message.createdAt.toISOString(),
|
|
409
|
+
},
|
|
410
|
+
}, null, 2);
|
|
411
|
+
}
|
|
412
|
+
async function sendEmbed(args) {
|
|
413
|
+
const channelIdentifier = args['channel'];
|
|
414
|
+
const channel = await smartFindTextChannel(channelIdentifier);
|
|
415
|
+
const embed = new EmbedBuilder();
|
|
416
|
+
if (args['title'])
|
|
417
|
+
embed.setTitle(args['title']);
|
|
418
|
+
if (args['description'])
|
|
419
|
+
embed.setDescription(args['description']);
|
|
420
|
+
if (args['color'])
|
|
421
|
+
embed.setColor(args['color']);
|
|
422
|
+
if (args['url'])
|
|
423
|
+
embed.setURL(args['url']);
|
|
424
|
+
if (args['footer'])
|
|
425
|
+
embed.setFooter({ text: args['footer'] });
|
|
426
|
+
if (args['thumbnail'])
|
|
427
|
+
embed.setThumbnail(args['thumbnail']);
|
|
428
|
+
if (args['image'])
|
|
429
|
+
embed.setImage(args['image']);
|
|
430
|
+
embed.setTimestamp();
|
|
431
|
+
// Add fields
|
|
432
|
+
const fields = args['fields'];
|
|
433
|
+
if (fields && Array.isArray(fields)) {
|
|
434
|
+
for (const field of fields) {
|
|
435
|
+
embed.addFields({
|
|
436
|
+
name: field.name,
|
|
437
|
+
value: field.value,
|
|
438
|
+
inline: field.inline ?? false,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const messageOptions = { embeds: [embed] };
|
|
443
|
+
if (args['content']) {
|
|
444
|
+
messageOptions.content = args['content'];
|
|
445
|
+
}
|
|
446
|
+
const message = await channel.send(messageOptions);
|
|
447
|
+
const isVoice = channel.type === ChannelType.GuildVoice || channel.type === ChannelType.GuildStageVoice;
|
|
448
|
+
const channelPrefix = isVoice ? '🔊' : '#';
|
|
449
|
+
return JSON.stringify({
|
|
450
|
+
success: true,
|
|
451
|
+
message: `Embed sent to ${channelPrefix}${channel.name}`,
|
|
452
|
+
sentMessage: {
|
|
453
|
+
id: message.id,
|
|
454
|
+
channel: {
|
|
455
|
+
id: channel.id,
|
|
456
|
+
name: channel.name,
|
|
457
|
+
type: isVoice ? 'voice' : 'text',
|
|
458
|
+
},
|
|
459
|
+
createdAt: message.createdAt.toISOString(),
|
|
460
|
+
},
|
|
461
|
+
}, null, 2);
|
|
462
|
+
}
|
|
463
|
+
async function deleteMessage(args) {
|
|
464
|
+
const channelIdentifier = args['channel'];
|
|
465
|
+
const messageId = args['messageId'];
|
|
466
|
+
const channel = await smartFindTextChannel(channelIdentifier);
|
|
467
|
+
const isVoice = channel.type === ChannelType.GuildVoice || channel.type === ChannelType.GuildStageVoice;
|
|
468
|
+
const channelPrefix = isVoice ? '🔊' : '#';
|
|
469
|
+
try {
|
|
470
|
+
const message = await channel.messages.fetch(messageId);
|
|
471
|
+
await message.delete();
|
|
472
|
+
return JSON.stringify({
|
|
473
|
+
success: true,
|
|
474
|
+
message: `Message ${messageId} deleted from ${channelPrefix}${channel.name}`,
|
|
475
|
+
}, null, 2);
|
|
476
|
+
}
|
|
477
|
+
catch (error) {
|
|
478
|
+
if (error.code === 10008) {
|
|
479
|
+
throw new Error(`Message ${messageId} not found in ${channelPrefix}${channel.name}`);
|
|
480
|
+
}
|
|
481
|
+
throw error;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
async function bulkDeleteMessages(args) {
|
|
485
|
+
const channelIdentifier = args['channel'];
|
|
486
|
+
const count = Math.min(Math.max(args['count'] || 2, 2), 100);
|
|
487
|
+
const userId = args['userId'];
|
|
488
|
+
const channel = await smartFindTextChannel(channelIdentifier);
|
|
489
|
+
if (userId) {
|
|
490
|
+
// Fetch messages then filter by user
|
|
491
|
+
const messages = await channel.messages.fetch({ limit: 100 });
|
|
492
|
+
const userMessages = messages.filter(m => m.author.id === userId);
|
|
493
|
+
const toDelete = [...userMessages.values()].slice(0, count);
|
|
494
|
+
if (toDelete.length === 0) {
|
|
495
|
+
throw new Error(`No messages found from user ${userId} in #${channel.name}`);
|
|
496
|
+
}
|
|
497
|
+
const deleted = await channel.bulkDelete(toDelete, true);
|
|
498
|
+
return JSON.stringify({
|
|
499
|
+
success: true,
|
|
500
|
+
message: `Deleted ${deleted.size} messages from user ${userId} in #${channel.name}`,
|
|
501
|
+
deletedCount: deleted.size,
|
|
502
|
+
}, null, 2);
|
|
503
|
+
}
|
|
504
|
+
const deleted = await channel.bulkDelete(count, true);
|
|
505
|
+
return JSON.stringify({
|
|
506
|
+
success: true,
|
|
507
|
+
message: `Deleted ${deleted.size} messages from #${channel.name}`,
|
|
508
|
+
deletedCount: deleted.size,
|
|
509
|
+
}, null, 2);
|
|
510
|
+
}
|
|
511
|
+
async function pinMessage(args) {
|
|
512
|
+
const channelIdentifier = args['channel'];
|
|
513
|
+
const messageId = args['messageId'];
|
|
514
|
+
const reason = args['reason'];
|
|
515
|
+
const channel = await smartFindTextChannel(channelIdentifier);
|
|
516
|
+
const message = await channel.messages.fetch(messageId);
|
|
517
|
+
await message.pin(reason);
|
|
518
|
+
return JSON.stringify({
|
|
519
|
+
success: true,
|
|
520
|
+
message: `Message ${messageId} pinned in #${channel.name}`,
|
|
521
|
+
}, null, 2);
|
|
522
|
+
}
|
|
523
|
+
async function unpinMessage(args) {
|
|
524
|
+
const channelIdentifier = args['channel'];
|
|
525
|
+
const messageId = args['messageId'];
|
|
526
|
+
const reason = args['reason'];
|
|
527
|
+
const channel = await smartFindTextChannel(channelIdentifier);
|
|
528
|
+
const message = await channel.messages.fetch(messageId);
|
|
529
|
+
await message.unpin(reason);
|
|
530
|
+
return JSON.stringify({
|
|
531
|
+
success: true,
|
|
532
|
+
message: `Message ${messageId} unpinned in #${channel.name}`,
|
|
533
|
+
}, null, 2);
|
|
534
|
+
}
|
|
535
|
+
async function listPinnedMessages(args) {
|
|
536
|
+
const channelIdentifier = args['channel'];
|
|
537
|
+
const channel = await smartFindTextChannel(channelIdentifier);
|
|
538
|
+
const pinned = await channel.messages.fetchPinned();
|
|
539
|
+
const pinnedList = pinned
|
|
540
|
+
.sort((a, b) => a.createdTimestamp - b.createdTimestamp)
|
|
541
|
+
.map(msg => ({
|
|
542
|
+
id: msg.id,
|
|
543
|
+
content: msg.content || '(no text content)',
|
|
544
|
+
author: {
|
|
545
|
+
id: msg.author.id,
|
|
546
|
+
username: msg.author.username,
|
|
547
|
+
isBot: msg.author.bot,
|
|
548
|
+
},
|
|
549
|
+
createdAt: msg.createdAt.toISOString(),
|
|
550
|
+
hasEmbeds: msg.embeds.length > 0,
|
|
551
|
+
hasAttachments: msg.attachments.size > 0,
|
|
552
|
+
}));
|
|
553
|
+
return JSON.stringify({
|
|
554
|
+
channel: { id: channel.id, name: channel.name },
|
|
555
|
+
pinnedCount: pinnedList.length,
|
|
556
|
+
messages: [...pinnedList],
|
|
557
|
+
}, null, 2);
|
|
558
|
+
}
|
|
559
|
+
async function addReaction(args) {
|
|
560
|
+
const channelIdentifier = args['channel'];
|
|
561
|
+
const messageId = args['messageId'];
|
|
562
|
+
const emoji = args['emoji'];
|
|
563
|
+
const channel = await smartFindTextChannel(channelIdentifier);
|
|
564
|
+
const message = await channel.messages.fetch(messageId);
|
|
565
|
+
// Try direct emoji first, then search guild emojis by name
|
|
566
|
+
try {
|
|
567
|
+
await message.react(emoji);
|
|
568
|
+
}
|
|
569
|
+
catch {
|
|
570
|
+
const guild = await getGuild();
|
|
571
|
+
const guildEmoji = guild.emojis.cache.find(e => e.name?.toLowerCase() === emoji.toLowerCase());
|
|
572
|
+
if (guildEmoji) {
|
|
573
|
+
await message.react(guildEmoji);
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
throw new Error(`Could not find emoji "${emoji}". Use a unicode emoji or the exact name of a custom server emoji.`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return JSON.stringify({
|
|
580
|
+
success: true,
|
|
581
|
+
message: `Reacted with ${emoji} on message ${messageId} in #${channel.name}`,
|
|
582
|
+
}, null, 2);
|
|
583
|
+
}
|
|
584
|
+
async function removeReaction(args) {
|
|
585
|
+
const channelIdentifier = args['channel'];
|
|
586
|
+
const messageId = args['messageId'];
|
|
587
|
+
const emoji = args['emoji'];
|
|
588
|
+
const channel = await smartFindTextChannel(channelIdentifier);
|
|
589
|
+
const message = await channel.messages.fetch(messageId);
|
|
590
|
+
// Find the bot's reaction and remove it
|
|
591
|
+
const reaction = message.reactions.cache.find(r => {
|
|
592
|
+
const name = r.emoji.name?.toLowerCase() ?? '';
|
|
593
|
+
return name === emoji.toLowerCase() || r.emoji.toString() === emoji;
|
|
594
|
+
});
|
|
595
|
+
if (!reaction) {
|
|
596
|
+
throw new Error(`No reaction "${emoji}" found on message ${messageId}`);
|
|
597
|
+
}
|
|
598
|
+
await reaction.users.remove(message.client.user.id);
|
|
599
|
+
return JSON.stringify({
|
|
600
|
+
success: true,
|
|
601
|
+
message: `Removed ${emoji} reaction from message ${messageId} in #${channel.name}`,
|
|
602
|
+
}, null, 2);
|
|
603
|
+
}
|
|
604
|
+
async function getMessage(args) {
|
|
605
|
+
const channelIdentifier = args['channel'];
|
|
606
|
+
const messageId = args['messageId'];
|
|
607
|
+
const channel = await smartFindTextChannel(channelIdentifier);
|
|
608
|
+
const msg = await channel.messages.fetch(messageId);
|
|
609
|
+
const isVoice = channel.type === ChannelType.GuildVoice || channel.type === ChannelType.GuildStageVoice;
|
|
610
|
+
return JSON.stringify({
|
|
611
|
+
channel: {
|
|
612
|
+
id: channel.id,
|
|
613
|
+
name: channel.name,
|
|
614
|
+
type: isVoice ? 'voice' : 'text',
|
|
615
|
+
},
|
|
616
|
+
message: {
|
|
617
|
+
id: msg.id,
|
|
618
|
+
content: msg.content || '(no text content)',
|
|
619
|
+
author: {
|
|
620
|
+
id: msg.author.id,
|
|
621
|
+
username: msg.author.username,
|
|
622
|
+
isBot: msg.author.bot,
|
|
623
|
+
},
|
|
624
|
+
createdAt: msg.createdAt.toISOString(),
|
|
625
|
+
editedAt: msg.editedAt?.toISOString() ?? null,
|
|
626
|
+
hasEmbeds: msg.embeds.length > 0,
|
|
627
|
+
embeds: msg.embeds.map(e => ({
|
|
628
|
+
title: e.title,
|
|
629
|
+
description: e.description,
|
|
630
|
+
url: e.url,
|
|
631
|
+
color: e.hexColor,
|
|
632
|
+
fields: e.fields.map(f => ({ name: f.name, value: f.value, inline: f.inline })),
|
|
633
|
+
footer: e.footer?.text ?? null,
|
|
634
|
+
image: e.image?.url ?? null,
|
|
635
|
+
thumbnail: e.thumbnail?.url ?? null,
|
|
636
|
+
})),
|
|
637
|
+
hasAttachments: msg.attachments.size > 0,
|
|
638
|
+
attachments: msg.attachments.map(a => ({
|
|
639
|
+
id: a.id,
|
|
640
|
+
name: a.name,
|
|
641
|
+
url: a.url,
|
|
642
|
+
size: a.size,
|
|
643
|
+
contentType: a.contentType,
|
|
644
|
+
})),
|
|
645
|
+
replyTo: msg.reference?.messageId ?? null,
|
|
646
|
+
pinned: msg.pinned,
|
|
647
|
+
reactions: msg.reactions.cache.map(r => ({
|
|
648
|
+
emoji: r.emoji.toString(),
|
|
649
|
+
count: r.count,
|
|
650
|
+
})),
|
|
651
|
+
},
|
|
652
|
+
}, null, 2);
|
|
653
|
+
}
|
|
654
|
+
async function editMessage(args) {
|
|
655
|
+
const channelIdentifier = args['channel'];
|
|
656
|
+
const messageId = args['messageId'];
|
|
657
|
+
const content = args['content'];
|
|
658
|
+
const channel = await smartFindTextChannel(channelIdentifier);
|
|
659
|
+
const msg = await channel.messages.fetch(messageId);
|
|
660
|
+
if (msg.author.id !== msg.client.user.id) {
|
|
661
|
+
throw new Error(`Cannot edit message ${messageId} — it was not sent by the bot`);
|
|
662
|
+
}
|
|
663
|
+
const edited = await msg.edit(content);
|
|
664
|
+
return JSON.stringify({
|
|
665
|
+
success: true,
|
|
666
|
+
message: `Message ${messageId} edited in #${channel.name}`,
|
|
667
|
+
editedMessage: {
|
|
668
|
+
id: edited.id,
|
|
669
|
+
content: edited.content,
|
|
670
|
+
editedAt: edited.editedAt?.toISOString() ?? null,
|
|
671
|
+
},
|
|
672
|
+
}, null, 2);
|
|
673
|
+
}
|
|
674
|
+
async function crosspostMessage(args) {
|
|
675
|
+
const channelIdentifier = args['channel'];
|
|
676
|
+
const messageId = args['messageId'];
|
|
677
|
+
const channel = await smartFindTextChannel(channelIdentifier);
|
|
678
|
+
if (channel.type !== ChannelType.GuildAnnouncement) {
|
|
679
|
+
throw new Error(`#${channel.name} is not an announcement channel — crossposting only works in announcement channels`);
|
|
680
|
+
}
|
|
681
|
+
const msg = await channel.messages.fetch(messageId);
|
|
682
|
+
await msg.crosspost();
|
|
683
|
+
return JSON.stringify({
|
|
684
|
+
success: true,
|
|
685
|
+
message: `Message ${messageId} published from #${channel.name} to all followers`,
|
|
686
|
+
}, null, 2);
|
|
687
|
+
}
|
|
688
|
+
//# sourceMappingURL=messages.js.map
|