Skip to content

Commit ff3aad0

Browse files
committed
feat(discord): fetch live client build number at startup
1 parent c75eb8f commit ff3aad0

4 files changed

Lines changed: 67 additions & 4 deletions

File tree

src/app.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ impl App {
3636
let resolved_token = resolve_token().await?;
3737
let token = resolved_token.token;
3838
let token_warnings = resolved_token.warnings;
39+
40+
// Must run before the REST client bakes the build number into
41+
// X-Super-Properties and the gateway sends IDENTIFY.
42+
crate::discord::refresh_client_build_number().await;
43+
3944
let client = DiscordClient::new(token)?;
4045
let effects = client.take_effects();
4146
let snapshots = client.subscribe_snapshots();

src/discord/fingerprint.rs

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
use std::process::Command;
2+
use std::sync::OnceLock;
3+
use std::time::Duration;
24

35
use base64::{Engine as _, engine::general_purpose::STANDARD};
46
use reqwest::header::{
@@ -9,7 +11,10 @@ use uuid::Uuid;
911

1012
use 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+
1318
pub(super) const CLIENT_BROWSER: &str = "Chrome";
1419
pub(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+
127177
fn 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();

src/discord/gateway.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use super::{
2626
client::publish_app_event,
2727
events::{AppEvent, SequencedAppEvent},
2828
fingerprint::{
29-
CLIENT_BROWSER, CLIENT_BROWSER_VERSION, CLIENT_BUILD_NUMBER, discord_web_os,
29+
CLIENT_BROWSER, CLIENT_BROWSER_VERSION, client_build_number, discord_web_os,
3030
discord_web_os_version, discord_web_user_agent,
3131
},
3232
state::{DiscordState, SnapshotRevision},
@@ -858,7 +858,7 @@ fn build_identify_payload(token: &str) -> String {
858858
"referrer_current": "",
859859
"referring_domain_current": "",
860860
"release_channel": "stable",
861-
"client_build_number": CLIENT_BUILD_NUMBER,
861+
"client_build_number": client_build_number(),
862862
"client_event_source": Value::Null,
863863
},
864864
"presence": {

src/discord/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ pub use channel::{
5353
};
5454
pub use client::DiscordClient;
5555
pub(crate) use client::validate_token_header;
56+
pub(crate) use fingerprint::refresh_client_build_number;
5657
pub use commands::{
5758
AppCommand, AttachmentDownloadId, DownloadAttachmentSource, ForumPostArchiveState,
5859
ForumPostCreate, GlobalUserProfileUpdate, GuildUserProfileUpdate, MediaPlaybackRequestId,

0 commit comments

Comments
 (0)