Lots of additions to federation.

- Added a shared inbox.
- Added federated comments, comment updates, and tests.
- Abstracted ap object sends into a common trait.
This commit is contained in:
Dessalines 2020-04-27 12:57:00 -04:00
parent 3ce0618362
commit 22abbebd41
18 changed files with 811 additions and 175 deletions

View file

@ -12,7 +12,8 @@ sudo docker-compose --file ../federation/docker-compose.yml --project-directory
pushd ../../ui pushd ../../ui
yarn yarn
echo "Waiting for Lemmy to start..." 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 while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8540/api/v1/site')" != "200" ]]; do sleep 1; done
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8550/api/v1/site')" != "200" ]]; do sleep 1; done
yarn api-test || true yarn api-test || true
popd popd

View file

@ -87,7 +87,8 @@ impl Perform for Oper<CreateComment> {
} }
// Check for a site ban // Check for a site ban
if UserView::read(&conn, user_id)?.banned { let user = User_::read(&conn, user_id)?;
if user.banned {
return Err(APIError::err("site_ban").into()); return Err(APIError::err("site_ban").into());
} }
@ -101,6 +102,7 @@ impl Perform for Oper<CreateComment> {
removed: None, removed: None,
deleted: None, deleted: None,
read: None, read: None,
published: None,
updated: None, updated: None,
ap_id: "changeme".into(), ap_id: "changeme".into(),
local: true, local: true,
@ -111,11 +113,13 @@ impl Perform for Oper<CreateComment> {
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()), Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
}; };
match Comment::update_ap_id(&conn, inserted_comment.id) { let updated_comment = match Comment::update_ap_id(&conn, inserted_comment.id) {
Ok(comment) => comment, Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()), Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
}; };
updated_comment.send_create(&user, &conn)?;
let mut recipient_ids = Vec::new(); let mut recipient_ids = Vec::new();
// Scan the comment for user mentions, add those rows // Scan the comment for user mentions, add those rows
@ -273,6 +277,8 @@ impl Perform for Oper<EditComment> {
let conn = pool.get()?; let conn = pool.get()?;
let user = User_::read(&conn, user_id)?;
let orig_comment = CommentView::read(&conn, data.edit_id, None)?; let orig_comment = CommentView::read(&conn, data.edit_id, None)?;
// You are allowed to mark the comment as read even if you're banned. // You are allowed to mark the comment as read even if you're banned.
@ -297,7 +303,7 @@ impl Perform for Oper<EditComment> {
} }
// Check for a site ban // Check for a site ban
if UserView::read(&conn, user_id)?.banned { if user.banned {
return Err(APIError::err("site_ban").into()); return Err(APIError::err("site_ban").into());
} }
} }
@ -314,6 +320,7 @@ impl Perform for Oper<EditComment> {
removed: data.removed.to_owned(), removed: data.removed.to_owned(),
deleted: data.deleted.to_owned(), deleted: data.deleted.to_owned(),
read: data.read.to_owned(), read: data.read.to_owned(),
published: None,
updated: if data.read.is_some() { updated: if data.read.is_some() {
orig_comment.updated orig_comment.updated
} else { } else {
@ -323,11 +330,13 @@ impl Perform for Oper<EditComment> {
local: read_comment.local, local: read_comment.local,
}; };
let _updated_comment = match Comment::update(&conn, data.edit_id, &comment_form) { let updated_comment = match Comment::update(&conn, data.edit_id, &comment_form) {
Ok(comment) => comment, Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()), Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
}; };
updated_comment.send_update(&user, &conn)?;
let mut recipient_ids = Vec::new(); let mut recipient_ids = Vec::new();
// Scan the comment for user mentions, add those rows // Scan the comment for user mentions, add those rows

View file

@ -23,19 +23,17 @@ use crate::{
}; };
use crate::apub::{ use crate::apub::{
activities::{send_post_create, send_post_update},
fetcher::search_by_apub_id, fetcher::search_by_apub_id,
signatures::generate_actor_keypair, signatures::generate_actor_keypair,
{make_apub_endpoint, ActorType, EndpointType}, {make_apub_endpoint, ActorType, ApubObjectType, EndpointType},
}; };
use crate::settings::Settings; use crate::settings::Settings;
use crate::websocket::UserOperation;
use crate::websocket::{ use crate::websocket::{
server::{ server::{
JoinCommunityRoom, JoinPostRoom, JoinUserRoom, SendAllMessage, SendComment, JoinCommunityRoom, JoinPostRoom, JoinUserRoom, SendAllMessage, SendComment,
SendCommunityRoomMessage, SendPost, SendUserRoomMessage, SendCommunityRoomMessage, SendPost, SendUserRoomMessage,
}, },
WebsocketInfo, UserOperation, WebsocketInfo,
}; };
use diesel::r2d2::{ConnectionManager, Pool}; use diesel::r2d2::{ConnectionManager, Pool};
use diesel::PgConnection; use diesel::PgConnection;

