跳至主要内容

Plugin Events

插件通过为它们关心的每个事件定义一个处理程序来响应在Owncast中发生的事情。 只定义您想要的处理程序:缺少处理程序意味着没有订阅,SDK根据存在的处理程序推导清单的订阅列表,因此没有其他内容需要保持同步。

下面的代码同时显示了两个SDK。 通过标签选择您的语言,您的选择会在文档中跟随您。 这是新手吗? 请先查看JavaScriptPython设置页面。

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

处理程序是您传递给definePlugin的对象上命名为camelCase的方法(onChatMessageonStreamStarted等)。 有效负载字段也是camelCase(msg.user.displayNamemsg.clientId)。

有效负载以它们的线形结构显示。 每个SDK以习惯用法公开字段:JavaScript SDK保持原样,Python SDK作为相同JSON上的snake_case属性(同时也提供原始字典)。

聊天事件

构建一个以聊天为中心的插件? 聊天插件是一个更友好的起点。

聊天消息: chat.message.received

每条聊天消息仅在过滤器运行并且消息正在广播给观众后触发一次。

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携带完整的发送者身份,因此要在稳定的user.id上保存每个用户的状态并基于user.scopes拒绝只有版主的行为(例如"MODERATOR"),而不是匹配显示名称。 要私下回复发送者,使用聊天回复API(参见Owncast APIs)。

timestamp是消息的主机墙时钟时间。 沙箱时钟可以工作,但当比较事件之间的经过时间或在测试中进行断言时,timestamp是确定性的正确选择。

订阅不需要权限。

旧的主机user作为普通显示名称字符串而不是身份对象传递。 如果您支持早于身份有效负载的主机,请谨慎读取。 请查看您的SDK页面,以了解习惯用法。

聊天用户加入/离开: chat.user.joinedchat.user.parted

在聊天用户连接或断开的情况下触发。

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

不需要权限。

聊天用户重命名: chat.user.renamed

当聊天用户更改其显示名称时触发。

interface { user: User; previousName: string }

不需要权限。

消息被审查: chat.message.moderated

当管理员隐藏或显示聊天消息时触发。

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

不需要权限。

流生命周期

流已启动: stream.started

当广播开始时触发。

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

不需要权限。

流已停止: stream.stopped

当广播结束时触发。

interface { stoppedAt?: string }

不需要权限。

流标题已更改: stream.title.changed

当流主更改中途标题时触发。

interface { from: string; to: string }

from目前总是为空:Owncast的标题更改事件只携带新标题。

不需要权限。

联邦宇宙事件

Owncast exposes internal plugin event subscriptions for inbound Fediverse activity. 这些是插件事件,不是外部HTTP Webhook。 该部分的每个订阅都需要fediverse.inbound权限。

事件JavaScript处理程序Python处理程序有效负载
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_fediverse原始ActivityPub JSON对象

关注、点赞、转发和引用

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

关注只包含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是完全合格的地址,例如@alice@fediverse.example。 关注示例也会调用owncast.chat.send,这还需要chat.send

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

提及和回复

两者都接收一个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;
}

这些特殊的钩子接受一个经过验证的Create活动,该活动仅包含一个Note。 该注释必须归属于活动参与者。 提及必须地址本地的Owncast参与者。 回复必须引用由本地Owncast实例存储的帖子。

使用contentText进行分析或回声到聊天中。 仅在需要原始格式时使用content,并在渲染之前对其进行清理。

原始入站活动

fediverse.activity接收经过验证的入站ActivityPub活动作为其原始JSON对象。 Owncast在HTTP签名通过验证且活动参与者的来源与签名密钥所有者的来源匹配之后发送它。

通用处理程序除了专用处理程序之外还会运行。 例如,接受的引用可以同时调用onFediverseQuoteonFediverse

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

签名和参与者来源验证确定活动来自何处。 它们并未使其字段变得安全。 将原始对象视为不可信的插件输入。 检查字段类型和必需值,在渲染内容之前清理内容,并在获取 URL 之前验证它们。

过滤链

过滤器在消息广播之前查看聊天消息,并能够重写或丢弃它们。 它们按优先级顺序依次运行(最低优先级优先),且任何一个过滤器都可以短路该链:drop 结束链,而 modify 将新有效负载传递给下一个过滤器。

聊天消息过滤器:chat.message.received(过滤器)

过滤器处理程序接收与聊天消息事件相同的 ChatMessage 形状,并返回三种结果之一:

  • pass:让消息保持不变。
  • modify:用新有效负载替换消息,该有效负载流向下一个过滤器。
  • drop:以理由阻止消息。 链在这里停止。
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();
},
});

需要 chat.filter 权限。 读取或重写每条聊天消息是一个有意义的副作用,因此管理员必须看到权限以授予它。 如果一个插件定义了过滤器处理程序但没有声明权限,则宿主会拒绝负载。

过滤器优先级(可选)

每个过滤器可以声明一个优先级。 较小的数字优先运行(默认值为 100)。 当插件的行为依赖于其他过滤器是否已经运行时使用此选项(例如,脏话过滤器通常应先于翻译器运行)。 请查看您的 SDK 页面以了解如何设置。

