import { Bot } from "https://deno.land/x/grammy@v1.36.1/mod.ts"; import { TELEGRAM_API_KEY } from "@lib/env.ts"; import { transcribe } from "@lib/openai.ts"; import { createDocument } from "@lib/documents.ts"; import { renderMarkdown } from "@lib/documents.ts"; const bot = new Bot(TELEGRAM_API_KEY); // In-memory task state const activeTasks: Record< string, { noteName: string; entries: Array< { type: string; content: string | Uint8Array; fileName?: string } >; } > = {}; async function downloadFile(filePath: string): Promise { const url = `https://api.telegram.org/file/bot${bot.token}/${filePath}`; const response = await fetch(url); if (!response.ok) { throw new Error("Failed to download file: " + response.statusText); } const buffer = await response.arrayBuffer(); return new Uint8Array(buffer); } bot.command("start", async (ctx) => { const [_, noteName] = ctx.message?.text?.split(" ") || []; if (!noteName) { return ctx.reply("Please provide a note name. Usage: /start NoteName"); } activeTasks[ctx.chat.id.toString()] = { noteName, entries: [] }; await ctx.reply(`Started note: ${noteName}`); }); bot.command("end", async (ctx) => { const task = activeTasks[ctx.chat.id.toString()]; if (!task) return ctx.reply("No active note found."); console.log("Ending note", task.noteName); let finalNote = `# ${task.noteName}\n\n`; const photoTasks: { content: Uint8Array; path: string }[] = []; let photoIndex = 0; for (const entry of task.entries) { if (entry.type === "text") { finalNote += entry.content + "\n\n"; } else if (entry.type === "voice") { try { console.log("Converting OGG to MP3"); const mp3Data = await convertOggToMp3(entry.content as Uint8Array); console.log("Finished converting OGG to MP3, transcribing..."); const transcript = await transcribe(mp3Data); finalNote += `**Voice Transcript:**\n${transcript}\n\n`; console.log("Finished transcribing"); } catch (error) { console.log(error); finalNote += "**[Voice message could not be transcribed]**\n\n"; } } else if (entry.type === "photo") { const photoUrl = `${ task.noteName.replace(/\.md$/, "") }/photo-${photoIndex++}.jpg`; finalNote += `**Photo**:\n ${photoUrl}\n\n`; photoTasks.push({ content: entry.content as Uint8Array, path: photoUrl, }); } } try { for (const entry of photoTasks) { await createDocument(entry.path, entry.content, "image/jpeg"); } await createDocument(task.noteName, finalNote, "text/markdown"); delete activeTasks[ctx.chat.id.toString()]; await ctx.reply("Note complete. Here is your markdown:"); await ctx.reply(renderMarkdown(finalNote), { parse_mode: "HTML" }); } catch (error) { console.error("Error creating document:", error); await ctx.reply("Error creating document:"); if (error instanceof Error) { await ctx.reply(error?.toString()); } else if (error instanceof Response) { await ctx.reply(error?.statusText); } else if (typeof error === "string") { await ctx.reply(error); } } }); bot.on("message:text", (ctx) => { const task = activeTasks[ctx.chat.id.toString()]; if (!task) return; const entry = { type: "text", content: ctx.message.text }; console.log("New Entry", entry); task.entries.push(entry); }); bot.on("message:voice", async (ctx) => { const task = activeTasks[ctx.chat.id.toString()]; if (!task) return; const file = await ctx.getFile(); const buffer = await downloadFile(file.file_path!); const entry = { type: "voice", content: buffer }; console.log("New Entry", entry); task.entries.push(entry); }); bot.on("message:photo", async (ctx) => { const task = activeTasks[ctx.chat.id.toString()]; if (!task) return; const file = await ctx.getFile(); const buffer = await downloadFile(file.file_path!); const fileName = file.file_path!.split("/").pop()!; const entry = { type: "photo", content: buffer, fileName }; console.log("New Entry", entry); task.entries.push(entry); }); export async function convertOggToMp3( oggData: Uint8Array, ): Promise { const ffmpeg = new Deno.Command("ffmpeg", { args: ["-f", "ogg", "-i", "pipe:0", "-f", "mp3", "pipe:1"], stdin: "piped", stdout: "piped", stderr: "null", }); const process = ffmpeg.spawn(); const writer = process.stdin.getWriter(); await writer.write(oggData); await writer.close(); const output = await process.output(); const { code } = await process.status; if (code !== 0) throw new Error(`FFmpeg exited with code ${code}`); return output.stdout; } bot.start();