community::{BlockCommunity, BlockCommunityResponse},
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::{
source::{
community::{Community, CommunityFollower, CommunityFollowerForm},
.await
.ok();
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 {
CommunityBlock::unblock(context.pool(), &community_block_form)
.await
use lemmy_apub::{
objects::community::ApubCommunity,
protocol::activities::following::{
- follow::FollowCommunity as FollowCommunityApub,
- undo_follow::UndoFollowCommunity,
+ follow::Follow as FollowCommunityApub,
+ undo_follow::UndoFollow,
},
};
use lemmy_db_schema::{
FollowCommunityApub::send(&local_user_view.person.clone().into(), &community, context)
.await?;
} else {
- UndoFollowCommunity::send(&local_user_view.person.clone().into(), &community, context)
- .await?;
+ UndoFollow::send(&local_user_view.person.clone().into(), &community, context).await?;
CommunityFollower::unfollow(context.pool(), &community_follower_form)
.await
.map_err(|e| LemmyError::from_error_message(e, "community_follower_already_exists"))?;
SiteOrCommunity::Community(c) => {
let activity = AnnouncableActivities::BlockUser(block);
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
}
}
}
}
SiteOrCommunity::Community(c) => {
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
}
}
}
let activity = AnnouncableActivities::AddMod(add);
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
}
}
activities::send_lemmy_activity,
activity_lists::AnnouncableActivities,
local_instance,
- objects::community::ApubCommunity,
+ objects::{community::ApubCommunity, person::ApubPerson},
protocol::activities::community::announce::AnnounceActivity,
- ActorType,
};
use activitypub_federation::{core::object_id::ObjectId, traits::Actor};
+use lemmy_db_schema::source::person::PersonFollower;
use lemmy_utils::error::LemmyError;
use lemmy_websocket::LemmyContext;
use url::Url;
pub mod report;
pub mod update;
-#[tracing::instrument(skip_all)]
-pub(crate) async fn send_activity_in_community<ActorT>(
+/// This function sends all activities which are happening in a community to the right inboxes.
+/// 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,
- actor: &ActorT,
+ actor: &ApubPerson,
community: &ApubCommunity,
- mut inboxes: Vec<Url>,
+ extra_inboxes: Vec<Url>,
+ is_mod_action: bool,
context: &LemmyContext,
-) -> Result<(), LemmyError>
-where
- ActorT: Actor + ActorType,
-{
- inboxes.push(community.shared_inbox_or_inbox());
- send_lemmy_activity(context, activity.clone(), actor, inboxes, false).await?;
+) -> Result<(), LemmyError> {
+ // send to extra_inboxes
+ send_lemmy_activity(context, activity.clone(), actor, extra_inboxes, false).await?;
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(())
let activity = AnnouncableActivities::RemoveMod(remove);
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
}
}
};
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
}
}
}
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
}
}
let community: ApubCommunity = Community::read(context.pool(), community_id).await?.into();
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);
- send_activity_in_community(activity, actor, &community, vec![], context).await?;
+ send_activity_in_community(activity, actor, &community, vec![], is_mod_action, context).await?;
Ok(())
}
}
context: &LemmyContext,
) -> Result<(), LemmyError> {
let actor = ApubPerson::from(actor);
+ let is_mod_action = reason.is_some();
let activity = if deleted {
let delete = Delete::new(&actor, object, public(), Some(&community), reason, context)?;
AnnouncableActivities::Delete(delete)
let undo = UndoDelete::new(&actor, object, public(), Some(&community), reason, context)?;
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)]
use crate::{
activities::{generate_activity_id, send_lemmy_activity},
local_instance,
- protocol::activities::following::{accept::AcceptFollowCommunity, follow::FollowCommunity},
+ protocol::activities::following::{accept::AcceptFollow, follow::Follow},
ActorType,
};
use activitypub_federation::{
use lemmy_websocket::{messages::SendUserRoomMessage, LemmyContext, UserOperation};
use url::Url;
-impl AcceptFollowCommunity {
+impl AcceptFollow {
#[tracing::instrument(skip_all)]
pub async fn send(
- follow: FollowCommunity,
+ follow: Follow,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
- let community = follow.object.dereference_local(context).await?;
+ let user_or_community = follow.object.dereference_local(context).await?;
let person = follow
.actor
.clone()
.dereference(context, local_instance(context).await, request_counter)
.await?;
- let accept = AcceptFollowCommunity {
- actor: ObjectId::new(community.actor_id()),
+ let accept = AcceptFollow {
+ actor: ObjectId::new(user_or_community.actor_id()),
object: follow,
kind: AcceptType::Accept,
id: generate_activity_id(
)?,
};
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
#[async_trait::async_trait(?Send)]
-impl ActivityHandler for AcceptFollowCommunity {
+impl ActivityHandler for AcceptFollow {
type DataType = LemmyContext;
type Error = LemmyError;
verify_person,
verify_person_in_community,
},
+ fetcher::user_or_community::UserOrCommunity,
local_instance,
objects::{community::ApubCommunity, person::ApubPerson},
- protocol::activities::following::{accept::AcceptFollowCommunity, follow::FollowCommunity},
+ protocol::activities::following::{accept::AcceptFollow, follow::Follow},
ActorType,
};
use activitypub_federation::{
};
use activitystreams_kinds::activity::FollowType;
use lemmy_db_schema::{
- source::community::{CommunityFollower, CommunityFollowerForm},
+ source::{
+ community::{CommunityFollower, CommunityFollowerForm},
+ person::{PersonFollower, PersonFollowerForm},
+ },
traits::Followable,
};
use lemmy_utils::error::LemmyError;
use lemmy_websocket::LemmyContext;
use url::Url;
-impl FollowCommunity {
+impl Follow {
pub(in crate::activities::following) fn new(
actor: &ApubPerson,
community: &ApubCommunity,
context: &LemmyContext,
- ) -> Result<FollowCommunity, LemmyError> {
- Ok(FollowCommunity {
+ ) -> Result<Follow, LemmyError> {
+ Ok(Follow {
actor: ObjectId::new(actor.actor_id()),
object: ObjectId::new(community.actor_id()),
kind: FollowType::Follow,
.await
.ok();
- let follow = FollowCommunity::new(actor, community, context)?;
+ let follow = Follow::new(actor, community, context)?;
let inbox = vec![community.shared_inbox_or_inbox()];
send_lemmy_activity(context, follow, actor, inbox, true).await
}
}
#[async_trait::async_trait(?Send)]
-impl ActivityHandler for FollowCommunity {
+impl ActivityHandler for Follow {
type DataType = LemmyContext;
type Error = LemmyError;
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_person(&self.actor, context, request_counter).await?;
- let community = self
+ let object = self
.object
.dereference(context, local_instance(context).await, request_counter)
.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(())
}
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
- let person = self
+ let actor = self
.actor
.dereference(context, local_instance(context).await, request_counter)
.await?;
- let community = self
+ let object = self
.object
.dereference(context, local_instance(context).await, request_counter)
.await?;
- let community_follower_form = CommunityFollowerForm {
- community_id: community.id,
- person_id: person.id,
- pending: false,
- };
-
- // This will fail if they're already a follower, but ignore the error.
- CommunityFollower::follow(context.pool(), &community_follower_form)
- .await
- .ok();
+ match object {
+ UserOrCommunity::User(u) => {
+ let form = PersonFollowerForm {
+ person_id: u.id,
+ follower_id: actor.id,
+ pending: false,
+ };
+ PersonFollower::follow(context.pool(), &form).await?;
+ }
+ UserOrCommunity::Community(c) => {
+ let form = CommunityFollowerForm {
+ community_id: c.id,
+ person_id: actor.id,
+ pending: false,
+ };
+ CommunityFollower::follow(context.pool(), &form).await?;
+ }
+ }
- AcceptFollowCommunity::send(self, context, request_counter).await
+ AcceptFollow::send(self, context, request_counter).await
}
}
use crate::{
activities::{generate_activity_id, send_lemmy_activity, verify_person},
+ fetcher::user_or_community::UserOrCommunity,
local_instance,
objects::{community::ApubCommunity, person::ApubPerson},
- protocol::activities::following::{follow::FollowCommunity, undo_follow::UndoFollowCommunity},
+ protocol::activities::following::{follow::Follow, undo_follow::UndoFollow},
ActorType,
};
use activitypub_federation::{
};
use activitystreams_kinds::activity::UndoType;
use lemmy_db_schema::{
- source::community::{CommunityFollower, CommunityFollowerForm},
+ source::{
+ community::{CommunityFollower, CommunityFollowerForm},
+ person::{PersonFollower, PersonFollowerForm},
+ },
traits::Followable,
};
use lemmy_utils::error::LemmyError;
use lemmy_websocket::LemmyContext;
use url::Url;
-impl UndoFollowCommunity {
+impl UndoFollow {
#[tracing::instrument(skip_all)]
pub async fn send(
actor: &ApubPerson,
community: &ApubCommunity,
context: &LemmyContext,
) -> Result<(), LemmyError> {
- let object = FollowCommunity::new(actor, community, context)?;
- let undo = UndoFollowCommunity {
+ let object = Follow::new(actor, community, context)?;
+ let undo = UndoFollow {
actor: ObjectId::new(actor.actor_id()),
object,
kind: UndoType::Undo,
}
#[async_trait::async_trait(?Send)]
-impl ActivityHandler for UndoFollowCommunity {
+impl ActivityHandler for UndoFollow {
type DataType = LemmyContext;
type Error = LemmyError;
.actor
.dereference(context, local_instance(context).await, request_counter)
.await?;
- let community = self
+ let object = self
.object
.object
.dereference(context, local_instance(context).await, request_counter)
.await?;
- let community_follower_form = CommunityFollowerForm {
- community_id: community.id,
- person_id: person.id,
- pending: false,
- };
+ match object {
+ 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,
+ 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(())
}
}
id: id.clone(),
};
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
}
}
let vote = Vote::new(object, actor, kind, context)?;
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
}
}
private_message::CreateOrUpdatePrivateMessage,
},
deletion::{delete::Delete, delete_user::DeleteUser, undo_delete::UndoDelete},
- following::{
- accept::AcceptFollowCommunity,
- follow::FollowCommunity,
- undo_follow::UndoFollowCommunity,
- },
+ following::{accept::AcceptFollow, follow::Follow, undo_follow::UndoFollow},
voting::{undo_vote::UndoVote, vote::Vote},
},
objects::page::Page,
#[serde(untagged)]
#[enum_delegate::implement(ActivityHandler)]
pub enum GroupInboxActivities {
- FollowCommunity(FollowCommunity),
- UndoFollowCommunity(UndoFollowCommunity),
+ Follow(Follow),
+ UndoFollow(UndoFollow),
Report(Report),
// This is a catch-all and needs to be last
AnnouncableActivities(RawAnnouncableActivities),
#[serde(untagged)]
#[enum_delegate::implement(ActivityHandler)]
pub enum PersonInboxActivities {
- AcceptFollowCommunity(AcceptFollowCommunity),
+ AcceptFollow(AcceptFollow),
+ UndoFollow(UndoFollow),
+ FollowCommunity(Follow),
CreateOrUpdatePrivateMessage(CreateOrUpdatePrivateMessage),
Delete(Delete),
UndoDelete(UndoDelete),
use crate::{
objects::{community::ApubCommunity, person::ApubPerson},
protocol::objects::{group::Group, person::Person},
+ ActorType,
};
use activitypub_federation::traits::{Actor, ApubObject};
use chrono::NaiveDateTime;
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(),
+ }
+ }
+}
use url::Url;
#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct ApubPerson(DbPerson);
+pub struct ApubPerson(pub(crate) DbPerson);
impl Deref for ApubPerson {
type Target = DbPerson;
-use crate::{
- objects::community::ApubCommunity,
- protocol::activities::following::follow::FollowCommunity,
-};
+use crate::{objects::community::ApubCommunity, protocol::activities::following::follow::Follow};
use activitypub_federation::core::object_id::ObjectId;
use activitystreams_kinds::activity::AcceptType;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
-pub struct AcceptFollowCommunity {
+pub struct AcceptFollow {
pub(crate) actor: ObjectId<ApubCommunity>,
- pub(crate) object: FollowCommunity,
+ pub(crate) object: Follow,
#[serde(rename = "type")]
pub(crate) kind: AcceptType,
pub(crate) id: Url,
-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 activitystreams_kinds::activity::FollowType;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
-pub struct FollowCommunity {
+pub struct Follow {
pub(crate) actor: ObjectId<ApubPerson>,
- pub(crate) object: ObjectId<ApubCommunity>,
+ pub(crate) object: ObjectId<UserOrCommunity>,
#[serde(rename = "type")]
pub(crate) kind: FollowType,
pub(crate) id: Url,
#[cfg(test)]
mod tests {
use crate::protocol::{
- activities::following::{
- accept::AcceptFollowCommunity,
- follow::FollowCommunity,
- undo_follow::UndoFollowCommunity,
- },
+ activities::following::{accept::AcceptFollow, follow::Follow, undo_follow::UndoFollow},
tests::test_parse_lemmy_item,
};
#[test]
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();
+ test_parse_lemmy_item::<AcceptFollow>("assets/lemmy/activities/following/accept.json").unwrap();
+ test_parse_lemmy_item::<UndoFollow>("assets/lemmy/activities/following/undo_follow.json")
.unwrap();
- test_parse_lemmy_item::<AcceptFollowCommunity>("assets/lemmy/activities/following/accept.json")
- .unwrap();
- test_parse_lemmy_item::<UndoFollowCommunity>(
- "assets/lemmy/activities/following/undo_follow.json",
- )
- .unwrap();
}
}
-use crate::{
- objects::person::ApubPerson,
- protocol::activities::following::follow::FollowCommunity,
-};
+use crate::{objects::person::ApubPerson, protocol::activities::following::follow::Follow};
use activitypub_federation::core::object_id::ObjectId;
use activitystreams_kinds::activity::UndoType;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
-pub struct UndoFollowCommunity {
+pub struct UndoFollow {
pub(crate) actor: ObjectId<ApubPerson>,
- pub(crate) object: FollowCommunity,
+ pub(crate) object: Follow,
#[serde(rename = "type")]
pub(crate) kind: UndoType,
pub(crate) id: Url,
community::announce::AnnounceActivity,
create_or_update::{comment::CreateOrUpdateComment, post::CreateOrUpdatePost},
deletion::delete::Delete,
- following::{follow::FollowCommunity, undo_follow::UndoFollowCommunity},
+ following::{follow::Follow, undo_follow::UndoFollow},
voting::{undo_vote::UndoVote, vote::Vote},
},
tests::test_json,
fn test_parse_pleroma_activities() {
test_json::<CreateOrUpdateComment>("assets/pleroma/activities/create_note.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]
fn test_parse_mastodon_activities() {
test_json::<CreateOrUpdateComment>("assets/mastodon/activities/create_note.json").unwrap();
test_json::<Delete>("assets/mastodon/activities/delete.json").unwrap();
- test_json::<FollowCommunity>("assets/mastodon/activities/follow.json").unwrap();
- test_json::<UndoFollowCommunity>("assets/mastodon/activities/undo_follow.json").unwrap();
+ test_json::<Follow>("assets/mastodon/activities/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::<UndoVote>("assets/mastodon/activities/undo_like_page.json").unwrap();
}
utils::{functions::lower, get_conn, DbPool},
SubscribedType,
};
-use diesel::{
- dsl::{exists, insert_into},
- result::Error,
- ExpressionMethods,
- QueryDsl,
- TextExpressionMethods,
-};
+use diesel::{dsl::insert_into, result::Error, ExpressionMethods, QueryDsl, TextExpressionMethods};
use diesel_async::RunQueryDsl;
mod safe_type {
pub fn to_subscribed_type(follower: &Option<Self>) -> SubscribedType {
match follower {
Some(f) => {
- if f.pending.unwrap_or(false) {
+ if f.pending {
SubscribedType::Pending
} else {
SubscribedType::Subscribed
#[async_trait]
impl Followable for CommunityFollower {
type Form = CommunityFollowerForm;
- async fn follow(
- pool: &DbPool,
- community_follower_form: &CommunityFollowerForm,
- ) -> Result<Self, Error> {
+ async fn follow(pool: &DbPool, form: &CommunityFollowerForm) -> Result<Self, Error> {
use crate::schema::community_follower::dsl::{community_follower, community_id, person_id};
let conn = &mut get_conn(pool).await?;
insert_into(community_follower)
- .values(community_follower_form)
+ .values(form)
.on_conflict((community_id, person_id))
.do_update()
- .set(community_follower_form)
+ .set(form)
.get_result::<Self>(conn)
.await
}
.get_result::<Self>(conn)
.await
}
- async fn unfollow(
- pool: &DbPool,
- community_follower_form: &CommunityFollowerForm,
- ) -> Result<usize, Error> {
+ async fn unfollow(pool: &DbPool, form: &CommunityFollowerForm) -> Result<usize, Error> {
use crate::schema::community_follower::dsl::{community_follower, community_id, person_id};
let conn = &mut get_conn(pool).await?;
diesel::delete(
community_follower
- .filter(community_id.eq(&community_follower_form.community_id))
- .filter(person_id.eq(&community_follower_form.person_id)),
+ .filter(community_id.eq(&form.community_id))
+ .filter(person_id.eq(&form.person_id)),
)
.execute(conn)
.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]
id: inserted_community_follower.id,
community_id: inserted_community.id,
person_id: inserted_person.id,
- pending: Some(false),
+ pending: false,
published: inserted_community_follower.published,
};
use crate::{
- newtypes::{DbUrl, PersonId},
+ newtypes::{CommunityId, DbUrl, PersonId},
schema::person::dsl::{
actor_id,
avatar,
person,
updated,
},
- source::person::{Person, PersonInsertForm, PersonUpdateForm},
- traits::{ApubActor, Crud},
+ source::person::{
+ Person,
+ PersonFollower,
+ PersonFollowerForm,
+ PersonInsertForm,
+ PersonUpdateForm,
+ },
+ traits::{ApubActor, Crud, Followable},
utils::{functions::lower, get_conn, naive_now, DbPool},
};
use diesel::{dsl::insert_into, result::Error, ExpressionMethods, QueryDsl, TextExpressionMethods};
}
}
+#[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)]
mod tests {
use crate::{
source::{
instance::Instance,
- person::{Person, PersonInsertForm, PersonUpdateForm},
+ person::{Person, PersonFollower, PersonFollowerForm, PersonInsertForm, PersonUpdateForm},
},
- traits::Crud,
+ traits::{Crud, Followable},
utils::build_db_pool_for_tests,
};
use serial_test::serial;
assert_eq!(expected_person, updated_person);
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);
+ }
}
community_id -> Int4,
person_id -> Int4,
published -> Timestamp,
- pending -> Nullable<Bool>,
+ pending -> Bool,
}
}
}
}
+table! {
+ person_follower (id) {
+ id -> Int4,
+ person_id -> Int4,
+ follower_id -> Int4,
+ published -> Timestamp,
+ pending -> Bool,
+ }
+}
+
joinable!(person_block -> person (person_id));
joinable!(comment -> person (creator_id));
joinable!(site_language -> site (site_id));
joinable!(community_language -> language (language_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 -> post (post_id));
federation_blocklist,
local_site,
local_site_rate_limit,
+ person_follower
);
pub community_id: CommunityId,
pub person_id: PersonId,
pub published: chrono::NaiveDateTime,
- pub pending: Option<bool>,
+ pub pending: bool,
}
#[derive(Clone)]
use crate::newtypes::{DbUrl, InstanceId, PersonId};
#[cfg(feature = "full")]
-use crate::schema::person;
+use crate::schema::{person, person_follower};
use serde::{Deserialize, Serialize};
use typed_builder::TypedBuilder;
pub bot_account: Option<bool>,
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,
+}
async fn unfollow(pool: &DbPool, form: &Self::Form) -> Result<usize, Error>
where
Self: Sized;
- async fn has_local_followers(pool: &DbPool, community_id: CommunityId) -> Result<bool, Error>;
}
#[async_trait]
--- /dev/null
+drop table person_follower;
+
+alter table community_follower alter column pending drop not null;
--- /dev/null
+-- 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;