UNPKG

cctsa-wxccdemobot-commons

Version:

Common routines for building demo/reference bots with Webex Contact Center and Google Dialogflow ES using the codingforconvos NPM library.

597 lines (528 loc) 25.4 kB
/** * Copyright 2022 Justin Randall, Cisco Systems Inc. All Rights Reserved. * * This program is free software: you can redistribute it and/or modify it under the terms of the GNU * General Public License as published by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with this program. If not, * see <https://www.gnu.org/licenses/>. */ const { Connector,fmtLog } = require("codingforconvos"); const Redmine = require('axios-redmine'); const { DialogContext } = require("codingforconvos/src/contexts"); const CXTR_REDMINE_NAME = 'redmine'; const CTX_AUTH = 'authentication'; const REDMINE_SOURCES = { 'phone': 'Phone', 'chat': 'Chat', 'sms': 'SMS', 'facebookMessenger': 'Facebook Messenger', 'whatsapp': 'WhatsApp', 'web': 'Web' }; const CASE_TEMPLATES = new Map(); function DEFAULT_CREATE_CASE () { let newCase = { subject: 'A new case has been created.', description: 'I need to create a case, but I don\'t know what type of case to create.', note: 'Initial case creation.' }; return newCase; } /** * A Connector for the Redmine REST API. */ class RedmineConnector extends Connector { /** * Construct a new RedmineConnector instance. * * @example * const redmineConnector = New RedmineConnector({ * hostname: 'http://cctsa-redmine.outofservice.org', * apiKey: 'abc123def456abc123def456abc123def456' * }); * * @param {Object} params The constructor parameters. */ constructor(params) { if (params.hostname == undefined) { throw new Error('hostname is a required parameter for RedmineConnector objects.'); } if (params.apiKey == undefined) { throw new Error('apiKey is a required parameter for RedmineConnector objects.'); } // Create Redmine API client. const hostname = params.hostname; const config = { apiKey: params.apiKey, rejectUnauthorized: params.rejectUnauthorized }; params.endpoint = new Redmine(hostname, config); params.name = CXTR_REDMINE_NAME; params.params = params; params.sessionParams = { redmineUserId: '', customerFirstName: '', customerLastName: '', accountNumber: '', accountTier: '', accountStatus: '', preferredLanguage: '', redmineOpenCaseId: '', redmineOpenAppointmentId: '', advisoryNotice: '', advisoryEvent: '' }; params.populateFromPayload = (context, dialogContext) => { let payload = dialogContext.payload; context.parameters.redmineUserId = (payload.redmineUserId) ? payload.redmineUserId : '-1'; context.parameters.customerFirstName = (payload.customerFirstName) ? payload.customerFirstName : 'Justin'; context.parameters.customerLastName = (payload.customerLastName) ? payload.customerLastName : 'Randall'; context.parameters.accountNumber = (payload.accountNumber) ? payload.accountNumber : ''; context.parameters.accountTier = (payload.accountTier) ? payload.accountTier : ''; context.parameters.accountStatus = (payload.accountStatus) ? payload.accountStatus : ''; context.parameters.preferredLanguage = (payload.preferredLanguage) ? payload.preferredLanguage : ''; context.parameters.redmineOpenCaseId = (payload.redmineOpenCaseId) ? payload.redmineOpenCaseId : '-1'; context.parameters.redmineOpenAppointmentId = (payload.redmineOpenAppointmentId) ? payload.redmineOpenAppointmentId : '-1'; context.parameters.advisoryNotice = (payload.advisoryNotice) ? payload.advisoryNotice : ''; context.parameters.advisoryEvent = (payload.advisoryEvent) ? payload.advisoryEvent : ''; return context; } super(params); this._redmineUsersByEmail = []; this._redmineUsersByMobilePhoneFound = []; this._redmineNewIssue = {}; this.findRedmineUsersByAccountId = this.findRedmineUsersByAccountId.bind(this); this.findRedmineUserByMobilePhone = this.findRedmineUserByMobilePhone.bind(this); } /** * Get the Redmine channel source. * * @returns the Redmine channel source. */ static redmineSource(wxccChannel) { return REDMINE_SOURCES[wxccChannel]; } /** * Get the Redmine case creation factory template method. * * @param {string} templateName The case creation template name. * @returns the Redmine case creation factory template method. */ static caseTemplate(templateName) { if (!CASE_TEMPLATES.has(templateName)) { return DEFAULT_CREATE_CASE; } return CASE_TEMPLATES.get(templateName); } /** * Register a Redmine case creation factory template method. * * @param {string} templateName The template name. * @param {Function} templateFunc The case creation factory template method. * @returns */ static registerCaseTemplate(templateName, templateFunc) { return CASE_TEMPLATES.set(templateName,templateFunc); } /** * Get the static name of the connector. * * @returns the static name of the connector. */ static name() { return CXTR_REDMINE_NAME; } /** * Parse the Redmine REST API user response into a more accessible format. * * @param {Object} redmineUser The Redmine REST API user response. * @returns a parsed Redmine User with accessible custom fields. */ static createResponseUser(redmineUser) { let responseUser = {}; responseUser.id = redmineUser.id; responseUser.login = redmineUser.login; responseUser.admin = redmineUser.admin; responseUser.firstName = redmineUser.firstname; responseUser.lastName = redmineUser.lastname; responseUser.mail = redmineUser.mail; responseUser.lastLoginOn = redmineUser.last_login_on; responseUser.passwdChangedOn = redmineUser.passwd_changed_on; redmineUser.custom_fields.forEach(cf => { switch (cf.name) { case 'Mobile Phone': responseUser.mobileNumber = cf.value; break; case 'Customer Account ID': responseUser.accountNumber = cf.value; break; case 'Account Tier': responseUser.accountTier = cf.value; break; case 'Account Status': responseUser.accountStatus = cf.value; break; case 'Preferred Language': responseUser.preferredLanguage = cf.value; break; case 'Open Case ID': responseUser.openCaseId = cf.value; break; case 'Open Appointment ID': responseUser.openAppointmentId = cf.value; break; case 'Advisory': responseUser.advisory = cf.value; break; case 'Advisory Event': responseUser.advisoryEvent = cf.value; break; default: // do nothing for unknown customer fields. break; } }); return responseUser; } /** * Finds a list of Redmine Users by Email. * * @param {string} email The email. * @returns a parsed Redmine User with accessible custom fields. */ async findRedmineUsersByEmail(email) { this._redmineUsersByEmail = []; await super.endpoint .users({ include: 'memberships,groups' }) .then(response => { response.data.users.forEach(user => { user.customFieldsByName = {}; user.custom_fields.forEach(customField => { user.customFieldsByName[customField.name] = customField; }); if (user.mail === email) { this._redmineUsersByEmail.push(RedmineConnector.createResponseUser(user)); } }); }) .catch(err => { console.error(err); }); return this._redmineUsersByEmail; } /** * Finds a list of Redmine Users by Account ID. * * @param {string} accountId The account ID. * @returns a parsed Redmine User with accessible custom fields. */ async findRedmineUsersByAccountId(accountId) { this._redmineUsersByEmail = []; await super.endpoint .users({ include: 'memberships,groups' }) .then(response => { response.data.users.forEach(user => { user.customFieldsByName = {}; user.custom_fields.forEach(customField => { user.customFieldsByName[customField.name] = customField; }); let cfAccountId = user.customFieldsByName['Customer Account ID'].value; if (cfAccountId === accountId) { this._redmineUsersByEmail.push(RedmineConnector.createResponseUser(user)); } }); }) .catch(err => { console.error(err); }); return this._redmineUsersByEmail; } /** * Finds a list of Redmine Users by Phone Number. * * @param {string} phoneNumber The Phone Number. * @returns a parsed Redmine User with accessible custom fields. */ async findRedmineUserByMobilePhone(phoneNumber) { this._redmineUsersByMobilePhoneFound = []; await super.endpoint .users({ include: 'memberships,groups' }) .then(response => { response.data.users.forEach(user => { user.customFieldsByName = {}; user.custom_fields.forEach(customField => { user.customFieldsByName[customField.name] = customField; }); let cfPhoneNumber = user.customFieldsByName['Mobile Phone'].value; if (cfPhoneNumber === phoneNumber) { this._redmineUsersByMobilePhoneFound.push(RedmineConnector.createResponseUser(user)); } }); }) .catch(err => { console.error(err); }); return this._redmineUsersByMobilePhoneFound; } /** * Resets a password for a Redmine user. */ async resetRedmineUserPassword(redmineUserId,tempPw) { super.endpoint .get_user_by_id(redmineUserId, { include: 'memberships,groups' }) .then(response => { response.data.user.password = tempPw; response.data.user.must_change_passwd = 'true'; let updatedUser = { user: response.data.user }; super.endpoint.update_user(redmineUserId, updatedUser, function(err, data) { if (err) console.log(err); console.log(data); }); }) .catch(err => { console.log(err); }); } /** * Creates a new Redmine Issue. * * @param {Object} newCase The Redmine Issue data. * @param {DialogContext} dialogContext The dialog context. * @returns the newly created Redmine Issue. */ async createRedmineTriage(newCase,dialogContext,triage) { let accountNumber = dialogContext.params.accountNumber; // accountId let source = RedmineConnector.redmineSource(dialogContext.params.interactionSource); let initiatorId = dialogContext.params.redmineUserId; // Initiator let subject = newCase.subject; let description = newCase.description; let trackerId = newCase.trackerId != undefined ? newCase.trackerId : 8; // Default tracker = 'Triage' let statusId = newCase.statusId != undefined ? newCase.statusId : 1; // Default status = 'New' let priorityId = newCase.priorityId != undefined ? newCase.priorityId : 4; // Default priority = 'Normal' this._redmineNewIssue = {}; const issue = { issue: { project_id: 11, subject: subject, description: description, //assigned_to_id: 5, // jusranda tracker_id: trackerId, status_id: statusId, priority_id: priorityId, custom_fields: [ { id: 5, value: accountNumber }, // Customer ID { id: 12, value: source }, // Source { id: 13, value: initiatorId }, // Initiator. { id: 15, value: triage.symptoms }, // Symptoms { id: 16, value: triage.countryName }, // Countries Visited (< 14 Days) { id: 17, value: triage.diagnosedWithCovid }, { id: 18, value: triage.diagnosedWithCovidDate }, { id: 19, value: triage.livesWithCovid } ], } }; await super.endpoint .create_issue(issue) .then(response => { this._redmineNewIssue = response.data.issue; }) .catch(err => { console.log(err); }); return this._redmineNewIssue; } /** * Creates a new Redmine Issue. * * @param {Object} newCase The Redmine Issue data. * @param {DialogContext} dialogContext The dialog context. * @returns the newly created Redmine Issue. */ async createRedmineIssue(newCase,dialogContext) { let accountNumber = dialogContext.params.accountNumber; // accountId let source = RedmineConnector.redmineSource(dialogContext.params.interactionSource); let initiatorId = dialogContext.params.redmineUserId; // Initiator let subject = newCase.subject; let description = newCase.description; let trackerId = newCase.trackerId != undefined ? newCase.trackerId : 7; // Default tracker = 'Case' let statusId = newCase.statusId != undefined ? newCase.statusId : 1; // Default status = 'New' let priorityId = newCase.priorityId != undefined ? newCase.priorityId : 4; // Default priority = 'Normal' this._redmineNewIssue = {}; const issue = { issue: { project_id: 11, subject: subject, description: description, //assigned_to_id: 5, // jusranda tracker_id: trackerId, status_id: statusId, priority_id: priorityId, custom_fields: [ { id: 5, value: accountNumber }, { id: 12, value: source }, { id: 13, value: initiatorId } // Request initiator. FIXME: Pass this in as a variable. ], } }; await super.endpoint .create_issue(issue) .then(response => { this._redmineNewIssue = response.data.issue; }) .catch(err => { console.log(err); }); return this._redmineNewIssue; } /** * Updates nodes for a Redmine Issue. * * @param {number} issueId The Redmine Issue ID. * @param {string} notes The notes. * @param {Boolean} privateFlag The note privacy flag. */ async updateRedmineIssueNotes(issueId,notes,privateFlag='false') { const issue = { issue: { id: issueId, notes: notes, private_notes: privateFlag } }; super.endpoint .update_issue(issueId, issue) .then(response => { console.log('HTTP PUT '+response.status); }) .catch(err => { console.log(err); }); } /** * Populate the session props with Redmine user lookup details. * * @param {Object} ctxSessionProps The session props. * @param {Object} redmineUser The Redmine API result user. * @returns the session props. */ static populateFromRedmineUser(ctxSessionProps, redmineUser) { ctxSessionProps.parameters.smsNumber = redmineUser.mobileNumber; ctxSessionProps.parameters.redmineUserId = Math.floor(redmineUser.id).toString(); // convert from float->int->string ctxSessionProps.parameters.mail = redmineUser.mail; ctxSessionProps.parameters.customerFirstName = redmineUser.firstName; ctxSessionProps.parameters.customerLastName = redmineUser.lastName; ctxSessionProps.parameters.redmineOpenCaseId = redmineUser.openCaseId; ctxSessionProps.parameters.redmineOpenAppointmentId = redmineUser.openAppointmentId; ctxSessionProps.parameters.accountNumber = redmineUser.accountNumber; ctxSessionProps.parameters.accountTier = redmineUser.accountTier; ctxSessionProps.parameters.accountStatus = redmineUser.accountStatus; ctxSessionProps.parameters.preferredLanguage = redmineUser.preferredLanguage; ctxSessionProps.parameters.advisory = redmineUser.advisory; ctxSessionProps.parameters.advisoryEvent = redmineUser.advisoryEvent; return ctxSessionProps; } /** * Populate the session props with Redmine user lookup details. * * @param {Object} ctxSessionProps The session props. * @param {DialogContext} dialogContext The dialog context. * @returns the session props. */ static async populateFromRedmineLookup(ctxSessionProps, dialogContext) { if (ctxSessionProps.parameters.callingNumber !== '') { console.log(fmtLog('populateFromRedmineLookup', 'Lookup Redmine User by Calling Number '+ctxSessionProps.parameters.callingNumber, dialogContext)); return await RedmineConnector.populateFromRedmineMobileNumber(ctxSessionProps, ctxSessionProps.parameters.callingNumber, dialogContext); } else if (ctxSessionProps.parameters.smsNumber !== '') { console.log(fmtLog('populateFromRedmineLookup', 'Lookup Redmine User by SMS Number '+ctxSessionProps.parameters.smsNumber, dialogContext)); return await RedmineConnector.populateFromRedmineMobileNumber(ctxSessionProps, ctxSessionProps.parameters.smsNumber, dialogContext); } else if (ctxSessionProps.parameters.whatsAppNumber !== '') { console.log(fmtLog('populateFromRedmineLookup', 'Lookup Redmine User by WhatsApp Number '+ctxSessionProps.parameters.whatsAppNumber, dialogContext)); return await RedmineConnector.populateFromRedmineMobileNumber(ctxSessionProps, ctxSessionProps.parameters.whatsAppNumber, dialogContext); } else if (ctxSessionProps.parameters.mail !== '') { console.log(fmtLog('populateFromRedmineLookup', 'Lookup Redmine User by Email '+ctxSessionProps.parameters.mail, dialogContext)); return await RedmineConnector.populateFromRedmineEmail(ctxSessionProps, ctxSessionProps.parameters.mail, dialogContext); } return ctxSessionProps; } /** * Populate the session props with Redmine user lookup by phone number. * * @param {Object} ctxSessionProps The session props. * @param {string} phoneNumber The phone number. * @param {DialogContext} dialogContext The dialog context. * @returns the session props. */ static async populateFromRedmineMobileNumber(ctxSessionProps, phoneNumber, dialogContext) { console.log('Creating the CTX_AUTH sequence.'); let context = dialogContext.getOrCreateCtx(CTX_AUTH); console.log('findRedmineUserByMobilePhone: '+phoneNumber); const redmineUsersByMobilePhoneFound = await dialogContext.connectorManager.get(RedmineConnector.name()).findRedmineUserByMobilePhone(phoneNumber); if (redmineUsersByMobilePhoneFound.length === 0) { context.parameters.phoneNumberNotFound = '1'; dialogContext.dialogflowAgent.context.set(context); return ctxSessionProps; } let resultUser = redmineUsersByMobilePhoneFound[0]; // TODO: Handle multiple user disambiguation in future. if (resultUser.mobileNumber == null) { console.log('Mobile Phone field not found for user: '+JSON.stringify(resultUser)); context.parameters.phoneNumberNotFound = '1'; dialogContext.dialogflowAgent.context.set(context); return ctxSessionProps; } context.parameters.phoneNumberFound = '1'; context.parameters.validatedBy = 'Calling Number'; dialogContext.dialogflowAgent.context.set(context); ctxSessionProps.parameters.identityPhoneNumber = resultUser.mobileNumber; ctxSessionProps.parameters.identityEmail = resultUser.mail; ctxSessionProps.parameters.customerIdentified = '1'; ctxSessionProps.parameters.customerIdentifiedBy = 'Calling Number'; dialogContext.dialogflowAgent.context.set(ctxSessionProps); ctxSessionProps = RedmineConnector.populateFromRedmineUser(ctxSessionProps, resultUser); dialogContext.dialogflowAgent.context.set(ctxSessionProps); console.log('findRedmineUserByMobilePhone: ' + JSON.stringify(resultUser)); return ctxSessionProps; } /** * Populate the session props with Redmine user lookup by email. * * @param {Object} ctxSessionProps The session props. * @param {string} email The email. * @param {DialogContext} dialogContext The dialog context. * @returns the session props. */ static async populateFromRedmineEmail(ctxSessionProps, email, dialogContext) { console.log('Creating the CTX_AUTH sequence.'); let context = dialogContext.getOrCreateCtx(CTX_AUTH); console.log('populateFromRedmineEmail: '+email); const redmineUsersByEmailFound = await dialogContext.connectorManager.get(RedmineConnector.name()).findRedmineUsersByEmail(email); if (redmineUsersByEmailFound.length === 0) { context.parameters.emailNotFound = '1'; dialogContext.dialogflowAgent.context.set(context); return ctxSessionProps; } let resultUser = redmineUsersByEmailFound[0]; // TODO: Handle multiple user disambiguation in future. if (resultUser.mail == null) { console.log('email field not found for user: '+JSON.stringify(resultUser)); context.parameters.emailNotFound = '1'; dialogContext.dialogflowAgent.context.set(context); return ctxSessionProps; } context.parameters.emailFound = '1'; context.parameters.validatedBy = 'Email'; dialogContext.dialogflowAgent.context.set(context); ctxSessionProps.parameters.identityPhoneNumber = resultUser.mobileNumber; ctxSessionProps.parameters.identityEmail = resultUser.mail; ctxSessionProps.parameters.customerIdentified = '1'; ctxSessionProps.parameters.customerIdentifiedBy = 'Email'; dialogContext.dialogflowAgent.context.set(ctxSessionProps); ctxSessionProps = RedmineConnector.populateFromRedmineUser(ctxSessionProps, resultUser); dialogContext.dialogflowAgent.context.set(ctxSessionProps); console.log('populateFromRedmineEmail: ' + JSON.stringify(resultUser)); return ctxSessionProps; } /** * Get a Redmine issue by ID. * * @param {number} issueId The Redmine issue ID. * @returns the Redmine issue. */ async getRedmineIssue(issueId) { let redmineFoundIssue = undefined; await super.endpoint .get_issue_by_id(issueId) .then(response => { redmineFoundIssue = response.data.issue; }) .catch(err => { console.log(err); }); return redmineFoundIssue; } } module.exports = {RedmineConnector};