1use std::sync::Arc;
4
5use async_trait::async_trait;
6
7use futu_cache::user_profile::{UserProfile, UserProfileCache};
8use futu_core::error::FutuError;
9use futu_core::proto_id;
10use futu_server::conn::IncomingRequest;
11use futu_server::router::{RequestHandler, RequestRouter};
12
13use crate::bridge::{BrokerRuntime, CacheBundle, GatewayBridge};
14
15mod commands;
16
17use commands::{RemoteCmdHandler, TestCmdHandler};
18
19const DEFAULT_USER_INFO_FLAG: i32 = 0xFFFFF7FFu32 as i32;
20const USER_INFO_FIELD_BASIC: i32 = 1;
21const USER_INFO_FIELD_API: i32 = 2;
22const USER_INFO_FIELD_QOT_RIGHT: i32 = 4;
23const USER_INFO_FIELD_DISCLAIMER: i32 = 8;
24const USER_INFO_FIELD_UPDATE: i32 = 16;
25
26pub fn register_handlers(router: &Arc<RequestRouter>, bridge: &GatewayBridge) {
28 register_handlers_inner(router, bridge, None);
29}
30
31pub fn register_handlers_with_shutdown(
38 router: &Arc<RequestRouter>,
39 bridge: &GatewayBridge,
40 shutdown_tx: tokio::sync::watch::Sender<bool>,
41) {
42 register_handlers_inner(router, bridge, Some(shutdown_tx));
43}
44
45fn register_handlers_inner(
46 router: &Arc<RequestRouter>,
47 bridge: &GatewayBridge,
48 shutdown_tx: Option<tokio::sync::watch::Sender<bool>>,
49) {
50 let caches = bridge.caches();
51 router.register(
52 proto_id::GET_USER_INFO,
53 Arc::new(GetUserInfoHandler {
54 caches: caches.clone(),
55 broker_runtime: bridge.broker_runtime().clone(),
56 }),
57 );
58 router.register(
59 proto_id::GET_DELAY_STATISTICS,
60 Arc::new(GetDelayStatisticsHandler),
61 );
62 router.register(proto_id::VERIFICATION, Arc::new(VerificationHandler));
63 router.register(
66 proto_id::TEST_CMD,
67 Arc::new(TestCmdHandler {
68 caches: caches.clone(),
69 broker_runtime: bridge.broker_runtime().clone(),
70 shutdown_tx,
71 }),
72 );
73 router.register(
74 proto_id::REMOTE_CMD,
75 Arc::new(RemoteCmdHandler {
76 caches: caches.clone(),
77 broker_runtime: bridge.broker_runtime().clone(),
78 }),
79 );
80
81 router.register(
83 proto_id::GET_TOKEN_STATE,
84 Arc::new(GetTokenStateHandler {
85 caches: caches.clone(),
86 broker_runtime: bridge.broker_runtime().clone(),
87 }),
88 );
89
90 tracing::debug!("system handlers registered");
91}
92
93struct GetUserInfoHandler {
97 caches: CacheBundle,
98 broker_runtime: BrokerRuntime,
99}
100
101#[async_trait]
102impl RequestHandler for GetUserInfoHandler {
103 async fn handle(&self, _conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
104 let req: futu_proto::get_user_info::Request =
105 match crate::handlers_shared::decode_request_or_error(
106 request.body.as_ref(),
107 "sys",
108 "GetUserInfo",
109 ) {
110 Ok(req) => req,
111 Err(resp) => return Some(resp),
112 };
113 let flag = req.c2s.flag.unwrap_or(DEFAULT_USER_INFO_FLAG);
114
115 let include_basic = has_user_info_field(flag, USER_INFO_FIELD_BASIC);
116 let include_api = has_user_info_field(flag, USER_INFO_FIELD_API);
117 let include_qot_right = has_user_info_field(flag, USER_INFO_FIELD_QOT_RIGHT);
118 let include_disclaimer = has_user_info_field(flag, USER_INFO_FIELD_DISCLAIMER);
119 let include_update = has_user_info_field(flag, USER_INFO_FIELD_UPDATE);
120
121 let login_state = self.caches.login_cache.get_login_state();
122 let user_id = login_state.as_ref().map(|s| s.user_id as i64).unwrap_or(0);
123 let user_id_u64 = u64::try_from(user_id).unwrap_or(0);
124 let user_attribution = login_state.as_ref().and_then(|s| s.user_attribution);
125 if user_id_u64 != 0
126 && include_basic
127 && let Err(e) = refresh_user_profile_cache(
128 &self.broker_runtime,
129 &self.caches.user_profile_cache,
130 user_id_u64,
131 )
132 .await
133 && !matches!(e, FutuError::Timeout)
134 {
135 return Some(crate::handlers_shared::make_error_response(
136 -1,
137 &format!("GetUserInfo QueryUserProfile failed: {e}"),
138 ));
139 }
140 let profile = self.caches.user_profile_cache.get(user_id_u64);
141 let nick_name = profile
142 .as_ref()
143 .map(|profile| profile.nick_name.clone())
144 .unwrap_or_default();
145 let avatar_url = profile
146 .as_ref()
147 .map(|profile| profile.avatar_url.clone())
148 .unwrap_or_default();
149
150 let qr = self.caches.qot_right_cache.get();
155 let freshness = self.caches.qot_right_cache.freshness();
156 let meta = self.caches.qot_right_cache.meta_snapshot();
157 let needs_qot_cache = include_api || include_qot_right;
158 let (use_real, ret_msg_v) = match &freshness {
159 futu_cache::qot_right::QotRightFreshness::Fresh => (true, None),
160 futu_cache::qot_right::QotRightFreshness::Stale => (
161 true,
162 needs_qot_cache.then(|| format!(
163 "qot_right cache stale (login_epoch={}, backend_generation={}); refresh in flight",
164 meta.login_epoch, meta.backend_generation,
165 )),
166 ),
167 futu_cache::qot_right::QotRightFreshness::Pending => (
168 false,
169 needs_qot_cache.then(|| {
170 "qot_right refresh pending (CMD6032 in flight, retry shortly)".to_string()
171 }),
172 ),
173 futu_cache::qot_right::QotRightFreshness::Unknown => (
174 false,
175 needs_qot_cache.then(|| {
176 "qot_right cache uninitialized; daemon may still be booting".to_string()
177 }),
178 ),
179 futu_cache::qot_right::QotRightFreshness::Failed { error, since_ms } => (
180 false,
181 needs_qot_cache.then(|| format!(
182 "qot_right refresh failed: {} (since_ms={}); call request_highest_quote_right to retry",
183 error, since_ms,
184 )),
185 ),
186 };
187 let pick_r = |v: i32| if use_real { v } else { 0 };
188 let pick_b = |v: bool| if use_real { v } else { false };
189 let pick_q = |v: i32| if use_real { v } else { 0 };
190
191 let s2c = futu_proto::get_user_info::S2c {
192 nick_name: include_basic.then_some(nick_name),
193 avatar_url: include_basic.then_some(avatar_url),
194 api_level: None,
195 hk_qot_right: include_qot_right.then_some(pick_r(qr.hk_qot_right)),
196 us_qot_right: include_qot_right.then_some(pick_r(qr.api_us_qot_right)),
197 cn_qot_right: None,
200 is_need_agree_disclaimer: include_disclaimer.then_some(false),
201 user_id: include_basic.then_some(user_id),
202 update_type: include_update.then_some(0), web_key: None,
204 web_jump_url_head: None,
205 hk_option_qot_right: include_qot_right.then_some(pick_r(qr.hk_option_qot_right)),
206 has_us_option_qot_right: include_qot_right
207 .then_some(pick_b(qr.has_us_option_qot_right)),
208 hk_future_qot_right: include_qot_right.then_some(pick_r(qr.hk_future_qot_right)),
209 sub_quota: include_api.then_some(pick_q(qr.sub_quota)),
210 history_kl_quota: include_api.then_some(pick_q(qr.history_kl_quota)),
211 us_future_qot_right: None,
215 us_option_qot_right: include_qot_right.then_some(pick_r(qr.us_option_qot_right)),
216 user_attribution: (include_basic || user_id_u64 == 0)
217 .then_some(user_attribution)
218 .flatten(),
219 update_whats_new: None,
220 us_index_qot_right: include_qot_right.then_some(pick_r(qr.us_index_qot_right)),
221 us_otc_qot_right: include_qot_right.then_some(pick_r(qr.us_otc_qot_right)),
222 us_cme_future_qot_right: include_qot_right
223 .then_some(pick_r(qr.us_cme_future_qot_right)),
224 us_cbot_future_qot_right: include_qot_right
225 .then_some(pick_r(qr.us_cbot_future_qot_right)),
226 us_nymex_future_qot_right: include_qot_right
227 .then_some(pick_r(qr.us_nymex_future_qot_right)),
228 us_comex_future_qot_right: include_qot_right
229 .then_some(pick_r(qr.us_comex_future_qot_right)),
230 us_cboe_future_qot_right: include_qot_right
231 .then_some(pick_r(qr.us_cboe_future_qot_right)),
232 sg_future_qot_right: include_qot_right.then_some(pick_r(qr.sg_future_qot_right)),
233 jp_future_qot_right: include_qot_right.then_some(pick_r(qr.jp_future_qot_right)),
234 is_app_nn_or_mm: include_basic.then_some(true), sh_qot_right: include_qot_right.then_some(pick_r(qr.sh_qot_right)),
236 sz_qot_right: include_qot_right.then_some(pick_r(qr.sz_qot_right)),
237 extra: None,
238 cc_qot_right: include_qot_right.then_some(pick_r(qr.cc_qot_right)),
239 sg_stock_qot_right: include_qot_right.then_some(pick_r(qr.sg_stock_qot_right)),
240 my_stock_qot_right: include_qot_right.then_some(pick_r(qr.my_stock_qot_right)),
241 jp_stock_qot_right: include_qot_right.then_some(pick_r(qr.jp_stock_qot_right)),
242 };
243
244 let resp = futu_proto::get_user_info::Response {
245 ret_type: 0,
246 ret_msg: ret_msg_v,
247 err_code: None,
248 s2c: Some(s2c),
249 };
250 Some(prost::Message::encode_to_vec(&resp))
251 }
252}
253
254fn has_user_info_field(flag: i32, field: i32) -> bool {
255 (flag & field) != 0
256}
257
258async fn refresh_user_profile_cache(
259 broker_runtime: &BrokerRuntime,
260 user_profile_cache: &UserProfileCache,
261 user_id: u64,
262) -> Result<(), FutuError> {
263 let Some(backend) = broker_runtime.platform_backend() else {
264 return Ok(());
265 };
266
267 match futu_backend::user_profile::query_user_profile(&backend, user_id).await {
268 Ok(profile) => {
269 user_profile_cache.set(UserProfile {
270 user_id: profile.user_id,
271 nick_name: profile.nick_name,
272 avatar_url: profile.avatar_url,
273 });
274 Ok(())
275 }
276 Err(e) => {
277 tracing::warn!(
278 user_id,
279 error = %e,
280 "CMD7506 QueryUserProfile failed; using cached profile if available"
281 );
282 Err(e)
283 }
284 }
285}
286
287struct GetDelayStatisticsHandler;
292
293#[async_trait]
294impl RequestHandler for GetDelayStatisticsHandler {
295 async fn handle(&self, _conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
296 let req: futu_proto::get_delay_statistics::Request =
297 match crate::handlers_shared::decode_request_or_error(
298 request.body.as_ref(),
299 "sys",
300 "GetDelayStatistics",
301 ) {
302 Ok(req) => req,
303 Err(resp) => return Some(resp),
304 };
305
306 let include_req_reply = req
307 .c2s
308 .type_list
309 .contains(&(futu_proto::get_delay_statistics::DelayStatisticsType::ReqReply as i32));
310 let include_qot_push = req
311 .c2s
312 .type_list
313 .contains(&(futu_proto::get_delay_statistics::DelayStatisticsType::QotPush as i32));
314 let include_place_order = req
315 .c2s
316 .type_list
317 .contains(&(futu_proto::get_delay_statistics::DelayStatisticsType::PlaceOrder as i32));
318
319 let s2c = futu_proto::get_delay_statistics::S2c {
320 qot_push_statistics_list: if include_qot_push {
321 futu_core::delay_stats::snapshot_qot_push_statistics(
322 req.c2s.qot_push_stage.unwrap_or(0),
323 &req.c2s.segment_list,
324 )
325 .into_iter()
326 .map(|item| futu_proto::get_delay_statistics::DelayStatistics {
327 qot_push_type: item.qot_push_type,
328 item_list: item
329 .item_list
330 .into_iter()
331 .map(
332 |bucket| futu_proto::get_delay_statistics::DelayStatisticsItem {
333 begin: bucket.begin,
334 end: bucket.end,
335 count: bucket.count,
336 proportion: bucket.proportion,
337 cumulative_ratio: bucket.cumulative_ratio,
338 },
339 )
340 .collect(),
341 delay_avg: item.delay_avg_ms,
342 count: item.count,
343 })
344 .collect()
345 } else {
346 vec![]
347 },
348 req_reply_statistics_list: if include_req_reply {
349 futu_core::delay_stats::snapshot_req_reply_statistics()
350 .into_iter()
351 .map(
352 |item| futu_proto::get_delay_statistics::ReqReplyStatisticsItem {
353 proto_id: item.proto_id as i32,
354 count: item.count as i32,
355 total_cost_avg: item.total_cost_avg_ms,
356 open_d_cost_avg: item.open_d_cost_avg_ms,
357 net_delay_avg: item.net_delay_avg_ms,
358 is_local_reply: item.is_local_reply,
359 },
360 )
361 .collect()
362 } else {
363 vec![]
364 },
365 place_order_statistics_list: if include_place_order {
366 futu_core::delay_stats::snapshot_place_order_statistics()
367 .into_iter()
368 .map(
369 |item| futu_proto::get_delay_statistics::PlaceOrderStatisticsItem {
370 order_id: item.order_id,
371 total_cost: item.total_cost_ms,
372 open_d_cost: item.open_d_cost_ms,
373 net_delay: item.net_delay_ms,
374 update_cost: item.update_cost_ms,
375 },
376 )
377 .collect()
378 } else {
379 vec![]
380 },
381 };
382
383 let resp = futu_proto::get_delay_statistics::Response {
384 ret_type: 0,
385 ret_msg: None,
386 err_code: None,
387 s2c: Some(s2c),
388 };
389 Some(prost::Message::encode_to_vec(&resp))
390 }
391}
392
393struct VerificationHandler;
399
400#[async_trait]
401impl RequestHandler for VerificationHandler {
402 async fn handle(&self, _conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
403 let _req: futu_proto::verification::Request =
404 match crate::handlers_shared::decode_request_or_error(
405 request.body.as_ref(),
406 "sys",
407 "Verification",
408 ) {
409 Ok(req) => req,
410 Err(resp) => return Some(resp),
411 };
412
413 let resp = futu_proto::verification::Response {
414 ret_type: -1,
415 ret_msg: Some(
416 "Verification requires the C++ INNLoginCenter captcha/SMS flow; \
417 Rust OpenD does not implement this flow yet"
418 .to_string(),
419 ),
420 err_code: None,
421 s2c: None,
422 };
423 Some(prost::Message::encode_to_vec(&resp))
424 }
425}
426
427struct GetTokenStateHandler {
447 caches: CacheBundle,
448 broker_runtime: BrokerRuntime,
449}
450
451#[async_trait]
452impl RequestHandler for GetTokenStateHandler {
453 async fn handle(&self, conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
454 let req: futu_backend::proto_internal::futu_token_state::DaemonGetTokenStateReq =
456 match prost::Message::decode(request.body.as_ref()) {
457 Ok(r) => r,
458 Err(e) => {
459 tracing::warn!(conn_id, error = %e, "GetTokenState: decode req failed");
460 return Some(crate::handlers_shared::make_error_response(
461 -1,
462 "invalid GetTokenState request body",
463 ));
464 }
465 };
466
467 let user_id = match self.caches.login_cache.get_login_state() {
469 Some(s) => s.user_id as u64,
470 None => {
471 return Some(crate::handlers_shared::make_error_response(
472 -1,
473 "GetTokenState: not logged in (login_cache empty). \
474 Run f3clogin via futu-opend before querying token state.",
475 ));
476 }
477 };
478
479 let app_id_str = req.c2s.app_id.unwrap_or_else(|| "all".to_string());
480 let app_id = if app_id_str.is_empty() {
481 "all".to_string()
482 } else {
483 app_id_str
484 };
485
486 let backend = match self.broker_runtime.platform_backend() {
487 Some(b) => b,
488 None => {
489 tracing::warn!(conn_id, "GetTokenState: no backend connection");
490 return Some(crate::handlers_shared::make_error_response(
491 -1,
492 "no backend connection (try again after backend reconnect)",
493 ));
494 }
495 };
496
497 let backend_req = futu_backend::proto_internal::futu_token_state::GetTokenListRequest {
499 uid: Some(user_id),
500 app_id: Some(app_id.as_bytes().to_vec()),
501 };
502 let body = prost::Message::encode_to_vec(&backend_req);
503
504 tracing::debug!(
505 conn_id,
506 uid = user_id,
507 app_id = %app_id,
508 "sending CMD1326 GetTokenListRequest"
509 );
510
511 let resp_frame = match backend
512 .request(proto_id::GET_TOKEN_STATE as u16, body)
513 .await
514 {
515 Ok(f) => f,
516 Err(e) => {
517 tracing::error!(conn_id, error = %e, "CMD1326 backend request failed");
518 return Some(crate::handlers_shared::make_error_response(
519 -1,
520 &format!("GetTokenState: backend cmd 1326 failed: {e}"),
521 ));
522 }
523 };
524
525 let backend_rsp: futu_backend::proto_internal::futu_token_state::GetTokenListResponse =
526 match crate::handlers_shared::decode_backend_proto(&resp_frame.body) {
527 Ok(r) => r,
528 Err(e) => {
529 tracing::error!(conn_id, error = %e, "CMD1326 decode response failed");
530 return Some(crate::handlers_shared::make_error_response(
531 -1,
532 "GetTokenState: failed to decode backend response",
533 ));
534 }
535 };
536
537 let err_code = backend_rsp.err_code;
548 if err_code != Some(0) {
549 let err_msg = backend_rsp.err_msg.as_deref().unwrap_or("");
550 let detail = match err_code {
551 Some(c) => format!("backend cmd 1326 err: {err_msg} (code={c})"),
552 None => format!(
553 "backend cmd 1326 response missing err_code (raw err_msg='{err_msg}'); cannot determine token state"
554 ),
555 };
556 tracing::warn!(
557 conn_id,
558 err_code = ?err_code,
559 err_msg,
560 "CMD1326 backend response not Some(0): refusing silent success"
561 );
562 return Some(prost::Message::encode_to_vec(
563 &futu_backend::proto_internal::futu_token_state::DaemonGetTokenStateRsp {
564 ret_type: -1,
565 ret_msg: Some(detail),
566 err_code,
567 s2c: None,
568 },
569 ));
570 }
571
572 let all_token_fields_missing = backend_rsp.nn_token_enable.is_none()
574 && backend_rsp.nn_token_bind.is_none()
575 && backend_rsp.mm_token_enable.is_none()
576 && backend_rsp.mm_token_bind.is_none();
577 if all_token_fields_missing {
578 tracing::warn!(
579 conn_id,
580 "CMD1326 backend success (err_code=0) but all 4 token state fields missing — refusing silent ret_type=0"
581 );
582 return Some(prost::Message::encode_to_vec(
583 &futu_backend::proto_internal::futu_token_state::DaemonGetTokenStateRsp {
584 ret_type: -1,
585 ret_msg: Some(
586 "backend cmd 1326 success but token state fields all missing; cannot return reliable token-state".to_string(),
587 ),
588 err_code: Some(0),
589 s2c: None,
590 },
591 ));
592 }
593
594 let daemon_rsp = futu_backend::proto_internal::futu_token_state::DaemonGetTokenStateRsp {
596 ret_type: 0,
597 ret_msg: None,
598 err_code: None,
599 s2c: Some(
600 futu_backend::proto_internal::futu_token_state::daemon_get_token_state_rsp::S2c {
601 nn_token_enable: backend_rsp.nn_token_enable,
602 nn_token_bind: backend_rsp.nn_token_bind,
603 mm_token_enable: backend_rsp.mm_token_enable,
604 mm_token_bind: backend_rsp.mm_token_bind,
605 },
606 ),
607 };
608 Some(prost::Message::encode_to_vec(&daemon_rsp))
609 }
610}