hyaenidae/src/submissions.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

1383 lines
39 KiB
Rust

use crate::{
comments::{Cache, CommentNode},
error::{Error, OptionExt},
extensions::SubmissionExt,
images::{largest_image, FullImage, ImageType},
middleware::{CurrentSubmission, UserProfile},
nav::NavState,
profiles::Settings,
views::{OwnedProfileView, OwnedSubmissionView},
ActixLoader, State,
};
use actix_web::{client::Client, web, HttpRequest, HttpResponse, Scope};
use hyaenidae_profiles::{
apub::actions::{
CreateComment, CreateReport, CreateSubmission, DeleteSubmission, UpdateSubmission,
},
store::{File, Profile, Submission, Visibility},
};
use hyaenidae_toolkit::{Button, FileInput, Select, TextInput, Tile};
use i18n_embed_fl::fl;
use std::collections::HashMap;
use uuid::Uuid;
pub(super) fn scope() -> Scope {
web::scope("/submissions")
.service(
web::resource("/create")
.route(web::get().to(files_page))
.route(web::post().to(upload_files)),
)
.service(
web::scope("/{submission_id}")
.route("", web::get().to(submission_page))
.service(
web::resource("/report")
.route(web::get().to(report_page))
.route(web::post().to(report)),
)
.route("/report-success", web::get().to(report_success_page))
.service(
web::resource("/comment")
.route(web::get().to(route_to_update_page))
.route(web::post().to(create_comment)),
)
.service(
web::resource("/update")
.route(web::get().to(update_page))
.route(web::post().to(update_submission)),
)
.service(
web::resource("/visibility")
.route(web::get().to(route_to_update_page))
.route(web::post().to(update_visibility)),
)
.service(
web::resource("/sensitive")
.route(web::get().to(route_to_update_page))
.route(web::post().to(update_sensitive)),
)
.service(
web::resource("/add-file")
.route(web::get().to(route_to_update_page))
.route(web::post().to(add_file)),
)
.service(
web::resource("/remove-file")
.route(web::get().to(route_to_update_page))
.route(web::post().to(remove_file)),
)
.service(
web::resource("/publish")
.route(web::get().to(route_to_update_page))
.route(web::post().to(publish_submission)),
)
.service(
web::resource("/delete")
.route(web::get().to(route_to_update_page))
.route(web::post().to(delete_submission)),
),
)
}
pub struct ReportView {
pub(crate) files: HashMap<Uuid, File>,
pub(crate) submission: Submission,
pub(crate) author: Profile,
error: Option<String>,
value: Option<String>,
}
impl ReportView {
async fn build(submission_id: Uuid, state: &State) -> Result<Self, Error> {
let store = state.profiles.clone();
let view = web::block(move || {
let mut files = HashMap::new();
let submission = store.store.submissions.by_id(submission_id)?.req()?;
let author = store.store.profiles.by_id(submission.profile_id())?.req()?;
let file_ids = submission
.files()
.iter()
.copied()
.chain(author.icon())
.chain(author.banner());
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(ReportView {
files,
submission,
author,
error: None,
value: None,
})
})
.await?;
Ok(view)
}
pub(crate) fn submission(&self) -> OwnedSubmissionView {
OwnedSubmissionView {
submission: self.submission.clone(),
files: self
.submission
.files()
.iter()
.filter_map(move |file_id| self.files.get(file_id))
.map(|file| file.clone())
.collect(),
current_file: None,
}
}
pub(crate) fn author(&self) -> OwnedProfileView {
OwnedProfileView {
profile: self.author.clone(),
icon: self
.author
.icon()
.and_then(|i| self.files.get(&i))
.map(|i| i.clone()),
banner: self
.author
.banner()
.and_then(|b| self.files.get(&b))
.map(|b| b.clone()),
}
}
pub(crate) fn input(&self, loader: &ActixLoader) -> TextInput {
let input = TextInput::new("body")
.title(&fl!(loader, "report-input"))
.placeholder(&fl!(loader, "report-placeholder"))
.textarea();
let input = if let Some(error) = &self.error {
input.error_opt(Some(error.to_owned()))
} else {
input
};
if let Some(value) = &self.value {
input.value(value)
} else {
input
}
}
fn error_opt(&mut self, opt: Option<String>) -> &mut Self {
self.error = opt;
self
}
fn value(&mut self, text: String) -> &mut Self {
self.value = Some(text);
self
}
}
async fn report_page(
loader: ActixLoader,
submission_id: web::Path<Uuid>,
profile: UserProfile,
nav_state: NavState,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
let profile = profile.0;
let view = ReportView::build(submission_id.into_inner(), &state).await?;
let can_view_sensitive = state.settings.for_profile(profile.id()).await?.sensitive;
if let Some(res) = can_view(
Some(profile.id()),
can_view_sensitive,
&view.submission,
&state,
)
.await?
{
return Ok(res);
}
crate::rendered(HttpResponse::Ok(), |cursor| {
crate::templates::submissions::report(cursor, &loader, &view, &nav_state)
})
}
#[derive(Clone, Debug, serde::Deserialize)]
struct ReportForm {
body: String,
}
const MAX_REPORT_LEN: usize = 1000;
async fn report(
loader: ActixLoader,
form: web::Form<ReportForm>,
submission_id: web::Path<Uuid>,
profile: UserProfile,
nav_state: NavState,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
let profile = profile.0;
let mut view = ReportView::build(submission_id.into_inner(), &state).await?;
let can_view_sensitive = state.settings.for_profile(profile.id()).await?.sensitive;
if let Some(res) = can_view(
Some(profile.id()),
can_view_sensitive,
&view.submission,
&state,
)
.await?
{
return Ok(res);
}
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_submission(
view.submission.id(),
profile.id(),
Some(form.body.clone()),
))
.await;
match res {
Ok(_) => return Ok(to_report_success_page(view.submission.id())),
Err(e) => e.to_string(),
}
};
view.error_opt(Some(error)).value(form.body);
crate::rendered(HttpResponse::BadRequest(), |cursor| {
crate::templates::submissions::report(cursor, &loader, &view, &nav_state)
})
}
async fn report_success_page(
loader: ActixLoader,
submission_id: web::Path<Uuid>,
profile: UserProfile,
nav_state: NavState,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
let profile = profile.0;
let view = ReportView::build(submission_id.into_inner(), &state).await?;
let can_view_sensitive = state.settings.for_profile(profile.id()).await?.sensitive;
if let Some(res) = can_view(
Some(profile.id()),
can_view_sensitive,
&view.submission,
&state,
)
.await?
{
return Ok(res);
}
crate::rendered(HttpResponse::Ok(), |cursor| {
crate::templates::submissions::report_success(cursor, &loader, &view, &nav_state)
})
}
fn to_report_success_page(submission_id: Uuid) -> HttpResponse {
crate::redirect(&format!("/submissions/{}/report-success", submission_id))
}
pub struct SubmissionState {
pub(crate) files: HashMap<Uuid, File>,
pub(crate) submission: Submission,
title_value: Option<String>,
title_error: Option<String>,
description_value: Option<String>,
description_error: Option<String>,
file_error: Option<String>,
pub(crate) remove_file_error: HashMap<Uuid, String>,
pub(crate) publish_error: Option<String>,
pub(crate) delete_error: Option<String>,
pub(crate) visibility_error: Option<String>,
pub(crate) sensitive_error: Option<String>,
pub(crate) settings: Settings,
}
impl SubmissionState {
async fn new(submission: Submission, state: &State) -> Result<Self, Error> {
let file_ids: Vec<Uuid> = submission.files().iter().copied().collect();
let settings = state.settings.for_profile(submission.profile_id()).await?;
let store = state.profiles.clone();
let files = web::block(move || {
let mut files = HashMap::new();
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<HashMap<Uuid, File>, Error>
})
.await?;
Ok(SubmissionState {
files,
submission,
title_value: None,
title_error: None,
description_value: None,
description_error: None,
file_error: None,
remove_file_error: HashMap::new(),
publish_error: None,
delete_error: None,
visibility_error: None,
sensitive_error: None,
settings,
})
}
pub(crate) fn visibility(&self, loader: &ActixLoader) -> Select {
Select::new("visibility")
.title(&fl!(loader, "submission-visibility-select"))
.options(&[
(&fl!(loader, "submission-visibility-followers"), "Followers"),
(&fl!(loader, "submission-visibility-unlisted"), "Unlisted"),
(&fl!(loader, "submission-visibility-public"), "Public"),
])
.default_option(&self.submission.visibility().to_string())
}
pub(crate) fn title_input(&self, loader: &ActixLoader) -> TextInput {
TextInput::new("title")
.title(&fl!(loader, "update-submission-title-input"))
.placeholder(&fl!(loader, "update-submission-title-placeholder"))
.value(
self.title_value.as_deref().unwrap_or(
self.submission
.title_source()
.unwrap_or(self.submission.title()),
),
)
.error_opt(self.title_error.clone())
}
pub(crate) fn description_input(&self, loader: &ActixLoader) -> TextInput {
let input = TextInput::new("description")
.title(&fl!(loader, "update-submission-description-input"))
.placeholder(&fl!(loader, "update-submission-description-placeholder"))
.textarea()
.error_opt(self.description_error.clone());
if let Some(text) = self
.description_value
.as_deref()
.or_else(|| self.submission.description_source())
{
input.value(text)
} else {
input
}
}
pub(crate) fn file_input(&self, loader: &ActixLoader) -> FileInput {
file_input(loader)
}
pub(crate) fn is_published(&self) -> bool {
self.submission.published().is_some()
}
pub(crate) fn view_path(&self) -> String {
self.submission.view_path()
}
pub(crate) fn visibility_path(&self) -> String {
format!("/submissions/{}/visibility", self.submission.id())
}
pub(crate) fn sensitive_path(&self) -> String {
format!("/submissions/{}/sensitive", self.submission.id())
}
pub(crate) fn update_path(&self) -> String {
format!("/submissions/{}/update", self.submission.id())
}
pub(crate) fn add_file_path(&self) -> String {
format!("/submissions/{}/add-file", self.submission.id())
}
pub(crate) fn remove_file_path(&self) -> String {
format!("/submissions/{}/remove-file", self.submission.id())
}
pub(crate) fn publish_path(&self) -> String {
format!("/submissions/{}/publish", self.submission.id())
}
pub(crate) fn delete_path(&self) -> String {
format!("/submissions/{}/delete", self.submission.id())
}
pub(crate) fn images<'a>(&'a self) -> impl Iterator<Item = (Uuid, FullImage)> + 'a {
self.submission
.files()
.iter()
.filter_map(move |file_id| self.files.get(file_id))
.filter_map(|file| Some((file.id(), file.pictrs_key()?)))
.enumerate()
.map(move |(index, (id, key))| {
let title = format!("{} file {}", self.submission.title_text(), index + 1);
(id, FullImage::new(key, &title))
})
}
fn title(mut self, text: &str) -> Self {
self.title_value = Some(text.to_owned());
self
}
fn title_error(mut self, error: String) -> Self {
self.title_error = Some(error);
self
}
fn description(mut self, text: &str) -> Self {
self.description_value = Some(text.to_owned());
self
}
fn description_error(mut self, error: Option<String>) -> Self {
self.description_error = error;
self
}
fn file_error(mut self, error: String) -> Self {
self.file_error = Some(error);
self
}
fn remove_file_error(mut self, file_id: Uuid, error: String) -> Self {
self.remove_file_error.insert(file_id, error);
self
}
fn publish_error(mut self, error: String) -> Self {
self.publish_error = Some(error);
self
}
fn delete_error(mut self, error: String) -> Self {
self.delete_error = Some(error);
self
}
fn visibility_error(mut self, error: String) -> Self {
self.visibility_error = Some(error);
self
}
fn sensitive_error(mut self, error: String) -> Self {
self.sensitive_error = Some(error);
self
}
}
async fn files_page(
loader: ActixLoader,
_: UserProfile,
nav_state: NavState,
) -> Result<HttpResponse, Error> {
let file_input = file_input(&loader);
crate::rendered(HttpResponse::Ok(), |cursor| {
crate::templates::submissions::create(cursor, &loader, file_input, None, &nav_state)
})
}
async fn upload_files(
loader: ActixLoader,
profile: UserProfile,
req: HttpRequest,
pl: web::Payload,
nav_state: NavState,
client: web::Data<Client>,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
let profile = profile.0;
let res = state
.profiles
.upload_image(req, pl.into_inner(), &client)
.await;
let error = match res {
Ok(file_ids) if file_ids.len() == 1 => {
let res = state
.profiles
.run(CreateSubmission::from_file(profile.id(), file_ids[0]))
.await;
match res {
Ok(Some(submission_id)) => return Ok(to_update_page(submission_id)),
Ok(None) => "Error creating submission".to_string(),
Err(e) => e.to_string(),
}
}
Ok(_) => "Incorrect number of files".to_owned(),
Err(e) => e.to_string(),
};
let file_input = file_input(&loader);
crate::rendered(HttpResponse::BadRequest(), |cursor| {
crate::templates::submissions::create(cursor, &loader, file_input, Some(error), &nav_state)
})
}
#[derive(Clone, Debug, serde::Deserialize)]
struct FileQuery {
file_page: usize,
}
pub struct ViewSubmissionState {
pub(crate) cache: Cache,
pub(crate) profile: Option<Profile>,
pub(crate) submission: Submission,
pub(crate) poster: Profile,
pub(crate) current_file: Uuid,
pub(crate) nav: Vec<Button>,
pub(crate) is_self: bool,
comment_error: Option<String>,
comment_value: Option<String>,
pub(crate) comments: CommentNode,
}
impl ViewSubmissionState {
pub(crate) fn image(&self) -> Option<FullImage> {
let file = self.cache.files.get(&self.current_file)?;
let key = file.pictrs_key()?;
Some(FullImage::new(key, self.submission.title()))
}
pub(crate) fn comment_input(&self, loader: &ActixLoader) -> TextInput {
let input = TextInput::new("body")
.title(&fl!(loader, "comment-input"))
.placeholder(&fl!(loader, "comment-placeholder"))
.textarea()
.error_opt(self.comment_error.clone());
if let Some(value) = &self.comment_value {
input.value(value)
} else {
input
}
}
pub(crate) fn parent(&self) -> CommentNode {
CommentNode {
item: crate::comments::Item::Submission(self.submission.id()),
author_id: self.submission.profile_id(),
is_self: self.is_self,
children: vec![],
}
}
pub(crate) fn tiles(&self, loader: &ActixLoader) -> Vec<Tile> {
OwnedSubmissionView {
submission: self.submission.clone(),
files: self
.submission
.files()
.iter()
.filter_map(move |file_id| self.cache.files.get(file_id))
.map(|file| file.clone())
.collect(),
current_file: Some(self.current_file),
}
.tiles(loader)
}
pub(crate) fn poster(&self) -> OwnedProfileView {
OwnedProfileView {
profile: self.poster.clone(),
icon: self
.poster
.icon()
.and_then(|i| self.cache.files.get(&i))
.map(|i| i.clone()),
banner: self
.poster
.banner()
.and_then(|b| self.cache.files.get(&b))
.map(|b| b.clone()),
}
}
}
async fn can_view(
viewer: Option<Uuid>,
can_view_sensitive: bool,
submission: &Submission,
state: &State,
) -> Result<Option<HttpResponse>, Error> {
let store = state.profiles.clone();
let submission = submission.clone();
let opt = web::block(move || {
Ok(crate::pagination::submission::can_view(
viewer,
&submission,
&store.store,
&mut Default::default(),
true,
can_view_sensitive,
)) as Result<Option<()>, Error>
})
.await?;
if opt.is_none() {
return Ok(Some(crate::to_404()));
} else {
Ok(None)
}
}
async fn files_for_submission(
submission: &Submission,
mut cache: Cache,
state: &State,
) -> Result<Cache, Error> {
let file_ids: Vec<Uuid> = submission.files().iter().copied().collect();
let store = state.profiles.clone();
let cache = web::block(move || {
for file_id in file_ids {
if !cache.files.contains_key(&file_id) {
let file = store.store.files.by_id(file_id)?.req()?;
cache.files.insert(file.id(), file);
}
}
Ok(cache) as Result<Cache, Error>
})
.await?;
Ok(cache)
}
async fn files_for_profile(
profile: &Profile,
mut cache: Cache,
state: &State,
) -> Result<Cache, Error> {
let file_ids: Vec<Uuid> = profile
.icon()
.iter()
.chain(profile.banner().iter())
.copied()
.collect();
let store = state.profiles.clone();
let cache = web::block(move || {
for file_id in file_ids {
if !cache.files.contains_key(&file_id) {
let file = store.store.files.by_id(file_id)?.req()?;
cache.files.insert(file.id(), file);
}
}
Ok(cache)
})
.await?;
Ok(cache)
}
fn submission_nav(
page: Option<FileQuery>,
submission: &Submission,
next_submission: Option<Uuid>,
previous_submission: Option<Uuid>,
files: &HashMap<Uuid, File>,
) -> (Vec<Button>, Uuid) {
let file_count = submission.files().len();
let file_num: usize = if let Some(page) = page {
file_count.saturating_sub(1).min(page.file_page)
} else {
0
};
let current_file_id = submission.files()[file_num];
let current_file = files.get(&current_file_id);
let mut nav = vec![];
if let Some(prev) = previous_submission {
nav.push(Button::secondary("Previous").href(&format!("/submissions/{}", prev)));
}
if let Some(key) = current_file.and_then(|file| file.pictrs_key()) {
nav.push(Button::secondary("Download").href(&largest_image(key, ImageType::Png)));
}
nav.push(Button::primary_outline("Report").href(&submission.report_path()));
if let Some(next) = next_submission {
nav.push(Button::secondary("Next").href(&format!("/submissions/{}", next)));
}
(nav, current_file_id)
}
async fn adjacent_submissions(
viewer: Option<Uuid>,
can_view_sensitive: bool,
submission: &Submission,
state: &State,
) -> Result<(Option<Uuid>, Option<Uuid>), Error> {
let submission_id = submission.id();
let is_published = submission.published().is_some();
let store = state.profiles.clone();
let (next, prev) = web::block(move || {
if is_published {
let inner_store = store.clone();
let prev = store
.store
.submissions
.published_newer_than_for_profile(submission_id)
.filter(|id| *id != submission_id)
.find_map(move |id| {
let submission = inner_store.store.submissions.by_id(id).ok()??;
crate::pagination::submission::can_view(
viewer,
&submission,
&inner_store.store,
&mut Default::default(),
true,
can_view_sensitive,
)
.map(move |_| id)
});
let next = store
.store
.submissions
.published_older_than_for_profile(submission_id)
.filter(|id| *id != submission_id)
.find_map(move |id| {
let submission = store.store.submissions.by_id(id).ok()??;
crate::pagination::submission::can_view(
viewer,
&submission,
&store.store,
&mut Default::default(),
true,
can_view_sensitive,
)
.map(move |_| id)
});
Ok((next, prev)) as Result<_, Error>
} else {
let inner_store = store.clone();
let prev = store
.store
.submissions
.drafted_newer_than_for_profile(submission_id)
.filter(|id| *id != submission_id)
.find_map(move |id| {
let submission = inner_store.store.submissions.by_id(id).ok()??;
crate::pagination::submission::can_view(
viewer,
&submission,
&inner_store.store,
&mut Default::default(),
true,
can_view_sensitive,
)
.map(move |_| id)
});
let next = store
.store
.submissions
.drafted_older_than_for_profile(submission_id)
.filter(|id| *id != submission_id)
.find_map(move |id| {
let submission = store.store.submissions.by_id(id).ok()??;
crate::pagination::submission::can_view(
viewer,
&submission,
&store.store,
&mut Default::default(),
true,
can_view_sensitive,
)
.map(move |_| id)
});
Ok((next, prev)) as Result<_, Error>
}
})
.await?;
Ok((next, prev))
}
async fn submission_page(
loader: ActixLoader,
profile: Option<UserProfile>,
submission: Option<CurrentSubmission>,
page: Option<web::Query<FileQuery>>,
nav_state: NavState,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
let submission = submission.req()?.0;
let profile = profile.map(|p| p.0);
let viewer = profile.as_ref().map(|p| p.id());
let is_self = viewer
.map(|id| id == submission.profile_id())
.unwrap_or(false);
let poster_id = submission.profile_id();
let store = state.profiles.clone();
let poster = web::block(move || store.store.profiles.by_id(poster_id)?.req()).await?;
let cache = Cache::new();
let cache = files_for_submission(&submission, cache, &state).await?;
let cache = files_for_profile(&poster, cache, &state).await?;
let can_view_sensitive = if let Some(viewer) = viewer {
state.settings.for_profile(viewer).await?.sensitive
} else {
false
};
if let Some(res) = can_view(viewer, can_view_sensitive, &submission, &state).await? {
return Ok(res);
}
let (next_submission, previous_submission) =
adjacent_submissions(viewer, can_view_sensitive, &submission, &state).await?;
let (nav, current_file) = submission_nav(
page.map(|p| p.into_inner()),
&submission,
next_submission,
previous_submission,
&cache.files,
);
let (comments, cache) =
CommentNode::from_submission(submission.id(), viewer, cache, &state).await?;
let view = ViewSubmissionState {
cache,
profile,
submission,
poster,
current_file,
nav,
is_self,
comment_value: None,
comment_error: None,
comments,
};
crate::rendered(HttpResponse::Ok(), |cursor| {
crate::templates::submissions::public(cursor, &loader, &view, &nav_state)
})
}
async fn update_page(
loader: ActixLoader,
submission: CurrentSubmission,
profile: UserProfile,
nav_state: NavState,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
let submission = submission.0;
let profile = profile.0;
if submission.profile_id() != profile.id() {
return Ok(crate::to_404());
}
let state = SubmissionState::new(submission, &state).await?;
crate::rendered(HttpResponse::Ok(), |cursor| {
crate::templates::submissions::update(cursor, &loader, &state, &nav_state)
})
}
const MAX_TITLE_LEN: usize = 50;
const MAX_DESCRIPTION_LEN: usize = 500;
const MAX_COMMENT_LEN: usize = 500;
fn validate_title(title: &str) -> Option<String> {
if title.len() > MAX_TITLE_LEN {
return Some(format!("Must be shorter than {} characters", MAX_TITLE_LEN));
}
if title.trim().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
}
#[derive(Clone, Debug, serde::Deserialize)]
struct CommentForm {
body: String,
}
async fn create_comment(
loader: ActixLoader,
form: web::Form<CommentForm>,
submission: CurrentSubmission,
profile: UserProfile,
nav_state: NavState,
page: Option<web::Query<FileQuery>>,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
let submission = submission.0;
let profile = profile.0;
let form = form.into_inner();
let store = state.profiles.clone();
let poster_id = submission.profile_id();
let poster = web::block(move || store.store.profiles.by_id(poster_id)?.req()).await?;
let can_view_sensitive = state.settings.for_profile(profile.id()).await?.sensitive;
if let Some(res) = can_view(Some(profile.id()), can_view_sensitive, &submission, &state).await?
{
return Ok(res);
}
let error = if form.body.trim().is_empty() {
"Must be present".to_string()
} else if form.body.len() > MAX_COMMENT_LEN {
format!("Must be shorter than {} characters", MAX_COMMENT_LEN)
} else {
let res = state
.profiles
.run(CreateComment::from_text(
submission.id(),
profile.id(),
None,
form.body.clone(),
))
.await;
match res {
Ok(_) => return Ok(to_submission_page(submission.id())),
Err(e) => e.to_string(),
}
};
let cache = Cache::new();
let cache = files_for_submission(&submission, cache, &state).await?;
let cache = files_for_profile(&poster, cache, &state).await?;
let (next_submission, previous_submission) =
adjacent_submissions(Some(profile.id()), can_view_sensitive, &submission, &state).await?;
let (nav, current_file) = submission_nav(
page.map(|p| p.into_inner()),
&submission,
next_submission,
previous_submission,
&cache.files,
);
let (comments, cache) =
CommentNode::from_submission(submission.id(), Some(profile.id()), cache, &state).await?;
let is_self = profile.id() == poster.id();
let view = ViewSubmissionState {
cache,
profile: Some(profile),
submission,
poster,
current_file,
nav,
is_self,
comment_error: Some(error),
comment_value: Some(form.body),
comments,
};
crate::rendered(HttpResponse::Ok(), |cursor| {
crate::templates::submissions::public(cursor, &loader, &view, &nav_state)
})
}
#[derive(Clone, Debug, serde::Deserialize)]
struct UpdateSubmissionForm {
title: String,
description: String,
}
async fn update_submission(
loader: ActixLoader,
form: web::Form<UpdateSubmissionForm>,
submission: CurrentSubmission,
profile: UserProfile,
nav_state: NavState,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
let submission = submission.0;
let profile = profile.0;
let form = form.into_inner();
if profile.id() != submission.profile_id() {
return Ok(crate::to_404());
}
let mut title_error = validate_title(&form.title);
let description_error = validate_description(&form.description);
if title_error.is_none() && description_error.is_none() {
let res = state
.profiles
.run(UpdateSubmission::from_text(
submission.id(),
form.title.clone(),
Some(form.description.clone()),
))
.await;
title_error = match res {
Ok(_) => return Ok(to_update_page(submission.id())),
Err(e) => Some(e.to_string()),
};
}
let state = SubmissionState::new(submission, &state)
.await?
.description_error(description_error)
.title(&form.title)
.description(&form.description);
let state = if let Some(error) = title_error {
state.title_error(error)
} else {
state
};
crate::rendered(HttpResponse::BadRequest(), |cursor| {
crate::templates::submissions::update(cursor, &loader, &state, &nav_state)
})
}
#[derive(Clone, Debug, serde::Deserialize)]
struct UpdateVisibilityForm {
visibility: Visibility,
local_only: Option<String>,
logged_in_only: Option<String>,
}
async fn update_visibility(
loader: ActixLoader,
form: web::Form<UpdateVisibilityForm>,
submission: CurrentSubmission,
profile: UserProfile,
nav_state: NavState,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
let submission = submission.0;
let profile = profile.0;
let form = form.into_inner();
if profile.id() != submission.profile_id() {
return Ok(crate::to_404());
}
let res = state
.profiles
.run(UpdateSubmission::from_visibility(
submission.id(),
form.visibility,
form.local_only.is_some(),
form.logged_in_only.is_some(),
))
.await;
let error = match res {
Ok(_) => return Ok(to_update_page(submission.id())),
Err(e) => e.to_string(),
};
let state = SubmissionState::new(submission, &state)
.await?
.visibility_error(error);
crate::rendered(HttpResponse::BadRequest(), |cursor| {
crate::templates::submissions::update(cursor, &loader, &state, &nav_state)
})
}
#[derive(Clone, Debug, serde::Deserialize)]
struct UpdateSensitiveForm {
sensitive: Option<String>,
}
async fn update_sensitive(
loader: ActixLoader,
form: web::Form<UpdateSensitiveForm>,
submission: CurrentSubmission,
profile: UserProfile,
nav_state: NavState,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
let submission = submission.0;
let profile = profile.0;
let form = form.into_inner();
if profile.id() != submission.profile_id() {
return Ok(crate::to_404());
}
let res = state
.profiles
.run(UpdateSubmission::from_sensitive(
submission.id(),
form.sensitive.is_some(),
))
.await;
let error = match res {
Ok(_) => return Ok(to_update_page(submission.id())),
Err(e) => e.to_string(),
};
let state = SubmissionState::new(submission, &state)
.await?
.sensitive_error(error);
crate::rendered(HttpResponse::BadRequest(), |cursor| {
crate::templates::submissions::update(cursor, &loader, &state, &nav_state)
})
}
async fn add_file(
loader: ActixLoader,
req: HttpRequest,
pl: web::Payload,
submission: CurrentSubmission,
profile: UserProfile,
nav_state: NavState,
client: web::Data<Client>,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
let submission = submission.0;
let profile = profile.0;
if profile.id() != submission.profile_id() {
return Ok(crate::to_404());
}
let res = state
.profiles
.upload_image(req, pl.into_inner(), &client)
.await;
let error = match res {
Ok(files) if files.len() == 1 => {
let res = state
.profiles
.run(UpdateSubmission::from_new_file(submission.id(), files[0]))
.await;
match res {
Ok(_) => return Ok(to_update_page(submission.id())),
Err(e) => e.to_string(),
}
}
Ok(_) => "Invalid number of files".to_owned(),
Err(e) => e.to_string(),
};
let state = SubmissionState::new(submission, &state)
.await?
.file_error(error);
crate::rendered(HttpResponse::BadRequest(), |cursor| {
crate::templates::submissions::update(cursor, &loader, &state, &nav_state)
})
}
#[derive(Clone, Debug, serde::Deserialize)]
struct RemoveFileForm {
file_id: Uuid,
}
async fn remove_file(
loader: ActixLoader,
form: web::Form<RemoveFileForm>,
submission: CurrentSubmission,
profile: UserProfile,
nav_state: NavState,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
let submission = submission.0;
let profile = profile.0;
if profile.id() != submission.profile_id() {
return Ok(crate::to_404());
}
let res = state
.profiles
.run(UpdateSubmission::from_removed_file(
submission.id(),
form.file_id,
))
.await;
let error = match res {
Ok(_) => return Ok(to_update_page(submission.id())),
Err(e) => e.to_string(),
};
let state = SubmissionState::new(submission, &state)
.await?
.remove_file_error(form.file_id, error);
crate::rendered(HttpResponse::BadRequest(), |cursor| {
crate::templates::submissions::update(cursor, &loader, &state, &nav_state)
})
}
async fn publish_submission(
loader: ActixLoader,
submission: CurrentSubmission,
profile: UserProfile,
nav_state: NavState,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
let submission = submission.0;
let profile = profile.0;
if profile.id() != submission.profile_id() {
return Ok(crate::to_404());
}
let error = if !submission.has_title() {
"Title must be present".to_owned()
} else {
let res = state
.profiles
.run(UpdateSubmission::publish_now(submission.id()))
.await;
match res {
Ok(_) => return Ok(to_submission_page(submission.id())),
Err(e) => e.to_string(),
}
};
let state = SubmissionState::new(submission, &state)
.await?
.publish_error(error);
crate::rendered(HttpResponse::BadRequest(), |cursor| {
crate::templates::submissions::update(cursor, &loader, &state, &nav_state)
})
}
async fn delete_submission(
loader: ActixLoader,
submission: CurrentSubmission,
profile: UserProfile,
nav_state: NavState,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
let submission = submission.0;
let profile = profile.0;
if profile.id() != submission.profile_id() {
return Ok(crate::to_404());
}
let res = state
.profiles
.run(DeleteSubmission::from_id(submission.id()))
.await;
let error = match res {
Ok(_) => return Ok(crate::to_home()),
Err(e) => e.to_string(),
};
let state = SubmissionState::new(submission, &state)
.await?
.delete_error(error);
crate::rendered(HttpResponse::BadRequest(), |cursor| {
crate::templates::submissions::update(cursor, &loader, &state, &nav_state)
})
}
async fn route_to_update_page(path: web::Path<Uuid>) -> HttpResponse {
to_update_page(path.into_inner())
}
fn to_update_page(id: Uuid) -> HttpResponse {
crate::redirect(&format!("/submissions/{}/update", id))
}
fn to_submission_page(id: Uuid) -> HttpResponse {
crate::redirect(&format!("/submissions/{}", id))
}
fn file_input(loader: &ActixLoader) -> FileInput {
FileInput::secondary("images[]", &fl!(loader, "create-submission-input"))
}