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, query: web::Query>, 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(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, 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: SearchPage, federated: SearchPage, inbound_requests: SearchPage, outbound_requests: SearchPage, known: SearchPage, discover_value: Option, discover_error: Option, query: HashMap, } 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> + '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