1 | 'use strict'
|
2 |
|
3 | const _ = require('lodash')
|
4 | const request = require('request')
|
5 |
|
6 | const attributeSelection = require('./attributeSelection')()
|
7 | const isJSON = require('is-json')
|
8 |
|
9 | const config = require('./rc')().get()
|
10 | const auth = config.medx.auth
|
11 | const c3 = config.medx.c3.replace(/c3$/i, 'ada')
|
12 | const OAUTH_BASICAUTH = 'Basic YWRhOjl4Q0dqZFJVcEJaVlVHdzJjVUROcTd2S3JCckFNQUU0'
|
13 |
|
14 | const no = ['no', 'none', 'nope', 'nein']
|
15 | const abortSearch = (term) => _.includes(no, term.trim().toLowerCase())
|
16 |
|
17 | let currentProfile
|
18 |
|
19 | const convertToDate = (date) => {
|
20 | const from = date.trim().split('.')
|
21 | return new Date(from[2], from[1] - 1, from[0], 12).toISOString()
|
22 | }
|
23 |
|
24 | const invalidResponse = `The server's response is invalid.
|
25 | There is probably a deployment ongoing.
|
26 | Please check back in a few minutes.`
|
27 |
|
28 | module.exports = (username, password) => {
|
29 | let api = null
|
30 | const initialFormData = JSON.stringify({answer: {state: 'GET_HEALTH_ADVICE'}})
|
31 | const getResult = (adaCaseKey, cb) => {
|
32 | api.get({url: `result/${adaCaseKey}`}, (err, res, body) => {
|
33 | if (err) cb(err)
|
34 | cb(null, JSON.parse(body))
|
35 | })
|
36 | }
|
37 |
|
38 | const normalizeResponse = (response) => {
|
39 | const firstType = _.get(response, 'answerOptions[0].type')
|
40 | const secondType = _.get(response, 'answerOptions[1].type')
|
41 |
|
42 | response.answerType = firstType === 'SELECT' ? 'SELECT' : 'TEXT'
|
43 | response.search = firstType === 'SEARCH'
|
44 |
|
45 | if (firstType === 'SEARCH' && secondType) {
|
46 | response.question += ' Type "no" or enter a symptom.'
|
47 | }
|
48 | if (firstType === 'DATE') {
|
49 | response.question += ' e.g.: 19.07.1994'
|
50 | }
|
51 | if (firstType === 'ATTRIBUTE_SELECTION_MAP') {
|
52 | response.answerType = 'SELECT'
|
53 | response.answerOptions = attributeSelection.map((a) => {
|
54 | return _.defaults(a, response.answerOptions[0])
|
55 | })
|
56 | }
|
57 | return response
|
58 | }
|
59 |
|
60 | const symptomSearch = (answer, previousMessage, isCli, cb) => {
|
61 | const keys = previousMessage.answerOptions[0]
|
62 | const search = (sex, birthday) => {
|
63 | api.get({
|
64 | url: `/dialog_search?limit=20&offset=0&query=${encodeURIComponent(answer)}&` +
|
65 | `selfAssessmentKey=${keys.selfAssessmentKey}&sex=${sex}&birthday=${birthday}`
|
66 | },
|
67 | (err, res, body) => {
|
68 | let results = JSON.parse(body).results
|
69 | results.forEach((result) => {
|
70 | if (!result.name) return
|
71 | if (isCli) result.patientName += ` // medical: ${result.name}`
|
72 | if (!result.matchedName) return
|
73 | if (isCli) result.patientName += ` // matched: ${result.matchedName}`
|
74 | })
|
75 | const answerOptions = results.concat({
|
76 | key: 'backToSearch',
|
77 | name: '< back to search',
|
78 | patientName: '< back to search'
|
79 | })
|
80 |
|
81 | let newMessage = {
|
82 | question: 'Select a result:',
|
83 | answerType: 'SELECT',
|
84 | stateStack: previousMessage.stateStack,
|
85 | answerOptions: answerOptions,
|
86 | previousAnswerOptions: previousMessage.answerOptions,
|
87 | previousMessage
|
88 | }
|
89 | if (!results.length) console.log('No results were found.')
|
90 | cb(err, !err && results.length ? newMessage : previousMessage)
|
91 | })
|
92 | }
|
93 | if (!currentProfile || currentProfile.key !== keys.profileKey) {
|
94 | api.get({url: '/profiles'}, (err, res, rawProfiles) => {
|
95 | if (err) cb(err)
|
96 | const profiles = JSON.parse(rawProfiles)
|
97 | currentProfile = _.find(profiles, {key: keys.profileKey})
|
98 | search(currentProfile.sex, currentProfile.birthday)
|
99 | })
|
100 | } else {
|
101 | search(currentProfile.sex, currentProfile.birthday)
|
102 | }
|
103 | }
|
104 |
|
105 | const initiateChat = (cb) => {
|
106 | api.post({url: '/dialog', body: initialFormData}, (err, res, body) => {
|
107 | if (!isJSON(body)) return cb(invalidResponse)
|
108 | cb(err, !err && normalizeResponse(JSON.parse(body)))
|
109 | })
|
110 | }
|
111 |
|
112 | const deleteProfiles = (cb) => {
|
113 | api.del({url: '/profiles'}, (err, res, body) => {
|
114 | if (err) return cb(new Error(err))
|
115 | if (res.statusCode !== 204) return cb(new Error('Some error occured.'))
|
116 | cb(null, 'Successfully deleted all Profiles. Please add a new main profile via Start Self Assessment > Someone else')
|
117 | })
|
118 | }
|
119 |
|
120 | return {
|
121 | login: (deleteAllProfiles, cb) => {
|
122 | const url = `${auth.replace(/api\/login$/i, '')}oauth/token?username=${encodeURIComponent(
|
123 | username)}&password=${encodeURIComponent(password)}&grant_type=password`
|
124 | request.post({url, headers: {
|
125 | Authorization: OAUTH_BASICAUTH
|
126 | }}, (err, res) => {
|
127 | if (err) return cb(new Error(err))
|
128 | const token = JSON.parse(res.body).access_token
|
129 | if (!token) return cb(new Error('login failed'))
|
130 |
|
131 | api = request.defaults({
|
132 | baseUrl: c3,
|
133 | headers: {
|
134 | Authorization: `Bearer ${token}`,
|
135 | 'Content-Type': 'application/json;charset=UTF-8'
|
136 | }
|
137 | })
|
138 | if (deleteAllProfiles) return deleteProfiles(cb)
|
139 | initiateChat(cb)
|
140 | })
|
141 | },
|
142 |
|
143 | isLoggedIn: () => !!api,
|
144 |
|
145 | ask: (answer, previousMessage, isCli, cb) => {
|
146 | if (!api) cb(new Error('wrong function'))
|
147 | if (!previousMessage) return initiateChat(cb)
|
148 |
|
149 | if (answer.key === 'backToSearch') {
|
150 | return cb(null, previousMessage.previousMessage)
|
151 | }
|
152 |
|
153 | if (previousMessage.search && !abortSearch(answer)) {
|
154 | return symptomSearch(answer, previousMessage, isCli, cb)
|
155 | }
|
156 |
|
157 | let dialogFormData = {
|
158 | answer,
|
159 | stateStack: previousMessage.stateStack
|
160 | }
|
161 |
|
162 | if (previousMessage.answerType === 'TEXT') {
|
163 | const type = previousMessage.answerOptions[0].type
|
164 | const abortSearchAnswer = previousMessage.answerOptions[1] && previousMessage.answerOptions[1]
|
165 |
|
166 | answer = answer.trim()
|
167 | dialogFormData.answer = abortSearch(answer) ? abortSearchAnswer
|
168 | : {key: answer, input: type === 'DATE' ? convertToDate(answer) : answer}
|
169 |
|
170 | if (!abortSearchAnswer) {
|
171 | if (!dialogFormData.answer) return cb(null, previousMessage)
|
172 | dialogFormData.answer.state = previousMessage.answerOptions[0].state
|
173 | }
|
174 |
|
175 | dialogFormData.answer = _.defaults(dialogFormData.answer, previousMessage.answerOptions[0])
|
176 | }
|
177 |
|
178 | if (answer.score) {
|
179 | dialogFormData.answer.state = previousMessage.previousAnswerOptions[0].state
|
180 | dialogFormData.answer.input = answer.key
|
181 | Object.assign(dialogFormData.answer, previousMessage.previousAnswerOptions[0])
|
182 | }
|
183 |
|
184 | dialogFormData = JSON.stringify(dialogFormData)
|
185 |
|
186 | api.post({url: '/dialog', body: dialogFormData}, (err, res, body) => {
|
187 | if (!isJSON(body)) return cb(invalidResponse)
|
188 | body = JSON.parse(body)
|
189 | if (res.statusCode >= '300') return cb(new Error(res))
|
190 | if (!body.answerOptions) cb(new Error('No answer options were defined ', body))
|
191 | if (_.includes(['NAVIGATE', 'NAVIGATE_INSTANT'], body.answerOptions[0].type)) {
|
192 | if (body.answerOptions[0].route === 'assessmentReport') {
|
193 | return getResult(body.answerOptions[0].adaCaseKey, cb)
|
194 | }
|
195 | body.isOver = true
|
196 | return cb(null, body)
|
197 | }
|
198 | cb(err, !err && normalizeResponse(body))
|
199 | })
|
200 | }
|
201 | }
|
202 | }
|