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.
- JavaScript
- 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).
from owncast_plugin import plugin, owncast, filter
@plugin.on_chat_message
def handle_chat(msg):
# react to a chat message
...
@plugin.on_stream_started
def handle_live(info):
# react to the stream going live
...
Les gestionnaires sont des fonctions décorées (@plugin.on_chat_message, @plugin.on_stream_started, …). Les champs de charge utile sont en snake_case (msg.user.display_name, msg.client_id). Utilisez msg.raw pour le dictionnaire sous-jacent.
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"
}
- JavaScript
- Python
module.exports = definePlugin({
onChatMessage(msg) {
if (msg.user?.scopes?.includes('MODERATOR')) {
owncast.chat.send(`hi mod ${msg.user.displayName}`);
}
},
});
@plugin.on_chat_message
def greet_mods(msg):
if msg.user and "MODERATOR" in (msg.user.scopes or []):
owncast.chat.send(f"hi mod {msg.user.display_name}")
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é
useren 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[];
}
- JavaScript
- Python
module.exports = definePlugin({
onChatUserJoined(user) {
owncast.chat.send(`welcome ${user.displayName}`);
},
onChatUserParted(user) {
/* … */
},
});
@plugin.on_chat_user_joined
def welcome(user):
owncast.chat.send(f"welcome {user.display_name}")
@plugin.on_chat_user_parted
def farewell(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 }
- JavaScript
- Python
module.exports = definePlugin({
onStreamStarted(info) {
owncast.chat.send(`live now: ${info.title}`);
},
onStreamStopped(info) {
/* … */
},
onStreamTitleChanged(change) {
/* change.to */
},
});
@plugin.on_stream_started
def announce(info):
owncast.chat.send(f"live now: {info.title}")
@plugin.on_stream_stopped
def wrap_up(info):
...
@plugin.on_stream_title_changed
def retitle(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énement | Gestionnaire JavaScript | Gestionnaire Python | Charge utile |
|---|---|---|---|
fediverse.follow | onFediverseFollow | @plugin.on_fediverse_follow | { actor } |
fediverse.like | onFediverseLike | @plugin.on_fediverse_like | { actor, target } |
fediverse.repost | onFediverseRepost | @plugin.on_fediverse_repost | { actor, target } |
fediverse.quote | onFediverseQuote | @plugin.on_fediverse_quote | FediverseQuote |
fediverse.mention | onFediverseMention | @plugin.on_fediverse_mention | FediverseInboundPost |
fediverse.reply | onFediverseReply | @plugin.on_fediverse_reply | FediverseInboundPost |
fediverse.activity | onFediverse | @plugin.on_fediverse | Objet 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.
- JavaScript
- Python
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}`);
},
});
@plugin.on_fediverse_follow
def thank(event):
owncast.chat.send(f"new follower: {event.actor.handle}")
@plugin.on_fediverse_quote
def record_quote(event):
print(f"{event.actor.handle}: {event.content_text or 'quoted your post'}")
print(f"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.
- JavaScript
- Python
module.exports = definePlugin({
onFediverse(activity) {
if (typeof activity.type === 'string') {
console.log(`inbound activity: ${activity.type}`);
}
},
});
@plugin.on_fediverse
def record_activity(activity):
if isinstance(activity.type, str):
print(f"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.
- JavaScript
- Python
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();
},
});
@plugin.filter_chat_message
def clean(msg):
if "spam" in msg.body:
return filter.drop("spam")
if "damn" in msg.body:
return filter.modify({**msg.raw, "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.
- JavaScript
- Python
module.exports = definePlugin({
commands: {
uptime: { description: "How long we've been live", run: ctx => ctx.reply('a while!') },
},
});
plugin.commands({
"uptime": {"description": "How long we've been live",
"run": lambda 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;
}
- JavaScript
- Python
module.exports = definePlugin({
onHttpRequest(req) {
if (req.path === '/status') return { status: 200, body: '{"ok":true}' };
return { status: 404 };
},
});
@plugin.get("/status")
def status(req):
return {"status": 200, "body": '{"ok":true}'}
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
ttlen 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 };
- JavaScript
- Python
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();
},
});
from owncast_plugin import plugin, owncast, auth_check
@plugin.on_auth_check
def check(req):
if owncast.kv.get(f"banned:{req.user.id}"):
return auth_check.deny("access revoked")
return auth_check.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.
- JavaScript
- Python
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>' : '';
},
});
@plugin.on_tab_content("stats")
def stats(ctx):
name = ctx.user.display_name if ctx.user else "viewer"
return f"<h1>Live stats for {name}</h1>"
@plugin.on_page_content("banner")
def banner(ctx):
return "<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
}
- JavaScript
- Python
module.exports = definePlugin({
onSseConnect(e) {
/* e.connectionId, e.channel */
},
onSseDisconnect(e) {
/* same connectionId as the matching connect */
},
});
@plugin.on_sse_connect
def joined(e):
... # e.connection_id, e.channel
@plugin.on_sse_disconnect
def left(e):
...
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
}
- JavaScript
- Python
module.exports = definePlugin({
onTick(e) {
/* e.now */
},
});
@plugin.on_tick
def each_second(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.
- JavaScript
- Python
// 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' });
# In the plugin whose slug is "announcer":
@plugin.on("announcement.broadcast")
def announce(payload):
...
# 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énement | Charge utile | Permission |
|---|---|---|
chat.message.received | ChatMessage | none |
chat.user.joined | Utilisateur | none |
chat.user.parted | Utilisateur | none |
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.quote | FediverseQuote | fediverse.inbound |
fediverse.mention | FediverseInboundPost | fediverse.inbound |
fediverse.reply | FediverseInboundPost | fediverse.inbound |
fediverse.activity | Objet JSON Raw ActivityPub | fediverse.inbound |
| filtre de message chat | ChatMessage | chat.filter |
| requête HTTP | IncomingHttpRequest | http.serve |
| vérification d'authentification | AuthCheckRequest | auth.gate |
sse.connect | SSEConnectionEvent | http.sse |
sse.disconnect | SSEConnectionEvent | http.sse |
tick | { now } | none |
| contenu de l'onglet | ContentRequest | none. Quelles que soient les API appelées par le gestionnaire |
| contenu de la page | ContentRequest | none. 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.
