Zum Hauptinhalt springen

Plugin Events

Plugins reagieren auf Ereignisse, die in Owncast stattfinden, indem sie einen Handler für jedes Ereignis definieren, das sie interessiert. Definieren Sie nur die Handler, die Sie möchten: Ein fehlender Handler bedeutet keine Anmeldung, und das SDK leitet die Abonnentenliste des Manifests davon ab, welche Handler vorhanden sind, sodass es nichts anderes gibt, was synchronisiert werden muss.

Der untenstehende Code wird für beide SDKs angezeigt. Wählen Sie Ihre Sprache mit den Tabs, und Ihre Auswahl bleibt über die Dokumentation hinweg bestehen. Neu dabei? Sehen Sie sich zuerst die JavaScript oder Python Einrichtungseiten an.

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

Handler sind Methoden des Objekts, das Sie an definePlugin übergeben, benannt in camelCase (onChatMessage, onStreamStarted, …). Payload-Felder sind ebenfalls camelCase (msg.user.displayName, msg.clientId).

Payloads werden in ihrer Drahtform angezeigt. Jedes SDK exponiert die Felder idiomatisch: das JavaScript SDK wie es ist, das Python SDK als snake_case Attribute über dasselbe JSON (mit dem rohen Dict ebenfalls verfügbar).

Chat-Ereignisse

Ein Chat-fokussiertes Plugin erstellen? Chat-Plugins sind ein freundlicherer Ausgangspunkt.

Chatnachricht: chat.message.received

Feuert einmal pro Chatnachricht, nachdem die Filter ausgeführt wurden und die Nachricht an die Zuschauer gesendet wird.

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 enthält die vollständige Senderidentität, also speichert den benutzerspezifischen Zustand auf der stabilen user.id und steuert moderatorenexklusive Verhaltensweisen auf user.scopes (z.B. "MODERATOR") anstatt mit dem Anzeigenamen abzugleichen. Um privat auf den Sender zu antworten, verwenden Sie die Chat-Antwort-API (siehe Owncast APIs).

timestamp ist die Wanduhrzeit des Hosts für die Nachricht. Die Sandbox-Uhr funktioniert, aber timestamp ist deterministisch und die richtige Wahl, um die verstrichene Zeit über Ereignisse hinweg zu vergleichen oder in Tests zu bestätigen.

Es sind keine Berechtigungen erforderlich, um zu abonnieren.

Ältere Hosts lieferten user als einfachen Anzeigenamen-String anstelle des Identitätsobjekts. Wenn Sie Hosts unterstützen, die vor der Identitäts-Payload existierten, lesen Sie sie defensiv. Sehen Sie auf Ihrer SDK-Seite nach dem Idiom.

Chatbenutzer beigetreten / getrennt: chat.user.joined, chat.user.parted

Feuert, wenn sich ein Chatbenutzer verbindet oder trennt.

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

Es sind keine Berechtigungen erforderlich.

Chatbenutzer umbenannt: chat.user.renamed

Feuert, wenn ein Chatbenutzer seinen Anzeigenamen ändert.

interface { user: User; previousName: string }

Es sind keine Berechtigungen erforderlich.

Nachricht moderiert: chat.message.moderated

Feuert, wenn ein Moderator eine Chatnachricht verbirgt oder wieder sichtbar macht.

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

Es sind keine Berechtigungen erforderlich.

Stream-Lebenszyklus

Stream gestartet: stream.started

Feuert, wenn eine Übertragung beginnt.

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

Es sind keine Berechtigungen erforderlich.

Stream gestoppt: stream.stopped

Feuert, wenn eine Übertragung endet.

interface { stoppedAt?: string }

Es sind keine Berechtigungen erforderlich.

Streamtitel geändert: stream.title.changed

Feuert, wenn der Streamer den Titel während des Streams aktualisiert.

interface { from: string; to: string }

from ist derzeit immer leer: Das Ereignis Titel geändert von Owncast enthält nur den neuen Titel.

Es sind keine Berechtigungen erforderlich.

Fediverse-Ereignisse

