stealth 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.circleci/config.yml +58 -0
- data/.gitignore +12 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +81 -0
- data/LICENSE +20 -0
- data/README.md +1 -0
- data/VERSION +1 -0
- data/bin/stealth +5 -0
- data/lib/stealth/base.rb +87 -0
- data/lib/stealth/cli.rb +82 -0
- data/lib/stealth/cli_base.rb +25 -0
- data/lib/stealth/commands/command.rb +14 -0
- data/lib/stealth/commands/console.rb +75 -0
- data/lib/stealth/commands/server.rb +20 -0
- data/lib/stealth/configuration.rb +54 -0
- data/lib/stealth/controller.rb +190 -0
- data/lib/stealth/dispatcher.rb +48 -0
- data/lib/stealth/errors.rb +32 -0
- data/lib/stealth/flow/base.rb +256 -0
- data/lib/stealth/flow/errors.rb +25 -0
- data/lib/stealth/flow/event.rb +43 -0
- data/lib/stealth/flow/event_collection.rb +41 -0
- data/lib/stealth/flow/specification.rb +67 -0
- data/lib/stealth/flow/state.rb +48 -0
- data/lib/stealth/jobs.rb +10 -0
- data/lib/stealth/logger.rb +16 -0
- data/lib/stealth/reply.rb +19 -0
- data/lib/stealth/server.rb +38 -0
- data/lib/stealth/service_message.rb +17 -0
- data/lib/stealth/service_reply.rb +30 -0
- data/lib/stealth/services/base_client.rb +28 -0
- data/lib/stealth/services/base_message_handler.rb +28 -0
- data/lib/stealth/services/base_reply_handler.rb +65 -0
- data/lib/stealth/services/facebook/client.rb +35 -0
- data/lib/stealth/services/facebook/events/message_event.rb +59 -0
- data/lib/stealth/services/facebook/events/postback_event.rb +36 -0
- data/lib/stealth/services/facebook/message_handler.rb +84 -0
- data/lib/stealth/services/facebook/reply_handler.rb +471 -0
- data/lib/stealth/services/facebook/setup.rb +25 -0
- data/lib/stealth/services/jobs/handle_message_job.rb +22 -0
- data/lib/stealth/session.rb +74 -0
- data/lib/stealth/version.rb +12 -0
- data/lib/stealth.rb +1 -0
- data/spec/configuration_spec.rb +52 -0
- data/spec/flow/custom_transitions_spec.rb +99 -0
- data/spec/flow/flow_spec.rb +91 -0
- data/spec/flow/transition_callbacks_spec.rb +228 -0
- data/spec/replies/nested_reply_with_erb.yml +16 -0
- data/spec/sample_services_yml/services.yml +31 -0
- data/spec/sample_services_yml/services_with_erb.yml +31 -0
- data/spec/service_reply_spec.rb +34 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/version_spec.rb +16 -0
- data/stealth.gemspec +30 -0
- metadata +247 -0
@@ -0,0 +1,471 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Stealth
|
5
|
+
module Services
|
6
|
+
module Facebook
|
7
|
+
|
8
|
+
class ReplyHandler < Stealth::Services::BaseReplyHandler
|
9
|
+
|
10
|
+
attr_reader :recipient_id, :reply
|
11
|
+
|
12
|
+
def initialize(recipient_id: nil, reply: nil)
|
13
|
+
@recipient_id = recipient_id
|
14
|
+
@reply = reply
|
15
|
+
end
|
16
|
+
|
17
|
+
def text
|
18
|
+
check_if_arguments_are_valid!(
|
19
|
+
suggestions: reply['suggestions'],
|
20
|
+
buttons: reply['buttons']
|
21
|
+
)
|
22
|
+
|
23
|
+
template = unstructured_template
|
24
|
+
template['message']['text'] = reply['text']
|
25
|
+
|
26
|
+
if reply['suggestions'].present?
|
27
|
+
fb_suggestions = generate_suggestions(suggestions: reply['suggestions'])
|
28
|
+
template["message"]["quick_replies"] = fb_suggestions
|
29
|
+
end
|
30
|
+
|
31
|
+
# If buttons are present, we need to convert this to a button template
|
32
|
+
if reply['buttons'].present?
|
33
|
+
template['message'].delete('text')
|
34
|
+
|
35
|
+
fb_buttons = generate_buttons(buttons: reply['buttons'])
|
36
|
+
attachment = button_attachment_template(text: reply['text'], buttons: fb_buttons)
|
37
|
+
template['message']['attachment'] = attachment
|
38
|
+
end
|
39
|
+
|
40
|
+
template
|
41
|
+
end
|
42
|
+
|
43
|
+
def image
|
44
|
+
check_if_arguments_are_valid!(
|
45
|
+
suggestions: reply['suggestions'],
|
46
|
+
buttons: reply['buttons']
|
47
|
+
)
|
48
|
+
|
49
|
+
template = unstructured_template
|
50
|
+
attachment = attachment_template(
|
51
|
+
attachment_type: 'image',
|
52
|
+
attachment_url: reply['image_url']
|
53
|
+
)
|
54
|
+
template['message']['attachment'] = attachment
|
55
|
+
|
56
|
+
if reply['suggestions'].present?
|
57
|
+
fb_suggestions = generate_suggestions(suggestions: reply['suggestions'])
|
58
|
+
template["message"]["quick_replies"] = fb_suggestions
|
59
|
+
end
|
60
|
+
|
61
|
+
template
|
62
|
+
end
|
63
|
+
|
64
|
+
def audio
|
65
|
+
check_if_arguments_are_valid!(
|
66
|
+
suggestions: reply['suggestions'],
|
67
|
+
buttons: reply['buttons']
|
68
|
+
)
|
69
|
+
|
70
|
+
template = unstructured_template
|
71
|
+
attachment = attachment_template(
|
72
|
+
attachment_type: 'audio',
|
73
|
+
attachment_url: reply['audio_url']
|
74
|
+
)
|
75
|
+
template['message']['attachment'] = attachment
|
76
|
+
|
77
|
+
if reply['suggestions'].present?
|
78
|
+
fb_suggestions = generate_suggestions(suggestions: reply['suggestions'])
|
79
|
+
template["message"]["quick_replies"] = fb_suggestions
|
80
|
+
end
|
81
|
+
|
82
|
+
template
|
83
|
+
end
|
84
|
+
|
85
|
+
def video
|
86
|
+
check_if_arguments_are_valid!(
|
87
|
+
suggestions: reply['suggestions'],
|
88
|
+
buttons: reply['buttons']
|
89
|
+
)
|
90
|
+
|
91
|
+
template = unstructured_template
|
92
|
+
attachment = attachment_template(
|
93
|
+
attachment_type: 'video',
|
94
|
+
attachment_url: reply['video_url']
|
95
|
+
)
|
96
|
+
template['message']['attachment'] = attachment
|
97
|
+
|
98
|
+
if reply['suggestions'].present?
|
99
|
+
fb_suggestions = generate_suggestions(suggestions: reply['suggestions'])
|
100
|
+
template["message"]["quick_replies"] = fb_suggestions
|
101
|
+
end
|
102
|
+
|
103
|
+
template
|
104
|
+
end
|
105
|
+
|
106
|
+
def file
|
107
|
+
check_if_arguments_are_valid!(
|
108
|
+
suggestions: reply['suggestions'],
|
109
|
+
buttons: reply['buttons']
|
110
|
+
)
|
111
|
+
|
112
|
+
template = unstructured_template
|
113
|
+
attachment = attachment_template(
|
114
|
+
attachment_type: 'file',
|
115
|
+
attachment_url: reply['file_url']
|
116
|
+
)
|
117
|
+
template['message']['attachment'] = attachment
|
118
|
+
|
119
|
+
if reply['suggestions'].present?
|
120
|
+
fb_suggestions = generate_suggestions(suggestions: reply['suggestions'])
|
121
|
+
template["message"]["quick_replies"] = fb_suggestions
|
122
|
+
end
|
123
|
+
|
124
|
+
template
|
125
|
+
end
|
126
|
+
|
127
|
+
def cards
|
128
|
+
template = card_template(
|
129
|
+
sharable: reply["details"]["sharable"],
|
130
|
+
aspect_ratio: reply["details"]["aspect_ratio"]
|
131
|
+
)
|
132
|
+
|
133
|
+
fb_elements = generate_card_elements(elements: reply["details"]["elements"])
|
134
|
+
template["message"]["attachments"]["payload"]["elements"] = fb_elements
|
135
|
+
|
136
|
+
template
|
137
|
+
end
|
138
|
+
|
139
|
+
def list
|
140
|
+
template = list_template(
|
141
|
+
top_element_style: reply["details"]["top_element_style"]
|
142
|
+
)
|
143
|
+
|
144
|
+
fb_elements = generate_list_elements(elements: reply["details"]["elements"])
|
145
|
+
template["message"]["attachments"]["payload"]["elements"] = fb_elements
|
146
|
+
|
147
|
+
if reply["details"]["buttons"].present?
|
148
|
+
if reply["details"]["buttons"].size > 1
|
149
|
+
raise(ArgumentError, "Facebook lists support a single button attached to the list itsef.")
|
150
|
+
end
|
151
|
+
|
152
|
+
template["message"]["attachments"]["payload"]["buttons"] = generate_buttons(buttons: reply["details"]["buttons"])
|
153
|
+
end
|
154
|
+
|
155
|
+
template
|
156
|
+
end
|
157
|
+
|
158
|
+
def mark_seen
|
159
|
+
sender_action_template(action: 'mark_seen')
|
160
|
+
end
|
161
|
+
|
162
|
+
def enable_typing_indicator
|
163
|
+
sender_action_template(action: 'typing_on')
|
164
|
+
end
|
165
|
+
|
166
|
+
def disable_typing_indicator
|
167
|
+
sender_action_template(action: 'typing_off')
|
168
|
+
end
|
169
|
+
|
170
|
+
def delay
|
171
|
+
enable_typing_indicator
|
172
|
+
end
|
173
|
+
|
174
|
+
# generates property/value pairs required to set the profile
|
175
|
+
def messenger_profile
|
176
|
+
unless Stealth.config.facebook.setup.present?
|
177
|
+
raise Stealth::Errors::ConfigurationError, "Setup for Facebook is not specified in services.yml."
|
178
|
+
end
|
179
|
+
|
180
|
+
profile = {}
|
181
|
+
Stealth.config.facebook.setup.each do |profile_option, _|
|
182
|
+
profile[profile_option] = self.send(profile_option)
|
183
|
+
end
|
184
|
+
|
185
|
+
profile
|
186
|
+
end
|
187
|
+
|
188
|
+
private
|
189
|
+
|
190
|
+
def unstructured_template
|
191
|
+
{
|
192
|
+
"recipient" => {
|
193
|
+
"id" => recipient_id
|
194
|
+
},
|
195
|
+
"message" => { }
|
196
|
+
}
|
197
|
+
end
|
198
|
+
|
199
|
+
def card_template(sharable: nil, aspect_ratio: nil)
|
200
|
+
template = {
|
201
|
+
"recipient" => {
|
202
|
+
"id" => recipient_id
|
203
|
+
},
|
204
|
+
"message" => {
|
205
|
+
"type" => "template",
|
206
|
+
"payload" => {
|
207
|
+
"template_type" => "generic",
|
208
|
+
"elements" => []
|
209
|
+
}
|
210
|
+
}
|
211
|
+
}
|
212
|
+
|
213
|
+
if sharable.present?
|
214
|
+
template["message"]["payload"]["sharable"] = sharable
|
215
|
+
end
|
216
|
+
|
217
|
+
if aspect_ratio.present?
|
218
|
+
template["message"]["payload"]["image_aspect_ratio"] = aspect_ratio
|
219
|
+
end
|
220
|
+
|
221
|
+
template
|
222
|
+
end
|
223
|
+
|
224
|
+
def list_template(top_element_style: nil, buttons: [])
|
225
|
+
template = {
|
226
|
+
"recipient" => {
|
227
|
+
"id" => recipient_id
|
228
|
+
},
|
229
|
+
"message" => {
|
230
|
+
"type" => "template",
|
231
|
+
"payload" => {
|
232
|
+
"template_type" => "list",
|
233
|
+
"elements" => []
|
234
|
+
}
|
235
|
+
}
|
236
|
+
}
|
237
|
+
|
238
|
+
if top_element_style.present?
|
239
|
+
unless ['large', 'compact'].include?(top_element_style)
|
240
|
+
raise(ArgumentError, "Facebook list replies only support 'large' or 'compact' as the top_element_style.")
|
241
|
+
end
|
242
|
+
|
243
|
+
template["message"]["payload"]["top_element_style"] = top_element_style
|
244
|
+
end
|
245
|
+
|
246
|
+
if buttons.present?
|
247
|
+
unless buttons.size > 1
|
248
|
+
raise(ArgumentError, "Facebook lists only support a single button in the top element.")
|
249
|
+
end
|
250
|
+
|
251
|
+
template["message"]["payload"]["buttons"] = aspect_ratio
|
252
|
+
end
|
253
|
+
|
254
|
+
template
|
255
|
+
end
|
256
|
+
|
257
|
+
def element_template(element_type:, element:)
|
258
|
+
unless element["text"].present?
|
259
|
+
raise(ArgumentError, "Facebook card and list elements must have a 'text' attribute.")
|
260
|
+
end
|
261
|
+
|
262
|
+
template = {
|
263
|
+
"title" => element["title"]
|
264
|
+
}
|
265
|
+
|
266
|
+
if element["subtitle"].present?
|
267
|
+
template["subtitle"] = element["subtitle"]
|
268
|
+
end
|
269
|
+
|
270
|
+
if element["image_url"].present?
|
271
|
+
template["image_url"] = element["image_url"]
|
272
|
+
end
|
273
|
+
|
274
|
+
if element["default_action"].present?
|
275
|
+
default_action = generate_default_action(action_params: element["default_action"])
|
276
|
+
template["default_action"] = default_action
|
277
|
+
end
|
278
|
+
|
279
|
+
if element["buttons"].present?
|
280
|
+
if element_type == 'card' && element["buttons"].size > 3
|
281
|
+
raise(ArgumentError, "Facebook card elements only support 3 buttons.")
|
282
|
+
end
|
283
|
+
|
284
|
+
if element_type == 'list' && element["buttons"].size > 1
|
285
|
+
raise(ArgumentError, "Facebook list elements only support 1 button.")
|
286
|
+
end
|
287
|
+
|
288
|
+
fb_buttons = generate_buttons(buttons: element["buttons"])
|
289
|
+
template["buttons"] = fb_buttons
|
290
|
+
end
|
291
|
+
|
292
|
+
template
|
293
|
+
end
|
294
|
+
|
295
|
+
def attachment_template(attachment_type:, attachment_url:)
|
296
|
+
{
|
297
|
+
"type" => attachment_type,
|
298
|
+
"payload" => {
|
299
|
+
"url" => attachment_url
|
300
|
+
}
|
301
|
+
}
|
302
|
+
end
|
303
|
+
|
304
|
+
def button_attachment_template(text:, buttons:)
|
305
|
+
{
|
306
|
+
"type" => "template",
|
307
|
+
"payload" => {
|
308
|
+
"template_type" => "button",
|
309
|
+
"text" => text,
|
310
|
+
"buttons" => buttons
|
311
|
+
}
|
312
|
+
}
|
313
|
+
end
|
314
|
+
|
315
|
+
def sender_action_template(action:)
|
316
|
+
{
|
317
|
+
"recipient" => {
|
318
|
+
"id" => recipient_id
|
319
|
+
},
|
320
|
+
"sender_action" => action
|
321
|
+
}
|
322
|
+
end
|
323
|
+
|
324
|
+
def generate_card_elements(elements:)
|
325
|
+
if elements.size > 10
|
326
|
+
raise(ArgumentError, "Facebook cards can have at most 10 cards.")
|
327
|
+
end
|
328
|
+
|
329
|
+
fb_elements = elements.collect do |element|
|
330
|
+
element_template(element_type: 'card', element: element)
|
331
|
+
end
|
332
|
+
|
333
|
+
fb_elements
|
334
|
+
end
|
335
|
+
|
336
|
+
def generate_list_elements(elements:)
|
337
|
+
if elements.size < 2 || elements.size > 4
|
338
|
+
raise(ArgumentError, "Facebook lists must have 2-4 elements.")
|
339
|
+
end
|
340
|
+
|
341
|
+
fb_elements = elements.collect do |element|
|
342
|
+
element_template(element_type: 'list', element: element)
|
343
|
+
end
|
344
|
+
|
345
|
+
fb_elements
|
346
|
+
end
|
347
|
+
|
348
|
+
def generate_suggestions(suggestions:)
|
349
|
+
quick_replies = suggestions.collect do |suggestion|
|
350
|
+
# If the user selected a location-type button, no other info needed
|
351
|
+
if suggestion["type"] == 'location'
|
352
|
+
quick_reply = { "content_type" => "location" }
|
353
|
+
|
354
|
+
# Facebook only supports these two types for now
|
355
|
+
else
|
356
|
+
quick_reply = {
|
357
|
+
"content_type" => "text",
|
358
|
+
"title" => suggestion["text"]
|
359
|
+
}
|
360
|
+
|
361
|
+
if suggestion["payload"].present?
|
362
|
+
quick_reply["payload"] = suggestion["payload"]
|
363
|
+
else
|
364
|
+
quick_reply["payload"] = suggestion["text"]
|
365
|
+
end
|
366
|
+
|
367
|
+
if suggestion["image_url"].present?
|
368
|
+
quick_reply["image_url"] = suggestion["image_url"]
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
quick_reply
|
373
|
+
end
|
374
|
+
|
375
|
+
quick_replies
|
376
|
+
end
|
377
|
+
|
378
|
+
# Requires adding support for Buy, Log In, Log Out, and Share button types
|
379
|
+
def generate_buttons(buttons:)
|
380
|
+
fb_buttons = buttons.collect do |button|
|
381
|
+
case button['type']
|
382
|
+
when 'url'
|
383
|
+
button = {
|
384
|
+
"type" => "web_url",
|
385
|
+
"url" => button["url"],
|
386
|
+
"title" => button["text"]
|
387
|
+
}
|
388
|
+
|
389
|
+
if button["webview_height"].present?
|
390
|
+
button["webview_height_ratio"] = button["webview_height"]
|
391
|
+
end
|
392
|
+
|
393
|
+
button
|
394
|
+
|
395
|
+
when 'payload'
|
396
|
+
button = {
|
397
|
+
"type" => "postback",
|
398
|
+
"payload" => button["payload"],
|
399
|
+
"title" => button["text"]
|
400
|
+
}
|
401
|
+
|
402
|
+
when 'call'
|
403
|
+
button = {
|
404
|
+
"type" => "phone_number",
|
405
|
+
"payload" => button["phone_number"],
|
406
|
+
"title" => button["text"]
|
407
|
+
}
|
408
|
+
|
409
|
+
when 'nested'
|
410
|
+
button = {
|
411
|
+
"type" => "nested",
|
412
|
+
"title" => button["text"],
|
413
|
+
"call_to_actions" => generate_buttons(buttons: button["buttons"])
|
414
|
+
}
|
415
|
+
|
416
|
+
else
|
417
|
+
raise(Stealth::Errors::ServiceImpaired, "Sorry, we don't yet support #{button["type"]} buttons yet!")
|
418
|
+
end
|
419
|
+
|
420
|
+
button
|
421
|
+
end
|
422
|
+
|
423
|
+
fb_buttons
|
424
|
+
end
|
425
|
+
|
426
|
+
def generate_default_action(action_params:)
|
427
|
+
default_action = {
|
428
|
+
"type" => "web_url",
|
429
|
+
"url" => action_params["url"]
|
430
|
+
}
|
431
|
+
|
432
|
+
if action_params["webview_height"].present?
|
433
|
+
action_params["webview_height_ratio"] = action_params["webview_height"]
|
434
|
+
end
|
435
|
+
|
436
|
+
default_action
|
437
|
+
end
|
438
|
+
|
439
|
+
def check_if_arguments_are_valid!(suggestions:, buttons:)
|
440
|
+
if suggestions.present? && buttons.present?
|
441
|
+
raise(ArgumentError, "A reply cannot have buttons and suggestions!")
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
def greeting
|
446
|
+
Stealth.config.facebook.setup.greeting.map do |greeting|
|
447
|
+
{
|
448
|
+
"locale" => greeting["locale"],
|
449
|
+
"text" => greeting["text"]
|
450
|
+
}
|
451
|
+
end
|
452
|
+
end
|
453
|
+
|
454
|
+
def persistent_menu
|
455
|
+
Stealth.config.facebook.setup.persistent_menu.map do |persistent_menu|
|
456
|
+
{
|
457
|
+
"locale" => persistent_menu['locale'],
|
458
|
+
"composer_input_disabled" => (persistent_menu['composer_input_disabled'] || false),
|
459
|
+
"call_to_actions" => generate_buttons(buttons: persistent_menu['call_to_actions'])
|
460
|
+
}
|
461
|
+
end
|
462
|
+
end
|
463
|
+
|
464
|
+
def get_started
|
465
|
+
Stealth.config.facebook.setup.get_started
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
469
|
+
end
|
470
|
+
end
|
471
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'stealth/services/facebook/client'
|
5
|
+
|
6
|
+
module Stealth
|
7
|
+
module Services
|
8
|
+
module Facebook
|
9
|
+
|
10
|
+
class Setup
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def trigger
|
14
|
+
reply_handler = Stealth::Services::Facebook::ReplyHandler.new
|
15
|
+
reply = reply_handler.messenger_profile
|
16
|
+
client = Stealth::Services::Facebook::Client.new(reply: reply, endpoint: 'messenger_profile')
|
17
|
+
client.transmit
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Stealth
|
5
|
+
module Services
|
6
|
+
|
7
|
+
class HandleMessageJob < Stealth::Jobs
|
8
|
+
sidekiq_options queue: :webhooks, retry: false
|
9
|
+
|
10
|
+
def perform(service, params, headers)
|
11
|
+
dispatcher = Stealth::Dispatcher.new(
|
12
|
+
service: service,
|
13
|
+
params: params,
|
14
|
+
headers: headers
|
15
|
+
)
|
16
|
+
|
17
|
+
dispatcher.process
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Stealth
|
5
|
+
class Session
|
6
|
+
|
7
|
+
SLUG_SEPARATOR = '->'
|
8
|
+
|
9
|
+
attr_reader :session, :flow, :state, :user_id
|
10
|
+
|
11
|
+
def initialize(user_id:)
|
12
|
+
@user_id = user_id
|
13
|
+
|
14
|
+
unless defined?($redis)
|
15
|
+
raise(Stealth::Errors::RedisNotConfigured, "Please make sure REDIS_URL is configured before using sessions.")
|
16
|
+
end
|
17
|
+
|
18
|
+
get
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.flow_and_state_from_session_slug(slug:)
|
23
|
+
{
|
24
|
+
flow: slug&.split(SLUG_SEPARATOR)&.first,
|
25
|
+
state: slug&.split(SLUG_SEPARATOR)&.last
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def flow
|
30
|
+
@flow = begin
|
31
|
+
flow_klass = [flow_string, 'flow'].join('_').classify.constantize
|
32
|
+
flow = flow_klass.new.init_state(state_string)
|
33
|
+
flow
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def state
|
38
|
+
flow.current_state
|
39
|
+
end
|
40
|
+
|
41
|
+
def flow_string
|
42
|
+
session&.split(SLUG_SEPARATOR)&.first
|
43
|
+
end
|
44
|
+
|
45
|
+
def state_string
|
46
|
+
session&.split(SLUG_SEPARATOR)&.last
|
47
|
+
end
|
48
|
+
|
49
|
+
def get
|
50
|
+
@session ||= $redis.get(user_id)
|
51
|
+
end
|
52
|
+
|
53
|
+
def set(flow:, state:)
|
54
|
+
@session = canonical_session_slug(flow: flow, state: state)
|
55
|
+
flow
|
56
|
+
$redis.set(user_id, canonical_session_slug(flow: flow, state: state))
|
57
|
+
end
|
58
|
+
|
59
|
+
def present?
|
60
|
+
session.present?
|
61
|
+
end
|
62
|
+
|
63
|
+
def blank?
|
64
|
+
!present?
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def canonical_session_slug(flow:, state:)
|
70
|
+
[flow, state].join(SLUG_SEPARATOR)
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
data/lib/stealth.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'stealth/base'
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
4
|
+
|
5
|
+
describe "Stealth::Configuration" do
|
6
|
+
|
7
|
+
describe "accessing via method calling" do
|
8
|
+
let(:services_yml) { File.read(File.join(File.dirname(__FILE__), 'sample_services_yml', 'services.yml')) }
|
9
|
+
let(:parsed_config) { YAML.load(ERB.new(services_yml).result)[Stealth.env] }
|
10
|
+
let(:config) { Stealth.load_services_config!(services_yml) }
|
11
|
+
|
12
|
+
it "should return the root node" do
|
13
|
+
expect(config.facebook).to eq parsed_config['facebook']
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should access deeply nested nodes" do
|
17
|
+
expect(config.facebook.setup.greeting).to eq parsed_config['facebook']['setup']['greeting']
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should handle values that are arrays correctly" do
|
21
|
+
expect(config.facebook.setup.persistent_menu).to be_a(Array)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should retain the configuration at the class level" do
|
25
|
+
expect(Stealth.config.facebook.setup.greeting).to eq parsed_config['facebook']['setup']['greeting']
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should handle multiple keys at the root level" do
|
29
|
+
expect(config.twilio_sms.account_sid).to eq parsed_config['twilio_sms']['account_sid']
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "config files with ERB" do
|
34
|
+
let(:services_yml) { File.read(File.join(File.dirname(__FILE__), 'sample_services_yml', 'services_with_erb.yml')) }
|
35
|
+
let(:config) { Stealth.load_services_config!(services_yml) }
|
36
|
+
|
37
|
+
it "should replace available embedded env vars" do
|
38
|
+
ENV['FACEBOOK_VERIFY_TOKEN'] = 'it works'
|
39
|
+
expect(config.facebook.verify_token).to eq 'it works'
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should replace unavailable embedded env vars with nil" do
|
43
|
+
expect(config.facebook.challenge).to be_nil
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should not reload the configuration file if one already exists" do
|
47
|
+
Stealth.load_services_config(services_yml)
|
48
|
+
expect(config.facebook.challenge).to be_nil
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|