Add 'core/' from commit 'b0dfc4146ca0da535a87f8509aec68817fb2ab14'
git-subtree-dir: core git-subtree-mainline:a07f3dcd23git-subtree-split:b0dfc4146c
This commit is contained in:
142
core/kordophone-db/src/models/conversation.rs
Normal file
142
core/kordophone-db/src/models/conversation.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
53
core/kordophone-db/src/models/db/conversation.rs
Normal file
53
core/kordophone-db/src/models/db/conversation.rs
Normal 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![],
|
||||
}
|
||||
}
|
||||
}
|
||||
70
core/kordophone-db/src/models/db/message.rs
Normal file
70
core/kordophone-db/src/models/db/message.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
3
core/kordophone-db/src/models/db/mod.rs
Normal file
3
core/kordophone-db/src/models/db/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod conversation;
|
||||
pub mod message;
|
||||
pub mod participant;
|
||||
81
core/kordophone-db/src/models/db/participant.rs
Normal file
81
core/kordophone-db/src/models/db/participant.rs
Normal 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
148
core/kordophone-db/src/models/message.rs
Normal file
148
core/kordophone-db/src/models/message.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
8
core/kordophone-db/src/models/mod.rs
Normal file
8
core/kordophone-db/src/models/mod.rs
Normal 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;
|
||||
22
core/kordophone-db/src/models/participant.rs
Normal file
22
core/kordophone-db/src/models/participant.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user