(
files: TelegramFileRef[],
token: string,
getFilePath: (fileId: string) => Promise<string>,
config: FileDeliveryConfig = {},
)
| 108 | * @param config Optional delivery configuration. |
| 109 | */ |
| 110 | export async function buildFileContentParts( |
| 111 | files: TelegramFileRef[], |
| 112 | token: string, |
| 113 | getFilePath: (fileId: string) => Promise<string>, |
| 114 | config: FileDeliveryConfig = {}, |
| 115 | ): Promise<{ parts: AgentContentPart[]; notes: string[] }> { |
| 116 | const maxBytes = config.maxBytesPerFile ?? DEFAULTS.maxBytesPerFile; |
| 117 | const maxFiles = config.maxFiles ?? DEFAULTS.maxFiles; |
| 118 | |
| 119 | const parts: AgentContentPart[] = []; |
| 120 | const notes: string[] = []; |
| 121 | const considered = files.slice(0, maxFiles); |
| 122 | if (files.length > maxFiles) { |
| 123 | notes.push( |
| 124 | `(only the first ${maxFiles} of ${files.length} files processed)`, |
| 125 | ); |
| 126 | } |
| 127 | |
| 128 | for (const f of considered) { |
| 129 | const label = f.fileName ?? f.fileId; |
| 130 | const mime = (f.mimeType ?? "application/octet-stream").toLowerCase(); |
| 131 | |
| 132 | if (typeof f.size === "number" && f.size > maxBytes) { |
| 133 | notes.push( |
| 134 | `skipped "${label}": ${f.size} bytes too large (cap is ${maxBytes} bytes)`, |
| 135 | ); |
| 136 | continue; |
| 137 | } |
| 138 | |
| 139 | let filePath: string; |
| 140 | try { |
| 141 | filePath = await getFilePath(f.fileId); |
| 142 | } catch (err) { |
| 143 | const msg = err instanceof Error ? err.message : String(err); |
| 144 | notes.push( |
| 145 | `skipped "${label}": could not resolve file path — ${redactToken(msg, token)}`, |
| 146 | ); |
| 147 | continue; |
| 148 | } |
| 149 | |
| 150 | const url = `https://api.telegram.org/file/bot${token}/${filePath}`; |
| 151 | let bytes: Buffer; |
| 152 | try { |
| 153 | const res = await fetch(url); |
| 154 | if (!res.ok) { |
| 155 | notes.push(`skipped "${label}": download failed (HTTP ${res.status})`); |
| 156 | continue; |
| 157 | } |
| 158 | |
| 159 | // Pre-check Content-Length to avoid buffering oversized responses entirely |
| 160 | // into memory (memory-DoS guard for cases where f.size is absent, e.g. photos). |
| 161 | const contentLength = res.headers.get("content-length"); |
| 162 | if (contentLength !== null) { |
| 163 | const declared = parseInt(contentLength, 10); |
| 164 | if (!isNaN(declared) && declared > maxBytes) { |
| 165 | notes.push( |
| 166 | `skipped "${label}": Content-Length ${declared} bytes too large (cap is ${maxBytes} bytes)`, |
| 167 | ); |
no test coverage detected
searching dependent graphs…