Ir al contenido principal

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.

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

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

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

EventoControlador de JavaScriptControlador de PythonCarga útil
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_fediverseObjeto 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.

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

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

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.

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

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

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.

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

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

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

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

EventoCarga útilPermission
chat.message.receivedChatMessagenone
chat.user.joinedUsuarionone
chat.user.partedUsuarionone
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.activityObjeto JSON de ActivityPub en brutofediverse.inbound
filtro de mensajes de chatChatMessagechat.filter
solicitud HTTPIncomingHttpRequesthttp.serve
verificación de autenticaciónAuthCheckRequestauth.gate
sse.connectSSEConnectionEventhttp.sse
sse.disconnectSSEConnectionEventhttp.sse
tick{ now }none
contenido de la pestañaContentRequestnone. Cualquier API que llame el controlador
contenido de la páginaContentRequestnone. 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.

Contributors to this documentation