diff --git a/Cargo.lock b/Cargo.lock index 2a9c5e8d1..d41277520 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1728,6 +1728,7 @@ dependencies = [ "lazy_static", "lemmy_api_common", "lemmy_apub", + "lemmy_apub_lib", "lemmy_db_schema", "lemmy_db_views", "lemmy_db_views_actor", diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml index 8db08a013..8c0671e51 100644 --- a/crates/api/Cargo.toml +++ b/crates/api/Cargo.toml @@ -14,6 +14,7 @@ doctest = false [dependencies] lemmy_apub = { version = "=0.13.5-rc.7", path = "../apub" } +lemmy_apub_lib = { version = "=0.13.5-rc.7", path = "../apub_lib" } lemmy_utils = { version = "=0.13.5-rc.7", path = "../utils" } lemmy_db_schema = { version = "=0.13.5-rc.7", path = "../db_schema" } lemmy_db_views = { version = "=0.13.5-rc.7", path = "../db_views" } diff --git a/crates/api/src/comment.rs b/crates/api/src/comment.rs index 62b01b33f..e6eef2cba 100644 --- a/crates/api/src/comment.rs +++ b/crates/api/src/comment.rs @@ -1,5 +1,7 @@ -use crate::Perform; +use std::convert::TryInto; + use actix_web::web::Data; + use lemmy_api_common::{ blocking, check_community_ban, @@ -9,11 +11,11 @@ use lemmy_api_common::{ get_local_user_view_from_jwt, }; use lemmy_apub::{ - activities::voting::{ + fetcher::post_or_comment::PostOrComment, + protocol::activities::voting::{ undo_vote::UndoVote, vote::{Vote, VoteType}, }, - fetcher::post_or_comment::PostOrComment, }; use lemmy_db_schema::{ newtypes::LocalUserId, @@ -23,7 +25,8 @@ use lemmy_db_schema::{ use lemmy_db_views::{comment_view::CommentView, local_user_view::LocalUserView}; use lemmy_utils::{ApiError, ConnectionId, LemmyError}; use lemmy_websocket::{send::send_comment_ws_message, LemmyContext, UserOperation}; -use std::convert::TryInto; + +use crate::Perform; #[async_trait::async_trait(?Send)] impl Perform for MarkCommentAsRead { diff --git a/crates/api/src/comment_report.rs b/crates/api/src/comment_report.rs index a7299b7ae..82ecc1f44 100644 --- a/crates/api/src/comment_report.rs +++ b/crates/api/src/comment_report.rs @@ -1,5 +1,5 @@ -use crate::Perform; use actix_web::web::Data; + use lemmy_api_common::{ blocking, check_community_ban, @@ -7,7 +7,7 @@ use lemmy_api_common::{ get_local_user_view_from_jwt, is_mod_or_admin, }; -use lemmy_apub::{activities::report::Report, fetcher::object_id::ObjectId}; +use lemmy_apub::{fetcher::object_id::ObjectId, protocol::activities::community::report::Report}; use lemmy_db_schema::{source::comment_report::*, traits::Reportable}; use lemmy_db_views::{ comment_report_view::{CommentReportQueryBuilder, CommentReportView}, @@ -16,6 +16,8 @@ use lemmy_db_views::{ use lemmy_utils::{ApiError, ConnectionId, LemmyError}; use lemmy_websocket::{messages::SendModRoomMessage, LemmyContext, UserOperation}; +use crate::Perform; + /// Creates a comment report and notifies the moderators of the community #[async_trait::async_trait(?Send)] impl Perform for CreateCommentReport { diff --git a/crates/api/src/community.rs b/crates/api/src/community.rs index ac39751c8..b47a4ec60 100644 --- a/crates/api/src/community.rs +++ b/crates/api/src/community.rs @@ -10,16 +10,16 @@ use lemmy_api_common::{ is_mod_or_admin, }; use lemmy_apub::{ - activities::{ + objects::{community::ApubCommunity, person::ApubPerson}, + protocol::activities::{ community::{ add_mod::AddMod, block_user::BlockUserFromCommunity, remove_mod::RemoveMod, undo_block_user::UndoBlockUserFromCommunity, }, - following::{follow::FollowCommunity as FollowCommunityApub, undo::UndoFollowCommunity}, + following::{follow::FollowCommunity as FollowCommunityApub, undo_follow::UndoFollowCommunity}, }, - objects::{community::ApubCommunity, person::ApubPerson}, }; use lemmy_db_schema::{ source::{ diff --git a/crates/api/src/post.rs b/crates/api/src/post.rs index 488c8f590..3564f1352 100644 --- a/crates/api/src/post.rs +++ b/crates/api/src/post.rs @@ -12,16 +12,16 @@ use lemmy_api_common::{ post::*, }; use lemmy_apub::{ - activities::{ - post::create_or_update::CreateOrUpdatePost, + fetcher::post_or_comment::PostOrComment, + objects::post::ApubPost, + protocol::activities::{ + create_or_update::post::CreateOrUpdatePost, voting::{ undo_vote::UndoVote, vote::{Vote, VoteType}, }, CreateOrUpdateType, }, - fetcher::post_or_comment::PostOrComment, - objects::post::ApubPost, }; use lemmy_db_schema::{ source::{moderator::*, post::*}, diff --git a/crates/api/src/post_report.rs b/crates/api/src/post_report.rs index 3e610bff8..98b2f1c11 100644 --- a/crates/api/src/post_report.rs +++ b/crates/api/src/post_report.rs @@ -1,5 +1,5 @@ -use crate::Perform; use actix_web::web::Data; + use lemmy_api_common::{ blocking, check_community_ban, @@ -13,7 +13,7 @@ use lemmy_api_common::{ ResolvePostReport, }, }; -use lemmy_apub::{activities::report::Report, fetcher::object_id::ObjectId}; +use lemmy_apub::{fetcher::object_id::ObjectId, protocol::activities::community::report::Report}; use lemmy_db_schema::{ source::post_report::{PostReport, PostReportForm}, traits::Reportable, @@ -25,6 +25,8 @@ use lemmy_db_views::{ use lemmy_utils::{ApiError, ConnectionId, LemmyError}; use lemmy_websocket::{messages::SendModRoomMessage, LemmyContext, UserOperation}; +use crate::Perform; + /// Creates a post report and notifies the moderators of the community #[async_trait::async_trait(?Send)] impl Perform for CreatePostReport { diff --git a/crates/api/src/site.rs b/crates/api/src/site.rs index 2797955a5..8d89d5db6 100644 --- a/crates/api/src/site.rs +++ b/crates/api/src/site.rs @@ -11,10 +11,10 @@ use lemmy_api_common::{ site::*, }; use lemmy_apub::{ - build_actor_id_from_shortname, fetcher::search::{search_by_apub_id, SearchableObjects}, - EndpointType, + get_actor_id_from_name, }; +use lemmy_apub_lib::webfinger::WebfingerType; use lemmy_db_schema::{ from_opt_str_to_opt_enum, newtypes::PersonId, @@ -174,11 +174,13 @@ impl Perform for Search { let listing_type: Option = from_opt_str_to_opt_enum(&data.listing_type); let search_type: SearchType = from_opt_str_to_opt_enum(&data.type_).unwrap_or(SearchType::All); let community_id = data.community_id; - let community_actor_id = data - .community_name - .as_ref() - .map(|t| build_actor_id_from_shortname(EndpointType::Community, t, &context.settings()).ok()) - .unwrap_or(None); + let community_actor_id = if let Some(name) = &data.community_name { + get_actor_id_from_name(WebfingerType::Group, name, context) + .await + .ok() + } else { + None + }; let creator_id = data.creator_id; match search_type { SearchType::Posts => { diff --git a/crates/api_crud/src/comment/create.rs b/crates/api_crud/src/comment/create.rs index 690a50820..ab093ea19 100644 --- a/crates/api_crud/src/comment/create.rs +++ b/crates/api_crud/src/comment/create.rs @@ -1,5 +1,5 @@ -use crate::PerformCrud; use actix_web::web::Data; + use lemmy_api_common::{ blocking, check_community_ban, @@ -11,13 +11,13 @@ use lemmy_api_common::{ get_post, }; use lemmy_apub::{ - activities::{ - comment::create_or_update::CreateOrUpdateComment, + fetcher::post_or_comment::PostOrComment, + generate_local_apub_endpoint, + protocol::activities::{ + create_or_update::comment::CreateOrUpdateComment, voting::vote::{Vote, VoteType}, CreateOrUpdateType, }, - fetcher::post_or_comment::PostOrComment, - generate_apub_endpoint, EndpointType, }; use lemmy_db_schema::{ @@ -40,6 +40,8 @@ use lemmy_websocket::{ UserOperationCrud, }; +use crate::PerformCrud; + #[async_trait::async_trait(?Send)] impl PerformCrud for CreateComment { type Response = CommentResponse; @@ -109,7 +111,7 @@ impl PerformCrud for CreateComment { let updated_comment: Comment = blocking(context.pool(), move |conn| -> Result { - let apub_id = generate_apub_endpoint( + let apub_id = generate_local_apub_endpoint( EndpointType::Comment, &inserted_comment_id.to_string(), &protocol_and_hostname, diff --git a/crates/api_crud/src/comment/read.rs b/crates/api_crud/src/comment/read.rs index 490c9670f..05f9f90fb 100644 --- a/crates/api_crud/src/comment/read.rs +++ b/crates/api_crud/src/comment/read.rs @@ -1,7 +1,8 @@ use crate::PerformCrud; use actix_web::web::Data; use lemmy_api_common::{blocking, comment::*, get_local_user_view_from_jwt_opt}; -use lemmy_apub::{build_actor_id_from_shortname, EndpointType}; +use lemmy_apub::get_actor_id_from_name; +use lemmy_apub_lib::webfinger::WebfingerType; use lemmy_db_schema::{ from_opt_str_to_opt_enum, traits::DeleteableOrRemoveable, @@ -34,11 +35,13 @@ impl PerformCrud for GetComments { let listing_type: Option = from_opt_str_to_opt_enum(&data.type_); let community_id = data.community_id; - let community_actor_id = data - .community_name - .as_ref() - .map(|t| build_actor_id_from_shortname(EndpointType::Community, t, &context.settings()).ok()) - .unwrap_or(None); + let community_actor_id = if let Some(name) = &data.community_name { + get_actor_id_from_name(WebfingerType::Group, name, context) + .await + .ok() + } else { + None + }; let saved_only = data.saved_only; let page = data.page; let limit = data.limit; diff --git a/crates/api_crud/src/comment/update.rs b/crates/api_crud/src/comment/update.rs index 9a164fc3f..70f15dfb2 100644 --- a/crates/api_crud/src/comment/update.rs +++ b/crates/api_crud/src/comment/update.rs @@ -1,5 +1,5 @@ -use crate::PerformCrud; use actix_web::web::Data; + use lemmy_api_common::{ blocking, check_community_ban, @@ -8,8 +8,8 @@ use lemmy_api_common::{ comment::*, get_local_user_view_from_jwt, }; -use lemmy_apub::activities::{ - comment::create_or_update::CreateOrUpdateComment, +use lemmy_apub::protocol::activities::{ + create_or_update::comment::CreateOrUpdateComment, CreateOrUpdateType, }; use lemmy_db_schema::source::comment::Comment; @@ -26,6 +26,8 @@ use lemmy_websocket::{ UserOperationCrud, }; +use crate::PerformCrud; + #[async_trait::async_trait(?Send)] impl PerformCrud for EditComment { type Response = CommentResponse; diff --git a/crates/api_crud/src/community/create.rs b/crates/api_crud/src/community/create.rs index 8b7daeb7b..523ccf00d 100644 --- a/crates/api_crud/src/community/create.rs +++ b/crates/api_crud/src/community/create.rs @@ -8,9 +8,9 @@ use lemmy_api_common::{ }; use lemmy_apub::{ fetcher::object_id::ObjectId, - generate_apub_endpoint, generate_followers_url, generate_inbox_url, + generate_local_apub_endpoint, generate_shared_inbox_url, objects::community::ApubCommunity, EndpointType, @@ -67,7 +67,7 @@ impl PerformCrud for CreateCommunity { } // Double check for duplicate community actor_ids - let community_actor_id = generate_apub_endpoint( + let community_actor_id = generate_local_apub_endpoint( EndpointType::Community, &data.name, &context.settings().get_protocol_and_hostname(), diff --git a/crates/api_crud/src/community/read.rs b/crates/api_crud/src/community/read.rs index 91effae87..feaf110fb 100644 --- a/crates/api_crud/src/community/read.rs +++ b/crates/api_crud/src/community/read.rs @@ -2,11 +2,11 @@ use crate::PerformCrud; use actix_web::web::Data; use lemmy_api_common::{blocking, community::*, get_local_user_view_from_jwt_opt}; use lemmy_apub::{ - build_actor_id_from_shortname, fetcher::object_id::ObjectId, + get_actor_id_from_name, objects::community::ApubCommunity, - EndpointType, }; +use lemmy_apub_lib::webfinger::WebfingerType; use lemmy_db_schema::{ from_opt_str_to_opt_enum, traits::DeleteableOrRemoveable, @@ -39,7 +39,7 @@ impl PerformCrud for GetCommunity { None => { let name = data.name.to_owned().unwrap_or_else(|| "main".to_string()); let community_actor_id = - build_actor_id_from_shortname(EndpointType::Community, &name, &context.settings())?; + get_actor_id_from_name(WebfingerType::Group, &name, context).await?; ObjectId::::new(community_actor_id) .dereference(context, &mut 0) diff --git a/crates/api_crud/src/community/update.rs b/crates/api_crud/src/community/update.rs index 97722a98b..4764b0254 100644 --- a/crates/api_crud/src/community/update.rs +++ b/crates/api_crud/src/community/update.rs @@ -5,7 +5,7 @@ use lemmy_api_common::{ community::{CommunityResponse, EditCommunity}, get_local_user_view_from_jwt, }; -use lemmy_apub::activities::community::update::UpdateCommunity; +use lemmy_apub::protocol::activities::community::update::UpdateCommunity; use lemmy_db_schema::{ diesel_option_overwrite_to_url, naive_now, diff --git a/crates/api_crud/src/post/create.rs b/crates/api_crud/src/post/create.rs index 3e5fb41e1..99b67d2ea 100644 --- a/crates/api_crud/src/post/create.rs +++ b/crates/api_crud/src/post/create.rs @@ -1,5 +1,7 @@ -use crate::PerformCrud; use actix_web::web::Data; +use log::warn; +use webmention::{Webmention, WebmentionError}; + use lemmy_api_common::{ blocking, check_community_ban, @@ -10,13 +12,13 @@ use lemmy_api_common::{ post::*, }; use lemmy_apub::{ - activities::{ - post::create_or_update::CreateOrUpdatePost, + fetcher::post_or_comment::PostOrComment, + generate_local_apub_endpoint, + protocol::activities::{ + create_or_update::post::CreateOrUpdatePost, voting::vote::{Vote, VoteType}, CreateOrUpdateType, }, - fetcher::post_or_comment::PostOrComment, - generate_apub_endpoint, EndpointType, }; use lemmy_db_schema::{ @@ -31,8 +33,8 @@ use lemmy_utils::{ LemmyError, }; use lemmy_websocket::{send::send_post_ws_message, LemmyContext, UserOperationCrud}; -use log::warn; -use webmention::{Webmention, WebmentionError}; + +use crate::PerformCrud; #[async_trait::async_trait(?Send)] impl PerformCrud for CreatePost { @@ -98,7 +100,7 @@ impl PerformCrud for CreatePost { let inserted_post_id = inserted_post.id; let protocol_and_hostname = context.settings().get_protocol_and_hostname(); let updated_post = blocking(context.pool(), move |conn| -> Result { - let apub_id = generate_apub_endpoint( + let apub_id = generate_local_apub_endpoint( EndpointType::Post, &inserted_post_id.to_string(), &protocol_and_hostname, diff --git a/crates/api_crud/src/post/read.rs b/crates/api_crud/src/post/read.rs index 901d1e935..d2a46f53e 100644 --- a/crates/api_crud/src/post/read.rs +++ b/crates/api_crud/src/post/read.rs @@ -1,7 +1,8 @@ use crate::PerformCrud; use actix_web::web::Data; use lemmy_api_common::{blocking, get_local_user_view_from_jwt_opt, mark_post_as_read, post::*}; -use lemmy_apub::{build_actor_id_from_shortname, EndpointType}; +use lemmy_apub::get_actor_id_from_name; +use lemmy_apub_lib::webfinger::WebfingerType; use lemmy_db_schema::{ from_opt_str_to_opt_enum, traits::DeleteableOrRemoveable, @@ -136,11 +137,13 @@ impl PerformCrud for GetPosts { let page = data.page; let limit = data.limit; let community_id = data.community_id; - let community_actor_id = data - .community_name - .as_ref() - .map(|t| build_actor_id_from_shortname(EndpointType::Community, t, &context.settings()).ok()) - .unwrap_or(None); + let community_actor_id = if let Some(name) = &data.community_name { + get_actor_id_from_name(WebfingerType::Group, name, context) + .await + .ok() + } else { + None + }; let saved_only = data.saved_only; let mut posts = blocking(context.pool(), move |conn| { diff --git a/crates/api_crud/src/post/update.rs b/crates/api_crud/src/post/update.rs index 96e4400a5..0a982d688 100644 --- a/crates/api_crud/src/post/update.rs +++ b/crates/api_crud/src/post/update.rs @@ -1,5 +1,5 @@ -use crate::PerformCrud; use actix_web::web::Data; + use lemmy_api_common::{ blocking, check_community_ban, @@ -7,7 +7,10 @@ use lemmy_api_common::{ get_local_user_view_from_jwt, post::*, }; -use lemmy_apub::activities::{post::create_or_update::CreateOrUpdatePost, CreateOrUpdateType}; +use lemmy_apub::protocol::activities::{ + create_or_update::post::CreateOrUpdatePost, + CreateOrUpdateType, +}; use lemmy_db_schema::{ naive_now, source::post::{Post, PostForm}, @@ -22,6 +25,8 @@ use lemmy_utils::{ }; use lemmy_websocket::{send::send_post_ws_message, LemmyContext, UserOperationCrud}; +use crate::PerformCrud; + #[async_trait::async_trait(?Send)] impl PerformCrud for EditPost { type Response = PostResponse; diff --git a/crates/api_crud/src/private_message/create.rs b/crates/api_crud/src/private_message/create.rs index d6560be25..705d781e2 100644 --- a/crates/api_crud/src/private_message/create.rs +++ b/crates/api_crud/src/private_message/create.rs @@ -7,11 +7,11 @@ use lemmy_api_common::{ person::{CreatePrivateMessage, PrivateMessageResponse}, }; use lemmy_apub::{ - activities::{ + generate_local_apub_endpoint, + protocol::activities::{ private_message::create_or_update::CreateOrUpdatePrivateMessage, CreateOrUpdateType, }, - generate_apub_endpoint, EndpointType, }; use lemmy_db_schema::{ @@ -67,7 +67,7 @@ impl PerformCrud for CreatePrivateMessage { let updated_private_message = blocking( context.pool(), move |conn| -> Result { - let apub_id = generate_apub_endpoint( + let apub_id = generate_local_apub_endpoint( EndpointType::PrivateMessage, &inserted_private_message_id.to_string(), &protocol_and_hostname, diff --git a/crates/api_crud/src/private_message/delete.rs b/crates/api_crud/src/private_message/delete.rs index f369f82b1..06bc22ede 100644 --- a/crates/api_crud/src/private_message/delete.rs +++ b/crates/api_crud/src/private_message/delete.rs @@ -5,7 +5,7 @@ use lemmy_api_common::{ get_local_user_view_from_jwt, person::{DeletePrivateMessage, PrivateMessageResponse}, }; -use lemmy_apub::activities::private_message::{ +use lemmy_apub::protocol::activities::private_message::{ delete::DeletePrivateMessage as DeletePrivateMessageApub, undo_delete::UndoDeletePrivateMessage, }; diff --git a/crates/api_crud/src/private_message/update.rs b/crates/api_crud/src/private_message/update.rs index d72e3b137..8114556c1 100644 --- a/crates/api_crud/src/private_message/update.rs +++ b/crates/api_crud/src/private_message/update.rs @@ -5,7 +5,7 @@ use lemmy_api_common::{ get_local_user_view_from_jwt, person::{EditPrivateMessage, PrivateMessageResponse}, }; -use lemmy_apub::activities::{ +use lemmy_apub::protocol::activities::{ private_message::create_or_update::CreateOrUpdatePrivateMessage, CreateOrUpdateType, }; diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs index ce37cd6a0..78b204ff9 100644 --- a/crates/api_crud/src/user/create.rs +++ b/crates/api_crud/src/user/create.rs @@ -2,9 +2,9 @@ use crate::PerformCrud; use actix_web::web::Data; use lemmy_api_common::{blocking, honeypot_check, password_length_check, person::*}; use lemmy_apub::{ - generate_apub_endpoint, generate_followers_url, generate_inbox_url, + generate_local_apub_endpoint, generate_shared_inbox_url, EndpointType, }; @@ -96,7 +96,7 @@ impl PerformCrud for Register { if !is_valid_actor_name(&data.username, context.settings().actor_name_max_length) { return Err(ApiError::err_plain("invalid_username").into()); } - let actor_id = generate_apub_endpoint( + let actor_id = generate_local_apub_endpoint( EndpointType::Person, &data.username, &context.settings().get_protocol_and_hostname(), @@ -179,7 +179,7 @@ impl PerformCrud for Register { Ok(c) => c, Err(_e) => { let default_community_name = "main"; - let actor_id = generate_apub_endpoint( + let actor_id = generate_local_apub_endpoint( EndpointType::Community, default_community_name, &protocol_and_hostname, diff --git a/crates/api_crud/src/user/read.rs b/crates/api_crud/src/user/read.rs index a5b69ba45..b649c8fbb 100644 --- a/crates/api_crud/src/user/read.rs +++ b/crates/api_crud/src/user/read.rs @@ -2,11 +2,11 @@ use crate::PerformCrud; use actix_web::web::Data; use lemmy_api_common::{blocking, get_local_user_view_from_jwt_opt, person::*}; use lemmy_apub::{ - build_actor_id_from_shortname, fetcher::object_id::ObjectId, + get_actor_id_from_name, objects::person::ApubPerson, - EndpointType, }; +use lemmy_apub_lib::webfinger::WebfingerType; use lemmy_db_schema::{from_opt_str_to_opt_enum, SortType}; use lemmy_db_views::{comment_view::CommentQueryBuilder, post_view::PostQueryBuilder}; use lemmy_db_views_actor::{ @@ -46,8 +46,7 @@ impl PerformCrud for GetPersonDetails { .username .to_owned() .unwrap_or_else(|| "admin".to_string()); - let actor_id = - build_actor_id_from_shortname(EndpointType::Person, &name, &context.settings())?; + let actor_id = get_actor_id_from_name(WebfingerType::Person, &name, context).await?; let person = ObjectId::::new(actor_id) .dereference(context, &mut 0) diff --git a/crates/apub/assets/lemmy-comment.json b/crates/apub/assets/lemmy-comment.json deleted file mode 100644 index c40e893c8..000000000 --- a/crates/apub/assets/lemmy-comment.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "@context": [ - "https://www.w3.org/ns/activitystreams", - { - "comments_enabled": { - "id": "pt:commentsEnabled", - "type": "sc:Boolean" - }, - "matrixUserId": { - "id": "as:alsoKnownAs", - "type": "sc:Text" - }, - "moderators": "as:moderators", - "pt": "https://join-lemmy.org#", - "sc": "http://schema.org#", - "sensitive": "as:sensitive", - "stickied": "as:stickied" - }, - "https://w3id.org/security/v1" - ], - "id": "https://enterprise.lemmy.ml/comment/38741", - "type": "Note", - "attributedTo": "https://enterprise.lemmy.ml/u/picard", - "to": ["https://www.w3.org/ns/activitystreams#Public"], - "inReplyTo": "https://enterprise.lemmy.ml/post/55143", - "content": "first comment!", - "mediaType": "text/html", - "source": { - "content": "first comment!", - "mediaType": "text/markdown" - }, - "published": "2021-03-01T13:42:43.966208+00:00", - "updated": "2021-03-01T13:43:03.955787+00:00" -} diff --git a/crates/apub/assets/lemmy-private-message.json b/crates/apub/assets/lemmy-private-message.json deleted file mode 100644 index 374754215..000000000 --- a/crates/apub/assets/lemmy-private-message.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "@context": [ - "https://www.w3.org/ns/activitystreams", - { - "comments_enabled": { - "id": "pt:commentsEnabled", - "type": "sc:Boolean" - }, - "matrixUserId": { - "id": "as:alsoKnownAs", - "type": "sc:Text" - }, - "moderators": "as:moderators", - "pt": "https://join-lemmy.org#", - "sc": "http://schema.org#", - "sensitive": "as:sensitive", - "stickied": "as:stickied" - }, - "https://w3id.org/security/v1" - ], - "id": "https://enterprise.lemmy.ml/private_message/1621", - "type": "ChatMessage", - "attributedTo": "https://enterprise.lemmy.ml/u/picard", - "to": [ - "https://queer.hacktivis.me/users/lanodan" - ], - "content": "Hello hello, testing", - "mediaType": "text/html", - "source": { - "content": "Hello hello, testing", - "mediaType": "text/markdown" - }, - "published": "2021-10-21T10:13:14.597721+00:00" -} diff --git a/crates/apub/assets/lemmy/activities/community/add_mod.json b/crates/apub/assets/lemmy/activities/community/add_mod.json new file mode 100644 index 000000000..d0eedd8bf --- /dev/null +++ b/crates/apub/assets/lemmy/activities/community/add_mod.json @@ -0,0 +1,13 @@ +{ + "actor": "http://enterprise.lemmy.ml/u/lemmy_beta", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": "http://ds9.lemmy.ml/u/lemmy_alpha", + "target": "http://enterprise.lemmy.ml/c/main/moderators", + "cc": [ + "http://enterprise.lemmy.ml/c/main" + ], + "type": "Add", + "id": "http://enterprise.lemmy.ml/activities/add/ec069147-77c3-447f-88c8-0ef1df10403f" +} \ No newline at end of file diff --git a/crates/apub/assets/lemmy/activities/community/announce_create_page.json b/crates/apub/assets/lemmy/activities/community/announce_create_page.json new file mode 100644 index 000000000..6b58cf1cf --- /dev/null +++ b/crates/apub/assets/lemmy/activities/community/announce_create_page.json @@ -0,0 +1,37 @@ +{ + "actor": "http://enterprise.lemmy.ml/c/main", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": { + "actor": "http://enterprise.lemmy.ml/u/lemmy_beta", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": { + "type": "Page", + "id": "http://enterprise.lemmy.ml/post/7", + "attributedTo": "http://enterprise.lemmy.ml/u/lemmy_beta", + "to": [ + "http://enterprise.lemmy.ml/c/main", + "https://www.w3.org/ns/activitystreams#Public" + ], + "name": "post 4", + "mediaType": "text/html", + "commentsEnabled": true, + "sensitive": false, + "stickied": false, + "published": "2021-11-01T12:11:22.871846+00:00" + }, + "cc": [ + "http://enterprise.lemmy.ml/c/main" + ], + "type": "Create", + "id": "http://enterprise.lemmy.ml/activities/create/2807c9ec-3ad8-4859-a9e0-28b59b6e499f" + }, + "cc": [ + "http://enterprise.lemmy.ml/c/main/followers" + ], + "type": "Announce", + "id": "http://enterprise.lemmy.ml/activities/announce/8030b171-803a-4108-94b1-342688f375cf" +} \ No newline at end of file diff --git a/crates/apub/assets/lemmy/activities/community/block_user.json b/crates/apub/assets/lemmy/activities/community/block_user.json new file mode 100644 index 000000000..9ca00816d --- /dev/null +++ b/crates/apub/assets/lemmy/activities/community/block_user.json @@ -0,0 +1,13 @@ +{ + "actor": "http://enterprise.lemmy.ml/u/lemmy_beta", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": "http://ds9.lemmy.ml/u/lemmy_alpha", + "cc": [ + "http://enterprise.lemmy.ml/c/main" + ], + "target": "http://enterprise.lemmy.ml/c/main", + "type": "Block", + "id": "http://enterprise.lemmy.ml/activities/block/5d42fffb-0903-4625-86d4-0b39bb344fc2" +} \ No newline at end of file diff --git a/crates/apub/assets/lemmy/activities/community/remove_mod.json b/crates/apub/assets/lemmy/activities/community/remove_mod.json new file mode 100644 index 000000000..2932fec37 --- /dev/null +++ b/crates/apub/assets/lemmy/activities/community/remove_mod.json @@ -0,0 +1,13 @@ +{ + "actor": "http://enterprise.lemmy.ml/u/lemmy_beta", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": "http://ds9.lemmy.ml/u/lemmy_alpha", + "cc": [ + "http://enterprise.lemmy.ml/c/main" + ], + "type": "Remove", + "target": "http://enterprise.lemmy.ml/c/main/moderators", + "id": "http://enterprise.lemmy.ml/activities/remove/aab114f8-cfbd-4935-a5b7-e1a64603650d" +} \ No newline at end of file diff --git a/crates/apub/assets/lemmy/activities/community/report_page.json b/crates/apub/assets/lemmy/activities/community/report_page.json new file mode 100644 index 000000000..bd1691b57 --- /dev/null +++ b/crates/apub/assets/lemmy/activities/community/report_page.json @@ -0,0 +1,10 @@ +{ + "actor": "http://ds9.lemmy.ml/u/lemmy_alpha", + "to": [ + "http://enterprise.lemmy.ml/c/main" + ], + "object": "http://enterprise.lemmy.ml/post/7", + "summary": "report this post", + "type": "Flag", + "id": "http://ds9.lemmy.ml/activities/flag/98b0933f-5e45-4a95-a15f-e0dc86361ba4" +} \ No newline at end of file diff --git a/crates/apub/assets/lemmy/activities/community/undo_block_user.json b/crates/apub/assets/lemmy/activities/community/undo_block_user.json new file mode 100644 index 000000000..5810b4f48 --- /dev/null +++ b/crates/apub/assets/lemmy/activities/community/undo_block_user.json @@ -0,0 +1,24 @@ +{ + "actor": "http://enterprise.lemmy.ml/u/lemmy_beta", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": { + "actor": "http://enterprise.lemmy.ml/u/lemmy_beta", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": "http://ds9.lemmy.ml/u/lemmy_alpha", + "cc": [ + "http://enterprise.lemmy.ml/c/main" + ], + "target": "http://enterprise.lemmy.ml/c/main", + "type": "Block", + "id": "http://enterprise.lemmy.ml/activities/block/726f43ab-bd0e-4ab3-89c8-627e976f553c" + }, + "cc": [ + "http://enterprise.lemmy.ml/c/main" + ], + "type": "Undo", + "id": "http://enterprise.lemmy.ml/activities/undo/06a20ffb-3e32-42fb-8f4c-674b36d7c557" +} \ No newline at end of file diff --git a/crates/apub/assets/lemmy/activities/community/update_community.json b/crates/apub/assets/lemmy/activities/community/update_community.json new file mode 100644 index 000000000..275d6d2ba --- /dev/null +++ b/crates/apub/assets/lemmy/activities/community/update_community.json @@ -0,0 +1,37 @@ +{ + "actor": "http://ds9.lemmy.ml/u/lemmy_alpha", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": { + "type": "Group", + "id": "http://enterprise.lemmy.ml/c/main", + "preferredUsername": "main", + "name": "The Updated Community", + "summary": "

updated 2

\n", + "source": { + "content": "updated 2", + "mediaType": "text/markdown" + }, + "sensitive": false, + "moderators": "http://enterprise.lemmy.ml/c/main/moderators", + "inbox": "http://enterprise.lemmy.ml/c/main/inbox", + "outbox": "http://enterprise.lemmy.ml/c/main/outbox", + "followers": "http://enterprise.lemmy.ml/c/main/followers", + "endpoints": { + "sharedInbox": "http://enterprise.lemmy.ml/inbox" + }, + "publicKey": { + "id": "http://enterprise.lemmy.ml/c/main#main-key", + "owner": "http://enterprise.lemmy.ml/c/main", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA16Xh06V1l2yy0WAIMUTV\nnvZIuAuKDxzDQUNT+n8gmcVuvBu7tkpbPTQ3DjGB3bQfGC2ekew/yldwOXyZ7ry1\npbJSYSrCBJrAlPLs/ao3OPTqmcl3vnSWti/hqopEV+Um2t7fwpkCjVrnzVKRSlys\nihnrth64ZiwAqq2llpaXzWc1SR2URZYSdnry/4d9UNrZVkumIeg1gk9KbCAo4j/O\njsv/aBjpZcTeLmtMZf6fcrvGre9duJdx6e2Tg/YNcnSnARosqev/UwVTzzGNVWXg\n9rItaa0a0aea4se4Bn6QXvOBbcq3+OYZMR6a34hh5BTeNG8WbpwmVahS0WFUsv9G\nswIDAQAB\n-----END PUBLIC KEY-----\n" + }, + "published": "2021-10-29T15:05:51.476984+00:00", + "updated": "2021-11-01T12:23:50.151874+00:00" + }, + "cc": [ + "http://enterprise.lemmy.ml/c/main" + ], + "type": "Update", + "id": "http://ds9.lemmy.ml/activities/update/d3717cf5-096d-473f-9530-5d52f9d51f5f" +} \ No newline at end of file diff --git a/crates/apub/assets/lemmy/activities/create_or_update/create_note.json b/crates/apub/assets/lemmy/activities/create_or_update/create_note.json new file mode 100644 index 000000000..4360ce92a --- /dev/null +++ b/crates/apub/assets/lemmy/activities/create_or_update/create_note.json @@ -0,0 +1,29 @@ +{ + "actor": "http://ds9.lemmy.ml/u/lemmy_alpha", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": { + "type": "Note", + "id": "http://ds9.lemmy.ml/comment/1", + "attributedTo": "http://ds9.lemmy.ml/u/lemmy_alpha", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "content": "hello", + "mediaType": "text/html", + "source": { + "content": "hello", + "mediaType": "text/markdown" + }, + "inReplyTo": "http://ds9.lemmy.ml/post/1", + "published": "2021-11-01T11:45:49.794920+00:00" + }, + "cc": [ + "http://enterprise.lemmy.ml/c/main", + "http://ds9.lemmy.ml/u/lemmy_alpha" + ], + "tag": [], + "type": "Create", + "id": "http://ds9.lemmy.ml/activities/create/1e77d67c-44ac-45ed-bf2a-460e21f60236" +} \ No newline at end of file diff --git a/crates/apub/assets/lemmy/activities/create_or_update/create_page.json b/crates/apub/assets/lemmy/activities/create_or_update/create_page.json new file mode 100644 index 000000000..b223120b0 --- /dev/null +++ b/crates/apub/assets/lemmy/activities/create_or_update/create_page.json @@ -0,0 +1,32 @@ +{ + "actor": "http://ds9.lemmy.ml/u/lemmy_alpha", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": { + "type": "Page", + "id": "http://ds9.lemmy.ml/post/1", + "attributedTo": "http://ds9.lemmy.ml/u/lemmy_alpha", + "to": [ + "http://enterprise.lemmy.ml/c/main", + "https://www.w3.org/ns/activitystreams#Public" + ], + "name": "test post", + "content": "

test body

\n", + "mediaType": "text/html", + "source": { + "content": "test body", + "mediaType": "text/markdown" + }, + "url": "https://lemmy.ml/pictrs/image/xl8W7FZfk9.jpg", + "commentsEnabled": true, + "sensitive": false, + "stickied": false, + "published": "2021-10-29T15:10:51.557399+00:00" + }, + "cc": [ + "http://enterprise.lemmy.ml/c/main" + ], + "type": "Create", + "id": "http://ds9.lemmy.ml/activities/create/eee6a57a-622f-464d-b560-73ae1fcd3ddf" +} \ No newline at end of file diff --git a/crates/apub/assets/lemmy/activities/create_or_update/update_page.json b/crates/apub/assets/lemmy/activities/create_or_update/update_page.json new file mode 100644 index 000000000..beadfa0d1 --- /dev/null +++ b/crates/apub/assets/lemmy/activities/create_or_update/update_page.json @@ -0,0 +1,33 @@ +{ + "actor": "http://ds9.lemmy.ml/u/lemmy_alpha", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": { + "type": "Page", + "id": "http://ds9.lemmy.ml/post/1", + "attributedTo": "http://ds9.lemmy.ml/u/lemmy_alpha", + "to": [ + "http://enterprise.lemmy.ml/c/main", + "https://www.w3.org/ns/activitystreams#Public" + ], + "name": "test post 1", + "content": "

test body

\n", + "mediaType": "text/html", + "source": { + "content": "test body", + "mediaType": "text/markdown" + }, + "url": "https://lemmy.ml/pictrs/image/xl8W7FZfk9.jpg", + "commentsEnabled": true, + "sensitive": false, + "stickied": false, + "published": "2021-10-29T15:10:51.557399+00:00", + "updated": "2021-10-29T15:11:35.976374+00:00" + }, + "cc": [ + "http://enterprise.lemmy.ml/c/main" + ], + "type": "Update", + "id": "http://ds9.lemmy.ml/activities/update/ab360117-e165-4de4-b7fc-906b62c98631" +} \ No newline at end of file diff --git a/crates/apub/assets/lemmy/activities/deletion/delete_page.json b/crates/apub/assets/lemmy/activities/deletion/delete_page.json new file mode 100644 index 000000000..8dd26a109 --- /dev/null +++ b/crates/apub/assets/lemmy/activities/deletion/delete_page.json @@ -0,0 +1,12 @@ +{ + "actor": "http://ds9.lemmy.ml/u/lemmy_alpha", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": "http://ds9.lemmy.ml/post/1", + "cc": [ + "http://enterprise.lemmy.ml/c/main" + ], + "type": "Delete", + "id": "http://ds9.lemmy.ml/activities/delete/f2abee48-c7bb-41d5-9e27-8775ff32db12" +} \ No newline at end of file diff --git a/crates/apub/assets/lemmy/activities/deletion/remove_note.json b/crates/apub/assets/lemmy/activities/deletion/remove_note.json new file mode 100644 index 000000000..8ea354044 --- /dev/null +++ b/crates/apub/assets/lemmy/activities/deletion/remove_note.json @@ -0,0 +1,13 @@ +{ + "actor": "http://enterprise.lemmy.ml/u/lemmy_beta", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": "http://ds9.lemmy.ml/comment/1", + "cc": [ + "http://enterprise.lemmy.ml/c/main" + ], + "type": "Delete", + "summary": "bad comment", + "id": "http://enterprise.lemmy.ml/activities/delete/42ca1a79-f99e-4518-a2ca-ba2df221eb5e" +} \ No newline at end of file diff --git a/crates/apub/assets/lemmy/activities/deletion/undo_delete_page.json b/crates/apub/assets/lemmy/activities/deletion/undo_delete_page.json new file mode 100644 index 000000000..9f824fa1f --- /dev/null +++ b/crates/apub/assets/lemmy/activities/deletion/undo_delete_page.json @@ -0,0 +1,23 @@ +{ + "actor": "http://ds9.lemmy.ml/u/lemmy_alpha", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": { + "actor": "http://ds9.lemmy.ml/u/lemmy_alpha", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": "http://ds9.lemmy.ml/post/1", + "cc": [ + "http://enterprise.lemmy.ml/c/main" + ], + "type": "Delete", + "id": "http://ds9.lemmy.ml/activities/delete/b13cca96-7737-41e1-9769-8fbf972b3509" + }, + "cc": [ + "http://enterprise.lemmy.ml/c/main" + ], + "type": "Undo", + "id": "http://ds9.lemmy.ml/activities/undo/5e939cfb-b8a1-4de8-950f-9d684e9162b9" +} \ No newline at end of file diff --git a/crates/apub/assets/lemmy/activities/deletion/undo_remove_note.json b/crates/apub/assets/lemmy/activities/deletion/undo_remove_note.json new file mode 100644 index 000000000..413cf16b4 --- /dev/null +++ b/crates/apub/assets/lemmy/activities/deletion/undo_remove_note.json @@ -0,0 +1,24 @@ +{ + "actor": "http://enterprise.lemmy.ml/u/lemmy_beta", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": { + "actor": "http://enterprise.lemmy.ml/u/lemmy_beta", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": "http://ds9.lemmy.ml/comment/1", + "cc": [ + "http://enterprise.lemmy.ml/c/main" + ], + "type": "Delete", + "summary": "bad comment", + "id": "http://enterprise.lemmy.ml/activities/delete/2598435c-87a3-49cd-81f3-a44b03b7af9d" + }, + "cc": [ + "http://enterprise.lemmy.ml/c/main" + ], + "type": "Undo", + "id": "http://enterprise.lemmy.ml/activities/undo/a850cf21-3866-4b3a-b80b-56aa00997fee" +} \ No newline at end of file diff --git a/crates/apub/assets/lemmy/activities/following/accept.json b/crates/apub/assets/lemmy/activities/following/accept.json new file mode 100644 index 000000000..d5bcfaf48 --- /dev/null +++ b/crates/apub/assets/lemmy/activities/following/accept.json @@ -0,0 +1,17 @@ +{ + "actor": "http://enterprise.lemmy.ml/c/main", + "to": [ + "http://ds9.lemmy.ml/u/lemmy_alpha" + ], + "object": { + "actor": "http://ds9.lemmy.ml/u/lemmy_alpha", + "to": [ + "http://enterprise.lemmy.ml/c/main" + ], + "object": "http://enterprise.lemmy.ml/c/main", + "type": "Follow", + "id": "http://ds9.lemmy.ml/activities/follow/6abcd50b-b8ca-4952-86b0-a6dd8cc12866" + }, + "type": "Accept", + "id": "http://enterprise.lemmy.ml/activities/accept/75f080cc-3d45-4654-8186-8f3bb853fa27" +} \ No newline at end of file diff --git a/crates/apub/assets/lemmy/activities/following/follow.json b/crates/apub/assets/lemmy/activities/following/follow.json new file mode 100644 index 000000000..50cc77dc0 --- /dev/null +++ b/crates/apub/assets/lemmy/activities/following/follow.json @@ -0,0 +1,9 @@ +{ + "actor": "http://ds9.lemmy.ml/u/lemmy_alpha", + "to": [ + "http://enterprise.lemmy.ml/c/main" + ], + "object": "http://enterprise.lemmy.ml/c/main", + "type": "Follow", + "id": "http://ds9.lemmy.ml/activities/follow/6abcd50b-b8ca-4952-86b0-a6dd8cc12866" +} \ No newline at end of file diff --git a/crates/apub/assets/lemmy/activities/following/undo_follow.json b/crates/apub/assets/lemmy/activities/following/undo_follow.json new file mode 100644 index 000000000..d27bec0a4 --- /dev/null +++ b/crates/apub/assets/lemmy/activities/following/undo_follow.json @@ -0,0 +1,17 @@ +{ + "actor": "http://ds9.lemmy.ml/u/lemmy_alpha", + "to": [ + "http://enterprise.lemmy.ml/c/main" + ], + "object": { + "actor": "http://ds9.lemmy.ml/u/lemmy_alpha", + "to": [ + "http://enterprise.lemmy.ml/c/main" + ], + "object": "http://enterprise.lemmy.ml/c/main", + "type": "Follow", + "id": "http://ds9.lemmy.ml/activities/follow/dc2f1bc5-f3a0-4daa-a46b-428cbfbd023c" + }, + "type": "Undo", + "id": "http://ds9.lemmy.ml/activities/undo/dd83c482-8ebd-4b6c-9008-c8373bd1a86a" +} \ No newline at end of file diff --git a/crates/apub/assets/lemmy/activities/private_message/create.json b/crates/apub/assets/lemmy/activities/private_message/create.json new file mode 100644 index 000000000..de080a8f9 --- /dev/null +++ b/crates/apub/assets/lemmy/activities/private_message/create.json @@ -0,0 +1,23 @@ +{ + "id": "http://enterprise.lemmy.ml/activities/create/987d05fa-f637-46d7-85be-13d112bc269f", + "actor": "http://enterprise.lemmy.ml/u/lemmy_beta", + "to": [ + "http://ds9.lemmy.ml/u/lemmy_alpha" + ], + "object": { + "type": "ChatMessage", + "id": "http://enterprise.lemmy.ml/private_message/1", + "attributedTo": "http://enterprise.lemmy.ml/u/lemmy_beta", + "to": [ + "http://ds9.lemmy.ml/u/lemmy_alpha" + ], + "content": "hello", + "mediaType": "text/html", + "source": { + "content": "hello", + "mediaType": "text/markdown" + }, + "published": "2021-10-29T15:31:56.058289+00:00" + }, + "type": "Create" +} \ No newline at end of file diff --git a/crates/apub/assets/lemmy/activities/private_message/delete.json b/crates/apub/assets/lemmy/activities/private_message/delete.json new file mode 100644 index 000000000..f5f85ac3e --- /dev/null +++ b/crates/apub/assets/lemmy/activities/private_message/delete.json @@ -0,0 +1,9 @@ +{ + "actor": "http://enterprise.lemmy.ml/u/lemmy_beta", + "to": [ + "http://enterprise.lemmy.ml/u/lemmy_beta" + ], + "object": "http://enterprise.lemmy.ml/private_message/1", + "type": "Delete", + "id": "http://enterprise.lemmy.ml/activities/delete/041d9858-5eef-4ad9-84ae-7455b4d87ed9" +} \ No newline at end of file diff --git a/crates/apub/assets/lemmy/activities/private_message/undo_delete.json b/crates/apub/assets/lemmy/activities/private_message/undo_delete.json new file mode 100644 index 000000000..d9b4ed75a --- /dev/null +++ b/crates/apub/assets/lemmy/activities/private_message/undo_delete.json @@ -0,0 +1,17 @@ +{ + "actor": "http://enterprise.lemmy.ml/u/lemmy_beta", + "to": [ + "http://ds9.lemmy.ml/u/lemmy_alpha" + ], + "object": { + "actor": "http://enterprise.lemmy.ml/u/lemmy_beta", + "to": [ + "http://enterprise.lemmy.ml/u/lemmy_beta" + ], + "object": "http://enterprise.lemmy.ml/private_message/1", + "type": "Delete", + "id": "http://enterprise.lemmy.ml/activities/delete/616c41be-04ed-4bd4-b865-30712186b122" + }, + "type": "Undo", + "id": "http://enterprise.lemmy.ml/activities/undo/35e5b337-014c-4bbe-8d63-6fac96f51409" +} \ No newline at end of file diff --git a/crates/apub/assets/lemmy/activities/voting/dislike_page.json b/crates/apub/assets/lemmy/activities/voting/dislike_page.json new file mode 100644 index 000000000..822a9d357 --- /dev/null +++ b/crates/apub/assets/lemmy/activities/voting/dislike_page.json @@ -0,0 +1,12 @@ +{ + "actor": "http://enterprise.lemmy.ml/u/lemmy_beta", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": "http://ds9.lemmy.ml/post/1", + "cc": [ + "http://enterprise.lemmy.ml/c/main" + ], + "type": "Dislike", + "id": "http://enterprise.lemmy.ml/activities/dislike/64d40d40-a829-43a5-8247-1fb595b3ca1c" +} \ No newline at end of file diff --git a/crates/apub/assets/lemmy/activities/voting/like_note.json b/crates/apub/assets/lemmy/activities/voting/like_note.json new file mode 100644 index 000000000..35e969060 --- /dev/null +++ b/crates/apub/assets/lemmy/activities/voting/like_note.json @@ -0,0 +1,12 @@ +{ + "actor": "http://ds9.lemmy.ml/u/lemmy_alpha", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": "http://ds9.lemmy.ml/comment/1", + "cc": [ + "http://enterprise.lemmy.ml/c/main" + ], + "type": "Like", + "id": "http://ds9.lemmy.ml/activities/like/fd61d070-7382-46a9-b2b7-6bb253732877" +} \ No newline at end of file diff --git a/crates/apub/assets/lemmy/activities/voting/undo_dislike_page.json b/crates/apub/assets/lemmy/activities/voting/undo_dislike_page.json new file mode 100644 index 000000000..4123ebab4 --- /dev/null +++ b/crates/apub/assets/lemmy/activities/voting/undo_dislike_page.json @@ -0,0 +1,23 @@ +{ + "actor": "http://enterprise.lemmy.ml/u/lemmy_beta", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": { + "actor": "http://enterprise.lemmy.ml/u/lemmy_beta", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": "http://ds9.lemmy.ml/post/1", + "cc": [ + "http://enterprise.lemmy.ml/c/main" + ], + "type": "Like", + "id": "http://enterprise.lemmy.ml/activities/like/2227ab2c-79e2-4fca-a1d2-1d67dacf2457" + }, + "cc": [ + "http://enterprise.lemmy.ml/c/main" + ], + "type": "Undo", + "id": "http://enterprise.lemmy.ml/activities/undo/6cc6fb71-39fe-49ea-9506-f0423b101e98" +} \ No newline at end of file diff --git a/crates/apub/assets/lemmy/activities/voting/undo_like_note.json b/crates/apub/assets/lemmy/activities/voting/undo_like_note.json new file mode 100644 index 000000000..84a6efe50 --- /dev/null +++ b/crates/apub/assets/lemmy/activities/voting/undo_like_note.json @@ -0,0 +1,23 @@ +{ + "actor": "http://ds9.lemmy.ml/u/lemmy_alpha", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": { + "actor": "http://ds9.lemmy.ml/u/lemmy_alpha", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": "http://ds9.lemmy.ml/comment/1", + "cc": [ + "http://enterprise.lemmy.ml/c/main" + ], + "type": "Like", + "id": "http://ds9.lemmy.ml/activities/like/efcf7ae2-dfcc-4ff4-9ce4-6adf251ff004" + }, + "cc": [ + "http://enterprise.lemmy.ml/c/main" + ], + "type": "Undo", + "id": "http://ds9.lemmy.ml/activities/undo/3518565c-24a7-4d9e-8e0a-f7a2f45ac618" +} \ No newline at end of file diff --git a/crates/apub/assets/lemmy/collections/group_followers.json b/crates/apub/assets/lemmy/collections/group_followers.json new file mode 100644 index 000000000..a2b03ab17 --- /dev/null +++ b/crates/apub/assets/lemmy/collections/group_followers.json @@ -0,0 +1,6 @@ +{ + "id": "http://enterprise.lemmy.ml/c/main/followers", + "type": "Collection", + "totalItems": 3, + "items": [] +} \ No newline at end of file diff --git a/crates/apub/assets/lemmy/collections/group_moderators.json b/crates/apub/assets/lemmy/collections/group_moderators.json new file mode 100644 index 000000000..eccd0dad7 --- /dev/null +++ b/crates/apub/assets/lemmy/collections/group_moderators.json @@ -0,0 +1,7 @@ +{ + "type": "OrderedCollection", + "id": "https://enterprise.lemmy.ml/c/tenforward/moderators", + "orderedItems": [ + "https://enterprise.lemmy.ml/u/picard" + ] +} \ No newline at end of file diff --git a/crates/apub/assets/lemmy/collections/group_outbox.json b/crates/apub/assets/lemmy/collections/group_outbox.json new file mode 100644 index 000000000..cf68742f5 --- /dev/null +++ b/crates/apub/assets/lemmy/collections/group_outbox.json @@ -0,0 +1,209 @@ +{ + "type": "OrderedCollection", + "id": "https://ds9.lemmy.ml/c/main/outbox", + "totalItems": 7, + "orderedItems": [ + { + "actor": "https://ds9.lemmy.ml/u/dess_ds9", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": { + "type": "Page", + "id": "https://ds9.lemmy.ml/post/1685", + "attributedTo": "https://ds9.lemmy.ml/u/dess_ds9", + "to": [ + "https://ds9.lemmy.ml/c/main", + "https://www.w3.org/ns/activitystreams#Public" + ], + "name": "Test post", + "mediaType": "text/html", + "commentsEnabled": true, + "sensitive": false, + "stickied": false, + "published": "2021-09-30T16:37:58.425718+00:00", + "updated": "2021-09-30T16:39:50.934055+00:00" + }, + "cc": [ + "https://ds9.lemmy.ml/c/main" + ], + "type": "Create", + "id": "https://ds9.lemmy.ml/activities/create/157bc329-05cb-4dc3-ad9e-5110fde3f3aa" + }, + { + "actor": "https://ds9.lemmy.ml/u/nutomic", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": { + "type": "Page", + "id": "https://ds9.lemmy.ml/post/1665", + "attributedTo": "https://ds9.lemmy.ml/u/nutomic", + "to": [ + "https://ds9.lemmy.ml/c/main", + "https://www.w3.org/ns/activitystreams#Public" + ], + "name": "another webmention test", + "mediaType": "text/html", + "url": "https://webmention.rocks/test/1", + "commentsEnabled": true, + "sensitive": false, + "stickied": false, + "published": "2021-09-17T13:22:15.026912+00:00" + }, + "cc": [ + "https://ds9.lemmy.ml/c/main" + ], + "type": "Create", + "id": "https://ds9.lemmy.ml/activities/create/c54e4509-16ac-42bf-b3b4-0bf8516f8152" + }, + { + "actor": "https://ds9.lemmy.ml/u/nutomic", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": { + "type": "Page", + "id": "https://ds9.lemmy.ml/post/1664", + "attributedTo": "https://ds9.lemmy.ml/u/nutomic", + "to": [ + "https://ds9.lemmy.ml/c/main", + "https://www.w3.org/ns/activitystreams#Public" + ], + "name": "another test", + "mediaType": "text/html", + "url": "https://webmention.rocks/test/1", + "commentsEnabled": true, + "sensitive": false, + "stickied": false, + "published": "2021-09-17T13:13:21.675891+00:00" + }, + "cc": [ + "https://ds9.lemmy.ml/c/main" + ], + "type": "Create", + "id": "https://ds9.lemmy.ml/activities/create/25f7d2cb-11d5-4c9c-aa3c-85fbff9f9e0c" + }, + { + "actor": "https://ds9.lemmy.ml/u/nutomic", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": { + "type": "Page", + "id": "https://ds9.lemmy.ml/post/1663", + "attributedTo": "https://ds9.lemmy.ml/u/nutomic", + "to": [ + "https://ds9.lemmy.ml/c/main", + "https://www.w3.org/ns/activitystreams#Public" + ], + "name": "Webmention test from Lemmy", + "mediaType": "text/html", + "url": "https://webmention.rocks/test/1", + "commentsEnabled": true, + "sensitive": false, + "stickied": false, + "published": "2021-09-17T13:00:15.392844+00:00" + }, + "cc": [ + "https://ds9.lemmy.ml/c/main" + ], + "type": "Create", + "id": "https://ds9.lemmy.ml/activities/create/cfbd12b8-2e11-42b6-a609-b482decbaf11" + }, + { + "actor": "https://ds9.lemmy.ml/u/dess_tester_3", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": { + "type": "Page", + "id": "https://ds9.lemmy.ml/post/1644", + "attributedTo": "https://ds9.lemmy.ml/u/dess_tester_3", + "to": [ + "https://ds9.lemmy.ml/c/main", + "https://www.w3.org/ns/activitystreams#Public" + ], + "name": "The best wireless earbuds you can buy right now | Engadget", + "mediaType": "text/html", + "url": "https://www.engadget.com/best-wireless-earbuds-120058222.html", + "image": { + "type": "Image", + "url": "https://ds9.lemmy.ml/pictrs/image/0WWsYOuwAE.jpg" + }, + "commentsEnabled": true, + "sensitive": false, + "stickied": false, + "published": "2021-08-26T01:22:06.428368+00:00" + }, + "cc": [ + "https://ds9.lemmy.ml/c/main" + ], + "type": "Create", + "id": "https://ds9.lemmy.ml/activities/create/76c94408-944a-4a2f-a88b-d10f12b472b0" + }, + { + "actor": "https://ds9.lemmy.ml/u/dess_ds9", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": { + "type": "Page", + "id": "https://ds9.lemmy.ml/post/1643", + "attributedTo": "https://ds9.lemmy.ml/u/dess_ds9", + "to": [ + "https://ds9.lemmy.ml/c/main", + "https://www.w3.org/ns/activitystreams#Public" + ], + "name": "First Look: Cadillac’s luxury EV debut seems like a winner | Engadges", + "content": "

test

\n", + "mediaType": "text/html", + "source": { + "content": "test", + "mediaType": "text/markdown" + }, + "url": "https://www.engadget.com/cadillac-lyriq-luxury-ev-first-look-video-171543752.html", + "image": { + "type": "Image", + "url": "https://ds9.lemmy.ml/pictrs/image/gnmtvgXP31.jpg" + }, + "commentsEnabled": true, + "sensitive": false, + "stickied": false, + "published": "2021-08-23T23:43:06.560543+00:00", + "updated": "2021-08-23T23:52:51.832606+00:00" + }, + "cc": [ + "https://ds9.lemmy.ml/c/main" + ], + "type": "Create", + "id": "https://ds9.lemmy.ml/activities/create/b1f95918-f593-4951-91cf-2c3340cd9509" + }, + { + "actor": "https://ds9.lemmy.ml/u/dess_ds9_2", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": { + "type": "Page", + "id": "https://ds9.lemmy.ml/post/1642", + "attributedTo": "https://ds9.lemmy.ml/u/dess_ds9_2", + "to": [ + "https://ds9.lemmy.ml/c/main", + "https://www.w3.org/ns/activitystreams#Public" + ], + "name": "A test post from DS9", + "mediaType": "text/html", + "commentsEnabled": true, + "sensitive": false, + "stickied": false, + "published": "2021-08-06T14:10:47.493075+00:00" + }, + "cc": [ + "https://ds9.lemmy.ml/c/main" + ], + "type": "Create", + "id": "https://ds9.lemmy.ml/activities/create/6359b2e7-badb-4241-b5ee-b093078361bd" + } + ] +} \ No newline at end of file diff --git a/crates/apub/assets/lemmy/collections/person_outbox.json b/crates/apub/assets/lemmy/collections/person_outbox.json new file mode 100644 index 000000000..8da561023 --- /dev/null +++ b/crates/apub/assets/lemmy/collections/person_outbox.json @@ -0,0 +1,6 @@ +{ + "type": "OrderedCollection", + "id": "http://ds9.lemmy.ml/u/lemmy_alpha/outbox", + "orderedItems": [], + "totalItems": 0 +} \ No newline at end of file diff --git a/crates/apub/assets/lemmy/objects/chat_message.json b/crates/apub/assets/lemmy/objects/chat_message.json new file mode 100644 index 000000000..c639aef92 --- /dev/null +++ b/crates/apub/assets/lemmy/objects/chat_message.json @@ -0,0 +1,15 @@ +{ + "id": "https://enterprise.lemmy.ml/private_message/1621", + "type": "ChatMessage", + "attributedTo": "https://enterprise.lemmy.ml/u/picard", + "to": [ + "https://queer.hacktivis.me/users/lanodan" + ], + "content": "

Hello hello, testing

\n", + "mediaType": "text/html", + "source": { + "content": "Hello hello, testing", + "mediaType": "text/markdown" + }, + "published": "2021-10-21T10:13:14.597721+00:00" +} diff --git a/crates/apub/assets/lemmy-community.json b/crates/apub/assets/lemmy/objects/group.json similarity index 79% rename from crates/apub/assets/lemmy-community.json rename to crates/apub/assets/lemmy/objects/group.json index 3f56ed8ee..7eddd86bd 100644 --- a/crates/apub/assets/lemmy-community.json +++ b/crates/apub/assets/lemmy/objects/group.json @@ -1,23 +1,4 @@ { - "@context": [ - "https://www.w3.org/ns/activitystreams", - { - "comments_enabled": { - "id": "pt:commentsEnabled", - "type": "sc:Boolean" - }, - "matrixUserId": { - "id": "as:alsoKnownAs", - "type": "sc:Text" - }, - "moderators": "as:moderators", - "pt": "https://join-lemmy.org#", - "sc": "http://schema.org#", - "sensitive": "as:sensitive", - "stickied": "as:stickied" - }, - "https://w3id.org/security/v1" - ], "id": "https://enterprise.lemmy.ml/c/tenforward", "type": "Group", "preferredUsername": "main", diff --git a/crates/apub/assets/lemmy/objects/note.json b/crates/apub/assets/lemmy/objects/note.json new file mode 100644 index 000000000..c353d0b2b --- /dev/null +++ b/crates/apub/assets/lemmy/objects/note.json @@ -0,0 +1,15 @@ +{ + "id": "https://enterprise.lemmy.ml/comment/38741", + "type": "Note", + "attributedTo": "https://enterprise.lemmy.ml/u/picard", + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "inReplyTo": "https://enterprise.lemmy.ml/post/55143", + "content": "

first comment!

\n", + "mediaType": "text/html", + "source": { + "content": "first comment!", + "mediaType": "text/markdown" + }, + "published": "2021-03-01T13:42:43.966208+00:00", + "updated": "2021-03-01T13:43:03.955787+00:00" +} diff --git a/crates/apub/assets/lemmy-post.json b/crates/apub/assets/lemmy/objects/page.json similarity index 55% rename from crates/apub/assets/lemmy-post.json rename to crates/apub/assets/lemmy/objects/page.json index baf28ea06..36cac596f 100644 --- a/crates/apub/assets/lemmy-post.json +++ b/crates/apub/assets/lemmy/objects/page.json @@ -1,23 +1,4 @@ { - "@context": [ - "https://www.w3.org/ns/activitystreams", - { - "comments_enabled": { - "id": "pt:commentsEnabled", - "type": "sc:Boolean" - }, - "matrixUserId": { - "id": "as:alsoKnownAs", - "type": "sc:Text" - }, - "moderators": "as:moderators", - "pt": "https://join-lemmy.org#", - "sc": "http://schema.org#", - "sensitive": "as:sensitive", - "stickied": "as:stickied" - }, - "https://w3id.org/security/v1" - ], "id": "https://enterprise.lemmy.ml/post/55143", "type": "Page", "attributedTo": "https://enterprise.lemmy.ml/u/picard", @@ -32,6 +13,11 @@ "content": "This is a post in the /c/tenforward community", "mediaType": "text/markdown" }, + "url": "https://enterprise.lemmy.ml/pictrs/image/eOtYb9iEiB.png", + "image": { + "type": "Image", + "url": "https://enterprise.lemmy.ml/pictrs/image/eOtYb9iEiB.png" + }, "sensitive": false, "commentsEnabled": true, "stickied": true, diff --git a/crates/apub/assets/lemmy-person.json b/crates/apub/assets/lemmy/objects/person.json similarity index 74% rename from crates/apub/assets/lemmy-person.json rename to crates/apub/assets/lemmy/objects/person.json index 25c935177..e21fa4d22 100644 --- a/crates/apub/assets/lemmy-person.json +++ b/crates/apub/assets/lemmy/objects/person.json @@ -1,23 +1,4 @@ { - "@context": [ - "https://www.w3.org/ns/activitystreams", - { - "comments_enabled": { - "id": "pt:commentsEnabled", - "type": "sc:Boolean" - }, - "matrixUserId": { - "id": "as:alsoKnownAs", - "type": "sc:Text" - }, - "moderators": "as:moderators", - "pt": "https://join-lemmy.org#", - "sc": "http://schema.org#", - "sensitive": "as:sensitive", - "stickied": "as:stickied" - }, - "https://w3id.org/security/v1" - ], "id": "https://enterprise.lemmy.ml/u/picard", "type": "Person", "preferredUsername": "picard", @@ -35,6 +16,7 @@ "type": "Image", "url": "https://enterprise.lemmy.ml/pictrs/image/XenaYI5hTn.png" }, + "matrix_user_id": "@picard:matrix.org", "inbox": "https://enterprise.lemmy.ml/u/picard/inbox", "outbox": "https://enterprise.lemmy.ml/u/picard/outbox", "endpoints": { diff --git a/crates/apub/assets/pleroma/activities/create_note.json b/crates/apub/assets/pleroma/activities/create_note.json new file mode 100644 index 000000000..55594ced0 --- /dev/null +++ b/crates/apub/assets/pleroma/activities/create_note.json @@ -0,0 +1,52 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://greenish.red/schemas/litepub-0.1.jsonld", + { + "@language": "und" + } + ], + "actor": "https://greenish.red/users/nutomic", + "cc": [ + "https://greenish.red/users/nutomic/followers" + ], + "context": "https://greenish.red/contexts/f6244742-0526-4b84-ac4f-ceadf1fb4e56", + "context_id": 6336544, + "directMessage": false, + "id": "https://greenish.red/activities/db61d52b-9c35-486a-bf27-bbd4edc6c6a1", + "object": { + "actor": "https://greenish.red/users/nutomic", + "attachment": [], + "attributedTo": "https://greenish.red/users/nutomic", + "cc": [ + "https://greenish.red/users/nutomic/followers" + ], + "content": "@lanodan test", + "context": "https://greenish.red/contexts/f6244742-0526-4b84-ac4f-ceadf1fb4e56", + "conversation": "https://greenish.red/contexts/f6244742-0526-4b84-ac4f-ceadf1fb4e56", + "id": "https://greenish.red/objects/1a522f2e-d5ab-454b-93d7-e58bc0650c2a", + "inReplyTo": "https://enterprise.lemmy.ml/post/55143", + "published": "2021-10-26T10:28:35.602455Z", + "sensitive": false, + "source": "@lanodan@ds9.lemmy.ml test", + "summary": "", + "tag": [ + { + "href": "https://enterprise.lemmy.ml/u/picard", + "name": "@lanodan@ds9.lemmy.ml", + "type": "Mention" + } + ], + "to": [ + "https://enterprise.lemmy.ml/u/picard", + "https://www.w3.org/ns/activitystreams#Public" + ], + "type": "Note" + }, + "published": "2021-10-26T10:28:35.595650Z", + "to": [ + "https://enterprise.lemmy.ml/u/picard", + "https://www.w3.org/ns/activitystreams#Public" + ], + "type": "Create" +} \ No newline at end of file diff --git a/crates/apub/assets/pleroma-private-message.json b/crates/apub/assets/pleroma/objects/chat_message.json similarity index 100% rename from crates/apub/assets/pleroma-private-message.json rename to crates/apub/assets/pleroma/objects/chat_message.json diff --git a/crates/apub/assets/pleroma-comment.json b/crates/apub/assets/pleroma/objects/note.json similarity index 100% rename from crates/apub/assets/pleroma-comment.json rename to crates/apub/assets/pleroma/objects/note.json diff --git a/crates/apub/assets/pleroma-person.json b/crates/apub/assets/pleroma/objects/person.json similarity index 100% rename from crates/apub/assets/pleroma-person.json rename to crates/apub/assets/pleroma/objects/person.json diff --git a/crates/apub/src/activities/comment/create_or_update.rs b/crates/apub/src/activities/comment/create_or_update.rs index 0dcf3f7d8..a9c1a9ee8 100644 --- a/crates/apub/src/activities/comment/create_or_update.rs +++ b/crates/apub/src/activities/comment/create_or_update.rs @@ -1,28 +1,9 @@ -use crate::{ - activities::{ - check_community_deleted_or_removed, - comment::{collect_non_local_mentions, get_notif_recipients}, - community::{announce::AnnouncableActivities, send_to_community}, - extract_community, - generate_activity_id, - verify_activity, - verify_person_in_community, - CreateOrUpdateType, - }, - context::lemmy_context, - fetcher::object_id::ObjectId, - objects::{ - comment::{ApubComment, Note}, - community::ApubCommunity, - person::ApubPerson, - }, -}; -use activitystreams::{base::AnyBase, link::Mention, primitives::OneOrMany, unparsed::Unparsed}; +use activitystreams::public; + use lemmy_api_common::{blocking, check_post_deleted_or_removed}; use lemmy_apub_lib::{ data::Data, - traits::{ActivityFields, ActivityHandler, ActorType, ApubObject}, - values::PublicUrl, + traits::{ActivityHandler, ActorType, ApubObject}, verify::verify_domains_match, }; use lemmy_db_schema::{ @@ -31,25 +12,22 @@ use lemmy_db_schema::{ }; use lemmy_utils::LemmyError; use lemmy_websocket::{send::send_comment_ws_message, LemmyContext, UserOperationCrud}; -use serde::{Deserialize, Serialize}; -use url::Url; -#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] -#[serde(rename_all = "camelCase")] -pub struct CreateOrUpdateComment { - actor: ObjectId, - to: [PublicUrl; 1], - object: Note, - cc: Vec, - tag: Vec, - #[serde(rename = "type")] - kind: CreateOrUpdateType, - id: Url, - #[serde(rename = "@context")] - context: OneOrMany, - #[serde(flatten)] - unparsed: Unparsed, -} +use crate::{ + activities::{ + check_community_deleted_or_removed, + comment::{collect_non_local_mentions, get_notif_recipients}, + community::{announce::GetCommunity, send_to_community}, + generate_activity_id, + verify_activity, + verify_is_public, + verify_person_in_community, + }, + activity_lists::AnnouncableActivities, + fetcher::object_id::ObjectId, + objects::{comment::ApubComment, community::ApubCommunity, person::ApubPerson}, + protocol::activities::{create_or_update::comment::CreateOrUpdateComment, CreateOrUpdateType}, +}; impl CreateOrUpdateComment { pub async fn send( @@ -76,13 +54,12 @@ impl CreateOrUpdateComment { let create_or_update = CreateOrUpdateComment { actor: ObjectId::new(actor.actor_id()), - to: [PublicUrl::Public], + to: vec![public()], object: comment.to_apub(context).await?, cc: maa.ccs, tag: maa.tags, kind, id: id.clone(), - context: lemmy_context(), unparsed: Default::default(), }; @@ -100,12 +77,12 @@ impl ActivityHandler for CreateOrUpdateComment { context: &Data, request_counter: &mut i32, ) -> Result<(), LemmyError> { - let community = extract_community(&self.cc, context, request_counter).await?; - let community_id = ObjectId::new(community.actor_id()); + verify_is_public(&self.to)?; let post = self.object.get_parents(context, request_counter).await?.0; + let community = self.get_community(context, request_counter).await?; verify_activity(self, &context.settings())?; - verify_person_in_community(&self.actor, &community_id, context, request_counter).await?; + verify_person_in_community(&self.actor, &community, context, request_counter).await?; verify_domains_match(self.actor.inner(), self.object.id_unchecked())?; check_community_deleted_or_removed(&community)?; check_post_deleted_or_removed(&post)?; @@ -135,3 +112,19 @@ impl ActivityHandler for CreateOrUpdateComment { Ok(()) } } + +#[async_trait::async_trait(?Send)] +impl GetCommunity for CreateOrUpdateComment { + async fn get_community( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result { + let post = self.object.get_parents(context, request_counter).await?.0; + let community = blocking(context.pool(), move |conn| { + Community::read(conn, post.community_id) + }) + .await??; + Ok(community.into()) + } +} diff --git a/crates/apub/src/activities/community/add_mod.rs b/crates/apub/src/activities/community/add_mod.rs index e1cf03e0d..b18e50ada 100644 --- a/crates/apub/src/activities/community/add_mod.rs +++ b/crates/apub/src/activities/community/add_mod.rs @@ -1,28 +1,24 @@ use crate::{ activities::{ - community::{announce::AnnouncableActivities, send_to_community}, + community::{announce::GetCommunity, get_community_from_moderators_url, send_to_community}, generate_activity_id, verify_activity, verify_add_remove_moderator_target, + verify_is_public, verify_mod_action, verify_person_in_community, }, - context::lemmy_context, + activity_lists::AnnouncableActivities, fetcher::object_id::ObjectId, generate_moderators_url, objects::{community::ApubCommunity, person::ApubPerson}, + protocol::activities::community::add_mod::AddMod, }; -use activitystreams::{ - activity::kind::AddType, - base::AnyBase, - primitives::OneOrMany, - unparsed::Unparsed, -}; +use activitystreams::{activity::kind::AddType, public}; use lemmy_api_common::blocking; use lemmy_apub_lib::{ data::Data, - traits::{ActivityFields, ActivityHandler, ActorType}, - values::PublicUrl, + traits::{ActivityHandler, ActorType}, }; use lemmy_db_schema::{ source::community::{CommunityModerator, CommunityModeratorForm}, @@ -30,25 +26,6 @@ use lemmy_db_schema::{ }; use lemmy_utils::LemmyError; use lemmy_websocket::LemmyContext; -use serde::{Deserialize, Serialize}; -use url::Url; - -#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] -#[serde(rename_all = "camelCase")] -pub struct AddMod { - actor: ObjectId, - to: [PublicUrl; 1], - object: ObjectId, - target: Url, - cc: [ObjectId; 1], - #[serde(rename = "type")] - kind: AddType, - id: Url, - #[serde(rename = "@context")] - context: OneOrMany, - #[serde(flatten)] - unparsed: Unparsed, -} impl AddMod { pub async fn send( @@ -63,13 +40,12 @@ impl AddMod { )?; let add = AddMod { actor: ObjectId::new(actor.actor_id()), - to: [PublicUrl::Public], + to: vec![public()], object: ObjectId::new(added_mod.actor_id()), target: generate_moderators_url(&community.actor_id)?.into(), - cc: [ObjectId::new(community.actor_id())], + cc: vec![community.actor_id()], kind: AddType::Add, id: id.clone(), - context: lemmy_context(), unparsed: Default::default(), }; @@ -88,10 +64,12 @@ impl ActivityHandler for AddMod { context: &Data, request_counter: &mut i32, ) -> Result<(), LemmyError> { + verify_is_public(&self.to)?; verify_activity(self, &context.settings())?; - verify_person_in_community(&self.actor, &self.cc[0], context, request_counter).await?; - verify_mod_action(&self.actor, &self.cc[0], context, request_counter).await?; - verify_add_remove_moderator_target(&self.target, &self.cc[0])?; + let community = self.get_community(context, request_counter).await?; + verify_person_in_community(&self.actor, &community, context, request_counter).await?; + verify_mod_action(&self.actor, &community, context, request_counter).await?; + verify_add_remove_moderator_target(&self.target, &community)?; Ok(()) } @@ -100,7 +78,7 @@ impl ActivityHandler for AddMod { context: &Data, request_counter: &mut i32, ) -> Result<(), LemmyError> { - let community = self.cc[0].dereference(context, request_counter).await?; + let community = self.get_community(context, request_counter).await?; let new_mod = self.object.dereference(context, request_counter).await?; // If we had to refetch the community while parsing the activity, then the new mod has already @@ -124,3 +102,14 @@ impl ActivityHandler for AddMod { Ok(()) } } + +#[async_trait::async_trait(?Send)] +impl GetCommunity for AddMod { + async fn get_community( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result { + get_community_from_moderators_url(&self.target, context, request_counter).await + } +} diff --git a/crates/apub/src/activities/community/announce.rs b/crates/apub/src/activities/community/announce.rs index f3dc72c56..f560b09f6 100644 --- a/crates/apub/src/activities/community/announce.rs +++ b/crates/apub/src/activities/community/announce.rs @@ -1,76 +1,28 @@ use crate::{ - activities::{ - comment::create_or_update::CreateOrUpdateComment, - community::{ - add_mod::AddMod, - block_user::BlockUserFromCommunity, - list_community_follower_inboxes, - remove_mod::RemoveMod, - undo_block_user::UndoBlockUserFromCommunity, - update::UpdateCommunity, - }, - deletion::{delete::Delete, undo_delete::UndoDelete}, - generate_activity_id, - post::create_or_update::CreateOrUpdatePost, - verify_activity, - verify_community, - voting::{undo_vote::UndoVote, vote::Vote}, - }, - context::lemmy_context, + activities::{generate_activity_id, send_lemmy_activity, verify_activity, verify_is_public}, + activity_lists::AnnouncableActivities, fetcher::object_id::ObjectId, http::is_activity_already_known, insert_activity, objects::community::ApubCommunity, - send_lemmy_activity, - CommunityType, -}; -use activitystreams::{ - activity::kind::AnnounceType, - base::AnyBase, - primitives::OneOrMany, - unparsed::Unparsed, + protocol::activities::community::announce::AnnounceActivity, }; +use activitystreams::{activity::kind::AnnounceType, public}; use lemmy_apub_lib::{ data::Data, traits::{ActivityFields, ActivityHandler, ActorType}, - values::PublicUrl, }; use lemmy_utils::LemmyError; use lemmy_websocket::LemmyContext; -use serde::{Deserialize, Serialize}; use url::Url; -#[derive(Clone, Debug, Deserialize, Serialize, ActivityHandler, ActivityFields)] -#[serde(untagged)] -#[activity_handler(LemmyContext)] -pub enum AnnouncableActivities { - CreateOrUpdateComment(CreateOrUpdateComment), - CreateOrUpdatePost(Box), - Vote(Vote), - UndoVote(UndoVote), - Delete(Delete), - UndoDelete(UndoDelete), - UpdateCommunity(Box), - BlockUserFromCommunity(BlockUserFromCommunity), - UndoBlockUserFromCommunity(UndoBlockUserFromCommunity), - AddMod(AddMod), - RemoveMod(RemoveMod), -} - -#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] -#[serde(rename_all = "camelCase")] -pub struct AnnounceActivity { - actor: ObjectId, - to: [PublicUrl; 1], - object: AnnouncableActivities, - cc: Vec, - #[serde(rename = "type")] - kind: AnnounceType, - id: Url, - #[serde(rename = "@context")] - context: OneOrMany, - #[serde(flatten)] - unparsed: Unparsed, +#[async_trait::async_trait(?Send)] +pub(crate) trait GetCommunity { + async fn get_community( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result; } impl AnnounceActivity { @@ -82,18 +34,19 @@ impl AnnounceActivity { ) -> Result<(), LemmyError> { let announce = AnnounceActivity { actor: ObjectId::new(community.actor_id()), - to: [PublicUrl::Public], + to: vec![public()], object, - cc: vec![community.followers_url()], + cc: vec![community.followers_url.clone().into_inner()], kind: AnnounceType::Announce, id: generate_activity_id( &AnnounceType::Announce, &context.settings().get_protocol_and_hostname(), )?, - context: lemmy_context(), unparsed: Default::default(), }; - let inboxes = list_community_follower_inboxes(community, additional_inboxes, context).await?; + let inboxes = community + .get_follower_inboxes(additional_inboxes, context) + .await?; send_lemmy_activity(context, &announce, &announce.id, community, inboxes, false).await } } @@ -106,8 +59,8 @@ impl ActivityHandler for AnnounceActivity { context: &Data, request_counter: &mut i32, ) -> Result<(), LemmyError> { + verify_is_public(&self.to)?; verify_activity(self, &context.settings())?; - verify_community(&self.actor, context, request_counter).await?; self.object.verify(context, request_counter).await?; Ok(()) } diff --git a/crates/apub/src/activities/community/block_user.rs b/crates/apub/src/activities/community/block_user.rs index a1c76c23b..dfe6c4c92 100644 --- a/crates/apub/src/activities/community/block_user.rs +++ b/crates/apub/src/activities/community/block_user.rs @@ -1,26 +1,22 @@ use crate::{ activities::{ - community::{announce::AnnouncableActivities, send_to_community}, + community::{announce::GetCommunity, send_to_community}, generate_activity_id, verify_activity, + verify_is_public, verify_mod_action, verify_person_in_community, }, - context::lemmy_context, + activity_lists::AnnouncableActivities, fetcher::object_id::ObjectId, objects::{community::ApubCommunity, person::ApubPerson}, + protocol::activities::community::block_user::BlockUserFromCommunity, }; -use activitystreams::{ - activity::kind::BlockType, - base::AnyBase, - primitives::OneOrMany, - unparsed::Unparsed, -}; +use activitystreams::{activity::kind::BlockType, public}; use lemmy_api_common::blocking; use lemmy_apub_lib::{ data::Data, - traits::{ActivityFields, ActivityHandler, ActorType}, - values::PublicUrl, + traits::{ActivityHandler, ActorType}, }; use lemmy_db_schema::{ source::community::{ @@ -33,24 +29,6 @@ use lemmy_db_schema::{ }; use lemmy_utils::LemmyError; use lemmy_websocket::LemmyContext; -use serde::{Deserialize, Serialize}; -use url::Url; - -#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] -#[serde(rename_all = "camelCase")] -pub struct BlockUserFromCommunity { - actor: ObjectId, - to: [PublicUrl; 1], - pub(in crate::activities::community) object: ObjectId, - cc: [ObjectId; 1], - #[serde(rename = "type")] - kind: BlockType, - id: Url, - #[serde(rename = "@context")] - context: OneOrMany, - #[serde(flatten)] - unparsed: Unparsed, -} impl BlockUserFromCommunity { pub(in crate::activities::community) fn new( @@ -61,15 +39,15 @@ impl BlockUserFromCommunity { ) -> Result { Ok(BlockUserFromCommunity { actor: ObjectId::new(actor.actor_id()), - to: [PublicUrl::Public], + to: vec![public()], object: ObjectId::new(target.actor_id()), - cc: [ObjectId::new(community.actor_id())], + cc: vec![community.actor_id()], + target: ObjectId::new(community.actor_id()), kind: BlockType::Block, id: generate_activity_id( BlockType::Block, &context.settings().get_protocol_and_hostname(), )?, - context: lemmy_context(), unparsed: Default::default(), }) } @@ -97,9 +75,11 @@ impl ActivityHandler for BlockUserFromCommunity { context: &Data, request_counter: &mut i32, ) -> Result<(), LemmyError> { + verify_is_public(&self.to)?; verify_activity(self, &context.settings())?; - verify_person_in_community(&self.actor, &self.cc[0], context, request_counter).await?; - verify_mod_action(&self.actor, &self.cc[0], context, request_counter).await?; + let community = self.get_community(context, request_counter).await?; + verify_person_in_community(&self.actor, &community, context, request_counter).await?; + verify_mod_action(&self.actor, &community, context, request_counter).await?; Ok(()) } @@ -108,7 +88,7 @@ impl ActivityHandler for BlockUserFromCommunity { context: &Data, request_counter: &mut i32, ) -> Result<(), LemmyError> { - let community = self.cc[0].dereference(context, request_counter).await?; + let community = self.get_community(context, request_counter).await?; let blocked_user = self.object.dereference(context, request_counter).await?; let community_user_ban_form = CommunityPersonBanForm { @@ -136,3 +116,14 @@ impl ActivityHandler for BlockUserFromCommunity { Ok(()) } } + +#[async_trait::async_trait(?Send)] +impl GetCommunity for BlockUserFromCommunity { + async fn get_community( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result { + self.target.dereference(context, request_counter).await + } +} diff --git a/crates/apub/src/activities/community/mod.rs b/crates/apub/src/activities/community/mod.rs index f69ee026e..96e692d58 100644 --- a/crates/apub/src/activities/community/mod.rs +++ b/crates/apub/src/activities/community/mod.rs @@ -1,12 +1,11 @@ use crate::{ - activities::community::announce::{AnnouncableActivities, AnnounceActivity}, - check_is_apub_id_valid, + activities::send_lemmy_activity, + activity_lists::AnnouncableActivities, + fetcher::object_id::ObjectId, insert_activity, objects::community::ApubCommunity, - send_lemmy_activity, - CommunityType, + protocol::activities::community::announce::AnnounceActivity, }; -use itertools::Itertools; use lemmy_apub_lib::traits::ActorType; use lemmy_utils::LemmyError; use lemmy_websocket::LemmyContext; @@ -16,31 +15,10 @@ pub mod add_mod; pub mod announce; pub mod block_user; pub mod remove_mod; +pub mod report; pub mod undo_block_user; pub mod update; -async fn list_community_follower_inboxes( - community: &ApubCommunity, - additional_inboxes: Vec, - context: &LemmyContext, -) -> Result, LemmyError> { - Ok( - vec![ - community - .get_follower_inboxes(context.pool(), &context.settings()) - .await?, - additional_inboxes, - ] - .iter() - .flatten() - .unique() - .filter(|inbox| inbox.host_str() != Some(&context.settings().hostname)) - .filter(|inbox| check_is_apub_id_valid(inbox, false, &context.settings()).is_ok()) - .map(|inbox| inbox.to_owned()) - .collect(), - ) -} - pub(crate) async fn send_to_community( activity: AnnouncableActivities, activity_id: &Url, @@ -61,3 +39,14 @@ pub(crate) async fn send_to_community( Ok(()) } + +async fn get_community_from_moderators_url( + moderators: &Url, + context: &LemmyContext, + request_counter: &mut i32, +) -> Result { + let community_id = Url::parse(&moderators.to_string().replace("/moderators", ""))?; + ObjectId::new(community_id) + .dereference(context, request_counter) + .await +} diff --git a/crates/apub/src/activities/community/remove_mod.rs b/crates/apub/src/activities/community/remove_mod.rs index 9f4c9ae62..02ff3c064 100644 --- a/crates/apub/src/activities/community/remove_mod.rs +++ b/crates/apub/src/activities/community/remove_mod.rs @@ -1,28 +1,24 @@ use crate::{ activities::{ - community::{announce::AnnouncableActivities, send_to_community}, + community::{announce::GetCommunity, get_community_from_moderators_url, send_to_community}, generate_activity_id, verify_activity, verify_add_remove_moderator_target, + verify_is_public, verify_mod_action, verify_person_in_community, }, - context::lemmy_context, + activity_lists::AnnouncableActivities, fetcher::object_id::ObjectId, generate_moderators_url, objects::{community::ApubCommunity, person::ApubPerson}, + protocol::activities::community::remove_mod::RemoveMod, }; -use activitystreams::{ - activity::kind::RemoveType, - base::AnyBase, - primitives::OneOrMany, - unparsed::Unparsed, -}; +use activitystreams::{activity::kind::RemoveType, public}; use lemmy_api_common::blocking; use lemmy_apub_lib::{ data::Data, - traits::{ActivityFields, ActivityHandler, ActorType}, - values::PublicUrl, + traits::{ActivityHandler, ActorType}, }; use lemmy_db_schema::{ source::community::{CommunityModerator, CommunityModeratorForm}, @@ -30,26 +26,6 @@ use lemmy_db_schema::{ }; use lemmy_utils::LemmyError; use lemmy_websocket::LemmyContext; -use serde::{Deserialize, Serialize}; -use url::Url; - -#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] -#[serde(rename_all = "camelCase")] -pub struct RemoveMod { - actor: ObjectId, - to: [PublicUrl; 1], - pub(in crate::activities) object: ObjectId, - cc: [ObjectId; 1], - #[serde(rename = "type")] - kind: RemoveType, - // if target is set, this is means remove mod from community - pub(in crate::activities) target: Url, - id: Url, - #[serde(rename = "@context")] - context: OneOrMany, - #[serde(flatten)] - unparsed: Unparsed, -} impl RemoveMod { pub async fn send( @@ -64,12 +40,11 @@ impl RemoveMod { )?; let remove = RemoveMod { actor: ObjectId::new(actor.actor_id()), - to: [PublicUrl::Public], + to: vec![public()], object: ObjectId::new(removed_mod.actor_id()), target: generate_moderators_url(&community.actor_id)?.into(), id: id.clone(), - context: lemmy_context(), - cc: [ObjectId::new(community.actor_id())], + cc: vec![community.actor_id()], kind: RemoveType::Remove, unparsed: Default::default(), }; @@ -88,10 +63,12 @@ impl ActivityHandler for RemoveMod { context: &Data, request_counter: &mut i32, ) -> Result<(), LemmyError> { + verify_is_public(&self.to)?; verify_activity(self, &context.settings())?; - verify_person_in_community(&self.actor, &self.cc[0], context, request_counter).await?; - verify_mod_action(&self.actor, &self.cc[0], context, request_counter).await?; - verify_add_remove_moderator_target(&self.target, &self.cc[0])?; + let community = self.get_community(context, request_counter).await?; + verify_person_in_community(&self.actor, &community, context, request_counter).await?; + verify_mod_action(&self.actor, &community, context, request_counter).await?; + verify_add_remove_moderator_target(&self.target, &community)?; Ok(()) } @@ -100,7 +77,7 @@ impl ActivityHandler for RemoveMod { context: &Data, request_counter: &mut i32, ) -> Result<(), LemmyError> { - let community = self.cc[0].dereference(context, request_counter).await?; + let community = self.get_community(context, request_counter).await?; let remove_mod = self.object.dereference(context, request_counter).await?; let form = CommunityModeratorForm { @@ -115,3 +92,14 @@ impl ActivityHandler for RemoveMod { Ok(()) } } + +#[async_trait::async_trait(?Send)] +impl GetCommunity for RemoveMod { + async fn get_community( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result { + get_community_from_moderators_url(&self.target, context, request_counter).await + } +} diff --git a/crates/apub/src/activities/report.rs b/crates/apub/src/activities/community/report.rs similarity index 81% rename from crates/apub/src/activities/report.rs rename to crates/apub/src/activities/community/report.rs index d0b1f971c..1e7cc5fad 100644 --- a/crates/apub/src/activities/report.rs +++ b/crates/apub/src/activities/community/report.rs @@ -1,21 +1,9 @@ -use crate::{ - activities::{generate_activity_id, verify_activity, verify_person_in_community}, - context::lemmy_context, - fetcher::object_id::ObjectId, - objects::{community::ApubCommunity, person::ApubPerson}, - send_lemmy_activity, - PostOrComment, -}; -use activitystreams::{ - activity::kind::FlagType, - base::AnyBase, - primitives::OneOrMany, - unparsed::Unparsed, -}; +use activitystreams::activity::kind::FlagType; + use lemmy_api_common::{blocking, comment::CommentReportResponse, post::PostReportResponse}; use lemmy_apub_lib::{ data::Data, - traits::{ActivityFields, ActivityHandler, ActorType}, + traits::{ActivityHandler, ActorType}, }; use lemmy_db_schema::{ source::{ @@ -27,24 +15,19 @@ use lemmy_db_schema::{ use lemmy_db_views::{comment_report_view::CommentReportView, post_report_view::PostReportView}; use lemmy_utils::LemmyError; use lemmy_websocket::{messages::SendModRoomMessage, LemmyContext, UserOperation}; -use serde::{Deserialize, Serialize}; -use url::Url; -#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] -#[serde(rename_all = "camelCase")] -pub struct Report { - actor: ObjectId, - to: [ObjectId; 1], - object: ObjectId, - summary: String, - #[serde(rename = "type")] - kind: FlagType, - id: Url, - #[serde(rename = "@context")] - context: OneOrMany, - #[serde(flatten)] - unparsed: Unparsed, -} +use crate::{ + activities::{ + generate_activity_id, + send_lemmy_activity, + verify_activity, + verify_person_in_community, + }, + fetcher::object_id::ObjectId, + objects::{community::ApubCommunity, person::ApubPerson}, + protocol::activities::community::report::Report, + PostOrComment, +}; impl Report { pub async fn send( @@ -67,7 +50,6 @@ impl Report { summary: reason, kind, id: id.clone(), - context: lemmy_context(), unparsed: Default::default(), }; send_lemmy_activity( @@ -91,7 +73,8 @@ impl ActivityHandler for Report { request_counter: &mut i32, ) -> Result<(), LemmyError> { verify_activity(self, &context.settings())?; - verify_person_in_community(&self.actor, &self.to[0], context, request_counter).await?; + let community = self.to[0].dereference(context, request_counter).await?; + verify_person_in_community(&self.actor, &community, context, request_counter).await?; Ok(()) } diff --git a/crates/apub/src/activities/community/undo_block_user.rs b/crates/apub/src/activities/community/undo_block_user.rs index 1614de672..2bda94441 100644 --- a/crates/apub/src/activities/community/undo_block_user.rs +++ b/crates/apub/src/activities/community/undo_block_user.rs @@ -1,30 +1,25 @@ use crate::{ activities::{ - community::{ - announce::AnnouncableActivities, - block_user::BlockUserFromCommunity, - send_to_community, - }, + community::{announce::GetCommunity, send_to_community}, generate_activity_id, verify_activity, + verify_is_public, verify_mod_action, verify_person_in_community, }, - context::lemmy_context, + activity_lists::AnnouncableActivities, fetcher::object_id::ObjectId, objects::{community::ApubCommunity, person::ApubPerson}, + protocol::activities::community::{ + block_user::BlockUserFromCommunity, + undo_block_user::UndoBlockUserFromCommunity, + }, }; -use activitystreams::{ - activity::kind::UndoType, - base::AnyBase, - primitives::OneOrMany, - unparsed::Unparsed, -}; +use activitystreams::{activity::kind::UndoType, public}; use lemmy_api_common::blocking; use lemmy_apub_lib::{ data::Data, - traits::{ActivityFields, ActivityHandler, ActorType}, - values::PublicUrl, + traits::{ActivityHandler, ActorType}, }; use lemmy_db_schema::{ source::community::{CommunityPersonBan, CommunityPersonBanForm}, @@ -32,24 +27,6 @@ use lemmy_db_schema::{ }; use lemmy_utils::LemmyError; use lemmy_websocket::LemmyContext; -use serde::{Deserialize, Serialize}; -use url::Url; - -#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] -#[serde(rename_all = "camelCase")] -pub struct UndoBlockUserFromCommunity { - actor: ObjectId, - to: [PublicUrl; 1], - object: BlockUserFromCommunity, - cc: [ObjectId; 1], - #[serde(rename = "type")] - kind: UndoType, - id: Url, - #[serde(rename = "@context")] - context: OneOrMany, - #[serde(flatten)] - unparsed: Unparsed, -} impl UndoBlockUserFromCommunity { pub async fn send( @@ -66,12 +43,11 @@ impl UndoBlockUserFromCommunity { )?; let undo = UndoBlockUserFromCommunity { actor: ObjectId::new(actor.actor_id()), - to: [PublicUrl::Public], + to: vec![public()], object: block, - cc: [ObjectId::new(community.actor_id())], + cc: vec![community.actor_id()], kind: UndoType::Undo, id: id.clone(), - context: lemmy_context(), unparsed: Default::default(), }; @@ -89,9 +65,11 @@ impl ActivityHandler for UndoBlockUserFromCommunity { context: &Data, request_counter: &mut i32, ) -> Result<(), LemmyError> { + verify_is_public(&self.to)?; verify_activity(self, &context.settings())?; - verify_person_in_community(&self.actor, &self.cc[0], context, request_counter).await?; - verify_mod_action(&self.actor, &self.cc[0], context, request_counter).await?; + let community = self.get_community(context, request_counter).await?; + verify_person_in_community(&self.actor, &community, context, request_counter).await?; + verify_mod_action(&self.actor, &community, context, request_counter).await?; self.object.verify(context, request_counter).await?; Ok(()) } @@ -101,7 +79,7 @@ impl ActivityHandler for UndoBlockUserFromCommunity { context: &Data, request_counter: &mut i32, ) -> Result<(), LemmyError> { - let community = self.cc[0].dereference(context, request_counter).await?; + let community = self.get_community(context, request_counter).await?; let blocked_user = self .object .object @@ -121,3 +99,14 @@ impl ActivityHandler for UndoBlockUserFromCommunity { Ok(()) } } + +#[async_trait::async_trait(?Send)] +impl GetCommunity for UndoBlockUserFromCommunity { + async fn get_community( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result { + self.object.get_community(context, request_counter).await + } +} diff --git a/crates/apub/src/activities/community/update.rs b/crates/apub/src/activities/community/update.rs index f6d693dd6..28de0db00 100644 --- a/crates/apub/src/activities/community/update.rs +++ b/crates/apub/src/activities/community/update.rs @@ -1,29 +1,22 @@ use crate::{ activities::{ - community::{announce::AnnouncableActivities, send_to_community}, + community::{announce::GetCommunity, send_to_community}, generate_activity_id, verify_activity, + verify_is_public, verify_mod_action, verify_person_in_community, }, - context::lemmy_context, + activity_lists::AnnouncableActivities, fetcher::object_id::ObjectId, - objects::{ - community::{ApubCommunity, Group}, - person::ApubPerson, - }, -}; -use activitystreams::{ - activity::kind::UpdateType, - base::AnyBase, - primitives::OneOrMany, - unparsed::Unparsed, + objects::{community::ApubCommunity, person::ApubPerson}, + protocol::{activities::community::update::UpdateCommunity, objects::group::Group}, }; +use activitystreams::{activity::kind::UpdateType, public}; use lemmy_api_common::blocking; use lemmy_apub_lib::{ data::Data, - traits::{ActivityFields, ActivityHandler, ActorType, ApubObject}, - values::PublicUrl, + traits::{ActivityHandler, ActorType, ApubObject}, }; use lemmy_db_schema::{ source::community::{Community, CommunityForm}, @@ -31,27 +24,6 @@ use lemmy_db_schema::{ }; use lemmy_utils::LemmyError; use lemmy_websocket::{send::send_community_ws_message, LemmyContext, UserOperationCrud}; -use serde::{Deserialize, Serialize}; -use url::Url; - -/// This activity is received from a remote community mod, and updates the description or other -/// fields of a local community. -#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] -#[serde(rename_all = "camelCase")] -pub struct UpdateCommunity { - actor: ObjectId, - to: [PublicUrl; 1], - // TODO: would be nice to use a separate struct here, which only contains the fields updated here - object: Group, - cc: [ObjectId; 1], - #[serde(rename = "type")] - kind: UpdateType, - id: Url, - #[serde(rename = "@context")] - context: OneOrMany, - #[serde(flatten)] - unparsed: Unparsed, -} impl UpdateCommunity { pub async fn send( @@ -65,12 +37,11 @@ impl UpdateCommunity { )?; let update = UpdateCommunity { actor: ObjectId::new(actor.actor_id()), - to: [PublicUrl::Public], + to: vec![public()], object: community.to_apub(context).await?, - cc: [ObjectId::new(community.actor_id())], + cc: vec![community.actor_id()], kind: UpdateType::Update, id: id.clone(), - context: lemmy_context(), unparsed: Default::default(), }; @@ -87,9 +58,11 @@ impl ActivityHandler for UpdateCommunity { context: &Data, request_counter: &mut i32, ) -> Result<(), LemmyError> { + verify_is_public(&self.to)?; verify_activity(self, &context.settings())?; - verify_person_in_community(&self.actor, &self.cc[0], context, request_counter).await?; - verify_mod_action(&self.actor, &self.cc[0], context, request_counter).await?; + let community = self.get_community(context, request_counter).await?; + verify_person_in_community(&self.actor, &community, context, request_counter).await?; + verify_mod_action(&self.actor, &community, context, request_counter).await?; Ok(()) } @@ -98,8 +71,7 @@ impl ActivityHandler for UpdateCommunity { context: &Data, request_counter: &mut i32, ) -> Result<(), LemmyError> { - let cc = self.cc[0].clone(); - let community = cc.dereference(context, request_counter).await?; + let community = self.get_community(context, request_counter).await?; let updated_community = Group::from_apub_to_form( &self.object, @@ -133,3 +105,15 @@ impl ActivityHandler for UpdateCommunity { Ok(()) } } + +#[async_trait::async_trait(?Send)] +impl GetCommunity for UpdateCommunity { + async fn get_community( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result { + let cid = ObjectId::new(self.object.id.clone()); + cid.dereference(context, request_counter).await + } +} diff --git a/crates/apub/src/activities/deletion/delete.rs b/crates/apub/src/activities/deletion/delete.rs index 02df7dc38..672fca52a 100644 --- a/crates/apub/src/activities/deletion/delete.rs +++ b/crates/apub/src/activities/deletion/delete.rs @@ -1,31 +1,11 @@ -use crate::{ - activities::{ - community::{announce::AnnouncableActivities, send_to_community}, - deletion::{ - receive_delete_action, - verify_delete_activity, - DeletableObjects, - WebsocketMessages, - }, - generate_activity_id, - verify_activity, - }, - context::lemmy_context, - fetcher::object_id::ObjectId, - objects::{community::ApubCommunity, person::ApubPerson}, -}; -use activitystreams::{ - activity::kind::DeleteType, - base::AnyBase, - primitives::OneOrMany, - unparsed::Unparsed, -}; +use activitystreams::{activity::kind::DeleteType, public}; use anyhow::anyhow; +use url::Url; + use lemmy_api_common::blocking; use lemmy_apub_lib::{ data::Data, - traits::{ActivityFields, ActivityHandler, ActorType}, - values::PublicUrl, + traits::{ActivityHandler, ActorType}, }; use lemmy_db_schema::{ source::{ @@ -49,37 +29,25 @@ use lemmy_websocket::{ LemmyContext, UserOperationCrud, }; -use serde::{Deserialize, Serialize}; -use serde_with::skip_serializing_none; -use url::Url; -/// This is very confusing, because there are four distinct cases to handle: -/// - user deletes their post -/// - user deletes their comment -/// - remote community mod deletes local community -/// - remote community deletes itself (triggered by a mod) -/// -/// TODO: we should probably change how community deletions work to simplify this. Probably by -/// wrapping it in an announce just like other activities, instead of having the community send it. -#[skip_serializing_none] -#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] -#[serde(rename_all = "camelCase")] -pub struct Delete { - actor: ObjectId, - to: [PublicUrl; 1], - pub(in crate::activities::deletion) object: Url, - pub(in crate::activities::deletion) cc: [ObjectId; 1], - #[serde(rename = "type")] - kind: DeleteType, - /// If summary is present, this is a mod action (Remove in Lemmy terms). Otherwise, its a user - /// deleting their own content. - pub(in crate::activities::deletion) summary: Option, - id: Url, - #[serde(rename = "@context")] - context: OneOrMany, - #[serde(flatten)] - unparsed: Unparsed, -} +use crate::{ + activities::{ + community::{announce::GetCommunity, send_to_community}, + deletion::{ + receive_delete_action, + verify_delete_activity, + DeletableObjects, + WebsocketMessages, + }, + generate_activity_id, + verify_activity, + verify_is_public, + }, + activity_lists::AnnouncableActivities, + fetcher::object_id::ObjectId, + objects::{community::ApubCommunity, person::ApubPerson}, + protocol::activities::deletion::delete::Delete, +}; #[async_trait::async_trait(?Send)] impl ActivityHandler for Delete { @@ -89,11 +57,13 @@ impl ActivityHandler for Delete { context: &Data, request_counter: &mut i32, ) -> Result<(), LemmyError> { + verify_is_public(&self.to)?; verify_activity(self, &context.settings())?; + let community = self.get_community(context, request_counter).await?; verify_delete_activity( &self.object, self, - &self.cc[0], + &community, self.summary.is_some(), context, request_counter, @@ -144,16 +114,15 @@ impl Delete { ) -> Result { Ok(Delete { actor: ObjectId::new(actor.actor_id()), - to: [PublicUrl::Public], + to: vec![public()], object: object_id, - cc: [ObjectId::new(community.actor_id())], + cc: vec![community.actor_id()], kind: DeleteType::Delete, summary, id: generate_activity_id( DeleteType::Delete, &context.settings().get_protocol_and_hostname(), )?, - context: lemmy_context(), unparsed: Default::default(), }) } @@ -243,3 +212,26 @@ pub(in crate::activities) async fn receive_remove_action( } Ok(()) } + +#[async_trait::async_trait(?Send)] +impl GetCommunity for Delete { + async fn get_community( + &self, + context: &LemmyContext, + _request_counter: &mut i32, + ) -> Result { + let community_id = match DeletableObjects::read_from_db(&self.object, context).await? { + DeletableObjects::Community(c) => c.id, + DeletableObjects::Comment(c) => { + let post = blocking(context.pool(), move |conn| Post::read(conn, c.post_id)).await??; + post.community_id + } + DeletableObjects::Post(p) => p.community_id, + }; + let community = blocking(context.pool(), move |conn| { + Community::read(conn, community_id) + }) + .await??; + Ok(community.into()) + } +} diff --git a/crates/apub/src/activities/deletion/mod.rs b/crates/apub/src/activities/deletion/mod.rs index 4352afe87..b9c11291f 100644 --- a/crates/apub/src/activities/deletion/mod.rs +++ b/crates/apub/src/activities/deletion/mod.rs @@ -1,12 +1,5 @@ -use crate::{ - activities::{ - deletion::{delete::Delete, undo_delete::UndoDelete}, - verify_mod_action, - verify_person_in_community, - }, - fetcher::object_id::ObjectId, - objects::{comment::ApubComment, community::ApubCommunity, person::ApubPerson, post::ApubPost}, -}; +use url::Url; + use lemmy_api_common::blocking; use lemmy_apub_lib::{ traits::{ActivityFields, ActorType, ApubObject}, @@ -19,7 +12,13 @@ use lemmy_websocket::{ LemmyContext, UserOperationCrud, }; -use url::Url; + +use crate::{ + activities::{verify_mod_action, verify_person_in_community}, + fetcher::object_id::ObjectId, + objects::{comment::ApubComment, community::ApubCommunity, person::ApubPerson, post::ApubPost}, + protocol::activities::deletion::{delete::Delete, undo_delete::UndoDelete}, +}; pub mod delete; pub mod undo_delete; @@ -82,7 +81,7 @@ impl DeletableObjects { pub(in crate::activities) async fn verify_delete_activity( object: &Url, activity: &dyn ActivityFields, - community_id: &ObjectId, + community: &ApubCommunity, is_mod_action: bool, context: &LemmyContext, request_counter: &mut i32, @@ -90,26 +89,20 @@ pub(in crate::activities) async fn verify_delete_activity( let object = DeletableObjects::read_from_db(object, context).await?; let actor = ObjectId::new(activity.actor().clone()); match object { - DeletableObjects::Community(c) => { - if c.local { + DeletableObjects::Community(community) => { + if community.local { // can only do this check for local community, in remote case it would try to fetch the // deleted community (which fails) - verify_person_in_community(&actor, community_id, context, request_counter).await?; + verify_person_in_community(&actor, &community, context, request_counter).await?; } // community deletion is always a mod (or admin) action - verify_mod_action( - &actor, - &ObjectId::new(c.actor_id()), - context, - request_counter, - ) - .await?; + verify_mod_action(&actor, &community, context, request_counter).await?; } DeletableObjects::Post(p) => { verify_delete_activity_post_or_comment( activity, &p.ap_id.clone().into(), - community_id, + community, is_mod_action, context, request_counter, @@ -120,7 +113,7 @@ pub(in crate::activities) async fn verify_delete_activity( verify_delete_activity_post_or_comment( activity, &c.ap_id.clone().into(), - community_id, + community, is_mod_action, context, request_counter, @@ -134,15 +127,15 @@ pub(in crate::activities) async fn verify_delete_activity( async fn verify_delete_activity_post_or_comment( activity: &dyn ActivityFields, object_id: &Url, - community_id: &ObjectId, + community: &ApubCommunity, is_mod_action: bool, context: &LemmyContext, request_counter: &mut i32, ) -> Result<(), LemmyError> { let actor = ObjectId::new(activity.actor().clone()); - verify_person_in_community(&actor, community_id, context, request_counter).await?; + verify_person_in_community(&actor, community, context, request_counter).await?; if is_mod_action { - verify_mod_action(&actor, community_id, context, request_counter).await?; + verify_mod_action(&actor, community, context, request_counter).await?; } else { // domain of post ap_id and post.creator ap_id are identical, so we just check the former verify_domains_match(activity.actor(), object_id)?; diff --git a/crates/apub/src/activities/deletion/undo_delete.rs b/crates/apub/src/activities/deletion/undo_delete.rs index 327bf86c0..c16891190 100644 --- a/crates/apub/src/activities/deletion/undo_delete.rs +++ b/crates/apub/src/activities/deletion/undo_delete.rs @@ -1,32 +1,11 @@ -use crate::{ - activities::{ - community::{announce::AnnouncableActivities, send_to_community}, - deletion::{ - delete::Delete, - receive_delete_action, - verify_delete_activity, - DeletableObjects, - WebsocketMessages, - }, - generate_activity_id, - verify_activity, - }, - context::lemmy_context, - fetcher::object_id::ObjectId, - objects::{community::ApubCommunity, person::ApubPerson}, -}; -use activitystreams::{ - activity::kind::UndoType, - base::AnyBase, - primitives::OneOrMany, - unparsed::Unparsed, -}; +use activitystreams::{activity::kind::UndoType, public}; use anyhow::anyhow; +use url::Url; + use lemmy_api_common::blocking; use lemmy_apub_lib::{ data::Data, - traits::{ActivityFields, ActivityHandler, ActorType}, - values::PublicUrl, + traits::{ActivityHandler, ActorType}, }; use lemmy_db_schema::source::{comment::Comment, community::Community, post::Post}; use lemmy_utils::LemmyError; @@ -35,24 +14,25 @@ use lemmy_websocket::{ LemmyContext, UserOperationCrud, }; -use serde::{Deserialize, Serialize}; -use url::Url; -#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] -#[serde(rename_all = "camelCase")] -pub struct UndoDelete { - actor: ObjectId, - to: [PublicUrl; 1], - object: Delete, - cc: [ObjectId; 1], - #[serde(rename = "type")] - kind: UndoType, - id: Url, - #[serde(rename = "@context")] - context: OneOrMany, - #[serde(flatten)] - unparsed: Unparsed, -} +use crate::{ + activities::{ + community::{announce::GetCommunity, send_to_community}, + deletion::{ + receive_delete_action, + verify_delete_activity, + DeletableObjects, + WebsocketMessages, + }, + generate_activity_id, + verify_activity, + verify_is_public, + }, + activity_lists::AnnouncableActivities, + fetcher::object_id::ObjectId, + objects::{community::ApubCommunity, person::ApubPerson}, + protocol::activities::deletion::{delete::Delete, undo_delete::UndoDelete}, +}; #[async_trait::async_trait(?Send)] impl ActivityHandler for UndoDelete { @@ -62,12 +42,14 @@ impl ActivityHandler for UndoDelete { context: &Data, request_counter: &mut i32, ) -> Result<(), LemmyError> { + verify_is_public(&self.to)?; verify_activity(self, &context.settings())?; self.object.verify(context, request_counter).await?; + let community = self.get_community(context, request_counter).await?; verify_delete_activity( &self.object.object, self, - &self.cc[0], + &community, self.object.summary.is_some(), context, request_counter, @@ -117,12 +99,11 @@ impl UndoDelete { )?; let undo = UndoDelete { actor: ObjectId::new(actor.actor_id()), - to: [PublicUrl::Public], + to: vec![public()], object, - cc: [ObjectId::new(community.actor_id())], + cc: vec![community.actor_id()], kind: UndoType::Undo, id: id.clone(), - context: lemmy_context(), unparsed: Default::default(), }; @@ -164,3 +145,14 @@ impl UndoDelete { Ok(()) } } + +#[async_trait::async_trait(?Send)] +impl GetCommunity for UndoDelete { + async fn get_community( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result { + self.object.get_community(context, request_counter).await + } +} diff --git a/crates/apub/src/activities/following/accept.rs b/crates/apub/src/activities/following/accept.rs index 6fa65e7b1..984d622a8 100644 --- a/crates/apub/src/activities/following/accept.rs +++ b/crates/apub/src/activities/following/accept.rs @@ -1,21 +1,9 @@ use crate::{ - activities::{ - following::follow::FollowCommunity, - generate_activity_id, - verify_activity, - verify_community, - }, - context::lemmy_context, + activities::{generate_activity_id, send_lemmy_activity, verify_activity}, fetcher::object_id::ObjectId, - objects::{community::ApubCommunity, person::ApubPerson}, - send_lemmy_activity, -}; -use activitystreams::{ - activity::kind::AcceptType, - base::AnyBase, - primitives::OneOrMany, - unparsed::Unparsed, + protocol::activities::following::{accept::AcceptFollowCommunity, follow::FollowCommunity}, }; +use activitystreams::activity::kind::AcceptType; use lemmy_api_common::blocking; use lemmy_apub_lib::{ data::Data, @@ -25,23 +13,6 @@ use lemmy_apub_lib::{ use lemmy_db_schema::{source::community::CommunityFollower, traits::Followable}; use lemmy_utils::LemmyError; use lemmy_websocket::LemmyContext; -use serde::{Deserialize, Serialize}; -use url::Url; - -#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] -#[serde(rename_all = "camelCase")] -pub struct AcceptFollowCommunity { - actor: ObjectId, - to: ObjectId, - object: FollowCommunity, - #[serde(rename = "type")] - kind: AcceptType, - id: Url, - #[serde(rename = "@context")] - context: OneOrMany, - #[serde(flatten)] - unparsed: Unparsed, -} impl AcceptFollowCommunity { pub async fn send( @@ -57,14 +28,13 @@ impl AcceptFollowCommunity { .await?; let accept = AcceptFollowCommunity { actor: ObjectId::new(community.actor_id()), - to: ObjectId::new(person.actor_id()), + to: [ObjectId::new(person.actor_id())], object: follow, kind: AcceptType::Accept, id: generate_activity_id( AcceptType::Accept, &context.settings().get_protocol_and_hostname(), )?, - context: lemmy_context(), unparsed: Default::default(), }; let inbox = vec![person.inbox_url()]; @@ -82,9 +52,8 @@ impl ActivityHandler for AcceptFollowCommunity { request_counter: &mut i32, ) -> Result<(), LemmyError> { verify_activity(self, &context.settings())?; - verify_urls_match(self.to.inner(), self.object.actor())?; - verify_urls_match(self.actor(), self.object.to.inner())?; - verify_community(&self.actor, context, request_counter).await?; + verify_urls_match(self.to[0].inner(), self.object.actor())?; + verify_urls_match(self.actor(), self.object.to[0].inner())?; self.object.verify(context, request_counter).await?; Ok(()) } @@ -95,7 +64,7 @@ impl ActivityHandler for AcceptFollowCommunity { request_counter: &mut i32, ) -> Result<(), LemmyError> { let actor = self.actor.dereference(context, request_counter).await?; - let to = self.to.dereference(context, request_counter).await?; + let to = self.to[0].dereference(context, request_counter).await?; // This will throw an error if no follow was requested blocking(context.pool(), move |conn| { CommunityFollower::follow_accepted(conn, actor.id, to.id) diff --git a/crates/apub/src/activities/following/follow.rs b/crates/apub/src/activities/following/follow.rs index 144243812..e048907fe 100644 --- a/crates/apub/src/activities/following/follow.rs +++ b/crates/apub/src/activities/following/follow.rs @@ -1,25 +1,20 @@ use crate::{ activities::{ - following::accept::AcceptFollowCommunity, generate_activity_id, + send_lemmy_activity, verify_activity, verify_person, + verify_person_in_community, }, - context::lemmy_context, fetcher::object_id::ObjectId, objects::{community::ApubCommunity, person::ApubPerson}, - send_lemmy_activity, -}; -use activitystreams::{ - activity::kind::FollowType, - base::AnyBase, - primitives::OneOrMany, - unparsed::Unparsed, + protocol::activities::following::{accept::AcceptFollowCommunity, follow::FollowCommunity}, }; +use activitystreams::activity::kind::FollowType; use lemmy_api_common::blocking; use lemmy_apub_lib::{ data::Data, - traits::{ActivityFields, ActivityHandler, ActorType}, + traits::{ActivityHandler, ActorType}, verify::verify_urls_match, }; use lemmy_db_schema::{ @@ -28,24 +23,6 @@ use lemmy_db_schema::{ }; use lemmy_utils::LemmyError; use lemmy_websocket::LemmyContext; -use serde::{Deserialize, Serialize}; -use url::Url; - -#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] -#[serde(rename_all = "camelCase")] -pub struct FollowCommunity { - pub(in crate::activities::following) actor: ObjectId, - // TODO: is there any reason to put the same community id twice, in to and object? - pub(in crate::activities::following) to: ObjectId, - pub(in crate::activities::following) object: ObjectId, - #[serde(rename = "type")] - kind: FollowType, - id: Url, - #[serde(rename = "@context")] - context: OneOrMany, - #[serde(flatten)] - unparsed: Unparsed, -} impl FollowCommunity { pub(in crate::activities::following) fn new( @@ -55,14 +32,13 @@ impl FollowCommunity { ) -> Result { Ok(FollowCommunity { actor: ObjectId::new(actor.actor_id()), - to: ObjectId::new(community.actor_id()), + to: [ObjectId::new(community.actor_id())], object: ObjectId::new(community.actor_id()), kind: FollowType::Follow, id: generate_activity_id( FollowType::Follow, &context.settings().get_protocol_and_hostname(), )?, - context: lemmy_context(), unparsed: Default::default(), }) } @@ -96,8 +72,10 @@ impl ActivityHandler for FollowCommunity { request_counter: &mut i32, ) -> Result<(), LemmyError> { verify_activity(self, &context.settings())?; - verify_urls_match(self.to.inner(), self.object.inner())?; + verify_urls_match(self.to[0].inner(), self.object.inner())?; verify_person(&self.actor, context, request_counter).await?; + let community = self.to[0].dereference(context, request_counter).await?; + verify_person_in_community(&self.actor, &community, context, request_counter).await?; Ok(()) } @@ -107,7 +85,7 @@ impl ActivityHandler for FollowCommunity { request_counter: &mut i32, ) -> Result<(), LemmyError> { let actor = self.actor.dereference(context, request_counter).await?; - let community = self.object.dereference(context, request_counter).await?; + let community = self.to[0].dereference(context, request_counter).await?; let community_follower_form = CommunityFollowerForm { community_id: community.id, person_id: actor.id, diff --git a/crates/apub/src/activities/following/mod.rs b/crates/apub/src/activities/following/mod.rs index 050c36916..60bdd5f78 100644 --- a/crates/apub/src/activities/following/mod.rs +++ b/crates/apub/src/activities/following/mod.rs @@ -1,3 +1,3 @@ pub mod accept; pub mod follow; -pub mod undo; +pub mod undo_follow; diff --git a/crates/apub/src/activities/following/undo.rs b/crates/apub/src/activities/following/undo_follow.rs similarity index 69% rename from crates/apub/src/activities/following/undo.rs rename to crates/apub/src/activities/following/undo_follow.rs index 5c548ae48..c3fd78b51 100644 --- a/crates/apub/src/activities/following/undo.rs +++ b/crates/apub/src/activities/following/undo_follow.rs @@ -1,21 +1,10 @@ use crate::{ - activities::{ - following::follow::FollowCommunity, - generate_activity_id, - verify_activity, - verify_person, - }, - context::lemmy_context, + activities::{generate_activity_id, send_lemmy_activity, verify_activity, verify_person}, fetcher::object_id::ObjectId, objects::{community::ApubCommunity, person::ApubPerson}, - send_lemmy_activity, -}; -use activitystreams::{ - activity::kind::UndoType, - base::AnyBase, - primitives::OneOrMany, - unparsed::Unparsed, + protocol::activities::following::{follow::FollowCommunity, undo_follow::UndoFollowCommunity}, }; +use activitystreams::activity::kind::UndoType; use lemmy_api_common::blocking; use lemmy_apub_lib::{ data::Data, @@ -28,23 +17,6 @@ use lemmy_db_schema::{ }; use lemmy_utils::LemmyError; use lemmy_websocket::LemmyContext; -use serde::{Deserialize, Serialize}; -use url::Url; - -#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] -#[serde(rename_all = "camelCase")] -pub struct UndoFollowCommunity { - actor: ObjectId, - to: ObjectId, - object: FollowCommunity, - #[serde(rename = "type")] - kind: UndoType, - id: Url, - #[serde(rename = "@context")] - context: OneOrMany, - #[serde(flatten)] - unparsed: Unparsed, -} impl UndoFollowCommunity { pub async fn send( @@ -55,14 +27,13 @@ impl UndoFollowCommunity { let object = FollowCommunity::new(actor, community, context)?; let undo = UndoFollowCommunity { actor: ObjectId::new(actor.actor_id()), - to: ObjectId::new(community.actor_id()), + to: [ObjectId::new(community.actor_id())], object, kind: UndoType::Undo, id: generate_activity_id( UndoType::Undo, &context.settings().get_protocol_and_hostname(), )?, - context: lemmy_context(), unparsed: Default::default(), }; let inbox = vec![community.shared_inbox_or_inbox_url()]; @@ -79,7 +50,7 @@ impl ActivityHandler for UndoFollowCommunity { request_counter: &mut i32, ) -> Result<(), LemmyError> { verify_activity(self, &context.settings())?; - verify_urls_match(self.to.inner(), self.object.object.inner())?; + verify_urls_match(self.to[0].inner(), self.object.object.inner())?; verify_urls_match(self.actor(), self.object.actor())?; verify_person(&self.actor, context, request_counter).await?; self.object.verify(context, request_counter).await?; @@ -92,7 +63,7 @@ impl ActivityHandler for UndoFollowCommunity { request_counter: &mut i32, ) -> Result<(), LemmyError> { let actor = self.actor.dereference(context, request_counter).await?; - let community = self.to.dereference(context, request_counter).await?; + let community = self.to[0].dereference(context, request_counter).await?; let community_follower_form = CommunityFollowerForm { community_id: community.id, diff --git a/crates/apub/src/activities/mod.rs b/crates/apub/src/activities/mod.rs index 14a67394e..bc6cfb512 100644 --- a/crates/apub/src/activities/mod.rs +++ b/crates/apub/src/activities/mod.rs @@ -1,21 +1,28 @@ use crate::{ - check_community_or_site_ban, check_is_apub_id_valid, + context::WithContext, fetcher::object_id::ObjectId, generate_moderators_url, + insert_activity, objects::{community::ApubCommunity, person::ApubPerson}, }; use activitystreams::public; use anyhow::anyhow; use lemmy_api_common::blocking; -use lemmy_apub_lib::{traits::ActivityFields, verify::verify_domains_match}; +use lemmy_apub_lib::{ + activity_queue::send_activity, + traits::{ActivityFields, ActorType}, + verify::verify_domains_match, +}; use lemmy_db_schema::source::community::Community; -use lemmy_db_views_actor::community_view::CommunityView; +use lemmy_db_views_actor::{ + community_person_ban_view::CommunityPersonBanView, + community_view::CommunityView, +}; use lemmy_utils::{settings::structs::Settings, LemmyError}; use lemmy_websocket::LemmyContext; -use serde::{Deserialize, Serialize}; -use std::ops::Deref; -use strum_macros::ToString; +use log::info; +use serde::Serialize; use url::{ParseError, Url}; use uuid::Uuid; @@ -25,15 +32,8 @@ pub mod deletion; pub mod following; pub mod post; pub mod private_message; -pub mod report; pub mod voting; -#[derive(Clone, Debug, ToString, Deserialize, Serialize)] -pub enum CreateOrUpdateType { - Create, - Update, -} - /// Checks that the specified Url actually identifies a Person (by fetching it), and that the person /// doesn't have a site ban. async fn verify_person( @@ -48,44 +48,26 @@ async fn verify_person( Ok(()) } -pub(crate) async fn extract_community( - cc: &[Url], - context: &LemmyContext, - request_counter: &mut i32, -) -> Result { - let mut cc_iter = cc.iter(); - loop { - if let Some(cid) = cc_iter.next() { - let cid = ObjectId::new(cid.clone()); - if let Ok(c) = cid.dereference(context, request_counter).await { - break Ok(c); - } - } else { - return Err(anyhow!("No community found in cc").into()); - } - } -} - /// Fetches the person and community to verify their type, then checks if person is banned from site /// or community. pub(crate) async fn verify_person_in_community( person_id: &ObjectId, - community_id: &ObjectId, + community: &ApubCommunity, context: &LemmyContext, request_counter: &mut i32, ) -> Result<(), LemmyError> { - let community = community_id.dereference(context, request_counter).await?; let person = person_id.dereference(context, request_counter).await?; - check_community_or_site_ban(person.deref(), community.id, context.pool()).await -} + if person.banned { + return Err(anyhow!("Person is banned from site").into()); + } + let person_id = person.id; + let community_id = community.id; + let is_banned = + move |conn: &'_ _| CommunityPersonBanView::get(conn, person_id, community_id).is_ok(); + if blocking(context.pool(), is_banned).await? { + return Err(anyhow!("Person is banned from community").into()); + } -/// Simply check that the url actually refers to a valid group. -async fn verify_community( - community_id: &ObjectId, - context: &LemmyContext, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - community_id.dereference(context, request_counter).await?; Ok(()) } @@ -100,12 +82,10 @@ fn verify_activity(activity: &dyn ActivityFields, settings: &Settings) -> Result /// is not federated, we cant verify their actions remotely. pub(crate) async fn verify_mod_action( actor_id: &ObjectId, - community_id: &ObjectId, + community: &ApubCommunity, context: &LemmyContext, request_counter: &mut i32, ) -> Result<(), LemmyError> { - let community = community_id.dereference_local(context).await?; - if community.local { let actor = actor_id.dereference(context, request_counter).await?; @@ -128,9 +108,9 @@ pub(crate) async fn verify_mod_action( /// /c/community/moderators. Any different values are unsupported. fn verify_add_remove_moderator_target( target: &Url, - community: &ObjectId, + community: &ApubCommunity, ) -> Result<(), LemmyError> { - if target != &generate_moderators_url(&community.clone().into())?.into_inner() { + if target != &generate_moderators_url(&community.actor_id)?.into_inner() { return Err(anyhow!("Unkown target url").into()); } Ok(()) @@ -165,3 +145,47 @@ where ); Url::parse(&id) } + +async fn send_lemmy_activity( + context: &LemmyContext, + activity: &T, + activity_id: &Url, + actor: &dyn ActorType, + inboxes: Vec, + sensitive: bool, +) -> Result<(), LemmyError> { + if !context.settings().federation.enabled || inboxes.is_empty() { + return Ok(()); + } + let activity = WithContext::new(activity); + + info!("Sending activity {}", activity_id.to_string()); + + // Don't send anything to ourselves + // TODO: this should be a debug assert + let hostname = context.settings().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?; + + send_activity( + serialised_activity, + actor, + inboxes, + context.client(), + context.activity_queue(), + ) + .await +} diff --git a/crates/apub/src/activities/post/create_or_update.rs b/crates/apub/src/activities/post/create_or_update.rs index 4a048a965..41590493c 100644 --- a/crates/apub/src/activities/post/create_or_update.rs +++ b/crates/apub/src/activities/post/create_or_update.rs @@ -1,51 +1,31 @@ -use crate::{ - activities::{ - check_community_deleted_or_removed, - community::{announce::AnnouncableActivities, send_to_community}, - generate_activity_id, - verify_activity, - verify_mod_action, - verify_person_in_community, - CreateOrUpdateType, - }, - context::lemmy_context, - fetcher::object_id::ObjectId, - objects::{ - community::ApubCommunity, - person::ApubPerson, - post::{ApubPost, Page}, - }, -}; -use activitystreams::{base::AnyBase, primitives::OneOrMany, unparsed::Unparsed}; +use activitystreams::public; use anyhow::anyhow; + use lemmy_api_common::blocking; use lemmy_apub_lib::{ data::Data, traits::{ActivityFields, ActivityHandler, ActorType, ApubObject}, - values::PublicUrl, verify::{verify_domains_match, verify_urls_match}, }; use lemmy_db_schema::{source::community::Community, traits::Crud}; use lemmy_utils::LemmyError; use lemmy_websocket::{send::send_post_ws_message, LemmyContext, UserOperationCrud}; -use serde::{Deserialize, Serialize}; -use url::Url; -#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] -#[serde(rename_all = "camelCase")] -pub struct CreateOrUpdatePost { - actor: ObjectId, - to: [PublicUrl; 1], - object: Page, - cc: [ObjectId; 1], - #[serde(rename = "type")] - kind: CreateOrUpdateType, - id: Url, - #[serde(rename = "@context")] - context: OneOrMany, - #[serde(flatten)] - unparsed: Unparsed, -} +use crate::{ + activities::{ + check_community_deleted_or_removed, + community::{announce::GetCommunity, send_to_community}, + generate_activity_id, + verify_activity, + verify_is_public, + verify_mod_action, + verify_person_in_community, + }, + activity_lists::AnnouncableActivities, + fetcher::object_id::ObjectId, + objects::{community::ApubCommunity, person::ApubPerson, post::ApubPost}, + protocol::activities::{create_or_update::post::CreateOrUpdatePost, CreateOrUpdateType}, +}; impl CreateOrUpdatePost { pub(crate) async fn new( @@ -61,12 +41,11 @@ impl CreateOrUpdatePost { )?; Ok(CreateOrUpdatePost { actor: ObjectId::new(actor.actor_id()), - to: [PublicUrl::Public], + to: vec![public()], object: post.to_apub(context).await?, - cc: [ObjectId::new(community.actor_id())], + cc: vec![community.actor_id()], kind, id: id.clone(), - context: lemmy_context(), unparsed: Default::default(), }) } @@ -97,9 +76,10 @@ impl ActivityHandler for CreateOrUpdatePost { context: &Data, request_counter: &mut i32, ) -> Result<(), LemmyError> { + verify_is_public(&self.to)?; verify_activity(self, &context.settings())?; - let community = self.cc[0].dereference(context, request_counter).await?; - verify_person_in_community(&self.actor, &self.cc[0], context, request_counter).await?; + let community = self.get_community(context, request_counter).await?; + verify_person_in_community(&self.actor, &community, context, request_counter).await?; check_community_deleted_or_removed(&community)?; match self.kind { @@ -119,7 +99,7 @@ impl ActivityHandler for CreateOrUpdatePost { CreateOrUpdateType::Update => { let is_mod_action = self.object.is_mod_action(context).await?; if is_mod_action { - verify_mod_action(&self.actor, &self.cc[0], context, request_counter).await?; + verify_mod_action(&self.actor, &community, context, request_counter).await?; } else { verify_domains_match(self.actor.inner(), self.object.id_unchecked())?; verify_urls_match(self.actor(), self.object.attributed_to.inner())?; @@ -147,3 +127,17 @@ impl ActivityHandler for CreateOrUpdatePost { Ok(()) } } + +#[async_trait::async_trait(?Send)] +impl GetCommunity for CreateOrUpdatePost { + async fn get_community( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result { + self + .object + .extract_community(context, request_counter) + .await + } +} diff --git a/crates/apub/src/activities/private_message/create_or_update.rs b/crates/apub/src/activities/private_message/create_or_update.rs index 9ef22f0e4..cfd7c8bcf 100644 --- a/crates/apub/src/activities/private_message/create_or_update.rs +++ b/crates/apub/src/activities/private_message/create_or_update.rs @@ -1,40 +1,21 @@ use crate::{ - activities::{generate_activity_id, verify_activity, verify_person, CreateOrUpdateType}, - context::lemmy_context, + activities::{generate_activity_id, send_lemmy_activity, verify_activity, verify_person}, fetcher::object_id::ObjectId, - objects::{ - person::ApubPerson, - private_message::{ApubPrivateMessage, Note}, + objects::{person::ApubPerson, private_message::ApubPrivateMessage}, + protocol::activities::{ + private_message::create_or_update::CreateOrUpdatePrivateMessage, + CreateOrUpdateType, }, - send_lemmy_activity, }; -use activitystreams::{base::AnyBase, primitives::OneOrMany, unparsed::Unparsed}; use lemmy_api_common::blocking; use lemmy_apub_lib::{ data::Data, - traits::{ActivityFields, ActivityHandler, ActorType, ApubObject}, + traits::{ActivityHandler, ActorType, ApubObject}, verify::verify_domains_match, }; use lemmy_db_schema::{source::person::Person, traits::Crud}; use lemmy_utils::LemmyError; use lemmy_websocket::{send::send_pm_ws_message, LemmyContext, UserOperationCrud}; -use serde::{Deserialize, Serialize}; -use url::Url; - -#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] -#[serde(rename_all = "camelCase")] -pub struct CreateOrUpdatePrivateMessage { - #[serde(rename = "@context")] - pub context: OneOrMany, - id: Url, - actor: ObjectId, - to: ObjectId, - object: Note, - #[serde(rename = "type")] - kind: CreateOrUpdateType, - #[serde(flatten)] - pub unparsed: Unparsed, -} impl CreateOrUpdatePrivateMessage { pub async fn send( @@ -54,10 +35,9 @@ impl CreateOrUpdatePrivateMessage { &context.settings().get_protocol_and_hostname(), )?; let create_or_update = CreateOrUpdatePrivateMessage { - context: lemmy_context(), id: id.clone(), actor: ObjectId::new(actor.actor_id()), - to: ObjectId::new(recipient.actor_id()), + to: [ObjectId::new(recipient.actor_id())], object: private_message.to_apub(context).await?, kind, unparsed: Default::default(), diff --git a/crates/apub/src/activities/private_message/delete.rs b/crates/apub/src/activities/private_message/delete.rs index bb374eb47..da3b6472d 100644 --- a/crates/apub/src/activities/private_message/delete.rs +++ b/crates/apub/src/activities/private_message/delete.rs @@ -1,20 +1,14 @@ use crate::{ - activities::{generate_activity_id, verify_activity, verify_person}, - context::lemmy_context, + activities::{generate_activity_id, send_lemmy_activity, verify_activity, verify_person}, fetcher::object_id::ObjectId, objects::{person::ApubPerson, private_message::ApubPrivateMessage}, - send_lemmy_activity, -}; -use activitystreams::{ - activity::kind::DeleteType, - base::AnyBase, - primitives::OneOrMany, - unparsed::Unparsed, + protocol::activities::private_message::delete::DeletePrivateMessage, }; +use activitystreams::activity::kind::DeleteType; use lemmy_api_common::blocking; use lemmy_apub_lib::{ data::Data, - traits::{ActivityFields, ActivityHandler, ActorType}, + traits::{ActivityHandler, ActorType}, verify::verify_domains_match, }; use lemmy_db_schema::{ @@ -23,23 +17,6 @@ use lemmy_db_schema::{ }; use lemmy_utils::LemmyError; use lemmy_websocket::{send::send_pm_ws_message, LemmyContext, UserOperationCrud}; -use serde::{Deserialize, Serialize}; -use url::Url; - -#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] -#[serde(rename_all = "camelCase")] -pub struct DeletePrivateMessage { - actor: ObjectId, - to: ObjectId, - pub(in crate::activities::private_message) object: ObjectId, - #[serde(rename = "type")] - kind: DeleteType, - id: Url, - #[serde(rename = "@context")] - context: OneOrMany, - #[serde(flatten)] - unparsed: Unparsed, -} impl DeletePrivateMessage { pub(in crate::activities::private_message) fn new( @@ -49,14 +26,13 @@ impl DeletePrivateMessage { ) -> Result { Ok(DeletePrivateMessage { actor: ObjectId::new(actor.actor_id()), - to: ObjectId::new(actor.actor_id()), + to: [ObjectId::new(actor.actor_id())], object: ObjectId::new(pm.ap_id.clone()), kind: DeleteType::Delete, id: generate_activity_id( DeleteType::Delete, &context.settings().get_protocol_and_hostname(), )?, - context: lemmy_context(), unparsed: Default::default(), }) } diff --git a/crates/apub/src/activities/private_message/undo_delete.rs b/crates/apub/src/activities/private_message/undo_delete.rs index 0263b8115..bba9e0f22 100644 --- a/crates/apub/src/activities/private_message/undo_delete.rs +++ b/crates/apub/src/activities/private_message/undo_delete.rs @@ -1,21 +1,13 @@ use crate::{ - activities::{ - generate_activity_id, - private_message::delete::DeletePrivateMessage, - verify_activity, - verify_person, - }, - context::lemmy_context, + activities::{generate_activity_id, send_lemmy_activity, verify_activity, verify_person}, fetcher::object_id::ObjectId, objects::{person::ApubPerson, private_message::ApubPrivateMessage}, - send_lemmy_activity, -}; -use activitystreams::{ - activity::kind::UndoType, - base::AnyBase, - primitives::OneOrMany, - unparsed::Unparsed, + protocol::activities::private_message::{ + delete::DeletePrivateMessage, + undo_delete::UndoDeletePrivateMessage, + }, }; +use activitystreams::activity::kind::UndoType; use lemmy_api_common::blocking; use lemmy_apub_lib::{ data::Data, @@ -28,23 +20,6 @@ use lemmy_db_schema::{ }; use lemmy_utils::LemmyError; use lemmy_websocket::{send::send_pm_ws_message, LemmyContext, UserOperationCrud}; -use serde::{Deserialize, Serialize}; -use url::Url; - -#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] -#[serde(rename_all = "camelCase")] -pub struct UndoDeletePrivateMessage { - actor: ObjectId, - to: ObjectId, - object: DeletePrivateMessage, - #[serde(rename = "type")] - kind: UndoType, - id: Url, - #[serde(rename = "@context")] - context: OneOrMany, - #[serde(flatten)] - unparsed: Unparsed, -} impl UndoDeletePrivateMessage { pub async fn send( @@ -65,11 +40,10 @@ impl UndoDeletePrivateMessage { )?; let undo = UndoDeletePrivateMessage { actor: ObjectId::new(actor.actor_id()), - to: ObjectId::new(recipient.actor_id()), + to: [ObjectId::new(recipient.actor_id())], object, kind: UndoType::Undo, id: id.clone(), - context: lemmy_context(), unparsed: Default::default(), }; let inbox = vec![recipient.shared_inbox_or_inbox_url()]; diff --git a/crates/apub/src/activities/voting/mod.rs b/crates/apub/src/activities/voting/mod.rs index 829553d35..0a2a8fd1f 100644 --- a/crates/apub/src/activities/voting/mod.rs +++ b/crates/apub/src/activities/voting/mod.rs @@ -1,7 +1,3 @@ -use crate::{ - activities::voting::vote::VoteType, - objects::{comment::ApubComment, person::ApubPerson, post::ApubPost}, -}; use lemmy_api_common::blocking; use lemmy_db_schema::{ source::{ @@ -17,6 +13,11 @@ use lemmy_websocket::{ UserOperation, }; +use crate::{ + objects::{comment::ApubComment, person::ApubPerson, post::ApubPost}, + protocol::activities::voting::vote::VoteType, +}; + pub mod undo_vote; pub mod vote; diff --git a/crates/apub/src/activities/voting/undo_vote.rs b/crates/apub/src/activities/voting/undo_vote.rs index d72b5245d..e95d25179 100644 --- a/crates/apub/src/activities/voting/undo_vote.rs +++ b/crates/apub/src/activities/voting/undo_vote.rs @@ -1,55 +1,35 @@ -use crate::{ - activities::{ - community::{announce::AnnouncableActivities, send_to_community}, - generate_activity_id, - verify_activity, - verify_person_in_community, - voting::{ - undo_vote_comment, - undo_vote_post, - vote::{Vote, VoteType}, - }, - }, - context::lemmy_context, - fetcher::object_id::ObjectId, - objects::{community::ApubCommunity, person::ApubPerson}, - PostOrComment, -}; -use activitystreams::{ - activity::kind::UndoType, - base::AnyBase, - primitives::OneOrMany, - unparsed::Unparsed, -}; +use std::ops::Deref; + +use activitystreams::{activity::kind::UndoType, public}; + use lemmy_api_common::blocking; use lemmy_apub_lib::{ data::Data, traits::{ActivityFields, ActivityHandler, ActorType}, - values::PublicUrl, verify::verify_urls_match, }; use lemmy_db_schema::{newtypes::CommunityId, source::community::Community, traits::Crud}; use lemmy_utils::LemmyError; use lemmy_websocket::LemmyContext; -use serde::{Deserialize, Serialize}; -use std::ops::Deref; -use url::Url; -#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] -#[serde(rename_all = "camelCase")] -pub struct UndoVote { - actor: ObjectId, - to: [PublicUrl; 1], - object: Vote, - cc: [ObjectId; 1], - #[serde(rename = "type")] - kind: UndoType, - id: Url, - #[serde(rename = "@context")] - context: OneOrMany, - #[serde(flatten)] - unparsed: Unparsed, -} +use crate::{ + activities::{ + community::{announce::GetCommunity, send_to_community}, + generate_activity_id, + verify_activity, + verify_is_public, + verify_person_in_community, + voting::{undo_vote_comment, undo_vote_post}, + }, + activity_lists::AnnouncableActivities, + fetcher::object_id::ObjectId, + objects::{community::ApubCommunity, person::ApubPerson}, + protocol::activities::voting::{ + undo_vote::UndoVote, + vote::{Vote, VoteType}, + }, + PostOrComment, +}; impl UndoVote { pub async fn send( @@ -72,12 +52,11 @@ impl UndoVote { )?; let undo_vote = UndoVote { actor: ObjectId::new(actor.actor_id()), - to: [PublicUrl::Public], + to: vec![public()], object, - cc: [ObjectId::new(community.actor_id())], + cc: vec![community.actor_id()], kind: UndoType::Undo, id: id.clone(), - context: lemmy_context(), unparsed: Default::default(), }; let activity = AnnouncableActivities::UndoVote(undo_vote); @@ -93,8 +72,10 @@ impl ActivityHandler for UndoVote { context: &Data, request_counter: &mut i32, ) -> Result<(), LemmyError> { + verify_is_public(&self.to)?; verify_activity(self, &context.settings())?; - verify_person_in_community(&self.actor, &self.cc[0], context, request_counter).await?; + let community = self.get_community(context, request_counter).await?; + verify_person_in_community(&self.actor, &community, context, request_counter).await?; verify_urls_match(self.actor(), self.object.actor())?; self.object.verify(context, request_counter).await?; Ok(()) @@ -117,3 +98,14 @@ impl ActivityHandler for UndoVote { } } } + +#[async_trait::async_trait(?Send)] +impl GetCommunity for UndoVote { + async fn get_community( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result { + self.object.get_community(context, request_counter).await + } +} diff --git a/crates/apub/src/activities/voting/vote.rs b/crates/apub/src/activities/voting/vote.rs index 5d4c9066e..01df4b93e 100644 --- a/crates/apub/src/activities/voting/vote.rs +++ b/crates/apub/src/activities/voting/vote.rs @@ -1,74 +1,35 @@ -use crate::{ - activities::{ - community::{announce::AnnouncableActivities, send_to_community}, - generate_activity_id, - verify_activity, - verify_person_in_community, - voting::{vote_comment, vote_post}, - }, - context::lemmy_context, - fetcher::object_id::ObjectId, - objects::{community::ApubCommunity, person::ApubPerson}, - PostOrComment, -}; -use activitystreams::{base::AnyBase, primitives::OneOrMany, unparsed::Unparsed}; -use anyhow::anyhow; +use std::ops::Deref; + +use activitystreams::public; + use lemmy_api_common::blocking; use lemmy_apub_lib::{ data::Data, - traits::{ActivityFields, ActivityHandler, ActorType}, - values::PublicUrl, + traits::{ActivityHandler, ActorType}, +}; +use lemmy_db_schema::{ + newtypes::CommunityId, + source::{community::Community, post::Post}, + traits::Crud, }; -use lemmy_db_schema::{newtypes::CommunityId, source::community::Community, traits::Crud}; use lemmy_utils::LemmyError; use lemmy_websocket::LemmyContext; -use serde::{Deserialize, Serialize}; -use std::{convert::TryFrom, ops::Deref}; -use strum_macros::ToString; -use url::Url; -#[derive(Clone, Debug, ToString, Deserialize, Serialize)] -pub enum VoteType { - Like, - Dislike, -} - -impl TryFrom for VoteType { - type Error = LemmyError; - - fn try_from(value: i16) -> Result { - match value { - 1 => Ok(VoteType::Like), - -1 => Ok(VoteType::Dislike), - _ => Err(anyhow!("invalid vote value").into()), - } - } -} - -impl From<&VoteType> for i16 { - fn from(value: &VoteType) -> i16 { - match value { - VoteType::Like => 1, - VoteType::Dislike => -1, - } - } -} - -#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] -#[serde(rename_all = "camelCase")] -pub struct Vote { - actor: ObjectId, - to: [PublicUrl; 1], - pub(in crate::activities::voting) object: ObjectId, - cc: [ObjectId; 1], - #[serde(rename = "type")] - pub(in crate::activities::voting) kind: VoteType, - id: Url, - #[serde(rename = "@context")] - context: OneOrMany, - #[serde(flatten)] - unparsed: Unparsed, -} +use crate::{ + activities::{ + community::{announce::GetCommunity, send_to_community}, + generate_activity_id, + verify_activity, + verify_is_public, + verify_person_in_community, + voting::{vote_comment, vote_post}, + }, + activity_lists::AnnouncableActivities, + fetcher::object_id::ObjectId, + objects::{community::ApubCommunity, person::ApubPerson}, + protocol::activities::voting::vote::{Vote, VoteType}, + PostOrComment, +}; impl Vote { pub(in crate::activities::voting) fn new( @@ -80,12 +41,11 @@ impl Vote { ) -> Result { Ok(Vote { actor: ObjectId::new(actor.actor_id()), - to: [PublicUrl::Public], + to: vec![public()], object: ObjectId::new(object.ap_id()), - cc: [ObjectId::new(community.actor_id())], + cc: vec![community.actor_id()], kind: kind.clone(), id: generate_activity_id(kind, &context.settings().get_protocol_and_hostname())?, - context: lemmy_context(), unparsed: Default::default(), }) } @@ -118,8 +78,10 @@ impl ActivityHandler for Vote { context: &Data, request_counter: &mut i32, ) -> Result<(), LemmyError> { + verify_is_public(&self.to)?; verify_activity(self, &context.settings())?; - verify_person_in_community(&self.actor, &self.cc[0], context, request_counter).await?; + let community = self.get_community(context, request_counter).await?; + verify_person_in_community(&self.actor, &community, context, request_counter).await?; Ok(()) } @@ -136,3 +98,24 @@ impl ActivityHandler for Vote { } } } + +#[async_trait::async_trait(?Send)] +impl GetCommunity for Vote { + async fn get_community( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result { + let object = self.object.dereference(context, request_counter).await?; + let cid = match object { + PostOrComment::Post(p) => p.community_id, + PostOrComment::Comment(c) => { + blocking(context.pool(), move |conn| Post::read(conn, c.post_id)) + .await?? + .community_id + } + }; + let community = blocking(context.pool(), move |conn| Community::read(conn, cid)).await??; + Ok(community.into()) + } +} diff --git a/crates/apub/src/activity_lists.rs b/crates/apub/src/activity_lists.rs new file mode 100644 index 000000000..1197af85f --- /dev/null +++ b/crates/apub/src/activity_lists.rs @@ -0,0 +1,107 @@ +use crate::{ + activities::community::announce::GetCommunity, + objects::community::ApubCommunity, + protocol::activities::{ + community::{ + add_mod::AddMod, + announce::AnnounceActivity, + block_user::BlockUserFromCommunity, + remove_mod::RemoveMod, + report::Report, + undo_block_user::UndoBlockUserFromCommunity, + update::UpdateCommunity, + }, + create_or_update::{comment::CreateOrUpdateComment, post::CreateOrUpdatePost}, + deletion::{delete::Delete, undo_delete::UndoDelete}, + following::{ + accept::AcceptFollowCommunity, + follow::FollowCommunity, + undo_follow::UndoFollowCommunity, + }, + private_message::{ + create_or_update::CreateOrUpdatePrivateMessage, + delete::DeletePrivateMessage, + undo_delete::UndoDeletePrivateMessage, + }, + voting::{undo_vote::UndoVote, vote::Vote}, + }, +}; +use lemmy_apub_lib::traits::{ActivityFields, ActivityHandler}; +use lemmy_utils::LemmyError; +use lemmy_websocket::LemmyContext; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Serialize, ActivityHandler, ActivityFields)] +#[serde(untagged)] +#[activity_handler(LemmyContext)] +pub enum SharedInboxActivities { + GroupInboxActivities(GroupInboxActivities), + // Note, pm activities need to be at the end, otherwise comments will end up here. We can probably + // avoid this problem by replacing createpm.object with our own struct, instead of NoteExt. + PersonInboxActivities(PersonInboxActivities), +} + +#[derive(Clone, Debug, Deserialize, Serialize, ActivityHandler, ActivityFields)] +#[serde(untagged)] +#[activity_handler(LemmyContext)] +pub enum GroupInboxActivities { + FollowCommunity(FollowCommunity), + UndoFollowCommunity(UndoFollowCommunity), + AnnouncableActivities(AnnouncableActivities), + Report(Report), +} + +#[derive(Clone, Debug, Deserialize, Serialize, ActivityHandler, ActivityFields)] +#[serde(untagged)] +#[activity_handler(LemmyContext)] +pub enum PersonInboxActivities { + AcceptFollowCommunity(AcceptFollowCommunity), + /// Some activities can also be sent from user to user, eg a comment with mentions + AnnouncableActivities(AnnouncableActivities), + CreateOrUpdatePrivateMessage(CreateOrUpdatePrivateMessage), + DeletePrivateMessage(DeletePrivateMessage), + UndoDeletePrivateMessage(UndoDeletePrivateMessage), + AnnounceActivity(Box), +} + +#[derive(Clone, Debug, Deserialize, Serialize, ActivityHandler, ActivityFields)] +#[serde(untagged)] +#[activity_handler(LemmyContext)] +pub enum AnnouncableActivities { + CreateOrUpdateComment(CreateOrUpdateComment), + CreateOrUpdatePost(Box), + Vote(Vote), + UndoVote(UndoVote), + Delete(Delete), + UndoDelete(UndoDelete), + UpdateCommunity(Box), + BlockUserFromCommunity(BlockUserFromCommunity), + UndoBlockUserFromCommunity(UndoBlockUserFromCommunity), + AddMod(AddMod), + RemoveMod(RemoveMod), +} + +#[async_trait::async_trait(?Send)] +impl GetCommunity for AnnouncableActivities { + async fn get_community( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result { + use AnnouncableActivities::*; + let community = match self { + CreateOrUpdateComment(a) => a.get_community(context, request_counter).await?, + CreateOrUpdatePost(a) => a.get_community(context, request_counter).await?, + Vote(a) => a.get_community(context, request_counter).await?, + UndoVote(a) => a.get_community(context, request_counter).await?, + Delete(a) => a.get_community(context, request_counter).await?, + UndoDelete(a) => a.get_community(context, request_counter).await?, + UpdateCommunity(a) => a.get_community(context, request_counter).await?, + BlockUserFromCommunity(a) => a.get_community(context, request_counter).await?, + UndoBlockUserFromCommunity(a) => a.get_community(context, request_counter).await?, + AddMod(a) => a.get_community(context, request_counter).await?, + RemoveMod(a) => a.get_community(context, request_counter).await?, + }; + Ok(community) + } +} diff --git a/crates/apub/src/collections/community_moderators.rs b/crates/apub/src/collections/community_moderators.rs index dc1d57985..d97affe2c 100644 --- a/crates/apub/src/collections/community_moderators.rs +++ b/crates/apub/src/collections/community_moderators.rs @@ -1,17 +1,11 @@ use crate::{ collections::CommunityContext, - context::lemmy_context, fetcher::object_id::ObjectId, generate_moderators_url, objects::person::ApubPerson, + protocol::collections::group_moderators::GroupModerators, }; -use activitystreams::{ - base::AnyBase, - chrono::NaiveDateTime, - collection::kind::OrderedCollectionType, - primitives::OneOrMany, - url::Url, -}; +use activitystreams::{chrono::NaiveDateTime, collection::kind::OrderedCollectionType}; use lemmy_api_common::blocking; use lemmy_apub_lib::{traits::ApubObject, verify::verify_domains_match}; use lemmy_db_schema::{ @@ -20,19 +14,7 @@ use lemmy_db_schema::{ }; use lemmy_db_views_actor::community_moderator_view::CommunityModeratorView; use lemmy_utils::LemmyError; -use serde::{Deserialize, Serialize}; -use serde_with::skip_serializing_none; - -#[skip_serializing_none] -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct GroupModerators { - #[serde(rename = "@context")] - context: OneOrMany, - r#type: OrderedCollectionType, - id: Url, - ordered_items: Vec>, -} +use url::Url; #[derive(Clone, Debug)] pub(crate) struct ApubCommunityModerators(pub(crate) Vec); @@ -75,7 +57,6 @@ impl ApubObject for ApubCommunityModerators { .map(|m| ObjectId::::new(m.moderator.actor_id.clone().into_inner())) .collect(); Ok(GroupModerators { - context: lemmy_context(), r#type: OrderedCollectionType::OrderedCollection, id: generate_moderators_url(&data.0.actor_id)?.into(), ordered_items, @@ -139,3 +120,74 @@ impl ApubObject for ApubCommunityModerators { Ok(ApubCommunityModerators { 0: vec![] }) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::objects::{ + community::tests::parse_lemmy_community, + person::tests::parse_lemmy_person, + tests::{file_to_json_object, init_context}, + }; + use lemmy_db_schema::{ + source::{ + community::Community, + person::{Person, PersonForm}, + }, + traits::Crud, + }; + use serial_test::serial; + + #[actix_rt::test] + #[serial] + async fn test_parse_lemmy_community_moderators() { + let context = init_context(); + let community = parse_lemmy_community(&context).await; + let community_id = community.id; + + let old_mod = PersonForm { + name: "holly".into(), + ..PersonForm::default() + }; + let old_mod = Person::create(&context.pool().get().unwrap(), &old_mod).unwrap(); + let community_moderator_form = CommunityModeratorForm { + community_id: community.id, + person_id: old_mod.id, + }; + + CommunityModerator::join(&context.pool().get().unwrap(), &community_moderator_form).unwrap(); + + let new_mod = parse_lemmy_person(&context).await; + + let json: GroupModerators = + file_to_json_object("assets/lemmy/collections/group_moderators.json"); + let url = Url::parse("https://enterprise.lemmy.ml/c/tenforward").unwrap(); + let mut request_counter = 0; + let community_context = CommunityContext { + 0: community, + 1: context, + }; + ApubCommunityModerators::from_apub(&json, &community_context, &url, &mut request_counter) + .await + .unwrap(); + assert_eq!(request_counter, 0); + + let current_moderators = blocking(community_context.1.pool(), move |conn| { + CommunityModeratorView::for_community(conn, community_id) + }) + .await + .unwrap() + .unwrap(); + + assert_eq!(current_moderators.len(), 1); + assert_eq!(current_moderators[0].moderator.id, new_mod.id); + + Person::delete(&*community_context.1.pool().get().unwrap(), old_mod.id).unwrap(); + Person::delete(&*community_context.1.pool().get().unwrap(), new_mod.id).unwrap(); + Community::delete( + &*community_context.1.pool().get().unwrap(), + community_context.0.id, + ) + .unwrap(); + } +} diff --git a/crates/apub/src/collections/community_outbox.rs b/crates/apub/src/collections/community_outbox.rs index 24465c95c..451c3fa94 100644 --- a/crates/apub/src/collections/community_outbox.rs +++ b/crates/apub/src/collections/community_outbox.rs @@ -1,17 +1,7 @@ -use crate::{ - activities::{post::create_or_update::CreateOrUpdatePost, CreateOrUpdateType}, - collections::CommunityContext, - context::lemmy_context, - generate_outbox_url, - objects::{person::ApubPerson, post::ApubPost}, -}; -use activitystreams::{ - base::AnyBase, - chrono::NaiveDateTime, - collection::kind::OrderedCollectionType, - primitives::OneOrMany, - url::Url, -}; +use activitystreams::collection::kind::OrderedCollectionType; +use chrono::NaiveDateTime; +use url::Url; + use lemmy_api_common::blocking; use lemmy_apub_lib::{ data::Data, @@ -23,19 +13,16 @@ use lemmy_db_schema::{ traits::Crud, }; use lemmy_utils::LemmyError; -use serde::{Deserialize, Serialize}; -use serde_with::skip_serializing_none; -#[skip_serializing_none] -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct GroupOutbox { - #[serde(rename = "@context")] - context: OneOrMany, - r#type: OrderedCollectionType, - id: Url, - ordered_items: Vec, -} +use crate::{ + collections::CommunityContext, + generate_outbox_url, + objects::{person::ApubPerson, post::ApubPost}, + protocol::{ + activities::{create_or_update::post::CreateOrUpdatePost, CreateOrUpdateType}, + collections::group_outbox::GroupOutbox, + }, +}; #[derive(Clone, Debug)] pub(crate) struct ApubCommunityOutbox(Vec); @@ -88,9 +75,9 @@ impl ApubObject for ApubCommunityOutbox { } Ok(GroupOutbox { - context: lemmy_context(), r#type: OrderedCollectionType::OrderedCollection, id: generate_outbox_url(&data.0.actor_id)?.into(), + total_items: ordered_items.len() as i32, ordered_items, }) } diff --git a/crates/apub/src/collections/mod.rs b/crates/apub/src/collections/mod.rs index 948824e22..a2e77d1bc 100644 --- a/crates/apub/src/collections/mod.rs +++ b/crates/apub/src/collections/mod.rs @@ -1,5 +1,7 @@ -use crate::objects::community::ApubCommunity; use lemmy_websocket::LemmyContext; + +use crate::objects::community::ApubCommunity; + pub(crate) mod community_moderators; pub(crate) mod community_outbox; diff --git a/crates/apub/src/context.rs b/crates/apub/src/context.rs index bc58052f0..cf4b704fc 100644 --- a/crates/apub/src/context.rs +++ b/crates/apub/src/context.rs @@ -1,28 +1,51 @@ use activitystreams::{base::AnyBase, context, primitives::OneOrMany}; +use serde::{Deserialize, Serialize}; use serde_json::json; use url::Url; -pub(crate) fn lemmy_context() -> OneOrMany { - let context_ext = AnyBase::from_arbitrary_json(json!( - { - "sc": "http://schema.org#", - "sensitive": "as:sensitive", - "stickied": "as:stickied", - "pt": "https://join-lemmy.org#", - "comments_enabled": { - "type": "sc:Boolean", - "id": "pt:commentsEnabled" - }, - "moderators": "as:moderators", - "matrixUserId": { - "type": "sc:Text", - "id": "as:alsoKnownAs" - }, - })) - .expect("parse context"); - OneOrMany::from(vec![ - AnyBase::from(context()), - context_ext, - AnyBase::from(Url::parse("https://w3id.org/security/v1").expect("parse context")), - ]) +lazy_static! { + static ref CONTEXT: OneOrMany = { + let context_ext = AnyBase::from_arbitrary_json(json!( + { + "sc": "http://schema.org#", + "sensitive": "as:sensitive", + "stickied": "as:stickied", + "pt": "https://join-lemmy.org#", + "comments_enabled": { + "type": "sc:Boolean", + "id": "pt:commentsEnabled" + }, + "moderators": "as:moderators", + "matrixUserId": { + "type": "sc:Text", + "id": "as:alsoKnownAs" + }, + })) + .expect("parse context"); + OneOrMany::from(vec![ + AnyBase::from(context()), + context_ext, + AnyBase::from(Url::parse("https://w3id.org/security/v1").expect("parse context")), + ]) + }; +} + +#[derive(Serialize, Deserialize)] +pub(crate) struct WithContext { + #[serde(rename = "@context")] + context: OneOrMany, + #[serde(flatten)] + inner: T, +} + +impl WithContext { + pub(crate) fn new(inner: T) -> WithContext { + WithContext { + context: CONTEXT.clone(), + inner, + } + } + pub(crate) fn inner(self) -> T { + self.inner + } } diff --git a/crates/apub/src/fetcher/object_id.rs b/crates/apub/src/fetcher/object_id.rs index 66466362e..5c5c518f4 100644 --- a/crates/apub/src/fetcher/object_id.rs +++ b/crates/apub/src/fetcher/object_id.rs @@ -22,7 +22,6 @@ use url::Url; /// fetch through the search). This should be configurable. static REQUEST_LIMIT: i32 = 25; -// TODO: after moving this file to library, remove lazy_static dependency from apub crate lazy_static! { static ref CLIENT: Client = Client::builder() .user_agent(build_user_agent(&Settings::get())) diff --git a/crates/apub/src/fetcher/post_or_comment.rs b/crates/apub/src/fetcher/post_or_comment.rs index ff0562a6b..c0bc46a81 100644 --- a/crates/apub/src/fetcher/post_or_comment.rs +++ b/crates/apub/src/fetcher/post_or_comment.rs @@ -1,14 +1,16 @@ -use crate::objects::{ - comment::{ApubComment, Note}, - post::{ApubPost, Page}, -}; -use activitystreams::chrono::NaiveDateTime; +use chrono::NaiveDateTime; +use serde::Deserialize; +use url::Url; + use lemmy_apub_lib::traits::ApubObject; use lemmy_db_schema::source::{comment::CommentForm, post::PostForm}; use lemmy_utils::LemmyError; use lemmy_websocket::LemmyContext; -use serde::Deserialize; -use url::Url; + +use crate::{ + objects::{comment::ApubComment, post::ApubPost}, + protocol::objects::{note::Note, page::Page}, +}; #[derive(Clone, Debug)] pub enum PostOrComment { diff --git a/crates/apub/src/fetcher/search.rs b/crates/apub/src/fetcher/search.rs index 86cdcbe07..51699eb04 100644 --- a/crates/apub/src/fetcher/search.rs +++ b/crates/apub/src/fetcher/search.rs @@ -1,15 +1,9 @@ -use crate::{ - fetcher::object_id::ObjectId, - objects::{ - comment::{ApubComment, Note}, - community::{ApubCommunity, Group}, - person::{ApubPerson, Person}, - post::{ApubPost, Page}, - }, -}; -use activitystreams::chrono::NaiveDateTime; use anyhow::anyhow; +use chrono::NaiveDateTime; use itertools::Itertools; +use serde::Deserialize; +use url::Url; + use lemmy_api_common::blocking; use lemmy_apub_lib::{ traits::ApubObject, @@ -21,8 +15,12 @@ use lemmy_db_schema::{ }; use lemmy_utils::LemmyError; use lemmy_websocket::LemmyContext; -use serde::Deserialize; -use url::Url; + +use crate::{ + fetcher::object_id::ObjectId, + objects::{comment::ApubComment, community::ApubCommunity, person::ApubPerson, post::ApubPost}, + protocol::objects::{group::Group, note::Note, page::Page, person::Person}, +}; /// Attempt to parse the query as URL, and fetch an ActivityPub object from it. /// diff --git a/crates/apub/src/http/community.rs b/crates/apub/src/http/community.rs index dcaf551f1..4110d2b20 100644 --- a/crates/apub/src/http/community.rs +++ b/crates/apub/src/http/community.rs @@ -1,16 +1,12 @@ use crate::{ - activities::{ - community::announce::{AnnouncableActivities, AnnounceActivity}, - extract_community, - following::{follow::FollowCommunity, undo::UndoFollowCommunity}, - report::Report, - }, + activities::{community::announce::GetCommunity, verify_person_in_community}, + activity_lists::GroupInboxActivities, collections::{ community_moderators::ApubCommunityModerators, community_outbox::ApubCommunityOutbox, CommunityContext, }, - context::lemmy_context, + context::WithContext, fetcher::object_id::ObjectId, generate_outbox_url, http::{ @@ -20,20 +16,19 @@ use crate::{ receive_activity, }, objects::community::ApubCommunity, -}; -use activitystreams::{ - base::BaseExt, - collection::{CollectionExt, UnorderedCollection}, + protocol::{ + activities::community::announce::AnnounceActivity, + collections::group_followers::GroupFollowers, + }, }; use actix_web::{body::Body, web, web::Payload, HttpRequest, HttpResponse}; use lemmy_api_common::blocking; -use lemmy_apub_lib::traits::{ActivityFields, ActivityHandler, ApubObject}; +use lemmy_apub_lib::traits::{ActivityFields, ApubObject}; use lemmy_db_schema::source::community::Community; -use lemmy_db_views_actor::community_follower_view::CommunityFollowerView; use lemmy_utils::LemmyError; use lemmy_websocket::LemmyContext; -use log::trace; -use serde::{Deserialize, Serialize}; +use log::info; +use serde::Deserialize; #[derive(Deserialize)] pub(crate) struct CommunityQuery { @@ -60,16 +55,6 @@ pub(crate) async fn get_apub_community_http( } } -#[derive(Clone, Debug, Deserialize, Serialize, ActivityHandler, ActivityFields)] -#[serde(untagged)] -#[activity_handler(LemmyContext)] -pub enum GroupInboxActivities { - FollowCommunity(FollowCommunity), - UndoFollowCommunity(UndoFollowCommunity), - AnnouncableActivities(AnnouncableActivities), - Report(Report), -} - /// Handler for all incoming receive to community inboxes. pub async fn community_inbox( request: HttpRequest, @@ -78,10 +63,10 @@ pub async fn community_inbox( context: web::Data, ) -> Result { let unparsed = payload_to_string(payload).await?; - trace!("Received community inbox activity {}", unparsed); - let activity = serde_json::from_str::(&unparsed)?; + info!("Received community inbox activity {}", unparsed); + let activity = serde_json::from_str::>(&unparsed)?; - receive_group_inbox(activity.clone(), request, &context).await?; + receive_group_inbox(activity.inner(), request, &context).await?; Ok(HttpResponse::Ok().finish()) } @@ -92,12 +77,16 @@ pub(in crate::http) async fn receive_group_inbox( context: &LemmyContext, ) -> Result { let res = receive_activity(request, activity.clone(), context).await; - if let GroupInboxActivities::AnnouncableActivities(announcable) = activity.clone() { - let community = extract_community(&announcable.cc(), context, &mut 0).await?; + + if let GroupInboxActivities::AnnouncableActivities(announcable) = activity { + let community = announcable.get_community(context, &mut 0).await?; + let actor_id = ObjectId::new(announcable.actor().clone()); + verify_person_in_community(&actor_id, &community, context, &mut 0).await?; if community.local { AnnounceActivity::send(announcable, &community, vec![], context).await?; } } + res } @@ -110,19 +99,8 @@ pub(crate) async fn get_apub_community_followers( Community::read_from_name(conn, &info.community_name) }) .await??; - - let community_id = community.id; - let community_followers = blocking(context.pool(), move |conn| { - CommunityFollowerView::for_community(conn, community_id) - }) - .await??; - - let mut collection = UnorderedCollection::new(); - collection - .set_many_contexts(lemmy_context()) - .set_id(community.followers_url.into()) - .set_total_items(community_followers.len() as u64); - Ok(create_apub_response(&collection)) + let followers = GroupFollowers::new(community, &context).await?; + Ok(create_apub_response(&followers)) } /// Returns the community outbox, which is populated by a maximum of 20 posts (but no other diff --git a/crates/apub/src/http/mod.rs b/crates/apub/src/http/mod.rs index b82a6d890..9c61c2747 100644 --- a/crates/apub/src/http/mod.rs +++ b/crates/apub/src/http/mod.rs @@ -1,10 +1,9 @@ use crate::{ + activity_lists::SharedInboxActivities, check_is_apub_id_valid, + context::WithContext, fetcher::get_or_fetch_and_upsert_actor, - http::{ - community::{receive_group_inbox, GroupInboxActivities}, - person::{receive_person_inbox, PersonInboxActivities}, - }, + http::{community::receive_group_inbox, person::receive_person_inbox}, insert_activity, }; use actix_web::{ @@ -27,7 +26,7 @@ use lemmy_apub_lib::{ use lemmy_db_schema::{source::activity::Activity, DbPool}; use lemmy_utils::{location_info, LemmyError}; use lemmy_websocket::LemmyContext; -use log::{info, trace}; +use log::info; use serde::{Deserialize, Serialize}; use std::{fmt::Debug, io::Read}; use url::Url; @@ -38,25 +37,15 @@ mod person; mod post; pub mod routes; -#[derive(Clone, Debug, Deserialize, Serialize, ActivityHandler, ActivityFields)] -#[serde(untagged)] -#[activity_handler(LemmyContext)] -pub enum SharedInboxActivities { - GroupInboxActivities(GroupInboxActivities), - // Note, pm activities need to be at the end, otherwise comments will end up here. We can probably - // avoid this problem by replacing createpm.object with our own struct, instead of NoteExt. - PersonInboxActivities(PersonInboxActivities), -} - pub async fn shared_inbox( request: HttpRequest, payload: Payload, context: web::Data, ) -> Result { let unparsed = payload_to_string(payload).await?; - trace!("Received shared inbox activity {}", unparsed); - let activity = serde_json::from_str::(&unparsed)?; - match activity { + info!("Received shared inbox activity {}", unparsed); + let activity = serde_json::from_str::>(&unparsed)?; + match activity.inner() { SharedInboxActivities::GroupInboxActivities(g) => { receive_group_inbox(g, request, &context).await } @@ -134,7 +123,7 @@ where { HttpResponse::Ok() .content_type(APUB_JSON_CONTENT_TYPE) - .json(data) + .json(WithContext::new(data)) } fn create_apub_tombstone_response(data: &T) -> HttpResponse @@ -144,7 +133,7 @@ where HttpResponse::Gone() .content_type(APUB_JSON_CONTENT_TYPE) .status(StatusCode::GONE) - .json(data) + .json(WithContext::new(data)) } #[derive(Deserialize)] diff --git a/crates/apub/src/http/person.rs b/crates/apub/src/http/person.rs index a7e94020e..a5ea4ad17 100644 --- a/crates/apub/src/http/person.rs +++ b/crates/apub/src/http/person.rs @@ -1,15 +1,6 @@ use crate::{ - activities::{ - community::announce::{AnnouncableActivities, AnnounceActivity}, - following::accept::AcceptFollowCommunity, - private_message::{ - create_or_update::CreateOrUpdatePrivateMessage, - delete::DeletePrivateMessage, - undo_delete::UndoDeletePrivateMessage, - }, - }, - context::lemmy_context, - generate_outbox_url, + activity_lists::PersonInboxActivities, + context::WithContext, http::{ create_apub_response, create_apub_tombstone_response, @@ -17,20 +8,16 @@ use crate::{ receive_activity, }, objects::person::ApubPerson, -}; -use activitystreams::{ - base::BaseExt, - collection::{CollectionExt, OrderedCollection}, + protocol::collections::person_outbox::PersonOutbox, }; use actix_web::{body::Body, web, web::Payload, HttpRequest, HttpResponse}; use lemmy_api_common::blocking; -use lemmy_apub_lib::traits::{ActivityFields, ActivityHandler, ApubObject}; +use lemmy_apub_lib::traits::ApubObject; use lemmy_db_schema::source::person::Person; use lemmy_utils::LemmyError; use lemmy_websocket::LemmyContext; -use log::trace; -use serde::{Deserialize, Serialize}; -use url::Url; +use log::info; +use serde::Deserialize; #[derive(Deserialize)] pub struct PersonQuery { @@ -59,19 +46,6 @@ pub(crate) async fn get_apub_person_http( } } -#[derive(Clone, Debug, Deserialize, Serialize, ActivityHandler, ActivityFields)] -#[serde(untagged)] -#[activity_handler(LemmyContext)] -pub enum PersonInboxActivities { - AcceptFollowCommunity(AcceptFollowCommunity), - /// Some activities can also be sent from user to user, eg a comment with mentions - AnnouncableActivities(AnnouncableActivities), - CreateOrUpdatePrivateMessage(CreateOrUpdatePrivateMessage), - DeletePrivateMessage(DeletePrivateMessage), - UndoDeletePrivateMessage(UndoDeletePrivateMessage), - AnnounceActivity(Box), -} - pub async fn person_inbox( request: HttpRequest, payload: Payload, @@ -79,9 +53,9 @@ pub async fn person_inbox( context: web::Data, ) -> Result { let unparsed = payload_to_string(payload).await?; - trace!("Received person inbox activity {}", unparsed); - let activity = serde_json::from_str::(&unparsed)?; - receive_person_inbox(activity, request, &context).await + info!("Received person inbox activity {}", unparsed); + let activity = serde_json::from_str::>(&unparsed)?; + receive_person_inbox(activity.inner(), request, &context).await } pub(in crate::http) async fn receive_person_inbox( @@ -100,12 +74,6 @@ pub(crate) async fn get_apub_person_outbox( Person::find_by_name(conn, &info.user_name) }) .await??; - // TODO: populate the person outbox - let mut collection = OrderedCollection::new(); - collection - .set_many_items(Vec::::new()) - .set_many_contexts(lemmy_context()) - .set_id(generate_outbox_url(&person.actor_id)?.into()) - .set_total_items(0_u64); - Ok(create_apub_response(&collection)) + let outbox = PersonOutbox::new(person).await?; + Ok(create_apub_response(&outbox)) } diff --git a/crates/apub/src/lib.rs b/crates/apub/src/lib.rs index 675798b6f..75d7a62fa 100644 --- a/crates/apub/src/lib.rs +++ b/crates/apub/src/lib.rs @@ -1,10 +1,12 @@ pub mod activities; +pub(crate) mod activity_lists; pub(crate) mod collections; mod context; pub mod fetcher; pub mod http; pub mod migrations; pub mod objects; +pub mod protocol; #[macro_use] extern crate lazy_static; @@ -12,16 +14,10 @@ extern crate lazy_static; use crate::fetcher::post_or_comment::PostOrComment; use anyhow::{anyhow, Context}; use lemmy_api_common::blocking; -use lemmy_apub_lib::{activity_queue::send_activity, traits::ActorType}; -use lemmy_db_schema::{ - newtypes::{CommunityId, DbUrl}, - source::{activity::Activity, person::Person}, - DbPool, -}; -use lemmy_db_views_actor::community_person_ban_view::CommunityPersonBanView; +use lemmy_apub_lib::webfinger::{webfinger_resolve_actor, WebfingerType}; +use lemmy_db_schema::{newtypes::DbUrl, source::activity::Activity, DbPool}; use lemmy_utils::{location_info, settings::structs::Settings, LemmyError}; use lemmy_websocket::LemmyContext; -use log::info; use serde::Serialize; use std::net::IpAddr; use url::{ParseError, Url}; @@ -34,6 +30,8 @@ use url::{ParseError, Url}; /// - URL being in the allowlist (if it is active) /// - URL not being in the blocklist (if it is active) /// +/// `use_strict_allowlist` should be true only when parsing a remote community, or when parsing a +/// post/comment in a local community. pub(crate) fn check_is_apub_id_valid( apub_id: &Url, use_strict_allowlist: bool, @@ -92,16 +90,6 @@ pub(crate) fn check_is_apub_id_valid( Ok(()) } -#[async_trait::async_trait(?Send)] -pub trait CommunityType { - fn followers_url(&self) -> Url; - async fn get_follower_inboxes( - &self, - pool: &DbPool, - settings: &Settings, - ) -> Result, LemmyError>; -} - pub enum EndpointType { Community, Person, @@ -111,7 +99,7 @@ pub enum EndpointType { } /// Generates an apub endpoint for a given domain, IE xyz.tld -fn generate_apub_endpoint_for_domain( +pub fn generate_local_apub_endpoint( endpoint_type: EndpointType, name: &str, domain: &str, @@ -127,15 +115,6 @@ fn generate_apub_endpoint_for_domain( Ok(Url::parse(&format!("{}/{}/{}", domain, point, name))?.into()) } -/// Generates the ActivityPub ID for a given object type and ID. -pub fn generate_apub_endpoint( - endpoint_type: EndpointType, - name: &str, - protocol_and_hostname: &str, -) -> Result { - generate_apub_endpoint_for_domain(endpoint_type, name, protocol_and_hostname) -} - pub fn generate_followers_url(actor_id: &DbUrl) -> Result { Ok(Url::parse(&format!("{}/followers", actor_id))?.into()) } @@ -169,23 +148,31 @@ fn generate_moderators_url(community_id: &DbUrl) -> Result { /// Takes in a shortname of the type dessalines@xyz.tld or dessalines (assumed to be local), and outputs the actor id. /// Used in the API for communities and users. -pub fn build_actor_id_from_shortname( - endpoint_type: EndpointType, +pub async fn get_actor_id_from_name( + webfinger_type: WebfingerType, short_name: &str, - settings: &Settings, -) -> Result { + context: &LemmyContext, +) -> Result { let split = short_name.split('@').collect::>(); let name = split[0]; // If there's no @, its local - let domain = if split.len() == 1 { - settings.get_protocol_and_hostname() + if split.len() == 1 { + let domain = context.settings().get_protocol_and_hostname(); + let endpoint_type = match webfinger_type { + WebfingerType::Person => EndpointType::Person, + WebfingerType::Group => EndpointType::Community, + }; + Ok(generate_local_apub_endpoint(endpoint_type, name, &domain)?) } else { - format!("{}://{}", settings.get_protocol_string(), split[1]) - }; - - generate_apub_endpoint_for_domain(endpoint_type, name, &domain) + let protocol = context.settings().get_protocol_string(); + Ok( + webfinger_resolve_actor(name, split[1], webfinger_type, context.client(), protocol) + .await? + .into(), + ) + } } /// Store a sent or received activity in the database, for logging purposes. These records are not @@ -207,64 +194,3 @@ where .await??; Ok(()) } - -async fn check_community_or_site_ban( - person: &Person, - community_id: CommunityId, - pool: &DbPool, -) -> Result<(), LemmyError> { - if person.banned { - return Err(anyhow!("Person is banned from site").into()); - } - let person_id = person.id; - let is_banned = - move |conn: &'_ _| CommunityPersonBanView::get(conn, person_id, community_id).is_ok(); - if blocking(pool, is_banned).await? { - return Err(anyhow!("Person is banned from community").into()); - } - - Ok(()) -} - -pub(crate) async fn send_lemmy_activity( - context: &LemmyContext, - activity: &T, - activity_id: &Url, - actor: &dyn ActorType, - inboxes: Vec, - sensitive: bool, -) -> Result<(), LemmyError> { - if !context.settings().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 = context.settings().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?; - - send_activity( - serialised_activity, - actor, - inboxes, - context.client(), - context.activity_queue(), - ) - .await -} diff --git a/crates/apub/src/objects/comment.rs b/crates/apub/src/objects/comment.rs index b79f53e06..e5ffb7ca2 100644 --- a/crates/apub/src/objects/comment.rs +++ b/crates/apub/src/objects/comment.rs @@ -1,29 +1,17 @@ -use crate::{ - activities::{verify_is_public, verify_person_in_community}, - context::lemmy_context, - fetcher::object_id::ObjectId, - objects::{person::ApubPerson, post::ApubPost, tombstone::Tombstone, Source}, - PostOrComment, -}; -use activitystreams::{ - base::AnyBase, - chrono::NaiveDateTime, - object::kind::NoteType, - primitives::OneOrMany, - public, - unparsed::Unparsed, -}; +use std::ops::Deref; + +use activitystreams::{object::kind::NoteType, public}; use anyhow::anyhow; -use chrono::{DateTime, FixedOffset}; +use chrono::NaiveDateTime; use html2md::parse_html; +use url::Url; + use lemmy_api_common::blocking; use lemmy_apub_lib::{ traits::ApubObject, values::{MediaTypeHtml, MediaTypeMarkdown}, - verify::verify_domains_match, }; use lemmy_db_schema::{ - newtypes::CommentId, source::{ comment::{Comment, CommentForm}, community::Community, @@ -37,107 +25,21 @@ use lemmy_utils::{ LemmyError, }; use lemmy_websocket::LemmyContext; -use serde::{Deserialize, Serialize}; -use serde_with::skip_serializing_none; -use std::ops::Deref; -use url::Url; -#[skip_serializing_none] -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Note { - #[serde(rename = "@context")] - context: OneOrMany, - r#type: NoteType, - id: Url, - pub(crate) attributed_to: ObjectId, - /// Indicates that the object is publicly readable. Unlike [`Post.to`], this one doesn't contain - /// the community ID, as it would be incompatible with Pleroma (and we can get the community from - /// the post in [`in_reply_to`]). - to: Vec, - content: String, - media_type: Option, - source: SourceCompat, - in_reply_to: ObjectId, - published: Option>, - updated: Option>, - #[serde(flatten)] - unparsed: Unparsed, -} - -/// Pleroma puts a raw string in the source, so we have to handle it here for deserialization to work -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -#[serde(untagged)] -enum SourceCompat { - Lemmy(Source), - Pleroma(String), -} - -impl Note { - pub(crate) fn id_unchecked(&self) -> &Url { - &self.id - } - pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> { - verify_domains_match(&self.id, expected_domain)?; - Ok(&self.id) - } - - pub(crate) async fn get_parents( - &self, - context: &LemmyContext, - request_counter: &mut i32, - ) -> Result<(ApubPost, Option), LemmyError> { - // Fetch parent comment chain in a box, otherwise it can cause a stack overflow. - let parent = Box::pin( - self - .in_reply_to - .dereference(context, request_counter) - .await?, - ); - match parent.deref() { - PostOrComment::Post(p) => { - // Workaround because I cant figure out how to get the post out of the box (and we dont - // want to stackoverflow in a deep comment hierarchy). - let post_id = p.id; - let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; - Ok((post.into(), None)) - } - PostOrComment::Comment(c) => { - let post_id = c.post_id; - let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; - Ok((post.into(), Some(c.id))) - } - } - } - - pub(crate) async fn verify( - &self, - context: &LemmyContext, - request_counter: &mut i32, - ) -> Result<(), LemmyError> { - let (post, _parent_comment_id) = self.get_parents(context, request_counter).await?; - let community_id = post.community_id; - let community = blocking(context.pool(), move |conn| { - Community::read(conn, community_id) - }) - .await??; - - if post.locked { - return Err(anyhow!("Post is locked").into()); - } - verify_domains_match(self.attributed_to.inner(), &self.id)?; - verify_person_in_community( - &self.attributed_to, - &ObjectId::new(community.actor_id), - context, - request_counter, - ) - .await?; - verify_is_public(&self.to)?; - Ok(()) - } -} +use crate::{ + activities::verify_person_in_community, + check_is_apub_id_valid, + fetcher::object_id::ObjectId, + protocol::{ + objects::{ + note::{Note, SourceCompat}, + tombstone::Tombstone, + }, + Source, + }, + PostOrComment, +}; +use lemmy_utils::utils::markdown_to_html; #[derive(Clone, Debug)] pub struct ApubComment(Comment); @@ -202,12 +104,11 @@ impl ApubObject for ApubComment { }; let note = Note { - context: lemmy_context(), r#type: NoteType::Note, id: self.ap_id.to_owned().into_inner(), attributed_to: ObjectId::new(creator.actor_id), to: vec![public()], - content: self.content.clone(), + content: markdown_to_html(&self.content), media_type: Some(MediaTypeHtml::Html), source: SourceCompat::Lemmy(Source { content: self.content.clone(), @@ -244,6 +145,19 @@ impl ApubObject for ApubComment { .dereference(context, request_counter) .await?; let (post, parent_comment_id) = note.get_parents(context, request_counter).await?; + let community_id = post.community_id; + let community = blocking(context.pool(), move |conn| { + Community::read(conn, community_id) + }) + .await??; + check_is_apub_id_valid(¬e.id, community.local, &context.settings())?; + verify_person_in_community( + ¬e.attributed_to, + &community.into(), + context, + request_counter, + ) + .await?; if post.locked { return Err(anyhow!("Post is locked").into()); } @@ -274,10 +188,12 @@ impl ApubObject for ApubComment { } #[cfg(test)] -mod tests { +pub(crate) mod tests { use super::*; use crate::objects::{ - community::ApubCommunity, + community::{tests::parse_lemmy_community, ApubCommunity}, + person::{tests::parse_lemmy_person, ApubPerson}, + post::ApubPost, tests::{file_to_json_object, init_context}, }; use assert_json_diff::assert_json_include; @@ -287,15 +203,9 @@ mod tests { url: &Url, context: &LemmyContext, ) -> (ApubPerson, ApubCommunity, ApubPost) { - let person_json = file_to_json_object("assets/lemmy-person.json"); - let person = ApubPerson::from_apub(&person_json, context, url, &mut 0) - .await - .unwrap(); - let community_json = file_to_json_object("assets/lemmy-community.json"); - let community = ApubCommunity::from_apub(&community_json, context, url, &mut 0) - .await - .unwrap(); - let post_json = file_to_json_object("assets/lemmy-post.json"); + let person = parse_lemmy_person(context).await; + let community = parse_lemmy_community(context).await; + let post_json = file_to_json_object("assets/lemmy/objects/page.json"); let post = ApubPost::from_apub(&post_json, context, url, &mut 0) .await .unwrap(); @@ -310,14 +220,12 @@ mod tests { #[actix_rt::test] #[serial] - async fn test_parse_lemmy_comment() { - // TODO: changed ObjectId::dereference() so that it always fetches if - // last_refreshed_at() == None. But post doesnt store that and expects to never be refetched + pub(crate) async fn test_parse_lemmy_comment() { let context = init_context(); let url = Url::parse("https://enterprise.lemmy.ml/comment/38741").unwrap(); let data = prepare_comment_test(&url, &context).await; - let json = file_to_json_object("assets/lemmy-comment.json"); + let json = file_to_json_object("assets/lemmy/objects/note.json"); let mut request_counter = 0; let comment = ApubComment::from_apub(&json, &context, &url, &mut request_counter) .await @@ -345,11 +253,11 @@ mod tests { let pleroma_url = Url::parse("https://queer.hacktivis.me/objects/8d4973f4-53de-49cd-8c27-df160e16a9c2") .unwrap(); - let person_json = file_to_json_object("assets/pleroma-person.json"); + let person_json = file_to_json_object("assets/pleroma/objects/person.json"); ApubPerson::from_apub(&person_json, &context, &pleroma_url, &mut 0) .await .unwrap(); - let json = file_to_json_object("assets/pleroma-comment.json"); + let json = file_to_json_object("assets/pleroma/objects/note.json"); let mut request_counter = 0; let comment = ApubComment::from_apub(&json, &context, &pleroma_url, &mut request_counter) .await diff --git a/crates/apub/src/objects/community.rs b/crates/apub/src/objects/community.rs index e71addba5..0947ebf37 100644 --- a/crates/apub/src/objects/community.rs +++ b/crates/apub/src/objects/community.rs @@ -1,128 +1,37 @@ -use crate::{ - check_is_apub_id_valid, - collections::{ - community_moderators::ApubCommunityModerators, - community_outbox::ApubCommunityOutbox, - CommunityContext, - }, - context::lemmy_context, - fetcher::object_id::ObjectId, - generate_moderators_url, - generate_outbox_url, - objects::{get_summary_from_string_or_source, tombstone::Tombstone, ImageObject, Source}, - CommunityType, -}; use activitystreams::{ actor::{kind::GroupType, Endpoints}, - base::AnyBase, - chrono::NaiveDateTime, object::kind::ImageType, - primitives::OneOrMany, - unparsed::Unparsed, }; -use chrono::{DateTime, FixedOffset}; +use chrono::NaiveDateTime; use itertools::Itertools; -use lemmy_api_common::blocking; -use lemmy_apub_lib::{ - signatures::PublicKey, - traits::{ActorType, ApubObject}, - values::MediaTypeMarkdown, - verify::verify_domains_match, -}; -use lemmy_db_schema::{ - naive_now, - source::community::{Community, CommunityForm}, - DbPool, -}; -use lemmy_db_views_actor::community_follower_view::CommunityFollowerView; -use lemmy_utils::{ - settings::structs::Settings, - utils::{check_slurs, check_slurs_opt, convert_datetime, markdown_to_html}, - LemmyError, -}; -use lemmy_websocket::LemmyContext; use log::debug; -use serde::{Deserialize, Serialize}; -use serde_with::skip_serializing_none; use std::ops::Deref; use url::Url; -#[skip_serializing_none] -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Group { - #[serde(rename = "@context")] - context: OneOrMany, - #[serde(rename = "type")] - kind: GroupType, - id: Url, - /// username, set at account creation and can never be changed - preferred_username: String, - /// title (can be changed at any time) - name: String, - summary: Option, - source: Option, - icon: Option, - /// banner - image: Option, - // lemmy extension - sensitive: Option, - // lemmy extension - pub(crate) moderators: Option>, - inbox: Url, - pub(crate) outbox: ObjectId, - followers: Url, - endpoints: Endpoints, - public_key: PublicKey, - published: Option>, - updated: Option>, - #[serde(flatten)] - unparsed: Unparsed, -} - -impl Group { - pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> { - verify_domains_match(&self.id, expected_domain)?; - Ok(&self.id) - } - pub(crate) async fn from_apub_to_form( - group: &Group, - expected_domain: &Url, - settings: &Settings, - ) -> Result { - let actor_id = Some(group.id(expected_domain)?.clone().into()); - let name = group.preferred_username.clone(); - let title = group.name.clone(); - let description = get_summary_from_string_or_source(&group.summary, &group.source); - let shared_inbox = group.endpoints.shared_inbox.clone().map(|s| s.into()); - - let slur_regex = &settings.slur_regex(); - check_slurs(&name, slur_regex)?; - check_slurs(&title, slur_regex)?; - check_slurs_opt(&description, slur_regex)?; - - Ok(CommunityForm { - name, - title, - description, - removed: None, - published: group.published.map(|u| u.naive_local()), - updated: group.updated.map(|u| u.naive_local()), - deleted: None, - nsfw: Some(group.sensitive.unwrap_or(false)), - actor_id, - local: Some(false), - private_key: None, - public_key: Some(group.public_key.public_key_pem.clone()), - last_refreshed_at: Some(naive_now()), - icon: Some(group.icon.clone().map(|i| i.url.into())), - banner: Some(group.image.clone().map(|i| i.url.into())), - followers_url: Some(group.followers.clone().into()), - inbox_url: Some(group.inbox.clone().into()), - shared_inbox_url: Some(shared_inbox), - }) - } -} +use crate::{ + check_is_apub_id_valid, + collections::{community_moderators::ApubCommunityModerators, CommunityContext}, + fetcher::object_id::ObjectId, + generate_moderators_url, + generate_outbox_url, + protocol::{ + objects::{group::Group, tombstone::Tombstone}, + ImageObject, + Source, + }, +}; +use lemmy_api_common::blocking; +use lemmy_apub_lib::{ + traits::{ActorType, ApubObject}, + values::MediaTypeMarkdown, +}; +use lemmy_db_schema::source::community::Community; +use lemmy_db_views_actor::community_follower_view::CommunityFollowerView; +use lemmy_utils::{ + utils::{convert_datetime, markdown_to_html}, + LemmyError, +}; +use lemmy_websocket::LemmyContext; #[derive(Clone, Debug)] pub struct ApubCommunity(Community); @@ -186,7 +95,6 @@ impl ApubObject for ApubCommunity { }); let group = Group { - context: lemmy_context(), kind: GroupType::Group, id: self.actor_id(), preferred_username: self.name.clone(), @@ -283,32 +191,32 @@ impl ActorType for ApubCommunity { } } -#[async_trait::async_trait(?Send)] -impl CommunityType for Community { - fn followers_url(&self) -> Url { - self.followers_url.clone().into() - } - +impl ApubCommunity { /// For a given community, returns the inboxes of all followers. - async fn get_follower_inboxes( + pub(crate) async fn get_follower_inboxes( &self, - pool: &DbPool, - settings: &Settings, + additional_inboxes: Vec, + context: &LemmyContext, ) -> Result, LemmyError> { let id = self.id; - let follows = blocking(pool, move |conn| { + let follows = blocking(context.pool(), move |conn| { CommunityFollowerView::for_community(conn, id) }) .await??; - let inboxes = follows + let follower_inboxes: Vec = follows .into_iter() .filter(|f| !f.follower.local) .map(|f| f.follower.shared_inbox_url.unwrap_or(f.follower.inbox_url)) .map(|i| i.into_inner()) + .collect(); + let inboxes = vec![follower_inboxes, additional_inboxes] + .into_iter() + .flatten() .unique() + .filter(|inbox| inbox.host_str() != Some(&context.settings().hostname)) // Don't send to blocked instances - .filter(|inbox| check_is_apub_id_valid(inbox, false, settings).is_ok()) + .filter(|inbox| check_is_apub_id_valid(inbox, false, &context.settings()).is_ok()) .collect(); Ok(inboxes) @@ -316,42 +224,39 @@ impl CommunityType for Community { } #[cfg(test)] -mod tests { +pub(crate) mod tests { use super::*; use crate::objects::tests::{file_to_json_object, init_context}; - use assert_json_diff::assert_json_include; use lemmy_db_schema::traits::Crud; use serial_test::serial; - #[actix_rt::test] - #[serial] - async fn test_parse_lemmy_community() { - let context = init_context(); - let mut json: Group = file_to_json_object("assets/lemmy-community.json"); - let json_orig = json.clone(); + pub(crate) async fn parse_lemmy_community(context: &LemmyContext) -> ApubCommunity { + let mut json: Group = file_to_json_object("assets/lemmy/objects/group.json"); // change these links so they dont fetch over the network - json.moderators = Some(ObjectId::new( - Url::parse("https://enterprise.lemmy.ml/c/tenforward/not_moderators").unwrap(), - )); + json.moderators = None; json.outbox = ObjectId::new(Url::parse("https://enterprise.lemmy.ml/c/tenforward/not_outbox").unwrap()); let url = Url::parse("https://enterprise.lemmy.ml/c/tenforward").unwrap(); let mut request_counter = 0; - let community = ApubCommunity::from_apub(&json, &context, &url, &mut request_counter) + let community = ApubCommunity::from_apub(&json, context, &url, &mut request_counter) .await .unwrap(); + // this makes two requests to the (intentionally) broken outbox/moderators collections + assert_eq!(request_counter, 1); + community + } + + #[actix_rt::test] + #[serial] + async fn test_parse_lemmy_community() { + let context = init_context(); + let community = parse_lemmy_community(&context).await; - assert_eq!(community.actor_id.clone().into_inner(), url); assert_eq!(community.title, "Ten Forward"); assert!(community.public_key.is_some()); assert!(!community.local); assert_eq!(community.description.as_ref().unwrap().len(), 132); - // this makes two requests to the (intentionally) broken outbox/moderators collections - assert_eq!(request_counter, 2); - - let to_apub = community.to_apub(&context).await.unwrap(); - assert_json_include!(actual: json_orig, expected: to_apub); Community::delete(&*context.pool().get().unwrap(), community.id).unwrap(); } diff --git a/crates/apub/src/objects/mod.rs b/crates/apub/src/objects/mod.rs index 96700cb71..b577dabef 100644 --- a/crates/apub/src/objects/mod.rs +++ b/crates/apub/src/objects/mod.rs @@ -1,32 +1,13 @@ -use activitystreams::object::kind::ImageType; +use crate::protocol::Source; use html2md::parse_html; -use lemmy_apub_lib::values::MediaTypeMarkdown; -use serde::{Deserialize, Serialize}; -use url::Url; pub mod comment; pub mod community; pub mod person; pub mod post; pub mod private_message; -pub mod tombstone; -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Source { - content: String, - media_type: MediaTypeMarkdown, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ImageObject { - #[serde(rename = "type")] - kind: ImageType, - url: Url, -} - -fn get_summary_from_string_or_source( +pub(crate) fn get_summary_from_string_or_source( raw: &Option, source: &Option, ) -> Option { @@ -38,7 +19,7 @@ fn get_summary_from_string_or_source( } #[cfg(test)] -mod tests { +pub(crate) mod tests { use actix::Actor; use diesel::{ r2d2::{ConnectionManager, Pool}, diff --git a/crates/apub/src/objects/person.rs b/crates/apub/src/objects/person.rs index f4ba225b8..18522e569 100644 --- a/crates/apub/src/objects/person.rs +++ b/crates/apub/src/objects/person.rs @@ -1,21 +1,17 @@ use crate::{ check_is_apub_id_valid, - context::lemmy_context, generate_outbox_url, - objects::{get_summary_from_string_or_source, ImageObject, Source}, + objects::get_summary_from_string_or_source, + protocol::{ + objects::person::{Person, UserTypes}, + ImageObject, + Source, + }, }; -use activitystreams::{ - actor::Endpoints, - base::AnyBase, - chrono::NaiveDateTime, - object::{kind::ImageType, Tombstone}, - primitives::OneOrMany, - unparsed::Unparsed, -}; -use chrono::{DateTime, FixedOffset}; +use activitystreams::{actor::Endpoints, object::kind::ImageType}; +use chrono::NaiveDateTime; use lemmy_api_common::blocking; use lemmy_apub_lib::{ - signatures::PublicKey, traits::{ActorType, ApubObject}, values::MediaTypeMarkdown, verify::verify_domains_match, @@ -29,56 +25,9 @@ use lemmy_utils::{ LemmyError, }; use lemmy_websocket::LemmyContext; -use serde::{Deserialize, Serialize}; -use serde_with::skip_serializing_none; use std::ops::Deref; use url::Url; -#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)] -pub enum UserTypes { - Person, - Service, -} - -#[skip_serializing_none] -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Person { - #[serde(rename = "@context")] - context: OneOrMany, - #[serde(rename = "type")] - kind: UserTypes, - id: Url, - /// username, set at account creation and can never be changed - preferred_username: String, - /// displayname (can be changed at any time) - name: Option, - summary: Option, - source: Option, - /// user avatar - icon: Option, - /// user banner - image: Option, - matrix_user_id: Option, - inbox: Url, - /// mandatory field in activitypub, currently empty in lemmy - outbox: Url, - endpoints: Endpoints, - public_key: PublicKey, - published: Option>, - updated: Option>, - #[serde(flatten)] - unparsed: Unparsed, -} - -// TODO: can generate this with a derive macro -impl Person { - pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> { - verify_domains_match(&self.id, expected_domain)?; - Ok(&self.id) - } -} - #[derive(Clone, Debug, PartialEq)] pub struct ApubPerson(DbPerson); @@ -99,7 +48,7 @@ impl From for ApubPerson { impl ApubObject for ApubPerson { type DataType = LemmyContext; type ApubType = Person; - type TombstoneType = Tombstone; + type TombstoneType = (); fn last_refreshed_at(&self) -> Option { Some(self.last_refreshed_at) @@ -146,7 +95,6 @@ impl ApubObject for ApubPerson { }); let person = Person { - context: lemmy_context(), kind, id: self.actor_id.to_owned().into_inner(), preferred_username: self.name.clone(), @@ -170,7 +118,7 @@ impl ApubObject for ApubPerson { Ok(person) } - fn to_tombstone(&self) -> Result { + fn to_tombstone(&self) -> Result<(), LemmyError> { unimplemented!() } @@ -180,7 +128,8 @@ impl ApubObject for ApubPerson { expected_domain: &Url, _request_counter: &mut i32, ) -> Result { - let actor_id = Some(person.id(expected_domain)?.clone().into()); + verify_domains_match(&person.id, expected_domain)?; + let actor_id = Some(person.id.clone().into()); let name = person.preferred_username.clone(); let display_name: Option = person.name.clone(); let bio = get_summary_from_string_or_source(&person.summary, &person.source); @@ -255,33 +204,33 @@ impl ActorType for ApubPerson { } #[cfg(test)] -mod tests { +pub(crate) mod tests { use super::*; use crate::objects::tests::{file_to_json_object, init_context}; - use assert_json_diff::assert_json_include; use lemmy_db_schema::traits::Crud; use serial_test::serial; + pub(crate) async fn parse_lemmy_person(context: &LemmyContext) -> ApubPerson { + let json = file_to_json_object("assets/lemmy/objects/person.json"); + let url = Url::parse("https://enterprise.lemmy.ml/u/picard").unwrap(); + let mut request_counter = 0; + let person = ApubPerson::from_apub(&json, context, &url, &mut request_counter) + .await + .unwrap(); + assert_eq!(request_counter, 0); + person + } + #[actix_rt::test] #[serial] async fn test_parse_lemmy_person() { let context = init_context(); - let json = file_to_json_object("assets/lemmy-person.json"); - let url = Url::parse("https://enterprise.lemmy.ml/u/picard").unwrap(); - let mut request_counter = 0; - let person = ApubPerson::from_apub(&json, &context, &url, &mut request_counter) - .await - .unwrap(); + let person = parse_lemmy_person(&context).await; - assert_eq!(person.actor_id.clone().into_inner(), url); assert_eq!(person.display_name, Some("Jean-Luc Picard".to_string())); assert!(person.public_key.is_some()); assert!(!person.local); assert_eq!(person.bio.as_ref().unwrap().len(), 39); - assert_eq!(request_counter, 0); - - let to_apub = person.to_apub(&context).await.unwrap(); - assert_json_include!(actual: json, expected: to_apub); DbPerson::delete(&*context.pool().get().unwrap(), person.id).unwrap(); } @@ -290,7 +239,7 @@ mod tests { #[serial] async fn test_parse_pleroma_person() { let context = init_context(); - let json = file_to_json_object("assets/pleroma-person.json"); + let json = file_to_json_object("assets/pleroma/objects/person.json"); let url = Url::parse("https://queer.hacktivis.me/users/lanodan").unwrap(); let mut request_counter = 0; let person = ApubPerson::from_apub(&json, &context, &url, &mut request_counter) diff --git a/crates/apub/src/objects/post.rs b/crates/apub/src/objects/post.rs index 8b0161883..c19c62779 100644 --- a/crates/apub/src/objects/post.rs +++ b/crates/apub/src/objects/post.rs @@ -1,22 +1,22 @@ use crate::{ - activities::{extract_community, verify_is_public, verify_person_in_community}, - context::lemmy_context, + activities::verify_person_in_community, + check_is_apub_id_valid, fetcher::object_id::ObjectId, - objects::{person::ApubPerson, tombstone::Tombstone, ImageObject, Source}, + protocol::{ + objects::{page::Page, tombstone::Tombstone}, + ImageObject, + Source, + }, }; use activitystreams::{ - base::AnyBase, object::kind::{ImageType, PageType}, - primitives::OneOrMany, public, - unparsed::Unparsed, }; -use chrono::{DateTime, FixedOffset, NaiveDateTime}; +use chrono::NaiveDateTime; use lemmy_api_common::blocking; use lemmy_apub_lib::{ - traits::{ActorType, ApubObject}, + traits::ApubObject, values::{MediaTypeHtml, MediaTypeMarkdown}, - verify::verify_domains_match, }; use lemmy_db_schema::{ self, @@ -29,87 +29,13 @@ use lemmy_db_schema::{ }; use lemmy_utils::{ request::fetch_site_data, - utils::{check_slurs, convert_datetime, markdown_to_html, remove_slurs}, + utils::{convert_datetime, markdown_to_html, remove_slurs}, LemmyError, }; use lemmy_websocket::LemmyContext; -use serde::{Deserialize, Serialize}; -use serde_with::skip_serializing_none; use std::ops::Deref; use url::Url; -#[skip_serializing_none] -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Page { - #[serde(rename = "@context")] - context: OneOrMany, - r#type: PageType, - id: Url, - pub(crate) attributed_to: ObjectId, - to: Vec, - name: String, - content: Option, - media_type: Option, - source: Option, - url: Option, - image: Option, - pub(crate) comments_enabled: Option, - sensitive: Option, - pub(crate) stickied: Option, - published: Option>, - updated: Option>, - #[serde(flatten)] - unparsed: Unparsed, -} - -impl Page { - pub(crate) fn id_unchecked(&self) -> &Url { - &self.id - } - pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> { - verify_domains_match(&self.id, expected_domain)?; - Ok(&self.id) - } - - /// 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. - /// - /// Both stickied and locked need to be false on a newly created post (verified in [[CreatePost]]. - pub(crate) async fn is_mod_action(&self, context: &LemmyContext) -> Result { - let old_post = ObjectId::::new(self.id.clone()) - .dereference_local(context) - .await; - - 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> { - let community = extract_community(&self.to, context, request_counter).await?; - - check_slurs(&self.name, &context.settings().slur_regex())?; - verify_domains_match(self.attributed_to.inner(), &self.id.clone())?; - verify_person_in_community( - &self.attributed_to, - &ObjectId::new(community.actor_id()), - context, - request_counter, - ) - .await?; - verify_is_public(&self.to.clone())?; - Ok(()) - } -} - #[derive(Clone, Debug)] pub struct ApubPost(Post); @@ -177,7 +103,6 @@ impl ApubObject for ApubPost { }); let page = Page { - context: lemmy_context(), r#type: PageType::Page, id: self.ap_id.clone().into(), attributed_to: ObjectId::new(creator.actor_id), @@ -223,7 +148,9 @@ impl ApubObject for ApubPost { .attributed_to .dereference(context, request_counter) .await?; - let community = extract_community(&page.to, context, request_counter).await?; + let community = page.extract_community(context, request_counter).await?; + check_is_apub_id_valid(&page.id, community.local, &context.settings())?; + verify_person_in_community(&page.attributed_to, &community, context, request_counter).await?; let thumbnail_url: Option = page.image.clone().map(|i| i.url); let (metadata_res, pictrs_thumbnail) = if let Some(url) = &page.url { @@ -268,26 +195,22 @@ impl ApubObject for ApubPost { mod tests { use super::*; use crate::objects::{ - community::ApubCommunity, + community::tests::parse_lemmy_community, + person::tests::parse_lemmy_person, + post::ApubPost, tests::{file_to_json_object, init_context}, }; - use assert_json_diff::assert_json_include; use serial_test::serial; #[actix_rt::test] #[serial] async fn test_parse_lemmy_post() { let context = init_context(); + let community = parse_lemmy_community(&context).await; + let person = parse_lemmy_person(&context).await; + + let json = file_to_json_object("assets/lemmy/objects/page.json"); let url = Url::parse("https://enterprise.lemmy.ml/post/55143").unwrap(); - let community_json = file_to_json_object("assets/lemmy-community.json"); - let community = ApubCommunity::from_apub(&community_json, &context, &url, &mut 0) - .await - .unwrap(); - let person_json = file_to_json_object("assets/lemmy-person.json"); - let person = ApubPerson::from_apub(&person_json, &context, &url, &mut 0) - .await - .unwrap(); - let json = file_to_json_object("assets/lemmy-post.json"); let mut request_counter = 0; let post = ApubPost::from_apub(&json, &context, &url, &mut request_counter) .await @@ -301,9 +224,6 @@ mod tests { assert!(post.stickied); assert_eq!(request_counter, 0); - let to_apub = post.to_apub(&context).await.unwrap(); - assert_json_include!(actual: json, expected: to_apub); - Post::delete(&*context.pool().get().unwrap(), post.id).unwrap(); Person::delete(&*context.pool().get().unwrap(), person.id).unwrap(); Community::delete(&*context.pool().get().unwrap(), community.id).unwrap(); diff --git a/crates/apub/src/objects/private_message.rs b/crates/apub/src/objects/private_message.rs index 407ae8101..81615fdc9 100644 --- a/crates/apub/src/objects/private_message.rs +++ b/crates/apub/src/objects/private_message.rs @@ -1,23 +1,16 @@ use crate::{ - context::lemmy_context, fetcher::object_id::ObjectId, - objects::{person::ApubPerson, Source}, + protocol::{ + objects::chat_message::{ChatMessage, ChatMessageType}, + Source, + }, }; -use activitystreams::{ - base::AnyBase, - chrono::NaiveDateTime, - object::Tombstone, - primitives::OneOrMany, - unparsed::Unparsed, -}; -use anyhow::anyhow; -use chrono::{DateTime, FixedOffset}; +use chrono::NaiveDateTime; use html2md::parse_html; use lemmy_api_common::blocking; use lemmy_apub_lib::{ traits::ApubObject, values::{MediaTypeHtml, MediaTypeMarkdown}, - verify::verify_domains_match, }; use lemmy_db_schema::{ source::{ @@ -26,64 +19,14 @@ use lemmy_db_schema::{ }, traits::Crud, }; -use lemmy_utils::{utils::convert_datetime, LemmyError}; +use lemmy_utils::{ + utils::{convert_datetime, markdown_to_html}, + LemmyError, +}; use lemmy_websocket::LemmyContext; -use serde::{Deserialize, Serialize}; -use serde_with::skip_serializing_none; use std::ops::Deref; use url::Url; -#[skip_serializing_none] -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Note { - #[serde(rename = "@context")] - context: OneOrMany, - r#type: ChatMessageType, - id: Url, - pub(crate) attributed_to: ObjectId, - to: [ObjectId; 1], - content: String, - media_type: Option, - source: Option, - published: Option>, - updated: Option>, - #[serde(flatten)] - unparsed: Unparsed, -} - -/// https://docs.pleroma.social/backend/development/ap_extensions/#chatmessages -#[derive(Clone, Debug, Deserialize, Serialize)] -pub enum ChatMessageType { - ChatMessage, -} - -impl Note { - pub(crate) fn id_unchecked(&self) -> &Url { - &self.id - } - pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> { - verify_domains_match(&self.id, expected_domain)?; - Ok(&self.id) - } - - pub(crate) async fn verify( - &self, - context: &LemmyContext, - request_counter: &mut i32, - ) -> Result<(), LemmyError> { - verify_domains_match(self.attributed_to.inner(), &self.id)?; - let person = self - .attributed_to - .dereference(context, request_counter) - .await?; - if person.banned { - return Err(anyhow!("Person is banned from site").into()); - } - Ok(()) - } -} - #[derive(Clone, Debug)] pub struct ApubPrivateMessage(PrivateMessage); @@ -103,8 +46,8 @@ impl From for ApubPrivateMessage { #[async_trait::async_trait(?Send)] impl ApubObject for ApubPrivateMessage { type DataType = LemmyContext; - type ApubType = Note; - type TombstoneType = Tombstone; + type ApubType = ChatMessage; + type TombstoneType = (); fn last_refreshed_at(&self) -> Option { None @@ -128,7 +71,7 @@ impl ApubObject for ApubPrivateMessage { unimplemented!() } - async fn to_apub(&self, context: &LemmyContext) -> Result { + async fn to_apub(&self, context: &LemmyContext) -> Result { let creator_id = self.creator_id; let creator = blocking(context.pool(), move |conn| Person::read(conn, creator_id)).await??; @@ -136,13 +79,12 @@ impl ApubObject for ApubPrivateMessage { let recipient = blocking(context.pool(), move |conn| Person::read(conn, recipient_id)).await??; - let note = Note { - context: lemmy_context(), + let note = ChatMessage { r#type: ChatMessageType::ChatMessage, id: self.ap_id.clone().into(), attributed_to: ObjectId::new(creator.actor_id), to: [ObjectId::new(recipient.actor_id)], - content: self.content.clone(), + content: markdown_to_html(&self.content), media_type: Some(MediaTypeHtml::Html), source: Some(Source { content: self.content.clone(), @@ -155,12 +97,12 @@ impl ApubObject for ApubPrivateMessage { Ok(note) } - fn to_tombstone(&self) -> Result { + fn to_tombstone(&self) -> Result<(), LemmyError> { unimplemented!() } async fn from_apub( - note: &Note, + note: &ChatMessage, context: &LemmyContext, expected_domain: &Url, request_counter: &mut i32, @@ -199,16 +141,19 @@ impl ApubObject for ApubPrivateMessage { #[cfg(test)] mod tests { use super::*; - use crate::objects::tests::{file_to_json_object, init_context}; + use crate::objects::{ + person::ApubPerson, + tests::{file_to_json_object, init_context}, + }; use assert_json_diff::assert_json_include; use serial_test::serial; async fn prepare_comment_test(url: &Url, context: &LemmyContext) -> (ApubPerson, ApubPerson) { - let lemmy_person = file_to_json_object("assets/lemmy-person.json"); + let lemmy_person = file_to_json_object("assets/lemmy/objects/person.json"); let person1 = ApubPerson::from_apub(&lemmy_person, context, url, &mut 0) .await .unwrap(); - let pleroma_person = file_to_json_object("assets/pleroma-person.json"); + let pleroma_person = file_to_json_object("assets/pleroma/objects/person.json"); let pleroma_url = Url::parse("https://queer.hacktivis.me/users/lanodan").unwrap(); let person2 = ApubPerson::from_apub(&pleroma_person, context, &pleroma_url, &mut 0) .await @@ -227,7 +172,7 @@ mod tests { let context = init_context(); let url = Url::parse("https://enterprise.lemmy.ml/private_message/1621").unwrap(); let data = prepare_comment_test(&url, &context).await; - let json = file_to_json_object("assets/lemmy-private-message.json"); + let json = file_to_json_object("assets/lemmy/objects/chat_message.json"); let mut request_counter = 0; let pm = ApubPrivateMessage::from_apub(&json, &context, &url, &mut request_counter) .await @@ -251,7 +196,7 @@ mod tests { let url = Url::parse("https://enterprise.lemmy.ml/private_message/1621").unwrap(); let data = prepare_comment_test(&url, &context).await; let pleroma_url = Url::parse("https://queer.hacktivis.me/objects/2").unwrap(); - let json = file_to_json_object("assets/pleroma-private-message.json"); + let json = file_to_json_object("assets/pleroma/objects/chat_message.json"); let mut request_counter = 0; let pm = ApubPrivateMessage::from_apub(&json, &context, &pleroma_url, &mut request_counter) .await diff --git a/crates/apub/src/protocol/activities/community/add_mod.rs b/crates/apub/src/protocol/activities/community/add_mod.rs new file mode 100644 index 000000000..74ec46457 --- /dev/null +++ b/crates/apub/src/protocol/activities/community/add_mod.rs @@ -0,0 +1,20 @@ +use crate::{fetcher::object_id::ObjectId, objects::person::ApubPerson}; +use activitystreams::{activity::kind::AddType, unparsed::Unparsed}; +use lemmy_apub_lib::traits::ActivityFields; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] +#[serde(rename_all = "camelCase")] +pub struct AddMod { + pub(crate) actor: ObjectId, + pub(crate) to: Vec, + pub(crate) object: ObjectId, + pub(crate) target: Url, + pub(crate) cc: Vec, + #[serde(rename = "type")] + pub(crate) kind: AddType, + pub(crate) id: Url, + #[serde(flatten)] + pub(crate) unparsed: Unparsed, +} diff --git a/crates/apub/src/protocol/activities/community/announce.rs b/crates/apub/src/protocol/activities/community/announce.rs new file mode 100644 index 000000000..2f4e9bd26 --- /dev/null +++ b/crates/apub/src/protocol/activities/community/announce.rs @@ -0,0 +1,23 @@ +use crate::{ + activity_lists::AnnouncableActivities, + fetcher::object_id::ObjectId, + objects::community::ApubCommunity, +}; +use activitystreams::{activity::kind::AnnounceType, unparsed::Unparsed}; +use lemmy_apub_lib::traits::ActivityFields; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] +#[serde(rename_all = "camelCase")] +pub struct AnnounceActivity { + pub(crate) actor: ObjectId, + pub(crate) to: Vec, + pub(crate) object: AnnouncableActivities, + pub(crate) cc: Vec, + #[serde(rename = "type")] + pub(crate) kind: AnnounceType, + pub(crate) id: Url, + #[serde(flatten)] + pub(crate) unparsed: Unparsed, +} diff --git a/crates/apub/src/protocol/activities/community/block_user.rs b/crates/apub/src/protocol/activities/community/block_user.rs new file mode 100644 index 000000000..4ede06ae1 --- /dev/null +++ b/crates/apub/src/protocol/activities/community/block_user.rs @@ -0,0 +1,23 @@ +use crate::{ + fetcher::object_id::ObjectId, + objects::{community::ApubCommunity, person::ApubPerson}, +}; +use activitystreams::{activity::kind::BlockType, unparsed::Unparsed}; +use lemmy_apub_lib::traits::ActivityFields; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] +#[serde(rename_all = "camelCase")] +pub struct BlockUserFromCommunity { + pub(crate) actor: ObjectId, + pub(crate) to: Vec, + pub(crate) object: ObjectId, + pub(crate) cc: Vec, + pub(crate) target: ObjectId, + #[serde(rename = "type")] + pub(crate) kind: BlockType, + pub(crate) id: Url, + #[serde(flatten)] + pub(crate) unparsed: Unparsed, +} diff --git a/crates/apub/src/protocol/activities/community/mod.rs b/crates/apub/src/protocol/activities/community/mod.rs new file mode 100644 index 000000000..a7bc81411 --- /dev/null +++ b/crates/apub/src/protocol/activities/community/mod.rs @@ -0,0 +1,48 @@ +pub mod add_mod; +pub mod announce; +pub mod block_user; +pub mod remove_mod; +pub mod report; +pub mod undo_block_user; +pub mod update; + +#[cfg(test)] +mod tests { + use crate::protocol::{ + activities::community::{ + add_mod::AddMod, + block_user::BlockUserFromCommunity, + remove_mod::RemoveMod, + report::Report, + undo_block_user::UndoBlockUserFromCommunity, + update::UpdateCommunity, + }, + tests::test_parse_lemmy_item, + }; + use activitystreams::activity::Announce; + use serial_test::serial; + + #[actix_rt::test] + #[serial] + async fn test_parse_lemmy_community() { + test_parse_lemmy_item::( + "assets/lemmy/activities/community/announce_create_page.json", + ); + + test_parse_lemmy_item::("assets/lemmy/activities/community/add_mod.json"); + test_parse_lemmy_item::("assets/lemmy/activities/community/remove_mod.json"); + + test_parse_lemmy_item::( + "assets/lemmy/activities/community/block_user.json", + ); + test_parse_lemmy_item::( + "assets/lemmy/activities/community/undo_block_user.json", + ); + + test_parse_lemmy_item::( + "assets/lemmy/activities/community/update_community.json", + ); + + test_parse_lemmy_item::("assets/lemmy/activities/community/report_page.json"); + } +} diff --git a/crates/apub/src/protocol/activities/community/remove_mod.rs b/crates/apub/src/protocol/activities/community/remove_mod.rs new file mode 100644 index 000000000..db30ddbe4 --- /dev/null +++ b/crates/apub/src/protocol/activities/community/remove_mod.rs @@ -0,0 +1,20 @@ +use crate::{fetcher::object_id::ObjectId, objects::person::ApubPerson}; +use activitystreams::{activity::kind::RemoveType, unparsed::Unparsed}; +use lemmy_apub_lib::traits::ActivityFields; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] +#[serde(rename_all = "camelCase")] +pub struct RemoveMod { + pub(crate) actor: ObjectId, + pub(crate) to: Vec, + pub(crate) object: ObjectId, + pub(crate) cc: Vec, + #[serde(rename = "type")] + pub(crate) kind: RemoveType, + pub(crate) target: Url, + pub(crate) id: Url, + #[serde(flatten)] + pub(crate) unparsed: Unparsed, +} diff --git a/crates/apub/src/protocol/activities/community/report.rs b/crates/apub/src/protocol/activities/community/report.rs new file mode 100644 index 000000000..5efdd792e --- /dev/null +++ b/crates/apub/src/protocol/activities/community/report.rs @@ -0,0 +1,22 @@ +use crate::{ + fetcher::{object_id::ObjectId, post_or_comment::PostOrComment}, + objects::{community::ApubCommunity, person::ApubPerson}, +}; +use activitystreams::{activity::kind::FlagType, unparsed::Unparsed}; +use lemmy_apub_lib::traits::ActivityFields; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] +#[serde(rename_all = "camelCase")] +pub struct Report { + pub(crate) actor: ObjectId, + pub(crate) to: [ObjectId; 1], + pub(crate) object: ObjectId, + pub(crate) summary: String, + #[serde(rename = "type")] + pub(crate) kind: FlagType, + pub(crate) id: Url, + #[serde(flatten)] + pub(crate) unparsed: Unparsed, +} diff --git a/crates/apub/src/protocol/activities/community/undo_block_user.rs b/crates/apub/src/protocol/activities/community/undo_block_user.rs new file mode 100644 index 000000000..0e89f87ef --- /dev/null +++ b/crates/apub/src/protocol/activities/community/undo_block_user.rs @@ -0,0 +1,23 @@ +use crate::{ + fetcher::object_id::ObjectId, + objects::person::ApubPerson, + protocol::activities::community::block_user::BlockUserFromCommunity, +}; +use activitystreams::{activity::kind::UndoType, unparsed::Unparsed}; +use lemmy_apub_lib::traits::ActivityFields; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] +#[serde(rename_all = "camelCase")] +pub struct UndoBlockUserFromCommunity { + pub(crate) actor: ObjectId, + pub(crate) to: Vec, + pub(crate) object: BlockUserFromCommunity, + pub(crate) cc: Vec, + #[serde(rename = "type")] + pub(crate) kind: UndoType, + pub(crate) id: Url, + #[serde(flatten)] + pub(crate) unparsed: Unparsed, +} diff --git a/crates/apub/src/protocol/activities/community/update.rs b/crates/apub/src/protocol/activities/community/update.rs new file mode 100644 index 000000000..4ba1ed843 --- /dev/null +++ b/crates/apub/src/protocol/activities/community/update.rs @@ -0,0 +1,26 @@ +use crate::{ + fetcher::object_id::ObjectId, + objects::person::ApubPerson, + protocol::objects::group::Group, +}; +use activitystreams::{activity::kind::UpdateType, unparsed::Unparsed}; +use lemmy_apub_lib::traits::ActivityFields; +use serde::{Deserialize, Serialize}; +use url::Url; + +/// This activity is received from a remote community mod, and updates the description or other +/// fields of a local community. +#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] +#[serde(rename_all = "camelCase")] +pub struct UpdateCommunity { + pub(crate) actor: ObjectId, + pub(crate) to: Vec, + // TODO: would be nice to use a separate struct here, which only contains the fields updated here + pub(crate) object: Group, + pub(crate) cc: Vec, + #[serde(rename = "type")] + pub(crate) kind: UpdateType, + pub(crate) id: Url, + #[serde(flatten)] + pub(crate) unparsed: Unparsed, +} diff --git a/crates/apub/src/protocol/activities/create_or_update/comment.rs b/crates/apub/src/protocol/activities/create_or_update/comment.rs new file mode 100644 index 000000000..ede7417bc --- /dev/null +++ b/crates/apub/src/protocol/activities/create_or_update/comment.rs @@ -0,0 +1,25 @@ +use crate::{ + fetcher::object_id::ObjectId, + objects::person::ApubPerson, + protocol::{activities::CreateOrUpdateType, objects::note::Note}, +}; +use activitystreams::{link::Mention, unparsed::Unparsed}; +use lemmy_apub_lib::traits::ActivityFields; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] +#[serde(rename_all = "camelCase")] +pub struct CreateOrUpdateComment { + pub(crate) actor: ObjectId, + pub(crate) to: Vec, + pub(crate) object: Note, + pub(crate) cc: Vec, + #[serde(default)] + pub(crate) tag: Vec, + #[serde(rename = "type")] + pub(crate) kind: CreateOrUpdateType, + pub(crate) id: Url, + #[serde(flatten)] + pub(crate) unparsed: Unparsed, +} diff --git a/crates/apub/src/protocol/activities/create_or_update/mod.rs b/crates/apub/src/protocol/activities/create_or_update/mod.rs new file mode 100644 index 000000000..1048d46ac --- /dev/null +++ b/crates/apub/src/protocol/activities/create_or_update/mod.rs @@ -0,0 +1,34 @@ +pub mod comment; +pub mod post; + +#[cfg(test)] +mod tests { + use crate::{ + objects::tests::file_to_json_object, + protocol::{ + activities::create_or_update::{comment::CreateOrUpdateComment, post::CreateOrUpdatePost}, + tests::test_parse_lemmy_item, + }, + }; + use serial_test::serial; + + #[actix_rt::test] + #[serial] + async fn test_parse_lemmy_create_or_update() { + test_parse_lemmy_item::( + "assets/lemmy/activities/create_or_update/create_page.json", + ); + test_parse_lemmy_item::( + "assets/lemmy/activities/create_or_update/update_page.json", + ); + test_parse_lemmy_item::( + "assets/lemmy/activities/create_or_update/create_note.json", + ); + } + + #[actix_rt::test] + #[serial] + async fn test_parse_pleroma_create_or_update() { + file_to_json_object::("assets/pleroma/activities/create_note.json"); + } +} diff --git a/crates/apub/src/protocol/activities/create_or_update/post.rs b/crates/apub/src/protocol/activities/create_or_update/post.rs new file mode 100644 index 000000000..03b283e3c --- /dev/null +++ b/crates/apub/src/protocol/activities/create_or_update/post.rs @@ -0,0 +1,23 @@ +use crate::{ + fetcher::object_id::ObjectId, + objects::person::ApubPerson, + protocol::{activities::CreateOrUpdateType, objects::page::Page}, +}; +use activitystreams::unparsed::Unparsed; +use lemmy_apub_lib::traits::ActivityFields; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] +#[serde(rename_all = "camelCase")] +pub struct CreateOrUpdatePost { + pub(crate) actor: ObjectId, + pub(crate) to: Vec, + pub(crate) object: Page, + pub(crate) cc: Vec, + #[serde(rename = "type")] + pub(crate) kind: CreateOrUpdateType, + pub(crate) id: Url, + #[serde(flatten)] + pub(crate) unparsed: Unparsed, +} diff --git a/crates/apub/src/protocol/activities/deletion/delete.rs b/crates/apub/src/protocol/activities/deletion/delete.rs new file mode 100644 index 000000000..f8e81b47a --- /dev/null +++ b/crates/apub/src/protocol/activities/deletion/delete.rs @@ -0,0 +1,24 @@ +use crate::{fetcher::object_id::ObjectId, objects::person::ApubPerson}; +use activitystreams::{activity::kind::DeleteType, unparsed::Unparsed}; +use lemmy_apub_lib::traits::ActivityFields; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +use url::Url; + +#[skip_serializing_none] +#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] +#[serde(rename_all = "camelCase")] +pub struct Delete { + pub(crate) actor: ObjectId, + pub(crate) to: Vec, + pub(crate) object: Url, + pub(crate) cc: Vec, + #[serde(rename = "type")] + pub(crate) kind: DeleteType, + /// If summary is present, this is a mod action (Remove in Lemmy terms). Otherwise, its a user + /// deleting their own content. + pub(crate) summary: Option, + pub(crate) id: Url, + #[serde(flatten)] + pub(crate) unparsed: Unparsed, +} diff --git a/crates/apub/src/protocol/activities/deletion/mod.rs b/crates/apub/src/protocol/activities/deletion/mod.rs new file mode 100644 index 000000000..c77492a32 --- /dev/null +++ b/crates/apub/src/protocol/activities/deletion/mod.rs @@ -0,0 +1,21 @@ +pub mod delete; +pub mod undo_delete; + +#[cfg(test)] +mod tests { + use crate::protocol::{ + activities::deletion::{delete::Delete, undo_delete::UndoDelete}, + tests::test_parse_lemmy_item, + }; + use serial_test::serial; + + #[actix_rt::test] + #[serial] + async fn test_parse_lemmy_voting() { + test_parse_lemmy_item::("assets/lemmy/activities/deletion/remove_note.json"); + test_parse_lemmy_item::("assets/lemmy/activities/deletion/delete_page.json"); + + test_parse_lemmy_item::("assets/lemmy/activities/deletion/undo_remove_note.json"); + test_parse_lemmy_item::("assets/lemmy/activities/deletion/undo_delete_page.json"); + } +} diff --git a/crates/apub/src/protocol/activities/deletion/undo_delete.rs b/crates/apub/src/protocol/activities/deletion/undo_delete.rs new file mode 100644 index 000000000..d962820b3 --- /dev/null +++ b/crates/apub/src/protocol/activities/deletion/undo_delete.rs @@ -0,0 +1,25 @@ +use activitystreams::{activity::kind::UndoType, unparsed::Unparsed}; +use serde::{Deserialize, Serialize}; +use url::Url; + +use lemmy_apub_lib::traits::ActivityFields; + +use crate::{ + fetcher::object_id::ObjectId, + objects::person::ApubPerson, + protocol::activities::deletion::delete::Delete, +}; + +#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] +#[serde(rename_all = "camelCase")] +pub struct UndoDelete { + pub(crate) actor: ObjectId, + pub(crate) to: Vec, + pub(crate) object: Delete, + pub(crate) cc: Vec, + #[serde(rename = "type")] + pub(crate) kind: UndoType, + pub(crate) id: Url, + #[serde(flatten)] + pub(crate) unparsed: Unparsed, +} diff --git a/crates/apub/src/protocol/activities/following/accept.rs b/crates/apub/src/protocol/activities/following/accept.rs new file mode 100644 index 000000000..502a908c2 --- /dev/null +++ b/crates/apub/src/protocol/activities/following/accept.rs @@ -0,0 +1,22 @@ +use crate::{ + fetcher::object_id::ObjectId, + objects::{community::ApubCommunity, person::ApubPerson}, + protocol::activities::following::follow::FollowCommunity, +}; +use activitystreams::{activity::kind::AcceptType, unparsed::Unparsed}; +use lemmy_apub_lib::traits::ActivityFields; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] +#[serde(rename_all = "camelCase")] +pub struct AcceptFollowCommunity { + pub(crate) actor: ObjectId, + pub(crate) to: [ObjectId; 1], + pub(crate) object: FollowCommunity, + #[serde(rename = "type")] + pub(crate) kind: AcceptType, + pub(crate) id: Url, + #[serde(flatten)] + pub(crate) unparsed: Unparsed, +} diff --git a/crates/apub/src/protocol/activities/following/follow.rs b/crates/apub/src/protocol/activities/following/follow.rs new file mode 100644 index 000000000..9dfec2163 --- /dev/null +++ b/crates/apub/src/protocol/activities/following/follow.rs @@ -0,0 +1,21 @@ +use crate::{ + fetcher::object_id::ObjectId, + objects::{community::ApubCommunity, person::ApubPerson}, +}; +use activitystreams::{activity::kind::FollowType, unparsed::Unparsed}; +use lemmy_apub_lib::traits::ActivityFields; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] +#[serde(rename_all = "camelCase")] +pub struct FollowCommunity { + pub(crate) actor: ObjectId, + pub(crate) to: [ObjectId; 1], + pub(crate) object: ObjectId, + #[serde(rename = "type")] + pub(crate) kind: FollowType, + pub(crate) id: Url, + #[serde(flatten)] + pub(crate) unparsed: Unparsed, +} diff --git a/crates/apub/src/protocol/activities/following/mod.rs b/crates/apub/src/protocol/activities/following/mod.rs new file mode 100644 index 000000000..473dfca35 --- /dev/null +++ b/crates/apub/src/protocol/activities/following/mod.rs @@ -0,0 +1,26 @@ +pub(crate) mod accept; +pub mod follow; +pub mod undo_follow; + +#[cfg(test)] +mod tests { + use crate::protocol::{ + activities::following::{ + accept::AcceptFollowCommunity, + follow::FollowCommunity, + undo_follow::UndoFollowCommunity, + }, + tests::test_parse_lemmy_item, + }; + use serial_test::serial; + + #[actix_rt::test] + #[serial] + async fn test_parse_lemmy_accept_follow() { + test_parse_lemmy_item::("assets/lemmy/activities/following/follow.json"); + test_parse_lemmy_item::("assets/lemmy/activities/following/accept.json"); + test_parse_lemmy_item::( + "assets/lemmy/activities/following/undo_follow.json", + ); + } +} diff --git a/crates/apub/src/protocol/activities/following/undo_follow.rs b/crates/apub/src/protocol/activities/following/undo_follow.rs new file mode 100644 index 000000000..be6a7ab89 --- /dev/null +++ b/crates/apub/src/protocol/activities/following/undo_follow.rs @@ -0,0 +1,22 @@ +use crate::{ + fetcher::object_id::ObjectId, + objects::{community::ApubCommunity, person::ApubPerson}, + protocol::activities::following::follow::FollowCommunity, +}; +use activitystreams::{activity::kind::UndoType, unparsed::Unparsed}; +use lemmy_apub_lib::traits::ActivityFields; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] +#[serde(rename_all = "camelCase")] +pub struct UndoFollowCommunity { + pub(crate) actor: ObjectId, + pub(crate) to: [ObjectId; 1], + pub(crate) object: FollowCommunity, + #[serde(rename = "type")] + pub(crate) kind: UndoType, + pub(crate) id: Url, + #[serde(flatten)] + pub(crate) unparsed: Unparsed, +} diff --git a/crates/apub/src/protocol/activities/mod.rs b/crates/apub/src/protocol/activities/mod.rs new file mode 100644 index 000000000..23575c279 --- /dev/null +++ b/crates/apub/src/protocol/activities/mod.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; +use strum_macros::ToString; + +pub mod community; +pub mod create_or_update; +pub mod deletion; +pub mod following; +pub mod private_message; +pub mod voting; + +#[derive(Clone, Debug, ToString, Deserialize, Serialize)] +pub enum CreateOrUpdateType { + Create, + Update, +} diff --git a/crates/apub/src/protocol/activities/private_message/create_or_update.rs b/crates/apub/src/protocol/activities/private_message/create_or_update.rs new file mode 100644 index 000000000..7632ef9fe --- /dev/null +++ b/crates/apub/src/protocol/activities/private_message/create_or_update.rs @@ -0,0 +1,22 @@ +use crate::{ + fetcher::object_id::ObjectId, + objects::person::ApubPerson, + protocol::{activities::CreateOrUpdateType, objects::chat_message::ChatMessage}, +}; +use activitystreams::unparsed::Unparsed; +use lemmy_apub_lib::traits::ActivityFields; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] +#[serde(rename_all = "camelCase")] +pub struct CreateOrUpdatePrivateMessage { + pub(crate) id: Url, + pub(crate) actor: ObjectId, + pub(crate) to: [ObjectId; 1], + pub(crate) object: ChatMessage, + #[serde(rename = "type")] + pub(crate) kind: CreateOrUpdateType, + #[serde(flatten)] + pub(crate) unparsed: Unparsed, +} diff --git a/crates/apub/src/protocol/activities/private_message/delete.rs b/crates/apub/src/protocol/activities/private_message/delete.rs new file mode 100644 index 000000000..499d7d1d6 --- /dev/null +++ b/crates/apub/src/protocol/activities/private_message/delete.rs @@ -0,0 +1,21 @@ +use crate::{ + fetcher::object_id::ObjectId, + objects::{person::ApubPerson, private_message::ApubPrivateMessage}, +}; +use activitystreams::{activity::kind::DeleteType, unparsed::Unparsed}; +use lemmy_apub_lib::traits::ActivityFields; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] +#[serde(rename_all = "camelCase")] +pub struct DeletePrivateMessage { + pub(crate) actor: ObjectId, + pub(crate) to: [ObjectId; 1], + pub(crate) object: ObjectId, + #[serde(rename = "type")] + pub(crate) kind: DeleteType, + pub(crate) id: Url, + #[serde(flatten)] + pub(crate) unparsed: Unparsed, +} diff --git a/crates/apub/src/protocol/activities/private_message/mod.rs b/crates/apub/src/protocol/activities/private_message/mod.rs new file mode 100644 index 000000000..b54e66c0c --- /dev/null +++ b/crates/apub/src/protocol/activities/private_message/mod.rs @@ -0,0 +1,30 @@ +pub mod create_or_update; +pub mod delete; +pub mod undo_delete; + +#[cfg(test)] +mod tests { + use crate::protocol::{ + activities::private_message::{ + create_or_update::CreateOrUpdatePrivateMessage, + delete::DeletePrivateMessage, + undo_delete::UndoDeletePrivateMessage, + }, + tests::test_parse_lemmy_item, + }; + use serial_test::serial; + + #[actix_rt::test] + #[serial] + async fn test_parse_lemmy_private_message() { + test_parse_lemmy_item::( + "assets/lemmy/activities/private_message/create.json", + ); + test_parse_lemmy_item::( + "assets/lemmy/activities/private_message/delete.json", + ); + test_parse_lemmy_item::( + "assets/lemmy/activities/private_message/undo_delete.json", + ); + } +} diff --git a/crates/apub/src/protocol/activities/private_message/undo_delete.rs b/crates/apub/src/protocol/activities/private_message/undo_delete.rs new file mode 100644 index 000000000..699f6eec9 --- /dev/null +++ b/crates/apub/src/protocol/activities/private_message/undo_delete.rs @@ -0,0 +1,22 @@ +use crate::{ + fetcher::object_id::ObjectId, + objects::person::ApubPerson, + protocol::activities::private_message::delete::DeletePrivateMessage, +}; +use activitystreams::{activity::kind::UndoType, unparsed::Unparsed}; +use lemmy_apub_lib::traits::ActivityFields; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] +#[serde(rename_all = "camelCase")] +pub struct UndoDeletePrivateMessage { + pub(crate) actor: ObjectId, + pub(crate) to: [ObjectId; 1], + pub(crate) object: DeletePrivateMessage, + #[serde(rename = "type")] + pub(crate) kind: UndoType, + pub(crate) id: Url, + #[serde(flatten)] + pub(crate) unparsed: Unparsed, +} diff --git a/crates/apub/src/protocol/activities/voting/mod.rs b/crates/apub/src/protocol/activities/voting/mod.rs new file mode 100644 index 000000000..48e30f0b1 --- /dev/null +++ b/crates/apub/src/protocol/activities/voting/mod.rs @@ -0,0 +1,21 @@ +pub mod undo_vote; +pub mod vote; + +#[cfg(test)] +mod tests { + use crate::protocol::{ + activities::voting::{undo_vote::UndoVote, vote::Vote}, + tests::test_parse_lemmy_item, + }; + use serial_test::serial; + + #[actix_rt::test] + #[serial] + async fn test_parse_lemmy_voting() { + test_parse_lemmy_item::("assets/lemmy/activities/voting/like_note.json"); + test_parse_lemmy_item::("assets/lemmy/activities/voting/dislike_page.json"); + + test_parse_lemmy_item::("assets/lemmy/activities/voting/undo_like_note.json"); + test_parse_lemmy_item::("assets/lemmy/activities/voting/undo_dislike_page.json"); + } +} diff --git a/crates/apub/src/protocol/activities/voting/undo_vote.rs b/crates/apub/src/protocol/activities/voting/undo_vote.rs new file mode 100644 index 000000000..0d3e66360 --- /dev/null +++ b/crates/apub/src/protocol/activities/voting/undo_vote.rs @@ -0,0 +1,25 @@ +use activitystreams::{activity::kind::UndoType, unparsed::Unparsed}; +use serde::{Deserialize, Serialize}; +use url::Url; + +use lemmy_apub_lib::traits::ActivityFields; + +use crate::{ + fetcher::object_id::ObjectId, + objects::person::ApubPerson, + protocol::activities::voting::vote::Vote, +}; + +#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] +#[serde(rename_all = "camelCase")] +pub struct UndoVote { + pub(crate) actor: ObjectId, + pub(crate) to: Vec, + pub(crate) object: Vote, + pub(crate) cc: Vec, + #[serde(rename = "type")] + pub(crate) kind: UndoType, + pub(crate) id: Url, + #[serde(flatten)] + pub(crate) unparsed: Unparsed, +} diff --git a/crates/apub/src/protocol/activities/voting/vote.rs b/crates/apub/src/protocol/activities/voting/vote.rs new file mode 100644 index 000000000..fdc87a3bd --- /dev/null +++ b/crates/apub/src/protocol/activities/voting/vote.rs @@ -0,0 +1,53 @@ +use crate::{ + fetcher::{object_id::ObjectId, post_or_comment::PostOrComment}, + objects::person::ApubPerson, +}; +use activitystreams::unparsed::Unparsed; +use anyhow::anyhow; +use lemmy_apub_lib::traits::ActivityFields; +use lemmy_utils::LemmyError; +use serde::{Deserialize, Serialize}; +use std::convert::TryFrom; +use strum_macros::ToString; +use url::Url; + +#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)] +#[serde(rename_all = "camelCase")] +pub struct Vote { + pub(crate) actor: ObjectId, + pub(crate) to: Vec, + pub(crate) object: ObjectId, + pub(crate) cc: Vec, + #[serde(rename = "type")] + pub(crate) kind: VoteType, + pub(crate) id: Url, + #[serde(flatten)] + pub(crate) unparsed: Unparsed, +} + +#[derive(Clone, Debug, ToString, Deserialize, Serialize)] +pub enum VoteType { + Like, + Dislike, +} + +impl TryFrom for VoteType { + type Error = LemmyError; + + fn try_from(value: i16) -> Result { + match value { + 1 => Ok(VoteType::Like), + -1 => Ok(VoteType::Dislike), + _ => Err(anyhow!("invalid vote value").into()), + } + } +} + +impl From<&VoteType> for i16 { + fn from(value: &VoteType) -> i16 { + match value { + VoteType::Like => 1, + VoteType::Dislike => -1, + } + } +} diff --git a/crates/apub/src/protocol/collections/group_followers.rs b/crates/apub/src/protocol/collections/group_followers.rs new file mode 100644 index 000000000..d3df5e22b --- /dev/null +++ b/crates/apub/src/protocol/collections/group_followers.rs @@ -0,0 +1,38 @@ +use crate::generate_followers_url; +use activitystreams::collection::kind::CollectionType; +use lemmy_api_common::blocking; +use lemmy_db_schema::source::community::Community; +use lemmy_db_views_actor::community_follower_view::CommunityFollowerView; +use lemmy_utils::LemmyError; +use lemmy_websocket::LemmyContext; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GroupFollowers { + id: Url, + r#type: CollectionType, + total_items: i32, + items: Vec<()>, +} + +impl GroupFollowers { + pub(crate) async fn new( + community: Community, + context: &LemmyContext, + ) -> Result { + let community_id = community.id; + let community_followers = blocking(context.pool(), move |conn| { + CommunityFollowerView::for_community(conn, community_id) + }) + .await??; + + Ok(GroupFollowers { + id: generate_followers_url(&community.actor_id)?.into_inner(), + r#type: CollectionType::Collection, + total_items: community_followers.len() as i32, + items: vec![], + }) + } +} diff --git a/crates/apub/src/protocol/collections/group_moderators.rs b/crates/apub/src/protocol/collections/group_moderators.rs new file mode 100644 index 000000000..d37751a16 --- /dev/null +++ b/crates/apub/src/protocol/collections/group_moderators.rs @@ -0,0 +1,12 @@ +use crate::{fetcher::object_id::ObjectId, objects::person::ApubPerson}; +use activitystreams::collection::kind::OrderedCollectionType; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GroupModerators { + pub(crate) r#type: OrderedCollectionType, + pub(crate) id: Url, + pub(crate) ordered_items: Vec>, +} diff --git a/crates/apub/src/protocol/collections/group_outbox.rs b/crates/apub/src/protocol/collections/group_outbox.rs new file mode 100644 index 000000000..3b295003a --- /dev/null +++ b/crates/apub/src/protocol/collections/group_outbox.rs @@ -0,0 +1,13 @@ +use crate::protocol::activities::create_or_update::post::CreateOrUpdatePost; +use activitystreams::collection::kind::OrderedCollectionType; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GroupOutbox { + pub(crate) r#type: OrderedCollectionType, + pub(crate) id: Url, + pub(crate) total_items: i32, + pub(crate) ordered_items: Vec, +} diff --git a/crates/apub/src/protocol/collections/mod.rs b/crates/apub/src/protocol/collections/mod.rs new file mode 100644 index 000000000..2e2d5c20a --- /dev/null +++ b/crates/apub/src/protocol/collections/mod.rs @@ -0,0 +1,28 @@ +pub(crate) mod group_followers; +pub(crate) mod group_moderators; +pub(crate) mod group_outbox; +pub(crate) mod person_outbox; + +#[cfg(test)] +mod tests { + use crate::protocol::{ + collections::{ + group_followers::GroupFollowers, + group_moderators::GroupModerators, + group_outbox::GroupOutbox, + person_outbox::PersonOutbox, + }, + tests::test_parse_lemmy_item, + }; + use serial_test::serial; + + #[actix_rt::test] + #[serial] + async fn test_parse_lemmy_collections() { + test_parse_lemmy_item::("assets/lemmy/collections/group_followers.json"); + let outbox = test_parse_lemmy_item::("assets/lemmy/collections/group_outbox.json"); + assert_eq!(outbox.ordered_items.len() as i32, outbox.total_items); + test_parse_lemmy_item::("assets/lemmy/collections/group_moderators.json"); + test_parse_lemmy_item::("assets/lemmy/collections/person_outbox.json"); + } +} diff --git a/crates/apub/src/protocol/collections/person_outbox.rs b/crates/apub/src/protocol/collections/person_outbox.rs new file mode 100644 index 000000000..6ec758403 --- /dev/null +++ b/crates/apub/src/protocol/collections/person_outbox.rs @@ -0,0 +1,26 @@ +use crate::generate_outbox_url; +use activitystreams::collection::kind::OrderedCollectionType; +use lemmy_db_schema::source::person::Person; +use lemmy_utils::LemmyError; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct PersonOutbox { + r#type: OrderedCollectionType, + id: Url, + ordered_items: Vec<()>, + total_items: i32, +} + +impl PersonOutbox { + pub(crate) async fn new(user: Person) -> Result { + Ok(PersonOutbox { + r#type: OrderedCollectionType::OrderedCollection, + id: generate_outbox_url(&user.actor_id)?.into_inner(), + ordered_items: vec![], + total_items: 0, + }) + } +} diff --git a/crates/apub/src/protocol/mod.rs b/crates/apub/src/protocol/mod.rs new file mode 100644 index 000000000..22df0354d --- /dev/null +++ b/crates/apub/src/protocol/mod.rs @@ -0,0 +1,41 @@ +use activitystreams::object::kind::ImageType; +use serde::{Deserialize, Serialize}; +use url::Url; + +use lemmy_apub_lib::values::MediaTypeMarkdown; + +pub mod activities; +pub(crate) mod collections; +pub(crate) mod objects; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Source { + pub(crate) content: String, + pub(crate) media_type: MediaTypeMarkdown, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ImageObject { + #[serde(rename = "type")] + pub(crate) kind: ImageType, + pub(crate) url: Url, +} + +#[cfg(test)] +pub(crate) mod tests { + use crate::objects::tests::file_to_json_object; + use assert_json_diff::assert_json_include; + use serde::{de::DeserializeOwned, Serialize}; + use std::collections::HashMap; + + pub(crate) fn test_parse_lemmy_item(path: &str) -> T { + let parsed = file_to_json_object::(path); + + // ensure that no field is ignored when parsing + let raw = file_to_json_object::>(path); + assert_json_include!(actual: &parsed, expected: raw); + parsed + } +} diff --git a/crates/apub/src/protocol/objects/chat_message.rs b/crates/apub/src/protocol/objects/chat_message.rs new file mode 100644 index 000000000..038af4edf --- /dev/null +++ b/crates/apub/src/protocol/objects/chat_message.rs @@ -0,0 +1,61 @@ +use crate::{fetcher::object_id::ObjectId, objects::person::ApubPerson, protocol::Source}; +use activitystreams::{ + chrono::{DateTime, FixedOffset}, + unparsed::Unparsed, +}; +use anyhow::anyhow; +use lemmy_apub_lib::{values::MediaTypeHtml, verify::verify_domains_match}; +use lemmy_utils::LemmyError; +use lemmy_websocket::LemmyContext; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +use url::Url; + +#[skip_serializing_none] +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChatMessage { + pub(crate) r#type: ChatMessageType, + pub(crate) id: Url, + pub(crate) attributed_to: ObjectId, + pub(crate) to: [ObjectId; 1], + pub(crate) content: String, + pub(crate) media_type: Option, + pub(crate) source: Option, + pub(crate) published: Option>, + pub(crate) updated: Option>, + #[serde(flatten)] + pub(crate) unparsed: Unparsed, +} + +/// https://docs.pleroma.social/backend/development/ap_extensions/#chatmessages +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum ChatMessageType { + ChatMessage, +} + +impl ChatMessage { + pub(crate) fn id_unchecked(&self) -> &Url { + &self.id + } + pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> { + verify_domains_match(&self.id, expected_domain)?; + Ok(&self.id) + } + + pub(crate) async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + verify_domains_match(self.attributed_to.inner(), &self.id)?; + let person = self + .attributed_to + .dereference(context, request_counter) + .await?; + if person.banned { + return Err(anyhow!("Person is banned from site").into()); + } + Ok(()) + } +} diff --git a/crates/apub/src/protocol/objects/group.rs b/crates/apub/src/protocol/objects/group.rs new file mode 100644 index 000000000..4da987a25 --- /dev/null +++ b/crates/apub/src/protocol/objects/group.rs @@ -0,0 +1,97 @@ +use crate::{ + check_is_apub_id_valid, + collections::{ + community_moderators::ApubCommunityModerators, + community_outbox::ApubCommunityOutbox, + }, + fetcher::object_id::ObjectId, + objects::get_summary_from_string_or_source, + protocol::{ImageObject, Source}, +}; +use activitystreams::{ + actor::{kind::GroupType, Endpoints}, + unparsed::Unparsed, +}; +use chrono::{DateTime, FixedOffset}; +use lemmy_apub_lib::{signatures::PublicKey, verify::verify_domains_match}; +use lemmy_db_schema::{naive_now, source::community::CommunityForm}; +use lemmy_utils::{ + settings::structs::Settings, + utils::{check_slurs, check_slurs_opt}, + LemmyError, +}; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +use url::Url; + +#[skip_serializing_none] +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Group { + #[serde(rename = "type")] + pub(crate) kind: GroupType, + pub(crate) id: Url, + /// username, set at account creation and can never be changed + pub(crate) preferred_username: String, + /// title (can be changed at any time) + pub(crate) name: String, + pub(crate) summary: Option, + pub(crate) source: Option, + pub(crate) icon: Option, + /// banner + pub(crate) image: Option, + // lemmy extension + pub(crate) sensitive: Option, + // lemmy extension + pub(crate) moderators: Option>, + pub(crate) inbox: Url, + pub(crate) outbox: ObjectId, + pub(crate) followers: Url, + pub(crate) endpoints: Endpoints, + pub(crate) public_key: PublicKey, + pub(crate) published: Option>, + pub(crate) updated: Option>, + #[serde(flatten)] + pub(crate) unparsed: Unparsed, +} + +impl Group { + pub(crate) async fn from_apub_to_form( + group: &Group, + expected_domain: &Url, + settings: &Settings, + ) -> Result { + check_is_apub_id_valid(&group.id, true, settings)?; + verify_domains_match(expected_domain, &group.id)?; + let name = group.preferred_username.clone(); + let title = group.name.clone(); + let description = get_summary_from_string_or_source(&group.summary, &group.source); + let shared_inbox = group.endpoints.shared_inbox.clone().map(|s| s.into()); + + let slur_regex = &settings.slur_regex(); + check_slurs(&name, slur_regex)?; + check_slurs(&title, slur_regex)?; + check_slurs_opt(&description, slur_regex)?; + + Ok(CommunityForm { + name, + title, + description, + removed: None, + published: group.published.map(|u| u.naive_local()), + updated: group.updated.map(|u| u.naive_local()), + deleted: None, + nsfw: Some(group.sensitive.unwrap_or(false)), + actor_id: Some(group.id.clone().into()), + local: Some(false), + private_key: None, + public_key: Some(group.public_key.public_key_pem.clone()), + last_refreshed_at: Some(naive_now()), + icon: Some(group.icon.clone().map(|i| i.url.into())), + banner: Some(group.image.clone().map(|i| i.url.into())), + followers_url: Some(group.followers.clone().into()), + inbox_url: Some(group.inbox.clone().into()), + shared_inbox_url: Some(shared_inbox), + }) + } +} diff --git a/crates/apub/src/protocol/objects/mod.rs b/crates/apub/src/protocol/objects/mod.rs new file mode 100644 index 000000000..529ee6370 --- /dev/null +++ b/crates/apub/src/protocol/objects/mod.rs @@ -0,0 +1,25 @@ +pub(crate) mod chat_message; +pub(crate) mod group; +pub(crate) mod note; +pub(crate) mod page; +pub(crate) mod person; +pub(crate) mod tombstone; + +#[cfg(test)] +mod tests { + use crate::protocol::{ + objects::{chat_message::ChatMessage, group::Group, note::Note, page::Page, person::Person}, + tests::test_parse_lemmy_item, + }; + use serial_test::serial; + + #[actix_rt::test] + #[serial] + async fn test_parse_lemmy_object() { + test_parse_lemmy_item::("assets/lemmy/objects/person.json"); + test_parse_lemmy_item::("assets/lemmy/objects/group.json"); + test_parse_lemmy_item::("assets/lemmy/objects/page.json"); + test_parse_lemmy_item::("assets/lemmy/objects/note.json"); + test_parse_lemmy_item::("assets/lemmy/objects/chat_message.json"); + } +} diff --git a/crates/apub/src/protocol/objects/note.rs b/crates/apub/src/protocol/objects/note.rs new file mode 100644 index 000000000..bdc4da66b --- /dev/null +++ b/crates/apub/src/protocol/objects/note.rs @@ -0,0 +1,112 @@ +use crate::{ + activities::{verify_is_public, verify_person_in_community}, + fetcher::{object_id::ObjectId, post_or_comment::PostOrComment}, + objects::{community::ApubCommunity, person::ApubPerson, post::ApubPost}, + protocol::Source, +}; +use activitystreams::{object::kind::NoteType, unparsed::Unparsed}; +use anyhow::anyhow; +use chrono::{DateTime, FixedOffset}; +use lemmy_api_common::blocking; +use lemmy_apub_lib::{values::MediaTypeHtml, verify::verify_domains_match}; +use lemmy_db_schema::{ + newtypes::CommentId, + source::{community::Community, post::Post}, + traits::Crud, +}; +use lemmy_utils::LemmyError; +use lemmy_websocket::LemmyContext; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +use std::ops::Deref; +use url::Url; + +#[skip_serializing_none] +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Note { + pub(crate) r#type: NoteType, + pub(crate) id: Url, + pub(crate) attributed_to: ObjectId, + /// Indicates that the object is publicly readable. Unlike [`Post.to`], this one doesn't contain + /// the community ID, as it would be incompatible with Pleroma (and we can get the community from + /// the post in [`in_reply_to`]). + pub(crate) to: Vec, + pub(crate) content: String, + pub(crate) media_type: Option, + pub(crate) source: SourceCompat, + pub(crate) in_reply_to: ObjectId, + pub(crate) published: Option>, + pub(crate) updated: Option>, + #[serde(flatten)] + pub(crate) unparsed: Unparsed, +} + +/// Pleroma puts a raw string in the source, so we have to handle it here for deserialization to work +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +#[serde(untagged)] +pub(crate) enum SourceCompat { + Lemmy(Source), + Pleroma(String), +} + +impl Note { + pub(crate) fn id_unchecked(&self) -> &Url { + &self.id + } + pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> { + verify_domains_match(&self.id, expected_domain)?; + Ok(&self.id) + } + + pub(crate) async fn get_parents( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(ApubPost, Option), LemmyError> { + // Fetch parent comment chain in a box, otherwise it can cause a stack overflow. + let parent = Box::pin( + self + .in_reply_to + .dereference(context, request_counter) + .await?, + ); + match parent.deref() { + PostOrComment::Post(p) => { + // Workaround because I cant figure out how to get the post out of the box (and we dont + // want to stackoverflow in a deep comment hierarchy). + let post_id = p.id; + let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; + Ok((post.into(), None)) + } + PostOrComment::Comment(c) => { + let post_id = c.post_id; + let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; + Ok((post.into(), Some(c.id))) + } + } + } + + pub(crate) async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + let (post, _parent_comment_id) = self.get_parents(context, request_counter).await?; + let community_id = post.community_id; + let community: ApubCommunity = blocking(context.pool(), move |conn| { + Community::read(conn, community_id) + }) + .await?? + .into(); + + if post.locked { + return Err(anyhow!("Post is locked").into()); + } + verify_domains_match(self.attributed_to.inner(), &self.id)?; + verify_person_in_community(&self.attributed_to, &community, context, request_counter).await?; + verify_is_public(&self.to)?; + Ok(()) + } +} diff --git a/crates/apub/src/protocol/objects/page.rs b/crates/apub/src/protocol/objects/page.rs new file mode 100644 index 000000000..7887f19c1 --- /dev/null +++ b/crates/apub/src/protocol/objects/page.rs @@ -0,0 +1,97 @@ +use crate::{ + activities::{verify_is_public, verify_person_in_community}, + fetcher::object_id::ObjectId, + objects::{community::ApubCommunity, person::ApubPerson, post::ApubPost}, + protocol::{ImageObject, Source}, +}; +use activitystreams::{object::kind::PageType, unparsed::Unparsed}; +use anyhow::anyhow; +use chrono::{DateTime, FixedOffset}; +use lemmy_apub_lib::{values::MediaTypeHtml, verify::verify_domains_match}; +use lemmy_utils::{utils::check_slurs, LemmyError}; +use lemmy_websocket::LemmyContext; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +use url::Url; + +#[skip_serializing_none] +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Page { + pub(crate) r#type: PageType, + pub(crate) id: Url, + pub(crate) attributed_to: ObjectId, + pub(crate) to: Vec, + pub(crate) name: String, + pub(crate) content: Option, + pub(crate) media_type: Option, + pub(crate) source: Option, + pub(crate) url: Option, + pub(crate) image: Option, + pub(crate) comments_enabled: Option, + pub(crate) sensitive: Option, + pub(crate) stickied: Option, + pub(crate) published: Option>, + pub(crate) updated: Option>, + #[serde(flatten)] + pub(crate) unparsed: Unparsed, +} + +impl Page { + pub(crate) fn id_unchecked(&self) -> &Url { + &self.id + } + pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> { + verify_domains_match(&self.id, expected_domain)?; + Ok(&self.id) + } + + /// 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. + /// + /// Both stickied and locked need to be false on a newly created post (verified in [[CreatePost]]. + pub(crate) async fn is_mod_action(&self, context: &LemmyContext) -> Result { + let old_post = ObjectId::::new(self.id.clone()) + .dereference_local(context) + .await; + + 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> { + let community = self.extract_community(context, request_counter).await?; + + check_slurs(&self.name, &context.settings().slur_regex())?; + verify_domains_match(self.attributed_to.inner(), &self.id.clone())?; + verify_person_in_community(&self.attributed_to, &community, context, request_counter).await?; + verify_is_public(&self.to.clone())?; + Ok(()) + } + + pub(crate) async fn extract_community( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result { + let mut to_iter = self.to.iter(); + loop { + if let Some(cid) = to_iter.next() { + let cid = ObjectId::new(cid.clone()); + if let Ok(c) = cid.dereference(context, request_counter).await { + break Ok(c); + } + } else { + return Err(anyhow!("No community found in cc").into()); + } + } + } +} diff --git a/crates/apub/src/protocol/objects/person.rs b/crates/apub/src/protocol/objects/person.rs new file mode 100644 index 000000000..2aecf945e --- /dev/null +++ b/crates/apub/src/protocol/objects/person.rs @@ -0,0 +1,41 @@ +use crate::protocol::{ImageObject, Source}; +use activitystreams::{actor::Endpoints, unparsed::Unparsed, url::Url}; +use chrono::{DateTime, FixedOffset}; +use lemmy_apub_lib::signatures::PublicKey; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)] +pub enum UserTypes { + Person, + Service, +} + +#[skip_serializing_none] +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Person { + #[serde(rename = "type")] + pub(crate) kind: UserTypes, + pub(crate) id: Url, + /// username, set at account creation and can never be changed + pub(crate) preferred_username: String, + /// displayname (can be changed at any time) + pub(crate) name: Option, + pub(crate) summary: Option, + pub(crate) source: Option, + /// user avatar + pub(crate) icon: Option, + /// user banner + pub(crate) image: Option, + pub(crate) matrix_user_id: Option, + pub(crate) inbox: Url, + /// mandatory field in activitypub, currently empty in lemmy + pub(crate) outbox: Url, + pub(crate) endpoints: Endpoints, + pub(crate) public_key: PublicKey, + pub(crate) published: Option>, + pub(crate) updated: Option>, + #[serde(flatten)] + pub(crate) unparsed: Unparsed, +} diff --git a/crates/apub/src/objects/tombstone.rs b/crates/apub/src/protocol/objects/tombstone.rs similarity index 69% rename from crates/apub/src/objects/tombstone.rs rename to crates/apub/src/protocol/objects/tombstone.rs index fc78c9682..2746c5ae6 100644 --- a/crates/apub/src/objects/tombstone.rs +++ b/crates/apub/src/protocol/objects/tombstone.rs @@ -1,10 +1,5 @@ -use crate::context::lemmy_context; -use activitystreams::{ - base::AnyBase, - chrono::{DateTime, FixedOffset, NaiveDateTime}, - object::kind::TombstoneType, - primitives::OneOrMany, -}; +use activitystreams::object::kind::TombstoneType; +use chrono::{DateTime, FixedOffset, NaiveDateTime}; use lemmy_utils::utils::convert_datetime; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; @@ -13,8 +8,6 @@ use serde_with::skip_serializing_none; #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Tombstone { - #[serde(rename = "@context")] - context: OneOrMany, #[serde(rename = "type")] kind: TombstoneType, former_type: String, @@ -24,7 +17,6 @@ pub struct Tombstone { impl Tombstone { pub fn new(former_type: T, updated_time: NaiveDateTime) -> Tombstone { Tombstone { - context: lemmy_context(), kind: TombstoneType::Tombstone, former_type: former_type.to_string(), deleted: convert_datetime(updated_time), diff --git a/crates/apub_lib/src/traits.rs b/crates/apub_lib/src/traits.rs index 2240946d4..8d819a27b 100644 --- a/crates/apub_lib/src/traits.rs +++ b/crates/apub_lib/src/traits.rs @@ -8,7 +8,6 @@ use url::Url; pub trait ActivityFields { fn id_unchecked(&self) -> &Url; fn actor(&self) -> &Url; - fn cc(&self) -> Vec; } #[async_trait::async_trait(?Send)] diff --git a/crates/apub_lib/src/values.rs b/crates/apub_lib/src/values.rs index 2c9f65ffe..bd014c12b 100644 --- a/crates/apub_lib/src/values.rs +++ b/crates/apub_lib/src/values.rs @@ -33,15 +33,6 @@ use serde::{Deserialize, Serialize}; -/// The identifier used to address activities to the public. -/// -/// -#[derive(Debug, Clone, Deserialize, Serialize)] -pub enum PublicUrl { - #[serde(rename = "https://www.w3.org/ns/activitystreams#Public")] - Public, -} - /// Media type for markdown text. /// /// diff --git a/crates/apub_lib_derive/src/lib.rs b/crates/apub_lib_derive/src/lib.rs index 44185d14e..1f8d12a7d 100644 --- a/crates/apub_lib_derive/src/lib.rs +++ b/crates/apub_lib_derive/src/lib.rs @@ -145,36 +145,18 @@ pub fn derive_activity_fields(input: proc_macro::TokenStream) -> proc_macro::Tok let impl_actor = variants .iter() .map(|v| generate_match_arm(&name, v, "e! {a.actor()})); - let impl_cc = variants - .iter() - .map(|v| generate_match_arm(&name, v, "e! {a.cc()})); quote! { impl #impl_generics lemmy_apub_lib::traits::ActivityFields for #name #ty_generics #where_clause { fn id_unchecked(&self) -> &url::Url { match self { #(#impl_id)* } } fn actor(&self) -> &url::Url { match self { #(#impl_actor)* } } - fn cc(&self) -> Vec { match self { #(#impl_cc)* } } } } } - Data::Struct(s) => { - // check if the struct has a field "cc", and generate impl for cc() function depending on that - let has_cc = if let syn::Fields::Named(n) = s.fields { - n.named - .iter() - .any(|i| format!("{}", i.ident.as_ref().unwrap()) == "cc") - } else { - unimplemented!() - }; - let cc_impl = if has_cc { - quote! {self.cc.iter().map(|i| i.clone().into()).collect()} - } else { - quote! {vec![]} - }; + Data::Struct(_) => { quote! { impl #impl_generics lemmy_apub_lib::traits::ActivityFields for #name #ty_generics #where_clause { fn id_unchecked(&self) -> &url::Url { &self.id } fn actor(&self) -> &url::Url { &self.actor.inner() } - fn cc(&self) -> Vec { #cc_impl } } } } diff --git a/crates/db_schema/src/impls/activity.rs b/crates/db_schema/src/impls/activity.rs index 4f1cfd2dd..5efb3b234 100644 --- a/crates/db_schema/src/impls/activity.rs +++ b/crates/db_schema/src/impls/activity.rs @@ -1,7 +1,6 @@ use crate::{newtypes::DbUrl, source::activity::*, traits::Crud}; -use diesel::{dsl::*, result::Error, sql_types::Text, *}; +use diesel::{dsl::*, result::Error, *}; use serde::Serialize; -use serde_json::Value; use std::{ fmt::Debug, io::{Error as IoError, ErrorKind}, @@ -75,26 +74,6 @@ impl Activity { use crate::schema::activity::dsl::*; diesel::delete(activity.filter(published.lt(now - 6.months()))).execute(conn) } - - pub fn read_community_outbox( - conn: &PgConnection, - community_actor_id: &DbUrl, - ) -> Result, Error> { - use crate::schema::activity::dsl::*; - let res: Vec = activity - .select(data) - .filter( - sql("activity.data ->> 'type' = 'Announce'") - .sql(" AND activity.data -> 'object' ->> 'type' = 'Create'") - .sql(" AND activity.data -> 'object' -> 'object' ->> 'type' = 'Page'") - .sql(" AND activity.data ->> 'actor' = ") - .bind::(community_actor_id) - .sql(" ORDER BY activity.published DESC"), - ) - .limit(20) - .get_results(conn)?; - Ok(res) - } } #[cfg(test)] diff --git a/docker/federation/docker-compose.yml b/docker/federation/docker-compose.yml index 3a8447c1a..bcd4f914c 100644 --- a/docker/federation/docker-compose.yml +++ b/docker/federation/docker-compose.yml @@ -28,7 +28,7 @@ services: - ./volumes/pictrs_alpha:/mnt lemmy-alpha-ui: - image: dessalines/lemmy-ui:dev + image: dessalines/lemmy-ui:0.13.3 environment: - LEMMY_INTERNAL_HOST=lemmy-alpha:8541 - LEMMY_EXTERNAL_HOST=localhost:8541 @@ -57,7 +57,7 @@ services: - ./volumes/postgres_alpha:/var/lib/postgresql/data lemmy-beta-ui: - image: dessalines/lemmy-ui:dev + image: dessalines/lemmy-ui:0.13.3 environment: - LEMMY_INTERNAL_HOST=lemmy-beta:8551 - LEMMY_EXTERNAL_HOST=localhost:8551 diff --git a/src/code_migrations.rs b/src/code_migrations.rs index 8910733a5..2feefbdfb 100644 --- a/src/code_migrations.rs +++ b/src/code_migrations.rs @@ -4,9 +4,9 @@ use diesel::{ *, }; use lemmy_apub::{ - generate_apub_endpoint, generate_followers_url, generate_inbox_url, + generate_local_apub_endpoint, generate_shared_inbox_url, EndpointType, }; @@ -58,7 +58,7 @@ fn user_updates_2020_04_02( let form = PersonForm { name: cperson.name.to_owned(), - actor_id: Some(generate_apub_endpoint( + actor_id: Some(generate_local_apub_endpoint( EndpointType::Person, &cperson.name, protocol_and_hostname, @@ -93,7 +93,7 @@ fn community_updates_2020_04_02( for ccommunity in &incorrect_communities { let keypair = generate_actor_keypair()?; - let community_actor_id = generate_apub_endpoint( + let community_actor_id = generate_local_apub_endpoint( EndpointType::Community, &ccommunity.name, protocol_and_hostname, @@ -143,7 +143,7 @@ fn post_updates_2020_04_03( .load::(conn)?; for cpost in &incorrect_posts { - let apub_id = generate_apub_endpoint( + let apub_id = generate_local_apub_endpoint( EndpointType::Post, &cpost.id.to_string(), protocol_and_hostname, @@ -171,7 +171,7 @@ fn comment_updates_2020_04_03( .load::(conn)?; for ccomment in &incorrect_comments { - let apub_id = generate_apub_endpoint( + let apub_id = generate_local_apub_endpoint( EndpointType::Comment, &ccomment.id.to_string(), protocol_and_hostname, @@ -199,7 +199,7 @@ fn private_message_updates_2020_05_05( .load::(conn)?; for cpm in &incorrect_pms { - let apub_id = generate_apub_endpoint( + let apub_id = generate_local_apub_endpoint( EndpointType::PrivateMessage, &cpm.id.to_string(), protocol_and_hostname,