Created
March 27, 2026 21:30
-
-
Save ascott/f52366cdfb443fb16371c4ae9229da46 to your computer and use it in GitHub Desktop.
feat(discord): add emoji reaction support
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| diff --git a/container/agent-runner/src/ipc-mcp-stdio.ts b/container/agent-runner/src/ipc-mcp-stdio.ts | |
| index 5b03478..b2dc7e7 100644 | |
| --- a/container/agent-runner/src/ipc-mcp-stdio.ts | |
| +++ b/container/agent-runner/src/ipc-mcp-stdio.ts | |
| @@ -301,6 +301,29 @@ server.tool( | |
| }, | |
| ); | |
| +server.tool( | |
| + 'add_reaction', | |
| + 'Add an emoji reaction to a message in the current group/channel. Use standard Unicode emoji (e.g., "π", "β€οΈ", "π").', | |
| + { | |
| + message_id: z.string().describe('The Discord message ID to react to'), | |
| + emoji: z.string().describe('Emoji to add (e.g., "π", "β€οΈ", "π")'), | |
| + }, | |
| + async (args) => { | |
| + const data = { | |
| + type: 'add_reaction', | |
| + messageId: args.message_id, | |
| + emoji: args.emoji, | |
| + chatJid, | |
| + groupFolder, | |
| + timestamp: new Date().toISOString(), | |
| + }; | |
| + | |
| + writeIpcFile(TASKS_DIR, data); | |
| + | |
| + return { content: [{ type: 'text' as const, text: `Reaction ${args.emoji} added.` }] }; | |
| + }, | |
| +); | |
| + | |
| server.tool( | |
| 'register_group', | |
| `Register a new chat/group so the agent can respond to messages there. Main group only. | |
| diff --git a/src/channels/discord.ts b/src/channels/discord.ts | |
| index b045350..4c13d5f 100644 | |
| --- a/src/channels/discord.ts | |
| +++ b/src/channels/discord.ts | |
| @@ -40,6 +40,7 @@ export class DiscordChannel implements Channel { | |
| intents: [ | |
| GatewayIntentBits.Guilds, | |
| GatewayIntentBits.GuildMessages, | |
| + GatewayIntentBits.GuildMessageReactions, | |
| GatewayIntentBits.MessageContent, | |
| GatewayIntentBits.DirectMessages, | |
| ], | |
| @@ -239,6 +240,20 @@ export class DiscordChannel implements Channel { | |
| } | |
| } | |
| + async addReaction(jid: string, messageId: string, emoji: string): Promise<void> { | |
| + if (!this.client) return; | |
| + try { | |
| + const channelId = jid.replace(/^dc:/, ''); | |
| + const channel = await this.client.channels.fetch(channelId); | |
| + if (!channel || !('messages' in channel)) return; | |
| + const msg = await (channel as TextChannel).messages.fetch(messageId); | |
| + await msg.react(emoji); | |
| + logger.info({ jid, messageId, emoji }, 'Discord reaction added'); | |
| + } catch (err) { | |
| + logger.error({ jid, messageId, emoji, err }, 'Failed to add Discord reaction'); | |
| + } | |
| + } | |
| + | |
| async setTyping(jid: string, isTyping: boolean): Promise<void> { | |
| if (!this.client || !isTyping) return; | |
| try { | |
| diff --git a/src/index.ts b/src/index.ts | |
| index 0a2480e..7d75122 100644 | |
| --- a/src/index.ts | |
| +++ b/src/index.ts | |
| @@ -684,6 +684,7 @@ async function main(): Promise<void> { | |
| getAvailableGroups, | |
| writeGroupsSnapshot: (gf, im, ag, rj) => | |
| writeGroupsSnapshot(gf, im, ag, rj), | |
| + channels: () => channels, | |
| onTasksChanged: () => { | |
| const tasks = getAllTasks(); | |
| const taskRows = tasks.map((t) => ({ | |
| diff --git a/src/ipc-auth.test.ts b/src/ipc-auth.test.ts | |
| index 0adf899..fb84572 100644 | |
| --- a/src/ipc-auth.test.ts | |
| +++ b/src/ipc-auth.test.ts | |
| @@ -63,6 +63,7 @@ beforeEach(() => { | |
| getAvailableGroups: () => [], | |
| writeGroupsSnapshot: () => {}, | |
| onTasksChanged: () => {}, | |
| + channels: () => [], | |
| }; | |
| }); | |
| diff --git a/src/ipc.ts b/src/ipc.ts | |
| index 043b07a..174a3d8 100644 | |
| --- a/src/ipc.ts | |
| +++ b/src/ipc.ts | |
| @@ -8,7 +8,8 @@ import { AvailableGroup } from './container-runner.js'; | |
| import { createTask, deleteTask, getTaskById, updateTask } from './db.js'; | |
| import { isValidGroupFolder } from './group-folder.js'; | |
| import { logger } from './logger.js'; | |
| -import { RegisteredGroup } from './types.js'; | |
| +import { findChannel } from './router.js'; | |
| +import { Channel, RegisteredGroup } from './types.js'; | |
| export interface IpcDeps { | |
| sendMessage: (jid: string, text: string) => Promise<void>; | |
| @@ -23,6 +24,7 @@ export interface IpcDeps { | |
| registeredJids: Set<string>, | |
| ) => void; | |
| onTasksChanged: () => void; | |
| + channels: () => Channel[]; | |
| } | |
| let ipcWatcherRunning = false; | |
| @@ -173,6 +175,9 @@ export async function processTaskIpc( | |
| trigger?: string; | |
| requiresTrigger?: boolean; | |
| containerConfig?: RegisteredGroup['containerConfig']; | |
| + // For reactions | |
| + messageId?: string; | |
| + emoji?: string; | |
| }, | |
| sourceGroup: string, // Verified identity from IPC directory | |
| isMain: boolean, // Verified from directory path | |
| @@ -458,6 +463,21 @@ export async function processTaskIpc( | |
| } | |
| break; | |
| + case 'add_reaction': | |
| + if (data.messageId && data.emoji && data.chatJid) { | |
| + const reactionChannel = findChannel(deps.channels(), data.chatJid); | |
| + if (reactionChannel?.addReaction) { | |
| + await reactionChannel.addReaction(data.chatJid, data.messageId, data.emoji); | |
| + logger.info( | |
| + { chatJid: data.chatJid, messageId: data.messageId, emoji: data.emoji }, | |
| + 'Reaction added via IPC', | |
| + ); | |
| + } else { | |
| + logger.warn({ chatJid: data.chatJid }, 'Channel not found or does not support reactions'); | |
| + } | |
| + } | |
| + break; | |
| + | |
| default: | |
| logger.warn({ type: data.type }, 'Unknown IPC task type'); | |
| } | |
| diff --git a/src/types.ts b/src/types.ts | |
| index d3bab10..cc03d6c 100644 | |
| --- a/src/types.ts | |
| +++ b/src/types.ts | |
| @@ -92,6 +92,8 @@ export interface Channel { | |
| setTyping?(jid: string, isTyping: boolean): Promise<void>; | |
| // Optional: sync group/chat names from the platform. | |
| syncGroups?(force: boolean): Promise<void>; | |
| + // Optional: emoji reactions on messages. | |
| + addReaction?(jid: string, messageId: string, emoji: string): Promise<void>; | |
| } | |
| // Callback type that channels use to deliver inbound messages |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment