]> Untitled Git - lemmy.git/blob - crates/apub_lib/src/object_id.rs
Adding unique constraint for activity ap_id. Fixes #1878 (#1935)
[lemmy.git] / crates / apub_lib / src / object_id.rs
1 use crate::{traits::ApubObject, APUB_JSON_CONTENT_TYPE};
2 use activitystreams::chrono::{Duration as ChronoDuration, NaiveDateTime, Utc};
3 use anyhow::anyhow;
4 use diesel::NotFound;
5 use lemmy_utils::{
6   request::{build_user_agent, retry},
7   settings::structs::Settings,
8   LemmyError,
9 };
10 use log::info;
11 use reqwest::{Client, StatusCode};
12 use serde::{Deserialize, Serialize};
13 use std::{
14   fmt::{Debug, Display, Formatter},
15   marker::PhantomData,
16   time::Duration,
17 };
18 use url::Url;
19
20 /// Maximum number of HTTP requests allowed to handle a single incoming activity (or a single object
21 /// fetch through the search). This should be configurable.
22 static REQUEST_LIMIT: i32 = 25;
23
24 lazy_static! {
25   static ref CLIENT: Client = Client::builder()
26     .user_agent(build_user_agent(&Settings::get()))
27     .build()
28     .expect("Couldn't build client");
29 }
30
31 /// We store Url on the heap because it is quite large (88 bytes).
32 #[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
33 #[serde(transparent)]
34 pub struct ObjectId<Kind>(Box<Url>, #[serde(skip)] PhantomData<Kind>)
35 where
36   Kind: ApubObject + Send + 'static,
37   for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>;
38
39 impl<Kind> ObjectId<Kind>
40 where
41   Kind: ApubObject + Send + 'static,
42   for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>,
43 {
44   pub fn new<T>(url: T) -> Self
45   where
46     T: Into<Url>,
47   {
48     ObjectId(Box::new(url.into()), PhantomData::<Kind>)
49   }
50
51   pub fn inner(&self) -> &Url {
52     &self.0
53   }
54
55   /// Fetches an activitypub object, either from local database (if possible), or over http.
56   pub async fn dereference(
57     &self,
58     data: &<Kind as ApubObject>::DataType,
59     request_counter: &mut i32,
60   ) -> Result<Kind, LemmyError> {
61     let db_object = self.dereference_from_db(data).await?;
62
63     // if its a local object, only fetch it from the database and not over http
64     if self.0.domain() == Some(&Settings::get().get_hostname_without_port()?) {
65       return match db_object {
66         None => Err(NotFound {}.into()),
67         Some(o) => Ok(o),
68       };
69     }
70
71     // object found in database
72     if let Some(object) = db_object {
73       // object is old and should be refetched
74       if let Some(last_refreshed_at) = object.last_refreshed_at() {
75         if should_refetch_object(last_refreshed_at) {
76           return self
77             .dereference_from_http(data, request_counter, Some(object))
78             .await;
79         }
80       }
81       Ok(object)
82     }
83     // object not found, need to fetch over http
84     else {
85       self
86         .dereference_from_http(data, request_counter, None)
87         .await
88     }
89   }
90
91   /// Fetch an object from the local db. Instead of falling back to http, this throws an error if
92   /// the object is not found in the database.
93   pub async fn dereference_local(
94     &self,
95     data: &<Kind as ApubObject>::DataType,
96   ) -> Result<Kind, LemmyError> {
97     let object = self.dereference_from_db(data).await?;
98     object.ok_or_else(|| anyhow!("object not found in database {}", self).into())
99   }
100
101   /// returning none means the object was not found in local db
102   async fn dereference_from_db(
103     &self,
104     data: &<Kind as ApubObject>::DataType,
105   ) -> Result<Option<Kind>, LemmyError> {
106     let id = self.0.clone();
107     ApubObject::read_from_apub_id(*id, data).await
108   }
109
110   async fn dereference_from_http(
111     &self,
112     data: &<Kind as ApubObject>::DataType,
113     request_counter: &mut i32,
114     db_object: Option<Kind>,
115   ) -> Result<Kind, LemmyError> {
116     // dont fetch local objects this way
117     debug_assert!(self.0.domain() != Some(&Settings::get().hostname));
118     info!("Fetching remote object {}", self.to_string());
119
120     *request_counter += 1;
121     if *request_counter > REQUEST_LIMIT {
122       return Err(LemmyError::from(anyhow!("Request limit reached")));
123     }
124
125     let res = retry(|| {
126       CLIENT
127         .get(self.0.as_str())
128         .header("Accept", APUB_JSON_CONTENT_TYPE)
129         .timeout(Duration::from_secs(60))
130         .send()
131     })
132     .await?;
133
134     if res.status() == StatusCode::GONE {
135       if let Some(db_object) = db_object {
136         db_object.delete(data).await?;
137       }
138       return Err(anyhow!("Fetched remote object {} which was deleted", self).into());
139     }
140
141     let res2: Kind::ApubType = res.json().await?;
142
143     Kind::verify(&res2, self.inner(), data, request_counter).await?;
144     Ok(Kind::from_apub(res2, data, request_counter).await?)
145   }
146 }
147
148 static ACTOR_REFETCH_INTERVAL_SECONDS: i64 = 24 * 60 * 60;
149 static ACTOR_REFETCH_INTERVAL_SECONDS_DEBUG: i64 = 10;
150
151 /// Determines when a remote actor should be refetched from its instance. In release builds, this is
152 /// `ACTOR_REFETCH_INTERVAL_SECONDS` after the last refetch, in debug builds
153 /// `ACTOR_REFETCH_INTERVAL_SECONDS_DEBUG`.
154 ///
155 /// TODO it won't pick up new avatars, summaries etc until a day after.
156 /// Actors need an "update" activity pushed to other servers to fix this.
157 fn should_refetch_object(last_refreshed: NaiveDateTime) -> bool {
158   let update_interval = if cfg!(debug_assertions) {
159     // avoid infinite loop when fetching community outbox
160     ChronoDuration::seconds(ACTOR_REFETCH_INTERVAL_SECONDS_DEBUG)
161   } else {
162     ChronoDuration::seconds(ACTOR_REFETCH_INTERVAL_SECONDS)
163   };
164   let refresh_limit = Utc::now().naive_utc() - update_interval;
165   last_refreshed.lt(&refresh_limit)
166 }
167
168 impl<Kind> Display for ObjectId<Kind>
169 where
170   Kind: ApubObject + Send + 'static,
171   for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>,
172 {
173   #[allow(clippy::to_string_in_display)]
174   fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
175     // Use to_string here because Url.display is not useful for us
176     write!(f, "{}", self.0.to_string())
177   }
178 }
179
180 impl<Kind> From<ObjectId<Kind>> for Url
181 where
182   Kind: ApubObject + Send + 'static,
183   for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>,
184 {
185   fn from(id: ObjectId<Kind>) -> Self {
186     *id.0
187   }
188 }
189
190 #[cfg(test)]
191 mod tests {
192   use super::*;
193   use crate::object_id::should_refetch_object;
194
195   #[test]
196   fn test_should_refetch_object() {
197     let one_second_ago = Utc::now().naive_utc() - ChronoDuration::seconds(1);
198     assert!(!should_refetch_object(one_second_ago));
199
200     let two_days_ago = Utc::now().naive_utc() - ChronoDuration::days(2);
201     assert!(should_refetch_object(two_days_ago));
202   }
203 }