Files
chanora/crates/chanora_protocol/src/lib.rs
T

117 lines
4.1 KiB
Rust

//! # `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 use adapter::{ConnectConfig, DisconnectReason, InboundVoice, ProtocolClient, SnapshotProbe};
pub use dto::{
ChannelId, ChannelInfo, ChatMessage, ClientId, ClientInfo, ClientProfile, MessageTarget,
ServerActivity, ServerSnapshot,
};
// 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),
}