Skip to content

Commit ee682b0

Browse files
authored
Merge pull request #19 from topcoder-platform/dev
Production release
2 parents 4d8e21b + 3e767ed commit ee682b0

File tree

9 files changed

+182
-18
lines changed

9 files changed

+182
-18
lines changed

connect/config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ module.exports = {
66
TC_API_V3_BASE_URL: process.env.TC_API_V3_BASE_URL || 'https://api.topcoder-dev.com/v3',
77
TC_API_V4_BASE_URL: process.env.TC_API_V4_BASE_URL || 'https://api.topcoder-dev.com/v4',
88
// eslint-disable-next-line max-len
9-
TC_ADMIN_TOKEN: process.env.TC_ADMIN_TOKEN,
9+
TC_ADMIN_TOKEN: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBNYW5hZ2VyIiwiQ29ubmVjdCBBZG1pbiJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoicGF0X21vbmFoYW4iLCJleHAiOjE1MTk4NDIyMzYsInVzZXJJZCI6IjQwMTUyOTMzIiwiaWF0IjoxNTE0MzE4NTA0LCJlbWFpbCI6ImRldm9wcytwYXRfbW9uYWhhbkB0b3Bjb2Rlci5jb20iLCJqdGkiOiIwOThjMGNjOS05OTljLTRlZjktYmM5ZS0yNTExZWJkZmJkMzIifQ.N3rbYMOfniLZ3TV3z08MAD46TwgFUGJ--UYhQVuu1Uw',
1010

1111
// Probably temporary variables for TopCoder role ids for 'Connect Manager', 'Connect Copilot' and 'administrator'
1212
// These are values for development backend. For production backend they may be different.

connect/connectNotificationServer.js

Lines changed: 120 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const PROJECT_ROLE_OWNER = require('./events-config').PROJECT_ROLE_OWNER;
2222
* @return {Promise} resolves to a list of notifications
2323
*/
2424
const getTopCoderMembersNotifications = (eventConfig) => {
25+
// if event doesn't have to be notified to topcoder member, just ignore
2526
if (!eventConfig.topcoderRoles) {
2627
return Promise.resolve([]);
2728
}
@@ -51,6 +52,49 @@ const getTopCoderMembersNotifications = (eventConfig) => {
5152
});
5253
};
5354

