Implement federated user following (fixes #752) (#2577)

* Implement federated user following (fixes #752)

* rewrite send_activity_in_community and add docs, remove default for column pending

* improve migration

* replace null values in db migration
This commit is contained in:
Nutomic 2022-11-23 23:40:47 +00:00 committed by GitHub
parent 4ddca46228
commit d20d2b9218
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 329 additions and 156 deletions

View file

@ -4,7 +4,7 @@ use lemmy_api_common::{
community::{BlockCommunity, BlockCommunityResponse}, community::{BlockCommunity, BlockCommunityResponse},
utils::get_local_user_view_from_jwt, utils::get_local_user_view_from_jwt,
}; };
use lemmy_apub::protocol::activities::following::undo_follow::UndoFollowCommunity; use lemmy_apub::protocol::activities::following::undo_follow::UndoFollow;
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{ source::{
community::{Community, CommunityFollower, CommunityFollowerForm}, community::{Community, CommunityFollower, CommunityFollowerForm},
@ -53,7 +53,7 @@ impl Perform for BlockCommunity {
.await .await
.ok(); .ok();
let community = Community::read(context.pool(), community_id).await?; let community = Community::read(context.pool(), community_id).await?;
UndoFollowCommunity::send(&local_user_view.person.into(), &community.into(), context).await?; UndoFollow::send(&local_user_view.person.into(), &community.into(), context).await?;
} else { } else {
CommunityBlock::unblock(context.pool(), &community_block_form) CommunityBlock::unblock(context.pool(), &community_block_form)
.await .await

View file

@ -7,8 +7,8 @@ use lemmy_api_common::{
use lemmy_apub::{ use lemmy_apub::{
objects::community::ApubCommunity, objects::community::ApubCommunity,
protocol::activities::following::{ protocol::activities::following::{
follow::FollowCommunity as FollowCommunityApub, follow::Follow as FollowCommunityApub,
undo_follow::UndoFollowCommunity, undo_follow::UndoFollow,
}, },
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
@ -60,8 +60,7 @@ impl Perform for FollowCommunity {
FollowCommunityApub::send(&local_user_view.person.clone().into(), &community, context) FollowCommunityApub::send(&local_user_view.person.clone().into(), &community, context)
.await?; .await?;
} else { } else {
UndoFollowCommunity::send(&local_user_view.person.clone().into(), &community, context) UndoFollow::send(&local_user_view.person.clone().into(), &community, context).await?;
.await?;
CommunityFollower::unfollow(context.pool(), &community_follower_form) CommunityFollower::unfollow(context.pool(), &community_follower_form)
.await .await
.map_err(|e| LemmyError::from_error_message(e, "community_follower_already_exists"))?; .map_err(|e| LemmyError::from_error_message(e, "community_follower_already_exists"))?;

View file

@ -97,7 +97,7 @@ impl BlockUser {
SiteOrCommunity::Community(c) => { SiteOrCommunity::Community(c) => {
let activity = AnnouncableActivities::BlockUser(block); let activity = AnnouncableActivities::BlockUser(block);
let inboxes = vec![user.shared_inbox_or_inbox()]; let inboxes = vec![user.shared_inbox_or_inbox()];
send_activity_in_community(activity, mod_, c, inboxes, context).await send_activity_in_community(activity, mod_, c, inboxes, true, context).await
} }
} }
} }

View file

@ -63,7 +63,7 @@ impl UndoBlockUser {
} }
SiteOrCommunity::Community(c) => { SiteOrCommunity::Community(c) => {
let activity = AnnouncableActivities::UndoBlockUser(undo); let activity = AnnouncableActivities::UndoBlockUser(undo);
send_activity_in_community(activity, mod_, c, inboxes, context).await send_activity_in_community(activity, mod_, c, inboxes, true, context).await
} }
} }
} }

View file

@ -59,7 +59,7 @@ impl AddMod {
let activity = AnnouncableActivities::AddMod(add); let activity = AnnouncableActivities::AddMod(add);
let inboxes = vec![added_mod.shared_inbox_or_inbox()]; let inboxes = vec![added_mod.shared_inbox_or_inbox()];
send_activity_in_community(activity, actor, community, inboxes, context).await send_activity_in_community(activity, actor, community, inboxes, true, context).await
} }
} }

View file

