feat: add Telegram bot frontend with message handling
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,96 @@
|
|||||||
|
import { Bot, Context } from 'grammy';
|
||||||
|
import type { NativeAgent } from '../../backends/index.js';
|
||||||
|
import type { TelegramConfig } from '../../config/index.js';
|
||||||
|
import { isAllowedChat, createMessageHandler, createResetHandler } from './handlers.js';
|
||||||
|
|
||||||
|
export interface TelegramBotConfig {
|
||||||
|
telegram: TelegramConfig;
|
||||||
|
agent: NativeAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTelegramBot(config: TelegramBotConfig): Bot {
|
||||||
|
const bot = new Bot(config.telegram.bot_token);
|
||||||
|
const handleMessage = createMessageHandler(config.agent);
|
||||||
|
const handleReset = createResetHandler(config.agent);
|
||||||
|
const allowedChatIds = config.telegram.allowed_chat_ids;
|
||||||
|
|
||||||
|
// Middleware to check chat ID
|
||||||
|
bot.use(async (ctx, next) => {
|
||||||
|
const chatId = ctx.chat?.id;
|
||||||
|
if (chatId === undefined || !isAllowedChat(chatId, allowedChatIds)) {
|
||||||
|
console.log(`Rejected message from unauthorized chat: ${chatId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Command handlers
|
||||||
|
bot.command('start', async (ctx) => {
|
||||||
|
await ctx.reply('Flynn is ready. Send me a message!');
|
||||||
|
});
|
||||||
|
|
||||||
|
bot.command('reset', async (ctx) => {
|
||||||
|
handleReset();
|
||||||
|
await ctx.reply('Conversation reset.');
|
||||||
|
});
|
||||||
|
|
||||||
|
bot.command('status', async (ctx) => {
|
||||||
|
await ctx.reply('Flynn is running.');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Message handler
|
||||||
|
bot.on('message:text', async (ctx) => {
|
||||||
|
const text = ctx.message.text;
|
||||||
|
|
||||||
|
// Show typing indicator
|
||||||
|
await ctx.replyWithChatAction('typing');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await handleMessage(text);
|
||||||
|
|
||||||
|
// Telegram has a 4096 character limit per message
|
||||||
|
if (response.length <= 4096) {
|
||||||
|
await ctx.reply(response, { parse_mode: 'Markdown' });
|
||||||
|
} else {
|
||||||
|
// Split into chunks
|
||||||
|
const chunks = splitMessage(response, 4096);
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
await ctx.reply(chunk, { parse_mode: 'Markdown' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing message:', error);
|
||||||
|
await ctx.reply('Sorry, an error occurred while processing your message.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return bot;
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitMessage(text: string, maxLength: number): string[] {
|
||||||
|
const chunks: string[] = [];
|
||||||
|
let remaining = text;
|
||||||
|
|
||||||
|
while (remaining.length > 0) {
|
||||||
|
if (remaining.length <= maxLength) {
|
||||||
|
chunks.push(remaining);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to split at a newline
|
||||||
|
let splitIndex = remaining.lastIndexOf('\n', maxLength);
|
||||||
|
if (splitIndex === -1 || splitIndex < maxLength / 2) {
|
||||||
|
// Fall back to splitting at space
|
||||||
|
splitIndex = remaining.lastIndexOf(' ', maxLength);
|
||||||
|
}
|
||||||
|
if (splitIndex === -1 || splitIndex < maxLength / 2) {
|
||||||
|
// Hard split
|
||||||
|
splitIndex = maxLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
chunks.push(remaining.slice(0, splitIndex));
|
||||||
|
remaining = remaining.slice(splitIndex).trimStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { createMessageHandler, isAllowedChat } from './handlers.js';
|
||||||
|
import type { NativeAgent } from '../../backends/native/agent.js';
|
||||||
|
|
||||||
|
describe('isAllowedChat', () => {
|
||||||
|
it('returns true for allowed chat ID', () => {
|
||||||
|
expect(isAllowedChat(123, [123, 456])).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for disallowed chat ID', () => {
|
||||||
|
expect(isAllowedChat(789, [123, 456])).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createMessageHandler', () => {
|
||||||
|
it('processes message and returns response', async () => {
|
||||||
|
const mockAgent: NativeAgent = {
|
||||||
|
process: vi.fn().mockResolvedValue('Agent response'),
|
||||||
|
reset: vi.fn(),
|
||||||
|
getHistory: vi.fn(),
|
||||||
|
} as unknown as NativeAgent;
|
||||||
|
|
||||||
|
const handler = createMessageHandler(mockAgent);
|
||||||
|
const response = await handler('Hello');
|
||||||
|
|
||||||
|
expect(response).toBe('Agent response');
|
||||||
|
expect(mockAgent.process).toHaveBeenCalledWith('Hello');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { NativeAgent } from '../../backends/index.js';
|
||||||
|
|
||||||
|
export function isAllowedChat(chatId: number, allowedIds: number[]): boolean {
|
||||||
|
return allowedIds.includes(chatId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createMessageHandler(agent: NativeAgent): (text: string) => Promise<string> {
|
||||||
|
return async (text: string): Promise<string> => {
|
||||||
|
return agent.process(text);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createResetHandler(agent: NativeAgent): () => void {
|
||||||
|
return (): void => {
|
||||||
|
agent.reset();
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { createTelegramBot, type TelegramBotConfig } from './bot.js';
|
||||||
|
export { isAllowedChat, createMessageHandler, createResetHandler } from './handlers.js';
|
||||||
Reference in New Issue
Block a user