feat: discoverability (NodeInfo, hashtags) and moderation (domain/actor blocking)
- NodeInfo at /.well-known/nodeinfo + /nodeinfo/2.0
- Hashtags #MoviesDiary + #MovieTitle on review posts; /tags/{tag} redirect
- Domain blocking: blocked_domains table, admin API + HTML, inbox enforcement
- Per-actor blocking: blocked_actors table, user API + HTML, BlockActivity send/receive
- Delivery filter excludes blocked actors and blocked-domain inboxes
This commit is contained in:
@@ -63,9 +63,22 @@ impl Activity for FollowActivity {
|
||||
}
|
||||
|
||||
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
||||
let domain = self.actor().host_str().unwrap_or("");
|
||||
if data.federation_repo.is_domain_blocked(domain).await? {
|
||||
tracing::info!(actor = %self.actor(), "ignoring activity from blocked domain");
|
||||
return Ok(());
|
||||
}
|
||||
let _follower = self.actor.dereference(data).await?;
|
||||
let local_actor = self.object.dereference(data).await?;
|
||||
|
||||
if data.federation_repo
|
||||
.is_actor_blocked(local_actor.user_id, self.actor.inner().as_str())
|
||||
.await?
|
||||
{
|
||||
tracing::info!(actor = %self.actor.inner(), "ignoring follow from blocked actor");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
data.federation_repo
|
||||
.add_follower(
|
||||
local_actor.user_id,
|
||||
@@ -114,6 +127,11 @@ impl Activity for AcceptActivity {
|
||||
}
|
||||
|
||||
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
||||
let domain = self.actor().host_str().unwrap_or("");
|
||||
if data.federation_repo.is_domain_blocked(domain).await? {
|
||||
tracing::info!(actor = %self.actor(), "ignoring activity from blocked domain");
|
||||
return Ok(());
|
||||
}
|
||||
let local_user_id = crate::urls::extract_user_id_from_url(self.object.actor.inner())
|
||||
.ok_or_else(|| Error::bad_request(anyhow::anyhow!("invalid actor URL in Follow")))?;
|
||||
data.federation_repo
|
||||
@@ -158,6 +176,11 @@ impl Activity for RejectActivity {
|
||||
}
|
||||
|
||||
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
||||
let domain = self.actor().host_str().unwrap_or("");
|
||||
if data.federation_repo.is_domain_blocked(domain).await? {
|
||||
tracing::info!(actor = %self.actor(), "ignoring activity from blocked domain");
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(user_id) = crate::urls::extract_user_id_from_url(self.object.actor.inner()) {
|
||||
data.federation_repo
|
||||
.remove_following(user_id, self.actor.inner().as_str())
|
||||
@@ -198,6 +221,11 @@ impl Activity for UndoActivity {
|
||||
}
|
||||
|
||||
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
||||
let domain = self.actor().host_str().unwrap_or("");
|
||||
if data.federation_repo.is_domain_blocked(domain).await? {
|
||||
tracing::info!(actor = %self.actor(), "ignoring activity from blocked domain");
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(user_id) = crate::urls::extract_user_id_from_url(self.object.object.inner()) {
|
||||
data.federation_repo
|
||||
.remove_follower(user_id, self.actor.inner().as_str())
|
||||
@@ -242,6 +270,11 @@ impl Activity for CreateActivity {
|
||||
}
|
||||
|
||||
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
||||
let domain = self.actor().host_str().unwrap_or("");
|
||||
if data.federation_repo.is_domain_blocked(domain).await? {
|
||||
tracing::info!(actor = %self.actor(), "ignoring activity from blocked domain");
|
||||
return Ok(());
|
||||
}
|
||||
let ap_id = self.id.clone();
|
||||
let actor_url = self.actor.inner().clone();
|
||||
data.object_handler
|
||||
@@ -283,6 +316,11 @@ impl Activity for DeleteActivity {
|
||||
}
|
||||
|
||||
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
||||
let domain = self.actor().host_str().unwrap_or("");
|
||||
if data.federation_repo.is_domain_blocked(domain).await? {
|
||||
tracing::info!(actor = %self.actor(), "ignoring activity from blocked domain");
|
||||
return Ok(());
|
||||
}
|
||||
let actor_url = self.actor.inner().clone();
|
||||
data.object_handler
|
||||
.on_delete(&self.object, &actor_url)
|
||||
@@ -323,6 +361,11 @@ impl Activity for UpdateActivity {
|
||||
}
|
||||
|
||||
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
||||
let domain = self.actor().host_str().unwrap_or("");
|
||||
if data.federation_repo.is_domain_blocked(domain).await? {
|
||||
tracing::info!(actor = %self.actor(), "ignoring activity from blocked domain");
|
||||
return Ok(());
|
||||
}
|
||||
let ap_id = self.id.clone();
|
||||
let actor_url = self.actor.inner().clone();
|
||||
data.object_handler
|
||||
@@ -365,6 +408,11 @@ impl Activity for AnnounceActivity {
|
||||
}
|
||||
|
||||
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
||||
let domain = self.actor().host_str().unwrap_or("");
|
||||
if data.federation_repo.is_domain_blocked(domain).await? {
|
||||
tracing::info!(actor = %self.actor(), "ignoring activity from blocked domain");
|
||||
return Ok(());
|
||||
}
|
||||
let object_domain = self.object.host_str().unwrap_or("");
|
||||
if object_domain != data.domain {
|
||||
return Ok(());
|
||||
@@ -382,6 +430,57 @@ impl Activity for AnnounceActivity {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Block ---
|
||||
|
||||
#[derive(Clone, Default, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename = "Block")]
|
||||
pub struct BlockType;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BlockActivity {
|
||||
pub(crate) id: Url,
|
||||
#[serde(rename = "type", default)]
|
||||
pub(crate) kind: BlockType,
|
||||
pub(crate) actor: ObjectId<DbActor>,
|
||||
pub(crate) object: Url,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Activity for BlockActivity {
|
||||
type DataType = FederationData;
|
||||
type Error = Error;
|
||||
|
||||
fn id(&self) -> &Url {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn actor(&self) -> &Url {
|
||||
self.actor.inner()
|
||||
}
|
||||
|
||||
async fn verify(&self, _data: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
||||
let domain = self.actor().host_str().unwrap_or("");
|
||||
if data.federation_repo.is_domain_blocked(domain).await? {
|
||||
tracing::info!(actor = %self.actor(), "ignoring activity from blocked domain");
|
||||
return Ok(());
|
||||
}
|
||||
// They blocked us — remove them from our following list
|
||||
if let Some(local_user_id) = crate::urls::extract_user_id_from_url(&self.object) {
|
||||
let _ = data
|
||||
.federation_repo
|
||||
.remove_following(local_user_id, self.actor.inner().as_str())
|
||||
.await;
|
||||
}
|
||||
tracing::info!(actor = %self.actor.inner(), "received block");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// --- Inbox dispatch enum ---
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
@@ -404,4 +503,6 @@ pub enum InboxActivities {
|
||||
Update(UpdateActivity),
|
||||
#[serde(rename = "Announce")]
|
||||
Announce(AnnounceActivity),
|
||||
#[serde(rename = "Block")]
|
||||
Block(BlockActivity),
|
||||
}
|
||||
|
||||
@@ -27,7 +27,8 @@ pub struct DbActor {
|
||||
pub ap_id: Url,
|
||||
pub last_refreshed_at: DateTime<Utc>,
|
||||
pub bio: Option<String>,
|
||||
pub avatar_path: Option<String>,
|
||||
pub avatar_url: Option<Url>,
|
||||
pub profile_url: Option<Url>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
@@ -105,7 +106,8 @@ pub async fn get_local_actor(
|
||||
ap_id,
|
||||
last_refreshed_at: Utc::now(),
|
||||
bio: user.bio,
|
||||
avatar_path: user.avatar_path,
|
||||
avatar_url: user.avatar_url,
|
||||
profile_url: user.profile_url,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -164,7 +166,8 @@ impl Object for DbActor {
|
||||
ap_id,
|
||||
last_refreshed_at: Utc::now(),
|
||||
bio: None,
|
||||
avatar_path: None,
|
||||
avatar_url: None,
|
||||
profile_url: None,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -175,14 +178,11 @@ impl Object for DbActor {
|
||||
public_key_pem: self.public_key_pem.clone(),
|
||||
};
|
||||
|
||||
let icon = self.avatar_path.as_ref().map(|p| ApImageObject {
|
||||
let icon = self.avatar_url.map(|url| ApImageObject {
|
||||
kind: "Image".to_string(),
|
||||
url: Url::parse(&format!("{}/images/{}", data.base_url, p))
|
||||
.expect("valid avatar url"),
|
||||
url,
|
||||
});
|
||||
let profile_url =
|
||||
Url::parse(&format!("{}/u/{}", data.base_url, self.username))
|
||||
.expect("valid profile url");
|
||||
let profile_url = self.profile_url;
|
||||
|
||||
Ok(Person {
|
||||
kind: Default::default(),
|
||||
@@ -196,7 +196,7 @@ impl Object for DbActor {
|
||||
name: Some(self.username.clone()),
|
||||
summary: self.bio.clone(),
|
||||
icon,
|
||||
url: Some(profile_url),
|
||||
url: profile_url,
|
||||
discoverable: Some(true),
|
||||
manually_approves_followers: false,
|
||||
})
|
||||
@@ -242,7 +242,8 @@ impl Object for DbActor {
|
||||
ap_id,
|
||||
last_refreshed_at: Utc::now(),
|
||||
bio: None,
|
||||
avatar_path: None,
|
||||
avatar_url: None,
|
||||
profile_url: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,4 +41,7 @@ pub trait ApObjectHandler: Send + Sync {
|
||||
|
||||
/// Actor unfollowed/was removed — clean up all their remote content.
|
||||
async fn on_actor_removed(&self, actor_url: &Url) -> anyhow::Result<()>;
|
||||
|
||||
/// Total number of locally-authored posts across all users.
|
||||
async fn count_local_posts(&self) -> anyhow::Result<u64>;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ pub struct FederationData {
|
||||
pub(crate) object_handler: Arc<dyn ApObjectHandler>,
|
||||
pub(crate) base_url: String,
|
||||
pub(crate) domain: String,
|
||||
pub(crate) allow_registration: bool,
|
||||
pub(crate) software_name: String,
|
||||
}
|
||||
|
||||
impl FederationData {
|
||||
@@ -19,6 +21,8 @@ impl FederationData {
|
||||
user_repo: Arc<dyn ApUserRepository>,
|
||||
object_handler: Arc<dyn ApObjectHandler>,
|
||||
base_url: String,
|
||||
allow_registration: bool,
|
||||
software_name: String,
|
||||
) -> Self {
|
||||
let domain = base_url
|
||||
.trim_start_matches("https://")
|
||||
@@ -33,6 +37,8 @@ impl FederationData {
|
||||
object_handler,
|
||||
base_url,
|
||||
domain,
|
||||
allow_registration,
|
||||
software_name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ pub mod error;
|
||||
pub mod federation;
|
||||
pub mod followers_handler;
|
||||
pub mod inbox;
|
||||
pub mod nodeinfo;
|
||||
pub mod outbox;
|
||||
pub mod repository;
|
||||
pub mod service;
|
||||
@@ -19,7 +20,7 @@ pub use data::FederationData;
|
||||
pub use error::Error;
|
||||
pub use federation::ApFederationConfig;
|
||||
pub use repository::{
|
||||
FederationRepository, Follower, FollowerStatus, FollowingStatus, RemoteActor,
|
||||
BlockedDomain, FederationRepository, Follower, FollowerStatus, FollowingStatus, RemoteActor,
|
||||
};
|
||||
pub use service::ActivityPubService;
|
||||
pub use user::{ApUser, ApUserRepository};
|
||||
|
||||
119
crates/adapters/activitypub-base/src/nodeinfo.rs
Normal file
119
crates/adapters/activitypub-base/src/nodeinfo.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use activitypub_federation::config::Data;
|
||||
use axum::Json;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::data::FederationData;
|
||||
use crate::error::Error;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct NodeInfoWellKnown {
|
||||
pub links: Vec<NodeInfoLink>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct NodeInfoLink {
|
||||
pub rel: String,
|
||||
pub href: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct NodeInfoSoftware {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NodeInfoUsage {
|
||||
pub users: NodeInfoUsers,
|
||||
pub local_posts: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct NodeInfoUsers {
|
||||
pub total: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NodeInfo {
|
||||
pub version: String,
|
||||
pub software: NodeInfoSoftware,
|
||||
pub protocols: Vec<String>,
|
||||
pub usage: NodeInfoUsage,
|
||||
pub open_registrations: bool,
|
||||
}
|
||||
|
||||
pub async fn nodeinfo_well_known_handler(
|
||||
data: Data<FederationData>,
|
||||
) -> Result<Json<NodeInfoWellKnown>, Error> {
|
||||
let href = format!("{}/nodeinfo/2.0", data.base_url);
|
||||
Ok(Json(NodeInfoWellKnown {
|
||||
links: vec![NodeInfoLink {
|
||||
rel: "http://nodeinfo.diaspora.software/ns/schema/2.0".to_string(),
|
||||
href,
|
||||
}],
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn nodeinfo_handler(
|
||||
data: Data<FederationData>,
|
||||
) -> Result<Json<NodeInfo>, Error> {
|
||||
let user_count = data.user_repo.count_users().await.unwrap_or(0);
|
||||
let local_posts = data.object_handler.count_local_posts().await.unwrap_or(0);
|
||||
|
||||
Ok(Json(NodeInfo {
|
||||
version: "2.0".to_string(),
|
||||
software: NodeInfoSoftware {
|
||||
name: data.software_name.clone(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
},
|
||||
protocols: vec!["activitypub".to_string()],
|
||||
usage: NodeInfoUsage {
|
||||
users: NodeInfoUsers { total: user_count },
|
||||
local_posts,
|
||||
},
|
||||
open_registrations: data.allow_registration,
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn nodeinfo_well_known_serializes_correctly() {
|
||||
let doc = NodeInfoWellKnown {
|
||||
links: vec![NodeInfoLink {
|
||||
rel: "http://nodeinfo.diaspora.software/ns/schema/2.0".to_string(),
|
||||
href: "https://example.com/nodeinfo/2.0".to_string(),
|
||||
}],
|
||||
};
|
||||
let json = serde_json::to_value(&doc).unwrap();
|
||||
assert_eq!(json["links"][0]["rel"], "http://nodeinfo.diaspora.software/ns/schema/2.0");
|
||||
assert_eq!(json["links"][0]["href"], "https://example.com/nodeinfo/2.0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nodeinfo_serializes_camel_case() {
|
||||
let doc = NodeInfo {
|
||||
version: "2.0".to_string(),
|
||||
software: NodeInfoSoftware {
|
||||
name: "my-app".to_string(),
|
||||
version: "0.1.0".to_string(),
|
||||
},
|
||||
protocols: vec!["activitypub".to_string()],
|
||||
usage: NodeInfoUsage {
|
||||
users: NodeInfoUsers { total: 3 },
|
||||
local_posts: 42,
|
||||
},
|
||||
open_registrations: false,
|
||||
};
|
||||
let json = serde_json::to_value(&doc).unwrap();
|
||||
assert_eq!(json["version"], "2.0");
|
||||
assert_eq!(json["software"]["name"], "my-app");
|
||||
assert_eq!(json["usage"]["users"]["total"], 3);
|
||||
assert_eq!(json["usage"]["localPosts"], 42);
|
||||
assert_eq!(json["openRegistrations"], false);
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,13 @@ pub struct Follower {
|
||||
pub status: FollowerStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BlockedDomain {
|
||||
pub domain: String,
|
||||
pub reason: Option<String>,
|
||||
pub blocked_at: String,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait FederationRepository: Send + Sync {
|
||||
async fn add_follower(
|
||||
@@ -97,4 +104,12 @@ pub trait FederationRepository: Send + Sync {
|
||||
announced_at: chrono::DateTime<chrono::Utc>,
|
||||
) -> Result<()>;
|
||||
async fn count_announces(&self, object_url: &str) -> Result<usize>;
|
||||
async fn add_blocked_domain(&self, domain: &str, reason: Option<&str>) -> Result<()>;
|
||||
async fn remove_blocked_domain(&self, domain: &str) -> Result<()>;
|
||||
async fn get_blocked_domains(&self) -> Result<Vec<BlockedDomain>>;
|
||||
async fn is_domain_blocked(&self, domain: &str) -> Result<bool>;
|
||||
async fn add_blocked_actor(&self, local_user_id: uuid::Uuid, actor_url: &str) -> Result<()>;
|
||||
async fn remove_blocked_actor(&self, local_user_id: uuid::Uuid, actor_url: &str) -> Result<()>;
|
||||
async fn get_blocked_actors(&self, local_user_id: uuid::Uuid) -> Result<Vec<String>>;
|
||||
async fn is_actor_blocked(&self, local_user_id: uuid::Uuid, actor_url: &str) -> Result<bool>;
|
||||
}
|
||||
|
||||
@@ -18,9 +18,10 @@ use crate::{
|
||||
followers_handler::{followers_handler, following_handler},
|
||||
inbox::inbox_handler,
|
||||
outbox::outbox_handler,
|
||||
repository::{FederationRepository, FollowerStatus, FollowingStatus, RemoteActor},
|
||||
repository::{BlockedDomain, FederationRepository, FollowerStatus, FollowingStatus, RemoteActor},
|
||||
urls::activity_url,
|
||||
user::ApUserRepository,
|
||||
nodeinfo::{nodeinfo_handler, nodeinfo_well_known_handler},
|
||||
webfinger::webfinger_handler,
|
||||
};
|
||||
|
||||
@@ -78,9 +79,11 @@ impl ActivityPubService {
|
||||
user_repo: Arc<dyn ApUserRepository>,
|
||||
object_handler: Arc<dyn ApObjectHandler>,
|
||||
base_url: String,
|
||||
allow_registration: bool,
|
||||
software_name: String,
|
||||
debug: bool,
|
||||
) -> anyhow::Result<Self> {
|
||||
let data = FederationData::new(repo, user_repo, object_handler, base_url.clone());
|
||||
let data = FederationData::new(repo, user_repo, object_handler, base_url.clone(), allow_registration, software_name);
|
||||
let federation_config = ApFederationConfig::new(data, debug).await?;
|
||||
Ok(Self {
|
||||
federation_config,
|
||||
@@ -112,6 +115,8 @@ impl ActivityPubService {
|
||||
|
||||
pub fn router(&self) -> Router {
|
||||
Router::new()
|
||||
.route("/.well-known/nodeinfo", get(nodeinfo_well_known_handler))
|
||||
.route("/nodeinfo/2.0", get(nodeinfo_handler))
|
||||
.route("/.well-known/webfinger", get(webfinger_handler))
|
||||
.route("/users/{id}/inbox", post(inbox_handler))
|
||||
.route("/users/{id}/outbox", get(outbox_handler))
|
||||
@@ -443,9 +448,30 @@ impl ActivityPubService {
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
|
||||
let followers = data.federation_repo.get_followers(local_user_id).await?;
|
||||
let blocked = data
|
||||
.federation_repo
|
||||
.get_blocked_actors(local_user_id)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let blocked_set: std::collections::HashSet<String> = blocked.into_iter().collect();
|
||||
let blocked_domains = data
|
||||
.federation_repo
|
||||
.get_blocked_domains()
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let blocked_domain_set: std::collections::HashSet<String> =
|
||||
blocked_domains.into_iter().map(|d| d.domain).collect();
|
||||
let accepted: Vec<_> = followers
|
||||
.into_iter()
|
||||
.filter(|f| f.status == FollowerStatus::Accepted)
|
||||
.filter(|f| !blocked_set.contains(&f.actor.url))
|
||||
.filter(|f| {
|
||||
let domain = url::Url::parse(&f.actor.inbox_url)
|
||||
.ok()
|
||||
.and_then(|u| u.host_str().map(|s| s.to_string()))
|
||||
.unwrap_or_default();
|
||||
!blocked_domain_set.contains(&domain)
|
||||
})
|
||||
.collect();
|
||||
|
||||
if accepted.is_empty() {
|
||||
@@ -526,6 +552,88 @@ impl ActivityPubService {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn block_actor(&self, local_user_id: uuid::Uuid, actor_url: &str) -> anyhow::Result<()> {
|
||||
let data = self.federation_config.to_request_data();
|
||||
|
||||
data.federation_repo
|
||||
.add_blocked_actor(local_user_id, actor_url)
|
||||
.await?;
|
||||
let _ = data.federation_repo.remove_follower(local_user_id, actor_url).await;
|
||||
let _ = data.federation_repo.remove_following(local_user_id, actor_url).await;
|
||||
|
||||
let local_actor = get_local_actor(local_user_id, &data)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
|
||||
if let Ok(Some(remote_actor)) = data.federation_repo.get_remote_actor(actor_url).await {
|
||||
let block_id = crate::urls::activity_url(&self.base_url)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
let block = crate::activities::BlockActivity {
|
||||
id: block_id,
|
||||
kind: Default::default(),
|
||||
actor: ObjectId::from(local_actor.ap_id.clone()),
|
||||
object: Url::parse(actor_url)?,
|
||||
};
|
||||
let inbox = Url::parse(&remote_actor.inbox_url)?;
|
||||
let sends = SendActivityTask::prepare(
|
||||
&WithContext::new_default(block),
|
||||
&local_actor,
|
||||
vec![inbox],
|
||||
&data,
|
||||
)
|
||||
.await?;
|
||||
let failures = send_with_retry(sends, &data).await;
|
||||
if !failures.is_empty() {
|
||||
tracing::warn!(actor = %actor_url, "failed to deliver Block activity");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn unblock_actor(&self, local_user_id: uuid::Uuid, actor_url: &str) -> anyhow::Result<()> {
|
||||
let data = self.federation_config.to_request_data();
|
||||
data.federation_repo
|
||||
.remove_blocked_actor(local_user_id, actor_url)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_blocked_actors(&self, local_user_id: uuid::Uuid) -> anyhow::Result<Vec<RemoteActor>> {
|
||||
let data = self.federation_config.to_request_data();
|
||||
let actor_urls = data.federation_repo.get_blocked_actors(local_user_id).await?;
|
||||
let mut actors = Vec::new();
|
||||
for url in actor_urls {
|
||||
let actor = match data.federation_repo.get_remote_actor(&url).await {
|
||||
Ok(Some(a)) => a,
|
||||
_ => RemoteActor {
|
||||
url: url.clone(),
|
||||
handle: url.clone(),
|
||||
inbox_url: url.clone(),
|
||||
shared_inbox_url: None,
|
||||
display_name: None,
|
||||
avatar_url: None,
|
||||
},
|
||||
};
|
||||
actors.push(actor);
|
||||
}
|
||||
Ok(actors)
|
||||
}
|
||||
|
||||
pub async fn add_blocked_domain(&self, domain: &str, reason: Option<&str>) -> anyhow::Result<()> {
|
||||
let data = self.federation_config.to_request_data();
|
||||
data.federation_repo.add_blocked_domain(domain, reason).await
|
||||
}
|
||||
|
||||
pub async fn remove_blocked_domain(&self, domain: &str) -> anyhow::Result<()> {
|
||||
let data = self.federation_config.to_request_data();
|
||||
data.federation_repo.remove_blocked_domain(domain).await
|
||||
}
|
||||
|
||||
pub async fn get_blocked_domains(&self) -> anyhow::Result<Vec<BlockedDomain>> {
|
||||
let data = self.federation_config.to_request_data();
|
||||
data.federation_repo.get_blocked_domains().await
|
||||
}
|
||||
|
||||
async fn follow_local(
|
||||
&self,
|
||||
local_user_id: uuid::Uuid,
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
use async_trait::async_trait;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ApUser {
|
||||
pub id: uuid::Uuid,
|
||||
pub username: String,
|
||||
pub bio: Option<String>,
|
||||
pub avatar_path: Option<String>,
|
||||
pub avatar_url: Option<Url>,
|
||||
pub profile_url: Option<Url>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait ApUserRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: uuid::Uuid) -> anyhow::Result<Option<ApUser>>;
|
||||
async fn find_by_username(&self, username: &str) -> anyhow::Result<Option<ApUser>>;
|
||||
async fn count_users(&self) -> anyhow::Result<usize>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user