@grinev/opencode-telegram-bot 0.19.3 → 0.20.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 +5 -0
- package/README.md +5 -0
- package/dist/attach/service.js +0 -2
- package/dist/background-session/tracker.js +137 -0
- package/dist/bot/commands/definitions.js +2 -0
- package/dist/bot/commands/detach.js +54 -0
- package/dist/bot/commands/ls.js +445 -0
- package/dist/bot/commands/open.js +9 -4
- package/dist/bot/commands/projects.js +6 -2
- package/dist/bot/commands/sessions.js +233 -82
- package/dist/bot/commands/worktree.js +7 -2
- package/dist/bot/handlers/inline-menu.js +1 -0
- package/dist/bot/index.js +70 -8
- package/dist/bot/utils/send-downloaded-file.js +57 -0
- package/dist/bot/utils/switch-project.js +9 -14
- package/dist/config.js +1 -0
- package/dist/i18n/de.js +26 -0
- package/dist/i18n/en.js +26 -0
- package/dist/i18n/es.js +26 -0
- package/dist/i18n/fr.js +26 -0
- package/dist/i18n/ru.js +26 -0
- package/dist/i18n/zh.js +26 -0
- package/dist/interaction/busy.js +1 -1
- package/dist/interaction/manager.js +1 -1
- package/dist/scheduled-task/executor.js +3 -0
- package/dist/scheduled-task/runtime.js +2 -0
- package/dist/scheduled-task/schedule-parser.js +6 -0
- package/dist/scheduled-task/session-ignore.js +53 -0
- package/dist/settings/manager.js +12 -0
- package/package.json +1 -1
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import { InlineKeyboard } from "grammy";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { promises as fs } from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { appendInlineMenuCancelButton, clearActiveInlineMenu, ensureActiveInlineMenu, } from "../handlers/inline-menu.js";
|
|
6
|
+
import { interactionManager } from "../../interaction/manager.js";
|
|
7
|
+
import { isForegroundBusy, replyBusyBlocked } from "../utils/busy-guard.js";
|
|
8
|
+
import { getCurrentProject } from "../../settings/manager.js";
|
|
9
|
+
import { sendDownloadedFile } from "../utils/send-downloaded-file.js";
|
|
10
|
+
import { formatFileSize } from "../utils/file-download.js";
|
|
11
|
+
import { logger } from "../../utils/logger.js";
|
|
12
|
+
import { t } from "../../i18n/index.js";
|
|
13
|
+
const CALLBACK_PREFIX = "ls:";
|
|
14
|
+
const CALLBACK_NAV_PREFIX = "ls:nav:";
|
|
15
|
+
const CALLBACK_FILE_PREFIX = "ls:file:";
|
|
16
|
+
const CALLBACK_DOWNLOAD_PREFIX = "ls:download:";
|
|
17
|
+
const CALLBACK_BACK_PREFIX = "ls:back:";
|
|
18
|
+
const CALLBACK_PAGE_PREFIX = "ls:pg:";
|
|
19
|
+
const PAGE_SEPARATOR = "|";
|
|
20
|
+
const MAX_ENTRIES_PER_PAGE = 8;
|
|
21
|
+
const MAX_BUTTON_LABEL_LENGTH = 64;
|
|
22
|
+
const sessionDirectories = new Map();
|
|
23
|
+
const pathIndex = new Map();
|
|
24
|
+
let pathCounter = 0;
|
|
25
|
+
function escapeHtml(text) {
|
|
26
|
+
return text
|
|
27
|
+
.replace(/&/g, "&")
|
|
28
|
+
.replace(/</g, "<")
|
|
29
|
+
.replace(/>/g, ">")
|
|
30
|
+
.replace(/\"/g, """)
|
|
31
|
+
.replace(/'/g, "'");
|
|
32
|
+
}
|
|
33
|
+
function truncateLabel(label, maxLen = MAX_BUTTON_LABEL_LENGTH) {
|
|
34
|
+
if (label.length <= maxLen) {
|
|
35
|
+
return label;
|
|
36
|
+
}
|
|
37
|
+
return `${label.slice(0, Math.max(0, maxLen - 3))}...`;
|
|
38
|
+
}
|
|
39
|
+
function pathToDisplayPath(absolutePath) {
|
|
40
|
+
const home = os.homedir();
|
|
41
|
+
if (absolutePath === home) {
|
|
42
|
+
return "~";
|
|
43
|
+
}
|
|
44
|
+
if (absolutePath.startsWith(home + path.sep)) {
|
|
45
|
+
return `~${absolutePath.slice(home.length)}`;
|
|
46
|
+
}
|
|
47
|
+
return absolutePath;
|
|
48
|
+
}
|
|
49
|
+
function usesWindowsPath(filePath) {
|
|
50
|
+
return /^[A-Za-z]:[\\/]/.test(filePath) || filePath.startsWith("\\\\");
|
|
51
|
+
}
|
|
52
|
+
function getPathApi(filePath) {
|
|
53
|
+
return usesWindowsPath(filePath) ? path.win32 : path.posix;
|
|
54
|
+
}
|
|
55
|
+
function joinPath(parentPath, childName) {
|
|
56
|
+
return getPathApi(parentPath).join(parentPath, childName);
|
|
57
|
+
}
|
|
58
|
+
function getBaseName(filePath) {
|
|
59
|
+
return getPathApi(filePath).basename(filePath);
|
|
60
|
+
}
|
|
61
|
+
function getParentPath(filePath) {
|
|
62
|
+
return getPathApi(filePath).dirname(filePath);
|
|
63
|
+
}
|
|
64
|
+
function getRootPath(filePath) {
|
|
65
|
+
return getPathApi(filePath).parse(filePath).root;
|
|
66
|
+
}
|
|
67
|
+
function isSamePath(leftPath, rightPath) {
|
|
68
|
+
return getPathApi(rightPath).relative(rightPath, leftPath) === "";
|
|
69
|
+
}
|
|
70
|
+
function buildEntryLabel(entry) {
|
|
71
|
+
return `${entry.type === "directory" ? "📁" : "📄"} ${entry.name}`;
|
|
72
|
+
}
|
|
73
|
+
function isPathWithinDirectory(targetPath, directoryPath) {
|
|
74
|
+
const pathApi = getPathApi(directoryPath);
|
|
75
|
+
const relativePath = pathApi.relative(directoryPath, targetPath);
|
|
76
|
+
return relativePath === "" || (!relativePath.startsWith("..") && !pathApi.isAbsolute(relativePath));
|
|
77
|
+
}
|
|
78
|
+
function getProjectRoot() {
|
|
79
|
+
return getCurrentProject()?.worktree ?? null;
|
|
80
|
+
}
|
|
81
|
+
function isWithinProjectRoot(targetPath) {
|
|
82
|
+
const projectRoot = getProjectRoot();
|
|
83
|
+
return projectRoot !== null && isPathWithinDirectory(targetPath, projectRoot);
|
|
84
|
+
}
|
|
85
|
+
function isProjectRoot(targetPath) {
|
|
86
|
+
const projectRoot = getProjectRoot();
|
|
87
|
+
return projectRoot !== null && isSamePath(targetPath, projectRoot);
|
|
88
|
+
}
|
|
89
|
+
function buildLsHeader(displayPath, totalCount, page, totalPages) {
|
|
90
|
+
let header = `📁 ${t("ls.header")}\n<code>${escapeHtml(displayPath)}</code>`;
|
|
91
|
+
if (totalPages > 1) {
|
|
92
|
+
header += `\n(${page + 1}/${totalPages})`;
|
|
93
|
+
}
|
|
94
|
+
header += `\n${t("ls.total", { count: totalCount })}`;
|
|
95
|
+
return header;
|
|
96
|
+
}
|
|
97
|
+
function buildFileDetailsText(fileDetails) {
|
|
98
|
+
return (`📄 ${t("ls.file.header")}\n<code>${escapeHtml(fileDetails.name)}</code>\n` +
|
|
99
|
+
`${t("commands.download.size")}: ${formatFileSize(fileDetails.size)}\n` +
|
|
100
|
+
`${t("commands.download.modified")}: ${fileDetails.modified.toLocaleDateString()}`);
|
|
101
|
+
}
|
|
102
|
+
function encodePathForCallback(prefix, fullPath, reserveBytes = 0) {
|
|
103
|
+
const naive = `${prefix}${fullPath}`;
|
|
104
|
+
if (Buffer.byteLength(naive, "utf-8") + reserveBytes <= 64) {
|
|
105
|
+
return naive;
|
|
106
|
+
}
|
|
107
|
+
const key = `#${pathCounter++}`;
|
|
108
|
+
pathIndex.set(key, fullPath);
|
|
109
|
+
return `${prefix}${key}`;
|
|
110
|
+
}
|
|
111
|
+
function decodePathFromCallback(prefix, data) {
|
|
112
|
+
if (!data.startsWith(prefix)) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const raw = data.slice(prefix.length);
|
|
116
|
+
if (raw.startsWith("#")) {
|
|
117
|
+
return pathIndex.get(raw) ?? null;
|
|
118
|
+
}
|
|
119
|
+
return raw;
|
|
120
|
+
}
|
|
121
|
+
function encodePathWithPageCallback(prefix, fullPath, page) {
|
|
122
|
+
const pageSuffix = `${PAGE_SEPARATOR}${page}`;
|
|
123
|
+
const reserveBytes = Buffer.byteLength(pageSuffix, "utf-8");
|
|
124
|
+
const pathRef = encodePathForCallback(prefix, fullPath, reserveBytes);
|
|
125
|
+
return `${pathRef}${pageSuffix}`;
|
|
126
|
+
}
|
|
127
|
+
function decodePathWithPageCallback(data, prefix) {
|
|
128
|
+
if (!data.startsWith(prefix)) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const payload = data.slice(prefix.length);
|
|
132
|
+
const separatorIndex = payload.lastIndexOf(PAGE_SEPARATOR);
|
|
133
|
+
if (separatorIndex < 0) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const pathRef = payload.slice(0, separatorIndex);
|
|
137
|
+
const page = Number.parseInt(payload.slice(separatorIndex + 1), 10);
|
|
138
|
+
if (Number.isNaN(page)) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const resolvedPath = pathRef.startsWith("#") ? (pathIndex.get(pathRef) ?? null) : pathRef;
|
|
142
|
+
if (resolvedPath === null) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
return { path: resolvedPath, page };
|
|
146
|
+
}
|
|
147
|
+
function encodePaginationCallback(currentPath, page) {
|
|
148
|
+
return encodePathWithPageCallback(CALLBACK_PAGE_PREFIX, currentPath, page);
|
|
149
|
+
}
|
|
150
|
+
function decodePaginationCallback(data) {
|
|
151
|
+
return decodePathWithPageCallback(data, CALLBACK_PAGE_PREFIX);
|
|
152
|
+
}
|
|
153
|
+
function encodeFileCallback(fullPath, page) {
|
|
154
|
+
return encodePathWithPageCallback(CALLBACK_FILE_PREFIX, fullPath, page);
|
|
155
|
+
}
|
|
156
|
+
function decodeFileCallback(data) {
|
|
157
|
+
return decodePathWithPageCallback(data, CALLBACK_FILE_PREFIX);
|
|
158
|
+
}
|
|
159
|
+
function encodeBackCallback(directoryPath, page) {
|
|
160
|
+
return encodePathWithPageCallback(CALLBACK_BACK_PREFIX, directoryPath, page);
|
|
161
|
+
}
|
|
162
|
+
function decodeBackCallback(data) {
|
|
163
|
+
return decodePathWithPageCallback(data, CALLBACK_BACK_PREFIX);
|
|
164
|
+
}
|
|
165
|
+
async function scanDirectory(dirPath, page = 0) {
|
|
166
|
+
try {
|
|
167
|
+
if (!isWithinProjectRoot(dirPath)) {
|
|
168
|
+
return { error: t("ls.access_denied") };
|
|
169
|
+
}
|
|
170
|
+
const dirEntries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
171
|
+
const entries = dirEntries
|
|
172
|
+
.map((entry) => ({
|
|
173
|
+
name: entry.name,
|
|
174
|
+
fullPath: joinPath(dirPath, entry.name),
|
|
175
|
+
type: entry.isDirectory() ? "directory" : "file",
|
|
176
|
+
}))
|
|
177
|
+
.sort((left, right) => {
|
|
178
|
+
if (left.type !== right.type) {
|
|
179
|
+
return left.type === "directory" ? -1 : 1;
|
|
180
|
+
}
|
|
181
|
+
return left.name.localeCompare(right.name, undefined, { sensitivity: "base" });
|
|
182
|
+
});
|
|
183
|
+
const totalPages = Math.max(1, Math.ceil(entries.length / MAX_ENTRIES_PER_PAGE));
|
|
184
|
+
const safePage = Math.max(0, Math.min(page, totalPages - 1));
|
|
185
|
+
const startIndex = safePage * MAX_ENTRIES_PER_PAGE;
|
|
186
|
+
return {
|
|
187
|
+
entries: entries.slice(startIndex, startIndex + MAX_ENTRIES_PER_PAGE),
|
|
188
|
+
totalCount: entries.length,
|
|
189
|
+
currentPath: dirPath,
|
|
190
|
+
displayPath: pathToDisplayPath(dirPath),
|
|
191
|
+
hasParent: dirPath !== getRootPath(dirPath),
|
|
192
|
+
page: safePage,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
return {
|
|
197
|
+
error: `${t("ls.scan_error")}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function buildBrowseKeyboard(entries, currentPath, hasParent, page, totalCount) {
|
|
202
|
+
const keyboard = new InlineKeyboard();
|
|
203
|
+
const totalPages = Math.max(1, Math.ceil(totalCount / MAX_ENTRIES_PER_PAGE));
|
|
204
|
+
for (const entry of entries) {
|
|
205
|
+
const label = truncateLabel(buildEntryLabel(entry));
|
|
206
|
+
const callbackData = entry.type === "directory"
|
|
207
|
+
? encodePathForCallback(CALLBACK_NAV_PREFIX, entry.fullPath)
|
|
208
|
+
: encodeFileCallback(entry.fullPath, page);
|
|
209
|
+
keyboard.text(label, callbackData).row();
|
|
210
|
+
}
|
|
211
|
+
if (hasParent && !isProjectRoot(currentPath)) {
|
|
212
|
+
keyboard.text(t("open.back"), encodePathForCallback(CALLBACK_NAV_PREFIX, getParentPath(currentPath))).row();
|
|
213
|
+
}
|
|
214
|
+
if (totalPages > 1) {
|
|
215
|
+
if (page > 0) {
|
|
216
|
+
keyboard.text(t("open.prev_page"), encodePaginationCallback(currentPath, page - 1));
|
|
217
|
+
}
|
|
218
|
+
if (page < totalPages - 1) {
|
|
219
|
+
keyboard.text(t("open.next_page"), encodePaginationCallback(currentPath, page + 1));
|
|
220
|
+
}
|
|
221
|
+
keyboard.row();
|
|
222
|
+
}
|
|
223
|
+
appendInlineMenuCancelButton(keyboard, "ls");
|
|
224
|
+
return keyboard;
|
|
225
|
+
}
|
|
226
|
+
function buildFileDetailsKeyboard(filePath, page) {
|
|
227
|
+
const keyboard = new InlineKeyboard();
|
|
228
|
+
const parentPath = getParentPath(filePath);
|
|
229
|
+
keyboard.text(t("ls.file.download"), encodePathForCallback(CALLBACK_DOWNLOAD_PREFIX, filePath));
|
|
230
|
+
keyboard.text(t("ls.file.back"), encodeBackCallback(parentPath, page));
|
|
231
|
+
keyboard.row();
|
|
232
|
+
appendInlineMenuCancelButton(keyboard, "ls");
|
|
233
|
+
return keyboard;
|
|
234
|
+
}
|
|
235
|
+
function hasBrowseActions(currentPath, hasParent, totalCount) {
|
|
236
|
+
if (totalCount > 0) {
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
return hasParent && !isProjectRoot(currentPath);
|
|
240
|
+
}
|
|
241
|
+
async function renderBrowseView(dirPath, page = 0) {
|
|
242
|
+
const result = await scanDirectory(dirPath, page);
|
|
243
|
+
if ("error" in result) {
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
const totalPages = Math.max(1, Math.ceil(result.totalCount / MAX_ENTRIES_PER_PAGE));
|
|
247
|
+
return {
|
|
248
|
+
text: buildLsHeader(result.displayPath, result.totalCount, result.page, totalPages),
|
|
249
|
+
hasActions: hasBrowseActions(result.currentPath, result.hasParent, result.totalCount),
|
|
250
|
+
keyboard: buildBrowseKeyboard(result.entries, result.currentPath, result.hasParent, result.page, result.totalCount),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
async function getFileDetails(filePath) {
|
|
254
|
+
try {
|
|
255
|
+
if (!isWithinProjectRoot(filePath)) {
|
|
256
|
+
return { error: t("ls.access_denied") };
|
|
257
|
+
}
|
|
258
|
+
const stat = await fs.stat(filePath);
|
|
259
|
+
if (!stat.isFile()) {
|
|
260
|
+
return { error: t("commands.download.not_file") };
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
name: getBaseName(filePath),
|
|
264
|
+
fullPath: filePath,
|
|
265
|
+
size: stat.size,
|
|
266
|
+
modified: stat.mtime,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
return {
|
|
271
|
+
error: `${t("ls.scan_error")}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
async function renderFileDetailsView(filePath, page) {
|
|
276
|
+
const fileDetails = await getFileDetails(filePath);
|
|
277
|
+
if ("error" in fileDetails) {
|
|
278
|
+
return fileDetails;
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
text: buildFileDetailsText(fileDetails),
|
|
282
|
+
keyboard: buildFileDetailsKeyboard(fileDetails.fullPath, page),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function resolveInitialDirectory(userId) {
|
|
286
|
+
const currentProject = getProjectRoot();
|
|
287
|
+
if (!currentProject) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
if (userId) {
|
|
291
|
+
const cachedDirectory = sessionDirectories.get(userId);
|
|
292
|
+
if (cachedDirectory && isPathWithinDirectory(cachedDirectory, currentProject)) {
|
|
293
|
+
return cachedDirectory;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return currentProject;
|
|
297
|
+
}
|
|
298
|
+
export function clearLsPathIndex() {
|
|
299
|
+
pathIndex.clear();
|
|
300
|
+
pathCounter = 0;
|
|
301
|
+
}
|
|
302
|
+
export function clearSessionDirectories() {
|
|
303
|
+
sessionDirectories.clear();
|
|
304
|
+
}
|
|
305
|
+
export async function lsCommand(ctx) {
|
|
306
|
+
if (isForegroundBusy()) {
|
|
307
|
+
await replyBusyBlocked(ctx);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
clearLsPathIndex();
|
|
311
|
+
const projectRoot = getProjectRoot();
|
|
312
|
+
if (!projectRoot) {
|
|
313
|
+
await ctx.reply(t("bot.project_not_selected"));
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const args = typeof ctx.match === "string" ? ctx.match.trim() : undefined;
|
|
317
|
+
const targetDir = args || resolveInitialDirectory(ctx.from?.id);
|
|
318
|
+
if (!targetDir) {
|
|
319
|
+
await ctx.reply(t("bot.project_not_selected"));
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (!isWithinProjectRoot(targetDir)) {
|
|
323
|
+
await ctx.reply(`❌ ${t("ls.access_denied")}`);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const view = await renderBrowseView(targetDir);
|
|
327
|
+
if ("error" in view) {
|
|
328
|
+
await ctx.reply(`❌ ${view.error}`);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (ctx.from) {
|
|
332
|
+
sessionDirectories.set(ctx.from.id, targetDir);
|
|
333
|
+
}
|
|
334
|
+
if (!view.hasActions) {
|
|
335
|
+
await ctx.reply(view.text, { parse_mode: "HTML" });
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const message = await ctx.reply(view.text, { parse_mode: "HTML", reply_markup: view.keyboard });
|
|
339
|
+
interactionManager.start({
|
|
340
|
+
kind: "inline",
|
|
341
|
+
expectedInput: "callback",
|
|
342
|
+
metadata: {
|
|
343
|
+
menuKind: "ls",
|
|
344
|
+
messageId: message.message_id,
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
async function navigateTo(ctx, dirPath, page = 0) {
|
|
349
|
+
const view = await renderBrowseView(dirPath, page);
|
|
350
|
+
if ("error" in view) {
|
|
351
|
+
await ctx.answerCallbackQuery({ text: view.error });
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (ctx.from) {
|
|
355
|
+
sessionDirectories.set(ctx.from.id, dirPath);
|
|
356
|
+
}
|
|
357
|
+
await ctx.answerCallbackQuery();
|
|
358
|
+
await ctx.editMessageText(view.text, { parse_mode: "HTML", reply_markup: view.keyboard });
|
|
359
|
+
}
|
|
360
|
+
async function showFileDetails(ctx, filePath, page) {
|
|
361
|
+
const view = await renderFileDetailsView(filePath, page);
|
|
362
|
+
if ("error" in view) {
|
|
363
|
+
await ctx.answerCallbackQuery({ text: view.error });
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
await ctx.answerCallbackQuery();
|
|
367
|
+
await ctx.editMessageText(view.text, { parse_mode: "HTML", reply_markup: view.keyboard });
|
|
368
|
+
}
|
|
369
|
+
async function downloadFileAndClose(ctx, filePath) {
|
|
370
|
+
await ctx.answerCallbackQuery({ text: t("commands.download.downloading") });
|
|
371
|
+
const downloaded = await sendDownloadedFile(ctx, filePath, { announce: false });
|
|
372
|
+
if (!downloaded) {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
clearActiveInlineMenu("ls_downloaded");
|
|
376
|
+
clearLsPathIndex();
|
|
377
|
+
await ctx.deleteMessage().catch(() => { });
|
|
378
|
+
}
|
|
379
|
+
export async function handleLsCallback(ctx) {
|
|
380
|
+
const data = ctx.callbackQuery?.data;
|
|
381
|
+
if (!data || !data.startsWith(CALLBACK_PREFIX)) {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
if (isForegroundBusy()) {
|
|
385
|
+
await replyBusyBlocked(ctx);
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
const isActiveMenu = await ensureActiveInlineMenu(ctx, "ls");
|
|
389
|
+
if (!isActiveMenu) {
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
try {
|
|
393
|
+
const navPath = decodePathFromCallback(CALLBACK_NAV_PREFIX, data);
|
|
394
|
+
if (navPath !== null) {
|
|
395
|
+
if (!isWithinProjectRoot(navPath)) {
|
|
396
|
+
await ctx.answerCallbackQuery({ text: t("ls.access_denied") });
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
await navigateTo(ctx, navPath);
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
const pageInfo = decodePaginationCallback(data);
|
|
403
|
+
if (pageInfo !== null) {
|
|
404
|
+
if (!isWithinProjectRoot(pageInfo.path)) {
|
|
405
|
+
await ctx.answerCallbackQuery({ text: t("ls.access_denied") });
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
await navigateTo(ctx, pageInfo.path, pageInfo.page);
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
const fileInfo = decodeFileCallback(data);
|
|
412
|
+
if (fileInfo !== null) {
|
|
413
|
+
if (!isWithinProjectRoot(fileInfo.path)) {
|
|
414
|
+
await ctx.answerCallbackQuery({ text: t("ls.access_denied") });
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
417
|
+
await showFileDetails(ctx, fileInfo.path, fileInfo.page);
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
const downloadPath = decodePathFromCallback(CALLBACK_DOWNLOAD_PREFIX, data);
|
|
421
|
+
if (downloadPath !== null) {
|
|
422
|
+
if (!isWithinProjectRoot(downloadPath)) {
|
|
423
|
+
await ctx.answerCallbackQuery({ text: t("ls.access_denied") });
|
|
424
|
+
return true;
|
|
425
|
+
}
|
|
426
|
+
await downloadFileAndClose(ctx, downloadPath);
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
const backInfo = decodeBackCallback(data);
|
|
430
|
+
if (backInfo !== null) {
|
|
431
|
+
if (!isWithinProjectRoot(backInfo.path)) {
|
|
432
|
+
await ctx.answerCallbackQuery({ text: t("ls.access_denied") });
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
await navigateTo(ctx, backInfo.path, backInfo.page);
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
logger.error("[Ls] Error handling callback:", error);
|
|
442
|
+
await ctx.answerCallbackQuery({ text: t("callback.processing_error") });
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
@@ -210,7 +210,7 @@ export async function openCommand(ctx) {
|
|
|
210
210
|
await ctx.reply(t("open.open_error"));
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
|
-
export async function handleOpenCallback(ctx) {
|
|
213
|
+
export async function handleOpenCallback(ctx, deps = {}) {
|
|
214
214
|
const data = ctx.callbackQuery?.data;
|
|
215
215
|
if (!data || !data.startsWith(CALLBACK_PREFIX)) {
|
|
216
216
|
return false;
|
|
@@ -256,7 +256,7 @@ export async function handleOpenCallback(ctx) {
|
|
|
256
256
|
await ctx.answerCallbackQuery({ text: t("open.access_denied") });
|
|
257
257
|
return true;
|
|
258
258
|
}
|
|
259
|
-
await selectDirectory(ctx, selectPath);
|
|
259
|
+
await selectDirectory(ctx, selectPath, deps);
|
|
260
260
|
return true;
|
|
261
261
|
}
|
|
262
262
|
return false;
|
|
@@ -284,7 +284,7 @@ async function navigateTo(ctx, dirPath, page = 0) {
|
|
|
284
284
|
reply_markup: view.keyboard,
|
|
285
285
|
});
|
|
286
286
|
}
|
|
287
|
-
async function selectDirectory(ctx, directory) {
|
|
287
|
+
async function selectDirectory(ctx, directory, deps = {}) {
|
|
288
288
|
const displayPath = pathToDisplayPath(directory);
|
|
289
289
|
try {
|
|
290
290
|
logger.info(`[Bot] Adding project directory: ${directory}`);
|
|
@@ -295,7 +295,12 @@ async function selectDirectory(ctx, directory) {
|
|
|
295
295
|
// will simply appear in /projects for retry.
|
|
296
296
|
await upsertSessionDirectory(directory, Date.now());
|
|
297
297
|
const projectInfo = await getProjectByWorktree(directory);
|
|
298
|
-
const
|
|
298
|
+
const selectedProjectInfo = { ...projectInfo, name: displayPath };
|
|
299
|
+
const replyKeyboard = deps.ensureEventSubscription
|
|
300
|
+
? await switchToProject(ctx, selectedProjectInfo, "open_project_selected", {
|
|
301
|
+
ensureEventSubscription: deps.ensureEventSubscription,
|
|
302
|
+
})
|
|
303
|
+
: await switchToProject(ctx, selectedProjectInfo, "open_project_selected");
|
|
299
304
|
await ctx.answerCallbackQuery();
|
|
300
305
|
await ctx.reply(t("open.selected", { project: displayPath }), {
|
|
301
306
|
reply_markup: replyKeyboard,
|
|
@@ -148,7 +148,7 @@ export async function projectsCommand(ctx) {
|
|
|
148
148
|
await ctx.reply(t("projects.fetch_error"));
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
-
export async function handleProjectSelect(ctx) {
|
|
151
|
+
export async function handleProjectSelect(ctx, deps = {}) {
|
|
152
152
|
const callbackQuery = ctx.callbackQuery;
|
|
153
153
|
if (!callbackQuery?.data) {
|
|
154
154
|
return false;
|
|
@@ -199,7 +199,11 @@ export async function handleProjectSelect(ctx) {
|
|
|
199
199
|
}
|
|
200
200
|
const projectName = selectedProject.name || selectedProject.worktree;
|
|
201
201
|
logger.info(`[Bot] Project selected: ${projectName} (id: ${projectId})`);
|
|
202
|
-
const keyboard =
|
|
202
|
+
const keyboard = deps.ensureEventSubscription
|
|
203
|
+
? await switchToProject(ctx, selectedProject, "project_switched", {
|
|
204
|
+
ensureEventSubscription: deps.ensureEventSubscription,
|
|
205
|
+
})
|
|
206
|
+
: await switchToProject(ctx, selectedProject, "project_switched");
|
|
203
207
|
await ctx.answerCallbackQuery();
|
|
204
208
|
await ctx.reply(t("projects.selected", { project: projectName }), {
|
|
205
209
|
reply_markup: keyboard,
|