diff --git a/.gitignore b/.gitignore index 1313fed..a46695e 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ local.settings.json node_modules dist + __blobstorage__ __queuestorage__ __azurite_db_blob__.json diff --git a/InputData/function.json b/InputData/function.json new file mode 100644 index 0000000..5241644 --- /dev/null +++ b/InputData/function.json @@ -0,0 +1,20 @@ +{ + "bindings": [ + { + "authLevel": "function", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "res" + } + ], + "scriptFile": "../dist/InputData/index.js" +} diff --git a/InputData/index.ts b/InputData/index.ts new file mode 100644 index 0000000..5f518b4 --- /dev/null +++ b/InputData/index.ts @@ -0,0 +1,200 @@ +import { AzureFunction, Context, HttpRequest } from '@azure/functions' +import Mongo from '../ReceiveMessage/Scripts/db' +import xlsxFile from 'read-excel-file/node' +import ChatbotMessage, { IMessage } from '../ReceiveMessage/models/ChatbotMessage' + +type MessageToBeCreated = { + record: IMessage + keywords: string[] + nextMessages: number[] + previousMessage: number +} + +//https://medium.com/javascript-in-plain-english/how-to-read-an-excel-file-in-node-js-6e669e9a3ce1 +const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise { + context.log('HTTP trigger function processed a request.') + + const success = true + + const path = req.query.path || (req.body && req.body.path) + + //from the root dir, for whatever reason + let defaultPath = './InputData/messages.xlsx' + if (path) { + defaultPath = path + } + //CHANGE THIS TO MATCH THE CHATBOTMESSAGE SCHEMA + //TODO: FIgure out keywords + const schema = { + ID: { + prop: 'messageId', + type: Number, + required: true, + }, + BODY: { + prop: 'body', + type: String, + required: true, + }, + MULTIMEDIA: { + prop: 'multimedia', + type: String, + }, + 'NEXT MESSAGES': { + prop: 'nextMessages', + type: String, + }, + 'PREVIOUS MESSAGE': { + prop: 'prevMessage', + type: Number, + }, + MODULE: { + prop: 'module', + type: Number, + }, + 'LOW DATA': { + prop: 'lowDataBody', + type: String, // it is possible to validate each of these values. + required: true, + }, + KEYWORDS: { + prop: 'keywords', + type: String, + }, + MESSAGETYPE: { + prop: 'messageType', + type: String, + }, + } + await Mongo() + const readXlsx = await xlsxFile(defaultPath, { schema }) + const rows = readXlsx.rows + const errors = readXlsx.errors + if (errors.length != 0) { + context.log(errors) + let errorBody = `There were ${errors.length} number of errors:\n` + for (const error of errors) { + errorBody += `Row ${error.row}; Col ${error.column}; Error: ${error.error}` + } + context.res = { + // status: 200, /* Defaults to 200 */ + body: errorBody, + } + return + } + const messagesToInsert = new Map() + // 1.) Make a map based upon all of the messageIds, with the value being a chatbotMessage + for (const [i, row] of rows.entries()) { + // 0.) Convert nextMessages into an array; same with keywords: + const nextMessagesArray: number[] = row.nextMessages + .toString() + .split(',') + .map((x: string) => parseInt(x.trim())) + const nextKeywordsArray: string[] = row.keywords + ? row.keywords + .toString() + .split(',') + .map((x: string) => x.trim()) + : ['default'] + + if (nextMessagesArray.length != nextKeywordsArray.length) { + context.res = { + // status: 200, /* Defaults to 200 */ + body: `on probably line ${i + 2} for the message with id ${ + row.messageId + } there was a mismatch between the next messages and number of corresponding keywords.`, + } + return + } + + // 1.) query for the messageId + const existingMessage = await ChatbotMessage.findOne({ + messageId: row.messageId, + }).exec() + + // 2.) If that record exists, set the value to be that chatbotMessage + if (existingMessage) { + existingMessage.body = row.body + existingMessage.image = row.multimedia ? row.multimedia.toString() : '' + existingMessage.module = row.module + existingMessage.messageType = row.messageType + existingMessage.lowData = row.lowDataBody + messagesToInsert.set(row.messageId, { + record: existingMessage, + keywords: nextKeywordsArray, + nextMessages: nextMessagesArray, + previousMessage: row.prevMessage, + }) + } + // 3.) else, create a new one + else { + messagesToInsert.set(row.messageId, { + record: new ChatbotMessage({ + //put stuff here + messageId: row.messageId, + body: row.body, + nextMessages: {}, + image: row.multimedia ? row.multimedia.toString() : '', + module: row.module, + messageType: row.messageType, + lowData: row.lowDataBody, + previousMessage: 'not yet', + }), + keywords: nextKeywordsArray, + nextMessages: nextMessagesArray, + previousMessage: row.prevMessage, + }) + } + } + + // 2.) Iterate through every entry of the map, and make connections between all of the messages. + for (const [key, record] of messagesToInsert) { + for (let i = 0; i < record.nextMessages.length; i++) { + const nextMessage = messagesToInsert.get(record.nextMessages[i]) + const prevMessage = messagesToInsert.get(record.previousMessage) + if (!nextMessage) { + context.res = { + // status: 200, /* Defaults to 200 */ + body: + record.nextMessages[i] + + ' is not a valid messageId. Please check to make sure this id was put in properly', + } + return + } + if (!prevMessage) { + context.res = { + // status: 200, /* Defaults to 200 */ + body: + record.previousMessage + + ' is not a valid messageId. Please check to make sure this id was put in properly', + } + return + } + record.record.nextMessages.set(record.keywords[i], nextMessage.record['_id']) + record.record.previousMessage = prevMessage.record['_id'] + } + } + + // 3.) Save every record. This could be done in conjunction with (2), but save() is safer than insertMany(), and this function + // won't be constantly called, so we can take the hit in efficiency. + // The one caveat is that if there is an error somehow with saving, then it will keep saving everything else, which probably + // isn't desirable. If we do a good job error checking above, though, that would be fine. + for (const [key, record] of messagesToInsert) { + //context.log(record.record) + await record.record.save() + } + + let responseMessage = path + ? 'Input, ' + path + '. This HTTP triggered function executed successfully.' + : 'This HTTP triggered function executed successfully. Pass a path for not the default excel sheet.' + if (!success) { + responseMessage = 'Your upload was incorrect. Please verify the xlsx spreadsheet format matches.' + } + + context.res = { + // status: 200, /* Defaults to 200 */ + body: responseMessage, + } +} + +export default httpTrigger diff --git a/InputData/messages.xlsx b/InputData/messages.xlsx new file mode 100644 index 0000000..881045e Binary files /dev/null and b/InputData/messages.xlsx differ diff --git a/InputData/sample.dat b/InputData/sample.dat new file mode 100644 index 0000000..26aac46 --- /dev/null +++ b/InputData/sample.dat @@ -0,0 +1,3 @@ +{ + "name": "Azure" +} \ No newline at end of file diff --git a/ReceiveMessage/Scripts/readRequest.ts b/ReceiveMessage/Scripts/readRequest.ts index 0ba8169..df8409b 100644 --- a/ReceiveMessage/Scripts/readRequest.ts +++ b/ReceiveMessage/Scripts/readRequest.ts @@ -1,31 +1,25 @@ -import { AzureFunction, Context, HttpRequest } from '@azure/functions'; -import * as twilio from 'twilio'; -import qs from 'qs'; -import MongoConnect from './db'; -import UserState from '../models/UserState'; - -const MessagingResponse = twilio.twiml.MessagingResponse; +import { AzureFunction, Context, HttpRequest } from '@azure/functions' +import * as twilio from 'twilio' +import qs from 'qs' +import MongoConnect from './db' +import UserState from '../models/UserState' //change to Promise later const getUserState = async function (req: HttpRequest) { - const body = qs.parse(req.body); + const body = qs.parse(req.body) // do necessary processing on the request (nothing at this point) - await MongoConnect(); - const userStateResult = await UserState.find({ userId: body.From as string }); - if (userStateResult.length === 0) { - const newUser = new UserState({ userId: body.From }); + await MongoConnect() + const userStateResult = await UserState.findOne({ userId: body.From as string }) + if (!userStateResult) { + const newUser = new UserState({ userId: body.From }) newUser.save(function (err) { if (err) { - console.log(err); + console.log(err) } - ``; - }); + }) } // retrieve and return the corresponding state - const id = body.AccountSid; - await MongoConnect(); - const result = await UserState.find({ userId: id as string }); - return result[0]; -}; + return userStateResult +} -export default getUserState; +export default getUserState diff --git a/ReceiveMessage/Scripts/sendMessage.js b/ReceiveMessage/Scripts/sendMessage.js index 0a5d488..7076c2b 100644 --- a/ReceiveMessage/Scripts/sendMessage.js +++ b/ReceiveMessage/Scripts/sendMessage.js @@ -9,7 +9,7 @@ const formResponse = async function (state, message) { await MongoConnect() const prev = await ChatbotMessage.findById(p) const map = prev.nextMessages - let nextChatbotMessageId = map.get(prev.isQuestion ? message : 'default') + let nextChatbotMessageId = map.get(map.size > 1 ? message : 'default') if (nextChatbotMessageId == null) { nextChatbotMessageId = '6022178429efc055c8e74e50' //change this to whatever error message we want to send (for now it is welcome message) } diff --git a/ReceiveMessage/fixedMessages.ts b/ReceiveMessage/fixedMessages.ts new file mode 100644 index 0000000..bb7602e --- /dev/null +++ b/ReceiveMessage/fixedMessages.ts @@ -0,0 +1,33 @@ +import ChatbotMessage, { IMessage } from './models/ChatbotMessage' +import db from './Scripts/db' + +interface templateFixedMessageHandler { + (messageId: number): Promise +} + +const generalHandler: templateFixedMessageHandler = async function (messageId: number): Promise { + db() + return ChatbotMessage.findOne({ messageId: messageId }) +} + +const fixedMessages: Map> = new Map< + string | qs.ParsedQs | string[] | qs.ParsedQs[], + Promise +>() + +fixedMessages.set('MessagePermission', generalHandler(-1)) +fixedMessages.set('DataPermission', generalHandler(0)) +fixedMessages.set('Welcome', generalHandler(1)) +fixedMessages.set('ModuleOne', generalHandler(100)) +fixedMessages.set('ModuleTwo', generalHandler(200)) +fixedMessages.set('ModuleThree', generalHandler(300)) +fixedMessages.set('ModuleFour', generalHandler(400)) +fixedMessages.set('ModuleFive', generalHandler(500)) +fixedMessages.set('commands', generalHandler(911)) +fixedMessages.set('Exercise Diagnostic', generalHandler(1100)) +fixedMessages.set('WASH Diagnostic', generalHandler(1200)) +fixedMessages.set('Nutrition Diagnostic', generalHandler(1300)) +fixedMessages.set('Maternal Infant Care Diagnostic', generalHandler(1400)) +fixedMessages.set('Mental Health Diagnostic', generalHandler(1500)) + +export default fixedMessages diff --git a/ReceiveMessage/index.ts b/ReceiveMessage/index.ts index 9e70007..5a449c6 100644 --- a/ReceiveMessage/index.ts +++ b/ReceiveMessage/index.ts @@ -5,7 +5,11 @@ import getUserState from './Scripts/readRequest' import MessageResponse from './models/MessageResponse' import { Schema } from 'mongoose' import formResponse from './Scripts/sendMessage' +import UserState, { IUserState } from './models/UserState' import specialMessageIds from './specialMessageIds' +import fixedMessages from './fixedMessages' +import ChatbotMessage, { IMessage } from './models/ChatbotMessage' +import Mongoose from 'mongoose' const MessagingResponse = twilio.twiml.MessagingResponse @@ -14,27 +18,24 @@ const httpTrigger: AzureFunction = async function (context: Context, req: HttpRe const sentMessage = qs.parse(req.body) const curUserState = await getUserState(req) - context.log(curUserState) - context.log(sentMessage) - // const response = await formResponse(curUserState, sentMessage.Body); - const response = await manageKeywordSent(sentMessage, curUserState) - - // broke based on manageKeywordSent functionality - // if (response.images != null) { - // response.images.forEach(function (image) { - // const imageResponse = new MessagingResponse() - // const imageMessage = imageResponse.message('') - // imageMessage.media(image) - // }) - // } + const response = await manageKeywordSent(sentMessage, curUserState, req) // returns IMessage const message = new MessagingResponse() - message.message(response) + const messageContent = message.message('') + messageContent.body(response.body) + + context.log(response) + if (response.image && response.image != '') { + messageContent.media(response.image) + } // if there's a conditional (like not recording all messages), put that here - if (sentMessage.isQuestion) { + // make sure user has consented to data storage + // make sure curUserState is not null + if (curUserState && curUserState.dataConsent && sentMessage.messageType == 'question') { storeMessage(sentMessage, curUserState.currMessage) } + context.log(message.toString()) context.res = { // status: 200, /* Defaults to 200 */ /* @@ -59,28 +60,42 @@ const storeMessage = async function (sentMessage: qs.ParsedQs, curMessageID: Sch }; //checks if a special keyword is in the message sent -const manageKeywordSent = async function (sentMessage: qs.ParsedQs, curUserState) { - if (specialMessageIds.has(sentMessage.Body)) { - // special message handling - const responseString = specialMessageIds.get(sentMessage.Body) +const manageKeywordSent = async function ( + sentMessage: qs.ParsedQs, + curUserState: IUserState, + req: HttpRequest, +): Promise { + const msg = 'i consent' + const body = qs.parse(req.body) - // update curUserState depending on the specialMessageId - if (sentMessage.Body == 'restart') { - curUserState.currMessage = '6022178429efc055c8e74e50' - await curUserState.save() - } else if (sentMessage.Body == 'completed') { - // do not update userstate - const responseStringCompleted = - 'You have completed ' + curUserState.moduleCompletionTime.length + ' modules.' - return responseStringCompleted - } + if (!curUserState && msg == sentMessage.Body) { + //new user that consents + const messageId = Mongoose.Types.ObjectId('60563903ea9f2e441461118c') //points to data consent question + const newUser = new UserState({ userId: body.From, dataConsent: false, currMessage: messageId }) + await newUser.save() - // return message text + const returnMessage = new ChatbotMessage() + returnMessage.body = + 'Thank you for consenting! if angry at data collect write "no data pls" otherwise we\'ll sneaky sneaky. respond to continue thanks' + return returnMessage + } else if (!curUserState) { + //new user that hasn't consented + const returnMessage = new ChatbotMessage() + returnMessage.body = + 'Doth thee consent to receiving messages from this numb\'r. Replyeth with "i consent" if \'t be true thee doth' + return returnMessage + } else if (specialMessageIds.has(sentMessage.Body)) { + // special message handling + const responseHandler = specialMessageIds.get(sentMessage.Body) + const responseString = responseHandler(curUserState) return responseString + } else if (fixedMessages.has(sentMessage.Body)) { + // fixed message handling + return fixedMessages.get(sentMessage.Body) } else { // normal handling const responseString = await formResponse(curUserState, sentMessage.Body) - return responseString.body + return responseString } }; diff --git a/ReceiveMessage/models/ChatbotMessage.ts b/ReceiveMessage/models/ChatbotMessage.ts index 69619ad..01ffd25 100644 --- a/ReceiveMessage/models/ChatbotMessage.ts +++ b/ReceiveMessage/models/ChatbotMessage.ts @@ -2,21 +2,22 @@ import mongoose, { Schema, Document } from 'mongoose' export interface IMessage extends Document { body: string - images?: Array - nextMessages: Map + image: string + nextMessages: Map previousMessage: Schema.Types.ObjectId module: string - isQuestion?: boolean + messageType: string //can only be "question", "final-message", "message", or "splitting" lowData: string } const ChatbotMessageSchema = new Schema({ + messageId: { type: Number, required: true }, body: { type: String, required: true }, - images: { type: Array, default: [], required: false }, + image: { type: String, required: false }, nextMessages: { type: Map, of: { type: Schema.Types.ObjectId, ref: 'ChatbotMessage' }, required: true }, previousMessage: { type: Schema.Types.ObjectId, ref: 'ChatbotMessage', required: false }, module: { type: String, required: true }, - isQuestion: { type: Boolean, required: false }, + messageType: { type: String, enum: ['question', 'final-message', 'message', 'splitting'], required: true }, lowData: { type: String, required: true }, }) diff --git a/ReceiveMessage/models/UserState.ts b/ReceiveMessage/models/UserState.ts index cbbb496..7bbfb03 100644 --- a/ReceiveMessage/models/UserState.ts +++ b/ReceiveMessage/models/UserState.ts @@ -6,6 +6,7 @@ export interface IUserState extends Document { currMessage: IMessage['_id'] lowData: boolean moduleCompletionTime: Array + dataConsent: boolean lastActivity: Date } @@ -19,6 +20,7 @@ const UserStateSchema = new Schema({ }, lowData: { type: Boolean }, moduleCompletionTime: { type: [Date], default: [null, null, null, null, null] }, + dataConsent: { type: Boolean, required: true }, lastActivity: { type: Date }, }) diff --git a/ReceiveMessage/specialMessageIds.ts b/ReceiveMessage/specialMessageIds.ts index b49f643..1886398 100644 --- a/ReceiveMessage/specialMessageIds.ts +++ b/ReceiveMessage/specialMessageIds.ts @@ -1,15 +1,52 @@ -const specialMessageIds = new Map() +import qs from 'qs' +import UserState, { IUserState } from './models/UserState' +import ChatbotMessage, { IMessage } from './models/ChatbotMessage' +import Mongoose from 'mongoose' -//restarts progress -specialMessageIds.set( - 'restart', - '(Welcome Message) What would you like to learn about? 1. (emoji) Exercise - brief description2. (emoji) WASH- brief description3. (emoji) Nutrition-brief description4. (emoji) Maternal Infant Care-brief description5. (emoji) Mental Health- brief description', -) +interface templateSpecialMessageHandler { + (curUserState: IUserState): Promise +} -//checking # of completed modules -specialMessageIds.set('completed', '') +const restartHandler: templateSpecialMessageHandler = async function (curUserState: IUserState): Promise { + curUserState.currMessage = '6022178429efc055c8e74e50' + await curUserState.save() + const returnMessage = new ChatbotMessage() + returnMessage.body = + '(Welcome Message) What would you like to learn about? 1. (emoji) Exercise - brief description2. (emoji) WASH- brief description3. (emoji) Nutrition-brief description4. (emoji) Maternal Infant Care-brief description5. (emoji) Mental Health- brief description' + return returnMessage +} -// help message that gives full list of special messages commands -specialMessageIds.set('commands', 'Here is a list of commands: restart, completed, help') +const commandsHandler: templateSpecialMessageHandler = async function (curUserState: IUserState): Promise { + const returnMessage = new ChatbotMessage() + returnMessage.body = 'Here is a list of commands: restart, completed, help' + return returnMessage +} + +const completedHandler: templateSpecialMessageHandler = async function (curUserState: IUserState): Promise { + const returnMessage = new ChatbotMessage() + let numCompleted = 0 + for (let i = 0; i < curUserState.moduleCompletionTime.length; i++) { + if (curUserState.moduleCompletionTime[i] != null) { + numCompleted++ + } + } + returnMessage.body = 'You have completed ' + numCompleted + ' modules.' + return returnMessage +} + +// mostly for testing purposes +const currentHandler: templateSpecialMessageHandler = async function (curUserState: IUserState): Promise { + return ChatbotMessage.findById(curUserState.currMessage) +} + +const specialMessageIds: Map = new Map< + string | qs.ParsedQs | string[] | qs.ParsedQs[], + templateSpecialMessageHandler +>() + +specialMessageIds.set('restart', restartHandler) +specialMessageIds.set('commands', commandsHandler) +specialMessageIds.set('completed', completedHandler) +specialMessageIds.set('current', currentHandler) export default specialMessageIds diff --git a/package-lock.json b/package-lock.json index 2e4452d..d4dffc5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1068,14 +1068,12 @@ "big-integer": { "version": "1.6.36", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.36.tgz", - "integrity": "sha512-t70bfa7HYEA1D9idDbmuv7YbsbVkQ+Hp+8KFSul4aE5e/i1bjCNIRYJZlA8Q8p0r9T8cF/RVvwUgRA//FydEyg==", - "dev": true + "integrity": "sha512-t70bfa7HYEA1D9idDbmuv7YbsbVkQ+Hp+8KFSul4aE5e/i1bjCNIRYJZlA8Q8p0r9T8cF/RVvwUgRA//FydEyg==" }, "binary": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", - "dev": true, "requires": { "buffers": "~0.1.1", "chainsaw": "~0.1.0" @@ -1215,11 +1213,15 @@ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" }, + "buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==" + }, "buffers": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=", - "dev": true + "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=" }, "bytes": { "version": "3.1.0", @@ -1252,7 +1254,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", - "dev": true, "requires": { "traverse": ">=0.3.0 <0.4" } @@ -1730,6 +1731,48 @@ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" }, + "duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "requires": { + "readable-stream": "^2.0.2" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -2511,6 +2554,27 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, + "fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "requires": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "requires": { + "glob": "^7.1.3" + } + } + } + }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -2718,8 +2782,7 @@ "graceful-fs": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", - "dev": true + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" }, "har-schema": { "version": "2.0.0", @@ -2915,6 +2978,11 @@ "minimatch": "^3.0.4" } }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" + }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -3344,6 +3412,51 @@ "verror": "1.10.0" } }, + "jszip": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.6.0.tgz", + "integrity": "sha512-jgnQoG9LKnWO3mnVNBnfhkh0QknICd1FGSrXcgrl67zioyJ4wgx25o9ZqwNtrROSflGBCGYnJfjrIyRIby1OoQ==", + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "set-immediate-shim": "~1.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -3378,6 +3491,14 @@ "type-check": "~0.4.0" } }, + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "requires": { + "immediate": "~3.0.5" + } + }, "lines-and-columns": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", @@ -3458,6 +3579,11 @@ } } }, + "listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=" + }, "listr2": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.3.0.tgz", @@ -4330,6 +4456,11 @@ "aggregate-error": "^3.0.0" } }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, "papaparse": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-4.6.0.tgz", @@ -4577,6 +4708,17 @@ } } }, + "read-excel-file": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/read-excel-file/-/read-excel-file-5.0.0.tgz", + "integrity": "sha512-Lk2rRORUA6ugz1gWemBqyO/9xynxWfmEURnT9oEL7GwyC1I6fsRYWMNaIyu614iZGLZsxYZlFlZMrF/LOe8jdQ==", + "requires": { + "jszip": "^3.5.0", + "unzipper": "^0.9.15", + "xmldom": "^0.1.27", + "xpath": "0.0.27" + } + }, "read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", @@ -4885,6 +5027,16 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" + }, "setprototypeof": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", @@ -5314,8 +5466,7 @@ "traverse": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=", - "dev": true + "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=" }, "tslib": { "version": "1.14.1", @@ -5422,6 +5573,61 @@ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" }, + "unzipper": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.9.15.tgz", + "integrity": "sha512-2aaUvO4RAeHDvOCuEtth7jrHFaCKTSXPqUkXwADaLBzGbgZGzUDccoEdJ5lW+3RmfpOZYNx0Rw6F6PUzM6caIA==", + "requires": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + }, + "dependencies": { + "bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -5671,6 +5877,16 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, + "xmldom": { + "version": "0.1.31", + "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.31.tgz", + "integrity": "sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ==" + }, + "xpath": { + "version": "0.0.27", + "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.27.tgz", + "integrity": "sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ==" + }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/package.json b/package.json index fc293dc..5d97f9c 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "login": "^0.8.0", "mongoose": "^5.11.15", "qs": "^6.9.6", + "read-excel-file": "^5.0.0", "twilio": "^3.55.0" }, "husky": {