]> Untitled Git - lemmy.git/blob - crates/apub/src/fetcher/search.rs
Rewrite activitypub following, person, community, pm (#1692)
[lemmy.git] / crates / apub / src / fetcher / search.rs
1 use crate::{
2   fetcher::{
3     fetch::fetch_remote_object,
4     get_or_fetch_and_upsert_community,
5     get_or_fetch_and_upsert_person,
6     is_deleted,
7   },
8   find_object_by_id,
9   objects::{comment::Note, community::Group, person::Person as ApubPerson, post::Page, FromApub},
10   Object,
11 };
12 use anyhow::anyhow;
13 use lemmy_api_common::{blocking, site::SearchResponse};
14 use lemmy_db_queries::{
15   source::{
16     comment::Comment_,
17     community::Community_,
18     person::Person_,
19     post::Post_,
20     private_message::PrivateMessage_,
21   },
22   SearchType,
23 };
24 use lemmy_db_schema::source::{
25   comment::Comment,
26   community::Community,
27   person::Person,
28   post::Post,
29   private_message::PrivateMessage,
30 };
31 use lemmy_db_views::{comment_view::CommentView, post_view::PostView};
32 use lemmy_db_views_actor::{community_view::CommunityView, person_view::PersonViewSafe};
33 use lemmy_utils::{settings::structs::Settings, LemmyError};
34 use lemmy_websocket::LemmyContext;
35 use log::debug;
36 use url::Url;
37
38 /// The types of ActivityPub objects that can be fetched directly by searching for their ID.
39 #[derive(serde::Deserialize, Debug)]
40 #[serde(untagged)]
41 enum SearchAcceptedObjects {
42   Person(Box<ApubPerson>),
43   Group(Box<Group>),
44   Page(Box<Page>),
45   Comment(Box<Note>),
46 }
47
48 /// Attempt to parse the query as URL, and fetch an ActivityPub object from it.
49 ///
50 /// Some working examples for use with the `docker/federation/` setup:
51 /// http://lemmy_alpha:8541/c/main, or !main@lemmy_alpha:8541
52 /// http://lemmy_beta:8551/u/lemmy_alpha, or @lemmy_beta@lemmy_beta:8551
53 /// http://lemmy_gamma:8561/post/3
54 /// http://lemmy_delta:8571/comment/2
55 pub async fn search_by_apub_id(
56   query: &str,
57   context: &LemmyContext,
58 ) -> Result<SearchResponse, LemmyError> {
59   // Parse the shorthand query url
60   let query_url = if query.contains('@') {
61     debug!("Search for {}", query);
62     let split = query.split('@').collect::<Vec<&str>>();
63
64     // Person type will look like ['', username, instance]
65     // Community will look like [!community, instance]
66     let (name, instance) = if split.len() == 3 {
67       (format!("/u/{}", split[1]), split[2])
68     } else if split.len() == 2 {
69       if split[0].contains('!') {
70         let split2 = split[0].split('!').collect::<Vec<&str>>();
71         (format!("/c/{}", split2[1]), split[1])
72       } else {
73         return Err(anyhow!("Invalid search query: {}", query).into());
74       }
75     } else {
76       return Err(anyhow!("Invalid search query: {}", query).into());
77     };
78
79     let url = format!(
80       "{}://{}{}",
81       Settings::get().get_protocol_string(),
82       instance,
83       name
84     );
85     Url::parse(&url)?
86   } else {
87     Url::parse(query)?
88   };
89
90   let recursion_counter = &mut 0;
91   let fetch_response =
92     fetch_remote_object::<SearchAcceptedObjects>(context.client(), &query_url, recursion_counter)
93       .await;
94   if is_deleted(&fetch_response) {
95     delete_object_locally(&query_url, context).await?;
96   }
97
98   // Necessary because we get a stack overflow using FetchError
99   let fet_res = fetch_response.map_err(|e| LemmyError::from(e.inner))?;
100   build_response(fet_res, query_url, recursion_counter, context).await
101 }
102
103 async fn build_response(
104   fetch_response: SearchAcceptedObjects,
105   query_url: Url,
106   recursion_counter: &mut i32,
107   context: &LemmyContext,
108 ) -> Result<SearchResponse, LemmyError> {
109   let mut response = SearchResponse {
110     type_: SearchType::All.to_string(),
111     comments: vec![],
112     posts: vec![],
113     communities: vec![],
114     users: vec![],
115   };
116
117   match fetch_response {
118     SearchAcceptedObjects::Person(p) => {
119       let person_id = p.id(&query_url)?;
120       let person = get_or_fetch_and_upsert_person(person_id, context, recursion_counter).await?;
121
122       response.users = vec![
123         blocking(context.pool(), move |conn| {
124           PersonViewSafe::read(conn, person.id)
125         })
126         .await??,
127       ];
128     }
129     SearchAcceptedObjects::Group(g) => {
130       let community_uri = g.id(&query_url)?;
131       let community =
132         get_or_fetch_and_upsert_community(community_uri, context, recursion_counter).await?;
133
134       response.communities = vec![
135         blocking(context.pool(), move |conn| {
136           CommunityView::read(conn, community.id, None)
137         })
138         .await??,
139       ];
140     }
141     SearchAcceptedObjects::Page(p) => {
142       let p = Post::from_apub(&p, context, &query_url, recursion_counter).await?;
143
144       response.posts =
145         vec![blocking(context.pool(), move |conn| PostView::read(conn, p.id, None)).await??];
146     }
147     SearchAcceptedObjects::Comment(c) => {
148       let c = Comment::from_apub(&c, context, &query_url, recursion_counter).await?;
149
150       response.comments = vec![
151         blocking(context.pool(), move |conn| {
152           CommentView::read(conn, c.id, None)
153         })
154         .await??,
155       ];
156     }
157   };
158
159   Ok(response)
160 }
161
162 async fn delete_object_locally(query_url: &Url, context: &LemmyContext) -> Result<(), LemmyError> {
163   let res = find_object_by_id(context, query_url.to_owned()).await?;
164   match res {
165     Object::Comment(c) => {
166       blocking(context.pool(), move |conn| {
167         Comment::update_deleted(conn, c.id, true)
168       })
169       .await??;
170     }
171     Object::Post(p) => {
172       blocking(context.pool(), move |conn| {
173         Post::update_deleted(conn, p.id, true)
174       })
175       .await??;
176     }
177     Object::Person(u) => {
178       // TODO: implement update_deleted() for user, move it to ApubObject trait
179       blocking(context.pool(), move |conn| {
180         Person::delete_account(conn, u.id)
181       })
182       .await??;
183     }
184     Object::Community(c) => {
185       blocking(context.pool(), move |conn| {
186         Community::update_deleted(conn, c.id, true)
187       })
188       .await??;
189     }
190     Object::PrivateMessage(pm) => {
191       blocking(context.pool(), move |conn| {
192         PrivateMessage::update_deleted(conn, pm.id, true)
193       })
194       .await??;
195     }
196   }
197   Err(anyhow!("Object was deleted").into())
198 }