fmt
This commit is contained in:
@@ -44,7 +44,7 @@ pub trait ActivityPubRepository: Send + Sync {
|
|||||||
|
|
||||||
/// Find the local UserId for a remote actor by its AP URL.
|
/// Find the local UserId for a remote actor by its AP URL.
|
||||||
async fn find_remote_actor_id(&self, actor_ap_url: &str)
|
async fn find_remote_actor_id(&self, actor_ap_url: &str)
|
||||||
-> Result<Option<UserId>, DomainError>;
|
-> Result<Option<UserId>, DomainError>;
|
||||||
|
|
||||||
/// Ensure a remote actor placeholder exists; create one if absent.
|
/// Ensure a remote actor placeholder exists; create one if absent.
|
||||||
/// Idempotent — safe to call multiple times with the same URL.
|
/// Idempotent — safe to call multiple times with the same URL.
|
||||||
@@ -99,7 +99,7 @@ pub trait ActivityPubRepository: Send + Sync {
|
|||||||
/// Return the AP actor URL and inbox URL for a user, if stored.
|
/// Return the AP actor URL and inbox URL for a user, if stored.
|
||||||
/// Returns None for users that have not been federated.
|
/// Returns None for users that have not been federated.
|
||||||
async fn get_actor_ap_urls(&self, user_id: &UserId)
|
async fn get_actor_ap_urls(&self, user_id: &UserId)
|
||||||
-> Result<Option<ActorApUrls>, DomainError>;
|
-> Result<Option<ActorApUrls>, DomainError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ pub mod user;
|
|||||||
pub mod webfinger;
|
pub mod webfinger;
|
||||||
|
|
||||||
pub use activitypub_federation::kinds::object::NoteType;
|
pub use activitypub_federation::kinds::object::NoteType;
|
||||||
pub use ap_ports::{ActorApUrls, ActivityPubRepository, OutboxEntry, OutboundFederationPort};
|
pub use ap_ports::{ActivityPubRepository, ActorApUrls, OutboundFederationPort, OutboxEntry};
|
||||||
pub use content::ApObjectHandler;
|
pub use content::ApObjectHandler;
|
||||||
pub use data::FederationData;
|
pub use data::FederationData;
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
|
|||||||
@@ -1666,10 +1666,7 @@ impl domain::ports::FederationSchedulerPort for ActivityPubService {
|
|||||||
let empty = vec![];
|
let empty = vec![];
|
||||||
let items = val["orderedItems"].as_array().unwrap_or(&empty);
|
let items = val["orderedItems"].as_array().unwrap_or(&empty);
|
||||||
for item in items {
|
for item in items {
|
||||||
let actor_url = item
|
let actor_url = item.as_str().or_else(|| item["id"].as_str()).unwrap_or("");
|
||||||
.as_str()
|
|
||||||
.or_else(|| item["id"].as_str())
|
|
||||||
.unwrap_or("");
|
|
||||||
if !actor_url.is_empty() {
|
if !actor_url.is_empty() {
|
||||||
all_urls.push(actor_url.to_string());
|
all_urls.push(actor_url.to_string());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,7 +141,8 @@ impl ApObjectHandler for ThoughtsObjectHandler {
|
|||||||
"direct"
|
"direct"
|
||||||
};
|
};
|
||||||
|
|
||||||
let thought_id = self.repo
|
let thought_id = self
|
||||||
|
.repo
|
||||||
.accept_note(
|
.accept_note(
|
||||||
ap_id.as_str(),
|
ap_id.as_str(),
|
||||||
&author_id,
|
&author_id,
|
||||||
|
|||||||
@@ -18,7 +18,13 @@ impl ApiKeyRepository for FakeApiKeyRepo {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
async fn find_by_hash(&self, hash: &str) -> Result<Option<ApiKey>, DomainError> {
|
async fn find_by_hash(&self, hash: &str) -> Result<Option<ApiKey>, DomainError> {
|
||||||
Ok(self.0.lock().unwrap().iter().find(|k| k.key_hash == hash).cloned())
|
Ok(self
|
||||||
|
.0
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.find(|k| k.key_hash == hash)
|
||||||
|
.cloned())
|
||||||
}
|
}
|
||||||
async fn list_for_user(&self, _uid: &UserId) -> Result<Vec<ApiKey>, DomainError> {
|
async fn list_for_user(&self, _uid: &UserId) -> Result<Vec<ApiKey>, DomainError> {
|
||||||
Ok(vec![])
|
Ok(vec![])
|
||||||
|
|||||||
@@ -356,6 +356,5 @@ impl TryFrom<EventPayload> for DomainEvent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|||||||
@@ -109,6 +109,5 @@ impl<S: MessageSource> EventConsumer for EventConsumerAdapter<S> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|||||||
@@ -239,6 +239,5 @@ impl MessageSource for NatsMessageSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|||||||
@@ -254,12 +254,11 @@ impl ActivityPubRepository for PgActivityPubRepository {
|
|||||||
.into_domain()?;
|
.into_domain()?;
|
||||||
|
|
||||||
// SELECT the id — works whether the INSERT was a no-op or not (idempotent).
|
// SELECT the id — works whether the INSERT was a no-op or not (idempotent).
|
||||||
let row: (uuid::Uuid,) =
|
let row: (uuid::Uuid,) = sqlx::query_as("SELECT id FROM thoughts WHERE ap_id=$1")
|
||||||
sqlx::query_as("SELECT id FROM thoughts WHERE ap_id=$1")
|
.bind(ap_id)
|
||||||
.bind(ap_id)
|
.fetch_one(&self.pool)
|
||||||
.fetch_one(&self.pool)
|
.await
|
||||||
.await
|
.into_domain()?;
|
||||||
.into_domain()?;
|
|
||||||
Ok(ThoughtId::from_uuid(row.0))
|
Ok(ThoughtId::from_uuid(row.0))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,56 @@
|
|||||||
use super::*;
|
|
||||||
use activitypub_base::ActivityPubRepository;
|
|
||||||
|
|
||||||
#[sqlx::test(migrations = "./migrations")]
|
use super::*;
|
||||||
async fn intern_remote_actor_is_idempotent(pool: sqlx::PgPool) {
|
use activitypub_base::ActivityPubRepository;
|
||||||
let repo = PgActivityPubRepository::new(pool);
|
|
||||||
let url = "https://mastodon.social/users/alice";
|
|
||||||
let id1 = repo.intern_remote_actor(url).await.unwrap();
|
|
||||||
let id2 = repo.intern_remote_actor(url).await.unwrap();
|
|
||||||
assert_eq!(id1, id2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[sqlx::test(migrations = "./migrations")]
|
#[sqlx::test(migrations = "./migrations")]
|
||||||
async fn accept_and_retract_note(pool: sqlx::PgPool) {
|
async fn intern_remote_actor_is_idempotent(pool: sqlx::PgPool) {
|
||||||
let repo = PgActivityPubRepository::new(pool);
|
let repo = PgActivityPubRepository::new(pool);
|
||||||
let actor_url = "https://remote.example/users/bob";
|
let url = "https://mastodon.social/users/alice";
|
||||||
let ap_id = "https://remote.example/notes/1";
|
let id1 = repo.intern_remote_actor(url).await.unwrap();
|
||||||
let author = repo.intern_remote_actor(actor_url).await.unwrap();
|
let id2 = repo.intern_remote_actor(url).await.unwrap();
|
||||||
repo.accept_note(
|
assert_eq!(id1, id2);
|
||||||
ap_id,
|
}
|
||||||
&author,
|
|
||||||
"hello from remote",
|
#[sqlx::test(migrations = "./migrations")]
|
||||||
|
async fn accept_and_retract_note(pool: sqlx::PgPool) {
|
||||||
|
let repo = PgActivityPubRepository::new(pool);
|
||||||
|
let actor_url = "https://remote.example/users/bob";
|
||||||
|
let ap_id = "https://remote.example/notes/1";
|
||||||
|
let author = repo.intern_remote_actor(actor_url).await.unwrap();
|
||||||
|
repo.accept_note(
|
||||||
|
ap_id,
|
||||||
|
&author,
|
||||||
|
"hello from remote",
|
||||||
|
chrono::Utc::now(),
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
"public",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
repo.retract_note(ap_id).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(migrations = "./migrations")]
|
||||||
|
async fn count_local_notes_excludes_remote(pool: sqlx::PgPool) {
|
||||||
|
let repo = PgActivityPubRepository::new(pool);
|
||||||
|
assert_eq!(repo.count_local_notes().await.unwrap(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(migrations = "./migrations")]
|
||||||
|
async fn accept_note_returns_thought_id(pool: sqlx::PgPool) {
|
||||||
|
let repo = PgActivityPubRepository::new(pool.clone());
|
||||||
|
let actor_user_id = repo
|
||||||
|
.intern_remote_actor("https://remote.example/users/alice")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let thought_id = repo
|
||||||
|
.accept_note(
|
||||||
|
"https://remote.example/notes/1",
|
||||||
|
&actor_user_id,
|
||||||
|
"Hello #rust world",
|
||||||
chrono::Utc::now(),
|
chrono::Utc::now(),
|
||||||
false,
|
false,
|
||||||
None,
|
None,
|
||||||
@@ -28,41 +59,11 @@
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
repo.retract_note(ap_id).await.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[sqlx::test(migrations = "./migrations")]
|
let row: (uuid::Uuid,) = sqlx::query_as("SELECT id FROM thoughts WHERE ap_id=$1")
|
||||||
async fn count_local_notes_excludes_remote(pool: sqlx::PgPool) {
|
.bind("https://remote.example/notes/1")
|
||||||
let repo = PgActivityPubRepository::new(pool);
|
.fetch_one(&pool)
|
||||||
assert_eq!(repo.count_local_notes().await.unwrap(), 0);
|
.await
|
||||||
}
|
.unwrap();
|
||||||
|
assert_eq!(thought_id.as_uuid(), row.0);
|
||||||
#[sqlx::test(migrations = "./migrations")]
|
}
|
||||||
async fn accept_note_returns_thought_id(pool: sqlx::PgPool) {
|
|
||||||
let repo = PgActivityPubRepository::new(pool.clone());
|
|
||||||
let actor_user_id = repo
|
|
||||||
.intern_remote_actor("https://remote.example/users/alice")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let thought_id = repo
|
|
||||||
.accept_note(
|
|
||||||
"https://remote.example/notes/1",
|
|
||||||
&actor_user_id,
|
|
||||||
"Hello #rust world",
|
|
||||||
chrono::Utc::now(),
|
|
||||||
false,
|
|
||||||
None,
|
|
||||||
"public",
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let row: (uuid::Uuid,) = sqlx::query_as("SELECT id FROM thoughts WHERE ap_id=$1")
|
|
||||||
.bind("https://remote.example/notes/1")
|
|
||||||
.fetch_one(&pool)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(thought_id.as_uuid(), row.0);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,49 +1,50 @@
|
|||||||
use super::*;
|
|
||||||
use crate::user::PgUserRepository;
|
|
||||||
use chrono::Utc;
|
|
||||||
use domain::ports::UserWriter;
|
|
||||||
use domain::{models::user::User, value_objects::*};
|
|
||||||
|
|
||||||
async fn seed_user(pool: &sqlx::PgPool) -> User {
|
use super::*;
|
||||||
let repo = PgUserRepository::new(pool.clone());
|
use crate::user::PgUserRepository;
|
||||||
let u = User::new_local(
|
use chrono::Utc;
|
||||||
UserId::new(),
|
use domain::ports::UserWriter;
|
||||||
Username::new("alice").unwrap(),
|
use domain::{models::user::User, value_objects::*};
|
||||||
Email::new("alice@ex.com").unwrap(),
|
|
||||||
PasswordHash("h".into()),
|
|
||||||
);
|
|
||||||
repo.save(&u).await.unwrap();
|
|
||||||
u
|
|
||||||
}
|
|
||||||
|
|
||||||
#[sqlx::test(migrations = "./migrations")]
|
async fn seed_user(pool: &sqlx::PgPool) -> User {
|
||||||
async fn save_and_find_by_hash(pool: sqlx::PgPool) {
|
let repo = PgUserRepository::new(pool.clone());
|
||||||
let user = seed_user(&pool).await;
|
let u = User::new_local(
|
||||||
let repo = PgApiKeyRepository::new(pool);
|
UserId::new(),
|
||||||
let key = ApiKey {
|
Username::new("alice").unwrap(),
|
||||||
id: ApiKeyId::new(),
|
Email::new("alice@ex.com").unwrap(),
|
||||||
user_id: user.id.clone(),
|
PasswordHash("h".into()),
|
||||||
key_hash: "abc123".into(),
|
);
|
||||||
name: "test".into(),
|
repo.save(&u).await.unwrap();
|
||||||
created_at: Utc::now(),
|
u
|
||||||
};
|
}
|
||||||
repo.save(&key).await.unwrap();
|
|
||||||
let found = repo.find_by_hash("abc123").await.unwrap().unwrap();
|
|
||||||
assert_eq!(found.name, "test");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[sqlx::test(migrations = "./migrations")]
|
#[sqlx::test(migrations = "./migrations")]
|
||||||
async fn delete_key(pool: sqlx::PgPool) {
|
async fn save_and_find_by_hash(pool: sqlx::PgPool) {
|
||||||
let user = seed_user(&pool).await;
|
let user = seed_user(&pool).await;
|
||||||
let repo = PgApiKeyRepository::new(pool);
|
let repo = PgApiKeyRepository::new(pool);
|
||||||
let key = ApiKey {
|
let key = ApiKey {
|
||||||
id: ApiKeyId::new(),
|
id: ApiKeyId::new(),
|
||||||
user_id: user.id.clone(),
|
user_id: user.id.clone(),
|
||||||
key_hash: "def456".into(),
|
key_hash: "abc123".into(),
|
||||||
name: "key2".into(),
|
name: "test".into(),
|
||||||
created_at: Utc::now(),
|
created_at: Utc::now(),
|
||||||
};
|
};
|
||||||
repo.save(&key).await.unwrap();
|
repo.save(&key).await.unwrap();
|
||||||
repo.delete(&key.id, &user.id).await.unwrap();
|
let found = repo.find_by_hash("abc123").await.unwrap().unwrap();
|
||||||
assert!(repo.find_by_hash("def456").await.unwrap().is_none());
|
assert_eq!(found.name, "test");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(migrations = "./migrations")]
|
||||||
|
async fn delete_key(pool: sqlx::PgPool) {
|
||||||
|
let user = seed_user(&pool).await;
|
||||||
|
let repo = PgApiKeyRepository::new(pool);
|
||||||
|
let key = ApiKey {
|
||||||
|
id: ApiKeyId::new(),
|
||||||
|
user_id: user.id.clone(),
|
||||||
|
key_hash: "def456".into(),
|
||||||
|
name: "key2".into(),
|
||||||
|
created_at: Utc::now(),
|
||||||
|
};
|
||||||
|
repo.save(&key).await.unwrap();
|
||||||
|
repo.delete(&key.id, &user.id).await.unwrap();
|
||||||
|
assert!(repo.find_by_hash("def456").await.unwrap().is_none());
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,34 +1,35 @@
|
|||||||
use super::*;
|
|
||||||
use crate::test_helpers::seed_user;
|
|
||||||
use chrono::Utc;
|
|
||||||
use domain::value_objects::*;
|
|
||||||
|
|
||||||
#[sqlx::test(migrations = "./migrations")]
|
use super::*;
|
||||||
async fn block_exists(pool: sqlx::PgPool) {
|
use crate::test_helpers::seed_user;
|
||||||
let alice = seed_user(&pool, "alice", "alice@ex.com").await;
|
use chrono::Utc;
|
||||||
let bob = seed_user(&pool, "bob", "bob@ex.com").await;
|
use domain::value_objects::*;
|
||||||
let repo = PgBlockRepository::new(pool);
|
|
||||||
let block = Block {
|
|
||||||
blocker_id: alice.id.clone(),
|
|
||||||
blocked_id: bob.id.clone(),
|
|
||||||
created_at: Utc::now(),
|
|
||||||
};
|
|
||||||
repo.save(&block).await.unwrap();
|
|
||||||
assert!(repo.exists(&alice.id, &bob.id).await.unwrap());
|
|
||||||
assert!(!repo.exists(&bob.id, &alice.id).await.unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[sqlx::test(migrations = "./migrations")]
|
#[sqlx::test(migrations = "./migrations")]
|
||||||
async fn unblock(pool: sqlx::PgPool) {
|
async fn block_exists(pool: sqlx::PgPool) {
|
||||||
let alice = seed_user(&pool, "alice", "alice@ex.com").await;
|
let alice = seed_user(&pool, "alice", "alice@ex.com").await;
|
||||||
let bob = seed_user(&pool, "bob", "bob@ex.com").await;
|
let bob = seed_user(&pool, "bob", "bob@ex.com").await;
|
||||||
let repo = PgBlockRepository::new(pool);
|
let repo = PgBlockRepository::new(pool);
|
||||||
let block = Block {
|
let block = Block {
|
||||||
blocker_id: alice.id.clone(),
|
blocker_id: alice.id.clone(),
|
||||||
blocked_id: bob.id.clone(),
|
blocked_id: bob.id.clone(),
|
||||||
created_at: Utc::now(),
|
created_at: Utc::now(),
|
||||||
};
|
};
|
||||||
repo.save(&block).await.unwrap();
|
repo.save(&block).await.unwrap();
|
||||||
repo.delete(&alice.id, &bob.id).await.unwrap();
|
assert!(repo.exists(&alice.id, &bob.id).await.unwrap());
|
||||||
assert!(!repo.exists(&alice.id, &bob.id).await.unwrap());
|
assert!(!repo.exists(&bob.id, &alice.id).await.unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(migrations = "./migrations")]
|
||||||
|
async fn unblock(pool: sqlx::PgPool) {
|
||||||
|
let alice = seed_user(&pool, "alice", "alice@ex.com").await;
|
||||||
|
let bob = seed_user(&pool, "bob", "bob@ex.com").await;
|
||||||
|
let repo = PgBlockRepository::new(pool);
|
||||||
|
let block = Block {
|
||||||
|
blocker_id: alice.id.clone(),
|
||||||
|
blocked_id: bob.id.clone(),
|
||||||
|
created_at: Utc::now(),
|
||||||
|
};
|
||||||
|
repo.save(&block).await.unwrap();
|
||||||
|
repo.delete(&alice.id, &bob.id).await.unwrap();
|
||||||
|
assert!(!repo.exists(&alice.id, &bob.id).await.unwrap());
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,35 +1,36 @@
|
|||||||
use super::*;
|
|
||||||
use crate::test_helpers::seed_user_and_thought;
|
|
||||||
use chrono::Utc;
|
|
||||||
use domain::value_objects::*;
|
|
||||||
|
|
||||||
#[sqlx::test(migrations = "./migrations")]
|
use super::*;
|
||||||
async fn boost_and_count(pool: sqlx::PgPool) {
|
use crate::test_helpers::seed_user_and_thought;
|
||||||
let (user, thought) = seed_user_and_thought(&pool).await;
|
use chrono::Utc;
|
||||||
let repo = PgBoostRepository::new(pool);
|
use domain::value_objects::*;
|
||||||
let boost = Boost {
|
|
||||||
id: BoostId::new(),
|
|
||||||
user_id: user.id.clone(),
|
|
||||||
thought_id: thought.id.clone(),
|
|
||||||
ap_id: None,
|
|
||||||
created_at: Utc::now(),
|
|
||||||
};
|
|
||||||
repo.save(&boost).await.unwrap();
|
|
||||||
assert_eq!(repo.count_for_thought(&thought.id).await.unwrap(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[sqlx::test(migrations = "./migrations")]
|
#[sqlx::test(migrations = "./migrations")]
|
||||||
async fn unboost(pool: sqlx::PgPool) {
|
async fn boost_and_count(pool: sqlx::PgPool) {
|
||||||
let (user, thought) = seed_user_and_thought(&pool).await;
|
let (user, thought) = seed_user_and_thought(&pool).await;
|
||||||
let repo = PgBoostRepository::new(pool);
|
let repo = PgBoostRepository::new(pool);
|
||||||
let boost = Boost {
|
let boost = Boost {
|
||||||
id: BoostId::new(),
|
id: BoostId::new(),
|
||||||
user_id: user.id.clone(),
|
user_id: user.id.clone(),
|
||||||
thought_id: thought.id.clone(),
|
thought_id: thought.id.clone(),
|
||||||
ap_id: None,
|
ap_id: None,
|
||||||
created_at: Utc::now(),
|
created_at: Utc::now(),
|
||||||
};
|
};
|
||||||
repo.save(&boost).await.unwrap();
|
repo.save(&boost).await.unwrap();
|
||||||
repo.delete(&user.id, &thought.id).await.unwrap();
|
assert_eq!(repo.count_for_thought(&thought.id).await.unwrap(), 1);
|
||||||
assert_eq!(repo.count_for_thought(&thought.id).await.unwrap(), 0);
|
}
|
||||||
}
|
|
||||||
|
#[sqlx::test(migrations = "./migrations")]
|
||||||
|
async fn unboost(pool: sqlx::PgPool) {
|
||||||
|
let (user, thought) = seed_user_and_thought(&pool).await;
|
||||||
|
let repo = PgBoostRepository::new(pool);
|
||||||
|
let boost = Boost {
|
||||||
|
id: BoostId::new(),
|
||||||
|
user_id: user.id.clone(),
|
||||||
|
thought_id: thought.id.clone(),
|
||||||
|
ap_id: None,
|
||||||
|
created_at: Utc::now(),
|
||||||
|
};
|
||||||
|
repo.save(&boost).await.unwrap();
|
||||||
|
repo.delete(&user.id, &thought.id).await.unwrap();
|
||||||
|
assert_eq!(repo.count_for_thought(&thought.id).await.unwrap(), 0);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,69 +1,76 @@
|
|||||||
use super::*;
|
|
||||||
use crate::{thought::PgThoughtRepository, user::PgUserRepository};
|
|
||||||
use domain::{
|
|
||||||
models::{
|
|
||||||
feed::PageParams,
|
|
||||||
thought::{Thought, Visibility},
|
|
||||||
user::User,
|
|
||||||
},
|
|
||||||
ports::{FeedQuery, ThoughtRepository, UserWriter},
|
|
||||||
value_objects::*,
|
|
||||||
};
|
|
||||||
|
|
||||||
async fn seed(pool: &sqlx::PgPool, username: &str, content: &str) -> (User, Thought) {
|
use super::*;
|
||||||
let urepo = PgUserRepository::new(pool.clone());
|
use crate::{thought::PgThoughtRepository, user::PgUserRepository};
|
||||||
let trepo = PgThoughtRepository::new(pool.clone());
|
use domain::{
|
||||||
let u = User::new_local(
|
models::{
|
||||||
UserId::new(),
|
feed::PageParams,
|
||||||
Username::new(username).unwrap(),
|
thought::{Thought, Visibility},
|
||||||
Email::new(format!("{username}@ex.com")).unwrap(),
|
user::User,
|
||||||
PasswordHash("h".into()),
|
},
|
||||||
);
|
ports::{FeedQuery, ThoughtRepository, UserWriter},
|
||||||
urepo.save(&u).await.unwrap();
|
value_objects::*,
|
||||||
let t = Thought::new_local(
|
};
|
||||||
ThoughtId::new(),
|
|
||||||
u.id.clone(),
|
async fn seed(pool: &sqlx::PgPool, username: &str, content: &str) -> (User, Thought) {
|
||||||
Content::new_local(content).unwrap(),
|
let urepo = PgUserRepository::new(pool.clone());
|
||||||
|
let trepo = PgThoughtRepository::new(pool.clone());
|
||||||
|
let u = User::new_local(
|
||||||
|
UserId::new(),
|
||||||
|
Username::new(username).unwrap(),
|
||||||
|
Email::new(format!("{username}@ex.com")).unwrap(),
|
||||||
|
PasswordHash("h".into()),
|
||||||
|
);
|
||||||
|
urepo.save(&u).await.unwrap();
|
||||||
|
let t = Thought::new_local(
|
||||||
|
ThoughtId::new(),
|
||||||
|
u.id.clone(),
|
||||||
|
Content::new_local(content).unwrap(),
|
||||||
|
None,
|
||||||
|
Visibility::Public,
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
trepo.save(&t).await.unwrap();
|
||||||
|
(u, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(migrations = "./migrations")]
|
||||||
|
async fn public_feed_returns_local_thoughts(pool: sqlx::PgPool) {
|
||||||
|
let (_, _) = seed(&pool, "alice", "hello").await;
|
||||||
|
let repo = PgFeedRepository::new(pool);
|
||||||
|
let result = repo
|
||||||
|
.query(&FeedQuery::public(
|
||||||
|
PageParams {
|
||||||
|
page: 1,
|
||||||
|
per_page: 20,
|
||||||
|
},
|
||||||
None,
|
None,
|
||||||
Visibility::Public,
|
))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(result.total, 1);
|
||||||
|
assert_eq!(result.items[0].thought.content.as_str(), "hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(migrations = "./migrations")]
|
||||||
|
async fn search_returns_matching_thoughts(pool: sqlx::PgPool) {
|
||||||
|
let (_, _) = seed(&pool, "alice", "hello world").await;
|
||||||
|
let (_, _) = seed(&pool, "bob", "goodbye world").await;
|
||||||
|
let repo = PgFeedRepository::new(pool);
|
||||||
|
let result = repo
|
||||||
|
.query(&FeedQuery::search(
|
||||||
|
"hello world",
|
||||||
|
PageParams {
|
||||||
|
page: 1,
|
||||||
|
per_page: 20,
|
||||||
|
},
|
||||||
None,
|
None,
|
||||||
false,
|
))
|
||||||
);
|
.await
|
||||||
trepo.save(&t).await.unwrap();
|
.unwrap();
|
||||||
(u, t)
|
assert!(result.total >= 1);
|
||||||
}
|
assert!(result
|
||||||
|
.items
|
||||||
#[sqlx::test(migrations = "./migrations")]
|
.iter()
|
||||||
async fn public_feed_returns_local_thoughts(pool: sqlx::PgPool) {
|
.any(|e| e.thought.content.as_str() == "hello world"));
|
||||||
let (_, _) = seed(&pool, "alice", "hello").await;
|
}
|
||||||
let repo = PgFeedRepository::new(pool);
|
|
||||||
let result = repo
|
|
||||||
.query(&FeedQuery::public(
|
|
||||||
PageParams { page: 1, per_page: 20 },
|
|
||||||
None,
|
|
||||||
))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(result.total, 1);
|
|
||||||
assert_eq!(result.items[0].thought.content.as_str(), "hello");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[sqlx::test(migrations = "./migrations")]
|
|
||||||
async fn search_returns_matching_thoughts(pool: sqlx::PgPool) {
|
|
||||||
let (_, _) = seed(&pool, "alice", "hello world").await;
|
|
||||||
let (_, _) = seed(&pool, "bob", "goodbye world").await;
|
|
||||||
let repo = PgFeedRepository::new(pool);
|
|
||||||
let result = repo
|
|
||||||
.query(&FeedQuery::search(
|
|
||||||
"hello world",
|
|
||||||
PageParams { page: 1, per_page: 20 },
|
|
||||||
None,
|
|
||||||
))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert!(result.total >= 1);
|
|
||||||
assert!(result
|
|
||||||
.items
|
|
||||||
.iter()
|
|
||||||
.any(|e| e.thought.content.as_str() == "hello world"));
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,58 +1,59 @@
|
|||||||
use super::*;
|
|
||||||
use crate::test_helpers::seed_user;
|
|
||||||
use chrono::Utc;
|
|
||||||
use domain::value_objects::*;
|
|
||||||
|
|
||||||
#[sqlx::test(migrations = "./migrations")]
|
use super::*;
|
||||||
async fn save_and_find_follow(pool: sqlx::PgPool) {
|
use crate::test_helpers::seed_user;
|
||||||
let alice = seed_user(&pool, "alice", "alice@ex.com").await;
|
use chrono::Utc;
|
||||||
let bob = seed_user(&pool, "bob", "bob@ex.com").await;
|
use domain::value_objects::*;
|
||||||
let repo = PgFollowRepository::new(pool);
|
|
||||||
let follow = Follow {
|
|
||||||
follower_id: alice.id.clone(),
|
|
||||||
following_id: bob.id.clone(),
|
|
||||||
state: FollowState::Accepted,
|
|
||||||
ap_id: None,
|
|
||||||
created_at: Utc::now(),
|
|
||||||
};
|
|
||||||
repo.save(&follow).await.unwrap();
|
|
||||||
let found = repo.find(&alice.id, &bob.id).await.unwrap().unwrap();
|
|
||||||
assert_eq!(found.state, FollowState::Accepted);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[sqlx::test(migrations = "./migrations")]
|
#[sqlx::test(migrations = "./migrations")]
|
||||||
async fn update_state(pool: sqlx::PgPool) {
|
async fn save_and_find_follow(pool: sqlx::PgPool) {
|
||||||
let alice = seed_user(&pool, "alice", "alice@ex.com").await;
|
let alice = seed_user(&pool, "alice", "alice@ex.com").await;
|
||||||
let bob = seed_user(&pool, "bob", "bob@ex.com").await;
|
let bob = seed_user(&pool, "bob", "bob@ex.com").await;
|
||||||
let repo = PgFollowRepository::new(pool);
|
let repo = PgFollowRepository::new(pool);
|
||||||
let follow = Follow {
|
let follow = Follow {
|
||||||
follower_id: alice.id.clone(),
|
follower_id: alice.id.clone(),
|
||||||
following_id: bob.id.clone(),
|
following_id: bob.id.clone(),
|
||||||
state: FollowState::Pending,
|
state: FollowState::Accepted,
|
||||||
ap_id: None,
|
ap_id: None,
|
||||||
created_at: Utc::now(),
|
created_at: Utc::now(),
|
||||||
};
|
};
|
||||||
repo.save(&follow).await.unwrap();
|
repo.save(&follow).await.unwrap();
|
||||||
repo.update_state(&alice.id, &bob.id, &FollowState::Accepted)
|
let found = repo.find(&alice.id, &bob.id).await.unwrap().unwrap();
|
||||||
.await
|
assert_eq!(found.state, FollowState::Accepted);
|
||||||
.unwrap();
|
}
|
||||||
let found = repo.find(&alice.id, &bob.id).await.unwrap().unwrap();
|
|
||||||
assert_eq!(found.state, FollowState::Accepted);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[sqlx::test(migrations = "./migrations")]
|
#[sqlx::test(migrations = "./migrations")]
|
||||||
async fn get_accepted_following_ids(pool: sqlx::PgPool) {
|
async fn update_state(pool: sqlx::PgPool) {
|
||||||
let alice = seed_user(&pool, "alice", "alice@ex.com").await;
|
let alice = seed_user(&pool, "alice", "alice@ex.com").await;
|
||||||
let bob = seed_user(&pool, "bob", "bob@ex.com").await;
|
let bob = seed_user(&pool, "bob", "bob@ex.com").await;
|
||||||
let repo = PgFollowRepository::new(pool);
|
let repo = PgFollowRepository::new(pool);
|
||||||
let follow = Follow {
|
let follow = Follow {
|
||||||
follower_id: alice.id.clone(),
|
follower_id: alice.id.clone(),
|
||||||
following_id: bob.id.clone(),
|
following_id: bob.id.clone(),
|
||||||
state: FollowState::Accepted,
|
state: FollowState::Pending,
|
||||||
ap_id: None,
|
ap_id: None,
|
||||||
created_at: Utc::now(),
|
created_at: Utc::now(),
|
||||||
};
|
};
|
||||||
repo.save(&follow).await.unwrap();
|
repo.save(&follow).await.unwrap();
|
||||||
let ids = repo.get_accepted_following_ids(&alice.id).await.unwrap();
|
repo.update_state(&alice.id, &bob.id, &FollowState::Accepted)
|
||||||
assert_eq!(ids, vec![bob.id]);
|
.await
|
||||||
}
|
.unwrap();
|
||||||
|
let found = repo.find(&alice.id, &bob.id).await.unwrap().unwrap();
|
||||||
|
assert_eq!(found.state, FollowState::Accepted);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(migrations = "./migrations")]
|
||||||
|
async fn get_accepted_following_ids(pool: sqlx::PgPool) {
|
||||||
|
let alice = seed_user(&pool, "alice", "alice@ex.com").await;
|
||||||
|
let bob = seed_user(&pool, "bob", "bob@ex.com").await;
|
||||||
|
let repo = PgFollowRepository::new(pool);
|
||||||
|
let follow = Follow {
|
||||||
|
follower_id: alice.id.clone(),
|
||||||
|
following_id: bob.id.clone(),
|
||||||
|
state: FollowState::Accepted,
|
||||||
|
ap_id: None,
|
||||||
|
created_at: Utc::now(),
|
||||||
|
};
|
||||||
|
repo.save(&follow).await.unwrap();
|
||||||
|
let ids = repo.get_accepted_following_ids(&alice.id).await.unwrap();
|
||||||
|
assert_eq!(ids, vec![bob.id]);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
pub mod activitypub;
|
pub mod activitypub;
|
||||||
pub mod engagement;
|
|
||||||
pub mod api_key;
|
pub mod api_key;
|
||||||
pub mod block;
|
pub mod block;
|
||||||
pub mod boost;
|
pub mod boost;
|
||||||
mod db_error;
|
mod db_error;
|
||||||
|
pub mod engagement;
|
||||||
pub mod failed_event;
|
pub mod failed_event;
|
||||||
pub mod outbox;
|
|
||||||
pub mod feed;
|
pub mod feed;
|
||||||
pub mod follow;
|
pub mod follow;
|
||||||
pub mod like;
|
pub mod like;
|
||||||
pub mod notification;
|
pub mod notification;
|
||||||
|
pub mod outbox;
|
||||||
pub mod remote_actor;
|
pub mod remote_actor;
|
||||||
pub mod remote_actor_connections;
|
pub mod remote_actor_connections;
|
||||||
pub mod tag;
|
pub mod tag;
|
||||||
|
|||||||
@@ -1,35 +1,36 @@
|
|||||||
use super::*;
|
|
||||||
use crate::test_helpers::seed_user_and_thought;
|
|
||||||
use chrono::Utc;
|
|
||||||
use domain::value_objects::*;
|
|
||||||
|
|
||||||
#[sqlx::test(migrations = "./migrations")]
|
use super::*;
|
||||||
async fn like_and_count(pool: sqlx::PgPool) {
|
use crate::test_helpers::seed_user_and_thought;
|
||||||
let (user, thought) = seed_user_and_thought(&pool).await;
|
use chrono::Utc;
|
||||||
let repo = PgLikeRepository::new(pool);
|
use domain::value_objects::*;
|
||||||
let like = Like {
|
|
||||||
id: LikeId::new(),
|
|
||||||
user_id: user.id.clone(),
|
|
||||||
thought_id: thought.id.clone(),
|
|
||||||
ap_id: None,
|
|
||||||
created_at: Utc::now(),
|
|
||||||
};
|
|
||||||
repo.save(&like).await.unwrap();
|
|
||||||
assert_eq!(repo.count_for_thought(&thought.id).await.unwrap(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[sqlx::test(migrations = "./migrations")]
|
#[sqlx::test(migrations = "./migrations")]
|
||||||
async fn unlike(pool: sqlx::PgPool) {
|
async fn like_and_count(pool: sqlx::PgPool) {
|
||||||
let (user, thought) = seed_user_and_thought(&pool).await;
|
let (user, thought) = seed_user_and_thought(&pool).await;
|
||||||
let repo = PgLikeRepository::new(pool);
|
let repo = PgLikeRepository::new(pool);
|
||||||
let like = Like {
|
let like = Like {
|
||||||
id: LikeId::new(),
|
id: LikeId::new(),
|
||||||
user_id: user.id.clone(),
|
user_id: user.id.clone(),
|
||||||
thought_id: thought.id.clone(),
|
thought_id: thought.id.clone(),
|
||||||
ap_id: None,
|
ap_id: None,
|
||||||
created_at: Utc::now(),
|
created_at: Utc::now(),
|
||||||
};
|
};
|
||||||
repo.save(&like).await.unwrap();
|
repo.save(&like).await.unwrap();
|
||||||
repo.delete(&user.id, &thought.id).await.unwrap();
|
assert_eq!(repo.count_for_thought(&thought.id).await.unwrap(), 1);
|
||||||
assert_eq!(repo.count_for_thought(&thought.id).await.unwrap(), 0);
|
}
|
||||||
}
|
|
||||||
|
#[sqlx::test(migrations = "./migrations")]
|
||||||
|
async fn unlike(pool: sqlx::PgPool) {
|
||||||
|
let (user, thought) = seed_user_and_thought(&pool).await;
|
||||||
|
let repo = PgLikeRepository::new(pool);
|
||||||
|
let like = Like {
|
||||||
|
id: LikeId::new(),
|
||||||
|
user_id: user.id.clone(),
|
||||||
|
thought_id: thought.id.clone(),
|
||||||
|
ap_id: None,
|
||||||
|
created_at: Utc::now(),
|
||||||
|
};
|
||||||
|
repo.save(&like).await.unwrap();
|
||||||
|
repo.delete(&user.id, &thought.id).await.unwrap();
|
||||||
|
assert_eq!(repo.count_for_thought(&thought.id).await.unwrap(), 0);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,67 +1,68 @@
|
|||||||
use super::*;
|
|
||||||
use crate::test_helpers;
|
use super::*;
|
||||||
use chrono::Utc;
|
use crate::test_helpers;
|
||||||
use domain::{
|
use chrono::Utc;
|
||||||
models::{notification::NotificationKind, user::User},
|
use domain::{
|
||||||
value_objects::*,
|
models::{notification::NotificationKind, user::User},
|
||||||
|
value_objects::*,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[sqlx::test(migrations = "./migrations")]
|
||||||
|
async fn save_and_list(pool: sqlx::PgPool) {
|
||||||
|
let user = test_helpers::seed_user(&pool, "alice", "alice@ex.com").await;
|
||||||
|
let from_user = test_helpers::seed_user(&pool, "bob", "bob@ex.com").await;
|
||||||
|
let repo = PgNotificationRepository::new(pool);
|
||||||
|
use domain::models::feed::PageParams;
|
||||||
|
let n = Notification {
|
||||||
|
id: NotificationId::new(),
|
||||||
|
user_id: user.id.clone(),
|
||||||
|
kind: NotificationKind::Follow {
|
||||||
|
from_user_id: from_user.id.clone(),
|
||||||
|
},
|
||||||
|
read: false,
|
||||||
|
created_at: Utc::now(),
|
||||||
};
|
};
|
||||||
|
repo.save(&n).await.unwrap();
|
||||||
#[sqlx::test(migrations = "./migrations")]
|
let page = repo
|
||||||
async fn save_and_list(pool: sqlx::PgPool) {
|
.list_for_user(
|
||||||
let user = test_helpers::seed_user(&pool, "alice", "alice@ex.com").await;
|
&user.id,
|
||||||
let from_user = test_helpers::seed_user(&pool, "bob", "bob@ex.com").await;
|
&PageParams {
|
||||||
let repo = PgNotificationRepository::new(pool);
|
page: 1,
|
||||||
use domain::models::feed::PageParams;
|
per_page: 20,
|
||||||
let n = Notification {
|
|
||||||
id: NotificationId::new(),
|
|
||||||
user_id: user.id.clone(),
|
|
||||||
kind: NotificationKind::Follow {
|
|
||||||
from_user_id: from_user.id.clone(),
|
|
||||||
},
|
},
|
||||||
read: false,
|
)
|
||||||
created_at: Utc::now(),
|
.await
|
||||||
};
|
.unwrap();
|
||||||
repo.save(&n).await.unwrap();
|
assert_eq!(page.total, 1);
|
||||||
let page = repo
|
assert!(!page.items[0].read);
|
||||||
.list_for_user(
|
}
|
||||||
&user.id,
|
|
||||||
&PageParams {
|
|
||||||
page: 1,
|
|
||||||
per_page: 20,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(page.total, 1);
|
|
||||||
assert!(!page.items[0].read);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[sqlx::test(migrations = "./migrations")]
|
#[sqlx::test(migrations = "./migrations")]
|
||||||
async fn mark_all_read(pool: sqlx::PgPool) {
|
async fn mark_all_read(pool: sqlx::PgPool) {
|
||||||
let user = test_helpers::seed_user(&pool, "alice", "alice@ex.com").await;
|
let user = test_helpers::seed_user(&pool, "alice", "alice@ex.com").await;
|
||||||
let from_user = test_helpers::seed_user(&pool, "bob", "bob@ex.com").await;
|
let from_user = test_helpers::seed_user(&pool, "bob", "bob@ex.com").await;
|
||||||
let repo = PgNotificationRepository::new(pool);
|
let repo = PgNotificationRepository::new(pool);
|
||||||
use domain::models::feed::PageParams;
|
use domain::models::feed::PageParams;
|
||||||
let n = Notification {
|
let n = Notification {
|
||||||
id: NotificationId::new(),
|
id: NotificationId::new(),
|
||||||
user_id: user.id.clone(),
|
user_id: user.id.clone(),
|
||||||
kind: NotificationKind::Follow {
|
kind: NotificationKind::Follow {
|
||||||
from_user_id: from_user.id.clone(),
|
from_user_id: from_user.id.clone(),
|
||||||
|
},
|
||||||
|
read: false,
|
||||||
|
created_at: Utc::now(),
|
||||||
|
};
|
||||||
|
repo.save(&n).await.unwrap();
|
||||||
|
repo.mark_all_read(&user.id).await.unwrap();
|
||||||
|
let page = repo
|
||||||
|
.list_for_user(
|
||||||
|
&user.id,
|
||||||
|
&PageParams {
|
||||||
|
page: 1,
|
||||||
|
per_page: 20,
|
||||||
},
|
},
|
||||||
read: false,
|
)
|
||||||
created_at: Utc::now(),
|
.await
|
||||||
};
|
.unwrap();
|
||||||
repo.save(&n).await.unwrap();
|
assert!(page.items[0].read);
|
||||||
repo.mark_all_read(&user.id).await.unwrap();
|
}
|
||||||
let page = repo
|
|
||||||
.list_for_user(
|
|
||||||
&user.id,
|
|
||||||
&PageParams {
|
|
||||||
page: 1,
|
|
||||||
per_page: 20,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert!(page.items[0].read);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,48 +1,49 @@
|
|||||||
use super::*;
|
|
||||||
use crate::{thought::PgThoughtRepository, user::PgUserRepository};
|
|
||||||
use domain::ports::{ThoughtRepository, UserWriter};
|
|
||||||
use domain::{
|
|
||||||
models::{
|
|
||||||
thought::{Thought, Visibility},
|
|
||||||
user::User,
|
|
||||||
},
|
|
||||||
value_objects::*,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[sqlx::test(migrations = "./migrations")]
|
use super::*;
|
||||||
async fn find_or_create_tag(pool: sqlx::PgPool) {
|
use crate::{thought::PgThoughtRepository, user::PgUserRepository};
|
||||||
let repo = PgTagRepository::new(pool);
|
use domain::ports::{ThoughtRepository, UserWriter};
|
||||||
let t1 = repo.find_or_create("rust").await.unwrap();
|
use domain::{
|
||||||
let t2 = repo.find_or_create("rust").await.unwrap();
|
models::{
|
||||||
assert_eq!(t1.id, t2.id);
|
thought::{Thought, Visibility},
|
||||||
assert_eq!(t1.name, "rust");
|
user::User,
|
||||||
}
|
},
|
||||||
|
value_objects::*,
|
||||||
|
};
|
||||||
|
|
||||||
#[sqlx::test(migrations = "./migrations")]
|
#[sqlx::test(migrations = "./migrations")]
|
||||||
async fn attach_and_list(pool: sqlx::PgPool) {
|
async fn find_or_create_tag(pool: sqlx::PgPool) {
|
||||||
let urepo = PgUserRepository::new(pool.clone());
|
let repo = PgTagRepository::new(pool);
|
||||||
let trepo = PgThoughtRepository::new(pool.clone());
|
let t1 = repo.find_or_create("rust").await.unwrap();
|
||||||
let u = User::new_local(
|
let t2 = repo.find_or_create("rust").await.unwrap();
|
||||||
UserId::new(),
|
assert_eq!(t1.id, t2.id);
|
||||||
Username::new("alice").unwrap(),
|
assert_eq!(t1.name, "rust");
|
||||||
Email::new("alice@ex.com").unwrap(),
|
}
|
||||||
PasswordHash("h".into()),
|
|
||||||
);
|
#[sqlx::test(migrations = "./migrations")]
|
||||||
urepo.save(&u).await.unwrap();
|
async fn attach_and_list(pool: sqlx::PgPool) {
|
||||||
let t = Thought::new_local(
|
let urepo = PgUserRepository::new(pool.clone());
|
||||||
ThoughtId::new(),
|
let trepo = PgThoughtRepository::new(pool.clone());
|
||||||
u.id.clone(),
|
let u = User::new_local(
|
||||||
Content::new_local("hi").unwrap(),
|
UserId::new(),
|
||||||
None,
|
Username::new("alice").unwrap(),
|
||||||
Visibility::Public,
|
Email::new("alice@ex.com").unwrap(),
|
||||||
None,
|
PasswordHash("h".into()),
|
||||||
false,
|
);
|
||||||
);
|
urepo.save(&u).await.unwrap();
|
||||||
trepo.save(&t).await.unwrap();
|
let t = Thought::new_local(
|
||||||
let repo = PgTagRepository::new(pool);
|
ThoughtId::new(),
|
||||||
let tag = repo.find_or_create("greetings").await.unwrap();
|
u.id.clone(),
|
||||||
repo.attach_to_thought(&t.id, tag.id).await.unwrap();
|
Content::new_local("hi").unwrap(),
|
||||||
let tags = repo.list_for_thought(&t.id).await.unwrap();
|
None,
|
||||||
assert_eq!(tags.len(), 1);
|
Visibility::Public,
|
||||||
assert_eq!(tags[0].name, "greetings");
|
None,
|
||||||
}
|
false,
|
||||||
|
);
|
||||||
|
trepo.save(&t).await.unwrap();
|
||||||
|
let repo = PgTagRepository::new(pool);
|
||||||
|
let tag = repo.find_or_create("greetings").await.unwrap();
|
||||||
|
repo.attach_to_thought(&t.id, tag.id).await.unwrap();
|
||||||
|
let tags = repo.list_for_thought(&t.id).await.unwrap();
|
||||||
|
assert_eq!(tags.len(), 1);
|
||||||
|
assert_eq!(tags[0].name, "greetings");
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,90 +1,91 @@
|
|||||||
use super::*;
|
|
||||||
use crate::test_helpers::seed_user;
|
|
||||||
use domain::{
|
|
||||||
models::thought::{Thought, Visibility},
|
|
||||||
value_objects::*,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[sqlx::test(migrations = "./migrations")]
|
use super::*;
|
||||||
async fn save_and_find_thought(pool: sqlx::PgPool) {
|
use crate::test_helpers::seed_user;
|
||||||
let user = seed_user(&pool, "alice", "alice@ex.com").await;
|
use domain::{
|
||||||
let repo = PgThoughtRepository::new(pool);
|
models::thought::{Thought, Visibility},
|
||||||
let t = Thought::new_local(
|
value_objects::*,
|
||||||
ThoughtId::new(),
|
};
|
||||||
user.id.clone(),
|
|
||||||
Content::new_local("hello world").unwrap(),
|
|
||||||
None,
|
|
||||||
Visibility::Public,
|
|
||||||
None,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
repo.save(&t).await.unwrap();
|
|
||||||
let found = repo.find_by_id(&t.id).await.unwrap().unwrap();
|
|
||||||
assert_eq!(found.content.as_str(), "hello world");
|
|
||||||
assert!(found.local);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[sqlx::test(migrations = "./migrations")]
|
#[sqlx::test(migrations = "./migrations")]
|
||||||
async fn delete_thought(pool: sqlx::PgPool) {
|
async fn save_and_find_thought(pool: sqlx::PgPool) {
|
||||||
let user = seed_user(&pool, "bob", "bob@ex.com").await;
|
let user = seed_user(&pool, "alice", "alice@ex.com").await;
|
||||||
let repo = PgThoughtRepository::new(pool);
|
let repo = PgThoughtRepository::new(pool);
|
||||||
let t = Thought::new_local(
|
let t = Thought::new_local(
|
||||||
ThoughtId::new(),
|
ThoughtId::new(),
|
||||||
user.id.clone(),
|
user.id.clone(),
|
||||||
Content::new_local("bye").unwrap(),
|
Content::new_local("hello world").unwrap(),
|
||||||
None,
|
None,
|
||||||
Visibility::Public,
|
Visibility::Public,
|
||||||
None,
|
None,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
repo.save(&t).await.unwrap();
|
repo.save(&t).await.unwrap();
|
||||||
repo.delete(&t.id, &user.id).await.unwrap();
|
let found = repo.find_by_id(&t.id).await.unwrap().unwrap();
|
||||||
assert!(repo.find_by_id(&t.id).await.unwrap().is_none());
|
assert_eq!(found.content.as_str(), "hello world");
|
||||||
}
|
assert!(found.local);
|
||||||
|
}
|
||||||
|
|
||||||
#[sqlx::test(migrations = "./migrations")]
|
#[sqlx::test(migrations = "./migrations")]
|
||||||
async fn delete_wrong_owner_returns_not_found(pool: sqlx::PgPool) {
|
async fn delete_thought(pool: sqlx::PgPool) {
|
||||||
let alice = seed_user(&pool, "alice", "alice@ex.com").await;
|
let user = seed_user(&pool, "bob", "bob@ex.com").await;
|
||||||
let bob = seed_user(&pool, "bob", "bob@ex.com").await;
|
let repo = PgThoughtRepository::new(pool);
|
||||||
let repo = PgThoughtRepository::new(pool);
|
let t = Thought::new_local(
|
||||||
let t = Thought::new_local(
|
ThoughtId::new(),
|
||||||
ThoughtId::new(),
|
user.id.clone(),
|
||||||
alice.id.clone(),
|
Content::new_local("bye").unwrap(),
|
||||||
Content::new_local("secret").unwrap(),
|
None,
|
||||||
None,
|
Visibility::Public,
|
||||||
Visibility::Public,
|
None,
|
||||||
None,
|
false,
|
||||||
false,
|
);
|
||||||
);
|
repo.save(&t).await.unwrap();
|
||||||
repo.save(&t).await.unwrap();
|
repo.delete(&t.id, &user.id).await.unwrap();
|
||||||
let err = repo.delete(&t.id, &bob.id).await.unwrap_err();
|
assert!(repo.find_by_id(&t.id).await.unwrap().is_none());
|
||||||
assert!(matches!(err, DomainError::NotFound));
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[sqlx::test(migrations = "./migrations")]
|
#[sqlx::test(migrations = "./migrations")]
|
||||||
async fn get_thread_returns_root_and_replies(pool: sqlx::PgPool) {
|
async fn delete_wrong_owner_returns_not_found(pool: sqlx::PgPool) {
|
||||||
let user = seed_user(&pool, "charlie", "charlie@ex.com").await;
|
let alice = seed_user(&pool, "alice", "alice@ex.com").await;
|
||||||
let repo = PgThoughtRepository::new(pool);
|
let bob = seed_user(&pool, "bob", "bob@ex.com").await;
|
||||||
let root = Thought::new_local(
|
let repo = PgThoughtRepository::new(pool);
|
||||||
ThoughtId::new(),
|
let t = Thought::new_local(
|
||||||
user.id.clone(),
|
ThoughtId::new(),
|
||||||
Content::new_local("root").unwrap(),
|
alice.id.clone(),
|
||||||
None,
|
Content::new_local("secret").unwrap(),
|
||||||
Visibility::Public,
|
None,
|
||||||
None,
|
Visibility::Public,
|
||||||
false,
|
None,
|
||||||
);
|
false,
|
||||||
let reply = Thought::new_local(
|
);
|
||||||
ThoughtId::new(),
|
repo.save(&t).await.unwrap();
|
||||||
user.id.clone(),
|
let err = repo.delete(&t.id, &bob.id).await.unwrap_err();
|
||||||
Content::new_local("reply").unwrap(),
|
assert!(matches!(err, DomainError::NotFound));
|
||||||
Some(root.id.clone()),
|
}
|
||||||
Visibility::Public,
|
|
||||||
None,
|
#[sqlx::test(migrations = "./migrations")]
|
||||||
false,
|
async fn get_thread_returns_root_and_replies(pool: sqlx::PgPool) {
|
||||||
);
|
let user = seed_user(&pool, "charlie", "charlie@ex.com").await;
|
||||||
repo.save(&root).await.unwrap();
|
let repo = PgThoughtRepository::new(pool);
|
||||||
repo.save(&reply).await.unwrap();
|
let root = Thought::new_local(
|
||||||
let thread = repo.get_thread(&root.id).await.unwrap();
|
ThoughtId::new(),
|
||||||
assert_eq!(thread.len(), 2);
|
user.id.clone(),
|
||||||
}
|
Content::new_local("root").unwrap(),
|
||||||
|
None,
|
||||||
|
Visibility::Public,
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
let reply = Thought::new_local(
|
||||||
|
ThoughtId::new(),
|
||||||
|
user.id.clone(),
|
||||||
|
Content::new_local("reply").unwrap(),
|
||||||
|
Some(root.id.clone()),
|
||||||
|
Visibility::Public,
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
repo.save(&root).await.unwrap();
|
||||||
|
repo.save(&reply).await.unwrap();
|
||||||
|
let thread = repo.get_thread(&root.id).await.unwrap();
|
||||||
|
assert_eq!(thread.len(), 2);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,47 +1,48 @@
|
|||||||
use super::*;
|
|
||||||
use crate::user::PgUserRepository;
|
|
||||||
use domain::ports::UserWriter;
|
|
||||||
use domain::{models::user::User, value_objects::*};
|
|
||||||
|
|
||||||
async fn seed_user(pool: &sqlx::PgPool, username: &str, email: &str) -> User {
|
use super::*;
|
||||||
let repo = PgUserRepository::new(pool.clone());
|
use crate::user::PgUserRepository;
|
||||||
let u = User::new_local(
|
use domain::ports::UserWriter;
|
||||||
UserId::new(),
|
use domain::{models::user::User, value_objects::*};
|
||||||
Username::new(username).unwrap(),
|
|
||||||
Email::new(email).unwrap(),
|
|
||||||
PasswordHash("h".into()),
|
|
||||||
);
|
|
||||||
repo.save(&u).await.unwrap();
|
|
||||||
u
|
|
||||||
}
|
|
||||||
|
|
||||||
#[sqlx::test(migrations = "./migrations")]
|
async fn seed_user(pool: &sqlx::PgPool, username: &str, email: &str) -> User {
|
||||||
async fn set_and_list_top_friends(pool: sqlx::PgPool) {
|
let repo = PgUserRepository::new(pool.clone());
|
||||||
let alice = seed_user(&pool, "alice", "alice@ex.com").await;
|
let u = User::new_local(
|
||||||
let bob = seed_user(&pool, "bob", "bob@ex.com").await;
|
UserId::new(),
|
||||||
let repo = PgTopFriendRepository::new(pool);
|
Username::new(username).unwrap(),
|
||||||
repo.set_top_friends(&alice.id, vec![(bob.id.clone(), 1)])
|
Email::new(email).unwrap(),
|
||||||
.await
|
PasswordHash("h".into()),
|
||||||
.unwrap();
|
);
|
||||||
let friends = repo.list_for_user(&alice.id).await.unwrap();
|
repo.save(&u).await.unwrap();
|
||||||
assert_eq!(friends.len(), 1);
|
u
|
||||||
assert_eq!(friends[0].0.position, 1);
|
}
|
||||||
assert_eq!(friends[0].1.username.as_str(), "bob");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[sqlx::test(migrations = "./migrations")]
|
#[sqlx::test(migrations = "./migrations")]
|
||||||
async fn replace_top_friends(pool: sqlx::PgPool) {
|
async fn set_and_list_top_friends(pool: sqlx::PgPool) {
|
||||||
let alice = seed_user(&pool, "alice", "alice@ex.com").await;
|
let alice = seed_user(&pool, "alice", "alice@ex.com").await;
|
||||||
let bob = seed_user(&pool, "bob", "bob@ex.com").await;
|
let bob = seed_user(&pool, "bob", "bob@ex.com").await;
|
||||||
let carol = seed_user(&pool, "carol", "carol@ex.com").await;
|
let repo = PgTopFriendRepository::new(pool);
|
||||||
let repo = PgTopFriendRepository::new(pool);
|
repo.set_top_friends(&alice.id, vec![(bob.id.clone(), 1)])
|
||||||
repo.set_top_friends(&alice.id, vec![(bob.id.clone(), 1)])
|
.await
|
||||||
.await
|
.unwrap();
|
||||||
.unwrap();
|
let friends = repo.list_for_user(&alice.id).await.unwrap();
|
||||||
repo.set_top_friends(&alice.id, vec![(carol.id.clone(), 1)])
|
assert_eq!(friends.len(), 1);
|
||||||
.await
|
assert_eq!(friends[0].0.position, 1);
|
||||||
.unwrap();
|
assert_eq!(friends[0].1.username.as_str(), "bob");
|
||||||
let friends = repo.list_for_user(&alice.id).await.unwrap();
|
}
|
||||||
assert_eq!(friends.len(), 1);
|
|
||||||
assert_eq!(friends[0].1.username.as_str(), "carol");
|
#[sqlx::test(migrations = "./migrations")]
|
||||||
}
|
async fn replace_top_friends(pool: sqlx::PgPool) {
|
||||||
|
let alice = seed_user(&pool, "alice", "alice@ex.com").await;
|
||||||
|
let bob = seed_user(&pool, "bob", "bob@ex.com").await;
|
||||||
|
let carol = seed_user(&pool, "carol", "carol@ex.com").await;
|
||||||
|
let repo = PgTopFriendRepository::new(pool);
|
||||||
|
repo.set_top_friends(&alice.id, vec![(bob.id.clone(), 1)])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
repo.set_top_friends(&alice.id, vec![(carol.id.clone(), 1)])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let friends = repo.list_for_user(&alice.id).await.unwrap();
|
||||||
|
assert_eq!(friends.len(), 1);
|
||||||
|
assert_eq!(friends[0].1.username.as_str(), "carol");
|
||||||
|
}
|
||||||
|
|||||||
@@ -139,7 +139,10 @@ impl UserReader for PgUserRepository {
|
|||||||
.into_domain()
|
.into_domain()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_paginated(&self, page: PageParams) -> Result<Paginated<UserSummary>, DomainError> {
|
async fn list_paginated(
|
||||||
|
&self,
|
||||||
|
page: PageParams,
|
||||||
|
) -> Result<Paginated<UserSummary>, DomainError> {
|
||||||
#[derive(sqlx::FromRow)]
|
#[derive(sqlx::FromRow)]
|
||||||
struct Row {
|
struct Row {
|
||||||
id: uuid::Uuid,
|
id: uuid::Uuid,
|
||||||
@@ -187,7 +190,12 @@ impl UserReader for PgUserRepository {
|
|||||||
following_count: r.following_count,
|
following_count: r.following_count,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
Ok(Paginated { items, total, page: page.page, per_page: page.per_page })
|
Ok(Paginated {
|
||||||
|
items,
|
||||||
|
total,
|
||||||
|
page: page.page,
|
||||||
|
per_page: page.per_page,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn find_by_ids(&self, ids: &[UserId]) -> Result<HashMap<UserId, User>, DomainError> {
|
async fn find_by_ids(&self, ids: &[UserId]) -> Result<HashMap<UserId, User>, DomainError> {
|
||||||
@@ -195,18 +203,19 @@ impl UserReader for PgUserRepository {
|
|||||||
return Ok(HashMap::new());
|
return Ok(HashMap::new());
|
||||||
}
|
}
|
||||||
let uuids: Vec<uuid::Uuid> = ids.iter().map(|id| id.as_uuid()).collect();
|
let uuids: Vec<uuid::Uuid> = ids.iter().map(|id| id.as_uuid()).collect();
|
||||||
let rows = sqlx::query_as::<_, UserRow>(
|
let rows = sqlx::query_as::<_, UserRow>(&format!("{USER_SELECT} WHERE id = ANY($1)"))
|
||||||
&format!("{USER_SELECT} WHERE id = ANY($1)")
|
.bind(&uuids[..])
|
||||||
)
|
.fetch_all(&self.pool)
|
||||||
.bind(&uuids[..])
|
.await
|
||||||
.fetch_all(&self.pool)
|
.into_domain()?;
|
||||||
.await
|
|
||||||
.into_domain()?;
|
|
||||||
|
|
||||||
Ok(rows.into_iter().map(|r| {
|
Ok(rows
|
||||||
let user = User::from(r);
|
.into_iter()
|
||||||
(user.id.clone(), user)
|
.map(|r| {
|
||||||
}).collect())
|
let user = User::from(r);
|
||||||
|
(user.id.clone(), user)
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,69 +1,70 @@
|
|||||||
use super::*;
|
|
||||||
use domain::{models::user::User, value_objects::*};
|
|
||||||
|
|
||||||
#[sqlx::test(migrations = "./migrations")]
|
use super::*;
|
||||||
async fn save_and_find_by_id(pool: sqlx::PgPool) {
|
use domain::{models::user::User, value_objects::*};
|
||||||
let repo = PgUserRepository::new(pool);
|
|
||||||
let user = User::new_local(
|
|
||||||
UserId::new(),
|
|
||||||
Username::new("alice").unwrap(),
|
|
||||||
Email::new("alice@ex.com").unwrap(),
|
|
||||||
PasswordHash("hash".into()),
|
|
||||||
);
|
|
||||||
repo.save(&user).await.unwrap();
|
|
||||||
let found = repo.find_by_id(&user.id).await.unwrap().unwrap();
|
|
||||||
assert_eq!(found.username.as_str(), "alice");
|
|
||||||
assert_eq!(found.email.as_str(), "alice@ex.com");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[sqlx::test(migrations = "./migrations")]
|
#[sqlx::test(migrations = "./migrations")]
|
||||||
async fn find_by_username_returns_none_when_missing(pool: sqlx::PgPool) {
|
async fn save_and_find_by_id(pool: sqlx::PgPool) {
|
||||||
let repo = PgUserRepository::new(pool);
|
let repo = PgUserRepository::new(pool);
|
||||||
let result = repo
|
let user = User::new_local(
|
||||||
.find_by_username(&Username::new("ghost").unwrap())
|
UserId::new(),
|
||||||
.await
|
Username::new("alice").unwrap(),
|
||||||
.unwrap();
|
Email::new("alice@ex.com").unwrap(),
|
||||||
assert!(result.is_none());
|
PasswordHash("hash".into()),
|
||||||
}
|
);
|
||||||
|
repo.save(&user).await.unwrap();
|
||||||
|
let found = repo.find_by_id(&user.id).await.unwrap().unwrap();
|
||||||
|
assert_eq!(found.username.as_str(), "alice");
|
||||||
|
assert_eq!(found.email.as_str(), "alice@ex.com");
|
||||||
|
}
|
||||||
|
|
||||||
#[sqlx::test(migrations = "./migrations")]
|
#[sqlx::test(migrations = "./migrations")]
|
||||||
async fn find_by_email(pool: sqlx::PgPool) {
|
async fn find_by_username_returns_none_when_missing(pool: sqlx::PgPool) {
|
||||||
let repo = PgUserRepository::new(pool);
|
let repo = PgUserRepository::new(pool);
|
||||||
let user = User::new_local(
|
let result = repo
|
||||||
UserId::new(),
|
.find_by_username(&Username::new("ghost").unwrap())
|
||||||
Username::new("bob").unwrap(),
|
|
||||||
Email::new("bob@ex.com").unwrap(),
|
|
||||||
PasswordHash("hash".into()),
|
|
||||||
);
|
|
||||||
repo.save(&user).await.unwrap();
|
|
||||||
let found = repo
|
|
||||||
.find_by_email(&Email::new("bob@ex.com").unwrap())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert!(found.is_some());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[sqlx::test(migrations = "./migrations")]
|
|
||||||
async fn update_profile_changes_fields(pool: sqlx::PgPool) {
|
|
||||||
let repo = PgUserRepository::new(pool);
|
|
||||||
let user = User::new_local(
|
|
||||||
UserId::new(),
|
|
||||||
Username::new("charlie").unwrap(),
|
|
||||||
Email::new("charlie@ex.com").unwrap(),
|
|
||||||
PasswordHash("hash".into()),
|
|
||||||
);
|
|
||||||
repo.save(&user).await.unwrap();
|
|
||||||
repo.update_profile(
|
|
||||||
&user.id,
|
|
||||||
Some("Charlie".into()),
|
|
||||||
Some("bio".into()),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let found = repo.find_by_id(&user.id).await.unwrap().unwrap();
|
assert!(result.is_none());
|
||||||
assert_eq!(found.display_name.as_deref(), Some("Charlie"));
|
}
|
||||||
assert_eq!(found.bio.as_deref(), Some("bio"));
|
|
||||||
}
|
#[sqlx::test(migrations = "./migrations")]
|
||||||
|
async fn find_by_email(pool: sqlx::PgPool) {
|
||||||
|
let repo = PgUserRepository::new(pool);
|
||||||
|
let user = User::new_local(
|
||||||
|
UserId::new(),
|
||||||
|
Username::new("bob").unwrap(),
|
||||||
|
Email::new("bob@ex.com").unwrap(),
|
||||||
|
PasswordHash("hash".into()),
|
||||||
|
);
|
||||||
|
repo.save(&user).await.unwrap();
|
||||||
|
let found = repo
|
||||||
|
.find_by_email(&Email::new("bob@ex.com").unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(found.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(migrations = "./migrations")]
|
||||||
|
async fn update_profile_changes_fields(pool: sqlx::PgPool) {
|
||||||
|
let repo = PgUserRepository::new(pool);
|
||||||
|
let user = User::new_local(
|
||||||
|
UserId::new(),
|
||||||
|
Username::new("charlie").unwrap(),
|
||||||
|
Email::new("charlie@ex.com").unwrap(),
|
||||||
|
PasswordHash("hash".into()),
|
||||||
|
);
|
||||||
|
repo.save(&user).await.unwrap();
|
||||||
|
repo.update_profile(
|
||||||
|
&user.id,
|
||||||
|
Some("Charlie".into()),
|
||||||
|
Some("bio".into()),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let found = repo.find_by_id(&user.id).await.unwrap().unwrap();
|
||||||
|
assert_eq!(found.display_name.as_deref(), Some("Charlie"));
|
||||||
|
assert_eq!(found.bio.as_deref(), Some("bio"));
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::testing::TestApRepo;
|
||||||
use activitypub_base::{ActorApUrls, OutboundFederationPort};
|
use activitypub_base::{ActorApUrls, OutboundFederationPort};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use crate::testing::TestApRepo;
|
|
||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
events::DomainEvent,
|
events::DomainEvent,
|
||||||
@@ -56,21 +56,12 @@ impl OutboundFederationPort for SpyPort {
|
|||||||
self.announced.lock().unwrap().push(ap_id.to_string());
|
self.announced.lock().unwrap().push(ap_id.to_string());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
async fn broadcast_undo_announce(
|
async fn broadcast_undo_announce(&self, _: &UserId, ap_id: &str) -> Result<(), DomainError> {
|
||||||
&self,
|
|
||||||
_: &UserId,
|
|
||||||
ap_id: &str,
|
|
||||||
) -> Result<(), DomainError> {
|
|
||||||
self.undo_announced.lock().unwrap().push(ap_id.to_string());
|
self.undo_announced.lock().unwrap().push(ap_id.to_string());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn broadcast_like(
|
async fn broadcast_like(&self, _: &UserId, ap_id: &str, _: &str) -> Result<(), DomainError> {
|
||||||
&self,
|
|
||||||
_: &UserId,
|
|
||||||
ap_id: &str,
|
|
||||||
_: &str,
|
|
||||||
) -> Result<(), DomainError> {
|
|
||||||
self.liked.lock().unwrap().push(ap_id.to_string());
|
self.liked.lock().unwrap().push(ap_id.to_string());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -123,7 +114,11 @@ fn svc(store: &TestStore, spy: Arc<SpyPort>) -> FederationEventService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn svc_with_ap(store: &TestStore, ap_repo: TestApRepo, spy: Arc<SpyPort>) -> FederationEventService {
|
fn svc_with_ap(
|
||||||
|
store: &TestStore,
|
||||||
|
ap_repo: TestApRepo,
|
||||||
|
spy: Arc<SpyPort>,
|
||||||
|
) -> FederationEventService {
|
||||||
FederationEventService {
|
FederationEventService {
|
||||||
thoughts: Arc::new(store.clone()),
|
thoughts: Arc::new(store.clone()),
|
||||||
users: Arc::new(store.clone()),
|
users: Arc::new(store.clone()),
|
||||||
|
|||||||
@@ -106,11 +106,7 @@ impl ActivityPubRepository for TestApRepo {
|
|||||||
) -> Result<ThoughtId, DomainError> {
|
) -> Result<ThoughtId, DomainError> {
|
||||||
Ok(ThoughtId::from_uuid(uuid::Uuid::new_v4()))
|
Ok(ThoughtId::from_uuid(uuid::Uuid::new_v4()))
|
||||||
}
|
}
|
||||||
async fn apply_note_update(
|
async fn apply_note_update(&self, _ap_id: &str, _new_content: &str) -> Result<(), DomainError> {
|
||||||
&self,
|
|
||||||
_ap_id: &str,
|
|
||||||
_new_content: &str,
|
|
||||||
) -> Result<(), DomainError> {
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
async fn retract_note(&self, _ap_id: &str) -> Result<(), DomainError> {
|
async fn retract_note(&self, _ap_id: &str) -> Result<(), DomainError> {
|
||||||
|
|||||||
@@ -34,21 +34,16 @@ pub async fn register(
|
|||||||
}
|
}
|
||||||
let hash = hasher.hash(&input.password).await?;
|
let hash = hasher.hash(&input.password).await?;
|
||||||
let user = User::new_local(UserId::new(), username, email, hash);
|
let user = User::new_local(UserId::new(), username, email, hash);
|
||||||
users
|
users.save(&user).await.map_err(|e| match e {
|
||||||
.save(&user)
|
DomainError::UniqueViolation { field: "username" } => {
|
||||||
.await
|
DomainError::Conflict("username taken".into())
|
||||||
.map_err(|e| match e {
|
}
|
||||||
DomainError::UniqueViolation { field: "username" } => {
|
DomainError::UniqueViolation { field: "email" } => {
|
||||||
DomainError::Conflict("username taken".into())
|
DomainError::Conflict("email taken".into())
|
||||||
}
|
}
|
||||||
DomainError::UniqueViolation { field: "email" } => {
|
DomainError::UniqueViolation { .. } => DomainError::Conflict("already exists".into()),
|
||||||
DomainError::Conflict("email taken".into())
|
other => other,
|
||||||
}
|
})?;
|
||||||
DomainError::UniqueViolation { .. } => {
|
|
||||||
DomainError::Conflict("already exists".into())
|
|
||||||
}
|
|
||||||
other => other,
|
|
||||||
})?;
|
|
||||||
events
|
events
|
||||||
.publish(&DomainEvent::UserRegistered {
|
.publish(&DomainEvent::UserRegistered {
|
||||||
user_id: user.id.clone(),
|
user_id: user.id.clone(),
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ use async_trait::async_trait;
|
|||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
events::DomainEvent,
|
events::DomainEvent,
|
||||||
models::{feed::{PageParams, Paginated, UserSummary}, user::User},
|
models::{
|
||||||
|
feed::{PageParams, Paginated, UserSummary},
|
||||||
|
user::User,
|
||||||
|
},
|
||||||
ports::{AuthService, GeneratedToken, PasswordHasher, UserReader, UserWriter},
|
ports::{AuthService, GeneratedToken, PasswordHasher, UserReader, UserWriter},
|
||||||
testing::{NoOpEventPublisher, TestStore},
|
testing::{NoOpEventPublisher, TestStore},
|
||||||
value_objects::{Email, PasswordHash, UserId, Username},
|
value_objects::{Email, PasswordHash, UserId, Username},
|
||||||
@@ -19,10 +22,7 @@ impl UserReader for ConflictOnSaveStore {
|
|||||||
async fn find_by_id(&self, id: &UserId) -> Result<Option<User>, DomainError> {
|
async fn find_by_id(&self, id: &UserId) -> Result<Option<User>, DomainError> {
|
||||||
self.0.find_by_id(id).await
|
self.0.find_by_id(id).await
|
||||||
}
|
}
|
||||||
async fn find_by_username(
|
async fn find_by_username(&self, username: &Username) -> Result<Option<User>, DomainError> {
|
||||||
&self,
|
|
||||||
username: &Username,
|
|
||||||
) -> Result<Option<User>, DomainError> {
|
|
||||||
self.0.find_by_username(username).await
|
self.0.find_by_username(username).await
|
||||||
}
|
}
|
||||||
async fn find_by_email(&self, email: &Email) -> Result<Option<User>, DomainError> {
|
async fn find_by_email(&self, email: &Email) -> Result<Option<User>, DomainError> {
|
||||||
@@ -34,10 +34,16 @@ impl UserReader for ConflictOnSaveStore {
|
|||||||
async fn count(&self) -> Result<i64, DomainError> {
|
async fn count(&self) -> Result<i64, DomainError> {
|
||||||
self.0.count().await
|
self.0.count().await
|
||||||
}
|
}
|
||||||
async fn list_paginated(&self, page: PageParams) -> Result<Paginated<UserSummary>, DomainError> {
|
async fn list_paginated(
|
||||||
|
&self,
|
||||||
|
page: PageParams,
|
||||||
|
) -> Result<Paginated<UserSummary>, DomainError> {
|
||||||
self.0.list_paginated(page).await
|
self.0.list_paginated(page).await
|
||||||
}
|
}
|
||||||
async fn find_by_ids(&self, ids: &[UserId]) -> Result<std::collections::HashMap<UserId, User>, DomainError> {
|
async fn find_by_ids(
|
||||||
|
&self,
|
||||||
|
ids: &[UserId],
|
||||||
|
) -> Result<std::collections::HashMap<UserId, User>, DomainError> {
|
||||||
self.0.find_by_ids(ids).await
|
self.0.find_by_ids(ids).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,7 +63,14 @@ impl UserWriter for ConflictOnSaveStore {
|
|||||||
custom_css: Option<String>,
|
custom_css: Option<String>,
|
||||||
) -> Result<(), DomainError> {
|
) -> Result<(), DomainError> {
|
||||||
self.0
|
self.0
|
||||||
.update_profile(user_id, display_name, bio, avatar_url, header_url, custom_css)
|
.update_profile(
|
||||||
|
user_id,
|
||||||
|
display_name,
|
||||||
|
bio,
|
||||||
|
avatar_url,
|
||||||
|
header_url,
|
||||||
|
custom_css,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,10 +80,7 @@ impl UserReader for EmailConflictOnSaveStore {
|
|||||||
async fn find_by_id(&self, id: &UserId) -> Result<Option<User>, DomainError> {
|
async fn find_by_id(&self, id: &UserId) -> Result<Option<User>, DomainError> {
|
||||||
self.0.find_by_id(id).await
|
self.0.find_by_id(id).await
|
||||||
}
|
}
|
||||||
async fn find_by_username(
|
async fn find_by_username(&self, username: &Username) -> Result<Option<User>, DomainError> {
|
||||||
&self,
|
|
||||||
username: &Username,
|
|
||||||
) -> Result<Option<User>, DomainError> {
|
|
||||||
self.0.find_by_username(username).await
|
self.0.find_by_username(username).await
|
||||||
}
|
}
|
||||||
async fn find_by_email(&self, email: &Email) -> Result<Option<User>, DomainError> {
|
async fn find_by_email(&self, email: &Email) -> Result<Option<User>, DomainError> {
|
||||||
@@ -82,10 +92,16 @@ impl UserReader for EmailConflictOnSaveStore {
|
|||||||
async fn count(&self) -> Result<i64, DomainError> {
|
async fn count(&self) -> Result<i64, DomainError> {
|
||||||
self.0.count().await
|
self.0.count().await
|
||||||
}
|
}
|
||||||
async fn list_paginated(&self, page: PageParams) -> Result<Paginated<UserSummary>, DomainError> {
|
async fn list_paginated(
|
||||||
|
&self,
|
||||||
|
page: PageParams,
|
||||||
|
) -> Result<Paginated<UserSummary>, DomainError> {
|
||||||
self.0.list_paginated(page).await
|
self.0.list_paginated(page).await
|
||||||
}
|
}
|
||||||
async fn find_by_ids(&self, ids: &[UserId]) -> Result<std::collections::HashMap<UserId, User>, DomainError> {
|
async fn find_by_ids(
|
||||||
|
&self,
|
||||||
|
ids: &[UserId],
|
||||||
|
) -> Result<std::collections::HashMap<UserId, User>, DomainError> {
|
||||||
self.0.find_by_ids(ids).await
|
self.0.find_by_ids(ids).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,7 +121,14 @@ impl UserWriter for EmailConflictOnSaveStore {
|
|||||||
custom_css: Option<String>,
|
custom_css: Option<String>,
|
||||||
) -> Result<(), DomainError> {
|
) -> Result<(), DomainError> {
|
||||||
self.0
|
self.0
|
||||||
.update_profile(user_id, display_name, bio, avatar_url, header_url, custom_css)
|
.update_profile(
|
||||||
|
user_id,
|
||||||
|
display_name,
|
||||||
|
bio,
|
||||||
|
avatar_url,
|
||||||
|
header_url,
|
||||||
|
custom_css,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ use domain::{
|
|||||||
remote_actor::RemoteActor,
|
remote_actor::RemoteActor,
|
||||||
},
|
},
|
||||||
ports::{
|
ports::{
|
||||||
EventPublisher, FederationActionPort, FederationFollowPort,
|
EventPublisher, FederationActionPort, FederationFollowPort, FederationFollowRequestPort,
|
||||||
FederationFollowRequestPort, FederationSchedulerPort, FeedQuery, FeedRepository,
|
FederationSchedulerPort, FeedQuery, FeedRepository, FollowRepository,
|
||||||
FollowRepository, RemoteActorConnectionRepository, UserReader,
|
RemoteActorConnectionRepository, UserReader,
|
||||||
},
|
},
|
||||||
value_objects::UserId,
|
value_objects::UserId,
|
||||||
};
|
};
|
||||||
@@ -86,7 +86,13 @@ pub async fn get_remote_actor_posts(
|
|||||||
Some(id) => id,
|
Some(id) => id,
|
||||||
None => ap_repo.intern_remote_actor(&actor.url).await?,
|
None => ap_repo.intern_remote_actor(&actor.url).await?,
|
||||||
};
|
};
|
||||||
let result = feed.query(&FeedQuery::user(author_id, page.clone(), viewer_id.cloned())).await?;
|
let result = feed
|
||||||
|
.query(&FeedQuery::user(
|
||||||
|
author_id,
|
||||||
|
page.clone(),
|
||||||
|
viewer_id.cloned(),
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
if let Some(outbox_url) = actor.outbox_url {
|
if let Some(outbox_url) = actor.outbox_url {
|
||||||
let _ = scheduler
|
let _ = scheduler
|
||||||
.schedule_actor_posts_fetch(&actor.url, &outbox_url)
|
.schedule_actor_posts_fetch(&actor.url, &outbox_url)
|
||||||
|
|||||||
@@ -13,5 +13,6 @@ pub async fn get_home_feed(
|
|||||||
) -> Result<Paginated<FeedEntry>, DomainError> {
|
) -> Result<Paginated<FeedEntry>, DomainError> {
|
||||||
let mut following_ids = follows.get_accepted_following_ids(user_id).await?;
|
let mut following_ids = follows.get_accepted_following_ids(user_id).await?;
|
||||||
following_ids.push(user_id.clone());
|
following_ids.push(user_id.clone());
|
||||||
feed.query(&FeedQuery::home(user_id.clone(), following_ids, page)).await
|
feed.query(&FeedQuery::home(user_id.clone(), following_ids, page))
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ use domain::{
|
|||||||
feed::{EngagementStats, FeedEntry},
|
feed::{EngagementStats, FeedEntry},
|
||||||
thought::{Thought, Visibility},
|
thought::{Thought, Visibility},
|
||||||
},
|
},
|
||||||
ports::{EngagementRepository, EventPublisher, OutboxWriter, TagRepository, ThoughtRepository, UserReader},
|
ports::{
|
||||||
|
EngagementRepository, EventPublisher, OutboxWriter, TagRepository, ThoughtRepository,
|
||||||
|
UserReader,
|
||||||
|
},
|
||||||
value_objects::{Content, ThoughtId, UserId},
|
value_objects::{Content, ThoughtId, UserId},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -133,10 +136,20 @@ pub async fn get_thought_view(
|
|||||||
.await?
|
.await?
|
||||||
.ok_or(DomainError::NotFound)?;
|
.ok_or(DomainError::NotFound)?;
|
||||||
let mut map = engagement.get_for_thoughts(&[id.clone()], viewer).await?;
|
let mut map = engagement.get_for_thoughts(&[id.clone()], viewer).await?;
|
||||||
let (stats, viewer_ctx) = map.remove(id).unwrap_or(
|
let (stats, viewer_ctx) = map.remove(id).unwrap_or((
|
||||||
(EngagementStats { like_count: 0, boost_count: 0, reply_count: 0 }, None)
|
EngagementStats {
|
||||||
);
|
like_count: 0,
|
||||||
Ok(FeedEntry { thought, author, stats, viewer: viewer_ctx })
|
boost_count: 0,
|
||||||
|
reply_count: 0,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
));
|
||||||
|
Ok(FeedEntry {
|
||||||
|
thought,
|
||||||
|
author,
|
||||||
|
stats,
|
||||||
|
viewer: viewer_ctx,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetches a thread (root + replies) enriched with authors + real engagement stats.
|
/// Fetches a thread (root + replies) enriched with authors + real engagement stats.
|
||||||
@@ -169,10 +182,20 @@ pub async fn get_thread_views(
|
|||||||
.get(&thought.user_id)
|
.get(&thought.user_id)
|
||||||
.cloned()
|
.cloned()
|
||||||
.ok_or(DomainError::NotFound)?;
|
.ok_or(DomainError::NotFound)?;
|
||||||
let (stats, viewer_ctx) = engagement_map.remove(&thought.id).unwrap_or(
|
let (stats, viewer_ctx) = engagement_map.remove(&thought.id).unwrap_or((
|
||||||
(EngagementStats { like_count: 0, boost_count: 0, reply_count: 0 }, None)
|
EngagementStats {
|
||||||
);
|
like_count: 0,
|
||||||
entries.push(FeedEntry { thought, author, stats, viewer: viewer_ctx });
|
boost_count: 0,
|
||||||
|
reply_count: 0,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
));
|
||||||
|
entries.push(FeedEntry {
|
||||||
|
thought,
|
||||||
|
author,
|
||||||
|
stats,
|
||||||
|
viewer: viewer_ctx,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Ok(entries)
|
Ok(entries)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,9 +31,16 @@ async fn create_thought_saves_and_stages_outbox_event() {
|
|||||||
let outbox = TestOutbox::default();
|
let outbox = TestOutbox::default();
|
||||||
let u = user();
|
let u = user();
|
||||||
store.users.lock().unwrap().push(u.clone());
|
store.users.lock().unwrap().push(u.clone());
|
||||||
let out = create_thought(&store, &store, &store, &NoOpEventPublisher, &outbox, input(u.id.clone()))
|
let out = create_thought(
|
||||||
.await
|
&store,
|
||||||
.unwrap();
|
&store,
|
||||||
|
&store,
|
||||||
|
&NoOpEventPublisher,
|
||||||
|
&outbox,
|
||||||
|
input(u.id.clone()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
assert_eq!(out.thought.content.as_str(), "hello");
|
assert_eq!(out.thought.content.as_str(), "hello");
|
||||||
let staged = outbox.staged();
|
let staged = outbox.staged();
|
||||||
assert_eq!(staged.len(), 1);
|
assert_eq!(staged.len(), 1);
|
||||||
@@ -64,7 +71,9 @@ async fn delete_thought_stages_outbox_event() {
|
|||||||
|
|
||||||
let staged = outbox.staged();
|
let staged = outbox.staged();
|
||||||
assert_eq!(staged.len(), 1);
|
assert_eq!(staged.len(), 1);
|
||||||
assert!(matches!(&staged[0], DomainEvent::ThoughtDeleted { thought_id, .. } if *thought_id == tid));
|
assert!(
|
||||||
|
matches!(&staged[0], DomainEvent::ThoughtDeleted { thought_id, .. } if *thought_id == tid)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -82,9 +91,15 @@ async fn delete_own_thought_succeeds() {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
delete_thought(&store, &NoOpEventPublisher, &NoOpOutboxWriter, &out.thought.id, &u.id)
|
delete_thought(
|
||||||
.await
|
&store,
|
||||||
.unwrap();
|
&NoOpEventPublisher,
|
||||||
|
&NoOpOutboxWriter,
|
||||||
|
&out.thought.id,
|
||||||
|
&u.id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
assert!(store.thoughts.lock().unwrap().is_empty());
|
assert!(store.thoughts.lock().unwrap().is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,9 +128,15 @@ async fn delete_other_thought_returns_not_found() {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let err = delete_thought(&store, &NoOpEventPublisher, &NoOpOutboxWriter, &out.thought.id, &bob.id)
|
let err = delete_thought(
|
||||||
.await
|
&store,
|
||||||
.unwrap_err();
|
&NoOpEventPublisher,
|
||||||
|
&NoOpOutboxWriter,
|
||||||
|
&out.thought.id,
|
||||||
|
&bob.id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
assert!(matches!(err, DomainError::NotFound));
|
assert!(matches!(err, DomainError::NotFound));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,9 +145,16 @@ async fn edit_thought_changes_content_and_emits_event() {
|
|||||||
let store = TestStore::default();
|
let store = TestStore::default();
|
||||||
let alice = user();
|
let alice = user();
|
||||||
store.users.lock().unwrap().push(alice.clone());
|
store.users.lock().unwrap().push(alice.clone());
|
||||||
let out = create_thought(&store, &store, &store, &NoOpEventPublisher, &NoOpOutboxWriter, input(alice.id.clone()))
|
let out = create_thought(
|
||||||
.await
|
&store,
|
||||||
.unwrap();
|
&store,
|
||||||
|
&store,
|
||||||
|
&NoOpEventPublisher,
|
||||||
|
&NoOpOutboxWriter,
|
||||||
|
input(alice.id.clone()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
let tid = out.thought.id.clone();
|
let tid = out.thought.id.clone();
|
||||||
|
|
||||||
edit_thought(&store, &store, &tid, &alice.id, "updated".to_string())
|
edit_thought(&store, &store, &tid, &alice.id, "updated".to_string())
|
||||||
@@ -222,9 +250,13 @@ fn make_thought(user_id: UserId) -> Thought {
|
|||||||
async fn get_thought_view_returns_feed_entry() {
|
async fn get_thought_view_returns_feed_entry() {
|
||||||
let store = TestStore::default();
|
let store = TestStore::default();
|
||||||
let user = make_user();
|
let user = make_user();
|
||||||
<TestStore as UserWriter>::save(&store, &user).await.unwrap();
|
<TestStore as UserWriter>::save(&store, &user)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
let thought = make_thought(user.id.clone());
|
let thought = make_thought(user.id.clone());
|
||||||
<TestStore as ThoughtRepository>::save(&store, &thought).await.unwrap();
|
<TestStore as ThoughtRepository>::save(&store, &thought)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let entry = get_thought_view(&store, &store, &store, &thought.id, None)
|
let entry = get_thought_view(&store, &store, &store, &thought.id, None)
|
||||||
.await
|
.await
|
||||||
@@ -248,9 +280,13 @@ async fn get_thought_view_returns_not_found_for_missing_thought() {
|
|||||||
async fn get_thread_views_batches_correctly() {
|
async fn get_thread_views_batches_correctly() {
|
||||||
let store = TestStore::default();
|
let store = TestStore::default();
|
||||||
let user = make_user();
|
let user = make_user();
|
||||||
<TestStore as UserWriter>::save(&store, &user).await.unwrap();
|
<TestStore as UserWriter>::save(&store, &user)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
let root = make_thought(user.id.clone());
|
let root = make_thought(user.id.clone());
|
||||||
<TestStore as ThoughtRepository>::save(&store, &root).await.unwrap();
|
<TestStore as ThoughtRepository>::save(&store, &root)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
let reply = Thought::new_local(
|
let reply = Thought::new_local(
|
||||||
ThoughtId::new(),
|
ThoughtId::new(),
|
||||||
user.id.clone(),
|
user.id.clone(),
|
||||||
@@ -260,7 +296,9 @@ async fn get_thread_views_batches_correctly() {
|
|||||||
None,
|
None,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
<TestStore as ThoughtRepository>::save(&store, &reply).await.unwrap();
|
<TestStore as ThoughtRepository>::save(&store, &reply)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let entries = get_thread_views(&store, &store, &store, &root.id, None)
|
let entries = get_thread_views(&store, &store, &store, &root.id, None)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ use std::sync::Arc;
|
|||||||
use activitypub::ThoughtsObjectHandler;
|
use activitypub::ThoughtsObjectHandler;
|
||||||
use activitypub_base::service::ActivityPubService;
|
use activitypub_base::service::ActivityPubService;
|
||||||
use auth::ApiKeyServiceImpl;
|
use auth::ApiKeyServiceImpl;
|
||||||
use domain::{errors::DomainError, events::DomainEvent, ports::{EventPublisher, OutboxWriter}};
|
use domain::{
|
||||||
|
errors::DomainError,
|
||||||
|
events::DomainEvent,
|
||||||
|
ports::{EventPublisher, OutboxWriter},
|
||||||
|
};
|
||||||
use event_transport::EventPublisherAdapter;
|
use event_transport::EventPublisherAdapter;
|
||||||
use nats::NatsTransport;
|
use nats::NatsTransport;
|
||||||
use postgres::activitypub::PgActivityPubRepository;
|
use postgres::activitypub::PgActivityPubRepository;
|
||||||
@@ -130,9 +134,9 @@ pub async fn build(cfg: &Config) -> Infrastructure {
|
|||||||
ap_repo: Arc::new(PgActivityPubRepository::new(pool.clone())),
|
ap_repo: Arc::new(PgActivityPubRepository::new(pool.clone())),
|
||||||
remote_actor_connections: Arc::new(PgRemoteActorConnectionRepository::new(pool.clone())),
|
remote_actor_connections: Arc::new(PgRemoteActorConnectionRepository::new(pool.clone())),
|
||||||
federation_scheduler: ap_service.clone() as Arc<dyn domain::ports::FederationSchedulerPort>,
|
federation_scheduler: ap_service.clone() as Arc<dyn domain::ports::FederationSchedulerPort>,
|
||||||
api_key_auth: Arc::new(ApiKeyServiceImpl::new(
|
api_key_auth: Arc::new(ApiKeyServiceImpl::new(Arc::new(
|
||||||
Arc::new(postgres::api_key::PgApiKeyRepository::new(pool.clone())),
|
postgres::api_key::PgApiKeyRepository::new(pool.clone()),
|
||||||
)),
|
))),
|
||||||
engagement: Arc::new(PgEngagementRepository::new(pool.clone())),
|
engagement: Arc::new(PgEngagementRepository::new(pool.clone())),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ pub trait UserReader: Send + Sync {
|
|||||||
async fn find_by_email(&self, email: &Email) -> Result<Option<User>, DomainError>;
|
async fn find_by_email(&self, email: &Email) -> Result<Option<User>, DomainError>;
|
||||||
async fn list_with_stats(&self) -> Result<Vec<UserSummary>, DomainError>;
|
async fn list_with_stats(&self) -> Result<Vec<UserSummary>, DomainError>;
|
||||||
async fn count(&self) -> Result<i64, DomainError>;
|
async fn count(&self) -> Result<i64, DomainError>;
|
||||||
async fn list_paginated(&self, page: PageParams) -> Result<Paginated<UserSummary>, DomainError>;
|
async fn list_paginated(&self, page: PageParams)
|
||||||
|
-> Result<Paginated<UserSummary>, DomainError>;
|
||||||
async fn find_by_ids(&self, ids: &[UserId]) -> Result<HashMap<UserId, User>, DomainError>;
|
async fn find_by_ids(&self, ids: &[UserId]) -> Result<HashMap<UserId, User>, DomainError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,19 +354,43 @@ pub struct FeedQuery {
|
|||||||
|
|
||||||
impl FeedQuery {
|
impl FeedQuery {
|
||||||
pub fn home(viewer_id: UserId, following_ids: Vec<UserId>, page: PageParams) -> Self {
|
pub fn home(viewer_id: UserId, following_ids: Vec<UserId>, page: PageParams) -> Self {
|
||||||
Self { scope: FeedScope::Home { following_ids }, page, viewer_id: Some(viewer_id) }
|
Self {
|
||||||
|
scope: FeedScope::Home { following_ids },
|
||||||
|
page,
|
||||||
|
viewer_id: Some(viewer_id),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pub fn public(page: PageParams, viewer_id: Option<UserId>) -> Self {
|
pub fn public(page: PageParams, viewer_id: Option<UserId>) -> Self {
|
||||||
Self { scope: FeedScope::Public, page, viewer_id }
|
Self {
|
||||||
|
scope: FeedScope::Public,
|
||||||
|
page,
|
||||||
|
viewer_id,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pub fn tag(tag_name: impl Into<String>, page: PageParams, viewer_id: Option<UserId>) -> Self {
|
pub fn tag(tag_name: impl Into<String>, page: PageParams, viewer_id: Option<UserId>) -> Self {
|
||||||
Self { scope: FeedScope::Tag { tag_name: tag_name.into() }, page, viewer_id }
|
Self {
|
||||||
|
scope: FeedScope::Tag {
|
||||||
|
tag_name: tag_name.into(),
|
||||||
|
},
|
||||||
|
page,
|
||||||
|
viewer_id,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pub fn user(user_id: UserId, page: PageParams, viewer_id: Option<UserId>) -> Self {
|
pub fn user(user_id: UserId, page: PageParams, viewer_id: Option<UserId>) -> Self {
|
||||||
Self { scope: FeedScope::User { user_id }, page, viewer_id }
|
Self {
|
||||||
|
scope: FeedScope::User { user_id },
|
||||||
|
page,
|
||||||
|
viewer_id,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pub fn search(query: impl Into<String>, page: PageParams, viewer_id: Option<UserId>) -> Self {
|
pub fn search(query: impl Into<String>, page: PageParams, viewer_id: Option<UserId>) -> Self {
|
||||||
Self { scope: FeedScope::Search { query: query.into() }, page, viewer_id }
|
Self {
|
||||||
|
scope: FeedScope::Search {
|
||||||
|
query: query.into(),
|
||||||
|
},
|
||||||
|
page,
|
||||||
|
viewer_id,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,7 +417,6 @@ pub trait SearchPort: Send + Sync {
|
|||||||
) -> Result<Paginated<User>, DomainError>;
|
) -> Result<Paginated<User>, DomainError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait FederationSchedulerPort: Send + Sync {
|
pub trait FederationSchedulerPort: Send + Sync {
|
||||||
async fn schedule_actor_posts_fetch(
|
async fn schedule_actor_posts_fetch(
|
||||||
|
|||||||
@@ -83,17 +83,30 @@ impl UserReader for TestStore {
|
|||||||
.count() as i64)
|
.count() as i64)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_paginated(&self, page: PageParams) -> Result<Paginated<UserSummary>, DomainError> {
|
async fn list_paginated(
|
||||||
|
&self,
|
||||||
|
page: PageParams,
|
||||||
|
) -> Result<Paginated<UserSummary>, DomainError> {
|
||||||
let all = self.list_with_stats().await?;
|
let all = self.list_with_stats().await?;
|
||||||
let total = all.len() as i64;
|
let total = all.len() as i64;
|
||||||
let start = page.offset() as usize;
|
let start = page.offset() as usize;
|
||||||
let items: Vec<UserSummary> = all.into_iter().skip(start).take(page.limit() as usize).collect();
|
let items: Vec<UserSummary> = all
|
||||||
Ok(Paginated { items, total, page: page.page, per_page: page.per_page })
|
.into_iter()
|
||||||
|
.skip(start)
|
||||||
|
.take(page.limit() as usize)
|
||||||
|
.collect();
|
||||||
|
Ok(Paginated {
|
||||||
|
items,
|
||||||
|
total,
|
||||||
|
page: page.page,
|
||||||
|
per_page: page.per_page,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn find_by_ids(&self, ids: &[UserId]) -> Result<HashMap<UserId, User>, DomainError> {
|
async fn find_by_ids(&self, ids: &[UserId]) -> Result<HashMap<UserId, User>, DomainError> {
|
||||||
let g = self.users.lock().unwrap();
|
let g = self.users.lock().unwrap();
|
||||||
let map = g.iter()
|
let map = g
|
||||||
|
.iter()
|
||||||
.filter(|u| ids.contains(&u.id))
|
.filter(|u| ids.contains(&u.id))
|
||||||
.map(|u| (u.id.clone(), u.clone()))
|
.map(|u| (u.id.clone(), u.clone()))
|
||||||
.collect();
|
.collect();
|
||||||
@@ -294,7 +307,16 @@ impl EngagementRepository for TestStore {
|
|||||||
&self,
|
&self,
|
||||||
thought_ids: &[ThoughtId],
|
thought_ids: &[ThoughtId],
|
||||||
viewer_id: Option<&UserId>,
|
viewer_id: Option<&UserId>,
|
||||||
) -> Result<HashMap<ThoughtId, (crate::models::feed::EngagementStats, Option<crate::models::feed::ViewerContext>)>, DomainError> {
|
) -> Result<
|
||||||
|
HashMap<
|
||||||
|
ThoughtId,
|
||||||
|
(
|
||||||
|
crate::models::feed::EngagementStats,
|
||||||
|
Option<crate::models::feed::ViewerContext>,
|
||||||
|
),
|
||||||
|
>,
|
||||||
|
DomainError,
|
||||||
|
> {
|
||||||
use crate::models::feed::{EngagementStats, ViewerContext};
|
use crate::models::feed::{EngagementStats, ViewerContext};
|
||||||
let likes = self.likes.lock().unwrap();
|
let likes = self.likes.lock().unwrap();
|
||||||
let boosts = self.boosts.lock().unwrap();
|
let boosts = self.boosts.lock().unwrap();
|
||||||
@@ -304,12 +326,29 @@ impl EngagementRepository for TestStore {
|
|||||||
for tid in thought_ids {
|
for tid in thought_ids {
|
||||||
let like_count = likes.iter().filter(|l| &l.thought_id == tid).count() as i64;
|
let like_count = likes.iter().filter(|l| &l.thought_id == tid).count() as i64;
|
||||||
let boost_count = boosts.iter().filter(|b| &b.thought_id == tid).count() as i64;
|
let boost_count = boosts.iter().filter(|b| &b.thought_id == tid).count() as i64;
|
||||||
let reply_count = thoughts.iter().filter(|t| t.in_reply_to_id.as_ref() == Some(tid)).count() as i64;
|
let reply_count = thoughts
|
||||||
|
.iter()
|
||||||
|
.filter(|t| t.in_reply_to_id.as_ref() == Some(tid))
|
||||||
|
.count() as i64;
|
||||||
let viewer = viewer_id.map(|vid| ViewerContext {
|
let viewer = viewer_id.map(|vid| ViewerContext {
|
||||||
liked: likes.iter().any(|l| &l.thought_id == tid && &l.user_id == vid),
|
liked: likes
|
||||||
boosted: boosts.iter().any(|b| &b.thought_id == tid && &b.user_id == vid),
|
.iter()
|
||||||
|
.any(|l| &l.thought_id == tid && &l.user_id == vid),
|
||||||
|
boosted: boosts
|
||||||
|
.iter()
|
||||||
|
.any(|b| &b.thought_id == tid && &b.user_id == vid),
|
||||||
});
|
});
|
||||||
result.insert(tid.clone(), (EngagementStats { like_count, boost_count, reply_count }, viewer));
|
result.insert(
|
||||||
|
tid.clone(),
|
||||||
|
(
|
||||||
|
EngagementStats {
|
||||||
|
like_count,
|
||||||
|
boost_count,
|
||||||
|
reply_count,
|
||||||
|
},
|
||||||
|
viewer,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
@@ -763,7 +802,10 @@ impl RemoteActorConnectionRepository for TestStore {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl FeedRepository for TestStore {
|
impl FeedRepository for TestStore {
|
||||||
async fn query(&self, _q: &crate::ports::FeedQuery) -> Result<Paginated<FeedEntry>, DomainError> {
|
async fn query(
|
||||||
|
&self,
|
||||||
|
_q: &crate::ports::FeedQuery,
|
||||||
|
) -> Result<Paginated<FeedEntry>, DomainError> {
|
||||||
Ok(Paginated {
|
Ok(Paginated {
|
||||||
items: vec![],
|
items: vec![],
|
||||||
total: 0,
|
total: 0,
|
||||||
|
|||||||
@@ -8,11 +8,7 @@ use api_types::{
|
|||||||
responses::{ApiKeyResponse, CreatedApiKeyResponse},
|
responses::{ApiKeyResponse, CreatedApiKeyResponse},
|
||||||
};
|
};
|
||||||
use application::use_cases::api_keys::{create_api_key, delete_api_key, list_api_keys};
|
use application::use_cases::api_keys::{create_api_key, delete_api_key, list_api_keys};
|
||||||
use axum::{
|
use axum::{extract::Path, http::StatusCode, Json};
|
||||||
extract::Path,
|
|
||||||
http::StatusCode,
|
|
||||||
Json,
|
|
||||||
};
|
|
||||||
use domain::{ports::ApiKeyRepository, value_objects::ApiKeyId};
|
use domain::{ports::ApiKeyRepository, value_objects::ApiKeyId};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
use crate::{
|
use crate::{deps_struct, errors::ApiError, extractors::Deps};
|
||||||
deps_struct,
|
|
||||||
errors::ApiError,
|
|
||||||
extractors::Deps,
|
|
||||||
};
|
|
||||||
use api_types::{
|
use api_types::{
|
||||||
requests::{LoginRequest, RegisterRequest},
|
requests::{LoginRequest, RegisterRequest},
|
||||||
responses::{AuthResponse, ErrorResponse, UserResponse},
|
responses::{AuthResponse, ErrorResponse, UserResponse},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use crate::{
|
|||||||
handlers::feed::to_thought_response,
|
handlers::feed::to_thought_response,
|
||||||
state::AppState,
|
state::AppState,
|
||||||
};
|
};
|
||||||
|
use activitypub_base::ActivityPubRepository;
|
||||||
use api_types::{
|
use api_types::{
|
||||||
requests::PaginationQuery,
|
requests::PaginationQuery,
|
||||||
responses::{ActorConnectionPageResponse, ActorConnectionResponse},
|
responses::{ActorConnectionPageResponse, ActorConnectionResponse},
|
||||||
@@ -15,7 +16,6 @@ use axum::{
|
|||||||
extract::{Path, Query},
|
extract::{Path, Query},
|
||||||
Json,
|
Json,
|
||||||
};
|
};
|
||||||
use activitypub_base::ActivityPubRepository;
|
|
||||||
use domain::{
|
use domain::{
|
||||||
models::feed::PageParams,
|
models::feed::PageParams,
|
||||||
ports::{
|
ports::{
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ use axum::{
|
|||||||
};
|
};
|
||||||
use domain::{
|
use domain::{
|
||||||
models::feed::PageParams,
|
models::feed::PageParams,
|
||||||
ports::{FederationActionPort, FeedQuery, FeedRepository, FollowRepository, SearchPort, TagRepository, UserRepository},
|
ports::{
|
||||||
|
FederationActionPort, FeedQuery, FeedRepository, FollowRepository, SearchPort,
|
||||||
|
TagRepository, UserRepository,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
deps_struct!(FeedDeps {
|
deps_struct!(FeedDeps {
|
||||||
@@ -224,7 +227,10 @@ pub async fn user_thoughts_handler(
|
|||||||
page: q.page(),
|
page: q.page(),
|
||||||
per_page: q.per_page(),
|
per_page: q.per_page(),
|
||||||
};
|
};
|
||||||
let result = d.feed.query(&FeedQuery::user(user.id.clone(), page, viewer)).await?;
|
let result = d
|
||||||
|
.feed
|
||||||
|
.query(&FeedQuery::user(user.id.clone(), page, viewer))
|
||||||
|
.await?;
|
||||||
Ok(Json(serde_json::json!({
|
Ok(Json(serde_json::json!({
|
||||||
"total": result.total,
|
"total": result.total,
|
||||||
"page": result.page,
|
"page": result.page,
|
||||||
@@ -241,7 +247,10 @@ pub async fn get_popular_tags(
|
|||||||
.get("limit")
|
.get("limit")
|
||||||
.and_then(|v| v.parse().ok())
|
.and_then(|v| v.parse().ok())
|
||||||
.unwrap_or(api_types::requests::DEFAULT_PER_PAGE as usize);
|
.unwrap_or(api_types::requests::DEFAULT_PER_PAGE as usize);
|
||||||
let tags = d.tags.popular_tags(limit.min(api_types::requests::MAX_PER_PAGE as usize)).await?;
|
let tags = d
|
||||||
|
.tags
|
||||||
|
.popular_tags(limit.min(api_types::requests::MAX_PER_PAGE as usize))
|
||||||
|
.await?;
|
||||||
Ok(Json(serde_json::json!({
|
Ok(Json(serde_json::json!({
|
||||||
"tags": tags.iter().map(|(name, count)| serde_json::json!({
|
"tags": tags.iter().map(|(name, count)| serde_json::json!({
|
||||||
"name": name,
|
"name": name,
|
||||||
@@ -268,7 +277,10 @@ pub async fn tag_thoughts_handler(
|
|||||||
page: q.page(),
|
page: q.page(),
|
||||||
per_page: q.per_page(),
|
per_page: q.per_page(),
|
||||||
};
|
};
|
||||||
let result = d.feed.query(&FeedQuery::tag(&tag_name, page, viewer)).await?;
|
let result = d
|
||||||
|
.feed
|
||||||
|
.query(&FeedQuery::tag(&tag_name, page, viewer))
|
||||||
|
.await?;
|
||||||
Ok(Json(serde_json::json!({
|
Ok(Json(serde_json::json!({
|
||||||
"tag": tag_name,
|
"tag": tag_name,
|
||||||
"total": result.total,
|
"total": result.total,
|
||||||
|
|||||||
@@ -8,11 +8,7 @@ use application::use_cases::notifications::{
|
|||||||
count_unread_notifications, list_notifications as uc_list_notifications,
|
count_unread_notifications, list_notifications as uc_list_notifications,
|
||||||
mark_all_notifications_read, mark_notification_read as uc_mark_notification_read,
|
mark_all_notifications_read, mark_notification_read as uc_mark_notification_read,
|
||||||
};
|
};
|
||||||
use axum::{
|
use axum::{extract::Path, http::StatusCode, Json};
|
||||||
extract::Path,
|
|
||||||
http::StatusCode,
|
|
||||||
Json,
|
|
||||||
};
|
|
||||||
use domain::{
|
use domain::{
|
||||||
models::feed::PageParams, ports::NotificationRepository, value_objects::NotificationId,
|
models::feed::PageParams, ports::NotificationRepository, value_objects::NotificationId,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::handlers::auth::to_user_response;
|
||||||
use crate::{
|
use crate::{
|
||||||
deps_struct,
|
deps_struct,
|
||||||
errors::ApiError,
|
errors::ApiError,
|
||||||
@@ -5,14 +6,9 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use api_types::requests::SetTopFriendsRequest;
|
use api_types::requests::SetTopFriendsRequest;
|
||||||
use api_types::responses::TopFriendsResponse;
|
use api_types::responses::TopFriendsResponse;
|
||||||
use crate::handlers::auth::to_user_response;
|
|
||||||
use application::use_cases::profile::{get_top_friends, get_user_by_username, set_top_friends};
|
use application::use_cases::profile::{get_top_friends, get_user_by_username, set_top_friends};
|
||||||
use application::use_cases::social::*;
|
use application::use_cases::social::*;
|
||||||
use axum::{
|
use axum::{extract::Path, http::StatusCode, Json};
|
||||||
extract::Path,
|
|
||||||
http::StatusCode,
|
|
||||||
Json,
|
|
||||||
};
|
|
||||||
use domain::{
|
use domain::{
|
||||||
ports::{
|
ports::{
|
||||||
BlockRepository, BoostRepository, EventPublisher, FederationActionPort, FollowRepository,
|
BlockRepository, BoostRepository, EventPublisher, FederationActionPort, FollowRepository,
|
||||||
|
|||||||
@@ -9,18 +9,16 @@ use api_types::{
|
|||||||
responses::ErrorResponse,
|
responses::ErrorResponse,
|
||||||
};
|
};
|
||||||
use application::use_cases::thoughts::{
|
use application::use_cases::thoughts::{
|
||||||
create_thought, delete_thought, edit_thought, get_thread_views, get_thought_view,
|
create_thought, delete_thought, edit_thought, get_thought_view, get_thread_views,
|
||||||
CreateThoughtInput,
|
CreateThoughtInput,
|
||||||
};
|
};
|
||||||
use axum::{
|
use axum::{extract::Path, http::StatusCode, response::IntoResponse, Json};
|
||||||
extract::Path,
|
|
||||||
http::StatusCode,
|
|
||||||
response::IntoResponse,
|
|
||||||
Json,
|
|
||||||
};
|
|
||||||
use domain::{
|
use domain::{
|
||||||
models::feed::{EngagementStats, FeedEntry, ViewerContext},
|
models::feed::{EngagementStats, FeedEntry, ViewerContext},
|
||||||
ports::{EngagementRepository, EventPublisher, OutboxWriter, TagRepository, ThoughtRepository, UserRepository},
|
ports::{
|
||||||
|
EngagementRepository, EventPublisher, OutboxWriter, TagRepository, ThoughtRepository,
|
||||||
|
UserRepository,
|
||||||
|
},
|
||||||
value_objects::ThoughtId,
|
value_objects::ThoughtId,
|
||||||
};
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -74,8 +72,15 @@ pub async fn post_thought(
|
|||||||
let entry = FeedEntry {
|
let entry = FeedEntry {
|
||||||
thought: out.thought,
|
thought: out.thought,
|
||||||
author,
|
author,
|
||||||
stats: EngagementStats { like_count: 0, boost_count: 0, reply_count: 0 },
|
stats: EngagementStats {
|
||||||
viewer: Some(ViewerContext { liked: false, boosted: false }),
|
like_count: 0,
|
||||||
|
boost_count: 0,
|
||||||
|
reply_count: 0,
|
||||||
|
},
|
||||||
|
viewer: Some(ViewerContext {
|
||||||
|
liked: false,
|
||||||
|
boosted: false,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
Ok((StatusCode::CREATED, Json(to_thought_response(&entry))))
|
Ok((StatusCode::CREATED, Json(to_thought_response(&entry))))
|
||||||
}
|
}
|
||||||
@@ -101,7 +106,9 @@ pub async fn get_thought_handler(
|
|||||||
viewer.as_ref(),
|
viewer.as_ref(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(serde_json::to_value(to_thought_response(&entry)).unwrap()))
|
Ok(Json(
|
||||||
|
serde_json::to_value(to_thought_response(&entry)).unwrap(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -119,7 +126,14 @@ pub async fn delete_thought_handler(
|
|||||||
AuthUser(uid): AuthUser,
|
AuthUser(uid): AuthUser,
|
||||||
Path(id): Path<Uuid>,
|
Path(id): Path<Uuid>,
|
||||||
) -> Result<StatusCode, ApiError> {
|
) -> Result<StatusCode, ApiError> {
|
||||||
delete_thought(&*d.thoughts, &*d.events, &*d.outbox, &ThoughtId::from_uuid(id), &uid).await?;
|
delete_thought(
|
||||||
|
&*d.thoughts,
|
||||||
|
&*d.events,
|
||||||
|
&*d.outbox,
|
||||||
|
&ThoughtId::from_uuid(id),
|
||||||
|
&uid,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(StatusCode::NO_CONTENT)
|
Ok(StatusCode::NO_CONTENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -191,9 +191,7 @@ pub async fn get_users(
|
|||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_user_count(
|
pub async fn get_user_count(Deps(d): Deps<UsersDeps>) -> Result<Json<serde_json::Value>, ApiError> {
|
||||||
Deps(d): Deps<UsersDeps>,
|
|
||||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
|
||||||
let count = d.users.count().await?;
|
let count = d.users.count().await?;
|
||||||
Ok(Json(serde_json::json!({ "count": count })))
|
Ok(Json(serde_json::json!({ "count": count })))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,11 +79,7 @@ impl ActivityPubRepository for NoOpApRepo {
|
|||||||
) -> Result<ThoughtId, DomainError> {
|
) -> Result<ThoughtId, DomainError> {
|
||||||
Ok(ThoughtId::from_uuid(uuid::Uuid::new_v4()))
|
Ok(ThoughtId::from_uuid(uuid::Uuid::new_v4()))
|
||||||
}
|
}
|
||||||
async fn apply_note_update(
|
async fn apply_note_update(&self, _ap_id: &str, _new_content: &str) -> Result<(), DomainError> {
|
||||||
&self,
|
|
||||||
_ap_id: &str,
|
|
||||||
_new_content: &str,
|
|
||||||
) -> Result<(), DomainError> {
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
async fn retract_note(&self, _ap_id: &str) -> Result<(), DomainError> {
|
async fn retract_note(&self, _ap_id: &str) -> Result<(), DomainError> {
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use activitypub::ThoughtsObjectHandler;
|
use activitypub::ThoughtsObjectHandler;
|
||||||
use activitypub_base::ActivityPubService;
|
use activitypub_base::ActivityPubService;
|
||||||
use application::services::{FederationEventService, NotificationEventService};
|
|
||||||
use activitypub_base::{ActivityPubRepository, OutboundFederationPort};
|
use activitypub_base::{ActivityPubRepository, OutboundFederationPort};
|
||||||
|
use application::services::{FederationEventService, NotificationEventService};
|
||||||
use domain::ports::EventPublisher;
|
use domain::ports::EventPublisher;
|
||||||
use postgres::activitypub::PgActivityPubRepository;
|
use postgres::activitypub::PgActivityPubRepository;
|
||||||
use postgres_federation::{PostgresApUserRepository, PostgresFederationRepository};
|
use postgres_federation::{PostgresApUserRepository, PostgresFederationRepository};
|
||||||
|
|||||||
@@ -44,7 +44,11 @@ async fn main() {
|
|||||||
Ok(envelope) => {
|
Ok(envelope) => {
|
||||||
let event = &envelope.event;
|
let event = &envelope.event;
|
||||||
let event_type = event_payload::EventPayload::from(event).subject();
|
let event_type = event_payload::EventPayload::from(event).subject();
|
||||||
tracing::info!(event_type, delivery = envelope.delivery_count, "received event");
|
tracing::info!(
|
||||||
|
event_type,
|
||||||
|
delivery = envelope.delivery_count,
|
||||||
|
"received event"
|
||||||
|
);
|
||||||
|
|
||||||
let n = infra.handlers.notification.handle(event).await;
|
let n = infra.handlers.notification.handle(event).await;
|
||||||
let f = infra.handlers.federation.handle(event).await;
|
let f = infra.handlers.federation.handle(event).await;
|
||||||
|
|||||||
@@ -57,7 +57,11 @@ impl OutboxRelay {
|
|||||||
let payload: EventPayload = match serde_json::from_value(row.payload.clone()) {
|
let payload: EventPayload = match serde_json::from_value(row.payload.clone()) {
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!(seq = row.seq, event_type = row.event_type, "outbox: failed to deserialize payload: {e}");
|
tracing::error!(
|
||||||
|
seq = row.seq,
|
||||||
|
event_type = row.event_type,
|
||||||
|
"outbox: failed to deserialize payload: {e}"
|
||||||
|
);
|
||||||
// Mark delivered to avoid blocking; investigate manually.
|
// Mark delivered to avoid blocking; investigate manually.
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"UPDATE outbox_events \
|
"UPDATE outbox_events \
|
||||||
@@ -75,7 +79,10 @@ impl OutboxRelay {
|
|||||||
let domain_event = match DomainEvent::try_from(payload) {
|
let domain_event = match DomainEvent::try_from(payload) {
|
||||||
Ok(ev) => ev,
|
Ok(ev) => ev,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!(seq = row.seq, "outbox: failed to convert to DomainEvent: {e}");
|
tracing::error!(
|
||||||
|
seq = row.seq,
|
||||||
|
"outbox: failed to convert to DomainEvent: {e}"
|
||||||
|
);
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"UPDATE outbox_events \
|
"UPDATE outbox_events \
|
||||||
SET delivered = true, delivered_at = now() \
|
SET delivered = true, delivered_at = now() \
|
||||||
@@ -100,7 +107,11 @@ impl OutboxRelay {
|
|||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await?;
|
.await?;
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
tracing::info!(seq = row.seq, event_type = row.event_type, "outbox: delivered");
|
tracing::info!(
|
||||||
|
seq = row.seq,
|
||||||
|
event_type = row.event_type,
|
||||||
|
"outbox: delivered"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!(seq = row.seq, "outbox: publish failed (will retry): {e}");
|
tracing::warn!(seq = row.seq, "outbox: publish failed (will retry): {e}");
|
||||||
|
|||||||
Reference in New Issue
Block a user