futu_backend/auth/commconfig/
fetch_page.rs1use 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
41pub 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}