Passer au contenu principal

Plugin Events

Les plugins réagissent aux événements se produisant dans Owncast en définissant un gestionnaire pour chaque événement qui les concerne. Ne définissez que les gestionnaires que vous souhaitez : un gestionnaire manquant signifie aucune abonnement, et le SDK dérive la liste d'abonnement du manifeste en fonction des gestionnaires présents, donc il n'y a rien d'autre à synchroniser.

Le code ci-dessous est présenté pour les deux SDK. Choisissez votre langue avec les onglets, et votre choix vous suit à travers la documentation. Nouvelle ici ? Voir d'abord les pages de configuration JavaScript ou Python.

const { definePlugin, owncast, filter } = require('@owncast/plugin-sdk');

module.exports = definePlugin({
onChatMessage(msg) {
/* react to a chat message */
},
onStreamStarted(info) {
/* react to the stream going live */
},
});

Les gestionnaires sont des méthodes sur l'objet que vous passez à definePlugin, nommées en camelCase (onChatMessage, onStreamStarted, …). Les champs de charge utile sont également en camelCase (msg.user.displayName, msg.clientId).

Les charges utiles sont montrées sous leur forme brute. Chaque SDK expose les champs de manière idiomatique : le SDK JavaScript tel quel, le SDK Python en tant qu'attributs snake_case sur le même JSON (avec le dictionnaire brut également disponible).

Événements de chat

Construire un plugin axé sur le chat ? Les plugins de chat constituent un point de départ plus convivial.

Message de chat : chat.message.received

Se déclenche une fois par message de chat après le passage des filtres et le message étant diffusé aux spectateurs.

interface ChatMessage {
id: string;
user?: User; // full sender identity (see User below); absent for the rare message with no account
clientId?: number; // originating connection; pass to the chat send-to / reply-to APIs for private replies
body: string; // raw text, not HTML-rendered markup
timestamp: string; // RFC3339Nano / ISO-8601, e.g. "2026-05-28T14:00:00.123456789Z"
}
module.exports = definePlugin({
onChatMessage(msg) {
if (msg.user?.scopes?.includes('MODERATOR')) {
owncast.chat.send(`hi mod ${msg.user.displayName}`);
}
},
});

user représente l'identité complète de l'expéditeur, donc la clé pour chaque état utilisateur sur le stable user.id et la porte vers un comportement réservé aux modérateurs sur user.scopes (par exemple "MODERATOR") plutôt que de faire correspondre le nom affiché. Pour répondre en privé à l'expéditeur, utilisez l'API de réponse au chat (voir Owncast APIs).

timestamp est l'heure du serveur pour le message. L'horloge du bac à sable fonctionne, mais timestamp est déterministe et le bon choix lors de la comparaison du temps écoulé entre les événements ou lors d'assertions dans les tests.

Aucune permission requise pour s'abonner.

Les anciens hôtes ont délivré user en tant que chaîne de nom d'affichage simple plutôt qu'en tant qu'objet d'identité. Si vous supportez des hôtes qui datent de l'envoi de la charge d'identité, lisez cela de manière défensive. Consultez votre page SDK pour l'idiome.

Utilisateur de chat rejoint / parti : chat.user.joined, chat.user.parted

Se déclenche lorsqu'un utilisateur de chat se connecte ou se déconnecte.

interface User {
id: string;
displayName: string;
displayColor: number; // index into the instance's user-color palette, not a literal color
previousNames?: string[];
createdAt?: string; // ISO-8601
disabledAt?: string; // ISO-8601 if banned, omitted otherwise
isBot?: boolean;
isAuthenticated?: boolean;
scopes?: string[];
}
module.exports = definePlugin({
onChatUserJoined(user) {
owncast.chat.send(`welcome ${user.displayName}`);
},
onChatUserParted(user) {
/* … */
},
});

Aucune permission requise.

Utilisateur de chat renommé : chat.user.renamed

Se déclenche lorsqu'un utilisateur de chat change son nom d'affichage.

interface { user: User; previousName: string }

Aucune permission requise.

Message modéré : chat.message.moderated

Se déclenche lorsqu'un modérateur cache ou affiche un message de chat.

interface { messageId: string; visible: boolean; moderator?: User }

Aucune permission requise.

Cycle de vie du flux

Flux démarré : stream.started

Se déclenche lorsqu'une diffusion commence.

