Bot Twitch de ma chaîne. Le bot est modulaire à partir de plugin. Différents plugins sont fournis par défaut pour faciliter mon quotidien de streamer et augmenter ma flemme.
  • Python 59.4%
  • JavaScript 23.4%
  • HTML 11.8%
  • CSS 5.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-23 02:50:29 +02:00
instance/plugin_data/soundboard Ajout de plugins + Triggers 2026-08-26 13:09:42 +02:00
plugins Correction du bouton enregistrer dans le plugin « Bienvenue » 4 2026-09-23 00:52:16 +02:00
tests Passage à yt-dlp plutôt 2026-09-23 02:41:15 +02:00
twitchiou Contrainte vidéo pour éviter coupure verticale. 2026-09-23 02:50:29 +02:00
.env.example Quelques mises à jours au niveau des plugins 2026-09-11 18:13:35 +02:00
.gitignore Ignore Python cache files 2026-09-11 19:34:24 +02:00
external_api.md Ajout d'une interfaçage extérieur pour des appels d'API Externe (évènnements personnalisés). 2026-08-20 14:40:09 +02:00
README.md Ajout du plugin MetaSave / Mario Kart 7 avec intégration 2026-09-19 15:10:50 +02:00
requirements.txt Ajout d'un rapport en PJ 2026-09-13 12:11:23 +02:00
run.py Commit initial parce que j'avais la flemme de le faire avant. Il y a des plugins packagés avec de base. 2026-08-13 16:25:08 +02:00

Twitchiou

Twitchiou est un bot Twitch modulaire en Python/Flask doté dun panneau de contrôle Bootstrap pleine largeur. Il utilise deux identités OAuth séparées :

  • le bot lit et écrit dans le chat et exécute les actions de modération ;
  • le broadcaster pilote les sondages, prédictions, récompenses et opérations réservées à la chaîne.

Le cœur maintient deux transports EventSub WebSocket, normalise les notifications dans une enveloppe JSON stable, les affiche en direct dans lUI et les diffuse aux plugins actifs.

Installation

Prérequis : Python 3.11 ou plus récent et une application créée dans la console développeur Twitch.

Dans lapplication Twitch, ajoutez exactement cette URL OAuth :

https://localhost:5000/oauth/callback

Puis lancez :

python -m venv .venv
.venv/bin/pip install -r requirements.txt
cp .env.example .env

Sous PowerShell :

python -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
Copy-Item .env.example .env

Renseignez les paires TWITCH_BROADCASTER_CLIENT_ID / TWITCH_BROADCASTER_CLIENT_SECRET et TWITCH_BOT_CLIENT_ID / TWITCH_BOT_CLIENT_SECRET, ainsi quune longue valeur aléatoire dans APP_SECRET. TOKEN_ENCRYPTION_KEY peut être une autre longue valeur dédiée ; sinon APP_SECRET sert à chiffrer les jetons OAuth.

.venv/bin/python run.py

Sous PowerShell :

.\.venv\Scripts\python.exe run.py

Ouvrez ensuite https://localhost:5000. Au premier démarrage, Twitchiou génère un certificat HTTPS autosigné dans instance/certificates/. Le navigateur affichera donc un avertissement tant que ce certificat naura pas été approuvé localement. Vérifiez son empreinte avant de poursuivre.

Les anciennes adresses http://localhost:5000/... sont redirigées sur le même port vers leur équivalent HTTPS ; aucune page ni WebSocket applicatif nest servi en clair.

Les identifiants Twitch du Broadcaster et du Bot peuvent être renseignés et connectés séparément dans Paramètres → Comptes Twitch. LURL https://localhost:5000/oauth/callback doit être autorisée exactement dans les deux applications de la console Twitch. Une ancienne valeur HTTP présente dans lenvironnement est automatiquement forcée en HTTPS au chargement.

Une tâche darrière-plan surveille les deux jetons chaque minute et les renouvelle dix minutes avant leur expiration. Ces délais peuvent être ajustés avec TOKEN_REFRESH_INTERVAL et TOKEN_REFRESH_MARGIN (en secondes).

Le composant principal surveille également laudience : le nombre de viewers est contrôlé toutes les 30 secondes, puis les abonnements et followers toutes les 5 minutes. Les délais sont configurables avec VIEWER_COUNT_INTERVAL, SUB_COUNT_INTERVAL et FOLLOWER_COUNT_INTERVAL. Le suivi détaillé des followers utilise le scope moderator:read:followers ; reconnectez le Broadcaster après une mise à jour si Twitch doit accorder ce scope.

