feat: unified social identity layer — SocialIdentity, ports, use cases, adapter
SocialIdentity value object (Local|Remote), SocialCommand/SocialQuery domain ports, 11 CQRS use cases w/ 14 tests, CompositeSocialAdapter wrapping k_ap, 6 new domain events, handlers migrated from ap_service to use cases. Closes #12 foundation — AP handlers + legacy cleanup TBD.
This commit is contained in:
@@ -19,8 +19,10 @@ use crate::{
|
||||
};
|
||||
use api_types::{
|
||||
ActorListResponse, ActorUrlRequest, AddBlockedDomainRequest, BlockedActorResponse,
|
||||
BlockedDomainResponse, FollowRequest,
|
||||
BlockedDomainResponse, FollowRequest, RemoteActorDto,
|
||||
};
|
||||
use application::social::deps::{SocialCommandDeps, SocialQueryDeps};
|
||||
use domain::value_objects::SocialIdentity;
|
||||
use template_askama::{
|
||||
BlockedActorsTemplate, BlockedDomainsTemplate, FollowersTemplate, FollowingTemplate,
|
||||
RemoteActorData,
|
||||
@@ -33,6 +35,38 @@ fn ap_to_domain(e: anyhow::Error) -> domain::errors::DomainError {
|
||||
domain::errors::DomainError::InfrastructureError(e.to_string())
|
||||
}
|
||||
|
||||
fn social_identity_to_dto(id: SocialIdentity) -> RemoteActorDto {
|
||||
match id {
|
||||
SocialIdentity::Remote { actor_url } => RemoteActorDto {
|
||||
url: actor_url,
|
||||
handle: String::new(),
|
||||
display_name: None,
|
||||
},
|
||||
SocialIdentity::Local(uid) => RemoteActorDto {
|
||||
url: format!("local:{}", uid.value()),
|
||||
handle: String::new(),
|
||||
display_name: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn social_identity_to_blocked_dto(id: SocialIdentity) -> BlockedActorResponse {
|
||||
match id {
|
||||
SocialIdentity::Remote { actor_url } => BlockedActorResponse {
|
||||
url: actor_url,
|
||||
handle: String::new(),
|
||||
display_name: None,
|
||||
avatar_url: None,
|
||||
},
|
||||
SocialIdentity::Local(uid) => BlockedActorResponse {
|
||||
url: format!("local:{}", uid.value()),
|
||||
handle: String::new(),
|
||||
display_name: None,
|
||||
avatar_url: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ── API ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -125,11 +159,21 @@ pub async fn block_actor_api(
|
||||
user: AuthenticatedUser,
|
||||
axum::Json(body): axum::Json<ActorUrlRequest>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
state
|
||||
.ap_service
|
||||
.block_actor(user.0.value(), &body.actor_url)
|
||||
.await
|
||||
.map_err(ap_to_domain)?;
|
||||
let deps = SocialCommandDeps {
|
||||
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(),
|
||||
};
|
||||
application::social::block::execute(
|
||||
&deps,
|
||||
application::social::commands::BlockCommand {
|
||||
blocker_id: user.0.value(),
|
||||
target: SocialIdentity::Remote {
|
||||
actor_url: body.actor_url,
|
||||
},
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
@@ -147,11 +191,21 @@ pub async fn unblock_actor_api(
|
||||
user: AuthenticatedUser,
|
||||
axum::Json(body): axum::Json<ActorUrlRequest>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
state
|
||||
.ap_service
|
||||
.unblock_actor(user.0.value(), &body.actor_url)
|
||||
.await
|
||||
.map_err(ap_to_domain)?;
|
||||
let deps = SocialCommandDeps {
|
||||
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(),
|
||||
};
|
||||
application::social::unblock::execute(
|
||||
&deps,
|
||||
application::social::commands::UnblockCommand {
|
||||
blocker_id: user.0.value(),
|
||||
target: SocialIdentity::Remote {
|
||||
actor_url: body.actor_url,
|
||||
},
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
@@ -167,20 +221,20 @@ pub async fn get_blocked_actors_api(
|
||||
State(state): State<AppState>,
|
||||
user: AuthenticatedUser,
|
||||
) -> Result<Json<Vec<BlockedActorResponse>>, ApiError> {
|
||||
let actors = state
|
||||
.ap_service
|
||||
.get_blocked_actors(user.0.value())
|
||||
.await
|
||||
.map_err(ap_to_domain)?;
|
||||
let deps = SocialQueryDeps {
|
||||
social_query: state.app_ctx.repos.social_query_unified.clone(),
|
||||
};
|
||||
let identities = application::social::get_blocked::execute(
|
||||
&deps,
|
||||
application::social::queries::GetBlockedQuery {
|
||||
user_id: user.0.value(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(
|
||||
actors
|
||||
identities
|
||||
.into_iter()
|
||||
.map(|a| BlockedActorResponse {
|
||||
url: a.url,
|
||||
handle: a.handle,
|
||||
display_name: a.display_name,
|
||||
avatar_url: a.avatar_url,
|
||||
})
|
||||
.map(social_identity_to_blocked_dto)
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
@@ -197,15 +251,20 @@ pub async fn get_following(
|
||||
State(state): State<AppState>,
|
||||
user: AuthenticatedUser,
|
||||
) -> Result<Json<ActorListResponse>, ApiError> {
|
||||
let actors = state
|
||||
.ap_service
|
||||
.get_following(user.0.value())
|
||||
.await
|
||||
.map_err(ap_to_domain)?;
|
||||
let deps = SocialQueryDeps {
|
||||
social_query: state.app_ctx.repos.social_query_unified.clone(),
|
||||
};
|
||||
let identities = application::social::get_following::execute(
|
||||
&deps,
|
||||
application::social::queries::GetFollowingQuery {
|
||||
user_id: user.0.value(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(ActorListResponse {
|
||||
actors: actors
|
||||
actors: identities
|
||||
.into_iter()
|
||||
.map(crate::mappers::social::remote_actor_to_dto)
|
||||
.map(social_identity_to_dto)
|
||||
.collect(),
|
||||
}))
|
||||
}
|
||||
@@ -222,15 +281,20 @@ pub async fn get_followers(
|
||||
State(state): State<AppState>,
|
||||
user: AuthenticatedUser,
|
||||
) -> Result<Json<ActorListResponse>, ApiError> {
|
||||
let actors = state
|
||||
.ap_service
|
||||
.get_accepted_followers(user.0.value())
|
||||
.await
|
||||
.map_err(ap_to_domain)?;
|
||||
let deps = SocialQueryDeps {
|
||||
social_query: state.app_ctx.repos.social_query_unified.clone(),
|
||||
};
|
||||
let identities = application::social::get_followers::execute(
|
||||
&deps,
|
||||
application::social::queries::GetFollowersQuery {
|
||||
user_id: user.0.value(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(ActorListResponse {
|
||||
actors: actors
|
||||
actors: identities
|
||||
.into_iter()
|
||||
.map(crate::mappers::social::remote_actor_to_dto)
|
||||
.map(social_identity_to_dto)
|
||||
.collect(),
|
||||
}))
|
||||
}
|
||||
@@ -240,15 +304,18 @@ pub async fn get_user_following(
|
||||
_user: AuthenticatedUser,
|
||||
Path(user_id): Path<Uuid>,
|
||||
) -> Result<Json<ActorListResponse>, ApiError> {
|
||||
let actors = state
|
||||
.ap_service
|
||||
.get_following(user_id)
|
||||
.await
|
||||
.map_err(ap_to_domain)?;
|
||||
let deps = SocialQueryDeps {
|
||||
social_query: state.app_ctx.repos.social_query_unified.clone(),
|
||||
};
|
||||
let identities = application::social::get_following::execute(
|
||||
&deps,
|
||||
application::social::queries::GetFollowingQuery { user_id },
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(ActorListResponse {
|
||||
actors: actors
|
||||
actors: identities
|
||||
.into_iter()
|
||||
.map(crate::mappers::social::remote_actor_to_dto)
|
||||
.map(social_identity_to_dto)
|
||||
.collect(),
|
||||
}))
|
||||
}
|
||||
@@ -258,15 +325,18 @@ pub async fn get_user_followers(
|
||||
_user: AuthenticatedUser,
|
||||
Path(user_id): Path<Uuid>,
|
||||
) -> Result<Json<ActorListResponse>, ApiError> {
|
||||
let actors = state
|
||||
.ap_service
|
||||
.get_accepted_followers(user_id)
|
||||
.await
|
||||
.map_err(ap_to_domain)?;
|
||||
let deps = SocialQueryDeps {
|
||||
social_query: state.app_ctx.repos.social_query_unified.clone(),
|
||||
};
|
||||
let identities = application::social::get_followers::execute(
|
||||
&deps,
|
||||
application::social::queries::GetFollowersQuery { user_id },
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(ActorListResponse {
|
||||
actors: actors
|
||||
actors: identities
|
||||
.into_iter()
|
||||
.map(crate::mappers::social::remote_actor_to_dto)
|
||||
.map(social_identity_to_dto)
|
||||
.collect(),
|
||||
}))
|
||||
}
|
||||
@@ -285,11 +355,21 @@ pub async fn follow(
|
||||
user: AuthenticatedUser,
|
||||
Json(body): Json<FollowRequest>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
state
|
||||
.ap_service
|
||||
.follow(user.0.value(), &body.handle)
|
||||
.await
|
||||
.map_err(ap_to_domain)?;
|
||||
let deps = SocialCommandDeps {
|
||||
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(),
|
||||
};
|
||||
application::social::follow::execute(
|
||||
&deps,
|
||||
application::social::commands::FollowCommand {
|
||||
follower_id: user.0.value(),
|
||||
target: SocialIdentity::Remote {
|
||||
actor_url: body.handle,
|
||||
},
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
@@ -307,11 +387,21 @@ pub async fn unfollow(
|
||||
user: AuthenticatedUser,
|
||||
Json(body): Json<ActorUrlRequest>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
state
|
||||
.ap_service
|
||||
.unfollow(user.0.value(), &body.actor_url)
|
||||
.await
|
||||
.map_err(ap_to_domain)?;
|
||||
let deps = SocialCommandDeps {
|
||||
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(),
|
||||
};
|
||||
application::social::unfollow::execute(
|
||||
&deps,
|
||||
application::social::commands::UnfollowCommand {
|
||||
follower_id: user.0.value(),
|
||||
target: SocialIdentity::Remote {
|
||||
actor_url: body.actor_url,
|
||||
},
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
@@ -329,11 +419,21 @@ pub async fn accept_follower(
|
||||
user: AuthenticatedUser,
|
||||
Json(body): Json<ActorUrlRequest>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
state
|
||||
.ap_service
|
||||
.accept_follower(user.0.value(), &body.actor_url)
|
||||
.await
|
||||
.map_err(ap_to_domain)?;
|
||||
let deps = SocialCommandDeps {
|
||||
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(),
|
||||
};
|
||||
application::social::accept::execute(
|
||||
&deps,
|
||||
application::social::commands::AcceptFollowCommand {
|
||||
owner_id: user.0.value(),
|
||||
requester: SocialIdentity::Remote {
|
||||
actor_url: body.actor_url,
|
||||
},
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
@@ -351,11 +451,21 @@ pub async fn reject_follower(
|
||||
user: AuthenticatedUser,
|
||||
Json(body): Json<ActorUrlRequest>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
state
|
||||
.ap_service
|
||||
.reject_follower(user.0.value(), &body.actor_url)
|
||||
.await
|
||||
.map_err(ap_to_domain)?;
|
||||
let deps = SocialCommandDeps {
|
||||
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(),
|
||||
};
|
||||
application::social::reject::execute(
|
||||
&deps,
|
||||
application::social::commands::RejectFollowCommand {
|
||||
owner_id: user.0.value(),
|
||||
requester: SocialIdentity::Remote {
|
||||
actor_url: body.actor_url,
|
||||
},
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
@@ -373,11 +483,21 @@ pub async fn remove_follower(
|
||||
user: AuthenticatedUser,
|
||||
Json(body): Json<ActorUrlRequest>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
state
|
||||
.ap_service
|
||||
.remove_follower(user.0.value(), &body.actor_url)
|
||||
.await
|
||||
.map_err(ap_to_domain)?;
|
||||
let deps = SocialCommandDeps {
|
||||
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(),
|
||||
};
|
||||
application::social::remove_follower::execute(
|
||||
&deps,
|
||||
application::social::commands::RemoveFollowerCommand {
|
||||
owner_id: user.0.value(),
|
||||
follower: SocialIdentity::Remote {
|
||||
actor_url: body.actor_url,
|
||||
},
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
@@ -393,15 +513,20 @@ pub async fn get_pending_followers(
|
||||
State(state): State<AppState>,
|
||||
user: AuthenticatedUser,
|
||||
) -> Result<Json<ActorListResponse>, ApiError> {
|
||||
let actors = state
|
||||
.ap_service
|
||||
.get_pending_followers(user.0.value())
|
||||
.await
|
||||
.map_err(ap_to_domain)?;
|
||||
let deps = SocialQueryDeps {
|
||||
social_query: state.app_ctx.repos.social_query_unified.clone(),
|
||||
};
|
||||
let identities = application::social::get_pending::execute(
|
||||
&deps,
|
||||
application::social::queries::GetPendingFollowersQuery {
|
||||
user_id: user.0.value(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(ActorListResponse {
|
||||
actors: actors
|
||||
actors: identities
|
||||
.into_iter()
|
||||
.map(crate::mappers::social::remote_actor_to_dto)
|
||||
.map(social_identity_to_dto)
|
||||
.collect(),
|
||||
}))
|
||||
}
|
||||
@@ -428,7 +553,20 @@ pub async fn follow_remote_user(
|
||||
.unwrap_or(&format!("/users/{}", profile_user_uuid))
|
||||
.to_string();
|
||||
|
||||
match state.ap_service.follow(user_id.value(), &form.handle).await {
|
||||
let deps = SocialCommandDeps {
|
||||
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(),
|
||||
};
|
||||
match application::social::follow::execute(
|
||||
&deps,
|
||||
application::social::commands::FollowCommand {
|
||||
follower_id: user_id.value(),
|
||||
target: SocialIdentity::Remote { actor_url: form.handle },
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(()) => Redirect::to(&redirect_base).into_response(),
|
||||
Err(e) => {
|
||||
tracing::error!("follow error: {:?}", e);
|
||||
@@ -456,10 +594,19 @@ pub async fn unfollow_remote_user(
|
||||
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
||||
return StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
match state
|
||||
.ap_service
|
||||
.unfollow(user_id.value(), &form.actor_url)
|
||||
.await
|
||||
let deps = SocialCommandDeps {
|
||||
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(),
|
||||
};
|
||||
match application::social::unfollow::execute(
|
||||
&deps,
|
||||
application::social::commands::UnfollowCommand {
|
||||
follower_id: user_id.value(),
|
||||
target: SocialIdentity::Remote { actor_url: form.actor_url },
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
Redirect::to(&format!("/users/{}/following-list", profile_user_uuid)).into_response()
|
||||
@@ -488,10 +635,19 @@ pub async fn accept_follower_html(
|
||||
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
||||
return StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
match state
|
||||
.ap_service
|
||||
.accept_follower(user_id.value(), &form.actor_url)
|
||||
.await
|
||||
let deps = SocialCommandDeps {
|
||||
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(),
|
||||
};
|
||||
match application::social::accept::execute(
|
||||
&deps,
|
||||
application::social::commands::AcceptFollowCommand {
|
||||
owner_id: user_id.value(),
|
||||
requester: SocialIdentity::Remote { actor_url: form.actor_url },
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => Redirect::to(&format!("/users/{}", profile_user_uuid)).into_response(),
|
||||
Err(e) => {
|
||||
@@ -514,10 +670,19 @@ pub async fn reject_follower_html(
|
||||
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
||||
return StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
match state
|
||||
.ap_service
|
||||
.reject_follower(user_id.value(), &form.actor_url)
|
||||
.await
|
||||
let deps = SocialCommandDeps {
|
||||
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(),
|
||||
};
|
||||
match application::social::reject::execute(
|
||||
&deps,
|
||||
application::social::commands::RejectFollowCommand {
|
||||
owner_id: user_id.value(),
|
||||
requester: SocialIdentity::Remote { actor_url: form.actor_url },
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => Redirect::to(&format!("/users/{}", profile_user_uuid)).into_response(),
|
||||
Err(e) => {
|
||||
@@ -698,10 +863,19 @@ pub async fn remove_follower_html(
|
||||
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
||||
return StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
match state
|
||||
.ap_service
|
||||
.remove_follower(user_id.value(), &form.actor_url)
|
||||
.await
|
||||
let deps = SocialCommandDeps {
|
||||
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(),
|
||||
};
|
||||
match application::social::remove_follower::execute(
|
||||
&deps,
|
||||
application::social::commands::RemoveFollowerCommand {
|
||||
owner_id: user_id.value(),
|
||||
follower: SocialIdentity::Remote { actor_url: form.actor_url },
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
Redirect::to(&format!("/users/{}/followers-list", profile_user_uuid)).into_response()
|
||||
@@ -838,10 +1012,19 @@ pub async fn post_block_actor_html(
|
||||
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
||||
return StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
match state
|
||||
.ap_service
|
||||
.block_actor(user_id.value(), &form.actor_url)
|
||||
.await
|
||||
let deps = SocialCommandDeps {
|
||||
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(),
|
||||
};
|
||||
match application::social::block::execute(
|
||||
&deps,
|
||||
application::social::commands::BlockCommand {
|
||||
blocker_id: user_id.value(),
|
||||
target: SocialIdentity::Remote { actor_url: form.actor_url },
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(()) => Redirect::to("/social/blocked").into_response(),
|
||||
Err(e) => {
|
||||
@@ -860,10 +1043,19 @@ pub async fn post_unblock_actor(
|
||||
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
||||
return StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
match state
|
||||
.ap_service
|
||||
.unblock_actor(user_id.value(), &form.actor_url)
|
||||
.await
|
||||
let deps = SocialCommandDeps {
|
||||
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(),
|
||||
};
|
||||
match application::social::unblock::execute(
|
||||
&deps,
|
||||
application::social::commands::UnblockCommand {
|
||||
blocker_id: user_id.value(),
|
||||
target: SocialIdentity::Remote { actor_url: form.actor_url },
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(()) => Redirect::to("/social/blocked").into_response(),
|
||||
Err(e) => {
|
||||
|
||||
Reference in New Issue
Block a user