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>
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;
Option B: Separate Settings Table (Recommended)
-- 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
- Navigate to
/admin/settings/notifications - Enable Telegram notifications
- Enter your Chat ID
- Click "Telegram Test"
- 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:
- Search for
@userinfobotin Telegram - Send
/start - Bot replies with Chat ID
Bot Setup
To create a Telegram Bot:
- Message
@BotFatherin Telegram - Send
/newbot - Follow the prompts
- 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