refactor: replace long arg lists with input/config structs and builder
- Thought::new_local → NewThought struct (7 args → 1) - UserWriter::update_profile → UpdateProfileInput struct (6 args → 2) - update_profile use case → UpdateProfileInput (8 args → 3) - ActivityPubService::new → builder pattern (9 args → 5 required + 4 optional setters) - accept_note → AcceptNoteInput struct (8 args → 1) - ThoughtNote::new_public → ThoughtNoteInput struct (8 args → 1) Remove all #[allow(clippy::too_many_arguments)] annotations.
This commit is contained in:
@@ -5,6 +5,17 @@ use domain::{
|
||||
value_objects::{ThoughtId, UserId, Username},
|
||||
};
|
||||
|
||||
pub struct AcceptNoteInput<'a> {
|
||||
pub ap_id: &'a str,
|
||||
pub author_id: &'a UserId,
|
||||
pub content: &'a str,
|
||||
pub published: chrono::DateTime<chrono::Utc>,
|
||||
pub sensitive: bool,
|
||||
pub content_warning: Option<String>,
|
||||
pub visibility: &'a str,
|
||||
pub in_reply_to: Option<&'a str>,
|
||||
}
|
||||
|
||||
/// AP-protocol endpoints for a locally-stored user (local or interned remote).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ActorApUrls {
|
||||
@@ -61,18 +72,8 @@ pub trait ActivityPubRepository: Send + Sync {
|
||||
// ── Inbox processing (remote → local) ───────────────────────────
|
||||
|
||||
/// Persist an incoming remote Note. Idempotent on ap_id.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn accept_note(
|
||||
&self,
|
||||
ap_id: &str,
|
||||
author_id: &UserId,
|
||||
content: &str,
|
||||
published: chrono::DateTime<chrono::Utc>,
|
||||
sensitive: bool,
|
||||
content_warning: Option<String>,
|
||||
visibility: &str,
|
||||
in_reply_to: Option<&str>,
|
||||
) -> Result<ThoughtId, DomainError>;
|
||||
|
||||
async fn accept_note(&self, input: AcceptNoteInput<'_>) -> Result<ThoughtId, DomainError>;
|
||||
|
||||
/// Apply an Update to a previously accepted remote Note.
|
||||
async fn apply_note_update(&self, ap_id: &str, new_content: &str) -> Result<(), DomainError>;
|
||||
|
||||
@@ -18,7 +18,9 @@ pub mod user;
|
||||
pub mod webfinger;
|
||||
|
||||
pub use activitypub_federation::kinds::object::NoteType;
|
||||
pub use ap_ports::{ActivityPubRepository, ActorApUrls, OutboundFederationPort, OutboxEntry};
|
||||
pub use ap_ports::{
|
||||
AcceptNoteInput, ActivityPubRepository, ActorApUrls, OutboundFederationPort, OutboxEntry,
|
||||
};
|
||||
pub use content::ApObjectHandler;
|
||||
pub use data::FederationData;
|
||||
pub use error::Error;
|
||||
|
||||
@@ -163,34 +163,73 @@ pub struct ActivityPubService {
|
||||
connections_repo: Arc<dyn domain::ports::RemoteActorConnectionRepository>,
|
||||
}
|
||||
|
||||
pub struct ActivityPubServiceBuilder {
|
||||
repo: Arc<dyn FederationRepository>,
|
||||
user_repo: Arc<dyn ApUserRepository>,
|
||||
object_handler: Arc<dyn ApObjectHandler>,
|
||||
base_url: String,
|
||||
connections_repo: Arc<dyn domain::ports::RemoteActorConnectionRepository>,
|
||||
allow_registration: bool,
|
||||
software_name: String,
|
||||
debug: bool,
|
||||
event_publisher: Option<Arc<dyn domain::ports::EventPublisher>>,
|
||||
}
|
||||
|
||||
impl ActivityPubServiceBuilder {
|
||||
pub fn allow_registration(mut self, v: bool) -> Self {
|
||||
self.allow_registration = v;
|
||||
self
|
||||
}
|
||||
pub fn software_name(mut self, v: impl Into<String>) -> Self {
|
||||
self.software_name = v.into();
|
||||
self
|
||||
}
|
||||
pub fn debug(mut self, v: bool) -> Self {
|
||||
self.debug = v;
|
||||
self
|
||||
}
|
||||
pub fn event_publisher(mut self, v: Arc<dyn domain::ports::EventPublisher>) -> Self {
|
||||
self.event_publisher = Some(v);
|
||||
self
|
||||
}
|
||||
pub async fn build(self) -> anyhow::Result<ActivityPubService> {
|
||||
let data = FederationData::new(
|
||||
self.repo,
|
||||
self.user_repo,
|
||||
self.object_handler,
|
||||
self.base_url.clone(),
|
||||
self.allow_registration,
|
||||
self.software_name,
|
||||
self.event_publisher,
|
||||
);
|
||||
let federation_config = ApFederationConfig::new(data, self.debug).await?;
|
||||
Ok(ActivityPubService {
|
||||
federation_config,
|
||||
base_url: self.base_url,
|
||||
connections_repo: self.connections_repo,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ActivityPubService {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn new(
|
||||
pub fn builder(
|
||||
repo: Arc<dyn FederationRepository>,
|
||||
user_repo: Arc<dyn ApUserRepository>,
|
||||
object_handler: Arc<dyn ApObjectHandler>,
|
||||
base_url: String,
|
||||
allow_registration: bool,
|
||||
software_name: String,
|
||||
debug: bool,
|
||||
event_publisher: Option<Arc<dyn domain::ports::EventPublisher>>,
|
||||
base_url: impl Into<String>,
|
||||
connections_repo: Arc<dyn domain::ports::RemoteActorConnectionRepository>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let data = FederationData::new(
|
||||
) -> ActivityPubServiceBuilder {
|
||||
ActivityPubServiceBuilder {
|
||||
repo,
|
||||
user_repo,
|
||||
object_handler,
|
||||
base_url.clone(),
|
||||
allow_registration,
|
||||
software_name,
|
||||
event_publisher,
|
||||
);
|
||||
let federation_config = ApFederationConfig::new(data, debug).await?;
|
||||
Ok(Self {
|
||||
federation_config,
|
||||
base_url,
|
||||
base_url: base_url.into(),
|
||||
connections_repo,
|
||||
})
|
||||
allow_registration: false,
|
||||
software_name: String::new(),
|
||||
debug: false,
|
||||
event_publisher: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn federation_config(&self) -> &ApFederationConfig {
|
||||
|
||||
@@ -7,9 +7,9 @@ use chrono::{DateTime, Utc};
|
||||
use std::sync::Arc;
|
||||
use url::Url;
|
||||
|
||||
use crate::note::ThoughtNote;
|
||||
use crate::note::{ThoughtNote, ThoughtNoteInput};
|
||||
use crate::urls::ThoughtsUrls;
|
||||
use activitypub_base::{ActivityPubRepository, ApObjectHandler};
|
||||
use activitypub_base::{AcceptNoteInput, ActivityPubRepository, ApObjectHandler};
|
||||
use domain::ports::{EventPublisher, TagRepository};
|
||||
use domain::value_objects::UserId;
|
||||
|
||||
@@ -58,16 +58,16 @@ impl ApObjectHandler for ThoughtsObjectHandler {
|
||||
.thought
|
||||
.in_reply_to_id
|
||||
.map(|id| self.urls.thought_url(id.as_uuid()));
|
||||
let note = ThoughtNote::new_public(
|
||||
note_url.clone(),
|
||||
let note = ThoughtNote::new_public(ThoughtNoteInput {
|
||||
id: note_url.clone(),
|
||||
actor_url,
|
||||
e.thought.content.as_str().to_owned(),
|
||||
e.thought.created_at,
|
||||
content: e.thought.content.as_str().to_owned(),
|
||||
published: e.thought.created_at,
|
||||
in_reply_to,
|
||||
e.thought.sensitive,
|
||||
e.thought.content_warning,
|
||||
followers,
|
||||
);
|
||||
sensitive: e.thought.sensitive,
|
||||
summary: e.thought.content_warning,
|
||||
followers_url: followers,
|
||||
});
|
||||
Ok((note_url, serde_json::to_value(¬e)?))
|
||||
})
|
||||
.collect()
|
||||
@@ -96,16 +96,16 @@ impl ApObjectHandler for ThoughtsObjectHandler {
|
||||
.thought
|
||||
.in_reply_to_id
|
||||
.map(|id| self.urls.thought_url(id.as_uuid()));
|
||||
let note = ThoughtNote::new_public(
|
||||
note_url.clone(),
|
||||
let note = ThoughtNote::new_public(ThoughtNoteInput {
|
||||
id: note_url.clone(),
|
||||
actor_url,
|
||||
e.thought.content.as_str().to_owned(),
|
||||
created_at,
|
||||
content: e.thought.content.as_str().to_owned(),
|
||||
published: created_at,
|
||||
in_reply_to,
|
||||
e.thought.sensitive,
|
||||
e.thought.content_warning,
|
||||
followers,
|
||||
);
|
||||
sensitive: e.thought.sensitive,
|
||||
summary: e.thought.content_warning,
|
||||
followers_url: followers,
|
||||
});
|
||||
Ok((note_url, serde_json::to_value(¬e)?, created_at))
|
||||
})
|
||||
.collect()
|
||||
@@ -143,16 +143,16 @@ impl ApObjectHandler for ThoughtsObjectHandler {
|
||||
|
||||
let thought_id = self
|
||||
.repo
|
||||
.accept_note(
|
||||
ap_id.as_str(),
|
||||
&author_id,
|
||||
¬e.content,
|
||||
note.published,
|
||||
note.sensitive,
|
||||
note.summary,
|
||||
.accept_note(AcceptNoteInput {
|
||||
ap_id: ap_id.as_str(),
|
||||
author_id: &author_id,
|
||||
content: ¬e.content,
|
||||
published: note.published,
|
||||
sensitive: note.sensitive,
|
||||
content_warning: note.summary,
|
||||
visibility,
|
||||
note.in_reply_to.as_ref().map(|u| u.as_str()),
|
||||
)
|
||||
in_reply_to: note.in_reply_to.as_ref().map(|u| u.as_str()),
|
||||
})
|
||||
.await
|
||||
.map_err(|e| anyhow!("{e}"))?;
|
||||
|
||||
|
||||
@@ -30,30 +30,31 @@ pub struct ThoughtNote {
|
||||
pub tag: Vec<serde_json::Value>,
|
||||
}
|
||||
|
||||
pub struct ThoughtNoteInput {
|
||||
pub id: Url,
|
||||
pub actor_url: Url,
|
||||
pub content: String,
|
||||
pub published: DateTime<Utc>,
|
||||
pub in_reply_to: Option<Url>,
|
||||
pub sensitive: bool,
|
||||
pub summary: Option<String>,
|
||||
pub followers_url: Url,
|
||||
}
|
||||
|
||||
impl ThoughtNote {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new_public(
|
||||
id: Url,
|
||||
actor_url: Url,
|
||||
content: String,
|
||||
published: DateTime<Utc>,
|
||||
in_reply_to: Option<Url>,
|
||||
sensitive: bool,
|
||||
summary: Option<String>,
|
||||
followers_url: Url,
|
||||
) -> Self {
|
||||
pub fn new_public(p: ThoughtNoteInput) -> Self {
|
||||
Self {
|
||||
kind: Default::default(),
|
||||
url: Some(id.clone()),
|
||||
id,
|
||||
attributed_to: actor_url,
|
||||
content,
|
||||
published,
|
||||
url: Some(p.id.clone()),
|
||||
id: p.id,
|
||||
attributed_to: p.actor_url,
|
||||
content: p.content,
|
||||
published: p.published,
|
||||
to: vec![AS_PUBLIC.to_string()],
|
||||
cc: vec![followers_url.to_string()],
|
||||
in_reply_to,
|
||||
sensitive,
|
||||
summary,
|
||||
cc: vec![p.followers_url.to_string()],
|
||||
in_reply_to: p.in_reply_to,
|
||||
sensitive: p.sensitive,
|
||||
summary: p.summary,
|
||||
tag: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,16 @@ use super::*;
|
||||
|
||||
#[test]
|
||||
fn note_serializes_with_public_audience() {
|
||||
let note = ThoughtNote::new_public(
|
||||
"https://example.com/thoughts/1".parse().unwrap(),
|
||||
"https://example.com/users/alice".parse().unwrap(),
|
||||
"Hello world".to_string(),
|
||||
chrono::Utc::now(),
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
"https://example.com/users/alice/followers".parse().unwrap(),
|
||||
);
|
||||
let note = ThoughtNote::new_public(super::ThoughtNoteInput {
|
||||
id: "https://example.com/thoughts/1".parse().unwrap(),
|
||||
actor_url: "https://example.com/users/alice".parse().unwrap(),
|
||||
content: "Hello world".to_string(),
|
||||
published: chrono::Utc::now(),
|
||||
in_reply_to: None,
|
||||
sensitive: false,
|
||||
summary: None,
|
||||
followers_url: "https://example.com/users/alice/followers".parse().unwrap(),
|
||||
});
|
||||
let json = serde_json::to_string(¬e).unwrap();
|
||||
assert!(json.contains(AS_PUBLIC));
|
||||
assert!(json.contains("Hello world"));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::*;
|
||||
use domain::{
|
||||
models::{
|
||||
thought::{Thought, Visibility},
|
||||
thought::{NewThought, Thought, Visibility},
|
||||
user::User,
|
||||
},
|
||||
ports::{SearchPort, ThoughtRepository, UserWriter},
|
||||
@@ -19,15 +19,15 @@ async fn seed_thought(pool: &sqlx::PgPool, username: &str, content: &str) -> (Us
|
||||
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,
|
||||
);
|
||||
let t = Thought::new_local(NewThought {
|
||||
id: ThoughtId::new(),
|
||||
user_id: u.id.clone(),
|
||||
content: Content::new_local(content).unwrap(),
|
||||
in_reply_to_id: None,
|
||||
visibility: Visibility::Public,
|
||||
content_warning: None,
|
||||
sensitive: false,
|
||||
});
|
||||
trepo.save(&t).await.unwrap();
|
||||
(u, t)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ const THOUGHTS_PATH_PREFIX: &str = "/thoughts/";
|
||||
use chrono::{DateTime, Utc};
|
||||
use sqlx::PgPool;
|
||||
|
||||
use activitypub_base::{ActivityPubRepository, ActorApUrls, OutboxEntry};
|
||||
use activitypub_base::{AcceptNoteInput, ActivityPubRepository, ActorApUrls, OutboxEntry};
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
models::thought::{Thought, Visibility},
|
||||
@@ -210,17 +210,17 @@ impl ActivityPubRepository for PgActivityPubRepository {
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
async fn accept_note(
|
||||
&self,
|
||||
ap_id: &str,
|
||||
author_id: &UserId,
|
||||
content: &str,
|
||||
published: DateTime<Utc>,
|
||||
sensitive: bool,
|
||||
content_warning: Option<String>,
|
||||
visibility: &str,
|
||||
in_reply_to: Option<&str>,
|
||||
) -> Result<ThoughtId, DomainError> {
|
||||
async fn accept_note(&self, input: AcceptNoteInput<'_>) -> Result<ThoughtId, DomainError> {
|
||||
let AcceptNoteInput {
|
||||
ap_id,
|
||||
author_id,
|
||||
content,
|
||||
published,
|
||||
sensitive,
|
||||
content_warning,
|
||||
visibility,
|
||||
in_reply_to,
|
||||
} = input;
|
||||
let capped: String = content.chars().take(MAX_REMOTE_CONTENT_CHARS).collect();
|
||||
let (in_reply_to_id, in_reply_to_url) = match in_reply_to {
|
||||
Some(url) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::*;
|
||||
use activitypub_base::ActivityPubRepository;
|
||||
use activitypub_base::{AcceptNoteInput, ActivityPubRepository};
|
||||
|
||||
#[sqlx::test(migrations = "./migrations")]
|
||||
async fn intern_remote_actor_is_idempotent(pool: sqlx::PgPool) {
|
||||
@@ -16,16 +16,16 @@ async fn accept_and_retract_note(pool: sqlx::PgPool) {
|
||||
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(
|
||||
repo.accept_note(AcceptNoteInput {
|
||||
ap_id,
|
||||
&author,
|
||||
"hello from remote",
|
||||
chrono::Utc::now(),
|
||||
false,
|
||||
None,
|
||||
"public",
|
||||
None,
|
||||
)
|
||||
author_id: &author,
|
||||
content: "hello from remote",
|
||||
published: chrono::Utc::now(),
|
||||
sensitive: false,
|
||||
content_warning: None,
|
||||
visibility: "public",
|
||||
in_reply_to: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
repo.retract_note(ap_id).await.unwrap();
|
||||
@@ -46,16 +46,16 @@ async fn accept_note_returns_thought_id(pool: sqlx::PgPool) {
|
||||
.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,
|
||||
)
|
||||
.accept_note(AcceptNoteInput {
|
||||
ap_id: "https://remote.example/notes/1",
|
||||
author_id: &actor_user_id,
|
||||
content: "Hello #rust world",
|
||||
published: chrono::Utc::now(),
|
||||
sensitive: false,
|
||||
content_warning: None,
|
||||
visibility: "public",
|
||||
in_reply_to: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{thought::PgThoughtRepository, user::PgUserRepository};
|
||||
use domain::{
|
||||
models::{
|
||||
feed::PageParams,
|
||||
thought::{Thought, Visibility},
|
||||
thought::{NewThought, Thought, Visibility},
|
||||
user::User,
|
||||
},
|
||||
ports::{FeedQuery, ThoughtRepository, UserWriter},
|
||||
@@ -20,15 +20,15 @@ async fn seed(pool: &sqlx::PgPool, username: &str, content: &str) -> (User, Thou
|
||||
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,
|
||||
);
|
||||
let t = Thought::new_local(NewThought {
|
||||
id: ThoughtId::new(),
|
||||
user_id: u.id.clone(),
|
||||
content: Content::new_local(content).unwrap(),
|
||||
in_reply_to_id: None,
|
||||
visibility: Visibility::Public,
|
||||
content_warning: None,
|
||||
sensitive: false,
|
||||
});
|
||||
trepo.save(&t).await.unwrap();
|
||||
(u, t)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{thought::PgThoughtRepository, user::PgUserRepository};
|
||||
use domain::ports::{ThoughtRepository, UserWriter};
|
||||
use domain::{
|
||||
models::{
|
||||
thought::{Thought, Visibility},
|
||||
thought::{NewThought, Thought, Visibility},
|
||||
user::User,
|
||||
},
|
||||
value_objects::*,
|
||||
@@ -29,15 +29,15 @@ async fn attach_and_list(pool: sqlx::PgPool) {
|
||||
PasswordHash("h".into()),
|
||||
);
|
||||
urepo.save(&u).await.unwrap();
|
||||
let t = Thought::new_local(
|
||||
ThoughtId::new(),
|
||||
u.id.clone(),
|
||||
Content::new_local("hi").unwrap(),
|
||||
None,
|
||||
Visibility::Public,
|
||||
None,
|
||||
false,
|
||||
);
|
||||
let t = Thought::new_local(NewThought {
|
||||
id: ThoughtId::new(),
|
||||
user_id: u.id.clone(),
|
||||
content: Content::new_local("hi").unwrap(),
|
||||
in_reply_to_id: None,
|
||||
visibility: Visibility::Public,
|
||||
content_warning: None,
|
||||
sensitive: false,
|
||||
});
|
||||
trepo.save(&t).await.unwrap();
|
||||
let repo = PgTagRepository::new(pool);
|
||||
let tag = repo.find_or_create("greetings").await.unwrap();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{thought::PgThoughtRepository, user::PgUserRepository};
|
||||
use domain::{
|
||||
models::{
|
||||
thought::{Thought, Visibility},
|
||||
thought::{NewThought, Thought, Visibility},
|
||||
user::User,
|
||||
},
|
||||
ports::{ThoughtRepository, UserWriter},
|
||||
@@ -23,15 +23,15 @@ pub async fn seed_user(pool: &sqlx::PgPool, username: &str, email: &str) -> User
|
||||
pub async fn seed_user_and_thought(pool: &sqlx::PgPool) -> (User, Thought) {
|
||||
let user = seed_user(pool, "alice", "alice@ex.com").await;
|
||||
let trepo = PgThoughtRepository::new(pool.clone());
|
||||
let t = Thought::new_local(
|
||||
ThoughtId::new(),
|
||||
user.id.clone(),
|
||||
Content::new_local("hi").unwrap(),
|
||||
None,
|
||||
Visibility::Public,
|
||||
None,
|
||||
false,
|
||||
);
|
||||
let t = Thought::new_local(NewThought {
|
||||
id: ThoughtId::new(),
|
||||
user_id: user.id.clone(),
|
||||
content: Content::new_local("hi").unwrap(),
|
||||
in_reply_to_id: None,
|
||||
visibility: Visibility::Public,
|
||||
content_warning: None,
|
||||
sensitive: false,
|
||||
});
|
||||
trepo.save(&t).await.unwrap();
|
||||
(user, t)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::*;
|
||||
use crate::test_helpers::seed_user;
|
||||
use domain::{
|
||||
models::thought::{Thought, Visibility},
|
||||
models::thought::{NewThought, Thought, Visibility},
|
||||
value_objects::*,
|
||||
};
|
||||
|
||||
@@ -9,15 +9,15 @@ use domain::{
|
||||
async fn save_and_find_thought(pool: sqlx::PgPool) {
|
||||
let user = seed_user(&pool, "alice", "alice@ex.com").await;
|
||||
let repo = PgThoughtRepository::new(pool);
|
||||
let t = Thought::new_local(
|
||||
ThoughtId::new(),
|
||||
user.id.clone(),
|
||||
Content::new_local("hello world").unwrap(),
|
||||
None,
|
||||
Visibility::Public,
|
||||
None,
|
||||
false,
|
||||
);
|
||||
let t = Thought::new_local(NewThought {
|
||||
id: ThoughtId::new(),
|
||||
user_id: user.id.clone(),
|
||||
content: Content::new_local("hello world").unwrap(),
|
||||
in_reply_to_id: None,
|
||||
visibility: Visibility::Public,
|
||||
content_warning: None,
|
||||
sensitive: 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");
|
||||
@@ -28,15 +28,15 @@ async fn save_and_find_thought(pool: sqlx::PgPool) {
|
||||
async fn delete_thought(pool: sqlx::PgPool) {
|
||||
let user = seed_user(&pool, "bob", "bob@ex.com").await;
|
||||
let repo = PgThoughtRepository::new(pool);
|
||||
let t = Thought::new_local(
|
||||
ThoughtId::new(),
|
||||
user.id.clone(),
|
||||
Content::new_local("bye").unwrap(),
|
||||
None,
|
||||
Visibility::Public,
|
||||
None,
|
||||
false,
|
||||
);
|
||||
let t = Thought::new_local(NewThought {
|
||||
id: ThoughtId::new(),
|
||||
user_id: user.id.clone(),
|
||||
content: Content::new_local("bye").unwrap(),
|
||||
in_reply_to_id: None,
|
||||
visibility: Visibility::Public,
|
||||
content_warning: None,
|
||||
sensitive: false,
|
||||
});
|
||||
repo.save(&t).await.unwrap();
|
||||
repo.delete(&t.id, &user.id).await.unwrap();
|
||||
assert!(repo.find_by_id(&t.id).await.unwrap().is_none());
|
||||
@@ -47,15 +47,15 @@ async fn delete_wrong_owner_returns_not_found(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 = PgThoughtRepository::new(pool);
|
||||
let t = Thought::new_local(
|
||||
ThoughtId::new(),
|
||||
alice.id.clone(),
|
||||
Content::new_local("secret").unwrap(),
|
||||
None,
|
||||
Visibility::Public,
|
||||
None,
|
||||
false,
|
||||
);
|
||||
let t = Thought::new_local(NewThought {
|
||||
id: ThoughtId::new(),
|
||||
user_id: alice.id.clone(),
|
||||
content: Content::new_local("secret").unwrap(),
|
||||
in_reply_to_id: None,
|
||||
visibility: Visibility::Public,
|
||||
content_warning: None,
|
||||
sensitive: false,
|
||||
});
|
||||
repo.save(&t).await.unwrap();
|
||||
let err = repo.delete(&t.id, &bob.id).await.unwrap_err();
|
||||
assert!(matches!(err, DomainError::NotFound));
|
||||
@@ -65,24 +65,24 @@ async fn delete_wrong_owner_returns_not_found(pool: sqlx::PgPool) {
|
||||
async fn get_thread_returns_root_and_replies(pool: sqlx::PgPool) {
|
||||
let user = seed_user(&pool, "charlie", "charlie@ex.com").await;
|
||||
let repo = PgThoughtRepository::new(pool);
|
||||
let root = Thought::new_local(
|
||||
ThoughtId::new(),
|
||||
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,
|
||||
);
|
||||
let root = Thought::new_local(NewThought {
|
||||
id: ThoughtId::new(),
|
||||
user_id: user.id.clone(),
|
||||
content: Content::new_local("root").unwrap(),
|
||||
in_reply_to_id: None,
|
||||
visibility: Visibility::Public,
|
||||
content_warning: None,
|
||||
sensitive: false,
|
||||
});
|
||||
let reply = Thought::new_local(NewThought {
|
||||
id: ThoughtId::new(),
|
||||
user_id: user.id.clone(),
|
||||
content: Content::new_local("reply").unwrap(),
|
||||
in_reply_to_id: Some(root.id.clone()),
|
||||
visibility: Visibility::Public,
|
||||
content_warning: None,
|
||||
sensitive: false,
|
||||
});
|
||||
repo.save(&root).await.unwrap();
|
||||
repo.save(&reply).await.unwrap();
|
||||
let thread = repo.get_thread(&root.id).await.unwrap();
|
||||
|
||||
@@ -4,7 +4,7 @@ use chrono::{DateTime, Utc};
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
models::feed::{PageParams, Paginated, UserSummary},
|
||||
models::user::User,
|
||||
models::user::{UpdateProfileInput, User},
|
||||
ports::{UserReader, UserWriter},
|
||||
value_objects::{Email, PasswordHash, UserId, Username},
|
||||
};
|
||||
@@ -265,21 +265,17 @@ impl UserWriter for PgUserRepository {
|
||||
async fn update_profile(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
display_name: Option<String>,
|
||||
bio: Option<String>,
|
||||
avatar_url: Option<String>,
|
||||
header_url: Option<String>,
|
||||
custom_css: Option<String>,
|
||||
input: UpdateProfileInput,
|
||||
) -> Result<(), DomainError> {
|
||||
sqlx::query(
|
||||
"UPDATE users SET display_name=$2,bio=$3,avatar_url=$4,header_url=$5,custom_css=$6,updated_at=NOW() WHERE id=$1"
|
||||
)
|
||||
.bind(user_id.as_uuid())
|
||||
.bind(display_name)
|
||||
.bind(bio)
|
||||
.bind(avatar_url)
|
||||
.bind(header_url)
|
||||
.bind(custom_css)
|
||||
.bind(input.display_name)
|
||||
.bind(input.bio)
|
||||
.bind(input.avatar_url)
|
||||
.bind(input.header_url)
|
||||
.bind(input.custom_css)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.into_domain()
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use super::*;
|
||||
use domain::{models::user::User, value_objects::*};
|
||||
use domain::{
|
||||
models::user::{UpdateProfileInput, User},
|
||||
value_objects::*,
|
||||
};
|
||||
|
||||
#[sqlx::test(migrations = "./migrations")]
|
||||
async fn save_and_find_by_id(pool: sqlx::PgPool) {
|
||||
@@ -55,11 +58,11 @@ async fn update_profile_changes_fields(pool: sqlx::PgPool) {
|
||||
repo.save(&user).await.unwrap();
|
||||
repo.update_profile(
|
||||
&user.id,
|
||||
Some("Charlie".into()),
|
||||
Some("bio".into()),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
UpdateProfileInput {
|
||||
display_name: Some("Charlie".into()),
|
||||
bio: Some("bio".into()),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -5,7 +5,7 @@ use async_trait::async_trait;
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
events::DomainEvent,
|
||||
models::thought::{Thought, Visibility},
|
||||
models::thought::{NewThought, Thought, Visibility},
|
||||
models::user::User,
|
||||
testing::TestStore,
|
||||
value_objects::*,
|
||||
@@ -92,15 +92,15 @@ fn alice() -> User {
|
||||
}
|
||||
|
||||
fn local_thought(author_id: UserId) -> Thought {
|
||||
Thought::new_local(
|
||||
ThoughtId::new(),
|
||||
author_id,
|
||||
Content::new_local("hello").unwrap(),
|
||||
None,
|
||||
Visibility::Public,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
Thought::new_local(NewThought {
|
||||
id: ThoughtId::new(),
|
||||
user_id: author_id,
|
||||
content: Content::new_local("hello").unwrap(),
|
||||
in_reply_to_id: None,
|
||||
visibility: Visibility::Public,
|
||||
content_warning: None,
|
||||
sensitive: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn svc(store: &TestStore, spy: Arc<SpyPort>) -> FederationEventService {
|
||||
@@ -275,15 +275,15 @@ async fn boost_of_remote_thought_announces_remote_ap_id() {
|
||||
async fn direct_thought_created_does_not_broadcast() {
|
||||
let store = TestStore::default();
|
||||
let alice = alice();
|
||||
let thought = Thought::new_local(
|
||||
ThoughtId::new(),
|
||||
alice.id.clone(),
|
||||
Content::new_local("private").unwrap(),
|
||||
None,
|
||||
Visibility::Direct,
|
||||
None,
|
||||
false,
|
||||
);
|
||||
let thought = Thought::new_local(NewThought {
|
||||
id: ThoughtId::new(),
|
||||
user_id: alice.id.clone(),
|
||||
content: Content::new_local("private").unwrap(),
|
||||
in_reply_to_id: None,
|
||||
visibility: Visibility::Direct,
|
||||
content_warning: None,
|
||||
sensitive: false,
|
||||
});
|
||||
store.users.lock().unwrap().push(alice.clone());
|
||||
store.thoughts.lock().unwrap().push(thought.clone());
|
||||
|
||||
@@ -304,15 +304,15 @@ async fn direct_thought_created_does_not_broadcast() {
|
||||
async fn followers_only_thought_does_not_broadcast_publicly() {
|
||||
let store = TestStore::default();
|
||||
let alice = alice();
|
||||
let thought = Thought::new_local(
|
||||
ThoughtId::new(),
|
||||
alice.id.clone(),
|
||||
Content::new_local("for followers").unwrap(),
|
||||
None,
|
||||
Visibility::Followers,
|
||||
None,
|
||||
false,
|
||||
);
|
||||
let thought = Thought::new_local(NewThought {
|
||||
id: ThoughtId::new(),
|
||||
user_id: alice.id.clone(),
|
||||
content: Content::new_local("for followers").unwrap(),
|
||||
in_reply_to_id: None,
|
||||
visibility: Visibility::Followers,
|
||||
content_warning: None,
|
||||
sensitive: false,
|
||||
});
|
||||
store.users.lock().unwrap().push(alice.clone());
|
||||
store.thoughts.lock().unwrap().push(thought.clone());
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::*;
|
||||
use domain::{
|
||||
models::{
|
||||
notification::NotificationKind,
|
||||
thought::{Thought, Visibility},
|
||||
thought::{NewThought, Thought, Visibility},
|
||||
user::User,
|
||||
},
|
||||
testing::TestStore,
|
||||
@@ -24,15 +24,15 @@ async fn like_creates_notification_for_thought_author() {
|
||||
let store = TestStore::default();
|
||||
let alice = alice();
|
||||
let bob_id = UserId::new();
|
||||
let thought = Thought::new_local(
|
||||
ThoughtId::new(),
|
||||
alice.id.clone(),
|
||||
Content::new_local("hello").unwrap(),
|
||||
None,
|
||||
Visibility::Public,
|
||||
None,
|
||||
false,
|
||||
);
|
||||
let thought = Thought::new_local(NewThought {
|
||||
id: ThoughtId::new(),
|
||||
user_id: alice.id.clone(),
|
||||
content: Content::new_local("hello").unwrap(),
|
||||
in_reply_to_id: None,
|
||||
visibility: Visibility::Public,
|
||||
content_warning: None,
|
||||
sensitive: false,
|
||||
});
|
||||
store.thoughts.lock().unwrap().push(thought.clone());
|
||||
let svc = NotificationEventService {
|
||||
thoughts: Arc::new(store.clone()),
|
||||
@@ -54,15 +54,15 @@ async fn like_creates_notification_for_thought_author() {
|
||||
async fn self_like_creates_no_notification() {
|
||||
let store = TestStore::default();
|
||||
let alice = alice();
|
||||
let thought = Thought::new_local(
|
||||
ThoughtId::new(),
|
||||
alice.id.clone(),
|
||||
Content::new_local("hello").unwrap(),
|
||||
None,
|
||||
Visibility::Public,
|
||||
None,
|
||||
false,
|
||||
);
|
||||
let thought = Thought::new_local(NewThought {
|
||||
id: ThoughtId::new(),
|
||||
user_id: alice.id.clone(),
|
||||
content: Content::new_local("hello").unwrap(),
|
||||
in_reply_to_id: None,
|
||||
visibility: Visibility::Public,
|
||||
content_warning: None,
|
||||
sensitive: false,
|
||||
});
|
||||
store.thoughts.lock().unwrap().push(thought.clone());
|
||||
let svc = NotificationEventService {
|
||||
thoughts: Arc::new(store.clone()),
|
||||
@@ -103,15 +103,15 @@ async fn reply_creates_notification_for_original_author() {
|
||||
let store = TestStore::default();
|
||||
let alice = alice();
|
||||
let bob_id = UserId::new();
|
||||
let original = Thought::new_local(
|
||||
ThoughtId::new(),
|
||||
alice.id.clone(),
|
||||
Content::new_local("original").unwrap(),
|
||||
None,
|
||||
Visibility::Public,
|
||||
None,
|
||||
false,
|
||||
);
|
||||
let original = Thought::new_local(NewThought {
|
||||
id: ThoughtId::new(),
|
||||
user_id: alice.id.clone(),
|
||||
content: Content::new_local("original").unwrap(),
|
||||
in_reply_to_id: None,
|
||||
visibility: Visibility::Public,
|
||||
content_warning: None,
|
||||
sensitive: false,
|
||||
});
|
||||
store.thoughts.lock().unwrap().push(original.clone());
|
||||
let svc = NotificationEventService {
|
||||
thoughts: Arc::new(store.clone()),
|
||||
@@ -133,15 +133,15 @@ async fn reply_creates_notification_for_original_author() {
|
||||
async fn self_reply_creates_no_notification() {
|
||||
let store = TestStore::default();
|
||||
let alice = alice();
|
||||
let original = Thought::new_local(
|
||||
ThoughtId::new(),
|
||||
alice.id.clone(),
|
||||
Content::new_local("original").unwrap(),
|
||||
None,
|
||||
Visibility::Public,
|
||||
None,
|
||||
false,
|
||||
);
|
||||
let original = Thought::new_local(NewThought {
|
||||
id: ThoughtId::new(),
|
||||
user_id: alice.id.clone(),
|
||||
content: Content::new_local("original").unwrap(),
|
||||
in_reply_to_id: None,
|
||||
visibility: Visibility::Public,
|
||||
content_warning: None,
|
||||
sensitive: false,
|
||||
});
|
||||
store.thoughts.lock().unwrap().push(original.clone());
|
||||
let svc = NotificationEventService {
|
||||
thoughts: Arc::new(store.clone()),
|
||||
@@ -161,15 +161,15 @@ async fn self_reply_creates_no_notification() {
|
||||
async fn self_boost_creates_no_notification() {
|
||||
let store = TestStore::default();
|
||||
let alice = alice();
|
||||
let thought = Thought::new_local(
|
||||
ThoughtId::new(),
|
||||
alice.id.clone(),
|
||||
Content::new_local("hello").unwrap(),
|
||||
None,
|
||||
Visibility::Public,
|
||||
None,
|
||||
false,
|
||||
);
|
||||
let thought = Thought::new_local(NewThought {
|
||||
id: ThoughtId::new(),
|
||||
user_id: alice.id.clone(),
|
||||
content: Content::new_local("hello").unwrap(),
|
||||
in_reply_to_id: None,
|
||||
visibility: Visibility::Public,
|
||||
content_warning: None,
|
||||
sensitive: false,
|
||||
});
|
||||
store.thoughts.lock().unwrap().push(thought.clone());
|
||||
let svc = NotificationEventService {
|
||||
thoughts: Arc::new(store.clone()),
|
||||
|
||||
@@ -95,14 +95,7 @@ impl ActivityPubRepository for TestApRepo {
|
||||
}
|
||||
async fn accept_note(
|
||||
&self,
|
||||
_ap_id: &str,
|
||||
_author_id: &UserId,
|
||||
_content: &str,
|
||||
_published: chrono::DateTime<chrono::Utc>,
|
||||
_sensitive: bool,
|
||||
_content_warning: Option<String>,
|
||||
_visibility: &str,
|
||||
_in_reply_to: Option<&str>,
|
||||
_input: activitypub_base::AcceptNoteInput<'_>,
|
||||
) -> Result<ThoughtId, DomainError> {
|
||||
Ok(ThoughtId::from_uuid(uuid::Uuid::new_v4()))
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use domain::{
|
||||
events::DomainEvent,
|
||||
models::{
|
||||
feed::{PageParams, Paginated, UserSummary},
|
||||
user::User,
|
||||
user::{UpdateProfileInput, User},
|
||||
},
|
||||
ports::{AuthService, GeneratedToken, PasswordHasher, UserReader, UserWriter},
|
||||
testing::{NoOpEventPublisher, TestStore},
|
||||
@@ -56,22 +56,9 @@ impl UserWriter for ConflictOnSaveStore {
|
||||
async fn update_profile(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
display_name: Option<String>,
|
||||
bio: Option<String>,
|
||||
avatar_url: Option<String>,
|
||||
header_url: Option<String>,
|
||||
custom_css: Option<String>,
|
||||
input: UpdateProfileInput,
|
||||
) -> Result<(), DomainError> {
|
||||
self.0
|
||||
.update_profile(
|
||||
user_id,
|
||||
display_name,
|
||||
bio,
|
||||
avatar_url,
|
||||
header_url,
|
||||
custom_css,
|
||||
)
|
||||
.await
|
||||
self.0.update_profile(user_id, input).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,22 +101,9 @@ impl UserWriter for EmailConflictOnSaveStore {
|
||||
async fn update_profile(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
display_name: Option<String>,
|
||||
bio: Option<String>,
|
||||
avatar_url: Option<String>,
|
||||
header_url: Option<String>,
|
||||
custom_css: Option<String>,
|
||||
input: UpdateProfileInput,
|
||||
) -> Result<(), DomainError> {
|
||||
self.0
|
||||
.update_profile(
|
||||
user_id,
|
||||
display_name,
|
||||
bio,
|
||||
avatar_url,
|
||||
header_url,
|
||||
custom_css,
|
||||
)
|
||||
.await
|
||||
self.0.update_profile(user_id, input).await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,10 @@ const MAX_TOP_FRIENDS: usize = 8;
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
events::DomainEvent,
|
||||
models::{top_friend::TopFriend, user::User},
|
||||
models::{
|
||||
top_friend::TopFriend,
|
||||
user::{UpdateProfileInput, User},
|
||||
},
|
||||
ports::{EventPublisher, TopFriendRepository, UserReader, UserWriter},
|
||||
value_objects::{UserId, Username},
|
||||
};
|
||||
@@ -41,27 +44,13 @@ pub async fn get_user_by_id_or_username(
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn update_profile(
|
||||
users: &dyn UserWriter,
|
||||
events: &dyn EventPublisher,
|
||||
user_id: &UserId,
|
||||
display_name: Option<String>,
|
||||
bio: Option<String>,
|
||||
avatar_url: Option<String>,
|
||||
header_url: Option<String>,
|
||||
custom_css: Option<String>,
|
||||
input: UpdateProfileInput,
|
||||
) -> Result<(), DomainError> {
|
||||
users
|
||||
.update_profile(
|
||||
user_id,
|
||||
display_name,
|
||||
bio,
|
||||
avatar_url,
|
||||
header_url,
|
||||
custom_css,
|
||||
)
|
||||
.await?;
|
||||
users.update_profile(user_id, input).await?;
|
||||
events
|
||||
.publish(&DomainEvent::ProfileUpdated {
|
||||
user_id: user_id.clone(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::*;
|
||||
use domain::{
|
||||
models::{
|
||||
thought::{Thought, Visibility},
|
||||
thought::{NewThought, Thought, Visibility},
|
||||
user::User,
|
||||
},
|
||||
testing::TestStore,
|
||||
@@ -22,15 +22,19 @@ async fn like_and_unlike() {
|
||||
let store = TestStore::default();
|
||||
let alice = user("alice");
|
||||
let tid = ThoughtId::new();
|
||||
store.thoughts.lock().unwrap().push(Thought::new_local(
|
||||
tid.clone(),
|
||||
alice.id.clone(),
|
||||
Content::new_local("hi").unwrap(),
|
||||
None,
|
||||
Visibility::Public,
|
||||
None,
|
||||
false,
|
||||
));
|
||||
store
|
||||
.thoughts
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(Thought::new_local(NewThought {
|
||||
id: tid.clone(),
|
||||
user_id: alice.id.clone(),
|
||||
content: Content::new_local("hi").unwrap(),
|
||||
in_reply_to_id: None,
|
||||
visibility: Visibility::Public,
|
||||
content_warning: None,
|
||||
sensitive: false,
|
||||
}));
|
||||
like_thought(&store, &store, &alice.id, &tid).await.unwrap();
|
||||
assert_eq!(store.likes.lock().unwrap().len(), 1);
|
||||
unlike_thought(&store, &store, &alice.id, &tid)
|
||||
|
||||
@@ -3,7 +3,7 @@ use domain::{
|
||||
events::DomainEvent,
|
||||
models::{
|
||||
feed::{EngagementStats, FeedEntry},
|
||||
thought::{Thought, Visibility},
|
||||
thought::{NewThought, Thought, Visibility},
|
||||
},
|
||||
ports::{
|
||||
EngagementRepository, EventPublisher, OutboxWriter, TagRepository, ThoughtRepository,
|
||||
@@ -46,15 +46,15 @@ pub async fn create_thought(
|
||||
Some("direct") => Visibility::Direct,
|
||||
_ => Visibility::Public,
|
||||
};
|
||||
let thought = Thought::new_local(
|
||||
ThoughtId::new(),
|
||||
input.user_id,
|
||||
content.clone(),
|
||||
input.in_reply_to_id.clone(),
|
||||
let thought = Thought::new_local(NewThought {
|
||||
id: ThoughtId::new(),
|
||||
user_id: input.user_id,
|
||||
content: content.clone(),
|
||||
in_reply_to_id: input.in_reply_to_id.clone(),
|
||||
visibility,
|
||||
input.content_warning,
|
||||
input.sensitive,
|
||||
);
|
||||
content_warning: input.content_warning,
|
||||
sensitive: input.sensitive,
|
||||
});
|
||||
thoughts.save(&thought).await?;
|
||||
|
||||
// Extract and attach hashtags from content.
|
||||
|
||||
@@ -222,7 +222,7 @@ async fn create_reply_sets_in_reply_to_id() {
|
||||
|
||||
// enrichment_tests (combined from second cfg(test) block)
|
||||
|
||||
use domain::models::thought::{Thought, Visibility};
|
||||
use domain::models::thought::{NewThought, Thought, Visibility};
|
||||
use domain::ports::{ThoughtRepository, UserWriter};
|
||||
|
||||
fn make_user() -> User {
|
||||
@@ -235,15 +235,15 @@ fn make_user() -> User {
|
||||
}
|
||||
|
||||
fn make_thought(user_id: UserId) -> Thought {
|
||||
Thought::new_local(
|
||||
ThoughtId::new(),
|
||||
Thought::new_local(NewThought {
|
||||
id: ThoughtId::new(),
|
||||
user_id,
|
||||
Content::new_local(String::from("hello")).unwrap(),
|
||||
None,
|
||||
Visibility::Public,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
content: Content::new_local(String::from("hello")).unwrap(),
|
||||
in_reply_to_id: None,
|
||||
visibility: Visibility::Public,
|
||||
content_warning: None,
|
||||
sensitive: false,
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -287,15 +287,15 @@ async fn get_thread_views_batches_correctly() {
|
||||
<TestStore as ThoughtRepository>::save(&store, &root)
|
||||
.await
|
||||
.unwrap();
|
||||
let reply = Thought::new_local(
|
||||
ThoughtId::new(),
|
||||
user.id.clone(),
|
||||
Content::new_local(String::from("reply")).unwrap(),
|
||||
Some(root.id.clone()),
|
||||
Visibility::Public,
|
||||
None,
|
||||
false,
|
||||
);
|
||||
let reply = Thought::new_local(NewThought {
|
||||
id: ThoughtId::new(),
|
||||
user_id: user.id.clone(),
|
||||
content: Content::new_local(String::from("reply")).unwrap(),
|
||||
in_reply_to_id: Some(root.id.clone()),
|
||||
visibility: Visibility::Public,
|
||||
content_warning: None,
|
||||
sensitive: false,
|
||||
});
|
||||
<TestStore as ThoughtRepository>::save(&store, &reply)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -73,7 +73,7 @@ pub async fn build(cfg: &Config) -> Infrastructure {
|
||||
|
||||
// 3. ActivityPub federation
|
||||
let ap_service = Arc::new(
|
||||
ActivityPubService::new(
|
||||
ActivityPubService::builder(
|
||||
Arc::new(PostgresFederationRepository::new(pool.clone())),
|
||||
Arc::new(PostgresApUserRepository::new(
|
||||
pool.clone(),
|
||||
@@ -86,12 +86,12 @@ pub async fn build(cfg: &Config) -> Infrastructure {
|
||||
Arc::new(postgres::tag::PgTagRepository::new(pool.clone())),
|
||||
)),
|
||||
cfg.base_url.clone(),
|
||||
cfg.allow_registration,
|
||||
"thoughts".to_string(),
|
||||
cfg.debug,
|
||||
None,
|
||||
Arc::new(PgRemoteActorConnectionRepository::new(pool.clone())),
|
||||
)
|
||||
.allow_registration(cfg.allow_registration)
|
||||
.software_name("thoughts")
|
||||
.debug(cfg.debug)
|
||||
.build()
|
||||
.await
|
||||
.expect("Failed to build ActivityPubService"),
|
||||
);
|
||||
|
||||
@@ -46,24 +46,26 @@ impl Visibility {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NewThought {
|
||||
pub id: ThoughtId,
|
||||
pub user_id: UserId,
|
||||
pub content: Content,
|
||||
pub in_reply_to_id: Option<ThoughtId>,
|
||||
pub visibility: Visibility,
|
||||
pub content_warning: Option<String>,
|
||||
pub sensitive: bool,
|
||||
}
|
||||
|
||||
impl Thought {
|
||||
pub fn new_local(
|
||||
id: ThoughtId,
|
||||
user_id: UserId,
|
||||
content: Content,
|
||||
in_reply_to_id: Option<ThoughtId>,
|
||||
visibility: Visibility,
|
||||
content_warning: Option<String>,
|
||||
sensitive: bool,
|
||||
) -> Self {
|
||||
pub fn new_local(p: NewThought) -> Self {
|
||||
Self {
|
||||
id,
|
||||
user_id,
|
||||
content,
|
||||
in_reply_to_id,
|
||||
visibility,
|
||||
content_warning,
|
||||
sensitive,
|
||||
id: p.id,
|
||||
user_id: p.user_id,
|
||||
content: p.content,
|
||||
in_reply_to_id: p.in_reply_to_id,
|
||||
visibility: p.visibility,
|
||||
content_warning: p.content_warning,
|
||||
sensitive: p.sensitive,
|
||||
local: true,
|
||||
created_at: Utc::now(),
|
||||
updated_at: None,
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
use crate::value_objects::{Email, PasswordHash, UserId, Username};
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct UpdateProfileInput {
|
||||
pub display_name: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
pub avatar_url: Option<String>,
|
||||
pub header_url: Option<String>,
|
||||
pub custom_css: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct User {
|
||||
pub id: UserId,
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::{
|
||||
tag::Tag,
|
||||
thought::Thought,
|
||||
top_friend::TopFriend,
|
||||
user::User,
|
||||
user::{UpdateProfileInput, User},
|
||||
},
|
||||
value_objects::{
|
||||
ApiKeyId, Content, Email, NotificationId, PasswordHash, ThoughtId, UserId, Username,
|
||||
@@ -69,11 +69,7 @@ pub trait UserWriter: Send + Sync {
|
||||
async fn update_profile(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
display_name: Option<String>,
|
||||
bio: Option<String>,
|
||||
avatar_url: Option<String>,
|
||||
header_url: Option<String>,
|
||||
custom_css: Option<String>,
|
||||
input: UpdateProfileInput,
|
||||
) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::{
|
||||
tag::Tag,
|
||||
thought::Thought,
|
||||
top_friend::TopFriend,
|
||||
user::User,
|
||||
user::{UpdateProfileInput, User},
|
||||
},
|
||||
ports::*,
|
||||
value_objects::{
|
||||
@@ -125,11 +125,7 @@ impl UserWriter for TestStore {
|
||||
async fn update_profile(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
display_name: Option<String>,
|
||||
bio: Option<String>,
|
||||
avatar_url: Option<String>,
|
||||
header_url: Option<String>,
|
||||
custom_css: Option<String>,
|
||||
input: UpdateProfileInput,
|
||||
) -> Result<(), DomainError> {
|
||||
if let Some(u) = self
|
||||
.users
|
||||
@@ -138,11 +134,11 @@ impl UserWriter for TestStore {
|
||||
.iter_mut()
|
||||
.find(|u| &u.id == user_id)
|
||||
{
|
||||
u.display_name = display_name;
|
||||
u.bio = bio;
|
||||
u.avatar_url = avatar_url;
|
||||
u.header_url = header_url;
|
||||
u.custom_css = custom_css;
|
||||
u.display_name = input.display_name;
|
||||
u.bio = input.bio;
|
||||
u.avatar_url = input.avatar_url;
|
||||
u.header_url = input.header_url;
|
||||
u.custom_css = input.custom_css;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -17,8 +17,9 @@ use axum::{
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use domain::ports::{
|
||||
EventPublisher, FederationActionPort, FollowRepository, SearchPort, UserRepository,
|
||||
use domain::{
|
||||
models::user::UpdateProfileInput,
|
||||
ports::{EventPublisher, FederationActionPort, FollowRepository, SearchPort, UserRepository},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -96,11 +97,13 @@ pub async fn patch_profile(
|
||||
&*d.users,
|
||||
&*d.events,
|
||||
&uid,
|
||||
body.display_name,
|
||||
body.bio,
|
||||
body.avatar_url,
|
||||
body.header_url,
|
||||
body.custom_css,
|
||||
UpdateProfileInput {
|
||||
display_name: body.display_name,
|
||||
bio: body.bio,
|
||||
avatar_url: body.avatar_url,
|
||||
header_url: body.header_url,
|
||||
custom_css: body.custom_css,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let user = fetch_user(&*d.users, &uid).await?;
|
||||
|
||||
@@ -68,14 +68,7 @@ impl ActivityPubRepository for NoOpApRepo {
|
||||
}
|
||||
async fn accept_note(
|
||||
&self,
|
||||
_ap_id: &str,
|
||||
_author_id: &UserId,
|
||||
_content: &str,
|
||||
_published: chrono::DateTime<chrono::Utc>,
|
||||
_sensitive: bool,
|
||||
_content_warning: Option<String>,
|
||||
_visibility: &str,
|
||||
_in_reply_to: Option<&str>,
|
||||
_input: activitypub_base::AcceptNoteInput<'_>,
|
||||
) -> Result<ThoughtId, DomainError> {
|
||||
Ok(ThoughtId::from_uuid(uuid::Uuid::new_v4()))
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ pub async fn build(database_url: &str, base_url: &str, nats_url: &str) -> Worker
|
||||
|
||||
// ActivityPub service (for federation fan-out)
|
||||
let ap_service = Arc::new(
|
||||
ActivityPubService::new(
|
||||
ActivityPubService::builder(
|
||||
Arc::new(PostgresFederationRepository::new(pool.clone())),
|
||||
Arc::new(PostgresApUserRepository::new(
|
||||
pool.clone(),
|
||||
@@ -52,13 +52,11 @@ pub async fn build(database_url: &str, base_url: &str, nats_url: &str) -> Worker
|
||||
None,
|
||||
Arc::new(postgres::tag::PgTagRepository::new(pool.clone())),
|
||||
)),
|
||||
base_url.to_string(),
|
||||
false,
|
||||
"thoughts".to_string(),
|
||||
false,
|
||||
None,
|
||||
base_url,
|
||||
Arc::new(PgRemoteActorConnectionRepository::new(pool.clone())),
|
||||
)
|
||||
.software_name("thoughts")
|
||||
.build()
|
||||
.await
|
||||
.expect("ActivityPubService build failed"),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user