refactor: split telegram.ts into seperate files
This commit is contained in:
parent
001c524d73
commit
0beb3b1071
@ -107,3 +107,24 @@ export function parseRating(rating: string | number) {
|
|||||||
}
|
}
|
||||||
return rating;
|
return rating;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function convertOggToMp3(
|
||||||
|
oggData: ArrayBuffer,
|
||||||
|
): 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(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;
|
||||||
|
}
|
||||||
|
110
lib/taskManager.ts
Normal file
110
lib/taskManager.ts
Normal file
@ -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<string | null> {
|
||||||
|
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);
|
||||||
|
}
|
122
lib/telegram.ts
122
lib/telegram.ts
@ -1,149 +1,65 @@
|
|||||||
import { Bot } from "https://deno.land/x/grammy@v1.36.1/mod.ts";
|
import { Bot } from "https://deno.land/x/grammy@v1.36.1/mod.ts";
|
||||||
import { TELEGRAM_API_KEY } from "@lib/env.ts";
|
import { TELEGRAM_API_KEY } from "@lib/env.ts";
|
||||||
import { transcribe } from "@lib/openai.ts";
|
import { createLogger } from "./log/index.ts";
|
||||||
import { createDocument } from "@lib/documents.ts";
|
|
||||||
import { renderMarkdown } from "@lib/documents.ts";
|
|
||||||
|
|
||||||
const bot = new Bot(TELEGRAM_API_KEY);
|
const bot = new Bot(TELEGRAM_API_KEY);
|
||||||
|
const log = createLogger("telegram");
|
||||||
|
|
||||||
// In-memory task state
|
import * as manager from "./taskManager.ts";
|
||||||
const activeTasks: Record<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
noteName: string;
|
|
||||||
entries: Array<
|
|
||||||
{ type: string; content: string | Uint8Array; fileName?: string }
|
|
||||||
>;
|
|
||||||
}
|
|
||||||
> = {};
|
|
||||||
|
|
||||||
async function downloadFile(filePath: string): Promise<Uint8Array> {
|
async function downloadFile(filePath: string): Promise<Uint8Array> {
|
||||||
|
log.info(`Downloading file from path: ${filePath}`);
|
||||||
const url = `https://api.telegram.org/file/bot${bot.token}/${filePath}`;
|
const url = `https://api.telegram.org/file/bot${bot.token}/${filePath}`;
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Failed to download file: " + response.statusText);
|
throw new Error("Failed to download file: " + response.statusText);
|
||||||
}
|
}
|
||||||
|
log.info("File downloaded successfully");
|
||||||
const buffer = await response.arrayBuffer();
|
const buffer = await response.arrayBuffer();
|
||||||
return new Uint8Array(buffer);
|
return new Uint8Array(buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
bot.command("start", async (ctx) => {
|
bot.command("start", async (ctx) => {
|
||||||
|
log.info("Received /start command");
|
||||||
const [_, noteName] = ctx.message?.text?.split(" ") || [];
|
const [_, noteName] = ctx.message?.text?.split(" ") || [];
|
||||||
if (!noteName) {
|
if (!noteName) {
|
||||||
return ctx.reply("Please provide a note name. Usage: /start 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}`);
|
await ctx.reply(`Started note: ${noteName}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
bot.command("end", async (ctx) => {
|
bot.command("end", async (ctx) => {
|
||||||
const task = activeTasks[ctx.chat.id.toString()];
|
log.info("Received /end command");
|
||||||
if (!task) return ctx.reply("No active note found.");
|
const finalNote = await manager.endTask(ctx.chat.id.toString());
|
||||||
|
if (!finalNote) 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 {
|
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("Note complete. Here is your markdown:");
|
||||||
await ctx.reply(renderMarkdown(finalNote), { parse_mode: "HTML" });
|
await ctx.reply(finalNote, { parse_mode: "MarkdownV2" });
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
console.error("Error creating document:", error);
|
console.error("Error sending final note:", err);
|
||||||
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) => {
|
bot.on("message:text", (ctx) => {
|
||||||
const task = activeTasks[ctx.chat.id.toString()];
|
log.info("Received text message");
|
||||||
if (!task) return;
|
manager.addTextEntry(ctx.chat.id.toString(), ctx.message.text);
|
||||||
const entry = { type: "text", content: ctx.message.text };
|
|
||||||
console.log("New Entry", entry);
|
|
||||||
task.entries.push(entry);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
bot.on("message:voice", async (ctx) => {
|
bot.on("message:voice", async (ctx) => {
|
||||||
const task = activeTasks[ctx.chat.id.toString()];
|
log.info("Received photo message");
|
||||||
if (!task) return;
|
log.info("Received voice message");
|
||||||
const file = await ctx.getFile();
|
const file = await ctx.getFile();
|
||||||
const buffer = await downloadFile(file.file_path!);
|
const buffer = await downloadFile(file.file_path!);
|
||||||
const entry = { type: "voice", content: buffer };
|
manager.addVoiceEntry(ctx.chat.id.toString(), buffer.buffer);
|
||||||
console.log("New Entry", entry);
|
|
||||||
task.entries.push(entry);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
bot.on("message:photo", async (ctx) => {
|
bot.on("message:photo", async (ctx) => {
|
||||||
const task = activeTasks[ctx.chat.id.toString()];
|
|
||||||
if (!task) return;
|
|
||||||
const file = await ctx.getFile();
|
const file = await ctx.getFile();
|
||||||
const buffer = await downloadFile(file.file_path!);
|
const buffer = await downloadFile(file.file_path!);
|
||||||
const fileName = file.file_path!.split("/").pop()!;
|
const fileName = file.file_path!.split("/").pop()!;
|
||||||
const entry = { type: "photo", content: buffer, fileName };
|
manager.addPhotoEntry(ctx.chat.id.toString(), buffer.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();
|
bot.start();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user