diff --git a/.gitignore b/.gitignore
index 9899ad4..a3583f9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
.idea
node_modules
*.log
+log.txt
.DS_Store
dist
diff --git a/README.md b/README.md
index 73df890..c4af032 100644
--- a/README.md
+++ b/README.md
@@ -6,46 +6,75 @@
- Heroku Toolbelt https://toolbelt.heroku.com
- git
- PostgreSQL 9.5
-
+
## Configuration
+
+### Notification server
Configuration for the notification server is at `config/default.js`.
The following parameters can be set in config files or in env variables:
-- LOG_LEVEL: the log level
-- PORT: the notification server port
-- AUTH_SECRET: TC auth secret
-- VALID_ISSUERS: TC auth valid issuers
-- jwksUri: TC auth JWKS URI
-- DATABASE_URL: URI to PostgreSQL database
-- DATABASE_OPTIONS: database connection options
-- KAFKA_URL: comma separated Kafka hosts
-- KAFKA_TOPIC_IGNORE_PREFIX: ignore this prefix for topics in the Kafka
-- KAFKA_GROUP_ID: Kafka consumer group id
-- KAFKA_CLIENT_CERT: Kafka connection certificate, optional;
- if not provided, then SSL connection is not used, direct insecure connection is used;
- if provided, it can be either path to certificate file or certificate content
-- KAFKA_CLIENT_CERT_KEY: Kafka connection private key, optional;
- if not provided, then SSL connection is not used, direct insecure connection is used;
- if provided, it can be either path to private key file or private key content
-- BUS_API_BASE_URL: Bus API url
-- REPLY_EMAIL_PREFIX: prefix of the genereated reply email address
-- REPLY_EMAIL_DOMAIN: email domain
-- DEFAULT_REPLY_EMAIL: default reply to email address, for example no-reply@topcoder.com
-- MENTION_EMAIL: recipient email used for email.project.post.mention event
-
+- **General**
+ - `LOG_LEVEL`: the log level
+ - `PORT`: the notification server port
+ - `DATABASE_URL`: URI to PostgreSQL database
+ - `DATABASE_OPTIONS`: database connection options
+- **JWT authentication**
+ - `AUTH_SECRET`: TC auth secret
+ - `VALID_ISSUERS`: TC auth valid issuers
+ - `JWKS_URI`: TC auth JWKS URI (need only for local deployment)
+- **KAFKA**
+ - `KAFKA_URL`: comma separated Kafka hosts
+ - `KAFKA_TOPIC_IGNORE_PREFIX`: ignore this prefix for topics in the Kafka
+ - `KAFKA_GROUP_ID`: Kafka consumer group id
+ - `KAFKA_CLIENT_CERT`: Kafka connection certificate, optional;
+ if not provided, then SSL connection is not used, direct insecure connection is used;
+ if provided, it can be either path to certificate file or certificate content
+ - `KAFKA_CLIENT_CERT_KEY`: Kafka connection private key, optional;
+ if not provided, then SSL connection is not used, direct insecure connection is used;
+ if provided, it can be either path to private key file or private key content
+- **Topcoder API**
+ - `TC_API_V5_BASE_URL`: the TopCoder API V5 base URL
+- **Notifications API**
+ - `API_CONTEXT_PATH`: path to serve API on
+- **Machine to machine auth0 token**
+ - `AUTH0_URL`: auth0 URL
+ - `AUTH0_AUDIENCE`: auth0 audience
+ - `TOKEN_CACHE_TIME`: time period of the cached token
+ - `AUTH0_CLIENT_ID`: auth0 client id
+ - `AUTH0_CLIENT_SECRET`: auth0 client secret
+
+### Connect notification server
Configuration for the connect notification server is at `connect/config.js`.
The following parameters can be set in config files or in env variables:
-- TC_API_V3_BASE_URL: the TopCoder API V3 base URL
-- TC_API_V4_BASE_URL: the TopCoder API V4 base URL
-- TC_ADMIN_TOKEN: the admin token to access TopCoder API - same for V3 and V4
- Also it has probably temporary variables of TopCoder role ids for 'Connect Manager', 'Connect Copilot' and 'administrator':
-- CONNECT_MANAGER_ROLE_ID: 8,
-- CONNECT_COPILOT_ROLE_ID: 4,
-- ADMINISTRATOR_ROLE_ID: 1
- Provided values are for development backend. For production backend they may be different.
- These variables are currently being used to retrieve above role members using API V3 `/roles` endpoint. As soon as this endpoint is replaced with more suitable one, these variables has to be removed if no need anymore.
-- TCWEBSERVICE_ID - id of the BOT user which creates post with various events in discussions
-
+- **Topcoder API**
+ - `TC_API_V3_BASE_URL`: the TopCoder API V3 base URL
+ - `TC_API_V4_BASE_URL`: the TopCoder API V4 base URL
+ - `MESSAGE_API_BASE_URL`: the TopCoder message service API base URL
+ - `TC_ADMIN_TOKEN`: the admin token to access TopCoder API - same for V3 and V4
+- **Topcder specific**
+ Also it has probably temporary variables of TopCoder role ids for 'Connect Manager', 'Connect Copilot' and 'administrator':
+ - `CONNECT_MANAGER_ROLE_ID`: 8,
+ - `CONNECT_COPILOT_ROLE_ID`: 4,
+ - `ADMINISTRATOR_ROLE_ID`: 1
+ Provided values are for development backend. For production backend they may be different.
+ These variables are currently being used to retrieve above role members using API V3 `/roles` endpoint. As soon as this endpoint is replaced with more suitable one, these variables has to be removed if no need anymore.
+ - `TCWEBSERVICE_ID` - id of the BOT user which creates post with various events in discussions
+- **Machine to machine auth0 token**
+ - `AUTH0_URL`: auth0 URL
+ - `AUTH0_AUDIENCE`: auth0 audience
+ - `TOKEN_CACHE_TIME`: time period of the cached token
+ - `AUTH0_CLIENT_ID`: auth0 client id
+ - `AUTH0_CLIENT_SECRET`: auth0 client secret
+- **Email notification service**
+ - `ENV`: environment variable (used to generate reply emails)
+ - `AUTH_SECRET`: auth secret (used to sign reply emails)
+ - `ENABLE_EMAILS`: if email service has to be enabled
+ - `ENABLE_DEV_MODE`: send all emails to the `DEV_MODE_EMAIL` email address
+ - `DEV_MODE_EMAIL`: address to send all email when `ENABLE_DEV_MODE` is enabled
+ - `MENTION_EMAIL`: recipient email used for `notifications.action.email.connect.project.post.mention` event
+ - `REPLY_EMAIL_PREFIX`: prefix of the genereated reply email address
+ - `REPLY_EMAIL_DOMAIN`: email domain
+ - `DEFAULT_REPLY_EMAIL`: default reply to email address, for example no-reply@topcoder.com
Note that the above two configuration are separate because the common notification server config
will be deployed to a NPM package, the connect notification server will use that NPM package,
@@ -89,6 +118,11 @@ In case it expires, you may get a new token in this way:
- `TC_API_V3_BASE_URL=https://api.topcoder-dev.com/v3`
- `TC_ADMIN_TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoic3VzZXIxIiwiZXhwIjoxNTEzNDAxMjU4LCJ1c2VySWQiOiI0MDE1MzkzOCIsImlhdCI6MTUwOTYzNzYzOSwiZW1haWwiOiJtdHdvbWV5QGJlYWtzdGFyLmNvbSIsImp0aSI6IjIzZTE2YjA2LWM1NGItNDNkNS1iY2E2LTg0ZGJiN2JiNDA0NyJ9.REds35fdBvY7CMDGGFyT_tOD7DxGimFfVzIyEy9YA0Y` or follow section **TC API Admin Token** to obtain a new one if expired
- `KAFKA_URL`, `KAFKA_CLIENT_CERT` and `KAFKA_CLIENT_CERT_KEY` get from [tc-bus-api readme](https://github.com/topcoder-platform/tc-bus-api/tree/dev)
+- if you are willing to use notifications API which is hosted by the notifications server locally, you will need to use some patched `tc-core-library-js` module, which skips verification of user token. Because we don't know Topcoder `AUTH_SECRET` locally. So you can install this fork:
+ ```
+ npm i https://github.com/maxceem/tc-core-library-js/tree/skip-validation
+ ```
+ **WARNING** do not push package.json with this dependency as it skips users token validation.
- start local PostgreSQL db, create an empty database, update the config/default.js DATABASE_URL param to point to the db
- install dependencies `npm i`
- run code lint check `npm run lint`
@@ -127,5 +161,5 @@ In case it expires, you may get a new token in this way:
## Swagger
Swagger API definition is provided at `docs/swagger_api.yaml`,
-you may check it at `http://editor.swagger.io`.
+you may check it at `http://editor.swagger.io`.
diff --git a/config/default.js b/config/default.js
index da9f5a3..73bf19b 100644
--- a/config/default.js
+++ b/config/default.js
@@ -2,10 +2,8 @@
* The configuration file.
*/
module.exports = {
- ENV: process.env.ENV,
LOG_LEVEL: process.env.LOG_LEVEL,
PORT: process.env.PORT,
- AUTH_SECRET: process.env.authSecret,
DATABASE_URL: process.env.DATABASE_URL,
DATABASE_OPTIONS: {
dialect: 'postgres',
@@ -19,7 +17,12 @@ module.exports = {
},
},
+ AUTH_SECRET: process.env.authSecret,
VALID_ISSUERS: process.env.validIssuers ? process.env.validIssuers.replace(/\\"/g, '') : null,
+ // keep it here for dev purposes, it's only needed by modified version of tc-core-library-js
+ // which skips token validation when locally deployed
+ JWKS_URI: process.env.jwksUri,
+
KAFKA_URL: process.env.KAFKA_URL,
KAFKA_TOPIC_IGNORE_PREFIX: process.env.KAFKA_TOPIC_IGNORE_PREFIX,
KAFKA_GROUP_ID: process.env.KAFKA_GROUP_ID,
@@ -27,19 +30,7 @@ module.exports = {
KAFKA_CLIENT_CERT_KEY: process.env.KAFKA_CLIENT_CERT_KEY ?
process.env.KAFKA_CLIENT_CERT_KEY.replace('\\n', '\n') : null,
- MENTION_EMAIL: process.env.MENTION_EMAIL,
- REPLY_EMAIL_PREFIX: process.env.REPLY_EMAIL_PREFIX,
- REPLY_EMAIL_DOMAIN: process.env.REPLY_EMAIL_DOMAIN,
-
- TC_ADMIN_TOKEN: process.env.TC_ADMIN_TOKEN,
- TC_API_BASE_URL: process.env.TC_API_BASE_URL || 'https://api.topcoder-dev.com',
- TC_API_V3_BASE_URL: process.env.TC_API_V3_BASE_URL || 'https://api.topcoder-dev.com/v3',
- TC_API_V4_BASE_URL: process.env.TC_API_V4_BASE_URL || 'https://api.topcoder-dev.com/v4',
TC_API_V5_BASE_URL: process.env.TC_API_V5_BASE_URL || 'https://api.topcoder-dev.com/v5',
- MESSAGE_API_BASE_URL: process.env.MESSAGE_API_BASE_URL || 'https://api.topcoder-dev.com/v5',
- ENABLE_EMAILS: process.env.ENABLE_EMAILS || true,
- ENABLE_DEV_MODE: process.env.ENABLE_DEV_MODE || true,
- DEV_MODE_EMAIL: process.env.DEV_MODE_EMAIL,
API_CONTEXT_PATH: process.env.API_CONTEXT_PATH || '/v5/notifications',
// Configuration for generating machine to machine auth0 token.
diff --git a/connect/config.js b/connect/config.js
index 52b9785..cbff96f 100644
--- a/connect/config.js
+++ b/connect/config.js
@@ -3,10 +3,10 @@
*/
module.exports = {
+ // TC API related variables
TC_API_V3_BASE_URL: process.env.TC_API_V3_BASE_URL || 'https://api.topcoder-dev.com/v3',
TC_API_V4_BASE_URL: process.env.TC_API_V4_BASE_URL || 'https://api.topcoder-dev.com/v4',
- MESSAGE_API_BASE_URL: process.env.MESSAGE_API_BASE_URL || 'https://api.topcoder-dev.com/v4',
- // eslint-disable-next-line max-len
+ MESSAGE_API_BASE_URL: process.env.MESSAGE_API_BASE_URL || 'https://api.topcoder-dev.com/v5',
TC_ADMIN_TOKEN: process.env.TC_ADMIN_TOKEN,
// Probably temporary variables for TopCoder role ids for 'Connect Manager', 'Connect Copilot' and 'administrator'
@@ -16,8 +16,28 @@ module.exports = {
CONNECT_MANAGER_ROLE_ID: 8,
CONNECT_COPILOT_ROLE_ID: 4,
ADMINISTRATOR_ROLE_ID: 1,
-
// id of the BOT user which creates post with various events in discussions
TCWEBSERVICE_ID: process.env.TCWEBSERVICE_ID || '22838965',
+ // Configuration for generating machine to machine auth0 token.
+ // The token will be used for calling another internal API.
+ AUTH0_URL: process.env.AUTH0_URL,
+ AUTH0_AUDIENCE: process.env.AUTH0_AUDIENCE,
+ // The token will be cached.
+ // We define the time period of the cached token.
+ TOKEN_CACHE_TIME: process.env.TOKEN_CACHE_TIME || 86400000,
+ AUTH0_CLIENT_ID: process.env.AUTH0_CLIENT_ID,
+ AUTH0_CLIENT_SECRET: process.env.AUTH0_CLIENT_SECRET,
+
+ // email notification service related variables
+ ENV: process.env.ENV,
+ AUTH_SECRET: process.env.authSecret,
+ ENABLE_EMAILS: process.env.ENABLE_EMAILS || true,
+ ENABLE_DEV_MODE: process.env.ENABLE_DEV_MODE || true,
+ DEV_MODE_EMAIL: process.env.DEV_MODE_EMAIL,
+ MENTION_EMAIL: process.env.MENTION_EMAIL,
+ REPLY_EMAIL_PREFIX: process.env.REPLY_EMAIL_PREFIX,
+ REPLY_EMAIL_DOMAIN: process.env.REPLY_EMAIL_DOMAIN,
+ REPLY_EMAIL_FROM: process.env.REPLY_EMAIL_FROM,
+ DEFAULT_REPLY_EMAIL: process.env.DEFAULT_REPLY_EMAIL,
};
diff --git a/connect/connectNotificationServer.js b/connect/connectNotificationServer.js
index a5678ef..289ef1c 100644
--- a/connect/connectNotificationServer.js
+++ b/connect/connectNotificationServer.js
@@ -9,11 +9,12 @@ const config = require('./config');
const notificationServer = require('../index');
const _ = require('lodash');
const service = require('./service');
-const { BUS_API_EVENT } = require('../src/constants')
+const { BUS_API_EVENT } = require('./constants');
const EVENTS = require('./events-config').EVENTS;
const TOPCODER_ROLE_RULES = require('./events-config').TOPCODER_ROLE_RULES;
const PROJECT_ROLE_RULES = require('./events-config').PROJECT_ROLE_RULES;
const PROJECT_ROLE_OWNER = require('./events-config').PROJECT_ROLE_OWNER;
+const emailNotificationServiceHandler = require('./notificationServices/email').handler;
/**
* Get TopCoder members notifications
@@ -62,14 +63,13 @@ const getTopCoderMembersNotifications = (eventConfig) => {
* @return {Promise} resolves to a list of notifications
*/
const getNotificationsForMentionedUser = (eventConfig, content) => {
- if (!eventConfig.toMentionedUsers) {
+ if (!eventConfig.toMentionedUsers || !content) {
return Promise.resolve([]);
}
let notifications = [];
// eslint-disable-next-line
- const regexUserHandle = /title=\"@([a-zA-Z0-9-_.{}\[\]]+)\"|\[.*\]\(.*\"\@(.*)\"\)/g;
- const handles = [];
+ const regexUserHandle = /title=\"@([a-zA-Z0-9-_.{}\[\]]+)\"|\[.*?\]\(.*?\"\@(.*?)\"\)/g;
let matches = regexUserHandle.exec(content);
while (matches) {
const handle = matches[1] ? matches[1].toString() : matches[2].toString();
@@ -81,18 +81,22 @@ const getNotificationsForMentionedUser = (eventConfig, content) => {
},
});
matches = regexUserHandle.exec(content);
- handles.push(handle);
}
// only one per userHandle
notifications = _.uniqBy(notifications, 'userHandle');
return new Promise((resolve) => {
- service.getUsersByHandle(handles).then((users) => {
- _.map(notifications, (notification) => {
- notification.userId = _.find(users, { handle: notification.userHandle }).userId.toString();
+ const handles = _.map(notifications, 'userHandle');
+ if (handles.length > 0) {
+ service.getUsersByHandle(handles).then((users) => {
+ _.forEach(notifications, (notification) => {
+ notification.userId = _.find(users, { handle: notification.userHandle }).userId.toString();
+ });
+ resolve(notifications);
});
- resolve(notifications);
- });
+ } else {
+ resolve([]);
+ }
});
};
@@ -243,6 +247,7 @@ const excludeNotifications = (notifications, eventConfig, message, data) => {
return Promise.all([
getNotificationsForTopicStarter(excludeEventConfig, message.topicId),
getNotificationsForUserId(excludeEventConfig, message.userId),
+ getNotificationsForMentionedUser(eventConfig, message.postContent),
getProjectMembersNotifications(excludeEventConfig, project),
getTopCoderMembersNotifications(excludeEventConfig),
]).then((notificationsPerSource) => (
@@ -296,7 +301,7 @@ const handler = (topic, message, callback) => {
// - check that event has everything required or throw error
getNotificationsForTopicStarter(eventConfig, message.topicId),
getNotificationsForUserId(eventConfig, message.userId),
- message.postContent ? getNotificationsForMentionedUser(eventConfig, message.postContent) : Promise.resolve([]),
+ getNotificationsForMentionedUser(eventConfig, message.postContent),
getProjectMembersNotifications(eventConfig, project),
getTopCoderMembersNotifications(eventConfig),
]).then((notificationsPerSource) => (
@@ -344,6 +349,11 @@ EVENTS.forEach(eventConfig => {
notificationServer.addTopicHandler(eventConfig.type, handler);
});
+// add notification service handlers
+if (config.ENABLE_EMAILS) {
+ notificationServer.addNotificationServiceHandler(emailNotificationServiceHandler);
+}
+
// init database, it will clear and re-create all tables
notificationServer
.initDatabase()
diff --git a/src/constants.js b/connect/constants.js
similarity index 63%
rename from src/constants.js
rename to connect/constants.js
index fda41c5..3936d31 100644
--- a/src/constants.js
+++ b/connect/constants.js
@@ -1,6 +1,17 @@
module.exports = {
+ // periods of time in cron format (node-cron)
+ SCHEDULED_EVENT_PERIOD: {
+ every10minutes: '*/10 * * * *',
+ hourly: '0 * * * *',
+ daily: '0 7 * * *', // every day at 7am
+ weekly: '0 7 * * 6', // every Saturday at 7am
+ },
+
+ // email service id for settings
+ SETTINGS_EMAIL_SERVICE_ID: 'email',
+
BUS_API_EVENT: {
- CONNECT : {
+ CONNECT: {
TOPIC_CREATED: 'notifications.connect.project.topic.created',
TOPIC_DELETED: 'notifications.connect.project.topic.deleted',
POST_CREATED: 'notifications.connect.project.post.created',
@@ -8,10 +19,11 @@ module.exports = {
POST_DELETED: 'notifications.connect.project.post.deleted',
MENTIONED_IN_POST: 'notifications.connect.project.post.mention',
},
- EMAIL : {
+ EMAIL: {
TOPIC_CREATED: 'notifications.action.email.connect.project.topic.created',
POST_CREATED: 'notifications.action.email.connect.project.post.created',
MENTIONED_IN_POST: 'notifications.action.email.connect.project.post.mention',
+ BUNDLED: 'notifications.action.email.connect.project.bundled',
},
},
};
diff --git a/connect/events-config.js b/connect/events-config.js
index 511ef0d..cb2ee33 100644
--- a/connect/events-config.js
+++ b/connect/events-config.js
@@ -109,6 +109,7 @@ const EVENTS = [
type: 'notifications.connect.project.topic.created',
version: 2,
projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, PROJECT_ROLE_MEMBER],
+ toMentionedUsers: true,
}, {
type: 'notifications.connect.project.post.created',
version: 2,
diff --git a/connect/helpers.js b/connect/helpers.js
new file mode 100644
index 0000000..3092323
--- /dev/null
+++ b/connect/helpers.js
@@ -0,0 +1,48 @@
+/**
+ * Helper functions
+ */
+const Remarkable = require('remarkable');
+
+/**
+ * Convert markdown into raw draftjs state
+ *
+ * @param {String} markdown - markdown to convert into raw draftjs object
+ * @param {Object} options - optional additional data
+ *
+ * @return {Object} ContentState
+**/
+const markdownToHTML = (markdown) => {
+ const md = new Remarkable('full', {
+ html: true,
+ linkify: true,
+ // typographer: true,
+ });
+
+ // Replace the BBCode [u][/u] to markdown '++' for underline style
+ const _markdown = markdown.replace(new RegExp('\\[/?u\\]', 'g'), '++');
+
+ // remarkable js takes markdown and makes it an array of style objects for us to easily parse
+ return md.render(_markdown, {});
+};
+
+/**
+ * Helper method to clean up the provided email address for deducing the final address that matters for
+ * the delivery of the email i.e. removing any non standard parts in the email address e.g. getting rid
+ * of anything after + sign in the local part of the email.
+ *
+ * @param {String} email email address to be sanitized
+ *
+ * @returns {String} sanitized email
+ */
+const sanitizeEmail = (email) => {
+ if (email) {
+ return email.substring(0, email.indexOf('+') !== -1 ? email.indexOf('+') : email.indexOf('@'))
+ + email.substring(email.indexOf('@'));
+ }
+ return '';
+};
+
+module.exports = {
+ markdownToHTML,
+ sanitizeEmail,
+};
diff --git a/connect/notificationServices/email.js b/connect/notificationServices/email.js
new file mode 100644
index 0000000..6a3d445
--- /dev/null
+++ b/connect/notificationServices/email.js
@@ -0,0 +1,226 @@
+/**
+ * Email notification service
+ */
+const _ = require('lodash');
+const jwt = require('jsonwebtoken');
+const co = require('co');
+const { logger, busService, eventScheduler, notificationService } = require('../../index');
+const { createEventScheduler, SCHEDULED_EVENT_STATUS } = eventScheduler;
+
+const config = require('../config');
+const { BUS_API_EVENT, SCHEDULED_EVENT_PERIOD, SETTINGS_EMAIL_SERVICE_ID } = require('../constants');
+const helpers = require('../helpers');
+const service = require('../service');
+
+/**
+ * Handles due events which are passed by scheduler
+ *
+ * Groups events by users, bundles them and sends using Bus API
+ *
+ * @param {Array} events due events
+ * @param {Function} setEventsStatus function which sets statuses of processed events
+ */
+function handleScheduledEvents(events, setEventsStatus) {
+ // do nothing if there are no due events
+ if (events.length === 0) {
+ return;
+ }
+
+ const eventsByUsers = _.groupBy(events, 'userId');
+
+ _.values(eventsByUsers).forEach((userEvents) => {
+ // clone data to avoid circular object
+ // we use common data from the first event
+ const eventMessage = _.clone(userEvents[0].data);
+
+ // update common values for bundled email
+ eventMessage.replyTo = config.DEFAULT_REPLY_EMAIL;
+ eventMessage.cc = [];
+ eventMessage.from = {
+ name: config.DEFAULT_REPLY_EMAIL,
+ email: config.DEFAULT_REPLY_EMAIL,
+ };
+
+ // TODO: consider using templating engine to format the bundle email
+ // until there is Sendgrid support for loops in email templates
+ let emailBody = '
Your recent updates on Topcoder Connect
'; + const eventsByTopics = _.groupBy(userEvents, 'data.data.topicId'); + _.values(eventsByTopics).forEach((topicEvents) => { + emailBody += `${topicEvents[0].data.data.topicTitle}
`; + topicEvents.forEach(topicEvent => { + emailBody += `By ${topicEvent.data.data.name} at ${topicEvent.data.data.date}
`; + emailBody += `
${topicEvent.data.data.post}
`; + }); + // eslint-disable-next-line + emailBody += `
`;
+ emailBody += '';
+ });
+
+ // data property we define as an array of data from each individual event
+ eventMessage.data = { notificationsHTML: emailBody };
+
+ busService.postEvent({
+ topic: BUS_API_EVENT.EMAIL.BUNDLED,
+ originator: 'tc-notifications',
+ timestamp: (new Date()).toISOString(),
+ 'mime-type': 'application/json',
+ payload: eventMessage,
+ }).then(() => {
+ logger.info(`Successfully sent ${BUS_API_EVENT.EMAIL.BUNDLED} event`
+ + ` with body ${JSON.stringify(eventMessage)} to bus api`);
+
+ setEventsStatus(userEvents, SCHEDULED_EVENT_STATUS.COMPLETED);
+ }).catch(() => {
+ logger.error(`Failed to send ${BUS_API_EVENT.EMAIL.BUNDLED} event`
+ + ` with body ${JSON.stringify(eventMessage)} to bus api`);
+
+ setEventsStatus(userEvents, SCHEDULED_EVENT_STATUS.FAILED);
+ });
+ });
+}
+
+// create and initialize scheduler
+const scheduler = createEventScheduler(
+ BUS_API_EVENT.EMAIL.BUNDLED,
+ SCHEDULED_EVENT_PERIOD,
+ handleScheduledEvents
+);
+
+/**
+ * Handler function which sends notification using email
+ *
+ * Depend on user settings it sends email immediately
+ * or bundles notifications for some period and send them together
+ *
+ * @param {String} topicName topic name (event type)
+ * @param {Object} messageJSON message raw JSON
+ * @param {Object} notification pre-processed notification object
+ */
+function handler(topicName, messageJSON, notification) {
+ // if it's interesting event, create email event and send to bus api
+ const notificationType = notification.newType || topicName;
+ logger.debug(`checking ${notificationType} notification ${JSON.stringify(notification)}`);
+ let eventType;
+
+ if (notificationType === BUS_API_EVENT.CONNECT.TOPIC_CREATED) {
+ eventType = BUS_API_EVENT.EMAIL.TOPIC_CREATED;
+ } else if (notificationType === BUS_API_EVENT.CONNECT.POST_CREATED) {
+ eventType = BUS_API_EVENT.EMAIL.POST_CREATED;
+ } else if (notificationType === BUS_API_EVENT.CONNECT.MENTIONED_IN_POST) {
+ eventType = BUS_API_EVENT.EMAIL.MENTIONED_IN_POST;
+ }
+
+ if (!!eventType) {
+ return co(function* () {
+ const settings = yield notificationService.getSettings(notification.userId);
+
+ // if email notification is explicitly disabled for current notification type do nothing
+ // by default we treat all notification types enabled
+ if (settings.notifications[notificationType]
+ && settings.notifications[notificationType][SETTINGS_EMAIL_SERVICE_ID]
+ && settings.notifications[notificationType][SETTINGS_EMAIL_SERVICE_ID].enabled === 'no'
+ ) {
+ logger.verbose(`Notification '${notificationType}' won't be sent by '${SETTINGS_EMAIL_SERVICE_ID}'`
+ + ` service to the userId '${notification.userId}' due to his notification settings.`);
+ return;
+ }
+
+ const topicId = parseInt(messageJSON.topicId, 10);
+ const postId = messageJSON.postId ? parseInt(messageJSON.postId, 10) : null;
+
+ const users = yield service.getUsersById([notification.userId]);
+ logger.debug(`got users ${JSON.stringify(users)}`);
+
+ const connectTopic = yield service.getTopic(topicId, logger);
+ logger.debug(`got topic ${JSON.stringify(connectTopic)}`);
+
+ const user = users[0];
+ let userEmail = user.email;
+ if (config.ENABLE_DEV_MODE === 'true') {
+ userEmail = config.DEV_MODE_EMAIL;
+ }
+ const recipients = [userEmail];
+ const cc = [];
+ if (eventType === BUS_API_EVENT.EMAIL.MENTIONED_IN_POST) {
+ cc.push(config.MENTION_EMAIL);
+ }
+ const categories = [`${config.ENV}:${eventType}`.toLowerCase()];
+
+ // get jwt token then encode it with base64
+ const body = {
+ userId: parseInt(notification.userId, 10),
+ topicId,
+ userEmail: helpers.sanitizeEmail(user.email),
+ };
+ logger.debug('body', body);
+ logger.debug(`body for generating token: ${JSON.stringify(body)}`);
+ logger.debug(`AUTH_SECRET: ${config.AUTH_SECRET.substring(-5)}`);
+ const token = jwt.sign(body, config.AUTH_SECRET, { noTimestamp: true }).split('.')[2];
+ logger.debug(`token: ${token}`);
+
+ const replyTo = `${config.REPLY_EMAIL_PREFIX}+${topicId}/${token}@${config.REPLY_EMAIL_DOMAIN}`;
+
+ const eventMessage = {
+ data: {
+ name: user.firstName + ' ' + user.lastName,
+ handle: user.handle,
+ topicTitle: connectTopic.title || '',
+ post: helpers.markdownToHTML(messageJSON.postContent),
+ date: (new Date()).toISOString(),
+ projectName: notification.contents.projectName,
+ projectId: messageJSON.projectId,
+ topicId,
+ postId,
+ authorHandle: notification.contents.userHandle,
+ },
+ recipients,
+ replyTo,
+ cc,
+ from: {
+ name: notification.contents.userHandle,
+ email: config.REPLY_EMAIL_FROM,
+ },
+ categories,
+ };
+
+ // if notifications has to be bundled
+ const bundlePeriod = settings.services[SETTINGS_EMAIL_SERVICE_ID]
+ && settings.services[SETTINGS_EMAIL_SERVICE_ID].bundlePeriod;
+
+ if (bundlePeriod) {
+ if (!SCHEDULED_EVENT_PERIOD[bundlePeriod]) {
+ throw new Error(`User's '${notification.userId}' setting for service`
+ + ` '${SETTINGS_EMAIL_SERVICE_ID}' option 'bundlePeriod' has unsupported value '${bundlePeriod}'.`);
+ }
+
+ // schedule event to be send later
+ scheduler.addEvent({
+ data: eventMessage,
+ period: bundlePeriod,
+ userId: notification.userId,
+ eventType,
+ reference: 'topic',
+ referenceId: topicId,
+ });
+ } else {
+ // send event to bus api
+ return busService.postEvent({
+ topic: eventType,
+ originator: 'tc-notifications',
+ timestamp: (new Date()).toISOString(),
+ 'mime-type': 'application/json',
+ payload: eventMessage,
+ }).then(() => {
+ logger.info(`Successfully sent ${eventType} event with body ${JSON.stringify(eventMessage)} to bus api`);
+ });
+ }
+ });
+ }
+
+ // if no need to send emails, return resolved promise for consistency
+ return Promise.resolve();
+}
+
+module.exports = {
+ handler,
+};
diff --git a/connect/service.js b/connect/service.js
index 03fffab..c0cb0ce 100644
--- a/connect/service.js
+++ b/connect/service.js
@@ -1,9 +1,11 @@
/**
* Service to get data from TopCoder API
*/
+/* global M2m */
const request = require('superagent');
-const config = require('config');
+const config = require('./config');
const _ = require('lodash');
+const { logger } = require('../index');
/**
* Get project details
@@ -45,7 +47,7 @@ const getRoleMembers = (roleId) => request
.set('authorization', `Bearer ${config.TC_ADMIN_TOKEN}`)
.then((res) => {
if (!_.get(res, 'body.result.success')) {
- throw new Error(`Failed to get role memebrs of role id: ${roleId}`);
+ throw new Error(`Failed to get role members of role id: ${roleId}`);
}
const members = _.get(res, 'body.result.content.subjects');
@@ -54,7 +56,7 @@ const getRoleMembers = (roleId) => request
}).catch((err) => {
const errorDetails = _.get(err, 'response.body.result.content.message');
throw new Error(
- `Failed to get role memebrs of role id: ${roleId}.` +
+ `Failed to get role members of role id: ${roleId}.` +
(errorDetails ? ' Server response: ' + errorDetails : '')
);
});
@@ -69,6 +71,10 @@ const getRoleMembers = (roleId) => request
const getUsersById = (ids) => {
const query = _.map(ids, (id) => 'userId:' + id).join(' OR ');
return M2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET)
+ .catch((err) => {
+ err.message = 'Error generating m2m token: ' + err.message;
+ throw err;
+ })
.then((token) => {
if (!token && config.TC_ADMIN_TOKEN) token = config.TC_ADMIN_TOKEN;
@@ -78,22 +84,19 @@ const getUsersById = (ids) => {
.set('authorization', `Bearer ${token}`)
.then((res) => {
if (!_.get(res, 'body.result.success')) {
- throw new Error(`Failed to get users by id: ${ids}`);
+ throw new Error(`Failed to get users by ids: ${ids}`);
}
const users = _.get(res, 'body.result.content');
return users;
}).catch((err) => {
- const errorDetails = _.get(err, 'response.body.result.content.message');
+ const errorDetails = _.get(err, 'response.body.result.content.message')
+ || `Status code: ${err.response.statusCode}`;
throw new Error(
`Failed to get users by ids: ${ids}.` +
(errorDetails ? ' Server response: ' + errorDetails : '')
);
});
- })
- .catch((err) => {
- err.message = 'Error generating m2m token: ' + err.message;
- throw err;
});
};
@@ -107,6 +110,10 @@ const getUsersById = (ids) => {
const getUsersByHandle = (handles) => {
const query = _.map(handles, (handle) => 'handle:' + handle).join(' OR ');
return M2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET)
+ .catch((err) => {
+ err.message = 'Error generating m2m token: ' + err.message;
+ throw err;
+ })
.then((token) => {
if (!token && config.TC_ADMIN_TOKEN) token = config.TC_ADMIN_TOKEN;
@@ -122,16 +129,13 @@ const getUsersByHandle = (handles) => {
return users;
}).catch((err) => {
- const errorDetails = _.get(err, 'response.body.result.content.message');
+ const errorDetails = _.get(err, 'response.body.result.content.message')
+ || `Status code: ${err.response.statusCode}`;
throw new Error(
`Failed to get users by handles: ${handles}.` +
(errorDetails ? ' Server response: ' + errorDetails : '')
);
});
- })
- .catch((err) => {
- err.message = 'Error generating m2m token: ' + err.message;
- throw err;
});
};
@@ -142,7 +146,7 @@ const getUsersByHandle = (handles) => {
*
* @return {Promise} promise resolved to topic details
*/
-const getTopic = (topicId, logger) => request
+const getTopic = (topicId) => request
.get(`${config.MESSAGE_API_BASE_URL}/topics/${topicId}/read`)
.set('accept', 'application/json')
.set('authorization', `Bearer ${config.TC_ADMIN_TOKEN}`)
diff --git a/docs/tc-notification-server-api-heroku-env.postman_environment.json b/docs/tc-notification-server-api-heroku-env.postman_environment.json
index 04005e3..d11474e 100644
--- a/docs/tc-notification-server-api-heroku-env.postman_environment.json
+++ b/docs/tc-notification-server-api-heroku-env.postman_environment.json
@@ -1,28 +1,27 @@
-{
- "id": "30bb1f5b-1185-2a81-b1c9-b95bf3c89140",
- "name": "tc-notification-server-api-heroku-env",
- "values": [
- {
- "enabled": true,
- "key": "URL",
- "value": "https://serene-basin-77096.herokuapp.com",
- "type": "text"
- },
- {
- "enabled": true,
- "key": "TOKEN",
- "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjMwNTM4NCwiaXNzIjoiaHR0cHM6Ly9hcGkudG9wY29kZXItZGV2LmNvbSIsImlhdCI6MTUxMDYxODE0NiwiZXhwIjoxNTEzMjEwMTQ2fQ.Miqo8OpHIPU5kI_YwiOcVgIjTqFlLEvpOptjNzVnF8E",
- "type": "text"
- },
- {
- "enabled": true,
- "key": "TC_ADMIN_TOKEN",
- "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoic3VzZXIxIiwiZXhwIjoxNTEzNDAxMjU4LCJ1c2VySWQiOiI0MDE1MzkzOCIsImlhdCI6MTUwOTYzNzYzOSwiZW1haWwiOiJtdHdvbWV5QGJlYWtzdGFyLmNvbSIsImp0aSI6IjIzZTE2YjA2LWM1NGItNDNkNS1iY2E2LTg0ZGJiN2JiNDA0NyJ9.REds35fdBvY7CMDGGFyT_tOD7DxGimFfVzIyEy9YA0Y",
- "type": "text"
- }
- ],
- "timestamp": 1510810911759,
- "_postman_variable_scope": "environment",
- "_postman_exported_at": "2017-11-16T05:43:05.095Z",
- "_postman_exported_using": "Postman/5.3.2"
+{
+ "id": "4a44c2b1-b1ff-4045-970e-bb2836d0634c",
+ "name": "tc-notification-server-api-heroku-env",
+ "values": [
+ {
+ "enabled": true,
+ "key": "URL",
+ "value": "https://serene-basin-77096.herokuapp.com/v5/notifications",
+ "type": "text"
+ },
+ {
+ "enabled": true,
+ "key": "TOKEN",
+ "value": "",
+ "type": "text"
+ },
+ {
+ "enabled": true,
+ "key": "TC_ADMIN_TOKEN",
+ "value": "",
+ "type": "text"
+ }
+ ],
+ "_postman_variable_scope": "environment",
+ "_postman_exported_at": "2018-05-14T06:13:53.899Z",
+ "_postman_exported_using": "Postman/6.0.10"
}
\ No newline at end of file
diff --git a/docs/tc-notification-server-api-local-env.postman_environment.json b/docs/tc-notification-server-api-local-env.postman_environment.json
index e6d0428..5158a51 100644
--- a/docs/tc-notification-server-api-local-env.postman_environment.json
+++ b/docs/tc-notification-server-api-local-env.postman_environment.json
@@ -1,28 +1,27 @@
-{
- "id": "6d9e5bca-6b22-ea18-e546-580f2c8fb5ff",
- "name": "tc-notification-server-api-local-env",
- "values": [
- {
- "enabled": true,
- "key": "URL",
- "value": "http://localhost:4000",
- "type": "text"
- },
- {
- "enabled": true,
- "key": "TOKEN",
- "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjMwNTM4NCwiaXNzIjoiaHR0cHM6Ly9hcGkudG9wY29kZXItZGV2LmNvbSIsImlhdCI6MTUxMDYxODE0NiwiZXhwIjoxNTEzMjEwMTQ2fQ.Miqo8OpHIPU5kI_YwiOcVgIjTqFlLEvpOptjNzVnF8E",
- "type": "text"
- },
- {
- "enabled": true,
- "key": "TC_ADMIN_TOKEN",
- "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoic3VzZXIxIiwiZXhwIjoxNTEzNDAxMjU4LCJ1c2VySWQiOiI0MDE1MzkzOCIsImlhdCI6MTUwOTYzNzYzOSwiZW1haWwiOiJtdHdvbWV5QGJlYWtzdGFyLmNvbSIsImp0aSI6IjIzZTE2YjA2LWM1NGItNDNkNS1iY2E2LTg0ZGJiN2JiNDA0NyJ9.REds35fdBvY7CMDGGFyT_tOD7DxGimFfVzIyEy9YA0Y",
- "type": "text"
- }
- ],
- "timestamp": 1510810894543,
- "_postman_variable_scope": "environment",
- "_postman_exported_at": "2017-11-16T05:42:38.253Z",
- "_postman_exported_using": "Postman/5.3.2"
+{
+ "id": "8087b254-f1d0-49f4-85ce-44fdab883b48",
+ "name": "tc-notification-server-api-local-env",
+ "values": [
+ {
+ "enabled": true,
+ "key": "URL",
+ "value": "http://localhost:4000/v5/notifications",
+ "type": "text"
+ },
+ {
+ "enabled": true,
+ "key": "TOKEN",
+ "value": "",
+ "type": "text"
+ },
+ {
+ "enabled": true,
+ "key": "TC_ADMIN_TOKEN",
+ "value": "",
+ "type": "text"
+ }
+ ],
+ "_postman_variable_scope": "environment",
+ "_postman_exported_at": "2018-05-14T06:12:15.589Z",
+ "_postman_exported_using": "Postman/6.0.10"
}
\ No newline at end of file
diff --git a/docs/tc-notification-server-api.postman_collection.json b/docs/tc-notification-server-api.postman_collection.json
index 640f0c6..e3b1c4e 100644
--- a/docs/tc-notification-server-api.postman_collection.json
+++ b/docs/tc-notification-server-api.postman_collection.json
@@ -1,742 +1,648 @@
-{
- "id": "be0fac1b-3d54-0215-59e4-ed1be841a89e",
- "name": "tc-notification-server-api",
- "description": "",
- "order": [
- "e16cc911-9393-9f47-cce4-3ef583449fba",
- "6e2f5e53-61db-bb45-f7ae-d0fc9883f92b",
- "919e4314-ca62-918b-e790-7e09afd4cb9b",
- "dc85dae6-5c2c-114a-7b29-2ddbb0310c06",
- "4d9e3771-b9d3-a186-45cd-b57b665b4647",
- "c8ddeee1-2851-b571-08bd-95b822632e40"
- ],
- "folders": [
- {
- "name": "failure",
- "description": "",
- "collectionId": "be0fac1b-3d54-0215-59e4-ed1be841a89e",
- "order": [
- "36087c50-2265-6db2-7f23-d5607006bb6c",
- "99bd0688-cdd3-9136-b64e-f1706884fced",
- "0c7f4099-db8a-5772-9627-335600a8d70e",
- "dac3727a-ed58-854d-73a2-ab8d5a2256bb",
- "9c272f57-3621-7f7b-a6a9-3a26969dd4d1",
- "f8bfa330-ac2e-f0fa-dae8-bb2bcdb3819f",
- "0f6ffaad-aadb-8ad1-a0b1-16c4b1d87718"
- ],
- "owner": 0,
- "folders_order": [],
- "id": "c54ef790-e9db-6bac-432b-80f10e9fd76a"
- }
- ],
- "folders_order": [
- "c54ef790-e9db-6bac-432b-80f10e9fd76a"
- ],
- "timestamp": 1507789221862,
- "owner": 0,
- "public": false,
- "requests": [
- {
- "id": "0c7f4099-db8a-5772-9627-335600a8d70e",
- "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
- "headerData": [
- {
- "key": "Content-Type",
- "value": "application/json",
- "description": "",
- "enabled": true
- },
- {
- "key": "Authorization",
- "value": "Bearer {{TOKEN}}",
- "description": "",
- "enabled": true
- }
- ],
- "url": "{{URL}}/notifications?offset=0&limit=20&type=notifications.connect.project.updated&read=yes",
- "folder": "c54ef790-e9db-6bac-432b-80f10e9fd76a",
- "queryParams": [
- {
- "key": "offset",
- "value": "0",
- "equals": true,
- "description": "",
- "enabled": true
- },
- {
- "key": "limit",
- "value": "20",
- "equals": true,
- "description": "",
- "enabled": true
- },
- {
- "key": "type",
- "value": "notifications.connect.project.updated",
- "equals": true,
- "description": "",
- "enabled": true
- },
- {
- "key": "read",
- "value": "yes",
- "equals": true,
- "description": "",
- "enabled": true
- }
- ],
- "preRequestScript": null,
- "pathVariables": {},
- "pathVariableData": [],
- "method": "GET",
- "data": [],
- "dataMode": "raw",
- "tests": null,
- "currentHelper": "normal",
- "helperAttributes": {},
- "time": 1509801563428,
- "name": "listNotifications - invalid read filter",
- "description": "",
- "collectionId": "be0fac1b-3d54-0215-59e4-ed1be841a89e",
- "responses": [],
- "rawModeData": "",
- "collection_id": "be0fac1b-3d54-0215-59e4-ed1be841a89e"
- },
- {
- "id": "0f6ffaad-aadb-8ad1-a0b1-16c4b1d87718",
- "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
- "headerData": [
- {
- "key": "Content-Type",
- "value": "application/json",
- "description": "",
- "enabled": true
- },
- {
- "key": "Authorization",
- "value": "Bearer {{TOKEN}}",
- "description": "",
- "enabled": true
- }
- ],
- "url": "{{URL}}/notificationsettings",
- "folder": "c54ef790-e9db-6bac-432b-80f10e9fd76a",
- "queryParams": [],
- "preRequestScript": null,
- "pathVariables": {},
- "pathVariableData": [],
- "method": "PUT",
- "data": [],
- "dataMode": "raw",
- "tests": null,
- "currentHelper": "normal",
- "helperAttributes": {},
- "time": 1509801716547,
- "name": "updateSettings - invalid body",
- "description": "",
- "collectionId": "be0fac1b-3d54-0215-59e4-ed1be841a89e",
- "responses": [],
- "rawModeData": "[\n\t{\n\t\t\"wrong\": \"notifications.connect.project.created\",\n\t\t\"deliveryMethod\": 123,\n\t\t\"value\": \"on\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.project.created\",\n\t\t\"deliveryMethod\": \"sms\",\n\t\t\"value\": \"off\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.project.created\",\n\t\t\"deliveryMethod\": \"web\",\n\t\t\"value\": \"off\"\n\t},\n\t{\n\t\t\"topic\": \"notifications.connect.message.posted\",\n\t\t\"deliveryMethod\": \"email\",\n\t\t\"value\": \"off\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.message.posted\",\n\t\t\"deliveryMethod\": \"sms\",\n\t\t\"value\": \"on\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.message.posted\",\n\t\t\"deliveryMethod\": \"web\",\n\t\t\"value\": \"on\"\n\t}\n]",
- "collection_id": "be0fac1b-3d54-0215-59e4-ed1be841a89e"
- },
- {
- "id": "36087c50-2265-6db2-7f23-d5607006bb6c",
- "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
- "headerData": [
- {
- "key": "Content-Type",
- "value": "application/json",
- "description": "",
- "enabled": true
- },
- {
- "key": "Authorization",
- "value": "Bearer {{TOKEN}}",
- "description": "",
- "enabled": true
- }
- ],
- "url": "{{URL}}/notifications?offset=-1&limit=20&type=notifications.connect.project.updated",
- "folder": "c54ef790-e9db-6bac-432b-80f10e9fd76a",
- "queryParams": [
- {
- "key": "offset",
- "value": "-1",
- "equals": true,
- "description": "",
- "enabled": true
- },
- {
- "key": "limit",
- "value": "20",
- "equals": true,
- "description": "",
- "enabled": true
- },
- {
- "key": "type",
- "value": "notifications.connect.project.updated",
- "equals": true,
- "description": "",
- "enabled": true
- },
- {
- "key": "read",
- "value": "false",
- "equals": true,
- "description": "",
- "enabled": false
- }
- ],
- "preRequestScript": null,
- "pathVariables": {},
- "pathVariableData": [],
- "method": "GET",
- "data": [],
- "dataMode": "raw",
- "tests": null,
- "currentHelper": "normal",
- "helperAttributes": {},
- "time": 1509801475461,
- "name": "listNotifications - invalid offset",
- "description": "",
- "collectionId": "be0fac1b-3d54-0215-59e4-ed1be841a89e",
- "responses": [],
- "rawModeData": "",
- "collection_id": "be0fac1b-3d54-0215-59e4-ed1be841a89e"
- },
- {
- "id": "4d9e3771-b9d3-a186-45cd-b57b665b4647",
- "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
- "headerData": [
- {
- "key": "Content-Type",
- "value": "application/json",
- "description": "",
- "enabled": true
- },
- {
- "key": "Authorization",
- "value": "Bearer {{TOKEN}}",
- "description": "",
- "enabled": true
- }
- ],
- "url": "{{URL}}/notificationsettings",
- "queryParams": [],
- "preRequestScript": null,
- "pathVariables": {},
- "pathVariableData": [],
- "method": "GET",
- "data": [],
- "dataMode": "raw",
- "tests": null,
- "currentHelper": "normal",
- "helperAttributes": {},
- "time": 1509793503637,
- "name": "getSettings",
- "description": "",
- "collectionId": "be0fac1b-3d54-0215-59e4-ed1be841a89e",
- "responses": [],
- "rawModeData": "",
- "collection_id": "be0fac1b-3d54-0215-59e4-ed1be841a89e"
- },
- {
- "id": "6e2f5e53-61db-bb45-f7ae-d0fc9883f92b",
- "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
- "headerData": [
- {
- "key": "Content-Type",
- "value": "application/json",
- "description": "",
- "enabled": true
- },
- {
- "key": "Authorization",
- "value": "Bearer {{TOKEN}}",
- "description": "",
- "enabled": true
- }
- ],
- "url": "{{URL}}/notifications?offset=0&limit=20",
- "queryParams": [
- {
- "key": "offset",
- "value": "0",
- "equals": true,
- "description": "",
- "enabled": true
- },
- {
- "key": "limit",
- "value": "20",
- "equals": true,
- "description": "",
- "enabled": true
- },
- {
- "key": "type",
- "value": "notifications.connect.project.updated",
- "equals": true,
- "description": "",
- "enabled": false
- },
- {
- "key": "read",
- "value": "false",
- "equals": true,
- "description": "",
- "enabled": false
- }
- ],
- "preRequestScript": null,
- "pathVariables": {},
- "pathVariableData": [],
- "method": "GET",
- "data": [],
- "dataMode": "raw",
- "tests": null,
- "currentHelper": "normal",
- "helperAttributes": {},
- "time": 1510707608411,
- "name": "listNotifications",
- "description": "",
- "collectionId": "be0fac1b-3d54-0215-59e4-ed1be841a89e",
- "responses": [],
- "rawModeData": ""
- },
- {
- "id": "919e4314-ca62-918b-e790-7e09afd4cb9b",
- "headers": "//Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
- "headerData": [
- {
- "key": "Content-Type",
- "value": "application/json",
- "description": "",
- "enabled": false
- },
- {
- "key": "Authorization",
- "value": "Bearer {{TOKEN}}",
- "description": "",
- "enabled": true
- }
- ],
- "url": "{{URL}}/notifications/1/read",
- "queryParams": [],
- "preRequestScript": null,
- "pathVariables": {},
- "pathVariableData": [],
- "method": "PUT",
- "data": [],
- "dataMode": "raw",
- "tests": null,
- "currentHelper": "normal",
- "helperAttributes": {},
- "time": 1509786004930,
- "name": "markAsRead",
- "description": "",
- "collectionId": "be0fac1b-3d54-0215-59e4-ed1be841a89e",
- "responses": [],
- "rawModeData": "",
- "collection_id": "be0fac1b-3d54-0215-59e4-ed1be841a89e"
- },
- {
- "id": "99bd0688-cdd3-9136-b64e-f1706884fced",
- "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
- "headerData": [
- {
- "key": "Content-Type",
- "value": "application/json",
- "description": "",
- "enabled": true
- },
- {
- "key": "Authorization",
- "value": "Bearer {{TOKEN}}",
- "description": "",
- "enabled": true
- }
- ],
- "url": "{{URL}}/notifications?offset=0&limit=abc&type=notifications.connect.project.updated",
- "folder": "c54ef790-e9db-6bac-432b-80f10e9fd76a",
- "queryParams": [
- {
- "key": "offset",
- "value": "0",
- "equals": true,
- "description": "",
- "enabled": true
- },
- {
- "key": "limit",
- "value": "abc",
- "equals": true,
- "description": "",
- "enabled": true
- },
- {
- "key": "type",
- "value": "notifications.connect.project.updated",
- "equals": true,
- "description": "",
- "enabled": true
- },
- {
- "key": "read",
- "value": "false",
- "equals": true,
- "description": "",
- "enabled": false
- }
- ],
- "preRequestScript": null,
- "pathVariables": {},
- "pathVariableData": [],
- "method": "GET",
- "data": [],
- "dataMode": "raw",
- "tests": null,
- "currentHelper": "normal",
- "helperAttributes": {},
- "time": 1509801518188,
- "name": "listNotifications - invalid limit",
- "description": "",
- "collectionId": "be0fac1b-3d54-0215-59e4-ed1be841a89e",
- "responses": [],
- "rawModeData": "",
- "collection_id": "be0fac1b-3d54-0215-59e4-ed1be841a89e"
- },
- {
- "id": "9c272f57-3621-7f7b-a6a9-3a26969dd4d1",
- "headers": "//Content-Type: application/json\n",
- "headerData": [
- {
- "key": "Content-Type",
- "value": "application/json",
- "description": "",
- "enabled": false
- }
- ],
- "url": "{{URL}}/notifications/read",
- "folder": "c54ef790-e9db-6bac-432b-80f10e9fd76a",
- "queryParams": [],
- "preRequestScript": null,
- "pathVariables": {},
- "pathVariableData": [],
- "method": "PUT",
- "data": [],
- "dataMode": "raw",
- "tests": null,
- "currentHelper": "normal",
- "helperAttributes": {},
- "time": 1509801636468,
- "name": "markAllRead - missing token",
- "description": "",
- "collectionId": "be0fac1b-3d54-0215-59e4-ed1be841a89e",
- "responses": [],
- "rawModeData": "",
- "collection_id": "be0fac1b-3d54-0215-59e4-ed1be841a89e"
- },
- {
- "id": "c8ddeee1-2851-b571-08bd-95b822632e40",
- "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
- "headerData": [
- {
- "key": "Content-Type",
- "value": "application/json",
- "description": "",
- "enabled": true
- },
- {
- "key": "Authorization",
- "value": "Bearer {{TOKEN}}",
- "description": "",
- "enabled": true
- }
- ],
- "url": "{{URL}}/notificationsettings",
- "queryParams": [],
- "preRequestScript": null,
- "pathVariables": {},
- "pathVariableData": [],
- "method": "PUT",
- "data": [],
- "dataMode": "raw",
- "tests": null,
- "currentHelper": "normal",
- "helperAttributes": {},
- "time": 1509794397038,
- "name": "updateSettings",
- "description": "",
- "collectionId": "be0fac1b-3d54-0215-59e4-ed1be841a89e",
- "responses": [],
- "rawModeData": "[\n\t{\n\t\t\"topic\": \"notifications.connect.project.created\",\n\t\t\"deliveryMethod\": \"email\",\n\t\t\"value\": \"on\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.project.created\",\n\t\t\"deliveryMethod\": \"sms\",\n\t\t\"value\": \"off\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.project.created\",\n\t\t\"deliveryMethod\": \"web\",\n\t\t\"value\": \"off\"\n\t},\n\t{\n\t\t\"topic\": \"notifications.connect.message.posted\",\n\t\t\"deliveryMethod\": \"email\",\n\t\t\"value\": \"off\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.message.posted\",\n\t\t\"deliveryMethod\": \"sms\",\n\t\t\"value\": \"on\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.message.posted\",\n\t\t\"deliveryMethod\": \"web\",\n\t\t\"value\": \"on\"\n\t}\n]",
- "collection_id": "be0fac1b-3d54-0215-59e4-ed1be841a89e"
- },
- {
- "id": "dac3727a-ed58-854d-73a2-ab8d5a2256bb",
- "headers": "//Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
- "headerData": [
- {
- "key": "Content-Type",
- "value": "application/json",
- "description": "",
- "enabled": false
- },
- {
- "key": "Authorization",
- "value": "Bearer {{TOKEN}}",
- "description": "",
- "enabled": true
- }
- ],
- "url": "{{URL}}/notifications/1111111/read",
- "folder": "c54ef790-e9db-6bac-432b-80f10e9fd76a",
- "queryParams": [],
- "preRequestScript": null,
- "pathVariables": {},
- "pathVariableData": [],
- "method": "PUT",
- "data": [],
- "dataMode": "raw",
- "tests": null,
- "currentHelper": "normal",
- "helperAttributes": {},
- "time": 1509801601644,
- "name": "markAsRead - not found",
- "description": "",
- "collectionId": "be0fac1b-3d54-0215-59e4-ed1be841a89e",
- "responses": [],
- "rawModeData": "",
- "collection_id": "be0fac1b-3d54-0215-59e4-ed1be841a89e"
- },
- {
- "id": "dc85dae6-5c2c-114a-7b29-2ddbb0310c06",
- "headers": "//Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
- "headerData": [
- {
- "key": "Content-Type",
- "value": "application/json",
- "description": "",
- "enabled": false
- },
- {
- "key": "Authorization",
- "value": "Bearer {{TOKEN}}",
- "description": "",
- "enabled": true
- }
- ],
- "url": "{{URL}}/notifications/read",
- "queryParams": [],
- "preRequestScript": null,
- "pathVariables": {},
- "pathVariableData": [],
- "method": "PUT",
- "data": [],
- "dataMode": "raw",
- "tests": null,
- "currentHelper": "normal",
- "helperAttributes": {},
- "time": 1509793307190,
- "name": "markAllRead",
- "description": "",
- "collectionId": "be0fac1b-3d54-0215-59e4-ed1be841a89e",
- "responses": [],
- "rawModeData": "",
- "collection_id": "be0fac1b-3d54-0215-59e4-ed1be841a89e"
- },
- {
- "id": "e16cc911-9393-9f47-cce4-3ef583449fba",
- "headers": "Content-Type: application/json\nauthorization: Bearer {{TC_ADMIN_TOKEN}}\n",
- "headerData": [
- {
- "key": "Content-Type",
- "value": "application/json",
- "description": "",
- "enabled": true
- },
- {
- "key": "authorization",
- "value": "Bearer {{TC_ADMIN_TOKEN}}",
- "description": "",
- "enabled": true
- }
- ],
- "url": "https://api.topcoder-dev.com/v4/projects/1936",
- "queryParams": [],
- "preRequestScript": null,
- "pathVariables": {},
- "pathVariableData": [],
- "method": "GET",
- "data": [],
- "dataMode": "raw",
- "version": 2,
- "tests": null,
- "currentHelper": "normal",
- "helperAttributes": {},
- "time": 1509766333902,
- "name": "TC API - get project",
- "description": "",
- "collectionId": "be0fac1b-3d54-0215-59e4-ed1be841a89e",
- "responses": [
- {
- "status": "",
- "responseCode": {
- "code": 200,
- "name": "OK",
- "detail": "Standard response for successful HTTP requests. The actual response will depend on the request method used. In a GET request, the response will contain an entity corresponding to the requested resource. In a POST request the response will contain an entity describing or containing the result of the action."
- },
- "time": 760,
- "headers": [
- {
- "name": "access-control-allow-credentials",
- "key": "access-control-allow-credentials",
- "value": "true",
- "description": "Indicates whether or not the response to the request can be exposed when the credentials flag is true. When used as part of a response to a preflight request, this indicates whether or not the actual request can be made using credentials."
- },
- {
- "name": "access-control-allow-headers",
- "key": "access-control-allow-headers",
- "value": "Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,Keep-Alive,X-Requested-With,If-Modified-Since",
- "description": "Used in response to a preflight request to indicate which HTTP headers can be used when making the actual request."
- },
- {
- "name": "access-control-allow-methods",
- "key": "access-control-allow-methods",
- "value": "GET, POST, OPTIONS, DELETE, PUT, PATCH",
- "description": "Specifies the method or methods allowed when accessing the resource. This is used in response to a preflight request."
- },
- {
- "name": "connection",
- "key": "connection",
- "value": "keep-alive",
- "description": "Options that are desired for the connection"
- },
- {
- "name": "content-encoding",
- "key": "content-encoding",
- "value": "gzip",
- "description": "The type of encoding used on the data."
- },
- {
- "name": "content-length",
- "key": "content-length",
- "value": "491",
- "description": "The length of the response body in octets (8-bit bytes)"
- },
- {
- "name": "content-type",
- "key": "content-type",
- "value": "application/json; charset=utf-8",
- "description": "The mime type of this content"
- },
- {
- "name": "date",
- "key": "date",
- "value": "Thu, 02 Nov 2017 04:28:20 GMT",
- "description": "The date and time that the message was sent"
- },
- {
- "name": "etag",
- "key": "etag",
- "value": "W/\"3a6-4pbtTNq19Shn10rc0k+HRsoAyMw\"",
- "description": "An identifier for a specific version of a resource, often a message digest"
- },
- {
- "name": "server",
- "key": "server",
- "value": "nginx/1.9.7",
- "description": "A name for the server"
- },
- {
- "name": "x-powered-by",
- "key": "x-powered-by",
- "value": "Express",
- "description": "Specifies the technology (ASP.NET, PHP, JBoss, e.g.) supporting the web application (version details are often in X-Runtime, X-Version, or X-AspNet-Version)"
- },
- {
- "name": "x-request-id",
- "key": "x-request-id",
- "value": "95744bd2-2830-4014-8885-7182a6225953",
- "description": "Custom header"
- }
- ],
- "cookies": [],
- "mime": "",
- "text": "{\"id\":\"95744bd2-2830-4014-8885-7182a6225953\",\"version\":\"v4\",\"result\":{\"success\":true,\"status\":200,\"content\":{\"id\":1936,\"directProjectId\":12147,\"billingAccountId\":null,\"name\":\"Test-prj\",\"description\":\"Test description\",\"external\":null,\"bookmarks\":[],\"estimatedPrice\":null,\"actualPrice\":null,\"terms\":[],\"type\":\"app_dev\",\"status\":\"draft\",\"details\":{\"products\":[\"api_dev\"],\"appDefinition\":{\"primaryTarget\":\"desktop\",\"goal\":{\"value\":\"Goal\"},\"users\":{\"value\":\"Developers\"},\"notes\":\"Notes\"},\"utm\":{},\"hideDiscussions\":true},\"challengeEligibility\":[],\"cancelReason\":null,\"createdAt\":\"2017-11-01T15:45:51.000Z\",\"updatedAt\":\"2017-11-01T15:45:51.000Z\",\"createdBy\":305384,\"updatedBy\":305384,\"members\":[{\"id\":2997,\"userId\":305384,\"role\":\"customer\",\"isPrimary\":true,\"createdAt\":\"2017-11-01T15:45:51.000Z\",\"updatedAt\":\"2017-11-01T15:45:51.000Z\",\"createdBy\":305384,\"updatedBy\":305384,\"projectId\":1936}],\"attachments\":[]},\"metadata\":{\"totalCount\":1}}}",
- "language": "json",
- "rawDataType": "text",
- "previewType": "text",
- "searchResultScrolledTo": -1,
- "forceNoPretty": false,
- "write": true,
- "empty": false,
- "failed": false,
- "name": "test111",
- "id": "24a44c8d-b9d3-e6ab-8439-62625dafbd07",
- "request": {
- "url": "http://api.topcoder-dev.com/v4/projects/1936",
- "pathVariables": {},
- "pathVariableData": [],
- "queryParams": [],
- "headerData": [
- {
- "key": "authorization",
- "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoic3VzZXIxIiwiZXhwIjoxNTA5NTk3MjM4LCJ1c2VySWQiOiI0MDE1MzkzOCIsImlhdCI6MTUwOTU5NjYzOCwiZW1haWwiOiJtdHdvbWV5QGJlYWtzdGFyLmNvbSIsImp0aSI6ImZmNWY3YWEzLWE0MDktNDE4Ny1hYTBjLWZhZDVmMjI1YTE0NyJ9.WcDvq6bS2R1CMl1YWFzyiSjo0C801RNNS6ACqVRWqWw",
- "description": "",
- "enabled": true,
- "warning": ""
- }
- ],
- "headers": "authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoic3VzZXIxIiwiZXhwIjoxNTA5NTk3MjM4LCJ1c2VySWQiOiI0MDE1MzkzOCIsImlhdCI6MTUwOTU5NjYzOCwiZW1haWwiOiJtdHdvbWV5QGJlYWtzdGFyLmNvbSIsImp0aSI6ImZmNWY3YWEzLWE0MDktNDE4Ny1hYTBjLWZhZDVmMjI1YTE0NyJ9.WcDvq6bS2R1CMl1YWFzyiSjo0C801RNNS6ACqVRWqWw\n",
- "data": "",
- "method": "GET",
- "dataMode": "raw"
- },
- "owner": 0
- }
- ],
- "rawModeData": "",
- "collection_id": "be0fac1b-3d54-0215-59e4-ed1be841a89e"
- },
- {
- "id": "f8bfa330-ac2e-f0fa-dae8-bb2bcdb3819f",
- "headers": "Content-Type: application/json\nAuthorization: Bearer invalid\n",
- "headerData": [
- {
- "key": "Content-Type",
- "value": "application/json",
- "description": "",
- "enabled": true
- },
- {
- "key": "Authorization",
- "value": "Bearer invalid",
- "description": "",
- "enabled": true
- }
- ],
- "url": "{{URL}}/notificationsettings",
- "folder": "c54ef790-e9db-6bac-432b-80f10e9fd76a",
- "queryParams": [],
- "preRequestScript": null,
- "pathVariables": {},
- "pathVariableData": [],
- "method": "GET",
- "data": [],
- "dataMode": "raw",
- "tests": null,
- "currentHelper": "normal",
- "helperAttributes": {},
- "time": 1509801668820,
- "name": "getSettings - invalid token",
- "description": "",
- "collectionId": "be0fac1b-3d54-0215-59e4-ed1be841a89e",
- "responses": [],
- "rawModeData": "",
- "collection_id": "be0fac1b-3d54-0215-59e4-ed1be841a89e"
- }
- ]
+{
+ "id": "3f30c4e3-3b7a-491b-bdb2-6629d081a452",
+ "name": "tc-notification-server-api",
+ "description": "",
+ "auth": null,
+ "events": null,
+ "variables": null,
+ "order": [
+ "19332a51-03e8-4f5c-8f85-4d28d6dfe6f4",
+ "543cab06-2c7d-4aed-8cf3-0808463254d5",
+ "76779830-a8a4-4636-8c03-1801b3d1863d",
+ "cb2299a5-dac7-4c40-80c4-7b1694138354",
+ "d57ba947-a5e7-410a-b978-76882f33c86e",
+ "fce69847-5bf8-4b07-bcaf-6352db4ba923"
+ ],
+ "folders_order": [
+ "dbebd550-6c33-4778-b467-d56decf16c91"
+ ],
+ "folders": [
+ {
+ "id": "dbebd550-6c33-4778-b467-d56decf16c91",
+ "name": "failure",
+ "description": "",
+ "auth": null,
+ "events": null,
+ "collection": "3f30c4e3-3b7a-491b-bdb2-6629d081a452",
+ "folder": null,
+ "order": [
+ "1b3b6480-ea94-4027-8898-f82f28e2bea6",
+ "59fc9f2b-28c5-4cff-b21b-11ab51bf67d8",
+ "cbc03cb1-6dfe-43fd-8e99-8c56923c2978",
+ "d293d2c5-230d-4f34-8c97-1adc1f2f89b4",
+ "da23d550-55b3-4f7d-9131-735956d62f6d",
+ "f2246cf7-7aae-4ea0-9d92-1d932d340302",
+ "f3f3a847-46f6-4059-b167-b436078fb112"
+ ],
+ "folders_order": []
+ }
+ ],
+ "requests": [
+ {
+ "id": "19332a51-03e8-4f5c-8f85-4d28d6dfe6f4",
+ "name": "getSettings",
+ "url": "{{URL}}/settings",
+ "description": "",
+ "data": [],
+ "dataMode": "raw",
+ "headerData": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "enabled": true
+ },
+ {
+ "key": "Authorization",
+ "value": "Bearer {{TOKEN}}",
+ "description": "",
+ "enabled": true
+ }
+ ],
+ "method": "GET",
+ "pathVariableData": [],
+ "queryParams": [],
+ "auth": null,
+ "events": null,
+ "folder": null,
+ "rawModeData": "",
+ "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
+ "pathVariables": {}
+ },
+ {
+ "id": "1b3b6480-ea94-4027-8898-f82f28e2bea6",
+ "name": "listNotifications - invalid read filter",
+ "url": "{{URL}}/list?offset=0&limit=20&type=notifications.connect.project.updated&read=yes",
+ "description": "",
+ "data": [],
+ "dataMode": "raw",
+ "headerData": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "enabled": true
+ },
+ {
+ "key": "Authorization",
+ "value": "Bearer {{TOKEN}}",
+ "description": "",
+ "enabled": true
+ }
+ ],
+ "method": "GET",
+ "pathVariableData": [],
+ "queryParams": [
+ {
+ "key": "offset",
+ "value": "0",
+ "equals": true,
+ "description": "",
+ "enabled": true
+ },
+ {
+ "key": "limit",
+ "value": "20",
+ "equals": true,
+ "description": "",
+ "enabled": true
+ },
+ {
+ "key": "type",
+ "value": "notifications.connect.project.updated",
+ "equals": true,
+ "description": "",
+ "enabled": true
+ },
+ {
+ "key": "read",
+ "value": "yes",
+ "equals": true,
+ "description": "",
+ "enabled": true
+ }
+ ],
+ "auth": null,
+ "events": null,
+ "folder": "dbebd550-6c33-4778-b467-d56decf16c91",
+ "rawModeData": "",
+ "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
+ "pathVariables": {}
+ },
+ {
+ "id": "543cab06-2c7d-4aed-8cf3-0808463254d5",
+ "name": "markAllRead",
+ "url": "{{URL}}/read",
+ "description": "",
+ "data": [],
+ "dataMode": "raw",
+ "headerData": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "enabled": false
+ },
+ {
+ "key": "Authorization",
+ "value": "Bearer {{TOKEN}}",
+ "description": "",
+ "enabled": true
+ }
+ ],
+ "method": "PUT",
+ "pathVariableData": [],
+ "queryParams": [],
+ "auth": null,
+ "events": null,
+ "folder": null,
+ "rawModeData": "",
+ "headers": "//Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
+ "pathVariables": {}
+ },
+ {
+ "id": "59fc9f2b-28c5-4cff-b21b-11ab51bf67d8",
+ "name": "getSettings - invalid token",
+ "url": "{{URL}}/settings",
+ "description": "",
+ "data": [],
+ "dataMode": "raw",
+ "headerData": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "enabled": true
+ },
+ {
+ "key": "Authorization",
+ "value": "Bearer invalid",
+ "description": "",
+ "enabled": true
+ }
+ ],
+ "method": "GET",
+ "pathVariableData": [],
+ "queryParams": [],
+ "auth": null,
+ "events": null,
+ "folder": "dbebd550-6c33-4778-b467-d56decf16c91",
+ "rawModeData": "",
+ "headers": "Content-Type: application/json\nAuthorization: Bearer invalid\n",
+ "pathVariables": {}
+ },
+ {
+ "id": "76779830-a8a4-4636-8c03-1801b3d1863d",
+ "name": "markAsRead",
+ "url": "{{URL}}/1/read",
+ "description": "",
+ "data": [],
+ "dataMode": "raw",
+ "headerData": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "enabled": false
+ },
+ {
+ "key": "Authorization",
+ "value": "Bearer {{TOKEN}}",
+ "description": "",
+ "enabled": true
+ }
+ ],
+ "method": "PUT",
+ "pathVariableData": [],
+ "queryParams": [],
+ "auth": null,
+ "events": null,
+ "folder": null,
+ "rawModeData": "",
+ "headers": "//Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
+ "pathVariables": {}
+ },
+ {
+ "id": "cb2299a5-dac7-4c40-80c4-7b1694138354",
+ "name": "TC API - get project",
+ "url": "https://api.topcoder-dev.com/v4/projects/1936",
+ "description": "",
+ "data": [],
+ "dataMode": "raw",
+ "headerData": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "enabled": true
+ },
+ {
+ "key": "authorization",
+ "value": "Bearer {{TC_ADMIN_TOKEN}}",
+ "description": "",
+ "enabled": true
+ }
+ ],
+ "method": "GET",
+ "pathVariableData": [],
+ "queryParams": [],
+ "auth": null,
+ "events": null,
+ "folder": null,
+ "responses": [
+ {
+ "id": "ae658c70-e29d-4d49-aefd-944af0e4f811",
+ "name": "test111",
+ "status": "",
+ "mime": "",
+ "language": "json",
+ "text": "{\"id\":\"95744bd2-2830-4014-8885-7182a6225953\",\"version\":\"v4\",\"result\":{\"success\":true,\"status\":200,\"content\":{\"id\":1936,\"directProjectId\":12147,\"billingAccountId\":null,\"name\":\"Test-prj\",\"description\":\"Test description\",\"external\":null,\"bookmarks\":[],\"estimatedPrice\":null,\"actualPrice\":null,\"terms\":[],\"type\":\"app_dev\",\"status\":\"draft\",\"details\":{\"products\":[\"api_dev\"],\"appDefinition\":{\"primaryTarget\":\"desktop\",\"goal\":{\"value\":\"Goal\"},\"users\":{\"value\":\"Developers\"},\"notes\":\"Notes\"},\"utm\":{},\"hideDiscussions\":true},\"challengeEligibility\":[],\"cancelReason\":null,\"createdAt\":\"2017-11-01T15:45:51.000Z\",\"updatedAt\":\"2017-11-01T15:45:51.000Z\",\"createdBy\":305384,\"updatedBy\":305384,\"members\":[{\"id\":2997,\"userId\":305384,\"role\":\"customer\",\"isPrimary\":true,\"createdAt\":\"2017-11-01T15:45:51.000Z\",\"updatedAt\":\"2017-11-01T15:45:51.000Z\",\"createdBy\":305384,\"updatedBy\":305384,\"projectId\":1936}],\"attachments\":[]},\"metadata\":{\"totalCount\":1}}}",
+ "responseCode": {
+ "code": 200,
+ "name": "OK",
+ "detail": "Standard response for successful HTTP requests. The actual response will depend on the request method used. In a GET request, the response will contain an entity corresponding to the requested resource. In a POST request the response will contain an entity describing or containing the result of the action."
+ },
+ "requestObject": null,
+ "headers": [
+ {
+ "name": "access-control-allow-credentials",
+ "key": "access-control-allow-credentials",
+ "value": "true",
+ "description": "Indicates whether or not the response to the request can be exposed when the credentials flag is true. When used as part of a response to a preflight request, this indicates whether or not the actual request can be made using credentials."
+ },
+ {
+ "name": "access-control-allow-headers",
+ "key": "access-control-allow-headers",
+ "value": "Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,Keep-Alive,X-Requested-With,If-Modified-Since",
+ "description": "Used in response to a preflight request to indicate which HTTP headers can be used when making the actual request."
+ },
+ {
+ "name": "access-control-allow-methods",
+ "key": "access-control-allow-methods",
+ "value": "GET, POST, OPTIONS, DELETE, PUT, PATCH",
+ "description": "Specifies the method or methods allowed when accessing the resource. This is used in response to a preflight request."
+ },
+ {
+ "name": "connection",
+ "key": "connection",
+ "value": "keep-alive",
+ "description": "Options that are desired for the connection"
+ },
+ {
+ "name": "content-encoding",
+ "key": "content-encoding",
+ "value": "gzip",
+ "description": "The type of encoding used on the data."
+ },
+ {
+ "name": "content-length",
+ "key": "content-length",
+ "value": "491",
+ "description": "The length of the response body in octets (8-bit bytes)"
+ },
+ {
+ "name": "content-type",
+ "key": "content-type",
+ "value": "application/json; charset=utf-8",
+ "description": "The mime type of this content"
+ },
+ {
+ "name": "date",
+ "key": "date",
+ "value": "Thu, 02 Nov 2017 04:28:20 GMT",
+ "description": "The date and time that the message was sent"
+ },
+ {
+ "name": "etag",
+ "key": "etag",
+ "value": "W/\"3a6-4pbtTNq19Shn10rc0k+HRsoAyMw\"",
+ "description": "An identifier for a specific version of a resource, often a message digest"
+ },
+ {
+ "name": "server",
+ "key": "server",
+ "value": "nginx/1.9.7",
+ "description": "A name for the server"
+ },
+ {
+ "name": "x-powered-by",
+ "key": "x-powered-by",
+ "value": "Express",
+ "description": "Specifies the technology (ASP.NET, PHP, JBoss, e.g.) supporting the web application (version details are often in X-Runtime, X-Version, or X-AspNet-Version)"
+ },
+ {
+ "name": "x-request-id",
+ "key": "x-request-id",
+ "value": "95744bd2-2830-4014-8885-7182a6225953",
+ "description": "Custom header"
+ }
+ ],
+ "cookies": [],
+ "request": "cb2299a5-dac7-4c40-80c4-7b1694138354",
+ "collection": "3f30c4e3-3b7a-491b-bdb2-6629d081a452"
+ }
+ ],
+ "rawModeData": "",
+ "headers": "Content-Type: application/json\nauthorization: Bearer {{TC_ADMIN_TOKEN}}\n",
+ "pathVariables": {}
+ },
+ {
+ "id": "cbc03cb1-6dfe-43fd-8e99-8c56923c2978",
+ "name": "markAsRead - not found",
+ "url": "{{URL}}/1111111/read",
+ "description": "",
+ "data": [],
+ "dataMode": "raw",
+ "headerData": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "enabled": false
+ },
+ {
+ "key": "Authorization",
+ "value": "Bearer {{TOKEN}}",
+ "description": "",
+ "enabled": true
+ }
+ ],
+ "method": "PUT",
+ "pathVariableData": [],
+ "queryParams": [],
+ "auth": null,
+ "events": null,
+ "folder": "dbebd550-6c33-4778-b467-d56decf16c91",
+ "rawModeData": "",
+ "headers": "//Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
+ "pathVariables": {}
+ },
+ {
+ "id": "d293d2c5-230d-4f34-8c97-1adc1f2f89b4",
+ "name": "listNotifications - invalid limit",
+ "url": "{{URL}}/list?offset=0&limit=abc&type=notifications.connect.project.updated",
+ "description": "",
+ "data": [],
+ "dataMode": "raw",
+ "headerData": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "enabled": true
+ },
+ {
+ "key": "Authorization",
+ "value": "Bearer {{TOKEN}}",
+ "description": "",
+ "enabled": true
+ }
+ ],
+ "method": "GET",
+ "pathVariableData": [],
+ "queryParams": [
+ {
+ "key": "offset",
+ "value": "0",
+ "equals": true,
+ "description": "",
+ "enabled": true
+ },
+ {
+ "key": "limit",
+ "value": "abc",
+ "equals": true,
+ "description": "",
+ "enabled": true
+ },
+ {
+ "key": "type",
+ "value": "notifications.connect.project.updated",
+ "equals": true,
+ "description": "",
+ "enabled": true
+ },
+ {
+ "key": "read",
+ "value": "false",
+ "equals": true,
+ "description": "",
+ "enabled": false
+ }
+ ],
+ "auth": null,
+ "events": null,
+ "folder": "dbebd550-6c33-4778-b467-d56decf16c91",
+ "rawModeData": "",
+ "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
+ "pathVariables": {}
+ },
+ {
+ "id": "d57ba947-a5e7-410a-b978-76882f33c86e",
+ "name": "updateSettings",
+ "url": "{{URL}}/settings",
+ "description": "",
+ "data": [],
+ "dataMode": "raw",
+ "headerData": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "enabled": true
+ },
+ {
+ "key": "Authorization",
+ "value": "Bearer {{TOKEN}}",
+ "description": "",
+ "enabled": true
+ }
+ ],
+ "method": "PUT",
+ "pathVariableData": [],
+ "queryParams": [],
+ "auth": null,
+ "events": null,
+ "folder": null,
+ "rawModeData": "{\n \"notifications\": {\n \"notifications.connect.project.active\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.updated\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.member.left\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.paused\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.approved\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.fileUploaded\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.canceled\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.topic.created\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.member.copilotJoined\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.post.deleted\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.created\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.member.assignedAsOwner\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.completed\": {\n \"web\": {\n \"enabled\": \"yes\"\n },\n \"email\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.topic.deleted\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.post.created\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.member.joined\": {\n \"web\": {\n \"enabled\": \"yes\"\n },\n \"email\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.member.removed\": {\n \"web\": {\n \"enabled\": \"yes\"\n },\n \"email\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.specificationModified\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.member.managerJoined\": {\n \"web\": {\n \"enabled\": \"yes\"\n },\n \"email\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.submittedForReview\": {\n \"web\": {\n \"enabled\": \"yes\"\n },\n \"email\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.linkCreated\": {\n \"email\": {\n \"enabled\": \"yes\"\n },\n \"web\": {\n \"enabled\": \"yes\"\n }\n },\n \"notifications.connect.project.post.edited\": {\n \"web\": {\n \"enabled\": \"yes\"\n },\n \"email\": {\n \"enabled\": \"yes\"\n }\n }\n },\n \"services\": {\n \"email\": {\n \"bundlePeriod\": \"every10minutes\"\n }\n }\n}",
+ "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
+ "pathVariables": {}
+ },
+ {
+ "id": "da23d550-55b3-4f7d-9131-735956d62f6d",
+ "name": "markAllRead - missing token",
+ "url": "{{URL}}/read",
+ "description": "",
+ "data": [],
+ "dataMode": "raw",
+ "headerData": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "enabled": false
+ }
+ ],
+ "method": "PUT",
+ "pathVariableData": [],
+ "queryParams": [],
+ "auth": null,
+ "events": null,
+ "folder": "dbebd550-6c33-4778-b467-d56decf16c91",
+ "rawModeData": "",
+ "headers": "//Content-Type: application/json\n",
+ "pathVariables": {}
+ },
+ {
+ "id": "f2246cf7-7aae-4ea0-9d92-1d932d340302",
+ "name": "updateSettings - invalid body",
+ "url": "{{URL}}/settings",
+ "description": "",
+ "data": [],
+ "dataMode": "raw",
+ "headerData": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "enabled": true
+ },
+ {
+ "key": "Authorization",
+ "value": "Bearer {{TOKEN}}",
+ "description": "",
+ "enabled": true
+ }
+ ],
+ "method": "PUT",
+ "pathVariableData": [],
+ "queryParams": [],
+ "auth": null,
+ "events": null,
+ "folder": "dbebd550-6c33-4778-b467-d56decf16c91",
+ "rawModeData": "[\n\t{\n\t\t\"wrong\": \"notifications.connect.project.created\",\n\t\t\"deliveryMethod\": 123,\n\t\t\"value\": \"on\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.project.created\",\n\t\t\"deliveryMethod\": \"sms\",\n\t\t\"value\": \"off\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.project.created\",\n\t\t\"deliveryMethod\": \"web\",\n\t\t\"value\": \"off\"\n\t},\n\t{\n\t\t\"topic\": \"notifications.connect.message.posted\",\n\t\t\"deliveryMethod\": \"email\",\n\t\t\"value\": \"off\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.message.posted\",\n\t\t\"deliveryMethod\": \"sms\",\n\t\t\"value\": \"on\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.message.posted\",\n\t\t\"deliveryMethod\": \"web\",\n\t\t\"value\": \"on\"\n\t}\n]",
+ "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
+ "pathVariables": {}
+ },
+ {
+ "id": "f3f3a847-46f6-4059-b167-b436078fb112",
+ "name": "listNotifications - invalid offset",
+ "url": "{{URL}}/list?offset=-1&limit=20&type=notifications.connect.project.updated",
+ "description": "",
+ "data": [],
+ "dataMode": "raw",
+ "headerData": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "enabled": true
+ },
+ {
+ "key": "Authorization",
+ "value": "Bearer {{TOKEN}}",
+ "description": "",
+ "enabled": true
+ }
+ ],
+ "method": "GET",
+ "pathVariableData": [],
+ "queryParams": [
+ {
+ "key": "offset",
+ "value": "-1",
+ "equals": true,
+ "description": "",
+ "enabled": true
+ },
+ {
+ "key": "limit",
+ "value": "20",
+ "equals": true,
+ "description": "",
+ "enabled": true
+ },
+ {
+ "key": "type",
+ "value": "notifications.connect.project.updated",
+ "equals": true,
+ "description": "",
+ "enabled": true
+ },
+ {
+ "key": "read",
+ "value": "false",
+ "equals": true,
+ "description": "",
+ "enabled": false
+ }
+ ],
+ "auth": null,
+ "events": null,
+ "folder": "dbebd550-6c33-4778-b467-d56decf16c91",
+ "rawModeData": "",
+ "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
+ "pathVariables": {}
+ },
+ {
+ "id": "fce69847-5bf8-4b07-bcaf-6352db4ba923",
+ "name": "listNotifications",
+ "url": "{{URL}}/list?offset=0&limit=20",
+ "description": "",
+ "data": [],
+ "dataMode": "raw",
+ "headerData": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "description": "",
+ "enabled": true
+ },
+ {
+ "key": "Authorization",
+ "value": "Bearer {{TOKEN}}",
+ "description": "",
+ "enabled": true
+ }
+ ],
+ "method": "GET",
+ "pathVariableData": [],
+ "queryParams": [
+ {
+ "key": "offset",
+ "value": "0",
+ "equals": true,
+ "description": "",
+ "enabled": true
+ },
+ {
+ "key": "limit",
+ "value": "20",
+ "equals": true,
+ "description": "",
+ "enabled": true
+ },
+ {
+ "key": "type",
+ "value": "notifications.connect.project.updated",
+ "equals": true,
+ "description": "",
+ "enabled": false
+ },
+ {
+ "key": "read",
+ "value": "false",
+ "equals": true,
+ "description": "",
+ "enabled": false
+ }
+ ],
+ "auth": null,
+ "events": null,
+ "folder": null,
+ "rawModeData": "",
+ "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n",
+ "pathVariables": {}
+ }
+ ]
}
\ No newline at end of file
diff --git a/index.js b/index.js
index 926484f..c13c164 100644
--- a/index.js
+++ b/index.js
@@ -7,6 +7,11 @@ const config = require('config');
const _ = require('lodash');
const errors = require('./src/common/errors');
const tcCoreLibAuth = require('tc-core-library-js').auth;
+// some useful components to exposure
+const logger = require('./src/common/logger');
+const busService = require('./src/services/BusAPI');
+const eventScheduler = require('./src/services/EventScheduler');
+const notificationService = require('./src/services/NotificationService');
global.M2m = tcCoreLibAuth.m2m(config);
// key is topic name, e.g. 'notifications.connect.project.created';
@@ -17,6 +22,17 @@ global.M2m = tcCoreLibAuth.m2m(config);
// the callback is function(error, userIds), where userIds is an array of user ids to receive notifications
const handlers = {};
+/**
+ * List of notification service handlers which will process notifications
+ *
+ * Each item is the function of the next signature
+ * function(topicName, messageJSON, notification)
+ * - {String} topicName topic name (event type)
+ * - {Object} messageJSON message raw JSON
+ * - {Object} notification pre-processed notification object
+ */
+const notificationServiceHandlers = [];
+
/**
* Set configuration, the default config will be overridden by the given config,
* unspecified config parameters will not be changed, i.e. still using default values.
@@ -47,6 +63,18 @@ function addTopicHandler(topic, handler) {
handlers[topic] = handler;
}
+/**
+ * Adds notification service handler
+ *
+ * @param {Function} handler notification service handler
+ */
+function addNotificationServiceHandler(handler) {
+ if (!handler) {
+ throw new errors.ValidationError('Missing notification service handler.');
+ }
+ notificationServiceHandlers.push(handler);
+}
+
/**
* Remove topic handler for topic.
* @param {String} topic the topic name
@@ -75,7 +103,7 @@ function start() {
}
// load app only after config is set
const app = require('./src/app');
- app.start(handlers);
+ app.start(handlers, notificationServiceHandlers);
}
/**
@@ -96,4 +124,11 @@ module.exports = {
getAllHandlers,
start,
initDatabase,
+ addNotificationServiceHandler,
+
+ // exposure some useful components
+ logger,
+ busService,
+ eventScheduler,
+ notificationService,
};
diff --git a/migrations/v2.0.sql b/migrations/v2.0.sql
new file mode 100644
index 0000000..bde0fde
--- /dev/null
+++ b/migrations/v2.0.sql
@@ -0,0 +1,16 @@
+ -- rename "deliveryMethod" column to "serviceId"
+ ALTER TABLE "public"."NotificationSettings"
+ RENAME COLUMN "deliveryMethod" TO "serviceId";
+
+ -- add "name" column
+ ALTER TABLE "public"."NotificationSettings"
+ ADD COLUMN "name" character varying(255);
+
+ -- fill "name" column with the value 'enabled'
+ UPDATE public."NotificationSettings"
+ SET "name" = 'enabled';
+
+ -- make "name" column NOT NULL
+ ALTER TABLE "public"."NotificationSettings"
+ ALTER COLUMN "name" SET NOT NULL;
+
diff --git a/package-lock.json b/package-lock.json
index f728156..2cb4986 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,7 +15,7 @@
"integrity": "sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==",
"requires": {
"@types/connect": "3.4.32",
- "@types/node": "10.0.0"
+ "@types/node": "10.0.8"
}
},
"@types/connect": {
@@ -23,7 +23,7 @@
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz",
"integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==",
"requires": {
- "@types/node": "10.0.0"
+ "@types/node": "10.0.8"
}
},
"@types/events": {
@@ -38,7 +38,7 @@
"requires": {
"@types/body-parser": "1.17.0",
"@types/express-serve-static-core": "4.11.1",
- "@types/serve-static": "1.13.1"
+ "@types/serve-static": "1.13.2"
}
},
"@types/express-jwt": {
@@ -56,7 +56,7 @@
"integrity": "sha512-EehCl3tpuqiM8RUb+0255M8PhhSwTtLfmO7zBBdv0ay/VTd/zmrqDfQdZFsa5z/PVMbH2yCMZPXsnrImpATyIw==",
"requires": {
"@types/events": "1.2.0",
- "@types/node": "10.0.0"
+ "@types/node": "10.0.8"
}
},
"@types/express-unless": {
@@ -83,14 +83,14 @@
"integrity": "sha512-A2TAGbTFdBw9azHbpVd+/FkdW2T6msN1uct1O9bH3vTerEHKZhTXJUQXy+hNq1B0RagfU8U+KBdqiZpxjhOUQA=="
},
"@types/node": {
- "version": "10.0.0",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-10.0.0.tgz",
- "integrity": "sha512-kctoM36XiNZT86a7tPsUje+Q/yl+dqELjtYApi0T5eOQ90Elhu0MI10rmYk44yEP4v1jdDvtjQ9DFtpRtHf2Bw=="
+ "version": "10.0.8",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-10.0.8.tgz",
+ "integrity": "sha512-MFFKFv2X4iZy/NFl1m1E8uwE1CR96SGwJjgHma09PLtqOWoj3nqeJHMG+P/EuJGVLvC2I6MdQRQsr4TcRduIow=="
},
"@types/serve-static": {
- "version": "1.13.1",
- "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.1.tgz",
- "integrity": "sha512-jDMH+3BQPtvqZVIcsH700Dfi8Q3MIcEx16g/VdxjoqiGR/NntekB10xdBpirMKnPe9z2C5cBmL0vte0YttOr3Q==",
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.2.tgz",
+ "integrity": "sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q==",
"requires": {
"@types/express-serve-static-core": "4.11.1",
"@types/mime": "2.0.0"
@@ -219,9 +219,9 @@
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
},
"auth0-js": {
- "version": "9.5.0",
- "resolved": "https://registry.npmjs.org/auth0-js/-/auth0-js-9.5.0.tgz",
- "integrity": "sha1-th3+hSJ8dbMb5oREvqyCbhk1vRI=",
+ "version": "9.5.1",
+ "resolved": "https://registry.npmjs.org/auth0-js/-/auth0-js-9.5.1.tgz",
+ "integrity": "sha1-NN6msPEbXl7hOWBWEfSbHA8V27E=",
"requires": {
"base64-js": "1.3.0",
"idtoken-verifier": "1.2.0",
@@ -260,7 +260,7 @@
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.6.1.tgz",
"integrity": "sha1-eIuUtvY04luRvWxd9y1GdFevsAA=",
"requires": {
- "core-js": "2.5.5"
+ "core-js": "2.5.6"
}
},
"backoff": {
@@ -567,9 +567,9 @@
"integrity": "sha1-Qa1XsbVVlR7BcUEqgZQrHoIA00o="
},
"core-js": {
- "version": "2.5.5",
- "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.5.tgz",
- "integrity": "sha1-sU3ek2xkDAV5prUMq8wTLdYSfjs="
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.6.tgz",
+ "integrity": "sha512-lQUVfQi0aLix2xpyjrrJEvfuYCqPc/HwmTKsC/VNf8q0zsjX7SQZtp4+oRONN5Tsur9GDETPjj+Ub2iDiGZfSQ=="
},
"core-util-is": {
"version": "1.0.2",
@@ -684,11 +684,6 @@
"isarray": "1.0.0"
}
},
- "dotenv": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-5.0.1.tgz",
- "integrity": "sha512-4As8uPrjfwb7VXC+WnLCbXK7y+Ueb2B3zgNCePYfhxS1PYeaO1YTeplffTEcbfLhvFNGLAz90VvJs9yomG7bow=="
- },
"dottie": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.0.tgz",
@@ -1532,11 +1527,6 @@
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.0.tgz",
"integrity": "sha1-Gywnmm7s44ChIWi5JIUmWzWx7/s="
},
- "js-string-escape": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz",
- "integrity": "sha1-4mJbrbwNZ8dTPp7cEGjFh65BN+8="
- },
"js-yaml": {
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.11.0.tgz",
@@ -1872,9 +1862,9 @@
"integrity": "sha512-shJkRTSebXvsVqk56I+lkb2latjBs8I+pc2TzWc545y2iFnSjm7Wg0QMh+ZWcdSLQyGEau5jI8ocnmkyTgr9YQ=="
},
"moment-timezone": {
- "version": "0.5.16",
- "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.16.tgz",
- "integrity": "sha512-4d1l92plNNqnMkqI/7boWNVXJvwGL2WyByl1Hxp3h/ao3HZiAqaoQY+6KBkYdiN5QtNDpndq+58ozl8W4GVoNw==",
+ "version": "0.5.17",
+ "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.17.tgz",
+ "integrity": "sha512-Y/JpVEWIOA9Gho4vO15MTnW1FCmHi3ypprrkUaxsZ1TKg3uqC8q/qMBjTddkHoiwwZN3qvZSr4zJP7x9V3LpXA==",
"requires": {
"moment": "2.22.1"
}
@@ -1961,6 +1951,11 @@
}
}
},
+ "node-cron": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-1.2.1.tgz",
+ "integrity": "sha1-jJC8XccjpWKJsHhmVatKHEy2A2g="
+ },
"number-is-nan": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
@@ -2065,12 +2060,11 @@
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
},
"pg": {
- "version": "7.4.1",
- "resolved": "https://registry.npmjs.org/pg/-/pg-7.4.1.tgz",
- "integrity": "sha512-Pi5qYuXro5PAD9xXx8h7bFtmHgAQEG6/SCNyi7gS3rvb/ZQYDmxKchfB0zYtiSJNWq9iXTsYsHjrM+21eBcN1A==",
+ "version": "7.4.3",
+ "resolved": "https://registry.npmjs.org/pg/-/pg-7.4.3.tgz",
+ "integrity": "sha1-97b5P1NA7MJZavu5ShPj1rYJg0s=",
"requires": {
"buffer-writer": "1.0.1",
- "js-string-escape": "1.0.1",
"packet-reader": "0.3.1",
"pg-connection-string": "0.1.3",
"pg-pool": "2.0.3",
@@ -2448,9 +2442,9 @@
}
},
"sequelize": {
- "version": "4.37.6",
- "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-4.37.6.tgz",
- "integrity": "sha512-x/6099L+6+3LQWms23wng/AR6yUE3X/VhrwSTSMbgOIk2ELY3DchI/9f9Ii7LIQRPxW1BHGpwboH7kxS/froXg==",
+ "version": "4.37.7",
+ "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-4.37.7.tgz",
+ "integrity": "sha512-1/M1Aua2GgejZbUI3T90G3uXXjcM4gTfFC36jGsepaJh3cRK9plPmlZeKkAQWWn4bCJaJozeEtuxfyPfQUY9wg==",
"requires": {
"bluebird": "3.5.1",
"cls-bluebird": "2.1.0",
@@ -2461,14 +2455,14 @@
"inflection": "1.12.0",
"lodash": "4.17.10",
"moment": "2.22.1",
- "moment-timezone": "0.5.16",
+ "moment-timezone": "0.5.17",
"retry-as-promised": "2.3.2",
"semver": "5.5.0",
"terraformer-wkt-parser": "1.1.2",
"toposort-class": "1.0.1",
"uuid": "3.2.1",
"validator": "9.4.1",
- "wkx": "0.4.4"
+ "wkx": "0.4.5"
},
"dependencies": {
"debug": {
@@ -2576,14 +2570,6 @@
"resolved": "https://registry.npmjs.org/stream-consume/-/stream-consume-0.1.1.tgz",
"integrity": "sha512-tNa3hzgkjEP7XbCkbRXe1jpg+ievoa0O4SCFlMOYEscGSS4JJsckGL8swUyAa/ApGU3Ae4t6Honor4HhL+tRyg=="
},
- "string_decoder": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
- "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
- "requires": {
- "safe-buffer": "5.1.1"
- }
- },
"string-width": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
@@ -2595,6 +2581,14 @@
"strip-ansi": "3.0.1"
}
},
+ "string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "requires": {
+ "safe-buffer": "5.1.1"
+ }
+ },
"stringstream": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz",
@@ -2708,7 +2702,7 @@
"tc-core-library-js": {
"version": "github:appirio-tech/tc-core-library-js#df1f5c1a5578d3d1e475bfb4a7413d9dec25525a",
"requires": {
- "auth0-js": "9.5.0",
+ "auth0-js": "9.5.1",
"axios": "0.12.0",
"bunyan": "1.8.12",
"config": "1.30.0",
@@ -2936,11 +2930,11 @@
}
},
"wkx": {
- "version": "0.4.4",
- "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.4.4.tgz",
- "integrity": "sha512-eVVHka2jRaAp9QanKhLpxWs3AGDV0b8cijlavxBnn4ryXzq5N/3Xe3nkQsI0XMRA16RURwviCWuOCj4mXCmrxw==",
+ "version": "0.4.5",
+ "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.4.5.tgz",
+ "integrity": "sha512-01dloEcJZAJabLO5XdcRgqdKpmnxS0zIT02LhkdWOZX2Zs2tPM6hlZ4XG9tWaWur1Qd1OO4kJxUbe2+5BofvnA==",
"requires": {
- "@types/node": "10.0.0"
+ "@types/node": "10.0.8"
}
},
"wordwrap": {
diff --git a/package.json b/package.json
index d6a7490..86a0681 100644
--- a/package.json
+++ b/package.json
@@ -31,12 +31,13 @@
"lodash": "^4.17.4",
"millisecond": "^0.1.2",
"no-kafka": "^3.2.4",
+ "node-cron": "^1.2.1",
"pg": "^7.3.0",
+ "remarkable": "^1.7.1",
"sequelize": "^4.21.0",
"superagent": "^3.8.0",
"tc-core-library-js": "appirio-tech/tc-core-library-js.git#v2.3",
- "winston": "^2.2.0",
- "remarkable": "^1.7.1"
+ "winston": "^2.2.0"
},
"engines": {
"node": "6.x"
diff --git a/src/app.js b/src/app.js
index 2674ce7..430951f 100644
--- a/src/app.js
+++ b/src/app.js
@@ -7,24 +7,21 @@ require('./bootstrap');
const config = require('config');
const express = require('express');
const jwtAuth = require('tc-core-library-js').middleware.jwtAuthenticator;
-const jwt = require('jsonwebtoken');
const _ = require('lodash');
const cors = require('cors');
const bodyParser = require('body-parser');
-const { BUS_API_EVENT } = require('./constants');
const helper = require('./common/helper');
-const helperService = require('./services/helper');
const logger = require('./common/logger');
const errors = require('./common/errors');
-const service = require('./services/BusAPI');
const models = require('./models');
const Kafka = require('no-kafka');
/**
* Start Kafka consumer.
- * @param {Object} handlers the handlers
+ * @param {Object} handlers the handlers
+ * @param {Array} notificationServiceHandlers list of notification service handlers
*/
-function startKafkaConsumer(handlers) {
+function startKafkaConsumer(handlers, notificationServiceHandlers) {
// create group consumer
const options = { groupId: config.KAFKA_GROUP_ID, connectionString: config.KAFKA_URL };
if (config.KAFKA_CLIENT_CERT && config.KAFKA_CLIENT_CERT_KEY) {
@@ -53,102 +50,22 @@ function startKafkaConsumer(handlers) {
const handlerAsync = Promise.promisify(handler);
// use handler to create notification instances for each recipient
return handlerAsync(topicName, messageJSON)
- .then((notifications) => Promise.all(_.map(notifications, (notification) =>
+ .then((notifications) => Promise.all(_.map(notifications, (notification) => {
+ // run other notification service handlers
+ notificationServiceHandlers.forEach((notificationServiceHandler) => {
+ notificationServiceHandler(topicName, messageJSON, notification);
+ });
+
// save notifications
- models.Notification.create({
+ return models.Notification.create({
userId: notification.userId,
type: notification.newType || topicName,
version: notification.version || null,
contents: _.extend({}, messageJSON, notification.contents),
read: false,
seen: false,
- })
- .then(() => {
- if (config.ENABLE_EMAILS) {
- // if it's interesting event, create email event and send to bus api
- const notificationType = notification.newType || topicName;
- logger.debug(`checking ${notificationType} notification ${JSON.stringify(notification)}`);
- let eventType;
-
- if (notificationType === BUS_API_EVENT.CONNECT.TOPIC_CREATED) {
- eventType = BUS_API_EVENT.EMAIL.TOPIC_CREATED;
- } else if (notificationType === BUS_API_EVENT.CONNECT.POST_CREATED) {
- eventType = BUS_API_EVENT.EMAIL.POST_CREATED;
- } else if (notificationType === BUS_API_EVENT.CONNECT.MENTIONED_IN_POST) {
- eventType = BUS_API_EVENT.EMAIL.MENTIONED_IN_POST;
- }
- if (!!eventType) {
- const topicId = parseInt(messageJSON.topicId, 10);
- const postId = messageJSON.postId ? parseInt(messageJSON.postId, 10) : null;
-
- helperService.getUsersById([notification.userId]).then((users) => {
- logger.debug(`got users ${JSON.stringify(users)}`);
- helperService.getTopic(topicId, logger).then((connectTopic) => {
- logger.debug(`got topic ${JSON.stringify(connectTopic)}`);
- const user = users[0];
- let userEmail = user.email;
- if (config.ENABLE_DEV_MODE === 'true') {
- userEmail = config.DEV_MODE_EMAIL;
- }
- const recipients = [userEmail];
- const cc = [];
- if (eventType === BUS_API_EVENT.EMAIL.MENTIONED_IN_POST) {
- cc.push(config.MENTION_EMAIL);
- }
- const categories = [`${config.ENV}:${eventType}`.toLowerCase()];
-
- // get jwt token then encode it with base64
- const body = {
- userId: parseInt(notification.userId, 10),
- topicId,
- userEmail: helper.sanitizeEmail(user.email),
- };
- logger.debug('body', body);
- logger.debug(`body for generating token: ${JSON.stringify(body)}`);
- logger.debug(`AUTH_SECRET: ${config.AUTH_SECRET.substring(-5)}`);
- const token = jwt.sign(body, config.AUTH_SECRET, { noTimestamp: true }).split('.')[2];
- logger.debug(`token: ${token}`);
-
- const replyTo = `${config.REPLY_EMAIL_PREFIX}+${topicId}/${token}@${config.REPLY_EMAIL_DOMAIN}`;
-
- const eventMessage = {
- data: {
- name: user.firstName + ' ' + user.lastName,
- handle: user.handle,
- topicTitle: connectTopic.title || '',
- post: helperService.markdownToHTML(messageJSON.postContent),
- date: (new Date()).toISOString(),
- projectName: notification.contents.projectName,
- projectId: messageJSON.projectId,
- topicId,
- postId,
- authorHandle: notification.contents.userHandle,
- },
- recipients,
- replyTo,
- cc,
- from: {
- name: notification.contents.userHandle,
- email: 'topcoder@connectemail.topcoder.com',//TODO pick from config
- },
- categories,
- };
- // send event to bus api
- return service.postEvent({
- "topic": eventType,
- "originator": "tc-notifications",
- "timestamp": (new Date()).toISOString(),
- "mime-type": "application/json",
- "payload": eventMessage,
- }).then(() => {
- logger.info(`sent ${eventType} event with body ${eventMessage} to bus api`);
- });
- });
- });
- }
- }
- })
- )))
+ });
+ })))
// commit offset
.then(() => consumer.commitOffset({ topic, partition, offset: m.offset }))
.catch((err) => logger.error(err));
@@ -164,9 +81,10 @@ function startKafkaConsumer(handlers) {
/**
* Start the notification server.
- * @param {Object} handlers the handlers
+ * @param {Object} handlers the handlers
+ * @param {Array} notificationServiceHandlers list of notification service handlers
*/
-function start(handlers) {
+function start(handlers, notificationServiceHandlers) {
const app = express();
app.set('port', config.PORT);
@@ -241,7 +159,7 @@ function start(handlers) {
logger.info(`Express server listening on port ${app.get('port')}`);
});
- startKafkaConsumer(handlers);
+ startKafkaConsumer(handlers, notificationServiceHandlers);
})
.catch((err) => logger.error(err));
}
diff --git a/src/common/helper.js b/src/common/helper.js
index a557673..c788f72 100644
--- a/src/common/helper.js
+++ b/src/common/helper.js
@@ -38,25 +38,7 @@ function autoWrapExpress(obj) {
return obj;
}
-/**
- * Helper method to clean up the provided email address for deducing the final address that matters for
- * the delivery of the email i.e. removing any non standard parts in the email address e.g. getting rid
- * of anything after + sign in the local part of the email.
- *
- * @param {String} email email address to be sanitized
- *
- * @returns {String} sanitized email
- */
-function sanitizeEmail(email) {
- if (email) {
- return email.substring(0, email.indexOf('+') !== -1 ? email.indexOf('+') : email.indexOf('@'))
- + email.substring(email.indexOf('@'));
- }
- return '';
-}
-
module.exports = {
wrapExpress,
autoWrapExpress,
- sanitizeEmail,
};
diff --git a/src/common/logger.js b/src/common/logger.js
index 8c592e1..f96f7ab 100644
--- a/src/common/logger.js
+++ b/src/common/logger.js
@@ -12,7 +12,10 @@ const getParams = require('get-parameter-names');
const transports = [];
if (!config.DISABLE_LOGGING) {
- transports.push(new (winston.transports.Console)({ level: config.LOG_LEVEL }));
+ transports.push(new (winston.transports.Console)({
+ colorize: true,
+ level: config.LOG_LEVEL,
+ }));
transports.push(new (winston.transports.File)({
filename: 'log.txt',
timestamp: true,
diff --git a/src/models/NotificationSetting.js b/src/models/NotificationSetting.js
index b3add22..39ed238 100644
--- a/src/models/NotificationSetting.js
+++ b/src/models/NotificationSetting.js
@@ -14,6 +14,7 @@ module.exports = (sequelize, DataTypes) => sequelize.define('NotificationSetting
id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
userId: { type: DataTypes.BIGINT, allowNull: false },
topic: { type: DataTypes.STRING, allowNull: false },
- deliveryMethod: { type: DataTypes.STRING, allowNull: false },
+ serviceId: { type: DataTypes.STRING, allowNull: false },
+ name: { type: DataTypes.STRING, allowNull: false },
value: { type: DataTypes.STRING, allowNull: false },
}, { timestamps: false });
diff --git a/src/models/ScheduledEvents.js b/src/models/ScheduledEvents.js
new file mode 100644
index 0000000..5782c88
--- /dev/null
+++ b/src/models/ScheduledEvents.js
@@ -0,0 +1,26 @@
+/**
+ * Copyright (C) 2017 TopCoder Inc., All Rights Reserved.
+ */
+
+/**
+ * the Scheduled Events schema
+ *
+ * @author TCSCODER
+ * @version 1.0
+ */
+
+module.exports = (sequelize, DataTypes) => sequelize.define('ScheduledEvents', {
+ id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
+ schedulerId: { type: DataTypes.STRING, allowNull: false },
+ data: { type: DataTypes.JSON, allowNull: false },
+ // keep period as an arbitrary string so any service can define their own periods
+ period: { type: DataTypes.STRING, allowNull: false },
+ status: { type: DataTypes.ENUM('pending', 'completed', 'failed'), allowNull: false },
+ // next fields are optional, scheduler by itself doesn't rely on them
+ // main intention to have them is debugging production issues quickly
+ // though particular services may defined and use them
+ eventType: { type: DataTypes.STRING, allowNull: true },
+ userId: { type: DataTypes.BIGINT, allowNull: true },
+ reference: { type: DataTypes.STRING, allowNull: true },
+ referenceId: { type: DataTypes.STRING, allowNull: true },
+});
diff --git a/src/models/ServiceSettings.js b/src/models/ServiceSettings.js
new file mode 100644
index 0000000..07ba324
--- /dev/null
+++ b/src/models/ServiceSettings.js
@@ -0,0 +1,19 @@
+/**
+ * Copyright (C) 2017 TopCoder Inc., All Rights Reserved.
+ */
+
+/**
+ * the Service Settings schema
+ *
+ * @author TCSCODER
+ * @version 1.0
+ */
+
+
+module.exports = (sequelize, DataTypes) => sequelize.define('ServiceSettings', {
+ id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
+ userId: { type: DataTypes.BIGINT, allowNull: false },
+ serviceId: { type: DataTypes.STRING, allowNull: false },
+ name: { type: DataTypes.STRING, allowNull: false },
+ value: { type: DataTypes.STRING, allowNull: true },
+}, { timestamps: false });
diff --git a/src/models/index.js b/src/models/index.js
index f8d927e..e6ef09e 100644
--- a/src/models/index.js
+++ b/src/models/index.js
@@ -14,9 +14,13 @@ const DataTypes = require('sequelize/lib/data-types');
const Notification = require('./Notification')(sequelize, DataTypes);
const NotificationSetting = require('./NotificationSetting')(sequelize, DataTypes);
+const ServiceSettings = require('./ServiceSettings')(sequelize, DataTypes);
+const ScheduledEvents = require('./ScheduledEvents')(sequelize, DataTypes);
module.exports = {
Notification,
NotificationSetting,
+ ServiceSettings,
+ ScheduledEvents,
init: () => sequelize.sync(),
};
diff --git a/src/services/BusAPI.js b/src/services/BusAPI.js
index e07eecd..49c89ce 100644
--- a/src/services/BusAPI.js
+++ b/src/services/BusAPI.js
@@ -1,3 +1,7 @@
+/**
+ * Bus API service
+ */
+/* global M2m */
const request = require('superagent');
const config = require('config');
const _ = require('lodash');
@@ -7,12 +11,12 @@ const _ = require('lodash');
*
* @param {Object} event event
*
- * @return {Promise} promise resolved to post event
+ * @return {Promise} promise resolved to post event
*/
-const postEvent = (event) => {
- return M2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET)
- .then((token) => {
- return request
+const postEvent = (event) => (
+ M2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET)
+ .then((token) => (
+ request
.post(`${config.TC_API_V5_BASE_URL}/bus/events`)
.set('Content-Type', 'application/json')
.set('Authorization', `Bearer ${token}`)
@@ -24,13 +28,13 @@ const postEvent = (event) => {
`Failed to post event ${event}.` +
(errorDetails ? ' Server response: ' + errorDetails : '')
);
- });
- })
+ })
+ ))
.catch((err) => {
err.message = 'Error generating m2m token: ' + err.message;
throw err;
- });
-}
+ })
+);
module.exports = {
postEvent,
diff --git a/src/services/EventScheduler.js b/src/services/EventScheduler.js
new file mode 100644
index 0000000..0622e79
--- /dev/null
+++ b/src/services/EventScheduler.js
@@ -0,0 +1,126 @@
+/**
+ * Event scheduler
+ *
+ * Keeps scheduled events in ScheduledEvents model.
+ * When scheduled time comes, retrieves all due events from DB
+ * and passes them to the events handler to process.
+ */
+const _ = require('lodash');
+const cron = require('node-cron');
+const models = require('../models');
+const logger = require('../common/logger');
+
+/**
+ * Statuses of scheduled events
+ *
+ * @constant
+ */
+const SCHEDULED_EVENT_STATUS = {
+ PENDING: 'pending',
+ COMPLETED: 'completed',
+ FAILED: 'failed',
+};
+
+class EventScheduler {
+ /**
+ * EventScheduler constructor
+ *
+ * @param {String} schedulerId scheduler id
+ * @param {Object} periods keys are period names to store in DB
+ * values are period definition in cron format
+ * @param {Function} handler function which is called when events time comes
+ */
+ constructor(schedulerId, periods, handler) {
+ this.schedulerId = schedulerId;
+ this.periods = periods;
+ this.handler = handler;
+
+ this.initSchedule();
+ }
+
+ /**
+ * Set status for the list of events
+ *
+ * @param {Array} events list of events
+ * @param {String} status events status
+ *
+ * @return {Promise} resolves to model update result
+ */
+ static setEventsStatus(events, status) {
+ return models.ScheduledEvents.update({
+ status,
+ }, {
+ where: {
+ id: _.map(events, 'id'),
+ },
+ });
+ }
+
+ /**
+ * Initialize cron schedule
+ */
+ initSchedule() {
+ _.forOwn(this.periods, (periodDefinition, periodName) => {
+ logger.verbose(`[EventScheduler] init handler '${this.schedulerId}'`
+ + ` period '${periodName}' (${periodDefinition}).`);
+
+ cron.schedule(periodDefinition, () => {
+ logger.verbose(`[EventScheduler] run task for handler '${this.schedulerId}'`
+ + ` period '${periodName}' (${periodDefinition}).`);
+
+ models.ScheduledEvents.findAll({
+ where: {
+ schedulerId: this.schedulerId,
+ period: periodName,
+ status: [
+ SCHEDULED_EVENT_STATUS.PENDING,
+ SCHEDULED_EVENT_STATUS.FAILED,
+ ],
+ },
+ }).then((events) => this.handler(events, EventScheduler.setEventsStatus));
+ });
+ });
+ }
+
+ /**
+ * Adds events to the list of pending events
+ *
+ * @param {Object} scheduledEvent event prams to schedule
+ * @param {Object} scheduledEvent.data arbitrary event data
+ * @param {String} scheduledEvent.period event period name
+ * @param {Number} scheduledEvent.userId (optional) user id
+ * @param {String} scheduledEvent.eventType (optional) event type
+ * @param {String} scheduledEvent.reference (optional) target entity name (like 'topic')
+ * @param {String} scheduledEvent.referenceId (optional) target entity id (like