Skip to main content

pubky_homeserver/
app_context.rs

1//!
2//! The application context shared between all components.
3//! Think of it as a simple Dependency Injection container.
4//!
5//! Create with a `DataDir` instance: `AppContext::try_from(data_dir)`
6//!
7
8use crate::services::user_service::UserService;
9#[cfg(any(test, feature = "testing"))]
10use crate::MockDataDir;
11use crate::{
12    metrics_server::routes::metrics::{Metrics, MetricsInitError},
13    persistence::{
14        files::{events::EventsService, FileIoError, FileService},
15        sql::{Migrator, PgEventListener, SqlDb},
16    },
17    ConfigToml, DataDir,
18};
19use pubky_common::crypto::Keypair;
20use std::sync::Arc;
21use std::time::Duration;
22
23/// Errors that can occur when converting a `DataDir` to an `AppContext`.
24#[derive(Debug, thiserror::Error)]
25pub enum AppContextConversionError {
26    /// Failed to ensure data directory exists and is writable.
27    #[error("Failed to ensure data directory exists and is writable: {0}")]
28    DataDir(anyhow::Error),
29    /// Failed to read or create config file.
30    #[error("Failed to read or create config file: {0}")]
31    Config(anyhow::Error),
32    /// Failed to read or create keypair.
33    #[error("Failed to read or create keypair: {0}")]
34    Keypair(anyhow::Error),
35    /// Failed to open SQL DB.
36    #[error("Failed to open SQL DB: {0}")]
37    SqlDb(sqlx::Error),
38    /// Failed to run migrations.
39    #[error("Failed to run migrations: {0}")]
40    Migrations(anyhow::Error),
41    /// Failed to build storage operator.
42    #[error("Failed to build storage operator: {0}")]
43    Storage(FileIoError),
44    /// Failed to build pkarr client.
45    #[error("Failed to build pkarr client: {0}")]
46    Pkarr(pkarr::errors::BuildError),
47    /// Failed to start the Postgres event listener.
48    #[error("Failed to start Postgres event listener: {0}")]
49    PgEventListener(sqlx::Error),
50    /// Failed to initialize metrics.
51    #[error("Failed to initialize metrics: {0}")]
52    Metrics(MetricsInitError),
53}
54
55/// The application context shared between all components.
56/// Think of it as a simple Dependency Injection container.
57///
58/// Create with a `DataDir` instance: `AppContext::try_from(data_dir)`
59///
60#[derive(Clone)]
61pub struct AppContext {
62    /// The SQL database connection.
63    pub(crate) sql_db: SqlDb,
64    /// The storage operator to store files.
65    pub(crate) file_service: FileService,
66    pub(crate) config_toml: ConfigToml,
67    /// Keep data_dir alive. The mock dir will cleanup on drop.
68    pub(crate) data_dir: Arc<dyn DataDir>,
69    pub(crate) keypair: Keypair,
70    /// Main pkarr instance. This will automatically turn into a DHT server after 15 minutes after startup.
71    /// We need to keep this alive.
72    pub(crate) pkarr_client: pkarr::Client,
73    /// pkarr client builder in case we need to create a more instances.
74    /// Comes ready with the correct bootstrap nodes and relays.
75    pub(crate) pkarr_builder: pkarr::ClientBuilder,
76    /// Events service for managing event creation and broadcasting.
77    pub(crate) events_service: EventsService,
78    /// Metrics for all endpoints.
79    pub(crate) metrics: Metrics,
80    /// Background listener for Postgres event notifications.
81    /// Enables cross-instance event propagation for /events-stream's SSE functionality.
82    /// Kept alive for the background task, not for direct access.
83    _pg_event_listener: Arc<PgEventListener>,
84    /// User service for quota resolution and user creation with defaults.
85    pub(crate) user_service: UserService,
86}
87
88impl AppContext {
89    /// Create a new AppContext for testing.
90    #[cfg(any(test, feature = "testing"))]
91    pub async fn test() -> Self {
92        let data_dir = MockDataDir::test();
93        Self::read_from(data_dir)
94            .await
95            .expect("failed to build AppContext from DataDirMock")
96    }
97
98    /// Create a new AppContext from a data directory.
99    pub async fn read_from<D: DataDir + 'static>(
100        dir: D,
101    ) -> Result<Self, AppContextConversionError> {
102        dir.ensure_data_dir_exists_and_is_writable()
103            .map_err(AppContextConversionError::DataDir)?;
104        let conf = dir
105            .read_or_create_config_file()
106            .map_err(AppContextConversionError::Config)?;
107        let keypair = dir
108            .read_or_create_keypair()
109            .map_err(AppContextConversionError::Keypair)?;
110
111        let sql_db = Self::connect_to_sql_db(&conf).await?;
112        Migrator::new(&sql_db)
113            .run()
114            .await
115            .map_err(AppContextConversionError::Migrations)?;
116
117        let events_service = EventsService::new(1000);
118
119        let pg_event_listener = PgEventListener::start(sql_db.pool(), events_service.clone())
120            .await
121            .map_err(AppContextConversionError::PgEventListener)?;
122
123        let user_service = UserService::new(sql_db.clone());
124
125        let file_service = FileService::new_from_config(
126            &conf,
127            dir.path(),
128            sql_db.clone(),
129            events_service.clone(),
130            user_service.clone(),
131        )
132        .map_err(AppContextConversionError::Storage)?;
133        let pkarr_builder = Self::build_pkarr_builder_from_config(&conf);
134
135        Ok(Self {
136            sql_db,
137            pkarr_client: pkarr_builder
138                .clone()
139                .build()
140                .map_err(AppContextConversionError::Pkarr)?,
141            file_service,
142            pkarr_builder,
143            config_toml: conf,
144            keypair,
145            data_dir: Arc::new(dir),
146            events_service,
147            metrics: Metrics::new().map_err(AppContextConversionError::Metrics)?,
148            _pg_event_listener: Arc::new(pg_event_listener),
149            user_service,
150        })
151    }
152}
153
154impl AppContext {
155    /// Build the pkarr client builder based on the config.
156    fn build_pkarr_builder_from_config(config_toml: &ConfigToml) -> pkarr::ClientBuilder {
157        let mut builder = pkarr::ClientBuilder::default();
158        if let Some(bootstrap_nodes) = &config_toml.pkdns.dht_bootstrap_nodes {
159            let nodes = bootstrap_nodes
160                .iter()
161                .map(|node| node.to_string())
162                .collect::<Vec<String>>();
163            builder.bootstrap(&nodes);
164
165            // If we set custom bootstrap nodes, we don't want to use the default pkarr relay nodes.
166            // Otherwise, we could end up with a DHT with testnet boostrap nodes and mainnet relays
167            // which would give very weird results.
168            builder.no_relays();
169        }
170
171        if let Some(relays) = &config_toml.pkdns.dht_relay_nodes {
172            builder
173                .relays(relays)
174                .expect("parameters are already urls and therefore valid.");
175        }
176        if let Some(request_timeout) = &config_toml.pkdns.dht_request_timeout_ms {
177            let duration = Duration::from_millis(request_timeout.get());
178            builder.request_timeout(duration);
179        }
180        builder
181    }
182
183    /// Connect to the SQL database.
184    /// If we are in a test environment and it's a test db connection string,
185    /// we use an empheral test db.
186    /// Otherwise, we use the normal db connection.
187    async fn connect_to_sql_db(
188        config_toml: &ConfigToml,
189    ) -> Result<SqlDb, AppContextConversionError> {
190        #[cfg(any(test, feature = "testing"))]
191        {
192            // If we are in a test environment and it's a test db connection string,
193            // we use an empheral test db.
194            return if config_toml.general.database_url.is_test_db() {
195                Ok(SqlDb::test().await)
196            } else {
197                SqlDb::connect(&config_toml.general.database_url)
198                    .await
199                    .map_err(AppContextConversionError::SqlDb)
200            };
201        }
202
203        #[cfg(not(any(test, feature = "testing")))]
204        {
205            // If we are not in a test environment, we use the normal db connection.
206            return SqlDb::connect(&config_toml.general.database_url)
207                .await
208                .map_err(AppContextConversionError::SqlDb);
209        }
210    }
211}