feat(imessage): implement Phase 5 daemon command handlers#8
Conversation
Implement all 8 command handlers in DaemonService to connect hot resources (SQLite connection + contacts cache) to query logic: - recent: 2.4ms - list recent messages with contact enrichment - unread: 2.8ms - list unread messages - analytics: 20.5ms - message stats, busiest times, top contacts - followup: 6.1ms - unanswered questions and stale conversations - handles: 3.0ms - list all message handles - unknown: filter handles not in contacts - discover: find frequent unknown senders as contact candidates - bundle: combine multiple queries in single request Key changes: - Add src/db/helpers.rs with shared query functions - Refactor analytics.rs to use db::helpers - Implement all handlers in daemon/service.rs Performance: 5/8 commands meet sub-5ms target. Analytics slower due to 6 sequential queries (future optimization opportunity). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Summary of ChangesHello @wolfiesch, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly advances the Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request is a significant step forward, implementing the daemon command handlers and refactoring query logic into a shared db::helpers module. The refactoring to centralize database logic is a great improvement for maintainability. My review focuses on further improving the new code by reducing duplication and increasing robustness. I've identified several areas in daemon/service.rs where repeated logic for parameter parsing and data enrichment can be extracted into helper methods. The bundle handler, in particular, has a high degree of code duplication that should be addressed. In the new db/helpers.rs file, I've suggested moving a hardcoded SQL query to the central queries.rs file for consistency and pointed out a potential panic that could affect the daemon's stability. A key finding is that while the analytics command was refactored, the followup command in commands/analytics.rs was not, leaving a large amount of duplicated code that should also be updated to use the new helpers.
| fn bundle(&self, params: HashMap<String, serde_json::Value>) -> Result<serde_json::Value> { | ||
| let include = params | ||
| .get("include") | ||
| .and_then(|v| v.as_str()) | ||
| .unwrap_or("unread_count,recent"); | ||
|
|
||
| let sections: Vec<&str> = include.split(',').map(|s| s.trim()).collect(); | ||
| let mut result = serde_json::Map::new(); | ||
|
|
||
| for section in sections { | ||
| match section { | ||
| "unread_count" => { | ||
| let unread = helpers::query_unread_messages(&self.conn, 100)?; | ||
| result.insert("unread_count".to_string(), serde_json::json!(unread.len())); | ||
| } | ||
| "recent" => { | ||
| let limit = params | ||
| .get("recent_limit") | ||
| .and_then(|v| v.as_u64()) | ||
| .unwrap_or(10) as u32; | ||
| let days = params | ||
| .get("recent_days") | ||
| .and_then(|v| v.as_u64()) | ||
| .unwrap_or(7) as u32; | ||
| let cutoff = queries::days_ago_cocoa(days); | ||
| let messages = helpers::query_recent_messages(&self.conn, cutoff, limit)?; | ||
|
|
||
| let enriched: Vec<serde_json::Value> = messages | ||
| .into_iter() | ||
| .map(|msg| { | ||
| let name = self.contacts.find_by_phone(&msg.phone).map(|c| c.name.clone()); | ||
| serde_json::json!({ | ||
| "text": msg.text, | ||
| "date": msg.date, | ||
| "is_from_me": msg.is_from_me, | ||
| "phone": msg.phone, | ||
| "contact_name": name, | ||
| }) | ||
| }) | ||
| .collect(); | ||
| result.insert("recent".to_string(), serde_json::json!(enriched)); | ||
| } | ||
| "analytics" => { | ||
| let days = params | ||
| .get("analytics_days") | ||
| .and_then(|v| v.as_u64()) | ||
| .unwrap_or(30) as u32; | ||
| let cutoff = queries::days_ago_cocoa(days); | ||
|
|
||
| let (total, sent, received) = | ||
| helpers::query_message_counts(&self.conn, cutoff, None)?; | ||
|
|
||
| result.insert( | ||
| "analytics".to_string(), | ||
| serde_json::json!({ | ||
| "total_messages": total, | ||
| "sent_count": sent, | ||
| "received_count": received, | ||
| "period_days": days, | ||
| }), | ||
| ); | ||
| } | ||
| "followup_count" => { | ||
| let days = params | ||
| .get("followup_days") | ||
| .and_then(|v| v.as_u64()) | ||
| .unwrap_or(30) as u32; | ||
| let stale = params | ||
| .get("followup_stale") | ||
| .and_then(|v| v.as_u64()) | ||
| .unwrap_or(3) as u32; | ||
|
|
||
| let cutoff = queries::days_ago_cocoa(days); | ||
| let stale_ns = (stale as i64) * 24 * 3600 * 1_000_000_000; | ||
|
|
||
| let unanswered = helpers::query_unanswered_questions(&self.conn, cutoff, stale_ns)?; | ||
| let stale_convos = helpers::query_stale_conversations(&self.conn, cutoff, stale_ns)?; | ||
|
|
||
| result.insert( | ||
| "followup_count".to_string(), | ||
| serde_json::json!(unanswered.len() + stale_convos.len()), | ||
| ); | ||
| } | ||
| _ => { | ||
| // Unknown section, skip silently | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Ok(serde_json::Value::Object(result)) | ||
| } |
There was a problem hiding this comment.
The bundle handler duplicates a significant amount of logic from other handlers like recent, analytics, and followup. This creates a major maintainability issue, as any change in the logic of those handlers needs to be manually synchronized here. The bundle handler should be refactored to reuse the logic from the other handlers, for example by calling them directly with appropriate parameters, or by extracting the shared logic (parameter parsing, querying, and enrichment) into private helper methods that all handlers can use.
| let days = params | ||
| .get("days") | ||
| .and_then(|v| v.as_u64()) | ||
| .unwrap_or(7) as u32; | ||
| let limit = params | ||
| .get("limit") | ||
| .and_then(|v| v.as_u64()) | ||
| .unwrap_or(20) as u32; |
There was a problem hiding this comment.
The logic for parsing optional u32 parameters from the params HashMap with a default value is repeated in almost every command handler (recent, unread, analytics, followup, etc.). This adds boilerplate and is prone to inconsistencies. Consider extracting this into a private helper function to improve code reuse and readability.
For example, you could add a helper method to DaemonService:
fn get_param_u32(&self, params: &HashMap<String, serde_json::Value>, key: &str, default: u32) -> u32 {
params
.get(key)
.and_then(|v| v.as_u64())
.map(|v| v as u32)
.unwrap_or(default)
}And then use it like this:
let days = self.get_param_u32(¶ms, "days", 7);
let limit = self.get_param_u32(¶ms, "limit", 20);| let enriched: Vec<serde_json::Value> = messages | ||
| .into_iter() | ||
| .map(|msg| { | ||
| let contact_name = self | ||
| .contacts | ||
| .find_by_phone(&msg.phone) | ||
| .map(|c| c.name.clone()); | ||
| serde_json::json!({ | ||
| "text": msg.text, | ||
| "date": msg.date, | ||
| "is_from_me": msg.is_from_me, | ||
| "phone": msg.phone, | ||
| "contact_name": contact_name, | ||
| }) | ||
| }) | ||
| .collect(); |
There was a problem hiding this comment.
The logic to enrich data with contact names is duplicated across multiple handlers (recent, unread, followup, handles, bundle). This is a lot of repeated code that could lead to maintenance issues. You should extract this into private helper methods on DaemonService for each data type to improve code reuse and maintainability.
For example, for RecentMessage:
// In `impl DaemonService`
fn enrich_recent_message(&self, msg: helpers::RecentMessage) -> serde_json::Value {
let contact_name = self.contacts.find_by_phone(&msg.phone).map(|c| c.name.clone());
serde_json::json!({
"text": msg.text,
"date": msg.date,
"is_from_me": msg.is_from_me,
"phone": msg.phone,
"contact_name": contact_name,
})
}Then you can simplify the collection mapping:
let enriched: Vec<serde_json::Value> = messages
.into_iter()
.map(|msg| self.enrich_recent_message(msg))
.collect();| .unwrap_or(3) as u32; | ||
|
|
||
| let cutoff_cocoa = queries::days_ago_cocoa(days); | ||
| let stale_threshold_ns = (stale as i64) * 24 * 3600 * 1_000_000_000; |
There was a problem hiding this comment.
The calculation (stale as i64) * 24 * 3600 * 1_000_000_000 uses magic numbers for time conversion. This makes the code harder to read and understand. It's better to define constants like SECONDS_IN_DAY and NANOS_IN_SECOND to make the calculation self-documenting. This calculation is also repeated in the bundle handler (line 474).
| let stale_threshold_ns = (stale as i64) * 24 * 3600 * 1_000_000_000; | |
| let stale_threshold_ns = (stale as i64) * 24 * 3600 * 1_000_000_000; // TODO: Use constants for time units |
| let mut stmt = conn.prepare( | ||
| r#" | ||
| SELECT | ||
| m.text, | ||
| m.date, | ||
| m.is_from_me, | ||
| h.id as handle | ||
| FROM message m | ||
| LEFT JOIN handle h ON m.handle_id = h.ROWID | ||
| WHERE m.date >= ?1 | ||
| AND (m.associated_message_type IS NULL OR m.associated_message_type = 0) | ||
| AND m.text IS NOT NULL | ||
| ORDER BY m.date DESC | ||
| LIMIT ?2 | ||
| "#, | ||
| )?; |
There was a problem hiding this comment.
| let now = SystemTime::now() | ||
| .duration_since(UNIX_EPOCH) | ||
| .expect("Time went backwards") |
There was a problem hiding this comment.
The use of .expect("Time went backwards") could cause the daemon to panic if the system clock is adjusted backwards. While this is a rare event, for a long-running service, it's better to handle this case gracefully instead of crashing. Consider changing this function to return a Result or handle the Err case from duration_since without panicking, for example by returning the current time as a fallback.
There was a problem hiding this comment.
Pull request overview
This PR implements Phase 5 of the iMessage daemon, completing all 8 command handlers that leverage hot resources (persistent SQLite connection and cached contacts) for sub-5ms query performance. The implementation introduces a shared db::helpers module containing reusable query functions, and refactors the existing analytics.rs CLI command to use these shared helpers, eliminating code duplication.
Changes:
- Added
db::helpersmodule with 9 query helper functions and 7 data structures for analytics, reading, discovery, and follow-up queries - Implemented 8 daemon command handlers (recent, unread, analytics, followup, handles, unknown, discover, bundle) in
DaemonService - Refactored
analytics.rsCLI command to use shared helpers, removing 90+ lines of duplicate query code
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/lib.rs | Added CHANGELOG entry for db::helpers module |
| src/db/mod.rs | Added helpers module export and alphabetized module declarations |
| src/db/helpers.rs | New shared query helpers module with data structures, query functions, and utility functions for timestamp conversion |
| src/daemon/service.rs | Implemented all 8 command handlers with contact enrichment and hot connection reuse |
| src/commands/analytics.rs | Refactored to use shared helpers from db::helpers module |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let mut stmt = conn.prepare( | ||
| r#" | ||
| SELECT | ||
| m.text, | ||
| m.date, | ||
| m.is_from_me, | ||
| h.id as handle | ||
| FROM message m | ||
| LEFT JOIN handle h ON m.handle_id = h.ROWID | ||
| WHERE m.date >= ?1 | ||
| AND (m.associated_message_type IS NULL OR m.associated_message_type = 0) | ||
| AND m.text IS NOT NULL | ||
| ORDER BY m.date DESC | ||
| LIMIT ?2 | ||
| "#, | ||
| )?; |
There was a problem hiding this comment.
The query_recent_messages function uses an inline SQL query string instead of referencing a constant from the queries module like other helper functions do (e.g., query_unread_messages uses queries::UNREAD_MESSAGES). For consistency and maintainability, consider extracting this SQL into a constant in queries.rs and referencing it here, similar to how other query helpers are implemented.
Summary
db::helpersmodule for reusable query functionsanalytics.rsto use shared helpersCommands Implemented
recentunreadanalyticsfollowuphandlesunknowndiscoverbundlePerformance
5/8 commands meet sub-5ms target. Analytics is slower (20ms) due to 6 sequential queries - future optimization opportunity.
Test plan
cargo build --releasewolfies-imessage-client🤖 Generated with Claude Code