1 use crate::{traits::ApubObject, APUB_JSON_CONTENT_TYPE};
3 use chrono::{Duration as ChronoDuration, NaiveDateTime, Utc};
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};
10 fmt::{Debug, Display, Formatter},
16 /// We store Url on the heap because it is quite large (88 bytes).
17 #[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
19 pub struct ObjectId<Kind>(Box<Url>, #[serde(skip)] PhantomData<Kind>)
21 Kind: ApubObject + Send + 'static,
22 for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>;
24 impl<Kind> ObjectId<Kind>
26 Kind: ApubObject + Send + 'static,
27 for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>,
29 pub fn new<T>(url: T) -> Self
33 ObjectId(Box::new(url.into()), PhantomData::<Kind>)
36 pub fn inner(&self) -> &Url {
40 /// Fetches an activitypub object, either from local database (if possible), or over http.
41 pub async fn dereference(
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?;
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()),
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) {
63 .dereference_from_http(data, client, request_counter, Some(object))
69 // object not found, need to fetch over http
72 .dereference_from_http(data, client, request_counter, None)
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(
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())
87 /// returning none means the object was not found in local db
88 async fn dereference_from_db(
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
96 async fn dereference_from_http(
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());
107 *request_counter += 1;
108 if *request_counter > Settings::get().http_fetch_retry_limit {
109 return Err(LemmyError::from(anyhow!("Request retry limit reached")));
114 .get(self.0.as_str())
115 .header("Accept", APUB_JSON_CONTENT_TYPE)
116 .timeout(REQWEST_TIMEOUT)
121 if res.status() == StatusCode::GONE {
122 if let Some(db_object) = db_object {
123 db_object.delete(data).await?;
125 return Err(anyhow!("Fetched remote object {} which was deleted", self).into());
128 let res2: Kind::ApubType = res.json().await?;
130 Kind::verify(&res2, self.inner(), data, request_counter).await?;
131 Kind::from_apub(res2, data, request_counter).await
135 static ACTOR_REFETCH_INTERVAL_SECONDS: i64 = 24 * 60 * 60;
136 static ACTOR_REFETCH_INTERVAL_SECONDS_DEBUG: i64 = 20;
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`.
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)
149 ChronoDuration::seconds(ACTOR_REFETCH_INTERVAL_SECONDS)
151 let refresh_limit = Utc::now().naive_utc() - update_interval;
152 last_refreshed.lt(&refresh_limit)
155 impl<Kind> Display for ObjectId<Kind>
157 Kind: ApubObject + Send + 'static,
158 for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>,
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)
167 impl<Kind> From<ObjectId<Kind>> for Url
169 Kind: ApubObject + Send + 'static,
170 for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>,
172 fn from(id: ObjectId<Kind>) -> Self {
180 use crate::object_id::should_refetch_object;
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));
187 let two_days_ago = Utc::now().naive_utc() - ChronoDuration::days(2);
188 assert!(should_refetch_object(two_days_ago));