diff --git a/Cargo.lock b/Cargo.lock index 9275baef..2a70586d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,6 +40,18 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -49,6 +61,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -237,6 +255,39 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "cached" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4d73155ae6b28cf5de4cfc29aeb02b8a1c6dab883cb015d15cd514e42766846" +dependencies = [ + "ahash", + "cached_proc_macro", + "cached_proc_macro_types", + "hashbrown 0.14.5", + "once_cell", + "thiserror", + "web-time", +] + +[[package]] +name = "cached_proc_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f42a145ed2d10dce2191e1dcf30cfccfea9026660e143662ba5eec4017d5daa" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cached_proc_macro_types" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" + [[package]] name = "cargo-husky" version = "1.5.0" @@ -816,6 +867,16 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + [[package]] name = "hashbrown" version = "0.15.0" @@ -1466,6 +1527,7 @@ name = "odin" version = "2.1.0" dependencies = [ "a2s", + "cached", "cargo-husky", "cc", "chrono", @@ -2662,6 +2724,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki-roots" version = "0.26.6" diff --git a/src/odin/Cargo.toml b/src/odin/Cargo.toml index 3e9dce30..d6f35228 100644 --- a/src/odin/Cargo.toml +++ b/src/odin/Cargo.toml @@ -53,6 +53,7 @@ regex = "1.10.4" tokio = { version = "1", features = ["full"] } notify = "6.1.1" json-patch = "*" +cached = "0" [dev-dependencies] once_cell = "1.19.0" diff --git a/src/odin/commands/logs.rs b/src/odin/commands/logs.rs index 5c6c843d..b6f9af99 100644 --- a/src/odin/commands/logs.rs +++ b/src/odin/commands/logs.rs @@ -1,6 +1,8 @@ use crate::notifications::enums::notification_event::NotificationEvent; +use crate::notifications::enums::player::PlayerStatus::{Joined, Left}; use crate::utils::common_paths::log_directory; use log::{error, info, warn}; +use crate::utils::environment::is_env_var_truthy; use std::collections::HashMap; use std::ffi::OsStr; use std::fs::{read_to_string, File}; @@ -25,6 +27,18 @@ fn handle_line(path: PathBuf, line: String) { return; } + if is_env_var_truthy("PLAYER_EVENT_NOTIFICATIONS") { + if line.contains("I HAVE ARRIVED!") { + + + NotificationEvent::Player(Joined).send_notification(None); + } + + if line.contains("Player disconnected") { + NotificationEvent::Player(Left).send_notification(None); + } + } + let file_name = Path::new(&path).file_name().unwrap().to_str().unwrap(); if line.contains("WARNING") { @@ -37,12 +51,12 @@ fn handle_line(path: PathBuf, line: String) { if line.contains("Game server connected") { NotificationEvent::Start(crate::notifications::enums::event_status::EventStatus::Successful) - .send_notification(); + .send_notification(None); } if line.contains("Steam manager on destroy") { NotificationEvent::Stop(crate::notifications::enums::event_status::EventStatus::Successful) - .send_notification(); + .send_notification(None); info!("The game server has been stopped"); } } diff --git a/src/odin/commands/start.rs b/src/odin/commands/start.rs index 9ffb0d5a..395bb871 100644 --- a/src/odin/commands/start.rs +++ b/src/odin/commands/start.rs @@ -9,7 +9,7 @@ use std::process::exit; pub fn invoke(dry_run: bool) { info!(target: "commands_start", "Setting up start scripts..."); - NotificationEvent::Start(EventStatus::Running).send_notification(); + NotificationEvent::Start(EventStatus::Running).send_notification(None); debug!(target: "commands_start", "Loading config file..."); let config = load_config(); debug!(target: "commands_start", "Dry run condition: {}", dry_run); diff --git a/src/odin/commands/stop.rs b/src/odin/commands/stop.rs index 4431b4c4..1bd0e923 100644 --- a/src/odin/commands/stop.rs +++ b/src/odin/commands/stop.rs @@ -7,7 +7,7 @@ use crate::notifications::enums::notification_event::NotificationEvent; use crate::{constants, server, utils::get_working_dir}; pub fn invoke(dry_run: bool) { - NotificationEvent::Stop(EventStatus::Running).send_notification(); + NotificationEvent::Stop(EventStatus::Running).send_notification(None); debug!("Stopping server, directory needs to be where the server executable is located."); info!( "Stopping server, using working directory {}", @@ -23,5 +23,5 @@ pub fn invoke(dry_run: bool) { } server::blocking_shutdown(); } - NotificationEvent::Stop(EventStatus::Successful).send_notification(); + NotificationEvent::Stop(EventStatus::Successful).send_notification(None); } diff --git a/src/odin/files/discord.rs b/src/odin/files/discord.rs index 875e3411..b1044064 100644 --- a/src/odin/files/discord.rs +++ b/src/odin/files/discord.rs @@ -1,6 +1,6 @@ use crate::{ files::{FileManager, ManagedFile}, - notifications::discord::{DiscordWebHookBody}, + notifications::discord::DiscordWebHookBody, utils::{environment::fetch_var, path_exists}, }; @@ -80,7 +80,7 @@ pub fn read_discord(discord: &dyn FileManager) -> DiscordConfig { pub fn write_discord(discord: &dyn FileManager) -> bool { if path_exists(&discord.path()) { debug!("Discord config file already exists, doing nothing."); - return true; + return true; } let template_notification = basic_template(); @@ -205,7 +205,9 @@ mod tests { fn test_read_discord_without_events_key() { let mut mock_file = MockManagedFile::new(); let no_events_content = r#"{}"#; - mock_file.expect_read().return_const(String::from(no_events_content)); + mock_file + .expect_read() + .return_const(String::from(no_events_content)); let config = read_discord(&mock_file); @@ -213,7 +215,6 @@ mod tests { assert!(config.events.contains_key("start")); } - #[test] fn test_read_discord_with_extra_event_keys() { let mut mock_file = MockManagedFile::new(); @@ -231,29 +232,36 @@ mod tests { } } }"#; - mock_file.expect_read().return_const(String::from(extra_keys_content)); + mock_file + .expect_read() + .return_const(String::from(extra_keys_content)); let config = read_discord(&mock_file); - assert!(!config.events.contains_key("extra_event"), "Unexpected key should be ignored"); + assert!( + !config.events.contains_key("extra_event"), + "Unexpected key should be ignored" + ); assert!(config.events.contains_key("broadcast")); assert!(config.events.contains_key("start")); } - - #[test] fn test_read_discord_with_malformed_json() { let mut mock_file = MockManagedFile::new(); let malformed_json = r#"{ "events": { "broadcast": { "content": "test_broadcast""#; - mock_file.expect_read().return_const(String::from(malformed_json)); + mock_file + .expect_read() + .return_const(String::from(malformed_json)); let result = std::panic::catch_unwind(|| read_discord(&mock_file)); - assert!(result.is_err(), "Expected a panic when the JSON is malformed"); + assert!( + result.is_err(), + "Expected a panic when the JSON is malformed" + ); } - #[test] fn test_discord_file() { let managed_file = discord_file(); diff --git a/src/odin/log_filters/mod.rs b/src/odin/log_filters/mod.rs new file mode 100644 index 00000000..4a77162b --- /dev/null +++ b/src/odin/log_filters/mod.rs @@ -0,0 +1,4 @@ +use regex::Regex; + +mod player; + diff --git a/src/odin/log_filters/player.rs b/src/odin/log_filters/player.rs new file mode 100644 index 00000000..71d63527 --- /dev/null +++ b/src/odin/log_filters/player.rs @@ -0,0 +1,30 @@ +use regex::Regex; +use crate::notifications::enums::notification_event::NotificationEvent; +use crate::notifications::enums::player::PlayerStatus::Joined; + +struct Player { + +} + +struct PlayerList { + players: Vec<> +} + + + +pub fn player_joined(message: &str) { + let re = Regex::new(r"(.*?)").unwrap(); + if let Some(captures) = re.captures(message) { + let name = captures.get(1).map_or("", |m| m.as_str()); + NotificationEvent::Player(Joined).send_notification(Some(format!("Player {name} has joined the server!"))); + } +} + +pub fn player_left(message: &str) { + let re = Regex::new(r"(.*?)").unwrap(); + + if let Some(captures) = re.captures(message) { + let name = captures.get(1).map_or("", |m| m.as_str()); + NotificationEvent::Player(Left).send_notification(Some(format!("Player {name} has left the server!"))); + } +} \ No newline at end of file diff --git a/src/odin/main.rs b/src/odin/main.rs index ba4ef64a..42d43dbd 100644 --- a/src/odin/main.rs +++ b/src/odin/main.rs @@ -24,6 +24,7 @@ pub mod server; mod steamcmd; pub mod traits; pub mod utils; +mod log_filters; #[tokio::main] async fn main() { diff --git a/src/odin/notifications/enums/notification_event.rs b/src/odin/notifications/enums/notification_event.rs index 511e4717..3df6b94a 100644 --- a/src/odin/notifications/enums/notification_event.rs +++ b/src/odin/notifications/enums/notification_event.rs @@ -131,12 +131,17 @@ impl NotificationEvent { }; self.handle_request(req); } - pub fn send_notification(&self) { + pub fn send_notification(&self, message: Option) { debug!("Checking for notification information..."); if is_webhook_enabled() { debug!("Webhook found! Starting notification process..."); - let event = self.create_notification_message(); + let mut event = self.create_notification_message(); let enabled_var = format!("WEBHOOK_STATUS_{}", event.event_type.status).to_uppercase(); + + if let Some(msg) = message { + event.event_message = msg; + } + debug!("Checking ENV Var: {}", &enabled_var); if fetch_var(&enabled_var, "0").eq("1") { self.send_custom_notification(&fetch_webhook_url(), &event); @@ -147,6 +152,7 @@ impl NotificationEvent { debug!("Skipping notification, no webhook supplied!"); } } + pub(crate) fn to_event_type(&self) -> EventType { let event = self.to_string(); let parsed_event: Vec<&str> = event.split(' ').collect(); @@ -166,23 +172,23 @@ impl fmt::Display for NotificationEvent { impl std::str::FromStr for NotificationEvent { type Err = VariantNotFound; - fn from_str(s: &str) -> core::result::Result { + fn from_str(s: &str) -> Result { use NotificationEvent::{Broadcast, Player, Start, Stop, Update}; let parts: Vec<&str> = s.split(' ').collect(); let event = parts[0]; if event.eq(Broadcast.to_string().as_str()) { - ::std::result::Result::Ok(Broadcast) + Ok(Broadcast) } else if event.eq("Player") { - let player_status = PlayerStatus::from_str(parts[1]).unwrap(); - ::std::result::Result::Ok(Player(player_status)) + let player_status = PlayerStatus::from_str(parts[1])?; + Ok(Player(player_status)) } else { let status = parts[1]; - let event_status = EventStatus::from_str(status).unwrap(); + let event_status = EventStatus::from_str(status)?; match event { - "Update" => ::std::result::Result::Ok(Update(event_status)), - "Start" => ::std::result::Result::Ok(Start(event_status)), - "Stop" => ::std::result::Result::Ok(Stop(event_status)), - _ => ::std::result::Result::Err(VariantNotFound { + "Update" => Ok(Update(event_status)), + "Start" => Ok(Start(event_status)), + "Stop" => Ok(Stop(event_status)), + _ => Err(VariantNotFound { v: String::from("Failed to find Notification Event"), }), } @@ -225,28 +231,28 @@ mod webhook_tests { #[serial] fn is_webhook_enabled_found_var_valid_url() { set_var("WEBHOOK_URL", "http://127.0.0.1:3000/dummy-url"); - assert_eq!(is_webhook_enabled(), true); + assert!(is_webhook_enabled()); } #[test] #[serial] fn is_webhook_enabled_found_var_invalid_url() { set_var("WEBHOOK_URL", "LOCALHOST"); - assert_eq!(is_webhook_enabled(), false); + assert!(!is_webhook_enabled()); } #[test] #[serial] fn is_webhook_enabled_not_found_var() { remove_var("WEBHOOK_URL"); - assert_eq!(is_webhook_enabled(), false); + assert!(!is_webhook_enabled()); } #[test] #[serial] fn is_webhook_enabled_empty_var() { set_var("WEBHOOK_URL", ""); - assert_eq!(is_webhook_enabled(), false); + assert!(!is_webhook_enabled()); } } diff --git a/src/odin/server/startup.rs b/src/odin/server/startup.rs index fee6db78..087e4868 100644 --- a/src/odin/server/startup.rs +++ b/src/odin/server/startup.rs @@ -44,7 +44,7 @@ pub fn start_daemonized(config: ValheimArguments) -> Result String { match env::var(name) { Ok(value) => { - debug!("Env var found '{}': '{}'", name, value); if value.is_empty() { String::from(default) } else { @@ -12,7 +12,6 @@ pub fn fetch_var(name: &str, default: &str) -> String { } } Err(_) => { - debug!("Env var default '{}': '{}'", name, default); String::from(default) } } @@ -27,6 +26,11 @@ pub fn fetch_multiple_var(name: &str, default: &str) -> String { } } +#[cached] +pub fn is_env_var_truthy(name: &'static str) -> bool { + parse_truthy(&fetch_var(name, "0")).unwrap_or(false) +} + #[cfg(test)] mod fetch_env_tests { use crate::utils::environment::{fetch_multiple_var, fetch_var}; diff --git a/src/scripts/entrypoint.sh b/src/scripts/entrypoint.sh index 5dae7b49..114e4d71 100644 --- a/src/scripts/entrypoint.sh +++ b/src/scripts/entrypoint.sh @@ -97,6 +97,7 @@ setup_cron_env() { "BEPINEX_RELEASES_URL" "BEPINEX_DOWNLOAD_URL" "BEPINEX_FULL_RELEASES_URL" + "PLAYER_EVENT_NOTIFICATIONS" "BETA_BRANCH" "BETA_BRANCH_PASSWORD" "HTTP_PORT"