// use std::{ collections::BTreeMap, net::SocketAddr, ops::Deref, path::{Path, PathBuf}, str::FromStr, }; use derive_more::{Constructor, Display}; use kxio::fs::FileSystem; use secrecy::Secret; use serde::{Deserialize, Serialize}; use tracing::info; use crate::{ config::{ForgeAlias, ForgeConfig, RepoAlias}, newtype, }; #[derive(Debug, thiserror::Error)] pub enum Error { #[error("fs: {0}")] KxioFs(#[from] kxio::fs::Error), #[error("deserialise toml: {0}")] TomlDe(#[from] toml::de::Error), #[error("parse IP addres/port: {0}")] AddressParse(#[from] std::net::AddrParseError), } type Result = core::result::Result; /// Mapped from the `git-next-server.toml` file #[derive( Clone, Debug, derive_more::From, PartialEq, Eq, PartialOrd, Ord, derive_more::AsRef, serde::Deserialize, derive_more::Constructor, )] pub struct AppConfig { listen: Listen, shout: Shout, storage: Storage, pub forge: BTreeMap, } impl AppConfig { #[tracing::instrument(skip_all)] pub fn load(fs: &FileSystem) -> Result { let file = fs.base().join("git-next-server.toml"); info!(?file, ""); let str = fs.file_read_to_string(&file)?; Ok(toml::from_str(&str)?) } pub fn forges(&self) -> impl Iterator { self.forge .iter() .map(|(alias, forge)| (ForgeAlias::new(alias.clone()), forge)) } #[must_use] pub const fn storage(&self) -> &Storage { &self.storage } #[must_use] pub const fn shout(&self) -> &Shout { &self.shout } #[must_use] pub const fn listen(&self) -> &Listen { &self.listen } /// Returns the `SocketAddr` to listen to for incoming webhooks. /// /// # Errors /// /// Will return an `Err` if the IP address or port from the config file are invalid. pub fn listen_socket_addr(&self) -> Result { self.listen.http.socket_addr() } } /// Defines how the server receives webhook notifications from forges. #[derive( Clone, Debug, derive_more::From, PartialEq, Eq, PartialOrd, Ord, derive_more::AsRef, serde::Deserialize, derive_more::Constructor, )] pub struct Listen { http: Http, url: ListenUrl, } impl Listen { // /// Returns the URL a Repo will listen to for updates from the Forge // pub fn repo_url(&self, forge_alias: ForgeAlias, repo_alias: RepoAlias) -> RepoListenUrl { // self.url.repo_url(forge_alias, repo_alias) // } #[must_use] pub const fn url(&self) -> &ListenUrl { &self.url } } newtype!( ListenUrl, String, Serialize, Deserialize, PartialOrd, Ord, Display, "The base url for receiving all webhooks from all forges" ); impl ListenUrl { #[must_use] pub fn repo_url(&self, forge_alias: ForgeAlias, repo_alias: RepoAlias) -> RepoListenUrl { RepoListenUrl::new((self.clone(), forge_alias, repo_alias)) } } newtype!(ForgeWebhookUrl, String, "Raw URL from a forge Webhook"); newtype!( RepoListenUrl, (ListenUrl, ForgeAlias, RepoAlias), "URL to listen for webhook from forges" ); impl Display for RepoListenUrl { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "{}/{}/{}", self.deref().0, self.deref().1, self.deref().2 ) } } impl From for ForgeWebhookUrl { fn from(value: RepoListenUrl) -> Self { Self::new(value.to_string()) } } /// Defines the port the server will listen to for incoming webhooks messages #[derive( Clone, Debug, derive_more::From, PartialEq, Eq, PartialOrd, Ord, derive_more::AsRef, serde::Deserialize, derive_more::Constructor, )] pub struct Http { addr: String, port: u16, } impl Http { fn socket_addr(&self) -> Result { Ok(SocketAddr::from_str(&format!( "{}:{}", self.addr, self.port ))?) } } /// The directory to store server data, such as cloned repos #[derive( Clone, Debug, derive_more::From, PartialEq, Eq, PartialOrd, Ord, derive_more::AsRef, serde::Deserialize, derive_more::Constructor, )] pub struct Storage { path: PathBuf, } impl Storage { #[must_use] pub fn path(&self) -> &Path { self.path.as_path() } } /// Defines the Webhook Forges should send updates to /// Must be an address that is accessible from the remote forge #[derive( Clone, Debug, Default, derive_more::From, PartialEq, Eq, PartialOrd, Ord, derive_more::AsRef, serde::Deserialize, Constructor, )] pub struct Shout { webhook: Option, email: Option, desktop: Option, } impl Shout { #[must_use] pub const fn webhook(&self) -> Option<&OutboundWebhook> { self.webhook.as_ref() } #[cfg(test)] pub(crate) fn webhook_url(&self) -> Option { self.webhook.clone().map(|x| x.url) } pub fn webhook_secret(&self) -> Option> { self.webhook.clone().map(|x| x.secret).map(Secret::new) } #[must_use] pub const fn email(&self) -> Option<&EmailConfig> { self.email.as_ref() } #[must_use] pub const fn desktop(&self) -> Option { self.desktop } } #[derive( Clone, Debug, derive_more::From, PartialEq, Eq, PartialOrd, Ord, serde::Deserialize, derive_more::Constructor, )] pub struct OutboundWebhook { url: String, secret: String, } impl OutboundWebhook { #[must_use] pub fn url(&self) -> &str { self.url.as_ref() } #[must_use] pub fn secret(&self) -> Secret { Secret::new(self.secret.clone()) } } #[derive( Clone, Debug, derive_more::From, PartialEq, Eq, PartialOrd, Ord, serde::Deserialize, derive_more::Constructor, )] pub struct EmailConfig { from: String, to: String, // email will be sent via sendmail, unless smtp is specified smtp: Option, } impl EmailConfig { #[must_use] pub fn from(&self) -> &str { &self.from } #[must_use] pub fn to(&self) -> &str { &self.to } #[must_use] pub const fn smtp(&self) -> Option<&SmtpConfig> { self.smtp.as_ref() } } #[derive( Clone, Debug, derive_more::From, PartialEq, Eq, PartialOrd, Ord, serde::Deserialize, derive_more::Constructor, )] pub struct SmtpConfig { hostname: String, username: String, password: String, } impl SmtpConfig { #[must_use] pub fn username(&self) -> &str { &self.username } #[must_use] pub fn password(&self) -> &str { &self.password } #[must_use] pub fn hostname(&self) -> &str { &self.hostname } }