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
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:
@@ -2,6 +2,27 @@ use crate::{errors::ApiError, state::AppState};
|
||||
use axum::{extract::FromRequestParts, http::request::Parts};
|
||||
use domain::value_objects::UserId;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Deps<S> extractor — narrows AppState to a handler-specific deps struct
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub struct Deps<S>(pub S);
|
||||
|
||||
pub trait FromAppState: Sized {
|
||||
fn from_state(s: &AppState) -> Self;
|
||||
}
|
||||
|
||||
impl<S: FromAppState + Send + 'static> FromRequestParts<AppState> for Deps<S> {
|
||||
type Rejection = std::convert::Infallible;
|
||||
|
||||
async fn from_request_parts(
|
||||
_parts: &mut Parts,
|
||||
state: &AppState,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
Ok(Deps(S::from_state(state)))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AuthUser(pub UserId);
|
||||
pub struct OptionalAuthUser(pub Option<UserId>);
|
||||
|
||||
|
||||
@@ -1,23 +1,40 @@
|
||||
use crate::{errors::ApiError, extractors::AuthUser, state::AppState};
|
||||
use crate::{
|
||||
errors::ApiError,
|
||||
extractors::{AuthUser, Deps, FromAppState},
|
||||
state::AppState,
|
||||
};
|
||||
use api_types::{
|
||||
requests::CreateApiKeyRequest,
|
||||
responses::{ApiKeyResponse, CreatedApiKeyResponse},
|
||||
};
|
||||
use application::use_cases::api_keys::{create_api_key, delete_api_key, list_api_keys};
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
extract::Path,
|
||||
http::StatusCode,
|
||||
Json,
|
||||
};
|
||||
use domain::value_objects::ApiKeyId;
|
||||
use domain::{ports::ApiKeyRepository, value_objects::ApiKeyId};
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct ApiKeysDeps {
|
||||
pub api_keys: Arc<dyn ApiKeyRepository>,
|
||||
}
|
||||
|
||||
impl FromAppState for ApiKeysDeps {
|
||||
fn from_state(s: &AppState) -> Self {
|
||||
Self {
|
||||
api_keys: s.api_keys.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(get, path = "/api-keys", responses((status = 200, description = "API keys", body = Vec<ApiKeyResponse>)), security(("bearer_auth" = [])))]
|
||||
pub async fn get_api_keys(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<ApiKeysDeps>,
|
||||
AuthUser(uid): AuthUser,
|
||||
) -> Result<Json<Vec<ApiKeyResponse>>, ApiError> {
|
||||
let keys = list_api_keys(&*s.api_keys, &uid).await?;
|
||||
let keys = list_api_keys(&*d.api_keys, &uid).await?;
|
||||
Ok(Json(
|
||||
keys.into_iter()
|
||||
.map(|k| ApiKeyResponse {
|
||||
@@ -30,21 +47,21 @@ pub async fn get_api_keys(
|
||||
}
|
||||
#[utoipa::path(post, path = "/api-keys", request_body = CreateApiKeyRequest, responses((status = 200, description = "Created — raw key shown once", body = CreatedApiKeyResponse)), security(("bearer_auth" = [])))]
|
||||
pub async fn post_api_key(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<ApiKeysDeps>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Json(body): Json<CreateApiKeyRequest>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let (key, raw) = create_api_key(&*s.api_keys, &uid, body.name).await?;
|
||||
let (key, raw) = create_api_key(&*d.api_keys, &uid, body.name).await?;
|
||||
Ok(Json(
|
||||
serde_json::json!({ "id": key.id.as_uuid(), "name": key.name, "key": raw }),
|
||||
))
|
||||
}
|
||||
#[utoipa::path(delete, path = "/api-keys/{id}", params(("id" = uuid::Uuid, Path, description = "Key ID")), responses((status = 204, description = "Deleted")), security(("bearer_auth" = [])))]
|
||||
pub async fn delete_api_key_handler(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<ApiKeysDeps>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
delete_api_key(&*s.api_keys, &uid, &ApiKeyId::from_uuid(id)).await?;
|
||||
delete_api_key(&*d.api_keys, &uid, &ApiKeyId::from_uuid(id)).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,34 @@
|
||||
use crate::{errors::ApiError, state::AppState};
|
||||
use crate::{
|
||||
errors::ApiError,
|
||||
extractors::{Deps, FromAppState},
|
||||
state::AppState,
|
||||
};
|
||||
use api_types::{
|
||||
requests::{LoginRequest, RegisterRequest},
|
||||
responses::{AuthResponse, ErrorResponse, UserResponse},
|
||||
};
|
||||
use application::use_cases::auth::{login, register, LoginInput, RegisterInput};
|
||||
use axum::{extract::State, http::StatusCode, response::IntoResponse, Json};
|
||||
use axum::{http::StatusCode, response::IntoResponse, Json};
|
||||
use domain::ports::{AuthService, EventPublisher, PasswordHasher, UserRepository};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct AuthDeps {
|
||||
pub users: Arc<dyn UserRepository>,
|
||||
pub hasher: Arc<dyn PasswordHasher>,
|
||||
pub auth: Arc<dyn AuthService>,
|
||||
pub events: Arc<dyn EventPublisher>,
|
||||
}
|
||||
|
||||
impl FromAppState for AuthDeps {
|
||||
fn from_state(s: &AppState) -> Self {
|
||||
Self {
|
||||
users: s.users.clone(),
|
||||
hasher: s.hasher.clone(),
|
||||
auth: s.auth.clone(),
|
||||
events: s.events.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_user_response(u: &domain::models::user::User) -> UserResponse {
|
||||
UserResponse {
|
||||
@@ -31,14 +55,14 @@ pub fn to_user_response(u: &domain::models::user::User) -> UserResponse {
|
||||
)
|
||||
)]
|
||||
pub async fn post_register(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<AuthDeps>,
|
||||
Json(body): Json<RegisterRequest>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let out = register(
|
||||
&*s.users,
|
||||
&*s.hasher,
|
||||
&*s.auth,
|
||||
&*s.events,
|
||||
&*d.users,
|
||||
&*d.hasher,
|
||||
&*d.auth,
|
||||
&*d.events,
|
||||
RegisterInput {
|
||||
username: body.username,
|
||||
email: body.email,
|
||||
@@ -62,13 +86,13 @@ pub async fn post_register(
|
||||
)
|
||||
)]
|
||||
pub async fn post_login(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<AuthDeps>,
|
||||
Json(body): Json<LoginRequest>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let out = login(
|
||||
&*s.users,
|
||||
&*s.hasher,
|
||||
&*s.auth,
|
||||
&*d.users,
|
||||
&*d.hasher,
|
||||
&*d.auth,
|
||||
LoginInput {
|
||||
email: body.email,
|
||||
password: body.password,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use crate::{
|
||||
errors::ApiError, extractors::OptionalAuthUser, handlers::feed::to_thought_response,
|
||||
errors::ApiError,
|
||||
extractors::{Deps, FromAppState, OptionalAuthUser},
|
||||
handlers::feed::to_thought_response,
|
||||
state::AppState,
|
||||
};
|
||||
use api_types::{
|
||||
@@ -10,13 +12,41 @@ use application::use_cases::federation_management::{
|
||||
get_actor_connections_page, get_remote_actor_posts,
|
||||
};
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
extract::{Path, Query},
|
||||
Json,
|
||||
};
|
||||
use domain::models::feed::PageParams;
|
||||
use activitypub_base::ActivityPubRepository;
|
||||
use domain::{
|
||||
models::feed::PageParams,
|
||||
ports::{
|
||||
FederationActionPort, FederationSchedulerPort, FeedRepository,
|
||||
RemoteActorConnectionRepository,
|
||||
},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct FederationActorsDeps {
|
||||
pub federation: Arc<dyn FederationActionPort>,
|
||||
pub ap_repo: Arc<dyn ActivityPubRepository>,
|
||||
pub feed: Arc<dyn FeedRepository>,
|
||||
pub federation_scheduler: Arc<dyn FederationSchedulerPort>,
|
||||
pub remote_actor_connections: Arc<dyn RemoteActorConnectionRepository>,
|
||||
}
|
||||
|
||||
impl FromAppState for FederationActorsDeps {
|
||||
fn from_state(s: &AppState) -> Self {
|
||||
Self {
|
||||
federation: s.federation.clone(),
|
||||
ap_repo: s.ap_repo.clone(),
|
||||
feed: s.feed.clone(),
|
||||
federation_scheduler: s.federation_scheduler.clone(),
|
||||
remote_actor_connections: s.remote_actor_connections.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn remote_actor_posts_handler(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<FederationActorsDeps>,
|
||||
Path(handle): Path<String>,
|
||||
Query(q): Query<PaginationQuery>,
|
||||
OptionalAuthUser(viewer): OptionalAuthUser,
|
||||
@@ -26,10 +56,10 @@ pub async fn remote_actor_posts_handler(
|
||||
per_page: q.per_page(),
|
||||
};
|
||||
let result = get_remote_actor_posts(
|
||||
&*s.federation,
|
||||
&*s.ap_repo,
|
||||
&*s.feed,
|
||||
&*s.federation_scheduler,
|
||||
&*d.federation,
|
||||
&*d.ap_repo,
|
||||
&*d.feed,
|
||||
&*d.federation_scheduler,
|
||||
&handle,
|
||||
page,
|
||||
viewer.as_ref(),
|
||||
@@ -44,31 +74,31 @@ pub async fn remote_actor_posts_handler(
|
||||
}
|
||||
|
||||
pub async fn actor_followers_handler(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<FederationActorsDeps>,
|
||||
Path(handle): Path<String>,
|
||||
Query(q): Query<PaginationQuery>,
|
||||
) -> Result<Json<ActorConnectionPageResponse>, ApiError> {
|
||||
actor_connections_handler(s, handle, "followers", q.page() as u32).await
|
||||
actor_connections_handler(d, handle, "followers", q.page() as u32).await
|
||||
}
|
||||
|
||||
pub async fn actor_following_handler(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<FederationActorsDeps>,
|
||||
Path(handle): Path<String>,
|
||||
Query(q): Query<PaginationQuery>,
|
||||
) -> Result<Json<ActorConnectionPageResponse>, ApiError> {
|
||||
actor_connections_handler(s, handle, "following", q.page() as u32).await
|
||||
actor_connections_handler(d, handle, "following", q.page() as u32).await
|
||||
}
|
||||
|
||||
async fn actor_connections_handler(
|
||||
s: AppState,
|
||||
d: FederationActorsDeps,
|
||||
handle: String,
|
||||
connection_type: &str,
|
||||
page: u32,
|
||||
) -> Result<Json<ActorConnectionPageResponse>, ApiError> {
|
||||
let (items, has_more) = get_actor_connections_page(
|
||||
&*s.federation,
|
||||
&*s.remote_actor_connections,
|
||||
&*s.federation_scheduler,
|
||||
&*d.federation,
|
||||
&*d.remote_actor_connections,
|
||||
&*d.federation_scheduler,
|
||||
&handle,
|
||||
connection_type,
|
||||
page,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
errors::ApiError,
|
||||
extractors::{AuthUser, OptionalAuthUser},
|
||||
extractors::{Deps, FromAppState, OptionalAuthUser, AuthUser},
|
||||
handlers::auth::to_user_response,
|
||||
state::AppState,
|
||||
};
|
||||
@@ -12,12 +12,38 @@ use application::use_cases::feed::{
|
||||
};
|
||||
use application::use_cases::profile::{get_user_by_id_or_username, get_user_by_username};
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
extract::{Path, Query},
|
||||
http::{header, HeaderMap},
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use domain::models::feed::PageParams;
|
||||
use domain::{
|
||||
models::feed::PageParams,
|
||||
ports::{FederationActionPort, FeedRepository, FollowRepository, SearchPort, TagRepository, UserRepository},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct FeedDeps {
|
||||
pub feed: Arc<dyn FeedRepository>,
|
||||
pub follows: Arc<dyn FollowRepository>,
|
||||
pub search: Arc<dyn SearchPort>,
|
||||
pub federation: Arc<dyn FederationActionPort>,
|
||||
pub users: Arc<dyn UserRepository>,
|
||||
pub tags: Arc<dyn TagRepository>,
|
||||
}
|
||||
|
||||
impl FromAppState for FeedDeps {
|
||||
fn from_state(s: &AppState) -> Self {
|
||||
Self {
|
||||
feed: s.feed.clone(),
|
||||
follows: s.follows.clone(),
|
||||
search: s.search.clone(),
|
||||
federation: s.federation.clone(),
|
||||
users: s.users.clone(),
|
||||
tags: s.tags.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_thought_response(e: &domain::models::feed::FeedEntry) -> ThoughtResponse {
|
||||
ThoughtResponse {
|
||||
@@ -46,7 +72,7 @@ pub fn to_thought_response(e: &domain::models::feed::FeedEntry) -> ThoughtRespon
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn home_feed(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<FeedDeps>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Query(q): Query<PaginationQuery>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
@@ -54,7 +80,7 @@ pub async fn home_feed(
|
||||
page: q.page(),
|
||||
per_page: q.per_page(),
|
||||
};
|
||||
let result = get_home_feed(&*s.feed, &*s.follows, &uid, page).await?;
|
||||
let result = get_home_feed(&*d.feed, &*d.follows, &uid, page).await?;
|
||||
Ok(Json(serde_json::json!({
|
||||
"items": result.items.iter().map(to_thought_response).collect::<Vec<_>>(),
|
||||
"total": result.total,
|
||||
@@ -69,7 +95,7 @@ pub async fn home_feed(
|
||||
responses((status = 200, description = "Public feed"))
|
||||
)]
|
||||
pub async fn public_feed(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<FeedDeps>,
|
||||
OptionalAuthUser(viewer): OptionalAuthUser,
|
||||
Query(q): Query<PaginationQuery>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
@@ -77,7 +103,7 @@ pub async fn public_feed(
|
||||
page: q.page(),
|
||||
per_page: q.per_page(),
|
||||
};
|
||||
let result = get_public_feed(&*s.feed, viewer.as_ref(), page).await?;
|
||||
let result = get_public_feed(&*d.feed, viewer.as_ref(), page).await?;
|
||||
Ok(Json(serde_json::json!({
|
||||
"items": result.items.iter().map(to_thought_response).collect::<Vec<_>>(),
|
||||
"total": result.total,
|
||||
@@ -92,7 +118,7 @@ pub async fn public_feed(
|
||||
responses((status = 200, description = "Search results: thoughts and users"))
|
||||
)]
|
||||
pub async fn search_handler(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<FeedDeps>,
|
||||
OptionalAuthUser(viewer): OptionalAuthUser,
|
||||
Query(q): Query<SearchQuery>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
@@ -103,8 +129,8 @@ pub async fn search_handler(
|
||||
let query = q.q.trim().to_string();
|
||||
|
||||
let (thoughts_result, users_result) = tokio::join!(
|
||||
s.search.search_thoughts(&query, &page, viewer.as_ref()),
|
||||
s.search.search_users(&query, &page),
|
||||
d.search.search_thoughts(&query, &page, viewer.as_ref()),
|
||||
d.search.search_users(&query, &page),
|
||||
);
|
||||
|
||||
let thoughts = thoughts_result?
|
||||
@@ -127,7 +153,7 @@ pub async fn search_handler(
|
||||
}
|
||||
|
||||
pub async fn get_following_handler(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<FeedDeps>,
|
||||
Path(param): Path<String>,
|
||||
Query(q): Query<PaginationQuery>,
|
||||
headers: HeaderMap,
|
||||
@@ -138,22 +164,22 @@ pub async fn get_following_handler(
|
||||
.unwrap_or("");
|
||||
|
||||
if accept.contains("application/activity+json") {
|
||||
let user = get_user_by_id_or_username(&*s.users, ¶m).await?;
|
||||
let user = get_user_by_id_or_username(&*d.users, ¶m).await?;
|
||||
let user_id = user.id;
|
||||
let page = q.page().try_into().ok();
|
||||
let json = s
|
||||
let json = d
|
||||
.federation
|
||||
.following_collection_json(&user_id, page)
|
||||
.await?;
|
||||
return Ok(([(header::CONTENT_TYPE, "application/activity+json")], json).into_response());
|
||||
}
|
||||
|
||||
let user = get_user_by_username(&*s.users, ¶m).await?;
|
||||
let user = get_user_by_username(&*d.users, ¶m).await?;
|
||||
let page = PageParams {
|
||||
page: q.page(),
|
||||
per_page: q.per_page(),
|
||||
};
|
||||
let result = get_following(&*s.follows, &user.id, page).await?;
|
||||
let result = get_following(&*d.follows, &user.id, page).await?;
|
||||
Ok(Json(serde_json::json!({
|
||||
"total": result.total,
|
||||
"items": result.items.iter().map(to_user_response).collect::<Vec<_>>()
|
||||
@@ -162,7 +188,7 @@ pub async fn get_following_handler(
|
||||
}
|
||||
|
||||
pub async fn get_followers_handler(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<FeedDeps>,
|
||||
Path(param): Path<String>,
|
||||
Query(q): Query<PaginationQuery>,
|
||||
headers: HeaderMap,
|
||||
@@ -173,22 +199,22 @@ pub async fn get_followers_handler(
|
||||
.unwrap_or("");
|
||||
|
||||
if accept.contains("application/activity+json") {
|
||||
let user = get_user_by_id_or_username(&*s.users, ¶m).await?;
|
||||
let user = get_user_by_id_or_username(&*d.users, ¶m).await?;
|
||||
let user_id = user.id;
|
||||
let page = q.page().try_into().ok();
|
||||
let json = s
|
||||
let json = d
|
||||
.federation
|
||||
.followers_collection_json(&user_id, page)
|
||||
.await?;
|
||||
return Ok(([(header::CONTENT_TYPE, "application/activity+json")], json).into_response());
|
||||
}
|
||||
|
||||
let user = get_user_by_username(&*s.users, ¶m).await?;
|
||||
let user = get_user_by_username(&*d.users, ¶m).await?;
|
||||
let page = PageParams {
|
||||
page: q.page(),
|
||||
per_page: q.per_page(),
|
||||
};
|
||||
let result = get_followers(&*s.follows, &user.id, page).await?;
|
||||
let result = get_followers(&*d.follows, &user.id, page).await?;
|
||||
Ok(Json(serde_json::json!({
|
||||
"total": result.total,
|
||||
"items": result.items.iter().map(to_user_response).collect::<Vec<_>>()
|
||||
@@ -205,17 +231,17 @@ pub async fn get_followers_handler(
|
||||
responses((status = 200, description = "User's public thoughts"))
|
||||
)]
|
||||
pub async fn user_thoughts_handler(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<FeedDeps>,
|
||||
Path(username): Path<String>,
|
||||
OptionalAuthUser(viewer): OptionalAuthUser,
|
||||
Query(q): Query<PaginationQuery>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let user = get_user_by_username(&*s.users, &username).await?;
|
||||
let user = get_user_by_username(&*d.users, &username).await?;
|
||||
let page = PageParams {
|
||||
page: q.page(),
|
||||
per_page: q.per_page(),
|
||||
};
|
||||
let result = get_user_feed(&*s.feed, &user.id, page, viewer.as_ref()).await?;
|
||||
let result = get_user_feed(&*d.feed, &user.id, page, viewer.as_ref()).await?;
|
||||
Ok(Json(serde_json::json!({
|
||||
"total": result.total,
|
||||
"page": result.page,
|
||||
@@ -225,7 +251,7 @@ pub async fn user_thoughts_handler(
|
||||
}
|
||||
|
||||
pub async fn get_popular_tags(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<FeedDeps>,
|
||||
Query(params): Query<std::collections::HashMap<String, String>>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let limit: usize = params
|
||||
@@ -233,7 +259,7 @@ pub async fn get_popular_tags(
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(api_types::requests::DEFAULT_PER_PAGE as usize);
|
||||
let tags = uc_get_popular_tags(
|
||||
&*s.tags,
|
||||
&*d.tags,
|
||||
limit.min(api_types::requests::MAX_PER_PAGE as usize),
|
||||
)
|
||||
.await?;
|
||||
@@ -254,7 +280,7 @@ pub async fn get_popular_tags(
|
||||
responses((status = 200, description = "Thoughts with this tag"))
|
||||
)]
|
||||
pub async fn tag_thoughts_handler(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<FeedDeps>,
|
||||
Path(tag_name): Path<String>,
|
||||
OptionalAuthUser(viewer): OptionalAuthUser,
|
||||
Query(q): Query<PaginationQuery>,
|
||||
@@ -263,7 +289,7 @@ pub async fn tag_thoughts_handler(
|
||||
page: q.page(),
|
||||
per_page: q.per_page(),
|
||||
};
|
||||
let result = get_by_tag(&*s.feed, &tag_name, page, viewer.as_ref()).await?;
|
||||
let result = get_by_tag(&*d.feed, &tag_name, page, viewer.as_ref()).await?;
|
||||
Ok(Json(serde_json::json!({
|
||||
"tag": tag_name,
|
||||
"total": result.total,
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
use crate::state::AppState;
|
||||
use axum::{extract::State, Json};
|
||||
use crate::{
|
||||
extractors::{Deps, FromAppState},
|
||||
state::AppState,
|
||||
};
|
||||
use axum::Json;
|
||||
use domain::ports::UserRepository;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct HealthDeps {
|
||||
pub users: Arc<dyn UserRepository>,
|
||||
}
|
||||
|
||||
impl FromAppState for HealthDeps {
|
||||
fn from_state(s: &AppState) -> Self {
|
||||
Self {
|
||||
users: s.users.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(get, path = "/health", responses((status = 200, description = "Service health status")))]
|
||||
pub async fn health_handler(State(s): State<AppState>) -> Json<serde_json::Value> {
|
||||
let db_ok = s.users.list_with_stats().await.is_ok();
|
||||
pub async fn health_handler(Deps(d): Deps<HealthDeps>) -> Json<serde_json::Value> {
|
||||
let db_ok = d.users.list_with_stats().await.is_ok();
|
||||
Json(serde_json::json!({
|
||||
"status": if db_ok { "ok" } else { "degraded" },
|
||||
"db": if db_ok { "connected" } else { "error" },
|
||||
|
||||
@@ -1,28 +1,47 @@
|
||||
use crate::{errors::ApiError, extractors::AuthUser, state::AppState};
|
||||
use crate::{
|
||||
errors::ApiError,
|
||||
extractors::{AuthUser, Deps, FromAppState},
|
||||
state::AppState,
|
||||
};
|
||||
use api_types::requests::NotificationUpdateRequest;
|
||||
use application::use_cases::notifications::{
|
||||
count_unread_notifications, list_notifications as uc_list_notifications,
|
||||
mark_all_notifications_read, mark_notification_read as uc_mark_notification_read,
|
||||
};
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
extract::Path,
|
||||
http::StatusCode,
|
||||
Json,
|
||||
};
|
||||
use domain::{models::feed::PageParams, value_objects::NotificationId};
|
||||
use domain::{
|
||||
models::feed::PageParams, ports::NotificationRepository, value_objects::NotificationId,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct NotificationsDeps {
|
||||
pub notifications: Arc<dyn NotificationRepository>,
|
||||
}
|
||||
|
||||
impl FromAppState for NotificationsDeps {
|
||||
fn from_state(s: &AppState) -> Self {
|
||||
Self {
|
||||
notifications: s.notifications.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(get, path = "/notifications", responses((status = 200, description = "Notification summary")), security(("bearer_auth" = [])))]
|
||||
pub async fn list_notifications(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<NotificationsDeps>,
|
||||
AuthUser(uid): AuthUser,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let page = PageParams {
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
};
|
||||
let result = uc_list_notifications(&*s.notifications, &uid, page).await?;
|
||||
let unread = count_unread_notifications(&*s.notifications, &uid).await?;
|
||||
let result = uc_list_notifications(&*d.notifications, &uid, page).await?;
|
||||
let unread = count_unread_notifications(&*d.notifications, &uid).await?;
|
||||
Ok(Json(serde_json::json!({
|
||||
"total": result.total,
|
||||
"unread": unread
|
||||
@@ -31,13 +50,13 @@ pub async fn list_notifications(
|
||||
|
||||
#[utoipa::path(patch, path = "/notifications/{id}", params(("id" = uuid::Uuid, Path, description = "Notification ID")), request_body = NotificationUpdateRequest, responses((status = 204, description = "Marked read")), security(("bearer_auth" = [])))]
|
||||
pub async fn mark_notification_read(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<NotificationsDeps>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(body): Json<NotificationUpdateRequest>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
uc_mark_notification_read(
|
||||
&*s.notifications,
|
||||
&*d.notifications,
|
||||
&NotificationId::from_uuid(id),
|
||||
&uid,
|
||||
body.read,
|
||||
@@ -48,11 +67,11 @@ pub async fn mark_notification_read(
|
||||
|
||||
#[utoipa::path(patch, path = "/notifications", request_body = NotificationUpdateRequest, responses((status = 204, description = "All marked read")), security(("bearer_auth" = [])))]
|
||||
pub async fn mark_all_read(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<NotificationsDeps>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Json(body): Json<NotificationUpdateRequest>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
mark_all_notifications_read(&*s.notifications, &uid, body.read).await?;
|
||||
mark_all_notifications_read(&*d.notifications, &uid, body.read).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,49 +1,86 @@
|
||||
use crate::{errors::ApiError, extractors::AuthUser, state::AppState};
|
||||
use crate::{
|
||||
errors::ApiError,
|
||||
extractors::{AuthUser, Deps, FromAppState},
|
||||
state::AppState,
|
||||
};
|
||||
use api_types::requests::SetTopFriendsRequest;
|
||||
use application::use_cases::profile::{get_top_friends, get_user_by_username, set_top_friends};
|
||||
use application::use_cases::social::*;
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
extract::Path,
|
||||
http::StatusCode,
|
||||
Json,
|
||||
};
|
||||
use domain::value_objects::{ThoughtId, UserId};
|
||||
use domain::{
|
||||
ports::{
|
||||
BlockRepository, BoostRepository, EventPublisher, FederationActionPort, FollowRepository,
|
||||
LikeRepository, TopFriendRepository, UserRepository,
|
||||
},
|
||||
value_objects::{ThoughtId, UserId},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct SocialDeps {
|
||||
pub likes: Arc<dyn LikeRepository>,
|
||||
pub boosts: Arc<dyn BoostRepository>,
|
||||
pub follows: Arc<dyn FollowRepository>,
|
||||
pub users: Arc<dyn UserRepository>,
|
||||
pub federation: Arc<dyn FederationActionPort>,
|
||||
pub events: Arc<dyn EventPublisher>,
|
||||
pub blocks: Arc<dyn BlockRepository>,
|
||||
pub top_friends: Arc<dyn TopFriendRepository>,
|
||||
}
|
||||
|
||||
impl FromAppState for SocialDeps {
|
||||
fn from_state(s: &AppState) -> Self {
|
||||
Self {
|
||||
likes: s.likes.clone(),
|
||||
boosts: s.boosts.clone(),
|
||||
follows: s.follows.clone(),
|
||||
users: s.users.clone(),
|
||||
federation: s.federation.clone(),
|
||||
events: s.events.clone(),
|
||||
blocks: s.blocks.clone(),
|
||||
top_friends: s.top_friends.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(post, path = "/thoughts/{id}/like", params(("id" = uuid::Uuid, Path, description = "Thought ID")), responses((status = 204, description = "Liked")), security(("bearer_auth" = [])))]
|
||||
pub async fn post_like(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<SocialDeps>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
like_thought(&*s.likes, &*s.events, &uid, &ThoughtId::from_uuid(id)).await?;
|
||||
like_thought(&*d.likes, &*d.events, &uid, &ThoughtId::from_uuid(id)).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
#[utoipa::path(delete, path = "/thoughts/{id}/like", params(("id" = uuid::Uuid, Path, description = "Thought ID")), responses((status = 204, description = "Unliked")), security(("bearer_auth" = [])))]
|
||||
pub async fn delete_like(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<SocialDeps>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
unlike_thought(&*s.likes, &*s.events, &uid, &ThoughtId::from_uuid(id)).await?;
|
||||
unlike_thought(&*d.likes, &*d.events, &uid, &ThoughtId::from_uuid(id)).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
#[utoipa::path(post, path = "/thoughts/{id}/boost", params(("id" = uuid::Uuid, Path, description = "Thought ID")), responses((status = 204, description = "Boosted")), security(("bearer_auth" = [])))]
|
||||
pub async fn post_boost(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<SocialDeps>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
boost_thought(&*s.boosts, &*s.events, &uid, &ThoughtId::from_uuid(id)).await?;
|
||||
boost_thought(&*d.boosts, &*d.events, &uid, &ThoughtId::from_uuid(id)).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
#[utoipa::path(delete, path = "/thoughts/{id}/boost", params(("id" = uuid::Uuid, Path, description = "Thought ID")), responses((status = 204, description = "Unboosted")), security(("bearer_auth" = [])))]
|
||||
pub async fn delete_boost(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<SocialDeps>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
unboost_thought(&*s.boosts, &*s.events, &uid, &ThoughtId::from_uuid(id)).await?;
|
||||
unboost_thought(&*d.boosts, &*d.events, &uid, &ThoughtId::from_uuid(id)).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
#[utoipa::path(
|
||||
@@ -53,15 +90,15 @@ pub async fn delete_boost(
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn post_follow(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<SocialDeps>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Path(username): Path<String>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
follow_actor(
|
||||
&*s.follows,
|
||||
&*s.users,
|
||||
&*s.federation,
|
||||
&*s.events,
|
||||
&*d.follows,
|
||||
&*d.users,
|
||||
&*d.federation,
|
||||
&*d.events,
|
||||
&uid,
|
||||
&username,
|
||||
)
|
||||
@@ -75,15 +112,15 @@ pub async fn post_follow(
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn delete_follow(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<SocialDeps>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Path(username): Path<String>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
unfollow_actor(
|
||||
&*s.follows,
|
||||
&*s.users,
|
||||
&*s.federation,
|
||||
&*s.events,
|
||||
&*d.follows,
|
||||
&*d.users,
|
||||
&*d.federation,
|
||||
&*d.events,
|
||||
&uid,
|
||||
&username,
|
||||
)
|
||||
@@ -92,39 +129,39 @@ pub async fn delete_follow(
|
||||
}
|
||||
#[utoipa::path(post, path = "/users/{username}/block", params(("username" = String, Path, description = "Username")), responses((status = 204, description = "Blocked")), security(("bearer_auth" = [])))]
|
||||
pub async fn post_block(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<SocialDeps>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Path(username): Path<String>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
block_by_username(&*s.blocks, &*s.users, &*s.events, &uid, &username).await?;
|
||||
block_by_username(&*d.blocks, &*d.users, &*d.events, &uid, &username).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
#[utoipa::path(delete, path = "/users/{username}/block", params(("username" = String, Path, description = "Username")), responses((status = 204, description = "Unblocked")), security(("bearer_auth" = [])))]
|
||||
pub async fn delete_block(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<SocialDeps>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Path(username): Path<String>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
unblock_by_username(&*s.blocks, &*s.users, &*s.events, &uid, &username).await?;
|
||||
unblock_by_username(&*d.blocks, &*d.users, &*d.events, &uid, &username).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
#[utoipa::path(put, path = "/users/me/top-friends", request_body = SetTopFriendsRequest, responses((status = 204, description = "Top friends updated")), security(("bearer_auth" = [])))]
|
||||
pub async fn put_top_friends(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<SocialDeps>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Json(body): Json<SetTopFriendsRequest>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
let ids: Vec<UserId> = body.friend_ids.into_iter().map(UserId::from_uuid).collect();
|
||||
set_top_friends(&*s.top_friends, &uid, ids).await?;
|
||||
set_top_friends(&*d.top_friends, &uid, ids).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
#[utoipa::path(get, path = "/users/{username}/top-friends", params(("username" = String, Path, description = "Username")), responses((status = 200, description = "Top friends list")))]
|
||||
pub async fn get_top_friends_handler(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<SocialDeps>,
|
||||
Path(username): Path<String>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let user = get_user_by_username(&*s.users, &username).await?;
|
||||
let friends = get_top_friends(&*s.top_friends, &user.id).await?;
|
||||
let user = get_user_by_username(&*d.users, &username).await?;
|
||||
let friends = get_top_friends(&*d.top_friends, &user.id).await?;
|
||||
let usernames: Vec<&str> = friends.iter().map(|(_, u)| u.username.as_str()).collect();
|
||||
Ok(Json(serde_json::json!({ "topFriends": usernames })))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
errors::ApiError,
|
||||
extractors::{AuthUser, OptionalAuthUser},
|
||||
extractors::{AuthUser, Deps, FromAppState, OptionalAuthUser},
|
||||
handlers::auth::to_user_response,
|
||||
state::AppState,
|
||||
};
|
||||
@@ -12,14 +12,38 @@ use application::use_cases::thoughts::{
|
||||
create_thought, delete_thought, edit_thought, get_thought, get_thread, CreateThoughtInput,
|
||||
};
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
extract::Path,
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
Json,
|
||||
};
|
||||
use domain::value_objects::ThoughtId;
|
||||
use domain::{
|
||||
ports::{EventPublisher, OutboxWriter, TagRepository, ThoughtRepository, UserRepository},
|
||||
value_objects::ThoughtId,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct ThoughtsDeps {
|
||||
pub thoughts: Arc<dyn ThoughtRepository>,
|
||||
pub users: Arc<dyn UserRepository>,
|
||||
pub tags: Arc<dyn TagRepository>,
|
||||
pub events: Arc<dyn EventPublisher>,
|
||||
pub outbox: Arc<dyn OutboxWriter>,
|
||||
}
|
||||
|
||||
impl FromAppState for ThoughtsDeps {
|
||||
fn from_state(s: &AppState) -> Self {
|
||||
Self {
|
||||
thoughts: s.thoughts.clone(),
|
||||
users: s.users.clone(),
|
||||
tags: s.tags.clone(),
|
||||
events: s.events.clone(),
|
||||
outbox: s.outbox.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn thought_to_json(
|
||||
t: &domain::models::thought::Thought,
|
||||
author: &domain::models::user::User,
|
||||
@@ -56,17 +80,17 @@ fn thought_to_json(
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn post_thought(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<ThoughtsDeps>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Json(body): Json<CreateThoughtRequest>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let in_reply_to = body.in_reply_to_id.map(ThoughtId::from_uuid);
|
||||
let out = create_thought(
|
||||
&*s.thoughts,
|
||||
&*s.users,
|
||||
&*s.tags,
|
||||
&*s.events,
|
||||
&*s.outbox,
|
||||
&*d.thoughts,
|
||||
&*d.users,
|
||||
&*d.tags,
|
||||
&*d.events,
|
||||
&*d.outbox,
|
||||
CreateThoughtInput {
|
||||
user_id: uid.clone(),
|
||||
content: body.content,
|
||||
@@ -77,7 +101,7 @@ pub async fn post_thought(
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let author = s
|
||||
let author = d
|
||||
.users
|
||||
.find_by_id(&uid)
|
||||
.await?
|
||||
@@ -97,12 +121,12 @@ pub async fn post_thought(
|
||||
)
|
||||
)]
|
||||
pub async fn get_thought_handler(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<ThoughtsDeps>,
|
||||
Path(id): Path<Uuid>,
|
||||
OptionalAuthUser(_viewer): OptionalAuthUser,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let thought = get_thought(&*s.thoughts, &ThoughtId::from_uuid(id)).await?;
|
||||
let author = s
|
||||
let thought = get_thought(&*d.thoughts, &ThoughtId::from_uuid(id)).await?;
|
||||
let author = d
|
||||
.users
|
||||
.find_by_id(&thought.user_id)
|
||||
.await?
|
||||
@@ -121,11 +145,11 @@ pub async fn get_thought_handler(
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn delete_thought_handler(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<ThoughtsDeps>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
delete_thought(&*s.thoughts, &*s.events, &*s.outbox, &ThoughtId::from_uuid(id), &uid).await?;
|
||||
delete_thought(&*d.thoughts, &*d.events, &*d.outbox, &ThoughtId::from_uuid(id), &uid).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
@@ -141,14 +165,14 @@ pub async fn delete_thought_handler(
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn patch_thought(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<ThoughtsDeps>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(body): Json<EditThoughtRequest>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
edit_thought(
|
||||
&*s.thoughts,
|
||||
&*s.events,
|
||||
&*d.thoughts,
|
||||
&*d.events,
|
||||
&ThoughtId::from_uuid(id),
|
||||
&uid,
|
||||
body.content,
|
||||
@@ -165,13 +189,13 @@ pub async fn patch_thought(
|
||||
)
|
||||
)]
|
||||
pub async fn get_thread_handler(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<ThoughtsDeps>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<Vec<serde_json::Value>>, ApiError> {
|
||||
let thoughts = get_thread(&*s.thoughts, &ThoughtId::from_uuid(id)).await?;
|
||||
let thoughts = get_thread(&*d.thoughts, &ThoughtId::from_uuid(id)).await?;
|
||||
let mut items = Vec::new();
|
||||
for t in &thoughts {
|
||||
if let Ok(Some(author)) = s.users.find_by_id(&t.user_id).await {
|
||||
if let Ok(Some(author)) = d.users.find_by_id(&t.user_id).await {
|
||||
items.push(thought_to_json(t, &author, 0, 0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
errors::ApiError,
|
||||
extractors::{AuthUser, OptionalAuthUser},
|
||||
extractors::{AuthUser, Deps, FromAppState, OptionalAuthUser},
|
||||
handlers::auth::to_user_response,
|
||||
state::AppState,
|
||||
};
|
||||
@@ -13,11 +13,35 @@ use application::use_cases::profile::{
|
||||
get_user as fetch_user, get_user_by_id_or_username, update_profile,
|
||||
};
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
extract::{Path, Query},
|
||||
http::{header, HeaderMap},
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use domain::ports::{
|
||||
EventPublisher, FederationActionPort, FollowRepository, SearchPort, UserRepository,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct UsersDeps {
|
||||
pub users: Arc<dyn UserRepository>,
|
||||
pub events: Arc<dyn EventPublisher>,
|
||||
pub follows: Arc<dyn FollowRepository>,
|
||||
pub federation: Arc<dyn FederationActionPort>,
|
||||
pub search: Arc<dyn SearchPort>,
|
||||
}
|
||||
|
||||
impl FromAppState for UsersDeps {
|
||||
fn from_state(s: &AppState) -> Self {
|
||||
Self {
|
||||
users: s.users.clone(),
|
||||
events: s.events.clone(),
|
||||
follows: s.follows.clone(),
|
||||
federation: s.federation.clone(),
|
||||
search: s.search.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get, path = "/users/{username}",
|
||||
@@ -28,12 +52,12 @@ use axum::{
|
||||
)
|
||||
)]
|
||||
pub async fn get_user(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<UsersDeps>,
|
||||
Path(username): Path<String>,
|
||||
OptionalAuthUser(viewer): OptionalAuthUser,
|
||||
headers: HeaderMap,
|
||||
) -> Result<Response, ApiError> {
|
||||
let user = get_user_by_id_or_username(&*s.users, &username).await?;
|
||||
let user = get_user_by_id_or_username(&*d.users, &username).await?;
|
||||
|
||||
let accept = headers
|
||||
.get(header::ACCEPT)
|
||||
@@ -41,11 +65,11 @@ pub async fn get_user(
|
||||
.unwrap_or("");
|
||||
|
||||
if accept.contains("application/activity+json") {
|
||||
let json = s.federation.actor_json(&user.id).await?;
|
||||
let json = d.federation.actor_json(&user.id).await?;
|
||||
Ok(([(header::CONTENT_TYPE, "application/activity+json")], json).into_response())
|
||||
} else {
|
||||
let is_followed = if let Some(viewer_id) = viewer {
|
||||
s.follows.find(&viewer_id, &user.id).await?.is_some()
|
||||
d.follows.find(&viewer_id, &user.id).await?.is_some()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
@@ -65,13 +89,13 @@ pub async fn get_user(
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn patch_profile(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<UsersDeps>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Json(body): Json<UpdateProfileRequest>,
|
||||
) -> Result<Json<UserResponse>, ApiError> {
|
||||
update_profile(
|
||||
&*s.users,
|
||||
&*s.events,
|
||||
&*d.users,
|
||||
&*d.events,
|
||||
&uid,
|
||||
body.display_name,
|
||||
body.bio,
|
||||
@@ -80,7 +104,7 @@ pub async fn patch_profile(
|
||||
body.custom_css,
|
||||
)
|
||||
.await?;
|
||||
let user = fetch_user(&*s.users, &uid).await?;
|
||||
let user = fetch_user(&*d.users, &uid).await?;
|
||||
Ok(Json(to_user_response(&user)))
|
||||
}
|
||||
|
||||
@@ -93,15 +117,15 @@ pub async fn patch_profile(
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn get_me(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<UsersDeps>,
|
||||
AuthUser(uid): AuthUser,
|
||||
) -> Result<Json<UserResponse>, ApiError> {
|
||||
let user = fetch_user(&*s.users, &uid).await?;
|
||||
let user = fetch_user(&*d.users, &uid).await?;
|
||||
Ok(Json(to_user_response(&user)))
|
||||
}
|
||||
|
||||
pub async fn get_me_following(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<UsersDeps>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Query(q): Query<PaginationQuery>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
@@ -111,7 +135,7 @@ pub async fn get_me_following(
|
||||
page: q.page(),
|
||||
per_page: q.per_page(),
|
||||
};
|
||||
let result = get_following(&*s.follows, &uid, page).await?;
|
||||
let result = get_following(&*d.follows, &uid, page).await?;
|
||||
Ok(Json(serde_json::json!({
|
||||
"total": result.total,
|
||||
"items": result.items.iter().map(to_user_response).collect::<Vec<_>>(),
|
||||
@@ -119,7 +143,7 @@ pub async fn get_me_following(
|
||||
}
|
||||
|
||||
pub async fn get_users(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<UsersDeps>,
|
||||
Query(params): Query<std::collections::HashMap<String, String>>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
use domain::models::feed::PageParams;
|
||||
@@ -134,7 +158,7 @@ pub async fn get_users(
|
||||
let page_params = PageParams { page, per_page };
|
||||
|
||||
if let Some(q) = params.get("q").filter(|q| !q.trim().is_empty()) {
|
||||
let result = s.search.search_users(q, &page_params).await?;
|
||||
let result = d.search.search_users(q, &page_params).await?;
|
||||
let users: Vec<_> = result
|
||||
.items
|
||||
.iter()
|
||||
@@ -145,7 +169,7 @@ pub async fn get_users(
|
||||
})));
|
||||
}
|
||||
|
||||
let result = list_users_paginated(&*s.users, page_params).await?;
|
||||
let result = list_users_paginated(&*d.users, page_params).await?;
|
||||
let items: Vec<_> = result
|
||||
.items
|
||||
.iter()
|
||||
@@ -170,9 +194,9 @@ pub async fn get_users(
|
||||
}
|
||||
|
||||
pub async fn get_user_count(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<UsersDeps>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let count = s.users.count().await?;
|
||||
let count = d.users.count().await?;
|
||||
Ok(Json(serde_json::json!({ "count": count })))
|
||||
}
|
||||
|
||||
@@ -182,10 +206,10 @@ pub struct LookupQuery {
|
||||
}
|
||||
|
||||
pub async fn lookup_handler(
|
||||
State(s): State<AppState>,
|
||||
Deps(d): Deps<UsersDeps>,
|
||||
Query(q): Query<LookupQuery>,
|
||||
) -> Result<Json<RemoteActorResponse>, ApiError> {
|
||||
let actor = s.federation.lookup_actor(&q.handle).await?;
|
||||
let actor = d.federation.lookup_actor(&q.handle).await?;
|
||||
Ok(Json(RemoteActorResponse {
|
||||
handle: actor.handle,
|
||||
display_name: actor.display_name,
|
||||
|
||||
@@ -6,3 +6,5 @@ pub mod routes;
|
||||
pub mod state;
|
||||
#[cfg(test)]
|
||||
pub mod testing;
|
||||
|
||||
pub use extractors::{Deps, FromAppState};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use activitypub_base::ActivityPubRepository;
|
||||
use domain::ports::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use crate::state::AppState;
|
||||
use activitypub_base::{ActivityPubRepository, ActorApUrls, OutboxEntry};
|
||||
use async_trait::async_trait;
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
ports::{AuthService, GeneratedToken, PasswordHasher},
|
||||
testing::{NoOpOutboxWriter, TestStore},
|
||||
value_objects::{PasswordHash, UserId},
|
||||
value_objects::{PasswordHash, ThoughtId, UserId},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -29,6 +30,85 @@ impl PasswordHasher for NoOpHasher {
|
||||
}
|
||||
}
|
||||
|
||||
/// No-op ActivityPubRepository for presentation layer tests.
|
||||
pub struct NoOpApRepo;
|
||||
|
||||
#[async_trait]
|
||||
impl ActivityPubRepository for NoOpApRepo {
|
||||
async fn outbox_entries_for_actor(
|
||||
&self,
|
||||
_uid: &UserId,
|
||||
) -> Result<Vec<OutboxEntry>, DomainError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
async fn outbox_page_for_actor(
|
||||
&self,
|
||||
_uid: &UserId,
|
||||
_before: Option<chrono::DateTime<chrono::Utc>>,
|
||||
_limit: usize,
|
||||
) -> Result<Vec<OutboxEntry>, DomainError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
async fn find_remote_actor_id(
|
||||
&self,
|
||||
_actor_ap_url: &str,
|
||||
) -> Result<Option<UserId>, DomainError> {
|
||||
Ok(None)
|
||||
}
|
||||
async fn intern_remote_actor(&self, _actor_ap_url: &str) -> Result<UserId, DomainError> {
|
||||
Err(DomainError::NotFound)
|
||||
}
|
||||
async fn update_remote_actor_display(
|
||||
&self,
|
||||
_user_id: &UserId,
|
||||
_display_name: Option<&str>,
|
||||
_avatar_url: Option<&str>,
|
||||
) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn accept_note(
|
||||
&self,
|
||||
_ap_id: &str,
|
||||
_author_id: &UserId,
|
||||
_content: &str,
|
||||
_published: chrono::DateTime<chrono::Utc>,
|
||||
_sensitive: bool,
|
||||
_content_warning: Option<String>,
|
||||
_visibility: &str,
|
||||
_in_reply_to: Option<&str>,
|
||||
) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn apply_note_update(
|
||||
&self,
|
||||
_ap_id: &str,
|
||||
_new_content: &str,
|
||||
) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn retract_note(&self, _ap_id: &str) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn retract_actor_notes(&self, _actor_ap_url: &str) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn count_local_notes(&self) -> Result<u64, DomainError> {
|
||||
Ok(0)
|
||||
}
|
||||
async fn get_thought_ap_id(
|
||||
&self,
|
||||
_thought_id: &ThoughtId,
|
||||
) -> Result<Option<String>, DomainError> {
|
||||
Ok(None)
|
||||
}
|
||||
async fn get_actor_ap_urls(
|
||||
&self,
|
||||
_user_id: &UserId,
|
||||
) -> Result<Option<ActorApUrls>, DomainError> {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn make_state() -> AppState {
|
||||
let store = Arc::new(TestStore::default());
|
||||
AppState {
|
||||
@@ -50,7 +130,7 @@ pub fn make_state() -> AppState {
|
||||
events: store.clone(),
|
||||
outbox: Arc::new(NoOpOutboxWriter),
|
||||
federation: store.clone(),
|
||||
ap_repo: store.clone(),
|
||||
ap_repo: Arc::new(NoOpApRepo),
|
||||
remote_actor_connections: store.clone(),
|
||||
federation_scheduler: store.clone(),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user