]> Untitled Git - lemmy.git/blob - crates/apub/src/objects/mod.rs
Merge remote-tracking branch 'origin/main'
[lemmy.git] / crates / apub / src / objects / mod.rs
1 use crate::{
2   check_is_apub_id_valid,
3   fetcher::{community::get_or_fetch_and_upsert_community, user::get_or_fetch_and_upsert_user},
4   inbox::community_inbox::check_community_or_site_ban,
5 };
6 use activitystreams::{
7   base::{AsBase, BaseExt, ExtendsExt},
8   markers::Base,
9   mime::{FromStrError, Mime},
10   object::{ApObjectExt, Object, ObjectExt, Tombstone, TombstoneExt},
11 };
12 use anyhow::{anyhow, Context};
13 use chrono::NaiveDateTime;
14 use diesel::result::Error::NotFound;
15 use lemmy_db_queries::{ApubObject, Crud, DbPool};
16 use lemmy_db_schema::source::community::Community;
17 use lemmy_structs::blocking;
18 use lemmy_utils::{location_info, settings::Settings, utils::convert_datetime, LemmyError};
19 use lemmy_websocket::LemmyContext;
20 use url::Url;
21
22 pub(crate) mod comment;
23 pub(crate) mod community;
24 pub(crate) mod post;
25 pub(crate) mod private_message;
26 pub(crate) mod user;
27
28 /// Trait for converting an object or actor into the respective ActivityPub type.
29 #[async_trait::async_trait(?Send)]
30 pub(crate) trait ToApub {
31   type ApubType;
32   async fn to_apub(&self, pool: &DbPool) -> Result<Self::ApubType, LemmyError>;
33   fn to_tombstone(&self) -> Result<Tombstone, LemmyError>;
34 }
35
36 #[async_trait::async_trait(?Send)]
37 pub(crate) trait FromApub {
38   type ApubType;
39   /// Converts an object from ActivityPub type to Lemmy internal type.
40   ///
41   /// * `apub` The object to read from
42   /// * `context` LemmyContext which holds DB pool, HTTP client etc
43   /// * `expected_domain` Domain where the object was received from
44   async fn from_apub(
45     apub: &Self::ApubType,
46     context: &LemmyContext,
47     expected_domain: Url,
48     request_counter: &mut i32,
49   ) -> Result<Self, LemmyError>
50   where
51     Self: Sized;
52 }
53
54 #[async_trait::async_trait(?Send)]
55 pub(in crate::objects) trait FromApubToForm<ApubType> {
56   async fn from_apub(
57     apub: &ApubType,
58     context: &LemmyContext,
59     expected_domain: Url,
60     request_counter: &mut i32,
61   ) -> Result<Self, LemmyError>
62   where
63     Self: Sized;
64 }
65
66 /// Updated is actually the deletion time
67 fn create_tombstone<T>(
68   deleted: bool,
69   object_id: Url,
70   updated: Option<NaiveDateTime>,
71   former_type: T,
72 ) -> Result<Tombstone, LemmyError>
73 where
74   T: ToString,
75 {
76   if deleted {
77     if let Some(updated) = updated {
78       let mut tombstone = Tombstone::new();
79       tombstone.set_id(object_id);
80       tombstone.set_former_type(former_type.to_string());
81       tombstone.set_deleted(convert_datetime(updated));
82       Ok(tombstone)
83     } else {
84       Err(anyhow!("Cant convert to tombstone because updated time was None.").into())
85     }
86   } else {
87     Err(anyhow!("Cant convert object to tombstone if it wasnt deleted").into())
88   }
89 }
90
91 pub(in crate::objects) fn check_object_domain<T, Kind>(
92   apub: &T,
93   expected_domain: Url,
94 ) -> Result<lemmy_db_schema::Url, LemmyError>
95 where
96   T: Base + AsBase<Kind>,
97 {
98   let domain = expected_domain.domain().context(location_info!())?;
99   let object_id = apub.id(domain)?.context(location_info!())?;
100   check_is_apub_id_valid(object_id)?;
101   Ok(object_id.to_owned().into())
102 }
103
104 pub(in crate::objects) fn set_content_and_source<T, Kind1, Kind2>(
105   object: &mut T,
106   markdown_text: &str,
107 ) -> Result<(), LemmyError>
108 where
109   T: ApObjectExt<Kind1> + ObjectExt<Kind2> + AsBase<Kind2>,
110 {
111   let mut source = Object::<()>::new_none_type();
112   source
113     .set_content(markdown_text)
114     .set_media_type(mime_markdown()?);
115   object.set_source(source.into_any_base()?);
116
117   // set `content` to markdown for compatibility with older Lemmy versions
118   // TODO: change this to HTML in a while
119   object.set_content(markdown_text);
120   object.set_media_type(mime_markdown()?);
121   //object.set_content(markdown_to_html(markdown_text));
122   Ok(())
123 }
124
125 pub(in crate::objects) fn get_source_markdown_value<T, Kind1, Kind2>(
126   object: &T,
127 ) -> Result<Option<String>, LemmyError>
128 where
129   T: ApObjectExt<Kind1> + ObjectExt<Kind2> + AsBase<Kind2>,
130 {
131   let content = object
132     .content()
133     .map(|s| s.as_single_xsd_string())
134     .flatten()
135     .map(|s| s.to_string());
136   if content.is_some() {
137     let source = object.source();
138     // updated lemmy version, read markdown from `source.content`
139     if let Some(source) = source {
140       let source = Object::<()>::from_any_base(source.to_owned())?.context(location_info!())?;
141       check_is_markdown(source.media_type())?;
142       let source_content = source
143         .content()
144         .map(|s| s.as_single_xsd_string())
145         .flatten()
146         .context(location_info!())?
147         .to_string();
148       return Ok(Some(source_content));
149     }
150     // older lemmy version, read markdown from `content`
151     // TODO: remove this after a while
152     else {
153       return Ok(content);
154     }
155   }
156   Ok(None)
157 }
158
159 pub(in crate::objects) fn mime_markdown() -> Result<Mime, FromStrError> {
160   "text/markdown".parse()
161 }
162
163 pub(in crate::objects) fn check_is_markdown(mime: Option<&Mime>) -> Result<(), LemmyError> {
164   let mime = mime.context(location_info!())?;
165   if !mime.eq(&mime_markdown()?) {
166     Err(LemmyError::from(anyhow!(
167       "Lemmy only supports markdown content"
168     )))
169   } else {
170     Ok(())
171   }
172 }
173
174 /// Converts an ActivityPub object (eg `Note`) to a database object (eg `Comment`). If an object
175 /// with the same ActivityPub ID already exists in the database, it is returned directly. Otherwise
176 /// the apub object is parsed, inserted and returned.
177 pub(in crate::objects) async fn get_object_from_apub<From, Kind, To, ToForm>(
178   from: &From,
179   context: &LemmyContext,
180   expected_domain: Url,
181   request_counter: &mut i32,
182 ) -> Result<To, LemmyError>
183 where
184   From: BaseExt<Kind>,
185   To: ApubObject<ToForm> + Crud<ToForm> + Send + 'static,
186   ToForm: FromApubToForm<From> + Send + 'static,
187 {
188   let object_id = from.id_unchecked().context(location_info!())?.to_owned();
189   let domain = object_id.domain().context(location_info!())?;
190
191   // if its a local object, return it directly from the database
192   if Settings::get().hostname == domain {
193     let object = blocking(context.pool(), move |conn| {
194       To::read_from_apub_id(conn, &object_id.into())
195     })
196     .await??;
197     Ok(object)
198   }
199   // otherwise parse and insert, assuring that it comes from the right domain
200   else {
201     let to_form = ToForm::from_apub(&from, context, expected_domain, request_counter).await?;
202
203     let to = blocking(context.pool(), move |conn| To::upsert(conn, &to_form)).await??;
204     Ok(to)
205   }
206 }
207
208 pub(in crate::objects) async fn check_object_for_community_or_site_ban<T, Kind>(
209   object: &T,
210   context: &LemmyContext,
211   request_counter: &mut i32,
212 ) -> Result<(), LemmyError>
213 where
214   T: ObjectExt<Kind>,
215 {
216   let user_id = object
217     .attributed_to()
218     .context(location_info!())?
219     .as_single_xsd_any_uri()
220     .context(location_info!())?;
221   let user = get_or_fetch_and_upsert_user(user_id, context, request_counter).await?;
222   let community = get_to_community(object, context, request_counter).await?;
223   check_community_or_site_ban(&user, &community, context.pool()).await
224 }
225
226 pub(in crate::objects) async fn get_to_community<T, Kind>(
227   object: &T,
228   context: &LemmyContext,
229   request_counter: &mut i32,
230 ) -> Result<Community, LemmyError>
231 where
232   T: ObjectExt<Kind>,
233 {
234   let community_ids = object
235     .to()
236     .context(location_info!())?
237     .as_many()
238     .context(location_info!())?
239     .iter()
240     .map(|a| a.as_xsd_any_uri().context(location_info!()))
241     .collect::<Result<Vec<&Url>, anyhow::Error>>()?;
242   for cid in community_ids {
243     let community = get_or_fetch_and_upsert_community(&cid, context, request_counter).await;
244     if community.is_ok() {
245       return community;
246     }
247   }
248   Err(NotFound.into())
249 }