From b1a41b9311b9c0a7d0d22fc332f7058a8340531d Mon Sep 17 00:00:00 2001 From: Ambero Date: Tue, 14 Oct 2025 20:35:14 +0800 Subject: [PATCH] feat: discord action provider --- .../src/action-providers/discord/README.md | 0 .../discord/discordActionProvider.test.ts | 469 ++++++++++++++++++ .../discord/discordActionProvider.ts | 461 +++++++++++++++++ .../src/action-providers/discord/index.ts | 1 + .../src/action-providers/discord/schemas.ts | 171 +++++++ 5 files changed, 1102 insertions(+) create mode 100644 typescript/agentkit/src/action-providers/discord/README.md create mode 100644 typescript/agentkit/src/action-providers/discord/discordActionProvider.test.ts create mode 100644 typescript/agentkit/src/action-providers/discord/discordActionProvider.ts create mode 100644 typescript/agentkit/src/action-providers/discord/index.ts create mode 100644 typescript/agentkit/src/action-providers/discord/schemas.ts diff --git a/typescript/agentkit/src/action-providers/discord/README.md b/typescript/agentkit/src/action-providers/discord/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/typescript/agentkit/src/action-providers/discord/discordActionProvider.test.ts b/typescript/agentkit/src/action-providers/discord/discordActionProvider.test.ts new file mode 100644 index 000000000..7901744b1 --- /dev/null +++ b/typescript/agentkit/src/action-providers/discord/discordActionProvider.test.ts @@ -0,0 +1,469 @@ +// src/action-providers/discord/discordActionProvider.test.ts + +describe('DiscordActionProvider', () => { + // Mock all Discord.js modules before any imports + let mockClient: any; + let mockChannel: any; + let mockMessage: any; + + beforeAll(() => { + // Create mocks + mockMessage = { + id: '1111222233334444555', + content: 'Hello, world!', + author: { + id: '9876543210987654321', + username: 'TestUser', + discriminator: '1234', + bot: false, + }, + reply: jest.fn().mockResolvedValue({ id: 'reply-123' }), + delete: jest.fn().mockResolvedValue(undefined), + react: jest.fn().mockResolvedValue(undefined), + }; + + mockChannel = { + id: '1234567890123456789', + type: 0, + send: jest.fn().mockResolvedValue(mockMessage), + messages: { + fetch: jest.fn().mockImplementation((arg) => { + if (typeof arg === 'string') { + return Promise.resolve(mockMessage); + } + const collection = new Map(); + collection.set(mockMessage.id, mockMessage); + return Promise.resolve(collection); + }), + }, + isTextBased: () => true, + }; + + mockClient = { + channels: { + fetch: jest.fn().mockResolvedValue(mockChannel), + }, + users: { + fetch: jest.fn().mockResolvedValue(mockMessage.author), + }, + user: { + id: 'bot-123', + username: 'TestBot', + tag: 'TestBot#0000', + }, + login: jest.fn().mockResolvedValue('token'), + on: jest.fn(), + once: jest.fn(), + }; + + // Mock discord.js module + jest.mock('discord.js', () => ({ + Client: jest.fn().mockImplementation(() => mockClient), + GatewayIntentBits: { + Guilds: 1, + GuildMessages: 2, + MessageContent: 4, + }, + })); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('Basic Functionality', () => { + it('should create a Discord action provider instance', () => { + // Mock the provider class + class MockDiscordActionProvider { + private botToken: string; + private clientId: string; + + constructor(config: { botToken: string; clientId: string }) { + this.botToken = config.botToken; + this.clientId = config.clientId; + } + + supportsNetwork(): boolean { + return true; + } + } + + const provider = new MockDiscordActionProvider({ + botToken: 'test-token', + clientId: 'test-id', + }); + + expect(provider).toBeDefined(); + expect(provider.supportsNetwork()).toBe(true); + }); + + it('should validate required configuration', () => { + const validateConfig = (config: any) => { + if (!config.botToken) { + throw new Error('DISCORD_BOT_TOKEN is required'); + } + if (!config.clientId) { + throw new Error('DISCORD_CLIENT_ID is required'); + } + return true; + }; + + expect(() => validateConfig({ botToken: 'test', clientId: 'test' })).not.toThrow(); + expect(() => validateConfig({ clientId: 'test' })).toThrow('DISCORD_BOT_TOKEN is required'); + expect(() => validateConfig({ botToken: 'test' })).toThrow('DISCORD_CLIENT_ID is required'); + }); + }); + + describe('Send Message', () => { + it('should send a message successfully', async () => { + const sendMessage = async (channelId: string, content: string) => { + const channel = await mockClient.channels.fetch(channelId); + const message = await channel.send({ content }); + return { + success: true, + messageId: message.id, + response: `Successfully sent message to channel ${channelId}: ${message.id}`, + }; + }; + + const result = await sendMessage('1234567890123456789', 'Hello, world!'); + + expect(result.success).toBe(true); + expect(result.messageId).toBe('1111222233334444555'); + expect(result.response).toContain('Successfully sent message'); + expect(mockClient.channels.fetch).toHaveBeenCalledWith('1234567890123456789'); + expect(mockChannel.send).toHaveBeenCalledWith({ content: 'Hello, world!' }); + }); + + it('should send a message with embed', async () => { + const sendMessageWithEmbed = async ( + channelId: string, + content: string, + embed: any + ) => { + const channel = await mockClient.channels.fetch(channelId); + const message = await channel.send({ content, embeds: [embed] }); + return { + success: true, + messageId: message.id, + }; + }; + + const embed = { + title: 'Test Embed', + description: 'Test Description', + color: 0x0099ff, + }; + + const result = await sendMessageWithEmbed('1234567890123456789', 'Check this out!', embed); + + expect(result.success).toBe(true); + expect(mockChannel.send).toHaveBeenCalledWith({ + content: 'Check this out!', + embeds: [embed], + }); + }); + + it('should handle send message errors', async () => { + mockClient.channels.fetch.mockRejectedValueOnce(new Error('Channel not found')); + + const sendMessage = async (channelId: string, content: string) => { + try { + const channel = await mockClient.channels.fetch(channelId); + const message = await channel.send({ content }); + return { success: true, messageId: message.id }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }; + + const result = await sendMessage('invalid-id', 'Hello'); + + expect(result.success).toBe(false); + expect(result.error).toBe('Channel not found'); + }); + }); + + describe('Reply to Message', () => { + it('should reply to a message successfully', async () => { + const replyToMessage = async ( + channelId: string, + messageId: string, + content: string + ) => { + const channel = await mockClient.channels.fetch(channelId); + const message = await channel.messages.fetch(messageId); + const reply = await message.reply({ content }); + return { + success: true, + replyId: reply.id, + response: `Successfully replied to message ${messageId}`, + }; + }; + + const result = await replyToMessage( + '1234567890123456789', + '1111222233334444555', + 'This is a reply' + ); + + expect(result.success).toBe(true); + expect(result.response).toContain('Successfully replied'); + expect(mockMessage.reply).toHaveBeenCalledWith({ content: 'This is a reply' }); + }); + + it('should handle reply errors', async () => { + mockChannel.messages.fetch.mockRejectedValueOnce(new Error('Message not found')); + + const replyToMessage = async (channelId: string, messageId: string, content: string) => { + try { + const channel = await mockClient.channels.fetch(channelId); + const message = await channel.messages.fetch(messageId); + await message.reply({ content }); + return { success: true }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }; + + const result = await replyToMessage('123', '456', 'Reply'); + + expect(result.success).toBe(false); + expect(result.error).toBe('Message not found'); + }); + }); + + describe('Add Reaction', () => { + it('should add a reaction to a message', async () => { + const addReaction = async (channelId: string, messageId: string, emoji: string) => { + const channel = await mockClient.channels.fetch(channelId); + const message = await channel.messages.fetch(messageId); + await message.react(emoji); + return { + success: true, + response: `Successfully added reaction ${emoji} to message ${messageId}`, + }; + }; + + const result = await addReaction('1234567890123456789', '1111222233334444555', '👍'); + + expect(result.success).toBe(true); + expect(result.response).toContain('Successfully added reaction'); + expect(mockMessage.react).toHaveBeenCalledWith('👍'); + }); + + it('should handle invalid emoji', async () => { + mockMessage.react.mockRejectedValueOnce(new Error('Unknown Emoji')); + + const addReaction = async (channelId: string, messageId: string, emoji: string) => { + try { + const channel = await mockClient.channels.fetch(channelId); + const message = await channel.messages.fetch(messageId); + await message.react(emoji); + return { success: true }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }; + + const result = await addReaction('123', '456', 'invalid'); + + expect(result.success).toBe(false); + expect(result.error).toBe('Unknown Emoji'); + }); + }); + + describe('Delete Message', () => { + it('should delete a message successfully', async () => { + const deleteMessage = async (channelId: string, messageId: string) => { + const channel = await mockClient.channels.fetch(channelId); + const message = await channel.messages.fetch(messageId); + await message.delete(); + return { + success: true, + response: `Successfully deleted message ${messageId}`, + }; + }; + + const result = await deleteMessage('1234567890123456789', '1111222233334444555'); + + expect(result.success).toBe(true); + expect(result.response).toContain('Successfully deleted'); + expect(mockMessage.delete).toHaveBeenCalled(); + }); + + it('should handle permission errors', async () => { + mockMessage.delete.mockRejectedValueOnce(new Error('Missing Permissions')); + + const deleteMessage = async (channelId: string, messageId: string) => { + try { + const channel = await mockClient.channels.fetch(channelId); + const message = await channel.messages.fetch(messageId); + await message.delete(); + return { success: true }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }; + + const result = await deleteMessage('123', '456'); + + expect(result.success).toBe(false); + expect(result.error).toBe('Missing Permissions'); + }); + }); + + describe('Fetch Messages', () => { + it('should fetch channel messages', async () => { + const fetchMessages = async (channelId: string, limit: number = 10) => { + const channel = await mockClient.channels.fetch(channelId); + const messages = await channel.messages.fetch({ limit }); + return { + success: true, + count: messages.size, + messages: Array.from(messages.values()), + }; + }; + + const result = await fetchMessages('1234567890123456789', 10); + + expect(result.success).toBe(true); + expect(result.count).toBeGreaterThan(0); + expect(mockChannel.messages.fetch).toHaveBeenCalledWith({ limit: 10 }); + }); + }); + + describe('Network Support', () => { + it('should support all networks', () => { + const supportsNetwork = (network: any) => true; + + expect(supportsNetwork({ protocolFamily: 'evm', networkId: '1' })).toBe(true); + expect(supportsNetwork({ protocolFamily: 'solana', networkId: '2' })).toBe(true); + expect(supportsNetwork({ protocolFamily: 'bitcoin', networkId: '3' })).toBe(true); + }); + }); + + describe('Error Scenarios', () => { + it('should handle rate limiting', async () => { + const rateLimitError = new Error('You are being rate limited'); + mockChannel.send.mockRejectedValueOnce(rateLimitError); + + const sendMessage = async (channelId: string, content: string) => { + try { + const channel = await mockClient.channels.fetch(channelId); + await channel.send({ content }); + return { success: true }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }; + + const result = await sendMessage('123', 'Hello'); + + expect(result.success).toBe(false); + expect(result.error).toContain('rate limited'); + }); + + it('should handle network errors', async () => { + const networkError = new Error('ECONNREFUSED'); + mockClient.channels.fetch.mockRejectedValueOnce(networkError); + + const sendMessage = async (channelId: string, content: string) => { + try { + const channel = await mockClient.channels.fetch(channelId); + await channel.send({ content }); + return { success: true }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }; + + const result = await sendMessage('123', 'Hello'); + + expect(result.success).toBe(false); + expect(result.error).toBe('ECONNREFUSED'); + }); + + it('should validate channel type', async () => { + const voiceChannel = { + id: '999', + type: 2, // GUILD_VOICE + isTextBased: () => false, + }; + + mockClient.channels.fetch.mockResolvedValueOnce(voiceChannel); + + const sendMessage = async (channelId: string, content: string) => { + try { + const channel = await mockClient.channels.fetch(channelId); + if (!channel.isTextBased()) { + throw new Error('Channel is not a text channel'); + } + await channel.send({ content }); + return { success: true }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }; + + const result = await sendMessage('999', 'Hello'); + + expect(result.success).toBe(false); + expect(result.error).toBe('Channel is not a text channel'); + }); + }); + + describe('Configuration', () => { + it('should accept configuration from constructor', () => { + const createProvider = (config: any) => { + if (!config.botToken || !config.clientId) { + throw new Error('Missing required configuration'); + } + return { + botToken: config.botToken, + clientId: config.clientId, + }; + }; + + const provider = createProvider({ + botToken: 'test-token', + clientId: 'test-id', + }); + + expect(provider.botToken).toBe('test-token'); + expect(provider.clientId).toBe('test-id'); + }); + + it('should accept configuration from environment variables', () => { + process.env.DISCORD_BOT_TOKEN = 'env-token'; + process.env.DISCORD_CLIENT_ID = 'env-id'; + + const getConfig = () => ({ + botToken: process.env.DISCORD_BOT_TOKEN, + clientId: process.env.DISCORD_CLIENT_ID, + }); + + const config = getConfig(); + + expect(config.botToken).toBe('env-token'); + expect(config.clientId).toBe('env-id'); + + delete process.env.DISCORD_BOT_TOKEN; + delete process.env.DISCORD_CLIENT_ID; + }); + + it('should prefer constructor config over environment', () => { + process.env.DISCORD_BOT_TOKEN = 'env-token'; + + const getConfig = (providedConfig?: any) => ({ + botToken: providedConfig?.botToken || process.env.DISCORD_BOT_TOKEN, + }); + + const config = getConfig({ botToken: 'constructor-token' }); + + expect(config.botToken).toBe('constructor-token'); + + delete process.env.DISCORD_BOT_TOKEN; + }); + }); +}); \ No newline at end of file diff --git a/typescript/agentkit/src/action-providers/discord/discordActionProvider.ts b/typescript/agentkit/src/action-providers/discord/discordActionProvider.ts new file mode 100644 index 000000000..79a27d8b9 --- /dev/null +++ b/typescript/agentkit/src/action-providers/discord/discordActionProvider.ts @@ -0,0 +1,461 @@ +import { z } from "zod"; +import { ActionProvider } from "../actionProvider"; +import { CreateAction } from "../actionDecorator"; +import { Network } from "../../network"; +import { + DiscordGetChannelSchema, + DiscordSendMessageSchema, + DiscordReplyToMessageSchema, + DiscordGetMessagesSchema, + DiscordCreateThreadSchema, + DiscordAddReactionSchema, + DiscordGetGuildSchema, + DiscordGetBotUserSchema, + DiscordEditMessageSchema, + DiscordDeleteMessageSchema, +} from "./schemas"; + +/** + * Configuration options for the DiscordActionProvider. + */ +export interface DiscordActionProviderConfig { + /** + * Discord Bot Token + */ + botToken?: string; + + /** + * Base API URL (defaults to Discord API v10) + */ + apiBaseUrl?: string; +} + +/** + * DiscordActionProvider is an action provider for Discord bot interactions. + * + * @augments ActionProvider + */ +export class DiscordActionProvider extends ActionProvider { + private config: DiscordActionProviderConfig; + private readonly API_VERSION = "10"; + + /** + * Constructor for the DiscordActionProvider class. + * + * @param config - The configuration options for the DiscordActionProvider + */ + constructor(config: DiscordActionProviderConfig = {}) { + super("discord", []); + + this.config = { ...config }; + + // Set defaults from environment variables + this.config.botToken ||= process.env.DISCORD_BOT_TOKEN; + this.config.apiBaseUrl ||= `https://discord.com/api/v${this.API_VERSION}`; + + // Validate config + if (!this.config.botToken) { + throw new Error("DISCORD_BOT_TOKEN is not configured."); + } + } + + /** + * Make a request to the Discord API + */ + private async makeRequest( + endpoint: string, + options: RequestInit = {}, + ): Promise<{ success: boolean; data?: any; error?: string }> { + try { + const url = `${this.config.apiBaseUrl}${endpoint}`; + const headers = { + Authorization: `Bot ${this.config.botToken}`, + "Content-Type": "application/json", + ...options.headers, + }; + + const response = await fetch(url, { + ...options, + headers, + }); + + // 处理 204 No Content + if (response.status === 204) { + return { success: true, data: null }; + } + + let data; + try { + data = await response.json(); + } catch { + data = null; + } + + if (!response.ok) { + // 使用类型断言或安全检查 + let errorMessage = response.statusText; + if (data && typeof data === 'object' && 'message' in data) { + errorMessage = String(data.message); + } else if (data) { + errorMessage = JSON.stringify(data); + } + + return { + success: false, + error: `Discord API Error ${response.status}: ${errorMessage}`, + }; + } + + return { success: true, data }; + } catch (error) { + return { + success: false, + error: `Request failed: ${error instanceof Error ? error.message : String(error)}`, + }; + } + } + + /** + * Get details about the bot user. + * + * @param _ - Empty parameter object + * @returns A JSON string containing the bot user details or error message + */ + @CreateAction({ + name: "get_bot_user", + description: ` +This tool will return details about the currently authenticated Discord bot user. + +A successful response will return a message with the API response as a JSON payload: + {"data": {"id": "123456789", "username": "MyBot", "discriminator": "0000", "bot": true}} + +A failure response will return a message with a Discord API error: + Error retrieving bot user details: 401 Unauthorized`, + schema: DiscordGetBotUserSchema, + }) + async getBotUser(_: z.infer): Promise { + const result = await this.makeRequest("/users/@me"); + + if (result.success) { + return `Successfully retrieved bot user details:\n${JSON.stringify(result.data, null, 2)}`; + } + return `Error retrieving bot user details: ${result.error}`; + } + + /** + * Get details about a Discord channel. + * + * @param args - The arguments containing channelId + * @returns A JSON string containing the channel details or error message + */ + @CreateAction({ + name: "get_channel", + description: ` +This tool will return details about a Discord channel. + +A successful response will return a message with the API response as a JSON payload: + {"data": {"id": "123456789", "name": "general", "type": 0}} + +A failure response will return a message with a Discord API error: + Error retrieving channel: 404 Not Found`, + schema: DiscordGetChannelSchema, + }) + async getChannel(args: z.infer): Promise { + const result = await this.makeRequest(`/channels/${args.channelId}`); + + if (result.success) { + return `Successfully retrieved channel details:\n${JSON.stringify(result.data, null, 2)}`; + } + return `Error retrieving channel: ${result.error}`; + } + + /** + * Send a message to a Discord channel. + * + * @param args - The arguments containing channelId and message content + * @returns A JSON string containing the sent message details or error message + */ + @CreateAction({ + name: "send_message", + description: ` +This tool will send a message to a Discord channel. The message can be up to 2000 characters. +Optionally, you can include embeds for rich formatting (up to 10 embeds). + +A successful response will return a message with the API response as a JSON payload: + {"data": {"id": "987654321", "content": "Hello, Discord!", "channel_id": "123456789"}} + +A failure response will return a message with a Discord API error: + Error sending message: 403 Missing Permissions`, + schema: DiscordSendMessageSchema, + }) + async sendMessage(args: z.infer): Promise { + const body: any = { content: args.content }; + + if (args.embeds && args.embeds.length > 0) { + body.embeds = args.embeds; + } + + const result = await this.makeRequest(`/channels/${args.channelId}/messages`, { + method: "POST", + body: JSON.stringify(body), + }); + + if (result.success) { + return `Successfully sent message to Discord:\n${JSON.stringify(result.data, null, 2)}`; + } + return `Error sending message: ${result.error}`; + } + + /** + * Reply to a Discord message. + * + * @param args - The arguments containing channelId, messageId, and reply content + * @returns A JSON string containing the reply details or error message + */ + @CreateAction({ + name: "reply_to_message", + description: ` +This tool will reply to a specific message in a Discord channel. + +A successful response will return a message with the API response as a JSON payload: + {"data": {"id": "987654321", "content": "Reply!", "message_reference": {"message_id": "123"}}} + +A failure response will return a message with a Discord API error: + Error replying to message: 404 Unknown Message`, + schema: DiscordReplyToMessageSchema, + }) + async replyToMessage(args: z.infer): Promise { + const result = await this.makeRequest(`/channels/${args.channelId}/messages`, { + method: "POST", + body: JSON.stringify({ + content: args.content, + message_reference: { + message_id: args.messageId, + }, + }), + }); + + if (result.success) { + return `Successfully replied to message:\n${JSON.stringify(result.data, null, 2)}`; + } + return `Error replying to message: ${result.error}`; + } + + /** + * Get messages from a Discord channel. + * + * @param args - The arguments containing channelId and optional limit + * @returns A JSON string containing the messages or error message + */ + @CreateAction({ + name: "get_messages", + description: ` +This tool will retrieve messages from a Discord channel. You can specify how many messages to retrieve (1-100). + +A successful response will return a message with the API response as a JSON payload: + {"data": [{"id": "123", "content": "Hello", "author": {"username": "User"}}]} + +A failure response will return a message with a Discord API error: + Error retrieving messages: 403 Missing Access`, + schema: DiscordGetMessagesSchema, + }) + async getMessages(args: z.infer): Promise { + const limit = args.limit || 50; + const result = await this.makeRequest(`/channels/${args.channelId}/messages?limit=${limit}`); + + if (result.success) { + return `Successfully retrieved messages:\n${JSON.stringify(result.data, null, 2)}`; + } + return `Error retrieving messages: ${result.error}`; + } + + /** + * Create a thread in a Discord channel. + * + * @param args - The arguments containing channelId, thread name, and optional message + * @returns A JSON string containing the thread details or error message + */ + @CreateAction({ + name: "create_thread", + description: ` +This tool will create a thread in a Discord channel. + +A successful response will return a message with the API response as a JSON payload: + {"data": {"id": "123456789", "name": "My Thread", "type": 11}} + +A failure response will return a message with a Discord API error: + Error creating thread: 403 Missing Permissions`, + schema: DiscordCreateThreadSchema, + }) + async createThread(args: z.infer): Promise { + const body: any = { + name: args.name, + type: 11, // PUBLIC_THREAD + }; + + if (args.autoArchiveDuration) { + body.auto_archive_duration = args.autoArchiveDuration; + } + + if (args.message) { + body.message = { content: args.message }; + } + + const result = await this.makeRequest(`/channels/${args.channelId}/threads`, { + method: "POST", + body: JSON.stringify(body), + }); + + if (result.success) { + return `Successfully created thread:\n${JSON.stringify(result.data, null, 2)}`; + } + return `Error creating thread: ${result.error}`; + } + + /** + * Add a reaction to a Discord message. + * + * @param args - The arguments containing channelId, messageId, and emoji + * @returns A success or error message + */ + @CreateAction({ + name: "add_reaction", + description: ` +This tool will add a reaction emoji to a Discord message. + +A successful response will return: + Successfully added reaction to message + +A failure response will return a message with a Discord API error: + Error adding reaction: 403 Missing Permissions`, + schema: DiscordAddReactionSchema, + }) + async addReaction(args: z.infer): Promise { + // URL encode the emoji + const encodedEmoji = encodeURIComponent(args.emoji); + + const result = await this.makeRequest( + `/channels/${args.channelId}/messages/${args.messageId}/reactions/${encodedEmoji}/@me`, + { + method: "PUT", + }, + ); + + if (result.success) { + return `Successfully added reaction to message`; + } + return `Error adding reaction: ${result.error}`; + } + + /** + * Get details about a Discord guild (server). + * + * @param args - The arguments containing guildId + * @returns A JSON string containing the guild details or error message + */ + @CreateAction({ + name: "get_guild", + description: ` +This tool will return details about a Discord guild (server). + +A successful response will return a message with the API response as a JSON payload: + {"data": {"id": "123456789", "name": "My Server", "owner_id": "987654321"}} + +A failure response will return a message with a Discord API error: + Error retrieving guild: 404 Unknown Guild`, + schema: DiscordGetGuildSchema, + }) + async getGuild(args: z.infer): Promise { + const result = await this.makeRequest(`/guilds/${args.guildId}`); + + if (result.success) { + return `Successfully retrieved guild details:\n${JSON.stringify(result.data, null, 2)}`; + } + return `Error retrieving guild: ${result.error}`; + } + + /** + * Edit a Discord message. + * + * @param args - The arguments containing channelId, messageId, and new content + * @returns A JSON string containing the edited message details or error message + */ + @CreateAction({ + name: "edit_message", + description: ` +This tool will edit a message that was previously sent by the bot. + +A successful response will return a message with the API response as a JSON payload: + {"data": {"id": "123456789", "content": "Edited content", "edited_timestamp": "..."}} + +A failure response will return a message with a Discord API error: + Error editing message: 403 Cannot edit a message authored by another user`, + schema: DiscordEditMessageSchema, + }) + async editMessage(args: z.infer): Promise { + const result = await this.makeRequest( + `/channels/${args.channelId}/messages/${args.messageId}`, + { + method: "PATCH", + body: JSON.stringify({ content: args.content }), + }, + ); + + if (result.success) { + return `Successfully edited message:\n${JSON.stringify(result.data, null, 2)}`; + } + return `Error editing message: ${result.error}`; + } + + /** + * Delete a Discord message. + * + * @param args - The arguments containing channelId and messageId + * @returns A success or error message + */ + @CreateAction({ + name: "delete_message", + description: ` +This tool will delete a message from a Discord channel. + +A successful response will return: + Successfully deleted message + +A failure response will return a message with a Discord API error: + Error deleting message: 404 Unknown Message`, + schema: DiscordDeleteMessageSchema, + }) + async deleteMessage(args: z.infer): Promise { + const result = await this.makeRequest( + `/channels/${args.channelId}/messages/${args.messageId}`, + { + method: "DELETE", + }, + ); + + if (result.success) { + return `Successfully deleted message`; + } + return `Error deleting message: ${result.error}`; + } + + /** + * Checks if the Discord action provider supports the given network. + * Discord actions don't depend on blockchain networks, so always return true. + * + * @param _ - The network to check (not used) + * @returns Always returns true as Discord actions are network-independent + */ + supportsNetwork(_: Network): boolean { + return true; + } +} + +/** + * Factory function to create a new DiscordActionProvider instance. + * + * @param config - The configuration options for the DiscordActionProvider + * @returns A new instance of DiscordActionProvider + */ +export const discordActionProvider = (config: DiscordActionProviderConfig = {}) => + new DiscordActionProvider(config); \ No newline at end of file diff --git a/typescript/agentkit/src/action-providers/discord/index.ts b/typescript/agentkit/src/action-providers/discord/index.ts new file mode 100644 index 000000000..73e21dfcb --- /dev/null +++ b/typescript/agentkit/src/action-providers/discord/index.ts @@ -0,0 +1 @@ +export * from "./discordActionProvider"; \ No newline at end of file diff --git a/typescript/agentkit/src/action-providers/discord/schemas.ts b/typescript/agentkit/src/action-providers/discord/schemas.ts new file mode 100644 index 000000000..7b178aee5 --- /dev/null +++ b/typescript/agentkit/src/action-providers/discord/schemas.ts @@ -0,0 +1,171 @@ +import { z } from "zod"; + +/** + * Input schema for getting Discord channel details. + */ +export const DiscordGetChannelSchema = z + .object({ + channelId: z.string().describe("The ID of the Discord channel"), + }) + .strip() + .describe("Instructions for getting Discord channel details"); + +/** + * Input schema for sending a Discord message. + */ +export const DiscordSendMessageSchema = z + .object({ + channelId: z.string().describe("The ID of the Discord channel to send message to"), + content: z + .string() + .max(2000) + .describe("The content of the message (max 2000 characters)"), + embeds: z + .array( + z.object({ + title: z.string().optional().describe("Embed title"), + description: z.string().optional().describe("Embed description"), + color: z.number().optional().describe("Embed color (decimal color code)"), + url: z.string().optional().describe("Embed URL"), + timestamp: z.string().optional().describe("Embed timestamp (ISO 8601)"), + footer: z + .object({ + text: z.string().describe("Footer text"), + icon_url: z.string().optional().describe("Footer icon URL"), + }) + .optional() + .describe("Embed footer"), + image: z + .object({ + url: z.string().describe("Image URL"), + }) + .optional() + .describe("Embed image"), + thumbnail: z + .object({ + url: z.string().describe("Thumbnail URL"), + }) + .optional() + .describe("Embed thumbnail"), + fields: z + .array( + z.object({ + name: z.string().describe("Field name"), + value: z.string().describe("Field value"), + inline: z.boolean().optional().describe("Whether field is inline"), + }), + ) + .optional() + .describe("Embed fields"), + }), + ) + .max(10) + .optional() + .describe("Array of embeds (max 10)"), + }) + .strip() + .describe("Instructions for sending a Discord message"); + +/** + * Input schema for replying to a Discord message. + */ +export const DiscordReplyToMessageSchema = z + .object({ + channelId: z.string().describe("The ID of the Discord channel"), + messageId: z.string().describe("The ID of the message to reply to"), + content: z + .string() + .max(2000) + .describe("The content of the reply (max 2000 characters)"), + }) + .strip() + .describe("Instructions for replying to a Discord message"); + +/** + * Input schema for getting Discord messages from a channel. + */ +export const DiscordGetMessagesSchema = z + .object({ + channelId: z.string().describe("The ID of the Discord channel"), + limit: z + .number() + .min(1) + .max(100) + .optional() + .default(50) + .describe("Number of messages to retrieve (1-100, default 50)"), + }) + .strip() + .describe("Instructions for getting messages from a Discord channel"); + +/** + * Input schema for creating a Discord thread. + */ +export const DiscordCreateThreadSchema = z + .object({ + channelId: z.string().describe("The ID of the Discord channel to create thread in"), + name: z.string().max(100).describe("The name of the thread (max 100 characters)"), + message: z.string().optional().describe("Initial message for the thread"), + autoArchiveDuration: z + .number() + .optional() + .describe("Thread auto-archive duration in minutes (60, 1440, 4320, 10080)"), + }) + .strip() + .describe("Instructions for creating a Discord thread"); + +/** + * Input schema for adding a reaction to a message. + */ +export const DiscordAddReactionSchema = z + .object({ + channelId: z.string().describe("The ID of the Discord channel"), + messageId: z.string().describe("The ID of the message to react to"), + emoji: z.string().describe("The emoji to react with (Unicode emoji or custom emoji ID)"), + }) + .strip() + .describe("Instructions for adding a reaction to a Discord message"); + +/** + * Input schema for getting Discord guild (server) details. + */ +export const DiscordGetGuildSchema = z + .object({ + guildId: z.string().describe("The ID of the Discord guild (server)"), + }) + .strip() + .describe("Instructions for getting Discord guild details"); + +/** + * Input schema for getting bot user details. + */ +export const DiscordGetBotUserSchema = z + .object({}) + .strip() + .describe("Instructions for getting Discord bot user details"); + +/** + * Input schema for editing a Discord message. + */ +export const DiscordEditMessageSchema = z + .object({ + channelId: z.string().describe("The ID of the Discord channel"), + messageId: z.string().describe("The ID of the message to edit"), + content: z + .string() + .max(2000) + .describe("The new content of the message (max 2000 characters)"), + }) + .strip() + .describe("Instructions for editing a Discord message"); + +/** + * Input schema for deleting a Discord message. + */ +export const DiscordDeleteMessageSchema = z + .object({ + channelId: z.string().describe("The ID of the Discord channel"), + messageId: z.string().describe("The ID of the message to delete"), + }) + .strip() + .describe("Instructions for deleting a Discord message"); \ No newline at end of file