View file

@ -160,7 +160,7 @@ impl Perform for Oper<CreatePost> {
Err(_e) => return Err(APIError::err("couldnt_create_post").into()), Err(_e) => return Err(APIError::err("couldnt_create_post").into()),
}; };
send_post_create(&updated_post, &user, &conn)?; updated_post.send_create(&user, &conn)?;
// They like their own post by default // They like their own post by default
let like_form = PostLikeForm { let like_form = PostLikeForm {
@ -531,7 +531,7 @@ impl Perform for Oper<EditPost> {
ModStickyPost::create(&conn, &form)?; ModStickyPost::create(&conn, &form)?;
} }
send_post_update(&updated_post, &user, &conn)?; updated_post.send_update(&user, &conn)?;
let post_view = PostView::read(&conn, data.edit_id, Some(user_id))?; let post_view = PostView::read(&conn, data.edit_id, Some(user_id))?;

View file

@ -1,6 +1,6 @@
use super::*; use super::*;
fn populate_object_props( pub fn populate_object_props(
props: &mut ObjectProperties, props: &mut ObjectProperties,
addressed_to: &str, addressed_to: &str,
object_id: &str, object_id: &str,
@ -47,63 +47,3 @@ where
} }
Ok(()) Ok(())
} }
/// For a given community, returns the inboxes of all followers.
fn get_follower_inboxes(conn: &PgConnection, community: &Community) -> Result<Vec<String>, Error> {
Ok(
CommunityFollowerView::for_community(conn, community.id)?
.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 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();
populate_object_props(
&mut create.object_props,
&community.get_followers_url(),
&post.ap_id,
)?;
create
.create_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(page)?;
send_activity(
&create,
&creator.private_key.as_ref().unwrap(),
&creator.actor_id,
get_follower_inboxes(conn, &community)?,
)?;
Ok(())
}
/// Send out information about an edited post, to the followers of the community.
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();
populate_object_props(
&mut update.object_props,
&community.get_followers_url(),
&post.ap_id,
)?;
update
.update_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(page)?;
send_activity(
&update,
&creator.private_key.as_ref().unwrap(),
&creator.actor_id,
get_follower_inboxes(conn, &community)?,
)?;
Ok(())
}

139
server/src/apub/comment.rs Normal file
View file

@ -0,0 +1,139 @@
use super::*;
impl ToApub for Comment {
type Response = Note;
fn to_apub(&self, conn: &PgConnection) -> Result<Note, Error> {
let mut comment = Note::default();
let oprops: &mut ObjectProperties = comment.as_mut();
let creator = User_::read(&conn, self.creator_id)?;
let post = Post::read(&conn, self.post_id)?;
let community = Community::read(&conn, post.community_id)?;
// Add a vector containing some important info to the "in_reply_to" field
// [post_ap_id, Option(parent_comment_ap_id)]
let mut in_reply_to_vec = vec![post.ap_id];
if let Some(parent_id) = self.parent_id {
let parent_comment = Comment::read(&conn, parent_id)?;
in_reply_to_vec.push(parent_comment.ap_id);
}
oprops
// Not needed when the Post is embedded in a collection (like for community outbox)
.set_context_xsd_any_uri(context())?
.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_many_in_reply_to_xsd_any_uris(in_reply_to_vec)?
.set_content_xsd_string(self.content.to_owned())?
.set_attributed_to_xsd_any_uri(creator.actor_id)?;
if let Some(u) = self.updated {
oprops.set_updated(convert_datetime(u))?;
}
Ok(comment)
}
}
impl FromApub for CommentForm {
type ApubType = Note;
/// Parse an ActivityPub note received from another instance into a Lemmy comment
fn from_apub(note: &Note, conn: &PgConnection) -> Result<CommentForm, Error> {
let oprops = &note.object_props;
let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string();
let creator = get_or_fetch_and_upsert_remote_user(&creator_actor_id, &conn)?;
let mut in_reply_tos = oprops.get_many_in_reply_to_xsd_any_uris().unwrap();
let post_ap_id = in_reply_tos.next().unwrap().to_string();
// The 2nd item, if it exists, is the parent comment apub_id
let parent_id: Option<i32> = match in_reply_tos.next() {
Some(parent_comment_uri) => {
let parent_comment_uri_str = &parent_comment_uri.to_string();
let parent_comment = Comment::read_from_apub_id(&conn, &parent_comment_uri_str)?;
Some(parent_comment.id)
}
None => None,
};
let post = Post::read_from_apub_id(&conn, &post_ap_id)?;
Ok(CommentForm {
creator_id: creator.id,
post_id: post.id,
parent_id,
content: oprops
.get_content_xsd_string()
.map(|c| c.to_string())
.unwrap(),
removed: None,
read: None,
published: oprops
.get_published()
.map(|u| u.as_ref().to_owned().naive_local()),
updated: oprops
.get_updated()
.map(|u| u.as_ref().to_owned().naive_local()),
deleted: None,
ap_id: oprops.get_id().unwrap().to_string(),
local: false,
})
}
}
impl ApubObjectType for Comment {
/// Send out information about a newly created comment, to the followers of the community.
fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let note = self.to_apub(conn)?;
let post = Post::read(&conn, self.post_id)?;
let community = Community::read(conn, post.community_id)?;
let mut create = Create::new();
populate_object_props(
&mut create.object_props,
&community.get_followers_url(),
&self.ap_id,
)?;
create
.create_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(note)?;
send_activity(
&create,
&creator.private_key.as_ref().unwrap(),
&creator.actor_id,
community.get_follower_inboxes(&conn)?,
)?;
Ok(())
}
/// Send out information about an edited post, to the followers of the community.
fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let note = self.to_apub(&conn)?;
let post = Post::read(&conn, self.post_id)?;
let community = Community::read(&conn, post.community_id)?;
let mut update = Update::new();
populate_object_props(
&mut update.object_props,
&community.get_followers_url(),
&self.ap_id,
)?;
update
.update_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(note)?;
send_activity(
&update,
&creator.private_key.as_ref().unwrap(),
&creator.actor_id,
community.get_follower_inboxes(&conn)?,
)?;
Ok(())
}
}

