Private
Public Access
1
0

Add 'core/' from commit 'b0dfc4146ca0da535a87f8509aec68817fb2ab14'

git-subtree-dir: core
git-subtree-mainline: a07f3dcd23
git-subtree-split: b0dfc4146c
This commit is contained in:
2025-09-06 19:33:33 -07:00
83 changed files with 12352 additions and 0 deletions

View File

@@ -0,0 +1,142 @@
use crate::models::{message::Message, participant::Participant};
use chrono::{DateTime, NaiveDateTime};
use uuid::Uuid;
#[derive(Clone, Debug)]
pub struct Conversation {
pub guid: String,
pub unread_count: u16,
pub display_name: Option<String>,
pub last_message_preview: Option<String>,
pub date: NaiveDateTime,
pub participants: Vec<Participant>,
}
impl Conversation {
pub fn builder() -> ConversationBuilder {
ConversationBuilder::new()
}
pub fn into_builder(&self) -> ConversationBuilder {
ConversationBuilder {
guid: Some(self.guid.clone()),
date: self.date,
participants: None,
unread_count: Some(self.unread_count),
last_message_preview: self.last_message_preview.clone(),
display_name: self.display_name.clone(),
}
}
pub fn merge(&self, other: &Conversation, last_message: Option<&Message>) -> Conversation {
let mut new_conversation = self.clone();
new_conversation.unread_count = other.unread_count;
new_conversation.participants = other.participants.clone();
new_conversation.display_name = other.display_name.clone();
if let Some(last_message) = last_message {
if last_message.date > self.date {
new_conversation.date = last_message.date;
}
if !last_message.text.is_empty() && !last_message.text.trim().is_empty() {
new_conversation.last_message_preview = Some(last_message.text.clone());
}
}
new_conversation
}
}
impl PartialEq for Conversation {
fn eq(&self, other: &Self) -> bool {
self.guid == other.guid
&& self.unread_count == other.unread_count
&& self.display_name == other.display_name
&& self.last_message_preview == other.last_message_preview
&& self.date == other.date
&& self.participants == other.participants
}
}
impl From<kordophone::model::Conversation> for Conversation {
fn from(value: kordophone::model::Conversation) -> Self {
Self {
guid: value.guid,
unread_count: u16::try_from(value.unread_count).unwrap(),
display_name: value.display_name,
last_message_preview: value.last_message_preview,
date: DateTime::from_timestamp(
value.date.unix_timestamp(),
value.date.unix_timestamp_nanos().try_into().unwrap_or(0),
)
.unwrap()
.naive_local(),
participants: value
.participant_display_names
.into_iter()
.map(|p| Participant::Remote {
handle: p,
contact_id: None,
}) // todo: this is wrong
.collect(),
}
}
}
#[derive(Default)]
pub struct ConversationBuilder {
guid: Option<String>,
date: NaiveDateTime,
unread_count: Option<u16>,
last_message_preview: Option<String>,
participants: Option<Vec<Participant>>,
display_name: Option<String>,
}
impl ConversationBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn guid(mut self, guid: &str) -> Self {
self.guid = Some(guid.into());
self
}
pub fn date(mut self, date: NaiveDateTime) -> Self {
self.date = date;
self
}
pub fn unread_count(mut self, unread_count: u16) -> Self {
self.unread_count = Some(unread_count);
self
}
pub fn last_message_preview(mut self, last_message_preview: &str) -> Self {
self.last_message_preview = Some(last_message_preview.into());
self
}
pub fn participants(mut self, participants: Vec<Participant>) -> Self {
self.participants = Some(participants);
self
}
pub fn display_name(mut self, display_name: &str) -> Self {
self.display_name = Some(display_name.into());
self
}
pub fn build(&self) -> Conversation {
Conversation {
guid: self.guid.clone().unwrap_or(Uuid::new_v4().to_string()),
unread_count: self.unread_count.unwrap_or(0),
last_message_preview: self.last_message_preview.clone(),
display_name: self.display_name.clone(),
date: self.date,
participants: self.participants.clone().unwrap_or_default(),
}
}
}

View File

