Add "followers-only" post visibility setting
This commit is contained in:
parent
05205c398e
commit
59a86ea827
7 changed files with 119 additions and 18 deletions
|
@ -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.
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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, ¤t_user.id, None, None).await?;
|
let followers = get_followers(db_client, ¤t_user.id, None, None).await?;
|
||||||
audience.extend(followers);
|
audience.extend(followers);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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, ¤t_user.id, post_data_2).await.unwrap();
|
let post_2 = create_post(db_client, ¤t_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, ¤t_user.id, &user_2.id).await.unwrap();
|
follow(db_client, ¤t_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, ¤t_user.id, None, 10).await.unwrap();
|
let timeline = get_home_timeline(db_client, ¤t_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]
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue