use sqlx::{PgConnection, Row};
use uuid::Uuid;
#[derive(Debug, Clone, serde::Serialize)]
pub struct FetchedMessage {
pub id: Uuid,
pub subject: Option<String>,
pub body: String,
pub message_type: String,
pub author_id: Option<Uuid>,
pub author_guest_id: Option<Uuid>,
pub email_from: Option<String>,
pub model: Option<String>,
pub res_id: Option<Uuid>,
pub record_name: Option<String>,
pub is_internal: bool,
pub pinned_at: Option<chrono::DateTime<chrono::Utc>>,
pub attachment_ids: Vec<Uuid>,
}
pub struct ChatterRepository;
const MESSAGE_SELECT: &str = r#"
SELECT m.id, m.subject, m.body, m.message_type::text AS message_type,
m.author_id, m.author_guest_id, m.email_from,
m.model, m.res_id, m.record_name, m.is_internal, m.pinned_at,
COALESCE(
(SELECT jsonb_agg(a.attachment_id ORDER BY a.attachment_id)
FROM messaging.mail_message_attachments a
WHERE a.message_id = m.id), '[]'
) AS attachment_ids
FROM messaging.mail_messages m
WHERE (m.metadata->>'deleted_at') IS NULL
"#;
fn map_fetched(row: sqlx::postgres::PgRow) -> Result<FetchedMessage, sqlx::Error> {
let raw: serde_json::Value = row.try_get("attachment_ids")?;
Ok(FetchedMessage {
id: row.try_get("id")?,
subject: row.try_get("subject")?,
body: row.try_get("body")?,
message_type: row.try_get("message_type")?,
author_id: row.try_get("author_id")?,
author_guest_id: row.try_get("author_guest_id")?,
email_from: row.try_get("email_from")?,
model: row.try_get("model")?,
res_id: row.try_get("res_id")?,
record_name: row.try_get("record_name")?,
is_internal: row.try_get("is_internal")?,
pinned_at: row.try_get("pinned_at")?,
attachment_ids: raw
.as_array()
.map(|a| a.iter().filter_map(|v| v.as_str().and_then(|s| Uuid::parse_str(s).ok())).collect())
.unwrap_or_default(),
})
}
impl ChatterRepository {
pub fn new() -> Self {
Self
}
pub async fn thread_messages(
conn: &mut PgConnection,
model: &str,
res_id: Uuid,
after_id: Option<Uuid>,
limit: i64,
) -> Result<Vec<FetchedMessage>, sqlx::Error> {
let rows = sqlx::query(&format!(
r#"{MESSAGE_SELECT}
AND m.model = $1 AND m.res_id = $2
AND ($3::uuid IS NULL
OR (m.date, m.id) >
(SELECT x.date, x.id
FROM messaging.mail_messages x WHERE x.id = $3))
ORDER BY m.date, m.id
LIMIT $4"#
))
.bind(model)
.bind(res_id)
.bind(after_id)
.bind(limit)
.fetch_all(&mut *conn)
.await?;
rows.into_iter().map(map_fetched).collect()
}
pub async fn channel_messages(
conn: &mut PgConnection,
channel_id: Uuid,
after_id: Option<Uuid>,
limit: i64,
) -> Result<Vec<FetchedMessage>, sqlx::Error> {
Self::thread_messages(conn, "discuss.channel", channel_id, after_id, limit).await
}
pub async fn inbox_messages(
conn: &mut PgConnection,
partner_id: Uuid,
limit: i64,
) -> Result<Vec<FetchedMessage>, sqlx::Error> {
let rows = sqlx::query(&format!(
r#"{MESSAGE_SELECT}
AND m.id IN (
SELECT n.mail_message_id FROM messaging.mail_notifications n
WHERE n.res_partner_id = $1
AND n.notification_type = 'inbox'
AND (n.metadata->>'deleted_at') IS NULL
)
ORDER BY m.date DESC, m.id DESC
LIMIT $2"#
))
.bind(partner_id)
.bind(limit)
.fetch_all(&mut *conn)
.await?;
rows.into_iter().map(map_fetched).collect()
}
pub async fn starred_messages(
conn: &mut PgConnection,
partner_id: Uuid,
limit: i64,
) -> Result<Vec<FetchedMessage>, sqlx::Error> {
let rows = sqlx::query(&format!(
r#"{MESSAGE_SELECT}
AND m.id IN (
SELECT s.message_id FROM messaging.mail_message_stars s
WHERE s.partner_id = $1
)
ORDER BY m.date DESC, m.id DESC
LIMIT $2"#
))
.bind(partner_id)
.bind(limit)
.fetch_all(&mut *conn)
.await?;
rows.into_iter().map(map_fetched).collect()
}
pub async fn inbox_unread_count(
conn: &mut PgConnection,
partner_id: Uuid,
) -> Result<i64, sqlx::Error> {
sqlx::query_scalar(
r#"SELECT COUNT(*) FROM messaging.mail_notifications
WHERE res_partner_id = $1
AND notification_type = 'inbox'
AND notification_status = 'ready'
AND (metadata->>'deleted_at') IS NULL"#,
)
.bind(partner_id)
.fetch_one(&mut *conn)
.await
}
pub async fn channel_unread_counts(
conn: &mut PgConnection,
partner_id: Uuid,
) -> Result<Vec<(Uuid, i64)>, sqlx::Error> {
let rows = sqlx::query(
r#"SELECT channel_id, message_unread_counter
FROM messaging.discuss_channel_members
WHERE partner_id = $1
AND (metadata->>'deleted_at') IS NULL
AND message_unread_counter > 0"#,
)
.bind(partner_id)
.fetch_all(&mut *conn)
.await?;
Ok(rows.into_iter().map(|r| (r.get("channel_id"), r.get("message_unread_counter"))).collect())
}
pub async fn message_thread(
conn: &mut PgConnection,
message_id: Uuid,
) -> Result<Option<(Option<String>, Option<Uuid>)>, sqlx::Error> {
sqlx::query_as(
r#"SELECT model, res_id FROM messaging.mail_messages
WHERE id = $1 AND (metadata->>'deleted_at') IS NULL"#,
)
.bind(message_id)
.fetch_optional(&mut *conn)
.await
}
pub async fn thread_authors(
conn: &mut PgConnection,
model: &str,
res_id: Uuid,
) -> Result<Vec<Uuid>, sqlx::Error> {
sqlx::query_scalar(
r#"SELECT DISTINCT author_id FROM messaging.mail_messages
WHERE model = $1 AND res_id = $2
AND author_id IS NOT NULL
AND (metadata->>'deleted_at') IS NULL"#,
)
.bind(model)
.bind(res_id)
.fetch_all(&mut *conn)
.await
}
}
impl Default for ChatterRepository {
fn default() -> Self {
Self::new()
}
}