From 6b86c032ee28fffe104dab832db075f015077901 Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 12 Jun 2024 21:52:56 -0400 Subject: [PATCH] Add TOML config support --- .gitignore | 2 +- Cargo.lock | 54 ++++++++++++++ Cargo.toml | 2 + src/bot.rs | 207 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 209 ++++++++-------------------------------------------- 5 files changed, 295 insertions(+), 179 deletions(-) create mode 100644 src/bot.rs diff --git a/.gitignore b/.gitignore index a07a452..bab85c2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ target/ -password.txt +config.toml diff --git a/Cargo.lock b/Cargo.lock index 5087608..90a7b4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -727,7 +727,9 @@ checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" name = "group-bot" version = "0.1.0" dependencies = [ + "serde", "tokio", + "toml", "veloren-client", "veloren-common", ] @@ -1938,6 +1940,15 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "serde_spanned" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +dependencies = [ + "serde", +] + [[package]] name = "sha2" version = "0.10.8" @@ -2227,6 +2238,40 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tower" version = "0.4.13" @@ -2879,6 +2924,15 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +[[package]] +name = "winnow" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.50.0" diff --git a/Cargo.toml b/Cargo.toml index fb244dd..4eee39f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,8 @@ edition = "2021" tokio = "1.38.0" veloren-common = { git = "https://gitlab.com/veloren/veloren", branch = "master", features = ["no-assets"] } veloren-client = { git = "https://gitlab.com/veloren/veloren", branch = "master" } +toml = "0.8.14" +serde = { version = "1.0.203", features = ["derive"] } [patch.crates-io] specs = { git = "https://github.com/amethyst/specs.git", rev = "4e2da1df29ee840baa9b936593c45592b7c9ae27" } diff --git a/src/bot.rs b/src/bot.rs new file mode 100644 index 0000000..5132595 --- /dev/null +++ b/src/bot.rs @@ -0,0 +1,207 @@ +use std::{sync::Arc, time::Duration}; + +use tokio::runtime::Runtime; +use veloren_client::{addr::ConnectionArgs, Client, Event}; +use veloren_common::{ + clock::Clock, + comp::{invite::InviteKind, ChatType, ControllerInputs}, + uid::Uid, + ViewDistances, +}; + +use crate::Config; + +pub struct Bot { + client: Client, + clock: Clock, + admin_list: Vec, +} + +impl Bot { + pub fn new(username: &str, password: &str, admin_list: Vec) -> Result { + let client = connect_to_veloren(username, password)?; + let clock = Clock::new(Duration::from_secs_f64(1.0)); + + Ok(Bot { + client, + clock, + admin_list, + }) + } + + pub fn select_character(&mut self) -> Result<(), String> { + self.client.load_character_list(); + + while self.client.character_list().loading { + self.client + .tick(ControllerInputs::default(), self.clock.dt()) + .map_err(|error| format!("{error:?}"))?; + self.clock.tick(); + } + + let character_id = self + .client + .character_list() + .characters + .first() + .expect("No characters to select") + .character + .id + .unwrap(); + + self.client.request_character( + character_id, + ViewDistances { + terrain: 0, + entity: 0, + }, + ); + + Ok(()) + } + + pub fn tick(&mut self) -> Result<(), String> { + let events = self + .client + .tick(ControllerInputs::default(), self.clock.dt()) + .expect("Failed to run client."); + + for event in events { + self.handle_event(event)?; + } + + self.client.cleanup(); + self.clock.tick(); + + Ok(()) + } + + fn handle_event(&mut self, event: Event) -> Result<(), String> { + if let Event::Chat(message) = event { + match message.chat_type { + ChatType::Tell(sender, _) | ChatType::Group(sender, _) => { + let sender_uuid = self + .client + .player_list() + .get(&sender) + .unwrap() + .uuid + .to_string(); + + self.handle_message( + message.into_content().as_plain().unwrap_or(""), + &sender_uuid, + )?; + } + _ => {} + } + } + + Ok(()) + } + + fn handle_message(&mut self, content: &str, sender: &String) -> Result<(), String> { + let mut words = content.split_whitespace(); + + if let Some(command) = words.next() { + match command { + "admin" => { + if self.admin_list.contains(sender) { + self.adminify_players(words)?; + } + } + "inv" => self.invite_players(words), + "kick" => { + if self.admin_list.contains(sender) { + self.kick_players(words) + } + } + _ => {} + } + } + + Ok(()) + } + + fn adminify_players<'a, T: Iterator>( + &mut self, + names: T, + ) -> Result<(), String> { + for name in names { + if let Some(player_id) = self.find_uuid(&name) { + self.admin_list.push(player_id); + } + } + + let old_config = Config::read(); + let new_config = Config { + username: old_config.username, + password: old_config.password, + admin_list: self.admin_list.clone(), + }; + + new_config.write()?; + + Ok(()) + } + + fn invite_players<'a, T: Iterator>(&mut self, names: T) { + for name in names { + if let Some(player_id) = self.find_uid(&name) { + self.client + .send_invite(player_id.clone(), InviteKind::Group); + } + } + } + + fn kick_players<'a, T: Iterator>(&mut self, names: T) { + for name in names { + if let Some(player_id) = self.find_uid(&name) { + self.client.kick_from_group(player_id.clone()); + } + } + } + + fn find_uid<'a>(&'a self, name: &str) -> Option<&'a Uid> { + self.client.player_list().iter().find_map(|(id, info)| { + if info.player_alias == name { + Some(id) + } else { + None + } + }) + } + + fn find_uuid(&self, name: &str) -> Option { + self.client.player_list().iter().find_map(|(_, info)| { + if info.player_alias == name { + Some(info.uuid.to_string()) + } else { + None + } + }) + } +} + +fn connect_to_veloren(username: &str, password: &str) -> Result { + let runtime = Arc::new(Runtime::new().unwrap()); + let runtime2 = Arc::clone(&runtime); + + runtime + .block_on(Client::new( + ConnectionArgs::Tcp { + hostname: "server.veloren.net".to_string(), + prefer_ipv6: false, + }, + runtime2, + &mut None, + username, + password, + None, + |provider| provider == "https://auth.veloren.net", + &|_| {}, + |_| {}, + Default::default(), + )) + .map_err(|error| format!("{error:?}")) +} diff --git a/src/main.rs b/src/main.rs index e43965d..3a7862f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,191 +1,44 @@ +mod bot; + use std::{ env::var, fs::{read_to_string, write}, - sync::Arc, - time::Duration, }; -use tokio::runtime::Runtime; -use veloren_client::{addr::ConnectionArgs, Client, Event}; -use veloren_common::{ - clock::Clock, - comp::{invite::InviteKind, ChatType, ControllerInputs}, - uuid::Uuid, - ViewDistances, -}; +use bot::Bot; +use serde::{Deserialize, Serialize}; -const CRABO_UUID: &str = "3127d1eb-72ea-4342-a4fa-06dd2fb0bad9"; +#[derive(Serialize, Deserialize)] +struct Config { + pub username: String, + pub password: String, + pub admin_list: Vec, +} + +impl Config { + fn read() -> Self { + let config_path = var("CONFIG_PATH").expect("Provide CONFIG_PATH environment variable"); + let config_file_content = read_to_string(config_path).expect("Failed to read config file"); + + toml::from_str::(&config_file_content).expect("Failed to deserialize config file.") + } + + fn write(&self) -> Result<(), String> { + let config_path = var("CONFIG_PATH").expect("Provide CONFIG_PATH environment variable"); + let config_string = toml::to_string(self).expect("Failed to serialize Config"); + + write(config_path, config_string).map_err(|error| error.to_string()) + } +} fn main() { - let password_file = var("PASSWORD_FILE").expect("Provide PASSWORD_FILE environment variable."); - let password = read_to_string(&password_file) - .expect(&format!("Failed to read password from {password_file}")); + let config = Config::read(); + let mut bot = Bot::new(&config.username, &config.password, config.admin_list) + .expect("Failed to create bot"); - let mut admin_list = vec![Uuid::parse_str(CRABO_UUID).unwrap()]; - - write("admin_list.txt", format!("{admin_list:?}")) - .expect("Failed to write initial admin list."); - - let mut client = connect_to_veloren(password); - let mut clock = Clock::new(Duration::from_secs_f64(1.0)); - - select_character(&mut client, &mut clock); + bot.select_character().expect("Failed to select character"); loop { - let events = client - .tick(ControllerInputs::default(), clock.dt()) - .expect("Failed to run client."); - - for event in events { - if let Event::Chat(message) = event { - match message.chat_type { - ChatType::Tell(sender, _) | ChatType::Group(sender, _) => { - let sender_uuid = client.player_list().get(&sender).unwrap().uuid; - - handle_message( - &mut client, - &mut admin_list, - message.into_content().as_plain().unwrap_or(""), - sender_uuid, - ); - } - _ => {} - } - } - } - - client.cleanup(); - clock.tick(); - } -} - -fn connect_to_veloren(password: String) -> Client { - let runtime = Arc::new(Runtime::new().unwrap()); - let runtime2 = Arc::clone(&runtime); - - runtime - .block_on(Client::new( - ConnectionArgs::Tcp { - hostname: "server.veloren.net".to_string(), - prefer_ipv6: false, - }, - runtime2, - &mut None, - "crabobot", - &password, - None, - |provider| provider == "https://auth.veloren.net", - &|_| {}, - |_| {}, - Default::default(), - )) - .expect("Failed to create client instance.") -} - -fn select_character(client: &mut Client, clock: &mut Clock) { - client.load_character_list(); - - while client.character_list().loading { - println!("Loading characters..."); - - client - .tick(ControllerInputs::default(), clock.dt()) - .expect("Failed to run client."); - clock.tick(); - } - - let character_id = client - .character_list() - .characters - .first() - .unwrap() - .character - .id - .unwrap(); - - client.request_character( - character_id, - ViewDistances { - terrain: 0, - entity: 0, - }, - ); -} - -fn handle_message( - mut client: &mut Client, - mut admin_list: &mut Vec, - content: &str, - sender: Uuid, -) { - let mut words = content.split_whitespace(); - - if let Some(command) = words.next() { - match command { - "admin" => { - if admin_list.contains(&sender) { - adminify_players(&mut client, &mut admin_list, words) - } - } - "inv" => invite_players(&mut client, words), - "kick" => { - if admin_list.contains(&sender) { - kick_players(&mut client, words) - } - } - _ => {} - } - } -} - -fn adminify_players<'a, T: Iterator>( - client: &mut Client, - admin_list: &mut Vec, - names: T, -) { - for name in names { - let find_id = client.player_list().iter().find_map(|(_, info)| { - if info.player_alias == name { - Some(info.uuid) - } else { - None - } - }); - - if let Some(player_id) = find_id { - admin_list.push(player_id); - } - } -} - -fn invite_players<'a, T: Iterator>(client: &mut Client, names: T) { - for name in names { - let find_id = client.player_list().iter().find_map(|(id, info)| { - if info.player_alias == name { - Some(id) - } else { - None - } - }); - - if let Some(player_id) = find_id { - client.send_invite(player_id.clone(), InviteKind::Group); - } - } -} - -fn kick_players<'a, T: Iterator>(client: &mut Client, names: T) { - for name in names { - let find_id = client.player_list().iter().find_map(|(id, info)| { - if info.player_alias == name { - Some(id) - } else { - None - } - }); - - if let Some(player_id) = find_id { - client.kick_from_group(player_id.clone()); - } + bot.tick().expect("Failed to run bot") } }