diff --git a/Cargo.lock b/Cargo.lock index cc64be4..291f4bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,9 +28,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.20" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -144,9 +144,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.39" +version = "1.2.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f" +checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb" dependencies = [ "find-msvc-tools", "shlex", @@ -324,15 +324,15 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" +checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" [[package]] name = "flate2" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" dependencies = [ "crc32fast", "miniz_oxide", @@ -816,11 +816,10 @@ checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] @@ -849,6 +848,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -983,9 +983,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -993,15 +993,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link 0.2.0", ] [[package]] @@ -1097,9 +1097,9 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags", ] @@ -1207,9 +1207,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.6" +version = "0.103.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" dependencies = [ "ring", "rustls-pki-types", @@ -1354,6 +1354,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "slab" version = "0.4.11" @@ -2283,9 +2289,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" diff --git a/Cargo.toml b/Cargo.toml index 1ac6339..c5cd80a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ uuid = { version = "^1.0", features = ["v4"] } chrono = { version = "^0.4", features = ["serde"] } colored = "^3.0" indicatif = "^0.18" -windows-sys = { version = "0.61.0", features = ["Win32_Foundation", "Win32_System_Console"] } +windows-sys = { version = "0.61.1", features = ["Win32_Foundation", "Win32_System_Console"] } anyhow = "^1.0" toml = "^0.9" url = "^2.4" diff --git a/config.toml b/config.toml index 4730014..2996fc3 100644 --- a/config.toml +++ b/config.toml @@ -12,3 +12,4 @@ default_interval = 3 # Display settings show_headers_by_default = false show_full_body_by_default = false +body_preview_length = 80 diff --git a/src/client.rs b/src/client.rs index 3328a0a..58ae745 100644 --- a/src/client.rs +++ b/src/client.rs @@ -24,7 +24,7 @@ impl WebhookClient { } pub async fn get_requests(&self, token: &str, count: u32) -> Result> { - let url = format!("{}/{}/log/{}", self.base_url, token, count); + let url = Config::join_url_segments(&self.base_url, &[token, "log", &count.to_string()]); let response = self .client diff --git a/src/commands.rs b/src/commands.rs index 449ec8f..a10af98 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -8,13 +8,12 @@ use uuid::Uuid; use crate::client::WebhookClient; use crate::config::Config; use crate::display::{ - print_full_request_body, print_request_body, print_request_details, print_request_headers, - print_request_summary, + print_full_request_body, print_request_details, print_request_headers, print_request_summary, }; pub async fn generate_token(config: &Config) -> Result<()> { let token = Uuid::new_v4(); - let webhook_url = format!("{}/{}", config.get_base_url(), token); + let webhook_url = Config::join_url_segments(config.get_base_url(), &[&token.to_string()]); println!("{}", "New webhook token generated!".bright_green().bold()); println!(); @@ -37,8 +36,10 @@ pub async fn generate_token(config: &Config) -> Result<()> { Ok(()) } +#[allow(clippy::too_many_arguments)] pub async fn monitor_requests( client: &WebhookClient, + config: &Config, token: &str, initial_count: u32, interval: u64, @@ -67,7 +68,7 @@ pub async fn monitor_requests( .into_iter() .filter(|req| { method_filter.is_none_or(|method| { - req.message_object.method.to_lowercase() == method.to_lowercase() + req.message_object.method.eq_ignore_ascii_case(method) }) }) .collect(); @@ -85,8 +86,13 @@ pub async fn monitor_requests( "Found".bright_blue(), filtered_requests.len() ); - for request in &filtered_requests { - print_request_summary(request); + // Reverse the order so latest requests appear at the end + for request in filtered_requests.iter().rev() { + print_request_summary( + request, + !full_body, + config.get_body_preview_length(), + ); // Don't show body preview in full body mode if show_headers { print_request_headers(request); } @@ -106,14 +112,16 @@ pub async fn monitor_requests( .collect(); for request in &new_requests { println!("{}", "NEW REQUEST".bright_green().bold()); - print_request_summary(request); + print_request_summary( + request, + !full_body, + config.get_body_preview_length(), + ); // Don't show body preview in full body mode if show_headers { print_request_headers(request); } if full_body { print_full_request_body(request); - } else { - print_request_body(request); } println!("{}", "─".repeat(80).bright_black()); last_seen_ids.insert(request.id.clone()); @@ -131,6 +139,7 @@ pub async fn monitor_requests( pub async fn show_logs( client: &WebhookClient, + config: &Config, token: &str, count: u32, method_filter: Option<&str>, @@ -150,9 +159,8 @@ pub async fn show_logs( let filtered_requests: Vec<_> = requests .into_iter() .filter(|req| { - method_filter.is_none_or(|method| { - req.message_object.method.to_lowercase() == method.to_lowercase() - }) + method_filter + .is_none_or(|method| req.message_object.method.eq_ignore_ascii_case(method)) }) .collect(); @@ -176,8 +184,9 @@ pub async fn show_logs( } println!("{}", "─".repeat(80).bright_black()); - for request in &filtered_requests { - print_request_summary(request); + // Reverse the order so latest requests appear at the end + for request in filtered_requests.iter().rev() { + print_request_summary(request, !full_body, config.get_body_preview_length()); // Don't show body preview in full body mode if show_headers { print_request_headers(request); } diff --git a/src/config.rs b/src/config.rs index 86db83f..3298e28 100644 --- a/src/config.rs +++ b/src/config.rs @@ -15,6 +15,14 @@ pub struct WebhookConfig { pub default_interval: u64, pub show_headers_by_default: bool, pub show_full_body_by_default: bool, + #[serde(default = "WebhookConfig::default_body_preview_length")] + pub body_preview_length: usize, +} + +impl WebhookConfig { + fn default_body_preview_length() -> usize { + 80 + } } impl Config { @@ -42,6 +50,7 @@ impl Config { default_interval: 3, show_headers_by_default: false, show_full_body_by_default: false, + body_preview_length: WebhookConfig::default_body_preview_length(), }, }; @@ -53,7 +62,31 @@ impl Config { Ok(default_config) } + /// Normalize a base URL by removing trailing slash + fn normalize_base_url(url: &str) -> &str { + url.trim_end_matches('/') + } + + /// Join URL segments properly without creating double slashes + pub fn join_url_segments(base: &str, segments: &[&str]) -> String { + let normalized_base = Self::normalize_base_url(base); + let mut url = normalized_base.to_string(); + + for segment in segments { + if !segment.is_empty() { + url.push('/'); + url.push_str(segment); + } + } + + url + } + pub fn get_base_url(&self) -> &str { &self.webhook.base_url } + + pub fn get_body_preview_length(&self) -> usize { + self.webhook.body_preview_length + } } diff --git a/src/display.rs b/src/display.rs index 4776e56..372fb00 100644 --- a/src/display.rs +++ b/src/display.rs @@ -1,4 +1,4 @@ -use chrono::DateTime; +use chrono::{DateTime, Local}; use colored::Colorize; use syntect::easy::HighlightLines; use syntect::highlighting::ThemeSet; @@ -7,35 +7,31 @@ use syntect::util::{LinesWithEndings, as_24_bit_terminal_escaped}; use crate::models::WebhookRequest; -pub fn print_request_summary(request: &WebhookRequest) { +pub fn print_request_summary( + request: &WebhookRequest, + show_body_preview: bool, + body_preview_length: usize, +) { let time = format_date(&request.date); let method = format_method(&request.message_object.method); let path = extract_path(&request.message_object.value, &request.token_id); - println!( - "{} {} {} {} {}", - time.bright_black(), - method, - path.bright_white(), - format!("({})", request.id).bright_black(), - get_body_preview(&request.message_object.body).bright_yellow() - ); -} - -pub fn print_request_body(request: &WebhookRequest) { - if let Some(body) = &request.message_object.body - && !body.trim().is_empty() - { - let display_body = if body.chars().count() > 200 { - format!("{}...", body.chars().take(200).collect::()) - } else { - body.clone() - }; - + if show_body_preview { println!( - "{}: {}", - "Body".bright_blue().bold(), - display_body.bright_white() + "{} {} {} {} {}", + time.bright_black(), + method, + path.bright_white(), + format!("({})", request.id).bright_black(), + get_body_preview(&request.message_object.body, body_preview_length).bright_white() + ); + } else { + println!( + "{} {} {} {}", + time.bright_black(), + method, + path.bright_white(), + format!("({})", request.id).bright_black() ); } } @@ -201,7 +197,7 @@ pub fn format_form_data(data: &str) -> String { pub fn format_method(method: &str) -> colored::ColoredString { match method.to_uppercase().as_str() { "GET" => method.green().bold(), - "POST" => method.blue().bold(), + "POST" => method.bright_blue().bold(), "PUT" => method.yellow().bold(), "DELETE" => method.red().bold(), "PATCH" => method.magenta().bold(), @@ -211,7 +207,11 @@ pub fn format_method(method: &str) -> colored::ColoredString { pub fn format_date(date_str: &str) -> String { match DateTime::parse_from_rfc3339(date_str) { - Ok(dt) => dt.format("%H:%M:%S").to_string(), + Ok(dt) => { + let utc_time = dt.format("%H:%M:%S UTC").to_string(); + let local_time = dt.with_timezone(&Local).format("%H:%M:%S").to_string(); + format!("{} ({})", local_time, utc_time) + } Err(_) => date_str.to_string(), } } @@ -229,12 +229,12 @@ pub fn extract_path(full_path: &str, token: &str) -> String { } } -pub fn get_body_preview(body: &Option) -> String { +pub fn get_body_preview(body: &Option, max_length: usize) -> String { match body { Some(b) if !b.trim().is_empty() => { let trimmed = b.trim(); - let mut preview: String = trimmed.chars().take(50).collect(); - if trimmed.chars().count() > 50 { + let mut preview: String = trimmed.chars().take(max_length).collect(); + if trimmed.chars().count() > max_length { preview.push('…'); } format!("[BODY] {}", preview) diff --git a/src/main.rs b/src/main.rs index c1ecf90..b3ccd88 100644 --- a/src/main.rs +++ b/src/main.rs @@ -67,6 +67,7 @@ async fn main() -> Result<()> { monitor_requests( &client, + &config, &token, count, interval, @@ -85,6 +86,7 @@ async fn main() -> Result<()> { } => { show_logs( &client, + &config, &token, count, method.as_deref(),