@songsid/agend 0.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 +210 -0
- package/README.zh-TW.md +134 -0
- package/dist/access-path.d.ts +10 -0
- package/dist/access-path.js +32 -0
- package/dist/access-path.js.map +1 -0
- package/dist/adapter-world.d.ts +25 -0
- package/dist/adapter-world.js +41 -0
- package/dist/adapter-world.js.map +1 -0
- package/dist/agent-cli-instructions.md +50 -0
- package/dist/agent-cli.d.ts +2 -0
- package/dist/agent-cli.js +200 -0
- package/dist/agent-cli.js.map +1 -0
- package/dist/agent-endpoint.d.ts +25 -0
- package/dist/agent-endpoint.js +162 -0
- package/dist/agent-endpoint.js.map +1 -0
- package/dist/backend/antigravity.d.ts +17 -0
- package/dist/backend/antigravity.js +98 -0
- package/dist/backend/antigravity.js.map +1 -0
- package/dist/backend/claude-code.d.ts +23 -0
- package/dist/backend/claude-code.js +171 -0
- package/dist/backend/claude-code.js.map +1 -0
- package/dist/backend/codex.d.ts +18 -0
- package/dist/backend/codex.js +160 -0
- package/dist/backend/codex.js.map +1 -0
- package/dist/backend/factory.d.ts +2 -0
- package/dist/backend/factory.js +28 -0
- package/dist/backend/factory.js.map +1 -0
- package/dist/backend/gemini-cli.d.ts +17 -0
- package/dist/backend/gemini-cli.js +163 -0
- package/dist/backend/gemini-cli.js.map +1 -0
- package/dist/backend/index.d.ts +7 -0
- package/dist/backend/index.js +7 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/kiro.d.ts +17 -0
- package/dist/backend/kiro.js +147 -0
- package/dist/backend/kiro.js.map +1 -0
- package/dist/backend/marker-utils.d.ts +13 -0
- package/dist/backend/marker-utils.js +64 -0
- package/dist/backend/marker-utils.js.map +1 -0
- package/dist/backend/mock.d.ts +25 -0
- package/dist/backend/mock.js +85 -0
- package/dist/backend/mock.js.map +1 -0
- package/dist/backend/opencode.d.ts +16 -0
- package/dist/backend/opencode.js +136 -0
- package/dist/backend/opencode.js.map +1 -0
- package/dist/backend/types.d.ts +86 -0
- package/dist/backend/types.js +33 -0
- package/dist/backend/types.js.map +1 -0
- package/dist/channel/access-manager.d.ts +18 -0
- package/dist/channel/access-manager.js +153 -0
- package/dist/channel/access-manager.js.map +1 -0
- package/dist/channel/adapters/telegram.d.ts +63 -0
- package/dist/channel/adapters/telegram.js +646 -0
- package/dist/channel/adapters/telegram.js.map +1 -0
- package/dist/channel/attachment-handler.d.ts +15 -0
- package/dist/channel/attachment-handler.js +88 -0
- package/dist/channel/attachment-handler.js.map +1 -0
- package/dist/channel/factory.d.ts +12 -0
- package/dist/channel/factory.js +67 -0
- package/dist/channel/factory.js.map +1 -0
- package/dist/channel/ipc-bridge.d.ts +26 -0
- package/dist/channel/ipc-bridge.js +220 -0
- package/dist/channel/ipc-bridge.js.map +1 -0
- package/dist/channel/mcp-server.d.ts +10 -0
- package/dist/channel/mcp-server.js +288 -0
- package/dist/channel/mcp-server.js.map +1 -0
- package/dist/channel/mcp-tools.d.ts +17 -0
- package/dist/channel/mcp-tools.js +110 -0
- package/dist/channel/mcp-tools.js.map +1 -0
- package/dist/channel/message-bus.d.ts +17 -0
- package/dist/channel/message-bus.js +86 -0
- package/dist/channel/message-bus.js.map +1 -0
- package/dist/channel/message-queue.d.ts +39 -0
- package/dist/channel/message-queue.js +253 -0
- package/dist/channel/message-queue.js.map +1 -0
- package/dist/channel/tool-router.d.ts +6 -0
- package/dist/channel/tool-router.js +75 -0
- package/dist/channel/tool-router.js.map +1 -0
- package/dist/channel/tool-tracker.d.ts +13 -0
- package/dist/channel/tool-tracker.js +58 -0
- package/dist/channel/tool-tracker.js.map +1 -0
- package/dist/channel/types.d.ts +118 -0
- package/dist/channel/types.js +2 -0
- package/dist/channel/types.js.map +1 -0
- package/dist/chat-export.d.ts +4 -0
- package/dist/chat-export.js +91 -0
- package/dist/chat-export.js.map +1 -0
- package/dist/classic-channel-manager.d.ts +59 -0
- package/dist/classic-channel-manager.js +193 -0
- package/dist/classic-channel-manager.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1833 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +118 -0
- package/dist/config.js.map +1 -0
- package/dist/context-guardian.d.ts +26 -0
- package/dist/context-guardian.js +73 -0
- package/dist/context-guardian.js.map +1 -0
- package/dist/cost-guard.d.ts +36 -0
- package/dist/cost-guard.js +147 -0
- package/dist/cost-guard.js.map +1 -0
- package/dist/daemon-entry.d.ts +1 -0
- package/dist/daemon-entry.js +29 -0
- package/dist/daemon-entry.js.map +1 -0
- package/dist/daemon.d.ts +152 -0
- package/dist/daemon.js +1714 -0
- package/dist/daemon.js.map +1 -0
- package/dist/daily-summary.d.ts +13 -0
- package/dist/daily-summary.js +55 -0
- package/dist/daily-summary.js.map +1 -0
- package/dist/event-log.d.ts +36 -0
- package/dist/event-log.js +100 -0
- package/dist/event-log.js.map +1 -0
- package/dist/export-import.d.ts +2 -0
- package/dist/export-import.js +162 -0
- package/dist/export-import.js.map +1 -0
- package/dist/fleet-context.d.ts +61 -0
- package/dist/fleet-context.js +4 -0
- package/dist/fleet-context.js.map +1 -0
- package/dist/fleet-dashboard-html.d.ts +6 -0
- package/dist/fleet-dashboard-html.js +443 -0
- package/dist/fleet-dashboard-html.js.map +1 -0
- package/dist/fleet-health-server.d.ts +35 -0
- package/dist/fleet-health-server.js +290 -0
- package/dist/fleet-health-server.js.map +1 -0
- package/dist/fleet-instructions.d.ts +5 -0
- package/dist/fleet-instructions.js +161 -0
- package/dist/fleet-instructions.js.map +1 -0
- package/dist/fleet-manager.d.ts +212 -0
- package/dist/fleet-manager.js +3655 -0
- package/dist/fleet-manager.js.map +1 -0
- package/dist/fleet-rpc-handlers.d.ts +42 -0
- package/dist/fleet-rpc-handlers.js +356 -0
- package/dist/fleet-rpc-handlers.js.map +1 -0
- package/dist/fleet-system-prompt.d.ts +11 -0
- package/dist/fleet-system-prompt.js +61 -0
- package/dist/fleet-system-prompt.js.map +1 -0
- package/dist/general-knowledge/skills.md +177 -0
- package/dist/hang-detector.d.ts +16 -0
- package/dist/hang-detector.js +53 -0
- package/dist/hang-detector.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/instance-lifecycle.d.ts +90 -0
- package/dist/instance-lifecycle.js +592 -0
- package/dist/instance-lifecycle.js.map +1 -0
- package/dist/instructions.d.ts +15 -0
- package/dist/instructions.js +90 -0
- package/dist/instructions.js.map +1 -0
- package/dist/logger.d.ts +7 -0
- package/dist/logger.js +84 -0
- package/dist/logger.js.map +1 -0
- package/dist/outbound-handlers.d.ts +51 -0
- package/dist/outbound-handlers.js +739 -0
- package/dist/outbound-handlers.js.map +1 -0
- package/dist/outbound-schemas.d.ts +238 -0
- package/dist/outbound-schemas.js +248 -0
- package/dist/outbound-schemas.js.map +1 -0
- package/dist/paths.d.ts +10 -0
- package/dist/paths.js +42 -0
- package/dist/paths.js.map +1 -0
- package/dist/plugin/agend/.claude-plugin/plugin.json +5 -0
- package/dist/quickstart.d.ts +1 -0
- package/dist/quickstart.js +595 -0
- package/dist/quickstart.js.map +1 -0
- package/dist/routing-engine.d.ts +22 -0
- package/dist/routing-engine.js +44 -0
- package/dist/routing-engine.js.map +1 -0
- package/dist/safe-async.d.ts +6 -0
- package/dist/safe-async.js +20 -0
- package/dist/safe-async.js.map +1 -0
- package/dist/scheduler/db.d.ts +37 -0
- package/dist/scheduler/db.js +360 -0
- package/dist/scheduler/db.js.map +1 -0
- package/dist/scheduler/db.test.d.ts +1 -0
- package/dist/scheduler/db.test.js +92 -0
- package/dist/scheduler/db.test.js.map +1 -0
- package/dist/scheduler/index.d.ts +4 -0
- package/dist/scheduler/index.js +4 -0
- package/dist/scheduler/index.js.map +1 -0
- package/dist/scheduler/scheduler.d.ts +44 -0
- package/dist/scheduler/scheduler.js +197 -0
- package/dist/scheduler/scheduler.js.map +1 -0
- package/dist/scheduler/scheduler.test.d.ts +1 -0
- package/dist/scheduler/scheduler.test.js +119 -0
- package/dist/scheduler/scheduler.test.js.map +1 -0
- package/dist/scheduler/types.d.ts +107 -0
- package/dist/scheduler/types.js +7 -0
- package/dist/scheduler/types.js.map +1 -0
- package/dist/service-installer.d.ts +17 -0
- package/dist/service-installer.js +182 -0
- package/dist/service-installer.js.map +1 -0
- package/dist/setup-wizard.d.ts +48 -0
- package/dist/setup-wizard.js +701 -0
- package/dist/setup-wizard.js.map +1 -0
- package/dist/statusline-watcher.d.ts +34 -0
- package/dist/statusline-watcher.js +73 -0
- package/dist/statusline-watcher.js.map +1 -0
- package/dist/stt.d.ts +10 -0
- package/dist/stt.js +33 -0
- package/dist/stt.js.map +1 -0
- package/dist/tmux-control.d.ts +52 -0
- package/dist/tmux-control.js +207 -0
- package/dist/tmux-control.js.map +1 -0
- package/dist/tmux-manager.d.ts +44 -0
- package/dist/tmux-manager.js +218 -0
- package/dist/tmux-manager.js.map +1 -0
- package/dist/topic-archiver.d.ts +40 -0
- package/dist/topic-archiver.js +103 -0
- package/dist/topic-archiver.js.map +1 -0
- package/dist/topic-commands.d.ts +28 -0
- package/dist/topic-commands.js +359 -0
- package/dist/topic-commands.js.map +1 -0
- package/dist/transcript-monitor.d.ts +23 -0
- package/dist/transcript-monitor.js +164 -0
- package/dist/transcript-monitor.js.map +1 -0
- package/dist/types.d.ts +211 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/dashboard.html +719 -0
- package/dist/web-api.d.ts +101 -0
- package/dist/web-api.js +648 -0
- package/dist/web-api.js.map +1 -0
- package/dist/webhook-emitter.d.ts +15 -0
- package/dist/webhook-emitter.js +41 -0
- package/dist/webhook-emitter.js.map +1 -0
- package/dist/workflow-templates/default.md +35 -0
- package/package.json +76 -0
- package/templates/launchd.plist.ejs +31 -0
- package/templates/systemd.service.ejs +16 -0
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { createReadStream, mkdirSync, readdirSync, statSync, unlinkSync } from "node:fs";
|
|
4
|
+
import { join, extname, basename } from "node:path";
|
|
5
|
+
import { Bot, GrammyError, InputFile } from "grammy";
|
|
6
|
+
import { InlineKeyboard } from "grammy";
|
|
7
|
+
import { MessageQueue } from "../message-queue.js";
|
|
8
|
+
const IMAGE_EXTENSIONS = new Set([".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"]);
|
|
9
|
+
/** Convert a threadId string to a Telegram message_thread_id number.
|
|
10
|
+
* Returns undefined for null/undefined or for the General topic (id=1),
|
|
11
|
+
* which must not receive message_thread_id in Telegram API calls. */
|
|
12
|
+
function toThreadId(threadId) {
|
|
13
|
+
if (threadId == null || threadId === "1")
|
|
14
|
+
return undefined;
|
|
15
|
+
return Number(threadId);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Whitelist of legitimate Telegram API roots. Misconfiguring `apiRoot`
|
|
19
|
+
* (or having it set by an attacker via a config file write) would leak
|
|
20
|
+
* the bot token — every API call sends the token in the path. Only the
|
|
21
|
+
* official endpoint and loopback (E2E mock servers) are allowed.
|
|
22
|
+
*/
|
|
23
|
+
const TELEGRAM_API_HOST_ALLOWLIST = new Set([
|
|
24
|
+
"api.telegram.org",
|
|
25
|
+
"localhost",
|
|
26
|
+
"127.0.0.1",
|
|
27
|
+
"::1",
|
|
28
|
+
]);
|
|
29
|
+
export function validateTelegramApiRoot(apiRoot) {
|
|
30
|
+
let url;
|
|
31
|
+
try {
|
|
32
|
+
url = new URL(apiRoot);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
throw new Error(`Invalid telegram_api_root URL: ${apiRoot}`);
|
|
36
|
+
}
|
|
37
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
38
|
+
throw new Error(`telegram_api_root must use http(s): ${apiRoot}`);
|
|
39
|
+
}
|
|
40
|
+
// http:// only allowed for loopback (mock servers); production must be https.
|
|
41
|
+
// URL.hostname wraps IPv6 in brackets ("[::1]") — strip them for allowlist match.
|
|
42
|
+
const host = url.hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
43
|
+
if (!TELEGRAM_API_HOST_ALLOWLIST.has(host)) {
|
|
44
|
+
throw new Error(`telegram_api_root host "${host}" is not in the allowlist ` +
|
|
45
|
+
`(${[...TELEGRAM_API_HOST_ALLOWLIST].join(", ")}). ` +
|
|
46
|
+
`Sending the bot token to an arbitrary host would leak credentials.`);
|
|
47
|
+
}
|
|
48
|
+
if (url.protocol === "http:" && host === "api.telegram.org") {
|
|
49
|
+
throw new Error("telegram_api_root must use https for api.telegram.org");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export class TelegramAdapter extends EventEmitter {
|
|
53
|
+
type = "telegram";
|
|
54
|
+
topology = "topics";
|
|
55
|
+
id;
|
|
56
|
+
bot;
|
|
57
|
+
accessManager;
|
|
58
|
+
inboxDir;
|
|
59
|
+
apiRoot;
|
|
60
|
+
queue;
|
|
61
|
+
cleanupTimer = null;
|
|
62
|
+
constructor(opts) {
|
|
63
|
+
super();
|
|
64
|
+
this.id = opts.id;
|
|
65
|
+
this.accessManager = opts.accessManager;
|
|
66
|
+
this.inboxDir = opts.inboxDir;
|
|
67
|
+
if (opts.apiRoot)
|
|
68
|
+
validateTelegramApiRoot(opts.apiRoot);
|
|
69
|
+
this.apiRoot = (opts.apiRoot ?? "https://api.telegram.org").replace(/\/+$/, "");
|
|
70
|
+
mkdirSync(this.inboxDir, { recursive: true });
|
|
71
|
+
this.bot = new Bot(opts.botToken, opts.apiRoot ? { client: { apiRoot: opts.apiRoot } } : undefined);
|
|
72
|
+
// Build MessageQueue backed by this bot
|
|
73
|
+
this.queue = new MessageQueue({
|
|
74
|
+
send: async (chatId, threadId, text) => {
|
|
75
|
+
const msg = await this.bot.api.sendMessage(Number(chatId), text, {
|
|
76
|
+
message_thread_id: toThreadId(threadId),
|
|
77
|
+
});
|
|
78
|
+
return { messageId: String(msg.message_id) };
|
|
79
|
+
},
|
|
80
|
+
edit: async (chatId, messageId, text) => {
|
|
81
|
+
await this.bot.api.editMessageText(Number(chatId), Number(messageId), text);
|
|
82
|
+
},
|
|
83
|
+
sendFile: async (chatId, threadId, filePath) => {
|
|
84
|
+
const ext = extname(filePath).toLowerCase();
|
|
85
|
+
const filename = basename(filePath);
|
|
86
|
+
if (IMAGE_EXTENSIONS.has(ext)) {
|
|
87
|
+
const msg = await this.bot.api.sendPhoto(Number(chatId), new InputFile(createReadStream(filePath), filename), { message_thread_id: toThreadId(threadId) });
|
|
88
|
+
return { messageId: String(msg.message_id) };
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const msg = await this.bot.api.sendDocument(Number(chatId), new InputFile(createReadStream(filePath), filename), { message_thread_id: toThreadId(threadId) });
|
|
92
|
+
return { messageId: String(msg.message_id) };
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
this._registerHandlers();
|
|
97
|
+
}
|
|
98
|
+
_registerHandlers() {
|
|
99
|
+
this.bot.on("message", async (ctx) => {
|
|
100
|
+
const msg = ctx.message;
|
|
101
|
+
if (!msg)
|
|
102
|
+
return;
|
|
103
|
+
const userId = msg.from?.id;
|
|
104
|
+
if (userId == null)
|
|
105
|
+
return;
|
|
106
|
+
// Access control
|
|
107
|
+
if (!this.accessManager.isAllowed(userId)) {
|
|
108
|
+
// In pairing mode, allow /pair commands through
|
|
109
|
+
if (msg.text?.startsWith("/pair")) {
|
|
110
|
+
await this._handlePairCommand(ctx);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// Allow messages from non-primary chats through for classic mode
|
|
114
|
+
// (classic mode has its own access control in fleet-manager)
|
|
115
|
+
const chatId = String(msg.chat.id);
|
|
116
|
+
if (this.lastChatId && chatId === this.lastChatId)
|
|
117
|
+
return;
|
|
118
|
+
// Non-primary chat: let it through to fleet-manager for classic routing
|
|
119
|
+
}
|
|
120
|
+
// Skip service messages (topic rename, pin, member join/leave, etc.)
|
|
121
|
+
if (this._isServiceMessage(msg))
|
|
122
|
+
return;
|
|
123
|
+
const chatId = String(msg.chat.id);
|
|
124
|
+
const threadId = msg.message_thread_id != null
|
|
125
|
+
? String(msg.message_thread_id)
|
|
126
|
+
: undefined;
|
|
127
|
+
const messageId = String(msg.message_id);
|
|
128
|
+
const username = msg.from?.username ?? msg.from?.first_name ?? String(userId);
|
|
129
|
+
const text = msg.text ?? msg.caption ?? "";
|
|
130
|
+
// Collect attachments
|
|
131
|
+
const attachments = this._extractAttachments(msg);
|
|
132
|
+
const replyToText = msg.reply_to_message?.text ?? msg.reply_to_message?.caption;
|
|
133
|
+
this.emit("message", {
|
|
134
|
+
source: "telegram",
|
|
135
|
+
adapterId: this.id,
|
|
136
|
+
chatId,
|
|
137
|
+
threadId,
|
|
138
|
+
messageId,
|
|
139
|
+
userId: String(userId),
|
|
140
|
+
username,
|
|
141
|
+
text,
|
|
142
|
+
timestamp: new Date(msg.date * 1000),
|
|
143
|
+
attachments: attachments.length > 0 ? attachments : undefined,
|
|
144
|
+
replyTo: msg.reply_to_message?.message_id != null
|
|
145
|
+
? String(msg.reply_to_message.message_id)
|
|
146
|
+
: undefined,
|
|
147
|
+
replyToText: replyToText || undefined,
|
|
148
|
+
chatTitle: msg.chat.title || undefined,
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
// Handle callback queries from approval inline keyboards and directory browser
|
|
152
|
+
this.bot.on("callback_query:data", async (ctx) => {
|
|
153
|
+
if (!ctx.callbackQuery?.data)
|
|
154
|
+
return;
|
|
155
|
+
await ctx.answerCallbackQuery();
|
|
156
|
+
this.emit("callback_query", {
|
|
157
|
+
callbackData: ctx.callbackQuery.data,
|
|
158
|
+
chatId: String(ctx.callbackQuery.message?.chat.id ?? ""),
|
|
159
|
+
threadId: ctx.callbackQuery.message?.message_thread_id != null
|
|
160
|
+
? String(ctx.callbackQuery.message.message_thread_id)
|
|
161
|
+
: undefined,
|
|
162
|
+
messageId: String(ctx.callbackQuery.message?.message_id ?? ""),
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
// Handle topic closed/deleted events (for auto-unbind)
|
|
166
|
+
this.bot.on("message:forum_topic_closed", (ctx) => {
|
|
167
|
+
const chatId = String(ctx.message?.chat.id ?? "");
|
|
168
|
+
const threadId = ctx.message?.message_thread_id != null
|
|
169
|
+
? String(ctx.message.message_thread_id)
|
|
170
|
+
: undefined;
|
|
171
|
+
if (threadId) {
|
|
172
|
+
this.emit("topic_closed", { chatId, threadId });
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
_extractAttachments(msg) {
|
|
177
|
+
const result = [];
|
|
178
|
+
if (msg.photo && msg.photo.length > 0) {
|
|
179
|
+
const largest = msg.photo[msg.photo.length - 1];
|
|
180
|
+
result.push({ kind: "photo", fileId: largest.file_id, size: largest.file_size });
|
|
181
|
+
}
|
|
182
|
+
if (msg.document) {
|
|
183
|
+
result.push({
|
|
184
|
+
kind: "document",
|
|
185
|
+
fileId: msg.document.file_id,
|
|
186
|
+
mime: msg.document.mime_type,
|
|
187
|
+
size: msg.document.file_size,
|
|
188
|
+
filename: msg.document.file_name,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
if (msg.audio) {
|
|
192
|
+
result.push({
|
|
193
|
+
kind: "audio",
|
|
194
|
+
fileId: msg.audio.file_id,
|
|
195
|
+
mime: msg.audio.mime_type,
|
|
196
|
+
size: msg.audio.file_size,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
if (msg.voice) {
|
|
200
|
+
result.push({
|
|
201
|
+
kind: "voice",
|
|
202
|
+
fileId: msg.voice.file_id,
|
|
203
|
+
mime: msg.voice.mime_type,
|
|
204
|
+
size: msg.voice.file_size,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
if (msg.video) {
|
|
208
|
+
result.push({
|
|
209
|
+
kind: "video",
|
|
210
|
+
fileId: msg.video.file_id,
|
|
211
|
+
mime: msg.video.mime_type,
|
|
212
|
+
size: msg.video.file_size,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
if (msg.sticker) {
|
|
216
|
+
result.push({ kind: "sticker", fileId: msg.sticker.file_id });
|
|
217
|
+
}
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
/** Returns true if the message is a Telegram service event with no user content. */
|
|
221
|
+
_isServiceMessage(msg) {
|
|
222
|
+
const hasText = !!(msg.text || msg.caption);
|
|
223
|
+
const hasMedia = !!(msg.photo || msg.document || msg.audio || msg.voice || msg.video || msg.sticker || msg.video_note);
|
|
224
|
+
if (hasText || hasMedia)
|
|
225
|
+
return false;
|
|
226
|
+
// If there's no text and no media, it's a service message
|
|
227
|
+
// (e.g. new_chat_title, pinned_message, new_chat_members, left_chat_member,
|
|
228
|
+
// forum_topic_created, forum_topic_edited, etc.)
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
async _handlePairCommand(ctx) {
|
|
232
|
+
const msg = ctx.message;
|
|
233
|
+
if (!msg || !msg.from)
|
|
234
|
+
return;
|
|
235
|
+
const userId = String(msg.from.id);
|
|
236
|
+
const chatId = String(msg.chat.id);
|
|
237
|
+
try {
|
|
238
|
+
const code = await this.handlePairing(chatId, userId);
|
|
239
|
+
await ctx.reply(`Your pairing code is: \`${code}\`\nShare it with the daemon owner to get access.`, {
|
|
240
|
+
parse_mode: "Markdown",
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
245
|
+
await ctx.reply(`Pairing failed: ${message}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// ── ChannelAdapter lifecycle ──────────────────────────────────────────────
|
|
249
|
+
/** Expose bot for fleet manager operations (topic existence checks etc.) */
|
|
250
|
+
getBot() { return this.bot; }
|
|
251
|
+
async start() {
|
|
252
|
+
this.queue.start();
|
|
253
|
+
this._pruneInbox();
|
|
254
|
+
this.cleanupTimer = setInterval(() => this._pruneInbox(), 60 * 60 * 1000);
|
|
255
|
+
// Grammy's default error handler calls bot.stop() on any throw — override
|
|
256
|
+
// to keep polling alive on handler errors
|
|
257
|
+
this.bot.catch((err) => {
|
|
258
|
+
this.emit("handler_error", err.error);
|
|
259
|
+
});
|
|
260
|
+
// 409 Conflict = another getUpdates consumer is active (official plugin zombie,
|
|
261
|
+
// or second Claude Code instance). Retry with backoff, but cap attempts so a
|
|
262
|
+
// permanently-stuck conflict does not loop forever.
|
|
263
|
+
const MAX_409_RETRIES = 30; // ~7 min total at 15s ceiling
|
|
264
|
+
const MAX_RECONNECT_RETRIES = 10; // auto-reconnect on non-409 errors
|
|
265
|
+
void (async () => {
|
|
266
|
+
let reconnects = 0;
|
|
267
|
+
while (true) {
|
|
268
|
+
for (let attempt = 1; attempt <= MAX_409_RETRIES; attempt++) {
|
|
269
|
+
try {
|
|
270
|
+
await this.bot.start({
|
|
271
|
+
drop_pending_updates: attempt === 1 && reconnects === 0,
|
|
272
|
+
onStart: (info) => {
|
|
273
|
+
reconnects = 0; // reset on successful start
|
|
274
|
+
this.emit("started", info.username);
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
return; // bot.stop() was called — clean exit
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
if (err instanceof GrammyError && err.error_code === 409) {
|
|
281
|
+
if (attempt >= MAX_409_RETRIES) {
|
|
282
|
+
this.emit("error", new Error(`Telegram polling: 409 conflict persisted after ${MAX_409_RETRIES} attempts; giving up`));
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const delay = Math.min(1000 * attempt, 15000);
|
|
286
|
+
this.emit("polling_conflict", { attempt, delay });
|
|
287
|
+
await new Promise(r => setTimeout(r, delay));
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (err instanceof Error && err.message === "Aborted delay")
|
|
291
|
+
return;
|
|
292
|
+
// Auto-reconnect on transient errors (network, timeout, 5xx)
|
|
293
|
+
reconnects++;
|
|
294
|
+
if (reconnects > MAX_RECONNECT_RETRIES) {
|
|
295
|
+
this.emit("error", new Error(`Telegram polling: gave up after ${reconnects} reconnect attempts. Last error: ${err.message}`));
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
console.warn(`[telegram] Polling error (reconnect ${reconnects}/${MAX_RECONNECT_RETRIES}): ${err.message}`);
|
|
299
|
+
await new Promise(r => setTimeout(r, Math.min(5000 * reconnects, 30000)));
|
|
300
|
+
break; // break inner loop to restart bot.start()
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
})();
|
|
305
|
+
}
|
|
306
|
+
async stop() {
|
|
307
|
+
if (this.cleanupTimer)
|
|
308
|
+
clearInterval(this.cleanupTimer);
|
|
309
|
+
this.queue.stop();
|
|
310
|
+
await this.bot.stop();
|
|
311
|
+
}
|
|
312
|
+
/** Delete inbox files older than 1 hour */
|
|
313
|
+
_pruneInbox() {
|
|
314
|
+
const maxAge = 60 * 60 * 1000;
|
|
315
|
+
const now = Date.now();
|
|
316
|
+
try {
|
|
317
|
+
for (const name of readdirSync(this.inboxDir)) {
|
|
318
|
+
const filePath = join(this.inboxDir, name);
|
|
319
|
+
try {
|
|
320
|
+
const mtime = statSync(filePath).mtimeMs;
|
|
321
|
+
if (now - mtime > maxAge)
|
|
322
|
+
unlinkSync(filePath);
|
|
323
|
+
}
|
|
324
|
+
catch { /* ignore per-file errors */ }
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
catch { /* ignore if dir doesn't exist */ }
|
|
328
|
+
}
|
|
329
|
+
// ── Text / file sending ───────────────────────────────────────────────────
|
|
330
|
+
async sendText(chatId, text, opts) {
|
|
331
|
+
return new Promise((resolve, reject) => {
|
|
332
|
+
// We enqueue and immediately capture the first sent messageId via a one-shot sender
|
|
333
|
+
// For simplicity we use the bot API directly for the first chunk resolution,
|
|
334
|
+
// and delegate subsequent chunks to the queue.
|
|
335
|
+
const threadId = opts?.threadId;
|
|
336
|
+
const chunkLimit = opts?.chunkLimit ?? 4096;
|
|
337
|
+
// Split text manually when caller needs the SentMessage back
|
|
338
|
+
const chunks = [];
|
|
339
|
+
let offset = 0;
|
|
340
|
+
while (offset < text.length) {
|
|
341
|
+
chunks.push(text.slice(offset, offset + chunkLimit));
|
|
342
|
+
offset += chunkLimit;
|
|
343
|
+
}
|
|
344
|
+
if (chunks.length === 0) {
|
|
345
|
+
reject(new Error("Empty text"));
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
// Send first chunk directly to get the messageId; enqueue the rest
|
|
349
|
+
const parseMode = opts?.format === "html" ? "HTML" : undefined;
|
|
350
|
+
this.bot.api
|
|
351
|
+
.sendMessage(Number(chatId), chunks[0], {
|
|
352
|
+
message_thread_id: toThreadId(threadId),
|
|
353
|
+
parse_mode: parseMode,
|
|
354
|
+
})
|
|
355
|
+
.then((msg) => {
|
|
356
|
+
const result = {
|
|
357
|
+
messageId: String(msg.message_id),
|
|
358
|
+
chatId,
|
|
359
|
+
threadId,
|
|
360
|
+
};
|
|
361
|
+
// Enqueue remaining chunks via the queue
|
|
362
|
+
for (let i = 1; i < chunks.length; i++) {
|
|
363
|
+
this.queue.enqueue(chatId, threadId, { type: "content", text: chunks[i] });
|
|
364
|
+
}
|
|
365
|
+
resolve(result);
|
|
366
|
+
})
|
|
367
|
+
.catch(reject);
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
async sendFile(chatId, filePath, opts) {
|
|
371
|
+
const threadId = opts?.threadId;
|
|
372
|
+
const ext = extname(filePath).toLowerCase();
|
|
373
|
+
const filename = basename(filePath);
|
|
374
|
+
let messageId;
|
|
375
|
+
if (IMAGE_EXTENSIONS.has(ext)) {
|
|
376
|
+
const msg = await this.bot.api.sendPhoto(Number(chatId), new InputFile(createReadStream(filePath), filename), { message_thread_id: toThreadId(threadId) });
|
|
377
|
+
messageId = String(msg.message_id);
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
const msg = await this.bot.api.sendDocument(Number(chatId), new InputFile(createReadStream(filePath), filename), { message_thread_id: toThreadId(threadId) });
|
|
381
|
+
messageId = String(msg.message_id);
|
|
382
|
+
}
|
|
383
|
+
return { messageId, chatId, threadId };
|
|
384
|
+
}
|
|
385
|
+
async editMessage(chatId, messageId, text) {
|
|
386
|
+
await this.bot.api.editMessageText(Number(chatId), Number(messageId), text);
|
|
387
|
+
}
|
|
388
|
+
async react(chatId, messageId, emoji) {
|
|
389
|
+
await this.bot.api.setMessageReaction(Number(chatId), Number(messageId), [
|
|
390
|
+
{ type: "emoji", emoji: emoji },
|
|
391
|
+
]);
|
|
392
|
+
}
|
|
393
|
+
// ── Approval ─────────────────────────────────────────────────────────────
|
|
394
|
+
async sendApproval(prompt, callback, signal, threadId) {
|
|
395
|
+
const TIMEOUT_SECS = 120;
|
|
396
|
+
const COUNTDOWN_INTERVAL_MS = 30_000;
|
|
397
|
+
const nonce = randomBytes(5).toString("hex");
|
|
398
|
+
const approveData = `approval:approve:${nonce}`;
|
|
399
|
+
const alwaysData = `approval:approve_always:${nonce}`;
|
|
400
|
+
const denyData = `approval:deny:${nonce}`;
|
|
401
|
+
const makeKeyboard = (remainingSecs) => {
|
|
402
|
+
const mm = Math.floor(remainingSecs / 60);
|
|
403
|
+
const ss = remainingSecs % 60;
|
|
404
|
+
const ts = `${mm}:${String(ss).padStart(2, "0")}`;
|
|
405
|
+
return new InlineKeyboard()
|
|
406
|
+
.text(`✅ Allow (${ts})`, approveData)
|
|
407
|
+
.text("✅ Always", alwaysData)
|
|
408
|
+
.text(`❌ Deny`, denyData);
|
|
409
|
+
};
|
|
410
|
+
// Format the permission message
|
|
411
|
+
let text = `⚠️ *Permission Request*\nTool: \`${prompt.tool_name}\``;
|
|
412
|
+
if (prompt.input_preview) {
|
|
413
|
+
const preview = prompt.input_preview.length > 200
|
|
414
|
+
? prompt.input_preview.slice(0, 200) + "…"
|
|
415
|
+
: prompt.input_preview;
|
|
416
|
+
text += `\n\`\`\`\n${preview}\n\`\`\``;
|
|
417
|
+
}
|
|
418
|
+
else if (prompt.description) {
|
|
419
|
+
text += `\n${prompt.description}`;
|
|
420
|
+
}
|
|
421
|
+
let sentChatId;
|
|
422
|
+
let sentMessageId;
|
|
423
|
+
let countdownTimer;
|
|
424
|
+
const startTime = Date.now();
|
|
425
|
+
const stopCountdown = () => {
|
|
426
|
+
if (countdownTimer) {
|
|
427
|
+
clearInterval(countdownTimer);
|
|
428
|
+
countdownTimer = undefined;
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
const cleanup = () => {
|
|
432
|
+
stopCountdown();
|
|
433
|
+
this.off("callback_query", handler);
|
|
434
|
+
};
|
|
435
|
+
const handler = (query) => {
|
|
436
|
+
if (!query.callbackData)
|
|
437
|
+
return;
|
|
438
|
+
const isApprove = query.callbackData === approveData;
|
|
439
|
+
const isAlways = query.callbackData === alwaysData;
|
|
440
|
+
const isDeny = query.callbackData === denyData;
|
|
441
|
+
if (!isApprove && !isAlways && !isDeny)
|
|
442
|
+
return;
|
|
443
|
+
cleanup();
|
|
444
|
+
// Post-decision feedback: update message with decision result
|
|
445
|
+
const editChatId = query.chatId ? Number(query.chatId) : sentChatId;
|
|
446
|
+
const editMsgId = query.messageId ? Number(query.messageId) : sentMessageId;
|
|
447
|
+
if (editChatId && editMsgId) {
|
|
448
|
+
const label = isDeny ? "❌ Denied" : isAlways ? "✅ Always Allowed" : "✅ Allowed";
|
|
449
|
+
this.bot.api.editMessageText(editChatId, editMsgId, `${label}\nTool: \`${prompt.tool_name}\``, { parse_mode: "Markdown" }).catch(() => { });
|
|
450
|
+
}
|
|
451
|
+
callback(isDeny ? "deny" : isAlways ? "approve_always" : "approve");
|
|
452
|
+
};
|
|
453
|
+
this.on("callback_query", handler);
|
|
454
|
+
if (signal) {
|
|
455
|
+
signal.addEventListener("abort", () => {
|
|
456
|
+
// On timeout/abort, update message to show expiry
|
|
457
|
+
stopCountdown();
|
|
458
|
+
if (sentChatId && sentMessageId) {
|
|
459
|
+
this.bot.api.editMessageText(sentChatId, sentMessageId, `⏱ Timed out\nTool: \`${prompt.tool_name}\``, { parse_mode: "Markdown" }).catch(() => { });
|
|
460
|
+
}
|
|
461
|
+
this.off("callback_query", handler);
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
const keyboard = makeKeyboard(TIMEOUT_SECS);
|
|
465
|
+
if (threadId) {
|
|
466
|
+
const chatId = this.getChatId();
|
|
467
|
+
if (chatId) {
|
|
468
|
+
const sent = await this.bot.api.sendMessage(Number(chatId), text, {
|
|
469
|
+
message_thread_id: Number(threadId),
|
|
470
|
+
reply_markup: keyboard,
|
|
471
|
+
parse_mode: "Markdown",
|
|
472
|
+
}).catch(() => {
|
|
473
|
+
return this.bot.api.sendMessage(Number(chatId), text, {
|
|
474
|
+
message_thread_id: Number(threadId),
|
|
475
|
+
reply_markup: keyboard,
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
sentChatId = sent.chat.id;
|
|
479
|
+
sentMessageId = sent.message_id;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
this.emit("approval_request", { prompt: text, keyboard, nonce });
|
|
484
|
+
}
|
|
485
|
+
// Start countdown timer — update buttons every 30s with remaining time
|
|
486
|
+
if (sentChatId && sentMessageId) {
|
|
487
|
+
const chatIdForEdit = sentChatId;
|
|
488
|
+
const msgIdForEdit = sentMessageId;
|
|
489
|
+
countdownTimer = setInterval(() => {
|
|
490
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
491
|
+
const remaining = Math.max(0, TIMEOUT_SECS - elapsed);
|
|
492
|
+
if (remaining <= 0) {
|
|
493
|
+
stopCountdown();
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const updatedKeyboard = makeKeyboard(remaining);
|
|
497
|
+
this.bot.api.editMessageReplyMarkup(chatIdForEdit, msgIdForEdit, {
|
|
498
|
+
reply_markup: updatedKeyboard,
|
|
499
|
+
}).catch(() => { });
|
|
500
|
+
}, COUNTDOWN_INTERVAL_MS);
|
|
501
|
+
}
|
|
502
|
+
return { cancel: cleanup };
|
|
503
|
+
}
|
|
504
|
+
/** Get the last known chatId (group ID for topic mode) */
|
|
505
|
+
lastChatId = null;
|
|
506
|
+
getChatId() { return this.lastChatId; }
|
|
507
|
+
setChatId(chatId) { this.lastChatId = chatId; }
|
|
508
|
+
// ── File download ─────────────────────────────────────────────────────────
|
|
509
|
+
async downloadAttachment(fileId) {
|
|
510
|
+
const file = await this.bot.api.getFile(fileId);
|
|
511
|
+
const filePath = file.file_path;
|
|
512
|
+
if (!filePath) {
|
|
513
|
+
throw new Error(`No file_path returned for fileId: ${fileId}`);
|
|
514
|
+
}
|
|
515
|
+
// Construct the download URL
|
|
516
|
+
const token = this.bot.token;
|
|
517
|
+
const url = `${this.apiRoot}/file/bot${token}/${filePath}`;
|
|
518
|
+
const filename = filePath.split("/").pop() ?? fileId;
|
|
519
|
+
const localPath = join(this.inboxDir, filename);
|
|
520
|
+
// Download using fetch
|
|
521
|
+
const response = await fetch(url);
|
|
522
|
+
if (!response.ok) {
|
|
523
|
+
throw new Error(`Failed to download file: ${response.status} ${response.statusText}`);
|
|
524
|
+
}
|
|
525
|
+
const { createWriteStream } = await import("node:fs");
|
|
526
|
+
const { pipeline } = await import("node:stream/promises");
|
|
527
|
+
const { Readable } = await import("node:stream");
|
|
528
|
+
const dest = createWriteStream(localPath);
|
|
529
|
+
const body = response.body;
|
|
530
|
+
if (!body)
|
|
531
|
+
throw new Error("No response body");
|
|
532
|
+
await pipeline(Readable.fromWeb(body), dest);
|
|
533
|
+
return localPath;
|
|
534
|
+
}
|
|
535
|
+
// ── Intent-oriented methods ──────────────────────────────────────────────
|
|
536
|
+
async promptUser(chatId, text, choices, opts) {
|
|
537
|
+
const keyboard = new InlineKeyboard();
|
|
538
|
+
for (const choice of choices) {
|
|
539
|
+
keyboard.text(choice.label, choice.id).row();
|
|
540
|
+
}
|
|
541
|
+
const threadId = opts?.threadId;
|
|
542
|
+
const msg = await this.bot.api.sendMessage(Number(chatId), text, {
|
|
543
|
+
message_thread_id: toThreadId(threadId),
|
|
544
|
+
reply_markup: keyboard,
|
|
545
|
+
});
|
|
546
|
+
return String(msg.message_id);
|
|
547
|
+
}
|
|
548
|
+
async notifyAlert(chatId, alert, opts) {
|
|
549
|
+
const threadId = opts?.threadId;
|
|
550
|
+
if (alert.choices && alert.choices.length > 0) {
|
|
551
|
+
const keyboard = new InlineKeyboard();
|
|
552
|
+
for (const choice of alert.choices) {
|
|
553
|
+
keyboard.text(choice.label, choice.id);
|
|
554
|
+
}
|
|
555
|
+
const msg = await this.bot.api.sendMessage(Number(chatId), alert.message, {
|
|
556
|
+
message_thread_id: toThreadId(threadId),
|
|
557
|
+
reply_markup: keyboard,
|
|
558
|
+
});
|
|
559
|
+
return { messageId: String(msg.message_id), chatId, threadId };
|
|
560
|
+
}
|
|
561
|
+
return this.sendText(chatId, alert.message, opts);
|
|
562
|
+
}
|
|
563
|
+
async createTopic(name) {
|
|
564
|
+
const chatId = this.getChatId();
|
|
565
|
+
if (!chatId)
|
|
566
|
+
throw new Error("No chat ID set — cannot create topic");
|
|
567
|
+
const res = await this.bot.api.createForumTopic(Number(chatId), name);
|
|
568
|
+
return res.message_thread_id;
|
|
569
|
+
}
|
|
570
|
+
async deleteTopic(topicId) {
|
|
571
|
+
const chatId = this.getChatId();
|
|
572
|
+
if (!chatId)
|
|
573
|
+
return;
|
|
574
|
+
await this.bot.api.raw.deleteForumTopic({ chat_id: Number(chatId), message_thread_id: Number(topicId) });
|
|
575
|
+
}
|
|
576
|
+
async topicExists(topicId) {
|
|
577
|
+
const chatId = this.getChatId();
|
|
578
|
+
if (!chatId)
|
|
579
|
+
return false;
|
|
580
|
+
try {
|
|
581
|
+
const msg = await this.bot.api.sendMessage(Number(chatId), "\u200B", {
|
|
582
|
+
message_thread_id: topicId,
|
|
583
|
+
});
|
|
584
|
+
await this.bot.api.deleteMessage(Number(chatId), msg.message_id).catch(() => { });
|
|
585
|
+
return true;
|
|
586
|
+
}
|
|
587
|
+
catch (err) {
|
|
588
|
+
const errMsg = String(err);
|
|
589
|
+
if (errMsg.includes("thread not found") || errMsg.includes("TOPIC_ID_INVALID")) {
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
throw err;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
// ── Pairing ───────────────────────────────────────────────────────────────
|
|
596
|
+
async closeForumTopic(threadId) {
|
|
597
|
+
const chatId = this.getChatId();
|
|
598
|
+
if (!chatId)
|
|
599
|
+
return;
|
|
600
|
+
try {
|
|
601
|
+
await this.bot.api.closeForumTopic(Number(chatId), Number(threadId));
|
|
602
|
+
}
|
|
603
|
+
catch {
|
|
604
|
+
// Best-effort — topic may already be closed
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
async reopenForumTopic(threadId) {
|
|
608
|
+
const chatId = this.getChatId();
|
|
609
|
+
if (!chatId)
|
|
610
|
+
return;
|
|
611
|
+
try {
|
|
612
|
+
await this.bot.api.reopenForumTopic(Number(chatId), Number(threadId));
|
|
613
|
+
}
|
|
614
|
+
catch {
|
|
615
|
+
// Best-effort — topic may already be open
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
async editForumTopic(threadId, opts) {
|
|
619
|
+
const chatId = this.getChatId();
|
|
620
|
+
if (!chatId)
|
|
621
|
+
return;
|
|
622
|
+
try {
|
|
623
|
+
await this.bot.api.editForumTopic(Number(chatId), Number(threadId), {
|
|
624
|
+
name: opts.name,
|
|
625
|
+
icon_custom_emoji_id: opts.iconCustomEmojiId,
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
catch {
|
|
629
|
+
// Best-effort — icon change is cosmetic
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
async getTopicIconStickers() {
|
|
633
|
+
const stickers = await this.bot.api.getForumTopicIconStickers();
|
|
634
|
+
return stickers
|
|
635
|
+
.filter((s) => s.custom_emoji_id != null)
|
|
636
|
+
.map((s) => ({ customEmojiId: s.custom_emoji_id, emoji: s.emoji ?? "" }));
|
|
637
|
+
}
|
|
638
|
+
async handlePairing(chatId, userId) {
|
|
639
|
+
const code = this.accessManager.generateCode(userId);
|
|
640
|
+
return code;
|
|
641
|
+
}
|
|
642
|
+
async confirmPairing(code, callerUserId) {
|
|
643
|
+
return this.accessManager.confirmCode(code, callerUserId);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
//# sourceMappingURL=telegram.js.map
|