过滤器安全性

  • 错误被视为通过。 抛出过滤器永远不会阻止聊天。 链继续使用原始消息。
  • 过滤器的时间限制为 50 毫秒。 慢速过滤器会被取消,并被视为通过。
  • 经过 5 次连续失败(错误或超时)后,插件将在剩余会话中自动禁用,并生成一条一次性日志。 成功的过滤器调用会重置计数器,因此过渡性的波动不会累积。 重新启动宿主以重新启用。

命令表

为别名、冷却时间、管理员网关、解析的参数和自动 !help 列表声明一个 命令表。 网关使用发送者身份 (user.scopesuser.id),而不是显示名称猜测。

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

请参见 聊天命令 以获取完整的命令表参考(别名、冷却时间、仅限管理员的网关,!help)。

HTTP 处理程序

HTTP 请求

当对未在 public/ 中匹配静态文件的 /plugins/\<your-slug>/* 发出每个请求时,触发。 返回响应对象。

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

端点默认是公开的。 在 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.

需要 http.serve 权限。 JavaScript SDK 提供了一个单独的 onHttpRequest 捕获。 Python SDK 添加了声明性每路径/每方法路由(@plugin.get@plugin.route 等)。 请参见 提供 HTTP 获取完整的请求模型。

身份验证

Auth check hook

仅在启用了 auth.gate 插件时触发,并且仅在查看者的 / 页面加载时触发,永远不会在热路径(视频片段、API、聊天)中触发。 在其运行时,宿主已经验证了查看者的会话 cookie,并解析了他们的身份。 您的处理程序决定该会话是否应该继续。 这是可选的:省略它,合法 cookie 就足够了,直到它过期。

通过 authCheck 帮助程序返回三种裁决之一:

  • ok: 保持会话不变。
  • refresh:保持它并重新签发 cookie,选项上带有新的 ttl 秒(滑动过期时间)。
  • deny:结束会话并将查看者跳回登录屏幕。 这就是您撤销访问权限的方式(用户在上游被删除或封禁)。
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();
},
});

需要 auth.gate,并且它在关闭时失败:如果处理程序出错或超时,宿主将此页面加载视为拒绝。 因为检查仅在 / 上运行,因此您撤销的访问权限的查看者将保持任何打开的标签页在工作状态,直到重载或 cookie 过期。 会话 ttl 是硬性上限。

内容处理程序

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.

两个处理程序都接收 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
}

返回内容块的完整 HTML 字符串。 如果您不识别 slug,则返回空字符串。

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

标签内容

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. 订阅时不需要权限。 您在处理程序内部调用的任何 Owncast API 都需要它们通常的权限。

页面内容

manifest.extraPageContent 没有静态 content 文件时调用。 宿主会从清单中传递 slug,因此处理程序知道请求的是哪个内容插槽。 与标签内容相同的权限规则。

请参见 贡献 UI 以获取清单方面信息。

SSE 连接事件

当浏览器打开或关闭您插件的 /plugins/\<name>/_sse/\<channel> 流时,Owncast 会触发 sse.connectsse.disconnect。 利用它们跟踪谁已连接,例如,以保持覆盖层的实时计数。 请参见 实时更新 以获取向这些浏览器发送数据的推送部分。

连接/断开: 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 在连接生命周期内是稳定的,因此您可以将断开与其匹配的连接配对,并在多个标签之间计算相同的观看者。 这两个处理程序都需要 http.sse 权限。

计时器

Owncast 每秒大约调度一次 tick 事件,针对定义了 tick 处理程序的任何插件。 用于周期性工作,例如刷新计数或刷新缓存数据。 定义处理程序是您选择加入的方式,因此跳过的插件无需支付任何费用。

周期性滴答: tick

interface TickEvent {
now: number; // host wall-clock time in unix milliseconds when the tick fired
}
module.exports = definePlugin({
onTick(e) {
/* e.now */
},
});

对于一次性或自定义间隔调度,使用定时器(owncast.timer.setTimeoutsetInterval)而不是 tick。 不需要权限。

插件间事件

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.

请参见 Owncast APIs 以获取 emit API。

完整处理程序参考

每行都是一个运行时事件。 处理程序名称遵循您的 SDK 约定:JavaScript 中的 camelCase 方法(onChatMessage),Python 中的 @plugin.* 装饰器(@plugin.on_chat_message)。

事件有效负载Permission
chat.message.received聊天消息none
chat.user.joined用户none
chat.user.parted用户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.quoteFediverseQuotefediverse.inbound
fediverse.mentionFediverseInboundPostfediverse.inbound
fediverse.replyFediverseInboundPostfediverse.inbound
fediverse.activity原始 ActivityPub JSON 对象fediverse.inbound
聊天消息过滤器聊天消息chat.filter
HTTP 请求IncomingHttpRequesthttp.serve
授权检查AuthCheckRequestauth.gate
sse.connectSSEConnectionEventhttp.sse
sse.disconnectSSEConnectionEventhttp.sse
tick{ now }none
标签内容ContentRequestnone. 处理程序调用的任何 API
页面内容ContentRequestnone. 处理程序调用的任何 API
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. 受限钩子需要表中列出的权限。 从处理程序内部调用 Owncast API 也需要该 API 的权限。 请查看 Owncast APIs 以获取方法目录及其各自的权限。


Improve this page

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

Contributors to this documentation