Skip to content
Merged
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
11 changes: 8 additions & 3 deletions .github/workflows/examples-js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,19 @@ jobs:

# Build owncast-plugin-test/serve from source and drop them into the
# shared cache, replacing the released binaries postinstall fetched, so
# examples run against the current runtime. The runtime lives in owncast
# (services/plugins); GOPRIVATE + @develop matches release.yml.
# examples run against the current runtime. A matching branch in the
# Owncast repository takes precedence over develop for cross-repo stacks.
- name: Build host binaries from source
working-directory: host-runtime
env:
GOPRIVATE: github.com/owncast/owncast
run: |
go get github.com/owncast/owncast@develop
owncast_ref=develop
if [[ -n "${GITHUB_HEAD_REF:-}" ]] && git ls-remote --exit-code --heads https://github.com/owncast/owncast.git "refs/heads/${GITHUB_HEAD_REF}" >/dev/null; then
owncast_ref="${GITHUB_HEAD_REF}"
fi
echo "Building against owncast@${owncast_ref}"
go get "github.com/owncast/owncast@${owncast_ref}"
go build -o ../sdks/js/bin/.cache/owncast-plugin-test ./cmd/owncast-plugin-test
go build -o ../sdks/js/bin/.cache/owncast-plugin-serve ./cmd/owncast-plugin-serve

Expand Down
11 changes: 8 additions & 3 deletions .github/workflows/examples-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,19 @@ jobs:
go-version-file: host-runtime/go.mod
cache-dependency-path: host-runtime/go.sum

# Build owncast-plugin-test/serve from the current owncast runtime
# (services/plugins). GOPRIVATE + @develop matches release.yml.
# Build owncast-plugin-test/serve from the current owncast runtime.
# A matching Owncast branch takes precedence over develop for stacked PRs.
- name: Build host binaries from source
working-directory: host-runtime
env:
GOPRIVATE: github.com/owncast/owncast
run: |
go get github.com/owncast/owncast@develop
owncast_ref=develop
if [[ -n "${GITHUB_HEAD_REF:-}" ]] && git ls-remote --exit-code --heads https://github.com/owncast/owncast.git "refs/heads/${GITHUB_HEAD_REF}" >/dev/null; then
owncast_ref="${GITHUB_HEAD_REF}"
fi
echo "Building against owncast@${owncast_ref}"
go get "github.com/owncast/owncast@${owncast_ref}"
mkdir -p "$GITHUB_WORKSPACE/pytoolchain/bin" "$GITHUB_WORKSPACE/pytoolchain/share"
go build -o "$GITHUB_WORKSPACE/pytoolchain/bin/owncast-plugin-test" ./cmd/owncast-plugin-test
go build -o "$GITHUB_WORKSPACE/pytoolchain/bin/owncast-plugin-serve" ./cmd/owncast-plugin-serve
Expand Down
26 changes: 23 additions & 3 deletions docs/PLUGIN_AUTHOR_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -380,8 +380,27 @@ Each method requires the matching permission in your manifest:
| `owncast.sse.send(channel, event, data)`, push to browsers | `http.sse` |
| `owncast.timer.setTimeout/setInterval/clear(...)`, schedule callbacks | none (ambient) |
| `owncast.config.get(key, fallback?)`, read manifest-declared config | none (ambient) |
| `owncast.log.info/warning/error(message)`, write to the Owncast server log | none (ambient) |

Calling an API without its permission throws a clear error.
Calling a permission-gated API without its permission throws a clear error.

### Server logging

Use `owncast.log` for operator-visible plugin logs:

```js
owncast.log.info("sync started");
owncast.log.warning("provider response is incomplete");
owncast.log.error("sync failed");
```

```python
owncast.log.info("sync started")
owncast.log.warning("provider response is incomplete")
owncast.log.error("sync failed")
```

`info`, `warning`, and `error` map directly to the same Owncast logrus levels. Every entry includes the calling plugin's slug, for example `plugin schedule-sync: sync started`. Owncast replaces control characters with spaces so each entry stays on one line, then truncates messages longer than 4 KiB. No permission is required. Prefer this API over `console.log` or `print` when the level and plugin identity need to reach the Owncast log reliably.

### SQL database

Expand Down Expand Up @@ -1148,6 +1167,7 @@ Per-step `expect` (on filter and http steps):
Final-state `expect` (on the whole scenario):

