diff --git a/Cargo.lock b/Cargo.lock index 705755b..062f632 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -512,6 +512,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "chumsky" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" +dependencies = [ + "hashbrown 0.14.5", + "stacker", +] + [[package]] name = "cipher" version = "0.4.4" @@ -896,6 +906,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "email-encoding" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea3d894bbbab314476b265f9b2d46bf24b123a36dd0e96b06a1b49545b9d9dcc" +dependencies = [ + "base64 0.22.1", + "memchr", +] + +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -1150,7 +1176,7 @@ dependencies = [ "libc", "log", "rustversion", - "windows", + "windows 0.48.0", ] [[package]] @@ -1350,6 +1376,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "hostname" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba" +dependencies = [ + "cfg-if", + "libc", + "windows 0.52.0", +] + [[package]] name = "http" version = "0.2.12" @@ -1840,6 +1877,34 @@ dependencies = [ "spin", ] +[[package]] +name = "lettre" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0161e452348e399deb685ba05e55ee116cae9410f4f51fe42d597361444521d9" +dependencies = [ + "async-trait", + "base64 0.22.1", + "chumsky", + "email-encoding", + "email_address", + "fastrand", + "futures-io", + "futures-util", + "hostname", + "httpdate", + "idna", + "mime", + "native-tls", + "nom", + "percent-encoding", + "quoted_printable", + "socket2", + "tokio", + "tokio-native-tls", + "url", +] + [[package]] name = "libc" version = "0.2.167" @@ -1912,6 +1977,7 @@ dependencies = [ "config", "fedimint-tonic-lnd", "ipnetwork", + "lettre", "lnvps_db", "log", "nostr", @@ -2686,6 +2752,15 @@ dependencies = [ "prost", ] +[[package]] +name = "psm" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200b9ff220857e53e184257720a14553b2f4aa02577d2ed9842d45d4b9654810" +dependencies = [ + "cc", +] + [[package]] name = "quote" version = "1.0.37" @@ -2695,6 +2770,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "quoted_printable" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" + [[package]] name = "rand" version = "0.8.5" @@ -3674,6 +3755,19 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "stacker" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799c883d55abdb5e98af1a7b3f23b9b6de8ecada0ecac058672d7635eb48ca7b" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.59.0", +] + [[package]] name = "state" version = "0.6.0" @@ -4445,6 +4539,16 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index 2812ba6..ce9d908 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,3 +28,4 @@ rand = "0.8.5" clap = { version = "4.5.21", features = ["derive"] } ssh2 = "0.9.4" ssh-key = "0.6.7" +lettre = { version = "0.11.10", features = ["tokio1-native-tls"] } diff --git a/src/bin/api.rs b/src/bin/api.rs index 1af3638..b5815ab 100644 --- a/src/bin/api.rs +++ b/src/bin/api.rs @@ -38,7 +38,12 @@ async fn main() -> Result<(), Error> { db.migrate().await?; let exchange = ExchangeRateCache::new(); - let lnd = connect(settings.lnd.url, settings.lnd.cert, settings.lnd.macaroon).await?; + let lnd = connect( + settings.lnd.url.clone(), + settings.lnd.cert.clone(), + settings.lnd.macaroon.clone(), + ) + .await?; #[cfg(debug_assertions)] { let setup_script = include_str!("../../dev_setup.sql"); @@ -52,13 +57,18 @@ async fn main() -> Result<(), Error> { .get_provisioner(db.clone(), lnd.clone(), exchange.clone()); worker_provisioner.init().await?; - let mut worker = Worker::new( - db.clone(), - worker_provisioner, - settings.delete_after, - status.clone(), - ); + let mut worker = Worker::new(db.clone(), worker_provisioner, &settings, status.clone()); let sender = worker.sender(); + + // send a startup notification + if let Some(admin) = &settings.smtp.and_then(|s| s.admin) { + sender.send(WorkJob::SendNotification { + title: Some("Startup".to_string()), + message: "System is starting!".to_string(), + user_id: *admin, + })?; + } + tokio::spawn(async move { loop { if let Err(e) = worker.handle().await { diff --git a/src/settings.rs b/src/settings.rs index 78af7c7..88cd1ed 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -6,25 +6,48 @@ use lnvps_db::LNVpsDb; use serde::{Deserialize, Serialize}; use std::path::PathBuf; -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct Settings { pub listen: Option, pub db: String, pub lnd: LndConfig, + + /// Main control process impl pub provisioner: ProvisionerConfig, /// Number of days after an expired VM is deleted pub delete_after: u16, + + /// SMTP settings for sending emails + pub smtp: Option, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct LndConfig { pub url: String, pub cert: PathBuf, pub macaroon: PathBuf, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct SmtpConfig { + /// Admin user id, for sending system notifications + pub admin: Option, + + /// Email server host:port + pub server: String, + + /// From header to use, otherwise empty + pub from: Option, + + /// Username for SMTP connection + pub username: String, + + /// Password for SMTP connection + pub password: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] pub enum ProvisionerConfig { Proxmox { /// Readonly mode, don't spawn any VM's diff --git a/src/worker.rs b/src/worker.rs index eea25a8..0236636 100644 --- a/src/worker.rs +++ b/src/worker.rs @@ -1,11 +1,18 @@ use crate::host::get_host_client; use crate::host::proxmox::{VmInfo, VmStatus}; use crate::provisioner::Provisioner; +use crate::settings::{Settings, SmtpConfig}; use crate::status::{VmRunningState, VmState, VmStateCache}; use anyhow::Result; use chrono::{Days, Utc}; +use lettre::message::MessageBuilder; +use lettre::transport::smtp::authentication::Credentials; +use lettre::transport::smtp::SmtpTransportBuilder; +use lettre::AsyncTransport; +use lettre::{AsyncSmtpTransport, SmtpTransport, Tokio1Executor, Transport}; use lnvps_db::LNVpsDb; use log::{debug, error, info}; +use rocket::futures::SinkExt; use std::ops::Add; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; @@ -17,11 +24,15 @@ pub enum WorkJob { /// This job starts a vm if stopped and also creates the vm if it doesn't exist yet CheckVm { vm_id: u64 }, /// Send a notification to the users chosen contact preferences - SendNotification { user_id: u64, message: String }, + SendNotification { + user_id: u64, + message: String, + title: Option, + }, } pub struct Worker { - delete_after: u16, + settings: WorkerSettings, db: Box, provisioner: Box, vm_state_cache: VmStateCache, @@ -29,11 +40,25 @@ pub struct Worker { rx: UnboundedReceiver, } +pub struct WorkerSettings { + pub delete_after: u16, + pub smtp: Option, +} + +impl Into for &Settings { + fn into(self) -> WorkerSettings { + WorkerSettings { + delete_after: self.delete_after, + smtp: self.smtp.clone(), + } + } +} + impl Worker { pub fn new( db: D, provisioner: P, - delete_after: u16, + settings: impl Into, vm_state_cache: VmStateCache, ) -> Self { let (tx, rx) = unbounded_channel(); @@ -41,7 +66,7 @@ impl Worker { db: Box::new(db), provisioner: Box::new(provisioner), vm_state_cache, - delete_after, + settings: settings.into(), tx, rx, } @@ -77,7 +102,11 @@ impl Worker { self.provisioner.stop_vm(db_vm.id).await?; } // Delete VM if expired > 3 days - if db_vm.expires.add(Days::new(self.delete_after as u64)) < Utc::now() { + if db_vm + .expires + .add(Days::new(self.settings.delete_after as u64)) + < Utc::now() + { info!("Deleting expired VM {}", db_vm.id); self.provisioner.delete_vm(db_vm.id).await?; } @@ -139,6 +168,44 @@ impl Worker { Ok(()) } + async fn send_notification( + &self, + user_id: u64, + message: String, + title: Option, + ) -> Result<()> { + let user = self.db.get_user(user_id).await?; + if let Some(smtp) = self.settings.smtp.as_ref() { + if user.contact_email && user.email.is_some() { + // send email + let mut b = MessageBuilder::new().to(user.email.unwrap().parse()?); + if let Some(t) = title { + b = b.subject(t); + } + if let Some(f) = &smtp.from { + b = b.from(f.parse()?); + } + let msg = b.body(message)?; + + let mut sender = AsyncSmtpTransport::::relay(&smtp.server)? + .credentials(Credentials::new( + smtp.username.to_string(), + smtp.password.to_string(), + )) + .build(); + + sender.send(msg).await?; + } + } + if user.contact_nip4 { + // send DM + } + if user.contact_nip17 { + // send dm + } + Ok(()) + } + pub async fn handle(&mut self) -> Result<()> { while let Some(job) = self.rx.recv().await { match job { @@ -147,7 +214,15 @@ impl Worker { error!("Failed to check VM {}: {}", vm_id, e); } } - WorkJob::SendNotification { .. } => {} + WorkJob::SendNotification { + user_id, + message, + title, + } => { + if let Err(e) = self.send_notification(user_id, message, title).await { + error!("Failed to send notification {}: {}", user_id, e); + } + } WorkJob::CheckVms => { if let Err(e) = self.check_vms().await { error!("Failed to check VMs: {}", e);