# 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` 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/`)