add: close friends stack

This commit is contained in:
trisua 2025-08-31 13:02:15 -04:00
parent 407155e6c4
commit 5fafc8d7b9
83 changed files with 479 additions and 213 deletions

View file

@ -130,6 +130,7 @@ impl DataManager {
checkouts: serde_json::from_str(&get!(x->32(String)).to_string()).unwrap(),
applied_configurations: serde_json::from_str(&get!(x->33(String)).to_string()).unwrap(),
last_policy_consent: get!(x->34(i64)) as usize,
close_friends_stack: get!(x->35(i64)) as usize,
}
}
@ -286,7 +287,7 @@ impl DataManager {
let res = execute!(
&conn,
"INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35)",
"INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36)",
params![
&(data.id as i64),
&(data.created as i64),
@ -322,7 +323,8 @@ impl DataManager {
&(data.coins as i32),
&serde_json::to_string(&data.checkouts).unwrap(),
&serde_json::to_string(&data.applied_configurations).unwrap(),
&(data.last_policy_consent as i64)
&(data.last_policy_consent as i64),
&(data.close_friends_stack as i64)
]
);
@ -1138,6 +1140,7 @@ impl DataManager {
auto_method!(update_user_checkouts(Vec<String>)@get_user_by_id -> "UPDATE users SET checkouts = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user);
auto_method!(update_user_applied_configurations(Vec<(AppliedConfigType, usize)>)@get_user_by_id -> "UPDATE users SET applied_configurations = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user);
auto_method!(update_user_last_policy_consent(i64)@get_user_by_id -> "UPDATE users SET last_policy_consent = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user);
auto_method!(update_user_close_friends_stack(i64)@get_user_by_id -> "UPDATE users SET close_friends_stack = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user);
auto_method!(get_user_by_stripe_id(&str)@get_user_from_row -> "SELECT * FROM users WHERE stripe_id = $1" --name="user" --returns=User);
auto_method!(update_user_stripe_id(&str)@get_user_by_id -> "UPDATE users SET stripe_id = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user);

View file

@ -6,5 +6,6 @@ CREATE TABLE IF NOT EXISTS stacks (
users TEXT NOT NULL,
privacy TEXT NOT NULL,
mode TEXT NOT NULL,
sort TEXT NOT NULL
sort TEXT NOT NULL,
is_locked INT NOT NULL
)

View file

@ -33,5 +33,6 @@ CREATE TABLE IF NOT EXISTS users (
coins INT NOT NULL,
checkouts TEXT NOT NULL,
applied_configurations TEXT NOT NULL,
last_policy_consent BIGINT NOT NULL
last_policy_consent BIGINT NOT NULL,
close_friends_stack BIGINT NOT NULL
)

View file

@ -77,3 +77,11 @@ ADD COLUMN IF NOT EXISTS likes INT DEFAULT 0;
-- letters dislikes
ALTER TABLE letters
ADD COLUMN IF NOT EXISTS dislikes INT DEFAULT 0;
-- users close_friends_stack
ALTER TABLE users
ADD COLUMN IF NOT EXISTS close_friends_stack BIGINT DEFAULT 0;
-- stacks is_locked
ALTER TABLE stacks
ADD COLUMN IF NOT EXISTS is_locked INT DEFAULT 0;

View file

@ -1209,12 +1209,12 @@ impl DataManager {
/// # Arguments
/// * `id` - the ID of the stack the requested posts belong to
/// * `batch` - the limit of posts in each page
/// * `page` - the page number
/// * `before` - the timestamp to pull posts before
pub async fn get_posts_by_stack(
&self,
id: usize,
batch: usize,
page: usize,
before: usize,
) -> Result<Vec<Post>> {
let conn = match self.0.connect().await {
Ok(c) => c,
@ -1223,8 +1223,17 @@ impl DataManager {
let res = query_rows!(
&conn,
"SELECT * FROM posts WHERE stack = $1 AND replying_to = 0 AND is_deleted = 0 ORDER BY created DESC LIMIT $2 OFFSET $3",
&[&(id as i64), &(batch as i64), &((page * batch) as i64)],
&format!(
"SELECT * FROM posts WHERE stack = $1 AND replying_to = 0 AND is_deleted = 0{} ORDER BY created DESC LIMIT $2",
{
if before > 0 {
format!(" AND created < {before}")
} else {
String::new()
}
}
),
&[&(id as i64), &(batch as i64)],
|x| { Self::get_post_from_row(x) }
);
@ -1452,12 +1461,12 @@ impl DataManager {
///
/// # Arguments
/// * `batch` - the limit of posts in each page
/// * `page` - the page number
/// * `before` - the timestamp to pull posts before
/// * `cutoff` - the maximum number of milliseconds ago the post could have been created
pub async fn get_popular_posts(
&self,
batch: usize,
page: usize,
before: usize,
cutoff: usize,
) -> Result<Vec<Post>> {
let conn = match self.0.connect().await {
@ -1467,18 +1476,24 @@ impl DataManager {
let res = query_rows!(
&conn,
"SELECT * FROM posts WHERE replying_to = 0 AND NOT context LIKE '%\"is_nsfw\":true%' AND ($1 - created) < $2 ORDER BY likes - dislikes DESC, created ASC LIMIT $3 OFFSET $4",
&format!(
"SELECT * FROM posts WHERE replying_to = 0 AND NOT context LIKE '%\"is_nsfw\":true%' AND ($1 - created) < $2{} ORDER BY (likes - dislikes) DESC, created ASC LIMIT $3",
if before > 0 {
format!(" AND created < {before}")
} else {
String::new()
}
),
&[
&(unix_epoch_timestamp() as i64),
&(cutoff as i64),
&(batch as i64),
&((page * batch) as i64)
],
|x| { Self::get_post_from_row(x) }
);
if res.is_err() {
return Err(Error::GeneralNotFound("post".to_string()));
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
Ok(res.unwrap())
@ -1492,7 +1507,6 @@ impl DataManager {
pub async fn get_latest_posts(
&self,
batch: usize,
page: usize,
as_user: &Option<User>,
before_time: usize,
) -> Result<Vec<Post>> {
@ -1518,7 +1532,7 @@ impl DataManager {
let res = query_rows!(
&conn,
&format!(
"SELECT * FROM posts WHERE replying_to = 0{}{}{} AND NOT context LIKE '%\"full_unlist\":true%' AND topic = 0 ORDER BY created DESC LIMIT $1 OFFSET $2",
"SELECT * FROM posts WHERE replying_to = 0{}{}{} AND NOT context LIKE '%\"full_unlist\":true%' AND topic = 0 ORDER BY created DESC LIMIT $1",
if before_time > 0 {
format!(" AND created < {before_time}")
} else {
@ -1535,12 +1549,12 @@ impl DataManager {
""
}
),
&[&(batch as i64), &((page * batch) as i64)],
&[&(batch as i64)],
|x| { Self::get_post_from_row(x) }
);
if res.is_err() {
return Err(Error::GeneralNotFound("post".to_string()));
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
Ok(res.unwrap())
@ -1556,7 +1570,6 @@ impl DataManager {
batch: usize,
page: usize,
as_user: &Option<User>,
before_time: usize,
) -> Result<Vec<Post>> {
// check if we should hide nsfw posts
let mut hide_nsfw: bool = true;
@ -1574,12 +1587,7 @@ impl DataManager {
let res = query_rows!(
&conn,
&format!(
"SELECT * FROM posts WHERE replying_to = 0{}{} AND NOT context LIKE '%\"full_unlist\":true%' AND NOT topic = 0 ORDER BY created DESC LIMIT $1 OFFSET $2",
if before_time > 0 {
format!(" AND created < {before_time}")
} else {
String::new()
},
"SELECT * FROM posts WHERE replying_to = 0{} AND NOT context LIKE '%\"full_unlist\":true%' AND NOT topic = 0 ORDER BY created DESC LIMIT $1 OFFSET $2",
if hide_nsfw {
" AND NOT context LIKE '%\"is_nsfw\":true%'"
} else {
@ -1590,8 +1598,8 @@ impl DataManager {
|x| { Self::get_post_from_row(x) }
);
if res.is_err() {
return Err(Error::GeneralNotFound("post".to_string()));
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
Ok(res.unwrap())
@ -1602,12 +1610,12 @@ impl DataManager {
/// # Arguments
/// * `id` - the ID of the user
/// * `batch` - the limit of posts in each page
/// * `page` - the page number
/// * `before` - the timestamp to pull posts before
pub async fn get_posts_from_user_communities(
&self,
id: usize,
batch: usize,
page: usize,
before: usize,
user: &User,
) -> Result<Vec<Post>> {
let memberships = self.get_memberships_by_owner(id).await?;
@ -1635,20 +1643,25 @@ impl DataManager {
let res = query_rows!(
&conn,
&format!(
"SELECT * FROM posts WHERE (community = {} {query_string}){} AND NOT context LIKE '%\"full_unlist\":true%' AND replying_to = 0 AND is_deleted = 0 ORDER BY created DESC LIMIT $1 OFFSET $2",
"SELECT * FROM posts WHERE (community = {} {query_string}){}{} AND NOT context LIKE '%\"full_unlist\":true%' AND replying_to = 0 AND is_deleted = 0 ORDER BY created DESC LIMIT $1",
first.community,
if hide_nsfw {
" AND NOT context LIKE '%\"is_nsfw\":true%'"
} else {
""
},
if before > 0 {
format!(" AND created < {before}")
} else {
String::new()
}
),
&[&(batch as i64), &((page * batch) as i64)],
&[&(batch as i64)],
|x| { Self::get_post_from_row(x) }
);
if res.is_err() {
return Err(Error::GeneralNotFound("post".to_string()));
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
Ok(res.unwrap())
@ -1659,12 +1672,12 @@ impl DataManager {
/// # Arguments
/// * `id` - the ID of the user
/// * `batch` - the limit of posts in each page
/// * `page` - the page number
/// * `before` - the timestamp to pull posts before
pub async fn get_posts_from_user_following(
&self,
id: usize,
batch: usize,
page: usize,
before: usize,
) -> Result<Vec<Post>> {
let following = self.get_userfollows_by_initiator_all(id).await?;
let mut following = following.iter();
@ -1688,15 +1701,20 @@ impl DataManager {
let res = query_rows!(
&conn,
&format!(
"SELECT * FROM posts WHERE (owner = {id} OR owner = {} {query_string}) AND replying_to = 0 AND is_deleted = 0 ORDER BY created DESC LIMIT $1 OFFSET $2",
first.receiver
"SELECT * FROM posts WHERE (owner = {id} OR owner = {} {query_string}) AND replying_to = 0 AND is_deleted = 0{} ORDER BY created DESC LIMIT $1",
first.receiver,
if before > 0 {
format!(" AND created < {before}")
} else {
String::new()
}
),
&[&(batch as i64), &((page * batch) as i64)],
&[&(batch as i64)],
|x| { Self::get_post_from_row(x) }
);
if res.is_err() {
return Err(Error::GeneralNotFound("post".to_string()));
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
Ok(res.unwrap())
@ -1750,8 +1768,8 @@ impl DataManager {
|x| { Self::get_post_from_row(x) }
);
if res.is_err() {
return Err(Error::GeneralNotFound("post".to_string()));
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
Ok(res.unwrap())
@ -1830,8 +1848,15 @@ impl DataManager {
));
}
if stack.owner != data.owner && !stack.users.contains(&data.owner) {
return Err(Error::NotAllowed);
if !stack.is_locked || data.replying_to.is_some() {
if stack.owner != data.owner && !stack.users.contains(&data.owner) {
return Err(Error::NotAllowed);
}
} else {
// only the owner can post in locked stacks UNLESS we're creating a reply
if stack.owner != data.owner {
return Err(Error::NotAllowed);
}
}
}

View file

@ -23,6 +23,7 @@ impl DataManager {
privacy: serde_json::from_str(&get!(x->5(String))).unwrap(),
mode: serde_json::from_str(&get!(x->6(String))).unwrap(),
sort: serde_json::from_str(&get!(x->7(String))).unwrap(),
is_locked: get!(x->8(i32)) == 1,
}
}
@ -56,7 +57,7 @@ impl DataManager {
match stack.sort {
StackSort::Created => {
self.fill_posts_with_community(
self.get_latest_posts(batch, page, &user, 0).await?,
self.get_latest_posts(batch, &user, 0).await?,
as_user_id,
&ignore_users,
user,
@ -184,7 +185,7 @@ impl DataManager {
let res = execute!(
&conn,
"INSERT INTO stacks VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
"INSERT INTO stacks VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
params![
&(data.id as i64),
&(data.created as i64),
@ -194,6 +195,7 @@ impl DataManager {
&serde_json::to_string(&data.privacy).unwrap(),
&serde_json::to_string(&data.mode).unwrap(),
&serde_json::to_string(&data.sort).unwrap(),
&if data.is_locked { 1 } else { 0 },
]
);
@ -207,6 +209,10 @@ impl DataManager {
pub async fn delete_stack(&self, id: usize, user: &User) -> Result<()> {
let stack = self.get_stack_by_id(id).await?;
if stack.is_locked {
return Err(Error::NotAllowed);
}
// check user permission
if user.id != stack.owner && !user.permissions.check(FinePermission::MANAGE_STACKS) {
return Err(Error::NotAllowed);

View file

@ -107,6 +107,12 @@ pub struct User {
/// The time in which the user last consented to the site's policies.
#[serde(default)]
pub last_policy_consent: usize,
/// The ID of the user's close friends stack.
///
/// The user's close friends stack is a circle stack which only allows the owner
/// (the user) to post to it.
#[serde(default)]
pub close_friends_stack: usize,
}
pub type UserConnections =
@ -430,6 +436,7 @@ impl User {
checkouts: Vec::new(),
applied_configurations: Vec::new(),
last_policy_consent: created,
close_friends_stack: 0,
}
}

View file

@ -60,6 +60,9 @@ pub struct UserStack {
pub privacy: StackPrivacy,
pub mode: StackMode,
pub sort: StackSort,
/// Locked stacks cannot be deleted or have their mode changed. Stacks cannot
/// be locked after creation, and must be locked by the server.
pub is_locked: bool,
}
impl UserStack {
@ -74,6 +77,7 @@ impl UserStack {
privacy: StackPrivacy::default(),
mode: StackMode::default(),
sort: StackSort::default(),
is_locked: false,
}
}
}