mirror of https://github.com/LemmyNet/lemmy
Move object and collection structs to protocol folder
parent
358ef99ea2
commit
5ff044346f
@ -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);
|
||||
|
@ -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>>,
|
||||
}
|
@ -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>,
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
pub(crate) mod group_followers;
|
||||
pub(crate) mod group_moderators;
|
||||
pub(crate) mod group_outbox;
|
||||
pub(crate) mod person_outbox;
|
@ -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,
|
||||
}
|
@ -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(())
|
||||
}
|
||||
}
|
@ -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),
|
||||
})
|
||||
}
|
||||
}
|
@ -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;
|
@ -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(())
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue