@owncast/plugin-sdk 0.1.0 → 0.3.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 +12 -10
- package/bin/owncast-plugin.js +179 -55
- package/index.d.ts +44 -24
- package/index.js +250 -110
- package/package.json +3 -2
- package/scripts/postinstall.js +17 -13
- package/testing.js +158 -0
package/index.js
CHANGED
|
@@ -10,45 +10,47 @@ let registered = null;
|
|
|
10
10
|
const FilterAction = Object.freeze({
|
|
11
11
|
Pass: "pass",
|
|
12
12
|
Modify: "modify",
|
|
13
|
-
Drop: "drop"
|
|
13
|
+
Drop: "drop",
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
const Events = Object.freeze({
|
|
17
17
|
// Chat events
|
|
18
|
-
ChatMessageReceived:
|
|
19
|
-
ChatUserJoined:
|
|
20
|
-
ChatUserParted:
|
|
21
|
-
ChatUserRenamed:
|
|
18
|
+
ChatMessageReceived: "chat.message.received",
|
|
19
|
+
ChatUserJoined: "chat.user.joined",
|
|
20
|
+
ChatUserParted: "chat.user.parted",
|
|
21
|
+
ChatUserRenamed: "chat.user.renamed",
|
|
22
22
|
ChatMessageModerated: "chat.message.moderated",
|
|
23
23
|
// Stream lifecycle
|
|
24
|
-
StreamStarted:
|
|
25
|
-
StreamStopped:
|
|
26
|
-
StreamTitleChanged:
|
|
27
|
-
// Fediverse
|
|
28
|
-
FediverseFollow:
|
|
29
|
-
FediverseLike:
|
|
30
|
-
FediverseRepost:
|
|
31
|
-
FediverseMention:
|
|
32
|
-
FediverseReply:
|
|
24
|
+
StreamStarted: "stream.started",
|
|
25
|
+
StreamStopped: "stream.stopped",
|
|
26
|
+
StreamTitleChanged: "stream.title.changed",
|
|
27
|
+
// Fediverse, engagement (metadata only) + inbound posts (with content)
|
|
28
|
+
FediverseFollow: "fediverse.follow",
|
|
29
|
+
FediverseLike: "fediverse.like",
|
|
30
|
+
FediverseRepost: "fediverse.repost",
|
|
31
|
+
FediverseMention: "fediverse.mention",
|
|
32
|
+
FediverseReply: "fediverse.reply",
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
const Permissions = Object.freeze({
|
|
36
|
-
ChatSend:
|
|
37
|
-
ChatHistory:
|
|
38
|
-
ChatModerate:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
36
|
+
ChatSend: "chat.send",
|
|
37
|
+
ChatHistory: "chat.history",
|
|
38
|
+
ChatModerate: "chat.moderate",
|
|
39
|
+
ChatFilter: "chat.filter",
|
|
40
|
+
StorageKV: "storage.kv",
|
|
41
|
+
StorageUpload: "storage.upload",
|
|
42
|
+
EventsEmit: "events.emit",
|
|
43
|
+
NetworkFetch: "network.fetch",
|
|
44
|
+
HttpServe: "http.serve",
|
|
45
|
+
ServerRead: "server.read",
|
|
45
46
|
NotificationsSend: "notifications.send",
|
|
46
|
-
UsersRead:
|
|
47
|
-
UsersModerate:
|
|
48
|
-
FediversePost:
|
|
49
|
-
HttpSSE:
|
|
50
|
-
VideoConfigRead:
|
|
51
|
-
VideoConfigWrite:
|
|
47
|
+
UsersRead: "users.read",
|
|
48
|
+
UsersModerate: "users.moderate",
|
|
49
|
+
FediversePost: "fediverse.post",
|
|
50
|
+
HttpSSE: "http.sse",
|
|
51
|
+
VideoConfigRead: "videoconfig.read",
|
|
52
|
+
VideoConfigWrite: "videoconfig.write",
|
|
53
|
+
UIModify: "ui.modify",
|
|
52
54
|
});
|
|
53
55
|
|
|
54
56
|
const filter = Object.freeze({
|
|
@@ -60,14 +62,14 @@ const filter = Object.freeze({
|
|
|
60
62
|
},
|
|
61
63
|
drop(reason) {
|
|
62
64
|
return { action: FilterAction.Drop, reason: reason || "" };
|
|
63
|
-
}
|
|
65
|
+
},
|
|
64
66
|
});
|
|
65
67
|
|
|
66
68
|
// Distinguishes notification handlers from filter handlers in the HANDLERS
|
|
67
|
-
// map below. Internal
|
|
69
|
+
// map below. Internal, not part of the public API.
|
|
68
70
|
const HandlerKind = Object.freeze({
|
|
69
71
|
Notify: "notify",
|
|
70
|
-
Filter: "filter"
|
|
72
|
+
Filter: "filter",
|
|
71
73
|
});
|
|
72
74
|
|
|
73
75
|
// Maps a built-in handler method name to the event type it subscribes to and
|
|
@@ -75,30 +77,54 @@ const HandlerKind = Object.freeze({
|
|
|
75
77
|
// new built-in Owncast events.
|
|
76
78
|
const HANDLERS = Object.freeze({
|
|
77
79
|
// Chat
|
|
78
|
-
onChatMessage:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
onChatMessage: {
|
|
81
|
+
event: Events.ChatMessageReceived,
|
|
82
|
+
kind: HandlerKind.Notify,
|
|
83
|
+
},
|
|
84
|
+
filterChatMessage: {
|
|
85
|
+
event: Events.ChatMessageReceived,
|
|
86
|
+
kind: HandlerKind.Filter,
|
|
87
|
+
},
|
|
88
|
+
onChatUserJoined: { event: Events.ChatUserJoined, kind: HandlerKind.Notify },
|
|
89
|
+
onChatUserParted: { event: Events.ChatUserParted, kind: HandlerKind.Notify },
|
|
90
|
+
onChatUserRenamed: {
|
|
91
|
+
event: Events.ChatUserRenamed,
|
|
92
|
+
kind: HandlerKind.Notify,
|
|
93
|
+
},
|
|
94
|
+
onMessageModerated: {
|
|
95
|
+
event: Events.ChatMessageModerated,
|
|
96
|
+
kind: HandlerKind.Notify,
|
|
97
|
+
},
|
|
84
98
|
// Stream lifecycle
|
|
85
|
-
onStreamStarted:
|
|
86
|
-
onStreamStopped:
|
|
87
|
-
onStreamTitleChanged: {
|
|
99
|
+
onStreamStarted: { event: Events.StreamStarted, kind: HandlerKind.Notify },
|
|
100
|
+
onStreamStopped: { event: Events.StreamStopped, kind: HandlerKind.Notify },
|
|
101
|
+
onStreamTitleChanged: {
|
|
102
|
+
event: Events.StreamTitleChanged,
|
|
103
|
+
kind: HandlerKind.Notify,
|
|
104
|
+
},
|
|
88
105
|
// Fediverse engagement (actor + target metadata)
|
|
89
|
-
onFediverseFollow:
|
|
90
|
-
|
|
91
|
-
|
|
106
|
+
onFediverseFollow: {
|
|
107
|
+
event: Events.FediverseFollow,
|
|
108
|
+
kind: HandlerKind.Notify,
|
|
109
|
+
},
|
|
110
|
+
onFediverseLike: { event: Events.FediverseLike, kind: HandlerKind.Notify },
|
|
111
|
+
onFediverseRepost: {
|
|
112
|
+
event: Events.FediverseRepost,
|
|
113
|
+
kind: HandlerKind.Notify,
|
|
114
|
+
},
|
|
92
115
|
// Fediverse inbound posts (with content)
|
|
93
|
-
onFediverseMention:
|
|
94
|
-
|
|
116
|
+
onFediverseMention: {
|
|
117
|
+
event: Events.FediverseMention,
|
|
118
|
+
kind: HandlerKind.Notify,
|
|
119
|
+
},
|
|
120
|
+
onFediverseReply: { event: Events.FediverseReply, kind: HandlerKind.Notify },
|
|
95
121
|
});
|
|
96
122
|
|
|
97
123
|
// typeof comparisons in well-known categories. JS guarantees these strings,
|
|
98
124
|
// but we go through named constants so a stray typo can't pass silently.
|
|
99
125
|
const JsType = Object.freeze({
|
|
100
126
|
Function: "function",
|
|
101
|
-
Object: "object"
|
|
127
|
+
Object: "object",
|
|
102
128
|
});
|
|
103
129
|
const isFn = (x) => typeof x === JsType.Function;
|
|
104
130
|
const isObj = (x) => x !== null && typeof x === JsType.Object;
|
|
@@ -115,7 +141,10 @@ function describeSubscriptions() {
|
|
|
115
141
|
const notify = [];
|
|
116
142
|
const filterSubs = [];
|
|
117
143
|
if (registered) {
|
|
118
|
-
const priority =
|
|
144
|
+
const priority =
|
|
145
|
+
typeof registered.filterPriority === "number"
|
|
146
|
+
? registered.filterPriority
|
|
147
|
+
: 100;
|
|
119
148
|
for (const [method, info] of Object.entries(HANDLERS)) {
|
|
120
149
|
if (!isFn(registered[method])) continue;
|
|
121
150
|
if (info.kind === HandlerKind.Notify) {
|
|
@@ -137,7 +166,11 @@ function dispatchEvent(envelope) {
|
|
|
137
166
|
if (!registered) return;
|
|
138
167
|
const { eventType, payload } = envelope;
|
|
139
168
|
for (const [method, info] of Object.entries(HANDLERS)) {
|
|
140
|
-
if (
|
|
169
|
+
if (
|
|
170
|
+
info.kind === HandlerKind.Notify &&
|
|
171
|
+
info.event === eventType &&
|
|
172
|
+
isFn(registered[method])
|
|
173
|
+
) {
|
|
141
174
|
registered[method](payload);
|
|
142
175
|
return;
|
|
143
176
|
}
|
|
@@ -151,7 +184,11 @@ function dispatchFilter(envelope) {
|
|
|
151
184
|
if (!registered) return filter.pass();
|
|
152
185
|
const { eventType, payload } = envelope;
|
|
153
186
|
for (const [method, info] of Object.entries(HANDLERS)) {
|
|
154
|
-
if (
|
|
187
|
+
if (
|
|
188
|
+
info.kind === HandlerKind.Filter &&
|
|
189
|
+
info.event === eventType &&
|
|
190
|
+
isFn(registered[method])
|
|
191
|
+
) {
|
|
155
192
|
return registered[method](payload) || filter.pass();
|
|
156
193
|
}
|
|
157
194
|
}
|
|
@@ -169,101 +206,140 @@ function dispatchHttp(request) {
|
|
|
169
206
|
return {
|
|
170
207
|
status: out.status || 200,
|
|
171
208
|
headers: out.headers || {},
|
|
172
|
-
body: out.body == null ? "" : String(out.body)
|
|
209
|
+
body: out.body == null ? "" : String(out.body),
|
|
173
210
|
};
|
|
174
211
|
}
|
|
175
212
|
|
|
213
|
+
// permError builds an actionable Error and logs it to stderr (which the
|
|
214
|
+
// host runtime captures), so a plugin author running `owncast-plugin
|
|
215
|
+
// serve` or hitting the host's logs sees exactly which permission to
|
|
216
|
+
// add to their manifest. apiName is the SDK call the author wrote
|
|
217
|
+
// (e.g. "owncast.actions.set"); perm is the manifest permission string.
|
|
218
|
+
function permError(apiName, perm) {
|
|
219
|
+
const msg = `${apiName} requires the '${perm}' permission. Add it to your plugin.manifest.json's "permissions" array.`;
|
|
220
|
+
console.error(`[owncast-plugin] ${msg}`);
|
|
221
|
+
return new Error(msg);
|
|
222
|
+
}
|
|
223
|
+
|
|
176
224
|
const owncast = {
|
|
177
225
|
chat: {
|
|
178
226
|
send(text) {
|
|
179
227
|
const fns = Host.getFunctions();
|
|
180
|
-
if (!fns.owncast_send_chat)
|
|
228
|
+
if (!fns.owncast_send_chat)
|
|
229
|
+
throw new Error(`permission '${Permissions.ChatSend}' not granted`);
|
|
181
230
|
fns.owncast_send_chat(Memory.fromString(text).offset);
|
|
182
231
|
},
|
|
183
232
|
sendAction(text) {
|
|
184
233
|
const fns = Host.getFunctions();
|
|
185
|
-
if (!fns.owncast_send_chat_action)
|
|
234
|
+
if (!fns.owncast_send_chat_action)
|
|
235
|
+
throw new Error(`permission '${Permissions.ChatSend}' not granted`);
|
|
186
236
|
fns.owncast_send_chat_action(Memory.fromString(text).offset);
|
|
187
237
|
},
|
|
188
238
|
system(body) {
|
|
189
239
|
const fns = Host.getFunctions();
|
|
190
|
-
if (!fns.owncast_send_chat_system)
|
|
240
|
+
if (!fns.owncast_send_chat_system)
|
|
241
|
+
throw new Error(`permission '${Permissions.ChatSend}' not granted`);
|
|
191
242
|
fns.owncast_send_chat_system(Memory.fromString(body).offset);
|
|
192
243
|
},
|
|
193
244
|
history(limit) {
|
|
194
245
|
const fns = Host.getFunctions();
|
|
195
|
-
if (!fns.owncast_chat_history)
|
|
246
|
+
if (!fns.owncast_chat_history)
|
|
247
|
+
throw new Error(`permission '${Permissions.ChatHistory}' not granted`);
|
|
196
248
|
const offset = fns.owncast_chat_history(limit || 0);
|
|
197
249
|
if (offset == 0) return [];
|
|
198
250
|
return JSON.parse(Memory.find(offset).readString());
|
|
199
251
|
},
|
|
200
252
|
deleteMessage(messageId) {
|
|
201
253
|
const fns = Host.getFunctions();
|
|
202
|
-
if (!fns.owncast_delete_message)
|
|
254
|
+
if (!fns.owncast_delete_message)
|
|
255
|
+
throw new Error(`permission '${Permissions.ChatModerate}' not granted`);
|
|
203
256
|
fns.owncast_delete_message(Memory.fromString(String(messageId)).offset);
|
|
204
257
|
},
|
|
205
258
|
kick(clientId) {
|
|
206
259
|
const fns = Host.getFunctions();
|
|
207
|
-
if (!fns.owncast_kick_client)
|
|
260
|
+
if (!fns.owncast_kick_client)
|
|
261
|
+
throw new Error(`permission '${Permissions.ChatModerate}' not granted`);
|
|
208
262
|
fns.owncast_kick_client(BigInt(clientId));
|
|
209
263
|
},
|
|
210
264
|
sendTo(clientId, text) {
|
|
211
265
|
const fns = Host.getFunctions();
|
|
212
|
-
if (!fns.owncast_send_chat_to)
|
|
213
|
-
|
|
266
|
+
if (!fns.owncast_send_chat_to)
|
|
267
|
+
throw new Error(`permission '${Permissions.ChatSend}' not granted`);
|
|
268
|
+
fns.owncast_send_chat_to(
|
|
269
|
+
BigInt(clientId),
|
|
270
|
+
Memory.fromString(text).offset,
|
|
271
|
+
);
|
|
214
272
|
},
|
|
215
273
|
clients() {
|
|
216
274
|
const fns = Host.getFunctions();
|
|
217
|
-
if (!fns.owncast_chat_clients)
|
|
275
|
+
if (!fns.owncast_chat_clients)
|
|
276
|
+
throw new Error(`permission '${Permissions.ChatHistory}' not granted`);
|
|
218
277
|
const offset = fns.owncast_chat_clients();
|
|
219
278
|
if (offset == 0) return [];
|
|
220
279
|
return JSON.parse(Memory.find(offset).readString());
|
|
221
|
-
}
|
|
280
|
+
},
|
|
222
281
|
},
|
|
223
282
|
users: {
|
|
224
283
|
list() {
|
|
225
284
|
const fns = Host.getFunctions();
|
|
226
|
-
if (!fns.owncast_users_list)
|
|
285
|
+
if (!fns.owncast_users_list)
|
|
286
|
+
throw new Error(`permission '${Permissions.UsersRead}' not granted`);
|
|
227
287
|
const offset = fns.owncast_users_list();
|
|
228
288
|
if (offset == 0) return [];
|
|
229
289
|
return JSON.parse(Memory.find(offset).readString());
|
|
230
290
|
},
|
|
231
291
|
get(id) {
|
|
232
292
|
const fns = Host.getFunctions();
|
|
233
|
-
if (!fns.owncast_user_get)
|
|
293
|
+
if (!fns.owncast_user_get)
|
|
294
|
+
throw new Error(`permission '${Permissions.UsersRead}' not granted`);
|
|
234
295
|
const offset = fns.owncast_user_get(Memory.fromString(id).offset);
|
|
235
296
|
if (offset == 0) return null;
|
|
236
297
|
return JSON.parse(Memory.find(offset).readString());
|
|
237
298
|
},
|
|
238
299
|
setEnabled(id, enabled, reason) {
|
|
239
300
|
const fns = Host.getFunctions();
|
|
240
|
-
if (!fns.owncast_user_set_enabled)
|
|
301
|
+
if (!fns.owncast_user_set_enabled)
|
|
302
|
+
throw new Error(
|
|
303
|
+
`permission '${Permissions.UsersModerate}' not granted`,
|
|
304
|
+
);
|
|
241
305
|
fns.owncast_user_set_enabled(
|
|
242
306
|
Memory.fromString(id).offset,
|
|
243
307
|
enabled ? 1 : 0,
|
|
244
|
-
Memory.fromString(reason || "").offset
|
|
308
|
+
Memory.fromString(reason || "").offset,
|
|
245
309
|
);
|
|
246
310
|
},
|
|
247
311
|
banIP(ip) {
|
|
248
312
|
const fns = Host.getFunctions();
|
|
249
|
-
if (!fns.owncast_ban_ip)
|
|
313
|
+
if (!fns.owncast_ban_ip)
|
|
314
|
+
throw new Error(
|
|
315
|
+
`permission '${Permissions.UsersModerate}' not granted`,
|
|
316
|
+
);
|
|
250
317
|
fns.owncast_ban_ip(Memory.fromString(ip).offset);
|
|
251
|
-
}
|
|
318
|
+
},
|
|
252
319
|
},
|
|
253
320
|
storage: {
|
|
254
321
|
upload(name, data) {
|
|
255
322
|
const fns = Host.getFunctions();
|
|
256
|
-
if (!fns.owncast_storage_upload)
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
323
|
+
if (!fns.owncast_storage_upload)
|
|
324
|
+
throw new Error(
|
|
325
|
+
`permission '${Permissions.StorageUpload}' not granted`,
|
|
326
|
+
);
|
|
327
|
+
const dataMem =
|
|
328
|
+
data instanceof Uint8Array
|
|
329
|
+
? Memory.fromBuffer(
|
|
330
|
+
data.buffer.slice(
|
|
331
|
+
data.byteOffset,
|
|
332
|
+
data.byteOffset + data.byteLength,
|
|
333
|
+
),
|
|
334
|
+
)
|
|
335
|
+
: Memory.fromString(String(data));
|
|
260
336
|
const offset = fns.owncast_storage_upload(
|
|
261
337
|
Memory.fromString(name).offset,
|
|
262
|
-
dataMem.offset
|
|
338
|
+
dataMem.offset,
|
|
263
339
|
);
|
|
264
340
|
if (offset == 0) return null;
|
|
265
341
|
return JSON.parse(Memory.find(offset).readString());
|
|
266
|
-
}
|
|
342
|
+
},
|
|
267
343
|
},
|
|
268
344
|
fediverse: {
|
|
269
345
|
/** Publish a public text-only post to the fediverse on the streamer's
|
|
@@ -271,82 +347,107 @@ const owncast = {
|
|
|
271
347
|
* disabled by admin, etc.). Requires `fediverse.post`. */
|
|
272
348
|
post(text) {
|
|
273
349
|
const fns = Host.getFunctions();
|
|
274
|
-
if (!fns.owncast_fediverse_post)
|
|
350
|
+
if (!fns.owncast_fediverse_post)
|
|
351
|
+
throw new Error(
|
|
352
|
+
`permission '${Permissions.FediversePost}' not granted`,
|
|
353
|
+
);
|
|
275
354
|
const offset = fns.owncast_fediverse_post(Memory.fromString(text).offset);
|
|
276
355
|
if (offset == 0) return null;
|
|
277
356
|
return JSON.parse(Memory.find(offset).readString());
|
|
278
|
-
}
|
|
357
|
+
},
|
|
279
358
|
},
|
|
280
359
|
notifications: {
|
|
281
360
|
discord(text) {
|
|
282
361
|
const fns = Host.getFunctions();
|
|
283
|
-
if (!fns.owncast_notify_discord)
|
|
362
|
+
if (!fns.owncast_notify_discord)
|
|
363
|
+
throw new Error(
|
|
364
|
+
`permission '${Permissions.NotificationsSend}' not granted`,
|
|
365
|
+
);
|
|
284
366
|
fns.owncast_notify_discord(Memory.fromString(text).offset);
|
|
285
367
|
},
|
|
286
368
|
browserPush(payload) {
|
|
287
369
|
const fns = Host.getFunctions();
|
|
288
|
-
if (!fns.owncast_notify_browser_push)
|
|
370
|
+
if (!fns.owncast_notify_browser_push)
|
|
371
|
+
throw new Error(
|
|
372
|
+
`permission '${Permissions.NotificationsSend}' not granted`,
|
|
373
|
+
);
|
|
289
374
|
const obj = typeof payload === "string" ? { title: payload } : payload;
|
|
290
|
-
fns.owncast_notify_browser_push(
|
|
375
|
+
fns.owncast_notify_browser_push(
|
|
376
|
+
Memory.fromString(JSON.stringify(obj)).offset,
|
|
377
|
+
);
|
|
291
378
|
},
|
|
292
379
|
fediverse(payload) {
|
|
293
380
|
const fns = Host.getFunctions();
|
|
294
|
-
if (!fns.owncast_notify_fediverse)
|
|
295
|
-
|
|
296
|
-
|
|
381
|
+
if (!fns.owncast_notify_fediverse)
|
|
382
|
+
throw new Error(
|
|
383
|
+
`permission '${Permissions.NotificationsSend}' not granted`,
|
|
384
|
+
);
|
|
385
|
+
fns.owncast_notify_fediverse(
|
|
386
|
+
Memory.fromString(JSON.stringify(payload)).offset,
|
|
387
|
+
);
|
|
388
|
+
},
|
|
297
389
|
},
|
|
298
390
|
stream: {
|
|
299
391
|
current() {
|
|
300
392
|
const fns = Host.getFunctions();
|
|
301
|
-
if (!fns.owncast_stream_current)
|
|
393
|
+
if (!fns.owncast_stream_current)
|
|
394
|
+
throw new Error(`permission '${Permissions.ServerRead}' not granted`);
|
|
302
395
|
const offset = fns.owncast_stream_current();
|
|
303
396
|
if (offset == 0) return { online: false, viewers: 0 };
|
|
304
397
|
return JSON.parse(Memory.find(offset).readString());
|
|
305
398
|
},
|
|
306
399
|
broadcaster() {
|
|
307
400
|
const fns = Host.getFunctions();
|
|
308
|
-
if (!fns.owncast_stream_broadcaster)
|
|
401
|
+
if (!fns.owncast_stream_broadcaster)
|
|
402
|
+
throw new Error(`permission '${Permissions.ServerRead}' not granted`);
|
|
309
403
|
const offset = fns.owncast_stream_broadcaster();
|
|
310
404
|
if (offset == 0) return {};
|
|
311
405
|
return JSON.parse(Memory.find(offset).readString());
|
|
312
|
-
}
|
|
406
|
+
},
|
|
313
407
|
},
|
|
314
408
|
server: {
|
|
315
409
|
info() {
|
|
316
410
|
const fns = Host.getFunctions();
|
|
317
|
-
if (!fns.owncast_server_info)
|
|
411
|
+
if (!fns.owncast_server_info)
|
|
412
|
+
throw new Error(`permission '${Permissions.ServerRead}' not granted`);
|
|
318
413
|
const offset = fns.owncast_server_info();
|
|
319
414
|
if (offset == 0) return {};
|
|
320
415
|
return JSON.parse(Memory.find(offset).readString());
|
|
321
416
|
},
|
|
322
417
|
socials() {
|
|
323
418
|
const fns = Host.getFunctions();
|
|
324
|
-
if (!fns.owncast_server_socials)
|
|
419
|
+
if (!fns.owncast_server_socials)
|
|
420
|
+
throw new Error(`permission '${Permissions.ServerRead}' not granted`);
|
|
325
421
|
const offset = fns.owncast_server_socials();
|
|
326
422
|
if (offset == 0) return [];
|
|
327
423
|
return JSON.parse(Memory.find(offset).readString());
|
|
328
424
|
},
|
|
329
425
|
federation() {
|
|
330
426
|
const fns = Host.getFunctions();
|
|
331
|
-
if (!fns.owncast_server_federation)
|
|
427
|
+
if (!fns.owncast_server_federation)
|
|
428
|
+
throw new Error(`permission '${Permissions.ServerRead}' not granted`);
|
|
332
429
|
const offset = fns.owncast_server_federation();
|
|
333
430
|
if (offset == 0) return { enabled: false };
|
|
334
431
|
return JSON.parse(Memory.find(offset).readString());
|
|
335
432
|
},
|
|
336
433
|
tags() {
|
|
337
434
|
const fns = Host.getFunctions();
|
|
338
|
-
if (!fns.owncast_server_tags)
|
|
435
|
+
if (!fns.owncast_server_tags)
|
|
436
|
+
throw new Error(`permission '${Permissions.ServerRead}' not granted`);
|
|
339
437
|
const offset = fns.owncast_server_tags();
|
|
340
438
|
if (offset == 0) return [];
|
|
341
439
|
return JSON.parse(Memory.find(offset).readString());
|
|
342
|
-
}
|
|
440
|
+
},
|
|
343
441
|
},
|
|
344
442
|
videoConfig: {
|
|
345
443
|
/** Read the current video/transcoding config: { latencyLevel, codec,
|
|
346
444
|
* variants }. Requires `videoconfig.read`. */
|
|
347
445
|
read() {
|
|
348
446
|
const fns = Host.getFunctions();
|
|
349
|
-
if (!fns.owncast_video_config_read)
|
|
447
|
+
if (!fns.owncast_video_config_read)
|
|
448
|
+
throw new Error(
|
|
449
|
+
`permission '${Permissions.VideoConfigRead}' not granted`,
|
|
450
|
+
);
|
|
350
451
|
const offset = fns.owncast_video_config_read();
|
|
351
452
|
if (offset == 0) return { latencyLevel: 0, codec: "", variants: [] };
|
|
352
453
|
return JSON.parse(Memory.find(offset).readString());
|
|
@@ -356,39 +457,73 @@ const owncast = {
|
|
|
356
457
|
* rejects the config. Requires `videoconfig.write`. */
|
|
357
458
|
write(config) {
|
|
358
459
|
const fns = Host.getFunctions();
|
|
359
|
-
if (!fns.owncast_video_config_write)
|
|
360
|
-
|
|
361
|
-
|
|
460
|
+
if (!fns.owncast_video_config_write)
|
|
461
|
+
throw new Error(
|
|
462
|
+
`permission '${Permissions.VideoConfigWrite}' not granted`,
|
|
463
|
+
);
|
|
464
|
+
const offset = fns.owncast_video_config_write(
|
|
465
|
+
Memory.fromString(JSON.stringify(config || {})).offset,
|
|
466
|
+
);
|
|
467
|
+
if (offset == 0) throw new Error("videoConfig.write failed");
|
|
362
468
|
const result = JSON.parse(Memory.find(offset).readString());
|
|
363
|
-
if (!result.ok)
|
|
364
|
-
|
|
469
|
+
if (!result.ok)
|
|
470
|
+
throw new Error(result.error || "videoConfig.write failed");
|
|
471
|
+
},
|
|
365
472
|
},
|
|
366
473
|
kv: {
|
|
367
474
|
get(key) {
|
|
368
475
|
const fns = Host.getFunctions();
|
|
369
|
-
if (!fns.owncast_kv_get)
|
|
476
|
+
if (!fns.owncast_kv_get)
|
|
477
|
+
throw new Error(`permission '${Permissions.StorageKV}' not granted`);
|
|
370
478
|
const offset = fns.owncast_kv_get(Memory.fromString(key).offset);
|
|
371
479
|
if (offset == 0) return null;
|
|
372
480
|
return Memory.find(offset).readString();
|
|
373
481
|
},
|
|
374
482
|
set(key, value) {
|
|
375
483
|
const fns = Host.getFunctions();
|
|
376
|
-
if (!fns.owncast_kv_set)
|
|
484
|
+
if (!fns.owncast_kv_set)
|
|
485
|
+
throw new Error(`permission '${Permissions.StorageKV}' not granted`);
|
|
377
486
|
fns.owncast_kv_set(
|
|
378
487
|
Memory.fromString(key).offset,
|
|
379
|
-
Memory.fromString(String(value)).offset
|
|
488
|
+
Memory.fromString(String(value)).offset,
|
|
380
489
|
);
|
|
381
|
-
}
|
|
490
|
+
},
|
|
382
491
|
},
|
|
383
492
|
events: {
|
|
384
493
|
emit(eventType, payload) {
|
|
385
494
|
const fns = Host.getFunctions();
|
|
386
|
-
if (!fns.owncast_emit_event)
|
|
495
|
+
if (!fns.owncast_emit_event)
|
|
496
|
+
throw new Error(`permission '${Permissions.EventsEmit}' not granted`);
|
|
387
497
|
fns.owncast_emit_event(
|
|
388
498
|
Memory.fromString(eventType).offset,
|
|
389
|
-
Memory.fromString(JSON.stringify(payload)).offset
|
|
499
|
+
Memory.fromString(JSON.stringify(payload)).offset,
|
|
390
500
|
);
|
|
391
|
-
}
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
actions: {
|
|
504
|
+
// Append one or more action buttons to the plugin's effective list
|
|
505
|
+
// (manifest.actions ++ runtime additions). Accepts a single button
|
|
506
|
+
// object or an array. The host validates each entry (title
|
|
507
|
+
// required, exactly one of url/html, relative URLs rewritten into
|
|
508
|
+
// this plugin's namespace, cross-plugin URLs rejected) and persists
|
|
509
|
+
// the result, so the next /api/config request returns the longer
|
|
510
|
+
// list. Requires 'ui.modify'.
|
|
511
|
+
add(actions) {
|
|
512
|
+
const fns = Host.getFunctions();
|
|
513
|
+
if (!fns.owncast_add_actions)
|
|
514
|
+
throw permError("owncast.actions.add", Permissions.UIModify);
|
|
515
|
+
const list = Array.isArray(actions) ? actions : [actions];
|
|
516
|
+
fns.owncast_add_actions(Memory.fromString(JSON.stringify(list)).offset);
|
|
517
|
+
},
|
|
518
|
+
// Drop the runtime additions so only manifest.actions remain in
|
|
519
|
+
// the effective list on the next /api/config request. Requires
|
|
520
|
+
// 'ui.modify'.
|
|
521
|
+
clear() {
|
|
522
|
+
const fns = Host.getFunctions();
|
|
523
|
+
if (!fns.owncast_clear_actions)
|
|
524
|
+
throw permError("owncast.actions.clear", Permissions.UIModify);
|
|
525
|
+
fns.owncast_clear_actions();
|
|
526
|
+
},
|
|
392
527
|
},
|
|
393
528
|
sse: {
|
|
394
529
|
// send(channel, event, data) pushes one Server-Sent-Event to every
|
|
@@ -401,14 +536,15 @@ const owncast = {
|
|
|
401
536
|
// the 'http.sse' permission.
|
|
402
537
|
send(channel, event, data) {
|
|
403
538
|
const fns = Host.getFunctions();
|
|
404
|
-
if (!fns.owncast_sse_send)
|
|
539
|
+
if (!fns.owncast_sse_send)
|
|
540
|
+
throw new Error(`permission '${Permissions.HttpSSE}' not granted`);
|
|
405
541
|
const payload = typeof data === "string" ? data : JSON.stringify(data);
|
|
406
542
|
fns.owncast_sse_send(
|
|
407
543
|
Memory.fromString(channel || "").offset,
|
|
408
544
|
Memory.fromString(event || "").offset,
|
|
409
|
-
Memory.fromString(payload).offset
|
|
545
|
+
Memory.fromString(payload).offset,
|
|
410
546
|
);
|
|
411
|
-
}
|
|
547
|
+
},
|
|
412
548
|
},
|
|
413
549
|
http: {
|
|
414
550
|
// fetch(url, opts) → { status, headers, body }
|
|
@@ -420,13 +556,17 @@ const owncast = {
|
|
|
420
556
|
const req = {
|
|
421
557
|
url,
|
|
422
558
|
method: opts.method || "GET",
|
|
423
|
-
headers: opts.headers || {}
|
|
559
|
+
headers: opts.headers || {},
|
|
424
560
|
};
|
|
425
561
|
const body = opts.body != null ? String(opts.body) : null;
|
|
426
562
|
const res = body != null ? Http.request(req, body) : Http.request(req);
|
|
427
|
-
return {
|
|
428
|
-
|
|
429
|
-
|
|
563
|
+
return {
|
|
564
|
+
status: res.status,
|
|
565
|
+
headers: res.headers || {},
|
|
566
|
+
body: res.body || "",
|
|
567
|
+
};
|
|
568
|
+
},
|
|
569
|
+
},
|
|
430
570
|
};
|
|
431
571
|
|
|
432
572
|
module.exports = {
|
|
@@ -439,5 +579,5 @@ module.exports = {
|
|
|
439
579
|
describeSubscriptions,
|
|
440
580
|
dispatchEvent,
|
|
441
581
|
dispatchFilter,
|
|
442
|
-
dispatchHttp
|
|
582
|
+
dispatchHttp,
|
|
443
583
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owncast/plugin-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "SDK for authoring Owncast plugins in JavaScript",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Owncast",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"files": [
|
|
29
29
|
"index.js",
|
|
30
30
|
"index.d.ts",
|
|
31
|
+
"testing.js",
|
|
31
32
|
"bin/owncast-plugin.js",
|
|
32
33
|
"scripts/postinstall.js"
|
|
33
34
|
],
|
|
@@ -44,4 +45,4 @@
|
|
|
44
45
|
"publishConfig": {
|
|
45
46
|
"access": "public"
|
|
46
47
|
}
|
|
47
|
-
}
|
|
48
|
+
}
|