diff --git a/README.md b/README.md index affbd38..b08fb02 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,10 @@ The following parameters can be set in config files or in env variables: - `REPLY_EMAIL_PREFIX`: prefix of the genereated reply email address - `REPLY_EMAIL_DOMAIN`: email domain - `DEFAULT_REPLY_EMAIL`: default reply to email address, for example no-reply@topcoder.com +- **Slack api** + - `SLACK_URL`: slack api url to post messages + - `SLACK_BOT_TOKEN`: slack bot user OAuth token + - `SLACK_NOTIFY`: slack notification switch, set to 'true' to enable slack notifications. Note that the above two configuration are separate because the common notification server config will be deployed to a NPM package, the connect notification server will use that NPM package, diff --git a/config/default.js b/config/default.js index 9d426a7..1f772e7 100644 --- a/config/default.js +++ b/config/default.js @@ -28,9 +28,9 @@ 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_BASE_URL: process.env.TC_API_V5_BASE_URL || 'https://api.topcoder-dev.com/v5', API_CONTEXT_PATH: process.env.API_CONTEXT_PATH || '/v5/notifications', TC_API_BASE_URL: process.env.TC_API_BASE_URL || '', @@ -47,7 +47,12 @@ module.exports = { AUTH0_CLIENT_ID: process.env.AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET: process.env.AUTH0_CLIENT_SECRET, AUTH0_PROXY_SERVER_URL: process.env.AUTH0_PROXY_SERVER_URL, - + // Slack configuration. + SLACK: { + URL: process.env.SLACK_URL || 'https://slack.com/api/chat.postMessage', + BOT_TOKEN: process.env.SLACK_BOT_TOKEN, + NOTIFY: process.env.SLACK_NOTIFY === 'true', + }, 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 @@ -115,19 +120,22 @@ module.exports = { // }, // }, // ], - // */ // issue - https://github.com/topcoder-platform/community-app/issues/4108 + // */ // issue - https://github.com/topcoder-platform/community-app/issues/4108 'admin.notification.broadcast': [{ - handleBulkNotification: {} - } - ] - //'notifications.community.challenge.created': ['handleChallengeCreated'], - //'notifications.community.challenge.phasewarning': ['handleChallengePhaseWarning'], + handleBulkNotification: {}, + }, + ], + 'notifications.action.create': [{ + handleUniversalNotification: {}, + }], + // 'notifications.community.challenge.created': ['handleChallengeCreated'], + // 'notifications.community.challenge.phasewarning': ['handleChallengePhaseWarning'], }, // 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, diff --git a/connect/config.js b/connect/config.js index c54bc28..e5d985c 100644 --- a/connect/config.js +++ b/connect/config.js @@ -36,6 +36,6 @@ module.exports = { DEFAULT_REPLY_EMAIL: process.env.DEFAULT_REPLY_EMAIL, CONNECT_URL: process.env.CONNECT_URL || 'https://connect.topcoder-dev.com', - ACCOUNTS_APP_URL: process.env.ACCOUNTS_APP_URL || "https://accounts-auth0.topcoder-dev.com", + ACCOUNTS_APP_URL: process.env.ACCOUNTS_APP_URL || 'https://accounts-auth0.topcoder-dev.com', TC_CDN_URL: process.env.TC_CDN_URL, }; diff --git a/constants.js b/constants.js index 4cadd8f..ba27c18 100644 --- a/constants.js +++ b/constants.js @@ -3,11 +3,14 @@ module.exports = { SEARCH_USERS_PAGE_SIZE: 5, SETTINGS_EMAIL_SERVICE_ID: 'email', + SETTINGS_WEB_SERVICE_ID: 'web', + SETTINGS_SLACK_SERVICE_ID: 'slack', ACTIVE_USER_STATUSES: ['ACTIVE'], BUS_API_EVENT: { EMAIL: { GENERAL: 'connect.notification.email.project.notifications.generic', + UNIVERSAL: 'external.action.email', }, }, }; diff --git a/docs/tc-notifications.postman_collection.json b/docs/tc-notifications.postman_collection.json new file mode 100644 index 0000000..fd6249a --- /dev/null +++ b/docs/tc-notifications.postman_collection.json @@ -0,0 +1,38 @@ +{ + "info": { + "_postman_id": "ad14efc8-1fed-4914-8273-330754500801", + "name": "TC-NOTIFICATIONS", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "list notifications", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{TOKEN}}", + "type": "text" + } + ], + "url": { + "raw": "{{URL}}/list?limit=100", + "host": [ + "{{URL}}" + ], + "path": [ + "list" + ], + "query": [ + { + "key": "limit", + "value": "100" + } + ] + } + }, + "response": [] + } + ] +} \ No newline at end of file diff --git a/docs/tc-notifications.postman_environment.json b/docs/tc-notifications.postman_environment.json new file mode 100644 index 0000000..d338b9e --- /dev/null +++ b/docs/tc-notifications.postman_environment.json @@ -0,0 +1,19 @@ +{ + "id": "9d9c9e1b-6004-4bbe-9a98-55ad3a5838d7", + "name": "tc-notifications", + "values": [ + { + "key": "URL", + "value": "http://localhost:3000/v5/notifications", + "enabled": true + }, + { + "key": "TOKEN", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjIxNDc0ODM2NDgsInVzZXJJZCI6IjQwMTUyODU2IiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.PKv0QrMCPf0-ZPjv4PGWT7eXne54m7i9YX9eq-fceMU", + "enabled": true + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2021-07-24T21:01:35.787Z", + "_postman_exported_using": "Postman/8.8.0" +} \ No newline at end of file diff --git a/local/Verification.md b/local/Verification.md new file mode 100644 index 0000000..cb85dae --- /dev/null +++ b/local/Verification.md @@ -0,0 +1,95 @@ +# Deployment & Verification Guide + +> This guide is prepared for Linux OS, npm install command fails for Windows. +> Please switch to Node.js version 8.x before proceed. + +1. I prepared a docker-compose file for local deployment. It will start the following services: + * postgres + * zookeeper + * kafka + * bus-api +2. Run following commands to start dependency services: + ```bash + cd local + docker-compose build --no-cache + docker-compose up -d + ``` +3. After services have started successfully, set environment variables for notifications api: + ```sh + # you need to provide the values for the next variables + export AUTH0_CLIENT_ID + export AUTH0_CLIENT_SECRET + export SLACK_BOT_TOKEN + + # for other variables can uses these ones + export AUTH0_URL='https://topcoder-dev.auth0.com/oauth/token' + export AUTH0_AUDIENCE='https://m2m.topcoder-dev.com/' + export AUTH0_AUDIENCE_UBAHN='https://u-bahn.topcoder.com' + export AUTH_SECRET='mysecret' + export VALID_ISSUERS='["https://api.topcoder-dev.com", "https://api.topcoder.com", "https://topcoder-dev.auth0.com/", "https://auth.topcoder-dev.com/"]' + export DATABASE_URL='postgres://postgres:postgres@localhost:5432/postgres' + export PORT=4000 + export TC_API_V5_BASE_URL='http://localhost:8002/v5' + export KAFKA_URL='localhost:9092' + export KAFKA_GROUP_ID='tc-notifications' + export SLACK_NOTIFY='true' + export ENABLE_DEV_MODE='false' + ``` + +4. Run command `npm install` +5. Run command `npm run reset:db` to initialize tables. +6. Run command `npm run startConsumer` to start kafka consumer. +7. Wait to see following logs: + ```bash + 2021-07-24T15:11:01.512Z INFO no-kafka-client Joined group tc-notifications generationId 1 as no-kafka-client-2689c63f-9850-448a-a3f0-11d2ec8e49ce + 2021-07-24T15:11:01.512Z INFO no-kafka-client Elected as group leader + 2021-07-24T15:11:01.583Z DEBUG no-kafka-client Subscribed to notifications.autopilot.events:0 offset 0 leader localhost:9092 + 2021-07-24T15:11:01.583Z DEBUG no-kafka-client Subscribed to challenge.notification.events:0 offset 0 leader localhost:9092 + 2021-07-24T15:11:01.584Z DEBUG no-kafka-client Subscribed to notification.action.create:0 offset 0 leader localhost:9092 + 2021-07-24T15:11:01.584Z DEBUG no-kafka-client Subscribed to admin.notification.broadcast:0 offset 0 leader localhost:9092 + ``` +8. Now, we will prepare terminals to be used later for verifying api. + * 1 terminal to watch kafka topic for email notifications. + * 1 terminal to query Notifications table for web notifications. + * Slack app, web or mobile application. +9. Open a new terminal for watching kafka topic `external.action.email` + ```bash + docker exec notification-kafka /opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic external.action.email + ``` +10. **Alternative-1:** Open a new terminal for postgres. It will be used to check if notifications are getting created. + ```bash + docker exec -it notification-postgres psql -U postgres + \c postgres + SELECT * FROM "Notifications"; + ``` + **Alternative-2:** Start notifications api and use postman to list notifications. Open a new terminal and change PORT to 3000 + ```bash + . ./environment.sh + export PORT=3000 + npm run startAPI + ``` + Open postman and use following collection and environment: + * collection: docs/tc-notifications.postman_collection.json + * environment: docs/tc-notifications.postman_environment.json + + Existing postman collections are outdated and need to be converted to version 2.0.0. So, I had to create a new collection. + +11. To verify slack messages, you can use following workspace and user credentials. Token for posting messages to slack was already shared inside environment variables and has been set at step 3. + `https://tc-notifications.slack.com/` + username: `tc.notification.slack@gmail.com` + password: `@Topcoder123` + + +12. Now we can start testing. Open a new terminal for producing kafka messages for the topic `notification.action.create` + ```bash + docker exec -it notification-kafka /opt/kafka/bin/kafka-console-producer.sh --broker-list localhost:9092 --topic notification.action.create + ``` +13. You can copy and paste the following messages to the terminal we opened for producing kafka messages. + send messages successfully + ```json + {"topic":"notification.action.create","originator":"tc-direct","timestamp":"2018-02-16T00:00:00","mime-type":"application\/json","payload":[{"serviceId":"email","type":"taas.notification.request-submitted","details":{"from":"example@example.com","recipients":[{"userId":123,"email":"test1@test.com"},{"userId":456,"email":"test2@test.com"}],"cc":[{"userId":789,"email":"test3@test.com"},{"userId":987,"email":"test4@test.com"}],"data":{"subject":"...","body":"...","field1":"...","field2":"...","filedN":"..."},"sendgridTemplateId":"...","version":"v3"}},{"serviceId":"slack","type":"taas.notification.request-submitted","details":{"channel":"general","text":"test message"}},{"serviceId":"web","type":"taas.notification.request-submitted","details":{"userId":40152856,"contents":{},"version":1}}]} + ``` + email: fail (invalid email), web: success, slack: success + ```json + {"topic":"notification.action.create","originator":"tc-direct","timestamp":"2018-02-16T00:00:00","mime-type":"application\/json","payload":[{"serviceId":"email","type":"taas.notification.request-submitted","details":{"from":"example.com","recipients":[{"userId":123,"email":"test1@test.com"},{"userId":456,"email":"test2@test.com"}],"cc":[{"userId":789,"email":"test3@test.com"},{"userId":987,"email":"test4@test.com"}],"data":{"subject":"...","body":"...","field1":"...","field2":"...","filedN":"..."},"sendgridTemplateId":"...","version":"v3"}},{"serviceId":"slack","type":"taas.notification.request-submitted","details":{"channel":"random","text":"test message"}},{"serviceId":"web","type":"taas.notification.request-submitted","details":{"userId":40152856,"contents":{},"version":1}}]} + ``` \ No newline at end of file diff --git a/local/docker-compose.yml b/local/docker-compose.yml new file mode 100644 index 0000000..9ed82a2 --- /dev/null +++ b/local/docker-compose.yml @@ -0,0 +1,56 @@ +version: "3" +services: + postgres: + container_name: notification-postgres + image: postgres:11.8 + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - 5432:5432 + + zookeeper: + image: wurstmeister/zookeeper + container_name: notification-zookeeper + ports: + - 2181:2181 + + kafka: + image: wurstmeister/kafka + container_name: notification-kafka + depends_on: + - zookeeper + ports: + - 9092:9092 + environment: + KAFKA_ADVERTISED_LISTENERS: INSIDE://kafka:9093,OUTSIDE://localhost:9092 + KAFKA_LISTENERS: INSIDE://kafka:9093,OUTSIDE://0.0.0.0:9092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: INSIDE + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_CREATE_TOPICS: "challenge.notification.events:1:1,notifications.autopilot.events:1:1,admin.notification.broadcast:1:1,notifications.action.create:1:1,external.action.email:1:1" + + tc-bus-api: + container_name: tc-bus-api + build: + context: ./generic-tc-service + args: + NODE_VERSION: 8.11.3 + GIT_URL: https://github.com/topcoder-platform/tc-bus-api + GIT_BRANCH: dev + BYPASS_TOKEN_VALIDATION: 1 + command: start + ports: + - 8002:8002 + depends_on: + - kafka + environment: + - PORT=8002 + - KAFKA_URL=kafka:9093 + - JWT_TOKEN_SECRET=secret + - VALID_ISSUERS="[\"https:\/\/topcoder-newauth.auth0.com\/\",\"https:\/\/api.topcoder-dev.com\",\"https:\/\/topcoder-dev.auth0.com\/\"]" + - AUTH0_URL + - AUTH0_AUDIENCE + - AUTH0_CLIENT_ID + - AUTH0_CLIENT_SECRET + - AUTH0_PROXY_SERVER_URL diff --git a/local/generic-tc-service/Dockerfile b/local/generic-tc-service/Dockerfile new file mode 100644 index 0000000..2d9c886 --- /dev/null +++ b/local/generic-tc-service/Dockerfile @@ -0,0 +1,15 @@ +ARG NODE_VERSION=12.16.3 + +FROM node:$NODE_VERSION +ARG GIT_URL +ARG GIT_BRANCH +ARG BYPASS_TOKEN_VALIDATION + +RUN git clone $GIT_URL /opt/app +WORKDIR /opt/app +RUN git checkout -b node-branch origin/$GIT_BRANCH + +RUN npm install +RUN if [ $BYPASS_TOKEN_VALIDATION -eq 1 ]; then sed -i '/decodedToken = jwt.decode/a \ callback(undefined, decodedToken.payload); return;' node_modules/tc-core-library-js/lib/auth/verifier.js; fi +COPY docker-entrypoint.sh /opt/ +ENTRYPOINT ["/opt/docker-entrypoint.sh"] \ No newline at end of file diff --git a/local/generic-tc-service/docker-entrypoint.sh b/local/generic-tc-service/docker-entrypoint.sh new file mode 100755 index 0000000..2c70231 --- /dev/null +++ b/local/generic-tc-service/docker-entrypoint.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +HOST_DOMAIN="host.docker.internal" +ping -q -c1 $HOST_DOMAIN > /dev/null 2>&1 +if [ $? -ne 0 ]; then + HOST_IP=$(ip route | awk 'NR==1 {print $3}') + echo -e "$HOST_IP\t$HOST_DOMAIN" >> /etc/hosts +fi + +cd /opt/app/ && npm run $1 \ No newline at end of file diff --git a/src/common/tcApiHelper.js b/src/common/tcApiHelper.js index ed67c59..7cf6654 100644 --- a/src/common/tcApiHelper.js +++ b/src/common/tcApiHelper.js @@ -88,6 +88,103 @@ function* getUsersByHandles(handles) { return yield searchUsersByQuery(query); } +/** + * Get users by handles or userIds. + * @param {Array} handles the objects that has user handles. + * @param {Array} userIds the objects that has userIds. + * @returns {Array} 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} emails the objects that has user emails. + * @returns {Array} 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} ids the objects that has user uuids. + * @returns {Array} the matched users + */ +function* getUsersByUserUUIDs(ids, enrich) { + 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_BASE_URL}/users/${id.userUUID}${enrich ? '?enrich=true' : ''}`) + .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(_.get(err, 'response.text', err.toString())); + error.status = err.status; + throw error; + } +} + /** * Send message to bus. * @param {Object} data the data to send @@ -108,12 +205,148 @@ function* sendMessageToBus(data) { }); } +/** + * Notify slack channel. + * @param {string} channel the slack channel name + * @param {string} text the message + * @param {string} blocks rich formatted message as per https://api.slack.com/block-kit + */ +function* notifySlackChannel(channel, text, blocks) { + if (config.SLACK.NOTIFY) { + const token = config.SLACK.BOT_TOKEN; + const url = config.SLACK.URL; + const res = yield request + .post(url) + .set('Content-Type', 'application/json') + .set('Authorization', `Bearer ${token}`) + .send({ channel, text, blocks }) + .catch((err) => { + const errorDetails = _.get(err, 'message'); + throw new Error( + 'Error posting message to Slack API.' + + (errorDetails ? ' Server response: ' + errorDetails : '') + ); + }); + if (res.body.ok) { + logger.info(`Message posted successfully to channel: ${channel}`); + } else { + logger.error(`Error posting message to Slack API: ${JSON.stringify(res.body, null, 4)}`); + } + } else { + logger.info(`Slack message won't be sent to channel: ${channel}`); + } +} + +/** + * Check if notification is explicitly disabled for given notification type. + * @param {number} userId the user id + * @param {string} notificationType the notification type + * @param {string} serviceId the service id + * @returns {boolean} is notification enabled? + */ +function* checkNotificationSetting(userId, notificationType, serviceId) { + const settings = yield NotificationService.getSettings(userId); + if (settings.notifications[notificationType] + && settings.notifications[notificationType][serviceId] + && settings.notifications[notificationType][serviceId].enabled === 'no' + ) { + return false; + } + return true; +} + +/** + * Notify user via web. + * @param {Object} message the Kafka message payload + * @return {Array} notification details. + */ +function* notifyUserViaWeb(message) { + const notificationType = message.type; + 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.`); + continue; + } + notifications.push(_.assign( + {}, + _.pick(message.details, ['contents', 'version']), + { + userId, + type: message.type, + } + )); + } + + return notifications; +} + /** * Notify user via email. + * @param {Object} message the Kafka message payload + */ +function* notifyUserViaEmail(message) { + const notificationType = message.type; + const topic = constants.BUS_API_EVENT.EMAIL.UNIVERSAL; + const cc = _.map(_.filter(message.details.cc, c => !_.isUndefined(c.email)), 'email'); + for (const recipient of message.details.recipients) { + const userId = recipient.userId; + let userEmail; + // if dev mode for email is enabled then replace recipient email + if (config.ENABLE_DEV_MODE) { + userEmail = config.DEV_MODE_EMAIL; + } else { + userEmail = recipient.email; + if (!userEmail) { + logger.error(`Email not received for user: ${JSON.stringify(recipient, null, 4)}`); + 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, + recipients, + cc, + data: message.details.data || {}, + sendgrid_template_id: message.details.sendgridTemplateId, + version: message.details.version, + }; + // send email message to bus api. + yield sendMessageToBus({ + topic, + originator: 'tc-notifications', + timestamp: (new Date()).toISOString(), + 'mime-type': 'application/json', + payload, + }); + logger.info(`Successfully sent ${topic} event with body ${JSON.stringify(payload, null, 4)} to bus api`); + } +} + +/** + * Notify challenge user via email. * @param {Object} user the user * @param {Object} message the Kafka message JSON */ -function* notifyUserViaEmail(user, message) { +function* notifyChallengeUserViaEmail(user, message) { const notificationType = message.topic; const eventType = constants.BUS_API_EVENT.EMAIL.GENERAL; @@ -381,8 +614,15 @@ module.exports = { getM2MToken, getUsersBySkills, getUsersByHandles, + getUsersByHandlesAndUserIds, + getUsersByEmails, + getUsersByUserUUIDs, sendMessageToBus, + notifySlackChannel, + checkNotificationSetting, + notifyUserViaWeb, notifyUserViaEmail, + notifyChallengeUserViaEmail, getChallenge, notifyUsersOfMessage, getUsersInfoFromChallenge, diff --git a/src/controllers/NotificationController.js b/src/controllers/NotificationController.js index 5f91450..b885ab7 100644 --- a/src/controllers/NotificationController.js +++ b/src/controllers/NotificationController.js @@ -32,15 +32,15 @@ function* listNotifications(req, res) { * disabling v5 API feature temporarily for connect-app (backward compatibility) */ - //res.json(items); + // res.json(items); // TODO disable this and revert to original res.json({ items, offset: currentPage, limit: perPage, - totalCount: total - }) + totalCount: total, + }); } function* updateNotification(req, res) { diff --git a/src/hooks/hookBulkMessage.js b/src/hooks/hookBulkMessage.js index 70801c0..97d5b91 100644 --- a/src/hooks/hookBulkMessage.js +++ b/src/hooks/hookBulkMessage.js @@ -2,182 +2,180 @@ * Hook to insert broadcast notification into database for a user. */ -'use strict' +'use strict'; -const _ = require('lodash') -const logger = require('../common/logger') -const models = require('../models') -const api = require('../common/broadcastAPIHelper') +const _ = require('lodash'); +const logger = require('../common/logger'); +const models = require('../models'); +const api = require('../common/broadcastAPIHelper'); -const logPrefix = "BulkNotificationHook: " +const logPrefix = 'BulkNotificationHook: '; /** * CREATE NEW TABLES IF NOT EXISTS */ models.BulkMessages.sync().then((t) => { - models.BulkMessageUserRefs.sync() -}) + models.BulkMessageUserRefs.sync(); +}); /** - * Main function - * @param {Integer} userId + * Main function + * @param {Integer} userId */ async function checkBulkMessageForUser(userId) { - return new Promise(function (resolve, reject) { - models.BulkMessages.count().then(function (tBulkMessages) { - if (tBulkMessages > 0) { + return new Promise(function (resolve, reject) { + models.BulkMessages.count().then(function (tBulkMessages) { + if (tBulkMessages > 0) { // the condition can help to optimize the execution - models.BulkMessageUserRefs.count({ - where: { - user_id: userId - } - }).then(async function (tUserRefs) { - let result = true - if (tUserRefs < tBulkMessages) { - logger.info(`${logPrefix} Need to sync broadcast message for current user ${userId}`) - result = await syncBulkMessageForUser(userId) - } - resolve(result) // resolve here - }).catch((e) => { - reject(`${logPrefix} Failed to check total userRefs condition. Error: ${e}`) - }) - } else { - resolve(true) - } + models.BulkMessageUserRefs.count({ + where: { + user_id: userId, + }, + }).then(async function (tUserRefs) { + let result = true; + if (tUserRefs < tBulkMessages) { + logger.info(`${logPrefix} Need to sync broadcast message for current user ${userId}`); + result = await syncBulkMessageForUser(userId); + } + resolve(result); // resolve here }).catch((e) => { - logger.error(`${logPrefix} Failed to check total broadcast message condition. Error: `, e) - reject(e) - }) - }) + reject(`${logPrefix} Failed to check total userRefs condition. Error: ${e}`); + }); + } else { + resolve(true); + } + }).catch((e) => { + logger.error(`${logPrefix} Failed to check total broadcast message condition. Error: `, e); + reject(e); + }); + }); } /** - * Helper function - * @param {Integer} userId + * Helper function + * @param {Integer} userId */ async function syncBulkMessageForUser(userId) { - - return new Promise(function (resolve, reject) { + return new Promise(function (resolve, reject) { /** * Check if all bulk mesaages processed for current user or not */ - let q = "SELECT a.* FROM bulk_messages AS a " + - " LEFT OUTER JOIN (SELECT id as refid, bulk_message_id " + - " FROM bulk_message_user_refs AS bmur WHERE bmur.user_id=$1)" + - " AS b ON a.id=b.bulk_message_id WHERE b.refid IS NULL" - let memberInfo = [] - let userGroupInfo = [] - models.sequelize.query(q, { bind: [userId] }) + let q = 'SELECT a.* FROM bulk_messages AS a ' + + ' LEFT OUTER JOIN (SELECT id as refid, bulk_message_id ' + + ' FROM bulk_message_user_refs AS bmur WHERE bmur.user_id=$1)' + + ' AS b ON a.id=b.bulk_message_id WHERE b.refid IS NULL'; + let memberInfo = []; + let userGroupInfo = []; + models.sequelize.query(q, { bind: [userId] }) .then(async function (res) { - try { - memberInfo = await api.getMemberInfo(userId) - userGroupInfo = await api.getUserGroup(userId) - } catch (e) { - reject(`${logPrefix} Failed to get member/group info: ${e}`) - } - Promise.all(res[0].map((r) => isBroadCastMessageForUser(userId, r, memberInfo, userGroupInfo))) + try { + memberInfo = await api.getMemberInfo(userId); + userGroupInfo = await api.getUserGroup(userId); + } catch (e) { + reject(`${logPrefix} Failed to get member/group info: ${e}`); + } + Promise.all(res[0].map((r) => isBroadCastMessageForUser(userId, r, memberInfo, userGroupInfo))) .then((results) => { - Promise.all(results.map((o) => { - if (o.result) { - return createNotificationForUser(userId, o.record) - } else { - return insertUserRefs(userId, o.record.id, null) - } - })).then((results) => { - resolve(results) - }).catch((e) => { - reject(e) - }) + Promise.all(results.map((o) => { + if (o.result) { + return createNotificationForUser(userId, o.record); + } else { + return insertUserRefs(userId, o.record.id, null); + } + })).then((results) => { + resolve(results); + }).catch((e) => { + reject(e); + }); }).catch((e) => { - reject(e) - }) + reject(e); + }); }).catch((e) => { - reject(`${logPrefix} Failed to check bulk message condition: error - ${e}`) - }) - }) + reject(`${logPrefix} Failed to check bulk message condition: error - ${e}`); + }); + }); } /** - * Helper function - * Check if current user in broadcast recipent group - * @param {Integer} userId - * @param {Object} bulkMessage + * Helper function + * Check if current user in broadcast recipent group + * @param {Integer} userId + * @param {Object} bulkMessage * @param {Object} memberInfo * @param {Object} userGroupInfo * - * @retun promise + * @retun promise */ async function isBroadCastMessageForUser(userId, bulkMessage, memberInfo, userGroupInfo) { - return api.checkBroadcastMessageForUser(userId, bulkMessage, memberInfo, userGroupInfo) + return api.checkBroadcastMessageForUser(userId, bulkMessage, memberInfo, userGroupInfo); } /** * Helper function - Insert in bulkMessage user reference table - * - * @param {Integer} userId - * @param {Integer} bulkMessageId + * + * @param {Integer} userId + * @param {Integer} bulkMessageId * @param {Object} notificationObj */ async function insertUserRefs(userId, bulkMessageId, notificationObj) { - let notificationId = null - if (notificationObj) { - notificationId = notificationObj.id - } - try { - const r = await models.BulkMessageUserRefs.create({ - bulk_message_id: bulkMessageId, - user_id: userId, - notification_id: notificationId, - }) - logger.info(`${logPrefix} Inserted userRef record for bulk message id ${r.id} for current user ${userId}`) - return r - } catch (e) { - logger.error(`${logPrefix} Failed to insert userRef record for user: ${userId}, error: ${e}`) - if (notificationId && notificationObj) { - try { - await notificationObj.destroy() - logger.info(`Deleted/reverted duplicate/ref-transaction failed, broadcast notification ${notificationId} for user: ${userId}`) - } catch (error) { - logger.error(`Error in deleting duplicate notification record, ${error}`) - } - - } - throw new Error(`insertUserRefs() : ${e}`) + let notificationId = null; + if (notificationObj) { + notificationId = notificationObj.id; + } + try { + const r = await models.BulkMessageUserRefs.create({ + bulk_message_id: bulkMessageId, + user_id: userId, + notification_id: notificationId, + }); + logger.info(`${logPrefix} Inserted userRef record for bulk message id ${r.id} for current user ${userId}`); + return r; + } catch (e) { + logger.error(`${logPrefix} Failed to insert userRef record for user: ${userId}, error: ${e}`); + if (notificationId && notificationObj) { + try { + await notificationObj.destroy(); + logger.info(`Deleted/reverted duplicate/ref-transaction failed, broadcast notification ${notificationId} for user: ${userId}`); + } catch (error) { + logger.error(`Error in deleting duplicate notification record, ${error}`); + } } + throw new Error(`insertUserRefs() : ${e}`); + } } /** - * Helper function - * @param {Integer} userId - * @param {Object} bulkMessage + * Helper function + * @param {Integer} userId + * @param {Object} bulkMessage */ async function createNotificationForUser(userId, bulkMessage) { - try { - const n = await models.Notification.create({ - userId: userId, - type: bulkMessage.type, - contents: { - id: bulkMessage.id, /** broadcast message id */ - message: bulkMessage.message, /** broadcast message */ - group: 'broadcast', - title: 'Broadcast Message', - }, - read: false, - seen: false, - version: null, - }) - logger.info(`${logPrefix} Inserted notification record ${n.id} for current user ${userId}`) + try { + const n = await models.Notification.create({ + userId, + type: bulkMessage.type, + contents: { + id: bulkMessage.id, /** broadcast message id */ + message: bulkMessage.message, /** broadcast message */ + group: 'broadcast', + title: 'Broadcast Message', + }, + read: false, + seen: false, + version: null, + }); + logger.info(`${logPrefix} Inserted notification record ${n.id} for current user ${userId}`); // TODO need to be in transaction so that rollback will be possible - const result = await insertUserRefs(userId, bulkMessage.id, n) - return result - } catch (e) { - logger.error(`${logPrefix} insert broadcast notification error: ${e} `) - throw new Error(`createNotificationForUser() : ${e}`) - } + const result = await insertUserRefs(userId, bulkMessage.id, n); + return result; + } catch (e) { + logger.error(`${logPrefix} insert broadcast notification error: ${e} `); + throw new Error(`createNotificationForUser() : ${e}`); + } } // Exports module.exports = { - checkBulkMessageForUser, -}; \ No newline at end of file + checkBulkMessageForUser, +}; diff --git a/src/hooks/index.js b/src/hooks/index.js index 5dcc1a6..ff68fed 100644 --- a/src/hooks/index.js +++ b/src/hooks/index.js @@ -3,13 +3,13 @@ */ /** - * Hook implementation + * Hook implementation * * @author TCSCODER * @version 1.0 */ -const hookBulkMessage = require("./hookBulkMessage") +const hookBulkMessage = require('./hookBulkMessage'); module.exports = { diff --git a/src/models/BulkMessageUserRefs.js b/src/models/BulkMessageUserRefs.js index 7f3baf3..db97f1d 100644 --- a/src/models/BulkMessageUserRefs.js +++ b/src/models/BulkMessageUserRefs.js @@ -11,24 +11,24 @@ module.exports = (sequelize, DataTypes) => sequelize.define('bulk_message_user_refs', { - id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, - bulk_message_id: { - type: DataTypes.BIGINT, - allowNull: false, - references: { - model: 'bulk_messages', - key: 'id' - } + id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, + bulk_message_id: { + type: DataTypes.BIGINT, + allowNull: false, + references: { + model: 'bulk_messages', + key: 'id', }, - notification_id: { - type: DataTypes.BIGINT, - allowNull: true, - references: { - model: 'Notifications', - key: 'id' - } + }, + notification_id: { + type: DataTypes.BIGINT, + allowNull: true, + references: { + model: 'Notifications', + key: 'id', }, - user_id: { type: DataTypes.BIGINT, allowNull: false } + }, + user_id: { type: DataTypes.BIGINT, allowNull: false }, }, {}); // sequelize will generate and manage createdAt, updatedAt fields diff --git a/src/processors/broadcast/bulkNotificationHandler.js b/src/processors/broadcast/bulkNotificationHandler.js index 93d1e04..f663d52 100644 --- a/src/processors/broadcast/bulkNotificationHandler.js +++ b/src/processors/broadcast/bulkNotificationHandler.js @@ -1,10 +1,10 @@ /** * Bulk notification handler. */ -const joi = require('joi') -const co = require('co') -const models = require('../../models') -const logger = require('../../common/logger') +const joi = require('joi'); +const co = require('co'); +const models = require('../../models'); +const logger = require('../../common/logger'); /** * Handle Kafka JSON message of broadcast. @@ -14,22 +14,23 @@ const logger = require('../../common/logger') * * @return {Promise} promise resolved to notifications */ +// eslint-disable-next-line no-unused-vars const handle = (message, ruleSets) => co(function* () { try { const bm = yield models.BulkMessages.create({ type: message.topic, message: message.payload.message, recipients: message.payload.recipients, - }) - logger.info("Broadcast message recieved and inserted in db with id:", bm.id) + }); + logger.info('Broadcast message recieved and inserted in db with id:', bm.id); } catch (e) { - logger.error(`Broadcast processor failed in db operation. Error: ${e}`) + logger.error(`Broadcast processor failed in db operation. Error: ${e}`); } - return [] // this point of time, send empty notification object + return []; // this point of time, send empty notification object }); /** - * validate kafka payload + * validate kafka payload */ handle.schema = { message: joi.object().keys({ @@ -49,4 +50,4 @@ module.exports = { handle, }; -logger.buildService(module.exports); \ No newline at end of file +logger.buildService(module.exports); diff --git a/src/processors/index.js b/src/processors/index.js index a0243be..a4b22b3 100644 --- a/src/processors/index.js +++ b/src/processors/index.js @@ -9,6 +9,7 @@ const ChallengeHandler = require('./challenge/ChallengeHandler'); const AutoPilotHandler = require('./challenge/AutoPilotHandler'); const SubmissionHandler = require('./challenge/SubmissionHandler'); const BulkNotificationHandler = require('./broadcast/bulkNotificationHandler'); +const UniversalNotificationHandler = require('./universal/universalNotificationHandler'); // Exports module.exports = { @@ -18,4 +19,5 @@ module.exports = { handleAutoPilot: AutoPilotHandler.handle, handleSubmission: SubmissionHandler.handle, handleBulkNotification: BulkNotificationHandler.handle, + handleUniversalNotification: UniversalNotificationHandler.handle, }; diff --git a/src/processors/universal/universalNotificationHandler.js b/src/processors/universal/universalNotificationHandler.js new file mode 100644 index 0000000..4b0b5e3 --- /dev/null +++ b/src/processors/universal/universalNotificationHandler.js @@ -0,0 +1,20 @@ +/** + * Universal notification handler. + */ + const co = require('co'); + const service = require('../../services/UniversalNotificationService'); + + /** + * Handle Kafka JSON message of notifications requested. + * + * @param {Object} message the Kafka JSON message + * + * @return {Promise} promise resolved to notifications + */ + const handle = (message) => co(function* () { + return yield service.handle(message); + }); + + module.exports = { + handle, + }; diff --git a/src/services/NotificationService.js b/src/services/NotificationService.js index 6fa81f8..c6501cd 100644 --- a/src/services/NotificationService.js +++ b/src/services/NotificationService.js @@ -61,8 +61,8 @@ getSettings.schema = { function* saveNotificationSetting(entry, userId) { const setting = yield models.NotificationSetting.findOne({ where: { - userId, topic: entry.topic, serviceId: entry.serviceId, name: entry.name - } + userId, topic: entry.topic, serviceId: entry.serviceId, name: entry.name, + }, }); if (setting) { setting.value = entry.value; @@ -86,8 +86,8 @@ function* saveNotificationSetting(entry, userId) { function* saveServiceSetting(entry, userId) { const setting = yield models.ServiceSettings.findOne({ where: { - userId, serviceId: entry.serviceId, name: entry.name - } + userId, serviceId: entry.serviceId, name: entry.name, + }, }); if (setting) { setting.value = entry.value; @@ -192,23 +192,32 @@ function* listNotifications(query, userId) { const filter = { where: { userId, - }, offset, limit, order: [['createdAt', 'DESC']] + }, offset, limit, order: [['createdAt', 'DESC']], }; + // eslint-disable-next-line default-case switch (query.platform) { case 'connect': filter.where.type = { $like: 'connect.notification.%' }; break; + case 'taas': + filter.where.type = { $like: 'taas.notification.%' }; + break; case 'community': - filter.where.type = { $notLike: 'connect.notification.%' }; + filter.where.type = { + $and: [ + { $notLike: 'connect.notification.%' }, + { $notLike: 'taas.notification.%' }, + ], + }; break; } if (config.ENABLE_HOOK_BULK_NOTIFICATION) { try { - yield hooks.hookBulkMessage.checkBulkMessageForUser(userId) + yield hooks.hookBulkMessage.checkBulkMessageForUser(userId); } catch (e) { - logger.error(`Error in calling bulk notification hook: ${e}`) + logger.error(`Error in calling bulk notification hook: ${e}`); } } diff --git a/src/services/UniversalNotificationService.js b/src/services/UniversalNotificationService.js new file mode 100644 index 0000000..f4927e6 --- /dev/null +++ b/src/services/UniversalNotificationService.js @@ -0,0 +1,236 @@ +/** + * Universal notification handler service. + */ + +'use strict'; +const _ = require('lodash'); +const joi = require('joi'); +const logger = require('../common/logger'); +const tcApiHelper = require('../common/tcApiHelper'); +const constants = require('../../constants'); + +const emailSchema = joi.object().keys({ + serviceId: joi.string().valid(constants.SETTINGS_EMAIL_SERVICE_ID).required(), + type: joi.string().required(), + details: joi.object().keys({ + from: joi.string().email().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(), + cc: joi.array().items( + joi.object().keys({ + userId: joi.number().integer(), + userUUID: joi.string().uuid(), + email: joi.string().email(), + handle: joi.string(), + }).min(1) + ), + data: joi.object().keys({ + subject: joi.string().allow(''), + body: joi.string().allow(''), + // any other fields are allowed inside `data` too + }).unknown(), + sendgridTemplateId: joi.string().required(), + version: joi.string().required(), + }).required(), +}).required(); + +const slackSchema = joi.object().keys({ + serviceId: joi.string().valid(constants.SETTINGS_SLACK_SERVICE_ID).required(), + type: joi.string().required(), + details: joi.object().keys({ + channel: joi.string().required(), + text: joi.string().required(), + blocks: joi.array().items(joi.object()), + }).required(), +}).required(); + +const webSchema = joi.object().keys({ + serviceId: joi.string().valid(constants.SETTINGS_WEB_SERVICE_ID).required(), + type: joi.string().required(), + details: joi.object().keys({ + 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(), +}).required(); + +function validator(data, schema) { + const validationResult = schema.validate(data); + if (validationResult.error) { + logger.error(validationResult.error.message); + return false; + } + return true; +} + + +/** + * Complete missing user fields of given payload details + * This function mutates the given details object. + * @param {Object} details the object which has recipients array + * @param {Boolean} findEmail true if emails are needed + * @param {Boolean} findUserId true if userIds are needed + * @returns {undefined} + */ +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, true); + 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 (!_.isUndefined(found.handle) && _.isUndefined(user.handle)) { + _.assign(user, { handle: found.handle }); + } + } + + if (findEmail) { + const usersHaveId = _.filter(getFieldsByUserUUID, u => !_.isUndefined(u.userId)); + const usersHaveHandle = _.filter(getFieldsByUserUUID, u => _.isUndefined(u.userId) && !_.isUndefined(u.handle)); + const foundUser = yield tcApiHelper.getUsersByHandlesAndUserIds(usersHaveHandle, usersHaveId); + if (!_.isEmpty(foundUser)) { + for (const user of getFieldsByUserUUID) { + const found = _.find(foundUser, !_.isUndefined(user.handle) + ? ['handle', user.handle] : ['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.notifications) { + try { + switch (data.serviceId) { + case constants.SETTINGS_EMAIL_SERVICE_ID: + if (validator(data, emailSchema)) { + // find missing emails and userIds + yield completeMissingFields(data.details, true, true); + yield tcApiHelper.notifyUserViaEmail(data); + } + break; + case constants.SETTINGS_SLACK_SERVICE_ID: + if (validator(data, slackSchema)) { + yield tcApiHelper.notifySlackChannel(data.details.channel, data.details.text, data.details.blocks); + } + break; + case constants.SETTINGS_WEB_SERVICE_ID: + if (validator(data, webSchema)) { + // find missing userIds + yield completeMissingFields(data.details, false, true); + const _notifications = yield tcApiHelper.notifyUserViaWeb(data); + if (_notifications) { + notifications.push(..._notifications); + } + } + break; + default: + break; + } + } catch (err) { + logger.logFullError(err); + } + } + return notifications; +} + +handle.schema = { + message: joi.object().keys({ + topic: joi.string().required(), + originator: joi.string().required(), + timestamp: joi.date().required(), + 'mime-type': joi.string().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(), +}; + +// Exports +module.exports = { + handle, +}; + +logger.buildService(module.exports); diff --git a/test/checkHooks.js b/test/checkHooks.js index 6371283..6f4844d 100644 --- a/test/checkHooks.js +++ b/test/checkHooks.js @@ -1,3 +1,3 @@ -const bulkhook = require("../src/hooks/hookBulkMessage") +const bulkhook = require('../src/hooks/hookBulkMessage'); -bulkhook.checkBulkMessageForUser(123) \ No newline at end of file +bulkhook.checkBulkMessageForUser(123);