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 lemmy_api_common::blocking;
17 traits::{ApubObject, FromApub, ToApub},
18 values::{MediaTypeHtml, MediaTypeMarkdown},
19 verify::verify_domains_match,
21 use lemmy_db_schema::{
24 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")]
40 #[serde(rename = "@context")]
41 context: OneOrMany<AnyBase>,
44 pub(crate) attributed_to: ObjectId<ApubPerson>,
45 to: ObjectId<ApubPerson>,
47 media_type: MediaTypeHtml,
49 published: Option<DateTime<FixedOffset>>,
50 updated: Option<DateTime<FixedOffset>>,
56 pub(crate) fn id_unchecked(&self) -> &Url {
59 pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> {
60 verify_domains_match(&self.id, expected_domain)?;
64 pub(crate) async fn verify(
66 context: &LemmyContext,
67 request_counter: &mut i32,
68 ) -> Result<(), LemmyError> {
69 verify_domains_match(self.attributed_to.inner(), &self.id)?;
72 .dereference(context, request_counter)
75 return Err(anyhow!("Person is banned from site").into());
81 #[derive(Clone, Debug)]
82 pub struct ApubPrivateMessage(PrivateMessage);
84 impl Deref for ApubPrivateMessage {
85 type Target = PrivateMessage;
86 fn deref(&self) -> &Self::Target {
91 impl From<PrivateMessage> for ApubPrivateMessage {
92 fn from(pm: PrivateMessage) -> Self {
93 ApubPrivateMessage { 0: pm }
97 #[async_trait::async_trait(?Send)]
98 impl ApubObject for ApubPrivateMessage {
99 type DataType = LemmyContext;
101 fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
105 async fn read_from_apub_id(
107 context: &LemmyContext,
108 ) -> Result<Option<Self>, LemmyError> {
110 blocking(context.pool(), move |conn| {
111 PrivateMessage::read_from_apub_id(conn, object_id)
118 async fn delete(self, _context: &LemmyContext) -> Result<(), LemmyError> {
119 // do nothing, because pm can't be fetched over http
124 #[async_trait::async_trait(?Send)]
125 impl ToApub for ApubPrivateMessage {
126 type ApubType = Note;
127 type TombstoneType = Tombstone;
128 type DataType = DbPool;
130 async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> {
131 let creator_id = self.creator_id;
132 let creator = blocking(pool, move |conn| Person::read(conn, creator_id)).await??;
134 let recipient_id = self.recipient_id;
135 let recipient = blocking(pool, move |conn| Person::read(conn, recipient_id)).await??;
138 context: lemmy_context(),
139 r#type: NoteType::Note,
140 id: self.ap_id.clone().into(),
141 attributed_to: ObjectId::new(creator.actor_id),
142 to: ObjectId::new(recipient.actor_id),
143 content: self.content.clone(),
144 media_type: MediaTypeHtml::Html,
146 content: self.content.clone(),
147 media_type: MediaTypeMarkdown::Markdown,
149 published: Some(convert_datetime(self.published)),
150 updated: self.updated.map(convert_datetime),
151 unparsed: Default::default(),
156 fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
159 self.ap_id.to_owned().into(),
166 #[async_trait::async_trait(?Send)]
167 impl FromApub for ApubPrivateMessage {
168 type ApubType = Note;
169 type DataType = LemmyContext;
173 context: &LemmyContext,
174 expected_domain: &Url,
175 request_counter: &mut i32,
176 ) -> Result<ApubPrivateMessage, LemmyError> {
177 let ap_id = Some(note.id(expected_domain)?.clone().into());
180 .dereference(context, request_counter)
182 let recipient = note.to.dereference(context, request_counter).await?;
184 let form = PrivateMessageForm {
185 creator_id: creator.id,
186 recipient_id: recipient.id,
187 content: note.source.content.clone(),
188 published: note.published.map(|u| u.to_owned().naive_local()),
189 updated: note.updated.map(|u| u.to_owned().naive_local()),
195 let pm = blocking(context.pool(), move |conn| {
196 PrivateMessage::upsert(conn, &form)
206 use crate::objects::tests::{file_to_json_object, init_context};
207 use assert_json_diff::assert_json_include;
208 use serial_test::serial;
212 async fn test_fetch_lemmy_pm() {
213 let context = init_context();
214 let url = Url::parse("https://lemmy.ml/private_message/1621").unwrap();
215 let lemmy_person = file_to_json_object("assets/lemmy-person.json");
216 let person1 = ApubPerson::from_apub(&lemmy_person, &context, &url, &mut 0)
219 let pleroma_person = file_to_json_object("assets/pleroma-person.json");
220 let pleroma_url = Url::parse("https://queer.hacktivis.me/users/lanodan").unwrap();
221 let person2 = ApubPerson::from_apub(&pleroma_person, &context, &pleroma_url, &mut 0)
224 let json = file_to_json_object("assets/lemmy-private-message.json");
225 let mut request_counter = 0;
226 let pm = ApubPrivateMessage::from_apub(&json, &context, &url, &mut request_counter)
230 assert_eq!(pm.ap_id.clone().into_inner(), url);
231 assert_eq!(pm.content.len(), 4);
232 assert_eq!(request_counter, 0);
234 let to_apub = pm.to_apub(context.pool()).await.unwrap();
235 assert_json_include!(actual: json, expected: to_apub);
237 PrivateMessage::delete(&*context.pool().get().unwrap(), pm.id).unwrap();
238 Person::delete(&*context.pool().get().unwrap(), person1.id).unwrap();
239 Person::delete(&*context.pool().get().unwrap(), person2.id).unwrap();