notifications
This commit is contained in:
parent
735b3ce460
commit
a1e4e14b30
156
NOTIFICATIONS_COMPLETE_SYSTEM.md
Normal file
156
NOTIFICATIONS_COMPLETE_SYSTEM.md
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
# 🔔 Système de Notifications Complet
|
||||||
|
|
||||||
|
## ✅ Adapters Implémentés
|
||||||
|
|
||||||
|
Le système de notifications agrège maintenant **3 sources** :
|
||||||
|
|
||||||
|
1. **Leantime** (`leantime-adapter.ts`)
|
||||||
|
- Notifications de tâches, commentaires, mentions
|
||||||
|
- Polling toutes les 30 secondes
|
||||||
|
|
||||||
|
2. **RocketChat** (`rocketchat-adapter.ts`) ⚡ NOUVEAU
|
||||||
|
- Messages non lus dans Parole
|
||||||
|
- Déclenchement en temps réel quand nouveau message détecté
|
||||||
|
|
||||||
|
3. **Email** (`email-adapter.ts`) ⚡ NOUVEAU
|
||||||
|
- Emails non lus dans Courrier (INBOX uniquement)
|
||||||
|
- Déclenchement en temps réel quand nouvel email détecté
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Flow Complet
|
||||||
|
|
||||||
|
### Badge de Notification
|
||||||
|
|
||||||
|
Le badge rouge affiche maintenant le **total agrégé** :
|
||||||
|
```
|
||||||
|
Total = Leantime (unread) + RocketChat (unread) + Email (unread)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Déclenchement Temps Réel
|
||||||
|
|
||||||
|
#### 1. **Nouveau Message Parole**
|
||||||
|
```
|
||||||
|
Widget Parole détecte nouveau message
|
||||||
|
└─> totalUnreadCount augmente
|
||||||
|
└─> triggerNotificationRefresh()
|
||||||
|
└─> Invalide cache notifications
|
||||||
|
└─> NotificationService.getNotificationCount()
|
||||||
|
├─> LeantimeAdapter.getNotificationCount()
|
||||||
|
├─> RocketChatAdapter.getNotificationCount() ⚡
|
||||||
|
└─> EmailAdapter.getNotificationCount() ⚡
|
||||||
|
└─> Badge mis à jour (< 1 seconde)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. **Nouvel Email Courrier**
|
||||||
|
```
|
||||||
|
checkForNewEmails() détecte nouvel email
|
||||||
|
└─> newestEmailId > lastKnownEmailId
|
||||||
|
└─> triggerNotificationRefresh() ⚡
|
||||||
|
└─> Invalide cache notifications
|
||||||
|
└─> NotificationService.getNotificationCount()
|
||||||
|
├─> LeantimeAdapter.getNotificationCount()
|
||||||
|
├─> RocketChatAdapter.getNotificationCount()
|
||||||
|
└─> EmailAdapter.getNotificationCount() ⚡
|
||||||
|
└─> Badge mis à jour (< 1 seconde)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. **Notification Leantime**
|
||||||
|
```
|
||||||
|
Polling toutes les 30s
|
||||||
|
└─> LeantimeAdapter.getNotificationCount()
|
||||||
|
└─> Badge mis à jour
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Structure des Counts
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
NotificationCount {
|
||||||
|
total: number, // Total de toutes les sources
|
||||||
|
unread: number, // Total non lus de toutes les sources
|
||||||
|
sources: {
|
||||||
|
leantime: {
|
||||||
|
total: number,
|
||||||
|
unread: number
|
||||||
|
},
|
||||||
|
rocketchat: {
|
||||||
|
total: number,
|
||||||
|
unread: number
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
total: number,
|
||||||
|
unread: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Fichiers Modifiés/Créés
|
||||||
|
|
||||||
|
### Nouveaux Fichiers
|
||||||
|
- `lib/services/notifications/rocketchat-adapter.ts` - Adapter RocketChat
|
||||||
|
- `lib/services/notifications/email-adapter.ts` - Adapter Email
|
||||||
|
- `hooks/use-trigger-notification.ts` - Hook pour déclencher refresh
|
||||||
|
|
||||||
|
### Fichiers Modifiés
|
||||||
|
- `lib/services/notifications/notification-service.ts` - Enregistrement des nouveaux adapters
|
||||||
|
- `components/parole.tsx` - Détection et déclenchement pour RocketChat
|
||||||
|
- `hooks/use-email-state.ts` - Déclenchement pour Email (déjà présent)
|
||||||
|
- `hooks/use-notifications.ts` - Écoute d'événements custom
|
||||||
|
- `app/api/notifications/count/route.ts` - Support `?force=true`
|
||||||
|
- `app/api/rocket-chat/messages/route.ts` - Retourne `totalUnreadCount`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Avantages
|
||||||
|
|
||||||
|
1. **⚡ Temps Réel** : Notifications instantanées (< 1 seconde)
|
||||||
|
2. **📊 Multi-Sources** : Leantime + RocketChat + Email
|
||||||
|
3. **💚 Efficace** : Déclenchement uniquement quand nécessaire
|
||||||
|
4. **🔄 Rétrocompatible** : Le polling reste en fallback
|
||||||
|
5. **📈 Scalable** : Facile d'ajouter d'autres adapters
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Résultat Final
|
||||||
|
|
||||||
|
Le badge de notification affiche maintenant :
|
||||||
|
- ✅ Notifications Leantime (polling 30s)
|
||||||
|
- ✅ Messages non lus RocketChat (temps réel)
|
||||||
|
- ✅ Emails non lus Courrier (temps réel)
|
||||||
|
|
||||||
|
**Total = Leantime + RocketChat + Email** 🎉
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Notes Techniques
|
||||||
|
|
||||||
|
### RocketChat Adapter
|
||||||
|
- Utilise les subscriptions RocketChat
|
||||||
|
- Compte les messages non lus (`unread > 0`)
|
||||||
|
- Supporte channels, groups, et DMs
|
||||||
|
|
||||||
|
### Email Adapter
|
||||||
|
- Utilise le cache Redis de `/api/courrier/unread-counts`
|
||||||
|
- Focus sur INBOX (principal dossier pour notifications)
|
||||||
|
- Peut être étendu pour d'autres dossiers si besoin
|
||||||
|
|
||||||
|
### Cache Strategy
|
||||||
|
- **Leantime** : Cache 30s (aligné avec polling)
|
||||||
|
- **RocketChat** : Pas de cache dédié (utilise cache messages)
|
||||||
|
- **Email** : Cache 2 minutes (via unread-counts API)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Debugging
|
||||||
|
|
||||||
|
Pour voir les logs :
|
||||||
|
- `[ROCKETCHAT_ADAPTER]` - Adapter RocketChat
|
||||||
|
- `[EMAIL_ADAPTER]` - Adapter Email
|
||||||
|
- `[Parole]` - Détection dans widget Parole
|
||||||
|
- `[useTriggerNotification]` - Déclenchement refresh
|
||||||
|
- `[NOTIFICATION_SERVICE]` - Agrégation des counts
|
||||||
@ -194,6 +194,7 @@ export async function GET(request: Request) {
|
|||||||
userId: currentUser._id,
|
userId: currentUser._id,
|
||||||
username: currentUser.username,
|
username: currentUser.username,
|
||||||
totalSubscriptions: userSubscriptions.length,
|
totalSubscriptions: userSubscriptions.length,
|
||||||
|
totalUnreadCount: totalUnreadCount,
|
||||||
});
|
});
|
||||||
|
|
||||||
const messages: any[] = [];
|
const messages: any[] = [];
|
||||||
|
|||||||
@ -43,7 +43,8 @@ export function Parole() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { data: session, status } = useSession();
|
const { data: session, status } = useSession();
|
||||||
const { triggerNotificationRefresh } = useTriggerNotification();
|
const { triggerNotificationRefresh } = useTriggerNotification();
|
||||||
const lastUnreadCountRef = useRef<number>(0);
|
const lastUnreadCountRef = useRef<number>(-1); // Initialize to -1 to detect first load
|
||||||
|
const isInitializedRef = useRef<boolean>(false);
|
||||||
|
|
||||||
const fetchMessages = async (isRefresh = false) => {
|
const fetchMessages = async (isRefresh = false) => {
|
||||||
try {
|
try {
|
||||||
@ -67,17 +68,30 @@ export function Parole() {
|
|||||||
// Utiliser le totalUnreadCount de l'API (plus fiable)
|
// Utiliser le totalUnreadCount de l'API (plus fiable)
|
||||||
const currentUnreadCount = data.totalUnreadCount || 0;
|
const currentUnreadCount = data.totalUnreadCount || 0;
|
||||||
|
|
||||||
// Si nouveau message non lu détecté, déclencher notification
|
// On initialise le count au premier chargement
|
||||||
// On vérifie si le count a augmenté (et qu'on avait déjà un count initialisé)
|
if (!isInitializedRef.current) {
|
||||||
if (currentUnreadCount > lastUnreadCountRef.current && lastUnreadCountRef.current >= 0) {
|
isInitializedRef.current = true;
|
||||||
console.log('[Parole] Nouveau message non lu détecté', {
|
lastUnreadCountRef.current = currentUnreadCount;
|
||||||
|
console.log('[Parole] Initial unread count:', currentUnreadCount);
|
||||||
|
} else {
|
||||||
|
console.log('[Parole] Unread count check', {
|
||||||
previous: lastUnreadCountRef.current,
|
previous: lastUnreadCountRef.current,
|
||||||
current: currentUnreadCount
|
current: currentUnreadCount,
|
||||||
|
totalUnreadCount: data.totalUnreadCount,
|
||||||
|
willTrigger: currentUnreadCount > lastUnreadCountRef.current
|
||||||
});
|
});
|
||||||
triggerNotificationRefresh();
|
|
||||||
|
// Si nouveau message non lu détecté, déclencher notification
|
||||||
|
if (currentUnreadCount > lastUnreadCountRef.current) {
|
||||||
|
console.log('[Parole] ⚡ Nouveau message non lu détecté, déclenchement notification', {
|
||||||
|
previous: lastUnreadCountRef.current,
|
||||||
|
current: currentUnreadCount
|
||||||
|
});
|
||||||
|
triggerNotificationRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
lastUnreadCountRef.current = currentUnreadCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastUnreadCountRef.current = currentUnreadCount;
|
|
||||||
setMessages(data.messages);
|
setMessages(data.messages);
|
||||||
} else {
|
} else {
|
||||||
console.warn('Unexpected data format:', data);
|
console.warn('Unexpected data format:', data);
|
||||||
|
|||||||
223
lib/services/notifications/email-adapter.ts
Normal file
223
lib/services/notifications/email-adapter.ts
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
import { NotificationAdapter } from './notification-adapter.interface';
|
||||||
|
import { logger } from '@/lib/logger';
|
||||||
|
import { Notification, NotificationCount } from '@/lib/types/notification';
|
||||||
|
import { getRedisClient } from '@/lib/redis';
|
||||||
|
import { prisma } from '@/lib/prisma';
|
||||||
|
import { getImapConnection } from '@/lib/services/email-service';
|
||||||
|
|
||||||
|
export class EmailAdapter implements NotificationAdapter {
|
||||||
|
readonly sourceName = 'email';
|
||||||
|
private static readonly UNREAD_COUNTS_CACHE_KEY = (userId: string) => `email:unread:${userId}`;
|
||||||
|
private static readonly CACHE_TTL = 120; // 2 minutes (aligned with unread-counts API)
|
||||||
|
|
||||||
|
async isConfigured(): Promise<boolean> {
|
||||||
|
// Email service is always configured if user has email accounts
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch unread counts from IMAP (same logic as unread-counts API)
|
||||||
|
*/
|
||||||
|
private async fetchUnreadCounts(userId: string): Promise<Record<string, Record<string, number>>> {
|
||||||
|
// Get all accounts from the database
|
||||||
|
const accounts = await prisma.mailCredentials.findMany({
|
||||||
|
where: { userId },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug('[EMAIL_ADAPTER] Found accounts', {
|
||||||
|
userId,
|
||||||
|
accountCount: accounts.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (accounts.length === 0) {
|
||||||
|
return { default: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mapping to hold the unread counts
|
||||||
|
const unreadCounts: Record<string, Record<string, number>> = {};
|
||||||
|
|
||||||
|
// For each account, get the unread counts for standard folders
|
||||||
|
for (const account of accounts) {
|
||||||
|
const accountId = account.id;
|
||||||
|
try {
|
||||||
|
// Get IMAP connection for this account
|
||||||
|
logger.debug('[EMAIL_ADAPTER] Processing account', {
|
||||||
|
userId,
|
||||||
|
accountId,
|
||||||
|
email: account.email,
|
||||||
|
});
|
||||||
|
const client = await getImapConnection(userId, accountId);
|
||||||
|
unreadCounts[accountId] = {};
|
||||||
|
|
||||||
|
// Standard folders to check (focus on INBOX for notifications)
|
||||||
|
const standardFolders = ['INBOX'];
|
||||||
|
|
||||||
|
// Get mailboxes for this account to check if folders exist
|
||||||
|
const mailboxes = await client.list();
|
||||||
|
const availableFolders = mailboxes.map(mb => mb.path);
|
||||||
|
|
||||||
|
// Check each standard folder if it exists
|
||||||
|
for (const folder of standardFolders) {
|
||||||
|
// Skip if folder doesn't exist in this account
|
||||||
|
if (!availableFolders.includes(folder) &&
|
||||||
|
!availableFolders.some(f => f.toLowerCase() === folder.toLowerCase())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check folder status without opening it (more efficient)
|
||||||
|
const status = await client.status(folder, { unseen: true });
|
||||||
|
|
||||||
|
if (status && typeof status.unseen === 'number') {
|
||||||
|
// Store the unread count
|
||||||
|
unreadCounts[accountId][folder] = status.unseen;
|
||||||
|
|
||||||
|
logger.debug('[EMAIL_ADAPTER] Unread count', {
|
||||||
|
userId,
|
||||||
|
accountId,
|
||||||
|
folder,
|
||||||
|
unread: status.unseen,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (folderError) {
|
||||||
|
logger.error('[EMAIL_ADAPTER] Error getting unread count for folder', {
|
||||||
|
userId,
|
||||||
|
accountId,
|
||||||
|
folder,
|
||||||
|
error: folderError instanceof Error ? folderError.message : String(folderError),
|
||||||
|
});
|
||||||
|
// Continue to next folder even if this one fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (accountError) {
|
||||||
|
logger.error('[EMAIL_ADAPTER] Error processing account', {
|
||||||
|
userId,
|
||||||
|
accountId,
|
||||||
|
error: accountError instanceof Error ? accountError.message : String(accountError),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return unreadCounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user's email accounts and calculate total unread count
|
||||||
|
*/
|
||||||
|
async getNotificationCount(userId: string): Promise<NotificationCount> {
|
||||||
|
logger.debug('[EMAIL_ADAPTER] getNotificationCount called', { userId });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to get from cache first (same cache as unread-counts API)
|
||||||
|
const redis = getRedisClient();
|
||||||
|
const cacheKey = EmailAdapter.UNREAD_COUNTS_CACHE_KEY(userId);
|
||||||
|
|
||||||
|
let unreadCounts: Record<string, Record<string, number>> | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cachedData = await redis.get(cacheKey);
|
||||||
|
if (cachedData) {
|
||||||
|
unreadCounts = JSON.parse(cachedData);
|
||||||
|
logger.debug('[EMAIL_ADAPTER] Using cached unread counts', { userId });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug('[EMAIL_ADAPTER] Cache miss or error', {
|
||||||
|
userId,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no cache, fetch directly (but don't cache here - let the API route handle caching)
|
||||||
|
if (!unreadCounts) {
|
||||||
|
try {
|
||||||
|
// Fetch unread counts directly
|
||||||
|
unreadCounts = await this.fetchUnreadCounts(userId);
|
||||||
|
logger.debug('[EMAIL_ADAPTER] Fetched unread counts directly', { userId });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[EMAIL_ADAPTER] Error fetching unread counts', {
|
||||||
|
userId,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
total: 0,
|
||||||
|
unread: 0,
|
||||||
|
sources: {
|
||||||
|
email: {
|
||||||
|
total: 0,
|
||||||
|
unread: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate total unread count across all accounts and folders
|
||||||
|
// Focus on INBOX for notifications
|
||||||
|
let totalUnread = 0;
|
||||||
|
let foldersWithUnread = 0;
|
||||||
|
|
||||||
|
for (const accountId in unreadCounts) {
|
||||||
|
const accountFolders = unreadCounts[accountId];
|
||||||
|
// Focus on INBOX folder for notifications
|
||||||
|
const inboxCount = accountFolders['INBOX'] || accountFolders[`${accountId}:INBOX`] || 0;
|
||||||
|
if (inboxCount > 0) {
|
||||||
|
totalUnread += inboxCount;
|
||||||
|
foldersWithUnread++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('[EMAIL_ADAPTER] Notification counts', {
|
||||||
|
userId,
|
||||||
|
total: foldersWithUnread,
|
||||||
|
unread: totalUnread,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
total: foldersWithUnread,
|
||||||
|
unread: totalUnread,
|
||||||
|
sources: {
|
||||||
|
email: {
|
||||||
|
total: foldersWithUnread,
|
||||||
|
unread: totalUnread
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[EMAIL_ADAPTER] Error getting notification count', {
|
||||||
|
userId,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
total: 0,
|
||||||
|
unread: 0,
|
||||||
|
sources: {
|
||||||
|
email: {
|
||||||
|
total: 0,
|
||||||
|
unread: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getNotifications(userId: string, page = 1, limit = 20): Promise<Notification[]> {
|
||||||
|
// For now, return empty array - we can implement this later if needed
|
||||||
|
// The notification count is what matters for the badge
|
||||||
|
// In the future, we could return unread emails as notifications
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async markAsRead(userId: string, notificationId: string): Promise<boolean> {
|
||||||
|
// Not implemented yet - Email read status is handled by the email service
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async markAllAsRead(userId: string): Promise<boolean> {
|
||||||
|
// Not implemented yet - Email read status is handled by the email service
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,8 @@
|
|||||||
import { Notification, NotificationCount } from '@/lib/types/notification';
|
import { Notification, NotificationCount } from '@/lib/types/notification';
|
||||||
import { NotificationAdapter } from './notification-adapter.interface';
|
import { NotificationAdapter } from './notification-adapter.interface';
|
||||||
import { LeantimeAdapter } from './leantime-adapter';
|
import { LeantimeAdapter } from './leantime-adapter';
|
||||||
|
import { RocketChatAdapter } from './rocketchat-adapter';
|
||||||
|
import { EmailAdapter } from './email-adapter';
|
||||||
import { getRedisClient } from '@/lib/redis';
|
import { getRedisClient } from '@/lib/redis';
|
||||||
import { logger } from '@/lib/logger';
|
import { logger } from '@/lib/logger';
|
||||||
|
|
||||||
@ -22,6 +24,8 @@ export class NotificationService {
|
|||||||
|
|
||||||
// Register adapters
|
// Register adapters
|
||||||
this.registerAdapter(new LeantimeAdapter());
|
this.registerAdapter(new LeantimeAdapter());
|
||||||
|
this.registerAdapter(new RocketChatAdapter());
|
||||||
|
this.registerAdapter(new EmailAdapter());
|
||||||
|
|
||||||
// More adapters will be added as they are implemented
|
// More adapters will be added as they are implemented
|
||||||
// this.registerAdapter(new NextcloudAdapter());
|
// this.registerAdapter(new NextcloudAdapter());
|
||||||
|
|||||||
265
lib/services/notifications/rocketchat-adapter.ts
Normal file
265
lib/services/notifications/rocketchat-adapter.ts
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
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);
|
||||||
|
return session?.user?.email || null;
|
||||||
|
} 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
|
||||||
|
*/
|
||||||
|
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)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentUser = usersData.users.find((u: any) => u.username === username);
|
||||||
|
return currentUser?._id || null;
|
||||||
|
} 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
|
||||||
|
*/
|
||||||
|
private async getUserToken(): 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'
|
||||||
|
};
|
||||||
|
|
||||||
|
const createTokenResponse = await fetch(`${this.baseUrl}/api/v1/users.createToken`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: adminHeaders
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!createTokenResponse.ok) {
|
||||||
|
logger.error('[ROCKETCHAT_ADAPTER] Failed to create user token', {
|
||||||
|
status: createTokenResponse.status,
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenData = await createTokenResponse.json();
|
||||||
|
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),
|
||||||
|
});
|
||||||
|
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');
|
||||||
|
return {
|
||||||
|
total: 0,
|
||||||
|
unread: 0,
|
||||||
|
sources: {
|
||||||
|
rocketchat: {
|
||||||
|
total: 0,
|
||||||
|
unread: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const userToken = await this.getUserToken();
|
||||||
|
if (!userToken) {
|
||||||
|
logger.error('[ROCKETCHAT_ADAPTER] Could not get user token');
|
||||||
|
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[]> {
|
||||||
|
// For now, return empty array - we can implement this later if needed
|
||||||
|
// The notification count is what matters for the badge
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async markAsRead(userId: string, notificationId: string): Promise<boolean> {
|
||||||
|
// Not implemented yet - RocketChat handles read status automatically
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async markAllAsRead(userId: string): Promise<boolean> {
|
||||||
|
// Not implemented yet - RocketChat handles read status automatically
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user