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
5 changes: 5 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ file (see [build flow](#toolchain-and-build-flow)).
the calling plugin's identity (from a per-instance config value) and rejects
the call if the plugin's manifest didn't grant the permission.
- Everything crosses the boundary as JSON.
- Inbound Fediverse hooks are internal notify subscriptions. They are not
external HTTP webhooks. Owncast verifies the HTTP signature and actor origin,
then sends the raw activity to `onFediverse` / `on_fediverse` and also sends
any matching specialized follow, like, repost, quote, mention, or reply event.
The `fediverse.inbound` manifest permission gates all seven subscriptions.

## Repository layout

Expand Down
21 changes: 20 additions & 1 deletion docs/PLUGIN_AUTHOR_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ module.exports = definePlugin({
onFediverseRepost(event) {
/* {actor, target: {url}} */
},
onFediverseQuote(event) {
/* {actor, target: {url}}; target is the locally authored quoted post */
},

// Fediverse, inbound posts (with content)
onFediverseMention(post) {
Expand All @@ -171,6 +174,11 @@ module.exports = definePlugin({
/* same shape; inReplyTo set */
},

// Every verified inbound activity, including activities handled above
onFediverse(activity) {
/* raw ActivityPub JSON object */
},

// Filter chain (sequential, can mutate or drop)
filterChatMessage(msg) {
return filter.pass();
Expand All @@ -193,6 +201,8 @@ module.exports = definePlugin({

Only define the handlers you actually need. Missing handlers = no subscription.

All seven Fediverse handlers require `fediverse.inbound`: `onFediverse`, `onFediverseFollow`, `onFediverseLike`, `onFediverseRepost`, `onFediverseQuote`, `onFediverseMention`, and `onFediverseReply`. These are internal plugin event subscriptions, not external HTTP webhooks. `fediverse.post` is a separate permission for publishing outbound posts.

### Chat message shape

`onChatMessage(msg)` and `filterChatMessage(msg)` receive a `ChatMessage`:
Expand Down Expand Up @@ -230,9 +240,17 @@ privately to whoever sent a message, pass `msg` (or `msg.clientId`) to
> [`owncast.chat.history()`](#chat-history), and in the dev server. It is
> `undefined` only for the rare message with no associated account.

### Fediverse engagement and raw activity

`onFediverseFollow` receives `{actor}`. `onFediverseLike`, `onFediverseRepost`, and `onFediverseQuote` receive `{actor, target}`. `actor` uses the SDK's `FediverseActor` shape. `target` is `{url: string}`. For a quote, it identifies the locally authored post that the remote actor quoted.

`onFediverse` receives the verified inbound ActivityPub activity after the host validates the HTTP signature and actor origin. In JavaScript, the payload is the raw JSON object. It fires in addition to a specialized handler when one applies. It also fires for verified activity types that have no specialized handler.

Python exposes the same hooks as `@plugin.on_fediverse`, `@plugin.on_fediverse_follow`, `@plugin.on_fediverse_like`, `@plugin.on_fediverse_repost`, `@plugin.on_fediverse_quote`, `@plugin.on_fediverse_mention`, and `@plugin.on_fediverse_reply`. Python notify payloads are non-subscriptable `_Obj` attribute views. Read normal fields as attributes, or use `.raw` for the underlying dictionary and keys that are not valid Python attributes, such as `payload.raw["@context"]`.

### Fediverse inbound posts

`onFediverseMention` and `onFediverseReply` carry a `FediverseInboundPost`, a post that someone on the fediverse made that's relevant to the streamer:
`onFediverseMention` and `onFediverseReply` carry a `FediverseInboundPost`. The host accepts these specialized events only from a verified public `Create(Note)` that either mentions the local Fediverse account or replies to a locally authored post:

```ts
interface FediverseInboundPost {
Expand Down Expand Up @@ -477,6 +495,7 @@ every chat message, the router first, then your handler.
| `videoconfig.read` | `owncast.videoConfig.read()`, read the output/transcoding config |
| `videoconfig.write` | `owncast.videoConfig.write()`, change video config, high-trust. Changes apply on the next stream start (the host does not restart a live stream). |
| `notifications.send` | `owncast.notifications.discord`, `.browserPush` |
| `fediverse.inbound` | Subscribe to `fediverse.activity`, `fediverse.follow`, `fediverse.like`, `fediverse.repost`, `fediverse.quote`, `fediverse.mention`, and `fediverse.reply` through `onFediverse` and the six specialized handlers. Required for any plugin that declares one of those handlers. |
| `fediverse.post` | `owncast.fediverse.post(text)`, high-trust (posts under the streamer's handle), admin should grant sparingly. |
| `ui.modify` | Place UI inside Owncast's own chrome. Required for `manifest.actions`, `manifest.styles`, `manifest.scripts`, and `manifest.extraPageContent`. |

Expand Down
7 changes: 7 additions & 0 deletions docs/WIRE_PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,13 @@ the gate re-validate a session on each `/` page load and return
- Not a custom host function. Gates the `filter_chat_message` export: a plugin that registers a `filterChatMessage` handler must declare this permission at load time, otherwise the host rejects the manifest.
- This is deliberately separate from `chat.send`, `chat.history`, and `chat.moderate`: filtering happens inline on every chat message before broadcast (modify the body, drop the message, or pass it through), so the manifest reviewer needs to see it called out explicitly.

### `fediverse.inbound`

- Not a custom host function. Gates notify subscriptions to `fediverse.activity`, `fediverse.follow`, `fediverse.like`, `fediverse.repost`, `fediverse.quote`, `fediverse.mention`, and `fediverse.reply`. If `register` reports any of these subscriptions, the plugin manifest must declare this permission or the host rejects the plugin at load time.
- `fediverse.activity` carries the verified inbound activity's raw JSON object after HTTP signature and actor-origin checks. It is dispatched in addition to any matching specialized event and is also dispatched for verified activity types with no specialized event.
- `fediverse.quote` carries `{actor, target}`, where `target` is the locally authored post that the remote actor quoted. `fediverse.mention` and `fediverse.reply` are limited to verified public `Create(Note)` activities that mention the local account or reply to a locally authored post.
- These are internal plugin notify events, not external HTTP webhooks. `fediverse.post` is separate and permits outbound posts under the streamer's Fediverse identity.

### ambient (no permission)

These imports are granted to every plugin without a declared permission. A plugin can't `setTimeout` or read its own config without the host, and the acts themselves are benign (a scheduled callback still needs its own permissions to do anything, and reading your own manifest-declared config exposes nothing new).
Expand Down
5 changes: 3 additions & 2 deletions examples/js/engagement-bot/INSTRUCTIONS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Engagement Bot

Connects Owncast events to your outside channels: it pings Discord and posts to the fediverse when you go live, browser-pushes on new fediverse followers, forwards fediverse mentions and replies to Discord, and quietly removes obvious chat spam.
Connects Owncast events to your outside channels: it pings Discord and posts to the fediverse when you go live, browser-pushes on new fediverse followers, forwards likes, reposts, quotes, mentions, replies, and raw inbound activities to Discord, and quietly removes obvious chat spam.

## Setup

Expand All @@ -15,11 +15,12 @@ Then enable the plugin in **Admin → Plugins**.

- **Stream start** → posts "Stream live: \<title\>" to Discord and "🔴 Going live: \<title\>" to the fediverse.
- **New fediverse follower** → sends a browser-push notification to subscribed viewers.
- **Fediverse mention or reply** → forwards a short snippet plus a link to Discord.
- **Fediverse activity** → forwards likes, reposts, quotes, mentions, replies, and raw verified activities to Discord.
- **Chat spam** → automatically deletes messages containing obvious spam phrases (e.g. "free money", "click here").

## Permissions

- **notifications.send**: Discord messages and browser-push notifications.
- **fediverse.inbound**: receiving all seven inbound event subscriptions, including raw verified activities and the six specialized handlers.
- **fediverse.post**: the go-live fediverse announcement.
- **chat.moderate**: deleting spam messages from chat.
4 changes: 2 additions & 2 deletions examples/js/engagement-bot/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# engagement-bot

A cross-platform notifier that connects Owncast events to the streamer's outside channels. When the stream goes live it pings Discord and posts a fediverse announcement. New fediverse followers trigger a browser-push notification. Mentions and replies on the fediverse get forwarded to Discord so the streamer sees them alongside their normal chatter. As a small side feature it also removes obvious chat spam.
A cross-platform notifier that connects Owncast events to the streamer's outside channels. When the stream goes live it pings Discord and posts a fediverse announcement. Fediverse follows trigger browser push. Likes, reposts, quotes, mentions, replies, and raw inbound activities get forwarded to Discord. As a small side feature it also removes obvious chat spam.

**Demonstrates:** `owncast.notifications.discord(text)` and `.browserPush(payload)` (`notifications.send`), `owncast.fediverse.post(text)` (`fediverse.post`), `owncast.chat.deleteMessage(id)` (`chat.moderate`), the typed fediverse handlers (`onFediverseFollow`, `onFediverseMention`, `onFediverseReply`), and the `onStreamStarted` lifecycle handler.
**Demonstrates:** `owncast.notifications.discord(text)` and `.browserPush(payload)` (`notifications.send`), `owncast.fediverse.post(text)` (`fediverse.post`), `owncast.chat.deleteMessage(id)` (`chat.moderate`), all seven `fediverse.inbound` handlers (`onFediverse`, `onFediverseFollow`, `onFediverseLike`, `onFediverseRepost`, `onFediverseQuote`, `onFediverseMention`, and `onFediverseReply`), and `onStreamStarted`.
87 changes: 87 additions & 0 deletions examples/js/engagement-bot/__tests__/mod.test.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,93 @@
]
}
},
{
"name": "fediverse like is forwarded to Discord",
"events": [
{
"event": "fediverse.like",
"payload": {
"actor": {
"name": "Bob",
"handle": "@bob@fediverse.example",
"url": "https://fediverse.example/@bob"
},
"target": {
"url": "https://demo.owncast.example/@demo/100"
}
}
}
],
"expect": {
"discordPosts": [
"like from @bob@fediverse.example: https://demo.owncast.example/@demo/100"
]
}
},
{
"name": "fediverse repost is forwarded to Discord",
"events": [
{
"event": "fediverse.repost",
"payload": {
"actor": {
"name": "Carol",
"handle": "@carol@example.social",
"url": "https://example.social/@carol"
},
"target": {
"url": "https://demo.owncast.example/@demo/101"
}
}
}
],
"expect": {
"discordPosts": [
"repost from @carol@example.social: https://demo.owncast.example/@demo/101"
]
}
},
{
"name": "fediverse quote is forwarded to Discord",
"events": [
{
"event": "fediverse.quote",
"payload": {
"actor": {
"name": "Dana",
"handle": "@dana@social.example",
"url": "https://social.example/@dana"
},
"target": {
"url": "https://demo.owncast.example/@demo/102"
}
}
}
],
"expect": {
"discordPosts": [
"quote from @dana@social.example: https://demo.owncast.example/@demo/102"
]
}
},
{
"name": "raw fediverse activity exposes arbitrary ActivityPub fields",
"events": [
{
"event": "fediverse.activity",
"payload": {
"type": "Announce",
"actor": "https://fediverse.example/users/erin",
"object": "https://demo.owncast.example/@demo/103"
}
}
],
"expect": {
"discordPosts": [
"activity Announce: https://fediverse.example/users/erin -> https://demo.owncast.example/@demo/103"
]
}
},
{
"name": "fediverse mention is forwarded to Discord with a snippet",
"events": [
Expand Down
3 changes: 2 additions & 1 deletion examples/js/engagement-bot/plugin.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"permissions": [
"chat.moderate",
"notifications.send",
"fediverse.post"
"fediverse.post",
"fediverse.inbound"
]
}
24 changes: 24 additions & 0 deletions examples/js/engagement-bot/src/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,30 @@ module.exports = definePlugin({
});
},

onFediverseLike(event) {
owncast.notifications.discord(
`like from ${event.actor.handle}: ${event.target.url}`,
);
},

onFediverseRepost(event) {
owncast.notifications.discord(
`repost from ${event.actor.handle}: ${event.target.url}`,
);
},

onFediverseQuote(event) {
owncast.notifications.discord(
`quote from ${event.actor.handle}: ${event.target.url}`,
);
},

onFediverse(activity) {
owncast.notifications.discord(
`activity ${activity.type}: ${activity.actor} -> ${activity.object}`,
);
},

// Mentions and replies carry content, so echo a short summary to Discord
// so the streamer sees off-platform engagement in their normal channel.
onFediverseMention(post) {
Expand Down
1 change: 1 addition & 0 deletions examples/js/fediverse-chat-bridge/INSTRUCTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ All remote text is HTML-escaped before display, so a malicious post can't inject

## Permissions

- **fediverse.inbound**: receiving mention and reply events. These are two of the seven gated subscriptions. This plugin does not handle quote or raw activity events.
- **chat.send**: posts the system messages under the plugin's bot identity.
2 changes: 1 addition & 1 deletion examples/js/fediverse-chat-bridge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

When someone on the fediverse mentions or replies to the streamer, posts a system message in chat showing the poster's avatar, display name, handle (linked to their profile), and the post text (linked to the original).

**Demonstrates:** the typed `onFediverseMention` / `onFediverseReply` handlers, `owncast.chat.system(html)` for posting an HTML system message (no user identity), and the discipline of escaping every untrusted field before inserting it into rendered HTML.
**Demonstrates:** the typed `onFediverseMention` / `onFediverseReply` handlers, `owncast.chat.system(html)` for posting an HTML system message (no user identity), and the discipline of escaping every untrusted field before inserting it into rendered HTML. The plugin subscribes to two of the seven events gated by `fediverse.inbound`. It does not handle follows, likes, reposts, quotes, or raw activities.
3 changes: 2 additions & 1 deletion examples/js/fediverse-chat-bridge/plugin.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"version": "0.3.1",
"description": "Surfaces inbound fediverse mentions and replies as system messages in chat, with the poster's avatar. This example was written in JavaScript.",
"permissions": [
"chat.send"
"chat.send",
"fediverse.inbound"
],
"bot": {
"displayName": "Example Fediverse"
Expand Down
5 changes: 3 additions & 2 deletions examples/python/engagement-bot/INSTRUCTIONS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Engagement Bot

Connects Owncast events to your outside channels: it pings Discord and posts to the fediverse when you go live, browser-pushes on new fediverse followers, forwards fediverse mentions and replies to Discord, and quietly removes obvious chat spam.
Connects Owncast events to your outside channels: it pings Discord and posts to the fediverse when you go live, browser-pushes on new fediverse followers, forwards likes, reposts, quotes, mentions, replies, and raw inbound activities to Discord, and quietly removes obvious chat spam.

## Setup

Expand All @@ -15,11 +15,12 @@ Then enable the plugin in **Admin → Plugins**.

- **Stream start** → posts "Stream live: \<title\>" to Discord and "🔴 Going live: \<title\>" to the fediverse.
- **New fediverse follower** → sends a browser-push notification to subscribed viewers.
- **Fediverse mention or reply** → forwards a short snippet plus a link to Discord.
- **Fediverse activity** → forwards likes, reposts, quotes, mentions, replies, and raw verified activities to Discord.
- **Chat spam** → automatically deletes messages containing obvious spam phrases (e.g. "free money", "click here").

## Permissions

- **notifications.send**: Discord messages and browser-push notifications.
- **fediverse.inbound**: receiving all seven inbound event subscriptions, including raw verified activities and the six specialized handlers.
- **fediverse.post**: the go-live fediverse announcement.
- **chat.moderate**: deleting spam messages from chat.
4 changes: 2 additions & 2 deletions examples/python/engagement-bot/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# engagement-bot

A cross-platform notifier that connects Owncast events to the streamer's outside channels. When the stream goes live it pings Discord and posts a fediverse announcement. New fediverse followers trigger a browser-push notification. Mentions and replies on the fediverse get forwarded to Discord so the streamer sees them alongside their normal chatter. As a small side feature it also removes obvious chat spam.
A cross-platform notifier that connects Owncast events to the streamer's outside channels. When the stream goes live it pings Discord and posts a fediverse announcement. Fediverse follows trigger browser push. Likes, reposts, quotes, mentions, replies, and raw inbound activities get forwarded to Discord. As a small side feature it also removes obvious chat spam.

**Demonstrates:** `owncast.notifications.discord(text)` and `.browser_push(payload)` (`notifications.send`), `owncast.fediverse.post(text)` (`fediverse.post`), `owncast.chat.delete_message(id)` (`chat.moderate`), the typed fediverse handlers (`@plugin.on_fediverse_follow`, `on_fediverse_mention`, `on_fediverse_reply`), and the `@plugin.on_stream_started` lifecycle handler.
**Demonstrates:** `owncast.notifications.discord(text)` and `.browser_push(payload)` (`notifications.send`), `owncast.fediverse.post(text)` (`fediverse.post`), `owncast.chat.delete_message(id)` (`chat.moderate`), all seven `fediverse.inbound` handlers (`@plugin.on_fediverse`, `@plugin.on_fediverse_follow`, `@plugin.on_fediverse_like`, `@plugin.on_fediverse_repost`, `@plugin.on_fediverse_quote`, `@plugin.on_fediverse_mention`, and `@plugin.on_fediverse_reply`), and `@plugin.on_stream_started`.

The plugin uses the integrations the server is already configured with: Discord / browser-push go through the host's notification channels, and `fediverse.post` uses the server's federated account. None of them are configured by the plugin.
Loading
Loading