@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.
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +324 -0
- package/dist/index.js.map +1 -0
- package/package.json +24 -0
- package/src/index.ts +400 -0
- package/tsconfig.json +9 -0
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|