Add "followers-only" post visibility setting

This commit is contained in:
silverpill 2022-01-05 18:47:02 +00:00
parent 05205c398e
commit 59a86ea827
7 changed files with 119 additions and 18 deletions

View file

@ -294,6 +294,7 @@ paths:
type: string type: string
enum: enum:
- public - public
- private
- direct - direct
mentions: mentions:
description: Array of profile IDs to be mentioned description: Array of profile IDs to be mentioned
@ -581,6 +582,7 @@ components:
type: string type: string
enum: enum:
- public - public
- private
- direct - direct
mentions: mentions:
description: Mentions of users within the post. description: Mentions of users within the post.

View file

@ -185,12 +185,14 @@ pub fn create_note(
}).collect(); }).collect();
let mut primary_audience = vec![]; let mut primary_audience = vec![];
let mut secondary_audience = vec![]; let mut secondary_audience = vec![];
let followers_collection_url =
get_followers_url(instance_url, &post.author.username);
let mut tags = vec![]; let mut tags = vec![];
if matches!(post.visibility, Visibility::Public) { if matches!(post.visibility, Visibility::Public) {
primary_audience.push(AP_PUBLIC.to_string()); primary_audience.push(AP_PUBLIC.to_string());
secondary_audience.push(get_followers_url( secondary_audience.push(followers_collection_url);
instance_url, &post.author.username, } else if matches!(post.visibility, Visibility::Followers) {
)); primary_audience.push(followers_collection_url);
}; };
for profile in &post.mentions { for profile in &post.mentions {
let actor_id = profile.actor_id(instance_url); let actor_id = profile.actor_id(instance_url);
@ -506,6 +508,20 @@ mod tests {
]); ]);
} }
#[test]
fn test_create_note_followers_only() {
let post = Post {
visibility: Visibility::Followers,
..Default::default()
};
let note = create_note(INSTANCE_HOST, INSTANCE_URL, &post);
assert_eq!(note.to, vec![
get_followers_url(INSTANCE_URL, &post.author.username),
]);
assert_eq!(note.cc.is_empty(), true);
}
#[test] #[test]
fn test_create_note_with_local_parent() { fn test_create_note_with_local_parent() {
let parent = Post::default(); let parent = Post::default();

View file

@ -13,7 +13,7 @@ pub async fn get_note_recipients(
post: &Post, post: &Post,
) -> Result<Vec<Actor>, DatabaseError> { ) -> Result<Vec<Actor>, DatabaseError> {
let mut audience = vec![]; let mut audience = vec![];
if matches!(post.visibility, Visibility::Public) { if matches!(post.visibility, Visibility::Public | Visibility::Followers) {
let followers = get_followers(db_client, &current_user.id, None, None).await?; let followers = get_followers(db_client, &current_user.id, None, None).await?;
audience.extend(followers); audience.extend(followers);
}; };

View file

@ -97,6 +97,7 @@ impl Status {
let visibility = match post.visibility { let visibility = match post.visibility {
Visibility::Public => "public", Visibility::Public => "public",
Visibility::Direct => "direct", Visibility::Direct => "direct",
Visibility::Followers => "private",
}; };
Self { Self {
id: post.id, id: post.id,
@ -145,6 +146,7 @@ impl TryFrom<StatusData> for PostCreateData {
let visibility = match value.visibility.as_deref() { let visibility = match value.visibility.as_deref() {
Some("public") => Visibility::Public, Some("public") => Visibility::Public,
Some("direct") => Visibility::Direct, Some("direct") => Visibility::Direct,
Some("private") => Visibility::Followers,
Some(_) => return Err(ValidationError("invalid visibility parameter")), Some(_) => return Err(ValidationError("invalid visibility parameter")),
None => Visibility::Public, None => Visibility::Public,
}; };

View file

@ -3,6 +3,7 @@ use uuid::Uuid;
use crate::errors::DatabaseError; use crate::errors::DatabaseError;
use crate::models::reactions::queries::find_favourited_by_user; use crate::models::reactions::queries::find_favourited_by_user;
use crate::models::relationships::queries::get_relationship;
use crate::models::users::types::User; use crate::models::users::types::User;
use super::queries::{get_posts, find_reposted_by_user}; use super::queries::{get_posts, find_reposted_by_user};
use super::types::{Post, PostActions, Visibility}; use super::types::{Post, PostActions, Visibility};
@ -60,7 +61,7 @@ pub async fn get_actions_for_posts(
} }
pub async fn can_view_post( pub async fn can_view_post(
_db_client: &impl GenericClient, db_client: &impl GenericClient,
user: Option<&User>, user: Option<&User>,
post: &Post, post: &Post,
) -> Result<bool, DatabaseError> { ) -> Result<bool, DatabaseError> {
@ -75,6 +76,20 @@ pub async fn can_view_post(
false false
} }
}, },
Visibility::Followers => {
if let Some(user) = user {
let relationship = get_relationship(
db_client,
&post.author.id,
&user.id,
).await?;
let is_mentioned = post.mentions.iter()
.any(|profile| profile.id == user.profile.id);
relationship.followed_by || is_mentioned
} else {
false
}
},
}; };
Ok(result) Ok(result)
} }
@ -82,7 +97,11 @@ pub async fn can_view_post(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use serial_test::serial; use serial_test::serial;
use tokio_postgres::Client;
use crate::database::test_utils::create_test_database; use crate::database::test_utils::create_test_database;
use crate::models::relationships::queries::follow;
use crate::models::users::queries::create_user;
use crate::models::users::types::UserCreateData;
use super::*; use super::*;
#[tokio::test] #[tokio::test]
@ -123,4 +142,42 @@ mod tests {
let result = can_view_post(db_client, Some(&user), &post).await.unwrap(); let result = can_view_post(db_client, Some(&user), &post).await.unwrap();
assert_eq!(result, true); assert_eq!(result, true);
} }
async fn create_test_user(db_client: &mut Client, username: &str) -> User {
let user_data = UserCreateData {
username: username.to_string(),
..Default::default()
};
create_user(db_client, user_data).await.unwrap()
}
#[tokio::test]
#[serial]
async fn test_can_view_post_followers_only_anonymous() {
let db_client = &mut create_test_database().await;
let author = create_test_user(db_client, "author").await;
let post = Post {
author: author.profile,
visibility: Visibility::Followers,
..Default::default()
};
let result = can_view_post(db_client, None, &post).await.unwrap();
assert_eq!(result, false);
}
#[tokio::test]
#[serial]
async fn test_can_view_post_followers_only_follower() {
let db_client = &mut create_test_database().await;
let author = create_test_user(db_client, "author").await;
let follower = create_test_user(db_client, "follower").await;
follow(db_client, &follower.id, &author.id).await.unwrap();
let post = Post {
author: author.profile,
visibility: Visibility::Followers,
..Default::default()
};
let result = can_view_post(db_client, Some(&follower), &post).await.unwrap();
assert_eq!(result, true);
}
} }

