2 api::{check_slurs, check_slurs_opt},
4 activities::{generate_activity_id, send_activity},
7 create_apub_tombstone_response,
9 extensions::group_extensions::GroupExtension,
10 fetcher::get_or_fetch_and_upsert_user,
23 use activitystreams::{
25 kind::{AcceptType, AnnounceType, DeleteType, LikeType, RemoveType, UndoType},
33 actor::{kind::GroupType, ApActor, Endpoints, Group},
34 base::{AnyBase, BaseExt},
35 collection::{OrderedCollection, UnorderedCollection},
41 use activitystreams_ext::Ext2;
42 use actix_web::{body::Body, client::Client, web, HttpResponse};
43 use itertools::Itertools;
45 community::{Community, CommunityForm},
46 community_view::{CommunityFollowerView, CommunityModeratorView},
51 use lemmy_utils::convert_datetime;
52 use serde::Deserialize;
55 #[derive(Deserialize)]
56 pub struct CommunityQuery {
57 community_name: String,
60 #[async_trait::async_trait(?Send)]
61 impl ToApub for Community {
62 type Response = GroupExt;
64 // Turn a Lemmy Community into an ActivityPub group that can be sent out over the network.
65 async fn to_apub(&self, pool: &DbPool) -> Result<GroupExt, LemmyError> {
66 // The attributed to, is an ordered vector with the creator actor_ids first,
67 // then the rest of the moderators
68 // TODO Technically the instance admins can mod the community, but lets
69 // ignore that for now
71 let moderators = blocking(pool, move |conn| {
72 CommunityModeratorView::for_community(&conn, id)
75 let moderators: Vec<String> = moderators.into_iter().map(|m| m.user_actor_id).collect();
77 let mut group = Group::new();
79 .set_context(context())
80 .set_id(Url::parse(&self.actor_id)?)
81 .set_name(self.name.to_owned())
82 .set_published(convert_datetime(self.published))
83 .set_many_attributed_tos(moderators);
85 if let Some(u) = self.updated.to_owned() {
86 group.set_updated(convert_datetime(u));
88 if let Some(d) = self.description.to_owned() {
89 // TODO: this should be html, also add source field with raw markdown
90 // -> same for post.content and others
94 let mut ap_actor = ApActor::new(self.get_inbox_url()?, group);
96 .set_preferred_username(self.title.to_owned())
97 .set_outbox(self.get_outbox_url()?)
98 .set_followers(self.get_followers_url().parse()?)
99 .set_following(self.get_following_url().parse()?)
100 .set_liked(self.get_liked_url().parse()?)
101 .set_endpoints(Endpoints {
102 shared_inbox: Some(self.get_shared_inbox_url().parse()?),
106 let nsfw = self.nsfw;
107 let category_id = self.category_id;
108 let group_extension = blocking(pool, move |conn| {
109 GroupExtension::new(conn, category_id, nsfw)
116 self.get_public_key_ext(),
120 fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
121 create_tombstone(self.deleted, &self.actor_id, self.updated, GroupType::Group)
125 #[async_trait::async_trait(?Send)]
126 impl ActorType for Community {
127 fn actor_id_str(&self) -> String {
128 self.actor_id.to_owned()
131 fn public_key(&self) -> String {
132 self.public_key.to_owned().unwrap()
134 fn private_key(&self) -> String {
135 self.private_key.to_owned().unwrap()
138 /// As a local community, accept the follow request from a remote user.
139 async fn send_accept_follow(
144 ) -> Result<(), LemmyError> {
145 let actor_uri = follow.actor()?.as_single_xsd_any_uri().unwrap().to_string();
147 let mut accept = Accept::new(self.actor_id.to_owned(), follow.into_any_base()?);
148 let to = format!("{}/inbox", actor_uri);
150 .set_context(context())
151 .set_id(generate_activity_id(AcceptType::Accept)?)
154 insert_activity(self.creator_id, accept.clone(), true, pool).await?;
156 send_activity(client, &accept.into_any_base()?, self, vec![to]).await?;
160 async fn send_delete(
165 ) -> Result<(), LemmyError> {
166 let group = self.to_apub(pool).await?;
168 let mut delete = Delete::new(creator.actor_id.to_owned(), group.into_any_base()?);
170 .set_context(context())
171 .set_id(generate_activity_id(DeleteType::Delete)?)
173 .set_many_ccs(vec![self.get_followers_url()]);
175 insert_activity(self.creator_id, delete.clone(), true, pool).await?;
177 let inboxes = self.get_follower_inboxes(pool).await?;
179 // Note: For an accept, since it was automatic, no one pushed a button,
180 // the community was the actor.
181 // But for delete, the creator is the actor, and does the signing
182 send_activity(client, &delete.into_any_base()?, creator, inboxes).await?;
186 async fn send_undo_delete(
191 ) -> Result<(), LemmyError> {
192 let group = self.to_apub(pool).await?;
194 let mut delete = Delete::new(creator.actor_id.to_owned(), group.into_any_base()?);
196 .set_context(context())
197 .set_id(generate_activity_id(DeleteType::Delete)?)
199 .set_many_ccs(vec![self.get_followers_url()]);
202 // Undo that fake activity
203 let mut undo = Undo::new(creator.actor_id.to_owned(), delete.into_any_base()?);
205 .set_context(context())
206 .set_id(generate_activity_id(UndoType::Undo)?)
208 .set_many_ccs(vec![self.get_followers_url()]);
210 insert_activity(self.creator_id, undo.clone(), true, pool).await?;
212 let inboxes = self.get_follower_inboxes(pool).await?;
214 // Note: For an accept, since it was automatic, no one pushed a button,
215 // the community was the actor.
216 // But for delete, the creator is the actor, and does the signing
217 send_activity(client, &undo.into_any_base()?, creator, inboxes).await?;
221 async fn send_remove(
226 ) -> Result<(), LemmyError> {
227 let group = self.to_apub(pool).await?;
229 let mut remove = Remove::new(mod_.actor_id.to_owned(), group.into_any_base()?);
231 .set_context(context())
232 .set_id(generate_activity_id(RemoveType::Remove)?)
234 .set_many_ccs(vec![self.get_followers_url()]);
236 insert_activity(mod_.id, remove.clone(), true, pool).await?;
238 let inboxes = self.get_follower_inboxes(pool).await?;
240 // Note: For an accept, since it was automatic, no one pushed a button,
241 // the community was the actor.
242 // But for delete, the creator is the actor, and does the signing
243 send_activity(client, &remove.into_any_base()?, mod_, inboxes).await?;
247 async fn send_undo_remove(
252 ) -> Result<(), LemmyError> {
253 let group = self.to_apub(pool).await?;
255 let mut remove = Remove::new(mod_.actor_id.to_owned(), group.into_any_base()?);
257 .set_context(context())
258 .set_id(generate_activity_id(RemoveType::Remove)?)
260 .set_many_ccs(vec![self.get_followers_url()]);
262 // Undo that fake activity
263 let mut undo = Undo::new(mod_.actor_id.to_owned(), remove.into_any_base()?);
265 .set_context(context())
266 .set_id(generate_activity_id(LikeType::Like)?)
268 .set_many_ccs(vec![self.get_followers_url()]);
270 insert_activity(mod_.id, undo.clone(), true, pool).await?;
272 let inboxes = self.get_follower_inboxes(pool).await?;
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(client, &undo.into_any_base()?, mod_, inboxes).await?;
281 /// For a given community, returns the inboxes of all followers.
282 async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<String>, LemmyError> {
285 let inboxes = blocking(pool, move |conn| {
286 CommunityFollowerView::for_community(conn, id)
289 let inboxes = inboxes
291 .map(|c| get_shared_inbox(&Url::parse(&c.user_actor_id).unwrap()))
292 .filter(|s| !s.is_empty())
299 async fn send_follow(
301 _follow_actor_id: &str,
304 ) -> Result<(), LemmyError> {
308 async fn send_unfollow(
310 _follow_actor_id: &str,
313 ) -> Result<(), LemmyError> {
317 fn user_id(&self) -> i32 {
322 #[async_trait::async_trait(?Send)]
323 impl FromApub for CommunityForm {
324 type ApubType = GroupExt;
326 /// Parse an ActivityPub group received from another instance into a Lemmy community.
331 expected_domain: Option<Url>,
332 ) -> Result<Self, LemmyError> {
333 let creator_and_moderator_uris = group.inner.attributed_to().unwrap();
334 let creator_uri = creator_and_moderator_uris
343 let creator = get_or_fetch_and_upsert_user(creator_uri, client, pool).await?;
353 let title = group.inner.preferred_username().unwrap().to_string();
354 // TODO: should be parsed as html and tags like <script> removed (or use markdown source)
355 // -> same for post.content etc
356 let description = group
359 .map(|s| s.as_single_xsd_string().unwrap().into());
361 check_slurs(&title)?;
362 check_slurs_opt(&description)?;
368 category_id: group.ext_one.category.identifier.parse::<i32>()?,
369 creator_id: creator.id,
371 published: group.inner.published().map(|u| u.to_owned().naive_local()),
372 updated: group.inner.updated().map(|u| u.to_owned().naive_local()),
374 nsfw: group.ext_one.sensitive,
375 actor_id: check_actor_domain(group, expected_domain)?,
378 public_key: Some(group.ext_two.to_owned().public_key.public_key_pem),
379 last_refreshed_at: Some(naive_now()),
384 /// Return the community json over HTTP.
385 pub async fn get_apub_community_http(
386 info: web::Path<CommunityQuery>,
388 ) -> Result<HttpResponse<Body>, LemmyError> {
389 let community = blocking(&db, move |conn| {
390 Community::read_from_name(conn, &info.community_name)
394 if !community.deleted {
395 let apub = community.to_apub(&db).await?;
397 Ok(create_apub_response(&apub))
399 Ok(create_apub_tombstone_response(&community.to_tombstone()?))
403 /// Returns an empty followers collection, only populating the size (for privacy).
404 pub async fn get_apub_community_followers(
405 info: web::Path<CommunityQuery>,
407 ) -> Result<HttpResponse<Body>, LemmyError> {
408 let community = blocking(&db, move |conn| {
409 Community::read_from_name(&conn, &info.community_name)
413 let community_id = community.id;
414 let community_followers = blocking(&db, move |conn| {
415 CommunityFollowerView::for_community(&conn, community_id)
419 let mut collection = UnorderedCollection::new();
421 .set_context(context())
422 // TODO: this needs its own ID
423 .set_id(community.actor_id.parse()?)
424 .set_total_items(community_followers.len() as u64);
425 Ok(create_apub_response(&collection))
428 pub async fn get_apub_community_outbox(
429 info: web::Path<CommunityQuery>,
431 ) -> Result<HttpResponse<Body>, LemmyError> {
432 let community = blocking(&db, move |conn| {
433 Community::read_from_name(&conn, &info.community_name)
437 let community_id = community.id;
438 let posts = blocking(&db, move |conn| {
439 Post::list_for_community(conn, community_id)
443 let mut pages: Vec<AnyBase> = vec![];
445 pages.push(p.to_apub(&db).await?.into_any_base()?);
448 let len = pages.len();
449 let mut collection = OrderedCollection::new();
451 .set_many_items(pages)
452 .set_context(context())
453 .set_id(community.get_outbox_url()?)
454 .set_total_items(len as u64);
455 Ok(create_apub_response(&collection))
458 pub async fn do_announce(
460 community: &Community,
464 ) -> Result<(), LemmyError> {
465 let mut announce = Announce::new(community.actor_id.to_owned(), activity);
467 .set_context(context())
468 .set_id(generate_activity_id(AnnounceType::Announce)?)
470 .set_many_ccs(vec![community.get_followers_url()]);
472 insert_activity(community.creator_id, announce.clone(), true, pool).await?;
474 let mut to = community.get_follower_inboxes(pool).await?;
476 // dont send to the local instance, nor to the instance where the activity originally came from,
477 // because that would result in a database error (same data inserted twice)
478 // this seems to be the "easiest" stable alternative for remove_item()
479 to.retain(|x| *x != sender.get_shared_inbox_url());
480 to.retain(|x| *x != community.get_shared_inbox_url());
482 send_activity(client, &announce.into_any_base()?, community, to).await?;