zardbot-telegram 1.0.0
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/.env.example +116 -0
- package/LICENSE +21 -0
- package/README.md +250 -0
- package/dist/agent/manager.js +88 -0
- package/dist/agent/types.js +26 -0
- package/dist/app/start-bot-app.js +49 -0
- package/dist/bot/commands/abort.js +121 -0
- package/dist/bot/commands/commands.js +480 -0
- package/dist/bot/commands/definitions.js +27 -0
- package/dist/bot/commands/help.js +10 -0
- package/dist/bot/commands/models.js +38 -0
- package/dist/bot/commands/new.js +70 -0
- package/dist/bot/commands/opencode-start.js +101 -0
- package/dist/bot/commands/opencode-stop.js +44 -0
- package/dist/bot/commands/projects.js +223 -0
- package/dist/bot/commands/rename.js +139 -0
- package/dist/bot/commands/sessions.js +351 -0
- package/dist/bot/commands/start.js +43 -0
- package/dist/bot/commands/status.js +95 -0
- package/dist/bot/commands/task.js +399 -0
- package/dist/bot/commands/tasklist.js +220 -0
- package/dist/bot/commands/voice.js +145 -0
- package/dist/bot/handlers/agent.js +118 -0
- package/dist/bot/handlers/context.js +100 -0
- package/dist/bot/handlers/document.js +65 -0
- package/dist/bot/handlers/inline-menu.js +119 -0
- package/dist/bot/handlers/model.js +143 -0
- package/dist/bot/handlers/permission.js +235 -0
- package/dist/bot/handlers/prompt.js +240 -0
- package/dist/bot/handlers/question.js +390 -0
- package/dist/bot/handlers/tts.js +89 -0
- package/dist/bot/handlers/variant.js +138 -0
- package/dist/bot/handlers/voice.js +173 -0
- package/dist/bot/index.js +977 -0
- package/dist/bot/message-patterns.js +4 -0
- package/dist/bot/middleware/auth.js +30 -0
- package/dist/bot/middleware/interaction-guard.js +95 -0
- package/dist/bot/middleware/unknown-command.js +22 -0
- package/dist/bot/streaming/response-streamer.js +286 -0
- package/dist/bot/streaming/tool-call-streamer.js +285 -0
- package/dist/bot/utils/busy-guard.js +15 -0
- package/dist/bot/utils/commands.js +21 -0
- package/dist/bot/utils/file-download.js +91 -0
- package/dist/bot/utils/finalize-assistant-response.js +52 -0
- package/dist/bot/utils/keyboard.js +69 -0
- package/dist/bot/utils/send-with-markdown-fallback.js +165 -0
- package/dist/bot/utils/telegram-text.js +28 -0
- package/dist/bot/utils/thinking-message.js +8 -0
- package/dist/cli/args.js +98 -0
- package/dist/cli.js +80 -0
- package/dist/config.js +97 -0
- package/dist/i18n/de.js +357 -0
- package/dist/i18n/en.js +357 -0
- package/dist/i18n/es.js +357 -0
- package/dist/i18n/fr.js +357 -0
- package/dist/i18n/index.js +109 -0
- package/dist/i18n/ru.js +357 -0
- package/dist/i18n/zh.js +357 -0
- package/dist/index.js +26 -0
- package/dist/interaction/busy.js +8 -0
- package/dist/interaction/cleanup.js +32 -0
- package/dist/interaction/guard.js +140 -0
- package/dist/interaction/manager.js +106 -0
- package/dist/interaction/types.js +1 -0
- package/dist/keyboard/manager.js +172 -0
- package/dist/keyboard/types.js +1 -0
- package/dist/model/capabilities.js +62 -0
- package/dist/model/context-limit.js +57 -0
- package/dist/model/manager.js +259 -0
- package/dist/model/types.js +24 -0
- package/dist/opencode/client.js +13 -0
- package/dist/opencode/events.js +140 -0
- package/dist/permission/manager.js +100 -0
- package/dist/permission/types.js +1 -0
- package/dist/pinned/format.js +29 -0
- package/dist/pinned/manager.js +682 -0
- package/dist/pinned/types.js +1 -0
- package/dist/process/manager.js +273 -0
- package/dist/process/types.js +1 -0
- package/dist/project/manager.js +88 -0
- package/dist/question/manager.js +176 -0
- package/dist/question/types.js +1 -0
- package/dist/rename/manager.js +53 -0
- package/dist/runtime/bootstrap.js +350 -0
- package/dist/runtime/mode.js +74 -0
- package/dist/runtime/paths.js +37 -0
- package/dist/scheduled-task/creation-manager.js +113 -0
- package/dist/scheduled-task/display.js +239 -0
- package/dist/scheduled-task/executor.js +87 -0
- package/dist/scheduled-task/foreground-state.js +32 -0
- package/dist/scheduled-task/next-run.js +207 -0
- package/dist/scheduled-task/runtime.js +368 -0
- package/dist/scheduled-task/schedule-parser.js +169 -0
- package/dist/scheduled-task/store.js +65 -0
- package/dist/scheduled-task/types.js +19 -0
- package/dist/session/cache-manager.js +455 -0
- package/dist/session/manager.js +10 -0
- package/dist/settings/manager.js +158 -0
- package/dist/stt/client.js +97 -0
- package/dist/summary/aggregator.js +1136 -0
- package/dist/summary/formatter.js +491 -0
- package/dist/summary/subagent-formatter.js +63 -0
- package/dist/summary/tool-message-batcher.js +90 -0
- package/dist/tts/client.js +130 -0
- package/dist/utils/error-format.js +29 -0
- package/dist/utils/logger.js +127 -0
- package/dist/utils/safe-background-task.js +33 -0
- package/dist/utils/telegram-rate-limit-retry.js +93 -0
- package/dist/variant/manager.js +103 -0
- package/dist/variant/types.js +1 -0
- package/package.json +79 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import https from "node:https";
|
|
3
|
+
import { URL } from "node:url";
|
|
4
|
+
import { HttpsProxyAgent } from "https-proxy-agent";
|
|
5
|
+
import { SocksProxyAgent } from "socks-proxy-agent";
|
|
6
|
+
import { config } from "../../config.js";
|
|
7
|
+
import { isSttConfigured, transcribeAudio } from "../../stt/client.js";
|
|
8
|
+
import { processUserPrompt } from "./prompt.js";
|
|
9
|
+
import { logger } from "../../utils/logger.js";
|
|
10
|
+
import { t } from "../../i18n/index.js";
|
|
11
|
+
const TELEGRAM_DOWNLOAD_TIMEOUT_MS = 30_000;
|
|
12
|
+
const TELEGRAM_DOWNLOAD_MAX_REDIRECTS = 3;
|
|
13
|
+
let telegramDownloadAgent;
|
|
14
|
+
function getTelegramDownloadAgent() {
|
|
15
|
+
if (telegramDownloadAgent !== undefined) {
|
|
16
|
+
return telegramDownloadAgent || undefined;
|
|
17
|
+
}
|
|
18
|
+
const proxyUrl = config.telegram.proxyUrl.trim();
|
|
19
|
+
if (!proxyUrl) {
|
|
20
|
+
telegramDownloadAgent = null;
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
telegramDownloadAgent = proxyUrl.startsWith("socks")
|
|
24
|
+
? new SocksProxyAgent(proxyUrl)
|
|
25
|
+
: new HttpsProxyAgent(proxyUrl);
|
|
26
|
+
logger.info(`[Voice] Using Telegram download proxy: ${proxyUrl.replace(/\/\/.*@/, "//***@")}`);
|
|
27
|
+
return telegramDownloadAgent;
|
|
28
|
+
}
|
|
29
|
+
async function downloadTelegramFileByUrl(url, redirectDepth = 0) {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const targetUrl = new URL(url);
|
|
32
|
+
const requestModule = targetUrl.protocol === "http:" ? http : https;
|
|
33
|
+
const request = requestModule.get(targetUrl, { agent: getTelegramDownloadAgent() }, (response) => {
|
|
34
|
+
const statusCode = response.statusCode ?? 0;
|
|
35
|
+
if (statusCode >= 300 && statusCode < 400 && response.headers.location) {
|
|
36
|
+
response.resume();
|
|
37
|
+
if (redirectDepth >= TELEGRAM_DOWNLOAD_MAX_REDIRECTS) {
|
|
38
|
+
reject(new Error("Too many redirects while downloading Telegram file"));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const redirectUrl = new URL(response.headers.location, targetUrl).toString();
|
|
42
|
+
void downloadTelegramFileByUrl(redirectUrl, redirectDepth + 1)
|
|
43
|
+
.then(resolve)
|
|
44
|
+
.catch(reject);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
48
|
+
response.resume();
|
|
49
|
+
reject(new Error(`Telegram file download failed with HTTP ${statusCode}`));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const chunks = [];
|
|
53
|
+
response.on("data", (chunk) => {
|
|
54
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
55
|
+
});
|
|
56
|
+
response.on("end", () => {
|
|
57
|
+
resolve(Buffer.concat(chunks));
|
|
58
|
+
});
|
|
59
|
+
response.on("error", reject);
|
|
60
|
+
});
|
|
61
|
+
request.on("error", reject);
|
|
62
|
+
request.setTimeout(TELEGRAM_DOWNLOAD_TIMEOUT_MS, () => {
|
|
63
|
+
request.destroy(new Error(`Telegram file download timed out after ${TELEGRAM_DOWNLOAD_TIMEOUT_MS}ms`));
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Downloads the audio file from Telegram servers.
|
|
69
|
+
*
|
|
70
|
+
* @returns Buffer with file content, or null on failure
|
|
71
|
+
*/
|
|
72
|
+
async function downloadTelegramFile(ctx, fileId) {
|
|
73
|
+
try {
|
|
74
|
+
const file = await ctx.api.getFile(fileId);
|
|
75
|
+
if (!file.file_path) {
|
|
76
|
+
logger.error("[Voice] Telegram getFile returned no file_path");
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const fileUrl = `https://api.telegram.org/file/bot${ctx.api.token}/${file.file_path}`;
|
|
80
|
+
logger.debug(`[Voice] Downloading file: ${file.file_path} (${file.file_size ?? "?"} bytes)`);
|
|
81
|
+
const buffer = await downloadTelegramFileByUrl(fileUrl);
|
|
82
|
+
// Extract filename from file_path (e.g., "voice/file_123.oga" -> "file_123.oga")
|
|
83
|
+
let filename = file.file_path.split("/").pop() || "audio.ogg";
|
|
84
|
+
if (filename.endsWith(".oga")) {
|
|
85
|
+
filename = filename.slice(0, -4) + ".ogg";
|
|
86
|
+
}
|
|
87
|
+
logger.debug(`[Voice] Downloaded file: ${filename} (${buffer.length} bytes)`);
|
|
88
|
+
return { buffer, filename };
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
logger.error("[Voice] Error downloading file from Telegram:", err);
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Creates the voice message handler function.
|
|
97
|
+
*
|
|
98
|
+
* The factory pattern is used so that `bot` and `ensureEventSubscription` dependencies
|
|
99
|
+
* can be injected from createBot() without circular imports.
|
|
100
|
+
*/
|
|
101
|
+
export function createVoiceHandler(deps) {
|
|
102
|
+
return async (ctx) => {
|
|
103
|
+
await handleVoiceMessage(ctx, deps);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Handles incoming voice and audio messages:
|
|
108
|
+
* 1. Checks if STT is configured
|
|
109
|
+
* 2. Downloads the audio file from Telegram
|
|
110
|
+
* 3. Sends "recognizing..." status message
|
|
111
|
+
* 4. Calls STT API
|
|
112
|
+
* 5. Shows recognized text
|
|
113
|
+
* 6. Passes text to processUserPrompt
|
|
114
|
+
*/
|
|
115
|
+
export async function handleVoiceMessage(ctx, deps) {
|
|
116
|
+
const sttConfigured = deps.isSttConfigured ?? isSttConfigured;
|
|
117
|
+
const downloadFile = deps.downloadTelegramFile ?? downloadTelegramFile;
|
|
118
|
+
const transcribe = deps.transcribeAudio ?? transcribeAudio;
|
|
119
|
+
const processPrompt = deps.processPrompt ?? processUserPrompt;
|
|
120
|
+
// Determine file_id from voice or audio message
|
|
121
|
+
const voice = ctx.message?.voice;
|
|
122
|
+
const audio = ctx.message?.audio;
|
|
123
|
+
const fileId = voice?.file_id ?? audio?.file_id;
|
|
124
|
+
if (!fileId) {
|
|
125
|
+
logger.warn("[Voice] Received voice/audio message with no file_id");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
// Check if STT is configured
|
|
129
|
+
if (!sttConfigured()) {
|
|
130
|
+
await ctx.reply(t("stt.not_configured"));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
// Send "recognizing..." status message (will be edited later)
|
|
134
|
+
const statusMessage = await ctx.reply(t("stt.recognizing"));
|
|
135
|
+
try {
|
|
136
|
+
// Download the audio file from Telegram
|
|
137
|
+
const fileData = await downloadFile(ctx, fileId);
|
|
138
|
+
if (!fileData) {
|
|
139
|
+
await ctx.api.editMessageText(ctx.chat.id, statusMessage.message_id, t("stt.error", { error: "download failed" }));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// Transcribe the audio
|
|
143
|
+
const result = await transcribe(fileData.buffer, fileData.filename);
|
|
144
|
+
const recognizedText = result.text.trim();
|
|
145
|
+
if (!recognizedText) {
|
|
146
|
+
await ctx.api.editMessageText(ctx.chat.id, statusMessage.message_id, t("stt.empty_result"));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// Show the recognized text by editing the status message.
|
|
150
|
+
// IMPORTANT: even if this edit fails (e.g. Telegram message length limits),
|
|
151
|
+
// we still send the recognized text to OpenCode as a prompt.
|
|
152
|
+
try {
|
|
153
|
+
await ctx.api.editMessageText(ctx.chat.id, statusMessage.message_id, t("stt.recognized", { text: recognizedText }));
|
|
154
|
+
}
|
|
155
|
+
catch (editError) {
|
|
156
|
+
logger.warn("[Voice] Failed to edit status message with recognized text:", editError);
|
|
157
|
+
}
|
|
158
|
+
logger.info(`[Voice] Transcribed audio: ${recognizedText.length} chars`);
|
|
159
|
+
// Process the recognized text as a prompt
|
|
160
|
+
await processPrompt(ctx, recognizedText, deps);
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
const errorMessage = err instanceof Error ? err.message : "unknown error";
|
|
164
|
+
logger.error("[Voice] Error processing voice message:", err);
|
|
165
|
+
try {
|
|
166
|
+
await ctx.api.editMessageText(ctx.chat.id, statusMessage.message_id, t("stt.error", { error: errorMessage }));
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// If we can't edit the status message, try sending a new one
|
|
170
|
+
await ctx.reply(t("stt.error", { error: errorMessage })).catch(() => { });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|