55+
/**
56+
* Get notifications for mentioned users
57+
*
58+
* @param {Object} eventConfig event configuration
59+
* @param {Object} message content
60+
*
61+
* @return {Promise} resolves to a list of notifications
62+
*/
63+
const getNotificationsForMentionedUser = (eventConfig, content) => {
64+
if (!eventConfig.toMentionedUsers) {
65+
return Promise.resolve([]);
66+
}
67+
68+
let notifications = [];
69+
const regexUserHandle = /title=\"@([a-zA-Z0-9-_.{}\[\]]+)\"/g;
70+
let handles=[];
71+
let matches = regexUserHandle.exec(content);
72+
console.log(content)
73+
while (matches) {
74+
let handle = matches[1].toString();
75+
notifications.push({
76+
userHandle: handle,
77+
newType: 'notifications.connect.project.post.mention',
78+
contents: {
79+
toUserHandle: true,
80+
},
81+
});
82+
matches = regexUserHandle.exec(content);
83+
handles.push(handle);
84+
}
85+
// only one per userHandle
86+
notifications = _.uniqBy(notifications, 'userHandle');
87+
88+
return new Promise((resolve)=>{
89+
service.getUsersByHandle(handles).then((users)=>{
90+
_.map(notifications,(notification)=>{
91+
notification.userId = _.find(users,{handle:notification.userHandle}).userId;
92+
});
93+
resolve(notifications);
94+
})
95+
});
96+
};
97+
5498
/**
5599
* Get project members notifications
56100
*
@@ -59,8 +103,13 @@ const getTopCoderMembersNotifications = (eventConfig) => {
59103
*
60104
* @return {Promise} resolves to a list of notifications
61105
*/
62-
const getProjectMembersNotifications = (eventConfig, project) => (
63-
new Promise((resolve) => {
106+
const getProjectMembersNotifications = (eventConfig, project) => {
107+
// if event doesn't have to be notified to project member, just ignore
108+
if (!eventConfig.projectRoles) {
109+
return Promise.resolve([]);
110+
}
111+
112+
return new Promise((resolve) => {
64113
let notifications = [];
65114
const projectMembers = _.get(project, 'members', []);
66115

@@ -96,8 +145,8 @@ const getProjectMembersNotifications = (eventConfig, project) => (
96145
notifications = _.uniqBy(notifications, 'userId');
97146

98147
resolve(notifications);
99-
})
100-
);
148+
});
149+
};
101150

102151
/**
103152
* Get notifications for users obtained from userId
@@ -147,12 +196,64 @@ const getNotificationsForTopicStarter = (eventConfig, topicId) => {
147196
return Promise.reject(new Error('Missing topicId in the event message.'));
148197
}
149198

150-
return service.getTopic(topicId).then((topic) => ({
151-
userId: topic.userId.toString(),
152-
contents: {
153-
toTopicStarter: true,
154-
},
155-
}));
199+
return service.getTopic(topicId).then((topic) => {
200+
const userId = topic.userId.toString();
201+
202+
// special case: if topic created by CoderBot, don't send notification to him
203+
if (userId === 'CoderBot') {
204+
return [];
205+
}
206+
207+
return [{
208+
userId,
209+
contents: {
210+
toTopicStarter: true,
211+
},
212+
}];
213+
});
214+
};
215+
216+
/**
217+
* Exclude notifications using exclude rules of the event config
218+
*
219+
* @param {Array} notifications notifications list
220+
* @param {Object} eventConfig event configuration
221+
* @param {Object} message message
222+
* @param {Object} data any additional data which is retrieved once
223+
*
224+
* @returns {Promise} resolves to the list of filtered notifications
225+
*/
226+
const excludeNotifications = (notifications, eventConfig, message, data) => {
227+
// if there are no rules to exclude notifications, just return all of them untouched
228+
if (!eventConfig.exclude) {
229+
return Promise.resolve(notifications);
230+
}
231+
232+
const { project } = data;
233+
// create event config using rules to exclude notifications
234+
const excludeEventConfig = Object.assign({
235+
type: eventConfig.type,
236+
}, eventConfig.exclude);
237+
238+
// get notifications using rules for exclude notifications
239+
// and after filter out such notifications from the notifications list
240+
// TODO move this promise all together with `_.uniqBy` to one function
241+
// and reuse it here and in `handler` function
242+
return Promise.all([
243+
getNotificationsForTopicStarter(excludeEventConfig, message.topicId),
244+
getNotificationsForUserId(excludeEventConfig, message.userId),
245+
getProjectMembersNotifications(excludeEventConfig, project),
246+
getTopCoderMembersNotifications(excludeEventConfig),
247+
]).then((notificationsPerSource) => (
248+
_.uniqBy(_.flatten(notificationsPerSource), 'userId')
249+
)).then((excludedNotifications) => {
250+
const excludedUserIds = _.map(excludedNotifications, 'userId');
251+
const filteredNotifications = notifications.filter((notification) => (
252+
!_.includes(excludedUserIds, notification.userId)
253+
));
254+
255+
return filteredNotifications;
256+
});
156257
};
157258

158259
// set configuration for the server, see ../config/default.js for available config parameters
@@ -194,18 +295,23 @@ const handler = (topic, message, callback) => {
194295
// - check that event has everything required or throw error
195296
getNotificationsForTopicStarter(eventConfig, message.topicId),
196297
getNotificationsForUserId(eventConfig, message.userId),
298+
message.postContent ? getNotificationsForMentionedUser(eventConfig, message.postContent) : Promise.resolve([]),
197299
getProjectMembersNotifications(eventConfig, project),
198300
getTopCoderMembersNotifications(eventConfig),
199301
]).then((notificationsPerSource) => (
200302
// first found notification for one user will be send, the rest ignored
201303
// NOTE all userId has to be string
202304
_.uniqBy(_.flatten(notificationsPerSource), 'userId')
305+
)).then((notifications) => (
306+
excludeNotifications(notifications, eventConfig, message, {
307+
project,
308+
})
203309
)).then((notifications) => {
204-
allNotifications = notifications;
310+
allNotifications = _.filter(notifications,notification=>notification.userId!=message.initiatorUserId);
205311

206312
// now let's retrieve some additional data
207313

208-
// if message has userId such messages will likely need userHandle
314+
// if message has userId such messages will likely need userHandle and user full name
209315
// so let's get it
210316
if (message.userId) {
211317
const ids = [message.userId];
@@ -214,10 +320,12 @@ const handler = (topic, message, callback) => {
214320
return [];
215321
}).then((users) => {
216322
_.map(allNotifications, (notification) => {
323+
notification.version = eventConfig.version;
217324
notification.contents.projectName = project.name;
218325
// if found a user then add user handle
219326
if (users.length) {
220327
notification.contents.userHandle = users[0].handle;
328+
notification.contents.userFullName = `${users[0].firstName} ${users[0].lastName}`;
221329
}
222330
});
223331
callback(null, allNotifications);

connect/events-config.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,14 @@ const TOPCODER_ROLE_RULES = {
3434
*
3535
* Each event configuration object has
3636
* type {String} [mandatory] Event type
37+
* version {Number} [optional] Version of the event.
3738
* projectRoles {Array} [optional] List of project member roles which has to get notification
3839
* topcoderRoles {Array} [optional] List of TopCoder member roles which has to get notification
3940
* toUserHandle {Boolean} [optional] If set to true, user defined in `message.userHandle` will get notification
40-
* toTopicStarter {Boolean} [optional] If set to true, than will find who started topic `message.topicId` and send notification to him
41+
* toTopicStarter {Boolean} [optional] If set to true, than will find who started topic `message.topicId` and
42+
* send notification to him
43+
* exclude {Object} [optional] May contains any rules like `projectRoles`, `toUserHandle` etc
44+
* but these rules will forbid sending notifications to members who satisfy them
4145
*
4246
* @type {Array}
4347
*/
@@ -46,6 +50,9 @@ const EVENTS = [
4650
{
4751
type: 'notifications.connect.project.created',
4852
projectRoles: [PROJECT_ROLE_OWNER],
53+
exclude: {
54+
topcoderRoles: [ROLE_CONNECT_MANAGER, ROLE_ADMINISTRATOR],
55+
},
4956
}, {
5057
type: 'notifications.connect.project.submittedForReview',
5158
projectRoles: [PROJECT_ROLE_OWNER],
@@ -73,13 +80,16 @@ const EVENTS = [
7380
projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER],
7481
}, {
7582
type: 'notifications.connect.project.member.left',
83+
version: 2,
7684
projectRoles: [PROJECT_ROLE_MANAGER],
7785
}, {
7886
type: 'notifications.connect.project.member.removed',
87+
version: 2,
7988
projectRoles: [PROJECT_ROLE_MANAGER],
8089
toUserHandle: true,
8190
}, {
8291
type: 'notifications.connect.project.member.assignedAsOwner',
92+
version: 2,
8393
projectRoles: [PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER],
8494
toUserHandle: true,
8595
}, {
@@ -93,20 +103,28 @@ const EVENTS = [
93103
// Project activity
94104
{
95105
type: 'notifications.connect.project.topic.created',
106+
version: 2,
96107
projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, PROJECT_ROLE_MEMBER],
97108
}, {
98109
type: 'notifications.connect.project.post.created',
110+
version: 2,
99111
projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, PROJECT_ROLE_MEMBER],
100112
toTopicStarter: true,
113+
toMentionedUsers: true,
114+
}, {
115+
type: 'notifications.connect.project.post.mention',
101116
},
102117
{
103118
type: 'notifications.connect.project.linkCreated',
119+
version: 2,
104120
projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, PROJECT_ROLE_MEMBER],
105121
}, {
106122
type: 'notifications.connect.project.fileUploaded',
123+
version: 2,
107124
projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, PROJECT_ROLE_MEMBER],
108125
}, {
109126
type: 'notifications.connect.project.specificationModified',
127+
version: 2,
110128
projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, PROJECT_ROLE_MEMBER],
111129
},
112130
];

connect/service.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,36 @@ const getUsersById = (ids) => {
8989
});
9090
};
9191