Le compte bot doit être modérateur de la chaîne. Vous pouvez le faire avec /mod nom_du_bot dans le chat du broadcaster. Sans ce rôle, Twitch refusera les abonnements et actions de modération même si le jeton possède les scopes nécessaires.

Vidéos et clips

La page Contenus liste les vidéos et les clips de la chaîne dans deux onglets, par pages de 10. Chaque élément peut être ouvert sur Twitch ou téléchargé en arrière-plan avec yt-dlp. Le téléchargement continue lorsque longlet du navigateur est fermé ; sa progression est restaurée sur la miniature à la prochaine ouverture. Les fichiers sont enregistrés sur le bureau par défaut ; changez le dossier dans .env si nécessaire :

TWITCH_CONTENT_OUTPUT_DIR=~/Desktop
TWITCH_CONTENT_AUTO_DOWNLOAD=false

Le dossier est créé automatiquement lors du premier téléchargement. Un chemin relatif est résolu depuis la racine du projet. Ces deux valeurs se règlent aussi depuis Paramètres → Centre de contenu. Lorsque le téléchargement automatique est activé, Twitchiou repère la fin du live, attend que la rediffusion correspondante soit publiée par Twitch, puis démarre son téléchargement en arrière-plan.

Statistiques et chat

La page Statistiques conserve dans SQLite les mesures daudience, dabonnements et de followers du core, calcule leurs évolutions, le nombre de messages par heure et classe les participants les plus actifs. La barre de période permet de basculer entre 24 heures, 7, 30 ou 90 jours, ou disoler une session de stream précise. Les sessions et les changements de jeu sont détectés automatiquement ; les pochettes sont récupérées avec Helix puis superposées au graphique.

La page Chat permet denvoyer un message avec le compte Bot et de consulter les messages, sondages et résultats archivés par session. Le bouton de son active une courte notification locale lors de chaque nouveau message. Les sondages en cours proposent Répondre sur Twitch, qui ouvre le chat officiel : lAPI Twitch publique permet de créer, lire et terminer un sondage, mais ne fournit pas dendpoint permettant de voter au nom dun viewer.

Overlays OBS

La page Overlays contient un overlay système prêt à lemploi, le preset supprimable Overlay principal glossy, et permet de créer dautres sorties. Le preset affiche une barre dinformations, les statistiques de la chaîne, le dernier message, les sondages et les alertes Twitch en temps réel. Le preset Just Chatting · Flash info ouvre la scène sur une sphère quadrillée rouge calculée en HTML/Canvas, dont la grille tourne autour dun axe incliné sous une horloge analogique accélérée. Après la transition glitch, le petit globe et son horloge superposée rejoignent le coin supérieur gauche, sans bandeau supérieur, et lhabillage affiche autour dune zone centrale transparente le titre du live, la catégorie, lheure et les viewers. Ajoutez &intro=0 à son URL OBS pour ignorer louverture ou &headline=Votre%20titre pour remplacer ponctuellement le titre Twitch. Pour chaque sortie, copiez le lien fourni dans une source Navigateur dOBS et reportez la largeur et la hauteur affichées. Le fond est transparent. Chaque URL contient une clé aléatoire renouvelable ; son renouvellement invalide immédiatement lancien canal.

Le gestionnaire permet de tester une animation HTML/CSS, un son distant, de vider loverlay et de louvrir dans un navigateur. Les commandes sont diffusées en temps réel par Socket.IO et journalisées. Le HTML/CSS de chaque animation est isolé dans un Shadow DOM.

Entrée vidéo RTMP

Twitchiou peut recevoir directement un flux RTMP envoyé par OBS. Installez FFmpeg, puis ouvrez Paramètres → Interfaçage → Entrée RTMP pour activer le flux interne et copier ladresse du serveur et sa clé. Dans OBS, choisissez Service personnalisé, collez ces deux valeurs, puis démarrez la diffusion. La clé est créée une seule fois si aucune clé nexiste, puis conservée lors des arrêts, réactivations et redémarrages. Elle est masquée par défaut et peut être remplacée, uniquement à la demande, par une nouvelle clé aléatoire de 10 caractères après confirmation.

