mirror of
https://github.com/wassname/talk.git
synced 2026-06-27 19:33:06 +08:00
[CORL-924/CORL-923] show all comments or staff comments in slack (#2872)
* add allcomments option to slack config * create separate class for hadnling slack event publishing * add strings Co-authored-by: Wyatt Johnson <wyattjoh@gmail.com> Co-authored-by: Kim Gardner <kgardnr@gmail.com>
This commit is contained in:
@@ -153,6 +153,42 @@ const SlackChannel: FunctionComponent<Props> = ({
|
||||
</div>
|
||||
|
||||
<div className={styles.notificationToggles}>
|
||||
<Field
|
||||
name={`${channel}.triggers.allComments`}
|
||||
type="checkbox"
|
||||
parse={parseBool}
|
||||
>
|
||||
{({ input }) => (
|
||||
<CheckBox
|
||||
id={`configure-slack-channel-triggers-allComments-${input.name}`}
|
||||
disabled={disabled || !channelEnabled}
|
||||
className={styles.trigger}
|
||||
{...input}
|
||||
>
|
||||
<Localized id="configure-slack-channel-triggers-allComments">
|
||||
All Comments
|
||||
</Localized>
|
||||
</CheckBox>
|
||||
)}
|
||||
</Field>
|
||||
<Field
|
||||
name={`${channel}.triggers.staffComments`}
|
||||
type="checkbox"
|
||||
parse={parseBool}
|
||||
>
|
||||
{({ input }) => (
|
||||
<CheckBox
|
||||
id={`configure-slack-channel-triggers-staffComments-${input.name}`}
|
||||
disabled={disabled || !channelEnabled}
|
||||
className={styles.trigger}
|
||||
{...input}
|
||||
>
|
||||
<Localized id="configure-slack-channel-triggers-staffComments">
|
||||
Staff Comments
|
||||
</Localized>
|
||||
</CheckBox>
|
||||
)}
|
||||
</Field>
|
||||
<Field
|
||||
name={`${channel}.triggers.reportedComments`}
|
||||
type="checkbox"
|
||||
|
||||
@@ -144,6 +144,8 @@ const enhanced = withFragmentContainer<Props>({
|
||||
reportedComments
|
||||
pendingComments
|
||||
featuredComments
|
||||
allComments
|
||||
staffComments
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
import striptags from "striptags";
|
||||
|
||||
import { reconstructTenantURL } from "coral-server/app/url";
|
||||
import GraphContext from "coral-server/graph/context";
|
||||
import logger from "coral-server/logger";
|
||||
import { getLatestRevision } from "coral-server/models/comment";
|
||||
import { getStoryTitle, getURLWithCommentID } from "coral-server/models/story";
|
||||
import { createFetch } from "coral-server/services/fetch";
|
||||
|
||||
import { GQLMODERATION_QUEUE } from "coral-server/graph/schema/__generated__/types";
|
||||
|
||||
import {
|
||||
CommentEnteredModerationQueueCoralEventPayload,
|
||||
CommentFeaturedCoralEventPayload,
|
||||
} from "../events";
|
||||
import { CoralEventListener, CoralEventPublisherFactory } from "../publisher";
|
||||
import { CoralEventType } from "../types";
|
||||
|
||||
type SlackCoralEventListenerPayloads =
|
||||
| CommentFeaturedCoralEventPayload
|
||||
| CommentEnteredModerationQueueCoralEventPayload;
|
||||
|
||||
type Trigger = "reported" | "pending" | "featured";
|
||||
|
||||
export class SlackCoralEventListener
|
||||
implements CoralEventListener<SlackCoralEventListenerPayloads> {
|
||||
public readonly name = "slack";
|
||||
public readonly events = [
|
||||
CoralEventType.COMMENT_FEATURED,
|
||||
CoralEventType.COMMENT_ENTERED_MODERATION_QUEUE,
|
||||
];
|
||||
private readonly fetch = createFetch({ name: "slack" });
|
||||
|
||||
private payloadTrigger(
|
||||
payload: SlackCoralEventListenerPayloads
|
||||
): Trigger | null {
|
||||
switch (payload.type) {
|
||||
case CoralEventType.COMMENT_ENTERED_MODERATION_QUEUE:
|
||||
if (payload.data.queue === GQLMODERATION_QUEUE.REPORTED) {
|
||||
return "reported";
|
||||
} else if (payload.data.queue === GQLMODERATION_QUEUE.PENDING) {
|
||||
return "pending";
|
||||
}
|
||||
break;
|
||||
case CoralEventType.COMMENT_FEATURED:
|
||||
return "featured";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* postMessage will prepare and send the incoming Slack webhook.
|
||||
*
|
||||
* @param ctx context of the request
|
||||
* @param message the message prefix for the request
|
||||
* @param payload payload for the event that occurred
|
||||
* @param hookURL url to the Slack webhook that we should send the message to
|
||||
*/
|
||||
private async postMessage(
|
||||
{ loaders, config, tenant, req }: GraphContext,
|
||||
message: string,
|
||||
payload: SlackCoralEventListenerPayloads,
|
||||
hookURL: string
|
||||
) {
|
||||
// Get the comment.
|
||||
const comment = await loaders.Comments.comment.load(payload.data.commentID);
|
||||
if (!comment || !comment.authorID) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the story.
|
||||
const story = await loaders.Stories.story.load(payload.data.storyID);
|
||||
if (!story) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the author.
|
||||
const author = await loaders.Users.user.load(comment.authorID);
|
||||
if (!author) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get some properties about the event.
|
||||
const storyTitle = getStoryTitle(story);
|
||||
const moderateLink = reconstructTenantURL(
|
||||
config,
|
||||
tenant,
|
||||
req,
|
||||
`/admin/moderate/comment/${comment.id}`
|
||||
);
|
||||
const commentLink = getURLWithCommentID(story.url, comment.id);
|
||||
|
||||
// Replace HTML link breaks with newlines.
|
||||
const body = striptags(getLatestRevision(comment).body);
|
||||
|
||||
// Send the post to the Slack URL. We don't wrap this in a try/catch because
|
||||
// it's handled in the calling function.
|
||||
const res = await this.fetch(hookURL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
blocks: [
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `${message} on *<${story.url}|${storyTitle}>*`,
|
||||
},
|
||||
},
|
||||
{ type: "divider" },
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: body,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "context",
|
||||
elements: [
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: `Authored by *${author.username}* | <${moderateLink}|Go to Moderation> | <${commentLink}|See Comment>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ type: "divider" },
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
// Check that the request was completed successfully.
|
||||
if (!res.ok) {
|
||||
throw new Error(`slack returned non-200 status code: ${res.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
private getMessage(trigger: Trigger): string {
|
||||
switch (trigger) {
|
||||
case "featured":
|
||||
return "This comment has been featured";
|
||||
case "pending":
|
||||
return "This comment is pending";
|
||||
case "reported":
|
||||
return "This comment has been reported";
|
||||
default:
|
||||
throw new Error("invalid trigger");
|
||||
}
|
||||
}
|
||||
|
||||
public initialize: CoralEventPublisherFactory<
|
||||
SlackCoralEventListenerPayloads
|
||||
> = ctx => async payload => {
|
||||
const {
|
||||
tenant: { id: tenantID, slack },
|
||||
} = ctx;
|
||||
|
||||
if (
|
||||
// If slack is not defined,
|
||||
!slack ||
|
||||
// Or there are no slack channels,
|
||||
slack.channels.length === 0 ||
|
||||
// Or each channel isn't enabled or configured right.
|
||||
slack.channels.every(c => !c.enabled || !c.hookURL)
|
||||
) {
|
||||
// Exit out then.
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the trigger that is associated with this payload.
|
||||
const trigger = this.payloadTrigger(payload);
|
||||
if (!trigger) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For each channel that is enabled with configuration.
|
||||
for (const channel of slack.channels) {
|
||||
if (!channel.enabled || !channel.hookURL) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
// If featured comments are, and it's a featured comment,
|
||||
(channel.triggers.featuredComments && trigger === "featured") ||
|
||||
// Or reported comments are, and it's a reported comment,
|
||||
(channel.triggers.reportedComments && trigger === "reported") ||
|
||||
// Or pending comments are, and it's a pending comment,
|
||||
(channel.triggers.pendingComments && trigger === "pending")
|
||||
) {
|
||||
try {
|
||||
// Post the message to slack.
|
||||
await this.postMessage(
|
||||
ctx,
|
||||
this.getMessage(trigger),
|
||||
payload,
|
||||
channel.hookURL
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
{ err, tenantID, payload, channel },
|
||||
"could not post the comment to slack"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./slack";
|
||||
@@ -0,0 +1,127 @@
|
||||
import striptags from "striptags";
|
||||
|
||||
import { reconstructTenantURL } from "coral-server/app/url";
|
||||
import GraphContext from "coral-server/graph/context";
|
||||
import { Comment, getLatestRevision } from "coral-server/models/comment";
|
||||
import {
|
||||
getStoryTitle,
|
||||
getURLWithCommentID,
|
||||
Story,
|
||||
} from "coral-server/models/story";
|
||||
import { User } from "coral-server/models/user";
|
||||
|
||||
import {
|
||||
GQLSlackChannel,
|
||||
GQLUSER_ROLE,
|
||||
} from "coral-server/graph/schema/__generated__/types";
|
||||
|
||||
export type Trigger = "reported" | "pending" | "featured" | "created";
|
||||
|
||||
export default class SlackPublishEvent {
|
||||
public comment: Comment;
|
||||
public story: Story;
|
||||
public author: User;
|
||||
public actionType: Trigger;
|
||||
constructor(
|
||||
actionType: Trigger,
|
||||
comment: Comment,
|
||||
story: Story,
|
||||
author: User
|
||||
) {
|
||||
this.actionType = actionType;
|
||||
this.comment = comment;
|
||||
this.story = story;
|
||||
this.author = author;
|
||||
}
|
||||
|
||||
private getTrigger(): Trigger | "staffCreated" | null {
|
||||
if (
|
||||
this.actionType &&
|
||||
this.actionType === "created" &&
|
||||
this.authorIsStaff()
|
||||
) {
|
||||
return "staffCreated";
|
||||
}
|
||||
return this.actionType;
|
||||
}
|
||||
|
||||
private authorIsStaff() {
|
||||
return Boolean(
|
||||
this.author &&
|
||||
[
|
||||
GQLUSER_ROLE.ADMIN,
|
||||
GQLUSER_ROLE.MODERATOR,
|
||||
GQLUSER_ROLE.STAFF,
|
||||
].includes(this.author.role)
|
||||
);
|
||||
}
|
||||
|
||||
public getMessage(): string {
|
||||
switch (this.getTrigger()) {
|
||||
case "created":
|
||||
return "This comment has been created";
|
||||
case "staffCreated":
|
||||
return "This comment has been created by staff";
|
||||
case "featured":
|
||||
return "This comment has been featured";
|
||||
case "pending":
|
||||
return "This comment is pending";
|
||||
case "reported":
|
||||
return "This comment has been reported";
|
||||
default:
|
||||
throw new Error("invalid trigger");
|
||||
}
|
||||
}
|
||||
|
||||
public shouldPublishToChannel({ triggers }: GQLSlackChannel) {
|
||||
const trigger = this.getTrigger();
|
||||
return (
|
||||
(triggers.allComments && trigger === "created") ||
|
||||
(triggers.featuredComments && trigger === "featured") ||
|
||||
(triggers.reportedComments && trigger === "reported") ||
|
||||
(triggers.pendingComments && trigger === "pending") ||
|
||||
(triggers.staffComments && trigger === "staffCreated")
|
||||
);
|
||||
}
|
||||
|
||||
public getBlocks({ loaders, config, tenant, req }: GraphContext) {
|
||||
const storyTitle = getStoryTitle(this.story);
|
||||
const moderateLink = reconstructTenantURL(
|
||||
config,
|
||||
tenant,
|
||||
req,
|
||||
`/admin/moderate/comment/${this.comment.id}`
|
||||
);
|
||||
const commentLink = getURLWithCommentID(this.story.url, this.comment.id);
|
||||
|
||||
// Replace HTML link breaks with newlines.
|
||||
const body = striptags(getLatestRevision(this.comment).body);
|
||||
return [
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `${this.getMessage()} on *<${this.story.url}|${storyTitle}>*`,
|
||||
},
|
||||
},
|
||||
{ type: "divider" },
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: body,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "context",
|
||||
elements: [
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: `Authored by *${this.author.username}* | <${moderateLink}|Go to Moderation> | <${commentLink}|See Comment>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ type: "divider" },
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import logger from "coral-server/logger";
|
||||
import { createFetch } from "coral-server/services/fetch";
|
||||
|
||||
import SlackPublishEvent, { Trigger } from "./publishEvent";
|
||||
|
||||
import {
|
||||
CommentCreatedCoralEventPayload,
|
||||
CommentEnteredModerationQueueCoralEventPayload,
|
||||
CommentFeaturedCoralEventPayload,
|
||||
} from "../../events";
|
||||
import {
|
||||
CoralEventListener,
|
||||
CoralEventPublisherFactory,
|
||||
} from "../../publisher";
|
||||
import { CoralEventType } from "../../types";
|
||||
|
||||
import { GQLMODERATION_QUEUE } from "coral-server/graph/schema/__generated__/types";
|
||||
type SlackCoralEventListenerPayloads =
|
||||
| CommentFeaturedCoralEventPayload
|
||||
| CommentEnteredModerationQueueCoralEventPayload
|
||||
| CommentCreatedCoralEventPayload;
|
||||
|
||||
export class SlackCoralEventListener
|
||||
implements CoralEventListener<SlackCoralEventListenerPayloads> {
|
||||
public readonly name = "slack";
|
||||
public readonly events = [
|
||||
CoralEventType.COMMENT_FEATURED,
|
||||
CoralEventType.COMMENT_ENTERED_MODERATION_QUEUE,
|
||||
CoralEventType.COMMENT_CREATED,
|
||||
];
|
||||
private readonly fetch = createFetch({ name: "slack" });
|
||||
|
||||
/**
|
||||
* postMessage will prepare and send the incoming Slack webhook.
|
||||
*
|
||||
* @param hookURL url to the Slack webhook that we should send the message to
|
||||
* @param blocks the blocks for the message
|
||||
*/
|
||||
private async postMessage(hookURL: string, blocks: any[]) {
|
||||
// Send the post to the Slack URL. We don't wrap this in a try/catch because
|
||||
// it's handled in the calling function.
|
||||
const res = await this.fetch(hookURL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
blocks,
|
||||
}),
|
||||
});
|
||||
|
||||
// Check that the request was completed successfully.
|
||||
if (!res.ok) {
|
||||
throw new Error(`slack returned non-200 status code: ${res.status}`);
|
||||
}
|
||||
}
|
||||
private getActionType(
|
||||
payload: SlackCoralEventListenerPayloads
|
||||
): Trigger | null {
|
||||
switch (payload.type) {
|
||||
case CoralEventType.COMMENT_CREATED:
|
||||
return "created";
|
||||
case CoralEventType.COMMENT_ENTERED_MODERATION_QUEUE:
|
||||
if (payload.data.queue === GQLMODERATION_QUEUE.REPORTED) {
|
||||
return "reported";
|
||||
} else if (payload.data.queue === GQLMODERATION_QUEUE.PENDING) {
|
||||
return "pending";
|
||||
}
|
||||
break;
|
||||
case CoralEventType.COMMENT_FEATURED:
|
||||
return "featured";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public initialize: CoralEventPublisherFactory<
|
||||
SlackCoralEventListenerPayloads
|
||||
> = ctx => async payload => {
|
||||
const {
|
||||
tenant: { id: tenantID, slack },
|
||||
} = ctx;
|
||||
|
||||
if (
|
||||
// If slack is not defined,
|
||||
!slack ||
|
||||
// Or there are no slack channels,
|
||||
slack.channels.length === 0 ||
|
||||
// Or each channel isn't enabled or configured right.
|
||||
slack.channels.every(c => !c.enabled || !c.hookURL)
|
||||
) {
|
||||
// Exit out then.
|
||||
return;
|
||||
}
|
||||
|
||||
const actionType = this.getActionType(payload);
|
||||
if (!actionType) {
|
||||
return;
|
||||
}
|
||||
|
||||
const comment = await ctx.loaders.Comments.comment.load(
|
||||
payload.data.commentID
|
||||
);
|
||||
if (!comment || !comment.authorID) {
|
||||
return;
|
||||
}
|
||||
|
||||
const author = await ctx.loaders.Users.user.load(comment.authorID);
|
||||
if (!author) {
|
||||
return;
|
||||
}
|
||||
|
||||
const story = await ctx.loaders.Stories.story.load(payload.data.storyID);
|
||||
if (!story) {
|
||||
return;
|
||||
}
|
||||
|
||||
const publishEvent = new SlackPublishEvent(
|
||||
actionType,
|
||||
comment,
|
||||
story,
|
||||
author
|
||||
);
|
||||
|
||||
// For each channel that is enabled with configuration.
|
||||
for (const channel of slack.channels) {
|
||||
if (!channel.enabled || !channel.hookURL) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (publishEvent.shouldPublishToChannel(channel)) {
|
||||
try {
|
||||
// Post the message to slack.
|
||||
await this.postMessage(channel.hookURL, publishEvent.getBlocks(ctx));
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
{ err, tenantID, payload, channel },
|
||||
"could not post the comment to slack"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1040,17 +1040,27 @@ type SlackChannelTriggers {
|
||||
"""
|
||||
reportedComments is whether this channel will receive reported comments
|
||||
"""
|
||||
reportedComments: Boolean!
|
||||
reportedComments: Boolean
|
||||
|
||||
"""
|
||||
pendingComments is whether this channel will receive pending comments
|
||||
"""
|
||||
pendingComments: Boolean!
|
||||
pendingComments: Boolean
|
||||
|
||||
"""
|
||||
featuredComments is whether this channel will receive featured comments
|
||||
"""
|
||||
featuredComments: Boolean!
|
||||
featuredComments: Boolean
|
||||
|
||||
"""
|
||||
allComents is whether the channel will receive all comments
|
||||
"""
|
||||
allComments: Boolean
|
||||
|
||||
"""
|
||||
staffComments is whether the channel will receive staff comments
|
||||
"""
|
||||
staffComments: Boolean
|
||||
}
|
||||
|
||||
type SlackChannel {
|
||||
@@ -3895,6 +3905,15 @@ input SlackTriggersConfigurationInput {
|
||||
featuredComments is whether this channel will receive featured comments
|
||||
"""
|
||||
featuredComments: Boolean
|
||||
"""
|
||||
allComents is whether the channel will receive all comments
|
||||
"""
|
||||
allComments: Boolean
|
||||
|
||||
"""
|
||||
staffComments is whether the channel will receive staff comments
|
||||
"""
|
||||
staffComments: Boolean
|
||||
}
|
||||
|
||||
input SlackChannelConfigurationInput {
|
||||
|
||||
@@ -672,6 +672,8 @@ configure-slack-channel-triggers-label =
|
||||
configure-slack-channel-triggers-reportedComments = Reported Comments
|
||||
configure-slack-channel-triggers-pendingComments = Pending Comments
|
||||
configure-slack-channel-triggers-featuredComments = Featured Comments
|
||||
configure-slack-channel-triggers-allComments = All Comments
|
||||
configure-slack-channel-triggers-staffComments = Staff Comments
|
||||
|
||||
## moderate
|
||||
moderate-navigation-reported = reported
|
||||
|
||||
Reference in New Issue
Block a user