Email confirmation check

- filtering for confirmed emails
- fixes to logger to expose logger controls
- refactored graphql calls in other notification plugins
This commit is contained in:
Wyatt Johnson
2018-03-01 14:43:29 -07:00
parent 58269a7cae
commit 86c36f7d39
12 changed files with 213 additions and 92 deletions
@@ -1,20 +1,16 @@
const { graphql } = require('graphql');
const { get } = require('lodash');
const path = require('path');
const handle = async (ctx, { comment }) => {
const { connectors: { graph: { schema } } } = ctx;
// Check to see if this is a reply to an existing comment.
const commentID = get(comment, 'id', null);
if (commentID === null) {
ctx.log.debug('could not get comment id');
ctx.log.info('could not get comment id');
return;
}
// Execute the graph request.
const reply = await graphql(
schema,
const reply = await ctx.graphql(
`
query GetAuthorUserMetadata($comment_id: ID!) {
comment(id: $comment_id) {
@@ -28,8 +24,6 @@ const handle = async (ctx, { comment }) => {
}
}
`,
{},
ctx,
{ comment_id: commentID }
);
if (reply.errors) {
@@ -49,7 +43,7 @@ const handle = async (ctx, { comment }) => {
const userID = get(reply, 'data.comment.user.id', null);
if (!userID) {
ctx.log.debug('could not get comment user id');
ctx.log.info('could not get comment user id');
return;
}
@@ -59,10 +53,7 @@ const handle = async (ctx, { comment }) => {
};
const hydrate = async (ctx, category, context) => {
const { connectors: { graph: { schema } } } = ctx;
const reply = await graphql(
schema,
const reply = await ctx.graphql(
`
query GetNotificationData($context: ID!) {
comment(id: $context) {
@@ -74,8 +65,6 @@ const hydrate = async (ctx, category, context) => {
}
}
`,
{},
ctx,
{ context }
);
if (reply.errors) {
@@ -1,20 +1,16 @@
const { graphql } = require('graphql');
const { get } = require('lodash');
const path = require('path');
const handle = async (ctx, comment) => {
const { connectors: { graph: { schema } } } = ctx;
// Check to see if this is a reply to an existing comment.
const parentID = get(comment, 'parent_id', null);
if (parentID === null) {
ctx.log.debug('could not get parent comment id');
ctx.log.info('could not get parent comment id');
return;
}
// Execute the graph request.
const reply = await graphql(
schema,
const reply = await ctx.graphql(
`
query GetAuthorUserMetadata($comment_id: ID!) {
comment(id: $comment_id) {
@@ -28,8 +24,6 @@ const handle = async (ctx, comment) => {
}
}
`,
{},
ctx,
{ comment_id: parentID }
);
if (reply.errors) {
@@ -49,14 +43,14 @@ const handle = async (ctx, comment) => {
const userID = get(reply, 'data.comment.user.id', null);
if (!userID) {
ctx.log.debug('could not get parent comment user id');
ctx.log.info('could not get parent comment user id');
return;
}
// Check to see if this is yourself replying to yourself, if that's the case
// don't send a notification.
if (userID === get(comment, 'author_id')) {
ctx.log.debug('user id of parent comment is the same as the new comment');
ctx.log.info('user id of parent comment is the same as the new comment');
return;
}
@@ -66,10 +60,7 @@ const handle = async (ctx, comment) => {
};
const hydrate = async (ctx, category, context) => {
const { connectors: { graph: { schema } } } = ctx;
const reply = await graphql(
schema,
const reply = await ctx.graphql(
`
query GetNotificationData($context: ID!) {
comment(id: $context) {
@@ -84,8 +75,6 @@ const hydrate = async (ctx, category, context) => {
}
}
`,
{},
ctx,
{ context }
);
if (reply.errors) {
@@ -1,14 +1,11 @@
const { graphql } = require('graphql');
const { get } = require('lodash');
const path = require('path');
const handle = async (ctx, comment) => {
const { connectors: { graph: { schema } } } = ctx;
// Check to see if this is a reply to an existing comment.
const parentID = get(comment, 'parent_id', null);
if (parentID === null) {
ctx.log.debug('could not get parent comment id');
ctx.log.info('could not get parent comment id');
return;
}
@@ -19,8 +16,7 @@ const handle = async (ctx, comment) => {
}
// Execute the graph request.
const reply = await graphql(
schema,
const reply = await ctx.graphql(
`
query GetAuthorUserMetadata($comment_id: ID!, $author_id: ID!) {
author: user(id: $author_id) {
@@ -37,8 +33,6 @@ const handle = async (ctx, comment) => {
}
}
`,
{},
ctx,
{ comment_id: parentID, author_id: authorID }
);
if (reply.errors) {
@@ -53,27 +47,27 @@ const handle = async (ctx, comment) => {
false
);
if (!enabled) {
ctx.log.debug('onStaffReply is false, will not send the notification');
ctx.log.info('onStaffReply is false, will not send the notification');
return;
}
const userID = get(reply, 'data.comment.user.id', null);
if (!userID) {
ctx.log.debug('could not get parent comment user id');
ctx.log.info('could not get parent comment user id');
return;
}
// Check to see if this is yourself replying to yourself, if that's the case
// don't send a notification.
if (userID === authorID) {
ctx.log.debug('user id of parent comment is the same as the new comment');
ctx.log.info('user id of parent comment is the same as the new comment');
return;
}
// Check to see that this comment was indeed from a staff member.
const role = get(reply, 'data.author.role');
if (!['ADMIN', 'MODERATOR', 'STAFF'].includes(role)) {
ctx.log.debug({ role }, 'reply author is not a staff member');
ctx.log.info({ role }, 'reply author is not a staff member');
return;
}
@@ -83,10 +77,7 @@ const handle = async (ctx, comment) => {
};
const hydrate = async (ctx, category, context) => {
const { connectors: { graph: { schema } } } = ctx;
const reply = await graphql(
schema,
const reply = await ctx.graphql(
`
query GetNotificationData($context: ID!) {
comment(id: $context) {
@@ -104,8 +95,6 @@ const hydrate = async (ctx, category, context) => {
}
}
`,
{},
ctx,
{ context }
);
if (reply.errors) {
@@ -1,7 +1,10 @@
const { groupBy, forEach, property } = require('lodash');
const { get, find, groupBy, forEach, property } = require('lodash');
const debug = require('debug')('talk-plugin-notifications');
const uuid = require('uuid/v4');
const { UNSUBSCRIBE_SUBJECT } = require('./config');
const {
UNSUBSCRIBE_SUBJECT,
DISABLE_REQUIRE_EMAIL_VERIFICATIONS,
} = require('./config');
// handleHandlers will call the handle method on each handler to determine if a
// notification should be sent for it.
@@ -15,12 +18,12 @@ const handleHandlers = (ctx, handlers, ...args) =>
// Attempt to create a notification out of it.
const notification = await handle(ctx, ...args);
if (!notification) {
ctx.log.debug('no notification deemed by event handler');
ctx.log.info('no notification deemed by event handler');
return;
}
// Send the notification back.
ctx.log.debug({ category, event }, 'notification detected for event');
ctx.log.info({ category, event }, 'notification detected for event');
return { handler, notification };
} catch (err) {
ctx.log.error({ err }, 'could not handle the event');
@@ -31,13 +34,82 @@ const handleHandlers = (ctx, handlers, ...args) =>
// filterSuperseded will filter all the possible notifications and only send
// those notifications that are not superseded by another type of notification.
const filterSuperseded = ({ handler: { category } }, index, notifications) =>
!notifications.some(({ handler: { supersedesCategories = [] } }) =>
supersedesCategories.some(
supersededCategory => supersededCategory === category
)
const filterSuperseded = (
{ handler: { category }, notification: { userID: destinationUserID } },
index,
notifications
) =>
!notifications.some(
({
handler: { supersedesCategories = [] },
notification: { userID: notificationUserID },
}) =>
// Only allow notifications to supersede another notification if that
// notification is also destined for the same user.
notificationUserID === destinationUserID &&
// If another notification that is destined for the same user also exists
// and declares that it supersedes this one, return true so we can filter
// this one from the list.
supersedesCategories.some(
supersededCategory => supersededCategory === category
)
);
const filterVerified = async (ctx, notifications) => {
notifications = await Promise.all(
notifications.map(async notification => {
// Grab the user that we're supposed to be sending the notification to.
const { notification: { userID } } = notification;
// Check their confirmed status.
const { errors, data } = await ctx.graphql(
`
query CheckUserConfirmation($userID: ID!) {
user(id: $userID) {
profiles {
provider
... on LocalUserProfile {
confirmedAt
}
}
}
}
`,
{ userID }
);
if (errors) {
ctx.log.error(
{ err: errors },
'could not query for user confirmation status'
);
return;
}
const profile = find(get(data, 'user.profiles', []), [
'provider',
'local',
]);
if (!profile) {
ctx.log.warn({ user_id: userID }, 'user did not have a local profile');
return;
}
const confirmed = get(profile, 'confirmedAt', null) !== null;
if (!confirmed) {
ctx.log.info(
{ user_id: userID },
'user did not have their local profile confirmed, but had settings enabled, not mailing'
);
return;
}
return notification;
})
);
return notifications.filter(property('notification'));
};
class NotificationManager {
constructor(context) {
this.context = context;
@@ -93,6 +165,12 @@ class NotificationManager {
// had this notification superseded.
notifications = notifications.filter(filterSuperseded);
// Only let notifications through for users who have their email addresses
// verified if we are configured to do so.
if (!DISABLE_REQUIRE_EMAIL_VERIFICATIONS) {
notifications = await filterVerified(ctx, notifications);
}
// Send the remaining notifications.
return Promise.all(
notifications.map(
@@ -120,7 +198,7 @@ class NotificationManager {
'organizationName'
);
if (organizationName === null) {
ctx.log.debug(
ctx.log.error(
'could not send the notification, organization name not in settings'
);
return;
@@ -153,7 +231,7 @@ class NotificationManager {
user: userID,
});
ctx.log.debug(`Sent the notification for Job.ID[${task.id}]`);
ctx.log.info(`Sent the notification for Job.ID[${task.id}]`);
} catch (err) {
ctx.log.error(
{ err, message: err.message },
@@ -172,10 +250,10 @@ class NotificationManager {
*/
async getBody(ctx, handler, context) {
const { connectors: { services: { I18n: { t } } } } = ctx;
const { category } = handler;
const { category, hydrate = () => [] } = handler;
// Get the body replacement variables for the translation key.
const replacements = await handler.hydrate(ctx, category, context);
const replacements = await hydrate(ctx, category, context);
// Generate the body.
return t(
@@ -1,3 +1,8 @@
module.exports = {
UNSUBSCRIBE_SUBJECT: 'nunsub',
// TODO: replace this with a config option in the plugin config when we get there..
DISABLE_REQUIRE_EMAIL_VERIFICATIONS:
process.env.TALK_DISABLE_REQUIRE_EMAIL_VERIFICATIONS_NOTIFICATIONS ===
'TRUE',
};