Files
openhuman/scripts/mock-api/routes/telegram.mjs
T

212 lines
5.9 KiB
JavaScript

/**
* Mock Telegram Bot API route handler.
*
* Intercepts all requests matching `/bot<token>/<method>` and responds with
* realistic Telegram Bot API shapes. Behaviour is driven by the mock behavior
* keys documented below so E2E specs can configure failure modes without
* changing code.
*
* Behavior keys:
* telegramBotUsername - bot username returned by getMe (default: "e2e_test_bot")
* telegramBotToken - expected bot token; if set, mismatches return 401
* telegramPollDelayMs - delay before getUpdates returns (simulates long-poll)
* telegramGetMeFails - if "1", getMe returns HTTP 401 Unauthorized
* telegramSendFails - if "1", sendMessage returns HTTP 400 Bad Request
*/
import { json } from "../http.mjs";
import {
behavior,
drainMockTelegramUpdates,
nextMockTelegramMessageId,
recordMockTelegramSent,
sleep,
} from "../state.mjs";
/** Extract the bot token and method name from a Telegram Bot API path. */
const BOT_PATH_RE = /^\/bot([^/]+)\/([^/?]+)/;
/**
* Attempt to parse a Telegram Bot API path.
* Returns `{ token, method }` or `null` if the path does not match.
*/
function parseBotPath(url) {
const m = BOT_PATH_RE.exec(url);
if (!m) return null;
return { token: m[1], method: m[2] };
}
/**
* Main route handler. Returns `true` when the request was handled, `false`
* when it should fall through to the next handler.
*/
export async function handleTelegram(ctx) {
const { url, parsedBody, res } = ctx;
const parsed = parseBotPath(url);
if (!parsed) return false;
const { token, method } = parsed;
const b = behavior();
console.log(`[telegram-mock] ${method} token=${token.slice(0, 8)}...`);
// Optional token validation — only enforced when behavior key is set.
if (b.telegramBotToken && token !== b.telegramBotToken) {
console.warn(
`[telegram-mock] token mismatch: got ${token.slice(0, 8)}... expected ${b.telegramBotToken.slice(0, 8)}...`,
);
json(res, 401, {
ok: false,
error_code: 401,
description: "Unauthorized",
});
return true;
}
switch (method) {
case "getMe":
return handleGetMe(res, b);
case "getUpdates":
return handleGetUpdates(res, b);
case "sendMessage":
return handleSendMessage(res, b, parsedBody);
case "sendChatAction":
return handleSimpleRecord(res, "sendChatAction", parsedBody);
case "deleteWebhook":
return handleSimpleRecord(res, "deleteWebhook", parsedBody);
case "setMessageReaction":
return handleSimpleRecord(res, "setMessageReaction", parsedBody);
case "editMessageText":
return handleSimpleRecord(res, "editMessageText", parsedBody);
case "editMessageReplyMarkup":
return handleSimpleRecord(res, "editMessageReplyMarkup", parsedBody);
case "answerCallbackQuery":
return handleSimpleRecord(res, "answerCallbackQuery", parsedBody);
case "sendPhoto":
case "sendDocument":
case "sendVideo":
case "sendAudio":
case "sendVoice":
case "sendAnimation":
case "sendSticker": {
// Multipart bodies are not parsed — just record the method and any JSON
// keys we received (body may be null for multipart).
const messageId = nextMockTelegramMessageId();
recordMockTelegramSent({
method,
body: parsedBody ?? {},
message_id: messageId,
});
console.log(
`[telegram-mock] ${method} recorded message_id=${messageId}`,
);
json(res, 200, { ok: true, result: { message_id: messageId } });
return true;
}
default:
console.log(
`[telegram-mock] unhandled method="${method}" — returning ok:true, result:null`,
);
json(res, 200, { ok: true, result: null });
return true;
}
}
// ── Individual method handlers ─────────────────────────────────────────────
function handleGetMe(res, b) {
if (b.telegramGetMeFails === "1") {
console.warn("[telegram-mock] getMe failing per behavior.telegramGetMeFails");
json(res, 401, {
ok: false,
error_code: 401,
description: "Unauthorized",
});
return true;
}
const username = b.telegramBotUsername || "e2e_test_bot";
console.log(`[telegram-mock] getMe -> username=${username}`);
json(res, 200, {
ok: true,
result: {
id: 123456789,
is_bot: true,
first_name: "E2E Bot",
username,
},
});
return true;
}
async function handleGetUpdates(res, b) {
const delayMs = Math.min(
Number(b.telegramPollDelayMs) || 50,
30_000,
);
if (Number.isFinite(delayMs) && delayMs > 0) {
console.log(`[telegram-mock] getUpdates: waiting ${delayMs}ms before reply`);
await sleep(delayMs);
}
const updates = drainMockTelegramUpdates();
console.log(`[telegram-mock] getUpdates: returning ${updates.length} update(s)`);
json(res, 200, { ok: true, result: updates });
return true;
}
function handleSendMessage(res, b, body) {
if (b.telegramSendFails === "1") {
console.warn("[telegram-mock] sendMessage failing per behavior.telegramSendFails");
json(res, 400, {
ok: false,
error_code: 400,
description: "Bad Request",
});
return true;
}
const messageId = nextMockTelegramMessageId();
const chatId = body?.chat_id ?? 0;
const text = body?.text ?? "";
recordMockTelegramSent({
method: "sendMessage",
body: body ?? {},
message_id: messageId,
});
console.log(
`[telegram-mock] sendMessage chat_id=${chatId} message_id=${messageId} text="${String(text).slice(0, 80)}"`,
);
json(res, 200, {
ok: true,
result: {
message_id: messageId,
date: Math.floor(Date.now() / 1000),
chat: { id: chatId },
text,
},
});
return true;
}
function handleSimpleRecord(res, method, body) {
recordMockTelegramSent({ method, body: body ?? {} });
console.log(`[telegram-mock] ${method} recorded`);
json(res, 200, { ok: true, result: true });
return true;
}