diff --git a/crates/app/src/public/html/components.html b/crates/app/src/public/html/components.html
index f2bf48e..73cba83 100644
--- a/crates/app/src/public/html/components.html
+++ b/crates/app/src/public/html/components.html
@@ -227,6 +227,14 @@ and show_community and community.id != config.town_square or question %}
>
{{ icon "user-round" }}
+ {% endif %} {% if post.is_deleted %}
+
+ {{ icon "trash-2" }}
+
{% endif %}
diff --git a/crates/app/src/routes/api/v1/channels/channels.rs b/crates/app/src/routes/api/v1/channels/channels.rs
index cb8c3c1..584cc78 100644
--- a/crates/app/src/routes/api/v1/channels/channels.rs
+++ b/crates/app/src/routes/api/v1/channels/channels.rs
@@ -138,7 +138,7 @@ pub async fn update_title_request(
None => return Json(Error::NotAllowed.into()),
};
- match data.update_channel_title(id, user, &req.title).await {
+ match data.update_channel_title(id, &user, &req.title).await {
Ok(_) => Json(ApiReturn {
ok: true,
message: "Channel updated".to_string(),
@@ -160,7 +160,7 @@ pub async fn update_position_request(
None => return Json(Error::NotAllowed.into()),
};
- match data.update_channel_position(id, user, req.position).await {
+ match data.update_channel_position(id, &user, req.position).await {
Ok(_) => Json(ApiReturn {
ok: true,
message: "Channel updated".to_string(),
diff --git a/crates/app/src/routes/api/v1/communities/communities.rs b/crates/app/src/routes/api/v1/communities/communities.rs
index 1601d69..5f8688a 100644
--- a/crates/app/src/routes/api/v1/communities/communities.rs
+++ b/crates/app/src/routes/api/v1/communities/communities.rs
@@ -117,7 +117,7 @@ pub async fn update_context_request(
None => return Json(Error::NotAllowed.into()),
};
- match data.update_community_context(id, user, req.context).await {
+ match data.update_community_context(id, &user, req.context).await {
Ok(_) => Json(ApiReturn {
ok: true,
message: "Community updated".to_string(),
@@ -140,7 +140,7 @@ pub async fn update_read_access_request(
};
match data
- .update_community_read_access(id, user, req.access)
+ .update_community_read_access(id, &user, req.access)
.await
{
Ok(_) => Json(ApiReturn {
@@ -165,7 +165,7 @@ pub async fn update_write_access_request(
};
match data
- .update_community_write_access(id, user, req.access)
+ .update_community_write_access(id, &user, req.access)
.await
{
Ok(_) => Json(ApiReturn {
@@ -190,7 +190,7 @@ pub async fn update_join_access_request(
};
match data
- .update_community_join_access(id, user, req.access)
+ .update_community_join_access(id, &user, req.access)
.await
{
Ok(_) => Json(ApiReturn {
diff --git a/crates/app/src/routes/api/v1/communities/emojis.rs b/crates/app/src/routes/api/v1/communities/emojis.rs
index 2676d6c..c8d1713 100644
--- a/crates/app/src/routes/api/v1/communities/emojis.rs
+++ b/crates/app/src/routes/api/v1/communities/emojis.rs
@@ -183,7 +183,7 @@ pub async fn update_name_request(
None => return Json(Error::NotAllowed.into()),
};
- match data.update_emoji_name(id, user, &req.name).await {
+ match data.update_emoji_name(id, &user, &req.name).await {
Ok(_) => Json(ApiReturn {
ok: true,
message: "Emoji updated".to_string(),
diff --git a/crates/app/src/routes/api/v1/communities/posts.rs b/crates/app/src/routes/api/v1/communities/posts.rs
index 8f565bc..b278adf 100644
--- a/crates/app/src/routes/api/v1/communities/posts.rs
+++ b/crates/app/src/routes/api/v1/communities/posts.rs
@@ -173,6 +173,31 @@ pub async fn delete_request(
None => return Json(Error::NotAllowed.into()),
};
+ match data.fake_delete_post(id, user, true).await {
+ Ok(_) => Json(ApiReturn {
+ ok: true,
+ message: "Post deleted".to_string(),
+ payload: (),
+ }),
+ Err(e) => Json(e.into()),
+ }
+}
+
+pub async fn real_delete_request(
+ jar: CookieJar,
+ Extension(data): Extension,
+ Path(id): Path,
+) -> impl IntoResponse {
+ let data = &(data.read().await).0;
+ let user = match get_user_from_token!(jar, data) {
+ Some(ua) => ua,
+ None => return Json(Error::NotAllowed.into()),
+ };
+
+ if !user.permissions.check(FinePermission::MANAGE_POSTS) {
+ return Json(Error::NotAllowed.into());
+ }
+
match data.delete_post(id, user).await {
Ok(_) => Json(ApiReturn {
ok: true,
diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs
index 1cea099..9d35aa8 100644
--- a/crates/app/src/routes/api/v1/mod.rs
+++ b/crates/app/src/routes/api/v1/mod.rs
@@ -93,6 +93,10 @@ pub fn routes() -> Router {
// posts
.route("/posts", post(communities::posts::create_request))
.route("/posts/{id}", delete(communities::posts::delete_request))
+ .route(
+ "/posts/{id}/real_delete",
+ delete(communities::posts::real_delete_request),
+ )
.route(
"/posts/{id}/repost",
post(communities::posts::create_repost_request),
diff --git a/crates/app/src/routes/api/v1/stacks.rs b/crates/app/src/routes/api/v1/stacks.rs
index a33df1e..85ebd5a 100644
--- a/crates/app/src/routes/api/v1/stacks.rs
+++ b/crates/app/src/routes/api/v1/stacks.rs
@@ -43,7 +43,7 @@ pub async fn update_name_request(
None => return Json(Error::NotAllowed.into()),
};
- match data.update_stack_name(id, user, &req.name).await {
+ match data.update_stack_name(id, &user, &req.name).await {
Ok(_) => Json(ApiReturn {
ok: true,
message: "Stack updated".to_string(),
@@ -65,7 +65,7 @@ pub async fn update_privacy_request(
None => return Json(Error::NotAllowed.into()),
};
- match data.update_stack_privacy(id, user, req.privacy).await {
+ match data.update_stack_privacy(id, &user, req.privacy).await {
Ok(_) => Json(ApiReturn {
ok: true,
message: "Stack updated".to_string(),
@@ -87,7 +87,7 @@ pub async fn update_mode_request(
None => return Json(Error::NotAllowed.into()),
};
- match data.update_stack_mode(id, user, req.mode).await {
+ match data.update_stack_mode(id, &user, req.mode).await {
Ok(_) => Json(ApiReturn {
ok: true,
message: "Stack updated".to_string(),
@@ -109,7 +109,7 @@ pub async fn update_sort_request(
None => return Json(Error::NotAllowed.into()),
};
- match data.update_stack_sort(id, user, req.sort).await {
+ match data.update_stack_sort(id, &user, req.sort).await {
Ok(_) => Json(ApiReturn {
ok: true,
message: "Stack updated".to_string(),
@@ -152,7 +152,7 @@ pub async fn add_user_request(
};
stack.users.push(other_user.id);
- match data.update_stack_users(id, user, stack.users).await {
+ match data.update_stack_users(id, &user, stack.users).await {
Ok(_) => Json(ApiReturn {
ok: true,
message: "User added".to_string(),
@@ -191,7 +191,7 @@ pub async fn remove_user_request(
None => return Json(Error::GeneralNotFound("user".to_string()).into()),
});
- match data.update_stack_users(id, user, stack.users).await {
+ match data.update_stack_users(id, &user, stack.users).await {
Ok(_) => Json(ApiReturn {
ok: true,
message: "User removed".to_string(),
diff --git a/crates/app/src/routes/pages/communities.rs b/crates/app/src/routes/pages/communities.rs
index e6949ff..8de6c41 100644
--- a/crates/app/src/routes/pages/communities.rs
+++ b/crates/app/src/routes/pages/communities.rs
@@ -589,6 +589,33 @@ pub async fn post_request(
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
};
+ if post.is_deleted {
+ // act like the post doesn't exist (if missing MANAGE_POSTS)
+ if let Some(ref ua) = user {
+ if !ua.permissions.check(FinePermission::MANAGE_POSTS) {
+ return Err(Html(
+ render_error(
+ Error::GeneralNotFound("post".to_string()),
+ &jar,
+ &data,
+ &user,
+ )
+ .await,
+ ));
+ }
+ } else {
+ return Err(Html(
+ render_error(
+ Error::GeneralNotFound("post".to_string()),
+ &jar,
+ &data,
+ &user,
+ )
+ .await,
+ ));
+ }
+ }
+
let community = match data.0.get_community_by_id(post.community).await {
Ok(c) => c,
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
diff --git a/crates/app/src/routes/pages/stacks.rs b/crates/app/src/routes/pages/stacks.rs
index e103636..52cb44d 100644
--- a/crates/app/src/routes/pages/stacks.rs
+++ b/crates/app/src/routes/pages/stacks.rs
@@ -120,15 +120,29 @@ pub async fn manage_request(
));
}
+ let mut new_users = stack.users.clone();
+ let mut changed_stack_users: bool = false;
+
let mut users: Vec = Vec::new();
for uid in &stack.users {
users.push(match data.0.get_user_by_id(uid.to_owned()).await {
Ok(ua) => ua,
- Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
+ Err(_) => {
+ // user deleted profile, remove from list
+ new_users.remove(stack.users.iter().position(|x| x == uid).unwrap());
+ changed_stack_users = true;
+ continue;
+ }
});
}
+ if changed_stack_users {
+ if let Err(e) = data.0.update_stack_users(stack.id, &user, new_users).await {
+ return Err(Html(render_error(e, &jar, &data, &None).await));
+ }
+ }
+
let lang = get_lang!(jar, data.0);
let mut context = initial_context(&data.0.0, lang, &Some(user)).await;
diff --git a/crates/core/src/database/common.rs b/crates/core/src/database/common.rs
index a500067..399005b 100644
--- a/crates/core/src/database/common.rs
+++ b/crates/core/src/database/common.rs
@@ -69,7 +69,9 @@ macro_rules! auto_method {
($name:ident()@$select_fn:ident -> $query:literal --name=$name_:literal --returns=$returns_:tt --cache-key-tmpl=$cache_key_tmpl:literal) => {
pub async fn $name(&self, id: usize) -> Result<$returns_> {
if let Some(cached) = self.2.get(format!($cache_key_tmpl, id)).await {
- return Ok(serde_json::from_str(&cached).unwrap());
+ if let Ok(c) = serde_json::from_str(&cached) {
+ return Ok(c);
+ }
}
let conn = match self.connect().await {
@@ -183,7 +185,7 @@ macro_rules! auto_method {
};
($name:ident()@$select_fn:ident:$permission:ident -> $query:literal) => {
- pub async fn $name(&self, id: usize, user: User) -> Result<()> {
+ pub async fn $name(&self, id: usize, user: &User) -> Result<()> {
let y = self.$select_fn(id).await?;
if user.id != y.owner {
@@ -213,7 +215,7 @@ macro_rules! auto_method {
};
($name:ident()@$select_fn:ident:$permission:ident -> $query:literal --cache-key-tmpl=$cache_key_tmpl:literal) => {
- pub async fn $name(&self, id: usize, user: User) -> Result<()> {
+ pub async fn $name(&self, id: usize, user: &User) -> Result<()> {
let y = self.$select_fn(id).await?;
if user.id != y.owner {
@@ -245,7 +247,7 @@ macro_rules! auto_method {
};
($name:ident($x:ty)@$select_fn:ident:$permission:ident -> $query:literal) => {
- pub async fn $name(&self, id: usize, user: User, x: $x) -> Result<()> {
+ pub async fn $name(&self, id: usize, user: &User, x: $x) -> Result<()> {
let y = self.$select_fn(id).await?;
if user.id != y.owner {
@@ -276,7 +278,7 @@ macro_rules! auto_method {
};
($name:ident($x:ty)@$select_fn:ident:$permission:ident -> $query:literal --cache-key-tmpl=$cache_key_tmpl:literal) => {
- pub async fn $name(&self, id: usize, user: User, x: $x) -> Result<()> {
+ pub async fn $name(&self, id: usize, user: &User, x: $x) -> Result<()> {
let y = self.$select_fn(id).await?;
if user.id != y.owner {
@@ -309,7 +311,7 @@ macro_rules! auto_method {
};
($name:ident($x:ty)@$select_fn:ident:$permission:ident -> $query:literal --serde) => {
- pub async fn $name(&self, id: usize, user: User, x: $x) -> Result<()> {
+ pub async fn $name(&self, id: usize, user: &User, x: $x) -> Result<()> {
let y = self.$select_fn(id).await?;
if user.id != y.owner {
@@ -344,7 +346,7 @@ macro_rules! auto_method {
};
($name:ident($x:ty)@$select_fn:ident:$permission:ident -> $query:literal --serde --cache-key-tmpl=$cache_key_tmpl:literal) => {
- pub async fn $name(&self, id: usize, user: User, x: $x) -> Result<()> {
+ pub async fn $name(&self, id: usize, user: &User, x: $x) -> Result<()> {
let y = self.$select_fn(id).await?;
if user.id != y.owner {
@@ -499,7 +501,7 @@ macro_rules! auto_method {
};
($name:ident()@$select_fn:ident:$permission:ident -> $query:literal --cache-key-tmpl=$cache_key_tmpl:ident) => {
- pub async fn $name(&self, id: usize, user: User) -> Result<()> {
+ pub async fn $name(&self, id: usize, user: &User) -> Result<()> {
let y = self.$select_fn(id).await?;
if user.id != y.owner {
@@ -532,7 +534,7 @@ macro_rules! auto_method {
};
($name:ident($x:ty)@$select_fn:ident:$permission:ident -> $query:literal --cache-key-tmpl=$cache_key_tmpl:ident) => {
- pub async fn $name(&self, id: usize, user: User, x: $x) -> Result<()> {
+ pub async fn $name(&self, id: usize, user: &User, x: $x) -> Result<()> {
let y = self.$select_fn(id).await?;
if user.id != y.owner {
@@ -611,7 +613,7 @@ macro_rules! auto_method {
};
($name:ident($x:ty)@$select_fn:ident:$permission:ident -> $query:literal --serde --cache-key-tmpl=$cache_key_tmpl:ident) => {
- pub async fn $name(&self, id: usize, user: User, x: $x) -> Result<()> {
+ pub async fn $name(&self, id: usize, user: &User, x: $x) -> Result<()> {
let y = self.$select_fn(id).await?;
if user.id != y.owner {
diff --git a/crates/core/src/database/drivers/sql/create_posts.sql b/crates/core/src/database/drivers/sql/create_posts.sql
index ab0bc6f..76350d7 100644
--- a/crates/core/src/database/drivers/sql/create_posts.sql
+++ b/crates/core/src/database/drivers/sql/create_posts.sql
@@ -12,5 +12,6 @@ CREATE TABLE IF NOT EXISTS posts (
-- other counts
comment_count INT NOT NULL,
-- ...
- uploads TEXT NOT NULL
+ uploads TEXT NOT NULL,
+ is_deleted INT NOT NULL
)
diff --git a/crates/core/src/database/posts.rs b/crates/core/src/database/posts.rs
index dfa279b..99bb4b8 100644
--- a/crates/core/src/database/posts.rs
+++ b/crates/core/src/database/posts.rs
@@ -42,6 +42,7 @@ impl DataManager {
comment_count: get!(x->9(i32)) as usize,
// ...
uploads: serde_json::from_str(&get!(x->10(String))).unwrap(),
+ is_deleted: get!(x->11(i32)) as i8 == 1,
}
}
@@ -195,6 +196,8 @@ impl DataManager {
// check relationship
if ua.settings.private_profile {
+ // if someone were to look for places to optimize memory usage,
+ // look no further than here
if let Some(ua1) = user {
if ua1.id == 0 {
continue;
@@ -202,7 +205,10 @@ impl DataManager {
if let Some(is_following) = seen_user_follow_statuses.get(&(ua.id, ua1.id))
{
- if !is_following && (ua.id != ua1.id) {
+ if !is_following
+ && (ua.id != ua1.id)
+ && !ua1.permissions.check(FinePermission::MANAGE_POSTS)
+ {
// post owner is not following us
continue;
}
@@ -211,6 +217,7 @@ impl DataManager {
.get_userfollow_by_initiator_receiver(ua.id, ua1.id)
.await
.is_err()
+ && !ua1.permissions.check(FinePermission::MANAGE_POSTS)
{
// post owner is not following us
seen_user_follow_statuses.insert((ua.id, ua1.id), false);
@@ -1105,7 +1112,7 @@ impl DataManager {
let res = execute!(
&conn,
- "INSERT INTO posts VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)",
+ "INSERT INTO posts VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)",
params![
&(data.id as i64),
&(data.created as i64),
@@ -1121,7 +1128,8 @@ impl DataManager {
&0_i32,
&0_i32,
&0_i32,
- &serde_json::to_string(&data.uploads).unwrap()
+ &serde_json::to_string(&data.uploads).unwrap(),
+ &{ if data.is_deleted { 1 } else { 0 } }
]
);
@@ -1212,7 +1220,7 @@ impl DataManager {
let question = self.get_question_by_id(y.context.answering).await?;
if question.is_global {
- self.incr_question_answer_count(y.context.answering).await?;
+ self.decr_question_answer_count(y.context.answering).await?;
}
}
@@ -1225,6 +1233,94 @@ impl DataManager {
Ok(())
}
+ pub async fn fake_delete_post(&self, id: usize, user: User, is_deleted: bool) -> Result<()> {
+ let y = self.get_post_by_id(id).await?;
+
+ let user_membership = self
+ .get_membership_by_owner_community(user.id, y.community)
+ .await?;
+
+ if (user.id != y.owner)
+ && !user_membership
+ .role
+ .check(CommunityPermission::MANAGE_POSTS)
+ {
+ if !user.permissions.check(FinePermission::MANAGE_POSTS) {
+ return Err(Error::NotAllowed);
+ } else {
+ self.create_audit_log_entry(AuditLogEntry::new(
+ user.id,
+ format!("invoked `fake_delete_post` with x value `{id}`"),
+ ))
+ .await?
+ }
+ }
+
+ let conn = match self.connect().await {
+ Ok(c) => c,
+ Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
+ };
+
+ let res = execute!(
+ &conn,
+ "UPDATE posts SET is_deleted = $1 WHERE id = $2",
+ params![if is_deleted { 1 } else { 0 }, &(id as i64)]
+ );
+
+ if let Err(e) = res {
+ return Err(Error::DatabaseError(e.to_string()));
+ }
+
+ self.2.remove(format!("atto.post:{}", id)).await;
+
+ if is_deleted {
+ // decr parent comment count
+ if let Some(replying_to) = y.replying_to {
+ self.decr_post_comments(replying_to).await.unwrap();
+ }
+
+ // decr user post count
+ let owner = self.get_user_by_id(y.owner).await?;
+
+ if owner.post_count > 0 {
+ self.decr_user_post_count(y.owner).await?;
+ }
+
+ // decr question answer count
+ if y.context.answering != 0 {
+ let question = self.get_question_by_id(y.context.answering).await?;
+
+ if question.is_global {
+ self.incr_question_answer_count(y.context.answering).await?;
+ }
+ }
+ } else {
+ // incr parent comment count
+ if let Some(replying_to) = y.replying_to {
+ self.incr_post_comments(replying_to).await.unwrap();
+ }
+
+ // incr user post count
+ let owner = self.get_user_by_id(y.owner).await?;
+
+ if owner.post_count > 0 {
+ self.incr_user_post_count(y.owner).await?;
+ }
+
+ // incr question answer count
+ if y.context.answering != 0 {
+ let question = self.get_question_by_id(y.context.answering).await?;
+
+ if question.is_global {
+ self.decr_question_answer_count(y.context.answering).await?;
+ }
+ }
+ }
+
+ // return
+ Ok(())
+ }
+
pub async fn update_post_context(
&self,
id: usize,
diff --git a/crates/core/src/model/communities.rs b/crates/core/src/model/communities.rs
index 1e7094e..886dfd0 100644
--- a/crates/core/src/model/communities.rs
+++ b/crates/core/src/model/communities.rs
@@ -238,6 +238,8 @@ pub struct Post {
pub comment_count: usize,
/// IDs of all uploads linked to this post.
pub uploads: Vec,
+ /// If the post was deleted.
+ pub is_deleted: bool,
}
impl Post {
@@ -260,6 +262,7 @@ impl Post {
dislikes: 0,
comment_count: 0,
uploads: Vec::new(),
+ is_deleted: false,
}
}
diff --git a/sql_changes/posts_is_deleted.sql b/sql_changes/posts_is_deleted.sql
new file mode 100644
index 0000000..01207f2
--- /dev/null
+++ b/sql_changes/posts_is_deleted.sql
@@ -0,0 +1,2 @@
+ALTER TABLE posts
+ADD COLUMN is_deleted INT NOT NULL DEFAULT 0;