Skip to content

Feature/skip notifications for customers #117

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
Show file tree
Hide file tree
Changes from all commits
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
33 changes: 17 additions & 16 deletions config/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,56 +45,57 @@ module.exports = {
AUTH0_PROXY_SERVER_URL: process.env.AUTH0_PROXY_SERVER_URL,

KAFKA_CONSUMER_RULESETS: {
// key is Kafka topic name, value is array of ruleset which have key as handler function name defined in src/processors/index.js
// key is Kafka topic name, value is array of ruleset which have key as
// handler function name defined in src/processors/index.js
'challenge.notification.events': [
{
handleChallenge: /** topic handler name */
{
type: 'UPDATE_DRAFT_CHALLENGE',
roles: ["Submitter" /** Competitor */, "Copilot", "Reviewer"],
roles: ['Submitter' /** Competitor */, 'Copilot', 'Reviewer'],
notification:
{
id: 0, /** challengeid or projectid */
name: '', /** challenge name */
group: 'Challenge',
title: 'Challenge specification is modified.'
}
}
}
title: 'Challenge specification is modified.',
},
},
},
],
'notifications.autopilot.events': [
{
handleAutoPilot:
{
phaseTypeName: 'Checkpoint Screening',
state: 'START',
roles: ["Copilot", "Reviewer"],
roles: ['Copilot', 'Reviewer'],
notification:
{
id: 0, /** challengeid or projectid */
name: '', /** challenge name */
group: 'Challenge',
title: 'Challenge checkpoint review.'
}
}
}
title: 'Challenge checkpoint review.',
},
},
},
],
'submission.notification.create': [
{
handleSubmission:
{
resource: 'submission',
roles: ["Copilot", "Reviewer"],
roles: ['Copilot', 'Reviewer'],
selfOnly: true /** Submitter only */,
notification:
{
id: 0, /** challengeid or projectid */
name: '', /** challenge name */
group: 'Submission',
title: 'A new submission is uploaded.'
}
}
}
title: 'A new submission is uploaded.',
},
},
},
],
//'notifications.community.challenge.created': ['handleChallengeCreated'],
//'notifications.community.challenge.phasewarning': ['handleChallengePhaseWarning'],
Expand Down
85 changes: 84 additions & 1 deletion connect/connectNotificationServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const config = require('./config');
const notificationServer = require('../index');
const _ = require('lodash');
const service = require('./service');
const helpers = require('./helpers');
const { BUS_API_EVENT } = require('./constants');
const EVENTS = require('./events-config').EVENTS;
const PROJECT_ROLE_RULES = require('./events-config').PROJECT_ROLE_RULES;
Expand Down Expand Up @@ -246,6 +247,83 @@ const getNotificationsForTopicStarter = (eventConfig, topicId) => {
});
};

/**
* Filter members by project roles
*
* @params {Array} List of project roles
* @params {Array} List of project members
*
* @returns {Array} List of objects with user ids
*/
const filterMembersByRoles = (roles, members) => {
let result = [];

roles.forEach(projectRole => {
result = result.concat(
_.filter(members, PROJECT_ROLE_RULES[projectRole])
.map(projectMember => ({
userId: projectMember.userId.toString(),
}))
);
});

return result;
};

/**
* Exclude private posts notification
*
* @param {Object} eventConfig event configuration
* @param {Object} project project details
* @param {Array} tags list of message tags
*
* @return {Promise} resolves to a list of notifications
*/
const getExcludedPrivatePostNotifications = (eventConfig, project, tags) => {
// skip if message is not private or exclusion rule is not configured
if (!_.includes(tags, 'MESSAGES') || !eventConfig.privatePostsForProjectRoles) {
return Promise.resolve([]);
}

const members = _.get(project, 'members', []);
const notifications = filterMembersByRoles(eventConfig.privatePostsForProjectRoles, members);

return Promise.resolve(notifications);
};

/**
* Exclude notifications about posts inside draft phases
*
* @param {Object} eventConfig event configuration
* @param {Object} project project details
* @param {Array} tags list of message tags
*
* @return {Promise} resolves to a list of notifications
*/
const getExcludeDraftPhasesNotifications = (eventConfig, project, tags) => {
// skip is no exclusion rule is configured
if (!eventConfig.draftPhasesForProjectRoles) {
return Promise.resolve([]);
}

const phaseId = helpers.extractPhaseId(tags);
// skip if it is not phase notification
if (!phaseId) {
return Promise.resolve([]);
}

// exclude all user with configured roles if phase is in draft state
return service.getPhase(project.id, phaseId)
.then((phase) => {
if (phase.status === 'draft') {
const members = _.get(project, 'members', []);
const notifications = filterMembersByRoles(eventConfig.draftPhasesForProjectRoles, members);

return Promise.resolve(notifications);
}
});
};

