mirror of
https://github.com/wassname/talk.git
synced 2026-07-17 11:33:39 +08:00
[CORL-645] Add Slack support for v5 (#2713)
* Create preliminary schema for setting Slack channels CORL-645 * Implement preliminary slack notification using tenant slack channels CORL-645 * Very preliminarily get data loading with FieldArray's for slack channels CORL-645 * Update settings input on schema to allow saving Slack settings to tenant CORL-645 * Filter off UNMODERATED queue events from slack channels We don't send these to slack through our filters, we only care about pending, reported, and featured CORL-645 * Include a moderation link in comments pushed to Slack CORL-645 * Hook up proper callback functions for adding/removing slack channels CORL-645 * Add missing translation for Slack navigation item CORL-645 * Update snapshots for preliminary Slack configuration changes CORL-645 * Add some FormField wrappers around slack config elements Makes the UI appear a little nicer CORL-645 * Set up slack config to only provide one slack channel We need to do this until we can get ArrayField's working in final-form. CORL-645 * Disable the other trigger checkboxes when "All Comments" is checked CORL-645 * Clean up the formatting of Slack messages CORL-645 * Add error handling around sending comments to Slack CORL-645 * Add links to external Slack setup documentation CORL-645 * Replace form state with wrapped field element CORL-645 * Clean up fetch request sending Slack notifications CORL-645 * Prefer global string replacement over RegEx CORL-645 * Use URL class to construct comment URL's CORL-645 * Require slack configuration in schema CORL-645 * Initialize Slack in fixtures Also fix up a flaky test that wasn't waiting on form submission and on-change events. CORL-645 * Preliminarily fix up styles to match other config pages CORL-645 * Create placeholder add/remove buttons * Convert SlackConfigContainer to FunctionalComponent CORL-645 * Add name field to slack channels CORL-645 * Disable inner fields on Slack channel when not enabled CORL-645 * Improve the delete channel button CORL-645 * Use pureMerge to extract slack channel settings CORL-645 * Do a bit of cleanup on the add channel button CORL-645
This commit is contained in:
Generated
+33
-1
@@ -14563,6 +14563,12 @@
|
||||
"@babel/runtime": "^7.3.1"
|
||||
}
|
||||
},
|
||||
"final-form-arrays": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/final-form-arrays/-/final-form-arrays-1.1.2.tgz",
|
||||
"integrity": "sha512-Pmq3MXI9zbSsY7WZ2eodWQAHpsw2/i5YkagzcCgqzRcSSHB8BD3yWi7YPSGTPXuG0ixcQe9VAYpQ+UEWcqf4zg==",
|
||||
"dev": true
|
||||
},
|
||||
"finalhandler": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
|
||||
@@ -24795,7 +24801,7 @@
|
||||
"dependencies": {
|
||||
"async": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
|
||||
"resolved": "http://registry.npmjs.org/async/-/async-1.5.2.tgz",
|
||||
"integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
|
||||
"dev": true
|
||||
}
|
||||
@@ -29421,6 +29427,32 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-final-form-arrays": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-final-form-arrays/-/react-final-form-arrays-3.1.0.tgz",
|
||||
"integrity": "sha512-eJdAlhTKzlDD/d1wedD592a99eJNGO0e9GzY++RLN99P23cMGKSzCmsiGWLPwpY0H/C/LmNSL4XzWyH/aZembA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.4.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": {
|
||||
"version": "7.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.7.4.tgz",
|
||||
"integrity": "sha512-r24eVUUr0QqNZa+qrImUk8fn5SPhHq+IfYvIoIMg0do3GdK9sMdiLKP3GYVVaxpPKORgm8KRKaNTEhAjgIpLMw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.13.2"
|
||||
}
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"version": "0.13.3",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz",
|
||||
"integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-helmet": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-5.2.1.tgz",
|
||||
|
||||
@@ -277,6 +277,7 @@
|
||||
"eventemitter2": "^5.0.1",
|
||||
"farce": "^0.2.6",
|
||||
"final-form": "4.18.6",
|
||||
"final-form-arrays": "1.1.2",
|
||||
"flat": "^4.1.0",
|
||||
"fluent-intl-polyfill": "^0.1.0",
|
||||
"fluent-langneg": "^0.1.1",
|
||||
@@ -328,6 +329,7 @@
|
||||
"react-dom": "^16.9.0-alpha.0",
|
||||
"react-error-overlay": "^5.1.6",
|
||||
"react-final-form": "6.3.0",
|
||||
"react-final-form-arrays": "3.1.0",
|
||||
"react-popper": "^1.3.2",
|
||||
"react-relay": "^4.0.0",
|
||||
"react-relay-network-modern": "^4.0.4",
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
GeneralConfigRoute,
|
||||
ModerationConfigRoute,
|
||||
OrganizationConfigRoute,
|
||||
SlackConfigRoute,
|
||||
WordListConfigRoute,
|
||||
} from "./routes/Configure/sections";
|
||||
import ForgotPasswordRoute from "./routes/ForgotPassword";
|
||||
@@ -75,6 +76,7 @@ export default makeRouteConfig(
|
||||
<Route path="auth" {...AuthConfigRoute.routeConfig} />
|
||||
<Route path="advanced" {...AdvancedConfigRoute.routeConfig} />
|
||||
<Route path="email" {...EmailConfigRoute.routeConfig} />
|
||||
<Route path="slack" {...SlackConfigRoute.routeConfig} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FormApi, FormState } from "final-form";
|
||||
import arrayMutators from "final-form-arrays";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { Form, FormSpy } from "react-final-form";
|
||||
@@ -24,7 +25,7 @@ const Configure: FunctionComponent<Props> = ({
|
||||
children,
|
||||
}) => (
|
||||
<MainLayout data-testid="configure-container">
|
||||
<Form onSubmit={onSubmit}>
|
||||
<Form onSubmit={onSubmit} mutators={{ ...arrayMutators }}>
|
||||
{({ handleSubmit, submitting, form, pristine, submitError }) => (
|
||||
<form autoComplete="off" onSubmit={handleSubmit} id="configure-form">
|
||||
<FormSpy onChange={onChange} />
|
||||
@@ -52,6 +53,9 @@ const Configure: FunctionComponent<Props> = ({
|
||||
<Localized id="configure-sideBarNavigation-email">
|
||||
<Link to="/admin/configure/email">Email</Link>
|
||||
</Localized>
|
||||
<Localized id="configure-sideBarNavigation-slack">
|
||||
<Link to="/admin/configure/slack">Slack</Link>
|
||||
</Localized>
|
||||
<Localized id="configure-sideBarNavigation-advanced">
|
||||
<Link to="/admin/configure/advanced">Advanced</Link>
|
||||
</Localized>
|
||||
|
||||
@@ -30,6 +30,7 @@ const UpdateSettingsMutation = createMutation(
|
||||
...OrganizationConfigContainer_settings
|
||||
...WordListConfigContainer_settings
|
||||
...AdvancedConfigContainer_settings
|
||||
...SlackConfigContainer_settings
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
|
||||
@@ -5,6 +5,19 @@ exports[`renders correctly 1`] = `
|
||||
data-testid="configure-container"
|
||||
>
|
||||
<ReactFinalForm
|
||||
mutators={
|
||||
Object {
|
||||
"insert": [Function],
|
||||
"move": [Function],
|
||||
"pop": [Function],
|
||||
"push": [Function],
|
||||
"remove": [Function],
|
||||
"shift": [Function],
|
||||
"swap": [Function],
|
||||
"unshift": [Function],
|
||||
"update": [Function],
|
||||
}
|
||||
}
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
[Function]
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
.header {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.description {
|
||||
padding-bottom: var(--spacing-1);
|
||||
}
|
||||
|
||||
.channelName {
|
||||
margin-right: var(--spacing-2);
|
||||
}
|
||||
|
||||
.trigger {
|
||||
padding-left: var(--spacing-1);
|
||||
}
|
||||
|
||||
.removeButton {
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
|
||||
margin-bottom: 2px;
|
||||
|
||||
float: right;
|
||||
}
|
||||
|
||||
.buttonIcon {
|
||||
padding-right: var(--spacing-1);
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
import { Field } from "react-final-form";
|
||||
|
||||
import { parseBool } from "coral-framework/lib/form";
|
||||
import { ExternalLink } from "coral-framework/lib/i18n/components";
|
||||
import { InputDescription, InputLabel } from "coral-ui/components";
|
||||
import {
|
||||
Button,
|
||||
ButtonIcon,
|
||||
CheckBox,
|
||||
Flex,
|
||||
FormField,
|
||||
TextField,
|
||||
} from "coral-ui/components/v2";
|
||||
|
||||
import Header from "../../Header";
|
||||
import ConfigBoxWithToggleField from "../Auth/ConfigBoxWithToggleField";
|
||||
|
||||
import styles from "./SlackChannel.css";
|
||||
|
||||
interface Props {
|
||||
channel: any;
|
||||
disabled: boolean;
|
||||
index: number;
|
||||
onRemoveClicked: (index: number) => void;
|
||||
}
|
||||
|
||||
const SlackChannel: FunctionComponent<Props> = ({
|
||||
channel,
|
||||
disabled,
|
||||
index,
|
||||
onRemoveClicked,
|
||||
}) => {
|
||||
const onRemove = useCallback(() => {
|
||||
onRemoveClicked(index);
|
||||
}, [index, onRemoveClicked]);
|
||||
|
||||
return (
|
||||
<ConfigBoxWithToggleField
|
||||
title={
|
||||
<Flex
|
||||
className={styles.header}
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<div>
|
||||
<Field name={`${channel}.name`}>
|
||||
{({ input }) => (
|
||||
<Header className={styles.channelName}>{input.value}</Header>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
size="small"
|
||||
variant="filled"
|
||||
color="alert"
|
||||
onClick={onRemove}
|
||||
className={styles.removeButton}
|
||||
>
|
||||
<ButtonIcon size="md" className={styles.buttonIcon}>
|
||||
delete_forever
|
||||
</ButtonIcon>
|
||||
<Localized id="configure-slack-channel-remove">Remove</Localized>
|
||||
</Button>
|
||||
</div>
|
||||
</Flex>
|
||||
}
|
||||
name={`${channel}.enabled`}
|
||||
disabled={disabled}
|
||||
>
|
||||
{(disabledInside: boolean) => (
|
||||
<>
|
||||
<FormField>
|
||||
<Field name={`${channel}.name`}>
|
||||
{({ input, meta }) => (
|
||||
<>
|
||||
<Localized id="configure-slack-channel-name-label">
|
||||
<InputLabel container="legend">Name</InputLabel>
|
||||
</Localized>
|
||||
<Localized id="configure-slack-channel-name-description">
|
||||
<InputDescription className={styles.description}>
|
||||
This is only for your information, to easily identify each
|
||||
Slack connection. Slack does not tell us the name of the
|
||||
channel/s you're connecting to Coral.
|
||||
</InputDescription>
|
||||
</Localized>
|
||||
<TextField
|
||||
id={`configure-slack-channel-name-${input.name}`}
|
||||
disabled={disabled || disabledInside}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck={false}
|
||||
fullWidth
|
||||
{...input}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
</FormField>
|
||||
<FormField>
|
||||
<Field name={`${channel}.hookURL`}>
|
||||
{({ input, meta }) => (
|
||||
<>
|
||||
<Localized id="configure-slack-channel-hookURL-label">
|
||||
<InputLabel container="legend">Webhook URL</InputLabel>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="configure-slack-channel-hookURL-description"
|
||||
externalLink={
|
||||
<ExternalLink href="https://docs.coralproject.net/coral/v5/integrating/slack/#i-need-to-find-the-webhook-url-again-where-is-it" />
|
||||
}
|
||||
>
|
||||
<InputDescription className={styles.description}>
|
||||
Slack provides a channel-specific URL to activate webhook
|
||||
connections. To find the URL for one of your Slack
|
||||
channels, follow the instructions here.
|
||||
</InputDescription>
|
||||
</Localized>
|
||||
<TextField
|
||||
id={`configure-slack-channel-hookURL-${input.name}`}
|
||||
disabled={disabled || disabledInside}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck={false}
|
||||
fullWidth
|
||||
{...input}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
</FormField>
|
||||
<FormField>
|
||||
<Localized id="configure-slack-channel-triggers-label">
|
||||
<InputLabel container="legend">
|
||||
Receive notifications in this Slack channel for
|
||||
</InputLabel>
|
||||
</Localized>
|
||||
<Field
|
||||
name={`${channel}.triggers.allComments`}
|
||||
subscription={{ value: true }}
|
||||
>
|
||||
{({ input: { value } }) => (
|
||||
<>
|
||||
<Field
|
||||
name={`${channel}.triggers.allComments`}
|
||||
type="checkbox"
|
||||
parse={parseBool}
|
||||
>
|
||||
{({ input }) => (
|
||||
<CheckBox
|
||||
id={`configure-slack-channel-triggers-allComments-${input.name}`}
|
||||
disabled={disabled || disabledInside}
|
||||
className={styles.trigger}
|
||||
{...input}
|
||||
>
|
||||
<Localized id="configure-slack-channel-triggers-allComments">
|
||||
All Comments
|
||||
</Localized>
|
||||
</CheckBox>
|
||||
)}
|
||||
</Field>
|
||||
<Field
|
||||
name={`${channel}.triggers.reportedComments`}
|
||||
type="checkbox"
|
||||
parse={parseBool}
|
||||
>
|
||||
{({ input }) => (
|
||||
<CheckBox
|
||||
id={`configure-slack-channel-triggers-reportedComments-${input.name}`}
|
||||
disabled={disabled || value || disabledInside}
|
||||
className={styles.trigger}
|
||||
{...input}
|
||||
>
|
||||
<Localized id="configure-slack-channel-triggers-reportedComments">
|
||||
Reported Comments
|
||||
</Localized>
|
||||
</CheckBox>
|
||||
)}
|
||||
</Field>
|
||||
<Field
|
||||
name={`${channel}.triggers.pendingComments`}
|
||||
type="checkbox"
|
||||
parse={parseBool}
|
||||
>
|
||||
{({ input }) => (
|
||||
<CheckBox
|
||||
id={`configure-slack-channel-triggers-pendingComments-${input.name}`}
|
||||
disabled={disabled || value || disabledInside}
|
||||
className={styles.trigger}
|
||||
{...input}
|
||||
>
|
||||
<Localized id="configure-slack-channel-triggers-pendingComments">
|
||||
Pending Comments
|
||||
</Localized>
|
||||
</CheckBox>
|
||||
)}
|
||||
</Field>
|
||||
<Field
|
||||
name={`${channel}.triggers.featuredComments`}
|
||||
type="checkbox"
|
||||
parse={parseBool}
|
||||
>
|
||||
{({ input }) => (
|
||||
<CheckBox
|
||||
id={`configure-slack-channel-triggers-featuredComments-${input.name}`}
|
||||
disabled={disabled || value || disabledInside}
|
||||
className={styles.trigger}
|
||||
{...input}
|
||||
>
|
||||
<Localized id="configure-slack-channel-triggers-featuredComments">
|
||||
Featured Comments
|
||||
</Localized>
|
||||
</CheckBox>
|
||||
)}
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
</FormField>
|
||||
</>
|
||||
)}
|
||||
</ConfigBoxWithToggleField>
|
||||
);
|
||||
};
|
||||
|
||||
export default SlackChannel;
|
||||
@@ -0,0 +1,3 @@
|
||||
.icon {
|
||||
padding-right: var(--spacing-2);
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import { FormApi } from "final-form";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { FieldArray } from "react-final-form-arrays";
|
||||
|
||||
import { pureMerge } from "coral-common/utils";
|
||||
import { ExternalLink } from "coral-framework/lib/i18n/components";
|
||||
import { graphql, withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import {
|
||||
Button,
|
||||
ButtonIcon,
|
||||
FormFieldDescription,
|
||||
HorizontalGutter,
|
||||
} from "coral-ui/components/v2";
|
||||
|
||||
import { SlackConfigContainer_settings } from "coral-admin/__generated__/SlackConfigContainer_settings.graphql";
|
||||
|
||||
import ConfigBox from "../../ConfigBox";
|
||||
import Header from "../../Header";
|
||||
import SlackChannel from "./SlackChannel";
|
||||
|
||||
import styles from "./SlackConfigContainer.css";
|
||||
|
||||
interface Props {
|
||||
form: FormApi;
|
||||
submitting: boolean;
|
||||
settings: SlackConfigContainer_settings;
|
||||
}
|
||||
|
||||
const SlackConfigContainer: FunctionComponent<Props> = ({ form, settings }) => {
|
||||
const [defaultValues] = useState({
|
||||
slack: {
|
||||
channels: [
|
||||
{
|
||||
enabled: false,
|
||||
name: "",
|
||||
hookURL: "",
|
||||
triggers: {
|
||||
allComments: false,
|
||||
reportedComments: false,
|
||||
pendingComments: false,
|
||||
featuredComments: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const onAddChannel = useCallback(() => {
|
||||
const mutators = form.mutators;
|
||||
mutators.insert("slack.channels", 0, {
|
||||
enabled: true,
|
||||
hookURL: "",
|
||||
triggers: {
|
||||
allComments: false,
|
||||
reportedComments: false,
|
||||
pendingComments: false,
|
||||
featuredComments: false,
|
||||
},
|
||||
});
|
||||
}, [form]);
|
||||
const onRemoveChannel = useCallback(
|
||||
(index: number) => {
|
||||
const mutators = form.mutators;
|
||||
mutators.remove("slack.channels", index);
|
||||
},
|
||||
[form]
|
||||
);
|
||||
|
||||
useMemo(() => {
|
||||
if (
|
||||
!settings ||
|
||||
!settings.slack ||
|
||||
!settings.slack.channels ||
|
||||
settings.slack.channels.length === 0
|
||||
) {
|
||||
form.initialize(defaultValues);
|
||||
} else {
|
||||
const settingsValues = pureMerge(defaultValues, settings);
|
||||
form.initialize(settingsValues);
|
||||
}
|
||||
}, [settings, defaultValues]);
|
||||
|
||||
return (
|
||||
<HorizontalGutter size="double">
|
||||
<ConfigBox
|
||||
title={
|
||||
<Localized id="configure-slack-header-title">
|
||||
<Header htmlFor="configure-slack-header.title">
|
||||
Slack Integrations
|
||||
</Header>
|
||||
</Localized>
|
||||
}
|
||||
>
|
||||
<Localized
|
||||
id="configure-slack-description"
|
||||
externalLink={
|
||||
<ExternalLink href="https://docs.coralproject.net/coral/v5/integrating/slack/" />
|
||||
}
|
||||
>
|
||||
<FormFieldDescription>
|
||||
Automatically send comments from Coral moderation queues to Slack
|
||||
channels. You will need Slack admin access to set this up. For steps
|
||||
on how to create a Slack App see our documentation.
|
||||
</FormFieldDescription>
|
||||
</Localized>
|
||||
<Button
|
||||
size="medium"
|
||||
variant="filled"
|
||||
color="emphasis"
|
||||
onClick={onAddChannel}
|
||||
>
|
||||
<ButtonIcon size="md" className={styles.icon}>
|
||||
add
|
||||
</ButtonIcon>
|
||||
<Localized id="configure-slack-addChannel">Add</Localized>
|
||||
</Button>
|
||||
<FieldArray name="slack.channels">
|
||||
{({ fields }) =>
|
||||
fields.map((channel: any, index: number) => (
|
||||
<div key={index}>
|
||||
<SlackChannel
|
||||
channel={channel}
|
||||
disabled={false}
|
||||
index={index}
|
||||
onRemoveClicked={onRemoveChannel}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</FieldArray>
|
||||
</ConfigBox>
|
||||
</HorizontalGutter>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
settings: graphql`
|
||||
fragment SlackConfigContainer_settings on Settings {
|
||||
slack {
|
||||
channels {
|
||||
enabled
|
||||
name
|
||||
hookURL
|
||||
triggers {
|
||||
allComments
|
||||
reportedComments
|
||||
pendingComments
|
||||
featuredComments
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(SlackConfigContainer);
|
||||
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,50 @@
|
||||
import { FormApi } from "final-form";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { SlackConfigRouteQueryResponse } from "coral-admin/__generated__/SlackConfigRouteQuery.graphql";
|
||||
import { withRouteConfig } from "coral-framework/lib/router";
|
||||
import { Delay, Spinner } from "coral-ui/components";
|
||||
|
||||
import SlackConfigContainer from "./SlackConfigContainer";
|
||||
|
||||
interface Props {
|
||||
data: SlackConfigRouteQueryResponse | null;
|
||||
form: FormApi;
|
||||
submitting: boolean;
|
||||
}
|
||||
|
||||
const SlackConfigRoute: FunctionComponent<Props> = ({
|
||||
data,
|
||||
form,
|
||||
submitting,
|
||||
}) => {
|
||||
if (!data) {
|
||||
return (
|
||||
<Delay>
|
||||
<Spinner />
|
||||
</Delay>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SlackConfigContainer
|
||||
settings={data.settings}
|
||||
form={form}
|
||||
submitting={submitting}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withRouteConfig<Props>({
|
||||
query: graphql`
|
||||
query SlackConfigRouteQuery {
|
||||
settings {
|
||||
...SlackConfigContainer_settings
|
||||
}
|
||||
}
|
||||
`,
|
||||
cacheConfig: { force: true },
|
||||
})(SlackConfigRoute);
|
||||
|
||||
export default enhanced;
|
||||
@@ -0,0 +1 @@
|
||||
export { default, default as SlackConfigRoute } from "./SlackConfigRoute";
|
||||
@@ -5,3 +5,4 @@ export { GeneralConfigRoute } from "./General";
|
||||
export { ModerationConfigRoute } from "./Moderation";
|
||||
export { OrganizationConfigRoute } from "./Organization";
|
||||
export { WordListConfigRoute } from "./WordList";
|
||||
export { SlackConfigRoute } from "./Slack";
|
||||
|
||||
@@ -79,6 +79,15 @@ exports[`renders configure advanced 1`] = `
|
||||
Email
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link"
|
||||
href="/admin/configure/slack"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Slack
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link Link-linkActive"
|
||||
|
||||
@@ -79,6 +79,15 @@ exports[`renders configure auth 1`] = `
|
||||
Email
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link"
|
||||
href="/admin/configure/slack"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Slack
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link"
|
||||
|
||||
@@ -79,6 +79,15 @@ exports[`renders configure general 1`] = `
|
||||
Email
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link"
|
||||
href="/admin/configure/slack"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Slack
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link"
|
||||
|
||||
@@ -79,6 +79,15 @@ exports[`renders configure moderation 1`] = `
|
||||
Email
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link"
|
||||
href="/admin/configure/slack"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Slack
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link"
|
||||
|
||||
@@ -79,6 +79,15 @@ exports[`renders configure organization 1`] = `
|
||||
Email
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link"
|
||||
href="/admin/configure/slack"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Slack
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link"
|
||||
|
||||
@@ -79,6 +79,15 @@ exports[`renders configure wordList 1`] = `
|
||||
Email
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link"
|
||||
href="/admin/configure/slack"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Slack
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link"
|
||||
|
||||
@@ -90,14 +90,16 @@ it("change language", async () => {
|
||||
act(() => languageField.props.onChange("es"));
|
||||
|
||||
// Send form
|
||||
act(() => {
|
||||
within(configureContainer)
|
||||
await act(async () => {
|
||||
await within(configureContainer)
|
||||
.getByType("form")
|
||||
.props.onSubmit();
|
||||
});
|
||||
|
||||
// Submit button and text field should be disabled.
|
||||
expect(saveChangesButton.props.disabled).toBe(true);
|
||||
await wait(() => {
|
||||
expect(saveChangesButton.props.disabled).toBe(true);
|
||||
});
|
||||
|
||||
// Wait for submission to be finished
|
||||
await act(async () => {
|
||||
|
||||
@@ -161,6 +161,9 @@ export const settings = createFixture<GQLSettings>({
|
||||
changeUsername: true,
|
||||
deleteAccount: true,
|
||||
},
|
||||
slack: {
|
||||
channels: [],
|
||||
},
|
||||
});
|
||||
|
||||
export const settingsWithEmptyAuth = createFixture<GQLSettings>(
|
||||
|
||||
@@ -12,6 +12,7 @@ import { MailerQueue } from "coral-server/queue/tasks/mailer";
|
||||
import { NotifierQueue } from "coral-server/queue/tasks/notifier";
|
||||
import { ScraperQueue } from "coral-server/queue/tasks/scraper";
|
||||
import { JWTSigningConfig } from "coral-server/services/jwt";
|
||||
import createSlackPublisher from "coral-server/services/slack/publisher";
|
||||
import TenantCache from "coral-server/services/tenant/cache";
|
||||
|
||||
import loaders from "./loaders";
|
||||
@@ -58,12 +59,13 @@ export default class TenantContext extends CommonContext {
|
||||
this.mailerQueue = options.mailerQueue;
|
||||
this.signingConfig = options.signingConfig;
|
||||
this.clientID = options.clientID;
|
||||
this.publisher = createPublisher(
|
||||
this.pubsub,
|
||||
this.publisher = createPublisher({
|
||||
pubsub: this.pubsub,
|
||||
slackPublisher: createSlackPublisher(this.mongo, this.tenant),
|
||||
notifierQueue,
|
||||
this.tenant.id,
|
||||
this.clientID
|
||||
);
|
||||
tenantID: this.tenant.id,
|
||||
clientID: this.clientID,
|
||||
});
|
||||
this.loaders = loaders(this);
|
||||
this.mutators = mutators(this);
|
||||
}
|
||||
|
||||
@@ -2,5 +2,5 @@ import { GQLSettingsTypeResolver } from "coral-server/graph/tenant/schema/__gene
|
||||
import { Tenant } from "coral-server/models/tenant";
|
||||
|
||||
export const Settings: GQLSettingsTypeResolver<Tenant> = {
|
||||
staticURI: (tenant, args, ctx) => ctx.config.get("static_uri") || null,
|
||||
slack: ({ slack = {} }) => slack,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import {
|
||||
GQLSlackConfiguration,
|
||||
GQLSlackConfigurationTypeResolver,
|
||||
} from "coral-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
export const SlackConfiguration: GQLSlackConfigurationTypeResolver<
|
||||
GQLSlackConfiguration
|
||||
> = {
|
||||
// TODO: Remove this when we create a migration to generate slack channels on existing tenants
|
||||
channels: ({ channels = [] }) => channels,
|
||||
};
|
||||
@@ -37,6 +37,8 @@ import { Profile } from "./Profile";
|
||||
import { Query } from "./Query";
|
||||
import { RecentCommentHistory } from "./RecentCommentHistory";
|
||||
import { RejectCommentPayload } from "./RejectCommentPayload";
|
||||
import { Settings } from "./Settings";
|
||||
import { SlackConfiguration } from "./SlackConfiguration";
|
||||
import { SSOAuthIntegration } from "./SSOAuthIntegration";
|
||||
import { Story } from "./Story";
|
||||
import { StorySettings } from "./StorySettings";
|
||||
@@ -97,6 +99,8 @@ const Resolvers: GQLResolver = {
|
||||
UsernameHistory,
|
||||
UsernameStatus,
|
||||
UserStatus,
|
||||
Settings,
|
||||
SlackConfiguration,
|
||||
};
|
||||
|
||||
export default Resolvers;
|
||||
|
||||
@@ -936,6 +936,64 @@ type EmailConfiguration {
|
||||
smtp: SMTP! @auth(roles: [ADMIN])
|
||||
}
|
||||
|
||||
################################################################################
|
||||
## Slack Configuration
|
||||
################################################################################
|
||||
|
||||
type SlackConfiguration {
|
||||
"""
|
||||
channels is the set of Slack channels configured to receive comments
|
||||
"""
|
||||
channels: [SlackChannel!]!
|
||||
}
|
||||
|
||||
type SlackChannelTriggers {
|
||||
"""
|
||||
allComments is whether this channel will receive all comments entering moderation
|
||||
"""
|
||||
allComments: Boolean!
|
||||
|
||||
"""
|
||||
reportedComments is whether this channel will receive reported comments
|
||||
"""
|
||||
reportedComments: Boolean!
|
||||
|
||||
"""
|
||||
pendingComments is whether this channel will receive pending comments
|
||||
"""
|
||||
pendingComments: Boolean!
|
||||
|
||||
"""
|
||||
featuredComments is whether this channel will receive featured comments
|
||||
"""
|
||||
featuredComments: Boolean!
|
||||
|
||||
}
|
||||
|
||||
type SlackChannel {
|
||||
"""
|
||||
enabled is whether this Slack channel is enabled to send comments to its
|
||||
correlated web hook
|
||||
"""
|
||||
enabled: Boolean!
|
||||
|
||||
"""
|
||||
name is the name assigned to the Slack channel
|
||||
"""
|
||||
name: String
|
||||
|
||||
"""
|
||||
hookURL defines the URL that comments will be sent to
|
||||
"""
|
||||
hookURL: String
|
||||
|
||||
"""
|
||||
triggers are the filters of types of comments that will be sent to
|
||||
the correlated channel
|
||||
"""
|
||||
triggers: SlackChannelTriggers
|
||||
}
|
||||
|
||||
################################################################################
|
||||
## ReactionConfiguration
|
||||
################################################################################
|
||||
@@ -1217,6 +1275,11 @@ type Settings {
|
||||
"""
|
||||
email: EmailConfiguration! @auth(roles: [ADMIN, MODERATOR])
|
||||
|
||||
"""
|
||||
slack is the configuration for slack communication
|
||||
"""
|
||||
slack: SlackConfiguration! @auth(roles: [ADMIN, MODERATOR])
|
||||
|
||||
"""
|
||||
wordList will return a given list of words.
|
||||
"""
|
||||
@@ -3397,6 +3460,60 @@ input CommenterAccountFeaturesInput {
|
||||
deleteAccount: Boolean
|
||||
}
|
||||
|
||||
input SlackTriggersConfigurationInput {
|
||||
"""
|
||||
allComments is whether this channel will receive all comments entering moderation
|
||||
"""
|
||||
allComments: Boolean
|
||||
|
||||
"""
|
||||
reportedComments is whether this channel will receive reported comments
|
||||
"""
|
||||
reportedComments: Boolean
|
||||
|
||||
"""
|
||||
pendingComments is whether this channel will receive pending comments
|
||||
"""
|
||||
pendingComments: Boolean
|
||||
|
||||
"""
|
||||
featuredComments is whether this channel will receive featured comments
|
||||
"""
|
||||
featuredComments: Boolean
|
||||
|
||||
}
|
||||
|
||||
input SlackChannelConfigurationInput {
|
||||
"""
|
||||
enabled is whether this Slack channel is enabled to send comments to its
|
||||
correlated web hook
|
||||
"""
|
||||
enabled: Boolean
|
||||
|
||||
"""
|
||||
name is the name assigned to the Slack channel
|
||||
"""
|
||||
name: String
|
||||
|
||||
"""
|
||||
hookURL defines the URL that comments will be sent to
|
||||
"""
|
||||
hookURL: String
|
||||
|
||||
"""
|
||||
triggers are the filters of types of comments that will be sent to
|
||||
the correlated channel
|
||||
"""
|
||||
triggers: SlackTriggersConfigurationInput
|
||||
}
|
||||
|
||||
input SlackConfigurationInput {
|
||||
"""
|
||||
channels are the channels configured for Slack integration
|
||||
"""
|
||||
channels: [SlackChannelConfigurationInput!]
|
||||
}
|
||||
|
||||
"""
|
||||
SettingsInput is the partial type of the Settings type for performing mutations.
|
||||
"""
|
||||
@@ -3507,6 +3624,11 @@ input SettingsInput {
|
||||
"""
|
||||
accountFeatures: CommenterAccountFeaturesInput
|
||||
|
||||
"""
|
||||
slack specifies the configuration for Slack integration.
|
||||
"""
|
||||
slack: SlackConfigurationInput
|
||||
|
||||
"""
|
||||
locale specifies the locale for this Tenant.
|
||||
"""
|
||||
|
||||
@@ -4,24 +4,35 @@ import { createSubscriptionChannelName } from "coral-server/graph/tenant/resolve
|
||||
import { SUBSCRIPTION_INPUT } from "coral-server/graph/tenant/resolvers/Subscription/types";
|
||||
import logger from "coral-server/logger";
|
||||
import { NotifierQueue } from "coral-server/queue/tasks/notifier";
|
||||
import { SlackPublisher } from "coral-server/services/slack/publisher";
|
||||
|
||||
export type Publisher = (input: SUBSCRIPTION_INPUT) => Promise<void>;
|
||||
|
||||
export interface PublisherOptions {
|
||||
pubsub: RedisPubSub;
|
||||
slackPublisher: SlackPublisher;
|
||||
notifierQueue: NotifierQueue;
|
||||
tenantID: string;
|
||||
clientID?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* createPublisher will create a new Publisher that can be used to send events
|
||||
* over the pubsub broker to facilitate live updates and notifications.
|
||||
*
|
||||
* TODO: Update
|
||||
* @param pubsub the pubsub broker to be used to facilitate the publish action
|
||||
* @param notifier
|
||||
* @param tenantID the ID of the Tenant where the event will be published with
|
||||
* @param clientID the ID of the client to de-duplicate mutation responses
|
||||
*/
|
||||
export const createPublisher = (
|
||||
pubsub: RedisPubSub,
|
||||
notifier: NotifierQueue,
|
||||
tenantID: string,
|
||||
clientID?: string
|
||||
): Publisher => async input => {
|
||||
export const createPublisher = ({
|
||||
pubsub,
|
||||
slackPublisher,
|
||||
notifierQueue,
|
||||
tenantID,
|
||||
clientID,
|
||||
}: PublisherOptions): Publisher => async input => {
|
||||
const { channel, payload } = input;
|
||||
|
||||
logger.trace({ channel, tenantID, clientID }, "publishing event");
|
||||
@@ -33,8 +44,11 @@ export const createPublisher = (
|
||||
...payload,
|
||||
clientID,
|
||||
}),
|
||||
|
||||
slackPublisher(channel, payload),
|
||||
|
||||
// Notify the notifications queue so we can offload notification processing
|
||||
// to it.
|
||||
notifier.add({ tenantID, input }),
|
||||
notifierQueue.add({ tenantID, input }),
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -137,6 +137,7 @@ export type Settings = GlobalModerationSettings &
|
||||
| "communityGuidelines"
|
||||
| "stories"
|
||||
| "createdAt"
|
||||
| "slack"
|
||||
> & {
|
||||
/**
|
||||
* auth is the set of configured authentication integrations.
|
||||
|
||||
@@ -190,6 +190,9 @@ export async function createTenant(
|
||||
downloadComments: false,
|
||||
},
|
||||
createdAt: now,
|
||||
slack: {
|
||||
channels: [],
|
||||
},
|
||||
};
|
||||
|
||||
// Create the new Tenant by merging it together with the defaults.
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import Logger from "bunyan";
|
||||
import DataLoader from "dataloader";
|
||||
import { Db } from "mongodb";
|
||||
|
||||
import { Comment, retrieveManyComments } from "coral-server/models/comment";
|
||||
import { retrieveManyStories, Story } from "coral-server/models/story";
|
||||
import { retrieveManyUsers, User } from "coral-server/models/user";
|
||||
|
||||
interface Options {
|
||||
mongo: Db;
|
||||
tenantID: string;
|
||||
}
|
||||
|
||||
class SlackContext {
|
||||
public readonly mongo: Db;
|
||||
public readonly tenantID: string;
|
||||
public readonly logger: Logger;
|
||||
|
||||
public readonly comments: DataLoader<
|
||||
string,
|
||||
Readonly<Comment> | null
|
||||
> = new DataLoader(commentIDs =>
|
||||
retrieveManyComments(this.mongo, this.tenantID, commentIDs)
|
||||
);
|
||||
|
||||
public readonly stories: DataLoader<
|
||||
string,
|
||||
Readonly<Story> | null
|
||||
> = new DataLoader(storyIDs =>
|
||||
retrieveManyStories(this.mongo, this.tenantID, storyIDs)
|
||||
);
|
||||
|
||||
public readonly users: DataLoader<
|
||||
string,
|
||||
Readonly<User> | null
|
||||
> = new DataLoader(userIDs =>
|
||||
retrieveManyUsers(this.mongo, this.tenantID, userIDs)
|
||||
);
|
||||
|
||||
constructor({ mongo, tenantID }: Options) {
|
||||
this.mongo = mongo;
|
||||
this.tenantID = tenantID;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlackContext;
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as slackPublisher } from "./publisher";
|
||||
export * from "./publisher";
|
||||
export * from "./context";
|
||||
@@ -0,0 +1,213 @@
|
||||
import { CommentCreatedInput } from "coral-server/graph/tenant/resolvers/Subscription/commentCreated";
|
||||
import { CommentEnteredModerationQueueInput } from "coral-server/graph/tenant/resolvers/Subscription/commentEnteredModerationQueue";
|
||||
import { CommentFeaturedInput } from "coral-server/graph/tenant/resolvers/Subscription/commentFeatured";
|
||||
import { CommentLeftModerationQueueInput } from "coral-server/graph/tenant/resolvers/Subscription/commentLeftModerationQueue";
|
||||
import { CommentReleasedInput } from "coral-server/graph/tenant/resolvers/Subscription/commentReleased";
|
||||
import { CommentReplyCreatedInput } from "coral-server/graph/tenant/resolvers/Subscription/commentReplyCreated";
|
||||
import { CommentStatusUpdatedInput } from "coral-server/graph/tenant/resolvers/Subscription/commentStatusUpdated";
|
||||
import { SUBSCRIPTION_CHANNELS } from "coral-server/graph/tenant/resolvers/Subscription/types";
|
||||
import logger from "coral-server/logger";
|
||||
|
||||
import { GQLMODERATION_QUEUE } from "coral-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
import { Tenant } from "coral-server/models/tenant";
|
||||
import { Db } from "mongodb";
|
||||
import SlackContext from "./context";
|
||||
|
||||
type Payload =
|
||||
| CommentEnteredModerationQueueInput
|
||||
| CommentLeftModerationQueueInput
|
||||
| CommentStatusUpdatedInput
|
||||
| CommentReplyCreatedInput
|
||||
| CommentCreatedInput
|
||||
| CommentFeaturedInput
|
||||
| CommentReleasedInput;
|
||||
|
||||
function enteredModeration(channel: SUBSCRIPTION_CHANNELS, payload: Payload) {
|
||||
const p: any = payload;
|
||||
return (
|
||||
channel === SUBSCRIPTION_CHANNELS.COMMENT_ENTERED_MODERATION_QUEUE &&
|
||||
p.hasOwnProperty("queue") &&
|
||||
(p.queue === GQLMODERATION_QUEUE.REPORTED ||
|
||||
p.queue === GQLMODERATION_QUEUE.PENDING)
|
||||
);
|
||||
}
|
||||
|
||||
function isFeatured(channel: SUBSCRIPTION_CHANNELS) {
|
||||
return channel === SUBSCRIPTION_CHANNELS.COMMENT_FEATURED;
|
||||
}
|
||||
|
||||
function isReported(channel: SUBSCRIPTION_CHANNELS, payload: Payload) {
|
||||
const p: any = payload;
|
||||
return (
|
||||
channel === SUBSCRIPTION_CHANNELS.COMMENT_ENTERED_MODERATION_QUEUE &&
|
||||
p.hasOwnProperty("queue") &&
|
||||
p.queue === GQLMODERATION_QUEUE.REPORTED
|
||||
);
|
||||
}
|
||||
|
||||
function isPending(channel: SUBSCRIPTION_CHANNELS, payload: Payload) {
|
||||
const p: any = payload;
|
||||
return (
|
||||
channel === SUBSCRIPTION_CHANNELS.COMMENT_ENTERED_MODERATION_QUEUE &&
|
||||
p.hasOwnProperty("queue") &&
|
||||
p.queue === GQLMODERATION_QUEUE.PENDING
|
||||
);
|
||||
}
|
||||
|
||||
function createModerationLink(rootURL: string, commentID: string) {
|
||||
return `${rootURL}/admin/moderate/comment/${commentID}`;
|
||||
}
|
||||
|
||||
function createCommentLink(storyURL: string, commentID: string) {
|
||||
const urlBuilder = new URL(storyURL);
|
||||
urlBuilder.searchParams.set("commentID", commentID);
|
||||
return urlBuilder.href;
|
||||
}
|
||||
|
||||
async function postCommentToSlack(
|
||||
ctx: SlackContext,
|
||||
orgURL: string,
|
||||
commentID: string,
|
||||
webhookURL: string
|
||||
) {
|
||||
const comment = await ctx.comments.load(commentID);
|
||||
if (comment === null) {
|
||||
return;
|
||||
}
|
||||
const story = await ctx.stories.load(comment.storyID);
|
||||
if (story === null) {
|
||||
return;
|
||||
}
|
||||
const author = await ctx.users.load(comment.authorID);
|
||||
if (author === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const storyTitle = story.metadata ? story.metadata.title : "";
|
||||
const commentBody =
|
||||
comment.revisions.length > 0
|
||||
? comment.revisions[comment.revisions.length - 1].body
|
||||
: "";
|
||||
const body = commentBody.replace(/<br\/?>/g, "\n");
|
||||
const moderateLink = createModerationLink(orgURL, comment.id);
|
||||
const commentLink = createCommentLink(story.url, comment.id);
|
||||
|
||||
const data = {
|
||||
text: `${author.username} commented on: ${storyTitle}`,
|
||||
blocks: [
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `${author.username} commented on:\n<${story.url}|${storyTitle}>`,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `<${moderateLink}|Go to Moderation> | <${commentLink}|See Comment>`,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "divider",
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: body,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(webhookURL, {
|
||||
method: "POST",
|
||||
cache: "no-cache",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
logger.error({ response }, "error sending Slack comment");
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error({ err }, "error sending Slack comment");
|
||||
}
|
||||
}
|
||||
|
||||
export type SlackPublisher = (
|
||||
channel: SUBSCRIPTION_CHANNELS,
|
||||
payload: Payload
|
||||
) => Promise<void>;
|
||||
|
||||
function createSlackPublisher(mongo: Db, tenant: Tenant): SlackPublisher {
|
||||
if (
|
||||
!tenant.slack ||
|
||||
!tenant.slack.channels ||
|
||||
tenant.slack.channels.length === 0
|
||||
) {
|
||||
return async () => {
|
||||
// noop
|
||||
};
|
||||
}
|
||||
|
||||
const { channels } = tenant.slack;
|
||||
|
||||
return async (channel: SUBSCRIPTION_CHANNELS, payload: Payload) => {
|
||||
const ctx = new SlackContext({ mongo, tenantID: tenant.id });
|
||||
|
||||
try {
|
||||
const inModeration = enteredModeration(channel, payload);
|
||||
const reported = isReported(channel, payload);
|
||||
const pending = isPending(channel, payload);
|
||||
const featured = isFeatured(channel);
|
||||
|
||||
const { commentID } = payload;
|
||||
|
||||
for (const ch of channels) {
|
||||
if (!ch) {
|
||||
return;
|
||||
}
|
||||
if (!ch.enabled) {
|
||||
return;
|
||||
}
|
||||
const { hookURL } = ch;
|
||||
if (!hookURL) {
|
||||
return;
|
||||
}
|
||||
const { triggers } = ch;
|
||||
if (!triggers) {
|
||||
return;
|
||||
}
|
||||
|
||||
const orgURL = tenant.organization.url;
|
||||
|
||||
if (
|
||||
triggers.allComments &&
|
||||
(reported || pending || featured || inModeration)
|
||||
) {
|
||||
await postCommentToSlack(ctx, orgURL, commentID, hookURL);
|
||||
} else if (triggers.reportedComments && reported) {
|
||||
await postCommentToSlack(ctx, orgURL, commentID, hookURL);
|
||||
} else if (triggers.pendingComments && pending) {
|
||||
await postCommentToSlack(ctx, orgURL, commentID, hookURL);
|
||||
} else if (triggers.featuredComments && featured) {
|
||||
await postCommentToSlack(ctx, orgURL, commentID, hookURL);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
{ err, tenantID: tenant.id, channel, payload },
|
||||
"could not handle comment in Slack publisher"
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default createSlackPublisher;
|
||||
@@ -104,7 +104,9 @@ export async function sendUserDownload(
|
||||
const commentID = comment.id;
|
||||
const createdAt = formatter.format(new Date(comment.createdAt));
|
||||
const storyURL = story.url;
|
||||
const commentURL = `${storyURL}?commentID=${commentID}`;
|
||||
const urlBuilder = new URL(storyURL);
|
||||
urlBuilder.searchParams.set("commentID", commentID);
|
||||
const commentURL = urlBuilder.href;
|
||||
const body = revision.body;
|
||||
|
||||
csv.write([commentID, createdAt, storyURL, commentURL, body]);
|
||||
|
||||
@@ -78,6 +78,7 @@ configure-sideBarNavigation-organization = Organization
|
||||
configure-sideBarNavigation-advanced = Advanced
|
||||
configure-sideBarNavigation-email = Email
|
||||
configure-sideBarNavigation-bannedAndSuspectWords = Banned and Suspect Words
|
||||
configure-sideBarNavigation-slack = Slack
|
||||
|
||||
configure-sideBar-saveChanges = Save Changes
|
||||
configure-configurationSubHeader = Configuration
|
||||
@@ -380,6 +381,34 @@ decisionHistory-rejectedCommentBy = Rejected comment by <Username></Username>
|
||||
decisionHistory-approvedCommentBy = Approved comment by <Username></Username>
|
||||
decisionHistory-goToComment = Go to comment
|
||||
|
||||
### Slack
|
||||
|
||||
configure-slack-header-title = Slack Integrations
|
||||
configure-slack-description =
|
||||
Automatically send comments from Coral moderation queues to Slack
|
||||
channels. You will need Slack admin access to set this up. For
|
||||
steps on how to create a Slack App see our <externalLink>documentation</externalLink>.
|
||||
configure-slack-addChannel = Add
|
||||
|
||||
configure-slack-channel-enabled = Enabled
|
||||
configure-slack-channel-remove = Remove
|
||||
configure-slack-channel-name-label = Name
|
||||
configure-slack-channel-name-description =
|
||||
This is only for your information, to easily identify
|
||||
each Slack connection. Slack does not tell us the name
|
||||
of the channel/s you're connecting to Coral.
|
||||
configure-slack-channel-hookURL-label = Webhook URL
|
||||
configure-slack-channel-hookURL-description =
|
||||
Slack provides a channel-specific URL to activate webhook
|
||||
connections. To find the URL for one of your Slack channels,
|
||||
follow the instructions <externalLink>here</externalLink>.
|
||||
configure-slack-channel-triggers-label =
|
||||
Receive notifications in this Slack channel for
|
||||
configure-slack-channel-triggers-allComments = All Comments
|
||||
configure-slack-channel-triggers-reportedComments = Reported Comments
|
||||
configure-slack-channel-triggers-pendingComments = Pending Comments
|
||||
configure-slack-channel-triggers-featuredComments = Featured Comments
|
||||
|
||||
## moderate
|
||||
moderate-navigation-reported = reported
|
||||
moderate-navigation-pending = Pending
|
||||
|
||||
Reference in New Issue
Block a user