@bytesbrains/pi-telegram-bridge 1.1.5 → 1.1.6
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/package.json +2 -2
- package/src/__tests__/helpers.test.ts +9 -9
- package/src/__tests__/index.test.ts +3 -2
- package/src/helpers.ts +136 -5
- package/src/index.ts +101 -10
- package/src/templates.ts +14 -6
- package/src/tools/telegram.ts +5 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytesbrains/pi-telegram-bridge",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"description": "Telegram bot bridge for pi agents
|
|
3
|
+
"version": "1.1.6",
|
|
4
|
+
"description": "Telegram bot bridge for pi agents \u2014 send messages, ask questions, and listen for human replies via Telegram.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
7
7
|
"pi-extension",
|
|
@@ -437,7 +437,7 @@ describe("pollUpdates", () => {
|
|
|
437
437
|
// pollUpdates has 5s delay between polls — this test takes ~5s
|
|
438
438
|
await helpers.pollUpdates(
|
|
439
439
|
TEST_TOKEN, TEST_CHAT_ID, 12345, 0,
|
|
440
|
-
controller.signal, onMessage, onUpdateId,
|
|
440
|
+
controller.signal, onMessage, vi.fn(), onUpdateId,
|
|
441
441
|
);
|
|
442
442
|
|
|
443
443
|
expect(onMessage).toHaveBeenCalledWith("Hello agent!");
|
|
@@ -482,7 +482,7 @@ describe("pollUpdates", () => {
|
|
|
482
482
|
|
|
483
483
|
await helpers.pollUpdates(
|
|
484
484
|
TEST_TOKEN, TEST_CHAT_ID, 12345, 0,
|
|
485
|
-
controller.signal, onMessage, onUpdateId,
|
|
485
|
+
controller.signal, onMessage, vi.fn(), onUpdateId,
|
|
486
486
|
);
|
|
487
487
|
|
|
488
488
|
expect(onMessage).not.toHaveBeenCalled();
|
|
@@ -527,7 +527,7 @@ describe("pollUpdates", () => {
|
|
|
527
527
|
|
|
528
528
|
await helpers.pollUpdates(
|
|
529
529
|
TEST_TOKEN, TEST_CHAT_ID, 12345, 0,
|
|
530
|
-
controller.signal, onMessage, onUpdateId,
|
|
530
|
+
controller.signal, onMessage, vi.fn(), onUpdateId,
|
|
531
531
|
);
|
|
532
532
|
|
|
533
533
|
expect(onMessage).not.toHaveBeenCalled();
|
|
@@ -567,7 +567,7 @@ describe("pollUpdates", () => {
|
|
|
567
567
|
|
|
568
568
|
await helpers.pollUpdates(
|
|
569
569
|
TEST_TOKEN, TEST_CHAT_ID, 12345, 0,
|
|
570
|
-
controller.signal, onMessage, onUpdateId,
|
|
570
|
+
controller.signal, onMessage, vi.fn(), onUpdateId,
|
|
571
571
|
);
|
|
572
572
|
|
|
573
573
|
expect(onMessage).not.toHaveBeenCalled();
|
|
@@ -615,7 +615,7 @@ describe("pollUpdates", () => {
|
|
|
615
615
|
|
|
616
616
|
await helpers.pollUpdates(
|
|
617
617
|
TEST_TOKEN, TEST_CHAT_ID, 12345, 0,
|
|
618
|
-
controller.signal, onMessage, onUpdateId,
|
|
618
|
+
controller.signal, onMessage, vi.fn(), onUpdateId,
|
|
619
619
|
);
|
|
620
620
|
|
|
621
621
|
expect(onMessage).toHaveBeenCalledWith("recovered");
|
|
@@ -660,7 +660,7 @@ describe("pollUpdates", () => {
|
|
|
660
660
|
|
|
661
661
|
await helpers.pollUpdates(
|
|
662
662
|
TEST_TOKEN, TEST_CHAT_ID, 12345, 0,
|
|
663
|
-
controller.signal, onMessage, onUpdateId,
|
|
663
|
+
controller.signal, onMessage, vi.fn(), onUpdateId,
|
|
664
664
|
);
|
|
665
665
|
|
|
666
666
|
expect(onMessage).toHaveBeenCalledWith("eventually");
|
|
@@ -681,7 +681,7 @@ describe("pollUpdates", () => {
|
|
|
681
681
|
|
|
682
682
|
await helpers.pollUpdates(
|
|
683
683
|
TEST_TOKEN, TEST_CHAT_ID, 12345, 0,
|
|
684
|
-
controller.signal, onMessage, onUpdateId,
|
|
684
|
+
controller.signal, onMessage, vi.fn(), onUpdateId,
|
|
685
685
|
);
|
|
686
686
|
|
|
687
687
|
expect(onMessage).not.toHaveBeenCalled();
|
|
@@ -731,7 +731,7 @@ describe("pollUpdates", () => {
|
|
|
731
731
|
|
|
732
732
|
await helpers.pollUpdates(
|
|
733
733
|
TEST_TOKEN, TEST_CHAT_ID, 12345, 0,
|
|
734
|
-
controller.signal, onMessage, onUpdateId,
|
|
734
|
+
controller.signal, onMessage, vi.fn(), onUpdateId,
|
|
735
735
|
);
|
|
736
736
|
|
|
737
737
|
expect(onMessage).toHaveBeenCalledWith("after not-ok");
|
|
@@ -775,7 +775,7 @@ describe("pollUpdates", () => {
|
|
|
775
775
|
|
|
776
776
|
await helpers.pollUpdates(
|
|
777
777
|
TEST_TOKEN, TEST_CHAT_ID, 12345, 0,
|
|
778
|
-
controller.signal, onMessage, onUpdateId,
|
|
778
|
+
controller.signal, onMessage, vi.fn(), onUpdateId,
|
|
779
779
|
);
|
|
780
780
|
|
|
781
781
|
expect(onMessage).not.toHaveBeenCalled();
|
|
@@ -104,17 +104,18 @@ afterEach(() => {
|
|
|
104
104
|
// ── Tool Registration ─────────────────────────────────────────────
|
|
105
105
|
|
|
106
106
|
describe("extension registration", () => {
|
|
107
|
-
it("registers all
|
|
107
|
+
it("registers all 7 tools", () => {
|
|
108
108
|
const pi = createMockPI();
|
|
109
109
|
telegramBridge(pi as any);
|
|
110
110
|
|
|
111
|
-
expect(pi.registerTool).toHaveBeenCalledTimes(
|
|
111
|
+
expect(pi.registerTool).toHaveBeenCalledTimes(7);
|
|
112
112
|
expect(pi._getTool("telegram_listen")).toBeDefined();
|
|
113
113
|
expect(pi._getTool("telegram_send")).toBeDefined();
|
|
114
114
|
expect(pi._getTool("telegram_ask")).toBeDefined();
|
|
115
115
|
expect(pi._getTool("telegram_override")).toBeDefined();
|
|
116
116
|
expect(pi._getTool("telegram_status")).toBeDefined();
|
|
117
117
|
expect(pi._getTool("telegram_notify")).toBeDefined();
|
|
118
|
+
expect(pi._getTool("telegram_send_photo")).toBeDefined();
|
|
118
119
|
});
|
|
119
120
|
|
|
120
121
|
it("registers session_start and session_shutdown handlers", () => {
|
package/src/helpers.ts
CHANGED
|
@@ -48,11 +48,131 @@ export async function sendMsg(
|
|
|
48
48
|
parse_mode: parseMode,
|
|
49
49
|
};
|
|
50
50
|
if (replyMarkup) body.reply_markup = JSON.stringify(replyMarkup);
|
|
51
|
-
|
|
51
|
+
try {
|
|
52
|
+
const r = (await telegramApi("sendMessage", body)) as {
|
|
53
|
+
ok: boolean;
|
|
54
|
+
result: { message_id: number };
|
|
55
|
+
};
|
|
56
|
+
return r.result.message_id;
|
|
57
|
+
} catch (e: unknown) {
|
|
58
|
+
// Retry without parse_mode if formatting caused a 400 parse error
|
|
59
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
60
|
+
if (parseMode && msg.includes("400") && (msg.includes("parse") || msg.includes("entity"))) {
|
|
61
|
+
delete body.parse_mode;
|
|
62
|
+
const r2 = (await telegramApi("sendMessage", body)) as {
|
|
63
|
+
ok: boolean;
|
|
64
|
+
result: { message_id: number };
|
|
65
|
+
};
|
|
66
|
+
return r2.result.message_id;
|
|
67
|
+
}
|
|
68
|
+
throw e;
|
|
69
|
+
}}
|
|
70
|
+
|
|
71
|
+
// ── Photo / File Support ────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get a Telegram file URL for downloading.
|
|
75
|
+
* Returns { file_path } from the getFile API.
|
|
76
|
+
*/
|
|
77
|
+
export async function getFileUrl(fileId: string): Promise<string | null> {
|
|
78
|
+
const token = getToken();
|
|
79
|
+
try {
|
|
80
|
+
const res = await fetch(
|
|
81
|
+
`${BASE_URL}${token}/getFile`,
|
|
82
|
+
{
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers: { "Content-Type": "application/json" },
|
|
85
|
+
body: JSON.stringify({ file_id: fileId }),
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
if (!res.ok) return null;
|
|
89
|
+
const data = (await res.json()) as {
|
|
90
|
+
ok: boolean;
|
|
91
|
+
result: { file_path: string };
|
|
92
|
+
};
|
|
93
|
+
if (!data.ok || !data.result?.file_path) return null;
|
|
94
|
+
return `https://api.telegram.org/file/bot${token}/${data.result.file_path}`;
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Download a Telegram file to a local path.
|
|
102
|
+
* Returns the file path on success, null on failure.
|
|
103
|
+
* Creates parent directories automatically.
|
|
104
|
+
*/
|
|
105
|
+
export async function downloadFile(fileId: string, destPath: string): Promise<string | null> {
|
|
106
|
+
try {
|
|
107
|
+
const fileUrl = await getFileUrl(fileId);
|
|
108
|
+
if (!fileUrl) return null;
|
|
109
|
+
const res = await fetch(fileUrl);
|
|
110
|
+
if (!res.ok) return null;
|
|
111
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
112
|
+
const path = await import("node:path");
|
|
113
|
+
const fsPromises = await import("node:fs/promises");
|
|
114
|
+
const dir = path.dirname(destPath);
|
|
115
|
+
await fsPromises.mkdir(dir, { recursive: true });
|
|
116
|
+
await fsPromises.writeFile(destPath, buffer);
|
|
117
|
+
return destPath;
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Send a photo to the Telegram chat.
|
|
125
|
+
* Supports sending from a local file path or a remote URL.
|
|
126
|
+
*/
|
|
127
|
+
export async function sendPhoto(
|
|
128
|
+
photoPath: string,
|
|
129
|
+
caption?: string,
|
|
130
|
+
): Promise<number> {
|
|
131
|
+
const fs = await import("node:fs/promises");
|
|
132
|
+
let fileData: Buffer;
|
|
133
|
+
let filename: string;
|
|
134
|
+
|
|
135
|
+
// Check if it's a URL
|
|
136
|
+
if (photoPath.startsWith("http://") || photoPath.startsWith("https://")) {
|
|
137
|
+
filename = "photo.jpg";
|
|
138
|
+
const res = await fetch(photoPath);
|
|
139
|
+
if (!res.ok) throw new Error(`Failed to fetch photo: ${res.status}`);
|
|
140
|
+
fileData = Buffer.from(await res.arrayBuffer());
|
|
141
|
+
} else {
|
|
142
|
+
filename = photoPath;
|
|
143
|
+
fileData = await fs.readFile(photoPath);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const token = getToken();
|
|
147
|
+
const formData = new FormData();
|
|
148
|
+
formData.set("chat_id", getChatId());
|
|
149
|
+
const blob = new Blob([fileData] as any);
|
|
150
|
+
formData.set("photo", blob, filename);
|
|
151
|
+
if (caption) {
|
|
152
|
+
formData.set("caption", caption);
|
|
153
|
+
formData.set("parse_mode", "HTML");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const url = `${BASE_URL}${token}/sendPhoto`;
|
|
157
|
+
const res = await fetch(url, {
|
|
158
|
+
method: "POST",
|
|
159
|
+
body: formData,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (!res.ok) {
|
|
163
|
+
const errText = await res.text();
|
|
164
|
+
// Retry without caption if parse_mode caused a 400
|
|
165
|
+
if (caption && res.status === 400 && (errText.includes("parse") || errText.includes("entity"))) {
|
|
166
|
+
return sendPhoto(photoPath);
|
|
167
|
+
}
|
|
168
|
+
throw new Error(`sendPhoto failed: ${res.status} ${errText}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const data = (await res.json()) as {
|
|
52
172
|
ok: boolean;
|
|
53
173
|
result: { message_id: number };
|
|
54
174
|
};
|
|
55
|
-
return
|
|
175
|
+
return data.result.message_id;
|
|
56
176
|
}
|
|
57
177
|
|
|
58
178
|
// ── Polling ─────────────────────────────────────────────────────────
|
|
@@ -189,6 +309,7 @@ export async function pollUpdates(
|
|
|
189
309
|
lastUpdateId: number,
|
|
190
310
|
signal: AbortSignal,
|
|
191
311
|
onMessage: (text: string) => void,
|
|
312
|
+
onPhoto: (photoId: string, caption?: string) => void,
|
|
192
313
|
onUpdateId: (id: number) => void,
|
|
193
314
|
): Promise<void> {
|
|
194
315
|
while (!signal.aborted) {
|
|
@@ -212,6 +333,8 @@ export async function pollUpdates(
|
|
|
212
333
|
chat: { id: number };
|
|
213
334
|
from?: { id: number; is_bot?: boolean };
|
|
214
335
|
text?: string;
|
|
336
|
+
photo?: Array<{ file_id: string; file_unique_id: string; width: number; height: number }>;
|
|
337
|
+
caption?: string;
|
|
215
338
|
};
|
|
216
339
|
callback_query?: unknown;
|
|
217
340
|
}>;
|
|
@@ -225,16 +348,24 @@ export async function pollUpdates(
|
|
|
225
348
|
onUpdateId(lastUpdateId);
|
|
226
349
|
// Skip callback queries (handled by telegram_ask)
|
|
227
350
|
if (u.callback_query) continue;
|
|
228
|
-
//
|
|
351
|
+
// Process messages from the configured chat, not from our bot
|
|
229
352
|
if (
|
|
230
353
|
u.message &&
|
|
231
|
-
u.message.text &&
|
|
232
354
|
String(u.message.chat.id) === chatId
|
|
233
355
|
) {
|
|
234
356
|
if (u.message.from?.is_bot || u.message.from?.id === botId) {
|
|
235
357
|
continue;
|
|
236
358
|
}
|
|
237
|
-
|
|
359
|
+
// Photo message
|
|
360
|
+
if (u.message.photo && u.message.photo.length > 0) {
|
|
361
|
+
const largest = u.message.photo[u.message.photo.length - 1];
|
|
362
|
+
onPhoto(largest.file_id, u.message.caption);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
// Text message
|
|
366
|
+
if (u.message.text) {
|
|
367
|
+
onMessage(u.message.text);
|
|
368
|
+
}
|
|
238
369
|
}
|
|
239
370
|
}
|
|
240
371
|
} catch (e: unknown) {
|
package/src/index.ts
CHANGED
|
@@ -13,11 +13,16 @@ import {
|
|
|
13
13
|
getChatId,
|
|
14
14
|
telegramApi,
|
|
15
15
|
sendMsg,
|
|
16
|
+
sendPhoto,
|
|
17
|
+
getFileUrl,
|
|
18
|
+
downloadFile,
|
|
16
19
|
pollReply,
|
|
17
20
|
getBotId,
|
|
18
21
|
seedLastUpdateId,
|
|
19
22
|
pollUpdates,
|
|
20
23
|
} from "./helpers";
|
|
24
|
+
import * as path from "node:path";
|
|
25
|
+
import * as os from "node:os";
|
|
21
26
|
import {
|
|
22
27
|
listenSchema,
|
|
23
28
|
sendSchema,
|
|
@@ -25,6 +30,7 @@ import {
|
|
|
25
30
|
statusSchema,
|
|
26
31
|
overrideSchema,
|
|
27
32
|
notifySchema,
|
|
33
|
+
sendPhotoSchema,
|
|
28
34
|
} from "./tools/telegram";
|
|
29
35
|
|
|
30
36
|
export default function telegramBridge(pi: ExtensionAPI) {
|
|
@@ -35,6 +41,10 @@ export default function telegramBridge(pi: ExtensionAPI) {
|
|
|
35
41
|
// ── Session lifecycle ───────────────────────────────────────────
|
|
36
42
|
|
|
37
43
|
pi.on("session_start", async (_event, ctx) => {
|
|
44
|
+
// Ensure photo download directory exists
|
|
45
|
+
const { mkdir } = await import("node:fs/promises");
|
|
46
|
+
const photoDir = path.join(os.tmpdir(), "telegram-photos");
|
|
47
|
+
await mkdir(photoDir, { recursive: true }).catch(() => {});
|
|
38
48
|
// Restore lastUpdateId from session state
|
|
39
49
|
for (const entry of ctx.sessionManager.getEntries()) {
|
|
40
50
|
if (entry.type === "custom" && entry.customType === "tg-last-update") {
|
|
@@ -67,6 +77,11 @@ export default function telegramBridge(pi: ExtensionAPI) {
|
|
|
67
77
|
// Seed lastUpdateId
|
|
68
78
|
lastUpdateId = await seedLastUpdateId(lastUpdateId, signal);
|
|
69
79
|
|
|
80
|
+
// Create photo download directory
|
|
81
|
+
const fsPromises = await import("node:fs/promises");
|
|
82
|
+
const photoDir = path.join(os.tmpdir(), "telegram-photos");
|
|
83
|
+
await fsPromises.mkdir(photoDir, { recursive: true }).catch(() => {});
|
|
84
|
+
|
|
70
85
|
// Fire and forget — poll in background
|
|
71
86
|
pollUpdates(
|
|
72
87
|
token,
|
|
@@ -74,7 +89,7 @@ export default function telegramBridge(pi: ExtensionAPI) {
|
|
|
74
89
|
botId,
|
|
75
90
|
lastUpdateId,
|
|
76
91
|
signal,
|
|
77
|
-
// onMessage: forward to pi as user message
|
|
92
|
+
// onMessage: forward text or photo to pi as user message
|
|
78
93
|
async (text: string) => {
|
|
79
94
|
pi.sendUserMessage(text);
|
|
80
95
|
await sendMsg(
|
|
@@ -83,6 +98,23 @@ export default function telegramBridge(pi: ExtensionAPI) {
|
|
|
83
98
|
"Markdown",
|
|
84
99
|
);
|
|
85
100
|
},
|
|
101
|
+
// onPhoto: download and forward photo + caption to pi
|
|
102
|
+
async (photoId: string, caption?: string) => {
|
|
103
|
+
const timestamp = Date.now();
|
|
104
|
+
const filename = `photo_${timestamp}.jpg`;
|
|
105
|
+
const destPath = path.join(photoDir, filename);
|
|
106
|
+
const result = await downloadFile(photoId, destPath);
|
|
107
|
+
if (result) {
|
|
108
|
+
const captionText = caption ? `\n\n📝 Caption: ${caption}` : "";
|
|
109
|
+
pi.sendUserMessage(
|
|
110
|
+
`📸 Photo received from Telegram\nPath: ${result}${captionText}\n\nUse read("${result}") to view the image.`,
|
|
111
|
+
);
|
|
112
|
+
await sendMsg("📸 Photo received! Processing...");
|
|
113
|
+
} else {
|
|
114
|
+
pi.sendUserMessage("📸 Photo received from Telegram but could not be downloaded.");
|
|
115
|
+
await sendMsg("⚠️ Could not download your photo. Please check the bot configuration.");
|
|
116
|
+
}
|
|
117
|
+
},
|
|
86
118
|
// onUpdateId: persist offset
|
|
87
119
|
(id: number) => {
|
|
88
120
|
lastUpdateId = id;
|
|
@@ -147,20 +179,50 @@ export default function telegramBridge(pi: ExtensionAPI) {
|
|
|
147
179
|
if (u.callback_query) continue;
|
|
148
180
|
if (
|
|
149
181
|
u.message &&
|
|
150
|
-
u.message.text &&
|
|
151
182
|
String(u.message.chat.id) === chatId
|
|
152
183
|
) {
|
|
153
184
|
if (u.message.from?.is_bot || u.message.from?.id === botId)
|
|
154
185
|
continue;
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
186
|
+
// Photo message
|
|
187
|
+
if (u.message.photo && u.message.photo.length > 0) {
|
|
188
|
+
const photoId = u.message.photo[u.message.photo.length - 1].file_id;
|
|
189
|
+
const caption = u.message.caption || "";
|
|
190
|
+
const timestamp = Date.now();
|
|
191
|
+
const fsPromises = await import("node:fs/promises");
|
|
192
|
+
const photoDirLocal = path.join(os.tmpdir(), "telegram-photos");
|
|
193
|
+
await fsPromises.mkdir(photoDirLocal, { recursive: true }).catch(() => {});
|
|
194
|
+
const destFile = path.join(photoDirLocal, `photo_${timestamp}.jpg`);
|
|
195
|
+
const result = await downloadFile(photoId, destFile);
|
|
196
|
+
const captionInfo = caption ? `\nCaption: "${caption}"` : "";
|
|
197
|
+
if (result) {
|
|
198
|
+
return {
|
|
199
|
+
content: [{
|
|
200
|
+
type: "text",
|
|
201
|
+
text: `📸 Photo received from Telegram\nPath: ${result}${captionInfo}\n\nUse read("${result}") to view the image.`,
|
|
202
|
+
}],
|
|
203
|
+
details: { message: caption, photoPath: result, hasPhoto: true },
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
content: [{
|
|
158
208
|
type: "text",
|
|
159
|
-
text:
|
|
160
|
-
},
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
209
|
+
text: `📸 Photo received from Telegram but could not be downloaded.${captionInfo}`,
|
|
210
|
+
}],
|
|
211
|
+
details: { message: caption, hasPhoto: true, downloadFailed: true },
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
// Text message
|
|
215
|
+
if (u.message.text) {
|
|
216
|
+
return {
|
|
217
|
+
content: [
|
|
218
|
+
{
|
|
219
|
+
type: "text",
|
|
220
|
+
text: `📩 New message: "${u.message.text}"`,
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
details: { message: u.message.text },
|
|
224
|
+
};
|
|
225
|
+
}
|
|
164
226
|
}
|
|
165
227
|
}
|
|
166
228
|
return {
|
|
@@ -409,6 +471,35 @@ export default function telegramBridge(pi: ExtensionAPI) {
|
|
|
409
471
|
},
|
|
410
472
|
});
|
|
411
473
|
|
|
474
|
+
// telegram_send_photo
|
|
475
|
+
pi.registerTool({
|
|
476
|
+
name: "telegram_send_photo",
|
|
477
|
+
label: "Send Photo",
|
|
478
|
+
description:
|
|
479
|
+
"Send a photo to the Telegram chat. Supports local file paths and remote URLs.",
|
|
480
|
+
parameters: sendPhotoSchema,
|
|
481
|
+
async execute(_id: string, params: { photoPath: string; caption?: string }) {
|
|
482
|
+
try {
|
|
483
|
+
const mid = await sendPhoto(params.photoPath, params.caption);
|
|
484
|
+
return {
|
|
485
|
+
content: [{ type: "text", text: `Photo sent (id:${mid})` }],
|
|
486
|
+
details: { messageId: mid },
|
|
487
|
+
};
|
|
488
|
+
} catch (e: unknown) {
|
|
489
|
+
return {
|
|
490
|
+
content: [
|
|
491
|
+
{
|
|
492
|
+
type: "text",
|
|
493
|
+
text: `Failed to send photo: ${e instanceof Error ? e.message : e}`,
|
|
494
|
+
},
|
|
495
|
+
],
|
|
496
|
+
details: {},
|
|
497
|
+
isError: true,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
},
|
|
501
|
+
});
|
|
502
|
+
|
|
412
503
|
// telegram_status
|
|
413
504
|
pi.registerTool({
|
|
414
505
|
name: "telegram_status",
|
package/src/templates.ts
CHANGED
|
@@ -10,23 +10,31 @@
|
|
|
10
10
|
|
|
11
11
|
const BAR = "━".repeat(20);
|
|
12
12
|
|
|
13
|
+
/** Escape user text for safe embedding inside HTML tags. */
|
|
14
|
+
function escapeHtml(s: string): string {
|
|
15
|
+
return s
|
|
16
|
+
.replace(/&/g, "&")
|
|
17
|
+
.replace(/</g, "<")
|
|
18
|
+
.replace(/>/g, ">");
|
|
19
|
+
}
|
|
20
|
+
|
|
13
21
|
function bold(s: string) {
|
|
14
|
-
return `<b>${s}</b>`;
|
|
22
|
+
return `<b>${escapeHtml(s)}</b>`;
|
|
15
23
|
}
|
|
16
24
|
function italic(s: string) {
|
|
17
|
-
return `<i>${s}</i>`;
|
|
25
|
+
return `<i>${escapeHtml(s)}</i>`;
|
|
18
26
|
}
|
|
19
27
|
function code(s: string) {
|
|
20
|
-
return `<code>${s}</code>`;
|
|
28
|
+
return `<code>${escapeHtml(s)}</code>`;
|
|
21
29
|
}
|
|
22
30
|
function strike(s: string) {
|
|
23
|
-
return `<s>${s}</s>`;
|
|
31
|
+
return `<s>${escapeHtml(s)}</s>`;
|
|
24
32
|
}
|
|
25
33
|
function labelValue(label: string, value: string) {
|
|
26
|
-
return `${bold(label + ":")} ${value}`;
|
|
34
|
+
return `${bold(label + ":")} ${escapeHtml(value)}`;
|
|
27
35
|
}
|
|
28
36
|
function link(url: string, text: string) {
|
|
29
|
-
return `<a href="${url}">${text}</a>`;
|
|
37
|
+
return `<a href="${url}">${escapeHtml(text)}</a>`;
|
|
30
38
|
}
|
|
31
39
|
function progressBar(pct: number, width = 12) {
|
|
32
40
|
const filled = Math.round((pct / 100) * width);
|
package/src/tools/telegram.ts
CHANGED
|
@@ -199,6 +199,11 @@ export const notifySchema = Type.Object({
|
|
|
199
199
|
ackEmoji: Type.Optional(Type.String()),
|
|
200
200
|
});
|
|
201
201
|
|
|
202
|
+
export const sendPhotoSchema = Type.Object({
|
|
203
|
+
photoPath: Type.String({ description: "Local file path or remote URL of the photo to send" }),
|
|
204
|
+
caption: Type.Optional(Type.String({ description: "Optional caption text (HTML formatting supported)" })),
|
|
205
|
+
});
|
|
206
|
+
|
|
202
207
|
export const listenSchema = Type.Object({});
|
|
203
208
|
|
|
204
209
|
export const sendSchema = Type.Object({ message: Type.String() });
|