2021-01-14 04:46:34 +00:00
|
|
|
use crate::{
|
|
|
|
comments::Comment,
|
|
|
|
error::{Error, OptionExt},
|
|
|
|
nav::NavState,
|
|
|
|
profiles::Profile,
|
|
|
|
submissions::Submission,
|
|
|
|
State,
|
|
|
|
};
|
|
|
|
use actix_web::{dev::Payload, web, FromRequest, HttpRequest, HttpResponse, Scope};
|
|
|
|
use chrono::{DateTime, Utc};
|
2021-01-15 05:51:17 +00:00
|
|
|
use futures::future::LocalBoxFuture;
|
2021-01-14 04:46:34 +00:00
|
|
|
use hyaenidae_accounts::{State as AccountState, User};
|
2021-01-16 17:49:03 +00:00
|
|
|
use hyaenidae_profiles::store::{ReportKind, Server};
|
2021-01-14 04:46:34 +00:00
|
|
|
use hyaenidae_toolkit::{Select, TextInput};
|
|
|
|
use sled::{Db, Transactional, Tree};
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use uuid::Uuid;
|
|
|
|
|
|
|
|
pub use hyaenidae_profiles::store::Report;
|
|
|
|
|
|
|
|
pub(super) fn scope() -> Scope {
|
|
|
|
web::scope("/admin")
|
|
|
|
.service(web::resource("").route(web::get().to(admin_page)))
|
2021-01-16 17:49:03 +00:00
|
|
|
.service(
|
|
|
|
web::resource("/server")
|
|
|
|
.route(web::get().to(to_admin))
|
|
|
|
.route(web::post().to(update_server)),
|
|
|
|
)
|
2021-01-14 04:46:34 +00:00
|
|
|
.service(
|
|
|
|
web::resource("/reports/{report_id}")
|
|
|
|
.route(web::get().to(view_report))
|
|
|
|
.route(web::post().to(close_report)),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2021-01-16 17:49:03 +00:00
|
|
|
#[derive(Clone, Debug, serde::Deserialize)]
|
|
|
|
struct ServerForm {
|
|
|
|
title: String,
|
|
|
|
description: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn update_server(
|
|
|
|
_: Admin,
|
|
|
|
form: web::Form<ServerForm>,
|
|
|
|
state: web::Data<State>,
|
|
|
|
) -> Result<HttpResponse, Error> {
|
|
|
|
use hyaenidae_profiles::apub::actions::UpdateServer;
|
|
|
|
let ServerForm { title, description } = form.into_inner();
|
|
|
|
|
|
|
|
let server_view = ServerView::build(&state).await?;
|
|
|
|
|
|
|
|
state
|
|
|
|
.profiles
|
|
|
|
.run(UpdateServer::from_text(
|
|
|
|
server_view.server.id(),
|
|
|
|
Some(title),
|
|
|
|
Some(description),
|
|
|
|
))
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
Ok(to_admin())
|
|
|
|
}
|
|
|
|
|
2021-01-14 04:46:34 +00:00
|
|
|
async fn view_report(
|
|
|
|
_: Admin,
|
|
|
|
report: web::Path<Uuid>,
|
|
|
|
nav_state: NavState,
|
|
|
|
state: web::Data<State>,
|
|
|
|
) -> Result<HttpResponse, Error> {
|
|
|
|
let report_id = report.into_inner();
|
|
|
|
let report_store = state.profiles.store.reports.clone();
|
|
|
|
let report = web::block(move || Ok(report_store.by_id(report_id)?)).await?;
|
|
|
|
|
2021-01-16 17:49:03 +00:00
|
|
|
let report_view = ReportView::new(report, nav_state.dark(), state).await?;
|
2021-01-14 04:46:34 +00:00
|
|
|
|
|
|
|
crate::rendered(HttpResponse::Ok(), |cursor| {
|
2021-01-16 17:49:03 +00:00
|
|
|
crate::templates::admin::report(cursor, &report_view, &nav_state)
|
2021-01-14 04:46:34 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, serde::Deserialize)]
|
|
|
|
enum CloseAction {
|
|
|
|
Delete,
|
|
|
|
Suspend,
|
|
|
|
Ignore,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl std::fmt::Display for CloseAction {
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
|
|
match self {
|
|
|
|
CloseAction::Delete => write!(f, "Delete"),
|
|
|
|
CloseAction::Suspend => write!(f, "Suspend"),
|
|
|
|
CloseAction::Ignore => write!(f, "Ignore"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, serde::Deserialize)]
|
|
|
|
struct CloseForm {
|
|
|
|
action: CloseAction,
|
|
|
|
body: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn close_report(
|
|
|
|
admin: Admin,
|
|
|
|
form: web::Form<CloseForm>,
|
|
|
|
report: web::Path<Uuid>,
|
|
|
|
account_state: web::Data<AccountState>,
|
|
|
|
nav_state: NavState,
|
|
|
|
state: web::Data<State>,
|
|
|
|
) -> Result<HttpResponse, Error> {
|
|
|
|
let report_id = report.into_inner();
|
|
|
|
let report_store = state.profiles.store.reports.clone();
|
|
|
|
let report = web::block(move || Ok(report_store.by_id(report_id)?)).await?;
|
|
|
|
|
|
|
|
let form = form.into_inner();
|
|
|
|
|
|
|
|
let error = if form.body.trim().is_empty() {
|
|
|
|
Some("Reports must be resolved with a resolution message".to_owned())
|
|
|
|
} else {
|
|
|
|
match handle_report(form.action, &report, account_state, &state).await {
|
|
|
|
Ok(_) => None,
|
|
|
|
Err(e) => Some(e.to_string()),
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
let error = if let Some(error) = error {
|
|
|
|
error
|
|
|
|
} else {
|
|
|
|
match resolve_report(
|
|
|
|
admin.id(),
|
|
|
|
report.id(),
|
|
|
|
format!("{}: {}", form.action, form.body),
|
|
|
|
&state,
|
|
|
|
)
|
|
|
|
.await
|
|
|
|
{
|
|
|
|
Ok(_) => return Ok(to_admin()),
|
|
|
|
Err(e) => e.to_string(),
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
let mut view = ReportView::new(report, nav_state.dark(), state).await?;
|
|
|
|
view.input.error_opt(Some(error));
|
|
|
|
|
|
|
|
crate::rendered(HttpResponse::Ok(), |cursor| {
|
|
|
|
crate::templates::admin::report(cursor, &view, &nav_state)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn handle_report(
|
|
|
|
form_action: CloseAction,
|
|
|
|
report: &Report,
|
|
|
|
account_state: web::Data<AccountState>,
|
|
|
|
state: &State,
|
|
|
|
) -> Result<(), Error> {
|
|
|
|
match form_action {
|
|
|
|
CloseAction::Ignore => Ok(()),
|
|
|
|
CloseAction::Suspend => {
|
|
|
|
let profile_id = match report.kind() {
|
|
|
|
ReportKind::Profile => report.item(),
|
|
|
|
ReportKind::Submission => {
|
|
|
|
let submission_id = report.item();
|
|
|
|
let submission_store = state.profiles.store.submissions.clone();
|
|
|
|
let submission = web::block(move || Ok(submission_store.by_id(submission_id)?))
|
|
|
|
.await?
|
|
|
|
.req()?;
|
|
|
|
submission.profile_id()
|
|
|
|
}
|
|
|
|
ReportKind::Comment => {
|
|
|
|
let comment_id = report.item();
|
|
|
|
let comment_store = state.profiles.store.comments.clone();
|
|
|
|
let comment = web::block(move || Ok(comment_store.by_id(comment_id)?))
|
|
|
|
.await?
|
|
|
|
.req()?;
|
|
|
|
comment.profile_id()
|
|
|
|
}
|
|
|
|
_ => unimplemented!("Profile ID can't be fetched for report kind"),
|
|
|
|
};
|
|
|
|
|
|
|
|
let profile_store = state.profiles.store.profiles.clone();
|
|
|
|
let profile = web::block(move || Ok(profile_store.by_id(profile_id)?))
|
|
|
|
.await?
|
|
|
|
.req()?;
|
|
|
|
|
|
|
|
use hyaenidae_profiles::apub::actions::SuspendProfile;
|
|
|
|
if let Some(user_id) = profile.local_owner() {
|
|
|
|
account_state.suspend(user_id).await?;
|
|
|
|
|
|
|
|
let profile_store = state.profiles.store.profiles.clone();
|
|
|
|
let profiles = web::block(move || {
|
|
|
|
let profiles: Vec<_> = profile_store.for_local(user_id).collect();
|
|
|
|
|
|
|
|
Ok(profiles) as Result<Vec<_>, Error>
|
|
|
|
})
|
|
|
|
.await?;
|
|
|
|
for profile in profiles {
|
|
|
|
state.profiles.run(SuspendProfile::from_id(profile)).await?;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
state
|
|
|
|
.profiles
|
|
|
|
.run(SuspendProfile::from_id(profile_id))
|
|
|
|
.await?;
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
CloseAction::Delete => match report.kind() {
|
|
|
|
ReportKind::Profile => {
|
|
|
|
use hyaenidae_profiles::apub::actions::DeleteProfile;
|
|
|
|
|
|
|
|
state
|
|
|
|
.profiles
|
|
|
|
.run(DeleteProfile::from_id(report.item()))
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
ReportKind::Submission => {
|
|
|
|
use hyaenidae_profiles::apub::actions::DeleteSubmission;
|
|
|
|
|
|
|
|
state
|
|
|
|
.profiles
|
|
|
|
.run(DeleteSubmission::from_id(report.item()))
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
ReportKind::Comment => {
|
|
|
|
use hyaenidae_profiles::apub::actions::DeleteComment;
|
|
|
|
|
|
|
|
state
|
|
|
|
.profiles
|
|
|
|
.run(DeleteComment::from_id(report.item()))
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
_ => Ok(()),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn to_admin() -> HttpResponse {
|
|
|
|
crate::redirect("/admin")
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn admin_page(
|
|
|
|
_: Admin,
|
|
|
|
nav_state: NavState,
|
|
|
|
state: web::Data<State>,
|
|
|
|
) -> Result<HttpResponse, Error> {
|
2021-01-16 17:49:03 +00:00
|
|
|
let server_view = ServerView::build(&state).await?;
|
2021-01-14 04:46:34 +00:00
|
|
|
let open_reports = ReportsView::new(state).await?;
|
|
|
|
|
|
|
|
crate::rendered(HttpResponse::Ok(), |cursor| {
|
2021-01-16 17:49:03 +00:00
|
|
|
crate::templates::admin::index(cursor, &open_reports, &server_view, &nav_state)
|
2021-01-14 04:46:34 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
enum ReportedItem {
|
|
|
|
Submission {
|
|
|
|
submission: Submission,
|
|
|
|
author: Profile,
|
|
|
|
},
|
|
|
|
Profile(Profile),
|
|
|
|
Comment {
|
|
|
|
comment: Comment,
|
|
|
|
author: Profile,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
pub struct ReportView {
|
|
|
|
report: Report,
|
|
|
|
reported_item: ReportedItem,
|
|
|
|
author: Option<Profile>,
|
|
|
|
select: Select,
|
|
|
|
input: TextInput,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl ReportView {
|
|
|
|
pub(crate) fn id(&self) -> Uuid {
|
|
|
|
self.report.id()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn note(&self) -> Option<&str> {
|
|
|
|
self.report.note()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn select(&self) -> &Select {
|
|
|
|
&self.select
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn input(&self) -> &TextInput {
|
|
|
|
&self.input
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn update_path(&self) -> String {
|
|
|
|
format!("/admin/reports/{}", self.id())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn author(&self) -> Option<&Profile> {
|
|
|
|
self.author.as_ref()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn comment<'a>(&'a self) -> Option<CommentView<'a>> {
|
|
|
|
match &self.reported_item {
|
|
|
|
ReportedItem::Comment { comment, author } => Some(CommentView { comment, author }),
|
|
|
|
_ => None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn submission<'a>(&'a self) -> Option<SubmissionView<'a>> {
|
|
|
|
match &self.reported_item {
|
|
|
|
ReportedItem::Submission { submission, author } => {
|
|
|
|
Some(SubmissionView { submission, author })
|
|
|
|
}
|
|
|
|
_ => None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn profile(&self) -> Option<&Profile> {
|
|
|
|
match &self.reported_item {
|
|
|
|
ReportedItem::Profile(ref profile) => Some(profile),
|
|
|
|
_ => None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn new(report: Report, dark: bool, state: web::Data<State>) -> Result<Self, Error> {
|
|
|
|
let author = if let Some(author_id) = report.reporter_profile() {
|
|
|
|
let profile_store = state.profiles.store.profiles.clone();
|
|
|
|
let file_store = state.profiles.store.files.clone();
|
|
|
|
let author =
|
|
|
|
web::block(move || Profile::from_stores(author_id, &profile_store, &file_store))
|
|
|
|
.await?;
|
|
|
|
Some(author)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
};
|
|
|
|
|
|
|
|
let reported_item = match report.kind() {
|
|
|
|
ReportKind::Profile => {
|
|
|
|
let profile_id = report.item();
|
|
|
|
let profile_store = state.profiles.store.profiles.clone();
|
|
|
|
let file_store = state.profiles.store.files.clone();
|
|
|
|
let profile = web::block(move || {
|
|
|
|
Profile::from_stores(profile_id, &profile_store, &file_store)
|
|
|
|
})
|
|
|
|
.await?;
|
|
|
|
ReportedItem::Profile(profile)
|
|
|
|
}
|
|
|
|
ReportKind::Submission => {
|
|
|
|
let submission_id = report.item();
|
|
|
|
let submission_store = state.profiles.store.submissions.clone();
|
|
|
|
let file_store = state.profiles.store.files.clone();
|
|
|
|
let submission = web::block(move || {
|
|
|
|
Submission::from_stores(submission_id, &submission_store, &file_store)
|
|
|
|
})
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
let profile_id = submission.profile_id();
|
|
|
|
let profile_store = state.profiles.store.profiles.clone();
|
|
|
|
let file_store = state.profiles.store.files.clone();
|
|
|
|
let author = web::block(move || {
|
|
|
|
Profile::from_stores(profile_id, &profile_store, &file_store)
|
|
|
|
})
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
ReportedItem::Submission { submission, author }
|
|
|
|
}
|
|
|
|
ReportKind::Comment => {
|
|
|
|
let comment_id = report.item();
|
|
|
|
let comment_store = state.profiles.store.comments.clone();
|
|
|
|
let comment = web::block(move || Ok(comment_store.by_id(comment_id)?))
|
|
|
|
.await?
|
|
|
|
.req()?;
|
|
|
|
|
|
|
|
let profile_id = comment.profile_id();
|
|
|
|
let profile_store = state.profiles.store.profiles.clone();
|
|
|
|
let file_store = state.profiles.store.files.clone();
|
|
|
|
let author = web::block(move || {
|
|
|
|
Profile::from_stores(profile_id, &profile_store, &file_store)
|
|
|
|
})
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
ReportedItem::Comment { comment, author }
|
|
|
|
}
|
|
|
|
_ => None.req()?,
|
|
|
|
};
|
|
|
|
|
|
|
|
let mut input = TextInput::new("body");
|
|
|
|
input
|
|
|
|
.textarea()
|
|
|
|
.title("Resolution Message")
|
|
|
|
.placeholder("Explain why you're resolving this report")
|
|
|
|
.dark(dark);
|
|
|
|
|
|
|
|
let mut select = Select::new("action");
|
|
|
|
select
|
|
|
|
.title("Resolution")
|
|
|
|
.options(&[
|
|
|
|
("Close Report", "Ignore"),
|
|
|
|
("Delete Offending Item", "Delete"),
|
|
|
|
("Suspend Account", "Suspend"),
|
|
|
|
])
|
|
|
|
.default_option("Ignore")
|
|
|
|
.dark(dark);
|
|
|
|
|
|
|
|
Ok(ReportView {
|
|
|
|
report,
|
|
|
|
author,
|
|
|
|
reported_item,
|
|
|
|
input,
|
|
|
|
select,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-16 17:49:03 +00:00
|
|
|
pub struct ServerView {
|
|
|
|
server: Server,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl ServerView {
|
|
|
|
pub(crate) fn title_input(&self, dark: bool) -> TextInput {
|
|
|
|
let mut title = TextInput::new("title");
|
|
|
|
title.title("Title").placeholder("Server Title").dark(dark);
|
|
|
|
|
|
|
|
if let Some(text) = self.server.title() {
|
|
|
|
title.value(text);
|
|
|
|
}
|
|
|
|
|
|
|
|
title
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn description_input(&self, dark: bool) -> TextInput {
|
|
|
|
let mut description = TextInput::new("description");
|
|
|
|
description
|
|
|
|
.title("Description")
|
|
|
|
.placeholder("Server Description")
|
|
|
|
.textarea()
|
|
|
|
.dark(dark);
|
|
|
|
|
|
|
|
if let Some(text) = self.server.description() {
|
|
|
|
description.value(text);
|
|
|
|
}
|
|
|
|
|
|
|
|
description
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn update_path(&self) -> &str {
|
|
|
|
"/admin/server"
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn build(state: &State) -> Result<Self, Error> {
|
|
|
|
let servers = state.profiles.store.servers.clone();
|
|
|
|
|
|
|
|
let self_server = web::block(move || {
|
|
|
|
let id = servers.get_self()?.req()?;
|
|
|
|
servers.by_id(id)?.req()
|
|
|
|
})
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
Ok(ServerView {
|
|
|
|
server: self_server,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-14 04:46:34 +00:00
|
|
|
pub struct ReportsView {
|
|
|
|
reports: Vec<Report>,
|
|
|
|
profiles: HashMap<Uuid, Profile>,
|
|
|
|
submissions: HashMap<Uuid, Submission>,
|
|
|
|
comments: HashMap<Uuid, Comment>,
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) struct CommentView<'a> {
|
|
|
|
pub(crate) author: &'a Profile,
|
|
|
|
pub(crate) comment: &'a Comment,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> CommentView<'a> {
|
|
|
|
pub(crate) fn view_path(&self) -> String {
|
|
|
|
crate::comments::comment_path(self.comment)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn body(&self) -> &str {
|
|
|
|
self.comment.body()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn author_name(&self) -> String {
|
|
|
|
self.author.name()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn author_path(&self) -> String {
|
|
|
|
self.author.view_path()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) struct SubmissionView<'a> {
|
|
|
|
pub(crate) author: &'a Profile,
|
|
|
|
pub(crate) submission: &'a Submission,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> SubmissionView<'a> {
|
|
|
|
pub(crate) fn view_path(&self) -> String {
|
|
|
|
self.submission.view_path()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn title(&self) -> String {
|
|
|
|
self.submission.title()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn author_name(&self) -> String {
|
|
|
|
self.author.name()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn author_path(&self) -> String {
|
|
|
|
self.author.view_path()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn tiles(&self) -> Vec<crate::profiles::SubmissionView> {
|
|
|
|
self.submission
|
|
|
|
.files()
|
|
|
|
.iter()
|
|
|
|
.map(|f| {
|
|
|
|
crate::profiles::SubmissionView::from_parts(&self.submission.inner, self.author, f)
|
|
|
|
})
|
|
|
|
.collect()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl ReportsView {
|
|
|
|
pub(crate) fn reports(&self) -> &[Report] {
|
|
|
|
&self.reports
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn view_path(&self, report: &Report) -> String {
|
|
|
|
format!("/admin/reports/{}", report.id())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn reporter_profile<'a>(&'a self, report: &Report) -> Option<&'a Profile> {
|
|
|
|
let author_id = report.reporter_profile()?;
|
|
|
|
self.profiles.get(&author_id)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn profile<'a>(&'a self, report: &Report) -> Option<&'a Profile> {
|
|
|
|
let profile_id = report.profile()?;
|
|
|
|
self.profiles.get(&profile_id)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn submission<'a>(&'a self, report: &Report) -> Option<SubmissionView<'a>> {
|
|
|
|
let submission_id = report.submission()?;
|
|
|
|
let submission = self.submissions.get(&submission_id)?;
|
|
|
|
let author_id = submission.profile_id();
|
|
|
|
let author = self.profiles.get(&author_id)?;
|
|
|
|
|
|
|
|
Some(SubmissionView { author, submission })
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn comment<'a>(&'a self, report: &Report) -> Option<CommentView<'a>> {
|
|
|
|
let comment_id = report.comment()?;
|
|
|
|
let comment = self.comments.get(&comment_id)?;
|
|
|
|
let author_id = comment.profile_id();
|
|
|
|
let author = self.profiles.get(&author_id)?;
|
|
|
|
|
|
|
|
Some(CommentView { author, comment })
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn new(state: web::Data<State>) -> Result<Self, Error> {
|
|
|
|
let report_store = state.profiles.store.reports.clone();
|
|
|
|
let profile_store = state.profiles.store.profiles.clone();
|
|
|
|
let submission_store = state.profiles.store.submissions.clone();
|
|
|
|
let comment_store = state.profiles.store.comments.clone();
|
|
|
|
let file_store = state.profiles.store.files.clone();
|
|
|
|
|
|
|
|
let view = web::block(move || {
|
|
|
|
let mut profiles = HashMap::new();
|
|
|
|
let mut submissions = HashMap::new();
|
|
|
|
let mut comments = HashMap::new();
|
|
|
|
|
|
|
|
let reports = report_store
|
|
|
|
.all()
|
|
|
|
.filter_map(|id| {
|
|
|
|
let report = report_store.by_id(id).ok()?;
|
|
|
|
|
|
|
|
if let Some(id) = report.reporter_profile() {
|
|
|
|
if !profiles.contains_key(&id) {
|
|
|
|
let profile =
|
|
|
|
Profile::from_stores(id, &profile_store, &file_store).ok()?;
|
|
|
|
profiles.insert(profile.id(), profile);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
match report.kind() {
|
|
|
|
ReportKind::Profile => {
|
|
|
|
if !profiles.contains_key(&report.item()) {
|
|
|
|
let profile = Profile::from_stores(
|
|
|
|
report.item(),
|
|
|
|
&profile_store,
|
|
|
|
&file_store,
|
|
|
|
)
|
|
|
|
.ok()?;
|
|
|
|
profiles.insert(profile.id(), profile);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ReportKind::Submission => {
|
|
|
|
if !submissions.contains_key(&report.item()) {
|
|
|
|
let submission = Submission::from_stores(
|
|
|
|
report.item(),
|
|
|
|
&submission_store,
|
|
|
|
&file_store,
|
|
|
|
)
|
|
|
|
.ok()?;
|
|
|
|
if !profiles.contains_key(&submission.profile_id()) {
|
|
|
|
let profile = Profile::from_stores(
|
|
|
|
submission.profile_id(),
|
|
|
|
&profile_store,
|
|
|
|
&file_store,
|
|
|
|
)
|
|
|
|
.ok()?;
|
|
|
|
profiles.insert(profile.id(), profile);
|
|
|
|
}
|
|
|
|
submissions.insert(submission.id(), submission);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ReportKind::Comment => {
|
|
|
|
if !comments.contains_key(&report.item()) {
|
|
|
|
let comment = comment_store.by_id(report.item()).ok()??;
|
|
|
|
if !profiles.contains_key(&comment.profile_id()) {
|
|
|
|
let profile = Profile::from_stores(
|
|
|
|
comment.profile_id(),
|
|
|
|
&profile_store,
|
|
|
|
&file_store,
|
|
|
|
)
|
|
|
|
.ok()?;
|
|
|
|
profiles.insert(profile.id(), profile);
|
|
|
|
}
|
|
|
|
comments.insert(comment.id(), comment);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_ => (),
|
|
|
|
}
|
|
|
|
|
|
|
|
Some(report)
|
|
|
|
})
|
|
|
|
.rev()
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
Ok(ReportsView {
|
|
|
|
reports,
|
|
|
|
profiles,
|
|
|
|
submissions,
|
|
|
|
comments,
|
|
|
|
}) as Result<_, Error>
|
|
|
|
})
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
Ok(view)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) struct Admin(User);
|
|
|
|
|
|
|
|
impl Admin {
|
|
|
|
fn id(&self) -> Uuid {
|
|
|
|
self.0.id()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl FromRequest for Admin {
|
|
|
|
type Config = ();
|
|
|
|
type Error = actix_web::Error;
|
|
|
|
type Future = LocalBoxFuture<'static, Result<Self, Self::Error>>;
|
|
|
|
|
|
|
|
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
|
|
|
|
let user_fut = User::extract(req);
|
|
|
|
let state_fut = web::Data::<State>::extract(&req);
|
|
|
|
|
|
|
|
Box::pin(async move {
|
|
|
|
let user = user_fut.await?;
|
|
|
|
let state = state_fut.await?;
|
|
|
|
|
|
|
|
let admin = state.admin.clone();
|
|
|
|
let user_id = user.id();
|
|
|
|
if web::block(move || admin.is_admin(user_id)).await? {
|
|
|
|
return Ok(Admin(user));
|
|
|
|
}
|
|
|
|
|
|
|
|
Err(Error::Required.into())
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)]
|
|
|
|
enum ActionKind {
|
|
|
|
ResolveReport,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
pub(super) struct Store {
|
|
|
|
admin: Tree,
|
|
|
|
admin_resolved_actions: Tree,
|
|
|
|
action_kind: Tree,
|
|
|
|
action_report: Tree,
|
|
|
|
action_admin: Tree,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Store {
|
|
|
|
pub(super) fn build(db: &Db) -> Result<Self, sled::Error> {
|
|
|
|
Ok(Store {
|
|
|
|
admin: db.open_tree("/server/admin")?,
|
|
|
|
admin_resolved_actions: db.open_tree("/server/admin_resolved_actions")?,
|
|
|
|
action_kind: db.open_tree("/server/action_kind")?,
|
|
|
|
action_report: db.open_tree("/server/action_report")?,
|
|
|
|
action_admin: db.open_tree("/server/action_admin")?,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
fn is_admin(&self, user_id: Uuid) -> Result<bool, Error> {
|
|
|
|
self.admin
|
|
|
|
.get(user_id.as_bytes())
|
|
|
|
.map(|opt| opt.is_some())
|
|
|
|
.map_err(Error::from)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(super) fn make_admin(&self, user_id: Uuid) -> Result<(), Error> {
|
|
|
|
let now = Utc::now();
|
|
|
|
self.admin
|
|
|
|
.insert(user_id.as_bytes(), now.to_rfc3339().as_bytes())?;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn resolve_report(&self, admin_id: Uuid, report_id: Uuid) -> Result<(), Error> {
|
|
|
|
let resolved = Utc::now();
|
|
|
|
|
|
|
|
let action_kind = ActionKind::ResolveReport;
|
|
|
|
let action_kind_vec = serde_json::to_vec(&action_kind)?;
|
|
|
|
|
|
|
|
let mut id;
|
|
|
|
|
|
|
|
while {
|
|
|
|
id = Uuid::new_v4();
|
|
|
|
self.action_kind
|
|
|
|
.compare_and_swap(
|
|
|
|
id.as_bytes(),
|
|
|
|
None as Option<&[u8]>,
|
|
|
|
Some(action_kind_vec.as_slice()),
|
|
|
|
)?
|
|
|
|
.is_err()
|
|
|
|
} {}
|
|
|
|
|
|
|
|
let res = [
|
|
|
|
&self.admin_resolved_actions,
|
|
|
|
&self.action_report,
|
|
|
|
&self.action_admin,
|
|
|
|
]
|
|
|
|
.transaction(move |trees| {
|
|
|
|
let admin_resolved_actions = &trees[0];
|
|
|
|
let action_report = &trees[1];
|
|
|
|
let action_admin = &trees[2];
|
|
|
|
|
|
|
|
admin_resolved_actions.insert(
|
|
|
|
admin_resolved_actions_key(admin_id, resolved).as_bytes(),
|
|
|
|
id.as_bytes(),
|
|
|
|
)?;
|
|
|
|
action_report.insert(id.as_bytes(), report_id.as_bytes())?;
|
|
|
|
action_admin.insert(id.as_bytes(), admin_id.as_bytes())?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
});
|
|
|
|
|
|
|
|
if let Err(e) = res {
|
|
|
|
self.action_kind.remove(id.as_bytes())?;
|
|
|
|
return Err(e.into());
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn resolve_report(
|
|
|
|
admin_id: Uuid,
|
|
|
|
report_id: Uuid,
|
|
|
|
resolution: String,
|
|
|
|
state: &State,
|
|
|
|
) -> Result<(), Error> {
|
|
|
|
use hyaenidae_profiles::apub::actions::ResolveReport;
|
|
|
|
|
|
|
|
state
|
|
|
|
.profiles
|
|
|
|
.run(ResolveReport::from_resolution(report_id, resolution))
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
let admin = state.admin.clone();
|
|
|
|
actix_web::web::block(move || admin.resolve_report(admin_id, report_id)).await?;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn admin_resolved_actions_key(admin_id: Uuid, resolved: DateTime<Utc>) -> String {
|
|
|
|
format!("/admin/{}/report/{}", admin_id, resolved.to_rfc3339())
|
|
|
|
}
|