]> Untitled Git - lemmy.git/blob - crates/apub/src/lib.rs
Rewrite delete activities (#1699)
[lemmy.git] / crates / 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 extensions;
7 pub mod fetcher;
8 pub mod http;
9 pub mod migrations;
10 pub mod objects;
11
12 use crate::extensions::signatures::PublicKey;
13 use activitystreams::base::AnyBase;
14 use anyhow::{anyhow, Context};
15 use diesel::NotFound;
16 use lemmy_api_common::blocking;
17 use lemmy_db_queries::{source::activity::Activity_, ApubObject, DbPool};
18 use lemmy_db_schema::{
19   source::{
20     activity::Activity,
21     comment::Comment,
22     community::Community,
23     person::{Person as DbPerson, Person},
24     post::Post,
25     private_message::PrivateMessage,
26   },
27   CommunityId,
28   DbUrl,
29 };
30 use lemmy_db_views_actor::community_person_ban_view::CommunityPersonBanView;
31 use lemmy_utils::{location_info, settings::structs::Settings, LemmyError};
32 use lemmy_websocket::LemmyContext;
33 use serde::Serialize;
34 use std::net::IpAddr;
35 use url::{ParseError, Url};
36
37 static APUB_JSON_CONTENT_TYPE: &str = "application/activity+json";
38
39 /// Checks if the ID is allowed for sending or receiving.
40 ///
41 /// In particular, it checks for:
42 /// - federation being enabled (if its disabled, only local URLs are allowed)
43 /// - the correct scheme (either http or https)
44 /// - URL being in the allowlist (if it is active)
45 /// - URL not being in the blocklist (if it is active)
46 ///
47 pub(crate) fn check_is_apub_id_valid(
48   apub_id: &Url,
49   use_strict_allowlist: bool,
50 ) -> Result<(), LemmyError> {
51   let settings = Settings::get();
52   let domain = apub_id.domain().context(location_info!())?.to_string();
53   let local_instance = settings.get_hostname_without_port()?;
54
55   if !settings.federation.enabled {
56     return if domain == local_instance {
57       Ok(())
58     } else {
59       Err(
60         anyhow!(
61           "Trying to connect with {}, but federation is disabled",
62           domain
63         )
64         .into(),
65       )
66     };
67   }
68
69   let host = apub_id.host_str().context(location_info!())?;
70   let host_as_ip = host.parse::<IpAddr>();
71   if host == "localhost" || host_as_ip.is_ok() {
72     return Err(anyhow!("invalid hostname {}: {}", host, apub_id).into());
73   }
74
75   if apub_id.scheme() != Settings::get().get_protocol_string() {
76     return Err(anyhow!("invalid apub id scheme {}: {}", apub_id.scheme(), apub_id).into());
77   }
78
79   // TODO: might be good to put the part above in one method, and below in another
80   //       (which only gets called in apub::objects)
81   //        -> no that doesnt make sense, we still need the code below for blocklist and strict allowlist
82   if let Some(blocked) = Settings::get().federation.blocked_instances {
83     if blocked.contains(&domain) {
84       return Err(anyhow!("{} is in federation blocklist", domain).into());
85     }
86   }
87
88   if let Some(mut allowed) = Settings::get().federation.allowed_instances {
89     // Only check allowlist if this is a community, or strict allowlist is enabled.
90     let strict_allowlist = Settings::get().federation.strict_allowlist;
91     if use_strict_allowlist || strict_allowlist {
92       // need to allow this explicitly because apub receive might contain objects from our local
93       // instance.
94       allowed.push(local_instance);
95
96       if !allowed.contains(&domain) {
97         return Err(anyhow!("{} not in federation allowlist", domain).into());
98       }
99     }
100   }
101
102   Ok(())
103 }
104
105 /// Common methods provided by ActivityPub actors (community and person). Not all methods are
106 /// implemented by all actors.
107 trait ActorType {
108   fn is_local(&self) -> bool;
109   fn actor_id(&self) -> Url;
110   fn name(&self) -> String;
111
112   // TODO: every actor should have a public key, so this shouldnt be an option (needs to be fixed in db)
113   fn public_key(&self) -> Option<String>;
114   fn private_key(&self) -> Option<String>;
115
116   fn get_shared_inbox_or_inbox_url(&self) -> Url;
117
118   /// Outbox URL is not generally used by Lemmy, so it can be generated on the fly (but only for
119   /// local actors).
120   fn get_outbox_url(&self) -> Result<Url, LemmyError> {
121     /* TODO
122     if !self.is_local() {
123       return Err(anyhow!("get_outbox_url() called for remote actor").into());
124     }
125     */
126     Ok(Url::parse(&format!("{}/outbox", &self.actor_id()))?)
127   }
128
129   fn get_public_key(&self) -> Result<PublicKey, LemmyError> {
130     Ok(PublicKey {
131       id: format!("{}#main-key", self.actor_id()),
132       owner: self.actor_id(),
133       public_key_pem: self.public_key().context(location_info!())?,
134     })
135   }
136 }
137
138 #[async_trait::async_trait(?Send)]
139 pub trait CommunityType {
140   fn followers_url(&self) -> Url;
141   async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<Url>, LemmyError>;
142
143   async fn send_update(&self, mod_: Person, context: &LemmyContext) -> Result<(), LemmyError>;
144
145   async fn send_announce(
146     &self,
147     activity: AnyBase,
148     object: Option<Url>,
149     context: &LemmyContext,
150   ) -> Result<(), LemmyError>;
151
152   async fn send_add_mod(
153     &self,
154     actor: &Person,
155     added_mod: Person,
156     context: &LemmyContext,
157   ) -> Result<(), LemmyError>;
158   async fn send_remove_mod(
159     &self,
160     actor: &Person,
161     removed_mod: Person,
162     context: &LemmyContext,
163   ) -> Result<(), LemmyError>;
164
165   async fn send_block_user(
166     &self,
167     actor: &Person,
168     blocked_user: Person,
169     context: &LemmyContext,
170   ) -> Result<(), LemmyError>;
171   async fn send_undo_block_user(
172     &self,
173     actor: &Person,
174     blocked_user: Person,
175     context: &LemmyContext,
176   ) -> Result<(), LemmyError>;
177 }
178
179 pub enum EndpointType {
180   Community,
181   Person,
182   Post,
183   Comment,
184   PrivateMessage,
185 }
186
187 /// Generates an apub endpoint for a given domain, IE xyz.tld
188 fn generate_apub_endpoint_for_domain(
189   endpoint_type: EndpointType,
190   name: &str,
191   domain: &str,
192 ) -> Result<DbUrl, ParseError> {
193   let point = match endpoint_type {
194     EndpointType::Community => "c",
195     EndpointType::Person => "u",
196     EndpointType::Post => "post",
197     EndpointType::Comment => "comment",
198     EndpointType::PrivateMessage => "private_message",
199   };
200
201   Ok(Url::parse(&format!("{}/{}/{}", domain, point, name))?.into())
202 }
203
204 /// Generates the ActivityPub ID for a given object type and ID.
205 pub fn generate_apub_endpoint(
206   endpoint_type: EndpointType,
207   name: &str,
208 ) -> Result<DbUrl, ParseError> {
209   generate_apub_endpoint_for_domain(
210     endpoint_type,
211     name,
212     &Settings::get().get_protocol_and_hostname(),
213   )
214 }
215
216 pub fn generate_followers_url(actor_id: &DbUrl) -> Result<DbUrl, ParseError> {
217   Ok(Url::parse(&format!("{}/followers", actor_id))?.into())
218 }
219
220 pub fn generate_inbox_url(actor_id: &DbUrl) -> Result<DbUrl, ParseError> {
221   Ok(Url::parse(&format!("{}/inbox", actor_id))?.into())
222 }
223
224 pub fn generate_shared_inbox_url(actor_id: &DbUrl) -> Result<DbUrl, LemmyError> {
225   let actor_id: Url = actor_id.clone().into();
226   let url = format!(
227     "{}://{}{}/inbox",
228     &actor_id.scheme(),
229     &actor_id.host_str().context(location_info!())?,
230     if let Some(port) = actor_id.port() {
231       format!(":{}", port)
232     } else {
233       "".to_string()
234     },
235   );
236   Ok(Url::parse(&url)?.into())
237 }
238
239 fn generate_moderators_url(community_id: &DbUrl) -> Result<DbUrl, LemmyError> {
240   Ok(Url::parse(&format!("{}/moderators", community_id))?.into())
241 }
242
243 /// Takes in a shortname of the type dessalines@xyz.tld or dessalines (assumed to be local), and outputs the actor id.
244 /// Used in the API for communities and users.
245 pub fn build_actor_id_from_shortname(
246   endpoint_type: EndpointType,
247   short_name: &str,
248 ) -> Result<DbUrl, ParseError> {
249   let split = short_name.split('@').collect::<Vec<&str>>();
250
251   let name = split[0];
252
253   // If there's no @, its local
254   let domain = if split.len() == 1 {
255     Settings::get().get_protocol_and_hostname()
256   } else {
257     format!("{}://{}", Settings::get().get_protocol_string(), split[1])
258   };
259
260   generate_apub_endpoint_for_domain(endpoint_type, name, &domain)
261 }
262
263 /// Store a sent or received activity in the database, for logging purposes. These records are not
264 /// persistent.
265 async fn insert_activity<T>(
266   ap_id: &Url,
267   activity: T,
268   local: bool,
269   sensitive: bool,
270   pool: &DbPool,
271 ) -> Result<(), LemmyError>
272 where
273   T: Serialize + std::fmt::Debug + Send + 'static,
274 {
275   let ap_id = ap_id.to_owned().into();
276   blocking(pool, move |conn| {
277     Activity::insert(conn, ap_id, &activity, local, sensitive)
278   })
279   .await??;
280   Ok(())
281 }
282
283 pub enum PostOrComment {
284   Comment(Box<Comment>),
285   Post(Box<Post>),
286 }
287
288 impl PostOrComment {
289   pub(crate) fn ap_id(&self) -> Url {
290     match self {
291       PostOrComment::Post(p) => p.ap_id.clone(),
292       PostOrComment::Comment(c) => c.ap_id.clone(),
293     }
294     .into()
295   }
296 }
297
298 /// Tries to find a post or comment in the local database, without any network requests.
299 /// This is used to handle deletions and removals, because in case we dont have the object, we can
300 /// simply ignore the activity.
301 pub(crate) async fn find_post_or_comment_by_id(
302   context: &LemmyContext,
303   apub_id: Url,
304 ) -> Result<PostOrComment, LemmyError> {
305   let ap_id = apub_id.clone();
306   let post = blocking(context.pool(), move |conn| {
307     Post::read_from_apub_id(conn, &ap_id.into())
308   })
309   .await?;
310   if let Ok(p) = post {
311     return Ok(PostOrComment::Post(Box::new(p)));
312   }
313
314   let ap_id = apub_id.clone();
315   let comment = blocking(context.pool(), move |conn| {
316     Comment::read_from_apub_id(conn, &ap_id.into())
317   })
318   .await?;
319   if let Ok(c) = comment {
320     return Ok(PostOrComment::Comment(Box::new(c)));
321   }
322
323   Err(NotFound.into())
324 }
325
326 #[derive(Debug)]
327 enum Object {
328   Comment(Box<Comment>),
329   Post(Box<Post>),
330   Community(Box<Community>),
331   Person(Box<DbPerson>),
332   PrivateMessage(Box<PrivateMessage>),
333 }
334
335 async fn find_object_by_id(context: &LemmyContext, apub_id: Url) -> Result<Object, LemmyError> {
336   let ap_id = apub_id.clone();
337   if let Ok(pc) = find_post_or_comment_by_id(context, ap_id.to_owned()).await {
338     return Ok(match pc {
339       PostOrComment::Post(p) => Object::Post(Box::new(*p)),
340       PostOrComment::Comment(c) => Object::Comment(Box::new(*c)),
341     });
342   }
343
344   let ap_id = apub_id.clone();
345   let person = blocking(context.pool(), move |conn| {
346     DbPerson::read_from_apub_id(conn, &ap_id.into())
347   })
348   .await?;
349   if let Ok(u) = person {
350     return Ok(Object::Person(Box::new(u)));
351   }
352
353   let ap_id = apub_id.clone();
354   let community = blocking(context.pool(), move |conn| {
355     Community::read_from_apub_id(conn, &ap_id.into())
356   })
357   .await?;
358   if let Ok(c) = community {
359     return Ok(Object::Community(Box::new(c)));
360   }
361
362   let private_message = blocking(context.pool(), move |conn| {
363     PrivateMessage::read_from_apub_id(conn, &apub_id.into())
364   })
365   .await?;
366   if let Ok(pm) = private_message {
367     return Ok(Object::PrivateMessage(Box::new(pm)));
368   }
369
370   Err(NotFound.into())
371 }
372
373 async fn check_community_or_site_ban(
374   person: &Person,
375   community_id: CommunityId,
376   pool: &DbPool,
377 ) -> Result<(), LemmyError> {
378   if person.banned {
379     return Err(anyhow!("Person is banned from site").into());
380   }
381   let person_id = person.id;
382   let is_banned =
383     move |conn: &'_ _| CommunityPersonBanView::get(conn, person_id, community_id).is_ok();
384   if blocking(pool, is_banned).await? {
385     return Err(anyhow!("Person is banned from community").into());
386   }
387
388   Ok(())
389 }