mirror of
https://github.com/wassname/talk.git
synced 2026-07-30 12:40:41 +08:00
[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:
committed by
Wyatt Johnson
co-authored by
Wyatt Johnson
parent
0dc3e8968a
commit
e3e2e0f52e
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user