]> Untitled Git - lemmy.git/blob - server/src/apub/puller.rs
Federation DB Changes.
[lemmy.git] / server / src / apub / puller.rs
1 use crate::api::community::{GetCommunityResponse, ListCommunitiesResponse};
2 use crate::api::post::GetPostsResponse;
3 use crate::apub::get_apub_protocol_string;
4 use crate::db::community_view::CommunityView;
5 use crate::db::post_view::PostView;
6 use crate::naive_now;
7 use crate::routes::nodeinfo::{NodeInfo, NodeInfoWellKnown};
8 use crate::settings::Settings;
9 use activitystreams::actor::{properties::ApActorProperties, Group};
10 use activitystreams::collection::{OrderedCollection, UnorderedCollection};
11 use activitystreams::ext::Ext;
12 use activitystreams::object::Page;
13 use activitystreams::BaseBox;
14 use failure::Error;
15 use isahc::prelude::*;
16 use log::warn;
17 use serde::Deserialize;
18
19 fn fetch_node_info(domain: &str) -> Result<NodeInfo, Error> {
20   let well_known_uri = format!(
21     "{}://{}/.well-known/nodeinfo",
22     get_apub_protocol_string(),
23     domain
24   );
25   let well_known = fetch_remote_object::<NodeInfoWellKnown>(&well_known_uri)?;
26   Ok(fetch_remote_object::<NodeInfo>(&well_known.links.href)?)
27 }
28
29 fn fetch_communities_from_instance(domain: &str) -> Result<Vec<CommunityView>, Error> {
30   let node_info = fetch_node_info(domain)?;
31   if node_info.software.name != "lemmy" {
32     return Err(format_err!(
33       "{} is not a Lemmy instance, federation is not supported",
34       domain
35     ));
36   }
37
38   // TODO: follow pagination (seems like page count is missing?)
39   // TODO: see if there is any standard for discovering remote actors, so we dont have to rely on lemmy apis
40   let communities_uri = format!(
41     "http://{}/api/v1/communities/list?sort=Hot&local_only=true",
42     domain
43   );
44   let communities1 = fetch_remote_object::<ListCommunitiesResponse>(&communities_uri)?;
45   let mut communities2 = communities1.communities;
46   for c in &mut communities2 {
47     c.name = format_community_name(&c.name, domain);
48   }
49   Ok(communities2)
50 }
51
52 fn get_remote_community_uri(identifier: &str) -> String {
53   let x: Vec<&str> = identifier.split('@').collect();
54   let name = x[0].replace("!", "");
55   let instance = x[1];
56   format!("http://{}/federation/c/{}", instance, name)
57 }
58
59 fn fetch_remote_object<Response>(uri: &str) -> Result<Response, Error>
60 where
61   Response: for<'de> Deserialize<'de>,
62 {
63   if Settings::get().federation.tls_enabled && !uri.starts_with("https") {
64     return Err(format_err!("Activitypub uri is insecure: {}", uri));
65   }
66   // TODO: should cache responses here when we are in production
67   // TODO: this function should return a future
68   let text = isahc::get(uri)?.text()?;
69   let res: Response = serde_json::from_str(&text)?;
70   Ok(res)
71 }
72
73 pub fn get_remote_community_posts(identifier: &str) -> Result<GetPostsResponse, Error> {
74   let community =
75     fetch_remote_object::<Ext<Group, ApActorProperties>>(&get_remote_community_uri(identifier))?;
76   let outbox_uri = &community.extension.get_outbox().to_string();
77   let outbox = fetch_remote_object::<OrderedCollection>(outbox_uri)?;
78   let items = outbox.collection_props.get_many_items_base_boxes();
79
80   let posts: Vec<PostView> = items
81     .unwrap()
82     .map(|obox: &BaseBox| {
83       let page: Page = obox.clone().to_concrete::<Page>().unwrap();
84       PostView {
85         id: -1,
86         name: page.object_props.get_name_xsd_string().unwrap().to_string(),
87         url: page
88           .object_props
89           .get_url_xsd_any_uri()
90           .map(|u| u.to_string()),
91         body: page
92           .object_props
93           .get_content_xsd_string()
94           .map(|c| c.to_string()),
95         creator_id: -1,
96         community_id: -1,
97         removed: false,
98         locked: false,
99         published: page
100           .object_props
101           .get_published()
102           .unwrap()
103           .as_ref()
104           .naive_local()
105           .to_owned(),
106         updated: page
107           .object_props
108           .get_updated()
109           .map(|u| u.as_ref().to_owned().naive_local()),
110         deleted: false,
111         nsfw: false,
112         stickied: false,
113         embed_title: None,
114         embed_description: None,
115         embed_html: None,
116         thumbnail_url: None,
117         banned: false,
118         banned_from_community: false,
119         creator_name: "".to_string(),
120         creator_avatar: None,
121         community_name: "".to_string(),
122         community_removed: false,
123         community_deleted: false,
124         community_nsfw: false,
125         number_of_comments: -1,
126         score: -1,
127         upvotes: -1,
128         downvotes: -1,
129         hot_rank: -1,
130         newest_activity_time: naive_now(),
131         user_id: None,
132         my_vote: None,
133         subscribed: None,
134         read: None,
135         saved: None,
136       }
137     })
138     .collect();
139   Ok(GetPostsResponse { posts })
140 }
141
142 pub fn get_remote_community(identifier: &str) -> Result<GetCommunityResponse, failure::Error> {
143   let community =
144     fetch_remote_object::<Ext<Group, ApActorProperties>>(&get_remote_community_uri(identifier))?;
145   let followers_uri = &community.extension.get_followers().unwrap().to_string();
146   let outbox_uri = &community.extension.get_outbox().to_string();
147   let outbox = fetch_remote_object::<OrderedCollection>(outbox_uri)?;
148   let followers = fetch_remote_object::<UnorderedCollection>(followers_uri)?;
149   // TODO: this is only for testing until we can call that function from GetPosts
150   // (once string ids are supported)
151   //dbg!(get_remote_community_posts(identifier)?);
152
153   Ok(GetCommunityResponse {
154     moderators: vec![],
155     admins: vec![],
156     community: CommunityView {
157       // TODO: we need to merge id and name into a single thing (stuff like @user@instance.com)
158       id: 1337, //community.object_props.get_id()
159       name: identifier.to_string(),
160       title: community
161         .as_ref()
162         .get_name_xsd_string()
163         .unwrap()
164         .to_string(),
165       description: community
166         .as_ref()
167         .get_summary_xsd_string()
168         .map(|s| s.to_string()),
169       category_id: -1,
170       creator_id: -1, //community.object_props.get_attributed_to_xsd_any_uri()
171       removed: false,
172       published: community
173         .as_ref()
174         .get_published()
175         .unwrap()
176         .as_ref()
177         .naive_local()
178         .to_owned(),
179       updated: community
180         .as_ref()
181         .get_updated()
182         .map(|u| u.as_ref().to_owned().naive_local()),
183       deleted: false,
184       nsfw: false,
185       creator_name: "".to_string(),
186       creator_avatar: None,
187       category_name: "".to_string(),
188       number_of_subscribers: *followers
189         .collection_props
190         .get_total_items()
191         .unwrap()
192         .as_ref() as i64, // TODO: need to use the same type
193       number_of_posts: *outbox.collection_props.get_total_items().unwrap().as_ref() as i64,
194       number_of_comments: -1,
195       hot_rank: -1,
196       user_id: None,
197       subscribed: None,
198     },
199     online: 0,
200   })
201 }
202
203 pub fn get_following_instances() -> Vec<&'static str> {
204   Settings::get()
205     .federation
206     .followed_instances
207     .split(',')
208     .collect()
209 }
210
211 pub fn get_all_communities() -> Result<Vec<CommunityView>, Error> {
212   let mut communities_list: Vec<CommunityView> = vec![];
213   for instance in &get_following_instances() {
214     match fetch_communities_from_instance(instance) {
215       Ok(mut c) => communities_list.append(c.as_mut()),
216       Err(e) => warn!("Failed to fetch instance list from remote instance: {}", e),
217     };
218   }
219   Ok(communities_list)
220 }
221
222 /// If community is on local instance, don't include the @instance part. This is only for displaying
223 /// to the user and should never be used otherwise.
224 pub fn format_community_name(name: &str, instance: &str) -> String {
225   if instance == Settings::get().hostname {
226     format!("!{}", name)
227   } else {
228     format!("!{}@{}", name, instance)
229   }
230 }