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"