diff --git a/.gitignore b/.gitignore index 992aa32..b12cf19 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ *.pem *.lock -target/ \ No newline at end of file +target/ +*.stats* +.vscode \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 138afab..6ee2d6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pulse" -version = "0.0.1" +version = "0.0.2" authors = ["Jerboa"] edition="2021" diff --git a/src/lib.rs b/src/lib.rs index 31d49f6..f6d16a8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ pub mod web; pub mod server; - +pub mod stats; pub mod util; #[cfg(feature = "http")] @@ -8,6 +8,13 @@ pub mod server_http; const DEBUG: bool = true; +/// Completely drop Github POST requests concerning private repos +pub const IGNORE_PRIVATE_REPOS: bool = true; + +/// Process Github POST requests concerning private repos +/// but never send outbound trafic (e.g. Discord) +pub const DONT_MESSAGE_ON_PRIVATE_REPOS: bool = true; + pub fn debug(msg: String, context: Option) { if DEBUG == false { return } diff --git a/src/main.rs b/src/main.rs index f854b67..7f7097b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ -use pulse::{server::Server, web::request::discord::model::Webhook}; +use pulse::{server::Server, web::discord::request::model::Webhook, stats}; +use tokio::task::spawn; #[tokio::main] async fn main() { @@ -97,6 +98,7 @@ async fn main() { "./key.pem".to_string() }; + let _stats_watcher = spawn(stats::io::watch(Webhook::new(disc_url.clone()))); let server = Server::new(0,0,0,0, port,token, Webhook::new(disc_url)); diff --git a/src/main_http.rs b/src/main_http.rs index 538d72e..c678637 100644 --- a/src/main_http.rs +++ b/src/main_http.rs @@ -1,6 +1,7 @@ #[cfg(feature = "http")] -use pulse::{server_http::ServerHttp, web::request::discord::model::Webhook}; +use pulse::{server_http::ServerHttp, web::discord::request::model::Webhook, stats}; +use tokio::task::spawn; #[cfg(feature = "http")] #[tokio::main] @@ -66,6 +67,7 @@ async fn main() { 3030 }; + let _stats_watcher = spawn(stats::io::watch(Webhook::new(disc_url.clone()))); let server = ServerHttp::new(0,0,0,0, port,token, Webhook::new(disc_url)); diff --git a/src/server.rs b/src/server.rs index 07317a9..c81ba1d 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,13 +1,16 @@ use crate::web:: { throttle::{IpThrottler, handle_throttle}, - response::github::{github_filter::filter_github, model::GithubConfig}, - request::discord::model::Webhook + github::{response::github_filter::filter_github, model::{GithubConfig, GithubStats}}, + discord::request::model::Webhook }; +use crate::stats; + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::PathBuf; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; +use tokio::sync::Mutex; use axum:: { @@ -33,7 +36,7 @@ impl Server d: u8, port: u16, token: String, - disc: Webhook + disc: Webhook, ) -> Server { @@ -46,7 +49,7 @@ impl Server let throttle_state = Arc::new(Mutex::new(requests)); - let github = GithubConfig::new(token, disc); + let github = Arc::new(Mutex::new(GithubConfig::new(token, disc.clone(), GithubStats::new()))); Server { @@ -55,7 +58,6 @@ impl Server .route("/", post(|| async move { })) .layer(middleware::from_fn_with_state(github, filter_github)) .layer(middleware::from_fn_with_state(throttle_state.clone(), handle_throttle)) - } } diff --git a/src/server_http.rs b/src/server_http.rs index 6879db7..dd17901 100644 --- a/src/server_http.rs +++ b/src/server_http.rs @@ -1,17 +1,16 @@ - use crate::web:: { throttle::{IpThrottler, handle_throttle}, - response::github::{github_filter::filter_github, model::GithubConfig}, - request::discord::model::Webhook + github::{response::github_filter::filter_github, model::{GithubConfig, GithubStats}}, + discord::request::model::Webhook }; +use crate::stats; -use std::clone; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; +use tokio::sync::Mutex; -use axum::extract::State; use axum:: { routing::post, @@ -48,8 +47,8 @@ impl ServerHttp let throttle_state = Arc::new(Mutex::new(requests)); - let github = GithubConfig::new(token, disc); - + let github = Arc::new(Mutex::new(GithubConfig::new(token, disc.clone(), GithubStats::new()))); + ServerHttp { addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(a,b,c,d)), port), @@ -57,7 +56,6 @@ impl ServerHttp .route("/", post(|| async move { })) .layer(middleware::from_fn_with_state(github, filter_github)) .layer(middleware::from_fn_with_state(throttle_state.clone(), handle_throttle)) - } } diff --git a/src/stats/io.rs b/src/stats/io.rs new file mode 100644 index 0000000..5c21916 --- /dev/null +++ b/src/stats/io.rs @@ -0,0 +1,156 @@ +use std::cmp::min; +use std::path::Path; +use std::{sync::Arc, collections::HashMap}; + +use chrono::{Local, Datelike, Timelike, DateTime}; +use tokio::sync::Mutex; + +use crate::DONT_MESSAGE_ON_PRIVATE_REPOS; +use crate::util::{write_file, read_file_utf8}; +use crate::web::discord::request::model::Webhook; +use crate::web::discord::request::post::post; +use crate::web::github::model::{GithubStats, GithubRepoStats, GithubConfig}; + +use std::time::{Duration, SystemTime}; +use std::thread::sleep; + +const STATS_PATH: &str = "repo.stats"; + +pub async fn collect(stats: Arc>, data: HashMap) +{ + + let mut held_stats = stats.lock().await; + + let mut name = match data["repository"]["name"].as_str() + { + Some(s) => s.to_string(), + None => return + }; + + let push = if data.contains_key("pusher") + { + 1 + } + else + { + 0 + }; + + if data["repository"]["private"].as_bool().is_some_and(|x|x) + { + name = format!("{}_private", name); + } + + let stars = data["repository"]["stargazers_count"].as_u64().unwrap(); + + let new_stats = GithubRepoStats {stars: stars, pushes: push}; + + if !held_stats.get_stats().repos.contains_key(&name) + { + held_stats.get_stats().repos.insert(name.to_string(), GithubRepoStats::new()); + } + + held_stats.get_stats().repos.get_mut(&name).unwrap().update(new_stats); + + + if Path::new(STATS_PATH).exists() + { + match std::fs::copy(STATS_PATH, format!("{}.bk",STATS_PATH)) + { + Ok(_) => {}, + Err(why) => + { + crate::debug(format!("error {} copying stats to {}.bk", why, STATS_PATH), None); + return + } + } + } + + match serde_json::to_string_pretty(held_stats.get_stats()) + { + Ok(se) => + { + write_file(STATS_PATH, se.as_bytes()) + }, + Err(why) => + { + crate::debug(format!("error {} writing stats to {}", why, STATS_PATH), None); + return + } + } + + crate::debug(format!("wrote data"), None); +} + +pub async fn watch(disc: Webhook) +{ + let mut last_message = SystemTime::UNIX_EPOCH; + loop + { + let date = Local::now(); + + if date.weekday() == chrono::Weekday::Fri && last_message.elapsed().unwrap().as_secs() > 24*60*60 + { + last_message = SystemTime::now(); + + let data = match read_file_utf8(STATS_PATH) + { + Some(d) => d, + None => + { + crate::debug(format!("error reading stats at {}", STATS_PATH), None); + break + } + }; + + let stats: GithubStats = match serde_json::from_str(&data) + { + Ok(data) => {data}, + Err(why) => + { + crate::debug(format!("error {} reading stats at {}", why, STATS_PATH), None); + break + } + }; + + let mut pushes: Vec<(u64, u64, String)> = Vec::new(); + + for repo in stats.repos.into_iter() + { + if repo.0.contains("private") && DONT_MESSAGE_ON_PRIVATE_REPOS + { + continue; + } + + pushes.push((repo.1.pushes, repo.1.stars, repo.0)); + } + + if pushes.len() == 0 + { + break; + } + + pushes.sort_by(| a:&(u64, u64, String), b:&(u64, u64, String) | b.0.partial_cmp(&a.0).unwrap()); + + if pushes[0].0 == 0 + { + continue; + } + + let mut msg = "Top activity this week :bar_chart:\n".to_string(); + + for i in 0..min(pushes.len(), 3) + { + msg.push_str(format!(" {} | {} pushes | {} stars\n", pushes[i].2, pushes[i].0, pushes[i].1).as_str()); + } + + match post(disc.clone(), msg).await + { + Ok(_) => {}, + Err(e) => {crate::debug(format!("error posting message to discord {}", e), Some("stats watch".to_string()))} + } + } + + sleep(Duration::from_secs(60*60)); + } +} \ No newline at end of file diff --git a/src/stats/mod.rs b/src/stats/mod.rs new file mode 100644 index 0000000..678b90e --- /dev/null +++ b/src/stats/mod.rs @@ -0,0 +1 @@ +pub mod io; \ No newline at end of file diff --git a/src/util.rs b/src/util.rs index 5544ba5..3bce52a 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,4 +1,4 @@ -use std::fmt::Write; +use std::{fmt::Write, fs::File, io::{Write as ioWrite, Read}}; use regex::Regex; pub fn dump_bytes(v: &[u8]) -> String @@ -25,4 +25,32 @@ pub fn strip_control_characters(s: String) -> String { let re = Regex::new(r"[\u0000-\u001F]").unwrap().replace_all(&s, ""); return re.to_string() +} + +pub fn write_file(path: &str, data: &[u8]) +{ + let mut file = File::create(path).unwrap(); + file.write_all(data).unwrap(); +} + +pub fn read_file_utf8(path: &str) -> Option +{ + let mut file = match File::open(path) { + Err(why) => + { + crate::debug(format!("error reading file to utf8, {}", why), None); + return None + }, + Ok(file) => file, + }; + + let mut s = String::new(); + match file.read_to_string(&mut s) { + Err(why) => + { + crate::debug(format!("error reading file to utf8, {}", why), None); + return None + }, + Ok(_) => Some(s) + } } \ No newline at end of file diff --git a/src/web/discord/mod.rs b/src/web/discord/mod.rs new file mode 100644 index 0000000..2d73595 --- /dev/null +++ b/src/web/discord/mod.rs @@ -0,0 +1 @@ +pub mod request; \ No newline at end of file diff --git a/src/web/request/discord/mod.rs b/src/web/discord/request/mod.rs similarity index 100% rename from src/web/request/discord/mod.rs rename to src/web/discord/request/mod.rs diff --git a/src/web/request/discord/model.rs b/src/web/discord/request/model.rs similarity index 100% rename from src/web/request/discord/model.rs rename to src/web/discord/request/model.rs diff --git a/src/web/request/discord/post.rs b/src/web/discord/request/post.rs similarity index 87% rename from src/web/request/discord/post.rs rename to src/web/discord/request/post.rs index 9f69383..42ebe25 100644 --- a/src/web/request/discord/post.rs +++ b/src/web/discord/request/post.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; -use crate::web::request::discord::model::Webhook; +use crate::web::discord::request::model::Webhook; /// Send a simple plaintext string message, msg, to the webhook w /// @@ -15,7 +15,7 @@ use crate::web::request::discord::model::Webhook; /// # Example /// ```rust /// -/// use pulse::web::request::discord::{model::Webhook, post::post}; +/// use pulse::web::discord::request::{model::Webhook, post::post}; /// /// pub async fn post_to_discord(){ /// let w = Webhook::new("https://discord.com/api/webhooks/xxx/yyy".to_string()); @@ -37,6 +37,7 @@ use crate::web::request::discord::model::Webhook; pub async fn post(w: Webhook, msg: String) -> Result { + crate::debug(format!("Posting to Discord {:?}", msg), None); let client = reqwest::Client::new(); let mut map = HashMap::new(); diff --git a/src/web/github/mod.rs b/src/web/github/mod.rs new file mode 100644 index 0000000..abde836 --- /dev/null +++ b/src/web/github/mod.rs @@ -0,0 +1,2 @@ +pub mod response; +pub mod model; \ No newline at end of file diff --git a/src/web/github/model.rs b/src/web/github/model.rs new file mode 100644 index 0000000..a6c2894 --- /dev/null +++ b/src/web/github/model.rs @@ -0,0 +1,77 @@ +use std::collections::HashMap; + +use serde::{Serialize, Deserialize}; + +use crate::web::discord::request::model::Webhook; + +#[derive(Clone, Serialize, Deserialize)] +pub struct GithubRepoStats +{ + pub stars: u64, + pub pushes: u64, +} + +impl GithubRepoStats +{ + pub fn new() -> GithubRepoStats + { + GithubRepoStats {stars: 0, pushes: 0} + } + + pub fn update(&mut self, stats: GithubRepoStats) + { + self.stars = stats.stars; + self.pushes += stats.pushes; + } + + pub fn clear(&mut self) + { + self.pushes = 0; + self.stars = 0; + } +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct GithubStats +{ + pub repos: HashMap +} + +impl GithubStats +{ + pub fn new() -> GithubStats + { + GithubStats {repos: HashMap::new()} + } +} + +#[derive(Clone)] +pub struct GithubConfig +{ + token: String, + discord: Webhook, + stats: GithubStats +} + +impl GithubConfig +{ + pub fn new(t: String, w: Webhook, s: GithubStats) -> GithubConfig + { + GithubConfig {token: t, discord: w, stats: s} + } + + pub fn get_token(&self) -> String + { + String::from(self.token.clone()) + } + + pub fn get_webhook(&self) -> Webhook + { + self.discord.clone() + } + + pub fn get_stats(&mut self) -> &mut GithubStats + { + &mut self.stats + } +} \ No newline at end of file diff --git a/src/web/response/github/github_filter.rs b/src/web/github/response/github_filter.rs similarity index 84% rename from src/web/response/github/github_filter.rs rename to src/web/github/response/github_filter.rs index 0f2bb84..5405da9 100644 --- a/src/web/response/github/github_filter.rs +++ b/src/web/github/response/github_filter.rs @@ -18,21 +18,28 @@ use openssl::sign::Signer; use regex::Regex; use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::Mutex; use crate::util::{dump_bytes, read_bytes, strip_control_characters}; -use crate::web::response::github:: +use crate::web::github:: { - github_release::respond_release, - github_starred::respond_starred, - model:: + model::GithubConfig, + response:: { - GithubConfig, - GithubReleaseActionType, - GithubStarredActionType + github_release::respond_release, + github_starred::respond_starred, + github_pushed::respond_pushed, + model:: + { + GithubReleaseActionType, + GithubStarredActionType + } } }; + /// Middleware to detect, verify, and respond to a github POST request from a /// Github webhook /// @@ -50,7 +57,8 @@ use crate::web::response::github:: /// /// ```rust /// use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -/// use std::sync::{Arc, Mutex}; +/// use std::sync::Arc; +/// use tokio::sync::Mutex; /// /// use axum:: /// { @@ -59,13 +67,16 @@ use crate::web::response::github:: /// middleware /// }; /// -/// use pulse::web::request::discord::model::Webhook; -/// use pulse::web::response::github::{github_filter::filter_github, model::GithubConfig}; +/// use pulse::web:: +/// { +/// throttle::{IpThrottler, handle_throttle}, +/// github::{response::github_filter::filter_github, model::{GithubConfig, GithubStats}}, +/// discord::request::model::Webhook +/// }; /// /// pub async fn server() { /// -/// let github = GithubConfig::new("secret".to_string(), Webhook::new("url".to_string())); -/// +/// let github = Arc::new(Mutex::new(GithubConfig::new("token".to_string(), Webhook::new("url".to_string()), GithubStats::new()))); /// let app = Router::new() /// .route("/", post(|| async move { })) /// .layer(middleware::from_fn_with_state(github, filter_github)); @@ -82,7 +93,7 @@ use crate::web::response::github:: /// ```` pub async fn filter_github ( - State(app_state): State, + State(app_state): State>>, headers: HeaderMap, request: Request, next: Next @@ -137,7 +148,7 @@ where B: axum::body::HttpBody /// async fn github_verify ( - app_state: GithubConfig, + app_state: Arc>, headers: HeaderMap, body: Bytes ) -> StatusCode @@ -165,7 +176,7 @@ async fn github_verify let post_digest = Regex::new(r"sha256=").unwrap().replace_all(signature, "").into_owned().to_uppercase(); - let token = app_state.get_token().clone(); + let token = app_state.lock().await.get_token(); let key = match PKey::hmac(token.as_bytes()) { Ok(k) => k, @@ -232,7 +243,7 @@ async fn github_verify /// async fn github_respond ( - app_state: GithubConfig, + app_state: Arc>, body: Bytes ) -> StatusCode { @@ -249,6 +260,14 @@ async fn github_respond } }; + if crate::IGNORE_PRIVATE_REPOS && parsed_data.contains_key("repository") + { + if parsed_data["repository"]["private"].as_bool().is_some_and(|x|x) + { + return StatusCode::CONTINUE; + } + } + if parsed_data.contains_key("action") { if parsed_data.contains_key("release") @@ -263,7 +282,7 @@ async fn github_respond } }; - return respond_release(action, parsed_data, app_state.get_webhook()).await; + return respond_release(action, parsed_data, app_state).await; } else if parsed_data.contains_key("starred_at") { @@ -277,13 +296,17 @@ async fn github_respond } }; - return respond_starred(action, parsed_data, app_state.get_webhook()).await; + return respond_starred(action, parsed_data, app_state).await; } else { return StatusCode::OK; } } + else if parsed_data.contains_key("pusher") + { + return respond_pushed(parsed_data, app_state).await; + } else { crate::debug(format!("no action entry in JSON payload \n\nGot:\n {:?}", parsed_data), None); diff --git a/src/web/github/response/github_pushed.rs b/src/web/github/response/github_pushed.rs new file mode 100644 index 0000000..b742562 --- /dev/null +++ b/src/web/github/response/github_pushed.rs @@ -0,0 +1,24 @@ +use std::collections::HashMap; + +use std::sync::Arc; +use tokio::sync::Mutex; + +use reqwest::StatusCode; + +use crate::{web::github::model::GithubConfig, stats::io::collect}; + +pub async fn respond_pushed(data: HashMap, app_state: Arc>) -> StatusCode +{ + let name = match data["repository"]["name"].as_str() + { + Some(s) => s, + None => {return StatusCode::INTERNAL_SERVER_ERROR} + }; + + crate::debug(format!("got a push to {}", name), None); + + collect(app_state, data).await; + + StatusCode::OK + +} \ No newline at end of file diff --git a/src/web/response/github/github_release.rs b/src/web/github/response/github_release.rs similarity index 74% rename from src/web/response/github/github_release.rs rename to src/web/github/response/github_release.rs index bbe1a7c..5a4d645 100644 --- a/src/web/response/github/github_release.rs +++ b/src/web/github/response/github_release.rs @@ -1,12 +1,15 @@ use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::Mutex; + use reqwest::StatusCode; -use crate::web::request::discord::{model::Webhook, post::post}; +use crate::web::{discord::request::post::post, github::model::GithubConfig}; use super::model::GithubReleaseActionType; -pub async fn respond_release(action: GithubReleaseActionType, data: HashMap, disc: Webhook) -> StatusCode +pub async fn respond_release(action: GithubReleaseActionType, data: HashMap, app_state: Arc>) -> StatusCode { crate::debug(format!("Processing github release payload: {:?}", action), None); @@ -38,7 +41,12 @@ pub async fn respond_release(action: GithubReleaseActionType, data: HashMap StatusCode::OK, Err(e) => diff --git a/src/web/response/github/github_starred.rs b/src/web/github/response/github_starred.rs similarity index 70% rename from src/web/response/github/github_starred.rs rename to src/web/github/response/github_starred.rs index d2d3196..d7b1e70 100644 --- a/src/web/response/github/github_starred.rs +++ b/src/web/github/response/github_starred.rs @@ -1,12 +1,15 @@ use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::Mutex; + use reqwest::StatusCode; -use crate::web::request::discord::{model::Webhook, post::post}; +use crate::{web::{discord::request::post::post, github::model::GithubConfig}, stats::io::collect}; use super::model::GithubStarredActionType; -pub async fn respond_starred(action: GithubStarredActionType, data: HashMap, disc: Webhook) -> StatusCode +pub async fn respond_starred(action: GithubStarredActionType, data: HashMap, app_state: Arc>) -> StatusCode { crate::debug(format!("Processing github starred payload: {:?}", action), None); @@ -29,6 +32,8 @@ pub async fn respond_starred(action: GithubStarredActionType, data: HashMap @@ -52,7 +57,16 @@ pub async fn respond_starred(action: GithubStarredActionType, data: HashMap {return StatusCode::INTERNAL_SERVER_ERROR} }; - match post(disc, msg).await + crate::debug(format!("Formatted message {:?}", msg), None); + + collect(app_state.clone(), data.clone()).await; + + if crate::DONT_MESSAGE_ON_PRIVATE_REPOS && data["repository"]["private"].as_bool().is_some_and(|x|x) + { + return StatusCode::OK; + } + + match post(app_state.lock().await.get_webhook(), msg).await { Ok(_) => StatusCode::OK, Err(e) => diff --git a/src/web/response/github/mod.rs b/src/web/github/response/mod.rs similarity index 78% rename from src/web/response/github/mod.rs rename to src/web/github/response/mod.rs index 99379c8..7063f62 100644 --- a/src/web/response/github/mod.rs +++ b/src/web/github/response/mod.rs @@ -1,4 +1,5 @@ pub mod github_filter; pub mod github_release; pub mod github_starred; +pub mod github_pushed; pub mod model; \ No newline at end of file diff --git a/src/web/response/github/model.rs b/src/web/github/response/model.rs similarity index 82% rename from src/web/response/github/model.rs rename to src/web/github/response/model.rs index f6fe902..dcad303 100644 --- a/src/web/response/github/model.rs +++ b/src/web/github/response/model.rs @@ -1,30 +1,3 @@ -use crate::web::request::discord::model::Webhook; - -#[derive(Clone)] -pub struct GithubConfig -{ - token: String, - discord: Webhook -} - -impl GithubConfig -{ - pub fn new(t: String, w: Webhook) -> GithubConfig - { - GithubConfig {token: t, discord: w} - } - - pub fn get_token(self: GithubConfig) -> String - { - self.token - } - - pub fn get_webhook(self: GithubConfig) -> Webhook - { - self.discord - } -} - #[derive(Debug)] pub enum GithubReleaseActionType { diff --git a/src/web/mod.rs b/src/web/mod.rs index 7f68927..99e3ad8 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -1,3 +1,4 @@ pub mod throttle; pub mod response; -pub mod request; \ No newline at end of file +pub mod github; +pub mod discord; \ No newline at end of file diff --git a/src/web/request/mod.rs b/src/web/request/mod.rs deleted file mode 100644 index d598d52..0000000 --- a/src/web/request/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod discord; \ No newline at end of file diff --git a/src/web/response/mod.rs b/src/web/response/mod.rs index 075a4f0..929d03a 100644 --- a/src/web/response/mod.rs +++ b/src/web/response/mod.rs @@ -1,2 +1 @@ -pub mod util; -pub mod github; \ No newline at end of file +pub mod util; \ No newline at end of file diff --git a/src/web/throttle.rs b/src/web/throttle.rs index bd3442a..12fb449 100644 --- a/src/web/throttle.rs +++ b/src/web/throttle.rs @@ -1,7 +1,8 @@ use std::collections::HashMap; use std::net::{SocketAddr, Ipv4Addr, IpAddr}; use std::time::Instant; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; +use tokio::sync::Mutex; use axum:: { @@ -104,7 +105,7 @@ pub async fn handle_throttle ) -> Result { - if state.lock().unwrap().is_limited(addr) + if state.lock().await.is_limited(addr) { Err(StatusCode::TOO_MANY_REQUESTS) }