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

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