feat: migrate k-ap 0.1.10→0.3.1, fix AP gaps
- split FederationRepository into FollowRepository, ActorRepository, BlocklistRepository, ActivityRepository
- RemoteActor: 5 new fields (bio, banner_url, followers_url, following_url, also_known_as)
- ApObjectHandler split: get_local_objects_page/count_local_posts → ApContentReader trait
- builder API: positional args → named setters
- broadcast_create_note/update_note: add ApVisibility + mentioned_inboxes params
- backfill_outbox → import_remote_outbox
- ApUser: also_known_as Option<String> → Vec<String>, new fields
AP gaps fixed:
- add GET /users/{id}/followers + /following with content negotiation
- wire EventPublisher into builder via FederationEventBridge adapter
- add display_name field full stack (domain→DB→API→AP)
- DB-side outbox pagination (get_local_reviews_page)
- set featured_url on ApUser
This commit is contained in:
12
crates/adapters/postgres/migrations/0020_kap_v03.sql
Normal file
12
crates/adapters/postgres/migrations/0020_kap_v03.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
-- k-ap 0.3: new RemoteActor fields + activity dedup table
|
||||
|
||||
ALTER TABLE ap_remote_actors ADD COLUMN IF NOT EXISTS bio TEXT;
|
||||
ALTER TABLE ap_remote_actors ADD COLUMN IF NOT EXISTS banner_url TEXT;
|
||||
ALTER TABLE ap_remote_actors ADD COLUMN IF NOT EXISTS followers_url TEXT;
|
||||
ALTER TABLE ap_remote_actors ADD COLUMN IF NOT EXISTS following_url TEXT;
|
||||
ALTER TABLE ap_remote_actors ADD COLUMN IF NOT EXISTS also_known_as TEXT; -- JSON array
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ap_activities (
|
||||
id TEXT PRIMARY KEY,
|
||||
processed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS display_name TEXT;
|
||||
@@ -145,4 +145,55 @@ impl LocalApContentQuery for PostgresApContentQuery {
|
||||
.map_err(Self::map_err)?;
|
||||
Ok(count as u64)
|
||||
}
|
||||
|
||||
async fn get_local_reviews_page(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
before: Option<chrono::NaiveDateTime>,
|
||||
limit: usize,
|
||||
) -> Result<Vec<DiaryEntry>, DomainError> {
|
||||
let uid = user_id.value().to_string();
|
||||
let limit_i64 = limit as i64;
|
||||
|
||||
let rows = if let Some(before_ts) = before {
|
||||
let ts = before_ts.format("%Y-%m-%d %H:%M:%S").to_string();
|
||||
sqlx::query_as::<_, DiaryRow>(
|
||||
"SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,
|
||||
r.id AS review_id, r.movie_id, r.user_id, r.rating, r.comment,
|
||||
to_char(r.watched_at AT TIME ZONE 'UTC', 'YYYY-MM-DD HH24:MI:SS') AS watched_at,
|
||||
to_char(r.created_at AT TIME ZONE 'UTC', 'YYYY-MM-DD HH24:MI:SS') AS created_at,
|
||||
r.remote_actor_url
|
||||
FROM reviews r
|
||||
INNER JOIN movies m ON m.id = r.movie_id
|
||||
WHERE r.user_id = $1 AND r.remote_actor_url IS NULL AND r.watched_at < $2::timestamptz
|
||||
ORDER BY r.watched_at DESC
|
||||
LIMIT $3",
|
||||
)
|
||||
.bind(&uid)
|
||||
.bind(&ts)
|
||||
.bind(limit_i64)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?
|
||||
} else {
|
||||
sqlx::query_as::<_, DiaryRow>(
|
||||
"SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,
|
||||
r.id AS review_id, r.movie_id, r.user_id, r.rating, r.comment,
|
||||
to_char(r.watched_at AT TIME ZONE 'UTC', 'YYYY-MM-DD HH24:MI:SS') AS watched_at,
|
||||
to_char(r.created_at AT TIME ZONE 'UTC', 'YYYY-MM-DD HH24:MI:SS') AS created_at,
|
||||
r.remote_actor_url
|
||||
FROM reviews r
|
||||
INNER JOIN movies m ON m.id = r.movie_id
|
||||
WHERE r.user_id = $1 AND r.remote_actor_url IS NULL
|
||||
ORDER BY r.watched_at DESC
|
||||
LIMIT $2",
|
||||
)
|
||||
.bind(&uid)
|
||||
.bind(limit_i64)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?
|
||||
};
|
||||
rows.into_iter().map(DiaryRow::into_domain).collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ impl PostgresUserRepository {
|
||||
username_str: String,
|
||||
hash_str: String,
|
||||
role: UserRole,
|
||||
display_name: Option<String>,
|
||||
bio: Option<String>,
|
||||
avatar_path: Option<String>,
|
||||
banner_path: Option<String>,
|
||||
@@ -58,6 +59,7 @@ impl PostgresUserRepository {
|
||||
username,
|
||||
hash,
|
||||
role,
|
||||
display_name,
|
||||
bio,
|
||||
avatar_path,
|
||||
banner_path,
|
||||
@@ -78,13 +80,14 @@ impl UserRepository for PostgresUserRepository {
|
||||
username: String,
|
||||
password_hash: String,
|
||||
role: String,
|
||||
display_name: Option<String>,
|
||||
bio: Option<String>,
|
||||
avatar_path: Option<String>,
|
||||
banner_path: Option<String>,
|
||||
also_known_as: Option<String>,
|
||||
}
|
||||
let row = sqlx::query_as::<_, Row>(
|
||||
"SELECT id, email, username, password_hash, role, bio, avatar_path, banner_path, also_known_as FROM users WHERE email = $1",
|
||||
"SELECT id, email, username, password_hash, role, display_name, bio, avatar_path, banner_path, also_known_as FROM users WHERE email = $1",
|
||||
)
|
||||
.bind(email_str)
|
||||
.fetch_optional(&self.pool)
|
||||
@@ -97,6 +100,7 @@ impl UserRepository for PostgresUserRepository {
|
||||
r.username,
|
||||
r.password_hash,
|
||||
Self::parse_role(&r.role),
|
||||
r.display_name,
|
||||
r.bio,
|
||||
r.avatar_path,
|
||||
r.banner_path,
|
||||
@@ -116,13 +120,14 @@ impl UserRepository for PostgresUserRepository {
|
||||
username: String,
|
||||
password_hash: String,
|
||||
role: String,
|
||||
display_name: Option<String>,
|
||||
bio: Option<String>,
|
||||
avatar_path: Option<String>,
|
||||
banner_path: Option<String>,
|
||||
also_known_as: Option<String>,
|
||||
}
|
||||
let row = sqlx::query_as::<_, Row>(
|
||||
"SELECT id, email, username, password_hash, role, bio, avatar_path, banner_path, also_known_as FROM users WHERE username = $1",
|
||||
"SELECT id, email, username, password_hash, role, display_name, bio, avatar_path, banner_path, also_known_as FROM users WHERE username = $1",
|
||||
)
|
||||
.bind(username_str)
|
||||
.fetch_optional(&self.pool)
|
||||
@@ -135,6 +140,7 @@ impl UserRepository for PostgresUserRepository {
|
||||
r.username,
|
||||
r.password_hash,
|
||||
Self::parse_role(&r.role),
|
||||
r.display_name,
|
||||
r.bio,
|
||||
r.avatar_path,
|
||||
r.banner_path,
|
||||
@@ -192,13 +198,14 @@ impl UserRepository for PostgresUserRepository {
|
||||
username: String,
|
||||
password_hash: String,
|
||||
role: String,
|
||||
display_name: Option<String>,
|
||||
bio: Option<String>,
|
||||
avatar_path: Option<String>,
|
||||
banner_path: Option<String>,
|
||||
also_known_as: Option<String>,
|
||||
}
|
||||
let row = sqlx::query_as::<_, Row>(
|
||||
"SELECT id, email, username, password_hash, role, bio, avatar_path, banner_path, also_known_as FROM users WHERE id = $1",
|
||||
"SELECT id, email, username, password_hash, role, display_name, bio, avatar_path, banner_path, also_known_as FROM users WHERE id = $1",
|
||||
)
|
||||
.bind(&id_str)
|
||||
.fetch_optional(&self.pool)
|
||||
@@ -234,6 +241,7 @@ impl UserRepository for PostgresUserRepository {
|
||||
r.username,
|
||||
r.password_hash,
|
||||
Self::parse_role(&r.role),
|
||||
r.display_name,
|
||||
r.bio,
|
||||
r.avatar_path,
|
||||
r.banner_path,
|
||||
@@ -246,6 +254,7 @@ impl UserRepository for PostgresUserRepository {
|
||||
async fn update_profile(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
display_name: Option<String>,
|
||||
bio: Option<String>,
|
||||
avatar_path: Option<String>,
|
||||
banner_path: Option<String>,
|
||||
@@ -253,8 +262,9 @@ impl UserRepository for PostgresUserRepository {
|
||||
) -> Result<(), DomainError> {
|
||||
let id_str = user_id.value().to_string();
|
||||
sqlx::query(
|
||||
"UPDATE users SET bio = $1, avatar_path = $2, banner_path = $3, also_known_as = $4 WHERE id = $5",
|
||||
"UPDATE users SET display_name = $1, bio = $2, avatar_path = $3, banner_path = $4, also_known_as = $5 WHERE id = $6",
|
||||
)
|
||||
.bind(&display_name)
|
||||
.bind(&bio)
|
||||
.bind(&avatar_path)
|
||||
.bind(&banner_path)
|
||||
|
||||
Reference in New Issue
Block a user