Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 104 additions & 41 deletions lib/routes/telegram/tglib/channel.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable no-await-in-loop */
/* oxlint-disable no-await-in-loop */
import type { Context } from 'hono';
import { Api } from 'teleproto';
import { HTMLParser } from 'teleproto/extensions/html.js';
Expand All @@ -7,6 +8,7 @@ import { getDisplayName } from 'teleproto/Utils.js';

import type { DataItem } from '@/types';
import cache from '@/utils/cache';
import { parseDate } from '@/utils/parse-date';

import { getClient, getDocument, getFilename, unwrapMedia } from './client';

Expand Down Expand Up @@ -139,47 +141,70 @@ export function humanDuration(seconds: number) {
return `0:${paddedSeconds}`; // Show only seconds
}

export default async function handler(ctx: Context) {
const client = await getClient();
const username = ctx.req.param('username');
function groupMessages(messages: Api.Message[]): Api.Message[][] {
const groups: Api.Message[][] = [];
const groupMap = new Map<string, Api.Message[]>();

let peerCache = await cache.get(`telegram:inputEntity:${username}`);
if (!peerCache) {
const p = await client.getInputEntity(username!);
peerCache = JSON.stringify(p.toJSON());
await cache.set(`telegram:inputEntity:${username}`, peerCache);
for (const message of messages) {
const gid = message.groupedId ? String(message.groupedId) : null;
if (gid) {
const existing = groupMap.get(gid);
if (existing) {
existing.push(message);
continue;
}
const g = [message];
groupMap.set(gid, g);
groups.push(g);
} else {
groups.push([message]);
}
}
const peerData = JSON.parse(peerCache, (k, v) => (k === 'channelId' || k === 'accessHash' ? returnBigInt(v) : v));
const peer = new Api.InputPeerChannel(peerData);

const entity = await client.getEntity(peer);
return groups;
}

let attachments: string[] = [];
const messages = await client.getMessages(peer, { limit: 50 });
async function getForwardPrefix(client: any, msgs: Api.Message[]) {
const fwdMsg = msgs.find((m) => m.fwdFrom);
if (!fwdMsg?.fwdFrom) {
return '';
}
let fwdName = fwdMsg.fwdFrom.fromName;
if (fwdMsg.fwdFrom.fromId) {
try {
const fwdFrom = await client.getEntity(fwdMsg.fwdFrom.fromId);
fwdName = getDisplayName(fwdFrom);
} catch {
fwdName ||= 'Private Channel';
}
}
return fwdName ? `<p>Forwarded From <b>${fwdName}</b></p>` : '';
}

let i = 0;
const item: DataItem[] = [];
for (const message of messages) {
let text = message.text; // must not be HTML
async function getStoryPrefix(client: any, msgs: Api.Message[]) {
const storyMsg = msgs.find((m) => m.media instanceof Api.MessageMediaStory);
if (!storyMsg || !(storyMsg.media instanceof Api.MessageMediaStory)) {
return '';
}
let storyName = 'Private Peer';
try {
const storyFrom = await client.getEntity(storyMsg.media.peer);
storyName = getDisplayName(storyFrom);
} catch {
// Inaccessible story peer
}
return `<p>Story From <b>${storyName}</b></p>`;
}

if (message.fwdFrom?.fromId) {
const fwdFrom = await client.getEntity(message.fwdFrom.fromId);
text = `Forwarded From: ${getDisplayName(fwdFrom)}: ${text}`;
}
async function getMessageAttachments(client: any, ctx: Context, username: string, msgs: Api.Message[]) {
const attachments: string[] = [];
for (const message of msgs) {
const media = await unwrapMedia(message.media, message.peerId);
if (message.media instanceof Api.MessageMediaStory && media) {
// if successfully loaded the story
const storyFrom = await client.getEntity(message.media.peer);
text = `Story From: ${getDisplayName(storyFrom)}: ${text}`;
}
if (media) {
if (media instanceof Api.MessageMediaPoll) {
attachments.push(await getPollResults(client, message, media));
continue;
}
// messages that have no text are shown as if they're one post
// because in TG only 1 attachment per message is possible
const src = getMessageMediaUrl(ctx.req.url, username!, message.id, ctx.req.header('x-forwarded-prefix'));
const src = getMessageMediaUrl(ctx.req.url, username, message.id, ctx.req.header('x-forwarded-prefix'));
attachments.push(getMediaLink(src, media));
}
if (message.replyMarkup instanceof Api.ReplyInlineMarkup) {
Expand All @@ -191,23 +216,61 @@ export default async function handler(ctx: Context) {
}
}
}
if (text === '' && ++i !== messages.length - 1) {
continue;
}
let description = attachments.join('<br/>\n');
attachments = []; // emitting these, buffer other ones
}
return attachments;
}

export default async function handler(ctx: Context) {
const client = await getClient();
const username = ctx.req.param('username');

let peerCache = await cache.get(`telegram:inputEntity:${username}`);
if (!peerCache) {
const p = await client.getInputEntity(username!);
peerCache = JSON.stringify(p.toJSON());
await cache.set(`telegram:inputEntity:${username}`, peerCache);
}
const peerData = JSON.parse(peerCache, (k, v) => (k === 'channelId' || k === 'accessHash' ? returnBigInt(v) : v));
const peer = new Api.InputPeerChannel(peerData);

const entity = await client.getEntity(peer);

const messages = await client.getMessages(peer, { limit: 50 });
const groups = groupMessages(messages);

if (text) {
description += `<p>${HTMLParser.unparse(message.message, message.entities).replaceAll('\n', '<br/>')}</p>`;
const item: DataItem[] = [];
for (const msgs of groups) {
// Sort album messages by ID ascending to preserve original media order
msgs.sort((a, b) => a.id - b.id);
const primaryMsg = msgs[0];
const textMsg = msgs.find((m) => m.text) || primaryMsg;

const fwdPrefix = await getForwardPrefix(client, msgs);
const storyPrefix = await getStoryPrefix(client, msgs);
const attachments = await getMessageAttachments(client, ctx, username!, msgs);

let description = attachments.join('<br/>\n');
if (fwdPrefix) {
description += fwdPrefix;
}
if (storyPrefix) {
description += storyPrefix;
}
if (textMsg.text) {
description += `<p>${HTMLParser.unparse(textMsg.message, textMsg.entities).replaceAll('\n', '<br/>')}</p>`;
}

const pubDate = parseDate(primaryMsg.date * 1000);
const title = textMsg.text || pubDate.toUTCString();
const postLink = `https://t.me/${username}/${primaryMsg.id}`;

const title = message.text ? message.text.slice(0, 80) + (message.text.length > 80 ? '...' : '') : new Date(message.date * 1000).toUTCString();
item.push({
title,
description,
pubDate: new Date(message.date * 1000).toUTCString(),
link: `https://t.me/s/${username}/${message.id}`,
author: getDisplayName(message.sender ?? entity),
pubDate,
link: postLink,
guid: postLink,
author: getDisplayName(textMsg.sender ?? entity),
});
}

Expand Down
Loading