Move object and collection structs to protocol folder

This commit is contained in:
Felix Ableitner 2021-10-28 23:17:59 +02:00
parent 358ef99ea2
commit 5ff044346f
31 changed files with 605 additions and 508 deletions

View file

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
set -e set -e
cargo +nightly fmt cargo +nightly fmt -- --check
cargo +nightly clippy --workspace --tests --all-targets --all-features -- \ cargo +nightly clippy --workspace --tests --all-targets --all-features -- \
-D warnings -D deprecated -D clippy::perf -D clippy::complexity -D clippy::dbg_macro -D warnings -D deprecated -D clippy::perf -D clippy::complexity -D clippy::dbg_macro

View file

@ -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::{ use crate::{
activities::{ activities::{
check_community_deleted_or_removed, check_community_deleted_or_removed,
@ -13,27 +30,9 @@ use crate::{
CreateOrUpdateType, CreateOrUpdateType,
}, },
fetcher::object_id::ObjectId, fetcher::object_id::ObjectId,
objects::{ objects::{comment::ApubComment, community::ApubCommunity, person::ApubPerson},
comment::{ApubComment, Note}, protocol::objects::note::Note,
community::ApubCommunity,
person::ApubPerson,
},
}; };
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)] #[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@ -153,10 +152,12 @@ impl GetCommunity for CreateOrUpdateComment {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use crate::objects::tests::file_to_json_object;
use serial_test::serial; use serial_test::serial;
use crate::objects::tests::file_to_json_object;
use super::*;
#[actix_rt::test] #[actix_rt::test]
#[serial] #[serial]
async fn test_parse_pleroma_create_comment() { async fn test_parse_pleroma_create_comment() {

View file

@ -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::{ use crate::{
activities::{ activities::{
community::{ community::{
@ -11,25 +27,9 @@ use crate::{
verify_person_in_community, verify_person_in_community,
}, },
fetcher::object_id::ObjectId, fetcher::object_id::ObjectId,
objects::{ objects::{community::ApubCommunity, person::ApubPerson},
community::{ApubCommunity, Group}, protocol::objects::group::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},
};
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 /// This activity is received from a remote community mod, and updates the description or other
/// fields of a local community. /// fields of a local community.

View file

@ -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::{ use crate::{
activities::{ activities::{
check_community_deleted_or_removed, check_community_deleted_or_removed,
@ -13,25 +28,9 @@ use crate::{
CreateOrUpdateType, CreateOrUpdateType,
}, },
fetcher::object_id::ObjectId, fetcher::object_id::ObjectId,
objects::{ objects::{community::ApubCommunity, person::ApubPerson, post::ApubPost},
community::ApubCommunity, protocol::objects::page::Page,
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},
};
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)] #[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]

View file

@ -7,10 +7,8 @@ use crate::{
CreateOrUpdateType, CreateOrUpdateType,
}, },
fetcher::object_id::ObjectId, fetcher::object_id::ObjectId,
objects::{ objects::{person::ApubPerson, private_message::ApubPrivateMessage},
person::ApubPerson, protocol::objects::chat_message::ChatMessage,
private_message::{ApubPrivateMessage, ChatMessage},
},
}; };
use activitystreams::unparsed::Unparsed; use activitystreams::unparsed::Unparsed;
use lemmy_api_common::blocking; use lemmy_api_common::blocking;

View file

@ -3,6 +3,7 @@ use crate::{
fetcher::object_id::ObjectId, fetcher::object_id::ObjectId,
generate_moderators_url, generate_moderators_url,
objects::person::ApubPerson, objects::person::ApubPerson,
protocol::collections::group_moderators::GroupModerators,
}; };
use activitystreams::{chrono::NaiveDateTime, collection::kind::OrderedCollectionType}; use activitystreams::{chrono::NaiveDateTime, collection::kind::OrderedCollectionType};
use lemmy_api_common::blocking; use lemmy_api_common::blocking;
@ -13,17 +14,8 @@ use lemmy_db_schema::{
}; };
use lemmy_db_views_actor::community_moderator_view::CommunityModeratorView; use lemmy_db_views_actor::community_moderator_view::CommunityModeratorView;
use lemmy_utils::LemmyError; use lemmy_utils::LemmyError;
use serde::{Deserialize, Serialize};
use url::Url; use url::Url;
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GroupModerators {
r#type: OrderedCollectionType,
id: Url,
ordered_items: Vec<ObjectId<ApubPerson>>,
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) struct ApubCommunityModerators(pub(crate) Vec<CommunityModeratorView>); pub(crate) struct ApubCommunityModerators(pub(crate) Vec<CommunityModeratorView>);

View file

@ -3,6 +3,7 @@ use crate::{
collections::CommunityContext, collections::CommunityContext,
generate_outbox_url, generate_outbox_url,
objects::{person::ApubPerson, post::ApubPost}, objects::{person::ApubPerson, post::ApubPost},
protocol::collections::group_outbox::GroupOutbox,
}; };
use activitystreams::collection::kind::OrderedCollectionType; use activitystreams::collection::kind::OrderedCollectionType;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
@ -17,18 +18,8 @@ use lemmy_db_schema::{
traits::Crud, traits::Crud,
}; };
use lemmy_utils::LemmyError; use lemmy_utils::LemmyError;
use serde::{Deserialize, Serialize};
use url::Url; 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<CreateOrUpdatePost>,
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) struct ApubCommunityOutbox(Vec<ApubPost>); pub(crate) struct ApubCommunityOutbox(Vec<ApubPost>);

