]> Untitled Git - lemmy.git/blob - crates/apub/src/objects/community.rs
7a1583ca09239ee5b107c7623a6df8c32114e50d
[lemmy.git] / crates / apub / src / objects / community.rs
1 use crate::{
2   check_apub_id_valid_with_strictness,
3   collections::CommunityContext,
4   fetch_local_site_data,
5   local_instance,
6   objects::instance::fetch_instance_actor_for_object,
7   protocol::{
8     objects::{group::Group, Endpoints, LanguageTag},
9     ImageObject,
10     Source,
11   },
12   ActorType,
13 };
14 use activitypub_federation::{
15   core::object_id::ObjectId,
16   traits::{Actor, ApubObject},
17 };
18 use activitystreams_kinds::actor::GroupType;
19 use chrono::NaiveDateTime;
20 use itertools::Itertools;
21 use lemmy_api_common::{
22   context::LemmyContext,
23   utils::{generate_featured_url, generate_moderators_url, generate_outbox_url},
24 };
25 use lemmy_db_schema::{
26   source::{
27     actor_language::CommunityLanguage,
28     community::{Community, CommunityUpdateForm},
29   },
30   traits::{ApubActor, Crud},
31 };
32 use lemmy_db_views_actor::structs::CommunityFollowerView;
33 use lemmy_utils::{
34   error::LemmyError,
35   utils::{markdown::markdown_to_html, time::convert_datetime},
36 };
37 use std::ops::Deref;
38 use tracing::debug;
39 use url::Url;
40
41 #[derive(Clone, Debug)]
42 pub struct ApubCommunity(Community);
43
44 impl Deref for ApubCommunity {
45   type Target = Community;
46   fn deref(&self) -> &Self::Target {
47     &self.0
48   }
49 }
50
51 impl From<Community> for ApubCommunity {
52   fn from(c: Community) -> Self {
53     ApubCommunity(c)
54   }
55 }
56
57 #[async_trait::async_trait(?Send)]
58 impl ApubObject for ApubCommunity {
59   type DataType = LemmyContext;
60   type ApubType = Group;
61   type DbType = Community;
62   type Error = LemmyError;
63
64   fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
65     Some(self.last_refreshed_at)
66   }
67
68   #[tracing::instrument(skip_all)]
69   async fn read_from_apub_id(
70     object_id: Url,
71     context: &LemmyContext,
72   ) -> Result<Option<Self>, LemmyError> {
73     Ok(
74       Community::read_from_apub_id(context.pool(), &object_id.into())
75         .await?
76         .map(Into::into),
77     )
78   }
79
80   #[tracing::instrument(skip_all)]
81   async fn delete(self, context: &LemmyContext) -> Result<(), LemmyError> {
82     let form = CommunityUpdateForm::builder().deleted(Some(true)).build();
83     Community::update(context.pool(), self.id, &form).await?;
84     Ok(())
85   }
86
87   #[tracing::instrument(skip_all)]
88   async fn into_apub(self, data: &LemmyContext) -> Result<Group, LemmyError> {
89     let community_id = self.id;
90     let langs = CommunityLanguage::read(data.pool(), community_id).await?;
91     let language = LanguageTag::new_multiple(langs, data.pool()).await?;
92
93     let group = Group {
94       kind: GroupType::Group,
95       id: ObjectId::new(self.actor_id()),
96       preferred_username: self.name.clone(),
97       name: Some(self.title.clone()),
98       summary: self.description.as_ref().map(|b| markdown_to_html(b)),
99       source: self.description.clone().map(Source::new),
100       icon: self.icon.clone().map(ImageObject::new),
101       image: self.banner.clone().map(ImageObject::new),
102       sensitive: Some(self.nsfw),
103       moderators: Some(generate_moderators_url(&self.actor_id)?.into()),
104       featured: Some(generate_featured_url(&self.actor_id)?.into()),
105       inbox: self.inbox_url.clone().into(),
106       outbox: ObjectId::new(generate_outbox_url(&self.actor_id)?),
107       followers: self.followers_url.clone().into(),
108       endpoints: self.shared_inbox_url.clone().map(|s| Endpoints {
109         shared_inbox: s.into(),
110       }),
111       public_key: self.get_public_key(),
112       language,
113       published: Some(convert_datetime(self.published)),
114       updated: self.updated.map(convert_datetime),
115       posting_restricted_to_mods: Some(self.posting_restricted_to_mods),
116       attributed_to: Some(generate_moderators_url(&self.actor_id)?.into()),
117     };
118     Ok(group)
119   }
120
121   #[tracing::instrument(skip_all)]
122   async fn verify(
123     group: &Group,
124     expected_domain: &Url,
125     context: &LemmyContext,
126     _request_counter: &mut i32,
127   ) -> Result<(), LemmyError> {
128     group.verify(expected_domain, context).await
129   }
130
131   /// Converts a `Group` to `Community`, inserts it into the database and updates moderators.
132   #[tracing::instrument(skip_all)]
133   async fn from_apub(
134     group: Group,
135     context: &LemmyContext,
136     request_counter: &mut i32,
137   ) -> Result<ApubCommunity, LemmyError> {
138     let instance_id = fetch_instance_actor_for_object(&group.id, context, request_counter).await?;
139
140     let form = Group::into_insert_form(group.clone(), instance_id);
141     let languages = LanguageTag::to_language_id_multiple(group.language, context.pool()).await?;
142
143     let community = Community::create(context.pool(), &form).await?;
144     CommunityLanguage::update(context.pool(), languages, community.id).await?;
145
146     let community: ApubCommunity = community.into();
147     let outbox_data = CommunityContext(community.clone(), context.clone());
148
149     // Fetching mods and outbox is not necessary for Lemmy to work, so ignore errors. Besides,
150     // we need to ignore these errors so that tests can work entirely offline.
151     group
152       .outbox
153       .dereference(&outbox_data, local_instance(context).await, request_counter)
154       .await
155       .map_err(|e| debug!("{}", e))
156       .ok();
157
158     if let Some(moderators) = group.attributed_to.or(group.moderators) {
159       moderators
160         .dereference(&outbox_data, local_instance(context).await, request_counter)
161         .await
162         .map_err(|e| debug!("{}", e))
163         .ok();
164     }
165
166     Ok(community)
167   }
168 }
169
170 impl Actor for ApubCommunity {
171   fn public_key(&self) -> &str {
172     &self.public_key
173   }
174
175   fn inbox(&self) -> Url {
176     self.inbox_url.clone().into()
177   }
178
179   fn shared_inbox(&self) -> Option<Url> {
180     self.shared_inbox_url.clone().map(Into::into)
181   }
182 }
183
184 impl ActorType for ApubCommunity {
185   fn actor_id(&self) -> Url {
186     self.actor_id.clone().into()
187   }
188   fn private_key(&self) -> Option<String> {
189     self.private_key.clone()
190   }
191 }
192
193 impl ApubCommunity {
194   /// For a given community, returns the inboxes of all followers.
195   #[tracing::instrument(skip_all)]
196   pub(crate) async fn get_follower_inboxes(
197     &self,
198     context: &LemmyContext,
199   ) -> Result<Vec<Url>, LemmyError> {
200     let id = self.id;
201
202     let local_site_data = fetch_local_site_data(context.pool()).await?;
203     let follows = CommunityFollowerView::for_community(context.pool(), id).await?;
204     let inboxes: Vec<Url> = follows
205       .into_iter()
206       .filter(|f| !f.follower.local)
207       .map(|f| {
208         f.follower
209           .shared_inbox_url
210           .unwrap_or(f.follower.inbox_url)
211           .into()
212       })
213       .unique()
214       .filter(|inbox: &Url| inbox.host_str() != Some(&context.settings().hostname))
215       // Don't send to blocked instances
216       .filter(|inbox| {
217         check_apub_id_valid_with_strictness(inbox, false, &local_site_data, context.settings())
218           .is_ok()
219       })
220       .collect();
221
222     Ok(inboxes)
223   }
224 }
225
226 #[cfg(test)]
227 pub(crate) mod tests {
228   use super::*;
229   use crate::{
230     objects::{instance::tests::parse_lemmy_instance, tests::init_context},
231     protocol::tests::file_to_json_object,
232   };
233   use lemmy_db_schema::{source::site::Site, traits::Crud};
234   use serial_test::serial;
235
236   pub(crate) async fn parse_lemmy_community(context: &LemmyContext) -> ApubCommunity {
237     let mut json: Group = file_to_json_object("assets/lemmy/objects/group.json").unwrap();
238     // change these links so they dont fetch over the network
239     json.moderators = None;
240     json.attributed_to = None;
241     json.outbox =
242       ObjectId::new(Url::parse("https://enterprise.lemmy.ml/c/tenforward/not_outbox").unwrap());
243
244     let url = Url::parse("https://enterprise.lemmy.ml/c/tenforward").unwrap();
245     let mut request_counter = 0;
246     ApubCommunity::verify(&json, &url, context, &mut request_counter)
247       .await
248       .unwrap();
249     let community = ApubCommunity::from_apub(json, context, &mut request_counter)
250       .await
251       .unwrap();
252     // this makes one requests to the (intentionally broken) outbox collection
253     assert_eq!(request_counter, 1);
254     community
255   }
256
257   #[actix_rt::test]
258   #[serial]
259   async fn test_parse_lemmy_community() {
260     let context = init_context().await;
261     let site = parse_lemmy_instance(&context).await;
262     let community = parse_lemmy_community(&context).await;
263
264     assert_eq!(community.title, "Ten Forward");
265     assert!(!community.local);
266     assert_eq!(community.description.as_ref().unwrap().len(), 132);
267
268     Community::delete(context.pool(), community.id)
269       .await
270       .unwrap();
271     Site::delete(context.pool(), site.id).await.unwrap();
272   }
273 }