import { analyzeText, writeToMonica, appendLocalRecord, monicaFetch, cryptoRandomId, } from "./server.js"; const monicaBaseUrl = (process.env.MONICA_BASE_URL || "https://crm.mischlabs.de").replace(/\/$/, ""); // Helper to escape HTML characters for Telegram HTML parse_mode function escapeHtml(text) { if (!text) return ""; return text .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } // Send a message back to the Telegram chat async function sendTelegramMessage(chatId, text, replyToMessageId = null) { const token = process.env.TELEGRAM_BOT_TOKEN; if (!token) return; const url = `https://api.telegram.org/bot${token}/sendMessage`; const body = { chat_id: chatId, text, parse_mode: "HTML", disable_web_page_preview: true, }; if (replyToMessageId) { body.reply_to_message_id = replyToMessageId; } try { const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!res.ok) { console.error(`[Telegram] Failed to send message: ${res.status} - ${await res.text()}`); } } catch (err) { console.error("[Telegram] Error sending message:", err); } } // Send Chat Action (e.g. typing, upload_voice) to show the bot is active async function sendChatAction(chatId, action) { const token = process.env.TELEGRAM_BOT_TOKEN; if (!token) return; const url = `https://api.telegram.org/bot${token}/sendChatAction`; try { await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ chat_id: chatId, action }), }); } catch (e) { // Silently ignore chat action errors } } // Download voice note file from Telegram servers async function downloadTelegramFile(filePath) { const token = process.env.TELEGRAM_BOT_TOKEN; const url = `https://api.telegram.org/file/bot${token}/${filePath}`; const res = await fetch(url); if (!res.ok) { throw new Error(`Failed to download Telegram file: ${res.status} ${res.statusText}`); } return await res.arrayBuffer(); } // Transcribe OGG/Opus voice data using Google Gemini 1.5 Flash API (100% Free) async function transcribeVoice(audioBuffer) { const geminiKey = process.env.GEMINI_API_KEY; if (!geminiKey) { throw new Error("GEMINI_API_KEY is not configured in .env."); } const base64Audio = Buffer.from(audioBuffer).toString("base64"); const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${geminiKey}`; const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ contents: [ { parts: [ { inlineData: { mimeType: "audio/ogg", data: base64Audio, }, }, { text: "Transkribiere diese Sprachnachricht wortgetreu in deutschen Text. Gib NUR die Transkription zurück, ohne Einleitung, Kommentare oder sonstige Zusätze. Falls nichts verständlich gesprochen wurde, antworte mit einem leeren Text.", }, ], }, ], }), }); if (!res.ok) { const errorText = await res.text(); throw new Error(`Gemini API failed (${res.status}): ${errorText}`); } const data = await res.json(); const text = data.candidates?.[0]?.content?.parts?.[0]?.text || ""; return text.trim(); } // Fetch available contacts from Monica to perform NLP name matching async function fetchMonicaContacts() { if (!process.env.MONICA_API_TOKEN) { // Return standard demo contacts if no Monica token configured return [ { id: 1, name: "Aaron Lingel", description: "Demo-Kontakt" }, { id: 2, name: "Tom", description: "Demo-Kontakt" }, { id: 3, name: "MrDiderot", description: "Demo-Kontakt" }, ]; } try { const payload = await monicaFetch("/api/contacts?limit=150"); const contacts = Array.isArray(payload.data) ? payload.data : []; return contacts.map((c) => ({ id: c.id, name: c.complete_name || [c.first_name, c.last_name].filter(Boolean).join(" "), description: c.description || "", })); } catch (err) { console.error("[Telegram] Error fetching Monica contacts:", err.message); return []; } } // Process an incoming update from the poller async function handleUpdate(update) { const message = update.message; if (!message) return; const chatId = String(message.chat?.id || ""); const fromId = String(message.from?.id || ""); if (!chatId) return; // 1. Security & Access Control check const allowedIds = (process.env.TELEGRAM_ALLOWED_CHAT_IDS || "") .split(",") .map((id) => id.trim()) .filter(Boolean); const isAllowed = allowedIds.includes(chatId) || allowedIds.includes(fromId); if (!isAllowed) { console.warn(`[Telegram] Unauthorized access attempt from Chat ID: ${chatId}, From ID: ${fromId}`); await sendTelegramMessage( chatId, `⚠️ Zugriff verweigert\n\nDieses Friends-System ist privat. Deine Telegram Chat-ID ist:\n${chatId}\n\nBitte füge diese ID der Variablen TELEGRAM_ALLOWED_CHAT_IDS in deiner .env-Datei auf dem Server hinzu und starte Friends neu, um dich zu autorisieren.`, message.message_id ); return; } const msgText = message.text; const voice = message.voice; if (!msgText && !voice) { await sendTelegramMessage( chatId, "ℹ️ Hinweis: Ich unterstütze aktuell nur Textnachrichten und Sprachnachrichten (Sprachnotizen). Sende mir einfach einen Text oder eine Sprachaufzeichnung!", message.message_id ); return; } let processedText = ""; let isVoice = false; // Send a typing status immediately to show the bot is thinking await sendChatAction(chatId, voice ? "record_audio" : "typing"); // 2. Extract text (either message text or transcribe voice note) if (voice) { isVoice = true; if (!process.env.GEMINI_API_KEY) { await sendTelegramMessage( chatId, "⚠️ Sprachnachrichten deaktiviert:\nEs ist kein GEMINI_API_KEY in der .env-Konfiguration hinterlegt. Bitte erstelle einen kostenlosen Key im Google AI Studio und füge ihn hinzu.", message.message_id ); return; } try { const token = process.env.TELEGRAM_BOT_TOKEN; // Get file path from Telegram Bot API const fileInfoRes = await fetch(`https://api.telegram.org/bot${token}/getFile?file_id=${voice.file_id}`); if (!fileInfoRes.ok) { throw new Error(`Telegram getFile endpoint failed: ${fileInfoRes.status}`); } const fileInfo = await fileInfoRes.json(); if (!fileInfo.ok || !fileInfo.result?.file_path) { throw new Error(`Invalid Telegram file info response: ${JSON.stringify(fileInfo)}`); } // Download file binary const audioBuffer = await downloadTelegramFile(fileInfo.result.file_path); // Transcribe via Gemini processedText = await transcribeVoice(audioBuffer); if (!processedText.trim()) { await sendTelegramMessage( chatId, "🎤 ❌ Transkriptionsfehler: Die Sprachnachricht konnte transkribiert werden, enthielt aber keinen Text. Bitte versuche es deutlicher.", message.message_id ); return; } } catch (err) { console.error("[Telegram] Voice transcription error:", err); await sendTelegramMessage( chatId, `🎤 ❌ Fehler bei Sprachnachricht:\n${escapeHtml(err.message)}`, message.message_id ); return; } } else { processedText = msgText.trim(); } // 3. Match contacts and run NLP engine await sendChatAction(chatId, "typing"); const contacts = await fetchMonicaContacts(); const analysis = analyzeText(processedText, "auto", contacts); // 4. Save entry locally and sync to Monica CRM const entry = { rawText: processedText, type: analysis.type, summary: analysis.summary, contact: analysis.contact, note: analysis.note, journal: analysis.journal, reminders: analysis.reminders, writeToMonica: true, }; const savedAt = new Date().toISOString(); const localRecord = { id: cryptoRandomId(), savedAt, entry, monicaResults: [], }; if (process.env.MONICA_API_TOKEN) { localRecord.monicaResults = await writeToMonica(entry); } await appendLocalRecord(localRecord); // 5. Build and send beautiful success report to the user let responseText = `Eintrag erfasst! ✅\n\n`; if (isVoice) { responseText += `🎤 Transkription (Gemini):\n"${escapeHtml(processedText)}"\n\n`; } if (analysis.contact) { const contactUrl = `${monicaBaseUrl}/people/${analysis.contact.id}`; responseText += `👤 Kontakt: ${escapeHtml(analysis.contact.name)}\n`; } else { responseText += `👤 Kontakt: Keine Zuordnung\n`; } if (analysis.note?.enabled && analysis.note?.body) { responseText += `📝 Kontaktnotiz: ${escapeHtml(analysis.note.body)}\n`; } if (analysis.journal?.enabled && analysis.journal?.title) { responseText += `📖 Tagebuch: ${escapeHtml(analysis.journal.title)}\n`; } if (analysis.reminders && analysis.reminders.length > 0) { for (const reminder of analysis.reminders) { responseText += `📅 Erinnerung: ${escapeHtml(reminder.title)} am ${escapeHtml(reminder.initial_date)}\n`; } } responseText += `\nMonica CRM Synchronisation:\n`; if (localRecord.monicaResults && localRecord.monicaResults.length > 0) { for (const res of localRecord.monicaResults) { const typeLabel = res.type === "journal" ? "📖 Tagebuch" : res.type === "note" ? "📝 Notiz" : "📅 Erinnerung"; if (res.ok) { responseText += `- ${typeLabel}: ✅ Erfolgreich (ID: ${res.id})\n`; } else { responseText += `- ${typeLabel}: ❌ Fehler: ${escapeHtml(res.error)}\n`; } } } else { responseText += `Lokal protokolliert (Keine Übertragung an Monica CRM).\n`; } await sendTelegramMessage(chatId, responseText, message.message_id); } // Infinite loop long poller for Telegram Updates let offset = 0; async function pollUpdates() { const token = process.env.TELEGRAM_BOT_TOKEN; if (!token) return; const url = `https://api.telegram.org/bot${token}/getUpdates?offset=${offset}&timeout=30`; try { const res = await fetch(url); if (!res.ok) { console.error(`[Telegram] Polling failed (${res.status}): ${res.statusText}`); await new Promise((resolve) => setTimeout(resolve, 5000)); return; } const data = await res.json(); if (data.ok && Array.isArray(data.result)) { for (const update of data.result) { offset = update.update_id + 1; await handleUpdate(update).catch((err) => { console.error("[Telegram] Error processing update:", err); }); } } } catch (err) { console.error("[Telegram] Network error during polling:", err); await new Promise((resolve) => setTimeout(resolve, 5000)); } } // Main bot entrypoint export function startTelegramBot() { console.log("[Telegram] Long-Polling Bot started successfully."); (async () => { while (true) { await pollUpdates(); } })(); }