From 0beb3b10713add107ddbbffeb3695951344e2695 Mon Sep 17 00:00:00 2001 From: Max Richter Date: Thu, 22 May 2025 15:36:38 +0200 Subject: [PATCH] refactor: split telegram.ts into seperate files --- lib/helpers.ts | 21 ++++++++ lib/taskManager.ts | 110 ++++++++++++++++++++++++++++++++++++++++ lib/telegram.ts | 122 +++++++-------------------------------------- 3 files changed, 150 insertions(+), 103 deletions(-) create mode 100644 lib/taskManager.ts diff --git a/lib/helpers.ts b/lib/helpers.ts index 1661774..6d6a745 100644 --- a/lib/helpers.ts +++ b/lib/helpers.ts @@ -107,3 +107,24 @@ export function parseRating(rating: string | number) { } return rating; } + +export async function convertOggToMp3( + oggData: ArrayBuffer, +): 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(new Uint8Array(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; +} diff --git a/lib/taskManager.ts b/lib/taskManager.ts new file mode 100644 index 0000000..3182da4 --- /dev/null +++ b/lib/taskManager.ts @@ -0,0 +1,110 @@ +import { transcribe } from "@lib/openai.ts"; +import { createDocument } from "@lib/documents.ts"; +import { createLogger } from "./log/index.ts"; +import { convertOggToMp3 } from "./helpers.ts"; + +const log = createLogger("taskManager"); + +// In-memory task state +const activeTasks: Record< + string, + { + noteName: string; + entries: Array< + { type: string; content: string | ArrayBufferLike; fileName?: string } + >; + } +> = {}; + +export function startTask(chatId: string, noteName: string) { + activeTasks[chatId] = { noteName, entries: [] }; + log.info(`Started note: ${noteName}`); +} + +export async function endTask(chatId: string): Promise { + const task = activeTasks[chatId]; + if (!task) return null; + + log.info("Ending note", task.noteName); + + let finalNote = `# ${task.noteName}\n\n`; + + const photoTasks: { content: ArrayBuffer; 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 { + log.info("Converting OGG to MP3"); + const mp3Data = await convertOggToMp3(entry.content as ArrayBuffer); + log.info("Finished converting OGG to MP3, transcribing..."); + const transcript = await transcribe(mp3Data); + finalNote += `**Voice Transcript:**\n${transcript}\n\n`; + log.info("Finished transcribing"); + } catch (error) { + log.error(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 ArrayBuffer, + path: photoUrl, + }); + } + } + + try { + for (const entry of photoTasks) { + await createDocument(entry.path, entry.content, "image/jpeg"); + } + } catch (err) { + log.error("Error creating photo document:", err); + } + try { + await createDocument(task.noteName, finalNote, "text/markdown"); + } catch (error) { + log.error("Error creating document:", error); + return error instanceof Error + ? error.toString() + : "Error creating document"; + } + + delete activeTasks[chatId]; + log.debug({ finalNote }); + return finalNote; +} + +export function addTextEntry(chatId: string, text: string) { + const task = activeTasks[chatId]; + if (!task) return; + const entry = { type: "text", content: text }; + log.debug("New Entry", entry); + task.entries.push(entry); +} + +export function addVoiceEntry(chatId: string, buffer: ArrayBufferLike) { + const task = activeTasks[chatId]; + if (!task) return; + const entry = { type: "voice", content: buffer }; + log.debug("New Entry", entry); + task.entries.push(entry); +} + +export function addPhotoEntry( + chatId: string, + buffer: ArrayBufferLike, + fileName: string, +) { + const task = activeTasks[chatId]; + if (!task) return; + const entry = { type: "photo", content: buffer, fileName }; + log.debug("New Entry", entry); + task.entries.push(entry); +} diff --git a/lib/telegram.ts b/lib/telegram.ts index a981eb5..d4d671d 100644 --- a/lib/telegram.ts +++ b/lib/telegram.ts @@ -1,149 +1,65 @@ 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"; +import { createLogger } from "./log/index.ts"; const bot = new Bot(TELEGRAM_API_KEY); +const log = createLogger("telegram"); -// In-memory task state -const activeTasks: Record< - string, - { - noteName: string; - entries: Array< - { type: string; content: string | Uint8Array; fileName?: string } - >; - } -> = {}; +import * as manager from "./taskManager.ts"; async function downloadFile(filePath: string): Promise { + log.info(`Downloading file from path: ${filePath}`); 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); } + log.info("File downloaded successfully"); const buffer = await response.arrayBuffer(); return new Uint8Array(buffer); } bot.command("start", async (ctx) => { + log.info("Received /start command"); 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: [] }; + manager.startTask(ctx.chat.id.toString(), noteName); 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, - }); - } - } + log.info("Received /end command"); + const finalNote = await manager.endTask(ctx.chat.id.toString()); + if (!finalNote) return ctx.reply("No active note found."); 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); - } + await ctx.reply(finalNote, { parse_mode: "MarkdownV2" }); + } catch (err) { + console.error("Error sending final note:", err); } }); 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); + log.info("Received text message"); + manager.addTextEntry(ctx.chat.id.toString(), ctx.message.text); }); bot.on("message:voice", async (ctx) => { - const task = activeTasks[ctx.chat.id.toString()]; - if (!task) return; + log.info("Received photo message"); + log.info("Received voice message"); 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); + manager.addVoiceEntry(ctx.chat.id.toString(), buffer.buffer); }); 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); + manager.addPhotoEntry(ctx.chat.id.toString(), buffer.buffer, fileName); }); -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();