92+
/**
93+
* Get users details by ids
94+
*
95+
* @param {Array} ids list of user ids
96+
*
97+
* @return {Promise} resolves to the list of user details
98+
*/
99+
const getUsersByHandle = (handles) => {
100+
const query = _.map(handles, (handle) => 'handle:' + handle).join(' OR ');
101+
return request
102+
.get(`${config.TC_API_V3_BASE_URL}/members/_search?fields=userId,handle,firstName,lastName&query=${query}`)
103+
.set('accept', 'application/json')
104+
.set('authorization', `Bearer ${config.TC_ADMIN_TOKEN}`)
105+
.then((res) => {
106+
if (!_.get(res, 'body.result.success')) {
107+
throw new Error(`Failed to get users by handle: ${handles}`);
108+
}
109+
110+
const users = _.get(res, 'body.result.content');
111+
112+
return users;
113+
}).catch((err) => {
114+
const errorDetails = _.get(err, 'response.body.result.content.message');
115+
throw new Error(
116+
`Failed to get users by handles: ${handles}.` +
117+
(errorDetails ? ' Server response: ' + errorDetails : '')
118+
);
119+
});
120+
};
121+
92122
/**
93123
* Get topic details
94124
*
@@ -119,5 +149,6 @@ module.exports = {
119149
getProject,
120150
getRoleMembers,
121151
getUsersById,
152+
getUsersByHandle,
122153
getTopic,
123154
};

migrations/v1.1.sql

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
-- Column: public."Notifications".version
2+
3+
-- ALTER TABLE public."Notifications" DROP COLUMN version;
4+
5+
ALTER TABLE public."Notifications"
6+
ADD COLUMN version smallint;

src/app.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ function startKafkaConsumer(handlers) {
5252
// save notifications
5353
.then((notifications) => Promise.all(_.map(notifications, (notification) => models.Notification.create({
5454
userId: notification.userId,
55-
type: topicName,
55+
type: notification.newType || topicName,
56+
version: notification.version || null,
5657
contents: _.extend({}, messageJSON, notification.contents),
5758
read: false,
5859
}))))

src/models/Notification.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ module.exports = (sequelize, DataTypes) => sequelize.define('Notification', {
1616
type: { type: DataTypes.STRING, allowNull: false },
1717
contents: { type: DataTypes.JSONB, allowNull: false },
1818
read: { type: DataTypes.BOOLEAN, allowNull: false },
19+
version: { type: DataTypes.SMALLINT, allowNull: true },
1920
}, {});
2021

2122
// sequelize will generate and manage createdAt, updatedAt fields

src/models/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@ const NotificationSetting = require('./NotificationSetting')(sequelize, DataType
1818
module.exports = {
1919
Notification,
2020
NotificationSetting,
21-
init: () => sequelize.sync({ }),
21+
init: () => sequelize.sync(),
2222
};

src/services/NotificationService.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,11 @@ function* updateSettings(data, userId) {
8787
*/
8888
function* listNotifications(query, userId) {
8989
const settings = yield getSettings(userId);
90-
9190

9291
const filter = { where: {
9392
userId,
9493
}, offset: query.offset, limit: query.limit, order: [['createdAt', 'DESC']] };
95-
if (_.keys(settings).length>0){
94+
if (_.keys(settings).length > 0) {
9695
// only filter out notifications types which were explicitly set to 'no' - so we return notification by default
9796
const notificationTypes = _.keys(settings).filter((notificationType) => settings[notificationType].web !== 'no');
9897
filter.where.type = { $in: notificationTypes };

0 commit comments

Comments
 (0)