feat: implement merge readiness plan to close gaps between v2 and v1
Some checks failed
lint / lint (push) Has been cancelled
test / unit (push) Has been cancelled
test / integration (push) Has been cancelled
lint / lint (pull_request) Failing after 5m8s
test / unit (pull_request) Successful in 16m18s
test / integration (pull_request) Failing after 16m59s

- Task 1: Fix feed response hydration by adding `to_thought_response` helper and updating feed handlers to return full `ThoughtResponse`.
- Task 2: Wire follower/following REST routes for user feeds.
- Task 3: Add user listing and count endpoints, including `GET /users` and `GET /users/count`.
- Task 4: Implement popular tags feature with `GET /tags/popular`.
- Task 5: Enhance configuration with HOST, CORS_ORIGINS, and optional rate limiting using tower-governor.
This commit is contained in:
2026-05-14 16:28:18 +02:00
parent e6f4a6256f
commit 004bfb427b
30 changed files with 8716 additions and 808 deletions

View File

@@ -22,7 +22,10 @@ impl PgActivityPubRepository {
#[async_trait]
impl ActivityPubRepository for PgActivityPubRepository {
async fn outbox_entries_for_actor(&self, user_id: &UserId) -> Result<Vec<OutboxEntry>, DomainError> {
async fn outbox_entries_for_actor(
&self,
user_id: &UserId,
) -> Result<Vec<OutboxEntry>, DomainError> {
#[derive(sqlx::FromRow)]
struct Row {
id: uuid::Uuid,
@@ -134,7 +137,10 @@ impl ActivityPubRepository for PgActivityPubRepository {
.collect())
}
async fn find_remote_actor_id(&self, actor_ap_url: &Url) -> Result<Option<UserId>, DomainError> {
async fn find_remote_actor_id(
&self,
actor_ap_url: &Url,
) -> Result<Option<UserId>, DomainError> {
sqlx::query_scalar::<_, uuid::Uuid>("SELECT id FROM users WHERE ap_id=$1")
.bind(actor_ap_url.as_str())
.fetch_optional(&self.pool)
@@ -148,7 +154,10 @@ impl ActivityPubRepository for PgActivityPubRepository {
return Ok(id);
}
let new_id = uuid::Uuid::new_v4();
let handle = actor_ap_url.path().trim_start_matches('/').replace('/', "_");
let handle = actor_ap_url
.path()
.trim_start_matches('/')
.replace('/', "_");
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",
@@ -163,7 +172,11 @@ impl ActivityPubRepository for PgActivityPubRepository {
// Re-fetch to get whichever id won the race
self.find_remote_actor_id(actor_ap_url)
.await?
.ok_or_else(|| DomainError::Internal("intern_remote_actor: insert succeeded but row not found".into()))
.ok_or_else(|| {
DomainError::Internal(
"intern_remote_actor: insert succeeded but row not found".into(),
)
})
}
async fn accept_note(
@@ -195,13 +208,15 @@ impl ActivityPubRepository for PgActivityPubRepository {
async fn apply_note_update(&self, ap_id: &Url, new_content: &str) -> Result<(), DomainError> {
let capped: String = new_content.chars().take(500).collect();
sqlx::query("UPDATE thoughts SET content=$2,updated_at=NOW() WHERE ap_id=$1 AND local=false")
.bind(ap_id.as_str())
.bind(&capped)
.execute(&self.pool)
.await
.map_err(|e| DomainError::Internal(e.to_string()))
.map(|_| ())
sqlx::query(
"UPDATE thoughts SET content=$2,updated_at=NOW() WHERE ap_id=$1 AND local=false",
)
.bind(ap_id.as_str())
.bind(&capped)
.execute(&self.pool)
.await
.map_err(|e| DomainError::Internal(e.to_string()))
.map(|_| ())
}
async fn retract_note(&self, ap_id: &Url) -> Result<(), DomainError> {
@@ -253,9 +268,16 @@ mod tests {
let actor_url = url::Url::parse("https://remote.example/users/bob").unwrap();
let ap_id = url::Url::parse("https://remote.example/notes/1").unwrap();
let author = repo.intern_remote_actor(&actor_url).await.unwrap();
repo.accept_note(&ap_id, &author, "hello from remote", chrono::Utc::now(), false, None)
.await
.unwrap();
repo.accept_note(
&ap_id,
&author,
"hello from remote",
chrono::Utc::now(),
false,
None,
)
.await
.unwrap();
repo.retract_note(&ap_id).await.unwrap();
}