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