diff --git a/src/__tests__/bot-commands.test.ts b/src/__tests__/bot-commands.test.ts index e2720eb..0bda3ad 100644 --- a/src/__tests__/bot-commands.test.ts +++ b/src/__tests__/bot-commands.test.ts @@ -22,18 +22,6 @@ describe("parseCommand", () => { it("rejects /launch without slug", () => { expect(parseCommand("/launch").tag).toBe("launch-missing"); }); - it("parses /lang without arg as lang-help", () => { - expect(parseCommand("/lang")).toEqual({ tag: "lang-help" }); - }); - it("parses /lang en", () => { - expect(parseCommand("/lang en")).toEqual({ tag: "lang-set", locale: "en" }); - }); - it("parses /lang ru", () => { - expect(parseCommand("/lang ru")).toEqual({ tag: "lang-set", locale: "ru" }); - }); - it("parses unknown /lang code as lang-invalid", () => { - expect(parseCommand("/lang de")).toEqual({ tag: "lang-invalid", arg: "de" }); - }); it("parses /getToken", () => { expect(parseCommand("/getToken")).toEqual({ tag: "get-token" }); }); @@ -50,28 +38,8 @@ describe("parseCommand", () => { }); describe("bot messages", () => { - it("createdMessage (ru) includes bootstrap steps without links", () => { - const message = createdMessage( - "ru", - "demo-app", - "npx @spawn-dock/create --token pair_demo", - ); - - expect(message).toContain("Проект demo-app создан."); - expect(message).toContain("1. Запусти bootstrap-команду локально:"); - expect(message).toContain("npx @spawn-dock/create --token pair_demo"); - expect(message).toContain("Эту команду можно запускать повторно для этого проекта."); - expect(message).toContain("2. После bootstrap запусти:"); - expect(message).toContain("pnpm run dev"); - expect(message).not.toContain("Preview URL:"); - expect(message).not.toContain("Telegram Link:"); - expect(message).not.toContain("TMA URL:"); - expect(message).not.toContain("Ссылки:"); - }); - - it("createdMessage (en) uses English bootstrap copy", () => { + it("createdMessage uses English bootstrap copy", () => { const message = createdMessage( - "en", "demo-app", "npx @spawn-dock/create --token pair_demo", ); @@ -86,7 +54,6 @@ describe("bot messages", () => { it("includes TMA, preview, and telegram links in launchMessage", () => { const message = launchMessage( - "en", "demo-app", "connected", "https://example.com/tma?tgWebAppStartParam=demo-app", @@ -103,7 +70,6 @@ describe("bot messages", () => { it("previewReadyMessage keeps all links", () => { const message = previewReadyMessage( - "ru", "demo-app", "https://example.com/preview/demo-app", "https://t.me/rustgpt_bot/tma?startapp=demo-app", diff --git a/src/__tests__/bot-telegram.test.ts b/src/__tests__/bot-telegram.test.ts index a7c1f99..33287e1 100644 --- a/src/__tests__/bot-telegram.test.ts +++ b/src/__tests__/bot-telegram.test.ts @@ -71,6 +71,8 @@ describe("setMyCommands", () => { for (const cmd of body.commands) { expect(cmd.command).toMatch(/^[a-z0-9_]+$/); } - expect(body.commands.map((c) => c.command)).toContain("gettoken"); + const commandNames = body.commands.map((c) => c.command); + expect(commandNames).toContain("gettoken"); + expect(commandNames).not.toContain("lang"); }); }); diff --git a/src/__tests__/mcp-auth.test.ts b/src/__tests__/mcp-auth.test.ts index 3db819c..4871ad2 100644 --- a/src/__tests__/mcp-auth.test.ts +++ b/src/__tests__/mcp-auth.test.ts @@ -17,7 +17,6 @@ const state: StoreState = { revokedAt: null, }], tunnelSessions: [], - userLocales: {}, }; describe("readMcpApiKey", () => { diff --git a/src/__tests__/server.test.ts b/src/__tests__/server.test.ts index 3b49f57..8650b1c 100644 --- a/src/__tests__/server.test.ts +++ b/src/__tests__/server.test.ts @@ -27,7 +27,7 @@ process.env.TELEGRAM_MINI_APP_SHORT_NAME ??= "tma"; function createRuntime(): Runtime { return { - state: { projects: [], pairingTokens: [], deviceCredentials: [], tunnelSessions: [], userLocales: {} }, + state: { projects: [], pairingTokens: [], deviceCredentials: [], tunnelSessions: [] }, connectionsBySlug: new Map(), pendingResponses: new Map(), }; @@ -49,7 +49,6 @@ function createAuthorizedRuntime(): Runtime { revokedAt: null, }], tunnelSessions: [], - userLocales: {}, }, }; } @@ -59,7 +58,7 @@ const authorizedHeaders = { }; const mockRuntime: Runtime = { - state: { projects: [], pairingTokens: [], deviceCredentials: [], tunnelSessions: [], userLocales: {} }, + state: { projects: [], pairingTokens: [], deviceCredentials: [], tunnelSessions: [] }, connectionsBySlug: new Map(), pendingResponses: new Map(), }; @@ -116,7 +115,7 @@ describe("Express server", () => { it("POST /v1/bootstrap/claim returns flat bootstrap fields", async () => { const app = createApp({ - state: { projects: [], pairingTokens: [], deviceCredentials: [], tunnelSessions: [], userLocales: {} }, + state: { projects: [], pairingTokens: [], deviceCredentials: [], tunnelSessions: [] }, connectionsBySlug: new Map(), pendingResponses: new Map(), }); @@ -172,7 +171,7 @@ describe("Express server", () => { it("POST /v1/bootstrap/claim replays the same credential when the token was already claimed", async () => { const app = createApp({ - state: { projects: [], pairingTokens: [], deviceCredentials: [], tunnelSessions: [], userLocales: {} }, + state: { projects: [], pairingTokens: [], deviceCredentials: [], tunnelSessions: [] }, connectionsBySlug: new Map(), pendingResponses: new Map(), }); @@ -209,35 +208,6 @@ describe("Express server", () => { expect(res.body.error).toBe("bot_unauthorized"); }); - it("POST /api/bot/user-locale/sync stores locale from Telegram language_code", async () => { - const runtime = createRuntime(); - const app = createApp(runtime); - - const res = await request(app) - .post("/api/bot/user-locale/sync") - .set(botHeaders) - .send({ ownerTelegramId: 99, telegramLanguageCode: "ru-RU" }); - - expect(res.status).toBe(200); - expect(res.body.locale).toBe("ru"); - expect(runtime.state.userLocales["99"]).toBe("ru"); - }); - - it("POST /api/bot/user-locale/set updates explicit locale", async () => { - const runtime = createRuntime(); - runtime.state.userLocales["5"] = "ru"; - const app = createApp(runtime); - - const res = await request(app) - .post("/api/bot/user-locale/set") - .set(botHeaders) - .send({ ownerTelegramId: 5, locale: "en" }); - - expect(res.status).toBe(200); - expect(res.body.locale).toBe("en"); - expect(runtime.state.userLocales["5"]).toBe("en"); - }); - it("POST /projects accepts bot-authorized creation without /api prefix", async () => { const app = createApp(createRuntime()); @@ -314,7 +284,6 @@ describe("Express server", () => { pairingTokens: [], deviceCredentials: [], tunnelSessions: [], - userLocales: {}, }, connectionsBySlug: new Map(), pendingResponses: new Map(), diff --git a/src/bot/commands.ts b/src/bot/commands.ts index 9de0ba5..c8fa9d2 100644 --- a/src/bot/commands.ts +++ b/src/bot/commands.ts @@ -1,16 +1,10 @@ // src/bot/commands.ts -import type { BotLocale } from "../types.js"; -import { isBotLocale } from "./user-lang.js"; - export type BotCommand = | { tag: "start" } | { tag: "help" } | { tag: "new"; title: string } | { tag: "launch"; slug: string } | { tag: "launch-missing" } - | { tag: "lang-help" } - | { tag: "lang-set"; locale: BotLocale } - | { tag: "lang-invalid"; arg: string } | { tag: "get-token" } | { tag: "unknown"; input: string }; @@ -30,12 +24,6 @@ export function parseCommand(input: string): BotCommand { const text = normalizeTelegramCommandText(input); if (text === "/start") return { tag: "start" }; if (text === "/help") return { tag: "help" }; - if (text.startsWith("/lang")) { - const rest = text.slice(5).trim().toLowerCase(); - if (!rest) return { tag: "lang-help" }; - if (isBotLocale(rest)) return { tag: "lang-set", locale: rest }; - return { tag: "lang-invalid", arg: rest }; - } if (text.startsWith("/new")) { const title = text.slice(4).trim(); return { tag: "new", title: title || "SpawnDock App" }; diff --git a/src/bot/i18n.ts b/src/bot/i18n.ts index ca2a870..47e9efb 100644 --- a/src/bot/i18n.ts +++ b/src/bot/i18n.ts @@ -1,6 +1,4 @@ // src/bot/i18n.ts -import type { BotLocale } from "../types.js"; - const esc = (s: string) => s.replace(/&/g, "&").replace(//g, ">"); const escAttr = (s: string) => s .replace(/&/g, "&") @@ -12,18 +10,11 @@ function renderLink(url: string): string { return `${esc(url)}`; } -export function linkButtonLabels(locale: BotLocale): { +export function linkButtonLabels(): { openTma: string; openPreview: string; openTelegramLink: string; } { - if (locale === "ru") { - return { - openTma: "Открыть TMA", - openPreview: "Открыть Preview", - openTelegramLink: "Открыть Telegram Link", - }; - } return { openTma: "Open TMA", openPreview: "Open Preview", @@ -31,19 +22,7 @@ export function linkButtonLabels(locale: BotLocale): { }; } -export function welcomeMessage(locale: BotLocale): string { - if (locale === "ru") { - return [ - "SpawnDock бот готов.", - "", - "Команды:", - "/new <название проекта> — создать проект и получить ссылку (или команду) для spawn-dock", - "/launch <slug> — получить TMA и preview ссылки проекта", - "/gettoken — показать общий API_TOKEN для MCP и dev tunnel", - "/lang <en|ru> — язык интерфейса", - "/help — показать справку", - ].join("\n"); - } +export function welcomeMessage(): string { return [ "SpawnDock bot is ready.", "", @@ -51,26 +30,13 @@ export function welcomeMessage(locale: BotLocale): string { "/new <project title> — create a project and get the spawn-dock link (or command)", "/launch <slug> — get TMA and preview links for the project", "/gettoken — show shared API_TOKEN for MCP and dev tunnel", - "/lang <en|ru> — interface language", "/help — show this help", ].join("\n"); } -export function getTokenMessage(locale: BotLocale, token: string): string { +export function getTokenMessage(token: string): string { if (token.length === 0) { - return locale === "ru" - ? "API_TOKEN не настроен на сервере." - : "API_TOKEN is not configured on the server."; - } - - if (locale === "ru") { - return [ - "Общий токен для MCP и dev tunnel:", - `${esc(token)}`, - "", - "Пример:", - "export API_TOKEN=...", - ].join("\n"); + return "API_TOKEN is not configured on the server."; } return [ @@ -82,38 +48,18 @@ export function getTokenMessage(locale: BotLocale, token: string): string { ].join("\n"); } -export function unknownMessage(locale: BotLocale): string { - if (locale === "ru") { - return "Не понял команду.\n\nИспользуй /new, /launch, /gettoken или /help."; - } +export function unknownMessage(): string { return "I did not understand that command.\n\nUse /new, /launch, /gettoken, or /help."; } -export function launchUsageMessage(locale: BotLocale): string { - if (locale === "ru") { - return "Используй /launch <slug>, чтобы получить preview URL и текущий статус туннеля."; - } +export function launchUsageMessage(): string { return "Use /launch <slug> to get the preview URL and current tunnel status."; } export function createdMessage( - locale: BotLocale, slug: string, bootstrapCmd: string, ): string { - if (locale === "ru") { - return [ - `Проект ${esc(slug)} создан.`, - "", - "1. Запусти bootstrap-команду локально:", - `${esc(bootstrapCmd)}`, - "", - "Эту команду можно запускать повторно для этого проекта.", - "", - "2. После bootstrap запусти:", - "pnpm run dev", - ].join("\n"); - } return [ `Project ${esc(slug)} created.`, "", @@ -128,20 +74,17 @@ export function createdMessage( } export function launchMessage( - locale: BotLocale, slug: string, status: "connected" | "offline", tmaUrl: string, previewUrl: string, telegramMiniAppUrl: string, ): string { - const statusLabel = locale === "ru" ? "Статус туннеля" : "Tunnel status"; - const offlineHint = locale === "ru" - ? "Если preview офлайн, запусти npm run agent в каталоге проекта." - : "If preview is offline, run npm run agent in the project directory."; + const statusLabel = "Tunnel status"; + const offlineHint = "If preview is offline, run npm run agent in the project directory."; return [ - locale === "ru" ? `Проект ${esc(slug)}.` : `Project ${esc(slug)}.`, + `Project ${esc(slug)}.`, "", `${statusLabel}: ${esc(status)}`, `TMA URL: ${renderLink(tmaUrl)}`, @@ -152,87 +95,32 @@ export function launchMessage( ].join("\n"); } -export function ackCreatingProject(locale: BotLocale): string { - return locale === "ru" - ? "Создаю проект и готовлю ссылку на рабочее окружение..." - : "Creating the project and preparing the workspace link..."; -} - -export function ackCheckingLaunch(locale: BotLocale): string { - return locale === "ru" - ? "Проверяю текущий статус проекта..." - : "Checking the current project status..."; -} - -export function errCreateProject(locale: BotLocale): string { - return locale === "ru" - ? "Не удалось создать проект. Убедись, что сервер запущен, и попробуй ещё раз." - : "Could not create the project. Make sure the server is running and try again."; +export function ackCreatingProject(): string { + return "Creating the project and preparing the workspace link..."; } -export function errLaunchNotFound(locale: BotLocale): string { - return locale === "ru" - ? "Проект не найден. Проверь slug и попробуй ещё раз." - : "Project not found. Check the slug and try again."; +export function ackCheckingLaunch(): string { + return "Checking the current project status..."; } -export function errLaunchGeneric(locale: BotLocale): string { - return locale === "ru" - ? "Не удалось получить статус проекта. Попробуй позже." - : "Could not load project status. Try again later."; +export function errCreateProject(): string { + return "Could not create the project. Make sure the server is running and try again."; } -export function errLocaleSet(locale: BotLocale): string { - return locale === "ru" - ? "Не удалось сохранить язык. Попробуй позже." - : "Could not save language preference. Try again later."; +export function errLaunchNotFound(): string { + return "Project not found. Check the slug and try again."; } -export function langHelpMessage(locale: BotLocale): string { - if (locale === "ru") { - return [ - "Текущий язык можно сменить так:", - "/lang en — English", - "/lang ru — русский", - ].join("\n"); - } - return [ - "Change language with:", - "/lang en — English", - "/lang ru — русский", - ].join("\n"); -} - -export function langSetConfirm(selectedLocale: BotLocale): string { - if (selectedLocale === "ru") { - return "Язык установлен: русский."; - } - return "Language set to English."; -} - -export function langInvalid(locale: BotLocale, arg: string): string { - if (locale === "ru") { - return `Неизвестный язык «${esc(arg)}». Доступны: en, ru.`; - } - return `Unknown language «${esc(arg)}». Supported: en, ru.`; +export function errLaunchGeneric(): string { + return "Could not load project status. Try again later."; } export function previewReadyMessage( - locale: BotLocale, slug: string, previewUrl: string, telegramMiniAppUrl: string, tmaUrl: string, ): string { - if (locale === "ru") { - return [ - `Проект ${esc(slug)}: локальный сервер подключён, превью доступно.`, - "", - `Preview URL: ${renderLink(previewUrl)}`, - `Telegram Link: ${renderLink(telegramMiniAppUrl)}`, - `TMA URL: ${renderLink(tmaUrl)}`, - ].join("\n"); - } return [ `Project ${esc(slug)}: local server is connected; preview is available.`, "", diff --git a/src/bot/polling.ts b/src/bot/polling.ts index 7756251..6c8bf07 100644 --- a/src/bot/polling.ts +++ b/src/bot/polling.ts @@ -1,5 +1,4 @@ // src/bot/polling.ts -import type { BotLocale } from "../types.js"; import { readBotConfig } from "./config.js"; import { getUpdates, sendMessage, deleteWebhook, setMyCommands, type TelegramUpdate } from "./telegram.js"; import { @@ -15,11 +14,7 @@ import { ackCreatingProject, errCreateProject, errLaunchGeneric, - errLocaleSet, errLaunchNotFound, - langHelpMessage, - langInvalid, - langSetConfirm, linkButtonLabels, getTokenMessage, } from "./i18n.js"; @@ -55,104 +50,30 @@ async function getLaunchInfo(controlPlaneUrl: string, botSecret: string, ownerTe return (await res.json()) as any; } -async function syncUserLocale( - controlPlaneUrl: string, - botSecret: string, - ownerTelegramId: number, - telegramLanguageCode?: string, -): Promise { - const res = await fetch(`${controlPlaneUrl}/api/bot/user-locale/sync`, { - method: "POST", - headers: { - "content-type": "application/json", - "x-spawndock-bot-secret": botSecret, - }, - body: JSON.stringify({ ownerTelegramId, telegramLanguageCode }), - }); - if (!res.ok) throw new Error(`Locale sync failed: ${res.status}`); - const data = (await res.json()) as { locale: BotLocale }; - return data.locale; -} - -async function setUserLocale( - controlPlaneUrl: string, - botSecret: string, - ownerTelegramId: number, - locale: BotLocale, -): Promise { - const res = await fetch(`${controlPlaneUrl}/api/bot/user-locale/set`, { - method: "POST", - headers: { - "content-type": "application/json", - "x-spawndock-bot-secret": botSecret, - }, - body: JSON.stringify({ ownerTelegramId, locale }), - }); - if (!res.ok) throw new Error(`Locale set failed: ${res.status}`); - const data = (await res.json()) as { locale: BotLocale }; - return data.locale; -} - -async function resolveLocale( - cfg: ReturnType, - from: { id: number; language_code?: string }, -): Promise { - try { - return await syncUserLocale(cfg.controlPlaneUrl, cfg.controlPlaneBotSecret, from.id, from.language_code); - } catch { - return "en"; - } -} - async function processUpdate(cfg: ReturnType, update: TelegramUpdate): Promise { const msg = update.message; if (!msg?.text || !msg.from) return; const cmd = parseCommand(msg.text); - if (cmd.tag === "lang-set") { - let selected: BotLocale; - try { - selected = await setUserLocale(cfg.controlPlaneUrl, cfg.controlPlaneBotSecret, msg.from.id, cmd.locale); - } catch { - const locale = await resolveLocale(cfg, msg.from); - await sendMessage(cfg.telegramBotToken, msg.chat.id, errLocaleSet(locale)); - return; - } - await sendMessage(cfg.telegramBotToken, msg.chat.id, langSetConfirm(selected)); - return; - } - - const locale = await resolveLocale(cfg, msg.from); - - if (cmd.tag === "lang-help") { - await sendMessage(cfg.telegramBotToken, msg.chat.id, langHelpMessage(locale)); - return; - } - - if (cmd.tag === "lang-invalid") { - await sendMessage(cfg.telegramBotToken, msg.chat.id, langInvalid(locale, cmd.arg)); - return; - } - if (cmd.tag === "start" || cmd.tag === "help") { - await sendMessage(cfg.telegramBotToken, msg.chat.id, welcomeMessage(locale)); + await sendMessage(cfg.telegramBotToken, msg.chat.id, welcomeMessage()); return; } if (cmd.tag === "get-token") { - await sendMessage(cfg.telegramBotToken, msg.chat.id, getTokenMessage(locale, cfg.apiToken)); + await sendMessage(cfg.telegramBotToken, msg.chat.id, getTokenMessage(cfg.apiToken)); return; } if (cmd.tag === "new") { let data: any; - const ackTimeout = scheduleAcknowledgement(cfg, msg.chat.id, ackCreatingProject(locale)); + const ackTimeout = scheduleAcknowledgement(cfg, msg.chat.id, ackCreatingProject()); try { data = await createProjectViaApi(cfg.controlPlaneUrl, cfg.controlPlaneBotSecret, msg.from.id, cmd.title); } catch (err: any) { clearTimeout(ackTimeout); - await sendMessage(cfg.telegramBotToken, msg.chat.id, errCreateProject(locale)); + await sendMessage(cfg.telegramBotToken, msg.chat.id, errCreateProject()); return; } clearTimeout(ackTimeout); @@ -162,45 +83,45 @@ async function processUpdate(cfg: ReturnType, update: Tele await sendMessage( cfg.telegramBotToken, msg.chat.id, - createdMessage(locale, slug, bootstrapCmd), + createdMessage(slug, bootstrapCmd), ); return; } if (cmd.tag === "launch-missing") { - await sendMessage(cfg.telegramBotToken, msg.chat.id, launchUsageMessage(locale)); + await sendMessage(cfg.telegramBotToken, msg.chat.id, launchUsageMessage()); return; } if (cmd.tag === "launch") { let data: any; - const ackTimeout = scheduleAcknowledgement(cfg, msg.chat.id, ackCheckingLaunch(locale)); + const ackTimeout = scheduleAcknowledgement(cfg, msg.chat.id, ackCheckingLaunch()); try { data = await getLaunchInfo(cfg.controlPlaneUrl, cfg.controlPlaneBotSecret, msg.from.id, cmd.slug); } catch (err: any) { clearTimeout(ackTimeout); if (String(err.message).includes("404")) { - await sendMessage(cfg.telegramBotToken, msg.chat.id, errLaunchNotFound(locale)); + await sendMessage(cfg.telegramBotToken, msg.chat.id, errLaunchNotFound()); return; } - await sendMessage(cfg.telegramBotToken, msg.chat.id, errLaunchGeneric(locale)); + await sendMessage(cfg.telegramBotToken, msg.chat.id, errLaunchGeneric()); return; } clearTimeout(ackTimeout); const previewUrl = data.launchUrl; const tmaUrl = buildGatewayMiniAppUrl(previewUrl, cmd.slug); const telegramMiniAppUrl = buildTelegramMiniAppUrl(cfg.telegramBotUsername, cfg.telegramMiniAppShortName, cmd.slug); - const buttons = linkButtonLabels(locale); + const buttons = linkButtonLabels(); await sendMessage( cfg.telegramBotToken, msg.chat.id, - launchMessage(locale, cmd.slug, data.status, tmaUrl, previewUrl, telegramMiniAppUrl), + launchMessage(cmd.slug, data.status, tmaUrl, previewUrl, telegramMiniAppUrl), { tmaUrl, previewUrl, telegramMiniAppUrl, buttonLabels: buttons }, ); return; } - await sendMessage(cfg.telegramBotToken, msg.chat.id, unknownMessage(locale)); + await sendMessage(cfg.telegramBotToken, msg.chat.id, unknownMessage()); } function scheduleAcknowledgement( diff --git a/src/bot/preview-notify.ts b/src/bot/preview-notify.ts index 32facbe..713071c 100644 --- a/src/bot/preview-notify.ts +++ b/src/bot/preview-notify.ts @@ -1,18 +1,15 @@ // src/bot/preview-notify.ts -import type { StoreState } from "../types.js"; import { buildLaunchUrl } from "../projects.js"; import { buildGatewayMiniAppUrl, buildTelegramMiniAppUrl } from "./links.js"; import { sendMessage } from "./telegram.js"; import { config } from "../config.js"; -import { getUserLocale } from "./user-lang.js"; -import { linkButtonLabels, previewReadyMessage } from "./i18n.js"; +import { previewReadyMessage } from "./i18n.js"; /** * When the control plane has Telegram credentials, notify the project owner that preview is reachable. * Best-effort: failures are logged only. */ export async function notifyOwnerPreviewReady(input: { - state: StoreState; ownerTelegramId: number; slug: string; }): Promise { @@ -23,17 +20,15 @@ export async function notifyOwnerPreviewReady(input: { return; } - const locale = getUserLocale(input.state, input.ownerTelegramId); const previewUrl = buildLaunchUrl(config.publicOrigin, input.slug, config.previewPrefix); const tmaUrl = buildGatewayMiniAppUrl(previewUrl, input.slug); const telegramMiniAppUrl = buildTelegramMiniAppUrl(botUsername, miniAppShortName, input.slug); - const html = previewReadyMessage(locale, input.slug, previewUrl, telegramMiniAppUrl, tmaUrl); + const html = previewReadyMessage(input.slug, previewUrl, telegramMiniAppUrl, tmaUrl); await sendMessage(token, input.ownerTelegramId, html, { tmaUrl, previewUrl, telegramMiniAppUrl, - buttonLabels: linkButtonLabels(locale), }); } diff --git a/src/bot/telegram.ts b/src/bot/telegram.ts index 0c4f66c..412a188 100644 --- a/src/bot/telegram.ts +++ b/src/bot/telegram.ts @@ -207,7 +207,6 @@ export async function setMyCommands(token: string): Promise { { command: "new", description: "Create TMA and get bootstrap command" }, { command: "launch", description: "Get TMA and preview links for a project" }, { command: "gettoken", description: "Shared API token for MCP and dev tunnel" }, - { command: "lang", description: "Set language (en or ru)" }, { command: "help", description: "Show help" }, ], }), diff --git a/src/bot/user-lang.ts b/src/bot/user-lang.ts deleted file mode 100644 index 4e054e0..0000000 --- a/src/bot/user-lang.ts +++ /dev/null @@ -1,50 +0,0 @@ -// src/bot/user-lang.ts -import type { BotLocale, StoreState } from "../types.js"; - -export const SUPPORTED_BOT_LOCALES: readonly BotLocale[] = ["en", "ru"]; - -export function detectLocaleFromTelegram(languageCode?: string): BotLocale { - if (!languageCode) return "en"; - const normalized = languageCode.toLowerCase().split("-")[0]?.trim() ?? ""; - return normalized === "ru" ? "ru" : "en"; -} - -export function isBotLocale(value: string): value is BotLocale { - return value === "en" || value === "ru"; -} - -export function getUserLocale(state: StoreState, telegramUserId: number): BotLocale { - return state.userLocales[String(telegramUserId)] ?? "en"; -} - -export function ensureUserLocale( - state: StoreState, - telegramUserId: number, - telegramLanguageCode?: string, -): { state: StoreState; locale: BotLocale; changed: boolean } { - const key = String(telegramUserId); - const existing = state.userLocales[key]; - if (existing) { - return { state, locale: existing, changed: false }; - } - const locale = detectLocaleFromTelegram(telegramLanguageCode); - return { - state: { - ...state, - userLocales: { ...state.userLocales, [key]: locale }, - }, - locale, - changed: true, - }; -} - -export function setUserLocaleInState( - state: StoreState, - telegramUserId: number, - locale: BotLocale, -): StoreState { - return { - ...state, - userLocales: { ...state.userLocales, [String(telegramUserId)]: locale }, - }; -} diff --git a/src/routes/bot-user.ts b/src/routes/bot-user.ts deleted file mode 100644 index 94fdfb1..0000000 --- a/src/routes/bot-user.ts +++ /dev/null @@ -1,64 +0,0 @@ -// src/routes/bot-user.ts -import { Router, type RequestHandler } from "express"; -import type { BotLocale, Runtime } from "../types.js"; -import { saveState } from "../store.js"; -import { isAuthorizedBotRequest } from "./bot-auth.js"; -import { ensureUserLocale, isBotLocale, setUserLocaleInState } from "../bot/user-lang.js"; - -export function botUserRoutes(runtime: Runtime, stateFilePath: string): Router { - const router = Router(); - - const handleSync: RequestHandler = (req, res) => { - if (!isAuthorizedBotRequest(req.headers)) { - res.status(401).json({ error: "bot_unauthorized" }); - return; - } - - const { ownerTelegramId, telegramLanguageCode } = req.body ?? {}; - if (typeof ownerTelegramId !== "number" || !Number.isFinite(ownerTelegramId)) { - res.status(400).json({ error: "ownerTelegramId is required" }); - return; - } - - const telegramLang = - typeof telegramLanguageCode === "string" && telegramLanguageCode.length > 0 - ? telegramLanguageCode - : undefined; - - const result = ensureUserLocale(runtime.state, ownerTelegramId, telegramLang); - if (result.changed) { - runtime.state = result.state; - saveState(stateFilePath, runtime.state); - } - - res.json({ locale: result.locale }); - }; - - const handleSet: RequestHandler = (req, res) => { - if (!isAuthorizedBotRequest(req.headers)) { - res.status(401).json({ error: "bot_unauthorized" }); - return; - } - - const { ownerTelegramId, locale } = req.body ?? {}; - if (typeof ownerTelegramId !== "number" || !Number.isFinite(ownerTelegramId)) { - res.status(400).json({ error: "ownerTelegramId is required" }); - return; - } - - if (typeof locale !== "string" || !isBotLocale(locale)) { - res.status(400).json({ error: "locale must be en or ru" }); - return; - } - - runtime.state = setUserLocaleInState(runtime.state, ownerTelegramId, locale as BotLocale); - saveState(stateFilePath, runtime.state); - - res.json({ locale }); - }; - - router.post("/api/bot/user-locale/sync", handleSync); - router.post("/api/bot/user-locale/set", handleSet); - - return router; -} diff --git a/src/server.ts b/src/server.ts index 18310c9..89213e1 100644 --- a/src/server.ts +++ b/src/server.ts @@ -10,7 +10,6 @@ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; import { createMcpServer } from "./mcp.js"; import { config } from "./config.js"; import { projectRoutes } from "./routes/projects.js"; -import { botUserRoutes } from "./routes/bot-user.js"; import { previewRoutes } from "./routes/preview.js"; import { tmaRoutes } from "./routes/tma.js"; import { authenticateMcpRequest } from "./mcp-auth.js"; @@ -140,7 +139,6 @@ export function createApp(runtime: Runtime): Application { return next(); }); app.use(projectRoutes(runtime, config.stateFile)); - app.use(botUserRoutes(runtime, config.stateFile)); app.use(tmaRoutes()); // Preview proxy — MUST be mounted WITHOUT express.json() to read raw body stream diff --git a/src/store.ts b/src/store.ts index 5734609..e50368b 100644 --- a/src/store.ts +++ b/src/store.ts @@ -8,17 +8,18 @@ export const emptyState = (): StoreState => ({ pairingTokens: [], deviceCredentials: [], tunnelSessions: [], - userLocales: {}, }); export function loadState(filePath: string): StoreState { try { const data = readFileSync(filePath, "utf-8"); - const parsed = JSON.parse(data) as StoreState; - if (!parsed.userLocales || typeof parsed.userLocales !== "object") { - parsed.userLocales = {}; - } - return parsed; + const parsed = JSON.parse(data) as Partial & Record; + return { + projects: Array.isArray(parsed.projects) ? parsed.projects : [], + pairingTokens: Array.isArray(parsed.pairingTokens) ? parsed.pairingTokens : [], + deviceCredentials: Array.isArray(parsed.deviceCredentials) ? parsed.deviceCredentials : [], + tunnelSessions: Array.isArray(parsed.tunnelSessions) ? parsed.tunnelSessions : [], + }; } catch { return emptyState(); } diff --git a/src/tunnel.ts b/src/tunnel.ts index c6600cd..6946e06 100644 --- a/src/tunnel.ts +++ b/src/tunnel.ts @@ -139,7 +139,6 @@ function handleTunnelConnection( if (!previousConnection && result.project.ownerTelegramId != null) { void notifyOwnerPreviewReady({ - state: runtime.state, ownerTelegramId: result.project.ownerTelegramId, slug: result.project.slug, }).catch((err) => { diff --git a/src/types.ts b/src/types.ts index 56e5e47..763b2ec 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,4 @@ // src/types.ts -export type BotLocale = "en" | "ru"; - export type ProjectStatus = "draft" | "active"; export type TunnelStatus = "connected" | "disconnected"; @@ -48,8 +46,6 @@ export interface StoreState { pairingTokens: PairingToken[]; deviceCredentials: DeviceCredential[]; tunnelSessions: TunnelSession[]; - /** Telegram user id (string) → preferred bot locale */ - userLocales: Record; } export interface Runtime {