]> Untitled Git - lemmy.git/blob - src/apub/community.rs
routes.api: fix get_captcha endpoint (#1135)
[lemmy.git] / src / apub / community.rs
1 use crate::{
2   apub::{
3     activities::generate_activity_id,
4     activity_queue::send_activity,
5     check_actor_domain,
6     create_apub_response,
7     create_apub_tombstone_response,
8     create_tombstone,
9     extensions::group_extensions::GroupExtension,
10     fetcher::{get_or_fetch_and_upsert_actor, get_or_fetch_and_upsert_user},
11     insert_activity,
12     ActorType,
13     FromApub,
14     GroupExt,
15     ToApub,
16   },
17   DbPool,
18   LemmyContext,
19 };
20 use activitystreams::{
21   activity::{
22     kind::{AcceptType, AnnounceType, DeleteType, LikeType, RemoveType, UndoType},
23     Accept,
24     Announce,
25     Delete,
26     Follow,
27     Remove,
28     Undo,
29   },
30   actor::{kind::GroupType, ApActor, Endpoints, Group},
31   base::{AnyBase, BaseExt},
32   collection::{OrderedCollection, UnorderedCollection},
33   object::{Image, Tombstone},
34   prelude::*,
35   public,
36 };
37 use activitystreams_ext::Ext2;
38 use actix_web::{body::Body, web, HttpResponse};
39 use anyhow::Context;
40 use itertools::Itertools;
41 use lemmy_api_structs::blocking;
42 use lemmy_db::{
43   community::{Community, CommunityForm},
44   community_view::{CommunityFollowerView, CommunityModeratorView},
45   naive_now,
46   post::Post,
47   user::User_,
48 };
49 use lemmy_utils::{
50   apub::get_apub_protocol_string,
51   location_info,
52   utils::{check_slurs, check_slurs_opt, convert_datetime},
53   LemmyError,
54 };
55 use serde::Deserialize;
56 use url::Url;
57
58 #[derive(Deserialize)]
59 pub struct CommunityQuery {
60   community_name: String,
61 }
62
63 #[async_trait::async_trait(?Send)]
64 impl ToApub for Community {
65   type Response = GroupExt;
66
67   // Turn a Lemmy Community into an ActivityPub group that can be sent out over the network.
68   async fn to_apub(&self, pool: &DbPool) -> Result<GroupExt, LemmyError> {
69     // The attributed to, is an ordered vector with the creator actor_ids first,
70     // then the rest of the moderators
71     // TODO Technically the instance admins can mod the community, but lets
72     // ignore that for now
73     let id = self.id;
74     let moderators = blocking(pool, move |conn| {
75       CommunityModeratorView::for_community(&conn, id)
76     })
77     .await??;
78     let moderators: Vec<String> = moderators.into_iter().map(|m| m.user_actor_id).collect();
79
80     let mut group = Group::new();
81     group
82       .set_context(activitystreams::context())
83       .set_id(Url::parse(&self.actor_id)?)
84       .set_name(self.name.to_owned())
85       .set_published(convert_datetime(self.published))
86       .set_many_attributed_tos(moderators);
87
88     if let Some(u) = self.updated.to_owned() {
89       group.set_updated(convert_datetime(u));
90     }
91     if let Some(d) = self.description.to_owned() {
92       // TODO: this should be html, also add source field with raw markdown
93       //       -> same for post.content and others
94       group.set_content(d);
95     }
96
97     if let Some(icon) = &self.icon {
98       let mut image = Image::new();
99       image.set_url(icon.to_owned());
100       group.set_icon(image.into_any_base()?);
101     }
102
103     if let Some(banner_url) = &self.banner {
104       let mut image = Image::new();
105       image.set_url(banner_url.to_owned());
106       group.set_image(image.into_any_base()?);
107     }
108
109     let mut ap_actor = ApActor::new(self.get_inbox_url()?, group);
110     ap_actor
111       .set_preferred_username(self.title.to_owned())
112       .set_outbox(self.get_outbox_url()?)
113       .set_followers(self.get_followers_url()?)
114       .set_following(self.get_following_url().parse()?)
115       .set_liked(self.get_liked_url().parse()?)
116       .set_endpoints(Endpoints {
117         shared_inbox: Some(self.get_shared_inbox_url()?),
118         ..Default::default()
119       });
120
121     let nsfw = self.nsfw;
122     let category_id = self.category_id;
123     let group_extension = blocking(pool, move |conn| {
124       GroupExtension::new(conn, category_id, nsfw)
125     })
126     .await??;
127
128     Ok(Ext2::new(
129       ap_actor,
130       group_extension,
131       self.get_public_key_ext()?,
132     ))
133   }
134
135   fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
136     create_tombstone(self.deleted, &self.actor_id, self.updated, GroupType::Group)
137   }
138 }
139
140 #[async_trait::async_trait(?Send)]
141 impl ActorType for Community {
142   fn actor_id_str(&self) -> String {
143     self.actor_id.to_owned()
144   }
145
146   fn public_key(&self) -> Option<String> {
147     self.public_key.to_owned()
148   }
149   fn private_key(&self) -> Option<String> {
150     self.private_key.to_owned()
151   }
152
153   /// As a local community, accept the follow request from a remote user.
154   async fn send_accept_follow(
155     &self,
156     follow: Follow,
157     context: &LemmyContext,
158   ) -> Result<(), LemmyError> {
159     let actor_uri = follow
160       .actor()?
161       .as_single_xsd_any_uri()
162       .context(location_info!())?;
163     let actor = get_or_fetch_and_upsert_actor(actor_uri, context).await?;
164
165     let mut accept = Accept::new(self.actor_id.to_owned(), follow.into_any_base()?);
166     let to = actor.get_inbox_url()?;
167     accept
168       .set_context(activitystreams::context())
169       .set_id(generate_activity_id(AcceptType::Accept)?)
170       .set_to(to.clone());
171
172     insert_activity(self.creator_id, accept.clone(), true, context.pool()).await?;
173
174     send_activity(context.activity_queue(), accept, self, vec![to])?;
175     Ok(())
176   }
177
178   async fn send_delete(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
179     let group = self.to_apub(context.pool()).await?;
180
181     let mut delete = Delete::new(creator.actor_id.to_owned(), group.into_any_base()?);
182     delete
183       .set_context(activitystreams::context())
184       .set_id(generate_activity_id(DeleteType::Delete)?)
185       .set_to(public())
186       .set_many_ccs(vec![self.get_followers_url()?]);
187
188     insert_activity(self.creator_id, delete.clone(), true, context.pool()).await?;
189
190     let inboxes = self.get_follower_inboxes(context.pool()).await?;
191
192     // Note: For an accept, since it was automatic, no one pushed a button,
193     // the community was the actor.
194     // But for delete, the creator is the actor, and does the signing
195     send_activity(context.activity_queue(), delete, creator, inboxes)?;
196     Ok(())
197   }
198
199   async fn send_undo_delete(
200     &self,
201     creator: &User_,
202     context: &LemmyContext,
203   ) -> Result<(), LemmyError> {
204     let group = self.to_apub(context.pool()).await?;
205
206     let mut delete = Delete::new(creator.actor_id.to_owned(), group.into_any_base()?);
207     delete
208       .set_context(activitystreams::context())
209       .set_id(generate_activity_id(DeleteType::Delete)?)
210       .set_to(public())
211       .set_many_ccs(vec![self.get_followers_url()?]);
212
213     let mut undo = Undo::new(creator.actor_id.to_owned(), delete.into_any_base()?);
214     undo
215       .set_context(activitystreams::context())
216       .set_id(generate_activity_id(UndoType::Undo)?)
217       .set_to(public())
218       .set_many_ccs(vec![self.get_followers_url()?]);
219
220     insert_activity(self.creator_id, undo.clone(), true, context.pool()).await?;
221
222     let inboxes = self.get_follower_inboxes(context.pool()).await?;
223
224     // Note: For an accept, since it was automatic, no one pushed a button,
225     // the community was the actor.
226     // But for delete, the creator is the actor, and does the signing
227     send_activity(context.activity_queue(), undo, creator, inboxes)?;
228     Ok(())
229   }
230
231   async fn send_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
232     let group = self.to_apub(context.pool()).await?;
233
234     let mut remove = Remove::new(mod_.actor_id.to_owned(), group.into_any_base()?);
235     remove
236       .set_context(activitystreams::context())
237       .set_id(generate_activity_id(RemoveType::Remove)?)
238       .set_to(public())
239       .set_many_ccs(vec![self.get_followers_url()?]);
240
241     insert_activity(mod_.id, remove.clone(), true, context.pool()).await?;
242
243     let inboxes = self.get_follower_inboxes(context.pool()).await?;
244
245     // Note: For an accept, since it was automatic, no one pushed a button,
246     // the community was the actor.
247     // But for delete, the creator is the actor, and does the signing
248     send_activity(context.activity_queue(), remove, mod_, inboxes)?;
249     Ok(())
250   }
251
252   async fn send_undo_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
253     let group = self.to_apub(context.pool()).await?;
254
255     let mut remove = Remove::new(mod_.actor_id.to_owned(), group.into_any_base()?);
256     remove
257       .set_context(activitystreams::context())
258       .set_id(generate_activity_id(RemoveType::Remove)?)
259       .set_to(public())
260       .set_many_ccs(vec![self.get_followers_url()?]);
261
262     // Undo that fake activity
263     let mut undo = Undo::new(mod_.actor_id.to_owned(), remove.into_any_base()?);
264     undo
265       .set_context(activitystreams::context())
266       .set_id(generate_activity_id(LikeType::Like)?)
267       .set_to(public())
268       .set_many_ccs(vec![self.get_followers_url()?]);
269
270     insert_activity(mod_.id, undo.clone(), true, context.pool()).await?;
271
272     let inboxes = self.get_follower_inboxes(context.pool()).await?;
273
274     // Note: For an accept, since it was automatic, no one pushed a button,
275     // the community was the actor.
276     // But for remove , the creator is the actor, and does the signing
277     send_activity(context.activity_queue(), undo, mod_, inboxes)?;
278     Ok(())
279   }
280
281   /// For a given community, returns the inboxes of all followers.
282   ///
283   /// TODO: this function is very badly implemented, we should just store shared_inbox_url in
284   ///       CommunityFollowerView
285   async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<Url>, LemmyError> {
286     let id = self.id;
287
288     let inboxes = blocking(pool, move |conn| {
289       CommunityFollowerView::for_community(conn, id)
290     })
291     .await??;
292     let inboxes = inboxes
293       .into_iter()
294       .map(|u| -> Result<Url, LemmyError> {
295         let url = Url::parse(&u.user_actor_id)?;
296         let domain = url.domain().context(location_info!())?;
297         let port = if let Some(port) = url.port() {
298           format!(":{}", port)
299         } else {
300           "".to_string()
301         };
302         Ok(Url::parse(&format!(
303           "{}://{}{}/inbox",
304           get_apub_protocol_string(),
305           domain,
306           port,
307         ))?)
308       })
309       .filter_map(Result::ok)
310       .unique()
311       .collect();
312
313     Ok(inboxes)
314   }
315
316   async fn send_follow(
317     &self,
318     _follow_actor_id: &Url,
319     _context: &LemmyContext,
320   ) -> Result<(), LemmyError> {
321     unimplemented!()
322   }
323
324   async fn send_unfollow(
325     &self,
326     _follow_actor_id: &Url,
327     _context: &LemmyContext,
328   ) -> Result<(), LemmyError> {
329     unimplemented!()
330   }
331
332   fn user_id(&self) -> i32 {
333     self.creator_id
334   }
335 }
336
337 #[async_trait::async_trait(?Send)]
338 impl FromApub for CommunityForm {
339   type ApubType = GroupExt;
340
341   /// Parse an ActivityPub group received from another instance into a Lemmy community.
342   async fn from_apub(
343     group: &GroupExt,
344     context: &LemmyContext,
345     expected_domain: Option<Url>,
346   ) -> Result<Self, LemmyError> {
347     let creator_and_moderator_uris = group.inner.attributed_to().context(location_info!())?;
348     let creator_uri = creator_and_moderator_uris
349       .as_many()
350       .context(location_info!())?
351       .iter()
352       .next()
353       .context(location_info!())?
354       .as_xsd_any_uri()
355       .context(location_info!())?;
356
357     let creator = get_or_fetch_and_upsert_user(creator_uri, context).await?;
358     let name = group
359       .inner
360       .name()
361       .context(location_info!())?
362       .as_one()
363       .context(location_info!())?
364       .as_xsd_string()
365       .context(location_info!())?
366       .to_string();
367     let title = group
368       .inner
369       .preferred_username()
370       .context(location_info!())?
371       .to_string();
372     // TODO: should be parsed as html and tags like <script> removed (or use markdown source)
373     //       -> same for post.content etc
374     let description = group
375       .inner
376       .content()
377       .map(|s| s.as_single_xsd_string())
378       .flatten()
379       .map(|s| s.to_string());
380     check_slurs(&name)?;
381     check_slurs(&title)?;
382     check_slurs_opt(&description)?;
383
384     let icon = match group.icon() {
385       Some(any_image) => Some(
386         Image::from_any_base(any_image.as_one().context(location_info!())?.clone())
387           .context(location_info!())?
388           .context(location_info!())?
389           .url()
390           .context(location_info!())?
391           .as_single_xsd_any_uri()
392           .map(|u| u.to_string()),
393       ),
394       None => None,
395     };
396
397     let banner = match group.image() {
398       Some(any_image) => Some(
399         Image::from_any_base(any_image.as_one().context(location_info!())?.clone())
400           .context(location_info!())?
401           .context(location_info!())?
402           .url()
403           .context(location_info!())?
404           .as_single_xsd_any_uri()
405           .map(|u| u.to_string()),
406       ),
407       None => None,
408     };
409
410     Ok(CommunityForm {
411       name,
412       title,
413       description,
414       category_id: group.ext_one.category.identifier.parse::<i32>()?,
415       creator_id: creator.id,
416       removed: None,
417       published: group.inner.published().map(|u| u.to_owned().naive_local()),
418       updated: group.inner.updated().map(|u| u.to_owned().naive_local()),
419       deleted: None,
420       nsfw: group.ext_one.sensitive,
421       actor_id: Some(check_actor_domain(group, expected_domain)?),
422       local: false,
423       private_key: None,
424       public_key: Some(group.ext_two.to_owned().public_key.public_key_pem),
425       last_refreshed_at: Some(naive_now()),
426       icon,
427       banner,
428     })
429   }
430 }
431
432 /// Return the community json over HTTP.
433 pub async fn get_apub_community_http(
434   info: web::Path<CommunityQuery>,
435   context: web::Data<LemmyContext>,
436 ) -> Result<HttpResponse<Body>, LemmyError> {
437   let community = blocking(context.pool(), move |conn| {
438     Community::read_from_name(conn, &info.community_name)
439   })
440   .await??;
441
442   if !community.deleted {
443     let apub = community.to_apub(context.pool()).await?;
444
445     Ok(create_apub_response(&apub))
446   } else {
447     Ok(create_apub_tombstone_response(&community.to_tombstone()?))
448   }
449 }
450
451 /// Returns an empty followers collection, only populating the size (for privacy).
452 pub async fn get_apub_community_followers(
453   info: web::Path<CommunityQuery>,
454   context: web::Data<LemmyContext>,
455 ) -> Result<HttpResponse<Body>, LemmyError> {
456   let community = blocking(context.pool(), move |conn| {
457     Community::read_from_name(&conn, &info.community_name)
458   })
459   .await??;
460
461   let community_id = community.id;
462   let community_followers = blocking(context.pool(), move |conn| {
463     CommunityFollowerView::for_community(&conn, community_id)
464   })
465   .await??;
466
467   let mut collection = UnorderedCollection::new();
468   collection
469     .set_context(activitystreams::context())
470     .set_id(community.get_followers_url()?)
471     .set_total_items(community_followers.len() as u64);
472   Ok(create_apub_response(&collection))
473 }
474
475 pub async fn get_apub_community_outbox(
476   info: web::Path<CommunityQuery>,
477   context: web::Data<LemmyContext>,
478 ) -> Result<HttpResponse<Body>, LemmyError> {
479   let community = blocking(context.pool(), move |conn| {
480     Community::read_from_name(&conn, &info.community_name)
481   })
482   .await??;
483
484   let community_id = community.id;
485   let posts = blocking(context.pool(), move |conn| {
486     Post::list_for_community(conn, community_id)
487   })
488   .await??;
489
490   let mut pages: Vec<AnyBase> = vec![];
491   for p in posts {
492     pages.push(p.to_apub(context.pool()).await?.into_any_base()?);
493   }
494
495   let len = pages.len();
496   let mut collection = OrderedCollection::new();
497   collection
498     .set_many_items(pages)
499     .set_context(activitystreams::context())
500     .set_id(community.get_outbox_url()?)
501     .set_total_items(len as u64);
502   Ok(create_apub_response(&collection))
503 }
504
505 pub async fn do_announce(
506   activity: AnyBase,
507   community: &Community,
508   sender: &User_,
509   context: &LemmyContext,
510 ) -> Result<(), LemmyError> {
511   let mut announce = Announce::new(community.actor_id.to_owned(), activity);
512   announce
513     .set_context(activitystreams::context())
514     .set_id(generate_activity_id(AnnounceType::Announce)?)
515     .set_to(public())
516     .set_many_ccs(vec![community.get_followers_url()?]);
517
518   insert_activity(community.creator_id, announce.clone(), true, context.pool()).await?;
519
520   let mut to: Vec<Url> = community.get_follower_inboxes(context.pool()).await?;
521
522   // dont send to the local instance, nor to the instance where the activity originally came from,
523   // because that would result in a database error (same data inserted twice)
524   // this seems to be the "easiest" stable alternative for remove_item()
525   let sender_shared_inbox = sender.get_shared_inbox_url()?;
526   to.retain(|x| x != &sender_shared_inbox);
527   let community_shared_inbox = community.get_shared_inbox_url()?;
528   to.retain(|x| x != &community_shared_inbox);
529
530   send_activity(context.activity_queue(), announce, community, to)?;
531
532   Ok(())
533 }