@owncast/plugin-sdk 0.6.0 → 0.11.0
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.
- package/README.md +6 -6
- package/bin/owncast-plugin.js +53 -36
- package/index.d.ts +331 -107
- package/index.js +327 -320
- package/package.json +2 -1
- package/scripts/postinstall.js +5 -5
- package/slug.js +26 -0
- package/testing.js +15 -48
package/index.js
CHANGED
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
// @owncast/plugin-sdk runtime, bundled into every plugin.
|
|
2
2
|
//
|
|
3
3
|
// Authors define typed handlers (onChatMessage, filterChatMessage, ...) plus
|
|
4
|
-
// an `on: { [
|
|
4
|
+
// an `on: { [localCustomHook]: handler }` object for plugin-owned events. The
|
|
5
5
|
// SDK derives the manifest's subscriptions from which handlers are present
|
|
6
|
-
// and returns them via register()
|
|
6
|
+
// and returns them via register(). The host qualifies custom hooks with the
|
|
7
|
+
// declaring plugin's slug.
|
|
7
8
|
|
|
8
9
|
let registered = null;
|
|
9
10
|
|
|
10
|
-
// Command
|
|
11
|
-
// register() so it can build a unified `!help` across all plugins. One entry
|
|
12
|
-
// per command: { name, prefix, description, usage, aliases, modOnly }.
|
|
11
|
+
// Command registrations used for matching, dispatch, and the unified `!help`.
|
|
13
12
|
const commandManifest = [];
|
|
14
13
|
|
|
15
14
|
// Host-driven timers. The sandbox has no setTimeout, so owncast.timer.* asks
|
|
16
15
|
// the host to schedule a callback and call back via the internal "timer.fire"
|
|
17
16
|
// event. The author's callback stays here in the long-lived instance, keyed by
|
|
18
17
|
// a guest-allocated id the host echoes back. State persists across calls
|
|
19
|
-
// because the plugin instance is reused
|
|
18
|
+
// because the plugin instance is reused. Timers are dropped on reload.
|
|
20
19
|
let nextTimerId = 1;
|
|
21
20
|
const timerCallbacks = new Map(); // id -> { fn, repeat }
|
|
22
21
|
|
|
@@ -43,13 +42,22 @@ const Events = Object.freeze({
|
|
|
43
42
|
// Once-a-second tick for periodic work (opt in by defining onTick)
|
|
44
43
|
Tick: "tick",
|
|
45
44
|
// Fediverse, engagement (metadata only) + inbound posts (with content)
|
|
45
|
+
FediverseActivity: "fediverse.activity",
|
|
46
46
|
FediverseFollow: "fediverse.follow",
|
|
47
47
|
FediverseLike: "fediverse.like",
|
|
48
48
|
FediverseRepost: "fediverse.repost",
|
|
49
|
+
FediverseQuote: "fediverse.quote",
|
|
49
50
|
FediverseMention: "fediverse.mention",
|
|
50
51
|
FediverseReply: "fediverse.reply",
|
|
51
52
|
});
|
|
52
53
|
|
|
54
|
+
const InternalEvents = Object.freeze({
|
|
55
|
+
ChatCommand: "chat.command",
|
|
56
|
+
TimerFire: "timer.fire",
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const DefaultCommandPrefix = "!";
|
|
60
|
+
|
|
53
61
|
const Permissions = Object.freeze({
|
|
54
62
|
ChatSend: "chat.send",
|
|
55
63
|
ChatHistory: "chat.history",
|
|
@@ -58,6 +66,7 @@ const Permissions = Object.freeze({
|
|
|
58
66
|
StorageKV: "storage.kv",
|
|
59
67
|
StorageUpload: "storage.upload",
|
|
60
68
|
StorageFS: "storage.fs",
|
|
69
|
+
StorageSQL: "storage.sql",
|
|
61
70
|
EventsEmit: "events.emit",
|
|
62
71
|
NetworkFetch: "network.fetch",
|
|
63
72
|
HttpServe: "http.serve",
|
|
@@ -65,7 +74,10 @@ const Permissions = Object.freeze({
|
|
|
65
74
|
NotificationsSend: "notifications.send",
|
|
66
75
|
UsersRead: "users.read",
|
|
67
76
|
UsersModerate: "users.moderate",
|
|
77
|
+
UsersRegister: "users.register",
|
|
78
|
+
AuthGate: "auth.gate",
|
|
68
79
|
FediversePost: "fediverse.post",
|
|
80
|
+
FediverseInbound: "fediverse.inbound",
|
|
69
81
|
HttpSSE: "http.sse",
|
|
70
82
|
VideoConfigRead: "videoconfig.read",
|
|
71
83
|
VideoConfigWrite: "videoconfig.write",
|
|
@@ -84,6 +96,30 @@ const filter = Object.freeze({
|
|
|
84
96
|
},
|
|
85
97
|
});
|
|
86
98
|
|
|
99
|
+
// Verdict helpers for onAuthCheck (the optional re-validation hook the host runs
|
|
100
|
+
// on a viewer's page load). `ok` keeps the session, `refresh` keeps it and
|
|
101
|
+
// extends the cookie (optional `ttl` seconds), and `deny` ends it and bounces
|
|
102
|
+
// the viewer back to the login screen.
|
|
103
|
+
const authCheck = Object.freeze({
|
|
104
|
+
ok() {
|
|
105
|
+
return { action: "ok" };
|
|
106
|
+
},
|
|
107
|
+
refresh(opts) {
|
|
108
|
+
return { action: "refresh", ...(opts || {}) };
|
|
109
|
+
},
|
|
110
|
+
deny(reason) {
|
|
111
|
+
return { action: "deny", reason: reason || "" };
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// dispatchAuthCheck routes the host's re-validation call to the author's
|
|
116
|
+
// onAuthCheck handler. No handler → always "ok" (the hook is optional, and a
|
|
117
|
+
// plugin that doesn't implement it simply never revokes mid-session).
|
|
118
|
+
function dispatchAuthCheck(req) {
|
|
119
|
+
if (!registered || !isFn(registered.onAuthCheck)) return { action: "ok" };
|
|
120
|
+
return registered.onAuthCheck(req) || { action: "ok" };
|
|
121
|
+
}
|
|
122
|
+
|
|
87
123
|
// Distinguishes notification handlers from filter handlers in the HANDLERS
|
|
88
124
|
// map below. Internal, not part of the public API.
|
|
89
125
|
const HandlerKind = Object.freeze({
|
|
@@ -126,6 +162,11 @@ const HANDLERS = Object.freeze({
|
|
|
126
162
|
onSseDisconnect: { event: Events.SseDisconnect, kind: HandlerKind.Notify },
|
|
127
163
|
// Once-a-second tick
|
|
128
164
|
onTick: { event: Events.Tick, kind: HandlerKind.Notify },
|
|
165
|
+
// Verified inbound ActivityPub activity (raw JSON object)
|
|
166
|
+
onFediverse: {
|
|
167
|
+
event: Events.FediverseActivity,
|
|
168
|
+
kind: HandlerKind.Notify,
|
|
169
|
+
},
|
|
129
170
|
// Fediverse engagement (actor + target metadata)
|
|
130
171
|
onFediverseFollow: {
|
|
131
172
|
event: Events.FediverseFollow,
|
|
@@ -136,6 +177,10 @@ const HANDLERS = Object.freeze({
|
|
|
136
177
|
event: Events.FediverseRepost,
|
|
137
178
|
kind: HandlerKind.Notify,
|
|
138
179
|
},
|
|
180
|
+
onFediverseQuote: {
|
|
181
|
+
event: Events.FediverseQuote,
|
|
182
|
+
kind: HandlerKind.Notify,
|
|
183
|
+
},
|
|
139
184
|
// Fediverse inbound posts (with content)
|
|
140
185
|
onFediverseMention: {
|
|
141
186
|
event: Events.FediverseMention,
|
|
@@ -144,156 +189,70 @@ const HANDLERS = Object.freeze({
|
|
|
144
189
|
onFediverseReply: { event: Events.FediverseReply, kind: HandlerKind.Notify },
|
|
145
190
|
});
|
|
146
191
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const JsType = Object.freeze({
|
|
150
|
-
Function: "function",
|
|
151
|
-
Object: "object",
|
|
152
|
-
});
|
|
153
|
-
const isFn = (x) => typeof x === JsType.Function;
|
|
154
|
-
const isObj = (x) => x !== null && typeof x === JsType.Object;
|
|
192
|
+
const isFn = (x) => typeof x === "function";
|
|
193
|
+
const isObj = (x) => x !== null && typeof x === "object";
|
|
155
194
|
|
|
156
195
|
function definePlugin(def) {
|
|
157
|
-
// `commands` is declarative sugar: give definePlugin a command table (and an
|
|
158
|
-
// optional commandPrefix) and the SDK wires the chat subscription for you —
|
|
159
|
-
// no onChatMessage needed. It expands into an onChatMessage handler here, so
|
|
160
|
-
// subscription derivation and dispatch treat it like any chat handler. If you
|
|
161
|
-
// also pass onChatMessage, the command router runs first, then your handler.
|
|
162
|
-
// For advanced composition (e.g. dropping command messages from chat via a
|
|
163
|
-
// filter), use the lower-level defineCommands() router directly instead.
|
|
164
|
-
if (def && isObj(def.commands)) {
|
|
165
|
-
const router = defineCommands({
|
|
166
|
-
prefix: def.commandPrefix,
|
|
167
|
-
caseSensitive: def.commandsCaseSensitive,
|
|
168
|
-
commands: def.commands,
|
|
169
|
-
onUnknown: def.onUnknownCommand,
|
|
170
|
-
});
|
|
171
|
-
const userHandler = def.onChatMessage;
|
|
172
|
-
def.onChatMessage = isFn(userHandler)
|
|
173
|
-
? (msg) => {
|
|
174
|
-
router(msg);
|
|
175
|
-
userHandler(msg);
|
|
176
|
-
}
|
|
177
|
-
: router;
|
|
178
|
-
}
|
|
179
196
|
registered = def;
|
|
180
|
-
|
|
181
|
-
|
|
197
|
+
commandManifest.length = 0;
|
|
198
|
+
if (!def || !isObj(def.commands)) return def;
|
|
182
199
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
// top-level onCooldown(ctx) / onDenied(ctx), and a top-level onUnknown(ctx).
|
|
207
|
-
function defineCommands(config) {
|
|
208
|
-
config = config || {};
|
|
209
|
-
const prefix = config.prefix || "!";
|
|
210
|
-
const caseSensitive = !!config.caseSensitive;
|
|
211
|
-
const norm = (s) => (caseSensitive ? s : s.toLowerCase());
|
|
212
|
-
|
|
213
|
-
// Resolve every name and alias to its canonical command definition, and
|
|
214
|
-
// record metadata so the host can build a unified `!help` (see
|
|
215
|
-
// describeCommands). Metadata is reported via register(); it never affects
|
|
216
|
-
// routing.
|
|
217
|
-
const table = new Map();
|
|
218
|
-
const defs = config.commands || {};
|
|
219
|
-
for (const name of Object.keys(defs)) {
|
|
220
|
-
const def = defs[name];
|
|
221
|
-
table.set(norm(name), { name, def });
|
|
222
|
-
for (const alias of def.aliases || []) table.set(norm(alias), { name, def });
|
|
200
|
+
const prefix =
|
|
201
|
+
def.commandPrefix == null ? DefaultCommandPrefix : def.commandPrefix;
|
|
202
|
+
if (typeof prefix !== "string" || prefix.length === 0) {
|
|
203
|
+
throw new TypeError("commandPrefix must be a non-empty string");
|
|
204
|
+
}
|
|
205
|
+
const caseSensitive = !!def.commandsCaseSensitive;
|
|
206
|
+
for (const name of Object.keys(def.commands)) {
|
|
207
|
+
const command = def.commands[name];
|
|
208
|
+
if (!isObj(command)) {
|
|
209
|
+
throw new TypeError(`command "${name}" must be an object`);
|
|
210
|
+
}
|
|
211
|
+
const aliases = command.aliases == null ? [] : command.aliases;
|
|
212
|
+
if (
|
|
213
|
+
!Array.isArray(aliases) ||
|
|
214
|
+
!aliases.every((alias) => typeof alias === "string")
|
|
215
|
+
) {
|
|
216
|
+
throw new TypeError(`command "${name}" aliases must be an array of strings`);
|
|
217
|
+
}
|
|
218
|
+
const cooldownMs =
|
|
219
|
+
command.cooldownMs == null ? 0 : command.cooldownMs;
|
|
220
|
+
if (!Number.isSafeInteger(cooldownMs) || cooldownMs < 0) {
|
|
221
|
+
throw new TypeError(`command "${name}" cooldownMs must be a non-negative integer`);
|
|
222
|
+
}
|
|
223
223
|
commandManifest.push({
|
|
224
224
|
name,
|
|
225
225
|
prefix,
|
|
226
|
-
description:
|
|
227
|
-
usage:
|
|
228
|
-
aliases
|
|
229
|
-
modOnly: !!
|
|
226
|
+
description: command.description || "",
|
|
227
|
+
usage: command.usage || "",
|
|
228
|
+
aliases,
|
|
229
|
+
modOnly: !!command.modOnly,
|
|
230
|
+
caseSensitive,
|
|
231
|
+
cooldownMs,
|
|
230
232
|
});
|
|
231
233
|
}
|
|
234
|
+
return def;
|
|
235
|
+
}
|
|
232
236
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (!body.startsWith(prefix)) return false;
|
|
239
|
-
const rest = body.slice(prefix.length);
|
|
240
|
-
const parts = rest.split(/\s+/).filter(Boolean);
|
|
241
|
-
if (parts.length === 0) return false;
|
|
242
|
-
const called = parts[0];
|
|
243
|
-
const args = parts.slice(1);
|
|
244
|
-
const argString = rest.slice(called.length).trim();
|
|
245
|
-
|
|
246
|
-
const ctx = {
|
|
247
|
-
msg,
|
|
248
|
-
user: msg && msg.user,
|
|
249
|
-
command: norm(called),
|
|
250
|
-
args,
|
|
251
|
-
argString,
|
|
252
|
-
reply: (text) => owncast.chat.send(text),
|
|
253
|
-
replyPrivately: (text) => {
|
|
254
|
-
if (!owncast.chat.replyTo(msg, text)) owncast.chat.send(text);
|
|
255
|
-
},
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
const entry = table.get(norm(called));
|
|
259
|
-
if (!entry) {
|
|
260
|
-
if (isFn(config.onUnknown)) config.onUnknown(ctx);
|
|
261
|
-
return false;
|
|
262
|
-
}
|
|
263
|
-
const { name, def } = entry;
|
|
264
|
-
ctx.command = name;
|
|
265
|
-
|
|
266
|
-
// Moderator gating: the sender's scopes must include MODERATOR.
|
|
267
|
-
if (def.modOnly) {
|
|
268
|
-
const scopes = (msg.user && msg.user.scopes) || [];
|
|
269
|
-
if (!scopes.includes("MODERATOR")) {
|
|
270
|
-
if (isFn(def.onDenied)) def.onDenied(ctx);
|
|
271
|
-
else if (isFn(config.onDenied)) config.onDenied(ctx);
|
|
272
|
-
return true; // matched a command, but the caller wasn't allowed
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Per-user cooldown, clocked off msg.timestamp so it's deterministic in
|
|
277
|
-
// tests and independent of any sandbox clock quirks.
|
|
278
|
-
const cooldownMs = def.cooldownMs || 0;
|
|
279
|
-
if (cooldownMs > 0) {
|
|
280
|
-
const userId =
|
|
281
|
-
(msg.user && msg.user.id) ||
|
|
282
|
-
(msg.clientId != null ? `c${msg.clientId}` : "anon");
|
|
283
|
-
const key = `${name}${userId}`;
|
|
284
|
-
const now = msg.timestamp ? new Date(msg.timestamp).getTime() : 0;
|
|
285
|
-
const prev = lastRun.get(key);
|
|
286
|
-
if (now && prev && now - prev < cooldownMs) {
|
|
287
|
-
if (isFn(def.onCooldown)) def.onCooldown(ctx);
|
|
288
|
-
else if (isFn(config.onCooldown)) config.onCooldown(ctx);
|
|
289
|
-
return true;
|
|
290
|
-
}
|
|
291
|
-
if (now) lastRun.set(key, now);
|
|
292
|
-
}
|
|
237
|
+
function dispatchCommand(event) {
|
|
238
|
+
if (!isObj(event)) return;
|
|
239
|
+
if (!registered || !isObj(registered.commands)) return;
|
|
240
|
+
const command = registered.commands[event.command];
|
|
241
|
+
if (!command || !isFn(command.run)) return;
|
|
293
242
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
243
|
+
const msg = event.message;
|
|
244
|
+
command.run({
|
|
245
|
+
msg,
|
|
246
|
+
user: msg && msg.user,
|
|
247
|
+
command: event.command,
|
|
248
|
+
invokedAs: event.invokedAs || event.command,
|
|
249
|
+
args: event.args || [],
|
|
250
|
+
argString: event.argString || "",
|
|
251
|
+
reply: (text) => owncast.chat.send(text),
|
|
252
|
+
replyPrivately: (text) => {
|
|
253
|
+
if (!owncast.chat.replyTo(msg, text)) owncast.chat.send(text);
|
|
254
|
+
},
|
|
255
|
+
});
|
|
297
256
|
}
|
|
298
257
|
|
|
299
258
|
// Used by the build-generated entry to compute subscriptions for register().
|
|
@@ -324,9 +283,8 @@ function describeSubscriptions() {
|
|
|
324
283
|
return { notify, filter: filterSubs };
|
|
325
284
|
}
|
|
326
285
|
|
|
327
|
-
// Used by the build-generated entry to report
|
|
328
|
-
//
|
|
329
|
-
// declares no commands.
|
|
286
|
+
// Used by the build-generated entry to report command registrations to the
|
|
287
|
+
// host for matching, dispatch, and the unified `!help`.
|
|
330
288
|
function describeCommands() {
|
|
331
289
|
return commandManifest;
|
|
332
290
|
}
|
|
@@ -336,7 +294,7 @@ function dispatchEvent(envelope) {
|
|
|
336
294
|
// Internal: a host-scheduled timer elapsed. Run the author's callback,
|
|
337
295
|
// dropping one-shot entries first so a throw still cleans up. Not routed to
|
|
338
296
|
// user handlers or the `on` map.
|
|
339
|
-
if (eventType ===
|
|
297
|
+
if (eventType === InternalEvents.TimerFire) {
|
|
340
298
|
const id = payload && payload.id;
|
|
341
299
|
const entry = timerCallbacks.get(id);
|
|
342
300
|
if (entry) {
|
|
@@ -345,6 +303,10 @@ function dispatchEvent(envelope) {
|
|
|
345
303
|
}
|
|
346
304
|
return;
|
|
347
305
|
}
|
|
306
|
+
if (eventType === InternalEvents.ChatCommand) {
|
|
307
|
+
dispatchCommand(payload);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
348
310
|
if (!registered) return;
|
|
349
311
|
for (const [method, info] of Object.entries(HANDLERS)) {
|
|
350
312
|
if (
|
|
@@ -391,16 +353,6 @@ function dispatchHttp(request) {
|
|
|
391
353
|
};
|
|
392
354
|
}
|
|
393
355
|
|
|
394
|
-
// permError builds an actionable Error and logs it to stderr (which the
|
|
395
|
-
// host runtime captures), so a plugin author running `owncast-plugin
|
|
396
|
-
// serve` or hitting the host's logs sees exactly which permission to
|
|
397
|
-
// add to their manifest. apiName is the SDK call the author wrote
|
|
398
|
-
// (e.g. "owncast.actions.set"); perm is the manifest permission string.
|
|
399
|
-
function permError(apiName, perm) {
|
|
400
|
-
const msg = `${apiName} requires the '${perm}' permission. Add it to your plugin.manifest.json's "permissions" array.`;
|
|
401
|
-
console.error(`[owncast-plugin] ${msg}`);
|
|
402
|
-
return new Error(msg);
|
|
403
|
-
}
|
|
404
356
|
|
|
405
357
|
// scheduleTimer registers a callback and asks the host to schedule it. The id
|
|
406
358
|
// is guest-allocated and echoed back on "timer.fire". Throws if the host
|
|
@@ -423,50 +375,110 @@ function scheduleTimer(fn, ms, repeat) {
|
|
|
423
375
|
return id;
|
|
424
376
|
}
|
|
425
377
|
|
|
378
|
+
// hostFns returns the complete host import table. Missing imports indicate an
|
|
379
|
+
// incompatible host. Permission denials are reported by result-returning calls.
|
|
380
|
+
function hostFns(name, perm) {
|
|
381
|
+
const fns = Host.getFunctions();
|
|
382
|
+
if (!fns[name]) throw new Error(`permission '${perm}' not granted`);
|
|
383
|
+
return fns;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function operationResult(offset, failureMessage) {
|
|
387
|
+
if (offset == 0) return { error: failureMessage };
|
|
388
|
+
try {
|
|
389
|
+
const result = JSON.parse(Memory.find(offset).readString());
|
|
390
|
+
if (result === null || typeof result !== "object" || Array.isArray(result)) {
|
|
391
|
+
return { error: failureMessage };
|
|
392
|
+
}
|
|
393
|
+
return result;
|
|
394
|
+
} catch {
|
|
395
|
+
return { error: failureMessage };
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function requireOperationResult(offset, failureMessage) {
|
|
400
|
+
const result = operationResult(offset, failureMessage);
|
|
401
|
+
if (Object.prototype.hasOwnProperty.call(result, "error")) {
|
|
402
|
+
throw new Error(result.error || failureMessage);
|
|
403
|
+
}
|
|
404
|
+
return result;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function sqlResult(offset) {
|
|
408
|
+
return requireOperationResult(offset, "SQL host call failed");
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function sqlRows(result) {
|
|
412
|
+
if (!Array.isArray(result.columns) || !Array.isArray(result.rows)) {
|
|
413
|
+
throw new Error("SQL host returned an invalid result");
|
|
414
|
+
}
|
|
415
|
+
return result.rows.map((values) => {
|
|
416
|
+
if (!Array.isArray(values)) {
|
|
417
|
+
throw new Error("SQL host returned an invalid result");
|
|
418
|
+
}
|
|
419
|
+
return Object.fromEntries(result.columns.map((column, i) => [column, values[i]]));
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function sqlQuery(sql, params, maxRows) {
|
|
424
|
+
const fns = hostFns("owncast_sql_query", Permissions.StorageSQL);
|
|
425
|
+
const payload = { sql: String(sql), params: Array.from(params || []) };
|
|
426
|
+
if (maxRows) payload.maxRows = maxRows;
|
|
427
|
+
const request = Memory.fromString(JSON.stringify(payload));
|
|
428
|
+
return sqlResult(fns.owncast_sql_query(request.offset));
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function logToHost(name, message) {
|
|
432
|
+
const fn = Host.getFunctions()[name];
|
|
433
|
+
if (!fn) throw new Error("owncast.log is unavailable in this host");
|
|
434
|
+
fn(Memory.fromString(String(message)).offset);
|
|
435
|
+
}
|
|
436
|
+
|
|
426
437
|
const owncast = {
|
|
438
|
+
log: {
|
|
439
|
+
info(message) {
|
|
440
|
+
logToHost("owncast_log_info", message);
|
|
441
|
+
},
|
|
442
|
+
warning(message) {
|
|
443
|
+
logToHost("owncast_log_warning", message);
|
|
444
|
+
},
|
|
445
|
+
error(message) {
|
|
446
|
+
logToHost("owncast_log_error", message);
|
|
447
|
+
},
|
|
448
|
+
},
|
|
427
449
|
chat: {
|
|
428
450
|
send(text) {
|
|
429
|
-
const fns =
|
|
430
|
-
if (!fns.owncast_send_chat)
|
|
431
|
-
throw new Error(`permission '${Permissions.ChatSend}' not granted`);
|
|
451
|
+
const fns = hostFns("owncast_send_chat", Permissions.ChatSend);
|
|
432
452
|
fns.owncast_send_chat(Memory.fromString(text).offset);
|
|
433
453
|
},
|
|
434
454
|
sendAction(text) {
|
|
435
|
-
const fns =
|
|
436
|
-
if (!fns.owncast_send_chat_action)
|
|
437
|
-
throw new Error(`permission '${Permissions.ChatSend}' not granted`);
|
|
455
|
+
const fns = hostFns("owncast_send_chat_action", Permissions.ChatSend);
|
|
438
456
|
fns.owncast_send_chat_action(Memory.fromString(text).offset);
|
|
439
457
|
},
|
|
440
458
|
system(body) {
|
|
441
|
-
const fns =
|
|
442
|
-
if (!fns.owncast_send_chat_system)
|
|
443
|
-
throw new Error(`permission '${Permissions.ChatSend}' not granted`);
|
|
459
|
+
const fns = hostFns("owncast_send_chat_system", Permissions.ChatSend);
|
|
444
460
|
fns.owncast_send_chat_system(Memory.fromString(body).offset);
|
|
445
461
|
},
|
|
446
462
|
history(limit) {
|
|
447
|
-
const fns =
|
|
448
|
-
if (!fns.owncast_chat_history)
|
|
449
|
-
throw new Error(`permission '${Permissions.ChatHistory}' not granted`);
|
|
463
|
+
const fns = hostFns("owncast_chat_history", Permissions.ChatHistory);
|
|
450
464
|
const offset = fns.owncast_chat_history(limit || 0);
|
|
451
465
|
if (offset == 0) return [];
|
|
452
466
|
return JSON.parse(Memory.find(offset).readString());
|
|
453
467
|
},
|
|
454
468
|
deleteMessage(messageId) {
|
|
455
|
-
const fns =
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
469
|
+
const fns = hostFns("owncast_delete_message", Permissions.ChatModerate);
|
|
470
|
+
const offset = fns.owncast_delete_message(
|
|
471
|
+
Memory.fromString(String(messageId)).offset,
|
|
472
|
+
);
|
|
473
|
+
requireOperationResult(offset, "chat.deleteMessage failed");
|
|
459
474
|
},
|
|
460
475
|
kick(clientId) {
|
|
461
|
-
const fns =
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
fns.owncast_kick_client(BigInt(clientId));
|
|
476
|
+
const fns = hostFns("owncast_kick_client", Permissions.ChatModerate);
|
|
477
|
+
const offset = fns.owncast_kick_client(BigInt(clientId));
|
|
478
|
+
requireOperationResult(offset, "chat.kick failed");
|
|
465
479
|
},
|
|
466
480
|
sendTo(clientId, text) {
|
|
467
|
-
const fns =
|
|
468
|
-
if (!fns.owncast_send_chat_to)
|
|
469
|
-
throw new Error(`permission '${Permissions.ChatSend}' not granted`);
|
|
481
|
+
const fns = hostFns("owncast_send_chat_to", Permissions.ChatSend);
|
|
470
482
|
fns.owncast_send_chat_to(
|
|
471
483
|
BigInt(clientId),
|
|
472
484
|
Memory.fromString(text).offset,
|
|
@@ -487,9 +499,7 @@ const owncast = {
|
|
|
487
499
|
return true;
|
|
488
500
|
},
|
|
489
501
|
clients() {
|
|
490
|
-
const fns =
|
|
491
|
-
if (!fns.owncast_chat_clients)
|
|
492
|
-
throw new Error(`permission '${Permissions.ChatHistory}' not granted`);
|
|
502
|
+
const fns = hostFns("owncast_chat_clients", Permissions.ChatHistory);
|
|
493
503
|
const offset = fns.owncast_chat_clients();
|
|
494
504
|
if (offset == 0) return [];
|
|
495
505
|
return JSON.parse(Memory.find(offset).readString());
|
|
@@ -497,49 +507,79 @@ const owncast = {
|
|
|
497
507
|
},
|
|
498
508
|
users: {
|
|
499
509
|
list() {
|
|
500
|
-
const fns =
|
|
501
|
-
if (!fns.owncast_users_list)
|
|
502
|
-
throw new Error(`permission '${Permissions.UsersRead}' not granted`);
|
|
510
|
+
const fns = hostFns("owncast_users_list", Permissions.UsersRead);
|
|
503
511
|
const offset = fns.owncast_users_list();
|
|
504
512
|
if (offset == 0) return [];
|
|
505
513
|
return JSON.parse(Memory.find(offset).readString());
|
|
506
514
|
},
|
|
507
515
|
get(id) {
|
|
508
|
-
const fns =
|
|
509
|
-
if (!fns.owncast_user_get)
|
|
510
|
-
throw new Error(`permission '${Permissions.UsersRead}' not granted`);
|
|
516
|
+
const fns = hostFns("owncast_user_get", Permissions.UsersRead);
|
|
511
517
|
const offset = fns.owncast_user_get(Memory.fromString(id).offset);
|
|
512
518
|
if (offset == 0) return null;
|
|
513
519
|
return JSON.parse(Memory.find(offset).readString());
|
|
514
520
|
},
|
|
515
521
|
setEnabled(id, enabled, reason) {
|
|
516
|
-
const fns =
|
|
517
|
-
|
|
518
|
-
throw new Error(
|
|
519
|
-
`permission '${Permissions.UsersModerate}' not granted`,
|
|
520
|
-
);
|
|
521
|
-
fns.owncast_user_set_enabled(
|
|
522
|
+
const fns = hostFns("owncast_user_set_enabled", Permissions.UsersModerate);
|
|
523
|
+
const offset = fns.owncast_user_set_enabled(
|
|
522
524
|
Memory.fromString(id).offset,
|
|
523
525
|
enabled ? 1 : 0,
|
|
524
526
|
Memory.fromString(reason || "").offset,
|
|
525
527
|
);
|
|
528
|
+
requireOperationResult(offset, "users.setEnabled failed");
|
|
526
529
|
},
|
|
527
530
|
banIP(ip) {
|
|
528
|
-
const fns =
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
531
|
+
const fns = hostFns("owncast_ban_ip", Permissions.UsersModerate);
|
|
532
|
+
const offset = fns.owncast_ban_ip(Memory.fromString(ip).offset);
|
|
533
|
+
requireOperationResult(offset, "users.banIP failed");
|
|
534
|
+
},
|
|
535
|
+
// Find or create an authenticated Owncast user for an external identity.
|
|
536
|
+
// profileUrl and handle describe a verified profile. public opts that
|
|
537
|
+
// identity into public display. Returns { userId }. Throws on host error.
|
|
538
|
+
// Requires `users.register`.
|
|
539
|
+
register(opts) {
|
|
540
|
+
const fns = hostFns("owncast_users_register", Permissions.UsersRegister);
|
|
541
|
+
const source = typeof opts === "string" ? { authId: opts } : opts || {};
|
|
542
|
+
const req = {
|
|
543
|
+
authId: source.authId,
|
|
544
|
+
displayName: source.displayName,
|
|
545
|
+
scopes: source.scopes,
|
|
546
|
+
profileUrl: source.profileUrl,
|
|
547
|
+
handle: source.handle,
|
|
548
|
+
public: source.public,
|
|
549
|
+
};
|
|
550
|
+
const offset = fns.owncast_users_register(
|
|
551
|
+
Memory.fromString(JSON.stringify(req)).offset,
|
|
552
|
+
);
|
|
553
|
+
return requireOperationResult(offset, "users.register failed"); // { userId }
|
|
554
|
+
},
|
|
555
|
+
},
|
|
556
|
+
// Viewer-authentication gate. Only a plugin holding `auth.gate` (and enabled by
|
|
557
|
+
// an admin) can issue sessions, and these are valid only inside onHttpRequest,
|
|
558
|
+
// where the host attaches/clears the signed session cookie on the response.
|
|
559
|
+
// The admin selects the cumulative, host-owned access mode. Plugins cannot
|
|
560
|
+
// read or change it.
|
|
561
|
+
auth: {
|
|
562
|
+
// Issue a gate session for an already-registered user (see users.register).
|
|
563
|
+
// `ttl` is optional seconds, and 0/omitted uses the host default. Throws on
|
|
564
|
+
// host error. Requires `auth.gate`.
|
|
565
|
+
grantSession(opts) {
|
|
566
|
+
const fns = hostFns("owncast_auth_grant_session", Permissions.AuthGate);
|
|
567
|
+
const req = typeof opts === "string" ? { userId: opts } : opts || {};
|
|
568
|
+
const offset = fns.owncast_auth_grant_session(
|
|
569
|
+
Memory.fromString(JSON.stringify(req)).offset,
|
|
570
|
+
);
|
|
571
|
+
requireOperationResult(offset, "auth.grantSession failed");
|
|
572
|
+
},
|
|
573
|
+
// Clear the current viewer's gate session (logout). The plugin still owns the
|
|
574
|
+
// response/redirect. Requires `auth.gate`.
|
|
575
|
+
endSession() {
|
|
576
|
+
const fns = hostFns("owncast_auth_end_session", Permissions.AuthGate);
|
|
577
|
+
fns.owncast_auth_end_session();
|
|
534
578
|
},
|
|
535
579
|
},
|
|
536
580
|
storage: {
|
|
537
581
|
upload(name, data) {
|
|
538
|
-
const fns =
|
|
539
|
-
if (!fns.owncast_storage_upload)
|
|
540
|
-
throw new Error(
|
|
541
|
-
`permission '${Permissions.StorageUpload}' not granted`,
|
|
542
|
-
);
|
|
582
|
+
const fns = hostFns("owncast_storage_upload", Permissions.StorageUpload);
|
|
543
583
|
const dataMem =
|
|
544
584
|
data instanceof Uint8Array
|
|
545
585
|
? Memory.fromBuffer(
|
|
@@ -557,7 +597,7 @@ const owncast = {
|
|
|
557
597
|
return JSON.parse(Memory.find(offset).readString());
|
|
558
598
|
},
|
|
559
599
|
},
|
|
560
|
-
// Private, sandboxed filesystem under data/plugin-
|
|
600
|
+
// Private, sandboxed filesystem under data/plugin-storage/<slug>/files/. Unlike
|
|
561
601
|
// storage.upload (which publishes browser-accessible files), these bytes
|
|
562
602
|
// stay server-side. The host confines every path to this plugin's own
|
|
563
603
|
// directory. All methods require the 'storage.fs' permission.
|
|
@@ -565,9 +605,7 @@ const owncast = {
|
|
|
565
605
|
// Read a file's raw bytes. Returns a Uint8Array, or null if the file
|
|
566
606
|
// doesn't exist (or can't be read).
|
|
567
607
|
read(path) {
|
|
568
|
-
const fns =
|
|
569
|
-
if (!fns.owncast_fs_read)
|
|
570
|
-
throw new Error(`permission '${Permissions.StorageFS}' not granted`);
|
|
608
|
+
const fns = hostFns("owncast_fs_read", Permissions.StorageFS);
|
|
571
609
|
const offset = fns.owncast_fs_read(Memory.fromString(path).offset);
|
|
572
610
|
if (offset == 0) return null;
|
|
573
611
|
return new Uint8Array(Memory.find(offset).readBytes());
|
|
@@ -575,19 +613,15 @@ const owncast = {
|
|
|
575
613
|
// Read a file as UTF-8 text. Returns a string, or null if the file
|
|
576
614
|
// doesn't exist. (The Extism boundary decodes the bytes as UTF-8.)
|
|
577
615
|
readText(path) {
|
|
578
|
-
const fns =
|
|
579
|
-
if (!fns.owncast_fs_read)
|
|
580
|
-
throw new Error(`permission '${Permissions.StorageFS}' not granted`);
|
|
616
|
+
const fns = hostFns("owncast_fs_read", Permissions.StorageFS);
|
|
581
617
|
const offset = fns.owncast_fs_read(Memory.fromString(path).offset);
|
|
582
618
|
if (offset == 0) return null;
|
|
583
619
|
return Memory.find(offset).readString();
|
|
584
620
|
},
|
|
585
621
|
// Write bytes (Uint8Array) or a string to a file, creating parent
|
|
586
|
-
// directories as needed. Returns {
|
|
622
|
+
// directories as needed. Returns { error? }.
|
|
587
623
|
write(path, data) {
|
|
588
|
-
const fns =
|
|
589
|
-
if (!fns.owncast_fs_write)
|
|
590
|
-
throw new Error(`permission '${Permissions.StorageFS}' not granted`);
|
|
624
|
+
const fns = hostFns("owncast_fs_write", Permissions.StorageFS);
|
|
591
625
|
const dataMem =
|
|
592
626
|
data instanceof Uint8Array
|
|
593
627
|
? Memory.fromBuffer(
|
|
@@ -601,46 +635,51 @@ const owncast = {
|
|
|
601
635
|
Memory.fromString(path).offset,
|
|
602
636
|
dataMem.offset,
|
|
603
637
|
);
|
|
604
|
-
|
|
605
|
-
return JSON.parse(Memory.find(offset).readString());
|
|
638
|
+
return operationResult(offset, "write failed");
|
|
606
639
|
},
|
|
607
640
|
// List the entry names (files and subdirectories) directly inside dir.
|
|
608
641
|
// A missing directory lists as empty. Returns string[].
|
|
609
642
|
list(dir) {
|
|
610
|
-
const fns =
|
|
611
|
-
if (!fns.owncast_fs_list)
|
|
612
|
-
throw new Error(`permission '${Permissions.StorageFS}' not granted`);
|
|
643
|
+
const fns = hostFns("owncast_fs_list", Permissions.StorageFS);
|
|
613
644
|
const offset = fns.owncast_fs_list(Memory.fromString(dir || "").offset);
|
|
614
645
|
if (offset == 0) return [];
|
|
615
646
|
return JSON.parse(Memory.find(offset).readString());
|
|
616
647
|
},
|
|
617
|
-
// Remove a single file or empty directory. Returns {
|
|
648
|
+
// Remove a single file or empty directory. Returns { error? }.
|
|
618
649
|
delete(path) {
|
|
619
|
-
const fns =
|
|
620
|
-
if (!fns.owncast_fs_delete)
|
|
621
|
-
throw new Error(`permission '${Permissions.StorageFS}' not granted`);
|
|
650
|
+
const fns = hostFns("owncast_fs_delete", Permissions.StorageFS);
|
|
622
651
|
const offset = fns.owncast_fs_delete(Memory.fromString(path).offset);
|
|
623
|
-
|
|
624
|
-
return JSON.parse(Memory.find(offset).readString());
|
|
652
|
+
return operationResult(offset, "delete failed");
|
|
625
653
|
},
|
|
626
654
|
// Report whether a path exists inside the sandbox. Returns boolean.
|
|
627
655
|
exists(path) {
|
|
628
|
-
const fns =
|
|
629
|
-
if (!fns.owncast_fs_exists)
|
|
630
|
-
throw new Error(`permission '${Permissions.StorageFS}' not granted`);
|
|
656
|
+
const fns = hostFns("owncast_fs_exists", Permissions.StorageFS);
|
|
631
657
|
return fns.owncast_fs_exists(Memory.fromString(path).offset) === 1;
|
|
632
658
|
},
|
|
633
659
|
},
|
|
660
|
+
sql: {
|
|
661
|
+
exec(sql, params = []) {
|
|
662
|
+
const fns = hostFns("owncast_sql_exec", Permissions.StorageSQL);
|
|
663
|
+
const request = Memory.fromString(
|
|
664
|
+
JSON.stringify({ sql: String(sql), params: Array.from(params || []) }),
|
|
665
|
+
);
|
|
666
|
+
return sqlResult(fns.owncast_sql_exec(request.offset));
|
|
667
|
+
},
|
|
668
|
+
query(sql, params = []) {
|
|
669
|
+
return sqlRows(sqlQuery(sql, params));
|
|
670
|
+
},
|
|
671
|
+
queryRow(sql, params = []) {
|
|
672
|
+
// Asking the host for one row keeps a first-row read off the result
|
|
673
|
+
// budget, so this works against a table `query` would be too big for.
|
|
674
|
+
return sqlRows(sqlQuery(sql, params, 1))[0] || null;
|
|
675
|
+
},
|
|
676
|
+
},
|
|
634
677
|
fediverse: {
|
|
635
678
|
/** Publish a public text-only post to the fediverse on the streamer's
|
|
636
679
|
* behalf. Returns { url } on success, null on failure (rate-limited,
|
|
637
680
|
* disabled by admin, etc.). Requires `fediverse.post`. */
|
|
638
681
|
post(text) {
|
|
639
|
-
const fns =
|
|
640
|
-
if (!fns.owncast_fediverse_post)
|
|
641
|
-
throw new Error(
|
|
642
|
-
`permission '${Permissions.FediversePost}' not granted`,
|
|
643
|
-
);
|
|
682
|
+
const fns = hostFns("owncast_fediverse_post", Permissions.FediversePost);
|
|
644
683
|
const offset = fns.owncast_fediverse_post(Memory.fromString(text).offset);
|
|
645
684
|
if (offset == 0) return null;
|
|
646
685
|
return JSON.parse(Memory.find(offset).readString());
|
|
@@ -648,30 +687,18 @@ const owncast = {
|
|
|
648
687
|
},
|
|
649
688
|
notifications: {
|
|
650
689
|
discord(text) {
|
|
651
|
-
const fns =
|
|
652
|
-
if (!fns.owncast_notify_discord)
|
|
653
|
-
throw new Error(
|
|
654
|
-
`permission '${Permissions.NotificationsSend}' not granted`,
|
|
655
|
-
);
|
|
690
|
+
const fns = hostFns("owncast_notify_discord", Permissions.NotificationsSend);
|
|
656
691
|
fns.owncast_notify_discord(Memory.fromString(text).offset);
|
|
657
692
|
},
|
|
658
693
|
browserPush(payload) {
|
|
659
|
-
const fns =
|
|
660
|
-
if (!fns.owncast_notify_browser_push)
|
|
661
|
-
throw new Error(
|
|
662
|
-
`permission '${Permissions.NotificationsSend}' not granted`,
|
|
663
|
-
);
|
|
694
|
+
const fns = hostFns("owncast_notify_browser_push", Permissions.NotificationsSend);
|
|
664
695
|
const obj = typeof payload === "string" ? { title: payload } : payload;
|
|
665
696
|
fns.owncast_notify_browser_push(
|
|
666
697
|
Memory.fromString(JSON.stringify(obj)).offset,
|
|
667
698
|
);
|
|
668
699
|
},
|
|
669
700
|
fediverse(payload) {
|
|
670
|
-
const fns =
|
|
671
|
-
if (!fns.owncast_notify_fediverse)
|
|
672
|
-
throw new Error(
|
|
673
|
-
`permission '${Permissions.NotificationsSend}' not granted`,
|
|
674
|
-
);
|
|
701
|
+
const fns = hostFns("owncast_notify_fediverse", Permissions.NotificationsSend);
|
|
675
702
|
fns.owncast_notify_fediverse(
|
|
676
703
|
Memory.fromString(JSON.stringify(payload)).offset,
|
|
677
704
|
);
|
|
@@ -679,17 +706,13 @@ const owncast = {
|
|
|
679
706
|
},
|
|
680
707
|
stream: {
|
|
681
708
|
current() {
|
|
682
|
-
const fns =
|
|
683
|
-
if (!fns.owncast_stream_current)
|
|
684
|
-
throw new Error(`permission '${Permissions.ServerRead}' not granted`);
|
|
709
|
+
const fns = hostFns("owncast_stream_current", Permissions.ServerRead);
|
|
685
710
|
const offset = fns.owncast_stream_current();
|
|
686
711
|
if (offset == 0) return { online: false, viewers: 0 };
|
|
687
712
|
return JSON.parse(Memory.find(offset).readString());
|
|
688
713
|
},
|
|
689
714
|
broadcaster() {
|
|
690
|
-
const fns =
|
|
691
|
-
if (!fns.owncast_stream_broadcaster)
|
|
692
|
-
throw new Error(`permission '${Permissions.ServerRead}' not granted`);
|
|
715
|
+
const fns = hostFns("owncast_stream_broadcaster", Permissions.ServerRead);
|
|
693
716
|
const offset = fns.owncast_stream_broadcaster();
|
|
694
717
|
if (offset == 0) return {};
|
|
695
718
|
return JSON.parse(Memory.find(offset).readString());
|
|
@@ -697,41 +720,31 @@ const owncast = {
|
|
|
697
720
|
},
|
|
698
721
|
server: {
|
|
699
722
|
info() {
|
|
700
|
-
const fns =
|
|
701
|
-
if (!fns.owncast_server_info)
|
|
702
|
-
throw new Error(`permission '${Permissions.ServerRead}' not granted`);
|
|
723
|
+
const fns = hostFns("owncast_server_info", Permissions.ServerRead);
|
|
703
724
|
const offset = fns.owncast_server_info();
|
|
704
725
|
if (offset == 0) return {};
|
|
705
726
|
return JSON.parse(Memory.find(offset).readString());
|
|
706
727
|
},
|
|
707
728
|
socials() {
|
|
708
|
-
const fns =
|
|
709
|
-
if (!fns.owncast_server_socials)
|
|
710
|
-
throw new Error(`permission '${Permissions.ServerRead}' not granted`);
|
|
729
|
+
const fns = hostFns("owncast_server_socials", Permissions.ServerRead);
|
|
711
730
|
const offset = fns.owncast_server_socials();
|
|
712
731
|
if (offset == 0) return [];
|
|
713
732
|
return JSON.parse(Memory.find(offset).readString());
|
|
714
733
|
},
|
|
715
734
|
emotes() {
|
|
716
|
-
const fns =
|
|
717
|
-
if (!fns.owncast_server_emotes)
|
|
718
|
-
throw new Error(`permission '${Permissions.ServerRead}' not granted`);
|
|
735
|
+
const fns = hostFns("owncast_server_emotes", Permissions.ServerRead);
|
|
719
736
|
const offset = fns.owncast_server_emotes();
|
|
720
737
|
if (offset == 0) return [];
|
|
721
738
|
return JSON.parse(Memory.find(offset).readString());
|
|
722
739
|
},
|
|
723
740
|
federation() {
|
|
724
|
-
const fns =
|
|
725
|
-
if (!fns.owncast_server_federation)
|
|
726
|
-
throw new Error(`permission '${Permissions.ServerRead}' not granted`);
|
|
741
|
+
const fns = hostFns("owncast_server_federation", Permissions.ServerRead);
|
|
727
742
|
const offset = fns.owncast_server_federation();
|
|
728
743
|
if (offset == 0) return { enabled: false };
|
|
729
744
|
return JSON.parse(Memory.find(offset).readString());
|
|
730
745
|
},
|
|
731
746
|
tags() {
|
|
732
|
-
const fns =
|
|
733
|
-
if (!fns.owncast_server_tags)
|
|
734
|
-
throw new Error(`permission '${Permissions.ServerRead}' not granted`);
|
|
747
|
+
const fns = hostFns("owncast_server_tags", Permissions.ServerRead);
|
|
735
748
|
const offset = fns.owncast_server_tags();
|
|
736
749
|
if (offset == 0) return [];
|
|
737
750
|
return JSON.parse(Memory.find(offset).readString());
|
|
@@ -741,50 +754,36 @@ const owncast = {
|
|
|
741
754
|
/** Read the current video/transcoding config: { latencyLevel, codec,
|
|
742
755
|
* variants }. Requires `videoconfig.read`. */
|
|
743
756
|
read() {
|
|
744
|
-
const fns =
|
|
745
|
-
if (!fns.owncast_video_config_read)
|
|
746
|
-
throw new Error(
|
|
747
|
-
`permission '${Permissions.VideoConfigRead}' not granted`,
|
|
748
|
-
);
|
|
757
|
+
const fns = hostFns("owncast_video_config_read", Permissions.VideoConfigRead);
|
|
749
758
|
const offset = fns.owncast_video_config_read();
|
|
750
759
|
if (offset == 0) return { latencyLevel: 0, codec: "", variants: [] };
|
|
751
760
|
return JSON.parse(Memory.find(offset).readString());
|
|
752
761
|
},
|
|
753
762
|
/** Apply a partial video config change. Pass any of { latencyLevel, codec,
|
|
754
|
-
* variants }
|
|
763
|
+
* variants }, where omitted fields are left unchanged. Throws if the host
|
|
755
764
|
* rejects the config. Requires `videoconfig.write`. */
|
|
756
765
|
write(config) {
|
|
757
|
-
const fns =
|
|
758
|
-
if (!fns.owncast_video_config_write)
|
|
759
|
-
throw new Error(
|
|
760
|
-
`permission '${Permissions.VideoConfigWrite}' not granted`,
|
|
761
|
-
);
|
|
766
|
+
const fns = hostFns("owncast_video_config_write", Permissions.VideoConfigWrite);
|
|
762
767
|
const offset = fns.owncast_video_config_write(
|
|
763
768
|
Memory.fromString(JSON.stringify(config || {})).offset,
|
|
764
769
|
);
|
|
765
|
-
|
|
766
|
-
const result = JSON.parse(Memory.find(offset).readString());
|
|
767
|
-
if (!result.ok)
|
|
768
|
-
throw new Error(result.error || "videoConfig.write failed");
|
|
770
|
+
requireOperationResult(offset, "videoConfig.write failed");
|
|
769
771
|
},
|
|
770
772
|
},
|
|
771
773
|
kv: {
|
|
772
774
|
get(key) {
|
|
773
|
-
const fns =
|
|
774
|
-
if (!fns.owncast_kv_get)
|
|
775
|
-
throw new Error(`permission '${Permissions.StorageKV}' not granted`);
|
|
775
|
+
const fns = hostFns("owncast_kv_get", Permissions.StorageKV);
|
|
776
776
|
const offset = fns.owncast_kv_get(Memory.fromString(key).offset);
|
|
777
777
|
if (offset == 0) return null;
|
|
778
778
|
return Memory.find(offset).readString();
|
|
779
779
|
},
|
|
780
780
|
set(key, value) {
|
|
781
|
-
const fns =
|
|
782
|
-
|
|
783
|
-
throw new Error(`permission '${Permissions.StorageKV}' not granted`);
|
|
784
|
-
fns.owncast_kv_set(
|
|
781
|
+
const fns = hostFns("owncast_kv_set", Permissions.StorageKV);
|
|
782
|
+
const offset = fns.owncast_kv_set(
|
|
785
783
|
Memory.fromString(key).offset,
|
|
786
784
|
Memory.fromString(String(value)).offset,
|
|
787
785
|
);
|
|
786
|
+
requireOperationResult(offset, "kv.set failed");
|
|
788
787
|
},
|
|
789
788
|
// getJSON/setJSON are convenience wrappers over the string-only store, so
|
|
790
789
|
// plugins don't reimplement JSON.parse/stringify for every stored object.
|
|
@@ -837,9 +836,7 @@ const owncast = {
|
|
|
837
836
|
},
|
|
838
837
|
events: {
|
|
839
838
|
emit(eventType, payload) {
|
|
840
|
-
const fns =
|
|
841
|
-
if (!fns.owncast_emit_event)
|
|
842
|
-
throw new Error(`permission '${Permissions.EventsEmit}' not granted`);
|
|
839
|
+
const fns = hostFns("owncast_emit_event", Permissions.EventsEmit);
|
|
843
840
|
fns.owncast_emit_event(
|
|
844
841
|
Memory.fromString(eventType).offset,
|
|
845
842
|
Memory.fromString(JSON.stringify(payload)).offset,
|
|
@@ -847,43 +844,40 @@ const owncast = {
|
|
|
847
844
|
},
|
|
848
845
|
},
|
|
849
846
|
actions: {
|
|
850
|
-
// Append one or more action buttons to the plugin's effective list
|
|
851
|
-
//
|
|
852
|
-
// object or an array. The host validates each entry (title
|
|
853
|
-
// required, exactly one of url/html, relative URLs rewritten into
|
|
854
|
-
// this plugin's namespace, cross-plugin URLs rejected) and persists
|
|
855
|
-
// the result, so the next /api/config request returns the longer
|
|
856
|
-
// list. Requires 'ui.modify'.
|
|
847
|
+
// Append one or more action buttons to the plugin's effective list.
|
|
848
|
+
// The host validates and persists the list, returning { error? }.
|
|
857
849
|
add(actions) {
|
|
858
|
-
const fns =
|
|
859
|
-
if (!fns.owncast_add_actions)
|
|
860
|
-
throw permError("owncast.actions.add", Permissions.UIModify);
|
|
850
|
+
const fns = hostFns("owncast_add_actions", Permissions.UIModify);
|
|
861
851
|
const list = Array.isArray(actions) ? actions : [actions];
|
|
862
|
-
|
|
852
|
+
requireOperationResult(
|
|
853
|
+
fns.owncast_add_actions(
|
|
854
|
+
Memory.fromString(JSON.stringify(list)).offset,
|
|
855
|
+
),
|
|
856
|
+
"owncast.actions.add failed",
|
|
857
|
+
);
|
|
863
858
|
},
|
|
864
859
|
// Drop the runtime additions so only manifest.actions remain in
|
|
865
860
|
// the effective list on the next /api/config request. Requires
|
|
866
861
|
// 'ui.modify'.
|
|
867
862
|
clear() {
|
|
868
|
-
const fns =
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
863
|
+
const fns = hostFns("owncast_clear_actions", Permissions.UIModify);
|
|
864
|
+
requireOperationResult(
|
|
865
|
+
fns.owncast_clear_actions(),
|
|
866
|
+
"owncast.actions.clear failed",
|
|
867
|
+
);
|
|
872
868
|
},
|
|
873
869
|
},
|
|
874
870
|
sse: {
|
|
875
871
|
// send(channel, event, data) pushes one Server-Sent-Event to every
|
|
876
872
|
// browser connected to this plugin's /plugins/<name>/_sse/<channel>
|
|
877
873
|
// stream. `event` is the SSE event name (browser side:
|
|
878
|
-
// source.addEventListener(event, ...))
|
|
874
|
+
// source.addEventListener(event, ...)). Pass "" for the default
|
|
879
875
|
// "message" event. `data` is sent as-is if it's a string, otherwise
|
|
880
876
|
// JSON-stringified. Fire-and-forget: returns immediately, and frames to
|
|
881
877
|
// a slow client are dropped rather than blocking the plugin. Requires
|
|
882
878
|
// the 'http.sse' permission.
|
|
883
879
|
send(channel, event, data) {
|
|
884
|
-
const fns =
|
|
885
|
-
if (!fns.owncast_sse_send)
|
|
886
|
-
throw new Error(`permission '${Permissions.HttpSSE}' not granted`);
|
|
880
|
+
const fns = hostFns("owncast_sse_send", Permissions.HttpSSE);
|
|
887
881
|
const payload = typeof data === "string" ? data : JSON.stringify(data);
|
|
888
882
|
fns.owncast_sse_send(
|
|
889
883
|
Memory.fromString(channel || "").offset,
|
|
@@ -895,7 +889,7 @@ const owncast = {
|
|
|
895
889
|
timer: {
|
|
896
890
|
// setTimeout(fn, ms) runs fn once after ~ms milliseconds. setInterval
|
|
897
891
|
// repeats until clear(id). The host drives the schedule (the sandbox has
|
|
898
|
-
// no setTimeout)
|
|
892
|
+
// no setTimeout). Your callback runs in this instance when it fires.
|
|
899
893
|
// Returns an id for clear(). Very small delays are clamped up by the host,
|
|
900
894
|
// and there's a per-plugin cap on pending timers (throws past it).
|
|
901
895
|
// Note: timers are in-memory and do not survive a plugin reload or a host
|
|
@@ -945,11 +939,21 @@ function dispatchPageContent(req) {
|
|
|
945
939
|
return registered.onPageContent(req) || "";
|
|
946
940
|
}
|
|
947
941
|
|
|
942
|
+
function dispatchPageStyles() {
|
|
943
|
+
if (!registered || !isFn(registered.onPageStyles)) return "";
|
|
944
|
+
return registered.onPageStyles() || "";
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function dispatchPageScripts() {
|
|
948
|
+
if (!registered || !isFn(registered.onPageScripts)) return "";
|
|
949
|
+
return registered.onPageScripts() || "";
|
|
950
|
+
}
|
|
951
|
+
|
|
948
952
|
module.exports = {
|
|
949
953
|
definePlugin,
|
|
950
|
-
defineCommands,
|
|
951
954
|
owncast,
|
|
952
955
|
filter,
|
|
956
|
+
authCheck,
|
|
953
957
|
FilterAction,
|
|
954
958
|
Events,
|
|
955
959
|
Permissions,
|
|
@@ -958,6 +962,9 @@ module.exports = {
|
|
|
958
962
|
dispatchEvent,
|
|
959
963
|
dispatchFilter,
|
|
960
964
|
dispatchHttp,
|
|
965
|
+
dispatchAuthCheck,
|
|
961
966
|
dispatchTabContent,
|
|
962
967
|
dispatchPageContent,
|
|
968
|
+
dispatchPageStyles,
|
|
969
|
+
dispatchPageScripts,
|
|
963
970
|
};
|