Skip to main content

imessage_http/
state.rs

1use parking_lot::Mutex;
2/// Application state shared across all routes via Axum's State extractor.
3use std::collections::{HashMap, HashSet};
4use std::sync::Arc;
5use std::time::Instant;
6
7use serde_json::Value;
8
9use imessage_core::config::AppConfig;
10use imessage_db::imessage::repository::MessageRepository;
11use imessage_private_api::service::PrivateApiService;
12use imessage_webhooks::service::WebhookService;
13
14use crate::middleware::error::AppError;
15
16/// Cached FindMy friends locations (handle -> location JSON) + last refresh time.
17pub type FindMyFriendsCache = (HashMap<String, Value>, Option<Instant>);
18
19/// Shared application state.
20/// MessageRepository wraps rusqlite::Connection which is not Send+Sync,
21/// so we wrap it in a Mutex for thread-safe access.
22#[derive(Clone)]
23pub struct AppState {
24    pub config: Arc<AppConfig>,
25    pub imessage_repo: Arc<Mutex<MessageRepository>>,
26    pub private_api: Option<Arc<PrivateApiService>>,
27    pub webhook_service: Option<Arc<WebhookService>>,
28    /// Cached FindMy friends locations (handle -> location JSON) + last refresh time
29    pub findmy_friends_cache: Arc<Mutex<FindMyFriendsCache>>,
30    /// Cached FindMy device decryption key (32 bytes, from Keychain via FindMy.app)
31    pub findmy_key: Arc<Mutex<Option<[u8; 32]>>>,
32    /// Send cache for deduplication (tempGuid -> timestamp)
33    pub send_cache: Arc<Mutex<HashMap<String, Instant>>>,
34    /// Typing cache: tracks chats with active typing indicators (auto-stopped before sends)
35    pub typing_cache: Arc<Mutex<HashSet<String>>>,
36    /// Mutex to prevent concurrent FindMy device refreshes
37    pub findmy_refresh_lock: Arc<tokio::sync::Mutex<()>>,
38}
39
40impl AppState {
41    pub fn new(
42        config: AppConfig,
43        imessage_repo: MessageRepository,
44        private_api: Option<Arc<PrivateApiService>>,
45        webhook_service: Option<Arc<WebhookService>>,
46    ) -> Self {
47        Self {
48            config: Arc::new(config),
49            imessage_repo: Arc::new(Mutex::new(imessage_repo)),
50            private_api,
51            webhook_service,
52            findmy_friends_cache: Arc::new(Mutex::new((HashMap::new(), None))),
53            findmy_key: Arc::new(Mutex::new(None)),
54            send_cache: Arc::new(Mutex::new(HashMap::new())),
55            typing_cache: Arc::new(Mutex::new(HashSet::new())),
56            findmy_refresh_lock: Arc::new(tokio::sync::Mutex::new(())),
57        }
58    }
59
60    /// Get a reference to the Private API service, or return an error if not available/ready.
61    /// Requires the Messages.app dylib to have completed IMCore initialization.
62    pub fn require_private_api(&self) -> Result<Arc<PrivateApiService>, AppError> {
63        let api = self
64            .private_api
65            .as_ref()
66            .ok_or_else(|| AppError::imessage_error("Private API is not enabled"))?;
67        if !api.is_messages_ready() {
68            return Err(AppError::imessage_error(
69                "Private API helper is not connected",
70            ));
71        }
72        Ok(api.clone())
73    }
74
75    /// Get a reference to the FindMy Private API service, or return an error if not enabled/ready.
76    /// Requires the FindMy.app dylib to be connected and ready.
77    pub fn require_findmy_private_api(&self) -> Result<Arc<PrivateApiService>, AppError> {
78        if !self.config.enable_findmy_private_api {
79            return Err(AppError::imessage_error(
80                "FindMy Private API is not enabled",
81            ));
82        }
83        let api = self
84            .private_api
85            .as_ref()
86            .ok_or_else(|| AppError::imessage_error("FindMy Private API is not enabled"))?;
87        if !api.is_findmy_ready() {
88            return Err(AppError::imessage_error(
89                "FindMy Private API helper is not connected",
90            ));
91        }
92        Ok(api.clone())
93    }
94
95    /// Get a reference to the FaceTime Private API service, or return an error if not enabled/ready.
96    /// Requires the FaceTime.app dylib to be connected and ready.
97    pub fn require_facetime_private_api(&self) -> Result<Arc<PrivateApiService>, AppError> {
98        if !self.config.enable_facetime_private_api {
99            return Err(AppError::imessage_error(
100                "FaceTime Private API is not enabled",
101            ));
102        }
103        let api = self
104            .private_api
105            .as_ref()
106            .ok_or_else(|| AppError::imessage_error("FaceTime Private API is not enabled"))?;
107        if !api.is_facetime_ready() {
108            return Err(AppError::imessage_error(
109                "FaceTime Private API helper is not connected",
110            ));
111        }
112        Ok(api.clone())
113    }
114
115    /// Check if a tempGuid is already in the send cache
116    pub fn is_send_cached(&self, temp_guid: &str) -> bool {
117        self.send_cache.lock().contains_key(temp_guid)
118    }
119
120    /// Add a tempGuid to the send cache
121    pub fn cache_send(&self, temp_guid: String) {
122        self.send_cache.lock().insert(temp_guid, Instant::now());
123    }
124
125    /// Remove a tempGuid from the send cache
126    pub fn uncache_send(&self, temp_guid: &str) {
127        self.send_cache.lock().remove(temp_guid);
128    }
129}