]> Untitled Git - lemmy.git/blob - server/src/apub/fetcher.rs
20bab4e2636648f84f493a30fa7b6201f32fa424
[lemmy.git] / server / src / apub / fetcher.rs
1 use activitystreams::object::Note;
2 use actix_web::Result;
3 use diesel::{result::Error::NotFound, PgConnection};
4 use failure::{Error, _core::fmt::Debug};
5 use log::debug;
6 use serde::Deserialize;
7 use std::time::Duration;
8 use url::Url;
9
10 use crate::{
11   api::site::SearchResponse,
12   db::{
13     comment::{Comment, CommentForm},
14     comment_view::CommentView,
15     community::{Community, CommunityForm, CommunityModerator, CommunityModeratorForm},
16     community_view::CommunityView,
17     post::{Post, PostForm},
18     post_view::PostView,
19     user::{UserForm, User_},
20     Crud,
21     Joinable,
22     SearchType,
23   },
24   naive_now,
25   routes::nodeinfo::{NodeInfo, NodeInfoWellKnown},
26 };
27
28 use crate::{
29   apub::{
30     get_apub_protocol_string,
31     is_apub_id_valid,
32     FromApub,
33     GroupExt,
34     PageExt,
35     PersonExt,
36     APUB_JSON_CONTENT_TYPE,
37   },
38   db::user_view::UserView,
39 };
40 use chrono::NaiveDateTime;
41
42 static ACTOR_REFETCH_INTERVAL_SECONDS: i64 = 24 * 60 * 60;
43
44 // Fetch nodeinfo metadata from a remote instance.
45 fn _fetch_node_info(domain: &str) -> Result<NodeInfo, Error> {
46   let well_known_uri = Url::parse(&format!(
47     "{}://{}/.well-known/nodeinfo",
48     get_apub_protocol_string(),
49     domain
50   ))?;
51   let well_known = fetch_remote_object::<NodeInfoWellKnown>(&well_known_uri)?;
52   Ok(fetch_remote_object::<NodeInfo>(&well_known.links.href)?)
53 }
54
55 /// Fetch any type of ActivityPub object, handling things like HTTP headers, deserialisation,
56 /// timeouts etc.
57 pub fn fetch_remote_object<Response>(url: &Url) -> Result<Response, Error>
58 where
59   Response: for<'de> Deserialize<'de>,
60 {
61   if !is_apub_id_valid(&url) {
62     return Err(format_err!("Activitypub uri invalid or blocked: {}", url));
63   }
64   // TODO: this function should return a future
65   let timeout = Duration::from_secs(60);
66   let text: String = attohttpc::get(url.as_str())
67     .header("Accept", APUB_JSON_CONTENT_TYPE)
68     .connect_timeout(timeout)
69     .timeout(timeout)
70     // .body(())
71     .send()?
72     .text()?;
73   let res: Response = serde_json::from_str(&text)?;
74   Ok(res)
75 }
76
77 /// The types of ActivityPub objects that can be fetched directly by searching for their ID.
78 #[serde(untagged)]
79 #[derive(serde::Deserialize, Debug)]
80 pub enum SearchAcceptedObjects {
81   Person(Box<PersonExt>),
82   Group(Box<GroupExt>),
83   Page(Box<PageExt>),
84   Comment(Box<Note>),
85 }
86
87 /// Attempt to parse the query as URL, and fetch an ActivityPub object from it.
88 ///
89 /// Some working examples for use with the docker/federation/ setup:
90 /// http://lemmy_alpha:8540/c/main, or !main@lemmy_alpha:8540
91 /// http://lemmy_alpha:8540/u/lemmy_alpha, or @lemmy_alpha@lemmy_alpha:8540
92 /// http://lemmy_alpha:8540/post/3
93 /// http://lemmy_alpha:8540/comment/2
94 pub fn search_by_apub_id(query: &str, conn: &PgConnection) -> Result<SearchResponse, Error> {
95   // Parse the shorthand query url
96   let query_url = if query.contains('@') {
97     debug!("{}", query);
98     let split = query.split('@').collect::<Vec<&str>>();
99
100     // User type will look like ['', username, instance]
101     // Community will look like [!community, instance]
102     let (name, instance) = if split.len() == 3 {
103       (format!("/u/{}", split[1]), split[2])
104     } else if split.len() == 2 {
105       if split[0].contains('!') {
106         let split2 = split[0].split('!').collect::<Vec<&str>>();
107         (format!("/c/{}", split2[1]), split[1])
108       } else {
109         return Err(format_err!("Invalid search query: {}", query));
110       }
111     } else {
112       return Err(format_err!("Invalid search query: {}", query));
113     };
114
115     let url = format!("{}://{}{}", get_apub_protocol_string(), instance, name);
116     Url::parse(&url)?
117   } else {
118     Url::parse(&query)?
119   };
120
121   let mut response = SearchResponse {
122     type_: SearchType::All.to_string(),
123     comments: vec![],
124     posts: vec![],
125     communities: vec![],
126     users: vec![],
127   };
128   match fetch_remote_object::<SearchAcceptedObjects>(&query_url)? {
129     SearchAcceptedObjects::Person(p) => {
130       let user_uri = p.inner.object_props.get_id().unwrap().to_string();
131       let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?;
132       response.users = vec![UserView::read(conn, user.id)?];
133     }
134     SearchAcceptedObjects::Group(g) => {
135       let community_uri = g.inner.object_props.get_id().unwrap().to_string();
136       let community = get_or_fetch_and_upsert_remote_community(&community_uri, &conn)?;
137       // TODO Maybe at some point in the future, fetch all the history of a community
138       // fetch_community_outbox(&c, conn)?;
139       response.communities = vec![CommunityView::read(conn, community.id, None)?];
140     }
141     SearchAcceptedObjects::Page(p) => {
142       let p = upsert_post(&PostForm::from_apub(&p, conn)?, conn)?;
143       response.posts = vec![PostView::read(conn, p.id, None)?];
144     }
145     SearchAcceptedObjects::Comment(c) => {
146       let post_url = c
147         .object_props
148         .get_many_in_reply_to_xsd_any_uris()
149         .unwrap()
150         .next()
151         .unwrap()
152         .to_string();
153       // TODO: also fetch parent comments if any
154       let post = fetch_remote_object(&Url::parse(&post_url)?)?;
155       upsert_post(&PostForm::from_apub(&post, conn)?, conn)?;
156       let c = upsert_comment(&CommentForm::from_apub(&c, conn)?, conn)?;
157       response.comments = vec![CommentView::read(conn, c.id, None)?];
158     }
159   }
160   Ok(response)
161 }
162
163 /// Check if a remote user exists, create if not found, if its too old update it.Fetch a user, insert/update it in the database and return the user.
164 pub fn get_or_fetch_and_upsert_remote_user(
165   apub_id: &str,
166   conn: &PgConnection,
167 ) -> Result<User_, Error> {
168   match User_::read_from_actor_id(&conn, &apub_id) {
169     Ok(u) => {
170       // If its older than a day, re-fetch it
171       if !u.local && should_refetch_actor(u.last_refreshed_at) {
172         debug!("Fetching and updating from remote user: {}", apub_id);
173         let person = fetch_remote_object::<PersonExt>(&Url::parse(apub_id)?)?;
174         let mut uf = UserForm::from_apub(&person, &conn)?;
175         uf.last_refreshed_at = Some(naive_now());
176         Ok(User_::update(&conn, u.id, &uf)?)
177       } else {
178         Ok(u)
179       }
180     }
181     Err(NotFound {}) => {
182       debug!("Fetching and creating remote user: {}", apub_id);
183       let person = fetch_remote_object::<PersonExt>(&Url::parse(apub_id)?)?;
184       let uf = UserForm::from_apub(&person, &conn)?;
185       Ok(User_::create(conn, &uf)?)
186     }
187     Err(e) => Err(Error::from(e)),
188   }
189 }
190
191 /// Determines when a remote actor should be refetched from its instance. In release builds, this is
192 /// ACTOR_REFETCH_INTERVAL_SECONDS after the last refetch, in debug builds always.
193 ///
194 /// TODO it won't pick up new avatars, summaries etc until a day after.
195 /// Actors need an "update" activity pushed to other servers to fix this.
196 fn should_refetch_actor(last_refreshed: NaiveDateTime) -> bool {
197   if cfg!(debug_assertions) {
198     true
199   } else {
200     let update_interval = chrono::Duration::seconds(ACTOR_REFETCH_INTERVAL_SECONDS);
201     last_refreshed.lt(&(naive_now() - update_interval))
202   }
203 }
204
205 /// Check if a remote community exists, create if not found, if its too old update it.Fetch a community, insert/update it in the database and return the community.
206 pub fn get_or_fetch_and_upsert_remote_community(
207   apub_id: &str,
208   conn: &PgConnection,
209 ) -> Result<Community, Error> {
210   match Community::read_from_actor_id(&conn, &apub_id) {
211     Ok(c) => {
212       if !c.local && should_refetch_actor(c.last_refreshed_at) {
213         debug!("Fetching and updating from remote community: {}", apub_id);
214         let group = fetch_remote_object::<GroupExt>(&Url::parse(apub_id)?)?;
215         let mut cf = CommunityForm::from_apub(&group, conn)?;
216         cf.last_refreshed_at = Some(naive_now());
217         Ok(Community::update(&conn, c.id, &cf)?)
218       } else {
219         Ok(c)
220       }
221     }
222     Err(NotFound {}) => {
223       debug!("Fetching and creating remote community: {}", apub_id);
224       let group = fetch_remote_object::<GroupExt>(&Url::parse(apub_id)?)?;
225       let cf = CommunityForm::from_apub(&group, conn)?;
226       let community = Community::create(conn, &cf)?;
227
228       // Also add the community moderators too
229       let creator_and_moderator_uris = group
230         .inner
231         .object_props
232         .get_many_attributed_to_xsd_any_uris()
233         .unwrap();
234       let creator_and_moderators = creator_and_moderator_uris
235         .map(|c| get_or_fetch_and_upsert_remote_user(&c.to_string(), &conn).unwrap())
236         .collect::<Vec<User_>>();
237
238       for mod_ in creator_and_moderators {
239         let community_moderator_form = CommunityModeratorForm {
240           community_id: community.id,
241           user_id: mod_.id,
242         };
243         CommunityModerator::join(&conn, &community_moderator_form)?;
244       }
245
246       Ok(community)
247     }
248     Err(e) => Err(Error::from(e)),
249   }
250 }
251
252 fn upsert_post(post_form: &PostForm, conn: &PgConnection) -> Result<Post, Error> {
253   let existing = Post::read_from_apub_id(conn, &post_form.ap_id);
254   match existing {
255     Err(NotFound {}) => Ok(Post::create(conn, &post_form)?),
256     Ok(p) => Ok(Post::update(conn, p.id, &post_form)?),
257     Err(e) => Err(Error::from(e)),
258   }
259 }
260
261 pub fn get_or_fetch_and_insert_remote_post(
262   post_ap_id: &str,
263   conn: &PgConnection,
264 ) -> Result<Post, Error> {
265   match Post::read_from_apub_id(conn, post_ap_id) {
266     Ok(p) => Ok(p),
267     Err(NotFound {}) => {
268       debug!("Fetching and creating remote post: {}", post_ap_id);
269       let post = fetch_remote_object::<PageExt>(&Url::parse(post_ap_id)?)?;
270       let post_form = PostForm::from_apub(&post, conn)?;
271       Ok(Post::create(conn, &post_form)?)
272     }
273     Err(e) => Err(Error::from(e)),
274   }
275 }
276
277 fn upsert_comment(comment_form: &CommentForm, conn: &PgConnection) -> Result<Comment, Error> {
278   let existing = Comment::read_from_apub_id(conn, &comment_form.ap_id);
279   match existing {
280     Err(NotFound {}) => Ok(Comment::create(conn, &comment_form)?),
281     Ok(p) => Ok(Comment::update(conn, p.id, &comment_form)?),
282     Err(e) => Err(Error::from(e)),
283   }
284 }
285
286 pub fn get_or_fetch_and_insert_remote_comment(
287   comment_ap_id: &str,
288   conn: &PgConnection,
289 ) -> Result<Comment, Error> {
290   match Comment::read_from_apub_id(conn, comment_ap_id) {
291     Ok(p) => Ok(p),
292     Err(NotFound {}) => {
293       debug!(
294         "Fetching and creating remote comment and its parents: {}",
295         comment_ap_id
296       );
297       let comment = fetch_remote_object::<Note>(&Url::parse(comment_ap_id)?)?;
298       let comment_form = CommentForm::from_apub(&comment, conn)?;
299       Ok(Comment::create(conn, &comment_form)?)
300     }
301     Err(e) => Err(Error::from(e)),
302   }
303 }
304
305 // TODO It should not be fetching data from a community outbox.
306 // All posts, comments, comment likes, etc should be posts to our community_inbox
307 // The only data we should be periodically fetching (if it hasn't been fetched in the last day
308 // maybe), is community and user actors
309 // and user actors
310 // Fetch all posts in the outbox of the given user, and insert them into the database.
311 // fn fetch_community_outbox(community: &Community, conn: &PgConnection) -> Result<Vec<Post>, Error> {
312 //   let outbox_url = Url::parse(&community.get_outbox_url())?;
313 //   let outbox = fetch_remote_object::<OrderedCollection>(&outbox_url)?;
314 //   let items = outbox.collection_props.get_many_items_base_boxes();
315
316 //   Ok(
317 //     items
318 //       .unwrap()
319 //       .map(|obox: &BaseBox| -> Result<PostForm, Error> {
320 //         let page = obox.clone().to_concrete::<Page>()?;
321 //         PostForm::from_page(&page, conn)
322 //       })
323 //       .map(|pf| upsert_post(&pf?, conn))
324 //       .collect::<Result<Vec<Post>, Error>>()?,
325 //   )
326 // }