use crate::{ error::{Error, OptionExt, ResultExt, StateError}, State, }; use actix_session::Session; use actix_web::{dev::Payload, web, HttpRequest, HttpResponse, Scope}; use hyaenidae_accounts::{LogoutState, User}; use uuid::Uuid; mod middleware; pub(crate) use middleware::CurrentProfile; pub(super) fn scope() -> Scope { web::scope("/profiles") .service(web::resource("").route(web::get().to(profiles))) .service(web::resource("/current").route(web::get().to(current_profile))) .service( 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("/change") .route(web::get().to(change_profile_page)) .route(web::post().to(change_profile)), ) .service( 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 to_create() -> HttpResponse { redirect("/profiles/create/handle") } fn to_bio() -> HttpResponse { redirect("/profiles/create/bio") } fn to_icon() -> HttpResponse { redirect("/profiles/create/icon") } fn to_banner() -> HttpResponse { redirect("/profiles/create/banner") } fn to_require_login() -> HttpResponse { redirect("/profiles/create/require-login") } fn to_done() -> HttpResponse { redirect("/profiles/create/done") } fn to_current_profile() -> HttpResponse { redirect("/profiles/current") } fn to_change_profile_page() -> HttpResponse { redirect("/profiles/change") } fn redirect(path: &str) -> HttpResponse { HttpResponse::SeeOther().header("Location", path).finish() } const ACCEPT_TYPES: &str = "image/png,image/webp,image/jpeg,image/gif,.png,.webp,.jpg,.jpeg,.gif"; pub use state::ProfileState; mod state { use super::Profile; use super::ACCEPT_TYPES; use hyaenidae_toolkit::{FileInput, TextInput}; pub struct ProfileState { pub(crate) profile: Profile, pub(crate) display_name: TextInput, pub(crate) description: TextInput, pub(crate) icon: FileInput, pub(crate) icon_error: Option, pub(crate) banner: FileInput, pub(crate) banner_error: Option, pub(crate) login_required_error: Option, } impl ProfileState { pub(super) fn new(profile: Profile) -> Self { let mut display_name = TextInput::new("display_name"); display_name .title("Display Name") .placeholder("Display Name"); if let Some(text) = profile.display_name() { display_name.value(text); } let mut description = TextInput::new("description"); description .title("Description") .placeholder("Description") .textarea(); if let Some(text) = profile.description() { description.value(text); } let mut icon = FileInput::secondary("images[]", "Select Icon", "icon-input"); icon.accept(ACCEPT_TYPES).no_group(); let mut banner = FileInput::secondary("images[]", "Select Banner", "banner-input"); banner.accept(ACCEPT_TYPES).no_group(); ProfileState { profile, display_name, description, icon, icon_error: None, banner, banner_error: None, login_required_error: None, } } pub(super) fn display_name_error(&mut self, err: &str) -> &mut Self { self.display_name.error_opt(Some(err.to_owned())); self } pub(super) fn description_error(&mut self, err: &str) -> &mut Self { self.description.error_opt(Some(err.to_owned())); self } pub(super) fn icon_error(&mut self, err: &str) -> &mut Self { self.icon_error = Some(err.to_owned()); self } pub(super) fn banner_error(&mut self, err: &str) -> &mut Self { self.banner_error = Some(err.to_owned()); self } pub(super) fn login_required_error(&mut self, err: &str) -> &mut Self { self.login_required_error = Some(err.to_owned()); self } } } #[derive(Clone, Debug)] pub struct Profile { inner: hyaenidae_profiles::store::Profile, banner: Option, icon: Option, } impl Profile { fn from_id(profile_id: Uuid, state: &State) -> Result { let inner = state.profiles.store.profiles.by_id(profile_id)?.req()?; let banner = match inner.banner() { Some(banner_id) => { let file = state.profiles.store.files.by_id(banner_id)?.req()?; let hyaenidae_profiles::store::FileSource::PictRs(file) = file.source(); Some(file.key().to_owned()) } None => None, }; let icon = match inner.icon() { Some(icon_id) => { let file = state.profiles.store.files.by_id(icon_id)?.req()?; let hyaenidae_profiles::store::FileSource::PictRs(file) = file.source(); Some(file.key().to_owned()) } None => None, }; Ok(Profile { inner, banner, icon, }) } fn refresh(self, state: &State) -> Result { Self::from_id(self.inner.id(), state) } pub(crate) fn id(&self) -> Uuid { self.inner.id() } pub(crate) fn login_required(&self) -> bool { self.inner.login_required() } pub(crate) fn full_handle(&self) -> String { format!("{}@{}", self.inner.handle(), self.inner.domain()) } pub(crate) fn display_name(&self) -> Option<&str> { self.inner.display_name() } pub(crate) fn description(&self) -> Option<&str> { self.inner.description() } pub(crate) fn name(&self) -> &str { self.inner.display_name().unwrap_or(self.inner.handle()) } pub(crate) fn icon_key(&self) -> Option<&str> { self.icon.as_deref() } pub(crate) fn banner_key(&self) -> Option<&str> { self.banner.as_deref() } } async fn profiles() -> HttpResponse { to_current_profile() } async fn current_profile( profile: Profile, logout: LogoutState, state: web::Data, ) -> Result { do_profile(profile, logout).await.state(&state) } async fn do_profile(profile: Profile, logout: LogoutState) -> Result { let profile_state = ProfileState::new(profile); crate::rendered(HttpResponse::Ok(), |cursor| { crate::templates::profiles::current(cursor, &profile_state, logout) }) } async fn update_bio( form: web::Form, profile: Profile, logout: LogoutState, state: web::Data, ) -> Result { do_update_bio(form.into_inner(), profile, logout, &state) .await .state(&state) } async fn do_update_bio( form: BioForm, profile: Profile, logout: LogoutState, state: &State, ) -> Result { let display_name = form.display_name.clone(); let description = form.description.clone(); use hyaenidae_profiles::apub::actions::UpdateProfile; let res = state .profiles .run(&UpdateProfile::from_text( profile.id(), display_name, description, )) .await; let mut state = ProfileState::new(profile.refresh(state)?); match res { Ok(_) => return Ok(to_current_profile()), Err(e) => { state.display_name_error(&e.to_string()); } }; crate::rendered(HttpResponse::Ok(), |cursor| { crate::templates::profiles::current(cursor, &state, logout) }) } async fn update_icon( request: HttpRequest, payload: web::Payload, profile: Profile, logout: LogoutState, state: web::Data, ) -> Result { do_update_icon(request, payload.into_inner(), profile, logout, &state) .await .state(&state) } async fn do_update_icon( request: HttpRequest, payload: Payload, profile: Profile, logout: LogoutState, state: &State, ) -> Result { let res = state.profiles.upload_image(request, payload).await; let error = match res { Ok(file_ids) if file_ids.len() == 1 => { use hyaenidae_profiles::apub::actions::UpdateProfile; let res = state .profiles .run(&UpdateProfile::from_icon(profile.id(), file_ids[0])) .await; match res { Ok(_) => return Ok(to_current_profile()), Err(e) => e.to_string(), } } Ok(_) => "Incorrect number of files".to_owned(), Err(e) => e.to_string(), }; let mut state = ProfileState::new(profile.refresh(state)?); state.icon_error(&error); crate::rendered(HttpResponse::Ok(), |cursor| { crate::templates::profiles::current(cursor, &state, logout) }) } async fn update_banner( request: HttpRequest, payload: web::Payload, profile: Profile, logout: LogoutState, state: web::Data, ) -> Result { do_update_banner(request, payload.into_inner(), profile, logout, &state) .await .state(&state) } async fn do_update_banner( request: HttpRequest, payload: Payload, profile: Profile, logout: LogoutState, state: &State, ) -> Result { let res = state.profiles.upload_image(request, payload).await; let error = match res { Ok(file_ids) if file_ids.len() == 1 => { use hyaenidae_profiles::apub::actions::UpdateProfile; let res = state .profiles .run(&UpdateProfile::from_banner(profile.id(), file_ids[0])) .await; match res { Ok(_) => return Ok(to_current_profile()), Err(e) => e.to_string(), } } Ok(_) => "Incorrect number of files".to_owned(), Err(e) => e.to_string(), }; let mut state = ProfileState::new(profile.refresh(state)?); state.banner_error(&error); crate::rendered(HttpResponse::Ok(), |cursor| { crate::templates::profiles::current(cursor, &state, logout) }) } async fn update_require_login( form: web::Form, profile: Profile, logout: LogoutState, state: web::Data, ) -> Result { do_update_require_login(form.into_inner(), profile, logout, &state) .await .state(&state) } async fn do_update_require_login( form: RequireLoginForm, profile: Profile, logout: LogoutState, state: &State, ) -> Result { 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 mut state = ProfileState::new(profile.refresh(state)?); match res { Ok(_) => return Ok(to_current_profile()), Err(e) => { state.login_required_error(&e.to_string()); } }; crate::rendered(HttpResponse::Ok(), |cursor| { crate::templates::profiles::current(cursor, &state, logout) }) } #[derive(Clone, Debug, serde::Deserialize)] struct ChangeProfileForm { profile_id: Uuid, } async fn change_profile_page( user: User, logout: LogoutState, state: web::Data, ) -> Result { do_change_profile_page(user.id(), logout, &state) .await .state(&state) } async fn do_change_profile_page( user_id: Uuid, logout: LogoutState, state: &State, ) -> Result { let profiles = state .profiles .store .profiles .for_local(user_id) .filter_map(|profile_id| Profile::from_id(profile_id, state).ok()) .collect::>(); crate::rendered(HttpResponse::Ok(), |cursor| { crate::templates::profiles::list(cursor, &profiles, logout) }) } async fn change_profile( user: User, session: Session, form: web::Form, state: web::Data, ) -> Result { do_change_profile(user.id(), session, form.into_inner(), &state) .await .state(&state) } async fn do_change_profile( user_id: Uuid, session: Session, form: ChangeProfileForm, state: &State, ) -> Result { let profile = Profile::from_id(form.profile_id, state)?; if profile.inner.local_owner() != Some(user_id) { return Ok(to_change_profile_page()); } middleware::ProfileData::set_data(form.profile_id, &session) .ok() .req()?; Ok(crate::to_home()) } #[derive(Clone, Debug, serde::Deserialize)] pub struct HandleForm { pub(crate) handle: String, } async fn new_handle( _: User, logout: LogoutState, state: web::Data, ) -> Result { let mut handle_input = hyaenidae_toolkit::TextInput::new("handle"); handle_input.placeholder("Handle").title("Handle"); crate::rendered(HttpResponse::Ok(), |cursor| { crate::templates::profiles::create::handle(cursor, &handle_input, logout) }) .state(&state) } async fn create_handle( user: User, form: web::Form, session: Session, logout: LogoutState, state: web::Data, ) -> Result { do_create_handle(user.id(), form.into_inner(), session, logout, &state) .await .state(&state) } async fn do_create_handle( user_id: Uuid, form: HandleForm, session: Session, logout: LogoutState, state: &State, ) -> Result { let domain = state.domain.clone(); let handle = form.handle.clone(); let exists = state .profiles .store .profiles .by_handle(&handle, &domain)? .is_some(); let error = if !exists { use hyaenidae_profiles::apub::actions::CreateProfile; let res = state .profiles .run(&CreateProfile::from_local(user_id, handle, domain)) .await; match res { Ok(Some(id)) => { middleware::ProfileData::set_data(id, &session).ok().req()?; return Ok(to_bio()); } Ok(None) => None, Err(e) => Some(e.to_string()), } } else { Some("Handle already in use".to_owned()) }; let mut handle_input = hyaenidae_toolkit::TextInput::new("handle"); handle_input .placeholder("Handle") .title("Handle") .value(&form.handle) .error_opt(error); crate::rendered(HttpResponse::BadRequest(), |cursor| { crate::templates::profiles::create::handle(cursor, &handle_input, logout) }) } #[derive(Clone, Debug, serde::Deserialize)] pub struct BioForm { pub(crate) display_name: String, pub(crate) description: String, } async fn new_bio( profile: Profile, logout: LogoutState, state: web::Data, ) -> Result { do_new_bio(profile, logout).await.state(&state) } async fn do_new_bio(profile: Profile, logout: LogoutState) -> Result { let mut display_name = hyaenidae_toolkit::TextInput::new("display_name"); display_name .title("Display Name") .placeholder("Display Name"); if let Some(text) = profile.display_name() { display_name.value(text); } let mut description = hyaenidae_toolkit::TextInput::new("description"); description .title("Description") .placeholder("Description") .textarea(); if let Some(text) = profile.description() { description.value(text); } crate::rendered(HttpResponse::Ok(), |cursor| { crate::templates::profiles::create::bio( cursor, &display_name, &description, &profile, logout, ) }) } async fn create_bio( form: web::Form, profile: Profile, logout: LogoutState, state: web::Data, ) -> Result { do_create_bio(form.into_inner(), profile, logout, &state) .await .state(&state) } async fn do_create_bio( form: BioForm, profile: Profile, logout: LogoutState, state: &State, ) -> Result { let display_name = form.display_name.clone(); let description = form.description.clone(); use hyaenidae_profiles::apub::actions::UpdateProfile; let res = state .profiles .run(&UpdateProfile::from_text( profile.id(), display_name, description, )) .await; let error = match res { Ok(_) => return Ok(to_icon()), Err(e) => Some(e.to_string()), }; let mut display_name = hyaenidae_toolkit::TextInput::new("display_name"); display_name .title("Display Name") .placeholder("Display Name") .value(&form.display_name) .error_opt(error); let mut description = hyaenidae_toolkit::TextInput::new("description"); description .title("Description") .placeholder("Description") .value(&form.description); let profile = profile.refresh(state)?; crate::rendered(HttpResponse::BadRequest(), |cursor| { crate::templates::profiles::create::bio( cursor, &display_name, &description, &profile, logout, ) }) } async fn new_icon( profile: Profile, logout: LogoutState, state: web::Data, ) -> Result { do_new_icon(profile, logout).await.state(&state) } async fn do_new_icon(profile: Profile, logout: LogoutState) -> Result { let mut icon_input = hyaenidae_toolkit::FileInput::secondary("images[]", "Select Icon", "icon-input"); icon_input.accept(ACCEPT_TYPES); crate::rendered(HttpResponse::Ok(), |cursor| { crate::templates::profiles::create::icon(cursor, &icon_input, None, &profile, logout) }) } async fn create_icon( request: HttpRequest, payload: web::Payload, profile: Profile, logout: LogoutState, state: web::Data, ) -> Result { do_create_icon(request, payload.into_inner(), profile, logout, &state) .await .state(&state) } async fn do_create_icon( request: HttpRequest, payload: Payload, profile: Profile, logout: LogoutState, state: &State, ) -> Result { let res = state.profiles.upload_image(request, payload).await; let error = match res { Ok(file_ids) if file_ids.len() == 1 => { use hyaenidae_profiles::apub::actions::UpdateProfile; let res = state .profiles .run(&UpdateProfile::from_icon(profile.id(), file_ids[0])) .await; match res { Ok(_) => return Ok(to_banner()), Err(e) => Some(e.to_string()), } } Ok(_) => Some("Incorrect number of files".to_owned()), Err(e) => Some(e.to_string()), }; let mut icon_input = hyaenidae_toolkit::FileInput::secondary("images[]", "Select Icon", "icon-input"); icon_input.accept(ACCEPT_TYPES); let profile = profile.refresh(state)?; crate::rendered(HttpResponse::BadRequest(), |cursor| { crate::templates::profiles::create::icon(cursor, &icon_input, error, &profile, logout) }) } async fn new_banner( profile: Profile, logout: LogoutState, state: web::Data, ) -> Result { do_new_banner(profile, logout).await.state(&state) } async fn do_new_banner(profile: Profile, logout: LogoutState) -> Result { let mut banner_input = hyaenidae_toolkit::FileInput::secondary("images[]", "Select Banner", "banner-input"); banner_input.accept(ACCEPT_TYPES); crate::rendered(HttpResponse::Ok(), |cursor| { crate::templates::profiles::create::banner(cursor, &banner_input, None, &profile, logout) }) } async fn create_banner( request: HttpRequest, payload: web::Payload, profile: Profile, logout: LogoutState, state: web::Data, ) -> Result { do_create_banner(request, payload.into_inner(), profile, logout, &state) .await .state(&state) } async fn do_create_banner( request: HttpRequest, payload: Payload, profile: Profile, logout: LogoutState, state: &State, ) -> Result { let res = state.profiles.upload_image(request, payload).await; let error = match res { Ok(file_ids) if file_ids.len() == 1 => { use hyaenidae_profiles::apub::actions::UpdateProfile; let res = state .profiles .run(&UpdateProfile::from_banner(profile.id(), file_ids[0])) .await; match res { Ok(_) => return Ok(to_require_login()), Err(e) => Some(e.to_string()), } } Ok(_) => Some("Incorrect number of files".to_owned()), Err(e) => Some(e.to_string()), }; let mut banner_input = hyaenidae_toolkit::FileInput::secondary("images[]", "Select Banner", "banner-input"); banner_input.accept(ACCEPT_TYPES); let profile = profile.refresh(state)?; crate::rendered(HttpResponse::BadRequest(), |cursor| { crate::templates::profiles::create::banner(cursor, &banner_input, error, &profile, logout) }) } #[derive(Clone, Debug, serde::Deserialize)] struct RequireLoginForm { require_login: Option, } async fn new_require_login( profile: Profile, logout: LogoutState, state: web::Data, ) -> Result { do_new_require_login(profile, logout).await.state(&state) } async fn do_new_require_login( profile: Profile, logout: LogoutState, ) -> Result { crate::rendered(HttpResponse::Ok(), |cursor| { crate::templates::profiles::create::require_login( cursor, profile.inner.login_required(), None, &profile, logout, ) }) } async fn create_require_login( form: web::Form, profile: Profile, logout: LogoutState, state: web::Data, ) -> Result { do_create_require_login(form.into_inner(), profile, logout, &state) .await .state(&state) } async fn do_create_require_login( form: RequireLoginForm, profile: Profile, logout: LogoutState, state: &State, ) -> Result { 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) => Some(e.to_string()), }; let profile = profile.refresh(state)?; crate::rendered(HttpResponse::Ok(), |cursor| { crate::templates::profiles::create::require_login( cursor, form.require_login.is_some(), error, &profile, logout, ) }) } async fn done( profile: Profile, logout: LogoutState, state: web::Data, ) -> Result { do_done(profile, logout).await.state(&state) } async fn do_done(profile: Profile, logout: LogoutState) -> Result { crate::rendered(HttpResponse::Ok(), |cursor| { crate::templates::profiles::create::done(cursor, &profile, logout) }) }