@poncho-ai/messaging 0.2.8 → 0.3.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/.turbo/turbo-build.log +5 -5
- package/.turbo/turbo-test.log +29 -0
- package/CHANGELOG.md +17 -0
- package/dist/index.d.ts +31 -1
- package/dist/index.js +453 -5
- package/package.json +3 -2
- package/src/adapters/telegram/index.ts +344 -0
- package/src/adapters/telegram/utils.ts +369 -0
- package/src/adapters/telegram/verify.ts +18 -0
- package/src/bridge.ts +19 -5
- package/src/index.ts +2 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @poncho-ai/messaging@0.
|
|
2
|
+
> @poncho-ai/messaging@0.3.0 build /Users/cesar/Dev/latitude/poncho-ai/packages/messaging
|
|
3
3
|
> tsup src/index.ts --format esm --dts
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.ts
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
[34mCLI[39m tsup v8.5.1
|
|
8
8
|
[34mCLI[39m Target: es2022
|
|
9
9
|
[34mESM[39m Build start
|
|
10
|
-
[32mESM[39m [1mdist/index.js [22m[
|
|
11
|
-
[32mESM[39m ⚡️ Build success in
|
|
10
|
+
[32mESM[39m [1mdist/index.js [22m[32m44.67 KB[39m
|
|
11
|
+
[32mESM[39m ⚡️ Build success in 55ms
|
|
12
12
|
[34mDTS[39m Build start
|
|
13
|
-
[32mDTS[39m ⚡️ Build success in
|
|
14
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[
|
|
13
|
+
[32mDTS[39m ⚡️ Build success in 3450ms
|
|
14
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m9.98 KB[39m
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
|
|
2
|
+
> @poncho-ai/messaging@0.2.4 test /Users/cesar/Dev/latitude/poncho-ai/packages/messaging
|
|
3
|
+
> vitest
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
[7m[1m[36m RUN [39m[22m[27m [36mv1.6.1[39m [90m/Users/cesar/Dev/latitude/poncho-ai/packages/messaging[39m
|
|
7
|
+
|
|
8
|
+
[90mstderr[2m | test/bridge.test.ts[2m > [22m[2mAgentBridge[2m > [22m[2mposts an error message and cleans up on runner failure[22m[39m
|
|
9
|
+
[agent-bridge] handleMessage error: Model overloaded
|
|
10
|
+
|
|
11
|
+
[90mstderr[2m | test/bridge.test.ts[2m > [22m[2mAgentBridge[2m > [22m[2mskips sendReply when autoReply is false[22m[39m
|
|
12
|
+
[agent-bridge] tool mode completed without send_email being called; no reply sent
|
|
13
|
+
|
|
14
|
+
[90mstderr[2m | test/bridge.test.ts[2m > [22m[2mAgentBridge[2m > [22m[2msuppresses error reply when hasSentInCurrentRequest is true[22m[39m
|
|
15
|
+
[agent-bridge] handleMessage error: Oops
|
|
16
|
+
|
|
17
|
+
[90mstderr[2m | test/bridge.test.ts[2m > [22m[2mAgentBridge[2m > [22m[2mcalls resetRequestState before handling each message[22m[39m
|
|
18
|
+
[agent-bridge] tool mode completed without send_email being called; no reply sent
|
|
19
|
+
|
|
20
|
+
[32m✓[39m test/bridge.test.ts [2m ([22m[2m15 tests[22m[2m)[22m[90m 7[2mms[22m[39m
|
|
21
|
+
[32m✓[39m test/adapters/email-utils.test.ts [2m ([22m[2m44 tests[22m[2m)[22m[90m 13[2mms[22m[39m
|
|
22
|
+
[32m✓[39m test/adapters/resend.test.ts [2m ([22m[2m13 tests[22m[2m)[22m[90m 7[2mms[22m[39m
|
|
23
|
+
[32m✓[39m test/adapters/slack.test.ts [2m ([22m[2m17 tests[22m[2m)[22m[90m 61[2mms[22m[39m
|
|
24
|
+
|
|
25
|
+
[2m Test Files [22m [1m[32m4 passed[39m[22m[90m (4)[39m
|
|
26
|
+
[2m Tests [22m [1m[32m89 passed[39m[22m[90m (89)[39m
|
|
27
|
+
[2m Start at [22m 17:47:42
|
|
28
|
+
[2m Duration [22m 442ms[2m (transform 223ms, setup 0ms, collect 336ms, tests 88ms, environment 0ms, prepare 304ms)[22m
|
|
29
|
+
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @poncho-ai/messaging
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`de28ef5`](https://github.com/cesr/poncho-ai/commit/de28ef5acceed921269d15816acd392f5208f03d) Thanks [@cesr](https://github.com/cesr)! - Add Telegram messaging adapter with private/group chat support, file attachments, /new command, and typing indicators.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [[`de28ef5`](https://github.com/cesr/poncho-ai/commit/de28ef5acceed921269d15816acd392f5208f03d)]:
|
|
12
|
+
- @poncho-ai/sdk@1.4.1
|
|
13
|
+
|
|
14
|
+
## 0.2.9
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- [`e9b801f`](https://github.com/cesr/poncho-ai/commit/e9b801f0c70ffab6cb434b7adf05df22b29ea9fe) Thanks [@cesr](https://github.com/cesr)! - Derive deterministic UUIDs for messaging conversation IDs instead of composite strings. Fixes Latitude telemetry rejection and ensures consistency with web UI/API conversations.
|
|
19
|
+
|
|
3
20
|
## 0.2.8
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -189,6 +189,36 @@ declare class ResendAdapter implements MessagingAdapter {
|
|
|
189
189
|
private fetchAndDownloadAttachments;
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
interface TelegramAdapterOptions {
|
|
193
|
+
botTokenEnv?: string;
|
|
194
|
+
webhookSecretEnv?: string;
|
|
195
|
+
}
|
|
196
|
+
declare class TelegramAdapter implements MessagingAdapter {
|
|
197
|
+
readonly platform: "telegram";
|
|
198
|
+
readonly autoReply = true;
|
|
199
|
+
readonly hasSentInCurrentRequest = false;
|
|
200
|
+
private botToken;
|
|
201
|
+
private webhookSecret;
|
|
202
|
+
private botUsername;
|
|
203
|
+
private botId;
|
|
204
|
+
private readonly botTokenEnv;
|
|
205
|
+
private readonly webhookSecretEnv;
|
|
206
|
+
private handler;
|
|
207
|
+
private readonly sessionCounters;
|
|
208
|
+
private lastUpdateId;
|
|
209
|
+
constructor(options?: TelegramAdapterOptions);
|
|
210
|
+
initialize(): Promise<void>;
|
|
211
|
+
onMessage(handler: IncomingMessageHandler): void;
|
|
212
|
+
registerRoutes(router: RouteRegistrar): void;
|
|
213
|
+
sendReply(threadRef: ThreadRef, content: string, options?: {
|
|
214
|
+
files?: FileAttachment[];
|
|
215
|
+
}): Promise<void>;
|
|
216
|
+
indicateProcessing(threadRef: ThreadRef): Promise<() => Promise<void>>;
|
|
217
|
+
private handleRequest;
|
|
218
|
+
private sessionKey;
|
|
219
|
+
private extractFiles;
|
|
220
|
+
}
|
|
221
|
+
|
|
192
222
|
/** Extract the bare email address from a formatted string like `"Name <addr>"`. */
|
|
193
223
|
declare function extractEmailAddress(formatted: string): string;
|
|
194
224
|
/** Extract the display name from `"Name <addr>"`, or return `undefined`. */
|
|
@@ -241,4 +271,4 @@ declare function markdownToEmailHtml(text: string): string;
|
|
|
241
271
|
*/
|
|
242
272
|
declare function matchesSenderPattern(sender: string, patterns: string[] | undefined): boolean;
|
|
243
273
|
|
|
244
|
-
export { AgentBridge, type AgentBridgeOptions, type AgentRunner, type FileAttachment, type IncomingMessage, type IncomingMessageHandler, type MessagingAdapter, ResendAdapter, type ResendAdapterOptions, type RouteHandler, type RouteRegistrar, SlackAdapter, type SlackAdapterOptions, type ThreadRef, buildReplyHeaders, buildReplySubject, deriveRootMessageId, extractDisplayName, extractEmailAddress, markdownToEmailHtml, matchesSenderPattern, parseReferences, stripQuotedReply };
|
|
274
|
+
export { AgentBridge, type AgentBridgeOptions, type AgentRunner, type FileAttachment, type IncomingMessage, type IncomingMessageHandler, type MessagingAdapter, ResendAdapter, type ResendAdapterOptions, type RouteHandler, type RouteRegistrar, SlackAdapter, type SlackAdapterOptions, TelegramAdapter, type TelegramAdapterOptions, type ThreadRef, buildReplyHeaders, buildReplySubject, deriveRootMessageId, extractDisplayName, extractEmailAddress, markdownToEmailHtml, matchesSenderPattern, parseReferences, stripQuotedReply };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
// src/bridge.ts
|
|
2
|
-
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
var conversationIdFromThread = (platform, ref) => {
|
|
4
|
+
const key = `${platform}:${ref.channelId}:${ref.platformThreadId}`;
|
|
5
|
+
const hex = createHash("sha256").update(key).digest("hex").slice(0, 32);
|
|
6
|
+
return [
|
|
7
|
+
hex.slice(0, 8),
|
|
8
|
+
hex.slice(8, 12),
|
|
9
|
+
`4${hex.slice(13, 16)}`,
|
|
10
|
+
(parseInt(hex.slice(16, 18), 16) & 63 | 128).toString(16).padStart(2, "0") + hex.slice(18, 20),
|
|
11
|
+
hex.slice(20, 32)
|
|
12
|
+
].join("-");
|
|
13
|
+
};
|
|
3
14
|
var AgentBridge = class {
|
|
4
15
|
adapter;
|
|
5
16
|
runner;
|
|
@@ -30,9 +41,11 @@ var AgentBridge = class {
|
|
|
30
41
|
message.platform,
|
|
31
42
|
message.threadRef
|
|
32
43
|
);
|
|
33
|
-
const
|
|
44
|
+
const platformTag = `[${message.platform.charAt(0).toUpperCase()}${message.platform.slice(1)}]`;
|
|
45
|
+
const senderLabel = message.sender.name || message.sender.id;
|
|
46
|
+
const titleParts = [platformTag, senderLabel];
|
|
34
47
|
if (message.subject) titleParts.push(message.subject);
|
|
35
|
-
const title = titleParts.join("
|
|
48
|
+
const title = titleParts.join(" ") || `${message.platform} thread`;
|
|
36
49
|
const conversation = await this.runner.getOrCreateConversation(
|
|
37
50
|
conversationId,
|
|
38
51
|
{
|
|
@@ -319,7 +332,7 @@ var SlackAdapter = class {
|
|
|
319
332
|
import { createHmac as createHmac2 } from "crypto";
|
|
320
333
|
|
|
321
334
|
// src/adapters/email/utils.ts
|
|
322
|
-
import { createHash } from "crypto";
|
|
335
|
+
import { createHash as createHash2 } from "crypto";
|
|
323
336
|
var ADDR_RE = /<([^>]+)>/;
|
|
324
337
|
function extractEmailAddress(formatted) {
|
|
325
338
|
const match = ADDR_RE.exec(formatted);
|
|
@@ -350,7 +363,7 @@ function deriveRootMessageId(references, currentMessageId, fallback) {
|
|
|
350
363
|
if (references.length > 0) return references[0];
|
|
351
364
|
if (fallback) {
|
|
352
365
|
const normalised = normaliseSubject(fallback.subject) + "\0" + fallback.sender.toLowerCase();
|
|
353
|
-
const hash =
|
|
366
|
+
const hash = createHash2("sha256").update(normalised).digest("hex").slice(0, 16);
|
|
354
367
|
return `<fallback:${hash}>`;
|
|
355
368
|
}
|
|
356
369
|
return currentMessageId;
|
|
@@ -907,10 +920,445 @@ var ResendAdapter = class {
|
|
|
907
920
|
return results;
|
|
908
921
|
}
|
|
909
922
|
};
|
|
923
|
+
|
|
924
|
+
// src/adapters/telegram/verify.ts
|
|
925
|
+
import { timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
926
|
+
var verifyTelegramSecret = (expected, received) => {
|
|
927
|
+
if (!received) return false;
|
|
928
|
+
if (expected.length !== received.length) return false;
|
|
929
|
+
return timingSafeEqual2(Buffer.from(expected), Buffer.from(received));
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
// src/adapters/telegram/utils.ts
|
|
933
|
+
var TELEGRAM_API = "https://api.telegram.org";
|
|
934
|
+
var TELEGRAM_MAX_MESSAGE_LENGTH = 4096;
|
|
935
|
+
var telegramFetch = async (token, method, body) => {
|
|
936
|
+
const res = await fetch(`${TELEGRAM_API}/bot${token}/${method}`, {
|
|
937
|
+
method: "POST",
|
|
938
|
+
headers: { "content-type": "application/json" },
|
|
939
|
+
body: JSON.stringify(body)
|
|
940
|
+
});
|
|
941
|
+
return await res.json();
|
|
942
|
+
};
|
|
943
|
+
var telegramUpload = async (token, method, formData) => {
|
|
944
|
+
const res = await fetch(`${TELEGRAM_API}/bot${token}/${method}`, {
|
|
945
|
+
method: "POST",
|
|
946
|
+
body: formData
|
|
947
|
+
});
|
|
948
|
+
return await res.json();
|
|
949
|
+
};
|
|
950
|
+
var getMe = async (token) => {
|
|
951
|
+
const result = await telegramFetch(token, "getMe", {});
|
|
952
|
+
if (!result.ok) {
|
|
953
|
+
throw new Error(`Telegram getMe failed: ${result.description}`);
|
|
954
|
+
}
|
|
955
|
+
const user = result.result;
|
|
956
|
+
return { id: user.id, username: user.username ?? "" };
|
|
957
|
+
};
|
|
958
|
+
var getFile = async (token, fileId) => {
|
|
959
|
+
const result = await telegramFetch(token, "getFile", { file_id: fileId });
|
|
960
|
+
if (!result.ok) {
|
|
961
|
+
throw new Error(`Telegram getFile failed: ${result.description}`);
|
|
962
|
+
}
|
|
963
|
+
const file = result.result;
|
|
964
|
+
if (!file.file_path) {
|
|
965
|
+
throw new Error("Telegram getFile: no file_path returned");
|
|
966
|
+
}
|
|
967
|
+
return file.file_path;
|
|
968
|
+
};
|
|
969
|
+
var EXTENSION_MEDIA_TYPES = {
|
|
970
|
+
jpg: "image/jpeg",
|
|
971
|
+
jpeg: "image/jpeg",
|
|
972
|
+
png: "image/png",
|
|
973
|
+
gif: "image/gif",
|
|
974
|
+
webp: "image/webp",
|
|
975
|
+
pdf: "application/pdf",
|
|
976
|
+
mp4: "video/mp4",
|
|
977
|
+
ogg: "audio/ogg",
|
|
978
|
+
mp3: "audio/mpeg"
|
|
979
|
+
};
|
|
980
|
+
var inferMediaType = (filePath, header) => {
|
|
981
|
+
if (header && header !== "application/octet-stream") return header;
|
|
982
|
+
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
983
|
+
if (ext && EXTENSION_MEDIA_TYPES[ext]) return EXTENSION_MEDIA_TYPES[ext];
|
|
984
|
+
return header ?? "application/octet-stream";
|
|
985
|
+
};
|
|
986
|
+
var downloadFile = async (token, filePath) => {
|
|
987
|
+
const url = `${TELEGRAM_API}/file/bot${token}/${filePath}`;
|
|
988
|
+
const res = await fetch(url);
|
|
989
|
+
if (!res.ok) {
|
|
990
|
+
throw new Error(`Telegram file download failed: ${res.status}`);
|
|
991
|
+
}
|
|
992
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
993
|
+
const mediaType = inferMediaType(filePath, res.headers.get("content-type"));
|
|
994
|
+
return { data: buffer.toString("base64"), mediaType };
|
|
995
|
+
};
|
|
996
|
+
var sendMessage = async (token, chatId, text, opts) => {
|
|
997
|
+
const body = { chat_id: chatId, text };
|
|
998
|
+
if (opts?.reply_to_message_id) {
|
|
999
|
+
body.reply_parameters = {
|
|
1000
|
+
message_id: opts.reply_to_message_id,
|
|
1001
|
+
allow_sending_without_reply: true
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
if (opts?.message_thread_id) {
|
|
1005
|
+
body.message_thread_id = opts.message_thread_id;
|
|
1006
|
+
}
|
|
1007
|
+
const result = await telegramFetch(token, "sendMessage", body);
|
|
1008
|
+
if (!result.ok) {
|
|
1009
|
+
throw new Error(`Telegram sendMessage failed: ${result.description}`);
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
var sendPhoto = async (token, chatId, photoData, opts) => {
|
|
1013
|
+
const formData = new FormData();
|
|
1014
|
+
formData.append("chat_id", String(chatId));
|
|
1015
|
+
const blob = new Blob([Buffer.from(photoData, "base64")]);
|
|
1016
|
+
formData.append("photo", blob, opts?.filename ?? "photo.jpg");
|
|
1017
|
+
if (opts?.caption) formData.append("caption", opts.caption);
|
|
1018
|
+
if (opts?.reply_to_message_id) {
|
|
1019
|
+
formData.append(
|
|
1020
|
+
"reply_parameters",
|
|
1021
|
+
JSON.stringify({
|
|
1022
|
+
message_id: opts.reply_to_message_id,
|
|
1023
|
+
allow_sending_without_reply: true
|
|
1024
|
+
})
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
if (opts?.message_thread_id) {
|
|
1028
|
+
formData.append("message_thread_id", String(opts.message_thread_id));
|
|
1029
|
+
}
|
|
1030
|
+
const result = await telegramUpload(token, "sendPhoto", formData);
|
|
1031
|
+
if (!result.ok) {
|
|
1032
|
+
throw new Error(`Telegram sendPhoto failed: ${result.description}`);
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
var sendDocument = async (token, chatId, docData, opts) => {
|
|
1036
|
+
const formData = new FormData();
|
|
1037
|
+
formData.append("chat_id", String(chatId));
|
|
1038
|
+
const blob = new Blob([Buffer.from(docData, "base64")], {
|
|
1039
|
+
type: opts?.mediaType
|
|
1040
|
+
});
|
|
1041
|
+
formData.append("document", blob, opts?.filename ?? "file");
|
|
1042
|
+
if (opts?.caption) formData.append("caption", opts.caption);
|
|
1043
|
+
if (opts?.reply_to_message_id) {
|
|
1044
|
+
formData.append(
|
|
1045
|
+
"reply_parameters",
|
|
1046
|
+
JSON.stringify({
|
|
1047
|
+
message_id: opts.reply_to_message_id,
|
|
1048
|
+
allow_sending_without_reply: true
|
|
1049
|
+
})
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
if (opts?.message_thread_id) {
|
|
1053
|
+
formData.append("message_thread_id", String(opts.message_thread_id));
|
|
1054
|
+
}
|
|
1055
|
+
const result = await telegramUpload(token, "sendDocument", formData);
|
|
1056
|
+
if (!result.ok) {
|
|
1057
|
+
throw new Error(`Telegram sendDocument failed: ${result.description}`);
|
|
1058
|
+
}
|
|
1059
|
+
};
|
|
1060
|
+
var sendChatAction = async (token, chatId, action) => {
|
|
1061
|
+
await telegramFetch(token, "sendChatAction", {
|
|
1062
|
+
chat_id: chatId,
|
|
1063
|
+
action
|
|
1064
|
+
});
|
|
1065
|
+
};
|
|
1066
|
+
var splitMessage2 = (text) => {
|
|
1067
|
+
if (text.length <= TELEGRAM_MAX_MESSAGE_LENGTH) return [text];
|
|
1068
|
+
const chunks = [];
|
|
1069
|
+
let remaining = text;
|
|
1070
|
+
while (remaining.length > 0) {
|
|
1071
|
+
if (remaining.length <= TELEGRAM_MAX_MESSAGE_LENGTH) {
|
|
1072
|
+
chunks.push(remaining);
|
|
1073
|
+
break;
|
|
1074
|
+
}
|
|
1075
|
+
let cutPoint = remaining.lastIndexOf(
|
|
1076
|
+
"\n",
|
|
1077
|
+
TELEGRAM_MAX_MESSAGE_LENGTH
|
|
1078
|
+
);
|
|
1079
|
+
if (cutPoint <= 0) {
|
|
1080
|
+
cutPoint = TELEGRAM_MAX_MESSAGE_LENGTH;
|
|
1081
|
+
}
|
|
1082
|
+
chunks.push(remaining.slice(0, cutPoint));
|
|
1083
|
+
remaining = remaining.slice(cutPoint).replace(/^\n/, "");
|
|
1084
|
+
}
|
|
1085
|
+
return chunks;
|
|
1086
|
+
};
|
|
1087
|
+
var isBotMentioned = (entities, botUsername, botId, text) => {
|
|
1088
|
+
if (!entities || entities.length === 0) return false;
|
|
1089
|
+
const lower = botUsername.toLowerCase();
|
|
1090
|
+
for (const entity of entities) {
|
|
1091
|
+
if (entity.type === "mention") {
|
|
1092
|
+
const mentioned = text.slice(entity.offset, entity.offset + entity.length);
|
|
1093
|
+
if (mentioned.toLowerCase() === `@${lower}`) return true;
|
|
1094
|
+
}
|
|
1095
|
+
if (entity.type === "text_mention" && entity.user?.id === botId) {
|
|
1096
|
+
return true;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
return false;
|
|
1100
|
+
};
|
|
1101
|
+
var stripMention2 = (text, entities, botUsername, botId) => {
|
|
1102
|
+
if (!entities || entities.length === 0) return text.trim();
|
|
1103
|
+
const lower = botUsername.toLowerCase();
|
|
1104
|
+
for (const entity of entities) {
|
|
1105
|
+
let match = false;
|
|
1106
|
+
if (entity.type === "mention") {
|
|
1107
|
+
const mentioned = text.slice(entity.offset, entity.offset + entity.length);
|
|
1108
|
+
if (mentioned.toLowerCase() === `@${lower}`) match = true;
|
|
1109
|
+
}
|
|
1110
|
+
if (entity.type === "text_mention" && entity.user?.id === botId) {
|
|
1111
|
+
match = true;
|
|
1112
|
+
}
|
|
1113
|
+
if (match) {
|
|
1114
|
+
return (text.slice(0, entity.offset) + text.slice(entity.offset + entity.length)).trim();
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
return text.trim();
|
|
1118
|
+
};
|
|
1119
|
+
|
|
1120
|
+
// src/adapters/telegram/index.ts
|
|
1121
|
+
var TYPING_INTERVAL_MS = 4e3;
|
|
1122
|
+
var NEW_COMMAND_RE = /^\/new(?:@(\S+))?$/i;
|
|
1123
|
+
var collectBody3 = (req) => new Promise((resolve, reject) => {
|
|
1124
|
+
const chunks = [];
|
|
1125
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
1126
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
1127
|
+
req.on("error", reject);
|
|
1128
|
+
});
|
|
1129
|
+
var TelegramAdapter = class {
|
|
1130
|
+
platform = "telegram";
|
|
1131
|
+
autoReply = true;
|
|
1132
|
+
hasSentInCurrentRequest = false;
|
|
1133
|
+
botToken = "";
|
|
1134
|
+
webhookSecret = "";
|
|
1135
|
+
botUsername = "";
|
|
1136
|
+
botId = 0;
|
|
1137
|
+
botTokenEnv;
|
|
1138
|
+
webhookSecretEnv;
|
|
1139
|
+
handler;
|
|
1140
|
+
sessionCounters = /* @__PURE__ */ new Map();
|
|
1141
|
+
lastUpdateId = 0;
|
|
1142
|
+
constructor(options = {}) {
|
|
1143
|
+
this.botTokenEnv = options.botTokenEnv ?? "TELEGRAM_BOT_TOKEN";
|
|
1144
|
+
this.webhookSecretEnv = options.webhookSecretEnv ?? "TELEGRAM_WEBHOOK_SECRET";
|
|
1145
|
+
}
|
|
1146
|
+
// -----------------------------------------------------------------------
|
|
1147
|
+
// MessagingAdapter implementation
|
|
1148
|
+
// -----------------------------------------------------------------------
|
|
1149
|
+
async initialize() {
|
|
1150
|
+
this.botToken = process.env[this.botTokenEnv] ?? "";
|
|
1151
|
+
this.webhookSecret = process.env[this.webhookSecretEnv] ?? "";
|
|
1152
|
+
if (!this.botToken) {
|
|
1153
|
+
throw new Error(
|
|
1154
|
+
`Telegram messaging: ${this.botTokenEnv} environment variable is not set`
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
const me = await getMe(this.botToken);
|
|
1158
|
+
this.botUsername = me.username;
|
|
1159
|
+
this.botId = me.id;
|
|
1160
|
+
}
|
|
1161
|
+
onMessage(handler) {
|
|
1162
|
+
this.handler = handler;
|
|
1163
|
+
}
|
|
1164
|
+
registerRoutes(router) {
|
|
1165
|
+
router(
|
|
1166
|
+
"POST",
|
|
1167
|
+
"/api/messaging/telegram",
|
|
1168
|
+
(req, res) => this.handleRequest(req, res)
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
async sendReply(threadRef, content, options) {
|
|
1172
|
+
const chatId = threadRef.channelId;
|
|
1173
|
+
const replyTo = threadRef.messageId ? Number(threadRef.messageId) : void 0;
|
|
1174
|
+
if (content) {
|
|
1175
|
+
const chunks = splitMessage2(content);
|
|
1176
|
+
for (const chunk of chunks) {
|
|
1177
|
+
await sendMessage(this.botToken, chatId, chunk, {
|
|
1178
|
+
reply_to_message_id: replyTo
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
if (options?.files) {
|
|
1183
|
+
for (const file of options.files) {
|
|
1184
|
+
if (file.mediaType.startsWith("image/")) {
|
|
1185
|
+
await sendPhoto(this.botToken, chatId, file.data, {
|
|
1186
|
+
reply_to_message_id: replyTo,
|
|
1187
|
+
filename: file.filename
|
|
1188
|
+
});
|
|
1189
|
+
} else {
|
|
1190
|
+
await sendDocument(this.botToken, chatId, file.data, {
|
|
1191
|
+
reply_to_message_id: replyTo,
|
|
1192
|
+
filename: file.filename,
|
|
1193
|
+
mediaType: file.mediaType
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
async indicateProcessing(threadRef) {
|
|
1200
|
+
const chatId = threadRef.channelId;
|
|
1201
|
+
await sendChatAction(this.botToken, chatId, "typing");
|
|
1202
|
+
const interval = setInterval(() => {
|
|
1203
|
+
void sendChatAction(this.botToken, chatId, "typing").catch(() => {
|
|
1204
|
+
});
|
|
1205
|
+
}, TYPING_INTERVAL_MS);
|
|
1206
|
+
return async () => {
|
|
1207
|
+
clearInterval(interval);
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
// -----------------------------------------------------------------------
|
|
1211
|
+
// HTTP request handling
|
|
1212
|
+
// -----------------------------------------------------------------------
|
|
1213
|
+
async handleRequest(req, res) {
|
|
1214
|
+
const rawBody = await collectBody3(req);
|
|
1215
|
+
if (this.webhookSecret) {
|
|
1216
|
+
const headerSecret = req.headers["x-telegram-bot-api-secret-token"];
|
|
1217
|
+
if (!verifyTelegramSecret(this.webhookSecret, headerSecret)) {
|
|
1218
|
+
res.writeHead(401);
|
|
1219
|
+
res.end("Invalid secret");
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
let payload;
|
|
1224
|
+
try {
|
|
1225
|
+
payload = JSON.parse(rawBody);
|
|
1226
|
+
} catch {
|
|
1227
|
+
res.writeHead(400);
|
|
1228
|
+
res.end("Invalid JSON");
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
if (payload.update_id <= this.lastUpdateId) {
|
|
1232
|
+
res.writeHead(200);
|
|
1233
|
+
res.end();
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
this.lastUpdateId = payload.update_id;
|
|
1237
|
+
const message = payload.message;
|
|
1238
|
+
if (!message) {
|
|
1239
|
+
res.writeHead(200);
|
|
1240
|
+
res.end();
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
const text = message.text ?? message.caption ?? "";
|
|
1244
|
+
const hasFiles = !!(message.photo || message.document);
|
|
1245
|
+
if (!text && !hasFiles) {
|
|
1246
|
+
res.writeHead(200);
|
|
1247
|
+
res.end();
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
const chatId = String(message.chat.id);
|
|
1251
|
+
const chatType = message.chat.type;
|
|
1252
|
+
const isGroup = chatType === "group" || chatType === "supergroup";
|
|
1253
|
+
const entities = message.entities ?? message.caption_entities;
|
|
1254
|
+
const newMatch = text.match(NEW_COMMAND_RE);
|
|
1255
|
+
if (newMatch) {
|
|
1256
|
+
const suffix = newMatch[1];
|
|
1257
|
+
if (isGroup && suffix && suffix.toLowerCase() !== this.botUsername.toLowerCase()) {
|
|
1258
|
+
res.writeHead(200);
|
|
1259
|
+
res.end();
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
const key2 = this.sessionKey(message);
|
|
1263
|
+
const current = this.sessionCounters.get(key2) ?? 0;
|
|
1264
|
+
this.sessionCounters.set(key2, current + 1);
|
|
1265
|
+
res.writeHead(200);
|
|
1266
|
+
res.end();
|
|
1267
|
+
await sendMessage(
|
|
1268
|
+
this.botToken,
|
|
1269
|
+
chatId,
|
|
1270
|
+
"Conversation reset. Send a new message to start fresh.",
|
|
1271
|
+
{
|
|
1272
|
+
reply_to_message_id: message.message_id,
|
|
1273
|
+
message_thread_id: message.message_thread_id
|
|
1274
|
+
}
|
|
1275
|
+
);
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
if (isGroup) {
|
|
1279
|
+
if (!isBotMentioned(entities, this.botUsername, this.botId, text)) {
|
|
1280
|
+
res.writeHead(200);
|
|
1281
|
+
res.end();
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
const cleanText = isGroup ? stripMention2(text, entities, this.botUsername, this.botId) : text;
|
|
1286
|
+
if (!cleanText && !hasFiles) {
|
|
1287
|
+
res.writeHead(200);
|
|
1288
|
+
res.end();
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
res.writeHead(200);
|
|
1292
|
+
res.end();
|
|
1293
|
+
if (!this.handler) return;
|
|
1294
|
+
const files = await this.extractFiles(message);
|
|
1295
|
+
const key = this.sessionKey(message);
|
|
1296
|
+
const session = this.sessionCounters.get(key) ?? 0;
|
|
1297
|
+
const topicId = message.message_thread_id;
|
|
1298
|
+
const platformThreadId = topicId ? `${chatId}:${topicId}:${session}` : `${chatId}:${session}`;
|
|
1299
|
+
const userId = String(message.from?.id ?? "unknown");
|
|
1300
|
+
const userName = [message.from?.first_name, message.from?.last_name].filter(Boolean).join(" ") || void 0;
|
|
1301
|
+
const ponchoMessage = {
|
|
1302
|
+
text: cleanText,
|
|
1303
|
+
files: files.length > 0 ? files : void 0,
|
|
1304
|
+
threadRef: {
|
|
1305
|
+
platformThreadId,
|
|
1306
|
+
channelId: chatId,
|
|
1307
|
+
messageId: String(message.message_id)
|
|
1308
|
+
},
|
|
1309
|
+
sender: { id: userId, name: userName },
|
|
1310
|
+
platform: "telegram",
|
|
1311
|
+
raw: message
|
|
1312
|
+
};
|
|
1313
|
+
void this.handler(ponchoMessage).catch((err) => {
|
|
1314
|
+
console.error(
|
|
1315
|
+
"[telegram-adapter] unhandled message handler error",
|
|
1316
|
+
err
|
|
1317
|
+
);
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
// -----------------------------------------------------------------------
|
|
1321
|
+
// Helpers
|
|
1322
|
+
// -----------------------------------------------------------------------
|
|
1323
|
+
sessionKey(message) {
|
|
1324
|
+
const chatId = String(message.chat.id);
|
|
1325
|
+
return message.message_thread_id ? `${chatId}:${message.message_thread_id}` : chatId;
|
|
1326
|
+
}
|
|
1327
|
+
async extractFiles(message) {
|
|
1328
|
+
const files = [];
|
|
1329
|
+
try {
|
|
1330
|
+
if (message.photo && message.photo.length > 0) {
|
|
1331
|
+
const largest = message.photo[message.photo.length - 1];
|
|
1332
|
+
const filePath = await getFile(this.botToken, largest.file_id);
|
|
1333
|
+
const { data } = await downloadFile(this.botToken, filePath);
|
|
1334
|
+
files.push({ data, mediaType: "image/jpeg", filename: "photo.jpg" });
|
|
1335
|
+
}
|
|
1336
|
+
if (message.document) {
|
|
1337
|
+
const filePath = await getFile(
|
|
1338
|
+
this.botToken,
|
|
1339
|
+
message.document.file_id
|
|
1340
|
+
);
|
|
1341
|
+
const downloaded = await downloadFile(this.botToken, filePath);
|
|
1342
|
+
files.push({
|
|
1343
|
+
data: downloaded.data,
|
|
1344
|
+
mediaType: message.document.mime_type ?? downloaded.mediaType,
|
|
1345
|
+
filename: message.document.file_name
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
} catch (err) {
|
|
1349
|
+
console.warn(
|
|
1350
|
+
"[telegram-adapter] failed to download file:",
|
|
1351
|
+
err instanceof Error ? err.message : err
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
return files;
|
|
1355
|
+
}
|
|
1356
|
+
};
|
|
910
1357
|
export {
|
|
911
1358
|
AgentBridge,
|
|
912
1359
|
ResendAdapter,
|
|
913
1360
|
SlackAdapter,
|
|
1361
|
+
TelegramAdapter,
|
|
914
1362
|
buildReplyHeaders,
|
|
915
1363
|
buildReplySubject,
|
|
916
1364
|
deriveRootMessageId,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poncho-ai/messaging",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Messaging platform adapters for Poncho agents (Slack, Telegram, etc.)",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@poncho-ai/sdk": "1.4.
|
|
23
|
+
"@poncho-ai/sdk": "1.4.1"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
26
|
"resend": ">=4.0.0"
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"agent",
|
|
41
41
|
"messaging",
|
|
42
42
|
"slack",
|
|
43
|
+
"telegram",
|
|
43
44
|
"email",
|
|
44
45
|
"resend"
|
|
45
46
|
],
|