Skip to content

Instantly share code, notes, and snippets.

@ascott
Created March 27, 2026 21:30
Show Gist options
  • Select an option

  • Save ascott/f52366cdfb443fb16371c4ae9229da46 to your computer and use it in GitHub Desktop.

Select an option

Save ascott/f52366cdfb443fb16371c4ae9229da46 to your computer and use it in GitHub Desktop.
feat(discord): add emoji reaction support
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