Merge pull request #663 from LemmyNet/federation_changes_1

Federation changes.
This commit is contained in:
Dessalines 2020-04-27 13:00:21 -04:00 committed by GitHub
commit 10877fd45f
26 changed files with 508 additions and 451 deletions

1
.dockerignore vendored
View file

@ -3,6 +3,7 @@ ui/node_modules
server/target server/target
docker/dev/volumes docker/dev/volumes
docker/federation/volumes docker/federation/volumes
docker/federation-test/volumes
.git .git
ansible ansible

1
.gitignore vendored
View file

@ -7,6 +7,7 @@ ansible/passwords/
docker/lemmy_mine.hjson docker/lemmy_mine.hjson
docker/dev/env_deploy.sh docker/dev/env_deploy.sh
docker/federation/volumes docker/federation/volumes
docker/federation-test/volumes
docker/dev/volumes docker/dev/volumes
# local build files # local build files

21
docker/federation-test/run-tests.sh vendored Executable file
View file

@ -0,0 +1,21 @@
#!/bin/bash
set -e
pushd ../../server/
cargo build
popd
sudo docker build ../../ --file ../federation/Dockerfile --tag lemmy-federation:latest
sudo docker-compose --file ../federation/docker-compose.yml --project-directory . up -d
pushd ../../ui
yarn
echo "Waiting for Lemmy to start..."
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8540/api/v1/site')" != "200" ]]; do sleep 5; done
yarn api-test || true
popd
sudo docker-compose --file ../federation/docker-compose.yml --project-directory . down
sudo rm -r volumes/

View file

@ -7,7 +7,8 @@ services:
- "8540:8540" - "8540:8540"
- "8550:8550" - "8550:8550"
volumes: volumes:
- ./nginx.conf:/etc/nginx/nginx.conf # Hack to make this work from both docker/federation/ and docker/federation-test/
- ../federation/nginx.conf:/etc/nginx/nginx.conf
depends_on: depends_on:
- lemmy_alpha - lemmy_alpha
- pictshare_alpha - pictshare_alpha

View file

@ -12,6 +12,6 @@ pushd ../../server/ || exit
cargo build cargo build
popd || exit popd || exit
sudo docker build ../../ -f Dockerfile -t lemmy-federation:latest sudo docker build ../../ --file Dockerfile -t lemmy-federation:latest
sudo docker-compose up sudo docker-compose up

View file

