Référence
Référence
Constructeur de modules Sesame
L'API fluide de création des modules et commandes Sesame, avec chaque méthode du constructeur et l'ordre des contrôles appliqué par le moteur.
Sur cette page
Une fonctionnalité Sesame est une fonction qui renvoie une valeur module.Module.
Le constructeur de app/twitch/sesame/module/builder.go assemble cette valeur ;
le moteur de app/twitch/sesame/engine/ l’indexe et l’exécute.
Le paquet du constructeur ne contient aucun câblage d’exécution (ni NATS,
ni Valkey, ni projection ni pipeline). Un module reçoit engine.Deps et ses
handlers capturent leurs dépendances par fermeture ; l’API reste ainsi compacte
et testable unitairement sans cluster.
Module minimal
func Ping(d engine.Deps) module.Module {
m := module.NewModule("", module.KindCore)
m.Command("ping").Everyone().Run(
func(ctx context.Context, c *module.Context, args string, emit module.Emit) error {
emit(&module.Output{
Type: outgress.TypeChat,
BroadcasterID: c.Env.BroadcasterUserID,
Text: "pong",
})
return nil
})
return m.Build()
}
Ajoutez ensuite une ligne dans app/twitch/sesame/modules/all.go. C’est toute
l’étape d’enregistrement.
API au niveau du module
| Appel | Rôle |
|---|---|
module.NewModule(name, kind) |
Démarre un module. Renvoie *Builder. |
b.Beta() |
Marque un module nommé comme bêta. Il ne s’exécute que pour les diffuseurs Premium. Un module core ne peut pas être bêta. |
b.Trial() |
Marque l’ensemble de commandes d’essai. Il doit être un module core, ne peut pas être bêta, et seule l’origine d’essai le contrôle. |
b.Command(name) |
Commence une commande de chat. Renvoie *CmdBuilder pour enchaîner les contrôles. Le nom est converti en minuscules. |
b.On(eventType, fn) |
Enregistre un handler hors commande pour un type EventSub. Si le même type est enregistré deux fois, le dernier handler est conservé. |
b.Build() |
Valide et renvoie le Module immuable. Provoque une panique en cas d’erreur de programmation. |
b.Validate() |
Effectue les mêmes contrôles sans provoquer de panique. Utile pour les tests. |
Un Builder ne s’utilise qu’une fois et n’est pas sûr en cas d’accès concurrent.
Types
| Type | Nom | Comportement |
|---|---|---|
KindCore |
facultatif | Toujours actif. Jamais listé pour les chaînes, jamais activé ou désactivé, sans configuration. Le moteur ignore entièrement pour lui la lecture de projection ModuleView. |
KindDefault |
obligatoire | Nommé, livré activé. S’exécute sauf si la chaîne le désactive. |
KindOptIn |
obligatoire | Nommé, livré désactivé. S’exécute uniquement si la chaîne l’active. |
Build impose cette association : un type nommé avec un nom vide échoue.
Le fait que Core ignore la récupération de projection est une propriété du chemin critique.
Un module nommé coûte une recherche ModuleView par message pour décider s’il s’exécute ;
Core ne coûte rien. Un module Core qui souhaite malgré tout un contrôle par chaîne effectue
sa propre vérification différée dans Run (voir moduleEnabled dans modules/followage.go).
Contrôles des commandes
Chaque méthode ci-dessous renvoie le même *CmdBuilder, donc elles s’enchaînent. Run est
terminale et ne renvoie rien.
Permission
Chaque méthode définit le rôle minimal ; les rôles supérieurs passent également.
| Méthode | Rôle minimal |
|---|---|
.Everyone() |
toute personne du chat (par défaut) |
.Sub() |
abonné |
.VIP() |
VIP |
.Mod() |
modérateur |
.LeadMod() |
lead moderator |
.Broadcaster() |
propriétaire de la chaîne uniquement |
Ordre de la hiérarchie : RoleEveryone → RoleSubscriber → RoleVIP → RoleModerator →
RoleLeadModerator → RoleBroadcaster.
Twitch fournit le badge lead_moderator à la place de moderator, donc .Mod() inclut aussi les lead moderators et les rôles supérieurs. .LeadMod() exclut les modérateurs simples.
Ce niveau est aussi utilisé hors du constructeur : permRoles associe "lead_mod",
internal/domain/validate l’accepte, le schéma ent de commands le stocke et le
tableau de bord propose « Lead moderators & up ». Le moteur contrôle les commandes
intégrées et personnalisées via le même gate().
| Méthode | Rôle |
|---|---|
.AllowUser(id) |
Limite à un seul identifiant de chatter. Remplace entièrement le contrôle de rôle. |
Conditions
| Méthode | Rôle |
|---|---|
.Cooldown(d) |
Fenêtre partagée par commande. Zéro (par défaut) signifie aucune fenêtre. Les commandes personnalisées peuvent aussi définir une fenêtre par spectateur. |
.LiveOnly() |
S’exécute uniquement lorsque la chaîne est en direct. |
Correspondance
| Méthode | Rôle |
|---|---|
.Aliases(a, b, ...) |
Déclencheurs supplémentaires qui résolvent vers cette commande. Convertis en minuscules. Les doublons sont rejetés par Validate. |
.NumericSuffix() |
Le déclencheur absorbe les chiffres finaux saisis directement : !clip30 résout clip avec c.Num == "30". Les chiffres sont retirés et ne font pas partie de la chaîne d’arguments. |
Terminaison
| Méthode | Rôle |
|---|---|
.Run(fn) |
Définit le handler et termine la commande. Ne renvoie rien, afin qu’une déclaration ne puisse pas continuer après lui. |
Ordre des contrôles
Le moteur applique les contrôles de manière centralisée dans engine/dispatch.go :
func (p *Pipeline) gate(ctx, c, r gateRule) (bool, error) {
if !permits(c, r.allowedUserID, r.perm) { return false, nil } // 1
if ok, err := p.liveOK(ctx, c, r.liveOnly); !ok { return false, err } // 2
if c.Env.Origin == "trial" { return true, nil } // no cooldown claim
return p.cooldownOK(ctx, c, r) // 3
}
- permission
- uniquement en direct
- cooldown (ignoré pour les messages d’origine essai)
Le cooldown est volontairement en dernier. cooldownOK réserve la fenêtre lorsqu’il réussit.
S’il s’exécutait en premier, un chatter qui échoue au contrôle de permission consommerait quand même
le cooldown de toutes les autres personnes.
Les commandes intégrées et les commandes personnalisées écrites par la chaîne construisent la même
gateRule, afin que la sémantique des permissions reste identique entre les
commandes intégrées et les commandes utilisateur.
Ce que reçoit Run
type RunFunc func(ctx context.Context, c *Context, args string, emit Emit) error
argsest la chaîne nettoyée après le nom de la commande.c.Envest l’enveloppe de chat :BroadcasterUserID,ChatterUserID,ChatterUserLogin,ChatterName(), badges.c.Chatter()résout le rôle du chatter, analysé une fois puis mis en cache.c.Localeest la langue de la chaîne, chargée à la demande. Appelezc.EnsureLocale(ctx)avant de la lire, puis utilisezi18n.T(c.Locale, "key").c.Configest le blob de configuration brut du module ;c.Decode(&out)le désérialise. Il est vide pour les modules Core et une configuration absente n’est pas une erreur.c.Numest le suffixe numérique inline, défini uniquement pour une commandeNumericSuffix.
Règles de mutualisation
Ces deux règles corrompront d’autres messages si elles sont ignorées.
- Ne conservez pas
caprès l’appel.Contextest mutualisé et réinitialisé entre les messages. - Ne conservez pas le
Outputtransmis àemit. Le moteur peut le recycler dès le retour deEmit.
Copiez tout ce qui doit survivre à l’appel.
Émission
emit(&module.Output{
Type: outgress.TypeChat,
BroadcasterID: c.Env.BroadcasterUserID,
Text: "hello",
})
Output.Type sélectionne l’action (outgress.TypeChat, TypeAnnounce, clip,
shoutout, modération, résolution de récompense). Les champs propres à chaque type
sont documentés sur la structure de module/module.go ; les champs des autres
types sont ignorés. BatchID/Items transportent une réponse à plusieurs
messages comme un seul travail de file outgress, exécuté dans l’ordre de la slice.
Validation
Build provoque une panique au lieu de renvoyer une erreur, car il s’agit de
mauvaises configurations au démarrage plutôt que de données d’exécution, et un
échec explicite au boot vaut mieux qu’un comportement silencieusement incorrect
en production. Il rejette :
- un type nommé avec un nom vide
- un type inconnu
- un module core marqué bêta
- un module d’essai qui n’est pas core ou qui est aussi bêta
- un nom de commande ou un alias vide
- un déclencheur dupliqué dans le même module
- une commande sans
Run(signalée par « chain .Run to finish it »)
Une commande que vous oubliez de terminer existe tout de même comme entrée
incomplète, car Command() l’ajoute avant tout chaînage ; c’est précisément ainsi
que Validate la détecte.
Collisions entre modules
Les doublons dans un module provoquent une panique lors de la construction. Les doublons entre modules sont
le premier gagne, avec un avertissement dans les logs
(engine: duplicate command trigger ignored).
L’ordre dans all.go est donc déterminant : les modules Core sont listés en premier afin que
leurs déclencheurs réservés l’emportent sur ceux d’un module nommé en conflit.
Liste de contrôle pour un nouveau module
- Créez
app/twitch/sesame/modules/<name>.go. - Déclarez
func <Name>(d engine.Deps) module.Module. - Appelez
module.NewModule(name, kind). - Déclarez les commandes avec
.Command(...).<gates>.Run(fn)et les événements avec.On(type, fn). - Écrivez
return m.Build(). - Ajoutez une ligne dans
all.go. - Localisez les réponses avec
i18n.T(c.Locale, key). - Protégez les services facultatifs (
d.X != nil) et choisissez délibérément la politique d’échec ; les modules existants échouent ouvertement, afin qu’un incident temporaire de projection n’absorbe pas une commande. - Ajoutez
<name>_test.go.Validate()s’exécute sans cluster.