Files
chanora/crates/chanora_protocol/src/dto.rs
T
Edison Jwa 29afbb5e97 fix: address post-event-driven issues and client info parity (#16)
* fix(ui): restore speaking status indicators

Speaking state (isSpeaking) is computed from voice activity timestamps
in the protocol layer and cannot be represented as a discrete delta.
The event-driven refactor removed periodic snapshot refreshes, causing
speaking indicators to go stale.

Adds a 750ms periodic snapshot refresh (matching SPEAKING_ACTIVITY_WINDOW)
while the audio stats timer is active (in-channel only). Structural
changes (moves, joins, leaves) are still handled by instant deltas.

* fix(ui): hide server query clients from delta joins

When a ServerQuery client sends a message, a ClientJoined delta fires.
Before PR#15 the periodic snapshot rebuild would include the SQ client
but the snapshot_view filter hid it. With deltas, the client persisted
in the local snapshot. Now ClientJoined deltas skip SQ clients entirely.

* fix(proto): log getconnectioninfo errors instead of silently discarding

Ping and packet loss showing 'Unknown' in the client info sheet is
caused by getconnectioninfo failures being silently swallowed. Now
logs the error with the client_id so the root cause can be diagnosed
(e.g. missing b_client_connectioninfo_view permission on the server).

Also logs clientgetvariables failures.

* fix(proto): refresh non-self client profiles before mapping

* feat(protocol): add ping deviation to client profiles

* chore(ui): regenerate Flutter bridge bindings for ping deviation

* fix(l10n): add ping deviation labels to client info

* feat(ui): show ping deviation in client info sheet
2026-06-03 18:26:29 +09:00

220 lines
7.5 KiB
Rust

//! Public DTOs returned across the protocol boundary. All fields are
//! owned primitives or `String`s; no `tsclientlib` types leak.
use serde::{Deserialize, Serialize};
/// Opaque server-side channel identifier. Internal representation is
/// the upstream u64 but callers must treat it as opaque.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ChannelId(pub u64);
/// Opaque server-side client identifier.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ClientId(pub u64);
/// One channel in the server's tree.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChannelInfo {
/// Stable channel id.
pub id: ChannelId,
/// Parent channel id; `ChannelId(0)` indicates a top-level channel.
pub parent: ChannelId,
/// Display name, preserved verbatim per ADR-008.
pub name: String,
/// Server-side ordering hint.
pub order: i64,
/// True when the server marks the channel as password-protected.
pub has_password: bool,
/// Talk power threshold required to speak in this channel.
/// `None` means no talk-power restriction.
pub needed_talk_power: Option<i32>,
}
/// The target scope of a text message (mirrors TS3 `TextMessageTargetMode`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MessageTarget {
/// Broadcast to entire server.
Server,
/// Broadcast to current channel.
Channel,
/// Private message to a specific client.
Client(u64),
/// Poke a specific client.
Poke(u64),
}
/// An in-channel text message from a specific client.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChatMessage {
/// The client id of the sender.
pub sender_id: ClientId,
/// Nickname of the sender, preserved verbatim.
pub sender_name: String,
/// Message content, preserved verbatim.
pub message: String,
/// Target scope of this message.
pub target: MessageTarget,
}
/// A server-activity notification derived from TeamSpeak bookkeeping events.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ServerActivity {
/// Human-readable activity line.
pub message: String,
}
/// One connected client on the server.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClientInfo {
/// Stable client id.
pub id: ClientId,
/// Channel the client is currently in.
pub channel: ChannelId,
/// Nickname, preserved verbatim per ADR-008.
pub name: String,
/// True when this client has muted microphone/input capture.
pub input_muted: bool,
/// True when this client has muted speaker/output audio.
pub output_muted: bool,
/// True when recent inbound voice activity was observed for this client.
pub is_speaking: bool,
/// True for TeamSpeak ServerQuery clients.
pub is_server_query: bool,
/// Current talk power value assigned by the server.
pub talk_power: i32,
/// True when the server has granted talk power regardless of numeric value.
pub talk_power_granted: bool,
}
/// Best-effort profile and live connection details for one online
/// client, fetched through the normal TeamSpeak client protocol.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ClientProfile {
/// Stable online client id.
pub id: ClientId,
/// Channel the client is currently in.
pub channel: ChannelId,
/// Nickname, preserved verbatim per ADR-008.
pub name: String,
/// TeamSpeak unique id encoded in the canonical base64 form.
pub unique_id: String,
/// Stable TeamSpeak database id, when visible.
pub database_id: Option<u64>,
/// ISO country code, when the server exposes one.
pub country_code: String,
/// User description, when visible.
pub description: String,
/// Client version string, populated by `clientgetvariables`.
pub version: String,
/// Client platform string, populated by `clientgetvariables`.
pub platform: String,
/// Account creation time as Unix seconds.
pub created_unix_seconds: Option<i64>,
/// Last connection time as Unix seconds.
pub last_connected_unix_seconds: Option<i64>,
/// Total historical connections, when visible.
pub connections_total: Option<u64>,
/// Current online duration in seconds, when visible.
pub online_seconds: Option<i64>,
/// Current idle time in milliseconds, when visible.
pub idle_milliseconds: Option<i64>,
/// Current ping in milliseconds, when visible.
pub ping_milliseconds: Option<i64>,
/// Current ping deviation in milliseconds, when visible.
pub ping_deviation_milliseconds: Option<i64>,
/// Client address. Empty when permission-gated.
pub client_address: String,
/// Resolved server group names for the online client.
pub server_groups: Vec<String>,
/// Resolved channel group name for the online client.
pub channel_group: String,
/// TeamSpeak avatar file path suffix, when an avatar hash is present.
pub avatar_path: String,
/// Downloaded bytes this month.
pub bytes_downloaded_month: Option<u64>,
/// Uploaded bytes this month.
pub bytes_uploaded_month: Option<u64>,
/// Downloaded bytes across all time.
pub bytes_downloaded_total: Option<u64>,
/// Uploaded bytes across all time.
pub bytes_uploaded_total: Option<u64>,
/// Client-to-server total packet loss ratio.
pub packet_loss_client_to_server_total: Option<f32>,
/// Server-to-client total packet loss ratio.
pub packet_loss_server_to_client_total: Option<f32>,
}
/// Snapshot of the server's published state at a moment in time.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ServerSnapshot {
/// Server name.
pub server_name: String,
/// Server welcome banner. Contains markup from the upstream
/// server (BBCode-like); not parsed here.
pub welcome_message: String,
/// Server platform string.
pub platform: String,
/// Server version string.
pub version: String,
/// All channels currently known.
pub channels: Vec<ChannelInfo>,
/// All clients currently known.
pub clients: Vec<ClientInfo>,
/// Our own client id as the server published it. Used by the
/// core session to confirm that a `move_to_channel` request
/// actually applied (vs being silently rejected by the
/// server's permission check).
pub own_client_id: u64,
}
impl ChannelId {
pub const ROOT: ChannelId = ChannelId(0);
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ProtocolDelta {
ClientMoved {
client_id: u64,
new_channel_id: u64,
},
ClientJoined {
client_id: u64,
channel_id: u64,
name: String,
input_muted: bool,
output_muted: bool,
is_server_query: bool,
talk_power: i32,
talk_power_granted: bool,
},
ClientLeft {
client_id: u64,
name: String,
},
ClientUpdated {
client_id: u64,
input_muted: bool,
output_muted: bool,
is_server_query: bool,
talk_power: i32,
talk_power_granted: bool,
},
ChannelAdded {
id: u64,
parent: u64,
name: String,
order: i64,
has_password: bool,
needed_talk_power: Option<i32>,
},
ChannelRemoved {
id: u64,
},
ChannelUpdated {
id: u64,
name: String,
has_password: bool,
needed_talk_power: Option<i32>,
},
}