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