use axum::{ Form, Json, extract::{Extension, Path, Query, State}, http::StatusCode, response::{IntoResponse, Redirect}, }; use uuid::Uuid; use crate::{ csrf::CsrfToken, errors::ApiError, extractors::{AdminApiUser, AuthenticatedUser, RequiredCookieUser}, forms::{ ActorUrlForm, BlockDomainForm, FollowForm, FollowerActionForm, RemoveDomainForm, UnfollowForm, }, render::render_page, state::AppState, }; use api_types::{ ActorListResponse, ActorUrlRequest, AddBlockedDomainRequest, BlockedActorResponse, BlockedDomainResponse, FollowRequest, RemoteActorDto, }; use application::social::deps::{SocialCommandDeps, SocialQueryDeps}; use domain::value_objects::{FollowTarget, SocialActor, SocialIdentity}; use template_askama::{ BlockedActorsTemplate, BlockedDomainsTemplate, FollowersTemplate, FollowingTemplate, RemoteActorData, }; use super::helpers::{build_page_context, encode_error}; impl From<&AppState> for SocialCommandDeps { fn from(state: &AppState) -> Self { Self { social_command: state.app_ctx.repos.social_command.clone(), social_query: state.app_ctx.repos.social_query_unified.clone(), event_publisher: state.app_ctx.services.event_publisher.clone(), } } } impl From<&AppState> for SocialQueryDeps { fn from(state: &AppState) -> Self { Self { social_query: state.app_ctx.repos.social_query_unified.clone(), } } } fn ap_to_domain(e: anyhow::Error) -> domain::errors::DomainError { tracing::error!("ActivityPub error: {:?}", e); domain::errors::DomainError::InfrastructureError(e.to_string()) } fn actor_url(identity: &SocialIdentity) -> String { match identity { SocialIdentity::Remote { actor_url } => actor_url.clone(), SocialIdentity::Local(uid) => format!("local:{}", uid.value()), } } fn social_actor_to_dto(actor: SocialActor) -> RemoteActorDto { RemoteActorDto { url: actor_url(&actor.identity), handle: actor.handle, display_name: actor.display_name, } } fn social_actor_to_blocked_dto(actor: SocialActor) -> BlockedActorResponse { BlockedActorResponse { url: actor_url(&actor.identity), handle: actor.handle, display_name: actor.display_name, avatar_url: actor.avatar_url, } } fn social_actor_to_template(actor: SocialActor) -> RemoteActorData { RemoteActorData { url: actor_url(&actor.identity), handle: actor.handle, display_name: actor.display_name, avatar_url: actor.avatar_url, } } // ── API ────────────────────────────────────────────────────────────────────── #[utoipa::path( get, path = "/api/v1/admin/blocked-domains", responses( (status = 200, body = Vec), (status = 401, description = "Unauthorized"), (status = 403, description = "Forbidden - admin only"), ), security(("bearer_auth" = [])) )] pub async fn get_blocked_domains_admin( State(state): State, _admin: AdminApiUser, ) -> Result>, ApiError> { let domains = state .app_ctx .services .ap_service .get_blocked_domains() .await .map_err(ap_to_domain)?; Ok(Json( domains .into_iter() .map(|d| BlockedDomainResponse { domain: d.domain, reason: d.reason, blocked_at: d.blocked_at, }) .collect(), )) } #[utoipa::path( post, path = "/api/v1/admin/blocked-domains", request_body = AddBlockedDomainRequest, responses( (status = 201, description = "Domain blocked"), (status = 401, description = "Unauthorized"), (status = 403, description = "Forbidden - admin only"), ), security(("bearer_auth" = [])) )] pub async fn add_blocked_domain_admin( State(state): State, _admin: AdminApiUser, axum::Json(body): axum::Json, ) -> Result { state .app_ctx .services .ap_service .add_blocked_domain(&body.domain, body.reason.as_deref()) .await .map_err(ap_to_domain)?; Ok(StatusCode::CREATED) } #[utoipa::path( delete, path = "/api/v1/admin/blocked-domains/{domain}", params(("domain" = String, Path, description = "Domain to unblock")), responses( (status = 204, description = "Domain unblocked"), (status = 401, description = "Unauthorized"), (status = 403, description = "Forbidden - admin only"), ), security(("bearer_auth" = [])) )] pub async fn remove_blocked_domain_admin( State(state): State, _admin: AdminApiUser, axum::extract::Path(domain): axum::extract::Path, ) -> Result { state .app_ctx .services .ap_service .remove_blocked_domain(&domain) .await .map_err(ap_to_domain)?; Ok(StatusCode::NO_CONTENT) } #[utoipa::path( post, path = "/api/v1/social/block", request_body = ActorUrlRequest, responses( (status = 204, description = "Actor blocked"), (status = 401, description = "Unauthorized"), ), security(("bearer_auth" = [])) )] pub async fn block_actor_api( State(state): State, user: AuthenticatedUser, axum::Json(body): axum::Json, ) -> Result { let deps = SocialCommandDeps::from(&state); application::social::execute::execute_command( &deps, application::social::commands::SocialCmd::Block { blocker_id: user.0.value(), target: SocialIdentity::from_actor_url(&body.actor_url, &state.app_ctx.config.base_url), }, ) .await?; Ok(StatusCode::NO_CONTENT) } #[utoipa::path( post, path = "/api/v1/social/unblock", request_body = ActorUrlRequest, responses( (status = 204, description = "Actor unblocked"), (status = 401, description = "Unauthorized"), ), security(("bearer_auth" = [])) )] pub async fn unblock_actor_api( State(state): State, user: AuthenticatedUser, axum::Json(body): axum::Json, ) -> Result { let deps = SocialCommandDeps::from(&state); application::social::execute::execute_command( &deps, application::social::commands::SocialCmd::Unblock { blocker_id: user.0.value(), target: SocialIdentity::from_actor_url(&body.actor_url, &state.app_ctx.config.base_url), }, ) .await?; Ok(StatusCode::NO_CONTENT) } #[utoipa::path( get, path = "/api/v1/social/blocked", responses( (status = 200, body = Vec), (status = 401, description = "Unauthorized"), ), security(("bearer_auth" = [])) )] pub async fn get_blocked_actors_api( State(state): State, user: AuthenticatedUser, ) -> Result>, ApiError> { let deps = SocialQueryDeps::from(&state); let identities = application::social::execute::execute_query( &deps, application::social::queries::SocialQry::GetBlocked { user_id: user.0.value(), }, ) .await?; Ok(Json( identities .into_iter() .map(social_actor_to_blocked_dto) .collect(), )) } #[utoipa::path( get, path = "/api/v1/social/following", responses( (status = 200, body = ActorListResponse), (status = 401, description = "Unauthorized"), ), security(("bearer_auth" = [])) )] pub async fn get_following( State(state): State, user: AuthenticatedUser, ) -> Result, ApiError> { let deps = SocialQueryDeps::from(&state); let identities = application::social::execute::execute_query( &deps, application::social::queries::SocialQry::GetFollowing { user_id: user.0.value(), }, ) .await?; Ok(Json(ActorListResponse { actors: identities.into_iter().map(social_actor_to_dto).collect(), })) } #[utoipa::path( get, path = "/api/v1/social/followers", responses( (status = 200, body = ActorListResponse), (status = 401, description = "Unauthorized"), ), security(("bearer_auth" = [])) )] pub async fn get_followers( State(state): State, user: AuthenticatedUser, ) -> Result, ApiError> { let deps = SocialQueryDeps::from(&state); let identities = application::social::execute::execute_query( &deps, application::social::queries::SocialQry::GetFollowers { user_id: user.0.value(), }, ) .await?; Ok(Json(ActorListResponse { actors: identities.into_iter().map(social_actor_to_dto).collect(), })) } pub async fn get_user_following( State(state): State, _user: AuthenticatedUser, Path(user_id): Path, ) -> Result, ApiError> { let deps = SocialQueryDeps::from(&state); let identities = application::social::execute::execute_query( &deps, application::social::queries::SocialQry::GetFollowing { user_id }, ) .await?; Ok(Json(ActorListResponse { actors: identities.into_iter().map(social_actor_to_dto).collect(), })) } pub async fn get_user_followers( State(state): State, _user: AuthenticatedUser, Path(user_id): Path, ) -> Result, ApiError> { let deps = SocialQueryDeps::from(&state); let identities = application::social::execute::execute_query( &deps, application::social::queries::SocialQry::GetFollowers { user_id }, ) .await?; Ok(Json(ActorListResponse { actors: identities.into_iter().map(social_actor_to_dto).collect(), })) } #[utoipa::path( post, path = "/api/v1/social/follow", request_body = FollowRequest, responses( (status = 200, description = "Follow request sent"), (status = 401, description = "Unauthorized"), ), security(("bearer_auth" = [])) )] pub async fn follow( State(state): State, user: AuthenticatedUser, Json(body): Json, ) -> Result { let deps = SocialCommandDeps::from(&state); application::social::execute::execute_command( &deps, application::social::commands::SocialCmd::Follow { follower_id: user.0.value(), target: FollowTarget::Handle(body.handle), }, ) .await?; Ok(StatusCode::OK) } #[utoipa::path( post, path = "/api/v1/social/unfollow", request_body = ActorUrlRequest, responses( (status = 200, description = "Unfollowed"), (status = 401, description = "Unauthorized"), ), security(("bearer_auth" = [])) )] pub async fn unfollow( State(state): State, user: AuthenticatedUser, Json(body): Json, ) -> Result { let deps = SocialCommandDeps::from(&state); application::social::execute::execute_command( &deps, application::social::commands::SocialCmd::Unfollow { follower_id: user.0.value(), target: SocialIdentity::from_actor_url(&body.actor_url, &state.app_ctx.config.base_url), }, ) .await?; Ok(StatusCode::OK) } #[utoipa::path( post, path = "/api/v1/social/followers/accept", request_body = ActorUrlRequest, responses( (status = 200, description = "Follower accepted"), (status = 401, description = "Unauthorized"), ), security(("bearer_auth" = [])) )] pub async fn accept_follower( State(state): State, user: AuthenticatedUser, Json(body): Json, ) -> Result { let deps = SocialCommandDeps::from(&state); application::social::execute::execute_command( &deps, application::social::commands::SocialCmd::AcceptFollow { owner_id: user.0.value(), requester: SocialIdentity::from_actor_url( &body.actor_url, &state.app_ctx.config.base_url, ), }, ) .await?; Ok(StatusCode::OK) } #[utoipa::path( post, path = "/api/v1/social/followers/reject", request_body = ActorUrlRequest, responses( (status = 200, description = "Follower rejected"), (status = 401, description = "Unauthorized"), ), security(("bearer_auth" = [])) )] pub async fn reject_follower( State(state): State, user: AuthenticatedUser, Json(body): Json, ) -> Result { let deps = SocialCommandDeps::from(&state); application::social::execute::execute_command( &deps, application::social::commands::SocialCmd::RejectFollow { owner_id: user.0.value(), requester: SocialIdentity::from_actor_url( &body.actor_url, &state.app_ctx.config.base_url, ), }, ) .await?; Ok(StatusCode::OK) } #[utoipa::path( post, path = "/api/v1/social/followers/remove", request_body = ActorUrlRequest, responses( (status = 200, description = "Follower removed"), (status = 401, description = "Unauthorized"), ), security(("bearer_auth" = [])) )] pub async fn remove_follower( State(state): State, user: AuthenticatedUser, Json(body): Json, ) -> Result { let deps = SocialCommandDeps::from(&state); application::social::execute::execute_command( &deps, application::social::commands::SocialCmd::RemoveFollower { owner_id: user.0.value(), follower: SocialIdentity::from_actor_url( &body.actor_url, &state.app_ctx.config.base_url, ), }, ) .await?; Ok(StatusCode::OK) } #[utoipa::path( get, path = "/api/v1/social/followers/pending", responses( (status = 200, body = ActorListResponse), (status = 401, description = "Unauthorized"), ), security(("bearer_auth" = [])) )] pub async fn get_pending_followers( State(state): State, user: AuthenticatedUser, ) -> Result, ApiError> { let deps = SocialQueryDeps::from(&state); let identities = application::social::execute::execute_query( &deps, application::social::queries::SocialQry::GetPending { user_id: user.0.value(), }, ) .await?; Ok(Json(ActorListResponse { actors: identities.into_iter().map(social_actor_to_dto).collect(), })) } // ── HTML ───────────────────────────────────────────────────────────────────── pub async fn follow_remote_user( RequiredCookieUser(user_id): RequiredCookieUser, State(state): State, Path(profile_user_uuid): Path, Extension(csrf): Extension, Form(form): Form, ) -> impl IntoResponse { if user_id.value() != profile_user_uuid { return StatusCode::FORBIDDEN.into_response(); } if crate::csrf::mismatch(&csrf, &form.csrf_token) { return StatusCode::FORBIDDEN.into_response(); } let redirect_base = form .redirect_after .as_deref() .filter(|u| u.starts_with('/') && !u.starts_with("//")) .unwrap_or(&format!("/users/{}", profile_user_uuid)) .to_string(); let deps = SocialCommandDeps::from(&state); match application::social::execute::execute_command( &deps, application::social::commands::SocialCmd::Follow { follower_id: user_id.value(), target: FollowTarget::Handle(form.handle), }, ) .await { Ok(()) => Redirect::to(&redirect_base).into_response(), Err(e) => { tracing::error!("follow error: {:?}", e); let msg = encode_error(&e.to_string()); let sep = if redirect_base.contains('?') { '&' } else { '?' }; Redirect::to(&format!("{}{}error={}", redirect_base, sep, msg)).into_response() } } } pub async fn unfollow_remote_user( RequiredCookieUser(user_id): RequiredCookieUser, State(state): State, Path(profile_user_uuid): Path, Extension(csrf): Extension, Form(form): Form, ) -> impl IntoResponse { if user_id.value() != profile_user_uuid { return StatusCode::FORBIDDEN.into_response(); } if crate::csrf::mismatch(&csrf, &form.csrf_token) { return StatusCode::FORBIDDEN.into_response(); } let deps = SocialCommandDeps::from(&state); match application::social::execute::execute_command( &deps, application::social::commands::SocialCmd::Unfollow { follower_id: user_id.value(), target: SocialIdentity::from_actor_url(&form.actor_url, &state.app_ctx.config.base_url), }, ) .await { Ok(()) => { Redirect::to(&format!("/users/{}/following-list", profile_user_uuid)).into_response() } Err(e) => { let msg = encode_error(&e.to_string()); Redirect::to(&format!( "/users/{}/following-list?error={}", profile_user_uuid, msg )) .into_response() } } } pub async fn accept_follower_html( RequiredCookieUser(user_id): RequiredCookieUser, State(state): State, Path(profile_user_uuid): Path, Extension(csrf): Extension, Form(form): Form, ) -> impl IntoResponse { if user_id.value() != profile_user_uuid { return StatusCode::FORBIDDEN.into_response(); } if crate::csrf::mismatch(&csrf, &form.csrf_token) { return StatusCode::FORBIDDEN.into_response(); } let deps = SocialCommandDeps::from(&state); match application::social::execute::execute_command( &deps, application::social::commands::SocialCmd::AcceptFollow { owner_id: user_id.value(), requester: SocialIdentity::from_actor_url( &form.actor_url, &state.app_ctx.config.base_url, ), }, ) .await { Ok(_) => Redirect::to(&format!("/users/{}", profile_user_uuid)).into_response(), Err(e) => { let msg = encode_error(&e.to_string()); Redirect::to(&format!("/users/{}?error={}", profile_user_uuid, msg)).into_response() } } } pub async fn reject_follower_html( RequiredCookieUser(user_id): RequiredCookieUser, State(state): State, Path(profile_user_uuid): Path, Extension(csrf): Extension, Form(form): Form, ) -> impl IntoResponse { if user_id.value() != profile_user_uuid { return StatusCode::FORBIDDEN.into_response(); } if crate::csrf::mismatch(&csrf, &form.csrf_token) { return StatusCode::FORBIDDEN.into_response(); } let deps = SocialCommandDeps::from(&state); match application::social::execute::execute_command( &deps, application::social::commands::SocialCmd::RejectFollow { owner_id: user_id.value(), requester: SocialIdentity::from_actor_url( &form.actor_url, &state.app_ctx.config.base_url, ), }, ) .await { Ok(_) => Redirect::to(&format!("/users/{}", profile_user_uuid)).into_response(), Err(e) => { let msg = encode_error(&e.to_string()); Redirect::to(&format!("/users/{}?error={}", profile_user_uuid, msg)).into_response() } } } pub async fn get_followers_collection( State(state): State, Path(user_id): Path, headers: axum::http::HeaderMap, Query(params): Query>, ) -> impl IntoResponse { let accept = headers .get(axum::http::header::ACCEPT) .and_then(|v| v.to_str().ok()) .unwrap_or(""); if accept.contains("application/activity+json") || accept.contains("application/ld+json") { let page = params.get("page").and_then(|p| p.parse::().ok()); return match state .app_ctx .services .ap_service .followers_collection_json(user_id, page) .await { Ok(json) => ( [( axum::http::header::CONTENT_TYPE, "application/activity+json", )], json, ) .into_response(), Err(_) => StatusCode::NOT_FOUND.into_response(), }; } axum::response::Redirect::to(&format!("/users/{}/followers-list", user_id)).into_response() } pub async fn get_following_collection( State(state): State, Path(user_id): Path, headers: axum::http::HeaderMap, Query(params): Query>, ) -> impl IntoResponse { let accept = headers .get(axum::http::header::ACCEPT) .and_then(|v| v.to_str().ok()) .unwrap_or(""); if accept.contains("application/activity+json") || accept.contains("application/ld+json") { let page = params.get("page").and_then(|p| p.parse::().ok()); return match state .app_ctx .services .ap_service .following_collection_json(user_id, page) .await { Ok(json) => ( [( axum::http::header::CONTENT_TYPE, "application/activity+json", )], json, ) .into_response(), Err(_) => StatusCode::NOT_FOUND.into_response(), }; } axum::response::Redirect::to(&format!("/users/{}/following-list", user_id)).into_response() } pub async fn get_following_page( RequiredCookieUser(user_id): RequiredCookieUser, State(state): State, Path(profile_user_uuid): Path, Query(params): Query, Extension(csrf): Extension, ) -> impl IntoResponse { if user_id.value() != profile_user_uuid { return StatusCode::FORBIDDEN.into_response(); } let mut ctx = build_page_context(&state, Some(user_id.clone()), csrf.0).await; ctx.page_title = "Following — Movies Diary".to_string(); ctx.canonical_url = format!( "{}/users/{}/following-list", state.app_ctx.config.base_url, profile_user_uuid ); let deps = SocialQueryDeps::from(&state); match application::social::execute::execute_query( &deps, application::social::queries::SocialQry::GetFollowing { user_id: user_id.value(), }, ) .await { Ok(following) => { let actors: Vec = following .into_iter() .map(social_actor_to_template) .collect(); render_page(FollowingTemplate { ctx, user_id: profile_user_uuid, actors, error: params.error, }) .into_response() } Err(e) => { tracing::error!("get_following error: {:?}", e); ( StatusCode::INTERNAL_SERVER_ERROR, "Failed to load following list", ) .into_response() } } } pub async fn get_followers_page( RequiredCookieUser(user_id): RequiredCookieUser, State(state): State, Path(profile_user_uuid): Path, Query(params): Query, Extension(csrf): Extension, ) -> impl IntoResponse { if user_id.value() != profile_user_uuid { return StatusCode::FORBIDDEN.into_response(); } let mut ctx = build_page_context(&state, Some(user_id.clone()), csrf.0).await; ctx.page_title = "Followers — Movies Diary".to_string(); ctx.canonical_url = format!( "{}/users/{}/followers-list", state.app_ctx.config.base_url, profile_user_uuid ); let deps = SocialQueryDeps::from(&state); match application::social::execute::execute_query( &deps, application::social::queries::SocialQry::GetFollowers { user_id: user_id.value(), }, ) .await { Ok(followers) => { let actors: Vec = followers .into_iter() .map(social_actor_to_template) .collect(); render_page(FollowersTemplate { ctx, user_id: profile_user_uuid, actors, error: params.error, }) .into_response() } Err(e) => { tracing::error!("get_followers error: {:?}", e); ( StatusCode::INTERNAL_SERVER_ERROR, "Failed to load followers list", ) .into_response() } } } pub async fn remove_follower_html( RequiredCookieUser(user_id): RequiredCookieUser, State(state): State, Path(profile_user_uuid): Path, Extension(csrf): Extension, Form(form): Form, ) -> impl IntoResponse { if user_id.value() != profile_user_uuid { return StatusCode::FORBIDDEN.into_response(); } if crate::csrf::mismatch(&csrf, &form.csrf_token) { return StatusCode::FORBIDDEN.into_response(); } let deps = SocialCommandDeps::from(&state); match application::social::execute::execute_command( &deps, application::social::commands::SocialCmd::RemoveFollower { owner_id: user_id.value(), follower: SocialIdentity::from_actor_url( &form.actor_url, &state.app_ctx.config.base_url, ), }, ) .await { Ok(_) => { Redirect::to(&format!("/users/{}/followers-list", profile_user_uuid)).into_response() } Err(e) => { let msg = encode_error(&e.to_string()); Redirect::to(&format!( "/users/{}/followers-list?error={}", profile_user_uuid, msg )) .into_response() } } } pub async fn get_blocked_domains_page( crate::extractors::AdminUser(user_id): crate::extractors::AdminUser, State(state): State, Extension(csrf): Extension, ) -> impl IntoResponse { let mut ctx = build_page_context(&state, Some(user_id), csrf.0).await; ctx.page_title = "Blocked Domains — Movies Diary".to_string(); ctx.canonical_url = format!("{}/admin/blocked-domains", state.app_ctx.config.base_url); match state .app_ctx .services .ap_service .get_blocked_domains() .await { Ok(domains) => { let entries: Vec = domains .into_iter() .map(|d| template_askama::BlockedDomainEntry { domain: d.domain, reason: d.reason, blocked_at: d.blocked_at, }) .collect(); render_page(BlockedDomainsTemplate { ctx: &ctx, domains: &entries, }) .into_response() } Err(e) => { tracing::error!("get_blocked_domains error: {:?}", e); ( StatusCode::INTERNAL_SERVER_ERROR, "Failed to load blocked domains", ) .into_response() } } } pub async fn post_blocked_domain( crate::extractors::AdminUser(_): crate::extractors::AdminUser, State(state): State, Extension(csrf): Extension, Form(form): Form, ) -> impl IntoResponse { if crate::csrf::mismatch(&csrf, &form.csrf_token) { return StatusCode::FORBIDDEN.into_response(); } let reason = form.reason.as_deref().filter(|s| !s.trim().is_empty()); match state .app_ctx .services .ap_service .add_blocked_domain(&form.domain, reason) .await { Ok(()) => Redirect::to("/admin/blocked-domains").into_response(), Err(e) => { tracing::error!("add_blocked_domain error: {:?}", e); Redirect::to("/admin/blocked-domains").into_response() } } } pub async fn post_remove_blocked_domain( crate::extractors::AdminUser(_): crate::extractors::AdminUser, State(state): State, Extension(csrf): Extension, Form(form): Form, ) -> impl IntoResponse { if crate::csrf::mismatch(&csrf, &form.csrf_token) { return StatusCode::FORBIDDEN.into_response(); } match state .app_ctx .services .ap_service .remove_blocked_domain(&form.domain) .await { Ok(()) => Redirect::to("/admin/blocked-domains").into_response(), Err(e) => { tracing::error!("remove_blocked_domain error: {:?}", e); Redirect::to("/admin/blocked-domains").into_response() } } } pub async fn get_blocked_actors_page( RequiredCookieUser(user_id): RequiredCookieUser, State(state): State, Extension(csrf): Extension, ) -> impl IntoResponse { let mut ctx = build_page_context(&state, Some(user_id.clone()), csrf.0).await; ctx.page_title = "Blocked Users — Movies Diary".to_string(); ctx.canonical_url = format!("{}/social/blocked", state.app_ctx.config.base_url); let deps = SocialQueryDeps::from(&state); match application::social::execute::execute_query( &deps, application::social::queries::SocialQry::GetBlocked { user_id: user_id.value(), }, ) .await { Ok(blocked) => { let entries: Vec = blocked .into_iter() .map(|a| template_askama::BlockedActorEntry { url: actor_url(&a.identity), handle: a.handle, display_name: a.display_name, avatar_url: a.avatar_url, }) .collect(); render_page(BlockedActorsTemplate { ctx: &ctx, actors: &entries, }) .into_response() } Err(e) => { tracing::error!("get_blocked_actors error: {:?}", e); ( StatusCode::INTERNAL_SERVER_ERROR, "Failed to load blocked users", ) .into_response() } } } pub async fn post_block_actor_html( RequiredCookieUser(user_id): RequiredCookieUser, State(state): State, Extension(csrf): Extension, Form(form): Form, ) -> impl IntoResponse { if crate::csrf::mismatch(&csrf, &form.csrf_token) { return StatusCode::FORBIDDEN.into_response(); } let deps = SocialCommandDeps::from(&state); match application::social::execute::execute_command( &deps, application::social::commands::SocialCmd::Block { blocker_id: user_id.value(), target: SocialIdentity::from_actor_url(&form.actor_url, &state.app_ctx.config.base_url), }, ) .await { Ok(()) => Redirect::to("/social/blocked").into_response(), Err(e) => { tracing::error!("block_actor html error: {:?}", e); Redirect::to("/social/blocked").into_response() } } } pub async fn post_unblock_actor( RequiredCookieUser(user_id): RequiredCookieUser, State(state): State, Extension(csrf): Extension, Form(form): Form, ) -> impl IntoResponse { if crate::csrf::mismatch(&csrf, &form.csrf_token) { return StatusCode::FORBIDDEN.into_response(); } let deps = SocialCommandDeps::from(&state); match application::social::execute::execute_command( &deps, application::social::commands::SocialCmd::Unblock { blocker_id: user_id.value(), target: SocialIdentity::from_actor_url(&form.actor_url, &state.app_ctx.config.base_url), }, ) .await { Ok(()) => Redirect::to("/social/blocked").into_response(), Err(e) => { tracing::error!("unblock_actor error: {:?}", e); Redirect::to("/social/blocked").into_response() } } }