11use std:: process:: Command ;
2+ use std:: sync:: OnceLock ;
3+ use std:: time:: Duration ;
24
35use base64:: { Engine as _, engine:: general_purpose:: STANDARD } ;
46use reqwest:: header:: {
@@ -9,7 +11,10 @@ use uuid::Uuid;
911
1012use super :: auth_http:: DISCORD_ORIGIN ;
1113
12- pub ( super ) const CLIENT_BUILD_NUMBER : u64 = 536_121 ;
14+ /// Fallback used only when the live build number cannot be fetched at startup.
15+ pub ( super ) const CLIENT_BUILD_NUMBER : u64 = 573_410 ;
16+ static CLIENT_BUILD_NUMBER_CACHE : OnceLock < u64 > = OnceLock :: new ( ) ;
17+
1318pub ( super ) const CLIENT_BROWSER : & str = "Chrome" ;
1419pub ( super ) const CLIENT_BROWSER_VERSION : & str = "143.0.0.0" ;
1520
@@ -109,7 +114,7 @@ fn build_super_properties(identity: &ClientIdentity) -> String {
109114 has_client_mods : false ,
110115 browser_user_agent : identity. user_agent . clone ( ) ,
111116 browser_version : CLIENT_BROWSER_VERSION ,
112- client_build_number : CLIENT_BUILD_NUMBER ,
117+ client_build_number : client_build_number ( ) ,
113118 client_event_source : None ,
114119 launch_signature : generate_launch_signature ( ) ,
115120 client_launch_id : Uuid :: new_v4 ( ) . to_string ( ) ,
@@ -124,6 +129,51 @@ fn build_super_properties(identity: &ClientIdentity) -> String {
124129 STANDARD . encode ( raw)
125130}
126131
132+ pub ( super ) fn client_build_number ( ) -> u64 {
133+ CLIENT_BUILD_NUMBER_CACHE
134+ . get ( )
135+ . copied ( )
136+ . unwrap_or ( CLIENT_BUILD_NUMBER )
137+ }
138+
139+ /// Aligns our advertised build number with Discord's live value. A stale build
140+ /// number is a self-bot signal that can get accounts flagged. Best-effort. On
141+ /// any failure the compiled fallback stays in place.
142+ pub ( crate ) async fn refresh_client_build_number ( ) {
143+ if CLIENT_BUILD_NUMBER_CACHE . get ( ) . is_some ( ) {
144+ return ;
145+ }
146+ match fetch_client_build_number ( ) . await {
147+ Some ( build) => {
148+ let _ = CLIENT_BUILD_NUMBER_CACHE . set ( build) ;
149+ }
150+ None => crate :: logging:: debug (
151+ "fingerprint" ,
152+ "could not fetch Discord build number; using compiled fallback" ,
153+ ) ,
154+ }
155+ }
156+
157+ async fn fetch_client_build_number ( ) -> Option < u64 > {
158+ let response = discord_rest_client ( )
159+ . get ( format ! ( "{DISCORD_ORIGIN}/app" ) )
160+ . timeout ( Duration :: from_secs ( 5 ) )
161+ . send ( )
162+ . await
163+ . ok ( ) ?;
164+ let body = response. text ( ) . await . ok ( ) ?;
165+ parse_build_number ( & body)
166+ }
167+
168+ /// Discord embeds `"BUILD_NUMBER":"<n>"` in the `/app` HTML.
169+ fn parse_build_number ( html : & str ) -> Option < u64 > {
170+ const MARKER : & str = "\" BUILD_NUMBER\" :\" " ;
171+ let start = html. find ( MARKER ) ? + MARKER . len ( ) ;
172+ let rest = & html[ start..] ;
173+ let end = rest. find ( '"' ) ?;
174+ rest[ ..end] . parse :: < u64 > ( ) . ok ( )
175+ }
176+
127177fn client_identity ( ) -> ClientIdentity {
128178 let os = operating_system ( ) ;
129179 let os_version = operating_system_version ( ) ;
@@ -233,6 +283,13 @@ mod tests {
233283 thread,
234284 } ;
235285
286+ #[ test]
287+ fn parses_build_number_from_app_html ( ) {
288+ let html = r#"window.GLOBAL_ENV = {"BUILD_NUMBER":"573410","VERSION":"1.0.0"};"# ;
289+ assert_eq ! ( parse_build_number( html) , Some ( 573_410 ) ) ;
290+ assert_eq ! ( parse_build_number( "no build number here" ) , None ) ;
291+ }
292+
236293 #[ test]
237294 fn rest_headers_match_web_fingerprint_plan ( ) {
238295 let headers = discord_rest_headers ( ) ;
0 commit comments