@ -2,11 +2,11 @@ use crate::{
activities::send_lemmy_activity, activities::send_lemmy_activity,
activity_lists::AnnouncableActivities, activity_lists::AnnouncableActivities,
local_instance, local_instance,
objects::community::ApubCommunity, objects::{community::ApubCommunity, person::ApubPerson},
protocol::activities::community::announce::AnnounceActivity, protocol::activities::community::announce::AnnounceActivity,
ActorType,
}; };
use activitypub_federation::{core::object_id::ObjectId, traits::Actor}; use activitypub_federation::{core::object_id::ObjectId, traits::Actor};
use lemmy_db_schema::source::person::PersonFollower;
use lemmy_utils::error::LemmyError; use lemmy_utils::error::LemmyError;
use lemmy_websocket::LemmyContext; use lemmy_websocket::LemmyContext;
use url::Url; use url::Url;
@ -17,22 +17,47 @@ pub mod remove_mod;
pub mod report; pub mod report;
pub mod update; pub mod update;
#[tracing::instrument(skip_all)] /// This function sends all activities which are happening in a community to the right inboxes.
pub(crate) async fn send_activity_in_community<ActorT>( /// For example Create/Page, Add/Mod etc, but not private messages.
///
/// Activities are sent to the community itself if it lives on another instance. If the community
/// is local, the activity is directly wrapped into Announce and sent to community followers.
/// Activities are also sent to those who follow the actor (with exception of moderation activities).
///
/// * `activity` - The activity which is being sent
/// * `actor` - The user who is sending the activity
/// * `community` - Community inside which the activity is sent
/// * `inboxes` - Any additional inboxes the activity should be sent to (for example,
/// to the user who is being promoted to moderator)
/// * `is_mod_activity` - True for things like Add/Mod, these are not sent to user followers
pub(crate) async fn send_activity_in_community(
activity: AnnouncableActivities, activity: AnnouncableActivities,
actor: &ActorT, actor: &ApubPerson,
community: &ApubCommunity, community: &ApubCommunity,
mut inboxes: Vec<Url>, extra_inboxes: Vec<Url>,
is_mod_action: bool,
context: &LemmyContext, context: &LemmyContext,
) -> Result<(), LemmyError> ) -> Result<(), LemmyError> {
where // send to extra_inboxes
ActorT: Actor + ActorType, send_lemmy_activity(context, activity.clone(), actor, extra_inboxes, false).await?;
{
inboxes.push(community.shared_inbox_or_inbox());
send_lemmy_activity(context, activity.clone(), actor, inboxes, false).await?;
if community.local { if community.local {
AnnounceActivity::send(activity.try_into()?, community, context).await?; // send directly to community followers
AnnounceActivity::send(activity.clone().try_into()?, community, context).await?;
} else {
// send to the community, which will then forward to followers
let inbox = vec![community.shared_inbox_or_inbox()];
send_lemmy_activity(context, activity.clone(), actor, inbox, false).await?;
}
// send to those who follow `actor`
if !is_mod_action {
let inboxes = PersonFollower::list_followers(context.pool(), actor.id)
.await?
.into_iter()
.map(|p| ApubPerson(p).shared_inbox_or_inbox())
.collect();
send_lemmy_activity(context, activity, actor, inboxes, false).await?;
} }
Ok(()) Ok(())

View file

@ -59,7 +59,7 @@ impl RemoveMod {
let activity = AnnouncableActivities::RemoveMod(remove); let activity = AnnouncableActivities::RemoveMod(remove);
let inboxes = vec![removed_mod.shared_inbox_or_inbox()]; let inboxes = vec![removed_mod.shared_inbox_or_inbox()];
send_activity_in_community(activity, actor, community, inboxes, context).await send_activity_in_community(activity, actor, community, inboxes, true, context).await
} }
} }

View file

@ -44,7 +44,7 @@ impl UpdateCommunity {
}; };
let activity = AnnouncableActivities::UpdateCommunity(update); let activity = AnnouncableActivities::UpdateCommunity(update);
send_activity_in_community(activity, actor, &community, vec![], context).await send_activity_in_community(activity, actor, &community, vec![], true, context).await
} }
} }

View file

@ -87,7 +87,7 @@ impl CreateOrUpdateComment {
} }
let activity = AnnouncableActivities::CreateOrUpdateComment(create_or_update); let activity = AnnouncableActivities::CreateOrUpdateComment(create_or_update);
send_activity_in_community(activity, actor, &community, inboxes, context).await send_activity_in_community(activity, actor, &community, inboxes, false, context).await
} }
} }

View file

@ -63,8 +63,9 @@ impl CreateOrUpdatePost {
let community: ApubCommunity = Community::read(context.pool(), community_id).await?.into(); let community: ApubCommunity = Community::read(context.pool(), community_id).await?.into();
let create_or_update = CreateOrUpdatePost::new(post, actor, &community, kind, context).await?; let create_or_update = CreateOrUpdatePost::new(post, actor, &community, kind, context).await?;
let is_mod_action = create_or_update.object.is_mod_action(context).await?;
let activity = AnnouncableActivities::CreateOrUpdatePost(create_or_update); let activity = AnnouncableActivities::CreateOrUpdatePost(create_or_update);
send_activity_in_community(activity, actor, &community, vec![], context).await?; send_activity_in_community(activity, actor, &community, vec![], is_mod_action, context).await?;
Ok(()) Ok(())
} }
} }

View file

@ -65,6 +65,7 @@ pub async fn send_apub_delete_in_community(
context: &LemmyContext, context: &LemmyContext,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let actor = ApubPerson::from(actor); let actor = ApubPerson::from(actor);
let is_mod_action = reason.is_some();
let activity = if deleted { let activity = if deleted {
let delete = Delete::new(&actor, object, public(), Some(&community), reason, context)?; let delete = Delete::new(&actor, object, public(), Some(&community), reason, context)?;
AnnouncableActivities::Delete(delete) AnnouncableActivities::Delete(delete)
@ -72,7 +73,15 @@ pub async fn send_apub_delete_in_community(
let undo = UndoDelete::new(&actor, object, public(), Some(&community), reason, context)?; let undo = UndoDelete::new(&actor, object, public(), Some(&community), reason, context)?;
AnnouncableActivities::UndoDelete(undo) AnnouncableActivities::UndoDelete(undo)
}; };
send_activity_in_community(activity, &actor, &community.into(), vec![], context).await send_activity_in_community(
activity,
&actor,
&community.into(),
vec![],
is_mod_action,
context,
)
.await
} }
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]

View file

