use crate::{ error::{Error, OptionExt}, extensions::{CommentExt, ProfileExt, SubmissionExt}, nav::NavState, views::{OwnedProfileView, OwnedSubmissionView}, ActixLoader, State, }; use actix_web::{client::Client, dev::Payload, web, FromRequest, HttpRequest, HttpResponse, Scope}; use actix_webfinger::Webfinger; use chrono::{DateTime, Utc}; use futures::future::LocalBoxFuture; use hyaenidae_accounts::{State as AccountState, User}; use hyaenidae_profiles::store::{Comment, File, Profile, ReportKind, Server, Submission}; use hyaenidae_toolkit::{Button, Select, TextInput}; use i18n_embed_fl::fl; use sled::{Db, Transactional, Tree}; use std::collections::HashMap; use url::Url; 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))) .service( web::resource("/server") .route(web::get().to(to_admin)) .route(web::post().to(update_server)), ) .service( web::resource("/discover") .route(web::get().to(to_admin)) .route(web::post().to(discover_server)), ) .service( web::scope("/nodes/{node_id}") .service( web::resource("/block") .route(web::get().to(to_admin)) .route(web::post().to(block_server)), ) .service( web::resource("/unblock") .route(web::get().to(to_admin)) .route(web::post().to(unblock_server)), ) .service( web::resource("/follow") .route(web::get().to(to_admin)) .route(web::post().to(follow_server)), ) .service( web::resource("/accept") .route(web::get().to(to_admin)) .route(web::post().to(accept_follow)), ) .service( web::resource("/reject") .route(web::get().to(to_admin)) .route(web::post().to(reject_follow)), ) .service( web::resource("/cancel") .route(web::get().to(to_admin)) .route(web::post().to(cancel_request)), ) .service( web::resource("/defederate") .route(web::get().to(to_admin)) .route(web::post().to(defederate)), ), ) .service( web::resource("/reports/{report_id}") .route(web::get().to(view_report)) .route(web::post().to(close_report)), ) } #[derive(Clone, Debug, serde::Deserialize)] struct DiscoverForm { url: String, } async fn discover_server( loader: ActixLoader, _: Admin, form: web::Form, nav_state: NavState, client: web::Data, state: web::Data, ) -> Result { let DiscoverForm { url } = form.into_inner(); let url2 = url.clone(); let state2 = state.clone(); let fallible = || async move { let url: Url = url.parse()?; let domain = url.domain().req()?.to_owned(); let host = url.host().req()?; let host = if let Some(port) = url.port() { format!("{}:{}", host, port) } else { host.to_string() }; let actor_handle = format!("{}@{}", domain, domain); let https = url.scheme() == "https"; let wf = Webfinger::fetch(&client, &actor_handle, &host, https).await?; let activitypub = wf.activitypub().req()?; let href = activitypub.href.as_ref().req()?.parse()?; state.spawn.download_apub_anonymous(href); Ok(()) as Result<(), Error> }; if let Err(e) = (fallible)().await { let mut federation_view = FederationView::build(&state2).await?; federation_view .discover_error(e.to_string()) .discover_value(url2); let server_view = ServerView::build(&state2).await?; let open_reports = ReportsView::new(state2).await?; return crate::rendered(HttpResponse::Ok(), |cursor| { crate::templates::admin::index( cursor, &loader, &open_reports, &server_view, &federation_view, &nav_state, ) }); } Ok(to_admin()) } async fn block_server( _: Admin, server_id: web::Path, state: web::Data, ) -> Result { use hyaenidae_profiles::apub::actions::CreateServerBlock; let server_id = server_id.into_inner(); let fallible = || async move { let servers = state.profiles.store.servers.clone(); let self_server = web::block(move || servers.get_self()?.req()).await?; state .profiles .run(CreateServerBlock::from_servers(server_id, self_server)) .await?; Ok(()) as Result<(), Error> }; if let Err(e) = (fallible)().await { log::error!("Failed to process server block: {}", e); } Ok(to_admin()) } async fn unblock_server( _: Admin, server_id: web::Path, state: web::Data, ) -> Result { use hyaenidae_profiles::apub::actions::DeleteServerBlock; let server_id = server_id.into_inner(); let fallible = || async move { let servers = state.profiles.store.servers.clone(); let self_server = web::block(move || servers.get_self()?.req()).await?; let blocks = state.profiles.store.view.server_blocks.clone(); let block_id = web::block(move || blocks.by_forward(server_id, self_server)?.req()).await?; state .profiles .run(DeleteServerBlock::from_id(block_id)) .await?; Ok(()) as Result<(), Error> }; if let Err(e) = (fallible)().await { log::error!("Failed to process server unblock: {}", e); } Ok(to_admin()) } async fn follow_server( _: Admin, server_id: web::Path, state: web::Data, ) -> Result { use hyaenidae_profiles::apub::actions::CreateFederationRequest; let server_id = server_id.into_inner(); let fallible = || async move { let servers = state.profiles.store.servers.clone(); let self_server = web::block(move || servers.get_self()?.req()).await?; state .profiles .run(CreateFederationRequest::from_servers( server_id, self_server, )) .await?; Ok(()) as Result<(), Error> }; if let Err(e) = (fallible)().await { log::error!("Failed to process server follow request: {}", e); } Ok(to_admin()) } async fn accept_follow( _: Admin, server_id: web::Path, state: web::Data, ) -> Result { use hyaenidae_profiles::apub::actions::AcceptFederationRequest; let server_id = server_id.into_inner(); let fallible = || async move { let servers = state.profiles.store.servers.clone(); let self_server = web::block(move || servers.get_self()?.req()).await?; let federation_requests = state.profiles.store.view.server_follow_requests.clone(); let freq_id = web::block(move || { federation_requests .by_forward(self_server, server_id)? .req() }) .await?; state .profiles .run(AcceptFederationRequest::from_id(freq_id)) .await?; Ok(()) as Result<(), Error> }; if let Err(e) = (fallible)().await { log::error!("Failed to process server accept federation: {}", e); } Ok(to_admin()) } async fn reject_follow( _: Admin, server_id: web::Path, state: web::Data, ) -> Result { use hyaenidae_profiles::apub::actions::RejectFederationRequest; let server_id = server_id.into_inner(); let fallible = || async move { let servers = state.profiles.store.servers.clone(); let self_server = web::block(move || servers.get_self()?.req()).await?; let federation_requests = state.profiles.store.view.server_follow_requests.clone(); let freq_id = web::block(move || { federation_requests .by_forward(self_server, server_id)? .req() }) .await?; state .profiles .run(RejectFederationRequest::from_id(freq_id)) .await?; Ok(()) as Result<(), Error> }; if let Err(e) = (fallible)().await { log::error!("Failed to process server reject federation: {}", e); } Ok(to_admin()) } async fn cancel_request( _: Admin, server_id: web::Path, state: web::Data, ) -> Result { use hyaenidae_profiles::apub::actions::UndoFederationRequest; let server_id = server_id.into_inner(); let fallible = || async move { let servers = state.profiles.store.servers.clone(); let self_server = web::block(move || servers.get_self()?.req()).await?; let federation_requests = state.profiles.store.view.server_follow_requests.clone(); let freq_id = web::block(move || { federation_requests .by_forward(server_id, self_server)? .req() }) .await?; state .profiles .run(UndoFederationRequest::from_id(freq_id)) .await?; Ok(()) as Result<(), Error> }; if let Err(e) = (fallible)().await { log::error!("Failed to process cancel federation request: {}", e); } Ok(to_admin()) } async fn defederate( _: Admin, server_id: web::Path, state: web::Data, ) -> Result { use hyaenidae_profiles::apub::actions::{UndoAcceptFederation, UndoFederation}; let server_id = server_id.into_inner(); let fallible = || async move { let servers = state.profiles.store.servers.clone(); let self_server = web::block(move || servers.get_self()?.req()).await?; let federations = state.profiles.store.view.server_follows.clone(); let outbound_follow_id = web::block(move || Ok(federations.by_forward(server_id, self_server)?)).await?; let federations = state.profiles.store.view.server_follows.clone(); let inbound_follow_id = web::block(move || Ok(federations.by_forward(self_server, server_id)?)).await?; if let Some(follow_id) = outbound_follow_id { state .profiles .run(UndoFederation::from_id(follow_id)) .await?; } if let Some(follow_id) = inbound_follow_id { state .profiles .run(UndoAcceptFederation::from_id(follow_id)) .await?; } Ok(()) as Result<(), Error> }; if let Err(e) = (fallible)().await { log::error!("Failed to process server defederate: {}", e); } Ok(to_admin()) } pub struct FederationView { servers: HashMap, blocked: Vec, federated: Vec, inbound_requests: Vec, outbound_requests: Vec, known: Vec, discover_value: Option, discover_error: Option, } pub(crate) struct BlockView<'a> { pub(crate) server: &'a Server, } impl<'a> BlockView<'a> { pub(crate) fn unblock(&self, loader: &ActixLoader) -> Button { Button::secondary(&fl!(loader, "admin-federation-unblock")).form(&self.unblock_path()) } fn unblock_path(&self) -> String { format!("/admin/nodes/{}/unblock", self.server.id()) } } pub(crate) struct FederatedView<'a> { pub(crate) server: &'a Server, } impl<'a> FederatedView<'a> { pub(crate) fn defederate(&self, loader: &ActixLoader) -> Button { Button::secondary(&fl!(loader, "admin-federation-defederate")).form(&self.defederate_path()) } pub(crate) fn block(&self, loader: &ActixLoader) -> Button { Button::secondary(&fl!(loader, "admin-federation-block")).form(&self.block_path()) } fn defederate_path(&self) -> String { format!("/admin/nodes/{}/defederate", self.server.id()) } fn block_path(&self) -> String { format!("/admin/nodes/{}/block", self.server.id()) } } pub(crate) struct InboundRequestView<'a> { pub(crate) server: &'a Server, } impl<'a> InboundRequestView<'a> { pub(crate) fn accept(&self, loader: &ActixLoader) -> Button { Button::secondary(&fl!(loader, "admin-federation-accept")).form(&self.accept_path()) } pub(crate) fn reject(&self, loader: &ActixLoader) -> Button { Button::secondary(&fl!(loader, "admin-federation-reject")).form(&self.reject_path()) } pub(crate) fn block(&self, loader: &ActixLoader) -> Button { Button::secondary(&fl!(loader, "admin-federation-block")).form(&self.block_path()) } fn accept_path(&self) -> String { format!("/admin/nodes/{}/accept", self.server.id()) } fn reject_path(&self) -> String { format!("/admin/nodes/{}/reject", self.server.id()) } fn block_path(&self) -> String { format!("/admin/nodes/{}/block", self.server.id()) } } pub(crate) struct OutboundRequestView<'a> { pub(crate) server: &'a Server, } impl<'a> OutboundRequestView<'a> { pub(crate) fn cancel(&self, loader: &ActixLoader) -> Button { Button::secondary(&fl!(loader, "admin-federation-cancel")).form(&self.cancel_path()) } pub(crate) fn block(&self, loader: &ActixLoader) -> Button { Button::secondary(&fl!(loader, "admin-federation-block")).form(&self.block_path()) } fn cancel_path(&self) -> String { format!("/admin/nodes/{}/cancel", self.server.id()) } fn block_path(&self) -> String { format!("/admin/nodes/{}/block", self.server.id()) } } pub(crate) struct KnownView<'a> { pub(crate) server: &'a Server, } impl<'a> KnownView<'a> { pub(crate) fn federate(&self, loader: &ActixLoader) -> Button { Button::secondary(&fl!(loader, "admin-federation-federate")).form(&self.federate_path()) } pub(crate) fn block(&self, loader: &ActixLoader) -> Button { Button::secondary(&fl!(loader, "admin-federation-block")).form(&self.block_path()) } fn federate_path(&self) -> String { format!("/admin/nodes/{}/follow", self.server.id()) } fn block_path(&self) -> String { format!("/admin/nodes/{}/block", self.server.id()) } } impl FederationView { fn discover_error(&mut self, error: String) -> &mut Self { self.discover_error = Some(error); self } fn discover_value(&mut self, value: String) -> &mut Self { self.discover_value = Some(value); self } pub(crate) fn blocked<'a>(&'a self) -> impl Iterator> + 'a { self.blocked .iter() .filter_map(move |id| self.servers.get(id)) .map(|server| BlockView { server }) } pub(crate) fn discover_input(&self, loader: &ActixLoader) -> TextInput { let input = TextInput::new("url") .title(&fl!(loader, "admin-discover-input")) .placeholder(&fl!(loader, "admin-discover-placeholder")) .error_opt(self.discover_error.clone()); if let Some(v) = &self.discover_value { input.value(v) } else { input } } pub(crate) fn discover_path(&self) -> &'static str { "/admin/discover" } pub(crate) fn known<'a>(&'a self) -> impl Iterator> + 'a { self.known .iter() .filter_map(move |id| self.servers.get(id)) .map(|server| KnownView { server }) } pub(crate) fn federated<'a>(&'a self) -> impl Iterator> + 'a { self.federated .iter() .filter_map(move |id| self.servers.get(id)) .map(|server| FederatedView { server }) } pub(crate) fn inbound<'a>(&'a self) -> impl Iterator> + 'a { self.inbound_requests .iter() .filter_map(move |id| self.servers.get(id)) .map(|server| InboundRequestView { server }) } pub(crate) fn outbound<'a>(&'a self) -> impl Iterator> + 'a { self.outbound_requests .iter() .filter_map(move |id| self.servers.get(id)) .map(|server| OutboundRequestView { server }) } async fn build(state: &State) -> Result { let server_store = state.profiles.store.servers.clone(); let server_blocks = state.profiles.store.view.server_blocks.clone(); let server_requests = state.profiles.store.view.server_follow_requests.clone(); let server_follows = state.profiles.store.view.server_follows.clone(); let view = web::block(move || { let mut servers = HashMap::new(); let self_id = server_store.get_self()?.req()?; let self_server = server_store.by_id(self_id)?.req()?; servers.insert(self_server.id(), self_server); let federated = server_follows .forward_iter(self_id) .chain(server_follows.backward_iter(self_id)) .filter_map(|server_id| { if !servers.contains_key(&server_id) { let server = server_store.by_id(server_id).ok()??; servers.insert(server.id(), server); Some(server_id) } else { None } }) .collect(); let inbound_requests = server_requests .forward_iter(self_id) .filter_map(|server_id| { if !servers.contains_key(&server_id) { let server = server_store.by_id(server_id).ok()??; servers.insert(server.id(), server); } Some(server_id) }) .collect(); let outbound_requests = server_requests .backward_iter(self_id) .filter_map(|server_id| { if !servers.contains_key(&server_id) { let server = server_store.by_id(server_id).ok()??; servers.insert(server.id(), server); } Some(server_id) }) .collect(); let blocked = server_blocks .backward_iter(self_id) .filter_map(|server_id| { if !servers.contains_key(&server_id) { let server = server_store.by_id(server_id).ok()??; servers.insert(server.id(), server); } Some(server_id) }) .collect(); let known = server_store .known() .filter_map(|server_id| { if !servers.contains_key(&server_id) { let server = server_store.by_id(server_id).ok()??; servers.insert(server.id(), server); Some(server_id) } else { None } }) .collect(); Ok(FederationView { servers, blocked, federated, inbound_requests, outbound_requests, known, discover_value: None, discover_error: None, }) }) .await?; Ok(view) } } #[derive(Clone, Debug, serde::Deserialize)] struct ConfigForm { title: String, description: String, } async fn update_server( _: Admin, form: web::Form, state: web::Data, ) -> Result { use hyaenidae_profiles::apub::actions::UpdateServer; let ConfigForm { 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()) } async fn view_report( loader: ActixLoader, _: Admin, report: web::Path, nav_state: NavState, state: web::Data, ) -> Result { 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 report_view = ReportView::new(report, state).await?; crate::rendered(HttpResponse::Ok(), |cursor| { crate::templates::admin::report(cursor, &loader, &report_view, &nav_state) }) } #[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( loader: ActixLoader, admin: Admin, form: web::Form, report: web::Path, account_state: web::Data, nav_state: NavState, state: web::Data, ) -> Result { 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 view = ReportView::new(report, state) .await? .error_opt(Some(error)) .value(form.body); crate::rendered(HttpResponse::Ok(), |cursor| { crate::templates::admin::report(cursor, &loader, &view, &nav_state) }) } async fn handle_report( form_action: CloseAction, report: &Report, account_state: web::Data, 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, 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( loader: ActixLoader, _: Admin, nav_state: NavState, state: web::Data, ) -> Result { let federation_view = FederationView::build(&state).await?; let server_view = ServerView::build(&state).await?; let open_reports = ReportsView::new(state).await?; crate::rendered(HttpResponse::Ok(), |cursor| { crate::templates::admin::index( cursor, &loader, &open_reports, &server_view, &federation_view, &nav_state, ) }) } enum ReportedItem { Submission { submission: Submission, author: Profile, }, Profile(Profile), Comment { comment: Comment, author: Profile, }, } pub struct ReportView { files: HashMap, report: Report, reported_item: ReportedItem, author: Option, input_value: Option, input_error: Option, } 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, loader: &ActixLoader) -> Select { Select::new("action") .title(&fl!(loader, "admin-report-resolve-select")) .options(&[ (&fl!(loader, "admin-report-ignore"), "Ignore"), (&fl!(loader, "admin-report-delete"), "Delete"), (&fl!(loader, "admin-report-suspend"), "Suspend"), ]) .default_option("Ignore") } pub(crate) fn input(&self, loader: &ActixLoader) -> TextInput { let input = TextInput::new("body") .textarea() .title(&fl!(loader, "admin-report-resolve-input")) .placeholder(&fl!(loader, "admin-report-resolve-placeholder")) .error_opt(self.input_error.clone()); if let Some(value) = &self.input_value { input.value(value) } else { 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> { match &self.reported_item { ReportedItem::Comment { comment, author } => Some(CommentView { comment, author, files: &self.files, }), _ => None, } } pub(crate) fn submission<'a>(&'a self) -> Option> { match &self.reported_item { ReportedItem::Submission { submission, author } => Some(SubmissionView { submission, author, files: &self.files, }), _ => None, } } pub(crate) fn profile(&self) -> Option { match &self.reported_item { ReportedItem::Profile(ref profile) => Some(OwnedProfileView { profile: profile.clone(), icon: profile .icon() .and_then(|i| self.files.get(&i)) .map(|i| i.clone()), banner: profile .banner() .and_then(|b| self.files.get(&b)) .map(|b| b.clone()), }), _ => None, } } fn error_opt(mut self, opt: Option) -> Self { self.input_error = opt; self } fn value(mut self, value: String) -> Self { self.input_value = Some(value); self } async fn new(report: Report, state: web::Data) -> Result { let mut files: HashMap = HashMap::new(); let author = if let Some(author_id) = report.reporter_profile() { let store = state.profiles.clone(); let author = web::block(move || store.store.profiles.by_id(author_id)?.req()).await?; let mut file_ids = vec![]; file_ids.extend(author.icon()); file_ids.extend(author.banner()); let store = state.profiles.clone(); 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?; Some(author) } else { None }; let reported_item = match report.kind() { ReportKind::Profile => { let profile_id = report.item(); let store = state.profiles.clone(); let profile = web::block(move || store.store.profiles.by_id(profile_id)?.req()).await?; let store = state.profiles.clone(); let mut file_ids = vec![]; file_ids.extend(profile.icon()); file_ids.extend(profile.banner()); 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?; ReportedItem::Profile(profile) } ReportKind::Submission => { let submission_id = report.item(); let store = state.profiles.clone(); let submission = web::block(move || store.store.submissions.by_id(submission_id)?.req()).await?; let profile_id = submission.profile_id(); let store = state.profiles.clone(); let author = web::block(move || store.store.profiles.by_id(profile_id)?.req()).await?; let store = state.profiles.clone(); let mut file_ids: Vec = submission.files().iter().copied().collect(); file_ids.extend(author.icon()); file_ids.extend(author.banner()); 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?; ReportedItem::Submission { submission, author } } ReportKind::Comment => { let comment_id = report.item(); let store = state.profiles.clone(); let comment = web::block(move || store.store.comments.by_id(comment_id)?.req()).await?; let profile_id = comment.profile_id(); let store = state.profiles.clone(); let author = web::block(move || store.store.profiles.by_id(profile_id)?.req()).await?; let store = state.profiles.clone(); let mut file_ids = vec![]; file_ids.extend(author.icon()); file_ids.extend(author.banner()); 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?; ReportedItem::Comment { comment, author } } _ => None.req()?, }; Ok(ReportView { files, report, author, reported_item, input_error: None, input_value: None, }) } } pub struct ServerView { server: Server, } impl ServerView { pub(crate) fn title_input(&self, loader: &ActixLoader) -> TextInput { let title = TextInput::new("title") .title(&fl!(loader, "server-info-title-input")) .placeholder(&fl!(loader, "server-info-title-placeholder")); if let Some(text) = self.server.title() { title.value(text) } else { title } } pub(crate) fn description_input(&self, loader: &ActixLoader) -> TextInput { let description = TextInput::new("description") .title(&fl!(loader, "server-info-description-input")) .placeholder(&fl!(loader, "server-info-description-placeholder")) .textarea(); if let Some(text) = self.server.description() { description.value(text) } else { description } } pub(crate) fn update_path(&self) -> &str { "/admin/server" } async fn build(state: &State) -> Result { 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, }) } } pub struct ReportsView { reports: Vec, profiles: HashMap, submissions: HashMap, comments: HashMap, files: HashMap, } pub(crate) struct CommentView<'a> { pub(crate) author: &'a Profile, pub(crate) comment: &'a Comment, pub(crate) files: &'a HashMap, } impl<'a> CommentView<'a> { pub(crate) fn view_path(&self) -> String { self.comment.view_path() } 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) 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: None, } } } pub(crate) struct SubmissionView<'a> { pub(crate) author: &'a Profile, pub(crate) submission: &'a Submission, pub(crate) files: &'a HashMap, } impl<'a> SubmissionView<'a> { pub(crate) fn view_path(&self) -> String { self.submission.view_path() } pub(crate) fn title(&self) -> String { self.submission.title_text() } pub(crate) fn author_name(&self) -> String { self.author.name() } pub(crate) fn author_path(&self) -> String { self.author.view_path() } 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: None, } } pub(crate) fn submission(&self) -> OwnedSubmissionView { OwnedSubmissionView { submission: self.submission.clone(), files: self .submission .files() .iter() .filter_map(|file_id| self.files.get(file_id)) .cloned() .collect(), current_file: None, } } } 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> { 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, files: &self.files, }) } pub(crate) fn comment<'a>(&'a self, report: &Report) -> Option> { 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, files: &self.files, }) } async fn new(state: web::Data) -> Result { let store = state.profiles.clone(); let view = web::block(move || { let mut profiles = HashMap::new(); let mut submissions = HashMap::new(); let mut comments = HashMap::new(); let mut files = HashMap::new(); let reports = store .store .reports .all() .filter_map(|id| { let report = store.store.reports.by_id(id).ok()?; if let Some(id) = report.reporter_profile() { if !profiles.contains_key(&id) { let profile = store.store.profiles.by_id(id).ok()??; profiles.insert(profile.id(), profile); } } match report.kind() { ReportKind::Profile => { if !profiles.contains_key(&report.item()) { let profile = store.store.profiles.by_id(report.item()).ok()??; profiles.insert(profile.id(), profile); } } ReportKind::Submission => { if !submissions.contains_key(&report.item()) { let submission = store.store.submissions.by_id(report.item()).ok()??; if !profiles.contains_key(&submission.profile_id()) { let profile = store .store .profiles .by_id(submission.profile_id()) .ok()??; profiles.insert(profile.id(), profile); } for file_id in submission.files() { if !files.contains_key(file_id) { let file = store.store.files.by_id(*file_id).ok()??; files.insert(file.id(), file); } } submissions.insert(submission.id(), submission); } } ReportKind::Comment => { if !comments.contains_key(&report.item()) { let comment = store.store.comments.by_id(report.item()).ok()??; if !profiles.contains_key(&comment.profile_id()) { let profile = store.store.profiles.by_id(comment.profile_id()).ok()??; profiles.insert(profile.id(), profile); } comments.insert(comment.id(), comment); } } _ => (), } Some(report) }) .rev() .collect(); Ok(ReportsView { reports, profiles, submissions, comments, files, }) 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>; fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { let user_fut = User::extract(req); let state_fut = web::Data::::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 { 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 { 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) -> String { format!("/admin/{}/report/{}", admin_id, resolved.to_rfc3339()) }