diff --git a/.circleci/config.yml b/.circleci/config.yml index 7bc897d..6bb70a8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -102,7 +102,7 @@ workflows: context : org-global filters: branches: - only: [dev, 'hotfix/V5-API-Standards', 'v5-upgrade', 'feature/bulk-notification'] + only: [dev, 'bug/rate-limit'] - "build-prod": context : org-global filters: diff --git a/config/default.js b/config/default.js index 904b068..9563efe 100644 --- a/config/default.js +++ b/config/default.js @@ -70,35 +70,50 @@ module.exports = { { phaseTypeName: 'Checkpoint Screening', state: 'START', - roles: ['Copilot', 'Reviewer'], + roles: ['Primary Screener'], notification: { id: 0, /** challengeid or projectid */ name: '', /** challenge name */ group: 'challenge', - title: 'Challenge checkpoint review.', + title: 'Checkpoint Screening phase is now open. You may now begin screening checkpoint submissions.', }, }, - }, - ], - 'submission.notification.create': [ - { - handleSubmission: + }, { + handleAutoPilot: { - resource: 'submission', - roles: ['Copilot', 'Reviewer'], - selfOnly: true /** Submitter only */, + phaseTypeName: 'Checkpoint Review', + state: 'START', + roles: ['Copilot'], notification: { id: 0, /** challengeid or projectid */ name: '', /** challenge name */ - group: 'submission', - title: 'A new submission is uploaded.', + group: 'challenge', + title: 'Checkpoint Review is now open. You may now begin reviewing checkpoint submissions.', }, }, }, ], - 'admin.notification.broadcast' : [{ + // /** 'submission.notification.create': [ + // { + // handleSubmission: + // { + // resource: 'submission', + // roles: ['Copilot', 'Reviewer'], + // selfOnly: true /** Submitter only */, + // notification: + // { + // id: 0, /** challengeid or projectid */ + // name: '', /** challenge name */ + // group: 'submission', + // title: 'A new submission is uploaded.', + // }, + // }, + // }, + // ], + // */ // issue - https://github.com/topcoder-platform/community-app/issues/4108 + 'admin.notification.broadcast': [{ handleBulkNotification: {} } ] @@ -112,5 +127,5 @@ module.exports = { ENABLE_DEV_MODE: process.env.ENABLE_DEV_MODE ? Boolean(process.env.ENABLE_DEV_MODE) : true, DEV_MODE_EMAIL: process.env.DEV_MODE_EMAIL, DEFAULT_REPLY_EMAIL: process.env.DEFAULT_REPLY_EMAIL, - ENABLE_HOOK_BULK_NOTIFICATION : process.env.ENABLE_HOOK_BULK_NOTIFICATION || false, + ENABLE_HOOK_BULK_NOTIFICATION: process.env.ENABLE_HOOK_BULK_NOTIFICATION || false, }; diff --git a/package.json b/package.json index c7f4382..89da03b 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,8 @@ "tc-core-library-js": "appirio-tech/tc-core-library-js.git#v2.6.2", "topcoder-healthcheck-dropin": "^1.0.3", "urijs": "^1.19.1", - "winston": "^2.2.0" + "winston": "^2.2.0", + "node-cache": "^5.1.0" }, "engines": { "node": "6.x" diff --git a/src/common/broadcastAPIHelper.js b/src/common/broadcastAPIHelper.js index b196cf4..32c4988 100644 --- a/src/common/broadcastAPIHelper.js +++ b/src/common/broadcastAPIHelper.js @@ -6,10 +6,14 @@ const _ = require('lodash') const config = require('config') const request = require('superagent') const logger = require('./logger') -const m2mAuth = require('tc-core-library-js').auth.m2m; -const m2m = m2mAuth(config); +const m2mAuth = require('tc-core-library-js').auth.m2m +const NodeCache = require('node-cache') + +const m2m = m2mAuth(config) +const cache = new NodeCache() const logPrefix = "BroadcastAPI: " +const cachedTimeInSeconds = 300 //300 seconds /** * Helper Function - get m2m token @@ -27,6 +31,11 @@ async function getMemberInfo(userId) { "/members/_search/?" + `query=userId%3A${userId}` + `&limit=1` + if (cachedMemberInfo = cache.get(url)) { + return new Promise((resolve, reject) => { + resolve(cachedMemberInfo) + }) + } return new Promise(async function (resolve, reject) { let memberInfo = [] logger.info(`calling member api ${url} `) @@ -37,6 +46,7 @@ async function getMemberInfo(userId) { } memberInfo = _.get(res, 'body.result.content') logger.info(`BCA Memeber API: Feteched ${memberInfo.length} record(s) from member api`) + cache.set(url, memberInfo, cachedTimeInSeconds) resolve(memberInfo) } catch (err) { reject(new Error(`BCA Memeber API: Failed to get member ` + @@ -102,12 +112,16 @@ async function callApi(url, machineToken) { /** * Helper function - check Skills and Tracks condition + * + * @param {Integer} userId + * @param {Object} bulkMessage + * @param {Object} m memberInfo + * */ -async function checkUserSkillsAndTracks(userId, bulkMessage) { +async function checkUserSkillsAndTracks(userId, bulkMessage, m) { try { const skills = _.get(bulkMessage, 'recipients.skills') const tracks = _.get(bulkMessage, 'recipients.tracks') - const m = await getMemberInfo(userId) let skillMatch, trackMatch = false // default if (skills && skills.length > 0) { const ms = _.get(m[0], "skills") // get member skills @@ -159,25 +173,43 @@ async function checkUserSkillsAndTracks(userId, bulkMessage) { /** * Helper function - check group condition */ -async function checkUserGroup(userId, bulkMessage) { +async function checkUserGroup(userId, bulkMessage, userGroupInfo) { try { + const excludeGroupSign = '!' const groups = _.get(bulkMessage, 'recipients.groups') + let flag = false // default - const userGroupInfo = await getUserGroup(userId) - if (groups.length > 0) { + let excludeGroups = [] + let includeGroups = [] + + _.map(groups, (g) => { + if (_.startsWith(g, excludeGroupSign)) { + excludeGroups.push(g) + } else { + includeGroups.push(g) + } + }) + + if (includeGroups.length > 0) { _.map(userGroupInfo, (o) => { // particular group only condition - flag = (_.indexOf(groups, _.get(o, "name")) >= 0) ? true : flag + flag = (_.indexOf(includeGroups, _.get(o, "name")) >= 0) ? true : flag }) - } else { // no group condition means its for `public` no private group + } + if (excludeGroups.length > 0) { flag = true // default allow for all _.map(userGroupInfo, (o) => { - // not allow if user is part of any private group - flag = (_.get(o, "privateGroup")) ? false : flag + // not allow if user is part of any private group i.e. excludeGroups + flag = (_.indexOf(excludeGroups, (excludeGroupSign + _.get(o, "name"))) >= 0) ? false : flag }) logger.info(`public group condition for userId ${userId}` + ` and BC messageId ${bulkMessage.id}, the result is: ${flag}`) } + + if (groups.length === 0) { + flag = true // no restriction + } + return flag } catch (e) { throw new Error(`checkUserGroup(): ${e}`) @@ -189,12 +221,16 @@ async function checkUserGroup(userId, bulkMessage) { * * @param {Integer} userId * @param {Object} bulkMessage + * @param {Object} memberInfo + * @param {Object} userGroupInfo + * + * @return Promise */ -async function checkBroadcastMessageForUser(userId, bulkMessage) { +async function checkBroadcastMessageForUser(userId, bulkMessage, memberInfo, userGroupInfo) { return new Promise(function (resolve, reject) { Promise.all([ - checkUserSkillsAndTracks(userId, bulkMessage), - checkUserGroup(userId, bulkMessage), + checkUserSkillsAndTracks(userId, bulkMessage, memberInfo), + checkUserGroup(userId, bulkMessage, userGroupInfo), ]).then((results) => { let flag = true // TODO need to be sure about default value _.map(results, (r) => { @@ -214,4 +250,6 @@ async function checkBroadcastMessageForUser(userId, bulkMessage) { module.exports = { checkBroadcastMessageForUser, + getMemberInfo, + getUserGroup, } \ No newline at end of file diff --git a/src/common/tcApiHelper.js b/src/common/tcApiHelper.js index dcd7489..221125b 100644 --- a/src/common/tcApiHelper.js +++ b/src/common/tcApiHelper.js @@ -288,7 +288,7 @@ function filterChallengeUsers(usersInfo, filterOnRoles = [], filterOnUsers = []) } }); logger.info(`Total roles available in this challenge are: ${rolesAvailable.join(',')}`); - return users; + return _.uniqBy(users, 'userId'); } /** diff --git a/src/hooks/hookBulkMessage.js b/src/hooks/hookBulkMessage.js index cacbe44..b4006f1 100644 --- a/src/hooks/hookBulkMessage.js +++ b/src/hooks/hookBulkMessage.js @@ -65,9 +65,17 @@ async function syncBulkMessageForUser(userId) { " LEFT OUTER JOIN (SELECT id as refid, bulk_message_id " + " FROM bulk_message_user_refs AS bmur WHERE bmur.user_id=$1)" + " AS b ON a.id=b.bulk_message_id WHERE b.refid IS NULL" + let memberInfo = [] + let userGroupInfo = [] models.sequelize.query(q, { bind: [userId] }) - .then(function (res) { - Promise.all(res[0].map((r) => isBroadCastMessageForUser(userId, r))) + .then(async function (res) { + try { + memberInfo = await api.getMemberInfo(userId) + userGroupInfo = await api.getUserGroup(userId) + } catch (e) { + reject(`${logPrefix} Failed to get member/group info: ${e}`) + } + Promise.all(res[0].map((r) => isBroadCastMessageForUser(userId, r, memberInfo, userGroupInfo))) .then((results) => { Promise.all(results.map((o) => { if (o.result) { @@ -94,9 +102,13 @@ async function syncBulkMessageForUser(userId) { * Check if current user in broadcast recipent group * @param {Integer} userId * @param {Object} bulkMessage + * @param {Object} memberInfo + * @param {Object} userGroupInfo + * + * @retun promise */ -async function isBroadCastMessageForUser(userId, bulkMessage) { - return api.checkBroadcastMessageForUser(userId, bulkMessage) +async function isBroadCastMessageForUser(userId, bulkMessage, memberInfo, userGroupInfo) { + return api.checkBroadcastMessageForUser(userId, bulkMessage, memberInfo, userGroupInfo) } /**