mirror of
https://github.com/wassname/talk.git
synced 2026-07-12 07:47:22 +08:00
[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:
@@ -18,6 +18,7 @@ export type GraphMiddlewareOptions = Pick<
|
||||
| "tenantCache"
|
||||
| "metrics"
|
||||
| "broker"
|
||||
| "rejectorQueue"
|
||||
>;
|
||||
|
||||
export const graphQLHandler = ({
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user