memorium/lib/telegram.ts
2025-05-09 19:57:18 +02:00

150 lines
4.7 KiB
TypeScript

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<Uint8Array> {
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<Uint8Array> {
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();