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