Skip to content

Commit 0300c26

Browse files
authored
Merge pull request #138 from topcoder-platform/dev
Supporting Release For Connect 2.4.12
2 parents 9759f9d + 891d4b8 commit 0300c26

32 files changed

+1550
-1087
lines changed

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ workflows:
102102
context : org-global
103103
filters:
104104
branches:
105-
only: [dev, 'feature/general-purpose-notifications-usage']
105+
only: [dev, 'hotfix/V5-API-Standards']
106106
- "build-prod":
107107
context : org-global
108108
filters:

config/default.js

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ module.exports = {
3232
TC_API_V4_BASE_URL: process.env.TC_API_V4_BASE_URL || '',
3333
TC_API_V5_BASE_URL: process.env.TC_API_V5_BASE_URL || '',
3434
API_CONTEXT_PATH: process.env.API_CONTEXT_PATH || '/v5/notifications',
35+
TC_API_BASE_URL: process.env.TC_API_BASE_URL || '',
3536

3637
// Configuration for generating machine to machine auth0 token.
3738
// The token will be used for calling another internal API.
@@ -45,56 +46,57 @@ module.exports = {
4546
AUTH0_PROXY_SERVER_URL: process.env.AUTH0_PROXY_SERVER_URL,
4647

4748
KAFKA_CONSUMER_RULESETS: {
48-
// key is Kafka topic name, value is array of ruleset which have key as handler function name defined in src/processors/index.js
49+
// key is Kafka topic name, value is array of ruleset which have key as
50+
// handler function name defined in src/processors/index.js
4951
'challenge.notification.events': [
5052
{
5153
handleChallenge: /** topic handler name */
5254
{
5355
type: 'UPDATE_DRAFT_CHALLENGE',
54-
roles: ["Submitter" /** Competitor */, "Copilot", "Reviewer"],
56+
roles: ['Submitter' /** Competitor */, 'Copilot', 'Reviewer'],
5557
notification:
5658
{
5759
id: 0, /** challengeid or projectid */
5860
name: '', /** challenge name */
5961
group: 'Challenge',
60-
title: 'Challenge specification is modified.'
61-
}
62-
}
63-
}
62+
title: 'Challenge specification is modified.',
63+
},
64+
},
65+
},
6466
],
6567
'notifications.autopilot.events': [
6668
{
6769
handleAutoPilot:
6870
{
6971
phaseTypeName: 'Checkpoint Screening',
7072
state: 'START',
71-
roles: ["Copilot", "Reviewer"],
73+
roles: ['Copilot', 'Reviewer'],
7274
notification:
7375
{
7476
id: 0, /** challengeid or projectid */
7577
name: '', /** challenge name */
7678
group: 'Challenge',
77-
title: 'Challenge checkpoint review.'
78-
}
79-
}
80-
}
79+
title: 'Challenge checkpoint review.',
80+
},
81+
},
82+
},
8183
],
8284
'submission.notification.create': [
8385
{
8486
handleSubmission:
8587
{
8688
resource: 'submission',
87-
roles: ["Copilot", "Reviewer"],
89+
roles: ['Copilot', 'Reviewer'],
8890
selfOnly: true /** Submitter only */,
8991
notification:
9092
{
9193
id: 0, /** challengeid or projectid */
9294
name: '', /** challenge name */
9395
group: 'Submission',
94-
title: 'A new submission is uploaded.'
95-
}
96-
}
97-
}
96+
title: 'A new submission is uploaded.',
97+
},
98+
},
99+
},
98100
],
99101
//'notifications.community.challenge.created': ['handleChallengeCreated'],
100102
//'notifications.community.challenge.phasewarning': ['handleChallengePhaseWarning'],

connect/connectNotificationServer.js

