538 lines
17 KiB
TypeScript
538 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
|
|
// 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<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 [];
|
|
}
|
|
}
|
|
|
|
}
|