/** * FileConsentCard utilities for MS Teams large file uploads (>4MB) in personal chats. * * Teams requires user consent before the bot can upload large files. This module provides * utilities for: * - Building FileConsentCard attachments (to request upload permission) * - Building FileInfoCard attachments (to confirm upload completion) * - Parsing fileConsent/invoke activities */ export interface FileConsentCardParams { filename: string; description?: string; sizeInBytes: number; /** Custom context data to include in the card (passed back in the invoke) */ context?: Record; } export interface FileInfoCardParams { filename: string; contentUrl: string; uniqueId: string; fileType: string; } /** * Build a FileConsentCard attachment for requesting upload permission. * Use this for files >= 4MB in personal (1:1) chats. */ export function buildFileConsentCard(params: FileConsentCardParams) { return { contentType: "application/vnd.microsoft.teams.card.file.consent", name: params.filename, content: { description: params.description ?? `File: ${params.filename}`, sizeInBytes: params.sizeInBytes, acceptContext: { filename: params.filename, ...params.context }, declineContext: { filename: params.filename, ...params.context }, }, }; } /** * Build a FileInfoCard attachment for confirming upload completion. * Send this after successfully uploading the file to the consent URL. */ export function buildFileInfoCard(params: FileInfoCardParams) { return { contentType: "application/vnd.microsoft.teams.card.file.info", contentUrl: params.contentUrl, name: params.filename, content: { uniqueId: params.uniqueId, fileType: params.fileType, }, }; } export interface FileConsentUploadInfo { name: string; uploadUrl: string; contentUrl: string; uniqueId: string; fileType: string; } export interface FileConsentResponse { action: "accept" | "decline"; uploadInfo?: FileConsentUploadInfo; context?: Record; } /** * Parse a fileConsent/invoke activity. * Returns null if the activity is not a file consent invoke. */ export function parseFileConsentInvoke(activity: { name?: string; value?: unknown; }): FileConsentResponse | null { if (activity.name !== "fileConsent/invoke") return null; const value = activity.value as { type?: string; action?: string; uploadInfo?: FileConsentUploadInfo; context?: Record; }; if (value?.type !== "fileUpload") return null; return { action: value.action === "accept" ? "accept" : "decline", uploadInfo: value.uploadInfo, context: value.context, }; } /** * Upload a file to the consent URL provided by Teams. * The URL is provided in the fileConsent/invoke response after user accepts. */ export async function uploadToConsentUrl(params: { url: string; buffer: Buffer; contentType?: string; fetchFn?: typeof fetch; }): Promise { const fetchFn = params.fetchFn ?? fetch; const res = await fetchFn(params.url, { method: "PUT", headers: { "Content-Type": params.contentType ?? "application/octet-stream", "Content-Range": `bytes 0-${params.buffer.length - 1}/${params.buffer.length}`, }, body: new Uint8Array(params.buffer), }); if (!res.ok) { throw new Error(`File upload to consent URL failed: ${res.status} ${res.statusText}`); } }