Files
thoughts/docs/superpowers/specs/2026-05-14-remote-actor-search-follow-design.md

82 lines
3.0 KiB
Markdown

# 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:
```rust
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`: calls `webfinger_resolve_actor(handle, &data)` → maps `DbActor` to `domain::RemoteActor`
- `follow_remote`: delegates to existing `self.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:
```rust
pub federation: Arc<dyn FederationActionPort>,
```
Wired to `Arc::clone(&ap_service)` in factory.
## REST endpoints
**`api-types/src/responses.rs`** — new:
```rust
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` + `RemoteActor` type
- `lookupRemoteActor(handle, token)``GET /federation/lookup?handle=...`
- `followRemoteUser(handle, token)``POST /federation/follow`
**`app/search/page.tsx`**:
- Detect `@user@instance.tld` via regex `/^@[\w.-]+@[\w.-]+\.\w+$/`
- If matches: call `lookupRemoteActor` in 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)