@coinseeker/opencode-telegram-plugin 1.0.3 → 1.0.4
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 +1 -1
- package/dist/telegram-remote.js +298 -46
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ Keep this file private. Never commit or share your Telegram bot token.
|
|
|
54
54
|
- OpenCode question prompts via Telegram inline buttons.
|
|
55
55
|
- Multi-select question prompts with toggle buttons and **Done** submission.
|
|
56
56
|
- Custom free-text answers from Telegram.
|
|
57
|
-
- Permission
|
|
57
|
+
- Permission approve/reject buttons from Telegram.
|
|
58
58
|
- Multi-session-safe Telegram polling through a file-lock leader model.
|
|
59
59
|
- Log file output instead of stdout terminal spam.
|
|
60
60
|
|
package/dist/telegram-remote.js
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
// src/telegram-remote.ts
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
|
-
import { dirname as
|
|
9
|
-
import { tmpdir as
|
|
10
|
-
import { createHash as
|
|
8
|
+
import { dirname as dirname4, join as join6 } from "path";
|
|
9
|
+
import { tmpdir as tmpdir4 } from "os";
|
|
10
|
+
import { createHash as createHash4 } from "crypto";
|
|
11
11
|
|
|
12
12
|
// src/lib/logger.ts
|
|
13
13
|
import { appendFile } from "fs/promises";
|
|
@@ -314,17 +314,103 @@ function createQuestionShortHash(requestID) {
|
|
|
314
314
|
return createHash("sha256").update(requestID).digest("base64url").slice(0, 10);
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
+
// src/lib/pending-permissions.ts
|
|
318
|
+
import { createHash as createHash2 } from "crypto";
|
|
319
|
+
import { mkdir as mkdir3, readFile as readFile4, readdir as readdir2, rename as rename3, unlink as unlink3, writeFile as writeFile3 } from "fs/promises";
|
|
320
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
321
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
322
|
+
function hasCode4(err, code) {
|
|
323
|
+
return "code" in err && err.code === code;
|
|
324
|
+
}
|
|
325
|
+
function pendingFilePath2(dir, shortHash) {
|
|
326
|
+
return join3(dir, `${shortHash}.json`);
|
|
327
|
+
}
|
|
328
|
+
function parsePending2(text) {
|
|
329
|
+
const parsed = JSON.parse(text);
|
|
330
|
+
if (typeof parsed.requestID !== "string") throw new Error("Invalid pending permission: requestID");
|
|
331
|
+
if (typeof parsed.sessionID !== "string") throw new Error("Invalid pending permission: sessionID");
|
|
332
|
+
if (typeof parsed.title !== "string") throw new Error("Invalid pending permission: title");
|
|
333
|
+
if (typeof parsed.permission !== "string") throw new Error("Invalid pending permission: permission");
|
|
334
|
+
if (!Array.isArray(parsed.patterns)) throw new Error("Invalid pending permission: patterns");
|
|
335
|
+
if (!Array.isArray(parsed.always)) throw new Error("Invalid pending permission: always");
|
|
336
|
+
if (parsed.endpoint !== "request" && parsed.endpoint !== "session") throw new Error("Invalid pending permission: endpoint");
|
|
337
|
+
return parsed;
|
|
338
|
+
}
|
|
339
|
+
async function listPendingFiles2(dir) {
|
|
340
|
+
try {
|
|
341
|
+
const entries = await readdir2(dir, { withFileTypes: true });
|
|
342
|
+
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name);
|
|
343
|
+
} catch (err) {
|
|
344
|
+
if (err instanceof Error && hasCode4(err, "ENOENT")) return [];
|
|
345
|
+
throw err;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function shortHashFromFileName2(fileName) {
|
|
349
|
+
return fileName.slice(0, -".json".length);
|
|
350
|
+
}
|
|
351
|
+
function createPendingPermissionStore(opts) {
|
|
352
|
+
const dir = opts.baseDir ?? join3(tmpdir3(), `opencoder-telegram-pending-permissions-${opts.tokenHash}`);
|
|
353
|
+
return {
|
|
354
|
+
dir,
|
|
355
|
+
async savePending(shortHash, data) {
|
|
356
|
+
const filePath = pendingFilePath2(dir, shortHash);
|
|
357
|
+
await mkdir3(dirname3(filePath), { recursive: true });
|
|
358
|
+
const tmpPath = `${filePath}.tmp.${process.pid}`;
|
|
359
|
+
await writeFile3(tmpPath, JSON.stringify(data, null, 2), "utf8");
|
|
360
|
+
await rename3(tmpPath, filePath);
|
|
361
|
+
},
|
|
362
|
+
async loadPending(shortHash) {
|
|
363
|
+
try {
|
|
364
|
+
return parsePending2(await readFile4(pendingFilePath2(dir, shortHash), "utf8"));
|
|
365
|
+
} catch (err) {
|
|
366
|
+
if (err instanceof Error && hasCode4(err, "ENOENT")) return void 0;
|
|
367
|
+
throw err;
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
async deletePending(shortHash) {
|
|
371
|
+
try {
|
|
372
|
+
await unlink3(pendingFilePath2(dir, shortHash));
|
|
373
|
+
} catch (err) {
|
|
374
|
+
if (!(err instanceof Error) || !hasCode4(err, "ENOENT")) throw err;
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
async findByRequestID(requestID) {
|
|
378
|
+
for (const fileName of await listPendingFiles2(dir)) {
|
|
379
|
+
const shortHash = shortHashFromFileName2(fileName);
|
|
380
|
+
const data = await this.loadPending(shortHash);
|
|
381
|
+
if (data?.requestID === requestID) return { shortHash, data };
|
|
382
|
+
}
|
|
383
|
+
return void 0;
|
|
384
|
+
},
|
|
385
|
+
async sweepExpired() {
|
|
386
|
+
const expired = [];
|
|
387
|
+
for (const fileName of await listPendingFiles2(dir)) {
|
|
388
|
+
const shortHash = shortHashFromFileName2(fileName);
|
|
389
|
+
const data = await this.loadPending(shortHash);
|
|
390
|
+
if (data && data.expiresAt < Date.now()) {
|
|
391
|
+
expired.push(data);
|
|
392
|
+
await this.deletePending(shortHash);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return expired;
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
function createPermissionShortHash(requestID) {
|
|
400
|
+
return createHash2("sha256").update(requestID).digest("base64url").slice(0, 10);
|
|
401
|
+
}
|
|
402
|
+
|
|
317
403
|
// src/lib/env-loader.ts
|
|
318
404
|
import { existsSync } from "fs";
|
|
319
405
|
import { homedir as homedir2 } from "os";
|
|
320
|
-
import { join as
|
|
406
|
+
import { join as join4 } from "path";
|
|
321
407
|
import dotenv from "dotenv";
|
|
322
408
|
function loadPluginEnv(opts) {
|
|
323
409
|
const paths = [
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
410
|
+
join4(opts.pluginDir, "../../.env"),
|
|
411
|
+
join4(opts.pluginDir, "..", ".env"),
|
|
412
|
+
join4(opts.pluginDir, ".env"),
|
|
413
|
+
join4(opts.homeDir ?? homedir2(), ".config/opencode/telegram-remote/.env")
|
|
328
414
|
];
|
|
329
415
|
const loadedFrom = [];
|
|
330
416
|
const values = {};
|
|
@@ -384,6 +470,7 @@ function createTelegramBot(opts) {
|
|
|
384
470
|
const bot = new Bot(config.botToken);
|
|
385
471
|
let activeChatId = opts.initialChatId;
|
|
386
472
|
let questionDispatcher;
|
|
473
|
+
let permissionDispatcher;
|
|
387
474
|
if (polling) {
|
|
388
475
|
bot.use(async (ctx, next) => {
|
|
389
476
|
const userId = ctx.from?.id;
|
|
@@ -424,6 +511,13 @@ This chat is now active for OpenCode notifications.`);
|
|
|
424
511
|
if (!questionDispatcher || messageId === void 0 || chatId === void 0 || userId === void 0) return;
|
|
425
512
|
await questionDispatcher.handleCallbackQuery(data, messageId, chatId, userId);
|
|
426
513
|
});
|
|
514
|
+
bot.callbackQuery(/^p:([^:]+):(o|a|r)$/, async (ctx) => {
|
|
515
|
+
await ctx.answerCallbackQuery();
|
|
516
|
+
const data = ctx.callbackQuery.data;
|
|
517
|
+
const messageId = ctx.callbackQuery.message?.message_id;
|
|
518
|
+
if (!permissionDispatcher || messageId === void 0) return;
|
|
519
|
+
await permissionDispatcher.handleCallbackQuery(data, messageId);
|
|
520
|
+
});
|
|
427
521
|
bot.on("message:text", async (ctx) => {
|
|
428
522
|
const replyToMessageId = ctx.message.reply_to_message?.message_id;
|
|
429
523
|
const chatId = ctx.chat.id;
|
|
@@ -511,6 +605,9 @@ ${question.question}`, { reply_markup: { inline_keyboard: inlineKeyboard } });
|
|
|
511
605
|
},
|
|
512
606
|
setQuestionDispatcher(dispatcher) {
|
|
513
607
|
questionDispatcher = dispatcher;
|
|
608
|
+
},
|
|
609
|
+
setPermissionDispatcher(dispatcher) {
|
|
610
|
+
permissionDispatcher = dispatcher;
|
|
514
611
|
}
|
|
515
612
|
};
|
|
516
613
|
}
|
|
@@ -585,29 +682,29 @@ var SessionTitleService = class {
|
|
|
585
682
|
};
|
|
586
683
|
|
|
587
684
|
// src/lib/claim.ts
|
|
588
|
-
import { mkdir as
|
|
589
|
-
import { join as
|
|
590
|
-
import { createHash as
|
|
685
|
+
import { mkdir as mkdir4, open as open2, readdir as readdir3, stat as stat2, unlink as unlink4 } from "fs/promises";
|
|
686
|
+
import { join as join5 } from "path";
|
|
687
|
+
import { createHash as createHash3 } from "crypto";
|
|
591
688
|
var DEFAULT_TTL_MS2 = 6e4;
|
|
592
689
|
var sweptDirs = /* @__PURE__ */ new Set();
|
|
593
|
-
function
|
|
690
|
+
function hasCode5(err, code) {
|
|
594
691
|
return "code" in err && err.code === code;
|
|
595
692
|
}
|
|
596
693
|
function claimPath(claimsDir, key) {
|
|
597
|
-
const hash =
|
|
598
|
-
return
|
|
694
|
+
const hash = createHash3("sha256").update(key).digest("hex").slice(0, 16);
|
|
695
|
+
return join5(claimsDir, `${hash}.claim`);
|
|
599
696
|
}
|
|
600
697
|
async function sweep(claimsDir, ttlMs) {
|
|
601
698
|
if (sweptDirs.has(claimsDir)) return;
|
|
602
699
|
sweptDirs.add(claimsDir);
|
|
603
700
|
try {
|
|
604
|
-
const entries = await
|
|
701
|
+
const entries = await readdir3(claimsDir, { withFileTypes: true });
|
|
605
702
|
await Promise.all(entries.filter((entry) => entry.isFile() && entry.name.endsWith(".claim")).map(async (entry) => {
|
|
606
|
-
const filePath =
|
|
703
|
+
const filePath = join5(claimsDir, entry.name);
|
|
607
704
|
try {
|
|
608
705
|
const fileStat = await stat2(filePath);
|
|
609
706
|
if (Date.now() - fileStat.mtimeMs > ttlMs * 2) {
|
|
610
|
-
await
|
|
707
|
+
await unlink4(filePath);
|
|
611
708
|
}
|
|
612
709
|
} catch {
|
|
613
710
|
}
|
|
@@ -626,20 +723,20 @@ async function createClaim(filePath) {
|
|
|
626
723
|
}
|
|
627
724
|
async function claimOnce(opts) {
|
|
628
725
|
const ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS2;
|
|
629
|
-
await
|
|
726
|
+
await mkdir4(opts.claimsDir, { recursive: true });
|
|
630
727
|
await sweep(opts.claimsDir, ttlMs);
|
|
631
728
|
const filePath = claimPath(opts.claimsDir, opts.key);
|
|
632
729
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
633
730
|
try {
|
|
634
731
|
return await createClaim(filePath);
|
|
635
732
|
} catch (err) {
|
|
636
|
-
if (!(err instanceof Error) || !
|
|
733
|
+
if (!(err instanceof Error) || !hasCode5(err, "EEXIST")) throw err;
|
|
637
734
|
try {
|
|
638
735
|
const fileStat = await stat2(filePath);
|
|
639
736
|
if (Date.now() - fileStat.mtimeMs <= ttlMs || attempt === 1) return false;
|
|
640
|
-
await
|
|
737
|
+
await unlink4(filePath);
|
|
641
738
|
} catch (statErr) {
|
|
642
|
-
if (statErr instanceof Error &&
|
|
739
|
+
if (statErr instanceof Error && hasCode5(statErr, "ENOENT")) continue;
|
|
643
740
|
return false;
|
|
644
741
|
}
|
|
645
742
|
}
|
|
@@ -764,28 +861,158 @@ async function handleSessionUpdated(event, ctx) {
|
|
|
764
861
|
}
|
|
765
862
|
|
|
766
863
|
// src/events/permission-updated.ts
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
864
|
+
var PERMISSION_EXPIRY_MS = 5 * 6e4;
|
|
865
|
+
var CALLBACK_RE = /^p:([^:]+):(o|a|r)$/;
|
|
866
|
+
function isStringArray(value) {
|
|
867
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
868
|
+
}
|
|
869
|
+
function isEventPermissionAsked(event) {
|
|
870
|
+
if (event.type !== "permission.asked") return false;
|
|
871
|
+
const props = event.properties;
|
|
872
|
+
if (!props) return false;
|
|
873
|
+
if (typeof props.id !== "string") return false;
|
|
874
|
+
if (typeof props.sessionID !== "string") return false;
|
|
875
|
+
if (typeof props.permission !== "string") return false;
|
|
876
|
+
if (!isStringArray(props.patterns)) return false;
|
|
877
|
+
if (!isStringArray(props.always)) return false;
|
|
878
|
+
return true;
|
|
879
|
+
}
|
|
880
|
+
function buildCallbackData(shortHash, reply) {
|
|
881
|
+
const data = `p:${shortHash}:${reply}`;
|
|
882
|
+
if (Buffer.byteLength(data, "utf8") > 64) throw new Error("Telegram callback_data exceeds 64 bytes");
|
|
883
|
+
return data;
|
|
884
|
+
}
|
|
885
|
+
function normalizeUpdated(permission) {
|
|
886
|
+
const pattern = permission.pattern === void 0 ? [] : Array.isArray(permission.pattern) ? permission.pattern : [permission.pattern];
|
|
887
|
+
return {
|
|
888
|
+
requestID: permission.id,
|
|
889
|
+
sessionID: permission.sessionID,
|
|
890
|
+
title: permission.title,
|
|
891
|
+
permission: permission.type,
|
|
892
|
+
patterns: pattern,
|
|
893
|
+
always: [],
|
|
894
|
+
endpoint: "session",
|
|
895
|
+
claimKey: `permission.updated:${permission.id}`
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
function normalizeAsked(permission) {
|
|
899
|
+
return {
|
|
900
|
+
requestID: permission.id,
|
|
901
|
+
sessionID: permission.sessionID,
|
|
902
|
+
title: permission.patterns.join(", ") || permission.permission,
|
|
903
|
+
permission: permission.permission,
|
|
904
|
+
patterns: permission.patterns,
|
|
905
|
+
always: permission.always,
|
|
906
|
+
endpoint: "request",
|
|
907
|
+
claimKey: `permission.asked:${permission.id}`
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
function permissionMessage(permission, sessionTitle) {
|
|
772
911
|
const titleLine = sessionTitle ? `\u{1F4CB} ${sessionTitle}` : `Session: ${permission.sessionID}`;
|
|
773
|
-
const
|
|
912
|
+
const patterns = permission.patterns.length > 0 ? `
|
|
913
|
+
Patterns: ${permission.patterns.join(", ")}` : "";
|
|
914
|
+
const always = permission.always.length > 0 ? `
|
|
915
|
+
Always options: ${permission.always.join(", ")}` : "";
|
|
916
|
+
return `\u2753 Permission requested
|
|
774
917
|
|
|
775
918
|
${titleLine}
|
|
776
919
|
|
|
777
|
-
|
|
778
|
-
Detail: ${permission.title}`;
|
|
920
|
+
Permission: ${permission.permission}
|
|
921
|
+
Detail: ${permission.title}${patterns}${always}`;
|
|
922
|
+
}
|
|
923
|
+
function permissionKeyboard(shortHash) {
|
|
924
|
+
return [
|
|
925
|
+
[{ text: "\u2705 Allow once", callback_data: buildCallbackData(shortHash, "o") }],
|
|
926
|
+
[{ text: "\u267B\uFE0F Always allow", callback_data: buildCallbackData(shortHash, "a") }],
|
|
927
|
+
[{ text: "\u274C Reject", callback_data: buildCallbackData(shortHash, "r") }]
|
|
928
|
+
];
|
|
929
|
+
}
|
|
930
|
+
function replyFromSelection(selection) {
|
|
931
|
+
if (selection === "o") return "once";
|
|
932
|
+
if (selection === "a") return "always";
|
|
933
|
+
if (selection === "r") return "reject";
|
|
934
|
+
return void 0;
|
|
935
|
+
}
|
|
936
|
+
function replyLabel(reply) {
|
|
937
|
+
if (reply === "once") return "Allowed once";
|
|
938
|
+
if (reply === "always") return "Always allowed";
|
|
939
|
+
return "Rejected";
|
|
940
|
+
}
|
|
941
|
+
async function handleNormalizedPermission(permission, ctx) {
|
|
942
|
+
const claimed = await claimOnce({ claimsDir: ctx.claimsDir, key: permission.claimKey });
|
|
943
|
+
if (!claimed) return;
|
|
944
|
+
const shortHash = createPermissionShortHash(permission.requestID);
|
|
945
|
+
const sentAt = Date.now();
|
|
946
|
+
const rawSessionTitle = ctx.sessionTitleService.getSessionTitle(permission.sessionID);
|
|
947
|
+
const sessionTitle = rawSessionTitle === null ? void 0 : rawSessionTitle;
|
|
779
948
|
try {
|
|
780
|
-
await ctx.bot.sendMessage(
|
|
949
|
+
const message = await ctx.bot.sendMessage(permissionMessage(permission, sessionTitle), {
|
|
950
|
+
reply_markup: { inline_keyboard: permissionKeyboard(shortHash) }
|
|
951
|
+
});
|
|
952
|
+
const pending = {
|
|
953
|
+
requestID: permission.requestID,
|
|
954
|
+
sessionID: permission.sessionID,
|
|
955
|
+
title: permission.title,
|
|
956
|
+
permission: permission.permission,
|
|
957
|
+
patterns: permission.patterns,
|
|
958
|
+
always: permission.always,
|
|
959
|
+
sentAt,
|
|
960
|
+
expiresAt: sentAt + PERMISSION_EXPIRY_MS,
|
|
961
|
+
telegramMessageId: message.message_id,
|
|
962
|
+
endpoint: permission.endpoint
|
|
963
|
+
};
|
|
964
|
+
await ctx.pendingPermissions.savePending(shortHash, pending);
|
|
781
965
|
} catch (err) {
|
|
782
966
|
ctx.logger.error("failed to send permission notification", { error: String(err) });
|
|
783
967
|
}
|
|
784
968
|
}
|
|
969
|
+
async function expirePending(ctx, shortHash, pending, messageId) {
|
|
970
|
+
await ctx.bot.editMessageRemoveKeyboard(messageId, "\u23F1 Permission request expired");
|
|
971
|
+
await ctx.pendingPermissions.deletePending(shortHash);
|
|
972
|
+
ctx.logger.info("pending permission expired", { requestID: pending.requestID });
|
|
973
|
+
}
|
|
974
|
+
async function handlePermissionUpdated(event, ctx) {
|
|
975
|
+
await handleNormalizedPermission(normalizeUpdated(event.properties), ctx);
|
|
976
|
+
}
|
|
977
|
+
async function handlePermissionAsked(event, ctx) {
|
|
978
|
+
await handleNormalizedPermission(normalizeAsked(event.properties), ctx);
|
|
979
|
+
}
|
|
980
|
+
function createPermissionDispatcher(ctx) {
|
|
981
|
+
return {
|
|
982
|
+
async handleCallbackQuery(data, messageId) {
|
|
983
|
+
const match = CALLBACK_RE.exec(data);
|
|
984
|
+
if (!match) return;
|
|
985
|
+
const shortHash = match[1];
|
|
986
|
+
const reply = replyFromSelection(match[2]);
|
|
987
|
+
if (!reply) return;
|
|
988
|
+
const pending = await ctx.pendingPermissions.loadPending(shortHash);
|
|
989
|
+
if (!pending) {
|
|
990
|
+
await ctx.bot.editMessageRemoveKeyboard(messageId, "This permission request has expired.");
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
if (pending.expiresAt < Date.now()) {
|
|
994
|
+
await expirePending(ctx, shortHash, pending, messageId);
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
try {
|
|
998
|
+
await ctx.replyToPermission(pending.requestID, pending.sessionID, reply, pending.endpoint);
|
|
999
|
+
await ctx.bot.editMessageRemoveKeyboard(messageId, `\u2705 Permission ${replyLabel(reply)}
|
|
1000
|
+
|
|
1001
|
+
${pending.permission}: ${pending.title}`);
|
|
1002
|
+
ctx.logger.info("permission reply sent", { requestID: pending.requestID, sessionID: pending.sessionID, reply });
|
|
1003
|
+
} catch (err) {
|
|
1004
|
+
await ctx.bot.editMessageRemoveKeyboard(messageId, "\u26A0\uFE0F Failed to send permission reply to opencode");
|
|
1005
|
+
ctx.logger.error("failed to send permission reply", { error: String(err), requestID: pending.requestID });
|
|
1006
|
+
} finally {
|
|
1007
|
+
await ctx.pendingPermissions.deletePending(shortHash);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
785
1012
|
|
|
786
1013
|
// src/events/question-asked.ts
|
|
787
1014
|
var QUESTION_EXPIRY_MS = 5 * 6e4;
|
|
788
|
-
var
|
|
1015
|
+
var CALLBACK_RE2 = /^q:([^:]+):(\d+):(\d+|c|d)$/;
|
|
789
1016
|
function isQuestionOption(value) {
|
|
790
1017
|
return typeof value.label === "string" && typeof value.description === "string";
|
|
791
1018
|
}
|
|
@@ -804,14 +1031,14 @@ function isEventQuestionAsked(event) {
|
|
|
804
1031
|
if (!Array.isArray(props.questions)) return false;
|
|
805
1032
|
return props.questions.every((question) => typeof question === "object" && question !== null && isQuestionInfo(question));
|
|
806
1033
|
}
|
|
807
|
-
function
|
|
1034
|
+
function buildCallbackData2(shortHash, questionIndex, optionIndex) {
|
|
808
1035
|
const data = `q:${shortHash}:${questionIndex}:${optionIndex}`;
|
|
809
1036
|
if (Buffer.byteLength(data, "utf8") > 64) throw new Error("Telegram callback_data exceeds 64 bytes");
|
|
810
1037
|
return data;
|
|
811
1038
|
}
|
|
812
1039
|
function callbackDataForQuestion(shortHash, questionIndex, question) {
|
|
813
|
-
const data = question.options.map((_, optionIndex) =>
|
|
814
|
-
if (question.custom !== false) data.push(
|
|
1040
|
+
const data = question.options.map((_, optionIndex) => buildCallbackData2(shortHash, questionIndex, optionIndex));
|
|
1041
|
+
if (question.custom !== false) data.push(buildCallbackData2(shortHash, questionIndex, "c"));
|
|
815
1042
|
return data;
|
|
816
1043
|
}
|
|
817
1044
|
function useSimpleQuestionKeyboard(question) {
|
|
@@ -824,13 +1051,13 @@ function questionInlineKeyboard(shortHash, questionIndex, question, selected) {
|
|
|
824
1051
|
const multiple = question.multiple === true;
|
|
825
1052
|
const inlineKeyboard = question.options.map((option, optionIndex) => [{
|
|
826
1053
|
text: multiple && selected.includes(option.label) ? `\u2705 ${option.label}` : option.label,
|
|
827
|
-
callback_data:
|
|
1054
|
+
callback_data: buildCallbackData2(shortHash, questionIndex, optionIndex)
|
|
828
1055
|
}]);
|
|
829
1056
|
if (question.custom !== false) {
|
|
830
|
-
inlineKeyboard.push([{ text: "\u270F\uFE0F Custom answer", callback_data:
|
|
1057
|
+
inlineKeyboard.push([{ text: "\u270F\uFE0F Custom answer", callback_data: buildCallbackData2(shortHash, questionIndex, "c") }]);
|
|
831
1058
|
}
|
|
832
1059
|
if (multiple) {
|
|
833
|
-
inlineKeyboard.push([{ text: "\u2705 Done", callback_data:
|
|
1060
|
+
inlineKeyboard.push([{ text: "\u2705 Done", callback_data: buildCallbackData2(shortHash, questionIndex, "d") }]);
|
|
834
1061
|
}
|
|
835
1062
|
return inlineKeyboard;
|
|
836
1063
|
}
|
|
@@ -878,7 +1105,7 @@ ${answerSummary(pending.questions, answers)}`);
|
|
|
878
1105
|
await ctx.pendingQuestions.deletePending(shortHash);
|
|
879
1106
|
}
|
|
880
1107
|
}
|
|
881
|
-
async function
|
|
1108
|
+
async function expirePending2(ctx, shortHash, pending, messageId) {
|
|
882
1109
|
await ctx.bot.editMessageRemoveKeyboard(messageId, "\u23F1 Question expired");
|
|
883
1110
|
await ctx.pendingQuestions.deletePending(shortHash);
|
|
884
1111
|
ctx.logger.info("pending question expired", { requestID: pending.requestID });
|
|
@@ -917,7 +1144,7 @@ async function handleQuestionAsked(event, ctx) {
|
|
|
917
1144
|
function createQuestionDispatcher(ctx) {
|
|
918
1145
|
return {
|
|
919
1146
|
async handleCallbackQuery(data, messageId, chatId, userId) {
|
|
920
|
-
const match =
|
|
1147
|
+
const match = CALLBACK_RE2.exec(data);
|
|
921
1148
|
if (!match) return;
|
|
922
1149
|
const shortHash = match[1];
|
|
923
1150
|
const questionIndex = Number(match[2]);
|
|
@@ -928,7 +1155,7 @@ function createQuestionDispatcher(ctx) {
|
|
|
928
1155
|
return;
|
|
929
1156
|
}
|
|
930
1157
|
if (pending.expiresAt < Date.now()) {
|
|
931
|
-
await
|
|
1158
|
+
await expirePending2(ctx, shortHash, pending, messageId);
|
|
932
1159
|
return;
|
|
933
1160
|
}
|
|
934
1161
|
const question = pending.questions[questionIndex];
|
|
@@ -971,7 +1198,7 @@ function createQuestionDispatcher(ctx) {
|
|
|
971
1198
|
const awaiting = match.data.awaitingCustomFor;
|
|
972
1199
|
if (!awaiting || awaiting.promptMessageId !== replyToMessageId) return;
|
|
973
1200
|
if (match.data.expiresAt < Date.now()) {
|
|
974
|
-
await
|
|
1201
|
+
await expirePending2(ctx, match.shortHash, match.data, match.data.telegramMessageIds[0]);
|
|
975
1202
|
return;
|
|
976
1203
|
}
|
|
977
1204
|
const question = match.data.questions[awaiting.questionIndex];
|
|
@@ -1012,7 +1239,7 @@ async function handleQuestionReplied(event, ctx) {
|
|
|
1012
1239
|
}
|
|
1013
1240
|
|
|
1014
1241
|
// src/telegram-remote.ts
|
|
1015
|
-
var pluginDir =
|
|
1242
|
+
var pluginDir = dirname4(fileURLToPath(import.meta.url));
|
|
1016
1243
|
var TelegramRemote = async (input) => {
|
|
1017
1244
|
const logger = createLogger({ namespace: "telegram" });
|
|
1018
1245
|
try {
|
|
@@ -1021,10 +1248,11 @@ var TelegramRemote = async (input) => {
|
|
|
1021
1248
|
const config = loadConfig({ logger, env: process.env });
|
|
1022
1249
|
const stateStore = createStateStore();
|
|
1023
1250
|
const initialState = await stateStore.read();
|
|
1024
|
-
const tokenHash =
|
|
1025
|
-
const lockPath =
|
|
1026
|
-
const claimsDir =
|
|
1251
|
+
const tokenHash = createHash4("sha256").update(config.botToken).digest("hex").slice(0, 16);
|
|
1252
|
+
const lockPath = join6(tmpdir4(), `opencoder-telegram-${tokenHash}.lock`);
|
|
1253
|
+
const claimsDir = join6(tmpdir4(), `opencoder-telegram-claims-${tokenHash}`);
|
|
1027
1254
|
const pendingQuestions = createPendingQuestionStore({ tokenHash });
|
|
1255
|
+
const pendingPermissions = createPendingPermissionStore({ tokenHash });
|
|
1028
1256
|
const lockResult = await acquireLock({ lockPath });
|
|
1029
1257
|
const isLeader = lockResult.acquired;
|
|
1030
1258
|
logger.info(
|
|
@@ -1042,6 +1270,23 @@ var TelegramRemote = async (input) => {
|
|
|
1042
1270
|
throwOnError: true
|
|
1043
1271
|
});
|
|
1044
1272
|
};
|
|
1273
|
+
const replyToPermission = async (requestID, sessionID, reply, endpoint) => {
|
|
1274
|
+
if (endpoint === "request") {
|
|
1275
|
+
await client._client.post({
|
|
1276
|
+
url: `/permission/${encodeURIComponent(requestID)}/reply`,
|
|
1277
|
+
headers: { "Content-Type": "application/json" },
|
|
1278
|
+
body: { reply },
|
|
1279
|
+
throwOnError: true
|
|
1280
|
+
});
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
await client._client.post({
|
|
1284
|
+
url: `/session/${encodeURIComponent(sessionID)}/permissions/${encodeURIComponent(requestID)}`,
|
|
1285
|
+
headers: { "Content-Type": "application/json" },
|
|
1286
|
+
body: { response: reply },
|
|
1287
|
+
throwOnError: true
|
|
1288
|
+
});
|
|
1289
|
+
};
|
|
1045
1290
|
const bot = createTelegramBot({
|
|
1046
1291
|
config,
|
|
1047
1292
|
stateStore,
|
|
@@ -1085,10 +1330,13 @@ var TelegramRemote = async (input) => {
|
|
|
1085
1330
|
serverUrl: input.serverUrl,
|
|
1086
1331
|
tokenHash,
|
|
1087
1332
|
pendingQuestions,
|
|
1088
|
-
|
|
1333
|
+
pendingPermissions,
|
|
1334
|
+
replyToQuestion,
|
|
1335
|
+
replyToPermission
|
|
1089
1336
|
};
|
|
1090
1337
|
if (isLeader) {
|
|
1091
1338
|
bot.setQuestionDispatcher(createQuestionDispatcher(ctx));
|
|
1339
|
+
bot.setPermissionDispatcher(createPermissionDispatcher(ctx));
|
|
1092
1340
|
}
|
|
1093
1341
|
return {
|
|
1094
1342
|
event: async ({ event }) => {
|
|
@@ -1106,6 +1354,10 @@ var TelegramRemote = async (input) => {
|
|
|
1106
1354
|
case "permission.updated":
|
|
1107
1355
|
return handlePermissionUpdated(event, ctx);
|
|
1108
1356
|
default: {
|
|
1357
|
+
if (isEventPermissionAsked(extEvent)) {
|
|
1358
|
+
if (!isLeader) return;
|
|
1359
|
+
return handlePermissionAsked(extEvent, ctx);
|
|
1360
|
+
}
|
|
1109
1361
|
if (isEventSessionError(extEvent)) {
|
|
1110
1362
|
return handleSessionError(extEvent, ctx);
|
|
1111
1363
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coinseeker/opencode-telegram-plugin",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Control and monitor OpenCode from Telegram with notifications, question replies, and subagent-aware completion.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/telegram-remote.js",
|