//! # `chanora_protocol` //! //! TeamSpeak-compatible protocol adapter. Isolates `tsclientlib` //! behind a typed boundary so the rest of Chanora is decoupled from //! the upstream library's types (SAD-067, SysDes-011, SysDes-029). //! //! ## What this crate exposes //! //! * [`ConnectConfig`] — typed connection parameters. //! * [`ProtocolClient`] — async handle owning the connection task. //! * [`ServerSnapshot`], [`ChannelInfo`], [`ClientInfo`], //! [`ClientProfile`] — opaque //! DTOs containing only `String`s and primitives. //! * [`ProtocolError`] — typed error catalogue. //! //! ## What this crate does NOT expose //! //! * `tsclientlib::*` types. //! * `tsproto::*` types. //! * Any audio-related types — those live in `chanora_audio`. //! //! Promoted from `poc/tsclientlib-connect-spike` on 2026-05-14 //! as part of the Alpha build. //! //! ## Server address resolution (A.1) //! //! `chanora_resolver` owns TeamSpeak client address resolution: //! server-name aliases, `_ts3._udp` SRV, TSDNS SRV/TCP, and DNS //! fallback. This crate asks it for a final IP `host:port` and feeds //! the resulting `SocketAddr` directly to `tsclientlib::Connection::build` //! so tsclientlib's own resolver is not used. #![forbid(unsafe_code)] #![warn(missing_docs)] mod adapter; mod dto; pub mod poke_limiter; pub use adapter::{ConnectConfig, DisconnectReason, InboundVoice, ProtocolClient, SnapshotProbe}; pub use dto::{ ChannelId, ChannelInfo, ChatMessage, ClientId, ClientInfo, ClientProfile, MessageTarget, PokeStrength, ProtocolDelta, ServerActivity, ServerSnapshot, }; pub use poke_limiter::PokeLimiter; // Re-export the upstream voice types so chanora_audio can build outbound // voice packets without taking a direct dependency on tsclientlib / // tsproto_packets. Per SAD-067 this is the *one* deliberate // re-export: the audio path is performance-sensitive and a parallel // type hierarchy would force copies for every 20 ms frame. pub use tsproto_packets::packets::{ AudioData, CodecType, Direction, InAudioBuf, OutAudio, OutPacket, }; use thiserror::Error; /// Errors surfaced by the protocol adapter. None of these expose /// `tsclientlib`-specific types; raw upstream errors are mapped here /// to typed arms. #[derive(Debug, Error)] pub enum ProtocolError { /// Configuration is invalid before any I/O is attempted (bad /// hostname, missing identity, etc.). #[error("invalid protocol configuration: {0}")] Invalid(String), /// Hostname resolution failed. Distinct from [`Self::Connect`] /// so the UI can show a meaningful "Server not found" message /// instead of a generic connection error. #[error("dns lookup failed for '{host}': {reason}")] DnsFailed { /// The hostname (or `host:port`) the caller submitted. host: String, /// Reason from the platform resolver. reason: String, }, /// Failed to dial / handshake with the server. #[error("connect failed: {0}")] Connect(String), /// The connection ended before becoming ready. #[error("disconnected before ready: {0}")] DisconnectedEarly(String), /// Connection lost after becoming ready. #[error("connection lost: {0}")] Lost(String), /// Identity parsing failed. #[error("identity error: {0}")] Identity(String), /// Operation timed out. #[error("protocol timeout")] Timeout, /// A server command was rejected by the TeamSpeak server with /// a typed error code. The `code` is the raw TS3 error number /// (see https://github.com/ReSpeak/tsdeclarations Errors.csv), /// and `message` is the server-supplied human-readable text. /// Distinguishing this from `Backend` lets the UI surface a /// localised explanation (insufficient permission, wrong /// channel password, etc.) instead of a generic failure. #[error("server rejected (code {code}): {message}")] ServerRejected { /// Raw TS3 error code (e.g. 0x0a08 = `permissions_client_insufficient`). code: u32, /// Server-supplied message text. message: String, }, /// A backend error escaped the mapping. Production callers /// should never see this; if they do, it is a mapping bug here. #[error("protocol backend: {0}")] Backend(String), /// A file transfer failed while downloading protocol-owned assets. #[error("file transfer failed: {0}")] FileTransfer(String), }