]> Untitled Git - lemmy.git/blob - server/src/apub/puller.rs
Merge branch 'federation_add_fed_columns' of https://yerbamate.dev/dessalines/lemmy...
[lemmy.git] / server / src / apub / puller.rs
1 use crate::api::community::GetCommunityResponse;
2 use crate::api::post::GetPostsResponse;
3 use crate::apub::{get_apub_protocol_string, GroupExt};
4 use crate::db::community_view::CommunityView;
5 use crate::db::post_view::PostView;
6 use crate::routes::nodeinfo::{NodeInfo, NodeInfoWellKnown};
7 use crate::settings::Settings;
8 use activitystreams::collection::{OrderedCollection, UnorderedCollection};
9 use activitystreams::object::Page;
10 use activitystreams::BaseBox;
11 use failure::Error;
12 use isahc::prelude::*;
13 use log::warn;
14 use serde::Deserialize;
15
16 fn fetch_node_info(domain: &str) -> Result<NodeInfo, Error> {
17   let well_known_uri = format!(
18     "{}://{}/.well-known/nodeinfo",
19     get_apub_protocol_string(),
20     domain
21   );
22   let well_known = fetch_remote_object::<NodeInfoWellKnown>(&well_known_uri)?;
23   Ok(fetch_remote_object::<NodeInfo>(&well_known.links.href)?)
24 }
25
26 fn fetch_communities_from_instance(domain: &str) -> Result<Vec<CommunityView>, Error> {
27   let node_info = fetch_node_info(domain)?;
28
29   if let Some(community_list_url) = node_info.metadata.community_list_url {
30     let collection = fetch_remote_object::<UnorderedCollection>(&community_list_url)?;
31     let object_boxes = collection
32       .collection_props
33       .get_many_items_base_boxes()
34       .unwrap();
35     let communities: Result<Vec<CommunityView>, Error> = object_boxes
36       .map(|c| -> Result<CommunityView, Error> {
37         let group = c.to_owned().to_concrete::<GroupExt>()?;
38         CommunityView::from_group(&group, domain)
39       })
40       .collect();
41     Ok(communities?)
42   } else {
43     Err(format_err!(
44       "{} is not a Lemmy instance, federation is not supported",
45       domain
46     ))
47   }
48 }
49
50 /// Returns a tuple of (username, domain) from an identifier like "main@dev.lemmy.ml"
51 fn split_identifier(identifier: &str) -> (String, String) {
52   let x: Vec<&str> = identifier.split('@').collect();
53   (x[0].replace("!", ""), x[1].to_string())
54 }
55
56 fn get_remote_community_uri(identifier: &str) -> String {
57   let (name, domain) = split_identifier(identifier);
58   format!("http://{}/federation/c/{}", domain, name)
59 }
60
61 pub fn fetch_remote_object<Response>(uri: &str) -> Result<Response, Error>
62 where
63   Response: for<'de> Deserialize<'de>,
64 {
65   if Settings::get().federation.tls_enabled && !uri.starts_with("https://") {
66     return Err(format_err!("Activitypub uri is insecure: {}", uri));
67   }
68   // TODO: should cache responses here when we are in production
69   // TODO: this function should return a future
70   let text = isahc::get(uri)?.text()?;
71   let res: Response = serde_json::from_str(&text)?;
72   Ok(res)
73 }
74
75 pub fn get_remote_community_posts(identifier: &str) -> Result<GetPostsResponse, Error> {
76   let community = fetch_remote_object::<GroupExt>(&get_remote_community_uri(identifier))?;
77   let outbox_uri = &community.extension.get_outbox().to_string();
78   let outbox = fetch_remote_object::<OrderedCollection>(outbox_uri)?;
79   let items = outbox.collection_props.get_many_items_base_boxes();
80
81   let posts: Result<Vec<PostView>, Error> = items
82     .unwrap()
83     .map(|obox: &BaseBox| {
84       let page = obox.clone().to_concrete::<Page>().unwrap();
85       PostView::from_page(&page)
86     })
87     .collect();
88   Ok(GetPostsResponse { posts: posts? })
89 }
90
91 pub fn get_remote_community(identifier: &str) -> Result<GetCommunityResponse, failure::Error> {
92   let group = fetch_remote_object::<GroupExt>(&get_remote_community_uri(identifier))?;
93   // TODO: this is only for testing until we can call that function from GetPosts
94   // (once string ids are supported)
95   //dbg!(get_remote_community_posts(identifier)?);
96
97   let (_, domain) = split_identifier(identifier);
98   Ok(GetCommunityResponse {
99     moderators: vec![],
100     admins: vec![],
101     community: CommunityView::from_group(&group, &domain)?,
102     online: 0,
103   })
104 }
105
106 pub fn get_following_instances() -> Vec<&'static str> {
107   Settings::get()
108     .federation
109     .followed_instances
110     .split(',')
111     .collect()
112 }
113
114 pub fn get_all_communities() -> Result<Vec<CommunityView>, Error> {
115   let mut communities_list: Vec<CommunityView> = vec![];
116   for instance in &get_following_instances() {
117     match fetch_communities_from_instance(instance) {
118       Ok(mut c) => communities_list.append(c.as_mut()),
119       Err(e) => warn!("Failed to fetch instance list from remote instance: {}", e),
120     };
121   }
122   Ok(communities_list)
123 }
124
125 /// If community is on local instance, don't include the @instance part. This is only for displaying
126 /// to the user and should never be used otherwise.
127 pub fn format_community_name(name: &str, instance: &str) -> String {
128   if instance == Settings::get().hostname {
129     format!("!{}", name)
130   } else {
131     format!("!{}@{}", name, instance)
132   }
133 }