mirror of
https://github.com/LemmyNet/lemmy.git
synced 2026-01-05 21:30:27 -06:00
Comment lock (#5916)
* Add comment lock * Add test * Update tests + error types * fmt * Add missing files * JSON fmt * Fix tests * SQL fmt * Address comments * Trying to fix `locked` 1 * Fixing lemmy-js-client dep. * Fix migration time. * Creating an update_locked_for_comment_and_children helper function --------- Co-authored-by: Dessalines <dessalines@users.noreply.github.com> Co-authored-by: Dessalines <tyhou13@gmx.com>
This commit is contained in:
@@ -31,7 +31,7 @@
|
||||
"eslint-plugin-prettier": "^5.5.0",
|
||||
"jest": "^30.0.0",
|
||||
"joi": "^17.13.3",
|
||||
"lemmy-js-client": "1.0.0-instance-user-blocking.8",
|
||||
"lemmy-js-client": "1.0.0-comment-lock.0",
|
||||
"prettier": "^3.5.3",
|
||||
"ts-jest": "^29.4.0",
|
||||
"tsoa": "^6.6.0",
|
||||
|
||||
10
api_tests/pnpm-lock.yaml
generated
10
api_tests/pnpm-lock.yaml
generated
@@ -36,8 +36,8 @@ importers:
|
||||
specifier: ^17.13.3
|
||||
version: 17.13.3
|
||||
lemmy-js-client:
|
||||
specifier: 1.0.0-instance-user-blocking.8
|
||||
version: 1.0.0-instance-user-blocking.8
|
||||
specifier: 1.0.0-comment-lock.0
|
||||
version: 1.0.0-comment-lock.0
|
||||
prettier:
|
||||
specifier: ^3.5.3
|
||||
version: 3.5.3
|
||||
@@ -1637,8 +1637,8 @@ packages:
|
||||
keyv@4.5.4:
|
||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||
|
||||
lemmy-js-client@1.0.0-instance-user-blocking.8:
|
||||
resolution: {integrity: sha512-hJhb5lJK4iDPb7K2MbodEFT0pliO5mwyL67YqE/opnkUo/LibdsbkSn3Himg6AcdnGv9lWnOUnZbwxrIvGLB1g==}
|
||||
lemmy-js-client@1.0.0-comment-lock.0:
|
||||
resolution: {integrity: sha512-F9Ui5eYuw0A5+Wcw+DUpatGwxsdjtJsJ9T9ptkkuYAVzoEgjXbjVNStdRe7Om0AzKLfVurguoWMj+3s04Hc8nw==}
|
||||
|
||||
leven@3.1.0:
|
||||
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
|
||||
@@ -4327,7 +4327,7 @@ snapshots:
|
||||
dependencies:
|
||||
json-buffer: 3.0.1
|
||||
|
||||
lemmy-js-client@1.0.0-instance-user-blocking.8:
|
||||
lemmy-js-client@1.0.0-comment-lock.0:
|
||||
dependencies:
|
||||
'@tsoa/runtime': 6.6.0
|
||||
transitivePeerDependencies:
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
waitUntil,
|
||||
waitForPost,
|
||||
alphaUrl,
|
||||
betaUrl,
|
||||
followCommunity,
|
||||
blockCommunity,
|
||||
delay,
|
||||
@@ -37,6 +38,7 @@ import {
|
||||
listReports,
|
||||
listPersonContent,
|
||||
listNotifications,
|
||||
lockComment,
|
||||
} from "./shared";
|
||||
import {
|
||||
CommentReportView,
|
||||
@@ -903,6 +905,71 @@ test("Distinguish comment", async () => {
|
||||
assertCommentFederation(alphaComments.comments[0], commentRes.comment_view);
|
||||
});
|
||||
|
||||
test("Lock comment", async () => {
|
||||
let newBetaApi = await registerUser(beta, betaUrl);
|
||||
|
||||
const alphaCommunity = await resolveCommunity(
|
||||
alpha,
|
||||
"!main@lemmy-alpha:8541",
|
||||
);
|
||||
if (!alphaCommunity) {
|
||||
throw "Missing alpha community";
|
||||
}
|
||||
|
||||
let post = await createPost(alpha, alphaCommunity.community.id);
|
||||
let betaPost = await resolvePost(beta, post.post_view.post);
|
||||
|
||||
if (!betaPost) {
|
||||
throw "unable to locate post on beta";
|
||||
}
|
||||
|
||||
// Create a comment hierarchy like this:
|
||||
// 1
|
||||
// | \
|
||||
// 2 4
|
||||
// |
|
||||
// 3
|
||||
|
||||
let comment1 = await createComment(alpha, post.post_view.post.id);
|
||||
let betaComment1 = await resolveComment(beta, comment1.comment_view.comment);
|
||||
if (!betaComment1) {
|
||||
throw "unable to locate comment on beta";
|
||||
}
|
||||
|
||||
let comment2 = await createComment(
|
||||
alpha,
|
||||
post.post_view.post.id,
|
||||
comment1.comment_view.comment.id,
|
||||
);
|
||||
let betaComment2 = await resolveComment(beta, comment2.comment_view.comment);
|
||||
if (!betaComment2) {
|
||||
throw "unable to locate comment on beta";
|
||||
}
|
||||
let comment3 = await createComment(
|
||||
newBetaApi,
|
||||
betaPost.post.id,
|
||||
betaComment2.comment.id,
|
||||
);
|
||||
|
||||
// Lock comment2 and wait for it to federate
|
||||
await lockComment(alpha, true, comment2.comment_view.comment);
|
||||
await delay();
|
||||
|
||||
// Make sure newBeta can't respond to comment3
|
||||
await expect(
|
||||
createComment(
|
||||
newBetaApi,
|
||||
betaPost.post.id,
|
||||
comment3.comment_view.comment.id,
|
||||
),
|
||||
).rejects.toStrictEqual(new LemmyError("locked"));
|
||||
|
||||
// newBeta should still be able to respond to comment1
|
||||
await expect(
|
||||
createComment(newBetaApi, betaPost.post.id, betaComment1.comment.id),
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
function checkCommentReportReason(rcv: ReportCombinedView, reason: string) {
|
||||
switch (rcv.type_) {
|
||||
case "Comment":
|
||||
|
||||
@@ -43,6 +43,7 @@ import { GetComments } from "lemmy-js-client/dist/types/GetComments";
|
||||
import { GetCommentsResponse } from "lemmy-js-client/dist/types/GetCommentsResponse";
|
||||
import { GetPost } from "lemmy-js-client/dist/types/GetPost";
|
||||
import { GetPostResponse } from "lemmy-js-client/dist/types/GetPostResponse";
|
||||
import { LockComment } from "lemmy-js-client/dist/types/LockComment";
|
||||
import { LockPost } from "lemmy-js-client/dist/types/LockPost";
|
||||
import { Login } from "lemmy-js-client/dist/types/Login";
|
||||
import { Post } from "lemmy-js-client/dist/types/Post";
|
||||
@@ -364,6 +365,18 @@ export async function getPost(
|
||||
return api.getPost(form);
|
||||
}
|
||||
|
||||
export async function lockComment(
|
||||
api: LemmyHttp,
|
||||
locked: boolean,
|
||||
comment: Comment,
|
||||
): Promise<CommentResponse> {
|
||||
let form: LockComment = {
|
||||
comment_id: comment.id,
|
||||
locked,
|
||||
};
|
||||
return api.lockComment(form);
|
||||
}
|
||||
|
||||
export async function getComments(
|
||||
api: LemmyHttp,
|
||||
post_id?: number,
|
||||
|
||||
77
crates/api/api/src/comment/lock.rs
Normal file
77
crates/api/api/src/comment/lock.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use activitypub_federation::config::Data;
|
||||
use actix_web::web::Json;
|
||||
use lemmy_api_utils::{
|
||||
build_response::build_comment_response,
|
||||
context::LemmyContext,
|
||||
send_activity::{ActivityChannel, SendActivityData},
|
||||
utils::check_community_mod_action,
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
comment::Comment,
|
||||
mod_log::moderator::{ModLockComment, ModLockCommentForm},
|
||||
},
|
||||
traits::Crud,
|
||||
};
|
||||
use lemmy_db_views_comment::{
|
||||
api::{CommentResponse, LockComment},
|
||||
CommentView,
|
||||
};
|
||||
use lemmy_db_views_local_user::LocalUserView;
|
||||
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
||||
|
||||
pub async fn lock_comment(
|
||||
data: Json<LockComment>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<CommentResponse>> {
|
||||
let comment_id = data.comment_id;
|
||||
let local_instance_id = local_user_view.person.instance_id;
|
||||
let locked = data.locked;
|
||||
|
||||
let orig_comment =
|
||||
CommentView::read(&mut context.pool(), comment_id, None, local_instance_id).await?;
|
||||
|
||||
check_community_mod_action(
|
||||
&local_user_view,
|
||||
&orig_comment.community,
|
||||
false,
|
||||
&mut context.pool(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let comments = Comment::update_locked_for_comment_and_children(
|
||||
&mut context.pool(),
|
||||
&orig_comment.comment.path,
|
||||
locked,
|
||||
)
|
||||
.await?;
|
||||
let comment = comments.first().ok_or(LemmyErrorType::NotFound)?;
|
||||
|
||||
let form = ModLockCommentForm {
|
||||
mod_person_id: local_user_view.person.id,
|
||||
comment_id: data.comment_id,
|
||||
locked: Some(locked),
|
||||
reason: data.reason.clone(),
|
||||
};
|
||||
ModLockComment::create(&mut context.pool(), &form).await?;
|
||||
|
||||
ActivityChannel::submit_activity(
|
||||
SendActivityData::LockComment(
|
||||
comment.clone(),
|
||||
local_user_view.person.clone(),
|
||||
data.locked,
|
||||
data.reason.clone(),
|
||||
),
|
||||
&context,
|
||||
)?;
|
||||
|
||||
build_comment_response(
|
||||
&context,
|
||||
comment_id,
|
||||
local_user_view.into(),
|
||||
local_instance_id,
|
||||
)
|
||||
.await
|
||||
.map(Json)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod distinguish;
|
||||
pub mod like;
|
||||
pub mod list_comment_likes;
|
||||
pub mod lock;
|
||||
pub mod save;
|
||||
|
||||
@@ -67,14 +67,6 @@ pub async fn create_comment(
|
||||
check_community_user_action(&local_user_view, &post_view.community, &mut context.pool()).await?;
|
||||
check_post_deleted_or_removed(&post)?;
|
||||
|
||||
// Check if post is locked, no new comments
|
||||
let is_mod_or_admin = is_mod_or_admin(&mut context.pool(), &local_user_view, community_id)
|
||||
.await
|
||||
.is_ok();
|
||||
if post.locked && !is_mod_or_admin {
|
||||
Err(LemmyErrorType::Locked)?
|
||||
}
|
||||
|
||||
// Fetch the parent, if it exists
|
||||
let parent_opt = if let Some(parent_id) = data.parent_id {
|
||||
Comment::read(&mut context.pool(), parent_id).await.ok()
|
||||
@@ -82,6 +74,17 @@ pub async fn create_comment(
|
||||
None
|
||||
};
|
||||
|
||||
// Check if post or parent is locked, no new comments
|
||||
let is_mod_or_admin = is_mod_or_admin(&mut context.pool(), &local_user_view, community_id)
|
||||
.await
|
||||
.is_ok();
|
||||
// We only need to check the parent comment here as when we lock a
|
||||
// comment we also lock all of its children.
|
||||
let locked = post.locked || parent_opt.as_ref().is_some_and(|p| p.locked);
|
||||
if locked && !is_mod_or_admin {
|
||||
Err(LemmyErrorType::Locked)?
|
||||
}
|
||||
|
||||
// If there's a parent_id, check to make sure that comment is in that post
|
||||
// Strange issue where sometimes the post ID of the parent comment is incorrect
|
||||
if let Some(parent) = parent_opt.as_ref() {
|
||||
|
||||
@@ -57,6 +57,7 @@ pub enum SendActivityData {
|
||||
community: Community,
|
||||
reason: Option<String>,
|
||||
},
|
||||
LockComment(Comment, Person, bool, Option<String>),
|
||||
LikePostOrComment {
|
||||
object_id: DbUrl,
|
||||
actor: Person,
|
||||
|
||||
@@ -1129,6 +1129,7 @@ mod tests {
|
||||
report_count: 0,
|
||||
unresolved_report_count: 0,
|
||||
federation_pending: false,
|
||||
locked: false,
|
||||
};
|
||||
assert!(check_comment_depth(&comment).is_ok());
|
||||
comment.path = Ltree("0.123.456".to_string());
|
||||
|
||||
12
crates/apub/assets/lemmy/activities/community/lock_note.json
Normal file
12
crates/apub/assets/lemmy/activities/community/lock_note.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"actor": "https://lemmy-alpha/u/lemmy_aplha",
|
||||
"to": [
|
||||
"https://lemmy-alpha/c/test",
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"object": "https://lemmy-alpha/comment/1",
|
||||
"cc": ["https://lemmy-alpha/c/test"],
|
||||
"type": "Lock",
|
||||
"id": "https://lemmy-alpha/activities/lock/ae02478a-c7fa-4cc9-9838-eae131d3e9fa",
|
||||
"summary": "A reason for a lock"
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"id": "https://lemmy-alpha/activities/undo/8c0a65ff-eea6-47cf-9025-6b94a86252ff",
|
||||
"actor": "https://lemmy-alpha/u/lemmy_aplha",
|
||||
"to": [
|
||||
"https://lemmy-alpha/c/test",
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"object": {
|
||||
"actor": "https://lemmy-alpha/u/lemmy_aplha",
|
||||
"to": [
|
||||
"https://lemmy-alpha/c/test",
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"object": "https://lemmy-alpha/comment/1",
|
||||
"cc": ["https://lemmy-alpha/c/test"],
|
||||
"type": "Lock",
|
||||
"id": "https://lemmy-alpha/activities/lock/574b9805-19f5-4349-8c6e-c38c82898df9"
|
||||
},
|
||||
"cc": ["https://lemmy-alpha/c/test"],
|
||||
"type": "Undo",
|
||||
"summary": "A reason for an unlock."
|
||||
}
|
||||
@@ -5,7 +5,8 @@ use crate::{
|
||||
generate_activity_id,
|
||||
},
|
||||
activity_lists::AnnouncableActivities,
|
||||
protocol::activities::community::lock_page::{LockPage, LockType, UndoLockPage},
|
||||
post_or_comment_community,
|
||||
protocol::activities::community::lock::{LockPageOrNote, LockType, UndoLockPageOrNote},
|
||||
};
|
||||
use activitypub_federation::{
|
||||
config::Data,
|
||||
@@ -15,7 +16,7 @@ use activitypub_federation::{
|
||||
};
|
||||
use lemmy_api_utils::context::LemmyContext;
|
||||
use lemmy_apub_objects::{
|
||||
objects::community::ApubCommunity,
|
||||
objects::{community::ApubCommunity, PostOrComment},
|
||||
utils::{
|
||||
functions::{generate_to, verify_mod_action, verify_person_in_community, verify_visibility},
|
||||
protocol::InCommunity,
|
||||
@@ -24,8 +25,8 @@ use lemmy_apub_objects::{
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
activity::ActivitySendTargets,
|
||||
community::Community,
|
||||
mod_log::moderator::{ModLockPost, ModLockPostForm},
|
||||
comment::Comment,
|
||||
mod_log::moderator::{ModLockComment, ModLockCommentForm, ModLockPost, ModLockPostForm},
|
||||
person::Person,
|
||||
post::{Post, PostUpdateForm},
|
||||
},
|
||||
@@ -35,7 +36,7 @@ use lemmy_utils::error::{LemmyError, LemmyResult};
|
||||
use url::Url;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Activity for LockPage {
|
||||
impl Activity for LockPageOrNote {
|
||||
type DataType = LemmyContext;
|
||||
type Error = LemmyError;
|
||||
|
||||
@@ -57,29 +58,45 @@ impl Activity for LockPage {
|
||||
}
|
||||
|
||||
async fn receive(self, context: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
||||
let locked = Some(true);
|
||||
let locked = true;
|
||||
let reason = self.summary;
|
||||
let form = PostUpdateForm {
|
||||
locked,
|
||||
..Default::default()
|
||||
};
|
||||
let post = self.object.dereference(context).await?;
|
||||
Post::update(&mut context.pool(), post.id, &form).await?;
|
||||
|
||||
let form = ModLockPostForm {
|
||||
mod_person_id: self.actor.dereference(context).await?.id,
|
||||
post_id: post.id,
|
||||
locked,
|
||||
reason,
|
||||
};
|
||||
ModLockPost::create(&mut context.pool(), &form).await?;
|
||||
match self.object.dereference(context).await? {
|
||||
PostOrComment::Left(post) => {
|
||||
let form = PostUpdateForm {
|
||||
locked: Some(locked),
|
||||
..Default::default()
|
||||
};
|
||||
Post::update(&mut context.pool(), post.id, &form).await?;
|
||||
|
||||
let form = ModLockPostForm {
|
||||
mod_person_id: self.actor.dereference(context).await?.id,
|
||||
post_id: post.id,
|
||||
locked: Some(locked),
|
||||
reason,
|
||||
};
|
||||
ModLockPost::create(&mut context.pool(), &form).await?;
|
||||
}
|
||||
PostOrComment::Right(comment) => {
|
||||
Comment::update_locked_for_comment_and_children(&mut context.pool(), &comment.path, locked)
|
||||
.await?;
|
||||
|
||||
let form = ModLockCommentForm {
|
||||
mod_person_id: self.actor.dereference(context).await?.id,
|
||||
comment_id: comment.id,
|
||||
locked: Some(locked),
|
||||
reason,
|
||||
};
|
||||
ModLockComment::create(&mut context.pool(), &form).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Activity for UndoLockPage {
|
||||
impl Activity for UndoLockPageOrNote {
|
||||
type DataType = LemmyContext;
|
||||
type Error = LemmyError;
|
||||
|
||||
@@ -101,54 +118,73 @@ impl Activity for UndoLockPage {
|
||||
}
|
||||
|
||||
async fn receive(self, context: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
||||
let locked = Some(false);
|
||||
let locked = false;
|
||||
let reason = self.summary;
|
||||
let form = PostUpdateForm {
|
||||
locked,
|
||||
..Default::default()
|
||||
};
|
||||
let post = self.object.object.dereference(context).await?;
|
||||
Post::update(&mut context.pool(), post.id, &form).await?;
|
||||
|
||||
let form = ModLockPostForm {
|
||||
mod_person_id: self.actor.dereference(context).await?.id,
|
||||
post_id: post.id,
|
||||
locked,
|
||||
reason,
|
||||
};
|
||||
ModLockPost::create(&mut context.pool(), &form).await?;
|
||||
match self.object.object.dereference(context).await? {
|
||||
PostOrComment::Left(post) => {
|
||||
let form = PostUpdateForm {
|
||||
locked: Some(locked),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Post::update(&mut context.pool(), post.id, &form).await?;
|
||||
|
||||
let form = ModLockPostForm {
|
||||
mod_person_id: self.actor.dereference(context).await?.id,
|
||||
post_id: post.id,
|
||||
locked: Some(locked),
|
||||
reason,
|
||||
};
|
||||
ModLockPost::create(&mut context.pool(), &form).await?;
|
||||
}
|
||||
PostOrComment::Right(comment) => {
|
||||
Comment::update_locked_for_comment_and_children(&mut context.pool(), &comment.path, locked)
|
||||
.await?;
|
||||
|
||||
let form = ModLockCommentForm {
|
||||
mod_person_id: self.actor.dereference(context).await?.id,
|
||||
comment_id: comment.id,
|
||||
locked: Some(locked),
|
||||
reason,
|
||||
};
|
||||
ModLockComment::create(&mut context.pool(), &form).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn send_lock_post(
|
||||
post: Post,
|
||||
pub(crate) async fn send_lock(
|
||||
object: PostOrComment,
|
||||
actor: Person,
|
||||
locked: bool,
|
||||
reason: Option<String>,
|
||||
context: Data<LemmyContext>,
|
||||
) -> LemmyResult<()> {
|
||||
let community: ApubCommunity = Community::read(&mut context.pool(), post.community_id)
|
||||
.await?
|
||||
.into();
|
||||
let community: ApubCommunity = post_or_comment_community(&object, &context).await?.into();
|
||||
let id = generate_activity_id(LockType::Lock, &context)?;
|
||||
let community_id = community.ap_id.inner().clone();
|
||||
let ap_id = match object {
|
||||
PostOrComment::Left(p) => p.ap_id.clone(),
|
||||
PostOrComment::Right(c) => c.ap_id.clone(),
|
||||
};
|
||||
|
||||
let lock = LockPage {
|
||||
let lock = LockPageOrNote {
|
||||
actor: actor.ap_id.clone().into(),
|
||||
to: generate_to(&community)?,
|
||||
object: ObjectId::from(post.ap_id),
|
||||
object: ObjectId::from(ap_id),
|
||||
cc: vec![community_id.clone()],
|
||||
kind: LockType::Lock,
|
||||
id,
|
||||
summary: reason.clone(),
|
||||
};
|
||||
let activity = if locked {
|
||||
AnnouncableActivities::LockPost(lock)
|
||||
AnnouncableActivities::Lock(lock)
|
||||
} else {
|
||||
let id = generate_activity_id(UndoType::Undo, &context)?;
|
||||
let undo = UndoLockPage {
|
||||
let undo = UndoLockPageOrNote {
|
||||
actor: lock.actor.clone(),
|
||||
to: generate_to(&community)?,
|
||||
cc: lock.cc.clone(),
|
||||
@@ -157,7 +193,7 @@ pub(crate) async fn send_lock_post(
|
||||
object: lock,
|
||||
summary: reason,
|
||||
};
|
||||
AnnouncableActivities::UndoLockPost(undo)
|
||||
AnnouncableActivities::UndoLock(undo)
|
||||
};
|
||||
send_activity_in_community(
|
||||
activity,
|
||||
@@ -31,7 +31,7 @@ use lemmy_utils::error::LemmyResult;
|
||||
pub mod announce;
|
||||
pub mod collection_add;
|
||||
pub mod collection_remove;
|
||||
pub mod lock_page;
|
||||
pub mod lock;
|
||||
pub mod report;
|
||||
pub mod resolve_report;
|
||||
pub mod update;
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{
|
||||
block::{send_ban_from_community, send_ban_from_site},
|
||||
community::{
|
||||
collection_add::{send_add_mod_to_community, send_feature_post},
|
||||
lock_page::send_lock_post,
|
||||
lock::send_lock,
|
||||
update::{send_update_community, send_update_multi_community},
|
||||
},
|
||||
create_or_update::private_message::send_create_or_update_pm,
|
||||
@@ -34,7 +34,10 @@ use lemmy_api_utils::{
|
||||
context::LemmyContext,
|
||||
send_activity::{ActivityChannel, SendActivityData},
|
||||
};
|
||||
use lemmy_apub_objects::{objects::person::ApubPerson, utils::functions::GetActorType};
|
||||
use lemmy_apub_objects::{
|
||||
objects::{person::ApubPerson, PostOrComment},
|
||||
utils::functions::GetActorType,
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
activity::{ActivitySendTargets, SentActivity, SentActivityForm},
|
||||
@@ -191,7 +194,14 @@ pub async fn match_outgoing_activities(
|
||||
.await
|
||||
}
|
||||
LockPost(post, actor, locked, reason) => {
|
||||
send_lock_post(post, actor, locked, reason, context).await
|
||||
send_lock(
|
||||
PostOrComment::Left(post.into()),
|
||||
actor,
|
||||
locked,
|
||||
reason,
|
||||
context,
|
||||
)
|
||||
.await
|
||||
}
|
||||
FeaturePost(post, actor, featured) => send_feature_post(post, actor, featured, context).await,
|
||||
CreateComment(comment) => {
|
||||
@@ -220,6 +230,16 @@ pub async fn match_outgoing_activities(
|
||||
)
|
||||
.await
|
||||
}
|
||||
LockComment(comment, actor, locked, reason) => {
|
||||
send_lock(
|
||||
PostOrComment::Right(comment.into()),
|
||||
actor,
|
||||
locked,
|
||||
reason,
|
||||
context,
|
||||
)
|
||||
.await
|
||||
}
|
||||
LikePostOrComment {
|
||||
object_id,
|
||||
actor,
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::protocol::activities::{
|
||||
announce::{AnnounceActivity, RawAnnouncableActivities},
|
||||
collection_add::CollectionAdd,
|
||||
collection_remove::CollectionRemove,
|
||||
lock_page::{LockPage, UndoLockPage},
|
||||
lock::{LockPageOrNote, UndoLockPageOrNote},
|
||||
report::Report,
|
||||
resolve_report::ResolveReport,
|
||||
update::Update,
|
||||
@@ -65,8 +65,8 @@ pub enum AnnouncableActivities {
|
||||
UndoBlockUser(UndoBlockUser),
|
||||
CollectionAdd(CollectionAdd),
|
||||
CollectionRemove(CollectionRemove),
|
||||
LockPost(LockPage),
|
||||
UndoLockPost(UndoLockPage),
|
||||
Lock(LockPageOrNote),
|
||||
UndoLock(UndoLockPageOrNote),
|
||||
Report(Report),
|
||||
ResolveReport(ResolveReport),
|
||||
// For compatibility with Pleroma/Mastodon (send only)
|
||||
@@ -88,8 +88,8 @@ impl InCommunity for AnnouncableActivities {
|
||||
UndoBlockUser(a) => a.object.community(context).await,
|
||||
CollectionAdd(a) => a.community(context).await,
|
||||
CollectionRemove(a) => a.community(context).await,
|
||||
LockPost(a) => a.community(context).await,
|
||||
UndoLockPost(a) => a.object.community(context).await,
|
||||
Lock(a) => a.community(context).await,
|
||||
UndoLock(a) => a.object.community(context).await,
|
||||
Report(a) => a.community(context).await,
|
||||
ResolveReport(a) => a.object.community(context).await,
|
||||
Page(_) => Err(LemmyErrorType::NotFound.into()),
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
use activitypub_federation::{config::UrlVerifier, error::Error as ActivityPubError};
|
||||
use activitypub_federation::{
|
||||
config::{Data, UrlVerifier},
|
||||
error::Error as ActivityPubError,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use lemmy_apub_objects::utils::functions::{check_apub_id_valid, local_site_data_cached};
|
||||
use lemmy_db_schema::utils::ActualDbPool;
|
||||
use lemmy_utils::error::{FederationError, LemmyError, LemmyErrorType};
|
||||
use lemmy_api_utils::context::LemmyContext;
|
||||
use lemmy_apub_objects::{
|
||||
objects::PostOrComment,
|
||||
utils::functions::{check_apub_id_valid, local_site_data_cached},
|
||||
};
|
||||
use lemmy_db_schema::{source::community::Community, traits::Crud, utils::ActualDbPool};
|
||||
use lemmy_db_views_post::PostView;
|
||||
use lemmy_db_views_site::SiteView;
|
||||
use lemmy_utils::error::{FederationError, LemmyError, LemmyErrorType, LemmyResult};
|
||||
use url::Url;
|
||||
|
||||
pub mod activities;
|
||||
@@ -13,6 +22,29 @@ pub mod fetcher;
|
||||
pub mod http;
|
||||
pub mod protocol;
|
||||
|
||||
pub(crate) async fn post_or_comment_community(
|
||||
post_or_comment: &PostOrComment,
|
||||
context: &Data<LemmyContext>,
|
||||
) -> LemmyResult<Community> {
|
||||
match post_or_comment {
|
||||
PostOrComment::Left(p) => Community::read(&mut context.pool(), p.community_id).await,
|
||||
PostOrComment::Right(c) => {
|
||||
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
||||
Ok(
|
||||
PostView::read(
|
||||
&mut context.pool(),
|
||||
c.post_id,
|
||||
None,
|
||||
site_view.instance.id,
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.community,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Maximum number of outgoing HTTP requests to fetch a single object. Needs to be high enough
|
||||
/// to fetch a new community with posts, moderators and featured posts.
|
||||
pub const FEDERATION_HTTP_FETCH_LIMIT: u32 = 100;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::post_or_comment_community;
|
||||
use activitypub_federation::{
|
||||
config::Data,
|
||||
fetch::object_id::ObjectId,
|
||||
@@ -6,10 +7,9 @@ use activitypub_federation::{
|
||||
};
|
||||
use lemmy_api_utils::context::LemmyContext;
|
||||
use lemmy_apub_objects::{
|
||||
objects::{community::ApubCommunity, person::ApubPerson, post::ApubPost},
|
||||
objects::{community::ApubCommunity, person::ApubPerson, PostOrComment},
|
||||
utils::protocol::InCommunity,
|
||||
};
|
||||
use lemmy_db_schema::{source::community::Community, traits::Crud};
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::Display;
|
||||
@@ -22,11 +22,11 @@ pub enum LockType {
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LockPage {
|
||||
pub struct LockPageOrNote {
|
||||
pub(crate) actor: ObjectId<ApubPerson>,
|
||||
#[serde(deserialize_with = "deserialize_one_or_many")]
|
||||
pub(crate) to: Vec<Url>,
|
||||
pub(crate) object: ObjectId<ApubPost>,
|
||||
pub(crate) object: ObjectId<PostOrComment>,
|
||||
#[serde(deserialize_with = "deserialize_one_or_many")]
|
||||
pub(crate) cc: Vec<Url>,
|
||||
#[serde(rename = "type")]
|
||||
@@ -38,11 +38,11 @@ pub struct LockPage {
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UndoLockPage {
|
||||
pub struct UndoLockPageOrNote {
|
||||
pub(crate) actor: ObjectId<ApubPerson>,
|
||||
#[serde(deserialize_with = "deserialize_one_or_many")]
|
||||
pub(crate) to: Vec<Url>,
|
||||
pub(crate) object: LockPage,
|
||||
pub(crate) object: LockPageOrNote,
|
||||
#[serde(deserialize_with = "deserialize_one_or_many")]
|
||||
pub(crate) cc: Vec<Url>,
|
||||
#[serde(rename = "type")]
|
||||
@@ -52,10 +52,10 @@ pub struct UndoLockPage {
|
||||
pub(crate) summary: Option<String>,
|
||||
}
|
||||
|
||||
impl InCommunity for LockPage {
|
||||
impl InCommunity for LockPageOrNote {
|
||||
async fn community(&self, context: &Data<LemmyContext>) -> LemmyResult<ApubCommunity> {
|
||||
let post = self.object.dereference(context).await?;
|
||||
let community = Community::read(&mut context.pool(), post.community_id).await?;
|
||||
let post_or_comment = self.object.dereference(context).await?;
|
||||
let community = post_or_comment_community(&post_or_comment, context).await?;
|
||||
Ok(community.into())
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
pub mod announce;
|
||||
pub mod collection_add;
|
||||
pub mod collection_remove;
|
||||
pub mod lock_page;
|
||||
pub mod lock;
|
||||
pub mod report;
|
||||
pub mod resolve_report;
|
||||
pub mod update;
|
||||
@@ -13,7 +13,7 @@ mod tests {
|
||||
announce::AnnounceActivity,
|
||||
collection_add::CollectionAdd,
|
||||
collection_remove::CollectionRemove,
|
||||
lock_page::{LockPage, UndoLockPage},
|
||||
lock::{LockPageOrNote, UndoLockPageOrNote},
|
||||
report::Report,
|
||||
update::Update,
|
||||
};
|
||||
@@ -36,8 +36,15 @@ mod tests {
|
||||
"assets/lemmy/activities/community/remove_featured_post.json",
|
||||
)?;
|
||||
|
||||
test_parse_lemmy_item::<LockPage>("assets/lemmy/activities/community/lock_page.json")?;
|
||||
test_parse_lemmy_item::<UndoLockPage>("assets/lemmy/activities/community/undo_lock_page.json")?;
|
||||
test_parse_lemmy_item::<LockPageOrNote>("assets/lemmy/activities/community/lock_page.json")?;
|
||||
test_parse_lemmy_item::<UndoLockPageOrNote>(
|
||||
"assets/lemmy/activities/community/undo_lock_page.json",
|
||||
)?;
|
||||
|
||||
test_parse_lemmy_item::<LockPageOrNote>("assets/lemmy/activities/community/lock_note.json")?;
|
||||
test_parse_lemmy_item::<UndoLockPageOrNote>(
|
||||
"assets/lemmy/activities/community/undo_lock_note.json",
|
||||
)?;
|
||||
|
||||
test_parse_lemmy_item::<Update>("assets/lemmy/activities/community/update_community.json")?;
|
||||
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
use crate::post_or_comment_community;
|
||||
use activitypub_federation::{config::Data, fetch::object_id::ObjectId};
|
||||
use either::Either;
|
||||
use lemmy_api_utils::context::LemmyContext;
|
||||
use lemmy_apub_objects::{
|
||||
objects::{community::ApubCommunity, person::ApubPerson, PostOrComment},
|
||||
utils::protocol::InCommunity,
|
||||
};
|
||||
use lemmy_db_schema::{source::community::Community, traits::Crud};
|
||||
use lemmy_db_views_post::PostView;
|
||||
use lemmy_db_views_site::SiteView;
|
||||
use lemmy_utils::error::{FederationError, LemmyError, LemmyResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::Display;
|
||||
@@ -52,21 +49,8 @@ impl From<&VoteType> for i16 {
|
||||
|
||||
impl InCommunity for Vote {
|
||||
async fn community(&self, context: &Data<LemmyContext>) -> LemmyResult<ApubCommunity> {
|
||||
let community = match self.object.dereference(context).await? {
|
||||
Either::Left(p) => Community::read(&mut context.pool(), p.community_id).await?,
|
||||
Either::Right(c) => {
|
||||
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
||||
PostView::read(
|
||||
&mut context.pool(),
|
||||
c.post_id,
|
||||
None,
|
||||
site_view.instance.id,
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.community
|
||||
}
|
||||
};
|
||||
let post_or_comment = self.object.dereference(context).await?;
|
||||
let community = post_or_comment_community(&post_or_comment, context).await?;
|
||||
Ok(community.into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,13 +175,14 @@ impl Object for ApubComment {
|
||||
))
|
||||
.await?;
|
||||
|
||||
let (post, _) = Box::pin(note.get_parents(context)).await?;
|
||||
let (post, parent_comment) = Box::pin(note.get_parents(context)).await?;
|
||||
let creator = Box::pin(note.attributed_to.dereference(context)).await?;
|
||||
|
||||
let is_mod_or_admin = check_is_mod_or_admin(&mut context.pool(), creator.id, community.id)
|
||||
.await
|
||||
.is_ok();
|
||||
if post.locked && !is_mod_or_admin {
|
||||
let locked = post.locked || parent_comment.is_some_and(|c| c.locked);
|
||||
if locked && !is_mod_or_admin {
|
||||
Err(FederationError::PostIsLocked)?
|
||||
} else {
|
||||
Ok(())
|
||||
@@ -223,6 +224,7 @@ impl Object for ApubComment {
|
||||
local: Some(false),
|
||||
language_id,
|
||||
federation_pending: Some(false),
|
||||
locked: None,
|
||||
};
|
||||
form = plugin_hook_before("before_receive_federated_comment", form).await?;
|
||||
let parent_comment_path = parent_comment.map(|t| t.0.path);
|
||||
|
||||
@@ -28,7 +28,7 @@ use diesel::{
|
||||
QueryDsl,
|
||||
};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use diesel_ltree::Ltree;
|
||||
use diesel_ltree::{dsl::LtreeExtensions, Ltree};
|
||||
use diesel_uplete::{uplete, UpleteCount};
|
||||
use lemmy_db_schema_file::schema::{comment, comment_actions, community, post};
|
||||
use lemmy_utils::{
|
||||
@@ -237,6 +237,36 @@ impl Comment {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates the locked field for a comment and all its children.
|
||||
pub async fn update_locked_for_comment_and_children(
|
||||
pool: &mut DbPool<'_>,
|
||||
comment_path: &Ltree,
|
||||
locked: bool,
|
||||
) -> LemmyResult<Vec<Self>> {
|
||||
let form = CommentUpdateForm {
|
||||
locked: Some(locked),
|
||||
..Default::default()
|
||||
};
|
||||
Self::update_comment_and_children(pool, comment_path, &form).await
|
||||
}
|
||||
|
||||
/// A helper function to update comment and all its children.
|
||||
///
|
||||
/// Don't expose so as to make sure you aren't overwriting data.
|
||||
async fn update_comment_and_children(
|
||||
pool: &mut DbPool<'_>,
|
||||
comment_path: &Ltree,
|
||||
form: &CommentUpdateForm,
|
||||
) -> LemmyResult<Vec<Self>> {
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
diesel::update(comment::table)
|
||||
.filter(comment::path.contained_by(comment_path))
|
||||
.set(form)
|
||||
.get_results(conn)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntUpdate)
|
||||
}
|
||||
|
||||
pub async fn read_ap_ids_for_post(
|
||||
post_id: PostId,
|
||||
pool: &mut DbPool<'_>,
|
||||
@@ -468,6 +498,7 @@ mod tests {
|
||||
report_count: 0,
|
||||
unresolved_report_count: 0,
|
||||
federation_pending: false,
|
||||
locked: false,
|
||||
};
|
||||
|
||||
let child_comment_form = CommentInsertForm::new(
|
||||
@@ -614,4 +645,71 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_update_children() -> LemmyResult<()> {
|
||||
let pool = &build_db_pool_for_tests();
|
||||
let pool = &mut pool.into();
|
||||
|
||||
let inserted_instance = Instance::read_or_create(pool, "mydomain.tld".to_string()).await?;
|
||||
let new_person = PersonInsertForm::test_form(inserted_instance.id, "john");
|
||||
let inserted_person = Person::create(pool, &new_person).await?;
|
||||
let new_community = CommunityInsertForm::new(
|
||||
inserted_instance.id,
|
||||
"test".into(),
|
||||
"test".to_owned(),
|
||||
"pubkey".to_string(),
|
||||
);
|
||||
let inserted_community = Community::create(pool, &new_community).await?;
|
||||
|
||||
let new_post = PostInsertForm::new(
|
||||
"Post Title".to_string(),
|
||||
inserted_person.id,
|
||||
inserted_community.id,
|
||||
);
|
||||
let inserted_post = Post::create(pool, &new_post).await?;
|
||||
|
||||
let parent_comment_form = CommentInsertForm::new(
|
||||
inserted_person.id,
|
||||
inserted_post.id,
|
||||
"Top level".to_string(),
|
||||
);
|
||||
let inserted_parent_comment = Comment::create(pool, &parent_comment_form, None).await?;
|
||||
|
||||
let child_comment_form =
|
||||
CommentInsertForm::new(inserted_person.id, inserted_post.id, "Child".to_string());
|
||||
let inserted_child_comment = Comment::create(
|
||||
pool,
|
||||
&child_comment_form,
|
||||
Some(&inserted_parent_comment.path),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let grandchild_comment_form = CommentInsertForm::new(
|
||||
inserted_person.id,
|
||||
inserted_post.id,
|
||||
"Grandchild".to_string(),
|
||||
);
|
||||
let _inserted_grandchild_comment = Comment::create(
|
||||
pool,
|
||||
&grandchild_comment_form,
|
||||
Some(&inserted_child_comment.path),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let lock_form = CommentUpdateForm {
|
||||
locked: Some(true),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let updated_comments =
|
||||
Comment::update_comment_and_children(pool, &inserted_parent_comment.path, &lock_form).await?;
|
||||
|
||||
let locked_comments_num = updated_comments.iter().filter(|c| c.locked).count();
|
||||
|
||||
assert_eq!(3, locked_comments_num);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::{
|
||||
ModBanFromCommunityId,
|
||||
ModChangeCommunityVisibilityId,
|
||||
ModFeaturePostId,
|
||||
ModLockCommentId,
|
||||
ModLockPostId,
|
||||
ModRemoveCommentId,
|
||||
ModRemovePostId,
|
||||
@@ -18,6 +19,8 @@ use crate::{
|
||||
ModChangeCommunityVisibilityForm,
|
||||
ModFeaturePost,
|
||||
ModFeaturePostForm,
|
||||
ModLockComment,
|
||||
ModLockCommentForm,
|
||||
ModLockPost,
|
||||
ModLockPostForm,
|
||||
ModRemoveComment,
|
||||
@@ -37,6 +40,7 @@ use lemmy_db_schema_file::schema::{
|
||||
mod_ban_from_community,
|
||||
mod_change_community_visibility,
|
||||
mod_feature_post,
|
||||
mod_lock_comment,
|
||||
mod_lock_post,
|
||||
mod_remove_comment,
|
||||
mod_remove_post,
|
||||
@@ -184,6 +188,34 @@ impl ModRemoveComment {
|
||||
}
|
||||
}
|
||||
|
||||
impl Crud for ModLockComment {
|
||||
type InsertForm = ModLockCommentForm;
|
||||
type UpdateForm = ModLockCommentForm;
|
||||
type IdType = ModLockCommentId;
|
||||
|
||||
async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> LemmyResult<Self> {
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
insert_into(mod_lock_comment::table)
|
||||
.values(form)
|
||||
.get_result::<Self>(conn)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntCreate)
|
||||
}
|
||||
|
||||
async fn update(
|
||||
pool: &mut DbPool<'_>,
|
||||
from_id: Self::IdType,
|
||||
form: &Self::UpdateForm,
|
||||
) -> LemmyResult<Self> {
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
diesel::update(mod_lock_comment::table.find(from_id))
|
||||
.set(form)
|
||||
.get_result::<Self>(conn)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
impl Crud for ModBanFromCommunity {
|
||||
type InsertForm = ModBanFromCommunityForm;
|
||||
type UpdateForm = ModBanFromCommunityForm;
|
||||
|
||||
@@ -99,6 +99,7 @@ pub enum ModlogActionType {
|
||||
ModLockPost,
|
||||
ModFeaturePost,
|
||||
ModRemoveComment,
|
||||
ModLockComment,
|
||||
AdminRemoveCommunity,
|
||||
ModBanFromCommunity,
|
||||
ModAddToCommunity,
|
||||
|
||||
@@ -280,6 +280,12 @@ pub struct ModRemovePostId(pub i32);
|
||||
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
|
||||
pub struct ModRemoveCommentId(pub i32);
|
||||
|
||||
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)]
|
||||
#[cfg_attr(feature = "full", derive(DieselNewType))]
|
||||
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
|
||||
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
|
||||
pub struct ModLockCommentId(pub i32);
|
||||
|
||||
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)]
|
||||
#[cfg_attr(feature = "full", derive(DieselNewType))]
|
||||
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::newtypes::{
|
||||
ModBanFromCommunityId,
|
||||
ModChangeCommunityVisibilityId,
|
||||
ModFeaturePostId,
|
||||
ModLockCommentId,
|
||||
ModLockPostId,
|
||||
ModRemoveCommentId,
|
||||
ModRemovePostId,
|
||||
@@ -54,4 +55,5 @@ pub struct ModlogCombined {
|
||||
pub admin_remove_community_id: Option<AdminRemoveCommunityId>,
|
||||
pub mod_remove_post_id: Option<ModRemovePostId>,
|
||||
pub mod_transfer_community_id: Option<ModTransferCommunityId>,
|
||||
pub mod_lock_comment_id: Option<ModLockCommentId>,
|
||||
}
|
||||
|
||||
@@ -63,6 +63,8 @@ pub struct Comment {
|
||||
/// If a local user comments in a remote community, the comment is hidden until it is confirmed
|
||||
/// accepted by the community (by receiving it back via federation).
|
||||
pub federation_pending: bool,
|
||||
/// Whether the comment is locked.
|
||||
pub locked: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, derive_new::new)]
|
||||
@@ -93,6 +95,8 @@ pub struct CommentInsertForm {
|
||||
pub language_id: Option<LanguageId>,
|
||||
#[new(default)]
|
||||
pub federation_pending: Option<bool>,
|
||||
#[new(default)]
|
||||
pub locked: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
@@ -109,6 +113,7 @@ pub struct CommentUpdateForm {
|
||||
pub distinguished: Option<bool>,
|
||||
pub language_id: Option<LanguageId>,
|
||||
pub federation_pending: Option<bool>,
|
||||
pub locked: Option<bool>,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
|
||||
@@ -20,6 +20,7 @@ use lemmy_db_schema_file::schema::{
|
||||
mod_ban_from_community,
|
||||
mod_change_community_visibility,
|
||||
mod_feature_post,
|
||||
mod_lock_comment,
|
||||
mod_lock_post,
|
||||
mod_remove_comment,
|
||||
mod_remove_post,
|
||||
@@ -130,6 +131,31 @@ pub struct ModRemoveCommentForm {
|
||||
pub removed: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable))]
|
||||
#[cfg_attr(feature = "full", diesel(table_name = mod_lock_comment))]
|
||||
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
|
||||
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
|
||||
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
|
||||
/// When a moderator locks a comment (prevents new replies to a comment or its children).
|
||||
pub struct ModLockComment {
|
||||
pub id: ModLockPostId,
|
||||
pub mod_person_id: PersonId,
|
||||
pub comment_id: PostId,
|
||||
pub locked: bool,
|
||||
pub reason: Option<String>,
|
||||
pub published_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
|
||||
#[cfg_attr(feature = "full", diesel(table_name = mod_lock_comment))]
|
||||
pub struct ModLockCommentForm {
|
||||
pub mod_person_id: PersonId,
|
||||
pub comment_id: CommentId,
|
||||
pub locked: Option<bool>,
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable))]
|
||||
|
||||
@@ -276,6 +276,7 @@ pub fn comment_select_remove_deletes() -> _ {
|
||||
comment::report_count,
|
||||
comment::unresolved_report_count,
|
||||
comment::federation_pending,
|
||||
comment::locked,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -189,6 +189,7 @@ diesel::table! {
|
||||
report_count -> Int2,
|
||||
unresolved_report_count -> Int2,
|
||||
federation_pending -> Bool,
|
||||
locked -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -647,6 +648,17 @@ diesel::table! {
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
mod_lock_comment (id) {
|
||||
id -> Int4,
|
||||
mod_person_id -> Int4,
|
||||
comment_id -> Int4,
|
||||
locked -> Bool,
|
||||
reason -> Nullable<Text>,
|
||||
published_at -> Timestamptz,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
mod_lock_post (id) {
|
||||
id -> Int4,
|
||||
@@ -711,6 +723,7 @@ diesel::table! {
|
||||
mod_remove_post_id -> Nullable<Int4>,
|
||||
mod_transfer_community_id -> Nullable<Int4>,
|
||||
mod_change_community_visibility_id -> Nullable<Int4>,
|
||||
mod_lock_comment_id -> Nullable<Int4>,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1185,6 +1198,8 @@ diesel::joinable!(mod_change_community_visibility -> community (community_id));
|
||||
diesel::joinable!(mod_change_community_visibility -> person (mod_person_id));
|
||||
diesel::joinable!(mod_feature_post -> person (mod_person_id));
|
||||
diesel::joinable!(mod_feature_post -> post (post_id));
|
||||
diesel::joinable!(mod_lock_comment -> comment (comment_id));
|
||||
diesel::joinable!(mod_lock_comment -> person (mod_person_id));
|
||||
diesel::joinable!(mod_lock_post -> person (mod_person_id));
|
||||
diesel::joinable!(mod_lock_post -> post (post_id));
|
||||
diesel::joinable!(mod_remove_comment -> comment (comment_id));
|
||||
@@ -1205,6 +1220,7 @@ diesel::joinable!(modlog_combined -> mod_add_to_community (mod_add_to_community_
|
||||
diesel::joinable!(modlog_combined -> mod_ban_from_community (mod_ban_from_community_id));
|
||||
diesel::joinable!(modlog_combined -> mod_change_community_visibility (mod_change_community_visibility_id));
|
||||
diesel::joinable!(modlog_combined -> mod_feature_post (mod_feature_post_id));
|
||||
diesel::joinable!(modlog_combined -> mod_lock_comment (mod_lock_comment_id));
|
||||
diesel::joinable!(modlog_combined -> mod_lock_post (mod_lock_post_id));
|
||||
diesel::joinable!(modlog_combined -> mod_remove_comment (mod_remove_comment_id));
|
||||
diesel::joinable!(modlog_combined -> mod_remove_post (mod_remove_post_id));
|
||||
@@ -1297,6 +1313,7 @@ diesel::allow_tables_to_appear_in_same_query!(
|
||||
mod_ban_from_community,
|
||||
mod_change_community_visibility,
|
||||
mod_feature_post,
|
||||
mod_lock_comment,
|
||||
mod_lock_post,
|
||||
mod_remove_comment,
|
||||
mod_remove_post,
|
||||
|
||||
@@ -574,6 +574,7 @@ CALL r.create_person_liked_combined_trigger ('comment');
|
||||
-- mod_remove_community
|
||||
-- mod_remove_post
|
||||
-- mod_transfer_community
|
||||
-- mod_lock_comment
|
||||
CREATE PROCEDURE r.create_modlog_combined_trigger (table_name text)
|
||||
LANGUAGE plpgsql
|
||||
AS $a$
|
||||
@@ -613,6 +614,7 @@ CALL r.create_modlog_combined_trigger ('mod_remove_comment');
|
||||
CALL r.create_modlog_combined_trigger ('admin_remove_community');
|
||||
CALL r.create_modlog_combined_trigger ('mod_remove_post');
|
||||
CALL r.create_modlog_combined_trigger ('mod_transfer_community');
|
||||
CALL r.create_modlog_combined_trigger ('mod_lock_comment');
|
||||
-- Prevent using delete instead of uplete on action tables
|
||||
CREATE FUNCTION r.require_uplete ()
|
||||
RETURNS TRIGGER
|
||||
|
||||
@@ -118,6 +118,16 @@ pub struct ListCommentLikes {
|
||||
pub limit: Option<i64>,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
|
||||
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
|
||||
pub struct LockComment {
|
||||
pub comment_id: CommentId,
|
||||
pub locked: bool,
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
|
||||
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
|
||||
|
||||
@@ -71,6 +71,7 @@ use lemmy_db_schema_file::{
|
||||
mod_ban_from_community,
|
||||
mod_change_community_visibility,
|
||||
mod_feature_post,
|
||||
mod_lock_comment,
|
||||
mod_lock_post,
|
||||
mod_remove_comment,
|
||||
mod_remove_post,
|
||||
@@ -109,7 +110,8 @@ impl ModlogCombinedViewInternal {
|
||||
.or(mod_remove_comment::mod_person_id.eq(person::id))
|
||||
.or(admin_remove_community::mod_person_id.eq(person::id))
|
||||
.or(mod_remove_post::mod_person_id.eq(person::id))
|
||||
.or(mod_transfer_community::mod_person_id.eq(person::id)),
|
||||
.or(mod_transfer_community::mod_person_id.eq(person::id))
|
||||
.or(mod_lock_comment::mod_person_id.eq(person::id)),
|
||||
);
|
||||
|
||||
let other_person_join = aliases::person1.on(
|
||||
@@ -139,7 +141,12 @@ impl ModlogCombinedViewInternal {
|
||||
.is_not_null()
|
||||
.and(post::creator_id.eq(other_person)),
|
||||
)
|
||||
.or(mod_transfer_community::other_person_id.eq(other_person)),
|
||||
.or(mod_transfer_community::other_person_id.eq(other_person))
|
||||
.or(
|
||||
mod_lock_comment::id
|
||||
.is_not_null()
|
||||
.and(comment::creator_id.eq(other_person)),
|
||||
),
|
||||
);
|
||||
|
||||
let comment_join = comment::table.on(mod_remove_comment::comment_id.eq(comment::id));
|
||||
@@ -217,6 +224,7 @@ impl ModlogCombinedViewInternal {
|
||||
.left_join(admin_remove_community::table)
|
||||
.left_join(mod_remove_post::table)
|
||||
.left_join(mod_transfer_community::table)
|
||||
.left_join(mod_lock_comment::table)
|
||||
.left_join(moderator_join)
|
||||
.left_join(comment_join)
|
||||
.left_join(post_join)
|
||||
@@ -249,6 +257,7 @@ impl PaginationCursorBuilder for ModlogCombinedView {
|
||||
AdminRemoveCommunity(v) => ('O', v.admin_remove_community.id.0),
|
||||
ModRemovePost(v) => ('P', v.mod_remove_post.id.0),
|
||||
ModTransferCommunity(v) => ('Q', v.mod_transfer_community.id.0),
|
||||
ModLockComment(v) => ('R', v.mod_lock_comment.id.0),
|
||||
};
|
||||
PaginationCursor::new_single(prefix, id)
|
||||
}
|
||||
@@ -282,6 +291,7 @@ impl PaginationCursorBuilder for ModlogCombinedView {
|
||||
'O' => query.filter(modlog_combined::admin_remove_community_id.eq(id)),
|
||||
'P' => query.filter(modlog_combined::mod_remove_post_id.eq(id)),
|
||||
'Q' => query.filter(modlog_combined::mod_transfer_community_id.eq(id)),
|
||||
'R' => query.filter(modlog_combined::mod_lock_comment_id.eq(id)),
|
||||
_ => return Err(LemmyErrorType::CouldntParsePaginationToken.into()),
|
||||
};
|
||||
|
||||
@@ -349,6 +359,7 @@ impl ModlogCombinedQuery<'_> {
|
||||
ModLockPost => query.filter(modlog_combined::mod_lock_post_id.is_not_null()),
|
||||
ModFeaturePost => query.filter(modlog_combined::mod_feature_post_id.is_not_null()),
|
||||
ModRemoveComment => query.filter(modlog_combined::mod_remove_comment_id.is_not_null()),
|
||||
ModLockComment => query.filter(modlog_combined::mod_lock_comment_id.is_not_null()),
|
||||
AdminRemoveCommunity => {
|
||||
query.filter(modlog_combined::admin_remove_community_id.is_not_null())
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ use lemmy_db_schema::source::{
|
||||
ModBanFromCommunity,
|
||||
ModChangeCommunityVisibility,
|
||||
ModFeaturePost,
|
||||
ModLockComment,
|
||||
ModLockPost,
|
||||
ModRemoveComment,
|
||||
ModRemovePost,
|
||||
@@ -122,6 +123,21 @@ pub struct ModLockPostView {
|
||||
pub community: Community,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "full", derive(Queryable))]
|
||||
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
|
||||
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
|
||||
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
|
||||
/// When a moderator locks a comment (prevents replies to it or its children).
|
||||
pub struct ModLockCommentView {
|
||||
pub mod_lock_comment: ModLockComment,
|
||||
pub moderator: Option<Person>,
|
||||
pub other_person: Person,
|
||||
pub comment: Comment,
|
||||
pub community: Community,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "full", derive(Queryable))]
|
||||
@@ -311,6 +327,8 @@ pub(crate) struct ModlogCombinedViewInternal {
|
||||
pub mod_remove_post: Option<ModRemovePost>,
|
||||
#[cfg_attr(feature = "full", diesel(embed))]
|
||||
pub mod_transfer_community: Option<ModTransferCommunity>,
|
||||
#[cfg_attr(feature = "full", diesel(embed))]
|
||||
pub mod_lock_comment: Option<ModLockComment>,
|
||||
// Specific fields
|
||||
|
||||
// Shared
|
||||
@@ -356,4 +374,5 @@ pub enum ModlogCombinedView {
|
||||
AdminRemoveCommunity(AdminRemoveCommunityView),
|
||||
ModRemovePost(ModRemovePostView),
|
||||
ModTransferCommunity(ModTransferCommunityView),
|
||||
ModLockComment(ModLockCommentView),
|
||||
}
|
||||
|
||||
@@ -644,6 +644,22 @@ fn create_modlog_items(
|
||||
&None,
|
||||
settings,
|
||||
),
|
||||
ModlogCombinedView::ModLockComment(v) => build_modlog_item(
|
||||
&v.moderator,
|
||||
&v.mod_lock_comment.published_at,
|
||||
&modlog_url,
|
||||
&format!(
|
||||
"{} comment {}",
|
||||
if v.mod_lock_comment.locked {
|
||||
"Locked"
|
||||
} else {
|
||||
"Unlocked"
|
||||
},
|
||||
&v.comment.content
|
||||
),
|
||||
&v.mod_lock_comment.reason,
|
||||
settings,
|
||||
),
|
||||
})
|
||||
.collect::<LemmyResult<Vec<Item>>>()?;
|
||||
|
||||
|
||||
9
migrations/2025-08-20-000000_comment-lock/down.sql
Normal file
9
migrations/2025-08-20-000000_comment-lock/down.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
ALTER TABLE modlog_combined
|
||||
DROP COLUMN mod_lock_comment_id,
|
||||
ADD CONSTRAINT modlog_combined_check CHECK (num_nonnulls (admin_allow_instance_id, admin_block_instance_id, admin_purge_comment_id, admin_purge_community_id, admin_purge_person_id, admin_purge_post_id, admin_add_id, mod_add_to_community_id, admin_ban_id, mod_ban_from_community_id, mod_feature_post_id, mod_change_community_visibility_id, mod_lock_post_id, mod_remove_comment_id, admin_remove_community_id, mod_remove_post_id, mod_transfer_community_id) = 1);
|
||||
|
||||
DROP TABLE mod_lock_comment;
|
||||
|
||||
ALTER TABLE comment
|
||||
DROP COLUMN LOCKED;
|
||||
|
||||
24
migrations/2025-08-20-000000_comment-lock/up.sql
Normal file
24
migrations/2025-08-20-000000_comment-lock/up.sql
Normal file
@@ -0,0 +1,24 @@
|
||||
ALTER TABLE comment
|
||||
ADD COLUMN "locked" bool NOT NULL DEFAULT FALSE;
|
||||
|
||||
CREATE TABLE mod_lock_comment (
|
||||
id serial PRIMARY KEY,
|
||||
mod_person_id integer NOT NULL REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
comment_id integer NOT NULL REFERENCES COMMENT ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
locked boolean NOT NULL DEFAULT TRUE,
|
||||
reason text,
|
||||
published_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_mod_lock_comment_mod ON mod_lock_comment (mod_person_id);
|
||||
|
||||
CREATE INDEX idx_mod_lock_comment_comment ON mod_lock_comment (comment_id);
|
||||
|
||||
ALTER TABLE modlog_combined
|
||||
ADD COLUMN mod_lock_comment_id integer UNIQUE REFERENCES mod_lock_comment ON UPDATE CASCADE ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE modlog_combined
|
||||
DROP CONSTRAINT modlog_combined_check,
|
||||
ADD CONSTRAINT modlog_combined_check CHECK (num_nonnulls (admin_allow_instance_id, admin_block_instance_id, admin_purge_comment_id, admin_purge_community_id, admin_purge_person_id, admin_purge_post_id, admin_add_id, mod_add_to_community_id, admin_ban_id, mod_ban_from_community_id, mod_feature_post_id, mod_change_community_visibility_id, mod_lock_post_id, mod_remove_comment_id, admin_remove_community_id, mod_remove_post_id, mod_transfer_community_id, mod_lock_comment_id) = 1),
|
||||
ALTER CONSTRAINT modlog_combined_mod_lock_comment_id_fkey NOT DEFERRABLE;
|
||||
|
||||
@@ -4,6 +4,7 @@ use lemmy_api::{
|
||||
distinguish::distinguish_comment,
|
||||
like::like_comment,
|
||||
list_comment_likes::list_comment_likes,
|
||||
lock::lock_comment,
|
||||
save::save_comment,
|
||||
},
|
||||
community::{
|
||||
@@ -317,6 +318,7 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimit) {
|
||||
.route("/like", post().to(like_comment))
|
||||
.route("/like/list", get().to(list_comment_likes))
|
||||
.route("/save", put().to(save_comment))
|
||||
.route("/lock", post().to(lock_comment))
|
||||
.route("/list", get().to(list_comments))
|
||||
.route("/list/slim", get().to(list_comments_slim))
|
||||
.route("/report", post().to(create_comment_report))
|
||||
|
||||
Reference in New Issue
Block a user