]> Untitled Git - lemmy.git/blob - crates/apub/src/objects/community.rs
Merge branch 'main' into federated-moderation
[lemmy.git] / crates / apub / src / objects / community.rs
1 use crate::{
2   extensions::{context::lemmy_context, group_extensions::GroupExtension},
3   fetcher::{community::fetch_community_mods, person::get_or_fetch_and_upsert_person},
4   generate_moderators_url,
5   objects::{
6     check_object_domain,
7     create_tombstone,
8     get_object_from_apub,
9     get_source_markdown_value,
10     set_content_and_source,
11     FromApub,
12     FromApubToForm,
13     ToApub,
14   },
15   ActorType,
16   GroupExt,
17 };
18 use activitystreams::{
19   actor::{kind::GroupType, ApActor, Endpoints, Group},
20   base::BaseExt,
21   object::{ApObject, Image, Tombstone},
22   prelude::*,
23 };
24 use activitystreams_ext::Ext2;
25 use anyhow::Context;
26 use lemmy_api_structs::blocking;
27 use lemmy_db_queries::{DbPool, Joinable};
28 use lemmy_db_schema::{
29   naive_now,
30   source::community::{Community, CommunityForm, CommunityModerator, CommunityModeratorForm},
31   DbUrl,
32 };
33 use lemmy_db_views_actor::community_moderator_view::CommunityModeratorView;
34 use lemmy_utils::{
35   location_info,
36   utils::{check_slurs, check_slurs_opt, convert_datetime},
37   LemmyError,
38 };
39 use lemmy_websocket::LemmyContext;
40 use url::Url;
41
42 #[async_trait::async_trait(?Send)]
43 impl ToApub for Community {
44   type ApubType = GroupExt;
45
46   async fn to_apub(&self, _pool: &DbPool) -> Result<GroupExt, LemmyError> {
47     let mut group = ApObject::new(Group::new());
48     group
49       .set_many_contexts(lemmy_context()?)
50       .set_id(self.actor_id.to_owned().into())
51       .set_name(self.title.to_owned())
52       .set_published(convert_datetime(self.published));
53
54     if let Some(u) = self.updated.to_owned() {
55       group.set_updated(convert_datetime(u));
56     }
57     if let Some(d) = self.description.to_owned() {
58       set_content_and_source(&mut group, &d)?;
59     }
60
61     if let Some(icon_url) = &self.icon {
62       let mut image = Image::new();
63       image.set_url::<Url>(icon_url.to_owned().into());
64       group.set_icon(image.into_any_base()?);
65     }
66
67     if let Some(banner_url) = &self.banner {
68       let mut image = Image::new();
69       image.set_url::<Url>(banner_url.to_owned().into());
70       group.set_image(image.into_any_base()?);
71     }
72
73     let mut ap_actor = ApActor::new(self.inbox_url.clone().into(), group);
74     ap_actor
75       .set_preferred_username(self.name.to_owned())
76       .set_outbox(self.get_outbox_url()?)
77       .set_followers(self.followers_url.clone().into())
78       .set_endpoints(Endpoints {
79         shared_inbox: Some(self.get_shared_inbox_or_inbox_url()),
80         ..Default::default()
81       });
82
83     Ok(Ext2::new(
84       ap_actor,
85       GroupExtension::new(self.nsfw, generate_moderators_url(&self.actor_id)?.into())?,
86       self.get_public_key_ext()?,
87     ))
88   }
89
90   fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
91     create_tombstone(
92       self.deleted,
93       self.actor_id.to_owned().into(),
94       self.updated,
95       GroupType::Group,
96     )
97   }
98 }
99
100 #[async_trait::async_trait(?Send)]
101 impl FromApub for Community {
102   type ApubType = GroupExt;
103
104   /// Converts a `Group` to `Community`, inserts it into the database and updates moderators.
105   async fn from_apub(
106     group: &GroupExt,
107     context: &LemmyContext,
108     expected_domain: Url,
109     request_counter: &mut i32,
110     mod_action_allowed: bool,
111   ) -> Result<Community, LemmyError> {
112     let community: Community = get_object_from_apub(
113       group,
114       context,
115       expected_domain,
116       request_counter,
117       mod_action_allowed,
118     )
119     .await?;
120
121     let new_moderators = fetch_community_mods(context, group, request_counter).await?;
122     let community_id = community.id;
123     let current_moderators = blocking(context.pool(), move |conn| {
124       CommunityModeratorView::for_community(&conn, community_id)
125     })
126     .await??;
127     // Remove old mods from database which arent in the moderators collection anymore
128     for mod_user in &current_moderators {
129       if !new_moderators.contains(&&mod_user.moderator.actor_id.clone().into()) {
130         let community_moderator_form = CommunityModeratorForm {
131           community_id: mod_user.community.id,
132           person_id: mod_user.moderator.id,
133         };
134         blocking(context.pool(), move |conn| {
135           CommunityModerator::leave(conn, &community_moderator_form)
136         })
137         .await??;
138       }
139     }
140
141     // Add new mods to database which have been added to moderators collection
142     for mod_uri in new_moderators {
143       let mod_user = get_or_fetch_and_upsert_person(&mod_uri, context, request_counter).await?;
144       let current_mod_uris: Vec<DbUrl> = current_moderators
145         .clone()
146         .iter()
147         .map(|c| c.moderator.actor_id.clone())
148         .collect();
149       if !current_mod_uris.contains(&mod_user.actor_id) {
150         let community_moderator_form = CommunityModeratorForm {
151           community_id: community.id,
152           person_id: mod_user.id,
153         };
154         blocking(context.pool(), move |conn| {
155           CommunityModerator::join(conn, &community_moderator_form)
156         })
157         .await??;
158       }
159     }
160
161     Ok(community)
162   }
163 }
164
165 #[async_trait::async_trait(?Send)]
166 impl FromApubToForm<GroupExt> for CommunityForm {
167   async fn from_apub(
168     group: &GroupExt,
169     context: &LemmyContext,
170     expected_domain: Url,
171     request_counter: &mut i32,
172     _mod_action_allowed: bool,
173   ) -> Result<Self, LemmyError> {
174     let moderator_uris = fetch_community_mods(context, group, request_counter).await?;
175     let creator_uri = moderator_uris.first().context(location_info!())?;
176
177     let creator = get_or_fetch_and_upsert_person(creator_uri, context, request_counter).await?;
178     let name = group
179       .inner
180       .preferred_username()
181       .context(location_info!())?
182       .to_string();
183     let title = group
184       .inner
185       .name()
186       .context(location_info!())?
187       .as_one()
188       .context(location_info!())?
189       .as_xsd_string()
190       .context(location_info!())?
191       .to_string();
192
193     let description = get_source_markdown_value(group)?;
194
195     check_slurs(&name)?;
196     check_slurs(&title)?;
197     check_slurs_opt(&description)?;
198
199     let icon = match group.icon() {
200       Some(any_image) => Some(
201         Image::from_any_base(any_image.as_one().context(location_info!())?.clone())
202           .context(location_info!())?
203           .context(location_info!())?
204           .url()
205           .context(location_info!())?
206           .as_single_xsd_any_uri()
207           .map(|u| u.to_owned().into()),
208       ),
209       None => None,
210     };
211     let banner = match group.image() {
212       Some(any_image) => Some(
213         Image::from_any_base(any_image.as_one().context(location_info!())?.clone())
214           .context(location_info!())?
215           .context(location_info!())?
216           .url()
217           .context(location_info!())?
218           .as_single_xsd_any_uri()
219           .map(|u| u.to_owned().into()),
220       ),
221       None => None,
222     };
223     let shared_inbox = group
224       .inner
225       .endpoints()?
226       .map(|e| e.shared_inbox)
227       .flatten()
228       .map(|s| s.to_owned().into());
229
230     Ok(CommunityForm {
231       name,
232       title,
233       description,
234       creator_id: creator.id,
235       removed: None,
236       published: group.inner.published().map(|u| u.to_owned().naive_local()),
237       updated: group.inner.updated().map(|u| u.to_owned().naive_local()),
238       deleted: None,
239       nsfw: group.ext_one.sensitive.unwrap_or(false),
240       actor_id: Some(check_object_domain(group, expected_domain)?),
241       local: false,
242       private_key: None,
243       public_key: Some(group.ext_two.to_owned().public_key.public_key_pem),
244       last_refreshed_at: Some(naive_now()),
245       icon,
246       banner,
247       followers_url: Some(
248         group
249           .inner
250           .followers()?
251           .context(location_info!())?
252           .to_owned()
253           .into(),
254       ),
255       inbox_url: Some(group.inner.inbox()?.to_owned().into()),
256       shared_inbox_url: Some(shared_inbox),
257     })
258   }
259 }