Skip to content

Commit cd5b535

Browse files
committed
part of the winning submission from challenge 30090377 - Topcoder Notifications Service - Skip unnecessary notifications
this part contains changes to implement new functionality
1 parent 3d20925 commit cd5b535

File tree

4 files changed

+161
-1
lines changed

4 files changed

+161
-1
lines changed

connect/connectNotificationServer.js

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const config = require('./config');
99
const notificationServer = require('../index');
1010
const _ = require('lodash');
1111
const service = require('./service');
12+
const helpers = require('./helpers');
1213
const { BUS_API_EVENT } = require('./constants');
1314
const EVENTS = require('./events-config').EVENTS;
1415
const PROJECT_ROLE_RULES = require('./events-config').PROJECT_ROLE_RULES;
@@ -246,6 +247,83 @@ const getNotificationsForTopicStarter = (eventConfig, topicId) => {
246247
});
247248
};
248249

250+
/**
251+
* Filter members by project roles
252+
*
253+
* @params {Array} List of project roles
254+
* @params {Array} List of project members
255+
*
256+
* @returns {Array} List of objects with user ids
257+
*/
258+
const filterMembersByRoles = (roles, members) => {
259+
let result = [];
260+
261+
roles.forEach(projectRole => {
262+
result = result.concat(
263+
_.filter(members, PROJECT_ROLE_RULES[projectRole])
264+
.map(projectMember => ({
265+
userId: projectMember.userId.toString(),
266+
}))
267+
);
268+
});
269+
270+
return result;
271+
};
272+
273+
/**
274+
* Exclude private posts notification
275+
*
276+
* @param {Object} eventConfig event configuration
277+
* @param {Object} project project details
278+
* @param {Array} tags list of message tags
279+
*
280+
* @return {Promise} resolves to a list of notifications
281+
*/
282+
const getExcludedPrivatePostNotifications = (eventConfig, project, tags) => {
283+
// skip if message is not private or exclusion rule is not configured
284+
if (!_.includes(tags, 'MESSAGES') || !eventConfig.privatePostsForProjectRoles) {
285+
return Promise.resolve([]);
286+
}
287+
288+
const members = _.get(project, 'members', []);
289+
const notifications = filterMembersByRoles(eventConfig.privatePostsForProjectRoles, members);
290+
291+
return Promise.resolve(notifications);
292+
};
293+
294+
/**
295+
* Exclude notifications about posts inside draft phases
296+
*
297+
* @param {Object} eventConfig event configuration
298+
* @param {Object} project project details
299+
* @param {Array} tags list of message tags
300+
*
301+
* @return {Promise} resolves to a list of notifications
302+
*/
303+
const getExcludeDraftPhasesNotifications = (eventConfig, project, tags) => {
304+
// skip is no exclusion rule is configured
305+
if (!eventConfig.draftPhasesForProjectRoles) {
306+
return Promise.resolve([]);
307+
}
308+
309+
const phaseId = helpers.extractPhaseId(tags);
310+
// skip if it is not phase notification
311+
if (!phaseId) {
312+
return Promise.resolve([]);
313+
}
314+
315+
// exclude all user with configured roles if phase is in draft state
316+
return service.getPhase(project.id, phaseId)
317+
.then((phase) => {
318+
if (phase.status === 'draft') {
319+
const members = _.get(project, 'members', []);
320+
const notifications = filterMembersByRoles(eventConfig.draftPhasesForProjectRoles, members);
321+
322+
return Promise.resolve(notifications);
323+
}
324+
});
325+
};
326+
249327
/**
250328
* Exclude notifications using exclude rules of the event config
251329
*
@@ -272,12 +350,17 @@ const excludeNotifications = (notifications, eventConfig, message, data) => {
272350
// and after filter out such notifications from the notifications list
273351
// TODO move this promise all together with `_.uniqBy` to one function
274352
// and reuse it here and in `handler` function
353+
const tags = _.get(message, 'tags', []);
354+
275355
return Promise.all([
276356
getNotificationsForTopicStarter(excludeEventConfig, message.topicId),
277357
getNotificationsForUserId(excludeEventConfig, message.userId),
278-
getNotificationsForMentionedUser(eventConfig, message.postContent),
358+
getNotificationsForMentionedUser(excludeEventConfig, message.postContent),
279359
getProjectMembersNotifications(excludeEventConfig, project),
280360
getTopCoderMembersNotifications(excludeEventConfig),
361+
// these are special exclude rules which are only working for excluding notifications but not including
362+
getExcludedPrivatePostNotifications(excludeEventConfig, project, tags),
363+
getExcludeDraftPhasesNotifications(excludeEventConfig, project, tags),
281364
]).then((notificationsPerSource) => (
282365
_.uniqBy(_.flatten(notificationsPerSource), 'userId')
283366
)).then((excludedNotifications) => {

connect/events-config.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const { BUS_API_EVENT } = require('./constants');
55

66
// project member role names
77
const PROJECT_ROLE_OWNER = 'owner';
8+
const PROJECT_ROLE_CUSTOMER = 'customer';
89
const PROJECT_ROLE_COPILOT = 'copilot';
910
const PROJECT_ROLE_MANAGER = 'manager';
1011
const PROJECT_ROLE_MEMBER = 'member';
@@ -13,13 +14,15 @@ const PROJECT_ROLE_ACCOUNT_MANAGER = 'account_manager';
1314
// project member role rules
1415
const PROJECT_ROLE_RULES = {
1516
[PROJECT_ROLE_OWNER]: { role: 'customer', isPrimary: true },
17+
[PROJECT_ROLE_CUSTOMER]: { role: 'customer' },
1618
[PROJECT_ROLE_COPILOT]: { role: 'copilot' },
1719
[PROJECT_ROLE_MANAGER]: { role: 'manager' },
1820
[PROJECT_ROLE_ACCOUNT_MANAGER]: { role: 'account_manager' },
1921
[PROJECT_ROLE_MEMBER]: {},
2022
};
2123

2224
// TopCoder roles
25+
// eslint-disable-next-line no-unused-vars
2326
const ROLE_CONNECT_COPILOT = 'Connect Copilot';
2427
const ROLE_CONNECT_MANAGER = 'Connect Manager';
2528
const ROLE_CONNECT_COPILOT_MANAGER = 'Connect Copilot Manager';
@@ -123,31 +126,53 @@ const EVENTS = [
123126
version: 2,
124127
projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, PROJECT_ROLE_MEMBER],
125128
toMentionedUsers: true,
129+
exclude: {
130+
privatePostsForProjectRoles: [PROJECT_ROLE_CUSTOMER],
131+
},
126132
}, {
127133
type: BUS_API_EVENT.CONNECT.POST.CREATED,
128134
version: 2,
129135
projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, PROJECT_ROLE_MEMBER],
130136
toTopicStarter: true,
131137
toMentionedUsers: true,
138+
exclude: {
139+
draftPhasesForProjectRoles: [PROJECT_ROLE_CUSTOMER],
140+
privatePostsForProjectRoles: [PROJECT_ROLE_CUSTOMER],
141+
},
132142
}, {
133143
type: BUS_API_EVENT.CONNECT.POST.UPDATED,
134144
version: 2,
135145
projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, PROJECT_ROLE_MEMBER],
136146
toTopicStarter: true,
137147
toMentionedUsers: true,
148+
exclude: {
149+
draftPhasesForProjectRoles: [PROJECT_ROLE_CUSTOMER],
150+
privatePostsForProjectRoles: [PROJECT_ROLE_CUSTOMER],
151+
},
138152
}, {
139153
type: BUS_API_EVENT.CONNECT.POST.MENTION,
154+
exclude: {
155+
draftPhasesForProjectRoles: [PROJECT_ROLE_CUSTOMER],
156+
privatePostsForProjectRoles: [PROJECT_ROLE_CUSTOMER],
157+
},
140158
},
141159
{
142160
type: BUS_API_EVENT.CONNECT.TOPIC.DELETED,
143161
version: 2,
144162
projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, PROJECT_ROLE_MEMBER],
145163
toTopicStarter: false,
164+
exclude: {
165+
privatePostsForProjectRoles: [PROJECT_ROLE_CUSTOMER],
166+
},
146167
},
147168
{
148169
type: BUS_API_EVENT.CONNECT.POST.DELETED,
149170
version: 2,
150171
projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, PROJECT_ROLE_MEMBER],
172+
exclude: {
173+
draftPhasesForProjectRoles: [PROJECT_ROLE_CUSTOMER],
174+
privatePostsForProjectRoles: [PROJECT_ROLE_CUSTOMER],
175+
},
151176
},
152177
{
153178
type: BUS_API_EVENT.CONNECT.PROJECT.LINK_CREATED,

connect/helpers.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
* Helper functions
33
*/
44
const Remarkable = require('remarkable');
5+
const _ = require('lodash');
6+
7+
const PHASE_ID_REGEXP = /phase#(\d+)/;
58

