wiki: add Migration Guide page
196
Migration-Guide.md
Normal file
196
Migration-Guide.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# Migration Guide — 0.1.x → 0.3.0
|
||||
|
||||
Work through each step top-to-bottom. Each step is independent after the previous one compiles.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Update `Cargo.toml`
|
||||
|
||||
```toml
|
||||
# Before
|
||||
k-ap = { git = "...", tag = "v0.1.10" }
|
||||
|
||||
# After (registry)
|
||||
k-ap = { version = "0.3.0", registry = "gitea" }
|
||||
|
||||
# After (git fallback)
|
||||
k-ap = { git = "https://git.gabrielkaszewski.dev/GKaszewski/k-ap.git", tag = "v0.3.0" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Rename `backfill_outbox` → `import_remote_outbox`
|
||||
|
||||
```rust
|
||||
// Before
|
||||
service.backfill_outbox(outbox_url, actor_url).await?;
|
||||
|
||||
// After
|
||||
service.import_remote_outbox(outbox_url, actor_url).await?;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Split `FederationRepository` into four traits
|
||||
|
||||
In 0.1.x one `FederationRepository` trait had ~34 methods. In 0.3.0 it is split into four:
|
||||
|
||||
| Trait | Methods |
|
||||
|-------|---------|
|
||||
| `ActivityRepository` | `is_activity_processed`, `mark_activity_processed` |
|
||||
| `FollowRepository` | 20 methods — follower/following graph + migration |
|
||||
| `ActorRepository` | 7 methods — keypairs, cache, announce tracking |
|
||||
| `BlocklistRepository` | 8 methods — domain and actor blocklists |
|
||||
|
||||
Implement each on your existing struct. You will need to add these **new required methods** that did not exist in 0.1.x:
|
||||
|
||||
**`FollowRepository` — new:**
|
||||
```rust
|
||||
async fn count_accepted_followers(&self, local_user_id: Uuid) -> Result<usize>;
|
||||
async fn get_accepted_followers_page(&self, local_user_id: Uuid, offset: u32, limit: usize) -> Result<Vec<RemoteActor>>;
|
||||
async fn get_following_outbox_url(&self, local_user_id: Uuid, remote_actor_url: &str) -> Result<Option<String>>;
|
||||
```
|
||||
|
||||
**`ActorRepository` — new:**
|
||||
```rust
|
||||
async fn remove_announce(&self, activity_id: &str, actor_url: &str) -> Result<()>;
|
||||
async fn count_announces(&self, object_url: &str) -> Result<usize>;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Split `ApObjectHandler` into `ApContentReader` + `ApObjectHandler`
|
||||
|
||||
```rust
|
||||
// Before — read methods on ApObjectHandler
|
||||
async fn get_local_objects_page(&self, user_id: Uuid, cursor: Option<String>, limit: u64) -> ...;
|
||||
async fn count_local_posts(&self, user_id: Uuid) -> ...;
|
||||
|
||||
// After — split into a separate trait
|
||||
impl ApContentReader for MyDb {
|
||||
async fn get_local_objects_page(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
before: Option<DateTime<Utc>>, // cursor changed to before-timestamp
|
||||
limit: usize,
|
||||
) -> anyhow::Result<Vec<(Url, serde_json::Value, DateTime<Utc>)>> {
|
||||
// return (ap_id, object_json, published_at) tuples, newest-first
|
||||
}
|
||||
|
||||
async fn count_local_posts(&self) -> anyhow::Result<u64> {
|
||||
// NOTE: no user_id parameter — counts ALL local posts for NodeInfo
|
||||
}
|
||||
|
||||
// get_featured_objects has a default empty impl — override for pinned posts
|
||||
}
|
||||
```
|
||||
|
||||
`get_local_objects_for_user` (if present) was removed as dead code.
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Update `ApObjectHandler` method signatures
|
||||
|
||||
Several callbacks changed signatures:
|
||||
|
||||
```rust
|
||||
// Before
|
||||
async fn on_create(&self, user_id: Uuid, object: Value) -> ...;
|
||||
async fn on_update(&self, user_id: Uuid, object: Value) -> ...;
|
||||
async fn on_delete(&self, user_id: Uuid, object_id: Url) -> ...;
|
||||
async fn on_like(&self, user_id: Uuid, actor_url: Url, object_id: Url) -> ...;
|
||||
async fn on_unlike(&self, user_id: Uuid, actor_url: Url, object_id: Url) -> ...;
|
||||
async fn on_announce(&self, user_id: Uuid, actor_url: Url, object_id: Url) -> ...;
|
||||
async fn on_mention(&self, user_id: Uuid, actor_url: Url, object: Value) -> ...;
|
||||
|
||||
// After
|
||||
async fn on_create(&self, ap_id: &Url, actor_url: &Url, object: Value) -> ...;
|
||||
async fn on_update(&self, ap_id: &Url, actor_url: &Url, object: Value) -> ...;
|
||||
async fn on_delete(&self, ap_id: &Url, actor_url: &Url) -> ...;
|
||||
async fn on_like(&self, object_url: &Url, actor_url: &Url) -> ...;
|
||||
async fn on_unlike(&self, object_url: &Url, actor_url: &Url) -> ...;
|
||||
async fn on_announce_received(&self, object_url: &Url, actor_url: &Url) -> ...; // renamed
|
||||
async fn on_mention(&self, object_ap_id: &Url, mentioned_user_id: Uuid, actor_url: &Url) -> ...;
|
||||
```
|
||||
|
||||
**New required methods** (no defaults):
|
||||
```rust
|
||||
async fn on_actor_removed(&self, actor_url: &Url) -> anyhow::Result<()>;
|
||||
async fn on_announce_of_remote(&self, object_url: &Url, actor_url: &Url) -> anyhow::Result<()>;
|
||||
```
|
||||
|
||||
**New optional method** (default no-op):
|
||||
```rust
|
||||
async fn on_announce_removed(&self, object_url: &Url, actor_url: &Url) -> anyhow::Result<()>;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Update `broadcast_create_note` and `broadcast_update_note`
|
||||
|
||||
```rust
|
||||
// Before
|
||||
service.broadcast_create_note(user_id, note_json, ApVisibility::Public).await?;
|
||||
|
||||
// After — pass inboxes of mentioned non-followers, or vec![] if none
|
||||
service.broadcast_create_note(user_id, note_json, ApVisibility::Public, mentioned_inboxes).await?;
|
||||
```
|
||||
|
||||
`ApVisibility` is a new enum. Replace any visibility handling:
|
||||
- Public post → `ApVisibility::Public`
|
||||
- Followers only → `ApVisibility::FollowersOnly`
|
||||
- No delivery → `ApVisibility::Private`
|
||||
|
||||
---
|
||||
|
||||
## Step 7: Update `also_known_as` fields
|
||||
|
||||
```rust
|
||||
// Before — Option<String>
|
||||
ApUser { also_known_as: Some("https://old.example/users/alice".to_string()), .. }
|
||||
|
||||
// After — Vec<String>
|
||||
ApUser { also_known_as: vec!["https://old.example/users/alice".to_string()], .. }
|
||||
```
|
||||
|
||||
Same change for `LookedUpActor::also_known_as`.
|
||||
|
||||
---
|
||||
|
||||
## Step 8: Update the builder
|
||||
|
||||
```rust
|
||||
// Before (positional arguments)
|
||||
ActivityPubService::builder(repo, user_repo, handler, "https://example.com")
|
||||
|
||||
// After (named setters)
|
||||
ActivityPubService::builder("https://example.com")
|
||||
.activity_repo(db.clone())
|
||||
.follow_repo(db.clone())
|
||||
.actor_repo(db.clone())
|
||||
.blocklist_repo(db.clone())
|
||||
.user_repo(db.clone())
|
||||
.content_reader(db.clone())
|
||||
.object_handler(db.clone())
|
||||
.build()
|
||||
.await?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 9: Add content negotiation routes
|
||||
|
||||
In 0.3.0, `GET /users/{id}`, `/followers`, and `/following` are no longer registered by `service.router()`.
|
||||
|
||||
Add these routes to your axum router and implement the content negotiation pattern. See [Routes Reference](Routes-Reference) for the full example with `service.actor_json()`, `service.followers_collection_json()`, and `service.following_collection_json()`.
|
||||
|
||||
---
|
||||
|
||||
## New opt-in features (no migration required)
|
||||
|
||||
These are new in 0.3.0 — no code breaks if you don't use them:
|
||||
|
||||
- `ApUser::discoverable: bool` — was hard-coded `true`; now configurable per user
|
||||
- `ApUser::actor_type: ApActorType` — was hard-coded `Person`; now configurable
|
||||
- `ApUser::featured_url: Option<Url>` — expose pinned posts; see [Pinned Posts](Pinned-Posts)
|
||||
- `service.block_actor` / `service.unblock_actor` — block management from the service
|
||||
Reference in New Issue
Block a user