[CORL-688] Add user comment count tracking (#2744)

* feat: initial impl

* Create preliminary comment moderation slices

CORL-688

* Move slices logic into stacks

CORL-688

* Create user comment counts

CORL-688

* Create naive mutation that initializes user comment counts

CORL-688

* Use bulk updates in user counts migration

CORL-688

* fix: review

* fix: fixed issue with aggregation

* Migrate creating comment into stacks

CORL-688

* Migrate editing a comment to the stacks

CORL-688

* Break publishing comment status out of updateAllCounts

CORL-688

* review: removed variable scoping in favor of export

* revert: feb8e8196cd448f5cd24f1ca2eb0b91fe9bd43c7

* review: simplification of stacks implementation

This simplifies the stacks implementation to better reuse code related
to count management and event publishing. This can be used to great
effect with the upcomming events PR #2738.

* fix: check if authorID is null before update user counts

CORL-688

Co-authored-by: Wyatt Johnson <accounts+github@wyattjoh.ca>
This commit is contained in:
Nick Funk
2020-01-07 21:00:25 +00:00
committed by Wyatt Johnson
co-authored by Wyatt Johnson
parent 0dc3e8968a
commit e3e2e0f52e
21 changed files with 987 additions and 867 deletions
+212
View File
@@ -0,0 +1,212 @@
import { DateTime } from "luxon";
import { Db } from "mongodb";
import { Omit } from "coral-common/types";
import { Config } from "coral-server/config";
import { CommentNotFoundError, StoryNotFoundError } from "coral-server/errors";
import { Publisher } from "coral-server/graph/tenant/subscriptions/publisher";
import logger from "coral-server/logger";
import {
encodeActionCounts,
filterDuplicateActions,
} from "coral-server/models/action/comment";
import { createCommentModerationAction } from "coral-server/models/action/moderation/comment";
import {
editComment,
EditCommentInput,
retrieveComment,
validateEditable,
} from "coral-server/models/comment";
import { retrieveStory } from "coral-server/models/story";
import { Tenant } from "coral-server/models/tenant";
import { User } from "coral-server/models/user";
import {
addCommentActions,
CreateAction,
} from "coral-server/services/comments/actions";
import { processForModeration } from "coral-server/services/comments/pipeline";
import { AugmentedRedis } from "coral-server/services/redis";
import { Request } from "coral-server/types/express";
import { publishChanges, updateAllCounts } from "./helpers";
/**
* getLastCommentEditableUntilDate will return the `createdAt` date that will
* represent the _oldest_ date that a comment could have been created on in
* order to still be editable.
*
* @param tenant the tenant that contains settings related editing
* @param now the date that is the base, defaulting to the current time
*/
function getLastCommentEditableUntilDate(
tenant: Pick<Tenant, "editCommentWindowLength">,
now = new Date()
): Date {
return (
DateTime.fromJSDate(now)
// editCommentWindowLength is in seconds, so multiply by 1000 to get
// milliseconds.
.minus(tenant.editCommentWindowLength * 1000)
.toJSDate()
);
}
export type EditComment = Omit<
EditCommentInput,
"status" | "authorID" | "lastEditableCommentCreatedAt" | "metadata"
>;
export default async function edit(
mongo: Db,
redis: AugmentedRedis,
config: Config,
publisher: Publisher,
tenant: Tenant,
author: User,
input: EditComment,
now = new Date(),
req?: Request
) {
let log = logger.child({ commentID: input.id, tenantID: tenant.id }, true);
// Get the comment that we're editing. This comment is considered stale,
// because it wasn't involved in the atomic transaction.
const originalStaleComment = await retrieveComment(
mongo,
tenant.id,
input.id
);
if (!originalStaleComment) {
throw new CommentNotFoundError(input.id);
}
// The editable time is based on the current time, and the edit window
// length. By subtracting the current date from the edit window length, we
// get the maximum value for the `createdAt` time that would be permitted
// for the comment edit to succeed.
const lastEditableCommentCreatedAt = getLastCommentEditableUntilDate(
tenant,
now
);
// Validate and potentially return with a more useful error.
validateEditable(originalStaleComment, {
authorID: author.id,
lastEditableCommentCreatedAt,
});
// Grab the story that we'll use to check moderation pieces with.
const story = await retrieveStory(
mongo,
tenant.id,
originalStaleComment.storyID
);
if (!story) {
throw new StoryNotFoundError(originalStaleComment.storyID);
}
// Run the comment through the moderation phases.
const { body, status, metadata, actions } = await processForModeration({
action: "EDIT",
log,
mongo,
redis,
config,
story,
tenant,
comment: input,
author,
req,
now,
});
let actionCounts = {};
if (actions.length > 0) {
// Encode the new action counts that are going to be added to the new
// revision.
actionCounts = encodeActionCounts(...filterDuplicateActions(actions));
}
log.trace(
{ predictedActionCounts: actionCounts },
"associating action counts with comment"
);
// Perform the edit.
const result = await editComment(
mongo,
tenant.id,
{
id: input.id,
authorID: author.id,
body,
status,
metadata,
actionCounts,
lastEditableCommentCreatedAt,
},
now
);
if (!result) {
throw new CommentNotFoundError(input.id);
}
log = log.child({ revisionID: result.revision.id }, true);
if (actions.length > 0) {
// Insert and handle creating the actions.
const upsertedActions = await addCommentActions(
mongo,
tenant,
actions.map(
(action): CreateAction => ({
...action,
commentID: result.after.id,
commentRevisionID: result.revision.id,
storyID: story.id,
})
),
now
);
log.trace(
{
actualActionCounts: encodeActionCounts(...upsertedActions),
actions: upsertedActions.length,
},
"added actions to comment"
);
}
// If the comment status changed as a result of a pipeline operation, create a
// moderation action.
if (result.before.status !== result.after.status) {
await createCommentModerationAction(
mongo,
tenant.id,
{
commentID: result.after.id,
commentRevisionID: result.revision.id,
status: result.after.status,
moderatorID: null,
},
now
);
}
// Update all the comment counts on stories and users.
const counts = await updateAllCounts(mongo, redis, {
tenant,
...result,
});
// Publish changes to the event publisher.
await publishChanges(publisher, {
...result,
...counts,
moderatorID: null,
});
// Return the resulting comment.
return result.after;
}