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>
1163 lines
30 KiB
Markdown
1163 lines
30 KiB
Markdown
# 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<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`
|
|
|
|
```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>
|
|
): 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<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`
|
|
|
|
```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 (
|
|
<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`:
|
|
|
|
```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: '✅ <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:
|
|
|
|
```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
|