@owncast/plugin-sdk 0.1.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/index.d.ts ADDED
@@ -0,0 +1,459 @@
1
+ /** Built-in chat message payload. */
2
+ export interface ChatMessage {
3
+ id: string;
4
+ user: string;
5
+ body: string;
6
+ timestamp: string;
7
+ }
8
+
9
+ /** A chat user — payload of join/part/rename events. */
10
+ export interface ChatUser {
11
+ id: string;
12
+ displayName: string;
13
+ isBot?: boolean;
14
+ isAuthenticated?: boolean;
15
+ scopes?: string[];
16
+ }
17
+
18
+ /** Payload of `chat.user.renamed` — the same user changing their name. */
19
+ export interface ChatUserRename {
20
+ user: ChatUser;
21
+ previousName: string;
22
+ }
23
+
24
+ /** Payload of `chat.message.moderated` — a message hidden/restored by a mod. */
25
+ export interface ChatMessageModeration {
26
+ messageId: string;
27
+ visible: boolean;
28
+ moderator?: ChatUser;
29
+ }
30
+
31
+ /** Stream-lifecycle payloads. */
32
+ export interface StreamLifecycleEvent {
33
+ startedAt?: string; // ISO-8601, set for stream.started
34
+ stoppedAt?: string; // ISO-8601, set for stream.stopped
35
+ title?: string;
36
+ summary?: string;
37
+ }
38
+
39
+ export interface StreamTitleChange {
40
+ from: string;
41
+ to: string;
42
+ }
43
+
44
+ /** What owncast.stream.current() returns. */
45
+ export interface StreamInfo {
46
+ online: boolean;
47
+ title?: string;
48
+ summary?: string;
49
+ viewers: number;
50
+ startedAt?: string;
51
+ latencyLevel?: number;
52
+ }
53
+
54
+ /** What owncast.server.info() returns. */
55
+ export interface ServerInfo {
56
+ name?: string;
57
+ url?: string;
58
+ summary?: string;
59
+ welcomeMessage?: string;
60
+ version?: string;
61
+ }
62
+
63
+ /** What owncast.stream.broadcaster() returns. Empty when offline. */
64
+ export interface StreamBroadcaster {
65
+ remoteAddr?: string;
66
+ codecs?: string[];
67
+ resolution?: string;
68
+ framerate?: number;
69
+ bitrates?: number[];
70
+ }
71
+
72
+ /** One configured output rendition, part of VideoConfig (owncast.videoConfig). */
73
+ export interface StreamVariant {
74
+ width: number;
75
+ height: number;
76
+ framerate: number;
77
+ videoBitrate: number;
78
+ audioBitrate: number;
79
+ isPassthrough: boolean;
80
+ }
81
+
82
+ /** The current video/transcoding config returned by owncast.videoConfig.read(). */
83
+ export interface VideoConfig {
84
+ latencyLevel: number;
85
+ codec: string;
86
+ variants: StreamVariant[];
87
+ }
88
+
89
+ /** Partial video config passed to owncast.videoConfig.write(). Omitted fields
90
+ * are left unchanged. */
91
+ export interface VideoConfigUpdate {
92
+ latencyLevel?: number;
93
+ codec?: string;
94
+ variants?: StreamVariant[];
95
+ }
96
+
97
+ export const FilterAction: {
98
+ readonly Pass: "pass";
99
+ readonly Modify: "modify";
100
+ readonly Drop: "drop";
101
+ };
102
+ export type FilterAction = (typeof FilterAction)[keyof typeof FilterAction];
103
+
104
+ export type FilterResult =
105
+ | { action: typeof FilterAction.Pass }
106
+ | { action: typeof FilterAction.Modify; payload: any }
107
+ | { action: typeof FilterAction.Drop; reason?: string };
108
+
109
+ export const Events: {
110
+ readonly ChatMessageReceived: "chat.message.received";
111
+ readonly ChatUserJoined: "chat.user.joined";
112
+ readonly ChatUserParted: "chat.user.parted";
113
+ readonly ChatUserRenamed: "chat.user.renamed";
114
+ readonly ChatMessageModerated: "chat.message.moderated";
115
+ readonly StreamStarted: "stream.started";
116
+ readonly StreamStopped: "stream.stopped";
117
+ readonly StreamTitleChanged: "stream.title.changed";
118
+ readonly FediverseFollow: "fediverse.follow";
119
+ readonly FediverseLike: "fediverse.like";
120
+ readonly FediverseRepost: "fediverse.repost";
121
+ readonly FediverseMention: "fediverse.mention";
122
+ readonly FediverseReply: "fediverse.reply";
123
+ };
124
+
125
+ /** Payload shape for fediverse engagement events. */
126
+ export interface FediverseActor {
127
+ name: string;
128
+ handle: string; // e.g. "@alice@fediverse.example"
129
+ url?: string;
130
+ image?: string;
131
+ }
132
+
133
+ export interface FediverseEngagement {
134
+ actor: FediverseActor;
135
+ /** For likes and reposts: the target object URL. Not set for follows. */
136
+ target?: { url: string };
137
+ }
138
+
139
+ /** Inbound fediverse post — a mention or reply that contains content the
140
+ * plugin can act on. Carries both the rendered content (which has the
141
+ * source instance's HTML) and a plain-text version (HTML stripped). */
142
+ export interface FediverseInboundPost {
143
+ actor: FediverseActor;
144
+ content: string; // HTML from the source instance
145
+ contentText: string; // HTML stripped to plain text
146
+ url: string; // permalink to the post on its source
147
+ postedAt: string; // ISO-8601
148
+ inReplyTo?: string; // parent post URL, when this is a reply
149
+ attachments?: {
150
+ url: string;
151
+ mediaType: string;
152
+ alt?: string;
153
+ }[];
154
+ language?: string;
155
+ }
156
+
157
+ export const Permissions: {
158
+ readonly ChatSend: "chat.send";
159
+ readonly ChatHistory: "chat.history";
160
+ readonly ChatModerate: "chat.moderate";
161
+ readonly StorageKV: "storage.kv";
162
+ readonly StorageUpload: "storage.upload";
163
+ readonly EventsEmit: "events.emit";
164
+ readonly NetworkFetch: "network.fetch";
165
+ readonly HttpServe: "http.serve";
166
+ readonly ServerRead: "server.read";
167
+ readonly NotificationsSend: "notifications.send";
168
+ readonly UsersRead: "users.read";
169
+ readonly UsersModerate: "users.moderate";
170
+ readonly FediversePost: "fediverse.post";
171
+ readonly HttpSSE: "http.sse";
172
+ readonly VideoConfigRead: "videoconfig.read";
173
+ readonly VideoConfigWrite: "videoconfig.write";
174
+ };
175
+
176
+ export interface BrowserPushPayload {
177
+ title: string;
178
+ body?: string;
179
+ url?: string;
180
+ }
181
+
182
+ export interface FediversePayload {
183
+ type: "follow" | "like" | "repost" | string;
184
+ body: string;
185
+ image?: string;
186
+ link?: string;
187
+ }
188
+
189
+ export interface SocialHandle {
190
+ platform: string;
191
+ url: string;
192
+ icon?: string;
193
+ }
194
+
195
+ export interface FederationInfo {
196
+ enabled: boolean;
197
+ username?: string;
198
+ isPrivate?: boolean;
199
+ }
200
+
201
+ /** A user record from owncast.users.list() / .get(). */
202
+ export interface User {
203
+ id: string;
204
+ displayName: string;
205
+ previousNames?: string[];
206
+ createdAt?: string;
207
+ disabledAt?: string; // ISO-8601 if banned, omitted otherwise
208
+ scopes?: string[];
209
+ isBot?: boolean;
210
+ isAuthenticated?: boolean;
211
+ }
212
+
213
+ /** A connected chat client from owncast.chat.clients(). */
214
+ export interface ChatClient {
215
+ id: number;
216
+ userId?: string;
217
+ displayName?: string;
218
+ connectedAt?: string;
219
+ userAgent?: string;
220
+ ipAddress?: string;
221
+ messageCount: number;
222
+ }
223
+
224
+ /** Result of owncast.storage.upload(). */
225
+ export interface UploadResult {
226
+ url: string;
227
+ }
228
+
229
+ export const filter: {
230
+ pass(): FilterResult;
231
+ modify(payload: any): FilterResult;
232
+ drop(reason?: string): FilterResult;
233
+ };
234
+
235
+ /** Incoming HTTP request, paths are relative to the plugin's namespace
236
+ * (i.e. the leading /plugins/<name>/ has been stripped). */
237
+ export interface IncomingHttpRequest {
238
+ method: string;
239
+ path: string;
240
+ query: Record<string, string>;
241
+ headers: Record<string, string>;
242
+ body: string;
243
+ remoteAddr: string;
244
+ /** True when the request came with any form of Owncast auth (admin OR user). */
245
+ authenticated: boolean;
246
+ /** Identity of the user that made the request, when it came with a
247
+ * user-token. Undefined for anonymous or admin-only requests. */
248
+ user?: ChatUser;
249
+ }
250
+
251
+ export interface OutgoingHttpResponse {
252
+ status?: number;
253
+ headers?: Record<string, string>;
254
+ body?: string;
255
+ }
256
+
257
+ export interface PluginDef {
258
+ /** Notification handler for chat messages. Fire-and-forget. */
259
+ onChatMessage?(msg: ChatMessage): void | Promise<void>;
260
+
261
+ /** Filter handler for chat messages. Return filter.pass() / .modify() / .drop().
262
+ * Errors are treated as filter.pass() (fail-open). */
263
+ filterChatMessage?(msg: ChatMessage): FilterResult;
264
+
265
+ /** User connected to chat. */
266
+ onChatUserJoined?(user: ChatUser): void | Promise<void>;
267
+ /** User disconnected from chat. */
268
+ onChatUserParted?(user: ChatUser): void | Promise<void>;
269
+ /** User changed their display name. */
270
+ onChatUserRenamed?(change: ChatUserRename): void | Promise<void>;
271
+ /** A chat message was hidden or restored by a moderator. */
272
+ onMessageModerated?(event: ChatMessageModeration): void | Promise<void>;
273
+
274
+ /** Stream went live. */
275
+ onStreamStarted?(info: StreamLifecycleEvent): void | Promise<void>;
276
+ /** Stream stopped. */
277
+ onStreamStopped?(info: StreamLifecycleEvent): void | Promise<void>;
278
+ /** Stream title was updated. */
279
+ onStreamTitleChanged?(change: StreamTitleChange): void | Promise<void>;
280
+
281
+ /** Someone on the fediverse followed the streamer's account. */
282
+ onFediverseFollow?(event: FediverseEngagement): void | Promise<void>;
283
+ /** Someone on the fediverse liked a streamer post / federated stream announcement. */
284
+ onFediverseLike?(event: FediverseEngagement): void | Promise<void>;
285
+ /** Someone on the fediverse boosted (reposted) a streamer post. */
286
+ onFediverseRepost?(event: FediverseEngagement): void | Promise<void>;
287
+ /** Someone @-mentioned the streamer in a public post. */
288
+ onFediverseMention?(post: FediverseInboundPost): void | Promise<void>;
289
+ /** Someone replied to one of the streamer's federated posts. */
290
+ onFediverseReply?(post: FediverseInboundPost): void | Promise<void>;
291
+
292
+ /** HTTP request handler. Called for any path under /plugins/<name>/ that
293
+ * isn't served as a static asset. Default-public — gate admin features
294
+ * on `req.authenticated` yourself. Requires `http.serve` permission. */
295
+ onHttpRequest?(req: IncomingHttpRequest): OutgoingHttpResponse;
296
+
297
+ /** Handlers for plugin-emitted custom events. The key is the event type
298
+ * string (e.g. "announcement.broadcast"). Notifications only — to filter
299
+ * custom events, additional API will be needed. */
300
+ on?: { [eventType: string]: (payload: any) => void | Promise<void> };
301
+
302
+ /** Filter chain priority (lower = earlier). Applies to every filter*
303
+ * handler this plugin defines. Default 100. */
304
+ filterPriority?: number;
305
+ }
306
+
307
+ export function definePlugin(def: PluginDef): PluginDef;
308
+
309
+ /** Typed wrappers around the Owncast host. Each method throws if the
310
+ * corresponding permission was not declared in plugin.manifest.json. */
311
+ export const owncast: {
312
+ chat: {
313
+ /** Post as the plugin's own chat bot (display name = the plugin's name). */
314
+ send(text: string): void;
315
+ /** Same identity, but in action style (italic, like IRC "/me"). */
316
+ sendAction(text: string): void;
317
+ /** Post a system message — no user identity, rendered as a server
318
+ * announcement. The body is rendered as HTML, so the plugin is
319
+ * responsible for escaping any untrusted content. Same `chat.send`
320
+ * permission as the other send variants. */
321
+ system(body: string): void;
322
+ /** Private message to one chat client. */
323
+ sendTo(clientId: number | bigint, text: string): void;
324
+ /** Recent chat history (most recent last). Requires `chat.history`.
325
+ * Default limit is 50; pass a smaller number to get fewer. */
326
+ history(limit?: number): ChatMessage[];
327
+ /** Hide a chat message by ID. Requires `chat.moderate`. */
328
+ deleteMessage(messageId: string): void;
329
+ /** Disconnect a chat client by its numeric ID. Requires `chat.moderate`. */
330
+ kick(clientId: number | bigint): void;
331
+ /** List currently-connected chat clients. Requires `chat.history`. */
332
+ clients(): ChatClient[];
333
+ };
334
+ /** User directory access. */
335
+ users: {
336
+ /** List all users (active + disabled). Requires `users.read`. */
337
+ list(): User[];
338
+ /** Fetch one user by ID. Requires `users.read`. */
339
+ get(id: string): User | null;
340
+ /** Enable/disable a user; reason is optional. Requires `users.moderate`. */
341
+ setEnabled(id: string, enabled: boolean, reason?: string): void;
342
+ /** Ban an IP address. Requires `users.moderate`. */
343
+ banIP(ip: string): void;
344
+ };
345
+ /** Upload bytes to Owncast's storage backend (local or S3); returns a
346
+ * public URL. Requires `storage.upload`. */
347
+ storage: {
348
+ upload(name: string, data: Uint8Array | string): UploadResult | null;
349
+ };
350
+ /** Post to the fediverse on the streamer's behalf. Requires `fediverse.post`,
351
+ * which is high-trust — admins should grant it sparingly. The host
352
+ * rate-limits at ~5 posts/hour per plugin. */
353
+ fediverse: {
354
+ /** Publish a public, text-only post. Returns { url } on success or null
355
+ * on rate-limit / disabled / other failure. */
356
+ post(text: string): { url: string } | null;
357
+ };
358
+ /** Send notifications via Owncast's configured channels.
359
+ * Requires `notifications.send`. */
360
+ notifications: {
361
+ /** Post via the Owncast-configured Discord webhook. */
362
+ discord(text: string): void;
363
+ /** Send a browser push notification to subscribed clients. */
364
+ browserPush(payload: string | BrowserPushPayload): void;
365
+ /** Broadcast a fediverse engagement event. */
366
+ fediverse(payload: FediversePayload): void;
367
+ };
368
+ kv: {
369
+ get(key: string): string | null;
370
+ set(key: string, value: string | number): void;
371
+ };
372
+ events: {
373
+ emit(eventType: string, payload: unknown): void;
374
+ };
375
+ sse: {
376
+ /** Push one Server-Sent-Event to every browser connected to this
377
+ * plugin's `/plugins/<name>/_sse/<channel>` stream. `event` is the SSE
378
+ * event name (`""` → the default "message" event); `data` is sent as-is
379
+ * if a string, otherwise JSON-stringified. Fire-and-forget; frames to a
380
+ * slow client are dropped rather than blocking the plugin. Requires the
381
+ * `http.sse` permission. */
382
+ send(channel: string, event: string, data: unknown): void;
383
+ };
384
+ http: {
385
+ fetch(url: string, opts?: HttpRequestOpts): HttpResponse;
386
+ };
387
+ /** Read live stream state + read-only broadcast telemetry. Requires `server.read`. */
388
+ stream: {
389
+ current(): StreamInfo;
390
+ broadcaster(): StreamBroadcaster;
391
+ };
392
+ /** Read server config. Requires `server.read` permission. */
393
+ server: {
394
+ info(): ServerInfo;
395
+ socials(): SocialHandle[];
396
+ federation(): FederationInfo;
397
+ tags(): string[];
398
+ };
399
+ /** Read/change video/transcoding configuration. read() requires
400
+ * `videoconfig.read`; write() requires `videoconfig.write`. */
401
+ videoConfig: {
402
+ read(): VideoConfig;
403
+ write(config: VideoConfigUpdate): void;
404
+ };
405
+ };
406
+
407
+ export interface HttpRequestOpts {
408
+ method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD";
409
+ headers?: Record<string, string>;
410
+ body?: string;
411
+ }
412
+
413
+ export interface HttpResponse {
414
+ status: number;
415
+ headers: Record<string, string>;
416
+ body: string;
417
+ }
418
+
419
+ /** An entry in `manifest.actions` — declares an action button the Owncast
420
+ * UI surfaces while this plugin is enabled. Mirrors Owncast's existing
421
+ * ExternalAction shape; the host merges enabled-plugin buttons with the
422
+ * admin-configured list.
423
+ *
424
+ * Exactly one of `url` or `html` is required.
425
+ *
426
+ * URL ergonomics: if `url` starts with `/` but not `/plugins/`, the host
427
+ * rewrites it to `/plugins/<your-plugin-name>/<path>` at load time, so
428
+ * `"url": "/"` becomes `"/plugins/my-plugin/"`. Absolute http(s) URLs and
429
+ * explicit `/plugins/<your-name>/...` paths are accepted unchanged.
430
+ *
431
+ * When the resolved URL points back into this plugin, the manifest must
432
+ * declare `http.serve` — the host rejects the load otherwise. */
433
+ export interface ActionButton {
434
+ /** Button label. Required. */
435
+ title: string;
436
+ /** Load this URL when the button is pressed. Mutually exclusive with `html`. */
437
+ url?: string;
438
+ /** Render this raw HTML when the button is pressed. Mutually exclusive with `url`. */
439
+ html?: string;
440
+ /** Icon image URL — same path conventions as `url`. */
441
+ icon?: string;
442
+ /** Accent color, e.g. "#3b82f6". */
443
+ color?: string;
444
+ /** Tooltip / longer description. */
445
+ description?: string;
446
+ /** When true, open in a new tab instead of an in-page modal. */
447
+ openExternally?: boolean;
448
+ }
449
+
450
+ /** `manifest.network` — narrows outbound HTTP scope for plugins that
451
+ * declare the `network.fetch` permission. Required when that permission
452
+ * is granted; the host rejects loads otherwise. */
453
+ export interface NetworkConfig {
454
+ /** Hostname globs the plugin can reach via `owncast.http.fetch`.
455
+ * Bare names match exactly (`"api.discord.com"`); `*` is a wildcard
456
+ * segment (`"*.weather.com"`). The bare wildcard `"*"` matches any
457
+ * host but must be written explicitly. */
458
+ allowedHosts: string[];
459
+ }