interface { startedAt?: string; title?: string; summary?: string }
module.exports = definePlugin({
onStreamStarted(info) {
owncast.chat.send(`live now: ${info.title}`);
},
onStreamStopped(info) {
/* … */
},
onStreamTitleChanged(change) {
/* change.to */
},
});

Aucune permission requise.

Flux arrêté : stream.stopped

Se déclenche lorsqu'une diffusion prend fin.

interface { stoppedAt?: string }

Aucune permission requise.

Titre du flux changé : stream.title.changed

Se déclenche lorsque le streamer met à jour le titre en cours de diffusion.

interface { from: string; to: string }

from est actuellement toujours vide : l'événement de titre changé d'Owncast ne porte que le nouveau titre.

Aucune permission requise.

Événements fediverse

Owncast exposes internal plugin event subscriptions for inbound Fediverse activity. Ce sont des événements de plugin, pas des webhooks HTTP externes. Chaque abonnement dans cette section nécessite la permission fediverse.inbound.

ÉvénementGestionnaire JavaScriptGestionnaire PythonCharge utile
fediverse.followonFediverseFollow@plugin.on_fediverse_follow{ actor }
fediverse.likeonFediverseLike@plugin.on_fediverse_like{ actor, target }
fediverse.repostonFediverseRepost@plugin.on_fediverse_repost{ actor, target }
fediverse.quoteonFediverseQuote@plugin.on_fediverse_quoteFediverseQuote
fediverse.mentiononFediverseMention@plugin.on_fediverse_mentionFediverseInboundPost
fediverse.replyonFediverseReply@plugin.on_fediverse_replyFediverseInboundPost
fediverse.activityonFediverse@plugin.on_fediverseObjet JSON brut ActivityPub

Suivre, aimer, republier et citer

interface FediverseActor {
name: string;
handle: string;
url?: string;
image?: string;
}

interface FediverseEngagement {
actor: FediverseActor;
target?: { url: string };
}

interface FediverseQuote extends FediverseEngagement {
target: { url: string }; // locally authored post being quoted
content?: string; // rendered HTML from the source instance
contentText?: string; // plain-text version
url: string; // remote quote post permalink
postedAt?: string; // ISO-8601
inReplyTo?: string;
attachments?: { url: string; mediaType: string; alt?: string }[];
language?: string;
}

Un suivi ne contient que actor. Likes and reposts also contain target.

A quote contains target for the locally authored post and url for the remote quote post. Content metadata is included when the requesting server embeds its quote Note in the QuoteRequest. Some servers send only the quote post IRI, so content, contentText, postedAt, inReplyTo, attachments, and language are optional.

module.exports = definePlugin({
onFediverseFollow(event) {
owncast.chat.send(`new follower: ${event.actor.handle}`);
},
onFediverseQuote(event) {
console.log(`${event.actor.handle}: ${event.contentText ?? 'quoted your post'}`);
console.log(`quote: ${event.url}`);
},
});

actor.handle est l'adresse entièrement qualifiée, telle que @alice@fediverse.example. Les exemples de suivi appellent également owncast.chat.send, ce qui nécessite séparément chat.send :

{ "permissions": ["fediverse.inbound", "chat.send"] }

Mention et réponse

Tous deux reçoivent un FediverseInboundPost :

interface FediverseInboundPost {
actor: FediverseActor;
content: string; // rendered HTML from the source instance
contentText: string; // plain-text version, usually what you want
url: string; // permalink on the source instance
postedAt: string; // ISO-8601
inReplyTo?: string; // parent post URL, set when this is a reply
attachments?: { url: string; mediaType: string; alt?: string }[];
language?: string;
}

Ces hooks spécialisés acceptent une activité Create vérifiée contenant exactement un Note. La note doit être attribuée à l'acteur de l'activité. Une mention doit s'adresser à l'acteur local d'Owncast. Une réponse doit faire référence à un post stocké par l'instance locale d'Owncast.

Utilisez contentText pour l'analyse ou pour l'écho dans le chat. Utilisez content seulement lorsque vous avez besoin du formatage original, et nettoyez-le avant de le rendre.

Activité d'entrée brute

fediverse.activity reçoit l'activité ActivityPub entrée vérifiée sous forme d'objet JSON brut. Owncast l'envoie après que la signature HTTP ait passé la vérification et que l'origine de l'acteur de l'activité corresponde à l'origine du propriétaire de la clé de signature.

