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:
2026-05-12 00:49:30 +02:00
parent 80f620c840
commit f0620f5aa1
40 changed files with 1410 additions and 543 deletions

View File

@@ -1,7 +1,7 @@
use async_trait::async_trait;
use uuid::Uuid;
use activitypub_base::{ActivityPubService, RemoteActor};
use activitypub_base::{ActivityPubService, BlockedDomain, RemoteActor};
#[async_trait]
pub trait ActivityPubPort: Send + Sync {
@@ -25,6 +25,12 @@ pub trait ActivityPubPort: Send + Sync {
async fn get_accepted_followers(&self, local_user_id: Uuid)
-> anyhow::Result<Vec<RemoteActor>>;
async fn remove_follower(&self, local_user_id: Uuid, actor_url: &str) -> anyhow::Result<()>;
async fn block_actor(&self, local_user_id: Uuid, actor_url: &str) -> anyhow::Result<()>;
async fn unblock_actor(&self, local_user_id: Uuid, actor_url: &str) -> anyhow::Result<()>;
async fn get_blocked_actors(&self, local_user_id: Uuid) -> anyhow::Result<Vec<RemoteActor>>;
async fn add_blocked_domain(&self, domain: &str, reason: Option<&str>) -> anyhow::Result<()>;
async fn remove_blocked_domain(&self, domain: &str) -> anyhow::Result<()>;
async fn get_blocked_domains(&self) -> anyhow::Result<Vec<BlockedDomain>>;
}
#[async_trait]
@@ -73,6 +79,24 @@ impl ActivityPubPort for ActivityPubService {
async fn remove_follower(&self, local_user_id: Uuid, actor_url: &str) -> anyhow::Result<()> {
self.remove_follower(local_user_id, actor_url).await
}
async fn block_actor(&self, local_user_id: Uuid, actor_url: &str) -> anyhow::Result<()> {
self.block_actor(local_user_id, actor_url).await
}
async fn unblock_actor(&self, local_user_id: Uuid, actor_url: &str) -> anyhow::Result<()> {
self.unblock_actor(local_user_id, actor_url).await
}
async fn get_blocked_actors(&self, local_user_id: Uuid) -> anyhow::Result<Vec<RemoteActor>> {
self.get_blocked_actors(local_user_id).await
}
async fn add_blocked_domain(&self, domain: &str, reason: Option<&str>) -> anyhow::Result<()> {
self.add_blocked_domain(domain, reason).await
}
async fn remove_blocked_domain(&self, domain: &str) -> anyhow::Result<()> {
self.remove_blocked_domain(domain).await
}
async fn get_blocked_domains(&self) -> anyhow::Result<Vec<BlockedDomain>> {
self.get_blocked_domains().await
}
}
pub struct NoopActivityPubService;
@@ -112,4 +136,22 @@ impl ActivityPubPort for NoopActivityPubService {
async fn remove_follower(&self, _: Uuid, _: &str) -> anyhow::Result<()> {
Ok(())
}
async fn block_actor(&self, _: Uuid, _: &str) -> anyhow::Result<()> {
Ok(())
}
async fn unblock_actor(&self, _: Uuid, _: &str) -> anyhow::Result<()> {
Ok(())
}
async fn get_blocked_actors(&self, _: Uuid) -> anyhow::Result<Vec<RemoteActor>> {
Ok(vec![])
}
async fn add_blocked_domain(&self, _: &str, _: Option<&str>) -> anyhow::Result<()> {
Ok(())
}
async fn remove_blocked_domain(&self, _: &str) -> anyhow::Result<()> {
Ok(())
}
async fn get_blocked_domains(&self) -> anyhow::Result<Vec<BlockedDomain>> {
Ok(vec![])
}
}