1965 lines
57 KiB
Rust
1965 lines
57 KiB
Rust
use crate::{
|
|
error::{Error, OptionExt},
|
|
nav::NavState,
|
|
State,
|
|
};
|
|
use actix_session::Session;
|
|
use actix_web::{web, HttpRequest, HttpResponse, Scope};
|
|
use hyaenidae_accounts::User;
|
|
use hyaenidae_profiles::store::{File, FileStore, ProfileStore, Submission};
|
|
use hyaenidae_toolkit::{Button, TextInput};
|
|
use std::collections::HashMap;
|
|
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::resource("/delete")
|
|
.route(web::get().to(delete_page))
|
|
.route(web::post().to(delete_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))),
|
|
)
|
|
.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(
|
|
handle: web::Path<(String, String)>,
|
|
self_profile: Profile,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
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?;
|
|
|
|
Ok(to_profile_page(&handle, &domain))
|
|
}
|
|
|
|
async fn unfollow(
|
|
handle: web::Path<(String, String)>,
|
|
self_profile: Profile,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
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?;
|
|
}
|
|
|
|
Ok(to_profile_page(&handle, &domain))
|
|
}
|
|
|
|
async fn block(
|
|
handle: web::Path<(String, String)>,
|
|
self_profile: Profile,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
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: Profile,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
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) profile: Profile,
|
|
pub(crate) input: TextInput,
|
|
}
|
|
|
|
impl ReportView {
|
|
fn new(profile: Profile, dark: bool) -> Self {
|
|
let mut input = TextInput::new("body");
|
|
input
|
|
.title("Report")
|
|
.placeholder("Type your report info here")
|
|
.textarea()
|
|
.dark(dark);
|
|
|
|
ReportView { profile, input }
|
|
}
|
|
}
|
|
|
|
async fn profile_from_id(id: Uuid, state: &State) -> Result<Profile, Error> {
|
|
let profile_store = state.profiles.store.profiles.clone();
|
|
let file_store = state.profiles.store.files.clone();
|
|
|
|
let profile = web::block(move || {
|
|
let profile = Profile::from_stores(id, &profile_store, &file_store)?;
|
|
Ok(profile)
|
|
})
|
|
.await?;
|
|
|
|
Ok(profile)
|
|
}
|
|
|
|
async fn profile_from_handle(
|
|
handle: String,
|
|
domain: String,
|
|
state: &State,
|
|
) -> Result<Profile, Error> {
|
|
let profile_store = state.profiles.store.profiles.clone();
|
|
let file_store = state.profiles.store.files.clone();
|
|
|
|
let profile = web::block(move || {
|
|
let id = profile_store.by_handle(&handle, &domain)?.req()?;
|
|
let profile = Profile::from_stores(id, &profile_store, &file_store)?;
|
|
Ok(profile)
|
|
})
|
|
.await?;
|
|
|
|
Ok(profile)
|
|
}
|
|
|
|
async fn report_page(
|
|
self_profile: Profile,
|
|
handle: web::Path<(String, String)>,
|
|
nav_state: NavState,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
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 view = ReportView::new(profile, nav_state.dark());
|
|
|
|
crate::rendered(HttpResponse::Ok(), |cursor| {
|
|
crate::templates::profiles::report(cursor, &view, &nav_state)
|
|
})
|
|
}
|
|
|
|
const MAX_REPORT_LEN: usize = 1000;
|
|
|
|
#[derive(Clone, Debug, serde::Deserialize)]
|
|
struct ReportForm {
|
|
body: String,
|
|
}
|
|
|
|
async fn report(
|
|
self_profile: Profile,
|
|
handle: web::Path<(String, String)>,
|
|
form: web::Form<ReportForm>,
|
|
nav_state: NavState,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
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 mut view = ReportView::new(profile, nav_state.dark());
|
|
view.input.error_opt(Some(error)).value(&form.body);
|
|
|
|
crate::rendered(HttpResponse::BadRequest(), |cursor| {
|
|
crate::templates::profiles::report(cursor, &view, &nav_state)
|
|
})
|
|
}
|
|
|
|
async fn report_success_page(
|
|
self_profile: Profile,
|
|
handle: web::Path<(String, String)>,
|
|
nav_state: NavState,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
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 view = ReportView::new(profile, nav_state.dark());
|
|
|
|
crate::rendered(HttpResponse::Ok(), |cursor| {
|
|
crate::templates::profiles::report_success(cursor, &view, &nav_state)
|
|
})
|
|
}
|
|
|
|
fn to_report_success_page(full_handle: String) -> HttpResponse {
|
|
redirect(&format!("/profiles/{}/report-success", full_handle))
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
pub(super) 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<String>,
|
|
pub(crate) banner: FileInput,
|
|
pub(crate) banner_error: Option<String>,
|
|
pub(crate) login_required_error: Option<String>,
|
|
}
|
|
|
|
impl ProfileState {
|
|
pub(super) fn new(profile: Profile, dark: bool) -> Self {
|
|
let mut display_name = TextInput::new("display_name");
|
|
display_name
|
|
.title("Display Name")
|
|
.placeholder("Display Name")
|
|
.dark(dark);
|
|
if let Some(text) = profile.display_name() {
|
|
display_name.value(text);
|
|
}
|
|
|
|
let mut description = TextInput::new("description");
|
|
description
|
|
.title("Description")
|
|
.placeholder("Description")
|
|
.textarea()
|
|
.dark(dark);
|
|
if let Some(text) = profile.description() {
|
|
description.value(text);
|
|
}
|
|
let mut icon = FileInput::secondary("images[]", "Select Icon");
|
|
icon.accept(ACCEPT_TYPES).no_group().dark(dark);
|
|
let mut banner = FileInput::secondary("images[]", "Select Banner");
|
|
banner.accept(ACCEPT_TYPES).no_group().dark(dark);
|
|
|
|
ProfileState {
|
|
profile,
|
|
display_name,
|
|
description,
|
|
icon,
|
|
icon_error: None,
|
|
banner,
|
|
banner_error: None,
|
|
login_required_error: None,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn view_path(&self) -> String {
|
|
format!(
|
|
"/profiles/@{}@{}",
|
|
self.profile.inner.handle(),
|
|
self.profile.inner.domain()
|
|
)
|
|
}
|
|
|
|
pub(super) fn display_name_error(&mut self, err: String) -> &mut Self {
|
|
self.display_name.error_opt(Some(err));
|
|
self
|
|
}
|
|
|
|
pub(super) fn description_error(&mut self, err: String) -> &mut Self {
|
|
self.description.error_opt(Some(err));
|
|
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<String>,
|
|
icon: Option<String>,
|
|
}
|
|
|
|
impl Profile {
|
|
pub(crate) fn from_stores(
|
|
profile_id: Uuid,
|
|
profiles: &ProfileStore,
|
|
files: &FileStore,
|
|
) -> Result<Self, Error> {
|
|
let inner = profiles.by_id(profile_id)?.req()?;
|
|
let banner = match inner.banner() {
|
|
Some(banner_id) => {
|
|
let file = files.by_id(banner_id)?.req()?;
|
|
if let Some(key) = file.pictrs_key() {
|
|
Some(key.to_owned())
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
None => None,
|
|
};
|
|
let icon = match inner.icon() {
|
|
Some(icon_id) => {
|
|
let file = files.by_id(icon_id)?.req()?;
|
|
if let Some(key) = file.pictrs_key() {
|
|
Some(key.to_owned())
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
None => None,
|
|
};
|
|
|
|
Ok(Profile {
|
|
inner,
|
|
banner,
|
|
icon,
|
|
})
|
|
}
|
|
|
|
pub(crate) fn from_id(profile_id: Uuid, state: &State) -> Result<Self, Error> {
|
|
Self::from_stores(
|
|
profile_id,
|
|
&state.profiles.store.profiles,
|
|
&state.profiles.store.files,
|
|
)
|
|
}
|
|
|
|
pub(crate) fn view_path(&self) -> String {
|
|
format!("/profiles/{}", self.full_handle())
|
|
}
|
|
|
|
pub(crate) fn report_path(&self) -> String {
|
|
format!("/profiles/{}/report", self.full_handle())
|
|
}
|
|
|
|
pub(crate) fn follow_path(&self) -> String {
|
|
format!("/profiles/{}/follow", self.full_handle())
|
|
}
|
|
|
|
pub(crate) fn unfollow_path(&self) -> String {
|
|
format!("/profiles/{}/unfollow", self.full_handle())
|
|
}
|
|
|
|
pub(crate) fn block_path(&self) -> String {
|
|
format!("/profiles/{}/block", self.full_handle())
|
|
}
|
|
|
|
pub(crate) fn unblock_path(&self) -> String {
|
|
format!("/profiles/{}/unblock", self.full_handle())
|
|
}
|
|
|
|
fn refresh(self, state: &State) -> Result<Self, Error> {
|
|
Self::from_id(self.inner.id(), state)
|
|
}
|
|
|
|
fn is_suspended(&self) -> bool {
|
|
self.inner.is_suspended()
|
|
}
|
|
|
|
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> {
|
|
if self.is_suspended() {
|
|
Some("Profile Suspended")
|
|
} else {
|
|
self.inner.description()
|
|
}
|
|
}
|
|
|
|
pub(crate) fn name(&self) -> String {
|
|
self.inner
|
|
.display_name()
|
|
.map(|d| d.to_owned())
|
|
.unwrap_or(self.full_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()
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, serde::Deserialize)]
|
|
#[serde(untagged)]
|
|
pub(crate) enum SubmissionPage {
|
|
Max { max: Uuid },
|
|
Min { min: Uuid },
|
|
}
|
|
|
|
pub(crate) struct SubmissionAggregation {
|
|
pub(crate) submissions: Vec<SubmissionView>,
|
|
pub(crate) nav: Vec<Button>,
|
|
}
|
|
|
|
pub(crate) fn build_submissions(
|
|
base_url: &str,
|
|
drafts: bool,
|
|
profile: Option<Profile>,
|
|
viewed_by: Option<Uuid>,
|
|
page: Option<SubmissionPage>,
|
|
per_page: usize,
|
|
dark: bool,
|
|
state: &State,
|
|
) -> SubmissionAggregation {
|
|
let mut nav = vec![];
|
|
let mut submissions: Vec<SubmissionView>;
|
|
let mut view_state = ViewState::new();
|
|
|
|
match page {
|
|
Some(SubmissionPage::Max { max }) => {
|
|
submissions = if profile.is_some() {
|
|
if drafts {
|
|
state
|
|
.profiles
|
|
.store
|
|
.submissions
|
|
.drafted_older_than_for_profile(max)
|
|
.filter_map(|submission_id| {
|
|
SubmissionView::from_id(
|
|
submission_id,
|
|
viewed_by,
|
|
&mut view_state,
|
|
&state,
|
|
)
|
|
})
|
|
.take(per_page + 1)
|
|
.collect()
|
|
} else {
|
|
state
|
|
.profiles
|
|
.store
|
|
.submissions
|
|
.published_older_than_for_profile(max)
|
|
.filter_map(|submission_id| {
|
|
SubmissionView::from_id(
|
|
submission_id,
|
|
viewed_by,
|
|
&mut view_state,
|
|
&state,
|
|
)
|
|
})
|
|
.take(per_page + 1)
|
|
.collect()
|
|
}
|
|
} else {
|
|
state
|
|
.profiles
|
|
.store
|
|
.submissions
|
|
.published_older_than(max)
|
|
.filter_map(|submission_id| {
|
|
SubmissionView::from_id(submission_id, viewed_by, &mut view_state, &state)
|
|
})
|
|
.take(per_page + 1)
|
|
.collect()
|
|
};
|
|
|
|
if let Some(sub) = submissions.get(0) {
|
|
let prev = Button::secondary("Previous");
|
|
prev.href(&format!("{}?min={}", base_url, sub.id()))
|
|
.dark(dark);
|
|
nav.push(prev);
|
|
} else {
|
|
let reset = Button::secondary("Reset");
|
|
reset.href(base_url).dark(dark);
|
|
nav.push(reset);
|
|
}
|
|
|
|
if submissions.len() == per_page + 1 {
|
|
submissions.pop();
|
|
let next = Button::secondary("Next");
|
|
next.href(&format!(
|
|
"{}?max={}",
|
|
base_url,
|
|
submissions[per_page.saturating_sub(1)].id()
|
|
))
|
|
.dark(dark);
|
|
nav.push(next);
|
|
}
|
|
}
|
|
Some(SubmissionPage::Min { min }) => {
|
|
let mut tmp_submissions: Vec<_> = if profile.is_some() {
|
|
if drafts {
|
|
state
|
|
.profiles
|
|
.store
|
|
.submissions
|
|
.drafted_newer_than_for_profile(min)
|
|
.filter_map(|submission_id| {
|
|
SubmissionView::from_id(
|
|
submission_id,
|
|
viewed_by,
|
|
&mut view_state,
|
|
&state,
|
|
)
|
|
})
|
|
.take(per_page + 2)
|
|
.collect()
|
|
} else {
|
|
state
|
|
.profiles
|
|
.store
|
|
.submissions
|
|
.published_newer_than_for_profile(min)
|
|
.filter_map(|submission_id| {
|
|
SubmissionView::from_id(
|
|
submission_id,
|
|
viewed_by,
|
|
&mut view_state,
|
|
&state,
|
|
)
|
|
})
|
|
.take(per_page + 2)
|
|
.collect()
|
|
}
|
|
} else {
|
|
state
|
|
.profiles
|
|
.store
|
|
.submissions
|
|
.published_newer_than(min)
|
|
.filter_map(|submission_id| {
|
|
SubmissionView::from_id(submission_id, viewed_by, &mut view_state, &state)
|
|
})
|
|
.take(per_page + 2)
|
|
.collect()
|
|
};
|
|
|
|
if tmp_submissions.len() == per_page + 2 {
|
|
tmp_submissions.pop();
|
|
let prev = Button::secondary("Previous");
|
|
prev.href(&format!(
|
|
"{}?min={}",
|
|
base_url,
|
|
tmp_submissions[per_page.saturating_sub(1)].id()
|
|
))
|
|
.dark(dark);
|
|
nav.push(prev);
|
|
}
|
|
|
|
submissions = tmp_submissions.into_iter().rev().collect();
|
|
submissions.pop();
|
|
|
|
if let Some(sub) = submissions.get(submissions.len().saturating_sub(1)) {
|
|
let next = Button::secondary("Next");
|
|
next.href(&format!("{}?max={}", base_url, sub.id()))
|
|
.dark(dark);
|
|
nav.push(next);
|
|
} else {
|
|
let reset = Button::secondary("Reset");
|
|
reset.href(base_url).dark(dark);
|
|
nav.push(reset);
|
|
}
|
|
}
|
|
None => {
|
|
submissions = if let Some(profile) = profile {
|
|
if drafts {
|
|
state
|
|
.profiles
|
|
.store
|
|
.submissions
|
|
.drafted_for_profile(profile.id())
|
|
.filter_map(|submission_id| {
|
|
SubmissionView::from_id(
|
|
submission_id,
|
|
viewed_by,
|
|
&mut view_state,
|
|
&state,
|
|
)
|
|
})
|
|
.take(per_page + 1)
|
|
.collect()
|
|
} else {
|
|
state
|
|
.profiles
|
|
.store
|
|
.submissions
|
|
.published_for_profile(profile.id())
|
|
.filter_map(|submission_id| {
|
|
SubmissionView::from_id(
|
|
submission_id,
|
|
viewed_by,
|
|
&mut view_state,
|
|
&state,
|
|
)
|
|
})
|
|
.take(per_page + 1)
|
|
.collect()
|
|
}
|
|
} else {
|
|
state
|
|
.profiles
|
|
.store
|
|
.submissions
|
|
.published()
|
|
.filter_map(|submission_id| {
|
|
SubmissionView::from_id(submission_id, viewed_by, &mut view_state, &state)
|
|
})
|
|
.take(per_page + 1)
|
|
.collect()
|
|
};
|
|
|
|
if submissions.len() == per_page + 1 {
|
|
submissions.pop();
|
|
let next = Button::secondary("Next");
|
|
next.href(&format!(
|
|
"{}?max={}",
|
|
base_url,
|
|
submissions[per_page.saturating_sub(1)].id()
|
|
))
|
|
.dark(dark);
|
|
nav.push(next);
|
|
}
|
|
}
|
|
};
|
|
|
|
SubmissionAggregation { submissions, nav }
|
|
}
|
|
|
|
async fn id_view(
|
|
req: HttpRequest,
|
|
user: Option<User>,
|
|
self_profile: Option<Profile>,
|
|
id: web::Path<Uuid>,
|
|
page: Option<web::Query<SubmissionPage>>,
|
|
nav_state: NavState,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
let profile = profile_from_id(id.into_inner(), &state).await?;
|
|
|
|
do_public_view(
|
|
req.uri().path(),
|
|
user,
|
|
self_profile,
|
|
profile,
|
|
page.map(|q| q.into_inner()),
|
|
nav_state,
|
|
&state,
|
|
)
|
|
.await
|
|
}
|
|
|
|
async fn handle_view(
|
|
req: HttpRequest,
|
|
user: Option<User>,
|
|
self_profile: Option<Profile>,
|
|
handle: web::Path<(String, String)>,
|
|
page: Option<web::Query<SubmissionPage>>,
|
|
nav_state: NavState,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
let (handle, domain) = handle.into_inner();
|
|
let profile = profile_from_handle(handle, domain, &state).await?;
|
|
|
|
do_public_view(
|
|
req.uri().path(),
|
|
user,
|
|
self_profile,
|
|
profile,
|
|
page.map(|q| q.into_inner()),
|
|
nav_state,
|
|
&state,
|
|
)
|
|
.await
|
|
}
|
|
|
|
pub struct ProfileView {
|
|
pub(crate) profile: Profile,
|
|
pub(crate) submissions: Vec<SubmissionView>,
|
|
nav: Vec<Button>,
|
|
pub(crate) viewer: Option<Uuid>,
|
|
buttons: Vec<Button>,
|
|
}
|
|
|
|
impl ProfileView {
|
|
pub(crate) fn nav(&self) -> Vec<&Button> {
|
|
self.nav.iter().collect()
|
|
}
|
|
|
|
pub(crate) fn buttons(&self) -> Vec<&Button> {
|
|
self.buttons.iter().collect()
|
|
}
|
|
}
|
|
|
|
pub struct SubmissionView {
|
|
submission: Submission,
|
|
poster: Profile,
|
|
first_file: File,
|
|
}
|
|
|
|
struct ViewState {
|
|
profiles: HashMap<Uuid, Profile>,
|
|
blocks: HashMap<Uuid, bool>,
|
|
follows: HashMap<Uuid, bool>,
|
|
}
|
|
|
|
impl ViewState {
|
|
fn new() -> Self {
|
|
ViewState {
|
|
profiles: HashMap::new(),
|
|
blocks: HashMap::new(),
|
|
follows: HashMap::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl SubmissionView {
|
|
fn id(&self) -> Uuid {
|
|
self.submission.id()
|
|
}
|
|
|
|
pub(crate) fn from_parts(submission: &Submission, poster: &Profile, first_file: &File) -> Self {
|
|
SubmissionView {
|
|
submission: submission.clone(),
|
|
poster: poster.clone(),
|
|
first_file: first_file.clone(),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn posted_by(&self, profile: Option<Uuid>) -> bool {
|
|
profile.map(|p| p == self.poster.id()).unwrap_or(false)
|
|
}
|
|
|
|
pub(crate) fn is_published(&self) -> bool {
|
|
self.submission.published().is_some()
|
|
}
|
|
|
|
pub(crate) fn name(&self) -> String {
|
|
self.poster.name()
|
|
}
|
|
|
|
pub(crate) fn pictrs_key(&self) -> Option<&str> {
|
|
self.first_file.pictrs().map(|f| f.key())
|
|
}
|
|
|
|
pub(crate) fn title(&self) -> String {
|
|
if !self.is_published() {
|
|
if self.submission.title().len() == 0 {
|
|
"Not Published".to_owned()
|
|
} else {
|
|
format!("Not Published: {}", self.submission.title())
|
|
}
|
|
} else {
|
|
self.submission.title().to_owned()
|
|
}
|
|
}
|
|
|
|
pub(crate) fn view_path(&self) -> String {
|
|
format!("/submissions/{}", self.submission.id())
|
|
}
|
|
|
|
fn from_id(
|
|
submission_id: Uuid,
|
|
viewed_by: Option<Uuid>,
|
|
view_state: &mut ViewState,
|
|
state: &State,
|
|
) -> Option<Self> {
|
|
let submission = state
|
|
.profiles
|
|
.store
|
|
.submissions
|
|
.by_id(submission_id)
|
|
.ok()??;
|
|
|
|
let is_self = viewed_by
|
|
.map(|pid| pid == submission.profile_id())
|
|
.unwrap_or(false);
|
|
|
|
if !is_self && submission.published().is_none() {
|
|
return None;
|
|
}
|
|
|
|
let poster = if let Some(profile) = view_state.profiles.get(&submission.profile_id()) {
|
|
profile.clone()
|
|
} else {
|
|
let profile = Profile::from_id(submission.profile_id(), state).ok()?;
|
|
view_state.profiles.insert(profile.id(), profile.clone());
|
|
profile
|
|
};
|
|
|
|
if viewed_by.is_none() && poster.login_required() {
|
|
return None;
|
|
}
|
|
|
|
if let Some(block) = view_state.blocks.get(&poster.id()) {
|
|
if *block {
|
|
return None;
|
|
}
|
|
} else {
|
|
if let Some(profile_id) = viewed_by {
|
|
let blocking = state
|
|
.profiles
|
|
.store
|
|
.view
|
|
.blocks
|
|
.by_forward(submission.profile_id(), profile_id)
|
|
.ok()?
|
|
.is_some();
|
|
|
|
if blocking {
|
|
view_state.blocks.insert(poster.id(), true);
|
|
return None;
|
|
}
|
|
|
|
let blocked = state
|
|
.profiles
|
|
.store
|
|
.view
|
|
.blocks
|
|
.by_forward(profile_id, submission.profile_id())
|
|
.ok()?
|
|
.is_some();
|
|
|
|
if blocked {
|
|
view_state.blocks.insert(poster.id(), true);
|
|
return None;
|
|
}
|
|
|
|
view_state.blocks.insert(poster.id(), false);
|
|
}
|
|
}
|
|
|
|
if submission.is_followers_only() {
|
|
if let Some(follow) = view_state.follows.get(&poster.id()) {
|
|
if !follow {
|
|
return None;
|
|
}
|
|
} else {
|
|
let profile_id = viewed_by?;
|
|
|
|
if !is_self
|
|
&& state
|
|
.profiles
|
|
.store
|
|
.view
|
|
.follows
|
|
.by_forward(submission.profile_id(), profile_id)
|
|
.ok()?
|
|
.is_none()
|
|
{
|
|
view_state.follows.insert(poster.id(), false);
|
|
return None;
|
|
}
|
|
|
|
view_state.follows.insert(poster.id(), true);
|
|
}
|
|
}
|
|
|
|
let file_id = submission.files().get(0)?;
|
|
let first_file = state.profiles.store.files.by_id(*file_id).ok()??;
|
|
|
|
Some(SubmissionView {
|
|
submission,
|
|
poster,
|
|
first_file,
|
|
})
|
|
}
|
|
}
|
|
|
|
async fn profile_buttons(
|
|
profile: &Profile,
|
|
self_profile: Option<&Profile>,
|
|
is_drafts: bool,
|
|
dark: bool,
|
|
state: &State,
|
|
) -> Result<Vec<Button>, Error> {
|
|
let mut buttons = vec![];
|
|
|
|
if profile.is_suspended() {
|
|
return Ok(buttons);
|
|
}
|
|
|
|
let is_follow_requested = if let Some(self_profile) = self_profile {
|
|
let follow_requests = state.profiles.store.view.follow_requests.clone();
|
|
let self_id = self_profile.id();
|
|
let profile_id = profile.id();
|
|
web::block(move || Ok(follow_requests.by_forward(profile_id, self_id)?.is_some())).await?
|
|
} else {
|
|
false
|
|
};
|
|
|
|
let is_follower = if let Some(self_profile) = self_profile {
|
|
let follows = state.profiles.store.view.follows.clone();
|
|
let self_id = self_profile.id();
|
|
let profile_id = profile.id();
|
|
web::block(move || Ok(follows.by_forward(profile_id, self_id)?.is_some())).await?
|
|
} else {
|
|
false
|
|
};
|
|
|
|
let is_blocking = if let Some(self_profile) = self_profile {
|
|
let blocks = state.profiles.store.view.blocks.clone();
|
|
let self_id = self_profile.id();
|
|
let profile_id = profile.id();
|
|
web::block(move || Ok(blocks.by_forward(profile_id, self_id)?.is_some())).await?
|
|
} else {
|
|
false
|
|
};
|
|
|
|
if self_profile
|
|
.map(|p| p.id() == profile.id())
|
|
.unwrap_or(false)
|
|
{
|
|
let drafts = if is_drafts {
|
|
let view = Button::secondary("View Profile");
|
|
view.href(&profile.view_path()).dark(dark);
|
|
view
|
|
} else {
|
|
let drafts = Button::secondary("View Drafts");
|
|
drafts.href("/profiles/drafts").dark(dark);
|
|
drafts
|
|
};
|
|
|
|
let edit = Button::secondary("Edit Profile");
|
|
let switch = Button::secondary("Switch Profile");
|
|
|
|
edit.href("/profiles/current").dark(dark);
|
|
switch.href("/profiles/change").dark(dark);
|
|
|
|
buttons.push(drafts);
|
|
buttons.push(edit);
|
|
buttons.push(switch);
|
|
} else if is_follower || is_follow_requested {
|
|
let unfollow = if is_follower {
|
|
Button::secondary("Unfollow")
|
|
} else {
|
|
Button::secondary("Remove Request")
|
|
};
|
|
let block = Button::secondary("Block");
|
|
let report = Button::primary_outline("Report");
|
|
|
|
unfollow.form(&profile.unfollow_path()).dark(dark);
|
|
block.form(&profile.block_path()).dark(dark);
|
|
report.href(&profile.report_path()).dark(dark);
|
|
|
|
buttons.push(unfollow);
|
|
buttons.push(block);
|
|
buttons.push(report);
|
|
} else if is_blocking {
|
|
let unblock = Button::secondary("Unblock");
|
|
let report = Button::primary_outline("Report");
|
|
|
|
unblock.form(&profile.unblock_path()).dark(dark);
|
|
report.href(&profile.report_path()).dark(dark);
|
|
|
|
buttons.push(unblock);
|
|
buttons.push(report);
|
|
} else if self_profile.is_some() {
|
|
let follow = Button::secondary("Follow");
|
|
let block = Button::secondary("Block");
|
|
let report = Button::primary_outline("Report");
|
|
|
|
follow.form(&profile.follow_path()).dark(dark);
|
|
block.form(&profile.block_path()).dark(dark);
|
|
report.href(&profile.report_path()).dark(dark);
|
|
|
|
buttons.push(follow);
|
|
buttons.push(block);
|
|
buttons.push(report);
|
|
}
|
|
|
|
Ok(buttons)
|
|
}
|
|
|
|
async fn drafts_page(
|
|
req: HttpRequest,
|
|
profile: Profile,
|
|
page: Option<web::Query<SubmissionPage>>,
|
|
nav_state: NavState,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
let viewed_by = Some(profile.id());
|
|
let agg = build_submissions(
|
|
req.uri().path(),
|
|
true,
|
|
Some(profile.clone()),
|
|
viewed_by,
|
|
page.map(|q| q.into_inner()),
|
|
6,
|
|
nav_state.dark(),
|
|
&state,
|
|
);
|
|
|
|
let buttons = profile_buttons(&profile, Some(&profile), true, nav_state.dark(), &state).await?;
|
|
|
|
let view = ProfileView {
|
|
profile,
|
|
submissions: agg.submissions,
|
|
nav: agg.nav,
|
|
viewer: viewed_by,
|
|
buttons,
|
|
};
|
|
|
|
crate::rendered(HttpResponse::Ok(), |cursor| {
|
|
crate::templates::profiles::drafts(cursor, &view, &nav_state)
|
|
})
|
|
}
|
|
|
|
async fn do_public_view(
|
|
base_url: &str,
|
|
user: Option<User>,
|
|
self_profile: Option<Profile>,
|
|
profile: Profile,
|
|
page: Option<SubmissionPage>,
|
|
nav_state: NavState,
|
|
state: &State,
|
|
) -> Result<HttpResponse, Error> {
|
|
if profile.login_required() && user.is_none() {
|
|
return Ok(crate::to_404());
|
|
}
|
|
|
|
let is_blocked = if let Some(self_profile) = &self_profile {
|
|
let blocks = state.profiles.store.view.blocks.clone();
|
|
let self_id = self_profile.id();
|
|
let profile_id = profile.id();
|
|
|
|
web::block(move || Ok(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 agg = build_submissions(
|
|
base_url,
|
|
false,
|
|
Some(profile.clone()),
|
|
viewed_by,
|
|
page,
|
|
6,
|
|
nav_state.dark(),
|
|
state,
|
|
);
|
|
|
|
let buttons = profile_buttons(
|
|
&profile,
|
|
self_profile.as_ref(),
|
|
false,
|
|
nav_state.dark(),
|
|
state,
|
|
)
|
|
.await?;
|
|
|
|
let view = ProfileView {
|
|
profile,
|
|
submissions: agg.submissions,
|
|
nav: agg.nav,
|
|
viewer: viewed_by,
|
|
buttons,
|
|
};
|
|
|
|
crate::rendered(HttpResponse::Ok(), |cursor| {
|
|
crate::templates::profiles::public(cursor, &view, &nav_state)
|
|
})
|
|
}
|
|
|
|
async fn profiles() -> HttpResponse {
|
|
to_current_profile()
|
|
}
|
|
|
|
async fn delete_page(profile: Profile, nav_state: NavState) -> Result<HttpResponse, Error> {
|
|
crate::rendered(HttpResponse::Ok(), |cursor| {
|
|
crate::templates::profiles::delete(cursor, &profile, &nav_state)
|
|
})
|
|
}
|
|
|
|
async fn delete_profile(profile: Profile, state: web::Data<State>) -> Result<HttpResponse, Error> {
|
|
use hyaenidae_profiles::apub::actions::DeleteProfile;
|
|
|
|
state
|
|
.profiles
|
|
.run(DeleteProfile::from_id(profile.id()))
|
|
.await?;
|
|
|
|
Ok(to_change_profile_page())
|
|
}
|
|
|
|
async fn current_profile(profile: Profile, nav_state: NavState) -> Result<HttpResponse, Error> {
|
|
let profile_state = ProfileState::new(profile, nav_state.dark());
|
|
|
|
crate::rendered(HttpResponse::Ok(), |cursor| {
|
|
crate::templates::profiles::current(cursor, &profile_state, &nav_state)
|
|
})
|
|
}
|
|
|
|
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<String> {
|
|
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<String> {
|
|
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<String> {
|
|
if description.len() > MAX_DESCRIPTION_LEN {
|
|
return Some(format!(
|
|
"Must be shorter than {} characters",
|
|
MAX_DESCRIPTION_LEN
|
|
));
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
async fn update_bio(
|
|
form: web::Form<BioForm>,
|
|
profile: Profile,
|
|
nav_state: NavState,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
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() {
|
|
use hyaenidae_profiles::apub::actions::UpdateProfile;
|
|
|
|
let res = state
|
|
.profiles
|
|
.run(UpdateProfile::from_text(
|
|
profile.id(),
|
|
display_name.clone(),
|
|
description.clone(),
|
|
))
|
|
.await;
|
|
|
|
Some(res)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let mut profile_state = ProfileState::new(profile.refresh(&state)?, nav_state.dark());
|
|
profile_state.display_name.value(&display_name);
|
|
profile_state.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, &profile_state, &nav_state)
|
|
})
|
|
}
|
|
|
|
async fn update_icon(
|
|
request: HttpRequest,
|
|
payload: web::Payload,
|
|
profile: Profile,
|
|
nav_state: NavState,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
let res = state
|
|
.profiles
|
|
.upload_image(request, payload.into_inner())
|
|
.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 profile_state = ProfileState::new(profile.refresh(&state)?, nav_state.dark());
|
|
profile_state.icon_error(&error);
|
|
|
|
crate::rendered(HttpResponse::Ok(), |cursor| {
|
|
crate::templates::profiles::current(cursor, &profile_state, &nav_state)
|
|
})
|
|
}
|
|
|
|
async fn update_banner(
|
|
request: HttpRequest,
|
|
payload: web::Payload,
|
|
profile: Profile,
|
|
nav_state: NavState,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
let res = state
|
|
.profiles
|
|
.upload_image(request, payload.into_inner())
|
|
.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 profile_state = ProfileState::new(profile.refresh(&state)?, nav_state.dark());
|
|
profile_state.banner_error(&error);
|
|
|
|
crate::rendered(HttpResponse::Ok(), |cursor| {
|
|
crate::templates::profiles::current(cursor, &profile_state, &nav_state)
|
|
})
|
|
}
|
|
|
|
async fn update_require_login(
|
|
form: web::Form<RequireLoginForm>,
|
|
profile: Profile,
|
|
nav_state: NavState,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
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 profile_state = ProfileState::new(profile.refresh(&state)?, nav_state.dark());
|
|
|
|
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, &profile_state, &nav_state)
|
|
})
|
|
}
|
|
|
|
#[derive(Clone, Debug, serde::Deserialize)]
|
|
struct ChangeProfileForm {
|
|
profile_id: Uuid,
|
|
}
|
|
|
|
async fn change_profile_page(
|
|
user: User,
|
|
nav_state: NavState,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
let profiles = state
|
|
.profiles
|
|
.store
|
|
.profiles
|
|
.for_local(user.id())
|
|
.filter_map(|profile_id| Profile::from_id(profile_id, &state).ok())
|
|
.collect::<Vec<_>>();
|
|
|
|
crate::rendered(HttpResponse::Ok(), |cursor| {
|
|
crate::templates::profiles::list(cursor, &profiles, &nav_state)
|
|
})
|
|
}
|
|
|
|
async fn change_profile(
|
|
user: User,
|
|
session: Session,
|
|
form: web::Form<ChangeProfileForm>,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
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, nav_state: NavState) -> Result<HttpResponse, Error> {
|
|
let mut handle_input = TextInput::new("handle");
|
|
handle_input
|
|
.placeholder("Handle")
|
|
.title("Handle")
|
|
.dark(nav_state.dark());
|
|
|
|
crate::rendered(HttpResponse::Ok(), |cursor| {
|
|
crate::templates::profiles::create::handle(cursor, &handle_input, &nav_state)
|
|
})
|
|
}
|
|
|
|
async fn create_handle(
|
|
user: User,
|
|
form: web::Form<HandleForm>,
|
|
session: Session,
|
|
nav_state: NavState,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
let domain = state.domain.clone();
|
|
let handle = form.handle.clone();
|
|
|
|
let error = match validate_handle(&handle) {
|
|
Some(error) => Some(error),
|
|
None => {
|
|
let exists = state
|
|
.profiles
|
|
.store
|
|
.profiles
|
|
.by_handle(&handle, &domain)?
|
|
.is_some();
|
|
|
|
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 = TextInput::new("handle");
|
|
handle_input
|
|
.placeholder("Handle")
|
|
.title("Handle")
|
|
.value(&form.handle)
|
|
.error_opt(error)
|
|
.dark(nav_state.dark());
|
|
|
|
crate::rendered(HttpResponse::BadRequest(), |cursor| {
|
|
crate::templates::profiles::create::handle(cursor, &handle_input, &nav_state)
|
|
})
|
|
}
|
|
|
|
#[derive(Clone, Debug, serde::Deserialize)]
|
|
pub struct BioForm {
|
|
pub(crate) display_name: String,
|
|
pub(crate) description: String,
|
|
}
|
|
|
|
async fn new_bio(profile: Profile, nav_state: NavState) -> Result<HttpResponse, Error> {
|
|
let mut display_name = TextInput::new("display_name");
|
|
display_name
|
|
.title("Display Name")
|
|
.placeholder("Display Name")
|
|
.dark(nav_state.dark());
|
|
if let Some(text) = profile.display_name() {
|
|
display_name.value(text);
|
|
}
|
|
|
|
let mut description = TextInput::new("description");
|
|
description
|
|
.title("Description")
|
|
.placeholder("Description")
|
|
.textarea()
|
|
.dark(nav_state.dark());
|
|
if let Some(text) = profile.description() {
|
|
description.value(text);
|
|
}
|
|
|
|
crate::rendered(HttpResponse::Ok(), |cursor| {
|
|
crate::templates::profiles::create::bio(
|
|
cursor,
|
|
&display_name,
|
|
&description,
|
|
&profile,
|
|
&nav_state,
|
|
)
|
|
})
|
|
}
|
|
|
|
async fn create_bio(
|
|
form: web::Form<BioForm>,
|
|
profile: Profile,
|
|
nav_state: NavState,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
let display_name = form.display_name.clone();
|
|
let description = form.description.clone();
|
|
|
|
let mut display_name_error = validate_display_name(&display_name);
|
|
let description_error = validate_description(&description);
|
|
|
|
use hyaenidae_profiles::apub::actions::UpdateProfile;
|
|
|
|
if display_name_error.is_none() && description_error.is_none() {
|
|
let res = state
|
|
.profiles
|
|
.run(UpdateProfile::from_text(
|
|
profile.id(),
|
|
display_name,
|
|
description,
|
|
))
|
|
.await;
|
|
|
|
match res {
|
|
Ok(_) => return Ok(to_icon()),
|
|
Err(e) => {
|
|
display_name_error = Some(e.to_string());
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut display_name = TextInput::new("display_name");
|
|
display_name
|
|
.title("Display Name")
|
|
.placeholder("Display Name")
|
|
.value(&form.display_name)
|
|
.error_opt(display_name_error)
|
|
.dark(nav_state.dark());
|
|
|
|
let mut description = TextInput::new("description");
|
|
description
|
|
.title("Description")
|
|
.placeholder("Description")
|
|
.value(&form.description)
|
|
.error_opt(description_error)
|
|
.dark(nav_state.dark());
|
|
|
|
let profile = profile.refresh(&state)?;
|
|
crate::rendered(HttpResponse::BadRequest(), |cursor| {
|
|
crate::templates::profiles::create::bio(
|
|
cursor,
|
|
&display_name,
|
|
&description,
|
|
&profile,
|
|
&nav_state,
|
|
)
|
|
})
|
|
}
|
|
|
|
async fn new_icon(profile: Profile, nav_state: NavState) -> Result<HttpResponse, Error> {
|
|
let mut icon_input = hyaenidae_toolkit::FileInput::secondary("images[]", "Select Icon");
|
|
icon_input.accept(ACCEPT_TYPES).dark(nav_state.dark());
|
|
|
|
crate::rendered(HttpResponse::Ok(), |cursor| {
|
|
crate::templates::profiles::create::icon(cursor, &icon_input, None, &profile, &nav_state)
|
|
})
|
|
}
|
|
|
|
async fn create_icon(
|
|
request: HttpRequest,
|
|
payload: web::Payload,
|
|
profile: Profile,
|
|
nav_state: NavState,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
let res = state
|
|
.profiles
|
|
.upload_image(request, payload.into_inner())
|
|
.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.accept(ACCEPT_TYPES).dark(nav_state.dark());
|
|
|
|
let profile = profile.refresh(&state)?;
|
|
crate::rendered(HttpResponse::BadRequest(), |cursor| {
|
|
crate::templates::profiles::create::icon(cursor, &icon_input, error, &profile, &nav_state)
|
|
})
|
|
}
|
|
|
|
async fn new_banner(profile: Profile, nav_state: NavState) -> Result<HttpResponse, Error> {
|
|
let mut banner_input = hyaenidae_toolkit::FileInput::secondary("images[]", "Select Banner");
|
|
banner_input.accept(ACCEPT_TYPES).dark(nav_state.dark());
|
|
|
|
crate::rendered(HttpResponse::Ok(), |cursor| {
|
|
crate::templates::profiles::create::banner(
|
|
cursor,
|
|
&banner_input,
|
|
None,
|
|
&profile,
|
|
&nav_state,
|
|
)
|
|
})
|
|
}
|
|
|
|
async fn create_banner(
|
|
request: HttpRequest,
|
|
payload: web::Payload,
|
|
profile: Profile,
|
|
nav_state: NavState,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
let res = state
|
|
.profiles
|
|
.upload_image(request, payload.into_inner())
|
|
.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.accept(ACCEPT_TYPES).dark(nav_state.dark());
|
|
|
|
let profile = profile.refresh(&state)?;
|
|
crate::rendered(HttpResponse::BadRequest(), |cursor| {
|
|
crate::templates::profiles::create::banner(
|
|
cursor,
|
|
&banner_input,
|
|
error,
|
|
&profile,
|
|
&nav_state,
|
|
)
|
|
})
|
|
}
|
|
|
|
#[derive(Clone, Debug, serde::Deserialize)]
|
|
struct RequireLoginForm {
|
|
require_login: Option<String>,
|
|
}
|
|
|
|
async fn new_require_login(profile: Profile, nav_state: NavState) -> Result<HttpResponse, Error> {
|
|
crate::rendered(HttpResponse::Ok(), |cursor| {
|
|
crate::templates::profiles::create::require_login(
|
|
cursor,
|
|
profile.inner.login_required(),
|
|
None,
|
|
&profile,
|
|
&nav_state,
|
|
)
|
|
})
|
|
}
|
|
|
|
async fn create_require_login(
|
|
form: web::Form<RequireLoginForm>,
|
|
profile: Profile,
|
|
nav_state: NavState,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
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,
|
|
&nav_state,
|
|
)
|
|
})
|
|
}
|
|
|
|
async fn done(profile: Profile, nav_state: NavState) -> Result<HttpResponse, Error> {
|
|
crate::rendered(HttpResponse::Ok(), |cursor| {
|
|
crate::templates::profiles::create::done(cursor, &profile, &nav_state)
|
|
})
|
|
}
|