refactor: 5 architectural improvements (Tasks 2-5 + Task 6 fix)
Some checks failed
lint / lint (push) Has been cancelled
test / unit (push) Has been cancelled
test / integration (push) Has been cancelled
lint / lint (pull_request) Failing after 5m2s
test / unit (pull_request) Successful in 16m19s
test / integration (pull_request) Failing after 17m15s

- feat(domain): Hashtag value object with canonical extract() — unifies two
  divergent private implementations; fields pre-compute raw/normalized/url_slug/ap_name

- feat(presentation): Deps<S: FromAppState> extractor — each handler now
  declares its exact dependency surface; AppState unchanged; handlers
  become unit-testable without mocking all 20 deps

- refactor(feed): replace 5 flat FeedRepository methods with FeedQuery/FeedScope
  — single query() method; SQL shared logic lives once; adding feed types
  no longer requires 5 edits

- refactor(activitypub): ActivityPubRepository + OutboundFederationPort moved
  out of domain::ports into activitypub-base::ap_ports — domain crate no
  longer knows about AP IDs, inboxes, or actor URLs

- fix(outbox): OutboxRelay now opens a per-row transaction so FOR UPDATE
  SKIP LOCKED actually holds the lock during publish + mark_delivered
This commit is contained in:
2026-05-15 18:54:20 +02:00
parent 6024a65060
commit 0592861edd
37 changed files with 1401 additions and 865 deletions

View File

@@ -1,11 +1,17 @@
use crate::{errors::ApiError, extractors::AuthUser, state::AppState};
use crate::{
errors::ApiError,
extractors::{AuthUser, Deps, FromAppState},
state::AppState,
};
use api_types::responses::{ProfileField, RemoteActorResponse};
use application::use_cases::federation_management::{
accept_follow_request, list_pending_requests, list_remote_followers, list_remote_following,
reject_follow_request, remove_remote_following,
};
use axum::{extract::State, http::StatusCode, Json};
use axum::{http::StatusCode, Json};
use domain::ports::{EventPublisher, FederationActionPort, FollowRepository, UserRepository};
use serde::Deserialize;
use std::sync::Arc;
#[derive(Deserialize)]
pub struct ActorUrlBody {
@@ -17,6 +23,24 @@ pub struct HandleBody {
pub handle: String,
}
pub struct FederationManagementDeps {
pub federation: Arc<dyn FederationActionPort>,
pub follows: Arc<dyn FollowRepository>,
pub users: Arc<dyn UserRepository>,
pub events: Arc<dyn EventPublisher>,
}
impl FromAppState for FederationManagementDeps {
fn from_state(s: &AppState) -> Self {
Self {
federation: s.federation.clone(),
follows: s.follows.clone(),
users: s.users.clone(),
events: s.events.clone(),
}
}
}
fn to_response(a: domain::models::remote_actor::RemoteActor) -> RemoteActorResponse {
RemoteActorResponse {
handle: a.handle,
@@ -38,57 +62,57 @@ fn to_response(a: domain::models::remote_actor::RemoteActor) -> RemoteActorRespo
}
pub async fn get_pending_requests(
State(s): State<AppState>,
Deps(d): Deps<FederationManagementDeps>,
AuthUser(uid): AuthUser,
) -> Result<Json<Vec<RemoteActorResponse>>, ApiError> {
let actors = list_pending_requests(&*s.federation, &uid).await?;
let actors = list_pending_requests(&*d.federation, &uid).await?;
Ok(Json(actors.into_iter().map(to_response).collect()))
}
pub async fn post_accept_request(
State(s): State<AppState>,
Deps(d): Deps<FederationManagementDeps>,
AuthUser(uid): AuthUser,
Json(body): Json<ActorUrlBody>,
) -> Result<StatusCode, ApiError> {
accept_follow_request(&*s.federation, &uid, &body.actor_url).await?;
accept_follow_request(&*d.federation, &uid, &body.actor_url).await?;
Ok(StatusCode::NO_CONTENT)
}
pub async fn delete_follower(
State(s): State<AppState>,
Deps(d): Deps<FederationManagementDeps>,
AuthUser(uid): AuthUser,
Json(body): Json<ActorUrlBody>,
) -> Result<StatusCode, ApiError> {
reject_follow_request(&*s.federation, &uid, &body.actor_url).await?;
reject_follow_request(&*d.federation, &uid, &body.actor_url).await?;
Ok(StatusCode::NO_CONTENT)
}
pub async fn get_remote_followers(
State(s): State<AppState>,
Deps(d): Deps<FederationManagementDeps>,
AuthUser(uid): AuthUser,
) -> Result<Json<Vec<RemoteActorResponse>>, ApiError> {
let actors = list_remote_followers(&*s.federation, &uid).await?;
let actors = list_remote_followers(&*d.federation, &uid).await?;
Ok(Json(actors.into_iter().map(to_response).collect()))
}
pub async fn get_remote_following(
State(s): State<AppState>,
Deps(d): Deps<FederationManagementDeps>,
AuthUser(uid): AuthUser,
) -> Result<Json<Vec<RemoteActorResponse>>, ApiError> {
let actors = list_remote_following(&*s.federation, &uid).await?;
let actors = list_remote_following(&*d.federation, &uid).await?;
Ok(Json(actors.into_iter().map(to_response).collect()))
}
pub async fn delete_following(
State(s): State<AppState>,
Deps(d): Deps<FederationManagementDeps>,
AuthUser(uid): AuthUser,
Json(body): Json<HandleBody>,
) -> Result<StatusCode, ApiError> {
remove_remote_following(
&*s.follows,
&*s.users,
&*s.federation,
&*s.events,
&*d.follows,
&*d.users,
&*d.federation,
&*d.events,
&uid,
&body.handle,
)