add: circle stacks

This commit is contained in:
trisua 2025-06-15 16:09:02 -04:00
parent 50704d27a9
commit 56cea83933
27 changed files with 419 additions and 107 deletions

View file

@ -5,7 +5,7 @@ use crate::model::auth::Notification;
use crate::model::communities::{Poll, Question};
use crate::model::communities_permissions::CommunityPermission;
use crate::model::moderation::AuditLogEntry;
use crate::model::stacks::StackSort;
use crate::model::stacks::{StackMode, StackSort, UserStack};
use crate::model::{
Error, Result,
auth::User,
@ -25,6 +25,7 @@ pub type FullPost = (
Option<(User, Post)>,
Option<(Question, User)>,
Option<(Poll, bool, bool)>,
Option<UserStack>,
);
macro_rules! private_post_replying {
@ -114,7 +115,7 @@ impl DataManager {
poll_id: get!(x->13(i64)) as usize,
title: get!(x->14(String)),
is_open: get!(x->15(i32)) as i8 == 1,
circle: get!(x->16(i64)) as usize,
stack: get!(x->16(i64)) as usize,
}
}
@ -275,6 +276,39 @@ impl DataManager {
}
}
/// Get the stack of the given post (if some).
///
/// # Returns
/// `(can view post, stack)`
pub async fn get_post_stack(
&self,
seen_stacks: &mut HashMap<usize, UserStack>,
post: &Post,
as_user_id: usize,
) -> (bool, Option<UserStack>) {
if post.stack != 0 {
if let Some(s) = seen_stacks.get(&post.stack) {
(
(s.owner == as_user_id) | s.users.contains(&as_user_id),
Some(s.to_owned()),
)
} else {
let s = match self.get_stack_by_id(post.stack).await {
Ok(s) => s,
Err(_) => return (true, None),
};
seen_stacks.insert(s.id, s.to_owned());
(
(s.owner == as_user_id) | s.users.contains(&as_user_id),
Some(s.to_owned()),
)
}
} else {
(true, None)
}
}
/// Complete a vector of just posts with their owner as well.
pub async fn fill_posts(
&self,
@ -288,12 +322,14 @@ impl DataManager {
Option<(User, Post)>,
Option<(Question, User)>,
Option<(Poll, bool, bool)>,
Option<UserStack>,
)>,
> {
let mut out = Vec::new();
let mut users: HashMap<usize, User> = HashMap::new();
let mut seen_user_follow_statuses: HashMap<(usize, usize), bool> = HashMap::new();
let mut seen_stacks: HashMap<usize, UserStack> = HashMap::new();
let mut replying_posts: HashMap<usize, Post> = HashMap::new();
for post in posts {
@ -304,12 +340,25 @@ impl DataManager {
let owner = post.owner;
if let Some(ua) = users.get(&owner) {
let (can_view, stack) = self
.get_post_stack(
&mut seen_stacks,
&post,
if let Some(ua) = user { ua.id } else { 0 },
)
.await;
if !can_view {
continue;
}
out.push((
post.clone(),
ua.clone(),
self.get_post_reposting(&post, ignore_users, user).await,
self.get_post_question(&post, ignore_users).await?,
self.get_post_poll(&post, user).await?,
stack,
));
} else {
let ua = self.get_user_by_id(owner).await?;
@ -357,6 +406,18 @@ impl DataManager {
}
}
let (can_view, stack) = self
.get_post_stack(
&mut seen_stacks,
&post,
if let Some(ua) = user { ua.id } else { 0 },
)
.await;
if !can_view {
continue;
}
// ...
users.insert(owner, ua.clone());
out.push((
@ -365,6 +426,7 @@ impl DataManager {
self.get_post_reposting(&post, ignore_users, user).await,
self.get_post_question(&post, ignore_users).await?,
self.get_post_poll(&post, user).await?,
stack,
));
}
}
@ -384,6 +446,7 @@ impl DataManager {
let mut seen_before: HashMap<(usize, usize), (User, Community)> = HashMap::new();
let mut seen_user_follow_statuses: HashMap<(usize, usize), bool> = HashMap::new();
let mut seen_stacks: HashMap<usize, UserStack> = HashMap::new();
let mut replying_posts: HashMap<usize, Post> = HashMap::new();
for post in posts {
@ -395,6 +458,18 @@ impl DataManager {
let community = post.community;
if let Some((ua, community)) = seen_before.get(&(owner, community)) {
let (can_view, stack) = self
.get_post_stack(
&mut seen_stacks,
&post,
if let Some(ua) = user { ua.id } else { 0 },
)
.await;
if !can_view {
continue;
}
out.push((
post.clone(),
ua.clone(),
@ -402,6 +477,7 @@ impl DataManager {
self.get_post_reposting(&post, ignore_users, user).await,
self.get_post_question(&post, ignore_users).await?,
self.get_post_poll(&post, user).await?,
stack,
));
} else {
let ua = self.get_user_by_id(owner).await?;
@ -440,6 +516,18 @@ impl DataManager {
}
}
let (can_view, stack) = self
.get_post_stack(
&mut seen_stacks,
&post,
if let Some(ua) = user { ua.id } else { 0 },
)
.await;
if !can_view {
continue;
}
// ...
let community = self.get_community_by_id(community).await?;
seen_before.insert((owner, community.id), (ua.clone(), community.clone()));
@ -450,6 +538,7 @@ impl DataManager {
self.get_post_reposting(&post, ignore_users, user).await,
self.get_post_question(&post, ignore_users).await?,
self.get_post_poll(&post, user).await?,
stack,
));
}
}
@ -933,6 +1022,37 @@ impl DataManager {
Ok(res.unwrap())
}
/// Get all posts from the given stack (from most recent).
///
/// # Arguments
/// * `id` - the ID of the stack the requested posts belong to
/// * `batch` - the limit of posts in each page
/// * `page` - the page number
pub async fn get_posts_by_stack(
&self,
id: usize,
batch: usize,
page: usize,
) -> Result<Vec<Post>> {
let conn = match self.0.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
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)],
|x| { Self::get_post_from_row(x) }
);
if res.is_err() {
return Err(Error::GeneralNotFound("post".to_string()));
}
Ok(res.unwrap())
}
/// Get all pinned posts from the given community (from most recent).
///
/// # Arguments
@ -1370,7 +1490,30 @@ impl DataManager {
}
}
let community = self.get_community_by_id(data.community).await?;
// check stack
if data.stack != 0 {
let stack = self.get_stack_by_id(data.stack).await?;
if stack.mode != StackMode::Circle {
return Err(Error::MiscError(
"You must use a \"Circle\" stack for this".to_string(),
));
}
if stack.owner != data.owner && !stack.users.contains(&data.owner) {
return Err(Error::NotAllowed);
}
}
// ...
let community = if data.stack != 0 {
// if we're posting to a stack, the community should always be the town square
data.community = self.0.0.town_square;
self.get_community_by_id(self.0.0.town_square).await?
} else {
// otherwise, load whatever community the post is requesting
self.get_community_by_id(data.community).await?
};
// check values (if this isn't reposting something else)
let is_reposting = if let Some(ref repost) = data.context.repost {
@ -1466,6 +1609,10 @@ impl DataManager {
};
if let Some(ref rt) = reposting {
if rt.stack != data.stack && rt.stack != 0 {
return Err(Error::MiscError("Cannot repost out of stack".to_string()));
}
if data.content.is_empty() {
// reposting but NOT quoting... we shouldn't be able to repost a direct repost
data.context.reposts_enabled = false;
@ -1507,7 +1654,7 @@ impl DataManager {
// send notification
// this would look better if rustfmt didn't give up on this line
if owner.id != rt.owner && !owner.settings.private_profile {
if owner.id != rt.owner && !owner.settings.private_profile && data.stack == 0 {
self.create_notification(
Notification::new(
format!(
@ -1631,7 +1778,7 @@ impl DataManager {
&(data.poll_id as i64),
&data.title,
&{ if data.is_open { 1 } else { 0 } },
&(data.circle as i64),
&(data.stack as i64),
]
);
@ -1781,7 +1928,7 @@ impl DataManager {
let res = execute!(
&conn,
"UPDATE posts SET is_deleted = $1 WHERE id = $2",
params![if is_deleted { 1 } else { 0 }, &(id as i64)]
params![&if is_deleted { 1 } else { 0 }, &(id as i64)]
);
if let Err(e) = res {
@ -1793,7 +1940,9 @@ impl DataManager {
if is_deleted {
// decr parent comment count
if let Some(replying_to) = y.replying_to {
self.decr_post_comments(replying_to).await.unwrap();
if replying_to != 0 {
self.decr_post_comments(replying_to).await.unwrap();
}
}
// decr user post count
@ -1893,7 +2042,7 @@ impl DataManager {
let res = execute!(
&conn,
"UPDATE posts SET is_open = $1 WHERE id = $2",
params![if is_open { 1 } else { 0 }, &(id as i64)]
params![&if is_open { 1 } else { 0 }, &(id as i64)]
);
if let Err(e) = res {
@ -2091,5 +2240,5 @@ impl DataManager {
auto_method!(decr_post_dislikes() -> "UPDATE posts SET dislikes = dislikes - 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --decr);
auto_method!(incr_post_comments() -> "UPDATE posts SET comment_count = comment_count + 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --incr);
auto_method!(decr_post_comments() -> "UPDATE posts SET comment_count = comment_count - 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --decr);
auto_method!(decr_post_comments()@get_post_by_id -> "UPDATE posts SET comment_count = comment_count - 1 WHERE id = $1" --cache-key-tmpl="atto.post:{}" --decr=comment_count);
}