2 context::lemmy_context,
3 fetcher::object_id::ObjectId,
4 objects::{create_tombstone, person::ApubPerson, Source},
9 object::{kind::NoteType, Tombstone},
10 primitives::OneOrMany,
14 use chrono::{DateTime, FixedOffset};
15 use html2md::parse_html;
16 use lemmy_api_common::blocking;
18 traits::{ApubObject, FromApub, ToApub},
19 values::{MediaTypeHtml, MediaTypeMarkdown},
20 verify::verify_domains_match,
22 use lemmy_db_schema::{
25 private_message::{PrivateMessage, PrivateMessageForm},
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;
37 #[skip_serializing_none]
38 #[derive(Clone, Debug, Deserialize, Serialize)]
39 #[serde(rename_all = "camelCase")]
41 #[serde(rename = "@context")]
42 context: OneOrMany<AnyBase>,
43 r#type: ChatMessageType,
45 pub(crate) attributed_to: ObjectId<ApubPerson>,
46 to: [ObjectId<ApubPerson>; 1],
48 media_type: Option<MediaTypeHtml>,
49 source: Option<Source>,
50 published: Option<DateTime<FixedOffset>>,
51 updated: Option<DateTime<FixedOffset>>,
56 /// https://docs.pleroma.social/backend/development/ap_extensions/#chatmessages
57 #[derive(Clone, Debug, Deserialize, Serialize)]
58 pub enum ChatMessageType {
63 pub(crate) fn id_unchecked(&self) -> &Url {
66 pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> {
67 verify_domains_match(&self.id, expected_domain)?;
71 pub(crate) async fn verify(
73 context: &LemmyContext,
74 request_counter: &mut i32,
75 ) -> Result<(), LemmyError> {
76 verify_domains_match(self.attributed_to.inner(), &self.id)?;
79 .dereference(context, request_counter)
82 return Err(anyhow!("Person is banned from site").into());
88 #[derive(Clone, Debug)]
89 pub struct ApubPrivateMessage(PrivateMessage);
91 impl Deref for ApubPrivateMessage {
92 type Target = PrivateMessage;
93 fn deref(&self) -> &Self::Target {
98 impl From<PrivateMessage> for ApubPrivateMessage {
99 fn from(pm: PrivateMessage) -> Self {
100 ApubPrivateMessage { 0: pm }
104 #[async_trait::async_trait(?Send)]
105 impl ApubObject for ApubPrivateMessage {
106 type DataType = LemmyContext;
108 fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
112 async fn read_from_apub_id(
114 context: &LemmyContext,
115 ) -> Result<Option<Self>, LemmyError> {
117 blocking(context.pool(), move |conn| {
118 PrivateMessage::read_from_apub_id(conn, object_id)
125 async fn delete(self, _context: &LemmyContext) -> Result<(), LemmyError> {
126 // do nothing, because pm can't be fetched over http
131 #[async_trait::async_trait(?Send)]
132 impl ToApub for ApubPrivateMessage {
133 type ApubType = Note;
134 type TombstoneType = Tombstone;
135 type DataType = DbPool;
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??;
141 let recipient_id = self.recipient_id;
142 let recipient = blocking(pool, move |conn| Person::read(conn, recipient_id)).await??;
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,
156 published: Some(convert_datetime(self.published)),
157 updated: self.updated.map(convert_datetime),
158 unparsed: Default::default(),
163 fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
166 self.ap_id.to_owned().into(),
173 #[async_trait::async_trait(?Send)]
174 impl FromApub for ApubPrivateMessage {
175 type ApubType = Note;
176 type DataType = LemmyContext;
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());
187 .dereference(context, request_counter)
189 let recipient = note.to[0].dereference(context, request_counter).await?;
190 let content = if let Some(source) = ¬e.source {
191 source.content.clone()
193 parse_html(¬e.content)
196 let form = PrivateMessageForm {
197 creator_id: creator.id,
198 recipient_id: recipient.id,
200 published: note.published.map(|u| u.to_owned().naive_local()),
201 updated: note.updated.map(|u| u.to_owned().naive_local()),
207 let pm = blocking(context.pool(), move |conn| {
208 PrivateMessage::upsert(conn, &form)
218 use crate::objects::tests::{file_to_json_object, init_context};
219 use assert_json_diff::assert_json_include;
220 use serial_test::serial;
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)
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)
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();
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)
252 assert_eq!(pm.ap_id.clone().into_inner(), url);
253 assert_eq!(pm.content.len(), 20);
254 assert_eq!(request_counter, 0);
256 let to_apub = pm.to_apub(context.pool()).await.unwrap();
257 assert_json_include!(actual: json, expected: to_apub);
259 PrivateMessage::delete(&*context.pool().get().unwrap(), pm.id).unwrap();
260 cleanup(data, &context);
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)
276 assert_eq!(pm.ap_id.clone().into_inner(), pleroma_url);
277 assert_eq!(pm.content.len(), 3);
278 assert_eq!(request_counter, 0);
280 PrivateMessage::delete(&*context.pool().get().unwrap(), pm.id).unwrap();
281 cleanup(data, &context);