Plugin Events
Los complementos reaccionan a las cosas que suceden en Owncast definiendo un controlador para cada evento que les interesa. Solo define los controladores que deseas: un controlador que falta significa que no hay suscripción, y el SDK deriva la lista de suscripción del manifiesto de los controladores que están presentes, por lo que no hay nada más que mantener en sincronía.
El código a continuación se muestra para ambos SDK. Elige tu idioma con las pestañas, y tu elección te seguirá a través de la documentación. ¿Nuevo en esto? Mira primero las páginas de configuración de 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 */
},
});
Los controladores son métodos del objeto que pasas a definePlugin, nombrados en camelCase (onChatMessage, onStreamStarted, …). Los campos de carga útil también son 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
...
Los controladores son funciones decoradas (@plugin.on_chat_message, @plugin.on_stream_started, …). Los campos de carga útil son snake_case (msg.user.display_name, msg.client_id). Usa msg.raw para el diccionario subyacente.
Las cargas útiles se muestran como su forma en wire. Cada SDK expone los campos idiomáticamente: el SDK de JavaScript como está, el SDK de Python como atributos snake_case sobre el mismo JSON (con el diccionario raw disponible también).
Eventos de chat
¿Construyendo un complemento centrado en el chat? Los complementos de chat son un punto de partida más amigable.
Mensaje de chat: chat.message.received
Se activa una vez por mensaje de chat después de que se han ejecutado los filtros y el mensaje se está transmitiendo a los espectadores.
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 lleva la identidad completa del remitente, así que clave el estado por usuario en el estable user.id y gatea el comportamiento solo de moderador en user.scopes (p. ej. "MODERATOR") en lugar de emparejar el nombre mostrado. Para responder de forma privada al remitente, utiliza la API de respuesta al chat (ver APIs de Owncast).
timestamp es la hora del reloj del host para el mensaje. El reloj de sandbox funciona, pero timestamp es determinista y la elección correcta al comparar el tiempo transcurrido entre eventos o asentar en pruebas.
No se requiere permiso para suscribirse.
Los anfitriones más antiguos entregaron
usercomo una cadena de nombre de pantalla simple en lugar del objeto de identidad. Si soportas anfitriones que son anteriores a la carga útil de identidad, léela defensivamente. Consulta tu página SDK para el idioma.
Chat usuario se unió / partió: chat.user.joined, chat.user.parted
Se activa cuando un usuario de chat se conecta o desconecta.
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):
...
No se requiere permiso.
Chat usuario renombrado: chat.user.renamed
Se activa cuando un usuario de chat cambia su nombre mostrado.
interface { user: User; previousName: string }
No se requiere permiso.
Mensaje moderado: chat.message.moderated
Se activa cuando un moderador oculta o muestra un mensaje de chat.
interface { messageId: string; visible: boolean; moderator?: User }
No se requiere permiso.
Ciclo de vida de la transmisión
Transmisión iniciada: stream.started
Se activa cuando comienza una transmisión.
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
No se requiere permiso.
Transmisión detenida: stream.stopped
Se activa cuando termina una transmisión.
interface { stoppedAt?: string }
No se requiere permiso.
Título de la transmisión cambiado: stream.title.changed
Se activa cuando el streamer actualiza el título en medio de la transmisión.
interface { from: string; to: string }
from está actualmente siempre vacío: el evento de cambio de título de Owncast lleva solo el nuevo título.
No se requiere permiso.
Eventos del fediverso
Owncast exposes internal plugin event subscriptions for inbound Fediverse activity. Estos son eventos del complemento, no webhooks HTTP externos. Cada suscripción en esta sección requiere el permiso fediverse.inbound.
| Evento | Controlador de JavaScript | Controlador de Python | Carga útil |
|---|---|---|---|
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 | Objeto JSON de ActivityPub sin procesar |
Seguir, gustar, volver a publicar y citar
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 seguimiento 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 es la dirección completamente calificada, como @alice@fediverse.example. Los ejemplos de seguimiento también llaman a owncast.chat.send, que requiere por separado chat.send:
{ "permissions": ["fediverse.inbound", "chat.send"] }
Mencionar y responder
Ambos reciben 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;
}
Estos hooks especializados aceptan una actividad Create verificada que contiene exactamente una Nota. La nota debe estar atribuida al actor de la actividad. Una mención debe dirigirse al actor local de Owncast. Una respuesta debe hacer referencia a una publicación almacenada por la instancia local de Owncast.
Usa contentText para análisis o para eco en el chat. Usa content solo cuando necesites el formato original, y sanitízalo antes de mostrarlo.
Actividad entrante sin procesar
fediverse.activity recibe la actividad de ActivityPub entrante verificada como su objeto JSON sin procesar. Owncast lo envía después de que la firma HTTP pasa la verificación y el origen del actor de la actividad coincide con el origen del propietario de la clave de firma.
El catch-all se ejecuta además de un controlador especializado. Por ejemplo, una cita aceptada puede invocar tanto onFediverseQuote como 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 verificación de firma y del origen del actor establece de dónde vino la actividad. No hacen que sus campos sean seguros. Trata el objeto sin procesar como entrada no confiable del complemento. Verifique los tipos de campo y los valores requeridos, sanee el contenido antes de renderizarlo y valide las URL antes de recuperarlas.
Cadena de filtros
Los filtros ven los mensajes del chat antes de ser transmitidos, con la capacidad de reescribirlos o descartarlos. Se ejecutan secuencialmente en orden de prioridad (primero el más bajo), y cualquier filtro puede interrumpir la cadena: un drop la termina, mientras que un modify pasa la nueva carga útil al siguiente filtro.
Filtro de mensajes del chat: chat.message.received (filtro)
Un manejador de filtro recibe la misma forma de ChatMessage que el evento de mensaje de chat y devuelve uno de tres resultados:
- pasar: dejar el mensaje sin cambios.
- modificar: reemplazar el mensaje con una nueva carga útil, que fluye al siguiente filtro.
- descartar: bloquear el mensaje con una razón. La cadena se detiene aquí.
- 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_()
Requiere el permiso chat.filter. Leer o reescribir cada mensaje de chat es un efecto secundario significativo, por lo que el administrador tiene que ver el permiso para concederlo. El host rechaza la carga si un plugin define el manejador de filtro sin declarar el permiso.
Prioridad del filtro (opcional)
Cada filtro puede declarar una prioridad. Los números más bajos se ejecutan antes (por defecto 100). Usa esto cuando el comportamiento de tu plugin dependa de si otros filtros ya se han ejecutado (por ejemplo, un filtro de obscenidades generalmente debería ejecutarse antes de un traductor). Consulta tu página de SDK para donde configurarlo.
Seguridad del filtro
- Los errores se tratan como un paso. Un filtro que lanza nunca bloquea el chat. La cadena continúa con el mensaje original.
- Los filtros tienen un límite de tiempo de 50 ms. Un filtro lento se cancela y se trata como un paso.
- Después de 5 fallas consecutivas (errores o tiempos de espera), el plugin se desactiva automáticamente por el resto de la sesión, con una línea de registro única. Una llamada de filtro exitosa restablece el contador, por lo que la inestabilidad transitoria no se acumula. Reinicia el host para reactivarlo.
Tablas de comandos
Declara una tabla de comandos para alias, tiempos de espera, filtrado de moderadores, argumentos analizados y listados automáticos de !help. El filtrado utiliza la identidad del remitente (user.scopes, user.id), no una suposición del nombre de visualización.
- 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 Comandos de chat para la referencia completa de la tabla de comandos (alias, tiempos de espera, filtrado solo para moderadores, !help).
Manejador HTTP
Solicitud HTTP
Se activa para cada solicitud a /plugins/\<your-slug>/* que no coincide con un archivo estático en public/. Devuelve un objeto de respuesta.
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}'}
Los endpoints son públicos por defecto. Filtra las características de administrador en 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.
Requiere el permiso http.serve. El SDK de JavaScript expone un único onHttpRequest que abarca todo. El SDK de Python agrega rutas declarativas por ruta/método (@plugin.get, @plugin.route, …). Consulta Sirviendo HTTP para el modelo completo de solicitud.
Autenticación
Auth check hook
Solo se activa para el plugin auth.gate habilitado, y solo en la carga de página de un espectador, nunca en el camino caliente (segmentos de video, API, chat). Para cuando se ejecute, el host ya ha verificado la cookie de sesión del espectador y ha resuelto su identidad. Tu manejador decide si esa sesión debe continuar. Es opcional: omítelo y una cookie válida es suficiente hasta que expire.
Devuelve uno de tres veredictos a través del helper authCheck:
- ok: mantener la sesión tal como está.
- actualizar: mantenerla y volver a emitir la cookie, opcionalmente con un nuevo
ttlen segundos (expiración deslizante). - negar: finalizar la sesión y enviar al espectador de regreso a la pantalla de inicio de sesión. Esta es la forma en que revocas el acceso (un usuario eliminado o baneado superiormente).
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()
Requiere auth.gate, y falla cerrado: si el manejador lanza un error o se agota el tiempo, el host considera que esa carga de página es un rechazo. Debido a que la verificación se ejecuta solo en /, un espectador cuyo acceso revocas mantiene cualquier pestaña abierta funcionando hasta que se recargue o la cookie expire. El ttl de la sesión es el límite estricto.
Manejadores de contenido
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.
Ambos manejadores reciben 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
}
Devuelve la cadena HTML completa para el bloque de contenido. Si no reconoces el slug, devuelve una cadena vacía.
- 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>"
Contenido de la pestaña
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. No se requiere permiso para suscribirse. Cualquier API de Owncast que llames desde dentro del manejador requiere sus permisos habituales.
Contenido de la página
Llamado cuando manifest.extraPageContent no tiene un archivo de content estático. El host pasa el slug del manifiesto para que el manejador sepa qué ranura de contenido se está solicitando. Las mismas reglas de permisos que el contenido de la pestaña.
Consulta Contribuyendo UI para el lado del manifiesto.
Eventos de conexión SSE
Cuando un navegador abre o cierra uno de los streams /plugins/\<name>/_sse/\<channel> de tu plugin, Owncast dispara sse.connect y sse.disconnect. Úsalos para rastrear quién está conectado, por ejemplo, para mantener un conteo en vivo para un overlay. Consulta Actualizaciones en tiempo real para el lado de envío que envía datos a esos navegadores.
Conexión / desconexión: 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):
...
El connectionId es estable durante la vida de una conexión, por lo que puedes emparejar una desconexión con su conexión coincidente y contar el mismo espectador a través de varias pestañas. Ambos manejadores requieren el permiso http.sse.
Tick
Owncast envía un evento tick aproximadamente una vez por segundo a cualquier plugin que define un manejador de tick. Úsalo para trabajos periódicos como vaciar contadores o refrescar datos en caché. Definir el manejador es lo que opta por participar, por lo que los plugins que lo omiten no pagan nada.
Tick periódico: 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
Para programación de una sola vez o de intervalos personalizados, utiliza temporizadores (owncast.timer.setTimeout y setInterval) en lugar del tick. No se requiere permiso.
Eventos de plugin a 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 APIs de Owncast para la API de emisión.
Referencia completa del manejador
Cada fila es un evento en tiempo de ejecución. El nombre del manejador sigue la convención de tu SDK: métodos en camelCase (onChatMessage) en JavaScript, decoradores @plugin.* (@plugin.on_chat_message) en Python.
| Evento | Carga útil | Permission |
|---|---|---|
chat.message.received | ChatMessage | none |
chat.user.joined | Usuario | none |
chat.user.parted | Usuario | 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 | Objeto JSON de ActivityPub en bruto | fediverse.inbound |
| filtro de mensajes de chat | ChatMessage | chat.filter |
| solicitud HTTP | IncomingHttpRequest | http.serve |
| verificación de autenticación | AuthCheckRequest | auth.gate |
sse.connect | SSEConnectionEvent | http.sse |
sse.disconnect | SSEConnectionEvent | http.sse |
tick | { now } | none |
| contenido de la pestaña | ContentRequest | none. Cualquier API que llame el controlador |
| contenido de la página | ContentRequest | none. Cualquier API que llame el controlador |
| 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. Los ganchos cerrados requieren el permiso que se enumera en la tabla. Llamar a las API de Owncast desde dentro de un controlador también requiere el permiso de la API. Consulta APIs de Owncast para el catálogo de métodos y lo que cada uno otorga.
Improve this page
See something missing or incorrect? Edit the English version of this page or help improve translations.