View file

@ -208,14 +208,20 @@ pub const RELATED_TAGS: &str =
fn build_visibility_filter() -> String { fn build_visibility_filter() -> String {
format!( format!(
"( "(
post.visibility = {visibility_public} post.author_id = $current_user_id
OR post.author_id = $current_user_id OR post.visibility = {visibility_public}
OR EXISTS ( OR post.visibility = {visibility_direct} AND EXISTS (
SELECT 1 FROM mention SELECT 1 FROM mention
WHERE post_id = post.id AND profile_id = $current_user_id WHERE post_id = post.id AND profile_id = $current_user_id
) )
OR post.visibility = {visibility_followers} AND EXISTS (
SELECT 1 FROM relationship
WHERE source_id = $current_user_id AND target_id = post.author_id
)
)", )",
visibility_public=i16::from(&Visibility::Public), visibility_public=i16::from(&Visibility::Public),
visibility_direct=i16::from(&Visibility::Direct),
visibility_followers=i16::from(&Visibility::Followers),
) )
} }
@ -227,7 +233,6 @@ pub async fn get_home_timeline(
) -> Result<Vec<Post>, DatabaseError> { ) -> Result<Vec<Post>, DatabaseError> {
// Select posts from follows, posts where current user is mentioned // Select posts from follows, posts where current user is mentioned
// and user's own posts. // and user's own posts.
// Exclude direct messages where current user is not mentioned.
let statement = format!( let statement = format!(
" "
SELECT SELECT
@ -815,7 +820,7 @@ mod tests {
..Default::default() ..Default::default()
}; };
let post_2 = create_post(db_client, &current_user.id, post_data_2).await.unwrap(); let post_2 = create_post(db_client, &current_user.id, post_data_2).await.unwrap();
// Another user // Another user's public post
let user_data_1 = UserCreateData { let user_data_1 = UserCreateData {
username: "another-user".to_string(), username: "another-user".to_string(),
..Default::default() ..Default::default()
@ -834,35 +839,51 @@ mod tests {
..Default::default() ..Default::default()
}; };
let post_4 = create_post(db_client, &user_1.id, post_data_4).await.unwrap(); let post_4 = create_post(db_client, &user_1.id, post_data_4).await.unwrap();
// Followed // Followers-only post from another user
let post_data_5 = PostCreateData {
content: "followers only".to_string(),
visibility: Visibility::Followers,
..Default::default()
};
let post_5 = create_post(db_client, &user_1.id, post_data_5).await.unwrap();
// Followed user's public post
let user_data_2 = UserCreateData { let user_data_2 = UserCreateData {
username: "followed".to_string(), username: "followed".to_string(),
..Default::default() ..Default::default()
}; };
let user_2 = create_user(db_client, user_data_2).await.unwrap(); let user_2 = create_user(db_client, user_data_2).await.unwrap();
follow(db_client, &current_user.id, &user_2.id).await.unwrap(); follow(db_client, &current_user.id, &user_2.id).await.unwrap();
let post_data_5 = PostCreateData { let post_data_6 = PostCreateData {
content: "test post".to_string(), content: "test post".to_string(),
..Default::default() ..Default::default()
}; };
let post_5 = create_post(db_client, &user_2.id, post_data_5).await.unwrap(); let post_6 = create_post(db_client, &user_2.id, post_data_6).await.unwrap();
// Direct message from followed user sent to another user // Direct message from followed user sent to another user
let post_data_6 = PostCreateData { let post_data_7 = PostCreateData {
content: "test post".to_string(), content: "test post".to_string(),
visibility: Visibility::Direct, visibility: Visibility::Direct,
mentions: vec![user_1.id], mentions: vec![user_1.id],
..Default::default() ..Default::default()
}; };
let post_6 = create_post(db_client, &user_2.id, post_data_6).await.unwrap(); let post_7 = create_post(db_client, &user_2.id, post_data_7).await.unwrap();
// Followers-only post from followed user
let post_data_8 = PostCreateData {
content: "followers only".to_string(),
visibility: Visibility::Followers,
..Default::default()
};
let post_8 = create_post(db_client, &user_2.id, post_data_8).await.unwrap();
let timeline = get_home_timeline(db_client, &current_user.id, None, 10).await.unwrap(); let timeline = get_home_timeline(db_client, &current_user.id, None, 10).await.unwrap();
assert_eq!(timeline.len(), 4); assert_eq!(timeline.len(), 5);
assert_eq!(timeline.iter().any(|post| post.id == post_1.id), true); assert_eq!(timeline.iter().any(|post| post.id == post_1.id), true);
assert_eq!(timeline.iter().any(|post| post.id == post_2.id), true); assert_eq!(timeline.iter().any(|post| post.id == post_2.id), true);
assert_eq!(timeline.iter().any(|post| post.id == post_3.id), false); assert_eq!(timeline.iter().any(|post| post.id == post_3.id), false);
assert_eq!(timeline.iter().any(|post| post.id == post_4.id), true); assert_eq!(timeline.iter().any(|post| post.id == post_4.id), true);
assert_eq!(timeline.iter().any(|post| post.id == post_5.id), true); assert_eq!(timeline.iter().any(|post| post.id == post_5.id), false);
assert_eq!(timeline.iter().any(|post| post.id == post_6.id), false); assert_eq!(timeline.iter().any(|post| post.id == post_6.id), true);
assert_eq!(timeline.iter().any(|post| post.id == post_7.id), false);
assert_eq!(timeline.iter().any(|post| post.id == post_8.id), true);
} }
#[tokio::test] #[tokio::test]

View file

@ -16,6 +16,7 @@ use crate::utils::html::clean_html;
pub enum Visibility { pub enum Visibility {
Public, Public,
Direct, Direct,
Followers,
} }
impl Default for Visibility { impl Default for Visibility {
@ -27,6 +28,7 @@ impl From<&Visibility> for i16 {
match value { match value {
Visibility::Public => 1, Visibility::Public => 1,
Visibility::Direct => 2, Visibility::Direct => 2,
Visibility::Followers => 3,
} }
} }
} }
@ -38,6 +40,7 @@ impl TryFrom<i16> for Visibility {
let visibility = match value { let visibility = match value {
1 => Self::Public, 1 => Self::Public,
2 => Self::Direct, 2 => Self::Direct,
3 => Self::Followers,
_ => return Err(ConversionError), _ => return Err(ConversionError),
}; };
Ok(visibility) Ok(visibility)