← Tous les manuels · Portail

MANUAL — openreply

Manuel d'instructions de cette app. À LIRE avant toute intervention, et à METTRE À JOUR après chaque modification (ajouter une ligne au CHANGELOG en bas).
Dernière génération/MAJ : 2026-08-03 · Statut : en ligne, en attente des comptes externes (Resend + Meta)

1. Identité

  • - Rôle : auto-DM Instagram (alternative open-source à ManyChat). Quelqu'un commente un mot-clé sous un post/reel → reçoit automatiquement un DM avec un lien. Gère aussi réponses publiques, liens trackés + analytics, follow-gate, messages personnalisés.
  • - URL publique : https://openreply.panelbay.com
  • - Entité : Matt (perso / MOA / RP selon le compte Instagram connecté). Auto-hébergé, usage = comptes Instagram de Matt uniquement (pas d'ouverture à des tiers → pas de Meta App Review).
  • - Source : fork/clone de https://github.com/diwenne/openreply
  • 2. Exécution / infra

  • - Dossier : /root/workspace/apps/openreply
  • - Stack : Next.js 16 + React 19 (web/API), Prisma 7 + PostgreSQL, BullMQ + Redis (queue), Auth.js (magic link email via Resend), Tailwind. API Instagram officielle Meta.
  • - 2 process (les deux nécessaires) :
  • - Datastores (conteneurs Docker, bindés sur 127.0.0.1, --restart unless-stopped) :
  • - Reverse proxy : nginx vhost /etc/nginx/sites-enabled/openreply.panelbay.com.conf → 127.0.0.1:8351, HTTPS Let's Encrypt (exposé via broker DNS + skill expose-webapp).
  • - Env : EnvironmentFile=/root/workspace/apps/openreply/.env (les deux services). Le worker NE charge PAS .env seul, d'où l'EnvironmentFile systemd.
  • - Redémarrer : sudo systemctl restart openreply-web openreply-worker. Après un git pull / changement de code : npm install (si deps) puis npm run build puis restart web ; worker = restart (tsx lit le TS直). Après changement de .env : restart des 2 services.
  • 3. Structure & fichiers clés

  • - app/ (routes Next 16, dont app/api/health, app/api/webhook, app/api/instagram/callback), worker/dm-worker.ts (worker), lib/, prisma/schema.prisma + prisma/migrations/, .env (secrets, chmod 600, gitignored).
  • - docs/setup.md = la doc d'install officielle (Meta pas-à-pas). META_APP_REVIEW.md = justif si un jour ouverture à des tiers.
  • 4. Données

  • - PostgreSQL (conteneur openreply-pg) : campagnes, logs, comptes, sessions. Tables de diagnostic utiles : WebhookEvent (réception webhook), DmLog (statut/erreur d'envoi), OperationalEvent (crashs worker + sweeps du reconciler).
  • - Redis (conteneur openreply-redis) : queue BullMQ + rate limiter par compte.
  • - Migrations Prisma : npm run db:migrate (= prisma migrate deploy). Client généré dans app/generated/prisma.
  • 5. API / endpoints

  • - GET /api/health — état DB / Redis / queue / worker (heartbeat). worker.healthy:false = worker down → aucun DM ne partira.
  • - POST /api/webhook — reçoit les événements comments de Meta (à configurer côté Meta avec le WEBHOOK_VERIFY_TOKEN).
  • - GET /api/instagram/callback — callback OAuth Instagram (à déclarer dans l'app Meta : https://openreply.panelbay.com/api/instagram/callback).
  • - Pages publiques exigées par Meta : /privacy, /terms, /data-deletion.
  • - Endpoints cron (protégés par Authorization: Bearer $CRON_SECRET, méthode GET) : /api/cron/refresh-tokens (rafraîchit les tokens IG long-lived à J-10 de l'expiration + reset des compteurs d'usage mensuels), /api/cron/snapshot-followers, /api/cron/attach-next-reel. Un appel non authentifié renvoie 401.
  • 5bis. Cron / maintenance planifiée (systemd timers)

  • - Refresh des tokens Instagram (CRITIQUE) : les tokens long-lived Meta expirent à ~60 jours et le endpoint ne rafraîchit qu'à J-10. Sans déclencheur, tout compte IG connecté se déconnecte silencieusement à 60 jours. Câblé via un timer systemd :
  • 6. Intégrations & secrets

    Tout dans /root/workspace/apps/openreply/.env (jamais les valeurs ici, juste où) :

  • - Générés automatiquement (faits) : NEXTAUTH_SECRET, CRON_SECRET, ENCRYPTION_KEY (64 hex, identique web+worker, chiffre les tokens IG), WEBHOOK_VERIFY_TOKEN.
  • - Infra locale (faits) : DATABASE_URL, REDIS_URL, NEXTAUTH_URL=https://openreply.panelbay.com.
  • - À REMPLIR (comptes externes de Matt) :
  • 7. Pièges connus / à savoir

  • - ENCRYPTION_KEY = exactement 64 hex, sinon l'app throw au boot. Doit être identique web et worker (ils la partagent via le même .env). La changer = tous les comptes IG connectés doivent se reconnecter.
  • - Le worker doit tourner en permanence (queue consumer). /api/health → worker.healthy le dit.
  • - Compte Instagram Business/Creator obligatoire (pas perso). En dev, seul un compte ajouté comme *tester* ET qui a *accepté l'invite côté Instagram* peut se connecter (sinon "Insufficient Developer Role").
  • - L'app doit être "Live" côté Meta pour recevoir les vrais webhooks (en Development, seul le bouton Test envoie des events).
  • - Webhook callback = domaine primaire exact, sans slash final. Un domaine non-primaire fait un 307 que Meta ne suit pas → webhooks silencieusement perdus.
  • - DM aux non-abonnés = onglet "Général" (Message Requests), pas "Principale". Un DM d'automatisation envoyé à quelqu'un qui ne suit pas le compte (ou sans historique de conv) tombe dans l'onglet Général d'Instagram : c'est normal, le DM EST délivré (statut SENT), il n'apparait juste pas dans la boîte principale. Piège classique en test ("j'ai rien reçu" alors que c'est dans Général). Confirmé 2026-09-27.
  • - Un mot-clé = une entrée du tableau keywords. Chaque élément est matché tel quel (regex whole-word si wholeWordMatch=true). Mettre plusieurs mots dans une seule case séparés par un espace (ex. "paraguay paraguai") crée UN seul mot-clé = l'expression exacte "paraguay paraguai", qui ne matche jamais un commentaire ne disant que "Paraguay" → aucun DM. Pour plusieurs variantes, les saisir comme entrées séparées ({paraguay,paraguai}). Vu le 2026-09-27 sur la campagne "Paraguay" de mattparaguay7.
  • - Diagnostiquer une panne : interroger Postgres directement (WebhookEvent → DmLog → OperationalEvent), plus rapide que les logs. Comparer une campagne qui marche à une qui échoue (même compte ou autre) isole vite le champ fautif.
  • - Datastores en Docker : si docker redémarre, les conteneurs remontent seuls (--restart unless-stopped). Ne pas exposer 5432/6379 publiquement (ils sont bindés 127.0.0.1). Le docker-compose.yml upstream mappait les ports en 0.0.0.0 : corrigé en 127.0.0.1:... + images pinnées par digest (un futur docker compose up n'exposera donc plus Postgres/Redis).
  • - En-têtes de sécurité : next.config.ts renvoie désormais poweredByHeader:false (plus de X-Powered-By) + headers() (HSTS, X-Frame-Options DENY, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, CSP de base). La CSP est volontairement permissive sur script-src/style-src ('unsafe-inline' 'unsafe-eval' https:) pour ne pas casser le dashboard Next/React ; frame-ancestors 'none' verrouille le clickjacking. Toute modif de next.config.ts impose npm run build + restart des 2 services avant de considérer l'app saine.
  • 8. CHANGELOG

  • - 2026-09-27 — Fix campagne "Paraguay" de mattparaguay7 (mot-clé mal saisi, aucun DM). Symptôme (Matt) : sa dernière vidéo a des commentaires "Paraguay" mais personne n'est contacté. Diag Postgres : webhooks bien reçus et PROCESSED (arthur.k.diary, sylvain.dufayet, etc.), app/worker/compte sains, MAIS 0 DmLog pour mattparaguay7 alors que la campagne "palabra paraguay" de monica.inversiones ({Paraguay}) envoyait 81 DM. Cause : keywords={"paraguay paraguai"} = une seule expression (les 2 mots collés dans une case, séparés par un espace). Avec wholeWordMatch=true le matcher cherche la phrase exacte "paraguay paraguai" → ne matche jamais "Paraguay" seul. Fix : UPDATE Automation SET keywords=ARRAY['paraguay','paraguai']. Restart openreply-worker → le balayage de rattrapage (poll toutes les 5 min, lookback 72h) a enqueue+envoyé 8 DM d'un coup (dont arthur.k.diary et sylvain.dufayet). Piège ajouté en §7. Aucune modif de code.
  • - 2026-08-03 — Installation initiale (auto-hébergée VPS). Clone du repo, npm install, Postgres 16 + Redis 7 en conteneurs Docker (bindés 127.0.0.1), génération des 4 secrets locaux, .env écrit (infra + secrets faits ; Resend + Meta laissés vides), prisma migrate deploy (toutes migrations appliquées, Prisma 7.8), npm run build OK. 2 services systemd créés (openreply-web :8351 + openreply-worker). Exposé en HTTPS via broker : https://openreply.panelbay.com. /api/health = status ok (db/redis/queue/worker tous verts). RESTE (côté Matt) : compte Resend (login) + app Meta developer (webhooks Instagram) + connexion d'un compte Instagram Business.
  • CHANGELOG

  • - 2026-09-25 : Connexion de mattparaguay7 réparée + fix endpoint token IG. Symptôme : à la connexion de mattparaguay7, écran "La conexión con Instagram falló / Unsupported request - method type: get" (IGApiException code 100), reproductible dans 2 workspaces. Les 3 autres comptes du workspace resident.paraguay@gmail.com (mattatma, resident_paraguay, resident_paraguay_en) passent sans souci. Diagnostic pas à pas (logs [Meta debug]/[Meta diag] temporaires, retirés depuis) : (1) l'échange code→token court réussit (token IGAA…, 4 permissions business accordées, user_id présent) ; (2) MAIS tous les appels graph.instagram.com avec CE token échouent, y compris un simple GET /me, en GET comme en POST → ce n'est ni la méthode ni l'URL. (3) Preuve : un token stocké d'un compte qui marche (mattatma) répond 200 OK sur graph.instagram.com/me le même jour → serveur/URL/code OK. (4) Le compte est bien Professionnel (Créateur). Vraie cause : accès Standard / mode dev sur les permissions Instagram → seuls les comptes ayant un rôle sur l'app obtiennent un token FONCTIONNEL ; un compte externe (mattparaguay7) passe l'autorisation mais reçoit un token inerte. Fix : ajouté mattparaguay7 comme Instagram Tester (console Meta → App Roles → Roles → Instagram Testers) + invite acceptée côté Instagram → reconnexion OK (instagramId 17841449139467968, webhook actif). Côté code, correctif secondaire conservé : les endpoints token IG (getLongLivedToken/refreshLongLivedToken dans lib/meta/client.ts) sont désormais bâtis sur https://graph.instagram.com sans préfixe de version (conforme à la doc Meta, via constante INSTAGRAM_GRAPH_HOST) au lieu de …/v23.0/ ; tolérance ajoutée au format data:[] du token court dans lib/meta/oauth.ts (défensif). npm run build + restart openreply-web. Leçon : "Unsupported request - method type: get" à la connexion d'un compte externe = compte pas ajouté en Instagram Tester, PAS un bug de code.
  • - 2026-09-09 : Ouverture multi-agents (self-service). Objectif : permettre aux agents immobiliers de Matt de se connecter seuls et de gerer leurs propres campagnes, chacun isole sur SON compte Instagram. Constat : l'app est deja multi-tenant nativement (modele Workspace + ensureWorkspaceForUser cree un workspace OWNER par utilisateur au 1er login ; toutes les requetes filtrent par workspaceId derive de la session, jamais du client -> isolation etanche verifiee). Login = magic link email (self-service). Aucune modif de code. (1) Expediteur des emails de login corrige : EMAIL_FROM passe de onboarding@resend.dev (sandbox, n'envoie qu'a soi) a OpenReply <login@panelbay.com>. Domaine panelbay.com cree dans Resend (region eu-west-1, id 1f2278a5-...), 3 enregistrements DNS (DKIM resend._domainkey, MX+SPF send) poses dans Cloudflare (zone panelbay.com), resolvent publiquement ; verification Resend en cours au moment de l'ecriture (DNS OK). Backup .env.bak.* cree. Restart openreply-web, health OK. (2) Path Meta choisi = testers : chaque compte IG d'agent doit etre ajoute comme Instagram Tester dans la console Meta (app en mode Development), l'agent accepte l'invite cote Instagram ; pas d'App Review. (3) Tutoriel agents en espagnol : /root/workspace/OpenReply-tutorial-agentes-ES.md. NB : l'UI est en anglais (traduction ES non faite, chantier separe). RESTE cote Matt : ajouter les IG des agents en testers (console Meta) ; option future = allowlist d'emails a l'inscription (aujourd'hui signup ouvert, mais gate naturelle car sans statut tester l'agent ne peut pas connecter d'IG). MAJ fin de session : domaine panelbay.com verified cote Resend (email test envoye). UI traduite en espagnol : 24 fichiers de la surface agent (app/(dashboard)/*, login, verify-request, composants dashboard) passes en espagnol neutre LatAm (imperatifs "vos"), npm run build OK, restart web, login/verify rendus en ES verifies. Non traduits volontairement : pages marketing/SEO publiques, pages legales (privacy/terms/data-deletion), valeurs d'enum affichees telles quelles (OWNER/ADMIN/MEMBER). Espagnol code en dur, pas de toggle multilingue (chantier separe).
  • - 2026-08-05 : Durcissement config/instance (faible risque, pas de refonte).
  • (1) Cron refresh tokens IG câblé (item audit #2, P1) : timer systemd openreply-refresh-tokens.timer (quotidien 04:17, Persistent) + .service oneshot appelant /api/cron/refresh-tokens avec Bearer $CRON_SECRET. Testé : {"success":true,...} (auth OK). Empêche la déconnexion silencieuse des comptes IG à 60 jours.

    (2) En-têtes de sécurité (item #3) : next.config.ts → poweredByHeader:false + headers() (HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, CSP de base permissive scripts/styles). npm run build OK, restart des 2 services, healthcheck vert, X-Powered-By absent, pages Meta /privacy /terms /data-deletion = 200.

    (3) docker-compose.yml (item #9) : ports Postgres/Redis passés de 0.0.0.0 à 127.0.0.1:..., images pinnées par digest (postgres:16@sha256:33f923…, redis:7-alpine@sha256:e7723f…). N'affecte pas les conteneurs déjà lancés.

    NON fait (reporté à l'orchestrateur/Matt) : item #1 deps vulnérables (npm audit = 17 vulns, 2 critiques dans la couche auth @auth/core/next-auth beta) — npm audit fix NON appliqué (risque sur le flux magic-link d'un fork en prod, un build vert ne valide pas l'auth runtime) ; items nginx #4 rate-limit + en-têtes vhost, #5 DoS écriture DB, #6/#7/#8 (RBAC diagnostics, observabilité, systemd non-root).

  • - 2026-08-04 : URL definitive fixee sur panelbay.com. Nouvelle URL https://openreply.panelbay.com (HTTPS Cloudflare + Let's Encrypt), NEXTAUTH_URL et callbacks OAuth pointent desormais dessus. L'ancienne https://openreply.bouthors-m.tenga.run reste servie mais n'est plus l'URL de reference.