mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-12 10:12:11 +03:00
* fix(deps): bump transitive deps for 6 Dependabot + remaining audit vulns on main Same overrides as #9464 (ip-address, hono, fast-uri, socket.io-parser, undici) applied directly to main. Also covers brace-expansion (scoped), js-yaml v4 copies, and mermaid. npm audit: 6→0 vulnerabilities. Closes Dependabot #161-#166. * fix(deps): bump nanoid, dompurify for 2 new Dependabot alerts (#189, #190) Bumps: nanoid ^3.3.17 (was transitive, now overridden), dompurify ^3.4.13 (with monaco-editor scoped override). Closes Dependabot #189, #190. Remaining #182-#188 (js-yaml + mermaid) already closed by #9651 merge — awaiting Dependabot re-scan. npm audit → 0 vulnerabilities. * fix(repo): harden .gitignore to also ignore a _tasks symlink (/_tasks) _tasks is a SEPARATE nested git repo (gitignored). The pattern _tasks/ (trailing slash) ignores only a directory, not a SYMLINK named _tasks. A self-referential _tasks symlink can slip in via git add -A and, once pulled, checkout materializes it over the real _tasks repo (destroying plans/specs/hands-off). Anchored /_tasks ignores the symlink too, preventing re-capture. * feat(telegram): Mini App chat bridge — initData auth, update webhook, chat proxy Implements the Phase-1 slice of the Telegram Mini App integration (docs/proposals/TELEGRAM-MINIAPP.md): - src/lib/telegram/initData.ts — dependency-free WebApp initData HMAC-SHA256 verification (Telegram Bot API spec), with auth_date freshness check. - src/lib/telegram/config.ts — TELEGRAM_BOT_TOKEN / model / API base / timeout env config; token format validation; enabled gate. - src/lib/telegram/botApi.ts — minimal fetch-based Bot API client (sendMessage, editMessageText, setWebhook) + update shape helpers. - src/lib/telegram/chatProxy.ts — maps a Telegram user to a per-user OmniRoute API key (createApiKey, name telegram:<userId>) and proxies prompts through the existing handleChat pipeline. - src/app/api/telegram/update/route.ts — inbound endpoint serving both the Bot API update webhook (/start + chat replies) and the Mini App direct path (initData HMAC verified → 401 on mismatch). Public route prefix; own auth only. - src/app/miniapp/page.tsx — Telegram WebApp SDK chat UI. - Tests: telegram-init-data (7), telegram-botapi (5) — 12/12 pass. - Env docs: TELEGRAM_* vars in .env.example + ENVIRONMENT.md (sync ✓). - Route-validation check: PASS (body validated via Zod). --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> Co-authored-by: benzntech <bensonkbmca@gmail.com>
This commit is contained in:
committed by
GitHub
parent
3d590c310b
commit
0bb17b91c6
160
src/app/api/telegram/update/route.ts
Normal file
160
src/app/api/telegram/update/route.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Telegram Bot API update webhook + Mini App proxy.
|
||||
*
|
||||
* Two callers share this endpoint:
|
||||
* 1. Telegram POSTs bot updates here when the bot's webhook is registered
|
||||
* to {publicBase}/api/telegram/update (shape: TelegramUpdate).
|
||||
* 2. The Mini App frontend POSTs { initData, message } directly; the
|
||||
* initData HMAC is verified server-side before proxying.
|
||||
*
|
||||
* The route:
|
||||
* 1. Rejects when TELEGRAM_BOT_TOKEN is unset (never silently no-op).
|
||||
* 2. Verifies initData when present (Mini App path).
|
||||
* 3. Handles /start (returns the Mini App deep link) and everything else
|
||||
* as a chat prompt proxied through the OmniRoute pipeline.
|
||||
*/
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { validateBody, isValidationFailure } from "@/shared/validation/helpers";
|
||||
import type { TelegramUpdate } from "@/lib/telegram/botApi";
|
||||
import { extractChatMessage, sendTelegramMessage } from "@/lib/telegram/botApi";
|
||||
import { getTelegramBotToken, isTelegramEnabled } from "@/lib/telegram/config";
|
||||
import { verifyInitData, parseInitData } from "@/lib/telegram/initData";
|
||||
import { proxyChat } from "@/lib/telegram/chatProxy";
|
||||
import { resolveOmniRouteBaseUrl } from "@/shared/utils/resolveOmniRouteBaseUrl";
|
||||
|
||||
/**
|
||||
* Telegram update bodies are open-ended (many update types, evolving schema),
|
||||
* so validation is deliberately loose: a JSON object with optional string
|
||||
* fields for the two paths we handle. The initData HMAC check (Mini App path)
|
||||
* and bot-token gate (webhook path) provide the real security.
|
||||
*/
|
||||
const telegramBodySchema = z
|
||||
.object({
|
||||
initData: z.string().optional(),
|
||||
message: z.string().optional(),
|
||||
update_id: z.number().optional(),
|
||||
// allow unknown update fields
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
/** Pull the numeric Telegram user id out of a verified initData string. */
|
||||
function extractInitDataUserId(initData: string): number {
|
||||
try {
|
||||
const parsed = parseInitData(initData);
|
||||
const userRaw = parsed["user"];
|
||||
if (userRaw) {
|
||||
const user = JSON.parse(userRaw) as { id?: number };
|
||||
if (typeof user.id === "number" && user.id > 0) return user.id;
|
||||
}
|
||||
} catch {
|
||||
// fall through to default
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function buildMiniAppLink(botUsername?: string): string {
|
||||
const base = resolveOmniRouteBaseUrl();
|
||||
// Deep link: t.me/<bot>?startapp= opens the Mini App with start_param.
|
||||
const bot = botUsername || "YOUR_BOT";
|
||||
return `https://t.me/${bot}?startapp=miniapp`;
|
||||
}
|
||||
|
||||
const START_HELP =
|
||||
"👋 Welcome! This bot bridges Telegram and your OmniRoute gateway.\n\n" +
|
||||
"• Send any message and I'll route it through your configured models.\n" +
|
||||
"• Open the Mini App for a full chat UI.";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (!isTelegramEnabled()) {
|
||||
return NextResponse.json({ ok: false, error: "Telegram not configured" }, { status: 503 });
|
||||
}
|
||||
|
||||
let rawBody: unknown;
|
||||
try {
|
||||
rawBody = await request.json();
|
||||
} catch {
|
||||
return NextResponse.json({ ok: false, error: "Invalid JSON" }, { status: 400 });
|
||||
}
|
||||
|
||||
const validation = validateBody(telegramBodySchema, rawBody);
|
||||
if (isValidationFailure(validation)) {
|
||||
return NextResponse.json({ ok: false, error: "Invalid request" }, { status: 400 });
|
||||
}
|
||||
const body = validation.data as Record<string, unknown>;
|
||||
|
||||
// ── Mini App direct path: { initData, message } ──────────────────────────
|
||||
const initData = typeof body.initData === "string" ? body.initData : "";
|
||||
if (initData) {
|
||||
const botToken = getTelegramBotToken();
|
||||
if (!verifyInitData(initData, botToken)) {
|
||||
return NextResponse.json({ ok: false, error: "Invalid initData signature" }, { status: 401 });
|
||||
}
|
||||
const message = typeof body.message === "string" ? body.message : "";
|
||||
if (!message.trim()) {
|
||||
return NextResponse.json({ ok: false, error: "message is required" }, { status: 400 });
|
||||
}
|
||||
// Resolve the Telegram user id from the verified initData for key mapping.
|
||||
const telegramUserId = extractInitDataUserId(initData);
|
||||
// Proxy synchronously and return the reply (Mini App awaits the fetch).
|
||||
const reply = await proxyChat(telegramUserId, message);
|
||||
return NextResponse.json({ ok: true, reply: reply || "⚠️ Empty gateway response." });
|
||||
}
|
||||
|
||||
// ── Bot webhook path: TelegramUpdate ─────────────────────────────────────
|
||||
const update = body as unknown as TelegramUpdate;
|
||||
const chat = extractChatMessage(update);
|
||||
if (!chat) {
|
||||
// Non-message updates (callback_query etc.) — acknowledge silently.
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
// Fire-and-forget reply: Telegram retries on 5xx, so always 200 after
|
||||
// enqueueing the reply. Keep the handler non-blocking.
|
||||
void handleAndReply(chat.chatId, chat.text, chat.messageId);
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
async function handleAndReply(chatId: number, text: string, messageId?: number): Promise<void> {
|
||||
try {
|
||||
const trimmed = text.trim();
|
||||
if (trimmed === "/start" || trimmed === "/start@") {
|
||||
const link = buildMiniAppLink();
|
||||
await sendTelegramMessage({
|
||||
chat_id: chatId,
|
||||
text: `${START_HELP}\n\n🚀 Open the Mini App: ${link}`,
|
||||
parse_mode: "Markdown",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Strip bot-command prefixes that aren't /start (e.g. /help).
|
||||
if (trimmed.startsWith("/")) {
|
||||
await sendTelegramMessage({
|
||||
chat_id: chatId,
|
||||
text: "Unsupported command. Try /start or just send a message.",
|
||||
reply_to_message_id: messageId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const answer = await proxyChat(chatId, trimmed);
|
||||
const reply = answer || "⚠️ The gateway returned an empty response.";
|
||||
await sendTelegramMessage({
|
||||
chat_id: chatId,
|
||||
text: reply.length > 4096 ? `${reply.slice(0, 4090)}…` : reply,
|
||||
parse_mode: "Markdown",
|
||||
reply_to_message_id: messageId,
|
||||
});
|
||||
} catch (err) {
|
||||
try {
|
||||
await sendTelegramMessage({
|
||||
chat_id: chatId,
|
||||
text: `⚠️ Gateway error: ${(err as Error)?.message || "unknown"}`,
|
||||
});
|
||||
} catch {
|
||||
// Nothing more we can do — the reply channel is down.
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user