//! Follow/unfollow remote actors
//!
//! Implements sending Follow and Undo(Follow) activities to remote actors,
//! and handling Accept/Reject responses.
use sea_orm::{
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait, QueryFilter, Set,
};
use tracing::{info, instrument};
use uuid::Uuid;
use crate::activitypub::ApUrls;
use crate::entities::prelude::*;
use crate::entities::{ap_following, ap_remote_actors};
/// Send a Follow activity to a remote actor.
///
/// Creates a Follow activity, stores it in the database with state "pending",
/// and enqueues it for delivery to the remote actor's inbox.
#[instrument(skip(db, local_actor, remote_actor))]
pub async fn send_follow(
db: &DatabaseConnection,
public_url: &str,
local_actor: &crate::entities::ap_actors::Model,
remote_actor: &crate::entities::ap_remote_actors::Model,
) -> Result<(), String> {
let urls = ApUrls::new(public_url);
let user = match local_actor.user_id {
Some(user_id) => Users::find_by_id(user_id)
.one(db)
.await
.map_err(|e| format!("failed to fetch user: {}", e))?
.ok_or("user not found")?,
None => return Err("actor has no user_id".to_string()),
};
let local_actor_url = urls.actor(&user.name);
let activity_uuid = Uuid::new_v4();
let activity_url = urls.activity(&user.name, activity_uuid);
// Build the Follow activity JSON
let follow_activity = serde_json::json!({
"@context": "https://www.w3.org/ns/activitystreams",
"id": activity_url,
"type": "Follow",
"actor": local_actor_url,
"object": remote_actor.actor_url
});
let activity_json = serde_json::to_string(&follow_activity)
.map_err(|e| format!("failed to serialize Follow: {}", e))?;
// Insert into ap_following with state "pending"
let following_model = ap_following::ActiveModel {
local_actor_id: Set(local_actor.id),
remote_actor_id: Set(remote_actor.id),
follow_activity_id: Set(activity_url.clone()),
state: Set("pending".to_string()),
..Default::default()
};
following_model
.insert(db)
.await
.map_err(|e| format!("failed to insert following: {}", e))?;
// Enqueue delivery to remote inbox
crate::activitypub::delivery::enqueue_delivery(
db,
local_actor.id,
activity_uuid,
activity_json,
remote_actor.actor_url.clone(),
remote_actor.inbox_url.clone(),
)
.await
.map_err(|e| format!("failed to enqueue delivery: {}", e))?;
info!(
remote_actor_url = %remote_actor.actor_url,
activity_url = %activity_url,
"sent Follow activity"
);
Ok(())
}
/// Send an Undo(Follow) activity to a remote actor.
///
/// Creates an Undo activity wrapping the original Follow, enqueues it for
/// delivery, and removes the following relationship from the database.
#[instrument(skip(db, local_actor, remote_actor))]
pub async fn send_unfollow(
db: &DatabaseConnection,
public_url: &str,
local_actor: &crate::entities::ap_actors::Model,
remote_actor: &crate::entities::ap_remote_actors::Model,
) -> Result<(), String> {
let urls = ApUrls::new(public_url);
let user = match local_actor.user_id {
Some(user_id) => Users::find_by_id(user_id)
.one(db)
.await
.map_err(|e| format!("failed to fetch user: {}", e))?
.ok_or("user not found")?,
None => return Err("actor has no user_id".to_string()),
};
let local_actor_url = urls.actor(&user.name);
let activity_uuid = Uuid::new_v4();
let activity_url = urls.activity(&user.name, activity_uuid);
// Find the following relationship
let following = ApFollowing::find()
.filter(ap_following::Column::LocalActorId.eq(local_actor.id))
.filter(ap_following::Column::RemoteActorId.eq(remote_actor.id))
.one(db)
.await
.map_err(|e| format!("failed to find following: {}", e))?
.ok_or("not following this actor")?;
// Build the Undo activity
let undo_activity = serde_json::json!({
"@context": "https://www.w3.org/ns/activitystreams",
"id": activity_url,
"type": "Undo",
"actor": local_actor_url,
"object": {
"id": following.follow_activity_id,
"type": "Follow",
"actor": local_actor_url,
"object": remote_actor.actor_url
}
});
let activity_json = serde_json::to_string(&undo_activity)
.map_err(|e| format!("failed to serialize Undo: {}", e))?;
// Enqueue delivery to remote inbox
crate::activitypub::delivery::enqueue_delivery(
db,
local_actor.id,
activity_uuid,
activity_json,
remote_actor.actor_url.clone(),
remote_actor.inbox_url.clone(),
)
.await
.map_err(|e| format!("failed to enqueue delivery: {}", e))?;
// Delete the following relationship
following
.delete(db)
.await
.map_err(|e| format!("failed to delete following: {}", e))?;
info!(
remote_actor_url = %remote_actor.actor_url,
"sent Undo(Follow) activity"
);
Ok(())
}
/// Handle an Accept activity for a Follow.
///
/// Updates the following relationship state from "pending" to "accepted".
#[instrument(skip(db, accept))]
pub async fn handle_accept(
db: &DatabaseConnection,
verified_actor_url: &str,
accept: &crate::activitypub::types::AcceptRejectActivity,
) -> Result<(), String> {
// Extract the Follow activity ID from the object
let follow_activity_id = match &accept.object {
crate::activitypub::types::ObjectRef::Url(url) => url.clone(),
crate::activitypub::types::ObjectRef::Embedded(obj) => {
// If it's embedded, extract the id
if obj.kind != "Follow" {
return Err(format!("Accept for non-Follow activity: {}", obj.kind));
}
obj.id.clone()
}
};
// Find the remote actor
let remote_actor = ApRemoteActors::find()
.filter(ap_remote_actors::Column::ActorUrl.eq(verified_actor_url))
.one(db)
.await
.map_err(|e| format!("failed to find remote actor: {}", e))?
.ok_or("remote actor not found")?;
// Find the following relationship
let following = ApFollowing::find()
.filter(ap_following::Column::FollowActivityId.eq(&follow_activity_id))
.filter(ap_following::Column::RemoteActorId.eq(remote_actor.id))
.one(db)
.await
.map_err(|e| format!("failed to find following: {}", e))?
.ok_or("following relationship not found")?;
// Update state to "accepted"
let mut active_model: ap_following::ActiveModel = following.into();
active_model.state = Set("accepted".to_string());
active_model
.update(db)
.await
.map_err(|e| format!("failed to update following: {}", e))?;
info!(
remote_actor_url = %verified_actor_url,
follow_activity_id = %follow_activity_id,
"accepted Follow"
);
Ok(())
}
/// Handle a Reject activity for a Follow.
///
/// Updates the following relationship state from "pending" to "rejected".
#[instrument(skip(db, reject))]
pub async fn handle_reject(
db: &DatabaseConnection,
verified_actor_url: &str,
reject: &crate::activitypub::types::AcceptRejectActivity,
) -> Result<(), String> {
// Extract the Follow activity ID from the object
let follow_activity_id = match &reject.object {
crate::activitypub::types::ObjectRef::Url(url) => url.clone(),
crate::activitypub::types::ObjectRef::Embedded(obj) => {
// If it's embedded, extract the id
if obj.kind != "Follow" {
return Err(format!("Reject for non-Follow activity: {}", obj.kind));
}
obj.id.clone()
}
};
// Find the remote actor
let remote_actor = ApRemoteActors::find()
.filter(ap_remote_actors::Column::ActorUrl.eq(verified_actor_url))
.one(db)
.await
.map_err(|e| format!("failed to find remote actor: {}", e))?
.ok_or("remote actor not found")?;
// Find the following relationship
let following = ApFollowing::find()
.filter(ap_following::Column::FollowActivityId.eq(&follow_activity_id))
.filter(ap_following::Column::RemoteActorId.eq(remote_actor.id))
.one(db)
.await
.map_err(|e| format!("failed to find following: {}", e))?
.ok_or("following relationship not found")?;
// Update state to "rejected"
let mut active_model: ap_following::ActiveModel = following.into();
active_model.state = Set("rejected".to_string());
active_model
.update(db)
.await
.map_err(|e| format!("failed to update following: {}", e))?;
info!(
remote_actor_url = %verified_actor_url,
follow_activity_id = %follow_activity_id,
"rejected Follow"
);
Ok(())
}