owncast-plugin-py 0.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- owncast_plugin_py-0.9.0/PKG-INFO +172 -0
- owncast_plugin_py-0.9.0/README.md +160 -0
- owncast_plugin_py-0.9.0/owncast_plugin/__init__.py +897 -0
- owncast_plugin_py-0.9.0/owncast_plugin/build.py +316 -0
- owncast_plugin_py-0.9.0/owncast_plugin/cli.py +75 -0
- owncast_plugin_py-0.9.0/owncast_plugin/scaffold.py +72 -0
- owncast_plugin_py-0.9.0/owncast_plugin/template/.agents/skills/create-owncast-plugin-py/SKILL.md +394 -0
- owncast_plugin_py-0.9.0/owncast_plugin/template/AGENTS.md +142 -0
- owncast_plugin_py-0.9.0/owncast_plugin/template/INSTRUCTIONS.md +18 -0
- owncast_plugin_py-0.9.0/owncast_plugin/template/README.md +36 -0
- owncast_plugin_py-0.9.0/owncast_plugin/template/__tests__/plugin.test.json +26 -0
- owncast_plugin_py-0.9.0/owncast_plugin/template/plugin.manifest.json +11 -0
- owncast_plugin_py-0.9.0/owncast_plugin/template/src/plugin.py +35 -0
- owncast_plugin_py-0.9.0/owncast_plugin/toolchain.py +116 -0
- owncast_plugin_py-0.9.0/owncast_plugin_py.egg-info/PKG-INFO +172 -0
- owncast_plugin_py-0.9.0/owncast_plugin_py.egg-info/SOURCES.txt +19 -0
- owncast_plugin_py-0.9.0/owncast_plugin_py.egg-info/dependency_links.txt +1 -0
- owncast_plugin_py-0.9.0/owncast_plugin_py.egg-info/entry_points.txt +2 -0
- owncast_plugin_py-0.9.0/owncast_plugin_py.egg-info/top_level.txt +1 -0
- owncast_plugin_py-0.9.0/pyproject.toml +35 -0
- owncast_plugin_py-0.9.0/setup.cfg +4 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: owncast-plugin-py
|
|
3
|
+
Version: 0.9.0
|
|
4
|
+
Summary: SDK and CLI for authoring Owncast plugins in Python
|
|
5
|
+
Author: Owncast
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://owncast.online
|
|
8
|
+
Project-URL: Repository, https://github.com/owncast/plugin-sdk
|
|
9
|
+
Keywords: owncast,plugin,wasm,extism
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# owncast-plugin-py (Python)
|
|
14
|
+
|
|
15
|
+
SDK for authoring [Owncast](https://owncast.online) plugins in **Python**. Plugins ship their Python source and run sandboxed inside the Owncast server on an embedded Python engine. They use the same runtime, wire protocol, and `.ocpkg` format as the [JavaScript SDK](../js), so a Python plugin is a first-class peer of a JS one.
|
|
16
|
+
|
|
17
|
+
You write ordinary Python with decorators. There is no compile step and no PDK to install: the Owncast host embeds one Python engine and runs every Python plugin on it, so your plugin ships as plain source.
|
|
18
|
+
|
|
19
|
+
> ### Plugins are source, not a compiled binary
|
|
20
|
+
>
|
|
21
|
+
> Because the host supplies the Python engine, a plugin package contains only your `plugin.py`, its manifest, and any assets, with no bundled interpreter. That keeps packages small and means an author never installs or runs a wasm toolchain (`extism-py`, binaryen) at all. Those are a maintainer-only dependency of building the engine itself.
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
Scaffold a new plugin project (the Python peer of `npm create owncast-plugin`) with no install, straight from the published package:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
uvx owncast-plugin-py new my-plugin # drops a working starter into ./my-plugin
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
To get the `owncast-plugin-py` CLI on your `PATH` for the build/test/serve/package steps, install the SDK. It fetches and caches the host test/serve binaries on first use, so there's nothing else to install by hand. There's no wasm compiler to fetch, because plugins run on the engine the host embeds.
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
uv tool install owncast-plugin-py # or: pip install owncast-plugin-py
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This writes a `my-plugin/` directory with `plugin.manifest.json`, `src/plugin.py`, a sample `__tests__/plugin.test.json`, and docs (README, INSTRUCTIONS.md, AGENTS.md + a `create-owncast-plugin-py` skill) already wired up. A plugin is just a directory:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
my-plugin/
|
|
41
|
+
├── plugin.manifest.json # name, slug, version, permissions
|
|
42
|
+
├── src/plugin.py # your code
|
|
43
|
+
└── __tests__/*.test.json # optional scenario tests
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Build, test, serve, and package it:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
owncast-plugin-py build my-plugin # emit src/plugin.py -> <slug>.py
|
|
50
|
+
owncast-plugin-py test my-plugin # run the __tests__/ scenarios
|
|
51
|
+
owncast-plugin-py serve my-plugin # local dev server (POST /_dev/chat to drive it)
|
|
52
|
+
owncast-plugin-py package my-plugin # build + bundle -> <slug>.ocpkg (the only file you ship)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Install the `.ocpkg` in Owncast from the admin **Plugins** page (**Upload plugin**) or by copying it to the server's `data/plugins/` directory, then toggle **Enabled**.
|
|
56
|
+
|
|
57
|
+
> **CI / no-install:** the build/package step is also runnable directly with `python3 sdks/python/owncast_plugin_build.py <dir> [--package]`, with no toolchain on `PATH` (it just emits source). `OWNCAST_PLUGIN_HOST_BIN_DIR` points `test`/`serve` at locally-built host binaries, and `OWNCAST_PLUGIN_HOST_BINARIES_VERSION` pins the release they're fetched from.
|
|
58
|
+
|
|
59
|
+
## Writing a plugin
|
|
60
|
+
|
|
61
|
+
Import `plugin`, `owncast`, and `filter`, and register handlers with decorators:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from owncast_plugin import plugin, owncast, filter
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@plugin.on_chat_message
|
|
68
|
+
def greet(msg):
|
|
69
|
+
owncast.chat.send(f"echo: {msg.body}")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@plugin.filter_chat_message
|
|
73
|
+
def block_spam(msg):
|
|
74
|
+
return filter.drop("spam") if "spam" in msg.body else filter.pass_()
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Declare the permissions you use (`chat.send` above) in `plugin.manifest.json`. The build only wires up the host functions your permissions grant.
|
|
78
|
+
|
|
79
|
+
### Event handlers
|
|
80
|
+
|
|
81
|
+
Each decorator subscribes to one event, and the SDK derives the manifest subscriptions from which handlers you define.
|
|
82
|
+
|
|
83
|
+
| Decorator | Fires on |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `@plugin.on_chat_message` | a chat message (notify) |
|
|
86
|
+
| `@plugin.filter_chat_message` | a chat message, **before broadcast** (return a `filter` result, requires `chat.filter`) |
|
|
87
|
+
| `@plugin.on_chat_user_joined` / `_parted` / `_renamed` | chat presence |
|
|
88
|
+
| `@plugin.on_message_moderated` | a message hidden/restored |
|
|
89
|
+
| `@plugin.on_stream_started` / `_stopped` / `_title_changed` | stream lifecycle |
|
|
90
|
+
| `@plugin.on_sse_connect` / `_disconnect` | a viewer's SSE stream opened/closed |
|
|
91
|
+
| `@plugin.on_tick` | ~once per second |
|
|
92
|
+
| `@plugin.on_fediverse_follow` / `_like` / `_repost` / `_mention` / `_reply` | fediverse activity |
|
|
93
|
+
| `@plugin.on("custom.event")` | a plugin-emitted custom event |
|
|
94
|
+
| `@plugin.on_tab_content("slug")` / `@plugin.on_page_content("slug")` | render dynamic viewer-page HTML |
|
|
95
|
+
| `@plugin.on_page_styles` / `@plugin.on_page_scripts` | inject viewer-page CSS / JS computed at request time |
|
|
96
|
+
|
|
97
|
+
Payloads are attribute objects with `snake_case` accessors over the wire JSON (`msg.body`, `msg.user.display_name`, `msg.client_id`). Use `msg.raw` for the underlying dict.
|
|
98
|
+
|
|
99
|
+
### HTTP routing
|
|
100
|
+
|
|
101
|
+
Plugins with `http.serve` can answer requests under `/plugins/<slug>/…`. Route by path and method declaratively:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
@plugin.get("/api/messages")
|
|
105
|
+
def list_messages(req):
|
|
106
|
+
return {"status": 200, "body": "...", "headers": {"Content-Type": "application/json"}}
|
|
107
|
+
|
|
108
|
+
@plugin.post("/api/messages")
|
|
109
|
+
def add_message(req):
|
|
110
|
+
body = req.body
|
|
111
|
+
return {"status": 201}
|
|
112
|
+
|
|
113
|
+
@plugin.on_http_request("/health") # any method, exact path
|
|
114
|
+
def health(req):
|
|
115
|
+
return "ok" # a plain string → 200 with that body
|
|
116
|
+
|
|
117
|
+
@plugin.on_http_request # bare: catch-all fallback
|
|
118
|
+
def fallback(req):
|
|
119
|
+
return {"status": 404}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
- `@plugin.get/post/put/delete/patch(path)` and `@plugin.route(path, methods=[...])` for method-specific routes, and `@plugin.on_http_request(path)` for any method.
|
|
123
|
+
- Paths are exact and **plugin-relative** (e.g. `/api/messages`), excluding the query string. Read query params from `req.query`.
|
|
124
|
+
- A request whose path matches a route but not its method gets an automatic **405**. An unmatched path falls through to the bare catch-all, else **404**.
|
|
125
|
+
- A handler returns a `dict` (`{status, body, headers}`), a `str` (→ 200), or `None` (→ 204).
|
|
126
|
+
|
|
127
|
+
### The `owncast` host API
|
|
128
|
+
|
|
129
|
+
`owncast.<group>.<method>(...)`, and each group is gated by the matching manifest permission.
|
|
130
|
+
|
|
131
|
+
| Group | Methods |
|
|
132
|
+
|---|---|
|
|
133
|
+
| `chat` | `send`, `send_action`, `system`, `send_to`, `reply_to`, `history`, `clients`, `delete_message`, `kick` |
|
|
134
|
+
| `kv` | `get`, `set`, `get_json`, `set_json`, `delete` |
|
|
135
|
+
| `storage` / `fs` | `storage.upload`, `fs.read_text`, `fs.write`, `fs.list`, `fs.delete`, `fs.exists` |
|
|
136
|
+
| `server` / `stream` | `server.info/socials/emotes/federation/tags`, `stream.current/broadcaster` |
|
|
137
|
+
| `video_config` | `read`, `write` |
|
|
138
|
+
| `notifications` | `discord`, `browser_push`, `fediverse` |
|
|
139
|
+
| `users` | `list`, `get`, `set_enabled`, `ban_ip` |
|
|
140
|
+
| `events` / `fediverse` / `sse` | `events.emit`, `fediverse.post`, `sse.send` |
|
|
141
|
+
| `actions` | `add`, `clear` |
|
|
142
|
+
| `timer` | `set_timeout`, `set_interval`, `clear` |
|
|
143
|
+
| `config` / `assets` / `http` | `config.get`, `assets.read_text`, `http.fetch` (needs `network.fetch` + `network.allowedHosts`) |
|
|
144
|
+
|
|
145
|
+
Return values that are JSON objects come back as the same attribute objects (`owncast.server.info().name`), and lists come back as Python lists.
|
|
146
|
+
|
|
147
|
+
The concepts (events, permissions, the `.ocpkg` format, the manifest) are shared with the JS SDK, so the **[Owncast Plugin Author Guide](https://github.com/owncast/plugin-sdk/blob/main/docs/PLUGIN_AUTHOR_GUIDE.md)** applies. Just read the API names as their Pythonic `snake_case` forms.
|
|
148
|
+
|
|
149
|
+
## How it works (and how it differs from the JS SDK)
|
|
150
|
+
|
|
151
|
+
Plugins run on a Python engine the Owncast host embeds and shares across every Python plugin, so there's no per-plugin compile. `build` writes your `src/plugin.py` out as `<slug>.py`, and `package` zips that with the manifest and assets into the `.ocpkg`. A single-file plugin is emitted with the `from owncast_plugin import …` line stripped (the SDK names are already globals in the engine). A plugin that imports other local modules has them inlined into the one shipped `plugin.py`. You still `from owncast_plugin import …` for editor support and unit tests.
|
|
152
|
+
|
|
153
|
+
Consequences worth knowing:
|
|
154
|
+
|
|
155
|
+
- **The entry can't use relative imports.** In `src/plugin.py` import your own modules absolutely (`from helpers import …`), not `from . import helpers`. Relative imports inside a package's own modules are fine.
|
|
156
|
+
- **Pure-Python only, no `pip`.** The embedded engine runs pure Python with no filesystem, so there's no `pip install` and C extensions (numpy, pandas, etc.) won't load. You add a third-party library by copying its pure-Python source into `src/` and importing it like any local module. For outbound HTTP use `owncast.http.fetch`, not `requests`.
|
|
157
|
+
- **Don't shadow stdlib names at module top level.** Your code runs in the same global scope as the runtime (which does `import json`), so a top-level `def json(...)` in your plugin shadows it and breaks things. Name helpers like `json_response` instead.
|
|
158
|
+
- **`snake_case` everywhere**, vs the JS SDK's camelCase (`send_action`, `get_json`, `msg.user.display_name`, `filter.pass_()`, where the trailing underscore avoids the Python keyword `pass`).
|
|
159
|
+
|
|
160
|
+
## Testing
|
|
161
|
+
|
|
162
|
+
`__tests__/*.test.json` scenario files are **identical in format to the JS SDK's** and run through the same `owncast-plugin-test` binary, so a Python port of a plugin can reuse the JS version's test scenarios verbatim. Each scenario dispatches events / HTTP requests and asserts on observed side effects (`chatSends`, kv writes, HTTP responses, …).
|
|
163
|
+
|
|
164
|
+
## Status
|
|
165
|
+
|
|
166
|
+
Working today: the runtime (`owncast_plugin/`), the `owncast-plugin-py` CLI (`new`/`build`/`test`/`serve`/`package`) with lazy host-binary download, the full host API, HTTP routing, `.ocpkg` packaging, a pip/uv-installable package (`pyproject.toml`), and CI that builds + tests every example. All 27 of the JS example plugins have Python counterparts under [`examples/python/`](../../examples/python).
|
|
167
|
+
|
|
168
|
+
Not yet (roadmap): publishing to PyPI and type stubs.
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# owncast-plugin-py (Python)
|
|
2
|
+
|
|
3
|
+
SDK for authoring [Owncast](https://owncast.online) plugins in **Python**. Plugins ship their Python source and run sandboxed inside the Owncast server on an embedded Python engine. They use the same runtime, wire protocol, and `.ocpkg` format as the [JavaScript SDK](../js), so a Python plugin is a first-class peer of a JS one.
|
|
4
|
+
|
|
5
|
+
You write ordinary Python with decorators. There is no compile step and no PDK to install: the Owncast host embeds one Python engine and runs every Python plugin on it, so your plugin ships as plain source.
|
|
6
|
+
|
|
7
|
+
> ### Plugins are source, not a compiled binary
|
|
8
|
+
>
|
|
9
|
+
> Because the host supplies the Python engine, a plugin package contains only your `plugin.py`, its manifest, and any assets, with no bundled interpreter. That keeps packages small and means an author never installs or runs a wasm toolchain (`extism-py`, binaryen) at all. Those are a maintainer-only dependency of building the engine itself.
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
Scaffold a new plugin project (the Python peer of `npm create owncast-plugin`) with no install, straight from the published package:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
uvx owncast-plugin-py new my-plugin # drops a working starter into ./my-plugin
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
To get the `owncast-plugin-py` CLI on your `PATH` for the build/test/serve/package steps, install the SDK. It fetches and caches the host test/serve binaries on first use, so there's nothing else to install by hand. There's no wasm compiler to fetch, because plugins run on the engine the host embeds.
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
uv tool install owncast-plugin-py # or: pip install owncast-plugin-py
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
This writes a `my-plugin/` directory with `plugin.manifest.json`, `src/plugin.py`, a sample `__tests__/plugin.test.json`, and docs (README, INSTRUCTIONS.md, AGENTS.md + a `create-owncast-plugin-py` skill) already wired up. A plugin is just a directory:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
my-plugin/
|
|
29
|
+
├── plugin.manifest.json # name, slug, version, permissions
|
|
30
|
+
├── src/plugin.py # your code
|
|
31
|
+
└── __tests__/*.test.json # optional scenario tests
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Build, test, serve, and package it:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
owncast-plugin-py build my-plugin # emit src/plugin.py -> <slug>.py
|
|
38
|
+
owncast-plugin-py test my-plugin # run the __tests__/ scenarios
|
|
39
|
+
owncast-plugin-py serve my-plugin # local dev server (POST /_dev/chat to drive it)
|
|
40
|
+
owncast-plugin-py package my-plugin # build + bundle -> <slug>.ocpkg (the only file you ship)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Install the `.ocpkg` in Owncast from the admin **Plugins** page (**Upload plugin**) or by copying it to the server's `data/plugins/` directory, then toggle **Enabled**.
|
|
44
|
+
|
|
45
|
+
> **CI / no-install:** the build/package step is also runnable directly with `python3 sdks/python/owncast_plugin_build.py <dir> [--package]`, with no toolchain on `PATH` (it just emits source). `OWNCAST_PLUGIN_HOST_BIN_DIR` points `test`/`serve` at locally-built host binaries, and `OWNCAST_PLUGIN_HOST_BINARIES_VERSION` pins the release they're fetched from.
|
|
46
|
+
|
|
47
|
+
## Writing a plugin
|
|
48
|
+
|
|
49
|
+
Import `plugin`, `owncast`, and `filter`, and register handlers with decorators:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from owncast_plugin import plugin, owncast, filter
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@plugin.on_chat_message
|
|
56
|
+
def greet(msg):
|
|
57
|
+
owncast.chat.send(f"echo: {msg.body}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@plugin.filter_chat_message
|
|
61
|
+
def block_spam(msg):
|
|
62
|
+
return filter.drop("spam") if "spam" in msg.body else filter.pass_()
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Declare the permissions you use (`chat.send` above) in `plugin.manifest.json`. The build only wires up the host functions your permissions grant.
|
|
66
|
+
|
|
67
|
+
### Event handlers
|
|
68
|
+
|
|
69
|
+
Each decorator subscribes to one event, and the SDK derives the manifest subscriptions from which handlers you define.
|
|
70
|
+
|
|
71
|
+
| Decorator | Fires on |
|
|
72
|
+
|---|---|
|
|
73
|
+
| `@plugin.on_chat_message` | a chat message (notify) |
|
|
74
|
+
| `@plugin.filter_chat_message` | a chat message, **before broadcast** (return a `filter` result, requires `chat.filter`) |
|
|
75
|
+
| `@plugin.on_chat_user_joined` / `_parted` / `_renamed` | chat presence |
|
|
76
|
+
| `@plugin.on_message_moderated` | a message hidden/restored |
|
|
77
|
+
| `@plugin.on_stream_started` / `_stopped` / `_title_changed` | stream lifecycle |
|
|
78
|
+
| `@plugin.on_sse_connect` / `_disconnect` | a viewer's SSE stream opened/closed |
|
|
79
|
+
| `@plugin.on_tick` | ~once per second |
|
|
80
|
+
| `@plugin.on_fediverse_follow` / `_like` / `_repost` / `_mention` / `_reply` | fediverse activity |
|
|
81
|
+
| `@plugin.on("custom.event")` | a plugin-emitted custom event |
|
|
82
|
+
| `@plugin.on_tab_content("slug")` / `@plugin.on_page_content("slug")` | render dynamic viewer-page HTML |
|
|
83
|
+
| `@plugin.on_page_styles` / `@plugin.on_page_scripts` | inject viewer-page CSS / JS computed at request time |
|
|
84
|
+
|
|
85
|
+
Payloads are attribute objects with `snake_case` accessors over the wire JSON (`msg.body`, `msg.user.display_name`, `msg.client_id`). Use `msg.raw` for the underlying dict.
|
|
86
|
+
|
|
87
|
+
### HTTP routing
|
|
88
|
+
|
|
89
|
+
Plugins with `http.serve` can answer requests under `/plugins/<slug>/…`. Route by path and method declaratively:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
@plugin.get("/api/messages")
|
|
93
|
+
def list_messages(req):
|
|
94
|
+
return {"status": 200, "body": "...", "headers": {"Content-Type": "application/json"}}
|
|
95
|
+
|
|
96
|
+
@plugin.post("/api/messages")
|
|
97
|
+
def add_message(req):
|
|
98
|
+
body = req.body
|
|
99
|
+
return {"status": 201}
|
|
100
|
+
|
|
101
|
+
@plugin.on_http_request("/health") # any method, exact path
|
|
102
|
+
def health(req):
|
|
103
|
+
return "ok" # a plain string → 200 with that body
|
|
104
|
+
|
|
105
|
+
@plugin.on_http_request # bare: catch-all fallback
|
|
106
|
+
def fallback(req):
|
|
107
|
+
return {"status": 404}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
- `@plugin.get/post/put/delete/patch(path)` and `@plugin.route(path, methods=[...])` for method-specific routes, and `@plugin.on_http_request(path)` for any method.
|
|
111
|
+
- Paths are exact and **plugin-relative** (e.g. `/api/messages`), excluding the query string. Read query params from `req.query`.
|
|
112
|
+
- A request whose path matches a route but not its method gets an automatic **405**. An unmatched path falls through to the bare catch-all, else **404**.
|
|
113
|
+
- A handler returns a `dict` (`{status, body, headers}`), a `str` (→ 200), or `None` (→ 204).
|
|
114
|
+
|
|
115
|
+
### The `owncast` host API
|
|
116
|
+
|
|
117
|
+
`owncast.<group>.<method>(...)`, and each group is gated by the matching manifest permission.
|
|
118
|
+
|
|
119
|
+
| Group | Methods |
|
|
120
|
+
|---|---|
|
|
121
|
+
| `chat` | `send`, `send_action`, `system`, `send_to`, `reply_to`, `history`, `clients`, `delete_message`, `kick` |
|
|
122
|
+
| `kv` | `get`, `set`, `get_json`, `set_json`, `delete` |
|
|
123
|
+
| `storage` / `fs` | `storage.upload`, `fs.read_text`, `fs.write`, `fs.list`, `fs.delete`, `fs.exists` |
|
|
124
|
+
| `server` / `stream` | `server.info/socials/emotes/federation/tags`, `stream.current/broadcaster` |
|
|
125
|
+
| `video_config` | `read`, `write` |
|
|
126
|
+
| `notifications` | `discord`, `browser_push`, `fediverse` |
|
|
127
|
+
| `users` | `list`, `get`, `set_enabled`, `ban_ip` |
|
|
128
|
+
| `events` / `fediverse` / `sse` | `events.emit`, `fediverse.post`, `sse.send` |
|
|
129
|
+
| `actions` | `add`, `clear` |
|
|
130
|
+
| `timer` | `set_timeout`, `set_interval`, `clear` |
|
|
131
|
+
| `config` / `assets` / `http` | `config.get`, `assets.read_text`, `http.fetch` (needs `network.fetch` + `network.allowedHosts`) |
|
|
132
|
+
|
|
133
|
+
Return values that are JSON objects come back as the same attribute objects (`owncast.server.info().name`), and lists come back as Python lists.
|
|
134
|
+
|
|
135
|
+
The concepts (events, permissions, the `.ocpkg` format, the manifest) are shared with the JS SDK, so the **[Owncast Plugin Author Guide](https://github.com/owncast/plugin-sdk/blob/main/docs/PLUGIN_AUTHOR_GUIDE.md)** applies. Just read the API names as their Pythonic `snake_case` forms.
|
|
136
|
+
|
|
137
|
+
## How it works (and how it differs from the JS SDK)
|
|
138
|
+
|
|
139
|
+
Plugins run on a Python engine the Owncast host embeds and shares across every Python plugin, so there's no per-plugin compile. `build` writes your `src/plugin.py` out as `<slug>.py`, and `package` zips that with the manifest and assets into the `.ocpkg`. A single-file plugin is emitted with the `from owncast_plugin import …` line stripped (the SDK names are already globals in the engine). A plugin that imports other local modules has them inlined into the one shipped `plugin.py`. You still `from owncast_plugin import …` for editor support and unit tests.
|
|
140
|
+
|
|
141
|
+
Consequences worth knowing:
|
|
142
|
+
|
|
143
|
+
- **The entry can't use relative imports.** In `src/plugin.py` import your own modules absolutely (`from helpers import …`), not `from . import helpers`. Relative imports inside a package's own modules are fine.
|
|
144
|
+
- **Pure-Python only, no `pip`.** The embedded engine runs pure Python with no filesystem, so there's no `pip install` and C extensions (numpy, pandas, etc.) won't load. You add a third-party library by copying its pure-Python source into `src/` and importing it like any local module. For outbound HTTP use `owncast.http.fetch`, not `requests`.
|
|
145
|
+
- **Don't shadow stdlib names at module top level.** Your code runs in the same global scope as the runtime (which does `import json`), so a top-level `def json(...)` in your plugin shadows it and breaks things. Name helpers like `json_response` instead.
|
|
146
|
+
- **`snake_case` everywhere**, vs the JS SDK's camelCase (`send_action`, `get_json`, `msg.user.display_name`, `filter.pass_()`, where the trailing underscore avoids the Python keyword `pass`).
|
|
147
|
+
|
|
148
|
+
## Testing
|
|
149
|
+
|
|
150
|
+
`__tests__/*.test.json` scenario files are **identical in format to the JS SDK's** and run through the same `owncast-plugin-test` binary, so a Python port of a plugin can reuse the JS version's test scenarios verbatim. Each scenario dispatches events / HTTP requests and asserts on observed side effects (`chatSends`, kv writes, HTTP responses, …).
|
|
151
|
+
|
|
152
|
+
## Status
|
|
153
|
+
|
|
154
|
+
Working today: the runtime (`owncast_plugin/`), the `owncast-plugin-py` CLI (`new`/`build`/`test`/`serve`/`package`) with lazy host-binary download, the full host API, HTTP routing, `.ocpkg` packaging, a pip/uv-installable package (`pyproject.toml`), and CI that builds + tests every example. All 27 of the JS example plugins have Python counterparts under [`examples/python/`](../../examples/python).
|
|
155
|
+
|
|
156
|
+
Not yet (roadmap): publishing to PyPI and type stubs.
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
MIT
|