1785 lines
54 KiB
Rust
1785 lines
54 KiB
Rust
use crate::{
|
|
error::{Error, OptionExt},
|
|
extensions::{CommentExt, ProfileExt, SubmissionExt},
|
|
nav::NavState,
|
|
pagination::{PageNum, SearchPage},
|
|
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;
|
|
|
|
mod pagination;
|
|
use pagination::{
|
|
BlockedPager, FederatedPager, InboundPager, KnownPager, OutboundPager, ServerPager,
|
|
};
|
|
|
|
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<DiscoverForm>,
|
|
query: web::Query<HashMap<String, String>>,
|
|
nav_state: NavState,
|
|
client: web::Data<Client>,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
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(query.into_inner(), &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<Uuid>,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
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<Uuid>,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
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<Uuid>,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
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<Uuid>,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
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<Uuid>,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
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<Uuid>,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
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<Uuid>,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
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<Uuid, Server>,
|
|
blocked: SearchPage,
|
|
federated: SearchPage,
|
|
inbound_requests: SearchPage,
|
|
outbound_requests: SearchPage,
|
|
known: SearchPage,
|
|
discover_value: Option<String>,
|
|
discover_error: Option<String>,
|
|
query: HashMap<String, String>,
|
|
}
|
|
|
|
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 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 blocked<'a>(&'a self) -> impl Iterator<Item = BlockView<'a>> + 'a {
|
|
self.blocked
|
|
.items
|
|
.iter()
|
|
.filter_map(move |id| self.servers.get(id))
|
|
.map(|server| BlockView { server })
|
|
}
|
|
|
|
pub(crate) fn has_blocked_nav(&self) -> bool {
|
|
self.blocked.next.is_some() || self.blocked.prev.is_some()
|
|
}
|
|
|
|
fn blocked_next(&self, loader: &ActixLoader) -> Option<Button> {
|
|
if let Some(next) = self.blocked.next {
|
|
let mut query = self.query.clone();
|
|
query.insert("blocked_page".to_owned(), next.to_string());
|
|
let href = if let Ok(query) = serde_urlencoded::to_string(query) {
|
|
format!("/admin?{}", query)
|
|
} else {
|
|
"/admin".to_owned()
|
|
};
|
|
|
|
Some(Button::secondary(&fl!(loader, "server-next-page")).href(&href))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn blocked_previous(&self, loader: &ActixLoader) -> Option<Button> {
|
|
if let Some(prev) = self.blocked.prev {
|
|
let mut query = self.query.clone();
|
|
query.insert("blocked_page".to_owned(), prev.to_string());
|
|
let href = if let Ok(query) = serde_urlencoded::to_string(query) {
|
|
format!("/admin?{}", query)
|
|
} else {
|
|
"/admin".to_owned()
|
|
};
|
|
|
|
Some(Button::secondary(&fl!(loader, "server-previous-page")).href(&href))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
pub(crate) fn blocked_buttons(&self, loader: &ActixLoader) -> Vec<Button> {
|
|
self.blocked_previous(loader)
|
|
.into_iter()
|
|
.chain(self.blocked_next(loader))
|
|
.collect()
|
|
}
|
|
|
|
pub(crate) fn known<'a>(&'a self) -> impl Iterator<Item = KnownView<'a>> + 'a {
|
|
self.known
|
|
.items
|
|
.iter()
|
|
.filter_map(move |id| self.servers.get(id))
|
|
.map(|server| KnownView { server })
|
|
}
|
|
|
|
pub(crate) fn has_known_nav(&self) -> bool {
|
|
self.known.next.is_some() || self.known.prev.is_some()
|
|
}
|
|
|
|
fn known_next(&self, loader: &ActixLoader) -> Option<Button> {
|
|
if let Some(next) = self.known.next {
|
|
let mut query = self.query.clone();
|
|
query.insert("known_page".to_owned(), next.to_string());
|
|
let href = if let Ok(query) = serde_urlencoded::to_string(query) {
|
|
format!("/admin?{}", query)
|
|
} else {
|
|
"/admin".to_owned()
|
|
};
|
|
|
|
Some(Button::secondary(&fl!(loader, "server-next-page")).href(&href))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn known_previous(&self, loader: &ActixLoader) -> Option<Button> {
|
|
if let Some(prev) = self.known.prev {
|
|
let mut query = self.query.clone();
|
|
query.insert("known_page".to_owned(), prev.to_string());
|
|
let href = if let Ok(query) = serde_urlencoded::to_string(query) {
|
|
format!("/admin?{}", query)
|
|
} else {
|
|
"/admin".to_owned()
|
|
};
|
|
|
|
Some(Button::secondary(&fl!(loader, "server-previous-page")).href(&href))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
pub(crate) fn known_buttons(&self, loader: &ActixLoader) -> Vec<Button> {
|
|
self.known_previous(loader)
|
|
.into_iter()
|
|
.chain(self.known_next(loader))
|
|
.collect()
|
|
}
|
|
|
|
pub(crate) fn federated<'a>(&'a self) -> impl Iterator<Item = FederatedView<'a>> + 'a {
|
|
self.federated
|
|
.items
|
|
.iter()
|
|
.filter_map(move |id| self.servers.get(id))
|
|
.map(|server| FederatedView { server })
|
|
}
|
|
|
|
pub(crate) fn has_federated_nav(&self) -> bool {
|
|
self.federated.next.is_some() || self.federated.prev.is_some()
|
|
}
|
|
|
|
fn federated_next(&self, loader: &ActixLoader) -> Option<Button> {
|
|
if let Some(next) = self.federated.next {
|
|
let mut query = self.query.clone();
|
|
query.insert("federated_page".to_owned(), next.to_string());
|
|
let href = if let Ok(query) = serde_urlencoded::to_string(query) {
|
|
format!("/admin?{}", query)
|
|
} else {
|
|
"/admin".to_owned()
|
|
};
|
|
|
|
Some(Button::secondary(&fl!(loader, "server-next-page")).href(&href))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn federated_previous(&self, loader: &ActixLoader) -> Option<Button> {
|
|
if let Some(prev) = self.federated.prev {
|
|
let mut query = self.query.clone();
|
|
query.insert("federated_page".to_owned(), prev.to_string());
|
|
let href = if let Ok(query) = serde_urlencoded::to_string(query) {
|
|
format!("/admin?{}", query)
|
|
} else {
|
|
"/admin".to_owned()
|
|
};
|
|
|
|
Some(Button::secondary(&fl!(loader, "server-previous-page")).href(&href))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
pub(crate) fn federated_buttons(&self, loader: &ActixLoader) -> Vec<Button> {
|
|
self.federated_previous(loader)
|
|
.into_iter()
|
|
.chain(self.federated_next(loader))
|
|
.collect()
|
|
}
|
|
|
|
pub(crate) fn inbound<'a>(&'a self) -> impl Iterator<Item = InboundRequestView<'a>> + 'a {
|
|
self.inbound_requests
|
|
.items
|
|
.iter()
|
|
.filter_map(move |id| self.servers.get(id))
|
|
.map(|server| InboundRequestView { server })
|
|
}
|
|
|
|
pub(crate) fn has_inbound_nav(&self) -> bool {
|
|
self.inbound_requests.next.is_some() || self.inbound_requests.prev.is_some()
|
|
}
|
|
|
|
fn inbound_next(&self, loader: &ActixLoader) -> Option<Button> {
|
|
if let Some(next) = self.inbound_requests.next {
|
|
let mut query = self.query.clone();
|
|
query.insert("inbound_page".to_owned(), next.to_string());
|
|
let href = if let Ok(query) = serde_urlencoded::to_string(query) {
|
|
format!("/admin?{}", query)
|
|
} else {
|
|
"/admin".to_owned()
|
|
};
|
|
|
|
Some(Button::secondary(&fl!(loader, "server-next-page")).href(&href))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn inbound_previous(&self, loader: &ActixLoader) -> Option<Button> {
|
|
if let Some(prev) = self.inbound_requests.prev {
|
|
let mut query = self.query.clone();
|
|
query.insert("inbound_page".to_owned(), prev.to_string());
|
|
let href = if let Ok(query) = serde_urlencoded::to_string(query) {
|
|
format!("/admin?{}", query)
|
|
} else {
|
|
"/admin".to_owned()
|
|
};
|
|
|
|
Some(Button::secondary(&fl!(loader, "server-previous-page")).href(&href))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
pub(crate) fn inbound_buttons(&self, loader: &ActixLoader) -> Vec<Button> {
|
|
self.inbound_previous(loader)
|
|
.into_iter()
|
|
.chain(self.inbound_next(loader))
|
|
.collect()
|
|
}
|
|
|
|
pub(crate) fn outbound<'a>(&'a self) -> impl Iterator<Item = OutboundRequestView<'a>> + 'a {
|
|
self.outbound_requests
|
|
.items
|
|
.iter()
|
|
.filter_map(move |id| self.servers.get(id))
|
|
.map(|server| OutboundRequestView { server })
|
|
}
|
|
|
|
pub(crate) fn has_outbound_nav(&self) -> bool {
|
|
self.outbound_requests.next.is_some() || self.outbound_requests.prev.is_some()
|
|
}
|
|
|
|
fn outbound_next(&self, loader: &ActixLoader) -> Option<Button> {
|
|
if let Some(next) = self.outbound_requests.next {
|
|
let mut query = self.query.clone();
|
|
query.insert("outbound_page".to_owned(), next.to_string());
|
|
let href = if let Ok(query) = serde_urlencoded::to_string(query) {
|
|
format!("/admin?{}", query)
|
|
} else {
|
|
"/admin".to_owned()
|
|
};
|
|
|
|
Some(Button::secondary(&fl!(loader, "server-next-page")).href(&href))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn outbound_previous(&self, loader: &ActixLoader) -> Option<Button> {
|
|
if let Some(prev) = self.outbound_requests.prev {
|
|
let mut query = self.query.clone();
|
|
query.insert("outbound_page".to_owned(), prev.to_string());
|
|
let href = if let Ok(query) = serde_urlencoded::to_string(query) {
|
|
format!("/admin?{}", query)
|
|
} else {
|
|
"/admin".to_owned()
|
|
};
|
|
|
|
Some(Button::secondary(&fl!(loader, "server-previous-page")).href(&href))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
pub(crate) fn outbound_buttons(&self, loader: &ActixLoader) -> Vec<Button> {
|
|
self.outbound_previous(loader)
|
|
.into_iter()
|
|
.chain(self.outbound_next(loader))
|
|
.collect()
|
|
}
|
|
|
|
async fn build(query: HashMap<String, String>, state: &State) -> Result<Self, Error> {
|
|
let profiles = state.profiles.clone();
|
|
|
|
let view = web::block(move || {
|
|
let mut servers = HashMap::new();
|
|
|
|
let self_id = profiles.store.servers.get_self()?.req()?;
|
|
|
|
let self_server = profiles.store.servers.by_id(self_id)?.req()?;
|
|
servers.insert(self_server.id(), self_server);
|
|
|
|
let federated = SearchPage::from_pagination(
|
|
FederatedPager(ServerPager {
|
|
self_id,
|
|
store: &profiles.store,
|
|
servers: &mut servers,
|
|
}),
|
|
10,
|
|
"".to_owned(),
|
|
page_num(&query, "federated_page"),
|
|
);
|
|
|
|
let inbound_requests = SearchPage::from_pagination(
|
|
InboundPager(ServerPager {
|
|
self_id,
|
|
store: &profiles.store,
|
|
servers: &mut servers,
|
|
}),
|
|
10,
|
|
"".to_owned(),
|
|
page_num(&query, "inbound_page"),
|
|
);
|
|
|
|
let outbound_requests = SearchPage::from_pagination(
|
|
OutboundPager(ServerPager {
|
|
self_id,
|
|
store: &profiles.store,
|
|
servers: &mut servers,
|
|
}),
|
|
10,
|
|
"".to_owned(),
|
|
page_num(&query, "outbound_page"),
|
|
);
|
|
|
|
let blocked = SearchPage::from_pagination(
|
|
BlockedPager(ServerPager {
|
|
self_id,
|
|
store: &profiles.store,
|
|
servers: &mut servers,
|
|
}),
|
|
10,
|
|
"".to_owned(),
|
|
page_num(&query, "blocked_page"),
|
|
);
|
|
|
|
let known = SearchPage::from_pagination(
|
|
KnownPager(ServerPager {
|
|
self_id,
|
|
store: &profiles.store,
|
|
servers: &mut servers,
|
|
}),
|
|
10,
|
|
"".to_owned(),
|
|
page_num(&query, "known_page"),
|
|
);
|
|
|
|
Ok(FederationView {
|
|
servers,
|
|
blocked,
|
|
federated,
|
|
inbound_requests,
|
|
outbound_requests,
|
|
known,
|
|
discover_value: None,
|
|
discover_error: None,
|
|
query,
|
|
})
|
|
})
|
|
.await?;
|
|
|
|
Ok(view)
|
|
}
|
|
}
|
|
|
|
fn page_num(query: &HashMap<String, String>, name: &str) -> Option<PageNum> {
|
|
query.get(name).and_then(|page_str| {
|
|
Some(PageNum {
|
|
page: page_str.parse().ok()?,
|
|
})
|
|
})
|
|
}
|
|
|
|
#[derive(Clone, Debug, serde::Deserialize)]
|
|
struct ConfigForm {
|
|
title: String,
|
|
description: String,
|
|
}
|
|
|
|
async fn update_server(
|
|
_: Admin,
|
|
form: web::Form<ConfigForm>,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
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<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?;
|
|
|
|
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<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 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<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(
|
|
loader: ActixLoader,
|
|
query: web::Query<HashMap<String, String>>,
|
|
_: Admin,
|
|
nav_state: NavState,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
let federation_view = FederationView::build(query.into_inner(), &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<Uuid, File>,
|
|
report: Report,
|
|
reported_item: ReportedItem,
|
|
author: Option<Profile>,
|
|
input_value: Option<String>,
|
|
input_error: Option<String>,
|
|
}
|
|
|
|
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<CommentView<'a>> {
|
|
match &self.reported_item {
|
|
ReportedItem::Comment { comment, author } => Some(CommentView {
|
|
comment,
|
|
author,
|
|
files: &self.files,
|
|
}),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn submission<'a>(&'a self) -> Option<SubmissionView<'a>> {
|
|
match &self.reported_item {
|
|
ReportedItem::Submission { submission, author } => Some(SubmissionView {
|
|
submission,
|
|
author,
|
|
files: &self.files,
|
|
}),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn profile(&self) -> Option<OwnedProfileView> {
|
|
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<String>) -> 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<State>) -> Result<Self, Error> {
|
|
let mut files: HashMap<Uuid, File> = 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<Uuid> = 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<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,
|
|
})
|
|
}
|
|
}
|
|
|
|
pub struct ReportsView {
|
|
reports: Vec<Report>,
|
|
profiles: HashMap<Uuid, Profile>,
|
|
submissions: HashMap<Uuid, Submission>,
|
|
comments: HashMap<Uuid, Comment>,
|
|
files: HashMap<Uuid, File>,
|
|
}
|
|
|
|
pub(crate) struct CommentView<'a> {
|
|
pub(crate) author: &'a Profile,
|
|
pub(crate) comment: &'a Comment,
|
|
pub(crate) files: &'a HashMap<Uuid, File>,
|
|
}
|
|
|
|
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<Uuid, File>,
|
|
}
|
|
|
|
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<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,
|
|
files: &self.files,
|
|
})
|
|
}
|
|
|
|
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,
|
|
files: &self.files,
|
|
})
|
|
}
|
|
|
|
async fn new(state: web::Data<State>) -> Result<Self, Error> {
|
|
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<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())
|
|
}
|