feat(federation): enrich RemoteActor with bio, banner, followers/following URLs
Some checks failed
lint / lint (push) Failing after 9m25s
test / unit (push) Has been cancelled

Bumps k-ap to 0.3.1 which adds bio, banner_url, followers_url,
following_url, and also_known_as to RemoteActor. These are populated
from fetched actor JSON at follow/ingest time so followers and following
listings no longer return null profile fields.

Adds migration 015 for the new remote_actors columns, updates all JOIN
queries in postgres-federation to select and return the new fields, and
maps them through k_ap_actor_to_domain.

Also fixes clippy: remove empty line after doc comment in port.rs.
This commit is contained in:
2026-05-29 04:07:52 +02:00
parent bd370776fe
commit 5b4b747dd7
10 changed files with 151 additions and 262 deletions

6
Cargo.lock generated
View File

@@ -2017,9 +2017,9 @@ dependencies = [
[[package]] [[package]]
name = "k-ap" name = "k-ap"
version = "0.3.0" version = "0.3.1"
source = "sparse+https://git.gabrielkaszewski.dev/api/packages/GKaszewski/cargo/" source = "sparse+https://git.gabrielkaszewski.dev/api/packages/GKaszewski/cargo/"
checksum = "ce46ce331b0c85e5d9e1874056266d78d6a478f35ae188586fe488d840596346" checksum = "f73de37ac4feab6d7b78e73c60acbb07933c2be58dcbb12e8a34201f66e0480d"
dependencies = [ dependencies = [
"activitypub_federation", "activitypub_federation",
"anyhow", "anyhow",
@@ -4570,7 +4570,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.48.0",
] ]
[[package]] [[package]]

28
Makefile Normal file
View File

@@ -0,0 +1,28 @@
.DEFAULT_GOAL := check
# Run the full local check suite — same order as CI would.
check: fmt-check clippy test
@echo "✅ All checks passed"
# Apply rustfmt to all files.
fmt:
cargo fmt
# Check formatting without modifying files (CI-safe).
fmt-check:
cargo fmt --check
# Run Clippy and treat warnings as errors.
clippy:
cargo clippy -- -D warnings
# Run the test suite.
test:
cargo test
# Apply fmt + clippy auto-fixes in one shot.
fix:
cargo fmt
cargo clippy --fix --allow-dirty --allow-staged
.PHONY: check fmt fmt-check clippy test fix

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
k-ap = { version = "0.3.0", registry = "gitea" } k-ap = { version = "0.3.1", registry = "gitea" }
domain = { workspace = true } domain = { workspace = true }
url = { workspace = true } url = { workspace = true }
serde = { workspace = true } serde = { workspace = true }

View File

@@ -73,7 +73,6 @@ pub trait ActivityPubRepository: Send + Sync {
// ── Inbox processing (remote → local) ─────────────────────────── // ── Inbox processing (remote → local) ───────────────────────────
/// Persist an incoming remote Note. Idempotent on ap_id. /// Persist an incoming remote Note. Idempotent on ap_id.
async fn accept_note(&self, input: AcceptNoteInput<'_>) -> Result<ThoughtId, DomainError>; async fn accept_note(&self, input: AcceptNoteInput<'_>) -> Result<ThoughtId, DomainError>;
/// Apply an Update to a previously accepted remote Note. /// Apply an Update to a previously accepted remote Note.

View File

@@ -114,11 +114,11 @@ fn k_ap_actor_to_domain(a: k_ap::RemoteActor) -> DomainRemoteActor {
avatar_url: a.avatar_url, avatar_url: a.avatar_url,
outbox_url: a.outbox_url, outbox_url: a.outbox_url,
last_fetched_at: chrono::Utc::now(), last_fetched_at: chrono::Utc::now(),
bio: None, bio: a.bio,
banner_url: None, banner_url: a.banner_url,
also_known_as: None, also_known_as: a.also_known_as.into_iter().next(),
followers_url: None, followers_url: a.followers_url,
following_url: None, following_url: a.following_url,
attachment: vec![], attachment: vec![],
} }
} }

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
k-ap = { version = "0.3.0", registry = "gitea" } k-ap = { version = "0.3.1", registry = "gitea" }
sqlx = { workspace = true } sqlx = { workspace = true }
uuid = { workspace = true } uuid = { workspace = true }
chrono = { workspace = true } chrono = { workspace = true }

