use std::sync::Arc;
use std::sync::PoisonError;
use std::sync::RwLock;
use leptoskit_db::Actor;
use leptoskit_db::Connection;
use leptoskit_db::Expr;
use leptoskit_db::Id;
use leptoskit_db::Json;
use leptoskit_db::Timestamp;
use leptoskit_db::policy;
use leptoskit_db::table;
use leptoskit_query::BoxFuture;
use super::MutationCx;
use super::QueryCx;
use super::handle;
use crate::Error;
use crate::Operation;
use crate::Result;
use crate::inbox::Inbox;
use crate::inbox::InboxNotice;
use crate::inbox::Notice;
use crate::inbox::NoticeAction;
use crate::inbox::NoticeKind;
#[table(name = "inbox", pack = "rpc", live, index(account, created_at))]
#[policy(insert, |_, _| Expr::always())]
#[policy(select, update, |notice, cx| notice.account.eq(cx.setting::<Actor>()))]
pub struct InboxRow {
pub id: Id<Self>,
#[db(max_len = 64)]
pub account: String,
#[db(max_len = 255)]
pub title: String,
#[db(max_len = 4000)]
pub body: String,
#[db(max_len = 2000)]
pub link: Option<String>,
pub action: Option<Json<NoticeAction>>,
#[db(max_len = 32)]
pub kind: String,
pub read_at: Option<Timestamp>,
#[db(created_at)]
pub created_at: Timestamp,
}
impl InboxRow {
#[must_use]
pub fn to_notice(&self) -> InboxNotice {
InboxNotice {
id: self.id.to_string(),
notice: Notice {
title: self.title.clone(),
body: self.body.clone(),
link: self.link.clone(),
action: self.action.as_ref().map(|action| action.0.clone()),
kind: NoticeKind::parse(&self.kind),
},
read: self.read_at.is_some(),
created_at: self.created_at.to_rfc3339(),
}
}
}
pub trait Recipient: Send + Sync {
fn account(&self) -> Option<String>;
}
impl<T: ?Sized> Recipient for Id<T> {
fn account(&self) -> Option<String> {
Some(self.uuid().to_string())
}
}
impl Recipient for Actor {
fn account(&self) -> Option<String> {
Some(self.uuid().to_string())
}
}
impl<R: Recipient> Recipient for Option<R> {
fn account(&self) -> Option<String> {
self.as_ref().and_then(Recipient::account)
}
}
impl<R: Recipient + ?Sized> Recipient for &R {
fn account(&self) -> Option<String> {
(**self).account()
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Delivery {
pub id: String,
pub account: String,
pub notice: Notice,
}
pub trait Deliver: Send + Sync + 'static {
fn deliver<'a>(&'a self, delivery: &'a Delivery) -> BoxFuture<'a, Result<()>>;
}
static CHANNELS: RwLock<Vec<Arc<dyn Deliver>>> = RwLock::new(Vec::new());
pub fn deliver_with(channel: impl Deliver) {
CHANNELS
.write()
.unwrap_or_else(PoisonError::into_inner)
.push(Arc::new(channel));
}
pub(crate) async fn deliver(delivery: Delivery) {
let channels = CHANNELS
.read()
.unwrap_or_else(PoisonError::into_inner)
.clone();
for channel in channels {
let _ = channel.deliver(&delivery).await;
}
}
pub(crate) async fn insert(
connection: &impl Connection,
account: &impl Recipient,
notice: Notice,
) -> Result<Option<Delivery>> {
let Some(account) = account.account() else {
return Ok(None);
};
if notice.title.trim().is_empty() {
return Err(Error::invalid("a notice needs a title"));
}
let row = NewInboxRow {
account: account.clone(),
title: notice.title.clone(),
body: notice.body.clone(),
link: notice.link.clone(),
action: notice.action.clone().map(Json),
kind: notice.kind.as_str().to_owned(),
read_at: None,
}
.insert(connection)
.await?;
Ok(Some(Delivery {
id: row.id.to_string(),
account,
notice,
}))
}
impl MutationCx {
pub async fn notify(&self, account: impl Recipient, notice: Notice) -> Result<Option<String>> {
let Some(delivery) = insert(self.tx(), &account, notice).await? else {
return Ok(None);
};
let id = delivery.id.clone();
self.after_commit(deliver(delivery));
Ok(Some(id))
}
}
pub(crate) async fn read(cx: &QueryCx, limit: u32) -> Result<Inbox> {
let notices = InboxRow::query()
.order_by(|notice| notice.created_at.desc())
.limit(u64::from(limit.clamp(1, 200)))
.all(cx.db())
.await?;
let unread = InboxRow::query()
.filter(|notice| notice.read_at.is_null())
.count(cx.db())
.await?;
Ok(Inbox {
notices: notices.iter().map(InboxRow::to_notice).collect(),
unread,
})
}
pub(crate) async fn mark_read(cx: &MutationCx, id: String) -> Result<()> {
let id: Id<InboxRow> = id
.parse()
.map_err(|_| Error::invalid("that isn't a notice id"))?;
let notice = InboxRow::query()
.filter(|notice| notice.id.eq(id))
.first(cx.tx())
.await?
.ok_or_else(|| Error::not_found("no such notice"))?;
if notice.read_at.is_none() {
InboxRow::update()
.filter(|notice| notice.id.eq(id))
.set(|notice| notice.read_at, leptoskit_db::now())
.run(cx.tx())
.await?;
}
Ok(())
}
pub(crate) async fn mark_all_read(cx: &MutationCx) -> Result<u64> {
if !cx.signed_in() {
return Err(Error::unauthorized("sign in to read your notices"));
}
InboxRow::update()
.filter(|notice| notice.read_at.is_null())
.set(|notice| notice.read_at, leptoskit_db::now())
.run(cx.tx())
.await
.map_err(Into::into)
}
#[allow(deprecated)]
const _: () = {
inventory::submit! {
super::Registration {
name: <crate::inbox::InboxArgs as Operation>::NAME,
kind: <crate::inbox::InboxArgs as Operation>::KIND,
module_path: module_path!(),
handler: handle::<crate::inbox::InboxArgs>,
}
}
inventory::submit! {
super::Registration {
name: <crate::inbox::MarkRead as Operation>::NAME,
kind: <crate::inbox::MarkRead as Operation>::KIND,
module_path: module_path!(),
handler: handle::<crate::inbox::MarkRead>,
}
}
inventory::submit! {
super::Registration {
name: <crate::inbox::MarkAllRead as Operation>::NAME,
kind: <crate::inbox::MarkAllRead as Operation>::KIND,
module_path: module_path!(),
handler: handle::<crate::inbox::MarkAllRead>,
}
}
};