UNPKG

@gguf/claw

Version:

WhatsApp gateway CLI (Baileys web) with Pi RPC agent

284 lines (265 loc) 8.4 kB
import type { MSTeamsAccessTokenProvider, MSTeamsAttachmentLike, MSTeamsInboundMedia, } from "./types.js"; import { getMSTeamsRuntime } from "../runtime.js"; import { extractInlineImageCandidates, inferPlaceholder, isDownloadableAttachment, isRecord, isUrlAllowed, normalizeContentType, resolveAuthAllowedHosts, resolveAllowedHosts, } from "./shared.js"; type DownloadCandidate = { url: string; fileHint?: string; contentTypeHint?: string; placeholder: string; }; function resolveDownloadCandidate(att: MSTeamsAttachmentLike): DownloadCandidate | null { const contentType = normalizeContentType(att.contentType); const name = typeof att.name === "string" ? att.name.trim() : ""; if (contentType === "application/vnd.microsoft.teams.file.download.info") { if (!isRecord(att.content)) { return null; } const downloadUrl = typeof att.content.downloadUrl === "string" ? att.content.downloadUrl.trim() : ""; if (!downloadUrl) { return null; } const fileType = typeof att.content.fileType === "string" ? att.content.fileType.trim() : ""; const uniqueId = typeof att.content.uniqueId === "string" ? att.content.uniqueId.trim() : ""; const fileName = typeof att.content.fileName === "string" ? att.content.fileName.trim() : ""; const fileHint = name || fileName || (uniqueId && fileType ? `${uniqueId}.${fileType}` : ""); return { url: downloadUrl, fileHint: fileHint || undefined, contentTypeHint: undefined, placeholder: inferPlaceholder({ contentType, fileName: fileHint, fileType, }), }; } const contentUrl = typeof att.contentUrl === "string" ? att.contentUrl.trim() : ""; if (!contentUrl) { return null; } return { url: contentUrl, fileHint: name || undefined, contentTypeHint: contentType, placeholder: inferPlaceholder({ contentType, fileName: name }), }; } function scopeCandidatesForUrl(url: string): string[] { try { const host = new URL(url).hostname.toLowerCase(); const looksLikeGraph = host.endsWith("graph.microsoft.com") || host.endsWith("sharepoint.com") || host.endsWith("1drv.ms") || host.includes("sharepoint"); return looksLikeGraph ? ["https://graph.microsoft.com", "https://api.botframework.com"] : ["https://api.botframework.com", "https://graph.microsoft.com"]; } catch { return ["https://api.botframework.com", "https://graph.microsoft.com"]; } } async function fetchWithAuthFallback(params: { url: string; tokenProvider?: MSTeamsAccessTokenProvider; fetchFn?: typeof fetch; allowHosts: string[]; authAllowHosts: string[]; }): Promise<Response> { const fetchFn = params.fetchFn ?? fetch; const firstAttempt = await fetchFn(params.url); if (firstAttempt.ok) { return firstAttempt; } if (!params.tokenProvider) { return firstAttempt; } if (firstAttempt.status !== 401 && firstAttempt.status !== 403) { return firstAttempt; } if (!isUrlAllowed(params.url, params.authAllowHosts)) { return firstAttempt; } const scopes = scopeCandidatesForUrl(params.url); for (const scope of scopes) { try { const token = await params.tokenProvider.getAccessToken(scope); const res = await fetchFn(params.url, { headers: { Authorization: `Bearer ${token}` }, redirect: "manual", }); if (res.ok) { return res; } const redirectUrl = readRedirectUrl(params.url, res); if (redirectUrl && isUrlAllowed(redirectUrl, params.allowHosts)) { const redirectRes = await fetchFn(redirectUrl); if (redirectRes.ok) { return redirectRes; } if ( (redirectRes.status === 401 || redirectRes.status === 403) && isUrlAllowed(redirectUrl, params.authAllowHosts) ) { const redirectAuthRes = await fetchFn(redirectUrl, { headers: { Authorization: `Bearer ${token}` }, redirect: "manual", }); if (redirectAuthRes.ok) { return redirectAuthRes; } } } } catch { // Try the next scope. } } return firstAttempt; } function readRedirectUrl(baseUrl: string, res: Response): string | null { if (![301, 302, 303, 307, 308].includes(res.status)) { return null; } const location = res.headers.get("location"); if (!location) { return null; } try { return new URL(location, baseUrl).toString(); } catch { return null; } } /** * Download all file attachments from a Teams message (images, documents, etc.). * Renamed from downloadMSTeamsImageAttachments to support all file types. */ export async function downloadMSTeamsAttachments(params: { attachments: MSTeamsAttachmentLike[] | undefined; maxBytes: number; tokenProvider?: MSTeamsAccessTokenProvider; allowHosts?: string[]; authAllowHosts?: string[]; fetchFn?: typeof fetch; /** When true, embeds original filename in stored path for later extraction. */ preserveFilenames?: boolean; }): Promise<MSTeamsInboundMedia[]> { const list = Array.isArray(params.attachments) ? params.attachments : []; if (list.length === 0) { return []; } const allowHosts = resolveAllowedHosts(params.allowHosts); const authAllowHosts = resolveAuthAllowedHosts(params.authAllowHosts); // Download ANY downloadable attachment (not just images) const downloadable = list.filter(isDownloadableAttachment); const candidates: DownloadCandidate[] = downloadable .map(resolveDownloadCandidate) .filter(Boolean) as DownloadCandidate[]; const inlineCandidates = extractInlineImageCandidates(list); const seenUrls = new Set<string>(); for (const inline of inlineCandidates) { if (inline.kind === "url") { if (!isUrlAllowed(inline.url, allowHosts)) { continue; } if (seenUrls.has(inline.url)) { continue; } seenUrls.add(inline.url); candidates.push({ url: inline.url, fileHint: inline.fileHint, contentTypeHint: inline.contentType, placeholder: inline.placeholder, }); } } if (candidates.length === 0 && inlineCandidates.length === 0) { return []; } const out: MSTeamsInboundMedia[] = []; for (const inline of inlineCandidates) { if (inline.kind !== "data") { continue; } if (inline.data.byteLength > params.maxBytes) { continue; } try { // Data inline candidates (base64 data URLs) don't have original filenames const saved = await getMSTeamsRuntime().channel.media.saveMediaBuffer( inline.data, inline.contentType, "inbound", params.maxBytes, ); out.push({ path: saved.path, contentType: saved.contentType, placeholder: inline.placeholder, }); } catch { // Ignore decode failures and continue. } } for (const candidate of candidates) { if (!isUrlAllowed(candidate.url, allowHosts)) { continue; } try { const res = await fetchWithAuthFallback({ url: candidate.url, tokenProvider: params.tokenProvider, fetchFn: params.fetchFn, allowHosts, authAllowHosts, }); if (!res.ok) { continue; } const buffer = Buffer.from(await res.arrayBuffer()); if (buffer.byteLength > params.maxBytes) { continue; } const mime = await getMSTeamsRuntime().media.detectMime({ buffer, headerMime: res.headers.get("content-type"), filePath: candidate.fileHint ?? candidate.url, }); const originalFilename = params.preserveFilenames ? candidate.fileHint : undefined; const saved = await getMSTeamsRuntime().channel.media.saveMediaBuffer( buffer, mime ?? candidate.contentTypeHint, "inbound", params.maxBytes, originalFilename, ); out.push({ path: saved.path, contentType: saved.contentType, placeholder: candidate.placeholder, }); } catch { // Ignore download failures and continue with next candidate. } } return out; } /** * @deprecated Use `downloadMSTeamsAttachments` instead (supports all file types). */ export const downloadMSTeamsImageAttachments = downloadMSTeamsAttachments;