diff --git a/controllers/helpers/taskrouter-helper.js b/controllers/helpers/taskrouter-helper.js index b6da9bf..3164b67 100644 --- a/controllers/helpers/taskrouter-helper.js +++ b/controllers/helpers/taskrouter-helper.js @@ -1,81 +1,84 @@ -const twilio = require('twilio') -const context = require('../../context') - -const TaskRouterCapability = twilio.jwt.taskrouter.TaskRouterCapability - -const client = twilio( - process.env.TWILIO_ACCOUNT_SID, - process.env.TWILIO_AUTH_TOKEN) - -module.exports.createTask = async (attributes = {}) => { - const configuration = context.get().configuration - - const payload = { - workflowSid: configuration.twilio.workflowSid, - attributes: JSON.stringify(attributes), - timeout: 3600, - taskChannel: 'voice' - } - - return client.taskrouter.workspaces(process.env.TWILIO_WORKSPACE_SID).tasks.create(payload) -} - -module.exports.getOngoingTasks = (name) => { - let query = {} - query.assignmentStatus = 'pending,assigned,reserved' - query.evaluateTaskAttributes = 'name=\'' + name + '\'' - - return client.taskrouter.workspaces(process.env.TWILIO_WORKSPACE_SID).tasks.list(query) -} +const twilio = require('twilio'); + +const TaskRouterCapability = twilio.jwt.taskrouter.TaskRouterCapability; + +const client = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN); + +module.exports.createTask = function (workflowSid, attributes) { + attributes = attributes || {}; + + const data = { + workflowSid: workflowSid, + attributes: JSON.stringify(attributes), + timeout: 3600, + taskChannel: 'voice' + }; + + return client.taskrouter.workspaces(process.env.TWILIO_WORKSPACE_SID).tasks.create(data); +}; + +module.exports.getOngoingTasks = function (name) { + return new Promise(function (resolve, reject) { + let query = {}; + query.assignmentStatus = 'pending,assigned,reserved'; + query.evaluateTaskAttributes = "name='" + name + "'"; + + client.taskrouter + .workspaces(process.env.TWILIO_WORKSPACE_SID) + .tasks.list(query) + .then((tasks) => { + return resolve(tasks); + }) + .catch((error) => { + return reject(error); + }); + }); +}; const buildWorkspacePolicy = (options) => { - options = options || {} - - const resources = options.resources || [] - const urlComponents = ['https://taskrouter.twilio.com', 'v1', 'Workspaces', process.env.TWILIO_WORKSPACE_SID] - - return new TaskRouterCapability.Policy({ - url: urlComponents.concat(resources).join('/'), - method: options.method || 'GET', - allow: true - }) -} - -module.exports.createWorkerCapabilityToken = (sid) => { - const workerCapability = new TaskRouterCapability({ - accountSid: process.env.TWILIO_ACCOUNT_SID, - authToken: process.env.TWILIO_AUTH_TOKEN, - workspaceSid: process.env.TWILIO_WORKSPACE_SID, - channelId: sid, - ttl: 3600, - }) - - const eventBridgePolicies = twilio.jwt.taskrouter.util.defaultEventBridgePolicies(process.env.TWILIO_ACCOUNT_SID, sid) - - const workspacePolicies = [ - // Workspace fetch Policy - buildWorkspacePolicy(), - // Workspace subresources fetch Policy - buildWorkspacePolicy({ resources: ['**'] }), - // Workspace resources update Policy - buildWorkspacePolicy({ resources: ['**'], method: 'POST' }), - ] - - eventBridgePolicies.concat(workspacePolicies).forEach(policy => { - workerCapability.addPolicy(policy) - }) - - return workerCapability -} - -module.exports.findWorker = (friendlyName) => { - const filter = { friendlyName: friendlyName } - - return client.taskrouter - .workspaces(process.env.TWILIO_WORKSPACE_SID) - .workers.list(filter) - .then(workers => { - return workers.find(worker => worker.friendlyName === friendlyName) - }) - -} \ No newline at end of file + options = options || {}; + + const resources = options.resources || []; + const urlComponents = [ + 'https://taskrouter.twilio.com', + 'v1', + 'Workspaces', + process.env.TWILIO_WORKSPACE_SID + ]; + + return new TaskRouterCapability.Policy({ + url: urlComponents.concat(resources).join('/'), + method: options.method || 'GET', + allow: true + }); +}; + +module.exports.createWorkerCapabilityToken = function (sid) { + const workerCapability = new TaskRouterCapability({ + accountSid: process.env.TWILIO_ACCOUNT_SID, + authToken: process.env.TWILIO_AUTH_TOKEN, + workspaceSid: process.env.TWILIO_WORKSPACE_SID, + channelId: sid, + ttl: 3600 + }); + + const eventBridgePolicies = twilio.jwt.taskrouter.util.defaultEventBridgePolicies( + process.env.TWILIO_ACCOUNT_SID, + sid + ); + + const workspacePolicies = [ + // Workspace fetch Policy + buildWorkspacePolicy(), + // Workspace subresources fetch Policy + buildWorkspacePolicy({ resources: ['**'] }), + // Workspace resources update Policy + buildWorkspacePolicy({ resources: ['**'], method: 'POST' }) + ]; + + eventBridgePolicies.concat(workspacePolicies).forEach((policy) => { + workerCapability.addPolicy(policy); + }); + + return workerCapability; +}; diff --git a/controllers/messaging-adapter-helper.js b/controllers/messaging-adapter-helper.js new file mode 100644 index 0000000..3193903 --- /dev/null +++ b/controllers/messaging-adapter-helper.js @@ -0,0 +1,53 @@ +module.exports.getFrom = (to, configuration) => { + switch (getMessengerChannelKey(to)) { + case 'messenger': + return 'messenger:' + configuration.twilio.facebookPageId; + case 'whatsapp': + return 'whatsapp:' + configuration.twilio.whatsAppPhoneNumber; + default: + return configuration.twilio.callerId; + } +}; + +module.exports.createTaskAttributes = (from, channel) => { + return { + title: `${getMessengerChannelDetail(from).friendlyName} request`, + text: getMessengerChannelDetail(from).text, + channel: 'chat', + endpoint: getMessengerChannelKey(from), + team: 'support', + name: from, + channelSid: channel.sid + }; +}; + +const getMessengerChannelKey = (from) => { + if (from.includes('messenger')) { + return 'messenger'; + } else if (from.includes('whatsapp')) { + return 'whatsapp'; + } + + return 'sms'; +}; + +const getMessengerChannelDetail = (from) => { + const meta = new Map(); + + meta.set('messenger', { + friendlyName: 'Facebook Messenger', + text: 'Customer requested support on Faceboook' + }); + + meta.set('whatsapp', { + friendlyName: 'WhatsApp', + text: 'Customer requested support on WhatsApp' + }); + + meta.set('sms', { + friendlyName: 'SMS', + text: 'Customer requested support via SMS' + }); + + return meta.get(getMessengerChannelKey(from)); +}; diff --git a/controllers/messaging-adapter.js b/controllers/messaging-adapter.js index abb8fc0..68822c5 100755 --- a/controllers/messaging-adapter.js +++ b/controllers/messaging-adapter.js @@ -1,260 +1,194 @@ -const twilio = require('twilio') -const async = require('async') +const twilio = require('twilio'); +const taskrouterHelper = require('./helpers/taskrouter-helper.js'); +const helper = require('./messaging-adapter-helper'); -const taskrouterHelper = require('./helpers/taskrouter-helper.js') +const client = twilio(process.env.TWILIO_API_KEY_SID, process.env.TWILIO_API_KEY_SECRET, { + accountSid: process.env.TWILIO_ACCOUNT_SID +}); -const client = twilio( - process.env.TWILIO_ACCOUNT_SID, - process.env.TWILIO_AUTH_TOKEN) +module.exports.inbound = async (req, res) => { + req.direction = 'inbound:'; -module.exports.inbound = function (req, res) { - req.direction = 'inbound: ' - - console.log(req.direction + 'request received: %j', req.body) + console.log(`${req.direction} request received: ${JSON.stringify(req.body)}`); /* basic request body validation */ if (!req.body.From) { - return res.status(500).json({ message: 'Invalid request body. "From" is required' }) + return res.status(500).json({ message: 'Invalid request body. "From" is required' }); } if (!req.body.Body) { - return res.status(500).json({ message: 'Invalid request body. "Body" is required' }) + return res.status(500).json({ message: 'Invalid request body. "Body" is required' }); } - retrieveChannel(req).then(function (channel) { - console.log(req.direction + 'channel ' + channel.sid + ' received') - - return taskrouterHelper.getOngoingTasks(req.body.From).then(function (tasks) { - console.log(req.direction + 'user ' + req.body.From + ' has ' + tasks.length + ' ongoing task(s)') - - if (tasks.length === 0) { - return createTask(req, channel) - } - - }).then(function () { - - return client.chat.services(process.env.TWILIO_CHAT_SERVICE_SID).channels(channel.sid).messages.create({ - from: req.body.From, - body: req.body.Body - }).then(function (message) { - console.log(req.direction + 'chat message for ' + message.from + ' on channel ' + channel.sid + ' created, body "' + message.body + '"') - res.setHeader('Content-Type', 'application/xml') - res.status(200).end() - }) - - }) - - }).catch(function (err) { - console.log(req.direction + 'create chat message failed: %s', JSON.stringify(err, Object.getOwnPropertyNames(err))) - res.setHeader('Content-Type', 'application/xml') - res.status(500).send(JSON.stringify(err, Object.getOwnPropertyNames(err))) - }) - -} - -var retrieveChannel = function (req) { - return new Promise(function (resolve, reject) { + try { + const channel = await retrieveChannel(req); - console.log(req.direction + 'retrieve channel via API for user ', req.body.From) + console.log(`${req.direction} channel ${channel.sid} received`); - return client.chat.services(process.env.TWILIO_CHAT_SERVICE_SID).channels('support_channel_' + req.body.From).fetch() - .then(function (channel) { - resolve(channel) - }).catch(function (err) { - console.error(req.direction + 'retrieve channel failed: %s', JSON.stringify(err, Object.getOwnPropertyNames(err))) - return createChannel(req).then(function (channel) { - resolve(channel) - }).catch(function (err) { - reject(err) - }) - }) - - }) - -} - -var createChannel = function (req) { - - return new Promise(function (resolve, reject) { - - async.waterfall([ - - function (callback) { - - client.chat.services(process.env.TWILIO_CHAT_SERVICE_SID).channels.create({ - friendlyName: 'Support Chat with ' + req.body.From, - uniqueName: 'support_channel_' + req.body.From, - attributes: JSON.stringify({ forwarding: true}) - }).then(function (channel) { - console.log(req.direction + 'channel ' + channel.sid + ' created') - callback(null, channel) - }).catch(function (err) { - console.error(req.direction + 'create channel failed: %s', JSON.stringify(err, Object.getOwnPropertyNames(err))) - callback(err) - }) - - }, function (channel, callback) { + if (!await hasActiveTask(req.body.From)) { + await createTask(req, channel); + } - client.chat.services(process.env.TWILIO_CHAT_SERVICE_SID).channels(channel.sid).members.create({ - identity: req.body.From - }).then(function (identity) { - console.log(req.direction + 'added member ' + identity.sid + '(' + req.body.From + ') to channel ' + channel.sid) - callback(null, channel) - }).catch(function (err) { - console.error(req.direction + 'added member ' + req.body.From + ' to channel ' + channel.sid + 'failed: %s', JSON.stringify(err, Object.getOwnPropertyNames(err))) - callback(err) - }) + const message = await createMessage(channel, req.body.From, req.body.Body); + + console.log( + `${req.direction} chat message from ${message.from} on channel ${channel.sid} created, body ${message.body}` + ); + + res.setHeader('Content-Type', 'application/xml'); + res.status(200).end(); + } catch (error) { + console.log( + req.direction + 'create chat message failed: %s', + JSON.stringify(error, Object.getOwnPropertyNames(error)) + ); + res.setHeader('Content-Type', 'application/xml'); + res.status(500).send(JSON.stringify(error, Object.getOwnPropertyNames(error))); + } +}; - }, function (channel, callback) { +const fetchChannel = async (sid) => { + const channel = await client.chat.services(process.env.TWILIO_CHAT_SERVICE_SID).channels(sid).fetch(); - createTask(req, channel).then(function (task) { - callback(null, channel) - }).catch(function (error) { - callback(error) - }) + return channel; +}; - } - ], function (err, channel) { - if (err) { - reject(err) - } else { - resolve(channel) - } - }) +const retrieveChannel = async (req) => { + console.log(`${req.direction} retrieve channel for user ${req.body.From}`); - }) + const uniqueName = `support_channel_${req.body.From}`; + const friendlyName = `Support Chat with ${req.body.From}`; -} + let channel; -var createTask = function (req, channel) { + try { + channel = await fetchChannel(uniqueName); - return new Promise(function (resolve, reject) { - let title = null - let text = null - let endpoint = null + return channel; + } catch (error) { + if (error.code === 20404) { + channel = await createChannel(uniqueName, friendlyName, req.body.From); - if (req.body.From.includes('messenger')) { - title = 'Facebook Messenger request' - text = 'Customer requested support on Faceboook' - endpoint = 'messenger' + return channel; } else { - title = 'SMS request' - text = 'Customer requested support by sending SMS' - endpoint = 'sms' + console.error( + `${req.direction} retrieve channel failed: ${JSON.stringify(error, Object.getOwnPropertyNames(error))}` + ); + throw error; } + } +}; - const attributes = { - title: title, - text: text, - channel: 'chat', - endpoint: endpoint, - team: 'support', - name: req.body.From, - channelSid: channel.sid - } +const createChannel = async (uniqueName, friendlyName, from) => { + const payload = { + friendlyName: friendlyName, + uniqueName: uniqueName, + attributes: JSON.stringify({ forwarding: true }) + }; - taskrouterHelper.createTask(req.configuration.twilio.workflowSid, attributes).then(task => { - console.log(req.direction + 'task ' + task.sid + ' created with attributes %j', task.attributes) - resolve(task) - }).catch(error => { - console.error('create task failed: %s', JSON.stringify(error, Object.getOwnPropertyNames(error))) - reject(error) - }) + const channel = await client.chat.services(process.env.TWILIO_CHAT_SERVICE_SID).channels.create(payload); - }) + console.log(`channel ${channel.sid} created`); -} + const member = await client.chat + .services(process.env.TWILIO_CHAT_SERVICE_SID) + .channels(channel.sid) + .members.create({ + identity: from + }); -var forwardChannel = function (channel, req) { + console.log(`added member ${member.sid} (${from}) to channel ${channel.sid}`); - return client.chat.services(process.env.TWILIO_CHAT_SERVICE_SID).channels(channel.sid).members.list() - .then(function (members) { + return channel; +}; - return new Promise(function (resolve, reject) { - console.log(req.direction + 'channel ' + channel.sid + ' has ' + members.length + ' member(s)') +const createMessage = async (channel, from, body) => { + const message = await client.chat + .services(process.env.TWILIO_CHAT_SERVICE_SID) + .channels(channel.sid) + .messages.create({ + from: from, + body: body + }); - async.each(members, function (member, callback) { + return message; +}; - /* never forward message the user who created it */ - if (req.body.From === member.identity) { - return callback() - } +const hasActiveTask = async (from) => { + const tasks = await taskrouterHelper.getOngoingTasks(from); - console.log(req.direction + 'forward message "' + req.body.Body + '" to identity ' + member.identity) + console.log(`user ${from} has ${tasks.length} ongoing task(s)`); - forwardMessage(member.identity, req.body.Body, req).then(function (message) { - callback() - }).catch(function (err) { - callback(err) - }) + return tasks.length > 0; +}; - }, function (err) { - if (err) { - return reject(err) - } +const createTask = async (req, channel) => { + const attributes = helper.createTaskAttributes(req.body.From, channel); - resolve() - }) + const task = await taskrouterHelper.createTask(attributes); - }) + console.log(` ${req.direction} task ${task.sid} created with attributes ${JSON.stringify(task.attributes)}`); - }) -} + return task; +}; -var forwardMessage = function (to, body, req) { +const forwardChannel = async (channel, req) => { + const members = await client.chat + .services(process.env.TWILIO_CHAT_SERVICE_SID) + .channels(channel.sid) + .members.list(); - return new Promise(function (resolve, reject) { - let from + await Promise.all( + members.map(async (member) => { + if (req.body.From !== member.identity) { + console.log(`${req.direction} forward message "${req.body.Body}" to identity ${member.identity}`); - if (to.includes('messenger')) { - from = 'messenger:' + req.configuration.twilio.facebookPageId - } else { - from = req.configuration.twilio.callerId - } - - client.messages.create({ - to: to, - from: from, - body: body - }) - .then(message => { - console.log(req.direction + 'message ' + message.sid + ' create, body is "' + body + '" sent to endpoint ' + to + ', sender is ' + from) - resolve(message) - }).catch(error => { - console.error(req.direction + ' sending message failed: %s', JSON.stringify(error, Object.getOwnPropertyNames(error))) - reject(error) + await forwardMessage(member.identity, req.body.Body, req); + } }) + ); +}; - }) +const forwardMessage = async (to, body, req) => { + const message = await client.messages.create({ + to: to, + from: helper.getFrom(to, req.configuration), + body: body + }); -} + console.log( + `${req.direction} message ${message.sid} create, body "${body}" sent to endpoint ${to}, sender is ${helper.getFrom( + to, + req.configuration + )}` + ); -module.exports.outbound = function (req, res) { - req.direction = 'outbound: ' + return message; +}; - console.log(req.direction + 'request received: %j', req.body.Body + ' - ' + new Date()) +module.exports.outbound = async (req, res) => { + req.direction = 'outbound: '; - client.chat.services(process.env.TWILIO_CHAT_SERVICE_SID).channels(req.body.ChannelSid).fetch() - .then(function (channel) { - console.log(req.direction + 'channel ' + channel.sid + ' received') + console.log(`${req.direction} message received "${req.body.Body}", channel ${req.body.ChannelSid} - ${new Date()}`); - let attributes = JSON.parse(channel.attributes) + try { + const channel = await fetchChannel(req.body.ChannelSid); - if (!attributes.forwarding) { - console.log(req.direction + 'channel ' + channel.sid + ' needs no forwarding') - res.status(200).end() - } else { + console.log(`${req.direction} channel ${channel.sid} received`); - forwardChannel(channel, req) - .then(function () { - console.log(req.direction + 'message forwarding for channel ' + channel.sid + ' done') - res.status(200).send('blah') - }) + let attributes = JSON.parse(channel.attributes); - } + if (!attributes.forwarding) { + console.log(`${req.direction} channel ${channel.sid} needs no forwarding`); + res.status(200).end(); + } else { + await forwardChannel(channel, req); - }).catch(function (err) { - console.log(req.direction + 'forwarding chat message failed: %s', res.convertErrorToJSON(err)) - res.status(500).send(res.convertErrorToJSON(err)) - }) -} \ No newline at end of file + console.log(`${req.direction} message forwarding for channel ${channel.sid} done`); + res.status(200).end(); + } + } catch (error) { + console.log(`${req.direction} forwarding chat message failed: ${res.convertErrorToJSON(error)}`); + res.status(500).send(res.convertErrorToJSON(error)); + } +}; diff --git a/controllers/tasks.js b/controllers/tasks.js index ca6264b..29d17c5 100644 --- a/controllers/tasks.js +++ b/controllers/tasks.js @@ -3,94 +3,79 @@ const taskrouterHelper = require('./helpers/taskrouter-helper.js'); const chatHelper = require('./helpers/chat-helper.js'); const videoHelper = require('./helpers/video-helper.js'); -module.exports.createCallback = async (req, res) => { - const attributes = { - title: 'Callback request', - text: req.body.text, - channel: 'callback', - name: req.body.name, - team: req.body.team, - phone: req.body.phone - }; - - try { - const task = await taskrouterHelper.createTask(attributes); - - const response = { - taskSid: task.sid - }; - - res.status(200).json(response); - } catch (error) { - res.status(500).json(res.convertErrorToJSON(error)); - } +module.exports.createCallback = function (req, res) { + taskrouterHelper + .createTask(req.configuration.twilio.workflowSid, req.body) + .then((task) => { + res.status(200).end(); + }) + .catch((error) => { + res.status(500).send(res.convertErrorToJSON(error)); + }); }; -module.exports.createChat = async (req, res) => { - const friendlyName = 'Support Chat with ' + req.body.identity - const uniqueName = `chat_room_${Math.random().toString(36).substring(7)}` +module.exports.createChat = function (req, res) { + const friendlyName = 'Support Chat with ' + req.body.identity; + const uniqueName = `chat_room_${Math.random().toString(36).substring(7)}`; - try { - const channel = await chatHelper.createChannel(friendlyName, uniqueName); + let payload = { + identity: req.body.identity, + token: chatHelper.createAccessToken(req.body.identity, req.body.endpoint).toJwt() + }; - const attributes = { - title: 'Chat request', - text: 'Customer entered chat via support page', - channel: 'chat', - name: req.body.identity, - chat: { + chatHelper + .createChannel(friendlyName, uniqueName) + .then((channel) => { + payload.channel = { sid: channel.sid, friendlyName: channel.friendlyName, uniqueName: channel.uniqueName - } - }; + }; - const task = await taskrouterHelper.createTask(attributes); + const attributes = { + title: 'Chat request', + text: 'Customer entered chat via support page', + channel: 'chat', + name: payload.identity, + channelSid: channel.sid + }; - const response = { - identity: req.body.identity, - token: chatHelper.createAccessToken(req.body.identity, req.body.endpointId).toJwt(), - chat: { - sid: channel.sid, - friendlyName: channel.friendlyName, - uniqueName: channel.uniqueName - }, - taskSid: task.sid - }; - - res.status(200).json(response); - } catch (error) { - res.status(500).json(res.convertErrorToJSON(error)); - } + return taskrouterHelper + .createTask(req.configuration.twilio.workflowSid, attributes) + .then((task) => { + payload.task = task.sid; + res.status(200).json(payload); + }); + }) + .catch((error) => { + res.status(500).json(error); + }); }; -module.exports.createVideo = async (req, res) => { - const roomName = `video_room_${Math.random().toString(36).substring(7)}` +module.exports.createVideo = function (req, res) { + const uid = Math.random().toString(36).substring(7); + + let payload = { + identity: req.body.identity, + token: videoHelper.createAccessToken(req.body.identity, uid).toJwt(), + roomName: uid + }; const attributes = { title: 'Video request', text: 'Customer requested video support on web page', channel: 'video', - name: req.body.identity, - video: { - roomName: roomName - } + name: payload.identity, + roomName: payload.roomName }; - try { - const task = await taskrouterHelper.createTask(attributes); - - const response = { - identity: req.body.identity, - token: videoHelper.createAccessToken(req.body.identity, roomName).toJwt(), - video: { - roomName: roomName - }, - taskSid: task.sid - }; - - res.status(200).json(response); - } catch (error) { - res.status(500).json(res.convertErrorToJSON(error)); - } -}; \ No newline at end of file + taskrouterHelper + .createTask(req.configuration.twilio.workflowSid, attributes) + .then((task) => { + payload.task = task.sid; + res.status(200).json(payload); + }) + .catch((error) => { + res.status(500).send(res.convertErrorToJSON(error)); + }); +};