beecork 1.7.1 → 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/README.md +52 -111
- package/dist/index.js +1394 -581
- package/package.json +26 -54
- package/dist/capabilities/index.d.ts +0 -3
- package/dist/capabilities/index.js +0 -2
- package/dist/capabilities/manager.d.ts +0 -13
- package/dist/capabilities/manager.js +0 -115
- package/dist/capabilities/packs.d.ts +0 -2
- package/dist/capabilities/packs.js +0 -54
- package/dist/capabilities/types.d.ts +0 -21
- package/dist/capabilities/types.js +0 -1
- package/dist/channels/admin.d.ts +0 -10
- package/dist/channels/admin.js +0 -20
- package/dist/channels/command-handler.d.ts +0 -19
- package/dist/channels/command-handler.js +0 -235
- package/dist/channels/discord.d.ts +0 -17
- package/dist/channels/discord.js +0 -266
- package/dist/channels/index.d.ts +0 -7
- package/dist/channels/index.js +0 -6
- package/dist/channels/loader.d.ts +0 -7
- package/dist/channels/loader.js +0 -87
- package/dist/channels/pipeline.d.ts +0 -50
- package/dist/channels/pipeline.js +0 -92
- package/dist/channels/registry.d.ts +0 -36
- package/dist/channels/registry.js +0 -80
- package/dist/channels/send-helpers.d.ts +0 -19
- package/dist/channels/send-helpers.js +0 -21
- package/dist/channels/telegram.d.ts +0 -47
- package/dist/channels/telegram.js +0 -649
- package/dist/channels/types.d.ts +0 -41
- package/dist/channels/types.js +0 -1
- package/dist/channels/voice-state.d.ts +0 -29
- package/dist/channels/voice-state.js +0 -45
- package/dist/channels/webhook.d.ts +0 -16
- package/dist/channels/webhook.js +0 -222
- package/dist/channels/whatsapp.d.ts +0 -28
- package/dist/channels/whatsapp.js +0 -323
- package/dist/cli/capabilities.d.ts +0 -3
- package/dist/cli/capabilities.js +0 -72
- package/dist/cli/channel.d.ts +0 -3
- package/dist/cli/channel.js +0 -202
- package/dist/cli/commands.d.ts +0 -15
- package/dist/cli/commands.js +0 -279
- package/dist/cli/doctor.d.ts +0 -1
- package/dist/cli/doctor.js +0 -208
- package/dist/cli/handoff.d.ts +0 -8
- package/dist/cli/handoff.js +0 -33
- package/dist/cli/helpers.d.ts +0 -2
- package/dist/cli/helpers.js +0 -21
- package/dist/cli/mcp.d.ts +0 -3
- package/dist/cli/mcp.js +0 -47
- package/dist/cli/media.d.ts +0 -2
- package/dist/cli/media.js +0 -125
- package/dist/cli/setup.d.ts +0 -1
- package/dist/cli/setup.js +0 -239
- package/dist/cli/store.d.ts +0 -3
- package/dist/cli/store.js +0 -108
- package/dist/config.d.ts +0 -11
- package/dist/config.js +0 -115
- package/dist/cron/scheduler.d.ts +0 -22
- package/dist/cron/scheduler.js +0 -220
- package/dist/cron/store.d.ts +0 -12
- package/dist/cron/store.js +0 -83
- package/dist/daemon.d.ts +0 -1
- package/dist/daemon.js +0 -345
- package/dist/dashboard/html.d.ts +0 -1
- package/dist/dashboard/html.js +0 -849
- package/dist/dashboard/routes.d.ts +0 -17
- package/dist/dashboard/routes.js +0 -623
- package/dist/dashboard/server.d.ts +0 -1
- package/dist/dashboard/server.js +0 -104
- package/dist/db/connection.d.ts +0 -29
- package/dist/db/connection.js +0 -37
- package/dist/db/index.d.ts +0 -14
- package/dist/db/index.js +0 -127
- package/dist/db/migrations.d.ts +0 -2
- package/dist/db/migrations.js +0 -384
- package/dist/delegation/manager.d.ts +0 -18
- package/dist/delegation/manager.js +0 -89
- package/dist/index.d.ts +0 -2
- package/dist/knowledge/index.d.ts +0 -2
- package/dist/knowledge/index.js +0 -1
- package/dist/knowledge/manager.d.ts +0 -17
- package/dist/knowledge/manager.js +0 -160
- package/dist/knowledge/types.d.ts +0 -7
- package/dist/knowledge/types.js +0 -1
- package/dist/machines/forwarder.d.ts +0 -7
- package/dist/machines/forwarder.js +0 -35
- package/dist/machines/index.d.ts +0 -1
- package/dist/machines/index.js +0 -1
- package/dist/machines/registry.d.ts +0 -15
- package/dist/machines/registry.js +0 -46
- package/dist/mcp/handlers.d.ts +0 -37
- package/dist/mcp/handlers.js +0 -520
- package/dist/mcp/server.d.ts +0 -2
- package/dist/mcp/server.js +0 -86
- package/dist/mcp/tool-definitions.d.ts +0 -1225
- package/dist/mcp/tool-definitions.js +0 -412
- package/dist/mcp/validate.d.ts +0 -23
- package/dist/mcp/validate.js +0 -65
- package/dist/media/factory.d.ts +0 -6
- package/dist/media/factory.js +0 -36
- package/dist/media/generators/dall-e.d.ts +0 -10
- package/dist/media/generators/dall-e.js +0 -41
- package/dist/media/generators/elevenlabs-music.d.ts +0 -9
- package/dist/media/generators/elevenlabs-music.js +0 -34
- package/dist/media/generators/elevenlabs-sfx.d.ts +0 -9
- package/dist/media/generators/elevenlabs-sfx.js +0 -33
- package/dist/media/generators/kling.d.ts +0 -9
- package/dist/media/generators/kling.js +0 -47
- package/dist/media/generators/lyria.d.ts +0 -10
- package/dist/media/generators/lyria.js +0 -41
- package/dist/media/generators/nano-banana.d.ts +0 -10
- package/dist/media/generators/nano-banana.js +0 -45
- package/dist/media/generators/poll-util.d.ts +0 -12
- package/dist/media/generators/poll-util.js +0 -22
- package/dist/media/generators/recraft.d.ts +0 -9
- package/dist/media/generators/recraft.js +0 -52
- package/dist/media/generators/runway.d.ts +0 -9
- package/dist/media/generators/runway.js +0 -49
- package/dist/media/generators/stable-diffusion.d.ts +0 -9
- package/dist/media/generators/stable-diffusion.js +0 -35
- package/dist/media/generators/veo.d.ts +0 -9
- package/dist/media/generators/veo.js +0 -32
- package/dist/media/index.d.ts +0 -6
- package/dist/media/index.js +0 -8
- package/dist/media/loader.d.ts +0 -8
- package/dist/media/loader.js +0 -46
- package/dist/media/store.d.ts +0 -17
- package/dist/media/store.js +0 -69
- package/dist/media/types.d.ts +0 -43
- package/dist/media/types.js +0 -1
- package/dist/memory/extractor.d.ts +0 -5
- package/dist/memory/extractor.js +0 -157
- package/dist/notifications/index.d.ts +0 -4
- package/dist/notifications/index.js +0 -21
- package/dist/notifications/ntfy.d.ts +0 -9
- package/dist/notifications/ntfy.js +0 -25
- package/dist/notifications/pushover.d.ts +0 -9
- package/dist/notifications/pushover.js +0 -26
- package/dist/notifications/types.d.ts +0 -5
- package/dist/notifications/types.js +0 -1
- package/dist/notifications/webhook-provider.d.ts +0 -9
- package/dist/notifications/webhook-provider.js +0 -24
- package/dist/observability/analytics.d.ts +0 -30
- package/dist/observability/analytics.js +0 -118
- package/dist/pipe/anthropic-client.d.ts +0 -9
- package/dist/pipe/anthropic-client.js +0 -50
- package/dist/pipe/brain.d.ts +0 -16
- package/dist/pipe/brain.js +0 -89
- package/dist/pipe/memory-store.d.ts +0 -8
- package/dist/pipe/memory-store.js +0 -39
- package/dist/pipe/project-scanner.d.ts +0 -6
- package/dist/pipe/project-scanner.js +0 -26
- package/dist/pipe/types.d.ts +0 -34
- package/dist/pipe/types.js +0 -1
- package/dist/projects/index.d.ts +0 -4
- package/dist/projects/index.js +0 -2
- package/dist/projects/manager.d.ts +0 -13
- package/dist/projects/manager.js +0 -166
- package/dist/projects/router.d.ts +0 -17
- package/dist/projects/router.js +0 -246
- package/dist/projects/types.d.ts +0 -19
- package/dist/projects/types.js +0 -1
- package/dist/service/install.d.ts +0 -4
- package/dist/service/install.js +0 -146
- package/dist/service/templates.d.ts +0 -4
- package/dist/service/templates.js +0 -52
- package/dist/service/windows.d.ts +0 -8
- package/dist/service/windows.js +0 -59
- package/dist/session/budget-guard.d.ts +0 -20
- package/dist/session/budget-guard.js +0 -31
- package/dist/session/circuit-breaker.d.ts +0 -19
- package/dist/session/circuit-breaker.js +0 -91
- package/dist/session/context-compactor.d.ts +0 -32
- package/dist/session/context-compactor.js +0 -45
- package/dist/session/context-monitor.d.ts +0 -15
- package/dist/session/context-monitor.js +0 -44
- package/dist/session/handoff.d.ts +0 -21
- package/dist/session/handoff.js +0 -50
- package/dist/session/manager.d.ts +0 -60
- package/dist/session/manager.js +0 -416
- package/dist/session/memory-store.d.ts +0 -29
- package/dist/session/memory-store.js +0 -45
- package/dist/session/message-queue.d.ts +0 -28
- package/dist/session/message-queue.js +0 -52
- package/dist/session/pending-dispatcher.d.ts +0 -31
- package/dist/session/pending-dispatcher.js +0 -120
- package/dist/session/pending-store.d.ts +0 -60
- package/dist/session/pending-store.js +0 -118
- package/dist/session/stale-session.d.ts +0 -31
- package/dist/session/stale-session.js +0 -45
- package/dist/session/subprocess.d.ts +0 -32
- package/dist/session/subprocess.js +0 -230
- package/dist/session/tab-store.d.ts +0 -28
- package/dist/session/tab-store.js +0 -78
- package/dist/session/tool-classifier.d.ts +0 -4
- package/dist/session/tool-classifier.js +0 -56
- package/dist/tasks/scheduler.d.ts +0 -45
- package/dist/tasks/scheduler.js +0 -329
- package/dist/tasks/store.d.ts +0 -11
- package/dist/tasks/store.js +0 -103
- package/dist/timeline/index.d.ts +0 -3
- package/dist/timeline/index.js +0 -2
- package/dist/timeline/logger.d.ts +0 -8
- package/dist/timeline/logger.js +0 -11
- package/dist/timeline/query.d.ts +0 -11
- package/dist/timeline/query.js +0 -67
- package/dist/timeline/types.d.ts +0 -12
- package/dist/timeline/types.js +0 -1
- package/dist/types.d.ts +0 -225
- package/dist/types.js +0 -3
- package/dist/users/index.d.ts +0 -2
- package/dist/users/index.js +0 -1
- package/dist/users/service.d.ts +0 -17
- package/dist/users/service.js +0 -46
- package/dist/util/auto-heal.d.ts +0 -43
- package/dist/util/auto-heal.js +0 -107
- package/dist/util/install-info.d.ts +0 -37
- package/dist/util/install-info.js +0 -80
- package/dist/util/logger.d.ts +0 -18
- package/dist/util/logger.js +0 -118
- package/dist/util/paths.d.ts +0 -13
- package/dist/util/paths.js +0 -55
- package/dist/util/platform.d.ts +0 -2
- package/dist/util/platform.js +0 -11
- package/dist/util/progress.d.ts +0 -34
- package/dist/util/progress.js +0 -133
- package/dist/util/rate-limiter.d.ts +0 -14
- package/dist/util/rate-limiter.js +0 -50
- package/dist/util/retry.d.ts +0 -2
- package/dist/util/retry.js +0 -18
- package/dist/util/text.d.ts +0 -32
- package/dist/util/text.js +0 -101
- package/dist/version.d.ts +0 -1
- package/dist/version.js +0 -4
- package/dist/voice/index.d.ts +0 -17
- package/dist/voice/index.js +0 -42
- package/dist/voice/stt.d.ts +0 -16
- package/dist/voice/stt.js +0 -57
- package/dist/voice/tts.d.ts +0 -23
- package/dist/voice/tts.js +0 -84
- package/dist/watchers/evaluator.d.ts +0 -5
- package/dist/watchers/evaluator.js +0 -45
- package/dist/watchers/index.d.ts +0 -4
- package/dist/watchers/index.js +0 -3
- package/dist/watchers/scheduler.d.ts +0 -20
- package/dist/watchers/scheduler.js +0 -183
- package/dist/watchers/store.d.ts +0 -10
- package/dist/watchers/store.js +0 -56
- package/dist/watchers/types.d.ts +0 -15
- package/dist/watchers/types.js +0 -1
- package/scripts/postinstall.mjs +0 -50
|
@@ -1,649 +0,0 @@
|
|
|
1
|
-
import TelegramBot from 'node-telegram-bot-api';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { chunkText, parseTabMessage, formatTabbedResponse } from '../util/text.js';
|
|
5
|
-
import { logger } from '../util/logger.js';
|
|
6
|
-
import { retryWithBackoff } from '../util/retry.js';
|
|
7
|
-
import { sendChunkedResponse } from './send-helpers.js';
|
|
8
|
-
import { getLogsDir } from '../util/paths.js';
|
|
9
|
-
import { saveMedia, isOversized } from '../media/store.js';
|
|
10
|
-
import { inboundLimiter, groupLimiter } from '../util/rate-limiter.js';
|
|
11
|
-
import { processInboundMessage } from './pipeline.js';
|
|
12
|
-
import { isChannelAdmin } from './admin.js';
|
|
13
|
-
import { VoiceState } from './voice-state.js';
|
|
14
|
-
const DEFAULT_GROUP_CONFIG = {
|
|
15
|
-
activationMode: 'mention',
|
|
16
|
-
maxResponsesPerMinute: 3,
|
|
17
|
-
tabPerGroup: true,
|
|
18
|
-
};
|
|
19
|
-
/**
|
|
20
|
-
* Strip Telegram bot tokens out of strings before logging. Telegram embeds
|
|
21
|
-
* the token in the URL path (e.g. https://api.telegram.org/bot1234:abc.../method),
|
|
22
|
-
* so on fetch errors the message/cause can leak the token to disk.
|
|
23
|
-
*/
|
|
24
|
-
function sanitizeBotToken(text) {
|
|
25
|
-
return text.replace(/bot\d+:[A-Za-z0-9_-]+/g, 'bot<REDACTED>');
|
|
26
|
-
}
|
|
27
|
-
export class TelegramChannel {
|
|
28
|
-
id = 'telegram';
|
|
29
|
-
name = 'Telegram';
|
|
30
|
-
maxMessageLength = 4096;
|
|
31
|
-
bot;
|
|
32
|
-
ctx;
|
|
33
|
-
activeChatIds = new Set();
|
|
34
|
-
// Set form of config.telegram.allowedUserIds for O(1) per-message membership
|
|
35
|
-
// checks. Built lazily in start() so config edits via reload (if added later)
|
|
36
|
-
// can call rebuildAllowedSet().
|
|
37
|
-
allowedUserIdSet = new Set();
|
|
38
|
-
voice = new VoiceState('telegram');
|
|
39
|
-
botUserId = null;
|
|
40
|
-
botUsername = null;
|
|
41
|
-
mutedGroups = new Set();
|
|
42
|
-
welcomeSent = new Set();
|
|
43
|
-
// Polling-error tracking: warn the user once when the bot is silently broken
|
|
44
|
-
// (network drop, telegram 5xx, token revoked, etc.). Without this, the daemon
|
|
45
|
-
// looks fine but Telegram has stopped delivering inbound messages.
|
|
46
|
-
pollingErrorTimes = [];
|
|
47
|
-
pollingDegradedNotified = false;
|
|
48
|
-
constructor(ctx) {
|
|
49
|
-
this.ctx = ctx;
|
|
50
|
-
this.bot = new TelegramBot(ctx.config.telegram.token, {
|
|
51
|
-
polling: {
|
|
52
|
-
params: {
|
|
53
|
-
timeout: 30,
|
|
54
|
-
allowed_updates: ['message', 'callback_query'],
|
|
55
|
-
},
|
|
56
|
-
autoStart: false,
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
this.bot.sendMessage = this.bot.sendMessage.bind(this.bot);
|
|
60
|
-
}
|
|
61
|
-
async start() {
|
|
62
|
-
// Clear pending updates from old sessions, then start polling.
|
|
63
|
-
// node-telegram-bot-api's TS types don't include deleteWebHook, so we go
|
|
64
|
-
// through a typed shim rather than `as any`.
|
|
65
|
-
try {
|
|
66
|
-
await this.bot.deleteWebHook({ drop_pending_updates: true });
|
|
67
|
-
}
|
|
68
|
-
catch (err) {
|
|
69
|
-
logger.error('Failed to clear pending updates, starting anyway:', err);
|
|
70
|
-
}
|
|
71
|
-
// Initialize voice providers (STT + TTS)
|
|
72
|
-
this.voice.init(this.ctx.config);
|
|
73
|
-
// Subscribe to library-level error events BEFORE startPolling so transient
|
|
74
|
-
// failures don't disappear into the void. node-telegram-bot-api emits
|
|
75
|
-
// 'polling_error' on network/auth/5xx issues — without a listener these
|
|
76
|
-
// were previously silently dropped, leaving the channel dead with no signal.
|
|
77
|
-
this.bot.on('polling_error', (err) => {
|
|
78
|
-
logger.error('Telegram polling error:', sanitizeBotToken(err?.message || String(err)));
|
|
79
|
-
this.recordPollingError();
|
|
80
|
-
});
|
|
81
|
-
this.bot.on('error', (err) => {
|
|
82
|
-
logger.error('Telegram client error:', sanitizeBotToken(err?.message || String(err)));
|
|
83
|
-
});
|
|
84
|
-
this.bot.startPolling();
|
|
85
|
-
this.allowedUserIdSet = new Set(this.ctx.config.telegram.allowedUserIds);
|
|
86
|
-
if (this.allowedUserIdSet.size === 0) {
|
|
87
|
-
logger.warn('Telegram allowedUserIds is empty — bot will reject all inbound messages until you add at least one user ID.');
|
|
88
|
-
}
|
|
89
|
-
// Cache bot identity for group mention detection
|
|
90
|
-
try {
|
|
91
|
-
const me = await this.bot.getMe();
|
|
92
|
-
this.botUserId = me.id;
|
|
93
|
-
this.botUsername = me.username ?? null;
|
|
94
|
-
}
|
|
95
|
-
catch (err) {
|
|
96
|
-
logger.warn('Failed to fetch bot identity (group mentions may not work):', err);
|
|
97
|
-
}
|
|
98
|
-
this.setupHandlers();
|
|
99
|
-
logger.info('Telegram bot started (polling mode, cleared pending updates)');
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Track polling errors over a 60s rolling window. If we see 5+ errors in 60s
|
|
103
|
-
* we surface "polling degraded" exactly once via the notify callback so the
|
|
104
|
-
* user knows Telegram is broken instead of silently failing.
|
|
105
|
-
*/
|
|
106
|
-
recordPollingError() {
|
|
107
|
-
const now = Date.now();
|
|
108
|
-
this.pollingErrorTimes.push(now);
|
|
109
|
-
this.pollingErrorTimes = this.pollingErrorTimes.filter((t) => now - t < 60_000);
|
|
110
|
-
if (this.pollingErrorTimes.length >= 5 && !this.pollingDegradedNotified) {
|
|
111
|
-
this.pollingDegradedNotified = true;
|
|
112
|
-
this.ctx
|
|
113
|
-
.notifyCallback?.('⚠️ Telegram polling degraded (5+ errors in 60s). Check daemon.log.')
|
|
114
|
-
.catch((err) => logger.warn('Failed to send polling-degraded notice:', err));
|
|
115
|
-
// Reset notification flag after 5 minutes so a sustained outage that
|
|
116
|
-
// recovers and reoccurs can re-notify.
|
|
117
|
-
setTimeout(() => {
|
|
118
|
-
this.pollingDegradedNotified = false;
|
|
119
|
-
}, 5 * 60_000).unref();
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
stop() {
|
|
123
|
-
this.bot.stopPolling();
|
|
124
|
-
logger.info('Telegram bot stopped');
|
|
125
|
-
}
|
|
126
|
-
async sendMessage(peerId, text, _options) {
|
|
127
|
-
const chatId = Number(peerId);
|
|
128
|
-
const chunks = chunkText(text);
|
|
129
|
-
for (const chunk of chunks) {
|
|
130
|
-
await this.sendWithRetry(chatId, chunk);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
async sendNotification(message, _urgent) {
|
|
134
|
-
for (const chatId of this.activeChatIds) {
|
|
135
|
-
try {
|
|
136
|
-
await this.bot.sendMessage(chatId, message);
|
|
137
|
-
}
|
|
138
|
-
catch (err) {
|
|
139
|
-
logger.error(`Failed to send notification to chat ${chatId}:`, err);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
for (const userId of this.ctx.config.telegram.allowedUserIds) {
|
|
143
|
-
if (this.activeChatIds.has(userId))
|
|
144
|
-
continue;
|
|
145
|
-
try {
|
|
146
|
-
await this.bot.sendMessage(userId, message);
|
|
147
|
-
this.activeChatIds.add(userId);
|
|
148
|
-
}
|
|
149
|
-
catch (err) {
|
|
150
|
-
// Differentiate: 400 "chat not found" means the user has not started a
|
|
151
|
-
// conversation with the bot yet — silently skip. Anything else (rate
|
|
152
|
-
// limit, bot blocked, network) is a real delivery failure worth logging.
|
|
153
|
-
const errAny = err;
|
|
154
|
-
const status = errAny?.response?.statusCode;
|
|
155
|
-
const isChatNotFound = status === 400 || /chat not found/i.test(errAny?.message || '');
|
|
156
|
-
if (!isChatNotFound) {
|
|
157
|
-
logger.warn(`Telegram notify to ${userId} failed (status=${status ?? '?'}):`, sanitizeBotToken(errAny?.message || String(err)));
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Send a media file to every recipient the bot would notify. Used by the
|
|
164
|
-
* pending-message dispatcher to deliver media queued by MCP tools.
|
|
165
|
-
* Routes by attachment type to the right Telegram primitive (sendVoice for
|
|
166
|
-
* voice messages, sendPhoto for images, sendVideo for video, sendDocument
|
|
167
|
-
* for everything else).
|
|
168
|
-
*/
|
|
169
|
-
async broadcastMedia(media) {
|
|
170
|
-
const recipients = new Set(this.activeChatIds);
|
|
171
|
-
for (const userId of this.ctx.config.telegram.allowedUserIds)
|
|
172
|
-
recipients.add(userId);
|
|
173
|
-
if (recipients.size === 0)
|
|
174
|
-
return;
|
|
175
|
-
const caption = media.caption ? { caption: media.caption.slice(0, 1024) } : undefined;
|
|
176
|
-
for (const chatId of recipients) {
|
|
177
|
-
try {
|
|
178
|
-
switch (media.type) {
|
|
179
|
-
case 'voice':
|
|
180
|
-
await this.bot.sendVoice(chatId, media.filePath, caption);
|
|
181
|
-
break;
|
|
182
|
-
case 'audio':
|
|
183
|
-
await this.bot.sendAudio(chatId, media.filePath, caption);
|
|
184
|
-
break;
|
|
185
|
-
case 'image':
|
|
186
|
-
await this.bot.sendPhoto(chatId, media.filePath, caption);
|
|
187
|
-
break;
|
|
188
|
-
case 'video':
|
|
189
|
-
await this.bot.sendVideo(chatId, media.filePath, caption);
|
|
190
|
-
break;
|
|
191
|
-
case 'document':
|
|
192
|
-
default:
|
|
193
|
-
await this.bot.sendDocument(chatId, media.filePath, caption);
|
|
194
|
-
break;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
catch (err) {
|
|
198
|
-
logger.warn(`Telegram broadcastMedia to ${chatId} failed:`, sanitizeBotToken(err instanceof Error ? err.message : String(err)));
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
async setTyping(peerId, active) {
|
|
203
|
-
if (active) {
|
|
204
|
-
await this.bot.sendChatAction(Number(peerId), 'typing').catch((err) => {
|
|
205
|
-
logger.error(`Typing indicator failed for chat ${peerId}:`, err);
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
// ─── Private ───
|
|
210
|
-
setupHandlers() {
|
|
211
|
-
this.bot.on('message', async (msg) => {
|
|
212
|
-
if (!this.isAllowed(msg.from?.id))
|
|
213
|
-
return;
|
|
214
|
-
const chatId = msg.chat.id;
|
|
215
|
-
// Rate limit check
|
|
216
|
-
if (!inboundLimiter.check(this.id)) {
|
|
217
|
-
await this.bot.sendMessage(chatId, "I'm receiving too many messages right now. Please wait a moment.");
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
const isGroup = msg.chat.type === 'group' || msg.chat.type === 'supergroup';
|
|
221
|
-
// First-run welcome message (once per user)
|
|
222
|
-
if (msg.chat.type === 'private' && !this.welcomeSent.has(chatId)) {
|
|
223
|
-
const db = (await import('../db/index.js')).getDb();
|
|
224
|
-
const hasMessages = db.prepare('SELECT COUNT(*) as c FROM messages').get();
|
|
225
|
-
if (hasMessages.c === 0) {
|
|
226
|
-
this.welcomeSent.add(chatId);
|
|
227
|
-
const welcomeText = msg.text?.trim() || '';
|
|
228
|
-
await this.bot.sendMessage(chatId, [
|
|
229
|
-
'\uD83D\uDC4B Welcome to Beecork!\n',
|
|
230
|
-
"Send any message and I'll pass it to Claude Code.",
|
|
231
|
-
'',
|
|
232
|
-
'Quick tips:',
|
|
233
|
-
'\u2022 /tab name message \u2014 organize work into tabs',
|
|
234
|
-
"\u2022 /tabs \u2014 see what's running",
|
|
235
|
-
'\u2022 /stop name \u2014 stop a tab',
|
|
236
|
-
'',
|
|
237
|
-
"Let's get started! Send me something.",
|
|
238
|
-
].join('\n'));
|
|
239
|
-
// Don't return - let the actual message be processed too (unless it was just /start)
|
|
240
|
-
if (welcomeText === '/start')
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
else {
|
|
244
|
-
this.welcomeSent.add(chatId);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
// Only add to activeChatIds for private chats
|
|
248
|
-
if (!isGroup) {
|
|
249
|
-
this.activeChatIds.add(chatId);
|
|
250
|
-
}
|
|
251
|
-
// Extract text (from text, caption, etc.)
|
|
252
|
-
let text = msg.text?.trim() || msg.caption?.trim() || '';
|
|
253
|
-
// ─── Group activation logic ───
|
|
254
|
-
if (isGroup) {
|
|
255
|
-
// Check muted status
|
|
256
|
-
if (this.mutedGroups.has(chatId))
|
|
257
|
-
return;
|
|
258
|
-
const groupConfig = this.ctx.config.groups || DEFAULT_GROUP_CONFIG;
|
|
259
|
-
const isMentioned = this.botUsername ? text.includes(`@${this.botUsername}`) : false;
|
|
260
|
-
const isReplyToBot = msg.reply_to_message?.from?.id === this.botUserId;
|
|
261
|
-
let shouldActivate = false;
|
|
262
|
-
switch (groupConfig.activationMode) {
|
|
263
|
-
case 'mention':
|
|
264
|
-
shouldActivate = !!isMentioned;
|
|
265
|
-
break;
|
|
266
|
-
case 'reply':
|
|
267
|
-
shouldActivate = !!isReplyToBot;
|
|
268
|
-
break;
|
|
269
|
-
case 'keyword':
|
|
270
|
-
shouldActivate =
|
|
271
|
-
groupConfig.keywords?.some((kw) => text.toLowerCase().includes(kw.toLowerCase())) ??
|
|
272
|
-
false;
|
|
273
|
-
break;
|
|
274
|
-
case 'always':
|
|
275
|
-
shouldActivate = true;
|
|
276
|
-
break;
|
|
277
|
-
}
|
|
278
|
-
if (!shouldActivate)
|
|
279
|
-
return;
|
|
280
|
-
// Group rate limiting
|
|
281
|
-
const groupKey = `group:${chatId}`;
|
|
282
|
-
if (!groupLimiter.check(groupKey)) {
|
|
283
|
-
// Silently ignore — don't spam the group with rate limit messages
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
// Clean mention from text
|
|
287
|
-
if (isMentioned && this.botUsername) {
|
|
288
|
-
text = text.replace(new RegExp(`@${this.botUsername}`, 'gi'), '').trim();
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
// Track voice pipeline timing
|
|
292
|
-
const voicePipelineStart = msg.voice ? Date.now() : null;
|
|
293
|
-
// Download media if present (in parallel)
|
|
294
|
-
const downloadTasks = [];
|
|
295
|
-
if (msg.photo) {
|
|
296
|
-
const photo = msg.photo[msg.photo.length - 1];
|
|
297
|
-
downloadTasks.push(this.downloadTelegramFile(photo.file_id, 'jpg')
|
|
298
|
-
.then((fp) => fp
|
|
299
|
-
? {
|
|
300
|
-
type: 'image',
|
|
301
|
-
mimeType: 'image/jpeg',
|
|
302
|
-
filePath: fp,
|
|
303
|
-
fileName: `photo-${photo.file_id}.jpg`,
|
|
304
|
-
}
|
|
305
|
-
: null)
|
|
306
|
-
.catch(() => null));
|
|
307
|
-
}
|
|
308
|
-
if (msg.voice) {
|
|
309
|
-
downloadTasks.push(this.downloadTelegramFile(msg.voice.file_id, 'ogg')
|
|
310
|
-
.then((fp) => fp ? { type: 'voice', mimeType: 'audio/ogg', filePath: fp } : null)
|
|
311
|
-
.catch(() => null));
|
|
312
|
-
}
|
|
313
|
-
if (msg.audio) {
|
|
314
|
-
downloadTasks.push(this.downloadTelegramFile(msg.audio.file_id, 'mp3')
|
|
315
|
-
.then((fp) => fp
|
|
316
|
-
? {
|
|
317
|
-
type: 'audio',
|
|
318
|
-
mimeType: msg.audio.mime_type || 'audio/mpeg',
|
|
319
|
-
filePath: fp,
|
|
320
|
-
fileName: msg.audio.title,
|
|
321
|
-
}
|
|
322
|
-
: null)
|
|
323
|
-
.catch(() => null));
|
|
324
|
-
}
|
|
325
|
-
if (msg.document) {
|
|
326
|
-
const ext = msg.document.file_name?.split('.').pop() || 'bin';
|
|
327
|
-
downloadTasks.push(this.downloadTelegramFile(msg.document.file_id, ext)
|
|
328
|
-
.then((fp) => fp
|
|
329
|
-
? {
|
|
330
|
-
type: 'document',
|
|
331
|
-
mimeType: msg.document.mime_type || 'application/octet-stream',
|
|
332
|
-
filePath: fp,
|
|
333
|
-
fileName: msg.document.file_name,
|
|
334
|
-
}
|
|
335
|
-
: null)
|
|
336
|
-
.catch(() => null));
|
|
337
|
-
}
|
|
338
|
-
if (msg.video) {
|
|
339
|
-
downloadTasks.push(this.downloadTelegramFile(msg.video.file_id, 'mp4')
|
|
340
|
-
.then((fp) => fp
|
|
341
|
-
? {
|
|
342
|
-
type: 'video',
|
|
343
|
-
mimeType: msg.video.mime_type || 'video/mp4',
|
|
344
|
-
filePath: fp,
|
|
345
|
-
}
|
|
346
|
-
: null)
|
|
347
|
-
.catch(() => null));
|
|
348
|
-
}
|
|
349
|
-
const downloadResults = await Promise.allSettled(downloadTasks);
|
|
350
|
-
const media = downloadResults
|
|
351
|
-
.filter((r) => r.status === 'fulfilled' && r.value !== null)
|
|
352
|
-
.map((r) => r.value);
|
|
353
|
-
// Transcribe voice messages if STT is configured
|
|
354
|
-
await this.voice.transcribe(media);
|
|
355
|
-
// Skip if no text AND no media
|
|
356
|
-
if (!text && media.length === 0)
|
|
357
|
-
return;
|
|
358
|
-
try {
|
|
359
|
-
// Commands bypass debouncing (only if pure text, no media)
|
|
360
|
-
if (text.startsWith('/') && media.length === 0) {
|
|
361
|
-
await this.handleCommand(chatId, text, msg.from?.id, msg.message_id, isGroup);
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
// Send typing indicator immediately
|
|
365
|
-
this.bot.sendChatAction(chatId, 'typing').catch((err) => {
|
|
366
|
-
logger.error(`Typing indicator failed for chat ${chatId}:`, err);
|
|
367
|
-
});
|
|
368
|
-
logger.info(`[telegram] Message received from ${msg.from?.id}, sending typing`);
|
|
369
|
-
await this.handleMessage(chatId, text, msg.message_id, media, isGroup);
|
|
370
|
-
if (voicePipelineStart) {
|
|
371
|
-
logger.info(`[telegram] Voice-to-response total: ${Date.now() - voicePipelineStart}ms`);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
catch (err) {
|
|
375
|
-
logger.error('Telegram: error handling message:', err);
|
|
376
|
-
// Wrap the fallback send so a Telegram outage doesn't escalate to an
|
|
377
|
-
// unhandledRejection on the message-event handler.
|
|
378
|
-
this.bot
|
|
379
|
-
.sendMessage(chatId, 'Something went wrong processing your message. Check daemon logs for details.')
|
|
380
|
-
.catch((sendErr) => logger.error('Telegram: failed to send fallback error message:', sendErr));
|
|
381
|
-
}
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
async handleCommand(chatId, text, userId, messageId, isGroup = false) {
|
|
385
|
-
// Telegram-only group commands
|
|
386
|
-
if (text === '/mute' && isGroup) {
|
|
387
|
-
this.mutedGroups.add(chatId);
|
|
388
|
-
await this.bot.sendMessage(chatId, 'Beecork muted in this group. Use /unmute to re-enable.');
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
if (text === '/unmute' && isGroup) {
|
|
392
|
-
this.mutedGroups.delete(chatId);
|
|
393
|
-
await this.bot.sendMessage(chatId, 'Beecork unmuted in this group.');
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
// /history and /knowledge now handled by the shared command handler.
|
|
397
|
-
// Shared command handler (covers /tabs, /stop, /tab, /projects, /project, /newproject, /close, /fresh, /cost, /activity, /handoff, /history, /knowledge)
|
|
398
|
-
const { handleSharedCommand } = await import('./command-handler.js');
|
|
399
|
-
const result = await handleSharedCommand({
|
|
400
|
-
userId: String(userId || 'default'),
|
|
401
|
-
text,
|
|
402
|
-
isAdmin: this.isAdmin(userId),
|
|
403
|
-
channelId: 'telegram',
|
|
404
|
-
}, this.ctx.tabManager);
|
|
405
|
-
if (result.handled) {
|
|
406
|
-
if (result.response)
|
|
407
|
-
await this.bot.sendMessage(chatId, result.response);
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
|
-
// /tab with valid name — falls through from shared handler, treat as message
|
|
411
|
-
if (text.startsWith('/tab ')) {
|
|
412
|
-
await this.handleMessage(chatId, text, messageId);
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
// Unknown command — treat as regular message
|
|
416
|
-
await this.handleMessage(chatId, text, messageId);
|
|
417
|
-
}
|
|
418
|
-
async handleMessage(chatId, text, messageId, media = [], isGroup = false) {
|
|
419
|
-
const { tabName } = parseTabMessage(text);
|
|
420
|
-
if (!tabName && !text && media.length === 0)
|
|
421
|
-
return;
|
|
422
|
-
// Telegram-specific: group tab routing
|
|
423
|
-
let overrideTabName;
|
|
424
|
-
if (isGroup) {
|
|
425
|
-
const groupConfig = this.ctx.config.groups || DEFAULT_GROUP_CONFIG;
|
|
426
|
-
if (groupConfig.tabPerGroup && !text.startsWith('/tab ')) {
|
|
427
|
-
overrideTabName = `group-tg-${Math.abs(chatId)}`;
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
// React with ⏳
|
|
431
|
-
await this.setReaction(chatId, messageId, '⏳');
|
|
432
|
-
// Typing indicator — keep refreshing every 4s
|
|
433
|
-
const sendTyping = () => this.bot.sendChatAction(chatId, 'typing').catch((err) => {
|
|
434
|
-
logger.error(`Typing indicator failed:`, err);
|
|
435
|
-
});
|
|
436
|
-
await sendTyping();
|
|
437
|
-
const typingInterval = setInterval(sendTyping, 4000);
|
|
438
|
-
// "Still working" timeout
|
|
439
|
-
const stillWorkingTimeout = setTimeout(() => {
|
|
440
|
-
this.bot.sendMessage(chatId, `Still working on your request...`).catch(() => { });
|
|
441
|
-
}, 120000);
|
|
442
|
-
try {
|
|
443
|
-
// Telegram-specific: streaming message edits
|
|
444
|
-
let streamMsgId = null;
|
|
445
|
-
let streamBuffer = '';
|
|
446
|
-
let lastEditTime = 0;
|
|
447
|
-
// We need the effective tab name for the stream prefix, but it's determined
|
|
448
|
-
// inside the pipeline. Use a mutable ref that the pipeline result will fill.
|
|
449
|
-
let effectiveTabForStream = overrideTabName || tabName;
|
|
450
|
-
const onTextChunk = async (chunk) => {
|
|
451
|
-
streamBuffer += chunk;
|
|
452
|
-
const now = Date.now();
|
|
453
|
-
if (streamBuffer.length < 100 || now - lastEditTime < 1000)
|
|
454
|
-
return;
|
|
455
|
-
lastEditTime = now;
|
|
456
|
-
try {
|
|
457
|
-
const truncated = streamBuffer.slice(0, 4000) + (streamBuffer.length > 4000 ? '...' : '');
|
|
458
|
-
const preview = formatTabbedResponse(truncated, effectiveTabForStream);
|
|
459
|
-
if (!streamMsgId) {
|
|
460
|
-
const sent = await this.bot.sendMessage(chatId, preview);
|
|
461
|
-
streamMsgId = sent.message_id;
|
|
462
|
-
}
|
|
463
|
-
else {
|
|
464
|
-
await this.bot.editMessageText(preview, { chat_id: chatId, message_id: streamMsgId });
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
catch {
|
|
468
|
-
/* edit failures are non-critical */
|
|
469
|
-
}
|
|
470
|
-
};
|
|
471
|
-
// Shared pipeline handles: routing, media prompt, progress, sendMessage, TTS
|
|
472
|
-
const pipelineResult = await processInboundMessage({
|
|
473
|
-
text,
|
|
474
|
-
media,
|
|
475
|
-
channelId: 'telegram',
|
|
476
|
-
tabManager: this.ctx.tabManager,
|
|
477
|
-
voiceReplyMode: this.ctx.config.voice?.replyMode,
|
|
478
|
-
ttsProvider: this.voice.tts,
|
|
479
|
-
userId: String(chatId),
|
|
480
|
-
sendProgress: (msg) => {
|
|
481
|
-
this.bot.sendMessage(chatId, msg).catch(() => { });
|
|
482
|
-
},
|
|
483
|
-
overrideTabName,
|
|
484
|
-
onTextChunk,
|
|
485
|
-
});
|
|
486
|
-
// Empty result means no prompt and no media
|
|
487
|
-
if (!pipelineResult.responseText) {
|
|
488
|
-
clearInterval(typingInterval);
|
|
489
|
-
clearTimeout(stillWorkingTimeout);
|
|
490
|
-
return;
|
|
491
|
-
}
|
|
492
|
-
// Update the effective tab for stream prefix (now known)
|
|
493
|
-
effectiveTabForStream = pipelineResult.tabName;
|
|
494
|
-
const responseText = pipelineResult.responseText;
|
|
495
|
-
const responseError = pipelineResult.isError;
|
|
496
|
-
const responseTab = pipelineResult.tabName;
|
|
497
|
-
// Telegram-specific: if streaming was active and no error, edit the final message
|
|
498
|
-
if (streamMsgId && !responseError) {
|
|
499
|
-
clearInterval(typingInterval);
|
|
500
|
-
clearTimeout(stillWorkingTimeout);
|
|
501
|
-
await this.setReaction(chatId, messageId, '✅');
|
|
502
|
-
// Send voice if available (even with streaming)
|
|
503
|
-
if (pipelineResult.audioPath) {
|
|
504
|
-
await this.bot.sendVoice(chatId, pipelineResult.audioPath);
|
|
505
|
-
if (pipelineResult.voiceOnly)
|
|
506
|
-
return;
|
|
507
|
-
}
|
|
508
|
-
try {
|
|
509
|
-
const finalText = formatTabbedResponse(responseText, responseTab);
|
|
510
|
-
if (finalText.length <= 4096) {
|
|
511
|
-
await this.bot.editMessageText(finalText, { chat_id: chatId, message_id: streamMsgId });
|
|
512
|
-
}
|
|
513
|
-
else {
|
|
514
|
-
await this.sendResponse(chatId, responseText, responseTab);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
catch {
|
|
518
|
-
await this.sendResponse(chatId, responseText, responseTab);
|
|
519
|
-
}
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
// Send voice reply if TTS generated audio (non-streaming path)
|
|
523
|
-
if (pipelineResult.audioPath) {
|
|
524
|
-
clearInterval(typingInterval);
|
|
525
|
-
clearTimeout(stillWorkingTimeout);
|
|
526
|
-
await this.setReaction(chatId, messageId, responseError ? '❌' : '✅');
|
|
527
|
-
await this.bot.sendVoice(chatId, pipelineResult.audioPath);
|
|
528
|
-
if (pipelineResult.voiceOnly)
|
|
529
|
-
return;
|
|
530
|
-
if (!responseError) {
|
|
531
|
-
await this.sendResponse(chatId, responseText, responseTab);
|
|
532
|
-
return;
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
clearInterval(typingInterval);
|
|
536
|
-
clearTimeout(stillWorkingTimeout);
|
|
537
|
-
if (responseError) {
|
|
538
|
-
await this.setReaction(chatId, messageId, '❌');
|
|
539
|
-
await this.sendResponse(chatId, responseText, responseTab);
|
|
540
|
-
return;
|
|
541
|
-
}
|
|
542
|
-
await this.setReaction(chatId, messageId, '✅');
|
|
543
|
-
await this.sendResponse(chatId, responseText, responseTab);
|
|
544
|
-
}
|
|
545
|
-
catch (err) {
|
|
546
|
-
clearInterval(typingInterval);
|
|
547
|
-
clearTimeout(stillWorkingTimeout);
|
|
548
|
-
await this.setReaction(chatId, messageId, '❌');
|
|
549
|
-
throw err;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
async sendResponse(chatId, text, tabName) {
|
|
553
|
-
const fullText = formatTabbedResponse(text, tabName);
|
|
554
|
-
const chunks = chunkText(fullText);
|
|
555
|
-
// Telegram-specific quirk: if the response would be >10 chunks, send a
|
|
556
|
-
// preview + the rest as a file. This runs BEFORE sendChunkedResponse so the
|
|
557
|
-
// helper is only invoked for normal-sized responses.
|
|
558
|
-
if (chunks.length > 10) {
|
|
559
|
-
for (let i = 0; i < 3; i++) {
|
|
560
|
-
await this.sendWithRetry(chatId, chunks[i]);
|
|
561
|
-
}
|
|
562
|
-
const tmpPath = path.join(getLogsDir(), `response-${Date.now()}.txt`);
|
|
563
|
-
fs.writeFileSync(tmpPath, fullText);
|
|
564
|
-
await this.bot.sendDocument(chatId, tmpPath, {
|
|
565
|
-
caption: `Full response (${chunks.length} chunks)`,
|
|
566
|
-
});
|
|
567
|
-
fs.unlinkSync(tmpPath);
|
|
568
|
-
return;
|
|
569
|
-
}
|
|
570
|
-
// Use the shared chunked-send helper so prefix/chunk/retry logic stays
|
|
571
|
-
// identical across Telegram/Discord/WhatsApp. Quirky per-chunk error
|
|
572
|
-
// logging (delivery-failures.log) stays in sendWithRetry.
|
|
573
|
-
await sendChunkedResponse({
|
|
574
|
-
text,
|
|
575
|
-
tabName,
|
|
576
|
-
maxLength: this.maxMessageLength,
|
|
577
|
-
retryLabel: 'telegram-send',
|
|
578
|
-
sendChunk: (chunk) => this.sendWithRetryRaw(chatId, chunk),
|
|
579
|
-
});
|
|
580
|
-
}
|
|
581
|
-
async sendWithRetry(chatId, text) {
|
|
582
|
-
// Wrapped call used by the >10-chunk fallback path. retryWithBackoff +
|
|
583
|
-
// delivery-failures.log on permanent failure.
|
|
584
|
-
try {
|
|
585
|
-
await this.sendWithRetryRaw(chatId, text);
|
|
586
|
-
}
|
|
587
|
-
catch (err) {
|
|
588
|
-
const failLog = path.join(getLogsDir(), 'delivery-failures.log');
|
|
589
|
-
const sanitizedErr = sanitizeBotToken(err instanceof Error ? err.message : String(err));
|
|
590
|
-
const entry = `[${new Date().toISOString()}] chatId=${chatId} error=${sanitizedErr} text=${text.slice(0, 200)}\n`;
|
|
591
|
-
fs.appendFileSync(failLog, entry);
|
|
592
|
-
logger.error(`Delivery failed after retries for chat ${chatId}`);
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
async sendWithRetryRaw(chatId, text) {
|
|
596
|
-
await retryWithBackoff(
|
|
597
|
-
// Send as plain text — Telegram's legacy "Markdown" parser silently mangles
|
|
598
|
-
// underscores/asterisks in Claude's responses (code identifiers, names),
|
|
599
|
-
// and Beecork has no escaping pass for it.
|
|
600
|
-
() => this.bot.sendMessage(chatId, text), [1000, 5000, 15000], 'telegram-send');
|
|
601
|
-
}
|
|
602
|
-
async downloadTelegramFile(fileId, extension) {
|
|
603
|
-
const fileInfo = await this.bot.getFile(fileId);
|
|
604
|
-
if (!fileInfo.file_path)
|
|
605
|
-
return null;
|
|
606
|
-
// Check file size (Telegram provides file_size in bytes)
|
|
607
|
-
if (fileInfo.file_size && isOversized(fileInfo.file_size)) {
|
|
608
|
-
logger.warn(`Skipping oversized file: ${fileInfo.file_size} bytes`);
|
|
609
|
-
return null;
|
|
610
|
-
}
|
|
611
|
-
const url = `https://api.telegram.org/file/bot${this.ctx.config.telegram.token}/${fileInfo.file_path}`;
|
|
612
|
-
const response = await fetch(url, { signal: AbortSignal.timeout(30000) });
|
|
613
|
-
if (!response.ok)
|
|
614
|
-
return null;
|
|
615
|
-
const buffer = Buffer.from(await response.arrayBuffer());
|
|
616
|
-
return saveMedia(buffer, extension, fileInfo.file_path.split('/').pop());
|
|
617
|
-
}
|
|
618
|
-
isAllowed(userId) {
|
|
619
|
-
if (!userId)
|
|
620
|
-
return false;
|
|
621
|
-
// Set-based O(1) membership check; explicit empty-set check keeps the
|
|
622
|
-
// fail-closed contract documented in code.
|
|
623
|
-
if (this.allowedUserIdSet.size === 0)
|
|
624
|
-
return false;
|
|
625
|
-
return this.allowedUserIdSet.has(userId);
|
|
626
|
-
}
|
|
627
|
-
isAdmin(userId) {
|
|
628
|
-
const cfg = this.ctx.config.telegram;
|
|
629
|
-
return isChannelAdmin(cfg.allowedUserIds, userId, cfg.adminUserId);
|
|
630
|
-
}
|
|
631
|
-
async setReaction(chatId, messageId, emoji) {
|
|
632
|
-
try {
|
|
633
|
-
const url = `https://api.telegram.org/bot${this.ctx.config.telegram.token}/setMessageReaction`;
|
|
634
|
-
await fetch(url, {
|
|
635
|
-
method: 'POST',
|
|
636
|
-
headers: { 'Content-Type': 'application/json' },
|
|
637
|
-
body: JSON.stringify({
|
|
638
|
-
chat_id: chatId,
|
|
639
|
-
message_id: messageId,
|
|
640
|
-
reaction: [{ type: 'emoji', emoji }],
|
|
641
|
-
}),
|
|
642
|
-
signal: AbortSignal.timeout(5000),
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
catch {
|
|
646
|
-
// Reactions not supported or failed — non-critical
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
}
|