Ir para o conteúdo principal

Plugin Events

Os plugins reagem a eventos que acontecem no Owncast definindo um manipulador para cada evento do qual eles se importam. Defina apenas os manipuladores que você deseja: um manipulador ausente significa nenhuma assinatura, e o SDK deriva a lista de assinatura do manifesto a partir dos manipuladores que estão presentes, então não há nada mais para manter em sincronia.

O código abaixo é mostrado para ambos os SDKs. Escolha seu idioma nas guias, e sua escolha o acompanhará por toda a documentação. Novato nisso? Veja as páginas de configuração do JavaScript ou Python primeiro.

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 */
},
});

Os manipuladores são métodos no objeto que você passa para definePlugin, nomeados em camelCase (onChatMessage, onStreamStarted, …). Os campos do payload também são camelCase (msg.user.displayName, msg.clientId).

Os payloads são mostrados em sua forma bruta. Cada SDK expõe os campos de forma idiomática: o SDK JavaScript como está, o SDK Python como atributos snake_case sobre o mesmo JSON (com o dicionário bruto disponível também).

Eventos de chat

Construindo um plugin focado em chat? Plugins de chat são um ponto de partida mais amigável.

Mensagem de chat: chat.message.received

Dispara uma vez por mensagem de chat após os filtros terem sido executados e a mensagem está sendo transmitida para os 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 carrega a identidade completa do remetente, então chaves para o estado por usuário na estável user.id e controle o comportamento apenas de moderadores em user.scopes (por exemplo, "MODERATOR") em vez de correspondência com o nome de exibição. Para responder privadamente ao remetente, use a API de resposta de chat (veja APIs do Owncast).

timestamp é o horário do host para a mensagem. O relógio da sandbox funciona, mas timestamp é determinístico e a escolha certa ao comparar o tempo decorrido entre eventos ou fazendo afirmações em testes.

Nenhuma permissão necessária para se inscrever.

Hosts mais antigos entregavam user como uma string de nome de exibição simples em vez do objeto de identidade. Se você suportar hosts que precedem o payload de identidade, leia de forma defensiva. Veja a página do seu SDK para o idioma.

Usuário de chat entrou / saiu: chat.user.joined, chat.user.parted

Dispara quando um usuário de chat conecta ou 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) {
/* … */
},
});

Nenhuma permissão necessária.

Usuário de chat renomeado: chat.user.renamed

Dispara quando um usuário de chat altera seu nome de exibição.

interface { user: User; previousName: string }

Nenhuma permissão necessária.

Mensagem moderada: chat.message.moderated

Dispara quando um moderador esconde ou revela uma mensagem de chat.

interface { messageId: string; visible: boolean; moderator?: User }

Nenhuma permissão necessária.

Ciclo de vida do stream

Stream iniciado: stream.started

Dispara quando uma transmissão começa.

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 */
},
});

Nenhuma permissão necessária.

Stream parado: stream.stopped

Dispara quando uma transmissão termina.

interface { stoppedAt?: string }

Nenhuma permissão necessária.

Título do stream mudado: stream.title.changed

Dispara quando o streamer atualiza o título no meio do stream.

interface { from: string; to: string }

from está atualmente sempre vazio: o evento de mudança de título do Owncast carrega apenas o novo título.

Nenhuma permissão necessária.

Eventos do fediverse

Owncast exposes internal plugin event subscriptions for inbound Fediverse activity. Esses são eventos de plugin, não webhooks HTTP externos. Cada assinatura nesta seção requer a permissão fediverse.inbound.

EventoManipulador JavaScriptManipulador 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_fediverseObjeto JSON de atividade ActivityPub cru

Seguir, curtir, repostar e 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;
}

Um follow contém apenas 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 é o endereço totalmente qualificado, como @alice@fediverse.example. Os exemplos de follow também chamam owncast.chat.send, que separadamente exige chat.send:

{ "permissions": ["fediverse.inbound", "chat.send"] }

Menção e resposta

Ambos recebem um 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;
}

Esses hooks especializados aceitam uma atividade Create verificada contendo exatamente um Note. A nota deve ser atribuída ao ator da atividade. Uma menção deve endereçar o ator local do Owncast. Uma resposta deve referência uma postagem armazenada pela instância local do Owncast.

