Browse Source

Lots of additions to federation.

- Added a shared inbox.
- Added federated comments, comment updates, and tests.
- Abstracted ap object sends into a common trait.
shared_inbox_1
Dessalines 2 years ago
parent
commit
22abbebd41
  1. 3
      docker/federation-test/run-tests.sh
  2. 17
      server/src/api/comment.rs
  3. 6
      server/src/api/mod.rs
  4. 4
      server/src/api/post.rs
  5. 62
      server/src/apub/activities.rs
  6. 139
      server/src/apub/comment.rs
  7. 26
      server/src/apub/community.rs
  8. 56
      server/src/apub/mod.rs
  9. 48
      server/src/apub/post.rs
  10. 242
      server/src/apub/shared_inbox.rs
  11. 65
      server/src/apub/user_inbox.rs
  12. 8
      server/src/db/comment.rs
  13. 1
      server/src/db/comment_view.rs
  14. 1
      server/src/db/moderator.rs
  15. 1
      server/src/db/user_mention.rs
  16. 4
      server/src/routes/federation.rs
  17. 1
      ui/jest.config.js
  18. 300
      ui/src/api_tests/api.spec.ts

3
docker/federation-test/run-tests.sh

@ -12,7 +12,8 @@ sudo docker-compose --file ../federation/docker-compose.yml --project-directory
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
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
popd

17
server/src/api/comment.rs

