]> Untitled Git - lemmy.git/blob - crates/apub/src/objects/person.rs
Upgrade activitypub_federation to 0.2.0, add setting federation.debug (#2300)
[lemmy.git] / crates / apub / src / objects / person.rs
1 use crate::{
2   check_apub_id_valid_with_strictness,
3   generate_outbox_url,
4   objects::{instance::fetch_instance_actor_for_object, read_from_string_or_source_opt},
5   protocol::{
6     objects::{
7       person::{Person, UserTypes},
8       Endpoints,
9     },
10     ImageObject,
11     Source,
12   },
13   ActorType,
14 };
15 use activitypub_federation::{
16   core::object_id::ObjectId,
17   traits::{Actor, ApubObject},
18   utils::verify_domains_match,
19 };
20 use chrono::NaiveDateTime;
21 use lemmy_api_common::utils::blocking;
22 use lemmy_db_schema::{
23   source::person::{Person as DbPerson, PersonForm},
24   traits::ApubActor,
25   utils::naive_now,
26 };
27 use lemmy_utils::{
28   error::LemmyError,
29   utils::{check_slurs, check_slurs_opt, convert_datetime, markdown_to_html},
30 };
31 use lemmy_websocket::LemmyContext;
32 use std::ops::Deref;
33 use url::Url;
34
35 #[derive(Clone, Debug, PartialEq)]
36 pub struct ApubPerson(DbPerson);
37
38 impl Deref for ApubPerson {
39   type Target = DbPerson;
40   fn deref(&self) -> &Self::Target {
41     &self.0
42   }
43 }
44
45 impl From<DbPerson> for ApubPerson {
46   fn from(p: DbPerson) -> Self {
47     ApubPerson(p)
48   }
49 }
50
51 #[async_trait::async_trait(?Send)]
52 impl ApubObject for ApubPerson {
53   type DataType = LemmyContext;
54   type ApubType = Person;
55   type DbType = DbPerson;
56   type Error = LemmyError;
57
58   fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
59     Some(self.last_refreshed_at)
60   }
61
62   #[tracing::instrument(skip_all)]
63   async fn read_from_apub_id(
64     object_id: Url,
65     context: &LemmyContext,
66   ) -> Result<Option<Self>, LemmyError> {
67     Ok(
68       blocking(context.pool(), move |conn| {
69         DbPerson::read_from_apub_id(conn, &object_id.into())
70       })
71       .await??
72       .map(Into::into),
73     )
74   }
75
76   #[tracing::instrument(skip_all)]
77   async fn delete(self, context: &LemmyContext) -> Result<(), LemmyError> {
78     blocking(context.pool(), move |conn| {
79       DbPerson::update_deleted(conn, self.id, true)
80     })
81     .await??;
82     Ok(())
83   }
84
85   #[tracing::instrument(skip_all)]
86   async fn into_apub(self, _pool: &LemmyContext) -> Result<Person, LemmyError> {
87     let kind = if self.bot_account {
88       UserTypes::Service
89     } else {
90       UserTypes::Person
91     };
92
93     let person = Person {
94       kind,
95       id: ObjectId::new(self.actor_id.clone()),
96       preferred_username: self.name.clone(),
97       name: self.display_name.clone(),
98       summary: self.bio.as_ref().map(|b| markdown_to_html(b)),
99       source: self.bio.clone().map(Source::new),
100       icon: self.avatar.clone().map(ImageObject::new),
101       image: self.banner.clone().map(ImageObject::new),
102       matrix_user_id: self.matrix_user_id.clone(),
103       published: Some(convert_datetime(self.published)),
104       outbox: generate_outbox_url(&self.actor_id)?.into(),
105       endpoints: self.shared_inbox_url.clone().map(|s| Endpoints {
106         shared_inbox: s.into(),
107       }),
108       public_key: self.get_public_key(),
109       updated: self.updated.map(convert_datetime),
110       inbox: self.inbox_url.clone().into(),
111     };
112     Ok(person)
113   }
114
115   #[tracing::instrument(skip_all)]
116   async fn verify(
117     person: &Person,
118     expected_domain: &Url,
119     context: &LemmyContext,
120     _request_counter: &mut i32,
121   ) -> Result<(), LemmyError> {
122     verify_domains_match(person.id.inner(), expected_domain)?;
123     check_apub_id_valid_with_strictness(person.id.inner(), false, &context.settings())?;
124
125     let slur_regex = &context.settings().slur_regex();
126     check_slurs(&person.preferred_username, slur_regex)?;
127     check_slurs_opt(&person.name, slur_regex)?;
128     let bio = read_from_string_or_source_opt(&person.summary, &None, &person.source);
129     check_slurs_opt(&bio, slur_regex)?;
130     Ok(())
131   }
132
133   #[tracing::instrument(skip_all)]
134   async fn from_apub(
135     person: Person,
136     context: &LemmyContext,
137     request_counter: &mut i32,
138   ) -> Result<ApubPerson, LemmyError> {
139     let person_form = PersonForm {
140       name: person.preferred_username,
141       display_name: Some(person.name),
142       banned: None,
143       ban_expires: None,
144       deleted: None,
145       avatar: Some(person.icon.map(|i| i.url.into())),
146       banner: Some(person.image.map(|i| i.url.into())),
147       published: person.published.map(|u| u.naive_local()),
148       updated: person.updated.map(|u| u.naive_local()),
149       actor_id: Some(person.id.into()),
150       bio: Some(read_from_string_or_source_opt(
151         &person.summary,
152         &None,
153         &person.source,
154       )),
155       local: Some(false),
156       admin: Some(false),
157       bot_account: Some(person.kind == UserTypes::Service),
158       private_key: None,
159       public_key: person.public_key.public_key_pem,
160       last_refreshed_at: Some(naive_now()),
161       inbox_url: Some(person.inbox.into()),
162       shared_inbox_url: Some(person.endpoints.map(|e| e.shared_inbox.into())),
163       matrix_user_id: Some(person.matrix_user_id),
164     };
165     let person = blocking(context.pool(), move |conn| {
166       DbPerson::upsert(conn, &person_form)
167     })
168     .await??;
169
170     let actor_id = person.actor_id.clone().into();
171     fetch_instance_actor_for_object(actor_id, context, request_counter).await;
172
173     Ok(person.into())
174   }
175 }
176
177 impl ActorType for ApubPerson {
178   fn actor_id(&self) -> Url {
179     self.actor_id.to_owned().into()
180   }
181
182   fn private_key(&self) -> Option<String> {
183     self.private_key.to_owned()
184   }
185 }
186
187 impl Actor for ApubPerson {
188   fn public_key(&self) -> &str {
189     &self.public_key
190   }
191
192   fn inbox(&self) -> Url {
193     self.inbox_url.clone().into()
194   }
195
196   fn shared_inbox(&self) -> Option<Url> {
197     self.shared_inbox_url.clone().map(|s| s.into())
198   }
199 }
200
201 #[cfg(test)]
202 pub(crate) mod tests {
203   use super::*;
204   use crate::{
205     objects::{
206       instance::{tests::parse_lemmy_instance, ApubSite},
207       tests::init_context,
208     },
209     protocol::{objects::instance::Instance, tests::file_to_json_object},
210   };
211   use lemmy_db_schema::{source::site::Site, traits::Crud};
212   use serial_test::serial;
213
214   pub(crate) async fn parse_lemmy_person(context: &LemmyContext) -> (ApubPerson, ApubSite) {
215     let site = parse_lemmy_instance(context).await;
216     let json = file_to_json_object("assets/lemmy/objects/person.json").unwrap();
217     let url = Url::parse("https://enterprise.lemmy.ml/u/picard").unwrap();
218     let mut request_counter = 0;
219     ApubPerson::verify(&json, &url, context, &mut request_counter)
220       .await
221       .unwrap();
222     let person = ApubPerson::from_apub(json, context, &mut request_counter)
223       .await
224       .unwrap();
225     assert_eq!(request_counter, 0);
226     (person, site)
227   }
228
229   #[actix_rt::test]
230   #[serial]
231   async fn test_parse_lemmy_person() {
232     let context = init_context();
233     let (person, site) = parse_lemmy_person(&context).await;
234
235     assert_eq!(person.display_name, Some("Jean-Luc Picard".to_string()));
236     assert!(!person.local);
237     assert_eq!(person.bio.as_ref().unwrap().len(), 39);
238
239     DbPerson::delete(&*context.pool().get().unwrap(), person.id).unwrap();
240     Site::delete(&*context.pool().get().unwrap(), site.id).unwrap();
241   }
242
243   #[actix_rt::test]
244   #[serial]
245   async fn test_parse_pleroma_person() {
246     let context = init_context();
247
248     // create and parse a fake pleroma instance actor, to avoid network request during test
249     let mut json: Instance = file_to_json_object("assets/lemmy/objects/instance.json").unwrap();
250     let id = Url::parse("https://queer.hacktivis.me/").unwrap();
251     json.id = ObjectId::new(id);
252     let mut request_counter = 0;
253     let site = ApubSite::from_apub(json, &context, &mut request_counter)
254       .await
255       .unwrap();
256
257     let json = file_to_json_object("assets/pleroma/objects/person.json").unwrap();
258     let url = Url::parse("https://queer.hacktivis.me/users/lanodan").unwrap();
259     let mut request_counter = 0;
260     ApubPerson::verify(&json, &url, &context, &mut request_counter)
261       .await
262       .unwrap();
263     let person = ApubPerson::from_apub(json, &context, &mut request_counter)
264       .await
265       .unwrap();
266
267     assert_eq!(person.actor_id, url.into());
268     assert_eq!(person.name, "lanodan");
269     assert!(!person.local);
270     assert_eq!(request_counter, 0);
271     assert_eq!(person.bio.as_ref().unwrap().len(), 873);
272
273     DbPerson::delete(&*context.pool().get().unwrap(), person.id).unwrap();
274     Site::delete(&*context.pool().get().unwrap(), site.id).unwrap();
275   }
276 }