- `chatSends`, `chatActions`, `chatSystems`, exact-match lists of chat posts (the bot-sent, "/me" action, and system message variants). Captures sends from any step, including chat posted from an `onHttpRequest` handler.
- `logs`, ordered list of `{plugin, level, message}` entries written through `owncast.log`
- `chatTo`, list of `{clientId, text}` private replies (`owncast.chat.sendTo` / `replyTo`)
- `sseSends`, ordered list of `{channel, event?, data?}` pushed via `owncast.sse.send` (omit `event`/`data` to match only on channel)
- `videoConfigWrites`, list of partial configs applied via `owncast.videoConfig.write()`
Expand Down Expand Up @@ -1441,7 +1461,7 @@ module.exports = definePlugin({
module.exports = definePlugin({
on: {
"announcement.broadcast"(payload) {
console.log(`📢 ${payload.by}: ${payload.text}`);
owncast.log.info(`Announcement from ${payload.by}: ${payload.text}`);
},
},
});
Expand All @@ -1451,7 +1471,7 @@ module.exports = definePlugin({

- **TypeScript works**, name your file `src/plugin.ts` instead of `.js`. The SDK ships TypeScript declarations. Use `import` instead of `require`.
- **Third-party code is limited.** npm packages must be pure JavaScript (no Node built-ins like `fs` or `http`). Python has no `pip`: to use a library, copy its pure-Python source into your project. For outbound HTTP call `owncast.http.fetch`, not `requests` or Node's `http`.
- **`console.log`** in plugin code surfaces in the host log with a `[your-plugin]` prefix. Use it freely for debugging.
- **Server logs:** use `owncast.log.info`, `.warning`, or `.error`. Owncast preserves the level and adds your plugin slug.
- **One handler = one subscription.** Define `onChatMessage` → subscribed. Delete it → unsubscribed. Don't think about it.
- **Mocked tests are fast** (3-5 s including rebuild). Run them on every save.
- **State doesn't leak between scenarios.** Each test gets a fresh plugin instance and a clean in-memory plugin config.
Expand Down
5 changes: 5 additions & 0 deletions docs/WIRE_PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,11 @@ These imports are granted to every plugin without a declared permission. A plugi
- `owncast_timer_clear(id: I64): void`, cancel a pending timer by id.
- `owncast_config_get(keyPtr: PTR): PTR`, returns the JSON value of a `manifest.config` key (admin override, else declared default), or 0-offset for an unknown/unset key.
- `owncast_asset_read(pathPtr: PTR): PTR`, returns the raw bytes of a file from the plugin's own `assets/` directory, or 0-offset when the file is missing or the path escapes the directory. The path is relative to `assets/` and must not start with `/` or contain `..` segments. The host rejects any path that would escape the plugin's own asset tree. Plugins use this to load bundled resources (templates, data files) at request time without needing `storage.fs`.
- `owncast_log_info(messagePtr: PTR): void`, write an info entry to the Owncast server log
- `owncast_log_warning(messagePtr: PTR): void`, write a warning entry to the Owncast server log
- `owncast_log_error(messagePtr: PTR): void`, write an error entry to the Owncast server log

The host attributes every entry to the calling plugin's slug and preserves the selected severity in its logrus output. The fixed functions keep unknown levels out of the wire contract.

The host also dispatches a `tick` event (payload `{now}`, host wall-clock ms) about once a second to any plugin defining `onTick`, independent of timers.

Expand Down
3 changes: 3 additions & 0 deletions engines/build_py.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@
("owncast_timer_clear", "timer_id: int"),
("owncast_config_get", "key: str", "str"),
("owncast_asset_read", "path: str", "str"),
("owncast_log_info", "message: str"),
("owncast_log_warning", "message: str"),
("owncast_log_error", "message: str"),
]


Expand Down
3 changes: 3 additions & 0 deletions engines/javascript/engine.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ declare module 'extism:host' {
owncast_timer_clear(id: I64): void;
owncast_config_get(keyPtr: PTR): PTR;
owncast_asset_read(pathPtr: PTR): PTR;
owncast_log_info(messagePtr: PTR): void;
owncast_log_warning(messagePtr: PTR): void;
owncast_log_error(messagePtr: PTR): void;
owncast_send_chat(textPtr: PTR): void;
owncast_send_chat_action(textPtr: PTR): void;
owncast_send_chat_system(bodyPtr: PTR): void;
Expand Down
2 changes: 1 addition & 1 deletion examples/js/announcer/INSTRUCTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ 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 a line to the **server log** (stderr).
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`.

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

Expand Down
2 changes: 1 addition & 1 deletion examples/js/announcer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

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.

**Demonstrates:** custom-event subscription via the `on: { ... }` object in `definePlugin`. No `events.emit` permission needed, that's only required to _emit_, not to receive.
**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.
9 changes: 7 additions & 2 deletions examples/js/announcer/__tests__/announcer.test.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
[
{
"name": "handles announcement.broadcast without error",
"name": "logs announcement.broadcast at info level",
"events": [
{
"event": "announcement.broadcast",
"payload": { "by": "alice", "text": "stream is live", "at": "2024-01-01T00:00:00Z" }
}
]
],
"expect": {
"logs": [
{ "plugin": "announcer", "level": "info", "message": "Announcement from alice: stream is live" }
]
}
}
]
4 changes: 2 additions & 2 deletions examples/js/announcer/src/plugin.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
const { definePlugin } = require("@owncast/plugin-sdk");
const { definePlugin, owncast } = require("@owncast/plugin-sdk");

module.exports = definePlugin({
on: {
"announcement.broadcast"(payload) {
console.log(`ANNOUNCEMENT from ${payload.by}: ${payload.text}`);
owncast.log.info(`Announcement from ${payload.by}: ${payload.text}`);
}
}
});
11 changes: 6 additions & 5 deletions examples/js/chat-logger/INSTRUCTIONS.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
# Chat Logger

Writes a line to the Owncast **server log** for every chat message. A read-only example that needs no permissions.
Writes a line to the Owncast server log for every chat message. A read-only example that needs no permissions.

## How to use it

1. Enable the plugin in **Admin → Plugins**.
2. Have someone post in chat.
3. Each message shows up in the server's standard-error output, prefixed with `[chat-logger]`.
2. Post a normal chat message to write an info entry.
3. Post a message starting with `warning:` to write a warning entry.
4. Post a message starting with `error:` to write an error entry.

There is nothing to configure and no viewer-facing output. This is purely a server-side log. It's a good starting point for an analytics or archival plugin.
Each line includes `plugin chat-logger:` so an operator can identify its source. There is nothing to configure and no viewer-facing output.

## Permissions

None. A plugin can observe chat events through the message handler without declaring any permission. Permissions are only required to *act* (send, moderate, and so on).
None. Plugins can observe chat events and write attributed server log entries without declaring a permission.
4 changes: 2 additions & 2 deletions examples/js/chat-logger/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# chat-logger

Logs every chat message to stderr with a `[chat-logger]` prefix. No permissions required, read-only via the event payload.
Logs every chat message through Owncast's server log. Messages that start with `warning:` or `error:` use the matching level. Other messages use info. No permission is required.

**Demonstrates:** the `onChatMessage` notification handler, `console.log` debugging, the zero-permissions case (a plugin can react to events without declaring anything).
**Demonstrates:** the `onChatMessage` notification handler, `owncast.log.info/warning/error`, and a public host capability available with an empty permissions list.
19 changes: 17 additions & 2 deletions examples/js/chat-logger/__tests__/log.test.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
[
{
"name": "handles chat.message.received without error",
"name": "preserves info, warning, and error levels",
"events": [
{
"event": "chat.message.received",
"payload": { "id": "1", "user": { "id": "u-alice", "displayName": "alice" }, "body": "hello", "timestamp": "2024-01-01T00:00:00Z" }
},
{
"event": "chat.message.received",
"payload": { "id": "2", "user": { "id": "u-alice", "displayName": "alice" }, "body": "warning: check this", "timestamp": "2024-01-01T00:00:01Z" }
},
{
"event": "chat.message.received",
"payload": { "id": "3", "user": { "id": "u-alice", "displayName": "alice" }, "body": "error: failed", "timestamp": "2024-01-01T00:00:02Z" }
}
]
],
"expect": {
"logs": [
{ "plugin": "chat-logger", "level": "info", "message": "alice: hello" },
{ "plugin": "chat-logger", "level": "warning", "message": "alice: warning: check this" },
{ "plugin": "chat-logger", "level": "error", "message": "alice: error: failed" }
]
}
}
]
2 changes: 1 addition & 1 deletion examples/js/chat-logger/plugin.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "Example Chat Logger",
"slug": "chat-logger",
"version": "0.3.2",
"description": "Logs every chat message via console.log (host stderr). This example was written in JavaScript.",
"description": "Logs chat messages at info, warning, or error level through Owncast's server log. This example was written in JavaScript.",
"category": "examples",
"permissions": []
}
11 changes: 9 additions & 2 deletions examples/js/chat-logger/src/plugin.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
const { definePlugin } = require("@owncast/plugin-sdk");
const { definePlugin, owncast } = require("@owncast/plugin-sdk");

module.exports = definePlugin({
onChatMessage(msg) {
console.log(`${msg.user ? msg.user.displayName : "?"}: ${msg.body}`);
const line = `${msg.user ? msg.user.displayName : "?"}: ${msg.body}`;
if (msg.body.startsWith("error:")) {
owncast.log.error(line);
} else if (msg.body.startsWith("warning:")) {
owncast.log.warning(line);
} else {
owncast.log.info(line);
}
}
});
2 changes: 1 addition & 1 deletion examples/python/announcer/INSTRUCTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ 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 a line to the **server log** (stderr).
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`.

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

Expand Down
2 changes: 1 addition & 1 deletion examples/python/announcer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

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.

**Demonstrates:** custom-event subscription via the `@plugin.on("announcement.broadcast")` decorator. No `events.emit` permission needed. That's only required to _emit_, not to receive.
**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.
9 changes: 7 additions & 2 deletions examples/python/announcer/__tests__/announcer.test.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
[
{
"name": "handles announcement.broadcast without error",
"name": "logs announcement.broadcast at info level",
"events": [
{
"event": "announcement.broadcast",
"payload": { "by": "alice", "text": "stream is live", "at": "2024-01-01T00:00:00Z" }
}
]
],
"expect": {
"logs": [
{ "plugin": "announcer", "level": "info", "message": "Announcement from alice: stream is live" }
]
}
}
]
4 changes: 2 additions & 2 deletions examples/python/announcer/src/plugin.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from owncast_plugin import plugin
from owncast_plugin import owncast, plugin


@plugin.on("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
print(f"ANNOUNCEMENT from {by}: {text}")
owncast.log.info(f"Announcement from {by}: {text}")
11 changes: 6 additions & 5 deletions examples/python/chat-logger/INSTRUCTIONS.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
# Chat Logger

Writes a line to the Owncast **server log** for every chat message. A read-only example that needs no permissions.
Writes a line to the Owncast server log for every chat message. A read-only example that needs no permissions.

## How to use it

1. Enable the plugin in **Admin → Plugins**.
2. Have someone post in chat.
3. Each message shows up in the server's standard-error output, prefixed with `[chat-logger]`.
2. Post a normal chat message to write an info entry.
3. Post a message starting with `warning:` to write a warning entry.
4. Post a message starting with `error:` to write an error entry.

There is nothing to configure and no viewer-facing output. This is purely a server-side log. It's a good starting point for an analytics or archival plugin.
Each line includes `plugin chat-logger:` so an operator can identify its source. There is nothing to configure and no viewer-facing output.

## Permissions

None. A plugin can observe chat events through the message handler without declaring any permission. Permissions are only required to *act* (send, moderate, and so on).
None. Plugins can observe chat events and write attributed server log entries without declaring a permission.
4 changes: 2 additions & 2 deletions examples/python/chat-logger/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# chat-logger

Logs every chat message to stderr with the poster's name. No permissions required: read-only via the event payload.
Logs every chat message through Owncast's server log. Messages that start with `warning:` or `error:` use the matching level. Other messages use info. No permission is required.

**Demonstrates:** the `@plugin.on_chat_message` notification handler, `print()` debugging (host stderr), the zero-permissions case (a plugin can react to events without declaring anything).
**Demonstrates:** the `@plugin.on_chat_message` notification handler, `owncast.log.info/warning/error`, and a public host capability available with an empty permissions list.
19 changes: 17 additions & 2 deletions examples/python/chat-logger/__tests__/log.test.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
[
{
"name": "handles chat.message.received without error",
"name": "preserves info, warning, and error levels",
"events": [
{
"event": "chat.message.received",
"payload": { "id": "1", "user": { "id": "u-alice", "displayName": "alice" }, "body": "hello", "timestamp": "2024-01-01T00:00:00Z" }
},
{
"event": "chat.message.received",
"payload": { "id": "2", "user": { "id": "u-alice", "displayName": "alice" }, "body": "warning: check this", "timestamp": "2024-01-01T00:00:01Z" }
},
{
"event": "chat.message.received",
"payload": { "id": "3", "user": { "id": "u-alice", "displayName": "alice" }, "body": "error: failed", "timestamp": "2024-01-01T00:00:02Z" }
}
]
],
"expect": {
"logs": [
{ "plugin": "chat-logger", "level": "info", "message": "alice: hello" },
{ "plugin": "chat-logger", "level": "warning", "message": "alice: warning: check this" },
{ "plugin": "chat-logger", "level": "error", "message": "alice: error: failed" }
]
}
}
]
2 changes: 1 addition & 1 deletion examples/python/chat-logger/plugin.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "Example Chat Logger",
"slug": "chat-logger",
"version": "0.3.2",
"description": "Logs every chat message via console.log (host stderr). This example was written in Python.",
"description": "Logs chat messages at info, warning, or error level through Owncast's server log. This example was written in Python.",
"category": "examples",
"permissions": []
}
Loading
Loading