]> Untitled Git - lemmy.git/blob - crates/apub/src/objects/community.rs
Federate Matrix ID (fixes #1438)
[lemmy.git] / crates / apub / src / objects / community.rs
1 use crate::{
2   extensions::{context::lemmy_context, group_extension::GroupExtension},
3   fetcher::{community::fetch_community_mods, person::get_or_fetch_and_upsert_person},
4   generate_moderators_url,
5   objects::{
6     check_object_domain,
7     create_tombstone,
8     get_object_from_apub,
9     get_source_markdown_value,
10     set_content_and_source,
11     FromApub,
12     FromApubToForm,
13     ToApub,
14   },
15   ActorType,
16   GroupExt,
17 };
18 use activitystreams::{
19   actor::{kind::GroupType, ApActor, Endpoints, Group},
20   base::BaseExt,
21   object::{ApObject, Image, Tombstone},
22   prelude::*,
23 };
24 use activitystreams_ext::Ext2;
25 use anyhow::Context;
26 use lemmy_api_structs::blocking;
27 use lemmy_db_queries::DbPool;
28 use lemmy_db_schema::{
29   naive_now,
30   source::community::{Community, CommunityForm},
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     let id = self.id;
47     let moderators = blocking(pool, move |conn| {
48       CommunityModeratorView::for_community(&conn, id)
49     })
50     .await??;
51     let moderators: Vec<Url> = moderators
52       .into_iter()
53       .map(|m| m.moderator.actor_id.into_inner())
54       .collect();
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       // NOTE: included attritubed_to field for compatibility with lemmy v0.9.9
63       .set_many_attributed_tos(moderators);
64
65     if let Some(u) = self.updated.to_owned() {
66       group.set_updated(convert_datetime(u));
67     }
68     if let Some(d) = self.description.to_owned() {
69       set_content_and_source(&mut group, &d)?;
70     }
71
72     if let Some(icon_url) = &self.icon {
73       let mut image = Image::new();
74       image.set_url::<Url>(icon_url.to_owned().into());
75       group.set_icon(image.into_any_base()?);
76     }
77
78     if let Some(banner_url) = &self.banner {
79       let mut image = Image::new();
80       image.set_url::<Url>(banner_url.to_owned().into());
81       group.set_image(image.into_any_base()?);
82     }
83
84     let mut ap_actor = ApActor::new(self.inbox_url.clone().into(), group);
85     ap_actor
86       .set_preferred_username(self.name.to_owned())
87       .set_outbox(self.get_outbox_url()?)
88       .set_followers(self.followers_url.clone().into())
89       .set_endpoints(Endpoints {
90         shared_inbox: Some(self.get_shared_inbox_or_inbox_url()),
91         ..Default::default()
92       });
93
94     Ok(Ext2::new(
95       ap_actor,
96       GroupExtension::new(self.nsfw, generate_moderators_url(&self.actor_id)?.into())?,
97       self.get_public_key_ext()?,
98     ))
99   }
100
101   fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
102     create_tombstone(
103       self.deleted,
104       self.actor_id.to_owned().into(),
105       self.updated,
106       GroupType::Group,
107     )
108   }
109 }
110
111 #[async_trait::async_trait(?Send)]
112 impl FromApub for Community {
113   type ApubType = GroupExt;
114
115   /// Converts a `Group` to `Community`, inserts it into the database and updates moderators.
116   async fn from_apub(
117     group: &GroupExt,
118     context: &LemmyContext,
119     expected_domain: Url,
120     request_counter: &mut i32,
121     mod_action_allowed: bool,
122   ) -> Result<Community, LemmyError> {
123     get_object_from_apub(
124       group,
125       context,
126       expected_domain,
127       request_counter,
128       mod_action_allowed,
129     )
130     .await
131   }
132 }
133
134 #[async_trait::async_trait(?Send)]
135 impl FromApubToForm<GroupExt> for CommunityForm {
136   async fn from_apub(
137     group: &GroupExt,
138     context: &LemmyContext,
139     expected_domain: Url,
140     request_counter: &mut i32,
141     _mod_action_allowed: bool,
142   ) -> Result<Self, LemmyError> {
143     let moderator_uris = fetch_community_mods(context, group, request_counter).await?;
144     let creator = if let Some(creator_uri) = moderator_uris.first() {
145       get_or_fetch_and_upsert_person(creator_uri, context, request_counter)
146     } else {
147       // NOTE: code for compatibility with lemmy v0.9.9
148       let creator_uri = group
149         .inner
150         .attributed_to()
151         .map(|a| a.as_many())
152         .flatten()
153         .map(|a| a.first())
154         .flatten()
155         .map(|a| a.as_xsd_any_uri())
156         .flatten()
157         .context(location_info!())?;
158       get_or_fetch_and_upsert_person(creator_uri, context, request_counter)
159     }
160     .await?;
161
162     let name = group
163       .inner
164       .preferred_username()
165       .context(location_info!())?
166       .to_string();
167     let title = group
168       .inner
169       .name()
170       .context(location_info!())?
171       .as_one()
172       .context(location_info!())?
173       .as_xsd_string()
174       .context(location_info!())?
175       .to_string();
176
177     let description = get_source_markdown_value(group)?;
178
179     check_slurs(&name)?;
180     check_slurs(&title)?;
181     check_slurs_opt(&description)?;
182
183     let icon = match group.icon() {
184       Some(any_image) => Some(
185         Image::from_any_base(any_image.as_one().context(location_info!())?.clone())
186           .context(location_info!())?
187           .context(location_info!())?
188           .url()
189           .context(location_info!())?
190           .as_single_xsd_any_uri()
191           .map(|u| u.to_owned().into()),
192       ),
193       None => None,
194     };
195     let banner = match group.image() {
196       Some(any_image) => Some(
197         Image::from_any_base(any_image.as_one().context(location_info!())?.clone())
198           .context(location_info!())?
199           .context(location_info!())?
200           .url()
201           .context(location_info!())?
202           .as_single_xsd_any_uri()
203           .map(|u| u.to_owned().into()),
204       ),
205       None => None,
206     };
207     let shared_inbox = group
208       .inner
209       .endpoints()?
210       .map(|e| e.shared_inbox)
211       .flatten()
212       .map(|s| s.to_owned().into());
213
214     Ok(CommunityForm {
215       name,
216       title,
217       description,
218       creator_id: creator.id,
219       removed: None,
220       published: group.inner.published().map(|u| u.to_owned().naive_local()),
221       updated: group.inner.updated().map(|u| u.to_owned().naive_local()),
222       deleted: None,
223       nsfw: Some(group.ext_one.sensitive.unwrap_or(false)),
224       actor_id: Some(check_object_domain(group, expected_domain)?),
225       local: Some(false),
226       private_key: None,
227       public_key: Some(group.ext_two.to_owned().public_key.public_key_pem),
228       last_refreshed_at: Some(naive_now()),
229       icon,
230       banner,
231       followers_url: Some(
232         group
233           .inner
234           .followers()?
235           .context(location_info!())?
236           .to_owned()
237           .into(),
238       ),
239       inbox_url: Some(group.inner.inbox()?.to_owned().into()),
240       shared_inbox_url: Some(shared_inbox),
241     })
242   }
243 }