[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
This commit is contained in:
Tessa Thornton
2020-02-21 12:38:40 -05:00
committed by GitHub
parent ca52cc3253
commit d6db287c55
24 changed files with 491 additions and 120 deletions
@@ -18,6 +18,7 @@ export type GraphMiddlewareOptions = Pick<
| "tenantCache"
| "metrics"
| "broker"
| "rejectorQueue"
>;
export const graphQLHandler = ({
+2
View File
@@ -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;
}
/**
+4
View File
@@ -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;
+2
View File
@@ -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) =>
@@ -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 {
+2
View File
@@ -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:
+7 -3
View File
@@ -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<TaskQueue> {
@@ -73,11 +74,14 @@ export async function createQueue(options: QueueOptions): Promise<TaskQueue> {
});
const webhook = createWebhookTask(queueOptions, options);
const rejector = createRejectorTask(queueOptions, options);
// Return the tasks + client.
return {
mailer,
scraper,
notifier,
webhook,
rejector,
};
}
+124
View File
@@ -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<Connection<Readonly<Comment>>>
) {
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<RejectorData>) => {
// 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<RejectorData>;
export function createRejectorTask(
queue: Queue.QueueOptions,
options: RejectorProcessorOptions
) {
return new Task({
jobName: JOB_NAME,
jobProcessor: createJobProcessor(options),
queue,
});
}
+13
View File
@@ -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) {
+11 -7
View File
@@ -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)) {