]> Untitled Git - lemmy.git/blob - crates/apub/src/objects/person.rs
Rewrite collections to use new fetcher (#1861)
[lemmy.git] / crates / apub / src / objects / person.rs
1 use crate::{
2   check_is_apub_id_valid,
3   context::lemmy_context,
4   generate_outbox_url,
5   objects::{get_summary_from_string_or_source, ImageObject, Source},
6 };
7 use activitystreams::{
8   actor::Endpoints,
9   base::AnyBase,
10   chrono::NaiveDateTime,
11   object::{kind::ImageType, Tombstone},
12   primitives::OneOrMany,
13   unparsed::Unparsed,
14 };
15 use chrono::{DateTime, FixedOffset};
16 use lemmy_api_common::blocking;
17 use lemmy_apub_lib::{
18   signatures::PublicKey,
19   traits::{ActorType, ApubObject},
20   values::MediaTypeMarkdown,
21   verify::verify_domains_match,
22 };
23 use lemmy_db_schema::{
24   naive_now,
25   source::person::{Person as DbPerson, PersonForm},
26 };
27 use lemmy_utils::{
28   utils::{check_slurs, check_slurs_opt, convert_datetime, markdown_to_html},
29   LemmyError,
30 };
31 use lemmy_websocket::LemmyContext;
32 use serde::{Deserialize, Serialize};
33 use serde_with::skip_serializing_none;
34 use std::ops::Deref;
35 use url::Url;
36
37 #[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)]
38 pub enum UserTypes {
39   Person,
40   Service,
41 }
42
43 #[skip_serializing_none]
44 #[derive(Clone, Debug, Deserialize, Serialize)]
45 #[serde(rename_all = "camelCase")]
46 pub struct Person {
47   #[serde(rename = "@context")]
48   context: OneOrMany<AnyBase>,
49   #[serde(rename = "type")]
50   kind: UserTypes,
51   id: Url,
52   /// username, set at account creation and can never be changed
53   preferred_username: String,
54   /// displayname (can be changed at any time)
55   name: Option<String>,
56   summary: Option<String>,
57   source: Option<Source>,
58   /// user avatar
59   icon: Option<ImageObject>,
60   /// user banner
61   image: Option<ImageObject>,
62   matrix_user_id: Option<String>,
63   inbox: Url,
64   /// mandatory field in activitypub, currently empty in lemmy
65   outbox: Url,
66   endpoints: Endpoints<Url>,
67   public_key: PublicKey,
68   published: Option<DateTime<FixedOffset>>,
69   updated: Option<DateTime<FixedOffset>>,
70   #[serde(flatten)]
71   unparsed: Unparsed,
72 }
73
74 // TODO: can generate this with a derive macro
75 impl Person {
76   pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> {
77     verify_domains_match(&self.id, expected_domain)?;
78     Ok(&self.id)
79   }
80 }
81
82 #[derive(Clone, Debug, PartialEq)]
83 pub struct ApubPerson(DbPerson);
84
85 impl Deref for ApubPerson {
86   type Target = DbPerson;
87   fn deref(&self) -> &Self::Target {
88     &self.0
89   }
90 }
91
92 impl From<DbPerson> for ApubPerson {
93   fn from(p: DbPerson) -> Self {
94     ApubPerson { 0: p }
95   }
96 }
97
98 #[async_trait::async_trait(?Send)]
99 impl ApubObject for ApubPerson {
100   type DataType = LemmyContext;
101   type ApubType = Person;
102   type TombstoneType = Tombstone;
103
104   fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
105     Some(self.last_refreshed_at)
106   }
107
108   async fn read_from_apub_id(
109     object_id: Url,
110     context: &LemmyContext,
111   ) -> Result<Option<Self>, LemmyError> {
112     Ok(
113       blocking(context.pool(), move |conn| {
114         DbPerson::read_from_apub_id(conn, object_id)
115       })
116       .await??
117       .map(Into::into),
118     )
119   }
120
121   async fn delete(self, context: &LemmyContext) -> Result<(), LemmyError> {
122     blocking(context.pool(), move |conn| {
123       DbPerson::update_deleted(conn, self.id, true)
124     })
125     .await??;
126     Ok(())
127   }
128
129   async fn to_apub(&self, _pool: &LemmyContext) -> Result<Person, LemmyError> {
130     let kind = if self.bot_account {
131       UserTypes::Service
132     } else {
133       UserTypes::Person
134     };
135     let source = self.bio.clone().map(|bio| Source {
136       content: bio,
137       media_type: MediaTypeMarkdown::Markdown,
138     });
139     let icon = self.avatar.clone().map(|url| ImageObject {
140       kind: ImageType::Image,
141       url: url.into(),
142     });
143     let image = self.banner.clone().map(|url| ImageObject {
144       kind: ImageType::Image,
145       url: url.into(),
146     });
147
148     let person = Person {
149       context: lemmy_context(),
150       kind,
151       id: self.actor_id.to_owned().into_inner(),
152       preferred_username: self.name.clone(),
153       name: self.display_name.clone(),
154       summary: self.bio.as_ref().map(|b| markdown_to_html(b)),
155       source,
156       icon,
157       image,
158       matrix_user_id: self.matrix_user_id.clone(),
159       published: Some(convert_datetime(self.published)),
160       outbox: generate_outbox_url(&self.actor_id)?.into(),
161       endpoints: Endpoints {
162         shared_inbox: self.shared_inbox_url.clone().map(|s| s.into()),
163         ..Default::default()
164       },
165       public_key: self.get_public_key()?,
166       updated: self.updated.map(convert_datetime),
167       unparsed: Default::default(),
168       inbox: self.inbox_url.clone().into(),
169     };
170     Ok(person)
171   }
172
173   fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
174     unimplemented!()
175   }
176
177   async fn from_apub(
178     person: &Person,
179     context: &LemmyContext,
180     expected_domain: &Url,
181     _request_counter: &mut i32,
182   ) -> Result<ApubPerson, LemmyError> {
183     let actor_id = Some(person.id(expected_domain)?.clone().into());
184     let name = person.preferred_username.clone();
185     let display_name: Option<String> = person.name.clone();
186     let bio = get_summary_from_string_or_source(&person.summary, &person.source);
187     let shared_inbox = person.endpoints.shared_inbox.clone().map(|s| s.into());
188     let bot_account = match person.kind {
189       UserTypes::Person => false,
190       UserTypes::Service => true,
191     };
192
193     let slur_regex = &context.settings().slur_regex();
194     check_slurs(&name, slur_regex)?;
195     check_slurs_opt(&display_name, slur_regex)?;
196     check_slurs_opt(&bio, slur_regex)?;
197
198     check_is_apub_id_valid(&person.id, false, &context.settings())?;
199
200     let person_form = PersonForm {
201       name,
202       display_name: Some(display_name),
203       banned: None,
204       deleted: None,
205       avatar: Some(person.icon.clone().map(|i| i.url.into())),
206       banner: Some(person.image.clone().map(|i| i.url.into())),
207       published: person.published.map(|u| u.clone().naive_local()),
208       updated: person.updated.map(|u| u.clone().naive_local()),
209       actor_id,
210       bio: Some(bio),
211       local: Some(false),
212       admin: Some(false),
213       bot_account: Some(bot_account),
214       private_key: None,
215       public_key: Some(Some(person.public_key.public_key_pem.clone())),
216       last_refreshed_at: Some(naive_now()),
217       inbox_url: Some(person.inbox.to_owned().into()),
218       shared_inbox_url: Some(shared_inbox),
219       matrix_user_id: Some(person.matrix_user_id.clone()),
220     };
221     let person = blocking(context.pool(), move |conn| {
222       DbPerson::upsert(conn, &person_form)
223     })
224     .await??;
225     Ok(person.into())
226   }
227 }
228
229 impl ActorType for ApubPerson {
230   fn is_local(&self) -> bool {
231     self.local
232   }
233   fn actor_id(&self) -> Url {
234     self.actor_id.to_owned().into_inner()
235   }
236   fn name(&self) -> String {
237     self.name.clone()
238   }
239
240   fn public_key(&self) -> Option<String> {
241     self.public_key.to_owned()
242   }
243
244   fn private_key(&self) -> Option<String> {
245     self.private_key.to_owned()
246   }
247
248   fn inbox_url(&self) -> Url {
249     self.inbox_url.clone().into()
250   }
251
252   fn shared_inbox_url(&self) -> Option<Url> {
253     self.shared_inbox_url.clone().map(|s| s.into_inner())
254   }
255 }
256
257 #[cfg(test)]
258 mod tests {
259   use super::*;
260   use crate::objects::tests::{file_to_json_object, init_context};
261   use assert_json_diff::assert_json_include;
262   use lemmy_db_schema::traits::Crud;
263   use serial_test::serial;
264
265   #[actix_rt::test]
266   #[serial]
267   async fn test_parse_lemmy_person() {
268     let context = init_context();
269     let json = file_to_json_object("assets/lemmy-person.json");
270     let url = Url::parse("https://enterprise.lemmy.ml/u/picard").unwrap();
271     let mut request_counter = 0;
272     let person = ApubPerson::from_apub(&json, &context, &url, &mut request_counter)
273       .await
274       .unwrap();
275
276     assert_eq!(person.actor_id.clone().into_inner(), url);
277     assert_eq!(person.display_name, Some("Jean-Luc Picard".to_string()));
278     assert!(person.public_key.is_some());
279     assert!(!person.local);
280     assert_eq!(person.bio.as_ref().unwrap().len(), 39);
281     assert_eq!(request_counter, 0);
282
283     let to_apub = person.to_apub(&context).await.unwrap();
284     assert_json_include!(actual: json, expected: to_apub);
285
286     DbPerson::delete(&*context.pool().get().unwrap(), person.id).unwrap();
287   }
288
289   #[actix_rt::test]
290   #[serial]
291   async fn test_parse_pleroma_person() {
292     let context = init_context();
293     let json = file_to_json_object("assets/pleroma-person.json");
294     let url = Url::parse("https://queer.hacktivis.me/users/lanodan").unwrap();
295     let mut request_counter = 0;
296     let person = ApubPerson::from_apub(&json, &context, &url, &mut request_counter)
297       .await
298       .unwrap();
299
300     assert_eq!(person.actor_id.clone().into_inner(), url);
301     assert_eq!(person.name, "lanodan");
302     assert!(person.public_key.is_some());
303     assert!(!person.local);
304     assert_eq!(request_counter, 0);
305     assert_eq!(person.bio.as_ref().unwrap().len(), 873);
306
307     DbPerson::delete(&*context.pool().get().unwrap(), person.id).unwrap();
308   }
309 }