resolver.rs
147 lignes · 5819 octets
//! `LocalUserResolver` trait + SeaORM-backed default implementation. //! //! Federation handlers depend on this trait rather than directly on SeaORM, //! which keeps unit tests simple (we inject a stub resolver) and lets future //! deployments swap the backing store without touching the handler code. use async_trait::async_trait; use sea_orm::DatabaseConnection; use uuid::Uuid; use rustwarden_core::error::AppError; /// Minimal projection of a local user usable to render WebFinger and /// Person JSON. Extended by [RFC 0005](../../rfcs/0005-actor-key-storage.md) /// (`actor-key-storage`) with the `ap_public_key_pem` field populated. #[derive(Debug, Clone)] pub struct UserSummary { /// Stable internal user identifier. pub id: Uuid, /// Local username (matches the `alice` part of `@alice@example.org`). pub username: String, /// Optional display name (Mastodon `name` field). pub display_name: Option<String>, /// Optional bio in HTML (Mastodon `summary` field). pub bio: Option<String>, /// PEM-encoded RSA public key. `None` until RFC 0005 lands. pub ap_public_key_pem: Option<String>, } /// Read-only projection of users used by federation handlers. /// /// Concrete implementations provide a thin facade over the underlying user /// store. The default is [`SeaOrmLocalUserResolver`]; tests can inject their /// own implementation. #[async_trait] pub trait LocalUserResolver: Send + Sync { /// Look up a local user by username. /// /// Returns `Ok(None)` when no such user exists or the user is /// soft-deleted. Returns `Err(AppError::Database(_))` on transport errors. async fn find_local_user(&self, username: &str) -> Result<Option<UserSummary>, AppError>; /// Total number of registered users (NodeInfo `usage.users.total`). async fn count_users_total(&self) -> Result<u64, AppError>; /// Number of users active within the given window. /// /// "Active" is defined as having `last_login_at` within the past /// `days` days. Used to populate `usage.users.active_month` (`days = 30`) /// and `usage.users.active_halfyear` (`days = 180`). async fn count_users_active_since(&self, days: u32) -> Result<u64, AppError>; /// Total number of locally-authored posts (NodeInfo `usage.localPosts`). /// Returns `0` until later RFCs introduce post entities. async fn count_local_posts(&self) -> Result<u64, AppError>; } /// Default [`LocalUserResolver`] implementation backed by a SeaORM /// [`DatabaseConnection`]. Cheap to clone (the connection is `Arc`-backed). #[derive(Debug, Clone)] pub struct SeaOrmLocalUserResolver { db: DatabaseConnection, } impl SeaOrmLocalUserResolver { /// Build a resolver around an existing SeaORM connection. pub fn new(db: DatabaseConnection) -> Self { Self { db } } } #[async_trait] impl LocalUserResolver for SeaOrmLocalUserResolver { async fn find_local_user(&self, username: &str) -> Result<Option<UserSummary>, AppError> { use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; let row = rustwarden_core::models::user::Entity::find() .filter(rustwarden_core::models::user::Column::Username.eq(username)) .one(&self.db) .await?; let Some(r) = row else { return Ok(None) }; // RFC 0005: lazy-generate the actor key the first time someone // resolves the user, but only if encryption is configured. We // never block the resolution on a generation failure: better to // serve a key-less actor than to 500 the WebFinger flow. let public_pem = match &r.ap_public_key_pem { Some(pem) => Some(pem.clone()), None => { if rustwarden_core::services::crypto_service::CryptoService::is_available() { match crate::services::actor_key_service::ActorKeyService::ensure_keys( &self.db, r.id, ) .await { Ok(material) => Some(material.public_pem), Err(err) => { tracing::warn!( user_id = %r.id, error = %err, "actor key generation failed, serving key-less actor", ); None } } } else { None } } }; Ok(Some(UserSummary { id: r.id, username: r.username, // `display_name` / `bio` columns are not in the schema yet — a // future RFC will add them. display_name: None, bio: None, ap_public_key_pem: public_pem, })) } async fn count_users_total(&self) -> Result<u64, AppError> { use sea_orm::{EntityTrait, PaginatorTrait}; Ok(rustwarden_core::models::user::Entity::find() .count(&self.db) .await?) } async fn count_users_active_since(&self, days: u32) -> Result<u64, AppError> { use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter}; let cutoff = chrono::Utc::now() - chrono::Duration::days(days as i64); Ok(rustwarden_core::models::user::Entity::find() .filter(rustwarden_core::models::user::Column::LastLoginAt.gte(cutoff)) .count(&self.db) .await?) } async fn count_local_posts(&self) -> Result<u64, AppError> { // RFC 0002 — no post entity yet, always zero. Future RFCs that // introduce locally-authored posts will override this method or // ship a dedicated resolver. Ok(0) } }
GitRust