# Telegram Notifications Implementation Guide ## πŸ“‹ Overview This document describes how to implement Telegram notifications for the geofencing system, allowing users to receive geofence alerts via Telegram instead of or in addition to email notifications. ## 🎯 Features - βœ… **Text notifications** - Rich formatted messages with device and geofence info - βœ… **Location pins** - Telegram map with exact coordinates where event occurred - βœ… **Inline buttons** - Interactive buttons to view on map or open dashboard - βœ… **User-configurable** - Each user can choose: Email, Telegram, both, or neither - βœ… **Parallel execution** - Email and Telegram sent simultaneously for speed - βœ… **Robust** - If one channel fails, the other still works ## πŸ“ Architecture ``` Geofence Event ↓ geofence-notifications.ts ↓ getUserNotificationSettings(userId) ↓ β”œβ”€β†’ Email Service (if email_enabled && email exists) └─→ Telegram Service (if telegram_enabled && chat_id exists) ↓ Send Text + Location Pin + Inline Buttons ``` ## πŸ—„οΈ Database Changes ### Option A: Extend User Table ```sql -- Add to existing User table ALTER TABLE User ADD COLUMN telegram_chat_id TEXT; ALTER TABLE User ADD COLUMN notification_email INTEGER DEFAULT 1; ALTER TABLE User ADD COLUMN notification_telegram INTEGER DEFAULT 0; ``` ### Option B: Separate Settings Table (Recommended) ```sql -- Create dedicated notification settings table CREATE TABLE UserNotificationSettings ( user_id TEXT PRIMARY KEY, email_enabled INTEGER DEFAULT 1, telegram_enabled INTEGER DEFAULT 0, telegram_chat_id TEXT, created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')), FOREIGN KEY (user_id) REFERENCES User(id) ON DELETE CASCADE, CHECK (email_enabled IN (0, 1)), CHECK (telegram_enabled IN (0, 1)) ); -- Create index for performance CREATE INDEX idx_user_notification_settings_user ON UserNotificationSettings(user_id); ``` **Why Option B is better:** - Cleaner separation of concerns - Easier to add more notification channels later (Push, SMS, etc.) - Can track settings changes history - Doesn't pollute User table ## πŸ“ Implementation Steps ### 1. Create Telegram Service **File:** `lib/telegram-service.ts` ```typescript import axios from 'axios'; interface TelegramMessage { chatId: string; text: string; latitude?: number; longitude?: number; buttons?: Array<{ text: string; url: string }>; } interface GeofenceNotificationParams { chatId: string; deviceName: string; geofenceName: string; eventType: 'enter' | 'exit'; latitude: number; longitude: number; timestamp: string; mapUrl?: string; dashboardUrl?: string; } class TelegramService { private botToken: string; private baseUrl: string; constructor() { this.botToken = process.env.TELEGRAM_BOT_TOKEN || ''; if (!this.botToken) { console.warn('[TelegramService] No TELEGRAM_BOT_TOKEN configured'); } this.baseUrl = `https://api.telegram.org/bot${this.botToken}`; } /** * Check if Telegram is configured */ isConfigured(): boolean { return !!this.botToken; } /** * Send text message with optional inline buttons */ async sendMessage(params: TelegramMessage): Promise { if (!this.isConfigured()) { throw new Error('Telegram bot token not configured'); } const { chatId, text, buttons } = params; const payload: any = { chat_id: chatId, text: text, parse_mode: 'HTML', }; // Add inline buttons if provided if (buttons && buttons.length > 0) { payload.reply_markup = { inline_keyboard: [ buttons.map(btn => ({ text: btn.text, url: btn.url, })), ], }; } const response = await axios.post(`${this.baseUrl}/sendMessage`, payload); if (!response.data.ok) { throw new Error(`Telegram API error: ${response.data.description}`); } } /** * Send location pin on map */ async sendLocation( chatId: string, latitude: number, longitude: number ): Promise { if (!this.isConfigured()) { throw new Error('Telegram bot token not configured'); } const response = await axios.post(`${this.baseUrl}/sendLocation`, { chat_id: chatId, latitude, longitude, }); if (!response.data.ok) { throw new Error(`Telegram API error: ${response.data.description}`); } } /** * Send geofence notification (complete with text + location + buttons) */ async sendGeofenceNotification( params: GeofenceNotificationParams ): Promise { const { chatId, deviceName, geofenceName, eventType, latitude, longitude, timestamp, mapUrl, dashboardUrl, } = params; // Format message const emoji = eventType === 'enter' ? '🟒' : 'πŸ”΄'; const action = eventType === 'enter' ? 'BETRETEN' : 'VERLASSEN'; const verb = eventType === 'enter' ? 'betreten' : 'verlassen'; const formattedDate = new Date(timestamp).toLocaleString('de-DE', { dateStyle: 'short', timeStyle: 'short', }); const text = ` ${emoji} Geofence ${action} πŸ“± Device: ${deviceName} πŸ“ Geofence: ${geofenceName} πŸ• Zeit: ${formattedDate} πŸ“Š Ereignis: Hat ${geofenceName} ${verb} `.trim(); // Prepare inline buttons const buttons = []; if (mapUrl) { buttons.push({ text: 'πŸ—ΊοΈ Auf Karte zeigen', url: mapUrl }); } if (dashboardUrl) { buttons.push({ text: 'πŸ“Š Dashboard ΓΆffnen', url: dashboardUrl }); } // Send text message with buttons await this.sendMessage({ chatId, text, buttons }); // Send location pin await this.sendLocation(chatId, latitude, longitude); } /** * Test connection by sending a simple message */ async testConnection(chatId: string): Promise { try { await this.sendMessage({ chatId, text: 'βœ… Telegram Connection Test\n\nDie Verbindung funktioniert!', }); return true; } catch (error) { console.error('[TelegramService] Test failed:', error); return false; } } } export const telegramService = new TelegramService(); ``` ### 2. Create Notification Settings Database Layer **File:** `lib/notification-settings-db.ts` ```typescript import Database from 'better-sqlite3'; import path from 'path'; const dbPath = path.join(process.cwd(), 'data', 'database.sqlite'); export interface UserNotificationSettings { user_id: string; email_enabled: boolean; telegram_enabled: boolean; telegram_chat_id: string | null; created_at?: string; updated_at?: string; } /** * Get notification settings for a user * Returns default settings if not found */ export function getUserNotificationSettings( userId: string ): UserNotificationSettings { const db = new Database(dbPath); try { const stmt = db.prepare(` SELECT user_id, email_enabled, telegram_enabled, telegram_chat_id, created_at, updated_at FROM UserNotificationSettings WHERE user_id = ? `); const result = stmt.get(userId) as UserNotificationSettings | undefined; // Return defaults if no settings found if (!result) { return { user_id: userId, email_enabled: true, telegram_enabled: false, telegram_chat_id: null, }; } return result; } finally { db.close(); } } /** * Update notification settings for a user */ export function updateUserNotificationSettings( userId: string, settings: Partial ): UserNotificationSettings { const db = new Database(dbPath); try { // Check if settings exist const existing = db .prepare('SELECT user_id FROM UserNotificationSettings WHERE user_id = ?') .get(userId); if (existing) { // Update existing const updates: string[] = []; const values: any[] = []; if (settings.email_enabled !== undefined) { updates.push('email_enabled = ?'); values.push(settings.email_enabled ? 1 : 0); } if (settings.telegram_enabled !== undefined) { updates.push('telegram_enabled = ?'); values.push(settings.telegram_enabled ? 1 : 0); } if (settings.telegram_chat_id !== undefined) { updates.push('telegram_chat_id = ?'); values.push(settings.telegram_chat_id); } updates.push('updated_at = datetime(\'now\')'); values.push(userId); const stmt = db.prepare(` UPDATE UserNotificationSettings SET ${updates.join(', ')} WHERE user_id = ? `); stmt.run(...values); } else { // Insert new const stmt = db.prepare(` INSERT INTO UserNotificationSettings ( user_id, email_enabled, telegram_enabled, telegram_chat_id ) VALUES (?, ?, ?, ?) `); stmt.run( userId, settings.email_enabled ?? 1, settings.telegram_enabled ?? 0, settings.telegram_chat_id ?? null ); } return getUserNotificationSettings(userId); } finally { db.close(); } } /** * Initialize notification settings table */ export function initNotificationSettingsTable(): void { const db = new Database(dbPath); try { db.exec(` CREATE TABLE IF NOT EXISTS UserNotificationSettings ( user_id TEXT PRIMARY KEY, email_enabled INTEGER DEFAULT 1, telegram_enabled INTEGER DEFAULT 0, telegram_chat_id TEXT, created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')), FOREIGN KEY (user_id) REFERENCES User(id) ON DELETE CASCADE, CHECK (email_enabled IN (0, 1)), CHECK (telegram_enabled IN (0, 1)) ); CREATE INDEX IF NOT EXISTS idx_user_notification_settings_user ON UserNotificationSettings(user_id); `); console.log('βœ“ UserNotificationSettings table initialized'); } finally { db.close(); } } ``` ### 3. Update Geofence Notifications Handler **File:** `lib/geofence-notifications.ts` (extend existing) ```typescript import { emailService } from './email-service'; import { telegramService } from './telegram-service'; import { getUserNotificationSettings, UserNotificationSettings, } from './notification-settings-db'; import { renderGeofenceEnterEmail, renderGeofenceExitEmail, GeofenceEmailData, } from './email-renderer'; // ... existing imports ... /** * 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 (existing function - extracted) */ 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'; await telegramService.sendGeofenceNotification({ chatId, deviceName: device.name, geofenceName: geofence.name, eventType: event.event_type, latitude: event.latitude, longitude: event.longitude, timestamp: event.timestamp, mapUrl: `${baseUrl}/map?lat=${event.latitude}&lon=${event.longitude}`, dashboardUrl: `${baseUrl}/admin/geofences/events`, }); console.log( `[GeofenceNotification] Sent Telegram notification to ${chatId}` ); } catch (error) { console.error('[GeofenceNotification] Telegram error:', error); throw error; } } // Export for other modules export { sendEventNotification }; ``` ### 4. Create Database Init Script **File:** `scripts/init-notification-settings.js` ```javascript #!/usr/bin/env node /** * Initialize UserNotificationSettings table */ const { initNotificationSettingsTable } = require('../lib/notification-settings-db'); try { initNotificationSettingsTable(); console.log('βœ… Notification settings table initialized!'); } catch (error) { console.error('❌ Error:', error.message); process.exit(1); } ``` ### 5. Add API Routes for Settings **File:** `app/api/users/[id]/notification-settings/route.ts` ```typescript import { NextResponse } from 'next/server'; import { auth } from '@/lib/auth'; import { getUserNotificationSettings, updateUserNotificationSettings, } from '@/lib/notification-settings-db'; // GET /api/users/[id]/notification-settings export async function GET( request: Request, { params }: { params: Promise<{ id: string }> } ) { try { const session = await auth(); if (!session?.user) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const { id: userId } = await params; const currentUserId = (session.user as any).id; // Users can only view their own settings if (userId !== currentUserId) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); } const settings = getUserNotificationSettings(userId); return NextResponse.json({ settings }); } catch (error) { console.error('[GET /api/users/[id]/notification-settings]', error); return NextResponse.json( { error: 'Failed to get settings' }, { status: 500 } ); } } // PATCH /api/users/[id]/notification-settings export async function PATCH( request: Request, { params }: { params: Promise<{ id: string }> } ) { try { const session = await auth(); if (!session?.user) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const { id: userId } = await params; const currentUserId = (session.user as any).id; // Users can only update their own settings if (userId !== currentUserId) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); } const body = await request.json(); const { email_enabled, telegram_enabled, telegram_chat_id } = body; const settings = updateUserNotificationSettings(userId, { email_enabled, telegram_enabled, telegram_chat_id, }); return NextResponse.json({ settings }); } catch (error) { console.error('[PATCH /api/users/[id]/notification-settings]', error); return NextResponse.json( { error: 'Failed to update settings' }, { status: 500 } ); } } ``` **File:** `app/api/users/[id]/notification-settings/test/route.ts` ```typescript import { NextResponse } from 'next/server'; import { auth } from '@/lib/auth'; import { telegramService } from '@/lib/telegram-service'; import { getUserNotificationSettings } from '@/lib/notification-settings-db'; // POST /api/users/[id]/notification-settings/test export async function POST( request: Request, { params }: { params: Promise<{ id: string }> } ) { try { const session = await auth(); if (!session?.user) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const { id: userId } = await params; const currentUserId = (session.user as any).id; if (userId !== currentUserId) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); } const settings = getUserNotificationSettings(userId); if (!settings.telegram_chat_id) { return NextResponse.json( { error: 'No Telegram chat ID configured' }, { status: 400 } ); } const success = await telegramService.testConnection( settings.telegram_chat_id ); if (success) { return NextResponse.json({ success: true, message: 'Test message sent' }); } else { return NextResponse.json( { error: 'Failed to send test message' }, { status: 500 } ); } } catch (error) { console.error('[POST /api/users/[id]/notification-settings/test]', error); return NextResponse.json( { error: 'Failed to send test message' }, { status: 500 } ); } } ``` ### 6. Add Admin UI Page **File:** `app/admin/settings/notifications/page.tsx` ```tsx 'use client'; import { useState, useEffect } from 'react'; import { useSession } from 'next-auth/react'; export default function NotificationSettingsPage() { const { data: session } = useSession(); const [settings, setSettings] = useState({ email_enabled: true, telegram_enabled: false, telegram_chat_id: '', }); const [loading, setLoading] = useState(false); const [message, setMessage] = useState(''); useEffect(() => { if (session?.user) { loadSettings(); } }, [session]); async function loadSettings() { try { const userId = (session!.user as any).id; const res = await fetch(`/api/users/${userId}/notification-settings`); const data = await res.json(); setSettings(data.settings); } catch (error) { console.error('Failed to load settings:', error); } } async function handleSave() { setLoading(true); setMessage(''); try { const userId = (session!.user as any).id; const res = await fetch(`/api/users/${userId}/notification-settings`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(settings), }); if (res.ok) { setMessage('βœ… Einstellungen gespeichert!'); } else { setMessage('❌ Fehler beim Speichern'); } } catch (error) { setMessage('❌ Fehler beim Speichern'); } finally { setLoading(false); } } async function handleTest() { setLoading(true); setMessage(''); try { const userId = (session!.user as any).id; const res = await fetch( `/api/users/${userId}/notification-settings/test`, { method: 'POST' } ); if (res.ok) { setMessage('βœ… Test-Nachricht gesendet!'); } else { const data = await res.json(); setMessage(`❌ Fehler: ${data.error}`); } } catch (error) { setMessage('❌ Fehler beim Senden der Test-Nachricht'); } finally { setLoading(false); } } return (