Le catch-all s'exécute en plus d'un gestionnaire spécialisé. Par exemple, une citation acceptée peut invoquer à la fois onFediverseQuote et onFediverse.

module.exports = definePlugin({
onFediverse(activity) {
if (typeof activity.type === 'string') {
console.log(`inbound activity: ${activity.type}`);
}
},
});

La vérification de la signature et de l'origine de l'acteur établit d'où provient l'activité. Elles ne rendent pas ses champs sûrs. Traitez l'objet brut comme une entrée de plugin non fiable. Vérifiez les types de champs et les valeurs requises, assainissez le contenu avant de l'afficher et validez les URLs avant de les récupérer.

Chaîne de filtres

Les filtres examinent les messages de chat avant qu'ils ne soient diffusés, avec la possibilité de les réécrire ou de les ignorer. Ils s'exécutent de manière séquentielle selon l'ordre de priorité (le plus bas en premier), et un seul filtre peut arrêter la chaîne : un drop la termine, tandis qu'un modify transmet la nouvelle charge utile au filtre suivant.

Filtre de message de chat : chat.message.received (filtre)

Un gestionnaire de filtre reçoit la même structure ChatMessage que l'événement de message de chat et retourne l'un des trois résultats :

  • passer : laisser le message inchangé.
  • modifier : remplacer le message par une nouvelle charge utile, qui passe au filtre suivant.
  • ignorer : bloquer le message avec un motif. La chaîne s'arrête ici.
module.exports = definePlugin({
filterChatMessage(msg) {
if (msg.body.includes('spam')) return filter.drop('spam');
if (msg.body.includes('damn'))
return filter.modify({ ...msg, body: msg.body.replace('damn', '****') });
return filter.pass();
},
});

Nécessite la permission chat.filter. Lire ou réécrire chaque message de chat a un effet secondaire significatif, donc l'administrateur doit voir la permission pour l'accorder. L'hôte rejette la charge si un plugin définit le gestionnaire de filtre sans déclarer la permission.

Priorité des filtres (optionnelle)

Chaque filtre peut déclarer une priorité. Les chiffres les plus bas s'exécutent plus tôt (valeur par défaut 100). Utilisez cela lorsque le comportement de votre plugin dépend de l'exécution préalable d'autres filtres (par exemple, un filtre de grossièretés devrait généralement s'exécuter avant un traducteur). Voir votre page SDK pour savoir où le définir.

