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