Le core convertit le signal en HLS local, affiche directement la vidéo dans loverlay système et émet on_core_signal_in lorsque les premières images sont disponibles, puis on_core_signal_out quand elles disparaissent. Cet affichage ne dépend daucun plugin. Le plugin Diffusion assistant peut enrichir le rendu avec ses transitions et déclencher son écran glitché NO SIGNAL après la première perte de signal.

Le serveur écoute par défaut sur rtmp://0.0.0.0:1935. Les variables RTMP_HOST, RTMP_PUBLIC_HOST, RTMP_PORT, RTMP_STREAM_KEY, RTMP_SIGNAL_TIMEOUT, RTMP_HLS_DIR et FFMPEG_BINARY permettent dadapter lécoute, ladresse présentée à OBS, la clé, le délai de détection et lemplacement des segments. Désactivez complètement lentrée avec RTMP_ENABLED=false.

Sources OBS brutes

Des pages publiques volontairement sans feuille de style sont disponibles pour composer directement une source Navigateur OBS :

  • /chat : messages du chat, avec mise à jour, suppression et vidage en temps réel (?limit=100, jusquà 500) ;
  • /followers, /subs et /viewers : compteurs daudience ;
  • /title et /game : titre et jeu actuels ;
  • /game_pic : pochette du jeu dans un élément <img>.