Sécurité des filtres

  • Les erreurs sont traitées comme un passage. Un filtre qui lance une exception ne bloque jamais le chat. La chaîne continue avec le message original.
  • Les filtres ont une limite de temps de 50 ms. Un filtre lent est annulé et traité comme un passage.
  • Après 5 échecs consécutifs (erreurs ou délais d'attente), le plugin est automatiquement désactivé pour le reste de la session, avec une ligne de journal unique. Un appel de filtre réussi réinitialise le compteur, donc une instabilité transitoire ne s'accumule pas. Redémarrez l'hôte pour réactiver.

Tables de commandes

Déclarez une table de commandes pour les alias, les temps de recharge, le contrôle par modérateur, les arguments analysés et les listes automatiques !help. Le contrôle utilise l'identité de l'expéditeur (user.scopes, user.id), pas une supposition de nom d'affichage.

module.exports = definePlugin({
commands: {
uptime: { description: "How long we've been live", run: ctx => ctx.reply('a while!') },
},
});

Voir Commandes de chat pour la référence complète de la table de commandes (alias, temps de recharge, contrôle par modérateur, !help).

Gestionnaire HTTP

Requête HTTP

Se déclenche pour chaque requête à /plugins/\<your-slug>/* qui ne correspond pas à un fichier statique dans public/. Retourne un objet de réponse.

interface IncomingHttpRequest {
method: string;
path: string; // relative to /plugins/<your-slug>/
query: Record<string, string>;
headers: Record<string, string>;
body: string;
remoteAddr: string;
authenticated: boolean; // came from any authenticated Owncast session, admin or viewer
user?: { id: string; displayName: string; scopes: string[] }; // user-token requests only
}

interface OutgoingHttpResponse {
status?: number; // default 200
headers?: Record<string, string>;
body?: string;
}
module.exports = definePlugin({
onHttpRequest(req) {
if (req.path === '/status') return { status: 200, body: '{"ok":true}' };
return { status: 404 };
},
});

Les points de terminaison sont publics par défaut. Contrôlez les fonctionnalités administratives sur req.authenticated. Paths matching a key in admin.pages are auth-gated by the host before your handler runs, so for those routes you don't need to check.

Nécessite la permission http.serve. Le SDK JavaScript expose un unique gestionnaire onHttpRequest global. Le SDK Python ajoute des routes déclaratives par chemin/méthode (@plugin.get, @plugin.route, …). Voir Serveur HTTP pour le modèle de requête complet.

Authentification

Auth check hook

Se déclenche uniquement pour le plugin auth.gate activé, et uniquement lors du chargement de la page / d'un visualiseur, jamais dans le chemin critique (segments vidéo, API, chat). Au moment où il s'exécute, l'hôte a déjà vérifié le cookie de session du visualiseur et résolu leur identité. Votre gestionnaire décide si cette session doit continuer. C'est optionnel : omettez-le et un cookie valide suffit jusqu'à ce qu'il expire.

Retournez l'un des trois verdicts via l'assistant authCheck :

  • ok : garder la session telle quelle.
  • rafraîchir : la garder et réémettre le cookie, éventuellement avec un nouveau ttl en secondes (expiration glissante).
  • refuser : mettre fin à la session et renvoyer le visualiseur à l'écran de connexion. C'est ainsi que vous révoquez l'accès (un utilisateur supprimé ou banni en amont).
interface AuthCheckRequest {
user: {
id: string;
displayName: string;
scopes?: string[];
isAuthenticated?: boolean;
};
}

type AuthCheckResult =
{ action: 'ok' } | { action: 'refresh'; ttl?: number } | { action: 'deny'; reason?: string };
const { definePlugin, owncast, authCheck } = require('@owncast/plugin-sdk');

module.exports = definePlugin({
onAuthCheck(req) {
if (owncast.kv.get(`banned:${req.user.id}`)) {
return authCheck.deny('access revoked');
}
return authCheck.ok();
},
});

Nécessite auth.gate, et cela échoue en fermeture : si le gestionnaire génère une erreur ou expire, l'hôte traite ce chargement de page comme un refus. Parce que la vérification ne s'exécute qu'à /, un visualiseur dont vous révoquez l'accès conserve tous les onglets ouverts fonctionnels jusqu'à ce qu'il les recharge ou que le cookie expire. Le ttl de session est la véritable limite.

Gestionnaires de contenu

These two handlers let a plugin generate tab or extra-page HTML at request time. Use them when content should be personalised per viewer or depend on live stream data. They're the dynamic counterpart to shipping a static HTML file via a tab value's content member or manifest.extraPageContent.content.

Les deux gestionnaires reçoivent une ContentRequest :

interface ContentRequest {
slug: string; // manifest.tabs object key or manifest.extraPageContent.slug
user?: User; // viewer's chat identity: present when authenticated, absent for anonymous viewers
}

Retournez la chaîne HTML complète pour le bloc de contenu. Si vous ne reconnaissez pas le slug, retournez une chaîne vide.

module.exports = definePlugin({
onTabContent(ctx) {
if (ctx.slug === 'stats') {
return `<h1>Live stats for ${ctx.user?.displayName ?? 'viewer'}</h1>`;
}
return '';
},
onPageContent(ctx) {
return ctx.slug === 'banner' ? '<p>Welcome!</p>' : '';
},
});

Contenu de l'onglet

Called when a value in the manifest.tabs object has no static content file. The host passes that value's object key as slug, so a single plugin can serve multiple tabs. Aucune permission nécessaire pour s'abonner. Quelles que soient les API d'Owncast que vous appelez à l'intérieur du gestionnaire, elles nécessitent leurs permissions habituelles.

Contenu de la page

Appelé lorsque manifest.extraPageContent n'a pas de fichier content statique. L'hôte passe le slug depuis le manifeste afin que le gestionnaire sache quel créneau de contenu est demandé. Les mêmes règles de permission que pour le contenu d'onglet.

Voir Contribuer à l'UI pour le côté manifeste.

Événements de connexion SSE

Lorsqu'un navigateur ouvre ou ferme l'un des flux de votre plugin /plugins/\<name>/_sse/\<channel>, Owncast déclenche sse.connect et sse.disconnect. Utilisez-les pour suivre qui est connecté, par exemple pour maintenir un compte en direct pour une superposition. Voir Mises à jour en temps réel pour le côté push qui envoie des données à ces navigateurs.

Connecter / déconnecter : sse.connect, sse.disconnect

interface SSEConnectionEvent {
channel: string; // which _sse/<channel> stream the browser opened
connectionId: number; // unique per connection for the life of the host process
user?: User; // present only when the connection carried a chat identity
}
module.exports = definePlugin({
onSseConnect(e) {
/* e.connectionId, e.channel */
},
onSseDisconnect(e) {
/* same connectionId as the matching connect */
},
});

