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>
171 lines
4.0 KiB
TypeScript
171 lines
4.0 KiB
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();
|