mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-11-15 08:54:01 +00:00
Lots of additions to federation.
- Added a shared inbox. - Added federated comments, comment updates, and tests. - Abstracted ap object sends into a common trait.
This commit is contained in:
parent
3ce0618362
commit
22abbebd41
18 changed files with 811 additions and 175 deletions
3
docker/federation-test/run-tests.sh
vendored
3
docker/federation-test/run-tests.sh
vendored
|
@ -12,7 +12,8 @@ sudo docker-compose --file ../federation/docker-compose.yml --project-directory
|
||||||
pushd ../../ui
|
pushd ../../ui
|
||||||
yarn
|
yarn
|
||||||
echo "Waiting for Lemmy to start..."
|
echo "Waiting for Lemmy to start..."
|
||||||
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8540/api/v1/site')" != "200" ]]; do sleep 5; done
|
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8540/api/v1/site')" != "200" ]]; do sleep 1; done
|
||||||
|
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8550/api/v1/site')" != "200" ]]; do sleep 1; done
|
||||||
yarn api-test || true
|
yarn api-test || true
|
||||||
popd
|
popd
|
||||||
|
|
||||||
|
|
|
@ -87,7 +87,8 @@ impl Perform for Oper<CreateComment> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
if UserView::read(&conn, user_id)?.banned {
|
let user = User_::read(&conn, user_id)?;
|
||||||
|
if user.banned {
|
||||||
return Err(APIError::err("site_ban").into());
|
return Err(APIError::err("site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,6 +102,7 @@ impl Perform for Oper<CreateComment> {
|
||||||
removed: None,
|
removed: None,
|
||||||
deleted: None,
|
deleted: None,
|
||||||
read: None,
|
read: None,
|
||||||
|
published: None,
|
||||||
updated: None,
|
updated: None,
|
||||||
ap_id: "changeme".into(),
|
ap_id: "changeme".into(),
|
||||||
local: true,
|
local: true,
|
||||||
|
@ -111,11 +113,13 @@ impl Perform for Oper<CreateComment> {
|
||||||
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
|
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
match Comment::update_ap_id(&conn, inserted_comment.id) {
|
let updated_comment = match Comment::update_ap_id(&conn, inserted_comment.id) {
|
||||||
Ok(comment) => comment,
|
Ok(comment) => comment,
|
||||||
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
|
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
updated_comment.send_create(&user, &conn)?;
|
||||||
|
|
||||||
let mut recipient_ids = Vec::new();
|
let mut recipient_ids = Vec::new();
|
||||||
|
|
||||||
// Scan the comment for user mentions, add those rows
|
// Scan the comment for user mentions, add those rows
|
||||||
|
@ -273,6 +277,8 @@ impl Perform for Oper<EditComment> {
|
||||||
|
|
||||||
let conn = pool.get()?;
|
let conn = pool.get()?;
|
||||||
|
|
||||||
|
let user = User_::read(&conn, user_id)?;
|
||||||
|
|
||||||
let orig_comment = CommentView::read(&conn, data.edit_id, None)?;
|
let orig_comment = CommentView::read(&conn, data.edit_id, None)?;
|
||||||
|
|
||||||
// You are allowed to mark the comment as read even if you're banned.
|
// You are allowed to mark the comment as read even if you're banned.
|
||||||
|
@ -297,7 +303,7 @@ impl Perform for Oper<EditComment> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
if UserView::read(&conn, user_id)?.banned {
|
if user.banned {
|
||||||
return Err(APIError::err("site_ban").into());
|
return Err(APIError::err("site_ban").into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -314,6 +320,7 @@ impl Perform for Oper<EditComment> {
|
||||||
removed: data.removed.to_owned(),
|
removed: data.removed.to_owned(),
|
||||||
deleted: data.deleted.to_owned(),
|
deleted: data.deleted.to_owned(),
|
||||||
read: data.read.to_owned(),
|
read: data.read.to_owned(),
|
||||||
|
published: None,
|
||||||
updated: if data.read.is_some() {
|
updated: if data.read.is_some() {
|
||||||
orig_comment.updated
|
orig_comment.updated
|
||||||
} else {
|
} else {
|
||||||
|
@ -323,11 +330,13 @@ impl Perform for Oper<EditComment> {
|
||||||
local: read_comment.local,
|
local: read_comment.local,
|
||||||
};
|
};
|
||||||
|
|
||||||
let _updated_comment = match Comment::update(&conn, data.edit_id, &comment_form) {
|
let updated_comment = match Comment::update(&conn, data.edit_id, &comment_form) {
|
||||||
Ok(comment) => comment,
|
Ok(comment) => comment,
|
||||||
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
|
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
updated_comment.send_update(&user, &conn)?;
|
||||||
|
|
||||||
let mut recipient_ids = Vec::new();
|
let mut recipient_ids = Vec::new();
|
||||||
|
|
||||||
// Scan the comment for user mentions, add those rows
|
// Scan the comment for user mentions, add those rows
|
||||||
|
|
|
@ -23,19 +23,17 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::apub::{
|
use crate::apub::{
|
||||||
activities::{send_post_create, send_post_update},
|
|
||||||
fetcher::search_by_apub_id,
|
fetcher::search_by_apub_id,
|
||||||
signatures::generate_actor_keypair,
|
signatures::generate_actor_keypair,
|
||||||
{make_apub_endpoint, ActorType, EndpointType},
|
{make_apub_endpoint, ActorType, ApubObjectType, EndpointType},
|
||||||
};
|
};
|
||||||
use crate::settings::Settings;
|
use crate::settings::Settings;
|
||||||
use crate::websocket::UserOperation;
|
|
||||||
use crate::websocket::{
|
use crate::websocket::{
|
||||||
server::{
|
server::{
|
||||||
JoinCommunityRoom, JoinPostRoom, JoinUserRoom, SendAllMessage, SendComment,
|
JoinCommunityRoom, JoinPostRoom, JoinUserRoom, SendAllMessage, SendComment,
|
||||||
SendCommunityRoomMessage, SendPost, SendUserRoomMessage,
|
SendCommunityRoomMessage, SendPost, SendUserRoomMessage,
|
||||||
},
|
},
|
||||||
WebsocketInfo,
|
UserOperation, WebsocketInfo,
|
||||||
};
|
};
|
||||||
use diesel::r2d2::{ConnectionManager, Pool};
|
use diesel::r2d2::{ConnectionManager, Pool};
|
||||||
use diesel::PgConnection;
|
use diesel::PgConnection;
|
||||||
|
|
|
@ -160,7 +160,7 @@ impl Perform for Oper<CreatePost> {
|
||||||
Err(_e) => return Err(APIError::err("couldnt_create_post").into()),
|
Err(_e) => return Err(APIError::err("couldnt_create_post").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
send_post_create(&updated_post, &user, &conn)?;
|
updated_post.send_create(&user, &conn)?;
|
||||||
|
|
||||||
// They like their own post by default
|
// They like their own post by default
|
||||||
let like_form = PostLikeForm {
|
let like_form = PostLikeForm {
|
||||||
|
@ -531,7 +531,7 @@ impl Perform for Oper<EditPost> {
|
||||||
ModStickyPost::create(&conn, &form)?;
|
ModStickyPost::create(&conn, &form)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
send_post_update(&updated_post, &user, &conn)?;
|
updated_post.send_update(&user, &conn)?;
|
||||||
|
|
||||||
let post_view = PostView::read(&conn, data.edit_id, Some(user_id))?;
|
let post_view = PostView::read(&conn, data.edit_id, Some(user_id))?;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn populate_object_props(
|
pub fn populate_object_props(
|
||||||
props: &mut ObjectProperties,
|
props: &mut ObjectProperties,
|
||||||
addressed_to: &str,
|
addressed_to: &str,
|
||||||
object_id: &str,
|
object_id: &str,
|
||||||
|
@ -47,63 +47,3 @@ where
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// For a given community, returns the inboxes of all followers.
|
|
||||||
fn get_follower_inboxes(conn: &PgConnection, community: &Community) -> Result<Vec<String>, Error> {
|
|
||||||
Ok(
|
|
||||||
CommunityFollowerView::for_community(conn, community.id)?
|
|
||||||
.into_iter()
|
|
||||||
.filter(|c| !c.user_local)
|
|
||||||
// TODO eventually this will have to use the inbox or shared_inbox column, meaning that view
|
|
||||||
// will have to change
|
|
||||||
.map(|c| format!("{}/inbox", c.user_actor_id.to_owned()))
|
|
||||||
.unique()
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send out information about a newly created post, to the followers of the community.
|
|
||||||
pub fn send_post_create(post: &Post, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
|
|
||||||
let page = post.to_apub(conn)?;
|
|
||||||
let community = Community::read(conn, post.community_id)?;
|
|
||||||
let mut create = Create::new();
|
|
||||||
populate_object_props(
|
|
||||||
&mut create.object_props,
|
|
||||||
&community.get_followers_url(),
|
|
||||||
&post.ap_id,
|
|
||||||
)?;
|
|
||||||
create
|
|
||||||
.create_props
|
|
||||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(page)?;
|
|
||||||
send_activity(
|
|
||||||
&create,
|
|
||||||
&creator.private_key.as_ref().unwrap(),
|
|
||||||
&creator.actor_id,
|
|
||||||
get_follower_inboxes(conn, &community)?,
|
|
||||||
)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send out information about an edited post, to the followers of the community.
|
|
||||||
pub fn send_post_update(post: &Post, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
|
|
||||||
let page = post.to_apub(conn)?;
|
|
||||||
let community = Community::read(conn, post.community_id)?;
|
|
||||||
let mut update = Update::new();
|
|
||||||
populate_object_props(
|
|
||||||
&mut update.object_props,
|
|
||||||
&community.get_followers_url(),
|
|
||||||
&post.ap_id,
|
|
||||||
)?;
|
|
||||||
update
|
|
||||||
.update_props
|
|
||||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(page)?;
|
|
||||||
send_activity(
|
|
||||||
&update,
|
|
||||||
&creator.private_key.as_ref().unwrap(),
|
|
||||||
&creator.actor_id,
|
|
||||||
get_follower_inboxes(conn, &community)?,
|
|
||||||
)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
139
server/src/apub/comment.rs
Normal file
139
server/src/apub/comment.rs
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl ToApub for Comment {
|
||||||
|
type Response = Note;
|
||||||
|
|
||||||
|
fn to_apub(&self, conn: &PgConnection) -> Result<Note, Error> {
|
||||||
|
let mut comment = Note::default();
|
||||||
|
let oprops: &mut ObjectProperties = comment.as_mut();
|
||||||
|
let creator = User_::read(&conn, self.creator_id)?;
|
||||||
|
let post = Post::read(&conn, self.post_id)?;
|
||||||
|
let community = Community::read(&conn, post.community_id)?;
|
||||||
|
|
||||||
|
// Add a vector containing some important info to the "in_reply_to" field
|
||||||
|
// [post_ap_id, Option(parent_comment_ap_id)]
|
||||||
|
let mut in_reply_to_vec = vec![post.ap_id];
|
||||||
|
|
||||||
|
if let Some(parent_id) = self.parent_id {
|
||||||
|
let parent_comment = Comment::read(&conn, parent_id)?;
|
||||||
|
in_reply_to_vec.push(parent_comment.ap_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
oprops
|
||||||
|
// Not needed when the Post is embedded in a collection (like for community outbox)
|
||||||
|
.set_context_xsd_any_uri(context())?
|
||||||
|
.set_id(self.ap_id.to_owned())?
|
||||||
|
// Use summary field to be consistent with mastodon content warning.
|
||||||
|
// https://mastodon.xyz/@Louisa/103987265222901387.json
|
||||||
|
// .set_summary_xsd_string(self.name.to_owned())?
|
||||||
|
.set_published(convert_datetime(self.published))?
|
||||||
|
.set_to_xsd_any_uri(community.actor_id)?
|
||||||
|
.set_many_in_reply_to_xsd_any_uris(in_reply_to_vec)?
|
||||||
|
.set_content_xsd_string(self.content.to_owned())?
|
||||||
|
.set_attributed_to_xsd_any_uri(creator.actor_id)?;
|
||||||
|
|
||||||
|
if let Some(u) = self.updated {
|
||||||
|
oprops.set_updated(convert_datetime(u))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(comment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromApub for CommentForm {
|
||||||
|
type ApubType = Note;
|
||||||
|
|
||||||
|
/// Parse an ActivityPub note received from another instance into a Lemmy comment
|
||||||
|
fn from_apub(note: &Note, conn: &PgConnection) -> Result<CommentForm, Error> {
|
||||||
|
let oprops = ¬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(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -89,6 +89,32 @@ impl ActorType for Community {
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// For a given community, returns the inboxes of all followers.
|
||||||
|
fn get_follower_inboxes(&self, conn: &PgConnection) -> Result<Vec<String>, Error> {
|
||||||
|
debug!("got here.");
|
||||||
|
|
||||||
|
Ok(
|
||||||
|
CommunityFollowerView::for_community(conn, self.id)?
|
||||||
|
.into_iter()
|
||||||
|
// TODO eventually this will have to use the inbox or shared_inbox column, meaning that view
|
||||||
|
// will have to change
|
||||||
|
.map(|c| {
|
||||||
|
// If the user is local, but the community isn't, get the community shared inbox
|
||||||
|
// and vice versa
|
||||||
|
if c.user_local && !c.community_local {
|
||||||
|
get_shared_inbox(&c.community_actor_id)
|
||||||
|
} else if !c.user_local && c.community_local {
|
||||||
|
get_shared_inbox(&c.user_actor_id)
|
||||||
|
} else {
|
||||||
|
"".to_string()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.unique()
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromApub for CommunityForm {
|
impl FromApub for CommunityForm {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
pub mod activities;
|
pub mod activities;
|
||||||
|
pub mod comment;
|
||||||
pub mod community;
|
pub mod community;
|
||||||
pub mod community_inbox;
|
pub mod community_inbox;
|
||||||
pub mod fetcher;
|
pub mod fetcher;
|
||||||
|
@ -15,7 +16,11 @@ use activitystreams::{
|
||||||
context,
|
context,
|
||||||
endpoint::EndpointProperties,
|
endpoint::EndpointProperties,
|
||||||
ext::{Ext, Extensible, Extension},
|
ext::{Ext, Extensible, Extension},
|
||||||
object::{properties::ObjectProperties, Page},
|
object::{
|
||||||
|
kind::{NoteType, PageType},
|
||||||
|
properties::ObjectProperties,
|
||||||
|
Note, Page,
|
||||||
|
},
|
||||||
public, BaseBox,
|
public, BaseBox,
|
||||||
};
|
};
|
||||||
use actix_web::body::Body;
|
use actix_web::body::Body;
|
||||||
|
@ -38,7 +43,11 @@ use std::collections::BTreeMap;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::api::comment::CommentResponse;
|
||||||
|
use crate::api::post::PostResponse;
|
||||||
use crate::api::site::SearchResponse;
|
use crate::api::site::SearchResponse;
|
||||||
|
use crate::db::comment::{Comment, CommentForm};
|
||||||
|
use crate::db::comment_view::CommentView;
|
||||||
use crate::db::community::{Community, CommunityFollower, CommunityFollowerForm, CommunityForm};
|
use crate::db::community::{Community, CommunityFollower, CommunityFollowerForm, CommunityForm};
|
||||||
use crate::db::community_view::{CommunityFollowerView, CommunityView};
|
use crate::db::community_view::{CommunityFollowerView, CommunityView};
|
||||||
use crate::db::post::{Post, PostForm};
|
use crate::db::post::{Post, PostForm};
|
||||||
|
@ -48,9 +57,13 @@ use crate::db::user_view::UserView;
|
||||||
use crate::db::{Crud, Followable, SearchType};
|
use crate::db::{Crud, Followable, SearchType};
|
||||||
use crate::routes::nodeinfo::{NodeInfo, NodeInfoWellKnown};
|
use crate::routes::nodeinfo::{NodeInfo, NodeInfoWellKnown};
|
||||||
use crate::routes::{ChatServerParam, DbPoolParam};
|
use crate::routes::{ChatServerParam, DbPoolParam};
|
||||||
|
use crate::websocket::{
|
||||||
|
server::{SendComment, SendPost},
|
||||||
|
UserOperation,
|
||||||
|
};
|
||||||
use crate::{convert_datetime, naive_now, Settings};
|
use crate::{convert_datetime, naive_now, Settings};
|
||||||
|
|
||||||
use activities::send_activity;
|
use activities::{populate_object_props, send_activity};
|
||||||
use fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user};
|
use fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user};
|
||||||
use signatures::verify;
|
use signatures::verify;
|
||||||
use signatures::{sign, PublicKey, PublicKeyExtension};
|
use signatures::{sign, PublicKey, PublicKeyExtension};
|
||||||
|
@ -142,6 +155,25 @@ pub trait FromApub {
|
||||||
Self: Sized;
|
Self: Sized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait ApubObjectType {
|
||||||
|
fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
|
||||||
|
fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_shared_inbox(actor_id: &str) -> String {
|
||||||
|
let url = Url::parse(actor_id).unwrap();
|
||||||
|
format!(
|
||||||
|
"{}://{}{}/inbox",
|
||||||
|
&url.scheme(),
|
||||||
|
&url.host_str().unwrap(),
|
||||||
|
if let Some(port) = url.port() {
|
||||||
|
format!(":{}", port)
|
||||||
|
} else {
|
||||||
|
"".to_string()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub trait ActorType {
|
pub trait ActorType {
|
||||||
fn actor_id(&self) -> String;
|
fn actor_id(&self) -> String;
|
||||||
|
|
||||||
|
@ -159,24 +191,20 @@ pub trait ActorType {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO default because there is no user following yet.
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
/// For a given community, returns the inboxes of all followers.
|
||||||
|
fn get_follower_inboxes(&self, conn: &PgConnection) -> Result<Vec<String>, Error> {
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
|
||||||
// TODO move these to the db rows
|
// TODO move these to the db rows
|
||||||
fn get_inbox_url(&self) -> String {
|
fn get_inbox_url(&self) -> String {
|
||||||
format!("{}/inbox", &self.actor_id())
|
format!("{}/inbox", &self.actor_id())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_shared_inbox_url(&self) -> String {
|
fn get_shared_inbox_url(&self) -> String {
|
||||||
let url = Url::parse(&self.actor_id()).unwrap();
|
get_shared_inbox(&self.actor_id())
|
||||||
let url_str = format!(
|
|
||||||
"{}://{}{}/inbox",
|
|
||||||
&url.scheme(),
|
|
||||||
&url.host_str().unwrap(),
|
|
||||||
if let Some(port) = url.port() {
|
|
||||||
format!(":{}", port)
|
|
||||||
} else {
|
|
||||||
"".to_string()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
format!("{}/inbox", &url_str)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_outbox_url(&self) -> String {
|
fn get_outbox_url(&self) -> String {
|
||||||
|
|
|
@ -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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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, ¬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(())
|
||||||
|
}
|
||||||
|
|
|
@ -3,8 +3,6 @@ use super::*;
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
pub enum UserAcceptedObjects {
|
pub enum UserAcceptedObjects {
|
||||||
Create(Create),
|
|
||||||
Update(Update),
|
|
||||||
Accept(Accept),
|
Accept(Accept),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,73 +21,10 @@ pub async fn user_inbox(
|
||||||
debug!("User {} received activity: {:?}", &username, &input);
|
debug!("User {} received activity: {:?}", &username, &input);
|
||||||
|
|
||||||
match input {
|
match input {
|
||||||
UserAcceptedObjects::Create(c) => handle_create(&c, &request, &username, &conn),
|
|
||||||
UserAcceptedObjects::Update(u) => handle_update(&u, &request, &username, &conn),
|
|
||||||
UserAcceptedObjects::Accept(a) => handle_accept(&a, &request, &username, &conn),
|
UserAcceptedObjects::Accept(a) => handle_accept(&a, &request, &username, &conn),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle create activities and insert them in the database.
|
|
||||||
fn handle_create(
|
|
||||||
create: &Create,
|
|
||||||
request: &HttpRequest,
|
|
||||||
_username: &str,
|
|
||||||
conn: &PgConnection,
|
|
||||||
) -> Result<HttpResponse, Error> {
|
|
||||||
// TODO before this even gets named, because we don't know what type of object it is, we need
|
|
||||||
// to parse this out
|
|
||||||
let user_uri = create
|
|
||||||
.create_props
|
|
||||||
.get_actor_xsd_any_uri()
|
|
||||||
.unwrap()
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?;
|
|
||||||
verify(request, &user.public_key.unwrap())?;
|
|
||||||
|
|
||||||
let page = create
|
|
||||||
.create_props
|
|
||||||
.get_object_base_box()
|
|
||||||
.to_owned()
|
|
||||||
.unwrap()
|
|
||||||
.to_owned()
|
|
||||||
.to_concrete::<Page>()?;
|
|
||||||
let post = PostForm::from_apub(&page, conn)?;
|
|
||||||
Post::create(conn, &post)?;
|
|
||||||
// TODO: send the new post out via websocket
|
|
||||||
Ok(HttpResponse::Ok().finish())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle update activities and insert them in the database.
|
|
||||||
fn handle_update(
|
|
||||||
update: &Update,
|
|
||||||
request: &HttpRequest,
|
|
||||||
_username: &str,
|
|
||||||
conn: &PgConnection,
|
|
||||||
) -> Result<HttpResponse, Error> {
|
|
||||||
let user_uri = update
|
|
||||||
.update_props
|
|
||||||
.get_actor_xsd_any_uri()
|
|
||||||
.unwrap()
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?;
|
|
||||||
verify(request, &user.public_key.unwrap())?;
|
|
||||||
|
|
||||||
let page = update
|
|
||||||
.update_props
|
|
||||||
.get_object_base_box()
|
|
||||||
.to_owned()
|
|
||||||
.unwrap()
|
|
||||||
.to_owned()
|
|
||||||
.to_concrete::<Page>()?;
|
|
||||||
let post = PostForm::from_apub(&page, conn)?;
|
|
||||||
let id = Post::read_from_apub_id(conn, &post.ap_id)?.id;
|
|
||||||
Post::update(conn, id, &post)?;
|
|
||||||
// TODO: send the new post out via websocket
|
|
||||||
Ok(HttpResponse::Ok().finish())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle accepted follows.
|
/// Handle accepted follows.
|
||||||
fn handle_accept(
|
fn handle_accept(
|
||||||
accept: &Accept,
|
accept: &Accept,
|
||||||
|
|
|
@ -38,6 +38,7 @@ pub struct CommentForm {
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub removed: Option<bool>,
|
pub removed: Option<bool>,
|
||||||
pub read: Option<bool>,
|
pub read: Option<bool>,
|
||||||
|
pub published: Option<chrono::NaiveDateTime>,
|
||||||
pub updated: Option<chrono::NaiveDateTime>,
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
pub deleted: Option<bool>,
|
pub deleted: Option<bool>,
|
||||||
pub ap_id: String,
|
pub ap_id: String,
|
||||||
|
@ -84,6 +85,11 @@ impl Comment {
|
||||||
.get_result::<Self>(conn)
|
.get_result::<Self>(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn read_from_apub_id(conn: &PgConnection, object_id: &str) -> Result<Self, Error> {
|
||||||
|
use crate::schema::comment::dsl::*;
|
||||||
|
comment.filter(ap_id.eq(object_id)).first::<Self>(conn)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn mark_as_read(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
|
pub fn mark_as_read(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
|
||||||
use crate::schema::comment::dsl::*;
|
use crate::schema::comment::dsl::*;
|
||||||
|
|
||||||
|
@ -283,6 +289,7 @@ mod tests {
|
||||||
deleted: None,
|
deleted: None,
|
||||||
read: None,
|
read: None,
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
|
published: None,
|
||||||
updated: None,
|
updated: None,
|
||||||
ap_id: "changeme".into(),
|
ap_id: "changeme".into(),
|
||||||
local: true,
|
local: true,
|
||||||
|
@ -313,6 +320,7 @@ mod tests {
|
||||||
removed: None,
|
removed: None,
|
||||||
deleted: None,
|
deleted: None,
|
||||||
read: None,
|
read: None,
|
||||||
|
published: None,
|
||||||
updated: None,
|
updated: None,
|
||||||
ap_id: "changeme".into(),
|
ap_id: "changeme".into(),
|
||||||
local: true,
|
local: true,
|
||||||
|
|
|
@ -540,6 +540,7 @@ mod tests {
|
||||||
removed: None,
|
removed: None,
|
||||||
deleted: None,
|
deleted: None,
|
||||||
read: None,
|
read: None,
|
||||||
|
published: None,
|
||||||
updated: None,
|
updated: None,
|
||||||
ap_id: "changeme".into(),
|
ap_id: "changeme".into(),
|
||||||
local: true,
|
local: true,
|
||||||
|
|
|
@ -541,6 +541,7 @@ mod tests {
|
||||||
deleted: None,
|
deleted: None,
|
||||||
read: None,
|
read: None,
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
|
published: None,
|
||||||
updated: None,
|
updated: None,
|
||||||
ap_id: "changeme".into(),
|
ap_id: "changeme".into(),
|
||||||
local: true,
|
local: true,
|
||||||
|
|
|
@ -167,6 +167,7 @@ mod tests {
|
||||||
deleted: None,
|
deleted: None,
|
||||||
read: None,
|
read: None,
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
|
published: None,
|
||||||
updated: None,
|
updated: None,
|
||||||
ap_id: "changeme".into(),
|
ap_id: "changeme".into(),
|
||||||
local: true,
|
local: true,
|
||||||
|
|
|
@ -2,6 +2,7 @@ use super::*;
|
||||||
use crate::apub::community::*;
|
use crate::apub::community::*;
|
||||||
use crate::apub::community_inbox::community_inbox;
|
use crate::apub::community_inbox::community_inbox;
|
||||||
use crate::apub::post::get_apub_post;
|
use crate::apub::post::get_apub_post;
|
||||||
|
use crate::apub::shared_inbox::shared_inbox;
|
||||||
use crate::apub::user::*;
|
use crate::apub::user::*;
|
||||||
use crate::apub::user_inbox::user_inbox;
|
use crate::apub::user_inbox::user_inbox;
|
||||||
use crate::apub::APUB_JSON_CONTENT_TYPE;
|
use crate::apub::APUB_JSON_CONTENT_TYPE;
|
||||||
|
@ -31,6 +32,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
)
|
)
|
||||||
// Inboxes dont work with the header guard for some reason.
|
// Inboxes dont work with the header guard for some reason.
|
||||||
.route("/c/{community_name}/inbox", web::post().to(community_inbox))
|
.route("/c/{community_name}/inbox", web::post().to(community_inbox))
|
||||||
.route("/u/{user_name}/inbox", web::post().to(user_inbox));
|
.route("/u/{user_name}/inbox", web::post().to(user_inbox))
|
||||||
|
.route("/inbox", web::post().to(shared_inbox));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
1
ui/jest.config.js
vendored
1
ui/jest.config.js
vendored
|
@ -1,6 +1,7 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
preset: 'ts-jest',
|
preset: 'ts-jest',
|
||||||
testEnvironment: 'node',
|
testEnvironment: 'node',
|
||||||
|
testTimeout: 30000,
|
||||||
globals: {
|
globals: {
|
||||||
'ts-jest': {
|
'ts-jest': {
|
||||||
diagnostics: false,
|
diagnostics: false,
|
||||||
|
|
302
ui/src/api_tests/api.spec.ts
vendored
302
ui/src/api_tests/api.spec.ts
vendored
|
@ -6,6 +6,13 @@ import {
|
||||||
PostForm,
|
PostForm,
|
||||||
PostResponse,
|
PostResponse,
|
||||||
SearchResponse,
|
SearchResponse,
|
||||||
|
FollowCommunityForm,
|
||||||
|
CommunityResponse,
|
||||||
|
GetFollowedCommunitiesResponse,
|
||||||
|
GetPostForm,
|
||||||
|
GetPostResponse,
|
||||||
|
CommentForm,
|
||||||
|
CommentResponse,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
|
|
||||||
let lemmyAlphaUrl = 'http://localhost:8540';
|
let lemmyAlphaUrl = 'http://localhost:8540';
|
||||||
|
@ -13,6 +20,7 @@ let lemmyBetaUrl = 'http://localhost:8550';
|
||||||
let lemmyAlphaApiUrl = `${lemmyAlphaUrl}/api/v1`;
|
let lemmyAlphaApiUrl = `${lemmyAlphaUrl}/api/v1`;
|
||||||
let lemmyBetaApiUrl = `${lemmyBetaUrl}/api/v1`;
|
let lemmyBetaApiUrl = `${lemmyBetaUrl}/api/v1`;
|
||||||
let lemmyAlphaAuth: string;
|
let lemmyAlphaAuth: string;
|
||||||
|
let lemmyBetaAuth: string;
|
||||||
|
|
||||||
// Workaround for tests being run before beforeAll() is finished
|
// Workaround for tests being run before beforeAll() is finished
|
||||||
// https://github.com/facebook/jest/issues/9527#issuecomment-592406108
|
// https://github.com/facebook/jest/issues/9527#issuecomment-592406108
|
||||||
|
@ -33,37 +41,287 @@ describe('main', () => {
|
||||||
}).then(d => d.json());
|
}).then(d => d.json());
|
||||||
|
|
||||||
lemmyAlphaAuth = res.jwt;
|
lemmyAlphaAuth = res.jwt;
|
||||||
});
|
|
||||||
|
|
||||||
test('Create test post on alpha and fetch it on beta', async () => {
|
console.log('Logging in as lemmy_beta');
|
||||||
let name = 'A jest test post';
|
let formB = {
|
||||||
let postForm: PostForm = {
|
username_or_email: 'lemmy_beta',
|
||||||
name,
|
password: 'lemmy',
|
||||||
auth: lemmyAlphaAuth,
|
|
||||||
community_id: 2,
|
|
||||||
creator_id: 2,
|
|
||||||
nsfw: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let createResponse: PostResponse = await fetch(`${lemmyAlphaApiUrl}/post`, {
|
let resB: LoginResponse = await fetch(`${lemmyBetaApiUrl}/user/login`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'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());
|
}).then(d => d.json());
|
||||||
|
|
||||||
// TODO: check more fields
|
lemmyBetaAuth = resB.jwt;
|
||||||
expect(searchResponse.posts[0].name).toBe(name);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function wrapper(form: any): string {
|
describe('beta_fetch', () => {
|
||||||
return JSON.stringify(form);
|
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());
|
||||||
|
|
||||||
|
expect(getPostRes.comments[0].content).toBe(content);
|
||||||
|
expect(getPostRes.comments[0].community_local).toBe(true);
|
||||||
|
expect(getPostRes.comments[0].creator_local).toBe(false);
|
||||||
|
|
||||||
|
// Now do beta replying to that comment, as a child comment
|
||||||
|
let contentBeta = 'A child federated comment from beta';
|
||||||
|
let commentFormBeta: CommentForm = {
|
||||||
|
content: contentBeta,
|
||||||
|
post_id: getPostRes.post.id,
|
||||||
|
parent_id: getPostRes.comments[0].id,
|
||||||
|
auth: lemmyBetaAuth,
|
||||||
|
};
|
||||||
|
|
||||||
|
let createResponseBeta: CommentResponse = await fetch(
|
||||||
|
`${lemmyBetaApiUrl}/comment`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: wrapper(commentFormBeta),
|
||||||
|
}
|
||||||
|
).then(d => d.json());
|
||||||
|
|
||||||
|
expect(createResponseBeta.comment.content).toBe(contentBeta);
|
||||||
|
expect(createResponseBeta.comment.community_local).toBe(true);
|
||||||
|
expect(createResponseBeta.comment.creator_local).toBe(true);
|
||||||
|
expect(createResponseBeta.comment.parent_id).toBe(1);
|
||||||
|
|
||||||
|
// Make sure lemmy alpha sees that new child comment from beta
|
||||||
|
let getPostUrlAlpha = `${lemmyAlphaApiUrl}/post?id=2`;
|
||||||
|
let getPostResAlpha: GetPostResponse = await fetch(getPostUrlAlpha, {
|
||||||
|
method: 'GET',
|
||||||
|
}).then(d => d.json());
|
||||||
|
|
||||||
|
// The newest show up first
|
||||||
|
expect(getPostResAlpha.comments[0].content).toBe(contentBeta);
|
||||||
|
expect(getPostResAlpha.comments[0].community_local).toBe(false);
|
||||||
|
expect(getPostResAlpha.comments[0].creator_local).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('update test comment', () => {
|
||||||
|
test('/u/lemmy_alpha updates a comment on /c/lemmy_beta/main, its on both instances', async () => {
|
||||||
|
let content = 'A jest test federated comment update';
|
||||||
|
let commentForm: CommentForm = {
|
||||||
|
content,
|
||||||
|
post_id: 2,
|
||||||
|
edit_id: 1,
|
||||||
|
auth: lemmyAlphaAuth,
|
||||||
|
creator_id: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
let updateResponse: CommentResponse = await fetch(
|
||||||
|
`${lemmyAlphaApiUrl}/comment`,
|
||||||
|
{
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: wrapper(commentForm),
|
||||||
|
}
|
||||||
|
).then(d => d.json());
|
||||||
|
|
||||||
|
expect(updateResponse.comment.content).toBe(content);
|
||||||
|
expect(updateResponse.comment.community_local).toBe(false);
|
||||||
|
expect(updateResponse.comment.creator_local).toBe(true);
|
||||||
|
|
||||||
|
let getPostUrl = `${lemmyBetaApiUrl}/post?id=2`;
|
||||||
|
let getPostRes: GetPostResponse = await fetch(getPostUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
}).then(d => d.json());
|
||||||
|
|
||||||
|
expect(getPostRes.comments[1].content).toBe(content);
|
||||||
|
expect(getPostRes.comments[1].community_local).toBe(true);
|
||||||
|
expect(getPostRes.comments[1].creator_local).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function wrapper(form: any): string {
|
||||||
|
return JSON.stringify(form);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue