This commit is contained in:
Tobias Reisinger 2023-11-13 17:01:23 +01:00
commit 13abafae9d
Signed by: serguzim
GPG key ID: 13AD60C237A28DFE
12 changed files with 1095 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

230
Cargo.lock generated Normal file
View file

@ -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"

8
Cargo.toml Normal file
View file

@ -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"] }

12
install.sh Executable file
View file

@ -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

119
src/cli.rs Normal file
View file

@ -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<String>,
}
#[derive(Args)]
pub struct UpdateArgs {
#[arg(long, short)]
name: Option<String>,
#[arg(long, short)]
away: Option<String>,
#[arg(long, short)]
back: bool,
#[arg(long, short)]
microphone: Option<bool>,
#[arg(long, short)]
speakers: Option<bool>,
}
impl FetchArgs {
pub fn client(&self, connection: &mut Telnet) -> Result<Option<ResponseClient>, String> {
utils::find_client(connection, &self.client, self.strict_client)
}
}
impl MoveArgs {
pub fn channel(&self, connection: &mut Telnet) -> Result<Option<ResponseChannel>, String> {
utils::find_channel(connection, &self.channel, self.strict_channel)
}
pub fn client(&self, connection: &mut Telnet) -> Result<Option<ResponseClient>, 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
}

107
src/commands.rs Normal file
View file

@ -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<String, String> {
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<Response, String> {
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<Response, String> {
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<Response, String> {
send_command(connection, &format!("auth apikey={}\n", apikey), false)
}
#[allow(dead_code)]
pub fn set_name(connection: &mut Telnet, name: &str) -> Result<Response, String> {
send_command(connection, &format!("clientupdate client_nickname={}\n", name), true)
}
pub fn channellist(connection: &mut Telnet) -> Result<Response, String> {
send_command(connection, "channellist\n", true)
}
pub fn clientlist(connection: &mut Telnet) -> Result<Response, String> {
send_command(connection, "clientlist\n", true)
}
pub fn whoami(connection: &mut Telnet) -> Result<Response, String> {
send_command(connection, "whoami\n", true).map(to_single_response)
}
pub fn clientmove(connection: &mut Telnet, cid: &i32, clid_list: Vec<&i32>) -> Result<Response, String> {
let clid_str = clid_list
.iter()
.map(|clid| format!("clid={}", clid))
.collect::<Vec<String>>()
.join("|");
send_command(connection, &format!("clientmove cid={} {}\n", cid, clid_str), false)
}
pub fn clientupdate(connection: &mut Telnet, parameters: &ParameterList) -> Result<Response, String> {
let parameters_str = parameters
.iter()
.map(|param| format!("{}={}", param.name, param.value))
.collect::<Vec<String>>()
.join(" ");
send_command(connection, &format!("clientupdate {}\n", parameters_str), false)
}

114
src/main.rs Normal file
View file

@ -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);
}
}
}
}
}

87
src/parameter.rs Normal file
View file

@ -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<Parameter>;
pub fn parameter_find(params: &Vec<Parameter>, name: &str) -> Option<Parameter> {
for param in params {
if param.name == name {
return Some(param.clone());
}
}
None
}
pub fn parameter_list_find(param_lists: &Vec<ParameterList>, name: &str, value: &str, strict: bool) -> Option<ParameterList> {
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::<i32>().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(""))
}
}

93
src/response.rs Normal file
View file

@ -0,0 +1,93 @@
use std::fmt::{Debug, Display, Formatter};
use crate::parameter::*;
pub enum Response {
Data(ParameterList),
DataList(Vec<ParameterList>),
}
pub struct ResponseError {
pub id: i32,
pub msg: String,
}
impl TryFrom<String> for Response {
type Error = ResponseError;
fn try_from(response_str: String) -> Result<Self, ResponseError> {
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<ParameterList> = 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<Parameter>) -> 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,
}
}
}

106
src/response_classes.rs Normal file
View file

@ -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<ParameterList> 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<ResponseChannel>, order: i32, pid: i32) -> Option<ResponseChannel> {
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<ResponseChannel>) -> Vec<ResponseChannel> {
let mut list_sorted: Vec<ResponseChannel> = Vec::new();
let mut pids: Vec<i32> = 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<ParameterList> 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),
}
}
}

179
src/utils.rs Normal file
View file

@ -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<Vec<ResponseChannel>, String> {
match commands::channellist(connection) {
Ok(response) => {
match response {
Response::DataList(parameter_lists) => {
let channels: Vec<ResponseChannel> = 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<Option<ResponseChannel>, String> {
match commands::channellist(connection) {
Ok(response) => {
match response {
Response::DataList(parameter_lists) => {
match parameter::parameter_list_find(&parameter_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<Vec<ResponseClient>, String> {
match commands::clientlist(connection) {
Ok(response) => {
match response {
Response::DataList(parameter_lists) => {
let mut clients: Vec<ResponseClient> = 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<Option<ResponseClient>, String> {
match commands::clientlist(connection) {
Ok(response) => {
match response {
Response::DataList(parameter_lists) => {
match parameter::parameter_list_find(&parameter_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<String, String> {
match commands::whoami(connection) {
Ok(response) => {
match response {
Response::Data(params) => {
match parameter::parameter_find(&params, "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<ResponseClient, String> {
let clid = get_self_clid(connection)?;
match commands::clientlist(connection) {
Ok(response) => {
match response {
Response::DataList(parameter_lists) => {
match parameter::parameter_list_find(&parameter_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<Response, String> {
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<Response, String> {
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<Response, String> {
commands::clientupdate(connection, parameters)
}

39
ts-control Executable file
View file

@ -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