telegram-claude-mcp 1.6.0 → 2.0.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/ARCHITECTURE.md +234 -0
- package/README.md +249 -58
- package/bin/daemon-ctl.js +207 -0
- package/bin/daemon.js +20 -0
- package/bin/proxy.js +22 -0
- package/bin/setup.js +90 -63
- package/hooks-v2/notify-hook.sh +32 -0
- package/hooks-v2/permission-hook.sh +43 -0
- package/hooks-v2/stop-hook.sh +45 -0
- package/package.json +16 -5
- package/src/daemon/index.ts +415 -0
- package/src/daemon/session-manager.ts +173 -0
- package/src/daemon/telegram-multi.ts +611 -0
- package/src/proxy/index.ts +429 -0
- package/src/shared/protocol.ts +146 -0
- package/src/telegram.ts +85 -71
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-Session Telegram Manager for the Daemon
|
|
3
|
+
*
|
|
4
|
+
* Unlike the per-session TelegramManager, this one handles all sessions
|
|
5
|
+
* with proper routing based on session IDs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import TelegramBot from 'node-telegram-bot-api';
|
|
9
|
+
import type { SessionManager, ConnectedSession } from './session-manager.js';
|
|
10
|
+
|
|
11
|
+
export interface PermissionDecision {
|
|
12
|
+
behavior: 'allow' | 'deny';
|
|
13
|
+
message?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface HookEvent {
|
|
17
|
+
type: 'permission_request' | 'stop' | 'notification' | 'session_start' | 'session_end';
|
|
18
|
+
session_id?: string;
|
|
19
|
+
tool_name?: string;
|
|
20
|
+
tool_input?: Record<string, unknown>;
|
|
21
|
+
message?: string;
|
|
22
|
+
timestamp?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface PendingResponse {
|
|
26
|
+
sessionId: string;
|
|
27
|
+
resolve: (response: string) => void;
|
|
28
|
+
reject: (error: Error) => void;
|
|
29
|
+
messageId: number;
|
|
30
|
+
timestamp: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface PendingPermission {
|
|
34
|
+
sessionId: string;
|
|
35
|
+
resolve: (decision: PermissionDecision) => void;
|
|
36
|
+
reject: (error: Error) => void;
|
|
37
|
+
messageId: number;
|
|
38
|
+
timestamp: number;
|
|
39
|
+
toolName: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface MultiTelegramConfig {
|
|
43
|
+
botToken: string;
|
|
44
|
+
chatId: number;
|
|
45
|
+
responseTimeoutMs?: number;
|
|
46
|
+
permissionTimeoutMs?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class MultiTelegramManager {
|
|
50
|
+
private bot: TelegramBot;
|
|
51
|
+
private config: MultiTelegramConfig;
|
|
52
|
+
private sessionManager: SessionManager;
|
|
53
|
+
private pendingResponses: Map<string, PendingResponse> = new Map();
|
|
54
|
+
private pendingPermissions: Map<string, PendingPermission> = new Map();
|
|
55
|
+
private messageToSession: Map<number, string> = new Map(); // messageId -> sessionId
|
|
56
|
+
private isRunning = false;
|
|
57
|
+
|
|
58
|
+
constructor(config: MultiTelegramConfig, sessionManager: SessionManager) {
|
|
59
|
+
this.config = {
|
|
60
|
+
responseTimeoutMs: 600000,
|
|
61
|
+
permissionTimeoutMs: 600000,
|
|
62
|
+
...config,
|
|
63
|
+
};
|
|
64
|
+
this.sessionManager = sessionManager;
|
|
65
|
+
this.bot = new TelegramBot(config.botToken, { polling: true });
|
|
66
|
+
|
|
67
|
+
this.setupMessageHandler();
|
|
68
|
+
this.setupCallbackHandler();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
start(): void {
|
|
72
|
+
this.isRunning = true;
|
|
73
|
+
console.error('[MultiTelegram] Bot started');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
stop(): void {
|
|
77
|
+
this.isRunning = false;
|
|
78
|
+
this.bot.stopPolling();
|
|
79
|
+
console.error('[MultiTelegram] Bot stopped');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Send a message for a specific session and wait for response
|
|
84
|
+
*/
|
|
85
|
+
async sendMessageAndWait(
|
|
86
|
+
sessionId: string,
|
|
87
|
+
message: string
|
|
88
|
+
): Promise<{ chatId: string; response: string }> {
|
|
89
|
+
const session = this.sessionManager.get(sessionId);
|
|
90
|
+
if (!session) {
|
|
91
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const taggedMessage = `[${session.sessionName}] ${message}`;
|
|
95
|
+
const sent = await this.bot.sendMessage(this.config.chatId, taggedMessage);
|
|
96
|
+
|
|
97
|
+
// Track message ownership
|
|
98
|
+
this.messageToSession.set(sent.message_id, sessionId);
|
|
99
|
+
this.sessionManager.touch(sessionId);
|
|
100
|
+
|
|
101
|
+
// Wait for response
|
|
102
|
+
const response = await this.waitForResponse(sessionId, sent.message_id);
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
chatId: `${session.sessionName}:${this.config.chatId}`,
|
|
106
|
+
response,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Continue a chat for a specific session
|
|
112
|
+
*/
|
|
113
|
+
async continueChat(sessionId: string, message: string): Promise<string> {
|
|
114
|
+
const session = this.sessionManager.get(sessionId);
|
|
115
|
+
if (!session) {
|
|
116
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const taggedMessage = `[${session.sessionName}] ${message}`;
|
|
120
|
+
const sent = await this.bot.sendMessage(this.config.chatId, taggedMessage);
|
|
121
|
+
|
|
122
|
+
this.messageToSession.set(sent.message_id, sessionId);
|
|
123
|
+
this.sessionManager.touch(sessionId);
|
|
124
|
+
|
|
125
|
+
return this.waitForResponse(sessionId, sent.message_id);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Send a notification without waiting
|
|
130
|
+
*/
|
|
131
|
+
async notify(sessionId: string, message: string): Promise<void> {
|
|
132
|
+
const session = this.sessionManager.get(sessionId);
|
|
133
|
+
if (!session) {
|
|
134
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const taggedMessage = `[${session.sessionName}] ${message}`;
|
|
138
|
+
const sent = await this.bot.sendMessage(this.config.chatId, taggedMessage);
|
|
139
|
+
this.messageToSession.set(sent.message_id, sessionId);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* End a chat session
|
|
144
|
+
*/
|
|
145
|
+
async endChat(sessionId: string, message?: string): Promise<void> {
|
|
146
|
+
const session = this.sessionManager.get(sessionId);
|
|
147
|
+
if (!session) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (message) {
|
|
152
|
+
const taggedMessage = `[${session.sessionName}] ${message}`;
|
|
153
|
+
await this.bot.sendMessage(this.config.chatId, taggedMessage);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Clean up pending responses for this session
|
|
157
|
+
for (const [key, pending] of this.pendingResponses) {
|
|
158
|
+
if (pending.sessionId === sessionId) {
|
|
159
|
+
pending.reject(new Error('Chat ended'));
|
|
160
|
+
this.pendingResponses.delete(key);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Handle permission request for a session (called by hook)
|
|
167
|
+
*/
|
|
168
|
+
async handlePermissionRequest(
|
|
169
|
+
sessionName: string,
|
|
170
|
+
toolName: string,
|
|
171
|
+
toolInput: Record<string, unknown>
|
|
172
|
+
): Promise<PermissionDecision> {
|
|
173
|
+
const session = this.sessionManager.getByName(sessionName);
|
|
174
|
+
const displayName = session?.sessionName || sessionName;
|
|
175
|
+
|
|
176
|
+
const inputDisplay = this.formatToolInput(toolName, toolInput);
|
|
177
|
+
const message = `🔐 [${displayName}] Permission Request\n\nTool: ${toolName}\n${inputDisplay}`;
|
|
178
|
+
|
|
179
|
+
const sent = await this.bot.sendMessage(this.config.chatId, message);
|
|
180
|
+
|
|
181
|
+
if (session) {
|
|
182
|
+
this.messageToSession.set(sent.message_id, session.sessionId);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
await this.bot.editMessageReplyMarkup(
|
|
186
|
+
{
|
|
187
|
+
inline_keyboard: [
|
|
188
|
+
[
|
|
189
|
+
{ text: '✅ Allow', callback_data: `allow:${sent.message_id}` },
|
|
190
|
+
{ text: '❌ Deny', callback_data: `deny:${sent.message_id}` },
|
|
191
|
+
],
|
|
192
|
+
],
|
|
193
|
+
},
|
|
194
|
+
{ chat_id: this.config.chatId, message_id: sent.message_id }
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Wait with reminders
|
|
198
|
+
const reminderIntervalMs = 120000;
|
|
199
|
+
const maxReminders = 4;
|
|
200
|
+
|
|
201
|
+
for (let attempt = 0; attempt <= maxReminders; attempt++) {
|
|
202
|
+
try {
|
|
203
|
+
return await this.waitForPermissionWithTimeout(
|
|
204
|
+
sent.message_id,
|
|
205
|
+
toolName,
|
|
206
|
+
session?.sessionId || 'hook',
|
|
207
|
+
reminderIntervalMs
|
|
208
|
+
);
|
|
209
|
+
} catch {
|
|
210
|
+
if (attempt < maxReminders) {
|
|
211
|
+
await this.bot.sendMessage(
|
|
212
|
+
this.config.chatId,
|
|
213
|
+
`⏰ [${displayName}] Reminder: Still waiting for permission\n\nTool: ${toolName}`,
|
|
214
|
+
{ reply_to_message_id: sent.message_id }
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Final timeout message with buttons
|
|
221
|
+
const retryMsg = await this.bot.sendMessage(
|
|
222
|
+
this.config.chatId,
|
|
223
|
+
`⚠️ [${displayName}] Permission request timed out\n\nTool: ${toolName}\n\nClick below to respond:`
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
await this.bot.editMessageReplyMarkup(
|
|
227
|
+
{
|
|
228
|
+
inline_keyboard: [
|
|
229
|
+
[
|
|
230
|
+
{ text: '✅ Allow Now', callback_data: `allow:${sent.message_id}` },
|
|
231
|
+
{ text: '❌ Deny', callback_data: `deny:${sent.message_id}` },
|
|
232
|
+
],
|
|
233
|
+
],
|
|
234
|
+
},
|
|
235
|
+
{ chat_id: this.config.chatId, message_id: retryMsg.message_id }
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
return await this.waitForPermissionWithTimeout(
|
|
240
|
+
sent.message_id,
|
|
241
|
+
toolName,
|
|
242
|
+
session?.sessionId || 'hook',
|
|
243
|
+
this.config.permissionTimeoutMs!
|
|
244
|
+
);
|
|
245
|
+
} catch {
|
|
246
|
+
return { behavior: 'deny', message: 'Permission request timed out' };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Handle interactive stop for a session (called by hook)
|
|
252
|
+
*/
|
|
253
|
+
async handleInteractiveStop(
|
|
254
|
+
sessionName: string,
|
|
255
|
+
transcriptPath?: string
|
|
256
|
+
): Promise<Record<string, unknown>> {
|
|
257
|
+
const session = this.sessionManager.getByName(sessionName);
|
|
258
|
+
const displayName = session?.sessionName || sessionName;
|
|
259
|
+
|
|
260
|
+
let lastMessage = 'Claude has finished working.';
|
|
261
|
+
if (transcriptPath) {
|
|
262
|
+
lastMessage = await this.extractLastMessage(transcriptPath);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const message = `🏁 [${displayName}] Claude stopped\n\n${lastMessage}\n\n💬 Reply with instructions to continue, or "done" to finish.`;
|
|
266
|
+
const sent = await this.bot.sendMessage(this.config.chatId, message);
|
|
267
|
+
|
|
268
|
+
if (session) {
|
|
269
|
+
this.messageToSession.set(sent.message_id, session.sessionId);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const reminderIntervalMs = 120000;
|
|
273
|
+
const maxReminders = 4;
|
|
274
|
+
|
|
275
|
+
for (let attempt = 0; attempt <= maxReminders; attempt++) {
|
|
276
|
+
try {
|
|
277
|
+
const response = await this.waitForResponseWithTimeout(
|
|
278
|
+
session?.sessionId || 'hook',
|
|
279
|
+
sent.message_id,
|
|
280
|
+
reminderIntervalMs
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
const lower = response.toLowerCase().trim();
|
|
284
|
+
if (['done', 'stop', 'finish', 'ok'].includes(lower)) {
|
|
285
|
+
return {};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return { decision: 'block', reason: response };
|
|
289
|
+
} catch {
|
|
290
|
+
if (attempt < maxReminders) {
|
|
291
|
+
await this.bot.sendMessage(
|
|
292
|
+
this.config.chatId,
|
|
293
|
+
`⏰ [${displayName}] Reminder: Claude is waiting for your response`,
|
|
294
|
+
{ reply_to_message_id: sent.message_id }
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Final attempt
|
|
301
|
+
try {
|
|
302
|
+
await this.bot.sendMessage(
|
|
303
|
+
this.config.chatId,
|
|
304
|
+
`⚠️ [${displayName}] Last chance! Claude will stop soon.\n\n💬 Reply now to continue.`,
|
|
305
|
+
{ reply_to_message_id: sent.message_id }
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
const response = await this.waitForResponseWithTimeout(
|
|
309
|
+
session?.sessionId || 'hook',
|
|
310
|
+
sent.message_id,
|
|
311
|
+
this.config.responseTimeoutMs!
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
const lower = response.toLowerCase().trim();
|
|
315
|
+
if (['done', 'stop', 'finish', 'ok'].includes(lower)) {
|
|
316
|
+
return {};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return { decision: 'block', reason: response };
|
|
320
|
+
} catch {
|
|
321
|
+
await this.bot.sendMessage(
|
|
322
|
+
this.config.chatId,
|
|
323
|
+
`😴 [${displayName}] Claude stopped (no response received)`
|
|
324
|
+
);
|
|
325
|
+
return {};
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Send hook notification
|
|
331
|
+
*/
|
|
332
|
+
async sendHookNotification(sessionName: string, event: HookEvent): Promise<void> {
|
|
333
|
+
let emoji = '📢';
|
|
334
|
+
let title = 'Notification';
|
|
335
|
+
|
|
336
|
+
switch (event.type) {
|
|
337
|
+
case 'stop':
|
|
338
|
+
emoji = '🛑';
|
|
339
|
+
title = 'Agent Stopped';
|
|
340
|
+
break;
|
|
341
|
+
case 'session_start':
|
|
342
|
+
emoji = '🚀';
|
|
343
|
+
title = 'Session Started';
|
|
344
|
+
break;
|
|
345
|
+
case 'session_end':
|
|
346
|
+
emoji = '👋';
|
|
347
|
+
title = 'Session Ended';
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const message = `${emoji} [${sessionName}] ${title}\n\n${event.message || 'No details'}`;
|
|
352
|
+
await this.bot.sendMessage(this.config.chatId, message);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private async extractLastMessage(transcriptPath: string): Promise<string> {
|
|
356
|
+
try {
|
|
357
|
+
const fs = await import('fs');
|
|
358
|
+
if (!fs.existsSync(transcriptPath)) {
|
|
359
|
+
return 'Claude has finished working.';
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const content = fs.readFileSync(transcriptPath, 'utf-8');
|
|
363
|
+
const lines = content.trim().split('\n');
|
|
364
|
+
|
|
365
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
366
|
+
try {
|
|
367
|
+
const entry = JSON.parse(lines[i]);
|
|
368
|
+
if (entry.type === 'assistant' && entry.message?.content) {
|
|
369
|
+
const textContent = entry.message.content.find((c: any) => c.type === 'text');
|
|
370
|
+
if (textContent?.text) {
|
|
371
|
+
return this.truncateMiddle(textContent.text, 600);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
} catch {
|
|
375
|
+
// Skip invalid lines
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
} catch (err) {
|
|
379
|
+
console.error('[MultiTelegram] Error reading transcript:', err);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return 'Claude has finished working.';
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private truncateMiddle(text: string, maxLength: number): string {
|
|
386
|
+
if (text.length <= maxLength) return text;
|
|
387
|
+
|
|
388
|
+
const startLength = Math.floor(maxLength * 0.3);
|
|
389
|
+
const endLength = Math.floor(maxLength * 0.6);
|
|
390
|
+
|
|
391
|
+
return `${text.slice(0, startLength)}\n\n[...truncated...]\n\n${text.slice(-endLength)}`;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private formatToolInput(toolName: string, input: Record<string, unknown>): string {
|
|
395
|
+
if (toolName === 'Bash' && input.command) {
|
|
396
|
+
return `Command: \`${input.command}\``;
|
|
397
|
+
}
|
|
398
|
+
if (toolName === 'Write' && input.file_path) {
|
|
399
|
+
const content = input.content as string;
|
|
400
|
+
const preview = content?.length > 100 ? content.slice(0, 100) + '...' : content;
|
|
401
|
+
return `File: ${input.file_path}\nContent: ${preview || '(empty)'}`;
|
|
402
|
+
}
|
|
403
|
+
if (toolName === 'Edit' && input.file_path) {
|
|
404
|
+
return `File: ${input.file_path}\nOld: ${input.old_string}\nNew: ${input.new_string}`;
|
|
405
|
+
}
|
|
406
|
+
if (toolName === 'Read' && input.file_path) {
|
|
407
|
+
return `File: ${input.file_path}`;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
const str = JSON.stringify(input, null, 2);
|
|
412
|
+
return str.length > 500 ? str.slice(0, 500) + '...' : str;
|
|
413
|
+
} catch {
|
|
414
|
+
return '(unable to display input)';
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private waitForResponse(sessionId: string, messageId: number): Promise<string> {
|
|
419
|
+
return this.waitForResponseWithTimeout(sessionId, messageId, this.config.responseTimeoutMs!);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private waitForResponseWithTimeout(
|
|
423
|
+
sessionId: string,
|
|
424
|
+
messageId: number,
|
|
425
|
+
timeoutMs: number
|
|
426
|
+
): Promise<string> {
|
|
427
|
+
return new Promise((resolve, reject) => {
|
|
428
|
+
const key = `${messageId}`;
|
|
429
|
+
|
|
430
|
+
const existing = this.pendingResponses.get(key);
|
|
431
|
+
if (existing) {
|
|
432
|
+
existing.resolve = resolve;
|
|
433
|
+
existing.reject = reject;
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const timeout = setTimeout(() => {
|
|
438
|
+
this.pendingResponses.delete(key);
|
|
439
|
+
reject(new Error('Timeout'));
|
|
440
|
+
}, timeoutMs);
|
|
441
|
+
|
|
442
|
+
this.pendingResponses.set(key, {
|
|
443
|
+
sessionId,
|
|
444
|
+
resolve: (response: string) => {
|
|
445
|
+
clearTimeout(timeout);
|
|
446
|
+
this.pendingResponses.delete(key);
|
|
447
|
+
resolve(response);
|
|
448
|
+
},
|
|
449
|
+
reject: (error: Error) => {
|
|
450
|
+
clearTimeout(timeout);
|
|
451
|
+
this.pendingResponses.delete(key);
|
|
452
|
+
reject(error);
|
|
453
|
+
},
|
|
454
|
+
messageId,
|
|
455
|
+
timestamp: Date.now(),
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
private waitForPermissionWithTimeout(
|
|
461
|
+
messageId: number,
|
|
462
|
+
toolName: string,
|
|
463
|
+
sessionId: string,
|
|
464
|
+
timeoutMs: number
|
|
465
|
+
): Promise<PermissionDecision> {
|
|
466
|
+
return new Promise((resolve, reject) => {
|
|
467
|
+
const key = `${messageId}`;
|
|
468
|
+
|
|
469
|
+
const existing = this.pendingPermissions.get(key);
|
|
470
|
+
if (existing) {
|
|
471
|
+
existing.resolve = resolve;
|
|
472
|
+
existing.reject = reject;
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const timeout = setTimeout(() => {
|
|
477
|
+
this.pendingPermissions.delete(key);
|
|
478
|
+
reject(new Error('Timeout'));
|
|
479
|
+
}, timeoutMs);
|
|
480
|
+
|
|
481
|
+
this.pendingPermissions.set(key, {
|
|
482
|
+
sessionId,
|
|
483
|
+
resolve: (decision: PermissionDecision) => {
|
|
484
|
+
clearTimeout(timeout);
|
|
485
|
+
this.pendingPermissions.delete(key);
|
|
486
|
+
resolve(decision);
|
|
487
|
+
},
|
|
488
|
+
reject: (error: Error) => {
|
|
489
|
+
clearTimeout(timeout);
|
|
490
|
+
this.pendingPermissions.delete(key);
|
|
491
|
+
reject(error);
|
|
492
|
+
},
|
|
493
|
+
messageId,
|
|
494
|
+
timestamp: Date.now(),
|
|
495
|
+
toolName,
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
private setupMessageHandler(): void {
|
|
501
|
+
this.bot.on('message', (msg) => {
|
|
502
|
+
if (msg.chat.id !== this.config.chatId) return;
|
|
503
|
+
if (msg.from?.is_bot) return;
|
|
504
|
+
|
|
505
|
+
const text = msg.text || '';
|
|
506
|
+
console.error(`[MultiTelegram] Received: "${text}"`);
|
|
507
|
+
|
|
508
|
+
// Check if reply to known message
|
|
509
|
+
if (msg.reply_to_message) {
|
|
510
|
+
const replyToId = msg.reply_to_message.message_id;
|
|
511
|
+
const sessionId = this.messageToSession.get(replyToId);
|
|
512
|
+
|
|
513
|
+
if (sessionId) {
|
|
514
|
+
console.error(`[MultiTelegram] Is reply to session ${sessionId}`);
|
|
515
|
+
this.resolveResponseForSession(sessionId, text);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Check for @sessionName prefix
|
|
521
|
+
for (const session of this.sessionManager.getAll()) {
|
|
522
|
+
const prefix = `@${session.sessionName}`;
|
|
523
|
+
if (text.toLowerCase().startsWith(prefix.toLowerCase())) {
|
|
524
|
+
const response = text.slice(prefix.length).trim();
|
|
525
|
+
this.resolveResponseForSession(session.sessionId, response);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Route to most recently active waiting session
|
|
531
|
+
const mostRecent = this.findMostRecentWaitingSession();
|
|
532
|
+
if (mostRecent) {
|
|
533
|
+
console.error(`[MultiTelegram] Routing to most recent: ${mostRecent}`);
|
|
534
|
+
this.resolveResponseForSession(mostRecent, text);
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
this.bot.on('polling_error', (error) => {
|
|
539
|
+
console.error('[MultiTelegram] Polling error:', error.message);
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private setupCallbackHandler(): void {
|
|
544
|
+
this.bot.on('callback_query', async (query) => {
|
|
545
|
+
if (!query.data || !query.message) return;
|
|
546
|
+
|
|
547
|
+
const [action, messageId] = query.data.split(':');
|
|
548
|
+
const key = messageId;
|
|
549
|
+
|
|
550
|
+
const pending = this.pendingPermissions.get(key);
|
|
551
|
+
if (!pending) {
|
|
552
|
+
await this.bot.answerCallbackQuery(query.id, { text: 'Request expired' });
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const decision: PermissionDecision = {
|
|
557
|
+
behavior: action === 'allow' ? 'allow' : 'deny',
|
|
558
|
+
message: action === 'deny' ? 'Denied by user via Telegram' : undefined,
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
const statusEmoji = action === 'allow' ? '✅' : '❌';
|
|
562
|
+
const statusText = action === 'allow' ? 'Allowed' : 'Denied';
|
|
563
|
+
|
|
564
|
+
try {
|
|
565
|
+
await this.bot.editMessageReplyMarkup(
|
|
566
|
+
{ inline_keyboard: [] },
|
|
567
|
+
{ chat_id: query.message.chat.id, message_id: query.message.message_id }
|
|
568
|
+
);
|
|
569
|
+
await this.bot.editMessageText(
|
|
570
|
+
`${query.message.text}\n\n${statusEmoji} ${statusText}`,
|
|
571
|
+
{ chat_id: query.message.chat.id, message_id: query.message.message_id }
|
|
572
|
+
);
|
|
573
|
+
} catch {
|
|
574
|
+
// Ignore edit errors
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
await this.bot.answerCallbackQuery(query.id, { text: `${statusText}!` });
|
|
578
|
+
pending.resolve(decision);
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
private resolveResponseForSession(sessionId: string, text: string): void {
|
|
583
|
+
// Find pending response for this session
|
|
584
|
+
for (const [key, pending] of this.pendingResponses) {
|
|
585
|
+
if (pending.sessionId === sessionId) {
|
|
586
|
+
pending.resolve(text);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Also check hook-based pending responses
|
|
592
|
+
for (const [key, pending] of this.pendingResponses) {
|
|
593
|
+
if (pending.sessionId === 'hook') {
|
|
594
|
+
pending.resolve(text);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
private findMostRecentWaitingSession(): string | null {
|
|
601
|
+
let mostRecent: PendingResponse | null = null;
|
|
602
|
+
|
|
603
|
+
for (const pending of this.pendingResponses.values()) {
|
|
604
|
+
if (!mostRecent || pending.timestamp > mostRecent.timestamp) {
|
|
605
|
+
mostRecent = pending;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return mostRecent?.sessionId || null;
|
|
610
|
+
}
|
|
611
|
+
}
|