Skip to main content

imessage_core/
config.rs

1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3use tracing::info;
4
5/// YAML config file structure (config.yml in the data directory).
6#[derive(Debug, Deserialize, Serialize, Default, Clone)]
7pub struct YamlConfig {
8    #[serde(skip_serializing_if = "Option::is_none")]
9    pub password: Option<String>,
10    #[serde(skip_serializing_if = "Option::is_none")]
11    pub socket_port: Option<u16>,
12    #[serde(skip_serializing_if = "Option::is_none")]
13    pub server_address: Option<String>,
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub enable_private_api: Option<bool>,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub enable_facetime_private_api: Option<bool>,
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub enable_findmy_private_api: Option<bool>,
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub markdown_to_formatting: Option<bool>,
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub webhooks: Option<Vec<WebhookConfigEntry>>,
24}
25
26/// A webhook entry in YAML config.
27/// Can be a simple URL string or an object with url + events.
28#[derive(Debug, Deserialize, Serialize, Clone)]
29#[serde(untagged)]
30pub enum WebhookConfigEntry {
31    Simple(String),
32    Detailed {
33        url: String,
34        #[serde(skip_serializing_if = "Option::is_none")]
35        events: Option<Vec<String>>,
36    },
37}
38
39/// Merged configuration (YAML + CLI, with defaults applied).
40#[derive(Debug, Clone)]
41pub struct AppConfig {
42    pub password: String,
43    pub socket_port: u16,
44    pub server_address: String,
45    pub enable_private_api: bool,
46    pub enable_facetime_private_api: bool,
47    pub enable_findmy_private_api: bool,
48    pub markdown_to_formatting: bool,
49    pub webhooks: Vec<WebhookConfigEntry>,
50}
51
52impl Default for AppConfig {
53    fn default() -> Self {
54        Self {
55            password: String::new(),
56            socket_port: 1234,
57            server_address: String::new(),
58            enable_private_api: false,
59            enable_facetime_private_api: false,
60            enable_findmy_private_api: false,
61            markdown_to_formatting: false,
62            webhooks: vec![],
63        }
64    }
65}
66
67impl YamlConfig {
68    /// Convert to final AppConfig by applying defaults.
69    /// Uses exhaustive destructuring so the compiler errors if a field is added
70    /// to YamlConfig without updating this method.
71    pub fn into_app_config(self) -> AppConfig {
72        let defaults = AppConfig::default();
73        let Self {
74            password,
75            socket_port,
76            server_address,
77            enable_private_api,
78            enable_facetime_private_api,
79            enable_findmy_private_api,
80            markdown_to_formatting,
81            webhooks,
82        } = self;
83        AppConfig {
84            password: password.unwrap_or(defaults.password),
85            socket_port: socket_port.unwrap_or(defaults.socket_port),
86            server_address: server_address.unwrap_or(defaults.server_address),
87            enable_private_api: enable_private_api.unwrap_or(defaults.enable_private_api),
88            enable_facetime_private_api: enable_facetime_private_api
89                .unwrap_or(defaults.enable_facetime_private_api),
90            enable_findmy_private_api: enable_findmy_private_api
91                .unwrap_or(defaults.enable_findmy_private_api),
92            markdown_to_formatting: markdown_to_formatting
93                .unwrap_or(defaults.markdown_to_formatting),
94            webhooks: webhooks.unwrap_or(defaults.webhooks),
95        }
96    }
97}
98
99/// Paths used by the application.
100pub struct AppPaths;
101
102impl AppPaths {
103    /// User data directory: ~/Library/Application Support/imessage-rs
104    pub fn user_data() -> PathBuf {
105        home::home_dir()
106            .unwrap_or_else(|| PathBuf::from("/tmp"))
107            .join("Library")
108            .join("Application Support")
109            .join("imessage-rs")
110    }
111
112    /// PID file location
113    pub fn pid_file() -> PathBuf {
114        Self::user_data().join(".imessage-rs.pid")
115    }
116
117    /// iMessage database path
118    pub fn imessage_db() -> PathBuf {
119        home::home_dir()
120            .unwrap_or_else(|| PathBuf::from("/tmp"))
121            .join("Library")
122            .join("Messages")
123            .join("chat.db")
124    }
125
126    /// iMessage database WAL path
127    pub fn imessage_db_wal() -> PathBuf {
128        Self::imessage_db().with_extension("db-wal")
129    }
130
131    /// Attachments directory (app data)
132    pub fn attachments_dir() -> PathBuf {
133        Self::user_data().join("Attachments")
134    }
135
136    /// Messages attachments directory (inside ~/Library/Messages/).
137    /// Used for Private API sends and on macOS Monterey+ where AppleScript
138    /// requires files to already be inside the Messages sandbox.
139    pub fn messages_attachments_dir() -> PathBuf {
140        home::home_dir()
141            .unwrap_or_else(|| PathBuf::from("/tmp"))
142            .join("Library")
143            .join("Messages")
144            .join("Attachments")
145            .join("imessage-rs")
146    }
147
148    /// Cached attachments directory
149    pub fn attachment_cache_dir() -> PathBuf {
150        Self::attachments_dir().join("Cached")
151    }
152
153    /// Convert staging directory
154    pub fn convert_dir() -> PathBuf {
155        Self::user_data().join("Convert")
156    }
157
158    /// YAML config file path
159    pub fn yaml_config() -> PathBuf {
160        Self::user_data().join("config.yml")
161    }
162
163    /// Server version (from Cargo package version)
164    pub fn version() -> &'static str {
165        env!("CARGO_PKG_VERSION")
166    }
167}
168
169/// Ensure all required directories exist.
170pub fn setup_directories() -> anyhow::Result<()> {
171    let dirs = [
172        AppPaths::user_data(),
173        AppPaths::attachments_dir(),
174        AppPaths::attachment_cache_dir(),
175        AppPaths::convert_dir(),
176    ];
177
178    for dir in &dirs {
179        if !dir.exists() {
180            std::fs::create_dir_all(dir)?;
181            info!("Created directory: {}", dir.display());
182        }
183    }
184
185    Ok(())
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn default_config_values() {
194        let cfg = AppConfig::default();
195        assert_eq!(cfg.socket_port, 1234);
196        assert!(!cfg.enable_private_api);
197    }
198
199    #[test]
200    fn user_data_path_contains_imessage_rs() {
201        let path = AppPaths::user_data();
202        assert!(path.to_str().unwrap().contains("imessage-rs"));
203    }
204}