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