stealth 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|