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