Skip to content

feat(telegram-intel): add custom channel watchlists#2262

Open
lspassos1 wants to merge 2 commits intokoala73:mainfrom
lspassos1:feat/telegram-custom-watchlist
Open

feat(telegram-intel): add custom channel watchlists#2262
lspassos1 wants to merge 2 commits intokoala73:mainfrom
lspassos1:feat/telegram-custom-watchlist

Conversation

@lspassos1
Copy link
Copy Markdown
Collaborator

Summary

Adds a user-defined Telegram watchlist to Telegram Intel so any public channel can be added by @username, persisted locally, previewed before save, and merged into the existing curated feed.

Root cause

Telegram Intel only exposed the product-managed relay feed. There was no way for a user to resolve a public channel, persist it in personal settings, or fetch that channel's recent posts alongside the curated OSINT stream.

Changes

  • add local watchlist persistence in telegram:watchlist:v1 with normalization and change events
  • add telegram-resolve and telegram-channel edge handlers plus matching relay endpoints
  • resolve public channels via Telegram getEntity() and fetch member counts through GetFullChannel
  • fetch watchlist channel posts and merge them into the panel with removable pills and preview/add flow
  • add focused tests for watchlist persistence, service caching, edge handler validation, and the legacy endpoint allowlist

Validation

  • npx tsx --test tests/telegram-watchlist.test.mts tests/telegram-intel-service.test.mts tests/telegram-edge-handlers.test.mjs tests/edge-functions.test.mjs
  • npm run typecheck

Risk

Low to moderate. The change stays on the existing Telegram relay path, but it introduces new on-demand relay routes that depend on the configured Telegram session remaining valid.

Closes #1994

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 25, 2026

@lspassos1 is attempting to deploy a commit to the Elie Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 25, 2026

Greptile Summary

This PR adds a user-defined Telegram watchlist to the Telegram Intel panel, allowing users to resolve any public channel by @username, persist up to 20 entries in localStorage under telegram:watchlist:v1, preview channels before adding, and merge watchlist posts into the existing curated OSINT feed. It introduces two new Vercel Edge Functions (api/telegram-resolve.js, api/telegram-channel.js) that proxy to matching relay endpoints (/telegram/resolve, /telegram/channel) in scripts/ais-relay.cjs, where on-demand channel resolution and message fetching are implemented with TTL-scoped in-process caches.

Key findings:

  • P1 regression in setData error handling — the original guard if (!this.relayEnabled || response.error) was narrowed to if (!this.relayEnabled), so any error returned on the main feed (relay timeout, 5xx, empty response) is silently discarded. The base items are set to [] and syncWatchlistFeed runs without surfacing an error message to the user.
  • Semantically inverted ChatFull instanceof guard (scripts/ais-relay.cjs line 527) — GetFullChannel always returns ChannelFull, never ChatFull, so !(... instanceof ChatFull) is vacuously true and the guard provides no real protection. The check should test instanceof ChannelFull directly.
  • The architecture is clean and well-layered: telegram-watchlist.ts is a pure persistence/pub-sub module with no UI coupling; telegram-intel.ts adds new fetch helpers with in-flight deduplication; the panel wires them together with request-ID cancellation. Test coverage spans all new layers.

Confidence Score: 4/5

  • Safe to merge after restoring the response.error branch in setData; all other concerns are style-level.
  • The feature is well-structured, properly tested, and stays within the existing relay path. One concrete P1 regression (silent base-feed error swallowing) needs a one-line fix before merge; the remaining finding (ChatFull instanceof) is a style/clarity issue with no functional impact in practice.
  • src/components/TelegramIntelPanel.ts (setData error-handling regression) and scripts/ais-relay.cjs (ChatFull instanceof guard)

Important Files Changed

Filename Overview
src/components/TelegramIntelPanel.ts Adds watchlist input, preview/add flow, pill removal, and feed merging. P1 regression: response.error is no longer checked in setData, silently swallowing base-feed errors.
scripts/ais-relay.cjs Adds /telegram/resolve and /telegram/channel relay endpoints with in-process caching; Api namespace wired for member-count fetching. ChatFull instanceof guard is semantically inverted (P2 style).
src/services/telegram-watchlist.ts New service: localStorage persistence with normalization, 20-entry cap, dedup, and CustomEvent pub/sub. Well structured and fully tested.
src/services/telegram-intel.ts Adds fetchTelegramChannelPreview and fetchTelegramChannelFeed with in-flight deduplication and TTL caching; switches desktop path from proxyUrl to toRuntimeUrl.
api/telegram-channel.js New edge handler: validates username regex, clamps limit (1–50), then forwards to relay. CORS, method guards, and origin check all present.
api/telegram-resolve.js New edge handler: validates username regex and forwards to relay with aggressive CDN cache headers (24h s-maxage). Follows same pattern as telegram-channel.js.
tests/telegram-watchlist.test.mts Good coverage of normalization, dedup persistence, and pub/sub lifecycle using a lightweight localStorage mock.
tests/telegram-edge-handlers.test.mjs Tests invalid-username rejection, relay forwarding, cache headers, and limit clamping for both new edge handlers.
tests/telegram-intel-service.test.mts Tests client-side caching deduplication and watchlist metadata injection (watchlist flag, osint topic default).
src/locales/en.json Adds 9 new i18n keys for the watchlist UI; mirrored in pt.json with correct translations.
src/styles/main.css Adds ~130 lines of CSS for watchlist controls, preview card, pills, and custom tag badge; all scoped to new class names.

