3.0 KiB
Remote Actor Search & Follow
Allows local users to search for and follow users on other ActivityPub instances (e.g. @user@mastodon.social) directly from the existing search page.
Architecture
Approach A: new FederationActionPort domain trait + dedicated /federation/* REST endpoints. Keeps hexagonal arch intact — presentation has no dep on activitypub-base.
Domain changes
domain/src/models/remote_actor.rs — add avatar_url: Option<String>
domain/src/errors.rs — add ExternalService(String) variant
domain/src/ports.rs — new trait:
pub trait FederationActionPort: Send + Sync {
async fn lookup_actor(&self, handle: &str) -> Result<RemoteActor, DomainError>;
async fn follow_remote(&self, local_user_id: &UserId, handle: &str) -> Result<(), DomainError>;
}
activitypub-base impl
impl domain::ports::FederationActionPort for ActivityPubService in service.rs:
lookup_actor: callswebfinger_resolve_actor(handle, &data)→ mapsDbActortodomain::RemoteActorfollow_remote: delegates to existingself.follow(local_user_id.inner(), handle)(already handles WebFinger + Follow activity + federation DB record)
Bootstrap refactor
factory.rs currently builds FederationData + ApFederationConfig directly. Switch to ActivityPubService::new(...) which creates both internally. Infrastructure holds Arc<ActivityPubService> instead of ApFederationConfig. main.rs uses infra.ap_service.federation_config().middleware().
AppState gets one new field:
pub federation: Arc<dyn FederationActionPort>,
Wired to Arc::clone(&ap_service) in factory.
REST endpoints
api-types/src/responses.rs — new:
pub struct RemoteActorResponse {
pub handle: String,
pub display_name: Option<String>,
pub avatar_url: Option<String>,
pub url: String,
}
presentation/src/handlers/federation.rs (new file):
| Method | Path | Auth | Body | Response |
|---|---|---|---|---|
| GET | /federation/lookup?handle=@user@instance.tld |
none | — | RemoteActorResponse |
| POST | /federation/follow |
bearer | {"handle":"@user@instance.tld"} |
204 |
Mounted in routes.rs under /federation.
Error mapping: DomainError::ExternalService → 502, DomainError::NotFound → 404.
Frontend
lib/api.ts:
RemoteActorSchema+RemoteActortypelookupRemoteActor(handle, token)→GET /federation/lookup?handle=...followRemoteUser(handle, token)→POST /federation/follow
app/search/page.tsx:
- Detect
@user@instance.tldvia regex/^@[\w.-]+@[\w.-]+\.\w+$/ - If matches: call
lookupRemoteActorin parallel with local search - Pass remote actor result to component; show in Users tab above local results
components/remote-user-card.tsx (new client component):
- Displays avatar, handle, display name
- Follow button calls
followRemoteUser(handle, token) - No unfollow needed for MVP (remote following status not tracked locally)