feat: promote linux native audio path
This commit is contained in:
@@ -0,0 +1,776 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use base64::prelude::*;
|
||||
use clap::Parser;
|
||||
use futures::prelude::*;
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
use time::{Duration, OffsetDateTime};
|
||||
use tokio::time as tokio_time;
|
||||
use tracing::{info, warn};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use tsclientlib::data;
|
||||
use tsclientlib::messages::s2c::{InClientDbIdFromUidPart, InClientDbInfoPart, InMessage};
|
||||
use tsclientlib::{
|
||||
ChannelGroupId, ClientDbId, ClientId, Connection, DisconnectOptions, Identity, OutCommandExt,
|
||||
ServerGroupId, StreamItem, Uid,
|
||||
};
|
||||
use tsproto_packets::packets::{Direction, Flags, OutCommand, PacketType};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(
|
||||
name = "tsclientlib-query-spike",
|
||||
about = "Chanora PoC: inspect TeamSpeak client profile + connection info over the full client protocol."
|
||||
)]
|
||||
struct Args {
|
||||
/// Server address (hostname[:port] or TSDNS name).
|
||||
#[arg(short, long, default_value = "kr.teamspeak.app")]
|
||||
address: String,
|
||||
|
||||
/// Nickname used on the server.
|
||||
#[arg(short, long, default_value = "ChanoraPoC-Inspector")]
|
||||
nickname: String,
|
||||
|
||||
/// Optional TeamSpeak identity string. If omitted, a fresh identity is generated.
|
||||
#[arg(long)]
|
||||
identity: Option<String>,
|
||||
|
||||
/// Server password if required.
|
||||
#[arg(long)]
|
||||
password: Option<String>,
|
||||
|
||||
/// Inspect a specific online/offline nickname.
|
||||
#[arg(long)]
|
||||
target_nickname: Option<String>,
|
||||
|
||||
/// Inspect a specific TeamSpeak UID.
|
||||
#[arg(long)]
|
||||
target_uid: Option<String>,
|
||||
|
||||
/// Inspect a specific TeamSpeak database id.
|
||||
#[arg(long)]
|
||||
target_dbid: Option<u64>,
|
||||
|
||||
/// Inspect a specific online client id.
|
||||
#[arg(long)]
|
||||
target_clid: Option<u16>,
|
||||
|
||||
/// Print the online client table before detailed inspection.
|
||||
#[arg(long, default_value_t = true)]
|
||||
show_online: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Target {
|
||||
clid: Option<ClientId>,
|
||||
dbid: Option<ClientDbId>,
|
||||
uid_b64: Option<String>,
|
||||
nickname: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct OnlineTargetSnapshot {
|
||||
description: String,
|
||||
country_code: String,
|
||||
server_groups: Vec<ServerGroupId>,
|
||||
channel_group: ChannelGroupId,
|
||||
avatar_hash: String,
|
||||
uid_b64: Option<String>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
|
||||
let filter = EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| EnvFilter::new("info,tsproto=warn,tsclientlib=warn"));
|
||||
tracing_subscriber::fmt().with_env_filter(filter).init();
|
||||
|
||||
info!(target: "query-spike", address = %args.address, nickname = %args.nickname, "starting query spike");
|
||||
|
||||
let mut builder = Connection::build(args.address.clone()).name(args.nickname.clone());
|
||||
let identity = match &args.identity {
|
||||
Some(raw) => Identity::new_from_str(raw).context("parsing --identity")?,
|
||||
None => Identity::create(),
|
||||
};
|
||||
builder = builder.identity(identity);
|
||||
if let Some(password) = &args.password {
|
||||
builder = builder.password(password.clone());
|
||||
}
|
||||
|
||||
let mut con = builder.connect().context("dialling server")?;
|
||||
wait_for_initial_state(&mut con).await?;
|
||||
if let Ok(state) = con.get_state() {
|
||||
let _ = state.server.set_subscribed(true).send(&mut con);
|
||||
}
|
||||
pump_events(&mut con, std::time::Duration::from_secs(1)).await?;
|
||||
|
||||
let (mut target, online_snapshot) = {
|
||||
let state = con.get_state().context("reading connection state")?;
|
||||
if args.show_online {
|
||||
print_online_clients(state);
|
||||
}
|
||||
let target = resolve_target(&args, state)?;
|
||||
let snapshot = target
|
||||
.clid
|
||||
.and_then(|clid| state.clients.get(&clid))
|
||||
.map(|client| OnlineTargetSnapshot {
|
||||
description: client.description.clone(),
|
||||
country_code: client.country_code.clone(),
|
||||
server_groups: client.server_groups.iter().copied().collect(),
|
||||
channel_group: client.channel_group,
|
||||
avatar_hash: client.avatar_hash.clone(),
|
||||
uid_b64: client.uid.as_ref().map(|uid| uid_to_b64(uid.as_ref())),
|
||||
});
|
||||
(target, snapshot)
|
||||
};
|
||||
|
||||
let _ = request_server_group_list(&mut con).await;
|
||||
let _ = request_channel_group_list(&mut con).await;
|
||||
if target.dbid.is_none() {
|
||||
if let Some(uid) = target.uid_b64.as_deref() {
|
||||
if let Ok(lookup) = request_client_dbid_from_uid(&mut con, uid).await {
|
||||
target.dbid = Some(lookup.client_db_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!();
|
||||
println!("=== Detailed Target ===");
|
||||
println!("Target nickname : {}", target.nickname.as_deref().unwrap_or("Unknown"));
|
||||
println!("Target UID : {}", target.uid_b64.as_deref().unwrap_or("Unknown"));
|
||||
println!(
|
||||
"Target DBID : {}",
|
||||
target
|
||||
.dbid
|
||||
.map(|id| id.0.to_string())
|
||||
.unwrap_or_else(|| "Unknown".to_string())
|
||||
);
|
||||
println!(
|
||||
"Target CLID : {}",
|
||||
target
|
||||
.clid
|
||||
.map(|id| id.0.to_string())
|
||||
.unwrap_or_else(|| "Offline/Unknown".to_string())
|
||||
);
|
||||
|
||||
if let Some(clid) = target.clid {
|
||||
if let Err(error) = request_client_variables(&mut con, clid).await {
|
||||
warn!(target: "query-spike", %error, clid = clid.0, "clientgetvariables failed");
|
||||
}
|
||||
if let Err(error) = request_client_connection_info(&mut con, clid).await {
|
||||
warn!(target: "query-spike", %error, clid = clid.0, "getconnectioninfo failed");
|
||||
}
|
||||
}
|
||||
let db_info = match target.dbid {
|
||||
Some(dbid) => match request_client_db_info(&mut con, dbid).await {
|
||||
Ok(info) => Some(info),
|
||||
Err(error) => {
|
||||
warn!(target: "query-spike", %error, dbid = dbid.0, "clientdbinfo failed");
|
||||
None
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
|
||||
let (optional_data, connection_data, server_group_names, channel_group_names) = {
|
||||
let state = con.get_state().context("reading updated connection state")?;
|
||||
let server_group_names = state
|
||||
.server_groups
|
||||
.iter()
|
||||
.map(|(id, group)| (*id, group.name.clone()))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let channel_group_names = state
|
||||
.channel_groups
|
||||
.iter()
|
||||
.map(|(id, group)| (*id, group.name.clone()))
|
||||
.collect::<HashMap<_, _>>();
|
||||
match target.clid.and_then(|clid| state.clients.get(&clid)) {
|
||||
Some(client) => (
|
||||
client.optional_data.clone(),
|
||||
client.connection_data.clone(),
|
||||
server_group_names,
|
||||
channel_group_names,
|
||||
),
|
||||
None => (None, None, server_group_names, channel_group_names),
|
||||
}
|
||||
};
|
||||
|
||||
print_detailed_summary(
|
||||
target,
|
||||
optional_data.as_ref(),
|
||||
connection_data.as_ref(),
|
||||
db_info.as_ref(),
|
||||
online_snapshot.as_ref(),
|
||||
&server_group_names,
|
||||
&channel_group_names,
|
||||
)?;
|
||||
|
||||
con.disconnect(DisconnectOptions::new()).ok();
|
||||
con.events().for_each(|_| futures::future::ready(())).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn wait_for_initial_state(con: &mut Connection) -> Result<()> {
|
||||
loop {
|
||||
let event = con
|
||||
.events()
|
||||
.next()
|
||||
.await
|
||||
.ok_or_else(|| anyhow!("event stream ended before initial state"))??;
|
||||
if matches!(event, StreamItem::BookEvents(_)) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn pump_events(con: &mut Connection, budget: std::time::Duration) -> Result<()> {
|
||||
let deadline = tokio_time::Instant::now() + budget;
|
||||
loop {
|
||||
let now = tokio_time::Instant::now();
|
||||
if now >= deadline {
|
||||
return Ok(());
|
||||
}
|
||||
let remaining = deadline - now;
|
||||
match tokio_time::timeout(remaining, con.events().next()).await {
|
||||
Ok(Some(Ok(_))) => {}
|
||||
Ok(Some(Err(error))) => return Err(error).context("pumping connection events"),
|
||||
Ok(None) => bail!("event stream ended while pumping events"),
|
||||
Err(_) => return Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_command(name: &str, args: &[(&str, String)], flags: &[&str]) -> OutCommand {
|
||||
let mut command = OutCommand::new(Direction::C2S, Flags::empty(), PacketType::Command, name);
|
||||
for flag in flags {
|
||||
command.write_arg(flag, &"");
|
||||
}
|
||||
for (key, value) in args {
|
||||
command.write_arg(key, value);
|
||||
}
|
||||
command
|
||||
}
|
||||
|
||||
async fn request_messages(con: &mut Connection, command: OutCommand) -> Result<Vec<InMessage>> {
|
||||
let handle = command.send_with_result(con).context("sending command")?;
|
||||
let mut messages = Vec::new();
|
||||
loop {
|
||||
let item = con
|
||||
.events()
|
||||
.next()
|
||||
.await
|
||||
.ok_or_else(|| anyhow!("event stream ended before command response"))??;
|
||||
match item {
|
||||
StreamItem::MessageEvent(message) => messages.push(message),
|
||||
StreamItem::MessageResult(reply, status) if reply == handle => {
|
||||
status.map_err(|error| anyhow!("command failed: {error}"))?;
|
||||
return Ok(messages);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn request_messages_and_quiet(
|
||||
con: &mut Connection,
|
||||
command: OutCommand,
|
||||
quiet_period: std::time::Duration,
|
||||
) -> Result<Vec<InMessage>> {
|
||||
let messages = request_messages(con, command).await?;
|
||||
pump_events(con, quiet_period).await?;
|
||||
Ok(messages)
|
||||
}
|
||||
|
||||
async fn request_client_variables(con: &mut Connection, clid: ClientId) -> Result<()> {
|
||||
let command = build_command("clientgetvariables", &[("clid", clid.0.to_string())], &[]);
|
||||
request_messages_and_quiet(con, command, std::time::Duration::from_millis(350)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn request_client_connection_info(con: &mut Connection, clid: ClientId) -> Result<()> {
|
||||
let command = build_command("getconnectioninfo", &[("clid", clid.0.to_string())], &[]);
|
||||
request_messages_and_quiet(con, command, std::time::Duration::from_millis(350)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn request_client_db_info(con: &mut Connection, dbid: ClientDbId) -> Result<InClientDbInfoPart> {
|
||||
let command = build_command("clientdbinfo", &[("cldbid", dbid.0.to_string())], &[]);
|
||||
let messages = request_messages(con, command).await?;
|
||||
for message in messages {
|
||||
if let InMessage::ClientDbInfo(info) = message {
|
||||
if let Some(row) = info.iter().next() {
|
||||
return Ok(row.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
bail!("clientdbinfo returned no row");
|
||||
}
|
||||
|
||||
async fn request_client_dbid_from_uid(con: &mut Connection, uid: &str) -> Result<InClientDbIdFromUidPart> {
|
||||
let command = build_command("clientgetdbidfromuid", &[("cluid", uid.to_string())], &[]);
|
||||
let messages = request_messages(con, command).await?;
|
||||
for message in messages {
|
||||
if let InMessage::ClientDbIdFromUid(info) = message {
|
||||
if let Some(row) = info.iter().next() {
|
||||
return Ok(row.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
bail!("clientgetdbidfromuid returned no row");
|
||||
}
|
||||
|
||||
async fn request_server_group_list(con: &mut Connection) -> Result<()> {
|
||||
request_messages_and_quiet(
|
||||
con,
|
||||
build_command("servergrouplist", &[], &[]),
|
||||
std::time::Duration::from_millis(200),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn request_channel_group_list(con: &mut Connection) -> Result<()> {
|
||||
request_messages_and_quiet(
|
||||
con,
|
||||
build_command("channelgrouplist", &[], &[]),
|
||||
std::time::Duration::from_millis(200),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_target(args: &Args, state: &data::Connection) -> Result<Target> {
|
||||
if let Some(clid) = args.target_clid {
|
||||
if let Some(row) = state.clients.get(&ClientId(clid)) {
|
||||
return Ok(Target {
|
||||
clid: Some(row.id),
|
||||
dbid: Some(row.database_id),
|
||||
uid_b64: row.uid.as_ref().map(|uid| uid_to_b64(uid.as_ref())),
|
||||
nickname: Some(row.name.clone()),
|
||||
});
|
||||
}
|
||||
return Ok(Target {
|
||||
clid: Some(ClientId(clid)),
|
||||
dbid: None,
|
||||
uid_b64: None,
|
||||
nickname: None,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(uid) = &args.target_uid {
|
||||
if let Some(row) = state
|
||||
.clients
|
||||
.values()
|
||||
.find(|row| row.uid.as_ref().is_some_and(|value| uid_to_b64(value.as_ref()) == *uid))
|
||||
{
|
||||
return Ok(Target {
|
||||
clid: Some(row.id),
|
||||
dbid: Some(row.database_id),
|
||||
uid_b64: Some(uid.clone()),
|
||||
nickname: Some(row.name.clone()),
|
||||
});
|
||||
}
|
||||
return Ok(Target {
|
||||
clid: None,
|
||||
dbid: args.target_dbid.map(ClientDbId),
|
||||
uid_b64: Some(uid.clone()),
|
||||
nickname: args.target_nickname.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(nickname) = &args.target_nickname {
|
||||
if let Some(row) = state.clients.values().find(|row| row.name == *nickname) {
|
||||
return Ok(Target {
|
||||
clid: Some(row.id),
|
||||
dbid: Some(row.database_id),
|
||||
uid_b64: row.uid.as_ref().map(|uid| uid_to_b64(uid.as_ref())),
|
||||
nickname: Some(row.name.clone()),
|
||||
});
|
||||
}
|
||||
return Ok(Target {
|
||||
clid: None,
|
||||
dbid: args.target_dbid.map(ClientDbId),
|
||||
uid_b64: None,
|
||||
nickname: Some(nickname.clone()),
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(dbid) = args.target_dbid {
|
||||
if let Some(row) = state.clients.values().find(|row| row.database_id.0 == dbid) {
|
||||
return Ok(Target {
|
||||
clid: Some(row.id),
|
||||
dbid: Some(row.database_id),
|
||||
uid_b64: row.uid.as_ref().map(|uid| uid_to_b64(uid.as_ref())),
|
||||
nickname: Some(row.name.clone()),
|
||||
});
|
||||
}
|
||||
return Ok(Target {
|
||||
clid: None,
|
||||
dbid: Some(ClientDbId(dbid)),
|
||||
uid_b64: args.target_uid.clone(),
|
||||
nickname: args.target_nickname.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
let own_client_id = state.own_client;
|
||||
let own_client = state
|
||||
.clients
|
||||
.get(&own_client_id)
|
||||
.ok_or_else(|| anyhow!("own client missing from state"))?;
|
||||
Ok(Target {
|
||||
clid: Some(own_client_id),
|
||||
dbid: Some(own_client.database_id),
|
||||
uid_b64: own_client.uid.as_ref().map(|uid| uid_to_b64(uid.as_ref())),
|
||||
nickname: Some(own_client.name.clone()),
|
||||
})
|
||||
}
|
||||
|
||||
fn print_online_clients(state: &data::Connection) {
|
||||
println!("=== Online Clients ===");
|
||||
if !state.server.ips.is_empty() {
|
||||
let ips = state
|
||||
.server
|
||||
.ips
|
||||
.iter()
|
||||
.map(std::string::ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
println!("Server IPs: {ips}");
|
||||
}
|
||||
for row in state.clients.values() {
|
||||
let uid = row
|
||||
.uid
|
||||
.as_ref()
|
||||
.map(|value| uid_to_b64(value.as_ref()))
|
||||
.unwrap_or_else(|| "Hidden".to_string());
|
||||
let country = if row.country_code.is_empty() {
|
||||
"Unknown".to_string()
|
||||
} else {
|
||||
row.country_code.clone()
|
||||
};
|
||||
println!(
|
||||
"- {} | clid={} dbid={} cid={} uid={} country={}",
|
||||
row.name, row.id.0, row.database_id.0, row.channel.0, uid, country
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn print_detailed_summary(
|
||||
target: Target,
|
||||
client_info: Option<&data::OptionalClientData>,
|
||||
connection_info: Option<&data::ConnectionClientData>,
|
||||
db_info: Option<&InClientDbInfoPart>,
|
||||
online_snapshot: Option<&OnlineTargetSnapshot>,
|
||||
server_group_names: &HashMap<ServerGroupId, String>,
|
||||
channel_group_names: &HashMap<ChannelGroupId, String>,
|
||||
) -> Result<()> {
|
||||
let nickname = db_info
|
||||
.map(|info| info.name.clone())
|
||||
.or(target.nickname)
|
||||
.unwrap_or_else(|| "Unknown".to_string());
|
||||
let uid = db_info
|
||||
.map(|info| uid_to_b64(info.uid.as_ref()))
|
||||
.or(target.uid_b64)
|
||||
.unwrap_or_else(|| "Unknown".to_string());
|
||||
let dbid = db_info
|
||||
.map(|info| info.database_id.0)
|
||||
.or(target.dbid.map(|value| value.0))
|
||||
.unwrap_or_default();
|
||||
|
||||
println!();
|
||||
println!("=== Client Profile ===");
|
||||
println!("Nickname : {} ({})", nickname, dbid);
|
||||
println!("Unique ID : {}", uid);
|
||||
println!("Database ID : {}", dbid);
|
||||
println!(
|
||||
"Description : {}",
|
||||
db_info
|
||||
.map(|info| info.description.as_str())
|
||||
.or_else(|| online_snapshot.map(|info| info.description.as_str()))
|
||||
.unwrap_or("")
|
||||
);
|
||||
println!(
|
||||
"Version : {}",
|
||||
client_info.map(|info| info.version.as_str()).unwrap_or("Unknown")
|
||||
);
|
||||
println!(
|
||||
"Connections : {}",
|
||||
client_info
|
||||
.map(|info| info.connections_total)
|
||||
.or_else(|| db_info.map(|info| info.connections_total))
|
||||
.unwrap_or_default()
|
||||
);
|
||||
println!(
|
||||
"First Connected : {}",
|
||||
client_info
|
||||
.map(|info| format_timestamp(info.created))
|
||||
.or_else(|| db_info.map(|info| format_timestamp(info.created)))
|
||||
.unwrap_or_else(|| "Unknown".to_string())
|
||||
);
|
||||
println!(
|
||||
"Last Connected : {}",
|
||||
client_info
|
||||
.map(|info| format_timestamp(info.last_connected))
|
||||
.or_else(|| db_info.map(|info| format_timestamp(info.last_connected)))
|
||||
.unwrap_or_else(|| "Unknown".to_string())
|
||||
);
|
||||
println!(
|
||||
"Online Since : {}",
|
||||
connection_info
|
||||
.and_then(|info| info.connected_time.map(format_duration))
|
||||
.unwrap_or_else(|| "Offline".to_string())
|
||||
);
|
||||
println!(
|
||||
"Country : {}",
|
||||
online_snapshot
|
||||
.map(|info| info.country_code.as_str())
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or("Unknown")
|
||||
);
|
||||
if let Some(info) = client_info {
|
||||
println!("Platform : {}", info.platform);
|
||||
println!(
|
||||
"Server Groups : {}",
|
||||
online_snapshot
|
||||
.map(|snapshot| format_server_groups(&snapshot.server_groups, &server_group_names))
|
||||
.unwrap_or_else(|| "Unknown".to_string())
|
||||
);
|
||||
println!(
|
||||
"Channel Group : {}",
|
||||
online_snapshot
|
||||
.map(|snapshot| {
|
||||
channel_group_names
|
||||
.get(&snapshot.channel_group)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| format!("Unknown ({})", snapshot.channel_group.0))
|
||||
})
|
||||
.unwrap_or_else(|| "Unknown".to_string())
|
||||
);
|
||||
println!(
|
||||
"Avatar Path : {}",
|
||||
online_snapshot
|
||||
.map(|snapshot| {
|
||||
if snapshot.avatar_hash.is_empty() {
|
||||
"None".to_string()
|
||||
} else {
|
||||
snapshot
|
||||
.uid_b64
|
||||
.as_ref()
|
||||
.map(|uid| format!("/avatar_{}", uid_to_avatar_path(uid)))
|
||||
.unwrap_or_else(|| "Hidden".to_string())
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| "Unknown".to_string())
|
||||
);
|
||||
} else if let Some(info) = online_snapshot {
|
||||
println!(
|
||||
"Server Groups : {}",
|
||||
format_server_groups(&info.server_groups, &server_group_names)
|
||||
);
|
||||
println!(
|
||||
"Channel Group : {}",
|
||||
channel_group_names
|
||||
.get(&info.channel_group)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| format!("Unknown ({})", info.channel_group.0))
|
||||
);
|
||||
println!(
|
||||
"Avatar Path : {}",
|
||||
if info.avatar_hash.is_empty() {
|
||||
"None".to_string()
|
||||
} else {
|
||||
info.uid_b64
|
||||
.as_ref()
|
||||
.map(|uid| format!("/avatar_{}", uid_to_avatar_path(uid)))
|
||||
.unwrap_or_else(|| "Hidden".to_string())
|
||||
}
|
||||
);
|
||||
} else {
|
||||
println!("Server Groups : Unknown");
|
||||
println!("Channel Group : Unknown");
|
||||
println!("Avatar Path : Unknown");
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("=== Connection Info ===");
|
||||
if let Some(connection) = connection_info {
|
||||
println!(
|
||||
"Connection Time : {}",
|
||||
connection
|
||||
.connected_time
|
||||
.map(format_duration)
|
||||
.unwrap_or_else(|| "Unknown".to_string())
|
||||
);
|
||||
println!("Idle Time : {}", format_duration(connection.idle_time));
|
||||
println!(
|
||||
"Ping : {}",
|
||||
connection
|
||||
.ping
|
||||
.map(format_duration)
|
||||
.unwrap_or_else(|| "Unknown".to_string())
|
||||
);
|
||||
println!(
|
||||
"Client Address : {}",
|
||||
connection
|
||||
.client_address
|
||||
.map(|address| address.to_string())
|
||||
.unwrap_or_else(|| "Hidden".to_string())
|
||||
);
|
||||
println!(
|
||||
"Packet Loss C->S : total={} speech={} keepalive={} control={}",
|
||||
format_loss(connection.client_to_server_packetloss_total),
|
||||
format_loss(connection.client_to_server_packetloss_speech),
|
||||
format_loss(connection.client_to_server_packetloss_keepalive),
|
||||
format_loss(connection.client_to_server_packetloss_control),
|
||||
);
|
||||
println!(
|
||||
"Packet Loss S->C : total={} speech={} keepalive={} control={}",
|
||||
connection
|
||||
.server_to_client_packetloss_total
|
||||
.map(format_loss)
|
||||
.unwrap_or_else(|| "Hidden".to_string()),
|
||||
connection
|
||||
.server_to_client_packetloss_speech
|
||||
.map(format_loss)
|
||||
.unwrap_or_else(|| "Hidden".to_string()),
|
||||
connection
|
||||
.server_to_client_packetloss_keepalive
|
||||
.map(format_loss)
|
||||
.unwrap_or_else(|| "Hidden".to_string()),
|
||||
connection
|
||||
.server_to_client_packetloss_control
|
||||
.map(format_loss)
|
||||
.unwrap_or_else(|| "Hidden".to_string()),
|
||||
);
|
||||
println!(
|
||||
"Bandwidth Sent : 1s[speech={} keepalive={} control={}] 1m[speech={} keepalive={} control={}]",
|
||||
option_u64(connection.bandwidth_sent_last_second_speech),
|
||||
option_u64(connection.bandwidth_sent_last_second_keepalive),
|
||||
option_u64(connection.bandwidth_sent_last_second_control),
|
||||
option_u64(connection.bandwidth_sent_last_minute_speech),
|
||||
option_u64(connection.bandwidth_sent_last_minute_keepalive),
|
||||
option_u64(connection.bandwidth_sent_last_minute_control),
|
||||
);
|
||||
println!(
|
||||
"Bandwidth Received : 1s[speech={} keepalive={} control={}] 1m[speech={} keepalive={} control={}]",
|
||||
option_u64(connection.bandwidth_received_last_second_speech),
|
||||
option_u64(connection.bandwidth_received_last_second_keepalive),
|
||||
option_u64(connection.bandwidth_received_last_second_control),
|
||||
option_u64(connection.bandwidth_received_last_minute_speech),
|
||||
option_u64(connection.bandwidth_received_last_minute_keepalive),
|
||||
option_u64(connection.bandwidth_received_last_minute_control),
|
||||
);
|
||||
println!(
|
||||
"Filetransfer BW : sent={} recv={}",
|
||||
option_u64(connection.filetransfer_bandwidth_sent),
|
||||
option_u64(connection.filetransfer_bandwidth_received),
|
||||
);
|
||||
println!(
|
||||
"Packets Sent : speech={} keepalive={} control={}",
|
||||
option_u64(connection.packets_sent_speech),
|
||||
option_u64(connection.packets_sent_keepalive),
|
||||
option_u64(connection.packets_sent_control),
|
||||
);
|
||||
println!(
|
||||
"Packets Received : speech={} keepalive={} control={}",
|
||||
option_u64(connection.packets_received_speech),
|
||||
option_u64(connection.packets_received_keepalive),
|
||||
option_u64(connection.packets_received_control),
|
||||
);
|
||||
println!(
|
||||
"Bytes Sent : speech={} keepalive={} control={}",
|
||||
option_u64(connection.bytes_sent_speech),
|
||||
option_u64(connection.bytes_sent_keepalive),
|
||||
option_u64(connection.bytes_sent_control),
|
||||
);
|
||||
println!(
|
||||
"Bytes Received : speech={} keepalive={} control={}",
|
||||
option_u64(connection.bytes_received_speech),
|
||||
option_u64(connection.bytes_received_keepalive),
|
||||
option_u64(connection.bytes_received_control),
|
||||
);
|
||||
} else {
|
||||
println!("Connection details unavailable (offline or permission-gated).");
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("=== Transfer Quota ===");
|
||||
if let Some(info) = client_info {
|
||||
println!("Downloaded this month : {}", info.bytes_downloaded_month);
|
||||
println!("Uploaded this month : {}", info.bytes_uploaded_month);
|
||||
println!("Downloaded total : {}", info.bytes_downloaded_total);
|
||||
println!("Uploaded total : {}", info.bytes_uploaded_total);
|
||||
} else if let Some(info) = db_info {
|
||||
println!("Downloaded this month : {}", info.bytes_downloaded_month);
|
||||
println!("Uploaded this month : {}", info.bytes_uploaded_month);
|
||||
println!("Downloaded total : {}", info.bytes_downloaded_total);
|
||||
println!("Uploaded total : {}", info.bytes_uploaded_total);
|
||||
} else {
|
||||
println!("Quota details unavailable.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn format_server_groups(groups: &[ServerGroupId], names: &HashMap<ServerGroupId, String>) -> String {
|
||||
let mut rendered = Vec::new();
|
||||
for group in groups {
|
||||
rendered.push(
|
||||
names
|
||||
.get(group)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| format!("Unknown ({})", group.0)),
|
||||
);
|
||||
}
|
||||
rendered.join(", ")
|
||||
}
|
||||
|
||||
fn format_timestamp(value: OffsetDateTime) -> String {
|
||||
value
|
||||
.format(&Rfc3339)
|
||||
.unwrap_or_else(|_| value.unix_timestamp().to_string())
|
||||
}
|
||||
|
||||
fn format_duration(duration: Duration) -> String {
|
||||
let total_ms = duration.whole_milliseconds();
|
||||
if total_ms < 1000 {
|
||||
return format!("{total_ms} ms");
|
||||
}
|
||||
let total_seconds = duration.whole_seconds();
|
||||
let seconds = total_seconds % 60;
|
||||
let minutes = (total_seconds / 60) % 60;
|
||||
let hours = total_seconds / 3600;
|
||||
if hours > 0 {
|
||||
format!("{hours}h {minutes}m {seconds}s")
|
||||
} else if minutes > 0 {
|
||||
format!("{minutes}m {seconds}s")
|
||||
} else {
|
||||
format!("{seconds}s")
|
||||
}
|
||||
}
|
||||
|
||||
fn format_loss(value: f32) -> String {
|
||||
format!("{value:.4}")
|
||||
}
|
||||
|
||||
fn option_u64(value: Option<u64>) -> String {
|
||||
value
|
||||
.map(|number| number.to_string())
|
||||
.unwrap_or_else(|| "Hidden".to_string())
|
||||
}
|
||||
|
||||
fn uid_to_b64(uid: &Uid) -> String {
|
||||
BASE64_STANDARD.encode(&uid.0)
|
||||
}
|
||||
|
||||
fn uid_to_avatar_path(uid_b64: &str) -> String {
|
||||
let decoded = BASE64_STANDARD.decode(uid_b64).unwrap_or_default();
|
||||
let mut rendered = String::with_capacity(decoded.len() * 2);
|
||||
for byte in decoded {
|
||||
rendered.push((b'a' + (byte >> 4)) as char);
|
||||
rendered.push((b'a' + (byte & 0x0f)) as char);
|
||||
}
|
||||
rendered
|
||||
}
|
||||
Reference in New Issue
Block a user