Create Moderation actions notification plugin (#2175)

* Create Moderation actions plugin

* Clarify env var in readme

* Reword log message

* Update with suggestion from Wyatt

* Whitelist notification types

* Update messaging

* Rename unpublished to pending
This commit is contained in:
Mendel Konikov
2019-02-07 09:20:44 -05:00
committed by Kiwi
parent 5173af2a27
commit 1928a7b02e
8 changed files with 211 additions and 0 deletions
+1
View File
@@ -45,6 +45,7 @@ plugins/*
!plugins/talk-plugin-moderation-actions
!plugins/talk-plugin-notifications
!plugins/talk-plugin-notifications-category-featured
!plugins/talk-plugin-notifications-category-moderation-actions
!plugins/talk-plugin-notifications-category-reply
!plugins/talk-plugin-notifications-category-staff
!plugins/talk-plugin-notifications-digest-daily
@@ -0,0 +1,19 @@
---
title: talk-plugin-notifications-moderation-actions
permalink: /plugin/talk-plugin-notifications-moderation-actions/
layout: plugin
plugin:
name: talk-plugin-notifications-moderation-actions
depends:
- name: talk-plugin-notifications
provides:
- Server
- Client
---
When a comment that is initially withheld from publication and is then
approved or rejected, the user will receive a notification email.
## Configuration:
- `TALK_MODERATION_NOTIFICATION_TYPES`. This plugin requires values to be set. Available options: `APPROVED`, `REJECTED` as a single string (comma separated).
@@ -0,0 +1,3 @@
{
"extends": "@coralproject/eslint-config-talk/client"
}
@@ -0,0 +1,14 @@
import translations from './translations.yml';
import { t } from 'plugin-api/beta/client/services';
import { createSettingsToggle } from 'talk-plugin-notifications/client/api/factories';
const SettingsToggle = createSettingsToggle('onModeration', () =>
t('talk-plugin-notifications-category-moderation-actions.toggle_description')
);
export default {
slots: {
notificationSettings: [SettingsToggle],
},
translations,
};
@@ -0,0 +1,3 @@
en:
talk-plugin-notifications-category-moderation-actions:
toggle_description: My pending comments have been reviewed
@@ -0,0 +1,8 @@
const MODERATION_NOTIFICATION_TYPES =
(process.env.TALK_MODERATION_NOTIFICATION_TYPES &&
process.env.TALK_MODERATION_NOTIFICATION_TYPES.split(',')) ||
[];
module.exports = {
MODERATION_NOTIFICATION_TYPES,
};
@@ -0,0 +1,153 @@
const { get } = require('lodash');
const path = require('path');
const { MODERATION_NOTIFICATION_TYPES } = require('./config');
const PENDING_STATUS_TYPES = ['PREMOD', 'SYSTEM_WITHHELD'];
const AVAILABLE_NOTIFICATION_TYPES = ['APPROVED', 'REJECTED'];
const handle = async (ctx, comment) => {
const commentID = get(comment, 'id', null);
if (commentID === null) {
ctx.log.info('could not get comment id');
return;
}
// Check to see if this was a pending comment.
const commentHistory = get(comment, 'status_history', []);
let wasPending = false;
// Check for last status before current one
if (commentHistory.length >= 2) {
const previousStatus = commentHistory[commentHistory.length - 2];
if (PENDING_STATUS_TYPES.includes(previousStatus.type)) {
wasPending = true;
}
}
if (!wasPending) {
ctx.log.info('comment was not pending');
return;
}
// Execute the graph request.
const commentQl = await ctx.graphql(
`
query GetAuthorUserMetadata($comment_id: ID!) {
comment(id: $comment_id) {
id
user {
id
notificationSettings {
onModeration
}
}
}
}
`,
{ comment_id: commentID }
);
if (commentQl.errors) {
ctx.log.error(
{ err: commentQl.errors },
'could not query for author metadata'
);
return;
}
// Check if the user has notifications enabled.
const enabled = get(
commentQl,
'data.comment.user.notificationSettings.onModeration',
false
);
if (!enabled) {
return;
}
const userID = get(commentQl, 'data.comment.user.id', null);
if (!userID) {
ctx.log.info('could not get comment user id');
return;
}
// The user does have notifications for moderated comments enabled, queue the
// notification to be sent.
return { userID, date: comment.created_at, context: comment.id };
};
const hydrate = async (ctx, category, context) => {
const comment = await ctx.graphql(
`
query GetNotificationData($context: ID!) {
comment(id: $context) {
id
asset {
title
url
}
status_history {
type
}
}
}
`,
{ context }
);
if (comment.errors) {
throw comment.errors;
}
const commentData = get(comment, 'data.comment');
const headline = get(commentData, 'asset.title', null);
const assetURL = get(commentData, 'asset.url', null);
const permalink = `${assetURL}?commentId=${commentData.id}`;
return [headline, permalink];
};
const handlers = {
approved: {
handle,
category: 'moderation-actions.approved',
event: 'commentAccepted',
hydrate,
digestOrder: 10,
},
rejected: {
handle,
category: 'moderation-actions.rejected',
event: 'commentRejected',
hydrate,
digestOrder: 10,
},
};
const notifications = [];
MODERATION_NOTIFICATION_TYPES.forEach(type => {
if (AVAILABLE_NOTIFICATION_TYPES.includes(type)) {
notifications.push(handlers[type.toLowerCase()]);
} else {
console.error(`Unkown moderation notification type: ${type}`);
}
});
module.exports = {
typeDefs: `
type NotificationSettings {
onModeration: Boolean!
}
input NotificationSettingsInput {
onModeration: Boolean
}
`,
resolvers: {
NotificationSettings: {
// onModeration returns false by default if not specified.
onModeration: settings => get(settings, 'onModeration', false),
},
},
translations: path.join(__dirname, 'translations.yml'),
notifications,
};
@@ -0,0 +1,10 @@
en:
talk-plugin-notifications:
categories:
moderation-actions:
approved:
subject: "Your comment on {0} has been published"
body: "{0}\nThank you for submitting your comment. Your comment has now been published: {1}"
rejected:
subject: "Your comment on {0} was not published"
body: "{0}\nThe language used in one of your comments did not comply with our community guidelines, and so the comment has been removed."