]> Untitled Git - lemmy.git/commitdiff
Lots of additions to federation.
authorDessalines <tyhou13@gmx.com>
Mon, 27 Apr 2020 16:57:00 +0000 (12:57 -0400)
committerDessalines <tyhou13@gmx.com>
Mon, 27 Apr 2020 16:57:00 +0000 (12:57 -0400)
- Added a shared inbox.
- Added federated comments, comment updates, and tests.
- Abstracted ap object sends into a common trait.

18 files changed:
docker/federation-test/run-tests.sh
server/src/api/comment.rs
server/src/api/mod.rs
server/src/api/post.rs
server/src/apub/activities.rs
server/src/apub/comment.rs [new file with mode: 0644]
server/src/apub/community.rs
server/src/apub/mod.rs
server/src/apub/post.rs
server/src/apub/shared_inbox.rs
server/src/apub/user_inbox.rs
server/src/db/comment.rs
server/src/db/comment_view.rs
server/src/db/moderator.rs
server/src/db/user_mention.rs
server/src/routes/federation.rs
ui/jest.config.js
ui/src/api_tests/api.spec.ts

index 4206f060f5f5ff47ccdaf86e813fa49a19cbc195..9d8a7e58fb1e2b16c7786cc60242773357176de2 100755 (executable)
@@ -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
 
index eb67d8f2043c88600b2539b092e1be8889985dfd..fddb42abe236b80bf6c256166b537f3976e17262 100644 (file)
@@ -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
index 0595f2a40d4b90b4b3b9cc804360d2e43a3a45c4..70ff2bfec195910744720e897d6f62e6b38e2c15 100644 (file)
@@ -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;
index 89f1dd1d359237a3ba717193fdf9904c4627e51f..5be227d8d364991915c83e6dc892314d519ddf57 100644 (file)
@@ -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))?;
 
index cb98e973475bb8b039ce953d437e816f81ff1111..517fd24811b73cc07f714fe32747001020f37834 100644 (file)
@@ -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(())
-}
diff --git a/server/src/apub/comment.rs b/server/src/apub/comment.rs
new file mode 100644 (file)
index 0000000..3b7c0df
--- /dev/null
@@ -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(())
+  }
+}
index bc984b250c7dcf6ae633d0609ceb6801f362391b..d66bbc0196dc05e9774e0ecefd148251bd18bd1e 100644 (file)
@@ -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 {
index 5c5852991102f76d0b5b12271a8b4db041942aa6..a861156f542ae18fc3ff94c9ab171da87e7a83ea 100644 (file)
@@ -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 {
index 51ba861ef81aaf726a81de279bafcb113e35cf5d..0a054431e99f24b4b0b0c3c5b2fe027dc6e7126f 100644 (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(())
+  }
+}
index 35ba3908a4e29778dbdaf97fb341319ce04db769..f0cfc9900671bd5c11e2dfad679fdca276f7a698 100644 (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(())
+}
index 251a221c6fa29696ef413334634bd019a5789ecc..7c00b5bb857775a144fb9b632217c2b35888087f 100644 (file)
@@ -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,
index 0b8a2e206e173f99695a87a804aaf8072af9aca9..59c2ccd298e3944f4be857ed74423d935ac5c930 100644 (file)
@@ -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,
index f0b97cb51e664f5ffba6d3d6219418d4f90057ea..a94aa15792fa8135ab915b3ca1c00af560b32398 100644 (file)
@@ -540,6 +540,7 @@ mod tests {
       removed: None,
       deleted: None,
       read: None,
+      published: None,
       updated: None,
       ap_id: "changeme".into(),
       local: true,
index d56e3914ca7e0e00bcf61d48b4b9d5a82beab2a5..a040c0ca889a63b8cff5384c283cab9e91b423ff 100644 (file)
@@ -541,6 +541,7 @@ mod tests {
       deleted: None,
       read: None,
       parent_id: None,
+      published: None,
       updated: None,
       ap_id: "changeme".into(),
       local: true,
index aea1e2285acd4bc9c6acf348cd12525451d2a02a..d8305d3958b5adef758e93280736e95445c16084 100644 (file)
@@ -167,6 +167,7 @@ mod tests {
       deleted: None,
       read: None,
       parent_id: None,
+      published: None,
       updated: None,
       ap_id: "changeme".into(),
       local: true,
index bab88ca39bc79209012b322a71fb978c35085ba9..c1cb7408a2e92737f2f6794f13729127e29582d3 100644 (file)
@@ -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));
   }
 }
index ebe914b195cd46dbf4f32c959a2e7a8763f3da27..abe695be928d939b9b36cbc38e47609ac28cafc1 100644 (file)
@@ -1,6 +1,7 @@
 module.exports = {
   preset: 'ts-jest',
   testEnvironment: 'node',
+  testTimeout: 30000,
   globals: {
     'ts-jest': {
       diagnostics: false,
index 07e12ecfe60270d15b45ab37c598ea21cd444d3d..49fd08782a825d680267d45d6eb7077240097531 100644 (file)
@@ -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);
+}