# obby-client 0.3.0 ## Where the reference lives - Rust: https://obbyworld.github.io/obby-client/rust/obby_client/ - TypeScript: https://obbyworld.github.io/obby-client/typescript/ - Python: https://obbyworld.github.io/obby-client/python/obby_client.html - Dart: https://obbyworld.github.io/obby-client/dart/ - C: https://obbyworld.github.io/obby-client/c/obby_ffi.h ## Every type that crosses a binding Generated from the Rust. The same shapes reach TypeScript, Python, Dart and C as JSON. ```typescript // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. /** * The set of commands the server says we may currently use. * * The server pushes this on connect and again whenever it changes, such as after an `OPER`. A * batch carries additions and removals together, so both are applied at once. */ export type AllowedCommands = { available: Array, }; /** * What the server knows about a bot in a channel. */ export type Bot = { /** * The bot's nick. */ nick: string, /** * The identifier the server gave it. */ id: string | null, /** * True when an operator configured this bot, rather than it registering itself. * * Only a configured bot may claim a privileged command name. A bot that registered itself has * those names stripped, so it cannot shadow `oper` or `identify`. */ from_config: boolean, /** * The commands it offers. */ commands: Array, }; /** * One command a bot offers. */ export type BotCommand = { /** * What to type, without its leading slash. */ name: string, /** * What it does. */ description: string | null, }; /** * The bots we know about, keyed by their folded nick. */ export type BotRegistry = { known: { [key in string]?: Bot }, }; /** * What the server offers and what we hold. */ export type Capabilities = { available: { [key in string]?: string | null }, acknowledged: { [key in string]?: string | null }, /** * Capabilities we asked for and are still waiting on. Registration cannot finish while this is * non-empty, because `CAP END` before the last reply loses the capability. */ pending: Array, }; /** * One capability the server advertised, with the value it carried if any. */ export type Capability = { /** * The capability name, without any `=value` suffix. */ name: string, /** * The value, for capabilities like `sasl=PLAIN,EXTERNAL` that carry one. */ value: string | null, }; /** * A nick or channel name folded under a server's casemapping. * * Every map keyed by a nick or a channel is keyed by this type, so that a raw `String` can never be * used as an identity by accident. * * The mapping that produced it is not stored. Two `CaseFolded` values are only comparable when they * came from the same connection, which is the only place they are ever used together. */ export type CaseFolded = string; /** * A channel we are in. */ export type Channel = { /** * The name as the server spells it. */ name: string, /** * The topic, when one is set. */ topic: string | null, /** * Who set the topic and when. */ topic_by: string | null, /** * Channel modes currently set, with their arguments. */ modes: { [key in string]?: string | null }, /** * Who is in it. */ members: { [key in CaseFolded]?: Membership }, /** * What was said. */ log: MessageLog, /** * Messages since the last read marker. */ unread: number, /** * Unread messages that mention us. */ mentions: number, /** * Metadata the server holds, such as `display-name`, `color`, `avatar` and `bot`. * * These are plain `draft/metadata-2` keys with no vendor prefix, despite everything else Obby * adds being namespaced. */ metadata: { [key in string]?: string }, /** * The read marker timestamp the server last confirmed. */ read_marker: string | null, /** * Who is composing a message here right now. */ typing: Array, /** * Modes by their name rather than their letter, from `draft/named-modes`. * * A letter means nothing without the server telling you what it does, and two servers spell * the same feature differently. The names are stable, so this is what a user interface should * show and what a setting should be keyed by. */ named_modes: { [key in string]?: string | null }, }; /** * One message, as the model holds it. */ export type ChatMessage = { /** * Where it sits in the log. */ key: MessageKey, /** * The network-unique id, when the server assigned one. */ msgid: string | null, /** * Who sent it, as they spell their own nick. */ sender: string, /** * What kind of event it is. */ kind: MessageKind, /** * The body, empty for events that have none. */ text: string, /** * The account the sender was logged in as, from `account-tag`. */ account: string | null, /** * True when this arrived inside a history batch rather than live. */ historical: boolean, /** * True when this is our own message coming back through `echo-message`. */ own: boolean, /** * The message this one replies to, from the `+reply` tag. * * The server is inconsistent about the spelling and sends `+reply` from one module and * `+draft/reply` from another, so both are accepted on the way in. */ reply_to: string | null, /** * Reactions, keyed by the emoji, holding who reacted. */ reactions: { [key in string]?: Array }, /** * A preview of the first link in this message, when the server built one. */ link_preview: LinkPreview | null, /** * True once the message was redacted. The original is kept, because throwing it away leaves no * way to show who redacted what. */ redacted: boolean, }; /** * The chunk-correlation fields riding alongside a split `sdp` value. */ export type ChunkMeta = { /** * The id every chunk of one split frame shares. */ id: string, /** * This chunk's 0-based position. */ seq: number, /** * How many chunks the split frame was cut into. */ total: number, }; /** * Something to do on this connection. */ export type Command = { "type": "join", /** * The channel to join. */ channel: string, /** * Its key, when it has one. */ key: string | null, } | { "type": "part", /** * The channel to leave. */ channel: string, /** * Why, shown to the others in it. */ reason: string | null, } | { "type": "send_message", /** * Where to say it. */ target: string, /** * What to say. */ text: string, } | { "type": "send_notice", /** * Where to send it. */ target: string, /** * What to send. */ text: string, } | { "type": "send_action", /** * Where to send it. */ target: string, /** * What we are doing. */ text: string, } | { "type": "set_nick", /** * The nick to take. */ nick: string, } | { "type": "set_topic", /** * The channel. */ channel: string, /** * The new topic, or nothing to clear it. */ topic: string | null, } | { "type": "set_away", /** * The away message, or nothing to come back. */ message: string | null, } | { "type": "set_typing", /** * Who we are typing to. */ target: string, /** * How far along we are. */ state: TypingState, } | { "type": "add_reaction", /** * The channel or person the message is in. */ target: string, /** * The message reacted to. */ msgid: string, /** * The emoji. */ emoji: string, } | { "type": "remove_reaction", /** * The channel or person the message is in. */ target: string, /** * The message. */ msgid: string, /** * The emoji to remove. */ emoji: string, } | { "type": "redact_message", /** * Where the message is. */ target: string, /** * The message to delete. */ msgid: string, /** * Why, when the server wants a reason. */ reason: string | null, } | { "type": "mark_read", /** * The channel or person. */ target: string, /** * The last message read, in milliseconds since the Unix epoch. The engine writes the * `server-time` the wire wants. */ at_ms: number, } | { "type": "fetch_history", /** * The channel or person. */ target: string, /** * Fetch messages older than the message with this id. */ before_msgid: string | null, /** * How many to ask for. */ limit: number, } | { "type": "set_metadata", /** * The key, such as `display-name`, `color` or `avatar`. */ key: string, /** * The value, or nothing to clear the key. */ value: string | null, } | { "type": "subscribe_metadata", /** * The keys to watch. */ keys: Array, } | { "type": "whois", /** * Who to ask about. */ nick: string, } | { "type": "rename_channel", /** * The channel as it is called now. */ channel: string, /** * What to call it. */ new_name: string, /** * Why, shown to the others in it. */ reason: string | null, } | { "type": "create_invite_link", /** * The channel it joins, or nothing to invite to the network. */ channel: string | null, /** * What it is for. */ description: string | null, } | { "type": "list_invite_links" } | { "type": "delete_invite_link", /** * Which one, from [`Command::ListInviteLinks`]. */ share_id: string, } | { "type": "redeem_invite_code", /** * The code, which is the share id of the link that carried it. */ code: string, } | { "type": "generate_token", /** * Which service the token is for, such as `FILEHOST`. */ service: string, } | { "type": "watch_nicks", /** * The nicks to watch. */ nicks: Array, } | { "type": "unwatch_nicks", /** * The nicks to stop watching. */ nicks: Array, } | { "type": "send_voice_signal", /** * The channel to signal in. */ channel: string, /** * The frame. The engine encodes it as the JSON that travels in the tag. */ signal: VoiceSignal, } | { "type": "quit", /** * Why. */ reason: string | null, } | { "type": "send_raw_line", /** * The line, without its terminator. */ line: string, }; /** * What a host needs to give the engine before it can connect. * * Every field but the nick has a default, so a host deserialising one only has to supply what it * actually cares about. An empty username or realname is filled in from the nick. */ export type Config = { /** * The nick to register with. */ nick: string, /** * The username sent in `USER`. Defaults to the nick. */ username?: string, /** * The realname sent in `USER`. Defaults to the nick. */ realname?: string, /** * The server password, sent as `PASS` before anything else. */ password?: string, /** * What to authenticate with, when the server offers `sasl`. */ sasl?: SaslCredentials, /** * How many messages each channel and conversation keeps. */ retention?: number, /** * Nicks to fall back through when the server says ours is taken during registration. * * Registration stalls forever if nobody answers a 433, so the engine walks this list and then * starts appending underscores rather than leaving the connection hung. */ alt_nicks?: Array, }; /** * How far the connection has got. */ export type ConnectionPhase = "disconnected" | "negotiating" | "registering" | "registered"; /** * A private conversation with one other person. */ export type Conversation = { /** * Their nick, as they spell it. */ nick: string, /** * What was said. */ log: MessageLog, /** * Messages since the last read marker. */ unread: number, /** * The read marker timestamp the server last confirmed. */ read_marker: string | null, /** * Who is composing a message here right now. */ typing: Array, }; /** * An invitation link to the network or to one channel. */ export type Invitation = { /** * The identifier used to delete it. */ share_id: string, /** * The channel it joins, or nothing when it invites to the network. */ channel: string | null, /** * The link itself. */ url: string, /** * When it was made, as the server spells it. */ created: string | null, /** * How many people have used it. */ redeemed: number, /** * What it is for. */ description: string | null, }; /** * A preview of a link someone posted, built by the server and attached to the message. * * The server fetches the page; a client never does. There is no capability to negotiate, and the * server refuses these tags from any sender but itself, so a peer cannot forge one. */ export type LinkPreview = { /** * The page title. Always present when a preview exists at all. */ title: string, /** * A description of the page, when the page offered one. */ snippet: string | null, /** * An image, already re-hosted by the server when it has a filehost configured. */ image: string | null, }; /** * Who we are on this connection. */ export type LocalUser = { /** * Our current nick. */ nick: string, /** * The account we authenticated as. */ account: string | null, /** * Our user modes. */ modes: string, /** * Metadata the server holds, such as `display-name`, `color`, `avatar` and `bot`. * * These are plain `draft/metadata-2` keys with no vendor prefix, despite everything else Obby * adds being namespaced. */ metadata: { [key in string]?: string }, /** * Our away message, when we are away. */ away: string | null, }; /** * What one member holds in one channel. * * Only the channel-specific part. Who they are, what account they hold and whether they are away * are the same everywhere, so they live once on [`Person`] rather than being copied into every * channel they are in and drifting apart. */ export type Membership = { /** * The prefix characters they hold here, highest rank first. */ prefixes: string, }; /** * Where a message is ordered and how it is found again. * * Ordering is by the server's timestamp, with a monotonic sequence number breaking ties. A tie * broken by arrival order alone is what the reference client relies on, and it only holds there * because of an incidental property of the sort it uses. */ export type MessageKey = { /** * Milliseconds since the epoch, from `server-time` when the server sent one. */ time_ms: number, /** * Assigned in arrival order, unique for the life of the connection. */ seq: number, }; /** * What kind of thing happened. */ export type MessageKind = { "type": "privmsg" } | { "type": "notice" } | { "type": "ctcp", /** * The CTCP command, uppercased, such as `ACTION`. */ command: string, } | { "type": "tagmsg" } | { "type": "join" } | { "type": "part" } | { "type": "quit" } | { "type": "kick", /** * Who was removed. */ target: string, } | { "type": "nick", /** * What they changed it to. */ new_nick: string, } | { "type": "topic" } | { "type": "mode" }; /** * The messages of one channel or conversation, ordered and bounded. */ export type MessageLog = { messages: Array, by_msgid: { [key in string]?: MessageKey }, retention: number, }; /** * Where a message came from, as sent in the `:`-prefixed source. * * A server sends its own name; a client's message arrives as `nick!user@host`, though a server may * send only the nick. */ export type MessageSource = { /** * The nick, or the server name when there is no `!` or `@`. */ name: string, /** * The user part, when the source is a full hostmask. */ user: string | null, /** * The host part, when present. */ host: string | null, }; /** * One message tag. A tag with an empty value is the same as a tag with no value, so both parse to * `value: None`. */ export type MessageTag = { /** * The tag name, including a leading `+` on a client-only tag and any vendor prefix. */ key: string, /** * The unescaped value. */ value: string | null, }; /** * The tag section of a message, in the order it arrived. * * Order is kept rather than folded into a map because a round trip has to reproduce the line, and * because a server may legally send the same key twice. */ export type MessageTags = Array; /** * Everything the connection knows. */ export type Model = { /** * Who we are. */ me: LocalUser, channels: { [key in CaseFolded]?: Channel }, conversations: { [key in CaseFolded]?: Conversation }, people: { [key in CaseFolded]?: Person }, whois: { [key in CaseFolded]?: Whois }, retention: number, next_seq: number, }; /** * What changed, for a host that wants to react without diffing the whole model. */ export type ModelChange = { "type": "message_added", /** * The channel or nick it belongs to, as the server spells it. */ target: string, /** * Where it sits in that target's log. */ key: MessageKey, } | { "type": "channel_joined", /** * The channel. */ channel: string, } | { "type": "channel_parted", /** * The channel. */ channel: string, } | { "type": "channel_members_changed", /** * The channel. */ channel: string, } | { "type": "channel_topic_changed", /** * The channel. */ channel: string, } | { "type": "nick_changed", /** * What they were called. */ from: string, /** * What they are called now. */ to: string, } | { "type": "channel_modes_changed", /** * The channel. */ channel: string, } | { "type": "read_marker_moved", /** * The channel or person. */ target: string, } | { "type": "message_reacted", /** * Where the message is. */ target: string, /** * The message reacted to. */ msgid: string, } | { "type": "message_redacted", /** * Where the message was. */ target: string, /** * The message deleted. */ msgid: string, } | { "type": "metadata_changed", /** * Whose metadata changed. */ target: string, /** * The key that changed. */ key: string, } | { "type": "whois_received", /** * Who it describes. */ nick: string, } | { "type": "channel_renamed", /** * What it was called. */ from: string, /** * What it is called now. */ to: string, }; /** * Something the host needs to know about. * * Events are drained with [`Client::poll_event`] rather than delivered through a callback, because * a callback needs a different lifetime, threading and re-entrancy contract in every language this * engine is bound into. */ export type ObbyEvent = { "type": "capabilities_acknowledged", /** * The capability names, as acknowledged. */ names: Array, } | { "type": "registered", /** * The nick the server actually gave us, which may differ from the one we asked for. */ nick: string, } | { "type": "isupport_token", /** * The token name, such as `CHANMODES`. */ token: string, /** * Its value, absent for a boolean token, present and empty for `TOKEN=`. */ value: string | null, } | { "type": "logged_in", /** * The account the server logged us in as. */ account: string, } | { "type": "sasl_failed", /** * Why it ended. */ reason: SaslFailure, } | { "type": "nick_in_use", /** * The nick that was refused. */ refused: string, /** * What is being tried instead. */ trying: string, } | { "type": "model_changed", /** * What changed. */ change: ModelChange, } | { "type": "link_dead" } | { "type": "reconnect_after", /** * How long the host should wait first. */ after_ms: number, } | { "type": "reconnect_abandoned" } | { "type": "command_timed_out", /** * The command that went unanswered. */ command: string, } | { "type": "allowed_commands_changed" } | { "type": "bots_changed", /** * The bot the server told us about. */ nick: string, } | { "type": "auth_token", /** * The service it is for, as the server spells it. */ service: string, /** * Where to present it. */ endpoint: string, /** * The token itself. */ token: string, } | { "type": "voice", /** * The channel the room belongs to. */ channel: string, /** * The frame. */ signal: VoiceSignal, } | { "type": "typing_changed", /** * The channel or conversation they are composing in. */ target: string, /** * Who. */ nick: string, /** * True when they are composing now. */ active: boolean, } | { "type": "presence_changed", /** * Who. */ nick: string, /** * True when they are here now. */ online: boolean, } | { "type": "server_reply", /** * How serious it is. */ severity: Severity, /** * The command it concerns, or `*` when the server did not say. */ command: string, /** * The machine-readable code, such as `ACCOUNT_REQUIRED_TO_CONNECT`. */ code: string, /** * Any further parameters the server gave, before the description. */ context: Array, /** * The human-readable description. */ text: string, } | { "type": "raw_line", /** * The parsed line. */ message: RawMessage, }; /** * The two states an intent frame like `mic` or `hand` toggles between. */ export type OnOff = "on" | "off"; /** * Someone we know about, held once however many channels we share. */ export type Person = { /** * Their nick, as they spell it. */ nick: string, /** * The account they are logged in as. */ account: string | null, /** * Their away message, when they are away. */ away: string | null, /** * True when the server marks them as a bot. */ bot: boolean, /** * Their username, from WHO or a hostmask. */ username: string | null, /** * Their host, from WHO or a hostmask. */ host: string | null, /** * Their realname, from WHO. */ realname: string | null, /** * True when the server marks them as an operator. */ operator: boolean, /** * Metadata the server holds, such as `display-name`, `color` and `avatar`. * * These are plain `draft/metadata-2` keys with no vendor prefix, despite everything else Obby * adds being namespaced. */ metadata: { [key in string]?: string }, }; /** * A parsed protocol line. * * The types are owned rather than borrowed from the input. A borrowed `Message<'a>` would parse * faster, but every consumer of this crate reaches it across a language boundary that cannot carry * a Rust lifetime, so the copy has to happen somewhere and here is the only place it happens once. */ export type RawMessage = { /** * The tag section, empty when the line carried no `@`. */ tags: MessageTags, /** * The source, when the line carried one. */ source: MessageSource | null, /** * The command or three-digit numeric, as it arrived. Commands are case-insensitive on the wire, * so compare with [`Message::is`] rather than `==`. */ command: string, /** * The parameters, with the trailing parameter last and its `:` removed. */ params: Array, }; /** * What to authenticate with. */ export type SaslCredentials = { "mechanism": "plain", /** * The account to log in as. */ username: string, /** * Its password. */ password: string, } | { "mechanism": "external" } | { "mechanism": "scram", /** * The account to log in as. */ username: string, /** * Its password. */ password: string, /** * Unpredictable bytes, never reused. The core has no entropy source, so the host supplies * this, and reusing one destroys the replay protection the mechanism exists for. */ nonce: string, }; /** * Why authentication ended without succeeding. */ export type SaslFailure = "rejected" | "too_long" | "aborted" | "already_authenticated" | "no_shared_mechanism" | "server_not_verified"; /** * How serious a `standard-replies` message is. */ export type Severity = "fail" | "warn" | "note"; /** * TURN/STUN credentials the SFU hands us on `joined`. * * These are short-lived, and nothing in the `joined` handshake or anywhere else in the * signalling plane ever refreshes them mid-call. A call that outlives them loses its relay path * with no warning; * whoever integrates this signalling plane needs to leave and rejoin (or otherwise trigger a * fresh `joined`) before that happens, since nothing here does it automatically. */ export type TurnCredentials = { /** * The TURN/STUN server URLs to try, in order. */ urls: Array, /** * The short-lived TURN username. */ username: string, /** * The short-lived TURN password. */ password: string, }; /** * Whether we are still composing a message. */ export type TypingState = "active" | "paused" | "done"; /** * One participant's state within a [`Room`]. * * Every toggle is [`OnOff`] rather than `bool`: this is the same distinction clippy's own * `struct_excessive_bools` lint asks for (six independent flags read equally well as a state * machine's cases), and reusing `OnOff` rather than inventing six near-identical two-variant * enums keeps it to one type. */ export type VoiceParticipant = { /** * Their nick as the server spells it, since the map that holds them is keyed by a fold that * throws that spelling away. */ nick: string, /** * Whether they may publish, decided by the room kind and, in a `$` room, whether the * server named them a streamer. */ role: VoiceRole, /** * Microphone on. */ mic: OnOff, /** * Camera on. */ video: OnOff, /** * The voice-activity flag the last `presence` reported. */ speaking: OnOff, /** * Not receiving room audio. */ deaf: OnOff, /** * Screen share active. */ screen: OnOff, /** * Hand raised. */ hand: OnOff, }; /** * The `state` a `presence` notification carries. * * One wire `type: "presence"` actually carries three incompatible shapes: a membership change (`Joined`/`Left`), a toggle * (`On`/`Off`, read together with [`Signal::Presence`]'s `kind`), or an activity flag * (`Speaking`/`Silent`/`DeafOn`/`DeafOff`, which carries no `kind` at all). */ export type VoicePresence = "joined" | "left" | "on" | "off" | "speaking" | "silent" | "deaf_on" | "deaf_off"; /** * Whether a room participant may publish audio and video, or only receive it. */ export type VoiceRole = "publisher" | "viewer"; /** * The state of one voice room: who is in it, their kind of channel, and every participant's * mic, video, speaking, deaf, screen and hand state and role. */ export type VoiceRoom = { /** * The channel this room's signalling is scoped to. */ channel: string, /** * Whether every member publishes, or only the streamer and whoever they promote. */ kind: VoiceRoomKind, /** * Every known participant, keyed by their folded nick. */ participants: { [key in CaseFolded]?: VoiceParticipant }, /** * The TURN credentials the SFU handed us on `joined`, if it has yet. */ turn: TurnCredentials | null, }; /** * Who may publish in a voice room, decided by the channel's sigil. */ export type VoiceRoomKind = "publish" | "stream"; /** * One `+obsidianirc/rtc` signalling frame. * * Every variant is one JSON object's `type`. The published * documents only 9 of these, the * wire carries 19, and this enum follows the wire. Outbound intent and inbound notification are modelled as distinct shapes where the * wire actually distinguishes them (`mic`/`video`/`screen`/`hand`/`speaking`/`silent`/`deaf` * versus the `presence` they get rebroadcast as; `promote`/`demote` versus `role`), rather than * collapsed into one type as the published table's prose implies. */ export type VoiceSignal = { "type": "join", /** * The channel to join. */ channel: string, } | { "type": "leave", /** * The channel to leave. */ channel: string, } | { "type": "joined", /** * Every current member's nick. */ members: Array, /** * The room's kind, present only for a `$` channel. */ mode: VoiceRoomKind | null, /** * The role granted to us specifically, present only for a `$` channel. */ role: VoiceRole | null, /** * Who currently publishes, for a `$` channel. */ streamers: Array | null, /** * The TURN credentials for this call. */ turn: TurnCredentials | null, /** * Hints mapping media lines to the members they belong to. */ tracks: Array | null, } | { "type": "offer", /** * The offer SDP, or one slice of it when `chunk` is set. */ sdp: string, /** * Track-attribution hints, carried only on chunk 0 or an unchunked offer. */ tracks: Array | null, /** * Set when this frame is one of several chunks sharing an id. */ chunk: ChunkMeta | null, } | { "type": "answer", /** * The answer SDP, or one slice of it when `chunk` is set. */ sdp: string, /** * Set when this frame is one of several chunks sharing an id. */ chunk: ChunkMeta | null, } | { "type": "ice", /** * The candidate string. */ cand: string, /** * The media line it applies to. */ mid: string | null, /** * The media line's index, when `mid` is absent. */ mlineidx: number | null, } | { "type": "presence", /** * Who this is about. */ member: string, /** * What changed. */ state: VoicePresence, /** * Which toggle, when `state` is [`PresenceState::On`] or [`PresenceState::Off`]. */ kind: VoiceToggle | null, /** * The role granted, when `state` is [`PresenceState::Joined`] in a `$` room. */ role: VoiceRole | null, } | { "type": "mic", /** * The new state. */ state: OnOff, } | { "type": "video", /** * The new state. */ state: OnOff, } | { "type": "screen", /** * The new state. */ state: OnOff, } | { "type": "hand", /** * The new state. */ state: OnOff, } | { "type": "deaf", /** * The new state. */ state: OnOff, } | { "type": "speaking" } | { "type": "silent" } | { "type": "react", /** * Who reacted, present only on the inbound broadcast. */ member: string | null, /** * The emoji. */ emoji: string, } | { "type": "promote", /** * The member to promote. */ target: string, } | { "type": "demote", /** * The member to demote. */ target: string, } | { "type": "role", /** * Whose role changed. */ member: string, /** * Their new role. */ role: VoiceRole, } | { "type": "error", /** * A human-readable reason, when the server gave one. */ error: string | null, }; /** * Which per-participant toggle a `presence` notification reports, for its toggle sub-shape. */ export type VoiceToggle = "mic" | "video" | "screen" | "hand"; /** * A hint from the SFU mapping one negotiated media line to the member it belongs to. * * The SFU sends mid-to-member hints so an inbound track can be attributed to the right member * when the SDP's own `msid` is missing or unreliable. This is the minimal shape that serves that * purpose; a real server may send more fields, which this simply ignores on decode. */ export type VoiceTrackHint = { /** * The SDP media line identifier this hint names. */ mid: string, /** * The member that media line belongs to. */ member: string, }; /** * Who we are watching, and whether each is online. */ export type WatchList = { watching: Array, online: Array, }; /** * What a `WHOIS` said about someone. * * A reply is nine numerics that arrive one at a time, so they are collected here and reported once, * when the closing `318` lands. A host that reacted to each numeric would redraw a profile card * nine times and show eight incomplete ones. */ export type Whois = { /** * Their nick, as the server spells it. */ nick: string, /** * Their username, from `311`. */ username: string | null, /** * Their host, from `311`. */ host: string | null, /** * Their realname, from `311`. */ realname: string | null, /** * The server they are on, from `312`. */ server: string | null, /** * What that server calls itself, from `312`. */ server_info: string | null, /** * How the server describes their operator privileges, from `313`, when they have any. */ operator: string | null, /** * How long they have been idle, from `317`. */ idle_secs: number | null, /** * When they connected, in milliseconds since the Unix epoch, from `317`. */ signon_ms: number | null, /** * The channels they are in, keeping the prefix each one carries, from `319`. */ channels: Array, /** * The account they are logged in as, from `330`. */ account: string | null, /** * Where they are connecting from, as the server words it, from `338` or `378`. */ actual_host: string | null, /** * True when the server said the connection is over TLS, from `671`. */ secure: boolean, /** * True once the closing `318` arrived and there is nothing more to come. */ complete: boolean, }; ``` ## The C ABI ```c /* The C ABI for obby-client. Generated by cbindgen: do not edit by hand. * * Thread safety: a handle is not synchronised. Never use one handle from two threads at once, not * even for two calls that only read. Give each thread its own handle, or take your own lock. */ #ifndef OBBY_FFI_H #define OBBY_FFI_H /* Generated with cbindgen:0.29.4 */ /* Regenerate with: cbindgen --config bindings/obby-ffi/cbindgen.toml --crate obby-ffi --output bindings/obby-ffi/include/obby_ffi.h */ #include #include #include #include #include /** * What an [`ObbyEvent`] is. * * `Unknown` covers an event this ABI has no case for yet, which a caller reads with * [`obby_event_json`] rather than being blind to it. */ typedef enum obby_event_kind { /** * An event this ABI does not name. Read it with [`obby_event_json`]. */ OBBY_EVENT_KIND_UNKNOWN = 0, /** * The server acknowledged the capabilities we asked for. */ OBBY_EVENT_KIND_CAPABILITIES_ACKNOWLEDGED, /** * Registration finished and the connection is usable. */ OBBY_EVENT_KIND_REGISTERED, /** * One `005` token, with its value when it has one. */ OBBY_EVENT_KIND_ISUPPORT_TOKEN, /** * SASL authentication succeeded. */ OBBY_EVENT_KIND_LOGGED_IN, /** * SASL authentication failed. */ OBBY_EVENT_KIND_SASL_FAILED, /** * The nick we asked for is taken. */ OBBY_EVENT_KIND_NICK_IN_USE, /** * The model changed. The change itself is in [`obby_event_json`]. */ OBBY_EVENT_KIND_MODEL_CHANGED, /** * The link is dead and the host should redial. */ OBBY_EVENT_KIND_LINK_DEAD, /** * Redial after this many milliseconds. */ OBBY_EVENT_KIND_RECONNECT_AFTER, /** * Reconnection gave up. */ OBBY_EVENT_KIND_RECONNECT_ABANDONED, /** * A command we labelled went unanswered. */ OBBY_EVENT_KIND_COMMAND_TIMED_OUT, /** * The commands the server lets us use changed. */ OBBY_EVENT_KIND_ALLOWED_COMMANDS_CHANGED, /** * A voice signalling frame. The frame is in [`obby_event_json`]. */ OBBY_EVENT_KIND_VOICE, /** * Someone started or stopped composing a message. */ OBBY_EVENT_KIND_TYPING_CHANGED, /** * Someone we monitor came online or went offline. */ OBBY_EVENT_KIND_PRESENCE_CHANGED, /** * A `standard-replies` FAIL, WARN or NOTE. */ OBBY_EVENT_KIND_SERVER_REPLY, /** * A line the engine does not model. The message is in [`obby_event_json`]. */ OBBY_EVENT_KIND_RAW_LINE, /** * What we know about a bot changed. */ OBBY_EVENT_KIND_BOTS_CHANGED, /** * The server minted a bearer token for one of its services. */ OBBY_EVENT_KIND_AUTH_TOKEN, } obby_event_kind; /** * One value on an [`ObbyEvent`]. * * An event only has the fields its kind defines, and [`obby_event_text`] answers null for the * rest. Numbers, including the booleans, are read with [`obby_event_number`]. */ typedef enum obby_event_field { /** * The nick an event is about. */ OBBY_EVENT_FIELD_NICK = 0, /** * The account we authenticated as. */ OBBY_EVENT_FIELD_ACCOUNT, /** * A `005` token name. */ OBBY_EVENT_FIELD_TOKEN, /** * A `005` token's value. */ OBBY_EVENT_FIELD_VALUE, /** * The nick the server refused. */ OBBY_EVENT_FIELD_REFUSED, /** * The nick being tried instead. */ OBBY_EVENT_FIELD_TRYING, /** * Why something failed. */ OBBY_EVENT_FIELD_REASON, /** * The command an event is about. */ OBBY_EVENT_FIELD_COMMAND, /** * The channel or nick an event is about. */ OBBY_EVENT_FIELD_TARGET, /** * The channel an event is about. */ OBBY_EVENT_FIELD_CHANNEL, /** * A numeric or named reply code. */ OBBY_EVENT_FIELD_CODE, /** * Human-readable text from the server. */ OBBY_EVENT_FIELD_TEXT, /** * `fail`, `warn` or `note`. */ OBBY_EVENT_FIELD_SEVERITY, /** * The capability names the server acknowledged, separated by spaces. */ OBBY_EVENT_FIELD_NAMES, /** * How long to wait before redialling, in milliseconds. Read with [`obby_event_number`]. */ OBBY_EVENT_FIELD_AFTER_MS, /** * 1 when someone is composing, 0 when they stopped. Read with [`obby_event_number`]. */ OBBY_EVENT_FIELD_ACTIVE, /** * 1 when someone is online, 0 when they are not. Read with [`obby_event_number`]. */ OBBY_EVENT_FIELD_ONLINE, /** * The network service a token is for. */ OBBY_EVENT_FIELD_SERVICE, /** * Where to present a token. */ OBBY_EVENT_FIELD_ENDPOINT, /** * A bearer token. `Token` is already the name of a `005` token, which this is not. */ OBBY_EVENT_FIELD_BEARER_TOKEN, } obby_event_field; /** * One connection's engine state. * * Created by [`obby_client_new`] and destroyed by [`obby_client_free`]. The caller only ever holds * a pointer to one; every operation on it goes through a function in this crate. */ typedef struct obby_client_t obby_client_t; /** * One event, owned by the caller until [`obby_event_free`]. * * The strings [`obby_event_text`] and [`obby_event_json`] return point into this event and die * with it, so copy anything that must outlive the call to free. */ typedef struct obby_event_t obby_event_t; /** * An owned byte buffer handed out by [`obby_client_poll_transmit`]. * * Freed with [`obby_client_free_bytes`], the only legal way to release it. An empty buffer is * always the zeroed value: a null `ptr` and a zero `len`, which is safe to free as a no-op. */ typedef struct obby_bytes_t { /** * The first byte, or null when `len` is zero. */ uint8_t *ptr; /** * How many bytes `ptr` points to. */ size_t len; } obby_bytes_t; /** * A client's settings, in C types. * * Only `nick` is required. Every pointer may be null, and a null means "use the default": the * nick for `username` and `realname`, no password, no SASL. A zero `retention` keeps the engine's * own message limit. * * [`obby_client_new_from_json`] takes the fields this omits, such as SASL credentials and the * alternate nicks to try when one is taken. */ typedef struct obby_config_t { /** * The nick to register with. Required. */ const char *nick; /** * The username sent in `USER`, or null for the nick. */ const char *username; /** * The realname sent in `USER`, or null for the nick. */ const char *realname; /** * The server password sent as `PASS`, or null for none. */ const char *password; /** * How many messages each channel and conversation keeps, or 0 for the default. */ size_t retention; } obby_config_t; /** * Create a client from a JSON-encoded [`Config`], for a caller that wants a field * [`ObbyConfig`] does not have, such as SASL credentials or alternate nicks. * * Returns null when `config_json` is null, is not valid UTF-8, or does not parse as a `Config`. * * # Safety * `config_json` must be null or point to a NUL-terminated, valid UTF-8 C string, valid for reads * for the duration of this call. */ struct obby_client_t *obby_client_new_from_json(const char *config_json); /** * Destroy a client created by [`obby_client_new`]. * * # Safety * `client` must be null or a pointer returned by [`obby_client_new`] that has not already been * freed. It must not be used again after this call./// * * The handle must not be in use on another thread while this call runs. */ void obby_client_free(struct obby_client_t *client); /** * Tell the engine the transport is up. See [`obby_client::Client::handle_connected`]. * * # Safety * `client` must be null or a valid, non-freed pointer from [`obby_client_new`]. * * The handle must not be in use on another thread while this call runs. */ void obby_client_handle_connected(struct obby_client_t *client); /** * Tell the engine its transport died. See [`obby_client::Client::handle_disconnected`]. * * # Safety * `client` must be null or a valid, non-freed pointer from [`obby_client_new`]. * * The handle must not be in use on another thread while this call runs. */ void obby_client_handle_disconnected(struct obby_client_t *client); /** * Feed bytes read from the transport. * * A null `data` is only valid when `len` is zero; any other null-with-nonzero-length call is * treated as if nothing were fed, rather than read out of bounds. * * # Safety * `client` must be null or valid. When `len` is greater than zero, `data` must point to at least * `len` readable bytes, valid for the duration of this call. * * The handle must not be in use on another thread while this call runs. */ void obby_client_handle_bytes(struct obby_client_t *client, const uint8_t *data, size_t len); /** * Drain one pending outbound buffer, or the empty [`ObbyBytes`] when there is none. * * # Safety * `client` must be null or a valid, non-freed pointer from [`obby_client_new`]. * * The handle must not be in use on another thread while this call runs. */ struct obby_bytes_t obby_client_poll_transmit(struct obby_client_t *client); /** * Free a buffer returned by [`obby_client_poll_transmit`]. * * The only legal way to release one. Freeing the zeroed value (null `ptr`, zero `len`) is a no-op. * * # Safety * `bytes` must be a value returned by [`obby_client_poll_transmit`] that has not already been * freed, or the zeroed value./// */ void obby_client_free_bytes(struct obby_bytes_t bytes); /** * Drain every pending event as one JSON array, `"[]"` when there are none. * * Returns null only when `client` is null; a working client always produces valid JSON. * * # Safety * `client` must be null or a valid, non-freed pointer from [`obby_client_new`]. * * The handle must not be in use on another thread while this call runs. */ char *obby_client_poll_events_json(struct obby_client_t *client); /** * Free a string returned by [`obby_client_poll_events_json`] or [`obby_client_model_json`]. * * The only legal way to release one. Never pass the pointer returned by [`obby_client_version`] * here: that one is static and owned by the library, not by the caller. * * # Safety * `s` must be null, or a pointer returned by [`obby_client_poll_events_json`] or * [`obby_client_model_json`] that has not already been freed./// */ void obby_client_free_string(char *s); /** * Advance the clock. See [`obby_client::Client::tick`]. * * # Safety * `client` must be null or a valid, non-freed pointer from [`obby_client_new`]. * * The handle must not be in use on another thread while this call runs. */ void obby_client_tick(struct obby_client_t *client, uint64_t monotonic_ms, uint64_t unix_ms); /** * When the engine next has something to do, if ever. * * Returns `true` and writes the deadline to `*out_ms` when one is pending; returns `false` * otherwise, writing zero to `*out_ms` when it is non-null so the caller never reads uninitialised * memory either way. * * # Safety * `client` must be null or valid. `out_ms` must be null or point to a writable `u64` valid for the * duration of this call./// * * The handle must not be in use on another thread while this call runs. */ bool obby_client_poll_timeout(struct obby_client_t *client, uint64_t *out_ms); /** * Submit any command, as JSON. * * Every command has this form; the ones a client sends constantly also have a function of their * own, such as [`obby_client_join`]. * * Returns `true` when it parsed and was submitted, `false` when `client` or `command_json` is * null, `command_json` is not valid UTF-8, or it does not parse as a `Command`. * * # Safety * `client` must be null or valid. `command_json` must be null or point to a NUL-terminated, valid * UTF-8 C string, valid for reads for the duration of this call. * * The handle must not be in use on another thread while this call runs. */ bool obby_client_command_from_json(struct obby_client_t *client, const char *command_json); /** * Read the whole model as JSON, for a host that wants the full state rather than the changes. * * Returns null only when `client` is null; a working client always produces valid JSON. * * # Safety * `client` must be null or a valid, non-freed pointer from [`obby_client_new`]. * * The handle must not be in use on another thread while this call runs. */ char *obby_client_model_json(struct obby_client_t *client); /** * The crate version, as a static, NUL-terminated string. * * This pointer is owned by the library and lives for the process's lifetime: never pass it to * [`obby_client_free_string`]. */ const char *obby_client_version(void); /** * Create a client. * * Returns null when `config` is null, or when its `nick` is null or not valid UTF-8. * * # Safety * `config` must be null or point to a readable [`ObbyConfig`] whose string fields are each null or * a NUL-terminated, valid UTF-8 C string, all valid for reads for the duration of this call. */ struct obby_client_t *obby_client_new(const struct obby_config_t *config); /** * Join a channel, with `key` for a channel that needs one, or null. * * # Safety * `client` must be null or valid. Each string must be null or a NUL-terminated, valid UTF-8 C * string, valid for reads for the duration of this call. * * The handle must not be in use on another thread while this call runs. */ bool obby_client_join(struct obby_client_t *client, const char *channel, const char *key); /** * Leave a channel, with `reason` shown to the others in it, or null. * * # Safety * As [`obby_client_join`]. */ bool obby_client_part(struct obby_client_t *client, const char *channel, const char *reason); /** * Say something to a channel or a person. * * # Safety * As [`obby_client_join`]. */ bool obby_client_send_message(struct obby_client_t *client, const char *target, const char *text); /** * Send a notice, which by convention must never be auto-replied to. * * # Safety * As [`obby_client_join`]. */ bool obby_client_send_notice(struct obby_client_t *client, const char *target, const char *text); /** * Send a `CTCP ACTION`, the third-person form. * * # Safety * As [`obby_client_join`]. */ bool obby_client_send_action(struct obby_client_t *client, const char *target, const char *text); /** * Change our nick. * * # Safety * As [`obby_client_join`]. */ bool obby_client_set_nick(struct obby_client_t *client, const char *nick); /** * Set or clear a channel's topic. A null `topic` asks for the current one. * * # Safety * As [`obby_client_join`]. */ bool obby_client_set_topic(struct obby_client_t *client, const char *channel, const char *topic); /** * Go away with a message, or come back by passing null. * * # Safety * As [`obby_client_join`]. */ bool obby_client_set_away(struct obby_client_t *client, const char *message); /** * Quit, with a reason or null. * * # Safety * As [`obby_client_join`]. */ bool obby_client_quit(struct obby_client_t *client, const char *reason); /** * Say we are typing, so others can show it. `state` must be `"active"`, `"paused"` or `"done"`. * * # Safety * As [`obby_client_join`]. */ bool obby_client_set_typing(struct obby_client_t *client, const char *target, const char *state); /** * React to a message with an emoji. * * # Safety * As [`obby_client_join`]. */ bool obby_client_add_reaction(struct obby_client_t *client, const char *target, const char *msgid, const char *emoji); /** * Take a reaction back. * * # Safety * As [`obby_client_join`]. */ bool obby_client_remove_reaction(struct obby_client_t *client, const char *target, const char *msgid, const char *emoji); /** * Ask the server to delete a message, with `reason` when it wants one, or null. * * # Safety * As [`obby_client_join`]. */ bool obby_client_redact_message(struct obby_client_t *client, const char *target, const char *msgid, const char *reason); /** * Tell the server how far we have read, as milliseconds since the Unix epoch. * * # Safety * As [`obby_client_join`]. */ bool obby_client_mark_read(struct obby_client_t *client, const char *target, uint64_t at_ms); /** * Ask for older messages than the ones we hold. A null `before_msgid` asks for the most recent. * * # Safety * As [`obby_client_join`]. */ bool obby_client_fetch_history(struct obby_client_t *client, const char *target, const char *before_msgid, uint16_t limit); /** * Set one of our own metadata keys, or clear it with a null `value`. * * # Safety * As [`obby_client_join`]. */ bool obby_client_set_metadata(struct obby_client_t *client, const char *key, const char *value); /** * Ask to be told when these metadata keys change on anyone we can see. * * # Safety * As [`obby_client_join`]. `keys` must be null or point to `count` valid, NUL-terminated C * strings, valid for reads for the duration of this call; an element that is null or not valid * UTF-8 is skipped rather than failing the whole call. */ bool obby_client_subscribe_metadata(struct obby_client_t *client, const char *const *keys, size_t count); /** * Ask the server everything it will say about someone. * * # Safety * As [`obby_client_join`]. */ bool obby_client_whois(struct obby_client_t *client, const char *nick); /** * Rename a channel, keeping everyone in it and everything said in it. `reason` may be null. * * # Safety * As [`obby_client_join`]. */ bool obby_client_rename_channel(struct obby_client_t *client, const char *channel, const char *new_name, const char *reason); /** * Make an invitation link. A null `channel` invites to the network; `description` may be null too. * * # Safety * As [`obby_client_join`]. */ bool obby_client_create_invite_link(struct obby_client_t *client, const char *channel, const char *description); /** * Ask for the invitation links we have made. * * # Safety * As [`obby_client_join`]. */ bool obby_client_list_invite_links(struct obby_client_t *client); /** * Withdraw an invitation link. * * # Safety * As [`obby_client_join`]. */ bool obby_client_delete_invite_link(struct obby_client_t *client, const char *share_id); /** * Redeem an invitation code. Only before registering, which is the point of it. * * # Safety * As [`obby_client_join`]. */ bool obby_client_redeem_invite_code(struct obby_client_t *client, const char *code); /** * Mint a bearer token for one of the network's services, such as its file host. * * # Safety * As [`obby_client_join`]. */ bool obby_client_generate_token(struct obby_client_t *client, const char *service); /** * Watch these nicks, so the server says when they come and go. * * # Safety * As [`obby_client_subscribe_metadata`], with `nicks` in place of `keys`. */ bool obby_client_watch_nicks(struct obby_client_t *client, const char *const *nicks, size_t count); /** * Stop watching these nicks. * * # Safety * As [`obby_client_subscribe_metadata`], with `nicks` in place of `keys`. */ bool obby_client_unwatch_nicks(struct obby_client_t *client, const char *const *nicks, size_t count); /** * Send a voice signalling frame to a room. `signal_json` is one frame in the shape * [`obby_client::Signal`] serialises to, and a frame this ABI cannot read is refused. * * Every other binding takes the frame as a typed value; C has no type to take, so it takes the * JSON. * * # Safety * As [`obby_client_join`]. */ bool obby_client_send_voice_signal(struct obby_client_t *client, const char *channel, const char *signal_json); /** * Send one raw protocol line, without the trailing CRLF, for anything this ABI does not name. * * # Safety * As [`obby_client_join`]. */ bool obby_client_send_raw_line(struct obby_client_t *client, const char *line); /** * Take the next event, or null when there are none. * * The caller owns what comes back and releases it with [`obby_event_free`]. * * # Safety * `client` must be null or a valid, non-freed pointer from [`obby_client_new`]. * * The handle must not be in use on another thread while this call runs. */ struct obby_event_t *obby_client_poll_event(struct obby_client_t *client); /** * What kind of event this is. * * A null event reads as [`ObbyEventKind::Unknown`], so a caller that skipped the null check gets a * kind it already has to handle rather than a crash. * * # Safety * `event` must be null or a valid, non-freed pointer from [`obby_client_poll_event`]. */ enum obby_event_kind obby_event_get_kind(const struct obby_event_t *event); /** * One of the event's string fields, or null when this event has no such field. * * The pointer borrows from the event and is invalid once [`obby_event_free`] runs. * * # Safety * `event` must be null or a valid, non-freed pointer from [`obby_client_poll_event`]. */ const char *obby_event_text(const struct obby_event_t *event, enum obby_event_field field); /** * One of the event's numeric fields, written to `out`. * * Returns false, and leaves `out` alone, when this event has no such field. * * # Safety * `event` must be null or a valid, non-freed pointer from [`obby_client_poll_event`]. `out` must * be null or point to a writable `uint64_t`. */ bool obby_event_number(const struct obby_event_t *event, enum obby_event_field field, uint64_t *out); /** * The whole event as JSON, for the parts this ABI does not flatten into fields. * * The pointer borrows from the event and is invalid once [`obby_event_free`] runs. * * # Safety * `event` must be null or a valid, non-freed pointer from [`obby_client_poll_event`]. */ const char *obby_event_json(const struct obby_event_t *event); /** * Release an event. * * # Safety * `event` must be null, or a pointer from [`obby_client_poll_event`] that has not already been * passed here. Every string read from it is invalid afterwards. */ void obby_event_free(struct obby_event_t *event); #endif /* OBBY_FFI_H */ ```