docs: remote actor search & follow spec
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user