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