fix(federation): fix 27 AP bugs, gaps, and inconsistencies
Round 1 — 18 bug fixes:
- remote likes/boosts now persist in engagement tables
- intern_remote_actor uses name@domain, expanded username to VARCHAR(255)
- PgRemoteActorRepository upsert/find now handles all fields
- update_following_status no longer a no-op, count_followers counts all
- accept/reject follow publishes event before DB mark (atomicity)
- fetch_outbox_page follows pagination via next links
- actor URL canonicalized to /users/{uuid}
- content_to_html escapes single quotes
- WebFinger accepts application/ld+json type
- try_from_ap accepts Article and Page object types
- feed SQL uses parameterized viewer UUID instead of format!
- content cap raised from 500 to 5000 chars
- also_known_as changed from Option<String> to Vec<String>
- connections fetch always triggers from page 1
Round 2 — 9 gap fixes:
- on_announce_removed handler deletes boost row on Undo(Announce)
- on_update handles Person/Service/Group actor profile updates
- sync_remote_actor_to_user syncs remote_actors → users on create/update
- FederationBlockPort: block_by_username sends Block activity to remote
- domain RemoteActor gains inbox_url, shared_inbox_url fields
- remote_actors attachment column (JSONB) with read/write
- .well-known/host-meta endpoint
- 256KB body limit on AP inbox routes
- outbox cleanup job (7-day retention, hourly sweep)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
use crate::db_error::IntoDbResult;
|
||||
use async_trait::async_trait;
|
||||
|
||||
const MAX_REMOTE_CONTENT_CHARS: usize = 500;
|
||||
const MAX_REMOTE_CONTENT_CHARS: usize = 5000;
|
||||
const THOUGHTS_PATH_PREFIX: &str = "/thoughts/";
|
||||
use chrono::{DateTime, Utc};
|
||||
use sqlx::PgPool;
|
||||
@@ -155,24 +155,28 @@ impl ActivityPubRepository for PgActivityPubRepository {
|
||||
return Ok(id);
|
||||
}
|
||||
let new_id = uuid::Uuid::new_v4();
|
||||
// Use the last path segment as username (e.g. /users/alice → "alice").
|
||||
// Falls back to a random short id for long segments (e.g. UUID-based actor URLs).
|
||||
// username column is VARCHAR(32).
|
||||
let last_seg = url::Url::parse(actor_ap_url)
|
||||
.ok()
|
||||
let parsed = url::Url::parse(actor_ap_url).ok();
|
||||
let domain_str = parsed
|
||||
.as_ref()
|
||||
.and_then(|u| u.host_str().map(|s| s.to_string()))
|
||||
.unwrap_or_default();
|
||||
let last_seg = parsed
|
||||
.and_then(|u| {
|
||||
u.path_segments()
|
||||
.and_then(|mut s| s.next_back().map(|s| s.to_string()))
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let handle = if last_seg.is_empty() {
|
||||
format!("remote_{}", &new_id.to_string()[..13])
|
||||
} else if last_seg.len() <= 32 {
|
||||
last_seg
|
||||
let handle = if last_seg.is_empty() || domain_str.is_empty() {
|
||||
format!("r_{}", &new_id.to_string()[..13])
|
||||
} else {
|
||||
format!("remote_{}", &new_id.to_string()[..13])
|
||||
let candidate = format!("{}@{}", last_seg, domain_str);
|
||||
if candidate.len() <= 255 {
|
||||
candidate
|
||||
} else {
|
||||
format!("r_{}", &new_id.to_string()[..13])
|
||||
}
|
||||
};
|
||||
sqlx::query(
|
||||
let result = sqlx::query(
|
||||
"INSERT INTO users(id,username,email,password_hash,local,ap_id,created_at,updated_at)
|
||||
VALUES($1,$2,$3,'',false,$4,NOW(),NOW()) ON CONFLICT(ap_id) DO NOTHING",
|
||||
)
|
||||
@@ -181,9 +185,24 @@ impl ActivityPubRepository for PgActivityPubRepository {
|
||||
.bind(format!("{}@remote", new_id))
|
||||
.bind(actor_ap_url)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.into_domain()?;
|
||||
// Re-fetch to get whichever id won the race
|
||||
.await;
|
||||
|
||||
if result.is_err() {
|
||||
let fallback = format!("r_{}", &new_id.to_string()[..13]);
|
||||
let new_id2 = uuid::Uuid::new_v4();
|
||||
sqlx::query(
|
||||
"INSERT INTO users(id,username,email,password_hash,local,ap_id,created_at,updated_at)
|
||||
VALUES($1,$2,$3,'',false,$4,NOW(),NOW()) ON CONFLICT(ap_id) DO NOTHING",
|
||||
)
|
||||
.bind(new_id2)
|
||||
.bind(&fallback)
|
||||
.bind(format!("{}@remote", new_id2))
|
||||
.bind(actor_ap_url)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.into_domain()?;
|
||||
}
|
||||
|
||||
self.find_remote_actor_id(actor_ap_url)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
@@ -345,6 +364,19 @@ impl ActivityPubRepository for PgActivityPubRepository {
|
||||
.into_domain()
|
||||
.map(|opt| opt.map(|(ap_id, inbox_url)| ActorApUrls { ap_id, inbox_url }))
|
||||
}
|
||||
|
||||
async fn sync_remote_actor_to_user(&self, actor_ap_url: &str) -> Result<(), DomainError> {
|
||||
sqlx::query(
|
||||
"UPDATE users SET display_name = ra.display_name, avatar_url = ra.avatar_url, updated_at = NOW()
|
||||
FROM remote_actors ra
|
||||
WHERE users.ap_id = ra.url AND users.ap_id = $1 AND users.local = false",
|
||||
)
|
||||
.bind(actor_ap_url)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.into_domain()
|
||||
.map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user