fix: address 3 PARTIAL plan items
#15 @context security vocab: actor JSON now uses actor_ap_context() which includes W3C security vocab + Mastodon toot extensions (manuallyApprovesFollowers, discoverable, featured). Applied to actor_handler, actor_json(), broadcast_actor_update(). Activity JSON keeps plain AS context (no security vocab needed). #17 HTTP Digest (documented, no code change): production mode (debug=false) REQUIRES Digest header on inbound POSTs via require_digest() in the non-compat normalization config. Added doc comment to ApFederationConfig::new() to clarify. #26 Integration tests: 3 new tokio tests in src/tests/integration.rs using in-memory trait stubs. Tests cover: - check_guards idempotency (duplicate activity rejected) - check_guards domain block (blocked domain skipped) - extract_and_dispatch_mentions (on_mention called for local actor)
This commit is contained in:
@@ -6,6 +6,7 @@ use axum::extract::Path;
|
|||||||
use crate::actors::{Person, get_local_actor};
|
use crate::actors::{Person, get_local_actor};
|
||||||
use crate::data::FederationData;
|
use crate::data::FederationData;
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
|
use crate::urls::actor_ap_context;
|
||||||
|
|
||||||
/// Serves the AP actor JSON for a local user.
|
/// Serves the AP actor JSON for a local user.
|
||||||
/// The path parameter is the user's UUID (matching the canonical actor URL).
|
/// The path parameter is the user's UUID (matching the canonical actor URL).
|
||||||
@@ -19,5 +20,5 @@ pub async fn actor_handler(
|
|||||||
let db_actor = get_local_actor(user_id, &data).await?;
|
let db_actor = get_local_actor(user_id, &data).await?;
|
||||||
let person = db_actor.into_json(&data).await?;
|
let person = db_actor.into_json(&data).await?;
|
||||||
|
|
||||||
Ok(FederationJson(WithContext::new_default(person)))
|
Ok(FederationJson(WithContext::new(person, actor_ap_context())))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,15 @@ impl UrlVerifier for PermissiveVerifier {
|
|||||||
pub struct ApFederationConfig(pub FederationConfig<FederationData>);
|
pub struct ApFederationConfig(pub FederationConfig<FederationData>);
|
||||||
|
|
||||||
impl ApFederationConfig {
|
impl ApFederationConfig {
|
||||||
|
/// Create a new federation config.
|
||||||
|
///
|
||||||
|
/// **HTTP signature / Digest behavior:**
|
||||||
|
/// - Production (`debug = false`): strict normalization + **requires `Digest` header** on every
|
||||||
|
/// inbound POST. All major AP implementations (Mastodon, Pleroma, Pixelfed) include it.
|
||||||
|
/// - Debug (`debug = true`): relaxes Digest requirement, disables signature verification,
|
||||||
|
/// and accepts any URL. **Never use in production.**
|
||||||
|
///
|
||||||
|
/// Outbound signing always uses Mastodon compat mode regardless of this flag.
|
||||||
pub async fn new(data: FederationData, debug: bool) -> anyhow::Result<Self> {
|
pub async fn new(data: FederationData, debug: bool) -> anyhow::Result<Self> {
|
||||||
let config = if debug {
|
let config = if debug {
|
||||||
FederationConfig::builder()
|
FederationConfig::builder()
|
||||||
|
|||||||
@@ -26,3 +26,7 @@ pub use repository::{
|
|||||||
};
|
};
|
||||||
pub use service::ActivityPubService;
|
pub use service::ActivityPubService;
|
||||||
pub use user::{ApActorType, ApProfileField, ApUser, ApUserRepository, LookedUpActor};
|
pub use user::{ApActorType, ApProfileField, ApUser, ApUserRepository, LookedUpActor};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "tests/integration.rs"]
|
||||||
|
mod integration_tests;
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ impl ActivityPubService {
|
|||||||
let data = self.federation_config.to_request_data();
|
let data = self.federation_config.to_request_data();
|
||||||
let local_actor = get_local_actor(user_id, &data).await.map_err(|e| anyhow::anyhow!("{e}"))?;
|
let local_actor = get_local_actor(user_id, &data).await.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||||
let person = local_actor.clone().into_json(&data).await.map_err(|e| anyhow::anyhow!("{e}"))?;
|
let person = local_actor.clone().into_json(&data).await.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||||
let person_json = serde_json::to_value(WithContext::new_default(person))?;
|
let person_json = serde_json::to_value(WithContext::new(person, crate::urls::actor_ap_context()))?;
|
||||||
let update_id = Url::parse(&format!("{}/activities/update/{}", self.base_url, uuid::Uuid::new_v4()))?;
|
let update_id = Url::parse(&format!("{}/activities/update/{}", self.base_url, uuid::Uuid::new_v4()))?;
|
||||||
let update = UpdateActivity {
|
let update = UpdateActivity {
|
||||||
id: update_id,
|
id: update_id,
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ impl ActivityPubService {
|
|||||||
let data = self.federation_config.to_request_data();
|
let data = self.federation_config.to_request_data();
|
||||||
let actor = get_local_actor(uuid, &data).await.map_err(|e| anyhow::anyhow!("{e}"))?;
|
let actor = get_local_actor(uuid, &data).await.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||||
let person = actor.into_json(&data).await.map_err(|e| anyhow::anyhow!("{e}"))?;
|
let person = actor.into_json(&data).await.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||||
Ok(serde_json::to_string(&WithContext::new_default(person))?)
|
Ok(serde_json::to_string(&WithContext::new(person, crate::urls::actor_ap_context()))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn followers_collection_json(&self, user_id: uuid::Uuid, page: Option<u32>) -> anyhow::Result<String> {
|
pub async fn followers_collection_json(&self, user_id: uuid::Uuid, page: Option<u32>) -> anyhow::Result<String> {
|
||||||
|
|||||||
271
src/tests/integration.rs
Normal file
271
src/tests/integration.rs
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
/// Integration tests exercising multi-component flows with in-memory trait stubs.
|
||||||
|
///
|
||||||
|
/// These tests don't spin up an HTTP server but they do exercise:
|
||||||
|
/// - `check_guards` idempotency (is_activity_processed → mark → duplicate rejected)
|
||||||
|
/// - `extract_and_dispatch_mentions` dispatches on_mention for local actors
|
||||||
|
/// - Multiple trait implementations wired through FederationData
|
||||||
|
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::content::ApObjectHandler;
|
||||||
|
use crate::data::FederationData;
|
||||||
|
use crate::repository::{
|
||||||
|
BlockedDomain, FederationRepository, Follower, FollowerStatus, FollowingStatus, RemoteActor,
|
||||||
|
};
|
||||||
|
use crate::user::{ApActorType, ApProfileField, ApUser, ApUserRepository};
|
||||||
|
|
||||||
|
// ── In-memory FederationRepository ───────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct MemRepo {
|
||||||
|
processed: Mutex<HashSet<String>>,
|
||||||
|
blocked_domains: Mutex<HashSet<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl FederationRepository for MemRepo {
|
||||||
|
async fn is_activity_processed(&self, id: &str) -> anyhow::Result<bool> {
|
||||||
|
Ok(self.processed.lock().await.contains(id))
|
||||||
|
}
|
||||||
|
async fn mark_activity_processed(&self, id: &str) -> anyhow::Result<()> {
|
||||||
|
self.processed.lock().await.insert(id.to_string());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn is_domain_blocked(&self, domain: &str) -> anyhow::Result<bool> {
|
||||||
|
Ok(self.blocked_domains.lock().await.contains(domain))
|
||||||
|
}
|
||||||
|
// ── stubs ────────────────────────────────────────────────────────────────
|
||||||
|
async fn add_follower(&self, _: uuid::Uuid, _: &str, _: FollowerStatus, _: &str) -> anyhow::Result<()> { Ok(()) }
|
||||||
|
async fn get_follower_follow_activity_id(&self, _: uuid::Uuid, _: &str) -> anyhow::Result<Option<String>> { Ok(None) }
|
||||||
|
async fn remove_follower(&self, _: uuid::Uuid, _: &str) -> anyhow::Result<()> { Ok(()) }
|
||||||
|
async fn get_followers(&self, _: uuid::Uuid) -> anyhow::Result<Vec<Follower>> { Ok(vec![]) }
|
||||||
|
async fn get_followers_page(&self, _: uuid::Uuid, _: u32, _: usize) -> anyhow::Result<Vec<Follower>> { Ok(vec![]) }
|
||||||
|
async fn count_followers(&self, _: uuid::Uuid) -> anyhow::Result<usize> { Ok(0) }
|
||||||
|
async fn get_following_page(&self, _: uuid::Uuid, _: u32, _: usize) -> anyhow::Result<Vec<RemoteActor>> { Ok(vec![]) }
|
||||||
|
async fn update_follower_status(&self, _: uuid::Uuid, _: &str, _: FollowerStatus) -> anyhow::Result<()> { Ok(()) }
|
||||||
|
async fn add_following(&self, _: uuid::Uuid, _: RemoteActor, _: &str) -> anyhow::Result<()> { Ok(()) }
|
||||||
|
async fn get_follow_activity_id(&self, _: uuid::Uuid, _: &str) -> anyhow::Result<Option<String>> { Ok(None) }
|
||||||
|
async fn remove_following(&self, _: uuid::Uuid, _: &str) -> anyhow::Result<()> { Ok(()) }
|
||||||
|
async fn get_following(&self, _: uuid::Uuid) -> anyhow::Result<Vec<RemoteActor>> { Ok(vec![]) }
|
||||||
|
async fn count_following(&self, _: uuid::Uuid) -> anyhow::Result<usize> { Ok(0) }
|
||||||
|
async fn upsert_remote_actor(&self, _: RemoteActor) -> anyhow::Result<()> { Ok(()) }
|
||||||
|
async fn get_remote_actor(&self, _: &str) -> anyhow::Result<Option<RemoteActor>> { Ok(None) }
|
||||||
|
async fn get_local_actor_keypair(&self, _: uuid::Uuid) -> anyhow::Result<Option<(String, String)>> { Ok(None) }
|
||||||
|
async fn save_local_actor_keypair(&self, _: uuid::Uuid, _: String, _: String) -> anyhow::Result<()> { Ok(()) }
|
||||||
|
async fn get_pending_followers(&self, _: uuid::Uuid) -> anyhow::Result<Vec<RemoteActor>> { Ok(vec![]) }
|
||||||
|
async fn update_following_status(&self, _: uuid::Uuid, _: &str, _: FollowingStatus) -> anyhow::Result<()> { Ok(()) }
|
||||||
|
async fn get_following_outbox_url(&self, _: uuid::Uuid, _: &str) -> anyhow::Result<Option<String>> { Ok(None) }
|
||||||
|
async fn add_announce(&self, _: &str, _: &str, _: &str, _: DateTime<Utc>) -> anyhow::Result<()> { Ok(()) }
|
||||||
|
async fn count_announces(&self, _: &str) -> anyhow::Result<usize> { Ok(0) }
|
||||||
|
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![]) }
|
||||||
|
async fn add_blocked_actor(&self, _: uuid::Uuid, _: &str) -> anyhow::Result<()> { Ok(()) }
|
||||||
|
async fn remove_blocked_actor(&self, _: uuid::Uuid, _: &str) -> anyhow::Result<()> { Ok(()) }
|
||||||
|
async fn get_blocked_actors(&self, _: uuid::Uuid) -> anyhow::Result<Vec<String>> { Ok(vec![]) }
|
||||||
|
async fn is_actor_blocked(&self, _: uuid::Uuid, _: &str) -> anyhow::Result<bool> { Ok(false) }
|
||||||
|
async fn migrate_follower_actor(&self, _: &str, _: &str) -> anyhow::Result<Vec<uuid::Uuid>> { Ok(vec![]) }
|
||||||
|
async fn get_accepted_follower_inboxes(&self, _: uuid::Uuid) -> anyhow::Result<Vec<String>> { Ok(vec![]) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── In-memory ApUserRepository ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
struct MemUserRepo {
|
||||||
|
users: HashMap<uuid::Uuid, ApUser>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MemUserRepo {
|
||||||
|
fn with_user(id: uuid::Uuid, username: &str) -> Self {
|
||||||
|
let mut users = HashMap::new();
|
||||||
|
users.insert(id, ApUser {
|
||||||
|
id,
|
||||||
|
username: username.to_string(),
|
||||||
|
display_name: None,
|
||||||
|
bio: None,
|
||||||
|
avatar_url: None,
|
||||||
|
banner_url: None,
|
||||||
|
also_known_as: None,
|
||||||
|
profile_url: None,
|
||||||
|
attachment: vec![],
|
||||||
|
manually_approves_followers: true,
|
||||||
|
actor_type: ApActorType::Person,
|
||||||
|
featured_url: None,
|
||||||
|
});
|
||||||
|
Self { users }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ApUserRepository for MemUserRepo {
|
||||||
|
async fn find_by_id(&self, id: uuid::Uuid) -> anyhow::Result<Option<ApUser>> {
|
||||||
|
Ok(self.users.get(&id).cloned())
|
||||||
|
}
|
||||||
|
async fn find_by_username(&self, username: &str) -> anyhow::Result<Option<ApUser>> {
|
||||||
|
Ok(self.users.values().find(|u| u.username == username).cloned())
|
||||||
|
}
|
||||||
|
async fn count_users(&self) -> anyhow::Result<usize> { Ok(self.users.len()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── In-memory ApObjectHandler ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct MemHandler {
|
||||||
|
creates: Mutex<Vec<Url>>,
|
||||||
|
mentions: Mutex<Vec<(Url, uuid::Uuid)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ApObjectHandler for MemHandler {
|
||||||
|
async fn get_local_objects_for_user(&self, _: uuid::Uuid) -> anyhow::Result<Vec<(Url, serde_json::Value)>> { Ok(vec![]) }
|
||||||
|
async fn get_local_objects_page(&self, _: uuid::Uuid, _: Option<DateTime<Utc>>, _: usize) -> anyhow::Result<Vec<(Url, serde_json::Value, DateTime<Utc>)>> { Ok(vec![]) }
|
||||||
|
async fn on_create(&self, ap_id: &Url, _: &Url, _: serde_json::Value) -> anyhow::Result<()> {
|
||||||
|
self.creates.lock().await.push(ap_id.clone());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn on_update(&self, _: &Url, _: &Url, _: serde_json::Value) -> anyhow::Result<()> { Ok(()) }
|
||||||
|
async fn on_delete(&self, _: &Url, _: &Url) -> anyhow::Result<()> { Ok(()) }
|
||||||
|
async fn on_actor_removed(&self, _: &Url) -> anyhow::Result<()> { Ok(()) }
|
||||||
|
async fn on_like(&self, _: &Url, _: &Url) -> anyhow::Result<()> { Ok(()) }
|
||||||
|
async fn on_announce_received(&self, _: &Url, _: &Url) -> anyhow::Result<()> { Ok(()) }
|
||||||
|
async fn on_unlike(&self, _: &Url, _: &Url) -> anyhow::Result<()> { Ok(()) }
|
||||||
|
async fn on_mention(&self, ap_id: &Url, user_id: uuid::Uuid, _: &Url) -> anyhow::Result<()> {
|
||||||
|
self.mentions.lock().await.push((ap_id.clone(), user_id));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn on_announce_of_remote(&self, _: &Url, _: &Url) -> anyhow::Result<()> { Ok(()) }
|
||||||
|
async fn count_local_posts(&self) -> anyhow::Result<u64> { Ok(0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helper ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn make_data(
|
||||||
|
repo: Arc<MemRepo>,
|
||||||
|
user_repo: Arc<MemUserRepo>,
|
||||||
|
handler: Arc<MemHandler>,
|
||||||
|
) -> FederationData {
|
||||||
|
FederationData::new(
|
||||||
|
repo,
|
||||||
|
user_repo,
|
||||||
|
handler,
|
||||||
|
"https://example.com".to_string(),
|
||||||
|
false,
|
||||||
|
"test".to_string(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tests ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn check_guards_idempotency() {
|
||||||
|
use crate::activities::helpers::{already_processed, check_guards};
|
||||||
|
use activitypub_federation::config::FederationConfig;
|
||||||
|
|
||||||
|
let repo = Arc::new(MemRepo::default());
|
||||||
|
let user_repo = Arc::new(MemUserRepo::with_user(uuid::Uuid::new_v4(), "alice"));
|
||||||
|
let handler = Arc::new(MemHandler::default());
|
||||||
|
let data_inner = make_data(repo, user_repo, handler);
|
||||||
|
|
||||||
|
let config = FederationConfig::builder()
|
||||||
|
.domain("example.com")
|
||||||
|
.app_data(data_inner)
|
||||||
|
.debug(true)
|
||||||
|
.build()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let data = config.to_request_data();
|
||||||
|
|
||||||
|
let activity_id: Url = "https://remote.example/activities/abc123".parse().unwrap();
|
||||||
|
let actor: Url = "https://remote.example/users/bob".parse().unwrap();
|
||||||
|
|
||||||
|
// First call: not processed yet → should NOT skip
|
||||||
|
let skip = check_guards(&activity_id, &actor, &data).await.unwrap();
|
||||||
|
assert!(!skip, "first delivery should not be skipped");
|
||||||
|
|
||||||
|
// Second call with same activity ID → should skip (duplicate)
|
||||||
|
let skip = check_guards(&activity_id, &actor, &data).await.unwrap();
|
||||||
|
assert!(skip, "duplicate delivery should be skipped");
|
||||||
|
|
||||||
|
// Different activity ID → should not skip
|
||||||
|
let other_id: Url = "https://remote.example/activities/xyz999".parse().unwrap();
|
||||||
|
let skip = check_guards(&other_id, &actor, &data).await.unwrap();
|
||||||
|
assert!(!skip, "different activity should not be skipped");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn check_guards_blocks_domain() {
|
||||||
|
use crate::activities::helpers::check_guards;
|
||||||
|
use activitypub_federation::config::FederationConfig;
|
||||||
|
|
||||||
|
let repo = Arc::new(MemRepo {
|
||||||
|
blocked_domains: Mutex::new(["spam.example".to_string()].into()),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
let user_repo = Arc::new(MemUserRepo::with_user(uuid::Uuid::new_v4(), "alice"));
|
||||||
|
let handler = Arc::new(MemHandler::default());
|
||||||
|
let data_inner = make_data(repo, user_repo, handler);
|
||||||
|
|
||||||
|
let config = FederationConfig::builder()
|
||||||
|
.domain("example.com")
|
||||||
|
.app_data(data_inner)
|
||||||
|
.debug(true)
|
||||||
|
.build()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let data = config.to_request_data();
|
||||||
|
|
||||||
|
let activity_id: Url = "https://spam.example/activities/1".parse().unwrap();
|
||||||
|
let actor: Url = "https://spam.example/users/evil".parse().unwrap();
|
||||||
|
|
||||||
|
// Blocked domain → should skip
|
||||||
|
let skip = check_guards(&activity_id, &actor, &data).await.unwrap();
|
||||||
|
assert!(skip, "activity from blocked domain should be skipped");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn extract_and_dispatch_mentions_notifies_local_users() {
|
||||||
|
use crate::activities::helpers::extract_and_dispatch_mentions;
|
||||||
|
use activitypub_federation::config::FederationConfig;
|
||||||
|
|
||||||
|
let local_user_id = uuid::Uuid::new_v4();
|
||||||
|
let user_repo = Arc::new(MemUserRepo::with_user(local_user_id, "alice"));
|
||||||
|
let handler = Arc::new(MemHandler::default());
|
||||||
|
let repo = Arc::new(MemRepo::default());
|
||||||
|
let data_inner = make_data(repo, user_repo.clone(), handler.clone());
|
||||||
|
|
||||||
|
let config = FederationConfig::builder()
|
||||||
|
.domain("example.com")
|
||||||
|
.app_data(data_inner)
|
||||||
|
.debug(true)
|
||||||
|
.build()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let data = config.to_request_data();
|
||||||
|
|
||||||
|
let ap_id: Url = "https://remote.example/notes/1".parse().unwrap();
|
||||||
|
let actor_url: Url = "https://remote.example/users/bob".parse().unwrap();
|
||||||
|
|
||||||
|
// Object with a Mention tag pointing to local user URL
|
||||||
|
let local_user_url = format!("https://example.com/users/{}", local_user_id);
|
||||||
|
let object = serde_json::json!({
|
||||||
|
"type": "Note",
|
||||||
|
"id": ap_id.as_str(),
|
||||||
|
"content": "Hello @alice",
|
||||||
|
"tag": [
|
||||||
|
{"type": "Mention", "href": local_user_url, "name": "@alice@example.com"}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
extract_and_dispatch_mentions(&ap_id, &actor_url, &object, &data).await;
|
||||||
|
|
||||||
|
let mentions = handler.mentions.lock().await;
|
||||||
|
assert_eq!(mentions.len(), 1);
|
||||||
|
assert_eq!(mentions[0].0, ap_id);
|
||||||
|
assert_eq!(mentions[0].1, local_user_id);
|
||||||
|
}
|
||||||
18
src/urls.rs
18
src/urls.rs
@@ -6,6 +6,24 @@ pub const AS_PUBLIC: &str = "https://www.w3.org/ns/activitystreams#Public";
|
|||||||
pub const AP_CONTEXT: &str = "https://www.w3.org/ns/activitystreams";
|
pub const AP_CONTEXT: &str = "https://www.w3.org/ns/activitystreams";
|
||||||
pub const AP_PAGE_SIZE: usize = 20;
|
pub const AP_PAGE_SIZE: usize = 20;
|
||||||
|
|
||||||
|
/// Returns the `@context` array for actor AP JSON.
|
||||||
|
/// Includes the W3C security vocabulary (needed for `publicKey` resolution)
|
||||||
|
/// and common Mastodon/Toot extensions (`discoverable`, `featured`, etc.).
|
||||||
|
/// Activities use `WithContext::new_default` (plain AS context) — only actor
|
||||||
|
/// JSON needs the security vocab.
|
||||||
|
pub fn actor_ap_context() -> serde_json::Value {
|
||||||
|
serde_json::json!([
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
"https://w3id.org/security/v1",
|
||||||
|
{
|
||||||
|
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
||||||
|
"toot": "http://joinmastodon.org/ns#",
|
||||||
|
"discoverable": "toot:discoverable",
|
||||||
|
"featured": {"@id": "toot:featured", "@type": "@id"}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
pub fn extract_user_id_from_url(url: &Url) -> Option<uuid::Uuid> {
|
pub fn extract_user_id_from_url(url: &Url) -> Option<uuid::Uuid> {
|
||||||
let path = url.path();
|
let path = url.path();
|
||||||
path.strip_prefix("/users/")
|
path.strip_prefix("/users/")
|
||||||
|
|||||||
Reference in New Issue
Block a user