Skip to main content

futu_backend/
user_cloud_config.rs

1use std::fmt;
2
3use prost::Message;
4
5use crate::proto_internal::{cloud_sync_service, user_cloud_config};
6
7// Protocol/config identities, not server-discovered routing data. Ref:
8// INNData_UserCloudConfig.h:8-13 and NNProto_UserCloudConfig.cpp:63-76,135-141.
9// Re-audit if C++ changes the RequestedKline cloud config key.
10pub const REQUESTED_KLINE_CONFIG_ID: u32 = 60_000;
11pub const REQUESTED_KLINE_ITEM_ID: u64 = 1;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum UserCloudWireError {
15    DecodeEnvelope {
16        context: &'static str,
17        message: String,
18    },
19    InvalidUpload {
20        field: &'static str,
21    },
22}
23
24impl fmt::Display for UserCloudWireError {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        match self {
27            Self::DecodeEnvelope { context, message } => {
28                write!(f, "{context} protobuf decode failed: {message}")
29            }
30            Self::InvalidUpload { field } => {
31                write!(f, "UserCloud upload has invalid {field}")
32            }
33        }
34    }
35}
36
37impl std::error::Error for UserCloudWireError {}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub struct UserCloudKeyFacts {
41    pub cfg_id: Option<u32>,
42    pub item_id: Option<u64>,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub struct RequestedKlineWireItem {
47    pub stock_id: u64,
48    pub request_time: i64,
49    pub option_expiry_time_sec: Option<i64>,
50}
51
52pub type RequestedKlineUploadItem = RequestedKlineWireItem;
53
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub enum UserCloudDataFacts {
56    Missing,
57    Opaque { len: usize },
58    RequestedKline { items: Vec<RequestedKlineWireItem> },
59    MalformedRequestedKline { message: String },
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct UserCloudConfigItemFacts {
64    pub user_id: Option<u64>,
65    pub key_id: Option<UserCloudKeyFacts>,
66    pub updated_time: Option<u64>,
67    pub data: UserCloudDataFacts,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct UserCloudDownloadFacts {
72    pub ret_code: Option<i32>,
73    pub ret_msg: Option<String>,
74    pub items: Vec<UserCloudConfigItemFacts>,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct UserCloudUploadInput {
79    pub user_id: u64,
80    pub updated_time: u64,
81    pub sequence: String,
82    pub items: Vec<RequestedKlineUploadItem>,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct UserCloudUploadReplyFacts {
87    pub ret_code: Option<i32>,
88    pub ret_msg: Option<String>,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct UserCloudPushFacts {
93    pub items: Vec<UserCloudConfigItemFacts>,
94    pub sequence: Option<String>,
95}
96
97#[must_use]
98pub fn encode_requested_kline_download() -> Vec<u8> {
99    cloud_sync_service::ConfigDownloadReq {
100        key_ids: vec![cloud_sync_service::KeyId {
101            cfg_id: Some(REQUESTED_KLINE_CONFIG_ID),
102            item_id: Some(REQUESTED_KLINE_ITEM_ID),
103        }],
104    }
105    .encode_to_vec()
106}
107
108pub fn decode_requested_kline_download(
109    body: &[u8],
110) -> Result<UserCloudDownloadFacts, UserCloudWireError> {
111    let wire = cloud_sync_service::ConfigDownloadRsp::decode(body)
112        .map_err(|error| decode_error("ConfigDownloadRsp", error))?;
113    Ok(UserCloudDownloadFacts {
114        ret_code: wire.ret_code,
115        ret_msg: wire.ret_msg,
116        items: decode_config_items(wire.cfg_items),
117    })
118}
119
120pub fn encode_requested_kline_upload(
121    input: &UserCloudUploadInput,
122) -> Result<Vec<u8>, UserCloudWireError> {
123    validate_upload(input)?;
124    let nested = user_cloud_config::RequestedKline {
125        requested_items: input
126            .items
127            .iter()
128            .map(|item| user_cloud_config::RequestedKlineItem {
129                sec_id: item.stock_id,
130                time: item.request_time,
131                option_expiry_timestamp_sec: item.option_expiry_time_sec,
132            })
133            .collect(),
134    }
135    .encode_to_vec();
136    Ok(cloud_sync_service::ConfigUploadReq {
137        cfg_items: vec![cloud_sync_service::ConfigItem {
138            user_id: Some(input.user_id),
139            key_id: Some(cloud_sync_service::KeyId {
140                cfg_id: Some(REQUESTED_KLINE_CONFIG_ID),
141                item_id: Some(REQUESTED_KLINE_ITEM_ID),
142            }),
143            updated_time: Some(input.updated_time),
144            data: Some(nested),
145        }],
146        sequence: Some(input.sequence.clone()),
147    }
148    .encode_to_vec())
149}
150
151pub fn decode_requested_kline_upload(
152    body: &[u8],
153) -> Result<UserCloudUploadReplyFacts, UserCloudWireError> {
154    let wire = cloud_sync_service::ConfigUploadRsp::decode(body)
155        .map_err(|error| decode_error("ConfigUploadRsp", error))?;
156    Ok(UserCloudUploadReplyFacts {
157        ret_code: wire.ret_code,
158        ret_msg: wire.ret_msg,
159    })
160}
161
162pub fn decode_user_cloud_update_push(
163    body: &[u8],
164) -> Result<UserCloudPushFacts, UserCloudWireError> {
165    let wire = cloud_sync_service::ConfigUpdatePush::decode(body)
166        .map_err(|error| decode_error("ConfigUpdatePush", error))?;
167    Ok(UserCloudPushFacts {
168        items: decode_config_items(wire.cfg_items),
169        sequence: wire.sequence,
170    })
171}
172
173fn decode_config_items(
174    items: Vec<cloud_sync_service::ConfigItem>,
175) -> Vec<UserCloudConfigItemFacts> {
176    items
177        .into_iter()
178        .map(|item| {
179            let key_id = item.key_id.map(|key| UserCloudKeyFacts {
180                cfg_id: key.cfg_id,
181                item_id: key.item_id,
182            });
183            let requested_kline =
184                key_id.as_ref().and_then(|key| key.cfg_id) == Some(REQUESTED_KLINE_CONFIG_ID);
185            let data = match item.data {
186                None => UserCloudDataFacts::Missing,
187                Some(data) if requested_kline => decode_nested_requested_kline(&data),
188                Some(data) => UserCloudDataFacts::Opaque { len: data.len() },
189            };
190            UserCloudConfigItemFacts {
191                user_id: item.user_id,
192                key_id,
193                updated_time: item.updated_time,
194                data,
195            }
196        })
197        .collect()
198}
199
200fn decode_nested_requested_kline(data: &[u8]) -> UserCloudDataFacts {
201    match user_cloud_config::RequestedKline::decode(data) {
202        Ok(wire) => UserCloudDataFacts::RequestedKline {
203            items: wire
204                .requested_items
205                .into_iter()
206                .map(|item| RequestedKlineWireItem {
207                    stock_id: item.sec_id,
208                    request_time: item.time,
209                    option_expiry_time_sec: item.option_expiry_timestamp_sec,
210                })
211                .collect(),
212        },
213        Err(error) => UserCloudDataFacts::MalformedRequestedKline {
214            message: error.to_string(),
215        },
216    }
217}
218
219fn validate_upload(input: &UserCloudUploadInput) -> Result<(), UserCloudWireError> {
220    if input.user_id == 0 {
221        return Err(UserCloudWireError::InvalidUpload { field: "user_id" });
222    }
223    if input.sequence.trim().is_empty() {
224        return Err(UserCloudWireError::InvalidUpload { field: "sequence" });
225    }
226    if input.items.iter().any(|item| {
227        item.stock_id == 0 || matches!(item.option_expiry_time_sec, Some(expiry) if expiry == 0)
228    }) {
229        return Err(UserCloudWireError::InvalidUpload {
230            field: "requested_kline_item",
231        });
232    }
233    Ok(())
234}
235
236fn decode_error(context: &'static str, error: prost::DecodeError) -> UserCloudWireError {
237    UserCloudWireError::DecodeEnvelope {
238        context,
239        message: error.to_string(),
240    }
241}