From d6db287c55155a70917710126c9ff196bab9bc2b Mon Sep 17 00:00:00 2001 From: Tessa Thornton Date: Fri, 21 Feb 2020 12:38:40 -0500 Subject: [PATCH] [CORL-882] option to reject all a user's comments when banning (#2827) * show comment counts for stories in story table * remove debug code * add rejector task * connect comment rejection job to user banning * localize strings * remove debug code * remove debug code * resolve merge conflicts * add documentation to rejectExistingComments * clean up rejector task * add TODO about broker * make rejectExistingComments nullable --- .../ModerateCard/ModerateCardContainer.tsx | 8 +- .../admin/components/UserStatus/BanModal.tsx | 16 +- .../UserStatus/UserStatusChangeContainer.tsx | 4 +- .../client/admin/routes/Stories/StoryRow.css | 29 ++- .../client/admin/routes/Stories/StoryRow.tsx | 60 ++++-- .../routes/Stories/StoryRowContainer.tsx | 14 ++ .../admin/routes/Stories/StoryTable.css | 28 ++- .../admin/routes/Stories/StoryTable.tsx | 29 ++- src/core/client/admin/test/fixtures.ts | 39 ++++ .../__snapshots__/stories.spec.tsx.snap | 194 +++++++++++++----- .../Comment/UserBanPopover/BanUserMutation.ts | 3 +- .../UserBanPopoverContainer.tsx | 1 + .../test/comments/stream/moderation.spec.tsx | 1 + src/core/server/app/handlers/api/graphql.ts | 1 + src/core/server/app/index.ts | 2 + src/core/server/graph/context.ts | 4 + src/core/server/graph/mutators/Users.ts | 2 + src/core/server/graph/schema/schema.graphql | 5 + src/core/server/index.ts | 2 + src/core/server/queue/index.ts | 10 +- src/core/server/queue/tasks/rejector.ts | 124 +++++++++++ src/core/server/services/users/users.ts | 13 ++ src/core/server/stacks/rejectComment.ts | 18 +- src/locales/en-US/admin.ftl | 4 + 24 files changed, 491 insertions(+), 120 deletions(-) create mode 100644 src/core/server/queue/tasks/rejector.ts diff --git a/src/core/client/admin/components/ModerateCard/ModerateCardContainer.tsx b/src/core/client/admin/components/ModerateCard/ModerateCardContainer.tsx index fbfeb65a5..66ac30b3d 100644 --- a/src/core/client/admin/components/ModerateCard/ModerateCardContainer.tsx +++ b/src/core/client/admin/components/ModerateCard/ModerateCardContainer.tsx @@ -191,9 +191,13 @@ const ModerateCardContainer: FunctionComponent = ({ }, [comment]); const handleBanConfirm = useCallback( - async (message: string) => { + async (rejectExistingComments: boolean, message: string) => { if (comment.author) { - await banUser({ userID: comment.author.id, message }); + await banUser({ + userID: comment.author.id, + message, + rejectExistingComments, + }); } setShowBanModal(false); }, diff --git a/src/core/client/admin/components/UserStatus/BanModal.tsx b/src/core/client/admin/components/UserStatus/BanModal.tsx index c1a753831..4a7092a76 100644 --- a/src/core/client/admin/components/UserStatus/BanModal.tsx +++ b/src/core/client/admin/components/UserStatus/BanModal.tsx @@ -22,7 +22,7 @@ interface Props { username: string | null; open: boolean; onClose: () => void; - onConfirm: (message?: string) => void; + onConfirm: (rejectExistingComments: boolean, message?: string) => void; getMessage: GetMessage; } @@ -44,8 +44,8 @@ const BanModal: FunctionComponent = ({ }, [getMessage, username]); const onFormSubmit = useCallback( - ({ emailMessage }) => { - onConfirm(emailMessage); + ({ emailMessage, rejectExistingComments }) => { + onConfirm(rejectExistingComments, emailMessage); }, [onConfirm] ); @@ -83,12 +83,22 @@ const BanModal: FunctionComponent = ({ onSubmit={onFormSubmit} initialValues={{ showMessage: false, + rejectExistingComments: false, emailMessage: getDefaultMessage, }} > {({ handleSubmit }) => (
+ + {({ input }) => ( + + + Reject all comments by this user + + + )} + {({ input }) => ( diff --git a/src/core/client/admin/components/UserStatus/UserStatusChangeContainer.tsx b/src/core/client/admin/components/UserStatus/UserStatusChangeContainer.tsx index bc1366e23..82fa686fd 100644 --- a/src/core/client/admin/components/UserStatus/UserStatusChangeContainer.tsx +++ b/src/core/client/admin/components/UserStatus/UserStatusChangeContainer.tsx @@ -111,8 +111,8 @@ const UserStatusChangeContainer: FunctionComponent = props => { ); const handleBanConfirm = useCallback( - message => { - banUser({ userID: user.id, message }); + (rejectExistingComments, message) => { + banUser({ userID: user.id, message, rejectExistingComments }); setShowBanned(false); }, [user, setShowBanned] diff --git a/src/core/client/admin/routes/Stories/StoryRow.css b/src/core/client/admin/routes/Stories/StoryRow.css index e282f348d..899cbaba2 100644 --- a/src/core/client/admin/routes/Stories/StoryRow.css +++ b/src/core/client/admin/routes/Stories/StoryRow.css @@ -4,18 +4,25 @@ padding-top: 15px; padding-bottom: 15px; } -.authorColumn { - vertical-align: top; - word-break: break-word; - padding-top: 15px; - padding-bottom: 15px; -} -.publishDateColumn { - vertical-align: top; - padding-top: 15px; - padding-bottom: 15px; -} .statusColumn { } .siteColumn { } + +.meta { + color: var(--v2-colors-mono-100); +} + +.authorName { + font-weight: var(--v2-font-weight-primary-semi-bold); +} + +.reportedCountColumn, +.pendingCountColumn, +.totalCountColumn { + text-align: center; +} + +.boldColumn { + font-weight: var(--v2-font-weight-primary-semi-bold); +} diff --git a/src/core/client/admin/routes/Stories/StoryRow.tsx b/src/core/client/admin/routes/Stories/StoryRow.tsx index 751adb8ad..dc3a346cf 100644 --- a/src/core/client/admin/routes/Stories/StoryRow.tsx +++ b/src/core/client/admin/routes/Stories/StoryRow.tsx @@ -1,10 +1,16 @@ +import cn from "classnames"; import { Link } from "found"; import React, { FunctionComponent } from "react"; import NotAvailable from "coral-admin/components/NotAvailable"; import { getModerationLink } from "coral-framework/helpers"; import { PropTypesOf } from "coral-framework/types"; -import { TableCell, TableRow, TextLink } from "coral-ui/components/v2"; +import { + HorizontalGutter, + TableCell, + TableRow, + TextLink, +} from "coral-ui/components/v2"; import StoryStatus from "./StoryStatus"; @@ -20,27 +26,51 @@ interface Props { siteName: string; siteID: string; multisite: boolean; + reportedCount: number | null; + pendingCount: number | null; + totalCount: number; } const UserRow: FunctionComponent = props => ( - - {props.title || } - + +

+ + {props.title || } + +

+ {(props.author || props.publishDate) && ( +

+ {props.author}{" "} + {props.publishDate} +

+ )} +
- - {props.author || } + 0, + })} + > + {props.reportedCount} - {props.multisite && ( - - - {props.siteName} - - - )} - - {props.publishDate || } + 0, + })} + > + {props.pendingCount} + + 0, + })} + > + {props.totalCount} diff --git a/src/core/client/admin/routes/Stories/StoryRowContainer.tsx b/src/core/client/admin/routes/Stories/StoryRowContainer.tsx index 81156b06e..b057d0b2d 100644 --- a/src/core/client/admin/routes/Stories/StoryRowContainer.tsx +++ b/src/core/client/admin/routes/Stories/StoryRowContainer.tsx @@ -30,6 +30,9 @@ const StoryRowContainer: FunctionComponent = props => { siteName={props.story.site.name} siteID={props.story.site.id} multisite={props.multisite} + totalCount={props.story.commentCounts.totalPublished} + reportedCount={props.story.moderationQueues.reported.count} + pendingCount={props.story.moderationQueues.pending.count} publishDate={ publishedAt ? new Intl.DateTimeFormat(locales, { @@ -61,6 +64,17 @@ const enhanced = withFragmentContainer({ author publishedAt } + commentCounts { + totalPublished + } + moderationQueues { + reported { + count + } + pending { + count + } + } site { name id diff --git a/src/core/client/admin/routes/Stories/StoryTable.css b/src/core/client/admin/routes/Stories/StoryTable.css index 9d9468128..0a1c26196 100644 --- a/src/core/client/admin/routes/Stories/StoryTable.css +++ b/src/core/client/admin/routes/Stories/StoryTable.css @@ -1,25 +1,31 @@ $tableHeaderAltTextColor: var(--v2-colors-mono-100); .titleColumn { - width: 50%; + width: 60%; } .titleColumnNarrow { - width: 32.5%; -} -.authorColumn { - width: 17.5%; -} -.publishDateColumn { - width: 17.5%; + width: 42.5%; } .statusColumn { - width: 15%; + width: 14%; } -.siteColumn { - width: 17.5%; +.reportedCountColumn { + width: 9%; +} +.pendingCountColumn { + width: 9%; +} +.totalCountColumn { + width: 9%; } .clickToModerate { font-size: var(--v2-font-size-2); font-weight: var(--v2-font-weight-primary-semi-bold); color: $tableHeaderAltTextColor; } + +.reportedCountColumn, +.pendingCountColumn, +.totalCountColumn { + text-align: center; +} diff --git a/src/core/client/admin/routes/Stories/StoryTable.tsx b/src/core/client/admin/routes/Stories/StoryTable.tsx index 0d6793b8b..e44fd13bb 100644 --- a/src/core/client/admin/routes/Stories/StoryTable.tsx +++ b/src/core/client/admin/routes/Stories/StoryTable.tsx @@ -1,5 +1,4 @@ import { Localized } from "@fluent/react/compat"; -import cn from "classnames"; import React, { FunctionComponent } from "react"; import AutoLoadMore from "coral-admin/components/AutoLoadMore"; @@ -38,11 +37,7 @@ const StoryTable: FunctionComponent = props => ( - + Title {" "} @@ -55,19 +50,19 @@ const StoryTable: FunctionComponent = props => ( - - Author - - {props.multisite && ( - - Site - - )} - - - Publish Date + + + Reported + + + Pending + + + + Total + Status diff --git a/src/core/client/admin/test/fixtures.ts b/src/core/client/admin/test/fixtures.ts index 2c74572fc..159374816 100644 --- a/src/core/client/admin/test/fixtures.ts +++ b/src/core/client/admin/test/fixtures.ts @@ -492,6 +492,19 @@ export const stories = createFixtures([ title: "Finally a Cure for Cancer", publishedAt: "2018-11-29T16:01:51.897Z", }, + commentCounts: { + totalPublished: 5, + }, + moderationQueues: { + reported: { + id: "reported", + count: 3, + }, + pending: { + id: "pending", + count: 2, + }, + }, site: sites[0], }, { @@ -506,6 +519,19 @@ export const stories = createFixtures([ title: "First Colony on Mars", publishedAt: "2018-11-29T16:01:51.897Z", }, + commentCounts: { + totalPublished: 5, + }, + moderationQueues: { + reported: { + id: "reported", + count: 3, + }, + pending: { + id: "pending", + count: 2, + }, + }, site: sites[1], }, { @@ -515,6 +541,19 @@ export const stories = createFixtures([ isClosed: true, status: GQLSTORY_STATUS.CLOSED, url: "", + commentCounts: { + totalPublished: 5, + }, + moderationQueues: { + reported: { + id: "reported", + count: 3, + }, + pending: { + id: "pending", + count: 2, + }, + }, metadata: { author: undefined, title: "World hunger has been defeated", diff --git a/src/core/client/admin/test/stories/__snapshots__/stories.spec.tsx.snap b/src/core/client/admin/test/stories/__snapshots__/stories.spec.tsx.snap index bb67e420c..d1e7e37f8 100644 --- a/src/core/client/admin/test/stories/__snapshots__/stories.spec.tsx.snap +++ b/src/core/client/admin/test/stories/__snapshots__/stories.spec.tsx.snap @@ -155,14 +155,19 @@ exports[`renders empty stories 1`] = ` + + + + + +
- Author + Reported - Publish Date + Pending + + Total - - Finally a Cure for Cancer - +

+ + Finally a Cure for Cancer + +

+

+ + Vin Hoa + + + 11/29/2018, 4:01 PM +

+
- Vin Hoa + 3 - 11/29/2018, 4:01 PM + 2 + + 5 - - First Colony on Mars - +

+ + First Colony on Mars + +

+

+ + Linh Nguyen + + + 11/29/2018, 4:01 PM +

+
- Linh Nguyen + 3 - 11/29/2018, 4:01 PM + 2 + + 5 - Author + Reported - Publish Date + Pending + + Total - - Finally a Cure for Cancer - +

+ + Finally a Cure for Cancer + +

+

+ + Vin Hoa + + + 11/29/2018, 4:01 PM +

+
- Vin Hoa + 3 - 11/29/2018, 4:01 PM + 2 + + 5 - - First Colony on Mars - +

+ + First Colony on Mars + +

+

+ + Linh Nguyen + + + 11/29/2018, 4:01 PM +

+
- Linh Nguyen + 3 - 11/29/2018, 4:01 PM + 2 + + 5 = ({ banUser({ userID: user.id, commentID: comment.id, + rejectExistingComments: false, message: getMessage( localeBundles, "common-banEmailTemplate", diff --git a/src/core/client/stream/test/comments/stream/moderation.spec.tsx b/src/core/client/stream/test/comments/stream/moderation.spec.tsx index 8b37be323..e8c6008d8 100644 --- a/src/core/client/stream/test/comments/stream/moderation.spec.tsx +++ b/src/core/client/stream/test/comments/stream/moderation.spec.tsx @@ -229,6 +229,7 @@ it("ban user", async () => { banUser: ({ variables }) => { expectAndFail(variables).toMatchObject({ userID: firstComment.author!.id, + rejectExistingComments: false, }); return { user: pureMerge(firstComment.author, { diff --git a/src/core/server/app/handlers/api/graphql.ts b/src/core/server/app/handlers/api/graphql.ts index 513d6dc3a..ccb0f3ef9 100644 --- a/src/core/server/app/handlers/api/graphql.ts +++ b/src/core/server/app/handlers/api/graphql.ts @@ -18,6 +18,7 @@ export type GraphMiddlewareOptions = Pick< | "tenantCache" | "metrics" | "broker" + | "rejectorQueue" >; export const graphQLHandler = ({ diff --git a/src/core/server/app/index.ts b/src/core/server/app/index.ts index 652314d8b..a4bd51ce2 100644 --- a/src/core/server/app/index.ts +++ b/src/core/server/app/index.ts @@ -18,6 +18,7 @@ import { Config } from "coral-server/config"; import CoralEventListenerBroker from "coral-server/events/publisher"; import logger from "coral-server/logger"; import { MailerQueue } from "coral-server/queue/tasks/mailer"; +import { RejectorQueue } from "coral-server/queue/tasks/rejector"; import { ScraperQueue } from "coral-server/queue/tasks/scraper"; import { I18n } from "coral-server/services/i18n"; import { JWTSigningConfig } from "coral-server/services/jwt"; @@ -52,6 +53,7 @@ export interface AppOptions { tenantCache: TenantCache; migrationManager: MigrationManager; broker: CoralEventListenerBroker; + rejectorQueue: RejectorQueue; } /** diff --git a/src/core/server/graph/context.ts b/src/core/server/graph/context.ts index 92a9b5b84..e688e4aa9 100644 --- a/src/core/server/graph/context.ts +++ b/src/core/server/graph/context.ts @@ -12,6 +12,7 @@ import { PersistedQuery } from "coral-server/models/queries"; import { Tenant } from "coral-server/models/tenant"; import { User } from "coral-server/models/user"; import { MailerQueue } from "coral-server/queue/tasks/mailer"; +import { RejectorQueue } from "coral-server/queue/tasks/rejector"; import { ScraperQueue } from "coral-server/queue/tasks/scraper"; import { I18n } from "coral-server/services/i18n"; import { JWTSigningConfig } from "coral-server/services/jwt"; @@ -40,6 +41,7 @@ export interface GraphContextOptions { mongo: Db; pubsub: RedisPubSub; redis: AugmentedRedis; + rejectorQueue: RejectorQueue; scraperQueue: ScraperQueue; tenant: Tenant; tenantCache: TenantCache; @@ -61,6 +63,7 @@ export default class GraphContext { public readonly now: Date; public readonly pubsub: RedisPubSub; public readonly redis: AugmentedRedis; + public readonly rejectorQueue: RejectorQueue; public readonly scraperQueue: ScraperQueue; public readonly tenant: Tenant; public readonly tenantCache: TenantCache; @@ -94,6 +97,7 @@ export default class GraphContext { this.tenantCache = options.tenantCache; this.scraperQueue = options.scraperQueue; this.mailerQueue = options.mailerQueue; + this.rejectorQueue = options.rejectorQueue; this.signingConfig = options.signingConfig; this.clientID = options.clientID; diff --git a/src/core/server/graph/mutators/Users.ts b/src/core/server/graph/mutators/Users.ts index 932b73412..b7231871f 100644 --- a/src/core/server/graph/mutators/Users.ts +++ b/src/core/server/graph/mutators/Users.ts @@ -229,10 +229,12 @@ export const Users = (ctx: GraphContext) => ({ ban( ctx.mongo, ctx.mailerQueue, + ctx.rejectorQueue, ctx.tenant, ctx.user!, input.userID, input.message, + input.rejectExistingComments || false, ctx.now ), premodUser: async (input: GQLPremodUserInput) => diff --git a/src/core/server/graph/schema/schema.graphql b/src/core/server/graph/schema/schema.graphql index 7a17b35ee..15df340dd 100644 --- a/src/core/server/graph/schema/schema.graphql +++ b/src/core/server/graph/schema/schema.graphql @@ -5491,6 +5491,11 @@ input BanUserInput { message is sent to banned user via email. """ message: String! + + """ + whether or not to reject all the user's previous comments when banning them. + """ + rejectExistingComments: Boolean } type BanUserPayload { diff --git a/src/core/server/index.ts b/src/core/server/index.ts index d43de169c..53ebee64e 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -253,6 +253,7 @@ class Server { this.tasks.scraper.process(); this.tasks.notifier.process(); this.tasks.webhook.process(); + this.tasks.rejector.process(); // Start up the cron job processors. this.scheduledTasks = startScheduledTasks({ @@ -354,6 +355,7 @@ class Server { i18n: this.i18n, mailerQueue: this.tasks.mailer, scraperQueue: this.tasks.scraper, + rejectorQueue: this.tasks.rejector, disableClientRoutes, persistedQueryCache: this.persistedQueryCache, persistedQueriesRequired: diff --git a/src/core/server/queue/index.ts b/src/core/server/queue/index.ts index f64f9bd6d..3e366d2f4 100644 --- a/src/core/server/queue/index.ts +++ b/src/core/server/queue/index.ts @@ -1,15 +1,15 @@ import Queue from "bull"; -import { Redis } from "ioredis"; import { Db } from "mongodb"; import { Config } from "coral-server/config"; import { I18n } from "coral-server/services/i18n"; import { JWTSigningConfig } from "coral-server/services/jwt"; -import { createRedisClient } from "coral-server/services/redis"; +import { AugmentedRedis, createRedisClient } from "coral-server/services/redis"; import TenantCache from "coral-server/services/tenant/cache"; import { createMailerTask, MailerQueue } from "./tasks/mailer"; import { createNotifierTask, NotifierQueue } from "./tasks/notifier"; +import { createRejectorTask, RejectorQueue } from "./tasks/rejector"; import { createScraperTask, ScraperQueue } from "./tasks/scraper"; import { createWebhookTask, WebhookQueue } from "./tasks/webhook"; @@ -49,7 +49,7 @@ export interface QueueOptions { tenantCache: TenantCache; i18n: I18n; signingConfig: JWTSigningConfig; - redis: Redis; + redis: AugmentedRedis; } export interface TaskQueue { @@ -57,6 +57,7 @@ export interface TaskQueue { scraper: ScraperQueue; notifier: NotifierQueue; webhook: WebhookQueue; + rejector: RejectorQueue; } export async function createQueue(options: QueueOptions): Promise { @@ -73,11 +74,14 @@ export async function createQueue(options: QueueOptions): Promise { }); const webhook = createWebhookTask(queueOptions, options); + const rejector = createRejectorTask(queueOptions, options); + // Return the tasks + client. return { mailer, scraper, notifier, webhook, + rejector, }; } diff --git a/src/core/server/queue/tasks/rejector.ts b/src/core/server/queue/tasks/rejector.ts new file mode 100644 index 000000000..3f16ed0cb --- /dev/null +++ b/src/core/server/queue/tasks/rejector.ts @@ -0,0 +1,124 @@ +import Queue, { Job } from "bull"; +import { Db } from "mongodb"; +import now from "performance-now"; + +import { Config } from "coral-server/config"; +import logger from "coral-server/logger"; +import { + Comment, + getLatestRevision, + retrieveAllCommentsUserConnection, +} from "coral-server/models/comment"; +import { Connection } from "coral-server/models/helpers"; +import Task from "coral-server/queue/Task"; +import { AugmentedRedis } from "coral-server/services/redis"; +import TenantCache from "coral-server/services/tenant/cache"; +import { rejectComment } from "coral-server/stacks"; + +import { GQLCOMMENT_SORT } from "coral-server/graph/schema/__generated__/types"; + +const JOB_NAME = "rejector"; + +export interface RejectorProcessorOptions { + mongo: Db; + redis: AugmentedRedis; + config: Config; + tenantCache: TenantCache; +} + +export interface RejectorData { + authorID: string; + moderatorID: string; + tenantID: string; +} + +function getBatch( + mongo: Db, + tenantID: string, + authorID: string, + connection?: Readonly>> +) { + return retrieveAllCommentsUserConnection(mongo, tenantID, authorID, { + orderBy: GQLCOMMENT_SORT.CREATED_AT_DESC, + first: 100, + after: connection ? connection.pageInfo.endCursor : undefined, + }); +} + +const createJobProcessor = ({ + mongo, + redis, + tenantCache, + config, +}: RejectorProcessorOptions) => async (job: Job) => { + // Pull out the job data. + const { authorID, moderatorID, tenantID } = job.data; + const log = logger.child( + { + jobID: job.id, + jobName: JOB_NAME, + authorID, + moderatorID, + tenantID, + }, + true + ); + // Mark the start time. + const startTime = now(); + log.debug("starting to reject author comments"); + // Get the tenant. + const tenant = await tenantCache.retrieveByID(tenantID); + if (!tenant) { + log.error("referenced tenant was not found"); + return; + } + // Get the current time. + const currentTime = new Date(); + try { + // Find all comments written by the author that should be rejected. + let connection = await getBatch(mongo, tenantID, authorID); + while (connection.nodes.length > 0) { + for (const comment of connection.nodes) { + // Get the latest revision of the comment. + const revision = getLatestRevision(comment); + // Reject the comment. + await rejectComment( + mongo, + redis, + config, + null, + tenant, + comment.id, + revision.id, + moderatorID, + currentTime + ); + } + // If there was not another page, abort processing. + if (!connection.pageInfo.hasNextPage) { + break; + } + // Load the next page. + connection = await getBatch(mongo, tenantID, authorID, connection); + } + } catch (err) { + log.error({ err }, "could not reject the author's comments"); + throw err; + } + // Compute the end time. + const took = Math.round(now() - startTime); + log.debug({ took }, "rejected the author's comments"); +}; + +export type RejectorQueue = Task; + +export function createRejectorTask( + queue: Queue.QueueOptions, + options: RejectorProcessorOptions +) { + return new Task({ + jobName: JOB_NAME, + jobProcessor: createJobProcessor(options), + queue, + }); +} diff --git a/src/core/server/services/users/users.ts b/src/core/server/services/users/users.ts index da701733e..e33ece693 100644 --- a/src/core/server/services/users/users.ts +++ b/src/core/server/services/users/users.ts @@ -71,6 +71,7 @@ import { hasStaffRole, } from "coral-server/models/user/helpers"; import { MailerQueue } from "coral-server/queue/tasks/mailer"; +import { RejectorQueue } from "coral-server/queue/tasks/rejector"; import { JWTSigningConfig, signPATString } from "coral-server/services/jwt"; import { sendConfirmationEmail } from "coral-server/services/users/auth"; @@ -816,19 +817,23 @@ export async function destroyModeratorNote( * * @param mongo mongo database to interact with * @param mailer the mailer + * @param rejector the comment rejector queue * @param tenant Tenant where the User will be banned on * @param banner the User that is banning the User * @param userID the ID of the User being banned * @param message message to banned user + * @param rejectExistingComments whether all the authors previous comments should be rejected * @param now the current time that the ban took effect */ export async function ban( mongo: Db, mailer: MailerQueue, + rejector: RejectorQueue, tenant: Tenant, banner: User, userID: string, message: string, + rejectExistingComments: boolean, now = new Date() ) { // Get the user being banned to check to see if the user already has an @@ -847,6 +852,14 @@ export async function ban( // Ban the user. const user = await banUser(mongo, tenant.id, userID, banner.id, message, now); + if (rejectExistingComments) { + await rejector.add({ + tenantID: tenant.id, + authorID: userID, + moderatorID: banner.id, + }); + } + // If the user has an email address associated with their account, send them // a ban notification email. if (user.email) { diff --git a/src/core/server/stacks/rejectComment.ts b/src/core/server/stacks/rejectComment.ts index 3292d48e6..4d4c59200 100644 --- a/src/core/server/stacks/rejectComment.ts +++ b/src/core/server/stacks/rejectComment.ts @@ -20,7 +20,7 @@ const rejectComment = async ( mongo: Db, redis: AugmentedRedis, config: Config, - broker: CoralEventPublisherBroker, + broker: CoralEventPublisherBroker | null, tenant: Tenant, commentID: string, commentRevisionID: string, @@ -48,12 +48,16 @@ const rejectComment = async ( actionCounts: {}, }); - // Publish changes to the event publisher. - await publishChanges(broker, { - ...result, - ...counts, - moderatorID, - }); + // TODO: (wyattjoh) (tessalt) broker cannot easily be passed to stack from tasks, + // see CORL-935 in jira + if (broker) { + // Publish changes to the event publisher. + await publishChanges(broker, { + ...result, + ...counts, + moderatorID, + }); + } // If there was a featured tag on this comment, remove it. if (hasTag(result.after, GQLTAG.FEATURED)) { diff --git a/src/locales/en-US/admin.ftl b/src/locales/en-US/admin.ftl index 155d9ad33..ed3e277e0 100644 --- a/src/locales/en-US/admin.ftl +++ b/src/locales/en-US/admin.ftl @@ -833,6 +833,7 @@ community-banModal-consequence = community-banModal-cancel = Cancel community-banModal-banUser = Ban User community-banModal-customize = Customize ban email message +community-banModal-reject-existing = Reject all comments by this user community-suspendModal-areYouSure = Suspend { $username }? community-suspendModal-consequence = @@ -916,6 +917,9 @@ stories-column-author = Author stories-column-publishDate = Publish Date stories-column-status = Status stories-column-clickToModerate = Click title to moderate story +stories-column-reportedCount = Reported +stories-column-pendingCount = Pending +stories-column-totalCount = Total stories-status-popover = .description = A dropdown to change the story status