View File

@@ -35,7 +35,8 @@ fn str_status(s: &str) -> FollowerStatus {
} }
} }
fn map_remote_actor( #[derive(sqlx::FromRow)]
struct RemoteActorRow {
url: String, url: String,
handle: String, handle: String,
inbox_url: String, inbox_url: String,
@@ -43,15 +44,27 @@ fn map_remote_actor(
display_name: Option<String>, display_name: Option<String>,
avatar_url: Option<String>, avatar_url: Option<String>,
outbox_url: Option<String>, outbox_url: Option<String>,
) -> RemoteActor { bio: Option<String>,
banner_url: Option<String>,
followers_url: Option<String>,
following_url: Option<String>,
also_known_as: Option<Vec<String>>,
}
fn map_remote_actor(r: RemoteActorRow) -> RemoteActor {
RemoteActor { RemoteActor {
url, url: r.url,
handle, handle: r.handle,
inbox_url, inbox_url: r.inbox_url,
shared_inbox_url, shared_inbox_url: r.shared_inbox_url,
display_name, display_name: r.display_name,
avatar_url, avatar_url: r.avatar_url,
outbox_url, outbox_url: r.outbox_url,
bio: r.bio,
banner_url: r.banner_url,
followers_url: r.followers_url,
following_url: r.following_url,
also_known_as: r.also_known_as.unwrap_or_default(),
} }
} }
@@ -143,18 +156,15 @@ impl FollowRepository for PostgresFederationRepository {
async fn get_followers(&self, local_user_id: uuid::Uuid) -> Result<Vec<Follower>> { async fn get_followers(&self, local_user_id: uuid::Uuid) -> Result<Vec<Follower>> {
#[derive(sqlx::FromRow)] #[derive(sqlx::FromRow)]
struct Row { struct Row {
remote_actor_url: String, #[sqlx(flatten)]
actor: RemoteActorRow,
status: String, status: String,
handle: String,
inbox_url: String,
shared_inbox_url: Option<String>,
display_name: Option<String>,
avatar_url: Option<String>,
outbox_url: Option<String>,
} }
sqlx::query_as::<_, Row>( sqlx::query_as::<_, Row>(
"SELECT f.remote_actor_url, f.status, COALESCE(r.handle,'') AS handle, "SELECT f.remote_actor_url AS url, f.status,
COALESCE(r.inbox_url,'') AS inbox_url, r.shared_inbox_url, r.display_name, r.avatar_url, r.outbox_url COALESCE(r.handle,'') AS handle, COALESCE(r.inbox_url,'') AS inbox_url,
r.shared_inbox_url, r.display_name, r.avatar_url, r.outbox_url,
r.bio, r.banner_url, r.followers_url, r.following_url, r.also_known_as
FROM federation_followers f FROM federation_followers f
LEFT JOIN remote_actors r ON r.url=f.remote_actor_url LEFT JOIN remote_actors r ON r.url=f.remote_actor_url
WHERE f.local_user_id=$1 AND f.status='accepted'", WHERE f.local_user_id=$1 AND f.status='accepted'",
@@ -166,15 +176,7 @@ impl FollowRepository for PostgresFederationRepository {
.map(|rows| { .map(|rows| {
rows.into_iter() rows.into_iter()
.map(|r| Follower { .map(|r| Follower {
actor: map_remote_actor( actor: map_remote_actor(r.actor),
r.remote_actor_url,
r.handle,
r.inbox_url,
r.shared_inbox_url,
r.display_name,
r.avatar_url,
r.outbox_url,
),
status: str_status(&r.status), status: str_status(&r.status),
}) })
.collect() .collect()
@@ -189,18 +191,15 @@ impl FollowRepository for PostgresFederationRepository {
) -> Result<Vec<Follower>> { ) -> Result<Vec<Follower>> {
#[derive(sqlx::FromRow)] #[derive(sqlx::FromRow)]
struct Row { struct Row {
remote_actor_url: String, #[sqlx(flatten)]
actor: RemoteActorRow,
status: String, status: String,
handle: String,
inbox_url: String,
shared_inbox_url: Option<String>,
display_name: Option<String>,
avatar_url: Option<String>,
outbox_url: Option<String>,
} }
sqlx::query_as::<_, Row>( sqlx::query_as::<_, Row>(
"SELECT f.remote_actor_url, f.status, COALESCE(r.handle,'') AS handle, "SELECT f.remote_actor_url AS url, f.status,
COALESCE(r.inbox_url,'') AS inbox_url, r.shared_inbox_url, r.display_name, r.avatar_url, r.outbox_url COALESCE(r.handle,'') AS handle, COALESCE(r.inbox_url,'') AS inbox_url,
r.shared_inbox_url, r.display_name, r.avatar_url, r.outbox_url,
r.bio, r.banner_url, r.followers_url, r.following_url, r.also_known_as
FROM federation_followers f FROM federation_followers f
LEFT JOIN remote_actors r ON r.url=f.remote_actor_url LEFT JOIN remote_actors r ON r.url=f.remote_actor_url
WHERE f.local_user_id=$1 AND f.status='accepted' WHERE f.local_user_id=$1 AND f.status='accepted'
@@ -215,15 +214,7 @@ impl FollowRepository for PostgresFederationRepository {
.map(|rows| { .map(|rows| {
rows.into_iter() rows.into_iter()
.map(|r| Follower { .map(|r| Follower {
actor: map_remote_actor( actor: map_remote_actor(r.actor),
r.remote_actor_url,
r.handle,
r.inbox_url,
r.shared_inbox_url,
r.display_name,
r.avatar_url,
r.outbox_url,
),
status: str_status(&r.status), status: str_status(&r.status),
}) })
.collect() .collect()
@@ -258,19 +249,10 @@ impl FollowRepository for PostgresFederationRepository {
offset: u32, offset: u32,
limit: usize, limit: usize,
) -> Result<Vec<RemoteActor>> { ) -> Result<Vec<RemoteActor>> {
#[derive(sqlx::FromRow)] sqlx::query_as::<_, RemoteActorRow>(
struct Row { "SELECT f.remote_actor_url AS url, COALESCE(r.handle,'') AS handle,
remote_actor_url: String, COALESCE(r.inbox_url,'') AS inbox_url, r.shared_inbox_url, r.display_name, r.avatar_url, r.outbox_url,
handle: String, r.bio, r.banner_url, r.followers_url, r.following_url, r.also_known_as
inbox_url: String,
shared_inbox_url: Option<String>,
display_name: Option<String>,
avatar_url: Option<String>,
outbox_url: Option<String>,
}
sqlx::query_as::<_, Row>(
"SELECT f.remote_actor_url, COALESCE(r.handle,'') AS handle,
COALESCE(r.inbox_url,'') AS inbox_url, r.shared_inbox_url, r.display_name, r.avatar_url, r.outbox_url
FROM federation_followers f FROM federation_followers f
LEFT JOIN remote_actors r ON r.url=f.remote_actor_url LEFT JOIN remote_actors r ON r.url=f.remote_actor_url
WHERE f.local_user_id=$1 AND f.status='accepted' WHERE f.local_user_id=$1 AND f.status='accepted'
@@ -282,21 +264,7 @@ impl FollowRepository for PostgresFederationRepository {
.fetch_all(&self.pool) .fetch_all(&self.pool)
.await .await
.map_err(|e| anyhow!(e)) .map_err(|e| anyhow!(e))
.map(|rows| { .map(|rows| rows.into_iter().map(map_remote_actor).collect())
rows.into_iter()
.map(|r| {
map_remote_actor(
r.remote_actor_url,
r.handle,
r.inbox_url,
r.shared_inbox_url,
r.display_name,
r.avatar_url,
r.outbox_url,
)
})
.collect()
})
} }
async fn get_accepted_follower_inboxes( async fn get_accepted_follower_inboxes(
@@ -325,19 +293,10 @@ impl FollowRepository for PostgresFederationRepository {
} }
async fn get_pending_followers(&self, local_user_id: uuid::Uuid) -> Result<Vec<RemoteActor>> { async fn get_pending_followers(&self, local_user_id: uuid::Uuid) -> Result<Vec<RemoteActor>> {
#[derive(sqlx::FromRow)] sqlx::query_as::<_, RemoteActorRow>(
struct Row { "SELECT f.remote_actor_url AS url, COALESCE(r.handle,'') AS handle,
remote_actor_url: String, COALESCE(r.inbox_url,'') AS inbox_url, r.shared_inbox_url, r.display_name, r.avatar_url, r.outbox_url,
handle: String, r.bio, r.banner_url, r.followers_url, r.following_url, r.also_known_as
inbox_url: String,
shared_inbox_url: Option<String>,
display_name: Option<String>,
avatar_url: Option<String>,
outbox_url: Option<String>,
}
sqlx::query_as::<_, Row>(
"SELECT f.remote_actor_url, COALESCE(r.handle,'') AS handle,
COALESCE(r.inbox_url,'') AS inbox_url, r.shared_inbox_url, r.display_name, r.avatar_url, r.outbox_url
FROM federation_followers f FROM federation_followers f
LEFT JOIN remote_actors r ON r.url=f.remote_actor_url LEFT JOIN remote_actors r ON r.url=f.remote_actor_url
WHERE f.local_user_id=$1 AND f.status='pending'", WHERE f.local_user_id=$1 AND f.status='pending'",
@@ -346,21 +305,7 @@ impl FollowRepository for PostgresFederationRepository {
.fetch_all(&self.pool) .fetch_all(&self.pool)
.await .await
.map_err(|e| anyhow!(e)) .map_err(|e| anyhow!(e))
.map(|rows| { .map(|rows| rows.into_iter().map(map_remote_actor).collect())
rows.into_iter()
.map(|r| {
map_remote_actor(
r.remote_actor_url,
r.handle,
r.inbox_url,
r.shared_inbox_url,
r.display_name,
r.avatar_url,
r.outbox_url,
)
})
.collect()
})
} }
async fn update_follower_status( async fn update_follower_status(
@@ -432,19 +377,10 @@ impl FollowRepository for PostgresFederationRepository {
} }
async fn get_following(&self, local_user_id: uuid::Uuid) -> Result<Vec<RemoteActor>> { async fn get_following(&self, local_user_id: uuid::Uuid) -> Result<Vec<RemoteActor>> {
#[derive(sqlx::FromRow)] sqlx::query_as::<_, RemoteActorRow>(
struct Row { "SELECT f.remote_actor_url AS url, COALESCE(r.handle,'') AS handle,
remote_actor_url: String, COALESCE(r.inbox_url,'') AS inbox_url, r.shared_inbox_url, r.display_name, r.avatar_url, r.outbox_url,
handle: String, r.bio, r.banner_url, r.followers_url, r.following_url, r.also_known_as
inbox_url: String,
shared_inbox_url: Option<String>,
display_name: Option<String>,
avatar_url: Option<String>,
outbox_url: Option<String>,
}
sqlx::query_as::<_, Row>(
"SELECT f.remote_actor_url, COALESCE(r.handle,'') AS handle,
COALESCE(r.inbox_url,'') AS inbox_url, r.shared_inbox_url, r.display_name, r.avatar_url, r.outbox_url
FROM federation_following f FROM federation_following f
LEFT JOIN remote_actors r ON r.url=f.remote_actor_url LEFT JOIN remote_actors r ON r.url=f.remote_actor_url
WHERE f.local_user_id=$1", WHERE f.local_user_id=$1",
@@ -453,21 +389,7 @@ impl FollowRepository for PostgresFederationRepository {
.fetch_all(&self.pool) .fetch_all(&self.pool)
.await .await
.map_err(|e| anyhow!(e)) .map_err(|e| anyhow!(e))
.map(|rows| { .map(|rows| rows.into_iter().map(map_remote_actor).collect())
rows.into_iter()
.map(|r| {
map_remote_actor(
r.remote_actor_url,
r.handle,
r.inbox_url,
r.shared_inbox_url,
r.display_name,
r.avatar_url,
r.outbox_url,
)
})
.collect()
})
} }
async fn get_following_page( async fn get_following_page(
@@ -476,19 +398,10 @@ impl FollowRepository for PostgresFederationRepository {
offset: u32, offset: u32,
limit: usize, limit: usize,
) -> Result<Vec<RemoteActor>> { ) -> Result<Vec<RemoteActor>> {
#[derive(sqlx::FromRow)] sqlx::query_as::<_, RemoteActorRow>(
struct Row { "SELECT f.remote_actor_url AS url, COALESCE(r.handle,'') AS handle,
remote_actor_url: String, COALESCE(r.inbox_url,'') AS inbox_url, r.shared_inbox_url, r.display_name, r.avatar_url, r.outbox_url,
handle: String, r.bio, r.banner_url, r.followers_url, r.following_url, r.also_known_as
inbox_url: String,
shared_inbox_url: Option<String>,
display_name: Option<String>,
avatar_url: Option<String>,
outbox_url: Option<String>,
}
sqlx::query_as::<_, Row>(
"SELECT f.remote_actor_url, COALESCE(r.handle,'') AS handle,
COALESCE(r.inbox_url,'') AS inbox_url, r.shared_inbox_url, r.display_name, r.avatar_url, r.outbox_url
FROM federation_following f FROM federation_following f
LEFT JOIN remote_actors r ON r.url=f.remote_actor_url LEFT JOIN remote_actors r ON r.url=f.remote_actor_url
WHERE f.local_user_id=$1 WHERE f.local_user_id=$1
@@ -500,21 +413,7 @@ impl FollowRepository for PostgresFederationRepository {
.fetch_all(&self.pool) .fetch_all(&self.pool)
.await .await
.map_err(|e| anyhow!(e)) .map_err(|e| anyhow!(e))
.map(|rows| { .map(|rows| rows.into_iter().map(map_remote_actor).collect())
rows.into_iter()
.map(|r| {
map_remote_actor(
r.remote_actor_url,
r.handle,
r.inbox_url,
r.shared_inbox_url,
r.display_name,
r.avatar_url,
r.outbox_url,
)
})
.collect()
})
} }
async fn count_following(&self, local_user_id: uuid::Uuid) -> Result<usize> { async fn count_following(&self, local_user_id: uuid::Uuid) -> Result<usize> {
@@ -626,12 +525,22 @@ impl ActorRepository for PostgresFederationRepository {
} }
async fn upsert_remote_actor(&self, actor: RemoteActor) -> Result<()> { async fn upsert_remote_actor(&self, actor: RemoteActor) -> Result<()> {
let also_known_as: Option<Vec<String>> = if actor.also_known_as.is_empty() {
None
} else {
Some(actor.also_known_as)
};
sqlx::query( sqlx::query(
"INSERT INTO remote_actors(url,handle,display_name,inbox_url,shared_inbox_url,public_key,avatar_url,outbox_url,last_fetched_at) "INSERT INTO remote_actors(url,handle,display_name,inbox_url,shared_inbox_url,public_key,
VALUES($1,$2,$3,$4,$5,'',$6,$7,NOW()) avatar_url,outbox_url,bio,banner_url,followers_url,following_url,also_known_as,last_fetched_at)
ON CONFLICT(url) DO UPDATE SET handle=EXCLUDED.handle,display_name=EXCLUDED.display_name, VALUES($1,$2,$3,$4,$5,'',$6,$7,$8,$9,$10,$11,$12,NOW())
ON CONFLICT(url) DO UPDATE SET
handle=EXCLUDED.handle, display_name=EXCLUDED.display_name,
inbox_url=EXCLUDED.inbox_url, shared_inbox_url=EXCLUDED.shared_inbox_url, inbox_url=EXCLUDED.inbox_url, shared_inbox_url=EXCLUDED.shared_inbox_url,
avatar_url=EXCLUDED.avatar_url,outbox_url=EXCLUDED.outbox_url,last_fetched_at=NOW()", avatar_url=EXCLUDED.avatar_url, outbox_url=EXCLUDED.outbox_url,
bio=EXCLUDED.bio, banner_url=EXCLUDED.banner_url,
followers_url=EXCLUDED.followers_url, following_url=EXCLUDED.following_url,
also_known_as=EXCLUDED.also_known_as, last_fetched_at=NOW()",
) )
.bind(&actor.url) .bind(&actor.url)
.bind(&actor.handle) .bind(&actor.handle)
@@ -640,6 +549,11 @@ impl ActorRepository for PostgresFederationRepository {
.bind(&actor.shared_inbox_url) .bind(&actor.shared_inbox_url)
.bind(&actor.avatar_url) .bind(&actor.avatar_url)
.bind(&actor.outbox_url) .bind(&actor.outbox_url)
.bind(&actor.bio)
.bind(&actor.banner_url)
.bind(&actor.followers_url)
.bind(&actor.following_url)
.bind(also_known_as.as_deref())
.execute(&self.pool) .execute(&self.pool)
.await .await
.map_err(|e| anyhow!(e)) .map_err(|e| anyhow!(e))
@@ -647,36 +561,16 @@ impl ActorRepository for PostgresFederationRepository {
} }
async fn get_remote_actor(&self, actor_url: &str) -> Result<Option<RemoteActor>> { async fn get_remote_actor(&self, actor_url: &str) -> Result<Option<RemoteActor>> {
#[derive(sqlx::FromRow)] sqlx::query_as::<_, RemoteActorRow>(
struct Row { "SELECT url, handle, inbox_url, shared_inbox_url, display_name, avatar_url, outbox_url,
url: String, bio, banner_url, followers_url, following_url, also_known_as
handle: String, FROM remote_actors WHERE url=$1",
inbox_url: String,
shared_inbox_url: Option<String>,
display_name: Option<String>,
avatar_url: Option<String>,
outbox_url: Option<String>,
}
sqlx::query_as::<_, Row>(
"SELECT url,handle,inbox_url,shared_inbox_url,display_name,avatar_url,outbox_url FROM remote_actors WHERE url=$1",
) )
.bind(actor_url) .bind(actor_url)
.fetch_optional(&self.pool) .fetch_optional(&self.pool)
.await .await
.map_err(|e| anyhow!(e)) .map_err(|e| anyhow!(e))
.map(|o| { .map(|o| o.map(map_remote_actor))
o.map(|r| {
map_remote_actor(
r.url,
r.handle,
r.inbox_url,
r.shared_inbox_url,
r.display_name,
r.avatar_url,
r.outbox_url,
)
})
})
} }
async fn add_announce( async fn add_announce(
@@ -827,6 +721,17 @@ impl BlocklistRepository for PostgresFederationRepository {
// ── PostgresApUserRepository ────────────────────────────────────────────────── // ── PostgresApUserRepository ──────────────────────────────────────────────────
#[derive(sqlx::FromRow)]
struct UserRow {
id: uuid::Uuid,
username: String,
display_name: Option<String>,
bio: Option<String>,
avatar_url: Option<String>,
header_url: Option<String>,
also_known_as: Option<String>,
}
pub struct PostgresApUserRepository { pub struct PostgresApUserRepository {
pool: PgPool, pool: PgPool,
base_url: String, base_url: String,
@@ -837,27 +742,18 @@ impl PostgresApUserRepository {
Self { pool, base_url } Self { pool, base_url }
} }
fn row_to_ap_user( fn row_to_ap_user(&self, r: UserRow) -> ApUser {
&self, let profile_url = url::Url::parse(&format!("{}/users/{}", self.base_url, r.username)).ok();
id: uuid::Uuid, let avatar_url = r.avatar_url.and_then(|u| url::Url::parse(&u).ok());
username: String, let banner_url = r.header_url.and_then(|u| url::Url::parse(&u).ok());
display_name: Option<String>,
bio: Option<String>,
avatar_url: Option<String>,
header_url: Option<String>,
also_known_as: Option<String>,
) -> ApUser {
let profile_url = url::Url::parse(&format!("{}/users/{}", self.base_url, username)).ok();
let avatar_url = avatar_url.and_then(|u| url::Url::parse(&u).ok());
let banner_url = header_url.and_then(|u| url::Url::parse(&u).ok());
ApUser { ApUser {
id, id: r.id,
username, username: r.username,
display_name, display_name: r.display_name,
bio, bio: r.bio,
avatar_url, avatar_url,
banner_url, banner_url,
also_known_as: also_known_as.into_iter().collect(), also_known_as: r.also_known_as.into_iter().collect(),
profile_url, profile_url,
attachment: vec![], attachment: vec![],
manually_approves_followers: false, manually_approves_followers: false,
@@ -871,65 +767,25 @@ impl PostgresApUserRepository {
#[async_trait] #[async_trait]
impl ApUserRepository for PostgresApUserRepository { impl ApUserRepository for PostgresApUserRepository {
async fn find_by_id(&self, id: uuid::Uuid) -> Result<Option<ApUser>> { async fn find_by_id(&self, id: uuid::Uuid) -> Result<Option<ApUser>> {
#[derive(sqlx::FromRow)] let row = sqlx::query_as::<_, UserRow>(
struct Row {
id: uuid::Uuid,
username: String,
display_name: Option<String>,
bio: Option<String>,
avatar_url: Option<String>,
header_url: Option<String>,
also_known_as: Option<String>,
}
let row = sqlx::query_as::<_, Row>(
"SELECT id,username,display_name,bio,avatar_url,header_url,also_known_as FROM users WHERE id=$1 AND local=true", "SELECT id,username,display_name,bio,avatar_url,header_url,also_known_as FROM users WHERE id=$1 AND local=true",
) )
.bind(id) .bind(id)
.fetch_optional(&self.pool) .fetch_optional(&self.pool)
.await .await
.map_err(|e| anyhow!(e))?; .map_err(|e| anyhow!(e))?;
Ok(row.map(|r| { Ok(row.map(|r| self.row_to_ap_user(r)))
self.row_to_ap_user(
r.id,
r.username,
r.display_name,
r.bio,
r.avatar_url,
r.header_url,
r.also_known_as,
)
}))
} }
async fn find_by_username(&self, username: &str) -> Result<Option<ApUser>> { async fn find_by_username(&self, username: &str) -> Result<Option<ApUser>> {
#[derive(sqlx::FromRow)] let row = sqlx::query_as::<_, UserRow>(
struct Row {
id: uuid::Uuid,
username: String,
display_name: Option<String>,
bio: Option<String>,
avatar_url: Option<String>,
header_url: Option<String>,
also_known_as: Option<String>,
}
let row = sqlx::query_as::<_, Row>(
"SELECT id,username,display_name,bio,avatar_url,header_url,also_known_as FROM users WHERE username=$1 AND local=true", "SELECT id,username,display_name,bio,avatar_url,header_url,also_known_as FROM users WHERE username=$1 AND local=true",
) )
.bind(username) .bind(username)
.fetch_optional(&self.pool) .fetch_optional(&self.pool)
.await .await
.map_err(|e| anyhow!(e))?; .map_err(|e| anyhow!(e))?;
Ok(row.map(|r| { Ok(row.map(|r| self.row_to_ap_user(r)))
self.row_to_ap_user(
r.id,
r.username,
r.display_name,
r.bio,
r.avatar_url,
r.header_url,
r.also_known_as,
)
}))
} }
async fn count_users(&self) -> Result<usize> { async fn count_users(&self) -> Result<usize> {

View File

@@ -0,0 +1,6 @@
ALTER TABLE remote_actors
ADD COLUMN IF NOT EXISTS bio TEXT,
ADD COLUMN IF NOT EXISTS banner_url TEXT,
ADD COLUMN IF NOT EXISTS followers_url TEXT,
ADD COLUMN IF NOT EXISTS following_url TEXT,
ADD COLUMN IF NOT EXISTS also_known_as TEXT[];

View File

@@ -14,7 +14,7 @@ postgres = { workspace = true }
postgres-search = { workspace = true } postgres-search = { workspace = true }
postgres-federation = { workspace = true } postgres-federation = { workspace = true }
activitypub = { workspace = true } activitypub = { workspace = true }
k-ap = { version = "0.3.0", registry = "gitea" } k-ap = { version = "0.3.1", registry = "gitea" }
serde_json = { workspace = true } serde_json = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
nats = { workspace = true } nats = { workspace = true }

View File

@@ -13,7 +13,7 @@ application = { workspace = true }
nats = { workspace = true } nats = { workspace = true }
event-transport = { workspace = true } event-transport = { workspace = true }
event-payload = { workspace = true } event-payload = { workspace = true }
k-ap = { version = "0.3.0", registry = "gitea" } k-ap = { version = "0.3.1", registry = "gitea" }
activitypub = { workspace = true } activitypub = { workspace = true }
postgres = { workspace = true } postgres = { workspace = true }
postgres-federation = { workspace = true } postgres-federation = { workspace = true }