use crate::{ error::{Error, OptionExt}, extensions::ProfileExt, middleware::{ProfileData, Referer, UserProfile}, nav::NavState, views::OwnedProfileView, ActixLoader, State, }; use actix_session::Session; use actix_web::{web, HttpRequest, HttpResponse, Scope}; use hyaenidae_accounts::User; use hyaenidae_profiles::store::{File, Profile}; use hyaenidae_toolkit::TextInput; use i18n_embed_fl::fl; use std::collections::HashMap; use uuid::Uuid; mod state; mod update; pub use state::{EditProfileState, ViewProfileState}; pub use update::HandleState; pub(super) fn scope() -> Scope { web::scope("/profiles") .service(web::resource("").route(web::get().to(to_current_profile))) .service(web::resource("/current").route(web::get().to(current_profile))) .service( web::resource("/delete") .route(web::get().to(delete_page)) .route(web::post().to(delete_profile)), ) .service( web::resource("/change") .route(web::get().to(change_profile_page)) .route(web::post().to(change_profile)), ) .service(update::create_scope()) .service(update::update_scope()) .route("/drafts", web::get().to(drafts_page)) .service( web::scope("/@{handle}@{domain}") .route("", web::get().to(handle_view)) .service( web::resource("/follow") .route(web::get().to(route_to_profile_page)) .route(web::post().to(follow)), ) .service( web::resource("/unfollow") .route(web::get().to(route_to_profile_page)) .route(web::post().to(unfollow)), ) .service( web::resource("/block") .route(web::get().to(route_to_profile_page)) .route(web::post().to(block)), ) .service( web::resource("/unblock") .route(web::get().to(route_to_profile_page)) .route(web::post().to(unblock)), ) .service( web::resource("/report") .route(web::get().to(report_page)) .route(web::post().to(report)), ) .route("/report-success", web::get().to(report_success_page)), ) .route("/id/{id}", web::get().to(id_view)) } fn to_profile_page(handle: &str, domain: &str) -> HttpResponse { crate::redirect(&format!("/profiles/@{}@{}", handle, domain)) } async fn route_to_profile_page(handle: web::Path<(String, String)>) -> HttpResponse { let (handle, domain) = handle.into_inner(); to_profile_page(&handle, &domain) } async fn follow( referer: Option, handle: web::Path<(String, String)>, self_profile: UserProfile, state: web::Data, ) -> Result { let self_profile = self_profile.into_inner(); use hyaenidae_profiles::apub::actions::CreateFollowRequest; let (handle, domain) = handle.into_inner(); let profile = profile_from_handle(handle.clone(), domain.clone(), &state).await?; let self_id = self_profile.id(); let profile_id = profile.id(); let blocks = state.profiles.store.view.blocks.clone(); let is_blocked = web::block(move || Ok(blocks.by_forward(self_id, profile_id)?.is_some())).await?; if is_blocked { return Ok(crate::to_404()); } let follow_requests = state.profiles.store.view.follow_requests.clone(); let follows = state.profiles.store.view.follows.clone(); let follow_exists = web::block(move || { let exists = follow_requests.by_forward(profile_id, self_id)?.is_some() || follows.by_forward(profile_id, self_id)?.is_some(); Ok(exists) }) .await?; if follow_exists { return Ok(to_profile_page(&handle, &domain)); } state .profiles .run(CreateFollowRequest::from_profiles( profile.id(), self_profile.id(), )) .await?; if let Some(referer) = referer { Ok(crate::redirect(&referer.0)) } else { Ok(to_profile_page(&handle, &domain)) } } async fn unfollow( referer: Option, handle: web::Path<(String, String)>, self_profile: UserProfile, state: web::Data, ) -> Result { let self_profile = self_profile.0; use hyaenidae_profiles::apub::actions::{UndoFollow, UndoFollowRequest}; let (handle, domain) = handle.into_inner(); let profile = profile_from_handle(handle.clone(), domain.clone(), &state).await?; let self_id = self_profile.id(); let profile_id = profile.id(); let blocks = state.profiles.store.view.blocks.clone(); let is_blocked = web::block(move || Ok(blocks.by_forward(self_id, profile_id)?.is_some())).await?; if is_blocked { return Ok(crate::to_404()); } let follow_requests = state.profiles.store.view.follow_requests.clone(); let follows = state.profiles.store.view.follows.clone(); let (follow_request, follow) = web::block(move || { let follow_request = follow_requests.by_forward(profile_id, self_id)?; let follow = follows.by_forward(profile_id, self_id)?; Ok((follow_request, follow)) }) .await?; if let Some(follow_request) = follow_request { state .profiles .run(UndoFollowRequest::from_id(follow_request)) .await?; } if let Some(follow) = follow { state.profiles.run(UndoFollow::from_id(follow)).await?; } if let Some(referer) = referer { Ok(crate::redirect(&referer.0)) } else { Ok(to_profile_page(&handle, &domain)) } } async fn block( handle: web::Path<(String, String)>, self_profile: UserProfile, state: web::Data, ) -> Result { let self_profile = self_profile.0; use hyaenidae_profiles::apub::actions::CreateBlock; let (handle, domain) = handle.into_inner(); let profile = profile_from_handle(handle.clone(), domain.clone(), &state).await?; let self_id = self_profile.id(); let profile_id = profile.id(); let blocks = state.profiles.store.view.blocks.clone(); let is_blocked = web::block(move || Ok(blocks.by_forward(self_id, profile_id)?.is_some())).await?; if is_blocked { return Ok(crate::to_404()); } let blocks = state.profiles.store.view.blocks.clone(); let block_exists = web::block(move || Ok(blocks.by_forward(profile_id, self_id)?.is_some())).await?; if block_exists { return Ok(to_profile_page(&handle, &domain)); } state .profiles .run(CreateBlock::from_profiles(profile.id(), self_profile.id())) .await?; Ok(to_profile_page(&handle, &domain)) } async fn unblock( handle: web::Path<(String, String)>, self_profile: UserProfile, state: web::Data, ) -> Result { let self_profile = self_profile.0; use hyaenidae_profiles::apub::actions::DeleteBlock; let (handle, domain) = handle.into_inner(); let profile = profile_from_handle(handle.clone(), domain.clone(), &state).await?; let self_id = self_profile.id(); let profile_id = profile.id(); let blocks = state.profiles.store.view.blocks.clone(); let block = web::block(move || Ok(blocks.by_forward(profile_id, self_id)?)).await?; if let Some(block) = block { state.profiles.run(DeleteBlock::from_id(block)).await?; } Ok(to_profile_page(&handle, &domain)) } pub struct ReportView { pub(crate) files: HashMap, pub(crate) profile: Profile, input_value: Option, input_error: Option, } impl ReportView { fn new(profile: Profile, files: HashMap) -> Self { ReportView { files, profile, input_value: None, input_error: None, } } pub(crate) fn input(&self, loader: &ActixLoader) -> TextInput { let input = TextInput::new("body") .title(&fl!(loader, "report-input")) .placeholder(&fl!(loader, "report-placeholder")) .textarea() .error_opt(self.input_error.clone()); if let Some(value) = &self.input_value { input.value(value) } else { input } } pub(crate) fn profile(&self) -> OwnedProfileView { OwnedProfileView { profile: self.profile.clone(), icon: self .profile .icon() .and_then(|i| self.files.get(&i)) .map(|i| i.clone()), banner: self .profile .banner() .and_then(|b| self.files.get(&b)) .map(|b| b.clone()), } } fn error_opt(mut self, error: Option) -> Self { self.input_error = error; self } fn value(mut self, value: &str) -> Self { self.input_value = Some(value.to_owned()); self } } async fn profile_from_id(id: Uuid, state: &State) -> Result { let store = state.profiles.clone(); let profile = web::block(move || store.store.profiles.by_id(id)?.req()).await?; Ok(profile) } async fn profile_from_handle( handle: String, domain: String, state: &State, ) -> Result { let store = state.profiles.clone(); let profile = web::block(move || { let id = store.store.profiles.by_handle(&handle, &domain)?.req()?; let profile = store.store.profiles.by_id(id)?.req()?; Ok(profile) }) .await?; Ok(profile) } async fn get_files_for_profile( profile: &Profile, mut files: HashMap, state: &State, ) -> Result, Error> { let mut file_ids = vec![]; file_ids.extend(profile.icon()); file_ids.extend(profile.banner()); let store = state.profiles.clone(); let files = web::block(move || { for file_id in file_ids { if !files.contains_key(&file_id) { let file = store.store.files.by_id(file_id)?.req()?; files.insert(file.id(), file); } } Ok(files) as Result<_, Error> }) .await?; Ok(files) } async fn report_page( loader: ActixLoader, self_profile: UserProfile, handle: web::Path<(String, String)>, nav_state: NavState, state: web::Data, ) -> Result { let self_profile = self_profile.0; let (handle, domain) = handle.into_inner(); let profile = profile_from_handle(handle, domain, &state).await?; let blocks = state.profiles.store.view.blocks.clone(); let self_id = self_profile.id(); let profile_id = profile.id(); let is_blocked = web::block(move || Ok(blocks.by_forward(self_id, profile_id)?.is_some())).await?; if is_blocked { return Ok(crate::to_404()); } let files = get_files_for_profile(&profile, HashMap::new(), &state).await?; let view = ReportView::new(profile, files); crate::rendered(HttpResponse::Ok(), |cursor| { crate::templates::profiles::report(cursor, &loader, &view, &nav_state) }) } const MAX_REPORT_LEN: usize = 1000; #[derive(Clone, Debug, serde::Deserialize)] struct ReportForm { body: String, } async fn report( loader: ActixLoader, self_profile: UserProfile, handle: web::Path<(String, String)>, form: web::Form, nav_state: NavState, state: web::Data, ) -> Result { let self_profile = self_profile.0; use hyaenidae_profiles::apub::actions::CreateReport; let (handle, domain) = handle.into_inner(); let profile = profile_from_handle(handle, domain, &state).await?; let form = form.into_inner(); let error = if form.body.len() > MAX_REPORT_LEN { format!("Must be shorter than {} characters", MAX_REPORT_LEN) } else if form.body.trim().is_empty() { format!("Must be present") } else { let res = state .profiles .run(CreateReport::from_profile( profile.id(), self_profile.id(), Some(form.body.clone()), )) .await; match res { Ok(_) => return Ok(to_report_success_page(profile.full_handle())), Err(e) => e.to_string(), } }; let files = get_files_for_profile(&profile, HashMap::new(), &state).await?; let view = ReportView::new(profile, files) .error_opt(Some(error)) .value(&form.body); crate::rendered(HttpResponse::BadRequest(), |cursor| { crate::templates::profiles::report(cursor, &loader, &view, &nav_state) }) } async fn report_success_page( loader: ActixLoader, self_profile: UserProfile, handle: web::Path<(String, String)>, nav_state: NavState, state: web::Data, ) -> Result { let self_profile = self_profile.0; let (handle, domain) = handle.into_inner(); let profile = profile_from_handle(handle, domain, &state).await?; let blocks = state.profiles.store.view.blocks.clone(); let self_id = self_profile.id(); let profile_id = profile.id(); let is_blocked = web::block(move || Ok(blocks.by_forward(self_id, profile_id)?.is_some())).await?; if is_blocked { return Ok(crate::to_404()); } let files = get_files_for_profile(&profile, HashMap::new(), &state).await?; let view = ReportView::new(profile, files); crate::rendered(HttpResponse::Ok(), |cursor| { crate::templates::profiles::report_success(cursor, &loader, &view, &nav_state) }) } fn to_report_success_page(full_handle: String) -> HttpResponse { redirect(&format!("/profiles/{}/report-success", full_handle)) } fn to_current_profile() -> HttpResponse { redirect("/profiles/current") } pub(super) fn to_change_profile_page() -> HttpResponse { redirect("/profiles/change") } fn redirect(path: &str) -> HttpResponse { HttpResponse::SeeOther().header("Location", path).finish() } #[derive(Clone, Debug, serde::Deserialize)] #[serde(untagged)] pub(crate) enum SubmissionPage { Max { max: Uuid }, Min { min: Uuid }, } impl From for crate::pagination::PageSource { fn from(s: SubmissionPage) -> Self { match s { SubmissionPage::Max { max } => Self::OlderThan(max), SubmissionPage::Min { min } => Self::NewerThan(min), } } } async fn id_view( loader: ActixLoader, req: HttpRequest, user: Option, self_profile: Option, id: web::Path, page: Option>, nav_state: NavState, state: web::Data, ) -> Result { let profile = profile_from_id(id.into_inner(), &state).await?; if profile.is_suspended() { return Ok(crate::to_404()); } do_public_view( loader, req.uri().path(), user, self_profile.map(|p| p.into_inner()), profile, page.map(|q| q.into_inner()), nav_state, &state, ) .await } async fn handle_view( loader: ActixLoader, req: HttpRequest, user: Option, self_profile: Option, handle: web::Path<(String, String)>, page: Option>, nav_state: NavState, state: web::Data, ) -> Result { let (handle, domain) = handle.into_inner(); let profile = profile_from_handle(handle, domain, &state).await?; if profile.is_suspended() { return Ok(crate::to_404()); } do_public_view( loader, req.uri().path(), user, self_profile.map(|p| p.into_inner()), profile, page.map(|q| q.into_inner()), nav_state, &state, ) .await } async fn drafts_page( loader: ActixLoader, req: HttpRequest, profile: UserProfile, page: Option>, nav_state: NavState, state: web::Data, ) -> Result { let profile = profile.into_inner(); let viewed_by = Some(profile.id()); let view = ViewProfileState::for_profile( req.path().to_owned(), profile, viewed_by, true, page.map(|p| p.into_inner().into()), &state, ) .await?; crate::rendered(HttpResponse::Ok(), |cursor| { crate::templates::profiles::drafts(cursor, &loader, &view, &nav_state) }) } async fn do_public_view( loader: ActixLoader, base_url: &str, user: Option, self_profile: Option, profile: Profile, page: Option, nav_state: NavState, state: &State, ) -> Result { if profile.login_required() && user.is_none() { return Ok(crate::to_404()); } let is_blocked = if let Some(self_profile) = &self_profile { let store = state.profiles.clone(); let self_id = self_profile.id(); let profile_id = profile.id(); web::block(move || { Ok(store .store .view .blocks .by_forward(self_id, profile_id)? .is_some()) }) .await? } else { false }; if is_blocked { return Ok(crate::to_404()); } let viewed_by = self_profile.as_ref().map(|p| p.id()); let view = ViewProfileState::for_profile( base_url.to_owned(), profile, viewed_by, false, page.map(|p| p.into()), state, ) .await?; crate::rendered(HttpResponse::Ok(), |cursor| { crate::templates::profiles::public(cursor, &loader, &view, &nav_state) }) } async fn delete_page( loader: ActixLoader, profile: UserProfile, nav_state: NavState, state: web::Data, ) -> Result { let profile = profile.0; let files = get_files_for_profile(&profile, HashMap::new(), &state).await?; let view = OwnedProfileView { banner: profile .banner() .and_then(|b| files.get(&b)) .map(|b| b.clone()), icon: profile .icon() .and_then(|i| files.get(&i)) .map(|i| i.clone()), profile, }; crate::rendered(HttpResponse::Ok(), |cursor| { crate::templates::profiles::delete(cursor, &loader, &view, &nav_state) }) } async fn delete_profile( profile: UserProfile, state: web::Data, ) -> Result { use hyaenidae_profiles::apub::actions::DeleteProfile; state .profiles .run(DeleteProfile::from_id(profile.0.id())) .await?; Ok(to_change_profile_page()) } async fn current_profile( loader: ActixLoader, profile: UserProfile, nav_state: NavState, state: web::Data, ) -> Result { let profile = profile.into_inner(); let profile_state = EditProfileState::for_profile(profile, &state).await?; crate::rendered(HttpResponse::Ok(), |cursor| { crate::templates::profiles::current(cursor, &loader, &profile_state, &nav_state) }) } #[derive(Clone, Debug, serde::Deserialize)] struct ChangeProfileForm { profile_id: Uuid, } async fn change_profile_page( loader: ActixLoader, user: User, nav_state: NavState, state: web::Data, ) -> Result { let store = state.profiles.clone(); let profiles = web::block(move || { let profiles = store .store .profiles .for_local(user.id()) .filter_map(|profile_id| OwnedProfileView::from_id(profile_id, &store.store).ok()) .filter(|view| !view.profile.is_suspended()) .collect::>(); Ok(profiles) as Result, Error> }) .await?; if profiles.len() == 0 { return Ok(update::to_create()); } crate::rendered(HttpResponse::Ok(), |cursor| { crate::templates::profiles::list(cursor, &loader, &profiles, &nav_state) }) } async fn change_profile( user: User, session: Session, form: web::Form, state: web::Data, ) -> Result { let profile = profile_from_id(form.profile_id, &state).await?; if profile.local_owner() != Some(user.id()) { return Ok(to_change_profile_page()); } ProfileData::set_data(form.profile_id, &session) .ok() .req()?; Ok(crate::to_home()) }