talon-agent 1.0.0 → 1.2.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/LICENSE +21 -0
- package/README.md +1 -0
- package/package.json +15 -11
- package/prompts/dream.md +7 -3
- package/prompts/heartbeat.md +30 -0
- package/prompts/identity.md +1 -0
- package/prompts/teams.md +3 -0
- package/prompts/telegram.md +1 -0
- package/src/__tests__/chat-settings.test.ts +108 -2
- package/src/__tests__/cleanup-registry.test.ts +58 -0
- package/src/__tests__/config.test.ts +118 -52
- package/src/__tests__/cron-store-extended.test.ts +661 -0
- package/src/__tests__/cron-store.test.ts +145 -11
- package/src/__tests__/daily-log.test.ts +224 -13
- package/src/__tests__/dispatcher.test.ts +424 -23
- package/src/__tests__/dream.test.ts +1028 -0
- package/src/__tests__/errors-extended.test.ts +428 -0
- package/src/__tests__/errors.test.ts +95 -3
- package/src/__tests__/fuzz.test.ts +87 -15
- package/src/__tests__/gateway-actions.test.ts +1174 -433
- package/src/__tests__/gateway-http.test.ts +210 -19
- package/src/__tests__/gateway-retry.test.ts +359 -0
- package/src/__tests__/gateway-withRetry-extended.test.ts +343 -0
- package/src/__tests__/graph.test.ts +830 -0
- package/src/__tests__/handlers-stream.test.ts +208 -0
- package/src/__tests__/handlers.test.ts +2539 -70
- package/src/__tests__/heartbeat.test.ts +364 -0
- package/src/__tests__/history-extended.test.ts +775 -0
- package/src/__tests__/history-persistence.test.ts +74 -19
- package/src/__tests__/history.test.ts +113 -79
- package/src/__tests__/integration.test.ts +43 -8
- package/src/__tests__/log-init.test.ts +129 -0
- package/src/__tests__/log.test.ts +23 -5
- package/src/__tests__/media-index.test.ts +317 -35
- package/src/__tests__/plugin.test.ts +314 -0
- package/src/__tests__/prompt-builder-extended.test.ts +296 -0
- package/src/__tests__/prompt-builder.test.ts +44 -9
- package/src/__tests__/sessions.test.ts +258 -4
- package/src/__tests__/storage-save-errors.test.ts +342 -0
- package/src/__tests__/teams-frontend.test.ts +526 -31
- package/src/__tests__/telegram-formatting.test.ts +82 -0
- package/src/__tests__/terminal-commands.test.ts +208 -1
- package/src/__tests__/terminal-renderer.test.ts +223 -0
- package/src/__tests__/time.test.ts +107 -0
- package/src/__tests__/workspace-migrate.test.ts +256 -0
- package/src/__tests__/workspace.test.ts +63 -1
- package/src/backend/claude-sdk/tools.ts +64 -18
- package/src/bootstrap.ts +14 -14
- package/src/cli.ts +440 -125
- package/src/core/cron.ts +20 -5
- package/src/core/dispatcher.ts +27 -9
- package/src/core/dream.ts +79 -24
- package/src/core/errors.ts +12 -2
- package/src/core/gateway-actions.ts +182 -46
- package/src/core/gateway.ts +93 -41
- package/src/core/heartbeat.ts +515 -0
- package/src/core/plugin.ts +1 -1
- package/src/core/prompt-builder.ts +1 -4
- package/src/core/pulse.ts +4 -3
- package/src/frontend/teams/actions.ts +3 -1
- package/src/frontend/teams/formatting.ts +47 -8
- package/src/frontend/teams/graph.ts +35 -11
- package/src/frontend/teams/index.ts +155 -57
- package/src/frontend/teams/tools.ts +4 -6
- package/src/frontend/telegram/actions.ts +358 -82
- package/src/frontend/telegram/admin.ts +162 -72
- package/src/frontend/telegram/callbacks.ts +16 -10
- package/src/frontend/telegram/commands.ts +37 -21
- package/src/frontend/telegram/formatting.ts +2 -4
- package/src/frontend/telegram/handlers.ts +262 -66
- package/src/frontend/telegram/index.ts +39 -14
- package/src/frontend/telegram/middleware.ts +14 -4
- package/src/frontend/telegram/userbot.ts +16 -4
- package/src/frontend/terminal/renderer.ts +1 -4
- package/src/index.ts +28 -4
- package/src/storage/chat-settings.ts +32 -9
- package/src/storage/cron-store.ts +53 -11
- package/src/storage/daily-log.ts +72 -19
- package/src/storage/history.ts +39 -21
- package/src/storage/media-index.ts +37 -12
- package/src/storage/sessions.ts +3 -2
- package/src/util/cleanup-registry.ts +34 -0
- package/src/util/config.ts +85 -23
- package/src/util/log.ts +47 -17
- package/src/util/paths.ts +10 -0
- package/src/util/time.ts +29 -6
- package/src/util/watchdog.ts +5 -1
- package/src/util/workspace.ts +51 -10
|
@@ -6,7 +6,13 @@
|
|
|
6
6
|
* by core/gateway-actions.ts before this is called.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
readFileSync,
|
|
11
|
+
statSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
existsSync,
|
|
14
|
+
mkdirSync,
|
|
15
|
+
} from "node:fs";
|
|
10
16
|
import { basename, resolve } from "node:path";
|
|
11
17
|
import { dirs } from "../../util/paths.js";
|
|
12
18
|
import type { Bot, InputFile as GrammyInputFile } from "grammy";
|
|
@@ -30,9 +36,13 @@ const TELEGRAM_MAX_TEXT = 4096;
|
|
|
30
36
|
|
|
31
37
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
32
38
|
|
|
33
|
-
function replyParams(
|
|
39
|
+
function replyParams(
|
|
40
|
+
body: Record<string, unknown>,
|
|
41
|
+
): { message_id: number } | undefined {
|
|
34
42
|
const replyTo = body.reply_to ?? body.reply_to_message_id;
|
|
35
|
-
return typeof replyTo === "number" && replyTo > 0
|
|
43
|
+
return typeof replyTo === "number" && replyTo > 0
|
|
44
|
+
? { message_id: replyTo }
|
|
45
|
+
: undefined;
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
export async function sendText(
|
|
@@ -42,7 +52,9 @@ export async function sendText(
|
|
|
42
52
|
replyTo?: number,
|
|
43
53
|
): Promise<number> {
|
|
44
54
|
if (text.length > TELEGRAM_MAX_TEXT) {
|
|
45
|
-
throw new Error(
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Message too long (${text.length} chars, max ${TELEGRAM_MAX_TEXT}).`,
|
|
57
|
+
);
|
|
46
58
|
}
|
|
47
59
|
const html = markdownToTelegramHtml(text);
|
|
48
60
|
try {
|
|
@@ -73,23 +85,33 @@ export function createTelegramActionHandler(
|
|
|
73
85
|
) {
|
|
74
86
|
const scheduledMessages = new Map<string, ReturnType<typeof setTimeout>>();
|
|
75
87
|
|
|
76
|
-
return async (
|
|
88
|
+
return async (
|
|
89
|
+
body: Record<string, unknown>,
|
|
90
|
+
chatId: number,
|
|
91
|
+
): Promise<ActionResult | null> => {
|
|
77
92
|
const action = body.action as string;
|
|
78
93
|
|
|
79
94
|
switch (action) {
|
|
80
95
|
// ── Messaging ─────────────────────────────────────────────────────
|
|
81
96
|
case "send_message": {
|
|
82
97
|
const text = String(body.text ?? "");
|
|
83
|
-
const replyTo =
|
|
98
|
+
const replyTo =
|
|
99
|
+
typeof body.reply_to_message_id === "number"
|
|
100
|
+
? body.reply_to_message_id
|
|
101
|
+
: undefined;
|
|
84
102
|
gateway.incrementMessages(chatId);
|
|
85
|
-
const msgId = await withRetry(() =>
|
|
103
|
+
const msgId = await withRetry(() =>
|
|
104
|
+
sendText(bot, chatId, text, replyTo),
|
|
105
|
+
);
|
|
86
106
|
return { ok: true, message_id: msgId };
|
|
87
107
|
}
|
|
88
108
|
|
|
89
109
|
case "reply_to": {
|
|
90
110
|
const msgId = Number(body.message_id);
|
|
91
111
|
gateway.incrementMessages(chatId);
|
|
92
|
-
const sentId = await withRetry(() =>
|
|
112
|
+
const sentId = await withRetry(() =>
|
|
113
|
+
sendText(bot, chatId, String(body.text ?? ""), msgId),
|
|
114
|
+
);
|
|
93
115
|
return { ok: true, message_id: sentId };
|
|
94
116
|
}
|
|
95
117
|
|
|
@@ -97,26 +119,49 @@ export function createTelegramActionHandler(
|
|
|
97
119
|
gateway.incrementMessages(chatId);
|
|
98
120
|
const emoji = String(body.emoji ?? "\uD83D\uDC4D");
|
|
99
121
|
try {
|
|
100
|
-
await withRetry(() =>
|
|
101
|
-
|
|
102
|
-
|
|
122
|
+
await withRetry(() =>
|
|
123
|
+
bot.api.setMessageReaction(chatId, Number(body.message_id), [
|
|
124
|
+
{ type: "emoji", emoji: emoji as "\uD83D\uDC4D" },
|
|
125
|
+
]),
|
|
126
|
+
);
|
|
103
127
|
} catch {
|
|
104
128
|
try {
|
|
105
129
|
await bot.api.setMessageReaction(chatId, Number(body.message_id), [
|
|
106
130
|
{ type: "emoji", emoji: "\uD83D\uDC4D" },
|
|
107
131
|
]);
|
|
108
|
-
} catch (e) {
|
|
132
|
+
} catch (e) {
|
|
133
|
+
return {
|
|
134
|
+
ok: false,
|
|
135
|
+
error: `Reaction failed: ${e instanceof Error ? e.message : e}`,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
109
138
|
}
|
|
110
139
|
return { ok: true };
|
|
111
140
|
}
|
|
112
141
|
|
|
113
142
|
case "edit_message": {
|
|
114
143
|
const text = String(body.text ?? "");
|
|
115
|
-
if (text.length > TELEGRAM_MAX_TEXT)
|
|
144
|
+
if (text.length > TELEGRAM_MAX_TEXT)
|
|
145
|
+
return {
|
|
146
|
+
ok: false,
|
|
147
|
+
error: `Text too long (max ${TELEGRAM_MAX_TEXT})`,
|
|
148
|
+
};
|
|
116
149
|
const html = markdownToTelegramHtml(text);
|
|
117
150
|
await withRetry(async () => {
|
|
118
|
-
try {
|
|
119
|
-
|
|
151
|
+
try {
|
|
152
|
+
await bot.api.editMessageText(
|
|
153
|
+
chatId,
|
|
154
|
+
Number(body.message_id),
|
|
155
|
+
html,
|
|
156
|
+
{ parse_mode: "HTML" },
|
|
157
|
+
);
|
|
158
|
+
} catch {
|
|
159
|
+
await bot.api.editMessageText(
|
|
160
|
+
chatId,
|
|
161
|
+
Number(body.message_id),
|
|
162
|
+
text,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
120
165
|
});
|
|
121
166
|
return { ok: true };
|
|
122
167
|
}
|
|
@@ -130,49 +175,85 @@ export function createTelegramActionHandler(
|
|
|
130
175
|
return { ok: true };
|
|
131
176
|
|
|
132
177
|
case "unpin_message":
|
|
133
|
-
await bot.api.unpinChatMessage(
|
|
178
|
+
await bot.api.unpinChatMessage(
|
|
179
|
+
chatId,
|
|
180
|
+
body.message_id ? Number(body.message_id) : undefined,
|
|
181
|
+
);
|
|
134
182
|
return { ok: true };
|
|
135
183
|
|
|
136
184
|
case "forward_message": {
|
|
137
185
|
if (body.to_chat_id && Number(body.to_chat_id) !== chatId)
|
|
138
186
|
return { ok: false, error: "Cross-chat forwarding not allowed." };
|
|
139
|
-
const sent = await bot.api.forwardMessage(
|
|
187
|
+
const sent = await bot.api.forwardMessage(
|
|
188
|
+
chatId,
|
|
189
|
+
chatId,
|
|
190
|
+
Number(body.message_id),
|
|
191
|
+
);
|
|
140
192
|
return { ok: true, message_id: sent.message_id };
|
|
141
193
|
}
|
|
142
194
|
|
|
143
195
|
case "copy_message": {
|
|
144
|
-
const sent = await bot.api.copyMessage(
|
|
196
|
+
const sent = await bot.api.copyMessage(
|
|
197
|
+
chatId,
|
|
198
|
+
chatId,
|
|
199
|
+
Number(body.message_id),
|
|
200
|
+
);
|
|
145
201
|
return { ok: true, message_id: sent.message_id };
|
|
146
202
|
}
|
|
147
203
|
|
|
148
204
|
case "send_chat_action":
|
|
149
|
-
await bot.api.sendChatAction(
|
|
205
|
+
await bot.api.sendChatAction(
|
|
206
|
+
chatId,
|
|
207
|
+
String(body.chat_action ?? "typing") as "typing",
|
|
208
|
+
);
|
|
150
209
|
return { ok: true };
|
|
151
210
|
|
|
152
211
|
case "send_message_with_buttons": {
|
|
153
212
|
const text = String(body.text ?? "");
|
|
154
|
-
if (text.length > TELEGRAM_MAX_TEXT)
|
|
213
|
+
if (text.length > TELEGRAM_MAX_TEXT)
|
|
214
|
+
return { ok: false, error: `Text too long` };
|
|
155
215
|
const html = markdownToTelegramHtml(text);
|
|
156
|
-
const rows = body.rows as Array<
|
|
216
|
+
const rows = body.rows as Array<
|
|
217
|
+
Array<{ text: string; url?: string; callback_data?: string }>
|
|
218
|
+
>;
|
|
157
219
|
gateway.incrementMessages(chatId);
|
|
158
|
-
const keyboard = rows.map((row) =>
|
|
159
|
-
|
|
160
|
-
|
|
220
|
+
const keyboard = rows.map((row) =>
|
|
221
|
+
row.map((btn) =>
|
|
222
|
+
btn.url
|
|
223
|
+
? { text: btn.text, url: btn.url }
|
|
224
|
+
: {
|
|
225
|
+
text: btn.text,
|
|
226
|
+
callback_data: btn.callback_data ?? btn.text,
|
|
227
|
+
},
|
|
228
|
+
),
|
|
229
|
+
);
|
|
161
230
|
try {
|
|
162
|
-
const sent = await bot.api.sendMessage(chatId, html, {
|
|
231
|
+
const sent = await bot.api.sendMessage(chatId, html, {
|
|
232
|
+
parse_mode: "HTML",
|
|
233
|
+
reply_markup: { inline_keyboard: keyboard },
|
|
234
|
+
});
|
|
163
235
|
return { ok: true, message_id: sent.message_id };
|
|
164
236
|
} catch {
|
|
165
|
-
const sent = await bot.api.sendMessage(chatId, text, {
|
|
237
|
+
const sent = await bot.api.sendMessage(chatId, text, {
|
|
238
|
+
reply_markup: { inline_keyboard: keyboard },
|
|
239
|
+
});
|
|
166
240
|
return { ok: true, message_id: sent.message_id };
|
|
167
241
|
}
|
|
168
242
|
}
|
|
169
243
|
|
|
170
244
|
case "schedule_message": {
|
|
171
245
|
const text = String(body.text ?? "");
|
|
172
|
-
const delaySec = Math.max(
|
|
246
|
+
const delaySec = Math.max(
|
|
247
|
+
1,
|
|
248
|
+
Math.min(3600, Number(body.delay_seconds ?? 60)),
|
|
249
|
+
);
|
|
173
250
|
const scheduleId = `sched_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
174
251
|
const timer = setTimeout(async () => {
|
|
175
|
-
try {
|
|
252
|
+
try {
|
|
253
|
+
await sendText(bot, chatId, text);
|
|
254
|
+
} catch {
|
|
255
|
+
/* scheduled send failed */
|
|
256
|
+
}
|
|
176
257
|
scheduledMessages.delete(scheduleId);
|
|
177
258
|
}, delaySec * 1000);
|
|
178
259
|
scheduledMessages.set(scheduleId, timer);
|
|
@@ -181,88 +262,196 @@ export function createTelegramActionHandler(
|
|
|
181
262
|
|
|
182
263
|
case "cancel_scheduled": {
|
|
183
264
|
const timer = scheduledMessages.get(String(body.schedule_id ?? ""));
|
|
184
|
-
if (timer) {
|
|
265
|
+
if (timer) {
|
|
266
|
+
clearTimeout(timer);
|
|
267
|
+
scheduledMessages.delete(String(body.schedule_id));
|
|
268
|
+
return { ok: true, cancelled: true };
|
|
269
|
+
}
|
|
185
270
|
return { ok: false, error: "Schedule not found" };
|
|
186
271
|
}
|
|
187
272
|
|
|
188
273
|
// ── Media ──────────────────────────────────────────────────────────
|
|
189
|
-
case "send_file":
|
|
274
|
+
case "send_file":
|
|
275
|
+
case "send_photo":
|
|
276
|
+
case "send_video":
|
|
277
|
+
case "send_animation":
|
|
278
|
+
case "send_voice":
|
|
279
|
+
case "send_audio": {
|
|
190
280
|
const filePath = String(body.file_path ?? "");
|
|
191
281
|
const caption = body.caption ? String(body.caption) : undefined;
|
|
192
282
|
gateway.incrementMessages(chatId);
|
|
193
283
|
if (action === "send_file") {
|
|
194
284
|
const stat = statSync(filePath);
|
|
195
|
-
if (stat.size > 49 * 1024 * 1024)
|
|
285
|
+
if (stat.size > 49 * 1024 * 1024)
|
|
286
|
+
return { ok: false, error: "File too large (max 49MB)" };
|
|
196
287
|
}
|
|
197
288
|
const data = readFileSync(filePath);
|
|
198
289
|
const file = new InputFileClass(data, basename(filePath));
|
|
199
290
|
const rp = replyParams(body);
|
|
200
291
|
let sent;
|
|
201
292
|
switch (action) {
|
|
202
|
-
case "send_file":
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
293
|
+
case "send_file":
|
|
294
|
+
sent = await withRetry(() =>
|
|
295
|
+
bot.api.sendDocument(chatId, file, {
|
|
296
|
+
caption,
|
|
297
|
+
reply_parameters: rp,
|
|
298
|
+
}),
|
|
299
|
+
);
|
|
300
|
+
break;
|
|
301
|
+
case "send_photo":
|
|
302
|
+
sent = await withRetry(() =>
|
|
303
|
+
bot.api.sendPhoto(chatId, file, {
|
|
304
|
+
caption,
|
|
305
|
+
reply_parameters: rp,
|
|
306
|
+
}),
|
|
307
|
+
);
|
|
308
|
+
break;
|
|
309
|
+
case "send_video":
|
|
310
|
+
sent = await withRetry(() =>
|
|
311
|
+
bot.api.sendVideo(chatId, file, {
|
|
312
|
+
caption,
|
|
313
|
+
reply_parameters: rp,
|
|
314
|
+
}),
|
|
315
|
+
);
|
|
316
|
+
break;
|
|
317
|
+
case "send_animation":
|
|
318
|
+
sent = await withRetry(() =>
|
|
319
|
+
bot.api.sendAnimation(chatId, file, {
|
|
320
|
+
caption,
|
|
321
|
+
reply_parameters: rp,
|
|
322
|
+
}),
|
|
323
|
+
);
|
|
324
|
+
break;
|
|
325
|
+
case "send_audio":
|
|
326
|
+
sent = await withRetry(() =>
|
|
327
|
+
bot.api.sendAudio(chatId, file, {
|
|
328
|
+
caption,
|
|
329
|
+
reply_parameters: rp,
|
|
330
|
+
title: body.title as string | undefined,
|
|
331
|
+
performer: body.performer as string | undefined,
|
|
332
|
+
}),
|
|
333
|
+
);
|
|
334
|
+
break;
|
|
335
|
+
default:
|
|
336
|
+
sent = await withRetry(() =>
|
|
337
|
+
bot.api.sendVoice(chatId, file, {
|
|
338
|
+
caption,
|
|
339
|
+
reply_parameters: rp,
|
|
340
|
+
}),
|
|
341
|
+
);
|
|
342
|
+
break;
|
|
208
343
|
}
|
|
209
344
|
return { ok: true, message_id: sent.message_id };
|
|
210
345
|
}
|
|
211
346
|
|
|
212
347
|
case "send_sticker": {
|
|
213
348
|
gateway.incrementMessages(chatId);
|
|
214
|
-
const sent = await bot.api.sendSticker(
|
|
349
|
+
const sent = await bot.api.sendSticker(
|
|
350
|
+
chatId,
|
|
351
|
+
String(body.file_id ?? ""),
|
|
352
|
+
{ reply_parameters: replyParams(body) },
|
|
353
|
+
);
|
|
215
354
|
return { ok: true, message_id: sent.message_id };
|
|
216
355
|
}
|
|
217
356
|
|
|
218
357
|
case "send_poll": {
|
|
219
358
|
gateway.incrementMessages(chatId);
|
|
220
|
-
const sent = await bot.api.sendPoll(
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
359
|
+
const sent = await bot.api.sendPoll(
|
|
360
|
+
chatId,
|
|
361
|
+
String(body.question ?? ""),
|
|
362
|
+
((body.options as string[]) ?? []).map((o) => ({ text: o })),
|
|
363
|
+
{
|
|
364
|
+
is_anonymous: body.is_anonymous as boolean | undefined,
|
|
365
|
+
allows_multiple_answers: body.allows_multiple_answers as
|
|
366
|
+
| boolean
|
|
367
|
+
| undefined,
|
|
368
|
+
type: body.type as "regular" | "quiz" | undefined,
|
|
369
|
+
correct_option_ids:
|
|
370
|
+
body.correct_option_id != null
|
|
371
|
+
? [body.correct_option_id as number]
|
|
372
|
+
: undefined,
|
|
373
|
+
explanation: body.explanation as string | undefined,
|
|
374
|
+
},
|
|
375
|
+
);
|
|
225
376
|
return { ok: true, message_id: sent.message_id };
|
|
226
377
|
}
|
|
227
378
|
|
|
228
379
|
case "send_location": {
|
|
229
380
|
gateway.incrementMessages(chatId);
|
|
230
|
-
const sent = await bot.api.sendLocation(
|
|
381
|
+
const sent = await bot.api.sendLocation(
|
|
382
|
+
chatId,
|
|
383
|
+
Number(body.latitude),
|
|
384
|
+
Number(body.longitude),
|
|
385
|
+
);
|
|
231
386
|
return { ok: true, message_id: sent.message_id };
|
|
232
387
|
}
|
|
233
388
|
|
|
234
389
|
case "send_contact": {
|
|
235
390
|
gateway.incrementMessages(chatId);
|
|
236
|
-
const sent = await bot.api.sendContact(
|
|
237
|
-
|
|
391
|
+
const sent = await bot.api.sendContact(
|
|
392
|
+
chatId,
|
|
393
|
+
String(body.phone_number),
|
|
394
|
+
String(body.first_name),
|
|
395
|
+
{ last_name: body.last_name as string | undefined },
|
|
396
|
+
);
|
|
238
397
|
return { ok: true, message_id: sent.message_id };
|
|
239
398
|
}
|
|
240
399
|
|
|
241
400
|
case "send_dice": {
|
|
242
401
|
gateway.incrementMessages(chatId);
|
|
243
|
-
const sent = await bot.api.sendDice(
|
|
244
|
-
|
|
402
|
+
const sent = await bot.api.sendDice(
|
|
403
|
+
chatId,
|
|
404
|
+
(body.emoji as string) || "\uD83C\uDFB2",
|
|
405
|
+
);
|
|
406
|
+
return {
|
|
407
|
+
ok: true,
|
|
408
|
+
message_id: sent.message_id,
|
|
409
|
+
value: sent.dice?.value,
|
|
410
|
+
};
|
|
245
411
|
}
|
|
246
412
|
|
|
247
413
|
// ── Chat info ──────────────────────────────────────────────────────
|
|
248
414
|
case "get_chat_info": {
|
|
249
415
|
const chat = await bot.api.getChat(chatId);
|
|
250
|
-
const count = await bot.api
|
|
251
|
-
|
|
416
|
+
const count = await bot.api
|
|
417
|
+
.getChatMemberCount(chatId)
|
|
418
|
+
.catch(() => null);
|
|
419
|
+
return {
|
|
420
|
+
ok: true,
|
|
421
|
+
id: chat.id,
|
|
422
|
+
type: chat.type,
|
|
423
|
+
title: "title" in chat ? chat.title : undefined,
|
|
424
|
+
member_count: count,
|
|
425
|
+
};
|
|
252
426
|
}
|
|
253
427
|
|
|
254
428
|
case "get_chat_member": {
|
|
255
429
|
const m = await bot.api.getChatMember(chatId, Number(body.user_id));
|
|
256
|
-
return {
|
|
430
|
+
return {
|
|
431
|
+
ok: true,
|
|
432
|
+
status: m.status,
|
|
433
|
+
user: {
|
|
434
|
+
id: m.user.id,
|
|
435
|
+
first_name: m.user.first_name,
|
|
436
|
+
username: m.user.username,
|
|
437
|
+
},
|
|
438
|
+
};
|
|
257
439
|
}
|
|
258
440
|
|
|
259
441
|
case "get_chat_admins": {
|
|
260
442
|
const admins = await bot.api.getChatAdministrators(chatId);
|
|
261
|
-
const text = admins
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
443
|
+
const text = admins
|
|
444
|
+
.map((a) => {
|
|
445
|
+
const name = [a.user.first_name, a.user.last_name]
|
|
446
|
+
.filter(Boolean)
|
|
447
|
+
.join(" ");
|
|
448
|
+
const title =
|
|
449
|
+
"custom_title" in a && a.custom_title
|
|
450
|
+
? ` "${a.custom_title}"`
|
|
451
|
+
: "";
|
|
452
|
+
return `${name}${title} (${a.status}) id:${a.user.id}`;
|
|
453
|
+
})
|
|
454
|
+
.join("\n");
|
|
266
455
|
return { ok: true, text };
|
|
267
456
|
}
|
|
268
457
|
|
|
@@ -274,79 +463,140 @@ export function createTelegramActionHandler(
|
|
|
274
463
|
return { ok: true };
|
|
275
464
|
|
|
276
465
|
case "set_chat_description":
|
|
277
|
-
await bot.api.setChatDescription(
|
|
466
|
+
await bot.api.setChatDescription(
|
|
467
|
+
chatId,
|
|
468
|
+
String(body.description ?? ""),
|
|
469
|
+
);
|
|
278
470
|
return { ok: true };
|
|
279
471
|
|
|
280
472
|
// ── History (userbot-enhanced) ─────────────────────────────────────
|
|
281
473
|
// These OVERRIDE the shared in-memory history when userbot is available
|
|
282
474
|
case "read_history":
|
|
283
475
|
if (isUserClientReady()) {
|
|
284
|
-
return {
|
|
476
|
+
return {
|
|
477
|
+
ok: true,
|
|
478
|
+
text: await userbotHistory({
|
|
479
|
+
chatId,
|
|
480
|
+
limit: Math.min(100, Number(body.limit ?? 30)),
|
|
481
|
+
offsetId: body.offset_id as number | undefined,
|
|
482
|
+
before: body.before as string | undefined,
|
|
483
|
+
}),
|
|
484
|
+
};
|
|
285
485
|
}
|
|
286
486
|
return null; // fall through to shared handler
|
|
287
487
|
|
|
288
488
|
case "search_history":
|
|
289
489
|
if (isUserClientReady()) {
|
|
290
|
-
return {
|
|
490
|
+
return {
|
|
491
|
+
ok: true,
|
|
492
|
+
text: await userbotSearch({
|
|
493
|
+
chatId,
|
|
494
|
+
query: String(body.query ?? ""),
|
|
495
|
+
limit: Math.min(100, Number(body.limit ?? 20)),
|
|
496
|
+
}),
|
|
497
|
+
};
|
|
291
498
|
}
|
|
292
499
|
return null;
|
|
293
500
|
|
|
294
501
|
case "get_user_messages":
|
|
295
502
|
if (isUserClientReady()) {
|
|
296
|
-
return {
|
|
503
|
+
return {
|
|
504
|
+
ok: true,
|
|
505
|
+
text: await userbotSearch({
|
|
506
|
+
chatId,
|
|
507
|
+
query: String(body.user_name ?? ""),
|
|
508
|
+
limit: Math.min(50, Number(body.limit ?? 20)),
|
|
509
|
+
}),
|
|
510
|
+
};
|
|
297
511
|
}
|
|
298
512
|
return null;
|
|
299
513
|
|
|
300
514
|
case "list_known_users":
|
|
301
515
|
if (isUserClientReady()) {
|
|
302
|
-
return {
|
|
516
|
+
return {
|
|
517
|
+
ok: true,
|
|
518
|
+
text: await userbotParticipantDetails({
|
|
519
|
+
chatId,
|
|
520
|
+
limit: Number(body.limit ?? 50),
|
|
521
|
+
}),
|
|
522
|
+
};
|
|
303
523
|
}
|
|
304
524
|
return null;
|
|
305
525
|
|
|
306
526
|
case "get_member_info":
|
|
307
527
|
if (isUserClientReady()) {
|
|
308
|
-
return {
|
|
528
|
+
return {
|
|
529
|
+
ok: true,
|
|
530
|
+
text: await userbotGetUserInfo({
|
|
531
|
+
chatId,
|
|
532
|
+
userId: Number(body.user_id),
|
|
533
|
+
}),
|
|
534
|
+
};
|
|
309
535
|
}
|
|
310
536
|
return { ok: false, error: "User client not connected." };
|
|
311
537
|
|
|
312
538
|
case "get_message_by_id":
|
|
313
539
|
if (isUserClientReady()) {
|
|
314
|
-
return {
|
|
540
|
+
return {
|
|
541
|
+
ok: true,
|
|
542
|
+
text: await userbotGetMessage({
|
|
543
|
+
chatId,
|
|
544
|
+
messageId: Number(body.message_id),
|
|
545
|
+
}),
|
|
546
|
+
};
|
|
315
547
|
}
|
|
316
548
|
return { ok: false, error: "User client not connected." };
|
|
317
549
|
|
|
318
550
|
case "get_pinned_messages":
|
|
319
|
-
if (isUserClientReady())
|
|
551
|
+
if (isUserClientReady())
|
|
552
|
+
return { ok: true, text: await userbotPinnedMessages({ chatId }) };
|
|
320
553
|
return { ok: false, error: "User client not connected." };
|
|
321
554
|
|
|
322
555
|
case "online_count":
|
|
323
|
-
if (isUserClientReady())
|
|
556
|
+
if (isUserClientReady())
|
|
557
|
+
return { ok: true, text: await userbotOnlineCount({ chatId }) };
|
|
324
558
|
return { ok: false, error: "User client not connected." };
|
|
325
559
|
|
|
326
560
|
case "save_sticker_pack": {
|
|
327
|
-
const text = await userbotSaveStickerPack({
|
|
561
|
+
const text = await userbotSaveStickerPack({
|
|
562
|
+
setName: String(body.set_name ?? ""),
|
|
563
|
+
bot,
|
|
564
|
+
});
|
|
328
565
|
return { ok: true, text };
|
|
329
566
|
}
|
|
330
567
|
|
|
331
568
|
case "get_sticker_pack": {
|
|
332
|
-
const stickerSet = await bot.api.getStickerSet(
|
|
333
|
-
|
|
334
|
-
|
|
569
|
+
const stickerSet = await bot.api.getStickerSet(
|
|
570
|
+
String(body.set_name ?? ""),
|
|
571
|
+
);
|
|
572
|
+
const lines = stickerSet.stickers.map(
|
|
573
|
+
(s, i) =>
|
|
574
|
+
`${i + 1}. ${s.emoji ?? ""} [${s.is_animated ? "animated" : s.is_video ? "video" : "static"}] file_id: ${s.file_id}`,
|
|
575
|
+
);
|
|
576
|
+
return {
|
|
577
|
+
ok: true,
|
|
578
|
+
text: `Sticker pack: "${stickerSet.title}" (${stickerSet.stickers.length} stickers)\nSet name: ${stickerSet.name}\n\n${lines.join("\n")}`,
|
|
579
|
+
};
|
|
335
580
|
}
|
|
336
581
|
|
|
337
582
|
case "download_sticker": {
|
|
338
583
|
const file = await bot.api.getFile(String(body.file_id ?? ""));
|
|
339
|
-
if (!file.file_path)
|
|
584
|
+
if (!file.file_path)
|
|
585
|
+
return { ok: false, error: "Could not get file path" };
|
|
340
586
|
const url = `https://api.telegram.org/file/bot${botToken}/${file.file_path}`;
|
|
341
587
|
const resp = await fetch(url);
|
|
342
|
-
if (!resp.ok)
|
|
588
|
+
if (!resp.ok)
|
|
589
|
+
return { ok: false, error: `Download failed: ${resp.status}` };
|
|
343
590
|
const buffer = Buffer.from(await resp.arrayBuffer());
|
|
344
591
|
const ext = file.file_path.split(".").pop() ?? "webp";
|
|
345
592
|
const uploadsDir = dirs.uploads;
|
|
346
593
|
if (!existsSync(uploadsDir)) mkdirSync(uploadsDir, { recursive: true });
|
|
347
594
|
const filePath = resolve(uploadsDir, `${Date.now()}-sticker.${ext}`);
|
|
348
595
|
writeFileSync(filePath, buffer);
|
|
349
|
-
return {
|
|
596
|
+
return {
|
|
597
|
+
ok: true,
|
|
598
|
+
text: `Downloaded sticker to: ${filePath} (${buffer.length} bytes).`,
|
|
599
|
+
};
|
|
350
600
|
}
|
|
351
601
|
|
|
352
602
|
// ── Sticker pack management ──────────────────────────────────────
|
|
@@ -356,13 +606,19 @@ export function createTelegramActionHandler(
|
|
|
356
606
|
const title = String(body.title ?? "");
|
|
357
607
|
const filePath = String(body.file_path ?? "");
|
|
358
608
|
const emojis = (body.emoji_list as string[]) ?? ["🎨"];
|
|
359
|
-
const format =
|
|
609
|
+
const format =
|
|
610
|
+
(body.format as "static" | "animated" | "video") ?? "static";
|
|
360
611
|
if (!userId || !name || !title || !filePath) {
|
|
361
|
-
return {
|
|
612
|
+
return {
|
|
613
|
+
ok: false,
|
|
614
|
+
error: "Required: user_id, name, title, file_path",
|
|
615
|
+
};
|
|
362
616
|
}
|
|
363
617
|
// Sticker set names must end with _by_<bot_username>
|
|
364
618
|
const botUsername = bot.botInfo?.username ?? "";
|
|
365
|
-
const fullName = name.endsWith(`_by_${botUsername}`)
|
|
619
|
+
const fullName = name.endsWith(`_by_${botUsername}`)
|
|
620
|
+
? name
|
|
621
|
+
: `${name}_by_${botUsername}`;
|
|
366
622
|
const data = readFileSync(filePath);
|
|
367
623
|
const sticker = {
|
|
368
624
|
sticker: new InputFileClass(data, basename(filePath)),
|
|
@@ -370,7 +626,10 @@ export function createTelegramActionHandler(
|
|
|
370
626
|
emoji_list: emojis,
|
|
371
627
|
};
|
|
372
628
|
await bot.api.createNewStickerSet(userId, fullName, title, [sticker]);
|
|
373
|
-
return {
|
|
629
|
+
return {
|
|
630
|
+
ok: true,
|
|
631
|
+
text: `Created sticker pack "${title}" (${fullName}) with 1 sticker.`,
|
|
632
|
+
};
|
|
374
633
|
}
|
|
375
634
|
|
|
376
635
|
case "add_sticker_to_set": {
|
|
@@ -378,7 +637,8 @@ export function createTelegramActionHandler(
|
|
|
378
637
|
const name = String(body.name ?? "");
|
|
379
638
|
const filePath = String(body.file_path ?? "");
|
|
380
639
|
const emojis = (body.emoji_list as string[]) ?? ["🎨"];
|
|
381
|
-
const format =
|
|
640
|
+
const format =
|
|
641
|
+
(body.format as "static" | "animated" | "video") ?? "static";
|
|
382
642
|
if (!userId || !name || !filePath) {
|
|
383
643
|
return { ok: false, error: "Required: user_id, name, file_path" };
|
|
384
644
|
}
|
|
@@ -394,7 +654,8 @@ export function createTelegramActionHandler(
|
|
|
394
654
|
|
|
395
655
|
case "delete_sticker_from_set": {
|
|
396
656
|
const stickerId = String(body.sticker_file_id ?? "");
|
|
397
|
-
if (!stickerId)
|
|
657
|
+
if (!stickerId)
|
|
658
|
+
return { ok: false, error: "Required: sticker_file_id" };
|
|
398
659
|
await bot.api.deleteStickerFromSet(stickerId);
|
|
399
660
|
return { ok: true, text: "Sticker deleted from pack." };
|
|
400
661
|
}
|
|
@@ -402,7 +663,8 @@ export function createTelegramActionHandler(
|
|
|
402
663
|
case "set_sticker_set_title": {
|
|
403
664
|
const name = String(body.name ?? "");
|
|
404
665
|
const title = String(body.title ?? "");
|
|
405
|
-
if (!name || !title)
|
|
666
|
+
if (!name || !title)
|
|
667
|
+
return { ok: false, error: "Required: name, title" };
|
|
406
668
|
await bot.api.setStickerSetTitle(name, title);
|
|
407
669
|
return { ok: true, text: `Pack title updated to "${title}".` };
|
|
408
670
|
}
|
|
@@ -418,14 +680,28 @@ export function createTelegramActionHandler(
|
|
|
418
680
|
const msgId = Number(body.message_id);
|
|
419
681
|
if (!msgId) return { ok: false, error: "Required: message_id" };
|
|
420
682
|
const poll = await bot.api.stopPoll(chatId, msgId);
|
|
421
|
-
const results = poll.options
|
|
422
|
-
|
|
683
|
+
const results = poll.options
|
|
684
|
+
.map(
|
|
685
|
+
(o) =>
|
|
686
|
+
` ${o.text}: ${o.voter_count} vote${o.voter_count === 1 ? "" : "s"}`,
|
|
687
|
+
)
|
|
688
|
+
.join("\n");
|
|
689
|
+
return {
|
|
690
|
+
ok: true,
|
|
691
|
+
text: `Poll closed: "${poll.question}"\nTotal voters: ${poll.total_voter_count}\n\nResults:\n${results}`,
|
|
692
|
+
};
|
|
423
693
|
}
|
|
424
694
|
|
|
425
695
|
case "download_media": {
|
|
426
696
|
if (isUserClientReady()) {
|
|
427
697
|
const { downloadMessageMedia } = await import("./userbot.js");
|
|
428
|
-
return {
|
|
698
|
+
return {
|
|
699
|
+
ok: true,
|
|
700
|
+
text: await downloadMessageMedia({
|
|
701
|
+
chatId,
|
|
702
|
+
messageId: Number(body.message_id),
|
|
703
|
+
}),
|
|
704
|
+
};
|
|
429
705
|
}
|
|
430
706
|
return { ok: false, error: "User client not connected." };
|
|
431
707
|
}
|