/** * Geofence Notification Service * Handles sending email and Telegram notifications for geofence events */ import { emailService } from './email-service'; import { telegramService } from './telegram-service'; import { renderGeofenceEnterEmail, renderGeofenceExitEmail, GeofenceEmailData, } from './email-renderer'; import { geofenceDb } from './geofence-db'; import { getUserNotificationSettings, } from './notification-settings-db'; import type { GeofenceEvent } from './types'; import Database from 'better-sqlite3'; import path from 'path'; const dbPath = path.join(process.cwd(), 'data', 'database.sqlite'); interface DeviceInfo { id: string; name: string; ownerId: string; } interface UserInfo { id: string; username: string; email: string | null; } interface GeofenceInfo { id: string; name: string; owner_id: string; } /** * Get device information from database */ function getDevice(deviceId: string): DeviceInfo | null { const db = new Database(dbPath); try { const stmt = db.prepare('SELECT id, name, ownerId FROM Device WHERE id = ?'); return stmt.get(deviceId) as DeviceInfo | null; } finally { db.close(); } } /** * Get user information from database */ function getUser(userId: string): UserInfo | null { const db = new Database(dbPath); try { const stmt = db.prepare('SELECT id, username, email FROM User WHERE id = ?'); return stmt.get(userId) as UserInfo | null; } finally { db.close(); } } /** * Send notification for a single geofence event * Supports both Email and Telegram based on user preferences */ async function sendEventNotification(event: GeofenceEvent): Promise { try { // Get geofence details const geofence = geofenceDb.findById(event.geofence_id); if (!geofence) { throw new Error(`Geofence not found: ${event.geofence_id}`); } // Get device details const device = getDevice(event.device_id); if (!device) { throw new Error(`Device not found: ${event.device_id}`); } // Get owner details const owner = getUser(geofence.owner_id); if (!owner) { throw new Error(`Owner not found: ${geofence.owner_id}`); } // Get notification preferences const settings = getUserNotificationSettings(owner.id); // Prepare notification tasks const promises: Promise[] = []; // Send Email if enabled if (settings.email_enabled && owner.email) { promises.push( sendEmailNotification(event, owner, device, geofence) ); } // Send Telegram if enabled if (settings.telegram_enabled && settings.telegram_chat_id) { promises.push( sendTelegramNotification( event, owner, device, geofence, settings.telegram_chat_id ) ); } // If no notification channel enabled, skip if (promises.length === 0) { console.log( `[GeofenceNotification] No notification channels enabled for user ${owner.id}` ); geofenceDb.markNotificationSent(event.id!, true); return; } // Send notifications in parallel const results = await Promise.allSettled(promises); // Check if at least one succeeded const anySuccess = results.some((r) => r.status === 'fulfilled'); if (anySuccess) { geofenceDb.markNotificationSent(event.id!, true); console.log( `[GeofenceNotification] Sent notification for geofence ${geofence.name}` ); } else { // All failed const errors = results .filter((r) => r.status === 'rejected') .map((r: any) => r.reason.message) .join('; '); geofenceDb.markNotificationSent(event.id!, false, errors); throw new Error(`All notification channels failed: ${errors}`); } } catch (error) { console.error('[GeofenceNotification] Failed to send notification:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; geofenceDb.markNotificationSent(event.id!, false, errorMessage); throw error; } } /** * Send email notification (extracted from main function) */ async function sendEmailNotification( event: GeofenceEvent, owner: UserInfo, device: DeviceInfo, geofence: GeofenceInfo ): Promise { try { const emailData: GeofenceEmailData = { username: owner.username, deviceName: device.name, geofenceName: geofence.name, timestamp: event.timestamp, latitude: event.latitude, longitude: event.longitude, distanceFromCenter: event.distance_from_center || 0, }; let html: string; let subject: string; if (event.event_type === 'enter') { html = await renderGeofenceEnterEmail(emailData); subject = `${device.name} hat ${geofence.name} betreten`; } else { html = await renderGeofenceExitEmail(emailData); subject = `${device.name} hat ${geofence.name} verlassen`; } await emailService['sendEmail'](owner.email!, subject, html); console.log( `[GeofenceNotification] Sent email to ${owner.email}` ); } catch (error) { console.error('[GeofenceNotification] Email error:', error); throw error; } } /** * Send Telegram notification (new function) */ async function sendTelegramNotification( event: GeofenceEvent, owner: UserInfo, device: DeviceInfo, geofence: GeofenceInfo, chatId: string ): Promise { try { const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000'; // Ensure latitude and longitude are numbers const lat = typeof event.latitude === 'number' ? event.latitude : parseFloat(event.latitude); const lon = typeof event.longitude === 'number' ? event.longitude : parseFloat(event.longitude); await telegramService.sendGeofenceNotification({ chatId, deviceName: device.name, geofenceName: geofence.name, eventType: event.event_type, latitude: lat, longitude: lon, timestamp: event.timestamp, mapUrl: `${baseUrl}/map?lat=${lat}&lon=${lon}`, dashboardUrl: `${baseUrl}/admin/geofences/events`, }); console.log( `[GeofenceNotification] Sent Telegram notification to ${chatId}` ); } catch (error) { console.error('[GeofenceNotification] Telegram error:', error); throw error; } } /** * Send notifications for multiple geofence events * Processes events sequentially to avoid overwhelming SMTP server */ export async function sendGeofenceNotifications(events: GeofenceEvent[]): Promise { if (events.length === 0) { return; } console.log(`[GeofenceNotification] Processing ${events.length} geofence event(s)`); for (const event of events) { try { await sendEventNotification(event); } catch (error) { // Log error but continue with other notifications console.error(`[GeofenceNotification] Failed to send notification for event ${event.id}:`, error); } } }