use crate::{
activities::{
- comment::{create::CreateComment, update::UpdateComment},
- community::{
- add_mod::AddMod,
- block_user::BlockUserFromCommunity,
- list_community_follower_inboxes,
- undo_block_user::UndoBlockUserFromCommunity,
- },
- deletion::{
- delete::DeletePostCommentOrCommunity,
- undo_delete::UndoDeletePostCommentOrCommunity,
- },
generate_activity_id,
- post::{create::CreatePost, update::UpdatePost},
- removal::{
- remove::RemovePostCommentCommunityOrMod,
- undo_remove::UndoRemovePostCommentOrCommunity,
- },
- verify_activity,
- verify_community,
- voting::{
- dislike::DislikePostOrComment,
- like::LikePostOrComment,
- undo_dislike::UndoDislikePostOrComment,
- undo_like::UndoLikePostOrComment,
- },
+ send_lemmy_activity,
+ verify_is_public,
+ verify_person_in_community,
},
- activity_queue::send_activity_new,
- extensions::context::lemmy_context,
- http::is_activity_already_known,
- insert_activity,
- ActorType,
- CommunityType,
+ activity_lists::AnnouncableActivities,
+ insert_received_activity,
+ objects::community::ApubCommunity,
+ protocol::{
+ activities::community::announce::{AnnounceActivity, RawAnnouncableActivities},
+ Id,
+ IdOrNestedObject,
+ InCommunity,
+ },
+};
+use activitypub_federation::{
+ config::Data,
+ kinds::{activity::AnnounceType, public},
+ traits::{ActivityHandler, Actor},
};
-use activitystreams::activity::kind::AnnounceType;
-use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl};
-use lemmy_db_schema::source::community::Community;
-use lemmy_utils::LemmyError;
-use lemmy_websocket::LemmyContext;
-use serde::{Deserialize, Serialize};
+use lemmy_api_common::context::LemmyContext;
+use lemmy_utils::error::{LemmyError, LemmyErrorType};
+use serde_json::Value;
use url::Url;
-#[derive(Clone, Debug, Deserialize, Serialize, ActivityHandler)]
-#[serde(untagged)]
-pub enum AnnouncableActivities {
- CreateComment(CreateComment),
- UpdateComment(UpdateComment),
- CreatePost(CreatePost),
- UpdatePost(UpdatePost),
- LikePostOrComment(LikePostOrComment),
- DislikePostOrComment(DislikePostOrComment),
- UndoLikePostOrComment(UndoLikePostOrComment),
- UndoDislikePostOrComment(UndoDislikePostOrComment),
- DeletePostCommentOrCommunity(DeletePostCommentOrCommunity),
- UndoDeletePostCommentOrCommunity(UndoDeletePostCommentOrCommunity),
- RemovePostCommentCommunityOrMod(RemovePostCommentCommunityOrMod),
- UndoRemovePostCommentOrCommunity(UndoRemovePostCommentOrCommunity),
- BlockUserFromCommunity(BlockUserFromCommunity),
- UndoBlockUserFromCommunity(UndoBlockUserFromCommunity),
- AddMod(AddMod),
-}
+#[async_trait::async_trait]
+impl ActivityHandler for RawAnnouncableActivities {
+ type DataType = LemmyContext;
+ type Error = LemmyError;
+
+ fn id(&self) -> &Url {
+ &self.id
+ }
+
+ fn actor(&self) -> &Url {
+ &self.actor
+ }
+
+ #[tracing::instrument(skip_all)]
+ async fn verify(&self, _data: &Data<Self::DataType>) -> Result<(), Self::Error> {
+ Ok(())
+ }
-#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct AnnounceActivity {
- to: PublicUrl,
- object: AnnouncableActivities,
- cc: Vec<Url>,
- #[serde(rename = "type")]
- kind: AnnounceType,
- #[serde(flatten)]
- common: ActivityCommonFields,
+ #[tracing::instrument(skip_all)]
+ async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
+ let activity: AnnouncableActivities = self.clone().try_into()?;
+ // This is only for sending, not receiving so we reject it.
+ if let AnnouncableActivities::Page(_) = activity {
+ return Err(LemmyErrorType::CannotReceivePage)?;
+ }
+ let community = activity.community(data).await?;
+ let actor_id = activity.actor().clone().into();
+
+ // verify and receive activity
+ activity.verify(data).await?;
+ activity.receive(data).await?;
+
+ // send to community followers
+ if community.local {
+ verify_person_in_community(&actor_id, &community, data).await?;
+ AnnounceActivity::send(self, &community, data).await?;
+ }
+ Ok(())
+ }
}
impl AnnounceActivity {
+ pub(crate) fn new(
+ object: RawAnnouncableActivities,
+ community: &ApubCommunity,
+ context: &Data<LemmyContext>,
+ ) -> Result<AnnounceActivity, LemmyError> {
+ Ok(AnnounceActivity {
+ actor: community.id().into(),
+ to: vec![public()],
+ object: IdOrNestedObject::NestedObject(object),
+ cc: vec![community.followers_url.clone().into()],
+ kind: AnnounceType::Announce,
+ id: generate_activity_id(
+ &AnnounceType::Announce,
+ &context.settings().get_protocol_and_hostname(),
+ )?,
+ })
+ }
+
+ #[tracing::instrument(skip_all)]
pub async fn send(
- object: AnnouncableActivities,
- community: &Community,
- additional_inboxes: Vec<Url>,
- context: &LemmyContext,
+ object: RawAnnouncableActivities,
+ community: &ApubCommunity,
+ context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
- let announce = AnnounceActivity {
- to: PublicUrl::Public,
- object,
- cc: vec![community.followers_url()],
- kind: AnnounceType::Announce,
- common: ActivityCommonFields {
- context: lemmy_context()?.into(),
- id: generate_activity_id(AnnounceType::Announce)?,
- actor: community.actor_id(),
- unparsed: Default::default(),
- },
- };
- let inboxes = list_community_follower_inboxes(community, additional_inboxes, context).await?;
- send_activity_new(
- context,
- &announce,
- &announce.common.id,
- community,
- inboxes,
- false,
- )
- .await
+ let announce = AnnounceActivity::new(object.clone(), community, context)?;
+ let inboxes = community.get_follower_inboxes(context).await?;
+ send_lemmy_activity(context, announce, community, inboxes.clone(), false).await?;
+
+ // Pleroma and Mastodon can't handle activities like Announce/Create/Page. So for
+ // compatibility, we also send Announce/Page so that they can follow Lemmy communities.
+ let object_parsed = object.try_into()?;
+ if let AnnouncableActivities::CreateOrUpdatePost(c) = object_parsed {
+ // Hack: need to convert Page into a format which can be sent as activity, which requires
+ // adding actor field.
+ let announcable_page = RawAnnouncableActivities {
+ id: generate_activity_id(
+ AnnounceType::Announce,
+ &context.settings().get_protocol_and_hostname(),
+ )?,
+ actor: c.actor.clone().into_inner(),
+ other: serde_json::to_value(c.object)?
+ .as_object()
+ .expect("is object")
+ .clone(),
+ };
+ let announce_compat = AnnounceActivity::new(announcable_page, community, context)?;
+ send_lemmy_activity(context, announce_compat, community, inboxes, false).await?;
+ }
+ Ok(())
}
}
-#[async_trait::async_trait(?Send)]
+#[async_trait::async_trait]
impl ActivityHandler for AnnounceActivity {
- async fn verify(
- &self,
- context: &LemmyContext,
- request_counter: &mut i32,
- ) -> Result<(), LemmyError> {
- verify_activity(self.common())?;
- verify_community(&self.common.actor, context, request_counter).await?;
- self.object.verify(context, request_counter).await?;
+ type DataType = LemmyContext;
+ type Error = LemmyError;
+
+ fn id(&self) -> &Url {
+ &self.id
+ }
+
+ fn actor(&self) -> &Url {
+ self.actor.inner()
+ }
+
+ #[tracing::instrument(skip_all)]
+ async fn verify(&self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
+ insert_received_activity(&self.id, context).await?;
+ verify_is_public(&self.to, &self.cc)?;
Ok(())
}
- async fn receive(
- &self,
- context: &LemmyContext,
- request_counter: &mut i32,
- ) -> Result<(), LemmyError> {
- if is_activity_already_known(context.pool(), self.object.common().id_unchecked()).await? {
- return Ok(());
+ #[tracing::instrument(skip_all)]
+ async fn receive(self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
+ let object: AnnouncableActivities = self.object.object(context).await?.try_into()?;
+ // This is only for sending, not receiving so we reject it.
+ if let AnnouncableActivities::Page(_) = object {
+ return Err(LemmyErrorType::CannotReceivePage)?;
}
- insert_activity(
- self.object.common().id_unchecked(),
- self.object.clone(),
- false,
- true,
- context.pool(),
- )
- .await?;
- self.object.receive(context, request_counter).await
+
+ // verify here in order to avoid fetching the object twice over http
+ object.verify(context).await?;
+ object.receive(context).await
+ }
+}
+
+impl Id for RawAnnouncableActivities {
+ fn object_id(&self) -> &Url {
+ ActivityHandler::id(self)
+ }
+}
+
+impl TryFrom<RawAnnouncableActivities> for AnnouncableActivities {
+ type Error = serde_json::error::Error;
+
+ fn try_from(value: RawAnnouncableActivities) -> Result<Self, Self::Error> {
+ let mut map = value.other.clone();
+ map.insert("id".to_string(), Value::String(value.id.to_string()));
+ map.insert("actor".to_string(), Value::String(value.actor.to_string()));
+ serde_json::from_value(Value::Object(map))
}
+}
+
+impl TryFrom<AnnouncableActivities> for RawAnnouncableActivities {
+ type Error = serde_json::error::Error;
- fn common(&self) -> &ActivityCommonFields {
- &self.common
+ fn try_from(value: AnnouncableActivities) -> Result<Self, Self::Error> {
+ serde_json::from_value(serde_json::to_value(value)?)
}
}