@@ -0,0 +1,53 @@
use crate::models::{db::participant::InsertableRecord as InsertableParticipant, Conversation};
use chrono::NaiveDateTime;
use diesel::prelude::*;
#[derive(Queryable, Selectable, Insertable, AsChangeset, Clone, Identifiable)]
#[diesel(table_name = crate::schema::conversations)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct Record {
pub id: String,
pub unread_count: i64,
pub display_name: Option<String>,
pub last_message_preview: Option<String>,
pub date: NaiveDateTime,
}
impl From<Conversation> for Record {
fn from(conversation: Conversation) -> Self {
Self {
id: conversation.guid,
unread_count: conversation.unread_count as i64,
display_name: conversation.display_name,
last_message_preview: conversation.last_message_preview,
date: conversation.date,
}
}
}
// This implementation returns the insertable data types for the conversation and participants
impl From<Conversation> for (Record, Vec<InsertableParticipant>) {
fn from(conversation: Conversation) -> Self {
(
Record::from(conversation.clone()),
conversation
.participants
.into_iter()
.map(InsertableParticipant::from)
.collect(),
)
}
}
impl From<Record> for Conversation {
fn from(record: Record) -> Self {
Self {
guid: record.id,
unread_count: record.unread_count as u16,
display_name: record.display_name,
last_message_preview: record.last_message_preview,
date: record.date,
participants: vec![],
}
}
}

View File

@@ -0,0 +1,70 @@
use crate::models::{Message, Participant};
use chrono::NaiveDateTime;
use diesel::prelude::*;
#[derive(Queryable, Selectable, Insertable, AsChangeset, Clone, Identifiable, Debug)]
#[diesel(table_name = crate::schema::messages)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct Record {
pub id: String,
pub sender_participant_handle: Option<String>,
pub text: String,
pub date: NaiveDateTime,
pub file_transfer_guids: Option<String>, // JSON array
pub attachment_metadata: Option<String>, // JSON string
}
impl From<Message> for Record {
fn from(message: Message) -> Self {
let file_transfer_guids = if message.file_transfer_guids.is_empty() {
None
} else {
Some(serde_json::to_string(&message.file_transfer_guids).unwrap_or_default())
};
let attachment_metadata = message
.attachment_metadata
.map(|metadata| serde_json::to_string(&metadata).unwrap_or_default());
Self {
id: message.id,
sender_participant_handle: match message.sender {
Participant::Me => None,
Participant::Remote { handle, .. } => Some(handle),
},
text: message.text,
date: message.date,
file_transfer_guids,
attachment_metadata,
}
}
}
impl From<Record> for Message {
fn from(record: Record) -> Self {
let file_transfer_guids = record
.file_transfer_guids
.and_then(|json| serde_json::from_str(&json).ok())
.unwrap_or_default();
let attachment_metadata = record
.attachment_metadata
.and_then(|json| serde_json::from_str(&json).ok());
let message_sender = match record.sender_participant_handle {
Some(handle) => Participant::Remote {
handle,
contact_id: None,
},
None => Participant::Me,
};
Self {
id: record.id,
sender: message_sender,
text: record.text,
date: record.date,
file_transfer_guids,
attachment_metadata,
}
}
}

View File

@@ -0,0 +1,3 @@
pub mod conversation;
pub mod message;
pub mod participant;

View File

@@ -0,0 +1,81 @@
use crate::models::Participant;
use crate::schema::conversation_participants;
use diesel::prelude::*;
#[derive(Queryable, Selectable, AsChangeset, Identifiable)]
#[diesel(table_name = crate::schema::participants)]
#[diesel(primary_key(handle))]
pub struct Record {
pub handle: String,
pub is_me: bool,
pub contact_id: Option<String>,
}
#[derive(Insertable)]
#[diesel(table_name = crate::schema::participants)]
pub struct InsertableRecord {
pub handle: String,
pub is_me: bool,
pub contact_id: Option<String>,
}
impl From<Participant> for InsertableRecord {
fn from(participant: Participant) -> Self {
match participant {
Participant::Me => InsertableRecord {
handle: "me".to_string(),
is_me: true,
contact_id: None,
},
Participant::Remote {
handle, contact_id, ..
} => InsertableRecord {
handle,
is_me: false,
contact_id,
},
}
}
}
#[derive(Identifiable, Selectable, Queryable, Associations, Debug)]
#[diesel(belongs_to(super::conversation::Record, foreign_key = conversation_id))]
#[diesel(belongs_to(Record, foreign_key = participant_handle))]
#[diesel(table_name = conversation_participants)]
#[diesel(primary_key(conversation_id, participant_handle))]
pub struct ConversationParticipant {
pub conversation_id: String,
pub participant_handle: String,
}
impl From<Record> for Participant {
fn from(record: Record) -> Self {
if record.is_me {
Participant::Me
} else {
Participant::Remote {
handle: record.handle.clone(),
contact_id: record.contact_id,
}
}
}
}
impl From<Participant> for Record {
fn from(participant: Participant) -> Self {
match participant {
Participant::Me => Record {
handle: "me".to_string(),
is_me: true,
contact_id: None,
},
Participant::Remote {
handle, contact_id, ..
} => Record {
handle,
is_me: false,
contact_id,
},
}
}
}

