Files
clientsflow/frontend/server/utils/telegramBusinessConnect.ts

100 lines
2.7 KiB
TypeScript

import { randomBytes } from "node:crypto";
export type LinkTokenPayloadV1 = {
v: 1;
nonce: string;
exp: number;
};
const TOKEN_TTL_SEC = Number(process.env.TELEGRAM_LINK_TOKEN_TTL_SEC || 10 * 60);
export function requireBotUsername() {
const botUsername = String(process.env.TELEGRAM_BOT_USERNAME || "").trim().replace(/^@/, "");
if (!botUsername) {
throw createError({ statusCode: 500, statusMessage: "TELEGRAM_BOT_USERNAME is required" });
}
return botUsername;
}
export function issueLinkToken(input: { teamId: string; userId: string }) {
void input;
// Telegram deep-link `start` parameter is limited, keep token very short.
const token = randomBytes(12).toString("hex");
const payload: LinkTokenPayloadV1 = {
v: 1,
nonce: token,
exp: Math.floor(Date.now() / 1000) + Math.max(60, TOKEN_TTL_SEC),
};
return { token, payload };
}
export function verifyLinkToken(token: string): LinkTokenPayloadV1 | null {
const raw = String(token || "").trim();
if (!/^[a-f0-9]{24}$/.test(raw)) return null;
return {
v: 1,
nonce: raw,
exp: 0,
};
}
export function extractLinkTokenFromStartText(text: string) {
const trimmed = String(text || "").trim();
if (!trimmed.startsWith("/start")) return null;
const parts = trimmed.split(/\s+/).filter(Boolean);
if (parts.length < 2) return null;
const arg = parts[1] || "";
if (!arg.startsWith("link_")) return null;
return arg.slice("link_".length);
}
export function buildTelegramStartUrl(token: string) {
const botUsername = requireBotUsername();
return `https://t.me/${botUsername}?start=link_${token}`;
}
export function getTelegramChatIdFromUpdate(update: any): string | null {
const candidates = [
update?.message?.chat?.id,
update?.business_message?.chat?.id,
update?.edited_business_message?.chat?.id,
update?.business_connection?.user_chat_id,
];
for (const c of candidates) {
if (c == null) continue;
const v = String(c).trim();
if (v) return v;
}
return null;
}
export function getBusinessConnectionFromUpdate(update: any): {
id: string;
userChatId: string | null;
isEnabled: boolean | null;
canReply: boolean | null;
raw: any;
} | null {
const bc = (update?.business_connection ?? null) as any;
if (!bc || typeof bc !== "object") return null;
const id = String(bc.id ?? "").trim();
if (!id) return null;
const userChatId = bc.user_chat_id != null ? String(bc.user_chat_id) : null;
const isEnabled = typeof bc.is_enabled === "boolean" ? bc.is_enabled : null;
const canReply = typeof bc.can_reply === "boolean"
? bc.can_reply
: typeof bc.rights?.can_reply === "boolean"
? bc.rights.can_reply
: null;
return {
id,
userChatId,
isEnabled,
canReply,
raw: bc,
};
}