weavebot-telegram
Version:
Generic Telegram bot framework with command registration
598 lines (560 loc) • 20.2 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
TelegramBot: () => TelegramBot
});
module.exports = __toCommonJS(index_exports);
// src/bot.ts
var import_telegraf = require("telegraf");
var import_weavebot_core = __toESM(require("weavebot-core"));
var TelegramBot = class {
bot;
processor;
config;
constructor(config) {
this.config = config;
this.bot = new import_telegraf.Telegraf(config.botToken);
this.processor = config.processor || new import_weavebot_core.default();
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) => {
import_weavebot_core.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]";
import_weavebot_core.defaultLogger.info("Telegram message received", {
userId,
chatId,
messageText: messageText?.substring(0, 100)
});
await next();
const duration = Date.now() - start;
import_weavebot_core.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");
import_weavebot_core.defaultLogger.info("Airtable configuration saved", { path: configPath });
} catch (error) {
import_weavebot_core.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);
import_weavebot_core.defaultLogger.info("Data stored successfully", { schema, recordId: result?.id });
return result?.id;
}
}
} catch (error) {
import_weavebot_core.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) {
import_weavebot_core.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) {
import_weavebot_core.defaultLogger.warn("Could not generate Airtable link", { error: error.message });
}
return null;
}
// Public methods
async start() {
import_weavebot_core.defaultLogger.info("Starting Telegram bot...");
try {
await this.bot.launch();
import_weavebot_core.defaultLogger.info("Telegram bot started successfully");
process.once("SIGINT", () => this.stop());
process.once("SIGTERM", () => this.stop());
} catch (error) {
import_weavebot_core.defaultLogger.error("Failed to start Telegram bot", {
error: error.message
});
throw error;
}
}
async stop() {
import_weavebot_core.defaultLogger.info("Stopping Telegram bot...");
this.bot.stop();
import_weavebot_core.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;
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
TelegramBot
});
//# sourceMappingURL=index.js.map