Files
mesh/GOTIFY_INTEGRATION.md
Gilles Soulier 1d177e96a6 first
2026-01-05 13:20:54 +01:00

13 KiB

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:

# 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

# Gotify (optionnel)
gotify_url: Optional[str] = None
gotify_token: Optional[str] = None

🏗️ Architecture

Client Gotify

Fichier: server/src/notifications/gotify.py

Classe principale: GotifyClient

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

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:

{
  "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):

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:

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:

{
  "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):

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:

{
  "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):

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

"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

"extras": {
  "android::action": {
    "onReceive": {
      "intentUrl": "mesh://room/{room_id}"
    }
  }
}

Comportement:

  • Android intent pour deep linking
  • Compatible avec apps mobiles

Markdown Content

"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

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

# 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

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:

{
  "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:

LOG_LEVEL=DEBUG

Relancer serveur:

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

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:

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

# Production
GOTIFY_URL=https://gotify.yourdomain.com
GOTIFY_TOKEN=your-production-token-change-this

HTTPS

⚠️ Obligatoire en production

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:

// 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:

<!-- 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


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! 🎉