From e1f84b6796dfd36926c6f36bd67e0c20a47ba4bf Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Sat, 9 May 2026 18:28:44 +0200 Subject: [PATCH] Implement local follow and unfollow functionality in ActivityPubService --- .../adapters/activitypub-base/src/service.rs | 75 ++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/crates/adapters/activitypub-base/src/service.rs b/crates/adapters/activitypub-base/src/service.rs index 711230e..f145240 100644 --- a/crates/adapters/activitypub-base/src/service.rs +++ b/crates/adapters/activitypub-base/src/service.rs @@ -19,7 +19,7 @@ use crate::{ followers_handler::{followers_handler, following_handler}, inbox::inbox_handler, outbox::outbox_handler, - repository::{FederationRepository, FollowerStatus, RemoteActor}, + repository::{FederationRepository, FollowerStatus, FollowingStatus, RemoteActor}, user::ApUserRepository, urls::activity_url, webfinger::webfinger_handler, @@ -102,6 +102,12 @@ impl ActivityPubService { pub async fn follow(&self, local_user_id: uuid::Uuid, handle: &str) -> anyhow::Result<()> { let data = self.federation_config.to_request_data(); + let normalized = handle.trim_start_matches('@'); + let parts: Vec<&str> = normalized.splitn(2, '@').collect(); + if parts.len() == 2 && parts[1] == data.domain { + return self.follow_local(local_user_id, parts[0], &data).await; + } + let remote_actor: DbActor = webfinger_resolve_actor(handle, &data) .await .map_err(|e| anyhow::anyhow!("{e}"))?; @@ -149,6 +155,10 @@ impl ActivityPubService { pub async fn unfollow(&self, local_user_id: uuid::Uuid, actor_url_str: &str) -> anyhow::Result<()> { let data = self.federation_config.to_request_data(); + if actor_url_str.starts_with(&self.base_url) { + return self.unfollow_local(local_user_id, actor_url_str, &data).await; + } + let remote = data .federation_repo .get_remote_actor(actor_url_str) @@ -396,6 +406,69 @@ impl ActivityPubService { Ok(()) } + async fn follow_local( + &self, + local_user_id: uuid::Uuid, + target_username: &str, + data: &activitypub_federation::config::Data, + ) -> anyhow::Result<()> { + let target = data + .user_repo + .find_by_username(target_username) + .await? + .ok_or_else(|| anyhow::anyhow!("user not found: {}", target_username))?; + + if target.id == local_user_id { + return Err(anyhow::anyhow!("cannot follow yourself")); + } + + let follower_actor_url = crate::urls::actor_url(&self.base_url, local_user_id).to_string(); + let target_actor_url = crate::urls::actor_url(&self.base_url, target.id); + let target_inbox_url = format!("{}/inbox", target_actor_url); + let follow_id = activity_url(&self.base_url).map_err(|e| anyhow::anyhow!("{e}"))?.to_string(); + + data.federation_repo + .add_follower(target.id, &follower_actor_url, FollowerStatus::Accepted, &follow_id) + .await?; + + let target_as_remote = RemoteActor { + url: target_actor_url.to_string(), + handle: format!("{}@{}", target.username, data.domain), + inbox_url: target_inbox_url, + shared_inbox_url: None, + display_name: Some(target.username), + }; + data.federation_repo + .add_following(local_user_id, target_as_remote, &follow_id) + .await?; + + data.federation_repo + .update_following_status(local_user_id, &target_actor_url.to_string(), FollowingStatus::Accepted) + .await?; + + tracing::info!(follower = %local_user_id, followee = %target.id, "local follow"); + Ok(()) + } + + async fn unfollow_local( + &self, + local_user_id: uuid::Uuid, + target_actor_url: &str, + data: &activitypub_federation::config::Data, + ) -> anyhow::Result<()> { + let target_url = Url::parse(target_actor_url)?; + let target_user_id = crate::urls::extract_user_id_from_url(&target_url) + .ok_or_else(|| anyhow::anyhow!("invalid local actor URL: {}", target_actor_url))?; + + let local_actor_url = crate::urls::actor_url(&self.base_url, local_user_id).to_string(); + + data.federation_repo.remove_follower(target_user_id, &local_actor_url).await?; + data.federation_repo.remove_following(local_user_id, target_actor_url).await?; + + tracing::info!(follower = %local_user_id, followee = %target_user_id, "local unfollow"); + Ok(()) + } + fn spawn_backfill(&self, owner_user_id: uuid::Uuid, follower_inbox_url: String) { let config = self.federation_config.clone(); let base_url = self.base_url.clone();