]> Untitled Git - lemmy.git/blob - crates/apub/src/objects/instance.rs
Upgrade activitypub_federation to 0.2.0, add setting federation.debug (#2300)
[lemmy.git] / crates / apub / src / objects / instance.rs
1 use crate::{
2   check_apub_id_valid_with_strictness,
3   local_instance,
4   objects::read_from_string_or_source_opt,
5   protocol::{
6     objects::instance::{Instance, InstanceType},
7     ImageObject,
8     Source,
9   },
10   ActorType,
11 };
12 use activitypub_federation::{
13   core::object_id::ObjectId,
14   deser::values::MediaTypeHtml,
15   traits::{Actor, ApubObject},
16   utils::verify_domains_match,
17 };
18 use chrono::NaiveDateTime;
19 use lemmy_api_common::utils::blocking;
20 use lemmy_db_schema::{
21   source::site::{Site, SiteForm},
22   utils::{naive_now, DbPool},
23 };
24 use lemmy_utils::{
25   error::LemmyError,
26   utils::{check_slurs, check_slurs_opt, convert_datetime, markdown_to_html},
27 };
28 use lemmy_websocket::LemmyContext;
29 use std::ops::Deref;
30 use tracing::debug;
31 use url::Url;
32
33 #[derive(Clone, Debug)]
34 pub struct ApubSite(Site);
35
36 impl Deref for ApubSite {
37   type Target = Site;
38   fn deref(&self) -> &Self::Target {
39     &self.0
40   }
41 }
42
43 impl From<Site> for ApubSite {
44   fn from(s: Site) -> Self {
45     ApubSite(s)
46   }
47 }
48
49 #[async_trait::async_trait(?Send)]
50 impl ApubObject for ApubSite {
51   type DataType = LemmyContext;
52   type ApubType = Instance;
53   type DbType = Site;
54   type Error = LemmyError;
55
56   fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
57     Some(self.last_refreshed_at)
58   }
59
60   #[tracing::instrument(skip_all)]
61   async fn read_from_apub_id(
62     object_id: Url,
63     data: &Self::DataType,
64   ) -> Result<Option<Self>, LemmyError> {
65     Ok(
66       blocking(data.pool(), move |conn| {
67         Site::read_from_apub_id(conn, object_id)
68       })
69       .await??
70       .map(Into::into),
71     )
72   }
73
74   async fn delete(self, _data: &Self::DataType) -> Result<(), LemmyError> {
75     unimplemented!()
76   }
77
78   #[tracing::instrument(skip_all)]
79   async fn into_apub(self, _data: &Self::DataType) -> Result<Self::ApubType, LemmyError> {
80     let instance = Instance {
81       kind: InstanceType::Service,
82       id: ObjectId::new(self.actor_id()),
83       name: self.name.clone(),
84       content: self.sidebar.as_ref().map(|d| markdown_to_html(d)),
85       source: self.sidebar.clone().map(Source::new),
86       summary: self.description.clone(),
87       media_type: self.sidebar.as_ref().map(|_| MediaTypeHtml::Html),
88       icon: self.icon.clone().map(ImageObject::new),
89       image: self.banner.clone().map(ImageObject::new),
90       inbox: self.inbox_url.clone().into(),
91       outbox: Url::parse(&format!("{}/site_outbox", self.actor_id))?,
92       public_key: self.get_public_key(),
93       published: convert_datetime(self.published),
94       updated: self.updated.map(convert_datetime),
95     };
96     Ok(instance)
97   }
98
99   #[tracing::instrument(skip_all)]
100   async fn verify(
101     apub: &Self::ApubType,
102     expected_domain: &Url,
103     data: &Self::DataType,
104     _request_counter: &mut i32,
105   ) -> Result<(), LemmyError> {
106     check_apub_id_valid_with_strictness(apub.id.inner(), true, &data.settings())?;
107     verify_domains_match(expected_domain, apub.id.inner())?;
108
109     let slur_regex = &data.settings().slur_regex();
110     check_slurs(&apub.name, slur_regex)?;
111     check_slurs_opt(&apub.summary, slur_regex)?;
112     Ok(())
113   }
114
115   #[tracing::instrument(skip_all)]
116   async fn from_apub(
117     apub: Self::ApubType,
118     data: &Self::DataType,
119     _request_counter: &mut i32,
120   ) -> Result<Self, LemmyError> {
121     let site_form = SiteForm {
122       name: apub.name.clone(),
123       sidebar: Some(read_from_string_or_source_opt(
124         &apub.content,
125         &None,
126         &apub.source,
127       )),
128       updated: apub.updated.map(|u| u.clone().naive_local()),
129       icon: Some(apub.icon.clone().map(|i| i.url.into())),
130       banner: Some(apub.image.clone().map(|i| i.url.into())),
131       description: Some(apub.summary.clone()),
132       actor_id: Some(apub.id.clone().into()),
133       last_refreshed_at: Some(naive_now()),
134       inbox_url: Some(apub.inbox.clone().into()),
135       public_key: Some(apub.public_key.public_key_pem.clone()),
136       ..SiteForm::default()
137     };
138     let site = blocking(data.pool(), move |conn| Site::upsert(conn, &site_form)).await??;
139     Ok(site.into())
140   }
141 }
142
143 impl ActorType for ApubSite {
144   fn actor_id(&self) -> Url {
145     self.actor_id.to_owned().into()
146   }
147   fn private_key(&self) -> Option<String> {
148     self.private_key.to_owned()
149   }
150 }
151
152 impl Actor for ApubSite {
153   fn public_key(&self) -> &str {
154     &self.public_key
155   }
156
157   fn inbox(&self) -> Url {
158     self.inbox_url.clone().into()
159   }
160 }
161
162 /// Instance actor is at the root path, so we simply need to clear the path and other unnecessary
163 /// parts of the url.
164 pub fn instance_actor_id_from_url(mut url: Url) -> Url {
165   url.set_fragment(None);
166   url.set_path("");
167   url.set_query(None);
168   url
169 }
170
171 /// try to fetch the instance actor (to make things like instance rules available)
172 pub(in crate::objects) async fn fetch_instance_actor_for_object(
173   object_id: Url,
174   context: &LemmyContext,
175   request_counter: &mut i32,
176 ) {
177   // try to fetch the instance actor (to make things like instance rules available)
178   let instance_id = instance_actor_id_from_url(object_id);
179   let site = ObjectId::<ApubSite>::new(instance_id.clone())
180     .dereference(context, local_instance(context), request_counter)
181     .await;
182   if let Err(e) = site {
183     debug!("Failed to dereference site for {}: {}", instance_id, e);
184   }
185 }
186
187 pub(crate) async fn remote_instance_inboxes(pool: &DbPool) -> Result<Vec<Url>, LemmyError> {
188   Ok(
189     blocking(pool, Site::read_remote_sites)
190       .await??
191       .into_iter()
192       .map(|s| ApubSite::from(s).shared_inbox_or_inbox())
193       .collect(),
194   )
195 }
196
197 #[cfg(test)]
198 pub(crate) mod tests {
199   use super::*;
200   use crate::{objects::tests::init_context, protocol::tests::file_to_json_object};
201   use lemmy_db_schema::traits::Crud;
202   use serial_test::serial;
203
204   pub(crate) async fn parse_lemmy_instance(context: &LemmyContext) -> ApubSite {
205     let json: Instance = file_to_json_object("assets/lemmy/objects/instance.json").unwrap();
206     let id = Url::parse("https://enterprise.lemmy.ml/").unwrap();
207     let mut request_counter = 0;
208     ApubSite::verify(&json, &id, context, &mut request_counter)
209       .await
210       .unwrap();
211     let site = ApubSite::from_apub(json, context, &mut request_counter)
212       .await
213       .unwrap();
214     assert_eq!(request_counter, 0);
215     site
216   }
217
218   #[actix_rt::test]
219   #[serial]
220   async fn test_parse_lemmy_instance() {
221     let context = init_context();
222     let site = parse_lemmy_instance(&context).await;
223
224     assert_eq!(site.name, "Enterprise");
225     assert_eq!(site.description.as_ref().unwrap().len(), 15);
226
227     Site::delete(&*context.pool().get().unwrap(), site.id).unwrap();
228   }
229 }