mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-03 11:14:58 -05:00
fix more bugbot comments
This commit is contained in:
@@ -5,14 +5,12 @@ import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
import type {
|
||||
GraphApiErrorResponse,
|
||||
GraphChatMessage,
|
||||
GraphDriveItem,
|
||||
} from '@/tools/microsoft_teams/types'
|
||||
import { resolveMentionsForChannel, type TeamsMention } from '@/tools/microsoft_teams/utils'
|
||||
import type { GraphApiErrorResponse, GraphChatMessage } from '@/tools/microsoft_teams/types'
|
||||
import {
|
||||
resolveMentionsForChannel,
|
||||
type TeamsMention,
|
||||
uploadFilesForTeamsMessage,
|
||||
} from '@/tools/microsoft_teams/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -60,130 +58,12 @@ export async function POST(request: NextRequest) {
|
||||
fileCount: validatedData.files?.length || 0,
|
||||
})
|
||||
|
||||
const attachments: any[] = []
|
||||
const filesOutput: Array<{
|
||||
name: string
|
||||
mimeType: string
|
||||
data: string
|
||||
size: number
|
||||
}> = []
|
||||
if (validatedData.files && validatedData.files.length > 0) {
|
||||
const rawFiles = validatedData.files
|
||||
logger.info(`[${requestId}] Processing ${rawFiles.length} file(s) for upload to OneDrive`)
|
||||
|
||||
const userFiles = processFilesToUserFiles(rawFiles, requestId, logger)
|
||||
|
||||
for (const file of userFiles) {
|
||||
try {
|
||||
// Microsoft Graph API limits direct uploads to 4MB
|
||||
const maxSize = 4 * 1024 * 1024
|
||||
if (file.size > maxSize) {
|
||||
const sizeMB = (file.size / (1024 * 1024)).toFixed(2)
|
||||
logger.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.`
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Uploading file to Teams: ${file.name} (${file.size} bytes)`)
|
||||
|
||||
const buffer = await downloadFileFromStorage(file, requestId, logger)
|
||||
filesOutput.push({
|
||||
name: file.name,
|
||||
mimeType: file.type || 'application/octet-stream',
|
||||
data: buffer.toString('base64'),
|
||||
size: buffer.length,
|
||||
})
|
||||
|
||||
const uploadUrl =
|
||||
'https://graph.microsoft.com/v1.0/me/drive/root:/TeamsAttachments/' +
|
||||
encodeURIComponent(file.name) +
|
||||
':/content'
|
||||
|
||||
logger.info(`[${requestId}] Uploading to Teams: ${uploadUrl}`)
|
||||
|
||||
const uploadResponse = await secureFetchWithValidation(
|
||||
uploadUrl,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Bearer ${validatedData.accessToken}`,
|
||||
'Content-Type': file.type || 'application/octet-stream',
|
||||
},
|
||||
body: buffer,
|
||||
},
|
||||
'uploadUrl'
|
||||
)
|
||||
|
||||
if (!uploadResponse.ok) {
|
||||
const errorData = (await uploadResponse
|
||||
.json()
|
||||
.catch(() => ({}))) as GraphApiErrorResponse
|
||||
logger.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
|
||||
logger.info(`[${requestId}] File uploaded to Teams successfully`, {
|
||||
id: uploadedFile.id,
|
||||
webUrl: uploadedFile.webUrl,
|
||||
})
|
||||
|
||||
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 ${validatedData.accessToken}`,
|
||||
},
|
||||
},
|
||||
'fileDetailsUrl'
|
||||
)
|
||||
|
||||
if (!fileDetailsResponse.ok) {
|
||||
const errorData = (await fileDetailsResponse
|
||||
.json()
|
||||
.catch(() => ({}))) as GraphApiErrorResponse
|
||||
logger.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
|
||||
logger.info(`[${requestId}] Got file details`, {
|
||||
webDavUrl: fileDetails.webDavUrl,
|
||||
eTag: fileDetails.eTag,
|
||||
})
|
||||
|
||||
const attachmentId = fileDetails.eTag?.match(/\{([a-f0-9-]+)\}/i)?.[1] || fileDetails.id
|
||||
|
||||
attachments.push({
|
||||
id: attachmentId,
|
||||
contentType: 'reference',
|
||||
contentUrl: fileDetails.webDavUrl,
|
||||
name: file.name,
|
||||
})
|
||||
|
||||
logger.info(`[${requestId}] Created attachment reference for ${file.name}`)
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Failed to process file ${file.name}:`, error)
|
||||
throw new Error(
|
||||
`Failed to process file "${file.name}": ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] All ${attachments.length} file(s) uploaded and attachment references created`
|
||||
)
|
||||
}
|
||||
const { attachments, filesOutput } = await uploadFilesForTeamsMessage({
|
||||
rawFiles: validatedData.files || [],
|
||||
accessToken: validatedData.accessToken,
|
||||
requestId,
|
||||
logger,
|
||||
})
|
||||
|
||||
let messageContent = validatedData.content
|
||||
let contentType: 'text' | 'html' = 'text'
|
||||
|
||||
@@ -5,14 +5,12 @@ import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
import type {
|
||||
GraphApiErrorResponse,
|
||||
GraphChatMessage,
|
||||
GraphDriveItem,
|
||||
} from '@/tools/microsoft_teams/types'
|
||||
import { resolveMentionsForChat, type TeamsMention } from '@/tools/microsoft_teams/utils'
|
||||
import type { GraphApiErrorResponse, GraphChatMessage } from '@/tools/microsoft_teams/types'
|
||||
import {
|
||||
resolveMentionsForChat,
|
||||
type TeamsMention,
|
||||
uploadFilesForTeamsMessage,
|
||||
} from '@/tools/microsoft_teams/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -58,130 +56,12 @@ export async function POST(request: NextRequest) {
|
||||
fileCount: validatedData.files?.length || 0,
|
||||
})
|
||||
|
||||
const attachments: any[] = []
|
||||
const filesOutput: Array<{
|
||||
name: string
|
||||
mimeType: string
|
||||
data: string
|
||||
size: number
|
||||
}> = []
|
||||
if (validatedData.files && validatedData.files.length > 0) {
|
||||
const rawFiles = validatedData.files
|
||||
logger.info(`[${requestId}] Processing ${rawFiles.length} file(s) for upload to Teams`)
|
||||
|
||||
const userFiles = processFilesToUserFiles(rawFiles, requestId, logger)
|
||||
|
||||
for (const file of userFiles) {
|
||||
try {
|
||||
// Microsoft Graph API limits direct uploads to 4MB
|
||||
const maxSize = 4 * 1024 * 1024
|
||||
if (file.size > maxSize) {
|
||||
const sizeMB = (file.size / (1024 * 1024)).toFixed(2)
|
||||
logger.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.`
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Uploading file to Teams: ${file.name} (${file.size} bytes)`)
|
||||
|
||||
const buffer = await downloadFileFromStorage(file, requestId, logger)
|
||||
filesOutput.push({
|
||||
name: file.name,
|
||||
mimeType: file.type || 'application/octet-stream',
|
||||
data: buffer.toString('base64'),
|
||||
size: buffer.length,
|
||||
})
|
||||
|
||||
const uploadUrl =
|
||||
'https://graph.microsoft.com/v1.0/me/drive/root:/TeamsAttachments/' +
|
||||
encodeURIComponent(file.name) +
|
||||
':/content'
|
||||
|
||||
logger.info(`[${requestId}] Uploading to Teams: ${uploadUrl}`)
|
||||
|
||||
const uploadResponse = await secureFetchWithValidation(
|
||||
uploadUrl,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Bearer ${validatedData.accessToken}`,
|
||||
'Content-Type': file.type || 'application/octet-stream',
|
||||
},
|
||||
body: buffer,
|
||||
},
|
||||
'uploadUrl'
|
||||
)
|
||||
|
||||
if (!uploadResponse.ok) {
|
||||
const errorData = (await uploadResponse
|
||||
.json()
|
||||
.catch(() => ({}))) as GraphApiErrorResponse
|
||||
logger.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
|
||||
logger.info(`[${requestId}] File uploaded to Teams successfully`, {
|
||||
id: uploadedFile.id,
|
||||
webUrl: uploadedFile.webUrl,
|
||||
})
|
||||
|
||||
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 ${validatedData.accessToken}`,
|
||||
},
|
||||
},
|
||||
'fileDetailsUrl'
|
||||
)
|
||||
|
||||
if (!fileDetailsResponse.ok) {
|
||||
const errorData = (await fileDetailsResponse
|
||||
.json()
|
||||
.catch(() => ({}))) as GraphApiErrorResponse
|
||||
logger.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
|
||||
logger.info(`[${requestId}] Got file details`, {
|
||||
webDavUrl: fileDetails.webDavUrl,
|
||||
eTag: fileDetails.eTag,
|
||||
})
|
||||
|
||||
const attachmentId = fileDetails.eTag?.match(/\{([a-f0-9-]+)\}/i)?.[1] || fileDetails.id
|
||||
|
||||
attachments.push({
|
||||
id: attachmentId,
|
||||
contentType: 'reference',
|
||||
contentUrl: fileDetails.webDavUrl,
|
||||
name: file.name,
|
||||
})
|
||||
|
||||
logger.info(`[${requestId}] Created attachment reference for ${file.name}`)
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Failed to process file ${file.name}:`, error)
|
||||
throw new Error(
|
||||
`Failed to process file "${file.name}": ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] All ${attachments.length} file(s) uploaded and attachment references created`
|
||||
)
|
||||
}
|
||||
const { attachments, filesOutput } = await uploadFilesForTeamsMessage({
|
||||
rawFiles: validatedData.files || [],
|
||||
accessToken: validatedData.accessToken,
|
||||
requestId,
|
||||
logger,
|
||||
})
|
||||
|
||||
let messageContent = validatedData.content
|
||||
let contentType: 'text' | 'html' = 'text'
|
||||
|
||||
@@ -7,15 +7,9 @@ import {
|
||||
validateUrlWithDNS,
|
||||
} from '@/lib/core/security/input-validation.server'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { type StorageContext, StorageService } from '@/lib/uploads'
|
||||
import { RawFileInputSchema } from '@/lib/uploads/utils/file-schemas'
|
||||
import {
|
||||
inferContextFromKey,
|
||||
isInternalFileUrl,
|
||||
processSingleFileToUserFile,
|
||||
} from '@/lib/uploads/utils/file-utils'
|
||||
import { resolveInternalFileUrl } from '@/lib/uploads/utils/file-utils.server'
|
||||
import { verifyFileAccess } from '@/app/api/files/authorization'
|
||||
import { isInternalFileUrl } from '@/lib/uploads/utils/file-utils'
|
||||
import { resolveFileInputToUrl } from '@/lib/uploads/utils/file-utils.server'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -56,120 +50,31 @@ export async function POST(request: NextRequest) {
|
||||
const body = await request.json()
|
||||
const validatedData = PulseParseSchema.parse(body)
|
||||
|
||||
const fileInput = validatedData.file
|
||||
let fileUrl = ''
|
||||
if (fileInput) {
|
||||
logger.info(`[${requestId}] Pulse parse request`, {
|
||||
fileName: fileInput.name,
|
||||
userId,
|
||||
})
|
||||
logger.info(`[${requestId}] Pulse parse request`, {
|
||||
fileName: validatedData.file?.name,
|
||||
filePath: validatedData.filePath,
|
||||
isWorkspaceFile: validatedData.filePath ? isInternalFileUrl(validatedData.filePath) : false,
|
||||
userId,
|
||||
})
|
||||
|
||||
let userFile
|
||||
try {
|
||||
userFile = processSingleFileToUserFile(fileInput, requestId, logger)
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to process file',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const resolution = await resolveFileInputToUrl({
|
||||
file: validatedData.file,
|
||||
filePath: validatedData.filePath,
|
||||
userId,
|
||||
requestId,
|
||||
logger,
|
||||
})
|
||||
|
||||
fileUrl = userFile.url || ''
|
||||
if (fileUrl && isInternalFileUrl(fileUrl)) {
|
||||
const resolution = await resolveInternalFileUrl(fileUrl, userId, requestId, logger)
|
||||
if (resolution.error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: resolution.error.message,
|
||||
},
|
||||
{ status: resolution.error.status }
|
||||
)
|
||||
}
|
||||
fileUrl = resolution.fileUrl || ''
|
||||
}
|
||||
if (!fileUrl && userFile.key) {
|
||||
const context = (userFile.context as StorageContext) || inferContextFromKey(userFile.key)
|
||||
const hasAccess = await verifyFileAccess(userFile.key, userId, undefined, context, false)
|
||||
if (!hasAccess) {
|
||||
logger.warn(`[${requestId}] Unauthorized presigned URL generation attempt`, {
|
||||
userId,
|
||||
key: userFile.key,
|
||||
context,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'File not found',
|
||||
},
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
fileUrl = await StorageService.generatePresignedDownloadUrl(userFile.key, context, 5 * 60)
|
||||
}
|
||||
} else if (validatedData.filePath) {
|
||||
logger.info(`[${requestId}] Pulse parse request`, {
|
||||
filePath: validatedData.filePath,
|
||||
isWorkspaceFile: isInternalFileUrl(validatedData.filePath),
|
||||
userId,
|
||||
})
|
||||
|
||||
fileUrl = validatedData.filePath
|
||||
const isInternalFilePath = isInternalFileUrl(validatedData.filePath)
|
||||
if (isInternalFilePath) {
|
||||
const resolution = await resolveInternalFileUrl(
|
||||
validatedData.filePath,
|
||||
userId,
|
||||
requestId,
|
||||
logger
|
||||
)
|
||||
if (resolution.error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: resolution.error.message,
|
||||
},
|
||||
{ status: resolution.error.status }
|
||||
)
|
||||
}
|
||||
fileUrl = resolution.fileUrl || fileUrl
|
||||
} else if (validatedData.filePath.startsWith('/')) {
|
||||
logger.warn(`[${requestId}] Invalid internal path`, {
|
||||
userId,
|
||||
path: validatedData.filePath.substring(0, 50),
|
||||
})
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Invalid file path. Only uploaded files are supported for internal paths.',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
} else {
|
||||
const urlValidation = await validateUrlWithDNS(fileUrl, 'filePath')
|
||||
if (!urlValidation.isValid) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: urlValidation.error,
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
}
|
||||
if (resolution.error) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: resolution.error.message },
|
||||
{ status: resolution.error.status }
|
||||
)
|
||||
}
|
||||
|
||||
const fileUrl = resolution.fileUrl
|
||||
if (!fileUrl) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'File input is required',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
return NextResponse.json({ success: false, error: 'File input is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
@@ -7,15 +7,9 @@ import {
|
||||
validateUrlWithDNS,
|
||||
} from '@/lib/core/security/input-validation.server'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { type StorageContext, StorageService } from '@/lib/uploads'
|
||||
import { RawFileInputSchema } from '@/lib/uploads/utils/file-schemas'
|
||||
import {
|
||||
inferContextFromKey,
|
||||
isInternalFileUrl,
|
||||
processSingleFileToUserFile,
|
||||
} from '@/lib/uploads/utils/file-utils'
|
||||
import { resolveInternalFileUrl } from '@/lib/uploads/utils/file-utils.server'
|
||||
import { verifyFileAccess } from '@/app/api/files/authorization'
|
||||
import { isInternalFileUrl } from '@/lib/uploads/utils/file-utils'
|
||||
import { resolveFileInputToUrl } from '@/lib/uploads/utils/file-utils.server'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -52,122 +46,31 @@ export async function POST(request: NextRequest) {
|
||||
const body = await request.json()
|
||||
const validatedData = ReductoParseSchema.parse(body)
|
||||
|
||||
const fileInput = validatedData.file
|
||||
let fileUrl = ''
|
||||
if (fileInput) {
|
||||
logger.info(`[${requestId}] Reducto parse request`, {
|
||||
fileName: fileInput.name,
|
||||
userId,
|
||||
})
|
||||
logger.info(`[${requestId}] Reducto parse request`, {
|
||||
fileName: validatedData.file?.name,
|
||||
filePath: validatedData.filePath,
|
||||
isWorkspaceFile: validatedData.filePath ? isInternalFileUrl(validatedData.filePath) : false,
|
||||
userId,
|
||||
})
|
||||
|
||||
let userFile
|
||||
try {
|
||||
userFile = processSingleFileToUserFile(fileInput, requestId, logger)
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to process file',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const resolution = await resolveFileInputToUrl({
|
||||
file: validatedData.file,
|
||||
filePath: validatedData.filePath,
|
||||
userId,
|
||||
requestId,
|
||||
logger,
|
||||
})
|
||||
|
||||
fileUrl = userFile.url || ''
|
||||
if (fileUrl && isInternalFileUrl(fileUrl)) {
|
||||
const resolution = await resolveInternalFileUrl(fileUrl, userId, requestId, logger)
|
||||
if (resolution.error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: resolution.error.message,
|
||||
},
|
||||
{ status: resolution.error.status }
|
||||
)
|
||||
}
|
||||
fileUrl = resolution.fileUrl || ''
|
||||
}
|
||||
if (!fileUrl && userFile.key) {
|
||||
const context = (userFile.context as StorageContext) || inferContextFromKey(userFile.key)
|
||||
const hasAccess = await verifyFileAccess(userFile.key, userId, undefined, context, false)
|
||||
|
||||
if (!hasAccess) {
|
||||
logger.warn(`[${requestId}] Unauthorized presigned URL generation attempt`, {
|
||||
userId,
|
||||
key: userFile.key,
|
||||
context,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'File not found',
|
||||
},
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
fileUrl = await StorageService.generatePresignedDownloadUrl(userFile.key, context, 5 * 60)
|
||||
}
|
||||
} else if (validatedData.filePath) {
|
||||
logger.info(`[${requestId}] Reducto parse request`, {
|
||||
filePath: validatedData.filePath,
|
||||
isWorkspaceFile: isInternalFileUrl(validatedData.filePath),
|
||||
userId,
|
||||
})
|
||||
|
||||
fileUrl = validatedData.filePath
|
||||
const isInternalFilePath = isInternalFileUrl(validatedData.filePath)
|
||||
if (isInternalFilePath) {
|
||||
const resolution = await resolveInternalFileUrl(
|
||||
validatedData.filePath,
|
||||
userId,
|
||||
requestId,
|
||||
logger
|
||||
)
|
||||
if (resolution.error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: resolution.error.message,
|
||||
},
|
||||
{ status: resolution.error.status }
|
||||
)
|
||||
}
|
||||
fileUrl = resolution.fileUrl || fileUrl
|
||||
} else if (validatedData.filePath.startsWith('/')) {
|
||||
logger.warn(`[${requestId}] Invalid internal path`, {
|
||||
userId,
|
||||
path: validatedData.filePath.substring(0, 50),
|
||||
})
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Invalid file path. Only uploaded files are supported for internal paths.',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
} else {
|
||||
const urlValidation = await validateUrlWithDNS(fileUrl, 'filePath')
|
||||
if (!urlValidation.isValid) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: urlValidation.error,
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
}
|
||||
if (resolution.error) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: resolution.error.message },
|
||||
{ status: resolution.error.status }
|
||||
)
|
||||
}
|
||||
|
||||
const fileUrl = resolution.fileUrl
|
||||
if (!fileUrl) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'File input is required',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
return NextResponse.json({ success: false, error: 'File input is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const reductoBody: Record<string, unknown> = {
|
||||
|
||||
@@ -575,8 +575,8 @@ async function transcribeWithAssemblyAI(
|
||||
audio_url: upload_url,
|
||||
}
|
||||
|
||||
// AssemblyAI only supports 'best', 'slam-1', or 'universal' for speech_model
|
||||
if (model === 'best') {
|
||||
// AssemblyAI supports 'best', 'slam-1', or 'universal' for speech_model
|
||||
if (model === 'best' || model === 'slam-1' || model === 'universal') {
|
||||
transcriptRequest.speech_model = model
|
||||
}
|
||||
|
||||
|
||||
@@ -12,10 +12,124 @@ import {
|
||||
extractStorageKey,
|
||||
inferContextFromKey,
|
||||
isInternalFileUrl,
|
||||
processSingleFileToUserFile,
|
||||
type RawFileInput,
|
||||
} from '@/lib/uploads/utils/file-utils'
|
||||
import { verifyFileAccess } from '@/app/api/files/authorization'
|
||||
import type { UserFile } from '@/executor/types'
|
||||
|
||||
/**
|
||||
* Result type for file input resolution
|
||||
*/
|
||||
export interface FileResolutionResult {
|
||||
fileUrl?: string
|
||||
error?: {
|
||||
status: number
|
||||
message: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for resolving file input to a URL
|
||||
*/
|
||||
export interface ResolveFileInputOptions {
|
||||
file?: RawFileInput
|
||||
filePath?: string
|
||||
userId: string
|
||||
requestId: string
|
||||
logger: Logger
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves file input (either a file object or filePath string) to a publicly accessible URL.
|
||||
* Handles:
|
||||
* - Processing raw file input via processSingleFileToUserFile
|
||||
* - Resolving internal URLs via resolveInternalFileUrl
|
||||
* - Generating presigned URLs for storage keys
|
||||
* - Validating external URLs via validateUrlWithDNS
|
||||
*/
|
||||
export async function resolveFileInputToUrl(
|
||||
options: ResolveFileInputOptions
|
||||
): Promise<FileResolutionResult> {
|
||||
const { file, filePath, userId, requestId, logger } = options
|
||||
|
||||
if (file) {
|
||||
let userFile: UserFile
|
||||
try {
|
||||
userFile = processSingleFileToUserFile(file, requestId, logger)
|
||||
} catch (error) {
|
||||
return {
|
||||
error: {
|
||||
status: 400,
|
||||
message: error instanceof Error ? error.message : 'Failed to process file',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let fileUrl = userFile.url || ''
|
||||
|
||||
// Handle internal URLs
|
||||
if (fileUrl && isInternalFileUrl(fileUrl)) {
|
||||
const resolution = await resolveInternalFileUrl(fileUrl, userId, requestId, logger)
|
||||
if (resolution.error) {
|
||||
return { error: resolution.error }
|
||||
}
|
||||
fileUrl = resolution.fileUrl || ''
|
||||
}
|
||||
|
||||
// Generate presigned URL if we have a key but no URL
|
||||
if (!fileUrl && userFile.key) {
|
||||
const context = (userFile.context as StorageContext) || inferContextFromKey(userFile.key)
|
||||
const hasAccess = await verifyFileAccess(userFile.key, userId, undefined, context, false)
|
||||
|
||||
if (!hasAccess) {
|
||||
logger.warn(`[${requestId}] Unauthorized presigned URL generation attempt`, {
|
||||
userId,
|
||||
key: userFile.key,
|
||||
context,
|
||||
})
|
||||
return { error: { status: 404, message: 'File not found' } }
|
||||
}
|
||||
|
||||
fileUrl = await StorageService.generatePresignedDownloadUrl(userFile.key, context, 5 * 60)
|
||||
}
|
||||
|
||||
return { fileUrl }
|
||||
}
|
||||
|
||||
if (filePath) {
|
||||
let fileUrl = filePath
|
||||
|
||||
if (isInternalFileUrl(filePath)) {
|
||||
const resolution = await resolveInternalFileUrl(filePath, userId, requestId, logger)
|
||||
if (resolution.error) {
|
||||
return { error: resolution.error }
|
||||
}
|
||||
fileUrl = resolution.fileUrl || fileUrl
|
||||
} else if (filePath.startsWith('/')) {
|
||||
logger.warn(`[${requestId}] Invalid internal path`, {
|
||||
userId,
|
||||
path: filePath.substring(0, 50),
|
||||
})
|
||||
return {
|
||||
error: {
|
||||
status: 400,
|
||||
message: 'Invalid file path. Only uploaded files are supported for internal paths.',
|
||||
},
|
||||
}
|
||||
} else {
|
||||
const urlValidation = await validateUrlWithDNS(fileUrl, 'filePath')
|
||||
if (!urlValidation.isValid) {
|
||||
return { error: { status: 400, message: urlValidation.error || 'Invalid URL' } }
|
||||
}
|
||||
}
|
||||
|
||||
return { fileUrl }
|
||||
}
|
||||
|
||||
return { error: { status: 400, message: 'File input is required' } }
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a file from a URL (internal or external)
|
||||
* For internal URLs, uses direct storage access (server-side only)
|
||||
|
||||
@@ -1,9 +1,172 @@
|
||||
import type { Logger } from '@sim/logger'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { MicrosoftTeamsAttachment } from '@/tools/microsoft_teams/types'
|
||||
import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
import type { UserFile } from '@/executor/types'
|
||||
import type {
|
||||
GraphApiErrorResponse,
|
||||
GraphDriveItem,
|
||||
MicrosoftTeamsAttachment,
|
||||
} from '@/tools/microsoft_teams/types'
|
||||
import type { ToolFileData } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('MicrosoftTeamsUtils')
|
||||
|
||||
/** Maximum file size for Teams direct upload (4MB) */
|
||||
const MAX_TEAMS_FILE_SIZE = 4 * 1024 * 1024
|
||||
|
||||
/** Output format for uploaded files */
|
||||
export interface TeamsFileOutput {
|
||||
name: string
|
||||
mimeType: string
|
||||
data: string
|
||||
size: number
|
||||
}
|
||||
|
||||
/** Attachment reference for Teams message */
|
||||
export 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: unknown[]
|
||||
accessToken: string
|
||||
requestId: string
|
||||
logger: Logger
|
||||
}): Promise<TeamsFileUploadResult> {
|
||||
const { rawFiles, accessToken, requestId, logger: log } = 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)`)
|
||||
|
||||
// Download file from storage
|
||||
const buffer = await downloadFileFromStorage(file, requestId, log)
|
||||
filesOutput.push({
|
||||
name: file.name,
|
||||
mimeType: file.type || 'application/octet-stream',
|
||||
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': file.type || 'application/octet-stream',
|
||||
},
|
||||
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
|
||||
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,
|
||||
})
|
||||
|
||||
// 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 }
|
||||
}
|
||||
|
||||
interface ParsedMention {
|
||||
name: string
|
||||
fullTag: string
|
||||
|
||||
Reference in New Issue
Block a user