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}