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:
flamingos-cant
2025-09-03 11:42:24 +01:00
committed by GitHub
parent d79ebdd919
commit 9eacf8243c
37 changed files with 667 additions and 108 deletions

View File

@@ -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",

View File

@@ -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:

View File

@@ -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":

View File

@@ -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,

View 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)
}

View File

@@ -1,4 +1,5 @@
pub mod distinguish;
pub mod like;
pub mod list_comment_likes;
pub mod lock;
pub mod save;

View File

@@ -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() {

View File

@@ -57,6 +57,7 @@ pub enum SendActivityData {
community: Community,
reason: Option<String>,
},
LockComment(Comment, Person, bool, Option<String>),
LikePostOrComment {
object_id: DbUrl,
actor: Person,

View File

@@ -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());

View 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"
}

View File

@@ -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."
}

View File

@@ -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,

View File

@@ -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;

View File

@@ -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,

View File

@@ -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()),

View File

@@ -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;

View File

@@ -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())
}
}

View File

@@ -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")?;

View File

@@ -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())
}
}

View File

@@ -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);

View File

@@ -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(())
}
}

View File

@@ -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;

View File

@@ -99,6 +99,7 @@ pub enum ModlogActionType {
ModLockPost,
ModFeaturePost,
ModRemoveComment,
ModLockComment,
AdminRemoveCommunity,
ModBanFromCommunity,
ModAddToCommunity,

View File

@@ -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))]

View File

@@ -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>,
}

View File

@@ -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]

View File

@@ -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))]

View File

@@ -276,6 +276,7 @@ pub fn comment_select_remove_deletes() -> _ {
comment::report_count,
comment::unresolved_report_count,
comment::federation_pending,
comment::locked,
)
}

View File

@@ -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,

View File

@@ -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

View File

@@ -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))]

View File

@@ -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())
}

View File

@@ -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),
}

View File

@@ -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>>>()?;

View 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;

View 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;

View File

@@ -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))