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