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