import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { type NextRequest, NextResponse } from 'next/server' import { microsoftChatsSelectorContract } from '@/lib/api/contracts/selectors/microsoft' import { parseRequest } from '@/lib/api/server' import { authorizeCredentialUse } from '@/lib/auth/credential-access' import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' import { assertGraphNextPageUrl, getGraphNextPageUrl } from '@/tools/sharepoint/utils' export const dynamic = 'force-dynamic' const logger = createLogger('TeamsChatsAPI') /** * Largest page size the `me/chats` Microsoft Graph endpoint permits via `$top`. */ const CHATS_PAGE_SIZE = 50 /** * Upper bound on Microsoft Graph pages drained when listing the user's chats. * Paging follows `@odata.nextLink`. The cap prevents an unbounded loop; hitting * it is logged as a warning. */ const MAX_CHATS_PAGES = 20 interface GraphChat { id: string topic?: string } /** * Helper function to get chat members and create a meaningful name * * @param chatId - Microsoft Teams chat ID to get display name for * @param accessToken - Access token for Microsoft Graph API * @param chatTopic - Optional existing chat topic * @returns A meaningful display name for the chat */ const getChatDisplayName = async ( chatId: string, accessToken: string, chatTopic?: string ): Promise => { try { const chatIdValidation = validateMicrosoftGraphId(chatId, 'chatId') if (!chatIdValidation.isValid) { logger.warn('Invalid chat ID in getChatDisplayName', { error: chatIdValidation.error, chatId: chatId.substring(0, 50), }) return `Chat ${chatId.substring(0, 8)}...` } if (chatTopic?.trim() && chatTopic !== 'null') { return chatTopic } const membersResponse = await fetch( `https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(chatId)}/members`, { method: 'GET', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, } ) if (membersResponse.ok) { const membersData = await membersResponse.json() const members = membersData.value || [] const memberNames = members .filter((member: any) => member.displayName && member.displayName !== 'Unknown') .map((member: any) => member.displayName) .slice(0, 3) if (memberNames.length > 0) { if (memberNames.length === 1) { return memberNames[0] } if (memberNames.length === 2) { return memberNames.join(' & ') } return `${memberNames.slice(0, 2).join(', ')} & ${memberNames.length - 2} more` } } try { const messagesResponse = await fetch( `https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(chatId)}/messages?$top=10&$orderby=createdDateTime desc`, { method: 'GET', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, } ) if (messagesResponse.ok) { const messagesData = await messagesResponse.json() const messages = messagesData.value || [] for (const message of messages) { if (message.eventDetail?.chatDisplayName) { return message.eventDetail.chatDisplayName } } const senderNames = [ ...new Set( messages .filter( (msg: any) => msg.from?.user?.displayName && msg.from.user.displayName !== 'Unknown' ) .map((msg: any) => msg.from.user.displayName) ), ].slice(0, 3) if (senderNames.length > 0) { if (senderNames.length === 1) { return senderNames[0] as string } if (senderNames.length === 2) { return senderNames.join(' & ') } return `${senderNames.slice(0, 2).join(', ')} & ${senderNames.length - 2} more` } } } catch (error) { logger.warn( `Failed to get better name from messages for chat ${chatId}: ${toError(error).message}` ) } return `Chat ${chatId.split(':')[0] || chatId.substring(0, 8)}...` } catch (error) { logger.warn(`Failed to get display name for chat ${chatId}: ${toError(error).message}`) return `Chat ${chatId.split(':')[0] || chatId.substring(0, 8)}...` } } export const POST = withRouteHandler(async (request: NextRequest) => { try { const parsed = await parseRequest(microsoftChatsSelectorContract, request, {}) if (!parsed.success) return parsed.response const { credential, workflowId } = parsed.data.body try { const authz = await authorizeCredentialUse(request, { credentialId: credential, workflowId, }) if (!authz.ok || !authz.credentialOwnerUserId) { return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 }) } const accessToken = await refreshAccessTokenIfNeeded( credential, authz.credentialOwnerUserId, 'TeamsChatsAPI' ) if (!accessToken) { logger.error('Failed to get access token', { credentialId: credential, userId: authz.credentialOwnerUserId, }) return NextResponse.json({ error: 'Could not retrieve access token' }, { status: 401 }) } const rawChats: GraphChat[] = [] let nextPageUrl: string | undefined = `https://graph.microsoft.com/v1.0/me/chats?$top=${CHATS_PAGE_SIZE}` for (let page = 0; page < MAX_CHATS_PAGES; page++) { const response = await fetch(nextPageUrl, { method: 'GET', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, }) if (!response.ok) { const errorData = await response.json() logger.error('Microsoft Graph API error getting chats', { status: response.status, error: errorData, endpoint: nextPageUrl, }) if (response.status === 401) { return NextResponse.json( { error: 'Authentication failed. Please reconnect your Microsoft Teams account.', authRequired: true, }, { status: 401 } ) } throw new Error(`Microsoft Graph API error: ${JSON.stringify(errorData)}`) } const data = await response.json() if (Array.isArray(data.value)) { rawChats.push(...(data.value as GraphChat[])) } const rawNextLink = getGraphNextPageUrl(data) if (!rawNextLink) { nextPageUrl = undefined break } nextPageUrl = assertGraphNextPageUrl(rawNextLink) if (page === MAX_CHATS_PAGES - 1) { logger.warn('Hit Microsoft Graph chats pagination cap; chat list may be incomplete', { maxPages: MAX_CHATS_PAGES, collected: rawChats.length, }) } } const chats = await Promise.all( rawChats.map(async (chat) => ({ id: chat.id, displayName: await getChatDisplayName(chat.id, accessToken, chat.topic), })) ) return NextResponse.json({ chats: chats, }) } catch (innerError) { logger.error('Error during API requests:', innerError) const errorMessage = toError(innerError).message if ( errorMessage.includes('auth') || errorMessage.includes('token') || errorMessage.includes('unauthorized') || errorMessage.includes('unauthenticated') ) { return NextResponse.json( { error: 'Authentication failed. Please reconnect your Microsoft Teams account.', authRequired: true, details: errorMessage, }, { status: 401 } ) } throw innerError } } catch (error) { logger.error('Error processing Chats request:', error) return NextResponse.json( { error: 'Failed to retrieve Microsoft Teams chats', details: (error as Error).message, }, { status: 500 } ) } })