Skip to main content

futu_backend/auth/commconfig/
fetch_page.rs

1//! One CommConfig HTTP page request and JSON decode.
2
3use super::clock::server_now_ts;
4use super::totp::gen_totp_sha1;
5use super::types::AUTH_TOKEN_KEY_B32;
6
7#[derive(Debug, PartialEq, Eq)]
8pub(super) enum CommConfigPageFetchError {
9    Transport(String),
10    Decode(String),
11}
12
13impl CommConfigPageFetchError {
14    pub(super) fn runtime_failure(&self) -> futu_domain_auth::CommConfigRuntimeFailure {
15        match self {
16            Self::Transport(_) => futu_domain_auth::CommConfigRuntimeFailure::Transport,
17            Self::Decode(_) => futu_domain_auth::CommConfigRuntimeFailure::Decode,
18        }
19    }
20}
21
22impl std::fmt::Display for CommConfigPageFetchError {
23    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        match self {
25            Self::Transport(message) => write!(formatter, "transport: {message}"),
26            Self::Decode(message) => write!(formatter, "decode: {message}"),
27        }
28    }
29}
30
31impl std::error::Error for CommConfigPageFetchError {}
32
33pub fn api_root_for_client(client_type: u8) -> &'static str {
34    if client_type == 40 {
35        "https://api.futunn.com"
36    } else {
37        "https://api.moomoo.com"
38    }
39}
40
41/// `nClientVer = 1031` -> `"10.31.0"`, matching the C++ formatting.
42pub fn client_version_dotted(num_ver: u32) -> String {
43    let major = num_ver / 100;
44    let minor = num_ver % 100;
45    format!("{major}.{minor}.0")
46}
47
48pub(super) async fn fetch_page(
49    http: &reqwest::Client,
50    client_type: u8,
51    device_id: &str,
52    user_id: u64,
53    begin_id: i32,
54    svr_time_offset: i64,
55) -> Result<serde_json::Value, CommConfigPageFetchError> {
56    let svr_ts = server_now_ts(svr_time_offset);
57    let token = gen_totp_sha1(AUTH_TOKEN_KEY_B32, svr_ts, 30)
58        .ok_or_else(|| CommConfigPageFetchError::Transport("TOTP generation failed".to_string()))?;
59    let client_ver_num = crate::conn::BackendConn::CLIENT_VER_FTGTW as u32;
60    let client_ver_dotted = client_version_dotted(client_ver_num);
61    let url = format!(
62        "{root}/v2/conf/select_all?user_id={uid}&auth_token={tok}&is_visitor=0\
63         &clienttype={ct}&clientver={cv}&content=0",
64        root = api_root_for_client(client_type),
65        uid = user_id,
66        tok = token,
67        ct = client_type,
68        cv = client_ver_dotted,
69    );
70    let body = serde_json::json!({ "begin_id": begin_id });
71    let auth_headers = super::super::http_client::auth_http_default_headers(client_type)
72        .map_err(|error| CommConfigPageFetchError::Transport(error.to_string()))?;
73    let response = http
74        .post(&url)
75        .headers(auth_headers)
76        .header("X-Futu-Client-Deviceid", device_id)
77        .header("X-Futu-Client-NNid", user_id.to_string())
78        .json(&body)
79        .send()
80        .await
81        .map_err(|error| CommConfigPageFetchError::Transport(error.to_string()))?;
82    let status = response.status();
83    let text = response
84        .text()
85        .await
86        .map_err(|error| CommConfigPageFetchError::Transport(error.to_string()))?;
87    if !status.is_success() {
88        return Err(CommConfigPageFetchError::Transport(format!(
89            "HTTP {status}: {head}",
90            head = text.chars().take(200).collect::<String>()
91        )));
92    }
93    serde_json::from_str(&text).map_err(|error| {
94        CommConfigPageFetchError::Decode(format!(
95            "{error} (body head: {head})",
96            head = text.chars().take(200).collect::<String>()
97        ))
98    })
99}