import { $fetch } from 'ofetch' import * as cheerio from 'cheerio' import { LRUCache } from 'lru-cache' import { getEnv } from '../env' const cache = new LRUCache({ ttl: 1000 * 60 * 5, // 5 minutes maxSize: 50 * 1024 * 1024, // 50MB sizeCalculation: (item) => { return JSON.stringify(item).length }, }) function getImages($, item, { staticProxy, id, index, title }) { return $(item).find('.tgme_widget_message_photo_wrap')?.map((_index, photo) => { const url = $(photo).attr('style').match(/url\(["'](.*?)["']/)?.[1] const popoverId = `modal-${id}-${_index}` return ` ` })?.get()?.join('') } function getVideo($, item, { staticProxy, index }) { const video = $(item).find('.tgme_widget_message_video_wrap video') video?.attr('src', staticProxy + video?.attr('src')) ?.attr('controls', true) ?.attr('preload', index > 15 ? 'auto' : 'metadata') ?.attr('playsinline', true).attr('webkit-playsinline', true) const roundVideo = $(item).find('.tgme_widget_message_roundvideo_wrap video') roundVideo?.attr('src', staticProxy + roundVideo?.attr('src')) ?.attr('controls', true) ?.attr('preload', index > 15 ? 'auto' : 'metadata') ?.attr('playsinline', true).attr('webkit-playsinline', true) return $.html(video) + $.html(roundVideo) } function getLinkPreview($, item, { staticProxy, index }) { const link = $(item).find('.tgme_widget_message_link_preview') const title = $(item).find('.link_preview_title')?.text() || $(item).find('.link_preview_site_name')?.text() const description = $(item).find('.link_preview_description')?.text() link?.attr('target', '_blank').attr('rel', 'noopener').attr('title', description) const image = $(item).find('.link_preview_image') const src = image?.attr('style')?.match(/url\(["'](.*?)["']/i)?.[1] const imageSrc = src ? staticProxy + src : '' image?.replaceWith(`${title}`) return $.html(link) } function getPost($, item, { channel, staticProxy, index = 0 }) { item = item ? $(item).find('.tgme_widget_message') : $('.tgme_widget_message') const content = $(item).find('.tgme_widget_message_bubble > .tgme_widget_message_text') const title = content?.text()?.match(/[^。\n]*(?=[。\n]|http)/g)?.[0] ?? content?.text() ?? '' const id = $(item).attr('data-post')?.replace(`${channel}/`, '') $(content).find('a')?.each((_index, a) => { $(a)?.attr('title', $(a)?.text()) }) $(content).find('.emoji')?.attr('style', '') const tags = $(content).find('a[href^="?q="]')?.each((_index, a) => { $(a)?.attr('href', `/search/${encodeURIComponent($(a)?.text())}`) })?.map((_index, a) => $(a)?.text()?.replace('#', ''))?.get() return { id, title, type: $(item).attr('class')?.includes('service_message') ? 'service' : 'text', datetime: $(item).find('.tgme_widget_message_date time')?.attr('datetime'), tags, text: content?.text(), content: [ $.html($(item).find('.tgme_widget_message_reply')?.wrapInner('')?.wrapInner('
')), getImages($, item, { staticProxy, id, index, title }), getVideo($, item, { staticProxy, id, index, title }), content?.html(), // $(item).find('.tgme_widget_message_sticker_wrap')?.html(), $(item).find('.tgme_widget_message_poll')?.html(), $.html($(item).find('.tgme_widget_message_document_wrap')), $.html($(item).find('.tgme_widget_message_voice')?.attr('controls', true)), $.html($(item).find('.tgme_widget_message_location_wrap')), getLinkPreview($, item, { staticProxy, index }), ].filter(Boolean).join('').replace(/(url\(["'])((https?:)?\/\/)/g, (match, p1, p2, _p3) => { if (p2 === '//') { p2 = 'https://' } if (p2?.startsWith('t.me')) { return false } return `${p1}${staticProxy}${p2}` }), } } const unnessaryHeaders = ['host', 'cookie', 'origin', 'referer'] export async function getChannelInfo(Astro, { before = '', after = '', q = '', type = 'list', id = '' } = {}) { const cacheKey = JSON.stringify({ before, after, q, type, id }) const cachedResult = cache.get(cacheKey) if (cachedResult) { console.info('Macth Cache', { before, after, q, type, id }) return JSON.parse(JSON.stringify(cachedResult)) } // Where t.me can also be telegram.me, telegram.dog const host = getEnv(import.meta.env, Astro, 'HOST') ?? 't.me' const channel = getEnv(import.meta.env, Astro, 'CHANNEL') const staticProxy = getEnv(import.meta.env, Astro, 'STATIC_PROXY') ?? '/static/' const url = id ? `https://${host}/${channel}/${id}?embed=1&mode=tme` : `https://${host}/s/${channel}` const headers = Object.fromEntries(Astro.request.headers) Object.keys(headers).forEach((key) => { if (unnessaryHeaders.includes(key)) { delete headers[key] } }) console.info('Fetching', url, { before, after, q, type, id }) const html = await $fetch(url, { headers, query: { before: before || undefined, after: after || undefined, q: q || undefined, }, retry: 3, retryDelay: 100, }) const $ = cheerio.load(html, {}, false) if (id) { const post = getPost($, null, { channel, staticProxy }) cache.set(cacheKey, post) return post } const posts = $('.tgme_channel_history .tgme_widget_message_wrap')?.map((index, item) => { return getPost($, item, { channel, staticProxy, index }) })?.get()?.reverse().filter(post => ['text'].includes(post.type) && post.id && post.content) const channelInfo = { posts, title: $('.tgme_channel_info_header_title')?.text(), description: $('.tgme_channel_info_description')?.text(), avatar: $('.tgme_page_photo_image img')?.attr('src'), } cache.set(cacheKey, channelInfo) return channelInfo }