Add Telegram notification integration for geofencing
Features: - Multi-channel notifications (Email + Telegram) - User-configurable notification settings per channel - Telegram bot integration with rich messages, location pins, and inline buttons - QR code generation for easy bot access (@myidbot support) - Admin UI for notification settings management - Test functionality for Telegram connection - Comprehensive documentation Implementation: - lib/telegram-service.ts: Telegram API integration - lib/notification-settings-db.ts: Database layer for user notification preferences - lib/geofence-notifications.ts: Extended for parallel multi-channel delivery - API routes for settings management and testing - Admin UI with QR code display and step-by-step instructions - Database table: UserNotificationSettings Documentation: - docs/telegram.md: Technical implementation guide - docs/telegram-anleitung.md: User guide with @myidbot instructions - docs/telegram-setup.md: Admin setup guide - README.md: Updated NPM scripts section Docker: - Updated Dockerfile to copy public directory - Added TELEGRAM_BOT_TOKEN environment variable - Integrated notification settings initialization in db:init 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,15 +1,19 @@
|
||||
/**
|
||||
* Geofence Notification Service
|
||||
* Handles sending email notifications for geofence events
|
||||
* 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';
|
||||
@@ -62,6 +66,7 @@ function getUser(userId: string): UserInfo | null {
|
||||
|
||||
/**
|
||||
* Send notification for a single geofence event
|
||||
* Supports both Email and Telegram based on user preferences
|
||||
*/
|
||||
async function sendEventNotification(event: GeofenceEvent): Promise<void> {
|
||||
try {
|
||||
@@ -79,13 +84,87 @@ async function sendEventNotification(event: GeofenceEvent): Promise<void> {
|
||||
|
||||
// Get owner details
|
||||
const owner = getUser(geofence.owner_id);
|
||||
if (!owner || !owner.email) {
|
||||
console.log(`[GeofenceNotification] No email for owner ${geofence.owner_id}, skipping notification`);
|
||||
geofenceDb.markNotificationSent(event.id!, true); // Mark as "sent" (no email needed)
|
||||
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<void>[] = [];
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Prepare email data
|
||||
// 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<void> {
|
||||
try {
|
||||
const emailData: GeofenceEmailData = {
|
||||
username: owner.username,
|
||||
deviceName: device.name,
|
||||
@@ -94,11 +173,8 @@ async function sendEventNotification(event: GeofenceEvent): Promise<void> {
|
||||
latitude: event.latitude,
|
||||
longitude: event.longitude,
|
||||
distanceFromCenter: event.distance_from_center || 0,
|
||||
// Optional: Add map URL later
|
||||
// mapUrl: `${process.env.NEXT_PUBLIC_URL}/map?lat=${event.latitude}&lon=${event.longitude}`
|
||||
};
|
||||
|
||||
// Render and send email
|
||||
let html: string;
|
||||
let subject: string;
|
||||
|
||||
@@ -110,20 +186,51 @@ async function sendEventNotification(event: GeofenceEvent): Promise<void> {
|
||||
subject = `${device.name} hat ${geofence.name} verlassen`;
|
||||
}
|
||||
|
||||
// Send via existing email service
|
||||
await emailService['sendEmail'](owner.email, subject, html);
|
||||
await emailService['sendEmail'](owner.email!, subject, html);
|
||||
|
||||
// Mark notification as sent
|
||||
geofenceDb.markNotificationSent(event.id!, true);
|
||||
|
||||
console.log(`[GeofenceNotification] Sent ${event.event_type} notification for geofence ${geofence.name} to ${owner.email}`);
|
||||
console.log(
|
||||
`[GeofenceNotification] Sent email to ${owner.email}`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('[GeofenceNotification] Failed to send notification:', error);
|
||||
console.error('[GeofenceNotification] Email error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Mark notification as failed
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
geofenceDb.markNotificationSent(event.id!, false, errorMessage);
|
||||
/**
|
||||
* Send Telegram notification (new function)
|
||||
*/
|
||||
async function sendTelegramNotification(
|
||||
event: GeofenceEvent,
|
||||
owner: UserInfo,
|
||||
device: DeviceInfo,
|
||||
geofence: GeofenceInfo,
|
||||
chatId: string
|
||||
): Promise<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user