diff --git a/server/src/core/server/services/comments/pipeline/helpers.ts b/server/src/core/server/services/comments/pipeline/helpers.ts index 3c9738ca13..2f859b04b1 100644 --- a/server/src/core/server/services/comments/pipeline/helpers.ts +++ b/server/src/core/server/services/comments/pipeline/helpers.ts @@ -6,12 +6,16 @@ export function mergePhaseResult( result: Partial, final: Partial ) { - const { actions = [], tags = [], metadata = {} } = final; + const { commentActions = [], tags = [], metadata = {} } = final; // If this result contained actions, then we should push it into the // other actions. - if (result.actions) { - final.actions = [...actions, ...result.actions]; + if (result.commentActions) { + final.commentActions = [...commentActions, ...result.commentActions]; + } + + if (result.moderationAction) { + final.moderationAction = result.moderationAction; } // If this result contained metadata, then we should merge it into the diff --git a/server/src/core/server/services/comments/pipeline/phases/detectLinks.ts b/server/src/core/server/services/comments/pipeline/phases/detectLinks.ts index 8833307ed6..dc0914e00f 100755 --- a/server/src/core/server/services/comments/pipeline/phases/detectLinks.ts +++ b/server/src/core/server/services/comments/pipeline/phases/detectLinks.ts @@ -31,7 +31,7 @@ export const detectLinks: IntermediateModerationPhase = ({ // Add the flag related to Trust to the comment. return { status: GQLCOMMENT_STATUS.SYSTEM_WITHHELD, - actions: [ + commentActions: [ { actionType: ACTION_TYPE.FLAG, reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_LINKS, diff --git a/server/src/core/server/services/comments/pipeline/phases/external.ts b/server/src/core/server/services/comments/pipeline/phases/external.ts index ed1f7c5b7a..1aaa64faea 100644 --- a/server/src/core/server/services/comments/pipeline/phases/external.ts +++ b/server/src/core/server/services/comments/pipeline/phases/external.ts @@ -103,8 +103,8 @@ export interface ExternalModerationRequest { } export type ExternalModerationResponse = Partial< - Pick ->; + Pick +> & { actions: PhaseResult["commentActions"] }; const ExternalModerationResponseSchema = Joi.object().keys({ actions: Joi.array().items( @@ -267,6 +267,19 @@ async function processPhase( return validateResponse(body); } +/** + * Our external API still just has a concept of "actions", while + * internally we distinguish beteween "moderationActions" and "commentActions" + */ +const mapActions = ( + response: ExternalModerationResponse +): Partial => { + return { + ...response, + commentActions: response.actions, + }; +}; + export const external: IntermediateModerationPhase = async (ctx) => { // Check to see if any custom moderation phases have been defined, if there is // none, exit now. @@ -317,8 +330,10 @@ export const external: IntermediateModerationPhase = async (ctx) => { }, }); + const mappedResponse = mapActions(response); + // Merge the results in. If we're finished, return now! - const finished = mergePhaseResult(response, result); + const finished = mergePhaseResult(mappedResponse, result); if (finished) { return result; } diff --git a/server/src/core/server/services/comments/pipeline/phases/recentCommentHistory.ts b/server/src/core/server/services/comments/pipeline/phases/recentCommentHistory.ts index b199917129..74d9a88d66 100644 --- a/server/src/core/server/services/comments/pipeline/phases/recentCommentHistory.ts +++ b/server/src/core/server/services/comments/pipeline/phases/recentCommentHistory.ts @@ -47,7 +47,7 @@ export const recentCommentHistory = async ({ if (rate >= tenant.recentCommentHistory.triggerRejectionRate) { return { status: GQLCOMMENT_STATUS.SYSTEM_WITHHELD, - actions: [ + commentActions: [ { actionType: ACTION_TYPE.FLAG, reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_RECENT_HISTORY, diff --git a/server/src/core/server/services/comments/pipeline/phases/repeatPost.ts b/server/src/core/server/services/comments/pipeline/phases/repeatPost.ts index 00245be9ce..3dfeb1ba87 100644 --- a/server/src/core/server/services/comments/pipeline/phases/repeatPost.ts +++ b/server/src/core/server/services/comments/pipeline/phases/repeatPost.ts @@ -106,7 +106,7 @@ export const repeatPost: IntermediateModerationPhase = async ({ return { status: GQLCOMMENT_STATUS.SYSTEM_WITHHELD, - actions: [ + commentActions: [ { actionType: ACTION_TYPE.FLAG, reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_REPEAT_POST, diff --git a/server/src/core/server/services/comments/pipeline/phases/spam.ts b/server/src/core/server/services/comments/pipeline/phases/spam.ts index e1c381cbef..e8d7a38014 100644 --- a/server/src/core/server/services/comments/pipeline/phases/spam.ts +++ b/server/src/core/server/services/comments/pipeline/phases/spam.ts @@ -114,7 +114,7 @@ export const spam: IntermediateModerationPhase = async ({ return { status: GQLCOMMENT_STATUS.SYSTEM_WITHHELD, - actions: [ + commentActions: [ { actionType: ACTION_TYPE.FLAG, reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_SPAM, diff --git a/server/src/core/server/services/comments/pipeline/phases/statusPreModerateNewCommenter.ts b/server/src/core/server/services/comments/pipeline/phases/statusPreModerateNewCommenter.ts index 41d80b11af..1b164b1dfe 100644 --- a/server/src/core/server/services/comments/pipeline/phases/statusPreModerateNewCommenter.ts +++ b/server/src/core/server/services/comments/pipeline/phases/statusPreModerateNewCommenter.ts @@ -72,7 +72,7 @@ export const statusPreModerateNewCommenter = async ({ return { status: GQLCOMMENT_STATUS.SYSTEM_WITHHELD, - actions: [ + commentActions: [ { actionType: ACTION_TYPE.FLAG, reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_NEW_COMMENTER, diff --git a/server/src/core/server/services/comments/pipeline/phases/toxic.ts b/server/src/core/server/services/comments/pipeline/phases/toxic.ts index 2897ed08a3..ef39f49f9d 100644 --- a/server/src/core/server/services/comments/pipeline/phases/toxic.ts +++ b/server/src/core/server/services/comments/pipeline/phases/toxic.ts @@ -128,7 +128,7 @@ export const toxic: IntermediateModerationPhase = async ({ return { status: GQLCOMMENT_STATUS.SYSTEM_WITHHELD, - actions: [ + commentActions: [ { actionType: ACTION_TYPE.FLAG, reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_TOXIC, diff --git a/server/src/core/server/services/comments/pipeline/phases/wordList/phase.ts b/server/src/core/server/services/comments/pipeline/phases/wordList/phase.ts index 34e8174b49..358606d939 100644 --- a/server/src/core/server/services/comments/pipeline/phases/wordList/phase.ts +++ b/server/src/core/server/services/comments/pipeline/phases/wordList/phase.ts @@ -31,12 +31,16 @@ export const wordListPhase: IntermediateModerationPhase = async ({ if (banned.isMatched) { return { status: GQLCOMMENT_STATUS.REJECTED, - actions: [ + commentActions: [ { actionType: ACTION_TYPE.FLAG, reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_BANNED_WORD, }, ], + moderationAction: { + status: GQLCOMMENT_STATUS.REJECTED, + moderatorID: null, + }, metadata: { wordList: { bannedWords: banned.matches, @@ -45,7 +49,7 @@ export const wordListPhase: IntermediateModerationPhase = async ({ }; } else if (banned.timedOut) { return { - actions: [ + commentActions: [ { actionType: ACTION_TYPE.FLAG, reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_BANNED_WORD, @@ -68,7 +72,7 @@ export const wordListPhase: IntermediateModerationPhase = async ({ if (tenant.premoderateSuspectWords && suspect.isMatched) { return { status: GQLCOMMENT_STATUS.SYSTEM_WITHHELD, - actions: [ + commentActions: [ { actionType: ACTION_TYPE.FLAG, reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_SUSPECT_WORD, @@ -82,7 +86,7 @@ export const wordListPhase: IntermediateModerationPhase = async ({ }; } else if (suspect.isMatched) { return { - actions: [ + commentActions: [ { actionType: ACTION_TYPE.FLAG, reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_SUSPECT_WORD, @@ -96,7 +100,7 @@ export const wordListPhase: IntermediateModerationPhase = async ({ }; } else if (suspect.timedOut) { return { - actions: [ + commentActions: [ { actionType: ACTION_TYPE.FLAG, reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_SUSPECT_WORD, diff --git a/server/src/core/server/services/comments/pipeline/pipeline.spec.ts b/server/src/core/server/services/comments/pipeline/pipeline.spec.ts index 26abc5d25d..fba3d9068a 100644 --- a/server/src/core/server/services/comments/pipeline/pipeline.spec.ts +++ b/server/src/core/server/services/comments/pipeline/pipeline.spec.ts @@ -31,7 +31,7 @@ describe("compose", () => { body: context.comment.body, status, metadata: {}, - actions: [], + commentActions: [], tags: [], }); }); @@ -48,7 +48,7 @@ describe("compose", () => { body: context.comment.body, status, metadata: { akismet: false, linkCount: 1 }, - actions: [], + commentActions: [], tags: [], }); }); @@ -71,14 +71,14 @@ describe("compose", () => { const enhanced = compose([ () => ({ - actions: [flags[0]], + commentActions: [flags[0]], }), () => ({ status, - actions: [flags[1]], + commentActions: [flags[1]], }), () => ({ - actions: [ + commentActions: [ { userID: null, actionType: ACTION_TYPE.FLAG, @@ -91,10 +91,10 @@ describe("compose", () => { const final = await enhanced(context); for (const flag of flags) { - expect(final.actions).toContainEqual(flag); + expect(final.commentActions).toContainEqual(flag); } - expect(final.actions).not.toContainEqual({ + expect(final.commentActions).not.toContainEqual({ body: context.comment.body, actionType: ACTION_TYPE.FLAG, reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_LINKS, @@ -111,7 +111,7 @@ describe("compose", () => { body: context.comment.body, status: GQLCOMMENT_STATUS.NONE, metadata: { akismet: false }, - actions: [], + commentActions: [], tags: [], }); }); diff --git a/server/src/core/server/services/comments/pipeline/pipeline.ts b/server/src/core/server/services/comments/pipeline/pipeline.ts index d9e5e4fa61..66f6e24abe 100644 --- a/server/src/core/server/services/comments/pipeline/pipeline.ts +++ b/server/src/core/server/services/comments/pipeline/pipeline.ts @@ -4,6 +4,7 @@ import { Config } from "coral-server/config"; import { MongoContext } from "coral-server/data/context"; import { Logger } from "coral-server/logger"; import { CreateActionInput } from "coral-server/models/action/comment"; +import { CreateCommentModerationActionInput } from "coral-server/models/action/moderation/comment"; import { Comment, CreateCommentInput, @@ -26,16 +27,26 @@ import { mergePhaseResult } from "./helpers"; import { moderationPhases } from "./phases"; import { WordListService } from "./phases/wordList/service"; -export type ModerationAction = Omit< +export type CommentAction = Omit< CreateActionInput, "commentID" | "commentRevisionID" | "storyID" | "siteID" | "userID" >; +export type ModerationAction = Omit< + CreateCommentModerationActionInput, + "commentID" | "commentRevisionID" | "storyID" | "siteID" | "userID" +>; + export interface PhaseResult { /** - * actions are moderation actions that are added to the comment revision. + * moderationActions are moderation actions that are added to the comment revision. + */ + moderationAction?: ModerationAction; + + /** + * commentActions are comment actions that are added to the comment revision. */ - actions: ModerationAction[]; + commentActions: CommentAction[]; /** * status when provided decides and terminates the moderation process by @@ -122,7 +133,7 @@ export const compose = const final: PhaseResult = { status: GQLCOMMENT_STATUS.NONE, body: context.comment.body, - actions: [], + commentActions: [], metadata: { // Merge in the passed comment metadata. ...(context.comment.metadata || {}), diff --git a/server/src/core/server/stacks/createComment.ts b/server/src/core/server/stacks/createComment.ts index 65aab158da..5c15b08366 100644 --- a/server/src/core/server/stacks/createComment.ts +++ b/server/src/core/server/stacks/createComment.ts @@ -43,7 +43,7 @@ import { import { ensureFeatureFlag, Tenant } from "coral-server/models/tenant"; import { User } from "coral-server/models/user"; import { isSiteBanned } from "coral-server/models/user/helpers"; -import { removeTag } from "coral-server/services/comments"; +import { moderate, removeTag } from "coral-server/services/comments"; import { addCommentActions, CreateAction, @@ -347,11 +347,11 @@ export default async function create( // is added, that it can already know that the comment is already in the // queue. let actionCounts: EncodedCommentActionCounts = {}; - if (result.actions.length > 0) { + if (result.commentActions.length > 0) { // Determine the unique actions, we will use this to compute the comment // action counts. This should match what is added below. actionCounts = encodeActionCounts( - ...filterDuplicateActions(result.actions) + ...filterDuplicateActions(result.commentActions) ); } @@ -424,13 +424,13 @@ export default async function create( log.trace("pushed child comment id onto parent"); } - if (result.actions.length > 0) { + if (result.commentActions.length > 0) { // Actually add the actions to the database. This will not interact with the // counts at all. await addCommentActions( mongo, tenant, - result.actions.map( + result.commentActions.map( (action): CreateAction => ({ ...action, commentID: comment.id, @@ -447,6 +447,28 @@ export default async function create( ); } + if (result.moderationAction) { + // Actually add the actions to the database. This will not interact with the + // counts at all. + await moderate( + mongo, + redis, + config, + i18n, + tenant, + { + ...result.moderationAction, + commentID: comment.id, + commentRevisionID: revision.id, + }, + now, + false, + { + actionCounts, + } + ); + } + // Update all the comment counts on stories and users. const counts = await updateAllCommentCounts(mongo, redis, config, i18n, { tenant, diff --git a/server/src/core/server/stacks/editComment.ts b/server/src/core/server/stacks/editComment.ts index 0981d178c4..dd45455dc5 100644 --- a/server/src/core/server/stacks/editComment.ts +++ b/server/src/core/server/stacks/editComment.ts @@ -17,7 +17,7 @@ import { EncodedCommentActionCounts, filterDuplicateActions, } from "coral-server/models/action/comment"; -import { createCommentModerationAction } from "coral-server/models/action/moderation/comment"; + import { editComment, EditCommentInput, @@ -34,6 +34,7 @@ import { resolveStoryMode, retrieveStory } from "coral-server/models/story"; import { Tenant } from "coral-server/models/tenant"; import { User } from "coral-server/models/user"; import { isSiteBanned } from "coral-server/models/user/helpers"; +import { moderate } from "coral-server/services/comments"; import { addCommentActions, CreateAction, @@ -181,33 +182,36 @@ export default async function edit( } // Run the comment through the moderation phases. - const { body, status, metadata, actions } = await processForModeration({ - action: "EDIT", - log, - mongo, - redis, - config, - wordList, - tenant, - story, - storyMode, - parent, - comment: { - ...originalStaleComment, - ...input, - authorID: author.id, - }, - media, - author, - req, - now, - }); + const { body, status, metadata, commentActions, moderationAction } = + await processForModeration({ + action: "EDIT", + log, + mongo, + redis, + config, + wordList, + tenant, + story, + storyMode, + parent, + comment: { + ...originalStaleComment, + ...input, + authorID: author.id, + }, + media, + author, + req, + now, + }); let actionCounts: EncodedCommentActionCounts = {}; - if (actions.length > 0) { + if (commentActions.length > 0) { // Encode the new action counts that are going to be added to the new // revision. - actionCounts = encodeActionCounts(...filterDuplicateActions(actions)); + actionCounts = encodeActionCounts( + ...filterDuplicateActions(commentActions) + ); } // Perform the edit. @@ -234,11 +238,11 @@ export default async function edit( // If there were actions to insert, then insert them into the commentActions // collection. - if (actions.length > 0) { + if (commentActions.length > 0) { await addCommentActions( mongo, tenant, - actions.map( + commentActions.map( (action): CreateAction => ({ ...action, commentID: result.after.id, @@ -255,20 +259,25 @@ export default async function edit( ); } - // If the comment status changed as a result of a pipeline operation, create a - // moderation action (but don't associate it with a moderator). - if (result.before.status !== result.after.status) { - await createCommentModerationAction( + if (moderationAction) { + // Actually add the actions to the database. This will not interact with the + // counts at all. + await moderate( mongo, - tenant.id, + redis, + config, + i18n, + tenant, { - storyID: story.id, + ...moderationAction, commentID: result.after.id, - commentRevisionID: result.revision.id, - status: result.after.status, - moderatorID: null, + commentRevisionID: lastRevision.id, }, - now + now, + false, + { + actionCounts, + } ); }