11use crate :: { errors:: ApiError , ApiState } ;
2- use axum:: { extract:: State , Json } ;
2+ use axum:: {
3+ extract:: { FromRequestParts , State } ,
4+ http:: { self , request:: Parts } ,
5+ Json ,
6+ } ;
37use chrono:: { DateTime , Utc } ;
8+ use dashmap:: DashMap ;
49use reqwest:: { Client , StatusCode } ;
510use serde:: Serialize ;
611use serde_json:: Value ;
@@ -28,6 +33,7 @@ impl Default for GlobalDataContainer {
2833 latest_version : String :: new ( ) ,
2934 } ,
3035 notes : String :: new ( ) ,
36+ user_agents : DashMap :: new ( ) ,
3137 } ,
3238 }
3339 }
@@ -41,6 +47,8 @@ pub struct GlobalData {
4147 modrinth_data : ModrinthData ,
4248 #[ serde( skip_serializing_if = "String::is_empty" ) ]
4349 notes : String ,
50+ #[ serde( skip) ]
51+ pub user_agents : DashMap < String , u32 > ,
4452}
4553
4654#[ derive( Serialize ) ]
@@ -55,6 +63,7 @@ impl GlobalData {
5563 online_players : online,
5664 modrinth_data : self . modrinth_data . clone ( ) ,
5765 notes : self . notes . clone ( ) ,
66+ user_agents : self . user_agents . clone ( ) ,
5867 }
5968 }
6069}
@@ -66,6 +75,7 @@ impl Clone for GlobalData {
6675 online_players : self . online_players ,
6776 modrinth_data : self . modrinth_data . clone ( ) ,
6877 notes : self . notes . clone ( ) ,
78+ user_agents : self . user_agents . clone ( ) ,
6979 }
7080 }
7181}
@@ -102,13 +112,15 @@ pub async fn get(
102112 return Ok ( Json ( cloned) ) ;
103113 }
104114 let data = if full_refresh {
115+ let agents = data_container. data . user_agents . clone ( ) ;
105116 GlobalData {
106117 total_players : get_total_players ( & database) . await ?,
107118 online_players : online_users. len ( ) as u32 ,
108119 modrinth_data : fetch_modrinth_data ( client) . await ?,
109120 notes : ( cl_args. notes_file . as_ref ( ) )
110121 . map ( |file| read_to_string ( file) . unwrap_or_else ( |_| String :: new ( ) ) )
111122 . unwrap_or_else ( String :: new) ,
123+ user_agents : agents,
112124 }
113125 } else {
114126 data_container
@@ -130,7 +142,10 @@ pub async fn get(
130142
131143pub async fn metrics (
132144 State ( ApiState {
133- database, online_users, ..
145+ database,
146+ online_users,
147+ global_data,
148+ ..
134149 } ) : State < ApiState > ,
135150) -> Result < String , ApiError > {
136151 let lifetime_players = get_total_players ( & database) . await ?;
@@ -147,6 +162,12 @@ pub async fn metrics(
147162 writeln ! ( response, "" ) ;
148163 writeln ! ( response, "lifetime_players {lifetime_players}" ) ;
149164 writeln ! ( response, "online_players {online_players}" ) ;
165+ let data_container = global_data. read ( ) . await ;
166+ let agents = data_container. data . user_agents . clone ( ) ;
167+ for ( agent, count) in agents {
168+ writeln ! ( response, "request_count{{user_agent=\" {agent}\" }} {count}" ) ;
169+ }
170+ data_container. data . user_agents . clear ( ) ;
150171 } ;
151172
152173 Ok ( response)
@@ -183,3 +204,32 @@ async fn fetch_modrinth_data(client: Client) -> Result<ModrinthData, ApiError> {
183204 . to_string ( ) ,
184205 } )
185206}
207+
208+ pub struct UserAgent ;
209+
210+ impl FromRequestParts < ApiState > for UserAgent {
211+ type Rejection = ApiError ;
212+ async fn from_request_parts ( parts : & mut Parts , state : & ApiState ) -> Result < UserAgent , Self :: Rejection > {
213+ if parts. uri . path ( ) . ends_with ( "metrics" ) {
214+ return Ok ( Self ) ;
215+ }
216+ let agent = parts
217+ . headers
218+ . get ( http:: header:: USER_AGENT )
219+ . map ( |v| v. to_str ( ) )
220+ . ok_or ( StatusCode :: BAD_REQUEST ) ?
221+ . map_err ( |_| StatusCode :: BAD_REQUEST ) ?
222+ . replace ( "\\ " , "" )
223+ . replace ( "\" " , "" ) ;
224+
225+ let a = state. global_data . read ( ) . await ;
226+ let agents = & a. data . user_agents ;
227+ if agents. contains_key ( & agent) {
228+ let prev = agents. get ( & agent) . map ( |v| * v. value ( ) ) . unwrap_or ( 0 ) ;
229+ agents. insert ( agent, prev + 1 ) ;
230+ } else {
231+ agents. insert ( agent, 1 ) ;
232+ }
233+ Ok ( Self )
234+ }
235+ }
0 commit comments