Show "Forwarded from" attribution on forwarded Telegram messages
Mirrors the repost-line treatment tweets already get. GramJS resolves a forward's origin channel/user from entities Telegram sends alongside the same getMessages response (message.forward.chat/.sender), falling back to fwdFrom.fromName for the rarer case where the origin hid its identity. TelegramCard shows "↪️ Forwarded from @username" (or just the name if no public handle) above the meta row. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014c1L8ghNBFjfiH64UMViP8
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { Source, ContentItem, TweetMediaItem, TelegramMediaRef } from '../../storage/db/types.js';
|
||||
import type { Source, ContentItem, TweetMediaItem, TelegramMediaRef, TelegramForwardedFrom } from '../../storage/db/types.js';
|
||||
import { cleanHtml, toSummary } from '../clean.js';
|
||||
|
||||
export interface FetchedItem {
|
||||
@@ -17,6 +17,7 @@ export interface FetchedItem {
|
||||
channelUsername: string;
|
||||
messageId: string;
|
||||
media: TelegramMediaRef[];
|
||||
forwardedFrom: TelegramForwardedFrom | null;
|
||||
};
|
||||
raw: unknown;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
// where those refs turn into an actual servable url.
|
||||
|
||||
import type { Api } from 'telegram';
|
||||
import type { Source, TelegramMediaRef } from '../../storage/db/types.js';
|
||||
import type { Source, TelegramMediaRef, TelegramForwardedFrom } from '../../storage/db/types.js';
|
||||
import type { SourceAdapter, FetchedItem } from './base.js';
|
||||
import { logger } from '../../storage/db/logs.js';
|
||||
import { getClient, fetchChannelMessages } from '../../telegram/client.js';
|
||||
@@ -69,6 +69,30 @@ function refForMessage(message: TgMessage): TelegramMediaRef | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects a forwarded message and resolves where it came from. GramJS's `message.forward`
|
||||
* wraps the raw fwdFrom header using entities Telegram already sent alongside the same
|
||||
* getMessages response (the whole point of that header is letting clients show forward
|
||||
* attribution without a separate resolve call) — `.chat` is the origin channel/group,
|
||||
* `.sender` the origin user. Falls back to fwdFrom.fromName for the rarer case where the
|
||||
* origin has no resolvable identity (e.g. a user who hid their account from forwards).
|
||||
*/
|
||||
function detectForward(message: TgMessage): TelegramForwardedFrom | null {
|
||||
const fwd = message.fwdFrom;
|
||||
if (!fwd) return null;
|
||||
|
||||
const chat = message.forward?.chat as { title?: string; username?: string } | undefined;
|
||||
if (chat) return { name: chat.title ?? chat.username ?? 'Unknown', username: chat.username ?? null };
|
||||
|
||||
const sender = message.forward?.sender as { firstName?: string; lastName?: string; username?: string } | undefined;
|
||||
if (sender) {
|
||||
const name = [sender.firstName, sender.lastName].filter(Boolean).join(' ') || sender.username || 'Unknown';
|
||||
return { name, username: sender.username ?? null };
|
||||
}
|
||||
|
||||
return { name: fwd.fromName ?? 'Unknown', username: null };
|
||||
}
|
||||
|
||||
/** Groups consecutive messages sharing a non-null groupedId (Telegram's multi-photo/video "album" concept) into one entry each. */
|
||||
function groupMessages(messages: TgMessage[]): TgMessage[][] {
|
||||
const order: string[] = [];
|
||||
@@ -144,7 +168,8 @@ export const telegramAdapter: SourceAdapter = {
|
||||
channelName,
|
||||
channelUsername,
|
||||
messageId: String(group[0].id),
|
||||
media
|
||||
media,
|
||||
forwardedFrom: detectForward(primary)
|
||||
},
|
||||
raw: { messageIds: group.map((m) => m.id) }
|
||||
});
|
||||
|
||||
@@ -263,7 +263,8 @@ export async function publishDirect(item: ContentItem, settings: GlobalSettings)
|
||||
channelUsername,
|
||||
channelAvatarUrl: avatar.url,
|
||||
sourceItemId: item.id,
|
||||
media: resolvedMedia.media
|
||||
media: resolvedMedia.media,
|
||||
forwardedFrom: item.telegramMessage.forwardedFrom
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,12 @@ export interface TelegramMediaRef {
|
||||
height: number | null;
|
||||
}
|
||||
|
||||
/** Where a forwarded Telegram message originated — username is null when the origin has no public handle (e.g. a private channel/user, or a sender who hid their identity), in which case the card falls back to showing just the name. */
|
||||
export interface TelegramForwardedFrom {
|
||||
name: string;
|
||||
username: string | null;
|
||||
}
|
||||
|
||||
export interface ContentItem {
|
||||
id: string;
|
||||
sourceId: string;
|
||||
@@ -70,6 +76,7 @@ export interface ContentItem {
|
||||
channelUsername: string;
|
||||
messageId: string;
|
||||
media: TelegramMediaRef[];
|
||||
forwardedFrom: TelegramForwardedFrom | null;
|
||||
} | null;
|
||||
raw: unknown;
|
||||
}
|
||||
@@ -96,6 +103,7 @@ export interface MergedArticle {
|
||||
channelAvatarUrl: string | null;
|
||||
sourceItemId: string;
|
||||
media: TelegramMediaItem[];
|
||||
forwardedFrom: TelegramForwardedFrom | null;
|
||||
} | null;
|
||||
category: string[];
|
||||
geo: string | null;
|
||||
|
||||
@@ -28,6 +28,10 @@
|
||||
</script>
|
||||
|
||||
<a class="telegram-card" href={messageUrl} target="_blank" rel="noreferrer">
|
||||
{#if article.telegramMessage?.forwardedFrom}
|
||||
{@const from = article.telegramMessage.forwardedFrom}
|
||||
<div class="forward-line">↪️ Forwarded from {from.username ? `@${from.username}` : from.name}</div>
|
||||
{/if}
|
||||
<div class="meta">
|
||||
<span>{article.category[0] ?? ''}</span>
|
||||
<span>·</span>
|
||||
@@ -88,6 +92,12 @@
|
||||
text-decoration: none;
|
||||
border-color: var(--border-accent);
|
||||
}
|
||||
.forward-line {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.meta {
|
||||
font-size: 11px;
|
||||
color: var(--text-accent);
|
||||
|
||||
@@ -20,6 +20,12 @@ export interface TweetMediaItem {
|
||||
/** Same shape as TweetMediaItem — distinct name for readability at Telegram call sites. */
|
||||
export type TelegramMediaItem = TweetMediaItem;
|
||||
|
||||
/** Where a forwarded Telegram message originated — username is null when the origin has no public handle, in which case TelegramCard falls back to showing just the name. */
|
||||
export interface TelegramForwardedFrom {
|
||||
name: string;
|
||||
username: string | null;
|
||||
}
|
||||
|
||||
export interface MergedArticle {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -33,6 +39,7 @@ export interface MergedArticle {
|
||||
channelAvatarUrl: string | null;
|
||||
sourceItemId: string;
|
||||
media: TelegramMediaItem[];
|
||||
forwardedFrom: TelegramForwardedFrom | null;
|
||||
} | null;
|
||||
category: string[];
|
||||
geo: string | null;
|
||||
|
||||
Reference in New Issue
Block a user