Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/openstatusHQ/openstatus. ๐ซ Status page with uptime monitoring & API monitoring as code ๐ซ openstatus.dev
bun drizzle-orm monitoring monitoring-as-code nextjs observability on-call open-source shadcn-ui status-page statuspage synthetic-monitoring tinybird turso uptime uptime-checker uptime-monitor
Something went wrong. Try again.
6.4 kB ยท 256 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257import type { redis } from "@openstatus/upstash";
// ---- Telegram API Types -----------------------------------------------------
interface TelegramUser { id: number; is_bot: boolean; first_name: string; username?: string; language_code?: string;}
interface TelegramChat { id: number; type: "private" | "group" | "supergroup" | "channel"; title?: string; first_name?: string; all_members_are_administrators?: boolean;}
interface TelegramMessage { message_id: number; from: TelegramUser; chat: TelegramChat; date: number; text?: string; entities?: Array<{ offset: number; length: number; type: string; }>; new_chat_members?: TelegramUser[]; new_chat_member?: TelegramUser; new_chat_participant?: TelegramUser;}
interface TelegramChatMember { user: TelegramUser; status: | "member" | "administrator" | "left" | "creator" | "restricted" | "kicked"; can_be_edited?: boolean; can_manage_chat?: boolean; can_change_info?: boolean; can_delete_messages?: boolean; can_invite_users?: boolean; can_restrict_members?: boolean; can_pin_messages?: boolean; can_manage_topics?: boolean; can_promote_members?: boolean; can_manage_video_chats?: boolean; can_post_stories?: boolean; can_edit_stories?: boolean; can_delete_stories?: boolean; is_anonymous?: boolean;}
interface TelegramMyChatMemberUpdate { chat: TelegramChat; from: TelegramUser; date: number; old_chat_member: TelegramChatMember; new_chat_member: TelegramChatMember;}
export interface TelegramUpdate { update_id: number; message?: TelegramMessage; my_chat_member?: TelegramMyChatMemberUpdate;}
export interface TelegramGetUpdatesResponse { ok: boolean; result: TelegramUpdate[];}
export type ValidUpdate = | { chatId: string; chatType: "private"; user: { id: number; first_name: string; username?: string }; } | { chatId: string; chatType: "group"; chatTitle?: string; user: { id: number; first_name: string; username?: string }; };
// ---- Helpers ----------------------------------------------------------------
function extractPrivateChatStart( update: TelegramUpdate, token: string,): { chatId: string; user: { id: number; first_name: string; username?: string };} | null { const { message } = update;
if ( !message || message.chat.type !== "private" || !message.text?.startsWith("/start ") || !message.from ) { return null; }
const receivedToken = message.text.split(" ")[1]; if (!receivedToken || receivedToken !== token) return null;
return { chatId: String(message.chat.id), user: { id: message.from.id, first_name: message.from.first_name, username: message.from.username, }, };}
function extractGroupBotAddition( update: TelegramUpdate, privateChatId: string, botUsername: string,): { chatId: string; chatTitle?: string; user: { id: number; first_name: string; username?: string };} | null { // Modern Telegram Bot API (v5.0+): bot additions come as my_chat_member updates if (update.my_chat_member) { const { my_chat_member } = update;
const isInvalidGroupAddition = (my_chat_member.chat.type !== "group" && my_chat_member.chat.type !== "supergroup") || String(my_chat_member.from.id) !== privateChatId || my_chat_member.new_chat_member.user.username !== botUsername || (my_chat_member.new_chat_member.status !== "member" && my_chat_member.new_chat_member.status !== "administrator");
if (isInvalidGroupAddition) { return null; }
return { chatId: String(my_chat_member.chat.id), chatTitle: my_chat_member.chat.title, user: { id: my_chat_member.from.id, first_name: my_chat_member.from.first_name, username: my_chat_member.from.username, }, }; }
// Legacy fallback: service message with new_chat_member fields const { message } = update;
const isInvalidGroupAddition = !message || (message.chat.type !== "group" && message.chat.type !== "supergroup") || !message.from || String(message.from.id) !== privateChatId;
if (isInvalidGroupAddition) { return null; }
const isBotAdded = message.new_chat_participant?.username === botUsername || message.new_chat_member?.username === botUsername || message.new_chat_members?.some((m) => m.username === botUsername);
if (!isBotAdded) return null;
return { chatId: String(message.chat.id), chatTitle: message.chat.title, user: { id: message.from.id, first_name: message.from.first_name, username: message.from.username, }, };}
// ---- Main logic -------------------------------------------------------------
export async function processTelegramUpdates(args: { updates: TelegramUpdate[]; workspaceId: number; privateChatId?: string; since?: number; botUsername: string; redisClient: typeof redis;}): Promise<ValidUpdate[]> { const { updates, workspaceId, privateChatId, since, botUsername, redisClient, } = args;
const validUpdates: ValidUpdate[] = [];
// 1. Pre-filter by timestamp const recentUpdates = since ? updates.filter((u) => { if (u.message) return u.message.date >= since; if (u.my_chat_member) return u.my_chat_member.date >= since; return false; }) : updates;
// 2. Phase 1: private /start (no privateChatId filter) if (!privateChatId) { const tokenKey = `telegram:workspace_token:${workspaceId}`; const storedToken = await redisClient.get<string>(tokenKey);
if (storedToken) { for (const update of recentUpdates) { const result = extractPrivateChatStart(update, storedToken); if (result) { // Single-use token: delete and stop await redisClient.del(tokenKey); validUpdates.push({ chatType: "private", ...result }); break; } } } } // 3. Phase 2: group/supergroup additions else { for (const update of recentUpdates) { const result = extractGroupBotAddition( update, privateChatId, botUsername, ); if (result) { validUpdates.push({ chatType: "group", ...result }); } } }
return validUpdates;}