use std::collections::BTreeMap; use std::fmt; use std::str::FromStr; use chrono::{DateTime, Utc}; use clap::Parser; use clap::builder::ValueParserFactory; use color_eyre::eyre::eyre; use imbl_value::InternedString; use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async}; use serde::{Deserialize, Serialize}; use tracing::instrument; use ts_rs::TS; use crate::PackageId; use crate::backup::BackupReport; use crate::context::{CliContext, RpcContext}; use crate::db::model::DatabaseModel; use crate::prelude::*; use crate::util::FromStrParser; use crate::util::serde::{HandlerExtSerde, const_true}; // #[command(subcommands(list, delete, delete_before, create))] pub fn notification() -> ParentHandler { ParentHandler::new() .subcommand( "list", from_fn_async(list) .with_display_serializable() .with_about("about.list-notifications") .with_call_remote::(), ) .subcommand( "remove", from_fn_async(remove) .no_display() .with_about("about.remove-notification-for-ids") .with_call_remote::(), ) .subcommand( "remove-before", from_fn_async(remove_before) .no_display() .with_about("about.remove-notifications-before-id") .with_call_remote::(), ) .subcommand( "mark-seen", from_fn_async(mark_seen) .no_display() .with_about("about.mark-notifications-seen") .with_call_remote::(), ) .subcommand( "mark-seen-before", from_fn_async(mark_seen_before) .no_display() .with_about("about.mark-notifications-seen-before-id") .with_call_remote::(), ) .subcommand( "mark-unseen", from_fn_async(mark_unseen) .no_display() .with_about("about.mark-notifications-unseen") .with_call_remote::(), ) .subcommand( "create", from_fn_async(create) .no_display() .with_about("about.persist-new-notification") .with_call_remote::(), ) } #[derive(Deserialize, Serialize, Parser, TS)] #[ts(export)] #[serde(rename_all = "camelCase")] #[command(rename_all = "kebab-case")] pub struct ListNotificationParams { #[arg(help = "help.arg.notification-before-id")] #[ts(type = "number | null")] before: Option, #[arg(help = "help.arg.notification-limit")] #[ts(type = "number | null")] limit: Option, } // #[command(display(display_serializable))] #[instrument(skip_all)] pub async fn list( ctx: RpcContext, ListNotificationParams { before, limit }: ListNotificationParams, ) -> Result, Error> { ctx.db .mutate(|db| { let limit = limit.unwrap_or(40); match before { None => { let records = db .as_private() .as_notifications() .as_entries()? .into_iter() .rev() .take(limit); let notifs = records .into_iter() .map(|(id, notification)| { Ok(NotificationWithId { id, notification: notification.de()?, }) }) .collect::, Error>>()?; Ok(notifs) } Some(before) => { let records = db .as_private() .as_notifications() .as_entries()? .into_iter() .filter(|(id, _)| *id < before) .rev() .take(limit); records .into_iter() .map(|(id, notification)| { Ok(NotificationWithId { id, notification: notification.de()?, }) }) .collect() } } }) .await .result } #[derive(Deserialize, Serialize, Parser, TS)] #[ts(export)] #[serde(rename_all = "camelCase")] #[command(rename_all = "kebab-case")] pub struct ModifyNotificationParams { #[arg(help = "help.arg.notification-ids")] #[ts(type = "number[]")] ids: Vec, } pub async fn remove( ctx: RpcContext, ModifyNotificationParams { ids }: ModifyNotificationParams, ) -> Result<(), Error> { ctx.db .mutate(|db| { let n = db.as_private_mut().as_notifications_mut(); for id in ids { n.remove(&id)?; } let mut unread = 0; for (_, n) in n.as_entries()? { if !n.as_seen().de()? { unread += 1; } } db.as_public_mut() .as_server_info_mut() .as_unread_notification_count_mut() .ser(&unread)?; Ok(()) }) .await .result } #[derive(Deserialize, Serialize, Parser, TS)] #[ts(export)] #[serde(rename_all = "camelCase")] #[command(rename_all = "kebab-case")] pub struct ModifyNotificationBeforeParams { #[arg(help = "help.arg.notification-before-id")] #[ts(type = "number")] before: u32, } pub async fn remove_before( ctx: RpcContext, ModifyNotificationBeforeParams { before }: ModifyNotificationBeforeParams, ) -> Result<(), Error> { ctx.db .mutate(|db| { let n = db.as_private_mut().as_notifications_mut(); for id in n.keys()?.range(..before) { n.remove(&id)?; } let mut unread = 0; for (_, n) in n.as_entries()? { if !n.as_seen().de()? { unread += 1; } } db.as_public_mut() .as_server_info_mut() .as_unread_notification_count_mut() .ser(&unread)?; Ok(()) }) .await .result } pub async fn mark_seen( ctx: RpcContext, ModifyNotificationParams { ids }: ModifyNotificationParams, ) -> Result<(), Error> { ctx.db .mutate(|db| { let n = db.as_private_mut().as_notifications_mut(); for id in ids { n.as_idx_mut(&id) .or_not_found(lazy_format!("Notification #{id}"))? .as_seen_mut() .ser(&true)?; } let mut unread = 0; for (_, n) in n.as_entries()? { if !n.as_seen().de()? { unread += 1; } } db.as_public_mut() .as_server_info_mut() .as_unread_notification_count_mut() .ser(&unread)?; Ok(()) }) .await .result } pub async fn mark_seen_before( ctx: RpcContext, ModifyNotificationBeforeParams { before }: ModifyNotificationBeforeParams, ) -> Result<(), Error> { ctx.db .mutate(|db| { let n = db.as_private_mut().as_notifications_mut(); for id in n.keys()?.range(..=before) { n.as_idx_mut(&id) .or_not_found(lazy_format!("Notification #{id}"))? .as_seen_mut() .ser(&true)?; } let mut unread = 0; for (_, n) in n.as_entries()? { if !n.as_seen().de()? { unread += 1; } } db.as_public_mut() .as_server_info_mut() .as_unread_notification_count_mut() .ser(&unread)?; Ok(()) }) .await .result } pub async fn mark_unseen( ctx: RpcContext, ModifyNotificationParams { ids }: ModifyNotificationParams, ) -> Result<(), Error> { ctx.db .mutate(|db| { let n = db.as_private_mut().as_notifications_mut(); for id in ids { n.as_idx_mut(&id) .or_not_found(lazy_format!("Notification #{id}"))? .as_seen_mut() .ser(&false)?; } let mut unread = 0; for (_, n) in n.as_entries()? { if !n.as_seen().de()? { unread += 1; } } db.as_public_mut() .as_server_info_mut() .as_unread_notification_count_mut() .ser(&unread)?; Ok(()) }) .await .result } #[derive(Deserialize, Serialize, Parser, TS)] #[serde(rename_all = "camelCase")] #[command(rename_all = "kebab-case")] pub struct CreateParams { #[arg(help = "help.arg.package-id")] package: Option, #[arg(help = "help.arg.notification-level")] level: NotificationLevel, #[arg(help = "help.arg.notification-title")] title: String, #[arg(help = "help.arg.notification-message")] message: String, } pub async fn create( ctx: RpcContext, CreateParams { package, level, title, message, }: CreateParams, ) -> Result<(), Error> { ctx.db .mutate(|db| notify(db, package, level, title, message, ())) .await .result } #[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, TS)] #[ts(export)] #[serde(rename_all = "camelCase")] pub enum NotificationLevel { Success, Info, Warning, Error, } impl fmt::Display for NotificationLevel { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { NotificationLevel::Success => write!(f, "success"), NotificationLevel::Info => write!(f, "info"), NotificationLevel::Warning => write!(f, "warning"), NotificationLevel::Error => write!(f, "error"), } } } impl ValueParserFactory for NotificationLevel { type Parser = FromStrParser; fn value_parser() -> Self::Parser { FromStrParser::new() } } pub struct InvalidNotificationLevel(String); impl From for crate::Error { fn from(val: InvalidNotificationLevel) -> Self { Error::new( eyre!("{}", t!("notifications.invalid-level", level = val.0)), ErrorKind::ParseDbField, ) } } impl FromStr for NotificationLevel { type Err = InvalidNotificationLevel; fn from_str(s: &str) -> Result { match s { s if s == "success" => Ok(NotificationLevel::Success), s if s == "info" => Ok(NotificationLevel::Info), s if s == "warning" => Ok(NotificationLevel::Warning), s if s == "error" => Ok(NotificationLevel::Error), s => Err(InvalidNotificationLevel(s.to_string())), } } } impl fmt::Display for InvalidNotificationLevel { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Invalid Notification Level: {}", self.0) } } #[derive(Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Notifications(pub BTreeMap); impl Notifications { pub fn new() -> Self { Self(BTreeMap::new()) } } impl Map for Notifications { type Key = u32; type Value = Notification; fn key_str(key: &Self::Key) -> Result, Error> { Self::key_string(key) } fn key_string(key: &Self::Key) -> Result { Ok(InternedString::from_display(key)) } } #[derive(Debug, Serialize, Deserialize, HasModel, TS)] #[ts(export)] #[serde(rename_all = "camelCase")] #[model = "Model"] pub struct Notification { pub package_id: Option, #[ts(type = "string")] pub created_at: DateTime, pub code: u32, pub level: NotificationLevel, pub title: String, pub message: String, #[ts(type = "any")] pub data: Value, #[serde(default = "const_true")] pub seen: bool, } #[derive(Debug, Serialize, Deserialize, TS)] #[ts(export)] #[serde(rename_all = "camelCase")] pub struct NotificationWithId { id: u32, #[serde(flatten)] #[ts(flatten)] notification: Notification, } pub trait NotificationType: serde::Serialize + for<'de> serde::Deserialize<'de> + std::fmt::Debug { const CODE: u32; } impl NotificationType for () { const CODE: u32 = 0; } impl NotificationType for BackupReport { const CODE: u32 = 1; } impl NotificationType for String { const CODE: u32 = 2; } #[instrument(skip(subtype, db))] pub fn notify( db: &mut DatabaseModel, package_id: Option, level: NotificationLevel, title: String, message: String, subtype: T, ) -> Result<(), Error> { let data = to_value(&subtype)?; db.as_public_mut() .as_server_info_mut() .as_unread_notification_count_mut() .mutate(|c| { *c += 1; Ok(()) })?; let id = db .as_private() .as_notifications() .keys()? .into_iter() .max() .map_or(0, |id| id + 1); db.as_private_mut().as_notifications_mut().insert( &id, &Notification { package_id, created_at: Utc::now(), code: T::CODE, level, title, message, data, seen: false, }, )?; Ok(()) } #[test] fn serialization() { println!( "{}", serde_json::json!({ "test": "abcdefg", "num": 32, "nested": { "inner": null, "xyz": [0,2,4]}}) ) }