Allow sending report only to admins (fixes #2414) (#5350)

* Allow sending report only to admins (fixes #2414)

* fix api test

* Rename `to_local_admins` to `violates_instance_rules`

* Review fixes

* fix api test
This commit is contained in:
Nutomic
2025-02-12 15:34:43 +00:00
committed by GitHub
parent ae9c735e90
commit 0e87900953
18 changed files with 228 additions and 47 deletions

View File

@@ -29,7 +29,7 @@
"eslint": "^9.20.0",
"eslint-plugin-prettier": "^5.2.3",
"jest": "^29.5.0",
"lemmy-js-client": "0.20.0-ap-id.1",
"lemmy-js-client": "0.20.0-show-mod-reports.2",
"prettier": "^3.5.0",
"ts-jest": "^29.1.0",
"tsoa": "^6.6.0",

View File

@@ -33,8 +33,8 @@ importers:
specifier: ^29.5.0
version: 29.7.0(@types/node@22.13.1)
lemmy-js-client:
specifier: 0.20.0-ap-id.1
version: 0.20.0-ap-id.1
specifier: 0.20.0-show-mod-reports.2
version: 0.20.0-show-mod-reports.2
prettier:
specifier: ^3.5.0
version: 3.5.0
@@ -1528,8 +1528,8 @@ packages:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
engines: {node: '>=6'}
lemmy-js-client@0.20.0-ap-id.1:
resolution: {integrity: sha512-HzY005mhbINXa5i+GabuJSrwN27ExZKj2XxM1cAnfTWJ4ZqvbLuz4i26JDeE8pj6GGKbXBIj2VX4aOhKgCjkSA==}
lemmy-js-client@0.20.0-show-mod-reports.2:
resolution: {integrity: sha512-92Zgs5/Nf8h57U5kgL0Q9BZzoTG0JUTGSGg1e9DORzQFVfYv53dzKpdjNxvuHsxlmT4eOeZgCuyUGdRipYy6jw==}
leven@3.1.0:
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
@@ -4169,7 +4169,7 @@ snapshots:
kleur@3.0.3: {}
lemmy-js-client@0.20.0-ap-id.1: {}
lemmy-js-client@0.20.0-show-mod-reports.2: {}
leven@3.1.0: {}

View File

@@ -731,11 +731,12 @@ test("Report a post", async () => {
expect(betaReport.reason).toBe(gammaReport.reason);
await unfollowRemotes(alpha);
// Report was federated to poster's instance
// Report was federated to poster's instance. Alpha is not a community mod and doesnt see
// the report by default, so we need to pass show_mod_reports = true.
let alphaReport = (
(await waitUntil(
() =>
listReports(alpha).then(p =>
listReports(alpha, true).then(p =>
p.reports.find(r => {
return checkPostReportName(r, gammaReport);
}),

View File

@@ -801,8 +801,9 @@ export async function reportPost(
export async function listReports(
api: LemmyHttp,
show_community_rule_violations: boolean = false,
): Promise<ListReportsResponse> {
let form: ListReports = {};
let form: ListReports = { show_community_rule_violations };
return api.listReports(form);
}

View File

@@ -56,6 +56,7 @@ pub async fn create_comment_report(
comment_id,
original_comment_text: comment_view.comment.content,
reason,
violates_instance_rules: data.violates_instance_rules.unwrap_or_default(),
};
let report = CommentReport::report(&mut context.pool(), &report_form)

View File

@@ -52,6 +52,7 @@ pub async fn create_post_report(
original_post_url: post_view.post.url,
original_post_body: post_view.post.body,
reason,
violates_instance_rules: data.violates_instance_rules.unwrap_or_default(),
};
let report = PostReport::report(&mut context.pool(), &report_form)

View File

@@ -31,6 +31,7 @@ pub async fn list_reports(
unresolved_only: data.unresolved_only,
page_after,
page_back,
show_community_rule_violations: data.show_community_rule_violations,
}
.list(&mut context.pool(), &local_user_view)
.await?;

View File

@@ -30,6 +30,9 @@ pub struct ListReports {
pub page_cursor: Option<ReportCombinedPaginationCursor>,
#[cfg_attr(feature = "full", ts(optional))]
pub page_back: Option<bool>,
/// Only for admins: also show reports with `violates_instance_rules=false`
#[cfg_attr(feature = "full", ts(optional))]
pub show_community_rule_violations: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]

View File

@@ -11,6 +11,8 @@ use ts_rs::TS;
pub struct CreateCommentReport {
pub comment_id: CommentId,
pub reason: String,
#[cfg_attr(feature = "full", ts(optional))]
pub violates_instance_rules: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]

View File

@@ -11,6 +11,8 @@ use ts_rs::TS;
pub struct CreatePostReport {
pub post_id: PostId,
pub reason: String,
#[cfg_attr(feature = "full", ts(optional))]
pub violates_instance_rules: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]

View File

@@ -105,6 +105,7 @@ impl ActivityHandler for Report {
original_post_url: post.url.clone(),
reason,
original_post_body: post.body.clone(),
violates_instance_rules: false,
};
PostReport::report(&mut context.pool(), &report_form).await?;
}
@@ -116,6 +117,7 @@ impl ActivityHandler for Report {
comment_id: comment.id,
original_comment_text: comment.content.clone(),
reason,
violates_instance_rules: false,
};
CommentReport::report(&mut context.pool(), &report_form).await?;
}

View File

@@ -392,10 +392,11 @@ BEGIN
report_count = a.report_count + diff.report_count, unresolved_report_count = a.unresolved_report_count + diff.unresolved_report_count
FROM (
SELECT
(post_report).post_id, coalesce(sum(count_diff), 0) AS report_count, coalesce(sum(count_diff) FILTER (WHERE NOT (post_report).resolved), 0) AS unresolved_report_count
FROM select_old_and_new_rows AS old_and_new_rows GROUP BY (post_report).post_id) AS diff
(post_report).post_id, coalesce(sum(count_diff), 0) AS report_count, coalesce(sum(count_diff) FILTER (WHERE NOT (post_report).resolved
AND NOT (post_report).violates_instance_rules), 0) AS unresolved_report_count
FROM select_old_and_new_rows AS old_and_new_rows GROUP BY (post_report).post_id) AS diff
WHERE (diff.report_count, diff.unresolved_report_count) != (0, 0)
AND a.post_id = diff.post_id;
AND a.post_id = diff.post_id;
RETURN NULL;
@@ -411,10 +412,11 @@ BEGIN
report_count = a.report_count + diff.report_count, unresolved_report_count = a.unresolved_report_count + diff.unresolved_report_count
FROM (
SELECT
(comment_report).comment_id, coalesce(sum(count_diff), 0) AS report_count, coalesce(sum(count_diff) FILTER (WHERE NOT (comment_report).resolved), 0) AS unresolved_report_count
FROM select_old_and_new_rows AS old_and_new_rows GROUP BY (comment_report).comment_id) AS diff
(comment_report).comment_id, coalesce(sum(count_diff), 0) AS report_count, coalesce(sum(count_diff) FILTER (WHERE NOT (comment_report).resolved
AND NOT (comment_report).violates_instance_rules), 0) AS unresolved_report_count
FROM select_old_and_new_rows AS old_and_new_rows GROUP BY (comment_report).comment_id) AS diff
WHERE (diff.report_count, diff.unresolved_report_count) != (0, 0)
AND a.comment_id = diff.comment_id;
AND a.comment_id = diff.comment_id;
RETURN NULL;

View File

@@ -179,6 +179,7 @@ diesel::table! {
resolver_id -> Nullable<Int4>,
published -> Timestamptz,
updated -> Nullable<Timestamptz>,
violates_instance_rules -> Bool,
}
}
@@ -917,6 +918,7 @@ diesel::table! {
resolver_id -> Nullable<Int4>,
published -> Timestamptz,
updated -> Nullable<Timestamptz>,
violates_instance_rules -> Bool,
}
}

View File

@@ -30,6 +30,7 @@ pub struct CommentReport {
pub published: DateTime<Utc>,
#[cfg_attr(feature = "full", ts(optional))]
pub updated: Option<DateTime<Utc>>,
pub violates_instance_rules: bool,
}
#[derive(Clone)]
@@ -40,4 +41,5 @@ pub struct CommentReportForm {
pub comment_id: CommentId,
pub original_comment_text: String,
pub reason: String,
pub violates_instance_rules: bool,
}

View File

@@ -37,6 +37,7 @@ pub struct PostReport {
pub published: DateTime<Utc>,
#[cfg_attr(feature = "full", ts(optional))]
pub updated: Option<DateTime<Utc>>,
pub violates_instance_rules: bool,
}
#[derive(Clone, Default)]
@@ -49,4 +50,5 @@ pub struct PostReportForm {
pub original_post_url: Option<DbUrl>,
pub original_post_body: Option<String>,
pub reason: String,
pub violates_instance_rules: bool,
}

View File

@@ -8,6 +8,7 @@ use crate::structs::{
ReportCombinedView,
ReportCombinedViewInternal,
};
use chrono::{DateTime, Days, Utc};
use diesel::{
result::Error,
BoolExpressionMethods,
@@ -200,13 +201,10 @@ impl ReportCombinedViewInternal {
);
}
// If its not an admin, get only the ones you mod
if !user.local_user.admin {
query = query.filter(
community_actions::became_moderator
.is_not_null()
.and(report_combined::community_report_id.is_null()),
);
if user.local_user.admin {
query = query.filter(filter_admin_reports(Utc::now() - Days::new(3)));
} else {
query = query.filter(filter_mod_reports());
}
query.first::<i64>(conn).await
@@ -255,6 +253,8 @@ pub struct ReportCombinedQuery {
pub post_id: Option<PostId>,
pub community_id: Option<CommunityId>,
pub unresolved_only: Option<bool>,
/// For admins, also show reports with `violates_instance_rules=false`
pub show_community_rule_violations: Option<bool>,
pub page_after: Option<PaginationCursorData>,
pub page_back: Option<bool>,
}
@@ -322,17 +322,17 @@ impl ReportCombinedQuery {
);
}
if let Some(post_id) = self.post_id {
query = query.filter(post::id.eq(post_id));
if user.local_user.admin {
let show_community_rule_violations = self.show_community_rule_violations.unwrap_or_default();
if !show_community_rule_violations {
query = query.filter(filter_admin_reports(Utc::now() - Days::new(3)));
}
} else {
query = query.filter(filter_mod_reports());
}
// If its not an admin, get only the ones you mod
if !user.local_user.admin {
query = query.filter(
community_actions::became_moderator
.is_not_null()
.and(report_combined::community_report_id.is_null()),
);
if let Some(post_id) = self.post_id {
query = query.filter(post::id.eq(post_id));
}
let mut query = PaginatedQueryBuilder::new(query);
@@ -390,6 +390,38 @@ impl ReportCombinedQuery {
}
}
/// Mods can only see reports for posts/comments inside of communities where they are moderator,
/// and which have `violates_instance_rules == false`.
#[diesel::dsl::auto_type]
fn filter_mod_reports() -> _ {
community_actions::became_moderator
.is_not_null()
// Reporting a community or private message must go to admins
.and(report_combined::community_report_id.is_null())
.and(report_combined::private_message_report_id.is_null())
.and(filter_violates_instance_rules().is_distinct_from(true))
}
/// Admins can see reports intended for them, or mod reports older than 3 days. Also reports
/// on communities, person and private messages.
#[diesel::dsl::auto_type]
fn filter_admin_reports(interval: DateTime<Utc>) -> _ {
filter_violates_instance_rules()
.or(report_combined::published.lt(interval))
// Also show community reports where the admin is a community mod
.or(community_actions::became_moderator.is_not_null())
}
/// Filter reports which are only for admins (either post/comment report with
/// `violates_instance_rules=true`, or report on a community/person/private message.
#[diesel::dsl::auto_type]
fn filter_violates_instance_rules() -> _ {
post_report::violates_instance_rules
.or(comment_report::violates_instance_rules)
.or(report_combined::community_report_id.is_not_null())
.or(report_combined::private_message_report_id.is_not_null())
}
impl InternalToCombinedView for ReportCombinedViewInternal {
type CombinedView = ReportCombinedView;
@@ -509,9 +541,13 @@ mod tests {
ReportCombinedViewInternal,
},
};
use chrono::{Days, Utc};
use diesel::{update, ExpressionMethods, QueryDsl};
use diesel_async::RunQueryDsl;
use lemmy_db_schema::{
aggregates::structs::{CommentAggregates, PostAggregates},
assert_length,
schema::report_combined,
source::{
comment::{Comment, CommentInsertForm},
comment_report::{CommentReport, CommentReportForm},
@@ -527,7 +563,7 @@ mod tests {
private_message_report::{PrivateMessageReport, PrivateMessageReportForm},
},
traits::{Crud, Joinable, Reportable},
utils::{build_db_pool_for_tests, DbPool},
utils::{build_db_pool_for_tests, get_conn, DbPool},
ReportType,
};
use lemmy_utils::error::LemmyResult;
@@ -665,6 +701,7 @@ mod tests {
original_post_url: None,
original_post_body: None,
reason: "from sara".into(),
violates_instance_rules: false,
};
let inserted_post_report = PostReport::report(pool, &sara_report_post_form).await?;
@@ -674,6 +711,7 @@ mod tests {
comment_id: data.comment.id,
original_comment_text: "A test comment rv".into(),
reason: "from sara".into(),
violates_instance_rules: false,
};
CommentReport::report(pool, &sara_report_comment_form).await?;
@@ -695,9 +733,12 @@ mod tests {
PrivateMessageReport::report(pool, &pm_report_form).await?;
// Do a batch read of admins reports
let reports = ReportCombinedQuery::default()
.list(pool, &data.admin_view)
.await?;
let reports = ReportCombinedQuery {
show_community_rule_violations: Some(true),
..Default::default()
}
.list(pool, &data.admin_view)
.await?;
assert_length!(4, reports);
// Make sure the report types are correct
@@ -726,16 +767,19 @@ mod tests {
panic!("wrong type");
}
let report_count_mod =
ReportCombinedViewInternal::get_report_count(pool, &data.timmy_view, None).await?;
assert_eq!(2, report_count_mod);
let report_count_admin =
ReportCombinedViewInternal::get_report_count(pool, &data.admin_view, None).await?;
assert_eq!(4, report_count_admin);
assert_eq!(2, report_count_admin);
// Make sure the type_ filter is working
let reports_by_type = ReportCombinedQuery {
type_: Some(ReportType::Posts),
..Default::default()
}
.list(pool, &data.admin_view)
.list(pool, &data.timmy_view)
.await?;
assert_length!(1, reports_by_type);
@@ -745,7 +789,7 @@ mod tests {
post_id: Some(data.post.id),
..Default::default()
}
.list(pool, &data.admin_view)
.list(pool, &data.timmy_view)
.await?;
assert_length!(2, reports_by_post_id);
@@ -823,9 +867,12 @@ mod tests {
};
let pm_report = PrivateMessageReport::report(pool, &pm_report_form).await?;
let reports = ReportCombinedQuery::default()
.list(pool, &data.admin_view)
.await?;
let reports = ReportCombinedQuery {
show_community_rule_violations: Some(true),
..Default::default()
}
.list(pool, &data.admin_view)
.await?;
assert_length!(1, reports);
if let ReportCombinedView::PrivateMessage(v) = &reports[0] {
assert!(!v.private_message_report.resolved);
@@ -875,6 +922,7 @@ mod tests {
original_post_url: None,
original_post_body: None,
reason: "from sara".into(),
violates_instance_rules: false,
};
PostReport::report(pool, &sara_report_form).await?;
@@ -887,6 +935,7 @@ mod tests {
original_post_url: None,
original_post_body: None,
reason: "from jessica".into(),
violates_instance_rules: false,
};
let inserted_jessica_report = PostReport::report(pool, &jessica_report_form).await?;
@@ -1004,6 +1053,7 @@ mod tests {
comment_id: data.comment.id,
original_comment_text: "this was it at time of creation".into(),
reason: "from sara".into(),
violates_instance_rules: false,
};
CommentReport::report(pool, &sara_report_form).await?;
@@ -1014,6 +1064,7 @@ mod tests {
comment_id: data.comment.id,
original_comment_text: "this was it at time of creation".into(),
reason: "from jessica".into(),
violates_instance_rules: false,
};
let inserted_jessica_report = CommentReport::report(pool, &jessica_report_form).await?;
@@ -1126,9 +1177,12 @@ mod tests {
};
let community_report = CommunityReport::report(pool, &community_report_form).await?;
let reports = ReportCombinedQuery::default()
.list(pool, &data.admin_view)
.await?;
let reports = ReportCombinedQuery {
show_community_rule_violations: Some(true),
..Default::default()
}
.list(pool, &data.admin_view)
.await?;
assert_length!(1, reports);
if let ReportCombinedView::Community(v) = &reports[0] {
assert!(!v.community_report.resolved);
@@ -1143,9 +1197,12 @@ mod tests {
// admin resolves the report (after taking appropriate action)
CommunityReport::resolve(pool, community_report.id, data.admin_view.person.id).await?;
let reports = ReportCombinedQuery::default()
.list(pool, &data.admin_view)
.await?;
let reports = ReportCombinedQuery {
show_community_rule_violations: Some(true),
..Default::default()
}
.list(pool, &data.admin_view)
.await?;
assert_length!(1, reports);
if let ReportCombinedView::Community(v) = &reports[0] {
assert!(v.community_report.resolved);
@@ -1162,4 +1219,94 @@ mod tests {
Ok(())
}
#[tokio::test]
#[serial]
async fn test_violates_instance_rules() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let data = init_data(pool).await?;
// create report to admins
let report_form = PostReportForm {
creator_id: data.sara.id,
post_id: data.post_2.id,
original_post_name: "Orig post".into(),
original_post_url: None,
original_post_body: None,
reason: "from sara".into(),
violates_instance_rules: true,
};
PostReport::report(pool, &report_form).await?;
// timmy is a mod and cannot see the report
let mod_reports = ReportCombinedQuery::default()
.list(pool, &data.timmy_view)
.await?;
assert_length!(0, mod_reports);
let count = ReportCombinedViewInternal::get_report_count(pool, &data.timmy_view, None).await?;
assert_eq!(0, count);
// only admin can see the report
let admin_reports = ReportCombinedQuery::default()
.list(pool, &data.admin_view)
.await?;
assert_length!(1, admin_reports);
let count = ReportCombinedViewInternal::get_report_count(pool, &data.admin_view, None).await?;
assert_eq!(1, count);
// cleanup the report for easier checks below
Post::delete(pool, data.post_2.id).await?;
// now create a mod report
let report_form = CommentReportForm {
creator_id: data.sara.id,
comment_id: data.comment.id,
original_comment_text: "this was it at time of creation".into(),
reason: "from sara".into(),
violates_instance_rules: false,
};
let comment_report = CommentReport::report(pool, &report_form).await?;
// this time the mod can see it
let mod_reports = ReportCombinedQuery::default()
.list(pool, &data.timmy_view)
.await?;
assert_length!(1, mod_reports);
let count = ReportCombinedViewInternal::get_report_count(pool, &data.timmy_view, None).await?;
assert_eq!(1, count);
// but not the admin
let admin_reports = ReportCombinedQuery::default()
.list(pool, &data.admin_view)
.await?;
assert_length!(0, admin_reports);
let count = ReportCombinedViewInternal::get_report_count(pool, &data.admin_view, None).await?;
assert_eq!(0, count);
// admin can see the report with `view_mod_reports` set
let admin_reports = ReportCombinedQuery {
show_community_rule_violations: Some(true),
..Default::default()
}
.list(pool, &data.timmy_view)
.await?;
assert_length!(1, admin_reports);
// change a comment to be 3 days old, now admin can also see it by default
update(
report_combined::table.filter(report_combined::dsl::comment_report_id.eq(comment_report.id)),
)
.set(report_combined::published.eq(Utc::now() - Days::new(3)))
.execute(&mut get_conn(pool).await?)
.await?;
let admin_reports = ReportCombinedQuery::default()
.list(pool, &data.admin_view)
.await?;
assert_length!(1, admin_reports);
cleanup(data, pool).await?;
Ok(())
}
}

View File

@@ -0,0 +1,6 @@
ALTER TABLE post_report
DROP COLUMN violates_instance_rules;
ALTER TABLE comment_report
DROP COLUMN violates_instance_rules;

View File

@@ -0,0 +1,6 @@
ALTER TABLE post_report
ADD COLUMN violates_instance_rules bool NOT NULL DEFAULT FALSE;
ALTER TABLE comment_report
ADD COLUMN violates_instance_rules bool NOT NULL DEFAULT FALSE;