Plugin Events
I plugin reagiscono agli eventi che accadono in Owncast definendo un handler per ciascun evento che li interessa. Definisci solo gli handler che desideri: un handler mancante significa nessuna iscrizione, e l'SDK deriva l'elenco di iscrizione del manifesto da quali handler sono presenti, quindi non c'è nulla da mantenere sincronizzato.
Il codice sottostante è mostrato per entrambi gli SDK. Scegli il tuo linguaggio con le schede, e la tua scelta ti seguirà attraverso la documentazione. Nuovo in questo? Guarda prima le pagine di installazione JavaScript o 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 */
},
});
Gli handler sono metodi sull'oggetto che passi a definePlugin, nominati in camelCase (onChatMessage, onStreamStarted, …). I campi del payload sono anch'essi in 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
...
Gli handler sono funzioni decorate (@plugin.on_chat_message, @plugin.on_stream_started, …). I campi del payload sono in snake_case (msg.user.display_name, msg.client_id). Usa msg.raw per il dizionario sottostante.
I payload sono mostrati come la loro forma wire. Ogni SDK espone i campi in modo idiomatico: l'SDK JavaScript così com'è, l'SDK Python come attributi snake_case sullo stesso JSON (con il dizionario raw disponibile anche).
Eventi chat
Stai costruendo un plugin focalizzato sulla chat? I plugin di chat sono un punto di partenza più amichevole.
Messaggio di chat: chat.message.received
Si attiva una volta per ogni messaggio di chat dopo che i filtri sono stati eseguiti e il messaggio viene trasmesso ai visualizzatori.
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 porta l'identità completa del mittente, quindi chiave per lo stato per utente stabile su user.id e attiva il comportamento solo per moderatori su user.scopes (ad esempio "MODERATOR") piuttosto che corrispondere al nome visualizzato. Per rispondere privatamente al mittente, usa l'API di risposta della chat (vedi API di Owncast).
timestamp è l'ora di sistema dell'host per il messaggio. L'orologio sandbox funziona, ma timestamp è deterministico e la scelta giusta quando si confronta il tempo trascorso tra eventi o si affermando nei test.
Nessuna autorizzazione richiesta per iscriversi.
Gli host più vecchi trasmettevano
usercome semplice stringa del nome visualizzato anziché come oggetto di identità. Se supporti host che precedono il payload di identità, leggilo in modo difensivo. Guarda la pagina del tuo SDK per l'idioma.
L'utente della chat si è unito / è andato via: chat.user.joined, chat.user.parted
Si attiva quando un utente della chat si connette o si disconnette.
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):
...
Nessuna autorizzazione richiesta.
L'utente della chat ha cambiato nome: chat.user.renamed
Si attiva quando un utente della chat cambia il proprio nome visualizzato.
interface { user: User; previousName: string }
Nessuna autorizzazione richiesta.
Messaggio moderato: chat.message.moderated
Si attiva quando un moderatore nasconde o riporta in evidenza un messaggio di chat.
interface { messageId: string; visible: boolean; moderator?: User }
Nessuna autorizzazione richiesta.
Ciclo di vita del stream
Stream avviato: stream.started
Si attiva quando inizia una trasmissione.
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
Nessuna autorizzazione richiesta.
Stream fermato: stream.stopped
Si attiva quando termina una trasmissione.
interface { stoppedAt?: string }
Nessuna autorizzazione richiesta.
Stream titolo cambiato: stream.title.changed
Si attiva quando lo streamer aggiorna il titolo nel mezzo dello stream.
interface { from: string; to: string }
from è attualmente sempre vuoto: l'evento di cambiamento del titolo di Owncast porta solo il nuovo titolo.
Nessuna autorizzazione richiesta.
Eventi Fediverse
Owncast exposes internal plugin event subscriptions for inbound Fediverse activity. Questi sono eventi del plugin, non webhook HTTP esterni. Ogni iscrizione in questa sezione richiede il permesso fediverse.inbound.
| Evento | gestore JavaScript | gestore Python | Payload |
|---|---|---|---|
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 | Oggetto JSON Raw ActivityPub |
Segui, metti mi piace, riposta e cita
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 follow contiene solo 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 è l'indirizzo completamente qualificato, come @alice@fediverse.example. Gli esempi di follow chiamano anche owncast.chat.send, che richiede separatamente chat.send:
{ "permissions": ["fediverse.inbound", "chat.send"] }
Menziona e rispondi
Entrambi ricevono 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;
}
Questi hook specializzati accettano un'attività Create verificata contenente esattamente un Note. La nota deve essere attribuita all'attore dell'attività. Una menzione deve indirizzarsi all'attore locale di Owncast. Una risposta deve fare riferimento a un post memorizzato dall'istanza locale di Owncast.
Usa contentText per l'analisi o per ripetere nella chat. Usa content solo quando hai bisogno della formattazione originale e sanitizzala prima del rendering.
Attività in entrata raw
fediverse.activity riceve l'attività verificata in entrata ActivityPub come il suo oggetto JSON raw. Owncast lo invia dopo che la firma HTTP passa la verifica e l'origine dell'attore dell'attività corrisponde all'origine del proprietario della chiave di firma.
Il catch-all funziona in aggiunta a un handler specializzato. Ad esempio, una citazione accettata può attivare sia onFediverseQuote che 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 verifica della firma e dell'origine dell'attore stabilisce da dove proviene l'attività. Non rendono i suoi campi sicuri. Tratta l'oggetto raw come input non affidabile del plugin. Controlla i tipi di campo e i valori richiesti, sanitizza il contenuto prima di renderizzarlo e convalida gli URL prima di recuperarli.
Catena di filtri
I filtri vedono i messaggi della chat prima che vengano trasmessi, con la possibilità di modificarli o rimuoverli. Vengono eseguiti sequenzialmente in ordine di priorità (il più basso per primo), e un singolo filtro può interrompere la catena: un drop la termina, mentre un modify passa il nuovo payload al filtro successivo.
Filtro messaggio chat: chat.message.received (filtro)
Un gestore di filtri riceve la stessa forma di ChatMessage dell'evento chat-message e restituisce uno dei tre risultati:
- pass: lascia passare il messaggio senza modificarlo.
- modify: sostituisce il messaggio con un nuovo payload, che fluisce al filtro successivo.
- drop: blocca il messaggio con una motivazione. La catena si ferma qui.
- 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_()
Richiede il permesso chat.filter. Leggere o riscrivere ogni messaggio della chat è un effetto collaterale significativo, dunque l'amministratore deve vedere il permesso per concederlo. L'host rifiuta il caricamento se un plugin definisce il gestore di filtri senza dichiarare il permesso.
Priorità dei filtri (opzionale)
Ogni filtro può dichiarare una priorità. Numeri più bassi vengono eseguiti prima (predefinito 100). Usa questo quando il comportamento del tuo plugin dipende dal fatto che altri filtri siano già stati eseguiti (ad esempio, un filtro per le parolacce dovrebbe di solito essere eseguito prima di un traduttore). Consulta la tua pagina SDK per sapere dove impostarlo.
Sicurezza dei filtri
- Gli errori vengono trattati come un pass. Un filtro che genera errori non blocca la chat. La catena continua con il messaggio originale.
- I filtri hanno un limite di tempo di 50 ms. Un filtro lento viene annullato e trattato come un pass.
- Dopo 5 fallimenti consecutivi (errori o timeout), il plugin viene disabilitato automaticamente per il resto della sessione, con una riga di log una tantum. Una chiamata di filtro riuscita resetta il contatore, quindi le irregolarità transitorie non si accumulano. Riavvia l'host per riabilitare.
Tabelle dei comandi
Dichiarare una tabella dei comandi per alias, cooldown, gating per moderatori, argomenti analizzati e elenchi automatici !help. Il gating utilizza l'identità del mittente (user.scopes, user.id), non una supposizione sul nome visualizzato.
- 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!")},
})
Consulta Comandi chat per il riferimento completo della tabella dei comandi (alias, cooldown, gating solo per moderatori, !help).
Gestore HTTP
Richiesta HTTP
Viene attivato per ogni richiesta a /plugins/\<your-slug>/* che non corrisponde a un file statico in public/. Restituisce un oggetto di risposta.
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}'}
Gli endpoint sono pubblici per impostazione predefinita. Controlla le funzioni di amministrazione su 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.
Richiede il permesso http.serve. Il SDK di JavaScript espone un unico onHttpRequest catch-all. Il SDK di Python aggiunge percorsi per metodo/per percorso dichiarativi (@plugin.get, @plugin.route, …). Consulta Serving HTTP per il modello di richiesta completo.
Autenticazione
Auth check hook
Viene attivato solo per il plugin abilitato auth.gate, e solo al caricamento della pagina / da parte di un visualizzatore, mai sul percorso caldo (segmenti video, API, chat). Quando viene eseguito, l'host ha già verificato il cookie di sessione del visualizzatore e risolto la loro identità. Il tuo gestore decide se quella sessione dovrebbe continuare. È facoltativo: omettilo e un cookie valido è sufficiente fino a quando non scade.
Restituisci uno dei tre verdetti tramite l'aiuto authCheck:
- ok: mantieni la sessione così com'è.
- refresh: mantienila e riemetti il cookie, eventualmente con un nuovo
ttlin secondi (scadenza mobile). - deny: termina la sessione e riporta il visualizzatore alla schermata di login. Questo è il modo in cui revocare l'accesso (un utente eliminato o bandito upstream).
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()
Richiede auth.gate, e fallisce in modo sicuro: se il gestore genera errori o scade, l'host tratta quel caricamento di pagina come un diniego. Poiché il controllo viene eseguito solo su /, un visualizzatore a cui revoci l'accesso mantiene qualsiasi scheda aperta funzionante fino a quando non ricarica o il cookie scade. Il ttl della sessione è la garanzia finale.
Gestori di contenuti
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.
Entrambi i gestori ricevono una 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
}
Restituisce la stringa HTML completa per il blocco di contenuto. Se non riconosci lo slug, restituisci una stringa vuota.
- 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>"
Contenuto della scheda
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. Nessun permesso richiesto per iscriversi. Qualsiasi API di Owncast che chiami all'interno del gestore richiede i loro permessi abituali.
Contenuto della pagina
Chiamato quando manifest.extraPageContent non ha un file di content statico. L'host passa lo slug dal manifesto in modo che il gestore sappia quale slot di contenuto viene richiesto. Stesse regole di permesso del contenuto della scheda.
Consulta Contribuire UI per il lato manifesto.
Eventi di connessione SSE
Quando un browser apre o chiude uno dei flussi /plugins/\<name>/_sse/\<channel> del tuo plugin, Owncast attiva sse.connect e sse.disconnect. Usali per tenere traccia di chi è connesso, ad esempio per mantenere un conteggio live per un overlay. Consulta Aggiornamenti in tempo reale per il lato push che invia dati a quei browser.
Connetti / disconnetti: 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 è stabile per la vita di una connessione, quindi puoi abbinare un disconnetti con il suo corrispondente connetti e contare lo stesso visualizzatore attraverso più schede. Entrambi i gestori richiedono il permesso http.sse.
Tick
Owncast invia un evento tick circa una volta al secondo a qualsiasi plugin che definisce un gestore di tick. Usalo per lavori periodici come svuotare i contatori o aggiornare i dati memorizzati nella cache. Definire il gestore è ciò che ti consente di partecipare, quindi i plugin che lo omettono non pagano nulla.
Tick periodico: 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
Per pianificazioni una tantum o a intervallo personalizzato, usa timer (owncast.timer.setTimeout e setInterval) invece del tick. Nessun permesso richiesto.
Eventi plugin-to-plugin
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.
Consulta Owncast APIs per la API di emissione.
Riferimento completo del gestore
Ogni riga è un evento runtime. Il nome del gestore segue la convenzione del tuo SDK: metodi camelCase (onChatMessage) in JavaScript, decoratori @plugin.* (@plugin.on_chat_message) in Python.
| Evento | Payload | Permission |
|---|---|---|
chat.message.received | ChatMessage | none |
chat.user.joined | Utente | none |
chat.user.parted | Utente | 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 | Oggetto JSON ActivityPub non elaborato | fediverse.inbound |
| filtro messaggio chat | ChatMessage | chat.filter |
| Richiesta HTTP | IncomingHttpRequest | http.serve |
| verifica autenticazione | AuthCheckRequest | auth.gate |
sse.connect | SSEConnectionEvent | http.sse |
sse.disconnect | SSEConnectionEvent | http.sse |
tick | { now } | none |
| contenuto della scheda | ContentRequest | none. Qualsiasi API che il gestore chiama |
| contenuto della pagina | ContentRequest | none. Qualsiasi API che il gestore chiama |
| 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. Gli hook protetti richiedono l'autorizzazione elencata nella tabella. Chiamare le API Owncast dall'interno di un gestore richiede anche l'autorizzazione dell'API. Vedi API Owncast per il catalogo dei metodi e cosa concede ciascuno.
Improve this page
See something missing or incorrect? Edit this page and improve the documentation for everyone.
