- Added a shared inbox.
- Added federated comments, comment updates, and tests.
- Abstracted ap object sends into a common trait.
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
}
// 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());
}
removed: None,
deleted: None,
read: None,
+ published: None,
updated: None,
ap_id: "changeme".into(),
local: true,
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
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.
}
// Check for a site ban
- if UserView::read(&conn, user_id)?.banned {
+ if user.banned {
return Err(APIError::err("site_ban").into());
}
}
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 {
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
};
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;
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 {
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))?;
use super::*;
-fn populate_object_props(
+pub fn populate_object_props(
props: &mut ObjectProperties,
addressed_to: &str,
object_id: &str,
}
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(())
-}
--- /dev/null
+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 = ¬e.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(())
+ }
+}
)?;
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 {
pub mod activities;
+pub mod comment;
pub mod community;
pub mod community_inbox;
pub mod fetcher;
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;
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};
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};
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;
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 {
})
}
}
+
+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(())
+ }
+}
-// 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, ¬e, &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(¬e, &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, ¬e, &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(¬e, &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(())
+}
#[serde(untagged)]
#[derive(Deserialize, Debug)]
pub enum UserAcceptedObjects {
- Create(Create),
- Update(Update),
Accept(Accept),
}
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,
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,
.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::*;
deleted: None,
read: None,
parent_id: None,
+ published: None,
updated: None,
ap_id: "changeme".into(),
local: true,
removed: None,
deleted: None,
read: None,
+ published: None,
updated: None,
ap_id: "changeme".into(),
local: true,
removed: None,
deleted: None,
read: None,
+ published: None,
updated: None,
ap_id: "changeme".into(),
local: true,
deleted: None,
read: None,
parent_id: None,
+ published: None,
updated: None,
ap_id: "changeme".into(),
local: true,
deleted: None,
read: None,
parent_id: None,
+ published: None,
updated: None,
ap_id: "changeme".into(),
local: true,
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;
)
// 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));
}
}
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
+ testTimeout: 30000,
globals: {
'ts-jest': {
diagnostics: false,
PostForm,
PostResponse,
SearchResponse,
+ FollowCommunityForm,
+ CommunityResponse,
+ GetFollowedCommunitiesResponse,
+ GetPostForm,
+ GetPostResponse,
+ CommentForm,
+ CommentResponse,
} from '../interfaces';
let lemmyAlphaUrl = 'http://localhost:8540';
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
}).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);
+}