From 19c84613977724c4008aba3d37141c86901f9ca8 Mon Sep 17 00:00:00 2001 From: Felix Date: Tue, 14 Apr 2020 17:37:23 +0200 Subject: [PATCH] Implemented follow/accept --- server/src/api/community.rs | 35 ++++++---- server/src/apub/activities.rs | 83 ++++++++++++++++++++---- server/src/apub/community.rs | 8 +-- server/src/apub/inbox.rs | 111 ++++++++++++++++++-------------- server/src/apub/post.rs | 7 +- server/src/main.rs | 2 - server/src/routes/federation.rs | 9 ++- 7 files changed, 169 insertions(+), 86 deletions(-) diff --git a/server/src/api/community.rs b/server/src/api/community.rs index 3edecb4f..35ca1d26 100644 --- a/server/src/api/community.rs +++ b/server/src/api/community.rs @@ -1,4 +1,5 @@ use super::*; +use crate::apub::activities::follow_community; use crate::apub::{format_community_name, gen_keypair_str, make_apub_endpoint, EndpointType}; use diesel::PgConnection; use std::str::FromStr; @@ -401,21 +402,29 @@ impl Perform for Oper { let user_id = claims.id; - let community_follower_form = CommunityFollowerForm { - community_id: data.community_id, - user_id, - }; - - if data.follow { - match CommunityFollower::follow(&conn, &community_follower_form) { - Ok(user) => user, - Err(_e) => return Err(APIError::err("community_follower_already_exists").into()), + let community = Community::read(conn, data.community_id)?; + if community.local { + let community_follower_form = CommunityFollowerForm { + community_id: data.community_id, + user_id, }; + + if data.follow { + match CommunityFollower::follow(&conn, &community_follower_form) { + Ok(user) => user, + Err(_e) => return Err(APIError::err("community_follower_already_exists").into()), + }; + } else { + match CommunityFollower::ignore(&conn, &community_follower_form) { + Ok(user) => user, + Err(_e) => return Err(APIError::err("community_follower_already_exists").into()), + }; + } } else { - match CommunityFollower::ignore(&conn, &community_follower_form) { - Ok(user) => user, - Err(_e) => return Err(APIError::err("community_follower_already_exists").into()), - }; + // TODO: still have to implement unfollow + let user = User_::read(conn, user_id)?; + follow_community(&community, &user, conn)?; + // TODO: this needs to return a "pending" state, until Accept is received from the remote server } let community_view = CommunityView::read(&conn, data.community_id, Some(user_id))?; diff --git a/server/src/apub/activities.rs b/server/src/apub/activities.rs index 0c1a1901..a1707267 100644 --- a/server/src/apub/activities.rs +++ b/server/src/apub/activities.rs @@ -3,8 +3,9 @@ use crate::db::community::Community; use crate::db::post::Post; use crate::db::user::User_; use crate::db::Crud; -use activitystreams::activity::{Create, Update}; +use activitystreams::activity::{Accept, Create, Follow, Update}; use activitystreams::object::properties::ObjectProperties; +use activitystreams::BaseBox; use activitystreams::{context, public}; use diesel::PgConnection; use failure::Error; @@ -28,19 +29,15 @@ fn populate_object_props( Ok(()) } -fn send_activity(activity: &A) -> Result<(), Error> +fn send_activity(activity: &A, to: Vec) -> Result<(), Error> where A: Serialize + Debug, { let json = serde_json::to_string(&activity)?; - for i in get_following_instances() { - // TODO: need to send this to the inbox of following users - let inbox = format!( - "{}://{}/federation/inbox", - get_apub_protocol_string(), - i.domain - ); - let res = Request::post(inbox) + println!("sending data {}", json); + for t in to { + println!("to: {}", t); + let res = Request::post(t) .header("Content-Type", "application/json") .body(json.to_owned())? .send()?; @@ -49,6 +46,20 @@ where Ok(()) } +fn get_followers(_community: &Community) -> Vec { + // TODO: this is wrong, needs to go to the (non-local) followers of the community + get_following_instances() + .iter() + .map(|i| { + format!( + "{}://{}/federation/inbox", + get_apub_protocol_string(), + i.domain + ) + }) + .collect() +} + pub fn post_create(post: &Post, creator: &User_, conn: &PgConnection) -> Result<(), Error> { let page = post.as_page(conn)?; let community = Community::read(conn, post.community_id)?; @@ -62,7 +73,7 @@ pub fn post_create(post: &Post, creator: &User_, conn: &PgConnection) -> Result< .create_props .set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_object_base_box(page)?; - send_activity(&create)?; + send_activity(&create, get_followers(&community))?; Ok(()) } @@ -79,6 +90,54 @@ pub fn post_update(post: &Post, creator: &User_, conn: &PgConnection) -> Result< .update_props .set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_object_base_box(page)?; - send_activity(&update)?; + send_activity(&update, get_followers(&community))?; + Ok(()) +} + +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())?; + let to = format!("{}/inbox", community.actor_id); + send_activity(&follow, vec![to])?; + Ok(()) +} + +pub fn accept_follow(follow: &Follow) -> Result<(), Error> { + 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_object_base_box(BaseBox::from_concrete(follow.clone())?)?; + let to = format!( + "{}/inbox", + follow + .follow_props + .get_actor_xsd_any_uri() + .unwrap() + .to_string() + ); + send_activity(&accept, vec![to])?; Ok(()) } diff --git a/server/src/apub/community.rs b/server/src/apub/community.rs index a56d81d0..0bea4705 100644 --- a/server/src/apub/community.rs +++ b/server/src/apub/community.rs @@ -64,7 +64,7 @@ impl Community { .set_id(self.actor_id.to_owned())? .set_name_xsd_string(self.name.to_owned())? .set_published(convert_datetime(self.published))? - .set_attributed_to_xsd_any_uri(make_apub_endpoint(EndpointType::User, &creator.name))?; + .set_attributed_to_xsd_any_uri(creator.actor_id)?; if let Some(u) = self.updated.to_owned() { oprops.set_updated(convert_datetime(u))?; @@ -156,7 +156,6 @@ pub async fn get_apub_community_followers( db: web::Data>>, ) -> Result, Error> { let community = Community::read_from_name(&&db.get()?, info.community_name.to_owned())?; - let base_url = make_apub_endpoint(EndpointType::Community, &community.name); let connection = establish_unpooled_connection(); //As we are an object, we validated that the community id was valid @@ -167,7 +166,7 @@ pub async fn get_apub_community_followers( let oprops: &mut ObjectProperties = collection.as_mut(); oprops .set_context_xsd_any_uri(context())? - .set_id(base_url)?; + .set_id(community.actor_id)?; collection .collection_props .set_total_items(community_followers.len() as u64)?; @@ -179,7 +178,6 @@ pub async fn get_apub_community_outbox( db: web::Data>>, ) -> Result, Error> { let community = Community::read_from_name(&&db.get()?, info.community_name.to_owned())?; - let base_url = make_apub_endpoint(EndpointType::Community, &community.name); let conn = establish_unpooled_connection(); //As we are an object, we validated that the community id was valid @@ -189,7 +187,7 @@ pub async fn get_apub_community_outbox( let oprops: &mut ObjectProperties = collection.as_mut(); oprops .set_context_xsd_any_uri(context())? - .set_id(base_url)?; + .set_id(community.actor_id)?; collection .collection_props .set_many_items_base_boxes( diff --git a/server/src/apub/inbox.rs b/server/src/apub/inbox.rs index cc844224..a2db335a 100644 --- a/server/src/apub/inbox.rs +++ b/server/src/apub/inbox.rs @@ -1,16 +1,16 @@ +use crate::apub::activities::accept_follow; +use crate::apub::fetcher::fetch_remote_user; +use crate::db::community::{Community, CommunityFollower, CommunityFollowerForm}; use crate::db::post::{Post, PostForm}; use crate::db::Crud; +use crate::db::Followable; +use activitystreams::activity::{Accept, Create, Follow, Update}; use activitystreams::object::Page; -use activitystreams::{ - object::{Object, ObjectBox}, - primitives::XsdAnyUri, - Base, BaseBox, PropRefs, -}; use actix_web::{web, HttpResponse}; use diesel::r2d2::{ConnectionManager, Pool}; use diesel::PgConnection; use failure::Error; -use std::collections::HashMap; +use url::Url; // TODO: need a proper actor that has this inbox @@ -18,24 +18,40 @@ pub async fn inbox( input: web::Json, db: web::Data>>, ) -> Result { + // TODO: make sure that things are received in the correct inbox + // (by using seperate handler functions and checking the user/community name in the path) let input = input.into_inner(); let conn = &db.get().unwrap(); - match input.kind { - ValidTypes::Create => handle_create(&input, conn), - ValidTypes::Update => handle_update(&input, conn), + match input { + AcceptedObjects::Create(c) => handle_create(&c, conn), + AcceptedObjects::Update(u) => handle_update(&u, conn), + AcceptedObjects::Follow(f) => handle_follow(&f, conn), + AcceptedObjects::Accept(a) => handle_accept(&a, conn), } } -fn handle_create(create: &AcceptedObjects, conn: &PgConnection) -> Result { - let page = create.object.to_owned().to_concrete::()?; +fn handle_create(create: &Create, conn: &PgConnection) -> Result { + let page = create + .create_props + .get_object_base_box() + .to_owned() + .unwrap() + .to_owned() + .to_concrete::()?; let post = PostForm::from_page(&page, conn)?; Post::create(conn, &post)?; // TODO: send the new post out via websocket Ok(HttpResponse::Ok().finish()) } -fn handle_update(update: &AcceptedObjects, conn: &PgConnection) -> Result { - let page = update.object.to_owned().to_concrete::()?; +fn handle_update(update: &Update, conn: &PgConnection) -> Result { + let page = update + .update_props + .get_object_base_box() + .to_owned() + .unwrap() + .to_owned() + .to_concrete::()?; let post = PostForm::from_page(&page, conn)?; let id = Post::read_from_apub_id(conn, &post.ap_id)?.id; Post::update(conn, id, &post)?; @@ -43,46 +59,43 @@ fn handle_update(update: &AcceptedObjects, conn: &PgConnection) -> Result Result { + println!("received follow: {:?}", &follow); - #[serde(rename = "type")] - pub kind: ValidTypes, - - pub actor: XsdAnyUri, - - pub object: BaseBox, - - #[serde(flatten)] - ext: HashMap, + // TODO: make sure this is a local community + let community_uri = follow + .follow_props + .get_object_xsd_any_uri() + .unwrap() + .to_string(); + let community = Community::read_from_actor_id(conn, &community_uri)?; + let user_uri = follow + .follow_props + .get_actor_xsd_any_uri() + .unwrap() + .to_string(); + let user = fetch_remote_user(&Url::parse(&user_uri)?, conn)?; + // TODO: insert ID of the user into follows of the community + let community_follower_form = CommunityFollowerForm { + community_id: community.id, + user_id: user.id, + }; + CommunityFollower::follow(&conn, &community_follower_form)?; + accept_follow(&follow)?; + Ok(HttpResponse::Ok().finish()) } -#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "PascalCase")] -pub enum ValidTypes { - Create, - Update, +fn handle_accept(accept: &Accept, _conn: &PgConnection) -> Result { + println!("received accept: {:?}", &accept); + // TODO: at this point, indicate to the user that they are following the community + Ok(HttpResponse::Ok().finish()) } -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] #[serde(untagged)] -#[serde(rename_all = "camelCase")] -pub enum ValidObjects { - Id(XsdAnyUri), - Object(AnyExistingObject), -} - -#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize, PropRefs)] -#[serde(rename_all = "camelCase")] -#[prop_refs(Object)] -pub struct AnyExistingObject { - pub id: XsdAnyUri, - - #[serde(rename = "type")] - pub kind: String, - - #[serde(flatten)] - ext: HashMap, +#[derive(serde::Deserialize)] +pub enum AcceptedObjects { + Create(Create), + Update(Update), + Follow(Follow), + Accept(Accept), } diff --git a/server/src/apub/post.rs b/server/src/apub/post.rs index e8f53904..b574d09c 100644 --- a/server/src/apub/post.rs +++ b/server/src/apub/post.rs @@ -1,5 +1,5 @@ +use crate::apub::create_apub_response; use crate::apub::fetcher::{fetch_remote_community, fetch_remote_user}; -use crate::apub::{create_apub_response, make_apub_endpoint, EndpointType}; use crate::convert_datetime; use crate::db::community::Community; use crate::db::post::{Post, PostForm}; @@ -31,7 +31,6 @@ pub async fn get_apub_post( impl Post { pub fn as_page(&self, conn: &PgConnection) -> Result { - let base_url = make_apub_endpoint(EndpointType::Post, &self.id.to_string()); let mut page = Page::default(); let oprops: &mut ObjectProperties = page.as_mut(); let creator = User_::read(conn, self.creator_id)?; @@ -40,13 +39,13 @@ impl Post { oprops // Not needed when the Post is embedded in a collection (like for community outbox) .set_context_xsd_any_uri(context())? - .set_id(base_url)? + .set_id(self.ap_id.to_owned())? // Use summary field to be consistent with mastodon content warning. // https://mastodon.xyz/@Louisa/103987265222901387.json .set_summary_xsd_string(self.name.to_owned())? .set_published(convert_datetime(self.published))? .set_to_xsd_any_uri(community.actor_id)? - .set_attributed_to_xsd_any_uri(make_apub_endpoint(EndpointType::User, &creator.name))?; + .set_attributed_to_xsd_any_uri(creator.actor_id)?; if let Some(body) = &self.body { oprops.set_content_xsd_string(body.to_owned())?; diff --git a/server/src/main.rs b/server/src/main.rs index 9f78d43e..59dc2cb7 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -39,8 +39,6 @@ async fn main() -> Result<(), Error> { // Set up websocket server let server = ChatServer::startup(pool.clone()).start(); - // TODO: its probably failing because the other instance is not up yet - // need to make a new thread and wait a bit before fetching thread::spawn(move || { // some work here sleep(Duration::from_secs(5)); diff --git a/server/src/routes/federation.rs b/server/src/routes/federation.rs index 100e548f..2798e7a9 100644 --- a/server/src/routes/federation.rs +++ b/server/src/routes/federation.rs @@ -12,7 +12,14 @@ pub fn config(cfg: &mut web::ServiceConfig) { ) // TODO: this needs to be moved to the actors (eg /federation/u/{}/inbox) .route("/federation/inbox", web::post().to(apub::inbox::inbox)) - .route("/federation/inbox", web::post().to(apub::inbox::inbox)) + .route( + "/federation/c/{_}/inbox", + web::post().to(apub::inbox::inbox), + ) + .route( + "/federation/u/{_}/inbox", + web::post().to(apub::inbox::inbox), + ) .route( "/federation/c/{community_name}", web::get().to(apub::community::get_apub_community_http), -- 2.44.1