]> Untitled Git - lemmy.git/blob - lemmy_apub/src/lib.rs
Fix nginx config for local federation setup (#104)
[lemmy.git] / lemmy_apub / src / lib.rs
1 #[macro_use]
2 extern crate lazy_static;
3
4 pub mod activities;
5 pub mod activity_queue;
6 pub mod comment;
7 pub mod community;
8 pub mod extensions;
9 pub mod fetcher;
10 pub mod inbox;
11 pub mod post;
12 pub mod private_message;
13 pub mod user;
14
15 use crate::extensions::{
16   group_extensions::GroupExtension,
17   page_extension::PageExtension,
18   signatures::{PublicKey, PublicKeyExtension},
19 };
20 use activitystreams::{
21   activity::Follow,
22   actor::{ApActor, Group, Person},
23   base::AsBase,
24   markers::Base,
25   object::{Page, Tombstone},
26   prelude::*,
27 };
28 use activitystreams_ext::{Ext1, Ext2};
29 use actix_web::{body::Body, HttpResponse};
30 use anyhow::{anyhow, Context};
31 use chrono::NaiveDateTime;
32 use lemmy_db::{activity::do_insert_activity, user::User_, DbPool};
33 use lemmy_structs::{blocking, WebFingerResponse};
34 use lemmy_utils::{
35   apub::get_apub_protocol_string,
36   location_info,
37   request::{retry, RecvError},
38   settings::Settings,
39   utils::{convert_datetime, MentionData},
40   LemmyError,
41 };
42 use lemmy_websocket::LemmyContext;
43 use log::debug;
44 use reqwest::Client;
45 use serde::Serialize;
46 use url::{ParseError, Url};
47
48 type GroupExt = Ext2<ApActor<Group>, GroupExtension, PublicKeyExtension>;
49 type PersonExt = Ext1<ApActor<Person>, PublicKeyExtension>;
50 type PageExt = Ext1<Page, PageExtension>;
51
52 pub static APUB_JSON_CONTENT_TYPE: &str = "application/activity+json";
53
54 /// Convert the data to json and turn it into an HTTP Response with the correct ActivityPub
55 /// headers.
56 fn create_apub_response<T>(data: &T) -> HttpResponse<Body>
57 where
58   T: Serialize,
59 {
60   HttpResponse::Ok()
61     .content_type(APUB_JSON_CONTENT_TYPE)
62     .json(data)
63 }
64
65 fn create_apub_tombstone_response<T>(data: &T) -> HttpResponse<Body>
66 where
67   T: Serialize,
68 {
69   HttpResponse::Gone()
70     .content_type(APUB_JSON_CONTENT_TYPE)
71     .json(data)
72 }
73
74 // Checks if the ID has a valid format, correct scheme, and is in the allowed instance list.
75 fn check_is_apub_id_valid(apub_id: &Url) -> Result<(), LemmyError> {
76   let settings = Settings::get();
77   let domain = apub_id.domain().context(location_info!())?.to_string();
78   let local_instance = settings
79     .hostname
80     .split(':')
81     .collect::<Vec<&str>>()
82     .first()
83     .context(location_info!())?
84     .to_string();
85
86   if !settings.federation.enabled {
87     return if domain == local_instance {
88       Ok(())
89     } else {
90       Err(
91         anyhow!(
92           "Trying to connect with {}, but federation is disabled",
93           domain
94         )
95         .into(),
96       )
97     };
98   }
99
100   if apub_id.scheme() != get_apub_protocol_string() {
101     return Err(anyhow!("invalid apub id scheme: {:?}", apub_id.scheme()).into());
102   }
103
104   let mut allowed_instances = Settings::get().get_allowed_instances();
105   let blocked_instances = Settings::get().get_blocked_instances();
106
107   if !allowed_instances.is_empty() {
108     // need to allow this explicitly because apub activities might contain objects from our local
109     // instance. split is needed to remove the port in our federation test setup.
110     allowed_instances.push(local_instance);
111
112     if allowed_instances.contains(&domain) {
113       Ok(())
114     } else {
115       Err(anyhow!("{} not in federation allowlist", domain).into())
116     }
117   } else if !blocked_instances.is_empty() {
118     if blocked_instances.contains(&domain) {
119       Err(anyhow!("{} is in federation blocklist", domain).into())
120     } else {
121       Ok(())
122     }
123   } else {
124     panic!("Invalid config, both allowed_instances and blocked_instances are specified");
125   }
126 }
127
128 #[async_trait::async_trait(?Send)]
129 pub trait ToApub {
130   type Response;
131   async fn to_apub(&self, pool: &DbPool) -> Result<Self::Response, LemmyError>;
132   fn to_tombstone(&self) -> Result<Tombstone, LemmyError>;
133 }
134
135 /// Updated is actually the deletion time
136 fn create_tombstone<T>(
137   deleted: bool,
138   object_id: &str,
139   updated: Option<NaiveDateTime>,
140   former_type: T,
141 ) -> Result<Tombstone, LemmyError>
142 where
143   T: ToString,
144 {
145   if deleted {
146     if let Some(updated) = updated {
147       let mut tombstone = Tombstone::new();
148       tombstone.set_id(object_id.parse()?);
149       tombstone.set_former_type(former_type.to_string());
150       tombstone.set_deleted(convert_datetime(updated));
151       Ok(tombstone)
152     } else {
153       Err(anyhow!("Cant convert to tombstone because updated time was None.").into())
154     }
155   } else {
156     Err(anyhow!("Cant convert object to tombstone if it wasnt deleted").into())
157   }
158 }
159
160 #[async_trait::async_trait(?Send)]
161 pub trait FromApub {
162   type ApubType;
163   /// Converts an object from ActivityPub type to Lemmy internal type.
164   ///
165   /// * `apub` The object to read from
166   /// * `context` LemmyContext which holds DB pool, HTTP client etc
167   /// * `expected_domain` If present, ensure that the apub object comes from the same domain as
168   ///                     this URL
169   ///
170   async fn from_apub(
171     apub: &Self::ApubType,
172     context: &LemmyContext,
173     expected_domain: Option<Url>,
174   ) -> Result<Self, LemmyError>
175   where
176     Self: Sized;
177 }
178
179 #[async_trait::async_trait(?Send)]
180 pub trait ApubObjectType {
181   async fn send_create(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
182   async fn send_update(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
183   async fn send_delete(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
184   async fn send_undo_delete(
185     &self,
186     creator: &User_,
187     context: &LemmyContext,
188   ) -> Result<(), LemmyError>;
189   async fn send_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
190   async fn send_undo_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
191 }
192
193 pub(in crate) fn check_actor_domain<T, Kind>(
194   apub: &T,
195   expected_domain: Option<Url>,
196 ) -> Result<String, LemmyError>
197 where
198   T: Base + AsBase<Kind>,
199 {
200   let actor_id = if let Some(url) = expected_domain {
201     let domain = url.domain().context(location_info!())?;
202     apub.id(domain)?.context(location_info!())?
203   } else {
204     let actor_id = apub.id_unchecked().context(location_info!())?;
205     check_is_apub_id_valid(&actor_id)?;
206     actor_id
207   };
208   Ok(actor_id.to_string())
209 }
210
211 #[async_trait::async_trait(?Send)]
212 pub trait ApubLikeableType {
213   async fn send_like(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
214   async fn send_dislike(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
215   async fn send_undo_like(&self, creator: &User_, context: &LemmyContext)
216     -> Result<(), LemmyError>;
217 }
218
219 #[async_trait::async_trait(?Send)]
220 pub trait ActorType {
221   fn actor_id_str(&self) -> String;
222
223   // TODO: every actor should have a public key, so this shouldnt be an option (needs to be fixed in db)
224   fn public_key(&self) -> Option<String>;
225   fn private_key(&self) -> Option<String>;
226
227   /// numeric id in the database, used for insert_activity
228   fn user_id(&self) -> i32;
229
230   // These two have default impls, since currently a community can't follow anything,
231   // and a user can't be followed (yet)
232   #[allow(unused_variables)]
233   async fn send_follow(
234     &self,
235     follow_actor_id: &Url,
236     context: &LemmyContext,
237   ) -> Result<(), LemmyError>;
238   async fn send_unfollow(
239     &self,
240     follow_actor_id: &Url,
241     context: &LemmyContext,
242   ) -> Result<(), LemmyError>;
243
244   #[allow(unused_variables)]
245   async fn send_accept_follow(
246     &self,
247     follow: Follow,
248     context: &LemmyContext,
249   ) -> Result<(), LemmyError>;
250
251   async fn send_delete(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
252   async fn send_undo_delete(
253     &self,
254     creator: &User_,
255     context: &LemmyContext,
256   ) -> Result<(), LemmyError>;
257
258   async fn send_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
259   async fn send_undo_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
260
261   /// For a given community, returns the inboxes of all followers.
262   async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<Url>, LemmyError>;
263
264   fn actor_id(&self) -> Result<Url, ParseError> {
265     Url::parse(&self.actor_id_str())
266   }
267
268   // TODO move these to the db rows
269   fn get_inbox_url(&self) -> Result<Url, ParseError> {
270     Url::parse(&format!("{}/inbox", &self.actor_id_str()))
271   }
272
273   fn get_shared_inbox_url(&self) -> Result<Url, LemmyError> {
274     let actor_id = self.actor_id()?;
275     let url = format!(
276       "{}://{}{}/inbox",
277       &actor_id.scheme(),
278       &actor_id.host_str().context(location_info!())?,
279       if let Some(port) = actor_id.port() {
280         format!(":{}", port)
281       } else {
282         "".to_string()
283       },
284     );
285     Ok(Url::parse(&url)?)
286   }
287
288   fn get_outbox_url(&self) -> Result<Url, ParseError> {
289     Url::parse(&format!("{}/outbox", &self.actor_id_str()))
290   }
291
292   fn get_followers_url(&self) -> Result<Url, ParseError> {
293     Url::parse(&format!("{}/followers", &self.actor_id_str()))
294   }
295
296   fn get_following_url(&self) -> String {
297     format!("{}/following", &self.actor_id_str())
298   }
299
300   fn get_liked_url(&self) -> String {
301     format!("{}/liked", &self.actor_id_str())
302   }
303
304   fn get_public_key_ext(&self) -> Result<PublicKeyExtension, LemmyError> {
305     Ok(
306       PublicKey {
307         id: format!("{}#main-key", self.actor_id_str()),
308         owner: self.actor_id_str(),
309         public_key_pem: self.public_key().context(location_info!())?,
310       }
311       .to_ext(),
312     )
313   }
314 }
315
316 pub async fn fetch_webfinger_url(
317   mention: &MentionData,
318   client: &Client,
319 ) -> Result<Url, LemmyError> {
320   let fetch_url = format!(
321     "{}://{}/.well-known/webfinger?resource=acct:{}@{}",
322     get_apub_protocol_string(),
323     mention.domain,
324     mention.name,
325     mention.domain
326   );
327   debug!("Fetching webfinger url: {}", &fetch_url);
328
329   let response = retry(|| client.get(&fetch_url).send()).await?;
330
331   let res: WebFingerResponse = response
332     .json()
333     .await
334     .map_err(|e| RecvError(e.to_string()))?;
335
336   let link = res
337     .links
338     .iter()
339     .find(|l| l.type_.eq(&Some("application/activity+json".to_string())))
340     .ok_or_else(|| anyhow!("No application/activity+json link found."))?;
341   link
342     .href
343     .to_owned()
344     .map(|u| Url::parse(&u))
345     .transpose()?
346     .ok_or_else(|| anyhow!("No href found.").into())
347 }
348
349 pub async fn insert_activity<T>(
350   user_id: i32,
351   data: T,
352   local: bool,
353   pool: &DbPool,
354 ) -> Result<(), LemmyError>
355 where
356   T: Serialize + std::fmt::Debug + Send + 'static,
357 {
358   blocking(pool, move |conn| {
359     do_insert_activity(conn, user_id, &data, local)
360   })
361   .await??;
362   Ok(())
363 }