/**
* Exclude notifications using exclude rules of the event config
*
Expand All @@ -272,12 +350,17 @@ const excludeNotifications = (notifications, eventConfig, message, data) => {
// and after filter out such notifications from the notifications list
// TODO move this promise all together with `_.uniqBy` to one function
// and reuse it here and in `handler` function
const tags = _.get(message, 'tags', []);

return Promise.all([
getNotificationsForTopicStarter(excludeEventConfig, message.topicId),
getNotificationsForUserId(excludeEventConfig, message.userId),
getNotificationsForMentionedUser(eventConfig, message.postContent),
getNotificationsForMentionedUser(excludeEventConfig, message.postContent),
getProjectMembersNotifications(excludeEventConfig, project),
getTopCoderMembersNotifications(excludeEventConfig),
// these are special exclude rules which are only working for excluding notifications but not including
getExcludedPrivatePostNotifications(excludeEventConfig, project, tags),
getExcludeDraftPhasesNotifications(excludeEventConfig, project, tags),
]).then((notificationsPerSource) => (
_.uniqBy(_.flatten(notificationsPerSource), 'userId')
)).then((excludedNotifications) => {
Expand Down
25 changes: 25 additions & 0 deletions connect/events-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const { BUS_API_EVENT } = require('./constants');

// project member role names
const PROJECT_ROLE_OWNER = 'owner';
const PROJECT_ROLE_CUSTOMER = 'customer';
const PROJECT_ROLE_COPILOT = 'copilot';
const PROJECT_ROLE_MANAGER = 'manager';
const PROJECT_ROLE_MEMBER = 'member';
Expand All @@ -13,13 +14,15 @@ const PROJECT_ROLE_ACCOUNT_MANAGER = 'account_manager';
// project member role rules
const PROJECT_ROLE_RULES = {
[PROJECT_ROLE_OWNER]: { role: 'customer', isPrimary: true },
[PROJECT_ROLE_CUSTOMER]: { role: 'customer' },
[PROJECT_ROLE_COPILOT]: { role: 'copilot' },
[PROJECT_ROLE_MANAGER]: { role: 'manager' },
[PROJECT_ROLE_ACCOUNT_MANAGER]: { role: 'account_manager' },
[PROJECT_ROLE_MEMBER]: {},
};

// TopCoder roles
// eslint-disable-next-line no-unused-vars
const ROLE_CONNECT_COPILOT = 'Connect Copilot';
const ROLE_CONNECT_MANAGER = 'Connect Manager';
const ROLE_CONNECT_COPILOT_MANAGER = 'Connect Copilot Manager';
Expand Down Expand Up @@ -123,31 +126,53 @@ const EVENTS = [
version: 2,
projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, PROJECT_ROLE_MEMBER],
toMentionedUsers: true,
exclude: {
privatePostsForProjectRoles: [PROJECT_ROLE_CUSTOMER],
},
}, {
type: BUS_API_EVENT.CONNECT.POST.CREATED,
version: 2,
projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, PROJECT_ROLE_MEMBER],
toTopicStarter: true,
toMentionedUsers: true,
exclude: {
draftPhasesForProjectRoles: [PROJECT_ROLE_CUSTOMER],
privatePostsForProjectRoles: [PROJECT_ROLE_CUSTOMER],
},
}, {
type: BUS_API_EVENT.CONNECT.POST.UPDATED,
version: 2,
projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, PROJECT_ROLE_MEMBER],
toTopicStarter: true,
toMentionedUsers: true,
exclude: {
draftPhasesForProjectRoles: [PROJECT_ROLE_CUSTOMER],
privatePostsForProjectRoles: [PROJECT_ROLE_CUSTOMER],
},
}, {
type: BUS_API_EVENT.CONNECT.POST.MENTION,
exclude: {
draftPhasesForProjectRoles: [PROJECT_ROLE_CUSTOMER],
privatePostsForProjectRoles: [PROJECT_ROLE_CUSTOMER],
},
},
{
type: BUS_API_EVENT.CONNECT.TOPIC.DELETED,
version: 2,
projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, PROJECT_ROLE_MEMBER],
toTopicStarter: false,
exclude: {
privatePostsForProjectRoles: [PROJECT_ROLE_CUSTOMER],
},
},
{
type: BUS_API_EVENT.CONNECT.POST.DELETED,
version: 2,
projectRoles: [PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, PROJECT_ROLE_MEMBER],
exclude: {
draftPhasesForProjectRoles: [PROJECT_ROLE_CUSTOMER],
privatePostsForProjectRoles: [PROJECT_ROLE_CUSTOMER],
},
},
{
type: BUS_API_EVENT.CONNECT.PROJECT.LINK_CREATED,
Expand Down
16 changes: 16 additions & 0 deletions connect/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
* Helper functions
*/
const Remarkable = require('remarkable');
const _ = require('lodash');

const PHASE_ID_REGEXP = /phase#(\d+)/;

/**
* Convert markdown into raw draftjs state
Expand Down Expand Up @@ -42,7 +45,20 @@ const sanitizeEmail = (email) => {
return '';
};

/**
* Helper method to extract phaseId from tag
*
* @param {Array} tags list of message tags
*
* @returns {String} phase id
*/
const extractPhaseId = (tags) => {
const phaseIds = tags.map((tag) => _.get(tag.match(PHASE_ID_REGEXP), '1', null));
return _.find(phaseIds, (phaseId) => phaseId !== null);
};

module.exports = {
markdownToHTML,
sanitizeEmail,
extractPhaseId,
};
Loading