fix: FollowTarget enum — actor_url no longer lies about holding a handle

FollowCommand.target is now FollowTarget (Identity|Handle) instead of
SocialIdentity. actor_url field always holds a URL; handles go through
FollowTarget::Handle. Adapter resolves handles explicitly. Type system
prevents misuse in future commands.
This commit is contained in:
2026-07-10 16:18:57 +02:00
parent 2484f1e603
commit d60c47199c
18 changed files with 95 additions and 45 deletions

View File

@@ -4,7 +4,7 @@ use async_trait::async_trait;
use domain::{
errors::DomainError,
ports::{SocialCommand, SocialQuery, UserRepository},
value_objects::{SocialActor, SocialIdentity, UserId},
value_objects::{FollowTarget, SocialActor, SocialIdentity, UserId},
};
use k_ap::RemoteActor;
@@ -87,15 +87,18 @@ fn ap_err(e: anyhow::Error) -> DomainError {
#[async_trait]
impl SocialCommand for CompositeSocialAdapter {
async fn follow(&self, follower: &UserId, target: &SocialIdentity) -> Result<(), DomainError> {
if let SocialIdentity::Local(target_id) = target
async fn follow(&self, follower: &UserId, target: &FollowTarget) -> Result<(), DomainError> {
if let FollowTarget::Identity(SocialIdentity::Local(target_id)) = target
&& follower == target_id
{
return Err(DomainError::ValidationError(
"Cannot follow yourself".into(),
));
}
let handle = self.resolve_handle(target).await?;
let handle = match target {
FollowTarget::Handle(h) => h.clone(),
FollowTarget::Identity(id) => self.resolve_handle(id).await?,
};
self.ap_service
.follow(follower.value(), &handle)
.await

View File

@@ -196,6 +196,13 @@ fn identity_to_payload(id: &SocialIdentity) -> (String, String) {
}
}
fn follow_target_to_payload(target: &domain::value_objects::FollowTarget) -> (String, String) {
match target {
domain::value_objects::FollowTarget::Identity(id) => identity_to_payload(id),
domain::value_objects::FollowTarget::Handle(h) => ("handle".into(), h.clone()),
}
}
fn payload_to_identity(kind: &str, id: String) -> Result<SocialIdentity, DomainError> {
match kind {
"local" => Ok(SocialIdentity::Local(UserId::from_uuid(parse_uuid(
@@ -208,6 +215,18 @@ fn payload_to_identity(kind: &str, id: String) -> Result<SocialIdentity, DomainE
}
}
fn payload_to_follow_target(
kind: &str,
id: String,
) -> Result<domain::value_objects::FollowTarget, DomainError> {
match kind {
"handle" => Ok(domain::value_objects::FollowTarget::Handle(id)),
other => Ok(domain::value_objects::FollowTarget::Identity(
payload_to_identity(other, id)?,
)),
}
}
fn parse_ts(ts: i64) -> Result<NaiveDateTime, DomainError> {
chrono::DateTime::from_timestamp(ts, 0)
.map(|dt| dt.naive_utc())
@@ -294,7 +313,7 @@ impl From<&DomainEvent> for EventPayload {
}
}
DomainEvent::FollowRequested { follower, target } => {
let (kind, id) = identity_to_payload(target);
let (kind, id) = follow_target_to_payload(target);
EventPayload::FollowRequested {
follower_id: follower.value().to_string(),
target_kind: kind,
@@ -530,7 +549,7 @@ impl TryFrom<EventPayload> for DomainEvent {
target_id,
} => Ok(DomainEvent::FollowRequested {
follower: UserId::from_uuid(parse_uuid(&follower_id, "follower_id")?),
target: payload_to_identity(&target_kind, target_id)?,
target: payload_to_follow_target(&target_kind, target_id)?,
}),
EventPayload::FollowAccepted {
owner_id,