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