- Introduce client phone addon, UI, and XEH handlers - Route actor phone interaction to the new phone UI - Add initial phone state, event handling, and persistence
225 lines
6.9 KiB
Rust
225 lines
6.9 KiB
Rust
use forge_models::{PhoneEmail, PhoneMessage, PhonePayload};
|
|
use forge_repositories::PhoneRepository;
|
|
|
|
pub struct PhoneStateService<R: PhoneRepository> {
|
|
repository: R,
|
|
}
|
|
|
|
impl<R: PhoneRepository> PhoneStateService<R> {
|
|
pub fn new(repository: R) -> Self {
|
|
Self { repository }
|
|
}
|
|
|
|
pub fn init(&self, uid: String) -> Result<PhonePayload, String> {
|
|
let uid = Self::validate_uid(uid)?;
|
|
self.repository.init(&uid)?;
|
|
self.payload_for(&uid)
|
|
}
|
|
|
|
pub fn add_contact(&self, uid: String, contact_uid: String) -> Result<bool, String> {
|
|
let uid = Self::validate_uid(uid)?;
|
|
let contact_uid = Self::validate_uid(contact_uid)?;
|
|
if uid == contact_uid {
|
|
return Err("Cannot add self as a phone contact.".to_string());
|
|
}
|
|
self.repository.add_contact(&uid, &contact_uid)
|
|
}
|
|
|
|
pub fn remove_contact(&self, uid: String, contact_uid: String) -> Result<bool, String> {
|
|
let uid = Self::validate_uid(uid)?;
|
|
let contact_uid = Self::validate_uid(contact_uid)?;
|
|
self.repository.remove_contact(&uid, &contact_uid)
|
|
}
|
|
|
|
pub fn list_contacts(&self, uid: String) -> Result<Vec<String>, String> {
|
|
let uid = Self::validate_uid(uid)?;
|
|
self.repository.list_contacts(&uid)
|
|
}
|
|
|
|
pub fn send_message(
|
|
&self,
|
|
from_uid: String,
|
|
to_uid: String,
|
|
message: String,
|
|
timestamp: String,
|
|
) -> Result<PhoneMessage, String> {
|
|
let from_uid = Self::validate_uid(from_uid)?;
|
|
let to_uid = Self::validate_uid(to_uid)?;
|
|
let message = Self::validate_non_empty(message, "Message body is required.")?;
|
|
let timestamp = Self::parse_timestamp(timestamp);
|
|
let id = format!(
|
|
"phone-message:{}:{}:{}",
|
|
from_uid,
|
|
to_uid,
|
|
self.repository.next_sequence()?
|
|
);
|
|
let record = PhoneMessage {
|
|
id,
|
|
from: from_uid.clone(),
|
|
to: to_uid.clone(),
|
|
message,
|
|
timestamp,
|
|
read: false,
|
|
};
|
|
|
|
self.repository.append_message(&from_uid, record.clone())?;
|
|
self.repository.append_message(&to_uid, record.clone())?;
|
|
Ok(record)
|
|
}
|
|
|
|
pub fn list_messages(&self, uid: String) -> Result<Vec<PhoneMessage>, String> {
|
|
let uid = Self::validate_uid(uid)?;
|
|
self.repository.list_messages(&uid)
|
|
}
|
|
|
|
pub fn message_thread(
|
|
&self,
|
|
uid: String,
|
|
other_uid: String,
|
|
) -> Result<Vec<PhoneMessage>, String> {
|
|
let uid = Self::validate_uid(uid)?;
|
|
let other_uid = Self::validate_uid(other_uid)?;
|
|
Ok(self
|
|
.repository
|
|
.list_messages(&uid)?
|
|
.into_iter()
|
|
.filter(|message| {
|
|
(message.from == uid && message.to == other_uid)
|
|
|| (message.from == other_uid && message.to == uid)
|
|
})
|
|
.collect())
|
|
}
|
|
|
|
pub fn mark_message_read(&self, uid: String, message_id: String) -> Result<bool, String> {
|
|
let uid = Self::validate_uid(uid)?;
|
|
let message_id = Self::validate_non_empty(message_id, "Message ID is required.")?;
|
|
self.repository.mark_message_read(&uid, &message_id)
|
|
}
|
|
|
|
pub fn send_email(
|
|
&self,
|
|
from_uid: String,
|
|
to_uid: String,
|
|
subject: String,
|
|
body: String,
|
|
timestamp: String,
|
|
) -> Result<PhoneEmail, String> {
|
|
let from_uid = Self::validate_uid(from_uid)?;
|
|
let to_uid = Self::validate_uid(to_uid)?;
|
|
let subject = Self::validate_non_empty(subject, "Email subject is required.")?;
|
|
let body = Self::validate_non_empty(body, "Email body is required.")?;
|
|
let timestamp = Self::parse_timestamp(timestamp);
|
|
let id = format!(
|
|
"phone-email:{}:{}:{}",
|
|
from_uid,
|
|
to_uid,
|
|
self.repository.next_sequence()?
|
|
);
|
|
let record = PhoneEmail {
|
|
id,
|
|
from: from_uid,
|
|
to: to_uid.clone(),
|
|
subject,
|
|
body,
|
|
timestamp,
|
|
read: false,
|
|
};
|
|
|
|
self.repository.append_email(&to_uid, record.clone())?;
|
|
Ok(record)
|
|
}
|
|
|
|
pub fn list_emails(&self, uid: String) -> Result<Vec<PhoneEmail>, String> {
|
|
let uid = Self::validate_uid(uid)?;
|
|
self.repository.list_emails(&uid)
|
|
}
|
|
|
|
pub fn mark_email_read(&self, uid: String, email_id: String) -> Result<bool, String> {
|
|
let uid = Self::validate_uid(uid)?;
|
|
let email_id = Self::validate_non_empty(email_id, "Email ID is required.")?;
|
|
self.repository.mark_email_read(&uid, &email_id)
|
|
}
|
|
|
|
pub fn remove(&self, uid: String) -> Result<(), String> {
|
|
let uid = Self::validate_uid(uid)?;
|
|
self.repository.remove_phone(&uid)
|
|
}
|
|
|
|
fn payload_for(&self, uid: &str) -> Result<PhonePayload, String> {
|
|
Ok(PhonePayload {
|
|
contacts: self.repository.list_contacts(uid)?,
|
|
messages: self.repository.list_messages(uid)?,
|
|
emails: self.repository.list_emails(uid)?,
|
|
})
|
|
}
|
|
|
|
fn validate_uid(uid: String) -> Result<String, String> {
|
|
let uid = uid.trim().to_string();
|
|
if uid.is_empty() {
|
|
Err("UID is required.".to_string())
|
|
} else {
|
|
Ok(uid)
|
|
}
|
|
}
|
|
|
|
fn validate_non_empty(value: String, message: &str) -> Result<String, String> {
|
|
let value = value.trim().to_string();
|
|
if value.is_empty() {
|
|
Err(message.to_string())
|
|
} else {
|
|
Ok(value)
|
|
}
|
|
}
|
|
|
|
fn parse_timestamp(timestamp: String) -> f64 {
|
|
timestamp.trim().parse::<f64>().unwrap_or_default()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::PhoneStateService;
|
|
use forge_repositories::InMemoryPhoneRepository;
|
|
|
|
#[test]
|
|
fn send_message_indexes_sender_and_receiver_threads() {
|
|
let service = PhoneStateService::new(InMemoryPhoneRepository::new());
|
|
|
|
let message = service
|
|
.send_message(
|
|
"sender".to_string(),
|
|
"receiver".to_string(),
|
|
"Test".to_string(),
|
|
"123".to_string(),
|
|
)
|
|
.expect("message should send");
|
|
|
|
assert_eq!(
|
|
service
|
|
.list_messages("sender".to_string())
|
|
.expect("sender messages should load")
|
|
.len(),
|
|
1
|
|
);
|
|
assert_eq!(
|
|
service
|
|
.message_thread("receiver".to_string(), "sender".to_string())
|
|
.expect("thread should load")
|
|
.first()
|
|
.map(|entry| entry.id.clone()),
|
|
Some(message.id)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn contact_cannot_reference_self() {
|
|
let service = PhoneStateService::new(InMemoryPhoneRepository::new());
|
|
|
|
assert!(
|
|
service
|
|
.add_contact("same".to_string(), "same".to_string())
|
|
.is_err()
|
|
);
|
|
}
|
|
}
|