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