feat: added toxic labels (#2396)

This commit is contained in:
Wyatt Johnson
2019-07-12 20:41:34 +00:00
committed by GitHub
parent b5b9cb7e2f
commit f95b705585
42 changed files with 727 additions and 526 deletions
@@ -6,6 +6,7 @@ import { Field } from "react-final-form";
import { formatPercentage, parsePercentage } from "coral-framework/lib/form";
import {
TOXICITY_ENDPOINT_DEFAULT,
TOXICITY_MODEL_DEFAULT,
TOXICITY_THRESHOLD_DEFAULT,
} from "coral-common/constants";
@@ -220,7 +221,7 @@ const PerspectiveConfig: FunctionComponent<Props> = ({ disabled }) => {
</Localized>
<Localized
id="configure-moderation-perspective-defaultEndpoint"
$default="https://commentanalyzer.googleapis.com/v1alpha1"
$default={TOXICITY_ENDPOINT_DEFAULT}
>
<InputDescription>
By default the endpoint is set to $default. You may override this
@@ -242,6 +243,7 @@ const PerspectiveConfig: FunctionComponent<Props> = ({ disabled }) => {
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
placeholder={TOXICITY_ENDPOINT_DEFAULT}
spellCheck={false}
/>
{meta.touched && (meta.error || meta.submitError) && (
@@ -1,3 +0,0 @@
.category {
color: var(--palette-error-darkest);
}
@@ -2,8 +2,6 @@ import React, { FunctionComponent } from "react";
import { HorizontalGutter, Typography } from "coral-ui/components";
import styles from "./FlagDetailsCategory.css";
interface Props {
category: React.ReactNode;
children?: React.ReactNode;
@@ -15,9 +13,7 @@ const FlagDetailsCategory: FunctionComponent<Props> = ({
}) => {
return (
<HorizontalGutter size="half">
<Typography variant="bodyCopyBold" className={styles.category}>
{category}
</Typography>
<Typography variant="bodyCopyBold">{category}</Typography>
<HorizontalGutter size="half">{children}</HorizontalGutter>
</HorizontalGutter>
);
@@ -1,80 +1,94 @@
import { Localized } from "fluent-react/compat";
import React from "react";
import React, { FunctionComponent } from "react";
import { graphql } from "react-relay";
import { FlagDetailsContainer_comment as CommentData } from "coral-admin/__generated__/FlagDetailsContainer_comment.graphql";
import { FlagDetailsContainer_comment } from "coral-admin/__generated__/FlagDetailsContainer_comment.graphql";
import { FlagDetailsContainer_settings } from "coral-admin/__generated__/FlagDetailsContainer_settings.graphql";
import NotAvailable from "coral-admin/components/NotAvailable";
import { TOXICITY_THRESHOLD_DEFAULT } from "coral-common/constants";
import { withFragmentContainer } from "coral-framework/lib/relay";
import { GQLCOMMENT_FLAG_REASON } from "coral-framework/schema";
import { HorizontalGutter } from "coral-ui/components";
import FlagDetailsCategory from "./FlagDetailsCategory";
import FlagDetailsEntry from "./FlagDetailsEntry";
import ToxicityLabel from "./ToxicityLabel";
interface Props {
comment: CommentData;
comment: FlagDetailsContainer_comment;
settings: FlagDetailsContainer_settings;
}
export class FlagDetailsContainer extends React.Component<Props> {
public render() {
const nodes = this.props.comment.flags.nodes;
const offensiveList: React.ReactElement[] = [];
const spamList: React.ReactElement[] = [];
nodes.forEach((n, i) => {
switch (n.reason) {
case GQLCOMMENT_FLAG_REASON.COMMENT_REPORTED_OFFENSIVE:
offensiveList.push(
const FlagDetailsContainer: FunctionComponent<Props> = ({
comment: {
revision: { metadata },
flags: { nodes },
},
settings,
}) => {
const offensive = nodes.filter(
({ reason }) => reason === GQLCOMMENT_FLAG_REASON.COMMENT_REPORTED_OFFENSIVE
);
const spam = nodes.filter(
({ reason }) => reason === GQLCOMMENT_FLAG_REASON.COMMENT_REPORTED_SPAM
);
return (
<HorizontalGutter size="oneAndAHalf">
{metadata.perspective && (
<FlagDetailsCategory
category={
<Localized id="moderate-flagDetails-toxicityScore">
<span>Toxicity Score</span>
</Localized>
}
>
<ToxicityLabel
score={metadata.perspective.score}
threshold={
settings.integrations.perspective.threshold ||
TOXICITY_THRESHOLD_DEFAULT / 100
}
/>
</FlagDetailsCategory>
)}
{offensive.length > 0 && (
<FlagDetailsCategory
category={
<Localized id="moderate-flagDetails-offensive">
<span>Offensive</span>
</Localized>
}
>
{offensive.map((flag, i) => (
<FlagDetailsEntry
key={i}
user={n.flagger ? n.flagger.username : <NotAvailable />}
details={n.additionalDetails}
user={flag.flagger ? flag.flagger.username : <NotAvailable />}
details={flag.additionalDetails}
/>
);
return;
case GQLCOMMENT_FLAG_REASON.COMMENT_REPORTED_SPAM:
spamList.push(
))}
</FlagDetailsCategory>
)}
{spam.length > 0 && (
<FlagDetailsCategory
category={
<Localized id="moderate-flagDetails-spam">
<span>Spam</span>
</Localized>
}
>
{spam.map((flag, i) => (
<FlagDetailsEntry
key={i}
user={n.flagger ? n.flagger.username : <NotAvailable />}
details={n.additionalDetails}
user={flag.flagger ? flag.flagger.username : <NotAvailable />}
details={flag.additionalDetails}
/>
);
return;
default:
return;
}
});
if (offensiveList.length + spamList.length === 0) {
return null;
}
return (
<HorizontalGutter size="oneAndAHalf">
{offensiveList.length > 0 && (
<FlagDetailsCategory
category={
<Localized id="moderate-flagDetails-offensive">
<span>Offensive</span>
</Localized>
}
>
{offensiveList}
</FlagDetailsCategory>
)}
{spamList.length > 0 && (
<FlagDetailsCategory
category={
<Localized id="moderate-flagDetails-spam">
<span>Spam</span>
</Localized>
}
>
{spamList}
</FlagDetailsCategory>
)}
</HorizontalGutter>
);
}
}
))}
</FlagDetailsCategory>
)}
</HorizontalGutter>
);
};
const enhanced = withFragmentContainer<Props>({
comment: graphql`
@@ -88,6 +102,22 @@ const enhanced = withFragmentContainer<Props>({
additionalDetails
}
}
revision {
metadata {
perspective {
score
}
}
}
}
`,
settings: graphql`
fragment FlagDetailsContainer_settings on Settings {
integrations {
perspective {
threshold
}
}
}
`,
})(FlagDetailsContainer);
@@ -14,3 +14,7 @@
.detailsText {
margin: 0;
}
.detailsDivider {
border: 1px solid var(--palette-grey-lightest);
}
@@ -18,9 +18,9 @@ const Markers: FunctionComponent<Props> = ({ children, details }) => {
showDetails,
]);
return (
<HorizontalGutter size="double">
<Flex>
<Flex itemGutter>{children}</Flex>
<HorizontalGutter>
<Flex itemGutter>
{children}
{details && (
<Button
size="small"
@@ -39,7 +39,12 @@ const Markers: FunctionComponent<Props> = ({ children, details }) => {
</Button>
)}
</Flex>
{showDetails && <div id={uuid}>{details}</div>}
{showDetails && (
<div id={uuid}>
<hr className={styles.detailsDivider} />
{details}
</div>
)}
</HorizontalGutter>
);
};
@@ -1,6 +1,7 @@
import React from "react";
import { createRenderer } from "react-test-renderer/shallow";
import { TOXICITY_THRESHOLD_DEFAULT } from "coral-common/constants";
import { removeFragmentRefs } from "coral-framework/testHelpers";
import { PropTypesOf } from "coral-framework/types";
@@ -12,19 +13,33 @@ it("renders all markers", () => {
const props: PropTypesOf<typeof MarkersContainerN> = {
comment: {
status: "PREMOD",
actionCounts: {
flag: {
reasons: {
COMMENT_DETECTED_TOXIC: 1,
COMMENT_DETECTED_SPAM: 1,
COMMENT_DETECTED_TRUST: 1,
COMMENT_DETECTED_LINKS: 1,
COMMENT_DETECTED_BANNED_WORD: 1,
COMMENT_DETECTED_SUSPECT_WORD: 1,
COMMENT_REPORTED_OFFENSIVE: 2,
COMMENT_REPORTED_SPAM: 3,
revision: {
actionCounts: {
flag: {
reasons: {
COMMENT_DETECTED_TOXIC: 1,
COMMENT_DETECTED_SPAM: 1,
COMMENT_DETECTED_TRUST: 1,
COMMENT_DETECTED_LINKS: 1,
COMMENT_DETECTED_BANNED_WORD: 1,
COMMENT_DETECTED_SUSPECT_WORD: 1,
COMMENT_REPORTED_OFFENSIVE: 2,
COMMENT_REPORTED_SPAM: 3,
},
},
},
metadata: {
perspective: {
score: 0,
},
},
},
},
settings: {
integrations: {
perspective: {
threshold: TOXICITY_THRESHOLD_DEFAULT / 100,
},
},
},
};
@@ -37,19 +52,33 @@ it("renders some markers", () => {
const props: PropTypesOf<typeof MarkersContainerN> = {
comment: {
status: "PREMOD",
actionCounts: {
flag: {
reasons: {
COMMENT_DETECTED_TOXIC: 1,
COMMENT_DETECTED_SPAM: 0,
COMMENT_DETECTED_TRUST: 1,
COMMENT_DETECTED_LINKS: 0,
COMMENT_DETECTED_BANNED_WORD: 1,
COMMENT_DETECTED_SUSPECT_WORD: 0,
COMMENT_REPORTED_OFFENSIVE: 2,
COMMENT_REPORTED_SPAM: 0,
revision: {
actionCounts: {
flag: {
reasons: {
COMMENT_DETECTED_TOXIC: 1,
COMMENT_DETECTED_SPAM: 0,
COMMENT_DETECTED_TRUST: 1,
COMMENT_DETECTED_LINKS: 0,
COMMENT_DETECTED_BANNED_WORD: 1,
COMMENT_DETECTED_SUSPECT_WORD: 0,
COMMENT_REPORTED_OFFENSIVE: 2,
COMMENT_REPORTED_SPAM: 0,
},
},
},
metadata: {
perspective: {
score: 1,
},
},
},
},
settings: {
integrations: {
perspective: {
threshold: TOXICITY_THRESHOLD_DEFAULT / 100,
},
},
},
};
@@ -2,26 +2,30 @@ import { Localized } from "fluent-react/compat";
import React from "react";
import { graphql } from "react-relay";
import { MarkersContainer_comment as CommentData } from "coral-admin/__generated__/MarkersContainer_comment.graphql";
import { MarkersContainer_comment } from "coral-admin/__generated__/MarkersContainer_comment.graphql";
import { MarkersContainer_settings } from "coral-admin/__generated__/MarkersContainer_settings.graphql";
import { withFragmentContainer } from "coral-framework/lib/relay";
import { Marker, MarkerCount } from "coral-ui/components";
import FlagDetailsContainer from "./FlagDetailsContainer";
import Markers from "./Markers";
interface MarkersContainerProps {
comment: CommentData;
comment: MarkersContainer_comment;
settings: MarkersContainer_settings;
}
function hasDetails(c: CommentData) {
function hasDetails(c: MarkersContainer_comment) {
return (
c.actionCounts.flag.reasons.COMMENT_REPORTED_OFFENSIVE +
c.actionCounts.flag.reasons.COMMENT_REPORTED_SPAM >
0
c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_OFFENSIVE +
c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_SPAM >
0 || c.revision.metadata.perspective
);
}
let keyCounter = 0;
const markers: Array<(c: CommentData) => React.ReactElement<any> | null> = [
const markers: Array<
(c: MarkersContainer_comment) => React.ReactElement<any> | null
> = [
c =>
(c.status === "PREMOD" && (
<Localized id="moderate-marker-preMod" key={keyCounter++}>
@@ -30,21 +34,21 @@ const markers: Array<(c: CommentData) => React.ReactElement<any> | null> = [
)) ||
null,
c =>
(c.actionCounts.flag.reasons.COMMENT_DETECTED_LINKS && (
(c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_LINKS && (
<Localized id="moderate-marker-link" key={keyCounter++}>
<Marker color="primary">Link</Marker>
</Localized>
)) ||
null,
c =>
(c.actionCounts.flag.reasons.COMMENT_DETECTED_BANNED_WORD && (
(c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_BANNED_WORD && (
<Localized id="moderate-marker-bannedWord" key={keyCounter++}>
<Marker color="error">Banned Word</Marker>
</Localized>
)) ||
null,
c =>
(c.actionCounts.flag.reasons.COMMENT_DETECTED_SUSPECT_WORD && (
(c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_SUSPECT_WORD && (
<Localized id="moderate-marker-suspectWord" key={keyCounter++}>
<Marker color="error" variant="filled">
Suspect Word
@@ -53,46 +57,46 @@ const markers: Array<(c: CommentData) => React.ReactElement<any> | null> = [
)) ||
null,
c =>
(c.actionCounts.flag.reasons.COMMENT_DETECTED_SPAM && (
(c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_SPAM && (
<Localized id="moderate-marker-spam" key={keyCounter++}>
<Marker color="error">Spam</Marker>
</Localized>
)) ||
null,
c =>
(c.actionCounts.flag.reasons.COMMENT_DETECTED_TOXIC && (
(c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_TOXIC && (
<Localized id="moderate-marker-toxic" key={keyCounter++}>
<Marker color="error">Toxic</Marker>
</Localized>
)) ||
null,
c =>
(c.actionCounts.flag.reasons.COMMENT_DETECTED_TRUST && (
(c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_TRUST && (
<Localized id="moderate-marker-karma" key={keyCounter++}>
<Marker color="error">Karma</Marker>
</Localized>
)) ||
null,
c =>
(c.actionCounts.flag.reasons.COMMENT_REPORTED_OFFENSIVE && (
(c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_OFFENSIVE && (
<Marker key={keyCounter++} color="error">
<Localized id="moderate-marker-offensive">
<span>Offensive</span>
</Localized>{" "}
<MarkerCount>
{c.actionCounts.flag.reasons.COMMENT_REPORTED_OFFENSIVE}
{c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_OFFENSIVE}
</MarkerCount>
</Marker>
)) ||
null,
c =>
(c.actionCounts.flag.reasons.COMMENT_REPORTED_SPAM && (
(c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_SPAM && (
<Marker key={keyCounter++} color="error">
<Localized id="moderate-marker-spam">
<span>Spam</span>
</Localized>{" "}
<MarkerCount>
{c.actionCounts.flag.reasons.COMMENT_REPORTED_SPAM}
{c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_SPAM}
</MarkerCount>
</Marker>
)) ||
@@ -102,20 +106,25 @@ const markers: Array<(c: CommentData) => React.ReactElement<any> | null> = [
export class MarkersContainer extends React.Component<MarkersContainerProps> {
public render() {
const elements = markers.map(cb => cb(this.props.comment)).filter(m => m);
if (elements.length) {
return (
<Markers
details={
hasDetails(this.props.comment) ? (
<FlagDetailsContainer comment={this.props.comment} />
) : null
}
>
{elements}
</Markers>
);
const doesHaveDetails = hasDetails(this.props.comment);
if (elements.length === 0 && !doesHaveDetails) {
return null;
}
return null;
return (
<Markers
details={
doesHaveDetails ? (
<FlagDetailsContainer
comment={this.props.comment}
settings={this.props.settings}
/>
) : null
}
>
{elements}
</Markers>
);
}
}
@@ -124,22 +133,34 @@ const enhanced = withFragmentContainer<MarkersContainerProps>({
fragment MarkersContainer_comment on Comment {
...FlagDetailsContainer_comment
status
actionCounts {
flag {
reasons {
COMMENT_DETECTED_TOXIC
COMMENT_DETECTED_SPAM
COMMENT_DETECTED_TRUST
COMMENT_DETECTED_LINKS
COMMENT_DETECTED_BANNED_WORD
COMMENT_DETECTED_SUSPECT_WORD
COMMENT_REPORTED_OFFENSIVE
COMMENT_REPORTED_SPAM
revision {
actionCounts {
flag {
reasons {
COMMENT_DETECTED_TOXIC
COMMENT_DETECTED_SPAM
COMMENT_DETECTED_TRUST
COMMENT_DETECTED_LINKS
COMMENT_DETECTED_BANNED_WORD
COMMENT_DETECTED_SUSPECT_WORD
COMMENT_REPORTED_OFFENSIVE
COMMENT_REPORTED_SPAM
}
}
}
metadata {
perspective {
score
}
}
}
}
`,
settings: graphql`
fragment MarkersContainer_settings on Settings {
...FlagDetailsContainer_settings
}
`,
})(MarkersContainer);
export default enhanced;
@@ -16,6 +16,7 @@ const baseProps: PropTypesOf<typeof ModerateCardN> = {
body: "content",
inReplyTo: null,
comment: {},
settings: {},
status: "undecided",
featured: false,
viewContextHref: "http://localhost/comment",
@@ -29,6 +29,7 @@ interface Props {
body: string;
inReplyTo: string | null;
comment: PropTypesOf<typeof MarkersContainer>["comment"];
settings: PropTypesOf<typeof MarkersContainer>["settings"];
status: "approved" | "rejected" | "undecided";
featured: boolean;
moderatedBy: React.ReactNode | null;
@@ -60,6 +61,7 @@ const ModerateCard: FunctionComponent<Props> = ({
body,
inReplyTo,
comment,
settings,
viewContextHref,
status,
featured,
@@ -146,7 +148,7 @@ const ModerateCard: FunctionComponent<Props> = ({
</Localized>
</div>
)}
<MarkersContainer comment={comment} />
<MarkersContainer comment={comment} settings={settings} />
</HorizontalGutter>
</div>
</div>
@@ -141,6 +141,7 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
body={comment.body!}
inReplyTo={comment.parent && comment.parent.author!.username!}
comment={comment}
settings={settings}
dangling={danglingLogic(comment.status)}
status={getStatus(comment)}
featured={isFeatured(comment)}
@@ -211,6 +212,7 @@ const enhanced = withFragmentContainer<Props>({
banned
suspect
}
...MarkersContainer_settings
}
`,
viewer: graphql`
@@ -0,0 +1,3 @@
.root {
font-size: calc(14rem / var(--rem-base));
}
@@ -0,0 +1,45 @@
import { Localized } from "fluent-react/compat";
import React, { FunctionComponent } from "react";
import { Counter, Typography } from "coral-ui/components";
import styles from "./ToxicityLabel.css";
const ToxicityCounter: FunctionComponent<{ score: number }> = ({ score }) => (
<Counter>{Math.round(score * 100)}%</Counter>
);
const ToxicityLabel: FunctionComponent<{
score: number;
threshold: number;
}> = ({ score, threshold }) => {
const counter = <ToxicityCounter score={score} />;
if (score > threshold) {
return (
<Localized id="moderate-toxicityLabel-likely" score={counter}>
<Typography
className={styles.root}
variant="bodyCopy"
color="errorDark"
>
Likely
</Typography>
</Localized>
);
} else if (score <= 0.5) {
return (
<Localized id="moderate-toxicityLabel-unlikely" score={counter}>
<Typography className={styles.root}>Unlikely</Typography>
</Localized>
);
}
return (
<Localized id="moderate-toxicityLabel-maybe" score={counter}>
<Typography className={styles.root}>Maybe</Typography>
</Localized>
);
};
export default ToxicityLabel;
@@ -6,23 +6,39 @@ exports[`renders all markers 1`] = `
<Relay(FlagDetailsContainer)
comment={
Object {
"actionCounts": Object {
"flag": Object {
"reasons": Object {
"COMMENT_DETECTED_BANNED_WORD": 1,
"COMMENT_DETECTED_LINKS": 1,
"COMMENT_DETECTED_SPAM": 1,
"COMMENT_DETECTED_SUSPECT_WORD": 1,
"COMMENT_DETECTED_TOXIC": 1,
"COMMENT_DETECTED_TRUST": 1,
"COMMENT_REPORTED_OFFENSIVE": 2,
"COMMENT_REPORTED_SPAM": 3,
"revision": Object {
"actionCounts": Object {
"flag": Object {
"reasons": Object {
"COMMENT_DETECTED_BANNED_WORD": 1,
"COMMENT_DETECTED_LINKS": 1,
"COMMENT_DETECTED_SPAM": 1,
"COMMENT_DETECTED_SUSPECT_WORD": 1,
"COMMENT_DETECTED_TOXIC": 1,
"COMMENT_DETECTED_TRUST": 1,
"COMMENT_REPORTED_OFFENSIVE": 2,
"COMMENT_REPORTED_SPAM": 3,
},
},
},
"metadata": Object {
"perspective": Object {
"score": 0,
},
},
},
"status": "PREMOD",
}
}
settings={
Object {
"integrations": Object {
"perspective": Object {
"threshold": 0.8,
},
},
}
}
/>
}
>
@@ -129,23 +145,39 @@ exports[`renders some markers 1`] = `
<Relay(FlagDetailsContainer)
comment={
Object {
"actionCounts": Object {
"flag": Object {
"reasons": Object {
"COMMENT_DETECTED_BANNED_WORD": 1,
"COMMENT_DETECTED_LINKS": 0,
"COMMENT_DETECTED_SPAM": 0,
"COMMENT_DETECTED_SUSPECT_WORD": 0,
"COMMENT_DETECTED_TOXIC": 1,
"COMMENT_DETECTED_TRUST": 1,
"COMMENT_REPORTED_OFFENSIVE": 2,
"COMMENT_REPORTED_SPAM": 0,
"revision": Object {
"actionCounts": Object {
"flag": Object {
"reasons": Object {
"COMMENT_DETECTED_BANNED_WORD": 1,
"COMMENT_DETECTED_LINKS": 0,
"COMMENT_DETECTED_SPAM": 0,
"COMMENT_DETECTED_SUSPECT_WORD": 0,
"COMMENT_DETECTED_TOXIC": 1,
"COMMENT_DETECTED_TRUST": 1,
"COMMENT_REPORTED_OFFENSIVE": 2,
"COMMENT_REPORTED_SPAM": 0,
},
},
},
"metadata": Object {
"perspective": Object {
"score": 1,
},
},
},
"status": "PREMOD",
}
}
settings={
Object {
"integrations": Object {
"perspective": Object {
"threshold": 0.8,
},
},
}
}
/>
}
>
@@ -66,6 +66,7 @@ exports[`renders approved correctly 1`] = `
</div>
<Relay(MarkersContainer)
comment={Object {}}
settings={Object {}}
/>
</ForwardRef(forwardRef)>
</div>
@@ -174,6 +175,7 @@ exports[`renders correctly 1`] = `
</div>
<Relay(MarkersContainer)
comment={Object {}}
settings={Object {}}
/>
</ForwardRef(forwardRef)>
</div>
@@ -281,6 +283,7 @@ exports[`renders dangling correctly 1`] = `
</div>
<Relay(MarkersContainer)
comment={Object {}}
settings={Object {}}
/>
</ForwardRef(forwardRef)>
</div>
@@ -390,6 +393,7 @@ exports[`renders rejected correctly 1`] = `
</div>
<Relay(MarkersContainer)
comment={Object {}}
settings={Object {}}
/>
</ForwardRef(forwardRef)>
</div>
@@ -503,6 +507,7 @@ exports[`renders reply correctly 1`] = `
</div>
<Relay(MarkersContainer)
comment={Object {}}
settings={Object {}}
/>
</ForwardRef(forwardRef)>
</div>
@@ -639,6 +644,7 @@ exports[`renders story info 1`] = `
</div>
<Relay(MarkersContainer)
comment={Object {}}
settings={Object {}}
/>
</ForwardRef(forwardRef)>
</div>
@@ -352,7 +352,7 @@ comment is toxic, according to Perspective API. By default the threshold is set
placeholder="80"
spellCheck={false}
type="text"
value=""
value="80"
/>
<div
className="TextField-adornment"
@@ -545,7 +545,7 @@ improve the API over time.
id="configure-moderation-perspective-customEndpoint"
name="integrations.perspective.endpoint"
onChange={[Function]}
placeholder=""
placeholder="https://commentanalyzer.googleapis.com/v1alpha1"
spellCheck={false}
type="text"
value=""
+65 -40
View File
@@ -1,3 +1,4 @@
import { TOXICITY_THRESHOLD_DEFAULT } from "coral-common/constants";
import { pureMerge } from "coral-common/utils";
import {
GQLComment,
@@ -66,6 +67,7 @@ export const settings = createFixture<GQLSettings>({
},
perspective: {
enabled: false,
threshold: TOXICITY_THRESHOLD_DEFAULT / 100,
},
},
auth: {
@@ -432,19 +434,22 @@ export const baseComment = createFixture<GQLComment>({
edges: [],
pageInfo: { endCursor: null, hasNextPage: false },
},
actionCounts: {
flag: {
reasons: {
COMMENT_DETECTED_TOXIC: 0,
COMMENT_DETECTED_SPAM: 0,
COMMENT_DETECTED_TRUST: 0,
COMMENT_DETECTED_LINKS: 0,
COMMENT_DETECTED_BANNED_WORD: 0,
COMMENT_DETECTED_SUSPECT_WORD: 0,
COMMENT_REPORTED_OFFENSIVE: 0,
COMMENT_REPORTED_SPAM: 0,
revision: {
actionCounts: {
flag: {
reasons: {
COMMENT_DETECTED_TOXIC: 0,
COMMENT_DETECTED_SPAM: 0,
COMMENT_DETECTED_TRUST: 0,
COMMENT_DETECTED_LINKS: 0,
COMMENT_DETECTED_BANNED_WORD: 0,
COMMENT_DETECTED_SUSPECT_WORD: 0,
COMMENT_REPORTED_OFFENSIVE: 0,
COMMENT_REPORTED_SPAM: 0,
},
},
},
metadata: {},
},
flags: {
nodes: [],
@@ -498,17 +503,22 @@ export const reportedComments = createFixtures<GQLComment>(
author: users.commenters[0],
revision: {
id: "comment-0-revision-0",
actionCounts: {
flag: {
reasons: {
COMMENT_REPORTED_SPAM: 2,
},
},
},
metadata: {
perspective: {
score: 0.1,
},
},
},
permalink: "http://localhost/comment/0",
body:
"This is the last random sentence I will be writing and I am going to stop mid-sent",
actionCounts: {
flag: {
reasons: {
COMMENT_REPORTED_SPAM: 2,
},
},
},
flags: {
nodes: [
{
@@ -528,17 +538,22 @@ export const reportedComments = createFixtures<GQLComment>(
id: "comment-1",
revision: {
id: "comment-1-revision-1",
actionCounts: {
flag: {
reasons: {
COMMENT_REPORTED_OFFENSIVE: 3,
},
},
},
metadata: {
perspective: {
score: 0.1,
},
},
},
permalink: "http://localhost/comment/1",
author: users.commenters[1],
body: "Don't fool with me",
actionCounts: {
flag: {
reasons: {
COMMENT_REPORTED_OFFENSIVE: 3,
},
},
},
flags: {
nodes: [
{
@@ -563,19 +578,24 @@ export const reportedComments = createFixtures<GQLComment>(
id: "comment-2",
revision: {
id: "comment-2-revision-2",
actionCounts: {
flag: {
reasons: {
COMMENT_REPORTED_SPAM: 1,
COMMENT_REPORTED_OFFENSIVE: 1,
},
},
},
metadata: {
perspective: {
score: 0.1,
},
},
},
permalink: "http://localhost/comment/2",
status: GQLCOMMENT_STATUS.PREMOD,
author: users.commenters[2],
body: "I think I deserve better",
actionCounts: {
flag: {
reasons: {
COMMENT_REPORTED_SPAM: 1,
COMMENT_REPORTED_OFFENSIVE: 1,
},
},
},
flags: {
nodes: [
{
@@ -595,18 +615,23 @@ export const reportedComments = createFixtures<GQLComment>(
id: "comment-3",
revision: {
id: "comment-3-revision-3",
actionCounts: {
flag: {
reasons: {
COMMENT_REPORTED_SPAM: 1,
},
},
},
metadata: {
perspective: {
score: 0.1,
},
},
},
permalink: "http://localhost/comment/3",
status: GQLCOMMENT_STATUS.PREMOD,
author: users.commenters[3],
body: "World peace at last",
actionCounts: {
flag: {
reasons: {
COMMENT_REPORTED_SPAM: 1,
},
},
},
flags: {
nodes: [
{
@@ -120,28 +120,24 @@ exports[`approves comment in reported queue: dangling 1`] = `
</a>
</div>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-double"
className="Box-root HorizontalGutter-root HorizontalGutter-full"
>
<div
className="Box-root Flex-root Flex-flex"
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
>
<div
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Spam
</span>
<span
className="Count-root"
>
2
</span>
<span>
Spam
</span>
</div>
<span
className="Count-root"
>
2
</span>
</span>
<button
aria-controls="uuid-0"
aria-expanded={false}
@@ -349,28 +345,24 @@ exports[`rejects comment in reported queue: dangling 1`] = `
</a>
</div>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-double"
className="Box-root HorizontalGutter-root HorizontalGutter-full"
>
<div
className="Box-root Flex-root Flex-flex"
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
>
<div
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Spam
</span>
<span
className="Count-root"
>
2
</span>
<span>
Spam
</span>
</div>
<span
className="Count-root"
>
2
</span>
</span>
<button
aria-controls="uuid-0"
aria-expanded={false}
@@ -575,28 +567,24 @@ exports[`renders reported queue with comments 1`] = `
</a>
</div>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-double"
className="Box-root HorizontalGutter-root HorizontalGutter-full"
>
<div
className="Box-root Flex-root Flex-flex"
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
>
<div
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Spam
</span>
<span
className="Count-root"
>
2
</span>
<span>
Spam
</span>
</div>
<span
className="Count-root"
>
2
</span>
</span>
<button
aria-controls="uuid-0"
aria-expanded={false}
@@ -788,28 +776,24 @@ exports[`renders reported queue with comments 1`] = `
</a>
</div>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-double"
className="Box-root HorizontalGutter-root HorizontalGutter-full"
>
<div
className="Box-root Flex-root Flex-flex"
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
>
<div
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Offensive
</span>
<span
className="Count-root"
>
3
</span>
<span>
Offensive
</span>
</div>
<span
className="Count-root"
>
3
</span>
</span>
<button
aria-controls="uuid-1"
aria-expanded={false}
@@ -1017,28 +1001,24 @@ exports[`renders reported queue with comments 2`] = `
</a>
</div>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-double"
className="Box-root HorizontalGutter-root HorizontalGutter-full"
>
<div
className="Box-root Flex-root Flex-flex"
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
>
<div
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Spam
</span>
<span
className="Count-root"
>
2
</span>
<span>
Spam
</span>
</div>
<span
className="Count-root"
>
2
</span>
</span>
<button
aria-controls="uuid-0"
aria-expanded={false}
@@ -1230,28 +1210,24 @@ exports[`renders reported queue with comments 2`] = `
</a>
</div>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-double"
className="Box-root HorizontalGutter-root HorizontalGutter-full"
>
<div
className="Box-root Flex-root Flex-flex"
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
>
<div
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Offensive
</span>
<span
className="Count-root"
>
3
</span>
<span>
Offensive
</span>
</div>
<span
className="Count-root"
>
3
</span>
</span>
<button
aria-controls="uuid-1"
aria-expanded={false}
@@ -1449,46 +1425,42 @@ exports[`renders reported queue with comments and load more 1`] = `
</a>
</div>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-double"
className="Box-root HorizontalGutter-root HorizontalGutter-full"
>
<div
className="Box-root Flex-root Flex-flex"
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
>
<div
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
<span
className="Marker-root Marker-colorPrimary Marker-variantRegular"
>
<span
className="Marker-root Marker-colorPrimary Marker-variantRegular"
>
Pre-Mod
Pre-Mod
</span>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Offensive
</span>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
className="Count-root"
>
<span>
Offensive
</span>
<span
className="Count-root"
>
1
</span>
1
</span>
</span>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Spam
</span>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
className="Count-root"
>
<span>
Spam
</span>
<span
className="Count-root"
>
1
</span>
1
</span>
</div>
</span>
<button
aria-controls="uuid-2"
aria-expanded={false}
@@ -120,28 +120,24 @@ exports[`approves comment in rejected queue: dangling 1`] = `
</a>
</div>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-double"
className="Box-root HorizontalGutter-root HorizontalGutter-full"
>
<div
className="Box-root Flex-root Flex-flex"
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
>
<div
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Spam
</span>
<span
className="Count-root"
>
2
</span>
<span>
Spam
</span>
</div>
<span
className="Count-root"
>
2
</span>
</span>
<button
aria-controls="uuid-0"
aria-expanded={false}
@@ -346,28 +342,24 @@ exports[`renders rejected queue with comments 1`] = `
</a>
</div>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-double"
className="Box-root HorizontalGutter-root HorizontalGutter-full"
>
<div
className="Box-root Flex-root Flex-flex"
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
>
<div
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Spam
</span>
<span
className="Count-root"
>
2
</span>
<span>
Spam
</span>
</div>
<span
className="Count-root"
>
2
</span>
</span>
<button
aria-controls="uuid-0"
aria-expanded={false}
@@ -559,28 +551,24 @@ exports[`renders rejected queue with comments 1`] = `
</a>
</div>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-double"
className="Box-root HorizontalGutter-root HorizontalGutter-full"
>
<div
className="Box-root Flex-root Flex-flex"
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
>
<div
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Offensive
</span>
<span
className="Count-root"
>
3
</span>
<span>
Offensive
</span>
</div>
<span
className="Count-root"
>
3
</span>
</span>
<button
aria-controls="uuid-1"
aria-expanded={false}
@@ -778,41 +766,37 @@ exports[`renders rejected queue with comments and load more 1`] = `
</a>
</div>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-double"
className="Box-root HorizontalGutter-root HorizontalGutter-full"
>
<div
className="Box-root Flex-root Flex-flex"
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
>
<div
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Offensive
</span>
<span
className="Count-root"
>
1
</span>
<span>
Offensive
</span>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
className="Count-root"
>
<span>
Spam
</span>
<span
className="Count-root"
>
1
</span>
1
</span>
</div>
</span>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Spam
</span>
<span
className="Count-root"
>
1
</span>
</span>
<button
aria-controls="uuid-2"
aria-expanded={false}
@@ -86,28 +86,24 @@ exports[`approves single comment 1`] = `
</a>
</div>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-double"
className="Box-root HorizontalGutter-root HorizontalGutter-full"
>
<div
className="Box-root Flex-root Flex-flex"
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
>
<div
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Spam
</span>
<span
className="Count-root"
>
2
</span>
<span>
Spam
</span>
</div>
<span
className="Count-root"
>
2
</span>
</span>
<button
aria-controls="uuid-0"
aria-expanded={false}
@@ -281,28 +277,24 @@ exports[`rejects single comment 1`] = `
</a>
</div>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-double"
className="Box-root HorizontalGutter-root HorizontalGutter-full"
>
<div
className="Box-root Flex-root Flex-flex"
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
>
<div
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Spam
</span>
<span
className="Count-root"
>
2
</span>
<span>
Spam
</span>
</div>
<span
className="Count-root"
>
2
</span>
</span>
<button
aria-controls="uuid-0"
aria-expanded={false}
@@ -511,28 +503,24 @@ exports[`renders single comment view 1`] = `
</a>
</div>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-double"
className="Box-root HorizontalGutter-root HorizontalGutter-full"
>
<div
className="Box-root Flex-root Flex-flex"
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
>
<div
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Spam
</span>
<span
className="Count-root"
>
2
</span>
<span>
Spam
</span>
</div>
<span
className="Count-root"
>
2
</span>
</span>
<button
aria-controls="uuid-0"
aria-expanded={false}
@@ -35,6 +35,10 @@
background-color: var(--palette-grey-dark);
}
.colorError {
background-color: var(--palette-error-darkest);
}
.colorInherit {
background-color: currentColor;
}
@@ -14,7 +14,7 @@ interface Props {
/**
* The color of the component. It supports those theme colors that make sense for this component.
*/
color?: "inherit" | "dark" | "grey" | "primary";
color?: "inherit" | "dark" | "grey" | "primary" | "error";
size?: "sm" | "md";
}
@@ -34,6 +34,7 @@ const Counter: FunctionComponent<Props> = ({
[classes.colorDark]: color === "dark",
[classes.colorGrey]: color === "grey",
[classes.colorInherit]: color === "inherit",
[classes.colorError]: color === "error",
[classes.sizeSmall]: size === "sm",
},
className
@@ -5,6 +5,7 @@ exports[`renders correctly 1`] = `
classes={
Object {
"colorDark": "Counter-colorDark",
"colorError": "Counter-colorError",
"colorGrey": "Counter-colorGrey",
"colorInherit": "Counter-colorInherit",
"colorPrimary": "Counter-colorPrimary",
@@ -2,7 +2,7 @@
composes: markerText from "coral-ui/shared/typography.css";
border-left: 1px solid currentColor;
margin-left: calc(0.5 * var(--mini-unit));
margin-right: calc(-0.5 * var(--mini-unit));
margin-right: calc(-0.25 * var(--mini-unit));
padding-top: 2px;
padding-bottom: 2px;
padding-left: calc(0.5 * var(--mini-unit));
@@ -2,7 +2,7 @@
composes: markerText from "coral-ui/shared/typography.css";
color: var(--palette-error-main);
border: 1px solid currentColor;
border-radius: 4px;
border-radius: 20px;
padding: 1px var(--mini-unit);
white-space: nowrap;
}
@@ -122,6 +122,10 @@
color: var(--palette-error-main);
}
.colorErrorDark {
color: var(--palette-error-dark);
}
.colorWarning {
color: var(--palette-warning-main);
}
@@ -59,6 +59,7 @@ interface Props extends PropTypesOf<typeof Box> {
| "textLight"
| "warning"
| "error"
| "errorDark"
| "success";
/**
* The container used for the root node.
@@ -118,6 +119,7 @@ const Typography: FunctionComponent<Props> = props => {
[classes.colorTextLight]: color === "textLight",
[classes.colorPrimary]: color === "primary",
[classes.colorError]: color === "error",
[classes.colorErrorDark]: color === "errorDark",
[classes.colorSuccess]: color === "success",
[classes.colorWarning]: color === "warning",
[classes.noWrap]: noWrap,
+7
View File
@@ -27,3 +27,10 @@ export const TOXICITY_THRESHOLD_DEFAULT = 80;
* TOXICITY_MODEL_DEFAULT is the default value used for the toxicity model.
*/
export const TOXICITY_MODEL_DEFAULT = "TOXICITY";
/**
* TOXICITY_ENDPOINT_DEFAULT is the default value used for the toxicity endpoint
* for the API.
*/
export const TOXICITY_ENDPOINT_DEFAULT =
"https://commentanalyzer.googleapis.com/v1alpha1";
@@ -14,5 +14,7 @@ export const CommentRevision: Required<
comment: w => w.comment,
actionCounts: w => decodeActionCounts(w.revision.actionCounts),
body: w => w.revision.body,
// Defaults to an empty object if not set on the revision.
metadata: w => w.revision.metadata || {},
createdAt: w => w.revision.createdAt,
};
@@ -750,12 +750,12 @@ type PerspectiveExternalIntegration {
"""
The endpoint that Coral should use to communicate with the perspective API.
"""
endpoint: String
endpoint: String @auth(roles: [ADMIN])
"""
The key for the Perspective API integration.
"""
key: String
key: String @auth(roles: [ADMIN])
"""
The threshold that given a specific toxic comment score, the comment will
@@ -766,12 +766,12 @@ type PerspectiveExternalIntegration {
"""
model is the Perspective model to use.
"""
model: String
model: String @auth(roles: [ADMIN])
"""
When True, comments sent will not be stored by the Google Perspective API.
"""
doNotStore: Boolean
doNotStore: Boolean @auth(roles: [ADMIN])
}
type ExternalIntegrations {
@@ -1158,7 +1158,7 @@ type Settings {
"""
integrations contains all the external integrations that can be enabled.
"""
integrations: ExternalIntegrations! @auth(roles: [ADMIN])
integrations: ExternalIntegrations! @auth(roles: [ADMIN, MODERATOR])
"""
karma is the set of settings related to how user Trust and Karma are
@@ -1705,6 +1705,23 @@ type CommentModerationActionConnection {
pageInfo: PageInfo!
}
type CommentRevisionPerspectiveMetadata {
"""
score is the value detected from the perspective API. This is returned as the
percentage chance it would be considered toxic and can be compared to the
defined threshold value.
"""
score: Float!
}
type CommentRevisionMetadata {
"""
perspective stores metadata associated with the pipeline analysis of this
revision's body.
"""
perspective: CommentRevisionPerspectiveMetadata
}
type CommentRevision {
"""
id is the identifier of the CommentRevision.
@@ -1729,6 +1746,11 @@ type CommentRevision {
"""
body: String
"""
metadata stores details on a CommentRevision.
"""
metadata: CommentRevisionMetadata! @auth(roles: [ADMIN, MODERATOR])
"""
createdAt is the time that the CommentRevision was created.
"""
+21 -18
View File
@@ -42,6 +42,15 @@ function collection<T = Comment>(mongo: Db) {
return mongo.collection<Readonly<T>>("comments");
}
export interface RevisionMetadata {
akismet?: boolean;
linkCount?: number;
perspective?: {
score: number;
model: string;
};
}
/**
* Revision stores a Comment's body for a specific edit. Actions can be tied to
* a Revision, as can moderation actions.
@@ -62,6 +71,11 @@ export interface Revision {
*/
actionCounts: EncodedCommentActionCounts;
/**
* metadata stores properties on this revision.
*/
metadata: RevisionMetadata;
/**
* createdAt is the date that this revision was created at.
*/
@@ -140,11 +154,6 @@ export interface Comment extends TenantResource {
*/
childCount: number;
/**
* metadata stores the deep Comment properties.
*/
metadata?: Record<string, any>;
/**
* createdAt is the date that this Comment was created.
*/
@@ -240,6 +249,7 @@ export type CreateCommentInput = Omit<
| "deletedAt"
> &
Required<Pick<Revision, "body">> &
Pick<Revision, "metadata"> &
Partial<Pick<Comment, "actionCounts">>;
export async function createComment(
@@ -249,13 +259,14 @@ export async function createComment(
now = new Date()
) {
// Pull out some useful properties from the input.
const { body, actionCounts = {}, ...rest } = input;
const { body, actionCounts = {}, metadata, ...rest } = input;
// Generate the revision.
const revision: Revision = {
id: uuid.v4(),
body,
actionCounts,
metadata,
createdAt: now,
};
@@ -311,17 +322,14 @@ export async function pushChildCommentIDOntoParent(
return result.value;
}
export type EditCommentInput = Pick<
Comment,
"id" | "authorID" | "status" | "metadata"
> & {
export type EditCommentInput = Pick<Comment, "id" | "authorID" | "status"> & {
/**
* lastEditableCommentCreatedAt is the date that the last comment would have
* been editable. It is generally derived from the tenant's
* `editCommentWindowLength` property.
*/
lastEditableCommentCreatedAt: Date;
} & Required<Pick<Revision, "body">> &
} & Required<Pick<Revision, "body" | "metadata">> &
Partial<Pick<Comment, "actionCounts">>;
// Only comments with the following status's can be edited.
@@ -401,17 +409,12 @@ export async function editComment(
id: uuid.v4(),
body,
actionCounts,
metadata,
createdAt: now,
};
const update: Record<string, any> = {
$set: {
status,
// Embed all the metadata properties, this may override the existing
// metadata, but we won't replace metadata that has been recalculated.
// TODO: (wyattjoh) consider if we want to replace the metadata for edited comments instead of supplementing it
...dotize({ metadata }),
},
$set: { status },
$push: {
revisions: revision,
},
+1 -1
View File
@@ -248,7 +248,7 @@ export async function create(
export type EditComment = Omit<
EditCommentInput,
"status" | "authorID" | "lastEditableCommentCreatedAt"
"status" | "authorID" | "lastEditableCommentCreatedAt" | "metadata"
>;
export async function edit(
@@ -40,15 +40,15 @@ describe("compose", () => {
it("merges the metadata", async () => {
const status = GQLCOMMENT_STATUS.APPROVED;
const enhanced = compose([
() => ({ metadata: { first: true } }),
() => ({ status, metadata: { second: true } }),
() => ({ metadata: { third: true } }),
() => ({ metadata: { akismet: true } }),
() => ({ metadata: { linkCount: 1 } }),
() => ({ status, metadata: { akismet: false } }),
]);
await expect(enhanced(context)).resolves.toEqual({
body: context.comment.body,
status,
metadata: { first: true, second: true },
metadata: { akismet: false, linkCount: 1 },
actions: [],
tags: [],
});
@@ -104,14 +104,14 @@ describe("compose", () => {
it("handles when it does not return a status", async () => {
const enhanced = compose([
() => ({ metadata: { first: true } }),
() => ({ metadata: { second: true } }),
() => ({ metadata: { akismet: true } }),
() => ({ metadata: { akismet: false } }),
]);
await expect(enhanced(context)).resolves.toEqual({
body: context.comment.body,
status: GQLCOMMENT_STATUS.NONE,
metadata: { first: true, second: true },
metadata: { akismet: false },
actions: [],
tags: [],
});
@@ -1,7 +1,10 @@
import { Omit, Promiseable, RequireProperty } from "coral-common/types";
import { GQLCOMMENT_STATUS } from "coral-server/graph/tenant/schema/__generated__/types";
import { CreateActionInput } from "coral-server/models/action/comment";
import { EditCommentInput } from "coral-server/models/comment";
import {
EditCommentInput,
RevisionMetadata,
} from "coral-server/models/comment";
import { CommentTag } from "coral-server/models/comment/tag";
import { Story } from "coral-server/models/story";
import { Tenant } from "coral-server/models/tenant";
@@ -18,7 +21,7 @@ export type ModerationAction = Omit<
export interface PhaseResult {
actions: ModerationAction[];
status: GQLCOMMENT_STATUS;
metadata: Record<string, any>;
metadata: RevisionMetadata;
body: string;
tags: CommentTag[];
}
@@ -33,14 +36,19 @@ export interface ModerationPhaseContext {
req?: Request;
}
export type ModerationPhase = (
export type RootModerationPhase = (
context: ModerationPhaseContext
) => Promiseable<PhaseResult>;
export type IntermediatePhaseResult = Partial<PhaseResult> | void;
export interface IntermediateModerationPhaseContext
extends ModerationPhaseContext {
metadata: RevisionMetadata;
}
export type IntermediateModerationPhase = (
context: ModerationPhaseContext
context: IntermediateModerationPhaseContext
) => Promiseable<IntermediatePhaseResult>;
/**
@@ -49,7 +57,7 @@ export type IntermediateModerationPhase = (
*/
export const compose = (
phases: IntermediateModerationPhase[]
): ModerationPhase => async context => {
): RootModerationPhase => async context => {
const final: PhaseResult = {
status: GQLCOMMENT_STATUS.NONE,
body: context.comment.body,
@@ -65,8 +73,8 @@ export const compose = (
comment: {
...context.comment,
body: final.body,
metadata: final.metadata,
},
metadata: final.metadata,
});
if (result) {
// If this result contained actions, then we should push it into the
@@ -119,4 +127,6 @@ export const compose = (
/**
* process the comment and return moderation details.
*/
export const processForModeration: ModerationPhase = compose(moderationPhases);
export const processForModeration: RootModerationPhase = compose(
moderationPhases
);
@@ -27,7 +27,6 @@ const testCharCount = (settings: Partial<Settings>, length: number) => {
};
export const commentLength: IntermediateModerationPhase = ({
story,
tenant,
comment,
}): IntermediatePhaseResult | void => {
@@ -4,7 +4,7 @@ import {
GQLCOMMENT_STATUS,
} from "coral-server/graph/tenant/schema/__generated__/types";
import { ACTION_TYPE } from "coral-server/models/action/comment";
import { Comment } from "coral-server/models/comment";
import { RevisionMetadata } from "coral-server/models/comment";
import { GlobalModerationSettings } from "coral-server/models/settings";
import {
IntermediateModerationPhase,
@@ -13,21 +13,19 @@ import {
const testPremodLinksEnable = (
settings: DeepPartial<GlobalModerationSettings>,
comment: Pick<Comment, "metadata">
) =>
settings.premodLinksEnable && comment.metadata && comment.metadata.linkCount;
metadata: RevisionMetadata
) => settings.premodLinksEnable && metadata && metadata.linkCount;
// This phase checks the comment if it has any links in it if the check is
// enabled.
export const detectLinks: IntermediateModerationPhase = ({
story,
tenant,
comment,
metadata,
}): IntermediatePhaseResult | void => {
if (
comment &&
(testPremodLinksEnable(tenant, comment) ||
(story.settings && testPremodLinksEnable(story.settings, comment)))
testPremodLinksEnable(tenant, metadata) ||
(story.settings && testPremodLinksEnable(story.settings, metadata))
) {
// Add the flag related to Trust to the comment.
return {
@@ -21,7 +21,6 @@ export const preModerate: IntermediateModerationPhase = ({
// If the settings say that we're in premod mode, then the comment is in
// premod status.
// TODO: (wyattjoh) pull from the story settings.
if (
testModerationMode(tenant) ||
(story.settings && testModerationMode(story.settings))
@@ -115,7 +115,7 @@ export const spam: IntermediateModerationPhase = async ({
],
metadata: {
// Store the spam result from Akismet in the Comment metadata.
akismet: spam,
akismet: isSpam,
},
};
}
@@ -9,8 +9,6 @@ describe("storyClosed", () => {
storyClosed({
story: { closedAt: new Date() } as ModerationPhaseContext["story"],
tenant: {} as ModerationPhaseContext["tenant"],
comment: {} as ModerationPhaseContext["comment"],
author: {} as ModerationPhaseContext["author"],
now: new Date(),
})
).toThrow();
@@ -21,8 +19,6 @@ describe("storyClosed", () => {
closeCommenting: { auto: true },
} as ModerationPhaseContext["tenant"],
now: new Date(),
comment: {} as ModerationPhaseContext["comment"],
author: {} as ModerationPhaseContext["author"],
});
expect(() =>
@@ -35,8 +31,6 @@ describe("storyClosed", () => {
},
} as ModerationPhaseContext["tenant"],
now: new Date(),
comment: {} as ModerationPhaseContext["comment"],
author: {} as ModerationPhaseContext["author"],
})
).toThrow();
});
@@ -54,8 +48,6 @@ describe("storyClosed", () => {
tenant: {
closeCommenting: { auto: true },
} as ModerationPhaseContext["tenant"],
comment: {} as ModerationPhaseContext["comment"],
author: {} as ModerationPhaseContext["author"],
now,
})
).toBeUndefined();
@@ -66,8 +58,6 @@ describe("storyClosed", () => {
tenant: {
closeCommenting: { auto: true },
} as ModerationPhaseContext["tenant"],
comment: {} as ModerationPhaseContext["comment"],
author: {} as ModerationPhaseContext["author"],
now,
})
).toBeUndefined();
@@ -1,16 +1,19 @@
import { StoryClosedError } from "coral-server/errors";
import {
IntermediateModerationPhase,
IntermediatePhaseResult,
ModerationPhaseContext,
} from "coral-server/services/comments/pipeline";
import { getStoryClosedAt } from "coral-server/services/stories";
// This phase checks to see if the story being processed is closed or not.
export const storyClosed: IntermediateModerationPhase = ({
export const storyClosed = ({
story,
tenant,
now,
}): IntermediatePhaseResult | void => {
}: Pick<
ModerationPhaseContext,
"story" | "tenant" | "now"
>): IntermediatePhaseResult | void => {
const closedAt = getStoryClosedAt(tenant, story);
if (closedAt && closedAt <= now) {
throw new StoryClosedError();
@@ -3,6 +3,7 @@ import ms from "ms";
import fetch from "node-fetch";
import {
TOXICITY_ENDPOINT_DEFAULT,
TOXICITY_MODEL_DEFAULT,
TOXICITY_THRESHOLD_DEFAULT,
} from "coral-common/constants";
@@ -49,8 +50,7 @@ export const toxic: IntermediateModerationPhase = async ({
let endpoint = integration.endpoint;
if (isNil(endpoint)) {
// TODO: (wyattjoh) replace hardcoded default with config.
endpoint = "https://commentanalyzer.googleapis.com/v1alpha1";
endpoint = TOXICITY_ENDPOINT_DEFAULT;
log.trace(
{ endpoint },
@@ -79,7 +79,7 @@ export const toxic: IntermediateModerationPhase = async ({
}
// TODO: (wyattjoh) replace hardcoded default with config.
const timeout = ms("500ms");
const timeout = ms("800ms");
try {
logger.trace("checking comment toxicity");
@@ -125,6 +125,13 @@ export const toxic: IntermediateModerationPhase = async ({
}
log.trace({ score, isToxic, threshold }, "comment was not toxic");
return {
metadata: {
// Store the scores from perspective in the Comment metadata.
perspective: { model, score },
},
};
} catch (err) {
// Rethrow any ToxicCommentError.
if (err instanceof ToxicCommentError) {
+5
View File
@@ -338,6 +338,11 @@ moderate-markers-details = Details
moderate-flagDetails-offensive = Offensive
moderate-flagDetails-spam = Spam
moderate-flagDetails-toxicityScore = Toxicity Score
moderate-toxicityLabel-likely = Likely <score></score>
moderate-toxicityLabel-unlikely = Unlikely <score></score>
moderate-toxicityLabel-maybe = Maybe <score></score>
moderate-emptyQueue-pending = Nicely done! There are no more pending comments to moderate.
moderate-emptyQueue-reported = Nicely done! There are no more reported comments to moderate.
moderate-emptyQueue-unmoderated = Nicely done! All comments have been moderated.