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