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