]> Untitled Git - lemmy.git/blob - crates/apub/src/protocol/objects/page.rs
Federate with Peertube (#2244)
[lemmy.git] / crates / apub / src / protocol / objects / page.rs
1 use crate::{
2   fetcher::user_or_community::{PersonOrGroupType, UserOrCommunity},
3   objects::{community::ApubCommunity, person::ApubPerson, post::ApubPost},
4   protocol::{ImageObject, Source},
5 };
6 use activitystreams_kinds::link::LinkType;
7 use chrono::{DateTime, FixedOffset};
8 use itertools::Itertools;
9 use lemmy_apub_lib::{
10   data::Data,
11   object_id::ObjectId,
12   traits::{ActivityHandler, ApubObject},
13   values::MediaTypeMarkdownOrHtml,
14 };
15 use lemmy_db_schema::newtypes::DbUrl;
16 use lemmy_utils::LemmyError;
17 use lemmy_websocket::LemmyContext;
18 use serde::{Deserialize, Serialize};
19 use serde_with::skip_serializing_none;
20 use url::Url;
21
22 #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
23 pub enum PageType {
24   Page,
25   Article,
26   Note,
27   Video,
28 }
29
30 #[skip_serializing_none]
31 #[derive(Clone, Debug, Deserialize, Serialize)]
32 #[serde(rename_all = "camelCase")]
33 pub struct Page {
34   #[serde(rename = "type")]
35   pub(crate) kind: PageType,
36   pub(crate) id: ObjectId<ApubPost>,
37   pub(crate) attributed_to: AttributedTo,
38   #[serde(deserialize_with = "crate::deserialize_one_or_many")]
39   pub(crate) to: Vec<Url>,
40   pub(crate) name: String,
41
42   #[serde(deserialize_with = "crate::deserialize_one_or_many", default)]
43   pub(crate) cc: Vec<Url>,
44   pub(crate) content: Option<String>,
45   pub(crate) media_type: Option<MediaTypeMarkdownOrHtml>,
46   #[serde(deserialize_with = "crate::deserialize_skip_error", default)]
47   pub(crate) source: Option<Source>,
48   /// deprecated, use attachment field
49   #[serde(deserialize_with = "crate::deserialize_skip_error", default)]
50   pub(crate) url: Option<Url>,
51   /// most software uses array type for attachment field, so we do the same. nevertheless, we only
52   /// use the first item
53   #[serde(default)]
54   pub(crate) attachment: Vec<Attachment>,
55   pub(crate) image: Option<ImageObject>,
56   pub(crate) comments_enabled: Option<bool>,
57   pub(crate) sensitive: Option<bool>,
58   pub(crate) stickied: Option<bool>,
59   pub(crate) published: Option<DateTime<FixedOffset>>,
60   pub(crate) updated: Option<DateTime<FixedOffset>>,
61 }
62
63 #[derive(Clone, Debug, Deserialize, Serialize)]
64 #[serde(rename_all = "camelCase")]
65 pub(crate) struct Attachment {
66   pub(crate) href: Url,
67   pub(crate) r#type: LinkType,
68 }
69
70 #[derive(Clone, Debug, Deserialize, Serialize)]
71 #[serde(untagged)]
72 pub(crate) enum AttributedTo {
73   Lemmy(ObjectId<ApubPerson>),
74   Peertube([AttributedToPeertube; 2]),
75 }
76
77 #[derive(Clone, Debug, Deserialize, Serialize)]
78 #[serde(rename_all = "camelCase")]
79 pub(crate) struct AttributedToPeertube {
80   #[serde(rename = "type")]
81   pub kind: PersonOrGroupType,
82   pub id: ObjectId<UserOrCommunity>,
83 }
84
85 impl Page {
86   /// Only mods can change the post's stickied/locked status. So if either of these is changed from
87   /// the current value, it is a mod action and needs to be verified as such.
88   ///
89   /// Both stickied and locked need to be false on a newly created post (verified in [[CreatePost]].
90   pub(crate) async fn is_mod_action(&self, context: &LemmyContext) -> Result<bool, LemmyError> {
91     let old_post = ObjectId::<ApubPost>::new(self.id.clone())
92       .dereference_local(context)
93       .await;
94
95     let stickied_changed = Page::is_stickied_changed(&old_post, &self.stickied);
96     let locked_changed = Page::is_locked_changed(&old_post, &self.comments_enabled);
97     Ok(stickied_changed || locked_changed)
98   }
99
100   pub(crate) fn is_stickied_changed<E>(
101     old_post: &Result<ApubPost, E>,
102     new_stickied: &Option<bool>,
103   ) -> bool {
104     if let Some(new_stickied) = new_stickied {
105       if let Ok(old_post) = old_post {
106         return new_stickied != &old_post.stickied;
107       }
108     }
109
110     false
111   }
112
113   pub(crate) fn is_locked_changed<E>(
114     old_post: &Result<ApubPost, E>,
115     new_comments_enabled: &Option<bool>,
116   ) -> bool {
117     if let Some(new_comments_enabled) = new_comments_enabled {
118       if let Ok(old_post) = old_post {
119         return new_comments_enabled != &!old_post.locked;
120       }
121     }
122
123     false
124   }
125
126   pub(crate) async fn extract_community(
127     &self,
128     context: &LemmyContext,
129     request_counter: &mut i32,
130   ) -> Result<ApubCommunity, LemmyError> {
131     match &self.attributed_to {
132       AttributedTo::Lemmy(_) => {
133         let mut iter = self.to.iter().merge(self.cc.iter());
134         loop {
135           if let Some(cid) = iter.next() {
136             let cid = ObjectId::new(cid.clone());
137             if let Ok(c) = cid
138               .dereference(context, context.client(), request_counter)
139               .await
140             {
141               break Ok(c);
142             }
143           } else {
144             return Err(LemmyError::from_message("No community found in cc"));
145           }
146         }
147       }
148       AttributedTo::Peertube(p) => {
149         p.iter()
150           .find(|a| a.kind == PersonOrGroupType::Group)
151           .map(|a| ObjectId::<ApubCommunity>::new(a.id.clone().into_inner()))
152           .ok_or_else(|| LemmyError::from_message("page does not specify group"))?
153           .dereference(context, context.client(), request_counter)
154           .await
155       }
156     }
157   }
158
159   pub(crate) fn creator(&self) -> Result<ObjectId<ApubPerson>, LemmyError> {
160     match &self.attributed_to {
161       AttributedTo::Lemmy(l) => Ok(l.clone()),
162       AttributedTo::Peertube(p) => p
163         .iter()
164         .find(|a| a.kind == PersonOrGroupType::Person)
165         .map(|a| ObjectId::<ApubPerson>::new(a.id.clone().into_inner()))
166         .ok_or_else(|| LemmyError::from_message("page does not specify creator person")),
167     }
168   }
169 }
170
171 impl Attachment {
172   pub(crate) fn new(url: DbUrl) -> Attachment {
173     Attachment {
174       href: url.into(),
175       r#type: Default::default(),
176     }
177   }
178 }
179
180 // Used for community outbox, so that it can be compatible with Pleroma/Mastodon.
181 #[async_trait::async_trait(?Send)]
182 impl ActivityHandler for Page {
183   type DataType = LemmyContext;
184   async fn verify(
185     &self,
186     data: &Data<Self::DataType>,
187     request_counter: &mut i32,
188   ) -> Result<(), LemmyError> {
189     ApubPost::verify(self, self.id.inner(), data, request_counter).await
190   }
191   async fn receive(
192     self,
193     data: &Data<Self::DataType>,
194     request_counter: &mut i32,
195   ) -> Result<(), LemmyError> {
196     ApubPost::from_apub(self, data, request_counter).await?;
197     Ok(())
198   }
199 }