]> Untitled Git - lemmy.git/blob - crates/apub/src/fetcher/search.rs
00fbf67fe8795ce71ca3eaac889664d95574ca4d
[lemmy.git] / crates / apub / src / fetcher / search.rs
1 use crate::{
2   fetcher::webfinger::webfinger_resolve_actor,
3   local_instance,
4   objects::{comment::ApubComment, community::ApubCommunity, person::ApubPerson, post::ApubPost},
5   protocol::objects::{group::Group, note::Note, page::Page, person::Person},
6 };
7 use activitypub_federation::{core::object_id::ObjectId, traits::ApubObject};
8 use chrono::NaiveDateTime;
9 use lemmy_utils::error::LemmyError;
10 use lemmy_websocket::LemmyContext;
11 use serde::Deserialize;
12 use url::Url;
13
14 /// Attempt to parse the query as URL, and fetch an ActivityPub object from it.
15 ///
16 /// Some working examples for use with the `docker/federation/` setup:
17 /// http://lemmy_alpha:8541/c/main, or !main@lemmy_alpha:8541
18 /// http://lemmy_beta:8551/u/lemmy_alpha, or @lemmy_beta@lemmy_beta:8551
19 /// http://lemmy_gamma:8561/post/3
20 /// http://lemmy_delta:8571/comment/2
21 #[tracing::instrument(skip_all)]
22 pub async fn search_by_apub_id(
23   query: &str,
24   context: &LemmyContext,
25 ) -> Result<SearchableObjects, LemmyError> {
26   let request_counter = &mut 0;
27   let instance = local_instance(context);
28   match Url::parse(query) {
29     Ok(url) => {
30       ObjectId::new(url)
31         .dereference(context, instance, request_counter)
32         .await
33     }
34     Err(_) => {
35       let mut chars = query.chars();
36       let kind = chars.next();
37       let identifier = chars.as_str();
38       match kind {
39         Some('@') => {
40           let id =
41             webfinger_resolve_actor::<ApubPerson>(identifier, context, request_counter).await?;
42           Ok(SearchableObjects::Person(
43             ObjectId::new(id)
44               .dereference(context, instance, request_counter)
45               .await?,
46           ))
47         }
48         Some('!') => {
49           let id =
50             webfinger_resolve_actor::<ApubCommunity>(identifier, context, request_counter).await?;
51           Ok(SearchableObjects::Community(
52             ObjectId::new(id)
53               .dereference(context, instance, request_counter)
54               .await?,
55           ))
56         }
57         _ => Err(LemmyError::from_message("invalid query")),
58       }
59     }
60   }
61 }
62
63 /// The types of ActivityPub objects that can be fetched directly by searching for their ID.
64 #[derive(Debug)]
65 pub enum SearchableObjects {
66   Person(ApubPerson),
67   Community(ApubCommunity),
68   Post(ApubPost),
69   Comment(ApubComment),
70 }
71
72 #[derive(Deserialize)]
73 #[serde(untagged)]
74 pub enum SearchableApubTypes {
75   Group(Group),
76   Person(Person),
77   Page(Page),
78   Note(Note),
79 }
80
81 #[async_trait::async_trait(?Send)]
82 impl ApubObject for SearchableObjects {
83   type DataType = LemmyContext;
84   type ApubType = SearchableApubTypes;
85   type DbType = ();
86   type Error = LemmyError;
87
88   fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
89     match self {
90       SearchableObjects::Person(p) => p.last_refreshed_at(),
91       SearchableObjects::Community(c) => c.last_refreshed_at(),
92       SearchableObjects::Post(p) => p.last_refreshed_at(),
93       SearchableObjects::Comment(c) => c.last_refreshed_at(),
94     }
95   }
96
97   // TODO: this is inefficient, because if the object is not in local db, it will run 4 db queries
98   //       before finally returning an error. it would be nice if we could check all 4 tables in
99   //       a single query.
100   //       we could skip this and always return an error, but then it would always fetch objects
101   //       over http, and not be able to mark objects as deleted that were deleted by remote server.
102   #[tracing::instrument(skip_all)]
103   async fn read_from_apub_id(
104     object_id: Url,
105     context: &LemmyContext,
106   ) -> Result<Option<Self>, LemmyError> {
107     let c = ApubCommunity::read_from_apub_id(object_id.clone(), context).await?;
108     if let Some(c) = c {
109       return Ok(Some(SearchableObjects::Community(c)));
110     }
111     let p = ApubPerson::read_from_apub_id(object_id.clone(), context).await?;
112     if let Some(p) = p {
113       return Ok(Some(SearchableObjects::Person(p)));
114     }
115     let p = ApubPost::read_from_apub_id(object_id.clone(), context).await?;
116     if let Some(p) = p {
117       return Ok(Some(SearchableObjects::Post(p)));
118     }
119     let c = ApubComment::read_from_apub_id(object_id, context).await?;
120     if let Some(c) = c {
121       return Ok(Some(SearchableObjects::Comment(c)));
122     }
123     Ok(None)
124   }
125
126   #[tracing::instrument(skip_all)]
127   async fn delete(self, data: &Self::DataType) -> Result<(), LemmyError> {
128     match self {
129       SearchableObjects::Person(p) => p.delete(data).await,
130       SearchableObjects::Community(c) => c.delete(data).await,
131       SearchableObjects::Post(p) => p.delete(data).await,
132       SearchableObjects::Comment(c) => c.delete(data).await,
133     }
134   }
135
136   async fn into_apub(self, _data: &Self::DataType) -> Result<Self::ApubType, LemmyError> {
137     unimplemented!()
138   }
139
140   #[tracing::instrument(skip_all)]
141   async fn verify(
142     apub: &Self::ApubType,
143     expected_domain: &Url,
144     data: &Self::DataType,
145     request_counter: &mut i32,
146   ) -> Result<(), LemmyError> {
147     match apub {
148       SearchableApubTypes::Group(a) => {
149         ApubCommunity::verify(a, expected_domain, data, request_counter).await
150       }
151       SearchableApubTypes::Person(a) => {
152         ApubPerson::verify(a, expected_domain, data, request_counter).await
153       }
154       SearchableApubTypes::Page(a) => {
155         ApubPost::verify(a, expected_domain, data, request_counter).await
156       }
157       SearchableApubTypes::Note(a) => {
158         ApubComment::verify(a, expected_domain, data, request_counter).await
159       }
160     }
161   }
162
163   #[tracing::instrument(skip_all)]
164   async fn from_apub(
165     apub: Self::ApubType,
166     context: &LemmyContext,
167     rc: &mut i32,
168   ) -> Result<Self, LemmyError> {
169     use SearchableApubTypes as SAT;
170     use SearchableObjects as SO;
171     Ok(match apub {
172       SAT::Group(g) => SO::Community(ApubCommunity::from_apub(g, context, rc).await?),
173       SAT::Person(p) => SO::Person(ApubPerson::from_apub(p, context, rc).await?),
174       SAT::Page(p) => SO::Post(ApubPost::from_apub(p, context, rc).await?),
175       SAT::Note(n) => SO::Comment(ApubComment::from_apub(n, context, rc).await?),
176     })
177   }
178 }