@@ -6,11 +6,12 @@ use axum::{
66} ;
77use chrono:: { DateTime , Utc } ;
88use dashmap:: DashMap ;
9+ use regex:: Regex ;
910use reqwest:: { Client , StatusCode } ;
1011use serde:: Serialize ;
1112use serde_json:: Value ;
1213use sqlx:: { query, PgPool } ;
13- use std:: { fmt:: Write , fs:: read_to_string} ;
14+ use std:: { fmt:: Write , fs:: read_to_string, sync :: LazyLock } ;
1415
1516const PROJECT_ID : & str = "p2rxzX0q" ;
1617
@@ -172,18 +173,66 @@ pub async fn metrics(
172173 let data_container = global_data. read ( ) . await ;
173174 let request_agents = data_container. data . request_user_agents . clone ( ) ;
174175 for ( agent, count) in request_agents {
175- writeln ! ( response, "request_count{{user_agent=\" {agent}\" }} {count}" ) ;
176+ if let Some ( ( mod_ver, minecraft_ver, note) ) = parse_user_agent ( agent) {
177+ writeln ! ( response, "request_count{{mod_version=\" {mod_ver}\" , minecraft_version=\" {minecraft_ver}\" , mod=\" {note}\" }} {count}" ) ;
178+ }
176179 }
177180 data_container. data . request_user_agents . clear ( ) ;
178181 let gateway_agents = data_container. data . gateway_user_agents . clone ( ) ;
179182 for ( agent, count) in gateway_agents {
180- writeln ! ( response, "connections{{user_agent=\" {agent}\" }} {count}" ) ;
183+ if let Some ( ( mod_ver, minecraft_ver, note) ) = parse_user_agent ( agent) {
184+ writeln ! ( response, "connections{{mod_version=\" {mod_ver}\" , minecraft_version=\" {minecraft_ver}\" , mod=\" {note}\" }} {count}" ) ;
185+ }
181186 }
182187 } ;
183188
184189 Ok ( response)
185190}
186191
192+ static SEMVER_REGEX : LazyLock < Regex > = LazyLock :: new ( || {
193+ Regex :: new (
194+ r#"(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?"# ,
195+ )
196+ . unwrap ( )
197+ } ) ;
198+ static MCVER_REGEX : LazyLock < Regex > = LazyLock :: new ( || {
199+ Regex :: new (
200+ "0\\ .\\ d+(\\ .\\ d+)?a?(_\\ d+)?|\\ d+\\ .\\ d+(\\ .\\ d+)?(-pre\\ d+|Pre-[Rr]elease \\ d+)?|\\ d+\\ .\\ d+(\\ .\\ d+)?(-rc\\ d+| [Rr]elease Candidate \\ d+)?|\\ d+w\\ d+[a-z]|[a-c]\\ d\\ .\\ d+(\\ .\\ d+)?[a-z]?(_\\ d+)?[a-z]?|(Alpha|Beta) v?\\ d+\\ .\\ d+(\\ .\\ d+)?[a-z]?(_\\ d+)?[a-z]?|Inf?dev (0\\ .31 )?\\ d+(-\\ d+)?|(rd|inf?)-\\ d+|1\\ .RV-Pre1|3D Shareware v1\\ .34|23w13a_or_b|24w14potato|25w14craftmine|(.*[Ee]xperimental [Ss]napshot )(\\ d+)" ,
201+ )
202+ . unwrap ( )
203+ } ) ;
204+ static OLD_1_UA : LazyLock < Regex > =
205+ LazyLock :: new ( || Regex :: new ( r".*\((?:(AxolotlClient)/)(.+)(?:\+mc)?(.+)\) .*" ) . unwrap ( ) ) ;
206+ static OLD_2_UA : LazyLock < Regex > =
207+ LazyLock :: new ( || Regex :: new ( r".*\((?:(AxolotlClient)/)(.+) \(Minecraft .+\).*" ) . unwrap ( ) ) ;
208+ static CURRENT_UA : LazyLock < Regex > = LazyLock :: new ( || Regex :: new ( r"(AxolotlClient)/(.+) Minecraft/(.+)" ) . unwrap ( ) ) ;
209+ static SNAPPER_UA : LazyLock < Regex > = LazyLock :: new ( || Regex :: new ( r"(Snapper)/(.+)\+(.+)" ) . unwrap ( ) ) ;
210+
211+ fn parse_user_agent ( agent : String ) -> Option < ( String , String , String ) > {
212+ if agent. starts_with ( "Java-http-client" ) {
213+ return None ;
214+ }
215+ for regex in [ & OLD_1_UA , & OLD_2_UA , & CURRENT_UA , & SNAPPER_UA ] {
216+ if regex. is_match ( & agent) {
217+ let captures = regex. captures ( & agent) . unwrap ( ) ;
218+ let mod_name_capture = captures. get ( 1 ) ;
219+ let mod_ver_capture = captures. get ( 2 ) ;
220+ let mc_ver_capture = captures. get ( 3 ) ;
221+ if mod_name_capture. is_none ( ) || mod_ver_capture. is_none ( ) || mc_ver_capture. is_none ( ) {
222+ return None ;
223+ }
224+ let mod_name = mod_name_capture. unwrap ( ) . as_str ( ) . to_string ( ) ;
225+ let mod_ver = mod_ver_capture. unwrap ( ) . as_str ( ) ;
226+ let mc_ver = mc_ver_capture. unwrap ( ) . as_str ( ) ;
227+ if !SEMVER_REGEX . is_match ( mod_ver) || !MCVER_REGEX . is_match ( mc_ver) {
228+ return None ;
229+ }
230+ return Some ( ( mod_ver. to_string ( ) , mc_ver. to_string ( ) , mod_name) ) ;
231+ }
232+ }
233+ return None ;
234+ }
235+
187236async fn get_total_players ( database : & PgPool ) -> Result < u32 , ApiError > {
188237 Ok ( query ! ( "SELECT reltuples AS estimate FROM pg_class where relname = 'players'" )
189238 . fetch_one ( database)
0 commit comments