@ -1,7 +1,7 @@
use crate::{ use crate::{
activities::{generate_activity_id, send_lemmy_activity}, activities::{generate_activity_id, send_lemmy_activity},
local_instance, local_instance,
protocol::activities::following::{accept::AcceptFollowCommunity, follow::FollowCommunity}, protocol::activities::following::{accept::AcceptFollow, follow::Follow},
ActorType, ActorType,
}; };
use activitypub_federation::{ use activitypub_federation::{
@ -19,21 +19,21 @@ use lemmy_utils::error::LemmyError;
use lemmy_websocket::{messages::SendUserRoomMessage, LemmyContext, UserOperation}; use lemmy_websocket::{messages::SendUserRoomMessage, LemmyContext, UserOperation};
use url::Url; use url::Url;
impl AcceptFollowCommunity { impl AcceptFollow {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn send( pub async fn send(
follow: FollowCommunity, follow: Follow,
context: &LemmyContext, context: &LemmyContext,
request_counter: &mut i32, request_counter: &mut i32,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let community = follow.object.dereference_local(context).await?; let user_or_community = follow.object.dereference_local(context).await?;
let person = follow let person = follow
.actor .actor
.clone() .clone()
.dereference(context, local_instance(context).await, request_counter) .dereference(context, local_instance(context).await, request_counter)
.await?; .await?;
let accept = AcceptFollowCommunity { let accept = AcceptFollow {
actor: ObjectId::new(community.actor_id()), actor: ObjectId::new(user_or_community.actor_id()),
object: follow, object: follow,
kind: AcceptType::Accept, kind: AcceptType::Accept,
id: generate_activity_id( id: generate_activity_id(
@ -42,13 +42,13 @@ impl AcceptFollowCommunity {
)?, )?,
}; };
let inbox = vec![person.shared_inbox_or_inbox()]; let inbox = vec![person.shared_inbox_or_inbox()];
send_lemmy_activity(context, accept, &community, inbox, true).await send_lemmy_activity(context, accept, &user_or_community, inbox, true).await
} }
} }
/// Handle accepted follows /// Handle accepted follows
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
impl ActivityHandler for AcceptFollowCommunity { impl ActivityHandler for AcceptFollow {
type DataType = LemmyContext; type DataType = LemmyContext;
type Error = LemmyError; type Error = LemmyError;

View file

@ -5,9 +5,10 @@ use crate::{
verify_person, verify_person,
verify_person_in_community, verify_person_in_community,
}, },
fetcher::user_or_community::UserOrCommunity,
local_instance, local_instance,
objects::{community::ApubCommunity, person::ApubPerson}, objects::{community::ApubCommunity, person::ApubPerson},
protocol::activities::following::{accept::AcceptFollowCommunity, follow::FollowCommunity}, protocol::activities::following::{accept::AcceptFollow, follow::Follow},
ActorType, ActorType,
}; };
use activitypub_federation::{ use activitypub_federation::{
@ -17,20 +18,23 @@ use activitypub_federation::{
}; };
use activitystreams_kinds::activity::FollowType; use activitystreams_kinds::activity::FollowType;
use lemmy_db_schema::{ use lemmy_db_schema::{
source::community::{CommunityFollower, CommunityFollowerForm}, source::{
community::{CommunityFollower, CommunityFollowerForm},
person::{PersonFollower, PersonFollowerForm},
},
traits::Followable, traits::Followable,
}; };
use lemmy_utils::error::LemmyError; use lemmy_utils::error::LemmyError;
use lemmy_websocket::LemmyContext; use lemmy_websocket::LemmyContext;
use url::Url; use url::Url;
impl FollowCommunity { impl Follow {
pub(in crate::activities::following) fn new( pub(in crate::activities::following) fn new(
actor: &ApubPerson, actor: &ApubPerson,
community: &ApubCommunity, community: &ApubCommunity,
context: &LemmyContext, context: &LemmyContext,
) -> Result<FollowCommunity, LemmyError> { ) -> Result<Follow, LemmyError> {
Ok(FollowCommunity { Ok(Follow {
actor: ObjectId::new(actor.actor_id()), actor: ObjectId::new(actor.actor_id()),
object: ObjectId::new(community.actor_id()), object: ObjectId::new(community.actor_id()),
kind: FollowType::Follow, kind: FollowType::Follow,
@ -56,14 +60,14 @@ impl FollowCommunity {
.await .await
.ok(); .ok();
let follow = FollowCommunity::new(actor, community, context)?; let follow = Follow::new(actor, community, context)?;
let inbox = vec![community.shared_inbox_or_inbox()]; let inbox = vec![community.shared_inbox_or_inbox()];
send_lemmy_activity(context, follow, actor, inbox, true).await send_lemmy_activity(context, follow, actor, inbox, true).await
} }
} }
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
impl ActivityHandler for FollowCommunity { impl ActivityHandler for Follow {
type DataType = LemmyContext; type DataType = LemmyContext;
type Error = LemmyError; type Error = LemmyError;
@ -82,11 +86,13 @@ impl ActivityHandler for FollowCommunity {
request_counter: &mut i32, request_counter: &mut i32,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
verify_person(&self.actor, context, request_counter).await?; verify_person(&self.actor, context, request_counter).await?;
let community = self let object = self
.object .object
.dereference(context, local_instance(context).await, request_counter) .dereference(context, local_instance(context).await, request_counter)
.await?; .await?;
verify_person_in_community(&self.actor, &community, context, request_counter).await?; if let UserOrCommunity::Community(c) = object {
verify_person_in_community(&self.actor, &c, context, request_counter).await?;
}
Ok(()) Ok(())
} }
@ -96,25 +102,33 @@ impl ActivityHandler for FollowCommunity {
context: &Data<LemmyContext>, context: &Data<LemmyContext>,
request_counter: &mut i32, request_counter: &mut i32,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let person = self let actor = self
.actor .actor
.dereference(context, local_instance(context).await, request_counter) .dereference(context, local_instance(context).await, request_counter)
.await?; .await?;
let community = self let object = self
.object .object
.dereference(context, local_instance(context).await, request_counter) .dereference(context, local_instance(context).await, request_counter)
.await?; .await?;
let community_follower_form = CommunityFollowerForm { match object {
community_id: community.id, UserOrCommunity::User(u) => {
person_id: person.id, let form = PersonFollowerForm {
person_id: u.id,
follower_id: actor.id,
pending: false, pending: false,
}; };
PersonFollower::follow(context.pool(), &form).await?;
// This will fail if they're already a follower, but ignore the error. }
CommunityFollower::follow(context.pool(), &community_follower_form) UserOrCommunity::Community(c) => {
.await let form = CommunityFollowerForm {
.ok(); community_id: c.id,
person_id: actor.id,
AcceptFollowCommunity::send(self, context, request_counter).await pending: false,
};
CommunityFollower::follow(context.pool(), &form).await?;
}
}
AcceptFollow::send(self, context, request_counter).await
} }
} }

View file

@ -1,8 +1,9 @@
use crate::{ use crate::{
activities::{generate_activity_id, send_lemmy_activity, verify_person}, activities::{generate_activity_id, send_lemmy_activity, verify_person},
fetcher::user_or_community::UserOrCommunity,
local_instance, local_instance,
objects::{community::ApubCommunity, person::ApubPerson}, objects::{community::ApubCommunity, person::ApubPerson},
protocol::activities::following::{follow::FollowCommunity, undo_follow::UndoFollowCommunity}, protocol::activities::following::{follow::Follow, undo_follow::UndoFollow},
ActorType, ActorType,
}; };
use activitypub_federation::{ use activitypub_federation::{
@ -13,22 +14,25 @@ use activitypub_federation::{
}; };
use activitystreams_kinds::activity::UndoType; use activitystreams_kinds::activity::UndoType;
use lemmy_db_schema::{ use lemmy_db_schema::{
source::community::{CommunityFollower, CommunityFollowerForm}, source::{
community::{CommunityFollower, CommunityFollowerForm},
person::{PersonFollower, PersonFollowerForm},
},
traits::Followable, traits::Followable,
}; };
use lemmy_utils::error::LemmyError; use lemmy_utils::error::LemmyError;
use lemmy_websocket::LemmyContext; use lemmy_websocket::LemmyContext;
use url::Url; use url::Url;
impl UndoFollowCommunity { impl UndoFollow {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn send( pub async fn send(
actor: &ApubPerson, actor: &ApubPerson,
community: &ApubCommunity, community: &ApubCommunity,
context: &LemmyContext, context: &LemmyContext,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let object = FollowCommunity::new(actor, community, context)?; let object = Follow::new(actor, community, context)?;
let undo = UndoFollowCommunity { let undo = UndoFollow {
actor: ObjectId::new(actor.actor_id()), actor: ObjectId::new(actor.actor_id()),
object, object,
kind: UndoType::Undo, kind: UndoType::Undo,
@ -43,7 +47,7 @@ impl UndoFollowCommunity {
} }
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
impl ActivityHandler for UndoFollowCommunity { impl ActivityHandler for UndoFollow {
type DataType = LemmyContext; type DataType = LemmyContext;
type Error = LemmyError; type Error = LemmyError;
@ -77,22 +81,31 @@ impl ActivityHandler for UndoFollowCommunity {
.actor .actor
.dereference(context, local_instance(context).await, request_counter) .dereference(context, local_instance(context).await, request_counter)
.await?; .await?;
let community = self let object = self
.object .object
.object .object
.dereference(context, local_instance(context).await, request_counter) .dereference(context, local_instance(context).await, request_counter)
.await?; .await?;
let community_follower_form = CommunityFollowerForm { match object {
community_id: community.id, UserOrCommunity::User(u) => {
let form = PersonFollowerForm {
person_id: u.id,
follower_id: person.id,
pending: false,
};
PersonFollower::unfollow(context.pool(), &form).await?;
}
UserOrCommunity::Community(c) => {
let form = CommunityFollowerForm {
community_id: c.id,
person_id: person.id, person_id: person.id,
pending: false, pending: false,
}; };
CommunityFollower::unfollow(context.pool(), &form).await?;
}
}
// This will fail if they aren't a follower, but ignore the error.
CommunityFollower::unfollow(context.pool(), &community_follower_form)
.await
.ok();
Ok(()) Ok(())
} }
} }

View file

@ -53,7 +53,7 @@ impl UndoVote {
id: id.clone(), id: id.clone(),
}; };
let activity = AnnouncableActivities::UndoVote(undo_vote); let activity = AnnouncableActivities::UndoVote(undo_vote);
send_activity_in_community(activity, actor, &community, vec![], context).await send_activity_in_community(activity, actor, &community, vec![], false, context).await
} }
} }

View file

@ -52,7 +52,7 @@ impl Vote {
let vote = Vote::new(object, actor, kind, context)?; let vote = Vote::new(object, actor, kind, context)?;
let activity = AnnouncableActivities::Vote(vote); let activity = AnnouncableActivities::Vote(vote);
send_activity_in_community(activity, actor, &community, vec![], context).await send_activity_in_community(activity, actor, &community, vec![], false, context).await
} }
} }

View file

@ -17,11 +17,7 @@ use crate::{
private_message::CreateOrUpdatePrivateMessage, private_message::CreateOrUpdatePrivateMessage,
}, },
deletion::{delete::Delete, delete_user::DeleteUser, undo_delete::UndoDelete}, deletion::{delete::Delete, delete_user::DeleteUser, undo_delete::UndoDelete},
following::{ following::{accept::AcceptFollow, follow::Follow, undo_follow::UndoFollow},
accept::AcceptFollowCommunity,
follow::FollowCommunity,
undo_follow::UndoFollowCommunity,
},
voting::{undo_vote::UndoVote, vote::Vote}, voting::{undo_vote::UndoVote, vote::Vote},
}, },
objects::page::Page, objects::page::Page,
@ -45,8 +41,8 @@ pub enum SharedInboxActivities {
#[serde(untagged)] #[serde(untagged)]
#[enum_delegate::implement(ActivityHandler)] #[enum_delegate::implement(ActivityHandler)]
pub enum GroupInboxActivities { pub enum GroupInboxActivities {
FollowCommunity(FollowCommunity), Follow(Follow),
UndoFollowCommunity(UndoFollowCommunity), UndoFollow(UndoFollow),
Report(Report), Report(Report),
// This is a catch-all and needs to be last // This is a catch-all and needs to be last
AnnouncableActivities(RawAnnouncableActivities), AnnouncableActivities(RawAnnouncableActivities),
@ -56,7 +52,9 @@ pub enum GroupInboxActivities {
#[serde(untagged)] #[serde(untagged)]
#[enum_delegate::implement(ActivityHandler)] #[enum_delegate::implement(ActivityHandler)]
pub enum PersonInboxActivities { pub enum PersonInboxActivities {
AcceptFollowCommunity(AcceptFollowCommunity), AcceptFollow(AcceptFollow),
UndoFollow(UndoFollow),
FollowCommunity(Follow),
CreateOrUpdatePrivateMessage(CreateOrUpdatePrivateMessage), CreateOrUpdatePrivateMessage(CreateOrUpdatePrivateMessage),
Delete(Delete), Delete(Delete),
UndoDelete(UndoDelete), UndoDelete(UndoDelete),

View file

@ -1,6 +1,7 @@
use crate::{ use crate::{
objects::{community::ApubCommunity, person::ApubPerson}, objects::{community::ApubCommunity, person::ApubPerson},
protocol::objects::{group::Group, person::Person}, protocol::objects::{group::Group, person::Person},
ActorType,
}; };
use activitypub_federation::traits::{Actor, ApubObject}; use activitypub_federation::traits::{Actor, ApubObject};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
@ -114,3 +115,19 @@ impl Actor for UserOrCommunity {
unimplemented!() unimplemented!()
} }
} }
impl ActorType for UserOrCommunity {
fn actor_id(&self) -> Url {
match self {
UserOrCommunity::User(u) => u.actor_id(),
UserOrCommunity::Community(c) => c.actor_id(),
}
}
fn private_key(&self) -> Option<String> {
match self {
UserOrCommunity::User(u) => u.private_key(),
UserOrCommunity::Community(c) => c.private_key(),
}
}
}

View file

@ -37,7 +37,7 @@ use std::ops::Deref;
use url::Url; use url::Url;
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct ApubPerson(DbPerson); pub struct ApubPerson(pub(crate) DbPerson);
impl Deref for ApubPerson { impl Deref for ApubPerson {
type Target = DbPerson; type Target = DbPerson;

View file

@ -1,7 +1,4 @@
use crate::{ use crate::{objects::community::ApubCommunity, protocol::activities::following::follow::Follow};
objects::community::ApubCommunity,
protocol::activities::following::follow::FollowCommunity,
};
use activitypub_federation::core::object_id::ObjectId; use activitypub_federation::core::object_id::ObjectId;
use activitystreams_kinds::activity::AcceptType; use activitystreams_kinds::activity::AcceptType;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -9,9 +6,9 @@ use url::Url;
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct AcceptFollowCommunity { pub struct AcceptFollow {
pub(crate) actor: ObjectId<ApubCommunity>, pub(crate) actor: ObjectId<ApubCommunity>,
pub(crate) object: FollowCommunity, pub(crate) object: Follow,
#[serde(rename = "type")] #[serde(rename = "type")]
pub(crate) kind: AcceptType, pub(crate) kind: AcceptType,
pub(crate) id: Url, pub(crate) id: Url,

View file

@ -1,4 +1,4 @@
use crate::objects::{community::ApubCommunity, person::ApubPerson}; use crate::{fetcher::user_or_community::UserOrCommunity, objects::person::ApubPerson};
use activitypub_federation::core::object_id::ObjectId; use activitypub_federation::core::object_id::ObjectId;
use activitystreams_kinds::activity::FollowType; use activitystreams_kinds::activity::FollowType;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -6,9 +6,9 @@ use url::Url;
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct FollowCommunity { pub struct Follow {
pub(crate) actor: ObjectId<ApubPerson>, pub(crate) actor: ObjectId<ApubPerson>,
pub(crate) object: ObjectId<ApubCommunity>, pub(crate) object: ObjectId<UserOrCommunity>,
#[serde(rename = "type")] #[serde(rename = "type")]
pub(crate) kind: FollowType, pub(crate) kind: FollowType,
pub(crate) id: Url, pub(crate) id: Url,

View file

@ -5,23 +5,15 @@ pub mod undo_follow;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::protocol::{ use crate::protocol::{
activities::following::{ activities::following::{accept::AcceptFollow, follow::Follow, undo_follow::UndoFollow},
accept::AcceptFollowCommunity,
follow::FollowCommunity,
undo_follow::UndoFollowCommunity,
},
tests::test_parse_lemmy_item, tests::test_parse_lemmy_item,
}; };
#[test] #[test]
fn test_parse_lemmy_accept_follow() { fn test_parse_lemmy_accept_follow() {
test_parse_lemmy_item::<FollowCommunity>("assets/lemmy/activities/following/follow.json") test_parse_lemmy_item::<Follow>("assets/lemmy/activities/following/follow.json").unwrap();
.unwrap(); test_parse_lemmy_item::<AcceptFollow>("assets/lemmy/activities/following/accept.json").unwrap();
test_parse_lemmy_item::<AcceptFollowCommunity>("assets/lemmy/activities/following/accept.json") test_parse_lemmy_item::<UndoFollow>("assets/lemmy/activities/following/undo_follow.json")
.unwrap();
test_parse_lemmy_item::<UndoFollowCommunity>(
"assets/lemmy/activities/following/undo_follow.json",
)
.unwrap(); .unwrap();
} }
} }

View file

@ -1,7 +1,4 @@
use crate::{ use crate::{objects::person::ApubPerson, protocol::activities::following::follow::Follow};
objects::person::ApubPerson,
protocol::activities::following::follow::FollowCommunity,
};
use activitypub_federation::core::object_id::ObjectId; use activitypub_federation::core::object_id::ObjectId;
use activitystreams_kinds::activity::UndoType; use activitystreams_kinds::activity::UndoType;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -9,9 +6,9 @@ use url::Url;
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct UndoFollowCommunity { pub struct UndoFollow {
pub(crate) actor: ObjectId<ApubPerson>, pub(crate) actor: ObjectId<ApubPerson>,
pub(crate) object: FollowCommunity, pub(crate) object: Follow,
#[serde(rename = "type")] #[serde(rename = "type")]
pub(crate) kind: UndoType, pub(crate) kind: UndoType,
pub(crate) id: Url, pub(crate) id: Url,

View file

@ -21,7 +21,7 @@ mod tests {
community::announce::AnnounceActivity, community::announce::AnnounceActivity,
create_or_update::{comment::CreateOrUpdateComment, post::CreateOrUpdatePost}, create_or_update::{comment::CreateOrUpdateComment, post::CreateOrUpdatePost},
deletion::delete::Delete, deletion::delete::Delete,
following::{follow::FollowCommunity, undo_follow::UndoFollowCommunity}, following::{follow::Follow, undo_follow::UndoFollow},
voting::{undo_vote::UndoVote, vote::Vote}, voting::{undo_vote::UndoVote, vote::Vote},
}, },
tests::test_json, tests::test_json,
@ -36,15 +36,15 @@ mod tests {
fn test_parse_pleroma_activities() { fn test_parse_pleroma_activities() {
test_json::<CreateOrUpdateComment>("assets/pleroma/activities/create_note.json").unwrap(); test_json::<CreateOrUpdateComment>("assets/pleroma/activities/create_note.json").unwrap();
test_json::<Delete>("assets/pleroma/activities/delete.json").unwrap(); test_json::<Delete>("assets/pleroma/activities/delete.json").unwrap();
test_json::<FollowCommunity>("assets/pleroma/activities/follow.json").unwrap(); test_json::<Follow>("assets/pleroma/activities/follow.json").unwrap();
} }
#[test] #[test]
fn test_parse_mastodon_activities() { fn test_parse_mastodon_activities() {
test_json::<CreateOrUpdateComment>("assets/mastodon/activities/create_note.json").unwrap(); test_json::<CreateOrUpdateComment>("assets/mastodon/activities/create_note.json").unwrap();
test_json::<Delete>("assets/mastodon/activities/delete.json").unwrap(); test_json::<Delete>("assets/mastodon/activities/delete.json").unwrap();
test_json::<FollowCommunity>("assets/mastodon/activities/follow.json").unwrap(); test_json::<Follow>("assets/mastodon/activities/follow.json").unwrap();
test_json::<UndoFollowCommunity>("assets/mastodon/activities/undo_follow.json").unwrap(); test_json::<UndoFollow>("assets/mastodon/activities/undo_follow.json").unwrap();
test_json::<Vote>("assets/mastodon/activities/like_page.json").unwrap(); test_json::<Vote>("assets/mastodon/activities/like_page.json").unwrap();
test_json::<UndoVote>("assets/mastodon/activities/undo_like_page.json").unwrap(); test_json::<UndoVote>("assets/mastodon/activities/undo_like_page.json").unwrap();
} }

View file

@ -20,13 +20,7 @@ use crate::{
utils::{functions::lower, get_conn, DbPool}, utils::{functions::lower, get_conn, DbPool},
SubscribedType, SubscribedType,
}; };
use diesel::{ use diesel::{dsl::insert_into, result::Error, ExpressionMethods, QueryDsl, TextExpressionMethods};
dsl::{exists, insert_into},
result::Error,
ExpressionMethods,
QueryDsl,
TextExpressionMethods,
};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
mod safe_type { mod safe_type {
@ -265,7 +259,7 @@ impl CommunityFollower {
pub fn to_subscribed_type(follower: &Option<Self>) -> SubscribedType { pub fn to_subscribed_type(follower: &Option<Self>) -> SubscribedType {
match follower { match follower {
Some(f) => { Some(f) => {
if f.pending.unwrap_or(false) { if f.pending {
SubscribedType::Pending SubscribedType::Pending
} else { } else {
SubscribedType::Subscribed SubscribedType::Subscribed
@ -280,17 +274,14 @@ impl CommunityFollower {
#[async_trait] #[async_trait]
impl Followable for CommunityFollower { impl Followable for CommunityFollower {
type Form = CommunityFollowerForm; type Form = CommunityFollowerForm;
async fn follow( async fn follow(pool: &DbPool, form: &CommunityFollowerForm) -> Result<Self, Error> {
pool: &DbPool,
community_follower_form: &CommunityFollowerForm,
) -> Result<Self, Error> {
use crate::schema::community_follower::dsl::{community_follower, community_id, person_id}; use crate::schema::community_follower::dsl::{community_follower, community_id, person_id};
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
insert_into(community_follower) insert_into(community_follower)
.values(community_follower_form) .values(form)
.on_conflict((community_id, person_id)) .on_conflict((community_id, person_id))
.do_update() .do_update()
.set(community_follower_form) .set(form)
.get_result::<Self>(conn) .get_result::<Self>(conn)
.await .await
} }
@ -315,31 +306,17 @@ impl Followable for CommunityFollower {
.get_result::<Self>(conn) .get_result::<Self>(conn)
.await .await
} }
async fn unfollow( async fn unfollow(pool: &DbPool, form: &CommunityFollowerForm) -> Result<usize, Error> {
pool: &DbPool,
community_follower_form: &CommunityFollowerForm,
) -> Result<usize, Error> {
use crate::schema::community_follower::dsl::{community_follower, community_id, person_id}; use crate::schema::community_follower::dsl::{community_follower, community_id, person_id};
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
diesel::delete( diesel::delete(
community_follower community_follower
.filter(community_id.eq(&community_follower_form.community_id)) .filter(community_id.eq(&form.community_id))
.filter(person_id.eq(&community_follower_form.person_id)), .filter(person_id.eq(&form.person_id)),
) )
.execute(conn) .execute(conn)
.await .await
} }
// TODO: this function name only makes sense if you call it with a remote community. for a local
// community, it will also return true if only remote followers exist
async fn has_local_followers(pool: &DbPool, community_id_: CommunityId) -> Result<bool, Error> {
use crate::schema::community_follower::dsl::{community_follower, community_id};
let conn = &mut get_conn(pool).await?;
diesel::select(exists(
community_follower.filter(community_id.eq(community_id_)),
))
.get_result(conn)
.await
}
} }
#[async_trait] #[async_trait]
@ -472,7 +449,7 @@ mod tests {
id: inserted_community_follower.id, id: inserted_community_follower.id,
community_id: inserted_community.id, community_id: inserted_community.id,
person_id: inserted_person.id, person_id: inserted_person.id,
pending: Some(false), pending: false,
published: inserted_community_follower.published, published: inserted_community_follower.published,
}; };

View file

@ -1,5 +1,5 @@
use crate::{ use crate::{
newtypes::{DbUrl, PersonId}, newtypes::{CommunityId, DbUrl, PersonId},
schema::person::dsl::{ schema::person::dsl::{
actor_id, actor_id,
avatar, avatar,
@ -13,8 +13,14 @@ use crate::{
person, person,
updated, updated,
}, },
source::person::{Person, PersonInsertForm, PersonUpdateForm}, source::person::{
traits::{ApubActor, Crud}, Person,
PersonFollower,
PersonFollowerForm,
PersonInsertForm,
PersonUpdateForm,
},
traits::{ApubActor, Crud, Followable},
utils::{functions::lower, get_conn, naive_now, DbPool}, utils::{functions::lower, get_conn, naive_now, DbPool},
}; };
use diesel::{dsl::insert_into, result::Error, ExpressionMethods, QueryDsl, TextExpressionMethods}; use diesel::{dsl::insert_into, result::Error, ExpressionMethods, QueryDsl, TextExpressionMethods};
@ -219,14 +225,57 @@ impl ApubActor for Person {
} }
} }
#[async_trait]
impl Followable for PersonFollower {
type Form = PersonFollowerForm;
async fn follow(pool: &DbPool, form: &PersonFollowerForm) -> Result<Self, Error> {
use crate::schema::person_follower::dsl::{follower_id, person_follower, person_id};
let conn = &mut get_conn(pool).await?;
insert_into(person_follower)
.values(form)
.on_conflict((follower_id, person_id))
.do_update()
.set(form)
.get_result::<Self>(conn)
.await
}
async fn follow_accepted(_: &DbPool, _: CommunityId, _: PersonId) -> Result<Self, Error> {
unimplemented!()
}
async fn unfollow(pool: &DbPool, form: &PersonFollowerForm) -> Result<usize, Error> {
use crate::schema::person_follower::dsl::{follower_id, person_follower, person_id};
let conn = &mut get_conn(pool).await?;
diesel::delete(
person_follower
.filter(follower_id.eq(&form.follower_id))
.filter(person_id.eq(&form.person_id)),
)
.execute(conn)
.await
}
}
impl PersonFollower {
pub async fn list_followers(pool: &DbPool, person_id_: PersonId) -> Result<Vec<Person>, Error> {
use crate::schema::{person, person_follower, person_follower::person_id};
let conn = &mut get_conn(pool).await?;
person_follower::table
.inner_join(person::table)
.filter(person_id.eq(person_id_))
.select(person::all_columns)
.load(conn)
.await
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{ use crate::{
source::{ source::{
instance::Instance, instance::Instance,
person::{Person, PersonInsertForm, PersonUpdateForm}, person::{Person, PersonFollower, PersonFollowerForm, PersonInsertForm, PersonUpdateForm},
}, },
traits::Crud, traits::{Crud, Followable},
utils::build_db_pool_for_tests, utils::build_db_pool_for_tests,
}; };
use serial_test::serial; use serial_test::serial;
@ -288,4 +337,42 @@ mod tests {
assert_eq!(expected_person, updated_person); assert_eq!(expected_person, updated_person);
assert_eq!(1, num_deleted); assert_eq!(1, num_deleted);
} }
#[tokio::test]
#[serial]
async fn follow() {
let pool = &build_db_pool_for_tests().await;
let inserted_instance = Instance::create(pool, "my_domain.tld").await.unwrap();
let person_form_1 = PersonInsertForm::builder()
.name("erich".into())
.public_key("pubkey".to_string())
.instance_id(inserted_instance.id)
.build();
let person_1 = Person::create(pool, &person_form_1).await.unwrap();
let person_form_2 = PersonInsertForm::builder()
.name("michele".into())
.public_key("pubkey".to_string())
.instance_id(inserted_instance.id)
.build();
let person_2 = Person::create(pool, &person_form_2).await.unwrap();
let follow_form = PersonFollowerForm {
person_id: person_1.id,
follower_id: person_2.id,
pending: false,
};
let person_follower = PersonFollower::follow(pool, &follow_form).await.unwrap();
assert_eq!(person_1.id, person_follower.person_id);
assert_eq!(person_2.id, person_follower.follower_id);
assert!(!person_follower.pending);
let followers = PersonFollower::list_followers(pool, person_1.id)
.await
.unwrap();
assert_eq!(vec![person_2], followers);
let unfollow = PersonFollower::unfollow(pool, &follow_form).await.unwrap();
assert_eq!(1, unfollow);
}
} }

View file

@ -125,7 +125,7 @@ table! {
community_id -> Int4, community_id -> Int4,
person_id -> Int4, person_id -> Int4,
published -> Timestamp, published -> Timestamp,
pending -> Nullable<Bool>, pending -> Bool,
} }
} }
@ -729,6 +729,16 @@ table! {
} }
} }
table! {
person_follower (id) {
id -> Int4,
person_id -> Int4,
follower_id -> Int4,
published -> Timestamp,
pending -> Bool,
}
}
joinable!(person_block -> person (person_id)); joinable!(person_block -> person (person_id));
joinable!(comment -> person (creator_id)); joinable!(comment -> person (creator_id));
@ -797,6 +807,7 @@ joinable!(site_language -> language (language_id));
joinable!(site_language -> site (site_id)); joinable!(site_language -> site (site_id));
joinable!(community_language -> language (language_id)); joinable!(community_language -> language (language_id));
joinable!(community_language -> community (community_id)); joinable!(community_language -> community (community_id));
joinable!(person_follower -> person (follower_id));
joinable!(admin_purge_comment -> person (admin_person_id)); joinable!(admin_purge_comment -> person (admin_person_id));
joinable!(admin_purge_comment -> post (post_id)); joinable!(admin_purge_comment -> post (post_id));
@ -873,4 +884,5 @@ allow_tables_to_appear_in_same_query!(
federation_blocklist, federation_blocklist,
local_site, local_site,
local_site_rate_limit, local_site_rate_limit,
person_follower
); );

View file

@ -170,7 +170,7 @@ pub struct CommunityFollower {
pub community_id: CommunityId, pub community_id: CommunityId,
pub person_id: PersonId, pub person_id: PersonId,
pub published: chrono::NaiveDateTime, pub published: chrono::NaiveDateTime,
pub pending: Option<bool>, pub pending: bool,
} }
#[derive(Clone)] #[derive(Clone)]

View file

@ -1,6 +1,6 @@
use crate::newtypes::{DbUrl, InstanceId, PersonId}; use crate::newtypes::{DbUrl, InstanceId, PersonId};
#[cfg(feature = "full")] #[cfg(feature = "full")]
use crate::schema::person; use crate::schema::{person, person_follower};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use typed_builder::TypedBuilder; use typed_builder::TypedBuilder;
@ -113,3 +113,24 @@ pub struct PersonUpdateForm {
pub bot_account: Option<bool>, pub bot_account: Option<bool>,
pub ban_expires: Option<Option<chrono::NaiveDateTime>>, pub ban_expires: Option<Option<chrono::NaiveDateTime>>,
} }
#[derive(PartialEq, Eq, Debug)]
#[cfg_attr(feature = "full", derive(Identifiable, Queryable, Associations))]
#[cfg_attr(feature = "full", diesel(belongs_to(crate::source::person::Person)))]
#[cfg_attr(feature = "full", diesel(table_name = person_follower))]
pub struct PersonFollower {
pub id: i32,
pub person_id: PersonId,
pub follower_id: PersonId,
pub published: chrono::NaiveDateTime,
pub pending: bool,
}
#[derive(Clone)]
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
#[cfg_attr(feature = "full", diesel(table_name = person_follower))]
pub struct PersonFollowerForm {
pub person_id: PersonId,
pub follower_id: PersonId,
pub pending: bool,
}

View file

@ -44,7 +44,6 @@ pub trait Followable {
async fn unfollow(pool: &DbPool, form: &Self::Form) -> Result<usize, Error> async fn unfollow(pool: &DbPool, form: &Self::Form) -> Result<usize, Error>
where where
Self: Sized; Self: Sized;
async fn has_local_followers(pool: &DbPool, community_id: CommunityId) -> Result<bool, Error>;
} }
#[async_trait] #[async_trait]

View file

@ -0,0 +1,3 @@
drop table person_follower;
alter table community_follower alter column pending drop not null;

View file

@ -0,0 +1,12 @@
-- create user follower table with two references to persons
create table person_follower (
id serial primary key,
person_id int references person on update cascade on delete cascade not null,
follower_id int references person on update cascade on delete cascade not null,
published timestamp not null default now(),
pending boolean not null,
unique (follower_id, person_id)
);
update community_follower set pending = false where pending is null;
alter table community_follower alter column pending set not null;