Owncast exposes internal plugin event subscriptions for inbound Fediverse activity. Dies sind Plugin-Ereignisse, keine externen HTTP-Webhooks. Jedes Abonnement in diesem Abschnitt erfordert die Berechtigung fediverse.inbound.

EreignisJavaScript-HandlerPython-HandlerPayload
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_fediverseRohe ActivityPub JSON-Objekt

Folgen, liken, reposten und zitieren

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

Ein Follow enthält nur 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 ist die vollständig qualifizierte Adresse, wie @alice@fediverse.example. Die Follow-Beispiele rufen ebenfalls owncast.chat.send auf, das separat chat.send erfordert:

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

Erwähnen und Antworten

Beide erhalten ein 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;
}

Diese spezialisierten Hooks akzeptieren eine verifizierte Create-Aktivität, die genau einen Note enthält. Die Notiz muss dem Aktivitätsakteur zugeordnet sein. Eine Erwähnung muss sich auf den lokalen Owncast-Akteur beziehen. Eine Antwort muss auf einen Beitrag verweisen, der von der lokalen Owncast-Instanz gespeichert ist.

Verwenden Sie contentText für Analysen oder um in den Chat zurückzugeben. Verwenden Sie content nur, wenn Sie das ursprüngliche Format benötigen, und reinigen Sie es vor der Darstellung.

Rohe eingehende Aktivität

fediverse.activity erhält die verifizierte eingehende ActivityPub-Aktivität als ihr rohes JSON-Objekt. Owncast sendet es, nachdem die HTTP-Signatur die Überprüfung bestanden hat und der Ursprungsort des Aktivitätsakteurs mit dem Ursprungsort des unterzeichnenden Schlüssels übereinstimmt.

Der Catch-All wird zusätzlich zu einem spezialisierten Handler ausgeführt. Zum Beispiel kann ein akzeptiertes Zitat sowohl onFediverseQuote als auch onFediverse aufrufen.

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

Die Überprüfung der Signatur und des Ursprungs des Akteurs legt fest, wo die Aktivität herkam. Sie machen jedoch nicht die Felder sicher. Behandeln Sie das rohe Objekt als nicht vertrauenswürdige Plugin-Eingabe. Überprüfen Sie die Feldtypen und erforderlichen Werte, bereinigen Sie den Inhalt vor der Darstellung und validieren Sie URLs, bevor Sie diese abrufen.

Filterkette

Filter sehen Chatnachrichten, bevor sie gesendet werden, mit der Möglichkeit, diese zu ändern oder zu verwerfen. Sie werden sequenziell in der Prioritätsreihenfolge (niedrigste zuerst ausgeführt), und jeder Filter kann die Kette unterbrechen: Ein drop beendet sie, während eine modify die neue Nutzlast an den nächsten Filter übergibt.

Chatnachrichtenfilter: chat.message.received (Filter)

Ein Filter-Handler erhält die gleiche ChatMessage-Form wie das Chatnachrichtenevent und gibt eines von drei Ergebnissen zurück:

  • passieren: die Nachricht unverändert durchlassen.
  • modifizieren: die Nachricht durch eine neue Nutzlast ersetzen, die an den nächsten Filter weitergeleitet wird.
  • verwerfen: die Nachricht mit einem Grund blockieren. Die Kette endet hier.
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();
},
});

Benötigt die chat.filter-Berechtigung. Das Lesen oder Umschreiben jeder Chatnachricht hat eine bedeutende Nebenwirkung, daher muss der Admin die Berechtigung sehen, um sie zu erteilen. Der Host lehnt das Laden ab, wenn ein Plugin den Filter-Handler definiert, ohne die Berechtigung zu deklarieren.

Filterpriorität (optional)

Jeder Filter kann eine Priorität deklarieren. Niedrigere Zahlen werden früher ausgeführt (Standard 100). Verwenden Sie dies, wenn das Verhalten Ihres Plugins davon abhängt, ob andere Filter bereits ausgeführt wurden (zum Beispiel sollte ein Schimpfworte-Filter normalerweise vor einem Übersetzer ausgeführt werden). Siehe Ihre SDK-Seite, wo Sie es festlegen können.

