]> Untitled Git - lemmy.git/blob - crates/apub/src/objects/person.rs
Merge pull request #1850 from LemmyNet/refactor-apub
[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::{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, FromApub, ToApub},
20   values::{MediaTypeHtml, MediaTypeMarkdown},
21   verify::verify_domains_match,
22 };
23 use lemmy_db_schema::{
24   naive_now,
25   source::person::{Person as DbPerson, PersonForm},
26   DbPool,
27 };
28 use lemmy_utils::{
29   utils::{check_slurs, check_slurs_opt, convert_datetime, markdown_to_html},
30   LemmyError,
31 };
32 use lemmy_websocket::LemmyContext;
33 use serde::{Deserialize, Serialize};
34 use serde_with::skip_serializing_none;
35 use std::ops::Deref;
36 use url::Url;
37
38 #[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)]
39 pub enum UserTypes {
40   Person,
41   Service,
42 }
43
44 #[skip_serializing_none]
45 #[derive(Clone, Debug, Deserialize, Serialize)]
46 #[serde(rename_all = "camelCase")]
47 pub struct Person {
48   #[serde(rename = "@context")]
49   context: OneOrMany<AnyBase>,
50   #[serde(rename = "type")]
51   kind: UserTypes,
52   id: Url,
53   /// username, set at account creation and can never be changed
54   preferred_username: String,
55   /// displayname (can be changed at any time)
56   name: Option<String>,
57   content: Option<String>,
58   media_type: Option<MediaTypeHtml>,
59   source: Option<Source>,
60   /// user avatar
61   icon: Option<ImageObject>,
62   /// user banner
63   image: Option<ImageObject>,
64   matrix_user_id: Option<String>,
65   inbox: Url,
66   /// mandatory field in activitypub, currently empty in lemmy
67   outbox: Url,
68   endpoints: Endpoints<Url>,
69   public_key: PublicKey,
70   published: DateTime<FixedOffset>,
71   updated: Option<DateTime<FixedOffset>>,
72   #[serde(flatten)]
73   unparsed: Unparsed,
74 }
75
76 // TODO: can generate this with a derive macro
77 impl Person {
78   pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> {
79     verify_domains_match(&self.id, expected_domain)?;
80     Ok(&self.id)
81   }
82 }
83
84 #[derive(Clone, Debug)]
85 pub struct ApubPerson(DbPerson);
86
87 impl Deref for ApubPerson {
88   type Target = DbPerson;
89   fn deref(&self) -> &Self::Target {
90     &self.0
91   }
92 }
93
94 impl From<DbPerson> for ApubPerson {
95   fn from(p: DbPerson) -> Self {
96     ApubPerson { 0: p }
97   }
98 }
99
100 #[async_trait::async_trait(?Send)]
101 impl ApubObject for ApubPerson {
102   type DataType = LemmyContext;
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
130 impl ActorType for ApubPerson {
131   fn is_local(&self) -> bool {
132     self.local
133   }
134   fn actor_id(&self) -> Url {
135     self.actor_id.to_owned().into_inner()
136   }
137   fn name(&self) -> String {
138     self.name.clone()
139   }
140
141   fn public_key(&self) -> Option<String> {
142     self.public_key.to_owned()
143   }
144
145   fn private_key(&self) -> Option<String> {
146     self.private_key.to_owned()
147   }
148
149   fn inbox_url(&self) -> Url {
150     self.inbox_url.clone().into()
151   }
152
153   fn shared_inbox_url(&self) -> Option<Url> {
154     self.shared_inbox_url.clone().map(|s| s.into_inner())
155   }
156 }
157
158 #[async_trait::async_trait(?Send)]
159 impl ToApub for ApubPerson {
160   type ApubType = Person;
161   type TombstoneType = Tombstone;
162   type DataType = DbPool;
163
164   async fn to_apub(&self, _pool: &DbPool) -> Result<Person, LemmyError> {
165     let kind = if self.bot_account {
166       UserTypes::Service
167     } else {
168       UserTypes::Person
169     };
170     let source = self.bio.clone().map(|bio| Source {
171       content: bio,
172       media_type: MediaTypeMarkdown::Markdown,
173     });
174     let icon = self.avatar.clone().map(|url| ImageObject {
175       kind: ImageType::Image,
176       url: url.into(),
177     });
178     let image = self.banner.clone().map(|url| ImageObject {
179       kind: ImageType::Image,
180       url: url.into(),
181     });
182
183     let person = Person {
184       context: lemmy_context(),
185       kind,
186       id: self.actor_id.to_owned().into_inner(),
187       preferred_username: self.name.clone(),
188       name: self.display_name.clone(),
189       content: self.bio.as_ref().map(|b| markdown_to_html(b)),
190       media_type: self.bio.as_ref().map(|_| MediaTypeHtml::Html),
191       source,
192       icon,
193       image,
194       matrix_user_id: self.matrix_user_id.clone(),
195       published: convert_datetime(self.published),
196       outbox: generate_outbox_url(&self.actor_id)?.into(),
197       endpoints: Endpoints {
198         shared_inbox: self.shared_inbox_url.clone().map(|s| s.into()),
199         ..Default::default()
200       },
201       public_key: self.get_public_key()?,
202       updated: self.updated.map(convert_datetime),
203       unparsed: Default::default(),
204       inbox: self.inbox_url.clone().into(),
205     };
206     Ok(person)
207   }
208   fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
209     unimplemented!()
210   }
211 }
212
213 #[async_trait::async_trait(?Send)]
214 impl FromApub for ApubPerson {
215   type ApubType = Person;
216   type DataType = LemmyContext;
217
218   async fn from_apub(
219     person: &Person,
220     context: &LemmyContext,
221     expected_domain: &Url,
222     _request_counter: &mut i32,
223   ) -> Result<ApubPerson, LemmyError> {
224     let actor_id = Some(person.id(expected_domain)?.clone().into());
225     let name = person.preferred_username.clone();
226     let display_name: Option<String> = person.name.clone();
227     let bio = person.source.clone().map(|s| s.content);
228     let shared_inbox = person.endpoints.shared_inbox.clone().map(|s| s.into());
229     let bot_account = match person.kind {
230       UserTypes::Person => false,
231       UserTypes::Service => true,
232     };
233
234     let slur_regex = &context.settings().slur_regex();
235     check_slurs(&name, slur_regex)?;
236     check_slurs_opt(&display_name, slur_regex)?;
237     check_slurs_opt(&bio, slur_regex)?;
238
239     check_is_apub_id_valid(&person.id, false, &context.settings())?;
240
241     let person_form = PersonForm {
242       name,
243       display_name: Some(display_name),
244       banned: None,
245       deleted: None,
246       avatar: Some(person.icon.clone().map(|i| i.url.into())),
247       banner: Some(person.image.clone().map(|i| i.url.into())),
248       published: Some(person.published.naive_local()),
249       updated: person.updated.map(|u| u.clone().naive_local()),
250       actor_id,
251       bio: Some(bio),
252       local: Some(false),
253       admin: Some(false),
254       bot_account: Some(bot_account),
255       private_key: None,
256       public_key: Some(Some(person.public_key.public_key_pem.clone())),
257       last_refreshed_at: Some(naive_now()),
258       inbox_url: Some(person.inbox.to_owned().into()),
259       shared_inbox_url: Some(shared_inbox),
260       matrix_user_id: Some(person.matrix_user_id.clone()),
261     };
262     let person = blocking(context.pool(), move |conn| {
263       DbPerson::upsert(conn, &person_form)
264     })
265     .await??;
266     Ok(person.into())
267   }
268 }