#!/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
+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,
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")]
#[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() {
+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::{
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.
+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,
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")]
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;
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;
};
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<ObjectId<ApubPerson>>,
-}
-
#[derive(Clone, Debug)]
pub(crate) struct ApubCommunityModerators(pub(crate) Vec<CommunityModeratorView>);
collections::CommunityContext,
generate_outbox_url,
objects::{person::ApubPerson, post::ApubPost},
+ protocol::collections::group_outbox::GroupOutbox,
};
use activitystreams::collection::kind::OrderedCollectionType;
use chrono::NaiveDateTime;
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<CreateOrUpdatePost>,
-}
-
#[derive(Clone, Debug)]
pub(crate) struct ApubCommunityOutbox(Vec<ApubPost>);
-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);
-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 {
-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,
};
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.
///
+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},
verify_person_in_community,
},
collections::{
- community_followers::CommunityFollowers,
community_moderators::ApubCommunityModerators,
community_outbox::ApubCommunityOutbox,
CommunityContext,
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 {
+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},
undo_delete::UndoDeletePrivateMessage,
},
},
- collections::user_outbox::UserOutbox,
context::WithContext,
http::{
create_apub_response,
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 {
pub mod http;
pub mod migrations;
pub mod objects;
+pub(crate) mod protocol;
#[macro_use]
extern crate lazy_static;
-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,
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<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`]).
- to: Vec<Url>,
- 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(())
- }
-}
+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);
#[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,
-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<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),
- })
- }
-}
+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);
#[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() {
-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<String>,
source: &Option<Source>,
) -> Option<String> {
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};
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,
},
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,
};
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<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)]
pub struct ApubPost(Post);
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;
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::{
};
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<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)]
pub struct ApubPrivateMessage(PrivateMessage);
#[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;
--- /dev/null
+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>>,
+}
--- /dev/null
+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>,
+}
--- /dev/null
+pub(crate) mod group_followers;
+pub(crate) mod group_moderators;
+pub(crate) mod group_outbox;
+pub(crate) mod person_outbox;
--- /dev/null
+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,
+}
--- /dev/null
+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(())
+ }
+}
--- /dev/null
+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),
+ })
+ }
+}
--- /dev/null
+pub(crate) mod chat_message;
+pub(crate) mod group;
+pub(crate) mod note;
+pub(crate) mod page;
+pub(crate) mod tombstone;
--- /dev/null
+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(())
+ }
+}
--- /dev/null
+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());
+ }
+ }
+ }
+}