]> Untitled Git - lemmy.git/blob - crates/apub/src/objects/private_message.rs
bd9cc1da479ae16a2aca4cb39be5eef009790c58
[lemmy.git] / crates / apub / src / objects / private_message.rs
1 use crate::{
2   context::lemmy_context,
3   fetcher::object_id::ObjectId,
4   objects::{create_tombstone, person::ApubPerson, Source},
5 };
6 use activitystreams::{
7   base::AnyBase,
8   chrono::NaiveDateTime,
9   object::{kind::NoteType, Tombstone},
10   primitives::OneOrMany,
11   unparsed::Unparsed,
12 };
13 use anyhow::anyhow;
14 use chrono::{DateTime, FixedOffset};
15 use html2md::parse_html;
16 use lemmy_api_common::blocking;
17 use lemmy_apub_lib::{
18   traits::{ApubObject, FromApub, ToApub},
19   values::{MediaTypeHtml, MediaTypeMarkdown},
20   verify::verify_domains_match,
21 };
22 use lemmy_db_schema::{
23   source::{
24     person::Person,
25     private_message::{PrivateMessage, PrivateMessageForm},
26   },
27   traits::Crud,
28   DbPool,
29 };
30 use lemmy_utils::{utils::convert_datetime, LemmyError};
31 use lemmy_websocket::LemmyContext;
32 use serde::{Deserialize, Serialize};
33 use serde_with::skip_serializing_none;
34 use std::ops::Deref;
35 use url::Url;
36
37 #[skip_serializing_none]
38 #[derive(Clone, Debug, Deserialize, Serialize)]
39 #[serde(rename_all = "camelCase")]
40 pub struct Note {
41   #[serde(rename = "@context")]
42   context: OneOrMany<AnyBase>,
43   r#type: ChatMessageType,
44   id: Url,
45   pub(crate) attributed_to: ObjectId<ApubPerson>,
46   to: [ObjectId<ApubPerson>; 1],
47   content: String,
48   media_type: Option<MediaTypeHtml>,
49   source: Option<Source>,
50   published: Option<DateTime<FixedOffset>>,
51   updated: Option<DateTime<FixedOffset>>,
52   #[serde(flatten)]
53   unparsed: Unparsed,
54 }
55
56 /// https://docs.pleroma.social/backend/development/ap_extensions/#chatmessages
57 #[derive(Clone, Debug, Deserialize, Serialize)]
58 pub enum ChatMessageType {
59   ChatMessage,
60 }
61
62 impl Note {
63   pub(crate) fn id_unchecked(&self) -> &Url {
64     &self.id
65   }
66   pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> {
67     verify_domains_match(&self.id, expected_domain)?;
68     Ok(&self.id)
69   }
70
71   pub(crate) async fn verify(
72     &self,
73     context: &LemmyContext,
74     request_counter: &mut i32,
75   ) -> Result<(), LemmyError> {
76     verify_domains_match(self.attributed_to.inner(), &self.id)?;
77     let person = self
78       .attributed_to
79       .dereference(context, request_counter)
80       .await?;
81     if person.banned {
82       return Err(anyhow!("Person is banned from site").into());
83     }
84     Ok(())
85   }
86 }
87
88 #[derive(Clone, Debug)]
89 pub struct ApubPrivateMessage(PrivateMessage);
90
91 impl Deref for ApubPrivateMessage {
92   type Target = PrivateMessage;
93   fn deref(&self) -> &Self::Target {
94     &self.0
95   }
96 }
97
98 impl From<PrivateMessage> for ApubPrivateMessage {
99   fn from(pm: PrivateMessage) -> Self {
100     ApubPrivateMessage { 0: pm }
101   }
102 }
103
104 #[async_trait::async_trait(?Send)]
105 impl ApubObject for ApubPrivateMessage {
106   type DataType = LemmyContext;
107
108   fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
109     None
110   }
111
112   async fn read_from_apub_id(
113     object_id: Url,
114     context: &LemmyContext,
115   ) -> Result<Option<Self>, LemmyError> {
116     Ok(
117       blocking(context.pool(), move |conn| {
118         PrivateMessage::read_from_apub_id(conn, object_id)
119       })
120       .await??
121       .map(Into::into),
122     )
123   }
124
125   async fn delete(self, _context: &LemmyContext) -> Result<(), LemmyError> {
126     // do nothing, because pm can't be fetched over http
127     unimplemented!()
128   }
129 }
130
131 #[async_trait::async_trait(?Send)]
132 impl ToApub for ApubPrivateMessage {
133   type ApubType = Note;
134   type TombstoneType = Tombstone;
135   type DataType = DbPool;
136
137   async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> {
138     let creator_id = self.creator_id;
139     let creator = blocking(pool, move |conn| Person::read(conn, creator_id)).await??;
140
141     let recipient_id = self.recipient_id;
142     let recipient = blocking(pool, move |conn| Person::read(conn, recipient_id)).await??;
143
144     let note = Note {
145       context: lemmy_context(),
146       r#type: ChatMessageType::ChatMessage,
147       id: self.ap_id.clone().into(),
148       attributed_to: ObjectId::new(creator.actor_id),
149       to: [ObjectId::new(recipient.actor_id)],
150       content: self.content.clone(),
151       media_type: Some(MediaTypeHtml::Html),
152       source: Some(Source {
153         content: self.content.clone(),
154         media_type: MediaTypeMarkdown::Markdown,
155       }),
156       published: Some(convert_datetime(self.published)),
157       updated: self.updated.map(convert_datetime),
158       unparsed: Default::default(),
159     };
160     Ok(note)
161   }
162
163   fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
164     create_tombstone(
165       self.deleted,
166       self.ap_id.to_owned().into(),
167       self.updated,
168       NoteType::Note,
169     )
170   }
171 }
172
173 #[async_trait::async_trait(?Send)]
174 impl FromApub for ApubPrivateMessage {
175   type ApubType = Note;
176   type DataType = LemmyContext;
177
178   async fn from_apub(
179     note: &Note,
180     context: &LemmyContext,
181     expected_domain: &Url,
182     request_counter: &mut i32,
183   ) -> Result<ApubPrivateMessage, LemmyError> {
184     let ap_id = Some(note.id(expected_domain)?.clone().into());
185     let creator = note
186       .attributed_to
187       .dereference(context, request_counter)
188       .await?;
189     let recipient = note.to[0].dereference(context, request_counter).await?;
190     let content = if let Some(source) = &note.source {
191       source.content.clone()
192     } else {
193       parse_html(&note.content)
194     };
195
196     let form = PrivateMessageForm {
197       creator_id: creator.id,
198       recipient_id: recipient.id,
199       content,
200       published: note.published.map(|u| u.to_owned().naive_local()),
201       updated: note.updated.map(|u| u.to_owned().naive_local()),
202       deleted: None,
203       read: None,
204       ap_id,
205       local: Some(false),
206     };
207     let pm = blocking(context.pool(), move |conn| {
208       PrivateMessage::upsert(conn, &form)
209     })
210     .await??;
211     Ok(pm.into())
212   }
213 }
214
215 #[cfg(test)]
216 mod tests {
217   use super::*;
218   use crate::objects::tests::{file_to_json_object, init_context};
219   use assert_json_diff::assert_json_include;
220   use serial_test::serial;
221
222   async fn prepare_comment_test(url: &Url, context: &LemmyContext) -> (ApubPerson, ApubPerson) {
223     let lemmy_person = file_to_json_object("assets/lemmy-person.json");
224     let person1 = ApubPerson::from_apub(&lemmy_person, context, url, &mut 0)
225       .await
226       .unwrap();
227     let pleroma_person = file_to_json_object("assets/pleroma-person.json");
228     let pleroma_url = Url::parse("https://queer.hacktivis.me/users/lanodan").unwrap();
229     let person2 = ApubPerson::from_apub(&pleroma_person, context, &pleroma_url, &mut 0)
230       .await
231       .unwrap();
232     (person1, person2)
233   }
234
235   fn cleanup(data: (ApubPerson, ApubPerson), context: &LemmyContext) {
236     Person::delete(&*context.pool().get().unwrap(), data.0.id).unwrap();
237     Person::delete(&*context.pool().get().unwrap(), data.1.id).unwrap();
238   }
239
240   #[actix_rt::test]
241   #[serial]
242   async fn test_parse_lemmy_pm() {
243     let context = init_context();
244     let url = Url::parse("https://enterprise.lemmy.ml/private_message/1621").unwrap();
245     let data = prepare_comment_test(&url, &context).await;
246     let json = file_to_json_object("assets/lemmy-private-message.json");
247     let mut request_counter = 0;
248     let pm = ApubPrivateMessage::from_apub(&json, &context, &url, &mut request_counter)
249       .await
250       .unwrap();
251
252     assert_eq!(pm.ap_id.clone().into_inner(), url);
253     assert_eq!(pm.content.len(), 20);
254     assert_eq!(request_counter, 0);
255
256     let to_apub = pm.to_apub(context.pool()).await.unwrap();
257     assert_json_include!(actual: json, expected: to_apub);
258
259     PrivateMessage::delete(&*context.pool().get().unwrap(), pm.id).unwrap();
260     cleanup(data, &context);
261   }
262
263   #[actix_rt::test]
264   #[serial]
265   async fn test_parse_pleroma_pm() {
266     let context = init_context();
267     let url = Url::parse("https://enterprise.lemmy.ml/private_message/1621").unwrap();
268     let data = prepare_comment_test(&url, &context).await;
269     let pleroma_url = Url::parse("https://queer.hacktivis.me/objects/2").unwrap();
270     let json = file_to_json_object("assets/pleroma-private-message.json");
271     let mut request_counter = 0;
272     let pm = ApubPrivateMessage::from_apub(&json, &context, &pleroma_url, &mut request_counter)
273       .await
274       .unwrap();
275
276     assert_eq!(pm.ap_id.clone().into_inner(), pleroma_url);
277     assert_eq!(pm.content.len(), 3);
278     assert_eq!(request_counter, 0);
279
280     PrivateMessage::delete(&*context.pool().get().unwrap(), pm.id).unwrap();
281     cleanup(data, &context);
282   }
283 }