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.
- 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 */
},
});
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).
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
...
Os manipuladores são funções decoradas (@plugin.on_chat_message, @plugin.on_stream_started, …). Os campos do payload são snake_case (msg.user.display_name, msg.client_id). Use msg.raw para o dicionário subjacente.
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"
}
- 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 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
usercomo 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[];
}
- 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):
...
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 }
- 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
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.
| Evento | Manipulador JavaScript | Manipulador 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 | Objeto 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.
- 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 é 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.
- 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}")
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.
- 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_()
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.
- 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!")},
})
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;
}
- 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}'}
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
ttlem 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 };
- 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()
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.
- 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>"
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
}
- 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 é 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
}
- JavaScript
- Python
module.exports = definePlugin({
onTick(e) {
/* e.now */
},
});
@plugin.on_tick
def each_second(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.
- 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.
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.
| Evento | Payload | Permission |
|---|---|---|
chat.message.received | ChatMessage | none |
chat.user.joined | Usuário | none |
chat.user.parted | Usuário | 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 Raw ActivityPub | fediverse.inbound |
| filtro de mensagem do chat | ChatMessage | chat.filter |
| requisito HTTP | IncomingHttpRequest | http.serve |
| verificação de autenticação | AuthCheckRequest | auth.gate |
sse.connect | SSEConnectionEvent | http.sse |
sse.disconnect | SSEConnectionEvent | http.sse |
tick | { now } | none |
| conteúdo da aba | ContentRequest | none. Quaisquer APIs que o manipulador chama |
| conteúdo da página | ContentRequest | none. 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.
