/** * Server-side utilities for Microsoft Teams integration. * This file contains functions that require server-side dependencies and should * only be imported by API routes, NOT by tool definitions (to avoid circular imports). */ import type { Logger } from '@sim/logger' import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server' import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils' import { downloadServableFileFromStorage } from '@/lib/uploads/utils/file-utils.server' import { FileAccessDeniedError, verifyFileAccess } from '@/app/api/files/authorization' import type { UserFile } from '@/executor/types' import type { GraphApiErrorResponse, GraphDriveItem } from '@/tools/microsoft_teams/types' /** Maximum file size for Teams direct upload (4MB) */ const MAX_TEAMS_FILE_SIZE = 4 * 1024 * 1024 /** Output format for uploaded files */ interface TeamsFileOutput { name: string mimeType: string data: string size: number } /** Attachment reference for Teams message */ interface TeamsAttachmentRef { id: string contentType: 'reference' contentUrl: string name: string } /** Result from processing and uploading files for Teams */ export interface TeamsFileUploadResult { attachments: TeamsAttachmentRef[] filesOutput: TeamsFileOutput[] } /** * Process and upload files to OneDrive for Teams message attachments. * Handles size validation, downloading from storage, uploading to OneDrive, * and creating attachment references. */ export async function uploadFilesForTeamsMessage(params: { rawFiles: RawFileInput[] accessToken: string requestId: string logger: Logger userId: string }): Promise { const { rawFiles, accessToken, requestId, logger: log, userId } = params const attachments: TeamsAttachmentRef[] = [] const filesOutput: TeamsFileOutput[] = [] if (!rawFiles || rawFiles.length === 0) { return { attachments, filesOutput } } log.info(`[${requestId}] Processing ${rawFiles.length} file(s) for upload to OneDrive`) const userFiles = processFilesToUserFiles(rawFiles, requestId, log) as UserFile[] for (const file of userFiles) { // Check size limit if (file.size > MAX_TEAMS_FILE_SIZE) { const sizeMB = (file.size / (1024 * 1024)).toFixed(2) log.error( `[${requestId}] File ${file.name} is ${sizeMB}MB, exceeds 4MB limit for direct upload` ) throw new Error( `File "${file.name}" (${sizeMB}MB) exceeds the 4MB limit for Teams attachments. Use smaller files or upload to SharePoint/OneDrive first.` ) } log.info(`[${requestId}] Uploading file to Teams: ${file.name} (${file.size} bytes)`) const hasAccess = await verifyFileAccess(file.key, userId) if (!hasAccess) { throw new FileAccessDeniedError() } // Download file from storage const { buffer, contentType } = await downloadServableFileFromStorage(file, requestId, log) if (buffer.length > MAX_TEAMS_FILE_SIZE) { const sizeMB = (buffer.length / (1024 * 1024)).toFixed(2) throw new Error( `File "${file.name}" (${sizeMB}MB) exceeds the 4MB limit for Teams attachments. Use smaller files or upload to SharePoint/OneDrive first.` ) } const resolvedMimeType = contentType || file.type || 'application/octet-stream' filesOutput.push({ name: file.name, mimeType: resolvedMimeType, data: buffer.toString('base64'), size: buffer.length, }) // Upload to OneDrive const uploadUrl = 'https://graph.microsoft.com/v1.0/me/drive/root:/TeamsAttachments/' + encodeURIComponent(file.name) + ':/content' log.info(`[${requestId}] Uploading to OneDrive: ${uploadUrl}`) const uploadResponse = await secureFetchWithValidation( uploadUrl, { method: 'PUT', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': resolvedMimeType, }, body: buffer, }, 'uploadUrl' ) if (!uploadResponse.ok) { const errorData = (await uploadResponse.json().catch(() => ({}))) as GraphApiErrorResponse log.error(`[${requestId}] Teams upload failed:`, errorData) throw new Error( `Failed to upload file to Teams: ${errorData.error?.message || 'Unknown error'}` ) } const uploadedFile = (await uploadResponse.json()) as GraphDriveItem log.info(`[${requestId}] File uploaded to OneDrive successfully`, { id: uploadedFile.id, webUrl: uploadedFile.webUrl, }) // Get file details for attachment reference // Note: webDavUrl requires 'select' without the '$' prefix to be reliably returned const fileDetailsUrl = `https://graph.microsoft.com/v1.0/me/drive/items/${uploadedFile.id}?select=id,name,webDavUrl,eTag,size` const fileDetailsResponse = await secureFetchWithValidation( fileDetailsUrl, { method: 'GET', headers: { Authorization: `Bearer ${accessToken}`, }, }, 'fileDetailsUrl' ) if (!fileDetailsResponse.ok) { const errorData = (await fileDetailsResponse .json() .catch(() => ({}))) as GraphApiErrorResponse log.error(`[${requestId}] Failed to get file details:`, errorData) throw new Error(`Failed to get file details: ${errorData.error?.message || 'Unknown error'}`) } const fileDetails = (await fileDetailsResponse.json()) as GraphDriveItem log.info(`[${requestId}] Got file details`, { webDavUrl: fileDetails.webDavUrl, eTag: fileDetails.eTag, }) // Validate webDavUrl is present (required for Teams attachment references) if (!fileDetails.webDavUrl) { log.error(`[${requestId}] webDavUrl missing from file details`, { fileId: uploadedFile.id }) throw new Error( `Failed to get file URL for attachment "${file.name}". The file was uploaded but Teams attachment reference could not be created.` ) } // Create attachment reference const attachmentId = fileDetails.eTag?.match(/\{([a-f0-9-]+)\}/i)?.[1] || fileDetails.id attachments.push({ id: attachmentId, contentType: 'reference', contentUrl: fileDetails.webDavUrl, name: file.name, }) log.info(`[${requestId}] Created attachment reference for ${file.name}`) } log.info( `[${requestId}] All ${attachments.length} file(s) uploaded and attachment references created` ) return { attachments, filesOutput } }