futu_backend/stock_list/
sync.rs1use std::io::Read as IoRead;
4
5use bytes::Bytes;
6use flate2::read::GzDecoder;
7use futu_command_spec::StaticDataReadOperation;
8use futu_core::error::{FutuError, Result};
9use futu_domain_static_data::{
10 StockListServerChecksumPair, StockListServerChecksums, StockListSyncPageFacts,
11 ensure_stock_list_sync_backend_success,
12};
13use prost::Message;
14
15use crate::conn::BackendConn;
16use crate::proto_internal::{ftcmd6741, stock_list_sync_svr};
17
18use super::{StockInfo, parse::parse_stock_item};
19
20#[derive(Debug, Clone)]
21pub struct StockListPage {
22 pub items: Vec<StockInfo>,
23 pub facts: StockListSyncPageFacts,
24 pub all_count: u32,
25}
26
27pub async fn fetch_stock_list_page(
28 backend: &BackendConn,
29 request_version: u64,
30) -> Result<StockListPage> {
31 let req = stock_list_sync_svr::StockListReq {
32 stock_list_version: request_version,
33 if_req_all: 0,
34 special_version: Some(1),
35 };
36 let resp = crate::command_runtime::execute_static_data_read(
37 backend,
38 StaticDataReadOperation::StockListSync,
39 Bytes::from(req.encode_to_vec()),
40 )
41 .await?;
42 let decompressed = decompress_stock_list_body(&resp.body)?;
43 let parsed: stock_list_sync_svr::StockListRsp = Message::decode(decompressed.as_slice())
44 .map_err(|e| FutuError::Codec(format!("CMD6746 decode failed: {e}")))?;
45
46 if let Err(err) = ensure_stock_list_sync_backend_success(parsed.result) {
47 return Err(FutuError::Codec(format!(
48 "CMD6746 error: result={}",
49 match err {
50 futu_domain_static_data::StockListSyncBackendError::Result(result) => result,
51 }
52 )));
53 }
54
55 let items = parsed
56 .arry_items
57 .iter()
58 .map(parse_stock_item)
59 .collect::<Vec<_>>();
60 let facts = StockListSyncPageFacts {
61 item_count: items.len(),
62 array_max_version: parsed.array_max_version,
63 if_all_rsp: parsed.if_all_rsp,
64 next_request_interval_secs: parsed.next_request_interval,
65 server_checksum: Some(StockListServerChecksums {
66 environment_id: parsed.stock_list_id.unwrap_or(0),
67 #[allow(deprecated)]
68 legacy_id: parsed.id_check_sum.unwrap_or(0),
69 #[allow(deprecated)]
70 legacy_seq: parsed.seq_check_sum.unwrap_or(0),
71 stock_count: parsed.stock_count.unwrap_or(0),
72 id_v2: parsed
73 .id_check_sum_v2
74 .as_ref()
75 .map(checksum_pair_from_proto),
76 seq_v2: parsed
77 .seq_check_sum_v2
78 .as_ref()
79 .map(checksum_pair_from_proto),
80 }),
81 };
82
83 log_legacy_checksums(&parsed);
84 Ok(StockListPage {
85 items,
86 facts,
87 all_count: parsed.all_count.unwrap_or(0),
88 })
89}
90
91fn checksum_pair_from_proto(value: &ftcmd6741::CheckSum) -> StockListServerChecksumPair {
92 StockListServerChecksumPair {
93 low: value.low.unwrap_or(0),
94 high: value.high.unwrap_or(0),
95 }
96}
97
98fn log_legacy_checksums(parsed: &stock_list_sync_svr::StockListRsp) {
99 #[allow(deprecated)]
102 if let (Some(server_id_sum), Some(server_seq_sum)) = (parsed.id_check_sum, parsed.seq_check_sum)
103 && (server_id_sum > 0 || server_seq_sum > 0)
104 {
105 tracing::debug!(server_id_sum, server_seq_sum, "server checksums received");
106 }
107 if let (Some(id_v2), Some(seq_v2)) = (&parsed.id_check_sum_v2, &parsed.seq_check_sum_v2) {
108 tracing::debug!(
109 id_low = id_v2.low,
110 id_high = id_v2.high,
111 seq_low = seq_v2.low,
112 seq_high = seq_v2.high,
113 "server v2 checksums received"
114 );
115 }
116}
117
118fn decompress_stock_list_body(data: &[u8]) -> Result<Vec<u8>> {
119 if data.is_empty() {
120 return Ok(Vec::new());
121 }
122
123 let mut gzip = GzDecoder::new(data);
124 let mut decompressed = Vec::new();
125 if gzip.read_to_end(&mut decompressed).is_ok() {
126 return Ok(decompressed);
127 }
128
129 let mut zlib = flate2::read::ZlibDecoder::new(data);
130 let mut decompressed = Vec::new();
131 if zlib.read_to_end(&mut decompressed).is_ok() {
132 return Ok(decompressed);
133 }
134
135 tracing::debug!(
136 len = data.len(),
137 "stock-list data not compressed, using raw body"
138 );
139 Ok(data.to_vec())
140}