Add Telegram notification integration for geofencing
Features: - Multi-channel notifications (Email + Telegram) - User-configurable notification settings per channel - Telegram bot integration with rich messages, location pins, and inline buttons - QR code generation for easy bot access (@myidbot support) - Admin UI for notification settings management - Test functionality for Telegram connection - Comprehensive documentation Implementation: - lib/telegram-service.ts: Telegram API integration - lib/notification-settings-db.ts: Database layer for user notification preferences - lib/geofence-notifications.ts: Extended for parallel multi-channel delivery - API routes for settings management and testing - Admin UI with QR code display and step-by-step instructions - Database table: UserNotificationSettings Documentation: - docs/telegram.md: Technical implementation guide - docs/telegram-anleitung.md: User guide with @myidbot instructions - docs/telegram-setup.md: Admin setup guide - README.md: Updated NPM scripts section Docker: - Updated Dockerfile to copy public directory - Added TELEGRAM_BOT_TOKEN environment variable - Integrated notification settings initialization in db:init 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
170
lib/telegram-service.ts
Normal file
170
lib/telegram-service.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user