fix: pre-release improvements — scale, correctness, API clarity
#1 count_accepted_followers / get_accepted_followers_page: new DB-side methods on FollowRepository — no more loading all followers into memory to count or page them. #2 Move re-follows are now non-blocking: tokio::spawn instead of awaiting each sign_and_send inside receive() — inbox handler no longer stalls for slow remote servers during account migration. #3 Remove get_local_objects_for_user from ApContentReader (dead code). Backfill and outbox both use the paginated get_local_objects_page. #6 Rename backfill_outbox → import_remote_outbox with a clear doc explaining the direction (import FROM a remote server, not to a follower). #7 also_known_as: Option<String> → Vec<String> on ApUser, LookedUpActor, and DbActor. from_json now stores all aliases; move_act.rs checks all. Mentions: broadcast_create_note / broadcast_update_note now accept mentioned_inboxes: Vec<Url> — delivery goes to followers + mentioned actors who aren't already followers. Deduplication is done before sending. Pass vec![] if note has no external mentions. Docs: ApObjectHandler and ApContentReader now have complete doc comments with contracts, idempotency guidance, and error-handling semantics.
This commit is contained in:
@@ -56,63 +56,85 @@ impl Activity for MoveActivity {
|
||||
.dereference(data)
|
||||
.await
|
||||
.map_err(|e| Error::from(anyhow::anyhow!("{e}")))?;
|
||||
if target.also_known_as.as_deref() != Some(self.object.as_str()) {
|
||||
// Verify the new actor claims the old identity via alsoKnownAs.
|
||||
// The spec allows multiple aliases; check all of them.
|
||||
let old_url = self.object.as_str();
|
||||
if !target.also_known_as.iter().any(|a| a == old_url) {
|
||||
return Err(Error::bad_request(anyhow::anyhow!(
|
||||
"Move target alsoKnownAs does not reference old actor"
|
||||
)));
|
||||
}
|
||||
let affected = data
|
||||
.follow_repo
|
||||
.migrate_follower_actor(self.object.as_str(), self.target.as_str())
|
||||
.migrate_follower_actor(old_url, self.target.as_str())
|
||||
.await
|
||||
.map_err(|e| Error::from(anyhow::anyhow!("{e}")))?;
|
||||
let affected_count = affected.len();
|
||||
for local_user_id in &affected {
|
||||
let local_actor = match crate::actors::get_local_actor(*local_user_id, data).await {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, %local_user_id, "Move: failed to load local actor");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let follow_id = match crate::urls::activity_url(&data.base_url) {
|
||||
Ok(u) => u,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "Move: failed to generate follow activity URL");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let follow = FollowActivity {
|
||||
id: follow_id,
|
||||
kind: Default::default(),
|
||||
actor: ObjectId::from(local_actor.ap_id.clone()),
|
||||
object: ObjectId::from(self.target.clone()),
|
||||
};
|
||||
let sends = match SendActivityTask::prepare(
|
||||
&WithContext::new_default(follow),
|
||||
&local_actor,
|
||||
vec![target.inbox_url.clone()],
|
||||
data,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "Move: failed to prepare re-follow");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
for send in sends {
|
||||
if let Err(e) = send.sign_and_send(data).await {
|
||||
tracing::warn!(error = %e, %local_user_id, "Move: re-follow delivery failed");
|
||||
|
||||
// Spawn re-follows in the background — do NOT await them inside receive()
|
||||
// to avoid blocking the inbox handler while making outbound HTTP requests.
|
||||
let target_inbox = target.inbox_url.clone();
|
||||
let target_url = self.target.clone();
|
||||
let base_url = data.base_url.clone();
|
||||
let data_clone = data.clone();
|
||||
tokio::spawn(async move {
|
||||
for local_user_id in &affected {
|
||||
let local_actor =
|
||||
match crate::actors::get_local_actor(*local_user_id, &data_clone).await {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
error = %e,
|
||||
%local_user_id,
|
||||
"Move: failed to load local actor"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let follow_id = match crate::urls::activity_url(&base_url) {
|
||||
Ok(u) => u,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "Move: failed to generate follow activity URL");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let follow = FollowActivity {
|
||||
id: follow_id,
|
||||
kind: Default::default(),
|
||||
actor: ObjectId::from(local_actor.ap_id.clone()),
|
||||
object: ObjectId::from(target_url.clone()),
|
||||
};
|
||||
let sends = match SendActivityTask::prepare(
|
||||
&WithContext::new_default(follow),
|
||||
&local_actor,
|
||||
vec![target_inbox.clone()],
|
||||
&data_clone,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "Move: failed to prepare re-follow");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
for send in sends {
|
||||
if let Err(e) = send.sign_and_send(&data_clone).await {
|
||||
tracing::warn!(
|
||||
error = %e,
|
||||
%local_user_id,
|
||||
"Move: re-follow delivery failed"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tracing::info!(
|
||||
actor = %self.actor.inner(),
|
||||
target = %self.target,
|
||||
affected = affected_count,
|
||||
"received Move — migrated follower relationships"
|
||||
"received Move — migrated follower relationships, re-follows spawned"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user