use crate::services::user_service::UserService;
#[cfg(any(test, feature = "testing"))]
use crate::MockDataDir;
use crate::{
client_server::auth::RevocationListener,
observability::{Metrics, MetricsInitError},
persistence::{
files::{events::EventsService, FileIoError, FileService},
sql::{Migrator, PgEventListener, SqlDb},
},
ConfigToml, DataDir,
};
use pubky_common::crypto::Keypair;
use std::sync::Arc;
use std::time::Duration;
#[derive(Debug, thiserror::Error)]
pub enum AppContextConversionError {
#[error("Failed to ensure data directory exists and is writable: {0}")]
DataDir(anyhow::Error),
#[error("Failed to read or create config file: {0}")]
Config(anyhow::Error),
#[error("Failed to read or create keypair: {0}")]
Keypair(anyhow::Error),
#[error("Failed to open SQL DB: {0}")]
SqlDb(sqlx::Error),
#[error("Failed to resolve database mode: {0}")]
DatabaseResolution(anyhow::Error),
#[error("Failed to run migrations: {0}")]
Migrations(anyhow::Error),
#[error("Failed to build storage operator: {0}")]
Storage(FileIoError),
#[error("Failed to build pkarr client: {0}")]
Pkarr(pkarr::errors::BuildError),
#[error("Failed to start Postgres event listener: {0}")]
PgEventListener(sqlx::Error),
#[error("Failed to start the auth revocation listener: {0}")]
RevocationListener(sqlx::Error),
#[error("Failed to initialize metrics: {0}")]
Metrics(MetricsInitError),
}
#[derive(Clone)]
pub struct AppContext {
pub(crate) sql_db: SqlDb,
pub(crate) file_service: FileService,
pub(crate) config_toml: ConfigToml,
pub(crate) data_dir: Arc<dyn DataDir>,
pub(crate) keypair: Keypair,
pub(crate) pkarr_client: pkarr::Client,
pub(crate) pkarr_builder: pkarr::ClientBuilder,
pub(crate) events_service: EventsService,
pub(crate) metrics: Metrics,
_pg_event_listener: Arc<PgEventListener>,
pub(crate) revocation_listener: RevocationListener,
pub(crate) user_service: UserService,
}
impl AppContext {
#[cfg(test)]
pub(crate) fn with_pkarr(
mut self,
client: pkarr::Client,
builder: pkarr::ClientBuilder,
) -> Self {
self.pkarr_client = client;
self.pkarr_builder = builder;
self
}
#[cfg(test)]
pub(crate) fn with_keypair(mut self, keypair: Keypair) -> Self {
self.keypair = keypair;
self
}
#[cfg(any(test, feature = "testing"))]
pub async fn test() -> Arc<Self> {
let data_dir = MockDataDir::test();
Arc::new(
Self::read_from(data_dir)
.await
.expect("failed to build AppContext from DataDirMock"),
)
}
#[cfg(any(test, feature = "testing"))]
pub async fn test_with_config(f: impl FnOnce(&mut ConfigToml)) -> Arc<Self> {
let mut config = ConfigToml::default_test_config();
f(&mut config);
let data_dir = MockDataDir::new(config, None).expect("failed to create MockDataDir");
Arc::new(
Self::read_from(data_dir)
.await
.expect("failed to build AppContext from DataDirMock"),
)
}
pub async fn read_from<D: DataDir + 'static>(
dir: D,
) -> Result<Self, AppContextConversionError> {
dir.ensure_data_dir_exists_and_is_writable()
.map_err(AppContextConversionError::DataDir)?;
let conf = dir
.read_or_create_config_file()
.map_err(AppContextConversionError::Config)?;
let keypair = dir
.read_or_create_keypair()
.map_err(AppContextConversionError::Keypair)?;
let db_mode = dir
.resolve_database_mode(&conf)
.map_err(AppContextConversionError::DatabaseResolution)?;
let sql_db = SqlDb::connect(db_mode)
.await
.map_err(AppContextConversionError::SqlDb)?;
Migrator::new(&sql_db)
.run()
.await
.map_err(AppContextConversionError::Migrations)?;
let events_service = EventsService::new(sql_db.clone(), 1000);
let pg_event_listener = PgEventListener::start(sql_db.pool(), events_service.clone())
.await
.map_err(AppContextConversionError::PgEventListener)?;
let revocation_listener = RevocationListener::start(sql_db.pool())
.await
.map_err(AppContextConversionError::RevocationListener)?;
let user_service = UserService::new(sql_db.clone());
let file_service = FileService::new_from_config(
&conf,
dir.path(),
sql_db.clone(),
events_service.clone(),
user_service.clone(),
)
.map_err(AppContextConversionError::Storage)?;
let pkarr_builder = Self::build_pkarr_builder_from_config(&conf);
Ok(Self {
sql_db,
pkarr_client: pkarr_builder
.clone()
.build()
.map_err(AppContextConversionError::Pkarr)?,
file_service,
pkarr_builder,
config_toml: conf,
keypair,
data_dir: Arc::new(dir),
events_service,
metrics: Metrics::new().map_err(AppContextConversionError::Metrics)?,
_pg_event_listener: Arc::new(pg_event_listener),
revocation_listener,
user_service,
})
}
}
impl AppContext {
fn build_pkarr_builder_from_config(config_toml: &ConfigToml) -> pkarr::ClientBuilder {
let mut builder = pkarr::ClientBuilder::default();
#[cfg(any(test, feature = "testing"))]
if config_toml.general.database_url.is_none() {
builder
.no_default_network()
.bootstrap(&["127.0.0.1:9"])
.dht_report_policy(pkarr::dht::ReportPolicy::testnet());
}
if let Some(bootstrap_nodes) = &config_toml.pkdns.dht_bootstrap_nodes {
let nodes = bootstrap_nodes
.iter()
.map(|node| node.to_string())
.collect::<Vec<String>>();
builder.bootstrap(&nodes);
builder.no_relays();
}
if let Some(relays) = &config_toml.pkdns.dht_relay_nodes {
builder
.relays(relays)
.expect("parameters are already urls and therefore valid.");
}
if let Some(request_timeout) = &config_toml.pkdns.dht_request_timeout_ms {
let duration = Duration::from_millis(request_timeout.get());
builder.request_timeout(duration);
}
builder
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pkarr_builder_does_not_use_default_network() {
let config = ConfigToml::default_test_config();
assert!(
config.general.database_url.is_none(),
"default_test_config should have database_url = None"
);
let builder = AppContext::build_pkarr_builder_from_config(&config);
let builder_debug = format!("{builder:?}");
assert!(
builder_debug.contains("127.0.0.1:9"),
"expected sentinel bootstrap node in builder: {builder_debug}"
);
for relay in pkarr::DEFAULT_RELAYS {
assert!(
!builder_debug.contains(relay),
"default relay {relay} should not appear in test builder: {builder_debug}"
);
}
builder.build().expect("isolated pkarr client should build");
}
}