Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/telegram

src/get-updates.ts

136 lines · 3.8 KB · TypeScript
import { telegramRequest } from './request.ts'
import {
	boundedLimit,
	inputRecord,
	optionalInteger,
	secretNameFrom,
} from './validation.ts'

type TelegramChat = {
	id?: number
	type?: string
	title?: string
	username?: string
	first_name?: string
	[key: string]: unknown
}

type TelegramMessage = {
	message_id?: number
	date?: number
	text?: string
	caption?: string
	chat?: TelegramChat
	from?: { id?: number; username?: string; first_name?: string; is_bot?: boolean }
	[key: string]: unknown
}

type TelegramUpdate = {
	update_id?: number
	message?: TelegramMessage
	edited_message?: TelegramMessage
	channel_post?: TelegramMessage
	callback_query?: {
		id?: string
		data?: string
		from?: { id?: number; username?: string }
		message?: TelegramMessage
	}
	[key: string]: unknown
}

function summarizeChat(chat: TelegramChat | undefined) {
	if (!chat || typeof chat !== 'object') return null
	return {
		id: typeof chat.id === 'number' ? chat.id : null,
		type: typeof chat.type === 'string' ? chat.type : null,
		title: typeof chat.title === 'string' ? chat.title : null,
		username: typeof chat.username === 'string' ? chat.username : null,
		firstName: typeof chat.first_name === 'string' ? chat.first_name : null,
	}
}

function summarizeMessage(message: TelegramMessage | undefined) {
	if (!message || typeof message !== 'object') return null
	return {
		messageId: typeof message.message_id === 'number' ? message.message_id : null,
		date: typeof message.date === 'number' ? message.date : null,
		text: typeof message.text === 'string' ? message.text : null,
		caption: typeof message.caption === 'string' ? message.caption : null,
		chat: summarizeChat(message.chat),
		fromId: typeof message.from?.id === 'number' ? message.from.id : null,
		fromUsername:
			typeof message.from?.username === 'string' ? message.from.username : null,
	}
}

function summarizeUpdate(update: TelegramUpdate) {
	const kind = update.message
		? 'message'
		: update.edited_message
			? 'edited_message'
			: update.channel_post
				? 'channel_post'
				: update.callback_query
					? 'callback_query'
					: 'other'
	return {
		updateId: typeof update.update_id === 'number' ? update.update_id : null,
		kind,
		message: summarizeMessage(
			update.message ?? update.edited_message ?? update.channel_post,
		),
		callbackQueryId:
			typeof update.callback_query?.id === 'string'
				? update.callback_query.id
				: null,
		callbackData:
			typeof update.callback_query?.data === 'string'
				? update.callback_query.data
				: null,
	}
}

/**
 * Read pending Bot API updates (polling). Bots cannot fetch arbitrary chat
 * history; they only see messages delivered via getUpdates or a webhook.
 * Passing offset acknowledges earlier updates.
 */
export default async function getUpdates(params: Record<string, unknown> = {}) {
	const input = inputRecord(params)
	const secretName = secretNameFrom(input)
	const allowedUpdates = input.allowedUpdates
	if (
		allowedUpdates !== undefined &&
		(!Array.isArray(allowedUpdates) ||
			allowedUpdates.some((item) => typeof item !== 'string'))
	) {
		throw new Error('allowedUpdates must be an array of strings when provided.')
	}

	const result = await telegramRequest<TelegramUpdate[]>({
		method: 'getUpdates',
		secretName,
		params: {
			offset: optionalInteger(input, 'offset'),
			limit: boundedLimit(input.limit, 20, 100),
			timeout: optionalInteger(input, 'timeout') ?? 0,
			allowed_updates: allowedUpdates,
		},
	})

	const updates = Array.isArray(result) ? result.map(summarizeUpdate) : []
	const lastUpdateId = updates.reduce((max, update) => {
		if (typeof update.updateId === 'number' && update.updateId > max) {
			return update.updateId
		}
		return max
	}, 0)

	return {
		ok: true,
		count: updates.length,
		nextOffset: lastUpdateId > 0 ? lastUpdateId + 1 : null,
		updates,
	}
}