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};
6 use reqwest::StatusCode;
7 use reqwest_middleware::ClientWithMiddleware;
8 use serde::{Deserialize, Serialize};
10 fmt::{Debug, Display, Formatter},
17 /// We store Url on the heap because it is quite large (88 bytes).
18 #[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
20 pub struct ObjectId<Kind>(Box<Url>, #[serde(skip)] PhantomData<Kind>)
22 Kind: ApubObject + Send + 'static,
23 for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>;
25 impl<Kind> ObjectId<Kind>
27 Kind: ApubObject + Send + 'static,
28 for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>,
30 pub fn new<T>(url: T) -> Self
34 ObjectId(Box::new(url.into()), PhantomData::<Kind>)
37 pub fn inner(&self) -> &Url {
41 /// Fetches an activitypub object, either from local database (if possible), or over http.
42 pub async fn dereference(
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?;
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()),
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) {
64 .dereference_from_http(data, client, request_counter, Some(object))
70 // object not found, need to fetch over http
73 .dereference_from_http(data, client, request_counter, None)
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(
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())
88 /// returning none means the object was not found in local db
89 async fn dereference_from_db(
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
97 async fn dereference_from_http(
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());
108 *request_counter += 1;
109 if *request_counter > Settings::get().http_fetch_retry_limit {
110 return Err(LemmyError::from(anyhow!("Request retry limit reached")));
115 .get(self.0.as_str())
116 .header("Accept", APUB_JSON_CONTENT_TYPE)
117 .timeout(Duration::from_secs(60))
122 if res.status() == StatusCode::GONE {
123 if let Some(db_object) = db_object {
124 db_object.delete(data).await?;
126 return Err(anyhow!("Fetched remote object {} which was deleted", self).into());
129 let res2: Kind::ApubType = res.json().await?;
131 Kind::verify(&res2, self.inner(), data, request_counter).await?;
132 Ok(Kind::from_apub(res2, data, request_counter).await?)
136 static ACTOR_REFETCH_INTERVAL_SECONDS: i64 = 24 * 60 * 60;
137 static ACTOR_REFETCH_INTERVAL_SECONDS_DEBUG: i64 = 10;
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`.
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)
150 ChronoDuration::seconds(ACTOR_REFETCH_INTERVAL_SECONDS)
152 let refresh_limit = Utc::now().naive_utc() - update_interval;
153 last_refreshed.lt(&refresh_limit)
156 impl<Kind> Display for ObjectId<Kind>
158 Kind: ApubObject + Send + 'static,
159 for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>,
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)
168 impl<Kind> From<ObjectId<Kind>> for Url
170 Kind: ApubObject + Send + 'static,
171 for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>,
173 fn from(id: ObjectId<Kind>) -> Self {
181 use crate::object_id::should_refetch_object;
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));
188 let two_days_ago = Utc::now().naive_utc() - ChronoDuration::days(2);
189 assert!(should_refetch_object(two_days_ago));