docs: REST API cleanup design spec

This commit is contained in:
2026-05-14 20:38:05 +02:00
parent dbd891d60d
commit 93967e53a2

View File

@@ -0,0 +1,118 @@
# REST API Cleanup Design
Clean up the REST API to be professional, consistent, and RESTful. No new features — only renames, unifications, and content negotiation.
## Route Changes
| Before | After | Reason |
|--------|-------|--------|
| `GET /users/{username}/profile` | `GET /users/{username}` | content negotiation replaces the /profile workaround |
| `GET /federation/lookup?handle=` | `GET /users/lookup?handle=` | federation lookup belongs under /users |
| `POST /users/{id}/follow` | `POST /users/{username}/follow` | param was mislabelled; now also handles remote follows |
| `DELETE /users/{id}/follow` | `DELETE /users/{username}/follow` | param rename |
| `POST /users/{id}/block` | `POST /users/{username}/block` | param rename |
| `DELETE /users/{id}/block` | `DELETE /users/{username}/block` | param rename |
| `GET /users/{username}/follower-list` | `GET /users/{username}/followers` | verbose name |
| `GET /users/{username}/following-list` | `GET /users/{username}/following` | verbose name |
| `GET /users/me/following-list` | `GET /users/me/following` | verbose name |
| `POST /notifications/{id}/read` | `PATCH /notifications/{id}` | POST for state change → PATCH |
| `POST /notifications/read-all` | `PATCH /notifications` | POST bulk action → PATCH |
| `PUT /users/me` | removed | `PATCH /users/me` is sufficient |
| `POST /federation/follow` | removed | unified into `POST /users/{username}/follow` |
## Content Negotiation at `GET /users/{username}`
The AP router currently owns `/users/{username}` (returns `application/activity+json`). The REST profile was at `/users/{username}/profile` as a workaround.
**Solution:** Remove `/users/{username}` from the AP router. Add a single handler at `GET /users/{username}` in the REST router that checks the `Accept` header:
- `Accept: application/activity+json` → return AP actor JSON with `Content-Type: application/activity+json`
- Anything else → return `UserResponse` with `Content-Type: application/json`
**Implementation:**
Add `actor_json(&self, user_id: &UserId) -> Result<String, DomainError>` to `FederationActionPort` in domain. Implement in `ActivityPubService` by delegating to the existing `self.actor_json(&user_id.as_uuid().to_string())` inherent method.
The unified handler in `presentation/src/handlers/users.rs`:
1. Looks up user by username via `UserRepository` → 404 if not found
2. Checks `Accept` header
3. AP path: calls `s.federation.actor_json(&user.id)` → returns with `Content-Type: application/activity+json`
4. REST path: returns `UserResponse` as before
The AP router in `bootstrap/src/main.rs` no longer registers `/users/{username}`.
## Unified Follow at `POST /users/{username}/follow`
The handler detects whether `{username}` is a local user or a remote actor:
```rust
if username.contains('@') {
// Remote: e.g. "gabrielkaszewski@mastodon.social"
s.federation.follow_remote(&uid, &username).await?;
} else {
// Local: look up by username, call follow_user use case
let target = get_user_by_username(&*s.users, &username).await?;
follow_user(&*s.follows, &*s.events, &uid, &target.id).await?;
}
```
`POST /federation/follow` and `federation::follow_remote_handler` are deleted.
## Remote Actor Handle Format Fix
`lookup_actor` currently returns `handle: actor.username` (just `preferred_username`, e.g. `gabrielkaszewski`). Fix: return the full `user@domain` handle by extracting the domain from `actor.ap_id`:
```rust
let domain = actor.ap_id.host_str().unwrap_or("");
let full_handle = format!("{}@{}", actor.username, domain);
// RemoteActor { handle: full_handle, ... }
```
This means `RemoteActorResponse.handle` = `"gabrielkaszewski@mastodon.social"`, which the frontend passes directly to `POST /users/gabrielkaszewski@mastodon.social/follow`.
## Remote Unfollow Scope
`DELETE /users/{username}/follow` for a remote handle (contains `@`) is **out of scope**. The handler returns `501 Not Implemented` when `username` contains `@`. Remote unfollow requires an `Undo Follow` ActivityPub activity and is a separate feature.
## Notification Endpoints
Add `NotificationUpdateRequest { read: bool }` to `api-types/src/requests.rs`.
- `PATCH /notifications/{id}` — mark single notification read (body: `{"read": true}`)
- `PATCH /notifications` — mark all notifications read (body: `{"read": true}`)
Both replace their existing `POST` counterparts.
## Frontend (`thoughts-frontend/lib/api.ts`)
| Function | Change |
|----------|--------|
| `getUserProfile(username)` | URL: `/users/${username}/profile``/users/${username}` |
| `getFollowersList(username)` | URL: `/follower-list``/followers` |
| `getFollowingList(username)` | URL: `/following-list``/following` |
| `getMeFollowingList()` | URL: `/me/following-list``/me/following` |
| `lookupRemoteActor(handle)` | URL: `/federation/lookup?handle=``/users/lookup?handle=` |
| `followRemoteUser(handle)` | **Deleted** — use unified `followUser(handle)` instead |
| `markNotificationRead(id)` | **New**`PATCH /notifications/{id}` with body `{"read":true}` (no prior frontend impl) |
| `markAllNotificationsRead()` | **New**`PATCH /notifications` with body `{"read":true}` (no prior frontend impl) |
Also update `remote-user-card.tsx` to call `followUser(actor.handle, token)` instead of `followRemoteUser`.
## Files Touched
**Backend:**
- `crates/domain/src/ports.rs` — add `actor_json` to `FederationActionPort`
- `crates/domain/src/testing.rs` — add `actor_json` to `TestStore` impl
- `crates/adapters/activitypub-base/src/service.rs` — add `actor_json` to `FederationActionPort` impl; fix `lookup_actor` handle format
- `crates/presentation/src/handlers/users.rs` — unified `GET /users/{username}` handler; remove old `get_user` (was /profile)
- `crates/presentation/src/handlers/social.rs` — unify `post_follow`; rename `{id}``{username}` in follow/block; rename follower/following list handlers
- `crates/presentation/src/handlers/federation.rs` — delete `follow_remote_handler`; move `lookup_handler` to `users.rs`; delete file if empty
- `crates/presentation/src/handlers/notifications.rs` — replace read handlers with PATCH
- `crates/presentation/src/routes.rs` — all route changes
- `crates/api-types/src/requests.rs` — add `NotificationUpdateRequest`
- `crates/bootstrap/src/main.rs` — remove `/users/{username}` from ap_router
**Frontend:**
- `thoughts-frontend/lib/api.ts` — all URL/method changes listed above
- `thoughts-frontend/components/remote-user-card.tsx` — use `followUser` instead of `followRemoteUser`
- Any page that calls `getFollowersList`, `getFollowingList`, `getMeFollowingList`, `markNotificationRead`, `markAllNotificationsRead` (check all pages under `app/`)