opencode-telegram-group-topics-bot 0.11.2
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 +74 -0
- package/LICENSE +21 -0
- package/README.md +305 -0
- package/dist/agent/manager.js +60 -0
- package/dist/agent/types.js +26 -0
- package/dist/app/start-bot-app.js +47 -0
- package/dist/bot/commands/abort.js +116 -0
- package/dist/bot/commands/commands.js +389 -0
- package/dist/bot/commands/constants.js +20 -0
- package/dist/bot/commands/definitions.js +25 -0
- package/dist/bot/commands/help.js +27 -0
- package/dist/bot/commands/models.js +38 -0
- package/dist/bot/commands/new.js +247 -0
- package/dist/bot/commands/opencode-start.js +85 -0
- package/dist/bot/commands/opencode-stop.js +44 -0
- package/dist/bot/commands/projects.js +304 -0
- package/dist/bot/commands/rename.js +173 -0
- package/dist/bot/commands/sessions.js +491 -0
- package/dist/bot/commands/start.js +67 -0
- package/dist/bot/commands/status.js +138 -0
- package/dist/bot/constants.js +49 -0
- package/dist/bot/handlers/agent.js +127 -0
- package/dist/bot/handlers/context.js +125 -0
- package/dist/bot/handlers/document.js +65 -0
- package/dist/bot/handlers/inline-menu.js +124 -0
- package/dist/bot/handlers/model.js +152 -0
- package/dist/bot/handlers/permission.js +281 -0
- package/dist/bot/handlers/prompt.js +263 -0
- package/dist/bot/handlers/question.js +285 -0
- package/dist/bot/handlers/variant.js +147 -0
- package/dist/bot/handlers/voice.js +173 -0
- package/dist/bot/index.js +945 -0
- package/dist/bot/message-patterns.js +4 -0
- package/dist/bot/middleware/auth.js +30 -0
- package/dist/bot/middleware/interaction-guard.js +80 -0
- package/dist/bot/middleware/unknown-command.js +22 -0
- package/dist/bot/scope.js +222 -0
- package/dist/bot/telegram-constants.js +3 -0
- package/dist/bot/telegram-rate-limiter.js +263 -0
- package/dist/bot/utils/commands.js +21 -0
- package/dist/bot/utils/file-download.js +91 -0
- package/dist/bot/utils/keyboard.js +85 -0
- package/dist/bot/utils/send-with-markdown-fallback.js +57 -0
- package/dist/bot/utils/session-error-filter.js +34 -0
- package/dist/bot/utils/topic-link.js +29 -0
- package/dist/cli/args.js +98 -0
- package/dist/cli.js +80 -0
- package/dist/config.js +103 -0
- package/dist/i18n/de.js +330 -0
- package/dist/i18n/en.js +330 -0
- package/dist/i18n/es.js +330 -0
- package/dist/i18n/index.js +102 -0
- package/dist/i18n/ru.js +330 -0
- package/dist/i18n/zh.js +330 -0
- package/dist/index.js +28 -0
- package/dist/interaction/cleanup.js +24 -0
- package/dist/interaction/constants.js +25 -0
- package/dist/interaction/guard.js +100 -0
- package/dist/interaction/manager.js +113 -0
- package/dist/interaction/types.js +1 -0
- package/dist/keyboard/manager.js +115 -0
- package/dist/keyboard/types.js +1 -0
- package/dist/model/capabilities.js +62 -0
- package/dist/model/manager.js +257 -0
- package/dist/model/types.js +24 -0
- package/dist/opencode/client.js +13 -0
- package/dist/opencode/events.js +159 -0
- package/dist/opencode/prompt-submit-error.js +101 -0
- package/dist/permission/manager.js +92 -0
- package/dist/permission/types.js +1 -0
- package/dist/pinned/manager.js +405 -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 +186 -0
- package/dist/question/types.js +1 -0
- package/dist/rename/manager.js +64 -0
- package/dist/runtime/bootstrap.js +350 -0
- package/dist/runtime/mode.js +74 -0
- package/dist/runtime/paths.js +37 -0
- package/dist/runtime/process-error-handlers.js +24 -0
- package/dist/session/cache-manager.js +455 -0
- package/dist/session/manager.js +87 -0
- package/dist/settings/manager.js +283 -0
- package/dist/stt/client.js +64 -0
- package/dist/summary/aggregator.js +625 -0
- package/dist/summary/formatter.js +417 -0
- package/dist/summary/tool-message-batcher.js +277 -0
- package/dist/topic/colors.js +8 -0
- package/dist/topic/constants.js +10 -0
- package/dist/topic/manager.js +161 -0
- package/dist/topic/title-constants.js +2 -0
- package/dist/topic/title-format.js +10 -0
- package/dist/topic/title-sync.js +17 -0
- package/dist/utils/error-format.js +29 -0
- package/dist/utils/logger.js +175 -0
- package/dist/utils/safe-background-task.js +33 -0
- package/dist/variant/manager.js +103 -0
- package/dist/variant/types.js +1 -0
- package/package.json +76 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { InlineKeyboard } from "grammy";
|
|
2
|
+
import { getAvailableVariants, getCurrentVariant, setCurrentVariant, formatVariantForDisplay, formatVariantForButton, } from "../../variant/manager.js";
|
|
3
|
+
import { getStoredModel } from "../../model/manager.js";
|
|
4
|
+
import { getStoredAgent } from "../../agent/manager.js";
|
|
5
|
+
import { logger } from "../../utils/logger.js";
|
|
6
|
+
import { keyboardManager } from "../../keyboard/manager.js";
|
|
7
|
+
import { pinnedMessageManager } from "../../pinned/manager.js";
|
|
8
|
+
import { createMainKeyboard } from "../utils/keyboard.js";
|
|
9
|
+
import { clearActiveInlineMenu, ensureActiveInlineMenu, replyWithInlineMenu, } from "./inline-menu.js";
|
|
10
|
+
import { t } from "../../i18n/index.js";
|
|
11
|
+
import { SCOPE_CONTEXT, getScopeFromKey, getScopeKeyFromContext } from "../scope.js";
|
|
12
|
+
/**
|
|
13
|
+
* Handle variant selection callback
|
|
14
|
+
* @param ctx grammY context
|
|
15
|
+
* @returns true if handled, false otherwise
|
|
16
|
+
*/
|
|
17
|
+
export async function handleVariantSelect(ctx) {
|
|
18
|
+
const callbackQuery = ctx.callbackQuery;
|
|
19
|
+
if (!callbackQuery?.data || !callbackQuery.data.startsWith("variant:")) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const isActiveMenu = await ensureActiveInlineMenu(ctx, "variant");
|
|
23
|
+
if (!isActiveMenu) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
logger.debug(`[VariantHandler] Received callback: ${callbackQuery.data}`);
|
|
27
|
+
try {
|
|
28
|
+
const scopeKey = getScopeKeyFromContext(ctx);
|
|
29
|
+
if (ctx.chat) {
|
|
30
|
+
keyboardManager.initialize(ctx.api, ctx.chat.id, scopeKey);
|
|
31
|
+
}
|
|
32
|
+
if (pinnedMessageManager.getContextLimit(scopeKey) === 0) {
|
|
33
|
+
await pinnedMessageManager.refreshContextLimit(scopeKey);
|
|
34
|
+
}
|
|
35
|
+
// Parse callback data: "variant:variantId"
|
|
36
|
+
const variantId = callbackQuery.data.replace("variant:", "");
|
|
37
|
+
// Get current model
|
|
38
|
+
const currentModel = getStoredModel(scopeKey);
|
|
39
|
+
if (!currentModel.providerID || !currentModel.modelID) {
|
|
40
|
+
logger.error("[VariantHandler] No model selected");
|
|
41
|
+
await ctx.answerCallbackQuery({ text: t("variant.model_not_selected_callback") });
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
// Set variant
|
|
45
|
+
setCurrentVariant(variantId, scopeKey);
|
|
46
|
+
// Re-read model after variant update
|
|
47
|
+
const updatedModel = getStoredModel(scopeKey);
|
|
48
|
+
// Update keyboard manager state
|
|
49
|
+
keyboardManager.updateModel(updatedModel, scopeKey);
|
|
50
|
+
keyboardManager.updateVariant(variantId, scopeKey);
|
|
51
|
+
// Build keyboard with correct context info
|
|
52
|
+
const currentAgent = getStoredAgent(scopeKey);
|
|
53
|
+
const contextInfo = pinnedMessageManager.getContextInfo(scopeKey) ??
|
|
54
|
+
(pinnedMessageManager.getContextLimit(scopeKey) > 0
|
|
55
|
+
? { tokensUsed: 0, tokensLimit: pinnedMessageManager.getContextLimit(scopeKey) }
|
|
56
|
+
: keyboardManager.getContextInfo(scopeKey));
|
|
57
|
+
if (contextInfo) {
|
|
58
|
+
keyboardManager.updateContext(contextInfo.tokensUsed, contextInfo.tokensLimit, scopeKey);
|
|
59
|
+
}
|
|
60
|
+
const variantName = formatVariantForButton(variantId);
|
|
61
|
+
const scope = getScopeFromKey(scopeKey);
|
|
62
|
+
const keyboard = createMainKeyboard(currentAgent, updatedModel, contextInfo ?? undefined, variantName, scope?.context === SCOPE_CONTEXT.GROUP_GENERAL
|
|
63
|
+
? {
|
|
64
|
+
contextFirst: true,
|
|
65
|
+
contextLabel: t("keyboard.general_defaults"),
|
|
66
|
+
}
|
|
67
|
+
: undefined);
|
|
68
|
+
// Send confirmation message with updated keyboard
|
|
69
|
+
const displayName = formatVariantForDisplay(variantId);
|
|
70
|
+
clearActiveInlineMenu("variant_selected", scopeKey);
|
|
71
|
+
await ctx.answerCallbackQuery({ text: t("variant.changed_callback", { name: displayName }) });
|
|
72
|
+
await ctx.reply(t("variant.changed_message", { name: displayName }), {
|
|
73
|
+
reply_markup: keyboard,
|
|
74
|
+
});
|
|
75
|
+
// Delete the inline menu message
|
|
76
|
+
await ctx.deleteMessage().catch(() => { });
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
clearActiveInlineMenu("variant_select_error", getScopeKeyFromContext(ctx));
|
|
81
|
+
logger.error("[VariantHandler] Error handling variant select:", err);
|
|
82
|
+
await ctx.answerCallbackQuery({ text: t("variant.change_error_callback") }).catch(() => { });
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Build inline keyboard with available variants
|
|
88
|
+
* @param currentVariant Current variant for highlighting
|
|
89
|
+
* @param providerID Provider ID
|
|
90
|
+
* @param modelID Model ID
|
|
91
|
+
* @returns InlineKeyboard with variant selection buttons
|
|
92
|
+
*/
|
|
93
|
+
export async function buildVariantSelectionMenu(currentVariant, providerID, modelID) {
|
|
94
|
+
const keyboard = new InlineKeyboard();
|
|
95
|
+
const variants = await getAvailableVariants(providerID, modelID);
|
|
96
|
+
if (variants.length === 0) {
|
|
97
|
+
logger.warn("[VariantHandler] No variants found");
|
|
98
|
+
return keyboard;
|
|
99
|
+
}
|
|
100
|
+
// Filter only active variants (not disabled)
|
|
101
|
+
const activeVariants = variants.filter((v) => !v.disabled);
|
|
102
|
+
if (activeVariants.length === 0) {
|
|
103
|
+
logger.warn("[VariantHandler] No active variants found");
|
|
104
|
+
// If no active variants, show default at least
|
|
105
|
+
keyboard.text(`✅ ${formatVariantForDisplay("default")}`, "variant:default").row();
|
|
106
|
+
return keyboard;
|
|
107
|
+
}
|
|
108
|
+
// Add button for each variant (one per row)
|
|
109
|
+
activeVariants.forEach((variant) => {
|
|
110
|
+
const isActive = variant.id === currentVariant;
|
|
111
|
+
const label = formatVariantForDisplay(variant.id);
|
|
112
|
+
const labelWithCheck = isActive ? `✅ ${label}` : label;
|
|
113
|
+
keyboard.text(labelWithCheck, `variant:${variant.id}`).row();
|
|
114
|
+
});
|
|
115
|
+
return keyboard;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Show variant selection menu
|
|
119
|
+
* @param ctx grammY context
|
|
120
|
+
*/
|
|
121
|
+
export async function showVariantSelectionMenu(ctx) {
|
|
122
|
+
try {
|
|
123
|
+
const scopeKey = getScopeKeyFromContext(ctx);
|
|
124
|
+
const currentModel = getStoredModel(scopeKey);
|
|
125
|
+
if (!currentModel.providerID || !currentModel.modelID) {
|
|
126
|
+
await ctx.reply(t("variant.select_model_first"));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const currentVariant = getCurrentVariant(scopeKey);
|
|
130
|
+
const keyboard = await buildVariantSelectionMenu(currentVariant, currentModel.providerID, currentModel.modelID);
|
|
131
|
+
if (keyboard.inline_keyboard.length === 0) {
|
|
132
|
+
await ctx.reply(t("variant.menu.empty"));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const displayName = formatVariantForDisplay(currentVariant);
|
|
136
|
+
const text = t("variant.menu.current", { name: displayName });
|
|
137
|
+
await replyWithInlineMenu(ctx, {
|
|
138
|
+
menuKind: "variant",
|
|
139
|
+
text,
|
|
140
|
+
keyboard,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
logger.error("[VariantHandler] Error showing variant menu:", err);
|
|
145
|
+
await ctx.reply(t("variant.menu.error"));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -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
|
+
}
|