@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.d.ts
CHANGED
|
@@ -1,34 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Built-in chat message payload (`chat.message.received` and the chat filter).
|
|
3
3
|
*
|
|
4
|
-
* `user` carries the full sender identity
|
|
4
|
+
* `user` carries the full sender identity. Use `user.id` for stable per-user
|
|
5
5
|
* state and `user.scopes` (e.g. `"MODERATOR"`) for reliable, non-spoofable
|
|
6
6
|
* moderation gating rather than matching on the display name. `clientId`
|
|
7
|
-
* identifies the originating connection
|
|
7
|
+
* identifies the originating connection. Pass it to `owncast.chat.sendTo` (or
|
|
8
8
|
* `owncast.chat.replyTo(msg, …)`) to whisper a reply back to the sender.
|
|
9
9
|
*
|
|
10
10
|
* `user` is undefined for the rare message with no associated account.
|
|
11
11
|
*/
|
|
12
12
|
export interface ChatMessage {
|
|
13
13
|
id: string;
|
|
14
|
-
user?:
|
|
14
|
+
user?: User;
|
|
15
15
|
clientId?: number;
|
|
16
16
|
body: string;
|
|
17
17
|
timestamp: string;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
/** A chat user, payload of join/part/rename events. */
|
|
21
|
-
export interface ChatUser {
|
|
22
|
-
id: string;
|
|
23
|
-
displayName: string;
|
|
24
|
-
isBot?: boolean;
|
|
25
|
-
isAuthenticated?: boolean;
|
|
26
|
-
scopes?: string[];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
20
|
/** Payload of `chat.user.renamed`, the same user changing their name. */
|
|
30
21
|
export interface ChatUserRename {
|
|
31
|
-
user:
|
|
22
|
+
user: User;
|
|
32
23
|
previousName: string;
|
|
33
24
|
}
|
|
34
25
|
|
|
@@ -36,7 +27,7 @@ export interface ChatUserRename {
|
|
|
36
27
|
export interface ChatMessageModeration {
|
|
37
28
|
messageId: string;
|
|
38
29
|
visible: boolean;
|
|
39
|
-
moderator?:
|
|
30
|
+
moderator?: User;
|
|
40
31
|
}
|
|
41
32
|
|
|
42
33
|
/** Stream-lifecycle payloads. */
|
|
@@ -80,20 +71,36 @@ export interface StreamBroadcaster {
|
|
|
80
71
|
bitrates?: number[];
|
|
81
72
|
}
|
|
82
73
|
|
|
74
|
+
/** Viewer autoplay behavior. */
|
|
75
|
+
export type AutoplayMode = "off" | "always" | "sound-only";
|
|
76
|
+
|
|
77
|
+
/** H.264 encoder names accepted by Owncast video config writes.
|
|
78
|
+
* Hardware encoders must also be available in ffmpeg. */
|
|
79
|
+
export type VideoCodec =
|
|
80
|
+
| "libx264"
|
|
81
|
+
| "h264_omx"
|
|
82
|
+
| "h264_vaapi"
|
|
83
|
+
| "h264_qsv"
|
|
84
|
+
| "h264_nvenc"
|
|
85
|
+
| "h264_v4l2m2m"
|
|
86
|
+
| "h264_videotoolbox";
|
|
87
|
+
|
|
83
88
|
/** One configured output rendition, part of VideoConfig (owncast.videoConfig). */
|
|
84
89
|
export interface StreamVariant {
|
|
85
90
|
width: number;
|
|
86
91
|
height: number;
|
|
87
92
|
framerate: number;
|
|
88
93
|
videoBitrate: number;
|
|
89
|
-
|
|
94
|
+
cpuUsageLevel: number;
|
|
90
95
|
isPassthrough: boolean;
|
|
91
96
|
}
|
|
92
97
|
|
|
93
98
|
/** The current video/transcoding config returned by owncast.videoConfig.read(). */
|
|
94
99
|
export interface VideoConfig {
|
|
95
100
|
latencyLevel: number;
|
|
101
|
+
/** The configured encoder. Reads may report a legacy or newer host value. */
|
|
96
102
|
codec: string;
|
|
103
|
+
autoplay: AutoplayMode;
|
|
97
104
|
variants: StreamVariant[];
|
|
98
105
|
}
|
|
99
106
|
|
|
@@ -101,7 +108,8 @@ export interface VideoConfig {
|
|
|
101
108
|
* are left unchanged. */
|
|
102
109
|
export interface VideoConfigUpdate {
|
|
103
110
|
latencyLevel?: number;
|
|
104
|
-
codec?:
|
|
111
|
+
codec?: VideoCodec;
|
|
112
|
+
autoplay?: AutoplayMode;
|
|
105
113
|
variants?: StreamVariant[];
|
|
106
114
|
}
|
|
107
115
|
|
|
@@ -129,9 +137,11 @@ export const Events: {
|
|
|
129
137
|
readonly SseConnect: "sse.connect";
|
|
130
138
|
readonly SseDisconnect: "sse.disconnect";
|
|
131
139
|
readonly Tick: "tick";
|
|
140
|
+
readonly FediverseActivity: "fediverse.activity";
|
|
132
141
|
readonly FediverseFollow: "fediverse.follow";
|
|
133
142
|
readonly FediverseLike: "fediverse.like";
|
|
134
143
|
readonly FediverseRepost: "fediverse.repost";
|
|
144
|
+
readonly FediverseQuote: "fediverse.quote";
|
|
135
145
|
readonly FediverseMention: "fediverse.mention";
|
|
136
146
|
readonly FediverseReply: "fediverse.reply";
|
|
137
147
|
};
|
|
@@ -146,10 +156,31 @@ export interface FediverseActor {
|
|
|
146
156
|
|
|
147
157
|
export interface FediverseEngagement {
|
|
148
158
|
actor: FediverseActor;
|
|
149
|
-
/** For likes and
|
|
159
|
+
/** For likes, reposts, and quotes: the target object URL. Not set for follows. */
|
|
150
160
|
target?: { url: string };
|
|
151
161
|
}
|
|
152
162
|
|
|
163
|
+
export interface FediverseTargetedEngagement extends FediverseEngagement {
|
|
164
|
+
target: { url: string };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** An accepted quote request. `target` identifies the local post being quoted,
|
|
168
|
+
* while `url` identifies the remote quote post. Content metadata is present
|
|
169
|
+
* when the requesting server embeds the quote Note in its request. */
|
|
170
|
+
export interface FediverseQuote extends FediverseTargetedEngagement {
|
|
171
|
+
content?: string; // HTML from the source instance
|
|
172
|
+
contentText?: string; // HTML stripped to plain text
|
|
173
|
+
url: string; // permalink to the remote quote post
|
|
174
|
+
postedAt?: string; // ISO-8601
|
|
175
|
+
inReplyTo?: string;
|
|
176
|
+
attachments?: {
|
|
177
|
+
url: string;
|
|
178
|
+
mediaType: string;
|
|
179
|
+
alt?: string;
|
|
180
|
+
}[];
|
|
181
|
+
language?: string;
|
|
182
|
+
}
|
|
183
|
+
|
|
153
184
|
/** Inbound fediverse post, a mention or reply that contains content the
|
|
154
185
|
* plugin can act on. Carries both the rendered content (which has the
|
|
155
186
|
* source instance's HTML) and a plain-text version (HTML stripped). */
|
|
@@ -176,6 +207,7 @@ export const Permissions: {
|
|
|
176
207
|
readonly StorageKV: "storage.kv";
|
|
177
208
|
readonly StorageUpload: "storage.upload";
|
|
178
209
|
readonly StorageFS: "storage.fs";
|
|
210
|
+
readonly StorageSQL: "storage.sql";
|
|
179
211
|
readonly EventsEmit: "events.emit";
|
|
180
212
|
readonly NetworkFetch: "network.fetch";
|
|
181
213
|
readonly HttpServe: "http.serve";
|
|
@@ -183,13 +215,45 @@ export const Permissions: {
|
|
|
183
215
|
readonly NotificationsSend: "notifications.send";
|
|
184
216
|
readonly UsersRead: "users.read";
|
|
185
217
|
readonly UsersModerate: "users.moderate";
|
|
218
|
+
readonly UsersRegister: "users.register";
|
|
219
|
+
readonly AuthGate: "auth.gate";
|
|
186
220
|
readonly FediversePost: "fediverse.post";
|
|
221
|
+
readonly FediverseInbound: "fediverse.inbound";
|
|
187
222
|
readonly HttpSSE: "http.sse";
|
|
188
223
|
readonly VideoConfigRead: "videoconfig.read";
|
|
189
224
|
readonly VideoConfigWrite: "videoconfig.write";
|
|
190
225
|
readonly UIModify: "ui.modify";
|
|
191
226
|
};
|
|
192
227
|
|
|
228
|
+
/** Request for `owncast.users.register`. */
|
|
229
|
+
export interface UserRegisterRequest {
|
|
230
|
+
/** Stable external identity within this plugin's provider namespace. */
|
|
231
|
+
authId: string;
|
|
232
|
+
/** Optional display name to seed on the user. Omit or pass `null` to generate one. */
|
|
233
|
+
displayName?: string | null;
|
|
234
|
+
/** Optional scopes to grant the user (e.g. `["MODERATOR"]`). */
|
|
235
|
+
scopes?: string[];
|
|
236
|
+
/** Verified public profile URL. The host accepts only absolute HTTP(S) URLs. */
|
|
237
|
+
profileUrl?: string;
|
|
238
|
+
/** Label for the verified identity, such as a GitHub login or fediverse handle. */
|
|
239
|
+
handle?: string;
|
|
240
|
+
/** Surface the verified identity publicly only when the viewer opted in. */
|
|
241
|
+
public?: boolean;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Result of `owncast.users.register`: the resolved Owncast user ID. */
|
|
245
|
+
export interface UserRegisterResult {
|
|
246
|
+
userId: string;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Request for `owncast.auth.grantSession`. */
|
|
250
|
+
export interface GrantSessionRequest {
|
|
251
|
+
/** The Owncast user ID returned by `owncast.users.register`. */
|
|
252
|
+
userId: string;
|
|
253
|
+
/** Optional session lifetime in seconds. 0/omitted uses the host default. */
|
|
254
|
+
ttl?: number;
|
|
255
|
+
}
|
|
256
|
+
|
|
193
257
|
export interface BrowserPushPayload {
|
|
194
258
|
title: string;
|
|
195
259
|
body?: string;
|
|
@@ -221,10 +285,14 @@ export interface FederationInfo {
|
|
|
221
285
|
isPrivate?: boolean;
|
|
222
286
|
}
|
|
223
287
|
|
|
224
|
-
/** A user
|
|
288
|
+
/** A user. The sender identity carried by every chat payload
|
|
289
|
+
* (chat.message.received, join/part/rename, moderation) and the record
|
|
290
|
+
* returned by owncast.users.list() / .get(). `displayColor` is an index into
|
|
291
|
+
* the instance's configured user-color palette, not a literal color. */
|
|
225
292
|
export interface User {
|
|
226
293
|
id: string;
|
|
227
294
|
displayName: string;
|
|
295
|
+
displayColor: number;
|
|
228
296
|
previousNames?: string[];
|
|
229
297
|
createdAt?: string;
|
|
230
298
|
disabledAt?: string; // ISO-8601 if banned, omitted otherwise
|
|
@@ -249,19 +317,68 @@ export interface UploadResult {
|
|
|
249
317
|
url: string;
|
|
250
318
|
}
|
|
251
319
|
|
|
252
|
-
/** Result of a mutating owncast.fs call (write/delete).
|
|
253
|
-
* `error` is set when the host rejected the operation. */
|
|
320
|
+
/** Result of a mutating owncast.fs call (write/delete). An empty object means
|
|
321
|
+
* success. `error` is set when the host rejected the operation. */
|
|
254
322
|
export interface FsResult {
|
|
255
|
-
ok: boolean;
|
|
256
323
|
error?: string;
|
|
257
324
|
}
|
|
258
325
|
|
|
326
|
+
/** A value a plugin can bind to a statement parameter, or read back out of a
|
|
327
|
+
* column. Blobs arrive base64-encoded as strings. */
|
|
328
|
+
export type SQLValue = null | boolean | number | string;
|
|
329
|
+
|
|
330
|
+
/** Result of `owncast.sql.exec`. Absence of `error` means success. Both
|
|
331
|
+
* counters are SQLite 64-bit integers, so they lose precision in JavaScript
|
|
332
|
+
* above `Number.MAX_SAFE_INTEGER`. */
|
|
333
|
+
export interface SQLExecResult {
|
|
334
|
+
error?: string;
|
|
335
|
+
rowsAffected: number;
|
|
336
|
+
lastInsertId: number;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** One row as `owncast.sql.query` hands it back: column name to value. */
|
|
340
|
+
export type SQLRow = Record<string, SQLValue>;
|
|
341
|
+
|
|
342
|
+
/** The host's raw query response, before the SDK keys rows by column name.
|
|
343
|
+
* Absence of `error` means success. `rows` holds values in `columns` order,
|
|
344
|
+
* and `truncated` is set when more rows matched than the caller's row limit. */
|
|
345
|
+
export interface SQLQueryResult {
|
|
346
|
+
error?: string;
|
|
347
|
+
columns: string[];
|
|
348
|
+
rows: SQLValue[][];
|
|
349
|
+
truncated?: boolean;
|
|
350
|
+
}
|
|
351
|
+
|
|
259
352
|
export const filter: {
|
|
260
353
|
pass(): FilterResult;
|
|
261
354
|
modify(payload: any): FilterResult;
|
|
262
355
|
drop(reason?: string): FilterResult;
|
|
263
356
|
};
|
|
264
357
|
|
|
358
|
+
/** Request passed to `onAuthCheck`: the host-resolved identity of the viewer
|
|
359
|
+
* whose session is being re-validated (same `user` shape `onHttpRequest`
|
|
360
|
+
* receives, and the plugin never re-resolves it). */
|
|
361
|
+
export interface AuthCheckRequest {
|
|
362
|
+
user: User;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Verdict returned from `onAuthCheck`:
|
|
366
|
+
* - `ok` keep the session as-is
|
|
367
|
+
* - `refresh` keep it and re-issue the cookie (optionally with a new `ttl`
|
|
368
|
+
* in seconds) for sliding-expiry
|
|
369
|
+
* - `deny` end the session and bounce the viewer back to the login screen */
|
|
370
|
+
export type AuthCheckResult =
|
|
371
|
+
| { action: "ok" }
|
|
372
|
+
| { action: "refresh"; ttl?: number }
|
|
373
|
+
| { action: "deny"; reason?: string };
|
|
374
|
+
|
|
375
|
+
/** Verdict helpers for `onAuthCheck`. */
|
|
376
|
+
export const authCheck: {
|
|
377
|
+
ok(): AuthCheckResult;
|
|
378
|
+
refresh(opts?: { ttl?: number }): AuthCheckResult;
|
|
379
|
+
deny(reason?: string): AuthCheckResult;
|
|
380
|
+
};
|
|
381
|
+
|
|
265
382
|
/** Incoming HTTP request, paths are relative to the plugin's namespace
|
|
266
383
|
* (i.e. the leading /plugins/<name>/ has been stripped). */
|
|
267
384
|
export interface IncomingHttpRequest {
|
|
@@ -275,7 +392,7 @@ export interface IncomingHttpRequest {
|
|
|
275
392
|
authenticated: boolean;
|
|
276
393
|
/** Identity of the user that made the request, when it came with a
|
|
277
394
|
* user-token. Undefined for anonymous or admin-only requests. */
|
|
278
|
-
user?:
|
|
395
|
+
user?: User;
|
|
279
396
|
}
|
|
280
397
|
|
|
281
398
|
export interface OutgoingHttpResponse {
|
|
@@ -290,7 +407,7 @@ export interface ContentRequest {
|
|
|
290
407
|
slug: string;
|
|
291
408
|
/** The viewing user's chat identity, when available. Undefined for
|
|
292
409
|
* anonymous viewers or when the host cannot resolve an identity. */
|
|
293
|
-
user?:
|
|
410
|
+
user?: User;
|
|
294
411
|
}
|
|
295
412
|
|
|
296
413
|
/** Payload for the sse.connect / sse.disconnect events. Fired when a browser
|
|
@@ -302,7 +419,7 @@ export interface ContentRequest {
|
|
|
302
419
|
export interface SSEConnectionEvent {
|
|
303
420
|
channel: string;
|
|
304
421
|
connectionId: number;
|
|
305
|
-
user?:
|
|
422
|
+
user?: User;
|
|
306
423
|
}
|
|
307
424
|
|
|
308
425
|
/** Payload for the once-a-second tick event (onTick). `now` is the host
|
|
@@ -312,21 +429,13 @@ export interface TickEvent {
|
|
|
312
429
|
}
|
|
313
430
|
|
|
314
431
|
export interface PluginDef {
|
|
315
|
-
/** Declarative chat
|
|
316
|
-
*
|
|
317
|
-
* canonical command name → definition (run/description/usage/aliases/
|
|
318
|
-
* modOnly/cooldownMs/...); see {@link CommandDefinition}. For advanced
|
|
319
|
-
* composition (e.g. dropping command messages via a filter) use the
|
|
320
|
-
* lower-level {@link defineCommands} router instead. If you also provide
|
|
321
|
-
* onChatMessage, the router runs first and then onChatMessage runs for every
|
|
322
|
-
* message. */
|
|
432
|
+
/** Declarative chat commands with aliases, moderator gates, and per-user
|
|
433
|
+
* cooldowns. Command messages also remain available to onChatMessage. */
|
|
323
434
|
commands?: Record<string, CommandDefinition>;
|
|
324
435
|
/** Command prefix for the `commands` table. Default "!". */
|
|
325
436
|
commandPrefix?: string;
|
|
326
437
|
/** Match command names case-sensitively. Default false. */
|
|
327
438
|
commandsCaseSensitive?: boolean;
|
|
328
|
-
/** Called when a prefixed message matched no command in `commands`. */
|
|
329
|
-
onUnknownCommand?(ctx: CommandContext): void;
|
|
330
439
|
|
|
331
440
|
/** Notification handler for chat messages. Fire-and-forget. */
|
|
332
441
|
onChatMessage?(msg: ChatMessage): void | Promise<void>;
|
|
@@ -336,9 +445,9 @@ export interface PluginDef {
|
|
|
336
445
|
filterChatMessage?(msg: ChatMessage): FilterResult;
|
|
337
446
|
|
|
338
447
|
/** User connected to chat. */
|
|
339
|
-
onChatUserJoined?(user:
|
|
448
|
+
onChatUserJoined?(user: User): void | Promise<void>;
|
|
340
449
|
/** User disconnected from chat. */
|
|
341
|
-
onChatUserParted?(user:
|
|
450
|
+
onChatUserParted?(user: User): void | Promise<void>;
|
|
342
451
|
/** User changed their display name. */
|
|
343
452
|
onChatUserRenamed?(change: ChatUserRename): void | Promise<void>;
|
|
344
453
|
/** A chat message was hidden or restored by a moderator. */
|
|
@@ -362,15 +471,20 @@ export interface PluginDef {
|
|
|
362
471
|
* in unix milliseconds. Defining this opts the plugin into the tick. */
|
|
363
472
|
onTick?(event: TickEvent): void | Promise<void>;
|
|
364
473
|
|
|
365
|
-
/**
|
|
474
|
+
/** A verified inbound ActivityPub activity as its raw JSON object. Requires `fediverse.inbound`. */
|
|
475
|
+
onFediverse?(activity: Record<string, unknown>): void | Promise<void>;
|
|
476
|
+
|
|
477
|
+
/** Someone on the fediverse followed the streamer's account. Requires `fediverse.inbound`. */
|
|
366
478
|
onFediverseFollow?(event: FediverseEngagement): void | Promise<void>;
|
|
367
|
-
/** Someone on the fediverse liked a streamer post / federated stream announcement. */
|
|
368
|
-
onFediverseLike?(event:
|
|
369
|
-
/** Someone on the fediverse boosted (reposted) a streamer post. */
|
|
370
|
-
onFediverseRepost?(event:
|
|
371
|
-
/** Someone
|
|
479
|
+
/** Someone on the fediverse liked a streamer post / federated stream announcement. Requires `fediverse.inbound`. */
|
|
480
|
+
onFediverseLike?(event: FediverseTargetedEngagement): void | Promise<void>;
|
|
481
|
+
/** Someone on the fediverse boosted (reposted) a streamer post. Requires `fediverse.inbound`. */
|
|
482
|
+
onFediverseRepost?(event: FediverseTargetedEngagement): void | Promise<void>;
|
|
483
|
+
/** Someone on the fediverse quoted a locally authored post. `target.url` identifies the local post and `url` identifies the remote quote post. Requires `fediverse.inbound`. */
|
|
484
|
+
onFediverseQuote?(event: FediverseQuote): void | Promise<void>;
|
|
485
|
+
/** Someone @-mentioned the streamer in a public post. Requires `fediverse.inbound`. */
|
|
372
486
|
onFediverseMention?(post: FediverseInboundPost): void | Promise<void>;
|
|
373
|
-
/** Someone replied to one of the streamer's federated posts. */
|
|
487
|
+
/** Someone replied to one of the streamer's federated posts. Requires `fediverse.inbound`. */
|
|
374
488
|
onFediverseReply?(post: FediverseInboundPost): void | Promise<void>;
|
|
375
489
|
|
|
376
490
|
/** HTTP request handler. Called for any path under /plugins/<name>/ that
|
|
@@ -378,6 +492,15 @@ export interface PluginDef {
|
|
|
378
492
|
* on `req.authenticated` yourself. Requires `http.serve` permission. */
|
|
379
493
|
onHttpRequest?(req: IncomingHttpRequest): OutgoingHttpResponse;
|
|
380
494
|
|
|
495
|
+
/** Re-validate a viewer's gate session on page load. Only meaningful for the
|
|
496
|
+
* active `auth.gate` plugin: the host calls it on the viewer's `/` request
|
|
497
|
+
* with the resolved `req.user`, and acts on the verdict: `ok` to continue,
|
|
498
|
+
* `refresh` to extend the session, `deny` to revoke it and force re-login.
|
|
499
|
+
* Optional. Without it a granted session lasts until its cookie
|
|
500
|
+
* expires (no mid-session revocation). This is the revocation hook: return
|
|
501
|
+
* `deny` for users your provider has banned/deleted. Requires `auth.gate`. */
|
|
502
|
+
onAuthCheck?(req: AuthCheckRequest): AuthCheckResult;
|
|
503
|
+
|
|
381
504
|
/** Render HTML for a dynamic tab. Called by the host when the tab was
|
|
382
505
|
* declared in the manifest without a static `content` file. Return the
|
|
383
506
|
* full HTML string to inline as the tab body. `req.user` is the viewer's
|
|
@@ -390,9 +513,32 @@ export interface PluginDef {
|
|
|
390
513
|
* `req.user` is the viewer's chat identity when available. */
|
|
391
514
|
onPageContent?(req: ContentRequest): string;
|
|
392
515
|
|
|
393
|
-
/**
|
|
394
|
-
*
|
|
395
|
-
*
|
|
516
|
+
/** Return CSS to inline into the viewer page at request time, the dynamic
|
|
517
|
+
* counterpart to `manifest.styles`, applied to the whole UI. Called once
|
|
518
|
+
* per `/api/config` for any plugin holding `ui.modify`. No manifest field
|
|
519
|
+
* is needed, just export this handler. Return nothing (a bare `return`, or
|
|
520
|
+
* `""`) to contribute nothing. The output is appended after any static
|
|
521
|
+
* `manifest.styles` files, so returning only the active override wins
|
|
522
|
+
* within your plugin's own styles. Plugin styles sit below the admin's
|
|
523
|
+
* appearance settings, so an admin's explicit colors override yours.
|
|
524
|
+
* Global (no per-viewer argument) so `/api/config` stays cacheable.
|
|
525
|
+
* Requires `ui.modify`. */
|
|
526
|
+
onPageStyles?(): string | null | void;
|
|
527
|
+
|
|
528
|
+
/** Return JavaScript to append to the viewer page at request time, the
|
|
529
|
+
* dynamic counterpart to `manifest.scripts`. Called once per `/api/config`
|
|
530
|
+
* for any plugin holding `ui.modify`. The host wraps each plugin's script
|
|
531
|
+
* (static and dynamic) in a try/catch so a runtime error can't break other
|
|
532
|
+
* plugins, but it runs in the shared viewer `window`: wrap your code in an
|
|
533
|
+
* IIFE to avoid global collisions, and escape any untrusted strings you
|
|
534
|
+
* embed. Return nothing (a bare `return`, or `""`) to contribute nothing.
|
|
535
|
+
* Requires `ui.modify`. */
|
|
536
|
+
onPageScripts?(): string | null | void;
|
|
537
|
+
|
|
538
|
+
/** Handlers for custom events owned by this plugin. Keys are local hook names
|
|
539
|
+
* such as "announcement.broadcast". The host registers each hook as
|
|
540
|
+
* `<your-slug>.<hook>`, which emitters use as the target event type.
|
|
541
|
+
* Notifications only. Filtering custom events requires additional API. */
|
|
396
542
|
on?: { [eventType: string]: (payload: any) => void | Promise<void> };
|
|
397
543
|
|
|
398
544
|
/** Filter chain priority (lower = earlier). Applies to every filter*
|
|
@@ -407,65 +553,57 @@ export interface CommandContext {
|
|
|
407
553
|
/** The originating chat message. */
|
|
408
554
|
msg: ChatMessage;
|
|
409
555
|
/** The sender (same as `msg.user`). */
|
|
410
|
-
user?:
|
|
556
|
+
user?: User;
|
|
411
557
|
/** The canonical command name that matched (not the alias used). */
|
|
412
558
|
command: string;
|
|
559
|
+
/** The command name or alias exactly as the sender typed it. */
|
|
560
|
+
invokedAs: string;
|
|
413
561
|
/** Whitespace-split arguments after the command word. */
|
|
414
562
|
args: string[];
|
|
415
563
|
/** The raw argument string (everything after the command word, trimmed). */
|
|
416
564
|
argString: string;
|
|
417
565
|
/** Post a public reply as the plugin's chat bot. */
|
|
418
566
|
reply(text: string): void;
|
|
419
|
-
/** Whisper a reply to the sender
|
|
567
|
+
/** Whisper a reply to the sender, falling back to a public post if their
|
|
420
568
|
* connection is unknown. */
|
|
421
569
|
replyPrivately(text: string): void;
|
|
422
570
|
}
|
|
423
571
|
|
|
424
|
-
/** One command in a
|
|
572
|
+
/** One command in a declarative command table. */
|
|
425
573
|
export interface CommandDefinition {
|
|
426
|
-
/** Short, human-readable summary
|
|
427
|
-
* command listings (e.g. a future `!help`); ignored by the router itself. */
|
|
574
|
+
/** Short, human-readable summary shown in command listings. */
|
|
428
575
|
description?: string;
|
|
429
576
|
/** Optional usage/example string, e.g. "!latency <0-4>". */
|
|
430
577
|
usage?: string;
|
|
431
578
|
/** Alternate names that invoke this command. */
|
|
432
579
|
aliases?: string[];
|
|
433
|
-
/**
|
|
580
|
+
/** Dispatch only for senders whose scopes include "MODERATOR". */
|
|
434
581
|
modOnly?: boolean;
|
|
435
|
-
/**
|
|
436
|
-
* `msg.timestamp`). */
|
|
582
|
+
/** Non-negative integer milliseconds between invocations per user. */
|
|
437
583
|
cooldownMs?: number;
|
|
438
584
|
/** Invoked when the command runs. */
|
|
439
585
|
run(ctx: CommandContext): void;
|
|
440
|
-
/** Invoked instead of `run` when a non-moderator calls a `modOnly` command. */
|
|
441
|
-
onDenied?(ctx: CommandContext): void;
|
|
442
|
-
/** Invoked instead of `run` when the per-user cooldown hasn't elapsed. */
|
|
443
|
-
onCooldown?(ctx: CommandContext): void;
|
|
444
586
|
}
|
|
445
587
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
/** Build a chat-command router (prefix parsing, aliases, per-user cooldowns,
|
|
460
|
-
* moderator gating). Feed the returned function a `ChatMessage`; it returns
|
|
461
|
-
* true when the message was a command (even if gated), false otherwise. */
|
|
462
|
-
export function defineCommands(
|
|
463
|
-
config: CommandsConfig,
|
|
464
|
-
): (msg: ChatMessage) => boolean;
|
|
465
|
-
|
|
466
|
-
/** Typed wrappers around the Owncast host. Each method throws if the
|
|
467
|
-
* corresponding permission was not declared in plugin.manifest.json. */
|
|
588
|
+
/** Internal payload for a matched command declaration. */
|
|
589
|
+
export interface CommandEvent {
|
|
590
|
+
message: ChatMessage;
|
|
591
|
+
command: string;
|
|
592
|
+
invokedAs: string;
|
|
593
|
+
args: string[];
|
|
594
|
+
argString: string;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/** Typed wrappers around the Owncast host. Methods that require a permission
|
|
598
|
+
* say so in their documentation and throw when it was not declared. */
|
|
468
599
|
export const owncast: {
|
|
600
|
+
/** Write a plugin-attributed entry to Owncast's server log. No permission
|
|
601
|
+
* is required. */
|
|
602
|
+
log: {
|
|
603
|
+
info(message: string): void;
|
|
604
|
+
warning(message: string): void;
|
|
605
|
+
error(message: string): void;
|
|
606
|
+
};
|
|
469
607
|
chat: {
|
|
470
608
|
/** Post as the plugin's own chat bot (display name = the plugin's name). */
|
|
471
609
|
send(text: string): void;
|
|
@@ -485,11 +623,13 @@ export const owncast: {
|
|
|
485
623
|
* `chat.send`. */
|
|
486
624
|
replyTo(msg: ChatMessage | number | bigint, text: string): boolean;
|
|
487
625
|
/** Recent chat history (most recent last). Requires `chat.history`.
|
|
488
|
-
* Default limit is 50
|
|
626
|
+
* Default limit is 50. Pass a smaller number to get fewer. */
|
|
489
627
|
history(limit?: number): ChatMessage[];
|
|
490
|
-
/** Hide a chat message by ID.
|
|
628
|
+
/** Hide a chat message by ID. Throws when the host rejects the operation.
|
|
629
|
+
* Requires `chat.moderate`. */
|
|
491
630
|
deleteMessage(messageId: string): void;
|
|
492
|
-
/** Disconnect a chat client by its numeric ID.
|
|
631
|
+
/** Disconnect a chat client by its numeric ID. Throws when the host rejects
|
|
632
|
+
* the operation. Requires `chat.moderate`. */
|
|
493
633
|
kick(clientId: number | bigint): void;
|
|
494
634
|
/** List currently-connected chat clients. Requires `chat.history`. */
|
|
495
635
|
clients(): ChatClient[];
|
|
@@ -500,17 +640,39 @@ export const owncast: {
|
|
|
500
640
|
list(): User[];
|
|
501
641
|
/** Fetch one user by ID. Requires `users.read`. */
|
|
502
642
|
get(id: string): User | null;
|
|
503
|
-
/** Enable
|
|
643
|
+
/** Enable or disable a user, with an optional reason. Throws when the host
|
|
644
|
+
* rejects the operation. Requires `users.moderate`. */
|
|
504
645
|
setEnabled(id: string, enabled: boolean, reason?: string): void;
|
|
505
|
-
/** Ban an IP address.
|
|
646
|
+
/** Ban an IP address. Throws when the host rejects the operation. Requires
|
|
647
|
+
* `users.moderate`. */
|
|
506
648
|
banIP(ip: string): void;
|
|
649
|
+
/** Find or create an authenticated user for an external identity. The host
|
|
650
|
+
* scopes `authId` to this plugin's slug. `profileUrl` and `handle`
|
|
651
|
+
* describe a verified external profile, and `public` opts that identity
|
|
652
|
+
* into public display. Returns `{ userId }`. Throws on host error.
|
|
653
|
+
* Requires `users.register`. */
|
|
654
|
+
register(opts: UserRegisterRequest | string): UserRegisterResult;
|
|
655
|
+
};
|
|
656
|
+
/** Viewer-authentication gate. Only a plugin holding `auth.gate` (and enabled
|
|
657
|
+
* by an admin) can issue sessions, and only inside `onHttpRequest`, where the
|
|
658
|
+
* host attaches or clears the signed session cookie on the response. The
|
|
659
|
+
* admin selects the cumulative, host-owned access mode. Plugins cannot read
|
|
660
|
+
* or change it. */
|
|
661
|
+
auth: {
|
|
662
|
+
/** Issue a gate session for an already-registered user (see
|
|
663
|
+
* `users.register`). `ttl` is optional seconds (0/omitted = host default).
|
|
664
|
+
* Throws on host error. Requires `auth.gate`. */
|
|
665
|
+
grantSession(opts: GrantSessionRequest | string): void;
|
|
666
|
+
/** Clear the current viewer's gate session (logout). The plugin still owns
|
|
667
|
+
* the response/redirect. Requires `auth.gate`. */
|
|
668
|
+
endSession(): void;
|
|
507
669
|
};
|
|
508
|
-
/** Upload bytes to Owncast's storage backend (local or S3)
|
|
670
|
+
/** Upload bytes to Owncast's storage backend (local or S3). Returns a
|
|
509
671
|
* public URL. Requires `storage.upload`. */
|
|
510
672
|
storage: {
|
|
511
673
|
upload(name: string, data: Uint8Array | string): UploadResult | null;
|
|
512
674
|
};
|
|
513
|
-
/** Private, sandboxed filesystem under data/plugin-
|
|
675
|
+
/** Private, sandboxed filesystem under data/plugin-storage/<slug>/files/. The bytes
|
|
514
676
|
* stay server-side (never served over HTTP) and the host confines every
|
|
515
677
|
* path to this plugin's own directory. All methods require `storage.fs`. */
|
|
516
678
|
fs: {
|
|
@@ -520,15 +682,38 @@ export const owncast: {
|
|
|
520
682
|
readText(path: string): string | null;
|
|
521
683
|
/** Write bytes or a string, creating parent directories as needed. */
|
|
522
684
|
write(path: string, data: Uint8Array | string): FsResult;
|
|
523
|
-
/** List entry names directly inside dir
|
|
685
|
+
/** List entry names directly inside dir. A missing dir lists as empty. */
|
|
524
686
|
list(dir: string): string[];
|
|
525
687
|
/** Remove a single file or empty directory. */
|
|
526
688
|
delete(path: string): FsResult;
|
|
527
689
|
/** Report whether a path exists inside the sandbox. */
|
|
528
690
|
exists(path: string): boolean;
|
|
529
691
|
};
|
|
692
|
+
/** Private SQLite database, one per plugin, stored in `db/` next to the
|
|
693
|
+
* `storage.fs` sandbox in `files/`, outside anything `owncast.fs.*` can name,
|
|
694
|
+
* and quota'd separately. Every call runs with a 2 second timeout. Absence
|
|
695
|
+
* of `error` means success. An error, missing response, or non-object
|
|
696
|
+
* response throws. JavaScript loses unsafe integers before `JSON.stringify`
|
|
697
|
+
* on writes and during `JSON.parse` on reads, so store values above
|
|
698
|
+
* `Number.MAX_SAFE_INTEGER` (2^53 - 1) as TEXT when they must remain exact.
|
|
699
|
+
* Requires `storage.sql`. */
|
|
700
|
+
sql: {
|
|
701
|
+
/** Execute one statement batch as a single transaction: it commits whole
|
|
702
|
+
* or leaves the database untouched. A transaction cannot stay open
|
|
703
|
+
* across calls. */
|
|
704
|
+
exec(sql: string, params?: SQLValue[]): SQLExecResult;
|
|
705
|
+
/** Query rows as objects keyed by column name. Alias duplicate columns.
|
|
706
|
+
* The result is never silently shortened: a query returning more than
|
|
707
|
+
* 10000 rows, or more than 1 MiB of encoded data, throws asking for a
|
|
708
|
+
* LIMIT. */
|
|
709
|
+
query(sql: string, params?: SQLValue[]): SQLRow[];
|
|
710
|
+
/** Return the first matching row, or null. Only that row is read back, so
|
|
711
|
+
* this stays under the result budget on a table `query` is too big
|
|
712
|
+
* for. */
|
|
713
|
+
queryRow(sql: string, params?: SQLValue[]): SQLRow | null;
|
|
714
|
+
};
|
|
530
715
|
/** Post to the fediverse on the streamer's behalf. Requires `fediverse.post`,
|
|
531
|
-
* which is high-trust (posts go out under the streamer's own handle)
|
|
716
|
+
* which is high-trust (posts go out under the streamer's own handle), so
|
|
532
717
|
* admins should grant it sparingly. */
|
|
533
718
|
fediverse: {
|
|
534
719
|
/** Publish a public, text-only post. Returns `{ url }` (currently empty
|
|
@@ -549,6 +734,7 @@ export const owncast: {
|
|
|
549
734
|
};
|
|
550
735
|
kv: {
|
|
551
736
|
get(key: string): string | null;
|
|
737
|
+
/** Store a value. Throws when the host rejects the operation. */
|
|
552
738
|
set(key: string, value: string | number): void;
|
|
553
739
|
/** Read a JSON value, parsed. Returns `fallback` (default `undefined`)
|
|
554
740
|
* when the key is unset or holds invalid JSON. Requires `storage.kv`. */
|
|
@@ -557,7 +743,7 @@ export const owncast: {
|
|
|
557
743
|
setJSON(key: string, value: unknown): void;
|
|
558
744
|
};
|
|
559
745
|
/** Read this plugin's admin-configurable settings, declared under
|
|
560
|
-
* `config` in the manifest. Ambient
|
|
746
|
+
* `config` in the manifest. Ambient, so no permission is required. */
|
|
561
747
|
config: {
|
|
562
748
|
/** The effective value of a manifest-declared config key (admin override,
|
|
563
749
|
* else the declared default), parsed to its declared type. Returns
|
|
@@ -565,10 +751,10 @@ export const owncast: {
|
|
|
565
751
|
* value. */
|
|
566
752
|
get<T = unknown>(key: string, fallback?: T): T;
|
|
567
753
|
};
|
|
568
|
-
/** Read files the plugin bundled in its own `assets/` directory
|
|
754
|
+
/** Read files the plugin bundled in its own `assets/` directory: templates,
|
|
569
755
|
* data files, and other bundled resources loaded at request time. Path is
|
|
570
|
-
* relative to `assets/` and must not contain `..`. Ambient
|
|
571
|
-
* required. */
|
|
756
|
+
* relative to `assets/` and must not contain `..`. Ambient, so no permission
|
|
757
|
+
* is required. */
|
|
572
758
|
assets: {
|
|
573
759
|
/** Raw bytes of the file, or `null` if not found. */
|
|
574
760
|
read(path: string): Uint8Array | null;
|
|
@@ -576,6 +762,8 @@ export const owncast: {
|
|
|
576
762
|
readText(path: string): string | null;
|
|
577
763
|
};
|
|
578
764
|
events: {
|
|
765
|
+
/** Emit to a custom hook using its fully qualified
|
|
766
|
+
* `<recipient-plugin-slug>.<hook>` name. Requires `events.emit`. */
|
|
579
767
|
emit(eventType: string, payload: unknown): void;
|
|
580
768
|
};
|
|
581
769
|
/** Control over the viewer action buttons this plugin contributes.
|
|
@@ -585,30 +773,30 @@ export const owncast: {
|
|
|
585
773
|
actions: {
|
|
586
774
|
/** Append one or more buttons to the plugin's runtime list. Each
|
|
587
775
|
* entry is validated with the same rules as `manifest.actions`
|
|
588
|
-
* (title required
|
|
589
|
-
* rewritten into this plugin's namespace
|
|
590
|
-
* rejected).
|
|
591
|
-
* `manifest.actions` ++ the runtime list. */
|
|
776
|
+
* (title required, exactly one of `url` or `html`, relative URLs
|
|
777
|
+
* rewritten into this plugin's namespace, cross-plugin URLs
|
|
778
|
+
* rejected). Throws when the host rejects the action list. */
|
|
592
779
|
add(actions: ActionButton | ActionButton[]): void;
|
|
593
|
-
/** Drop the runtime additions
|
|
594
|
-
* the next viewer `/api/config` request.
|
|
780
|
+
/** Drop the runtime additions, so only `manifest.actions` remain on
|
|
781
|
+
* the next viewer `/api/config` request. Throws when the host rejects the
|
|
782
|
+
* operation. */
|
|
595
783
|
clear(): void;
|
|
596
784
|
};
|
|
597
785
|
sse: {
|
|
598
786
|
/** Push one Server-Sent-Event to every browser connected to this
|
|
599
787
|
* plugin's `/plugins/<name>/_sse/<channel>` stream. `event` is the SSE
|
|
600
|
-
* event name (`""` → the default "message" event)
|
|
601
|
-
* if a string, otherwise JSON-stringified. Fire-and-forget
|
|
788
|
+
* event name (`""` → the default "message" event). `data` is sent as-is
|
|
789
|
+
* if a string, otherwise JSON-stringified. Fire-and-forget, and frames to a
|
|
602
790
|
* slow client are dropped rather than blocking the plugin. Requires the
|
|
603
791
|
* `http.sse` permission. */
|
|
604
792
|
send(channel: string, event: string, data: unknown): void;
|
|
605
793
|
};
|
|
606
|
-
/** Host-driven timers. The sandbox has no setTimeout
|
|
794
|
+
/** Host-driven timers. The sandbox has no setTimeout, so these ask the host to
|
|
607
795
|
* call your callback back later (in this instance). No permission required.
|
|
608
796
|
* Timers do not survive a plugin reload or host restart. */
|
|
609
797
|
timer: {
|
|
610
798
|
/** Run `fn` once after ~`ms` milliseconds. Returns an id for `clear()`.
|
|
611
|
-
* Very small delays are clamped up by the host
|
|
799
|
+
* Very small delays are clamped up by the host. Throws past the
|
|
612
800
|
* per-plugin pending-timer cap. */
|
|
613
801
|
setTimeout(fn: () => void, ms: number): number;
|
|
614
802
|
/** Run `fn` every ~`ms` milliseconds until `clear()`. The next run is
|
|
@@ -635,7 +823,8 @@ export const owncast: {
|
|
|
635
823
|
tags(): string[];
|
|
636
824
|
};
|
|
637
825
|
/** Read/change video/transcoding configuration. read() requires
|
|
638
|
-
* `videoconfig.read
|
|
826
|
+
* `videoconfig.read`. write() requires `videoconfig.write` and throws when
|
|
827
|
+
* the host rejects the update or does not return an operation result. */
|
|
639
828
|
videoConfig: {
|
|
640
829
|
read(): VideoConfig;
|
|
641
830
|
write(config: VideoConfigUpdate): void;
|
|
@@ -656,7 +845,7 @@ export interface HttpResponse {
|
|
|
656
845
|
|
|
657
846
|
/** An entry in `manifest.actions`, declares an action button the Owncast
|
|
658
847
|
* UI surfaces while this plugin is enabled. Mirrors Owncast's existing
|
|
659
|
-
* ExternalAction shape
|
|
848
|
+
* ExternalAction shape. The host merges enabled-plugin buttons with the
|
|
660
849
|
* admin-configured list.
|
|
661
850
|
*
|
|
662
851
|
* Exactly one of `url` or `html` is required.
|
|
@@ -687,11 +876,46 @@ export interface ActionButton {
|
|
|
687
876
|
|
|
688
877
|
/** `manifest.network`, narrows outbound HTTP scope for plugins that
|
|
689
878
|
* declare the `network.fetch` permission. Required when that permission
|
|
690
|
-
* is granted
|
|
879
|
+
* is granted. The host rejects loads otherwise. */
|
|
691
880
|
export interface NetworkConfig {
|
|
692
881
|
/** Hostname globs the plugin can reach via `owncast.http.fetch`.
|
|
693
|
-
* Bare names match exactly (`"api.discord.com"`)
|
|
882
|
+
* Bare names match exactly (`"api.discord.com"`), and `*` is a wildcard
|
|
694
883
|
* segment (`"*.weather.com"`). The bare wildcard `"*"` matches any
|
|
695
884
|
* host but must be written explicitly. */
|
|
696
885
|
allowedHosts: string[];
|
|
697
886
|
}
|
|
887
|
+
|
|
888
|
+
/** `manifest.category` (optional), the plugin's registry browse category.
|
|
889
|
+
* One of the canonical slugs below. The plugin registry uses it to filter
|
|
890
|
+
* the browse listing; unknown values are tolerated but won't match any
|
|
891
|
+
* filter.
|
|
892
|
+
*
|
|
893
|
+
* - `chat-bots`: Chat bots
|
|
894
|
+
* - `chat-filters`: Chat filters
|
|
895
|
+
* - `moderation`: Moderation
|
|
896
|
+
* - `authentication`: Authentication
|
|
897
|
+
* - `themes`: Themes
|
|
898
|
+
* - `overlays`: Overlays & widgets
|
|
899
|
+
* - `notifications`: Notifications
|
|
900
|
+
* - `integrations`: Integrations
|
|
901
|
+
* - `video`: Video & streaming
|
|
902
|
+
* - `analytics`: Analytics & stats
|
|
903
|
+
* - `games`: Games & fun
|
|
904
|
+
* - `admin-utilities`: Admin utilities
|
|
905
|
+
* - `examples`: Examples
|
|
906
|
+
* - `other`: Other */
|
|
907
|
+
export type PluginCategory =
|
|
908
|
+
| "chat-bots"
|
|
909
|
+
| "chat-filters"
|
|
910
|
+
| "moderation"
|
|
911
|
+
| "authentication"
|
|
912
|
+
| "themes"
|
|
913
|
+
| "overlays"
|
|
914
|
+
| "notifications"
|
|
915
|
+
| "integrations"
|
|
916
|
+
| "video"
|
|
917
|
+
| "analytics"
|
|
918
|
+
| "games"
|
|
919
|
+
| "admin-utilities"
|
|
920
|
+
| "examples"
|
|
921
|
+
| "other";
|