From 72711e819c79f4adb536761206cfb144f8f9faa3 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Wed, 9 May 2018 13:36:34 +0800 Subject: [PATCH 01/10] getNotificationsForMentionedUser handler event type is always called checks required data by itself --- connect/connectNotificationServer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/connect/connectNotificationServer.js b/connect/connectNotificationServer.js index a5678ef..1a1ba4c 100644 --- a/connect/connectNotificationServer.js +++ b/connect/connectNotificationServer.js @@ -62,7 +62,7 @@ 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([]); } @@ -296,7 +296,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) => ( From 28eb1269f901f0426b71555b912c34149d46e163 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Wed, 9 May 2018 13:38:27 +0800 Subject: [PATCH 02/10] exclude handler calls all event handlers including getNotificationsForMentionedUser even with current config it doesn't make any difference, but in theory it can --- connect/connectNotificationServer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/connect/connectNotificationServer.js b/connect/connectNotificationServer.js index 1a1ba4c..40221df 100644 --- a/connect/connectNotificationServer.js +++ b/connect/connectNotificationServer.js @@ -243,6 +243,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) => ( From 897e20b2a23b7685f202f167cb3a05e40eface42 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Fri, 11 May 2018 12:37:53 +0800 Subject: [PATCH 03/10] issue #40 - Bundling support for email notifications and email settings and issue #38 - Refactor logic of email notification handling --- .gitignore | 1 + README.md | 104 +++++++++----- config/default.js | 20 +-- connect/config.js | 26 +++- connect/connectNotificationServer.js | 8 +- {src => connect}/constants.js | 16 ++- connect/helpers.js | 48 +++++++ connect/notificationServices/email.js | 197 ++++++++++++++++++++++++++ connect/service.js | 8 +- index.js | 37 ++++- package-lock.json | 44 +++--- package.json | 7 +- src/app.js | 114 +++------------ src/common/helper.js | 18 --- src/common/logger.js | 5 +- src/models/ScheduledEvents.js | 21 +++ src/models/ServiceSettings.js | 19 +++ src/models/index.js | 4 + src/services/BusAPI.js | 23 +-- src/services/EventScheduler.js | 124 ++++++++++++++++ src/services/SettingsService.js | 35 +++++ src/services/helper.js | 91 ------------ 22 files changed, 669 insertions(+), 301 deletions(-) rename {src => connect}/constants.js (63%) create mode 100644 connect/helpers.js create mode 100644 connect/notificationServices/email.js create mode 100644 src/models/ScheduledEvents.js create mode 100644 src/models/ServiceSettings.js create mode 100644 src/services/EventScheduler.js create mode 100644 src/services/SettingsService.js delete mode 100644 src/services/helper.js 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..2d7617c 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,8 @@ 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', +/* TODO remove */TC_ADMIN_TOKEN: process.env.TC_ADMIN_TOKEN, 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 40221df..fb9be08 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 @@ -345,6 +346,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..b7d1f8d 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: '*/1 * * * *', + 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: 'connect.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/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..06a5ee6 --- /dev/null +++ b/connect/notificationServices/email.js @@ -0,0 +1,197 @@ +/** + * Email notification service + */ +const _ = require('lodash'); +const jwt = require('jsonwebtoken'); +const { logger, busService, eventScheduler, settingsService } = 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, + }; + + // data property we define as an array of data from each individual event + eventMessage.data = []; + userEvents.forEach((event) => { + eventMessage.data.push(event.data.data); + }); + + 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) { + const topicId = parseInt(messageJSON.topicId, 10); + const postId = messageJSON.postId ? parseInt(messageJSON.postId, 10) : null; + + service.getUsersById([notification.userId]).then((users) => { + logger.debug(`got users ${JSON.stringify(users)}`); + service.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: 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, + }; + + settingsService.getServiceSettingsOption({ + userId: notification.userId, + serviceId: SETTINGS_EMAIL_SERVICE_ID, + name: 'bundlePeriod', + }).then((bundleSetting) => { + // if notifications has to be bundled + const bundlePeriod = bundleSetting && bundleSetting.value; + + 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}'.`); + } + + scheduler.addEvent( + eventMessage, + bundlePeriod, + notification.userId, + eventType + ); + } 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`); + }); + } + }); + }); + }); + } +} + +module.exports = { + handler, +}; diff --git a/connect/service.js b/connect/service.js index 03fffab..fcaf8d4 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 @@ -70,7 +72,7 @@ const getUsersById = (ids) => { const query = _.map(ids, (id) => 'userId:' + id).join(' OR '); return M2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET) .then((token) => { - if (!token && config.TC_ADMIN_TOKEN) token = config.TC_ADMIN_TOKEN; + /* if (!token && config.TC_ADMIN_TOKEN) */ token = config.TC_ADMIN_TOKEN; return request .get(`${config.TC_API_V3_BASE_URL}/members/_search?fields=userId,email,handle,firstName,lastName&query=${query}`) @@ -142,7 +144,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/index.js b/index.js index 926484f..0846df0 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 settingsService = require('./src/services/SettingsService'); 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, + settingsService, }; diff --git a/package-lock.json b/package-lock.json index f728156..cb7babe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", @@ -1961,6 +1956,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", @@ -2576,14 +2576,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 +2587,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", @@ -2706,9 +2706,9 @@ } }, "tc-core-library-js": { - "version": "github:appirio-tech/tc-core-library-js#df1f5c1a5578d3d1e475bfb4a7413d9dec25525a", + "version": "git+https://github.com/maxceem/tc-core-library-js.git#a4baa3be031c05322c02e0e4121f5dae9422aa28", "requires": { - "auth0-js": "9.5.0", + "auth0-js": "9.5.1", "axios": "0.12.0", "bunyan": "1.8.12", "config": "1.30.0", diff --git a/package.json b/package.json index d6a7490..1db08c9 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" + "tc-core-library-js": "git+https://github.com/maxceem/tc-core-library-js.git#skip-validation", + "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/ScheduledEvents.js b/src/models/ScheduledEvents.js new file mode 100644 index 0000000..944b87c --- /dev/null +++ b/src/models/ScheduledEvents.js @@ -0,0 +1,21 @@ +/** + * 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 }, + eventType: { type: DataTypes.STRING, allowNull: true }, + userId: { type: DataTypes.BIGINT, 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..7145e33 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,13 @@ 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) => ( + (token = config.TC_ADMIN_TOKEN), // TODO remove when get m2m credentials fixed + request .post(`${config.TC_API_V5_BASE_URL}/bus/events`) .set('Content-Type', 'application/json') .set('Authorization', `Bearer ${token}`) @@ -24,13 +29,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..9323fd9 --- /dev/null +++ b/src/services/EventScheduler.js @@ -0,0 +1,124 @@ +/** + * 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} data any event data + * @param {String} periodName event period name + * @param {Number} userId (optional) user id + * @param {String} eventType (optional) event type + * + * @return {Promise} resolves to model create result + */ + addEvent(data, periodName, userId, eventType) { + logger.verbose(`[EventScheduler] add event for handler '${this.schedulerId}' period '${periodName}'.`); + + return models.ScheduledEvents.create({ + schedulerId: this.schedulerId, + data, + period: periodName, + status: SCHEDULED_EVENT_STATUS.PENDING, + userId, + eventType, + }); + } +} + +/** + * Creates EventScheduler instance + * + * @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 + */ +function createEventScheduler(schedulerId, periods, handler) { + return new EventScheduler(schedulerId, periods, handler); +} + +module.exports = { + SCHEDULED_EVENT_STATUS, + createEventScheduler, +}; diff --git a/src/services/SettingsService.js b/src/services/SettingsService.js new file mode 100644 index 0000000..28a688f --- /dev/null +++ b/src/services/SettingsService.js @@ -0,0 +1,35 @@ +/** + * Settings service + * to be used by other modules + */ +const _ = require('lodash'); +const models = require('../models'); + +/** + * Get particular service settings option + * + * @param {Object} options defined which settings option to get + * + * @return {Promise} resolves to settings option + */ +function getServiceSettingsOption(options) { + // get only defined supported params + const where = _.omitBy( + _.pick(options, [ + 'userId', + 'serviceId', + 'name', + ]), + _.isUndefined + ); + + + return models.ServiceSettings.findOne({ + where, + raw: true, + }); +} + +module.exports = { + getServiceSettingsOption, +}; diff --git a/src/services/helper.js b/src/services/helper.js deleted file mode 100644 index b3584af..0000000 --- a/src/services/helper.js +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Service to get data from TopCoder API - */ -const request = require('superagent'); -const config = require('config'); -const _ = require('lodash'); -const Remarkable = require('remarkable') - -/** - * Get users details by ids - * - * @param {Array} ids list of user ids - * - * @return {Promise} resolves to the list of user details - */ -const getUsersById = (ids) => { - const query = _.map(ids, (id) => 'id=' + id).join(' OR '); - return request - .get(`${config.TC_API_V3_BASE_URL}/users?fields=userId,email,handle,firstName,lastName&filter=${query}`) - .set('accept', 'application/json') - .set('authorization', `Bearer ${config.TC_ADMIN_TOKEN}`) - .then((res) => { - if (!_.get(res, 'body.result.success')) { - throw new Error(`Failed to get users by id: ${ids}`); - } - const users = _.get(res, 'body.result.content'); - return users; - }).catch((err) => { - const errorDetails = _.get(err, 'response.body.result.content.message'); - throw new Error( - `Failed to get users by ids: ${ids}.` + - (errorDetails ? ' Server response: ' + errorDetails : '') - ); - }); -}; - -/** - * Get topic details - * - * @param {String} topicId topic id - * - * @return {Promise} promise resolved to topic details - */ -const getTopic = (topicId, logger) => request - .get(`${config.MESSAGE_API_BASE_URL}/topics/${topicId}/read`) - .set('accept', 'application/json') - .set('authorization', `Bearer ${config.TC_ADMIN_TOKEN}`) - .then((res) => { - if (!_.get(res, 'body.result.success')) { - throw new Error(`Failed to get topic details of topic id: ${topicId}`); - } - - return _.get(res, 'body.result.content'); - }).catch((err) => { - if (logger) { - logger.error(err, `Error while calling ${config.MESSAGE_API_BASE_URL}/topics/${topicId}/read`); - } - const errorDetails = _.get(err, 'response.body.result.content.message'); - throw new Error( - `Failed to get topic details of topic id: ${topicId}.` + - (errorDetails ? ' Server response: ' + errorDetails : '') - ); - }); - - - -/** - * 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'), '++') - return md.render(_markdown, {}) // remarkable js takes markdown and makes it an array of style objects for us to easily parse -} - - -module.exports = { - getUsersById, - getTopic, - markdownToHTML, -}; From d725371a708408ea38db4e0c32d4ca96fca68eb4 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Fri, 11 May 2018 14:43:06 +0800 Subject: [PATCH 04/10] issue #37 - Support multiple mentions - right now it only sends mention event for first mention in the post --- connect/connectNotificationServer.js | 5 ++--- connect/events-config.js | 1 + connect/service.js | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/connect/connectNotificationServer.js b/connect/connectNotificationServer.js index fb9be08..5bcd072 100644 --- a/connect/connectNotificationServer.js +++ b/connect/connectNotificationServer.js @@ -69,8 +69,7 @@ const getNotificationsForMentionedUser = (eventConfig, content) => { 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(); @@ -82,12 +81,12 @@ const getNotificationsForMentionedUser = (eventConfig, content) => { }, }); matches = regexUserHandle.exec(content); - handles.push(handle); } // only one per userHandle notifications = _.uniqBy(notifications, 'userHandle'); return new Promise((resolve) => { + const handles = _.map(notifications, 'userHandle'); service.getUsersByHandle(handles).then((users) => { _.map(notifications, (notification) => { notification.userId = _.find(users, { handle: notification.userHandle }).userId.toString(); 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/service.js b/connect/service.js index fcaf8d4..be20ea4 100644 --- a/connect/service.js +++ b/connect/service.js @@ -47,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'); @@ -56,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 : '') ); }); @@ -72,7 +72,7 @@ const getUsersById = (ids) => { const query = _.map(ids, (id) => 'userId:' + id).join(' OR '); return M2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET) .then((token) => { - /* if (!token && config.TC_ADMIN_TOKEN) */ token = config.TC_ADMIN_TOKEN; + /* if (!token && config.TC_ADMIN_TOKEN) */ token = config.TC_ADMIN_TOKEN; // TODO uncomment when get fixed m2m token return request .get(`${config.TC_API_V3_BASE_URL}/members/_search?fields=userId,email,handle,firstName,lastName&query=${query}`) @@ -110,7 +110,7 @@ const getUsersByHandle = (handles) => { const query = _.map(handles, (handle) => 'handle:' + handle).join(' OR '); return M2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET) .then((token) => { - if (!token && config.TC_ADMIN_TOKEN) token = config.TC_ADMIN_TOKEN; + /* if (!token && config.TC_ADMIN_TOKEN) */ token = config.TC_ADMIN_TOKEN; // TODO uncomment when get fixed m2m token return request .get(`${config.TC_API_V3_BASE_URL}/members/_search?fields=userId,handle,firstName,lastName&query=${query}`) From 892df24f0d7d09187b824aab58ce9299b20e3179 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Fri, 11 May 2018 15:56:14 +0800 Subject: [PATCH 05/10] added reference and referenceId fields to the scheduled events table --- connect/notificationServices/email.js | 14 +++++++------ src/models/ScheduledEvents.js | 5 +++++ src/services/EventScheduler.js | 30 ++++++++++++++------------- 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/connect/notificationServices/email.js b/connect/notificationServices/email.js index 06a5ee6..6247f24 100644 --- a/connect/notificationServices/email.js +++ b/connect/notificationServices/email.js @@ -168,12 +168,14 @@ function handler(topicName, messageJSON, notification) { + ` '${SETTINGS_EMAIL_SERVICE_ID}' option 'bundlePeriod' has unsupported value '${bundlePeriod}'.`); } - scheduler.addEvent( - eventMessage, - bundlePeriod, - notification.userId, - eventType - ); + scheduler.addEvent({ + data: eventMessage, + period: bundlePeriod, + userId: notification.userId, + eventType, + reference: 'topic', + referenceId: topicId, + }); } else { // send event to bus api return busService.postEvent({ diff --git a/src/models/ScheduledEvents.js b/src/models/ScheduledEvents.js index 944b87c..5782c88 100644 --- a/src/models/ScheduledEvents.js +++ b/src/models/ScheduledEvents.js @@ -16,6 +16,11 @@ module.exports = (sequelize, DataTypes) => sequelize.define('ScheduledEvents', { // 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/services/EventScheduler.js b/src/services/EventScheduler.js index 9323fd9..0622e79 100644 --- a/src/services/EventScheduler.js +++ b/src/services/EventScheduler.js @@ -85,24 +85,26 @@ class EventScheduler { /** * Adds events to the list of pending events * - * @param {Object} data any event data - * @param {String} periodName event period name - * @param {Number} userId (optional) user id - * @param {String} eventType (optional) event type + * @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 ) * * @return {Promise} resolves to model create result */ - addEvent(data, periodName, userId, eventType) { - logger.verbose(`[EventScheduler] add event for handler '${this.schedulerId}' period '${periodName}'.`); + addEvent(scheduledEvent) { + logger.verbose(`[EventScheduler] add event for handler '${this.schedulerId}' period '${scheduledEvent.period}'.`); - return models.ScheduledEvents.create({ - schedulerId: this.schedulerId, - data, - period: periodName, - status: SCHEDULED_EVENT_STATUS.PENDING, - userId, - eventType, - }); + const event = _.pick(scheduledEvent, [ + 'data', 'period', 'userId', 'eventType', 'reference', 'referenceId', + ]); + event.schedulerId = this.schedulerId; + event.status = SCHEDULED_EVENT_STATUS.PENDING; + + return models.ScheduledEvents.create(event); } } From ace8776c6ca4498b2b9f75f67d3a8ab9f6f6d1c2 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Sat, 12 May 2018 16:24:19 +0800 Subject: [PATCH 06/10] fix notification mention handler in case no users were mentioned --- connect/connectNotificationServer.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/connect/connectNotificationServer.js b/connect/connectNotificationServer.js index 5bcd072..289ef1c 100644 --- a/connect/connectNotificationServer.js +++ b/connect/connectNotificationServer.js @@ -87,12 +87,16 @@ const getNotificationsForMentionedUser = (eventConfig, content) => { return new Promise((resolve) => { const handles = _.map(notifications, 'userHandle'); - service.getUsersByHandle(handles).then((users) => { - _.map(notifications, (notification) => { - notification.userId = _.find(users, { handle: notification.userHandle }).userId.toString(); + 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([]); + } }); }; From 58f1910828902589b921d7697a4d97d92fd43b38 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Mon, 14 May 2018 09:45:50 +0800 Subject: [PATCH 07/10] updated notification settings service to be per service, notification settings apply to emails, revert some services to use m2m --- config/default.js | 1 - connect/constants.js | 4 +- connect/notificationServices/email.js | 191 +-- connect/service.js | 28 +- ...ver-api-local-env.postman_environment.json | 53 +- ...ication-server-api.postman_collection.json | 1388 ++++++++--------- index.js | 4 +- migrations/v2.0.sql | 16 + src/models/NotificationSetting.js | 3 +- src/services/BusAPI.js | 1 - src/services/NotificationService.js | 144 +- src/services/SettingsService.js | 35 - 12 files changed, 927 insertions(+), 941 deletions(-) create mode 100644 migrations/v2.0.sql delete mode 100644 src/services/SettingsService.js diff --git a/config/default.js b/config/default.js index 2d7617c..73bf19b 100644 --- a/config/default.js +++ b/config/default.js @@ -30,7 +30,6 @@ module.exports = { KAFKA_CLIENT_CERT_KEY: process.env.KAFKA_CLIENT_CERT_KEY ? process.env.KAFKA_CLIENT_CERT_KEY.replace('\\n', '\n') : null, -/* TODO remove */TC_ADMIN_TOKEN: process.env.TC_ADMIN_TOKEN, TC_API_V5_BASE_URL: process.env.TC_API_V5_BASE_URL || 'https://api.topcoder-dev.com/v5', API_CONTEXT_PATH: process.env.API_CONTEXT_PATH || '/v5/notifications', diff --git a/connect/constants.js b/connect/constants.js index b7d1f8d..3936d31 100644 --- a/connect/constants.js +++ b/connect/constants.js @@ -1,14 +1,14 @@ module.exports = { // periods of time in cron format (node-cron) SCHEDULED_EVENT_PERIOD: { - every10minutes: '*/1 * * * *', + 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: 'connect.email', + SETTINGS_EMAIL_SERVICE_ID: 'email', BUS_API_EVENT: { CONNECT: { diff --git a/connect/notificationServices/email.js b/connect/notificationServices/email.js index 6247f24..cfb0216 100644 --- a/connect/notificationServices/email.js +++ b/connect/notificationServices/email.js @@ -3,7 +3,8 @@ */ const _ = require('lodash'); const jwt = require('jsonwebtoken'); -const { logger, busService, eventScheduler, settingsService } = require('../../index'); +const co = require('co'); +const { logger, busService, eventScheduler, notificationService } = require('../../index'); const { createEventScheduler, SCHEDULED_EVENT_STATUS } = eventScheduler; const config = require('../config'); @@ -98,100 +99,114 @@ function handler(topicName, messageJSON, notification) { } if (!!eventType) { - const topicId = parseInt(messageJSON.topicId, 10); - const postId = messageJSON.postId ? parseInt(messageJSON.postId, 10) : null; - - service.getUsersById([notification.userId]).then((users) => { + 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)}`); - service.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), + 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, - 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, - }; - - settingsService.getServiceSettingsOption({ + 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, - serviceId: SETTINGS_EMAIL_SERVICE_ID, - name: 'bundlePeriod', - }).then((bundleSetting) => { - // if notifications has to be bundled - const bundlePeriod = bundleSetting && bundleSetting.value; - - 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}'.`); - } - - 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`); - }); - } + 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 = { diff --git a/connect/service.js b/connect/service.js index be20ea4..c0cb0ce 100644 --- a/connect/service.js +++ b/connect/service.js @@ -71,8 +71,12 @@ 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; // TODO uncomment when get fixed m2m token + if (!token && config.TC_ADMIN_TOKEN) token = config.TC_ADMIN_TOKEN; return request .get(`${config.TC_API_V3_BASE_URL}/members/_search?fields=userId,email,handle,firstName,lastName&query=${query}`) @@ -80,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; }); }; @@ -109,8 +110,12 @@ 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; // TODO uncomment when get fixed m2m token + if (!token && config.TC_ADMIN_TOKEN) token = config.TC_ADMIN_TOKEN; return request .get(`${config.TC_API_V3_BASE_URL}/members/_search?fields=userId,handle,firstName,lastName&query=${query}`) @@ -124,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; }); }; 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..452a1b5 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": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiY29waWxvdCIsIkNvbm5lY3QgTWFuYWdlciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoicHNoYWhfbWFuYWdlciIsImV4cCI6MTUyNjExNDkwNSwidXNlcklkIjoiNDAxNTI4NTYiLCJpYXQiOjE1MjYxMTQzMDUsImVtYWlsIjoicGFydGgrbWFuYWdlcmRldjFAdG9wY29kZXIuY29tIiwianRpIjoiMTkwNzVmNDUtM2M1YS00M2JiLWI1NTUtNTJlZDg4OGZiMmQzIn0.KmGbG-8vrMj9op3N1DV8Yv3TWN9UoEQGkxfNLDNwylI", + "type": "text" + }, + { + "enabled": true, + "key": "TC_ADMIN_TOKEN", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoic3VzZXIxIiwiZXhwIjoxNTEzNDAxMjU4LCJ1c2VySWQiOiI0MDE1MzkzOCIsImlhdCI6MTUwOTYzNzYzOSwiZW1haWwiOiJtdHdvbWV5QGJlYWtzdGFyLmNvbSIsImp0aSI6IjIzZTE2YjA2LWM1NGItNDNkNS1iY2E2LTg0ZGJiN2JiNDA0NyJ9.REds35fdBvY7CMDGGFyT_tOD7DxGimFfVzIyEy9YA0Y", + "type": "text" + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2018-05-12T09:09:51.291Z", + "_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 0846df0..c13c164 100644 --- a/index.js +++ b/index.js @@ -11,7 +11,7 @@ const tcCoreLibAuth = require('tc-core-library-js').auth; const logger = require('./src/common/logger'); const busService = require('./src/services/BusAPI'); const eventScheduler = require('./src/services/EventScheduler'); -const settingsService = require('./src/services/SettingsService'); +const notificationService = require('./src/services/NotificationService'); global.M2m = tcCoreLibAuth.m2m(config); // key is topic name, e.g. 'notifications.connect.project.created'; @@ -130,5 +130,5 @@ module.exports = { logger, busService, eventScheduler, - settingsService, + 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/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/services/BusAPI.js b/src/services/BusAPI.js index 7145e33..49c89ce 100644 --- a/src/services/BusAPI.js +++ b/src/services/BusAPI.js @@ -16,7 +16,6 @@ const _ = require('lodash'); const postEvent = (event) => ( M2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET) .then((token) => ( - (token = config.TC_ADMIN_TOKEN), // TODO remove when get m2m credentials fixed request .post(`${config.TC_API_V5_BASE_URL}/bus/events`) .set('Content-Type', 'application/json') diff --git a/src/services/NotificationService.js b/src/services/NotificationService.js index 5c47c04..dae9625 100644 --- a/src/services/NotificationService.js +++ b/src/services/NotificationService.js @@ -17,15 +17,33 @@ const DEFAULT_LIMIT = 10; * @returns {Object} the notification settings */ function* getSettings(userId) { - const settings = yield models.NotificationSetting.findAll({ where: { userId } }); - const result = {}; - _.each(settings, (setting) => { - if (!result[setting.topic]) { - result[setting.topic] = {}; + const notificationSettings = yield models.NotificationSetting.findAll({ where: { userId } }); + const serviceSettings = yield models.ServiceSettings.findAll({ where: { userId } }); + + // format settings per notification type + const notifications = {}; + _.each(notificationSettings, (setting) => { + if (!notifications[setting.topic]) { + notifications[setting.topic] = {}; + } + if (!notifications[setting.topic][setting.serviceId]) { + notifications[setting.topic][setting.serviceId] = {}; + } + notifications[setting.topic][setting.serviceId][setting.name] = setting.value; + }); + + // format settings per service + const services = {}; + _.each(serviceSettings, (setting) => { + if (!services[setting.serviceId]) { + services[setting.serviceId] = {}; } - result[setting.topic][setting.deliveryMethod] = setting.value; + services[setting.serviceId][setting.name] = setting.value; }); - return result; + return { + notifications, + services, + }; } getSettings.schema = { @@ -37,9 +55,9 @@ getSettings.schema = { * @param {Object} entry the notification setting entry * @param {Number} userId the user id */ -function* saveSetting(entry, userId) { +function* saveNotificationSetting(entry, userId) { const setting = yield models.NotificationSetting.findOne({ where: { - userId, topic: entry.topic, deliveryMethod: entry.deliveryMethod } }); + userId, topic: entry.topic, serviceId: entry.serviceId, name: entry.name } }); if (setting) { setting.value = entry.value; yield setting.save(); @@ -47,7 +65,29 @@ function* saveSetting(entry, userId) { yield models.NotificationSetting.create({ userId, topic: entry.topic, - deliveryMethod: entry.deliveryMethod, + serviceId: entry.serviceId, + name: entry.name, + value: entry.value, + }); + } +} + +/** + * Save service setting entry. If the entry is not found, it will be created; otherwise it will be updated. + * @param {Object} entry the service setting entry + * @param {Number} userId the user id + */ +function* saveServiceSetting(entry, userId) { + const setting = yield models.ServiceSettings.findOne({ where: { + userId, serviceId: entry.serviceId, name: entry.name } }); + if (setting) { + setting.value = entry.value; + yield setting.save(); + } else { + yield models.ServiceSettings.create({ + userId, + serviceId: entry.serviceId, + name: entry.name, value: entry.value, }); } @@ -59,21 +99,71 @@ function* saveSetting(entry, userId) { * @param {Number} userId the user id */ function* updateSettings(data, userId) { - // there should be no duplicate (topic + deliveryMethod) pairs - const pairs = {}; - _.each(data, (entry) => { - const key = `${entry.topic} | ${entry.deliveryMethod}`; - if (pairs[key]) { + // convert notification settings object to the list of entries + const notifications = []; + _.forOwn(data.notifications, (notification, topic) => { + _.forOwn(notification, (serviceSettings, serviceId) => { + _.forOwn(serviceSettings, (value, name) => { + notifications.push({ + topic, + serviceId, + name, + value, + }); + }); + }); + }); + + // validation + // there should be no duplicate (topic + serviceId + name) + const triples = {}; + notifications.forEach((entry) => { + const key = `${entry.topic} | ${entry.serviceId} | ${entry.name}`; + if (triples[key]) { throw new errors.BadRequestError(`There are duplicate data for topic: ${ - entry.topic}, deliveryMethod: ${entry.deliveryMethod}`); + entry.topic}, serviceId: ${entry.serviceId}, name: ${entry.name}`); } - pairs[key] = entry; + triples[key] = entry; }); // save each entry in parallel - yield _.map(data, (entry) => saveSetting(entry, userId)); + yield _.map(notifications, (entry) => saveNotificationSetting(entry, userId)); + + // convert services settings object the the list of entries + const services = []; + _.forOwn(data.services, (service, serviceId) => { + _.forOwn(service, (value, name) => { + services.push({ + serviceId, + name, + value, + }); + }); + }); + + // validation + // there should be no duplicate (serviceId + name) + const paris = {}; + services.forEach((entry) => { + const key = `${entry.serviceId} | ${entry.name}`; + if (paris[key]) { + throw new errors.BadRequestError('There are duplicate data for' + + ` serviceId: ${entry.serviceId}, name: ${entry.name}`); + } + paris[key] = entry; + }); + + yield _.map(services, (entry) => saveServiceSetting(entry, userId)); } +updateSettings.schema = { + data: Joi.object().keys({ + notifications: Joi.object(), + services: Joi.object(), + }).required(), + userId: Joi.number().required(), +}; + /** * List notifications. * @@ -87,14 +177,17 @@ function* updateSettings(data, userId) { */ function* listNotifications(query, userId) { const settings = yield getSettings(userId); + const notificationSettings = settings.notifications; const filter = { where: { userId, }, offset: query.offset, limit: query.limit, order: [['createdAt', 'DESC']] }; - if (_.keys(settings).length > 0) { + if (_.keys(notificationSettings).length > 0) { // only filter out notifications types which were explicitly set to 'no' - so we return notification by default - const notificationTypes = _.keys(settings).filter((notificationType) => settings[notificationType].web !== 'no'); - filter.where.type = { $in: notificationTypes }; + const notifications = _.keys(notificationSettings).filter((notificationType) => + notificationSettings[notificationType].web.enabled !== 'no' + ); + filter.where.type = { $in: notifications }; } if (query.type) { filter.where.type = query.type; @@ -204,15 +297,6 @@ markAsSeen.schema = { userId: Joi.number().required(), }; -updateSettings.schema = { - data: Joi.array().min(1).items(Joi.object().keys({ - topic: Joi.string().required(), - deliveryMethod: Joi.string().required(), - value: Joi.string().required(), - })).required(), - userId: Joi.number().required(), -}; - // Exports module.exports = { listNotifications, diff --git a/src/services/SettingsService.js b/src/services/SettingsService.js deleted file mode 100644 index 28a688f..0000000 --- a/src/services/SettingsService.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Settings service - * to be used by other modules - */ -const _ = require('lodash'); -const models = require('../models'); - -/** - * Get particular service settings option - * - * @param {Object} options defined which settings option to get - * - * @return {Promise} resolves to settings option - */ -function getServiceSettingsOption(options) { - // get only defined supported params - const where = _.omitBy( - _.pick(options, [ - 'userId', - 'serviceId', - 'name', - ]), - _.isUndefined - ); - - - return models.ServiceSettings.findOne({ - where, - raw: true, - }); -} - -module.exports = { - getServiceSettingsOption, -}; From 4545a94b5b44094d34da6163ed697c6c43674a1c Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Mon, 14 May 2018 10:01:17 +0800 Subject: [PATCH 08/10] revert to real version of the tc-core-library-js --- package-lock.json | 58 +++++++++++++++++++++-------------------------- package.json | 2 +- 2 files changed, 27 insertions(+), 33 deletions(-) diff --git a/package-lock.json b/package-lock.json index cb7babe..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" @@ -1527,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", @@ -1867,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" } @@ -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": { @@ -2706,7 +2700,7 @@ } }, "tc-core-library-js": { - "version": "git+https://github.com/maxceem/tc-core-library-js.git#a4baa3be031c05322c02e0e4121f5dae9422aa28", + "version": "github:appirio-tech/tc-core-library-js#df1f5c1a5578d3d1e475bfb4a7413d9dec25525a", "requires": { "auth0-js": "9.5.1", "axios": "0.12.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 1db08c9..86a0681 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "remarkable": "^1.7.1", "sequelize": "^4.21.0", "superagent": "^3.8.0", - "tc-core-library-js": "git+https://github.com/maxceem/tc-core-library-js.git#skip-validation", + "tc-core-library-js": "appirio-tech/tc-core-library-js.git#v2.3", "winston": "^2.2.0" }, "engines": { From 5e1700ec7e294f22517e89445db2137485dd02ca Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Mon, 14 May 2018 14:14:34 +0800 Subject: [PATCH 09/10] removed token values from postman environments --- ...er-api-heroku-env.postman_environment.json | 53 +++++++++---------- ...ver-api-local-env.postman_environment.json | 6 +-- 2 files changed, 29 insertions(+), 30 deletions(-) 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 452a1b5..5158a51 100644 --- a/docs/tc-notification-server-api-local-env.postman_environment.json +++ b/docs/tc-notification-server-api-local-env.postman_environment.json @@ -11,17 +11,17 @@ { "enabled": true, "key": "TOKEN", - "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiY29waWxvdCIsIkNvbm5lY3QgTWFuYWdlciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoicHNoYWhfbWFuYWdlciIsImV4cCI6MTUyNjExNDkwNSwidXNlcklkIjoiNDAxNTI4NTYiLCJpYXQiOjE1MjYxMTQzMDUsImVtYWlsIjoicGFydGgrbWFuYWdlcmRldjFAdG9wY29kZXIuY29tIiwianRpIjoiMTkwNzVmNDUtM2M1YS00M2JiLWI1NTUtNTJlZDg4OGZiMmQzIn0.KmGbG-8vrMj9op3N1DV8Yv3TWN9UoEQGkxfNLDNwylI", + "value": "", "type": "text" }, { "enabled": true, "key": "TC_ADMIN_TOKEN", - "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoic3VzZXIxIiwiZXhwIjoxNTEzNDAxMjU4LCJ1c2VySWQiOiI0MDE1MzkzOCIsImlhdCI6MTUwOTYzNzYzOSwiZW1haWwiOiJtdHdvbWV5QGJlYWtzdGFyLmNvbSIsImp0aSI6IjIzZTE2YjA2LWM1NGItNDNkNS1iY2E2LTg0ZGJiN2JiNDA0NyJ9.REds35fdBvY7CMDGGFyT_tOD7DxGimFfVzIyEy9YA0Y", + "value": "", "type": "text" } ], "_postman_variable_scope": "environment", - "_postman_exported_at": "2018-05-12T09:09:51.291Z", + "_postman_exported_at": "2018-05-14T06:12:15.589Z", "_postman_exported_using": "Postman/6.0.10" } \ No newline at end of file From 5247417cca34edb3ac731d79c597ffa0d318dfb1 Mon Sep 17 00:00:00 2001 From: Samir Gondzetovic Date: Sun, 20 May 2018 23:54:18 +0100 Subject: [PATCH 10/10] prepare bundled email html --- connect/notificationServices/email.js | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/connect/notificationServices/email.js b/connect/notificationServices/email.js index cfb0216..6a3d445 100644 --- a/connect/notificationServices/email.js +++ b/connect/notificationServices/email.js @@ -41,12 +41,24 @@ function handleScheduledEvents(events, setEventsStatus) { email: config.DEFAULT_REPLY_EMAIL, }; - // data property we define as an array of data from each individual event - eventMessage.data = []; - userEvents.forEach((event) => { - eventMessage.data.push(event.data.data); + // 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 += `

Visit message

`; + 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',