@mono-agent/telegram-adapter 0.3.0 → 0.4.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/README.md +13 -8
- package/dist/adapter.d.ts +128 -2
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +382 -10
- package/dist/adapter.js.map +1 -1
- package/dist/bot.d.ts +57 -5
- package/dist/bot.d.ts.map +1 -1
- package/dist/bot.js +534 -24
- package/dist/bot.js.map +1 -1
- package/dist/grammy-client.d.ts +1 -0
- package/dist/grammy-client.d.ts.map +1 -1
- package/dist/grammy-client.js +17 -1
- package/dist/grammy-client.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/message-stream.d.ts +32 -52
- package/dist/message-stream.d.ts.map +1 -1
- package/dist/message-stream.js +163 -404
- package/dist/message-stream.js.map +1 -1
- package/dist/start.d.ts +7 -1
- package/dist/start.d.ts.map +1 -1
- package/dist/start.js +1 -0
- package/dist/start.js.map +1 -1
- package/dist/types.d.ts +45 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -10,6 +10,10 @@ Telegram communication adapter for agent hosts. It provides a Bot API client, lo
|
|
|
10
10
|
|
|
11
11
|
The adapter is opt-in: `telegram.enabled` / `MONO_AGENT_TELEGRAM_ENABLED` defaults to `false`. While disabled the loader skips credential validation and the channel reports `disabled` rather than `waiting_for_config`. Set `enabled: true` to turn it on; a missing bot token or allowlist then surfaces as a real `waiting_for_config` reason.
|
|
12
12
|
|
|
13
|
+
Inbound Telegram document, photo, audio, video, and voice messages are downloaded (subject to the MIME allowlist and ~20 MB cap) and delivered to the responder as transport-agnostic `AgentAttachment` bytes on the shared `AgentRequestBase.attachments` contract (decoded `mimeType` + base64 `data` + `name`). Captions remain the request text; media-only messages still get a concise text summary so existing text-only responder paths can reason about what arrived. The original Telegram file metadata (file id, sizes, kind) is preserved under `metadata.telegram.attachments`.
|
|
14
|
+
|
|
15
|
+
Downloads are gated by the configured allowlist and size cap, and a download failure skips the attachment without failing the run. Whether the model actually consumes the images/documents depends on the host runtime's vision/document support — the adapter forwards bytes but does not itself guarantee model-level understanding.
|
|
16
|
+
|
|
13
17
|
## Install / Usage
|
|
14
18
|
|
|
15
19
|
```bash
|
|
@@ -18,10 +22,10 @@ pnpm --filter @mono-agent/telegram-adapter run build
|
|
|
18
22
|
|
|
19
23
|
```ts
|
|
20
24
|
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
TelegramLongPoller,
|
|
25
|
+
createTelegramBot,
|
|
26
|
+
createGrammyTelegramApi,
|
|
24
27
|
loadTelegramAdapterConfig,
|
|
28
|
+
startTelegramAdapter,
|
|
25
29
|
telegramFieldGroup,
|
|
26
30
|
} from "@mono-agent/telegram-adapter";
|
|
27
31
|
```
|
|
@@ -30,16 +34,17 @@ Load adapter settings separately from core config, then pass a structural `Agent
|
|
|
30
34
|
|
|
31
35
|
## Public API
|
|
32
36
|
|
|
33
|
-
- `
|
|
34
|
-
- `
|
|
35
|
-
- `
|
|
36
|
-
- `TelegramMessageStream`, `splitTelegramText`
|
|
37
|
+
- `createTelegramBot`, `startTelegramAdapter`
|
|
38
|
+
- `createGrammyTelegramApi`, `TelegramApiError`
|
|
39
|
+
- `TelegramMessageStream`, `classifyTelegramError`
|
|
37
40
|
- `loadTelegramAdapterConfig`, `redactTelegramAdapterConfig`, `telegramFieldGroup`
|
|
41
|
+
- `downloadTelegramAttachments` (+ `DownloadTelegramAttachmentsOptions`, `TelegramFileDownloader`): the inbound-bytes flow that maps `TelegramAttachment` metadata to `AgentAttachment` bytes on `request.attachments`
|
|
42
|
+
- `TelegramAttachment` and related Telegram-owned inbound attachment metadata types (preserved under `metadata.telegram.attachments`)
|
|
38
43
|
- Telegram Bot API, request/response, and config types
|
|
39
44
|
|
|
40
45
|
## Dependency Boundary
|
|
41
46
|
|
|
42
|
-
This adapter depends only on shared contracts and settings primitives inside the workspace. It does not depend on the harness,
|
|
47
|
+
This adapter depends only on shared contracts and settings primitives inside the workspace. It does not depend on the harness, core config, memory, runtime package, or other adapters. Hosts compose those pieces outside the adapter.
|
|
43
48
|
|
|
44
49
|
## What This Package Does Not Own
|
|
45
50
|
|
package/dist/adapter.d.ts
CHANGED
|
@@ -1,6 +1,55 @@
|
|
|
1
|
-
import type { AgentRequestBase, AgentResponder as SharedAgentResponder, AgentResponse } from "@mono-agent/agent-contracts";
|
|
1
|
+
import type { AgentAttachment, AgentRequestBase, AgentResponder as SharedAgentResponder, AgentResponse } from "@mono-agent/agent-contracts";
|
|
2
2
|
import { TelegramMessageStream, type AgentMessageStream, type TelegramMessageStreamLogger } from "./message-stream.js";
|
|
3
3
|
import type { TelegramChatId, TelegramMessage, TelegramUpdate } from "./types.js";
|
|
4
|
+
export type TelegramAttachmentKind = "document" | "photo" | "audio" | "video" | "voice";
|
|
5
|
+
export interface TelegramAttachmentBase {
|
|
6
|
+
kind: TelegramAttachmentKind;
|
|
7
|
+
fileId: string;
|
|
8
|
+
fileUniqueId: string;
|
|
9
|
+
fileSize?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface TelegramDocumentAttachment extends TelegramAttachmentBase {
|
|
12
|
+
kind: "document";
|
|
13
|
+
fileName?: string;
|
|
14
|
+
mimeType?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface TelegramPhotoAttachmentSize {
|
|
17
|
+
fileId: string;
|
|
18
|
+
fileUniqueId: string;
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
fileSize?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface TelegramPhotoAttachment extends TelegramAttachmentBase {
|
|
24
|
+
kind: "photo";
|
|
25
|
+
width: number;
|
|
26
|
+
height: number;
|
|
27
|
+
sizes: readonly TelegramPhotoAttachmentSize[];
|
|
28
|
+
}
|
|
29
|
+
export interface TelegramAudioAttachment extends TelegramAttachmentBase {
|
|
30
|
+
kind: "audio";
|
|
31
|
+
duration: number;
|
|
32
|
+
fileName?: string;
|
|
33
|
+
mimeType?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface TelegramVideoAttachment extends TelegramAttachmentBase {
|
|
36
|
+
kind: "video";
|
|
37
|
+
duration: number;
|
|
38
|
+
width: number;
|
|
39
|
+
height: number;
|
|
40
|
+
fileName?: string;
|
|
41
|
+
mimeType?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface TelegramVoiceAttachment extends TelegramAttachmentBase {
|
|
44
|
+
kind: "voice";
|
|
45
|
+
duration: number;
|
|
46
|
+
mimeType?: string;
|
|
47
|
+
}
|
|
48
|
+
export type TelegramAttachment = TelegramDocumentAttachment | TelegramPhotoAttachment | TelegramAudioAttachment | TelegramVideoAttachment | TelegramVoiceAttachment;
|
|
49
|
+
export interface TelegramAgentMessageInput {
|
|
50
|
+
text: string;
|
|
51
|
+
attachments: readonly TelegramAttachment[];
|
|
52
|
+
}
|
|
4
53
|
export interface AgentRequest extends AgentRequestBase {
|
|
5
54
|
conversationId: string;
|
|
6
55
|
chatId: TelegramChatId;
|
|
@@ -9,6 +58,19 @@ export interface AgentRequest extends AgentRequestBase {
|
|
|
9
58
|
userId?: number;
|
|
10
59
|
username?: string;
|
|
11
60
|
text: string;
|
|
61
|
+
/**
|
|
62
|
+
* Downloaded attachment bytes, ready for a vision/document-aware runtime, in
|
|
63
|
+
* the transport-agnostic {@link AgentAttachment} shape (base64 data + mime +
|
|
64
|
+
* name).
|
|
65
|
+
*
|
|
66
|
+
* BREAKING (intentional, unified contract): on earlier versions this field
|
|
67
|
+
* held Telegram-specific `TelegramAttachment[]` metadata. It is now
|
|
68
|
+
* `AgentAttachment[]` (matching Slack and the OpenAI-compatible channel). The
|
|
69
|
+
* original Telegram file metadata (fileId, sizes, kind, …) is preserved under
|
|
70
|
+
* `metadata.telegram.attachments` — custom responders that filtered by
|
|
71
|
+
* `fileId`/`sizes`/`kind` should read it from there.
|
|
72
|
+
*/
|
|
73
|
+
attachments?: readonly AgentAttachment[];
|
|
12
74
|
abortSignal: AbortSignal;
|
|
13
75
|
metadata: {
|
|
14
76
|
telegram: TelegramRequestMetadata;
|
|
@@ -27,6 +89,7 @@ export interface TelegramRequestMetadata {
|
|
|
27
89
|
id: number;
|
|
28
90
|
date?: number;
|
|
29
91
|
};
|
|
92
|
+
attachments?: readonly TelegramAttachment[];
|
|
30
93
|
from?: {
|
|
31
94
|
id: number;
|
|
32
95
|
isBot?: boolean;
|
|
@@ -60,7 +123,13 @@ export interface TelegramAdapterStreamOptions {
|
|
|
60
123
|
retryCapMs?: number;
|
|
61
124
|
retryBaseDelayMs?: number;
|
|
62
125
|
showThoughts?: boolean;
|
|
126
|
+
showHints?: boolean;
|
|
63
127
|
formatMarkdown?: boolean;
|
|
128
|
+
/**
|
|
129
|
+
* Deliver only the final answer with a "typing…" indicator while working,
|
|
130
|
+
* instead of streaming interim edits. Defaults to true for the Telegram bot.
|
|
131
|
+
*/
|
|
132
|
+
finalOnly?: boolean;
|
|
64
133
|
}
|
|
65
134
|
export interface TelegramAdapterLogger extends TelegramMessageStreamLogger {
|
|
66
135
|
info?(message: string, metadata?: Record<string, unknown>): void;
|
|
@@ -71,8 +140,65 @@ export declare const DEFAULT_MESSAGES: Required<TelegramAdapterMessages>;
|
|
|
71
140
|
* Build the responder-facing {@link AgentRequest} from a Telegram update. The
|
|
72
141
|
* grammY message handler passes `ctx.update` and `ctx.message`, which are
|
|
73
142
|
* structurally compatible with the wire types this reads.
|
|
143
|
+
*
|
|
144
|
+
* `resolvedAttachments` are the downloaded {@link AgentAttachment} bytes (when
|
|
145
|
+
* available) that populate `request.attachments`; the original Telegram file
|
|
146
|
+
* metadata is always preserved under `metadata.telegram.attachments`.
|
|
147
|
+
*/
|
|
148
|
+
export declare function buildAgentRequest(update: TelegramUpdate, message: TelegramMessage, input: TelegramAgentMessageInput, abortSignal: AbortSignal, resolvedAttachments?: readonly AgentAttachment[]): AgentRequest;
|
|
149
|
+
export declare function normalizeTelegramMessageInput(message: TelegramMessage): TelegramAgentMessageInput | undefined;
|
|
150
|
+
/**
|
|
151
|
+
* Merge a Telegram media-group (album) into a single input: Telegram delivers an
|
|
152
|
+
* album of N photos/videos as N separate messages sharing one `media_group_id`,
|
|
153
|
+
* with the caption on only one of them. We concatenate every message's
|
|
154
|
+
* attachments and take the single caption (first non-empty), so the agent sees
|
|
155
|
+
* all photos as one request instead of N single-attachment turns.
|
|
156
|
+
*/
|
|
157
|
+
export declare function mergeTelegramMessageInputs(messages: readonly TelegramMessage[]): TelegramAgentMessageInput | undefined;
|
|
158
|
+
/** Telegram's hard cap for bot file downloads is 20 MB. */
|
|
159
|
+
export declare const DEFAULT_ATTACHMENT_MAX_BYTES: number;
|
|
160
|
+
/**
|
|
161
|
+
* MIME types the adapter will download and inline. Images flow to vision-capable
|
|
162
|
+
* runtimes; documents/text are saved to disk (and decoded inline for text/*).
|
|
163
|
+
*/
|
|
164
|
+
export declare const DEFAULT_ATTACHMENT_MIME_ALLOWLIST: readonly string[];
|
|
165
|
+
/**
|
|
166
|
+
* Minimal seam over the Telegram Bot API needed to fetch attachment bytes:
|
|
167
|
+
* resolve a `file_id` to a `file_path` (getFile) then download it from the file
|
|
168
|
+
* URL. Both calls honor the request `abortSignal`.
|
|
169
|
+
*/
|
|
170
|
+
export interface TelegramFileDownloader {
|
|
171
|
+
/** Resolve a `file_id` to a downloadable `file_path` (Bot API `getFile`). */
|
|
172
|
+
resolveFilePath(fileId: string, signal: AbortSignal): Promise<string | undefined>;
|
|
173
|
+
/**
|
|
174
|
+
* Download the file at `file_path` (GET on the file URL). `maxBytes`, when
|
|
175
|
+
* provided, lets the downloader abort an oversized transfer mid-stream instead
|
|
176
|
+
* of buffering the whole body first; the caller still re-checks the cap as a
|
|
177
|
+
* backstop, so a custom downloader that ignores `maxBytes` stays bounded.
|
|
178
|
+
*/
|
|
179
|
+
download(filePath: string, signal: AbortSignal, maxBytes?: number): Promise<Uint8Array>;
|
|
180
|
+
}
|
|
181
|
+
export interface DownloadTelegramAttachmentsOptions {
|
|
182
|
+
/** Skip files larger than this many decoded bytes. Default ~20 MB. */
|
|
183
|
+
readonly maxBytes?: number;
|
|
184
|
+
/** Only download files whose MIME type is allowed. Defaults to images + common docs/text. */
|
|
185
|
+
readonly mimeAllowlist?: readonly string[];
|
|
186
|
+
/**
|
|
187
|
+
* Per-file download timeout (ms) for the default downloader, composed with the
|
|
188
|
+
* run abort signal. Defaults to 30000. Only consulted by the built-in
|
|
189
|
+
* downloader; custom downloaders manage their own timeouts.
|
|
190
|
+
*/
|
|
191
|
+
readonly downloadTimeoutMs?: number;
|
|
192
|
+
readonly logger?: TelegramAdapterLogger;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Download the bytes for each inbound {@link TelegramAttachment} and map them to
|
|
196
|
+
* the transport-agnostic {@link AgentAttachment} shape. Enforces a byte cap and a
|
|
197
|
+
* MIME allowlist, ties every request to `abortSignal`, and skips (never throws on)
|
|
198
|
+
* an attachment whose download fails so the run still proceeds. Photos and audio
|
|
199
|
+
* without a declared MIME type fall back to sensible defaults.
|
|
74
200
|
*/
|
|
75
|
-
export declare function
|
|
201
|
+
export declare function downloadTelegramAttachments(attachments: readonly TelegramAttachment[], downloader: TelegramFileDownloader, abortSignal: AbortSignal, options?: DownloadTelegramAttachmentsOptions): Promise<AgentAttachment[]>;
|
|
76
202
|
/**
|
|
77
203
|
* Deliver a terminal/system message (cancelled, error, …) in place. Such copy is
|
|
78
204
|
* fixed text we author, not model output, so it is delivered as plain text
|
package/dist/adapter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,cAAc,IAAI,oBAAoB,EACtC,aAAa,EACd,MAAM,6BAA6B,CAAC;AAErC,OAAO,EACL,qBAAqB,EACrB,KAAK,kBAAkB,EACvB,KAAK,2BAA2B,EACjC,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EACV,cAAc,
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
|
package/dist/adapter.js
CHANGED
|
@@ -1,36 +1,47 @@
|
|
|
1
1
|
import { TelegramMessageStream, } from "./message-stream.js";
|
|
2
2
|
export const DEFAULT_ERROR_TEXT = "The agent failed while processing your message.";
|
|
3
3
|
export const DEFAULT_MESSAGES = {
|
|
4
|
-
welcomeText: "Hello! Send
|
|
5
|
-
helpText: "Send
|
|
4
|
+
welcomeText: "Hello! Send text or Telegram media. I pass your caption and download allowed attachments to share with the configured agent.",
|
|
5
|
+
helpText: "Send text, documents, photos, audio, video, or voice messages. I forward your caption and download supported attachments (within size/type limits) for the agent. Use /cancel to stop the current response.",
|
|
6
6
|
busyText: "I am still working on your previous message. Use /cancel to stop it.",
|
|
7
7
|
unauthorizedText: "This Telegram chat is not authorized to use this bot.",
|
|
8
8
|
cancelledText: "Cancelled.",
|
|
9
9
|
errorText: DEFAULT_ERROR_TEXT,
|
|
10
|
-
unsupportedText: "I can
|
|
10
|
+
unsupportedText: "I can handle text and Telegram document, photo, audio, video, or voice metadata in this adapter.",
|
|
11
11
|
};
|
|
12
12
|
/**
|
|
13
13
|
* Build the responder-facing {@link AgentRequest} from a Telegram update. The
|
|
14
14
|
* grammY message handler passes `ctx.update` and `ctx.message`, which are
|
|
15
15
|
* structurally compatible with the wire types this reads.
|
|
16
|
+
*
|
|
17
|
+
* `resolvedAttachments` are the downloaded {@link AgentAttachment} bytes (when
|
|
18
|
+
* available) that populate `request.attachments`; the original Telegram file
|
|
19
|
+
* metadata is always preserved under `metadata.telegram.attachments`.
|
|
16
20
|
*/
|
|
17
|
-
export function buildAgentRequest(update, message,
|
|
21
|
+
export function buildAgentRequest(update, message, input, abortSignal, resolvedAttachments) {
|
|
18
22
|
const from = metadataFromUser(message.from);
|
|
23
|
+
const telegramMetadata = {
|
|
24
|
+
updateId: update.update_id,
|
|
25
|
+
chat: metadataFromChat(message.chat),
|
|
26
|
+
message: metadataFromMessage(message),
|
|
27
|
+
};
|
|
28
|
+
if (input.attachments.length > 0) {
|
|
29
|
+
telegramMetadata.attachments = input.attachments;
|
|
30
|
+
}
|
|
19
31
|
const request = {
|
|
20
32
|
conversationId: `telegram:${String(message.chat.id)}`,
|
|
21
33
|
chatId: message.chat.id,
|
|
22
34
|
messageId: message.message_id,
|
|
23
35
|
updateId: update.update_id,
|
|
24
|
-
text,
|
|
36
|
+
text: input.text,
|
|
25
37
|
abortSignal,
|
|
26
38
|
metadata: {
|
|
27
|
-
telegram:
|
|
28
|
-
updateId: update.update_id,
|
|
29
|
-
chat: metadataFromChat(message.chat),
|
|
30
|
-
message: metadataFromMessage(message),
|
|
31
|
-
},
|
|
39
|
+
telegram: telegramMetadata,
|
|
32
40
|
},
|
|
33
41
|
};
|
|
42
|
+
if (resolvedAttachments !== undefined && resolvedAttachments.length > 0) {
|
|
43
|
+
request.attachments = resolvedAttachments;
|
|
44
|
+
}
|
|
34
45
|
if (message.from?.id !== undefined) {
|
|
35
46
|
request.userId = message.from.id;
|
|
36
47
|
}
|
|
@@ -42,6 +53,50 @@ export function buildAgentRequest(update, message, text, abortSignal) {
|
|
|
42
53
|
}
|
|
43
54
|
return request;
|
|
44
55
|
}
|
|
56
|
+
export function normalizeTelegramMessageInput(message) {
|
|
57
|
+
if (message.animation !== undefined) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const text = normalizeMessageText(message);
|
|
61
|
+
const attachments = extractTelegramAttachments(message);
|
|
62
|
+
if (text.length === 0 && attachments.length === 0) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
text: text.length > 0 ? text : summarizeTelegramAttachments(attachments),
|
|
67
|
+
attachments,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Merge a Telegram media-group (album) into a single input: Telegram delivers an
|
|
72
|
+
* album of N photos/videos as N separate messages sharing one `media_group_id`,
|
|
73
|
+
* with the caption on only one of them. We concatenate every message's
|
|
74
|
+
* attachments and take the single caption (first non-empty), so the agent sees
|
|
75
|
+
* all photos as one request instead of N single-attachment turns.
|
|
76
|
+
*/
|
|
77
|
+
export function mergeTelegramMessageInputs(messages) {
|
|
78
|
+
const attachments = [];
|
|
79
|
+
let text = "";
|
|
80
|
+
for (const message of messages) {
|
|
81
|
+
if (message.animation !== undefined) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (text.length === 0) {
|
|
85
|
+
const messageText = normalizeMessageText(message);
|
|
86
|
+
if (messageText.length > 0) {
|
|
87
|
+
text = messageText;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
attachments.push(...extractTelegramAttachments(message));
|
|
91
|
+
}
|
|
92
|
+
if (text.length === 0 && attachments.length === 0) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
text: text.length > 0 ? text : summarizeTelegramAttachments(attachments),
|
|
97
|
+
attachments,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
45
100
|
function metadataFromChat(messageChat) {
|
|
46
101
|
const chat = { id: messageChat.id };
|
|
47
102
|
if (messageChat.type !== undefined) {
|
|
@@ -62,6 +117,323 @@ function metadataFromMessage(message) {
|
|
|
62
117
|
}
|
|
63
118
|
return metadata;
|
|
64
119
|
}
|
|
120
|
+
function normalizeMessageText(message) {
|
|
121
|
+
return (message.text ?? message.caption ?? "").trim();
|
|
122
|
+
}
|
|
123
|
+
function extractTelegramAttachments(message) {
|
|
124
|
+
const attachments = [];
|
|
125
|
+
const document = attachmentFromDocument(message.document);
|
|
126
|
+
if (document !== undefined) {
|
|
127
|
+
attachments.push(document);
|
|
128
|
+
}
|
|
129
|
+
const photo = attachmentFromPhoto(message.photo);
|
|
130
|
+
if (photo !== undefined) {
|
|
131
|
+
attachments.push(photo);
|
|
132
|
+
}
|
|
133
|
+
const audio = attachmentFromAudio(message.audio);
|
|
134
|
+
if (audio !== undefined) {
|
|
135
|
+
attachments.push(audio);
|
|
136
|
+
}
|
|
137
|
+
const video = attachmentFromVideo(message.video);
|
|
138
|
+
if (video !== undefined) {
|
|
139
|
+
attachments.push(video);
|
|
140
|
+
}
|
|
141
|
+
const voice = attachmentFromVoice(message.voice);
|
|
142
|
+
if (voice !== undefined) {
|
|
143
|
+
attachments.push(voice);
|
|
144
|
+
}
|
|
145
|
+
return attachments;
|
|
146
|
+
}
|
|
147
|
+
function attachmentFromDocument(document) {
|
|
148
|
+
if (document === undefined) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
const attachment = {
|
|
152
|
+
kind: "document",
|
|
153
|
+
fileId: document.file_id,
|
|
154
|
+
fileUniqueId: document.file_unique_id,
|
|
155
|
+
};
|
|
156
|
+
addFileSize(attachment, document.file_size);
|
|
157
|
+
if (document.file_name !== undefined) {
|
|
158
|
+
attachment.fileName = document.file_name;
|
|
159
|
+
}
|
|
160
|
+
if (document.mime_type !== undefined) {
|
|
161
|
+
attachment.mimeType = document.mime_type;
|
|
162
|
+
}
|
|
163
|
+
return attachment;
|
|
164
|
+
}
|
|
165
|
+
function attachmentFromPhoto(photos) {
|
|
166
|
+
const sizes = photos?.map(photoSizeFromTelegram).filter(isDefined) ?? [];
|
|
167
|
+
if (sizes.length === 0) {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
const largest = sizes.reduce((best, candidate) => candidate.width * candidate.height > best.width * best.height ? candidate : best);
|
|
171
|
+
const attachment = {
|
|
172
|
+
kind: "photo",
|
|
173
|
+
fileId: largest.fileId,
|
|
174
|
+
fileUniqueId: largest.fileUniqueId,
|
|
175
|
+
width: largest.width,
|
|
176
|
+
height: largest.height,
|
|
177
|
+
sizes,
|
|
178
|
+
};
|
|
179
|
+
addFileSize(attachment, largest.fileSize);
|
|
180
|
+
return attachment;
|
|
181
|
+
}
|
|
182
|
+
function attachmentFromAudio(audio) {
|
|
183
|
+
if (audio === undefined) {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
const attachment = {
|
|
187
|
+
kind: "audio",
|
|
188
|
+
fileId: audio.file_id,
|
|
189
|
+
fileUniqueId: audio.file_unique_id,
|
|
190
|
+
duration: audio.duration,
|
|
191
|
+
};
|
|
192
|
+
addFileSize(attachment, audio.file_size);
|
|
193
|
+
if (audio.file_name !== undefined) {
|
|
194
|
+
attachment.fileName = audio.file_name;
|
|
195
|
+
}
|
|
196
|
+
if (audio.mime_type !== undefined) {
|
|
197
|
+
attachment.mimeType = audio.mime_type;
|
|
198
|
+
}
|
|
199
|
+
return attachment;
|
|
200
|
+
}
|
|
201
|
+
function attachmentFromVideo(video) {
|
|
202
|
+
if (video === undefined) {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
const attachment = {
|
|
206
|
+
kind: "video",
|
|
207
|
+
fileId: video.file_id,
|
|
208
|
+
fileUniqueId: video.file_unique_id,
|
|
209
|
+
duration: video.duration,
|
|
210
|
+
width: video.width,
|
|
211
|
+
height: video.height,
|
|
212
|
+
};
|
|
213
|
+
addFileSize(attachment, video.file_size);
|
|
214
|
+
if (video.file_name !== undefined) {
|
|
215
|
+
attachment.fileName = video.file_name;
|
|
216
|
+
}
|
|
217
|
+
if (video.mime_type !== undefined) {
|
|
218
|
+
attachment.mimeType = video.mime_type;
|
|
219
|
+
}
|
|
220
|
+
return attachment;
|
|
221
|
+
}
|
|
222
|
+
function attachmentFromVoice(voice) {
|
|
223
|
+
if (voice === undefined) {
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
const attachment = {
|
|
227
|
+
kind: "voice",
|
|
228
|
+
fileId: voice.file_id,
|
|
229
|
+
fileUniqueId: voice.file_unique_id,
|
|
230
|
+
duration: voice.duration,
|
|
231
|
+
};
|
|
232
|
+
addFileSize(attachment, voice.file_size);
|
|
233
|
+
if (voice.mime_type !== undefined) {
|
|
234
|
+
attachment.mimeType = voice.mime_type;
|
|
235
|
+
}
|
|
236
|
+
return attachment;
|
|
237
|
+
}
|
|
238
|
+
function photoSizeFromTelegram(size) {
|
|
239
|
+
const attachmentSize = {
|
|
240
|
+
fileId: size.file_id,
|
|
241
|
+
fileUniqueId: size.file_unique_id,
|
|
242
|
+
width: size.width,
|
|
243
|
+
height: size.height,
|
|
244
|
+
};
|
|
245
|
+
addFileSize(attachmentSize, size.file_size);
|
|
246
|
+
return attachmentSize;
|
|
247
|
+
}
|
|
248
|
+
function addFileSize(target, fileSize) {
|
|
249
|
+
if (typeof fileSize === "number" && Number.isFinite(fileSize)) {
|
|
250
|
+
target.fileSize = fileSize;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function isDefined(value) {
|
|
254
|
+
return value !== undefined;
|
|
255
|
+
}
|
|
256
|
+
function summarizeTelegramAttachments(attachments) {
|
|
257
|
+
return attachments.map(describeTelegramAttachment).join("\n");
|
|
258
|
+
}
|
|
259
|
+
function describeTelegramAttachment(attachment) {
|
|
260
|
+
const details = attachmentDetails(attachment);
|
|
261
|
+
return details.length === 0
|
|
262
|
+
? `Telegram ${attachment.kind}`
|
|
263
|
+
: `Telegram ${attachment.kind}: ${details.join(", ")}`;
|
|
264
|
+
}
|
|
265
|
+
function attachmentDetails(attachment) {
|
|
266
|
+
const details = [];
|
|
267
|
+
if ("fileName" in attachment && attachment.fileName !== undefined) {
|
|
268
|
+
details.push(attachment.fileName);
|
|
269
|
+
}
|
|
270
|
+
if ("mimeType" in attachment && attachment.mimeType !== undefined) {
|
|
271
|
+
details.push(attachment.mimeType);
|
|
272
|
+
}
|
|
273
|
+
if ("width" in attachment && "height" in attachment) {
|
|
274
|
+
details.push(`${attachment.width}x${attachment.height}`);
|
|
275
|
+
}
|
|
276
|
+
if ("duration" in attachment) {
|
|
277
|
+
details.push(`${attachment.duration}s`);
|
|
278
|
+
}
|
|
279
|
+
if (attachment.fileSize !== undefined) {
|
|
280
|
+
details.push(formatBytes(attachment.fileSize));
|
|
281
|
+
}
|
|
282
|
+
return details;
|
|
283
|
+
}
|
|
284
|
+
function formatBytes(bytes) {
|
|
285
|
+
if (bytes < 1024) {
|
|
286
|
+
return `${bytes} B`;
|
|
287
|
+
}
|
|
288
|
+
if (bytes < 1024 * 1024) {
|
|
289
|
+
return `${Math.round(bytes / 1024)} KB`;
|
|
290
|
+
}
|
|
291
|
+
return `${Math.round(bytes / (1024 * 1024))} MB`;
|
|
292
|
+
}
|
|
293
|
+
/** Telegram's hard cap for bot file downloads is 20 MB. */
|
|
294
|
+
export const DEFAULT_ATTACHMENT_MAX_BYTES = 20 * 1024 * 1024;
|
|
295
|
+
/**
|
|
296
|
+
* MIME types the adapter will download and inline. Images flow to vision-capable
|
|
297
|
+
* runtimes; documents/text are saved to disk (and decoded inline for text/*).
|
|
298
|
+
*/
|
|
299
|
+
export const DEFAULT_ATTACHMENT_MIME_ALLOWLIST = [
|
|
300
|
+
"image/png",
|
|
301
|
+
"image/jpeg",
|
|
302
|
+
"image/gif",
|
|
303
|
+
"image/webp",
|
|
304
|
+
"application/pdf",
|
|
305
|
+
"application/json",
|
|
306
|
+
"application/zip",
|
|
307
|
+
"application/msword",
|
|
308
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
309
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
310
|
+
"application/vnd.ms-excel",
|
|
311
|
+
"text/plain",
|
|
312
|
+
"text/markdown",
|
|
313
|
+
"text/csv",
|
|
314
|
+
"text/html",
|
|
315
|
+
// Audio (Telegram voice messages normalize to audio/ogg; see attachmentMimeType).
|
|
316
|
+
"audio/ogg",
|
|
317
|
+
"audio/mpeg",
|
|
318
|
+
"audio/mp4",
|
|
319
|
+
"audio/aac",
|
|
320
|
+
"audio/wav",
|
|
321
|
+
"audio/webm",
|
|
322
|
+
// Video.
|
|
323
|
+
"video/mp4",
|
|
324
|
+
"video/mpeg",
|
|
325
|
+
"video/quicktime",
|
|
326
|
+
"video/webm",
|
|
327
|
+
];
|
|
328
|
+
/**
|
|
329
|
+
* Download the bytes for each inbound {@link TelegramAttachment} and map them to
|
|
330
|
+
* the transport-agnostic {@link AgentAttachment} shape. Enforces a byte cap and a
|
|
331
|
+
* MIME allowlist, ties every request to `abortSignal`, and skips (never throws on)
|
|
332
|
+
* an attachment whose download fails so the run still proceeds. Photos and audio
|
|
333
|
+
* without a declared MIME type fall back to sensible defaults.
|
|
334
|
+
*/
|
|
335
|
+
export async function downloadTelegramAttachments(attachments, downloader, abortSignal, options) {
|
|
336
|
+
const maxBytes = options?.maxBytes ?? DEFAULT_ATTACHMENT_MAX_BYTES;
|
|
337
|
+
const allowlist = new Set((options?.mimeAllowlist ?? DEFAULT_ATTACHMENT_MIME_ALLOWLIST).map((mime) => mime.toLowerCase()));
|
|
338
|
+
const logger = options?.logger;
|
|
339
|
+
const resolved = [];
|
|
340
|
+
for (const attachment of attachments) {
|
|
341
|
+
if (abortSignal.aborted) {
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
const source = attachmentSource(attachment);
|
|
345
|
+
const mimeType = source.mimeType.toLowerCase();
|
|
346
|
+
if (!allowlist.has(mimeType)) {
|
|
347
|
+
logger?.debug?.("Skipping Telegram attachment with disallowed MIME type.", {
|
|
348
|
+
mimeType: source.mimeType,
|
|
349
|
+
name: source.name,
|
|
350
|
+
});
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
if (source.declaredSize !== undefined && source.declaredSize > maxBytes) {
|
|
354
|
+
logger?.debug?.("Skipping oversized Telegram attachment.", {
|
|
355
|
+
sizeBytes: source.declaredSize,
|
|
356
|
+
maxBytes,
|
|
357
|
+
name: source.name,
|
|
358
|
+
});
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
try {
|
|
362
|
+
const filePath = await downloader.resolveFilePath(source.fileId, abortSignal);
|
|
363
|
+
if (filePath === undefined) {
|
|
364
|
+
logger?.warn?.("Telegram getFile returned no file_path; skipping attachment.", {
|
|
365
|
+
fileId: source.fileId,
|
|
366
|
+
name: source.name,
|
|
367
|
+
});
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
const bytes = await downloader.download(filePath, abortSignal, maxBytes);
|
|
371
|
+
if (bytes.byteLength > maxBytes) {
|
|
372
|
+
logger?.warn?.("Telegram attachment exceeded the size cap after download; skipping.", {
|
|
373
|
+
sizeBytes: bytes.byteLength,
|
|
374
|
+
maxBytes,
|
|
375
|
+
name: source.name,
|
|
376
|
+
});
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
resolved.push(buildAgentAttachment(source, mimeType, bytes));
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
// Download failures never fail the run — skip the attachment and continue.
|
|
383
|
+
logger?.warn?.("Failed to download Telegram attachment; skipping it.", {
|
|
384
|
+
fileId: source.fileId,
|
|
385
|
+
name: source.name,
|
|
386
|
+
error: error instanceof Error ? error.message : String(error),
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return resolved;
|
|
391
|
+
}
|
|
392
|
+
function buildAgentAttachment(source, mimeType, bytes) {
|
|
393
|
+
const kind = mimeType.startsWith("image/") ? "image" : "document";
|
|
394
|
+
const attachment = {
|
|
395
|
+
kind,
|
|
396
|
+
mimeType: source.mimeType,
|
|
397
|
+
data: Buffer.from(bytes).toString("base64"),
|
|
398
|
+
sizeBytes: bytes.byteLength,
|
|
399
|
+
};
|
|
400
|
+
if (source.name !== undefined) {
|
|
401
|
+
attachment.name = source.name;
|
|
402
|
+
}
|
|
403
|
+
if (mimeType.startsWith("text/")) {
|
|
404
|
+
attachment.text = Buffer.from(bytes).toString("utf8");
|
|
405
|
+
}
|
|
406
|
+
return attachment;
|
|
407
|
+
}
|
|
408
|
+
function attachmentSource(attachment) {
|
|
409
|
+
const name = "fileName" in attachment ? attachment.fileName : undefined;
|
|
410
|
+
return {
|
|
411
|
+
fileId: attachment.fileId,
|
|
412
|
+
mimeType: attachmentMimeType(attachment),
|
|
413
|
+
name,
|
|
414
|
+
declaredSize: attachment.fileSize,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
function attachmentMimeType(attachment) {
|
|
418
|
+
if ("mimeType" in attachment && attachment.mimeType !== undefined) {
|
|
419
|
+
return attachment.mimeType;
|
|
420
|
+
}
|
|
421
|
+
if (attachment.kind === "photo") {
|
|
422
|
+
return "image/jpeg";
|
|
423
|
+
}
|
|
424
|
+
if (attachment.kind === "voice") {
|
|
425
|
+
return "audio/ogg";
|
|
426
|
+
}
|
|
427
|
+
// Telegram may omit mime_type on audio/video; fall back to a sensible default
|
|
428
|
+
// on the allowlist so the attachment is not skipped as application/octet-stream.
|
|
429
|
+
if (attachment.kind === "audio") {
|
|
430
|
+
return "audio/mpeg";
|
|
431
|
+
}
|
|
432
|
+
if (attachment.kind === "video") {
|
|
433
|
+
return "video/mp4";
|
|
434
|
+
}
|
|
435
|
+
return "application/octet-stream";
|
|
436
|
+
}
|
|
65
437
|
function metadataFromUser(user) {
|
|
66
438
|
if (user === undefined) {
|
|
67
439
|
return undefined;
|