Merge pull request 'Add pending status for federated follows' (#130) from pending-follow into main

Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/130
This commit is contained in:
dessalines 2020-11-11 19:18:27 +00:00
commit 15c77f85c1
25 changed files with 109 additions and 57 deletions

3
Cargo.lock generated
View file

@ -1852,8 +1852,10 @@ dependencies = [
name = "lemmy_server" name = "lemmy_server"
version = "0.0.1" version = "0.0.1"
dependencies = [ dependencies = [
"activitystreams",
"actix", "actix",
"actix-files", "actix-files",
"actix-rt",
"actix-web", "actix-web",
"actix-web-actors", "actix-web-actors",
"anyhow", "anyhow",
@ -1877,6 +1879,7 @@ dependencies = [
"reqwest", "reqwest",
"rss", "rss",
"serde 1.0.117", "serde 1.0.117",
"serde_json",
"sha2", "sha2",
"strum", "strum",
"tokio 0.3.1", "tokio 0.3.1",

View file

@ -46,6 +46,9 @@ tokio = "0.3"
sha2 = "0.9" sha2 = "0.9"
anyhow = "1.0" anyhow = "1.0"
reqwest = { version = "0.10", features = ["json"] } reqwest = { version = "0.10", features = ["json"] }
activitystreams = "0.7.0-alpha.4"
actix-rt = { version = "1.1", default-features = false }
serde_json = { version = "1.0", features = ["preserve_order"]}
[dev-dependencies.cargo-husky] [dev-dependencies.cargo-husky]
version = "1" version = "1"

View file

@ -188,6 +188,7 @@ impl Perform for CreateCommunity {
let community_follower_form = CommunityFollowerForm { let community_follower_form = CommunityFollowerForm {
community_id: inserted_community.id, community_id: inserted_community.id,
user_id: user.id, user_id: user.id,
pending: false,
}; };
let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form); let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
@ -479,6 +480,7 @@ impl Perform for FollowCommunity {
let community_follower_form = CommunityFollowerForm { let community_follower_form = CommunityFollowerForm {
community_id: data.community_id, community_id: data.community_id,
user_id: user.id, user_id: user.id,
pending: false,
}; };
if community.local { if community.local {

View file

@ -251,6 +251,7 @@ impl Perform for Register {
let community_follower_form = CommunityFollowerForm { let community_follower_form = CommunityFollowerForm {
community_id: main_community.id, community_id: main_community.id,
user_id: inserted_user.id, user_id: inserted_user.id,
pending: false,
}; };
let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form); let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);

View file

@ -12,7 +12,12 @@ use activitystreams::{
base::{AnyBase, BaseExt, ExtendsExt}, base::{AnyBase, BaseExt, ExtendsExt},
object::ObjectExt, object::ObjectExt,
}; };
use lemmy_db::{community::Community, user::User_, DbPool}; use lemmy_db::{
community::{Community, CommunityFollower, CommunityFollowerForm},
user::User_,
DbPool,
Followable,
};
use lemmy_structs::blocking; use lemmy_structs::blocking;
use lemmy_utils::LemmyError; use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext; use lemmy_websocket::LemmyContext;
@ -44,6 +49,16 @@ impl ActorType for User_ {
}) })
.await??; .await??;
let community_follower_form = CommunityFollowerForm {
community_id: community.id,
user_id: self.id,
pending: true,
};
blocking(&context.pool(), move |conn| {
CommunityFollower::follow(conn, &community_follower_form).ok()
})
.await?;
let mut follow = Follow::new(self.actor_id.to_owned(), community.actor_id()?); let mut follow = Follow::new(self.actor_id.to_owned(), community.actor_id()?);
follow follow
.set_context(activitystreams::context()) .set_context(activitystreams::context())

View file

@ -54,7 +54,8 @@ pub async fn get_activity(
}) })
.await??; .await??;
if !activity.local || activity.sensitive { let sensitive = activity.sensitive.unwrap_or(true);
if !activity.local || sensitive {
Ok(HttpResponse::NotFound().finish()) Ok(HttpResponse::NotFound().finish())
} else { } else {
Ok(create_apub_response(&activity.data)) Ok(create_apub_response(&activity.data))

View file

@ -191,6 +191,7 @@ async fn handle_follow(
let community_follower_form = CommunityFollowerForm { let community_follower_form = CommunityFollowerForm {
community_id: community.id, community_id: community.id,
user_id: user.id, user_id: user.id,
pending: false,
}; };
// This will fail if they're already a follower, but ignore the error. // This will fail if they're already a follower, but ignore the error.
@ -245,6 +246,7 @@ async fn handle_undo_follow(
let community_follower_form = CommunityFollowerForm { let community_follower_form = CommunityFollowerForm {
community_id: community.id, community_id: community.id,
user_id: user.id, user_id: user.id,
pending: false,
}; };
// This will fail if they aren't a follower, but ignore the error. // This will fail if they aren't a follower, but ignore the error.

View file

@ -46,7 +46,7 @@ use actix_web::{web, HttpRequest, HttpResponse};
use anyhow::{anyhow, Context}; use anyhow::{anyhow, Context};
use diesel::NotFound; use diesel::NotFound;
use lemmy_db::{ use lemmy_db::{
community::{Community, CommunityFollower, CommunityFollowerForm}, community::{Community, CommunityFollower},
private_message::PrivateMessage, private_message::PrivateMessage,
user::User_, user::User_,
Followable, Followable,
@ -173,8 +173,6 @@ async fn receive_accept(
let accept = Accept::from_any_base(activity)?.context(location_info!())?; let accept = Accept::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&accept, &actor.actor_id()?, false)?; verify_activity_domains_valid(&accept, &actor.actor_id()?, false)?;
// TODO: we should check that we actually sent this activity, because the remote instance
// could just put a fake Follow
let object = accept.object().to_owned().one().context(location_info!())?; let object = accept.object().to_owned().one().context(location_info!())?;
let follow = Follow::from_any_base(object)?.context(location_info!())?; let follow = Follow::from_any_base(object)?.context(location_info!())?;
verify_activity_domains_valid(&follow, &user.actor_id()?, false)?; verify_activity_domains_valid(&follow, &user.actor_id()?, false)?;
@ -188,17 +186,13 @@ async fn receive_accept(
let community = let community =
get_or_fetch_and_upsert_community(&community_uri, context, request_counter).await?; get_or_fetch_and_upsert_community(&community_uri, context, request_counter).await?;
// Now you need to add this to the community follower let community_id = community.id;
let community_follower_form = CommunityFollowerForm { let user_id = user.id;
community_id: community.id, // This will throw an error if no follow was requested
user_id: user.id,
};
// This will fail if they're already a follower
blocking(&context.pool(), move |conn| { blocking(&context.pool(), move |conn| {
CommunityFollower::follow(conn, &community_follower_form).ok() CommunityFollower::follow_accepted(conn, community_id, user_id)
}) })
.await?; .await??;
Ok(()) Ok(())
} }

View file

@ -12,22 +12,22 @@ use std::{
#[table_name = "activity"] #[table_name = "activity"]
pub struct Activity { pub struct Activity {
pub id: i32, pub id: i32,
pub ap_id: String,
pub data: Value, pub data: Value,
pub local: bool, pub local: bool,
pub sensitive: bool,
pub published: chrono::NaiveDateTime, pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>, pub updated: Option<chrono::NaiveDateTime>,
pub ap_id: Option<String>,
pub sensitive: Option<bool>,
} }
#[derive(Insertable, AsChangeset)] #[derive(Insertable, AsChangeset)]
#[table_name = "activity"] #[table_name = "activity"]
pub struct ActivityForm { pub struct ActivityForm {
pub ap_id: String,
pub data: Value, pub data: Value,
pub local: bool, pub local: bool,
pub sensitive: bool,
pub updated: Option<chrono::NaiveDateTime>, pub updated: Option<chrono::NaiveDateTime>,
pub ap_id: String,
pub sensitive: bool,
} }
impl Crud<ActivityForm> for Activity { impl Crud<ActivityForm> for Activity {
@ -53,6 +53,10 @@ impl Crud<ActivityForm> for Activity {
.set(new_activity) .set(new_activity)
.get_result::<Self>(conn) .get_result::<Self>(conn)
} }
fn delete(conn: &PgConnection, activity_id: i32) -> Result<usize, Error> {
use crate::schema::activity::dsl::*;
diesel::delete(activity.find(activity_id)).execute(conn)
}
} }
impl Activity { impl Activity {
@ -115,7 +119,7 @@ mod tests {
avatar: None, avatar: None,
banner: None, banner: None,
admin: false, admin: false,
banned: false, banned: Some(false),
published: None, published: None,
updated: None, updated: None,
show_nsfw: false, show_nsfw: false,
@ -162,11 +166,11 @@ mod tests {
let inserted_activity = Activity::create(&conn, &activity_form).unwrap(); let inserted_activity = Activity::create(&conn, &activity_form).unwrap();
let expected_activity = Activity { let expected_activity = Activity {
ap_id: ap_id.to_string(), ap_id: Some(ap_id.to_string()),
id: inserted_activity.id, id: inserted_activity.id,
data: test_json, data: test_json,
local: true, local: true,
sensitive: false, sensitive: Some(false),
published: inserted_activity.published, published: inserted_activity.published,
updated: None, updated: None,
}; };
@ -174,6 +178,7 @@ mod tests {
let read_activity = Activity::read(&conn, inserted_activity.id).unwrap(); let read_activity = Activity::read(&conn, inserted_activity.id).unwrap();
let read_activity_by_apub_id = Activity::read_from_apub_id(&conn, ap_id).unwrap(); let read_activity_by_apub_id = Activity::read_from_apub_id(&conn, ap_id).unwrap();
User_::delete(&conn, inserted_creator.id).unwrap(); User_::delete(&conn, inserted_creator.id).unwrap();
Activity::delete(&conn, inserted_activity.id).unwrap();
assert_eq!(expected_activity, read_activity); assert_eq!(expected_activity, read_activity);
assert_eq!(expected_activity, read_activity_by_apub_id); assert_eq!(expected_activity, read_activity_by_apub_id);

View file

@ -280,7 +280,7 @@ mod tests {
avatar: None, avatar: None,
banner: None, banner: None,
admin: false, admin: false,
banned: false, banned: Some(false),
published: None, published: None,
updated: None, updated: None,
show_nsfw: false, show_nsfw: false,

View file

@ -519,7 +519,7 @@ mod tests {
avatar: None, avatar: None,
banner: None, banner: None,
admin: false, admin: false,
banned: false, banned: Some(false),
published: None, published: None,
updated: None, updated: None,
show_nsfw: false, show_nsfw: false,

View file

@ -276,6 +276,7 @@ pub struct CommunityFollower {
pub community_id: i32, pub community_id: i32,
pub user_id: i32, pub user_id: i32,
pub published: chrono::NaiveDateTime, pub published: chrono::NaiveDateTime,
pub pending: Option<bool>,
} }
#[derive(Insertable, AsChangeset, Clone)] #[derive(Insertable, AsChangeset, Clone)]
@ -283,6 +284,7 @@ pub struct CommunityFollower {
pub struct CommunityFollowerForm { pub struct CommunityFollowerForm {
pub community_id: i32, pub community_id: i32,
pub user_id: i32, pub user_id: i32,
pub pending: bool,
} }
impl Followable<CommunityFollowerForm> for CommunityFollower { impl Followable<CommunityFollowerForm> for CommunityFollower {
@ -295,6 +297,19 @@ impl Followable<CommunityFollowerForm> for CommunityFollower {
.values(community_follower_form) .values(community_follower_form)
.get_result::<Self>(conn) .get_result::<Self>(conn)
} }
fn follow_accepted(conn: &PgConnection, community_id_: i32, user_id_: i32) -> Result<Self, Error>
where
Self: Sized,
{
use crate::schema::community_follower::dsl::*;
diesel::update(
community_follower
.filter(community_id.eq(community_id_))
.filter(user_id.eq(user_id_)),
)
.set(pending.eq(true))
.get_result::<Self>(conn)
}
fn unfollow( fn unfollow(
conn: &PgConnection, conn: &PgConnection,
community_follower_form: &CommunityFollowerForm, community_follower_form: &CommunityFollowerForm,
@ -326,7 +341,7 @@ mod tests {
avatar: None, avatar: None,
banner: None, banner: None,
admin: false, admin: false,
banned: false, banned: Some(false),
published: None, published: None,
updated: None, updated: None,
show_nsfw: false, show_nsfw: false,
@ -392,6 +407,7 @@ mod tests {
let community_follower_form = CommunityFollowerForm { let community_follower_form = CommunityFollowerForm {
community_id: inserted_community.id, community_id: inserted_community.id,
user_id: inserted_user.id, user_id: inserted_user.id,
pending: false,
}; };
let inserted_community_follower = let inserted_community_follower =
@ -401,6 +417,7 @@ mod tests {
id: inserted_community_follower.id, id: inserted_community_follower.id,
community_id: inserted_community.id, community_id: inserted_community.id,
user_id: inserted_user.id, user_id: inserted_user.id,
pending: Some(false),
published: inserted_community_follower.published, published: inserted_community_follower.published,
}; };

View file

@ -54,6 +54,9 @@ pub trait Crud<T> {
pub trait Followable<T> { pub trait Followable<T> {
fn follow(conn: &PgConnection, form: &T) -> Result<Self, Error> fn follow(conn: &PgConnection, form: &T) -> Result<Self, Error>
where
Self: Sized;
fn follow_accepted(conn: &PgConnection, community_id: i32, user_id: i32) -> Result<Self, Error>
where where
Self: Sized; Self: Sized;
fn unfollow(conn: &PgConnection, form: &T) -> Result<usize, Error> fn unfollow(conn: &PgConnection, form: &T) -> Result<usize, Error>

View file

@ -416,7 +416,7 @@ mod tests {
avatar: None, avatar: None,
banner: None, banner: None,
admin: false, admin: false,
banned: false, banned: Some(false),
published: None, published: None,
updated: None, updated: None,
show_nsfw: false, show_nsfw: false,
@ -445,7 +445,7 @@ mod tests {
avatar: None, avatar: None,
banner: None, banner: None,
admin: false, admin: false,
banned: false, banned: Some(false),
published: None, published: None,
updated: None, updated: None,
show_nsfw: false, show_nsfw: false,

View file

@ -100,7 +100,7 @@ mod tests {
avatar: None, avatar: None,
banner: None, banner: None,
admin: false, admin: false,
banned: false, banned: Some(false),
published: None, published: None,
updated: None, updated: None,
show_nsfw: false, show_nsfw: false,

View file

@ -349,7 +349,7 @@ mod tests {
avatar: None, avatar: None,
banner: None, banner: None,
admin: false, admin: false,
banned: false, banned: Some(false),
published: None, published: None,
updated: None, updated: None,
show_nsfw: false, show_nsfw: false,

View file

@ -416,7 +416,7 @@ mod tests {
published: None, published: None,
updated: None, updated: None,
admin: false, admin: false,
banned: false, banned: Some(false),
show_nsfw: false, show_nsfw: false,
theme: "browser".into(), theme: "browser".into(),
default_sort_type: SortType::Hot as i16, default_sort_type: SortType::Hot as i16,

View file

@ -157,7 +157,7 @@ mod tests {
avatar: None, avatar: None,
banner: None, banner: None,
admin: false, admin: false,
banned: false, banned: Some(false),
published: None, published: None,
updated: None, updated: None,
show_nsfw: false, show_nsfw: false,
@ -186,7 +186,7 @@ mod tests {
avatar: None, avatar: None,
banner: None, banner: None,
admin: false, admin: false,
banned: false, banned: Some(false),
published: None, published: None,
updated: None, updated: None,
show_nsfw: false, show_nsfw: false,

View file

@ -1,12 +1,12 @@
table! { table! {
activity (id) { activity (id) {
id -> Int4, id -> Int4,
ap_id -> Text,
data -> Jsonb, data -> Jsonb,
local -> Bool, local -> Bool,
sensitive -> Bool,
published -> Timestamp, published -> Timestamp,
updated -> Nullable<Timestamp>, updated -> Nullable<Timestamp>,
ap_id -> Nullable<Text>,
sensitive -> Nullable<Bool>,
} }
} }
@ -150,6 +150,7 @@ table! {
community_id -> Int4, community_id -> Int4,
user_id -> Int4, user_id -> Int4,
published -> Timestamp, published -> Timestamp,
pending -> Nullable<Bool>,
} }
} }

View file

@ -196,7 +196,7 @@ mod tests {
avatar: None, avatar: None,
banner: None, banner: None,
admin: false, admin: false,
banned: false, banned: Some(false),
published: None, published: None,
updated: None, updated: None,
show_nsfw: false, show_nsfw: false,

View file

@ -96,7 +96,7 @@ mod tests {
avatar: None, avatar: None,
banner: None, banner: None,
admin: false, admin: false,
banned: false, banned: Some(false),
published: None, published: None,
updated: None, updated: None,
show_nsfw: false, show_nsfw: false,
@ -125,7 +125,7 @@ mod tests {
avatar: None, avatar: None,
banner: None, banner: None,
admin: false, admin: false,
banned: false, banned: Some(false),
published: None, published: None,
updated: None, updated: None,
show_nsfw: false, show_nsfw: false,

View file

@ -0,0 +1 @@
ALTER TABLE community_follower DROP COLUMN pending;

View file

@ -0,0 +1 @@
ALTER TABLE community_follower ADD COLUMN pending BOOLEAN DEFAULT FALSE;

View file

@ -2,4 +2,7 @@
export DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy export DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
diesel migration run diesel migration run
export LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy export LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
RUST_TEST_THREADS=1 cargo test --workspace --no-fail-fast # Integration tests only work on stable due to a bug in config-rs
# https://github.com/mehcode/config-rs/issues/158
RUST_BACKTRACE=1 RUST_TEST_THREADS=1 \
cargo +stable test --workspace --no-fail-fast

View file

@ -16,16 +16,8 @@ use diesel::{
PgConnection, PgConnection,
}; };
use http_signature_normalization_actix::PrepareVerifyError; use http_signature_normalization_actix::PrepareVerifyError;
use lemmy_db::{ use lemmy_api::match_websocket_operation;
community::{Community, CommunityForm}, use lemmy_apub::{
user::{User_, *},
Crud,
ListingType,
SortType,
};
use lemmy_rate_limit::{rate_limiter::RateLimiter, RateLimit};
use lemmy_server::{
apub::{
activity_queue::create_activity_queue, activity_queue::create_activity_queue,
inbox::{ inbox::{
community_inbox, community_inbox,
@ -35,11 +27,17 @@ use lemmy_server::{
user_inbox, user_inbox,
user_inbox::user_inbox, user_inbox::user_inbox,
}, },
},
websocket::chat_server::ChatServer,
LemmyContext,
}; };
use lemmy_db::{
community::{Community, CommunityForm},
user::{User_, *},
Crud,
ListingType,
SortType,
};
use lemmy_rate_limit::{rate_limiter::RateLimiter, RateLimit};
use lemmy_utils::{apub::generate_actor_keypair, settings::Settings}; use lemmy_utils::{apub::generate_actor_keypair, settings::Settings};
use lemmy_websocket::{chat_server::ChatServer, LemmyContext};
use reqwest::Client; use reqwest::Client;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::sync::Arc; use std::sync::Arc;
@ -61,11 +59,12 @@ fn create_context() -> LemmyContext {
let chat_server = ChatServer::startup( let chat_server = ChatServer::startup(
pool.clone(), pool.clone(),
rate_limiter.clone(), rate_limiter.clone(),
|c, i, o, d| Box::pin(match_websocket_operation(c, i, o, d)),
Client::default(), Client::default(),
activity_queue.clone(), activity_queue.clone(),
) )
.start(); .start();
LemmyContext::new( LemmyContext::create(
pool, pool,
chat_server, chat_server,
Client::default(), Client::default(),
@ -84,7 +83,7 @@ fn create_user(conn: &PgConnection, name: &str) -> User_ {
avatar: None, avatar: None,
banner: None, banner: None,
admin: false, admin: false,
banned: false, banned: Some(false),
updated: None, updated: None,
published: None, published: None,
show_nsfw: false, show_nsfw: false,
@ -177,7 +176,7 @@ async fn test_user_inbox_expired_signature() {
let connection = &context.pool().get().unwrap(); let connection = &context.pool().get().unwrap();
let user = create_user(connection, "user_inbox_cgsax"); let user = create_user(connection, "user_inbox_cgsax");
let activity = let activity =
create_activity::<CreateType, ActorAndObject<user_inbox::ValidTypes>>(user.actor_id); create_activity::<CreateType, ActorAndObject<user_inbox::UserValidTypes>>(user.actor_id);
let path = Path::<String> { let path = Path::<String> {
0: "username".to_string(), 0: "username".to_string(),
}; };
@ -196,8 +195,9 @@ async fn test_community_inbox_expired_signature() {
let user = create_user(connection, "community_inbox_hrxa"); let user = create_user(connection, "community_inbox_hrxa");
let community = create_community(connection, user.id); let community = create_community(connection, user.id);
let request = create_http_request(); let request = create_http_request();
let activity = let activity = create_activity::<FollowType, ActorAndObject<community_inbox::CommunityValidTypes>>(
create_activity::<FollowType, ActorAndObject<community_inbox::ValidTypes>>(user.actor_id); user.actor_id,
);
let path = Path::<String> { 0: community.name }; let path = Path::<String> { 0: community.name };
let response = community_inbox(request, activity, path, web::Data::new(context)).await; let response = community_inbox(request, activity, path, web::Data::new(context)).await;
assert_eq!( assert_eq!(