use apb::{ActivityMut, BaseMut, ObjectMut}; use sea_orm::{ActiveValue::{NotSet, Set}, DbErr, EntityTrait, QueryFilter, ColumnTrait}; use upub::traits::{fetch::RequestError, Fetcher}; #[derive(Debug, Clone, clap::Subcommand)] /// available actions to take on relays pub enum RelayCommand { /// get all current pending and accepted relays Status, /// request to follow a specific relay Follow { /// relay actor to follow (must be full AP id, like for pleroma) actor: String, }, /// accept a pending relay request Accept { /// relay actor to accept (must be full AP id, like for pleroma) actor: String, }, /// retract a follow relation to a relay, stopping receiving content Unfollow { /// relay actor to unfollow (must be full AP id, like for pleroma) actor: String, }, /// remove a follow relation from a relay, stopping sending content Remove { /// relay actor to unfollow (must be full AP id, like for pleroma) actor: String, }, } pub async fn relay(ctx: upub::Context, action: RelayCommand) -> Result<(), RequestError> { let my_internal = upub::model::actor::Entity::ap_to_internal(ctx.base(), ctx.db()) .await? .ok_or_else(|| DbErr::RecordNotFound(ctx.base().to_string()))?; let their_internal = match &action { RelayCommand::Status => 0, RelayCommand::Follow { actor } | RelayCommand::Accept { actor } | RelayCommand::Unfollow { actor } | RelayCommand::Remove { actor } => ctx.fetch_user(actor, ctx.db()).await?.internal, }; match action { RelayCommand::Status => { tracing::info!("active sinks:"); for sink in upub::Query::related(None, Some(my_internal), false) .into_model::<upub::model::actor::Model>() .all(ctx.db()) .await? { tracing::info!("[>>] {} {}", sink.name.unwrap_or_default(), sink.id); } tracing::info!("active sources:"); for source in upub::Query::related(Some(my_internal), None, false) .into_model::<upub::model::actor::Model>() .all(ctx.db()) .await? { tracing::info!("[<<] {} {}", source.name.unwrap_or_default(), source.id); } }, RelayCommand::Follow { actor } => { let aid = ctx.aid(&upub::Context::new_id()); let payload = apb::new() .set_id(Some(aid.clone())) .set_activity_type(Some(apb::ActivityType::Follow)) .set_actor(apb::Node::link(ctx.base().to_string())) .set_object(apb::Node::link(actor.clone())) .set_to(apb::Node::links(vec![actor.clone()])) .set_cc(apb::Node::links(vec![apb::target::PUBLIC.to_string()])) .set_published(Some(chrono::Utc::now())); let job = upub::model::job::ActiveModel { internal: NotSet, activity: Set(aid), job_type: Set(upub::model::job::JobType::Outbound), actor: Set(ctx.base().to_string()), target: Set(None), payload: Set(Some(payload)), attempt: Set(0), published: Set(chrono::Utc::now()), not_before: Set(chrono::Utc::now()), error: Set(None), }; tracing::info!("following relay {actor}"); upub::model::job::Entity::insert(job).exec(ctx.db()).await?; }, RelayCommand::Accept { actor } => { let relation = upub::model::relation::Entity::find() .filter(upub::model::relation::Column::Follower.eq(their_internal)) .filter(upub::model::relation::Column::Following.eq(my_internal)) .one(ctx.db()) .await? .ok_or_else(|| DbErr::RecordNotFound(format!("relation-{their_internal}-{my_internal}")))?; let activity = upub::model::activity::Entity::find_by_id(relation.activity) .one(ctx.db()) .await? .ok_or_else(|| DbErr::RecordNotFound(format!("activity#{}", relation.activity)))?; let aid = ctx.aid(&upub::Context::new_id()); let payload = apb::new() .set_id(Some(aid.clone())) .set_activity_type(Some(apb::ActivityType::Accept(apb::AcceptType::Accept))) .set_actor(apb::Node::link(ctx.base().to_string())) .set_object(apb::Node::link(activity.id)) .set_to(apb::Node::links(vec![actor.clone()])) .set_cc(apb::Node::links(vec![apb::target::PUBLIC.to_string()])) .set_published(Some(chrono::Utc::now())); let job = upub::model::job::ActiveModel { internal: NotSet, activity: Set(aid), job_type: Set(upub::model::job::JobType::Outbound), actor: Set(ctx.base().to_string()), target: Set(None), payload: Set(Some(payload)), attempt: Set(0), published: Set(chrono::Utc::now()), not_before: Set(chrono::Utc::now()), error: Set(None), }; tracing::info!("accepting relay {actor}"); upub::model::job::Entity::insert(job).exec(ctx.db()).await?; }, RelayCommand::Remove { actor } => { let relation = upub::model::relation::Entity::find() .filter(upub::model::relation::Column::Follower.eq(their_internal)) .filter(upub::model::relation::Column::Following.eq(my_internal)) .one(ctx.db()) .await? .ok_or_else(|| DbErr::RecordNotFound(format!("relation-{their_internal}-{my_internal}")))?; let accept_activity_id = relation.accept.ok_or(DbErr::RecordNotFound(format!("accept-{their_internal}-{my_internal}")))?; let activity = upub::model::activity::Entity::find_by_id(accept_activity_id) .one(ctx.db()) .await? .ok_or_else(|| DbErr::RecordNotFound(format!("activity#{}", accept_activity_id)))?; let aid = ctx.aid(&upub::Context::new_id()); let payload = apb::new() .set_id(Some(aid.clone())) .set_activity_type(Some(apb::ActivityType::Undo)) .set_actor(apb::Node::link(ctx.base().to_string())) .set_object(apb::Node::object(ctx.ap(activity))) .set_to(apb::Node::links(vec![actor.clone()])) .set_cc(apb::Node::links(vec![apb::target::PUBLIC.to_string()])) .set_published(Some(chrono::Utc::now())); let job = upub::model::job::ActiveModel { internal: NotSet, activity: Set(aid), job_type: Set(upub::model::job::JobType::Outbound), actor: Set(ctx.base().to_string()), target: Set(None), payload: Set(Some(payload)), attempt: Set(0), published: Set(chrono::Utc::now()), not_before: Set(chrono::Utc::now()), error: Set(None), }; tracing::info!("unfollowing relay {actor}"); upub::model::job::Entity::insert(job).exec(ctx.db()).await?; }, RelayCommand::Unfollow { actor } => { let relation = upub::model::relation::Entity::find() .filter(upub::model::relation::Column::Follower.eq(my_internal)) .filter(upub::model::relation::Column::Following.eq(their_internal)) .one(ctx.db()) .await? .ok_or_else(|| DbErr::RecordNotFound(format!("relation-{my_internal}-{their_internal}")))?; let activity = upub::model::activity::Entity::find_by_id(relation.activity) .one(ctx.db()) .await? .ok_or_else(|| DbErr::RecordNotFound(format!("activity#{}", relation.activity)))?; let aid = ctx.aid(&upub::Context::new_id()); let payload = apb::new() .set_id(Some(aid.clone())) .set_activity_type(Some(apb::ActivityType::Undo)) .set_actor(apb::Node::link(ctx.base().to_string())) .set_object(apb::Node::object(ctx.ap(activity))) .set_to(apb::Node::links(vec![actor.clone()])) .set_cc(apb::Node::links(vec![apb::target::PUBLIC.to_string()])) .set_published(Some(chrono::Utc::now())); let job = upub::model::job::ActiveModel { internal: NotSet, activity: Set(aid), job_type: Set(upub::model::job::JobType::Outbound), actor: Set(ctx.base().to_string()), target: Set(None), payload: Set(Some(payload)), attempt: Set(0), published: Set(chrono::Utc::now()), not_before: Set(chrono::Utc::now()), error: Set(None), }; tracing::info!("unfollowing relay {actor}"); upub::model::job::Entity::insert(job).exec(ctx.db()).await?; }, } Ok(()) }