@ -87,7 +87,8 @@ impl Perform for Oper<CreateComment> {
}
// 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());
}
@ -101,6 +102,7 @@ impl Perform for Oper<CreateComment> {
removed: None,
deleted: None,
read: None,
published: None,
updated: None,
ap_id: "changeme".into(),
local: true,
@ -111,11 +113,13 @@ impl Perform for Oper<CreateComment> {
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,
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
};
updated_comment.send_create(&user, &conn)?;
let mut recipient_ids = Vec::new();
// Scan the comment for user mentions, add those rows
@ -273,6 +277,8 @@ impl Perform for Oper<EditComment> {
let conn = pool.get()?;
let user = User_::read(&conn, user_id)?;
let orig_comment = CommentView::read(&conn, data.edit_id, None)?;
// 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
if UserView::read(&conn, user_id)?.banned {
if user.banned {
return Err(APIError::err("site_ban").into());
}
}
@ -314,6 +320,7 @@ impl Perform for Oper<EditComment> {
removed: data.removed.to_owned(),
deleted: data.deleted.to_owned(),
read: data.read.to_owned(),
published: None,
updated: if data.read.is_some() {
orig_comment.updated
} else {
@ -323,11 +330,13 @@ impl Perform for Oper<EditComment> {
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,
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
};
updated_comment.send_update(&user, &conn)?;
let mut recipient_ids = Vec::new();
// Scan the comment for user mentions, add those rows

6
server/src/api/mod.rs

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

4
server/src/api/post.rs

@ -160,7 +160,7 @@ impl Perform for Oper<CreatePost> {
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
let like_form = PostLikeForm {
@ -531,7 +531,7 @@ impl Perform for Oper<EditPost> {
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))?;

62
server/src/apub/activities.rs

@ -1,6 +1,6 @@
use super::*;
fn populate_object_props(
pub fn populate_object_props(
props: &mut ObjectProperties,
addressed_to: &str,
object_id: &str,
@ -47,63 +47,3 @@ where
}
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

@ -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(())
}
}

26
server/src/apub/community.rs

@ -89,6 +89,32 @@ impl ActorType for Community {
)?;
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 {

56
server/src/apub/mod.rs

@ -1,4 +1,5 @@
pub mod activities;
pub mod comment;
pub mod community;
pub mod community_inbox;
pub mod fetcher;
@ -15,7 +16,11 @@ use activitystreams::{
context,
endpoint::EndpointProperties,
ext::{Ext, Extensible, Extension},
object::{properties::ObjectProperties, Page},
object::{
kind::{NoteType, PageType},
properties::ObjectProperties,
Note, Page,
},
public, BaseBox,
};
use actix_web::body::Body;
@ -38,7 +43,11 @@ use std::collections::BTreeMap;
use std::time::Duration;
use url::Url;
use crate::api::comment::CommentResponse;
use crate::api::post::PostResponse;
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_view::{CommunityFollowerView, CommunityView};
use crate::db::post::{Post, PostForm};
@ -48,9 +57,13 @@ 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::websocket::{
server::{SendComment, SendPost},
UserOperation,
};
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 signatures::verify;
use signatures::{sign, PublicKey, PublicKeyExtension};
@ -142,6 +155,25 @@ pub trait FromApub {
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 {
fn actor_id(&self) -> String;
@ -159,24 +191,20 @@ pub trait ActorType {
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
fn get_inbox_url(&self) -> String {
format!("{}/inbox", &self.actor_id())
}
fn get_shared_inbox_url(&self) -> String {
let url = Url::parse(&self.actor_id()).unwrap();
let url_str = format!(
"{}://{}{}/inbox",
&url.scheme(),
&url.host_str().unwrap(),
if let Some(port) = url.port() {
format!(":{}", port)
} else {
"".to_string()
},
);
format!("{}/inbox", &url_str)
get_shared_inbox(&self.actor_id())
}
fn get_outbox_url(&self) -> String {

48
server/src/apub/post.rs

@ -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(())
}
}

242
server/src/apub/shared_inbox.rs

@ -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(())
}

65
server/src/apub/user_inbox.rs

@ -3,8 +3,6 @@ use super::*;
#[serde(untagged)]
#[derive(Deserialize, Debug)]
pub enum UserAcceptedObjects {
Create(Create),
Update(Update),
Accept(Accept),
}
@ -23,73 +21,10 @@ pub async fn user_inbox(
debug!("User {} received activity: {:?}", &username, &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),
}
}
/// 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.
fn handle_accept(
accept: &Accept,

8
server/src/db/comment.rs

@ -38,6 +38,7 @@ pub struct CommentForm {
pub content: String,
pub removed: Option<bool>,
pub read: Option<bool>,
pub published: Option<chrono::NaiveDateTime>,
pub updated: Option<chrono::NaiveDateTime>,
pub deleted: Option<bool>,
pub ap_id: String,
@ -84,6 +85,11 @@ impl Comment {
.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> {
use crate::schema::comment::dsl::*;
@ -283,6 +289,7 @@ mod tests {
deleted: None,
read: None,
parent_id: None,
published: None,
updated: None,
ap_id: "changeme".into(),
local: true,
@ -313,6 +320,7 @@ mod tests {
removed: None,
deleted: None,
read: None,
published: None,
updated: None,
ap_id: "changeme".into(),
local: true,

1
server/src/db/comment_view.rs

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

1
server/src/db/moderator.rs

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

1
server/src/db/user_mention.rs

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

4
server/src/routes/federation.rs

@ -2,6 +2,7 @@ use super::*;
use crate::apub::community::*;
use crate::apub::community_inbox::community_inbox;
use crate::apub::post::get_apub_post;
use crate::apub::shared_inbox::shared_inbox;
use crate::apub::user::*;
use crate::apub::user_inbox::user_inbox;
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.
.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

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

300
ui/src/api_tests/api.spec.ts

@ -6,6 +6,13 @@ import {
PostForm,
PostResponse,
SearchResponse,
FollowCommunityForm,
CommunityResponse,
GetFollowedCommunitiesResponse,
GetPostForm,
GetPostResponse,
CommentForm,
CommentResponse,
} from '../interfaces';
let lemmyAlphaUrl = 'http://localhost:8540';
@ -13,6 +20,7 @@ let lemmyBetaUrl = 'http://localhost:8550';
let lemmyAlphaApiUrl = `${lemmyAlphaUrl}/api/v1`;
let lemmyBetaApiUrl = `${lemmyBetaUrl}/api/v1`;
let lemmyAlphaAuth: string;
let lemmyBetaAuth: string;
// Workaround for tests being run before beforeAll() is finished
// https://github.com/facebook/jest/issues/9527#issuecomment-592406108
@ -33,37 +41,287 @@ describe('main', () => {
}).then(d => d.json());
lemmyAlphaAuth = res.jwt;
});
test('Create test post on alpha and fetch it on beta', async () => {
let name = 'A jest test post';
let postForm: PostForm = {
name,
auth: lemmyAlphaAuth,
community_id: 2,
creator_id: 2,
nsfw: false,
console.log('Logging in as lemmy_beta');
let formB = {
username_or_email: 'lemmy_beta',
password: 'lemmy',
};
let createResponse: PostResponse = await fetch(`${lemmyAlphaApiUrl}/post`, {
let resB: LoginResponse = await fetch(`${lemmyBetaApiUrl}/user/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: wrapper(postForm),
body: wrapper(formB),
}).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());
lemmyBetaAuth = resB.jwt;
});
describe('beta_fetch', () => {
test('Create test post on alpha and fetch it on beta', async () => {
let name = 'A jest test post';
let postForm: PostForm = {
name,
auth: lemmyAlphaAuth,
community_id: 2,
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);
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());
// TODO: check more fields
expect(searchResponse.posts[0].name).toBe(name);
});
});
describe('follow_accept', () => {
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());
// TODO: check more fields
expect(searchResponse.posts[0].name).toBe(name);
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);
});
});
function wrapper(form: any): string {
return JSON.stringify(form);
}
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);
}

Loading…
Cancel
Save