mirror of
https://github.com/wassname/talk.git
synced 2026-07-29 11:28:24 +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,247 @@
|
||||
import { Db } from "mongodb";
|
||||
|
||||
import { ERROR_TYPES } from "coral-common/errors";
|
||||
import { Omit } from "coral-common/types";
|
||||
import { Config } from "coral-server/config";
|
||||
import {
|
||||
CommentNotFoundError,
|
||||
CoralError,
|
||||
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 {
|
||||
createComment,
|
||||
CreateCommentInput,
|
||||
pushChildCommentIDOntoParent,
|
||||
retrieveComment,
|
||||
} from "coral-server/models/comment";
|
||||
import {
|
||||
getLatestRevision,
|
||||
hasAncestors,
|
||||
hasPublishedStatus,
|
||||
} from "coral-server/models/comment/helpers";
|
||||
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 {
|
||||
PhaseResult,
|
||||
processForModeration,
|
||||
} from "coral-server/services/comments/pipeline";
|
||||
import {
|
||||
publishCommentCreated,
|
||||
publishCommentReplyCreated,
|
||||
} from "coral-server/services/events";
|
||||
import { AugmentedRedis } from "coral-server/services/redis";
|
||||
import { updateUserLastCommentID } from "coral-server/services/users";
|
||||
import { Request } from "coral-server/types/express";
|
||||
|
||||
import { publishChanges, updateAllCounts } from "./helpers";
|
||||
|
||||
export type CreateComment = Omit<
|
||||
CreateCommentInput,
|
||||
"status" | "metadata" | "ancestorIDs" | "actionCounts" | "tags"
|
||||
>;
|
||||
|
||||
export default async function create(
|
||||
mongo: Db,
|
||||
redis: AugmentedRedis,
|
||||
config: Config,
|
||||
publisher: Publisher,
|
||||
tenant: Tenant,
|
||||
author: User,
|
||||
input: CreateComment,
|
||||
nudge: boolean,
|
||||
now = new Date(),
|
||||
req?: Request
|
||||
) {
|
||||
let log = logger.child(
|
||||
{
|
||||
authorID: author.id,
|
||||
tenantID: tenant.id,
|
||||
storyID: input.storyID,
|
||||
parentID: input.parentID,
|
||||
nudge,
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
log.trace("creating comment on story");
|
||||
|
||||
// Grab the story that we'll use to check moderation pieces with.
|
||||
const story = await retrieveStory(mongo, tenant.id, input.storyID);
|
||||
if (!story) {
|
||||
throw new StoryNotFoundError(input.storyID);
|
||||
}
|
||||
|
||||
const ancestorIDs: string[] = [];
|
||||
if (input.parentID) {
|
||||
// Check to see that the reference parent ID exists.
|
||||
const parent = await retrieveComment(mongo, tenant.id, input.parentID);
|
||||
if (!parent) {
|
||||
throw new CommentNotFoundError(input.parentID);
|
||||
}
|
||||
|
||||
// Check that the parent comment was visible.
|
||||
if (!hasPublishedStatus(parent)) {
|
||||
throw new CommentNotFoundError(parent.id);
|
||||
}
|
||||
|
||||
ancestorIDs.push(input.parentID);
|
||||
if (hasAncestors(parent)) {
|
||||
// Push the parent's ancestors id's into the comment's ancestor id's.
|
||||
ancestorIDs.push(...parent.ancestorIDs);
|
||||
}
|
||||
|
||||
log.trace(
|
||||
{ ancestorIDs: ancestorIDs.length },
|
||||
"pushed parent ancestorIDs into comment"
|
||||
);
|
||||
}
|
||||
|
||||
let result: PhaseResult;
|
||||
try {
|
||||
// Run the comment through the moderation phases.
|
||||
result = await processForModeration({
|
||||
action: "NEW",
|
||||
log,
|
||||
mongo,
|
||||
redis,
|
||||
config,
|
||||
nudge,
|
||||
story,
|
||||
tenant,
|
||||
comment: input,
|
||||
author,
|
||||
req,
|
||||
now,
|
||||
});
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof CoralError &&
|
||||
err.type === ERROR_TYPES.MODERATION_NUDGE_ERROR
|
||||
) {
|
||||
log.info({ err }, "detected pipeline nudge");
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
const { actions, body, status, metadata, tags } = result;
|
||||
|
||||
// This is the first time this comment is being published.. So we need to
|
||||
// ensure we don't run into any race conditions when we create the comment.
|
||||
// One of the situations where we could encounter a race is when the comment
|
||||
// is created, and does not have it's flag data associated with it. This would
|
||||
// cause the comment to not be added to the flagged queue. If a flag is
|
||||
// pending, and a user flags this comment before the next step can proceed,
|
||||
// then we would end up double adding the comment to the flagged queue.
|
||||
// Instead, we need to add the action metadata to the comment before we add it
|
||||
// for the first time to ensure that the data is there for when the next flag
|
||||
// is added, that it can already know that the comment is already in the
|
||||
// queue.
|
||||
let actionCounts = {};
|
||||
if (actions.length > 0) {
|
||||
// Determine the unique actions, we will use this to compute the comment
|
||||
// action counts. This should match what is added below.
|
||||
const deDuplicatedActions = filterDuplicateActions(actions);
|
||||
|
||||
// Encode the action counts.
|
||||
actionCounts = encodeActionCounts(...deDuplicatedActions);
|
||||
}
|
||||
|
||||
// Create the comment!
|
||||
const comment = await createComment(
|
||||
mongo,
|
||||
tenant.id,
|
||||
{
|
||||
...input,
|
||||
tags,
|
||||
body,
|
||||
status,
|
||||
ancestorIDs,
|
||||
metadata,
|
||||
actionCounts,
|
||||
},
|
||||
now
|
||||
);
|
||||
|
||||
await updateUserLastCommentID(redis, tenant, author, comment.id);
|
||||
|
||||
// Pull the revision out.
|
||||
const revision = getLatestRevision(comment);
|
||||
|
||||
log = log.child(
|
||||
{ commentID: comment.id, status, revisionID: revision.id },
|
||||
true
|
||||
);
|
||||
|
||||
log.trace("comment created");
|
||||
|
||||
if (input.parentID) {
|
||||
// Push the child's ID onto the parent.
|
||||
await pushChildCommentIDOntoParent(
|
||||
mongo,
|
||||
tenant.id,
|
||||
input.parentID,
|
||||
comment.id
|
||||
);
|
||||
|
||||
log.trace("pushed child comment id onto parent");
|
||||
}
|
||||
|
||||
if (actions.length > 0) {
|
||||
// Actually add the actions to the database. This will not interact with the
|
||||
// counts at all.
|
||||
const upsertedActions = await addCommentActions(
|
||||
mongo,
|
||||
tenant,
|
||||
actions.map(
|
||||
(action): CreateAction => ({
|
||||
...action,
|
||||
commentID: comment.id,
|
||||
commentRevisionID: revision.id,
|
||||
|
||||
// Store the Story ID on the action.
|
||||
storyID: story.id,
|
||||
})
|
||||
),
|
||||
now
|
||||
);
|
||||
|
||||
log.trace({ actions: upsertedActions.length }, "added actions to comment");
|
||||
}
|
||||
|
||||
// Update all the comment counts on stories and users.
|
||||
const counts = await updateAllCounts(mongo, redis, {
|
||||
tenant,
|
||||
after: comment,
|
||||
});
|
||||
|
||||
// Publish changes to the event publisher.
|
||||
await publishChanges(publisher, {
|
||||
...counts,
|
||||
after: comment,
|
||||
moderatorID: null,
|
||||
});
|
||||
|
||||
// If this is a reply, publish it.
|
||||
if (input.parentID) {
|
||||
publishCommentReplyCreated(publisher, comment);
|
||||
}
|
||||
|
||||
// If this comment is visible (and not a reply), publish it.
|
||||
if (!input.parentID && hasPublishedStatus(comment)) {
|
||||
publishCommentCreated(publisher, comment);
|
||||
}
|
||||
|
||||
return comment;
|
||||
}
|
||||
Reference in New Issue
Block a user