feat: add telegram bot
This commit is contained in:
parent
d450f4ed42
commit
acefbcbd14
@ -6,7 +6,8 @@
|
|||||||
],
|
],
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx",
|
"check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx",
|
||||||
"start": "deno run --env-file -A --watch=static/,routes/ dev.ts",
|
"dev": "deno run --env-file -A --watch=static/,routes/ dev.ts",
|
||||||
|
"start": "deno run --env-file -A main.ts",
|
||||||
"db": "deno run --env-file -A npm:drizzle-kit",
|
"db": "deno run --env-file -A npm:drizzle-kit",
|
||||||
"build": "deno run -A dev.ts build",
|
"build": "deno run -A dev.ts build",
|
||||||
"preview": "deno run -A main.ts",
|
"preview": "deno run -A main.ts",
|
||||||
|
@ -8,6 +8,7 @@ export const SILVERBULLET_SERVER = Deno.env.get("SILVERBULLET_SERVER");
|
|||||||
export const TMDB_API_KEY = Deno.env.get("TMDB_API_KEY");
|
export const TMDB_API_KEY = Deno.env.get("TMDB_API_KEY");
|
||||||
export const OPENAI_API_KEY = Deno.env.get("OPENAI_API_KEY");
|
export const OPENAI_API_KEY = Deno.env.get("OPENAI_API_KEY");
|
||||||
export const YOUTUBE_API_KEY = Deno.env.get("YOUTUBE_API_KEY");
|
export const YOUTUBE_API_KEY = Deno.env.get("YOUTUBE_API_KEY");
|
||||||
|
export const TELEGRAM_API_KEY = Deno.env.get("TELEGRAM_API_KEY")!;
|
||||||
|
|
||||||
export const GITEA_SERVER = Deno.env.get("GITEA_SERVER");
|
export const GITEA_SERVER = Deno.env.get("GITEA_SERVER");
|
||||||
export const GITEA_CLIENT_ID = Deno.env.get("GITEA_CLIENT_ID")!;
|
export const GITEA_CLIENT_ID = Deno.env.get("GITEA_CLIENT_ID")!;
|
||||||
|
@ -228,3 +228,21 @@ export async function extractRecipe(content: string) {
|
|||||||
|
|
||||||
return recipeResponseSchema.parse(completion.choices[0].message.parsed);
|
return recipeResponseSchema.parse(completion.choices[0].message.parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function transcribe(
|
||||||
|
mp3Data: Uint8Array,
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
if (!openAI) return;
|
||||||
|
|
||||||
|
const file = new File([mp3Data], "audio.mp3", {
|
||||||
|
type: "audio/mpeg",
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await openAI.audio.transcriptions.create({
|
||||||
|
file,
|
||||||
|
model: "whisper-1",
|
||||||
|
response_format: "text",
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
124
lib/telegram.ts
Normal file
124
lib/telegram.ts
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
const bot = new Bot(TELEGRAM_API_KEY);
|
||||||
|
|
||||||
|
// In-memory task state
|
||||||
|
const activeTasks: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
noteName: string;
|
||||||
|
entries: Array<{ type: string; content: string | Uint8Array }>;
|
||||||
|
}
|
||||||
|
> = {};
|
||||||
|
|
||||||
|
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.");
|
||||||
|
|
||||||
|
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 {
|
||||||
|
const mp3Data = await convertOggToMp3(entry.content as Uint8Array);
|
||||||
|
const transcript = await transcribe(mp3Data);
|
||||||
|
finalNote += `**Voice Transcript:**\n${transcript}\n\n`;
|
||||||
|
} catch (_) {
|
||||||
|
finalNote += "**[Voice message could not be transcribed]**\n\n";
|
||||||
|
}
|
||||||
|
} else if (entry.type === "photo") {
|
||||||
|
photoIndex++;
|
||||||
|
|
||||||
|
const photoUrl = `${
|
||||||
|
task.noteName.replace(/\.md$/, "")
|
||||||
|
}/photo-${photoIndex}.jpg`;
|
||||||
|
|
||||||
|
finalNote += `\n\n`;
|
||||||
|
photoTasks.push({
|
||||||
|
content: entry.content as Uint8Array,
|
||||||
|
path: photoUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(finalNote);
|
||||||
|
});
|
||||||
|
|
||||||
|
bot.on("message:text", (ctx) => {
|
||||||
|
const task = activeTasks[ctx.chat.id.toString()];
|
||||||
|
if (!task) return;
|
||||||
|
task.entries.push({ type: "text", content: ctx.message.text });
|
||||||
|
});
|
||||||
|
|
||||||
|
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!);
|
||||||
|
task.entries.push({ type: "voice", content: 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!);
|
||||||
|
task.entries.push({ type: "photo", content: buffer });
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
2
main.ts
2
main.ts
@ -7,5 +7,7 @@
|
|||||||
import { start } from "$fresh/server.ts";
|
import { start } from "$fresh/server.ts";
|
||||||
import manifest from "./fresh.gen.ts";
|
import manifest from "./fresh.gen.ts";
|
||||||
import config from "./fresh.config.ts";
|
import config from "./fresh.config.ts";
|
||||||
|
import "@lib/telegram.ts";
|
||||||
|
console.log("hello");
|
||||||
|
|
||||||
await start(manifest, config);
|
await start(manifest, config);
|
||||||
|
@ -4,6 +4,7 @@ import { PageProps } from "$fresh/server.ts";
|
|||||||
import { resources } from "@lib/resources.ts";
|
import { resources } from "@lib/resources.ts";
|
||||||
import { RedirectSearchHandler } from "@islands/Search.tsx";
|
import { RedirectSearchHandler } from "@islands/Search.tsx";
|
||||||
import { KMenu } from "@islands/KMenu.tsx";
|
import { KMenu } from "@islands/KMenu.tsx";
|
||||||
|
import "@lib/telegram.ts";
|
||||||
|
|
||||||
export default function Home(props: PageProps) {
|
export default function Home(props: PageProps) {
|
||||||
return (
|
return (
|
||||||
|
Loading…
x
Reference in New Issue
Block a user