Use contentText para análise ou para ecoar no chat. Use content apenas quando você precisar da formatação original, e sanitize antes de renderizar.

Atividade de entrada bruta

fediverse.activity recebe a atividade ActivityPub de entrada verificada como seu objeto JSON bruto. O Owncast a envia após a assinatura HTTP passar pela verificação e a origem do ator da atividade corresponder à origem do proprietário da chave de assinatura.

O catch-all é executado além de um manipulador especializado. Por exemplo, uma quote aceita pode invocar tanto onFediverseQuote quanto onFediverse.

module.exports = definePlugin({
onFediverse(activity) {
if (typeof activity.type === 'string') {
console.log(`inbound activity: ${activity.type}`);
}
},
});

Verificação de assinatura e origem do ator estabelece de onde a atividade veio. Eles não tornam seus campos seguros. Trate o objeto bruto como uma entrada de plugin não confiável. Verifique os tipos de campo e os valores obrigatórios, sanitize o conteúdo antes de renderizá-lo e valide URLs antes de buscá-las.

Cadeia de filtros

Filtros veem mensagens de chat antes de serem transmitidas, com a capacidade de reescrevê-las ou descartá-las. Eles são executados sequencialmente em ordem de prioridade (primeiro os mais baixos), e qualquer filtro pode encurtar a cadeia: um drop a termina, enquanto um modify passa o novo payload para o próximo filtro.

Filtro de mensagem de chat: chat.message.received (filtro)

Um manipulador de filtro recebe a mesma estrutura ChatMessage que o evento de mensagem de chat e retorna um de três resultados:

  • pass: deixe a mensagem passar inalterada.
  • modify: substitua a mensagem por um novo payload, que flui para o próximo filtro.
  • drop: bloqueie a mensagem com um motivo. A cadeia para aqui.
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();
},
});

Requer a permissão chat.filter. Ler ou reescrever cada mensagem de chat é um efeito colateral significativo, então o administrador deve ver a permissão para concedê-la. O host rejeita a carga se um plugin define o manipulador de filtro sem declarar a permissão.

Prioridade do filtro (opcional)

Cada filtro pode declarar uma prioridade. Números mais baixos são executados primeiro (padrão 100). Use isso quando o comportamento do seu plugin depende se outros filtros já foram executados (por exemplo, um filtro de profanidade deve geralmente ser executado antes de um tradutor). Consulte sua página SDK para onde defini-la.

Segurança do filtro

  • Erros são tratados como um pass. Um filtro que lança uma exceção nunca bloqueia o chat. A cadeia continua com a mensagem original.
  • Os filtros têm um limite de tempo de 50 ms. Um filtro lento é cancelado e tratado como um pass.
  • Após 5 falhas consecutivas (erros ou timeouts), o plugin é desativado automaticamente pelo restante da sessão, com uma linha de log única. Uma chamada bem-sucedida de filtro redefine o contador, para que a instabilidade transitória não se acumule. Reinicie o host para reabilitar.

Tabelas de comando

Declare uma tabela de comandos para aliases, recargas, bloqueios de moderador, argumentos analisados e listagens automáticas de !help. O bloqueio usa a identidade do remetente (user.scopes, user.id), não uma adivinhação do nome de exibição.

module.exports = definePlugin({
commands: {
uptime: { description: "How long we've been live", run: ctx => ctx.reply('a while!') },
},
});

Consulte Comandos de chat para a referência completa da tabela de comandos (aliases, recargas, bloqueios para moderadores, !help).

Manipulador HTTP

Requisição HTTP

