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:
@@ -36,6 +36,7 @@ RUN npm ci --omit=dev
|
||||
# Kopiere Build Artifacts
|
||||
COPY --from=builder /app/.next ./.next
|
||||
COPY --from=builder /app/data ./data
|
||||
COPY --from=builder /app/public ./public
|
||||
|
||||
# Kopiere App Code (benötigt für instrumentation.ts, lib/, etc.)
|
||||
COPY --from=builder /app/instrumentation.ts ./
|
||||
|
||||
@@ -1093,7 +1093,6 @@ npm run email:dev # Email Template Dev Server
|
||||
# Linting
|
||||
npm run lint # ESLint ausführen
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Automatische Dokumentations-Synchronisation
|
||||
|
||||
208
app/admin/settings/notifications/page.tsx
Normal file
208
app/admin/settings/notifications/page.tsx
Normal file
@@ -0,0 +1,208 @@
|
||||
'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-3">📱 Telegram Bot aktivieren</h3>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
{/* QR Code */}
|
||||
<div className="flex flex-col items-center justify-center bg-white p-4 rounded-lg">
|
||||
<p className="text-sm font-medium mb-2">Schritt 1: Bot starten</p>
|
||||
<img
|
||||
src="/telegram-bot-qr.png"
|
||||
alt="Telegram Bot QR Code"
|
||||
className="w-48 h-48 mb-2"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 text-center">
|
||||
Scanne den QR-Code mit deinem Smartphone
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
oder öffne: <a href="https://t.me/MeinTracking_bot" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">@MeinTracking_bot</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Instructions */}
|
||||
<div className="flex flex-col justify-center">
|
||||
<p className="text-sm font-medium mb-2">Schritt 2: Chat ID holen</p>
|
||||
<ol className="text-sm space-y-2 list-decimal list-inside text-gray-700">
|
||||
<li>Starte den Bot mit <code className="bg-gray-200 px-1 rounded">/start</code></li>
|
||||
<li>Suche in Telegram nach <code className="bg-gray-200 px-1 rounded">@myidbot</code></li>
|
||||
<li>Sende <code className="bg-gray-200 px-1 rounded">/getid</code> an @myidbot</li>
|
||||
<li>Der Bot antwortet mit deiner Chat ID</li>
|
||||
<li>Kopiere die Nummer und trage sie oben ein</li>
|
||||
</ol>
|
||||
<p className="text-xs text-gray-500 mt-2">
|
||||
Alternative: <code className="bg-gray-200 px-1 rounded">@userinfobot</code> mit <code className="bg-gray-200 px-1 rounded">/start</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
75
app/api/users/[id]/notification-settings/route.ts
Normal file
75
app/api/users/[id]/notification-settings/route.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
52
app/api/users/[id]/notification-settings/test/route.ts
Normal file
52
app/api/users/[id]/notification-settings/test/route.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,8 @@ services:
|
||||
- SMTP_FROM_EMAIL=${SMTP_FROM_EMAIL}
|
||||
- SMTP_FROM_NAME=${SMTP_FROM_NAME:-Location Tracker}
|
||||
- ENCRYPTION_KEY=${ENCRYPTION_KEY}
|
||||
# Telegram Configuration
|
||||
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
- ./public:/app/public # Mount public directory for static files
|
||||
|
||||
163
docs/telegram-anleitung.md
Normal file
163
docs/telegram-anleitung.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# 📱 Telegram-Benachrichtigungen aktivieren
|
||||
|
||||
## Übersicht
|
||||
|
||||
Erhalte Geofence-Benachrichtigungen direkt in Telegram mit:
|
||||
- 📍 Standort-Pin auf der Karte
|
||||
- 🔔 Sofortige Push-Benachrichtigungen
|
||||
- 🔗 Direktlinks zur Karte und zum Dashboard
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Schritt-für-Schritt Anleitung
|
||||
|
||||
### Schritt 1: Bot starten
|
||||
|
||||
#### Option A: QR-Code scannen (empfohlen für Smartphone)
|
||||
|
||||
<p align="center">
|
||||
<img src="../public/telegram-bot-qr.png" width="300" alt="Telegram Bot QR Code">
|
||||
</p>
|
||||
|
||||
1. Öffne die Kamera-App auf deinem Smartphone
|
||||
2. Scanne den QR-Code
|
||||
3. Telegram öffnet sich automatisch
|
||||
4. Klicke auf **"Start"** oder sende `/start`
|
||||
|
||||
#### Option B: Link öffnen
|
||||
|
||||
1. Klicke auf: **[https://t.me/MeinTracking_bot](https://t.me/MeinTracking_bot)**
|
||||
2. Telegram öffnet sich automatisch
|
||||
3. Klicke auf **"Start"** oder sende `/start`
|
||||
|
||||
✅ Du solltest eine Willkommensnachricht vom Bot erhalten
|
||||
|
||||
---
|
||||
|
||||
### Schritt 2: Chat ID herausfinden
|
||||
|
||||
#### Option A: Mit @myidbot (empfohlen)
|
||||
|
||||
1. Suche in Telegram nach: **@myidbot**
|
||||
2. Sende: `/getid`
|
||||
3. Der Bot antwortet sofort mit deiner Chat ID:
|
||||
```
|
||||
Your user ID: 123456789
|
||||
```
|
||||
4. **Kopiere die Nummer** (z.B. `123456789`)
|
||||
|
||||
#### Option B: Mit @userinfobot
|
||||
|
||||
1. Suche in Telegram nach: **@userinfobot**
|
||||
2. Sende: `/start`
|
||||
3. Der Bot antwortet mit deinen Informationen:
|
||||
```
|
||||
Id: 123456789
|
||||
First name: Dein Name
|
||||
...
|
||||
```
|
||||
4. **Kopiere die Nummer** bei "Id:" (z.B. `123456789`)
|
||||
|
||||
---
|
||||
|
||||
### Schritt 3: Chat ID im Dashboard eintragen
|
||||
|
||||
1. Melde dich im Location Tracker an
|
||||
2. Gehe zu: **Einstellungen** → **Benachrichtigungen**
|
||||
3. Aktiviere: ☑️ **Telegram Benachrichtigungen**
|
||||
4. Trage deine **Chat ID** ein (die Nummer aus Schritt 2)
|
||||
5. Klicke auf **"Speichern"**
|
||||
|
||||
---
|
||||
|
||||
### Schritt 4: Verbindung testen
|
||||
|
||||
1. Klicke auf den Button **"Telegram Test"**
|
||||
2. Du solltest sofort eine Test-Nachricht in Telegram erhalten:
|
||||
```
|
||||
✅ Telegram Connection Test
|
||||
|
||||
Die Verbindung funktioniert!
|
||||
```
|
||||
|
||||
✅ **Fertig!** Ab jetzt erhältst du alle Geofence-Benachrichtigungen auch per Telegram.
|
||||
|
||||
---
|
||||
|
||||
## 📨 Beispiel-Benachrichtigung
|
||||
|
||||
So sieht eine Geofence-Benachrichtigung aus:
|
||||
|
||||
```
|
||||
🟢 Geofence BETRETEN
|
||||
|
||||
📱 Device: Mein iPhone
|
||||
📍 Geofence: Zuhause
|
||||
🕐 Zeit: 04.12.25, 14:30
|
||||
📊 Ereignis: Hat Zuhause betreten
|
||||
```
|
||||
|
||||
**+ Standort-Pin auf der Karte**
|
||||
|
||||
**+ Buttons:**
|
||||
- 🗺️ Auf Karte zeigen
|
||||
- 📊 Dashboard öffnen
|
||||
|
||||
---
|
||||
|
||||
## ❓ Häufige Fragen
|
||||
|
||||
### Kann ich sowohl Email als auch Telegram aktivieren?
|
||||
|
||||
Ja! Du kannst beide Kanäle gleichzeitig nutzen:
|
||||
- ☑️ E-Mail Benachrichtigungen
|
||||
- ☑️ Telegram Benachrichtigungen
|
||||
|
||||
### Sehen andere User meine Benachrichtigungen?
|
||||
|
||||
**Nein!** Jede Chat ID ist ein privater 1-zu-1 Chat. Andere User können deine Nachrichten nicht sehen, auch wenn sie den gleichen Bot nutzen.
|
||||
|
||||
### Was ist, wenn die Chat ID falsch ist?
|
||||
|
||||
Dann erhältst du keine Benachrichtigungen. Nutze den "Telegram Test" Button, um die Verbindung zu prüfen.
|
||||
|
||||
### Kann ich Benachrichtigungen pausieren?
|
||||
|
||||
Ja! Deaktiviere einfach "Telegram Benachrichtigungen" in den Einstellungen. Deine Chat ID bleibt gespeichert.
|
||||
|
||||
### Wo finde ich meine Chat ID nochmal?
|
||||
|
||||
Sende `/getid` an **@myidbot** oder `/start` an **@userinfobot** in Telegram.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Troubleshooting
|
||||
|
||||
### "No Telegram chat ID configured"
|
||||
|
||||
→ Du hast noch keine Chat ID eingetragen. Folge Schritt 2 und 3.
|
||||
|
||||
### "Failed to send test message"
|
||||
|
||||
Mögliche Ursachen:
|
||||
1. **Chat ID falsch** → Überprüfe die Nummer bei @userinfobot
|
||||
2. **Bot nicht gestartet** → Sende `/start` an @MeinTracking_bot
|
||||
3. **Bot blockiert** → Entsperre den Bot in Telegram
|
||||
|
||||
### Test-Nachricht kommt nicht an
|
||||
|
||||
1. Überprüfe, ob du den Bot gestartet hast (`/start` an @MeinTracking_bot)
|
||||
2. Prüfe, ob die Chat ID korrekt ist (keine Leerzeichen, keine Buchstaben)
|
||||
- Hole sie neu mit `/getid` an @myidbot
|
||||
3. Stelle sicher, dass du den Bot nicht blockiert hast
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support
|
||||
|
||||
Bei Problemen wende dich an den Administrator oder überprüfe die Logs im System.
|
||||
|
||||
---
|
||||
|
||||
**Bot:** [@MeinTracking_bot](https://t.me/MeinTracking_bot)
|
||||
**Letzte Aktualisierung:** 04.12.2025
|
||||
345
docs/telegram-setup.md
Normal file
345
docs/telegram-setup.md
Normal file
@@ -0,0 +1,345 @@
|
||||
# 🚀 Telegram Integration - Setup Guide
|
||||
|
||||
## ✅ Was wurde implementiert
|
||||
|
||||
### Dateien erstellt:
|
||||
|
||||
1. **Backend Services:**
|
||||
- `lib/telegram-service.ts` - Telegram API Integration
|
||||
- `lib/notification-settings-db.ts` - Datenbank-Layer für Settings
|
||||
- `lib/geofence-notifications.ts` - Erweitert für Multi-Channel (Email + Telegram)
|
||||
|
||||
2. **Database:**
|
||||
- `scripts/init-notification-settings.js` - DB Init Script
|
||||
- Tabelle: `UserNotificationSettings` erstellt
|
||||
|
||||
3. **API Endpoints:**
|
||||
- `GET/PATCH /api/users/[id]/notification-settings` - Settings verwalten
|
||||
- `POST /api/users/[id]/notification-settings/test` - Test-Nachricht
|
||||
|
||||
4. **Admin UI:**
|
||||
- `app/admin/settings/notifications/page.tsx` - Settings Page mit QR Code
|
||||
|
||||
5. **Dokumentation:**
|
||||
- `docs/telegram-anleitung.md` - User-Anleitung
|
||||
- `docs/telegram-setup.md` - Dieses Dokument
|
||||
- `public/telegram-bot-qr.png` - QR Code für Bot
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Installation & Setup
|
||||
|
||||
### 1. Dependencies installiert
|
||||
|
||||
```bash
|
||||
✅ npm install axios
|
||||
✅ npm install qrcode
|
||||
```
|
||||
|
||||
### 2. Datenbank initialisiert
|
||||
|
||||
```bash
|
||||
✅ node scripts/init-notification-settings.js
|
||||
```
|
||||
|
||||
Die Tabelle `UserNotificationSettings` wurde erstellt mit:
|
||||
- `user_id` (PK, FK → User.id)
|
||||
- `email_enabled` (0/1)
|
||||
- `telegram_enabled` (0/1)
|
||||
- `telegram_chat_id` (TEXT)
|
||||
- `created_at`, `updated_at`
|
||||
|
||||
---
|
||||
|
||||
## 📱 Bot Setup
|
||||
|
||||
### Telegram Bot erstellen (für Admin):
|
||||
|
||||
1. Öffne Telegram und suche: **@BotFather**
|
||||
2. Sende: `/newbot`
|
||||
3. Folge den Anweisungen:
|
||||
- Bot Name: `MeinTracking Bot` (oder anders)
|
||||
- Bot Username: `MeinTracking_bot` (muss auf `_bot` enden)
|
||||
4. BotFather gibt dir einen Token: `1234567890:ABCdefGHIjklMNOpqrsTUVwxyz`
|
||||
5. **Wichtig:** Token NIEMALS committen!
|
||||
|
||||
### .env konfigurieren:
|
||||
|
||||
```bash
|
||||
# .env oder .env.local
|
||||
TELEGRAM_BOT_TOKEN=1234567890:ABCdefGHIjklMNOpqrsTUVwxyz
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Für User: Bot aktivieren
|
||||
|
||||
**Schritt-für-Schritt siehe:** `docs/telegram-anleitung.md`
|
||||
|
||||
**Kurzversion:**
|
||||
1. Bot starten: [https://t.me/MeinTracking_bot](https://t.me/MeinTracking_bot) oder QR scannen
|
||||
2. Chat ID holen: `@myidbot` → `/getid` (oder `@userinfobot` → `/start`)
|
||||
3. Im Dashboard: Settings → Notifications → Chat ID eintragen
|
||||
4. Test Button drücken
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### 1. QR Code generieren
|
||||
|
||||
```bash
|
||||
npm run telegram:qr
|
||||
```
|
||||
|
||||
Erstellt: `public/telegram-bot-qr.png`
|
||||
|
||||
### 2. Datenbank neu initialisieren
|
||||
|
||||
```bash
|
||||
npm run db:init:notifications
|
||||
```
|
||||
|
||||
Oder alles zusammen:
|
||||
```bash
|
||||
npm run db:init
|
||||
```
|
||||
|
||||
### 3. Test-Nachricht senden
|
||||
|
||||
**Option A: Über Admin UI**
|
||||
- Navigiere zu: `/admin/settings/notifications`
|
||||
- Telegram aktivieren
|
||||
- Chat ID eintragen
|
||||
- Button "Telegram Test" klicken
|
||||
|
||||
**Option B: Direkt via Script**
|
||||
|
||||
Erstelle `scripts/test-telegram.js`:
|
||||
```javascript
|
||||
const { telegramService } = require('../lib/telegram-service');
|
||||
|
||||
const CHAT_ID = '123456789'; // Deine Chat ID
|
||||
|
||||
async function test() {
|
||||
await telegramService.testConnection(CHAT_ID);
|
||||
}
|
||||
test();
|
||||
```
|
||||
|
||||
```bash
|
||||
TELEGRAM_BOT_TOKEN=dein_token node scripts/test-telegram.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Sicherheit & Privacy
|
||||
|
||||
### Wie funktioniert die Privacy?
|
||||
|
||||
- **1 Bot für alle User** (in `.env`)
|
||||
- **Jeder User hat eigene Chat ID** (in DB)
|
||||
- **Bot sendet nur an spezifische Chat ID** → Privater 1-zu-1 Chat
|
||||
- **User A sieht NIEMALS Nachrichten von User B**
|
||||
|
||||
### Telegram API Garantie:
|
||||
|
||||
```typescript
|
||||
telegram.sendMessage(chatId: "123456789", text: "...")
|
||||
→ Geht NUR an Chat ID 123456789
|
||||
→ Andere Chat IDs können dies NICHT sehen
|
||||
```
|
||||
|
||||
### Vergleich:
|
||||
Wie Email: 1 SMTP-Server (Bot) sendet an verschiedene Adressen (Chat IDs)
|
||||
|
||||
---
|
||||
|
||||
## 📊 Wie es funktioniert
|
||||
|
||||
### Notification Flow:
|
||||
|
||||
```
|
||||
1. Device triggers Geofence
|
||||
↓
|
||||
2. geofence-engine.ts erkennt Event
|
||||
↓
|
||||
3. GeofenceEvent in DB erstellt
|
||||
↓
|
||||
4. geofence-notifications.ts aufgerufen
|
||||
↓
|
||||
5. getUserNotificationSettings(owner_id)
|
||||
↓
|
||||
6. Parallel execution:
|
||||
├─→ Email (if enabled)
|
||||
└─→ Telegram (if enabled)
|
||||
↓
|
||||
├─→ Text mit Emoji, Device, Geofence, Zeit
|
||||
├─→ Inline Buttons (Karte, Dashboard)
|
||||
└─→ Location Pin
|
||||
```
|
||||
|
||||
### Multi-Channel Strategy:
|
||||
|
||||
- **Promise.allSettled()** → Beide parallel
|
||||
- **Mind. 1 erfolgreich** → Event als "sent" markiert
|
||||
- **Beide fehlgeschlagen** → Event als "failed" markiert
|
||||
- **Robust:** Email-Fehler betrifft nicht Telegram (und umgekehrt)
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Admin UI Features
|
||||
|
||||
**Page:** `/admin/settings/notifications`
|
||||
|
||||
**Komponenten:**
|
||||
- ☑️ Email Benachrichtigungen Toggle
|
||||
- ☑️ Telegram Benachrichtigungen Toggle
|
||||
- 📝 Telegram Chat ID Input
|
||||
- 💾 Speichern Button
|
||||
- 🧪 Telegram Test Button
|
||||
- 📱 QR Code Display (400x400px)
|
||||
- 📖 Schritt-für-Schritt Anleitung
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ NPM Scripts
|
||||
|
||||
```bash
|
||||
# Datenbank initialisieren
|
||||
npm run db:init # Alles (inkl. Notifications)
|
||||
npm run db:init:notifications # Nur Notifications Tabelle
|
||||
|
||||
# QR Code generieren
|
||||
npm run telegram:qr # Erstellt public/telegram-bot-qr.png
|
||||
|
||||
# Testing
|
||||
npm run test:geofence # Geofence Event simulieren
|
||||
npm run test:geofence:email # Email Notification testen
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Telegram Message Format
|
||||
|
||||
### Enter Event:
|
||||
```
|
||||
🟢 Geofence BETRETEN
|
||||
|
||||
📱 Device: Mein iPhone
|
||||
📍 Geofence: Zuhause
|
||||
🕐 Zeit: 04.12.25, 14:30
|
||||
📊 Ereignis: Hat Zuhause betreten
|
||||
```
|
||||
|
||||
**+ Location Pin** (Karte mit exakter Position)
|
||||
|
||||
**+ Buttons:**
|
||||
- 🗺️ Auf Karte zeigen → `/map?lat=...&lon=...`
|
||||
- 📊 Dashboard öffnen → `/admin/geofences/events`
|
||||
|
||||
### Exit Event:
|
||||
```
|
||||
🔴 Geofence VERLASSEN
|
||||
|
||||
📱 Device: Mein iPhone
|
||||
📍 Geofence: Zuhause
|
||||
🕐 Zeit: 04.12.25, 18:45
|
||||
📊 Ereignis: Hat Zuhause verlassen
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚨 Troubleshooting
|
||||
|
||||
### "Cannot find module telegram-service"
|
||||
|
||||
→ TypeScript nicht kompiliert. Lösung:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Oder für Development:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### "Telegram bot token not configured"
|
||||
|
||||
→ `.env` fehlt `TELEGRAM_BOT_TOKEN`
|
||||
|
||||
### "Failed to send test message"
|
||||
|
||||
**Mögliche Ursachen:**
|
||||
1. Bot nicht gestartet (`/start` an @MeinTracking_bot)
|
||||
2. Chat ID falsch
|
||||
3. Bot blockiert
|
||||
4. Token falsch/abgelaufen
|
||||
|
||||
**Debug:**
|
||||
```bash
|
||||
# Check .env
|
||||
cat .env | grep TELEGRAM
|
||||
|
||||
# Check DB
|
||||
sqlite3 data/database.sqlite "SELECT * FROM UserNotificationSettings;"
|
||||
|
||||
# Test Telegram API direkt
|
||||
curl https://api.telegram.org/bot<TOKEN>/getMe
|
||||
```
|
||||
|
||||
### QR Code wird nicht angezeigt
|
||||
|
||||
→ Prüfen ob Datei existiert:
|
||||
```bash
|
||||
ls -la public/telegram-bot-qr.png
|
||||
```
|
||||
|
||||
Falls nicht:
|
||||
```bash
|
||||
npm run telegram:qr
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Environment Variables
|
||||
|
||||
```bash
|
||||
# Required für Telegram
|
||||
TELEGRAM_BOT_TOKEN=<dein_bot_token>
|
||||
|
||||
# Optional (für Map/Dashboard Links)
|
||||
NEXTAUTH_URL=https://deine-domain.de
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Nächste Schritte
|
||||
|
||||
**Für Produktion:**
|
||||
|
||||
1. ✅ `.env.production` mit echtem Bot-Token erstellen
|
||||
2. ✅ Datenbank migrieren: `npm run db:init`
|
||||
3. ✅ QR Code generieren: `npm run telegram:qr`
|
||||
4. ✅ User-Anleitung teilen: `docs/telegram-anleitung.md`
|
||||
5. ✅ Test durchführen mit echtem Device
|
||||
|
||||
**Optional:**
|
||||
- Push Notifications hinzufügen
|
||||
- SMS Integration
|
||||
- Webhook Support
|
||||
- Notification History UI
|
||||
- Per-Geofence Settings (nicht global)
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support
|
||||
|
||||
**Bot:** [@MeinTracking_bot](https://t.me/MeinTracking_bot)
|
||||
|
||||
**Dokumentation:**
|
||||
- User-Anleitung: `docs/telegram-anleitung.md`
|
||||
- API Implementierung: `docs/telegram.md`
|
||||
- Dieses Setup-Guide: `docs/telegram-setup.md`
|
||||
|
||||
**Letzte Aktualisierung:** 04.12.2025
|
||||
1162
docs/telegram.md
Normal file
1162
docs/telegram.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,19 @@
|
||||
/**
|
||||
* Geofence Notification Service
|
||||
* Handles sending email notifications for geofence events
|
||||
* Handles sending email and Telegram notifications for geofence events
|
||||
*/
|
||||
|
||||
import { emailService } from './email-service';
|
||||
import { telegramService } from './telegram-service';
|
||||
import {
|
||||
renderGeofenceEnterEmail,
|
||||
renderGeofenceExitEmail,
|
||||
GeofenceEmailData,
|
||||
} from './email-renderer';
|
||||
import { geofenceDb } from './geofence-db';
|
||||
import {
|
||||
getUserNotificationSettings,
|
||||
} from './notification-settings-db';
|
||||
import type { GeofenceEvent } from './types';
|
||||
import Database from 'better-sqlite3';
|
||||
import path from 'path';
|
||||
@@ -62,6 +66,7 @@ function getUser(userId: string): UserInfo | null {
|
||||
|
||||
/**
|
||||
* Send notification for a single geofence event
|
||||
* Supports both Email and Telegram based on user preferences
|
||||
*/
|
||||
async function sendEventNotification(event: GeofenceEvent): Promise<void> {
|
||||
try {
|
||||
@@ -79,13 +84,87 @@ async function sendEventNotification(event: GeofenceEvent): Promise<void> {
|
||||
|
||||
// Get owner details
|
||||
const owner = getUser(geofence.owner_id);
|
||||
if (!owner || !owner.email) {
|
||||
console.log(`[GeofenceNotification] No email for owner ${geofence.owner_id}, skipping notification`);
|
||||
geofenceDb.markNotificationSent(event.id!, true); // Mark as "sent" (no email needed)
|
||||
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;
|
||||
}
|
||||
|
||||
// Prepare email data
|
||||
// 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 (extracted from main function)
|
||||
*/
|
||||
async function sendEmailNotification(
|
||||
event: GeofenceEvent,
|
||||
owner: UserInfo,
|
||||
device: DeviceInfo,
|
||||
geofence: GeofenceInfo
|
||||
): Promise<void> {
|
||||
try {
|
||||
const emailData: GeofenceEmailData = {
|
||||
username: owner.username,
|
||||
deviceName: device.name,
|
||||
@@ -94,11 +173,8 @@ async function sendEventNotification(event: GeofenceEvent): Promise<void> {
|
||||
latitude: event.latitude,
|
||||
longitude: event.longitude,
|
||||
distanceFromCenter: event.distance_from_center || 0,
|
||||
// Optional: Add map URL later
|
||||
// mapUrl: `${process.env.NEXT_PUBLIC_URL}/map?lat=${event.latitude}&lon=${event.longitude}`
|
||||
};
|
||||
|
||||
// Render and send email
|
||||
let html: string;
|
||||
let subject: string;
|
||||
|
||||
@@ -110,20 +186,51 @@ async function sendEventNotification(event: GeofenceEvent): Promise<void> {
|
||||
subject = `${device.name} hat ${geofence.name} verlassen`;
|
||||
}
|
||||
|
||||
// Send via existing email service
|
||||
await emailService['sendEmail'](owner.email, subject, html);
|
||||
await emailService['sendEmail'](owner.email!, subject, html);
|
||||
|
||||
// Mark notification as sent
|
||||
geofenceDb.markNotificationSent(event.id!, true);
|
||||
|
||||
console.log(`[GeofenceNotification] Sent ${event.event_type} notification for geofence ${geofence.name} to ${owner.email}`);
|
||||
console.log(
|
||||
`[GeofenceNotification] Sent email to ${owner.email}`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('[GeofenceNotification] Failed to send notification:', error);
|
||||
console.error('[GeofenceNotification] Email error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Mark notification as failed
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
geofenceDb.markNotificationSent(event.id!, false, errorMessage);
|
||||
/**
|
||||
* 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';
|
||||
|
||||
// Ensure latitude and longitude are numbers
|
||||
const lat = typeof event.latitude === 'number' ? event.latitude : parseFloat(event.latitude);
|
||||
const lon = typeof event.longitude === 'number' ? event.longitude : parseFloat(event.longitude);
|
||||
|
||||
await telegramService.sendGeofenceNotification({
|
||||
chatId,
|
||||
deviceName: device.name,
|
||||
geofenceName: geofence.name,
|
||||
eventType: event.event_type,
|
||||
latitude: lat,
|
||||
longitude: lon,
|
||||
timestamp: event.timestamp,
|
||||
mapUrl: `${baseUrl}/map?lat=${lat}&lon=${lon}`,
|
||||
dashboardUrl: `${baseUrl}/admin/geofences/events`,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[GeofenceNotification] Sent Telegram notification to ${chatId}`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('[GeofenceNotification] Telegram error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
157
lib/notification-settings-db.ts
Normal file
157
lib/notification-settings-db.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
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 any;
|
||||
|
||||
// Return defaults if no settings found
|
||||
if (!result) {
|
||||
return {
|
||||
user_id: userId,
|
||||
email_enabled: true,
|
||||
telegram_enabled: false,
|
||||
telegram_chat_id: null,
|
||||
};
|
||||
}
|
||||
|
||||
// Convert SQLite integers to booleans
|
||||
return {
|
||||
user_id: result.user_id,
|
||||
email_enabled: result.email_enabled === 1,
|
||||
telegram_enabled: result.telegram_enabled === 1,
|
||||
telegram_chat_id: result.telegram_chat_id,
|
||||
created_at: result.created_at,
|
||||
updated_at: result.updated_at,
|
||||
};
|
||||
} 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 !== undefined ? (settings.email_enabled ? 1 : 0) : 1,
|
||||
settings.telegram_enabled !== undefined ? (settings.telegram_enabled ? 1 : 0) : 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();
|
||||
}
|
||||
}
|
||||
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();
|
||||
600
package-lock.json
generated
600
package-lock.json
generated
@@ -16,6 +16,7 @@
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/react": "^19.2.4",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"axios": "^1.13.2",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"leaflet": "^1.9.4",
|
||||
@@ -23,6 +24,7 @@
|
||||
"next": "^16.0.7",
|
||||
"next-auth": "^5.0.0-beta.30",
|
||||
"nodemailer": "^7.0.10",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1",
|
||||
"react-email": "^4.3.2",
|
||||
@@ -3416,6 +3418,12 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/autoprefixer": {
|
||||
"version": "10.4.22",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz",
|
||||
@@ -3454,6 +3462,17 @@
|
||||
"postcss": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
||||
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
@@ -3619,6 +3638,28 @@
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/camelcase": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001754",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz",
|
||||
@@ -3714,6 +3755,87 @@
|
||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^6.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cliui/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -3732,6 +3854,18 @@
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "13.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
|
||||
@@ -3848,6 +3982,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/decompress-response": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
@@ -3881,6 +4024,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
@@ -3890,6 +4042,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dijkstrajs": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
@@ -3945,6 +4103,20 @@
|
||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
@@ -4044,6 +4216,51 @@
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
||||
@@ -4172,6 +4389,39 @@
|
||||
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/find-up": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"locate-path": "^5.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/foreground-child": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||
@@ -4188,6 +4438,43 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data/node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data/node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fraction.js": {
|
||||
"version": "5.3.4",
|
||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
|
||||
@@ -4208,6 +4495,24 @@
|
||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-east-asian-width": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
|
||||
@@ -4220,6 +4525,43 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/github-from-package": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
||||
@@ -4249,12 +4591,63 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/help-me": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
|
||||
@@ -4722,6 +5115,18 @@
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-locate": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/log-symbols": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz",
|
||||
@@ -4768,6 +5173,15 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.54.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||
@@ -5352,6 +5766,42 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-try": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-locate": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-limit": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/package-json-from-dist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
@@ -5371,6 +5821,15 @@
|
||||
"url": "https://ko-fi.com/killymxi"
|
||||
}
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
@@ -5428,6 +5887,15 @@
|
||||
"pathe": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/pngjs": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
@@ -5560,6 +6028,12 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
|
||||
@@ -5570,6 +6044,23 @@
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
|
||||
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dijkstrajs": "^1.0.1",
|
||||
"pngjs": "^5.0.0",
|
||||
"yargs": "^15.3.1"
|
||||
},
|
||||
"bin": {
|
||||
"qrcode": "bin/qrcode"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rc": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||
@@ -5695,6 +6186,21 @@
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-main-filename": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/restore-cursor": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
|
||||
@@ -5767,6 +6273,12 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
||||
@@ -6385,6 +6897,12 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/which-module": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/worker-factory": {
|
||||
"version": "7.0.46",
|
||||
"resolved": "https://registry.npmjs.org/worker-factory/-/worker-factory-7.0.46.tgz",
|
||||
@@ -6550,6 +7068,88 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "15.4.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^6.0.0",
|
||||
"decamelize": "^1.2.0",
|
||||
"find-up": "^4.1.0",
|
||||
"get-caller-file": "^2.0.1",
|
||||
"require-directory": "^2.1.1",
|
||||
"require-main-filename": "^2.0.0",
|
||||
"set-blocking": "^2.0.0",
|
||||
"string-width": "^4.2.0",
|
||||
"which-module": "^2.0.0",
|
||||
"y18n": "^4.0.0",
|
||||
"yargs-parser": "^18.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "18.1.3",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"camelcase": "^5.0.0",
|
||||
"decamelize": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/yargs/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yoctocolors": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz",
|
||||
|
||||
@@ -8,10 +8,11 @@
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"db:init": "node scripts/init-database.js && node scripts/init-locations-db.js && node scripts/init-geofence-db.js",
|
||||
"db:init": "node scripts/init-database.js && node scripts/init-locations-db.js && node scripts/init-geofence-db.js && node scripts/init-notification-settings.js",
|
||||
"db:init:app": "node scripts/init-database.js",
|
||||
"db:init:locations": "node scripts/init-locations-db.js",
|
||||
"db:init:geofence": "node scripts/init-geofence-db.js",
|
||||
"db:init:notifications": "node scripts/init-notification-settings.js",
|
||||
"db:cleanup": "node scripts/cleanup-old-locations.js",
|
||||
"db:cleanup:7d": "node scripts/cleanup-old-locations.js 168",
|
||||
"db:cleanup:30d": "node scripts/cleanup-old-locations.js 720",
|
||||
@@ -21,7 +22,8 @@
|
||||
"test:geofence:mqtt": "node scripts/test-mqtt-geofence.js",
|
||||
"docs:check": "node scripts/check-docs.js",
|
||||
"docs:sync": "node scripts/sync-docs.js",
|
||||
"email:dev": "email dev"
|
||||
"email:dev": "email dev",
|
||||
"telegram:qr": "node scripts/generate-telegram-qr.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
@@ -34,6 +36,7 @@
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/react": "^19.2.4",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"axios": "^1.13.2",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"leaflet": "^1.9.4",
|
||||
@@ -41,6 +44,7 @@
|
||||
"next": "^16.0.7",
|
||||
"next-auth": "^5.0.0-beta.30",
|
||||
"nodemailer": "^7.0.10",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1",
|
||||
"react-email": "^4.3.2",
|
||||
|
||||
BIN
public/telegram-bot-qr.png
Normal file
BIN
public/telegram-bot-qr.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
40
scripts/generate-telegram-qr.js
Executable file
40
scripts/generate-telegram-qr.js
Executable file
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Generate QR Code for Telegram Bot
|
||||
*/
|
||||
|
||||
const QRCode = require('qrcode');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const BOT_URL = 'https://t.me/MeinTracking_bot';
|
||||
const OUTPUT_DIR = path.join(process.cwd(), 'public');
|
||||
const OUTPUT_FILE = path.join(OUTPUT_DIR, 'telegram-bot-qr.png');
|
||||
|
||||
async function generateQR() {
|
||||
try {
|
||||
// Ensure public directory exists
|
||||
if (!fs.existsSync(OUTPUT_DIR)) {
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// Generate QR Code
|
||||
await QRCode.toFile(OUTPUT_FILE, BOT_URL, {
|
||||
width: 400,
|
||||
margin: 2,
|
||||
color: {
|
||||
dark: '#000000',
|
||||
light: '#FFFFFF'
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`✅ QR Code generated: ${OUTPUT_FILE}`);
|
||||
console.log(`📱 Bot URL: ${BOT_URL}`);
|
||||
console.log(`🖼️ QR Code size: 400x400px`);
|
||||
} catch (error) {
|
||||
console.error('❌ Error generating QR Code:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
generateQR();
|
||||
38
scripts/init-notification-settings.js
Executable file
38
scripts/init-notification-settings.js
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Initialize UserNotificationSettings table
|
||||
*/
|
||||
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
|
||||
const dbPath = path.join(process.cwd(), 'data', 'database.sqlite');
|
||||
|
||||
try {
|
||||
const db = new Database(dbPath);
|
||||
|
||||
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);
|
||||
`);
|
||||
|
||||
db.close();
|
||||
|
||||
console.log('✅ Notification settings table initialized!');
|
||||
} catch (error) {
|
||||
console.error('❌ Error:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
56
scripts/test-telegram-token.js
Executable file
56
scripts/test-telegram-token.js
Executable file
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Test Telegram Bot Token
|
||||
*/
|
||||
|
||||
const axios = require('axios');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Read .env manually
|
||||
const envPath = path.join(process.cwd(), '.env');
|
||||
const envContent = fs.readFileSync(envPath, 'utf-8');
|
||||
const BOT_TOKEN = envContent
|
||||
.split('\n')
|
||||
.find(line => line.startsWith('TELEGRAM_BOT_TOKEN='))
|
||||
?.split('=')[1]
|
||||
?.trim();
|
||||
|
||||
async function testToken() {
|
||||
if (!BOT_TOKEN) {
|
||||
console.error('❌ TELEGRAM_BOT_TOKEN not found in .env');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('🧪 Testing Telegram Bot Token...\n');
|
||||
console.log(`Token: ${BOT_TOKEN.substring(0, 15)}...`);
|
||||
|
||||
try {
|
||||
const response = await axios.get(`https://api.telegram.org/bot${BOT_TOKEN}/getMe`);
|
||||
|
||||
if (response.data.ok) {
|
||||
const bot = response.data.result;
|
||||
console.log('\n✅ Bot Token is VALID!\n');
|
||||
console.log('📱 Bot Information:');
|
||||
console.log(` ID: ${bot.id}`);
|
||||
console.log(` Name: ${bot.first_name}`);
|
||||
console.log(` Username: @${bot.username}`);
|
||||
console.log(` Can Join Groups: ${bot.can_join_groups}`);
|
||||
console.log(` Can Read Messages: ${bot.can_read_all_group_messages}`);
|
||||
console.log('\n🔗 Bot Link: https://t.me/' + bot.username);
|
||||
} else {
|
||||
console.error('❌ Invalid response:', response.data);
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('\n❌ Token test FAILED!');
|
||||
if (error.response) {
|
||||
console.error('Error:', error.response.data.description);
|
||||
} else {
|
||||
console.error('Error:', error.message);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
testToken();
|
||||
Reference in New Issue
Block a user