Files
location-mqtt-tracker-app/docs/telegram.md
Joachim Hummel 0d1dbeafda 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>
2025-12-04 14:54:19 +00:00

30 KiB

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

-- 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;
-- 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

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<void> {
    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<void> {
    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<void> {
    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} <b>Geofence ${action}</b>

📱 <b>Device:</b> ${deviceName}
📍 <b>Geofence:</b> ${geofenceName}
🕐 <b>Zeit:</b> ${formattedDate}
📊 <b>Ereignis:</b> 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<boolean> {
    try {
      await this.sendMessage({
        chatId,
        text: '✅ <b>Telegram Connection Test</b>\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

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>
): 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)

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<void> {
  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<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;
    }

    // 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<void> {
  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<void> {
  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

#!/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

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

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

'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 (
    <div className="max-w-2xl mx-auto p-6">
      <h1 className="text-2xl font-bold mb-6">Benachrichtigungseinstellungen</h1>

      <div className="space-y-6 bg-white p-6 rounded-lg shadow">
        {/* Email Settings */}
        <div>
          <label className="flex items-center space-x-3">
            <input
              type="checkbox"
              checked={settings.email_enabled}
              onChange={(e) =>
                setSettings({ ...settings, email_enabled: e.target.checked })
              }
              className="w-4 h-4"
            />
            <span className="font-medium">📧 E-Mail Benachrichtigungen</span>
          </label>
          <p className="text-sm text-gray-600 ml-7 mt-1">
            Geofence-Ereignisse per E-Mail erhalten
          </p>
        </div>

        {/* Telegram Settings */}
        <div>
          <label className="flex items-center space-x-3">
            <input
              type="checkbox"
              checked={settings.telegram_enabled}
              onChange={(e) =>
                setSettings({ ...settings, telegram_enabled: e.target.checked })
              }
              className="w-4 h-4"
            />
            <span className="font-medium">📱 Telegram Benachrichtigungen</span>
          </label>
          <p className="text-sm text-gray-600 ml-7 mt-1">
            Geofence-Ereignisse per Telegram erhalten (inkl. Karte und Buttons)
          </p>

          {settings.telegram_enabled && (
            <div className="ml-7 mt-3">
              <label className="block text-sm font-medium mb-1">
                Telegram Chat ID
              </label>
              <input
                type="text"
                value={settings.telegram_chat_id}
                onChange={(e) =>
                  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"
              />
              <p className="text-xs text-gray-500 mt-1">
                Deine Telegram Chat ID findest du über @userinfobot
              </p>
            </div>
          )}
        </div>

        {/* Action Buttons */}
        <div className="flex gap-3 pt-4 border-t">
          <button
            onClick={handleSave}
            disabled={loading}
            className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
          >
            {loading ? 'Speichern...' : 'Speichern'}
          </button>

          {settings.telegram_enabled && settings.telegram_chat_id && (
            <button
              onClick={handleTest}
              disabled={loading}
              className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 disabled:opacity-50"
            >
              Telegram Test
            </button>
          )}
        </div>

        {/* Status Message */}
        {message && (
          <div className="p-3 bg-gray-100 rounded-md text-sm">{message}</div>
        )}
      </div>

      {/* Help Section */}
      <div className="mt-6 p-4 bg-blue-50 rounded-lg">
        <h3 className="font-medium mb-2">📖 Wie bekomme ich meine Chat ID?</h3>
        <ol className="text-sm space-y-1 list-decimal list-inside">
          <li>Öffne Telegram und suche nach @userinfobot</li>
          <li>Starte den Bot mit /start</li>
          <li>Der Bot sendet dir deine Chat ID</li>
          <li>Kopiere die Nummer und füge sie oben ein</li>
        </ol>
      </div>
    </div>
  );
}

🔧 Environment Configuration

Add to .env or .env.local:

# Telegram Bot Configuration
TELEGRAM_BOT_TOKEN=1234567890:ABCdefGHIjklMNOpqrsTUVwxyz

🧪 Testing

1. Initialize Database

# 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:

#!/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: '✅ <b>Test erfolgreich!</b>\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:

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:

{
  "dependencies": {
    "axios": "^1.6.0"
  }
}

Install:

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:

-- 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

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