View file

@ -89,6 +89,32 @@ impl ActorType for Community {
)?; )?;
Ok(()) Ok(())
} }
/// For a given community, returns the inboxes of all followers.
fn get_follower_inboxes(&self, conn: &PgConnection) -> Result<Vec<String>, Error> {
debug!("got here.");
Ok(
CommunityFollowerView::for_community(conn, self.id)?
.into_iter()
// TODO eventually this will have to use the inbox or shared_inbox column, meaning that view
// will have to change
.map(|c| {
// If the user is local, but the community isn't, get the community shared inbox
// and vice versa
if c.user_local && !c.community_local {
get_shared_inbox(&c.community_actor_id)
} else if !c.user_local && c.community_local {
get_shared_inbox(&c.user_actor_id)
} else {
"".to_string()
}
})
.filter(|s| !s.is_empty())
.unique()
.collect(),
)
}
} }
impl FromApub for CommunityForm { impl FromApub for CommunityForm {

View file

@ -1,4 +1,5 @@
pub mod activities; pub mod activities;
pub mod comment;
pub mod community; pub mod community;
pub mod community_inbox; pub mod community_inbox;
pub mod fetcher; pub mod fetcher;
@ -15,7 +16,11 @@ use activitystreams::{
context, context,
endpoint::EndpointProperties, endpoint::EndpointProperties,
ext::{Ext, Extensible, Extension}, ext::{Ext, Extensible, Extension},
object::{properties::ObjectProperties, Page}, object::{
kind::{NoteType, PageType},
properties::ObjectProperties,
Note, Page,
},
public, BaseBox, public, BaseBox,
}; };
use actix_web::body::Body; use actix_web::body::Body;
@ -38,7 +43,11 @@ use std::collections::BTreeMap;
use std::time::Duration; use std::time::Duration;
use url::Url; use url::Url;
use crate::api::comment::CommentResponse;
use crate::api::post::PostResponse;
use crate::api::site::SearchResponse; use crate::api::site::SearchResponse;
use crate::db::comment::{Comment, CommentForm};
use crate::db::comment_view::CommentView;
use crate::db::community::{Community, CommunityFollower, CommunityFollowerForm, CommunityForm}; use crate::db::community::{Community, CommunityFollower, CommunityFollowerForm, CommunityForm};
use crate::db::community_view::{CommunityFollowerView, CommunityView}; use crate::db::community_view::{CommunityFollowerView, CommunityView};
use crate::db::post::{Post, PostForm}; use crate::db::post::{Post, PostForm};
@ -48,9 +57,13 @@ use crate::db::user_view::UserView;
use crate::db::{Crud, Followable, SearchType}; use crate::db::{Crud, Followable, SearchType};
use crate::routes::nodeinfo::{NodeInfo, NodeInfoWellKnown}; use crate::routes::nodeinfo::{NodeInfo, NodeInfoWellKnown};
use crate::routes::{ChatServerParam, DbPoolParam}; use crate::routes::{ChatServerParam, DbPoolParam};
use crate::websocket::{
server::{SendComment, SendPost},
UserOperation,
};
use crate::{convert_datetime, naive_now, Settings}; use crate::{convert_datetime, naive_now, Settings};
use activities::send_activity; use activities::{populate_object_props, send_activity};
use fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user}; use fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user};
use signatures::verify; use signatures::verify;
use signatures::{sign, PublicKey, PublicKeyExtension}; use signatures::{sign, PublicKey, PublicKeyExtension};
@ -142,6 +155,25 @@ pub trait FromApub {
Self: Sized; Self: Sized;
} }
pub trait ApubObjectType {
fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
}
pub fn get_shared_inbox(actor_id: &str) -> String {
let url = Url::parse(actor_id).unwrap();
format!(
"{}://{}{}/inbox",
&url.scheme(),
&url.host_str().unwrap(),
if let Some(port) = url.port() {
format!(":{}", port)
} else {
"".to_string()
},
)
}
pub trait ActorType { pub trait ActorType {
fn actor_id(&self) -> String; fn actor_id(&self) -> String;
@ -159,24 +191,20 @@ pub trait ActorType {
Ok(()) Ok(())
} }
// TODO default because there is no user following yet.
#[allow(unused_variables)]
/// For a given community, returns the inboxes of all followers.
fn get_follower_inboxes(&self, conn: &PgConnection) -> Result<Vec<String>, Error> {
Ok(vec![])
}
// TODO move these to the db rows // TODO move these to the db rows
fn get_inbox_url(&self) -> String { fn get_inbox_url(&self) -> String {
format!("{}/inbox", &self.actor_id()) format!("{}/inbox", &self.actor_id())
} }
fn get_shared_inbox_url(&self) -> String { fn get_shared_inbox_url(&self) -> String {
let url = Url::parse(&self.actor_id()).unwrap(); get_shared_inbox(&self.actor_id())
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 { fn get_outbox_url(&self) -> String {

View file

@ -92,3 +92,51 @@ impl FromApub for PostForm {
}) })
} }
} }
impl ApubObjectType for Post {
/// Send out information about a newly created post, to the followers of the community.
fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let page = self.to_apub(conn)?;
let community = Community::read(conn, self.community_id)?;
let mut create = Create::new();
populate_object_props(
&mut create.object_props,
&community.get_followers_url(),
&self.ap_id,
)?;
create
.create_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(page)?;
send_activity(
&create,
&creator.private_key.as_ref().unwrap(),
&creator.actor_id,
community.get_follower_inboxes(&conn)?,
)?;
Ok(())
}
/// Send out information about an edited post, to the followers of the community.
fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let page = self.to_apub(conn)?;
let community = Community::read(conn, self.community_id)?;
let mut update = Update::new();
populate_object_props(
&mut update.object_props,
&community.get_followers_url(),
&self.ap_id,
)?;
update
.update_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(page)?;
send_activity(
&update,
&creator.private_key.as_ref().unwrap(),
&creator.actor_id,
community.get_follower_inboxes(&conn)?,
)?;
Ok(())
}
}

