2 fetcher::object_id::ObjectId,
3 objects::{person::ApubPerson, Source},
5 use activitystreams::unparsed::Unparsed;
7 use chrono::{DateTime, FixedOffset, NaiveDateTime};
8 use html2md::parse_html;
9 use lemmy_api_common::blocking;
12 values::{MediaTypeHtml, MediaTypeMarkdown},
13 verify::verify_domains_match,
15 use lemmy_db_schema::{
18 private_message::{PrivateMessage, PrivateMessageForm},
22 use lemmy_utils::{utils::convert_datetime, LemmyError};
23 use lemmy_websocket::LemmyContext;
24 use serde::{Deserialize, Serialize};
25 use serde_with::skip_serializing_none;
29 #[skip_serializing_none]
30 #[derive(Clone, Debug, Deserialize, Serialize)]
31 #[serde(rename_all = "camelCase")]
32 pub struct ChatMessage {
33 r#type: ChatMessageType,
35 pub(crate) attributed_to: ObjectId<ApubPerson>,
36 to: [ObjectId<ApubPerson>; 1],
38 media_type: Option<MediaTypeHtml>,
39 source: Option<Source>,
40 published: Option<DateTime<FixedOffset>>,
41 updated: Option<DateTime<FixedOffset>>,
46 /// https://docs.pleroma.social/backend/development/ap_extensions/#chatmessages
47 #[derive(Clone, Debug, Deserialize, Serialize)]
48 pub enum ChatMessageType {
53 pub(crate) fn id_unchecked(&self) -> &Url {
56 pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> {
57 verify_domains_match(&self.id, expected_domain)?;
61 pub(crate) async fn verify(
63 context: &LemmyContext,
64 request_counter: &mut i32,
65 ) -> Result<(), LemmyError> {
66 verify_domains_match(self.attributed_to.inner(), &self.id)?;
69 .dereference(context, request_counter)
72 return Err(anyhow!("Person is banned from site").into());
78 #[derive(Clone, Debug)]
79 pub struct ApubPrivateMessage(PrivateMessage);
81 impl Deref for ApubPrivateMessage {
82 type Target = PrivateMessage;
83 fn deref(&self) -> &Self::Target {
88 impl From<PrivateMessage> for ApubPrivateMessage {
89 fn from(pm: PrivateMessage) -> Self {
90 ApubPrivateMessage { 0: pm }
94 #[async_trait::async_trait(?Send)]
95 impl ApubObject for ApubPrivateMessage {
96 type DataType = LemmyContext;
97 type ApubType = ChatMessage;
98 type TombstoneType = ();
100 fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
104 async fn read_from_apub_id(
106 context: &LemmyContext,
107 ) -> Result<Option<Self>, LemmyError> {
109 blocking(context.pool(), move |conn| {
110 PrivateMessage::read_from_apub_id(conn, object_id)
117 async fn delete(self, _context: &LemmyContext) -> Result<(), LemmyError> {
118 // do nothing, because pm can't be fetched over http
122 async fn to_apub(&self, context: &LemmyContext) -> Result<ChatMessage, LemmyError> {
123 let creator_id = self.creator_id;
124 let creator = blocking(context.pool(), move |conn| Person::read(conn, creator_id)).await??;
126 let recipient_id = self.recipient_id;
128 blocking(context.pool(), move |conn| Person::read(conn, recipient_id)).await??;
130 let note = ChatMessage {
131 r#type: ChatMessageType::ChatMessage,
132 id: self.ap_id.clone().into(),
133 attributed_to: ObjectId::new(creator.actor_id),
134 to: [ObjectId::new(recipient.actor_id)],
135 content: self.content.clone(),
136 media_type: Some(MediaTypeHtml::Html),
137 source: Some(Source {
138 content: self.content.clone(),
139 media_type: MediaTypeMarkdown::Markdown,
141 published: Some(convert_datetime(self.published)),
142 updated: self.updated.map(convert_datetime),
143 unparsed: Default::default(),
148 fn to_tombstone(&self) -> Result<(), LemmyError> {
154 context: &LemmyContext,
155 expected_domain: &Url,
156 request_counter: &mut i32,
157 ) -> Result<ApubPrivateMessage, LemmyError> {
158 let ap_id = Some(note.id(expected_domain)?.clone().into());
161 .dereference(context, request_counter)
163 let recipient = note.to[0].dereference(context, request_counter).await?;
164 let content = if let Some(source) = ¬e.source {
165 source.content.clone()
167 parse_html(¬e.content)
170 let form = PrivateMessageForm {
171 creator_id: creator.id,
172 recipient_id: recipient.id,
174 published: note.published.map(|u| u.to_owned().naive_local()),
175 updated: note.updated.map(|u| u.to_owned().naive_local()),
181 let pm = blocking(context.pool(), move |conn| {
182 PrivateMessage::upsert(conn, &form)
192 use crate::objects::tests::{file_to_json_object, init_context};
193 use assert_json_diff::assert_json_include;
194 use serial_test::serial;
196 async fn prepare_comment_test(url: &Url, context: &LemmyContext) -> (ApubPerson, ApubPerson) {
197 let lemmy_person = file_to_json_object("assets/lemmy-person.json");
198 let person1 = ApubPerson::from_apub(&lemmy_person, context, url, &mut 0)
201 let pleroma_person = file_to_json_object("assets/pleroma-person.json");
202 let pleroma_url = Url::parse("https://queer.hacktivis.me/users/lanodan").unwrap();
203 let person2 = ApubPerson::from_apub(&pleroma_person, context, &pleroma_url, &mut 0)
209 fn cleanup(data: (ApubPerson, ApubPerson), context: &LemmyContext) {
210 Person::delete(&*context.pool().get().unwrap(), data.0.id).unwrap();
211 Person::delete(&*context.pool().get().unwrap(), data.1.id).unwrap();
216 async fn test_parse_lemmy_pm() {
217 let context = init_context();
218 let url = Url::parse("https://enterprise.lemmy.ml/private_message/1621").unwrap();
219 let data = prepare_comment_test(&url, &context).await;
220 let json = file_to_json_object("assets/lemmy-private-message.json");
221 let mut request_counter = 0;
222 let pm = ApubPrivateMessage::from_apub(&json, &context, &url, &mut request_counter)
226 assert_eq!(pm.ap_id.clone().into_inner(), url);
227 assert_eq!(pm.content.len(), 20);
228 assert_eq!(request_counter, 0);
230 let to_apub = pm.to_apub(&context).await.unwrap();
231 assert_json_include!(actual: json, expected: to_apub);
233 PrivateMessage::delete(&*context.pool().get().unwrap(), pm.id).unwrap();
234 cleanup(data, &context);
239 async fn test_parse_pleroma_pm() {
240 let context = init_context();
241 let url = Url::parse("https://enterprise.lemmy.ml/private_message/1621").unwrap();
242 let data = prepare_comment_test(&url, &context).await;
243 let pleroma_url = Url::parse("https://queer.hacktivis.me/objects/2").unwrap();
244 let json = file_to_json_object("assets/pleroma-private-message.json");
245 let mut request_counter = 0;
246 let pm = ApubPrivateMessage::from_apub(&json, &context, &pleroma_url, &mut request_counter)
250 assert_eq!(pm.ap_id.clone().into_inner(), pleroma_url);
251 assert_eq!(pm.content.len(), 3);
252 assert_eq!(request_counter, 0);
254 PrivateMessage::delete(&*context.pool().get().unwrap(), pm.id).unwrap();
255 cleanup(data, &context);