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;
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;
15 use isahc::prelude::*;
17 use serde::Deserialize;
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(),
25 let well_known = fetch_remote_object::<NodeInfoWellKnown>(&well_known_uri)?;
26 Ok(fetch_remote_object::<NodeInfo>(&well_known.links.href)?)
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",
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",
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);
52 fn get_remote_community_uri(identifier: &str) -> String {
53 let x: Vec<&str> = identifier.split('@').collect();
54 let name = x[0].replace("!", "");
56 format!("http://{}/federation/c/{}", instance, name)
59 fn fetch_remote_object<Response>(uri: &str) -> Result<Response, Error>
61 Response: for<'de> Deserialize<'de>,
63 if Settings::get().federation.tls_enabled && !uri.starts_with("https") {
64 return Err(format_err!("Activitypub uri is insecure: {}", uri));
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)?;
73 pub fn get_remote_community_posts(identifier: &str) -> Result<GetPostsResponse, Error> {
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();
80 let posts: Vec<PostView> = items
82 .map(|obox: &BaseBox| {
83 let page: Page = obox.clone().to_concrete::<Page>().unwrap();
86 name: page.object_props.get_name_xsd_string().unwrap().to_string(),
89 .get_url_xsd_any_uri()
90 .map(|u| u.to_string()),
93 .get_content_xsd_string()
94 .map(|c| c.to_string()),
109 .map(|u| u.as_ref().to_owned().naive_local()),
114 embed_description: None,
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,
130 newest_activity_time: naive_now(),
139 Ok(GetPostsResponse { posts })
142 pub fn get_remote_community(identifier: &str) -> Result<GetCommunityResponse, failure::Error> {
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)?);
153 Ok(GetCommunityResponse {
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(),
162 .get_name_xsd_string()
165 description: community
167 .get_summary_xsd_string()
168 .map(|s| s.to_string()),
170 creator_id: -1, //community.object_props.get_attributed_to_xsd_any_uri()
182 .map(|u| u.as_ref().to_owned().naive_local()),
185 creator_name: "".to_string(),
186 creator_avatar: None,
187 category_name: "".to_string(),
188 number_of_subscribers: *followers
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,
203 pub fn get_following_instances() -> Vec<&'static str> {
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),
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 {
228 format!("!{}@{}", name, instance)