View file

@ -1 +1,241 @@
// use super::*; use super::*;
#[serde(untagged)]
#[derive(Serialize, Deserialize, Debug)]
pub enum SharedAcceptedObjects {
Create(Create),
Update(Update),
}
/// Handler for all incoming activities to user inboxes.
pub async fn shared_inbox(
request: HttpRequest,
input: web::Json<SharedAcceptedObjects>,
db: DbPoolParam,
chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> {
// 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 conn = &db.get().unwrap();
let json = serde_json::to_string(&input)?;
debug!("Shared inbox received activity: {:?}", &json);
match input {
SharedAcceptedObjects::Create(c) => handle_create(&c, &request, &conn, chat_server),
SharedAcceptedObjects::Update(u) => handle_update(&u, &request, &conn, chat_server),
}
}
/// Handle create activities and insert them in the database.
fn handle_create(
create: &Create,
request: &HttpRequest,
conn: &PgConnection,
chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> {
let base_box = create.create_props.get_object_base_box().unwrap();
if base_box.is_kind(PageType) {
let page = create
.create_props
.get_object_base_box()
.to_owned()
.unwrap()
.to_owned()
.to_concrete::<Page>()?;
receive_create_post(&create, &page, &request, &conn, chat_server)?;
} else if base_box.is_kind(NoteType) {
let note = create
.create_props
.get_object_base_box()
.to_owned()
.unwrap()
.to_owned()
.to_concrete::<Note>()?;
receive_create_comment(&create, &note, &request, &conn, chat_server)?;
} else {
return Err(format_err!("Unknown base box type"));
}
Ok(HttpResponse::Ok().finish())
}
fn receive_create_post(
create: &Create,
page: &Page,
request: &HttpRequest,
conn: &PgConnection,
chat_server: ChatServerParam,
) -> Result<(), Error> {
let user_uri = create
.create_props
.get_actor_xsd_any_uri()
.unwrap()
.to_string();
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?;
verify(request, &user.public_key.unwrap())?;
let post = PostForm::from_apub(&page, &conn)?;
let inserted_post = Post::create(conn, &post)?;
// Refetch the view
let post_view = PostView::read(&conn, inserted_post.id, None)?;
let res = PostResponse { post: post_view };
chat_server.do_send(SendPost {
op: UserOperation::CreatePost,
post: res,
my_id: None,
});
Ok(())
}
fn receive_create_comment(
create: &Create,
note: &Note,
request: &HttpRequest,
conn: &PgConnection,
chat_server: ChatServerParam,
) -> Result<(), Error> {
let user_uri = create
.create_props
.get_actor_xsd_any_uri()
.unwrap()
.to_string();
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?;
verify(request, &user.public_key.unwrap())?;
let comment = CommentForm::from_apub(&note, &conn)?;
let inserted_comment = Comment::create(conn, &comment)?;
// Refetch the view
let comment_view = CommentView::read(&conn, inserted_comment.id, None)?;
// TODO get those recipient actor ids from somewhere
let recipient_ids = vec![];
let res = CommentResponse {
comment: comment_view,
recipient_ids,
};
chat_server.do_send(SendComment {
op: UserOperation::CreateComment,
comment: res,
my_id: None,
});
Ok(())
}
/// Handle create activities and insert them in the database.
fn handle_update(
update: &Update,
request: &HttpRequest,
conn: &PgConnection,
chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> {
let base_box = update.update_props.get_object_base_box().unwrap();
if base_box.is_kind(PageType) {
let page = update
.update_props
.get_object_base_box()
.to_owned()
.unwrap()
.to_owned()
.to_concrete::<Page>()?;
receive_update_post(&update, &page, &request, &conn, chat_server)?;
} else if base_box.is_kind(NoteType) {
let note = update
.update_props
.get_object_base_box()
.to_owned()
.unwrap()
.to_owned()
.to_concrete::<Note>()?;
receive_update_comment(&update, &note, &request, &conn, chat_server)?;
} else {
return Err(format_err!("Unknown base box type"));
}
Ok(HttpResponse::Ok().finish())
}
fn receive_update_post(
update: &Update,
page: &Page,
request: &HttpRequest,
conn: &PgConnection,
chat_server: ChatServerParam,
) -> Result<(), Error> {
let user_uri = update
.update_props
.get_actor_xsd_any_uri()
.unwrap()
.to_string();
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?;
verify(request, &user.public_key.unwrap())?;
let post = PostForm::from_apub(&page, conn)?;
let post_id = Post::read_from_apub_id(conn, &post.ap_id)?.id;
Post::update(conn, post_id, &post)?;
// Refetch the view
let post_view = PostView::read(&conn, post_id, None)?;
let res = PostResponse { post: post_view };
chat_server.do_send(SendPost {
op: UserOperation::EditPost,
post: res,
my_id: None,
});
Ok(())
}
fn receive_update_comment(
update: &Update,
note: &Note,
request: &HttpRequest,
conn: &PgConnection,
chat_server: ChatServerParam,
) -> Result<(), Error> {
let user_uri = update
.update_props
.get_actor_xsd_any_uri()
.unwrap()
.to_string();
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?;
verify(request, &user.public_key.unwrap())?;
let comment = CommentForm::from_apub(&note, &conn)?;
let comment_id = Comment::read_from_apub_id(conn, &comment.ap_id)?.id;
Comment::update(conn, comment_id, &comment)?;
// Refetch the view
let comment_view = CommentView::read(&conn, comment_id, None)?;
// TODO get those recipient actor ids from somewhere
let recipient_ids = vec![];
let res = CommentResponse {
comment: comment_view,
recipient_ids,
};
chat_server.do_send(SendComment {
op: UserOperation::EditComment,
comment: res,
my_id: None,
});
Ok(())
}

View file

@ -3,8 +3,6 @@ use super::*;
#[serde(untagged)] #[serde(untagged)]
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub enum UserAcceptedObjects { pub enum UserAcceptedObjects {
Create(Create),
Update(Update),
Accept(Accept), Accept(Accept),
} }
@ -23,73 +21,10 @@ pub async fn user_inbox(
debug!("User {} received activity: {:?}", &username, &input); debug!("User {} received activity: {:?}", &username, &input);
match input { match input {
UserAcceptedObjects::Create(c) => handle_create(&c, &request, &username, &conn),
UserAcceptedObjects::Update(u) => handle_update(&u, &request, &username, &conn),
UserAcceptedObjects::Accept(a) => handle_accept(&a, &request, &username, &conn), UserAcceptedObjects::Accept(a) => handle_accept(&a, &request, &username, &conn),
} }
} }
/// Handle create activities and insert them in the database.
fn handle_create(
create: &Create,
request: &HttpRequest,
_username: &str,
conn: &PgConnection,
) -> Result<HttpResponse, Error> {
// 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
.get_actor_xsd_any_uri()
.unwrap()
.to_string();
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?;
verify(request, &user.public_key.unwrap())?;
let page = create
.create_props
.get_object_base_box()
.to_owned()
.unwrap()
.to_owned()
.to_concrete::<Page>()?;
let post = PostForm::from_apub(&page, conn)?;
Post::create(conn, &post)?;
// TODO: send the new post out via websocket
Ok(HttpResponse::Ok().finish())
}
/// Handle update activities and insert them in the database.
fn handle_update(
update: &Update,
request: &HttpRequest,
_username: &str,
conn: &PgConnection,
) -> Result<HttpResponse, Error> {
let user_uri = update
.update_props
.get_actor_xsd_any_uri()
.unwrap()
.to_string();
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?;
verify(request, &user.public_key.unwrap())?;
let page = update
.update_props
.get_object_base_box()
.to_owned()
.unwrap()
.to_owned()
.to_concrete::<Page>()?;
let post = PostForm::from_apub(&page, conn)?;
let id = Post::read_from_apub_id(conn, &post.ap_id)?.id;
Post::update(conn, id, &post)?;
// TODO: send the new post out via websocket
Ok(HttpResponse::Ok().finish())
}
/// Handle accepted follows. /// Handle accepted follows.
fn handle_accept( fn handle_accept(
accept: &Accept, accept: &Accept,

View file

@ -38,6 +38,7 @@ pub struct CommentForm {
pub content: String, pub content: String,
pub removed: Option<bool>, pub removed: Option<bool>,
pub read: Option<bool>, pub read: Option<bool>,
pub published: Option<chrono::NaiveDateTime>,
pub updated: Option<chrono::NaiveDateTime>, pub updated: Option<chrono::NaiveDateTime>,
pub deleted: Option<bool>, pub deleted: Option<bool>,
pub ap_id: String, pub ap_id: String,
@ -84,6 +85,11 @@ impl Comment {
.get_result::<Self>(conn) .get_result::<Self>(conn)
} }
pub fn read_from_apub_id(conn: &PgConnection, object_id: &str) -> Result<Self, Error> {
use crate::schema::comment::dsl::*;
comment.filter(ap_id.eq(object_id)).first::<Self>(conn)
}
pub fn mark_as_read(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> { pub fn mark_as_read(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
use crate::schema::comment::dsl::*; use crate::schema::comment::dsl::*;
@ -283,6 +289,7 @@ mod tests {
deleted: None, deleted: None,
read: None, read: None,
parent_id: None, parent_id: None,
published: None,
updated: None, updated: None,
ap_id: "changeme".into(), ap_id: "changeme".into(),
local: true, local: true,
@ -313,6 +320,7 @@ mod tests {
removed: None, removed: None,
deleted: None, deleted: None,
read: None, read: None,
published: None,
updated: None, updated: None,
ap_id: "changeme".into(), ap_id: "changeme".into(),
local: true, local: true,

View file

@ -540,6 +540,7 @@ mod tests {
removed: None, removed: None,
deleted: None, deleted: None,
read: None, read: None,
published: None,
updated: None, updated: None,
ap_id: "changeme".into(), ap_id: "changeme".into(),
local: true, local: true,

View file

@ -541,6 +541,7 @@ mod tests {
deleted: None, deleted: None,
read: None, read: None,
parent_id: None, parent_id: None,
published: None,
updated: None, updated: None,
ap_id: "changeme".into(), ap_id: "changeme".into(),
local: true, local: true,

View file

@ -167,6 +167,7 @@ mod tests {
deleted: None, deleted: None,
read: None, read: None,
parent_id: None, parent_id: None,
published: None,
updated: None, updated: None,
ap_id: "changeme".into(), ap_id: "changeme".into(),
local: true, local: true,

View file

@ -2,6 +2,7 @@ use super::*;
use crate::apub::community::*; use crate::apub::community::*;
use crate::apub::community_inbox::community_inbox; use crate::apub::community_inbox::community_inbox;
use crate::apub::post::get_apub_post; use crate::apub::post::get_apub_post;
use crate::apub::shared_inbox::shared_inbox;
use crate::apub::user::*; use crate::apub::user::*;
use crate::apub::user_inbox::user_inbox; use crate::apub::user_inbox::user_inbox;
use crate::apub::APUB_JSON_CONTENT_TYPE; use crate::apub::APUB_JSON_CONTENT_TYPE;
@ -31,6 +32,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
) )
// Inboxes dont work with the header guard for some reason. // Inboxes dont work with the header guard for some reason.
.route("/c/{community_name}/inbox", web::post().to(community_inbox)) .route("/c/{community_name}/inbox", web::post().to(community_inbox))
.route("/u/{user_name}/inbox", web::post().to(user_inbox)); .route("/u/{user_name}/inbox", web::post().to(user_inbox))
.route("/inbox", web::post().to(shared_inbox));
} }
} }

1
ui/jest.config.js vendored
View file

@ -1,6 +1,7 @@
module.exports = { module.exports = {
preset: 'ts-jest', preset: 'ts-jest',
testEnvironment: 'node', testEnvironment: 'node',
testTimeout: 30000,
globals: { globals: {
'ts-jest': { 'ts-jest': {
diagnostics: false, diagnostics: false,

View file

@ -6,6 +6,13 @@ import {
PostForm, PostForm,
PostResponse, PostResponse,
SearchResponse, SearchResponse,
FollowCommunityForm,
CommunityResponse,
GetFollowedCommunitiesResponse,
GetPostForm,
GetPostResponse,
CommentForm,
CommentResponse,
} from '../interfaces'; } from '../interfaces';
let lemmyAlphaUrl = 'http://localhost:8540'; let lemmyAlphaUrl = 'http://localhost:8540';
@ -13,6 +20,7 @@ let lemmyBetaUrl = 'http://localhost:8550';
let lemmyAlphaApiUrl = `${lemmyAlphaUrl}/api/v1`; let lemmyAlphaApiUrl = `${lemmyAlphaUrl}/api/v1`;
let lemmyBetaApiUrl = `${lemmyBetaUrl}/api/v1`; let lemmyBetaApiUrl = `${lemmyBetaUrl}/api/v1`;
let lemmyAlphaAuth: string; let lemmyAlphaAuth: string;
let lemmyBetaAuth: string;
// Workaround for tests being run before beforeAll() is finished // Workaround for tests being run before beforeAll() is finished
// https://github.com/facebook/jest/issues/9527#issuecomment-592406108 // https://github.com/facebook/jest/issues/9527#issuecomment-592406108
@ -33,8 +41,25 @@ describe('main', () => {
}).then(d => d.json()); }).then(d => d.json());
lemmyAlphaAuth = res.jwt; lemmyAlphaAuth = res.jwt;
console.log('Logging in as lemmy_beta');
let formB = {
username_or_email: 'lemmy_beta',
password: 'lemmy',
};
let resB: LoginResponse = await fetch(`${lemmyBetaApiUrl}/user/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: wrapper(formB),
}).then(d => d.json());
lemmyBetaAuth = resB.jwt;
}); });
describe('beta_fetch', () => {
test('Create test post on alpha and fetch it on beta', async () => { test('Create test post on alpha and fetch it on beta', async () => {
let name = 'A jest test post'; let name = 'A jest test post';
let postForm: PostForm = { let postForm: PostForm = {
@ -45,13 +70,16 @@ describe('main', () => {
nsfw: false, nsfw: false,
}; };
let createResponse: PostResponse = await fetch(`${lemmyAlphaApiUrl}/post`, { let createResponse: PostResponse = await fetch(
`${lemmyAlphaApiUrl}/post`,
{
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: wrapper(postForm), body: wrapper(postForm),
}).then(d => d.json()); }
).then(d => d.json());
expect(createResponse.post.name).toBe(name); expect(createResponse.post.name).toBe(name);
let searchUrl = `${lemmyBetaApiUrl}/search?q=${createResponse.post.ap_id}&type_=All&sort=TopAll`; let searchUrl = `${lemmyBetaApiUrl}/search?q=${createResponse.post.ap_id}&type_=All&sort=TopAll`;
@ -62,8 +90,238 @@ describe('main', () => {
// TODO: check more fields // TODO: check more fields
expect(searchResponse.posts[0].name).toBe(name); expect(searchResponse.posts[0].name).toBe(name);
}); });
});
function wrapper(form: any): string { describe('follow_accept', () => {
return JSON.stringify(form); test('/u/lemmy_alpha follows and accepts lemmy_beta/c/main', async () => {
// Make sure lemmy_beta/c/main is cached on lemmy_alpha
let searchUrl = `${lemmyAlphaApiUrl}/search?q=http://lemmy_beta:8550/c/main&type_=All&sort=TopAll`;
let searchResponse: SearchResponse = await fetch(searchUrl, {
method: 'GET',
}).then(d => d.json());
expect(searchResponse.communities[0].name).toBe('main');
// TODO
// Unfortunately the search is correctly
let followForm: FollowCommunityForm = {
community_id: searchResponse.communities[0].id,
follow: true,
auth: lemmyAlphaAuth,
};
let followRes: CommunityResponse = await fetch(
`${lemmyAlphaApiUrl}/community/follow`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: wrapper(followForm),
} }
).then(d => d.json());
// Make sure the follow response went through
expect(followRes.community.local).toBe(false);
expect(followRes.community.name).toBe('main');
// Check that you are subscribed to it locally
let followedCommunitiesUrl = `${lemmyAlphaApiUrl}/user/followed_communities?&auth=${lemmyAlphaAuth}`;
let followedCommunitiesRes: GetFollowedCommunitiesResponse = await fetch(
followedCommunitiesUrl,
{
method: 'GET',
}
).then(d => d.json());
expect(followedCommunitiesRes.communities[1].community_local).toBe(false);
});
});
describe('create test post', () => {
test('/u/lemmy_alpha creates a post on /c/lemmy_beta/main, its on both instances', async () => {
let name = 'A jest test federated post';
let postForm: PostForm = {
name,
auth: lemmyAlphaAuth,
community_id: 3,
creator_id: 2,
nsfw: false,
};
let createResponse: PostResponse = await fetch(
`${lemmyAlphaApiUrl}/post`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: wrapper(postForm),
}
).then(d => d.json());
expect(createResponse.post.name).toBe(name);
expect(createResponse.post.community_local).toBe(false);
expect(createResponse.post.creator_local).toBe(true);
let getPostUrl = `${lemmyBetaApiUrl}/post?id=2`;
let getPostRes: GetPostResponse = await fetch(getPostUrl, {
method: 'GET',
}).then(d => d.json());
expect(getPostRes.post.name).toBe(name);
expect(getPostRes.post.community_local).toBe(true);
expect(getPostRes.post.creator_local).toBe(false);
});
});
describe('update test post', () => {
test('/u/lemmy_alpha updates a post on /c/lemmy_beta/main, the update is on both', async () => {
let name = 'A jest test federated post, updated';
let postForm: PostForm = {
name,
edit_id: 2,
auth: lemmyAlphaAuth,
community_id: 3,
creator_id: 2,
nsfw: false,
};
let updateResponse: PostResponse = await fetch(
`${lemmyAlphaApiUrl}/post`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: wrapper(postForm),
}
).then(d => d.json());
expect(updateResponse.post.name).toBe(name);
expect(updateResponse.post.community_local).toBe(false);
expect(updateResponse.post.creator_local).toBe(true);
let getPostUrl = `${lemmyBetaApiUrl}/post?id=2`;
let getPostRes: GetPostResponse = await fetch(getPostUrl, {
method: 'GET',
}).then(d => d.json());
expect(getPostRes.post.name).toBe(name);
expect(getPostRes.post.community_local).toBe(true);
expect(getPostRes.post.creator_local).toBe(false);
});
});
describe('create test comment', () => {
test('/u/lemmy_alpha creates a comment on /c/lemmy_beta/main, its on both instances', async () => {
let content = 'A jest test federated comment';
let commentForm: CommentForm = {
content,
post_id: 2,
auth: lemmyAlphaAuth,
};
let createResponse: CommentResponse = await fetch(
`${lemmyAlphaApiUrl}/comment`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: wrapper(commentForm),
}
).then(d => d.json());
expect(createResponse.comment.content).toBe(content);
expect(createResponse.comment.community_local).toBe(false);
expect(createResponse.comment.creator_local).toBe(true);
let getPostUrl = `${lemmyBetaApiUrl}/post?id=2`;
let getPostRes: GetPostResponse = await fetch(getPostUrl, {
method: 'GET',
}).then(d => d.json());
expect(getPostRes.comments[0].content).toBe(content);
expect(getPostRes.comments[0].community_local).toBe(true);
expect(getPostRes.comments[0].creator_local).toBe(false);
// Now do beta replying to that comment, as a child comment
let contentBeta = 'A child federated comment from beta';
let commentFormBeta: CommentForm = {
content: contentBeta,
post_id: getPostRes.post.id,
parent_id: getPostRes.comments[0].id,
auth: lemmyBetaAuth,
};
let createResponseBeta: CommentResponse = await fetch(
`${lemmyBetaApiUrl}/comment`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: wrapper(commentFormBeta),
}
).then(d => d.json());
expect(createResponseBeta.comment.content).toBe(contentBeta);
expect(createResponseBeta.comment.community_local).toBe(true);
expect(createResponseBeta.comment.creator_local).toBe(true);
expect(createResponseBeta.comment.parent_id).toBe(1);
// Make sure lemmy alpha sees that new child comment from beta
let getPostUrlAlpha = `${lemmyAlphaApiUrl}/post?id=2`;
let getPostResAlpha: GetPostResponse = await fetch(getPostUrlAlpha, {
method: 'GET',
}).then(d => d.json());
// The newest show up first
expect(getPostResAlpha.comments[0].content).toBe(contentBeta);
expect(getPostResAlpha.comments[0].community_local).toBe(false);
expect(getPostResAlpha.comments[0].creator_local).toBe(false);
});
});
describe('update test comment', () => {
test('/u/lemmy_alpha updates a comment on /c/lemmy_beta/main, its on both instances', async () => {
let content = 'A jest test federated comment update';
let commentForm: CommentForm = {
content,
post_id: 2,
edit_id: 1,
auth: lemmyAlphaAuth,
creator_id: 2,
};
let updateResponse: CommentResponse = await fetch(
`${lemmyAlphaApiUrl}/comment`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: wrapper(commentForm),
}
).then(d => d.json());
expect(updateResponse.comment.content).toBe(content);
expect(updateResponse.comment.community_local).toBe(false);
expect(updateResponse.comment.creator_local).toBe(true);
let getPostUrl = `${lemmyBetaApiUrl}/post?id=2`;
let getPostRes: GetPostResponse = await fetch(getPostUrl, {
method: 'GET',
}).then(d => d.json());
expect(getPostRes.comments[1].content).toBe(content);
expect(getPostRes.comments[1].community_local).toBe(true);
expect(getPostRes.comments[1].creator_local).toBe(false);
});
});
}); });
function wrapper(form: any): string {
return JSON.stringify(form);
}