Sequence Diagram

sequenceDiagram
    participant User
    participant TelegramIntelPanel
    participant telegram-intel service
    participant EdgeFn as api/telegram-resolve<br/>api/telegram-channel
    participant Relay as ais-relay.cjs
    participant TG as Telegram API

    User->>TelegramIntelPanel: types @username in input
    TelegramIntelPanel->>TelegramIntelPanel: debounce 800ms
    TelegramIntelPanel->>telegram-intel service: fetchTelegramChannelPreview(username)
    telegram-intel service->>EdgeFn: GET /api/telegram-resolve?username=
    EdgeFn->>Relay: GET /telegram/resolve?username=
    Relay->>TG: getEntity(username)
    TG-->>Relay: entity
    Relay->>TG: GetFullChannel (member count)
    TG-->>Relay: ChannelFull
    Relay-->>EdgeFn: { username, title, memberCount, url }
    EdgeFn-->>telegram-intel service: preview (cached 24h CDN)
    telegram-intel service-->>TelegramIntelPanel: TelegramChannelPreview
    TelegramIntelPanel->>TelegramIntelPanel: renderPreview() – show Add button

    User->>TelegramIntelPanel: clicks Add / presses Enter
    TelegramIntelPanel->>telegram-watchlist: addTelegramWatchlistEntry()
    telegram-watchlist->>telegram-watchlist: persist to localStorage
    telegram-watchlist->>TelegramIntelPanel: dispatchEvent(wm-telegram-watchlist-changed)
    TelegramIntelPanel->>TelegramIntelPanel: renderWatchlistPills()
    TelegramIntelPanel->>telegram-intel service: fetchTelegramChannelFeed(username)
    telegram-intel service->>EdgeFn: GET /api/telegram-channel?username=&limit=
    EdgeFn->>Relay: GET /telegram/channel?username=&limit=
    Relay->>TG: getMessages(entity, limit)
    TG-->>Relay: messages[]
    Relay-->>EdgeFn: TelegramFeedResponse (cached 60s)
    EdgeFn-->>telegram-intel service: feed items
    telegram-intel service-->>TelegramIntelPanel: items with watchlist=true
    TelegramIntelPanel->>TelegramIntelPanel: mergeTelegramItems(watchlist, base) → renderItems()
Loading

Reviews (1): Last reviewed commit: "feat(telegram-intel): add custom channel..." | Re-trigger Greptile

Comment on lines 148 to 157
@@ -68,13 +156,193 @@ export class TelegramIntelPanel extends Panel {
return;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Base-feed errors are silently swallowed

The original guard was if (!this.relayEnabled || response.error), which surfaced the error message to the user whenever the relay returned an error (even while enabled is still true). The new guard only checks !this.relayEnabled, so any transient fetch error on the curated feed (e.g., relay timeout, 5xx, partial response) now falls through silently: baseItems is set to [], syncWatchlistFeed runs, and the user sees an empty panel with no indication that the main feed failed.

Consider restoring the response.error branch:

Suggested change
if (!this.relayEnabled || response.error) {
this.watchlistItems = [];
this.setCount(0);
replaceChildren(this.content,
h('div', { className: 'empty-state error' },
response.error || t('components.telegramIntel.disabled')
),
);
return;
}

Comment on lines +527 to +529
if (!(full?.fullChat instanceof telegramState.api.ChatFull)) {
memberCount = full?.fullChat?.participantsCount ?? null;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Semantically inverted ChatFull instanceof check

GetFullChannel always returns a ChannelFull, never a ChatFull. The condition !(... instanceof ChatFull) is therefore always true, so participantsCount is unconditionally extracted — the guard does nothing useful. If a future call ever yields an unexpected response type, the ?? null fallback rescues the value silently.

A clearer and more defensive form would be:

Suggested change
if (!(full?.fullChat instanceof telegramState.api.ChatFull)) {
memberCount = full?.fullChat?.participantsCount ?? null;
}
if (full?.fullChat instanceof telegramState.api.ChannelFull) {
memberCount = full.fullChat.participantsCount ?? null;
}

This way the intent is explicit, type-safe, and won't accidentally pick up participantsCount from an unrelated payload type.

@lspassos1
Copy link
Copy Markdown
Collaborator Author

Addressed the latest review feedback in 802f5bc7:

  • restored the response.error branch in TelegramIntelPanel.setData() so curated-feed failures still surface an error state
  • tightened the relay member-count guard to check ChannelFull explicitly after GetFullChannel

Validation rerun:

  • npx tsx --test tests/telegram-watchlist.test.mts tests/telegram-intel-service.test.mts tests/telegram-edge-handlers.test.mjs tests/edge-functions.test.mjs
  • npm run typecheck

@koala73 koala73 added High Value Meaningful contribution to the project Ready to Merge PR is mergeable, passes checks, and adds value labels Mar 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

High Value Meaningful contribution to the project Ready to Merge PR is mergeable, passes checks, and adds value

Projects

None yet

Development

Successfully merging this pull request may close these issues.

user-defined channel watchlist — add any public Telegram channel by @username

2 participants