mirror of
https://github.com/wassname/talk.git
synced 2026-06-29 23:09:26 +08:00
237 lines
7.1 KiB
JavaScript
237 lines
7.1 KiB
JavaScript
const { map, get, find, groupBy, property } = require('lodash');
|
|
const uuid = require('uuid/v4');
|
|
const { UNSUBSCRIBE_SUBJECT } = require('./config');
|
|
const { getOrganizationName, getNotificationBody } = require('./util');
|
|
const { checkDigests } = require('./digests');
|
|
|
|
// processNewNotifications will handle notifications that are collected after an
|
|
// event hook. These notifications will be batched by user and optionally
|
|
// queued for digesting or sent immediately depending on the user's settings.
|
|
const processNewNotifications = async (ctx, notifications) =>
|
|
Promise.all(
|
|
map(
|
|
// Group all the notifications so we don't have to redo the digest check
|
|
// multiple times for the same user.
|
|
groupBy(notifications, 'notification.userID'),
|
|
async (notifications, userID) => {
|
|
// Check to see if the user has digesting enabled.
|
|
const hasDigesting = await checkDigests(ctx, userID);
|
|
if (hasDigesting) {
|
|
// User has digesting enabled, queue the notifications to be sent
|
|
// at a later time.
|
|
return queueNotifications(ctx, userID, notifications);
|
|
}
|
|
|
|
// User does not have digesting enabled, send the messages the old
|
|
// way.
|
|
return sendNotificationsBatch(ctx, notifications);
|
|
}
|
|
)
|
|
);
|
|
|
|
// queueNotifications will queue the notifications onto the User.
|
|
const queueNotifications = async (ctx, userID, notifications) => {
|
|
// Mutate the notification payloads to what we can store in Mongo safely.
|
|
const digests = notifications.map(
|
|
({ notification, handler: { category } }) => ({ notification, category })
|
|
);
|
|
|
|
// Pull out some useful tools.
|
|
const { connectors: { models: { User } } } = ctx;
|
|
|
|
ctx.log.info(
|
|
{ notifications: notifications.length, userID },
|
|
'now queueing notifications for digesting'
|
|
);
|
|
|
|
// Push the digests into Mongo.
|
|
await User.update(
|
|
{ id: userID },
|
|
{ $push: { 'metadata.notifications.digests': { $each: digests } } }
|
|
);
|
|
};
|
|
|
|
// sendNotificationBatch will send a given set of notifications for several
|
|
// users that do not have digesting enabled.
|
|
const sendNotificationsBatch = async (ctx, notifications) => {
|
|
// Get the notification name for the subject.
|
|
const organizationName = await getOrganizationName(ctx);
|
|
if (!organizationName) {
|
|
ctx.log.error(
|
|
'could not send the notification, organization name not in settings'
|
|
);
|
|
return;
|
|
}
|
|
|
|
return Promise.all(
|
|
map(
|
|
notifications,
|
|
async ({ handler, notification: { userID, context } }) => {
|
|
const { connectors: { services: { I18n: { t } } } } = ctx;
|
|
const { category } = handler;
|
|
|
|
// Compose the subject for the email.
|
|
const subject = t(
|
|
`talk-plugin-notifications.categories.${category}.subject`,
|
|
organizationName
|
|
);
|
|
|
|
// Load the content into the comment.
|
|
const body = await getNotificationBody(ctx, handler, context);
|
|
|
|
// Send the email now.
|
|
return sendNotification(ctx, userID, subject, body);
|
|
}
|
|
)
|
|
);
|
|
};
|
|
|
|
// sendNotification will send the notification to the specified user with the
|
|
// given context.
|
|
const sendNotification = async (
|
|
ctx,
|
|
userID,
|
|
subject,
|
|
body,
|
|
template = 'notification'
|
|
) => {
|
|
const {
|
|
connectors: {
|
|
secrets: { jwt },
|
|
config: { JWT_ISSUER, JWT_AUDIENCE },
|
|
services: { Mailer },
|
|
},
|
|
} = ctx;
|
|
|
|
try {
|
|
const organizationName = await getOrganizationName(ctx);
|
|
if (!organizationName) {
|
|
ctx.log.error(
|
|
'could not send the notification, organization name not in settings'
|
|
);
|
|
return;
|
|
}
|
|
|
|
// unsubscribeToken is the token used to perform the one-click
|
|
// unsubscribe.
|
|
const unsubscribeToken = jwt.sign({
|
|
jti: uuid(),
|
|
iss: JWT_ISSUER,
|
|
aud: JWT_AUDIENCE,
|
|
sub: UNSUBSCRIBE_SUBJECT,
|
|
user: userID,
|
|
});
|
|
|
|
// Send the notification to the user.
|
|
const task = await Mailer.send({
|
|
template,
|
|
locals: { body, organizationName, unsubscribeToken },
|
|
subject,
|
|
user: userID,
|
|
});
|
|
|
|
ctx.log.info({ jobID: task.id }, 'sent the notification');
|
|
} catch (err) {
|
|
ctx.log.error(
|
|
{ err, message: err.message },
|
|
'could not send the notification, an error occurred'
|
|
);
|
|
return;
|
|
}
|
|
};
|
|
|
|
// 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 }, 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 USER_CONFIRMATION_QUERY = `
|
|
query CheckUserConfirmation($userID: ID!) {
|
|
user(id: $userID) {
|
|
profiles {
|
|
provider
|
|
... on LocalUserProfile {
|
|
confirmedAt
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
// filterVerifiedNotification checks to see if a user has a verified email
|
|
// address, and if they do, returns the notification payload again, otherwise,
|
|
// returns undefined.
|
|
const filterVerifiedNotification = ctx => async notification => {
|
|
// Grab the user that we're supposed to be sending the notification to.
|
|
const { notification: { userID } } = notification;
|
|
|
|
// Check their confirmed status. This should have already been hit by the
|
|
// loaders, so we shouldn't make any more database requests.
|
|
const { errors, data } = await ctx.graphql(USER_CONFIRMATION_QUERY, {
|
|
userID,
|
|
});
|
|
if (errors) {
|
|
ctx.log.error(
|
|
{ err: errors },
|
|
'could not query for user confirmation status'
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Get the first local profile from the user.
|
|
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;
|
|
}
|
|
|
|
// Pull out the confirmed status from the profile.
|
|
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;
|
|
};
|
|
|
|
// filterVerified performs filtering in a complicated way because we can't use
|
|
// Promise.all on a Array.prototype.filter call.
|
|
const filterVerified = async (ctx, notifications) => {
|
|
notifications = await Promise.all(
|
|
notifications.map(filterVerifiedNotification(ctx))
|
|
);
|
|
|
|
// This acts as a poor-mans identity filter to remove all falsy values.
|
|
return notifications.filter(property('notification'));
|
|
};
|
|
|
|
module.exports = {
|
|
processNewNotifications,
|
|
filterSuperseded,
|
|
filterVerified,
|
|
getNotificationBody,
|
|
sendNotification,
|
|
};
|