メインコンテンツへスキップ

Plugin Events

プラグインは、関心のある各イベントに対してハンドラーを定義することで、Owncast内で発生する事象に反応します。 欲しいハンドラーだけを定義してください:ハンドラーがない場合は購読されず、SDKは存在するハンドラーからマニフェストの購読リストを導き出しますので、同期を保つ必要はありません。

以下のコードは、両方のSDKで表示されます。 タブで言語を選択し、選択した内容はドキュメント全体にわたって保持されます。 これが初めてですか? 最初に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 */
},
});

ハンドラーは、definePluginに渡すオブジェクト上のメソッドで、キャメルケースで名付けられています(onChatMessageonStreamStartedなど)。 ペイロードのフィールドもキャメルケースです(msg.user.displayNamemsg.clientId)。

ペイロードはそのワイヤー形状として表示されます。 各SDKはフィールドをイディオマティックに公開します:JavaScript SDKはそのままで、Python SDKは同じJSONに対してスネークケース属性として(生の辞書も使用可能です)。

チャットイベント

チャット中心のプラグインを作成していますか? チャットプラグインから始めるのがよりフレンドリーです。

チャットメッセージ: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ウェブフックではありません。 このセクションのすべての購読には、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;
}

これらの専門のフックは、正確に1つのNoteを含む検証済みのCreate活動を受け入れます。 ノートは活動アクターに帰属される必要があります。 メンションは、ローカルの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を検証します。

フィルターチェーン

フィルターは、ブロードキャストされる前にチャットメッセージを確認し、書き換えたり削除したりする能力を持っています。 優先順位順にシーケンシャルに実行され、任意の1つのフィルターがチェーンを短絡させることができます:dropはそれを終了させ、modifyは新しいペイロードを次のフィルターに渡します。

チャットメッセージフィルター:chat.message.received(フィルター)

フィルターハンドラーは、チャットメッセージイベントと同じChatMessageの形を受け取り、3つの結果のいずれかを返します:

  • 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ページを参照してください。

フィルターの安全性

  • エラーは通過として扱われます。 スローなフィルターはキャンセルされ、通過として扱われます。 チェーンは元のメッセージで続行されます。
  • フィルターの制限時間は50msです。 スローなフィルターはキャンセルされ、通過として扱われます。
  • 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、チャット)では決して発生しません。 実行する時点で、ホストはすでにビューワーのセッションクッキーを確認し、そのアイデンティティを解決しています。 ハンドラーはそのセッションが継続するかどうかを決定します。 オプションです:省略すると、有効なクッキーだけで十分で、期限が切れるまでそのままです。

authCheckヘルパーを介して3つの判決のいずれかを返します:

  • ok:セッションをそのまま維持します。
  • refresh:それを維持し、オプションで新しい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が必要で、失敗時は閉じます:ハンドラーがエラーを起こしたりタイムアウトしたりした場合、ホストはそのページの読み込みを拒否します。 チェックが/でのみ実行されるため、あなたがアクセスを取り消すと、そのビューワーはリロードまたはクッキーが期限切れになるまで、オープンしているタブでの作業を続けます。 セッションの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文字列を返します。 スラグが認識されない場合は、空の文字列を返します。

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ファイルがない場合に呼び出されます。 ホストはマニフェストからスラグを渡すため、ハンドラーはどのコンテンツスロットがリクエストされているかを知ることができます。 タブコンテンツと同様の権限ルールです。

マニフェスト側についてはUIへの寄付を参照してください。

SSE接続イベント

ブラウザがプラグインの/plugins/\<name>/_sse/\<channel>ストリームの1つを開閉すると、Owncastはsse.connectおよびsse.disconnectを発火します。 これらを使用して、例えばオーバーレイ用に接続されている人数を追跡します。 これらのブラウザにデータを送信するプッシュ側については、リアルタイム更新を参照してください。

接続/切断:sse.connectsse.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は、ティックハンドラーを定義するプラグインに約1秒ごとに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)を使用します。 権限は不要です。

プラグイン間イベント

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.

発火APIについてはOwncast APIsを参照してください。

ハンドラーリファレンスの完全な情報

各行はランタイムイベントです。 ハンドラー名はあなたのSDKの規約に従います:JavaScriptではキャメルケースメソッド(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