commit 13abafae9dbdcb1ae45af3aea61896785ca6f42b Author: Tobias Reisinger Date: Mon Nov 13 17:01:23 2023 +0100 Init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..edd51d3 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,230 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anstream" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "clap" +version = "4.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2275f18819641850fa26c89acc84d465c1bf91ce57bc2748b28c420473352f64" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07cdf1b148b25c1e1f7a42225e30a0d99a615cd4637eae7365548dd4529b95bc" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "proc-macro2" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "2.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "teamspeak-query-lib" +version = "0.1.0" +dependencies = [ + "clap", + "telnet", +] + +[[package]] +name = "telnet" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99f2cc260bea5219955ab4832c9600a41b87101d280939edae6dd10b3c68a4f3" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f443be5 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "teamspeak-query-lib" +version = "0.1.0" +edition = "2021" + +[dependencies] +telnet = "0.2" +clap = { version = "4.4", features = ["derive"] } \ No newline at end of file diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..66e18b8 --- /dev/null +++ b/install.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env sh + +set -e + +INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/bin}" + +mkdir -p "$INSTALL_DIR" +cargo build --release + +install -Dm755 -t "$INSTALL_DIR" \ + ./target/release/teamspeak-query-lib \ + ./ts-control diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..646a7fe --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,119 @@ +use clap::{Parser, Subcommand, Args}; +use telnet::Telnet; +use crate::parameter::{Parameter, ParameterList}; +use crate::response_classes::{ResponseChannel, ResponseClient}; +use crate::utils; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +#[command(propagate_version = true)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +pub enum Commands { + Channels(ChannelsArgs), + Clients, + Fetch(FetchArgs), + Move(MoveArgs), + Update(UpdateArgs), +} + +#[derive(Args)] +pub struct ChannelsArgs { + #[arg(long)] + pub spacers: bool, +} + +#[derive(Args)] +pub struct FetchArgs { + #[arg(long)] + strict_client: bool, + client: String, +} + +#[derive(Args)] +pub struct MoveArgs { + #[arg(long)] + strict_client: bool, + #[arg(long)] + strict_channel: bool, + channel: String, + client: Option, +} + +#[derive(Args)] +pub struct UpdateArgs { + #[arg(long, short)] + name: Option, + #[arg(long, short)] + away: Option, + #[arg(long, short)] + back: bool, + #[arg(long, short)] + microphone: Option, + #[arg(long, short)] + speakers: Option, +} + + +impl FetchArgs { + pub fn client(&self, connection: &mut Telnet) -> Result, String> { + utils::find_client(connection, &self.client, self.strict_client) + } + +} + +impl MoveArgs { + pub fn channel(&self, connection: &mut Telnet) -> Result, String> { + utils::find_channel(connection, &self.channel, self.strict_channel) + } + pub fn client(&self, connection: &mut Telnet) -> Result, String> { + match &self.client { + Some(client) => { + utils::find_client(connection, client, self.strict_client) + } + None => { + match utils::find_self(connection) { + Ok(client) => Ok(Some(client)), + Err(msg) => Err(msg) + } + } + } + } +} + +impl UpdateArgs { + pub fn to_parameter_list(&self) -> ParameterList { + let mut params: ParameterList = Vec::new(); + + if let Some(name) = &self.name { + params.push(Parameter::new(String::from("client_nickname"), name.clone())); + } + + if let Some(away) = &self.away { + params.push(Parameter::new(String::from("client_away_message"), away.clone())); + params.push(Parameter::new(String::from("client_away"), String::from("1"))); + } + if self.back { + params.push(Parameter::new(String::from("client_away"), String::from("0"))); + } + + if let Some(microphone) = self.microphone { + let muted = u8::from(!microphone).to_string(); + params.push(Parameter::new(String::from("client_input_muted"), muted)); + } + if let Some(speakers) = self.speakers { + let muted = u8::from(!speakers).to_string(); + params.push(Parameter::new(String::from("client_output_muted"), muted)); + } + + params + } +} + +pub fn init() -> Commands { + Cli::parse().command +} \ No newline at end of file diff --git a/src/commands.rs b/src/commands.rs new file mode 100644 index 0000000..4cfa611 --- /dev/null +++ b/src/commands.rs @@ -0,0 +1,107 @@ +use telnet::Telnet; +use telnet::Event::Data; +use crate::parameter::ParameterList; + +use crate::response::Response; + +fn to_single_response(resp: Response) -> Response { + match resp { + Response::DataList(list) => Response::Data(list[0].clone()), + _ => resp + } +} + +fn read_part(connection: &mut Telnet) -> Result { + match connection.read() { + Ok(event) => { + match event { + Data(bytes) => Ok(String::from_utf8(bytes.to_vec()) + .map_err(|_| "Teamspeak returned a badly formatted response.")?), + _ => { + Err(String::from("Received unknown event from Teamspeak.")) + } + } + } + Err(_) => { + Err(String::from("Failed to read from Teamspeak.")) + } + } +} + +fn read_response_buffer(connection: &mut Telnet, buffer: &mut String) -> Result<(String, String), String> { + loop { + buffer.push_str(&read_part(connection)?); + match buffer.split_once("\n\r") { + None => {} + Some((response, remaining)) => { + return Ok((String::from(response), String::from(remaining))); + } + } + } +} + +fn send_command(connection: &mut Telnet, command: &str, skip_ok: bool) -> Result { + connection.write(command.as_bytes()).map_err(|_| "Failed to write to Teamspeak.")?; + + read_response(connection, skip_ok, String::new()) +} +fn read_response(connection: &mut Telnet, skip_ok: bool, mut buffer: String) -> Result { + let (response_str, buffer) = read_response_buffer(connection, &mut buffer)?; + + match Response::try_from(response_str) { + Ok(resp) => { + Ok(resp) + } + Err(err) => { + if err.is_error_okay() { + if skip_ok { + read_response(connection, skip_ok, buffer) + } else { + // empty Ok response + Ok(Response::Data(Vec::new())) + } + } else { + Err(format!("Received error response from Teamspeak: {} ({})", err.msg, err.id)) + } + } + } +} + +pub fn login(connection: &mut Telnet, apikey: &str) -> Result { + send_command(connection, &format!("auth apikey={}\n", apikey), false) +} + +#[allow(dead_code)] +pub fn set_name(connection: &mut Telnet, name: &str) -> Result { + send_command(connection, &format!("clientupdate client_nickname={}\n", name), true) +} + +pub fn channellist(connection: &mut Telnet) -> Result { + send_command(connection, "channellist\n", true) +} + +pub fn clientlist(connection: &mut Telnet) -> Result { + send_command(connection, "clientlist\n", true) +} + +pub fn whoami(connection: &mut Telnet) -> Result { + send_command(connection, "whoami\n", true).map(to_single_response) +} + +pub fn clientmove(connection: &mut Telnet, cid: &i32, clid_list: Vec<&i32>) -> Result { + let clid_str = clid_list + .iter() + .map(|clid| format!("clid={}", clid)) + .collect::>() + .join("|"); + send_command(connection, &format!("clientmove cid={} {}\n", cid, clid_str), false) +} + +pub fn clientupdate(connection: &mut Telnet, parameters: &ParameterList) -> Result { + let parameters_str = parameters + .iter() + .map(|param| format!("{}={}", param.name, param.value)) + .collect::>() + .join(" "); + send_command(connection, &format!("clientupdate {}\n", parameters_str), false) +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e363b32 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,114 @@ +mod response; +mod utils; +mod commands; +mod parameter; +mod response_classes; +mod cli; + +use std::process::exit; +use telnet::Telnet; +use crate::cli::Commands; + +fn main() { + + let cli = cli::init(); + + let connection = Telnet::connect(("127.0.0.1", 25639), 512 * 1024); + if connection.is_err() { + println!("Failed to connect to Teamspeak."); + exit(1); + } + let mut connection = connection.unwrap(); + + utils::skip_welcome(&mut connection); + utils::login(&mut connection); + + // You can check for the existence of subcommands, and if found use their + // matches just as you would the top level cmd + match &cli { + Commands::Channels(args) => { + match utils::get_channels(&mut connection, args.spacers) { + Ok(channels) => { + for channel in channels { + println!("{}", channel.channel_name); + } + } + Err(msg) => { + println!("Failed to get channels: {}", msg); + exit(1); + } + } + } + + Commands::Clients => { + match utils::get_clients(&mut connection) { + Ok(clients) => { + for client in clients { + println!("{}", client.client_nickname); + } + } + Err(msg) => { + println!("Failed to get clients: {}", msg); + exit(1); + } + } + } + + Commands::Fetch(args) => { + let client = args.client(&mut connection).unwrap_or_else(|err| { + println!("Failed to find client for move: {}", err); + exit(1); + }) + .unwrap_or_else(|| { + println!("Failed to find client for move."); + exit(1); + }); + + match utils::fetch_client(&mut connection, &[client]) { + Ok(resp) => println!("Successfully fetched client: {}", resp), + Err(msg) => { + println!("Failed to fetch client: {}", msg); + exit(1); + } + } + } + + Commands::Move(args) => { + let channel = args.channel(&mut connection).unwrap_or_else(|err| { + println!("Failed to find channel for move: {}", err); + exit(1); + }) + .unwrap_or_else(|| { + println!("Failed to find channel for move."); + exit(1); + }); + + let client = args.client(&mut connection).unwrap_or_else(|err| { + println!("Failed to find client for move: {}", err); + exit(1); + }) + .unwrap_or_else(|| { + println!("Failed to find client for move."); + exit(1); + }); + + match utils::move_client(&mut connection, &channel, &[client]) { + Ok(resp) => println!("Successfully moved client: {}", resp), + Err(msg) => { + println!("Failed to move client: {}", msg); + exit(1); + } + } + } + + Commands::Update(args) => { + match utils::update_client(&mut connection, &args.to_parameter_list()) { + Ok(_) => println!("Successfully updated client."), + Err(msg) => { + println!("Failed to update client: {}", msg); + exit(1); + } + } + } + } +} \ No newline at end of file diff --git a/src/parameter.rs b/src/parameter.rs new file mode 100644 index 0000000..d1aae1c --- /dev/null +++ b/src/parameter.rs @@ -0,0 +1,87 @@ +use std::fmt::{Debug, Display, Formatter}; + +#[derive(Clone)] +pub struct Parameter { + pub name: String, + pub value: String, +} + +pub type ParameterList = Vec; + +pub fn parameter_find(params: &Vec, name: &str) -> Option { + for param in params { + if param.name == name { + return Some(param.clone()); + } + } + None +} +pub fn parameter_list_find(param_lists: &Vec, name: &str, value: &str, strict: bool) -> Option { + for params in param_lists { + for param in params { + if param.name != name { + continue; + } + if param.value != value && strict { + continue; + } + if !param.value.contains(value) && !strict { + continue; + } + return Some(params.clone()); + } + } + None +} + +pub fn parameter_parse(params_str: &str) -> ParameterList { + let parts: Vec<&str> = params_str.split(' ').collect(); + + let mut response_params = Vec::new(); + parts.iter().for_each(|part| { + response_params.push(Parameter::from(part.split_once('=').unwrap())); + }); + + response_params +} + +impl From<(&str, &str)> for Parameter { + fn from(param: (&str, &str)) -> Parameter { + Parameter::new(String::from(param.0), String::from(param.1)) + } +} + +impl Parameter { + pub fn new(name: String, value: String) -> Parameter { + let value = value + .replace("\\s", " ") + .replace("\\p", "|"); + + Parameter { + name, + value, + } + } + + pub fn to_i32(&self, default: i32) -> i32 { + self.value.parse::().unwrap_or(default) + } +} + +impl Display for Parameter { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}={}", self.name, self.value) + } +} + +impl Debug for Parameter { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}={}", self.name, self.value) + } +} + +impl Default for Parameter { + fn default() -> Self { + Parameter::new(String::from(""), String::from("")) + } +} \ No newline at end of file diff --git a/src/response.rs b/src/response.rs new file mode 100644 index 0000000..e728338 --- /dev/null +++ b/src/response.rs @@ -0,0 +1,93 @@ +use std::fmt::{Debug, Display, Formatter}; +use crate::parameter::*; + +pub enum Response { + Data(ParameterList), + DataList(Vec), +} + +pub struct ResponseError { + pub id: i32, + pub msg: String, +} + +impl TryFrom for Response { + type Error = ResponseError; + + fn try_from(response_str: String) -> Result { + let mut response_str = response_str.trim_end_matches("\n\r"); + + if response_str.starts_with("error ") { + response_str = response_str.trim_start_matches("error "); + let response_params = parameter_parse(response_str); + Err(ResponseError::create_error(&response_params)) + } + else { + let mut parameter_lists: Vec = Vec::new(); + for response_entry in response_str.split('|') { + let response_params = parameter_parse(response_entry); + parameter_lists.push(response_params); + } + Ok(Response::DataList(parameter_lists)) + } + } +} + +impl Debug for Response { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Response::Data(params) => { + write!(f, "Data:")?; + write!(f, "{:?};", params)?; + Ok(()) + } + Response::DataList(params_list) => { + write!(f, "DataList:")?; + for params in params_list { + write!(f, "{:?};", params)?; + } + Ok(()) + } + } + } +} + +impl Display for Response { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Response::Data(params) => { + for param in params { + write!(f, "{};", param)?; + } + Ok(()) + } + Response::DataList(params_list) => { + for params in params_list { + write!(f, "[")?; + for param in params { + write!(f, "{};", param)?; + } + write!(f, "]")?; + } + Ok(()) + } + } + } +} + +impl ResponseError { + pub fn is_error_okay(&self) -> bool { + self.id == 0 + } + + fn create_error(params: &Vec) -> ResponseError { + ResponseError { + id: parameter_find(params, "id") + .unwrap_or_else(|| Parameter::new(String::from("id"), String::from("-1"))) + .to_i32(-1), + msg: parameter_find(params, "msg") + .unwrap_or_else(|| Parameter::new(String::from("msg"), String::from("Unknown error."))) + .value, + } + } +} \ No newline at end of file diff --git a/src/response_classes.rs b/src/response_classes.rs new file mode 100644 index 0000000..0006a54 --- /dev/null +++ b/src/response_classes.rs @@ -0,0 +1,106 @@ +use crate::parameter::{parameter_find, ParameterList}; + +#[derive(Debug)] +pub struct ResponseChannel { + pub cid: i32, + pub pid: i32, + pub channel_order: i32, + pub channel_name: String, + pub channel_topic: String, + pub channel_flag_are_subscribed: bool, +} + +impl From for ResponseChannel { + fn from(value: ParameterList) -> Self { + ResponseChannel { + cid: parameter_find(&value, "cid") + .unwrap_or_default() + .to_i32(-1), + pid: parameter_find(&value, "pid") + .unwrap_or_default() + .to_i32(-1), + channel_order: parameter_find(&value, "channel_order") + .unwrap_or_default() + .to_i32(-1), + channel_name: parameter_find(&value, "channel_name") + .unwrap_or_default() + .value, + channel_topic: parameter_find(&value, "channel_topic") + .unwrap_or_default() + .value, + channel_flag_are_subscribed: parameter_find(&value, "channel_flag_are_subscribed") + .unwrap_or_default() + .to_i32(0) == 1, + } + } +} + +impl ResponseChannel { + pub fn is_spacer(&self) -> bool { + self.channel_name.starts_with("[spacer") + || self.channel_name.starts_with("[*spacer") + || self.channel_name.starts_with("[lspacer") + || self.channel_name.starts_with("[cspacer") + || self.channel_name.starts_with("[rspacer") + } + + fn list_find_next(list: &mut Vec, order: i32, pid: i32) -> Option { + let index = list.iter().position(|a| a.channel_order == order && a.pid == pid)?; + Some(list.swap_remove(index)) + } + + pub fn sort_list(mut list: Vec) -> Vec { + let mut list_sorted: Vec = Vec::new(); + let mut pids: Vec = Vec::new(); + let mut pid = 0; + let mut order = 0; + + while !list.is_empty() { + match ResponseChannel::list_find_next(&mut list, order, pid) { + None => { + order = pid; + pid = pids.pop().unwrap_or(0); + } + Some(elem) => { + pids.push(pid); + pid = elem.cid; + order = 0; + list_sorted.push(elem); + } + } + } + + list_sorted + } +} + +#[derive(Debug)] +pub struct ResponseClient { + pub cid: i32, + pub clid: i32, + pub client_database_id: i32, + pub client_nickname: String, + pub client_type: i32, +} + +impl From for ResponseClient { + fn from(value: ParameterList) -> Self { + ResponseClient { + cid: parameter_find(&value, "cid") + .unwrap_or_default() + .to_i32(-1), + clid: parameter_find(&value, "clid") + .unwrap_or_default() + .to_i32(-1), + client_database_id: parameter_find(&value, "client_database_id") + .unwrap_or_default() + .to_i32(-1), + client_nickname: parameter_find(&value, "client_nickname") + .unwrap_or_default() + .value, + client_type: parameter_find(&value, "channel_topic") + .unwrap_or_default() + .to_i32(0), + } + } +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..6a56058 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,179 @@ +use std::process::exit; +use telnet::Telnet; +use std::time::Duration; +use telnet::Event::TimedOut; + +use crate::{commands, parameter}; +use crate::parameter::ParameterList; +use crate::response::Response; +use crate::response_classes::{ResponseChannel, ResponseClient}; + +pub fn skip_welcome(connection: &mut Telnet) { + loop { + let event_result = connection.read_timeout(Duration::from_millis(100)); + match event_result { + Ok(event) => { + if let TimedOut = event { + break; + } + } + Err(_) => println!("Failed to read from Teamspeak."), + } + } +} + +pub fn login(connection: &mut Telnet) { + // read api key from environment variable + let apikey = std::env::var("TS3_CLIENT_API_KEY").unwrap_or_else(|_| { + println!("No API key found in environment variable TS3_CLIENT_API_KEY."); + exit(1); + }); + + match commands::login(connection, &apikey) { + Ok(_) => {}, + Err(msg) => { + println!("Failed to authenticate with Teamspeak: {}", msg); + exit(1); + } + } +} + +pub fn get_channels(connection: &mut Telnet, spacers: bool) -> Result, String> { + match commands::channellist(connection) { + Ok(response) => { + match response { + Response::DataList(parameter_lists) => { + let channels: Vec = parameter_lists.iter() + .map(|params| ResponseChannel::from(params.clone())) + .collect(); + let mut channels = ResponseChannel::sort_list(channels); + if !spacers { + channels.retain(|c| !c.is_spacer()); + } + Ok(channels) + } + _ => Err(String::from("Received unexpected response from Teamspeak.")) + } + } + Err(msg) => Err(msg) + } +} + +pub fn find_channel(connection: &mut Telnet, name: &str, strict: bool) -> Result, String> { + match commands::channellist(connection) { + Ok(response) => { + match response { + Response::DataList(parameter_lists) => { + match parameter::parameter_list_find(¶meter_lists, "channel_name", name, strict) { + Some(params) => { + Ok(Some(ResponseChannel::from(params))) + } + None => { + Ok(None) + } + } + } + _ => Err(String::from("Received unexpected response from Teamspeak.")) + } + } + Err(msg) => Err(msg) + } +} + +pub fn get_clients(connection: &mut Telnet) -> Result, String> { + match commands::clientlist(connection) { + Ok(response) => { + match response { + Response::DataList(parameter_lists) => { + let mut clients: Vec = parameter_lists.iter() + .map(|params| ResponseClient::from(params.clone())) + .collect(); + + clients.sort_by(|a, b| a.client_nickname.cmp(&b.client_nickname)); + + Ok(clients) + } + _ => Err(String::from("Received unexpected response from Teamspeak.")) + } + } + Err(msg) => Err(msg) + } +} + +pub fn find_client(connection: &mut Telnet, name: &str, strict: bool) -> Result, String> { + match commands::clientlist(connection) { + Ok(response) => { + match response { + Response::DataList(parameter_lists) => { + match parameter::parameter_list_find(¶meter_lists, "client_nickname", name, strict) { + Some(params) => { + Ok(Some(ResponseClient::from(params))) + } + None => { + Ok(None) + } + } + } + _ => Err(String::from("Received unexpected response from Teamspeak.")) + } + } + Err(msg) => Err(msg) + } +} + +fn get_self_clid(connection: &mut Telnet) -> Result { + match commands::whoami(connection) { + Ok(response) => { + match response { + Response::Data(params) => { + match parameter::parameter_find(¶ms, "clid") { + None => Err(String::from("Could not find clid in response from Teamspeak.")), + Some(param) => Ok(param.value) + } + } + _ => Err(String::from("Received unexpected response from Teamspeak for whoami.")) + } + } + Err(msg) => Err(msg) + } +} + +pub fn find_self(connection: &mut Telnet) -> Result { + let clid = get_self_clid(connection)?; + + match commands::clientlist(connection) { + Ok(response) => { + match response { + Response::DataList(parameter_lists) => { + match parameter::parameter_list_find(¶meter_lists, "clid", &clid, false) { + Some(params) => { + Ok(ResponseClient::from(params)) + } + None => { + Err(String::from("Could not find self in response from Teamspeak.")) + } + } + } + _ => Err(String::from("Received unexpected response from Teamspeak for clientlist.")) + } + } + Err(msg) => Err(msg) + } +} + +pub fn fetch_client(connection: &mut Telnet, clients: &[ResponseClient]) -> Result { + let cid = find_self(connection)?.cid; + let clid_list: Vec<&i32> = clients.iter().map(|c| &c.clid).collect(); + + commands::clientmove(connection, &cid, clid_list) +} + +pub fn move_client(connection: &mut Telnet, channel: &ResponseChannel, clients: &[ResponseClient]) -> Result { + let clid_list: Vec<&i32> = clients.iter().map(|c| &c.clid).collect(); + + commands::clientmove(connection, &channel.cid, clid_list) +} + +pub fn update_client(connection: &mut Telnet, parameters: &ParameterList) -> Result { + commands::clientupdate(connection, parameters) +} \ No newline at end of file diff --git a/ts-control b/ts-control new file mode 100755 index 0000000..30912b4 --- /dev/null +++ b/ts-control @@ -0,0 +1,39 @@ +#!/usr/bin/env sh + +action=$(printf "move\nfetch\naway\nback" | $DMENU) + +ts_control_move_self() { + channel=$(teamspeak-query-lib channels | $DMENU) + if [ -z "$channel" ]; then + return 1 + fi + teamspeak-query-lib move --strict-channel "$channel" + return 0 +} + +case $action in + "move") + ts_control_move_self + ;; + "fetch") + client=$(teamspeak-query-lib clients | $DMENU) + if [ -z "$client" ]; then + exit 1 + fi + teamspeak-query-lib fetch --strict-client "$client" + ;; + "away") + message=$(printf "\n" | $DMENU -p "message") + if [ -z "$message" ]; then + exit 1 + fi + teamspeak-query-lib move "Away From Keyboard" + teamspeak-query-lib update --away "$message" + teamspeak-query-lib update --microphone=false --speakers=false --away "$message" + ;; + "back") + teamspeak-query-lib update --back + ts_control_move_self + teamspeak-query-lib update --microphone=true --speakers=true + ;; +esac