mirror of
https://github.com/wassname/talk.git
synced 2026-08-17 11:27:52 +08:00
[CORL-825-cp] Allow sending of moderation decisions to Google (#2783)
* Allow sending of moderation decisions to Google If enabled in the config, moderation decisions can be sent to Google. Simplify OnOffField test id's Consolidate guard clauses Reconstruct tenant url's when sending perspective feedback Use the comment revision id when sending perspective feedback Use retrieveStory instead of findStory Clarify logging warning levels on perspective feedback Clean html tags from sent comment body Fix include to match new graph context refactors CORL-825 * Handle asynchronous perspective operation more cleanly CORL-825
This commit is contained in:
@@ -15,6 +15,10 @@ interface Props {
|
||||
offLabel?: React.ReactNode;
|
||||
format?: (value: any, name: string) => any;
|
||||
parse?: (value: any, name: string) => any;
|
||||
testIDs?: {
|
||||
on: string;
|
||||
off: string;
|
||||
};
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -26,6 +30,7 @@ const OnOffField: FunctionComponent<Props> = ({
|
||||
invert = false,
|
||||
parse = parseStringBool,
|
||||
format = formatBool,
|
||||
testIDs,
|
||||
className,
|
||||
}) => (
|
||||
<div className={className}>
|
||||
@@ -37,7 +42,12 @@ const OnOffField: FunctionComponent<Props> = ({
|
||||
format={format}
|
||||
>
|
||||
{({ input }) => (
|
||||
<RadioButton {...input} id={`${input.name}-true`} disabled={disabled}>
|
||||
<RadioButton
|
||||
{...input}
|
||||
id={`${input.name}-true`}
|
||||
disabled={disabled}
|
||||
data-testid={testIDs ? testIDs.on : undefined}
|
||||
>
|
||||
{onLabel || (
|
||||
<Localized id="configure-onOffField-on">
|
||||
<span>On</span>
|
||||
@@ -52,6 +62,7 @@ const OnOffField: FunctionComponent<Props> = ({
|
||||
parse={parse}
|
||||
format={format}
|
||||
value={JSON.stringify(invert)}
|
||||
data-testid={testIDs ? testIDs.off : undefined}
|
||||
>
|
||||
{({ input }) => (
|
||||
<RadioButton {...input} id={`${input.name}-false`} disabled={disabled}>
|
||||
|
||||
@@ -52,6 +52,7 @@ graphql`
|
||||
model
|
||||
threshold
|
||||
doNotStore
|
||||
sendFeedback
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -200,6 +201,10 @@ const PerspectiveConfig: FunctionComponent<Props> = ({ disabled }) => {
|
||||
<OnOffField
|
||||
name="integrations.perspective.doNotStore"
|
||||
disabled={disabled}
|
||||
testIDs={{
|
||||
on: "test-allowStoreCommentData",
|
||||
off: "test-dontAllowStoreCommentData",
|
||||
}}
|
||||
onLabel={
|
||||
<Localized id="configure-radioButton-allow">
|
||||
<span>Allow</span>
|
||||
@@ -213,6 +218,39 @@ const PerspectiveConfig: FunctionComponent<Props> = ({ disabled }) => {
|
||||
invert
|
||||
/>
|
||||
</FormField>
|
||||
<FormField container={<FieldSet />}>
|
||||
<FormFieldHeader>
|
||||
<Localized id="configure-moderation-perspective-allowSendFeedback">
|
||||
<Label component="legend">
|
||||
Allow Coral to send moderation actions to Google
|
||||
</Label>
|
||||
</Localized>
|
||||
<Localized id="configure-moderation-perspective-allowSendFeedbackDescription">
|
||||
<HelperText>
|
||||
Sent moderation actions will be used for future research and
|
||||
community model building purposes to improve the API over time.
|
||||
</HelperText>
|
||||
</Localized>
|
||||
</FormFieldHeader>
|
||||
<OnOffField
|
||||
name="integrations.perspective.sendFeedback"
|
||||
disabled={disabled}
|
||||
testIDs={{
|
||||
on: "test-allowSendFeedback",
|
||||
off: "test-dontAllowSendFeedback",
|
||||
}}
|
||||
onLabel={
|
||||
<Localized id="configure-radioButton-allow">
|
||||
<span>Allow</span>
|
||||
</Localized>
|
||||
}
|
||||
offLabel={
|
||||
<Localized id="configure-radioButton-dontAllow">
|
||||
<span>Don't Allow</span>
|
||||
</Localized>
|
||||
}
|
||||
/>
|
||||
</FormField>
|
||||
<HorizontalGutter spacing={3}>
|
||||
<Localized id="configure-configurationSubHeader" strong={<strong />}>
|
||||
<Subheader>Configuration</Subheader>
|
||||
|
||||
@@ -819,6 +819,7 @@ improve the API over time.
|
||||
<input
|
||||
checked={true}
|
||||
className="RadioButton-input"
|
||||
data-testid="test-allowStoreCommentData"
|
||||
disabled={false}
|
||||
id="integrations.perspective.doNotStore-true"
|
||||
name="integrations.perspective.doNotStore"
|
||||
@@ -863,6 +864,76 @@ improve the API over time.
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset
|
||||
className="FieldSet-root Box-root HorizontalGutter-root FormField-root HorizontalGutter-spacing-2"
|
||||
>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root FormFieldHeader-root HorizontalGutter-spacing-1"
|
||||
>
|
||||
<legend
|
||||
className="Label-root"
|
||||
>
|
||||
Allow Coral to send moderation actions to Google
|
||||
</legend>
|
||||
<p
|
||||
className="HelperText-root"
|
||||
>
|
||||
Sent moderation actions will be used for future research and
|
||||
community model building purposes to improve the API over time.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className="Box-root Flex-root RadioButton-root Flex-flex Flex-alignCenter"
|
||||
>
|
||||
<input
|
||||
checked={false}
|
||||
className="RadioButton-input"
|
||||
data-testid="test-allowSendFeedback"
|
||||
disabled={false}
|
||||
id="integrations.perspective.sendFeedback-true"
|
||||
name="integrations.perspective.sendFeedback"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
type="radio"
|
||||
value="true"
|
||||
/>
|
||||
<label
|
||||
className="RadioButton-label"
|
||||
htmlFor="integrations.perspective.sendFeedback-true"
|
||||
>
|
||||
<span>
|
||||
Allow
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root Flex-root RadioButton-root Flex-flex Flex-alignCenter"
|
||||
>
|
||||
<input
|
||||
checked={true}
|
||||
className="RadioButton-input"
|
||||
disabled={false}
|
||||
id="integrations.perspective.sendFeedback-false"
|
||||
name="integrations.perspective.sendFeedback"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
type="radio"
|
||||
value="false"
|
||||
/>
|
||||
<label
|
||||
className="RadioButton-label"
|
||||
htmlFor="integrations.perspective.sendFeedback-false"
|
||||
>
|
||||
<span>
|
||||
Don't allow
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-3"
|
||||
>
|
||||
|
||||
@@ -21,14 +21,15 @@ beforeEach(() => {
|
||||
const viewer = users.admins[0];
|
||||
|
||||
async function createTestRenderer(
|
||||
params: CreateTestRendererParams<GQLResolver> = {}
|
||||
params: CreateTestRendererParams<GQLResolver> = {},
|
||||
settingsOverride?: any
|
||||
) {
|
||||
const { testRenderer } = create({
|
||||
...params,
|
||||
resolvers: pureMerge(
|
||||
createResolversStub<GQLResolver>({
|
||||
Query: {
|
||||
settings: () => settings,
|
||||
settings: () => (settingsOverride ? settingsOverride : settings),
|
||||
viewer: () => viewer,
|
||||
},
|
||||
}),
|
||||
@@ -239,6 +240,7 @@ it("change perspective settings", async () => {
|
||||
key: "my api key",
|
||||
model: null,
|
||||
threshold: 0.1,
|
||||
sendFeedback: false,
|
||||
}
|
||||
);
|
||||
break;
|
||||
@@ -262,7 +264,9 @@ it("change perspective settings", async () => {
|
||||
);
|
||||
|
||||
const onField = within(perspectiveContainer).getByLabelText("On");
|
||||
const allowField = within(perspectiveContainer).getByLabelText("Allow");
|
||||
const allowField = within(perspectiveContainer).getByTestID(
|
||||
"test-allowStoreCommentData"
|
||||
);
|
||||
const keyField = within(perspectiveContainer).getByLabelText("API key");
|
||||
const thresholdField = within(perspectiveContainer).getByLabelText(
|
||||
"Toxicity threshold"
|
||||
@@ -361,3 +365,73 @@ it("change perspective settings", async () => {
|
||||
// Should have successfully sent with server.
|
||||
expect(resolvers.Mutation!.updateSettings!.calledTwice).toBe(true);
|
||||
});
|
||||
|
||||
it("change perspective send feedback setting", async () => {
|
||||
const settingsOverride = settings;
|
||||
settingsOverride.integrations.perspective = {
|
||||
doNotStore: false,
|
||||
enabled: true,
|
||||
endpoint: "https://custom-endpoint.net",
|
||||
key: "api key",
|
||||
model: "TOXIC_MODEL",
|
||||
threshold: 0.1,
|
||||
sendFeedback: false,
|
||||
};
|
||||
|
||||
const resolvers = createResolversStub<GQLResolver>({
|
||||
Mutation: {
|
||||
updateSettings: ({ variables }) => {
|
||||
expectAndFail(variables.settings.integrations!.perspective).toEqual({
|
||||
doNotStore: false,
|
||||
enabled: true,
|
||||
endpoint: "https://custom-endpoint.net",
|
||||
key: "api key",
|
||||
model: "TOXIC_MODEL",
|
||||
threshold: 0.1,
|
||||
sendFeedback: true,
|
||||
});
|
||||
|
||||
return {
|
||||
settings: pureMerge(settings, variables.settings),
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
const { moderationContainer, saveChangesButton } = await createTestRenderer(
|
||||
{
|
||||
resolvers,
|
||||
},
|
||||
settingsOverride
|
||||
);
|
||||
|
||||
const perspectiveContainer = within(moderationContainer).getByTestID(
|
||||
"perspective-container"
|
||||
);
|
||||
const onField = within(perspectiveContainer).getByLabelText("On");
|
||||
const allowField = within(perspectiveContainer).getByTestID(
|
||||
"test-allowSendFeedback"
|
||||
);
|
||||
const form = findParentWithType(perspectiveContainer, "form")!;
|
||||
|
||||
// Let's turn it on.
|
||||
act(() => allowField.props.onChange(allowField.props.value.toString()));
|
||||
expect(saveChangesButton.props.disabled).toBe(false);
|
||||
|
||||
// Send form
|
||||
act(() => {
|
||||
form.props.onSubmit();
|
||||
});
|
||||
|
||||
// Wait for submission to be finished
|
||||
await act(async () => {
|
||||
await wait(() => {
|
||||
expect(onField.props.disabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// Submit button and text field should be disabled.
|
||||
expect(saveChangesButton.props.disabled).toBe(true);
|
||||
|
||||
// Should have successfully sent with server.
|
||||
expect(resolvers.Mutation!.updateSettings!.calledOnce).toBe(true);
|
||||
});
|
||||
|
||||
@@ -90,6 +90,7 @@ export const settings = createFixture<GQLSettings>({
|
||||
perspective: {
|
||||
enabled: false,
|
||||
threshold: TOXICITY_THRESHOLD_DEFAULT / 100,
|
||||
sendFeedback: false,
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
|
||||
@@ -11,6 +11,7 @@ export const Actions = (ctx: GraphContext) => ({
|
||||
approveComment(
|
||||
ctx.mongo,
|
||||
ctx.redis,
|
||||
ctx.config,
|
||||
ctx.publisher,
|
||||
ctx.tenant,
|
||||
input.commentID,
|
||||
@@ -22,6 +23,7 @@ export const Actions = (ctx: GraphContext) => ({
|
||||
rejectComment(
|
||||
ctx.mongo,
|
||||
ctx.redis,
|
||||
ctx.config,
|
||||
ctx.publisher,
|
||||
ctx.tenant,
|
||||
input.commentID,
|
||||
|
||||
@@ -173,6 +173,7 @@ export const Comments = (ctx: GraphContext) => ({
|
||||
? approveComment(
|
||||
ctx.mongo,
|
||||
ctx.redis,
|
||||
ctx.config,
|
||||
ctx.publisher,
|
||||
ctx.tenant,
|
||||
commentID,
|
||||
|
||||
@@ -827,6 +827,12 @@ type PerspectiveExternalIntegration {
|
||||
When True, comments sent will not be stored by the Google Perspective API.
|
||||
"""
|
||||
doNotStore: Boolean @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
When True, comment moderation decisions will be sent to the Google
|
||||
Perspective API to help improve the comment analysis algorithms.
|
||||
"""
|
||||
sendFeedback: Boolean @auth(roles: [ADMIN])
|
||||
}
|
||||
|
||||
type ExternalIntegrations {
|
||||
@@ -3275,6 +3281,11 @@ input SettingsPerspectiveExternalIntegrationInput {
|
||||
When True, comments sent will not be stored by the Google Perspective API.
|
||||
"""
|
||||
doNotStore: Boolean
|
||||
|
||||
"""
|
||||
When True, moderation actions will be sent to the Google Perspective API.
|
||||
"""
|
||||
sendFeedback: Boolean
|
||||
}
|
||||
|
||||
input SettingsExternalIntegrationsInput {
|
||||
|
||||
@@ -182,6 +182,7 @@ export async function createTenant(
|
||||
perspective: {
|
||||
enabled: false,
|
||||
doNotStore: true,
|
||||
sendFeedback: false,
|
||||
},
|
||||
},
|
||||
reaction: getDefaultReactionConfiguration(bundle),
|
||||
|
||||
@@ -210,7 +210,12 @@ async function getScore(
|
||||
endpoint,
|
||||
model,
|
||||
doNotStore,
|
||||
}: Required<Omit<GQLPerspectiveExternalIntegration, "enabled" | "threshold">>,
|
||||
}: Required<
|
||||
Omit<
|
||||
GQLPerspectiveExternalIntegration,
|
||||
"enabled" | "threshold" | "sendFeedback"
|
||||
>
|
||||
>,
|
||||
language: PerspectiveLanguage,
|
||||
timeout: number
|
||||
): Promise<number> {
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
import { Db } from "mongodb";
|
||||
import striptags from "striptags";
|
||||
|
||||
import { TOXICITY_ENDPOINT_DEFAULT } from "coral-common/constants";
|
||||
import { reconstructTenantURL } from "coral-server/app/url";
|
||||
import { Config } from "coral-server/config";
|
||||
import logger from "coral-server/logger";
|
||||
import { Comment } from "coral-server/models/comment";
|
||||
import { getURLWithCommentID, retrieveStory } from "coral-server/models/story";
|
||||
import { Tenant } from "coral-server/models/tenant";
|
||||
|
||||
import {
|
||||
GQLCOMMENT_STATUS,
|
||||
GQLPerspectiveExternalIntegration,
|
||||
} from "coral-server/graph/schema/__generated__/types";
|
||||
|
||||
interface SendResult {
|
||||
ok: boolean;
|
||||
status: number;
|
||||
data: any;
|
||||
}
|
||||
|
||||
async function send(
|
||||
endpoint: string,
|
||||
apiKey: string,
|
||||
method: string,
|
||||
body: any
|
||||
): Promise<SendResult> {
|
||||
const result = await fetch(`${endpoint}/${method}?key=${apiKey}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body, null, 2),
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
return {
|
||||
ok: result.ok,
|
||||
status: result.status,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
const data = await result.json();
|
||||
|
||||
return {
|
||||
ok: result.ok,
|
||||
status: result.status,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
function computeStatus(status: GQLCOMMENT_STATUS) {
|
||||
if (status === GQLCOMMENT_STATUS.APPROVED) {
|
||||
return "APPROVED";
|
||||
}
|
||||
if (status === GQLCOMMENT_STATUS.REJECTED) {
|
||||
return "DELETED";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function notifyPerspectiveModerationDecision(
|
||||
mongo: Db,
|
||||
tenant: Tenant,
|
||||
config: Config,
|
||||
perspectiveConfig: GQLPerspectiveExternalIntegration,
|
||||
comment: Comment,
|
||||
commentRevisionID: string,
|
||||
status: GQLCOMMENT_STATUS
|
||||
) {
|
||||
if (
|
||||
!perspectiveConfig.enabled ||
|
||||
!perspectiveConfig.key ||
|
||||
!perspectiveConfig.sendFeedback
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const commentStatus = computeStatus(status);
|
||||
if (!commentStatus) {
|
||||
return;
|
||||
}
|
||||
|
||||
const revision = comment.revisions.find(c => c.id === commentRevisionID);
|
||||
if (!revision) {
|
||||
logger.warn(
|
||||
{ commentID: comment.id, commentRevisionID },
|
||||
"unable to find comment revision ID in comment revision history"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const endpoint = perspectiveConfig.endpoint
|
||||
? perspectiveConfig.endpoint
|
||||
: TOXICITY_ENDPOINT_DEFAULT;
|
||||
const apiKey = perspectiveConfig.key;
|
||||
|
||||
const tenantUrl = reconstructTenantURL(config, tenant, undefined, "/");
|
||||
const communityId = `Coral:${tenantUrl}`;
|
||||
const clientToken = `comment:${comment.id}`;
|
||||
|
||||
try {
|
||||
const story = await retrieveStory(mongo, comment.tenantID, comment.storyID);
|
||||
if (!story) {
|
||||
logger.warn({ storyID: comment.storyID }, "could not find story");
|
||||
return;
|
||||
}
|
||||
|
||||
const url = getURLWithCommentID(story.url, comment.id);
|
||||
|
||||
const body = {
|
||||
comment: {
|
||||
text: striptags(revision.body),
|
||||
},
|
||||
context: {
|
||||
entries: [
|
||||
{
|
||||
text: JSON.stringify({
|
||||
url,
|
||||
reply_to_id_Coral_comment_id: comment.parentID,
|
||||
Coral_comment_id: comment.id,
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
attributeScores: {
|
||||
[commentStatus]: {
|
||||
summaryScore: {
|
||||
value: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
languages: ["EN"],
|
||||
communityId,
|
||||
clientToken,
|
||||
};
|
||||
|
||||
const result = await send(endpoint, apiKey, "comments:suggestscore", body);
|
||||
|
||||
if (result.ok) {
|
||||
logger.debug(
|
||||
{ commentID: comment.id, commentRevisionID },
|
||||
"successfully sent moderation feedback to perspective"
|
||||
);
|
||||
} else if (!result.ok) {
|
||||
logger.error(
|
||||
{ status: result.status },
|
||||
"unable to send moderation feedback to perspective"
|
||||
);
|
||||
} else if (!result.data || result.data.clientToken !== clientToken) {
|
||||
logger.error(
|
||||
{ data: result.data },
|
||||
"result data from perspective did not contain the clientToken we expected"
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error({ err }, "unable to send moderation feedback to perspective");
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Db } from "mongodb";
|
||||
|
||||
import { Config } from "coral-server/config";
|
||||
import { Publisher } from "coral-server/graph/subscriptions/publisher";
|
||||
import { Tenant } from "coral-server/models/tenant";
|
||||
import { moderate } from "coral-server/services/comments/moderation";
|
||||
import { notifyPerspectiveModerationDecision } from "coral-server/services/perspective";
|
||||
import { AugmentedRedis } from "coral-server/services/redis";
|
||||
|
||||
import { GQLCOMMENT_STATUS } from "coral-server/graph/schema/__generated__/types";
|
||||
@@ -12,6 +14,7 @@ import { publishChanges, updateAllCounts } from "./helpers";
|
||||
const approveComment = async (
|
||||
mongo: Db,
|
||||
redis: AugmentedRedis,
|
||||
config: Config,
|
||||
publisher: Publisher,
|
||||
tenant: Tenant,
|
||||
commentID: string,
|
||||
@@ -45,6 +48,18 @@ const approveComment = async (
|
||||
moderatorID,
|
||||
});
|
||||
|
||||
// We don't want to await on this so that
|
||||
// we don't hold up the moderation flow and response
|
||||
notifyPerspectiveModerationDecision(
|
||||
mongo,
|
||||
tenant,
|
||||
config,
|
||||
tenant.integrations.perspective,
|
||||
result.after,
|
||||
commentRevisionID,
|
||||
GQLCOMMENT_STATUS.APPROVED
|
||||
);
|
||||
|
||||
// Return the resulting comment.
|
||||
return result.after;
|
||||
};
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Db } from "mongodb";
|
||||
|
||||
import { Config } from "coral-server/config";
|
||||
import { Publisher } from "coral-server/graph/subscriptions/publisher";
|
||||
import { hasTag } from "coral-server/models/comment";
|
||||
import { Tenant } from "coral-server/models/tenant";
|
||||
import { removeTag } from "coral-server/services/comments";
|
||||
import { moderate } from "coral-server/services/comments/moderation";
|
||||
import { notifyPerspectiveModerationDecision } from "coral-server/services/perspective";
|
||||
import { AugmentedRedis } from "coral-server/services/redis";
|
||||
|
||||
import {
|
||||
@@ -17,6 +19,7 @@ import { publishChanges, updateAllCounts } from "./helpers";
|
||||
const rejectComment = async (
|
||||
mongo: Db,
|
||||
redis: AugmentedRedis,
|
||||
config: Config,
|
||||
publisher: Publisher,
|
||||
tenant: Tenant,
|
||||
commentID: string,
|
||||
@@ -55,6 +58,18 @@ const rejectComment = async (
|
||||
return removeTag(mongo, tenant, result.after.id, GQLTAG.FEATURED);
|
||||
}
|
||||
|
||||
// We don't want to await on this so that
|
||||
// we don't hold up the moderation flow and response
|
||||
notifyPerspectiveModerationDecision(
|
||||
mongo,
|
||||
tenant,
|
||||
config,
|
||||
tenant.integrations.perspective,
|
||||
result.after,
|
||||
commentRevisionID,
|
||||
GQLCOMMENT_STATUS.REJECTED
|
||||
);
|
||||
|
||||
// Return the resulting comment.
|
||||
return result.after;
|
||||
};
|
||||
|
||||
@@ -316,6 +316,11 @@ configure-moderation-perspective-allowStoreCommentData = Allow Google to store c
|
||||
configure-moderation-perspective-allowStoreCommentDataDescription =
|
||||
Stored comments will be used for future research and community model building purposes to
|
||||
improve the API over time.
|
||||
configure-moderation-perspective-allowSendFeedback =
|
||||
Allow Coral to send moderation actions to Google
|
||||
configure-moderation-perspective-allowSendFeedbackDescription =
|
||||
Sent moderation actions will be used for future research and
|
||||
community model building purposes to improve the API over time.
|
||||
configure-moderation-perspective-customEndpoint = Custom endpoint
|
||||
configure-moderation-perspective-defaultEndpoint =
|
||||
By default the endpoint is set to { $default }. You may override this here.
|
||||
|
||||
Reference in New Issue
Block a user