From 8595bb39509cdb5d8fbfe3b682f0fd48f4e3eedf Mon Sep 17 00:00:00 2001 From: kristof Date: Fri, 4 Apr 2025 13:17:40 +0200 Subject: [PATCH] ... --- herodb/Cargo.toml | 4 + herodb/src/cmd/dbexample_mcc/main.rs | 399 ++++++++++++++++++ herodb/src/models/circle/circle.rs | 2 +- herodb/src/models/circle/lib.rs | 2 +- herodb/src/models/circle/mod.rs | 4 +- herodb/src/models/circle/name.rs | 2 +- herodb/src/models/mcc/MCC_ENHANCEMENT_PLAN.md | 316 ++++++++++++++ herodb/src/models/mcc/README.md | 33 ++ herodb/src/models/mcc/calendar.rs | 33 +- herodb/src/models/mcc/contacts.rs | 52 ++- herodb/src/models/mcc/event.rs | 49 ++- herodb/src/models/mcc/lib.rs | 6 +- herodb/src/models/mcc/mail.rs | 82 +++- herodb/src/models/mcc/message.rs | 134 ++++++ herodb/src/models/mcc/mod.rs | 6 +- herodb/src/models/mod.rs | 4 +- 16 files changed, 1112 insertions(+), 16 deletions(-) create mode 100644 herodb/src/cmd/dbexample_mcc/main.rs create mode 100644 herodb/src/models/mcc/MCC_ENHANCEMENT_PLAN.md create mode 100644 herodb/src/models/mcc/message.rs diff --git a/herodb/Cargo.toml b/herodb/Cargo.toml index 143e48e..5d297d8 100644 --- a/herodb/Cargo.toml +++ b/herodb/Cargo.toml @@ -30,3 +30,7 @@ path = "examples/rhai_demo.rs" [[bin]] name = "dbexample2" path = "src/cmd/dbexample2/main.rs" + +[[bin]] +name = "dbexample_mcc" +path = "src/cmd/dbexample_mcc/main.rs" diff --git a/herodb/src/cmd/dbexample_mcc/main.rs b/herodb/src/cmd/dbexample_mcc/main.rs new file mode 100644 index 0000000..18a7d3a --- /dev/null +++ b/herodb/src/cmd/dbexample_mcc/main.rs @@ -0,0 +1,399 @@ +use chrono::{Utc, Duration}; +use herodb::db::DBBuilder; +use herodb::models::mcc::{ + Calendar, Event, + Email, Attachment, Envelope, + Contact, Message +}; +use herodb::models::circle::Circle; +use std::path::PathBuf; +use std::fs; + +fn main() -> Result<(), Box> { + println!("DB Example MCC: Mail, Calendar, Contacts with Group Support"); + println!("======================================================="); + + // Create a temporary directory for the database + let db_path = PathBuf::from("/tmp/dbexample_mcc"); + if db_path.exists() { + fs::remove_dir_all(&db_path)?; + } + fs::create_dir_all(&db_path)?; + println!("Database path: {:?}", db_path); + + // Create a database instance with our models registered + let db = DBBuilder::new(&db_path) + .register_model::() + .register_model::() + .register_model::() + .register_model::() + .register_model::() + .register_model::() + .build()?; + + println!("\n1. Creating Circles (Groups)"); + println!("---------------------------"); + + // Create circles (groups) + let work_circle = Circle::new( + 1, + "Work".to_string(), + "Work-related communications".to_string() + ); + + let family_circle = Circle::new( + 2, + "Family".to_string(), + "Family communications".to_string() + ); + + let friends_circle = Circle::new( + 3, + "Friends".to_string(), + "Friends communications".to_string() + ); + + // Insert circles + db.set::(&work_circle)?; + db.set::(&family_circle)?; + db.set::(&friends_circle)?; + + println!("Created circles:"); + println!(" - Circle #{}: {}", work_circle.id, work_circle.name); + println!(" - Circle #{}: {}", family_circle.id, family_circle.name); + println!(" - Circle #{}: {}", friends_circle.id, friends_circle.name); + + println!("\n2. Creating Contacts with Group Support"); + println!("------------------------------------"); + + // Create contacts + let mut john = Contact::new( + 1, + "John".to_string(), + "Doe".to_string(), + "john.doe@example.com".to_string(), + "work".to_string() + ); + john.add_group(work_circle.id); + + let mut alice = Contact::new( + 2, + "Alice".to_string(), + "Smith".to_string(), + "alice.smith@example.com".to_string(), + "family".to_string() + ); + alice.add_group(family_circle.id); + + let mut bob = Contact::new( + 3, + "Bob".to_string(), + "Johnson".to_string(), + "bob.johnson@example.com".to_string(), + "friends".to_string() + ); + bob.add_group(friends_circle.id); + bob.add_group(work_circle.id); // Bob is both a friend and a work contact + + // Insert contacts + db.set::(&john)?; + db.set::(&alice)?; + db.set::(&bob)?; + + println!("Created contacts:"); + println!(" - {}: {} (Groups: {:?})", john.full_name(), john.email, john.groups); + println!(" - {}: {} (Groups: {:?})", alice.full_name(), alice.email, alice.groups); + println!(" - {}: {} (Groups: {:?})", bob.full_name(), bob.email, bob.groups); + + println!("\n3. Creating Calendars with Group Support"); + println!("-------------------------------------"); + + // Create calendars + let mut work_calendar = Calendar::new( + 1, + "Work Calendar".to_string(), + "Work-related events".to_string() + ); + work_calendar.add_group(work_circle.id); + + let mut personal_calendar = Calendar::new( + 2, + "Personal Calendar".to_string(), + "Personal events".to_string() + ); + personal_calendar.add_group(family_circle.id); + personal_calendar.add_group(friends_circle.id); + + // Insert calendars + db.set::(&work_calendar)?; + db.set::(&personal_calendar)?; + + println!("Created calendars:"); + println!(" - {}: {} (Groups: {:?})", work_calendar.id, work_calendar.title, work_calendar.groups); + println!(" - {}: {} (Groups: {:?})", personal_calendar.id, personal_calendar.title, personal_calendar.groups); + + println!("\n4. Creating Events with Group Support"); + println!("----------------------------------"); + + // Create events + let now = Utc::now(); + let tomorrow = now + Duration::days(1); + let next_week = now + Duration::days(7); + + let mut work_meeting = Event::new( + 1, + work_calendar.id, + "Team Meeting".to_string(), + "Weekly team sync".to_string(), + "Conference Room A".to_string(), + tomorrow, + tomorrow + Duration::hours(1), + "organizer@example.com".to_string() + ); + work_meeting.add_group(work_circle.id); + work_meeting.add_attendee(john.email.clone()); + work_meeting.add_attendee(bob.email.clone()); + + let mut family_dinner = Event::new( + 2, + personal_calendar.id, + "Family Dinner".to_string(), + "Weekly family dinner".to_string(), + "Home".to_string(), + next_week, + next_week + Duration::hours(2), + "me@example.com".to_string() + ); + family_dinner.add_group(family_circle.id); + family_dinner.add_attendee(alice.email.clone()); + + // Insert events + db.set::(&work_meeting)?; + db.set::(&family_dinner)?; + + println!("Created events:"); + println!(" - {}: {} on {} (Groups: {:?})", + work_meeting.id, + work_meeting.title, + work_meeting.start_time.format("%Y-%m-%d %H:%M"), + work_meeting.groups + ); + println!(" - {}: {} on {} (Groups: {:?})", + family_dinner.id, + family_dinner.title, + family_dinner.start_time.format("%Y-%m-%d %H:%M"), + family_dinner.groups + ); + + println!("\n5. Creating Emails with Group Support"); + println!("----------------------------------"); + + // Create emails + let mut work_email = Email::new( + 1, + 101, + 1, + "INBOX".to_string(), + "Here are the meeting notes from yesterday's discussion.".to_string() + ); + work_email.add_group(work_circle.id); + + let work_attachment = Attachment { + filename: "meeting_notes.pdf".to_string(), + content_type: "application/pdf".to_string(), + hash: "abc123def456".to_string(), + size: 1024, + }; + work_email.add_attachment(work_attachment); + + let work_envelope = Envelope { + date: now.timestamp(), + subject: "Meeting Notes".to_string(), + from: vec!["john.doe@example.com".to_string()], + sender: vec!["john.doe@example.com".to_string()], + reply_to: vec!["john.doe@example.com".to_string()], + to: vec!["me@example.com".to_string()], + cc: vec!["bob.johnson@example.com".to_string()], + bcc: vec![], + in_reply_to: "".to_string(), + message_id: "msg123@example.com".to_string(), + }; + work_email.set_envelope(work_envelope); + + let mut family_email = Email::new( + 2, + 102, + 2, + "INBOX".to_string(), + "Looking forward to seeing you at dinner next week!".to_string() + ); + family_email.add_group(family_circle.id); + + let family_envelope = Envelope { + date: now.timestamp(), + subject: "Family Dinner".to_string(), + from: vec!["alice.smith@example.com".to_string()], + sender: vec!["alice.smith@example.com".to_string()], + reply_to: vec!["alice.smith@example.com".to_string()], + to: vec!["me@example.com".to_string()], + cc: vec![], + bcc: vec![], + in_reply_to: "".to_string(), + message_id: "msg456@example.com".to_string(), + }; + family_email.set_envelope(family_envelope); + + // Insert emails + db.set::(&work_email)?; + db.set::(&family_email)?; + + println!("Created emails:"); + println!(" - From: {}, Subject: {} (Groups: {:?})", + work_email.envelope.as_ref().unwrap().from[0], + work_email.envelope.as_ref().unwrap().subject, + work_email.groups + ); + println!(" - From: {}, Subject: {} (Groups: {:?})", + family_email.envelope.as_ref().unwrap().from[0], + family_email.envelope.as_ref().unwrap().subject, + family_email.groups + ); + + println!("\n6. Creating Messages (Chat) with Group Support"); + println!("-----------------------------------------"); + + // Create messages + let mut work_chat = Message::new( + 1, + "thread_work_123".to_string(), + "john.doe@example.com".to_string(), + "Can we move the meeting to 3pm?".to_string() + ); + work_chat.add_group(work_circle.id); + work_chat.add_recipient("me@example.com".to_string()); + work_chat.add_recipient("bob.johnson@example.com".to_string()); + + let mut friends_chat = Message::new( + 2, + "thread_friends_456".to_string(), + "bob.johnson@example.com".to_string(), + "Are we still on for the game this weekend?".to_string() + ); + friends_chat.add_group(friends_circle.id); + friends_chat.add_recipient("me@example.com".to_string()); + friends_chat.add_reaction("👍".to_string()); + + // Insert messages + db.set::(&work_chat)?; + db.set::(&friends_chat)?; + + println!("Created messages:"); + println!(" - From: {}, Content: {} (Groups: {:?})", + work_chat.sender_id, + work_chat.content, + work_chat.groups + ); + println!(" - From: {}, Content: {} (Groups: {:?}, Reactions: {:?})", + friends_chat.sender_id, + friends_chat.content, + friends_chat.groups, + friends_chat.meta.reactions + ); + + println!("\n7. Demonstrating Utility Methods"); + println!("------------------------------"); + + // Filter contacts by group + println!("\nFiltering contacts by work group (ID: {}):", work_circle.id); + let all_contacts = db.list::()?; + for contact in all_contacts { + if contact.filter_by_groups(&[work_circle.id]) { + println!(" - {} ({})", contact.full_name(), contact.email); + } + } + + // Search emails by subject + println!("\nSearching emails with subject containing 'Meeting':"); + let all_emails = db.list::()?; + for email in all_emails { + if email.search_by_subject("Meeting") { + println!(" - Subject: {}, From: {}", + email.envelope.as_ref().unwrap().subject, + email.envelope.as_ref().unwrap().from[0] + ); + } + } + + // Get events for a calendar + println!("\nGetting events for Work Calendar (ID: {}):", work_calendar.id); + let all_events = db.list::()?; + let work_events: Vec = all_events + .into_iter() + .filter(|event| event.calendar_id == work_calendar.id) + .collect(); + for event in work_events { + println!(" - {}: {} on {}", + event.id, + event.title, + event.start_time.format("%Y-%m-%d %H:%M") + ); + } + + // Get attendee contacts for an event + println!("\nGetting attendee contacts for Team Meeting (ID: {}):", work_meeting.id); + let all_contacts = db.list::()?; + let attendee_contacts: Vec = all_contacts + .into_iter() + .filter(|contact| work_meeting.attendees.contains(&contact.email)) + .collect(); + for contact in attendee_contacts { + println!(" - {} ({})", contact.full_name(), contact.email); + } + + // Convert email to message + println!("\nConverting work email to message:"); + let email_to_message = work_email.to_message(3, "thread_converted_789".to_string()); + println!(" - Original Email Subject: {}", work_email.envelope.as_ref().unwrap().subject); + println!(" - Converted Message Content: {}", email_to_message.content.split('\n').next().unwrap_or("")); + println!(" - Converted Message Groups: {:?}", email_to_message.groups); + + // Insert the converted message + db.set::(&email_to_message)?; + + println!("\n8. Relationship Management"); + println!("------------------------"); + + // Get the calendar for an event + println!("\nGetting calendar for Family Dinner event (ID: {}):", family_dinner.id); + let event_calendar = db.get::(&family_dinner.calendar_id.to_string())?; + println!(" - Calendar: {} ({})", event_calendar.title, event_calendar.description); + + // Get events for a contact + println!("\nGetting events where John Doe is an attendee:"); + let all_events = db.list::()?; + let john_events: Vec = all_events + .into_iter() + .filter(|event| event.attendees.contains(&john.email)) + .collect(); + for event in john_events { + println!(" - {}: {} on {}", + event.id, + event.title, + event.start_time.format("%Y-%m-%d %H:%M") + ); + } + + // Get messages in the same thread + println!("\nGetting all messages in the work chat thread:"); + let all_messages = db.list::()?; + let thread_messages: Vec = all_messages + .into_iter() + .filter(|message| message.thread_id == work_chat.thread_id) + .collect(); + for message in thread_messages { + println!(" - From: {}, Content: {}", message.sender_id, message.content); + } + + println!("\nExample completed successfully!"); + Ok(()) +} \ No newline at end of file diff --git a/herodb/src/models/circle/circle.rs b/herodb/src/models/circle/circle.rs index c8ddb94..a8d521d 100644 --- a/herodb/src/models/circle/circle.rs +++ b/herodb/src/models/circle/circle.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use crate::core::{SledModel, Storable}; +use crate::db::{SledModel, Storable}; use std::collections::HashMap; /// Role represents the role of a member in a circle diff --git a/herodb/src/models/circle/lib.rs b/herodb/src/models/circle/lib.rs index 2777132..61044c0 100644 --- a/herodb/src/models/circle/lib.rs +++ b/herodb/src/models/circle/lib.rs @@ -6,4 +6,4 @@ pub use circle::{Circle, Member, Role}; pub use name::{Name, Record, RecordType}; // Re-export database components -pub use crate::core::{SledDB, SledDBError, SledDBResult, Storable, SledModel, DB}; +pub use crate::db::{SledDB, SledDBError, SledDBResult, Storable, SledModel, DB}; diff --git a/herodb/src/models/circle/mod.rs b/herodb/src/models/circle/mod.rs index 79acec6..b47d562 100644 --- a/herodb/src/models/circle/mod.rs +++ b/herodb/src/models/circle/mod.rs @@ -5,5 +5,5 @@ pub mod name; pub use circle::{Circle, Member, Role}; pub use name::{Name, Record, RecordType}; -// Re-export database components from core module -pub use crate::core::{SledDB, SledDBError, SledDBResult, Storable, SledModel, DB}; +// Re-export database components from db module +pub use crate::db::{SledDB, SledDBError, SledDBResult, Storable, SledModel, DB}; diff --git a/herodb/src/models/circle/name.rs b/herodb/src/models/circle/name.rs index bbcf70f..953d115 100644 --- a/herodb/src/models/circle/name.rs +++ b/herodb/src/models/circle/name.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use crate::core::{SledModel, Storable}; +use crate::db::{SledModel, Storable}; /// Record types for a DNS record #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/herodb/src/models/mcc/MCC_ENHANCEMENT_PLAN.md b/herodb/src/models/mcc/MCC_ENHANCEMENT_PLAN.md new file mode 100644 index 0000000..e3ca985 --- /dev/null +++ b/herodb/src/models/mcc/MCC_ENHANCEMENT_PLAN.md @@ -0,0 +1,316 @@ +# MCC Models Enhancement Plan + +## 1. Current State Analysis + +The current MCC module consists of: +- **Mail**: Email, Attachment, Envelope models +- **Calendar**: Calendar model +- **Event**: Event, EventMeta models +- **Contacts**: Contact model + +All models implement the `Storable` and `SledModel` traits for database integration. + +## 2. Planned Enhancements + +### 2.1 Add Group Support to All Models + +Add a `groups: Vec` field to each model to enable linking to multiple groups defined in the Circle module. + +### 2.2 Create New Message Model + +Create a new `message.rs` file with a Message model for chat functionality: +- Different structure from Email +- Include thread_id, sender_id, content fields +- Include metadata for chat-specific features +- Implement Storable and SledModel traits + +### 2.3 Add Utility Methods + +Add utility methods to each model for: +- **Filtering/Searching**: Methods to filter by groups, search by content/subject +- **Format Conversion**: Methods to convert between formats (e.g., Email to Message) +- **Relationship Management**: Methods to manage relationships between models + +## 3. Implementation Plan + +```mermaid +flowchart TD + A[Review Current Models] --> B[Add groups field to all models] + B --> C[Create Message model] + C --> D[Add utility methods] + D --> E[Update mod.rs and lib.rs] + E --> F[Update README.md] +``` + +### 3.1 Detailed Changes + +#### 3.1.1 Mail Model (`mail.rs`) + +- Add `groups: Vec` field to `Email` struct +- Add utility methods: + - `filter_by_groups(groups: &[u32]) -> bool` + - `search_by_subject(query: &str) -> bool` + - `search_by_content(query: &str) -> bool` + - `to_message(&self) -> Message` (conversion method) + +#### 3.1.2 Calendar Model (`calendar.rs`) + +- Add `groups: Vec` field to `Calendar` struct +- Add utility methods: + - `filter_by_groups(groups: &[u32]) -> bool` + - `get_events(&self, db: &SledDB) -> SledDBResult>` (relationship method) + +#### 3.1.3 Event Model (`event.rs`) + +- Add `groups: Vec` field to `Event` struct +- Add utility methods: + - `filter_by_groups(groups: &[u32]) -> bool` + - `get_calendar(&self, db: &SledDB) -> SledDBResult` (relationship method) + - `get_attendee_contacts(&self, db: &SledDB) -> SledDBResult>` (relationship method) + +#### 3.1.4 Contacts Model (`contacts.rs`) + +- Add `groups: Vec` field to `Contact` struct +- Add utility methods: + - `filter_by_groups(groups: &[u32]) -> bool` + - `search_by_name(query: &str) -> bool` + - `search_by_email(query: &str) -> bool` + - `get_events(&self, db: &SledDB) -> SledDBResult>` (relationship method) + +#### 3.1.5 New Message Model (`message.rs`) + +```rust +use serde::{Deserialize, Serialize}; +use crate::core::{SledModel, Storable}; +use chrono::{DateTime, Utc}; + +/// MessageStatus represents the status of a message +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum MessageStatus { + Sent, + Delivered, + Read, + Failed, +} + +/// MessageMeta contains metadata for a chat message +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MessageMeta { + pub created_at: DateTime, + pub updated_at: DateTime, + pub status: MessageStatus, + pub is_edited: bool, + pub reactions: Vec, +} + +/// Message represents a chat message +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Message { + pub id: u32, // Unique identifier + pub thread_id: String, // Thread/conversation identifier + pub sender_id: String, // Sender identifier + pub recipients: Vec, // List of recipient identifiers + pub content: String, // Message content + pub attachments: Vec, // References to attachments + pub groups: Vec, // Groups this message belongs to + pub meta: MessageMeta, // Message metadata +} + +impl Message { + /// Create a new message + pub fn new(id: u32, thread_id: String, sender_id: String, content: String) -> Self { + let now = Utc::now(); + Self { + id, + thread_id, + sender_id, + recipients: Vec::new(), + content, + attachments: Vec::new(), + groups: Vec::new(), + meta: MessageMeta { + created_at: now, + updated_at: now, + status: MessageStatus::Sent, + is_edited: false, + reactions: Vec::new(), + }, + } + } + + /// Add a recipient to this message + pub fn add_recipient(&mut self, recipient: String) { + self.recipients.push(recipient); + } + + /// Add an attachment to this message + pub fn add_attachment(&mut self, attachment: String) { + self.attachments.push(attachment); + } + + /// Add a group to this message + pub fn add_group(&mut self, group_id: u32) { + if !self.groups.contains(&group_id) { + self.groups.push(group_id); + } + } + + /// Filter by groups + pub fn filter_by_groups(&self, groups: &[u32]) -> bool { + groups.iter().any(|g| self.groups.contains(g)) + } + + /// Search by content + pub fn search_by_content(&self, query: &str) -> bool { + self.content.to_lowercase().contains(&query.to_lowercase()) + } + + /// Update message status + pub fn update_status(&mut self, status: MessageStatus) { + self.meta.status = status; + self.meta.updated_at = Utc::now(); + } + + /// Edit message content + pub fn edit_content(&mut self, new_content: String) { + self.content = new_content; + self.meta.is_edited = true; + self.meta.updated_at = Utc::now(); + } + + /// Add a reaction to the message + pub fn add_reaction(&mut self, reaction: String) { + self.meta.reactions.push(reaction); + self.meta.updated_at = Utc::now(); + } +} + +// Implement Storable trait (provides default dump/load) +impl Storable for Message {} + +// Implement SledModel trait +impl SledModel for Message { + fn get_id(&self) -> String { + self.id.to_string() + } + + fn db_prefix() -> &'static str { + "message" + } +} +``` + +#### 3.1.6 Update Module Files + +Update `mod.rs` and `lib.rs` to include the new Message model. + +#### 3.1.7 Update README.md + +Update the README.md to include information about the Message model and the new utility methods. + +## 4. Data Model Diagram + +```mermaid +classDiagram + class Email { + +u32 id + +u32 uid + +u32 seq_num + +String mailbox + +String message + +Vec~Attachment~ attachments + +Vec~String~ flags + +i64 receivetime + +Option~Envelope~ envelope + +Vec~u32~ groups + +filter_by_groups() + +search_by_subject() + +search_by_content() + +to_message() + } + + class Calendar { + +u32 id + +String title + +String description + +Vec~u32~ groups + +filter_by_groups() + +get_events() + } + + class Event { + +u32 id + +u32 calendar_id + +String title + +String description + +String location + +DateTime start_time + +DateTime end_time + +bool all_day + +String recurrence + +Vec~String~ attendees + +String organizer + +String status + +EventMeta meta + +Vec~u32~ groups + +filter_by_groups() + +get_calendar() + +get_attendee_contacts() + } + + class Contact { + +u32 id + +i64 created_at + +i64 modified_at + +String first_name + +String last_name + +String email + +String group + +Vec~u32~ groups + +filter_by_groups() + +search_by_name() + +search_by_email() + +get_events() + } + + class Message { + +u32 id + +String thread_id + +String sender_id + +Vec~String~ recipients + +String content + +Vec~String~ attachments + +Vec~u32~ groups + +MessageMeta meta + +filter_by_groups() + +search_by_content() + +update_status() + +edit_content() + +add_reaction() + } + + class Circle { + +u32 id + +String name + +String description + +Vec~Member~ members + } + + Calendar "1" -- "many" Event: contains + Contact "many" -- "many" Event: attends + Circle "1" -- "many" Email: groups + Circle "1" -- "many" Calendar: groups + Circle "1" -- "many" Event: groups + Circle "1" -- "many" Contact: groups + Circle "1" -- "many" Message: groups +``` + +## 5. Testing Strategy + +1. Unit tests for each model to verify: + - Group field functionality + - New utility methods + - Serialization/deserialization with the new fields +2. Integration tests to verify: + - Database operations with the updated models + - Relationships between models \ No newline at end of file diff --git a/herodb/src/models/mcc/README.md b/herodb/src/models/mcc/README.md index cd42bad..7be5fb3 100644 --- a/herodb/src/models/mcc/README.md +++ b/herodb/src/models/mcc/README.md @@ -21,6 +21,14 @@ The Mail models provide email and IMAP functionality: - **Attachment**: Represents a file attachment with file information - **Envelope**: Represents an IMAP envelope structure with message headers +### Message (`message.rs`) + +The Message models provide chat functionality: + +- **Message**: Main struct for chat messages with thread and recipient information +- **MessageMeta**: Contains metadata for message status, editing, and reactions +- **MessageStatus**: Enum representing the status of a message (Sent, Delivered, Read, Failed) + ### Calendar (`calendar.rs`) The Calendar model represents a container for calendar events: @@ -40,6 +48,31 @@ The Contacts model provides contact management: - **Contact**: Main struct for contact information with personal details and grouping +## Group Support + +All models now support linking to multiple groups (Circle IDs): + +- Each model has a `groups: Vec` field to store multiple group IDs +- Utility methods for adding, removing, and filtering by groups +- Groups are defined in the Circle module + +## Utility Methods + +Each model provides utility methods for: + +### Filtering/Searching +- `filter_by_groups(groups: &[u32]) -> bool`: Filter by groups +- `search_by_subject/content/name/email(query: &str) -> bool`: Search by various fields + +### Format Conversion +- `to_message()`: Convert Email to Message + +### Relationship Management +- `get_events()`: Get events associated with a calendar or contact +- `get_calendar()`: Get the calendar an event belongs to +- `get_attendee_contacts()`: Get contacts for event attendees +- `get_thread_messages()`: Get all messages in the same thread + ## Usage These models are used by the MCC module to manage emails, calendar events, and contacts. They are typically accessed through the database handlers that implement the generic SledDB interface. diff --git a/herodb/src/models/mcc/calendar.rs b/herodb/src/models/mcc/calendar.rs index cd5acdc..816ae08 100644 --- a/herodb/src/models/mcc/calendar.rs +++ b/herodb/src/models/mcc/calendar.rs @@ -1,5 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::core::{SledModel, Storable}; +use crate::db::{SledModel, Storable, SledDB, SledDBResult}; +use crate::models::mcc::event::Event; /// Calendar represents a calendar container for events #[derive(Debug, Clone, Serialize, Deserialize)] @@ -7,6 +8,7 @@ pub struct Calendar { pub id: u32, // Unique identifier pub title: String, // Calendar title pub description: String, // Calendar details + pub groups: Vec, // Groups this calendar belongs to (references Circle IDs) } impl Calendar { @@ -16,8 +18,37 @@ impl Calendar { id, title, description, + groups: Vec::new(), } } + + /// Add a group to this calendar + pub fn add_group(&mut self, group_id: u32) { + if !self.groups.contains(&group_id) { + self.groups.push(group_id); + } + } + + /// Remove a group from this calendar + pub fn remove_group(&mut self, group_id: u32) { + self.groups.retain(|&id| id != group_id); + } + + /// Filter by groups - returns true if this calendar belongs to any of the specified groups + pub fn filter_by_groups(&self, groups: &[u32]) -> bool { + groups.iter().any(|g| self.groups.contains(g)) + } + + /// Get all events associated with this calendar + pub fn get_events(&self, db: &SledDB) -> SledDBResult> { + let all_events = db.list()?; + let calendar_events = all_events + .into_iter() + .filter(|event| event.calendar_id == self.id) + .collect(); + + Ok(calendar_events) + } } // Implement Storable trait (provides default dump/load) diff --git a/herodb/src/models/mcc/contacts.rs b/herodb/src/models/mcc/contacts.rs index bc6b681..0854d9e 100644 --- a/herodb/src/models/mcc/contacts.rs +++ b/herodb/src/models/mcc/contacts.rs @@ -1,6 +1,7 @@ use serde::{Deserialize, Serialize}; -use crate::core::{SledModel, Storable}; -use chrono::{DateTime, Utc}; +use crate::db::{SledModel, Storable, SledDB, SledDBResult}; +use crate::models::mcc::event::Event; +use chrono::Utc; /// Contact represents a contact entry in an address book #[derive(Debug, Clone, Serialize, Deserialize)] @@ -14,6 +15,7 @@ pub struct Contact { pub last_name: String, pub email: String, pub group: String, // Reference to a dns name, each group has a globally unique dns + pub groups: Vec, // Groups this contact belongs to (references Circle IDs) } impl Contact { @@ -28,9 +30,49 @@ impl Contact { last_name, email, group, + groups: Vec::new(), } } + /// Add a group to this contact + pub fn add_group(&mut self, group_id: u32) { + if !self.groups.contains(&group_id) { + self.groups.push(group_id); + } + } + + /// Remove a group from this contact + pub fn remove_group(&mut self, group_id: u32) { + self.groups.retain(|&id| id != group_id); + } + + /// Filter by groups - returns true if this contact belongs to any of the specified groups + pub fn filter_by_groups(&self, groups: &[u32]) -> bool { + groups.iter().any(|g| self.groups.contains(g)) + } + + /// Search by name - returns true if the name contains the query (case-insensitive) + pub fn search_by_name(&self, query: &str) -> bool { + let full_name = self.full_name().to_lowercase(); + query.to_lowercase().split_whitespace().all(|word| full_name.contains(word)) + } + + /// Search by email - returns true if the email contains the query (case-insensitive) + pub fn search_by_email(&self, query: &str) -> bool { + self.email.to_lowercase().contains(&query.to_lowercase()) + } + + /// Get events where this contact is an attendee + pub fn get_events(&self, db: &SledDB) -> SledDBResult> { + let all_events = db.list()?; + let contact_events = all_events + .into_iter() + .filter(|event| event.attendees.contains(&self.email)) + .collect(); + + Ok(contact_events) + } + /// Update the contact's information pub fn update(&mut self, first_name: Option, last_name: Option, email: Option, group: Option) { if let Some(first_name) = first_name { @@ -52,6 +94,12 @@ impl Contact { self.modified_at = Utc::now().timestamp(); } + /// Update the contact's groups + pub fn update_groups(&mut self, groups: Vec) { + self.groups = groups; + self.modified_at = Utc::now().timestamp(); + } + /// Get the full name of the contact pub fn full_name(&self) -> String { format!("{} {}", self.first_name, self.last_name) diff --git a/herodb/src/models/mcc/event.rs b/herodb/src/models/mcc/event.rs index dcfedc8..e79e56b 100644 --- a/herodb/src/models/mcc/event.rs +++ b/herodb/src/models/mcc/event.rs @@ -1,5 +1,7 @@ use serde::{Deserialize, Serialize}; -use crate::core::{SledModel, Storable}; +use crate::db::{SledModel, Storable, SledDB, SledDBResult}; +use crate::models::mcc::calendar::Calendar; +use crate::models::mcc::contacts::Contact; use chrono::{DateTime, Utc}; /// EventMeta contains additional metadata for a calendar event @@ -27,6 +29,7 @@ pub struct Event { pub organizer: String, // Organizer email pub status: String, // "CONFIRMED", "CANCELLED", "TENTATIVE" pub meta: EventMeta, // Additional metadata + pub groups: Vec, // Groups this event belongs to (references Circle IDs) } impl Event { @@ -60,9 +63,43 @@ impl Event { etag: String::new(), color: String::new(), }, + groups: Vec::new(), } } + /// Add a group to this event + pub fn add_group(&mut self, group_id: u32) { + if !self.groups.contains(&group_id) { + self.groups.push(group_id); + } + } + + /// Remove a group from this event + pub fn remove_group(&mut self, group_id: u32) { + self.groups.retain(|&id| id != group_id); + } + + /// Filter by groups - returns true if this event belongs to any of the specified groups + pub fn filter_by_groups(&self, groups: &[u32]) -> bool { + groups.iter().any(|g| self.groups.contains(g)) + } + + /// Get the calendar this event belongs to + pub fn get_calendar(&self, db: &SledDB) -> SledDBResult { + db.get(&self.calendar_id.to_string()) + } + + /// Get contacts for all attendees of this event + pub fn get_attendee_contacts(&self, db: &SledDB) -> SledDBResult> { + let all_contacts = db.list()?; + let attendee_contacts = all_contacts + .into_iter() + .filter(|contact| self.attendees.contains(&contact.email)) + .collect(); + + Ok(attendee_contacts) + } + /// Add an attendee to this event pub fn add_attendee(&mut self, attendee: String) { self.attendees.push(attendee); @@ -77,6 +114,16 @@ impl Event { pub fn set_status(&mut self, status: &str) { self.status = status.to_string(); } + + /// Search by title - returns true if the title contains the query (case-insensitive) + pub fn search_by_title(&self, query: &str) -> bool { + self.title.to_lowercase().contains(&query.to_lowercase()) + } + + /// Search by description - returns true if the description contains the query (case-insensitive) + pub fn search_by_description(&self, query: &str) -> bool { + self.description.to_lowercase().contains(&query.to_lowercase()) + } } // Implement Storable trait (provides default dump/load) diff --git a/herodb/src/models/mcc/lib.rs b/herodb/src/models/mcc/lib.rs index 86cd3d7..11ae716 100644 --- a/herodb/src/models/mcc/lib.rs +++ b/herodb/src/models/mcc/lib.rs @@ -2,12 +2,14 @@ pub mod calendar; pub mod event; pub mod mail; pub mod contacts; +pub mod message; // Re-export all model types for convenience pub use calendar::Calendar; pub use event::{Event, EventMeta}; pub use mail::{Email, Attachment, Envelope}; pub use contacts::Contact; +pub use message::{Message, MessageMeta, MessageStatus}; -// Re-export database components from core module -pub use crate::core::{SledDB, SledDBError, SledDBResult, Storable, SledModel, DB}; \ No newline at end of file +// Re-export database components from db module +pub use crate::db::{SledDB, SledDBError, SledDBResult, Storable, SledModel, DB}; \ No newline at end of file diff --git a/herodb/src/models/mcc/mail.rs b/herodb/src/models/mcc/mail.rs index 16f7c71..68912e0 100644 --- a/herodb/src/models/mcc/mail.rs +++ b/herodb/src/models/mcc/mail.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::core::{SledModel, Storable}; -use chrono::{DateTime, Utc}; +use crate::db::{SledModel, Storable, SledDBResult, SledDB}; +use chrono::Utc; /// Email represents an email message with all its metadata and content #[derive(Debug, Clone, Serialize, Deserialize)] @@ -17,6 +17,7 @@ pub struct Email { pub flags: Vec, // IMAP flags like \Seen, \Deleted, etc. pub receivetime: i64, // Unix timestamp when the email was received pub envelope: Option, // IMAP envelope information (contains From, To, Subject, etc.) + pub groups: Vec, // Groups this email belongs to (references Circle IDs) } /// Attachment represents an email attachment @@ -56,6 +57,7 @@ impl Email { flags: Vec::new(), receivetime: chrono::Utc::now().timestamp(), envelope: None, + groups: Vec::new(), } } @@ -64,10 +66,86 @@ impl Email { self.attachments.push(attachment); } + /// Add a group to this email + pub fn add_group(&mut self, group_id: u32) { + if !self.groups.contains(&group_id) { + self.groups.push(group_id); + } + } + + /// Remove a group from this email + pub fn remove_group(&mut self, group_id: u32) { + self.groups.retain(|&id| id != group_id); + } + + /// Filter by groups - returns true if this email belongs to any of the specified groups + pub fn filter_by_groups(&self, groups: &[u32]) -> bool { + groups.iter().any(|g| self.groups.contains(g)) + } + + /// Search by subject - returns true if the subject contains the query (case-insensitive) + pub fn search_by_subject(&self, query: &str) -> bool { + if let Some(env) = &self.envelope { + env.subject.to_lowercase().contains(&query.to_lowercase()) + } else { + false + } + } + + /// Search by content - returns true if the message content contains the query (case-insensitive) + pub fn search_by_content(&self, query: &str) -> bool { + self.message.to_lowercase().contains(&query.to_lowercase()) + } + /// Set the envelope for this email pub fn set_envelope(&mut self, envelope: Envelope) { self.envelope = Some(envelope); } + + /// Convert this email to a Message (for chat) + pub fn to_message(&self, id: u32, thread_id: String) -> crate::models::mcc::message::Message { + use crate::models::mcc::message::Message; + + let now = Utc::now(); + let sender = if let Some(env) = &self.envelope { + if !env.from.is_empty() { + env.from[0].clone() + } else { + "unknown@example.com".to_string() + } + } else { + "unknown@example.com".to_string() + }; + + let subject = if let Some(env) = &self.envelope { + env.subject.clone() + } else { + "No Subject".to_string() + }; + + let recipients = if let Some(env) = &self.envelope { + env.to.clone() + } else { + Vec::new() + }; + + let content = if !subject.is_empty() { + format!("{}\n\n{}", subject, self.message) + } else { + self.message.clone() + }; + + let mut message = Message::new(id, thread_id, sender, content); + message.recipients = recipients; + message.groups = self.groups.clone(); + + // Convert attachments to references + for attachment in &self.attachments { + message.add_attachment(attachment.filename.clone()); + } + + message + } } // Implement Storable trait (provides default dump/load) diff --git a/herodb/src/models/mcc/message.rs b/herodb/src/models/mcc/message.rs new file mode 100644 index 0000000..5728453 --- /dev/null +++ b/herodb/src/models/mcc/message.rs @@ -0,0 +1,134 @@ +use serde::{Deserialize, Serialize}; +use crate::db::{SledModel, Storable, SledDB, SledDBResult}; +use chrono::{DateTime, Utc}; + +/// MessageStatus represents the status of a message +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum MessageStatus { + Sent, + Delivered, + Read, + Failed, +} + +/// MessageMeta contains metadata for a chat message +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MessageMeta { + pub created_at: DateTime, + pub updated_at: DateTime, + pub status: MessageStatus, + pub is_edited: bool, + pub reactions: Vec, +} + +/// Message represents a chat message +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Message { + pub id: u32, // Unique identifier + pub thread_id: String, // Thread/conversation identifier + pub sender_id: String, // Sender identifier + pub recipients: Vec, // List of recipient identifiers + pub content: String, // Message content + pub attachments: Vec, // References to attachments + pub groups: Vec, // Groups this message belongs to (references Circle IDs) + pub meta: MessageMeta, // Message metadata +} + +impl Message { + /// Create a new message + pub fn new(id: u32, thread_id: String, sender_id: String, content: String) -> Self { + let now = Utc::now(); + Self { + id, + thread_id, + sender_id, + recipients: Vec::new(), + content, + attachments: Vec::new(), + groups: Vec::new(), + meta: MessageMeta { + created_at: now, + updated_at: now, + status: MessageStatus::Sent, + is_edited: false, + reactions: Vec::new(), + }, + } + } + + /// Add a recipient to this message + pub fn add_recipient(&mut self, recipient: String) { + self.recipients.push(recipient); + } + + /// Add an attachment to this message + pub fn add_attachment(&mut self, attachment: String) { + self.attachments.push(attachment); + } + + /// Add a group to this message + pub fn add_group(&mut self, group_id: u32) { + if !self.groups.contains(&group_id) { + self.groups.push(group_id); + } + } + + /// Remove a group from this message + pub fn remove_group(&mut self, group_id: u32) { + self.groups.retain(|&id| id != group_id); + } + + /// Filter by groups - returns true if this message belongs to any of the specified groups + pub fn filter_by_groups(&self, groups: &[u32]) -> bool { + groups.iter().any(|g| self.groups.contains(g)) + } + + /// Search by content - returns true if the content contains the query (case-insensitive) + pub fn search_by_content(&self, query: &str) -> bool { + self.content.to_lowercase().contains(&query.to_lowercase()) + } + + /// Update message status + pub fn update_status(&mut self, status: MessageStatus) { + self.meta.status = status; + self.meta.updated_at = Utc::now(); + } + + /// Edit message content + pub fn edit_content(&mut self, new_content: String) { + self.content = new_content; + self.meta.is_edited = true; + self.meta.updated_at = Utc::now(); + } + + /// Add a reaction to the message + pub fn add_reaction(&mut self, reaction: String) { + self.meta.reactions.push(reaction); + self.meta.updated_at = Utc::now(); + } + + /// Get all messages in the same thread + pub fn get_thread_messages(&self, db: &SledDB) -> SledDBResult> { + let all_messages = db.list()?; + let thread_messages = all_messages + .into_iter() + .filter(|msg| msg.thread_id == self.thread_id) + .collect(); + + Ok(thread_messages) + } +} + +// Implement Storable trait (provides default dump/load) +impl Storable for Message {} + +// Implement SledModel trait +impl SledModel for Message { + fn get_id(&self) -> String { + self.id.to_string() + } + + fn db_prefix() -> &'static str { + "message" + } +} \ No newline at end of file diff --git a/herodb/src/models/mcc/mod.rs b/herodb/src/models/mcc/mod.rs index 86cd3d7..11ae716 100644 --- a/herodb/src/models/mcc/mod.rs +++ b/herodb/src/models/mcc/mod.rs @@ -2,12 +2,14 @@ pub mod calendar; pub mod event; pub mod mail; pub mod contacts; +pub mod message; // Re-export all model types for convenience pub use calendar::Calendar; pub use event::{Event, EventMeta}; pub use mail::{Email, Attachment, Envelope}; pub use contacts::Contact; +pub use message::{Message, MessageMeta, MessageStatus}; -// Re-export database components from core module -pub use crate::core::{SledDB, SledDBError, SledDBResult, Storable, SledModel, DB}; \ No newline at end of file +// Re-export database components from db module +pub use crate::db::{SledDB, SledDBError, SledDBResult, Storable, SledModel, DB}; \ No newline at end of file diff --git a/herodb/src/models/mod.rs b/herodb/src/models/mod.rs index 574868c..d195478 100644 --- a/herodb/src/models/mod.rs +++ b/herodb/src/models/mod.rs @@ -1 +1,3 @@ -pub mod biz; \ No newline at end of file +pub mod biz; +pub mod mcc; +pub mod circle; \ No newline at end of file