Filter-Sicherheit

  • Fehler werden als passieren behandelt. Ein werfender Filter blockiert niemals den Chat. Die Kette wird mit der ursprünglichen Nachricht fortgesetzt.
  • Filter sind zeitlich auf 50 ms begrenzt. Ein langsamer Filter wird abgebrochen und als passieren behandelt.
  • Nach 5 aufeinanderfolgenden Fehlern (Fehler oder Zeitüberschreitungen) wird das Plugin für den Rest der Sitzung automatisch deaktiviert, mit einer einmaligen Protokollzeile. Ein erfolgreicher Filteraufruf setzt den Zähler zurück, sodass vorübergehende Unzuverlässigkeiten nicht akkumuliert werden. Starten Sie den Host neu, um ihn wieder zu aktivieren.

Befehltabellen

Deklarieren Sie eine Befehltabelle für Aliase, Abkühlzeiten, Moderatorenbeschränkungen, geparste Argumente und automatische !help-Listen. Die Beschränkung nutzt die Identität des Absenders (user.scopes, user.id), nicht einen Namenraten-Vorhersage.

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

Siehe Chatbefehle für die vollständige Referenz zur Befehltabelle (Aliases, Abkühlzeiten, Mod-only Gate, !help).

HTTP-Handler

HTTP-Anforderung

