mark_post_as_read,
post::*,
};
-use lemmy_apub::{ApubLikeableType, ApubObjectType};
+use lemmy_apub::{activities::post::update::UpdatePost, ApubLikeableType};
use lemmy_db_queries::{source::post::Post_, Crud, Likeable, Saveable};
use lemmy_db_schema::source::{moderator::*, post::*};
use lemmy_db_views::post_view::PostView;
blocking(context.pool(), move |conn| ModLockPost::create(conn, &form)).await??;
// apub updates
- updated_post
- .send_update(&local_user_view.person, context)
- .await?;
+ UpdatePost::send(&updated_post, &local_user_view.person, context).await?;
// Refetch the post
let post_id = data.post_id;
// Apub updates
// TODO stickied should pry work like locked for ease of use
- updated_post
- .send_update(&local_user_view.person, context)
- .await?;
+ UpdatePost::send(&updated_post, &local_user_view.person, context).await?;
// Refetch the post
let post_id = data.post_id;
mark_post_as_read,
post::*,
};
-use lemmy_apub::{generate_apub_endpoint, ApubLikeableType, ApubObjectType, EndpointType};
+use lemmy_apub::{generate_apub_endpoint, ApubLikeableType, EndpointType};
use lemmy_db_queries::{source::post::Post_, Crud, Likeable};
use lemmy_db_schema::source::post::*;
use lemmy_db_views::post_view::PostView;
.await?
.map_err(|_| ApiError::err("couldnt_create_post"))?;
- updated_post
- .send_create(&local_user_view.person, context)
- .await?;
+ lemmy_apub::activities::post::create::CreatePost::send(
+ &updated_post,
+ &local_user_view.person,
+ context,
+ )
+ .await?;
// They like their own post by default
let person_id = local_user_view.person.id;
use crate::PerformCrud;
use actix_web::web::Data;
use lemmy_api_common::{blocking, check_community_ban, get_local_user_view_from_jwt, post::*};
-use lemmy_apub::ApubObjectType;
+use lemmy_apub::activities::post::update::UpdatePost;
use lemmy_db_queries::{source::post::Post_, Crud, DeleteableOrRemoveable};
use lemmy_db_schema::{naive_now, source::post::*};
use lemmy_db_views::post_view::PostView;
};
// Send apub update
- updated_post
- .send_update(&local_user_view.person, context)
- .await?;
+ UpdatePost::send(&updated_post, &local_user_view.person, context).await?;
let post_id = data.post_id;
let mut post_view = blocking(context.pool(), move |conn| {
use crate::{
activities::{
comment::{get_notif_recipients, send_websocket_message},
+ extract_community,
verify_activity,
verify_person_in_community,
},
objects::FromApub,
+ ActorType,
NoteExt,
};
use activitystreams::{activity::kind::CreateType, base::BaseExt};
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
+ let community = extract_community(&self.cc, context, request_counter).await?;
+
verify_activity(self.common())?;
- verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
+ verify_person_in_community(
+ &self.common.actor,
+ &community.actor_id(),
+ context,
+ request_counter,
+ )
+ .await?;
verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?;
// TODO: should add a check that the correct community is in cc (probably needs changes to
// comment deserialization)
use crate::{
activities::{
comment::{get_notif_recipients, send_websocket_message},
+ extract_community,
verify_activity,
verify_person_in_community,
},
objects::FromApub,
+ ActorType,
NoteExt,
};
use activitystreams::{activity::kind::UpdateType, base::BaseExt};
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
+ let community = extract_community(&self.cc, context, request_counter).await?;
+
verify_activity(self.common())?;
- verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
+ verify_person_in_community(
+ &self.common.actor,
+ &community.actor_id(),
+ context,
+ request_counter,
+ )
+ .await?;
verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?;
Ok(())
}
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_activity(self.common())?;
- verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
+ verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?;
verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
verify_add_remove_moderator_target(&self.target, self.cc[0].clone())?;
Ok(())
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_like::UndoLikePostOrComment,
},
},
+ activity_queue::send_activity_new,
+ extensions::context::lemmy_context,
http::is_activity_already_known,
insert_activity,
+ ActorType,
+ CommunityType,
};
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};
common: ActivityCommonFields,
}
+impl AnnounceActivity {
+ pub async fn send(
+ object: AnnouncableActivities,
+ community: &Community,
+ additional_inboxes: Vec<Url>,
+ context: &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
+ }
+}
+
#[async_trait::async_trait(?Send)]
impl ActivityHandler for AnnounceActivity {
async fn verify(
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_activity(self.common())?;
- verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
+ verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?;
verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
Ok(())
}
+use crate::{check_is_apub_id_valid, CommunityType};
+use itertools::Itertools;
use lemmy_api_common::{blocking, community::CommunityResponse};
-use lemmy_db_schema::CommunityId;
+use lemmy_db_schema::{source::community::Community, CommunityId};
use lemmy_db_views_actor::community_view::CommunityView;
-use lemmy_utils::LemmyError;
+use lemmy_utils::{settings::structs::Settings, LemmyError};
use lemmy_websocket::{messages::SendCommunityRoomMessage, LemmyContext};
+use url::Url;
pub mod add_mod;
pub mod announce;
Ok(())
}
+
+async fn list_community_follower_inboxes(
+ community: &Community,
+ additional_inboxes: Vec<Url>,
+ context: &LemmyContext,
+) -> Result<Vec<Url>, LemmyError> {
+ Ok(
+ vec![
+ community.get_follower_inboxes(context.pool()).await?,
+ additional_inboxes,
+ ]
+ .iter()
+ .flatten()
+ .unique()
+ .filter(|inbox| inbox.host_str() != Some(&Settings::get().hostname()))
+ .filter(|inbox| check_is_apub_id_valid(inbox, false).is_ok())
+ .map(|inbox| inbox.to_owned())
+ .collect(),
+ )
+}
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_activity(self.common())?;
- verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
+ verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?;
verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
self.object.verify(context, request_counter).await?;
Ok(())
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_activity(self.common())?;
- verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
+ verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?;
verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
Ok(())
}
}
// deleting a post or comment
else {
- verify_person_in_community(&self.common().actor, &self.cc, context, request_counter).await?;
+ verify_person_in_community(&self.common().actor, &self.cc[0], context, request_counter)
+ .await?;
let object_creator =
get_post_or_comment_actor_id(&self.object, context, request_counter).await?;
verify_urls_match(&self.common.actor, &object_creator)?;
if let Ok(community) = object_community {
if community.local {
// repeat these checks just to be sure
- verify_person_in_community(&self.common().actor, &self.cc, context, request_counter)
+ verify_person_in_community(&self.common().actor, &self.cc[0], context, request_counter)
.await?;
verify_mod_action(&self.common.actor, self.object.clone(), context).await?;
let mod_ =
}
// restoring a post or comment
else {
- verify_person_in_community(&self.common().actor, &self.cc, context, request_counter).await?;
+ verify_person_in_community(&self.common().actor, &self.cc[0], context, request_counter)
+ .await?;
verify_urls_match(&self.common.actor, &self.object.common().actor)?;
}
Ok(())
if let Ok(community) = object_community {
if community.local {
// repeat these checks just to be sure
- verify_person_in_community(&self.common().actor, &self.cc, context, request_counter)
+ verify_person_in_community(&self.common().actor, &self.cc[0], context, request_counter)
.await?;
verify_mod_action(&self.common.actor, self.object.object.clone(), context).await?;
let mod_ =
DbUrl,
};
use lemmy_db_views_actor::community_view::CommunityView;
-use lemmy_utils::LemmyError;
+use lemmy_utils::{settings::structs::Settings, LemmyError};
use lemmy_websocket::LemmyContext;
-use url::Url;
+use url::{ParseError, Url};
+use uuid::Uuid;
pub mod comment;
pub mod community;
Ok(())
}
-/// Fetches the person and community to verify their type, then checks if person is banned from site
-/// or community.
-async fn verify_person_in_community(
- person_id: &Url,
+pub(crate) async fn extract_community(
cc: &[Url],
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<Community, LemmyError> {
- let person = get_or_fetch_and_upsert_person(person_id, context, request_counter).await?;
let mut cc_iter = cc.iter();
- let community: Community = loop {
+ loop {
if let Some(cid) = cc_iter.next() {
if let Ok(c) = get_or_fetch_and_upsert_community(cid, context, request_counter).await {
- break c;
+ break Ok(c);
}
} else {
return Err(anyhow!("No community found in cc").into());
}
- };
- check_community_or_site_ban(&person, community.id, context.pool()).await?;
- Ok(community)
+ }
+}
+
+/// Fetches the person and community to verify their type, then checks if person is banned from site
+/// or community.
+async fn verify_person_in_community(
+ person_id: &Url,
+ community_id: &Url,
+ context: &LemmyContext,
+ request_counter: &mut i32,
+) -> Result<(), LemmyError> {
+ let community = get_or_fetch_and_upsert_community(community_id, context, request_counter).await?;
+ let person = get_or_fetch_and_upsert_person(person_id, context, request_counter).await?;
+ check_community_or_site_ban(&person, community.id, context.pool()).await
}
/// Simply check that the url actually refers to a valid group.
Ok(())
}
-async fn verify_mod_action(
+/// Verify that the actor is a community mod. This check is only run if the community is local,
+/// because in case of remote communities, admins can also perform mod actions. As admin status
+/// is not federated, we cant verify their actions remotely.
+pub(crate) async fn verify_mod_action(
actor_id: &Url,
- activity_cc: Url,
+ community: Url,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let community = blocking(context.pool(), move |conn| {
- Community::read_from_apub_id(conn, &activity_cc.into())
+ Community::read_from_apub_id(conn, &community.into())
})
.await??;
}
Ok(())
}
+
+/// Generate a unique ID for an activity, in the format:
+/// `http(s)://example.com/receive/create/202daf0a-1489-45df-8d2e-c8a3173fed36`
+fn generate_activity_id<T>(kind: T) -> Result<Url, ParseError>
+where
+ T: ToString,
+{
+ let id = format!(
+ "{}/activities/{}/{}",
+ Settings::get().get_protocol_and_hostname(),
+ kind.to_string().to_lowercase(),
+ Uuid::new_v4()
+ );
+ Url::parse(&id)
+}
use crate::{
- activities::{post::send_websocket_message, verify_activity, verify_person_in_community},
+ activities::{
+ community::announce::AnnouncableActivities,
+ extract_community,
+ generate_activity_id,
+ post::send_websocket_message,
+ verify_activity,
+ verify_person_in_community,
+ },
+ activity_queue::send_to_community_new,
+ extensions::context::lemmy_context,
fetcher::person::get_or_fetch_and_upsert_person,
- objects::FromApub,
+ objects::{post::Page, FromApub, ToApub},
ActorType,
- PageExt,
};
-use activitystreams::{activity::kind::CreateType, base::BaseExt};
-use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler, PublicUrl};
-use lemmy_db_schema::source::post::Post;
+use activitystreams::activity::kind::CreateType;
+use anyhow::anyhow;
+use lemmy_api_common::blocking;
+use lemmy_apub_lib::{
+ verify_domains_match,
+ verify_urls_match,
+ ActivityCommonFields,
+ ActivityHandler,
+ PublicUrl,
+};
+use lemmy_db_queries::Crud;
+use lemmy_db_schema::source::{community::Community, person::Person, post::Post};
use lemmy_utils::LemmyError;
use lemmy_websocket::{LemmyContext, UserOperationCrud};
use url::Url;
#[serde(rename_all = "camelCase")]
pub struct CreatePost {
to: PublicUrl,
- object: PageExt,
- cc: Vec<Url>,
- #[serde(rename = "type")]
- kind: CreateType,
+ object: Page,
+ cc: [Url; 1],
+ r#type: CreateType,
#[serde(flatten)]
common: ActivityCommonFields,
}
+impl CreatePost {
+ pub async fn send(post: &Post, actor: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
+ let community_id = post.community_id;
+ let community = blocking(context.pool(), move |conn| {
+ Community::read(conn, community_id)
+ })
+ .await??;
+
+ let id = generate_activity_id(CreateType::Create)?;
+ let create = CreatePost {
+ to: PublicUrl::Public,
+ object: post.to_apub(context.pool()).await?,
+ cc: [community.actor_id()],
+ r#type: Default::default(),
+ common: ActivityCommonFields {
+ context: lemmy_context()?.into(),
+ id: id.clone(),
+ actor: actor.actor_id(),
+ unparsed: Default::default(),
+ },
+ };
+
+ let activity = AnnouncableActivities::CreatePost(create);
+ send_to_community_new(activity, &id, actor, &community, vec![], context).await
+ }
+}
+
#[async_trait::async_trait(?Send)]
impl ActivityHandler for CreatePost {
async fn verify(
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
+ let community = extract_community(&self.cc, context, request_counter).await?;
+ let community_id = &community.actor_id();
+
verify_activity(self.common())?;
- verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
- verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?;
+ verify_person_in_community(&self.common.actor, community_id, context, request_counter).await?;
+ verify_domains_match(&self.common.actor, &self.object.id)?;
+ verify_urls_match(&self.common.actor, &self.object.attributed_to)?;
+ // Check that the post isnt locked or stickied, as that isnt possible for newly created posts.
+ // However, when fetching a remote post we generate a new create activity with the current
+ // locked/stickied value, so this check may fail. So only check if its a local community,
+ // because then we will definitely receive all create and update activities separately.
+ let is_stickied_or_locked =
+ self.object.stickied == Some(true) || self.object.comments_enabled == Some(false);
+ if community.local && is_stickied_or_locked {
+ return Err(anyhow!("New post cannot be stickied or locked").into());
+ }
+ self.object.verify(context, request_counter).await?;
Ok(())
}
use crate::{
activities::{
+ community::announce::AnnouncableActivities,
+ generate_activity_id,
post::send_websocket_message,
verify_activity,
verify_mod_action,
verify_person_in_community,
},
- objects::{FromApub, FromApubToForm},
+ activity_queue::send_to_community_new,
+ extensions::context::lemmy_context,
+ fetcher::community::get_or_fetch_and_upsert_community,
+ objects::{post::Page, FromApub, ToApub},
ActorType,
- PageExt,
};
-use activitystreams::{activity::kind::UpdateType, base::BaseExt};
-use anyhow::Context;
+use activitystreams::activity::kind::UpdateType;
use lemmy_api_common::blocking;
-use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler, PublicUrl};
-use lemmy_db_queries::ApubObject;
-use lemmy_db_schema::{
- source::post::{Post, PostForm},
- DbUrl,
-};
-use lemmy_utils::{location_info, LemmyError};
+use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler, PublicUrl};
+use lemmy_db_queries::Crud;
+use lemmy_db_schema::source::{community::Community, person::Person, post::Post};
+use lemmy_utils::LemmyError;
use lemmy_websocket::{LemmyContext, UserOperationCrud};
use url::Url;
#[serde(rename_all = "camelCase")]
pub struct UpdatePost {
to: PublicUrl,
- object: PageExt,
- cc: Vec<Url>,
- #[serde(rename = "type")]
- kind: UpdateType,
+ object: Page,
+ cc: [Url; 1],
+ r#type: UpdateType,
#[serde(flatten)]
common: ActivityCommonFields,
}
+impl UpdatePost {
+ pub async fn send(post: &Post, actor: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
+ let community_id = post.community_id;
+ let community = blocking(context.pool(), move |conn| {
+ Community::read(conn, community_id)
+ })
+ .await??;
+
+ let id = generate_activity_id(UpdateType::Update)?;
+ let update = UpdatePost {
+ to: PublicUrl::Public,
+ object: post.to_apub(context.pool()).await?,
+ cc: [community.actor_id()],
+ r#type: Default::default(),
+ common: ActivityCommonFields {
+ context: lemmy_context()?.into(),
+ id: id.clone(),
+ actor: actor.actor_id(),
+ unparsed: Default::default(),
+ },
+ };
+ let activity = AnnouncableActivities::UpdatePost(update);
+ send_to_community_new(activity, &id, actor, &community, vec![], context).await
+ }
+}
+
#[async_trait::async_trait(?Send)]
impl ActivityHandler for UpdatePost {
async fn verify(
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
- verify_activity(self.common())?;
- let community =
- verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
+ let community_id = get_or_fetch_and_upsert_community(&self.cc[0], context, request_counter)
+ .await?
+ .actor_id();
+ let is_mod_action = self.object.is_mod_action(context.pool()).await?;
- let temp_post = PostForm::from_apub(
- &self.object,
- context,
- self.common.actor.clone(),
- request_counter,
- true,
- )
- .await?;
- let post_id: DbUrl = temp_post.ap_id.context(location_info!())?;
- let old_post = blocking(context.pool(), move |conn| {
- Post::read_from_apub_id(conn, &post_id)
- })
- .await??;
- let stickied = temp_post.stickied.context(location_info!())?;
- let locked = temp_post.locked.context(location_info!())?;
- // community mod changed locked/sticky status
- if (stickied != old_post.stickied) || (locked != old_post.locked) {
- verify_mod_action(&self.common.actor, community.actor_id(), context).await?;
- }
- // user edited their own post
- else {
- verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?;
+ verify_activity(self.common())?;
+ verify_person_in_community(&self.common.actor, &community_id, context, request_counter).await?;
+ if is_mod_action {
+ verify_mod_action(&self.common.actor, community_id, context).await?;
+ } else {
+ verify_urls_match(&self.common.actor, &self.object.attributed_to)?;
}
-
+ self.object.verify(context, request_counter).await?;
Ok(())
}
}
// removing community mod
else if let Some(target) = &self.target {
- verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
+ verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?;
verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
verify_add_remove_moderator_target(target, self.cc[0].clone())?;
}
// removing a post or comment
else {
- verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
+ verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?;
verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
}
Ok(())
}
// removing a post or comment
else {
- verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
+ verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?;
verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
}
self.object.verify(context, request_counter).await?;
use crate::{
- activities::send::generate_activity_id,
+ activities::generate_activity_id,
activity_queue::{send_comment_mentions, send_to_community},
extensions::context::lemmy_context,
fetcher::person::get_or_fetch_and_upsert_person,
use crate::{
- activities::send::generate_activity_id,
+ activities::generate_activity_id,
activity_queue::{send_activity_single_dest, send_to_community, send_to_community_followers},
check_is_apub_id_valid,
extensions::context::lemmy_context,
-use lemmy_utils::settings::structs::Settings;
-use url::{ParseError, Url};
-use uuid::Uuid;
-
pub(crate) mod comment;
pub(crate) mod community;
pub(crate) mod person;
pub(crate) mod post;
pub(crate) mod private_message;
-
-/// Generate a unique ID for an activity, in the format:
-/// `http(s)://example.com/receive/create/202daf0a-1489-45df-8d2e-c8a3173fed36`
-fn generate_activity_id<T>(kind: T) -> Result<Url, ParseError>
-where
- T: ToString,
-{
- let id = format!(
- "{}/activities/{}/{}",
- Settings::get().get_protocol_and_hostname(),
- kind.to_string().to_lowercase(),
- Uuid::new_v4()
- );
- Url::parse(&id)
-}
use crate::{
- activities::send::generate_activity_id,
+ activities::generate_activity_id,
activity_queue::send_activity_single_dest,
extensions::context::lemmy_context,
ActorType,
use crate::{
- activities::send::generate_activity_id,
+ activities::generate_activity_id,
activity_queue::send_to_community,
extensions::context::lemmy_context,
- objects::ToApub,
ActorType,
ApubLikeableType,
ApubObjectType,
};
use activitystreams::{
activity::{
- kind::{CreateType, DeleteType, DislikeType, LikeType, RemoveType, UndoType, UpdateType},
- Create,
+ kind::{DeleteType, DislikeType, LikeType, RemoveType, UndoType},
Delete,
Dislike,
Like,
Remove,
Undo,
- Update,
},
prelude::*,
public,
#[async_trait::async_trait(?Send)]
impl ApubObjectType for Post {
- /// Send out information about a newly created post, to the followers of the community.
- async fn send_create(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
- let page = self.to_apub(context.pool()).await?;
-
- let community_id = self.community_id;
- let community = blocking(context.pool(), move |conn| {
- Community::read(conn, community_id)
- })
- .await??;
-
- let mut create = Create::new(
- creator.actor_id.to_owned().into_inner(),
- page.into_any_base()?,
- );
- create
- .set_many_contexts(lemmy_context()?)
- .set_id(generate_activity_id(CreateType::Create)?)
- .set_to(public())
- .set_many_ccs(vec![community.actor_id()]);
-
- send_to_community(create, creator, &community, None, context).await?;
- Ok(())
+ async fn send_create(
+ &self,
+ _creator: &Person,
+ _context: &LemmyContext,
+ ) -> Result<(), LemmyError> {
+ unimplemented!()
}
- /// Send out information about an edited post, to the followers of the community.
- async fn send_update(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
- let page = self.to_apub(context.pool()).await?;
-
- let community_id = self.community_id;
- let community = blocking(context.pool(), move |conn| {
- Community::read(conn, community_id)
- })
- .await??;
-
- let mut update = Update::new(
- creator.actor_id.to_owned().into_inner(),
- page.into_any_base()?,
- );
- update
- .set_many_contexts(lemmy_context()?)
- .set_id(generate_activity_id(UpdateType::Update)?)
- .set_to(public())
- .set_many_ccs(vec![community.actor_id()]);
-
- send_to_community(update, creator, &community, None, context).await?;
- Ok(())
+ async fn send_update(
+ &self,
+ _creator: &Person,
+ _context: &LemmyContext,
+ ) -> Result<(), LemmyError> {
+ unimplemented!()
}
async fn send_delete(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
use crate::{
- activities::send::generate_activity_id,
+ activities::generate_activity_id,
activity_queue::send_activity_single_dest,
extensions::context::lemmy_context,
objects::ToApub,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_activity(self.common())?;
- verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
+ verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?;
Ok(())
}
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_activity(self.common())?;
- verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
+ verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?;
Ok(())
}
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_activity(self.common())?;
- verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
+ verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?;
verify_urls_match(&self.common.actor, &self.object.common().actor)?;
self.object.verify(context, request_counter).await?;
Ok(())
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_activity(self.common())?;
- verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
+ verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?;
verify_urls_match(&self.common.actor, &self.object.common().actor)?;
self.object.verify(context, request_counter).await?;
Ok(())
use crate::{
+ activities::community::announce::{AnnouncableActivities, AnnounceActivity},
check_is_apub_id_valid,
extensions::signatures::sign_and_send,
insert_activity,
use lemmy_db_schema::source::{community::Community, person::Person};
use lemmy_utils::{location_info, settings::structs::Settings, LemmyError};
use lemmy_websocket::LemmyContext;
-use log::{debug, warn};
+use log::{debug, info, warn};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::{collections::BTreeMap, env, fmt::Debug, future::Future, pin::Pin};
Ok(())
}
+pub(crate) async fn send_to_community_new(
+ activity: AnnouncableActivities,
+ activity_id: &Url,
+ actor: &dyn ActorType,
+ community: &Community,
+ additional_inboxes: Vec<Url>,
+ context: &LemmyContext,
+) -> Result<(), LemmyError> {
+ // if this is a local community, we need to do an announce from the community instead
+ if community.local {
+ insert_activity(activity_id, activity.clone(), true, false, context.pool()).await?;
+ AnnounceActivity::send(activity, community, additional_inboxes, context).await?;
+ } else {
+ let mut inboxes = additional_inboxes;
+ inboxes.push(community.get_shared_inbox_or_inbox_url());
+ send_activity_new(context, &activity, activity_id, actor, inboxes, false).await?;
+ }
+
+ Ok(())
+}
+
+pub(crate) async fn send_activity_new<T>(
+ context: &LemmyContext,
+ activity: &T,
+ activity_id: &Url,
+ actor: &dyn ActorType,
+ inboxes: Vec<Url>,
+ sensitive: bool,
+) -> Result<(), LemmyError>
+where
+ T: Serialize,
+{
+ if !Settings::get().federation().enabled || inboxes.is_empty() {
+ return Ok(());
+ }
+
+ info!("Sending activity {}", activity_id.to_string());
+
+ // Don't send anything to ourselves
+ // TODO: this should be a debug assert
+ let hostname = Settings::get().get_hostname_without_port()?;
+ let inboxes: Vec<&Url> = inboxes
+ .iter()
+ .filter(|i| i.domain().expect("valid inbox url") != hostname)
+ .collect();
+
+ let serialised_activity = serde_json::to_string(&activity)?;
+
+ insert_activity(
+ activity_id,
+ serialised_activity.clone(),
+ true,
+ sensitive,
+ context.pool(),
+ )
+ .await?;
+
+ for i in inboxes {
+ let message = SendActivityTask {
+ activity: serialised_activity.to_owned(),
+ inbox: i.to_owned(),
+ actor_id: actor.actor_id(),
+ private_key: actor.private_key().context(location_info!())?,
+ };
+ if env::var("LEMMY_TEST_SEND_SYNC").is_ok() {
+ do_send(message, &Client::default()).await?;
+ } else {
+ context.activity_queue.queue::<SendActivityTask>(message)?;
+ }
+ }
+
+ Ok(())
+}
+
/// Create new `SendActivityTasks`, which will deliver the given activity to inboxes, as well as
/// handling signing and retrying failed deliveres.
///
pub mod context;
pub(crate) mod group_extension;
-pub(crate) mod page_extension;
pub(crate) mod person_extension;
pub mod signatures;
+++ /dev/null
-use activitystreams::unparsed::UnparsedMutExt;
-use activitystreams_ext::UnparsedExtension;
-use serde::{Deserialize, Serialize};
-
-/// Activitystreams extension to allow (de)serializing additional Post fields
-/// `comemnts_enabled` (called 'locked' in Lemmy),
-/// `sensitive` (called 'nsfw') and `stickied`.
-#[derive(Clone, Debug, Default, Deserialize, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct PageExtension {
- pub comments_enabled: Option<bool>,
- pub sensitive: Option<bool>,
- pub stickied: Option<bool>,
-}
-
-impl<U> UnparsedExtension<U> for PageExtension
-where
- U: UnparsedMutExt,
-{
- type Error = serde_json::Error;
-
- fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> {
- Ok(PageExtension {
- comments_enabled: unparsed_mut.remove("commentsEnabled")?,
- sensitive: unparsed_mut.remove("sensitive")?,
- stickied: unparsed_mut.remove("stickied")?,
- })
- }
-
- fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
- unparsed_mut.insert("commentsEnabled", self.comments_enabled)?;
- unparsed_mut.insert("sensitive", self.sensitive)?;
- unparsed_mut.insert("stickied", self.stickied)?;
- Ok(())
- }
-}
use crate::{
fetcher::fetch::fetch_remote_object,
- objects::FromApub,
+ objects::{post::Page, FromApub},
NoteExt,
- PageExt,
PostOrComment,
};
use anyhow::anyhow;
Err(NotFound {}) => {
debug!("Fetching and creating remote post: {}", post_ap_id);
let page =
- fetch_remote_object::<PageExt>(context.client(), post_ap_id, recursion_counter).await?;
+ fetch_remote_object::<Page>(context.client(), post_ap_id, recursion_counter).await?;
let post = Post::from_apub(
&page,
context,
is_deleted,
},
find_object_by_id,
- objects::FromApub,
+ objects::{post::Page, FromApub},
GroupExt,
NoteExt,
Object,
- PageExt,
PersonExt,
};
use activitystreams::base::BaseExt;
enum SearchAcceptedObjects {
Person(Box<PersonExt>),
Group(Box<GroupExt>),
- Page(Box<PageExt>),
+ Page(Box<Page>),
Comment(Box<NoteExt>),
}
use crate::{
extensions::{
group_extension::GroupExtension,
- page_extension::PageExtension,
person_extension::PersonExtension,
signatures::{PublicKey, PublicKeyExtension},
},
activity::Follow,
actor,
base::AnyBase,
- object::{ApObject, AsObject, Note, ObjectExt, Page},
+ object::{ApObject, AsObject, Note, ObjectExt},
};
-use activitystreams_ext::{Ext1, Ext2};
+use activitystreams_ext::Ext2;
use anyhow::{anyhow, Context};
use diesel::NotFound;
use lemmy_api_common::blocking;
type PersonExt =
Ext2<actor::ApActor<ApObject<actor::Actor<UserTypes>>>, PersonExtension, PublicKeyExtension>;
pub type SiteExt = actor::ApActor<ApObject<actor::Service>>;
-/// Activitystreams type for post
-pub type PageExt = Ext1<ApObject<Page>, PageExtension>;
pub type NoteExt = ApObject<Note>;
#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize, PartialEq)]
use crate::{
- check_is_apub_id_valid,
- extensions::{context::lemmy_context, page_extension::PageExtension},
+ activities::extract_community,
+ extensions::context::lemmy_context,
fetcher::person::get_or_fetch_and_upsert_person,
- get_community_from_to_or_cc,
- objects::{
- check_object_domain,
- check_object_for_community_or_site_ban,
- create_tombstone,
- get_object_from_apub,
- get_source_markdown_value,
- set_content_and_source,
- FromApub,
- FromApubToForm,
- ToApub,
- },
- PageExt,
+ objects::{create_tombstone, FromApub, ToApub},
};
use activitystreams::{
- object::{kind::PageType, ApObject, Image, Page, Tombstone},
- prelude::*,
+ base::AnyBase,
+ object::{
+ kind::{ImageType, PageType},
+ Tombstone,
+ },
+ primitives::OneOrMany,
public,
+ unparsed::Unparsed,
};
-use activitystreams_ext::Ext1;
-use anyhow::Context;
+use chrono::{DateTime, FixedOffset};
use lemmy_api_common::blocking;
-use lemmy_db_queries::{Crud, DbPool};
+use lemmy_apub_lib::verify_domains_match;
+use lemmy_db_queries::{ApubObject, Crud, DbPool};
use lemmy_db_schema::{
self,
source::{
},
};
use lemmy_utils::{
- location_info,
request::fetch_iframely_and_pictrs_data,
- utils::{check_slurs, convert_datetime, remove_slurs},
+ utils::{check_slurs, convert_datetime, markdown_to_html, remove_slurs},
LemmyError,
};
use lemmy_websocket::LemmyContext;
#[async_trait::async_trait(?Send)]
impl ToApub for Post {
- type ApubType = PageExt;
+ type ApubType = Page;
// Turn a Lemmy post into an ActivityPub page that can be sent out over the network.
- async fn to_apub(&self, pool: &DbPool) -> Result<PageExt, LemmyError> {
- let mut page = ApObject::new(Page::new());
-
+ async fn to_apub(&self, pool: &DbPool) -> Result<Page, LemmyError> {
let creator_id = self.creator_id;
let creator = blocking(pool, move |conn| Person::read(conn, creator_id)).await??;
-
let community_id = self.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
- page
- // Not needed when the Post is embedded in a collection (like for community outbox)
- // TODO: need to set proper context defining sensitive/commentsEnabled fields
- // https://git.asonix.dog/Aardwolf/activitystreams/issues/5
- .set_many_contexts(lemmy_context()?)
- .set_id(self.ap_id.to_owned().into_inner())
- .set_name(self.name.to_owned())
- // `summary` field for compatibility with lemmy v0.9.9 and older,
- // TODO: remove this after some time
- .set_summary(self.name.to_owned())
- .set_published(convert_datetime(self.published))
- .set_many_tos(vec![community.actor_id.into_inner(), public()])
- .set_attributed_to(creator.actor_id.into_inner());
-
- if let Some(body) = &self.body {
- set_content_and_source(&mut page, body)?;
- }
-
- if let Some(url) = &self.url {
- page.set_url::<Url>(url.to_owned().into());
- }
-
- if let Some(thumbnail_url) = &self.thumbnail_url {
- let mut image = Image::new();
- image.set_url::<Url>(thumbnail_url.to_owned().into());
- page.set_image(image.into_any_base()?);
- }
-
- if let Some(u) = self.updated {
- page.set_updated(convert_datetime(u));
- }
-
- let ext = PageExtension {
+ let source = self.body.clone().map(|body| Source {
+ content: body,
+ media_type: MediaTypeMarkdown::Markdown,
+ });
+ let image = self.thumbnail_url.clone().map(|thumb| ImageObject {
+ content: ImageType::Image,
+ url: thumb.into(),
+ });
+
+ let page = Page {
+ context: lemmy_context()?.into(),
+ r#type: PageType::Page,
+ id: self.ap_id.clone().into(),
+ attributed_to: creator.actor_id.into(),
+ to: [community.actor_id.into(), public()],
+ name: self.name.clone(),
+ content: self.body.as_ref().map(|b| markdown_to_html(b)),
+ media_type: MediaTypeHtml::Markdown,
+ source,
+ url: self.url.clone().map(|u| u.into()),
+ image,
comments_enabled: Some(!self.locked),
sensitive: Some(self.nsfw),
stickied: Some(self.stickied),
+ published: convert_datetime(self.published),
+ updated: self.updated.map(convert_datetime),
+ unparsed: Default::default(),
};
- Ok(Ext1::new(page, ext))
+ Ok(page)
}
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
}
}
-#[async_trait::async_trait(?Send)]
-impl FromApub for Post {
- type ApubType = PageExt;
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+pub enum MediaTypeMarkdown {
+ #[serde(rename = "text/markdown")]
+ Markdown,
+}
+
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+pub enum MediaTypeHtml {
+ #[serde(rename = "text/html")]
+ Markdown,
+}
- /// Converts a `PageExt` to `PostForm`.
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Source {
+ content: String,
+ media_type: MediaTypeMarkdown,
+}
+
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ImageObject {
+ content: ImageType,
+ url: Url,
+}
+
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Page {
+ #[serde(rename = "@context")]
+ context: OneOrMany<AnyBase>,
+ r#type: PageType,
+ pub(crate) id: Url,
+ pub(crate) attributed_to: Url,
+ to: [Url; 2],
+ name: String,
+ content: Option<String>,
+ media_type: MediaTypeHtml,
+ source: Option<Source>,
+ url: Option<Url>,
+ image: Option<ImageObject>,
+ pub(crate) comments_enabled: Option<bool>,
+ sensitive: Option<bool>,
+ pub(crate) stickied: Option<bool>,
+ published: DateTime<FixedOffset>,
+ updated: Option<DateTime<FixedOffset>>,
+
+ // unparsed fields
+ #[serde(flatten)]
+ unparsed: Unparsed,
+}
+
+impl Page {
+ /// Only mods can change the post's stickied/locked status. So if either of these is changed from
+ /// the current value, it is a mod action and needs to be verified as such.
///
- /// If the post's community or creator are not known locally, these are also fetched.
- async fn from_apub(
- page: &PageExt,
- context: &LemmyContext,
- expected_domain: Url,
- request_counter: &mut i32,
- mod_action_allowed: bool,
- ) -> Result<Post, LemmyError> {
- let post: Post = get_object_from_apub(
- page,
- context,
- expected_domain,
- request_counter,
- mod_action_allowed,
- )
+ /// Both stickied and locked need to be false on a newly created post (verified in [[CreatePost]].
+ pub(crate) async fn is_mod_action(&self, pool: &DbPool) -> Result<bool, LemmyError> {
+ let post_id = self.id.clone();
+ let old_post = blocking(pool, move |conn| {
+ Post::read_from_apub_id(conn, &post_id.into())
+ })
.await?;
- check_object_for_community_or_site_ban(page, post.community_id, context, request_counter)
- .await?;
- Ok(post)
+
+ let is_mod_action = if let Ok(old_post) = old_post {
+ self.stickied != Some(old_post.stickied) || self.comments_enabled != Some(!old_post.locked)
+ } else {
+ false
+ };
+ Ok(is_mod_action)
+ }
+
+ pub(crate) async fn verify(
+ &self,
+ _context: &LemmyContext,
+ _request_counter: &mut i32,
+ ) -> Result<(), LemmyError> {
+ check_slurs(&self.name)?;
+ verify_domains_match(&self.attributed_to, &self.id)?;
+ Ok(())
}
}
#[async_trait::async_trait(?Send)]
-impl FromApubToForm<PageExt> for PostForm {
+impl FromApub for Post {
+ type ApubType = Page;
+
async fn from_apub(
- page: &PageExt,
+ page: &Page,
context: &LemmyContext,
- expected_domain: Url,
+ _expected_domain: Url,
request_counter: &mut i32,
- mod_action_allowed: bool,
- ) -> Result<PostForm, LemmyError> {
- let community = get_community_from_to_or_cc(page, context, request_counter).await?;
- let ap_id = if mod_action_allowed {
- let id = page.id_unchecked().context(location_info!())?;
- check_is_apub_id_valid(id, community.local)?;
- id.to_owned().into()
- } else {
- check_object_domain(page, expected_domain, community.local)?
- };
- let ext = &page.ext_one;
- let creator_actor_id = page
- .inner
- .attributed_to()
- .as_ref()
- .context(location_info!())?
- .as_single_xsd_any_uri()
- .context(location_info!())?;
-
+ _mod_action_allowed: bool,
+ ) -> Result<Post, LemmyError> {
let creator =
- get_or_fetch_and_upsert_person(creator_actor_id, context, request_counter).await?;
-
- let thumbnail_url: Option<Url> = match &page.inner.image() {
- Some(any_image) => Image::from_any_base(
- any_image
- .to_owned()
- .as_one()
- .context(location_info!())?
- .to_owned(),
- )?
- .context(location_info!())?
- .url()
- .context(location_info!())?
- .as_single_xsd_any_uri()
- .map(|url| url.to_owned()),
- None => None,
- };
- let url = page
- .inner
- .url()
- .map(|u| u.as_single_xsd_any_uri())
- .flatten()
- .map(|u| u.to_owned());
+ get_or_fetch_and_upsert_person(&page.attributed_to, context, request_counter).await?;
+ let community = extract_community(&page.to, context, request_counter).await?;
+ let thumbnail_url: Option<Url> = page.image.clone().map(|i| i.url);
let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
- if let Some(url) = &url {
+ if let Some(url) = &page.url {
fetch_iframely_and_pictrs_data(context.client(), Some(url)).await
} else {
(None, None, None, thumbnail_url)
};
- let name = page
- .inner
- .name()
- // The following is for compatibility with lemmy v0.9.9 and older
- // TODO: remove it after some time (along with the map above)
- .or_else(|| page.inner.summary())
- .context(location_info!())?
- .as_single_xsd_string()
- .context(location_info!())?
- .to_string();
- let body = get_source_markdown_value(page)?;
-
- // TODO: expected_domain is wrong in this case, because it simply takes the domain of the actor
- // maybe we need to take id_unchecked() if the activity is from community to user?
- // why did this work before? -> i dont think it did?
- // -> try to make expected_domain optional and set it null if it is a mod action
-
- check_slurs(&name)?;
- let body_slurs_removed = body.map(|b| remove_slurs(&b));
- Ok(PostForm {
- name,
- url: url.map(|u| u.into()),
+ let body_slurs_removed = page.source.as_ref().map(|s| remove_slurs(&s.content));
+ let form = PostForm {
+ name: page.name.clone(),
+ url: page.url.clone().map(|u| u.into()),
body: body_slurs_removed,
creator_id: creator.id,
community_id: community.id,
removed: None,
- locked: ext.comments_enabled.map(|e| !e),
- published: page
- .inner
- .published()
- .as_ref()
- .map(|u| u.to_owned().naive_local()),
- updated: page
- .inner
- .updated()
- .as_ref()
- .map(|u| u.to_owned().naive_local()),
+ locked: page.comments_enabled.map(|e| !e),
+ published: Some(page.published.naive_local()),
+ updated: page.updated.map(|u| u.naive_local()),
deleted: None,
- nsfw: ext.sensitive,
- stickied: ext.stickied,
+ nsfw: page.sensitive,
+ stickied: page.stickied,
embed_title: iframely_title,
embed_description: iframely_description,
embed_html: iframely_html,
thumbnail_url: pictrs_thumbnail.map(|u| u.into()),
- ap_id: Some(ap_id),
+ ap_id: Some(page.id.clone().into()),
local: Some(false),
- })
+ };
+ Ok(blocking(context.pool(), move |conn| Post::upsert(conn, &form)).await??)
}
}
pub struct ActivityCommonFields {
#[serde(rename = "@context")]
pub context: OneOrMany<AnyBase>,
- id: Url,
+ pub id: Url,
pub actor: Url,
// unparsed fields