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