diff --git a/crates/cli/src/server/actor/handlers/notify_user/desktop.rs b/crates/cli/src/alerts/desktop.rs similarity index 72% rename from crates/cli/src/server/actor/handlers/notify_user/desktop.rs rename to crates/cli/src/alerts/desktop.rs index dec1f44..78d9c1c 100644 --- a/crates/cli/src/server/actor/handlers/notify_user/desktop.rs +++ b/crates/cli/src/alerts/desktop.rs @@ -1,5 +1,5 @@ // -use crate::{repo::messages::NotifyUser, server::actor::handlers::notify_user::short_message}; +use crate::alerts::{messages::NotifyUser, short_message}; pub(super) fn send_desktop_notification(msg: &NotifyUser) { let message = short_message(msg); diff --git a/crates/cli/src/server/actor/handlers/notify_user/email.rs b/crates/cli/src/alerts/email.rs similarity index 96% rename from crates/cli/src/server/actor/handlers/notify_user/email.rs rename to crates/cli/src/alerts/email.rs index 163405e..5f1efbd 100644 --- a/crates/cli/src/server/actor/handlers/notify_user/email.rs +++ b/crates/cli/src/alerts/email.rs @@ -1,9 +1,7 @@ // use git_next_core::server::{EmailConfig, SmtpConfig}; -use crate::repo::messages::NotifyUser; - -use super::{full_message, short_message}; +use crate::alerts::{full_message, messages::NotifyUser, short_message}; #[derive(Debug)] struct EmailMessage { diff --git a/crates/cli/src/alerts/handlers/mod.rs b/crates/cli/src/alerts/handlers/mod.rs new file mode 100644 index 0000000..7a7087d --- /dev/null +++ b/crates/cli/src/alerts/handlers/mod.rs @@ -0,0 +1,2 @@ +mod notify_user; +mod update_shout; diff --git a/crates/cli/src/alerts/handlers/notify_user.rs b/crates/cli/src/alerts/handlers/notify_user.rs new file mode 100644 index 0000000..485dda8 --- /dev/null +++ b/crates/cli/src/alerts/handlers/notify_user.rs @@ -0,0 +1,36 @@ +// +use actix::prelude::*; +use tracing::{info, instrument, Instrument as _}; + +use crate::alerts::{ + desktop::send_desktop_notification, email::send_email, messages::NotifyUser, + webhook::send_webhook, AlertsActor, +}; + +impl Handler for AlertsActor { + type Result = (); + + #[instrument] + fn handle(&mut self, msg: NotifyUser, ctx: &mut Self::Context) -> Self::Result { + let Some(shout) = &self.shout else { + info!("No shout config available"); + return; + }; + let net = self.net.clone(); + let shout = shout.clone(); + async move { + if let Some(webhook_config) = shout.webhook() { + send_webhook(&msg, webhook_config, &net).await; + } + if let Some(email_config) = shout.email() { + send_email(&msg, email_config); + } + if shout.desktop() { + send_desktop_notification(&msg); + } + } + .in_current_span() + .into_actor(self) + .wait(ctx); + } +} diff --git a/crates/cli/src/alerts/handlers/update_shout.rs b/crates/cli/src/alerts/handlers/update_shout.rs new file mode 100644 index 0000000..7a7d8f3 --- /dev/null +++ b/crates/cli/src/alerts/handlers/update_shout.rs @@ -0,0 +1,12 @@ +// +use actix::prelude::*; + +use crate::alerts::{messages::UpdateShout, AlertsActor}; + +impl Handler for AlertsActor { + type Result = (); + + fn handle(&mut self, msg: UpdateShout, _ctx: &mut Self::Context) -> Self::Result { + self.shout.replace(msg.unwrap()); + } +} diff --git a/crates/cli/src/alerts/history.rs b/crates/cli/src/alerts/history.rs new file mode 100644 index 0000000..2848572 --- /dev/null +++ b/crates/cli/src/alerts/history.rs @@ -0,0 +1,2 @@ +#[derive(Debug, Default)] +pub struct History {} diff --git a/crates/cli/src/alerts/messages.rs b/crates/cli/src/alerts/messages.rs new file mode 100644 index 0000000..ef5f46e --- /dev/null +++ b/crates/cli/src/alerts/messages.rs @@ -0,0 +1,86 @@ +// +use derive_more::Deref as _; +use git_next_core::{git::UserNotification, message, server::Shout}; +use serde_json::json; + +message!(UpdateShout: Shout: "Updated Shout configuration"); + +message!(NotifyUser: UserNotification: "Request to send the message payload to the notification webhook"); +impl NotifyUser { + pub fn as_json(&self, timestamp: time::OffsetDateTime) -> serde_json::Value { + let timestamp = timestamp.unix_timestamp().to_string(); + match self.deref() { + UserNotification::CICheckFailed { + forge_alias, + repo_alias, + commit, + } => json!({ + "type": "cicheck.failed", + "timestamp": timestamp, + "data": { + "forge_alias": forge_alias, + "repo_alias": repo_alias, + "commit": { + "sha": commit.sha(), + "message": commit.message() + } + } + }), + UserNotification::RepoConfigLoadFailure { + forge_alias, + repo_alias, + reason, + } => json!({ + "type": "config.load.failed", + "timestamp": timestamp, + "data": { + "forge_alias": forge_alias, + "repo_alias": repo_alias, + "reason": reason + } + }), + UserNotification::WebhookRegistration { + forge_alias, + repo_alias, + reason, + } => json!({ + "type": "webhook.registration.failed", + "timestamp": timestamp, + "data": { + "forge_alias": forge_alias, + "repo_alias": repo_alias, + "reason": reason + } + }), + UserNotification::DevNotBasedOnMain { + forge_alias, + repo_alias, + dev_branch, + main_branch, + dev_commit, + main_commit, + } => json!({ + "type": "branch.dev.not-on-main", + "timestamp": timestamp, + "data": { + "forge_alias": forge_alias, + "repo_alias": repo_alias, + "branches": { + "dev": dev_branch, + "main": main_branch + }, + "commits": { + "dev": { + "sha": dev_commit.sha(), + "message": dev_commit.message() + }, + "main": { + "sha": main_commit.sha(), + "message": main_commit.message() + } + } + } + }), + } + } +} diff --git a/crates/cli/src/server/actor/handlers/notify_user/mod.rs b/crates/cli/src/alerts/mod.rs similarity index 75% rename from crates/cli/src/server/actor/handlers/notify_user/mod.rs rename to crates/cli/src/alerts/mod.rs index d28f482..9625792 100644 --- a/crates/cli/src/server/actor/handlers/notify_user/mod.rs +++ b/crates/cli/src/alerts/mod.rs @@ -1,44 +1,32 @@ -// -mod desktop; -mod email; -mod webhook; - use std::ops::Deref as _; +// use actix::prelude::*; -use desktop::send_desktop_notification; -use email::send_email; -use git_next_core::git::UserNotification; -use tracing::Instrument; -use webhook::send_webhook; +use derive_more::derive::Constructor; -use crate::{repo::messages::NotifyUser, server::actor::ServerActor}; +use git_next_core::{git::UserNotification, server::Shout}; -impl Handler for ServerActor { - type Result = (); +pub use history::History; +use messages::NotifyUser; - fn handle(&mut self, msg: NotifyUser, ctx: &mut Self::Context) -> Self::Result { - let Some(server_config) = &self.server_config else { - return; - }; - let shout_config = server_config.shout().clone(); - let net = self.net.clone(); - async move { - if let Some(webhook_config) = shout_config.webhook() { - send_webhook(&msg, webhook_config, &net).await; - } - if let Some(email_config) = shout_config.email() { - send_email(&msg, email_config); - } - if shout_config.desktop() { - send_desktop_notification(&msg); - } - } - .in_current_span() - .into_actor(self) - .wait(ctx); - } +mod desktop; +mod email; +mod handlers; +mod history; +pub mod messages; +mod webhook; + +#[derive(Debug, Constructor)] +pub struct AlertsActor { + shout: Option, // config for sending alerts to users + #[allow(dead_code)] // TODO (#128) Prevent duplicate user notifications + history: History, // record of alerts sent recently (e.g. 24 hours) + net: kxio::network::Network, +} + +impl Actor for AlertsActor { + type Context = Context; } fn short_message(msg: &NotifyUser) -> String { diff --git a/crates/cli/src/server/actor/handlers/notify_user/webhook.rs b/crates/cli/src/alerts/webhook.rs similarity index 97% rename from crates/cli/src/server/actor/handlers/notify_user/webhook.rs rename to crates/cli/src/alerts/webhook.rs index 4c72c5d..0108d15 100644 --- a/crates/cli/src/server/actor/handlers/notify_user/webhook.rs +++ b/crates/cli/src/alerts/webhook.rs @@ -3,7 +3,7 @@ use git_next_core::server::OutboundWebhook; use secrecy::ExposeSecret as _; use standardwebhooks::Webhook; -use crate::repo::messages::NotifyUser; +use crate::alerts::messages::NotifyUser; pub(super) async fn send_webhook( msg: &NotifyUser, diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index e5f3b9c..c5ceb23 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -1,4 +1,5 @@ // +mod alerts; mod file_watcher; mod forge; mod init; diff --git a/crates/cli/src/repo/mod.rs b/crates/cli/src/repo/mod.rs index a7e0a0c..c8b3325 100644 --- a/crates/cli/src/repo/mod.rs +++ b/crates/cli/src/repo/mod.rs @@ -1,9 +1,9 @@ // use actix::prelude::*; +use crate::alerts::messages::NotifyUser; use derive_more::Deref; use kxio::network::Network; -use messages::NotifyUser; use std::time::Duration; use tracing::{info, warn, Instrument}; diff --git a/crates/cli/src/server/actor/handlers/mod.rs b/crates/cli/src/server/actor/handlers/mod.rs index dcdcab6..c92aeeb 100644 --- a/crates/cli/src/server/actor/handlers/mod.rs +++ b/crates/cli/src/server/actor/handlers/mod.rs @@ -1,5 +1,4 @@ mod file_updated; -mod notify_user; mod receive_server_config; mod receive_valid_server_config; mod shutdown; diff --git a/crates/cli/src/server/actor/handlers/receive_valid_server_config.rs b/crates/cli/src/server/actor/handlers/receive_valid_server_config.rs index 3c3862a..53ce846 100644 --- a/crates/cli/src/server/actor/handlers/receive_valid_server_config.rs +++ b/crates/cli/src/server/actor/handlers/receive_valid_server_config.rs @@ -4,6 +4,7 @@ use actix::prelude::*; use tracing::info; use crate::{ + alerts::messages::UpdateShout, server::actor::{ messages::{ReceiveValidServerConfig, ValidServerConfig}, ServerActor, @@ -18,7 +19,7 @@ use crate::{ impl Handler for ServerActor { type Result = (); - fn handle(&mut self, msg: ReceiveValidServerConfig, ctx: &mut Self::Context) -> Self::Result { + fn handle(&mut self, msg: ReceiveValidServerConfig, _ctx: &mut Self::Context) -> Self::Result { let ValidServerConfig { server_config, socket_address, @@ -33,6 +34,7 @@ impl Handler for ServerActor { info!("Starting Webhook Server..."); let webhook_router = WebhookRouter::default().start(); let listen_url = server_config.listen().url(); + let alerts = self.alerts.clone(); // Forge Actors for (forge_alias, forge_config) in server_config.forges() { let repo_actors = self @@ -41,7 +43,7 @@ impl Handler for ServerActor { forge_alias.clone(), &server_storage, listen_url, - ctx.address().recipient(), + alerts.clone().recipient(), ) .into_iter() .map(|a| self.start_actor(a)) @@ -64,6 +66,8 @@ impl Handler for ServerActor { let webhook_actor_addr = WebhookActor::new(socket_address, webhook_router.recipient()).start(); self.webhook_actor_addr.replace(webhook_actor_addr); + let shout = server_config.shout().clone(); self.server_config.replace(server_config); + self.alerts.do_send(UpdateShout::new(shout)); } } diff --git a/crates/cli/src/server/actor/mod.rs b/crates/cli/src/server/actor/mod.rs index 2677e06..26e7179 100644 --- a/crates/cli/src/server/actor/mod.rs +++ b/crates/cli/src/server/actor/mod.rs @@ -10,11 +10,10 @@ mod handlers; pub mod messages; use crate::{ + alerts::messages::NotifyUser, + alerts::AlertsActor, forge::Forge, - repo::{ - messages::{CloneRepo, NotifyUser}, - RepoActor, - }, + repo::{messages::CloneRepo, RepoActor}, webhook::WebhookActor, }; @@ -56,6 +55,7 @@ pub struct ServerActor { webhook_actor_addr: Option>, fs: FileSystem, net: Network, + alerts: Addr, repository_factory: Box, sleep_duration: std::time::Duration, repo_actors: BTreeMap<(ForgeAlias, RepoAlias), Addr>, @@ -71,6 +71,7 @@ impl ServerActor { pub fn new( fs: FileSystem, net: Network, + alerts: Addr, repo: Box, sleep_duration: std::time::Duration, ) -> Self { @@ -81,6 +82,7 @@ impl ServerActor { webhook_actor_addr: None, fs, net, + alerts, repository_factory: repo, sleep_duration, repo_actors: BTreeMap::new(), diff --git a/crates/cli/src/server/actor/tests/given.rs b/crates/cli/src/server/actor/tests/given.rs index 9dd89cd..559a67e 100644 --- a/crates/cli/src/server/actor/tests/given.rs +++ b/crates/cli/src/server/actor/tests/given.rs @@ -1,3 +1,8 @@ +use actix::prelude::*; + +use crate::alerts::{AlertsActor, History}; + +// pub fn a_filesystem() -> kxio::fs::FileSystem { kxio::fs::temp().unwrap_or_else(|e| panic!("{}", e)) } @@ -5,3 +10,7 @@ pub fn a_filesystem() -> kxio::fs::FileSystem { pub fn a_network() -> kxio::network::MockNetwork { kxio::network::MockNetwork::new() } + +pub fn an_alerts_actor(net: kxio::network::Network) -> Addr { + AlertsActor::new(None, History::default(), net).start() +} diff --git a/crates/cli/src/server/actor/tests/receive_server_config.rs b/crates/cli/src/server/actor/tests/receive_server_config.rs index 40cb277..8879a17 100644 --- a/crates/cli/src/server/actor/tests/receive_server_config.rs +++ b/crates/cli/src/server/actor/tests/receive_server_config.rs @@ -18,11 +18,12 @@ async fn when_webhook_url_has_trailing_slash_should_not_send() { // parameters let fs = given::a_filesystem(); let net = given::a_network(); + let alerts = given::an_alerts_actor(net.clone().into()); let repo = git::repository::factory::mock(); let duration = std::time::Duration::from_millis(1); // sut - let server = ServerActor::new(fs.clone(), net.into(), repo, duration); + let server = ServerActor::new(fs.clone(), net.into(), alerts, repo, duration); // collaborators let listen = Listen::new( diff --git a/crates/cli/src/server/mod.rs b/crates/cli/src/server/mod.rs index f2b9673..3c0d8f9 100644 --- a/crates/cli/src/server/mod.rs +++ b/crates/cli/src/server/mod.rs @@ -6,7 +6,10 @@ mod tests; use actix::prelude::*; -use crate::file_watcher::{watch_file, FileUpdated}; +use crate::{ + alerts::{AlertsActor, History}, + file_watcher::{watch_file, FileUpdated}, +}; use actor::ServerActor; use git_next_core::git::RepositoryFactory; @@ -41,8 +44,18 @@ pub fn start( init_logging(); let execution = async move { + info!("Starting Alert Dispatcher..."); + let alerts_addr = AlertsActor::new(None, History::default(), net.clone()).start(); + info!("Starting Server..."); - let server = ServerActor::new(fs.clone(), net.clone(), repo, sleep_duration).start(); + let server = ServerActor::new( + fs.clone(), + net.clone(), + alerts_addr.clone(), + repo, + sleep_duration, + ) + .start(); server.do_send(FileUpdated); info!("Starting File Watcher...");