NeahStable/lib/services/notifications/rocketchat-adapter.ts
2026-01-11 22:56:25 +01:00

549 lines
17 KiB
TypeScript

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<boolean> {
return !!this.baseUrl &&
!!process.env.ROCKET_CHAT_TOKEN &&
!!process.env.ROCKET_CHAT_USER_ID;
}
/**
* Get user's email from session
*/
private async getUserEmail(): Promise<string | null> {
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<string | null> {
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
const createTokenResponse = await fetch(`${this.baseUrl}/api/v1/users.createToken`, {
method: 'POST',
headers: adminHeaders,
body: JSON.stringify({
userId: rocketChatUserId
})
});
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<NotificationCount> {
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<Notification[]> {
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 [];
}
}
async markAsRead(userId: string, notificationId: string): Promise<boolean> {
// RocketChat handles read status automatically when messages are viewed
// We return true to acknowledge the UI action, but the actual read status
// will be updated when the user views the message in RocketChat
logger.debug('[ROCKETCHAT_ADAPTER] markAsRead called (read status handled by RocketChat)', {
userId,
notificationId,
});
return true;
}
async markAllAsRead(userId: string): Promise<boolean> {
// RocketChat handles read status automatically when messages are viewed
// We return true to acknowledge the UI action
logger.debug('[ROCKETCHAT_ADAPTER] markAllAsRead called (read status handled by RocketChat)', {
userId,
});
return true;
}
}