-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathResponseForEach.ts
255 lines (234 loc) · 8.53 KB
/
ResponseForEach.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
import * as noflo from 'noflo'
import * as moment from 'moment'
import {
ContactableConfig,
Contactable,
Statement,
Option,
Reaction,
ContactableInitConfig
} from 'rsf-types'
import { init as contactableInit, makeContactable, shutdown as contactableShutdown } from 'rsf-contactable'
import {
DEFAULT_ALL_COMPLETED_TEXT,
DEFAULT_INVALID_RESPONSE_TEXT,
DEFAULT_MAX_RESPONSES_TEXT,
DEFAULT_TIMEOUT_TEXT,
whichToInit,
collectFromContactables,
timer,
CollectResults
} from '../libs/shared'
import { ProcessHandler, NofloComponent } from '../libs/noflo-types'
// define other constants or creator functions
// of the strings for user interaction here
const giveOptionsText = (options: Option[]) => {
return `The options for each item are: ${options.map(o => `${o.text} (${o.triggers.join(', ')})`).join(', ')}.
To respond, type and send a message with one of the values within the round brackets () that corresponds with your choice.`
}
const rulesText = (maxTime: number) => `Welcome.
A process has begun in which you are invited to respond to each item in a list, one at a time, by responding with messages.
The process is timed and will stop automatically after ${moment.duration(maxTime, 'seconds').humanize()}.`
// use of this trigger will allow any response to match
const WILDCARD_TRIGGER = '*'
const defaultReactionCb = (reaction: Reaction): void => { }
const formatStatementText = (numPerPerson: number, numSoFar: number, statement: Statement): string => {
return `(${numPerPerson - 1 - numSoFar} more remaining) ${statement.text}`
}
const coreLogic = async (
contactables: Contactable[],
statements: Statement[],
options: Option[],
maxTime: number,
reactionCb: (reaction: Reaction) => void = defaultReactionCb,
maxResponsesText: string = DEFAULT_MAX_RESPONSES_TEXT,
allCompletedText: string = DEFAULT_ALL_COMPLETED_TEXT,
timeoutText: string = DEFAULT_TIMEOUT_TEXT,
invalidResponseText: string = DEFAULT_INVALID_RESPONSE_TEXT,
speechDelay: number = 500
): Promise<Reaction[]> => {
// initiate contact with each person
// and set context, and "rules"
contactables.forEach(async (contactable: Contactable): Promise<void> => {
await contactable.speak(rulesText(maxTime))
await timer(speechDelay)
await contactable.speak(giveOptionsText(options))
// send the first one
if (statements.length) {
await timer(speechDelay)
await contactable.speak(formatStatementText(statements.length, 0, statements[0]))
}
})
const matchOption = (text: string): Option => {
return options.find(option => {
return option.triggers.find(trigger => trigger === text || trigger === WILDCARD_TRIGGER)
})
}
// for collectFromContactables
const validate = (msg: string): boolean => {
return !!matchOption(msg)
}
const onInvalid = (msg: string, contactable: Contactable): void => {
contactable.speak(invalidResponseText)
}
const isPersonalComplete = (personalResultsSoFar: Reaction[]): boolean => {
return personalResultsSoFar.length === statements.length
}
const onPersonalComplete = (personalResultsSoFar: Reaction[], contactable: Contactable): void => {
contactable.speak(maxResponsesText)
}
const convertToResult = (msg: string, personalResultsSoFar: Reaction[], contactable: Contactable): Reaction => {
const matchedOption = matchOption(msg)
const responsesSoFar = personalResultsSoFar.length
return {
statement: { ...statements[responsesSoFar] }, // clone
response: matchedOption.text,
responseTrigger: msg,
contact: contactable.config(),
timestamp: Date.now()
}
}
const onResult = (reaction: Reaction, personalResultsSoFar: Reaction[], contactable: Contactable): void => {
// each time it gets a valid result, send the next one
// until they're all responded to!
const responsesSoFar = personalResultsSoFar.length
if (statements[responsesSoFar]) {
const nextStatement = statements[responsesSoFar]
const nextStatementString = formatStatementText(statements.length, responsesSoFar, nextStatement)
contactable.speak(nextStatementString)
}
reactionCb(reaction)
}
const isTotalComplete = (allResultsSoFar: Reaction[]): boolean => {
return allResultsSoFar.length === contactables.length * statements.length
}
const collectResults: CollectResults<Reaction> = await collectFromContactables<Reaction>(
contactables,
maxTime,
validate,
onInvalid,
isPersonalComplete,
onPersonalComplete,
convertToResult,
onResult,
isTotalComplete
)
const { timeoutComplete, results } = collectResults
await Promise.all(contactables.map((contactable: Contactable): Promise<void> => contactable.speak(timeoutComplete ? timeoutText : allCompletedText)))
return results
}
const process: ProcessHandler = async (input, output) => {
if (!input.hasData('options', 'statements', 'max_time', 'contactable_configs', 'bot_configs')) {
return
}
const maxTime: number = input.getData('max_time')
const options: Option[] = input.getData('options')
const statements: Statement[] = input.getData('statements').slice(0) // make sure that this array is its own
const botConfigs: ContactableInitConfig = input.getData('bot_configs')
const contactableConfigs: ContactableConfig[] = input.getData('contactable_configs')
const invalidResponseText: string | undefined = input.getData('invalid_response_text')
const maxResponsesText: string | undefined = input.getData('max_responses_text')
const allCompletedText: string | undefined = input.getData('all_completed_text')
const timeoutText: string | undefined = input.getData('timeout_text')
let contactables: Contactable[]
try {
await contactableInit(whichToInit(contactableConfigs), botConfigs)
contactables = contactableConfigs.map(makeContactable)
} catch (e) {
output.send({
error: e
})
output.done()
return
}
try {
const results: Reaction[] = await coreLogic(
contactables,
statements,
options,
maxTime,
(reaction: Reaction): void => {
output.send({ reaction })
},
maxResponsesText,
allCompletedText,
timeoutText,
invalidResponseText
)
await contactableShutdown()
output.send({
results
})
} catch (e) {
await contactableShutdown()
output.send({
error: e
})
}
output.done()
}
const getComponent = (): NofloComponent => {
const c: NofloComponent = new noflo.Component()
/* META */
c.description = 'For a list/array of statements, collect a response or vote for each from a list of participants'
c.icon = 'compress'
/* IN PORTS */
c.inPorts.add('options', {
datatype: 'array', // rsf-types/Option[]
description: 'a list containing the options (as objects with properties "triggers": "array" and "text": "string") people have to respond with',
required: true
})
c.inPorts.add('statements', {
datatype: 'array', // rsf-types/Statement[]
description: 'the list of statements (as objects with property "text") to gather responses to',
required: true
})
c.inPorts.add('max_time', {
datatype: 'int',
description: 'the number of seconds to wait until stopping this process automatically',
required: true
})
c.inPorts.add('contactable_configs', {
datatype: 'array', // rsf-types/ContactableConfig[]
description: 'an array of rsf-contactable compatible config objects',
required: true
})
c.inPorts.add('bot_configs', {
datatype: 'object',
description: 'an object of rsf-contactable compatible bot config objects',
required: true
})
c.inPorts.add('max_responses_text', {
datatype: 'string',
description: 'msg override: the message sent when participant hits response limit'
})
c.inPorts.add('invalid_response_text', {
datatype: 'string',
description: 'msg override: the message sent when participant use an invalid response'
})
c.inPorts.add('all_completed_text', {
datatype: 'string',
description: 'msg override: the message sent to all participants when the process completes, by completion by all participants'
})
c.inPorts.add('timeout_text', {
datatype: 'string',
description: 'msg override: the message sent to all participants when the process completes because the timeout is reached'
})
/* OUT PORTS */
c.outPorts.add('reaction', {
datatype: 'object' // rsf-types/Reaction
})
c.outPorts.add('results', {
datatype: 'array' // rsf-types/Reaction[]
})
c.outPorts.add('error', {
datatype: 'all'
})
/* DEFINE PROCESS */
c.process(process)
return c
}
export {
coreLogic,
getComponent
}