Chaque page utilise des identifiants et classes stables (#chat, .chat-message, .chat-user, .chat-text, #followers, #subs, #viewers, #title, #game, #game_pic) et ne charge aucun thème Twitchiou. Les changements sont appliqués par Socket.IO. Létat initial complet est également disponible en JSON via GET /api/public/obs-state.

Un plugin actif peut utiliser le service du core :

self.context.overlays.show(
    '<div class="alert">Nouveau record !</div>',
    css='.alert{animation:pop .5s;color:white}',
    script='root.dataset.score = data.score',
    data={"score": 42},
    duration=5000,
    position="center",
)
self.context.overlays.sound("https://example.test/alert.mp3", volume=0.8)
self.context.overlays.emit("score_changed", {"score": 42})
self.context.overlays.clear()

Pour servir un son, une image ou une police depuis le dossier du plugin, déclarez explicitement le fichier dans manifest.json, puis utilisez son URL :

{"overlay_assets": ["overlay/alert.mp3", "overlay/logo.webp"]}
self.context.overlays.sound(self.context.overlays.asset("overlay/alert.mp3"))

Un plugin Python actif est déjà du code de confiance. Les champs html, css et surtout script disposent donc volontairement de toute la puissance nécessaire aux animations ; ninstallez que des plugins dont vous contrôlez la provenance.

Couverture Twitch

Le catalogue embarqué couvre les événements de chaîne EventSub actuels : chat, AutoMod, modération, abonnements, Bits, Power-ups, points de chaîne, sondages, prédictions, Hype Trains, objectifs, raids, publicités, charité, Guest Star, Shield Mode, shoutouts, utilisateurs suspects, avertissements, live/offline et whispers.

Les événements liés à une organisation, une extension, un Drop, un conduit ou à un identifiant externe ne peuvent pas être devinés depuis les deux comptes. Ajoutez-les avec le formulaire Temps réel Twitch → Ajouter un événement. Les conditions peuvent utiliser directement le compte Broadcaster ou Bot connecté, ou un identifiant Twitch saisi manuellement. Chaque abonnement peut ensuite être activé, désactivé, modifié ou supprimé depuis sa carte.

Trente-et-une actions conviviales sont fournies. helix.request offre en plus un accès générique à tous les endpoints Helix actuels ou futurs, sans exposer les jetons au navigateur :

{
  "role": "broadcaster",
  "method": "GET",
  "path": "/channels",
  "params": {"broadcaster_id": "1234"},
  "body": {}
}

Événements JSON

Tous les événements Twitch partagent cette enveloppe :

{
  "id": "identifiant EventSub",
  "type": "on_twitch_poll_end",
  "twitch_type": "channel.poll.end",
  "version": "1",
  "source": "twitch",
  "occurred_at": "2026-08-12T18:42:00Z",
  "received_at": "2026-08-12T18:42:00.123Z",
  "subscription": {},
  "data": {}
}

Les noms pratiques suivants sont notamment garantis : on_twitch_message, on_twitch_poll_start, on_twitch_poll_end, on_twitch_prediction_start et on_twitch_prediction_end. Tous les autres deviennent automatiquement on_twitch_ suivi du type Twitch avec les points remplacés par des underscores. Le type Twitch original est toujours conservé.

Deux événements sont calculés en arrière-plan par le composant principal après une première mesure de référence :

  • on_twitch_onviewercount_change contient count, previous_count, delta, létat live et les informations du stream ;
  • on_twitch_subcount_change contient total, points, les compteurs tiers (1000, 2000, 3000), gifts, plans, prime, ainsi que les valeurs précédentes et les deltas.

LAPI Helix des abonnements ne fournit pas systématiquement un indicateur Prime. prime.count comptabilise donc uniquement les lignes explicitement identifiées comme Prime ; prime.unknown indique les Tier 1 non classables et prime.complete vaut false tant quil en existe.

Événements HTTP externes

Le contrat complet, les exemples pour chaque système et le catalogue derreurs sont détaillés dans external_api.md.

La page Paramètres → Interfaçage crée des clés propres à chaque fournisseur, avec une expiration facultative, une liste dévénements autorisés et un callback HTTP optionnel. La clé complète est affichée uniquement à sa création ; seule son empreinte SHA-256 est ensuite conservée.

Le fournisseur envoie un objet JSON vers POST /api/external/events. Par exemple, une clé nommée Maison connectée autorisant porte_ouverte publie on_external_maison_connectee_porte_ouverte sur le bus, auprès de linterface temps réel et des plugins :

curl.exe -k -X POST "https://localhost:5000/api/external/events" -H "Authorization: Bearer twi_ext_REMPLACEZ_PAR_LA_CLE" -H "Content-Type: application/json" --data-raw "{\"event\":\"porte_ouverte\",\"data\":{\"porte\":\"garage\",\"ouverte\":true}}"

Cette forme est compatible avec linvite de commandes Windows (cmd.exe). LURL doit rester brute : ne copiez pas une représentation Markdown de la forme [URL](URL) et najoutez pas dantislash devant les underscores.

Le champ id peut être fourni pour rendre lenvoi idempotent et occurred_at accepte une date ISO 8601. Si un callback est activé, Twitchiou lui transmet en arrière-plan lenveloppe normalisée complète. Une répétition du même id nest ni redistribuée aux plugins, ni renvoyée au callback.

ACTION PROCESSOR

Le core fournit un moteur unique pour valider puis exécuter, dans lordre, une suite dactions. Léditeur de triggers sappuie directement sur ce moteur : les messages, timeouts, bannissements, injections HTML dans loverlay système, signaux personnalisés, actions Twitch/OBS autorisées et actions déclarées par les plugins utilisent donc le même contrat.

  • GET /api/core/action-processor retourne le catalogue complet et les variables disponibles.
  • POST /api/core/action-processor/validate valide une suite sans lexécuter.
  • POST /api/core/action-processor/execute valide puis exécute la suite avec son contexte.

Le composant dinterface réutilisable se trouve dans templates/components/action_processor_editor.html. Son contrôleur static/js/action_processor.js expose window.ActionProcessorEditor ; il reçoit les fonctions de lecture, rendu et mise à jour de léditeur hôte. Il peut ainsi être inclus dans un autre écran sans recopier le panneau dactions des triggers.

Le bouton Mode conception avancée ouvre un plan de travail plein écran. Les blocs sont glissés depuis le volet droit, déplacés librement puis raccordés en tirant un câble depuis leur sortie vers lentrée dun autre bloc. Valider et utiliser remplace la suite linéaire du trigger par le workflow dessiné ; Annuler ferme la modale sans appliquer le graphe. La bibliothèque située dans la barre supérieure permet denregistrer, charger et supprimer jusquà 100 workflows réutilisables. Les routes correspondantes sont GET/POST /api/core/action-processor/workflows et DELETE /api/core/action-processor/workflows/<id>.

Le moteur avancé fournit les blocs suivants : début et fin, définition de variable, calcul arithmétique, condition à deux branches, boucle, temporisation, récupération dun utilisateur Twitch, extraction dune valeur JSON, création/manipulation de listes et action core/plugin. Les comparaisons affichent des champs adaptés à lopérateur choisi, notamment un champ dexpression régulière dédié.

Une liste accepte jusquà 100 éléments et peut être lue par index, mélangée, dédupliquée, découpée, assemblée en texte ou enrichie ; un bloc permet aussi den choisir un élément au hasard. Le résultat complet de chaque action peut être conservé dans une variable puis lu avec un chemin, par exemple {resultat.data.0.id}. Le bloc daction permet en plus dassocier plusieurs chemins de la réponse à des variables nommées. Cela fonctionne avec les objets JSON retournés par les actions des plugins, dont les paramètres sont présentés dans le même formulaire structuré que dans léditeur simplifié. Un workflow est refusé si un bloc est déconnecté, si une branche obligatoire manque ou si une même sortie possède plusieurs câbles. Lexécution est limitée à 500 étapes, les boucles à 100 passages et une temporisation à 300 secondes.

Les variables sont résolues une seule fois avant lexécution de la série. {arguments} contient la chaîne complète, tandis que {arg_0}, {arg_1}, etc. correspondent aux arguments séparés (les guillemets permettent de conserver un argument contenant des espaces). Le core fournit notamment {followers}, {subs}, {viewers}, {title}, {game}, {current_scene}, {obs_connected}, {obs_streaming} et {obs_recording}. Une variable inconnue reste intacte dans le texte afin de rendre les erreurs de configuration visibles.

Exemple dexécution manuelle :

{
  "access": "moderator",
  "context": {"user": "Alice", "login": "alice", "user_id": "123", "arguments": "bonjour"},
  "actions": [
    {"type": "message", "message": "Message de {user} : {arguments}", "sender_role": "bot"},
    {"type": "custom_signal", "event": "suite_terminee", "data": {"auteur": "{login}"}}
  ]
}

Plugins

Un plugin est un dossier plugins/<slug> contenant manifest.json et plugin.py :

class Plugin:
    def __init__(self, context):
        self.context = context

    def on_event(self, event):
        pass

    def on_twitch_message(self, event):
        if event["data"]["message"]["text"] == "!hello":
            self.context.actions.twitch_message("Bonjour !")

    def on_unload(self):
        pass


def create_plugin(context):
    return Plugin(context)

La page Plugins → Tous les plugins affiche tous les modules détectés, actifs ou non. Chaque carte possède un interrupteur dactivation. Lorsquun plugin est actif, ses interfaces ordinaires apparaissent dans la même colonne de navigation ; ses pages de configuration restent disponibles dans Paramètres.

context.config contient la configuration enregistrée du plugin. context.actions expose directement les actions du composant principal sous forme de méthodes : twitch_message(), twitch_poll_start(), twitch_ban(), etc. Les paramètres peuvent être fournis comme arguments nommés ou dans un dictionnaire. context.action(name, payload) reste disponible pour les anciens plugins et context.emit(type, data) publie un événement interne. Les callbacks sont exécutés dans un pool de threads pour ne pas bloquer EventSub. Un plugin dexemple est inclus mais désactivé par défaut.

Les secrets configurables peuvent être déclarés avec "secret_fields": ["api_key"] dans le manifeste. Leur valeur reste disponible côté serveur dans context.config, mais elle est retirée des réponses de lAPI des plugins et des variables transmises aux templates. Le booléen api_key_configured permet à linterface dindiquer quune clé existe. Envoyer un champ secret vide conserve la valeur actuelle ; envoyer api_key_clear: true la supprime explicitement.

Un plugin actif peut également exposer une action dans léditeur Paramètres → Triggers. Les triggers peuvent être des commandes préfixées (!commande), des expressions régulières appliquées à lensemble du message ou des récompenses de points de chaîne identifiées par leur ID Twitch ou leur titre exact. Les trois utilisent les mêmes conditions et le même éditeur dactions. Pour une récompense, son éventuelle saisie utilisateur alimente {arguments}. Seules les actions ainsi déclarées sont proposées ; le handler reçoit les paramètres JSON après remplacement des variables du trigger :

def on_load(self):
    self.context.commands.register_action(
        "changer_mode",
        self.changer_mode,
        name="Changer de mode",
        description="Applique un mode au plugin.",
        example={"mode": "{arguments}"},
        schema={"mode": {
            "type": "select",
            "label": "Mode du plugin",
            "options": ["normal", "furtif", "festif"],
        }},
        returns={
            "active_mode": {
                "path": "mode",
                "label": "Mode réellement appliqué",
            },
        },
        access="moderator",
    )

def changer_mode(self, command):
    mode = command["payload"]["mode"]
    utilisateur = command["user_id"]

Un plugin peut aussi fournir ses propres variables. Il déclare leur documentation séparément de leur valeur ; le fournisseur reçoit le contexte déjà construit et est appelé au début de chaque série :

def on_load(self):
    self.context.commands.register_variables(
        self.action_variables,
        variables={
            "score": "Score actuel du viewer",
            "rang": {"label": "Rang", "description": "Rang du viewer dans le plugin"},
        },
    )

def action_variables(self, context):
    return {
        "score": self.score(context["user_id"]),
        "rang": self.rang(context["user_id"]),
    }

Les types de champs disponibles sont text, textarea, number, boolean, select, string_list et json. Si le schéma est omis, Twitchiou génère le formulaire à partir de example. returns documente les chemins de la réponse JSON : léditeur avancé les présente sous forme de boutons et crée la variable choisie sans demander à lutilisateur de connaître la structure brute. Chaque action conserve également une variable pour sa réponse complète. Les niveaux acceptés sont everyone, moderator et broadcaster. Toutes les variables déclarées fonctionnent dans les chaînes des paramètres, y compris dans les objets et tableaux imbriqués. Discord Webhook déclare ainsi laction Envoyer un message sur Discord. Lintégration OBS fournit notamment le changement de scène, le volume dune source, sa mise en sourdine, la diffusion et lenregistrement.

E-LOIS

Le plugin E-LOIS répond avec le compte Bot lorsquun message mentionne son pseudo, ou lorsquun viewer répond directement à lun de ses messages. Sa page de configuration accepte la clé API OpenAI, le modèle et le prompt système, ainsi quune taille maximale dhistorique. Chaque réponse Twitch utilise la fonction « reply » et référence donc le message dorigine.

Le contexte transmis à OpenAI est configurable indépendamment : titre, jeu, liste des personnes présentes, dernières statistiques connues et capture ponctuelle du rendu OBS. LorsquE-LOIS est interpellé, la capture utilise la scène Programme actuellement composée par OBS via WebSocket, avant toute publicité ajoutée par Twitch. Si OBS est indisponible, E-LOIS répond sans image et ne retombe pas sur le flux Twitch public. La clé OpenAI est un champ secret : elle nest jamais réinjectée dans le HTML ni renvoyée par lAPI Web.

Mario Kart 7 Toolkit

Le plugin Mario Kart 7 Toolkit consomme par défaut le même événement MetaSave que Pokémon GB Toolkit. Lorsque la catégorie Twitch courante est exactement Mario Kart 7, il injecte dans loverlay système le circuit et le classement animé avec lobjet détenu par chaque joueur. La première détection dune carapace bleue déclenche une alerte glitchée de deux secondes et une annonce TTS ; les collectes suivantes ne la répètent pas tant que la carapace na pas disparu.

Pages Web des plugins

Un plugin peut déclarer une ou plusieurs pages dans manifest.json. Une page ordinaire apparaît dans Plugins. Une page portant "kind": "settings" apparaît uniquement dans Paramètres → Configuration des plugins. Dans les deux cas, le plugin doit être actif :

{
  "slug": "scores",
  "name": "Scores viewers",
  "version": "1.0.0",
  "pages": [
    {
      "id": "dashboard",
      "title": "Tableau des scores",
      "icon": "bi-trophy",
      "template": "pages/dashboard.html",
      "style": "pages/dashboard.css",
      "script": "pages/dashboard.js"
    },
    {
      "id": "settings",
      "title": "Configuration",
      "icon": "bi-sliders",
      "kind": "settings",
      "template": "pages/settings.html",
      "script": "pages/settings.js"
    }
  ]
}

Widgets du Tableau de Stream

Un plugin actif peut aussi fournir des widgets déplaçables et redimensionnables pour le Tableau de Stream. Ils sont déclarés séparément des pages :

{
  "widgets": [
    {
      "id": "scores",
      "title": "Scores viewers",
      "icon": "bi-trophy",
      "description": "Classement compact de la session.",
      "template": "widgets/scores.html",
      "style": "widgets/scores.css",
      "script": "widgets/scores.js",
      "default_w": 4,
      "default_h": 4,
      "min_w": 3,
      "min_h": 2
    }
  ]
}

Le template Jinja reçoit plugin, widget, config et instance_id. style et script sont optionnels ; leurs fichiers doivent rester dans le dossier du plugin. Le script est chargé chaque fois que le widget devient visible et doit donc pouvoir initialiser plusieurs instances sans réutiliser didentifiants HTML globaux. La désactivation du plugin retire son widget du catalogue, mais conserve sa place dans la disposition afin quil puisse réapparaître après réactivation.

Le widget principal Onglets de widgets fournit une grille interne par onglet. En mode édition, ses boutons permettent dajouter ou supprimer des onglets et douvrir la bibliothèque directement sur la grille interne. Les widgets principaux et ceux des plugins peuvent y être déposés ; un widget Onglets ne peut toutefois pas en contenir un autre.

Tous les widgets peuvent être ajoutés plusieurs fois. Chaque exemplaire conserve sa propre position, sa taille et sa configuration ; les lecteurs vidéo et les chats utilisent également des instances DOM indépendantes.

Le widget principal Événements Twitch transforme les signaux techniques en phrases lisibles. Sa configuration permet de changer son titre, le nombre de lignes, laffichage des descriptions et les catégories visibles : chat, direct, audience, sondages/prédictions, récompenses, modération ou autres événements.

Widgets de léditeur doverlays

Un plugin actif peut enrichir la bibliothèque de léditeur visuel doverlays avec overlay_widgets. Ces widgets utilisent des dimensions en pixels correspondant directement à la résolution OBS :

{
  "overlay_widgets": [
    {
      "id": "score",
      "title": "Score",
      "icon": "bi-trophy-fill",
      "description": "Score courant affiché dans OBS.",
      "template": "overlay_widgets/score.html",
      "style": "overlay_widgets/score.css",
      "script": "overlay_widgets/score.js",
      "default_w": 420,
      "default_h": 160
    }
  ]
}

Les trois fichiers sont copiés dans chaque instance lors de son ajout. Lutilisateur peut donc personnaliser indépendamment le HTML, le CSS et le JavaScript de plusieurs exemplaires du même widget. Le script peut lire window.TwitchiouWidget, qui contient instanceId, type et config, puis écouter les messages {source: "twitchiou-overlay", kind: "twitch-event", payload: événement} envoyés en temps réel par le core.

Les widgets restent transparents par défaut. Leur code sexécute dans un document dédié afin disoler leurs styles du document permanent et des autres widgets. NMS Toolkit fournit trois exemples : Planète, Base et Position glyphes.

Le widget core Flux RTMP autonome réserve sa propre entrée vidéo dès lenregistrement de la composition. Chaque exemplaire dispose dun port et dune clé indépendants, affiche uniquement son signal dans sa zone et peut être activé, désactivé ou renouvelé séparément. La configuration du widget fournit les valeurs à copier dans OBS ainsi quun lien RTMP complet. Supprimer le widget libère son entrée sans affecter les autres flux.

Les ports autonomes sont attribués dans la plage 1940-1999. Adaptez-la avec RTMP_WIDGET_PORT_START et RTMP_WIDGET_PORT_END, notamment si les émetteurs sont sur une autre machine et que le pare-feu doit autoriser cette plage. RTMP_WIDGET_HLS_DIR définit le dossier des segments temporaires.

Intégration OBS

La page Paramètres → Interfaçage → Intégration OBS configure une connexion persistante au protocole OBS WebSocket 5.x. Activez dabord le serveur dans OBS → Outils → Paramètres du serveur WebSocket, puis renseignez lhôte, le port (4455 par défaut) et le mot de passe dans Twitchiou.

La page permet de tester la connexion, consulter les scènes, changer la scène programme et exécuter toute requête OBS 5.x. Le mot de passe est chiffré dans SQLite et nest jamais renvoyé à linterface. La connexion se rétablit automatiquement après une coupure.

Les routes principales sont GET /api/core/obs/scenes, PUT /api/core/obs/scenes/current et POST /api/core/obs/request. Les plugins disposent des actions obs_scene_list(), obs_scene_current(), obs_scene_set({"scene_name": "Pause"}) et obs_request(...). Les événements bruts reçus depuis OBS sont publiés sur le bus sous la forme on_obs_<nom_evenement>. Twitchiou publie aussi les signaux simplifiés on_obs_scene_changed, on_obs_stream_started, on_obs_stream_stopped, on_obs_record_started et on_obs_record_stopped pour les automatisations et les plugins.

Le template est rendu avec Jinja et reçoit plugin, page et config. style et script sont optionnels et doivent rester dans le dossier du plugin. Une page de paramètres peut enregistrer sa configuration avec PUT /api/plugins/<slug>/config ; le plugin dexemple fournit un formulaire complet servant de référence.

Deux contrôles HTML génériques sont fournis par le core :

<div data-core-control="game-picker" data-name="game_id"></div>
<div data-core-control="viewer-picker" data-name="viewer_id"></div>

Ils créent un champ caché portant le nom indiqué, puis publient un événement DOM core:select avec lobjet Twitch sélectionné dans event.detail.

Données utilisateurs

Chaque plugin dispose dun stockage JSON, isolé par défaut dans son slug :

score = self.context.users.get(user_id, "score", 0)
self.context.users.set(user_id, "score", score + 1, user_login=login)
self.context.users.increment(user_id, "score", 1, user_login=login)
self.context.users.delete(user_id, "score")
rows = self.context.users.list(query="viewer")

Le namespace peut être précisé avec namespace="commun". Toutes les valeurs sont consultables et modifiables depuis Paramètres → Données utilisateurs, ou par lAPI /api/core/user-data.

Chaque ajout, modification ou suppression produit :

{
  "type": "on_core_changevalue",
  "source": "core",
  "data": {
    "user_id": "123",
    "namespace": "scores",
    "key": "points",
    "old_value": 10,
    "value": 11,
    "deleted": false,
    "origin": "PLUGIN:scores",
    "origin_type": "PLUGIN",
    "origin_plugin": "scores"
  }
}

origin vaut API, WEB ou PLUGIN:<slug>.

Envoi de-mails

Le service SMTP du core se configure dans Paramètres → Envoi de-mails. Il prend en charge SSL/TLS direct, STARTTLS et, pour les réseaux maîtrisés uniquement, SMTP sans chiffrement. Le mot de passe est chiffré dans SQLite et nest jamais renvoyé à linterface.

La même page permet de choisir une adresse destinataire et dactiver séparément les notifications de follows, abonnements et cadeaux, cheers, raids, récompenses ainsi que le rapport de fin de stream. Les alertes sont envoyées en arrière-plan. Le rapport est produit à la fermeture de la session Twitch et récapitule sa durée, son audience, les nouveaux followers, les abonnements, le chat et les jeux diffusés.

Un plugin peut envoyer un message texte ou HTML avec le service central :

self.context.mail.send(
    "viewer@example.com",
    "Votre score",
    "Vous avez 42 points.",
    to_name="Viewer",
)

Le raccourci self.context.send_mail(...) fournit la même fonction.

LAPI équivalente est POST /api/core/mail/send, avec to, subject, body et, facultativement, html et to_name.

La validation TLS utilise dabord les autorités de confiance du système et vérifie le nom du serveur. Si le certificat est invalide ou auto-signé, aucune authentification SMTP nest envoyée : linterface affiche le sujet, lémetteur, les dates et lempreinte SHA-256. Après confirmation explicite, le certificat est conservé et son empreinte exacte est vérifiée avant chaque authentification. Tout changement de certificat provoque une nouvelle demande.

Les plugins sont du code Python de confiance : linterface ninstalle aucun paquet distant et ne sandboxe pas leur exécution.

Tests

.venv/bin/python -m unittest discover -s tests -v

Notes de sécurité

  • Les jetons OAuth sont chiffrés dans SQLite et ne sont jamais retournés par lAPI Web.
  • Ne changez pas APP_SECRET/TOKEN_ENCRYPTION_KEY sans reconnecter ensuite les comptes.
  • LUI est prévue pour une utilisation locale. Avant de lexposer sur Internet, ajoutez TLS, une authentification inverse-proxy, une politique dorigine stricte et une protection CSRF.
  • Twitch recommande de ne demander que les scopes réellement nécessaires. Cette configuration en demande volontairement beaucoup pour le centre de contrôle généraliste ; réduisez les listes dans twitchiou/twitch/scopes.py pour une application distribuée ou soumise à revue.

Documentation de référence : scopes Twitch, types EventSub, API Helix.