Skip to main content

longbridge/alert/
context.rs

1use std::sync::Arc;
2
3use longbridge_httpcli::{HttpClient, Json, Method};
4use serde::{Serialize, de::DeserializeOwned};
5use tracing::{Subscriber, dispatcher, instrument::WithSubscriber};
6
7use crate::{Config, Result, alert::types::*, utils::counter::symbol_to_counter_id};
8
9struct InnerAlertContext {
10    http_cli: HttpClient,
11    log_subscriber: Arc<dyn Subscriber + Send + Sync>,
12}
13
14impl Drop for InnerAlertContext {
15    fn drop(&mut self) {
16        dispatcher::with_default(&self.log_subscriber.clone().into(), || {
17            tracing::info!("alert context dropped");
18        });
19    }
20}
21
22/// Price alert management context.
23#[derive(Clone)]
24pub struct AlertContext(Arc<InnerAlertContext>);
25
26impl AlertContext {
27    /// Create an [`AlertContext`]
28    pub fn new(config: Arc<Config>) -> Self {
29        let log_subscriber = config.create_log_subscriber("alert");
30        dispatcher::with_default(&log_subscriber.clone().into(), || {
31            tracing::info!(language = ?config.language, "creating alert context");
32        });
33        let ctx = Self(Arc::new(InnerAlertContext {
34            http_cli: config.create_http_client(),
35            log_subscriber,
36        }));
37        dispatcher::with_default(&ctx.0.log_subscriber.clone().into(), || {
38            tracing::info!("alert context created");
39        });
40        ctx
41    }
42
43    /// Returns the log subscriber
44    #[inline]
45    pub fn log_subscriber(&self) -> Arc<dyn Subscriber + Send + Sync> {
46        self.0.log_subscriber.clone()
47    }
48
49    async fn get<R, Q>(&self, path: &'static str, query: Q) -> Result<R>
50    where
51        R: DeserializeOwned + Send + Sync + 'static,
52        Q: Serialize + Send + Sync,
53    {
54        Ok(self
55            .0
56            .http_cli
57            .request(Method::GET, path)
58            .query_params(query)
59            .response::<Json<R>>()
60            .send()
61            .with_subscriber(self.0.log_subscriber.clone())
62            .await?
63            .0)
64    }
65
66    async fn post<R, B>(&self, path: &'static str, body: B) -> Result<R>
67    where
68        R: DeserializeOwned + Send + Sync + 'static,
69        B: std::fmt::Debug + Serialize + Send + Sync + 'static,
70    {
71        Ok(self
72            .0
73            .http_cli
74            .request(Method::POST, path)
75            .body(Json(body))
76            .response::<Json<R>>()
77            .send()
78            .with_subscriber(self.0.log_subscriber.clone())
79            .await?
80            .0)
81    }
82
83    async fn http_delete<R, B>(&self, path: &'static str, body: B) -> Result<R>
84    where
85        R: DeserializeOwned + Send + Sync + 'static,
86        B: std::fmt::Debug + Serialize + Send + Sync + 'static,
87    {
88        Ok(self
89            .0
90            .http_cli
91            .request(Method::DELETE, path)
92            .body(Json(body))
93            .response::<Json<R>>()
94            .send()
95            .with_subscriber(self.0.log_subscriber.clone())
96            .await?
97            .0)
98    }
99
100    /// List all price alerts.
101    ///
102    /// Path: `GET /v1/notify/reminders`
103    pub async fn list(&self) -> Result<AlertList> {
104        #[derive(Serialize)]
105        struct Empty {}
106        self.get("/v1/notify/reminders", Empty {}).await
107    }
108
109    /// Add a price alert.
110    ///
111    /// Path: `POST /v1/notify/reminders`
112    pub async fn add(
113        &self,
114        symbol: impl Into<String>,
115        condition: AlertCondition,
116        trigger_value: impl Into<String>,
117        frequency: AlertFrequency,
118    ) -> Result<serde_json::Value> {
119        let cid = symbol_to_counter_id(&symbol.into());
120        let (key, val) = match condition {
121            AlertCondition::PriceRise | AlertCondition::PriceFall => {
122                ("price", trigger_value.into())
123            }
124            AlertCondition::PercentRise | AlertCondition::PercentFall => {
125                ("chg", trigger_value.into())
126            }
127        };
128        let indicator_id = condition as i32;
129        let freq = frequency as i32;
130        self.post(
131            "/v1/notify/reminders",
132            serde_json::json!({
133                "counter_id": cid,
134                "indicator_id": indicator_id.to_string(),
135                "value_map": { key: val },
136                "frequency": freq,
137                "enabled": true,
138                "scope": 0,
139                "state": [1]
140            }),
141        )
142        .await
143    }
144
145    /// Update a price alert.
146    ///
147    /// Requires the [`AlertItem`] from [`list`](Self::list). Set
148    /// `item.enabled` to `true` to re-enable or `false` to disable before
149    /// calling this method. All required fields are read from `item` directly
150    /// — no extra round-trip needed.
151    ///
152    /// Path: `POST /v1/notify/reminders`
153    pub async fn update(&self, item: &AlertItem) -> Result<serde_json::Value> {
154        self.post(
155            "/v1/notify/reminders",
156            serde_json::json!({
157                "id": item.id,
158                "indicator_id": item.indicator_id,
159                "frequency": item.frequency,
160                "scope": item.scope,
161                "state": item.state,
162                "value_map": item.value_map,
163                "enabled": item.enabled,
164            }),
165        )
166        .await
167    }
168
169    /// Delete price alerts.
170    ///
171    /// Path: `DELETE /v1/notify/reminders`
172    pub async fn delete(&self, alert_ids: Vec<String>) -> Result<serde_json::Value> {
173        self.http_delete(
174            "/v1/notify/reminders",
175            serde_json::json!({ "ids": alert_ids }),
176        )
177        .await
178    }
179}