use crate::html::helpers::{build_page_context, encode_domain_error};
use crate::html::templates::{FollowersTemplate, FollowingTemplate, RemoteActorData};
use crate::{
extractors::RequiredCookieUser,
html::csrf::CsrfToken,
html::forms::{FollowForm, FollowerActionForm, UnfollowForm},
html::render::render_page,
state::AppState,
};
use axum::{
Form,
extract::{Extension, Path, Query, State},
http::StatusCode,
response::{IntoResponse, Redirect},
};
use domain::value_objects::{FollowTarget, InstanceIdentity, SocialActor};
use uuid::Uuid;
fn social_actor_to_template(actor: SocialActor, instance: &InstanceIdentity) -> RemoteActorData {
RemoteActorData {
url: instance.actor_url_of(&actor.identity),
handle: actor.handle,
display_name: actor.display_name,
avatar_url: actor.avatar_url,
}
}
pub async fn follow_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::html::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();
match application::social::execute::execute_command(
&state.app_ctx.deps.social.command,
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_domain_error(&e);
let sep = if redirect_base.contains('?') {
'&'
} else {
'?'
};
Redirect::to(&format!("{}{}error={}", redirect_base, sep, msg)).into_response()
}
}
}
pub async fn unfollow_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::html::csrf::mismatch(&csrf, &form.csrf_token) {
return StatusCode::FORBIDDEN.into_response();
}
let instance = state.app_ctx.instance.clone();
match application::social::execute::execute_command(
&state.app_ctx.deps.social.command,
application::social::commands::SocialCmd::Unfollow {
follower_id: user_id.value(),
target: instance.identify(&form.actor_url),
},
)
.await
{
Ok(()) => {
Redirect::to(&format!("/users/{}/following-list", profile_user_uuid)).into_response()
}
Err(e) => {
let msg = encode_domain_error(&e);
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::html::csrf::mismatch(&csrf, &form.csrf_token) {
return StatusCode::FORBIDDEN.into_response();
}
let instance = state.app_ctx.instance.clone();
match application::social::execute::execute_command(
&state.app_ctx.deps.social.command,
application::social::commands::SocialCmd::AcceptFollow {
owner_id: user_id.value(),
requester: instance.identify(&form.actor_url),
},
)
.await
{
Ok(_) => Redirect::to(&format!("/users/{}", profile_user_uuid)).into_response(),
Err(e) => {
let msg = encode_domain_error(&e);
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::html::csrf::mismatch(&csrf, &form.csrf_token) {
return StatusCode::FORBIDDEN.into_response();
}
let instance = state.app_ctx.instance.clone();
match application::social::execute::execute_command(
&state.app_ctx.deps.social.command,
application::social::commands::SocialCmd::RejectFollow {
owner_id: user_id.value(),
requester: instance.identify(&form.actor_url),
},
)
.await
{
Ok(_) => Redirect::to(&format!("/users/{}", profile_user_uuid)).into_response(),
Err(e) => {
let msg = encode_domain_error(&e);
Redirect::to(&format!("/users/{}?error={}", profile_user_uuid, msg)).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 instance = state.app_ctx.instance.clone();
match application::social::get_following::execute(
&state.app_ctx.deps.social.query,
user_id.value(),
)
.await
{
Ok(following) => {
let actors: Vec = following
.into_iter()
.map(|a| social_actor_to_template(a, &instance))
.collect();
let pending_actors: Vec =
match application::social::get_pending_following::execute(
&state.app_ctx.deps.social.query,
user_id.value(),
)
.await
{
Ok(pending) => pending
.into_iter()
.map(|a| social_actor_to_template(a, &instance))
.collect(),
Err(e) => {
tracing::error!("get_pending_following error: {:?}", e);
Vec::new()
}
};
render_page(FollowingTemplate {
ctx,
user_id: profile_user_uuid,
actors,
pending_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 instance = state.app_ctx.instance.clone();
match application::social::get_followers::execute(
&state.app_ctx.deps.social.query,
user_id.value(),
)
.await
{
Ok(followers) => {
let actors: Vec = followers
.into_iter()
.map(|a| social_actor_to_template(a, &instance))
.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::html::csrf::mismatch(&csrf, &form.csrf_token) {
return StatusCode::FORBIDDEN.into_response();
}
let instance = state.app_ctx.instance.clone();
match application::social::execute::execute_command(
&state.app_ctx.deps.social.command,
application::social::commands::SocialCmd::RemoveFollower {
owner_id: user_id.value(),
follower: instance.identify(&form.actor_url),
},
)
.await
{
Ok(_) => {
Redirect::to(&format!("/users/{}/followers-list", profile_user_uuid)).into_response()
}
Err(e) => {
let msg = encode_domain_error(&e);
Redirect::to(&format!(
"/users/{}/followers-list?error={}",
profile_user_uuid, msg
))
.into_response()
}
}
}
#[cfg(test)]
mod tests;