]> Untitled Git - lemmy.git/blob - crates/apub/src/objects/community.rs
Merge pull request 'Include object id when logging apub errors' (#165) from log-ids...
[lemmy.git] / crates / apub / src / objects / community.rs
1 use crate::{
2   extensions::{context::lemmy_context, group_extensions::GroupExtension},
3   fetcher::user::get_or_fetch_and_upsert_user,
4   objects::{
5     check_object_domain,
6     create_tombstone,
7     get_object_from_apub,
8     get_source_markdown_value,
9     set_content_and_source,
10     FromApub,
11     FromApubToForm,
12     ToApub,
13   },
14   ActorType,
15   GroupExt,
16 };
17 use activitystreams::{
18   actor::{kind::GroupType, ApActor, Endpoints, Group},
19   base::BaseExt,
20   object::{ApObject, Image, Tombstone},
21   prelude::*,
22 };
23 use activitystreams_ext::Ext2;
24 use anyhow::Context;
25 use lemmy_db_queries::DbPool;
26 use lemmy_db_schema::{
27   naive_now,
28   source::community::{Community, CommunityForm},
29 };
30 use lemmy_db_views_actor::community_moderator_view::CommunityModeratorView;
31 use lemmy_structs::blocking;
32 use lemmy_utils::{
33   location_info,
34   utils::{check_slurs, check_slurs_opt, convert_datetime},
35   LemmyError,
36 };
37 use lemmy_websocket::LemmyContext;
38 use url::Url;
39
40 #[async_trait::async_trait(?Send)]
41 impl ToApub for Community {
42   type ApubType = GroupExt;
43
44   async fn to_apub(&self, pool: &DbPool) -> Result<GroupExt, LemmyError> {
45     // The attributed to, is an ordered vector with the creator actor_ids first,
46     // then the rest of the moderators
47     // TODO Technically the instance admins can mod the community, but lets
48     // ignore that for now
49     let id = self.id;
50     let moderators = blocking(pool, move |conn| {
51       CommunityModeratorView::for_community(&conn, id)
52     })
53     .await??;
54     let moderators: Vec<Url> = moderators
55       .into_iter()
56       .map(|m| m.moderator.actor_id.into_inner())
57       .collect();
58
59     let mut group = ApObject::new(Group::new());
60     group
61       .set_many_contexts(lemmy_context()?)
62       .set_id(self.actor_id.to_owned().into())
63       .set_name(self.title.to_owned())
64       .set_published(convert_datetime(self.published))
65       .set_many_attributed_tos(moderators);
66
67     if let Some(u) = self.updated.to_owned() {
68       group.set_updated(convert_datetime(u));
69     }
70     if let Some(d) = self.description.to_owned() {
71       set_content_and_source(&mut group, &d)?;
72     }
73
74     if let Some(icon_url) = &self.icon {
75       let mut image = Image::new();
76       image.set_url(Url::parse(icon_url)?);
77       group.set_icon(image.into_any_base()?);
78     }
79
80     if let Some(banner_url) = &self.banner {
81       let mut image = Image::new();
82       image.set_url(Url::parse(banner_url)?);
83       group.set_image(image.into_any_base()?);
84     }
85
86     let mut ap_actor = ApActor::new(self.inbox_url.clone().into(), group);
87     ap_actor
88       .set_preferred_username(self.name.to_owned())
89       .set_outbox(self.get_outbox_url()?)
90       .set_followers(self.followers_url.clone().into())
91       .set_endpoints(Endpoints {
92         shared_inbox: Some(self.get_shared_inbox_or_inbox_url()),
93         ..Default::default()
94       });
95
96     let nsfw = self.nsfw;
97     let category_id = self.category_id;
98     let group_extension = blocking(pool, move |conn| {
99       GroupExtension::new(conn, category_id, nsfw)
100     })
101     .await??;
102
103     Ok(Ext2::new(
104       ap_actor,
105       group_extension,
106       self.get_public_key_ext()?,
107     ))
108   }
109
110   fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
111     create_tombstone(
112       self.deleted,
113       self.actor_id.to_owned().into(),
114       self.updated,
115       GroupType::Group,
116     )
117   }
118 }
119
120 #[async_trait::async_trait(?Send)]
121 impl FromApub for Community {
122   type ApubType = GroupExt;
123
124   /// Converts a `Group` to `Community`.
125   async fn from_apub(
126     group: &GroupExt,
127     context: &LemmyContext,
128     expected_domain: Url,
129     request_counter: &mut i32,
130   ) -> Result<Community, LemmyError> {
131     get_object_from_apub(group, context, expected_domain, request_counter).await
132   }
133 }
134
135 #[async_trait::async_trait(?Send)]
136 impl FromApubToForm<GroupExt> for CommunityForm {
137   async fn from_apub(
138     group: &GroupExt,
139     context: &LemmyContext,
140     expected_domain: Url,
141     request_counter: &mut i32,
142   ) -> Result<Self, LemmyError> {
143     let creator_and_moderator_uris = group.inner.attributed_to().context(location_info!())?;
144     let creator_uri = creator_and_moderator_uris
145       .as_many()
146       .context(location_info!())?
147       .iter()
148       .next()
149       .context(location_info!())?
150       .as_xsd_any_uri()
151       .context(location_info!())?;
152
153     let creator = get_or_fetch_and_upsert_user(creator_uri, context, request_counter).await?;
154     let name = group
155       .inner
156       .preferred_username()
157       .context(location_info!())?
158       .to_string();
159     let title = group
160       .inner
161       .name()
162       .context(location_info!())?
163       .as_one()
164       .context(location_info!())?
165       .as_xsd_string()
166       .context(location_info!())?
167       .to_string();
168
169     let description = get_source_markdown_value(group)?;
170
171     check_slurs(&name)?;
172     check_slurs(&title)?;
173     check_slurs_opt(&description)?;
174
175     let icon = match group.icon() {
176       Some(any_image) => Some(
177         Image::from_any_base(any_image.as_one().context(location_info!())?.clone())
178           .context(location_info!())?
179           .context(location_info!())?
180           .url()
181           .context(location_info!())?
182           .as_single_xsd_any_uri()
183           .map(|u| u.to_string()),
184       ),
185       None => None,
186     };
187     let banner = match group.image() {
188       Some(any_image) => Some(
189         Image::from_any_base(any_image.as_one().context(location_info!())?.clone())
190           .context(location_info!())?
191           .context(location_info!())?
192           .url()
193           .context(location_info!())?
194           .as_single_xsd_any_uri()
195           .map(|u| u.to_string()),
196       ),
197       None => None,
198     };
199     let shared_inbox = group
200       .inner
201       .endpoints()?
202       .map(|e| e.shared_inbox)
203       .flatten()
204       .map(|s| s.to_owned().into());
205
206     Ok(CommunityForm {
207       name,
208       title,
209       description,
210       category_id: group
211         .ext_one
212         .category
213         .clone()
214         .map(|c| c.identifier.parse::<i32>().ok())
215         .flatten()
216         .unwrap_or(1),
217       creator_id: creator.id,
218       removed: None,
219       published: group.inner.published().map(|u| u.to_owned().naive_local()),
220       updated: group.inner.updated().map(|u| u.to_owned().naive_local()),
221       deleted: None,
222       nsfw: group.ext_one.sensitive.unwrap_or(false),
223       actor_id: Some(check_object_domain(group, expected_domain)?),
224       local: false,
225       private_key: None,
226       public_key: Some(group.ext_two.to_owned().public_key.public_key_pem),
227       last_refreshed_at: Some(naive_now()),
228       icon,
229       banner,
230       followers_url: Some(
231         group
232           .inner
233           .followers()?
234           .context(location_info!())?
235           .to_owned()
236           .into(),
237       ),
238       inbox_url: Some(group.inner.inbox()?.to_owned().into()),
239       shared_inbox_url: Some(shared_inbox),
240     })
241   }
242 }