Skip to main content

sova_notifications/
plugin.rs

1//! Notifications plugin: state, HTTP mount, optional WS / template helpers.
2
3use crate::channel::Channel;
4use crate::http::mount_routes;
5use crate::notify::NotificationService;
6use sova_core::extend::{IntoMwEntry, MwEntry};
7use sova_core::{App, Plugin, Router};
8use std::collections::HashMap;
9use std::sync::Arc;
10
11/// Database notifications with named channels.
12pub struct Notifications {
13    channels: HashMap<String, Channel>,
14    mount: Option<String>,
15    guard: Option<MwEntry>,
16    #[cfg(feature = "ws")]
17    ws_path: Option<String>,
18    #[cfg(feature = "templates")]
19    template_helpers: bool,
20}
21
22impl Notifications {
23    pub fn new() -> Self {
24        let mut channels = HashMap::new();
25        channels.insert("default".into(), Channel::new("default"));
26        Self {
27            channels,
28            mount: None,
29            guard: None,
30            #[cfg(feature = "ws")]
31            ws_path: None,
32            #[cfg(feature = "templates")]
33            template_helpers: false,
34        }
35    }
36
37    pub fn channel(mut self, ch: Channel) -> Self {
38        self.channels.insert(ch.slug.clone(), ch);
39        self
40    }
41
42    /// Inbox API under `path` (`GET /`, unread-count, mark read, broadcast).
43    pub fn mount(mut self, path: impl Into<String>) -> Self {
44        self.mount = Some(path.into());
45        self
46    }
47
48    pub fn guard(mut self, mw: impl IntoMwEntry) -> Self {
49        self.guard = Some(mw.into_mw_entry());
50        self
51    }
52
53    /// WebSocket path (`join user:{id}`); requires installed `Ws` + feature `ws`.
54    #[cfg(feature = "ws")]
55    pub fn ws_path(mut self, path: impl Into<String>) -> Self {
56        self.ws_path = Some(path.into());
57        self
58    }
59
60    /// Register Minijinja `notifications_unread` per-request helper.
61    #[cfg(feature = "templates")]
62    pub fn with_template_helpers(mut self) -> Self {
63        self.template_helpers = true;
64        self
65    }
66}
67
68impl Default for Notifications {
69    fn default() -> Self {
70        Self::new()
71    }
72}
73
74impl Plugin for Notifications {
75    fn id(&self) -> &'static str {
76        "notifications"
77    }
78
79    fn requires(&self) -> &'static [&'static str] {
80        #[cfg(all(feature = "ws", feature = "templates"))]
81        if self.ws_path.is_some() && self.template_helpers {
82            return &["db", "ws", "templates"];
83        }
84        #[cfg(feature = "ws")]
85        if self.ws_path.is_some() {
86            return &["db", "ws"];
87        }
88        #[cfg(feature = "templates")]
89        if self.template_helpers {
90            return &["db", "templates"];
91        }
92        &["db"]
93    }
94
95    fn meta(&self) -> sova_core::PluginMeta {
96        sova_core::PluginMeta::new("Notifications")
97            .description("DB inbox, channels with ACL, optional WS/mail")
98            .version(env!("CARGO_PKG_VERSION"))
99    }
100
101    fn install(self, app: &mut App) {
102        app.state(NotificationService {
103            channels: Arc::new(self.channels),
104        });
105
106        let guard = self.guard;
107
108        if let Some(path) = self.mount {
109            let mut r = Router::new();
110            if let Some(ref g) = guard {
111                r.use_middleware(g.clone());
112            }
113            #[cfg(feature = "templates")]
114            if self.template_helpers {
115                r.use_middleware(crate::templates::preload_middleware());
116            }
117            mount_routes(&mut r);
118            app.mount(&path, r);
119        }
120
121        #[cfg(feature = "ws")]
122        if let Some(path) = self.ws_path {
123            crate::ws::install_ws(app, &path, guard);
124        }
125
126        #[cfg(feature = "templates")]
127        if self.template_helpers {
128            crate::templates::register(app);
129        }
130    }
131}