Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,13 @@ cd host-runtime && go run . ../plugins

`tools/bootstrap.sh` compiles `owncast-plugin-test` and `owncast-plugin-serve` from `host-runtime/cmd/`. End users installing the published SDK get these as per-platform release-asset downloads via the postinstall instead, `bootstrap.sh` is for repo developers running against a not-yet-released checkout.

You should see the chat stream flow through the filter chain (slow-mode, buggy-filter, profanity-filter), then fan out to notification subscribers (chat-logger, echo-bot, message-counter, relay), with relay re-emitting `announcement.broadcast` events that announcer handles.
You should see the chat stream flow through the filter chain (slow-mode,
buggy-filter, profanity-filter), then fan out to notification subscribers
(chat-logger, echo-bot, message-counter, relay), with relay emitting
`announcement.broadcast`. announcer subscribes to the host-namespaced
`relay.announcement.broadcast`, so that leg stays silent until the
`host-runtime` Owncast pin carries slug-prefixed custom events (see
[Open items](#open-items--not-yet-done)).

## Run all example tests

Expand Down Expand Up @@ -199,3 +205,12 @@ See **[examples/js/README.md](./examples/js/README.md)** for the full catalog of
- **Action button HTML sanitization**: action buttons with an `html` field ship the HTML verbatim. The Owncast frontend renders trusted external-action HTML today; once these come from plugins, server-side sanitization (or a tighter allowlist) is worth considering.
- **Additional language SDKs**: `sdks/go/` and `sdks/python/` are planned. They'll implement the same wire protocol and consume the shared scenario test corpus and release binaries.
- **Drop-a-JS-file authoring**: the eventual dream is for the host to embed the JS-to-wasm compiler so authors can ship `.js` directly. Today the build step is mandatory.
- **Host-namespaced custom events need a pin bump**: `host-runtime/go.mod` pins
`github.com/owncast/owncast` at a build predating slug-prefixed custom events,
so `owncast-plugin test`, `owncast-plugin serve`, and the demo host still
dispatch the raw suffix. Until the pin moves to a release carrying the fix,
`examples/{js,python}/relay/__tests__/relay.test.json` expects the unprefixed
`announcement.broadcast`, and the relay to announcer round-trip does not fire
locally because announcer subscribes to `relay.announcement.broadcast`. Bump
the pin, flip both scenario expectations to the prefixed name, then re-run the
example suite.
17 changes: 11 additions & 6 deletions docs/PLUGIN_AUTHOR_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1111,19 +1111,24 @@ The `tabs-demo` example ships two static tabs. `page-content-demo` demonstrates

## Plugin-to-plugin events

Plugins compose by emitting custom events:
Plugins compose by emitting custom events. The host prefixes every emitted name
with the emitting plugin's slug, so another plugin cannot impersonate it:

```js
// Emitter (needs events.emit permission)
owncast.events.emit("my-plugin.thing-happened", { id: 123 });
// Emitter (relay, needs events.emit permission)
owncast.events.emit("announcement.broadcast", { id: 123 });

// Subscriber
on: {
"my-plugin.thing-happened"(payload) { /* ... */ }
"relay.announcement.broadcast"(payload) { /* ... */ }
}
```

Use `<your-plugin>.<event>` namespacing. Event names are arbitrary strings.
The name you pass to `emit` is a suffix and may contain dots for hierarchy.
Subscribe using the sender's fully qualified `<plugin-slug>.<event>` name.
If your slug plus the suffix would compose a built-in event name (a plugin
slugged `chat` emitting `message.received`), the host drops the emit rather
than deliver a forged core event.

## Testing

Expand Down Expand Up @@ -1517,7 +1522,7 @@ module.exports = definePlugin({
// announcer/src/plugin.js, no permissions needed
module.exports = definePlugin({
on: {
"announcement.broadcast"(payload) {
"relay.announcement.broadcast"(payload) {
owncast.log.info(`Announcement from ${payload.by}: ${payload.text}`);
},
},
Expand Down
9 changes: 7 additions & 2 deletions docs/WIRE_PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,13 @@ plugin should store values above `Number.MAX_SAFE_INTEGER` (2^53 - 1) as TEXT.
### `events.emit`

- `owncast_emit_event(eventTypePtr: PTR, payloadPtr: PTR): void`. Inputs:
`eventTypePtr` is a UTF-8 event name and `payloadPtr` is one JSON value.
Output: none.
`eventTypePtr` is a UTF-8 event-name suffix and `payloadPtr` is one JSON
value. The host prefixes the suffix with the calling plugin's slug, which it
resolves from the wasm call rather than trusting the guest, so a plugin can
only publish under `<its-own-slug>.` and cannot impersonate another plugin.
If the composed name would equal a built-in event (a plugin slugged `chat`
emitting `message.received`), the host drops the call instead of dispatching
a forged core event. Output: none.

### `server.read`

Expand Down
4 changes: 2 additions & 2 deletions examples/js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ One self-contained npm project per directory. Each has its own `README.md` with
| [profanity-filter](./profanity-filter/) | `filter.modify(payload)`, rewrites flagged words to asterisks. |
| [slow-mode](./slow-mode/) | `filter.drop(reason)`, rate-limits per user, with plugin-config-backed state. |
| [buggy-filter](./buggy-filter/) | Always throws, exercises the host's fail-open + strike system. |
| [relay](./relay/) | Emits a custom `announcement.broadcast` event (plugin → plugin). |
| [announcer](./announcer/) | Subscribes to `announcement.broadcast` via the `on: { ... }` map. |
| [relay](./relay/) | Emits `announcement.broadcast`, delivered as `relay.announcement.broadcast`. |
| [announcer](./announcer/) | Subscribes to `relay.announcement.broadcast` via the `on: { ... }` map. |
| [ip-bot](./ip-bot/) | Outbound HTTP via `owncast.http.fetch`, mocked in tests. |
| [overlay](./overlay/) | `http.serve`, static files from `public/` + dynamic JSON endpoint. |
| [stream-tracker](./stream-tracker/) | Every typed lifecycle / chat-user handler + read APIs. |
Expand Down
8 changes: 6 additions & 2 deletions examples/js/announcer/INSTRUCTIONS.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
# Announcer

A receiver-side demo. It listens for a custom `announcement.broadcast` event that the **relay** example plugin emits, and logs each one to the server. The event type is a plugin-defined string, not a built-in Owncast event.
A receiver-side demo. It listens for `relay.announcement.broadcast`, emitted by
the **relay** example plugin, and logs each one to the server. The event type is
plugin-defined, not a built-in Owncast event.

## How to use it

This plugin does nothing on its own. It's one half of a pair.

1. Install and enable **both** this plugin and the **relay** plugin.
2. In chat, type `/announce <text>` (that command is handled by relay).
3. relay emits an `announcement.broadcast` event. This plugin receives it and writes an info entry to the Owncast server log through `owncast.log.info`.
3. relay emits `announcement.broadcast`. The host delivers it as
`relay.announcement.broadcast`, this plugin receives it and writes an info
entry to the Owncast server log through `owncast.log.info`.

There is no viewer-facing output. Watch the Owncast server logs to see it fire.

Expand Down
4 changes: 3 additions & 1 deletion examples/js/announcer/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# announcer

Subscribes to the custom `announcement.broadcast` event emitted by `../relay` and logs it. The event type is a plugin-defined string, not a built-in Owncast event.
Subscribes to `relay.announcement.broadcast`, the custom event emitted by
`../relay`, and logs it. It is a plugin-defined event, not a built-in Owncast
event. The host adds the `relay.` prefix, so that is the name to subscribe to.

**Demonstrates:** custom-event subscription via the `on: { ... }` object in `definePlugin` and info-level server logging through `owncast.log.info`. Neither receiving the event nor writing the log requires a permission.
4 changes: 2 additions & 2 deletions examples/js/announcer/__tests__/announcer.test.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
[
{
"name": "logs announcement.broadcast at info level",
"name": "logs relay.announcement.broadcast at info level",
"events": [
{
"event": "announcement.broadcast",
"event": "relay.announcement.broadcast",
"payload": { "by": "alice", "text": "stream is live", "at": "2024-01-01T00:00:00Z" }
}
],
Expand Down
2 changes: 1 addition & 1 deletion examples/js/announcer/plugin.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "Example Announcer",
"slug": "announcer",
"version": "0.3.2",
"description": "Handles announcement.broadcast events. This example was written in JavaScript.",
"description": "Handles relay.announcement.broadcast events. This example was written in JavaScript.",
"category": "examples",
"permissions": []
}
2 changes: 1 addition & 1 deletion examples/js/announcer/src/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const { definePlugin, owncast } = require("@owncast/plugin-sdk");

module.exports = definePlugin({
on: {
"announcement.broadcast"(payload) {
"relay.announcement.broadcast"(payload) {
owncast.log.info(`Announcement from ${payload.by}: ${payload.text}`);
}
}
Expand Down
9 changes: 7 additions & 2 deletions examples/js/relay/INSTRUCTIONS.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
# Announcement Relay

Watches chat for an `/announce <text>` command and re-broadcasts it as a custom `announcement.broadcast` event that other plugins can subscribe to. Pairs with the **announcer** example.
Watches chat for an `/announce <text>` command and re-broadcasts it as a custom
`announcement.broadcast` event, delivered to subscribers as
`relay.announcement.broadcast`. Pairs with the **announcer** example.

## How to use it

1. Install and enable this plugin (and **announcer**, if you want to see the event received).
2. In chat, type `/announce Doors open at 8pm`.
3. relay emits an `announcement.broadcast` event carrying the text, the user, and a timestamp. The **announcer** plugin, or any plugin that subscribes, picks it up. Watch the server log to see the round-trip.
3. relay emits `announcement.broadcast` carrying the text, the user, and a
timestamp. The host delivers `relay.announcement.broadcast` to the
**announcer** plugin, or any plugin that subscribes. Watch the server log to
see the round-trip.

This is a plugin-to-plugin communication demo. On its own it has no viewer-facing output.

Expand Down
5 changes: 4 additions & 1 deletion examples/js/relay/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# relay

When a chat message starts with `/announce `, emits a custom `announcement.broadcast` event carrying the announcement body, user, and timestamp. Other plugins (see `../announcer`) can subscribe.
When a chat message starts with `/announce `, emits a custom
`announcement.broadcast` event carrying the announcement body, user, and
timestamp. The host delivers it as `relay.announcement.broadcast`, which other
plugins (see `../announcer`) can subscribe to.

**Demonstrates:** plugin → plugin communication via `owncast.events.emit(type, payload)`, the `events.emit` permission. Pairs with `announcer/` to show one full custom-event round-trip.
4 changes: 2 additions & 2 deletions examples/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ One self-contained plugin per directory, authored in Python and compiled to wasm
| [profanity-filter](./profanity-filter/) | `filter.modify(payload)`, rewrites flagged words to asterisks. |
| [slow-mode](./slow-mode/) | `filter.drop(reason)`, rate-limits per user, with in-memory state. |
| [buggy-filter](./buggy-filter/) | Always raises, exercises the host's fail-open + strike system. |
| [relay](./relay/) | Emits a custom `announcement.broadcast` event (plugin → plugin). |
| [announcer](./announcer/) | Subscribes to `announcement.broadcast` via `@plugin.on(...)`. |
| [relay](./relay/) | Emits `announcement.broadcast`, delivered as `relay.announcement.broadcast`. |
| [announcer](./announcer/) | Subscribes to `relay.announcement.broadcast` via `@plugin.on(...)`. |
| [ip-bot](./ip-bot/) | Outbound HTTP via `owncast.http.fetch`, mocked in tests. |
| [overlay](./overlay/) | `http.serve`, static files from `public/` + dynamic JSON endpoint. |
| [stream-tracker](./stream-tracker/) | Every typed lifecycle / chat-user handler + read APIs. |
Expand Down
8 changes: 6 additions & 2 deletions examples/python/announcer/INSTRUCTIONS.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
# Announcer

A receiver-side demo. It listens for a custom `announcement.broadcast` event that the **relay** example plugin emits, and logs each one to the server. The event type is a plugin-defined string, not a built-in Owncast event.
A receiver-side demo. It listens for `relay.announcement.broadcast`, emitted by
the **relay** example plugin, and logs each one to the server. The event type is
plugin-defined, not a built-in Owncast event.

## How to use it

This plugin does nothing on its own. It's one half of a pair.

1. Install and enable **both** this plugin and the **relay** plugin.
2. In chat, type `/announce <text>` (that command is handled by relay).
3. relay emits an `announcement.broadcast` event. This plugin receives it and writes an info entry to the Owncast server log through `owncast.log.info`.
3. relay emits `announcement.broadcast`. The host delivers it as
`relay.announcement.broadcast`, this plugin receives it and writes an info
entry to the Owncast server log through `owncast.log.info`.

There is no viewer-facing output. Watch the Owncast server logs to see it fire.

Expand Down
9 changes: 7 additions & 2 deletions examples/python/announcer/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# announcer

Subscribes to the custom `announcement.broadcast` event emitted by `../relay` and logs it. The event type is a plugin-defined string, not a built-in Owncast event.
Subscribes to `relay.announcement.broadcast`, the custom event emitted by
`../relay`, and logs it. It is a plugin-defined event, not a built-in Owncast
event. The host adds the `relay.` prefix, so that is the name to subscribe to.

**Demonstrates:** custom-event subscription via the `@plugin.on("announcement.broadcast")` decorator and info-level server logging through `owncast.log.info`. Neither receiving the event nor writing the log requires a permission.
**Demonstrates:** custom-event subscription via the
`@plugin.on("relay.announcement.broadcast")` decorator and info-level server
logging through `owncast.log.info`. Neither receiving the event nor writing the
log requires a permission.
4 changes: 2 additions & 2 deletions examples/python/announcer/__tests__/announcer.test.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
[
{
"name": "logs announcement.broadcast at info level",
"name": "logs relay.announcement.broadcast at info level",
"events": [
{
"event": "announcement.broadcast",
"event": "relay.announcement.broadcast",
"payload": { "by": "alice", "text": "stream is live", "at": "2024-01-01T00:00:00Z" }
}
],
Expand Down
2 changes: 1 addition & 1 deletion examples/python/announcer/plugin.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "Example Announcer",
"slug": "announcer",
"version": "0.3.2",
"description": "Handles announcement.broadcast events. This example was written in Python.",
"description": "Handles relay.announcement.broadcast events. This example was written in Python.",
"category": "examples",
"permissions": []
}
2 changes: 1 addition & 1 deletion examples/python/announcer/src/plugin.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from owncast_plugin import owncast, plugin


@plugin.on("announcement.broadcast")
@plugin.on("relay.announcement.broadcast")
def handle(payload):
by = payload.get("by") if isinstance(payload, dict) else None
text = payload.get("text") if isinstance(payload, dict) else None
Expand Down
9 changes: 7 additions & 2 deletions examples/python/relay/INSTRUCTIONS.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
# Announcement Relay

Watches chat for an `/announce <text>` command and re-broadcasts it as a custom `announcement.broadcast` event that other plugins can subscribe to. Pairs with the **announcer** example.
Watches chat for an `/announce <text>` command and re-broadcasts it as a custom
`announcement.broadcast` event, delivered to subscribers as
`relay.announcement.broadcast`. Pairs with the **announcer** example.

## How to use it

1. Install and enable this plugin (and **announcer**, if you want to see the event received).
2. In chat, type `/announce Doors open at 8pm`.
3. relay emits an `announcement.broadcast` event carrying the text, the user, and a timestamp. The **announcer** plugin, or any plugin that subscribes, picks it up. Watch the server log to see the round-trip.
3. relay emits `announcement.broadcast` carrying the text, the user, and a
timestamp. The host delivers `relay.announcement.broadcast` to the
**announcer** plugin, or any plugin that subscribes. Watch the server log to
see the round-trip.

This is a plugin-to-plugin communication demo. On its own it has no viewer-facing output.

Expand Down
5 changes: 4 additions & 1 deletion examples/python/relay/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# relay

When a chat message starts with `/announce `, emits a custom `announcement.broadcast` event carrying the announcement body, user, and timestamp. Other plugins (see `../announcer`) can subscribe.
When a chat message starts with `/announce `, emits a custom
`announcement.broadcast` event carrying the announcement body, user, and
timestamp. The host delivers it as `relay.announcement.broadcast`, which other
plugins (see `../announcer`) can subscribe to.

**Demonstrates:** plugin → plugin communication via `owncast.events.emit(type, payload)`, the `events.emit` permission. Pairs with `announcer/` to show one full custom-event round trip.
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ combine for richer plugins.
| Post publicly to the fediverse (high-trust) | (any) | `owncast.fediverse.post(text)` | `fediverse.post` |
| React to any verified inbound fediverse activity | `onFediverse(activity)` for raw JSON, plus `onFediverseFollow/Like/Repost/Quote/Mention/Reply` for specialized payloads | none | `fediverse.inbound` |
| Read/change video/transcoding config | (any) | `owncast.videoConfig.read/write` | `videoconfig.read` / `videoconfig.write` |
| Compose with other plugins via custom events | emit: `owncast.events.emit`, receive: `on:{}`| `owncast.events.emit(type, payload)` | `events.emit` (emitter only) |
| Compose with other plugins via custom events | emit: `owncast.events.emit`, receive: `on:{}`| `owncast.events.emit(suffix, payload)`, delivered as `<your-slug>.<suffix>` | `events.emit` (emitter only) |
| Gate the site behind a member login (paywall) | `onHttpRequest` (login flow) + `onAuthCheck` (re-validation) | `owncast.users.register` + `owncast.auth.grantSession/endSession` | `auth.gate` + `users.register` (+ `http.serve`); model on `examples/js/github-auth` |

**Golden rule:** the `permissions` array must contain exactly the permissions
Expand Down Expand Up @@ -240,7 +240,9 @@ which handlers exist. Full list of handlers: `onChatMessage`,
`onStreamTitleChanged`, `onFediverse`, `onFediverseFollow/Like/Repost/Quote/Mention/Reply`,
`onHttpRequest`, `onTick`, `onSseConnect/Disconnect`, `onTabContent`,
`onPageContent`, `onPageStyles`, `onPageScripts`, and
`on: { "namespace.event"() {} }` for custom events.
`on: { "emitter-slug.event"() {} }` for custom events. The host prefixes every
emitted name with the emitter's slug, so subscribe using the sender's slug and
pass only the suffix to `owncast.events.emit`.

Important shape/behavior notes:

Expand Down
Loading
Loading