2025-11-27 13:45:21 -06:00
|
|
|
use chrono::{DateTime, NaiveDate, Utc};
|
2026-01-26 22:14:58 -06:00
|
|
|
use sqlx::sqlite::SqlitePool;
|
2025-11-27 13:45:21 -06:00
|
|
|
use std::str::FromStr;
|
|
|
|
|
|
|
|
|
|
use super::Birthday;
|
2026-01-26 22:14:58 -06:00
|
|
|
use crate::AppError;
|
2025-11-27 13:45:21 -06:00
|
|
|
use crate::db::DbId;
|
2026-01-26 22:14:58 -06:00
|
|
|
use crate::switchboard::MentionHostType;
|
|
|
|
|
|
|
|
|
|
struct RawContact {
|
|
|
|
|
id: DbId,
|
|
|
|
|
birthday: Option<String>,
|
|
|
|
|
manually_freshened_at: Option<String>,
|
|
|
|
|
lives_with: String,
|
|
|
|
|
}
|
2025-11-27 13:45:21 -06:00
|
|
|
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
pub struct Contact {
|
|
|
|
|
pub id: DbId,
|
|
|
|
|
pub birthday: Option<Birthday>,
|
|
|
|
|
pub manually_freshened_at: Option<DateTime<Utc>>,
|
2026-01-24 11:13:27 -06:00
|
|
|
pub lives_with: String,
|
2025-11-27 13:45:21 -06:00
|
|
|
}
|
|
|
|
|
|
2026-01-26 22:14:58 -06:00
|
|
|
impl Into<Contact> for RawContact {
|
|
|
|
|
fn into(self) -> Contact {
|
|
|
|
|
Contact {
|
|
|
|
|
id: self.id,
|
|
|
|
|
birthday: self
|
|
|
|
|
.birthday
|
|
|
|
|
.and_then(|s| Birthday::from_str(s.as_ref()).ok()),
|
|
|
|
|
manually_freshened_at: self
|
|
|
|
|
.manually_freshened_at
|
|
|
|
|
.and_then(|str| DateTime::parse_from_str(str.as_ref(), "%+").ok())
|
|
|
|
|
.map(|d| d.to_utc()),
|
|
|
|
|
lives_with: self.lives_with,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct RawHydratedContact {
|
|
|
|
|
id: DbId,
|
|
|
|
|
birthday: Option<String>,
|
|
|
|
|
manually_freshened_at: Option<String>,
|
|
|
|
|
lives_with: String,
|
|
|
|
|
last_mention_date: Option<String>,
|
|
|
|
|
names: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-27 13:45:21 -06:00
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
pub struct HydratedContact {
|
|
|
|
|
pub contact: Contact,
|
|
|
|
|
pub last_mention_date: Option<NaiveDate>,
|
|
|
|
|
pub names: Vec<String>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-26 22:14:58 -06:00
|
|
|
impl Into<HydratedContact> for RawHydratedContact {
|
|
|
|
|
fn into(self) -> HydratedContact {
|
|
|
|
|
HydratedContact {
|
|
|
|
|
contact: Into::<Contact>::into(RawContact {
|
|
|
|
|
id: self.id,
|
|
|
|
|
birthday: self.birthday,
|
|
|
|
|
manually_freshened_at: self.manually_freshened_at,
|
|
|
|
|
lives_with: self.lives_with,
|
|
|
|
|
}),
|
|
|
|
|
names: self
|
|
|
|
|
.names
|
|
|
|
|
.unwrap_or(String::new())
|
|
|
|
|
.split('\x1c')
|
|
|
|
|
.map(|s| s.to_string())
|
|
|
|
|
.collect::<Vec<String>>(),
|
|
|
|
|
last_mention_date: self
|
|
|
|
|
.last_mention_date
|
|
|
|
|
.and_then(|str| NaiveDate::from_str(str.as_ref()).ok()),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-27 13:45:21 -06:00
|
|
|
impl std::ops::Deref for HydratedContact {
|
|
|
|
|
type Target = Contact;
|
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
|
|
|
&self.contact
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl HydratedContact {
|
|
|
|
|
pub fn display_name(&self) -> String {
|
|
|
|
|
if let Some(name) = self.names.first() {
|
|
|
|
|
name.clone()
|
|
|
|
|
} else {
|
|
|
|
|
"(unnamed)".to_string()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-26 22:14:58 -06:00
|
|
|
pub async fn load(id: DbId, pool: &SqlitePool) -> Result<Self, AppError> {
|
|
|
|
|
// copy-paste the query from 'all', then add "where c.id = $2" to the last line
|
|
|
|
|
let raw = sqlx::query_as!(
|
|
|
|
|
RawHydratedContact,
|
|
|
|
|
r#"select id, birthday, lives_with, manually_freshened_at as "manually_freshened_at: String", (
|
2026-01-31 21:01:01 -06:00
|
|
|
select string_agg(name,x'1c' order by sort)
|
2026-01-26 22:14:58 -06:00
|
|
|
from names where contact_id = c.id
|
|
|
|
|
) as names, (
|
|
|
|
|
select jes.date from journal_entries jes
|
|
|
|
|
join mentions ms on ms.entity_id = jes.id
|
|
|
|
|
where ms.entity_type = $1
|
|
|
|
|
and ms.url = '/contact/'||c.id
|
|
|
|
|
or ms.url in (
|
|
|
|
|
select '/group/'||slug from groups where
|
|
|
|
|
contact_id = c.id
|
|
|
|
|
)
|
|
|
|
|
order by jes.date desc limit 1
|
|
|
|
|
) as last_mention_date from contacts c
|
|
|
|
|
where c.id = $2"#,
|
|
|
|
|
MentionHostType::JournalEntry as DbId,
|
|
|
|
|
id
|
|
|
|
|
)
|
|
|
|
|
.fetch_one(pool)
|
|
|
|
|
.await?;
|
2025-11-27 13:45:21 -06:00
|
|
|
|
2026-01-26 22:14:58 -06:00
|
|
|
Ok(Into::<HydratedContact>::into(raw))
|
2025-11-27 13:45:21 -06:00
|
|
|
}
|
|
|
|
|
|
2026-01-26 22:14:58 -06:00
|
|
|
pub async fn all(pool: &SqlitePool) -> Result<Vec<Self>, AppError> {
|
|
|
|
|
let contacts = sqlx::query_as!(
|
|
|
|
|
RawHydratedContact,
|
|
|
|
|
r#"select id, birthday, lives_with, manually_freshened_at as "manually_freshened_at: String", (
|
2026-01-31 21:01:01 -06:00
|
|
|
select string_agg(name,x'1c' order by sort)
|
2026-01-26 22:14:58 -06:00
|
|
|
from names where contact_id = c.id
|
|
|
|
|
) as names, (
|
|
|
|
|
select jes.date from journal_entries jes
|
|
|
|
|
join mentions ms on ms.entity_id = jes.id
|
|
|
|
|
where ms.entity_type = $1
|
|
|
|
|
and ms.url = '/contact/'||c.id
|
|
|
|
|
or ms.url in (
|
|
|
|
|
select '/group/'||slug from groups where
|
|
|
|
|
contact_id = c.id
|
|
|
|
|
)
|
|
|
|
|
order by jes.date desc limit 1
|
|
|
|
|
) as last_mention_date from contacts c"#,
|
|
|
|
|
MentionHostType::JournalEntry as DbId
|
|
|
|
|
)
|
|
|
|
|
.fetch_all(pool)
|
|
|
|
|
.await?;
|
2025-11-27 13:45:21 -06:00
|
|
|
|
2026-01-26 22:14:58 -06:00
|
|
|
Ok(contacts
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|raw| Into::<HydratedContact>::into(raw))
|
|
|
|
|
.collect())
|
2025-11-27 13:45:21 -06:00
|
|
|
}
|
|
|
|
|
}
|