import { NotificationAdapter } from './notification-adapter.interface'; import { getServerSession } from 'next-auth'; import { authOptions } from "@/app/api/auth/options"; import { logger } from '@/lib/logger'; import { Notification, NotificationCount } from '@/lib/types/notification'; export class RocketChatAdapter implements NotificationAdapter { readonly sourceName = 'rocketchat'; private baseUrl: string; constructor() { this.baseUrl = process.env.NEXT_PUBLIC_IFRAME_PAROLE_URL?.split('/channel')[0] || ''; logger.debug('[ROCKETCHAT_ADAPTER] Initialized', { hasBaseUrl: !!this.baseUrl, }); } async isConfigured(): Promise { return !!this.baseUrl && !!process.env.ROCKET_CHAT_TOKEN && !!process.env.ROCKET_CHAT_USER_ID; } /** * Get user's email from session */ private async getUserEmail(): Promise { try { const session = await getServerSession(authOptions); const email = session?.user?.email || null; logger.debug('[ROCKETCHAT_ADAPTER] getUserEmail', { hasSession: !!session, hasEmail: !!email, emailHash: email ? Buffer.from(email.toLowerCase()).toString('base64').slice(0, 12) : null, }); return email; } catch (error) { logger.error('[ROCKETCHAT_ADAPTER] Error getting user email', { error: error instanceof Error ? error.message : String(error), }); return null; } } /** * Get RocketChat user ID from email * Uses the same logic as the messages API: search by username OR email */ private async getRocketChatUserId(email: string): Promise { try { const username = email.split('@')[0]; if (!username) return null; const adminHeaders = { 'X-Auth-Token': process.env.ROCKET_CHAT_TOKEN!, 'X-User-Id': process.env.ROCKET_CHAT_USER_ID!, 'Content-Type': 'application/json' }; const usersResponse = await fetch(`${this.baseUrl}/api/v1/users.list`, { method: 'GET', headers: adminHeaders }); if (!usersResponse.ok) { logger.error('[ROCKETCHAT_ADAPTER] Failed to get users list', { status: usersResponse.status, }); return null; } const usersData = await usersResponse.json(); if (!usersData.success || !Array.isArray(usersData.users)) { logger.error('[ROCKETCHAT_ADAPTER] Invalid users list response', { success: usersData.success, hasUsers: Array.isArray(usersData.users), }); return null; } logger.debug('[ROCKETCHAT_ADAPTER] Searching for user', { username, email, totalUsers: usersData.users.length, }); // Use the exact same logic as the messages API const currentUser = usersData.users.find((user: any) => user.username === username || user.emails?.some((e: any) => e.address === email) ); if (!currentUser) { logger.warn('[ROCKETCHAT_ADAPTER] User not found in RocketChat', { username, email, searchedIn: usersData.users.length, availableUsernames: usersData.users.slice(0, 5).map((u: any) => u.username), }); return null; } logger.debug('[ROCKETCHAT_ADAPTER] Found user', { username, email, rocketChatUsername: currentUser.username, userId: currentUser._id, }); return currentUser._id; } catch (error) { logger.error('[ROCKETCHAT_ADAPTER] Error getting RocketChat user ID', { error: error instanceof Error ? error.message : String(error), }); return null; } } /** * Get user token for RocketChat API * Creates a token for the specified RocketChat user ID */ private async getUserToken(rocketChatUserId: string): Promise<{ authToken: string; userId: string } | null> { try { const adminHeaders = { 'X-Auth-Token': process.env.ROCKET_CHAT_TOKEN!, 'X-User-Id': process.env.ROCKET_CHAT_USER_ID!, 'Content-Type': 'application/json' }; // Create token for the specific user // RocketChat 8.0.2+ requires a 'secret' parameter const secret = process.env.ROCKET_CHAT_CREATE_TOKEN_SECRET; if (!secret) { logger.error('[ROCKETCHAT_ADAPTER] ROCKET_CHAT_CREATE_TOKEN_SECRET is not configured'); return null; } const createTokenResponse = await fetch(`${this.baseUrl}/api/v1/users.createToken`, { method: 'POST', headers: adminHeaders, body: JSON.stringify({ userId: rocketChatUserId, secret: secret }) }); if (!createTokenResponse.ok) { logger.error('[ROCKETCHAT_ADAPTER] Failed to create user token', { status: createTokenResponse.status, rocketChatUserId, }); return null; } const tokenData = await createTokenResponse.json(); if (!tokenData.success || !tokenData.data) { logger.error('[ROCKETCHAT_ADAPTER] Invalid token response', { response: tokenData, }); return null; } logger.debug('[ROCKETCHAT_ADAPTER] User token created', { rocketChatUserId, tokenUserId: tokenData.data.userId, }); return { authToken: tokenData.data.authToken, userId: tokenData.data.userId }; } catch (error) { logger.error('[ROCKETCHAT_ADAPTER] Error getting user token', { error: error instanceof Error ? error.message : String(error), rocketChatUserId, }); return null; } } async getNotificationCount(userId: string): Promise { logger.debug('[ROCKETCHAT_ADAPTER] getNotificationCount called', { userId }); try { const email = await this.getUserEmail(); if (!email) { logger.error('[ROCKETCHAT_ADAPTER] Could not get user email'); return { total: 0, unread: 0, sources: { rocketchat: { total: 0, unread: 0 } } }; } const rocketChatUserId = await this.getRocketChatUserId(email); if (!rocketChatUserId) { logger.debug('[ROCKETCHAT_ADAPTER] User not found in RocketChat', { emailHash: email ? Buffer.from(email.toLowerCase()).toString('base64').slice(0, 12) : null, }); return { total: 0, unread: 0, sources: { rocketchat: { total: 0, unread: 0 } } }; } logger.debug('[ROCKETCHAT_ADAPTER] Found RocketChat user', { rocketChatUserId, emailHash: Buffer.from(email.toLowerCase()).toString('base64').slice(0, 12), }); const userToken = await this.getUserToken(rocketChatUserId); if (!userToken) { logger.error('[ROCKETCHAT_ADAPTER] Could not get user token', { rocketChatUserId, }); return { total: 0, unread: 0, sources: { rocketchat: { total: 0, unread: 0 } } }; } const userHeaders = { 'X-Auth-Token': userToken.authToken, 'X-User-Id': userToken.userId, 'Content-Type': 'application/json' }; // Get user's subscriptions const subscriptionsResponse = await fetch(`${this.baseUrl}/api/v1/subscriptions.get`, { method: 'GET', headers: userHeaders }); if (!subscriptionsResponse.ok) { logger.error('[ROCKETCHAT_ADAPTER] Failed to get subscriptions', { status: subscriptionsResponse.status, }); return { total: 0, unread: 0, sources: { rocketchat: { total: 0, unread: 0 } } }; } const subscriptionsData = await subscriptionsResponse.json(); if (!subscriptionsData.success || !Array.isArray(subscriptionsData.update)) { logger.error('[ROCKETCHAT_ADAPTER] Invalid subscriptions response'); return { total: 0, unread: 0, sources: { rocketchat: { total: 0, unread: 0 } } }; } // Filter subscriptions with unread messages const userSubscriptions = subscriptionsData.update.filter((sub: any) => { return (sub.unread > 0 || sub.alert) && ['d', 'c', 'p'].includes(sub.t); }); // Calculate total unread count const totalUnreadCount = userSubscriptions.reduce((sum: number, sub: any) => sum + (sub.unread || 0), 0); logger.debug('[ROCKETCHAT_ADAPTER] Notification counts', { total: userSubscriptions.length, unread: totalUnreadCount, }); return { total: userSubscriptions.length, unread: totalUnreadCount, sources: { rocketchat: { total: userSubscriptions.length, unread: totalUnreadCount } } }; } catch (error) { logger.error('[ROCKETCHAT_ADAPTER] Error getting notification count', { error: error instanceof Error ? error.message : String(error), }); return { total: 0, unread: 0, sources: { rocketchat: { total: 0, unread: 0 } } }; } } async getNotifications(userId: string, page = 1, limit = 20): Promise { logger.debug('[ROCKETCHAT_ADAPTER] getNotifications called', { userId, page, limit }); try { const email = await this.getUserEmail(); if (!email) { logger.error('[ROCKETCHAT_ADAPTER] Could not get user email'); return []; } const rocketChatUserId = await this.getRocketChatUserId(email); if (!rocketChatUserId) { logger.debug('[ROCKETCHAT_ADAPTER] User not found in RocketChat'); return []; } const userToken = await this.getUserToken(rocketChatUserId); if (!userToken) { logger.error('[ROCKETCHAT_ADAPTER] Could not get user token'); return []; } const userHeaders = { 'X-Auth-Token': userToken.authToken, 'X-User-Id': userToken.userId, 'Content-Type': 'application/json' }; // Get user's subscriptions with unread messages const subscriptionsResponse = await fetch(`${this.baseUrl}/api/v1/subscriptions.get`, { method: 'GET', headers: userHeaders }); if (!subscriptionsResponse.ok) { logger.error('[ROCKETCHAT_ADAPTER] Failed to get subscriptions', { status: subscriptionsResponse.status, }); return []; } const subscriptionsData = await subscriptionsResponse.json(); if (!subscriptionsData.success || !Array.isArray(subscriptionsData.update)) { logger.error('[ROCKETCHAT_ADAPTER] Invalid subscriptions response'); return []; } // Filter subscriptions with unread messages const userSubscriptions = subscriptionsData.update.filter((sub: any) => { return (sub.unread > 0 || sub.alert) && ['d', 'c', 'p'].includes(sub.t); }); // Get user info for comparison const usersResponse = await fetch(`${this.baseUrl}/api/v1/users.list`, { method: 'GET', headers: { 'X-Auth-Token': process.env.ROCKET_CHAT_TOKEN!, 'X-User-Id': process.env.ROCKET_CHAT_USER_ID!, 'Content-Type': 'application/json' } }); let currentUser: any = null; if (usersResponse.ok) { const usersData = await usersResponse.json(); if (usersData.success && Array.isArray(usersData.users)) { const username = email.split('@')[0]; currentUser = usersData.users.find((u: any) => u.username === username || u.emails?.some((e: any) => e.address === email) ); } } const notifications: Notification[] = []; // Fetch messages for each subscription with unread messages for (const subscription of userSubscriptions) { try { // Determine the correct endpoint based on room type let endpoint; switch (subscription.t) { case 'c': endpoint = 'channels.messages'; break; case 'p': endpoint = 'groups.messages'; break; case 'd': endpoint = 'im.messages'; break; default: continue; } const queryParams = new URLSearchParams({ roomId: subscription.rid, count: String(Math.max(subscription.unread, 1)) // Fetch at least 1 message }); const messagesResponse = await fetch( `${this.baseUrl}/api/v1/${endpoint}?${queryParams}`, { method: 'GET', headers: userHeaders } ); if (!messagesResponse.ok) { logger.error('[ROCKETCHAT_ADAPTER] Failed to get messages for room', { roomName: subscription.name, status: messagesResponse.status, }); continue; } const messageData = await messagesResponse.json(); if (messageData.success && messageData.messages?.length > 0) { // Get the latest unread message (skip own messages) const unreadMessages = messageData.messages.filter((msg: any) => { // Skip messages sent by current user if (currentUser && msg.u?._id === currentUser._id) { return false; } // Skip system messages if (msg.t || !msg.msg) { return false; } return true; }); if (unreadMessages.length > 0) { const latestMessage = unreadMessages[0]; const messageUser = latestMessage.u || {}; const roomName = subscription.fname || subscription.name || subscription.name; // Determine room type for link let roomTypePath = 'channel'; if (subscription.t === 'd') roomTypePath = 'direct'; else if (subscription.t === 'p') roomTypePath = 'group'; // Format title similar to Leantime (simple and consistent) const title = subscription.t === 'd' ? 'Message' : subscription.t === 'p' ? 'Message de groupe' : 'Message de canal'; // Format message with sender and room info const senderName = messageUser.name || messageUser.username || 'Utilisateur'; const formattedMessage = subscription.t === 'd' ? `${senderName}: ${latestMessage.msg || ''}` : `${senderName} dans ${roomName}: ${latestMessage.msg || ''}`; // Link to hub.slm-lab.net/parole instead of external RocketChat URL const notification: Notification = { id: `rocketchat-${latestMessage._id}`, source: 'rocketchat', sourceId: latestMessage._id, type: 'message', title: title, message: formattedMessage, link: `/parole`, isRead: false, // All messages here are unread timestamp: new Date(latestMessage.ts), priority: subscription.alert ? 'high' : 'normal', user: { id: messageUser._id || '', name: senderName, }, metadata: { roomId: subscription.rid, roomName: roomName, roomType: subscription.t, unreadCount: subscription.unread, } }; notifications.push(notification); } } } catch (error) { logger.error('[ROCKETCHAT_ADAPTER] Error fetching messages for room', { roomName: subscription.name, error: error instanceof Error ? error.message : String(error), }); continue; } } // Sort by timestamp (newest first) and apply pagination notifications.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); const startIndex = (page - 1) * limit; const endIndex = startIndex + limit; const paginatedNotifications = notifications.slice(startIndex, endIndex); logger.debug('[ROCKETCHAT_ADAPTER] getNotifications result', { total: notifications.length, returned: paginatedNotifications.length, page, limit, }); return paginatedNotifications; } catch (error) { logger.error('[ROCKETCHAT_ADAPTER] Error getting notifications', { error: error instanceof Error ? error.message : String(error), }); return []; } } }