69
/**
710
* Convert markdown into raw draftjs state
@@ -42,7 +45,20 @@ const sanitizeEmail = (email) => {
4245
return '';
4346
};
4447

48+
/**
49+
* Helper method to extract phaseId from tag
50+
*
51+
* @param {Array} tags list of message tags
52+
*
53+
* @returns {String} phase id
54+
*/
55+
const extractPhaseId = (tags) => {
56+
const phaseIds = tags.map((tag) => _.get(tag.match(PHASE_ID_REGEXP), '1', null));
57+
return _.find(phaseIds, (phaseId) => phaseId !== null);
58+
};
59+
4560
module.exports = {
4661
markdownToHTML,
4762
sanitizeEmail,
63+
extractPhaseId,
4864
};

connect/service.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,10 +232,46 @@ const getTopic = (topicId, logger) => (
232232
})
233233
);
234234

235+
/**
236+
* Get phase details
237+
*
238+
* @param {String} projectId project id
239+
* @param {String} phaseId phase id
240+
*
241+
* @return {Promise} promise resolved to phase details
242+
*/
243+
const getPhase = (projectId, phaseId) => (
244+
M2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET)
245+
.then((token) => (
246+
request
247+
.get(`${config.TC_API_V4_BASE_URL}/projects/${projectId}/phases/${phaseId}`)
248+
.set('accept', 'application/json')
249+
.set('authorization', `Bearer ${token}`)
250+
.then((res) => {
251+
if (!_.get(res, 'body.result.success')) {
252+
throw new Error(`Failed to get phase details of project id: ${projectId}, phase id: ${phaseId}`);
253+
}
254+
const project = _.get(res, 'body.result.content');
255+
return project;
256+
}).catch((err) => {
257+
const errorDetails = _.get(err, 'response.body.result.content.message');
258+
throw new Error(
259+
`Failed to get phase details of project id: ${projectId}, phase id: ${phaseId}.` +
260+
(errorDetails ? ' Server response: ' + errorDetails : '')
261+
);
262+
})
263+
))
264+
.catch((err) => {
265+
err.message = 'Error generating m2m token: ' + err.message;
266+
throw err;
267+
})
268+
);
269+
235270
module.exports = {
236271
getProject,
237272
getRoleMembers,
238273
getUsersById,
239274
getUsersByHandle,
240275
getTopic,
276+
getPhase,
241277
};

0 commit comments

Comments
 (0)