View file

@ -1,10 +1,9 @@
use crate::objects::community::ApubCommunity;
use lemmy_websocket::LemmyContext; use lemmy_websocket::LemmyContext;
pub(crate) mod community_followers; use crate::objects::community::ApubCommunity;
pub(crate) mod community_moderators; pub(crate) mod community_moderators;
pub(crate) mod community_outbox; 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. /// Put community in the data, so we dont have to read it again from the database.
pub(crate) struct CommunityContext(pub ApubCommunity, pub LemmyContext); pub(crate) struct CommunityContext(pub ApubCommunity, pub LemmyContext);

View file

@ -1,14 +1,16 @@
use crate::objects::{
comment::{ApubComment, Note},
post::{ApubPost, Page},
};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use serde::Deserialize;
use url::Url;
use lemmy_apub_lib::traits::ApubObject; use lemmy_apub_lib::traits::ApubObject;
use lemmy_db_schema::source::{comment::CommentForm, post::PostForm}; use lemmy_db_schema::source::{comment::CommentForm, post::PostForm};
use lemmy_utils::LemmyError; use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext; 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)] #[derive(Clone, Debug)]
pub enum PostOrComment { pub enum PostOrComment {

View file

@ -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 anyhow::anyhow;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use itertools::Itertools; use itertools::Itertools;
use serde::Deserialize;
use url::Url;
use lemmy_api_common::blocking; use lemmy_api_common::blocking;
use lemmy_apub_lib::{ use lemmy_apub_lib::{
traits::ApubObject, traits::ApubObject,
@ -21,8 +15,17 @@ use lemmy_db_schema::{
}; };
use lemmy_utils::LemmyError; use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext; 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. /// Attempt to parse the query as URL, and fetch an ActivityPub object from it.
/// ///

View file

@ -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::{ use crate::{
activities::{ activities::{
community::announce::{AnnouncableActivities, AnnounceActivity, GetCommunity}, community::announce::{AnnouncableActivities, AnnounceActivity, GetCommunity},
@ -6,7 +19,6 @@ use crate::{
verify_person_in_community, verify_person_in_community,
}, },
collections::{ collections::{
community_followers::CommunityFollowers,
community_moderators::ApubCommunityModerators, community_moderators::ApubCommunityModerators,
community_outbox::ApubCommunityOutbox, community_outbox::ApubCommunityOutbox,
CommunityContext, CommunityContext,
@ -21,18 +33,8 @@ use crate::{
receive_activity, receive_activity,
}, },
objects::community::ApubCommunity, 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)] #[derive(Deserialize)]
pub(crate) struct CommunityQuery { pub(crate) struct CommunityQuery {

View file

@ -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::{ use crate::{
activities::{ activities::{
community::announce::{AnnouncableActivities, AnnounceActivity}, community::announce::{AnnouncableActivities, AnnounceActivity},
@ -8,7 +18,6 @@ use crate::{
undo_delete::UndoDeletePrivateMessage, undo_delete::UndoDeletePrivateMessage,
}, },
}, },
collections::user_outbox::UserOutbox,
context::WithContext, context::WithContext,
http::{ http::{
create_apub_response, create_apub_response,
@ -17,15 +26,8 @@ use crate::{
receive_activity, receive_activity,
}, },
objects::person::ApubPerson, 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)] #[derive(Deserialize)]
pub struct PersonQuery { pub struct PersonQuery {

View file

@ -5,6 +5,7 @@ pub mod fetcher;
pub mod http; pub mod http;
pub mod migrations; pub mod migrations;
pub mod objects; pub mod objects;
pub(crate) mod protocol;
#[macro_use] #[macro_use]
extern crate lazy_static; extern crate lazy_static;

View file

@ -1,27 +1,17 @@
use crate::{ use std::ops::Deref;
activities::{verify_is_public, verify_person_in_community},
fetcher::object_id::ObjectId, use activitystreams::{object::kind::NoteType, public};
objects::{
community::ApubCommunity,
person::ApubPerson,
post::ApubPost,
tombstone::Tombstone,
Source,
},
PostOrComment,
};
use activitystreams::{object::kind::NoteType, public, unparsed::Unparsed};
use anyhow::anyhow; use anyhow::anyhow;
use chrono::{DateTime, FixedOffset, NaiveDateTime}; use chrono::NaiveDateTime;
use html2md::parse_html; use html2md::parse_html;
use url::Url;
use lemmy_api_common::blocking; use lemmy_api_common::blocking;
use lemmy_apub_lib::{ use lemmy_apub_lib::{
traits::ApubObject, traits::ApubObject,
values::{MediaTypeHtml, MediaTypeMarkdown}, values::{MediaTypeHtml, MediaTypeMarkdown},
verify::verify_domains_match,
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
newtypes::CommentId,
source::{ source::{
comment::{Comment, CommentForm}, comment::{Comment, CommentForm},
community::Community, community::Community,
@ -35,100 +25,19 @@ use lemmy_utils::{
LemmyError, LemmyError,
}; };
use lemmy_websocket::LemmyContext; use lemmy_websocket::LemmyContext;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use std::ops::Deref;
use url::Url;
#[skip_serializing_none] use crate::{
#[derive(Clone, Debug, Deserialize, Serialize)] activities::verify_person_in_community,
#[serde(rename_all = "camelCase")] fetcher::object_id::ObjectId,
pub struct Note { protocol::{
r#type: NoteType, objects::{
id: Url, note::{Note, SourceCompat},
pub(crate) attributed_to: ObjectId<ApubPerson>, tombstone::Tombstone,
/// 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 Source,
/// the post in [`in_reply_to`]). },
to: Vec<Url>, PostOrComment,
content: String, };
media_type: Option<MediaTypeHtml>,
source: SourceCompat,
in_reply_to: ObjectId<PostOrComment>,
published: Option<DateTime<FixedOffset>>,
updated: Option<DateTime<FixedOffset>>,
#[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<CommentId>), 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(())
}
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ApubComment(Comment); pub struct ApubComment(Comment);
@ -277,13 +186,16 @@ impl ApubObject for ApubComment {
#[cfg(test)] #[cfg(test)]
pub(crate) mod tests { pub(crate) mod tests {
use super::*; use assert_json_diff::assert_json_include;
use serial_test::serial;
use crate::objects::{ use crate::objects::{
community::ApubCommunity, community::ApubCommunity,
tests::{file_to_json_object, init_context}, 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( pub(crate) async fn prepare_comment_test(
url: &Url, url: &Url,

View file

@ -1,117 +1,40 @@
use crate::{ use std::ops::Deref;
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 activitystreams::{ use activitystreams::{
actor::{kind::GroupType, Endpoints}, actor::{kind::GroupType, Endpoints},
object::kind::ImageType, object::kind::ImageType,
unparsed::Unparsed,
}; };
use chrono::{DateTime, FixedOffset, NaiveDateTime}; use chrono::NaiveDateTime;
use itertools::Itertools; use itertools::Itertools;
use log::debug;
use url::Url;
use lemmy_api_common::blocking; use lemmy_api_common::blocking;
use lemmy_apub_lib::{ use lemmy_apub_lib::{
signatures::PublicKey,
traits::{ActorType, ApubObject}, traits::{ActorType, ApubObject},
values::MediaTypeMarkdown, 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_db_views_actor::community_follower_view::CommunityFollowerView;
use lemmy_utils::{ use lemmy_utils::{
settings::structs::Settings, settings::structs::Settings,
utils::{check_slurs, check_slurs_opt, convert_datetime, markdown_to_html}, utils::{convert_datetime, markdown_to_html},
LemmyError, LemmyError,
}; };
use lemmy_websocket::LemmyContext; 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] use crate::{
#[derive(Clone, Debug, Deserialize, Serialize)] check_is_apub_id_valid,
#[serde(rename_all = "camelCase")] collections::{community_moderators::ApubCommunityModerators, CommunityContext},
pub struct Group { fetcher::object_id::ObjectId,
#[serde(rename = "type")] generate_moderators_url,
kind: GroupType, generate_outbox_url,
pub(crate) id: Url, protocol::{
/// username, set at account creation and can never be changed objects::{group::Group, tombstone::Tombstone},
preferred_username: String, ImageObject,
/// title (can be changed at any time) Source,
name: String, },
summary: Option<String>, };
source: Option<Source>,
icon: Option<ImageObject>,
/// banner
image: Option<ImageObject>,
// lemmy extension
sensitive: Option<bool>,
// lemmy extension
pub(crate) moderators: Option<ObjectId<ApubCommunityModerators>>,
inbox: Url,
pub(crate) outbox: ObjectId<ApubCommunityOutbox>,
followers: Url,
endpoints: Endpoints<Url>,
public_key: PublicKey,
published: Option<DateTime<FixedOffset>>,
updated: Option<DateTime<FixedOffset>>,
#[serde(flatten)]
unparsed: Unparsed,
}
impl Group {
pub(crate) async fn from_apub_to_form(
group: &Group,
expected_domain: &Url,
settings: &Settings,
) -> Result<CommunityForm, LemmyError> {
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),
})
}
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ApubCommunity(Community); pub struct ApubCommunity(Community);
@ -300,12 +223,15 @@ impl ApubCommunity {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use crate::objects::tests::{file_to_json_object, init_context};
use assert_json_diff::assert_json_include; use assert_json_diff::assert_json_include;
use lemmy_db_schema::traits::Crud;
use serial_test::serial; 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] #[actix_rt::test]
#[serial] #[serial]
async fn test_parse_lemmy_community() { async fn test_parse_lemmy_community() {

View file

@ -1,32 +1,13 @@
use activitystreams::object::kind::ImageType; use crate::protocol::Source;
use html2md::parse_html; use html2md::parse_html;
use lemmy_apub_lib::values::MediaTypeMarkdown;
use serde::{Deserialize, Serialize};
use url::Url;
pub mod comment; pub mod comment;
pub mod community; pub mod community;
pub mod person; pub mod person;
pub mod post; pub mod post;
pub mod private_message; pub mod private_message;
pub mod tombstone;
#[derive(Clone, Debug, Deserialize, Serialize)] pub(crate) fn get_summary_from_string_or_source(
#[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(
raw: &Option<String>, raw: &Option<String>,
source: &Option<Source>, source: &Option<Source>,
) -> Option<String> { ) -> Option<String> {

View file

@ -1,7 +1,8 @@
use crate::{ use crate::{
check_is_apub_id_valid, check_is_apub_id_valid,
generate_outbox_url, 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 activitystreams::{actor::Endpoints, object::kind::ImageType, unparsed::Unparsed};
use chrono::{DateTime, FixedOffset, NaiveDateTime}; use chrono::{DateTime, FixedOffset, NaiveDateTime};

View file

@ -1,10 +1,8 @@
use crate::{ use crate::{
activities::{verify_is_public, verify_person_in_community}, activities::verify_person_in_community,
fetcher::object_id::ObjectId, fetcher::object_id::ObjectId,
objects::{ protocol::{
community::ApubCommunity, objects::{page::Page, tombstone::Tombstone},
person::ApubPerson,
tombstone::Tombstone,
ImageObject, ImageObject,
Source, Source,
}, },
@ -12,15 +10,12 @@ use crate::{
use activitystreams::{ use activitystreams::{
object::kind::{ImageType, PageType}, object::kind::{ImageType, PageType},
public, public,
unparsed::Unparsed,
}; };
use anyhow::anyhow; use chrono::NaiveDateTime;
use chrono::{DateTime, FixedOffset, NaiveDateTime};
use lemmy_api_common::blocking; use lemmy_api_common::blocking;
use lemmy_apub_lib::{ use lemmy_apub_lib::{
traits::ApubObject, traits::ApubObject,
values::{MediaTypeHtml, MediaTypeMarkdown}, values::{MediaTypeHtml, MediaTypeMarkdown},
verify::verify_domains_match,
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
self, self,
@ -33,97 +28,13 @@ use lemmy_db_schema::{
}; };
use lemmy_utils::{ use lemmy_utils::{
request::fetch_site_data, request::fetch_site_data,
utils::{check_slurs, convert_datetime, markdown_to_html, remove_slurs}, utils::{convert_datetime, markdown_to_html, remove_slurs},
LemmyError, LemmyError,
}; };
use lemmy_websocket::LemmyContext; use lemmy_websocket::LemmyContext;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use std::ops::Deref; use std::ops::Deref;
use url::Url; 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<ApubPerson>,
to: Vec<Url>,
name: String,
content: Option<String>,
media_type: Option<MediaTypeHtml>,
source: Option<Source>,
url: Option<Url>,
image: Option<ImageObject>,
pub(crate) comments_enabled: Option<bool>,
sensitive: Option<bool>,
pub(crate) stickied: Option<bool>,
published: Option<DateTime<FixedOffset>>,
updated: Option<DateTime<FixedOffset>>,
#[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<bool, LemmyError> {
let old_post = ObjectId::<ApubPost>::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<ApubCommunity, LemmyError> {
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)] #[derive(Clone, Debug)]
pub struct ApubPost(Post); pub struct ApubPost(Post);
@ -283,6 +194,8 @@ mod tests {
use super::*; use super::*;
use crate::objects::{ use crate::objects::{
community::ApubCommunity, community::ApubCommunity,
person::ApubPerson,
post::ApubPost,
tests::{file_to_json_object, init_context}, tests::{file_to_json_object, init_context},
}; };
use assert_json_diff::assert_json_include; use assert_json_diff::assert_json_include;

View file

@ -1,16 +1,16 @@
use crate::{ use crate::{
fetcher::object_id::ObjectId, fetcher::object_id::ObjectId,
objects::{person::ApubPerson, Source}, protocol::{
objects::chat_message::{ChatMessage, ChatMessageType},
Source,
},
}; };
use activitystreams::unparsed::Unparsed; use chrono::NaiveDateTime;
use anyhow::anyhow;
use chrono::{DateTime, FixedOffset, NaiveDateTime};
use html2md::parse_html; use html2md::parse_html;
use lemmy_api_common::blocking; use lemmy_api_common::blocking;
use lemmy_apub_lib::{ use lemmy_apub_lib::{
traits::ApubObject, traits::ApubObject,
values::{MediaTypeHtml, MediaTypeMarkdown}, values::{MediaTypeHtml, MediaTypeMarkdown},
verify::verify_domains_match,
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{ source::{
@ -21,60 +21,9 @@ use lemmy_db_schema::{
}; };
use lemmy_utils::{utils::convert_datetime, LemmyError}; use lemmy_utils::{utils::convert_datetime, LemmyError};
use lemmy_websocket::LemmyContext; use lemmy_websocket::LemmyContext;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use std::ops::Deref; use std::ops::Deref;
use url::Url; 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<ApubPerson>,
to: [ObjectId<ApubPerson>; 1],
content: String,
media_type: Option<MediaTypeHtml>,
source: Option<Source>,
published: Option<DateTime<FixedOffset>>,
updated: Option<DateTime<FixedOffset>>,
#[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)] #[derive(Clone, Debug)]
pub struct ApubPrivateMessage(PrivateMessage); pub struct ApubPrivateMessage(PrivateMessage);
@ -189,7 +138,10 @@ impl ApubObject for ApubPrivateMessage {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; 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 assert_json_diff::assert_json_include;
use serial_test::serial; use serial_test::serial;

View file

@ -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<ObjectId<ApubPerson>>,
}

View file

@ -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<CreateOrUpdatePost>,
}

View file

@ -0,0 +1,4 @@
pub(crate) mod group_followers;
pub(crate) mod group_moderators;
pub(crate) mod group_outbox;
pub(crate) mod person_outbox;

View file

@ -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,
}

View file

@ -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<ApubPerson>,
pub(crate) to: [ObjectId<ApubPerson>; 1],
pub(crate) content: String,
pub(crate) media_type: Option<MediaTypeHtml>,
pub(crate) source: Option<Source>,
pub(crate) published: Option<DateTime<FixedOffset>>,
pub(crate) updated: Option<DateTime<FixedOffset>>,
#[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(())
}
}

View file

@ -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<String>,
pub(crate) source: Option<Source>,
pub(crate) icon: Option<ImageObject>,
/// banner
pub(crate) image: Option<ImageObject>,
// lemmy extension
pub(crate) sensitive: Option<bool>,
// lemmy extension
pub(crate) moderators: Option<ObjectId<ApubCommunityModerators>>,
pub(crate) inbox: Url,
pub(crate) outbox: ObjectId<ApubCommunityOutbox>,
pub(crate) followers: Url,
pub(crate) endpoints: Endpoints<Url>,
pub(crate) public_key: PublicKey,
pub(crate) published: Option<DateTime<FixedOffset>>,
pub(crate) updated: Option<DateTime<FixedOffset>>,
#[serde(flatten)]
pub(crate) unparsed: Unparsed,
}
impl Group {
pub(crate) async fn from_apub_to_form(
group: &Group,
expected_domain: &Url,
settings: &Settings,
) -> Result<CommunityForm, LemmyError> {
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),
})
}
}

View file

@ -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;

View file

@ -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<ApubPerson>,
/// 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<Url>,
pub(crate) content: String,
pub(crate) media_type: Option<MediaTypeHtml>,
pub(crate) source: SourceCompat,
pub(crate) in_reply_to: ObjectId<PostOrComment>,
pub(crate) published: Option<DateTime<FixedOffset>>,
pub(crate) updated: Option<DateTime<FixedOffset>>,
#[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<CommentId>), 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(())
}
}

View file

@ -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<ApubPerson>,
pub(crate) to: Vec<Url>,
pub(crate) name: String,
pub(crate) content: Option<String>,
pub(crate) media_type: Option<MediaTypeHtml>,
pub(crate) source: Option<Source>,
pub(crate) url: Option<Url>,
pub(crate) image: Option<ImageObject>,
pub(crate) comments_enabled: Option<bool>,
pub(crate) sensitive: Option<bool>,
pub(crate) stickied: Option<bool>,
pub(crate) published: Option<DateTime<FixedOffset>>,
pub(crate) updated: Option<DateTime<FixedOffset>>,
#[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<bool, LemmyError> {
let old_post = ObjectId::<ApubPost>::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<ApubCommunity, LemmyError> {
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());
}
}
}
}