Lines changed: 109 additions & 25 deletions
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;
@@ -85,27 +86,28 @@ const getNotificationsForMentionedUser = (logger, eventConfig, content) => {
8586
// only one per userHandle
8687
notifications = _.uniqBy(notifications, 'userHandle');
8788

88-
return new Promise((resolve, reject) => { // eslint-disable-line no-unused-vars
89-
const handles = _.map(notifications, 'userHandle');
90-
if (handles.length > 0) {
91-
service.getUsersByHandle(handles).then((users) => {
92-
_.forEach(notifications, (notification) => {
93-
const mentionedUser = _.find(users, { handle: notification.userHandle });
94-
notification.userId = mentionedUser ? mentionedUser.userId.toString() : notification.userHandle;
95-
});
96-
resolve(notifications);
97-
}).catch((error) => {
98-
if (logger) {
99-
logger.error(error);
100-
logger.info('Unable to send notification to mentioned user')
89+
const handles = _.map(notifications, 'userHandle');
90+
if (handles.length > 0) {
91+
return service.getUsersByHandle(handles).then((users) => {
92+
_.forEach(notifications, (notification) => {
93+
const mentionedUser = _.find(users, { handle: notification.userHandle });
94+
notification.userId = mentionedUser ? mentionedUser.userId.toString() : null;
95+
if (!notification.userId && logger) {// such notifications would be discarded later after aggregation
96+
logger.info(`Unable to find user with handle ${notification.userHandle}`);
10197
}
102-
//resolves with empty notification which essentially means we are unable to send notification to mentioned user
103-
resolve([]);
10498
});
105-
} else {
106-
resolve([]);
107-
}
108-
});
99+
return Promise.resolve(notifications);
100+
}).catch((error) => {
101+
if (logger) {
102+
logger.error(error);
103+
logger.info('Unable to send notification to mentioned user');
104+
}
105+
//resolves with empty notification which essentially means we are unable to send notification to mentioned user
106+
return Promise.resolve([]);
107+
});
108+
} else {
109+
return Promise.resolve([]);
110+
}
109111
};
110112

111113
/**
@@ -150,7 +152,7 @@ const getProjectMembersNotifications = (eventConfig, project) => {
150152
return Promise.resolve([]);
151153
}
152154

153-
return new Promise((resolve) => {
155+
return Promise.promisify((callback) => {
154156
let notifications = [];
155157
const projectMembers = _.get(project, 'members', []);
156158

@@ -185,8 +187,8 @@ const getProjectMembersNotifications = (eventConfig, project) => {
185187
// only one per userId
186188
notifications = _.uniqBy(notifications, 'userId');
187189

188-
resolve(notifications);
189-
});
190+
callback(null, notifications);
191+
})();
190192
};
191193

192194
/**
@@ -254,6 +256,83 @@ const getNotificationsForTopicStarter = (eventConfig, topicId) => {
254256
});
255257
};
256258

259+
/**
260+
* Filter members by project roles
261+
*
262+
* @params {Array} List of project roles
263+
* @params {Array} List of project members
264+
*
265+
* @returns {Array} List of objects with user ids
266+
*/
267+
const filterMembersByRoles = (roles, members) => {
268+
let result = [];
269+
270+
roles.forEach(projectRole => {
271+
result = result.concat(
272+
_.filter(members, PROJECT_ROLE_RULES[projectRole])
273+
.map(projectMember => ({
274+
userId: projectMember.userId.toString(),
275+
}))
276+
);
277+
});
278+
279+
return result;
280+
};
281+
282+
/**
283+
* Exclude private posts notification
284+
*
285+
* @param {Object} eventConfig event configuration
286+
* @param {Object} project project details
287+
* @param {Array} tags list of message tags
288+
*
289+
* @return {Promise} resolves to a list of notifications
290+
*/
291+
const getExcludedPrivatePostNotifications = (eventConfig, project, tags) => {
292+
// skip if message is not private or exclusion rule is not configured
293+
if (!_.includes(tags, 'MESSAGES') || !eventConfig.privatePostsForProjectRoles) {
294+
return Promise.resolve([]);
295+
}
296+
297+
const members = _.get(project, 'members', []);
298+
const notifications = filterMembersByRoles(eventConfig.privatePostsForProjectRoles, members);
299+
300+
return Promise.resolve(notifications);
301+
};
302+
303+
/**
304+
* Exclude notifications about posts inside draft phases
305+
*
306+
* @param {Object} eventConfig event configuration
307+
* @param {Object} project project details
308+
* @param {Array} tags list of message tags
309+
*
310+
* @return {Promise} resolves to a list of notifications
311+
*/
312+
const getExcludeDraftPhasesNotifications = (eventConfig, project, tags) => {
313+
// skip is no exclusion rule is configured
314+
if (!eventConfig.draftPhasesForProjectRoles) {
315+
return Promise.resolve([]);
316+
}
317+
318+
const phaseId = helpers.extractPhaseId(tags);
319+
// skip if it is not phase notification
320+
if (!phaseId) {
321+
return Promise.resolve([]);
322+
}
323+
324+
// exclude all user with configured roles if phase is in draft state
325+
return service.getPhase(project.id, phaseId)
326+
.then((phase) => {
327+
if (phase.status === 'draft') {
328+
const members = _.get(project, 'members', []);
329+
const notifications = filterMembersByRoles(eventConfig.draftPhasesForProjectRoles, members);
330+
331+
return Promise.resolve(notifications);
332+
}
333+
});
334+
};
335+
257336
/**
258337
* Exclude notifications using exclude rules of the event config
259338
*
@@ -281,12 +360,17 @@ const excludeNotifications = (logger, notifications, eventConfig, message, data)
281360
// and after filter out such notifications from the notifications list
282361
// TODO move this promise all together with `_.uniqBy` to one function
283362
// and reuse it here and in `handler` function
363+
const tags = _.get(message, 'tags', []);
364+
284365
return Promise.all([
285366
getNotificationsForTopicStarter(excludeEventConfig, message.topicId),
286367
getNotificationsForUserId(excludeEventConfig, message.userId),
287368
getNotificationsForMentionedUser(logger, excludeEventConfig, message.postContent),
288369
getProjectMembersNotifications(excludeEventConfig, project),
289370
getTopCoderMembersNotifications(excludeEventConfig),
371+
// these are special exclude rules which are only working for excluding notifications but not including
372+
getExcludedPrivatePostNotifications(excludeEventConfig, project, tags),
373+
getExcludeDraftPhasesNotifications(excludeEventConfig, project, tags),
290374
]).then((notificationsPerSource) => (
291375
_.uniqBy(_.flatten(notificationsPerSource), 'userId')
292376
)).then((excludedNotifications) => {
@@ -332,10 +416,10 @@ const handler = (topic, message, logger, callback) => {
332416
}
333417

334418
// get project details
335-
service.getProject(projectId).then(project => {
419+
return service.getProject(projectId).then(project => {
336420
let allNotifications = [];
337421

338-
Promise.all([
422+
return Promise.all([
339423
// the order in this list defines the priority of notification for the SAME user
340424
// upper in this list - higher priority
341425
// NOTE: always add all handles here, they have to check by themselves:
@@ -357,7 +441,7 @@ const handler = (topic, message, logger, callback) => {
357441
project,
358442
})
359443
)).then((notifications) => {
360-
allNotifications = _.filter(notifications, notification => notification.userId !== `${message.initiatorUserId}`);
444+
allNotifications = _.filter(notifications, n => n.userId && n.userId !== `${message.initiatorUserId}`);
361445

362446
if (eventConfig.includeUsers && message[eventConfig.includeUsers] &&
363447
message[eventConfig.includeUsers].length > 0) {

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
};

0 commit comments

Comments
 (0)