diff --git a/config/default.js b/config/default.js index 750802c..d2f85b6 100644 --- a/config/default.js +++ b/config/default.js @@ -45,22 +45,23 @@ module.exports = { AUTH0_PROXY_SERVER_URL: process.env.AUTH0_PROXY_SERVER_URL, KAFKA_CONSUMER_RULESETS: { - // key is Kafka topic name, value is array of ruleset which have key as handler function name defined in src/processors/index.js + // key is Kafka topic name, value is array of ruleset which have key as + // handler function name defined in src/processors/index.js 'challenge.notification.events': [ { handleChallenge: /** topic handler name */ { type: 'UPDATE_DRAFT_CHALLENGE', - roles: ["Submitter" /** Competitor */, "Copilot", "Reviewer"], + roles: ['Submitter' /** Competitor */, 'Copilot', 'Reviewer'], notification: { id: 0, /** challengeid or projectid */ name: '', /** challenge name */ group: 'Challenge', - title: 'Challenge specification is modified.' - } - } - } + title: 'Challenge specification is modified.', + }, + }, + }, ], 'notifications.autopilot.events': [ { @@ -68,33 +69,33 @@ module.exports = { { phaseTypeName: 'Checkpoint Screening', state: 'START', - roles: ["Copilot", "Reviewer"], + roles: ['Copilot', 'Reviewer'], notification: { id: 0, /** challengeid or projectid */ name: '', /** challenge name */ group: 'Challenge', - title: 'Challenge checkpoint review.' - } - } - } + title: 'Challenge checkpoint review.', + }, + }, + }, ], 'submission.notification.create': [ { handleSubmission: { resource: 'submission', - roles: ["Copilot", "Reviewer"], + roles: ['Copilot', 'Reviewer'], selfOnly: true /** Submitter only */, notification: { id: 0, /** challengeid or projectid */ name: '', /** challenge name */ group: 'Submission', - title: 'A new submission is uploaded.' - } - } - } + title: 'A new submission is uploaded.', + }, + }, + }, ], //'notifications.community.challenge.created': ['handleChallengeCreated'], //'notifications.community.challenge.phasewarning': ['handleChallengePhaseWarning'], diff --git a/connect/connectNotificationServer.js b/connect/connectNotificationServer.js index d581e09..0b42233 100644 --- a/connect/connectNotificationServer.js +++ b/connect/connectNotificationServer.js @@ -9,6 +9,7 @@ const config = require('./config'); const notificationServer = require('../index'); const _ = require('lodash'); const service = require('./service'); +const helpers = require('./helpers'); const { BUS_API_EVENT } = require('./constants'); const EVENTS = require('./events-config').EVENTS; const PROJECT_ROLE_RULES = require('./events-config').PROJECT_ROLE_RULES; @@ -246,6 +247,83 @@ const getNotificationsForTopicStarter = (eventConfig, topicId) => { }); }; +/** + * Filter members by project roles + * + * @params {Array} List of project roles + * @params {Array} List of project members + * + * @returns {Array} List of objects with user ids + */ +const filterMembersByRoles = (roles, members) => { + let result = []; + + roles.forEach(projectRole => { + result = result.concat( + _.filter(members, PROJECT_ROLE_RULES[projectRole]) + .map(projectMember => ({ + userId: projectMember.userId.toString(), + })) + ); + }); + + return result; +}; + +/** + * Exclude private posts notification + * + * @param {Object} eventConfig event configuration + * @param {Object} project project details + * @param {Array} tags list of message tags + * + * @return {Promise} resolves to a list of notifications + */ +const getExcludedPrivatePostNotifications = (eventConfig, project, tags) => { + // skip if message is not private or exclusion rule is not configured + if (!_.includes(tags, 'MESSAGES') || !eventConfig.privatePostsForProjectRoles) { + return Promise.resolve([]); + } + + const members = _.get(project, 'members', []); + const notifications = filterMembersByRoles(eventConfig.privatePostsForProjectRoles, members); + + return Promise.resolve(notifications); +}; + +/** + * Exclude notifications about posts inside draft phases + * + * @param {Object} eventConfig event configuration + * @param {Object} project project details + * @param {Array} tags list of message tags + * + * @return {Promise} resolves to a list of notifications + */ +const getExcludeDraftPhasesNotifications = (eventConfig, project, tags) => { + // skip is no exclusion rule is configured + if (!eventConfig.draftPhasesForProjectRoles) { + return Promise.resolve([]); + } + + const phaseId = helpers.extractPhaseId(tags); + // skip if it is not phase notification + if (!phaseId) { + return Promise.resolve([]); + } + + // exclude all user with configured roles if phase is in draft state + return service.getPhase(project.id, phaseId) + .then((phase) => { + if (phase.status === 'draft') { + const members = _.get(project, 'members', []); + const notifications = filterMembersByRoles(eventConfig.draftPhasesForProjectRoles, members); + + return Promise.resolve(notifications); + } + }); +}; + /** * Exclude notifications using exclude rules of the event config * @@ -272,12 +350,17 @@ const excludeNotifications = (notifications, eventConfig, message, data) => { // and after filter out such notifications from the notifications list // TODO move this promise all together with `_.uniqBy` to one function // and reuse it here and in `handler` function + const tags = _.get(message, 'tags', []); + return Promise.all([ getNotificationsForTopicStarter(excludeEventConfig, message.topicId), getNotificationsForUserId(excludeEventConfig, message.userId), - getNotificationsForMentionedUser(eventConfig, message.postContent), + getNotificationsForMentionedUser(excludeEventConfig, message.postContent), getProjectMembersNotifications(excludeEventConfig, project), getTopCoderMembersNotifications(excludeEventConfig), + // these are special exclude rules which are only working for excluding notifications but not including + getExcludedPrivatePostNotifications(excludeEventConfig, project, tags), + getExcludeDraftPhasesNotifications(excludeEventConfig, project, tags), ]).then((notificationsPerSource) => ( _.uniqBy(_.flatten(notificationsPerSource), 'userId') )).then((excludedNotifications) => { diff --git a/connect/events-config.js b/connect/events-config.js index acf92bc..460cd47 100644 --- a/connect/events-config.js +++ b/connect/events-config.js @@ -5,6 +5,7 @@ const { BUS_API_EVENT } = require('./constants'); // project member role names const PROJECT_ROLE_OWNER = 'owner'; +const PROJECT_ROLE_CUSTOMER = 'customer'; const PROJECT_ROLE_COPILOT = 'copilot'; const PROJECT_ROLE_MANAGER = 'manager'; const PROJECT_ROLE_MEMBER = 'member'; @@ -13,6 +14,7 @@ const PROJECT_ROLE_ACCOUNT_MANAGER = 'account_manager'; // project member role rules const PROJECT_ROLE_RULES = { [PROJECT_ROLE_OWNER]: { role: 'customer', isPrimary: true }, + [PROJECT_ROLE_CUSTOMER]: { role: 'customer' }, [PROJECT_ROLE_COPILOT]: { role: 'copilot' }, [PROJECT_ROLE_MANAGER]: { role: 'manager' }, [PROJECT_ROLE_ACCOUNT_MANAGER]: { role: 'account_manager' }, @@ -20,6 +22,7 @@ const PROJECT_ROLE_RULES = { }; // TopCoder roles +// eslint-disable-next-line no-unused-vars const ROLE_CONNECT_COPILOT = 'Connect Copilot'; const ROLE_CONNECT_MANAGER = 'Connect Manager'; const ROLE_CONNECT_COPILOT_MANAGER = 'Connect Copilot Manager'; @@ -123,31 +126,53 @@ const EVENTS = [ version: 2, projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, PROJECT_ROLE_MEMBER], toMentionedUsers: true, + exclude: { + privatePostsForProjectRoles: [PROJECT_ROLE_CUSTOMER], + }, }, { type: BUS_API_EVENT.CONNECT.POST.CREATED, version: 2, projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, PROJECT_ROLE_MEMBER], toTopicStarter: true, toMentionedUsers: true, + exclude: { + draftPhasesForProjectRoles: [PROJECT_ROLE_CUSTOMER], + privatePostsForProjectRoles: [PROJECT_ROLE_CUSTOMER], + }, }, { type: BUS_API_EVENT.CONNECT.POST.UPDATED, version: 2, projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, PROJECT_ROLE_MEMBER], toTopicStarter: true, toMentionedUsers: true, + exclude: { + draftPhasesForProjectRoles: [PROJECT_ROLE_CUSTOMER], + privatePostsForProjectRoles: [PROJECT_ROLE_CUSTOMER], + }, }, { type: BUS_API_EVENT.CONNECT.POST.MENTION, + exclude: { + draftPhasesForProjectRoles: [PROJECT_ROLE_CUSTOMER], + privatePostsForProjectRoles: [PROJECT_ROLE_CUSTOMER], + }, }, { type: BUS_API_EVENT.CONNECT.TOPIC.DELETED, version: 2, projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, PROJECT_ROLE_MEMBER], toTopicStarter: false, + exclude: { + privatePostsForProjectRoles: [PROJECT_ROLE_CUSTOMER], + }, }, { type: BUS_API_EVENT.CONNECT.POST.DELETED, version: 2, projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, PROJECT_ROLE_MEMBER], + exclude: { + draftPhasesForProjectRoles: [PROJECT_ROLE_CUSTOMER], + privatePostsForProjectRoles: [PROJECT_ROLE_CUSTOMER], + }, }, { type: BUS_API_EVENT.CONNECT.PROJECT.LINK_CREATED, diff --git a/connect/helpers.js b/connect/helpers.js index 3092323..94691fd 100644 --- a/connect/helpers.js +++ b/connect/helpers.js @@ -2,6 +2,9 @@ * Helper functions */ const Remarkable = require('remarkable'); +const _ = require('lodash'); + +const PHASE_ID_REGEXP = /phase#(\d+)/; /** * Convert markdown into raw draftjs state @@ -42,7 +45,20 @@ const sanitizeEmail = (email) => { return ''; }; +/** + * Helper method to extract phaseId from tag + * + * @param {Array} tags list of message tags + * + * @returns {String} phase id + */ +const extractPhaseId = (tags) => { + const phaseIds = tags.map((tag) => _.get(tag.match(PHASE_ID_REGEXP), '1', null)); + return _.find(phaseIds, (phaseId) => phaseId !== null); +}; + module.exports = { markdownToHTML, sanitizeEmail, + extractPhaseId, }; diff --git a/connect/notificationServices/email.js b/connect/notificationServices/email.js index 40a64a5..cdcdaa2 100644 --- a/connect/notificationServices/email.js +++ b/connect/notificationServices/email.js @@ -4,8 +4,6 @@ const _ = require('lodash'); const jwt = require('jsonwebtoken'); const co = require('co'); -const fs = require('fs'); -const path = require('path'); const { logger, busService, eventScheduler, notificationService } = require('../../index'); const { createEventScheduler, SCHEDULED_EVENT_STATUS } = eventScheduler; @@ -16,29 +14,43 @@ const { SETTINGS_EMAIL_SERVICE_ID, ACTIVE_USER_STATUSES, } = require('../constants'); -const { EVENTS, EVENT_BUNDLES } = require('../events-config'); +const { EVENT_BUNDLES } = require('../events-config'); const helpers = require('../helpers'); const service = require('../service'); function replacePlaceholders(term, data) { - let placeholders = term.match(/<[a-zA-Z]+>/g); + const placeholders = term.match(/<[a-zA-Z]+>/g); let ret = term; if (placeholders && placeholders.length) { _(placeholders).each(p => { - let values = _.map(data, p.slice(1, -1)); - const total = values.length; - let replacement = values.length < 3 ? - values.join(', ') : - values.slice(0, 2).join(', ') + ' and ' + (total - 3) + 'others'; + const values = _.map(data, p.slice(1, -1)); + // TODO remove this code if possible. + // This code appears to be not in use causing lint errors. + // For now I'm commenting it, in case it contains some valuable logic. + // But after confirmation that it's redundant it has to be removed. + // + // const total = values.length; + // const replacement = values.length < 3 ? + // values.join(', ') : + // values.slice(0, 2).join(', ') + ' and ' + (total - 3) + 'others'; ret = ret.replace(p, values.join(', ')); }); } return ret; } +function getEventGroupKey(value) { + const key = _.chain(EVENT_BUNDLES) + .keys() + .find(k => _.includes(_.get(EVENT_BUNDLES, `${k}.types`), _.get(value, 'data.data.type'))) + .value(); + if (!key) return 'DEFAULT'; + return key; +} + function getSections(projectUserEvents) { - let sections = []; + const sections = []; _.chain(projectUserEvents) .groupBy(value => getEventGroupKey(value)) .forIn((value, key) => { @@ -49,7 +61,7 @@ function getSections(projectUserEvents) { notifications: _(value).map(v => v.data.data).value(), }); } else { - _.chain(value).groupBy(n => n.data.data[EVENT_BUNDLES[key].groupBy]).forIn((groupValue, groupKey) => { + _.chain(value).groupBy(n => n.data.data[EVENT_BUNDLES[key].groupBy]).forIn((groupValue) => { let title = EVENT_BUNDLES[key].title; title = replacePlaceholders(title, _(groupValue).map(g => g.data.data).value()); sections.push({ @@ -140,15 +152,6 @@ const scheduler = createEventScheduler( handleScheduledEvents ); -function getEventGroupKey(value) { - let key = _.chain(EVENT_BUNDLES) - .keys() - .find(key => _.includes(_.get(EVENT_BUNDLES, `${key}.types`), _.get(value, 'data.data.type'))) - .value(); - if (!key) return 'DEFAULT'; - return key; -} - /** * Prepares data to be provided to the template to render a single notification. * @@ -245,30 +248,31 @@ function handler(topicName, messageJSON, notification) { }; eventMessage.data[eventMessage.data.type] = true; _.assign(eventMessage.data, notification.contents); - + // message service may return tags // to understand if post notification is regarding phases or no, we will try to get phaseId from the tags - const tags = _.get(notification.contents, 'tags', []) - const PHASE_ID_REGEXP = /phase#(\d+)/ - const phaseIds = tags.map((tag) => _.get(tag.match(PHASE_ID_REGEXP), '1', null)) - const phaseId = _.find(phaseIds, (phaseId) => phaseId !== null) + const tags = _.get(notification.contents, 'tags', []); + const phaseId = helpers.extractPhaseId(tags); if (phaseId) { eventMessage.data.phaseId = phaseId; } - // if the notification is regarding topic: dashboard topic, dashboard post or phase post + // if the notification is regarding topic: dashboard topic, dashboard post or phase post // we build a link to the post if (eventMessage.data.topicId) { // phase post if (eventMessage.data.phaseId) { + // eslint-disable-next-line max-len eventMessage.data.postURL = `${config.CONNECT_URL}/projects/${eventMessage.data.projectId}/plan#phase-${eventMessage.data.phaseId}-posts-${eventMessage.data.postId}`; // dashboard post } else if (eventMessage.data.postId) { + // eslint-disable-next-line max-len eventMessage.data.postURL = `${config.CONNECT_URL}/projects/${eventMessage.data.projectId}#comment-${eventMessage.data.postId}`; // dashboard topic } else { + // eslint-disable-next-line max-len eventMessage.data.postURL = `${config.CONNECT_URL}/projects/${eventMessage.data.projectId}#feed-${eventMessage.data.topicId}`; } } @@ -319,19 +323,21 @@ function handler(topicName, messageJSON, notification) { } // if notifications has to be bundled - let bundlePeriod = _.get(settings, `notifications['${notificationType}'].${SETTINGS_EMAIL_SERVICE_ID}.bundlePeriod`); + let bundlePeriod = _.get(settings, + `notifications['${notificationType}'].${SETTINGS_EMAIL_SERVICE_ID}.bundlePeriod`); bundlePeriod = bundlePeriod && bundlePeriod.trim().length > 0 ? bundlePeriod : null; // if bundling is not explicitly set and the event is not a messaging event, assume bundling enabled if (!bundlePeriod && !messagingEvent) { // finds the event category for the notification type - let eventBundleCategory = _.findKey(EVENT_BUNDLES, b => b.types && b.types.indexOf(notificationType) !== -1); + const eventBundleCategory = _.findKey(EVENT_BUNDLES, b => b.types && b.types.indexOf(notificationType) !== -1); if (eventBundleCategory) { const eventBundle = EVENT_BUNDLES[eventBundleCategory]; // if we find the event category for the notification, use the bundle settings from the first event if (eventBundle && eventBundle.types && eventBundle.types.length) { const firstEvtInBundle = eventBundle.types[0]; - const firstEvtBundleSettingPath = `notifications['${firstEvtInBundle}'].${SETTINGS_EMAIL_SERVICE_ID}.bundlePeriod`; - let firstEvtBundlePeriod = _.get(settings, firstEvtBundleSettingPath); + const firstEvtBundleSettingPath = + `notifications['${firstEvtInBundle}'].${SETTINGS_EMAIL_SERVICE_ID}.bundlePeriod`; + const firstEvtBundlePeriod = _.get(settings, firstEvtBundleSettingPath); bundlePeriod = firstEvtBundlePeriod; logger.debug('Assuming bundle period of first event in the event category=>', bundlePeriod); } @@ -341,7 +347,7 @@ function handler(topicName, messageJSON, notification) { } logger.debug('bundlePeriod=>', bundlePeriod); - if (bundlePeriod && 'immediately' !== bundlePeriod && !requiresImmediateAttention) { + if (bundlePeriod && bundlePeriod !== 'immediately' && !requiresImmediateAttention) { 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}'.`); @@ -359,7 +365,7 @@ function handler(topicName, messageJSON, notification) { } else { // send single field "notificationsHTML" with the rendered template eventMessage.data = wrapIndividualNotification({ data: eventMessage }); - //console.log(eventMessage.data.contents); + // console.log(eventMessage.data.contents); // send event to bus api return busService.postEvent({ @@ -374,7 +380,7 @@ function handler(topicName, messageJSON, notification) { .catch((err) => { logger.error(`Failed to send ${eventType} event` + `; error: ${err.message}` - + `; with body ${JSON.stringify(eventMessage)} to bus api`); + + `; with body ${JSON.stringify(eventMessage)} to bus api`); }); } }); diff --git a/connect/service.js b/connect/service.js index 10a416c..d4ade02 100644 --- a/connect/service.js +++ b/connect/service.js @@ -232,10 +232,46 @@ const getTopic = (topicId, logger) => ( }) ); +/** + * Get phase details + * + * @param {String} projectId project id + * @param {String} phaseId phase id + * + * @return {Promise} promise resolved to phase details + */ +const getPhase = (projectId, phaseId) => ( + M2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET) + .then((token) => ( + request + .get(`${config.TC_API_V4_BASE_URL}/projects/${projectId}/phases/${phaseId}`) + .set('accept', 'application/json') + .set('authorization', `Bearer ${token}`) + .then((res) => { + if (!_.get(res, 'body.result.success')) { + throw new Error(`Failed to get phase details of project id: ${projectId}, phase id: ${phaseId}`); + } + const project = _.get(res, 'body.result.content'); + return project; + }).catch((err) => { + const errorDetails = _.get(err, 'response.body.result.content.message'); + throw new Error( + `Failed to get phase details of project id: ${projectId}, phase id: ${phaseId}.` + + (errorDetails ? ' Server response: ' + errorDetails : '') + ); + }) + )) + .catch((err) => { + err.message = 'Error generating m2m token: ' + err.message; + throw err; + }) +); + module.exports = { getProject, getRoleMembers, getUsersById, getUsersByHandle, getTopic, + getPhase, }; diff --git a/consumer.js b/consumer.js index 60676ae..5313f0a 100644 --- a/consumer.js +++ b/consumer.js @@ -8,7 +8,7 @@ const _ = require('lodash'); const Kafka = require('no-kafka'); const co = require('co'); global.Promise = require('bluebird'); -const healthcheck = require('topcoder-healthcheck-dropin') +const healthcheck = require('topcoder-healthcheck-dropin'); const logger = require('./src/common/logger'); const models = require('./src/models'); @@ -63,13 +63,13 @@ function startKafkaConsumer() { return co(function* () { // run each handler for (let i = 0; i < ruleSets.length; i += 1) { - const rule = ruleSets[i] - const handlerFuncArr = _.keys(rule) - const handlerFuncName = _.get(handlerFuncArr, "0") + const rule = ruleSets[i]; + const handlerFuncArr = _.keys(rule); + const handlerFuncName = _.get(handlerFuncArr, '0'); try { - const handler = processors[handlerFuncName] - const handlerRuleSets = rule[handlerFuncName] + const handler = processors[handlerFuncName]; + const handlerRuleSets = rule[handlerFuncName]; if (!handler) { logger.error(`Handler ${handlerFuncName} is not defined`); continue; @@ -79,7 +79,7 @@ function startKafkaConsumer() { const notifications = yield handler(messageJSON, handlerRuleSets); if (notifications && notifications.length > 0) { // save notifications in bulk to improve performance - logger.info(`Going to insert ${notifications.length} notifications in database.`) + logger.info(`Going to insert ${notifications.length} notifications in database.`); yield models.Notification.bulkCreate(_.map(notifications, (n) => ({ userId: n.userId, type: n.type || topic, @@ -87,9 +87,9 @@ function startKafkaConsumer() { read: false, seen: false, version: n.version || null, - }))) + }))); // logging - logger.info(`Saved ${notifications.length} notifications`) + logger.info(`Saved ${notifications.length} notifications`); /* logger.info(` for users: ${ _.map(notifications, (n) => n.userId).join(', ') }`); */ @@ -112,15 +112,15 @@ function startKafkaConsumer() { const check = function () { if (!consumer.client.initialBrokers && !consumer.client.initialBrokers.length) { - return false + return false; } - let connected = true + let connected = true; consumer.client.initialBrokers.forEach(conn => { - logger.debug(`url ${conn.server()} - connected=${conn.connected}`) - connected = conn.connected & connected - }) - return connected - } + logger.debug(`url ${conn.server()} - connected=${conn.connected}`); + connected = conn.connected & connected; + }); + return connected; + }; // Start kafka consumer logger.info('Starting kafka consumer'); @@ -132,7 +132,7 @@ function startKafkaConsumer() { }]) .then(() => { logger.info('Kafka consumer initialized successfully'); - healthcheck.init([check]) + healthcheck.init([check]); }) .catch((err) => { logger.error('Kafka consumer failed'); diff --git a/package.json b/package.json index 7e802da..bff9422 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "start": "node connect/connectNotificationServer", "startAPI": "node index-api", "startConsumer": "node consumer", - "lint": "eslint *.js src config test connect || true", - "lint:fix": "eslint *.js --fix src config test connect || true", + "lint": "eslint *.js src config test connect", + "lint:fix": "eslint *.js --fix src config test connect", "postinstall": "npm run build", "build": "gulp build", "watch": "gulp watch" diff --git a/src/app.js b/src/app.js index 3ee3e4a..12826a1 100644 --- a/src/app.js +++ b/src/app.js @@ -15,7 +15,7 @@ const logger = require('./common/logger'); const errors = require('./common/errors'); const models = require('./models'); const Kafka = require('no-kafka'); -const healthcheck = require('topcoder-healthcheck-dropin') +const healthcheck = require('topcoder-healthcheck-dropin'); /** * Start Kafka consumer for event bus events. @@ -77,22 +77,22 @@ function startKafkaConsumer(handlers, notificationServiceHandlers) { const check = function () { if (!consumer.client.initialBrokers && !consumer.client.initialBrokers.length) { - return false + return false; } - let connected = true + let connected = true; consumer.client.initialBrokers.forEach(conn => { - logger.debug(`url ${conn.server()} - connected=${conn.connected}`) - connected = conn.connected & connected - }) - return connected - } + logger.debug(`url ${conn.server()} - connected=${conn.connected}`); + connected = conn.connected & connected; + }); + return connected; + }; consumer .init() .then(() => { _.each(_.keys(handlers), - (topicName) => consumer.subscribe(topicName, dataHandler)) - healthcheck.init([check]) + (topicName) => consumer.subscribe(topicName, dataHandler)); + healthcheck.init([check]); }) .catch((err) => { logger.error('Kafka Consumer failed'); diff --git a/src/common/tcApiHelper.js b/src/common/tcApiHelper.js index d0bc56f..d83e25f 100644 --- a/src/common/tcApiHelper.js +++ b/src/common/tcApiHelper.js @@ -188,8 +188,8 @@ function* notifyUserViaEmail(user, message) { */ function* getChallenge(challengeId) { // this is public API, M2M token is not needed - const url = `${config.TC_API_V4_BASE_URL}/challenges/${challengeId}` - logger.info(`calling public challenge api ${url}`) + const url = `${config.TC_API_V4_BASE_URL}/challenges/${challengeId}`; + logger.info(`calling public challenge api ${url}`); const res = yield request.get(url); if (!_.get(res, 'body.result.success')) { throw new Error(`Failed to get challenge by id ${challengeId}`); @@ -214,9 +214,9 @@ function* notifyUsersOfMessage(users, notification) { for (let i = 0; i < users.length; i += 1) { const user = users[i]; // construct notification, rest fields are set in consumer.js - notifications.push({ userId: user.userId, notification: notification }); + notifications.push({ userId: user.userId, notification }); - /* TODO Sachin disabled this code + /* TODO Sachin disabled this code if (config.ENABLE_EMAILS) { // notify user by email, ignore error in order not to block rest processing try { @@ -226,9 +226,8 @@ function* notifyUsersOfMessage(users, notification) { logger.logFullError(e); } } */ - } - logger.info(`Total ${notifications.length} users would be notified.`) + logger.info(`Total ${notifications.length} users would be notified.`); return notifications; } @@ -238,10 +237,10 @@ function* notifyUsersOfMessage(users, notification) { * @returns {Array} the associated user's detail object */ function* getUsersInfoFromChallenge(challengeId) { - const token = yield getM2MToken() - let usersInfo = [] - const url = `${config.TC_API_V4_BASE_URL}/challenges/${challengeId}/resources` - logger.info(`calling challenge api ${url} `) + const token = yield getM2MToken(); + let usersInfo = []; + const url = `${config.TC_API_V4_BASE_URL}/challenges/${challengeId}/resources`; + logger.info(`calling challenge api ${url} `); const res = yield request .get(url) .set('Authorization', `Bearer ${token}`) @@ -250,71 +249,73 @@ function* getUsersInfoFromChallenge(challengeId) { throw new Error( `Error in call challenge api by id ${challengeId}` + (errorDetails ? ' Server response: ' + errorDetails : '') - ) - }) + ); + }); if (!_.get(res, 'body.result.success')) { throw new Error(`Failed to get challenge by id ${challengeId}`); } usersInfo = _.get(res, 'body.result.content'); - logger.info(`Feteched ${usersInfo.length} records from challenge api`) + logger.info(`Feteched ${usersInfo.length} records from challenge api`); return usersInfo; } -/** - * Filter associated challenge's user based on criteria +/** + * Filter associated challenge's user based on criteria * @param {Array} usersInfo user object array * @param {Array} filterOnRoles on roles * @param {Array} filterOnUsers on user's ids - * - * @returns {Array} of user object + * + * @returns {Array} of user object */ function filterChallengeUsers(usersInfo, filterOnRoles = [], filterOnUsers = []) { - const users = [] // filtered users - const rolesAvailable = [] // available roles in challenge api response + const users = []; // filtered users + const rolesAvailable = []; // available roles in challenge api response _.map(usersInfo, (user) => { - const userId = parseInt(_.get(user, 'properties.External Reference ID')) - const role = _.get(user, 'role') + const userId = parseInt(_.get(user, 'properties.External Reference ID'), 10); + const role = _.get(user, 'role'); - _.indexOf(rolesAvailable, role) == -1 ? rolesAvailable.push(role) : '' + if (_.indexOf(rolesAvailable, role) === -1) { + rolesAvailable.push(role); + } if (filterOnRoles.length > 0 && _.indexOf(filterOnRoles, role) >= 0) { - users.push({ userId: userId }) + users.push({ userId }); } else if (filterOnUsers.length > 0 && _.indexOf(filterOnUsers, userId) >= 0) { - users.push({ userId: userId }) /** Submitter only case */ - } else if (filterOnRoles.length == 0 && filterOnUsers.length == 0) { - users.push({ userId: userId }) + users.push({ userId }); /** Submitter only case */ + } else if (filterOnRoles.length === 0 && filterOnUsers.length === 0) { + users.push({ userId }); } - }) - logger.info(`Total roles available in this challenge are: ${rolesAvailable.join(',')}`) - return users + }); + logger.info(`Total roles available in this challenge are: ${rolesAvailable.join(',')}`); + return users; } -/** - * modify notification template +/** + * modify notification template * @param {Object} ruleSet rule - * @param {Object} data values to be filled - * + * @param {Object} data values to be filled + * * @returns {Object} notification node */ function* modifyNotificationNode(ruleSet, data) { - const notification = _.get(ruleSet, "notification") - const id = data.id || data.challengeId || 0 - const name = _.get(data, "name") + const notification = _.get(ruleSet, 'notification'); + const id = data.id || data.challengeId || 0; + const name = _.get(data, 'name'); - notification.id = id + notification.id = id; if (name) { - notification.name = name + notification.name = name; } else { try { - const challenge = yield getChallenge(id) - notification.name = _.get(challenge, "challengeTitle") + const challenge = yield getChallenge(id); + notification.name = _.get(challenge, 'challengeTitle'); } catch (error) { - notification.name = '' - logger.error(`Error in fetching challenge detail : ${error}`) + notification.name = ''; + logger.error(`Error in fetching challenge detail : ${error}`); } } - return notification + return notification; } module.exports = { diff --git a/src/processors/challenge/AutoPilotHandler.js b/src/processors/challenge/AutoPilotHandler.js index 47d6e7e..b3592ec 100644 --- a/src/processors/challenge/AutoPilotHandler.js +++ b/src/processors/challenge/AutoPilotHandler.js @@ -8,7 +8,7 @@ const service = require('../../services/AutoPilotService'); * Handle Kafka JSON message of autopilot. * * @param {Object} message the Kafka JSON message - * @param {Object} ruleSets + * @param {Object} ruleSets * * @return {Promise} promise resolved to notifications */ diff --git a/src/processors/challenge/ChallengeHandler.js b/src/processors/challenge/ChallengeHandler.js index 39e60ec..f2056a2 100644 --- a/src/processors/challenge/ChallengeHandler.js +++ b/src/processors/challenge/ChallengeHandler.js @@ -8,7 +8,7 @@ const service = require('../../services/ChallengeService'); * Handle Kafka JSON message of challenge created. * * @param {Object} message the Kafka JSON message - * @param {Object} ruleSets + * @param {Object} ruleSets * * @return {Promise} promise resolved to notifications */ diff --git a/src/processors/challenge/SubmissionHandler.js b/src/processors/challenge/SubmissionHandler.js index 9850563..78b048c 100644 --- a/src/processors/challenge/SubmissionHandler.js +++ b/src/processors/challenge/SubmissionHandler.js @@ -8,7 +8,7 @@ const service = require('../../services/SubmissionService'); * Handle Kafka JSON message of autopilot. * * @param {Object} message the Kafka JSON message - * @param {Object} ruleSets + * @param {Object} ruleSets * * @return {Promise} promise resolved to notifications */ diff --git a/src/processors/index.js b/src/processors/index.js index af3844c..70d8c6a 100644 --- a/src/processors/index.js +++ b/src/processors/index.js @@ -6,8 +6,8 @@ const ChallengeCreatedHandler = require('./challenge/ChallengeCreatedHandler'); const ChallengePhaseWarningHandler = require('./challenge/ChallengePhaseWarningHandler'); const ChallengeHandler = require('./challenge/ChallengeHandler'); -const AutoPilotHandler = require('./challenge/AutoPilotHandler') -const SubmissionHandler = require('./challenge/SubmissionHandler') +const AutoPilotHandler = require('./challenge/AutoPilotHandler'); +const SubmissionHandler = require('./challenge/SubmissionHandler'); // Exports module.exports = { diff --git a/src/services/AutoPilotService.js b/src/services/AutoPilotService.js index 6561daa..cd0ee9a 100644 --- a/src/services/AutoPilotService.js +++ b/src/services/AutoPilotService.js @@ -5,32 +5,31 @@ 'use strict'; const joi = require('joi'); -const _ = require('lodash') +const _ = require('lodash'); const logger = require('../common/logger'); const tcApiHelper = require('../common/tcApiHelper'); /** * Handle autopilot message * @param {Object} message the Kafka message - * @param {Object} ruleSets + * @param {Object} ruleSets * @returns {Array} the notifications */ function* handle(message, ruleSets) { + if ((message.payload.phaseTypeName === _.get(ruleSets, 'phaseTypeName')) + && (message.payload.state === _.get(ruleSets, 'state'))) { + const challengeId = message.payload.projectId; + const filerOnRoles = _.get(ruleSets, 'roles'); - if ((message.payload.phaseTypeName === _.get(ruleSets, "phaseTypeName")) - && (message.payload.state === _.get(ruleSets, "state"))) { - const challengeId = message.payload.projectId - const filerOnRoles = _.get(ruleSets, "roles") + const notification = yield tcApiHelper.modifyNotificationNode(ruleSets, { id: challengeId }); + const usersInfo = yield tcApiHelper.getUsersInfoFromChallenge(challengeId); + const users = tcApiHelper.filterChallengeUsers(usersInfo, filerOnRoles); - const notification = yield tcApiHelper.modifyNotificationNode(ruleSets, { id: challengeId}) - const usersInfo = yield tcApiHelper.getUsersInfoFromChallenge(challengeId) - const users = tcApiHelper.filterChallengeUsers(usersInfo, filerOnRoles) - - logger.info(`Successfully filetered ${users.length} users on rulesets ${JSON.stringify(filerOnRoles)} `) + logger.info(`Successfully filetered ${users.length} users on rulesets ${JSON.stringify(filerOnRoles)} `); // notify users of message return yield tcApiHelper.notifyUsersOfMessage(users, notification); } - return {} + return {}; } handle.schema = { @@ -42,15 +41,15 @@ handle.schema = { payload: joi.object().keys({ phaseTypeName: joi.string().required(), state: joi.string().required(), - projectId: joi.number().integer().min(1) + projectId: joi.number().integer().min(1), }).unknown(true).required(), }).required(), - ruleSets: joi.object() -} + ruleSets: joi.object(), +}; // Exports module.exports = { handle, -} +}; logger.buildService(module.exports); diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index 38d6e37..8c3dbb3 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -5,31 +5,30 @@ 'use strict'; const joi = require('joi'); -const _ = require('lodash') +const _ = require('lodash'); const logger = require('../common/logger'); const tcApiHelper = require('../common/tcApiHelper'); /** * Handle challenge message * @param {Object} message the Kafka message - * @param {Object} ruleSets + * @param {Object} ruleSets * @returns {Array} the notifications */ function* handle(message, ruleSets) { - - if (message.payload.type === _.get(ruleSets, "type")) { - const challengeId = message.payload.data.id - const filterOnRoles = _.get(ruleSets, "roles") - const challengeTitle = _.get(message.payload, "data.name") - - const notification = yield tcApiHelper.modifyNotificationNode(ruleSets, { id: challengeId, name: challengeTitle }) - const usersInfo = yield tcApiHelper.getUsersInfoFromChallenge(challengeId) - const users = tcApiHelper.filterChallengeUsers(usersInfo, filterOnRoles) - logger.info(`Successfully filetered ${users.length} users on rulesets ${JSON.stringify(filterOnRoles)} `) + if (message.payload.type === _.get(ruleSets, 'type')) { + const challengeId = message.payload.data.id; + const filterOnRoles = _.get(ruleSets, 'roles'); + const challengeTitle = _.get(message.payload, 'data.name'); + + const notification = yield tcApiHelper.modifyNotificationNode(ruleSets, { id: challengeId, name: challengeTitle }); + const usersInfo = yield tcApiHelper.getUsersInfoFromChallenge(challengeId); + const users = tcApiHelper.filterChallengeUsers(usersInfo, filterOnRoles); + logger.info(`Successfully filetered ${users.length} users on rulesets ${JSON.stringify(filterOnRoles)} `); // notify users of message return yield tcApiHelper.notifyUsersOfMessage(users, notification); } - return {} + return {}; } handle.schema = { @@ -40,15 +39,15 @@ handle.schema = { 'mime-type': joi.string().required(), payload: joi.object().keys({ type: joi.string().required(), - userId: joi.number().integer().min(1) + userId: joi.number().integer().min(1), }).unknown(true).required(), }).required(), - ruleSets: joi.object() -} + ruleSets: joi.object(), +}; // Exports module.exports = { handle, -} +}; logger.buildService(module.exports); diff --git a/src/services/SubmissionService.js b/src/services/SubmissionService.js index b3737dd..14f5e71 100644 --- a/src/services/SubmissionService.js +++ b/src/services/SubmissionService.js @@ -5,36 +5,35 @@ 'use strict'; const joi = require('joi'); -const _ = require('lodash') +const _ = require('lodash'); const logger = require('../common/logger'); const tcApiHelper = require('../common/tcApiHelper'); /** * Handle submission message * @param {Object} message the Kafka message - * @param {Object} ruleSets + * @param {Object} ruleSets * @returns {Array} the notifications */ function* handle(message, ruleSets) { + if (message.payload.resource === _.get(ruleSets, 'resource')) { + const challengeId = message.payload.challengeId; + const filterOnRoles = _.get(ruleSets, 'roles'); - if (message.payload.resource === _.get(ruleSets, "resource")) { - const challengeId = message.payload.challengeId - const filterOnRoles = _.get(ruleSets, "roles") - - const filterOnUsers = [] + const filterOnUsers = []; if (_.get(ruleSets, 'selfOnly')) { - const memberId = _.get(message.payload, "memberId") - filterOnUsers.push(memberId) + const memberId = _.get(message.payload, 'memberId'); + filterOnUsers.push(memberId); } - const usersInfo = yield tcApiHelper.getUsersInfoFromChallenge(challengeId) - const users = tcApiHelper.filterChallengeUsers(usersInfo, filterOnRoles, filterOnUsers) - const notification = yield tcApiHelper.modifyNotificationNode(ruleSets, { id: challengeId}) - logger.info(`Successfully filetered ${users.length} users on rulesets ${JSON.stringify(filterOnRoles)} `) + const usersInfo = yield tcApiHelper.getUsersInfoFromChallenge(challengeId); + const users = tcApiHelper.filterChallengeUsers(usersInfo, filterOnRoles, filterOnUsers); + const notification = yield tcApiHelper.modifyNotificationNode(ruleSets, { id: challengeId }); + logger.info(`Successfully filetered ${users.length} users on rulesets ${JSON.stringify(filterOnRoles)} `); // notify users of message return yield tcApiHelper.notifyUsersOfMessage(users, notification); } - return {} + return {}; } handle.schema = { @@ -44,15 +43,15 @@ handle.schema = { timestamp: joi.date().required(), 'mime-type': joi.string().required(), payload: joi.object().keys({ - resource: joi.string().required() + resource: joi.string().required(), }).unknown(true).required(), }).required(), - ruleSets: joi.object() -} + ruleSets: joi.object(), +}; // Exports module.exports = { handle, -} +}; logger.buildService(module.exports);