View File

@@ -0,0 +1,148 @@
use crate::models::participant::Participant;
use chrono::{DateTime, NaiveDateTime};
use kordophone::model::message::AttachmentMetadata;
use kordophone::model::outgoing_message::OutgoingMessage;
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Clone, Debug)]
pub struct Message {
pub id: String,
pub sender: Participant,
pub text: String,
pub date: NaiveDateTime,
pub file_transfer_guids: Vec<String>,
pub attachment_metadata: Option<HashMap<String, AttachmentMetadata>>,
}
impl Message {
pub fn builder() -> MessageBuilder {
MessageBuilder::new()
}
}
impl From<kordophone::model::Message> for Message {
fn from(value: kordophone::model::Message) -> Self {
let sender_participant = match value.sender {
Some(sender) => Participant::Remote {
contact_id: None,
// Weird server quirk: some sender handles are encoded with control characters.
handle: sender
.chars()
.filter(|c| {
!c.is_control()
&& !matches!(
c,
'\u{202A}' | // LRE
'\u{202B}' | // RLE
'\u{202C}' | // PDF
'\u{202D}' | // LRO
'\u{202E}' | // RLO
'\u{2066}' | // LRI
'\u{2067}' | // RLI
'\u{2068}' | // FSI
'\u{2069}' // PDI
)
})
.collect::<String>(),
},
None => Participant::Me,
};
Self {
id: value.guid,
sender: sender_participant,
text: value.text,
date: DateTime::from_timestamp(
value.date.unix_timestamp(),
value.date.unix_timestamp_nanos().try_into().unwrap_or(0),
)
.unwrap()
.naive_local(),
file_transfer_guids: value.file_transfer_guids,
attachment_metadata: value.attachment_metadata,
}
}
}
impl From<&OutgoingMessage> for Message {
fn from(value: &OutgoingMessage) -> Self {
Self {
id: value.guid.to_string(),
sender: Participant::Me,
text: value.text.clone(),
date: value.date,
file_transfer_guids: Vec::new(), // Outgoing messages don't have file transfer GUIDs initially
attachment_metadata: None, // Outgoing messages don't have attachment metadata initially
}
}
}
pub struct MessageBuilder {
id: Option<String>,
sender: Option<Participant>,
text: Option<String>,
date: Option<NaiveDateTime>,
file_transfer_guids: Option<Vec<String>>,
attachment_metadata: Option<HashMap<String, AttachmentMetadata>>,
}
impl Default for MessageBuilder {
fn default() -> Self {
Self::new()
}
}
impl MessageBuilder {
pub fn new() -> Self {
Self {
id: None,
sender: None,
text: None,
date: None,
file_transfer_guids: None,
attachment_metadata: None,
}
}
pub fn sender(mut self, sender: Participant) -> Self {
self.sender = Some(sender);
self
}
pub fn text(mut self, text: String) -> Self {
self.text = Some(text);
self
}
pub fn date(mut self, date: NaiveDateTime) -> Self {
self.date = Some(date);
self
}
pub fn file_transfer_guids(mut self, file_transfer_guids: Vec<String>) -> Self {
self.file_transfer_guids = Some(file_transfer_guids);
self
}
pub fn attachment_metadata(
mut self,
attachment_metadata: HashMap<String, AttachmentMetadata>,
) -> Self {
self.attachment_metadata = Some(attachment_metadata);
self
}
pub fn build(self) -> Message {
Message {
id: self.id.unwrap_or_else(|| Uuid::new_v4().to_string()),
sender: self.sender.unwrap_or(Participant::Me),
text: self.text.unwrap_or_default(),
date: self.date.unwrap_or_else(|| chrono::Utc::now().naive_utc()),
file_transfer_guids: self.file_transfer_guids.unwrap_or_default(),
attachment_metadata: self.attachment_metadata,
}
}
}

View File

@@ -0,0 +1,8 @@
pub mod conversation;
pub mod db;
pub mod message;
pub mod participant;
pub use conversation::Conversation;
pub use message::Message;
pub use participant::Participant;

View File

@@ -0,0 +1,22 @@
#[derive(Debug, Clone, PartialEq)]
pub enum Participant {
Me,
Remote {
handle: String,
contact_id: Option<String>,
},
}
impl Participant {
pub fn handle(&self) -> String {
match self {
Participant::Me => "(Me)".to_string(),
Participant::Remote { handle, .. } => handle.clone(),
}
}
// Temporary alias for backward compatibility
pub fn display_name(&self) -> String {
self.handle()
}
}