initial notification draft

- implements new notification manager using connect api
- first use case of plugins using the plugin api
This commit is contained in:
Wyatt Johnson
2018-02-09 18:17:25 -07:00
parent aaa1e0dbc3
commit 8b669a31c5
19 changed files with 570 additions and 0 deletions
@@ -0,0 +1,174 @@
const { get, groupBy, forEach } = require('lodash');
const debug = require('debug')('talk-plugin-notifications');
const { graphql } = require('graphql');
class NotificationManager {
constructor(context) {
this.context = context;
this.registry = [];
}
/**
* register will include the notification handlers on the manager.
*
* @param {Array<Object>} handlers notification handlers to register
*/
register(...handlers) {
this.registry.push(...handlers);
}
/**
* attach will setup the notifications by walking the registry and loading all
* the notification types onto the handler.
*
* @param {Object} broker the event emitter for the Talk events
*/
attach(broker) {
const events = groupBy(this.registry, 'event');
forEach(events, (handlers, event) => {
debug(
`will now notify the [${handlers
.map(({ category }) => category)
.join(', ')}] handlers when the '${event}' event is emitted`
);
broker.on(event, this.handle(handlers));
});
}
/**
* handle will wrap a notification handler and attach it to the notification
* stream system.
*
* @param {Object} handler a notification handler
*/
handle(handlers) {
return async (...args) =>
Promise.all(
handlers.map(async handler => {
// Grab the handler reference.
const { handle } = handler;
// Create a system context to send down.
const ctx = this.context.forSystem();
try {
// Attempt to create a notification out of it.
const notification = await handle(ctx, ...args);
if (!notification) {
return;
}
// Extract the notification details.
const { userID, date, context } = notification;
// Send the notification.
return this.send(ctx, userID, date, handler, context);
} catch (err) {
// TODO: handle error.
return;
}
})
);
}
/**
*
* @param {Object} ctx graph context
* @param {String} userID the user id for the user being sent the email
*/
async getEmail(ctx, userID) {
const { connectors: { graph: { schema } } } = ctx;
// Get the email for the user.
const reply = await graphql(
schema,
`
query GetUserEmail($userID: ID!) {
user(id: $userID) {
email
}
}
`,
{},
ctx,
{ userID }
);
if (reply.errors) {
throw reply.errors;
}
return get(reply, 'data.user.email', null);
}
async send(ctx, userID, date, handler, context) {
const {
connectors: { services: { Mailer, I18n: { t } } },
loaders: { Settings },
} = ctx;
const { category } = handler;
try {
// Get the settings.
const { organizationName = null } = await Settings.load(
'organizationName'
);
if (organizationName === null) {
// TODO: handle error
return;
}
// Get the User's email.
const to = await this.getEmail(ctx, userID);
if (!to) {
// TODO: handle error
return;
}
// 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 this.getBody(ctx, handler, context);
// Send the notification to the user.
const task = await Mailer.send({
template: 'notification',
locals: { body, organizationName },
subject,
to,
});
debug(`Sent the notification for Job.ID[${task.id}]`);
} catch (err) {
// TODO: print out the error.
return;
}
}
/**
* getBody will return the body for the notification payload.
*
* @param {Object} ctx the graph context
* @param {Object} handler the notification handler
* @param {Mixed} context the notification context
*/
async getBody(ctx, handler, context) {
const { connectors: { services: { I18n: { t } } } } = ctx;
const { category } = handler;
// Get the body replacement variables for the translation key.
const replacements = await handler.hydrate(ctx, category, context);
// Generate the body.
return t(
`talk-plugin-notifications.categories.${category}.body`,
...replacements
);
}
}
module.exports = NotificationManager;
@@ -0,0 +1,72 @@
const debug = require('debug')('talk-plugin-notifications');
const path = require('path');
const linkify = require('linkifyjs/html');
const NotificationManager = require('./NotificationManager');
module.exports = connectors => {
const {
graph: { subscriptions: { getBroker }, Context },
services: { Mailer, Plugins },
} = connectors;
// Setup the mailer. Other plugins registered before this one can replace the
// notification template by passing the same name + format for the template
// registration.
Mailer.templates.register(
path.join(__dirname, 'templates', 'notification.html.ejs'),
'notification',
'html'
);
Mailer.templates.register(
path.join(__dirname, 'templates', 'notification.txt.ejs'),
'notification',
'txt'
);
// Register the mail helpers. You can register your own helpers by calling
// this function in another plugin.
Mailer.registerHelpers({ linkify });
// Get the handle for the broker to attach to notifications.
const broker = getBroker();
// Create a NotificationManager to handle notifications.
const manager = new NotificationManager(Context);
// Get all the notification handlers. Additional plugins registered before
// this one can expose a `notifications` hook, that contains an array (or a
// single) notification handlers.
//
// A notification handler has the following form:
//
// {
// event // the graph event to listen for
// handle // the function called when the event is fired. It is called with
// // the (ctx, arg1, arg2, ...) where arg1, arg2 are args from the
// // event.
// category // the name representing the notification type (like 'reply')
// hydrate // returns the replacement parameters (in order!) to be used
// // in the translation.
// }
//
const notificationHandlers = Plugins.get('server', 'notifications').reduce(
(notificationHandlers, { plugin, notifications }) => {
debug(
`registered the ${
plugin.name
} plugin for notifications ${notifications.map(
({ category }) => category
)}`
);
notificationHandlers.push(...notifications);
return notificationHandlers;
},
[]
);
// Attach all the notification handlers.
manager.register(...notificationHandlers);
// Attach the broker to the manager so it can listen for the events.
manager.attach(broker);
};
@@ -0,0 +1,14 @@
const path = require('path');
const connect = require('./connect');
const typeDefs = require('./typeDefs');
const resolvers = require('./resolvers');
const router = require('./router');
const translations = path.join(__dirname, 'translations.yml');
module.exports = {
translations,
typeDefs,
resolvers,
connect,
router,
};
@@ -0,0 +1,14 @@
const { get } = require('lodash');
module.exports = {
User: {
notificationSettings(user, args, { user: currentUser }) {
if (
currentUser &&
(currentUser.id === user.id || currentUser.can('VIEW_USER_STATUS'))
) {
return get(user, 'metadata.notifications.settings');
}
},
},
};
@@ -0,0 +1,6 @@
module.exports = router => {
router.get('/account/unsubscribe-notifications', async (req, res) => {
// TODO: implement
res.json({ ok: true });
});
};
@@ -0,0 +1,3 @@
<p><%= linkify(body, {nl2br: true}) %></p>
<p><%= t('talk-plugin-notifications.templates.footer', organizationName) %></p>
<p><a href="<%= BASE_URL %>account/unsubscribe-notifications" target="_blank"><%= t('talk-plugin-notifications.templates.links.unsubscribe') %></a></p>
@@ -0,0 +1,7 @@
<%= body %>
<%= t('talk-plugin-notifications.templates.footer', organizationName) %>
<%= t('talk-plugin-notifications.templates.links.unsubscribe') %>
<%= BASE_URL %>account/unsubscribe-notifications
@@ -0,0 +1,6 @@
en:
talk-plugin-notifications:
templates:
footer: "You received this notification because you are a commenter on {0} and you opted in to receive notifications."
links:
unsubscribe: "Unsubscribe from comment notifications"
@@ -0,0 +1,7 @@
type NotificationSettings {
onReply: Boolean!
}
type User {
notificationSettings: NotificationSettings
}
@@ -0,0 +1,7 @@
const fs = require('fs');
const path = require('path');
module.exports = fs.readFileSync(
path.join(__dirname, 'typeDefs.graphql'),
'utf8'
);