Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions config/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ module.exports = {
KAFKA_CLIENT_CERT_KEY: process.env.KAFKA_CLIENT_CERT_KEY ?
process.env.KAFKA_CLIENT_CERT_KEY.replace('\\n', '\n') : null,

TC_API_V3_BASE_URL: process.env.TC_API_V3_BASE_URL || '',
TC_API_V3_BASE_URL: process.env.TC_API_V3_BASE_URL || 'http://api.topcoder-dev.com/v3',
TC_API_V4_BASE_URL: process.env.TC_API_V4_BASE_URL || '',
TC_API_V5_BASE_URL: process.env.TC_API_V5_BASE_URL || '',
TC_API_V5_USERS_URL: process.env.TC_API_V5_USERS_URL || 'https://api.topcoder-dev.com/v5/users',
API_CONTEXT_PATH: process.env.API_CONTEXT_PATH || '/v5/notifications',
TC_API_BASE_URL: process.env.TC_API_BASE_URL || '',

Expand Down Expand Up @@ -135,7 +136,7 @@ module.exports = {
// email notification service related variables
ENV: process.env.ENV,
ENABLE_EMAILS: process.env.ENABLE_EMAILS ? Boolean(process.env.ENABLE_EMAILS) : false,
ENABLE_DEV_MODE: process.env.ENABLE_DEV_MODE ? Boolean(process.env.ENABLE_DEV_MODE) : true,
ENABLE_DEV_MODE: 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,
Expand Down
144 changes: 128 additions & 16 deletions src/common/tcApiHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,103 @@ function* getUsersByHandles(handles) {
return yield searchUsersByQuery(query);
}

/**
* Get users by handles or userIds.
* @param {Array<Object>} handles the objects that has user handles.
* @param {Array<Object>} userIds the objects that has userIds.
* @returns {Array<Object>} the matched users
*/
function* getUsersByHandlesAndUserIds(handles, userIds) {
if ((!handles || handles.length === 0) && (!userIds || userIds.length === 0)) {
return [];
}
const handlesQuery = _.map(handles, h => `handleLower:${h.handle.toLowerCase()}`);
const userIdsQuery = _.map(userIds, u => `userId:${u.userId}`);
const query = _.concat(handlesQuery, userIdsQuery).join(URI.encodeQuery(' OR ', 'utf8'));
try {
return yield searchUsersByQuery(query);
} catch (err) {
const error = new Error(err.response.text);
error.status = err.status;
throw error;
}
}

/**
* Search users by query string.
* @param {String} query the query string
* @returns {Array} the matched users
*/
function* searchUsersByEmailQuery(query) {
const token = yield getM2MToken();
const res = yield request
.get(`${
config.TC_API_V3_BASE_URL
}/users?filter=${
query
}&fields=id,email,handle`)
.set('Authorization', `Bearer ${token}`);
if (!_.get(res, 'body.result.success')) {
throw new Error(`Failed to search users by query: ${query}`);
}
const records = _.get(res, 'body.result.content') || [];

logger.verbose(`Searched users: ${JSON.stringify(records, null, 4)}`);
return records;
}

/**
* Get users by emails.
* @param {Array<Object>} emails the objects that has user emails.
* @returns {Array<Object>} the matched users
*/
function* getUsersByEmails(emails) {
if (!emails || emails.length === 0) {
return [];
}
const users = [];
try {
for (const email of emails) {
const query = `email%3D${email.email}`;
const result = yield searchUsersByEmailQuery(query);
users.push(...result);
}
return users;
} catch (err) {
const error = new Error(err.response.text);
error.status = err.status;
throw error;
}
}

/**
* Get users by uuid.
* @param {Array<Object>} ids the objects that has user uuids.
* @returns {Array<Object>} the matched users
*/
function* getUsersByUserUUIDs(ids) {
if (!ids || ids.length === 0) {
return [];
}
const users = [];
const token = yield getM2MToken();
try {
for (const id of ids) {
const res = yield request
.get(`${config.TC_API_V5_USERS_URL}/${id}`)
.set('Authorization', `Bearer ${token}`);
const user = res.body;
logger.verbose(`Searched users: ${JSON.stringify(user, null, 4)}`);
users.push(user);
}
return users;
} catch (err) {
const error = new Error(err.response.text);
error.status = err.status;
throw error;
}
}

/**
* Send message to bus.
* @param {Object} data the data to send
Expand Down Expand Up @@ -158,21 +255,30 @@ function* checkNotificationSetting(userId, notificationType, serviceId) {
}

/**
* Notify user via email.
* Notify user via web.
* @param {Object} message the Kafka message payload
* @return {Object} notification details.
* @return {Array<Object>} notification details.
*/
function* notifyUserViaWeb(message) {
const notificationType = message.type;
const userId = message.details.userId;
// if web notification is explicitly disabled for current notification type do nothing
const allowed = yield checkNotificationSetting(userId, notificationType, constants.SETTINGS_WEB_SERVICE_ID);
if (!allowed) {
logger.verbose(`Notification '${notificationType}' won't be sent by '${constants.SETTINGS_WEB_SERVICE_ID}'`
const notifications = [];
for (const recipient of message.details.recipients) {
const userId = recipient.userId;
if (_.isUndefined(userId)) {
logger.error(`userId not received for user: ${JSON.stringify(recipient, null, 4)}`);
continue;
}
// if web notification is explicitly disabled for current notification type do nothing
const allowed = yield checkNotificationSetting(userId, notificationType, constants.SETTINGS_WEB_SERVICE_ID);
if (!allowed) {
logger.verbose(`Notification '${notificationType}' won't be sent by '${constants.SETTINGS_WEB_SERVICE_ID}'`
+ ` service to the userId '${userId}' due to his notification settings.`);
return;
continue;
}
notifications.push(_.assign({}, _.pick(message.details, ['contents', 'version']), { userId }));
}
return message.details;

return notifications;
}

/**
Expand All @@ -184,13 +290,6 @@ function* notifyUserViaEmail(message) {
const topic = constants.BUS_API_EVENT.EMAIL.UNIVERSAL;
for (const recipient of message.details.recipients) {
const userId = recipient.userId;
// if email notification is explicitly disabled for current notification type do nothing
const allowed = yield checkNotificationSetting(userId, notificationType, constants.SETTINGS_EMAIL_SERVICE_ID);
if (!allowed) {
logger.verbose(`Notification '${notificationType}' won't be sent by '${constants.SETTINGS_EMAIL_SERVICE_ID}'`
+ ` service to the userId '${userId}' due to his notification settings.`);
continue;
}
let userEmail;
// if dev mode for email is enabled then replace recipient email
if (config.ENABLE_DEV_MODE) {
Expand All @@ -202,6 +301,16 @@ function* notifyUserViaEmail(message) {
continue;
}
}
// skip checking notification setting if userId is not found.
if (!_.isUndefined(userId)) {
// if email notification is explicitly disabled for current notification type do nothing
const allowed = yield checkNotificationSetting(userId, notificationType, constants.SETTINGS_EMAIL_SERVICE_ID);
if (!allowed) {
logger.verbose(`Notification '${notificationType}' won't be sent by '${constants.SETTINGS_EMAIL_SERVICE_ID}'`
+ ` service to the userId '${userId}' due to his notification settings.`);
continue;
}
}
const recipients = [userEmail];
const payload = {
from: message.details.from,
Expand Down Expand Up @@ -496,6 +605,9 @@ module.exports = {
getM2MToken,
getUsersBySkills,
getUsersByHandles,
getUsersByHandlesAndUserIds,
getUsersByEmails,
getUsersByUserUUIDs,
sendMessageToBus,
notifySlackChannel,
checkNotificationSetting,
Expand Down
129 changes: 110 additions & 19 deletions src/services/UniversalNotificationService.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/

'use strict';

const _ = require('lodash');
const joi = require('joi');
const logger = require('../common/logger');
const tcApiHelper = require('../common/tcApiHelper');
Expand All @@ -16,15 +16,19 @@ const emailSchema = joi.object().keys({
from: joi.string().email().required(),
recipients: joi.array().items(
joi.object().keys({
userId: joi.number().integer().required(),
email: joi.string().email().required(),
}).required()
userId: joi.number().integer(),
userUUID: joi.string().uuid(),
email: joi.string().email(),
handle: joi.string(),
}).min(1).required()
).min(1).required(),
cc: joi.array().items(
joi.object().keys({
userId: joi.number().integer(),
email: joi.string().email().required(),
}).required()
userUUID: joi.string().uuid(),
email: joi.string().email(),
handle: joi.string(),
}).min(1).required()
),
data: joi.object().keys({
subject: joi.string(),
Expand All @@ -48,7 +52,14 @@ const webSchema = joi.object().keys({
serviceId: joi.string().valid(constants.SETTINGS_WEB_SERVICE_ID).required(),
type: joi.string().required(),
details: joi.object().keys({
userId: joi.number().integer().required(),
recipients: joi.array().items(
joi.object().keys({
userId: joi.number().integer(),
userUUID: joi.string().uuid(),
email: joi.string().email(),
handle: joi.string(),
}).min(1).required()
).min(1).required(),
contents: joi.object(),
version: joi.number().integer().required(),
}).required(),
Expand All @@ -63,18 +74,95 @@ function validator(data, schema) {
return true;
}

function* completeMissingFields(details, findEmail, findUserId) {
const getFieldsByUserId = [];
const getFieldsByHandle = [];
const getFieldsByUserUUID = [];
const getFieldsByEmail = [];
function findMissingFields(data, email, userId) {
for (const recipient of data) {
if (_.isUndefined(recipient.email) && email) {
if (!_.isUndefined(recipient.userId)) {
getFieldsByUserId.push(recipient);
} else if (!_.isUndefined(recipient.handle)) {
getFieldsByHandle.push(recipient);
} else {
getFieldsByUserUUID.push(recipient);
}
} else if (_.isUndefined(recipient.userId) && userId) {
if (!_.isUndefined(recipient.handle)) {
getFieldsByHandle.push(recipient);
} else if (!_.isUndefined(recipient.email)) {
getFieldsByEmail.push(recipient);
} else {
getFieldsByUserUUID.push(recipient);
}
}
}
}

findMissingFields(details.recipients, findEmail, findUserId);
if (_.isArray(details.cc) && !_.isEmpty(details.cc)) {
findMissingFields(details.cc, findEmail, false);
}
const foundUsersByHandleOrId = yield tcApiHelper.getUsersByHandlesAndUserIds(getFieldsByHandle, getFieldsByUserId);
if (!_.isEmpty(foundUsersByHandleOrId)) {
for (const user of [...getFieldsByUserId, ...getFieldsByHandle]) {
const found = _.find(foundUsersByHandleOrId, !_.isUndefined(user.handle)
? ['handle', user.handle] : ['userId', user.userId]) || {};
if (!_.isUndefined(found.email) && _.isUndefined(user.email)) {
_.assign(user, { email: found.email });
}
if (!_.isUndefined(found.userId) && _.isUndefined(user.userId)) {
_.assign(user, { userId: found.userId });
}
}
}
const foundUsersByEmail = yield tcApiHelper.getUsersByEmails(getFieldsByEmail);
if (!_.isEmpty(foundUsersByEmail)) {
for (const user of getFieldsByEmail) {
const found = _.find(foundUsersByEmail, ['email', user.email]) || {};
if (!_.isUndefined(found.id)) {
_.assign(user, { userId: found.id });
}
}
}
const foundUsersByUUID = yield tcApiHelper.getUsersByUserUUIDs(getFieldsByUserUUID);
if (!_.isEmpty(foundUsersByUUID)) {
for (const user of getFieldsByUserUUID) {
const found = _.find(foundUsersByUUID, ['id', user.userUUID]) || {};
if (!_.isUndefined(found.externalProfiles) && !_.isEmpty(found.externalProfiles)) {
_.assign(user, { userId: _.toInteger(_.get(found.externalProfiles[0], 'externalId')) });
}
}
if (findEmail) {
const usersHaveId = _.filter(getFieldsByUserUUID, u => !_.isUndefined(u.userId));
const foundUsersById = yield tcApiHelper.getUsersByHandlesAndUserIds([], usersHaveId);
if (!_.isEmpty(foundUsersById)) {
for (const user of getFieldsByUserUUID) {
const found = _.find(foundUsersById, ['userId', user.userId]) || {};
if (!_.isUndefined(found.email)) {
_.assign(user, { email: found.email });
}
}
}
}
}
}

/**
* Handle notification message
* @param {Object} message the Kafka message
* @returns {Array} the notifications
*/
function* handle(message) {
const notifications = [];
for (const data of message.payload) {
for (const data of message.payload.notifications) {
try {
switch (data.serviceId) {
case constants.SETTINGS_EMAIL_SERVICE_ID:
if (validator(data, emailSchema)) {
yield completeMissingFields(data.details, true, true);
yield tcApiHelper.notifyUserViaEmail(data);
}
break;
Expand All @@ -85,9 +173,10 @@ function* handle(message) {
break;
case constants.SETTINGS_WEB_SERVICE_ID:
if (validator(data, webSchema)) {
const notification = yield tcApiHelper.notifyUserViaWeb(data);
if (notification) {
notifications.push(notification);
yield completeMissingFields(data.details, false, true);
const _notifications = yield tcApiHelper.notifyUserViaWeb(data);
if (_notifications) {
notifications.push(..._notifications);
}
}
break;
Expand All @@ -107,14 +196,16 @@ handle.schema = {
originator: joi.string().required(),
timestamp: joi.date().required(),
'mime-type': joi.string().required(),
payload: joi.array().items(
joi.object().keys({
serviceId: joi.string().valid(
constants.SETTINGS_EMAIL_SERVICE_ID,
constants.SETTINGS_SLACK_SERVICE_ID,
constants.SETTINGS_WEB_SERVICE_ID).required(),
}).unknown()
).min(1).required(),
payload: joi.object().keys({
notifications: joi.array().items(
joi.object().keys({
serviceId: joi.string().valid(
constants.SETTINGS_EMAIL_SERVICE_ID,
constants.SETTINGS_SLACK_SERVICE_ID,
constants.SETTINGS_WEB_SERVICE_ID).required(),
}).unknown()
).min(1).required(),
}).required(),
}).required(),
};

Expand Down