connectionId est stable pour la durée d'une connexion, vous pouvez donc associer un déconnexion à son connect correspondant et compter le même visualiseur sur plusieurs onglets. Les deux gestionnaires nécessitent la permission http.sse.

Tick

Owncast déclenche un événement tick environ une fois par seconde pour tout plugin qui définit un gestionnaire de tick. Utilisez-le pour des travaux périodiques comme vider des compteurs ou rafraîchir des données mises en cache. Définir le gestionnaire est ce qui vous engage, donc les plugins qui l'omettent ne paient rien.

Tick périodique : tick

interface TickEvent {
now: number; // host wall-clock time in unix milliseconds when the tick fired
}
module.exports = definePlugin({
onTick(e) {
/* e.now */
},
});

Pour une planification unique ou à intervalle personnalisé, utilisez des minuteurs (owncast.timer.setTimeout et setInterval) plutôt que le tick. Aucune permission requise.

Événements entre plugins

Custom events are directed hooks for plugin-to-plugin composition. A plugin declares a local hook name, and the host registers it as \<plugin-slug>.\<hook>. The slug comes from the receiving plugin's manifest, so another plugin cannot claim the same fully qualified hook. Declaring a hook requires no permission. Emitting to one requires events.emit.

// In the plugin whose slug is "announcer":
module.exports = definePlugin({
on: {
'announcement.broadcast'(payload) {
/* react */
},
},
});

// Another plugin targets announcer's fully qualified hook:
owncast.events.emit('announcer.announcement.broadcast', { text: 'We are live' });

The receiving handler uses only its local hook name. Emitters use the full \<recipient-slug>.\<hook> target. Built-in event names remain canonical and cannot be claimed as custom hooks.

Voir APIs d'Owncast pour l'API d'émission.

Référence complète des gestionnaires

Chaque ligne est un événement d'exécution. Le nom du gestionnaire suit la convention de votre SDK : méthodes camelCase (onChatMessage) en JavaScript, décorateurs @plugin.* (@plugin.on_chat_message) en Python.

ÉvénementCharge utilePermission
chat.message.receivedChatMessagenone
chat.user.joinedUtilisateurnone
chat.user.partedUtilisateurnone
chat.user.renamed{ user, previousName }none
chat.message.moderated{ messageId, visible, moderator}none
stream.started{ startedAt, title, summary }none
stream.stopped{ stoppedAt }none
stream.title.changed{ from, to }none
fediverse.follow{ actor }fediverse.inbound
fediverse.like{ actor, target }fediverse.inbound
fediverse.repost{ actor, target }fediverse.inbound
fediverse.quoteFediverseQuotefediverse.inbound
fediverse.mentionFediverseInboundPostfediverse.inbound
fediverse.replyFediverseInboundPostfediverse.inbound
fediverse.activityObjet JSON Raw ActivityPubfediverse.inbound
filtre de message chatChatMessagechat.filter
requête HTTPIncomingHttpRequesthttp.serve
vérification d'authentificationAuthCheckRequestauth.gate
sse.connectSSEConnectionEventhttp.sse
sse.disconnectSSEConnectionEventhttp.sse
tick{ now }none
contenu de l'ongletContentRequestnone. Quelles que soient les API appelées par le gestionnaire
contenu de la pageContentRequestnone. Quelles que soient les API appelées par le gestionnaire
custom hooks(per-hook)none to declare, events.emit to target one

Subscribing to ungated built-in events and declaring custom hooks requires no permission. Les hooks restreints nécessitent la permission indiquée dans le tableau. Appeler les APIs Owncast depuis l'intérieur d'un gestionnaire nécessite également la permission de l'API. Voir les APIs Owncast pour le catalogue des méthodes et ce que chacune accorde.


Improve this page

See something missing or incorrect? Edit the English version of this page or help improve translations.

Contributors to this documentation