hyaenidae/src/profiles/mod.rs
asonix 010dd2952f Server: Expose NSFW toggle, Dark Mode toggle
Ensure all submission view permission logic is the same
2021-02-03 21:09:25 -06:00

744 lines
21 KiB
Rust

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