@ -122,7 +122,7 @@ impl Perform for Oper<CreateComment> {
let extracted_usernames = extract_usernames(&comment_form.content); let extracted_usernames = extract_usernames(&comment_form.content);
for username_mention in &extracted_usernames { for username_mention in &extracted_usernames {
if let Ok(mention_user) = User_::read_from_name(&conn, (*username_mention).to_string()) { if let Ok(mention_user) = User_::read_from_name(&conn, username_mention) {
// You can't mention yourself // You can't mention yourself
// At some point, make it so you can't tag the parent creator either // At some point, make it so you can't tag the parent creator either
// This can cause two notifications, one for reply and the other for mention // This can cause two notifications, one for reply and the other for mention
@ -334,7 +334,7 @@ impl Perform for Oper<EditComment> {
let extracted_usernames = extract_usernames(&comment_form.content); let extracted_usernames = extract_usernames(&comment_form.content);
for username_mention in &extracted_usernames { for username_mention in &extracted_usernames {
let mention_user = User_::read_from_name(&conn, (*username_mention).to_string()); let mention_user = User_::read_from_name(&conn, username_mention);
if mention_user.is_ok() { if mention_user.is_ok() {
let mention_user_id = mention_user?.id; let mention_user_id = mention_user?.id;

View file

@ -139,7 +139,7 @@ impl Perform for Oper<GetCommunity> {
None => { None => {
match Community::read_from_name( match Community::read_from_name(
&conn, &conn,
data.name.to_owned().unwrap_or_else(|| "main".to_string()), &data.name.to_owned().unwrap_or_else(|| "main".to_string()),
) { ) {
Ok(community) => community, Ok(community) => community,
Err(_e) => return Err(APIError::err("couldnt_find_community").into()), Err(_e) => return Err(APIError::err("couldnt_find_community").into()),

View file

@ -68,8 +68,8 @@ pub struct Oper<T> {
data: T, data: T,
} }
impl<T> Oper<T> { impl<Data> Oper<Data> {
pub fn new(data: T) -> Oper<T> { pub fn new(data: Data) -> Oper<Data> {
Oper { data } Oper { data }
} }
} }

View file

@ -261,7 +261,7 @@ impl Perform for Oper<Register> {
return Err(APIError::err("admin_already_created").into()); return Err(APIError::err("admin_already_created").into());
} }
let keypair = generate_actor_keypair()?; let user_keypair = generate_actor_keypair()?;
// Register the new user // Register the new user
let user_form = UserForm { let user_form = UserForm {
@ -284,8 +284,8 @@ impl Perform for Oper<Register> {
actor_id: make_apub_endpoint(EndpointType::User, &data.username).to_string(), actor_id: make_apub_endpoint(EndpointType::User, &data.username).to_string(),
bio: None, bio: None,
local: true, local: true,
private_key: Some(keypair.private_key), private_key: Some(user_keypair.private_key),
public_key: Some(keypair.public_key), public_key: Some(user_keypair.public_key),
last_refreshed_at: None, last_refreshed_at: None,
}; };
@ -305,7 +305,7 @@ impl Perform for Oper<Register> {
} }
}; };
let keypair = generate_actor_keypair()?; let main_community_keypair = generate_actor_keypair()?;
// Create the main community if it doesn't exist // Create the main community if it doesn't exist
let main_community: Community = match Community::read(&conn, 2) { let main_community: Community = match Community::read(&conn, 2) {
@ -324,8 +324,8 @@ impl Perform for Oper<Register> {
updated: None, updated: None,
actor_id: make_apub_endpoint(EndpointType::Community, default_community_name).to_string(), actor_id: make_apub_endpoint(EndpointType::Community, default_community_name).to_string(),
local: true, local: true,
private_key: Some(keypair.private_key), private_key: Some(main_community_keypair.private_key),
public_key: Some(keypair.public_key), public_key: Some(main_community_keypair.public_key),
last_refreshed_at: None, last_refreshed_at: None,
published: None, published: None,
}; };
@ -504,7 +504,7 @@ impl Perform for Oper<GetUserDetails> {
None => { None => {
match User_::read_from_name( match User_::read_from_name(
&conn, &conn,
data &data
.username .username
.to_owned() .to_owned()
.unwrap_or_else(|| "admin".to_string()), .unwrap_or_else(|| "admin".to_string()),

View file

@ -1,21 +1,4 @@
use crate::apub::is_apub_id_valid; use super::*;
use crate::apub::signatures::sign;
use crate::db::community::Community;
use crate::db::community_view::CommunityFollowerView;
use crate::db::post::Post;
use crate::db::user::User_;
use crate::db::Crud;
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;
use failure::_core::fmt::Debug;
use isahc::prelude::*;
use log::debug;
use serde::Serialize;
use url::Url;
fn populate_object_props( fn populate_object_props(
props: &mut ObjectProperties, props: &mut ObjectProperties,
@ -45,6 +28,8 @@ where
{ {
let json = serde_json::to_string(&activity)?; let json = serde_json::to_string(&activity)?;
debug!("Sending activitypub activity {} to {:?}", json, to); debug!("Sending activitypub activity {} to {:?}", json, to);
// TODO it needs to expand, the to field needs to expand and dedup the followers urls
// The inbox is determined by first retrieving the target actor's JSON-LD representation and then looking up the inbox property. If a recipient is a Collection or OrderedCollection, then the server MUST dereference the collection (with the user's credentials) and discover inboxes for each item in the collection. Servers MUST limit the number of layers of indirections through collections which will be performed, which MAY be one.
for t in to { for t in to {
let to_url = Url::parse(&t)?; let to_url = Url::parse(&t)?;
if !is_apub_id_valid(&to_url) { if !is_apub_id_valid(&to_url) {
@ -76,7 +61,7 @@ fn get_follower_inboxes(conn: &PgConnection, community: &Community) -> Result<Ve
/// Send out information about a newly created post, to the followers of the community. /// 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 post_create(post: &Post, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let page = post.as_page(conn)?; let page = post.to_apub(conn)?;
let community = Community::read(conn, post.community_id)?; let community = Community::read(conn, post.community_id)?;
let mut create = Create::new(); let mut create = Create::new();
populate_object_props( populate_object_props(
@ -99,7 +84,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. /// 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 post_update(post: &Post, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let page = post.as_page(conn)?; let page = post.to_apub(conn)?;
let community = Community::read(conn, post.community_id)?; let community = Community::read(conn, post.community_id)?;
let mut update = Update::new(); let mut update = Update::new();
populate_object_props( populate_object_props(
@ -136,6 +121,7 @@ pub fn follow_community(
.follow_props .follow_props
.set_actor_xsd_any_uri(user.actor_id.clone())? .set_actor_xsd_any_uri(user.actor_id.clone())?
.set_object_xsd_any_uri(community.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); let to = format!("{}/inbox", community.actor_id);
send_activity( send_activity(
&follow, &follow,
@ -153,6 +139,11 @@ pub fn accept_follow(follow: &Follow, conn: &PgConnection) -> Result<(), Error>
.get_object_xsd_any_uri() .get_object_xsd_any_uri()
.unwrap() .unwrap()
.to_string(); .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 community = Community::read_from_actor_id(conn, &community_uri)?;
let mut accept = Accept::new(); let mut accept = Accept::new();
accept accept
@ -168,8 +159,10 @@ pub fn accept_follow(follow: &Follow, conn: &PgConnection) -> Result<(), Error>
)?; )?;
accept accept
.accept_props .accept_props
.set_actor_xsd_any_uri(community.actor_id.clone())?
.set_object_base_box(BaseBox::from_concrete(follow.clone())?)?; .set_object_base_box(BaseBox::from_concrete(follow.clone())?)?;
let to = format!("{}/inbox", community_uri); // TODO this is incorrect, the to field should not be the inbox, but the followers url
let to = format!("{}/inbox", actor_uri);
send_activity( send_activity(
&accept, &accept,
&community.private_key.unwrap(), &community.private_key.unwrap(),

View file

@ -1,37 +1,15 @@
use crate::apub::fetcher::{fetch_remote_object, fetch_remote_user}; use super::*;
use crate::apub::signatures::PublicKey;
use crate::apub::*;
use crate::db::community::{Community, CommunityForm};
use crate::db::community_view::CommunityFollowerView;
use crate::db::establish_unpooled_connection;
use crate::db::post::Post;
use crate::db::user::User_;
use crate::db::Crud;
use crate::{convert_datetime, naive_now};
use activitystreams::actor::properties::ApActorProperties;
use activitystreams::collection::OrderedCollection;
use activitystreams::{
actor::Group, collection::UnorderedCollection, context, ext::Extensible,
object::properties::ObjectProperties,
};
use actix_web::body::Body;
use actix_web::web::Path;
use actix_web::HttpResponse;
use actix_web::{web, Result};
use diesel::r2d2::{ConnectionManager, Pool};
use diesel::PgConnection;
use failure::Error;
use serde::Deserialize;
use url::Url;
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct CommunityQuery { pub struct CommunityQuery {
community_name: String, community_name: String,
} }
impl Community { impl ToApub for Community {
type Response = GroupExt;
// Turn a Lemmy Community into an ActivityPub group that can be sent out over the network. // Turn a Lemmy Community into an ActivityPub group that can be sent out over the network.
fn as_group(&self, conn: &PgConnection) -> Result<GroupExt, Error> { fn to_apub(&self, conn: &PgConnection) -> Result<GroupExt, Error> {
let mut group = Group::default(); let mut group = Group::default();
let oprops: &mut ObjectProperties = group.as_mut(); let oprops: &mut ObjectProperties = group.as_mut();
@ -60,39 +38,36 @@ impl Community {
.set_outbox(self.get_outbox_url())? .set_outbox(self.get_outbox_url())?
.set_followers(self.get_followers_url())?; .set_followers(self.get_followers_url())?;
let public_key = PublicKey { Ok(group.extend(actor_props).extend(self.get_public_key_ext()))
id: format!("{}#main-key", self.actor_id),
owner: self.actor_id.to_owned(),
public_key_pem: self.public_key.to_owned().unwrap(),
};
Ok(group.extend(actor_props).extend(public_key.to_ext()))
}
pub fn get_followers_url(&self) -> String {
format!("{}/followers", &self.actor_id)
}
pub fn get_inbox_url(&self) -> String {
format!("{}/inbox", &self.actor_id)
}
pub fn get_outbox_url(&self) -> String {
format!("{}/outbox", &self.actor_id)
} }
} }
impl CommunityForm { impl ActorType for Community {
fn actor_id(&self) -> String {
self.actor_id.to_owned()
}
fn public_key(&self) -> String {
self.public_key.to_owned().unwrap()
}
}
impl FromApub for CommunityForm {
type ApubType = GroupExt;
/// Parse an ActivityPub group received from another instance into a Lemmy community. /// Parse an ActivityPub group received from another instance into a Lemmy community.
pub fn from_group(group: &GroupExt, conn: &PgConnection) -> Result<Self, Error> { fn from_apub(group: &GroupExt, conn: &PgConnection) -> Result<Self, Error> {
let oprops = &group.base.base.object_props; let oprops = &group.base.base.object_props;
let aprops = &group.base.extension; let aprops = &group.base.extension;
let public_key: &PublicKey = &group.extension.public_key; let public_key: &PublicKey = &group.extension.public_key;
let followers_uri = Url::parse(&aprops.get_followers().unwrap().to_string())?; let _followers_uri = Url::parse(&aprops.get_followers().unwrap().to_string())?;
let outbox_uri = Url::parse(&aprops.get_outbox().to_string())?; let _outbox_uri = Url::parse(&aprops.get_outbox().to_string())?;
let _outbox = fetch_remote_object::<OrderedCollection>(&outbox_uri)?; // TODO don't do extra fetching here
let _followers = fetch_remote_object::<UnorderedCollection>(&followers_uri)?; // let _outbox = fetch_remote_object::<OrderedCollection>(&outbox_uri)?;
let apub_id = Url::parse(&oprops.get_attributed_to_xsd_any_uri().unwrap().to_string())?; // let _followers = fetch_remote_object::<UnorderedCollection>(&followers_uri)?;
let creator = fetch_remote_user(&apub_id, conn)?; let apub_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string();
let creator = get_or_fetch_and_upsert_remote_user(&apub_id, conn)?;
Ok(CommunityForm { Ok(CommunityForm {
name: oprops.get_name_xsd_string().unwrap().to_string(), name: oprops.get_name_xsd_string().unwrap().to_string(),
@ -123,24 +98,25 @@ impl CommunityForm {
/// Return the community json over HTTP. /// Return the community json over HTTP.
pub async fn get_apub_community_http( pub async fn get_apub_community_http(
info: Path<CommunityQuery>, info: Path<CommunityQuery>,
db: web::Data<Pool<ConnectionManager<PgConnection>>>, db: DbPoolParam,
) -> Result<HttpResponse<Body>, Error> { ) -> Result<HttpResponse<Body>, Error> {
let community = Community::read_from_name(&&db.get()?, info.community_name.to_owned())?; let community = Community::read_from_name(&&db.get()?, &info.community_name)?;
let c = community.as_group(&db.get().unwrap())?; let c = community.to_apub(&db.get().unwrap())?;
Ok(create_apub_response(&c)) Ok(create_apub_response(&c))
} }
/// Returns an empty followers collection, only populating the siz (for privacy). /// Returns an empty followers collection, only populating the siz (for privacy).
// TODO this needs to return the actual followers, and the to: field needs this
pub async fn get_apub_community_followers( pub async fn get_apub_community_followers(
info: Path<CommunityQuery>, info: Path<CommunityQuery>,
db: web::Data<Pool<ConnectionManager<PgConnection>>>, db: DbPoolParam,
) -> Result<HttpResponse<Body>, Error> { ) -> Result<HttpResponse<Body>, Error> {
let community = Community::read_from_name(&&db.get()?, info.community_name.to_owned())?; let community = Community::read_from_name(&&db.get()?, &info.community_name)?;
let conn = db.get()?;
let connection = establish_unpooled_connection();
//As we are an object, we validated that the community id was valid //As we are an object, we validated that the community id was valid
let community_followers = let community_followers = CommunityFollowerView::for_community(&conn, community.id).unwrap();
CommunityFollowerView::for_community(&connection, community.id).unwrap();
let mut collection = UnorderedCollection::default(); let mut collection = UnorderedCollection::default();
let oprops: &mut ObjectProperties = collection.as_mut(); let oprops: &mut ObjectProperties = collection.as_mut();
@ -153,31 +129,33 @@ pub async fn get_apub_community_followers(
Ok(create_apub_response(&collection)) Ok(create_apub_response(&collection))
} }
/// Returns an UnorderedCollection with the latest posts from the community. // TODO should not be doing this
pub async fn get_apub_community_outbox( // Returns an UnorderedCollection with the latest posts from the community.
info: Path<CommunityQuery>, //pub async fn get_apub_community_outbox(
db: web::Data<Pool<ConnectionManager<PgConnection>>>, // info: Path<CommunityQuery>,
) -> Result<HttpResponse<Body>, Error> { // db: DbPoolParam,
let community = Community::read_from_name(&&db.get()?, info.community_name.to_owned())?; // chat_server: ChatServerParam,
//) -> Result<HttpResponse<Body>, Error> {
// let community = Community::read_from_name(&&db.get()?, &info.community_name)?;
let conn = establish_unpooled_connection(); // let conn = establish_unpooled_connection();
//As we are an object, we validated that the community id was valid // //As we are an object, we validated that the community id was valid
let community_posts: Vec<Post> = Post::list_for_community(&conn, community.id)?; // let community_posts: Vec<Post> = Post::list_for_community(&conn, community.id)?;
let mut collection = OrderedCollection::default(); // let mut collection = OrderedCollection::default();
let oprops: &mut ObjectProperties = collection.as_mut(); // let oprops: &mut ObjectProperties = collection.as_mut();
oprops // oprops
.set_context_xsd_any_uri(context())? // .set_context_xsd_any_uri(context())?
.set_id(community.actor_id)?; // .set_id(community.actor_id)?;
collection // collection
.collection_props // .collection_props
.set_many_items_base_boxes( // .set_many_items_base_boxes(
community_posts // community_posts
.iter() // .iter()
.map(|c| c.as_page(&conn).unwrap()) // .map(|c| c.as_page(&conn).unwrap())
.collect(), // .collect(),
)? // )?
.set_total_items(community_posts.len() as u64)?; // .set_total_items(community_posts.len() as u64)?;
Ok(create_apub_response(&collection)) // Ok(create_apub_response(&collection))
} //}

View file

@ -1,16 +1,4 @@
use crate::apub::activities::accept_follow; use super::*;
use crate::apub::fetcher::fetch_remote_user;
use crate::apub::signatures::verify;
use crate::db::community::{Community, CommunityFollower, CommunityFollowerForm};
use crate::db::Followable;
use activitystreams::activity::Follow;
use actix_web::{web, HttpRequest, HttpResponse};
use diesel::r2d2::{ConnectionManager, Pool};
use diesel::PgConnection;
use failure::Error;
use log::debug;
use serde::Deserialize;
use url::Url;
#[serde(untagged)] #[serde(untagged)]
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
@ -18,22 +6,25 @@ pub enum CommunityAcceptedObjects {
Follow(Follow), Follow(Follow),
} }
// TODO Consolidate community and user inboxes into a single shared one
/// Handler for all incoming activities to community inboxes. /// Handler for all incoming activities to community inboxes.
pub async fn community_inbox( pub async fn community_inbox(
request: HttpRequest, request: HttpRequest,
input: web::Json<CommunityAcceptedObjects>, input: web::Json<CommunityAcceptedObjects>,
path: web::Path<String>, path: web::Path<String>,
db: web::Data<Pool<ConnectionManager<PgConnection>>>, db: DbPoolParam,
chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let input = input.into_inner(); let input = input.into_inner();
let conn = &db.get().unwrap(); let community_name = path.into_inner();
debug!( debug!(
"Community {} received activity {:?}", "Community {} received activity {:?}",
&path.into_inner(), &community_name, &input
&input
); );
match input { match input {
CommunityAcceptedObjects::Follow(f) => handle_follow(&f, &request, conn), CommunityAcceptedObjects::Follow(f) => {
handle_follow(&f, &request, &community_name, db, chat_server)
}
} }
} }
@ -42,28 +33,36 @@ pub async fn community_inbox(
fn handle_follow( fn handle_follow(
follow: &Follow, follow: &Follow,
request: &HttpRequest, request: &HttpRequest,
conn: &PgConnection, community_name: &str,
db: DbPoolParam,
_chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let user_uri = follow let user_uri = follow
.follow_props .follow_props
.get_actor_xsd_any_uri() .get_actor_xsd_any_uri()
.unwrap() .unwrap()
.to_string(); .to_string();
let user = fetch_remote_user(&Url::parse(&user_uri)?, conn)?; let _community_uri = follow
verify(&request, &user.public_key.unwrap())?;
// TODO: make sure this is a local community
let community_uri = follow
.follow_props .follow_props
.get_object_xsd_any_uri() .get_object_xsd_any_uri()
.unwrap() .unwrap()
.to_string(); .to_string();
let community = Community::read_from_actor_id(conn, &community_uri)?;
let conn = db.get()?;
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?;
let community = Community::read_from_name(&conn, &community_name)?;
verify(&request, &user.public_key.unwrap())?;
let community_follower_form = CommunityFollowerForm { let community_follower_form = CommunityFollowerForm {
community_id: community.id, community_id: community.id,
user_id: user.id, user_id: user.id,
}; };
CommunityFollower::follow(&conn, &community_follower_form)?;
accept_follow(&follow, conn)?; // This will fail if they're already a follower, but ignore the error.
CommunityFollower::follow(&conn, &community_follower_form).ok();
accept_follow(&follow, &conn)?;
Ok(HttpResponse::Ok().finish()) Ok(HttpResponse::Ok().finish())
} }

View file

@ -1,23 +1,4 @@
use crate::api::site::SearchResponse; use super::*;
use crate::apub::*;
use crate::db::community::{Community, CommunityForm};
use crate::db::community_view::CommunityView;
use crate::db::post::{Post, PostForm};
use crate::db::post_view::PostView;
use crate::db::user::{UserForm, User_};
use crate::db::user_view::UserView;
use crate::db::{Crud, SearchType};
use crate::routes::nodeinfo::{NodeInfo, NodeInfoWellKnown};
use activitystreams::collection::OrderedCollection;
use activitystreams::object::Page;
use activitystreams::BaseBox;
use diesel::result::Error::NotFound;
use diesel::PgConnection;
use failure::Error;
use isahc::prelude::*;
use serde::Deserialize;
use std::time::Duration;
use url::Url;
// Fetch nodeinfo metadata from a remote instance. // Fetch nodeinfo metadata from a remote instance.
fn _fetch_node_info(domain: &str) -> Result<NodeInfo, Error> { fn _fetch_node_info(domain: &str) -> Result<NodeInfo, Error> {
@ -30,39 +11,8 @@ fn _fetch_node_info(domain: &str) -> Result<NodeInfo, Error> {
Ok(fetch_remote_object::<NodeInfo>(&well_known.links.href)?) Ok(fetch_remote_object::<NodeInfo>(&well_known.links.href)?)
} }
// TODO: move these to db
fn upsert_community(
community_form: &CommunityForm,
conn: &PgConnection,
) -> Result<Community, Error> {
let existing = Community::read_from_actor_id(conn, &community_form.actor_id);
match existing {
Err(NotFound {}) => Ok(Community::create(conn, &community_form)?),
Ok(c) => Ok(Community::update(conn, c.id, &community_form)?),
Err(e) => Err(Error::from(e)),
}
}
fn upsert_user(user_form: &UserForm, conn: &PgConnection) -> Result<User_, Error> {
let existing = User_::read_from_apub_id(conn, &user_form.actor_id);
Ok(match existing {
Err(NotFound {}) => User_::create(conn, &user_form)?,
Ok(u) => User_::update(conn, u.id, &user_form)?,
Err(e) => return Err(Error::from(e)),
})
}
fn upsert_post(post_form: &PostForm, conn: &PgConnection) -> Result<Post, Error> {
let existing = Post::read_from_apub_id(conn, &post_form.ap_id);
match existing {
Err(NotFound {}) => Ok(Post::create(conn, &post_form)?),
Ok(p) => Ok(Post::update(conn, p.id, &post_form)?),
Err(e) => Err(Error::from(e)),
}
}
/// Fetch any type of ActivityPub object, handling things like HTTP headers, deserialisation, /// Fetch any type of ActivityPub object, handling things like HTTP headers, deserialisation,
/// timeouts etc. /// timeouts etc.
/// TODO: add an optional param last_updated and only fetch if its too old
pub fn fetch_remote_object<Response>(url: &Url) -> Result<Response, Error> pub fn fetch_remote_object<Response>(url: &Url) -> Result<Response, Error>
where where
Response: for<'de> Deserialize<'de>, Response: for<'de> Deserialize<'de>,
@ -95,9 +45,9 @@ pub enum SearchAcceptedObjects {
/// Attempt to parse the query as URL, and fetch an ActivityPub object from it. /// Attempt to parse the query as URL, and fetch an ActivityPub object from it.
/// ///
/// Some working examples for use with the docker/federation/ setup: /// Some working examples for use with the docker/federation/ setup:
/// http://lemmy_alpha:8540/federation/c/main /// http://lemmy_alpha:8540/c/main
/// http://lemmy_alpha:8540/federation/u/lemmy_alpha /// http://lemmy_alpha:8540/u/lemmy_alpha
/// http://lemmy_alpha:8540/federation/p/3 /// http://lemmy_alpha:8540/p/3
pub fn search_by_apub_id(query: &str, conn: &PgConnection) -> Result<SearchResponse, Error> { pub fn search_by_apub_id(query: &str, conn: &PgConnection) -> Result<SearchResponse, Error> {
let query_url = Url::parse(&query)?; let query_url = Url::parse(&query)?;
let mut response = SearchResponse { let mut response = SearchResponse {
@ -109,50 +59,117 @@ pub fn search_by_apub_id(query: &str, conn: &PgConnection) -> Result<SearchRespo
}; };
match fetch_remote_object::<SearchAcceptedObjects>(&query_url)? { match fetch_remote_object::<SearchAcceptedObjects>(&query_url)? {
SearchAcceptedObjects::Person(p) => { SearchAcceptedObjects::Person(p) => {
let u = upsert_user(&UserForm::from_person(&p)?, conn)?; let user_uri = p.base.base.object_props.get_id().unwrap().to_string();
response.users = vec![UserView::read(conn, u.id)?]; let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?;
response.users = vec![UserView::read(conn, user.id)?];
} }
SearchAcceptedObjects::Group(g) => { SearchAcceptedObjects::Group(g) => {
let c = upsert_community(&CommunityForm::from_group(&g, conn)?, conn)?; let community_uri = g.base.base.object_props.get_id().unwrap().to_string();
fetch_community_outbox(&c, conn)?; let community = get_or_fetch_and_upsert_remote_community(&community_uri, &conn)?;
response.communities = vec![CommunityView::read(conn, c.id, None)?]; // TODO Maybe at some point in the future, fetch all the history of a community
// fetch_community_outbox(&c, conn)?;
response.communities = vec![CommunityView::read(conn, community.id, None)?];
} }
SearchAcceptedObjects::Page(p) => { SearchAcceptedObjects::Page(p) => {
let p = upsert_post(&PostForm::from_page(&p, conn)?, conn)?; let p = upsert_post(&PostForm::from_apub(&p, conn)?, conn)?;
response.posts = vec![PostView::read(conn, p.id, None)?]; response.posts = vec![PostView::read(conn, p.id, None)?];
} }
} }
Ok(response) Ok(response)
} }
/// Fetch all posts in the outbox of the given user, and insert them into the database. /// Check if a remote user exists, create if not found, if its too old update it.Fetch a user, insert/update it in the database and return the user.
fn fetch_community_outbox(community: &Community, conn: &PgConnection) -> Result<Vec<Post>, Error> { pub fn get_or_fetch_and_upsert_remote_user(
let outbox_url = Url::parse(&community.get_outbox_url())?; apub_id: &str,
let outbox = fetch_remote_object::<OrderedCollection>(&outbox_url)?; conn: &PgConnection,
let items = outbox.collection_props.get_many_items_base_boxes(); ) -> Result<User_, Error> {
match User_::read_from_actor_id(&conn, &apub_id) {
Ok( Ok(u) => {
items // If its older than a day, re-fetch it
.unwrap() // TODO the less than needs to be tested
.map(|obox: &BaseBox| -> Result<PostForm, Error> { if u
let page = obox.clone().to_concrete::<Page>()?; .last_refreshed_at
PostForm::from_page(&page, conn) .lt(&(naive_now() - chrono::Duration::days(1)))
}) {
.map(|pf| upsert_post(&pf?, conn)) debug!("Fetching and updating from remote user: {}", apub_id);
.collect::<Result<Vec<Post>, Error>>()?, let person = fetch_remote_object::<PersonExt>(&Url::parse(apub_id)?)?;
) let mut uf = UserForm::from_apub(&person, &conn)?;
uf.last_refreshed_at = Some(naive_now());
Ok(User_::update(&conn, u.id, &uf)?)
} else {
Ok(u)
}
}
Err(NotFound {}) => {
debug!("Fetching and creating remote user: {}", apub_id);
let person = fetch_remote_object::<PersonExt>(&Url::parse(apub_id)?)?;
let uf = UserForm::from_apub(&person, &conn)?;
Ok(User_::create(conn, &uf)?)
}
Err(e) => Err(Error::from(e)),
}
} }
/// Fetch a user, insert/update it in the database and return the user. /// Check if a remote community exists, create if not found, if its too old update it.Fetch a community, insert/update it in the database and return the community.
pub fn fetch_remote_user(apub_id: &Url, conn: &PgConnection) -> Result<User_, Error> { pub fn get_or_fetch_and_upsert_remote_community(
let person = fetch_remote_object::<PersonExt>(apub_id)?; apub_id: &str,
let uf = UserForm::from_person(&person)?; conn: &PgConnection,
upsert_user(&uf, conn) ) -> Result<Community, Error> {
match Community::read_from_actor_id(&conn, &apub_id) {
Ok(c) => {
// If its older than a day, re-fetch it
// TODO the less than needs to be tested
if c
.last_refreshed_at
.lt(&(naive_now() - chrono::Duration::days(1)))
{
debug!("Fetching and updating from remote community: {}", apub_id);
let group = fetch_remote_object::<GroupExt>(&Url::parse(apub_id)?)?;
let mut cf = CommunityForm::from_apub(&group, conn)?;
cf.last_refreshed_at = Some(naive_now());
Ok(Community::update(&conn, c.id, &cf)?)
} else {
Ok(c)
}
}
Err(NotFound {}) => {
debug!("Fetching and creating remote community: {}", apub_id);
let group = fetch_remote_object::<GroupExt>(&Url::parse(apub_id)?)?;
let cf = CommunityForm::from_apub(&group, conn)?;
Ok(Community::create(conn, &cf)?)
}
Err(e) => Err(Error::from(e)),
}
} }
/// Fetch a community, insert/update it in the database and return the community. fn upsert_post(post_form: &PostForm, conn: &PgConnection) -> Result<Post, Error> {
pub fn fetch_remote_community(apub_id: &Url, conn: &PgConnection) -> Result<Community, Error> { let existing = Post::read_from_apub_id(conn, &post_form.ap_id);
let group = fetch_remote_object::<GroupExt>(apub_id)?; match existing {
let cf = CommunityForm::from_group(&group, conn)?; Err(NotFound {}) => Ok(Post::create(conn, &post_form)?),
upsert_community(&cf, conn) Ok(p) => Ok(Post::update(conn, p.id, &post_form)?),
Err(e) => Err(Error::from(e)),
} }
}
// TODO It should not be fetching data from a community outbox.
// All posts, comments, comment likes, etc should be posts to our community_inbox
// The only data we should be periodically fetching (if it hasn't been fetched in the last day
// maybe), is community and user actors
// and user actors
// Fetch all posts in the outbox of the given user, and insert them into the database.
// fn fetch_community_outbox(community: &Community, conn: &PgConnection) -> Result<Vec<Post>, Error> {
// let outbox_url = Url::parse(&community.get_outbox_url())?;
// let outbox = fetch_remote_object::<OrderedCollection>(&outbox_url)?;
// let items = outbox.collection_props.get_many_items_base_boxes();
// Ok(
// items
// .unwrap()
// .map(|obox: &BaseBox| -> Result<PostForm, Error> {
// let page = obox.clone().to_concrete::<Page>()?;
// PostForm::from_page(&page, conn)
// })
// .map(|pf| upsert_post(&pf?, conn))
// .collect::<Result<Vec<Post>, Error>>()?,
// )
// }

View file

@ -6,15 +6,52 @@ pub mod post;
pub mod signatures; pub mod signatures;
pub mod user; pub mod user;
pub mod user_inbox; pub mod user_inbox;
use crate::apub::signatures::PublicKeyExtension;
use crate::Settings; use activitystreams::{
use activitystreams::actor::{properties::ApActorProperties, Group, Person}; activity::{Accept, Create, Follow, Update},
use activitystreams::ext::Ext; actor::{properties::ApActorProperties, Actor, Group, Person},
collection::UnorderedCollection,
context,
ext::{Ext, Extensible, Extension},
object::{properties::ObjectProperties, Page},
public, BaseBox,
};
use actix_web::body::Body; use actix_web::body::Body;
use actix_web::HttpResponse; use actix_web::web::Path;
use serde::ser::Serialize; use actix_web::{web, HttpRequest, HttpResponse, Result};
use diesel::result::Error::NotFound;
use diesel::PgConnection;
use failure::Error;
use failure::_core::fmt::Debug;
use http::request::Builder;
use http_signature_normalization::Config;
use isahc::prelude::*;
use log::debug;
use openssl::hash::MessageDigest;
use openssl::sign::{Signer, Verifier};
use openssl::{pkey::PKey, rsa::Rsa};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::time::Duration;
use url::Url; use url::Url;
use crate::api::site::SearchResponse;
use crate::db::community::{Community, CommunityFollower, CommunityFollowerForm, CommunityForm};
use crate::db::community_view::{CommunityFollowerView, CommunityView};
use crate::db::post::{Post, PostForm};
use crate::db::post_view::PostView;
use crate::db::user::{UserForm, User_};
use crate::db::user_view::UserView;
use crate::db::{Crud, Followable, SearchType};
use crate::routes::nodeinfo::{NodeInfo, NodeInfoWellKnown};
use crate::routes::{ChatServerParam, DbPoolParam};
use crate::{convert_datetime, naive_now, Settings};
use activities::accept_follow;
use fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user};
use signatures::verify;
use signatures::{sign, PublicKey, PublicKeyExtension};
type GroupExt = Ext<Ext<Group, ApActorProperties>, PublicKeyExtension>; type GroupExt = Ext<Ext<Group, ApActorProperties>, PublicKeyExtension>;
type PersonExt = Ext<Ext<Person, ApActorProperties>, PublicKeyExtension>; type PersonExt = Ext<Ext<Person, ApActorProperties>, PublicKeyExtension>;
@ -88,3 +125,48 @@ fn is_apub_id_valid(apub_id: &Url) -> bool {
None => false, None => false,
} }
} }
// TODO Not sure good names for these
pub trait ToApub {
type Response;
fn to_apub(&self, conn: &PgConnection) -> Result<Self::Response, Error>;
}
pub trait FromApub {
type ApubType;
fn from_apub(apub: &Self::ApubType, conn: &PgConnection) -> Result<Self, Error>
where
Self: Sized;
}
pub trait ActorType {
fn actor_id(&self) -> String;
fn public_key(&self) -> String;
fn get_inbox_url(&self) -> String {
format!("{}/inbox", &self.actor_id())
}
fn get_outbox_url(&self) -> String {
format!("{}/outbox", &self.actor_id())
}
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())
}
fn get_public_key_ext(&self) -> PublicKeyExtension {
PublicKey {
id: format!("{}#main-key", self.actor_id()),
owner: self.actor_id(),
public_key_pem: self.public_key(),
}
.to_ext()
}
}

View file

@ -1,19 +1,4 @@
use crate::apub::create_apub_response; use super::*;
use crate::apub::fetcher::{fetch_remote_community, fetch_remote_user};
use crate::convert_datetime;
use crate::db::community::Community;
use crate::db::post::{Post, PostForm};
use crate::db::user::User_;
use crate::db::Crud;
use activitystreams::{context, object::properties::ObjectProperties, object::Page};
use actix_web::body::Body;
use actix_web::web::Path;
use actix_web::{web, HttpResponse};
use diesel::r2d2::{ConnectionManager, Pool};
use diesel::PgConnection;
use failure::Error;
use serde::Deserialize;
use url::Url;
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct PostQuery { pub struct PostQuery {
@ -23,16 +8,18 @@ pub struct PostQuery {
/// Return the post json over HTTP. /// Return the post json over HTTP.
pub async fn get_apub_post( pub async fn get_apub_post(
info: Path<PostQuery>, info: Path<PostQuery>,
db: web::Data<Pool<ConnectionManager<PgConnection>>>, db: DbPoolParam,
) -> Result<HttpResponse<Body>, Error> { ) -> Result<HttpResponse<Body>, Error> {
let id = info.post_id.parse::<i32>()?; let id = info.post_id.parse::<i32>()?;
let post = Post::read(&&db.get()?, id)?; let post = Post::read(&&db.get()?, id)?;
Ok(create_apub_response(&post.as_page(&db.get().unwrap())?)) Ok(create_apub_response(&post.to_apub(&db.get().unwrap())?))
} }
impl Post { impl ToApub for Post {
type Response = Page;
// Turn a Lemmy post into an ActivityPub page that can be sent out over the network. // Turn a Lemmy post into an ActivityPub page that can be sent out over the network.
pub fn as_page(&self, conn: &PgConnection) -> Result<Page, Error> { fn to_apub(&self, conn: &PgConnection) -> Result<Page, Error> {
let mut page = Page::default(); let mut page = Page::default();
let oprops: &mut ObjectProperties = page.as_mut(); let oprops: &mut ObjectProperties = page.as_mut();
let creator = User_::read(conn, self.creator_id)?; let creator = User_::read(conn, self.creator_id)?;
@ -68,14 +55,16 @@ impl Post {
} }
} }
impl PostForm { impl FromApub for PostForm {
type ApubType = Page;
/// Parse an ActivityPub page received from another instance into a Lemmy post. /// Parse an ActivityPub page received from another instance into a Lemmy post.
pub fn from_page(page: &Page, conn: &PgConnection) -> Result<PostForm, Error> { fn from_apub(page: &Page, conn: &PgConnection) -> Result<PostForm, Error> {
let oprops = &page.object_props; let oprops = &page.object_props;
let creator_id = Url::parse(&oprops.get_attributed_to_xsd_any_uri().unwrap().to_string())?; let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string();
let creator = fetch_remote_user(&creator_id, conn)?; let creator = get_or_fetch_and_upsert_remote_user(&creator_actor_id, &conn)?;
let community_id = Url::parse(&oprops.get_to_xsd_any_uri().unwrap().to_string())?; let community_actor_id = &oprops.get_to_xsd_any_uri().unwrap().to_string();
let community = fetch_remote_community(&community_id, conn)?; let community = get_or_fetch_and_upsert_remote_community(&community_actor_id, &conn)?;
Ok(PostForm { Ok(PostForm {
name: oprops.get_summary_xsd_string().unwrap().to_string(), name: oprops.get_summary_xsd_string().unwrap().to_string(),

View file

@ -1,14 +1,4 @@
use activitystreams::{actor::Actor, ext::Extension}; use super::*;
use actix_web::HttpRequest;
use failure::Error;
use http::request::Builder;
use http_signature_normalization::Config;
use log::debug;
use openssl::hash::MessageDigest;
use openssl::sign::{Signer, Verifier};
use openssl::{pkey::PKey, rsa::Rsa};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
lazy_static! { lazy_static! {
static ref HTTP_SIG_CONFIG: Config = Config::new(); static ref HTTP_SIG_CONFIG: Config = Config::new();

View file

@ -1,72 +1,59 @@
use crate::apub::signatures::PublicKey; use super::*;
use crate::apub::{create_apub_response, PersonExt};
use crate::db::user::{UserForm, User_};
use crate::{convert_datetime, naive_now};
use activitystreams::{
actor::{properties::ApActorProperties, Person},
context,
ext::Extensible,
object::properties::ObjectProperties,
};
use actix_web::body::Body;
use actix_web::web::Path;
use actix_web::HttpResponse;
use actix_web::{web, Result};
use diesel::r2d2::{ConnectionManager, Pool};
use diesel::PgConnection;
use failure::Error;
use serde::Deserialize;
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct UserQuery { pub struct UserQuery {
user_name: String, user_name: String,
} }
// Turn a Lemmy user into an ActivityPub person and return it as json. impl ToApub for User_ {
pub async fn get_apub_user( type Response = PersonExt;
info: Path<UserQuery>,
db: web::Data<Pool<ConnectionManager<PgConnection>>>,
) -> Result<HttpResponse<Body>, Error> {
let user = User_::find_by_email_or_username(&&db.get()?, &info.user_name)?;
// Turn a Lemmy Community into an ActivityPub group that can be sent out over the network.
fn to_apub(&self, _conn: &PgConnection) -> Result<PersonExt, Error> {
// TODO go through all these to_string and to_owned()
let mut person = Person::default(); let mut person = Person::default();
let oprops: &mut ObjectProperties = person.as_mut(); let oprops: &mut ObjectProperties = person.as_mut();
oprops oprops
.set_context_xsd_any_uri(context())? .set_context_xsd_any_uri(context())?
.set_id(user.actor_id.to_string())? .set_id(self.actor_id.to_string())?
.set_name_xsd_string(user.name.to_owned())? .set_name_xsd_string(self.name.to_owned())?
.set_published(convert_datetime(user.published))?; .set_published(convert_datetime(self.published))?;
if let Some(u) = user.updated { if let Some(u) = self.updated {
oprops.set_updated(convert_datetime(u))?; oprops.set_updated(convert_datetime(u))?;
} }
if let Some(i) = &user.preferred_username { if let Some(i) = &self.preferred_username {
oprops.set_name_xsd_string(i.to_owned())?; oprops.set_name_xsd_string(i.to_owned())?;
} }
let mut actor_props = ApActorProperties::default(); let mut actor_props = ApActorProperties::default();
actor_props actor_props
.set_inbox(format!("{}/inbox", &user.actor_id))? .set_inbox(self.get_inbox_url())?
.set_outbox(format!("{}/outbox", &user.actor_id))? .set_outbox(self.get_outbox_url())?
.set_following(format!("{}/following", &user.actor_id))? .set_followers(self.get_followers_url())?
.set_liked(format!("{}/liked", &user.actor_id))?; .set_following(self.get_following_url())?
.set_liked(self.get_liked_url())?;
let public_key = PublicKey { Ok(person.extend(actor_props).extend(self.get_public_key_ext()))
id: format!("{}#main-key", user.actor_id), }
owner: user.actor_id.to_owned(),
public_key_pem: user.public_key.unwrap(),
};
Ok(create_apub_response(
&person.extend(actor_props).extend(public_key.to_ext()),
))
} }
impl UserForm { impl ActorType for User_ {
fn actor_id(&self) -> String {
self.actor_id.to_owned()
}
fn public_key(&self) -> String {
self.public_key.to_owned().unwrap()
}
}
impl FromApub for UserForm {
type ApubType = PersonExt;
/// Parse an ActivityPub person received from another instance into a Lemmy user. /// Parse an ActivityPub person received from another instance into a Lemmy user.
pub fn from_person(person: &PersonExt) -> Result<Self, Error> { fn from_apub(person: &PersonExt, _conn: &PgConnection) -> Result<Self, Error> {
let oprops = &person.base.base.object_props; let oprops = &person.base.base.object_props;
let aprops = &person.base.extension; let aprops = &person.base.extension;
let public_key: &PublicKey = &person.extension.public_key; let public_key: &PublicKey = &person.extension.public_key;
@ -99,3 +86,13 @@ impl UserForm {
}) })
} }
} }
/// Return the user json over HTTP.
pub async fn get_apub_user_http(
info: Path<UserQuery>,
db: DbPoolParam,
) -> Result<HttpResponse<Body>, Error> {
let user = User_::find_by_email_or_username(&&db.get()?, &info.user_name)?;
let u = user.to_apub(&db.get().unwrap())?;
Ok(create_apub_response(&u))
}

View file

@ -1,16 +1,4 @@
use crate::apub::fetcher::{fetch_remote_community, fetch_remote_user}; use super::*;
use crate::apub::signatures::verify;
use crate::db::post::{Post, PostForm};
use crate::db::Crud;
use activitystreams::activity::{Accept, Create, Update};
use activitystreams::object::Page;
use actix_web::{web, HttpRequest, HttpResponse};
use diesel::r2d2::{ConnectionManager, Pool};
use diesel::PgConnection;
use failure::Error;
use log::debug;
use serde::Deserialize;
use url::Url;
#[serde(untagged)] #[serde(untagged)]
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
@ -25,21 +13,19 @@ pub async fn user_inbox(
request: HttpRequest, request: HttpRequest,
input: web::Json<UserAcceptedObjects>, input: web::Json<UserAcceptedObjects>,
path: web::Path<String>, path: web::Path<String>,
db: web::Data<Pool<ConnectionManager<PgConnection>>>, db: DbPoolParam,
_chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
// TODO: would be nice if we could do the signature check here, but we cant access the actor property // TODO: would be nice if we could do the signature check here, but we cant access the actor property
let input = input.into_inner(); let input = input.into_inner();
let conn = &db.get().unwrap(); let conn = &db.get().unwrap();
debug!( let username = path.into_inner();
"User {} received activity: {:?}", debug!("User {} received activity: {:?}", &username, &input);
&path.into_inner(),
&input
);
match input { match input {
UserAcceptedObjects::Create(c) => handle_create(&c, &request, conn), UserAcceptedObjects::Create(c) => handle_create(&c, &request, &username, &conn),
UserAcceptedObjects::Update(u) => handle_update(&u, &request, conn), UserAcceptedObjects::Update(u) => handle_update(&u, &request, &username, &conn),
UserAcceptedObjects::Accept(a) => handle_accept(&a, &request, conn), UserAcceptedObjects::Accept(a) => handle_accept(&a, &request, &username, &conn),
} }
} }
@ -47,15 +33,18 @@ pub async fn user_inbox(
fn handle_create( fn handle_create(
create: &Create, create: &Create,
request: &HttpRequest, request: &HttpRequest,
_username: &str,
conn: &PgConnection, conn: &PgConnection,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let community_uri = create // TODO before this even gets named, because we don't know what type of object it is, we need
// to parse this out
let user_uri = create
.create_props .create_props
.get_actor_xsd_any_uri() .get_actor_xsd_any_uri()
.unwrap() .unwrap()
.to_string(); .to_string();
// TODO: should do this in a generic way so we dont need to know if its a user or a community
let user = fetch_remote_user(&Url::parse(&community_uri)?, conn)?; let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?;
verify(request, &user.public_key.unwrap())?; verify(request, &user.public_key.unwrap())?;
let page = create let page = create
@ -65,7 +54,7 @@ fn handle_create(
.unwrap() .unwrap()
.to_owned() .to_owned()
.to_concrete::<Page>()?; .to_concrete::<Page>()?;
let post = PostForm::from_page(&page, conn)?; let post = PostForm::from_apub(&page, conn)?;
Post::create(conn, &post)?; Post::create(conn, &post)?;
// TODO: send the new post out via websocket // TODO: send the new post out via websocket
Ok(HttpResponse::Ok().finish()) Ok(HttpResponse::Ok().finish())
@ -75,14 +64,16 @@ fn handle_create(
fn handle_update( fn handle_update(
update: &Update, update: &Update,
request: &HttpRequest, request: &HttpRequest,
_username: &str,
conn: &PgConnection, conn: &PgConnection,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let community_uri = update let user_uri = update
.update_props .update_props
.get_actor_xsd_any_uri() .get_actor_xsd_any_uri()
.unwrap() .unwrap()
.to_string(); .to_string();
let user = fetch_remote_user(&Url::parse(&community_uri)?, conn)?;
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?;
verify(request, &user.public_key.unwrap())?; verify(request, &user.public_key.unwrap())?;
let page = update let page = update
@ -92,7 +83,7 @@ fn handle_update(
.unwrap() .unwrap()
.to_owned() .to_owned()
.to_concrete::<Page>()?; .to_concrete::<Page>()?;
let post = PostForm::from_page(&page, conn)?; let post = PostForm::from_apub(&page, conn)?;
let id = Post::read_from_apub_id(conn, &post.ap_id)?.id; let id = Post::read_from_apub_id(conn, &post.ap_id)?.id;
Post::update(conn, id, &post)?; Post::update(conn, id, &post)?;
// TODO: send the new post out via websocket // TODO: send the new post out via websocket
@ -103,6 +94,7 @@ fn handle_update(
fn handle_accept( fn handle_accept(
accept: &Accept, accept: &Accept,
request: &HttpRequest, request: &HttpRequest,
username: &str,
conn: &PgConnection, conn: &PgConnection,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let community_uri = accept let community_uri = accept
@ -110,9 +102,21 @@ fn handle_accept(
.get_actor_xsd_any_uri() .get_actor_xsd_any_uri()
.unwrap() .unwrap()
.to_string(); .to_string();
let community = fetch_remote_community(&Url::parse(&community_uri)?, conn)?;
let community = get_or_fetch_and_upsert_remote_community(&community_uri, conn)?;
verify(request, &community.public_key.unwrap())?; verify(request, &community.public_key.unwrap())?;
let user = User_::read_from_name(&conn, username)?;
// Now you need to add this to the community follower
let community_follower_form = CommunityFollowerForm {
community_id: community.id,
user_id: user.id,
};
// This will fail if they're already a follower
CommunityFollower::follow(&conn, &community_follower_form)?;
// TODO: make sure that we actually requested a follow // TODO: make sure that we actually requested a follow
// TODO: at this point, indicate to the user that they are following the community // TODO: at this point, indicate to the user that they are following the community
Ok(HttpResponse::Ok().finish()) Ok(HttpResponse::Ok().finish())

View file

@ -73,7 +73,7 @@ impl Crud<CommunityForm> for Community {
} }
impl Community { impl Community {
pub fn read_from_name(conn: &PgConnection, community_name: String) -> Result<Self, Error> { pub fn read_from_name(conn: &PgConnection, community_name: &str) -> Result<Self, Error> {
use crate::schema::community::dsl::*; use crate::schema::community::dsl::*;
community community
.filter(name.eq(community_name)) .filter(name.eq(community_name))

View file

@ -104,7 +104,7 @@ impl User_ {
.get_result::<Self>(conn) .get_result::<Self>(conn)
} }
pub fn read_from_name(conn: &PgConnection, from_user_name: String) -> Result<Self, Error> { pub fn read_from_name(conn: &PgConnection, from_user_name: &str) -> Result<Self, Error> {
user_.filter(name.eq(from_user_name)).first::<Self>(conn) user_.filter(name.eq(from_user_name)).first::<Self>(conn)
} }
@ -120,7 +120,7 @@ impl User_ {
.get_result::<Self>(conn) .get_result::<Self>(conn)
} }
pub fn read_from_apub_id(conn: &PgConnection, object_id: &str) -> Result<Self, Error> { pub fn read_from_actor_id(conn: &PgConnection, object_id: &str) -> Result<Self, Error> {
use crate::schema::user_::dsl::*; use crate::schema::user_::dsl::*;
user_.filter(actor_id.eq(object_id)).first::<Self>(conn) user_.filter(actor_id.eq(object_id)).first::<Self>(conn)
} }

View file

@ -21,11 +21,12 @@ pub fn config(cfg: &mut web::ServiceConfig) {
"/c/{community_name}/followers", "/c/{community_name}/followers",
web::get().to(get_apub_community_followers), web::get().to(get_apub_community_followers),
) )
.route( // TODO This is only useful for history which we aren't doing right now
"/c/{community_name}/outbox", // .route(
web::get().to(get_apub_community_outbox), // "/c/{community_name}/outbox",
) // web::get().to(get_apub_community_outbox),
.route("/u/{user_name}", web::get().to(get_apub_user)) // )
.route("/u/{user_name}", web::get().to(get_apub_user_http))
.route("/post/{post_id}", web::get().to(get_apub_post)), .route("/post/{post_id}", web::get().to(get_apub_post)),
) )
// Inboxes dont work with the header guard for some reason. // Inboxes dont work with the header guard for some reason.

View file

@ -27,7 +27,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
async fn get_all_feed( async fn get_all_feed(
info: web::Query<Params>, info: web::Query<Params>,
db: web::Data<Pool<ConnectionManager<PgConnection>>>, db: DbPoolParam,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let res = web::block(move || { let res = web::block(move || {
let conn = db.get()?; let conn = db.get()?;
@ -144,7 +144,7 @@ fn get_feed_community(
community_name: String, community_name: String,
) -> Result<ChannelBuilder, failure::Error> { ) -> Result<ChannelBuilder, failure::Error> {
let site_view = SiteView::read(&conn)?; let site_view = SiteView::read(&conn)?;
let community = Community::read_from_name(&conn, community_name)?; let community = Community::read_from_name(&conn, &community_name)?;
let community_url = community.get_url(); let community_url = community.get_url();
let posts = PostQueryBuilder::create(&conn) let posts = PostQueryBuilder::create(&conn)

View file

@ -21,7 +21,7 @@ async fn node_info_well_known() -> Result<HttpResponse<Body>, failure::Error> {
} }
async fn node_info( async fn node_info(
db: web::Data<Pool<ConnectionManager<PgConnection>>>, db: DbPoolParam,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let res = web::block(move || { let res = web::block(move || {
let conn = db.get()?; let conn = db.get()?;

View file

@ -31,7 +31,7 @@ lazy_static! {
/// https://radical.town/.well-known/webfinger?resource=acct:felix@radical.town /// https://radical.town/.well-known/webfinger?resource=acct:felix@radical.town
async fn get_webfinger_response( async fn get_webfinger_response(
info: Query<Params>, info: Query<Params>,
db: web::Data<Pool<ConnectionManager<PgConnection>>>, db: DbPoolParam,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let res = web::block(move || { let res = web::block(move || {
let conn = db.get()?; let conn = db.get()?;
@ -46,7 +46,7 @@ async fn get_webfinger_response(
}; };
// Make sure the requested community exists. // Make sure the requested community exists.
let community = match Community::read_from_name(&conn, community_name.to_string()) { let community = match Community::read_from_name(&conn, &community_name) {
Ok(o) => o, Ok(o) => o,
Err(_) => return Err(format_err!("not_found")), Err(_) => return Err(format_err!("not_found")),
}; };

View file

@ -32,7 +32,6 @@ struct WSSession {
/// Client must send ping at least once per 10 seconds (CLIENT_TIMEOUT), /// Client must send ping at least once per 10 seconds (CLIENT_TIMEOUT),
/// otherwise we drop connection. /// otherwise we drop connection.
hb: Instant, hb: Instant,
// db: Pool<ConnectionManager<PgConnection>>,
} }
impl Actor for WSSession { impl Actor for WSSession {

View file

@ -3,26 +3,28 @@ import fetch from 'node-fetch';
import { import {
LoginForm, LoginForm,
LoginResponse, LoginResponse,
GetPostsForm, PostForm,
GetPostsResponse, PostResponse,
CommentForm, SearchResponse,
CommentResponse,
ListingType,
SortType,
} from '../interfaces'; } from '../interfaces';
let baseUrl = 'https://test.lemmy.ml'; let lemmyAlphaUrl = 'http://localhost:8540';
let apiUrl = `${baseUrl}/api/v1`; let lemmyBetaUrl = 'http://localhost:8550';
let auth: string; let lemmyAlphaApiUrl = `${lemmyAlphaUrl}/api/v1`;
let lemmyBetaApiUrl = `${lemmyBetaUrl}/api/v1`;
let lemmyAlphaAuth: string;
// Workaround for tests being run before beforeAll() is finished
// https://github.com/facebook/jest/issues/9527#issuecomment-592406108
describe('main', () => {
beforeAll(async () => { beforeAll(async () => {
console.log('Logging in as test user.'); console.log('Logging in as lemmy_alpha');
let form: LoginForm = { let form: LoginForm = {
username_or_email: 'tester', username_or_email: 'lemmy_alpha',
password: 'tester', password: 'lemmy',
}; };
let res: LoginResponse = await fetch(`${apiUrl}/user/login`, { let res: LoginResponse = await fetch(`${lemmyAlphaApiUrl}/user/login`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -30,56 +32,38 @@ beforeAll(async () => {
body: wrapper(form), body: wrapper(form),
}).then(d => d.json()); }).then(d => d.json());
auth = res.jwt; lemmyAlphaAuth = res.jwt;
}); });
test('Get test user posts', async () => { test('Create test post on alpha and fetch it on beta', async () => {
let form: GetPostsForm = { let name = 'A jest test post';
type_: ListingType[ListingType.All], let postForm: PostForm = {
sort: SortType[SortType.TopAll], name,
auth, auth: lemmyAlphaAuth,
community_id: 2,
creator_id: 2,
nsfw: false,
}; };
let res: GetPostsResponse = await fetch( let createResponse: PostResponse = await fetch(`${lemmyAlphaApiUrl}/post`, {
`${apiUrl}/post/list?type_=${form.type_}&sort=${form.sort}&auth=${auth}`
).then(d => d.json());
// console.debug(res);
expect(res.posts[0].id).toBe(2);
});
test('Create test comment', async () => {
let content = 'A jest test comment';
let form: CommentForm = {
post_id: 2,
content,
auth,
};
let res: CommentResponse = await fetch(`${apiUrl}/comment`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: wrapper(form), body: wrapper(postForm),
}).then(d => d.json());
expect(createResponse.post.name).toBe(name);
let searchUrl = `${lemmyBetaApiUrl}/search?q=${createResponse.post.ap_id}&type_=All&sort=TopAll`;
let searchResponse: SearchResponse = await fetch(searchUrl, {
method: 'GET',
}).then(d => d.json()); }).then(d => d.json());
expect(res.comment.content).toBe(content); // TODO: check more fields
}); expect(searchResponse.posts[0].name).toBe(name);
test('adds 1 + 2 to equal 3', () => {
let sum = (a: number, b: number) => a + b;
expect(sum(1, 2)).toBe(3);
});
test(`Get ${baseUrl} nodeinfo href`, async () => {
let url = `${baseUrl}/.well-known/nodeinfo`;
let href = `${baseUrl}/nodeinfo/2.0.json`;
let res = await fetch(url).then(d => d.json());
expect(res.links.href).toBe(href);
}); });
function wrapper(form: any): string { function wrapper(form: any): string {
return JSON.stringify(form); return JSON.stringify(form);
} }
});