Skip to main content

oxios_markdown/
types.rs

1//! Shared types for the oxios-markdown crate.
2//!
3//! Core data structures used across all modules.
4
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8// ============================================================================
9// Directory & Filename Constants
10// ============================================================================
11
12/// Root directory identifier.
13pub const DIR_USER_ROOT: &str = "/";
14
15/// Archive directory name.
16pub const DIR_ARCHIVE: &str = "archive";
17
18/// Media directory name.
19pub const DIR_MEDIA: &str = "media";
20
21/// Journal directory name.
22pub const DIR_JOURNAL: &str = "journal";
23
24/// Habits directory name.
25pub const DIR_HABITS: &str = "habits";
26
27/// Insights directory name.
28pub const DIR_INSIGHTS: &str = "insights";
29
30/// Chat filename.
31pub const CHAT_FILENAME: &str = "Chat.md";
32
33/// Later filename.
34pub const LATER_FILENAME: &str = "Later.md";
35
36/// Done filename.
37pub const DONE_FILENAME: &str = "Done.md";
38
39/// Shop filename.
40pub const SHOP_FILENAME: &str = "Shop.md";
41
42/// Watch filename.
43pub const WATCH_FILENAME: &str = "Watch.md";
44
45/// Read filename.
46pub const READ_FILENAME: &str = "Read.md";
47
48/// Pomodoro task marker.
49pub const POMODORO_TASK: &str = "Finished a break";
50
51/// Markdown file extension.
52pub const MD_EXT: &str = ".md";
53
54// ============================================================================
55// File / Entry Types
56// ============================================================================
57
58/// A file or directory entry in the knowledge base.
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct FileEntry {
61    /// Filename with extension (e.g., "Rust.md").
62    pub name: String,
63    /// MD5 hash (first 11 characters) for compact identification.
64    pub hash: String,
65    /// Display name: capitalized, without extension.
66    pub display_name: String,
67    /// Creation/modification time in milliseconds since epoch.
68    pub ctime: i64,
69    /// Whether the file has non-whitespace content.
70    pub has_content: bool,
71    /// Whether this is a directory.
72    pub is_dir: bool,
73    /// Parent directory path.
74    pub parent_dir: String,
75}
76
77impl FileEntry {
78    /// Create a new file entry.
79    pub fn new(
80        name: String,
81        hash: String,
82        display_name: String,
83        ctime: i64,
84        has_content: bool,
85        is_dir: bool,
86        parent_dir: String,
87    ) -> Self {
88        Self {
89            name,
90            hash,
91            display_name,
92            ctime,
93            has_content,
94            is_dir,
95            parent_dir,
96        }
97    }
98}
99
100// ============================================================================
101// Error Types
102// ============================================================================
103
104/// Filesystem errors for the knowledge base.
105#[derive(Debug, thiserror::Error)]
106pub enum FsError {
107    /// Storage quota exceeded.
108    #[error("storage quota exceeded")]
109    QuotaExceeded,
110    /// Unsafe path (path traversal attempt).
111    #[error("unsafe path, possible security issue")]
112    UnsafePath,
113    /// Cannot reverse a hash to find the original filename.
114    #[error("cannot unhash, maybe the file is missing")]
115    CannotUnhash,
116    /// IO error.
117    #[error("{0}")]
118    Io(#[from] std::io::Error),
119}
120
121// ============================================================================
122// Sync Types
123// ============================================================================
124
125/// Sync status: operation succeeded.
126pub const STATUS_OK: &str = "ok";
127
128/// Sync status: file not modified.
129pub const STATUS_NOT_MODIFIED: &str = "notModified";
130
131/// Sync status: file was updated on server.
132pub const STATUS_UPDATED_ON_SERVER: &str = "updatedOnServer";
133
134/// Sync status: file was merged from both sides.
135pub const STATUS_MERGED: &str = "merged";
136
137/// Maximum size for a single text sync (5 MB).
138pub const MAX_TEXT_SIZE: usize = 5 * 1024 * 1024;
139
140/// Maximum size for a batch text sync (10 MB).
141pub const MAX_TEXTS_SIZE: usize = 10 * 1024 * 1024;
142
143/// Maximum size for a single media sync (20 MB).
144pub const MAX_MEDIA_SIZE: usize = 20 * 1024 * 1024;
145
146/// Maximum size for a batch media sync (512 KB).
147pub const MAX_MEDIAS_SIZE: usize = 512 * 1024;
148
149/// Maximum size for an auth token (4 KB).
150pub const MAX_TOKEN_SIZE: usize = 4 * 1024;
151
152/// A file in the sync protocol.
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct SyncFile {
155    /// Status of this file in the sync response.
156    pub status: String,
157    /// File path (relative to knowledge base root).
158    pub path: String,
159    /// Last modified timestamp (ms since epoch).
160    #[serde(rename = "lastModified")]
161    pub last_modified: i64,
162    /// Client's last modification time.
163    #[serde(rename = "clientLastModified", default)]
164    pub client_last_modified: i64,
165    /// Client's last sync time.
166    #[serde(rename = "clientLastSynced", default)]
167    pub client_last_synced: i64,
168    /// File content.
169    #[serde(default)]
170    pub content: String,
171}
172
173/// A batch sync request from the client.
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct SyncRequest {
176    /// Modified files from the client.
177    pub modified: Vec<SyncFile>,
178    /// Deleted file paths from the client.
179    pub deleted: Vec<String>,
180    /// Client's known directory timestamps.
181    pub timestamps: HashMap<String, i64>,
182}
183
184/// A sync response to the client.
185#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct SyncResponse {
187    /// Overall sync status.
188    pub status: String,
189    /// Files that need to be sent to the client.
190    #[serde(default)]
191    pub files: Vec<SyncFile>,
192    /// Current directory timestamps on the server.
193    #[serde(default)]
194    pub timestamps: HashMap<String, i64>,
195    /// Rename map: new_path → old_path.
196    #[serde(default)]
197    pub renames: HashMap<String, String>,
198}
199
200impl Default for SyncResponse {
201    fn default() -> Self {
202        SyncResponse {
203            status: STATUS_OK.to_string(),
204            files: vec![],
205            timestamps: HashMap::new(),
206            renames: HashMap::new(),
207        }
208    }
209}
210
211/// Sync-specific errors.
212#[derive(Debug, thiserror::Error)]
213pub enum SyncError {
214    /// Invalid JSON in the request.
215    #[error("invalid JSON")]
216    InvalidJson,
217    /// File not found.
218    #[error("file not found")]
219    NotFound,
220    /// Storage quota exceeded.
221    #[error("quota exceeded")]
222    QuotaExceeded,
223    /// Storage layer error.
224    #[error("storage error: {0}")]
225    Storage(String),
226    /// Internal error.
227    #[error("internal error: {0}")]
228    Internal(String),
229}
230
231impl From<FsError> for SyncError {
232    fn from(err: FsError) -> Self {
233        match err {
234            FsError::QuotaExceeded => SyncError::QuotaExceeded,
235            _ => SyncError::Storage(err.to_string()),
236        }
237    }
238}
239
240// ============================================================================
241// Habits Types
242// ============================================================================
243
244/// Per-year habit map: day-of-year → status (0=skipped, 1=completed).
245pub type YearHabits = HashMap<i32, i32>;
246
247/// All habits: habit name → year data.
248pub type Habits = HashMap<String, YearHabits>;
249
250/// Habit skipped marker.
251pub const HABIT_SKIPPED: &str = "⚪️";
252
253/// Habit completed marker.
254pub const HABIT_COMPLETED: &str = "🟢";
255
256/// Habit completed at weekend marker.
257pub const HABIT_COMPLETED_AT_WEEKEND: &str = "🟡";
258
259/// Mood habit name.
260pub const MOOD_HABIT: &str = "Mood";
261
262/// Default mood emojis (index = mood level).
263pub const MOOD_EMOJIS: &[&str] = &["⚪️", "🤕", "😔", "😐", "🙂", "😊"];
264
265// ============================================================================
266// Schedule Types
267// ============================================================================
268
269/// A scheduled task.
270#[derive(Debug, Clone, Default, Serialize, Deserialize)]
271pub struct Schedule {
272    /// Target filename.
273    pub filename: String,
274    /// Scheduled timestamp (ms since epoch).
275    pub scheduled_at: i64,
276    /// Cron expression (e.g., "9:00").
277    pub cron: String,
278    /// Command placeholder (for future use).
279    #[serde(default)]
280    pub cmd: String,
281}
282
283// Knowledge Config Types
284// ============================================================================
285
286/// User knowledge base configuration.
287///
288/// Stored as `config.json` in the knowledge base root.
289/// Decoupled from any server-specific config.
290#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct KnowledgeConfig {
292    /// Language code (e.g., "en", "ko").
293    #[serde(default = "default_language")]
294    pub language: String,
295    /// Timezone string (e.g., "+09:00", "UTC").
296    #[serde(default = "default_timezone")]
297    pub timezone: String,
298    /// Move-to commands (quick file organization).
299    #[serde(default)]
300    pub move_to_commands: Vec<String>,
301    /// Pomodoro timer duration in minutes.
302    #[serde(default = "default_pomodoro_duration")]
303    pub pomodoro_duration_in_minutes: i64,
304    /// Scheduled tasks.
305    #[serde(default)]
306    pub schedules: Vec<Schedule>,
307    /// Quick commands.
308    #[serde(default)]
309    pub quick_commands: Vec<String>,
310    /// Whether to show two emojis per button.
311    #[serde(default)]
312    pub two_emojis_enabled: bool,
313    /// Mode: "chat", "full", "tasks", "notes", "journal".
314    #[serde(default = "default_mode")]
315    pub mode: String,
316    /// Whether quick habits are enabled.
317    #[serde(default)]
318    pub quick_habits_enabled: bool,
319    /// Associated channel IDs.
320    #[serde(default)]
321    pub channels: Vec<i64>,
322}
323
324fn default_language() -> String {
325    "en".to_string()
326}
327fn default_timezone() -> String {
328    "UTC".to_string()
329}
330fn default_pomodoro_duration() -> i64 {
331    50
332}
333fn default_mode() -> String {
334    "full".to_string()
335}
336
337impl Default for KnowledgeConfig {
338    fn default() -> Self {
339        Self {
340            language: default_language(),
341            timezone: default_timezone(),
342            move_to_commands: vec![],
343            pomodoro_duration_in_minutes: default_pomodoro_duration(),
344            schedules: vec![],
345            quick_commands: vec![],
346            two_emojis_enabled: false,
347            mode: default_mode(),
348            quick_habits_enabled: false,
349            channels: vec![],
350        }
351    }
352}
353
354/// Chat/Inbox mode constants.
355pub const MODE_CHAT: &str = "chat";
356/// Full mode constant.
357pub const MODE_FULL: &str = "full";
358/// Tasks-only mode constant.
359pub const MODE_TASKS: &str = "tasks";
360/// Notes-only mode constant.
361pub const MODE_NOTES: &str = "notes";
362/// Journal-only mode constant.
363pub const MODE_JOURNAL: &str = "journal";