635 lines
13 KiB
Markdown
635 lines
13 KiB
Markdown
<!--
|
|
Created by: Claude
|
|
Date: 2026-01-03
|
|
Purpose: Documentation intégration Gotify
|
|
Refs: CLAUDE.md
|
|
-->
|
|
|
|
# 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
|
|
<!-- AndroidManifest.xml -->
|
|
<intent-filter>
|
|
<action android:name="android.intent.action.VIEW" />
|
|
<category android:name="android.intent.category.DEFAULT" />
|
|
<data android:scheme="mesh" android:host="room" />
|
|
</intent-filter>
|
|
```
|
|
|
|
### 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!** 🎉
|