Vai al contenuto principale

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.

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).

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"
}
module.exports = definePlugin({
onChatMessage(msg) {
if (msg.user?.scopes?.includes('MODERATOR')) {
owncast.chat.send(`hi mod ${msg.user.displayName}`);
}
},
});

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 user come 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[];
}
module.exports = definePlugin({
onChatUserJoined(user) {
owncast.chat.send(`welcome ${user.displayName}`);
},
onChatUserParted(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 }
module.exports = definePlugin({
onStreamStarted(info) {
owncast.chat.send(`live now: ${info.title}`);
},
onStreamStopped(info) {
/* … */
},
onStreamTitleChanged(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.

Eventogestore JavaScriptgestore PythonPayload
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_fediverseOggetto 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.

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 è 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.

module.exports = definePlugin({
onFediverse(activity) {
if (typeof activity.type === 'string') {
console.log(`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.
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();
},
});

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.

module.exports = definePlugin({
commands: {
uptime: { description: "How long we've been live", run: 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;
}
module.exports = definePlugin({
onHttpRequest(req) {
if (req.path === '/status') return { status: 200, body: '{"ok":true}' };
return { status: 404 };
},
});

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 ttl in 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 };
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();
},
});

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.

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>' : '';
},
});

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
}
module.exports = definePlugin({
onSseConnect(e) {
/* e.connectionId, e.channel */
},
onSseDisconnect(e) {
/* same connectionId as the matching connect */
},
});

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
}
module.exports = definePlugin({
onTick(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.

// 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.

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.

EventoPayloadPermission
chat.message.receivedChatMessagenone
chat.user.joinedUtentenone
chat.user.partedUtentenone
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.activityOggetto JSON ActivityPub non elaboratofediverse.inbound
filtro messaggio chatChatMessagechat.filter
Richiesta HTTPIncomingHttpRequesthttp.serve
verifica autenticazioneAuthCheckRequestauth.gate
sse.connectSSEConnectionEventhttp.sse
sse.disconnectSSEConnectionEventhttp.sse
tick{ now }none
contenuto della schedaContentRequestnone. Qualsiasi API che il gestore chiama
contenuto della paginaContentRequestnone. 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.

Contributors to this documentation