hyaenidae/server/src/profiles.rs

657 lines
18 KiB
Rust

use crate::{
error::{Error, OptionExt, ResultExt, StateError},
State,
};
use actix_session::Session;
use actix_web::{dev::Payload, web, HttpRequest, HttpResponse, Scope};
use hyaenidae_accounts::Authenticated;
use uuid::Uuid;
pub(super) fn scope() -> Scope {
web::scope("/profiles")
.service(
web::resource("")
.route(web::get().to(profile))
.route(web::post().to(update_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("/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))),
)
}
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 redirect(path: &str) -> HttpResponse {
HttpResponse::SeeOther().header("Location", path).finish()
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
struct CurrentProfile {
id: Uuid,
}
pub struct Profile {
inner: hyaenidae_profiles::store::Profile,
banner: Option<String>,
icon: Option<String>,
}
impl Profile {
fn from_id(profile_id: Uuid, state: &State) -> Result<Self, Error> {
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,
})
}
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 profile(
_: Authenticated,
session: Session,
state: web::Data<State>,
) -> Result<HttpResponse, StateError> {
do_profile(session, &state).await.state(&state)
}
async fn do_profile(session: Session, state: &State) -> Result<HttpResponse, Error> {
let profile_id = match session
.get::<CurrentProfile>("current-profile")
.ok()
.req()?
{
Some(id) => id.id,
None => return Ok(to_create()),
};
let profile = match state.profiles.store.profiles.by_id(profile_id)? {
Some(profile) => profile,
None => return Ok(to_create()),
};
Ok(HttpResponse::Ok().finish())
}
async fn update_profile(_: Authenticated) -> &'static str {
"Hewwo, Mr Obama"
}
async fn change_profile(_: Authenticated) -> &'static str {
"Hewwo, Mr Obama"
}
async fn change_profile_page(_: Authenticated) -> &'static str {
"Hewwo, Mr Obama"
}
#[derive(Clone, Debug, serde::Deserialize)]
pub struct HandleForm {
pub(crate) handle: String,
}
async fn new_handle(_: Authenticated, state: web::Data<State>) -> Result<HttpResponse, StateError> {
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)
})
.state(&state)
}
async fn create_handle(
auth: Authenticated,
form: web::Form<HandleForm>,
session: Session,
state: web::Data<State>,
) -> Result<HttpResponse, StateError> {
do_create_handle(auth, form.into_inner(), session, &state)
.await
.state(&state)
}
async fn do_create_handle(
auth: Authenticated,
form: HandleForm,
session: Session,
state: &State,
) -> Result<HttpResponse, Error> {
let user_id = auth.user().id();
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)) => {
session
.set("current-profile", CurrentProfile { id })
.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)
})
}
#[derive(Clone, Debug, serde::Deserialize)]
pub struct BioForm {
pub(crate) display_name: String,
pub(crate) description: String,
}
async fn new_bio(
_: Authenticated,
session: Session,
state: web::Data<State>,
) -> Result<HttpResponse, StateError> {
do_new_bio(session, &state).await.state(&state)
}
async fn do_new_bio(session: Session, state: &State) -> Result<HttpResponse, Error> {
let profile_id = match session
.get::<CurrentProfile>("current-profile")
.ok()
.req()?
{
Some(id) => id.id,
None => return Ok(to_create()),
};
let profile = Profile::from_id(profile_id, state)?;
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)
})
}
async fn create_bio(
_: Authenticated,
form: web::Form<BioForm>,
session: Session,
state: web::Data<State>,
) -> Result<HttpResponse, StateError> {
do_create_bio(form.into_inner(), session, &state)
.await
.state(&state)
}
async fn do_create_bio(
form: BioForm,
session: Session,
state: &State,
) -> Result<HttpResponse, Error> {
let profile_id = match session
.get::<CurrentProfile>("current-profile")
.ok()
.req()?
{
Some(id) => id.id,
None => return Ok(to_create()),
};
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::from_id(profile_id, state)?;
crate::rendered(HttpResponse::BadRequest(), |cursor| {
crate::templates::profiles::create::bio(cursor, &display_name, &description, &profile)
})
}
async fn new_icon(
_: Authenticated,
session: Session,
state: web::Data<State>,
) -> Result<HttpResponse, StateError> {
do_new_icon(session, &state).await.state(&state)
}
async fn do_new_icon(session: Session, state: &State) -> Result<HttpResponse, Error> {
let profile_id = match session
.get::<CurrentProfile>("current-profile")
.ok()
.req()?
{
Some(id) => id.id,
None => return Ok(to_create()),
};
let mut icon_input =
hyaenidae_toolkit::FileInput::secondary("images[]", "Select Icon", "icon-input");
icon_input.accept("image/png,image/webp,image/jpeg,image/gif,.png,.webp,.jpg,.jpeg,.gif");
let profile = Profile::from_id(profile_id, state)?;
crate::rendered(HttpResponse::Ok(), |cursor| {
crate::templates::profiles::create::icon(cursor, &icon_input, None, &profile)
})
}
async fn create_icon(
_: Authenticated,
request: HttpRequest,
payload: web::Payload,
session: Session,
state: web::Data<State>,
) -> Result<HttpResponse, StateError> {
do_create_icon(request, payload.into_inner(), session, &state)
.await
.state(&state)
}
async fn do_create_icon(
request: HttpRequest,
payload: Payload,
session: Session,
state: &State,
) -> Result<HttpResponse, Error> {
let profile_id = match session
.get::<CurrentProfile>("current-profile")
.ok()
.req()?
{
Some(id) => id.id,
None => return Ok(to_create()),
};
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("image/png,image/webp,image/jpeg,image/gif,.png,.webp,.jpg,.jpeg,.gif");
let profile = Profile::from_id(profile_id, state)?;
crate::rendered(HttpResponse::BadRequest(), |cursor| {
crate::templates::profiles::create::icon(cursor, &icon_input, error, &profile)
})
}
async fn new_banner(
_: Authenticated,
session: Session,
state: web::Data<State>,
) -> Result<HttpResponse, StateError> {
do_new_banner(session, &state).await.state(&state)
}
async fn do_new_banner(session: Session, state: &State) -> Result<HttpResponse, Error> {
let profile_id = match session
.get::<CurrentProfile>("current-profile")
.ok()
.req()?
{
Some(id) => id.id,
None => return Ok(to_create()),
};
let mut banner_input =
hyaenidae_toolkit::FileInput::secondary("images[]", "Select Banner", "banner-input");
banner_input.accept("image/png,image/webp,image/jpeg,image/gif,.png,.webp,.jpg,.jpeg,.gif");
let profile = Profile::from_id(profile_id, state)?;
crate::rendered(HttpResponse::Ok(), |cursor| {
crate::templates::profiles::create::banner(cursor, &banner_input, None, &profile)
})
}
async fn create_banner(
_: Authenticated,
request: HttpRequest,
payload: web::Payload,
session: Session,
state: web::Data<State>,
) -> Result<HttpResponse, StateError> {
do_create_banner(request, payload.into_inner(), session, &state)
.await
.state(&state)
}
async fn do_create_banner(
request: HttpRequest,
payload: Payload,
session: Session,
state: &State,
) -> Result<HttpResponse, Error> {
let profile_id = match session
.get::<CurrentProfile>("current-profile")
.ok()
.req()?
{
Some(id) => id.id,
None => return Ok(to_create()),
};
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("image/png,image/webp,image/jpeg,image/gif,.png,.webp,.jpg,.jpeg,.gif");
let profile = Profile::from_id(profile_id, state)?;
crate::rendered(HttpResponse::BadRequest(), |cursor| {
crate::templates::profiles::create::banner(cursor, &banner_input, error, &profile)
})
}
#[derive(Clone, Debug, serde::Deserialize)]
struct RequireLoginForm {
require_login: Option<String>,
}
async fn new_require_login(
_: Authenticated,
session: Session,
state: web::Data<State>,
) -> Result<HttpResponse, StateError> {
do_new_require_login(session, &state).await.state(&state)
}
async fn do_new_require_login(session: Session, state: &State) -> Result<HttpResponse, Error> {
let profile_id = match session
.get::<CurrentProfile>("current-profile")
.ok()
.req()?
{
Some(id) => id.id,
None => return Ok(to_create()),
};
let profile = Profile::from_id(profile_id, state)?;
crate::rendered(HttpResponse::Ok(), |cursor| {
crate::templates::profiles::create::require_login(
cursor,
profile.inner.login_required(),
None,
&profile,
)
})
}
async fn create_require_login(
_: Authenticated,
form: web::Form<RequireLoginForm>,
session: Session,
state: web::Data<State>,
) -> Result<HttpResponse, StateError> {
do_create_require_login(form.into_inner(), session, &state)
.await
.state(&state)
}
async fn do_create_require_login(
form: RequireLoginForm,
session: Session,
state: &State,
) -> Result<HttpResponse, Error> {
let profile_id = match session
.get::<CurrentProfile>("current-profile")
.ok()
.req()?
{
Some(id) => id.id,
None => return Ok(to_create()),
};
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::from_id(profile_id, state)?;
crate::rendered(HttpResponse::Ok(), |cursor| {
crate::templates::profiles::create::require_login(
cursor,
form.require_login.is_some(),
error,
&profile,
)
})
}
async fn done(
_: Authenticated,
session: Session,
state: web::Data<State>,
) -> Result<HttpResponse, StateError> {
do_done(session, &state).await.state(&state)
}
async fn do_done(session: Session, state: &State) -> Result<HttpResponse, Error> {
let profile_id = match session
.get::<CurrentProfile>("current-profile")
.ok()
.req()?
{
Some(id) => id.id,
None => return Ok(to_create()),
};
let profile = Profile::from_id(profile_id, state)?;
crate::rendered(HttpResponse::Ok(), |cursor| {
crate::templates::profiles::create::done(cursor, &profile)
})
}