@bytesbrains/pi-telegram-bridge 1.1.3 → 1.1.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/package.json +2 -1
- package/src/helpers.ts +201 -207
- package/src/index.ts +14 -6
- package/src/templates.ts +640 -551
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytesbrains/pi-telegram-bridge",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"description": "Telegram bot bridge for pi agents — send messages, ask questions, and listen for human replies via Telegram.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"test:watch": "vitest"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
+
"@types/node": "^25.8.0",
|
|
52
53
|
"typescript": "^6.0.3",
|
|
53
54
|
"vitest": "^2.1.9"
|
|
54
55
|
}
|
package/src/helpers.ts
CHANGED
|
@@ -6,247 +6,241 @@ const BASE_URL = "https://api.telegram.org/bot";
|
|
|
6
6
|
// ── Config ──────────────────────────────────────────────────────────
|
|
7
7
|
|
|
8
8
|
export function getToken(): string {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
const tok = process.env.TELEGRAM_BOT_TOKEN;
|
|
10
|
+
if (!tok) throw new Error("TELEGRAM_BOT_TOKEN not set");
|
|
11
|
+
return tok;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export function getChatId(): string {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
const id = process.env.TELEGRAM_CHAT_ID;
|
|
16
|
+
if (!id) throw new Error("TELEGRAM_CHAT_ID not set");
|
|
17
|
+
return id;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
// ── API ─────────────────────────────────────────────────────────────
|
|
21
21
|
|
|
22
22
|
export async function telegramApi(
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
method: string,
|
|
24
|
+
body: Record<string, unknown>,
|
|
25
25
|
): Promise<unknown> {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
26
|
+
const url = `${BASE_URL}${getToken()}/${method}`;
|
|
27
|
+
const res = await fetch(url, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: { "Content-Type": "application/json" },
|
|
30
|
+
body: JSON.stringify(body),
|
|
31
|
+
});
|
|
32
|
+
if (!res.ok) {
|
|
33
|
+
throw new Error(`Telegram API ${res.status}: ${await res.text()}`);
|
|
34
|
+
}
|
|
35
|
+
return res.json();
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
// ── Messaging ───────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
40
|
export async function sendMsg(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
text: string,
|
|
42
|
+
replyMarkup?: object,
|
|
43
|
+
parseMode: "Markdown" | "HTML" | undefined = undefined,
|
|
44
44
|
): Promise<number> {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
45
|
+
const body: Record<string, unknown> = {
|
|
46
|
+
chat_id: getChatId(),
|
|
47
|
+
text,
|
|
48
|
+
parse_mode: parseMode,
|
|
49
|
+
};
|
|
50
|
+
if (replyMarkup) body.reply_markup = JSON.stringify(replyMarkup);
|
|
51
|
+
const r = (await telegramApi("sendMessage", body)) as {
|
|
52
|
+
ok: boolean;
|
|
53
|
+
result: { message_id: number };
|
|
54
|
+
};
|
|
55
|
+
return r.result.message_id;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
// ── Polling ─────────────────────────────────────────────────────────
|
|
59
59
|
|
|
60
60
|
export async function pollReply(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
sentId: number,
|
|
62
|
+
chatId: string,
|
|
63
|
+
timeoutMs: number,
|
|
64
64
|
): Promise<string | null> {
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
const token = getToken();
|
|
66
|
+
const deadline = Date.now() + timeoutMs;
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
68
|
+
// Drain pending updates to start fresh
|
|
69
|
+
let lastId = 0;
|
|
70
|
+
try {
|
|
71
|
+
const drain = await fetch(`${BASE_URL}${token}/getUpdates`, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: { "Content-Type": "application/json" },
|
|
74
|
+
body: JSON.stringify({ timeout: 0 }),
|
|
75
|
+
});
|
|
76
|
+
if (drain.ok) {
|
|
77
|
+
const d = (await drain.json()) as {
|
|
78
|
+
ok: boolean;
|
|
79
|
+
result: Array<{ update_id: number }>;
|
|
80
|
+
};
|
|
81
|
+
if (d.ok && d.result.length > 0) {
|
|
82
|
+
lastId = d.result[d.result.length - 1].update_id;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
/* ignore drain errors */
|
|
87
|
+
}
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
145
|
-
}
|
|
146
|
-
return null; // timeout
|
|
89
|
+
while (Date.now() < deadline) {
|
|
90
|
+
const res = await fetch(`${BASE_URL}${token}/getUpdates`, {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: { "Content-Type": "application/json" },
|
|
93
|
+
body: JSON.stringify({ offset: lastId + 1, timeout: 5 }),
|
|
94
|
+
});
|
|
95
|
+
if (!res.ok) {
|
|
96
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const data = (await res.json()) as {
|
|
100
|
+
ok: boolean;
|
|
101
|
+
result: Array<{
|
|
102
|
+
update_id: number;
|
|
103
|
+
message?: {
|
|
104
|
+
message_id: number;
|
|
105
|
+
chat: { id: number };
|
|
106
|
+
text?: string;
|
|
107
|
+
};
|
|
108
|
+
callback_query?: {
|
|
109
|
+
id: string;
|
|
110
|
+
message: { message_id: number; chat: { id: number } };
|
|
111
|
+
data: string;
|
|
112
|
+
};
|
|
113
|
+
}>;
|
|
114
|
+
};
|
|
115
|
+
if (!data.ok) {
|
|
116
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
for (const u of data.result) {
|
|
120
|
+
lastId = u.update_id;
|
|
121
|
+
// Check for text reply
|
|
122
|
+
if (
|
|
123
|
+
u.message &&
|
|
124
|
+
u.message.chat.id === Number(chatId) &&
|
|
125
|
+
u.message.message_id > sentId &&
|
|
126
|
+
u.message.text
|
|
127
|
+
) {
|
|
128
|
+
return u.message.text;
|
|
129
|
+
}
|
|
130
|
+
// Check for callback query (inline button)
|
|
131
|
+
if (
|
|
132
|
+
u.callback_query &&
|
|
133
|
+
u.callback_query.message.chat.id === Number(chatId)
|
|
134
|
+
) {
|
|
135
|
+
await telegramApi("answerCallbackQuery", {
|
|
136
|
+
callback_query_id: u.callback_query.id,
|
|
137
|
+
});
|
|
138
|
+
return u.callback_query.data;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
142
|
+
}
|
|
143
|
+
return null; // timeout
|
|
147
144
|
}
|
|
148
145
|
|
|
149
146
|
// ── Listener State ──────────────────────────────────────────────────
|
|
150
147
|
|
|
151
148
|
export async function getBotId(): Promise<number | null> {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
149
|
+
try {
|
|
150
|
+
const r = (await telegramApi("getMe", {})) as { result: { id: number } };
|
|
151
|
+
return r.result.id;
|
|
152
|
+
} catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
158
155
|
}
|
|
159
156
|
|
|
160
157
|
export async function seedLastUpdateId(
|
|
161
|
-
|
|
162
|
-
|
|
158
|
+
lastUpdateId: number,
|
|
159
|
+
signal?: AbortSignal,
|
|
163
160
|
): Promise<number> {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
161
|
+
if (lastUpdateId !== 0) return lastUpdateId;
|
|
162
|
+
const token = getToken();
|
|
163
|
+
try {
|
|
164
|
+
const res = await fetch(`${BASE_URL}${token}/getUpdates`, {
|
|
165
|
+
method: "POST",
|
|
166
|
+
headers: { "Content-Type": "application/json" },
|
|
167
|
+
body: JSON.stringify({ timeout: 0 }),
|
|
168
|
+
signal,
|
|
169
|
+
});
|
|
170
|
+
if (res.ok) {
|
|
171
|
+
const d = (await res.json()) as {
|
|
172
|
+
ok: boolean;
|
|
173
|
+
result: Array<{ update_id: number }>;
|
|
174
|
+
};
|
|
175
|
+
if (d.ok && d.result.length > 0) {
|
|
176
|
+
return d.result[d.result.length - 1].update_id;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
/* ignore */
|
|
181
|
+
}
|
|
182
|
+
return lastUpdateId;
|
|
186
183
|
}
|
|
187
184
|
|
|
188
185
|
export async function pollUpdates(
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
186
|
+
token: string,
|
|
187
|
+
chatId: string,
|
|
188
|
+
botId: number | null,
|
|
189
|
+
lastUpdateId: number,
|
|
190
|
+
signal: AbortSignal,
|
|
191
|
+
onMessage: (text: string) => void,
|
|
192
|
+
onUpdateId: (id: number) => void,
|
|
196
193
|
): Promise<void> {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
await new Promise((r) => setTimeout(r, 5000));
|
|
251
|
-
}
|
|
194
|
+
while (!signal.aborted) {
|
|
195
|
+
try {
|
|
196
|
+
const res = await fetch(`${BASE_URL}${token}/getUpdates`, {
|
|
197
|
+
method: "POST",
|
|
198
|
+
headers: { "Content-Type": "application/json" },
|
|
199
|
+
body: JSON.stringify({ offset: lastUpdateId + 1, timeout: 10 }),
|
|
200
|
+
signal,
|
|
201
|
+
});
|
|
202
|
+
if (!res.ok) {
|
|
203
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const data = (await res.json()) as {
|
|
207
|
+
ok: boolean;
|
|
208
|
+
result: Array<{
|
|
209
|
+
update_id: number;
|
|
210
|
+
message?: {
|
|
211
|
+
message_id: number;
|
|
212
|
+
chat: { id: number };
|
|
213
|
+
from?: { id: number; is_bot?: boolean };
|
|
214
|
+
text?: string;
|
|
215
|
+
};
|
|
216
|
+
callback_query?: unknown;
|
|
217
|
+
}>;
|
|
218
|
+
};
|
|
219
|
+
if (!data.ok) {
|
|
220
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
for (const u of data.result) {
|
|
224
|
+
lastUpdateId = u.update_id;
|
|
225
|
+
onUpdateId(lastUpdateId);
|
|
226
|
+
// Skip callback queries (handled by telegram_ask)
|
|
227
|
+
if (u.callback_query) continue;
|
|
228
|
+
// Only process text messages from the configured chat, not from our bot
|
|
229
|
+
if (
|
|
230
|
+
u.message &&
|
|
231
|
+
u.message.text &&
|
|
232
|
+
String(u.message.chat.id) === chatId
|
|
233
|
+
) {
|
|
234
|
+
if (u.message.from?.is_bot || u.message.from?.id === botId) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
onMessage(u.message.text);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
} catch (e: unknown) {
|
|
241
|
+
if (e instanceof Error && e.name === "AbortError") break;
|
|
242
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
243
|
+
}
|
|
244
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
245
|
+
}
|
|
252
246
|
}
|
package/src/index.ts
CHANGED
|
@@ -77,7 +77,11 @@ export default function telegramBridge(pi: ExtensionAPI) {
|
|
|
77
77
|
// onMessage: forward to pi as user message
|
|
78
78
|
async (text: string) => {
|
|
79
79
|
pi.sendUserMessage(text);
|
|
80
|
-
await sendMsg(
|
|
80
|
+
await sendMsg(
|
|
81
|
+
`👂 Got it! Working on: _${text.slice(0, 100)}_`,
|
|
82
|
+
undefined,
|
|
83
|
+
"Markdown",
|
|
84
|
+
);
|
|
81
85
|
},
|
|
82
86
|
// onUpdateId: persist offset
|
|
83
87
|
(id: number) => {
|
|
@@ -226,9 +230,13 @@ export default function telegramBridge(pi: ExtensionAPI) {
|
|
|
226
230
|
const opts = params.options ?? ["Yes", "No", "Explain"];
|
|
227
231
|
const kb = opts.map((o: string) => [{ text: o, callback_data: o }]);
|
|
228
232
|
const timeoutMs = (params.timeoutMinutes ?? 30) * 60000;
|
|
229
|
-
const mid = await sendMsg(
|
|
230
|
-
|
|
231
|
-
|
|
233
|
+
const mid = await sendMsg(
|
|
234
|
+
`❓ *${params.question}*`,
|
|
235
|
+
{
|
|
236
|
+
inline_keyboard: kb,
|
|
237
|
+
},
|
|
238
|
+
"Markdown",
|
|
239
|
+
);
|
|
232
240
|
const reply = await pollReply(mid, chatId, timeoutMs);
|
|
233
241
|
if (!reply) {
|
|
234
242
|
await sendMsg("⏰ No reply. Proceeding autonomously.");
|
|
@@ -296,7 +304,7 @@ export default function telegramBridge(pi: ExtensionAPI) {
|
|
|
296
304
|
message += `\n_What should I do?_`;
|
|
297
305
|
|
|
298
306
|
const kb = opts.map((o: string) => [{ text: o, callback_data: o }]);
|
|
299
|
-
const mid = await sendMsg(message, { inline_keyboard: kb });
|
|
307
|
+
const mid = await sendMsg(message, { inline_keyboard: kb }, "Markdown");
|
|
300
308
|
const reply = await pollReply(mid, chatId, timeoutMs);
|
|
301
309
|
|
|
302
310
|
if (!reply) {
|
|
@@ -328,7 +336,7 @@ export default function telegramBridge(pi: ExtensionAPI) {
|
|
|
328
336
|
action = "explain";
|
|
329
337
|
}
|
|
330
338
|
|
|
331
|
-
await sendMsg(`✅ Choice received: _${choice}_
|
|
339
|
+
await sendMsg(`✅ Choice received: _${choice}_`, undefined, "Markdown");
|
|
332
340
|
|
|
333
341
|
return {
|
|
334
342
|
content: [
|