Skip to main content

indodax_cli/
alerts.rs

1use serde::{Deserialize, Serialize};
2use std::fs;
3use std::path::PathBuf;
4use crate::errors::IndodaxError;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct PriceAlert {
8    pub id: u64,
9    pub pair: String,
10    pub condition: AlertCondition,
11    pub created_at: u64,
12    pub triggered_at: Option<u64>,
13    pub status: AlertStatus,
14    pub note: Option<String>,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
18#[serde(tag = "type")]
19pub enum AlertCondition {
20    #[serde(rename = "above")]
21    Above { price: f64 },
22    #[serde(rename = "below")]
23    Below { price: f64 },
24    #[serde(rename = "change_up")]
25    ChangeUp { percent: f64, from_price: f64 },
26    #[serde(rename = "change_down")]
27    ChangeDown { percent: f64, from_price: f64 },
28}
29
30#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
31#[serde(rename_all = "lowercase")]
32pub enum AlertStatus {
33    Active,
34    Triggered,
35    Cancelled,
36}
37
38impl PriceAlert {
39    pub fn new(id: u64, pair: String, condition: AlertCondition, note: Option<String>) -> Self {
40        Self {
41            id,
42            pair,
43            condition,
44            created_at: web_time::SystemTime::now()
45                .duration_since(web_time::UNIX_EPOCH)
46                .unwrap_or_default()
47                .as_millis() as u64,
48            triggered_at: None,
49            status: AlertStatus::Active,
50            note,
51        }
52    }
53}
54
55pub fn get_alerts_path() -> Result<PathBuf, IndodaxError> {
56    let config_dir = dirs::config_dir().ok_or_else(|| {
57        IndodaxError::Config("Could not find config directory".into())
58    })?;
59    let path = config_dir.join("indodax");
60    if !path.exists() {
61        fs::create_dir_all(&path).map_err(|e| {
62            IndodaxError::Config(format!("Failed to create config directory: {}", e))
63        })?;
64    }
65    Ok(path.join("alerts.json"))
66}
67
68pub fn load_alerts() -> Result<Vec<PriceAlert>, IndodaxError> {
69    let path = get_alerts_path()?;
70    if !path.exists() {
71        return Ok(Vec::new());
72    }
73    let content = fs::read_to_string(&path).map_err(|e| {
74        IndodaxError::Config(format!("Failed to read alerts file: {}", e))
75    })?;
76    if content.trim().is_empty() {
77        return Ok(Vec::new());
78    }
79    serde_json::from_str::<Vec<PriceAlert>>(&content).map_err(|e| {
80        IndodaxError::Config(format!("Failed to parse alerts: {}", e))
81    })
82}
83
84pub fn save_alerts(alerts: &[PriceAlert]) -> Result<(), IndodaxError> {
85    let path = get_alerts_path()?;
86    let content = serde_json::to_string_pretty(alerts).map_err(|e| {
87        IndodaxError::Config(format!("Failed to serialize alerts: {}", e))
88    })?;
89    
90    #[cfg(unix)]
91    {
92        use std::io::Write;
93        use std::os::unix::fs::OpenOptionsExt;
94        let mut file = std::fs::OpenOptions::new()
95            .write(true)
96            .create(true)
97            .truncate(true)
98            .mode(0o600)
99            .open(&path)
100            .map_err(|e| IndodaxError::Config(format!("Failed to open alerts file: {}", e)))?;
101        file.write_all(content.as_bytes())
102            .map_err(|e| IndodaxError::Config(format!("Failed to write alerts: {}", e)))?;
103    }
104    #[cfg(not(unix))]
105    {
106        fs::write(&path, content).map_err(|e| {
107            IndodaxError::Config(format!("Failed to write alerts file: {}", e))
108        })?;
109    }
110    Ok(())
111}