use crate::{ error::{Error, OptionExt}, middleware::{ProfileData, UserProfile}, nav::NavState, profiles::{settings::Settings, state::EditProfileState, to_current_profile}, ActixLoader, State, }; use actix_session::Session; use actix_web::{client::Client, web, HttpRequest, HttpResponse, Scope}; use hyaenidae_accounts::User; use hyaenidae_profiles::store::Profile; use hyaenidae_toolkit::TextInput; use i18n_embed_fl::fl; use uuid::Uuid; pub(super) fn create_scope() -> Scope { web::scope("/create") .service(web::resource("").route(web::get().to(to_create))) .service( web::resource("/handle") .route(web::get().to(new_handle)) .route(web::post().to(create_handle)), ) .service( web::resource("/bio") .route(web::get().to(new_bio)) .route(web::post().to(create_bio)), ) .service( web::resource("/icon") .route(web::get().to(new_icon)) .route(web::post().to(create_icon)), ) .service( web::resource("/banner") .route(web::get().to(new_banner)) .route(web::post().to(create_banner)), ) .service( web::resource("/require-login") .route(web::get().to(new_require_login)) .route(web::post().to(create_require_login)), ) .service(web::resource("/done").route(web::get().to(done))) } pub(super) fn update_scope() -> Scope { web::scope("/update") .service( web::resource("/bio") .route(web::post().to(update_bio)) .route(web::get().to(to_current_profile)), ) .service( web::resource("/icon") .route(web::post().to(update_icon)) .route(web::get().to(to_current_profile)), ) .service( web::resource("/banner") .route(web::post().to(update_banner)) .route(web::get().to(to_current_profile)), ) .service( web::resource("/require-login") .route(web::post().to(update_require_login)) .route(web::get().to(to_current_profile)), ) .service( web::resource("/settings") .route(web::post().to(update_settings)) .route(web::get().to(to_current_profile)), ) } pub(super) fn to_create() -> HttpResponse { crate::redirect("/profiles/create/handle") } fn to_bio() -> HttpResponse { crate::redirect("/profiles/create/bio") } fn to_icon() -> HttpResponse { crate::redirect("/profiles/create/icon") } fn to_banner() -> HttpResponse { crate::redirect("/profiles/create/banner") } fn to_require_login() -> HttpResponse { crate::redirect("/profiles/create/require-login") } fn to_done() -> HttpResponse { crate::redirect("/profiles/create/done") } async fn refresh(profile: Profile, state: &State) -> Result { let id = profile.id(); super::profile_from_id(id, state).await } const MAX_HANDLE_LEN: usize = 20; const MAX_DISPLAY_NAME_LEN: usize = 20; const MAX_DESCRIPTION_LEN: usize = 500; fn validate_handle(handle: &str) -> Option { if handle.chars().any(|c| !c.is_alphanumeric()) { return Some("Must contain only alphanumeric characters".to_owned()); } if handle.len() > MAX_HANDLE_LEN { return Some(format!( "Must be shorter than {} characters", MAX_HANDLE_LEN )); } None } fn validate_display_name(display_name: &str) -> Option { if display_name.len() > MAX_DISPLAY_NAME_LEN { return Some(format!( "Must be shorter than {} characters", MAX_DISPLAY_NAME_LEN )); } if display_name.len() == 0 { return Some(format!("Must be present")); } None } fn validate_description(description: &str) -> Option { if description.len() > MAX_DESCRIPTION_LEN { return Some(format!( "Must be shorter than {} characters", MAX_DESCRIPTION_LEN )); } None } async fn update_bio( loader: ActixLoader, form: web::Form, profile: UserProfile, nav_state: NavState, state: web::Data, ) -> Result { use hyaenidae_profiles::apub::actions::UpdateProfile; let profile = profile.into_inner(); let display_name = form.display_name.clone(); let description = form.description.clone(); let display_name_error = validate_display_name(&display_name); let description_error = validate_description(&description); let res = if display_name_error.is_none() && description_error.is_none() { let res = state .profiles .run(UpdateProfile::from_text( profile.id(), display_name.clone(), description.clone(), )) .await; Some(res) } else { None }; let profile = refresh(profile, &state).await?; let mut profile_state = EditProfileState::for_profile(profile, &state).await?; profile_state .display_name_value(display_name) .description_value(description); match res { Some(Ok(_)) => return Ok(to_current_profile()), Some(Err(e)) => { profile_state.display_name_error(e.to_string()); } None => { if let Some(e) = display_name_error { profile_state.display_name_error(e); } if let Some(e) = description_error { profile_state.description_error(e); } } }; crate::rendered(HttpResponse::Ok(), |cursor| { crate::templates::profiles::current(cursor, &loader, &profile_state, &nav_state) }) } async fn do_update_image( request: HttpRequest, payload: web::Payload, profile: UserProfile, client: web::Data, state: &State, f: F, ) -> Result, Error> where F: FnOnce(Uuid) -> A, A: hyaenidae_profiles::Action + Send + 'static, { let profile = profile.into_inner(); let res = state .profiles .upload_image(request, payload.into_inner(), &client) .await; let error = match res { Ok(file_ids) if file_ids.len() == 1 => { let res = state.profiles.run((f)(file_ids[0])).await; match res { Ok(_) => return Ok(Ok(())), Err(e) => e.to_string(), } } Ok(_) => "Incorrect number of files".to_owned(), Err(e) => e.to_string(), }; let profile = refresh(profile, state).await?; let profile_state = EditProfileState::for_profile(profile, state).await?; Ok(Err((profile_state, error))) } async fn update_icon( loader: ActixLoader, request: HttpRequest, payload: web::Payload, profile: UserProfile, nav_state: NavState, client: web::Data, state: web::Data, ) -> Result { use hyaenidae_profiles::apub::actions::UpdateProfile; let profile_id = profile.0.id(); let cb = move |file_id| UpdateProfile::from_icon(profile_id, file_id); let (mut profile_state, error) = match do_update_image(request, payload, profile, client, &state, cb).await? { Ok(_) => return Ok(to_current_profile()), Err(profile_state) => profile_state, }; profile_state.icon_error(&error); crate::rendered(HttpResponse::Ok(), |cursor| { crate::templates::profiles::current(cursor, &loader, &profile_state, &nav_state) }) } async fn update_banner( loader: ActixLoader, request: HttpRequest, payload: web::Payload, profile: UserProfile, nav_state: NavState, client: web::Data, state: web::Data, ) -> Result { use hyaenidae_profiles::apub::actions::UpdateProfile; let profile_id = profile.0.id(); let cb = move |file_id| UpdateProfile::from_banner(profile_id, file_id); let (mut profile_state, error) = match do_update_image(request, payload, profile, client, &state, cb).await? { Ok(()) => return Ok(to_current_profile()), Err(profile_state) => profile_state, }; profile_state.banner_error(&error); crate::rendered(HttpResponse::Ok(), |cursor| { crate::templates::profiles::current(cursor, &loader, &profile_state, &nav_state) }) } async fn update_require_login( loader: ActixLoader, form: web::Form, profile: UserProfile, nav_state: NavState, state: web::Data, ) -> Result { let profile = profile.into_inner(); let login_required = form.require_login.is_some(); use hyaenidae_profiles::apub::actions::UpdateProfile; let res = state .profiles .run(UpdateProfile::from_login_required( profile.id(), login_required, )) .await; let profile = refresh(profile, &state).await?; let mut profile_state = EditProfileState::for_profile(profile, &state).await?; profile_state.login_required_value(login_required); match res { Ok(_) => return Ok(to_current_profile()), Err(e) => { profile_state.login_required_error(&e.to_string()); } }; crate::rendered(HttpResponse::Ok(), |cursor| { crate::templates::profiles::current(cursor, &loader, &profile_state, &nav_state) }) } #[derive(serde::Deserialize)] struct SettingsForm { sensitive: Option, dark: Option, } async fn update_settings( loader: ActixLoader, form: web::Form, profile: UserProfile, nav_state: NavState, state: web::Data, ) -> Result { let profile = profile.0; let settings = Settings { sensitive: form.sensitive.is_some(), dark: form.dark.is_some(), }; let error = match state.settings.update(profile.id(), settings).await { Ok(_) => return Ok(to_current_profile()), Err(e) => e.to_string(), }; let mut profile_state = EditProfileState::for_profile(profile, &state).await?; profile_state.settings_error(error); crate::rendered(HttpResponse::InternalServerError(), |cursor| { crate::templates::profiles::current(cursor, &loader, &profile_state, &nav_state) }) } #[derive(Clone, Debug, serde::Deserialize)] pub struct HandleForm { pub(crate) handle: String, } #[derive(Default)] pub struct HandleState { handle_value: Option, handle_error: Option, } impl HandleState { pub(crate) fn handle(&self, loader: &ActixLoader) -> TextInput { let input = TextInput::new("handle") .title(&fl!(loader, "create-handle-input")) .placeholder(&fl!(loader, "create-handle-placeholder")) .error_opt(self.handle_error.clone()); if let Some(text) = &self.handle_value { input.value(text) } else { input } } fn new() -> Self { Self::default() } fn handle_value(&mut self, text: String) -> &mut Self { self.handle_value = Some(text); self } fn handle_error(&mut self, text: String) -> &mut Self { self.handle_error = Some(text); self } } async fn new_handle( loader: ActixLoader, _: User, nav_state: NavState, ) -> Result { let state = HandleState::new(); crate::rendered(HttpResponse::Ok(), |cursor| { crate::templates::profiles::create::handle(cursor, &loader, &state, &nav_state) }) } async fn create_handle( loader: ActixLoader, user: User, form: web::Form, session: Session, nav_state: NavState, state: web::Data, ) -> Result { use hyaenidae_profiles::apub::actions::CreateProfile; let form = form.into_inner(); let domain = state.domain.clone(); let handle = form.handle.clone(); let error = match validate_handle(&handle) { Some(error) => error, None => { let fallible = || async move { let exists = state .profiles .store .profiles .by_handle(&handle, &domain)? .is_some(); if !exists { let id = state .profiles .run(CreateProfile::from_local(user.id(), handle, domain)) .await? .ok_or_else(|| anyhow::anyhow!("Failed to create profile"))?; Ok(id) } else { Err(anyhow::anyhow!("Handle already in use")) } }; match (fallible)().await { Ok(id) => { ProfileData::set_data(id, &session).ok().req()?; return Ok(to_bio()); } Err(e) => e.to_string(), } } }; let mut state = HandleState::new(); state.handle_error(error).handle_value(form.handle); crate::rendered(HttpResponse::BadRequest(), |cursor| { crate::templates::profiles::create::handle(cursor, &loader, &state, &nav_state) }) } #[derive(Clone, Debug, serde::Deserialize)] pub struct BioForm { pub(crate) display_name: String, pub(crate) description: String, } async fn new_bio( 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::create::bio(cursor, &loader, &profile_state, &nav_state) }) } async fn create_bio( loader: ActixLoader, form: web::Form, profile: UserProfile, nav_state: NavState, state: web::Data, ) -> Result { use hyaenidae_profiles::apub::actions::UpdateProfile; let profile = profile.into_inner(); let display_name = form.display_name.clone(); let description = form.description.clone(); let display_name_error = validate_display_name(&display_name); let description_error = validate_description(&description); let res = if display_name_error.is_none() && description_error.is_none() { let res = state .profiles .run(UpdateProfile::from_text( profile.id(), display_name.clone(), description.clone(), )) .await; Some(res) } else { None }; let profile = refresh(profile, &state).await?; let mut profile_state = EditProfileState::for_profile(profile, &state).await?; profile_state .display_name_value(display_name) .description_value(description); match res { Some(Ok(_)) => return Ok(to_icon()), Some(Err(e)) => { profile_state.display_name_error(e.to_string()); } None => { if let Some(e) = display_name_error { profile_state.display_name_error(e); } if let Some(e) = description_error { profile_state.description_error(e); } } }; crate::rendered(HttpResponse::BadRequest(), |cursor| { crate::templates::profiles::create::bio(cursor, &loader, &profile_state, &nav_state) }) } async fn new_icon( 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::create::icon(cursor, &loader, &profile_state, &nav_state) }) } async fn create_icon( loader: ActixLoader, request: HttpRequest, payload: web::Payload, profile: UserProfile, nav_state: NavState, client: web::Data, state: web::Data, ) -> Result { use hyaenidae_profiles::apub::actions::UpdateProfile; let profile_id = profile.0.id(); let cb = move |file_id| UpdateProfile::from_icon(profile_id, file_id); let (mut profile_state, error) = match do_update_image(request, payload, profile, client, &state, cb).await? { Ok(_) => return Ok(to_banner()), Err(profile_state) => profile_state, }; profile_state.icon_error(&error); crate::rendered(HttpResponse::BadRequest(), |cursor| { crate::templates::profiles::create::icon(cursor, &loader, &profile_state, &nav_state) }) } async fn new_banner( 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::create::banner(cursor, &loader, &profile_state, &nav_state) }) } async fn create_banner( loader: ActixLoader, request: HttpRequest, payload: web::Payload, profile: UserProfile, nav_state: NavState, client: web::Data, state: web::Data, ) -> Result { use hyaenidae_profiles::apub::actions::UpdateProfile; let profile_id = profile.0.id(); let cb = move |file_id| UpdateProfile::from_banner(profile_id, file_id); let (mut profile_state, error) = match do_update_image(request, payload, profile, client, &state, cb).await? { Ok(()) => return Ok(to_require_login()), Err(profile_state) => profile_state, }; profile_state.banner_error(&error); crate::rendered(HttpResponse::BadRequest(), |cursor| { crate::templates::profiles::create::banner(cursor, &loader, &profile_state, &nav_state) }) } #[derive(Clone, Debug, serde::Deserialize)] struct RequireLoginForm { require_login: Option, } async fn new_require_login( 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::create::require_login( cursor, &loader, &profile_state, &nav_state, ) }) } async fn create_require_login( loader: ActixLoader, form: web::Form, profile: UserProfile, nav_state: NavState, state: web::Data, ) -> Result { let profile = profile.into_inner(); use hyaenidae_profiles::apub::actions::UpdateProfile; let res = state .profiles .run(UpdateProfile::from_login_required( profile.id(), form.require_login.is_some(), )) .await; let error = match res { Ok(_) => return Ok(to_done()), Err(e) => e.to_string(), }; let profile = refresh(profile, &state).await?; let mut profile_state = EditProfileState::for_profile(profile, &state).await?; profile_state .login_required_error(&error) .login_required_value(form.require_login.is_some()); crate::rendered(HttpResponse::Ok(), |cursor| { crate::templates::profiles::create::require_login( cursor, &loader, &profile_state, &nav_state, ) }) } async fn done( loader: ActixLoader, profile: UserProfile, nav_state: NavState, state: web::Data, ) -> Result { let profile = profile.into_inner(); let state = EditProfileState::for_profile(profile, &state).await?; crate::rendered(HttpResponse::Ok(), |cursor| { crate::templates::profiles::create::done(cursor, &loader, &state, &nav_state) }) }