diff --git a/.cargo-husky/hooks/pre-commit b/.cargo-husky/hooks/pre-commit index 4b50f2a5b..1c2858d4e 100755 --- a/.cargo-husky/hooks/pre-commit +++ b/.cargo-husky/hooks/pre-commit @@ -1,7 +1,7 @@ #!/bin/bash set -e -cargo +nightly fmt +cargo +nightly fmt -- --check cargo +nightly clippy --workspace --tests --all-targets --all-features -- \ -D warnings -D deprecated -D clippy::perf -D clippy::complexity -D clippy::dbg_macro diff --git a/crates/apub/src/activities/comment/create_or_update.rs b/crates/apub/src/activities/comment/create_or_update.rs index 18661cbfe..d53801aa2 100644 --- a/crates/apub/src/activities/comment/create_or_update.rs +++ b/crates/apub/src/activities/comment/create_or_update.rs @@ -1,3 +1,20 @@ +use activitystreams::{link::Mention, public, unparsed::Unparsed}; +use serde::{Deserialize, Serialize}; +use url::Url; + +use lemmy_api_common::{blocking, check_post_deleted_or_removed}; +use lemmy_apub_lib::{ + data::Data, + traits::{ActivityFields, ActivityHandler, ActorType, ApubObject}, + verify::verify_domains_match, +}; +use lemmy_db_schema::{ + source::{community::Community, post::Post}, + traits::Crud, +}; +use lemmy_utils::LemmyError; +use lemmy_websocket::{send::send_comment_ws_message, LemmyContext, UserOperationCrud}; + use crate::{ activities::{ check_community_deleted_or_removed, @@ -13,27 +30,9 @@ use crate::{ CreateOrUpdateType, }, fetcher::object_id::ObjectId, - objects::{ - comment::{ApubComment, Note}, - community::ApubCommunity, - person::ApubPerson, - }, + objects::{comment::ApubComment, community::ApubCommunity, person::ApubPerson}, + protocol::objects::note::Note, }; -use activitystreams::{link::Mention, public, unparsed::Unparsed}; -use lemmy_api_common::{blocking, check_post_deleted_or_removed}; -use lemmy_apub_lib::{ - data::Data, - traits::{ActivityFields, ActivityHandler, ActorType, ApubObject}, - verify::verify_domains_match, -}; -use lemmy_db_schema::{ - source::{community::Community, post::Post}, - traits::Crud, -}; -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")] @@ -153,10 +152,12 @@ impl GetCommunity for CreateOrUpdateComment { #[cfg(test)] mod tests { - use super::*; - use crate::objects::tests::file_to_json_object; use serial_test::serial; + use crate::objects::tests::file_to_json_object; + + use super::*; + #[actix_rt::test] #[serial] async fn test_parse_pleroma_create_comment() { diff --git a/crates/apub/src/activities/community/update.rs b/crates/apub/src/activities/community/update.rs index 78fce3248..df34ca289 100644 --- a/crates/apub/src/activities/community/update.rs +++ b/crates/apub/src/activities/community/update.rs @@ -1,3 +1,19 @@ +use activitystreams::{activity::kind::UpdateType, public, unparsed::Unparsed}; +use serde::{Deserialize, Serialize}; +use url::Url; + +use lemmy_api_common::blocking; +use lemmy_apub_lib::{ + data::Data, + traits::{ActivityFields, ActivityHandler, ActorType, ApubObject}, +}; +use lemmy_db_schema::{ + source::community::{Community, CommunityForm}, + traits::Crud, +}; +use lemmy_utils::LemmyError; +use lemmy_websocket::{send::send_community_ws_message, LemmyContext, UserOperationCrud}; + use crate::{ activities::{ community::{ @@ -11,25 +27,9 @@ use crate::{ verify_person_in_community, }, fetcher::object_id::ObjectId, - objects::{ - community::{ApubCommunity, Group}, - person::ApubPerson, - }, -}; -use activitystreams::{activity::kind::UpdateType, public, unparsed::Unparsed}; -use lemmy_api_common::blocking; -use lemmy_apub_lib::{ - data::Data, - traits::{ActivityFields, ActivityHandler, ActorType, ApubObject}, + objects::{community::ApubCommunity, person::ApubPerson}, + protocol::objects::group::Group, }; -use lemmy_db_schema::{ - source::community::{Community, CommunityForm}, - traits::Crud, -}; -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. diff --git a/crates/apub/src/activities/post/create_or_update.rs b/crates/apub/src/activities/post/create_or_update.rs index b5d9a202e..ee1bf19c8 100644 --- a/crates/apub/src/activities/post/create_or_update.rs +++ b/crates/apub/src/activities/post/create_or_update.rs @@ -1,3 +1,18 @@ +use activitystreams::{public, unparsed::Unparsed}; +use anyhow::anyhow; +use serde::{Deserialize, Serialize}; +use url::Url; + +use lemmy_api_common::blocking; +use lemmy_apub_lib::{ + data::Data, + traits::{ActivityFields, ActivityHandler, ActorType, ApubObject}, + 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 crate::{ activities::{ check_community_deleted_or_removed, @@ -13,25 +28,9 @@ use crate::{ CreateOrUpdateType, }, fetcher::object_id::ObjectId, - objects::{ - community::ApubCommunity, - person::ApubPerson, - post::{ApubPost, Page}, - }, -}; -use activitystreams::{public, unparsed::Unparsed}; -use anyhow::anyhow; -use lemmy_api_common::blocking; -use lemmy_apub_lib::{ - data::Data, - traits::{ActivityFields, ActivityHandler, ActorType, ApubObject}, - verify::{verify_domains_match, verify_urls_match}, + objects::{community::ApubCommunity, person::ApubPerson, post::ApubPost}, + protocol::objects::page::Page, }; -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")] 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 72a38e5e6..0067607ec 100644 --- a/crates/apub/src/activities/private_message/create_or_update.rs +++ b/crates/apub/src/activities/private_message/create_or_update.rs @@ -7,10 +7,8 @@ use crate::{ CreateOrUpdateType, }, fetcher::object_id::ObjectId, - objects::{ - person::ApubPerson, - private_message::{ApubPrivateMessage, ChatMessage}, - }, + objects::{person::ApubPerson, private_message::ApubPrivateMessage}, + protocol::objects::chat_message::ChatMessage, }; use activitystreams::unparsed::Unparsed; use lemmy_api_common::blocking; diff --git a/crates/apub/src/collections/community_moderators.rs b/crates/apub/src/collections/community_moderators.rs index d91b41c45..1a5a96d31 100644 --- a/crates/apub/src/collections/community_moderators.rs +++ b/crates/apub/src/collections/community_moderators.rs @@ -3,6 +3,7 @@ use crate::{ fetcher::object_id::ObjectId, generate_moderators_url, objects::person::ApubPerson, + protocol::collections::group_moderators::GroupModerators, }; use activitystreams::{chrono::NaiveDateTime, collection::kind::OrderedCollectionType}; use lemmy_api_common::blocking; @@ -13,17 +14,8 @@ use lemmy_db_schema::{ }; use lemmy_db_views_actor::community_moderator_view::CommunityModeratorView; use lemmy_utils::LemmyError; -use serde::{Deserialize, Serialize}; use url::Url; -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct GroupModerators { - r#type: OrderedCollectionType, - id: Url, - ordered_items: Vec>, -} - #[derive(Clone, Debug)] pub(crate) struct ApubCommunityModerators(pub(crate) Vec); diff --git a/crates/apub/src/collections/community_outbox.rs b/crates/apub/src/collections/community_outbox.rs index 98fb66448..cf5ad8b25 100644 --- a/crates/apub/src/collections/community_outbox.rs +++ b/crates/apub/src/collections/community_outbox.rs @@ -3,6 +3,7 @@ use crate::{ collections::CommunityContext, generate_outbox_url, objects::{person::ApubPerson, post::ApubPost}, + protocol::collections::group_outbox::GroupOutbox, }; use activitystreams::collection::kind::OrderedCollectionType; use chrono::NaiveDateTime; @@ -17,18 +18,8 @@ use lemmy_db_schema::{ traits::Crud, }; use lemmy_utils::LemmyError; -use serde::{Deserialize, Serialize}; use url::Url; -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct GroupOutbox { - r#type: OrderedCollectionType, - id: Url, - total_items: i32, - ordered_items: Vec, -} - #[derive(Clone, Debug)] pub(crate) struct ApubCommunityOutbox(Vec); diff --git a/crates/apub/src/collections/mod.rs b/crates/apub/src/collections/mod.rs index 378e14c04..a2e77d1bc 100644 --- a/crates/apub/src/collections/mod.rs +++ b/crates/apub/src/collections/mod.rs @@ -1,10 +1,9 @@ -use crate::objects::community::ApubCommunity; use lemmy_websocket::LemmyContext; -pub(crate) mod community_followers; +use crate::objects::community::ApubCommunity; + pub(crate) mod community_moderators; pub(crate) mod community_outbox; -pub(crate) mod user_outbox; /// Put community in the data, so we dont have to read it again from the database. pub(crate) struct CommunityContext(pub ApubCommunity, pub LemmyContext); diff --git a/crates/apub/src/fetcher/post_or_comment.rs b/crates/apub/src/fetcher/post_or_comment.rs index ac3911203..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 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 5c2a3e7bd..77a238288 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 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,17 @@ 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, Person}, + post::ApubPost, + }, + protocol::objects::{group::Group, note::Note, page::Page}, +}; /// 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 73f59d035..0a7cb664c 100644 --- a/crates/apub/src/http/community.rs +++ b/crates/apub/src/http/community.rs @@ -1,3 +1,16 @@ +use actix_web::{body::Body, web, web::Payload, HttpRequest, HttpResponse}; +use log::info; +use serde::{Deserialize, Serialize}; + +use lemmy_api_common::blocking; +use lemmy_apub_lib::{ + traits::{ActivityFields, ActivityHandler, ActorType, ApubObject}, + verify::verify_domains_match, +}; +use lemmy_db_schema::source::community::Community; +use lemmy_utils::LemmyError; +use lemmy_websocket::LemmyContext; + use crate::{ activities::{ community::announce::{AnnouncableActivities, AnnounceActivity, GetCommunity}, @@ -6,7 +19,6 @@ use crate::{ verify_person_in_community, }, collections::{ - community_followers::CommunityFollowers, community_moderators::ApubCommunityModerators, community_outbox::ApubCommunityOutbox, CommunityContext, @@ -21,18 +33,8 @@ use crate::{ receive_activity, }, objects::community::ApubCommunity, + protocol::collections::group_followers::CommunityFollowers, }; -use actix_web::{body::Body, web, web::Payload, HttpRequest, HttpResponse}; -use lemmy_api_common::blocking; -use lemmy_apub_lib::{ - traits::{ActivityFields, ActivityHandler, ActorType, ApubObject}, - verify::verify_domains_match, -}; -use lemmy_db_schema::source::community::Community; -use lemmy_utils::LemmyError; -use lemmy_websocket::LemmyContext; -use log::info; -use serde::{Deserialize, Serialize}; #[derive(Deserialize)] pub(crate) struct CommunityQuery { diff --git a/crates/apub/src/http/person.rs b/crates/apub/src/http/person.rs index a2d4fedd8..77bfd6940 100644 --- a/crates/apub/src/http/person.rs +++ b/crates/apub/src/http/person.rs @@ -1,3 +1,13 @@ +use actix_web::{body::Body, web, web::Payload, HttpRequest, HttpResponse}; +use log::info; +use serde::{Deserialize, Serialize}; + +use lemmy_api_common::blocking; +use lemmy_apub_lib::traits::{ActivityFields, ActivityHandler, ApubObject}; +use lemmy_db_schema::source::person::Person; +use lemmy_utils::LemmyError; +use lemmy_websocket::LemmyContext; + use crate::{ activities::{ community::announce::{AnnouncableActivities, AnnounceActivity}, @@ -8,7 +18,6 @@ use crate::{ undo_delete::UndoDeletePrivateMessage, }, }, - collections::user_outbox::UserOutbox, context::WithContext, http::{ create_apub_response, @@ -17,15 +26,8 @@ use crate::{ receive_activity, }, objects::person::ApubPerson, + protocol::collections::person_outbox::UserOutbox, }; -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_db_schema::source::person::Person; -use lemmy_utils::LemmyError; -use lemmy_websocket::LemmyContext; -use log::info; -use serde::{Deserialize, Serialize}; #[derive(Deserialize)] pub struct PersonQuery { diff --git a/crates/apub/src/lib.rs b/crates/apub/src/lib.rs index f60e04af8..d188274be 100644 --- a/crates/apub/src/lib.rs +++ b/crates/apub/src/lib.rs @@ -5,6 +5,7 @@ pub mod fetcher; pub mod http; pub mod migrations; pub mod objects; +pub(crate) mod protocol; #[macro_use] extern crate lazy_static; diff --git a/crates/apub/src/objects/comment.rs b/crates/apub/src/objects/comment.rs index 5698640da..cd491d87b 100644 --- a/crates/apub/src/objects/comment.rs +++ b/crates/apub/src/objects/comment.rs @@ -1,27 +1,17 @@ -use crate::{ - activities::{verify_is_public, verify_person_in_community}, - fetcher::object_id::ObjectId, - objects::{ - community::ApubCommunity, - person::ApubPerson, - post::ApubPost, - tombstone::Tombstone, - Source, - }, - PostOrComment, -}; -use activitystreams::{object::kind::NoteType, public, unparsed::Unparsed}; +use std::ops::Deref; + +use activitystreams::{object::kind::NoteType, public}; use anyhow::anyhow; -use chrono::{DateTime, FixedOffset, NaiveDateTime}; +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, @@ -35,100 +25,19 @@ 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 { - 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: 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(()) - } -} +use crate::{ + activities::verify_person_in_community, + fetcher::object_id::ObjectId, + protocol::{ + objects::{ + note::{Note, SourceCompat}, + tombstone::Tombstone, + }, + Source, + }, + PostOrComment, +}; #[derive(Clone, Debug)] pub struct ApubComment(Comment); @@ -277,13 +186,16 @@ impl ApubObject for ApubComment { #[cfg(test)] pub(crate) mod tests { - use super::*; + use assert_json_diff::assert_json_include; + use serial_test::serial; + use crate::objects::{ community::ApubCommunity, tests::{file_to_json_object, init_context}, }; - use assert_json_diff::assert_json_include; - use serial_test::serial; + + use super::*; + use crate::objects::{person::ApubPerson, post::ApubPost}; pub(crate) async fn prepare_comment_test( url: &Url, diff --git a/crates/apub/src/objects/community.rs b/crates/apub/src/objects/community.rs index c7d1dd3ed..0a4c06a4b 100644 --- a/crates/apub/src/objects/community.rs +++ b/crates/apub/src/objects/community.rs @@ -1,117 +1,40 @@ -use crate::{ - check_is_apub_id_valid, - collections::{ - community_moderators::ApubCommunityModerators, - community_outbox::ApubCommunityOutbox, - CommunityContext, - }, - fetcher::object_id::ObjectId, - generate_moderators_url, - generate_outbox_url, - objects::{get_summary_from_string_or_source, tombstone::Tombstone, ImageObject, Source}, -}; +use std::ops::Deref; + use activitystreams::{ actor::{kind::GroupType, Endpoints}, object::kind::ImageType, - unparsed::Unparsed, }; -use chrono::{DateTime, FixedOffset, NaiveDateTime}; +use chrono::NaiveDateTime; use itertools::Itertools; +use log::debug; +use url::Url; + 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_schema::{source::community::Community, 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}, + utils::{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 = "type")] - kind: GroupType, - pub(crate) 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) async fn from_apub_to_form( - group: &Group, - expected_domain: &Url, - settings: &Settings, - ) -> Result { - 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), - }) - } -} +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, + }, +}; #[derive(Clone, Debug)] pub struct ApubCommunity(Community); @@ -300,12 +223,15 @@ impl ApubCommunity { #[cfg(test)] 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; + use lemmy_db_schema::traits::Crud; + + use crate::objects::tests::{file_to_json_object, init_context}; + + use super::*; + #[actix_rt::test] #[serial] async fn test_parse_lemmy_community() { diff --git a/crates/apub/src/objects/mod.rs b/crates/apub/src/objects/mod.rs index d0cb1341d..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 { diff --git a/crates/apub/src/objects/person.rs b/crates/apub/src/objects/person.rs index 49ba7eb1b..1d914e32f 100644 --- a/crates/apub/src/objects/person.rs +++ b/crates/apub/src/objects/person.rs @@ -1,7 +1,8 @@ use crate::{ check_is_apub_id_valid, generate_outbox_url, - objects::{get_summary_from_string_or_source, ImageObject, Source}, + objects::get_summary_from_string_or_source, + protocol::{ImageObject, Source}, }; use activitystreams::{actor::Endpoints, object::kind::ImageType, unparsed::Unparsed}; use chrono::{DateTime, FixedOffset, NaiveDateTime}; diff --git a/crates/apub/src/objects/post.rs b/crates/apub/src/objects/post.rs index ee9aa7b88..3ade8de0f 100644 --- a/crates/apub/src/objects/post.rs +++ b/crates/apub/src/objects/post.rs @@ -1,10 +1,8 @@ use crate::{ - activities::{verify_is_public, verify_person_in_community}, + activities::verify_person_in_community, fetcher::object_id::ObjectId, - objects::{ - community::ApubCommunity, - person::ApubPerson, - tombstone::Tombstone, + protocol::{ + objects::{page::Page, tombstone::Tombstone}, ImageObject, Source, }, @@ -12,15 +10,12 @@ use crate::{ use activitystreams::{ object::kind::{ImageType, PageType}, public, - unparsed::Unparsed, }; -use anyhow::anyhow; -use chrono::{DateTime, FixedOffset, NaiveDateTime}; +use chrono::NaiveDateTime; use lemmy_api_common::blocking; use lemmy_apub_lib::{ traits::ApubObject, values::{MediaTypeHtml, MediaTypeMarkdown}, - verify::verify_domains_match, }; use lemmy_db_schema::{ self, @@ -33,97 +28,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 { - 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 = 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()); - } - } - } -} - #[derive(Clone, Debug)] pub struct ApubPost(Post); @@ -283,6 +194,8 @@ mod tests { use super::*; use crate::objects::{ community::ApubCommunity, + person::ApubPerson, + post::ApubPost, tests::{file_to_json_object, init_context}, }; use assert_json_diff::assert_json_include; diff --git a/crates/apub/src/objects/private_message.rs b/crates/apub/src/objects/private_message.rs index e971ebe33..ee0aec950 100644 --- a/crates/apub/src/objects/private_message.rs +++ b/crates/apub/src/objects/private_message.rs @@ -1,16 +1,16 @@ use crate::{ fetcher::object_id::ObjectId, - objects::{person::ApubPerson, Source}, + protocol::{ + objects::chat_message::{ChatMessage, ChatMessageType}, + Source, + }, }; -use activitystreams::unparsed::Unparsed; -use anyhow::anyhow; -use chrono::{DateTime, FixedOffset, NaiveDateTime}; +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::{ @@ -21,60 +21,9 @@ use lemmy_db_schema::{ }; use lemmy_utils::{utils::convert_datetime, 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 ChatMessage { - 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 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(()) - } -} - #[derive(Clone, Debug)] pub struct ApubPrivateMessage(PrivateMessage); @@ -189,7 +138,10 @@ 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; diff --git a/crates/apub/src/collections/community_followers.rs b/crates/apub/src/protocol/collections/group_followers.rs similarity index 100% rename from crates/apub/src/collections/community_followers.rs rename to crates/apub/src/protocol/collections/group_followers.rs 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..26da4b6fd --- /dev/null +++ b/crates/apub/src/protocol/collections/group_outbox.rs @@ -0,0 +1,13 @@ +use crate::activities::post::create_or_update::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..646abbeba --- /dev/null +++ b/crates/apub/src/protocol/collections/mod.rs @@ -0,0 +1,4 @@ +pub(crate) mod group_followers; +pub(crate) mod group_moderators; +pub(crate) mod group_outbox; +pub(crate) mod person_outbox; diff --git a/crates/apub/src/collections/user_outbox.rs b/crates/apub/src/protocol/collections/person_outbox.rs similarity index 100% rename from crates/apub/src/collections/user_outbox.rs rename to crates/apub/src/protocol/collections/person_outbox.rs diff --git a/crates/apub/src/protocol/mod.rs b/crates/apub/src/protocol/mod.rs new file mode 100644 index 000000000..f4ad9e234 --- /dev/null +++ b/crates/apub/src/protocol/mod.rs @@ -0,0 +1,23 @@ +use activitystreams::object::kind::ImageType; +use serde::{Deserialize, Serialize}; +use url::Url; + +use lemmy_apub_lib::values::MediaTypeMarkdown; + +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, +} 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..945878904 --- /dev/null +++ b/crates/apub/src/protocol/objects/group.rs @@ -0,0 +1,95 @@ +use crate::{ + 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 { + 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..3e133831e --- /dev/null +++ b/crates/apub/src/protocol/objects/mod.rs @@ -0,0 +1,5 @@ +pub(crate) mod chat_message; +pub(crate) mod group; +pub(crate) mod note; +pub(crate) mod page; +pub(crate) mod tombstone; 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/objects/tombstone.rs b/crates/apub/src/protocol/objects/tombstone.rs similarity index 100% rename from crates/apub/src/objects/tombstone.rs rename to crates/apub/src/protocol/objects/tombstone.rs