diff --git a/Cargo.lock b/Cargo.lock index 80bcb6b..cbab6fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -456,6 +456,9 @@ name = "bitflags" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +dependencies = [ + "serde", +] [[package]] name = "block-buffer" @@ -601,6 +604,55 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +[[package]] +name = "config" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" +dependencies = [ + "async-trait", + "convert_case", + "json5", + "lazy_static", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml", + "yaml-rust", +] + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -650,6 +702,12 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -752,6 +810,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + [[package]] name = "either" version = "1.12.0" @@ -1094,6 +1161,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" + [[package]] name = "hashbrown" version = "0.14.5" @@ -1462,6 +1535,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + [[package]] name = "jsonwebtoken" version = "9.3.0" @@ -1489,6 +1573,12 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -1615,6 +1705,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.3" @@ -1671,6 +1767,16 @@ dependencies = [ "libc", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nostr" version = "0.31.2" @@ -1908,6 +2014,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-multimap" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ed8acf08e98e744e5384c8bc63ceb0364e68a6854187221c18df61c4797690e" +dependencies = [ + "dlv-list", + "hashbrown 0.13.2", +] + [[package]] name = "overload" version = "0.1.1" @@ -1948,6 +2064,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + [[package]] name = "pbkdf2" version = "0.12.2" @@ -2305,6 +2427,7 @@ dependencies = [ "anyhow", "axum 0.7.5", "clap", + "config", "env_logger", "futures", "gcloud-sdk", @@ -2442,6 +2565,18 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.7", + "bitflags 2.5.0", + "serde", + "serde_derive", +] + [[package]] name = "rsb_derive" version = "0.5.1" @@ -2453,6 +2588,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "rust-ini" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e2a3bcec1f113553ef1c88aae6c020a369d03d55b58de9869a0908930385091" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -2767,6 +2912,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3125,6 +3279,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -3267,6 +3430,40 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" +dependencies = [ + "indexmap 2.2.6", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tonic" version = "0.11.0" @@ -3491,6 +3688,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + [[package]] name = "universal-hash" version = "0.5.1" @@ -3849,6 +4052,15 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +[[package]] +name = "winnow" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c52728401e1dc672a56e81e593e912aa54c78f40246869f78359a2bf24d29d" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.50.0" @@ -3869,6 +4081,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "yansi" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 37baa92..fdc874b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ default-run = "reportinator_server" anyhow = "1.0.86" axum = "0.7.5" clap = "4.5.4" +config_rs = { version = "0.14", package = "config", features = ["yaml"] } env_logger = "0.11.3" futures = "0.3.30" gcloud-sdk = { version = "0.24.6", features = ["google-pubsub-v1"] } diff --git a/config/settings.yml b/config/settings.yml new file mode 100644 index 0000000..fc649fb --- /dev/null +++ b/config/settings.yml @@ -0,0 +1,17 @@ +reportinator: + # test key + keys: 'feef9c2dcd6a1175a97dfbde700fa54f58ce69d4f30963f70efcc7257636759f' + relays: "ws://localhost" + +slack: + token: '' + channel_id: '' + signing_secret: '' + +http: + # Best practice would probably say + # default this to 127.0.0.1, and override + # when deployed. + bind_addr: '0.0.0.0' + bind_port: 3000 + templates_dir: 'templates' \ No newline at end of file diff --git a/src/actors/slack_writer.rs b/src/actors/slack_writer.rs index 9426889..cc36656 100644 --- a/src/actors/slack_writer.rs +++ b/src/actors/slack_writer.rs @@ -2,6 +2,7 @@ /// how to write to slack and can fetch info from Nostr to create its messages use super::messages::SupervisorMessage; use crate::actors::messages::SlackWriterMessage; +use crate::adapters::slack_client_adapter::Config as SlackConfig; use crate::domain_objects::{ReportRequest, ReportTarget}; use anyhow::Result; use metrics::counter; @@ -153,7 +154,11 @@ mod tests { } pub trait SlackClientPortBuilder: Send + Sync + 'static { - fn build(&self, nostr_actor: ActorRef) -> Result; + fn build( + &self, + config: SlackConfig, + nostr_actor: ActorRef, + ) -> Result; } #[ractor::async_trait] diff --git a/src/actors/supervisor.rs b/src/actors/supervisor.rs index ba99187..80bf327 100644 --- a/src/actors/supervisor.rs +++ b/src/actors/supervisor.rs @@ -3,6 +3,7 @@ use crate::actors::{ EventEnqueuer, GiftUnwrapper, NostrPort, PubsubPort, RelayEventDispatcher, SlackClientPortBuilder, SlackWriter, }; +use crate::config::Config; use anyhow::Result; use metrics::counter; use nostr_sdk::prelude::*; @@ -10,16 +11,14 @@ use ractor::{call_t, cast, Actor, ActorProcessingErr, ActorRef, SupervisionEvent use tracing::error; pub struct Supervisor { + config: Config, _phantom: std::marker::PhantomData<(T, U, V)>, } -impl Default for Supervisor -where - T: NostrPort, - U: PubsubPort, - V: SlackClientPortBuilder, -{ - fn default() -> Self { + +impl Supervisor { + pub fn new(config: Config) -> Self { Self { + config, _phantom: std::marker::PhantomData, } } @@ -76,7 +75,7 @@ where ) .await?; - let slack_client_port = slack_writer_builder.build(myself.clone())?; + let slack_client_port = slack_writer_builder.build(self.config.get()?, myself.clone())?; let (slack_writer, _slack_writer_handle) = Actor::spawn_linked( Some("slack_writer".to_string()), diff --git a/src/adapters/http_server.rs b/src/adapters/http_server.rs index 5a0966f..e365e06 100644 --- a/src/adapters/http_server.rs +++ b/src/adapters/http_server.rs @@ -2,18 +2,34 @@ mod app_errors; mod router; mod slack_interactions_route; use crate::actors::messages::SupervisorMessage; +use crate::config::Config as ConfigTree; use anyhow::{Context, Result}; use axum::Router; use handlebars::Handlebars; use ractor::ActorRef; +use reportinator_server::config::Configurable; use router::create_router; +use serde::Deserialize; use std::net::SocketAddr; +use std::str::FromStr; use std::sync::Arc; use std::time::Duration; use tokio::time::timeout; use tokio_util::sync::CancellationToken; use tracing::info; +#[derive(Debug, Clone, Deserialize)] +pub struct Config { + bind_addr: String, + bind_port: u16, +} + +impl Configurable for Config { + fn key() -> &'static str { + "http" + } +} + #[derive(Clone)] pub struct WebAppState { hb: Arc>, @@ -23,17 +39,22 @@ pub struct WebAppState { pub struct HttpServer; impl HttpServer { pub async fn run( - cancellation_token: CancellationToken, + config: ConfigTree, event_dispatcher: ActorRef, + cancellation_token: CancellationToken, ) -> Result<()> { - let router = create_router(event_dispatcher)?; + let router = create_router(&config, event_dispatcher)?; - start_http_server(router, cancellation_token).await + start_http_server(&config.get()?, router, cancellation_token).await } } -async fn start_http_server(router: Router, cancellation_token: CancellationToken) -> Result<()> { - let addr = SocketAddr::from(([0, 0, 0, 0], 3000)); +async fn start_http_server( + config: &Config, + router: Router, + cancellation_token: CancellationToken, +) -> Result<()> { + let addr = SocketAddr::from_str(&format!("{}:{}", config.bind_addr, config.bind_port))?; let listener = tokio::net::TcpListener::bind(addr).await?; let token_clone = cancellation_token.clone(); let server_future = tokio::spawn(async { diff --git a/src/adapters/http_server/router.rs b/src/adapters/http_server/router.rs index 3418129..f1e558b 100644 --- a/src/adapters/http_server/router.rs +++ b/src/adapters/http_server/router.rs @@ -1,6 +1,7 @@ use super::slack_interactions_route::slack_interactions_route; use super::WebAppState; use crate::actors::messages::SupervisorMessage; +use crate::config::Config as ConfigTree; use anyhow::Result; use axum::{extract::State, http::HeaderMap, response::Html}; use axum::{response::IntoResponse, routing::get, Router}; @@ -8,8 +9,9 @@ use handlebars::Handlebars; use metrics::describe_counter; use metrics_exporter_prometheus::PrometheusBuilder; use ractor::ActorRef; +use reportinator_server::config::Configurable; +use serde::Deserialize; use serde_json::json; -use std::env; use std::sync::Arc; use std::time::Duration; use tower_http::trace::{DefaultMakeSpan, DefaultOnResponse, TraceLayer}; @@ -17,8 +19,22 @@ use tower_http::LatencyUnit; use tower_http::{timeout::TimeoutLayer, trace::DefaultOnFailure}; use tracing::Level; -pub fn create_router(message_dispatcher: ActorRef) -> Result { - let web_app_state = create_web_app_state(message_dispatcher)?; +#[derive(Debug, Clone, Deserialize)] +pub struct Config { + pub templates_dir: String, +} + +impl Configurable for Config { + fn key() -> &'static str { + "http" + } +} + +pub fn create_router( + config: &ConfigTree, + message_dispatcher: ActorRef, +) -> Result { + let web_app_state = create_web_app_state(&config.get()?, message_dispatcher)?; let metrics_handle = setup_metrics()?; @@ -34,18 +50,20 @@ pub fn create_router(message_dispatcher: ActorRef) -> Result< Ok(Router::new() // TODO: Move this one away to its own file too .route("/", get(serve_root_page)) - .merge(slack_interactions_route()?) + .merge(slack_interactions_route(&config.get()?)?) .layer(tracing_layer) .layer(TimeoutLayer::new(Duration::from_secs(1))) .with_state(web_app_state) .route("/metrics", get(|| async move { metrics_handle.render() }))) } -fn create_web_app_state(message_dispatcher: ActorRef) -> Result { - let templates_dir = env::var("TEMPLATES_DIR").unwrap_or_else(|_| "/app/templates".to_string()); +fn create_web_app_state( + config: &Config, + message_dispatcher: ActorRef, +) -> Result { let mut hb = Handlebars::new(); - hb.register_template_file("root", format!("{}/root.hbs", templates_dir)) + hb.register_template_file("root", format!("{}/root.hbs", config.templates_dir)) .map_err(|e| anyhow::anyhow!("Failed to load template: {}", e))?; Ok(WebAppState { diff --git a/src/adapters/http_server/slack_interactions_route.rs b/src/adapters/http_server/slack_interactions_route.rs index d3f7d5e..510e9a3 100644 --- a/src/adapters/http_server/slack_interactions_route.rs +++ b/src/adapters/http_server/slack_interactions_route.rs @@ -1,27 +1,37 @@ use super::app_errors::AppError; use super::WebAppState; use crate::actors::messages::SupervisorMessage; +use crate::config::Configurable; use crate::domain_objects::{ModerationCategory, ReportRequest, ReportTarget}; -use anyhow::{anyhow, bail, Context, Result}; +use anyhow::{anyhow, bail, Result}; use axum::{extract::State, routing::post, Extension, Router}; use nostr_sdk::prelude::*; use ractor::{call_t, cast, ActorRef}; use reqwest::Client as ReqwestClient; +use serde::Deserialize; use serde_json::{json, Value}; use slack_morphism::prelude::*; +use std::str::FromStr; use std::sync::Arc; -use std::{env, str::FromStr}; use tracing::{debug, error, info}; -pub fn slack_interactions_route() -> Result> { +#[derive(Debug, Clone, Deserialize)] +pub struct Config { + signing_secret: SlackSigningSecret, +} + +impl Configurable for Config { + fn key() -> &'static str { + "slack" + } +} + +pub fn slack_interactions_route(config: &Config) -> Result> { let client = prepare_slack_client()?; let listener_environment = prepare_listener_environment(client); - let signing_secret = env::var("SLACK_SIGNING_SECRET") - .context("Missing SLACK_SIGNING_SECRET") - .map(|secret| secret.into())?; let listener = SlackEventsAxumListener::::new(listener_environment); let slack_layer = listener - .events_layer(&signing_secret) + .events_layer(&config.signing_secret) .with_event_extractor(SlackEventsExtractors::interaction_event()); let route = Router::new().route( @@ -419,7 +429,11 @@ mod tests { hb: Arc::new(Handlebars::new()), }; - let router = slack_interactions_route().unwrap().with_state(state); + let router = slack_interactions_route(&Config { + signing_secret: String::new().into(), + }) + .unwrap() + .with_state(state); let response = router .oneshot( diff --git a/src/adapters/slack_client_adapter.rs b/src/adapters/slack_client_adapter.rs index 9d792d8..2d44db2 100644 --- a/src/adapters/slack_client_adapter.rs +++ b/src/adapters/slack_client_adapter.rs @@ -1,5 +1,6 @@ use crate::actors::messages::SupervisorMessage; use crate::actors::{SlackClientPort, SlackClientPortBuilder}; +use crate::config::Configurable; use crate::domain_objects::{ModerationCategory, ReportRequest}; use anyhow::Result; use hyper_rustls::HttpsConnector; @@ -7,12 +8,25 @@ use hyper_util::client::legacy::connect::HttpConnector; use nostr_sdk::prelude::PublicKey; use nostr_sdk::ToBech32; use ractor::{call_t, ActorRef}; +use serde::Deserialize; use slack_morphism::prelude::*; -use std::env; use tracing::info; +#[derive(Debug, Clone, Deserialize)] +pub struct Config { + pub token: SlackApiToken, + pub channel_id: SlackChannelId, +} + +impl Configurable for Config { + fn key() -> &'static str { + "slack" + } +} + #[derive(Clone)] pub struct SlackClientAdapter { + config: Config, client: SlackClient>>, nostr_actor: ActorRef, } @@ -21,9 +35,14 @@ pub struct SlackClientAdapter { pub struct SlackClientAdapterBuilder {} impl SlackClientPortBuilder for SlackClientAdapterBuilder { - fn build(&self, nostr_actor: ActorRef) -> Result { + fn build( + &self, + config: Config, + nostr_actor: ActorRef, + ) -> Result { let client = SlackClient::new(SlackClientHyperConnector::new()?); Ok(SlackClientAdapter { + config, client, nostr_actor, }) @@ -31,15 +50,11 @@ impl SlackClientPortBuilder for SlackClientAdapterBuilder { } impl SlackClientAdapter { - async fn post_message(&self, message: SlackApiChatPostMessageRequest) -> Result<()> { - let slack_token = env::var("SLACK_TOKEN")?; - let token: SlackApiToken = SlackApiToken::new(slack_token.into()); - let session = self.client.open_session(&token); + async fn post_message(&self, message: SlackApiChatPostMessageRequest) { + let session = self.client.open_session(&self.config.token); let post_chat_resp = session.chat_post_message(&message).await; info!("post chat resp: {:#?}", &post_chat_resp); - - Ok(()) } // This fn is currently duplicated and lives too in the http client adapter. @@ -85,11 +100,14 @@ impl SlackClientPort for SlackClientAdapter { reporter_pubkey_or_nip05_link, ); - let channel_id = env::var("SLACK_CHANNEL_ID")?; - let message_req = - SlackApiChatPostMessageRequest::new(channel_id.into(), message.render_template()); + let message_req = SlackApiChatPostMessageRequest::new( + self.config.channel_id.clone(), + message.render_template(), + ); - self.post_message(message_req).await + self.post_message(message_req).await; + + Ok(()) } } diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..dbe1d26 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,74 @@ +pub mod reportinator; +pub use reportinator::Config as ReportinatorConfig; + +use anyhow::{Context, Result}; +use config_rs::{Config as ConfigTree, Environment, File}; +use serde::de::DeserializeOwned; +use std::{any::type_name, env}; + +/* + * Constants + */ + +pub const ENVIRONMENT_PREFIX: &str = "APP"; +pub const CONFIG_SEPARATOR: &str = "__"; + +#[must_use] +pub fn environment() -> String { + env::var(format!("{ENVIRONMENT_PREFIX}{CONFIG_SEPARATOR}ENVIRONMENT")) + .unwrap_or_else(|_| "development".into()) +} + +/* + * Configuration + */ + +pub trait Configurable { + fn key() -> &'static str; +} + +#[derive(Debug, Clone)] +pub struct Config { + config: ConfigTree, +} + +impl Config { + pub fn new(config_dir: &str) -> Result { + let environment = environment(); + + let default_config_path = format!("{}/settings", &config_dir); + let env_config_path = format!("{}/settings.{}", &config_dir, &environment); + let local_config_path = format!("{}/settings.local", &config_dir); + + ConfigTree::builder() + .add_source(File::with_name(&default_config_path)) + .add_source(File::with_name(&env_config_path).required(false)) + .add_source(File::with_name(&local_config_path).required(false)) + .add_source(Environment::with_prefix(ENVIRONMENT_PREFIX).separator(CONFIG_SEPARATOR)) + .build() + .map(|c| Config { config: c }) + .map_err(Into::into) + } + + pub fn get(&self) -> Result + where + T: Configurable, + T: DeserializeOwned, + { + self.config.get::(T::key()).context(format!( + "Error loading configuration for `{}` at `{}`", + type_name::(), + T::key(), + )) + } + + pub fn get_by_key(&self, key: &str) -> Result + where + T: DeserializeOwned, + { + self.config.get::(key).context(format!( + "Error loading configuration for `{}` at `{key}`", + type_name::(), + )) + } +} diff --git a/src/config/reportinator.rs b/src/config/reportinator.rs new file mode 100644 index 0000000..a575bfa --- /dev/null +++ b/src/config/reportinator.rs @@ -0,0 +1,57 @@ +use crate::config::Configurable; +use nostr_sdk::Keys; +use serde::{de, Deserialize, Deserializer}; +use std::sync::OnceLock; + +#[derive(Debug, Clone, Deserialize)] +pub struct Config { + #[serde(deserialize_with = "parse_keys")] + pub keys: Keys, + #[serde(deserialize_with = "parse_relays")] + pub relays: Vec, +} + +impl Configurable for Config { + fn key() -> &'static str { + "reportinator" + } +} + +fn parse_keys<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + Keys::parse(s).map_err(de::Error::custom) +} + +fn parse_relays<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + + if s.trim().is_empty() { + return Err(anyhow::anyhow!("RELAY_ADDRESSES_CSV env variable is empty")) + .map_err(de::Error::custom); + } + + Ok(s.split(',').map(|s| s.trim().to_string()).collect()) +} + +/* + * This is hopefully temporary. Generally its better to provide config + * via dependency injection, instead of having global state. Based on + * the current architecture though, there were a couple places where + * it was non-trivial to pass configuration to. + */ +static CONFIG: OnceLock = OnceLock::new(); + +/// This will panic if config was not set. +pub fn config<'a>() -> &'a Config { + CONFIG.get().unwrap() +} + +pub fn set_config(config: Config) { + CONFIG.set(config).expect("Failed to set config"); +} diff --git a/src/domain_objects/moderated_report.rs b/src/domain_objects/moderated_report.rs index 19942e2..574d8ae 100644 --- a/src/domain_objects/moderated_report.rs +++ b/src/domain_objects/moderated_report.rs @@ -1,9 +1,9 @@ +use crate::config; use crate::domain_objects::{ModerationCategory, ReportRequest, ReportTarget}; use anyhow::Result; use nostr_sdk::prelude::*; use serde::{Deserialize, Serialize}; use serde_json; -use std::env; use std::fmt::{self, Display, Formatter}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -16,10 +16,7 @@ impl ModeratedReport { reported_request: &ReportRequest, category: &ModerationCategory, ) -> Result { - let Ok(reportinator_secret) = env::var("REPORTINATOR_SECRET") else { - return Err(anyhow::anyhow!("REPORTINATOR_SECRET env variable not set")); - }; - let reportinator_keys = Keys::parse(reportinator_secret)?; + let reportinator_keys = &config::reportinator::config().keys; let (reported_pubkey, reported_event_id) = match reported_request.target() { ReportTarget::Event(event) => (event.pubkey, Some(event.id)), ReportTarget::Pubkey(pubkey) => (*pubkey, None), diff --git a/src/lib.rs b/src/lib.rs index f43fd6a..e4028d9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +pub mod config; mod domain_objects; pub use crate::domain_objects::as_gift_wrap::AsGiftWrap; pub use crate::domain_objects::report_request::{ReportRequest, ReportTarget}; diff --git a/src/main.rs b/src/main.rs index d60948a..dccff24 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,30 +3,34 @@ mod adapters; mod domain_objects; mod service_manager; -use crate::actors::Supervisor; -use crate::adapters::{GooglePublisher, HttpServer, NostrService, SlackClientAdapterBuilder}; -use crate::service_manager::ServiceManager; +use crate::{ + actors::Supervisor, + adapters::{GooglePublisher, HttpServer, NostrService, SlackClientAdapterBuilder}, + service_manager::ServiceManager, +}; use actors::{NostrPort, PubsubPort, SlackClientPortBuilder}; use anyhow::{Context, Result}; use nostr_sdk::prelude::*; -use std::env; +use reportinator_server::config::ReportinatorConfig; +use reportinator_server::config::{self, Config}; use tracing::info; use tracing_subscriber::{fmt, prelude::*, EnvFilter}; #[tokio::main] async fn main() -> Result<()> { + let config = Config::new("config")?; + tracing_subscriber::registry() .with(fmt::layer()) .with(EnvFilter::from_default_env()) .init(); - let Ok(reportinator_secret) = env::var("REPORTINATOR_SECRET") else { - return Err(anyhow::anyhow!("REPORTINATOR_SECRET env variable not set")); - }; + let app_config = config.get::()?; + // There are places that are non-trivial to pass app_config to, + // so we will set a global here for the interim. + config::reportinator::set_config(app_config.clone()); - let reportinator_keys = - Keys::parse(reportinator_secret).context("Error creating keys from secret")?; - let reportinator_public_key = reportinator_keys.public_key(); + let reportinator_public_key = app_config.keys.public_key(); info!( "Reportinator public key: {}", reportinator_public_key.to_string() @@ -38,17 +42,18 @@ async fn main() -> Result<()> { .limit(0) .kind(Kind::GiftWrap)]; - let relays = get_relays()?; + info!("Using relays: {:?}", app_config.relays); - let nostr_subscriber = NostrService::create(relays, gift_wrap_filter).await?; + let nostr_subscriber = NostrService::create(app_config.relays, gift_wrap_filter).await?; let google_publisher = GooglePublisher::create().await?; let slack_writer_builder = SlackClientAdapterBuilder::default(); start_server( + config, nostr_subscriber, google_publisher, slack_writer_builder, - reportinator_keys, + app_config.keys, ) .await } @@ -91,6 +96,7 @@ async fn main() -> Result<()> { /// │ Reportinator Server │ /// └───────────────────────────────────────────────────────────────────────┘ async fn start_server( + config: Config, nostr_subscriber: impl NostrPort, google_publisher: impl PubsubPort, slack_writer_builder: impl SlackClientPortBuilder, @@ -101,7 +107,7 @@ async fn start_server( // Spawn actors and wire them together let supervisor = manager .spawn_actor( - Supervisor::default(), + Supervisor::new(config.clone()), ( nostr_subscriber, google_publisher, @@ -111,29 +117,12 @@ async fn start_server( ) .await?; - manager.spawn_service(|cancellation_token| HttpServer::run(cancellation_token, supervisor)); + manager.spawn_service(|cancellation_token| { + HttpServer::run(config, supervisor, cancellation_token) + }); manager .listen_stop_signals() .await .context("Failed to spawn actors") } - -fn get_relays() -> Result> { - let Ok(value) = env::var("RELAY_ADDRESSES_CSV") else { - return Err(anyhow::anyhow!("RELAY_ADDRESSES_CSV env variable not set")); - }; - - if value.trim().is_empty() { - return Err(anyhow::anyhow!("RELAY_ADDRESSES_CSV env variable is empty")); - } - - let relays = value - .trim() - .split(',') - .map(|s| s.trim().to_string()) - .collect(); - - info!("Using relays: {:?}", relays); - Ok(relays) -}