Benachrichtigungseinstellungen

{/* Email Settings */}

Geofence-Ereignisse per E-Mail erhalten

{/* Telegram Settings */}

Geofence-Ereignisse per Telegram erhalten (inkl. Karte und Buttons)

{settings.telegram_enabled && (
setSettings({ ...settings, telegram_chat_id: e.target.value }) } placeholder="z.B. 123456789" className="w-full px-3 py-2 border border-gray-300 rounded-md" />

Deine Telegram Chat ID findest du ΓΌber @userinfobot

)}
{/* Action Buttons */}
{settings.telegram_enabled && settings.telegram_chat_id && ( )}
{/* Status Message */} {message && (
{message}
)}
{/* Help Section */}

πŸ“– Wie bekomme ich meine Chat ID?

  1. Γ–ffne Telegram und suche nach @userinfobot
  2. Starte den Bot mit /start
  3. Der Bot sendet dir deine Chat ID
  4. Kopiere die Nummer und fΓΌge sie oben ein
); } ``` ## πŸ”§ Environment Configuration Add to `.env` or `.env.local`: ```env # Telegram Bot Configuration TELEGRAM_BOT_TOKEN=1234567890:ABCdefGHIjklMNOpqrsTUVwxyz ``` ## πŸ§ͺ Testing ### 1. Initialize Database ```bash # Run the initialization script node scripts/init-notification-settings.js # Or add to main db init npm run db:init ``` ### 2. Test Telegram Service Create `scripts/test-telegram.js`: ```javascript #!/usr/bin/env node const { telegramService } = require('../lib/telegram-service'); // Replace with your chat ID const CHAT_ID = '123456789'; async function test() { console.log('πŸ§ͺ Testing Telegram service...\n'); try { // Test 1: Simple message console.log('1. Sending simple message...'); await telegramService.sendMessage({ chatId: CHAT_ID, text: 'βœ… Test erfolgreich!\n\nDie Telegram-Integration funktioniert.', }); console.log(' βœ“ Message sent\n'); // Test 2: Location console.log('2. Sending location...'); await telegramService.sendLocation(CHAT_ID, 52.520008, 13.404954); console.log(' βœ“ Location sent\n'); // Test 3: Full geofence notification console.log('3. Sending geofence notification...'); await telegramService.sendGeofenceNotification({ chatId: CHAT_ID, deviceName: 'Test Device', geofenceName: 'Test Geofence', eventType: 'enter', latitude: 52.520008, longitude: 13.404954, timestamp: new Date().toISOString(), mapUrl: 'https://example.com/map', dashboardUrl: 'https://example.com/admin', }); console.log(' βœ“ Geofence notification sent\n'); console.log('βœ… All tests passed!'); } catch (error) { console.error('❌ Test failed:', error.message); process.exit(1); } } test(); ``` Run it: ```bash chmod +x scripts/test-telegram.js TELEGRAM_BOT_TOKEN=your_token node scripts/test-telegram.js ``` ### 3. Test via Admin UI 1. Navigate to `/admin/settings/notifications` 2. Enable Telegram notifications 3. Enter your Chat ID 4. Click "Telegram Test" 5. Check your Telegram for the test message ## πŸ“¦ Package Dependencies Add to `package.json`: ```json { "dependencies": { "axios": "^1.6.0" } } ``` Install: ```bash npm install axios ``` ## 🎯 Example Notification Flow ``` 1. Device enters geofence ↓ 2. geofence-engine.ts detects event ↓ 3. GeofenceEvent created in database ↓ 4. geofence-notifications.ts triggered ↓ 5. getUserNotificationSettings(owner_id) ↓ 6. Parallel execution: β”œβ”€β†’ sendEmailNotification (if enabled) └─→ sendTelegramNotification (if enabled) ↓ Telegram receives: β”œβ”€β†’ Text message with emoji, device, geofence, time β”œβ”€β†’ Inline buttons (map, dashboard) └─→ Location pin on map ``` ## πŸ’‘ Tips & Best Practices ### Getting Chat ID Users can get their Telegram Chat ID: 1. Search for `@userinfobot` in Telegram 2. Send `/start` 3. Bot replies with Chat ID ### Bot Setup To create a Telegram Bot: 1. Message `@BotFather` in Telegram 2. Send `/newbot` 3. Follow the prompts 4. Save the token provided ### Rate Limiting Telegram API has rate limits: - 30 messages per second per bot - 20 messages per minute per chat The current implementation sends 2 messages per geofence event (text + location), so you can handle ~15 events per minute per user. ### Error Handling The implementation uses `Promise.allSettled()`, meaning: - If Telegram fails, Email still works (and vice versa) - At least one successful channel = event marked as "sent" - Both fail = event marked as "failed" with error details ### Security - Bot token should be in environment variables, never committed to git - Chat IDs are user-specific and stored per user - Users can only configure their own notification settings (enforced by API) ## πŸ”„ Migration Guide If you already have users, run this migration: ```sql -- Add default settings for existing users INSERT INTO UserNotificationSettings (user_id, email_enabled, telegram_enabled) SELECT id, 1, 0 FROM User WHERE id NOT IN (SELECT user_id FROM UserNotificationSettings); ``` ## πŸ“Š Database Schema Reference ```sql UserNotificationSettings β”œβ”€β”€ user_id (TEXT, PK, FK β†’ User.id) β”œβ”€β”€ email_enabled (INTEGER, 0 or 1) β”œβ”€β”€ telegram_enabled (INTEGER, 0 or 1) β”œβ”€β”€ telegram_chat_id (TEXT, nullable) β”œβ”€β”€ created_at (TEXT) └── updated_at (TEXT) Indexes: └── idx_user_notification_settings_user ON UserNotificationSettings(user_id) ``` ## πŸš€ Future Enhancements Possible extensions: - [ ] Push notifications (web push) - [ ] SMS notifications (Twilio) - [ ] Webhook notifications (custom endpoints) - [ ] Notification scheduling (quiet hours) - [ ] Notification grouping (batch multiple events) - [ ] Per-geofence notification settings - [ ] Rich Telegram messages with photos/videos ## πŸ“ Changelog ### Version 1.0 (Initial Implementation) - Basic Telegram notification support - Text messages with emoji and formatting - Location pins on map - Inline buttons for map and dashboard - User-configurable settings (Email, Telegram, both) - Test functionality via Admin UI - Parallel notification delivery - Robust error handling --- **Author:** Claude Code **Last Updated:** 2025-12-04