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