Dispara para cada requisição a /plugins/\<your-slug>/* que não combina com um arquivo estático em public/. Retorna um objeto de resposta.

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

Endpoints são públicos por padrão. Ative recursos de administração em 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.

Requer a permissão http.serve. O SDK JavaScript expõe um único onHttpRequest que captura tudo. O SDK Python adiciona rotas declarativas por caminho/método (@plugin.get, @plugin.route, …). Consulte Servindo HTTP para o modelo completo de requisições.

Autenticação

Auth check hook

Dispara apenas para o plugin auth.gate habilitado, e apenas na carga da página / de um visualizador, nunca no caminho crítico (segmentos de vídeo, a API, chat). Quando ele é executado, o host já verificou o cookie de sessão do visualizador e resolveu sua identidade. Seu manipulador decide se essa sessão deve continuar. É opcional: omita-o e um cookie válido é suficiente até expirar.

Retorne um de três veredictos via o helper authCheck:

  • ok: mantenha a sessão como está.
  • refresh: mantenha-a e re-emita o cookie, opcionalmente com um novo ttl em segundos (expiração deslizante).
  • deny: termine a sessão e redirecione o visualizador de volta para a tela de login. É assim que você revoga o acesso (um usuário excluído ou banido a montante).
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();
},
});

Requer auth.gate, e falha fechada: se o manipulador gerar um erro ou timeout, o host trata essa carga de página como uma negação. Como a verificação roda apenas em /, um visualizador cujo acesso você revogou mantém qualquer aba aberta funcionando até que ele recarregue ou o cookie expire. O ttl da sessão é a barreira extrema.

Manipuladores de conteúdo

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 os manipuladores recebem um 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
}

Retorne a string HTML completa para o bloco de conteúdo. Se você não reconhecer o slug, retorne uma string vazia.

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

Conteúdo da aba

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. Nenhuma permissão necessária para se inscrever. Quaisquer APIs Owncast que você chamar de dentro do manipulador requerem suas permissões habituais.

Conteúdo da página

Called when manifest.extraPageContent has no static content file. O host passa o slug do manifesto para que o manipulador saiba qual slot de conteúdo está sendo solicitado. As mesmas regras de permissão se aplicam ao conteúdo da aba.

Consulte Contribuindo UI para o lado do manifesto.

Eventos de conexão SSE

Quando um navegador abre ou fecha um dos streams /plugins/\<name>/_sse/\<channel> do seu plugin, Owncast dispara sse.connect e sse.disconnect. Use-os para rastrear quem está conectado, por exemplo, para manter uma contagem ao vivo para uma sobreposição. Consulte Atualizações em tempo real para o lado do push que envia dados para esses navegadores.

Conectar / desconectar: 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 é estável durante a vida de uma conexão, então você pode emparelhar um disconnect com seu connect correspondente e contar o mesmo visualizador em várias abas. Ambos os manipuladores requerem a permissão http.sse.

Tick

Owncast dispara um evento tick aproximadamente uma vez por segundo para qualquer plugin que define um manipulador de tick. Use-o para trabalhos periódicos, como limpar contadores ou atualizar dados em cache. Definir o manipulador é o que te inscreve, então plugins que o deixam de fora não pagam 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 agendamento de uma vez ou intervalos personalizados, use timers (owncast.timer.setTimeout e setInterval) em vez do tick. Nenhuma permissão necessária.

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

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

See Owncast APIs for the emit API.

Referência completa do manipulador

Cada linha é um evento em tempo de execução. O nome do manipulador segue a convenção do seu SDK: métodos camelCase (onChatMessage) em JavaScript, decoradores @plugin.* (@plugin.on_chat_message) em Python.

EventoPayloadPermission
chat.message.receivedChatMessagenone
chat.user.joinedUsuárionone
chat.user.partedUsuárionone
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 Raw ActivityPubfediverse.inbound
filtro de mensagem do chatChatMessagechat.filter
requisito HTTPIncomingHttpRequesthttp.serve
verificação de autenticaçãoAuthCheckRequestauth.gate
sse.connectSSEConnectionEventhttp.sse
sse.disconnectSSEConnectionEventhttp.sse
tick{ now }none
conteúdo da abaContentRequestnone. Quaisquer APIs que o manipulador chama
conteúdo da páginaContentRequestnone. Quaisquer APIs que o manipulador chama
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. Ganchos restritos requerem a permissão listada na tabela. Chamar APIs do Owncast de dentro de um manipulador também requer a permissão da API. Veja APIs do Owncast para o catálogo de métodos e o que cada um concede.


Improve this page

See something missing or incorrect? Edit this page and improve the documentation for everyone.

Contributors to this documentation