diff --git a/server/Cargo.lock b/server/Cargo.lock index cbb6248bc..fe54cfb4e 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -1455,6 +1455,15 @@ dependencies = [ "sluice", ] +[[package]] +name = "itertools" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "0.1.1" @@ -1544,6 +1553,7 @@ dependencies = [ "http", "http-signature-normalization", "isahc", + "itertools", "jsonwebtoken", "lazy_static 1.4.0", "lettre", diff --git a/server/Cargo.toml b/server/Cargo.toml index 5f4d24cec..a521a9665 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -44,3 +44,4 @@ http-signature-normalization = "0.4.1" base64 = "0.12.0" tokio = "0.2.18" futures = "0.3.4" +itertools = "0.9.0" diff --git a/server/src/api/community.rs b/server/src/api/community.rs index ace5b353e..174a91c84 100644 --- a/server/src/api/community.rs +++ b/server/src/api/community.rs @@ -488,7 +488,7 @@ impl Perform for Oper { } else { // TODO: still have to implement unfollow let user = User_::read(&conn, user_id)?; - follow_community(&community, &user, &conn)?; + user.send_follow(&community.actor_id)?; // TODO: this needs to return a "pending" state, until Accept is received from the remote server } diff --git a/server/src/api/mod.rs b/server/src/api/mod.rs index 04d690014..0595f2a40 100644 --- a/server/src/api/mod.rs +++ b/server/src/api/mod.rs @@ -23,10 +23,10 @@ use crate::{ }; use crate::apub::{ - activities::{follow_community, post_create, post_update}, + activities::{send_post_create, send_post_update}, fetcher::search_by_apub_id, signatures::generate_actor_keypair, - {make_apub_endpoint, EndpointType}, + {make_apub_endpoint, ActorType, EndpointType}, }; use crate::settings::Settings; use crate::websocket::UserOperation; diff --git a/server/src/api/post.rs b/server/src/api/post.rs index 32eb5470a..89f1dd1d3 100644 --- a/server/src/api/post.rs +++ b/server/src/api/post.rs @@ -160,7 +160,7 @@ impl Perform for Oper { Err(_e) => return Err(APIError::err("couldnt_create_post").into()), }; - post_create(&updated_post, &user, &conn)?; + send_post_create(&updated_post, &user, &conn)?; // They like their own post by default let like_form = PostLikeForm { @@ -531,7 +531,7 @@ impl Perform for Oper { ModStickyPost::create(&conn, &form)?; } - post_update(&updated_post, &user, &conn)?; + send_post_update(&updated_post, &user, &conn)?; let post_view = PostView::read(&conn, data.edit_id, Some(user_id))?; diff --git a/server/src/apub/activities.rs b/server/src/apub/activities.rs index 24631a352..cb98e9734 100644 --- a/server/src/apub/activities.rs +++ b/server/src/apub/activities.rs @@ -17,7 +17,7 @@ fn populate_object_props( } /// Send an activity to a list of recipients, using the correct headers etc. -fn send_activity( +pub fn send_activity( activity: &A, private_key: &str, sender_id: &str, @@ -52,15 +52,18 @@ where fn get_follower_inboxes(conn: &PgConnection, community: &Community) -> Result, Error> { Ok( CommunityFollowerView::for_community(conn, community.id)? - .iter() + .into_iter() .filter(|c| !c.user_local) + // TODO eventually this will have to use the inbox or shared_inbox column, meaning that view + // will have to change .map(|c| format!("{}/inbox", c.user_actor_id.to_owned())) + .unique() .collect(), ) } /// Send out information about a newly created post, to the followers of the community. -pub fn post_create(post: &Post, creator: &User_, conn: &PgConnection) -> Result<(), Error> { +pub fn send_post_create(post: &Post, creator: &User_, conn: &PgConnection) -> Result<(), Error> { let page = post.to_apub(conn)?; let community = Community::read(conn, post.community_id)?; let mut create = Create::new(); @@ -83,7 +86,7 @@ pub fn post_create(post: &Post, creator: &User_, conn: &PgConnection) -> Result< } /// Send out information about an edited post, to the followers of the community. -pub fn post_update(post: &Post, creator: &User_, conn: &PgConnection) -> Result<(), Error> { +pub fn send_post_update(post: &Post, creator: &User_, conn: &PgConnection) -> Result<(), Error> { let page = post.to_apub(conn)?; let community = Community::read(conn, post.community_id)?; let mut update = Update::new(); @@ -104,70 +107,3 @@ pub fn post_update(post: &Post, creator: &User_, conn: &PgConnection) -> Result< )?; Ok(()) } - -/// As a given local user, send out a follow request to a remote community. -pub fn follow_community( - community: &Community, - user: &User_, - _conn: &PgConnection, -) -> Result<(), Error> { - let mut follow = Follow::new(); - follow - .object_props - .set_context_xsd_any_uri(context())? - // TODO: needs proper id - .set_id(user.actor_id.clone())?; - follow - .follow_props - .set_actor_xsd_any_uri(user.actor_id.clone())? - .set_object_xsd_any_uri(community.actor_id.clone())?; - // TODO this is incorrect, the to field should not be the inbox, but the followers url - let to = format!("{}/inbox", community.actor_id); - send_activity( - &follow, - &user.private_key.as_ref().unwrap(), - &community.actor_id, - vec![to], - )?; - Ok(()) -} - -/// As a local community, accept the follow request from a remote user. -pub fn accept_follow(follow: &Follow, conn: &PgConnection) -> Result<(), Error> { - let community_uri = follow - .follow_props - .get_object_xsd_any_uri() - .unwrap() - .to_string(); - let actor_uri = follow - .follow_props - .get_actor_xsd_any_uri() - .unwrap() - .to_string(); - let community = Community::read_from_actor_id(conn, &community_uri)?; - let mut accept = Accept::new(); - accept - .object_props - .set_context_xsd_any_uri(context())? - // TODO: needs proper id - .set_id( - follow - .follow_props - .get_actor_xsd_any_uri() - .unwrap() - .to_string(), - )?; - accept - .accept_props - .set_actor_xsd_any_uri(community.actor_id.clone())? - .set_object_base_box(BaseBox::from_concrete(follow.clone())?)?; - // TODO this is incorrect, the to field should not be the inbox, but the followers url - let to = format!("{}/inbox", actor_uri); - send_activity( - &accept, - &community.private_key.unwrap(), - &community.actor_id, - vec![to], - )?; - Ok(()) -} diff --git a/server/src/apub/community.rs b/server/src/apub/community.rs index e74a5fd14..bc984b250 100644 --- a/server/src/apub/community.rs +++ b/server/src/apub/community.rs @@ -30,12 +30,17 @@ impl ToApub for Community { oprops.set_summary_xsd_string(d)?; } + let mut endpoint_props = EndpointProperties::default(); + + endpoint_props.set_shared_inbox(self.get_shared_inbox_url())?; + let mut actor_props = ApActorProperties::default(); actor_props .set_preferred_username(self.title.to_owned())? .set_inbox(self.get_inbox_url())? .set_outbox(self.get_outbox_url())? + .set_endpoints(endpoint_props)? .set_followers(self.get_followers_url())?; Ok(group.extend(actor_props).extend(self.get_public_key_ext())) @@ -50,6 +55,40 @@ impl ActorType for Community { fn public_key(&self) -> String { self.public_key.to_owned().unwrap() } + + /// As a local community, accept the follow request from a remote user. + fn send_accept_follow(&self, follow: &Follow) -> Result<(), Error> { + let actor_uri = follow + .follow_props + .get_actor_xsd_any_uri() + .unwrap() + .to_string(); + + let mut accept = Accept::new(); + accept + .object_props + .set_context_xsd_any_uri(context())? + // TODO: needs proper id + .set_id( + follow + .follow_props + .get_actor_xsd_any_uri() + .unwrap() + .to_string(), + )?; + accept + .accept_props + .set_actor_xsd_any_uri(self.actor_id.to_owned())? + .set_object_base_box(BaseBox::from_concrete(follow.clone())?)?; + let to = format!("{}/inbox", actor_uri); + send_activity( + &accept, + &self.private_key.to_owned().unwrap(), + &self.actor_id, + vec![to], + )?; + Ok(()) + } } impl FromApub for CommunityForm { diff --git a/server/src/apub/community_inbox.rs b/server/src/apub/community_inbox.rs index 6931cdf1a..04a64b69a 100644 --- a/server/src/apub/community_inbox.rs +++ b/server/src/apub/community_inbox.rs @@ -63,6 +63,7 @@ fn handle_follow( // This will fail if they're already a follower, but ignore the error. CommunityFollower::follow(&conn, &community_follower_form).ok(); - accept_follow(&follow, &conn)?; + community.send_accept_follow(&follow)?; + Ok(HttpResponse::Ok().finish()) } diff --git a/server/src/apub/mod.rs b/server/src/apub/mod.rs index 671f8c5ac..5c5852991 100644 --- a/server/src/apub/mod.rs +++ b/server/src/apub/mod.rs @@ -3,6 +3,7 @@ pub mod community; pub mod community_inbox; pub mod fetcher; pub mod post; +pub mod shared_inbox; pub mod signatures; pub mod user; pub mod user_inbox; @@ -12,6 +13,7 @@ use activitystreams::{ actor::{properties::ApActorProperties, Actor, Group, Person}, collection::UnorderedCollection, context, + endpoint::EndpointProperties, ext::{Ext, Extensible, Extension}, object::{properties::ObjectProperties, Page}, public, BaseBox, @@ -26,6 +28,7 @@ use failure::_core::fmt::Debug; use http::request::Builder; use http_signature_normalization::Config; use isahc::prelude::*; +use itertools::Itertools; use log::debug; use openssl::hash::MessageDigest; use openssl::sign::{Signer, Verifier}; @@ -47,7 +50,7 @@ use crate::routes::nodeinfo::{NodeInfo, NodeInfoWellKnown}; use crate::routes::{ChatServerParam, DbPoolParam}; use crate::{convert_datetime, naive_now, Settings}; -use activities::accept_follow; +use activities::send_activity; use fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user}; use signatures::verify; use signatures::{sign, PublicKey, PublicKeyExtension}; @@ -144,9 +147,38 @@ pub trait ActorType { fn public_key(&self) -> String; + // These two have default impls, since currently a community can't follow anything, + // and a user can't be followed (yet) + #[allow(unused_variables)] + fn send_follow(&self, follow_actor_id: &str) -> Result<(), Error> { + Ok(()) + } + + #[allow(unused_variables)] + fn send_accept_follow(&self, follow: &Follow) -> Result<(), Error> { + Ok(()) + } + + // TODO move these to the db rows fn get_inbox_url(&self) -> String { format!("{}/inbox", &self.actor_id()) } + + fn get_shared_inbox_url(&self) -> String { + let url = Url::parse(&self.actor_id()).unwrap(); + let url_str = format!( + "{}://{}{}/inbox", + &url.scheme(), + &url.host_str().unwrap(), + if let Some(port) = url.port() { + format!(":{}", port) + } else { + "".to_string() + }, + ); + format!("{}/inbox", &url_str) + } + fn get_outbox_url(&self) -> String { format!("{}/outbox", &self.actor_id()) } @@ -154,9 +186,11 @@ pub trait ActorType { fn get_followers_url(&self) -> String { format!("{}/followers", &self.actor_id()) } + fn get_following_url(&self) -> String { format!("{}/following", &self.actor_id()) } + fn get_liked_url(&self) -> String { format!("{}/liked", &self.actor_id()) } diff --git a/server/src/apub/shared_inbox.rs b/server/src/apub/shared_inbox.rs new file mode 100644 index 000000000..35ba3908a --- /dev/null +++ b/server/src/apub/shared_inbox.rs @@ -0,0 +1 @@ +// use super::*; diff --git a/server/src/apub/user.rs b/server/src/apub/user.rs index 88238b5d4..b4b3b35b6 100644 --- a/server/src/apub/user.rs +++ b/server/src/apub/user.rs @@ -27,11 +27,16 @@ impl ToApub for User_ { oprops.set_name_xsd_string(i.to_owned())?; } + let mut endpoint_props = EndpointProperties::default(); + + endpoint_props.set_shared_inbox(self.get_shared_inbox_url())?; + let mut actor_props = ApActorProperties::default(); actor_props .set_inbox(self.get_inbox_url())? .set_outbox(self.get_outbox_url())? + .set_endpoints(endpoint_props)? .set_followers(self.get_followers_url())? .set_following(self.get_following_url())? .set_liked(self.get_liked_url())?; @@ -48,6 +53,29 @@ impl ActorType for User_ { fn public_key(&self) -> String { self.public_key.to_owned().unwrap() } + + // TODO might be able to move this to a default trait fn + /// As a given local user, send out a follow request to a remote community. + fn send_follow(&self, follow_actor_id: &str) -> Result<(), Error> { + let mut follow = Follow::new(); + follow + .object_props + .set_context_xsd_any_uri(context())? + // TODO: needs proper id + .set_id(self.actor_id.to_owned())?; + follow + .follow_props + .set_actor_xsd_any_uri(self.actor_id.to_owned())? + .set_object_xsd_any_uri(follow_actor_id)?; + let to = format!("{}/inbox", follow_actor_id); + send_activity( + &follow, + &self.private_key.as_ref().unwrap(), + &follow_actor_id, + vec![to], + )?; + Ok(()) + } } impl FromApub for UserForm {