Skip to main content

max / goingson

10.3 KB · 313 lines History Blame Raw
1 //! Tauri command modules for the GoingsOn desktop application.
2 //!
3 //! This module organizes all IPC commands into domain-specific submodules
4 //! for better maintainability and separation of concerns.
5 //!
6 //! # Module Organization
7 //!
8 //! - [`project`] - Project CRUD operations
9 //! - [`task`] - Task management, annotations, and subtasks
10 //! - [`event`] - Calendar event operations
11 //! - [`email`] - Email management and IMAP account sync
12 //! - [`oauth`] - OAuth2 authentication for email providers
13 //! - [`search`] - Full-text search across all entities
14 //! - [`day_planning`] - Time blocking and scheduling
15 //! - [`saved_views`] - Custom filter views
16 //! - [`stats`] - Dashboard statistics
17 //! - [`window`] - Window management commands
18
19 mod app_info;
20 pub(crate) mod attachment;
21 mod contact;
22 mod daily_note;
23 mod day_planning;
24 pub(crate) mod email;
25 mod email_account;
26 mod email_sync;
27 pub mod error;
28 mod event;
29 mod export;
30 mod import;
31 mod import_external;
32 mod milestone;
33 mod monthly_review;
34 mod oauth;
35 mod preferences;
36 mod project;
37 mod saved_views;
38 mod search;
39 mod stats;
40 mod sync;
41 mod task;
42 mod task_state;
43 mod task_status_tokens;
44 mod task_subtasks;
45 mod time_tracking;
46 mod themes;
47 mod weekly_review;
48 mod window;
49
50 #[cfg(test)]
51 mod tests;
52
53 // Re-export error types for use in commands
54 pub use error::ApiError;
55 pub use error::{OptionNotFound, OptionApiError, ResultApiError};
56
57 // ============ Private temp-file helpers ============
58 //
59 // Email previews and opened attachments are copied into the system temp dir so
60 // an external app can render them. On Linux that dir is world-readable/traversable
61 // (`/tmp`, mode 1777) and a plain copy inherits mode 0644, so any other local user
62 // could read a decrypted attachment or email body. These helpers confine such
63 // files to the owner. No-ops on non-unix (Windows temp is per-user; macOS confines
64 // $TMPDIR per-user, and 0600/0700 there is still correct).
65
66 /// Restrict a temp directory to owner-only (0700). Call this immediately after
67 /// creating the directory and *before* writing any file into it: an
68 /// un-traversable parent makes every file inside unreachable to other users
69 /// regardless of the file's own mode, closing the copy-then-chmod race.
70 pub(crate) fn harden_temp_dir(path: &std::path::Path) {
71 #[cfg(unix)]
72 {
73 use std::os::unix::fs::PermissionsExt;
74 let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700));
75 }
76 #[cfg(not(unix))]
77 let _ = path;
78 }
79
80 /// Restrict a temp file to owner-only (0600). Defense-in-depth alongside
81 /// [`harden_temp_dir`]; also the sole protection for files written directly into
82 /// the (shared) temp root.
83 pub(crate) fn harden_temp_file(path: &std::path::Path) {
84 #[cfg(unix)]
85 {
86 use std::os::unix::fs::PermissionsExt;
87 let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600));
88 }
89 #[cfg(not(unix))]
90 let _ = path;
91 }
92
93 /// Write bytes to a fresh temp file created owner-only from the start. On unix
94 /// the file is opened with mode 0600 atomically (no world-readable window
95 /// between create and chmod) -- the right primitive for a file written directly
96 /// into the shared temp root, which [`harden_temp_dir`] can't protect.
97 pub(crate) fn write_private_temp(path: &std::path::Path, bytes: &[u8]) -> std::io::Result<()> {
98 use std::io::Write;
99 #[cfg(unix)]
100 let mut file = {
101 use std::os::unix::fs::OpenOptionsExt;
102 std::fs::OpenOptions::new()
103 .write(true)
104 .create(true)
105 .truncate(true)
106 .mode(0o600)
107 .open(path)?
108 };
109 #[cfg(not(unix))]
110 let mut file = std::fs::File::create(path)?;
111 file.write_all(bytes)
112 }
113
114 // Re-export all commands for registration in main.rs
115 pub use app_info::*;
116 pub use attachment::*;
117 pub use contact::*;
118 pub use daily_note::*;
119 pub use day_planning::*;
120 pub use email::*;
121 pub use email_account::*;
122 pub use email_sync::*;
123 pub use event::*;
124 pub use export::*;
125 pub use import::*;
126 pub use import_external::*;
127 pub use milestone::*;
128 pub use monthly_review::*;
129 pub use oauth::*;
130 pub use project::*;
131 pub use saved_views::*;
132 pub use search::*;
133 pub use stats::*;
134 pub use sync::*;
135 pub use task::*;
136 pub use task_status_tokens::*;
137 pub use task_state::*;
138 pub use task_subtasks::*;
139 pub use time_tracking::*;
140 pub use themes::*;
141 pub use weekly_review::*;
142 pub use window::*;
143 pub use preferences::*;
144 pub use preferences::load as load_preferences;
145
146 // ============ Shared Types ============
147
148 use chrono::{DateTime, Utc};
149 use serde::Deserialize;
150
151 /// Input for snoozing tasks or emails until a specific time.
152 #[derive(Debug, Deserialize)]
153 #[serde(rename_all = "camelCase")]
154 pub struct SnoozeInput {
155 pub until: DateTime<Utc>,
156 }
157
158 /// Input for marking items as waiting for response.
159 #[derive(Debug, Deserialize)]
160 #[serde(rename_all = "camelCase")]
161 pub struct WaitingInput {
162 pub expected_response_date: Option<DateTime<Utc>>,
163 }
164
165 /// Input for linking an entity to a project.
166 #[derive(Debug, Deserialize)]
167 #[serde(rename_all = "camelCase")]
168 pub struct LinkProjectInput {
169 pub project_id: Option<goingson_core::ProjectId>,
170 }
171
172 // ============ Snooze Options ============
173
174 use chrono::{Datelike, Duration, Local, NaiveTime, TimeZone, Timelike, Weekday};
175 use serde::Serialize;
176 use tracing::instrument;
177
178 /// A single snooze option with timestamp and display label.
179 ///
180 /// Each option represents a pre-computed snooze time that the user can select,
181 /// such as "Later Today" or "Next Week". The timestamp is in UTC for storage,
182 /// while the formatted string shows local time for display.
183 #[derive(Debug, Serialize)]
184 #[serde(rename_all = "camelCase")]
185 pub struct SnoozeOption {
186 /// Unique identifier for this option (e.g., "laterToday", "tomorrow").
187 pub key: String,
188 /// Human-readable label for the UI (e.g., "Later Today", "Tomorrow").
189 pub label: String,
190 /// The snooze timestamp in UTC, used for storage and sorting.
191 pub time: DateTime<Utc>,
192 /// Formatted local time for display (e.g., "Sat, Feb 15, 10:00 AM").
193 pub formatted: String,
194 }
195
196 /// Response containing all available snooze options.
197 ///
198 /// Provides pre-computed snooze times based on the current local time,
199 /// including smart defaults like "Later Today" (which is omitted if it's
200 /// already past a reasonable hour) and "Next Week" (always the next Monday).
201 #[derive(Debug, Serialize)]
202 #[serde(rename_all = "camelCase")]
203 pub struct SnoozeOptionsResponse {
204 /// List of available snooze options, ordered by time.
205 pub options: Vec<SnoozeOption>,
206 /// Minimum allowed time for custom snooze (current time in UTC).
207 pub min_custom: DateTime<Utc>,
208 }
209
210 /// Get pre-computed snooze options for the UI.
211 ///
212 /// Returns smart snooze times, sorted chronologically:
213 /// - Later Today: 3 hours from now, or 5pm if past 2pm (omitted if already past)
214 /// - Tomorrow: 9am tomorrow
215 /// - This Weekend: Saturday 10am (next Saturday if already Saturday)
216 /// - Next Week: Monday 9am (at least 2 days away to avoid overlap with Tomorrow)
217 #[tauri::command]
218 #[instrument(skip_all)]
219 pub fn get_snooze_options() -> SnoozeOptionsResponse {
220 let now = Local::now();
221 let today = now.date_naive();
222 let mut options = Vec::new();
223
224 // Later today: 3 hours from now, or 5pm if past 2pm
225 let later_today = if now.hour() >= 14 {
226 // After 2pm, suggest 5pm
227 let five_pm = NaiveTime::from_hms_opt(17, 0, 0).expect("17:00 is valid");
228 Local.from_local_datetime(&today.and_time(five_pm)).earliest()
229 } else {
230 // 3 hours from now
231 Some(now + Duration::hours(3))
232 };
233
234 if let Some(lt) = later_today
235 && lt > now {
236 let utc_time = lt.with_timezone(&Utc);
237 options.push(SnoozeOption {
238 key: "laterToday".to_string(),
239 label: "Later Today".to_string(),
240 time: utc_time,
241 formatted: format_snooze_time(&lt),
242 });
243 }
244
245 // Tomorrow 9am
246 let tomorrow = today + Duration::days(1);
247 if let Some(tomorrow_9am) = Local
248 .from_local_datetime(&tomorrow.and_time(NaiveTime::from_hms_opt(9, 0, 0).expect("09:00 is valid")))
249 .earliest()
250 {
251 options.push(SnoozeOption {
252 key: "tomorrow".to_string(),
253 label: "Tomorrow".to_string(),
254 time: tomorrow_9am.with_timezone(&Utc),
255 formatted: format_snooze_time(&tomorrow_9am),
256 });
257 }
258
259 // This weekend (Saturday 10am)
260 let days_until_saturday = (Weekday::Sat.num_days_from_monday() as i64
261 - now.weekday().num_days_from_monday() as i64
262 + 7) % 7;
263 let days_until_saturday = if days_until_saturday == 0 { 7 } else { days_until_saturday };
264 let saturday = today + Duration::days(days_until_saturday);
265 if let Some(weekend) = Local
266 .from_local_datetime(&saturday.and_time(NaiveTime::from_hms_opt(10, 0, 0).expect("10:00 is valid")))
267 .earliest()
268 {
269 options.push(SnoozeOption {
270 key: "weekend".to_string(),
271 label: "This Weekend".to_string(),
272 time: weekend.with_timezone(&Utc),
273 formatted: format_snooze_time(&weekend),
274 });
275 }
276
277 // Next week (Monday 9am) - ensure it's at least 2 days away to avoid overlap with "Tomorrow"
278 let days_until_monday = (Weekday::Mon.num_days_from_monday() as i64
279 - now.weekday().num_days_from_monday() as i64
280 + 7) % 7;
281 // If next Monday is tomorrow (1 day) or today (0 days), use the following Monday
282 let days_until_monday = if days_until_monday <= 1 { days_until_monday + 7 } else { days_until_monday };
283 let monday = today + Duration::days(days_until_monday);
284 if let Some(next_week) = Local
285 .from_local_datetime(&monday.and_time(NaiveTime::from_hms_opt(9, 0, 0).expect("09:00 is valid")))
286 .earliest()
287 {
288 options.push(SnoozeOption {
289 key: "nextWeek".to_string(),
290 label: "Next Week".to_string(),
291 time: next_week.with_timezone(&Utc),
292 formatted: format_snooze_time(&next_week),
293 });
294 }
295
296 // Sort options by time to ensure chronological order
297 options.sort_by_key(|o| o.time);
298
299 SnoozeOptionsResponse {
300 options,
301 min_custom: now.with_timezone(&Utc),
302 }
303 }
304
305 /// Format a snooze time for display (e.g., "Sat, Feb 15, 10:00 AM").
306 fn format_snooze_time<Tz: TimeZone>(dt: &DateTime<Tz>) -> String
307 where
308 Tz::Offset: std::fmt::Display,
309 {
310 dt.format("%a, %b %-d, %-I:%M %p").to_string()
311 }
312
313