fetch-telegram.mjs 3.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
  1. // Fetches the public Telegram channel feed (t.me/s/...) — the whole first page
  2. // chain via ?before=<id> pagination — and saves the posts for the home-page
  3. // Telegram widget. Run manually: npm run fetch-news
  4. import * as cheerio from "cheerio";
  5. import { mkdir, writeFile } from "node:fs/promises";
  6. import path from "node:path";
  7. const CHANNEL = "scientia_ru";
  8. const MAX_PAGES = 40; // safety cap (~20 posts per page)
  9. const UA =
  10. "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36";
  11. const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
  12. const posts = [];
  13. const seen = new Set();
  14. let before = null;
  15. for (let page = 0; page < MAX_PAGES; page++) {
  16. const url = `https://t.me/s/${CHANNEL}${before ? `?before=${before}` : ""}`;
  17. const res = await fetch(url, { headers: { "User-Agent": UA } });
  18. if (!res.ok) {
  19. console.error(`page ${page}: HTTP ${res.status}`);
  20. break;
  21. }
  22. const $ = cheerio.load(await res.text());
  23. let newCount = 0;
  24. let oldestId = null;
  25. $(".tgme_widget_message").each((_, el) => {
  26. const $el = $(el);
  27. const postId = ($el.attr("data-post") || "").split("/")[1];
  28. if (!postId || seen.has(postId)) return;
  29. seen.add(postId);
  30. newCount++;
  31. oldestId = postId;
  32. const textNode = $el.find(".tgme_widget_message_text").first();
  33. // Keep basic formatting tags, drop telegram-internal attributes
  34. textNode.find("a, b, i, u, s, strong, em, code, br, blockquote").each((__, keep) => {
  35. const attrs = $(keep).attr() || {};
  36. for (const name of Object.keys(attrs)) {
  37. if (name !== "href") $(keep).removeAttr(name);
  38. }
  39. });
  40. const text = textNode.html()?.trim() || "";
  41. const datetime = $el.find("time").first().attr("datetime") || "";
  42. const photoStyle = $el.find(".tgme_widget_message_photo_wrap").first().attr("style") || "";
  43. let photo = photoStyle.match(/url\('([^']+)'\)/)?.[1]?.split("?")[0] || "";
  44. const video = $el.find(".tgme_widget_message_video_wrap, .tgme_widget_message_video_player").length > 0;
  45. // direct mp4 (tokenized URL — expires after a while; re-run fetch-news to refresh)
  46. const videoSrc = video ? $el.find("video").first().attr("src") || "" : "";
  47. // video posts carry a full-size preview frame in .tgme_widget_message_video_thumb
  48. if (video && !photo) {
  49. const thumbStyle = $el.find(".tgme_widget_message_video_thumb").first().attr("style") || "";
  50. photo = thumbStyle.match(/url\('([^']+)'\)/)?.[1] || "";
  51. }
  52. const link = $el.find(".tgme_widget_message_date").first().attr("href") || `https://t.me/${CHANNEL}/${postId}`;
  53. if (text || photo || video) {
  54. posts.push({ id: Number(postId), text, datetime, photo, video, videoSrc, link });
  55. }
  56. });
  57. console.log(`page ${page}: +${newCount} posts (total kept ${posts.length})`);
  58. if (newCount === 0 || !oldestId) break;
  59. before = oldestId;
  60. await sleep(300);
  61. }
  62. // newest first
  63. posts.sort((a, b) => b.id - a.id);
  64. await mkdir(path.dirname("src/data/telegram-news.json"), { recursive: true });
  65. await writeFile("src/data/telegram-news.json", JSON.stringify(posts, null, 2));
  66. console.log(`saved ${posts.length} posts`);
  67. // Channel avatar for the widget header
  68. const avatar = await fetch(`https://t.me/i/userpic/320/${CHANNEL}.jpg`, { headers: { "User-Agent": UA } });
  69. if (avatar.ok) {
  70. const buf = Buffer.from(await avatar.arrayBuffer());
  71. if (buf.length > 500) {
  72. await writeFile("public/images/telegram-avatar.jpg", buf);
  73. console.log("avatar saved");
  74. } else {
  75. console.log("avatar looks empty, skipped");
  76. }
  77. } else {
  78. console.log(`avatar fetch failed: HTTP ${avatar.status}`);
  79. }