]> Untitled Git - lemmy.git/blob - crates/apub/src/objects/community.rs
Rewrite fetcher (#1792)
[lemmy.git] / crates / apub / src / objects / community.rs
1 use crate::{
2   extensions::{context::lemmy_context, signatures::PublicKey},
3   fetcher::community::{fetch_community_outbox, update_community_mods},
4   generate_moderators_url,
5   objects::{create_tombstone, FromApub, ImageObject, Source, ToApub},
6   ActorType,
7 };
8 use activitystreams::{
9   actor::{kind::GroupType, Endpoints},
10   base::AnyBase,
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   values::{MediaTypeHtml, MediaTypeMarkdown},
19   verify_domains_match,
20 };
21 use lemmy_db_queries::{source::community::Community_, DbPool};
22 use lemmy_db_schema::{
23   naive_now,
24   source::community::{Community, CommunityForm},
25 };
26 use lemmy_utils::{
27   utils::{check_slurs, check_slurs_opt, convert_datetime, markdown_to_html},
28   LemmyError,
29 };
30 use lemmy_websocket::LemmyContext;
31 use serde::{Deserialize, Serialize};
32 use serde_with::skip_serializing_none;
33 use url::Url;
34
35 #[skip_serializing_none]
36 #[derive(Clone, Debug, Deserialize, Serialize)]
37 #[serde(rename_all = "camelCase")]
38 pub struct Group {
39   #[serde(rename = "@context")]
40   context: OneOrMany<AnyBase>,
41   #[serde(rename = "type")]
42   kind: GroupType,
43   id: Url,
44   /// username, set at account creation and can never be changed
45   preferred_username: String,
46   /// title (can be changed at any time)
47   name: String,
48   content: Option<String>,
49   media_type: Option<MediaTypeHtml>,
50   source: Option<Source>,
51   icon: Option<ImageObject>,
52   /// banner
53   image: Option<ImageObject>,
54   // lemmy extension
55   sensitive: Option<bool>,
56   // lemmy extension
57   pub(crate) moderators: Option<Url>,
58   inbox: Url,
59   pub(crate) outbox: Url,
60   followers: Url,
61   endpoints: Endpoints<Url>,
62   public_key: PublicKey,
63   published: DateTime<FixedOffset>,
64   updated: Option<DateTime<FixedOffset>>,
65   #[serde(flatten)]
66   unparsed: Unparsed,
67 }
68
69 impl Group {
70   pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> {
71     verify_domains_match(&self.id, expected_domain)?;
72     Ok(&self.id)
73   }
74   pub(crate) async fn from_apub_to_form(
75     group: &Group,
76     expected_domain: &Url,
77   ) -> Result<CommunityForm, LemmyError> {
78     let actor_id = Some(group.id(expected_domain)?.clone().into());
79     let name = group.preferred_username.clone();
80     let title = group.name.clone();
81     let description = group.source.clone().map(|s| s.content);
82     let shared_inbox = group.endpoints.shared_inbox.clone().map(|s| s.into());
83
84     check_slurs(&name)?;
85     check_slurs(&title)?;
86     check_slurs_opt(&description)?;
87
88     Ok(CommunityForm {
89       name,
90       title,
91       description,
92       removed: None,
93       published: Some(group.published.naive_local()),
94       updated: group.updated.map(|u| u.naive_local()),
95       deleted: None,
96       nsfw: Some(group.sensitive.unwrap_or(false)),
97       actor_id,
98       local: Some(false),
99       private_key: None,
100       public_key: Some(group.public_key.public_key_pem.clone()),
101       last_refreshed_at: Some(naive_now()),
102       icon: Some(group.icon.clone().map(|i| i.url.into())),
103       banner: Some(group.image.clone().map(|i| i.url.into())),
104       followers_url: Some(group.followers.clone().into()),
105       inbox_url: Some(group.inbox.clone().into()),
106       shared_inbox_url: Some(shared_inbox),
107     })
108   }
109 }
110
111 #[async_trait::async_trait(?Send)]
112 impl ToApub for Community {
113   type ApubType = Group;
114
115   async fn to_apub(&self, _pool: &DbPool) -> Result<Group, LemmyError> {
116     let source = self.description.clone().map(|bio| Source {
117       content: bio,
118       media_type: MediaTypeMarkdown::Markdown,
119     });
120     let icon = self.icon.clone().map(|url| ImageObject {
121       kind: ImageType::Image,
122       url: url.into(),
123     });
124     let image = self.banner.clone().map(|url| ImageObject {
125       kind: ImageType::Image,
126       url: url.into(),
127     });
128
129     let group = Group {
130       context: lemmy_context(),
131       kind: GroupType::Group,
132       id: self.actor_id(),
133       preferred_username: self.name.clone(),
134       name: self.title.clone(),
135       content: self.description.as_ref().map(|b| markdown_to_html(b)),
136       media_type: self.description.as_ref().map(|_| MediaTypeHtml::Html),
137       source,
138       icon,
139       image,
140       sensitive: Some(self.nsfw),
141       moderators: Some(generate_moderators_url(&self.actor_id)?.into()),
142       inbox: self.inbox_url.clone().into(),
143       outbox: self.get_outbox_url()?,
144       followers: self.followers_url.clone().into(),
145       endpoints: Endpoints {
146         shared_inbox: self.shared_inbox_url.clone().map(|s| s.into()),
147         ..Default::default()
148       },
149       public_key: self.get_public_key()?,
150       published: convert_datetime(self.published),
151       updated: self.updated.map(convert_datetime),
152       unparsed: Default::default(),
153     };
154     Ok(group)
155   }
156
157   fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
158     create_tombstone(
159       self.deleted,
160       self.actor_id.to_owned().into(),
161       self.updated,
162       GroupType::Group,
163     )
164   }
165 }
166
167 #[async_trait::async_trait(?Send)]
168 impl FromApub for Community {
169   type ApubType = Group;
170
171   /// Converts a `Group` to `Community`, inserts it into the database and updates moderators.
172   async fn from_apub(
173     group: &Group,
174     context: &LemmyContext,
175     expected_domain: &Url,
176     request_counter: &mut i32,
177   ) -> Result<Community, LemmyError> {
178     let form = Group::from_apub_to_form(group, expected_domain).await?;
179
180     let community = blocking(context.pool(), move |conn| Community::upsert(conn, &form)).await??;
181     update_community_mods(group, &community, context, request_counter).await?;
182
183     // TODO: doing this unconditionally might cause infinite loop for some reason
184     fetch_community_outbox(context, &group.outbox, request_counter).await?;
185
186     Ok(community)
187   }
188 }