2 context::lemmy_context,
3 fetcher::object_id::ObjectId,
4 objects::{person::ApubPerson, Source},
10 primitives::OneOrMany,
14 use chrono::{DateTime, FixedOffset};
15 use html2md::parse_html;
16 use lemmy_api_common::blocking;
19 values::{MediaTypeHtml, MediaTypeMarkdown},
20 verify::verify_domains_match,
22 use lemmy_db_schema::{
25 private_message::{PrivateMessage, PrivateMessageForm},
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;
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,
44 pub(crate) attributed_to: ObjectId<ApubPerson>,
45 to: [ObjectId<ApubPerson>; 1],
47 media_type: Option<MediaTypeHtml>,
48 source: Option<Source>,
49 published: Option<DateTime<FixedOffset>>,
50 updated: Option<DateTime<FixedOffset>>,
55 /// https://docs.pleroma.social/backend/development/ap_extensions/#chatmessages
56 #[derive(Clone, Debug, Deserialize, Serialize)]
57 pub enum ChatMessageType {
62 pub(crate) fn id_unchecked(&self) -> &Url {
65 pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> {
66 verify_domains_match(&self.id, expected_domain)?;
70 pub(crate) async fn verify(
72 context: &LemmyContext,
73 request_counter: &mut i32,
74 ) -> Result<(), LemmyError> {
75 verify_domains_match(self.attributed_to.inner(), &self.id)?;
78 .dereference(context, request_counter)
81 return Err(anyhow!("Person is banned from site").into());
87 #[derive(Clone, Debug)]
88 pub struct ApubPrivateMessage(PrivateMessage);
90 impl Deref for ApubPrivateMessage {
91 type Target = PrivateMessage;
92 fn deref(&self) -> &Self::Target {
97 impl From<PrivateMessage> for ApubPrivateMessage {
98 fn from(pm: PrivateMessage) -> Self {
99 ApubPrivateMessage { 0: pm }
103 #[async_trait::async_trait(?Send)]
104 impl ApubObject for ApubPrivateMessage {
105 type DataType = LemmyContext;
106 type ApubType = ChatMessage;
107 type TombstoneType = Tombstone;
109 fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
113 async fn read_from_apub_id(
115 context: &LemmyContext,
116 ) -> Result<Option<Self>, LemmyError> {
118 blocking(context.pool(), move |conn| {
119 PrivateMessage::read_from_apub_id(conn, object_id)
126 async fn delete(self, _context: &LemmyContext) -> Result<(), LemmyError> {
127 // do nothing, because pm can't be fetched over http
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??;
135 let recipient_id = self.recipient_id;
137 blocking(context.pool(), move |conn| Person::read(conn, recipient_id)).await??;
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,
151 published: Some(convert_datetime(self.published)),
152 updated: self.updated.map(convert_datetime),
153 unparsed: Default::default(),
158 fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
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());
171 .dereference(context, request_counter)
173 let recipient = note.to[0].dereference(context, request_counter).await?;
174 let content = if let Some(source) = ¬e.source {
175 source.content.clone()
177 parse_html(¬e.content)
180 let form = PrivateMessageForm {
181 creator_id: creator.id,
182 recipient_id: recipient.id,
184 published: note.published.map(|u| u.to_owned().naive_local()),
185 updated: note.updated.map(|u| u.to_owned().naive_local()),
191 let pm = blocking(context.pool(), move |conn| {
192 PrivateMessage::upsert(conn, &form)
202 use crate::objects::tests::{file_to_json_object, init_context};
203 use assert_json_diff::assert_json_include;
204 use serial_test::serial;
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)
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)
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();
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)
236 assert_eq!(pm.ap_id.clone().into_inner(), url);
237 assert_eq!(pm.content.len(), 20);
238 assert_eq!(request_counter, 0);
240 let to_apub = pm.to_apub(&context).await.unwrap();
241 assert_json_include!(actual: json, expected: to_apub);
243 PrivateMessage::delete(&*context.pool().get().unwrap(), pm.id).unwrap();
244 cleanup(data, &context);
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)
260 assert_eq!(pm.ap_id.clone().into_inner(), pleroma_url);
261 assert_eq!(pm.content.len(), 3);
262 assert_eq!(request_counter, 0);
264 PrivateMessage::delete(&*context.pool().get().unwrap(), pm.id).unwrap();
265 cleanup(data, &context);