# Intégration Gotify - Notifications Push Ce document décrit l'intégration de Gotify pour les notifications push dans Mesh. --- ## 📋 Vue d'Ensemble Gotify est utilisé pour envoyer des notifications push aux utilisateurs lorsqu'ils sont **absents** (non connectés via WebSocket). Les notifications sont envoyées pour: 1. **Messages de chat** - Quand un utilisateur reçoit un message alors qu'il n'est pas dans la room 2. **Appels WebRTC** - Quand quelqu'un essaie d'appeler un utilisateur absent 3. **Partages de fichiers** (future) - Quand un fichier est partagé avec un utilisateur absent **Principe clé**: Les notifications sont envoyées **uniquement si l'utilisateur est absent**. Si l'utilisateur est connecté et actif dans la room, il reçoit les événements via WebSocket en temps réel (pas de notification). --- ## ⚙️ Configuration ### Serveur Gotify **URL de test**: `http://10.0.0.5:8185` **Application**: `mesh` **Token**: `AvKcy9o-yvVhyKd` ### Variables d'Environnement Dans `server/.env`: ```bash # Gotify Integration GOTIFY_URL=http://10.0.0.5:8185 GOTIFY_TOKEN=AvKcy9o-yvVhyKd ``` **Notes**: - `GOTIFY_URL` et `GOTIFY_TOKEN` sont **optionnels** - Si non configurés, les notifications sont désactivées (logs warning) - Le serveur Mesh fonctionne normalement sans Gotify ### Configuration dans le Code Fichier: `server/src/config.py` ```python # Gotify (optionnel) gotify_url: Optional[str] = None gotify_token: Optional[str] = None ``` --- ## 🏗️ Architecture ### Client Gotify Fichier: `server/src/notifications/gotify.py` **Classe principale**: `GotifyClient` ```python class GotifyClient: def __init__(self): self.url = settings.GOTIFY_URL self.token = settings.GOTIFY_TOKEN self.enabled = bool(self.url and self.token) async def send_notification( title: str, message: str, priority: int = 5, extras: Optional[Dict[str, Any]] = None ) -> bool ``` **Méthodes spécifiques**: 1. `send_chat_notification()` - Notification de chat 2. `send_call_notification()` - Notification d'appel WebRTC 3. `send_file_notification()` - Notification de fichier (future) ### Instance Globale ```python from src.notifications.gotify import gotify_client # Utilisation await gotify_client.send_chat_notification( from_username="Alice", room_name="Team Chat", message="Hello Bob!", room_id="room-uuid" ) ``` --- ## 📨 Types de Notifications ### 1. Messages de Chat **Trigger**: Utilisateur envoie un message via WebSocket **Condition**: Destinataire **pas connecté** dans la room **Exemple**: ```json { "title": "💬 Alice dans Team Chat", "message": "Hey, can you review my PR?", "priority": 6, "extras": { "client::notification": { "click": { "url": "mesh://room/abc-123-def" } } } } ``` **Code** (`server/src/websocket/handlers.py`): ```python async def handle_chat_message_send(...): # ... créer et broadcast message ... # Envoyer notifications aux absents await self._send_chat_notifications( room, sender, content, room_id_str, peer_id ) ``` **Logique**: ```python async def _send_chat_notifications(...): members = db.query(RoomMember).filter(...) for member in members: if member.user_id == sender.id: continue # Pas de notif pour l'expéditeur is_online = manager.is_user_in_room(user.user_id, room_id) if not is_online: await gotify_client.send_chat_notification(...) ``` ### 2. Appels WebRTC **Trigger**: Utilisateur envoie un `rtc.offer` via WebSocket **Condition**: Destinataire **pas connecté** **Exemple**: ```json { "title": "📞 Appel audio/vidéo de Alice", "message": "Appel entrant dans Team Chat", "priority": 8, "extras": { "client::notification": { "click": { "url": "mesh://room/abc-123-def" } } } } ``` **Code** (`server/src/websocket/handlers.py`): ```python async def handle_rtc_signal(...): if event_data.get("type") == EventType.RTC_OFFER: target_is_online = manager.is_connected(target_peer_id) if not target_is_online: await gotify_client.send_call_notification( from_username=user.username, room_name=room.name, room_id=room_id, call_type="audio/vidéo" ) ``` ### 3. Partages de Fichiers (Future) **Trigger**: Utilisateur partage un fichier via P2P **Condition**: Destinataire **pas connecté** **Exemple**: ```json { "title": "📁 Alice a partagé un fichier", "message": "Fichier: document.pdf\nDans: Team Chat", "priority": 5, "extras": { "client::notification": { "click": { "url": "mesh://room/abc-123-def" } } } } ``` **Code** (à implémenter): ```python await gotify_client.send_file_notification( from_username="Alice", room_name="Team Chat", filename="document.pdf", room_id="abc-123" ) ``` --- ## 🔔 Niveaux de Priorité Gotify utilise des priorités de 0 (minimum) à 10 (maximum). | Type | Priorité | Raison | |------|----------|--------| | Messages de chat | 6 | Important mais pas urgent | | Appels WebRTC | 8 | Haute priorité (appel entrant) | | Fichiers partagés | 5 | Normal | | Erreurs système | 7 | Attention requise | **Mapping Gotify**: - 0-3: Silent / Low - 4-7: Normal - 8-10: High / Emergency --- ## 🎯 Extras et Actions Gotify supporte des métadonnées supplémentaires pour enrichir les notifications. ### Click Action ```json "extras": { "client::notification": { "click": { "url": "mesh://room/{room_id}" } } } ``` **Comportement**: - Clic sur notification → Ouvre l'app Mesh sur la room - URL scheme: `mesh://room/{room_id}` ### Android Actions ```json "extras": { "android::action": { "onReceive": { "intentUrl": "mesh://room/{room_id}" } } } ``` **Comportement**: - Android intent pour deep linking - Compatible avec apps mobiles ### Markdown Content ```json "extras": { "client::display": { "contentType": "text/markdown" } } ``` **Comportement**: - Message formaté en Markdown - Liens, bold, italique supportés --- ## 🧪 Tests ### Test 1: Envoi Direct Fichier: `server/test_gotify.py` ```bash cd server python3 test_gotify.py ``` **Résultat attendu**: ``` ✅ Notification envoyée avec succès à Gotify Response: {'id': 78623, 'appid': 4, ...} ``` **Vérification**: - Ouvrir l'app Gotify sur mobile/web - Notification visible avec titre "🧪 Test Mesh" ### Test 2: Chat End-to-End **Setup**: 1. Alice et Bob créent des comptes 2. Alice crée une room "Test Gotify" 3. Alice invite Bob à la room 4. **Bob se déconnecte** (ferme navigateur) 5. Alice envoie un message dans la room **Résultat attendu**: - Bob reçoit une notification Gotify sur son téléphone - Titre: "💬 Alice dans Test Gotify" - Message: Contenu du message d'Alice (tronqué à 100 chars) - Clic → Ouvre Mesh sur la room **Logs serveur**: ``` INFO - Notification Gotify envoyée à bob pour message dans Test Gotify ``` ### Test 3: Appel WebRTC **Setup**: 1. Alice et Bob dans la room "Test Gotify" 2. **Bob se déconnecte** 3. Alice active sa caméra (déclenche WebRTC offer) **Résultat attendu**: - Bob reçoit une notification Gotify - Titre: "📞 Appel audio/vidéo de Alice" - Message: "Appel entrant dans Test Gotify" - Priorité: 8 (haute) **Logs serveur**: ``` DEBUG - Relayed rtc.offer from peer_xxx to peer_yyy ``` --- ## 🔍 Debugging ### Vérifier Configuration ```python # Dans server/src/notifications/gotify.py logger.info(f"Gotify configuré: {self.url}") logger.info(f"Gotify enabled: {self.enabled}") ``` **Logs attendus**: ``` INFO - Gotify configuré: http://10.0.0.5:8185 INFO - Gotify enabled: True ``` Si `enabled: False`: ``` WARNING - Gotify non configuré - notifications désactivées ``` ### Tester Envoi HTTP ```bash curl -X POST "http://10.0.0.5:8185/message?token=AvKcy9o-yvVhyKd" \ -H "Content-Type: application/json" \ -d '{ "title": "Test cURL", "message": "Hello from cURL", "priority": 5 }' ``` **Réponse attendue**: ```json { "id": 78624, "appid": 4, "message": "Hello from cURL", "title": "Test cURL", "priority": 5, "date": "2026-01-04T08:00:00Z" } ``` ### Logs Détaillés Activer DEBUG dans `server/.env`: ```bash LOG_LEVEL=DEBUG ``` **Relancer serveur**: ```bash docker restart mesh-server docker logs -f mesh-server ``` **Logs attendus**: ``` DEBUG - Notification Gotify envoyée à bob pour message dans Team Chat INFO - Notification Gotify envoyée: 💬 Alice dans Team Chat ``` --- ## 🚨 Gestion des Erreurs ### Gotify Inaccessible ```python try: response = await client.post(...) except httpx.HTTPError as e: logger.error(f"Erreur envoi Gotify: {e}") return False ``` **Comportement**: - Erreur loggée - Notification non envoyée - **Application continue normalement** - WebSocket events toujours envoyés ### Token Invalide **Erreur HTTP**: 401 Unauthorized **Log**: ``` ERROR - Erreur envoi Gotify: 401 Client Error: Unauthorized ``` **Fix**: - Vérifier `GOTIFY_TOKEN` dans `.env` - Régénérer token dans Gotify si nécessaire ### Timeout **Config**: ```python async with httpx.AsyncClient(timeout=5.0) as client: ``` **Erreur**: `httpx.ReadTimeout` **Log**: ``` ERROR - Erreur envoi Gotify: ReadTimeout ``` **Fix**: - Vérifier connectivité réseau - Augmenter timeout si nécessaire --- ## 📊 Métriques ### Taux d'Envoi Avec 100 utilisateurs et 10 messages/minute: - Utilisateurs en ligne: ~70% - Utilisateurs absents: ~30% - **Notifications Gotify**: ~30/minute (seulement les absents) ### Performance **Latence envoi**: <100ms (réseau local) **Timeout**: 5s (configurable) **Impact serveur**: Négligeable (requêtes async) --- ## 🔐 Sécurité ### Token Gotify **Stockage**: Variable d'environnement `.env` **Permissions**: Le token doit avoir permission `messages:create` **Rotation**: Régénérer le token régulièrement en production ### URL Scheme **Format**: `mesh://room/{room_id}` **Validation**: Le client mobile doit valider le `room_id` **Sécurité**: Pas de données sensibles dans l'URL ### Contenu Messages **Tronqué**: Messages >100 chars sont tronqués **Sanitization**: Pas d'exécution de code dans les messages **Markdown**: Désactivé par défaut (text/plain) --- ## 🚀 Production ### Variables d'Environnement ```bash # Production GOTIFY_URL=https://gotify.yourdomain.com GOTIFY_TOKEN=your-production-token-change-this ``` ### HTTPS ⚠️ **Obligatoire en production** ```bash GOTIFY_URL=https://gotify.yourdomain.com ``` Pas de HTTP en production pour éviter: - Interception du token - Man-in-the-middle attacks ### High Availability **Option 1**: Gotify derrière load balancer **Option 2**: Queue de notifications (Redis) - Si Gotify down → Queue les notifications - Retry automatique - Pas de perte de notifications **Option 3**: Fallback multiple providers - Gotify primaire - FCM/APNS fallback - Email en dernier recours --- ## 📱 Client Mobile (Future) ### Deep Linking **iOS**: ```swift // AppDelegate.swift func application( _ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:] ) -> Bool { if url.scheme == "mesh" { // Parse: mesh://room/{room_id} let roomId = url.host navigateToRoom(roomId) } } ``` **Android**: ```xml ``` ### Gotify Client **iOS/Android**: Utiliser l'app Gotify officielle **Custom app**: Implémenter WebSocket Gotify - `wss://gotify.yourdomain.com/stream?token=xxx` - Recevoir notifications en temps réel --- ## 🔗 Références - [Gotify Documentation](https://gotify.net/docs/) - [Gotify Message Extras](https://gotify.net/docs/msgextras) - [Gotify API](https://gotify.net/api-docs) - [httpx Documentation](https://www.python-httpx.org/) --- ## ✅ Checklist Déploiement Avant de déployer en production: - [ ] Gotify serveur installé et accessible - [ ] HTTPS activé sur Gotify - [ ] Token Gotify créé avec permissions correctes - [ ] Variables `GOTIFY_URL` et `GOTIFY_TOKEN` dans `.env` - [ ] Test envoi direct réussi (`test_gotify.py`) - [ ] Test end-to-end chat réussi - [ ] Test end-to-end appel WebRTC réussi - [ ] Logs serveur confirmant envois - [ ] App mobile configurée avec deep linking - [ ] Monitoring des erreurs Gotify (logs) - [ ] Plan de fallback si Gotify down --- **Intégration complète et testée!** 🎉