---@meta
-- ForgeCast plugin API 1.
--
-- This file is for your editor, not for running: it describes every global a ForgeCast plugin can
-- use, so a Lua language server (the "Lua" extension by sumneko in VS Code) can complete names,
-- show these notes while you type, and warn about mistakes. ForgeCast puts a copy of it in every
-- plugin's `.forgecast/` folder.
--
-- A plugin is a folder with a `plugin.json` and a `main.lua`. Everything it can do beyond plain
-- Lua is below, and most of it needs a permission listed in plugin.json ("permissions").

------------------------------------------------------------------------------------------------
-- chat
------------------------------------------------------------------------------------------------

---@alias ChatKind
---| "text"          an ordinary message
---| "super_chat"    a Super Chat (has `amount`)
---| "super_sticker" a Super Sticker (has `amount`)
---| "member"        a new member
---| "milestone"     a member milestone ("Member for 6 months")
---| "gift"          memberships gifted to the community (has `count`)
---| "gift_received" someone received a gifted membership
---| "jewel"         a gift sent in chat

---@class ChatAuthor
---@field id string                YouTube channel id of the viewer
---@field name string              display name, usually "@handle"
---@field avatar? string           picture address
---@field is_owner boolean         the streamer
---@field is_mod boolean           a moderator
---@field is_member boolean        a channel member
---@field is_verified boolean      verified
---@field member_label? string     "Member (1 year)"
---@field member_badge? string     picture address of the channel's member badge

---@class Money
---@field value number
---@field currency string          ISO code like "USD", "INR"

--- One line of chat, as plugins see it.
---@class Chat
---@field id string
---@field stream string            the video id of the stream it came from
---@field stream_title? string
---@field ts integer               when it was sent, milliseconds since 1970
---@field kind ChatKind
---@field text string              the message as plain text (custom emoji appear as :shortcode:)
---@field author ChatAuthor
---@field amount? Money            Super Chats and Super Stickers
---@field amount_text? string      the amount as YouTube shows it, like "$5.00"
---@field headline? string         "Welcome to the channel!", "Member for 6 months"…
---@field count? integer           memberships in a gift
---@field color? string            a Super Chat's colour, "#rrggbb"
---@field backlog boolean          true for the history read when a stream is first opened
---@field simulated boolean        true for chat from the plugin test environment

--- Narrowing which chat you mean (every field is optional).
---@class ChatQuery
---@field limit? integer           at most this many (default 50, up to 300), the newest
---@field stream? string           only this stream (a video id)
---@field kind? ChatKind|ChatKind[]
---@field author? string           a name ("@ann" or "ann") or a channel id
---@field since? number            only from the last N seconds
---@field members_only? boolean    members, moderators and the streamer
---@field contains? string         text to look for (ignores case)
---@field exclude? string[]        names or channel ids to leave out
---@field count? integer           for chats.random: how many to pick (changes the result to a list)
---@field unique_author? boolean   for chats.random: one line per person so chatty viewers don't win more often (default true)

---@class ChatEvents
---@field NewChat "chat"                 every chat line (after middleware)
---@field SuperChat "chat.super_chat"
---@field SuperSticker "chat.super_sticker"
---@field Member "chat.member"
---@field Milestone "chat.milestone"
---@field Gift "chat.gift"
---@field Jewel "chat.jewel"
---@field ChatRemoved "chat.removed"     a line was deleted or an author banned: handler gets { stream, ids, authors }
---@field StreamUpdate "stream.update"   viewers/likes changed: handler gets a StreamState
---@field StreamAdded "stream.added"
---@field StreamRemoved "stream.removed" handler gets { id }
---@field StreamStart "stream.start"     a stream went live
---@field StreamEnd "stream.end"         a stream ended
---@field ObsScene "obs.scene"           OBS switched scenes: handler gets { scene, previous }
---@field ObsStreaming "obs.streaming"   OBS started or stopped streaming: handler gets { streaming }
---@field ObsRecording "obs.recording"   OBS started or stopped recording: handler gets { recording }
---@field ObsConnected "obs.connected"   ForgeCast connected to OBS or lost it: handler gets { connected }
---@field StreamRecorded "history.recorded" a stream ended and was recorded on this PC: handler gets a PastStream
---@field ObsTalking "obs.talking"      an input started or stopped hearing a voice: handler gets { input, talking, kind }
---@field SystemBusy "system.busy"      the PC stayed over a system.watch limit (system.on): { reasons, usage }
---@field SystemCalm "system.calm"      …and came back under every limit: { usage }
---@field Away "activity.away"          no key press or mouse move for a while (activity.on): { idle_ms }
---@field Back "activity.back"          a key press or mouse move after being away (activity.on)
---@field GameChange "activity.game"    a different game (or none) is being played (activity.on): { game, previous }
---@field SongChange "media.song"       a different song is playing (media.on): handler gets media.now_playing()
---@field Subscriber "events.subscriber.new"  a new subscriber (events.on): handler gets an AppEvent
---@field PointsChange "points.change"  a viewer's points changed (points.on): { user, name, points, delta, reason }
---@field GoalChange "goals.change"     a goal changed (goals.on): handler gets the Goal
---@field Tip "donations.tip"         a tip from Ko-fi, Streamlabs or StreamElements (donations.on): handler gets a Tip
---@field Midi "midi.message"         a MIDI message (midi.on): handler gets a MidiMessage
---@field Captions "captions.text"     what the streamer said (captions.on): { text, final }

--- Names of the events you can listen for. You can also pass the strings.
---@type ChatEvents
Event = {}

---@class chats
chats = {}

--- Recent chat from every stream being watched, oldest first.
--- Needs "chat.read".
---@param opts? ChatQuery
---@return Chat[]
function chats.get(opts) end

--- How many chat lines match.
--- Needs "chat.read".
---@param opts? ChatQuery
---@return integer
function chats.count(opts) end

--- The people who wrote recently, most active first: { id, name, avatar, count, last_ts, is_member, is_mod, is_owner }.
--- Needs "chat.read".
---@param opts? ChatQuery
---@return table[]
function chats.authors(opts) end

