@easyoref/gramjs 1.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,26 @@
1
+ /**
2
+ * GramJS MTProto channel monitor.
3
+ *
4
+ * Connects to Telegram as a user account (burner), joins/monitors
5
+ * the configured public channels, and stores new messages in Redis
6
+ * when there's an active alert window.
7
+ *
8
+ * Rate-limited: 1-2s/channel with ±500ms jitter to avoid bans.
9
+ * Uses exponential backoff on flood errors.
10
+ */
11
+ export declare function startMonitor(): Promise<void>;
12
+ export declare function stopMonitor(): Promise<void>;
13
+ /**
14
+ * Fetch recent messages from a public Telegram channel via MTProto.
15
+ * Used by the telegram_mtproto_mcp_read_sources MCP tool.
16
+ *
17
+ * @param channel - Channel username with @ prefix (e.g. "@idf_telegram")
18
+ * @param limit - Number of messages to fetch (1-20)
19
+ * @returns Array of ChannelPost objects (newest first)
20
+ */
21
+ export declare function fetchRecentChannelPosts(channel: string, limit?: number): Promise<Array<{
22
+ text: string;
23
+ ts: number;
24
+ messageUrl?: string;
25
+ }>>;
26
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAiFH,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAgKlD;AAiGD,wBAAsB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAMjD;AAID;;;;;;;GAOG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,MAAM,EACf,KAAK,GAAE,MAAU,GAChB,OAAO,CAAC,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC,CA+BnE"}
package/dist/index.js ADDED
@@ -0,0 +1,324 @@
1
+ /**
2
+ * GramJS MTProto channel monitor.
3
+ *
4
+ * Connects to Telegram as a user account (burner), joins/monitors
5
+ * the configured public channels, and stores new messages in Redis
6
+ * when there's an active alert window.
7
+ *
8
+ * Rate-limited: 1-2s/channel with ±500ms jitter to avoid bans.
9
+ * Uses exponential backoff on flood errors.
10
+ */
11
+ import * as logger from "@easyoref/monitoring";
12
+ import { config, getActiveAlert, pushChannelPost } from "@easyoref/shared";
13
+ import { Api, TelegramClient } from "telegram";
14
+ import { NewMessage } from "telegram/events/index.js";
15
+ import { StringSession } from "telegram/sessions/index.js";
16
+ const SOCKS5_TYPE = 5;
17
+ let _client = undefined;
18
+ // ── Monitored channels (hardcoded) ────────────────────
19
+ const MONITORED_CHANNELS = [
20
+ // Original 5 channels
21
+ "@newsflashhhj",
22
+ "@yediotnews25",
23
+ "@Trueisrael",
24
+ "@israelsecurity",
25
+ "@N12LIVE",
26
+ // New channels added 2026-03-07
27
+ "@moriahdoron",
28
+ "@divuhim1234",
29
+ "@GLOBAL_Telegram_MOKED",
30
+ "@pkpoi",
31
+ "@lieldaphna",
32
+ "@News_cabinet_news",
33
+ "@yaronyanir1299",
34
+ "@ynetalerts",
35
+ "@idf_telegram",
36
+ "@israel_9", // 9tv — Israeli news channel
37
+ ];
38
+ const PRIVATE_CHANNELS = [
39
+ {
40
+ inviteHash: "AmLhsj0A5YJbpv0XtJQENg",
41
+ channelId: "1023468930",
42
+ title: "Private Intel Group",
43
+ },
44
+ ];
45
+ // ── Helpers ────────────────────────────────────────────
46
+ function sleep(ms) {
47
+ return new Promise((r) => setTimeout(r, ms));
48
+ }
49
+ function jitter(baseMs) {
50
+ return baseMs + Math.random() * 500 - 250;
51
+ }
52
+ // ── Client ─────────────────────────────────────────────
53
+ export async function startMonitor() {
54
+ if (!config.agent.enabled)
55
+ return;
56
+ const { apiId, apiHash, sessionString } = config.agent.mtproto;
57
+ if (!apiId || !apiHash) {
58
+ logger.warn("GramJS: api_id or api_hash not set — MTProto monitor disabled");
59
+ return;
60
+ }
61
+ const session = new StringSession(sessionString || "");
62
+ const clientOpts = {
63
+ connectionRetries: 5,
64
+ retryDelay: 2000,
65
+ autoReconnect: true,
66
+ deviceModel: "Desktop",
67
+ appVersion: "1.0.0",
68
+ systemVersion: "macOS 14",
69
+ langCode: "en",
70
+ proxy: undefined,
71
+ };
72
+ // SOCKS5 proxy support
73
+ if (config.agent.socks5Proxy) {
74
+ try {
75
+ const proxyUrl = new URL(config.agent.socks5Proxy);
76
+ clientOpts.proxy = {
77
+ socksType: SOCKS5_TYPE,
78
+ ip: proxyUrl.hostname,
79
+ port: Number(proxyUrl.port),
80
+ username: proxyUrl.username || undefined,
81
+ password: proxyUrl.password || undefined,
82
+ };
83
+ logger.info("GramJS: SOCKS5 proxy configured", {
84
+ host: proxyUrl.hostname,
85
+ });
86
+ }
87
+ catch {
88
+ logger.warn("GramJS: invalid socks5_proxy URL, ignoring");
89
+ }
90
+ }
91
+ _client = new TelegramClient(session, apiId, apiHash, clientOpts);
92
+ if (!sessionString) {
93
+ logger.warn("GramJS: no session_string configured. Run `npx tsx src/agent/auth.ts` first.");
94
+ return;
95
+ }
96
+ try {
97
+ await _client.connect();
98
+ logger.info("GramJS: connected to Telegram MTProto");
99
+ }
100
+ catch (err) {
101
+ logger.error("GramJS: connection failed", { error: String(err) });
102
+ return;
103
+ }
104
+ // Get all dialogs to check existing memberships
105
+ let existingChannels = new Set();
106
+ let existingPrivateIds = new Set();
107
+ try {
108
+ const dialogs = await _client.getDialogs({ limit: 200 });
109
+ for (const dialog of dialogs) {
110
+ const entity = dialog.entity;
111
+ // Public channel username
112
+ if (entity && "username" in entity && entity.username) {
113
+ existingChannels.add(String(entity.username).toLowerCase());
114
+ }
115
+ // Private channel ID
116
+ if (entity && "id" in entity) {
117
+ existingPrivateIds.add(String(entity.id));
118
+ }
119
+ }
120
+ logger.info("GramJS: fetched existing dialogs", {
121
+ total: dialogs.length,
122
+ });
123
+ }
124
+ catch (err) {
125
+ logger.warn("GramJS: failed to fetch dialogs, will try joining anyway", {
126
+ error: String(err),
127
+ });
128
+ }
129
+ // Auto-join all monitored public channels (required for NewMessage events)
130
+ for (const ch of MONITORED_CHANNELS) {
131
+ const username = ch.replace("@", "");
132
+ const normalizedUsername = username.toLowerCase();
133
+ // Check if already a member
134
+ if (existingChannels.has(normalizedUsername)) {
135
+ logger.debug("GramJS: already in channel", { channel: ch });
136
+ continue;
137
+ }
138
+ try {
139
+ await _client.invoke(new Api.channels.JoinChannel({ channel: username }));
140
+ logger.info("GramJS: joined channel", { channel: ch });
141
+ }
142
+ catch (err) {
143
+ const errStr = String(err);
144
+ if (errStr.includes("USER_ALREADY_PARTICIPANT")) {
145
+ logger.debug("GramJS: already in channel (via API)", { channel: ch });
146
+ }
147
+ else {
148
+ logger.warn("GramJS: failed to join channel", {
149
+ channel: ch,
150
+ error: errStr,
151
+ });
152
+ }
153
+ }
154
+ // Rate limit: 1-2s between joins
155
+ await sleep(jitter(1500));
156
+ }
157
+ // Auto-join private channels via invite hash
158
+ for (const priv of PRIVATE_CHANNELS) {
159
+ // Check if already a member by channel ID
160
+ if (existingPrivateIds.has(priv.channelId)) {
161
+ logger.debug("GramJS: already in private channel", { title: priv.title });
162
+ continue;
163
+ }
164
+ try {
165
+ await _client.invoke(new Api.messages.ImportChatInvite({ hash: priv.inviteHash }));
166
+ logger.info("GramJS: joined private channel", { title: priv.title });
167
+ }
168
+ catch (err) {
169
+ const errStr = String(err);
170
+ if (errStr.includes("USER_ALREADY_PARTICIPANT") ||
171
+ errStr.includes("INVITE_HASH_EXPIRED")) {
172
+ logger.debug("GramJS: already in private channel or hash expired", {
173
+ title: priv.title,
174
+ });
175
+ }
176
+ else {
177
+ logger.warn("GramJS: failed to join private channel", {
178
+ title: priv.title,
179
+ error: errStr,
180
+ });
181
+ }
182
+ }
183
+ // Rate limit: 1-2s between joins
184
+ await sleep(jitter(1500));
185
+ }
186
+ // Subscribe to new messages across all monitored channels
187
+ _client.addEventHandler(async (event) => {
188
+ await handleNewMessage(event).catch((err) => {
189
+ logger.warn("GramJS: handler error", { error: String(err) });
190
+ });
191
+ }, new NewMessage({}));
192
+ logger.info("GramJS: monitoring channels", {
193
+ public: MONITORED_CHANNELS.length,
194
+ private: PRIVATE_CHANNELS.length,
195
+ });
196
+ }
197
+ async function handleNewMessage(event) {
198
+ const msg = event.message;
199
+ if (!msg?.text || !msg.peerId) {
200
+ logger.debug("GramJS: skipped message (no text or peerId)");
201
+ return;
202
+ }
203
+ // Get channel identifier (username or title)
204
+ let channel = "";
205
+ let channelId = ""; // for private channels
206
+ let isPrivate = false;
207
+ try {
208
+ const chat = await event.message.getChat();
209
+ // Try to extract channel ID from peerId (for private channels)
210
+ if (msg.peerId && "channelId" in msg.peerId) {
211
+ // channelId is stored as bigint, convert to string
212
+ const rawId = String(msg.peerId.channelId);
213
+ channelId = rawId;
214
+ }
215
+ // Check if it's a monitored private channel
216
+ const privateMatch = PRIVATE_CHANNELS.find((p) => p.channelId === channelId);
217
+ if (privateMatch) {
218
+ channel = privateMatch.title;
219
+ isPrivate = true;
220
+ }
221
+ else if (chat && "username" in chat && chat.username) {
222
+ channel = `@${chat.username}`;
223
+ }
224
+ else if (chat && "title" in chat && chat.title) {
225
+ channel = String(chat.title);
226
+ }
227
+ else {
228
+ logger.debug("GramJS: skipped message (unidentifiable chat)");
229
+ return; // Not a channel we can identify
230
+ }
231
+ }
232
+ catch {
233
+ return;
234
+ }
235
+ // Only care about configured channels (public or private)
236
+ const normalizedChannel = channel.toLowerCase();
237
+ const isMonitored = isPrivate ||
238
+ MONITORED_CHANNELS.some((c) => c.toLowerCase() === normalizedChannel ||
239
+ c.toLowerCase().replace("@", "") === normalizedChannel.replace("@", ""));
240
+ if (!isMonitored) {
241
+ logger.debug("GramJS: skipped message (not monitored)", {
242
+ channel,
243
+ channelId,
244
+ });
245
+ return;
246
+ }
247
+ // Only store if there's an active alert window
248
+ const active = await getActiveAlert();
249
+ if (!active) {
250
+ logger.debug("GramJS: skipped message (no active alert)", { channel });
251
+ return;
252
+ }
253
+ // Anti-flood: jittered delay
254
+ await sleep(jitter(1000));
255
+ // Build direct message URL
256
+ let messageUrl;
257
+ if (isPrivate) {
258
+ // Private channel: https://t.me/c/1023468930/123
259
+ messageUrl = `https://t.me/c/${channelId}/${msg.id}`;
260
+ }
261
+ else {
262
+ // Public channel: https://t.me/username/123
263
+ const username = channel.replace("@", "");
264
+ messageUrl = `https://t.me/${username}/${msg.id}`;
265
+ }
266
+ await pushChannelPost(active.alertId, {
267
+ channel,
268
+ text: msg.text,
269
+ ts: Date.now(),
270
+ messageUrl,
271
+ });
272
+ logger.info("GramJS: stored channel post", {
273
+ channel,
274
+ alertId: active.alertId,
275
+ text_len: msg.text.length,
276
+ private: isPrivate,
277
+ });
278
+ }
279
+ export async function stopMonitor() {
280
+ if (_client) {
281
+ await _client.disconnect();
282
+ _client = undefined;
283
+ logger.info("GramJS: disconnected");
284
+ }
285
+ }
286
+ // ── Fetch recent posts (used by MCP tools) ─────────────
287
+ /**
288
+ * Fetch recent messages from a public Telegram channel via MTProto.
289
+ * Used by the telegram_mtproto_mcp_read_sources MCP tool.
290
+ *
291
+ * @param channel - Channel username with @ prefix (e.g. "@idf_telegram")
292
+ * @param limit - Number of messages to fetch (1-20)
293
+ * @returns Array of ChannelPost objects (newest first)
294
+ */
295
+ export async function fetchRecentChannelPosts(channel, limit = 5) {
296
+ if (!_client?.connected) {
297
+ logger.warn("GramJS: fetchRecentChannelPosts — client not connected");
298
+ return [];
299
+ }
300
+ const username = channel.replace("@", "");
301
+ const safeLimit = Math.min(Math.max(limit, 1), 20);
302
+ try {
303
+ // Rate limit: jittered delay before fetching
304
+ await sleep(jitter(1000));
305
+ const messages = await _client.getMessages(username, {
306
+ limit: safeLimit,
307
+ });
308
+ return messages
309
+ .filter((msg) => msg.text)
310
+ .map((msg) => ({
311
+ text: msg.text ?? "",
312
+ ts: msg.date ? msg.date * 1000 : Date.now(),
313
+ messageUrl: `https://t.me/${username}/${msg.id}`,
314
+ }));
315
+ }
316
+ catch (err) {
317
+ logger.warn("GramJS: fetchRecentChannelPosts failed", {
318
+ channel,
319
+ error: String(err),
320
+ });
321
+ return [];
322
+ }
323
+ }
324
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@easyoref/gramjs",
3
+ "version": "1.21.1",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "typecheck": "tsc --noEmit"
10
+ },
11
+ "dependencies": {
12
+ "telegram": "^2.26.0",
13
+ "qrcode-terminal": "^0.12.0",
14
+ "input": "^1.0.1",
15
+ "@easyoref/shared": "*",
16
+ "@easyoref/monitoring": "*",
17
+ "zod": "4.3.6"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^22.0.0",
21
+ "@types/qrcode-terminal": "^0.12.2",
22
+ "typescript": "^5.7.0"
23
+ }
24
+ }
package/src/index.ts ADDED
@@ -0,0 +1,400 @@
1
+ /**
2
+ * GramJS MTProto channel monitor.
3
+ *
4
+ * Connects to Telegram as a user account (burner), joins/monitors
5
+ * the configured public channels, and stores new messages in Redis
6
+ * when there's an active alert window.
7
+ *
8
+ * Rate-limited: 1-2s/channel with ±500ms jitter to avoid bans.
9
+ * Uses exponential backoff on flood errors.
10
+ */
11
+
12
+ import * as logger from "@easyoref/monitoring";
13
+ import { config, getActiveAlert, pushChannelPost } from "@easyoref/shared";
14
+ import { Api, TelegramClient } from "telegram";
15
+ import { NewMessage } from "telegram/events/index.js";
16
+ import type { NewMessageEvent } from "telegram/events/NewMessage.js";
17
+ import { StringSession } from "telegram/sessions/index.js";
18
+
19
+ const SOCKS5_TYPE = 5 as const;
20
+
21
+ type TelegramCtorOpts = ConstructorParameters<typeof TelegramClient>[3];
22
+
23
+ type TelegramClientOpts = {
24
+ connectionRetries: number;
25
+ retryDelay: number;
26
+ autoReconnect: boolean;
27
+ deviceModel: string;
28
+ appVersion: string;
29
+ systemVersion: string;
30
+ langCode: string;
31
+ proxy?: {
32
+ socksType: typeof SOCKS5_TYPE;
33
+ ip: string;
34
+ port: number;
35
+ username?: string;
36
+ password?: string;
37
+ };
38
+ };
39
+
40
+ let _client: TelegramClient | undefined = undefined;
41
+
42
+ // ── Monitored channels (hardcoded) ────────────────────
43
+
44
+ const MONITORED_CHANNELS = [
45
+ // Original 5 channels
46
+ "@newsflashhhj",
47
+ "@yediotnews25",
48
+ "@Trueisrael",
49
+ "@israelsecurity",
50
+ "@N12LIVE",
51
+ // New channels added 2026-03-07
52
+ "@moriahdoron",
53
+ "@divuhim1234",
54
+ "@GLOBAL_Telegram_MOKED",
55
+ "@pkpoi",
56
+ "@lieldaphna",
57
+ "@News_cabinet_news",
58
+ "@yaronyanir1299",
59
+ "@ynetalerts",
60
+ "@idf_telegram",
61
+ "@israel_9", // 9tv — Israeli news channel
62
+ ];
63
+
64
+ // Private channels (invite hash + channel ID for URL building)
65
+ interface PrivateChannel {
66
+ inviteHash: string; // from t.me/joinchat/...
67
+ channelId: string; // from t.me/c/1023468930/... (without -100 prefix)
68
+ title: string; // for logs/identification
69
+ }
70
+
71
+ const PRIVATE_CHANNELS: PrivateChannel[] = [
72
+ {
73
+ inviteHash: "AmLhsj0A5YJbpv0XtJQENg",
74
+ channelId: "1023468930",
75
+ title: "Private Intel Group",
76
+ },
77
+ ];
78
+
79
+ // ── Helpers ────────────────────────────────────────────
80
+
81
+ function sleep(ms: number): Promise<void> {
82
+ return new Promise((r) => setTimeout(r, ms));
83
+ }
84
+
85
+ function jitter(baseMs: number): number {
86
+ return baseMs + Math.random() * 500 - 250;
87
+ }
88
+
89
+ // ── Client ─────────────────────────────────────────────
90
+
91
+ export async function startMonitor(): Promise<void> {
92
+ if (!config.agent.enabled) return;
93
+
94
+ const { apiId, apiHash, sessionString } = config.agent.mtproto;
95
+
96
+ if (!apiId || !apiHash) {
97
+ logger.warn(
98
+ "GramJS: api_id or api_hash not set — MTProto monitor disabled",
99
+ );
100
+ return;
101
+ }
102
+
103
+ const session = new StringSession(sessionString || "");
104
+
105
+ const clientOpts = {
106
+ connectionRetries: 5,
107
+ retryDelay: 2000,
108
+ autoReconnect: true,
109
+ deviceModel: "Desktop",
110
+ appVersion: "1.0.0",
111
+ systemVersion: "macOS 14",
112
+ langCode: "en",
113
+ proxy: undefined as TelegramClientOpts["proxy"],
114
+ } satisfies TelegramClientOpts;
115
+
116
+ // SOCKS5 proxy support
117
+ if (config.agent.socks5Proxy) {
118
+ try {
119
+ const proxyUrl = new URL(config.agent.socks5Proxy);
120
+ clientOpts.proxy = {
121
+ socksType: SOCKS5_TYPE,
122
+ ip: proxyUrl.hostname,
123
+ port: Number(proxyUrl.port),
124
+ username: proxyUrl.username || undefined,
125
+ password: proxyUrl.password || undefined,
126
+ };
127
+ logger.info("GramJS: SOCKS5 proxy configured", {
128
+ host: proxyUrl.hostname,
129
+ });
130
+ } catch {
131
+ logger.warn("GramJS: invalid socks5_proxy URL, ignoring");
132
+ }
133
+ }
134
+
135
+ _client = new TelegramClient(session, apiId, apiHash, clientOpts);
136
+
137
+ if (!sessionString) {
138
+ logger.warn(
139
+ "GramJS: no session_string configured. Run `npx tsx src/agent/auth.ts` first.",
140
+ );
141
+ return;
142
+ }
143
+
144
+ try {
145
+ await _client.connect();
146
+ logger.info("GramJS: connected to Telegram MTProto");
147
+ } catch (err) {
148
+ logger.error("GramJS: connection failed", { error: String(err) });
149
+ return;
150
+ }
151
+
152
+ // Get all dialogs to check existing memberships
153
+ let existingChannels: Set<string> = new Set();
154
+ let existingPrivateIds: Set<string> = new Set();
155
+
156
+ try {
157
+ const dialogs = await _client.getDialogs({ limit: 200 });
158
+ for (const dialog of dialogs) {
159
+ const entity = dialog.entity;
160
+ // Public channel username
161
+ if (entity && "username" in entity && entity.username) {
162
+ existingChannels.add(String(entity.username).toLowerCase());
163
+ }
164
+ // Private channel ID
165
+ if (entity && "id" in entity) {
166
+ existingPrivateIds.add(String(entity.id));
167
+ }
168
+ }
169
+ logger.info("GramJS: fetched existing dialogs", {
170
+ total: dialogs.length,
171
+ });
172
+ } catch (err) {
173
+ logger.warn("GramJS: failed to fetch dialogs, will try joining anyway", {
174
+ error: String(err),
175
+ });
176
+ }
177
+
178
+ // Auto-join all monitored public channels (required for NewMessage events)
179
+ for (const ch of MONITORED_CHANNELS) {
180
+ const username = ch.replace("@", "");
181
+ const normalizedUsername = username.toLowerCase();
182
+
183
+ // Check if already a member
184
+ if (existingChannels.has(normalizedUsername)) {
185
+ logger.debug("GramJS: already in channel", { channel: ch });
186
+ continue;
187
+ }
188
+
189
+ try {
190
+ await _client.invoke(new Api.channels.JoinChannel({ channel: username }));
191
+ logger.info("GramJS: joined channel", { channel: ch });
192
+ } catch (err: unknown) {
193
+ const errStr = String(err);
194
+ if (errStr.includes("USER_ALREADY_PARTICIPANT")) {
195
+ logger.debug("GramJS: already in channel (via API)", { channel: ch });
196
+ } else {
197
+ logger.warn("GramJS: failed to join channel", {
198
+ channel: ch,
199
+ error: errStr,
200
+ });
201
+ }
202
+ }
203
+ // Rate limit: 1-2s between joins
204
+ await sleep(jitter(1500));
205
+ }
206
+
207
+ // Auto-join private channels via invite hash
208
+ for (const priv of PRIVATE_CHANNELS) {
209
+ // Check if already a member by channel ID
210
+ if (existingPrivateIds.has(priv.channelId)) {
211
+ logger.debug("GramJS: already in private channel", { title: priv.title });
212
+ continue;
213
+ }
214
+
215
+ try {
216
+ await _client.invoke(
217
+ new Api.messages.ImportChatInvite({ hash: priv.inviteHash }),
218
+ );
219
+ logger.info("GramJS: joined private channel", { title: priv.title });
220
+ } catch (err: unknown) {
221
+ const errStr = String(err);
222
+ if (
223
+ errStr.includes("USER_ALREADY_PARTICIPANT") ||
224
+ errStr.includes("INVITE_HASH_EXPIRED")
225
+ ) {
226
+ logger.debug("GramJS: already in private channel or hash expired", {
227
+ title: priv.title,
228
+ });
229
+ } else {
230
+ logger.warn("GramJS: failed to join private channel", {
231
+ title: priv.title,
232
+ error: errStr,
233
+ });
234
+ }
235
+ }
236
+ // Rate limit: 1-2s between joins
237
+ await sleep(jitter(1500));
238
+ }
239
+
240
+ // Subscribe to new messages across all monitored channels
241
+ _client.addEventHandler(async (event: NewMessageEvent) => {
242
+ await handleNewMessage(event).catch((err) => {
243
+ logger.warn("GramJS: handler error", { error: String(err) });
244
+ });
245
+ }, new NewMessage({}));
246
+
247
+ logger.info("GramJS: monitoring channels", {
248
+ public: MONITORED_CHANNELS.length,
249
+ private: PRIVATE_CHANNELS.length,
250
+ });
251
+ }
252
+
253
+ async function handleNewMessage(event: NewMessageEvent): Promise<void> {
254
+ const msg = event.message;
255
+ if (!msg?.text || !msg.peerId) {
256
+ logger.debug("GramJS: skipped message (no text or peerId)");
257
+ return;
258
+ }
259
+
260
+ // Get channel identifier (username or title)
261
+ let channel = "";
262
+ let channelId = ""; // for private channels
263
+ let isPrivate = false;
264
+
265
+ try {
266
+ const chat = await event.message.getChat();
267
+
268
+ // Try to extract channel ID from peerId (for private channels)
269
+ if (msg.peerId && "channelId" in msg.peerId) {
270
+ // channelId is stored as bigint, convert to string
271
+ const rawId = String(msg.peerId.channelId);
272
+ channelId = rawId;
273
+ }
274
+
275
+ // Check if it's a monitored private channel
276
+ const privateMatch = PRIVATE_CHANNELS.find(
277
+ (p) => p.channelId === channelId,
278
+ );
279
+ if (privateMatch) {
280
+ channel = privateMatch.title;
281
+ isPrivate = true;
282
+ } else if (chat && "username" in chat && chat.username) {
283
+ channel = `@${chat.username}`;
284
+ } else if (chat && "title" in chat && chat.title) {
285
+ channel = String(chat.title);
286
+ } else {
287
+ logger.debug("GramJS: skipped message (unidentifiable chat)");
288
+ return; // Not a channel we can identify
289
+ }
290
+ } catch {
291
+ return;
292
+ }
293
+
294
+ // Only care about configured channels (public or private)
295
+ const normalizedChannel = channel.toLowerCase();
296
+ const isMonitored =
297
+ isPrivate ||
298
+ MONITORED_CHANNELS.some(
299
+ (c) =>
300
+ c.toLowerCase() === normalizedChannel ||
301
+ c.toLowerCase().replace("@", "") === normalizedChannel.replace("@", ""),
302
+ );
303
+
304
+ if (!isMonitored) {
305
+ logger.debug("GramJS: skipped message (not monitored)", {
306
+ channel,
307
+ channelId,
308
+ });
309
+ return;
310
+ }
311
+
312
+ // Only store if there's an active alert window
313
+ const active = await getActiveAlert();
314
+ if (!active) {
315
+ logger.debug("GramJS: skipped message (no active alert)", { channel });
316
+ return;
317
+ }
318
+
319
+ // Anti-flood: jittered delay
320
+ await sleep(jitter(1000));
321
+
322
+ // Build direct message URL
323
+ let messageUrl: string;
324
+ if (isPrivate) {
325
+ // Private channel: https://t.me/c/1023468930/123
326
+ messageUrl = `https://t.me/c/${channelId}/${msg.id}`;
327
+ } else {
328
+ // Public channel: https://t.me/username/123
329
+ const username = channel.replace("@", "");
330
+ messageUrl = `https://t.me/${username}/${msg.id}`;
331
+ }
332
+
333
+ await pushChannelPost(active.alertId, {
334
+ channel,
335
+ text: msg.text,
336
+ ts: Date.now(),
337
+ messageUrl,
338
+ });
339
+
340
+ logger.info("GramJS: stored channel post", {
341
+ channel,
342
+ alertId: active.alertId,
343
+ text_len: msg.text.length,
344
+ private: isPrivate,
345
+ });
346
+ }
347
+
348
+ export async function stopMonitor(): Promise<void> {
349
+ if (_client) {
350
+ await _client.disconnect();
351
+ _client = undefined;
352
+ logger.info("GramJS: disconnected");
353
+ }
354
+ }
355
+
356
+ // ── Fetch recent posts (used by MCP tools) ─────────────
357
+
358
+ /**
359
+ * Fetch recent messages from a public Telegram channel via MTProto.
360
+ * Used by the telegram_mtproto_mcp_read_sources MCP tool.
361
+ *
362
+ * @param channel - Channel username with @ prefix (e.g. "@idf_telegram")
363
+ * @param limit - Number of messages to fetch (1-20)
364
+ * @returns Array of ChannelPost objects (newest first)
365
+ */
366
+ export async function fetchRecentChannelPosts(
367
+ channel: string,
368
+ limit: number = 5,
369
+ ): Promise<Array<{ text: string; ts: number; messageUrl?: string }>> {
370
+ if (!_client?.connected) {
371
+ logger.warn("GramJS: fetchRecentChannelPosts — client not connected");
372
+ return [];
373
+ }
374
+
375
+ const username = channel.replace("@", "");
376
+ const safeLimit = Math.min(Math.max(limit, 1), 20);
377
+
378
+ try {
379
+ // Rate limit: jittered delay before fetching
380
+ await sleep(jitter(1000));
381
+
382
+ const messages = await _client.getMessages(username, {
383
+ limit: safeLimit,
384
+ });
385
+
386
+ return messages
387
+ .filter((msg) => msg.text)
388
+ .map((msg) => ({
389
+ text: msg.text ?? "",
390
+ ts: msg.date ? msg.date * 1000 : Date.now(),
391
+ messageUrl: `https://t.me/${username}/${msg.id}`,
392
+ }));
393
+ } catch (err) {
394
+ logger.warn("GramJS: fetchRecentChannelPosts failed", {
395
+ channel,
396
+ error: String(err),
397
+ });
398
+ return [];
399
+ }
400
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist"
6
+ },
7
+ "references": [{ "path": "../shared" }, { "path": "../monitoring" }],
8
+ "include": ["src/**/*"]
9
+ }