2 check_is_apub_id_valid,
3 context::lemmy_context,
5 objects::{ImageObject, Source},
10 chrono::NaiveDateTime,
11 object::{kind::ImageType, Tombstone},
12 primitives::OneOrMany,
15 use chrono::{DateTime, FixedOffset};
16 use lemmy_api_common::blocking;
18 signatures::PublicKey,
19 traits::{ActorType, ApubObject, FromApub, ToApub},
20 values::{MediaTypeHtml, MediaTypeMarkdown},
21 verify::verify_domains_match,
23 use lemmy_db_schema::{
25 source::person::{Person as DbPerson, PersonForm},
29 utils::{check_slurs, check_slurs_opt, convert_datetime, markdown_to_html},
32 use lemmy_websocket::LemmyContext;
33 use serde::{Deserialize, Serialize};
34 use serde_with::skip_serializing_none;
38 #[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)]
44 #[skip_serializing_none]
45 #[derive(Clone, Debug, Deserialize, Serialize)]
46 #[serde(rename_all = "camelCase")]
48 #[serde(rename = "@context")]
49 context: OneOrMany<AnyBase>,
50 #[serde(rename = "type")]
53 /// username, set at account creation and can never be changed
54 preferred_username: String,
55 /// displayname (can be changed at any time)
57 content: Option<String>,
58 media_type: Option<MediaTypeHtml>,
59 source: Option<Source>,
61 icon: Option<ImageObject>,
63 image: Option<ImageObject>,
64 matrix_user_id: Option<String>,
66 /// mandatory field in activitypub, currently empty in lemmy
68 endpoints: Endpoints<Url>,
69 public_key: PublicKey,
70 published: DateTime<FixedOffset>,
71 updated: Option<DateTime<FixedOffset>>,
76 // TODO: can generate this with a derive macro
78 pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> {
79 verify_domains_match(&self.id, expected_domain)?;
84 #[derive(Clone, Debug)]
85 pub struct ApubPerson(DbPerson);
87 impl Deref for ApubPerson {
88 type Target = DbPerson;
89 fn deref(&self) -> &Self::Target {
94 impl From<DbPerson> for ApubPerson {
95 fn from(p: DbPerson) -> Self {
100 #[async_trait::async_trait(?Send)]
101 impl ApubObject for ApubPerson {
102 type DataType = LemmyContext;
104 fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
105 Some(self.last_refreshed_at)
108 async fn read_from_apub_id(
110 context: &LemmyContext,
111 ) -> Result<Option<Self>, LemmyError> {
113 blocking(context.pool(), move |conn| {
114 DbPerson::read_from_apub_id(conn, object_id)
121 async fn delete(self, context: &LemmyContext) -> Result<(), LemmyError> {
122 blocking(context.pool(), move |conn| {
123 DbPerson::update_deleted(conn, self.id, true)
130 impl ActorType for ApubPerson {
131 fn is_local(&self) -> bool {
134 fn actor_id(&self) -> Url {
135 self.actor_id.to_owned().into_inner()
137 fn name(&self) -> String {
141 fn public_key(&self) -> Option<String> {
142 self.public_key.to_owned()
145 fn private_key(&self) -> Option<String> {
146 self.private_key.to_owned()
149 fn inbox_url(&self) -> Url {
150 self.inbox_url.clone().into()
153 fn shared_inbox_url(&self) -> Option<Url> {
154 self.shared_inbox_url.clone().map(|s| s.into_inner())
158 #[async_trait::async_trait(?Send)]
159 impl ToApub for ApubPerson {
160 type ApubType = Person;
161 type TombstoneType = Tombstone;
162 type DataType = DbPool;
164 async fn to_apub(&self, _pool: &DbPool) -> Result<Person, LemmyError> {
165 let kind = if self.bot_account {
170 let source = self.bio.clone().map(|bio| Source {
172 media_type: MediaTypeMarkdown::Markdown,
174 let icon = self.avatar.clone().map(|url| ImageObject {
175 kind: ImageType::Image,
178 let image = self.banner.clone().map(|url| ImageObject {
179 kind: ImageType::Image,
183 let person = Person {
184 context: lemmy_context(),
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),
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()),
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(),
208 fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
213 #[async_trait::async_trait(?Send)]
214 impl FromApub for ApubPerson {
215 type ApubType = Person;
216 type DataType = LemmyContext;
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,
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)?;
239 check_is_apub_id_valid(&person.id, false, &context.settings())?;
241 let person_form = PersonForm {
243 display_name: Some(display_name),
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()),
254 bot_account: Some(bot_account),
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()),
262 let person = blocking(context.pool(), move |conn| {
263 DbPerson::upsert(conn, &person_form)