--- Pick a random line of recent chat (one per person by default, so it's fair for giveaways).
--- Returns nil when nothing matches. With `count` it returns a list instead.
--- Needs "chat.read".
---@param opts? ChatQuery
---@return Chat|nil
function chats.random(opts) end

--- Run `handler` for chat events. The history read when a stream is first opened is skipped unless
--- you pass `{ backlog = true }`. Returns an id for `chats.off`.
--- Needs "chat.read".
---
--- ```lua
--- chats.on(Event.SuperChat, function(chat)
---   ui.notify{ title = chat.author.name, message = chat.amount_text }
--- end)
--- ```
---@param event string|"chat"|"chat.super_chat"|"chat.super_sticker"|"chat.member"|"chat.milestone"|"chat.gift"|"chat.jewel"|"chat.removed"
---@param handler fun(chat: Chat)
---@param opts? { backlog?: boolean }
---@return integer id
function chats.on(event, handler, opts) end

--- Stop a handler made with chats.on.
---@param id integer
---@return boolean found
function chats.off(id) end

--- Middleware: look at every new line before it is shown anywhere (Dashboard, pop-out, alerts,
--- other plugins). Return `false` to block it (optionally with a reason for the console), a
--- string or `{ text = "…" }` to change its text, or nothing to let it through. Keep it quick:
--- a slow middleware is skipped.
--- Needs "chat.middleware".
---
--- ```lua
--- chats.use(function(chat)
---   if chat.text:lower():find("spam") then return false, "contains spam" end
--- end)
--- ```
---@param handler fun(chat: Chat): boolean|string|{ text?: string, drop?: boolean, reason?: string }|nil, string?
---@return integer id
function chats.use(handler) end

--- Remove middleware made with chats.use.
---@param id integer
---@return boolean found
function chats.unuse(id) end

------------------------------------------------------------------------------------------------
-- streams
------------------------------------------------------------------------------------------------

---@class StreamState
---@field id string                video id (or "simulator" in the test environment)
---@field title? string
---@field channel? string
---@field phase "connecting"|"live"|"upcoming"|"ended"|"error"
---@field viewers? integer
---@field peak_viewers integer
---@field likes? integer
---@field total_views? integer
---@field started_at? integer
---@field chat "off"|"connecting"|"on"|"limited"|"unavailable"
---@field simulated boolean

---@class StreamTotals
---@field streams integer          how many streams are being watched
---@field live boolean             at least one is live
---@field title? string            the stream's title, or "N streams"
---@field viewers? integer         summed over all streams
---@field likes? integer
---@field total_views? integer
---@field started_at? integer      the earliest start

---@class stream
stream = {}

--- The streams being watched (Developer mode can watch several at once).
--- Needs "stream.read".
---@return StreamState[]
function stream.list() end

--- Totals over all streams being watched.
--- Needs "stream.read".
---@return StreamTotals
function stream.state() end

--- Listen for stream events (Event.StreamUpdate, StreamStart, StreamEnd, StreamAdded, StreamRemoved).
--- Needs "stream.read".
---@param event string
---@param handler fun(state: StreamState)
---@return integer id
function stream.on(event, handler) end

--- Stop a handler made with stream.on.
---@param id integer
---@return boolean found
function stream.off(id) end

------------------------------------------------------------------------------------------------
-- notifications and dialogs
------------------------------------------------------------------------------------------------

---@class NotifySpec
---@field title? string
---@field message string
---@field level? "info"|"success"|"warning"|"error"
---@field duration? integer         milliseconds on screen (default 5000; 0 keeps it until dismissed)
---@field actions? { label: string, on_click: fun(ev: UiEvent) }[]   up to 3 buttons

---@class ModalButton
---@field id string
---@field label string
---@field primary? boolean
---@field danger? boolean

---@class ModalResult
---@field button? string            the id of the button pressed
---@field dismissed boolean         closed without a button
---@field values table<string, any> what the inputs inside the dialog held

---@class ModalSpec
---@field title? string
---@field body? string|UiNode       a message, or a whole interface
---@field content? UiNode           the same as body
---@field buttons? ModalButton[]    default: one OK button
---@field width? "sm"|"md"|"lg"
---@field dismissable? boolean      can be closed with Esc / outside click (default true)
---@field on_result? fun(result: ModalResult)

--- What a handler (on_click, on_change…) is given.
---@class UiEvent
---@field value any                 the new value (for inputs, switches, selects, sliders)
---@field values table<string, any> the current value of every named input in the same interface
---@field name? string              the `name` of the thing that was used

---@class ui
ui = {}

--- Show a notification inside ForgeCast. A plain string works too: `ui.notify("Done")`.
--- Needs "ui.notify".
---@param spec string|NotifySpec
---@return string id
function ui.notify(spec) end

--- Show a dialog over the app. Needs "ui.modal".
---@param spec ModalSpec
---@return string id
function ui.modal(spec) end

--- Close a dialog from code.
---@param id string
function ui.close(id) end

--- A dialog with a message and an OK button.
---@param message string
---@param on_close? fun()
---@return string id
function ui.alert(message, on_close) end

--- Ask yes or no. `on_answer` gets true or false.
---@param message string
---@param on_answer fun(yes: boolean)
---@param opts? { title?: string, yes?: string, no?: string, danger?: boolean }
---@return string id
function ui.confirm(message, on_answer, opts) end

--- Ask for a line of text. `on_answer` gets the text, or nil if cancelled.
---@param message string
---@param default string|fun(text: string|nil)
---@param on_answer? fun(text: string|nil)
---@return string id
function ui.prompt(message, default, on_answer) end

--- Draw a tree into one of your own windows or Dashboard sections (the handles' :render does this).
---@param target string        "window:<key>", "panel:<id>" or "settings"
---@param tree UiNode
function ui.render(target, tree) end

------------------------------------------------------------------------------------------------
-- interface: the building blocks for windows, Dashboard sections and dialogs
------------------------------------------------------------------------------------------------
-- A node is a table with a `type`; the constructors below fill it in. Put children inside the
-- braces, properties as name = value. Functions (on_click, on_change…) run when the user acts.
-- Inputs with a `name` keep what was typed: every handler gets it in `ev.values`.

--- What you put in the braces of an element: its properties, and its children as list items.
---@class UiProps
---@field [integer] UiNode|false|nil   children
---@field gap? integer              row, column: space between children (px)
---@field padding? integer
---@field align? "start"|"center"|"end"|"stretch"
---@field justify? "start"|"center"|"end"|"between"
---@field wrap? boolean             row: let children wrap
---@field grow? boolean             take the remaining space
---@field key? string|integer       any element: give it a different key and it starts fresh (an input is emptied)
---@field title? string             card
---@field description? string       card
---@field max_height? integer       scroll, chat
---@field size? "xs"|"sm"|"md"|"lg" text
---@field text? string              text, heading, badge, code
---@field level? 1|2|3              heading
---@field muted? boolean            text
---@field bold? boolean             text
---@field mono? boolean             text
---@field color? "success"|"warning"|"error"|"info"|"accent"|"muted"|string   text (a name, or "#rrggbb")
---@field tone? "neutral"|"success"|"warning"|"error"|"info"|"accent"   badge, progress
---@field label? string             button, link, checkbox, switch, input, select, slider, progress
---@field url? string               link (https only)
---@field src? string               image (https only)
---@field alt? string
---@field width? integer            image
---@field height? integer           image
---@field rounded? boolean          image
---@field value? any                progress (0 to 1), input, textarea, select, slider
---@field checked? boolean          checkbox, switch
---@field name? string              input, textarea, checkbox, switch, select, slider: its key in ev.values
---@field placeholder? string
---@field disabled? boolean
---@field autofocus? boolean
---@field variant? "primary"|"outline"|"ghost"|"danger"   button
---@field icon? string             button, menu, empty: a name from the icon list ("heart", "trash-2", "trophy"…)
---@field options? (string|{ value: string, label?: string })[]   select
---@field min? number               slider
---@field max? number               slider
---@field step? number              slider
---@field rows? integer|any[][]     textarea: height in lines; table: the rows
---@field columns? (string|{ key: string, label?: string })[]   table
---@field empty? string             table, list: what to say when there is nothing
---@field messages? Chat[]          chat: lines to show (from chats.get)
---@field compact? boolean          chat
---@field on_click? fun(ev: UiEvent)
---@field on_change? fun(ev: UiEvent)
---@field on_submit? fun(ev: UiEvent)   input: Enter was pressed
---@field tooltip? string           any element: a hint on hover
---@field hidden? boolean           any element: not drawn
---@field confirm? string           button: asks this before on_click runs ("Reset everything?")
---@field secret? boolean           input: a password field
---@field sortable? boolean         table: click a heading to sort
---@field filter? boolean           table: a box to filter the rows
---@field page_size? integer        table: rows per page (5 to 100)
---@field selectable? boolean       table: tick rows; on_select gets their ids
---@field on_select? fun(ev: UiEvent)   table: the ticked rows changed
---@field on_row_click? fun(ev: UiEvent)   table: a row was clicked (ev.value is the row)
---@field selected? any[]          table: the rows ticked at first (their ids: a row's `id`, else its position)
---@field filter_placeholder? string   table
---@field label? string             divider: a word in the middle of the line

--- A finished element, as the constructors below return it: the properties plus its `type`.
--- A plain table with a `type` works too: `{ type = "text", text = "hi" }`.
---@class UiNode : UiProps
---@field type string

--- Lay children out top to bottom.
---@param props? UiProps
---@return UiNode
function ui.column(props) end

--- Lay children out left to right.
---@param props? UiProps
---@return UiNode
function ui.row(props) end

--- A box with an optional title.
---@param props? UiProps
---@return UiNode
function ui.card(props) end

--- A scrolling area (set max_height).
---@param props? UiProps
---@return UiNode
function ui.scroll(props) end

--- A thin line between things.
---@return UiNode
function ui.divider(props) end

--- Empty space that grows, to push what follows to the end of a row or column.
---@param props? UiProps
---@return UiNode
function ui.spacer(props) end

--- Text: `ui.text("hi")` or `ui.text{ text = "hi", muted = true }`.
---@param text string|UiProps
---@param props? UiProps
---@return UiNode
function ui.text(text, props) end

--- A heading. `level` is 1 (largest) to 3.
---@param text string|UiProps
---@param props? UiProps
---@return UiNode
function ui.heading(text, props) end

--- A small coloured label; `tone` picks the colour.
---@param props? UiProps
---@return UiNode
function ui.badge(props) end

--- A block of fixed-width text.
---@param props? UiProps
---@return UiNode
function ui.code(props) end

--- A link that opens in the browser (https only).
---@param label string
---@param url string
---@param props? UiProps
---@return UiNode
function ui.link(label, url, props) end

--- A picture from an https address.
---@param props UiProps
---@return UiNode
function ui.image(props) end

--- A bar showing `value`, from 0 to 1.
---@param props UiProps
---@return UiNode
function ui.progress(props) end

--- A button: `ui.button("Add", function(ev) … end)` or `ui.button{ label = "Add", on_click = …, variant = "primary" }`.
---@param label string|UiProps
---@param on_click? fun(ev: UiEvent)
---@param props? UiProps
---@return UiNode
function ui.button(label, on_click, props) end

--- A text box. Give it a `name` to read it in `ev.values`.
---@param props? UiProps
---@return UiNode
function ui.input(props) end

--- A box for several lines of text.
---@param props? UiProps
---@return UiNode
function ui.textarea(props) end

--- A tick box.
---@param props? UiProps
---@return UiNode
function ui.checkbox(props) end

--- An on/off switch.
---@param props? UiProps
---@return UiNode
function ui.switch(props) end

--- A drop-down list of `options`.
---@param props? UiProps
---@return UiNode
function ui.select(props) end

--- A slider between `min` and `max`.
---@param props? UiProps
---@return UiNode
function ui.slider(props) end

--- A table: columns = { "Name", "Count" }, rows = { { "Ann", 3 }, { "Bob", 5 } }.
---@param props? UiProps
---@return UiNode
function ui.table(props) end

--- Children with a line between them.
---@param props? UiProps
---@return UiNode
function ui.list(props) end

--- Chat messages drawn like the Dashboard's chat: `ui.chat{ messages = chats.get{ limit = 20 } }`.
---@param props? UiProps
---@return UiNode
function ui.chat(props) end

------------------------------------------------------------------------------------------------
-- windows
------------------------------------------------------------------------------------------------

---@class WindowSpec
---@field key? string               a stable name; asking for a key that is open brings it forward
---@field title? string
---@field width? integer            default 420
---@field height? integer           default 520
---@field min_width? integer
---@field min_height? integer
---@field resizable? boolean
---@field always_on_top? boolean
---@field content? UiNode           what to show
---@field on_close? fun()

---@class Window
---@field key string
local Window = {}

--- Show a new interface in the window.
---@param tree UiNode
function Window:render(tree) end

--- Close this window.
function Window:close() end
--- Bring this window to the front.
function Window:focus() end

--- Change this window's title.
---@param title string
function Window:set_title(title) end

---@class window
window = {}

--- Open a window of your own. Needs "ui.window".
---
--- ```lua
--- local w = window.create{ title = "Counter", width = 300, height = 200 }
--- local n = 0
--- local function draw()
---   w:render(ui.column{ ui.heading("Count: " .. n), ui.button("+1", function() n = n + 1 draw() end) })
--- end
--- draw()
--- ```
---@param spec WindowSpec
---@return Window
function window.create(spec) end

--- Close one of your windows.
---@param key string
function window.close(key) end

--- Bring one of your windows to the front.
---@param key string
function window.focus(key) end

--- Change a window's title.
---@param key string
---@param title string
function window.set_title(key, title) end

------------------------------------------------------------------------------------------------
-- the Dashboard
------------------------------------------------------------------------------------------------

---@class SectionSpec
---@field id? string                a stable id
---@field title? string
---@field content? UiNode
---@field size? "half"|"full"       one column (the default) or the Dashboard's whole width
---@field height? integer           its body scrolls past this many pixels (120 to 900; 560 by default)

---@class Section
---@field id string
local Section = {}

--- Show a new interface in this Dashboard section.
---@param tree UiNode
function Section:render(tree) end

--- Remove this section from the Dashboard.
function Section:remove() end

---@class dashboard
dashboard = {}

--- Add a section to the Dashboard with an interface of your own: inputs, buttons, lists…
--- Needs "ui.dashboard".
---@param spec SectionSpec
---@return Section
function dashboard.section(spec) end

--- Remove a Dashboard section you added.
---@param id string
function dashboard.remove(id) end

------------------------------------------------------------------------------------------------
-- the plugin itself
------------------------------------------------------------------------------------------------

---@class PluginSettings
local PluginSettings = {}

--- Show a new interface in the settings panel on the plugin's card.
---@param tree UiNode
function PluginSettings:render(tree) end

---@class plugin
---@field id string
---@field name string
---@field version string
---@field dir string                the plugin's folder
plugin = {}

--- Run `handler` when the plugin is turned off or reloaded: clean up here.
---@param handler fun()
function plugin.on_unload(handler) end

--- A settings panel shown on the plugin's card in the Plugins page. Needs "ui.dashboard".
---@param spec { title?: string, content?: UiNode }
---@return PluginSettings
function plugin.settings(spec) end

------------------------------------------------------------------------------------------------
-- storage, timers, logging, data
------------------------------------------------------------------------------------------------

---@class storage
storage = {}

--- Read something you saved (nil, or `default`, when there is nothing). Needs "storage".
---@param key string
---@param default? any
---@return any
function storage.get(key, default) end

--- Save a value (a string, number, boolean or table of those; nil removes it). Up to 1 MB per plugin. Needs "storage".
---@param key string
---@param value any
function storage.set(key, value) end

--- Forget one saved value.
---@param key string
---@return boolean existed
function storage.delete(key) end

--- The names of everything saved, in order.
---@return string[]
function storage.keys() end

--- Forget everything saved.
function storage.clear() end

---@class timer
timer = {}

--- Call `fn` once after `ms` milliseconds. Returns an id for timer.cancel.
---@param ms number
---@param fn fun()
---@return integer id
function timer.after(ms, fn) end

--- Call `fn` every `ms` milliseconds (50 at the least).
---@param ms number
---@param fn fun()
---@return integer id
function timer.every(ms, fn) end

--- Stop a timer made with timer.every or timer.after.
---@param id integer
---@return boolean found
function timer.cancel(id) end

---@class log
log = {}
--- Write to the console at debug level (hidden until you show debug lines).
---@param ... any
function log.debug(...) end
--- Write an information line to the console.
---@param ... any
function log.info(...) end
--- Write a warning to the console.
---@param ... any
function log.warn(...) end
--- Write an error to the console. It doesn't stop the plugin.
---@param ... any
function log.error(...) end

--- Same as log.info: shows in the plugin's console.
---@param ... any
function print(...) end

--- The same as table.unpack: the items of a list as separate values (Lua 5.1's name, kept so
--- code from tutorials works).
---@param list table
---@param i? integer
---@param j? integer
---@return any ...
function unpack(list, i, j) end

---@class json
json = {}

--- Turn a value into JSON text. Tables with keys 1..n become lists, others objects.
---@param value any
---@param pretty? boolean
---@return string
function json.encode(value, pretty) end

--- Read JSON text. Returns nil and a message when it isn't valid.
---@param text string
---@return any value
---@return string? err
function json.decode(text) end

---@class time
time = {}

--- Milliseconds since 1970.
---@return integer
function time.now() end

--- Load another .lua file from the plugin's folder: `require("helpers")` or `require("lib.util")`.
---@param name string
---@return any
function require(name) end

------------------------------------------------------------------------------------------------
-- talking to the rest of ForgeCast
------------------------------------------------------------------------------------------------

---@class sounds
sounds = {}

--- Play a sound from the Sound Library by its name (capital letters don't matter). Get sounds under
--- Sound Library -> Find sounds, or upload your own. If there is no sound with that name, ForgeCast
--- says so in a message. Needs "sounds".
---@param name string
function sounds.play(name) end

---@class AlertSpec
---@field type? "superchat"|"supersticker"|"member.new"|"member.gift"|"member.milestone"|"subscriber.new"
---@field name? string              who it is from
---@field amount? number
---@field currency? string
---@field message? string
---@field detail? string

---@class alerts
alerts = {}

--- Show one of your alerts, as if it had happened on stream. Needs "alerts".
---@param spec AlertSpec
function alerts.show(spec) end

---@class AppInfo
---@field version string
---@field platform string
---@field api integer

---@class app
app = {}

--- Which ForgeCast is running: its version, the platform, and the plugin API number.
---@return AppInfo
function app.info() end

--- Open a web address in the browser (https only). Needs "app.open_url".
---@param url string
function app.open_url(url) end

---@class bus
bus = {}

--- Send a message to your other plugins. Needs "bus".
---@param name string
---@param data? any
function bus.emit(name, data) end

--- Listen for messages from other plugins: `handler` gets { from = "plugin-id", data = … }.
--- Needs "bus".
---@param name string
---@param handler fun(msg: { from: string, data: any })
---@return integer id
function bus.on(name, handler) end

--- Stop a handler made with bus.on.
---@param id integer
---@return boolean found
function bus.off(id) end

------------------------------------------------------------------------------------------------
-- overlays
------------------------------------------------------------------------------------------------

---@class overlay
overlay = {}

--- Show a value in the streamer's overlays, live. In an overlay's text, `{{plugin.<your plugin id>.<name>}}`
--- shows it (a number can be shortened as on any variable: `{{plugin.my-counter.total | compact}}`
--- shows 12K). A name is a letter, then up to 31 letters, digits or _. A value is text (up to 500
--- characters), a number or true/false (shown as on/off); nil takes it away. Up to 50 values. They leave the overlays when the
--- plugin stops. Needs "overlay".
---@param name string
---@param value string|number|boolean|nil
function overlay.set(name, value) end

--- A value you set (nil when there is none). Needs "overlay".
---@param name string
---@return string|number|boolean|nil
function overlay.get(name) end

--- Take all your values out of the overlays. Needs "overlay".
function overlay.clear() end

-- Effects: shown for a moment on your running full-screen overlays (or, when none runs, the biggest
-- running widget). The callbacks get how many overlays showed it, or (nil, why).

--- Press a button setting of your running overlays (the animation or sound it starts plays), like
--- pressing it on the Overlays page. Needs "overlay".
---@param button string             the button setting's name
---@param callback? fun(shown: integer?, error?: string)
function overlay.press(button, callback) end

--- Confetti over the stream. Needs "overlay".
---@param opts? { seconds: number?, count: integer?, colors: string[]? }
---@param callback? fun(shown: integer?, error?: string)
function overlay.confetti(opts, callback) end

--- Emotes or emoji falling down the stream: { image = emotes.image(":fire:") } or { text = "🔥" }. Needs "overlay".
---@param spec { image: string?, text: string?, count: integer?, seconds: number? }
---@param callback? fun(shown: integer?, error?: string)
function overlay.rain(spec, callback) end

--- A name card that slides in at the bottom ("Ann · our newest member"). Needs "overlay".
---@param spec { title: string, subtitle: string?, accent: string?, position: "left"|"right"|"center"|nil, seconds: number? }
---@param callback? fun(shown: integer?, error?: string)
function overlay.lower_third(spec, callback) end

--- Show a widget or overlay from your library for a moment, with its settings filled in. It has to
--- have been used once (pressed Use). x and y place its middle (0 to 1 across the stream). Needs "overlay".
---@param widget string             its name in your library
---@param opts? { vars: table?, seconds: number?, x: number?, y: number?, scale: number? }
---@param callback? fun(shown: integer?, error?: string)
function overlay.spawn(widget, opts, callback) end

--- Draw your own small widget on stream, from the same elements as windows (text, headings, badges,
--- pictures, bars, rows, columns, tables; nothing to click). Drawing the same id again replaces it.
--- Needs "overlay".
---@param id string
---@param tree UiNode
---@param opts? { x: number?, y: number?, width: number?, seconds: number? }
function overlay.draw(id, tree, opts) end

--- Take a drawing (or a canvas) off the stream. Needs "overlay".
---@param id string
function overlay.clear_draw(id) end

--- Set how much your running overlays may spend drawing: "auto", "quality", "balanced" or "saver". Needs "overlay".
---@param mode "auto"|"quality"|"balanced"|"saver"
---@param callback? fun(changed: integer?, error?: string)
function overlay.performance(mode, callback) end

---@class Canvas
---@field id string
local Canvas = {}
--- Start the frame again (filled with a colour, or see-through).
---@param color? string
---@return Canvas
function Canvas:clear(color) end
--- A filled rectangle.
---@return Canvas
function Canvas:rect(x, y, w, h, color) end
--- A filled circle.
---@return Canvas
function Canvas:circle(x, y, r, color) end
--- A line.
---@return Canvas
function Canvas:line(x1, y1, x2, y2, color, width) end
--- Text (align "left", "center" or "right").
---@return Canvas
function Canvas:text(x, y, text, color, size, align) end
--- A picture (an https address, an emote, or assets.url).
---@return Canvas
function Canvas:image(url, x, y, w, h) end
--- Show the frame drawn since the last commit (false when it came too soon: 20 frames a second at most).
---@return boolean
function Canvas:commit() end
--- Take the canvas off the stream.
function Canvas:hide() end

--- A drawing surface on stream for games chat plays (snake, a pixel wall…): draw, then :commit().
--- Up to 500 drawings a frame. Needs "overlay".
---@param spec { id: string?, width: integer?, height: integer?, x: number?, y: number? }
---@return Canvas
function overlay.canvas(spec) end

------------------------------------------------------------------------------------------------
-- OBS
------------------------------------------------------------------------------------------------

---@class ObsAudio
---@field name string               the input's name in OBS (what obs.set_mute takes)
---@field label string              "Desktop audio", "Microphone", or the input's own name
---@field kind "desktop"|"mic"|"other"
---@field muted boolean

---@class ObsState
---@field connected boolean         ForgeCast is connected to OBS (the rest is empty when it isn't)
---@field scene? string             the scene on air
---@field scenes string[]
---@field streaming boolean
---@field recording boolean
---@field audio ObsAudio[]
---@field fps? number
---@field cpu? number               OBS's own use of the processor, in percent
---@field dropped_frames? integer   frames OBS skipped while sending

---@class obs
obs = {}

--- OBS as ForgeCast sees it now. Needs "obs".
---@return ObsState
function obs.state() end

--- Switch OBS to a scene. `callback(ok, error)` is called when OBS has answered. Needs "obs".
--- (A plugin can never start or stop the stream or the recording.)
---@param scene string
---@param callback? fun(ok: boolean, error?: string)
---@return integer id
function obs.set_scene(scene, callback) end

--- Mute or unmute one of OBS's audio inputs (obs.state().audio lists them). Needs "obs".
---@param input string
---@param muted boolean
---@param callback? fun(ok: boolean, error?: string)
---@return integer id
function obs.set_mute(input, muted, callback) end

--- Show or hide a source in a scene (a webcam, an image, a browser source…). Needs "obs".
---@param scene string
---@param source string
---@param visible boolean
---@param callback? fun(ok: boolean, error?: string)
---@return integer id
function obs.set_source_visible(scene, source, visible, callback) end

--- Listen for what happens in OBS (Event.ObsScene, ObsStreaming, ObsRecording, ObsConnected).
--- Needs "obs".
---@param event string
---@param handler fun(e: table)
---@return integer id
function obs.on(event, handler) end

--- Stop a handler made with obs.on.
---@param id integer
---@return boolean found
function obs.off(id) end

---@class ObsLevel
---@field db number                 the loudest channel just now (-100 is silence, 0 the loudest)
---@field talking boolean           it hears a voice (louder than -40 dB for a moment)

--- Every input's loudness: { ["Mic/Aux"] = { db = -22.5, talking = true } }. OBS sends it while a
--- plugin asks (or listens for Event.ObsTalking): the first call can be empty. Needs "obs".
---@return table<string, ObsLevel>
function obs.levels() end

--- Whether an input hears someone talking (any microphone when no input is given). Needs "obs".
---@param input? string
---@return boolean
function obs.talking(input) end

--- Save the replay buffer as a clip (OBS's Replay Buffer has to be on). Needs "obs".
---@param callback? fun(ok: boolean?, error?: string)
function obs.save_replay(callback) end

--- A picture of a source or scene (the scene on air by default), as an image address the
--- interface can show or ai.ask can look at. Needs "obs".
---@param source? string
---@param opts? { width: integer, format: "jpg"|"png" }
---@param callback fun(image: string?, error?: string)
function obs.screenshot(source, opts, callback) end

---@class ObsFilter
---@field name string
---@field enabled boolean
---@field kind string

--- A source's filters. Needs "obs".
---@param source string
---@param callback fun(filters: ObsFilter[]?, error?: string)
function obs.filters(source, callback) end

--- Turn one of a source's filters on or off ("blur my cam"). Needs "obs".
---@param source string
---@param filter string
---@param on boolean
---@param callback? fun(ok: boolean?, error?: string)
function obs.filter(source, filter, on, callback) end

---@class ObsTransform
---@field x? number
---@field y? number
---@field scale? number             both directions at once
---@field scale_x? number
---@field scale_y? number
---@field rotation? number          degrees

--- Move, size or turn a source in a scene (only the fields you give change). Needs "obs".
---@param scene string
---@param source string
---@param transform ObsTransform
---@param callback? fun(ok: boolean?, error?: string)
function obs.transform(scene, source, transform, callback) end

--- Where a source is in a scene: { x, y, scale_x, scale_y, rotation, width, height }. Needs "obs".
---@param scene string
---@param source string
---@param callback fun(transform: ObsTransform?, error?: string)
function obs.get_transform(scene, source, callback) end

--- Play, pause, stop, restart, or skip a media source (a video or a sound). Needs "obs".
---@param source string
---@param action "play"|"pause"|"stop"|"restart"|"next"|"previous"
---@param callback? fun(ok: boolean?, error?: string)
function obs.media(source, action, callback) end

--- Change the text of an OBS text source (up to 2000 characters). Needs "obs".
---@param source string
---@param text string
---@param callback? fun(ok: boolean?, error?: string)
function obs.set_text(source, text, callback) end

--- An input's volume in dB. Needs "obs".
---@param input string
---@param callback fun(db: number?, error?: string)
function obs.get_volume(input, callback) end

--- Set an input's volume in dB (-100 is silent, 0 is as recorded). Needs "obs".
---@param input string
---@param db number
---@param callback? fun(ok: boolean?, error?: string)
function obs.set_volume(input, db, callback) end

--- Slide an input's volume to `db` over `ms` milliseconds (lower the music while a sound plays). Needs "obs".
---@param input string
---@param db number
---@param ms? integer               400 by default
---@param callback? fun(ok: boolean?, error?: string)
function obs.fade_volume(input, db, ms, callback) end

------------------------------------------------------------------------------------------------
-- AI
------------------------------------------------------------------------------------------------

---@class AiTurn
---@field role "user"|"model"       who said it
---@field text string

---@class AiOptions
---@field system? string            how the AI should behave ("You are a friendly co-host. One sentence.")
---@field json? boolean             answer in JSON: `answer` is then a table
---@field image? string             a picture to look at: a jpeg, png or webp data address (obs.screenshot gives one), up to about 1.5 MB
---@field history? AiTurn[]         the conversation so far, oldest first (up to 20 turns; ai.session keeps it for you)

---@class AiLabel
---@field label string              one of your labels, exactly as you wrote it
---@field confidence number         how sure the AI is, 0 to 1

---@class ai
ai = {}

--- Ask ForgeCast's AI. The answer comes later: `callback(answer)`, or `callback(nil, why)` when it
--- failed (no credits left, offline, the streamer stopped it…). Each request is paid with the
--- streamer's AI credits and shows under AI workers. A plugin may have 2 requests waiting at a time
--- and make 30 an hour. Needs "ai".
--- ```lua
--- obs.screenshot(nil, { width = 960 }, function(pic)
---   ai.ask("What's happening in this game?", { image = pic }, function(answer) ui.notify(answer) end)
--- end)
--- ```
---@param prompt string             up to 8000 characters
---@param opts? AiOptions|fun(answer: any, error?: string)
---@param callback? fun(answer: any, error?: string)
---@return integer id
function ai.ask(prompt, opts, callback) end

--- Sort a text into one of your labels: `callback({ label = "question", confidence = 0.9 })`.
--- Costs the same as ai.ask and counts toward its limits. Needs "ai".
--- ```lua
--- ai.classify(chat.text, { "question", "joke", "spam", "other" }, function(r)
---   if r and r.label == "question" then ui.notify("Question: " .. chat.text) end
--- end)
--- ```
---@param text string               up to 8000 characters
---@param labels string[]           2 to 20 labels
---@param callback fun(result: AiLabel?, error?: string)
---@return integer id
function ai.classify(text, labels, callback) end

--- Translate a text into another language, keeping names, @handles and emoji. Costs the same as
--- ai.ask and counts toward its limits. Needs "ai".
---@param text string               up to 8000 characters
---@param language string           "Hindi", "Spanish", "Japanese"…
---@param callback fun(text: string?, error?: string)
---@return integer id
function ai.translate(text, language, callback) end

---@class AiSessionOptions
---@field system? string            how the AI should behave in every answer
---@field keep? integer             how many questions and answers it remembers (10, at most 10)

---@class AiSession
local AiSession = {}
--- Ask, with the conversation so far: the question and its answer are remembered for the next one.
---@param prompt string
---@param opts? AiOptions|fun(answer: any, error?: string)
---@param callback? fun(answer: any, error?: string)
---@return integer id
function AiSession:ask(prompt, opts, callback) end
--- The conversation so far, oldest first.
---@return AiTurn[]
function AiSession:history() end
--- Forget the conversation and start over.
function AiSession:forget() end

--- A conversation with the AI that remembers what was said: a co-host that knows the last few
--- questions. Each ask is an ai.ask (same cost and limits). Needs "ai".
--- ```lua
--- local cohost = ai.session{ system = "You are a cheerful pirate co-host. One sentence." }
--- cohost:ask("Say hi to chat", function(a) print(a) end)
--- ```
---@param opts? AiSessionOptions
---@return AiSession
function ai.session(opts) end

------------------------------------------------------------------------------------------------
-- past streams
------------------------------------------------------------------------------------------------

---@class PastStream
---@field id string
---@field video_id? string          the YouTube video, when known
---@field title? string
---@field started_at integer        milliseconds since 1970
---@field ended_at integer
---@field minutes integer
---@field peak_viewers integer
---@field avg_viewers? integer
---@field likes? integer
---@field chat_lines integer
---@field chatters integer
---@field super_chats integer
---@field super_chat_totals table<string, number>   per currency ("USD" = 12.5)
---@field members integer           new members
---@field gifts integer             memberships gifted
---@field clips integer
---@field hype_moments integer
---@field step_min integer          minutes between two points of `viewers` and `chat`
---@field viewers? integer[]        viewers through the stream (only from history.stream)
---@field chat? integer[]           chat lines in each step (only from history.stream)

---@class WeekNumbers
---@field streams integer
---@field minutes integer
---@field avg_viewers? integer
---@field peak integer
---@field avg_chatters integer      people in chat, on average per stream
---@field chat_lines integer
---@field super_chats integer
---@field super_chat_totals table<string, number>
---@field members integer
---@field gifts integer
---@field previous? WeekNumbers     the 7 days before, to compare

---@class StreamsQuery
---@field limit? integer            how many (1-200, default 20)
---@field days? number              only the streams that started in the last `days` days

---@class history
history = {}

--- The streams recorded on this PC, newest first (without their curves). Needs "history".
---@param query? StreamsQuery
---@return PastStream[]
function history.streams(query) end

--- One stream, with its viewers and chat curves (nil when there is no such stream). Needs "history".
---@param id string
---@return PastStream|nil
function history.stream(id) end

--- The last 7 days added up, and the 7 days before (nil before any stream was recorded).
--- Needs "history".
---@return WeekNumbers|nil
function history.week() end

--- Listen for Event.StreamRecorded: a stream ended and was recorded. Needs "history".
---@param event string
---@param handler fun(s: PastStream)
---@return integer id
function history.on(event, handler) end

--- Stop a handler made with history.on.
---@param id integer
---@return boolean found
function history.off(id) end

------------------------------------------------------------------------------------------------
-- chat commands and tools
------------------------------------------------------------------------------------------------

---@class CommandOptions
---@field cooldown? number          seconds between two uses by anyone
---@field per_user? number          seconds before the same viewer can use it again
---@field who? "everyone"|"members"|"mods"|"owner"   who may use it (the streamer always can)
---@field aliases? string[]         other names for it ("roll" for "dice")
---@field description? string
---@field on_cooldown? fun(ctx: CommandContext, seconds_left: integer)

---@class CommandContext
---@field name string               the command's name
---@field args string[]             the words after it
---@field rest string               everything after it, as typed
---@field chat Chat                 the message
---@field user ChatAuthor           who used it
---@field reply fun(text: string)   answer them: in chat with youtube.send_chat, otherwise a notification

---@class CommandInfo
---@field name string
---@field who string
---@field cooldown number
---@field per_user number
---@field aliases string[]
---@field description? string
---@field used integer              times it ran

---@class commands
commands = {}

--- Run a function when someone types `!name` in chat. Return false from it when the command didn't
--- count (bad arguments): its cooldown isn't used. Needs "chat.read".
---@param name string               "dice" (or "!dice")
---@param opts? CommandOptions|fun(ctx: CommandContext): boolean?
---@param fn? fun(ctx: CommandContext): boolean?
---@return string name
function commands.add(name, opts, fn) end

--- Stop a command (and its aliases).
---@param name string
---@return boolean found
function commands.remove(name) end

--- Every command this plugin added.
---@return CommandInfo[]
function commands.list() end

---@class rate
rate = {}

--- Count one use of `key`: true while it is under `n` uses in `secs` seconds, otherwise false and
--- the seconds to wait. `rate.limit("hug:" .. chat.author.id, 3, 60)`.
---@param key string
---@param n integer
---@param secs number
---@return boolean ok
---@return integer? wait_seconds
function rate.limit(key, n, secs) end

--- Forget the uses of one key (or of every key).
---@param key? string
function rate.reset(key) end

---@class WaitSpec
---@field from? string              only this viewer (a name like "@ann", or a channel id)
---@field match? string|fun(chat: Chat): boolean   a Lua pattern the text must contain, or a test
---@field timeout? number           seconds (1-3600, default 30)
---@field stream? string            only this stream

---@class Waiting
local Waiting = {}
--- Stop waiting (the callback isn't called).
function Waiting:cancel() end

--- Wait for the next chat line that fits: `callback(chat)`, or `callback(nil)` when the time is up.
--- "@ann, pick a number": `chats.wait_for({ from = "@ann", match = "^%d+$" }, function(c) … end)`.
--- Needs "chat.read".
---@param spec WaitSpec
---@param callback fun(chat: Chat?)
---@return Waiting
function chats.wait_for(spec, callback) end

--- Chat messages a minute, over the last `minutes` (1 by default). Needs "chat.read".
---@param minutes? number
---@return number
function chats.rate(minutes) end

--- One viewer's recent lines (by name or channel id), oldest first. Needs "chat.read".
---@param user string
---@param n? integer                how many (20 by default)
---@return Chat[]
function chats.from(user, n) end

---@class Emote
---@field name string               ":fire:"
---@field image string              its picture's address
---@field count integer             how often it was used

---@class EmoteQuery
---@field minutes? number           how far back (10 by default)
---@field stream? string

---@class emotes
emotes = {}

--- The custom emoji used in recent chat, most used first. Needs "chat.read".
---@param opts? EmoteQuery
---@return Emote[]
function emotes.list(opts) end

--- The `n` most used custom emoji (5 by default). Needs "chat.read".
---@param n? integer
---@param opts? EmoteQuery
---@return Emote[]
function emotes.top(n, opts) end

--- An emoji's picture from recent chat (":fire:" or "fire"), or nil. Needs "chat.read".
---@param name string
---@return string?
function emotes.image(name) end

------------------------------------------------------------------------------------------------
-- counters, queues, countdowns and leaderboards
------------------------------------------------------------------------------------------------
-- Each one saves itself (with "storage"), shows its own controls on the Dashboard (with
-- "ui.dashboard"), and puts its numbers in your overlays (with "overlay"), as
-- {{plugin.<plugin id>.<name>}}. Without a permission it simply skips that part.

---@class CounterOptions
---@field title? string
---@field start? number             where it starts and goes back to (0)
---@field command? string           a chat command that shows it; a mod can add "+", "-2", "10" or "reset" (needs "chat.read")
---@field cooldown? number          that command's cooldown (5 s)
---@field dashboard? boolean        false: no Dashboard section
---@field save? boolean             false: start from `start` every time

---@class Counter
---@field name string
---@field value number
local Counter = {}
--- The number now.
---@return number
function Counter:get() end
--- Add to it (1 by default; negative takes away).
---@param n? number
---@return number
function Counter:add(n) end
--- Set it to a number.
---@param v number
---@return number
function Counter:set(v) end
--- Back to where it started.
---@return number
function Counter:reset() end
--- Run a function whenever it changes.
---@param fn fun(value: number)
function Counter:on_change(fn) end

---@class counter
counter = {}

--- A number with buttons: a death counter, wins, hydration… `{{plugin.<id>.deaths}}` shows it.
---@param name string               a letter, then letters, digits or _
---@param opts? CounterOptions
---@return Counter
function counter.create(name, opts) end

---@class QueueOptions
---@field title? string
---@field max? integer              how many can wait (50)
---@field join? string|false        chat command to join ("join"); false: none (needs "chat.read")
---@field leave? string|false       chat command to leave ("leave")
---@field who? "everyone"|"members"|"mods"   who may join from chat
---@field dashboard? boolean
---@field save? boolean

---@class QueueEntry
---@field id string
---@field name string
---@field at integer                when they joined

---@class Queue
---@field name string
local Queue = {}
--- Add a viewer (a chat author, or a name): their place, or nil and "already" (with their place) or "full".
---@param user ChatAuthor|string
---@return integer? place
---@return string? why
function Queue:join(user) end
--- Take a viewer off the queue.
---@param user ChatAuthor|string
---@return boolean
function Queue:leave(user) end
--- Take the first one off the queue.
---@return QueueEntry?
function Queue:next() end
--- The first `n` waiting (1 by default), without taking them off.
---@param n? integer
---@return QueueEntry[]
function Queue:peek(n) end
--- Everyone waiting, in order.
---@return QueueEntry[]
function Queue:list() end
--- How many are waiting.
---@return integer
function Queue:size() end
--- A viewer's place, or nil.
---@param user ChatAuthor|string
---@return integer?
function Queue:position(user) end
--- Take the one at place `i` off the queue.
---@param i integer
---@return QueueEntry?
function Queue:remove(i) end
--- Empty the queue.
function Queue:clear() end
--- Run a function whenever the queue changes.
---@param fn fun(items: QueueEntry[])
function Queue:on_change(fn) end

---@class queue
queue = {}

--- A queue of viewers (to play with you, to be next…). Overlays get `<name>_next`, `<name>_size`
--- and `<name>_list` (the first five).
---@param name string               up to 20 characters
---@param opts? QueueOptions
---@return Queue
function queue.create(name, opts) end

---@class CountdownOptions
---@field title? string
---@field dashboard? boolean

---@class Countdown
---@field name string
local Countdown = {}
--- Start (or carry on).
---@param seconds? number           a new length
function Countdown:start(seconds) end
--- Pause it, keeping the time left.
function Countdown:pause() end
--- Carry on after a pause.
function Countdown:resume() end
--- Stop it and put the full time back (or a new length).
---@param seconds? number
function Countdown:reset(seconds) end
--- Add (or, negative, take away) time.
---@param seconds number
function Countdown:add(seconds) end
--- Seconds left.
---@return integer seconds_left
function Countdown:left() end
--- Whether it is counting down now.
---@return boolean
function Countdown:running() end
--- Run a function when it reaches zero.
---@param fn fun()
function Countdown:on_done(fn) end
--- Run a function every second while it runs.
---@param fn fun(seconds_left: integer)
function Countdown:on_tick(fn) end

---@class countdown
countdown = {}

--- A countdown with Start, Pause, +1 min and Reset. Overlays get `<name>` ("4:59"),
--- `<name>_seconds` and `<name>_running`.
---@param name string
---@param seconds number
---@param opts? CountdownOptions
---@return Countdown
function countdown.create(name, seconds, opts) end

---@class LeaderboardOptions
---@field title? string
---@field size? integer             how many the Dashboard shows (10)
---@field reset? "never"|"daily"|"weekly"   start over every day or every Monday
---@field dashboard? boolean
---@field save? boolean

---@class Score
---@field id string
---@field name string
---@field score number
---@field rank integer

---@class Leaderboard
---@field name string
local Leaderboard = {}
--- Add points to a viewer (1 by default): their new score.
---@param user ChatAuthor|string
---@param n? number
---@return number score
function Leaderboard:add(user, n) end
--- Set a viewer's score.
---@param user ChatAuthor|string
---@param n number
---@return number
function Leaderboard:set(user, n) end
--- A viewer's score (0 when they have none).
---@param user ChatAuthor|string
---@return number
function Leaderboard:get(user) end
--- A viewer's place, or nil.
---@param user ChatAuthor|string
---@return integer?
function Leaderboard:rank(user) end
--- The best `n` (its size by default), with their rank.
---@param n? integer
---@return Score[]
function Leaderboard:top(n) end
--- Wipe every score.
function Leaderboard:reset() end
--- Run a function whenever a score changes.
---@param fn fun(top: Score[])
function Leaderboard:on_change(fn) end

---@class leaderboard
leaderboard = {}

--- Scores by viewer. Overlays get the top five as `<name>_1_name`, `<name>_1_score` … `<name>_5_score`.
---@param name string
---@param opts? LeaderboardOptions
---@return Leaderboard
function leaderboard.create(name, opts) end

------------------------------------------------------------------------------------------------
-- data tools
------------------------------------------------------------------------------------------------

---@class FindOptions
---@field sort? string              a field ("id" by default)
---@field desc? boolean
---@field limit? integer

--- A query: `{ name = "Ann" }`, or with comparisons `{ points = { gt = 100 } }` (eq, ne, gt, gte,
--- lt, lte, contains, in), or a function(row) that answers true.
---@alias Query table|fun(row: table): boolean

---@class StoredTable
---@field name string
local StoredTable = {}
--- Add a row: its new `id`.
---@param row table
---@return integer id
function StoredTable:insert(row) end
--- One row by its id, or nil.
---@param id integer
---@return table?
function StoredTable:get(id) end
--- Change some fields of a row.
---@param id integer
---@param fields table
---@return boolean found
function StoredTable:update(id, fields) end
--- Update the first row that matches, or add one: its id.
---@param query Query
---@param fields table
---@return integer id
function StoredTable:upsert(query, fields) end
--- Delete a row.
---@param id integer
---@return boolean found
function StoredTable:remove(id) end
--- The rows that match, in order.
---@param query? Query
---@param opts? FindOptions
---@return table[]
function StoredTable:find(query, opts) end
--- The first row that matches, or nil.
---@param query? Query
---@param opts? FindOptions
---@return table?
function StoredTable:first(query, opts) end
--- How many rows match (all of them without a query).
---@param query? Query
---@return integer
function StoredTable:count(query) end
--- Every row.
---@param opts? FindOptions
---@return table[]
function StoredTable:all(opts) end
--- Delete every row.
function StoredTable:clear() end

--- A table of rows kept in the plugin's storage, found by their fields. Needs "storage".
---@param name string
---@return StoredTable
function storage.table(name) end

--- Save rows (or a storage.table by its name) as a CSV file in the Downloads folder.
--- `callback(true)` once it is saved, or `(nil, why)`. Needs "storage".
---@param rows table[]|string
---@param filename? string          "export.csv"
---@param callback? fun(ok: boolean?, error?: string)
function storage.export_csv(rows, filename, callback) end

--- Save a .csv, .txt or .json file (up to 2 MB) in the Downloads folder. Needs "storage".
---@param name string
---@param text string
---@param callback? fun(ok: boolean?, error?: string)
function storage.save_file(name, text, callback) end

---@class csv
csv = {}

--- Rows of tables (or of lists) as CSV text; the columns are the keys, sorted, unless you list them.
---@param rows table[]
---@param columns? string[]
---@return string
function csv.encode(rows, columns) end

--- CSV text as a list of tables keyed by the first row (or, with `header = false`, of lists).
---@param text string
---@param opts? { header: boolean }
---@return table[]
function csv.decode(text, opts) end

---@class crypto
crypto = {}

--- SHA-256 of a string, as hex.
---@param s string
---@return string
function crypto.sha256(s) end

--- HMAC-SHA256 (to sign a webhook), as hex.
---@param key string
---@param message string
---@return string
function crypto.hmac_sha256(key, message) end

--- A string as standard base64.
---@param s string
---@return string
function crypto.base64_encode(s) end

--- Standard or URL-safe base64 back to a string, or nil when it isn't base64.
---@param s string
---@return string?
function crypto.base64_decode(s) end

--- A random id like "3f2b…-4…".
---@return string
function crypto.uuid() end

---@class qr
qr = {}

--- A QR code as a picture for `ui.image` (an svg data address).
---@param text string               up to 1000 characters
---@param opts? { size: integer, color: string, background: string }
---@return string
function qr.make(text, opts) end

---@class privacy
privacy = {}

--- Text with emails, phone numbers, UPI ids, card numbers and links replaced by [email], [phone],
--- [upi], [card] and [link]: for anything you send on (http, ai). Turn one off with `{ links = false }`.
---@param text string
---@param opts? { emails: boolean, phones: boolean, upi: boolean, cards: boolean, links: boolean }
---@return string
function privacy.redact(text, opts) end

--- A moment as text on this PC's clock: YYYY MM MMM DD ddd HH hh mm ss A ("HH:mm" by default),
--- or a strftime pattern with % ("%H:%M").
---@param ms? integer               milliseconds since 1970 (now by default)
---@param fmt? string
---@return string
function time.format(ms, fmt) end

--- "1h 05m", "4m 09s", "12s".
---@param ms integer
---@return string
function time.duration(ms) end

---@class schedule
schedule = {}

--- Run every day at a time on this PC's clock ("21:00"). Returns an id for schedule.cancel.
---@param hhmm string
---@param fn fun()
---@return integer id
function schedule.daily(hhmm, fn) end

--- Run once at a moment (milliseconds since 1970, like time.now() + 60000).
---@param ms integer
---@param fn fun()
---@return integer id
function schedule.at(ms, fn) end

--- When a job runs next, or nil.
---@param id integer
---@return integer?
function schedule.next(id) end

--- Stop a job made with schedule.daily or schedule.at.
---@param id integer
---@return boolean found
function schedule.cancel(id) end

------------------------------------------------------------------------------------------------
-- plugin tools
------------------------------------------------------------------------------------------------

--- A table that survives a reload while ForgeCast stays open: `local state = plugin.keep({ score = 0 })`
--- gives back what it held when the plugin last stopped (or `default` the first time).
---@param default table
---@return table
function plugin.keep(default) end

--- `fn(old_version, new_version)` once after the plugin was updated (not on its first start).
---@param fn fun(old: string, new: string)
function plugin.on_update(fn) end

--- Whether the plugin may use a permission right now.
---@param permission string
---@return boolean
function plugin.has(permission) end

--- Ask the streamer for one of the permissions in "optional_permissions", saying why. The callback
--- gets true when they allow it (at once if they already had).
---@param permission string
---@param why string
---@param callback? fun(allowed: boolean)
function plugin.request(permission, why, callback) end

--- A secret (an API key, a token) kept in the Windows Credential Manager, not in a file:
--- `plugin.secret("api_key", value)` keeps it, `plugin.secret("api_key")` reads it, nil removes it.
--- Needs "storage".
---@param name string
---@param value? string|nil
---@return string?
function plugin.secret(name, value) end

---@class ConfigField
---@field key string
---@field label? string
---@field type? "text"|"number"|"switch"|"select"|"textarea"
---@field default? any
---@field min? number
---@field max? number
---@field options? (string|{ value: string, label?: string })[]
---@field placeholder? string
---@field rows? integer

--- Settings from a description: draws the settings panel on the plugin's card, checks what is
--- typed, saves it and returns a table that always holds the current values. Needs "ui.dashboard"
--- (and "storage" to remember them).
---@param spec { title: string?, fields: ConfigField[], on_change: fun(cfg: table)? }
---@return table
function plugin.config(spec) end

--- Answer other plugins' bus.call for a method: return the result (an error is sent back as the error).
--- Needs "bus".
---@param method string
---@param fn fun(args: any, from: string): any
---@return integer id
function bus.handle(method, fn) end

--- Call a method another plugin handles. `callback(result)`, or `(nil, why)` (also after 10 s
--- without an answer). Needs "bus".
---@param plugin_id string
---@param method string
---@param args any
---@param callback fun(result: any, error?: string)
---@return integer id
function bus.call(plugin_id, method, args, callback) end

---@class lib
lib = {}

--- A module of a library plugin listed in "libraries": "chat-tools" loads its init.lua,
--- "chat-tools/text.words" its text/words.lua.
---@param name string
---@return any
function lib.require(name) end

---@class assets
assets = {}

--- The address of a picture, sound or font in the plugin's folder, for ui.image and overlays.
---@param path string               "img/logo.png"
---@return string
function assets.url(path) end

---@class i18n
i18n = {}

--- The app's language ("en", "hi-IN"…).
---@return string
function i18n.locale() end

--- A text from locales/<language>.json in the app's language (then English), with `{name}`
--- filled in from `vars`. Nested keys use dots: "menu.start".
---@param key string
---@param vars? table
---@return string
function i18n.t(key, vars) end

--- Graph a number in the Inspector (the last 120 values, up to 20 names).
---@param name string
---@param value number
function log.metric(name, value) end

--- Rows of tables as an aligned table in the console.
---@param rows table[]
---@param columns? string[]
---@return string
function log.table(rows, columns) end

--- Any value, nested tables and all, readably in the console.
---@param value any
---@return string
function log.inspect(value) end

--- A value as readable text (what log.inspect prints).
---@param value any
---@return string
function util.inspect(value) end

---@class test
test = {}

--- A test: it passes unless it errors (test.expect and test.equal error when they fail).
---@param name string
---@param fn fun()
function test.case(name, fn) end

--- A test that waits: call done() when it passed, or done("why") when it failed (5 s at most).
---@param name string
---@param fn fun(done: fun(error?: string))
function test.async(name, fn) end

--- Fail the test (with `msg`) unless `cond` is true.
---@param cond any
---@param msg? string
function test.expect(cond, msg) end

--- Equal, tables compared field by field.
---@param got any
---@param want any
---@param msg? string
function test.equal(got, want, msg) end

--- A made-up chat line delivered to this plugin's own handlers (not to other plugins).
---@param text string
---@param spec? { author: string, kind: string, amount: number, currency: string, member: boolean, mod: boolean, owner: boolean, count: integer }
function test.chat(text, spec) end

--- Run every test, one after the other; the console says how each went (also "Run tests" in the
--- Test environment).
---@param on_done? fun(passed: integer, failed: integer)
function test.run(on_done) end

--- How the last run went.
---@return { name: string, ok: boolean, error: string? }[]?
function test.results() end

------------------------------------------------------------------------------------------------
-- speech, the clipboard and the app
------------------------------------------------------------------------------------------------

---@class SpeakOptions
---@field voice? string             a name from tts.voices() (the PC's default otherwise)
---@field rate? number              0.5 to 2 (1)
---@field pitch? number             0 to 2 (1)
---@field volume? number            0 to 1 (1)
---@field interrupt? boolean        stop what is being said first (otherwise it waits its turn)

---@class tts
tts = {}

--- Say text out loud with the PC's voices (viewers hear it when OBS captures desktop audio).
--- `callback(true)` when it has been said. Needs "tts".
---@param text string               up to 500 characters
---@param opts? SpeakOptions
---@param callback? fun(ok: boolean?, error?: string)
function tts.say(text, opts, callback) end

--- Stop speaking (and forget what was waiting). Needs "tts".
function tts.stop() end

--- The voices this PC has: { { name, lang, default } }. Needs "tts".
---@param callback fun(voices: { name: string, lang: string, default: boolean }[]?)
function tts.voices(callback) end

---@class clipboard
clipboard = {}

--- Put text on the clipboard (a plugin can't read what is there). Needs "clipboard".
---@param text string
---@param callback? fun(ok: boolean?, error?: string)
function clipboard.copy(text, callback) end

---@class PageTab
---@field id string                 1-30 lowercase letters, digits, - or _
---@field title string
---@field icon? string              a lucide icon name ("chart", "users")
---@field content? UiNode

---@class PageSpec
---@field id? string                1-30 lowercase letters, digits, - or _ ("main" by default)
---@field title? string             its link in the sidebar (the plugin's name by default)
---@field icon? string              a lucide icon name ("chart", "list-music"); a puzzle piece by default
---@field order? number             where it goes among the plugin's pages (smaller first, then by title)
---@field content? UiNode           the page (without tabs)
---@field tabs? PageTab[]           up to 8 tabs: across the top of the page and under its link in the sidebar

---@class Page
---@field id string
local Page = {}
--- Show a new interface on the page (a page without tabs).
---@param tree UiNode
function Page:render(tree) end
--- Show a new interface in one of the page's tabs.
---@param tab string                the tab's id
---@param tree UiNode
function Page:render_tab(tab, tree) end
--- Take the page out of the sidebar.
function Page:remove() end

---@class nav
nav = {}

--- A page of the plugin's own in the sidebar, under a heading with the plugin's name (never mixed
--- with ForgeCast's own links). Up to 8 pages; each may have tabs. Calling it again with the same
--- id changes it. Needs "ui.app".
--- ```lua
--- local stats = nav.page{ id = "stats", title = "Stats", icon = "chart", tabs = {
---   { id = "today", title = "Today", content = ui.text("Loading…") },
---   { id = "chatters", title = "Chatters", icon = "users" },
--- } }
--- stats:render_tab("today", ui.grid{ columns = 3, ui.stat("Messages", 120), ui.stat("Chatters", 31), ui.stat("Tips", "$12") })
--- ```
---@param spec PageSpec
---@return Page
function nav.page(spec) end

--- Take a page out of the sidebar. Needs "ui.app".
---@param id string
function nav.remove(id) end

--- The same as nav.page. Needs "ui.app".
---@param spec PageSpec
---@return Page
function ui.page(spec) end

--- Take a page away (Page:remove does the same). Needs "ui.app".
---@param id string
function ui.remove_page(id) end

--- A command in the Ctrl+K command palette: { id, name, description, run }. Needs "ui.app".
---@param spec { id: string, name: string, description: string?, run: fun() }
---@return string id
function ui.command(spec) end

--- Take a command out of Ctrl+K. Needs "ui.app".
---@param id string
function ui.remove_command(id) end

---@class ContextMenuClick
---@field where "page"|"selection"|"link"|"chat"   what was right-clicked
---@field page string               the screen it was on ("/dashboard/")
---@field text? string              the text that was selected (where = "selection")
---@field link? string              the link's address (where = "link")
---@field chat? Chat                the chat message (where = "chat")

--- An item in ForgeCast's own right-click menu ("Translate this", "Add to my notes"). `where` says
--- when it shows: "page" (anywhere, the default), "selection" (on selected text), "link" (on a
--- link), "chat" (on a chat message), or a list of them. `on_click(ctx)` gets what was
--- right-clicked. `icon` is one of ui.icon's names. Up to 10 per plugin. Needs "ui.app".
---@param spec { id: string, label: string, icon: string?, where: string|string[]|nil, on_click: fun(ctx: ContextMenuClick) }
---@return string id
function ui.context_menu(spec) end

--- Take an item out of the right-click menu. Needs "ui.app".
---@param id string
function ui.remove_context_menu(id) end

--- An item in the menu of a chat message in ForgeCast ("Add to queue", "Give 100 points"):
--- `on_click(chat)` gets the message. Needs "ui.app".
---@param spec { id: string, label: string, on_click: fun(chat: Chat) }
---@return string id
function chats.menu(spec) end

--- Take a chat menu item away. Needs "ui.app".
---@param id string
function chats.remove_menu(id) end

--- A small badge (up to 12 characters) or a colour next to a viewer's name in ForgeCast's chat
--- ("VIP", "1,240 pts", "first time"). Needs "ui.app".
---@param user ChatAuthor|string
---@param spec { badge: string?, color: string?, title: string? }
function chats.decorate(user, spec) end

--- Take a viewer's badge away. Needs "ui.app".
---@param user ChatAuthor|string
---@return boolean found
function chats.undecorate(user) end

--- A chart: { kind = "line" | "bar" | "area" | "pie" | "donut", labels = { … }, series = { { name, values = { … }, color } }, height }.
---@param props table
---@return UiNode
function ui.chart(props) end

--- Tabs: ui.tabs{ value = "a", on_change = fn, ui.tab{ id = "a", label = "A", … }, ui.tab{ … } }.
---@param props table
---@return UiNode
function ui.tabs(props) end

--- One tab of ui.tabs: { id, label, …children }.
---@param props table
---@return UiNode
function ui.tab(props) end

--- A list the streamer can reorder by dragging: { name, items = { { id, label } }, on_change }.
--- `ev.value` is the ids in their new order.
---@param props table
---@return UiNode
function ui.sortable(props) end

--- A colour picker: { name, label, value = "#ff0044" }.
---@param props UiProps
---@return UiNode
function ui.color(props) end

--- A date (and time) picker: { name, label, value = ms, time = true }; `ev.value` is ms since 1970.
---@param props table
---@return UiNode
function ui.date(props) end

------------------------------------------------------------------------------------------------
-- interface: more elements
------------------------------------------------------------------------------------------------
-- More building blocks for pages, Dashboard sections, windows and dialogs. Every element also
-- takes `tooltip` (a hint on hover), `hidden` (not drawn), `grow` and `width` (pixels). Inputs take
-- `name`, `label`, `value`, `disabled` and `on_change(ev)`, like ui.input.
--
-- Icons (ui.icon, buttons, menus, pages, ui.empty):
-- activity archive arrow-down arrow-left arrow-right arrow-up at-sign award ban bell bell-off
-- bookmark bot calendar camera chart-bar chart-line chart-pie check chevron-down chevron-left
-- chevron-right chevron-up circle circle-alert circle-check circle-help clock cloud code coffee
-- coins copy crown database dices download ellipsis ellipsis-vertical external-link eye eye-off
-- file file-text filter flag flame folder gamepad-2 gauge gift globe hash headphones heart
-- hourglass house image inbox info keyboard layers layout-grid lightbulb link list list-checks
-- lock lock-open mail map-pin medal megaphone message-circle message-square mic mic-off minus
-- monitor moon music palette party-popper pause pencil pin play plug plus power puzzle radio
-- refresh-cw repeat rocket rotate-ccw save search send settings share-2 shield shield-check
-- shuffle skip-forward smile sparkles star sun swords tag target terminal thumbs-down thumbs-up
-- ticket timer trash-2 trending-down trending-up triangle-alert trophy tv upload user user-plus
-- users video volume-2 volume-x wallet wand-sparkles wifi wifi-off wrench x zap

---@class UiOption
---@field value string|number
---@field label string
---@field hint? string
---@field icon? string

--- Children in a grid: `columns` (1 to 6, 2 by default) or `min_width` (as many as fit), `gap`.
--- ```lua
--- ui.grid{ columns = 3, ui.stat("Viewers", 120), ui.stat("Likes", 48), ui.stat("Tips", "$12") }
--- ```
---@param props { columns?: integer, min_width?: integer, gap?: integer, [integer]: table }
---@return UiNode
function ui.grid(props) end

--- Its children in the middle, across and down.
---@param props table
---@return UiNode
function ui.center(props) end

--- Two children side by side (`ratio` of the width for the first, 0.2 to 0.8), stacked when narrow.
---@param props { ratio?: number, [1]: table, [2]: table }
---@return UiNode
function ui.split(props) end

--- A section that opens and closes: { title, open, on_toggle(ev) (ev.value is open), …children }.
---@param props { title?: string, open?: boolean, on_toggle?: fun(ev: UiEvent), [integer]: table }
---@return UiNode
function ui.collapse(props) end

--- Children under a small title, without a box: { title, description, …children }.
---@param props { title?: string, description?: string, [integer]: table }
---@return UiNode
function ui.group(props) end

--- A row spread across the width that wraps: actions on one side, filters on the other.
---@param props table
---@return UiNode
function ui.toolbar(props) end

--- A number that matters: ui.stat("Viewers", 120, { delta = "+12", trend = "up", hint = "last hour" }).
---@param label string|{ label?: string, value?: string|number, delta?: string|number, trend?: "up"|"down"|"flat", hint?: string, icon?: string }
---@param value? string|number
---@param props? { delta?: string|number, trend?: "up"|"down"|"flat", hint?: string, icon?: string }
---@return UiNode
function ui.stat(label, value, props) end

--- An icon by name ("heart", "trophy", "music", "users"…; the common lucide icons): ui.icon("heart", { size = 20, color = "#ff4fa3" }).
---@param name string|table
---@param props? { size?: integer, color?: string }
---@return UiNode
function ui.icon(name, props) end

--- A round picture (https) or someone's initials: { src, name, size }.
---@param props { src?: string, name?: string, size?: integer }
---@return UiNode
function ui.avatar(props) end

--- A key cap: ui.kbd("Ctrl+K").
---@param text string|table
---@return UiNode
function ui.kbd(text) end

--- A small tag: ui.chip("New", { tone = "accent", on_remove = fn }) (× when on_remove).
---@param text string|table
---@param props? { tone?: string, on_remove?: fun(ev: UiEvent) }
---@return UiNode
function ui.chip(text, props) end

--- A coloured dot and a few words: { text, tone = "success"|"warning"|"error"|"info"|"neutral", pulse }.
---@param props { text?: string, tone?: string, pulse?: boolean }
---@return UiNode
function ui.status(props) end

--- A calm banner: { tone = "info"|"success"|"warning"|"error", title, text, …children }.
---@param props { tone?: string, title?: string, text?: string, [integer]: table }
---@return UiNode
function ui.callout(props) end

--- What shows when there's nothing yet: { icon, title, text, …buttons }.
---@param props { icon?: string, title?: string, text?: string, [integer]: table }
---@return UiNode
function ui.empty(props) end

--- A spinning loader: { size, label }.
---@param props? { size?: integer, label?: string }
---@return UiNode
function ui.spinner(props) end

--- Grey placeholder lines while something loads: { lines = 3, height }.
---@param props? { lines?: integer, height?: integer }
---@return UiNode
function ui.skeleton(props) end

--- Things in order: { items = { { title, text, time, tone } } }.
---@param props { items?: { title?: string, text?: string, time?: string, tone?: string }[] }
---@return UiNode
function ui.timeline(props) end

--- Formatted text: **bold**, *italic*, `code`, [links](https://…), lists, headings and quotes.
--- Nothing else is drawn (no HTML, no pictures).
---@param text string|table
---@return UiNode
function ui.markdown(text) end

--- A quote: { text, cite }.
---@param props { text?: string, cite?: string }
---@return UiNode
function ui.quote(props) end

--- A round meter: { value = 0 to 1, label, text, tone }.
---@param props { value?: number, label?: string, text?: string, tone?: string, size?: integer }
---@return UiNode
function ui.gauge(props) end

--- A small line of numbers: { values = { … }, color, height }.
---@param props { values?: number[], color?: string, height?: integer }
---@return UiNode
function ui.sparkline(props) end

--- A clock that ticks by itself (no need to redraw): `to` counts down to a time, `from` counts up
--- from one (ms since 1970); format "auto", "mm:ss" or "hh:mm:ss"; `done_text` when it reaches 0.
--- ```lua
--- ui.timer{ to = time.now() + 5 * 60 * 1000, done_text = "Back!" }
--- ```
---@param props { to?: integer, from?: integer, format?: string, done_text?: string, label?: string, color?: string, size?: "sm"|"md"|"lg" }
---@return UiNode
function ui.timer(props) end

--- The same as ui.timer (a name that reads better for one counting down).
---@param props { to?: integer, from?: integer, format?: string, done_text?: string, label?: string, color?: string, size?: "sm"|"md"|"lg" }
---@return UiNode
function ui.countdown(props) end

--- Labels and values in two columns: { items = { { label, value } } }.
---@param props { items?: { label?: string, value?: string|number }[] }
---@return UiNode
function ui.properties(props) end

--- Steps with the current one lit: { steps = { "Connect", "Pick", "Go live" }, current = 2 }.
---@param props { steps?: string[], current?: integer }
---@return UiNode
function ui.steps(props) end

--- A number with − and + buttons: { name, label, value, min, max, step, suffix, on_change }.
---@param props { name?: string, label?: string, value?: number, min?: number, max?: number, step?: number, suffix?: string, on_change?: fun(ev: UiEvent) }
---@return UiNode
function ui.number(props) end

--- One choice of a few, all shown: { name, label, options = { { value, label, hint } }, value, on_change }.
---@param props { name?: string, label?: string, options?: UiOption[], value?: any, direction?: "row"|"column", on_change?: fun(ev: UiEvent) }
---@return UiNode
function ui.radio(props) end

--- One choice of a few, as joined buttons: { name, options = { { value, label, icon } }, value, on_change }.
---@param props { name?: string, options?: UiOption[], value?: any, on_change?: fun(ev: UiEvent) }
---@return UiNode
function ui.segmented(props) end

--- Several choices: { name, label, options, value = { … }, on_change } (`ev.value` is a list).
---@param props { name?: string, label?: string, options?: UiOption[], value?: any[], on_change?: fun(ev: UiEvent) }
---@return UiNode
function ui.multiselect(props) end

--- Words the streamer types (Enter adds one): { name, label, value = { … }, placeholder, max, on_change }.
---@param props { name?: string, label?: string, value?: string[], placeholder?: string, max?: integer, on_change?: fun(ev: UiEvent) }
---@return UiNode
function ui.tags(props) end

--- A search box: `on_change` a moment after typing stops, `on_submit` on Enter.
---@param props { name?: string, placeholder?: string, value?: string, on_change?: fun(ev: UiEvent), on_submit?: fun(ev: UiEvent) }
---@return UiNode
function ui.search(props) end

--- Stars: { name, label, value, max = 5, on_change }.
---@param props { name?: string, label?: string, value?: integer, max?: integer, on_change?: fun(ev: UiEvent) }
---@return UiNode
function ui.rating(props) end

--- A slider with two ends: { name, label, min, max, step, value = { lo, hi }, on_change }.
---@param props { name?: string, label?: string, min?: number, max?: number, step?: number, value?: number[], on_change?: fun(ev: UiEvent) }
---@return UiNode
function ui.range(props) end

--- A time of day: { name, label, value = "20:30", on_change }.
---@param props { name?: string, label?: string, value?: string, on_change?: fun(ev: UiEvent) }
---@return UiNode
function ui.time(props) end

--- Records a key combination, in the form hotkeys.on takes ("Ctrl+Shift+KeyB").
---@param props { name?: string, label?: string, value?: string, on_change?: fun(ev: UiEvent) }
---@return UiNode
function ui.hotkey(props) end

--- A button with a menu: { label or icon, align, items = { { label, icon, danger, disabled, on_click } | { divider = true } } }
--- (`ev.value` is the item's label).
---@param props { label?: string, icon?: string, items?: table[] }
---@return UiNode
function ui.menu(props) end

--- A button that copies text to the clipboard when the streamer clicks it: ui.copy("discord.gg/abc", { label = "Copy invite" }).
---@param text string|table
---@param props? { label?: string }
---@return UiNode
function ui.copy(text, props) end

--- Page numbers: { page, pages, on_change } (`ev.value` is the page asked for).
---@param props { page?: integer, pages?: integer, on_change?: fun(ev: UiEvent) }
---@return UiNode
function ui.pagination(props) end

--- Nested items to pick from: { items = { { id, label, icon, children = { … } } }, value, on_change }.
---@param props { items?: table[], value?: string, on_change?: fun(ev: UiEvent) }
---@return UiNode
function ui.tree(props) end

--- A button that opens a small panel with the children: { label, icon, panel_width, align, on_toggle, …children }.
--- What is typed in it is gone when it closes.
---@param props { label?: string, icon?: string, panel_width?: integer, align?: "start"|"center"|"end", on_toggle?: fun(ev: UiEvent), [integer]: table }
---@return UiNode
function ui.popover(props) end

--- Cards in columns the streamer drags between (a song queue, ideas, to do):
--- { columns = { { id, title, items = { { id, label, hint } } } }, on_change } (`ev.value` is the new columns).
---@param props { columns?: table[], on_change?: fun(ev: UiEvent) }
---@return UiNode
function ui.kanban(props) end

------------------------------------------------------------------------------------------------
-- money, events, goals and the channel
------------------------------------------------------------------------------------------------

---@class money
money = {}

--- The streamer's currency ("INR", "USD"…).
---@return string
function money.currency() end

--- An amount in another currency (the streamer's by default), from today's rates; nil when a
--- currency isn't known.
---@param value number
---@param from string
---@param to? string
---@return number?
function money.convert(value, from, to) end

--- An amount the way people write it: "₹1,23,456.00", "$5.00", "12.00 SEK" (the streamer's
--- currency by default).
---@param value number
---@param currency? string
---@return string
function money.format(value, currency) end

---@class AppEvent
---@field id string
---@field ts integer
---@field type "superchat"|"supersticker"|"member.new"|"member.gift"|"member.milestone"|"subscriber.new"
---@field name string               who
---@field amount? { value: number, currency: string }
---@field text? string
---@field detail? string            "Level 1", "5 gifted"…
---@field simulated boolean         a test

---@class events
events = {}

--- The app's latest events (what Recent events shows), newest first. Needs "stream.read".
---@param n? integer                20 by default
---@return AppEvent[]
function events.recent(n) end

--- Hear an event type ("superchat", "member.new", "subscriber.new"…, or "any"). "flow.<name>" hears a chat
--- action's "Tell plugins" block: the event is { name, text, user = { id, name }, message, args, rule }.
--- Needs "stream.read".
---@param event string
---@param handler fun(e: AppEvent)
---@return integer id
function events.on(event, handler) end

--- Stop a handler made with events.on.
---@param id integer
---@return boolean found
function events.off(id) end

---@class Goal
---@field name string
---@field title string
---@field current number
---@field target number
---@field percent number

---@class goals
goals = {}

--- Every goal the overlays can show ({{goal.<name>.current}}…). Needs "overlay".
---@return Goal[]
function goals.list() end

--- One goal, or nil. Needs "overlay".
---@param name string
---@return Goal?
function goals.get(name) end

--- Make or change a goal: { current, target, title }. Needs "overlay".
---@param name string               lowercase letters, digits or _ ("subs", "coffee")
---@param spec { current: number?, target: number?, title: string? }
---@param callback? fun(goal: Goal?, error?: string)
function goals.set(name, spec, callback) end

--- Add to a goal's progress (a Super Chat, a tip from outside YouTube…). Needs "overlay".
---@param name string
---@param amount number
---@param callback? fun(goal: Goal?, error?: string)
function goals.add(name, amount, callback) end

--- Hear Event.GoalChange (goals.on("change", fn)). Needs "overlay".
---@param event string
---@param handler fun(goal: Goal)
---@return integer id
function goals.on(event, handler) end

--- Stop a handler made with goals.on.
---@param id integer
---@return boolean found
function goals.off(id) end

---@class ChannelVideo
---@field id string
---@field title string
---@field published_at? integer
---@field views? integer
---@field thumbnail? string

---@class channel
channel = {}

--- The channel: { id, name, handle, subscribers, avatar, banner }. Needs "stream.read".
---@return table
function channel.info() end

--- Its latest videos, newest first (5 by default). Needs "stream.read".
---@param n? integer
---@return ChannelVideo[]
function channel.videos(n) end

--- Its scheduled streams: { { id, title, starts_at } }. Needs "stream.read".
---@return table[]
function channel.upcoming() end

------------------------------------------------------------------------------------------------
-- your YouTube channel
------------------------------------------------------------------------------------------------
-- Act on the streamer's channel through YouTube's own API, which they connected on the website.
-- Each call uses part of the channel's daily YouTube allowance (a chat message 20 units, deleting
-- or banning 50), so a plugin may send 10 chat messages a minute, 2 polls a minute, 20 moderation
-- actions a minute and 5 changes to the stream in 10 minutes. Every answer comes later, through
-- the callback: `callback(result)` or `callback(nil, why)` (not live, not connected, allowance used up…).

---@class YoutubeBan
---@field id string                 what youtube.unban takes
---@field type "temporary"|"permanent"
---@field seconds integer?          how long, for a timeout

---@class YoutubePoll
---@field id string
---@field question string
---@field options string[]

---@class YoutubePollResult
---@field id string
---@field question string
---@field status "active"|"closed"|"unknown"
---@field options { text: string, votes: integer? }[]

---@class YoutubeHealthIssue
---@field type string
---@field severity "info"|"warning"|"error"
---@field reason string
---@field description string

---@class YoutubeStreamHealth
---@field id string
---@field title string
---@field status "active"|"inactive"|"error"|"created"|"ready"|"unknown"   active: YouTube is receiving it
---@field health "good"|"ok"|"bad"|"noData"|nil
---@field issues YoutubeHealthIssue[]
---@field resolution? string
---@field frameRate? string
---@field ingestion? string

---@class YoutubeStats
---@field id string
---@field title string
---@field views integer?
---@field likes integer?            nil when the channel hides them
---@field comments integer?
---@field concurrentViewers integer?  only while it's live
---@field actualStartTime string?   ISO time
---@field actualEndTime string?
---@field scheduledStartTime string?

---@class YoutubeQuota
---@field state "ok"|"needs_relink"|"not_linked"|"not_configured"
---@field usedToday integer
---@field budget integer            the channel's daily share
---@field resetsAt string           when it starts again (midnight Pacific), ISO

---@class youtube
youtube = {}

--- Write in the live chat as the channel (1 to 200 characters). Needs "youtube.chat".
--- ```lua
--- chats.on(Event.NewChat, function(m)
---   if m.text == "!discord" then youtube.send_chat("Join us: discord.gg/example") end
--- end)
--- ```
---@param text string
---@param callback? fun(message: { id: string }?, error?: string)
---@return integer id
function youtube.send_chat(text, callback) end

--- Start a poll in the live chat: a question (up to 100 characters) and 2 to 4 answers (35 each).
--- Needs "youtube.chat".
---@param question string
---@param options string[]
---@param callback? fun(poll: YoutubePoll?, error?: string)
---@return integer id
function youtube.poll(question, options, callback) end

--- End the open poll (or the one with that id) and get its votes. Needs "youtube.chat".
---@param id? string|fun(result: YoutubePollResult?, error?: string)
---@param callback? fun(result: YoutubePollResult?, error?: string)
---@return integer id
function youtube.end_poll(id, callback) end

--- Take a message out of the live chat. Give the chat message itself: the app finds it in YouTube's
--- list by who wrote what. Needs "youtube.moderate".
---@param message ChatMessage|string
---@param callback? fun(ok: boolean?, error?: string)
---@return integer id
function youtube.delete_message(message, callback) end

--- Keep a viewer out of the live chat for 10 seconds to a day. Give a chat message, its author or a
--- channel id ("UC…"). Needs "youtube.moderate".
--- ```lua
--- youtube.timeout(m, 300, function(ban) if ban then print("timed out until it runs out") end end)
--- ```
---@param who ChatMessage|table|string
---@param seconds integer
---@param callback? fun(ban: YoutubeBan?, error?: string)
---@return integer id
function youtube.timeout(who, seconds, callback) end

--- Ban a viewer from the live chat for good. Needs "youtube.moderate".
---@param who ChatMessage|table|string
---@param callback? fun(ban: YoutubeBan?, error?: string)
---@return integer id
function youtube.ban(who, callback) end

--- Lift a ban, or end a timeout early: give what youtube.ban or youtube.timeout answered. Needs
--- "youtube.moderate".
---@param ban YoutubeBan|string
---@param callback? fun(ok: boolean?, error?: string)
---@return integer id
function youtube.unban(ban, callback) end

--- Change the live stream's title (up to 100 characters, no < or >); before you go live, the next
--- scheduled stream's. Needs "youtube.manage".
---@param title string
---@param callback? fun(ok: boolean?, error?: string)
---@return integer id
function youtube.set_title(title, callback) end

--- Change the live stream's description (up to 5000 characters, no < or >); before you go live, the
--- next scheduled stream's. Needs "youtube.manage".
---@param text string
---@param callback? fun(ok: boolean?, error?: string)
---@return integer id
function youtube.set_description(text, callback) end

--- How YouTube says the stream is arriving: the encoder's connection, resolution and any issues
--- (the stream key is never shown). The answer is reused for 15 seconds. Needs "stream.read".
---@param callback fun(streams: YoutubeStreamHealth[]?, error?: string)
---@return integer id
function youtube.health(callback) end

--- A video's numbers from YouTube: views, likes, comments and, while live, concurrent viewers. The
--- live stream by default. Needs "stream.read".
---@param video? string|fun(stats: YoutubeStats?, error?: string)   an 11-character video id
---@param callback? fun(stats: YoutubeStats?, error?: string)
---@return integer id
function youtube.stream_stats(video, callback) end

--- How much of the channel's daily YouTube allowance is used. Needs "stream.read".
---@param callback fun(quota: YoutubeQuota?, error?: string)
---@return integer id
function youtube.quota(callback) end

--- The channel's scheduled streams (the same as channel.upcoming). Needs "stream.read".
---@return table[]
function youtube.upcoming() end

------------------------------------------------------------------------------------------------
-- hotkeys, buttons, lights and controllers
------------------------------------------------------------------------------------------------
-- The streamer adds lights, Home Assistant and the Stream Deck once, in Devices; MIDI controllers
-- plugged into the PC are found by themselves. A plugin never sees addresses or tokens.

---@class hotkeys
hotkeys = {}

--- Run a function when a key is pressed anywhere, even while a game has the focus. Keys are
--- written like "Ctrl+Shift+KeyB", "Alt+Digit1" or "F9": a letter, digit or anything you type needs
--- Ctrl, Alt or Win with it. A key the app or another plugin uses is refused. At most 10. Needs
--- "hotkeys".
--- ```lua
--- hotkeys.on("Ctrl+Shift+KeyC", function() obs.save_replay() end)
--- ```
---@param keys string
---@param handler fun(e: { keys: string })
---@return integer id
function hotkeys.on(keys, handler) end

--- Let go of a hotkey. Needs "hotkeys".
---@param keys string
---@return boolean found
function hotkeys.off(keys) end

--- The keys this plugin registered. Needs "hotkeys".
---@return string[]
function hotkeys.list() end

---@class PadButton
---@field id string                 1-30 lowercase letters, digits, - or _
---@field label string              up to 40 characters
---@field group? string             the heading it goes under (the plugin's name by default)
---@field color? string             "#rrggbb"
---@field on_press fun()

---@class remote
remote = {}

--- A button on the phone remote (Beta Lab). The same id again changes it. At most 24 buttons.
--- Needs "ui.app".
---@param spec PadButton
---@return string id
function remote.button(spec) end

--- Take a button off the phone remote. Needs "ui.app".
---@param id string
function remote.remove(id) end

---@class deck
deck = {}

--- A button the Stream Deck can show, through ForgeCast's Stream Deck plugin: the streamer puts a
--- "ForgeCast button" on a key and picks it. Call it again to change its label (a counter on a
--- key). Needs "ui.app".
--- ```lua
--- local deaths = 0
--- local function show() deck.button{ id = "deaths", label = "Deaths: " .. deaths, on_press = add } end
--- function add() deaths = deaths + 1 show() end
--- show()
--- ```
---@param spec PadButton
---@return string id
function deck.button(spec) end

--- Take a button off the Stream Deck. Needs "ui.app".
---@param id string
function deck.remove(id) end

---@class LightInfo
---@field id string
---@field name string
---@field kind "elgato"|"hue"|"nanoleaf"|"govee"
---@field color boolean             it can show colours (a Key Light only does white)
---@field online boolean?

---@class LightLook
---@field color? string             "#rrggbb", or red, green, blue, white, yellow, orange, purple, pink, cyan
---@field brightness? number        0 to 100
---@field kelvin? number            white light, 2000 (warm) to 7000 (cold)
---@field on? boolean
---@field light? string             one light, by name, id or brand ("hue"); all of them by default

---@class lights
lights = {}

--- The lights the streamer added in Devices. Needs "lights".
---@return LightInfo[]
function lights.list() end

--- Change the lights: callback(how many changed) or callback(nil, why). Up to 10 changes a
--- second. Needs "lights".
--- ```lua
--- chats.on(Event.SuperChat, function() lights.set{ color = "#ffd700", brightness = 100 } end)
--- ```
---@param look LightLook
---@param callback? fun(changed: integer?, error?: string)
---@return integer id
function lights.set(look, callback) end

--- Flash a colour (2 times, 300 ms each by default), then put each light back as it was. Needs
--- "lights".
---@param color string
---@param opts? { times: integer?, ms: integer?, light: string? }|fun(changed: integer?, error?: string)
---@param callback? fun(changed: integer?, error?: string)
---@return integer id
function lights.flash(color, opts, callback) end

--- Turn the lights (or one) off. Needs "lights".
---@param light? string|fun(changed: integer?, error?: string)
---@param callback? fun(changed: integer?, error?: string)
---@return integer id
function lights.off(light, callback) end

---@class HaState
---@field entity_id string
---@field state string              "on", "off", "21.5"…
---@field name string?
---@field attributes? table
---@field last_changed? string

---@class homeassistant
homeassistant = {}

--- Call a Home Assistant service ("light.turn_on", "switch.toggle", "scene.turn_on"…):
--- callback(what changed). Locks, alarms, covers (garage doors), cameras and Home Assistant's own
--- management are never allowed. Needs "homeassistant".
--- ```lua
--- homeassistant.call("light.turn_on", { entity_id = "light.desk", rgb_color = { 255, 0, 80 } })
--- ```
---@param service string
---@param data? table|fun(changed: HaState[]?, error?: string)
---@param callback? fun(changed: HaState[]?, error?: string)
---@return integer id
function homeassistant.call(service, data, callback) end

--- One entity's state. Needs "homeassistant".
---@param entity string             "sensor.room_temperature"
---@param callback fun(state: HaState?, error?: string)
---@return integer id
function homeassistant.state(entity, callback) end

--- Every entity (or one domain's, like "light"). Needs "homeassistant".
---@param domain? string|fun(states: HaState[]?, error?: string)
---@param callback? fun(states: HaState[]?, error?: string)
---@return integer id
function homeassistant.states(domain, callback) end

---@class MidiMessage
---@field type "note_on"|"note_off"|"cc"|"program"|"pitch"|"aftertouch"|"channel_pressure"
---@field channel integer           1 to 16
---@field device string             the controller's name
---@field note? integer
---@field velocity? integer
---@field controller? integer       cc: which knob or fader
---@field value? integer            cc: 0 to 127; pitch: -8192 to 8191
---@field program? integer
---@field pressure? integer
---@field bytes integer[]

---@class midi
midi = {}

--- The MIDI controllers on this PC: { inputs = {…}, outputs = {…}, listening }. Needs "midi".
---@return { inputs: string[], outputs: string[], listening: boolean }
function midi.devices() end

--- Hear every MIDI controller (Event.Midi): pads, keys, knobs and faders. New controllers are
--- picked up within a few seconds. Needs "midi".
--- ```lua
--- midi.on(function(m)
---   if m.type == "note_on" and m.note == 36 then sounds.play("airhorn") end
---   if m.type == "cc" and m.controller == 7 then obs.set_volume("Music", m.value / 127 * 60 - 60) end
--- end)
--- ```
---@param handler fun(m: MidiMessage)
---@return integer id
function midi.on(handler) end

--- Stop a handler made with midi.on. Needs "midi".
---@param id integer
---@return boolean found
function midi.off(id) end

--- Send to a controller (light a pad): { type = "note_on", note, velocity, channel }, { type = "cc",
--- controller, value }, { type = "program", program } or raw bytes. Needs "midi".
---@param device string             its name, or part of it
---@param message table|integer[]
---@param callback? fun(ok: boolean?, error?: string)
---@return integer id
function midi.send(device, message, callback) end

------------------------------------------------------------------------------------------------
-- the microphone
------------------------------------------------------------------------------------------------
-- Windows' own speech recognition on this PC: audio never leaves it, plugins get text. ForgeCast
-- shows when a plugin is listening, and the streamer can stop them all.

---@class Heard
---@field phrase string             the phrase as the plugin listed it (lowercase, no punctuation)
---@field confidence "low"|"medium"|"high"

---@class speech
speech = {}

--- Voice commands: only these phrases count (1 to 50, a few words each), and works offline.
--- Calling it again replaces the list. Needs "microphone".
--- ```lua
--- speech.listen({ "clip that", "start the timer" }, function(h)
---   if h.phrase == "clip that" then obs.save_replay() end
--- end)
--- ```
---@param phrases string[]
---@param handler fun(h: Heard)
---@param opts? { confidence: "low"|"medium"|"high" }   the least sure it may be ("medium")
---@return integer id
function speech.listen(phrases, handler, opts) end

--- Stop listening for this plugin's phrases. Needs "microphone".
function speech.stop() end

---@class captions
captions = {}

--- Live captions (Event.Captions): what the streamer says, as it is said (`final` false) and
--- when a sentence is done (`final` true). Needs Windows' online speech recognition setting on.
--- Needs "microphone".
---@param handler fun(c: { text: string, final: boolean })
---@return integer id
function captions.on(handler) end

--- Stop a handler made with captions.on. Needs "microphone".
---@param id integer
---@return boolean found
function captions.off(id) end

------------------------------------------------------------------------------------------------
-- tips, connected services and Discord
------------------------------------------------------------------------------------------------
-- The streamer links Ko-fi, Streamlabs, StreamElements, Spotify, Google and Discord once, in
-- Connections. A plugin never sees their passwords, tokens or keys.

---@class Tip
---@field source "kofi"|"streamlabs"|"streamelements"
---@field id string
---@field name string
---@field amount { value: number, currency: string }?
---@field message string
---@field ts integer                milliseconds since 1970

---@class donations
donations = {}

--- Hear tips from outside YouTube (Event.Tip; donations.on("tip", fn)). They also arrive in the app's
--- event feed as support, so events.on("superchat") hears them too. Needs "donations".
--- ```lua
--- donations.on("tip", function(t)
---   if t.amount then goals.add("support", money.convert(t.amount.value, t.amount.currency)) end
--- end)
--- ```
---@param event string
---@param handler fun(t: Tip)
---@return integer id
function donations.on(event, handler) end

--- Stop a handler made with donations.on. Needs "donations".
---@param id integer
---@return boolean found
function donations.off(id) end

--- The latest tips, newest first (10 by default, up to 50). Needs "donations".
---@param n? integer
---@return Tip[]
function donations.recent(n) end

---@class OauthResponse
---@field ok boolean                status 200 to 299
---@field status integer
---@field body string
---@field json any                  the body read as JSON, when it is

---@class oauth
oauth = {}

--- Ask to use a service the streamer connects in ForgeCast: "spotify" or "google" (scopes:
--- "sheets", "calendar.readonly", "drive.file"). The streamer says yes once; callback({ provider,
--- account }) or callback(nil, why). Call it when the plugin starts. Needs "oauth".
--- ```lua
--- oauth.connect("spotify", { scopes = { "user-read-currently-playing" } }, function(ok)
---   if not ok then return end
---   oauth.request("spotify", { url = "https://api.spotify.com/v1/me/player/currently-playing" }, function(r)
---     if r.json then overlay.set("song", r.json.item.name) end
---   end)
--- end)
--- ```
---@param provider "spotify"|"google"
---@param opts? { scopes: string[] }|fun(result: { provider: string, account: string }?, error?: string)
---@param callback? fun(result: { provider: string, account: string }?, error?: string)
---@return integer id
function oauth.connect(provider, opts, callback) end

--- Whether a service is connected: { connected, account, scopes } (`account` only once the
--- streamer allowed this plugin to use it). Needs "oauth".
---@param provider string
---@param callback fun(status: { connected: boolean, account: string?, scopes: string[] }?, error?: string)
---@return integer id
function oauth.status(provider, callback) end

--- A request to that service's API (https://api.spotify.com; Google: https://sheets.googleapis.com,
--- and Calendar and Drive on https://www.googleapis.com), signed in as the streamer. Only after
--- oauth.connect said yes; the plugin never sees the token.
--- Needs "oauth".
---@param provider string
---@param req { url: string, method: string?, json: any?, body: string? }
---@param callback fun(res: OauthResponse?, error?: string)
---@return integer id
function oauth.request(provider, req, callback) end

---@class discord
discord = {}

--- Post in the streamer's Discord server through ForgeCast's bot (added in Connections). It never
--- pings @everyone, roles or people. Up to 10 messages a minute; an embed's title is up to 256
--- characters, its description 4096, its image https. Needs "discord".
--- ```lua
--- stream.on(Event.StreamStart, function(s) discord.send("announcements", "We're live: " .. (s.title or "come say hi")) end)
--- ```
---@param channel string            the channel's name (with or without #) or id
---@param message string|{ content: string?, embed: { title: string?, description: string?, url: string?, color: string?, image: string? }? }
---@param callback? fun(result: { id: string }?, error?: string)
---@return integer id
function discord.send(channel, message, callback) end

--- The server's text channels the bot can post in: { guild = { id, name }, channels = { { id,
--- name } } }. Needs "discord".
---@param callback fun(result: { guild: { id: string, name: string }?, channels: { id: string, name: string }[] }?, error?: string)
---@return integer id
function discord.channels(callback) end

------------------------------------------------------------------------------------------------
-- channel points and viewers
------------------------------------------------------------------------------------------------
-- One wallet for the channel, shared by every plugin and shown on the Points page. Chat can earn
-- points by itself there (chatting, Super Chats, memberships). A viewer is a chat author table, a
-- channel id or a name.

---@class points
points = {}

--- What the points are called ("points", or the streamer's own name for them). Needs "points".
---@return string
function points.name() end

--- A viewer's points (0 when they have none). Needs "points".
---@param user ChatAuthor|string
---@return integer
function points.get(user) end

--- { name, points, earned, spent }, or nil. Needs "points".
---@param user ChatAuthor|string
---@return table?
function points.account(user) end

--- Give points (negative takes them, never below zero): the new balance. Needs "points".
---@param user ChatAuthor|string
---@param n integer
---@param reason? string            shown on the Points page
---@return integer
function points.add(user, n, reason) end

--- Take points if they have enough: true and the new balance, or false and why. Needs "points".
---@param user ChatAuthor|string
---@param n integer
---@param reason? string
---@return boolean ok
---@return integer|string balance_or_why
function points.take(user, n, reason) end

--- Set a viewer's points. Needs "points".
---@param user ChatAuthor|string
---@param n integer
---@param reason? string
---@return integer
function points.set(user, n, reason) end

--- Move points between viewers (false when the giver hasn't enough). Needs "points".
---@param from ChatAuthor|string
---@param to ChatAuthor|string
---@param n integer
---@return boolean
function points.transfer(from, to, n) end

--- The richest viewers: { { id, name, points, rank } } (10 by default). Needs "points".
---@param n? integer
---@return table[]
function points.top(n) end

--- Hear Event.PointsChange (points.on("change", fn)). Needs "points".
---@param event string
---@param handler fun(e: table)
---@return integer id
function points.on(event, handler) end

--- Stop a handler made with points.on.
---@param id integer
---@return boolean found
function points.off(id) end

---@class RedeemContext : CommandContext
---@field text string               what they wrote after the redeem's name
---@field balance integer           their points after paying

---@class redeems
redeems = {}

--- Something viewers buy with points: chat types "!redeem <name>" (and "!redeem" lists them). The cost
--- is taken first; `on_redeem` returning false (or failing) gives it back. Every plugin's redeems
--- show on the Points page. Needs "points" and "chat.read".
---@param spec { name: string, cost: integer, cooldown: number?, per_user: number?, description: string?, on_redeem: fun(ctx: RedeemContext): boolean? }
---@return string name
function redeems.add(spec) end

--- Stop a redeem.
---@param name string
---@return boolean found
function redeems.remove(name) end

--- This plugin's redeems: { { name, cost, cooldown, per_user, description, used } }.
---@return table[]
function redeems.list() end

---@class Prediction
local Prediction = {}
--- { question, status = "open"|"locked"|"resolved"|"cancelled", total, winner, outcomes = { { label, points, people } } }.
---@return table
function Prediction:status() end
--- Stop taking bets.
---@return boolean
function Prediction:lock() end
--- Pick the winning outcome: its backers share the whole pot, in proportion to their bets.
---@param outcome integer
---@return boolean
function Prediction:resolve(outcome) end
--- Call it off and give every bet back.
---@return boolean
function Prediction:cancel() end
--- Bet for a viewer (what "!bet 1 50" does): their whole bet, or nil and why.
---@param user ChatAuthor|string
---@param outcome integer
---@param amount integer
---@return integer? total
---@return string? why
function Prediction:bet(user, outcome, amount) end
--- Run a function whenever the bets or the status change.
---@param fn fun(status: table)
function Prediction:on_change(fn) end

---@class predictions
predictions = {}

--- Viewers bet points on what happens: "!bet <outcome number> <points>". Locks by itself after
--- `seconds`. Overlays get prediction_question, prediction_status and prediction_<n>_label /
--- _points / _percent. Needs "points" (and "chat.read" for !bet).
---@param spec { question: string, outcomes: string[]?, seconds: number?, min: integer?, max: integer?, dashboard: boolean? }
---@return Prediction
function predictions.start(spec) end

--- The prediction running now (or the last one), or nil.
---@return Prediction?
function predictions.current() end

---@class Regular
---@field id string
---@field name string
---@field avatar? string
---@field first_seen integer        ms since 1970
---@field last_seen integer
---@field messages integer
---@field streams integer           streams they chatted in
---@field member boolean
---@field moderator boolean
---@field super_chats integer

---@class viewers
viewers = {}

--- What this PC knows of a viewer across streams, or nil. Needs "viewers".
---@param user ChatAuthor|string
---@return Regular?
function viewers.get(user) end

--- The top viewers by "messages" (default), "streams", "super_chats", "last_seen" or "first_seen".
--- Needs "viewers".
---@param n? integer
---@param by? string
---@return Regular[]
function viewers.top(n, by) end

--- How many viewers have chatted. Needs "viewers".
---@return integer
function viewers.count() end

------------------------------------------------------------------------------------------------
-- your PC
------------------------------------------------------------------------------------------------
-- Read at most once a second, shared by every plugin. Nothing leaves the PC unless a plugin with
-- "network" sends it.

---@class PcSpecs
---@field os string
---@field cpu { name: string, cores: integer?, threads: integer, mhz: integer? }
---@field ram_mb integer
---@field gpus { name: string, vram_mb: integer }[]
---@field displays { width: integer, height: integer, scale: number }[]

---@class PcUsage
---@field cpu_percent number
---@field ram_used_mb integer
---@field ram_total_mb integer
---@field gpu_percent? number       nil until a second reading (and on PCs that don't report it)
---@field disk_free_mb? integer     on the Windows drive
---@field net_up_kbps number
---@field net_down_kbps number
---@field uptime_sec integer

---@class system
system = {}

--- What the PC is: Windows, processor, memory, graphics cards and screens. Needs "system".
---@return PcSpecs
function system.specs() end

--- How busy the PC is now. Needs "system".
---@return PcUsage
function system.usage() end

--- What ForgeCast itself uses: { memory_mb, cpu_percent, gpu_percent }. Needs "system".
---@return table
function system.app() end

--- { has_battery, percent, plugged_in }. Needs "system".
---@return table
function system.battery() end

--- { cpu, gpu } in °C where Windows reports them (often only cpu, sometimes neither). Needs "system".
---@return table
function system.temps() end

--- Keep the PC and its screen from sleeping (until false, or the plugin stops). Needs "system".
---@param on boolean
function system.keep_awake(on) end

--- How fast a well-known server answers: { target, best_ms, average_ms }. "youtube" is YouTube's
--- ingest server (where your stream goes). Needs "system".
---@param target "youtube"|"twitch"|"cloudflare"|"google"
---@param callback fun(result: table?, error?: string)
function system.ping(target, callback) end

--- Watch the PC: Event.SystemBusy after `seconds` over a limit (percent), Event.SystemCalm after as
--- many under them all. Needs "system".
---@param spec { cpu: number?, ram: number?, gpu: number?, seconds: integer? }
function system.watch(spec) end

--- Stop watching.
function system.unwatch() end

--- Hear Event.SystemBusy or Event.SystemCalm (start watching with system.watch).
---@param event string
---@param fn fun(e: table)
---@return integer id
function system.on(event, fn) end

---@class activity
activity = {}

--- Milliseconds since the last key press or mouse move anywhere on the PC (only when, never what).
--- Needs "activity".
---@return integer?
function activity.idle_ms() end

--- The game being played (the one in front, or any known game running): { name, exe }, or nil.
--- Needs "activity".
---@return { name: string, exe: string }?
function activity.game() end

--- How long without a key press counts as away (120 s by default, at least 10).
---@param seconds integer
function activity.away_after(seconds) end

--- Hear Event.Away, Event.Back or Event.GameChange. Needs "activity".
---@param event string
---@param fn fun(e: table)
---@return integer id
function activity.on(event, fn) end

---@class NowPlaying
---@field title string
---@field artist string
---@field album string
---@field app string                "Spotify", "Microsoft Edge"…
---@field playing boolean

---@class media
media = {}

--- What is playing on the PC (any player Windows' media controls know), or nil. Needs "media".
---@return NowPlaying?
function media.now_playing() end

--- Hear Event.SongChange: `fn(song)` (nil when the music stopped). Needs "media".
---@param event string
---@param fn fun(song: NowPlaying?)
---@return integer id
function media.on(event, fn) end

------------------------------------------------------------------------------------------------
-- internet
------------------------------------------------------------------------------------------------

--- The options of a request. (`http.get` and `http.post` take the address as their own argument.)
---@class HttpOptions
---@field method? "GET"|"POST"|"PUT"|"PATCH"|"DELETE"
---@field headers? table<string, string>
---@field body? string
---@field json? table               sent as JSON (sets the content type)
---@field timeout? number           seconds (1-15, default 10)

---@class HttpRequest : HttpOptions
---@field url string                https only, public sites only

---@class HttpResponse
---@field ok boolean                the request worked and the status was 2xx
---@field status integer
---@field body string
---@field json? any                 the body parsed, when it is JSON
---@field headers table<string, string>   lower-case names
---@field error? string             why it failed (no answer, blocked…)

---@class http
http = {}

--- Make a request. `callback` is called later with the answer. Needs "network".
---@param spec HttpRequest
---@param callback? fun(res: HttpResponse)
---@return integer id
function http.request(spec, callback) end

--- Fetch an https address. `callback` is called later with the answer. Needs "network".
---@param url string
---@param opts? HttpOptions|fun(res: HttpResponse)
---@param callback? fun(res: HttpResponse)
---@return integer id
function http.get(url, opts, callback) end

--- A table body is sent as JSON.
---@param url string
---@param body string|table
---@param opts? HttpOptions|fun(res: HttpResponse)
---@param callback? fun(res: HttpResponse)
---@return integer id
function http.post(url, body, opts, callback) end

------------------------------------------------------------------------------------------------
-- small helpers
------------------------------------------------------------------------------------------------

---@class util
util = {}

--- The text without spaces at either end.
---@param s string
---@return string
function util.trim(s) end

--- Does the text begin with `prefix`?
---@param s string
---@param prefix string
---@return boolean
function util.starts_with(s, prefix) end

--- Does the text end with `suffix`?
---@param s string
---@param suffix string
---@return boolean
function util.ends_with(s, suffix) end

--- Split on a plain separator (default ",").
---@param s string
---@param sep? string
---@return string[]
function util.split(s, sep) end

--- Is the value in the list?
---@param list any[]
---@param value any
---@return boolean
function util.contains(list, value) end

--- Where the value is in the list (1 is the first), or nil.
---@param list any[]
---@param value any
---@return integer|nil
function util.index_of(list, value) end

--- Remove the first matching value; true if it was there.
---@param list any[]
---@param value any
---@return boolean
function util.remove_value(list, value) end

--- Shuffle a list in place.
---@generic T
---@param list T[]
---@return T[]
function util.shuffle(list) end

--- A random item of a list, or nil if it is empty.
---@generic T
---@param list T[]
---@return T
function util.pick(list) end

--- The keys of a table, sorted.
---@param t table
---@return any[]
function util.keys(t) end

--- "12:30:05"
---@param ms? integer
---@return string
function util.clock(ms) end

--- "5s ago", "3m ago", "2h ago".
---@param ms integer
---@return string
function util.ago(ms) end
