Skip to content

update: improve universal notifications payload #216

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
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