Skip to content

Commit 93e4665

Browse files
committed
update: improve universal notifications payload
1 parent 9fee095 commit 93e4665

File tree

3 files changed

+241
-37
lines changed

3 files changed

+241
-37
lines changed

config/default.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,10 @@ module.exports = {
2828
KAFKA_CLIENT_CERT_KEY: process.env.KAFKA_CLIENT_CERT_KEY ?
2929
process.env.KAFKA_CLIENT_CERT_KEY.replace('\\n', '\n') : null,
3030

31-
TC_API_V3_BASE_URL: process.env.TC_API_V3_BASE_URL || '',
31+
TC_API_V3_BASE_URL: process.env.TC_API_V3_BASE_URL || 'http://api.topcoder-dev.com/v3',
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 || '',
34+
TC_API_V5_USERS_URL: process.env.TC_API_V5_USERS_URL || 'https://api.topcoder-dev.com/v5/users',
3435
API_CONTEXT_PATH: process.env.API_CONTEXT_PATH || '/v5/notifications',
3536
TC_API_BASE_URL: process.env.TC_API_BASE_URL || '',
3637

@@ -135,7 +136,7 @@ module.exports = {
135136
// email notification service related variables
136137
ENV: process.env.ENV,
137138
ENABLE_EMAILS: process.env.ENABLE_EMAILS ? Boolean(process.env.ENABLE_EMAILS) : false,
138-
ENABLE_DEV_MODE: process.env.ENABLE_DEV_MODE ? Boolean(process.env.ENABLE_DEV_MODE) : true,
139+
ENABLE_DEV_MODE: process.env.ENABLE_DEV_MODE === 'true',
139140
DEV_MODE_EMAIL: process.env.DEV_MODE_EMAIL,
140141
DEFAULT_REPLY_EMAIL: process.env.DEFAULT_REPLY_EMAIL,
141142
ENABLE_HOOK_BULK_NOTIFICATION: process.env.ENABLE_HOOK_BULK_NOTIFICATION || false,

src/common/tcApiHelper.js

Lines changed: 128 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,103 @@ function* getUsersByHandles(handles) {
8888
return yield searchUsersByQuery(query);
8989
}
9090

91+
/**
92+
* Get users by handles or userIds.
93+
* @param {Array<Object>} handles the objects that has user handles.
94+
* @param {Array<Object>} userIds the objects that has userIds.
95+
* @returns {Array<Object>} the matched users
96+
*/
97+
function* getUsersByHandlesAndUserIds(handles, userIds) {
98+
if ((!handles || handles.length === 0) && (!userIds || userIds.length === 0)) {
99+
return [];
100+
}
101+
const handlesQuery = _.map(handles, h => `handleLower:${h.handle.toLowerCase()}`);
102+
const userIdsQuery = _.map(userIds, u => `userId:${u.userId}`);
103+
const query = _.concat(handlesQuery, userIdsQuery).join(URI.encodeQuery(' OR ', 'utf8'));
104+
try {
105+
return yield searchUsersByQuery(query);
106+
} catch (err) {
107+
const error = new Error(err.response.text);
108+
error.status = err.status;
109+
throw error;
110+
}
111+
}
112+
113+
/**
114+
* Search users by query string.
115+
* @param {String} query the query string
116+
* @returns {Array} the matched users
117+
*/
118+
function* searchUsersByEmailQuery(query) {
119+
const token = yield getM2MToken();
120+
const res = yield request
121+
.get(`${
122+
config.TC_API_V3_BASE_URL
123+
}/users?filter=${
124+
query
125+
}&fields=id,email,handle`)
126+
.set('Authorization', `Bearer ${token}`);
127+
if (!_.get(res, 'body.result.success')) {
128+
throw new Error(`Failed to search users by query: ${query}`);
129+
}
130+
const records = _.get(res, 'body.result.content') || [];
131+
132+
logger.verbose(`Searched users: ${JSON.stringify(records, null, 4)}`);
133+
return records;
134+
}
135+
136+
/**
137+
* Get users by emails.
138+
* @param {Array<Object>} emails the objects that has user emails.
139+
* @returns {Array<Object>} the matched users
140+
*/
141+
function* getUsersByEmails(emails) {
142+
if (!emails || emails.length === 0) {
143+
return [];
144+
}
145+
const users = [];
146+
try {
147+
for (const email of emails) {
148+
const query = `email%3D${email.email}`;
149+
const result = yield searchUsersByEmailQuery(query);
150+
users.push(...result);
151+
}
152+
return users;
153+
} catch (err) {
154+
const error = new Error(err.response.text);
155+
error.status = err.status;
156+
throw error;
157+
}
158+
}
159+
160+
/**
161+
* Get users by uuid.
162+
* @param {Array<Object>} ids the objects that has user uuids.
163+
* @returns {Array<Object>} the matched users
164+
*/
165+
function* getUsersByUserUUIDs(ids) {
166+
if (!ids || ids.length === 0) {
167+
return [];
168+
}
169+
const users = [];
170+
const token = yield getM2MToken();
171+
try {
172+
for (const id of ids) {
173+
const res = yield request
174+
.get(`${config.TC_API_V5_USERS_URL}/${id}`)
175+
.set('Authorization', `Bearer ${token}`);
176+
const user = res.body;
177+
logger.verbose(`Searched users: ${JSON.stringify(user, null, 4)}`);
178+
users.push(user);
179+
}
180+
return users;
181+
} catch (err) {
182+
const error = new Error(err.response.text);
183+
error.status = err.status;
184+
throw error;
185+
}
186+
}
187+
91188
/**
92189
* Send message to bus.
93190
* @param {Object} data the data to send
@@ -158,21 +255,30 @@ function* checkNotificationSetting(userId, notificationType, serviceId) {
158255
}
159256

160257
/**
161-
* Notify user via email.
258+
* Notify user via web.
162259
* @param {Object} message the Kafka message payload
163-
* @return {Object} notification details.
260+
* @return {Array<Object>} notification details.
164261
*/
165262
function* notifyUserViaWeb(message) {
166263
const notificationType = message.type;
167-
const userId = message.details.userId;
168-
// if web notification is explicitly disabled for current notification type do nothing
169-
const allowed = yield checkNotificationSetting(userId, notificationType, constants.SETTINGS_WEB_SERVICE_ID);
170-
if (!allowed) {
171-
logger.verbose(`Notification '${notificationType}' won't be sent by '${constants.SETTINGS_WEB_SERVICE_ID}'`
264+
const notifications = [];
265+
for (const recipient of message.details.recipients) {
266+
const userId = recipient.userId;
267+
if (_.isUndefined(userId)) {
268+
logger.error(`userId not received for user: ${JSON.stringify(recipient, null, 4)}`);
269+
continue;
270+
}
271+
// if web notification is explicitly disabled for current notification type do nothing
272+
const allowed = yield checkNotificationSetting(userId, notificationType, constants.SETTINGS_WEB_SERVICE_ID);
273+
if (!allowed) {
274+
logger.verbose(`Notification '${notificationType}' won't be sent by '${constants.SETTINGS_WEB_SERVICE_ID}'`
172275
+ ` service to the userId '${userId}' due to his notification settings.`);
173-
return;
276+
continue;
277+
}
278+
notifications.push(_.assign({}, _.pick(message.details, ['contents', 'version']), { userId }));
174279
}
175-
return message.details;
280+
281+
return notifications;
176282
}
177283

178284
/**
@@ -184,13 +290,6 @@ function* notifyUserViaEmail(message) {
184290
const topic = constants.BUS_API_EVENT.EMAIL.UNIVERSAL;
185291
for (const recipient of message.details.recipients) {
186292
const userId = recipient.userId;
187-
// if email notification is explicitly disabled for current notification type do nothing
188-
const allowed = yield checkNotificationSetting(userId, notificationType, constants.SETTINGS_EMAIL_SERVICE_ID);
189-
if (!allowed) {
190-
logger.verbose(`Notification '${notificationType}' won't be sent by '${constants.SETTINGS_EMAIL_SERVICE_ID}'`
191-
+ ` service to the userId '${userId}' due to his notification settings.`);
192-
continue;
193-
}
194293
let userEmail;
195294
// if dev mode for email is enabled then replace recipient email
196295
if (config.ENABLE_DEV_MODE) {
@@ -202,6 +301,16 @@ function* notifyUserViaEmail(message) {
202301
continue;
203302
}
204303
}
304+
// skip checking notification setting if userId is not found.
305+
if (!_.isUndefined(userId)) {
306+
// if email notification is explicitly disabled for current notification type do nothing
307+
const allowed = yield checkNotificationSetting(userId, notificationType, constants.SETTINGS_EMAIL_SERVICE_ID);
308+
if (!allowed) {
309+
logger.verbose(`Notification '${notificationType}' won't be sent by '${constants.SETTINGS_EMAIL_SERVICE_ID}'`
310+
+ ` service to the userId '${userId}' due to his notification settings.`);
311+
continue;
312+
}
313+
}
205314
const recipients = [userEmail];
206315
const payload = {
207316
from: message.details.from,
@@ -496,6 +605,9 @@ module.exports = {
496605
getM2MToken,
497606
getUsersBySkills,
498607
getUsersByHandles,
608+
getUsersByHandlesAndUserIds,
609+
getUsersByEmails,
610+
getUsersByUserUUIDs,
499611
sendMessageToBus,
500612
notifySlackChannel,
501613
checkNotificationSetting,

src/services/UniversalNotificationService.js

Lines changed: 110 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*/
44

55
'use strict';
6-
6+
const _ = require('lodash');
77
const joi = require('joi');
88
const logger = require('../common/logger');
99
const tcApiHelper = require('../common/tcApiHelper');
@@ -16,15 +16,19 @@ const emailSchema = joi.object().keys({
1616
from: joi.string().email().required(),
1717
recipients: joi.array().items(
1818
joi.object().keys({
19-
userId: joi.number().integer().required(),
20-
email: joi.string().email().required(),
21-
}).required()
19+
userId: joi.number().integer(),
20+
userUUID: joi.string().uuid(),
21+
email: joi.string().email(),
22+
handle: joi.string(),
23+
}).min(1).required()
2224
).min(1).required(),
2325
cc: joi.array().items(
2426
joi.object().keys({
2527
userId: joi.number().integer(),
26-
email: joi.string().email().required(),
27-
}).required()
28+
userUUID: joi.string().uuid(),
29+
email: joi.string().email(),
30+
handle: joi.string(),
31+
}).min(1).required()
2832
),
2933
data: joi.object().keys({
3034
subject: joi.string(),
@@ -48,7 +52,14 @@ const webSchema = joi.object().keys({
4852
serviceId: joi.string().valid(constants.SETTINGS_WEB_SERVICE_ID).required(),
4953
type: joi.string().required(),
5054
details: joi.object().keys({
51-
userId: joi.number().integer().required(),
55+
recipients: joi.array().items(
56+
joi.object().keys({
57+
userId: joi.number().integer(),
58+
userUUID: joi.string().uuid(),
59+
email: joi.string().email(),
60+
handle: joi.string(),
61+
}).min(1).required()
62+
).min(1).required(),
5263
contents: joi.object(),
5364
version: joi.number().integer().required(),
5465
}).required(),
@@ -63,18 +74,95 @@ function validator(data, schema) {
6374
return true;
6475
}
6576

77+
function* completeMissingFields(details, findEmail, findUserId) {
78+
const getFieldsByUserId = [];
79+
const getFieldsByHandle = [];
80+
const getFieldsByUserUUID = [];
81+
const getFieldsByEmail = [];
82+
function findMissingFields(data, email, userId) {
83+
for (const recipient of data) {
84+
if (_.isUndefined(recipient.email) && email) {
85+
if (!_.isUndefined(recipient.userId)) {
86+
getFieldsByUserId.push(recipient);
87+
} else if (!_.isUndefined(recipient.handle)) {
88+
getFieldsByHandle.push(recipient);
89+
} else {
90+
getFieldsByUserUUID.push(recipient);
91+
}
92+
} else if (_.isUndefined(recipient.userId) && userId) {
93+
if (!_.isUndefined(recipient.handle)) {
94+
getFieldsByHandle.push(recipient);
95+
} else if (!_.isUndefined(recipient.email)) {
96+
getFieldsByEmail.push(recipient);
97+
} else {
98+
getFieldsByUserUUID.push(recipient);
99+
}
100+
}
101+
}
102+
}
103+
104+
findMissingFields(details.recipients, findEmail, findUserId);
105+
if (_.isArray(details.cc) && !_.isEmpty(details.cc)) {
106+
findMissingFields(details.cc, findEmail, false);
107+
}
108+
const foundUsersByHandleOrId = yield tcApiHelper.getUsersByHandlesAndUserIds(getFieldsByHandle, getFieldsByUserId);
109+
if (!_.isEmpty(foundUsersByHandleOrId)) {
110+
for (const user of [...getFieldsByUserId, ...getFieldsByHandle]) {
111+
const found = _.find(foundUsersByHandleOrId, !_.isUndefined(user.handle)
112+
? ['handle', user.handle] : ['userId', user.userId]) || {};
113+
if (!_.isUndefined(found.email) && _.isUndefined(user.email)) {
114+
_.assign(user, { email: found.email });
115+
}
116+
if (!_.isUndefined(found.userId) && _.isUndefined(user.userId)) {
117+
_.assign(user, { userId: found.userId });
118+
}
119+
}
120+
}
121+
const foundUsersByEmail = yield tcApiHelper.getUsersByEmails(getFieldsByEmail);
122+
if (!_.isEmpty(foundUsersByEmail)) {
123+
for (const user of getFieldsByEmail) {
124+
const found = _.find(foundUsersByEmail, ['email', user.email]) || {};
125+
if (!_.isUndefined(found.id)) {
126+
_.assign(user, { userId: found.id });
127+
}
128+
}
129+
}
130+
const foundUsersByUUID = yield tcApiHelper.getUsersByUserUUIDs(getFieldsByUserUUID);
131+
if (!_.isEmpty(foundUsersByUUID)) {
132+
for (const user of getFieldsByUserUUID) {
133+
const found = _.find(foundUsersByUUID, ['id', user.userUUID]) || {};
134+
if (!_.isUndefined(found.externalProfiles) && !_.isEmpty(found.externalProfiles)) {
135+
_.assign(user, { userId: _.toInteger(_.get(found.externalProfiles[0], 'externalId')) });
136+
}
137+
}
138+
if (findEmail) {
139+
const usersHaveId = _.filter(getFieldsByUserUUID, u => !_.isUndefined(u.userId));
140+
const foundUsersById = yield tcApiHelper.getUsersByHandlesAndUserIds([], usersHaveId);
141+
if (!_.isEmpty(foundUsersById)) {
142+
for (const user of getFieldsByUserUUID) {
143+
const found = _.find(foundUsersById, ['userId', user.userId]) || {};
144+
if (!_.isUndefined(found.email)) {
145+
_.assign(user, { email: found.email });
146+
}
147+
}
148+
}
149+
}
150+
}
151+
}
152+
66153
/**
67154
* Handle notification message
68155
* @param {Object} message the Kafka message
69156
* @returns {Array} the notifications
70157
*/
71158
function* handle(message) {
72159
const notifications = [];
73-
for (const data of message.payload) {
160+
for (const data of message.payload.notifications) {
74161
try {
75162
switch (data.serviceId) {
76163
case constants.SETTINGS_EMAIL_SERVICE_ID:
77164
if (validator(data, emailSchema)) {
165+
yield completeMissingFields(data.details, true, true);
78166
yield tcApiHelper.notifyUserViaEmail(data);
79167
}
80168
break;
@@ -85,9 +173,10 @@ function* handle(message) {
85173
break;
86174
case constants.SETTINGS_WEB_SERVICE_ID:
87175
if (validator(data, webSchema)) {
88-
const notification = yield tcApiHelper.notifyUserViaWeb(data);
89-
if (notification) {
90-
notifications.push(notification);
176+
yield completeMissingFields(data.details, false, true);
177+
const _notifications = yield tcApiHelper.notifyUserViaWeb(data);
178+
if (_notifications) {
179+
notifications.push(..._notifications);
91180
}
92181
}
93182
break;
@@ -107,14 +196,16 @@ handle.schema = {
107196
originator: joi.string().required(),
108197
timestamp: joi.date().required(),
109198
'mime-type': joi.string().required(),
110-
payload: joi.array().items(
111-
joi.object().keys({
112-
serviceId: joi.string().valid(
113-
constants.SETTINGS_EMAIL_SERVICE_ID,
114-
constants.SETTINGS_SLACK_SERVICE_ID,
115-
constants.SETTINGS_WEB_SERVICE_ID).required(),
116-
}).unknown()
117-
).min(1).required(),
199+
payload: joi.object().keys({
200+
notifications: joi.array().items(
201+
joi.object().keys({
202+
serviceId: joi.string().valid(
203+
constants.SETTINGS_EMAIL_SERVICE_ID,
204+
constants.SETTINGS_SLACK_SERVICE_ID,
205+
constants.SETTINGS_WEB_SERVICE_ID).required(),
206+
}).unknown()
207+
).min(1).required(),
208+
}).required(),
118209
}).required(),
119210
};
120211

0 commit comments

Comments
 (0)