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