use crate::{ extensions::{context::lemmy_context, group_extensions::GroupExtension}, fetcher::{community::fetch_community_mods, person::get_or_fetch_and_upsert_person}, generate_moderators_url, objects::{ check_object_domain, create_tombstone, get_object_from_apub, get_source_markdown_value, set_content_and_source, FromApub, FromApubToForm, ToApub, }, ActorType, GroupExt, }; use activitystreams::{ actor::{kind::GroupType, ApActor, Endpoints, Group}, base::BaseExt, object::{ApObject, Image, Tombstone}, prelude::*, }; use activitystreams_ext::Ext2; use anyhow::Context; use lemmy_api_structs::blocking; use lemmy_db_queries::DbPool; use lemmy_db_schema::{ naive_now, source::community::{Community, CommunityForm}, }; use lemmy_db_views_actor::community_moderator_view::CommunityModeratorView; use lemmy_utils::{ location_info, utils::{check_slurs, check_slurs_opt, convert_datetime}, LemmyError, }; use lemmy_websocket::LemmyContext; use url::Url; #[async_trait::async_trait(?Send)] impl ToApub for Community { type ApubType = GroupExt; async fn to_apub(&self, pool: &DbPool) -> Result { let id = self.id; let moderators = blocking(pool, move |conn| { CommunityModeratorView::for_community(&conn, id) }) .await??; let moderators: Vec = moderators .into_iter() .map(|m| m.moderator.actor_id.into_inner()) .collect(); let mut group = ApObject::new(Group::new()); group .set_many_contexts(lemmy_context()?) .set_id(self.actor_id.to_owned().into()) .set_name(self.title.to_owned()) .set_published(convert_datetime(self.published)) // NOTE: included attritubed_to field for compatibility with lemmy v0.9.9 .set_many_attributed_tos(moderators); if let Some(u) = self.updated.to_owned() { group.set_updated(convert_datetime(u)); } if let Some(d) = self.description.to_owned() { set_content_and_source(&mut group, &d)?; } if let Some(icon_url) = &self.icon { let mut image = Image::new(); image.set_url::(icon_url.to_owned().into()); group.set_icon(image.into_any_base()?); } if let Some(banner_url) = &self.banner { let mut image = Image::new(); image.set_url::(banner_url.to_owned().into()); group.set_image(image.into_any_base()?); } let mut ap_actor = ApActor::new(self.inbox_url.clone().into(), group); ap_actor .set_preferred_username(self.name.to_owned()) .set_outbox(self.get_outbox_url()?) .set_followers(self.followers_url.clone().into()) .set_endpoints(Endpoints { shared_inbox: Some(self.get_shared_inbox_or_inbox_url()), ..Default::default() }); Ok(Ext2::new( ap_actor, GroupExtension::new(self.nsfw, generate_moderators_url(&self.actor_id)?.into())?, self.get_public_key_ext()?, )) } fn to_tombstone(&self) -> Result { create_tombstone( self.deleted, self.actor_id.to_owned().into(), self.updated, GroupType::Group, ) } } #[async_trait::async_trait(?Send)] impl FromApub for Community { type ApubType = GroupExt; /// Converts a `Group` to `Community`, inserts it into the database and updates moderators. async fn from_apub( group: &GroupExt, context: &LemmyContext, expected_domain: Url, request_counter: &mut i32, mod_action_allowed: bool, ) -> Result { get_object_from_apub( group, context, expected_domain, request_counter, mod_action_allowed, ) .await } } #[async_trait::async_trait(?Send)] impl FromApubToForm for CommunityForm { async fn from_apub( group: &GroupExt, context: &LemmyContext, expected_domain: Url, request_counter: &mut i32, _mod_action_allowed: bool, ) -> Result { let moderator_uris = fetch_community_mods(context, group, request_counter).await?; let creator = if let Some(creator_uri) = moderator_uris.first() { get_or_fetch_and_upsert_person(creator_uri, context, request_counter) } else { // NOTE: code for compatibility with lemmy v0.9.9 let creator_uri = group .inner .attributed_to() .map(|a| a.as_many()) .flatten() .map(|a| a.first()) .flatten() .map(|a| a.as_xsd_any_uri()) .flatten() .context(location_info!())?; get_or_fetch_and_upsert_person(creator_uri, context, request_counter) } .await?; let name = group .inner .preferred_username() .context(location_info!())? .to_string(); let title = group .inner .name() .context(location_info!())? .as_one() .context(location_info!())? .as_xsd_string() .context(location_info!())? .to_string(); let description = get_source_markdown_value(group)?; check_slurs(&name)?; check_slurs(&title)?; check_slurs_opt(&description)?; let icon = match group.icon() { Some(any_image) => Some( Image::from_any_base(any_image.as_one().context(location_info!())?.clone()) .context(location_info!())? .context(location_info!())? .url() .context(location_info!())? .as_single_xsd_any_uri() .map(|u| u.to_owned().into()), ), None => None, }; let banner = match group.image() { Some(any_image) => Some( Image::from_any_base(any_image.as_one().context(location_info!())?.clone()) .context(location_info!())? .context(location_info!())? .url() .context(location_info!())? .as_single_xsd_any_uri() .map(|u| u.to_owned().into()), ), None => None, }; let shared_inbox = group .inner .endpoints()? .map(|e| e.shared_inbox) .flatten() .map(|s| s.to_owned().into()); Ok(CommunityForm { name, title, description, creator_id: creator.id, removed: None, published: group.inner.published().map(|u| u.to_owned().naive_local()), updated: group.inner.updated().map(|u| u.to_owned().naive_local()), deleted: None, nsfw: Some(group.ext_one.sensitive.unwrap_or(false)), actor_id: Some(check_object_domain(group, expected_domain)?), local: Some(false), private_key: None, public_key: Some(group.ext_two.to_owned().public_key.public_key_pem), last_refreshed_at: Some(naive_now()), icon, banner, followers_url: Some( group .inner .followers()? .context(location_info!())? .to_owned() .into(), ), inbox_url: Some(group.inner.inbox()?.to_owned().into()), shared_inbox_url: Some(shared_inbox), }) } }