395 lines
12 KiB
TypeScript
395 lines
12 KiB
TypeScript
import { z } from "zod";
|
|
import { ChannelType } from "discord.js";
|
|
import { ToolContext, ToolResponse } from "./types.js";
|
|
import {
|
|
CreateTextChannelSchema,
|
|
DeleteChannelSchema,
|
|
ReadMessagesSchema,
|
|
GetServerInfoSchema,
|
|
CreateCategorySchema,
|
|
EditCategorySchema,
|
|
DeleteCategorySchema,
|
|
ListServersSchema
|
|
} from "../schemas.js";
|
|
import { handleDiscordError } from "../errorHandler.js";
|
|
|
|
// Category creation handler
|
|
export async function createCategoryHandler(
|
|
args: unknown,
|
|
context: ToolContext
|
|
): Promise<ToolResponse> {
|
|
const { guildId, name, position, reason } = CreateCategorySchema.parse(args);
|
|
try {
|
|
if (!context.client.isReady()) {
|
|
return {
|
|
content: [{ type: "text", text: "Discord client not logged in." }],
|
|
isError: true
|
|
};
|
|
}
|
|
const guild = await context.client.guilds.fetch(guildId);
|
|
if (!guild) {
|
|
return {
|
|
content: [{ type: "text", text: `Cannot find guild with ID: ${guildId}` }],
|
|
isError: true
|
|
};
|
|
}
|
|
const options: any = { name, type: ChannelType.GuildCategory };
|
|
if (typeof position === "number") options.position = position;
|
|
if (reason) options.reason = reason;
|
|
const category = await guild.channels.create(options);
|
|
return {
|
|
content: [{ type: "text", text: `Successfully created category "${name}" with ID: ${category.id}` }]
|
|
};
|
|
} catch (error) {
|
|
return handleDiscordError(error);
|
|
}
|
|
}
|
|
|
|
// Category edit handler
|
|
export async function editCategoryHandler(
|
|
args: unknown,
|
|
context: ToolContext
|
|
): Promise<ToolResponse> {
|
|
const { categoryId, name, position, reason } = EditCategorySchema.parse(args);
|
|
try {
|
|
if (!context.client.isReady()) {
|
|
return {
|
|
content: [{ type: "text", text: "Discord client not logged in." }],
|
|
isError: true
|
|
};
|
|
}
|
|
const category = await context.client.channels.fetch(categoryId);
|
|
if (!category || category.type !== ChannelType.GuildCategory) {
|
|
return {
|
|
content: [{ type: "text", text: `Cannot find category with ID: ${categoryId}` }],
|
|
isError: true
|
|
};
|
|
}
|
|
const update: any = {};
|
|
if (name) update.name = name;
|
|
if (typeof position === "number") update.position = position;
|
|
if (reason) update.reason = reason;
|
|
await category.edit(update);
|
|
return {
|
|
content: [{ type: "text", text: `Successfully edited category with ID: ${categoryId}` }]
|
|
};
|
|
} catch (error) {
|
|
return handleDiscordError(error);
|
|
}
|
|
}
|
|
|
|
// Category deletion handler
|
|
export async function deleteCategoryHandler(
|
|
args: unknown,
|
|
context: ToolContext
|
|
): Promise<ToolResponse> {
|
|
const { categoryId, reason } = DeleteCategorySchema.parse(args);
|
|
try {
|
|
if (!context.client.isReady()) {
|
|
return {
|
|
content: [{ type: "text", text: "Discord client not logged in." }],
|
|
isError: true
|
|
};
|
|
}
|
|
const category = await context.client.channels.fetch(categoryId);
|
|
if (!category || category.type !== ChannelType.GuildCategory) {
|
|
return {
|
|
content: [{ type: "text", text: `Cannot find category with ID: ${categoryId}` }],
|
|
isError: true
|
|
};
|
|
}
|
|
await category.delete(reason || "Category deleted via API");
|
|
return {
|
|
content: [{ type: "text", text: `Successfully deleted category with ID: ${categoryId}` }]
|
|
};
|
|
} catch (error) {
|
|
return handleDiscordError(error);
|
|
}
|
|
}
|
|
|
|
// Text channel creation handler
|
|
export async function createTextChannelHandler(
|
|
args: unknown,
|
|
context: ToolContext
|
|
): Promise<ToolResponse> {
|
|
const { guildId, channelName, topic, reason } = CreateTextChannelSchema.parse(args);
|
|
try {
|
|
if (!context.client.isReady()) {
|
|
return {
|
|
content: [{ type: "text", text: "Discord client not logged in." }],
|
|
isError: true
|
|
};
|
|
}
|
|
|
|
const guild = await context.client.guilds.fetch(guildId);
|
|
if (!guild) {
|
|
return {
|
|
content: [{ type: "text", text: `Cannot find guild with ID: ${guildId}` }],
|
|
isError: true
|
|
};
|
|
}
|
|
|
|
// Create the text channel
|
|
const channelOptions: any = {
|
|
name: channelName,
|
|
type: ChannelType.GuildText
|
|
};
|
|
if (topic) channelOptions.topic = topic;
|
|
if (reason) channelOptions.reason = reason;
|
|
const channel = await guild.channels.create(channelOptions);
|
|
|
|
return {
|
|
content: [{
|
|
type: "text",
|
|
text: `Successfully created text channel "${channelName}" with ID: ${channel.id}`
|
|
}]
|
|
};
|
|
} catch (error) {
|
|
return handleDiscordError(error);
|
|
}
|
|
}
|
|
|
|
// Channel deletion handler
|
|
export async function deleteChannelHandler(
|
|
args: unknown,
|
|
context: ToolContext
|
|
): Promise<ToolResponse> {
|
|
const { channelId, reason } = DeleteChannelSchema.parse(args);
|
|
try {
|
|
if (!context.client.isReady()) {
|
|
return {
|
|
content: [{ type: "text", text: "Discord client not logged in." }],
|
|
isError: true
|
|
};
|
|
}
|
|
|
|
const channel = await context.client.channels.fetch(channelId);
|
|
if (!channel) {
|
|
return {
|
|
content: [{ type: "text", text: `Cannot find channel with ID: ${channelId}` }],
|
|
isError: true
|
|
};
|
|
}
|
|
|
|
// Check if channel can be deleted (has delete method)
|
|
if (!('delete' in channel)) {
|
|
return {
|
|
content: [{ type: "text", text: `This channel type does not support deletion or the bot lacks permissions` }],
|
|
isError: true
|
|
};
|
|
}
|
|
|
|
// Delete the channel
|
|
await channel.delete(reason || "Channel deleted via API");
|
|
|
|
return {
|
|
content: [{
|
|
type: "text",
|
|
text: `Successfully deleted channel with ID: ${channelId}`
|
|
}]
|
|
};
|
|
} catch (error) {
|
|
return handleDiscordError(error);
|
|
}
|
|
}
|
|
|
|
// Message reading handler
|
|
export async function readMessagesHandler(
|
|
args: unknown,
|
|
context: ToolContext
|
|
): Promise<ToolResponse> {
|
|
const { channelId, limit } = ReadMessagesSchema.parse(args);
|
|
try {
|
|
if (!context.client.isReady()) {
|
|
return {
|
|
content: [{ type: "text", text: "Discord client not logged in." }],
|
|
isError: true
|
|
};
|
|
}
|
|
|
|
const channel = await context.client.channels.fetch(channelId);
|
|
if (!channel) {
|
|
return {
|
|
content: [{ type: "text", text: `Cannot find channel with ID: ${channelId}` }],
|
|
isError: true
|
|
};
|
|
}
|
|
|
|
// Check if channel has messages (text channel, thread, etc.)
|
|
if (!channel.isTextBased() || !('messages' in channel)) {
|
|
return {
|
|
content: [{ type: "text", text: `Channel type does not support reading messages` }],
|
|
isError: true
|
|
};
|
|
}
|
|
|
|
// Fetch messages
|
|
const messages = await channel.messages.fetch({ limit });
|
|
|
|
if (messages.size === 0) {
|
|
return {
|
|
content: [{ type: "text", text: `No messages found in channel` }]
|
|
};
|
|
}
|
|
|
|
// Format messages
|
|
const formattedMessages = messages.map(msg => ({
|
|
id: msg.id,
|
|
content: msg.content,
|
|
author: {
|
|
id: msg.author.id,
|
|
username: msg.author.username,
|
|
bot: msg.author.bot
|
|
},
|
|
timestamp: msg.createdAt,
|
|
attachments: msg.attachments.size,
|
|
embeds: msg.embeds.length,
|
|
replyTo: msg.reference ? msg.reference.messageId : null
|
|
})).sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
|
|
return {
|
|
content: [{
|
|
type: "text",
|
|
text: JSON.stringify({
|
|
channelId,
|
|
messageCount: formattedMessages.length,
|
|
messages: formattedMessages
|
|
}, null, 2)
|
|
}]
|
|
};
|
|
} catch (error) {
|
|
return handleDiscordError(error);
|
|
}
|
|
}
|
|
|
|
// Server information handler
|
|
export async function getServerInfoHandler(
|
|
args: unknown,
|
|
context: ToolContext
|
|
): Promise<ToolResponse> {
|
|
const { guildId } = GetServerInfoSchema.parse(args);
|
|
try {
|
|
if (!context.client.isReady()) {
|
|
return {
|
|
content: [{ type: "text", text: "Discord client not logged in." }],
|
|
isError: true
|
|
};
|
|
}
|
|
|
|
const guild = await context.client.guilds.fetch(guildId);
|
|
if (!guild) {
|
|
return {
|
|
content: [{ type: "text", text: `Cannot find guild with ID: ${guildId}` }],
|
|
isError: true
|
|
};
|
|
}
|
|
|
|
// Fetch additional server data
|
|
await guild.fetch();
|
|
|
|
// Fetch channel information
|
|
const channels = await guild.channels.fetch();
|
|
|
|
// Categorize channels by type
|
|
const channelsByType = {
|
|
text: channels.filter(c => c?.type === ChannelType.GuildText).size,
|
|
voice: channels.filter(c => c?.type === ChannelType.GuildVoice).size,
|
|
category: channels.filter(c => c?.type === ChannelType.GuildCategory).size,
|
|
forum: channels.filter(c => c?.type === ChannelType.GuildForum).size,
|
|
announcement: channels.filter(c => c?.type === ChannelType.GuildAnnouncement).size,
|
|
stage: channels.filter(c => c?.type === ChannelType.GuildStageVoice).size,
|
|
total: channels.size
|
|
};
|
|
|
|
// Get detailed information for all channels
|
|
const channelDetails = channels.map(channel => {
|
|
if (!channel) return null;
|
|
|
|
return {
|
|
id: channel.id,
|
|
name: channel.name,
|
|
type: ChannelType[channel.type] || channel.type,
|
|
categoryId: channel.parentId,
|
|
position: channel.position,
|
|
// Only add topic for text channels
|
|
topic: 'topic' in channel ? channel.topic : null,
|
|
};
|
|
}).filter(c => c !== null); // Filter out null values
|
|
|
|
// Group channels by type
|
|
const groupedChannels = {
|
|
text: channelDetails.filter(c => c.type === ChannelType[ChannelType.GuildText] || c.type === ChannelType.GuildText),
|
|
voice: channelDetails.filter(c => c.type === ChannelType[ChannelType.GuildVoice] || c.type === ChannelType.GuildVoice),
|
|
category: channelDetails.filter(c => c.type === ChannelType[ChannelType.GuildCategory] || c.type === ChannelType.GuildCategory),
|
|
forum: channelDetails.filter(c => c.type === ChannelType[ChannelType.GuildForum] || c.type === ChannelType.GuildForum),
|
|
announcement: channelDetails.filter(c => c.type === ChannelType[ChannelType.GuildAnnouncement] || c.type === ChannelType.GuildAnnouncement),
|
|
stage: channelDetails.filter(c => c.type === ChannelType[ChannelType.GuildStageVoice] || c.type === ChannelType.GuildStageVoice),
|
|
all: channelDetails
|
|
};
|
|
|
|
// Get member count
|
|
const approximateMemberCount = guild.approximateMemberCount || "unknown";
|
|
|
|
// Format guild information
|
|
const guildInfo = {
|
|
id: guild.id,
|
|
name: guild.name,
|
|
description: guild.description,
|
|
icon: guild.iconURL(),
|
|
owner: guild.ownerId,
|
|
createdAt: guild.createdAt,
|
|
memberCount: approximateMemberCount,
|
|
channels: {
|
|
count: channelsByType,
|
|
details: groupedChannels
|
|
},
|
|
features: guild.features,
|
|
premium: {
|
|
tier: guild.premiumTier,
|
|
subscriptions: guild.premiumSubscriptionCount
|
|
}
|
|
};
|
|
|
|
return {
|
|
content: [{ type: "text", text: JSON.stringify(guildInfo, null, 2) }]
|
|
};
|
|
} catch (error) {
|
|
return handleDiscordError(error);
|
|
}
|
|
}
|
|
|
|
// List servers handler
|
|
export async function listServersHandler(
|
|
args: unknown,
|
|
context: ToolContext
|
|
): Promise<ToolResponse> {
|
|
ListServersSchema.parse(args);
|
|
try {
|
|
if (!context.client.isReady()) {
|
|
return {
|
|
content: [{ type: "text", text: "Discord client not logged in." }],
|
|
isError: true
|
|
};
|
|
}
|
|
|
|
const guilds = await context.client.guilds.fetch();
|
|
|
|
if (guilds.size === 0) {
|
|
return {
|
|
content: [{ type: "text", text: "No servers found. The bot is not a member of any servers." }]
|
|
};
|
|
}
|
|
|
|
const guildsInfo = guilds.map(guild => ({
|
|
id: guild.id,
|
|
name: guild.name,
|
|
icon: guild.iconURL()
|
|
}));
|
|
|
|
return {
|
|
content: [{ type: "text", text: JSON.stringify(guildsInfo, null, 2) }]
|
|
};
|
|
} catch (error) {
|
|
return handleDiscordError(error);
|
|
}
|
|
}
|