Skip to content

Commit

Permalink
[CORL-810] Custom Moderation Phases (#2901)
Browse files Browse the repository at this point in the history
* feat: initial implementation

* feat: renamed fields from mutations

* fix: more renaming to streamline {Key,Secret}->SigningSecret

* feat: introduced WrappedInternalError

* feat: enhanced extern payload, more fetch options

- Added tenant.{id,domain} to extern payload
- Added site.id to the extern payload
- Added response size limit to fetch
- Added new SCRAPE_MAX_RESPONSE_SIZE env var for managing the size of
  responses for scraping

* fix: fixed bug with scrape invocation

* feat: added more queries + mutations

- Added Query.externalModerationPhase
- Added Mutation.createExternalModerationPhase
- Added Mutation.updateExternalModerationPhase
- Added Mutation.enableExternalModerationPhase
- Added Mutation.disableExternalModerationPhase
- Added Mutation.deleteExternalModerationPhase
- Added Mutation.rotateExternalModerationPhaseSigningSecret

* feat: added secret management

* fix: linting

* fix: merge conflict fix

* feat: added UI

* fix: linting

* fix: linting

* fix: updated snapshots

* fix: improved docs

* fix: improved docs

* fix: added locales

* review: improve naming

* review: some review changes

- Switched /moderation/phase to /moderation/phases
- Fixed scrolling
- Fixed redirection

* fix: added scroll timeout for webhooks
  • Loading branch information
wyattjoh committed May 14, 2020
1 parent ceb96db commit ed92f49
Show file tree
Hide file tree
Showing 154 changed files with 5,441 additions and 1,690 deletions.
240 changes: 240 additions & 0 deletions EXTERNAL_MODERATION_PHASES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
# External Moderation Phases Guide

This document is in reference to external moderation phases emitted by Coral.
You can configure external moderation phases on your installation of Coral by
visiting `/admin/configure/moderation/phases`.

Once you've configured a external moderation phase in Coral, you will start to
receive moderation requests in the form of a
[External Moderation Requests](#external-moderation-request) at the provided
callback URL. These will be in the form of `POST` requests with a `JSON`
payload.

When a comment is created or edited, it will be processed by moderation phases in
a predefined order. Any external moderation phase is run last, and only if all
other moderation phases before it do not return a status. The current set of
moderation phases is listed in order [here](https://github.com/coralproject/talk/blob/master/src/core/server/services/comments/pipeline/phases/index.ts).

Once you have received a moderation request, you must respond within the
provided timeout else the phase will be skipped and it will continue. It is
strongly recommended to [verify the request signature](#request-signing).

The external moderation phase must respond with one of the following:

1. Do not moderate the comment, and return a 204 without a body.
2. Perform a moderation action and return a 200 with a [External Moderation Response](#external-moderation-response)
as a `JSON` encoded body containing the operations you want to perform on the
comment.

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table of Contents

- [Request Signing](#request-signing)
- [Schema](#schema)
- [External Moderation Request](#external-moderation-request)
- [External Moderation Response](#external-moderation-response)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## Request Signing

Requests sent by Coral for external moderation phases use the same process as
those used by webhooks. Refer to the [webhooks documentation](WEBHOOKS.md#webhook-signing)
for instructions on how to verify signatures sent by Coral.

## Schema

### External Moderation Request

```ts
interface ExternalModerationRequest {
/**
* action refers to the specific operation being performed. If `NEW`, this
* is referring to a new comment being created. If `EDIT`, then this refers to
* an operation involving an edit operation on an existing Comment.
*/
action: "NEW" | "EDIT";

/**
* comment refers to the actual Comment data for the Comment being
* created/edited.
*/
comment: {
/**
* body refers to the actual body text of the Comment being created/edited.
*/
body: string;

/**
* parentID is the identifier for the parent comment (if this Comment is a
* reply, null otherwise).
*/
parentID: string | null;
};

/**
* author refers to the User that is creating/editing the Comment.
*/
author: {
/**
* id is the identifier for this User.
*/
id: string;

/**
* role refers to the role of this User.
*/
role: "COMMENTER" | "STAFF" | "MODERATOR" | "ADMIN";
};

/**
* story refers to the Story being commented on.
*/
story: {
/**
* id is the identifier for this Story.
*/
id: string;

/**
* url is the URL for this Story.
*/
url: string;
};

/**
* site refers to the Site that the story being commented on belongs to.
*/
site: {
/**
* id is the identifier for this Site.
*/
id: string;
};

/**
* tenantID is the identifer of the Tenant that this Comment is being
* created/edited on.
*/
tenantID: string;

/**
* tenantDomain is the domain that is associated with this Tenant that this
* Comment is being created/edited on.
*/
tenantDomain: string;
}
```

#### Example

New comment on a story:

```json
{
"action": "NEW",
"comment": {
"body": "Here's a comment!",
"parentID": null
},
"author": {
"id": "baf4e943-3594-4fcc-b2ba-3e8de7a76352",
"role": "COMMENTER"
},
"story": {
"id": "245b3856-b0a0-4d2f-a6bb-58c71f18d6a6",
"url": "http://localhost:1313/posts/a-story-url/"
},
"site": {
"id": "a4bede88-2d2c-4424-bc18-4322a9e285a6"
},
"tenantID": "19ba5794-7eeb-4d46-a81b-c00c61672501",
"tenantDomain": "localhost"
}
```

New reply on a comment on a story:

```json
{
"action": "NEW",
"comment": {
"body": "Here's a reply!",
"parentID": "d79b787f-f406-49a0-a179-72e3652e54be"
},
"author": {
"id": "baf4e943-3594-4fcc-b2ba-3e8de7a76352",
"role": "COMMENTER"
},
"story": {
"id": "245b3856-b0a0-4d2f-a6bb-58c71f18d6a6",
"url": "http://localhost:1313/posts/a-story-url/"
},
"site": {
"id": "a4bede88-2d2c-4424-bc18-4322a9e285a6"
},
"tenantID": "19ba5794-7eeb-4d46-a81b-c00c61672501",
"tenantDomain": "localhost"
}
```

### External Moderation Response

```ts
interface ExternalModerationResponse {
/**
* actions is an optional list of any flags to be added to this Comment.
*/
actions?: Array<{
actionType: "FLAG";
reason: "COMMENT_DETECTED_TOXIC" | "COMMENT_DETECTED_SPAM";
}>;

/**
* tags are any listed tags that should be added to the comment.
*/
tags?: Array<"FEATURED" | "STAFF">;

/**
* status when provided decides and terminates the moderation process by
* setting the status of the comment.
*/
status?: "NONE" | "APPROVED" | "REJECTED" | "PREMOD" | "SYSTEM_WITHHELD";
}
```

#### Examples

Add a flag to a comment and do not set a status:

```json
{
"actions": [{ "actionType": "FLAG", "reason": "COMMENT_DETECTED_TOXIC" }]
}
```

Reject a comment:

```json
{
"status": "REJECTED"
}
```

Feature a comment and do not set a status:

```json
{
"tags": ["FEATURED"]
}
```

Approve a comment and mark it as featured:

```json
{
"status": "APPROVED",
"tags": ["FEATURED"]
}
```
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright 2019 Vox Media, Inc
Copyright 2020 Vox Media, Inc

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"build:server": "gulp server",
"migration:create": "ts-node --transpile-only ./scripts/migration/create.ts",
"docs:events": "ts-node ./scripts/generateEventDocs.ts ./src/core/client/stream/events.ts ./CLIENT_EVENTS.md",
"doctoc": "doctoc --maxlevel=3 --title '## Table of Contents' README.md CLIENT_EVENTS.md CONTRIBUTING.md WEBHOOKS.md",
"doctoc": "doctoc --maxlevel=3 --title '## Table of Contents' README.md CLIENT_EVENTS.md CONTRIBUTING.md WEBHOOKS.md EXTERNAL_MODERATION_PHASES.md",
"generate": "npm-run-all generate:css-types generate:schema generate:relay",
"generate-persist": "npm-run-all generate:css-types generate:schema generate:relay-persist",
"generate:css-types": "tcm src/core/client/",
Expand Down Expand Up @@ -413,7 +413,7 @@
"{src/core/client/stream/events.ts,scripts/generateEventDocs.ts,CLIENT_EVENTS.md}": [
"npm run docs:events -- --verify"
],
"{README,CLIENT_EVENTS,CONTRIBUTING,WEBHOOKS}.md": [
"{README,CLIENT_EVENTS,CONTRIBUTING,WEBHOOKS,EXTERNAL_MODERATION_PHASES}.md": [
"npm run doctoc"
]
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { urls } from "coral-framework/helpers";

export default function getExternalModerationPhaseLink(phaseID: string) {
return `${urls.admin.configureExternalModerationPhase}/${phaseID}`;
}
21 changes: 20 additions & 1 deletion src/core/client/admin/routeConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,22 @@ import { createAuthCheckRoute } from "./routes/AuthCheck";
import CommunityRoute from "./routes/Community";
import ConfigureRoute from "./routes/Configure";
import {
AddExternalModerationPhaseRoute,
AddWebhookEndpointRoute,
AdvancedConfigRoute,
AuthConfigRoute,
ConfigureExternalModerationPhaseRoute,
ConfigureWebhookEndpointRoute,
EmailConfigRoute,
GeneralConfigRoute,
ModerationConfigRoute,
ModerationPhasesConfigRoute,
OrganizationConfigRoute,
SlackConfigRoute,
WebhookEndpointsConfigRoute,
WordListConfigRoute,
} from "./routes/Configure/sections";
import ModerationPhasesLayout from "./routes/Configure/sections/ModerationPhases/ModerationPhasesLayout";
import { Sites } from "./routes/Configure/sections/Sites";
import AddSiteRoute from "./routes/Configure/sections/Sites/AddSiteRoute";
import SiteRoute from "./routes/Configure/sections/Sites/SiteRoute";
Expand Down Expand Up @@ -120,13 +124,28 @@ export default makeRouteConfig(
path="organization"
{...OrganizationConfigRoute.routeConfig}
/>
<Route path="moderation" {...ModerationConfigRoute.routeConfig} />
<Route
exact
path="moderation"
{...ModerationConfigRoute.routeConfig}
/>
<Route path="wordList" {...WordListConfigRoute.routeConfig} />
<Route path="auth" {...AuthConfigRoute.routeConfig} />
<Route path="advanced" {...AdvancedConfigRoute.routeConfig} />
<Route path="email" {...EmailConfigRoute.routeConfig} />
<Route path="slack" {...SlackConfigRoute.routeConfig} />
</Route>
<Route
path="configure/moderation/phases"
Component={ModerationPhasesLayout}
>
<Route path="/" {...ModerationPhasesConfigRoute.routeConfig} />
<Route path="add" Component={AddExternalModerationPhaseRoute} />
<Route
path=":phaseID"
{...ConfigureExternalModerationPhaseRoute.routeConfig}
/>
</Route>
<Route path="configure/webhooks" Component={WebhookEndpointsLayout}>
<Route path="/" {...WebhookEndpointsConfigRoute.routeConfig} />
<Route path="add" {...AddWebhookEndpointRoute.routeConfig} />
Expand Down
7 changes: 6 additions & 1 deletion src/core/client/admin/routes/Configure/ConfigureLinks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ const ConfigureLinks: FunctionComponent<{}> = () => {
<Link to="/admin/configure/organization">Organization</Link>
</Localized>
<Localized id="configure-sideBarNavigation-moderation">
<Link to="/admin/configure/moderation">Moderation</Link>
<Link exact to="/admin/configure/moderation">
Moderation
</Link>
</Localized>
<Localized id="configure-sideBarNavigation-moderationPhases">
<Link to="/admin/configure/moderation/phases">Moderation Phases</Link>
</Localized>
<Localized id="configure-sideBarNavigation-bannedAndSuspectWords">
<Link to="/admin/configure/wordList">Banned and Suspect Words</Link>
Expand Down
2 changes: 2 additions & 0 deletions src/core/client/admin/routes/Configure/Link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ interface Props {
className?: string;
children: React.ReactNode;
to: string | LocationDescriptor;
exact?: boolean;
}

const Link: FunctionComponent<Props> = (props) => (
Expand All @@ -15,6 +16,7 @@ const Link: FunctionComponent<Props> = (props) => (
to={props.to}
className={styles.link}
activeClassName={styles.linkActive}
exact={props.exact}
>
{props.children}
</FoundLink>
Expand Down

This file was deleted.

Loading

0 comments on commit ed92f49

Please sign in to comment.