UNPKG

weavebot-telegram

Version:

Generic Telegram bot framework with command registration

563 lines (527 loc) 18.3 kB
// src/bot.ts import { Telegraf } from "telegraf"; import ContentProcessor, { defaultLogger } from "weavebot-core"; var TelegramBot = class { bot; processor; config; constructor(config) { this.config = config; this.bot = new Telegraf(config.botToken); this.processor = config.processor || new ContentProcessor(); this.setupCommands(); this.setupErrorHandling(); if (config.enableLogging !== false) { this.setupLogging(); } } setupCommands() { this.bot.command("help", async (ctx) => { await this.handleHelpCommand(ctx); }); this.bot.command("event", async (ctx) => { await this.handleEventCommand(ctx); }); this.bot.command("update", async (ctx) => { await this.handleUpdateCommand(ctx); }); this.bot.command("link", async (ctx) => { await this.handleLinkCommand(ctx); }); this.bot.command("status", async (ctx) => { await this.handleStatusCommand(ctx); }); this.bot.command("init", async (ctx) => { await this.handleInitCommand(ctx); }); this.bot.command("weekly_weave", async (ctx) => { await this.handleWeeklyWeaveCommand(ctx); }); } setupErrorHandling() { this.bot.catch((err, ctx) => { defaultLogger.error("Telegram bot error", { error: err instanceof Error ? err.message : String(err), userId: ctx.from?.id?.toString(), chatId: ctx.chat?.id?.toString() }); ctx.reply("\u274C An unexpected error occurred. Please try again later.").catch(() => { }); }); } setupLogging() { this.bot.use(async (ctx, next) => { const start = Date.now(); const userId = ctx.from?.id?.toString(); const chatId = ctx.chat?.id?.toString(); const messageText = "text" in ctx.message ? ctx.message.text : "[non-text]"; defaultLogger.info("Telegram message received", { userId, chatId, messageText: messageText?.substring(0, 100) }); await next(); const duration = Date.now() - start; defaultLogger.info("Telegram message processed", { userId, chatId, duration }); }); } // Command handlers async handleHelpCommand(ctx) { const helpMessage = `\u{1F916} *WeaveBot Help* *Available Commands:* \u{1F389} \`/event <URL>\` - Extract event information from a web page Example: \`/event https://www.meetup.com/some-event/\` \u{1F4F0} \`/update <URL or text>\` - Process an update or article Examples: \`/update https://blog.example.com/article\` \`/update This is a community announcement...\` \u{1F517} \`/link <URL>\` - Save a link with a one-sentence summary Example: \`/link https://github.com/some-project\` \u{1F4CA} \`/status\` - Show bot status and configuration \u{1F4EC} \`/weekly_weave\` - Generate weekly newsletter (Admin only) \u2753 \`/help\` - Show this help message *Supported Platforms:* \u2022 Meetup.com \u2022 Eventbrite.com \u2022 Facebook Events \u2022 Luma.com \u2022 Most event websites \u2022 Blog articles and news sites *How it works:* 1. Send a URL with \`/event\` or \`/update\` 2. WeaveBot scrapes the page content 3. AI extracts structured information 4. Data is saved to configured storage 5. You get a formatted summary *Tips:* \u2022 Make sure URLs are publicly accessible \u2022 The bot works best with pages that have clear event details \u2022 Processing may take 10-30 seconds for complex pages Need help? Contact the bot administrator.`; await ctx.reply(helpMessage, { parse_mode: "Markdown" }); } async handleEventCommand(ctx) { const userId = ctx.from?.id?.toString(); const chatId = ctx.chat?.id?.toString(); if (!userId || !chatId) { return; } const text = "text" in ctx.message ? ctx.message.text : ""; const args = text.split(" ").slice(1); if (args.length === 0) { await ctx.reply( "\u{1F916} Please provide a URL after the /event command.\n\nExample: `/event https://www.meetup.com/some-event/`", { parse_mode: "Markdown" } ); return; } const url = args[0]; if (!this.isValidUrl(url)) { await ctx.reply( "\u274C That doesn't look like a valid URL. Please provide a full URL starting with http or https." ); return; } await ctx.react([{ type: "emoji", emoji: "\u{1F440}" }]).catch(() => { }); try { const result = await this.processor.process({ type: "url", data: url, schema: "event", options: { timeout: 6e4 // Increase timeout to 60 seconds } }, { userId, chatId }); if (result.success && result.data) { const recordId = await this.storeData(result.data, "event"); const message = this.formatEventMessage(result.data, recordId); await ctx.reply(message, { parse_mode: "Markdown", link_preview_options: { is_disabled: true } }); await ctx.react([{ type: "emoji", emoji: "\u{1F44D}" }]).catch(() => { }); } else { throw new Error(result.error?.message || "Processing failed"); } } catch (error) { await ctx.react([{ type: "emoji", emoji: "\u{1F44E}" }]).catch(() => { }); const errorMessage = this.formatErrorMessage( error.message, url ); await ctx.reply(errorMessage, { parse_mode: "Markdown", link_preview_options: { is_disabled: true } }); } } async handleUpdateCommand(ctx) { const userId = ctx.from?.id?.toString(); const chatId = ctx.chat?.id?.toString(); if (!userId || !chatId) { return; } const text = "text" in ctx.message ? ctx.message.text : ""; const args = text.split(" ").slice(1); if (args.length === 0) { await ctx.reply( "\u{1F916} Please provide a URL or text after the /update command.\n\nExamples:\n`/update https://blog.example.com/article`\n`/update This is a community announcement...`", { parse_mode: "Markdown" } ); return; } const input = args.join(" "); const isUrl = this.isValidUrl(input); await ctx.react([{ type: "emoji", emoji: "\u{1F440}" }]).catch(() => { }); try { const result = await this.processor.process({ type: isUrl ? "url" : "text", data: input, schema: "update" }, { userId, chatId }); if (result.success && result.data) { const recordId = await this.storeData(result.data, "update"); const message = this.formatUpdateMessage(result.data, recordId); await ctx.reply(message, { parse_mode: "Markdown", link_preview_options: { is_disabled: true } }); await ctx.react([{ type: "emoji", emoji: "\u{1F44D}" }]).catch(() => { }); } else { throw new Error(result.error?.message || "Processing failed"); } } catch (error) { await ctx.react([{ type: "emoji", emoji: "\u{1F44E}" }]).catch(() => { }); const errorMessage = this.formatErrorMessage( error.message, isUrl ? input : void 0 ); await ctx.reply(errorMessage, { parse_mode: "Markdown", link_preview_options: { is_disabled: true } }); } } async handleLinkCommand(ctx) { const userId = ctx.from?.id?.toString(); const chatId = ctx.chat?.id?.toString(); if (!userId || !chatId) { return; } const text = "text" in ctx.message ? ctx.message.text : ""; const args = text.split(" ").slice(1); if (args.length === 0) { await ctx.reply( "\u{1F517} Please provide a URL after the /link command.\n\nExample: `/link https://example.com`", { parse_mode: "Markdown" } ); return; } const url = args[0]; if (!this.isValidUrl(url)) { await ctx.reply( "\u274C That doesn't look like a valid URL. Please provide a full URL starting with http or https." ); return; } await ctx.react([{ type: "emoji", emoji: "\u{1F440}" }]).catch(() => { }); try { const result = await this.processor.process({ type: "url", data: url, schema: "link", options: { timeout: 6e4 // Increase timeout to 60 seconds } }, { userId, chatId }); if (result.success && result.data) { const recordId = await this.storeData(result.data, "link"); const message = this.formatLinkMessage(result.data, recordId); await ctx.reply(message, { parse_mode: "Markdown", link_preview_options: { is_disabled: true } }); await ctx.react([{ type: "emoji", emoji: "\u{1F44D}" }]).catch(() => { }); } else { throw new Error(result.error?.message || "Processing failed"); } } catch (error) { await ctx.react([{ type: "emoji", emoji: "\u{1F44E}" }]).catch(() => { }); const errorMessage = this.formatErrorMessage( error.message, url ); await ctx.reply(errorMessage, { parse_mode: "Markdown", link_preview_options: { is_disabled: true } }); } } async handleStatusCommand(ctx) { const processors = this.processor.listProcessors(); const storageAdapters = this.processor.listStorageAdapters(); const schemas = this.processor.listSchemas(); const status = `\u{1F4CA} *Bot Status* *Processors:* ${processors.length > 0 ? processors.join(", ") : "None"} *Storage:* ${storageAdapters.length > 0 ? storageAdapters.join(", ") : "None"} *Schemas:* ${schemas.length > 0 ? schemas.join(", ") : "None"} *Bot Version:* 1.0.0 *Status:* \u2705 Running`; await ctx.reply(status, { parse_mode: "Markdown" }); } async handleInitCommand(ctx) { const userId = ctx.from?.id?.toString(); if (this.config.authorizedAdmins && this.config.authorizedAdmins.length > 0 && !this.config.authorizedAdmins.includes(userId)) { await ctx.reply("\u{1F512} You are not authorized to initialize Airtable configuration."); return; } await ctx.reply("\u{1F504} Initializing Airtable configuration...\n\nDiscovering tables and setting up configuration..."); try { const storageAdapters = this.processor.listStorageAdapters(); if (storageAdapters.length === 0) { throw new Error("No Airtable storage adapter configured"); } const storage = this.processor.getStorage(storageAdapters[0]); if (!storage || !("initializeSetup" in storage)) { throw new Error("Airtable storage adapter does not support initialization"); } const setup = await storage.initializeSetup(); storage.applySetup(setup); await this.saveAirtableConfig(setup); let message = `\u2705 *Airtable Initialization Complete* \u{1F4CA} *Events Table* \u2022 ID: \`${setup.eventsTable.id}\` \u2022 View: \`${setup.eventsTable.primaryViewId}\` \u2022 Fields: ${setup.eventsTable.fields.length} configured \u{1F4F0} *Updates Table* \u2022 ID: \`${setup.updatesTable.id}\` \u2022 View: \`${setup.updatesTable.primaryViewId}\` \u2022 Fields: ${setup.updatesTable.fields.length} configured`; if (setup.linksTable) { message += ` \u{1F517} *Links Table* \u2022 ID: \`${setup.linksTable.id}\` \u2022 View: \`${setup.linksTable.primaryViewId}\` \u2022 Fields: ${setup.linksTable.fields.length} configured`; } message += ` \u{1F4BE} Configuration saved to \`airtable-config.json\` \u{1F389} WeaveBot is now ready to process events, updates, and links with working Airtable links!`; await ctx.reply(message, { parse_mode: "Markdown" }); } catch (error) { const errorMessage = this.formatErrorMessage( error.message, void 0 ); await ctx.reply(errorMessage, { parse_mode: "Markdown", link_preview_options: { is_disabled: true } }); } } async handleWeeklyWeaveCommand(ctx) { const userId = ctx.from?.id?.toString(); if (this.config.authorizedAdmins && this.config.authorizedAdmins.length > 0 && !this.config.authorizedAdmins.includes(userId)) { await ctx.reply("\u{1F512} You are not authorized to generate newsletters."); return; } await ctx.reply( "\u{1F4F0} Newsletter generation is not yet implemented in the library version.\n\nThis feature will be available in a future release." ); } async saveAirtableConfig(setup) { try { const fs = await import("fs/promises"); const path = await import("path"); const configPath = path.join(process.cwd(), "airtable-config.json"); const configData = JSON.stringify(setup, null, 2); await fs.writeFile(configPath, configData, "utf-8"); defaultLogger.info("Airtable configuration saved", { path: configPath }); } catch (error) { defaultLogger.error("Failed to save Airtable configuration", { error: error.message }); throw new Error(`Failed to save configuration: ${error.message}`); } } // Utility methods isValidUrl(string) { try { new URL(string); return true; } catch (_) { return false; } } formatEventMessage(event, recordId) { const airtableLink = recordId ? this.getAirtableRecordLink("Events", recordId) : null; let message = `\u2705 *Event Processed Successfully* *${event.title}* \u{1F4C5} *Date:* ${this.formatDate(event.startDateTime)} \u{1F4CD} *Location:* ${event.location?.venue || event.location?.address || "TBD"} \u{1F3E2} *Organizer:* ${event.organizer?.name || "TBD"} \u{1F39F}\uFE0F *Price:* ${event.ticketPrice?.isFree ? "Free" : event.ticketPrice?.amount ? `${event.ticketPrice.currency} ${event.ticketPrice.amount}` : "TBD"} \u{1F310} *Online:* ${event.isOnline ? "Yes" : "No"} \u{1F4DD} *Description:* ${event.description?.substring(0, 300)}${event.description?.length > 300 ? "..." : ""} *Event saved to database* \u2705`; if (airtableLink) { message += ` [\u{1F4CA} View in Airtable](${airtableLink})`; } return message; } formatUpdateMessage(update, recordId) { const airtableLink = recordId ? this.getAirtableRecordLink("Updates", recordId) : null; let message = `\u2705 *Update Processed Successfully* *${update.title}* \u{1F464} *Author:* ${update.author || "Unknown"} \u{1F4C2} *Category:* ${update.category} \u2B50 *Relevance:* ${update.relevanceScore}/10 \u{1F4DD} *Summary:* ${update.summary} *Update saved to database* \u2705`; if (airtableLink) { message += ` [\u{1F4CA} View in Airtable](${airtableLink})`; } return message; } formatLinkMessage(link, recordId) { const airtableLink = recordId ? this.getAirtableRecordLink("Links", recordId) : null; let message = `\u2705 *Link Saved Successfully* \u{1F517} *URL:* ${link.url} \u{1F4DD} *Title:* ${link.title} \u{1F4AC} *Summary:* _${link.summary}_ *Link saved to database* \u2705`; if (airtableLink) { message += ` [\u{1F4CA} View in Airtable](${airtableLink})`; } return message; } formatErrorMessage(error, url) { let message = `\u274C *Processing Failed* `; if (url) { message += `*URL:* ${url} `; } message += `*Error:* ${error} `; message += `\u{1F527} *Troubleshooting:* `; message += `\u2022 Make sure the URL is publicly accessible `; message += `\u2022 Check if the page contains event/update information `; message += `\u2022 Try again in a few moments `; message += `\u2022 Contact administrator if the problem persists`; return message; } formatDate(dateString) { try { return new Date(dateString).toLocaleString(); } catch { return dateString; } } async storeData(data, schema) { try { const storageAdapters = this.processor.listStorageAdapters(); if (storageAdapters.length > 0) { const storage = this.processor.getStorage(storageAdapters[0]); if (storage) { const result = await storage.create(data, schema); defaultLogger.info("Data stored successfully", { schema, recordId: result?.id }); return result?.id; } } } catch (error) { defaultLogger.error("Failed to store data", { error: error.message, schema }); } return void 0; } getAirtableRecordLink(tableName, recordId) { try { const schemaMap = { "Events": "event", "Updates": "update", "Links": "link" }; const schema = schemaMap[tableName]; if (!schema) { defaultLogger.warn("Unknown table name for Airtable link", { tableName }); return null; } const storageAdapters = this.processor.listStorageAdapters(); if (storageAdapters.length > 0) { const storage = this.processor.getStorage(storageAdapters[0]); if (storage && "getBaseId" in storage && "getTableId" in storage && "getViewId" in storage) { const baseId = storage.getBaseId(); const tableId = storage.getTableId(schema); const viewId = storage.getViewId(schema); return `https://airtable.com/${baseId}/${tableId}/${viewId}/${recordId}?blocks=hide`; } } } catch (error) { defaultLogger.warn("Could not generate Airtable link", { error: error.message }); } return null; } // Public methods async start() { defaultLogger.info("Starting Telegram bot..."); try { await this.bot.launch(); defaultLogger.info("Telegram bot started successfully"); process.once("SIGINT", () => this.stop()); process.once("SIGTERM", () => this.stop()); } catch (error) { defaultLogger.error("Failed to start Telegram bot", { error: error.message }); throw error; } } async stop() { defaultLogger.info("Stopping Telegram bot..."); this.bot.stop(); defaultLogger.info("Telegram bot stopped"); } // Add custom command handler addCommand(command, handler) { this.bot.command(command, handler); } // Get underlying bot instance for advanced usage getBot() { return this.bot; } // Get processor instance getProcessor() { return this.processor; } }; export { TelegramBot }; //# sourceMappingURL=index.mjs.map