Wird für jede Anfrage an /plugins/\<your-slug>/* ausgelöst, die nicht mit einer statischen Datei in public/ übereinstimmt. Gibt ein Antwortobjekt zurück.

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

Endpunkte sind standardmäßig öffentlich. Administrationsfunktionen an req.authenticated beschränken. 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.

Benötigt die http.serve-Berechtigung. Das JavaScript-SDK bietet einen einzelnen onHttpRequest-Catch-All. Das Python-SDK fügt deklarative pro-Pfad/pro-Methode-Routen hinzu (@plugin.get, @plugin.route, …). Siehe HTTP-Dienste für das vollständige Anforderungsmodell.

Authentifizierung

Auth check hook

Wird nur für das aktivierte auth.gate Plugin ausgelöst und nur beim Laden der /-Seite eines Zuschauers, niemals auf dem heißen Pfad (Video-Segmente, die API, Chat). Zu diesem Zeitpunkt hat der Host bereits das Sitzungscookie des Zuschauers überprüft und ihre Identität ermittelt. Ihr Handler entscheidet, ob diese Sitzung fortgesetzt werden sollte. Es ist optional: Lassen Sie es weg, und ein gültiges Cookie reicht aus, bis es abläuft.

Geben Sie eines von drei Urteilen über den authCheck-Helper zurück:

  • ok: die Sitzung unverändert behalten.
  • aktualisieren: behalten und das Cookie neu ausstellen, optional mit einem neuen ttl in Sekunden (gleitende Ablaufzeit).
  • verweigern: die Sitzung beenden und den Zuschauer zurück zum Anmeldebildschirm leiten. So widerrufen Sie den Zugriff (ein Benutzer wurde gelöscht oder gesperrt).
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();
},
});

Benötigt auth.gate, und es schlägt geschlossen fehl: Wenn der Handler einen Fehler verursacht oder nicht antwortet, behandelt der Host diesen Seitenaufruf als Verweigerung. Da die Prüfung nur auf / ausgeführt wird, behält ein Zuschauer, dessen Zugriff Sie widerrufen, alle offenen Registerkarten bis sie neu laden oder das Cookie abläuft. Die Sitzung ttl ist die harte Grenze.

Inhalts-Handler

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.

Beide Handler erhalten eine 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
}

Geben Sie den kompletten HTML-String für den Inhaltsblock zurück. Wenn Sie den Slug nicht erkennen, geben Sie eine leere Zeichenfolge zurück.

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

Tab-Inhalt

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. Es sind keine Berechtigungen erforderlich, um sich anzumelden. Welche Owncast-APIs Sie innerhalb des Handlers aufrufen, benötigen ihre üblichen Berechtigungen.

Seiteninhalt

Wird aufgerufen, wenn manifest.extraPageContent keine statische content-Datei hat. Der Host übergibt den Slug aus dem Manifest, sodass der Handler weiß, welcher Inhaltsplatz angefordert wird. Die gleichen Berechtigungsregeln wie für den Tab-Inhalt.

Siehe UI beizutragen für die Manifest-Seite.

SSE-Verbindungsereignisse

Wenn ein Browser einen Ihrer Plugin-/plugins/<name>/_sse/<channel>-Streams öffnet oder schließt, löst Owncast sse.connect und sse.disconnect aus. Verwenden Sie sie, um nachzuverfolgen, wer verbunden ist, um beispielsweise eine Live-Zählung für ein Overlay aufrechtzuerhalten. Siehe Echtzeitaktualisierungen für die Push-Seite, die Daten an diese Browser sendet.

Verbinden/Trennen: 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 ist stabil für die Dauer einer Verbindung, sodass Sie eine Trennung mit der entsprechenden Verbindung kombinieren und denselben Zuschauer über mehrere Registerkarten hinweg zählen können. Beide Handler benötigen die http.sse-Berechtigung.

Tick

Owncast dispatcht etwa einmal pro Sekunde ein tick-Ereignis für jedes Plugin, das einen Tick-Handler definiert. Verwenden Sie es für periodische Arbeiten wie das Leeren von Zählern oder das Aktualisieren von zwischengespeicherten Daten. Die Definition des Handlers ist das, was Sie anmeldet, sodass Plugins, die ihn weglassen, nichts zahlen.

Periodisches Tick: tick

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

Für einmalige oder benutzerdefinierte zeitliche Planung verwenden Sie Timer (owncast.timer.setTimeout und setInterval) anstelle des Ticks. Es sind keine Berechtigungen erforderlich.

Plugin-zu-Plugin-Ereignisse

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.

Siehe Owncast APIs für die Emit-API.

Vollständige Handlerreferenz

Jede Zeile ist ein Laufzeitereignis. Der Handlername folgt der Konvention Ihres SDKs: CamelCase-Methoden (onChatMessage) in JavaScript, @plugin.*-Dekoratoren (@plugin.on_chat_message) in Python.

EreignisNutzlastPermission
chat.message.receivedChatNachrichtnone
chat.benutzer.beigetretenBenutzernone
chat.benutzer.verlassenBenutzernone
chat.benutzer.umbenannt{ benutzer, vorherigerName }none
chat.nachricht.moderiert{ nachrichtId, sichtbar, moderator}none
stream.ge gestartet{ gestartetAm, titel, zusammenfassung }none
stream.beendet{ beendetAm }none
stream.titel.geändert{ von, zu }none
fediverse.folgen{ akteur }fediverse.eingehend
fediverse.likes{ actor, target }fediverse.eingehend
fediverse.wiederposten{ akteur, ziel }fediverse.eingehend
fediverse.zitierenFediverseQuotefediverse.eingehend
fediverse.erwähnenFediverseInboundPostfediverse.eingehend
fediverse.antwortenFediverseInboundPostfediverse.eingehend
fediverse.aktivitätRaw ActivityPub JSON-Objektfediverse.eingehend
Chat-NachrichtenfilterChatNachrichtchat.filter
HTTP-AnfrageEingehendeHttpAnfragehttp.bedienen
auth ÜberprüfungAuthCheckRequestauth.tor
sse.verbindenSSEConnectionEventhttp.sse
sse.trennenSSEConnectionEventhttp.sse
takt{ jetzt }none
Tab-InhaltInhaltsanfragenone. Egal, welche APIs der Handler aufruft
SeiteninhaltInhaltsanfragenone. Egal, welche APIs der Handler aufruft
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. Eingeschränkte Hooks erfordern die in der Tabelle aufgeführten Berechtigungen. Das Aufrufen von Owncast APIs aus einem Handler erfordert ebenfalls die Berechtigung der API. Siehe Owncast APIs für das Verzeichnis der Methoden und welche Berechtigungen jede hat.


Improve this page

See something missing or incorrect? Edit the English version of this page or help improve translations.

Contributors to this documentation