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.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +58 -0
  3. data/.gitignore +12 -0
  4. data/Gemfile +6 -0
  5. data/Gemfile.lock +81 -0
  6. data/LICENSE +20 -0
  7. data/README.md +1 -0
  8. data/VERSION +1 -0
  9. data/bin/stealth +5 -0
  10. data/lib/stealth/base.rb +87 -0
  11. data/lib/stealth/cli.rb +82 -0
  12. data/lib/stealth/cli_base.rb +25 -0
  13. data/lib/stealth/commands/command.rb +14 -0
  14. data/lib/stealth/commands/console.rb +75 -0
  15. data/lib/stealth/commands/server.rb +20 -0
  16. data/lib/stealth/configuration.rb +54 -0
  17. data/lib/stealth/controller.rb +190 -0
  18. data/lib/stealth/dispatcher.rb +48 -0
  19. data/lib/stealth/errors.rb +32 -0
  20. data/lib/stealth/flow/base.rb +256 -0
  21. data/lib/stealth/flow/errors.rb +25 -0
  22. data/lib/stealth/flow/event.rb +43 -0
  23. data/lib/stealth/flow/event_collection.rb +41 -0
  24. data/lib/stealth/flow/specification.rb +67 -0
  25. data/lib/stealth/flow/state.rb +48 -0
  26. data/lib/stealth/jobs.rb +10 -0
  27. data/lib/stealth/logger.rb +16 -0
  28. data/lib/stealth/reply.rb +19 -0
  29. data/lib/stealth/server.rb +38 -0
  30. data/lib/stealth/service_message.rb +17 -0
  31. data/lib/stealth/service_reply.rb +30 -0
  32. data/lib/stealth/services/base_client.rb +28 -0
  33. data/lib/stealth/services/base_message_handler.rb +28 -0
  34. data/lib/stealth/services/base_reply_handler.rb +65 -0
  35. data/lib/stealth/services/facebook/client.rb +35 -0
  36. data/lib/stealth/services/facebook/events/message_event.rb +59 -0
  37. data/lib/stealth/services/facebook/events/postback_event.rb +36 -0
  38. data/lib/stealth/services/facebook/message_handler.rb +84 -0
  39. data/lib/stealth/services/facebook/reply_handler.rb +471 -0
  40. data/lib/stealth/services/facebook/setup.rb +25 -0
  41. data/lib/stealth/services/jobs/handle_message_job.rb +22 -0
  42. data/lib/stealth/session.rb +74 -0
  43. data/lib/stealth/version.rb +12 -0
  44. data/lib/stealth.rb +1 -0
  45. data/spec/configuration_spec.rb +52 -0
  46. data/spec/flow/custom_transitions_spec.rb +99 -0
  47. data/spec/flow/flow_spec.rb +91 -0
  48. data/spec/flow/transition_callbacks_spec.rb +228 -0
  49. data/spec/replies/nested_reply_with_erb.yml +16 -0
  50. data/spec/sample_services_yml/services.yml +31 -0
  51. data/spec/sample_services_yml/services_with_erb.yml +31 -0
  52. data/spec/service_reply_spec.rb +34 -0
  53. data/spec/spec_helper.rb +13 -0
  54. data/spec/version_spec.rb +16 -0
  55. data/stealth.gemspec +30 -0
  56. metadata +247 -0
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
4
+
5
+ describe "custom transitions" do
6
+
7
+ class FetchTodosFlow
8
+ include Stealth::Flow
9
+
10
+ flow do
11
+ state :todays_todos do
12
+ event :fetch_tomorrows_todos, :transitions_to => :tomorrows_todos
13
+ event :fetch_yesterdays_todos, :transitions_to => :yesterdays_todos
14
+ end
15
+
16
+ state :tomorrows_todos do
17
+ event :view_todo, :transitions_to => :show
18
+ event :edit_todo, :transitions_to => :edit
19
+ end
20
+
21
+ state :tomorrows_todos do
22
+ event :view_todo, :transitions_to => :show
23
+ event :edit_todo, :transitions_to => :edit
24
+ end
25
+
26
+ state :show
27
+
28
+ state :edit do
29
+ event :save_todo, :transitions_to => :show
30
+ event :error_in_input, :transitions_to => :error
31
+ end
32
+
33
+ state :error
34
+ end
35
+
36
+ def view_todo(todo_id)
37
+ unless todo_id > 0
38
+ halt('ID is not valid.')
39
+ end
40
+
41
+ :the_todo
42
+ end
43
+
44
+ def edit_todo(todo_id)
45
+ unless todo_id > 0
46
+ halt('ID is not valid.')
47
+ end
48
+
49
+ :edit_todo_view
50
+ end
51
+
52
+ def save_todo(params)
53
+ if params.nil?
54
+ halt!('Invalid todo params specified.')
55
+ end
56
+
57
+ :todo_saved
58
+ end
59
+ end
60
+
61
+ let(:flow) { FetchTodosFlow.new }
62
+
63
+ it "should transition via custom transition methods" do
64
+ flow.fetch_tomorrows_todos!
65
+ expect(flow.view_todo!(1)).to eq :the_todo
66
+ expect(flow.current_state).to eq :show
67
+ end
68
+
69
+ it "should follow multiple custom transitions" do
70
+ flow.fetch_tomorrows_todos!
71
+ expect(flow.edit_todo!(1)).to eq :edit_todo_view
72
+ expect(flow.current_state).to eq :edit
73
+
74
+ expect(flow.save_todo!({ task: 'test' })).to eq :todo_saved
75
+ expect(flow.current_state).to eq :show
76
+ end
77
+
78
+ describe "halting transitions" do
79
+ it "should halt the transition when halt() is called" do
80
+ flow.fetch_tomorrows_todos!
81
+ flow.view_todo!(-1)
82
+ expect(flow.current_state).to eq :tomorrows_todos
83
+ expect(flow.halted_because).to eq "ID is not valid."
84
+ end
85
+
86
+ it "should halt the transition when halt!() is called and raise Stealth::Flow::TransitionHalted" do
87
+ flow.fetch_tomorrows_todos!
88
+ flow.edit_todo!(1)
89
+ expect(flow.current_state).to eq :edit
90
+
91
+ expect {
92
+ flow.save_todo!(nil)
93
+ }.to raise_error(Stealth::Flow::TransitionHalted)
94
+
95
+ expect(flow.halted_because).to eq "Invalid todo params specified."
96
+ end
97
+ end
98
+
99
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
4
+
5
+ describe Stealth::Flow do
6
+
7
+ class NewTodoFlow
8
+ include Stealth::Flow
9
+
10
+ flow do
11
+ state :new do
12
+ event :submit_todo, :transitions_to => :get_due_date
13
+ event :error_in_input, :transitions_to => :error
14
+ end
15
+
16
+ state :get_due_date do
17
+ event :submit_due_date, :transitions_to => :created
18
+ end
19
+
20
+ state :created
21
+
22
+ state :error do
23
+ event :submit_todo, :transitions_to => :get_due_date
24
+ event :error_in_input, :transitions_to => :error
25
+ end
26
+ end
27
+ end
28
+
29
+ let(:flow) { NewTodoFlow.new }
30
+
31
+ describe "state transitions" do
32
+ it "should start out in the 'new' state" do
33
+ expect(flow.current_state).to eq :new
34
+ end
35
+
36
+ it "should transition into the 'get_due_date' state after submit" do
37
+ flow.submit_todo!
38
+ expect(flow.current_state).to eq :get_due_date
39
+ end
40
+
41
+ it "should transition into the 'error' state after error_in_input" do
42
+ flow.error_in_input!
43
+ expect(flow.current_state).to eq :error
44
+ end
45
+
46
+ it "should transition through multiple states" do
47
+ flow.submit_todo!
48
+ flow.submit_due_date!
49
+ expect(flow.current_state).to eq :created
50
+ end
51
+
52
+ it "should remain in the error state" do
53
+ flow.error_in_input!
54
+ expect(flow.current_state).to eq :error
55
+ flow.error_in_input!
56
+ expect(flow.current_state).to eq :error
57
+ end
58
+
59
+ it "should be false when checking the possibility of a non-valid transition" do
60
+ expect(flow.can_submit_due_date?).to be false
61
+ end
62
+
63
+ it "should be false when checking the possibility of a valid transition" do
64
+ flow.submit_todo!
65
+ expect(flow.can_submit_due_date?).to be true
66
+ end
67
+ end
68
+
69
+ describe "accessing states" do
70
+ it "should start out in the 'new' state" do
71
+ expect(flow.new?).to be true
72
+ end
73
+
74
+ it "should support comparing states" do
75
+ first_state = NewTodoFlow.flow_spec.states[:new]
76
+ last_state = NewTodoFlow.flow_spec.states[:error]
77
+ expect(first_state < last_state).to be true
78
+ expect(last_state > first_state).to be true
79
+ end
80
+
81
+ it "should allow every state to be fetched for the class" do
82
+ expect(NewTodoFlow.flow_spec.states.length).to eq 4
83
+ expect(NewTodoFlow.flow_spec.states.keys).to eq([:new, :get_due_date, :created, :error])
84
+ end
85
+
86
+ it "should return the states in an array for a given flow instance" do
87
+ expect(flow.states).to eq [:new, :get_due_date, :created, :error]
88
+ end
89
+ end
90
+
91
+ end
@@ -0,0 +1,228 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
4
+
5
+ describe "transition callbacks" do
6
+
7
+ class KonamiCodeFlow
8
+ include Stealth::Flow
9
+
10
+ attr_reader :code, :before_transition_count, :transition_count
11
+
12
+ def initialize
13
+ @code = []
14
+ @before_transition_count = 0
15
+ @transition_count = 0
16
+ end
17
+
18
+ flow do
19
+ state :up do
20
+ event :upward, :transitions_to => :up
21
+ event :downward, :transitions_to => :down
22
+ end
23
+
24
+ state :down do
25
+ event :downward, :transitions_to => :down
26
+ event :leftward, :transitions_to => :left
27
+ end
28
+
29
+ state :left do
30
+ event :rightward, :transitions_to => :right
31
+ end
32
+
33
+ state :right do
34
+ event :leftward, :transitions_to => :left
35
+ event :bward, :transitions_to => :b
36
+ end
37
+
38
+ state :a
39
+
40
+ state :b do
41
+ event :award, :transitions_to => :a
42
+ end
43
+
44
+ before_transition do |from, to, triggering_event, *event_args|
45
+ @before_transition_count += 1
46
+ end
47
+
48
+ after_transition do |from, to, triggering_event, *event_args|
49
+ @code << to.to_s
50
+ end
51
+
52
+ on_transition {
53
+ @transition_count += 1
54
+ }
55
+ end
56
+ end
57
+
58
+ let(:flow) { KonamiCodeFlow.new }
59
+
60
+ before(:each) do
61
+ flow.upward!
62
+ flow.upward!
63
+ flow.downward!
64
+ flow.downward!
65
+ flow.leftward!
66
+ flow.rightward!
67
+ flow.leftward!
68
+ flow.rightward!
69
+ flow.bward!
70
+ flow.award!
71
+ end
72
+
73
+ it "should have a correct transition count" do
74
+ expect(flow.transition_count).to eq 10
75
+ end
76
+
77
+ it "should have a correct before_transition count" do
78
+ expect(flow.before_transition_count).to eq 10
79
+ end
80
+
81
+ it "should have generated the correct code via after_transition" do
82
+ expect(flow.code).to eq(['up', 'up', 'down', 'down', 'left', 'right', 'left', 'right', 'b', 'a'])
83
+ end
84
+
85
+ describe "on_error callbacks" do
86
+ class ErrorFlow
87
+ include Stealth::Flow
88
+
89
+ attr_reader :errors
90
+
91
+ def initialize
92
+ @errors = {}
93
+ end
94
+
95
+ flow do
96
+ state :first do
97
+ event :advance, :transitions_to => :second do
98
+ raise "uh oh"
99
+ end
100
+ end
101
+
102
+ state :second
103
+
104
+ on_error do |error, from, to, event, *args|
105
+ @errors.merge!({
106
+ error: error.class,
107
+ from: from,
108
+ to: to,
109
+ event: event,
110
+ args: args
111
+ })
112
+ end
113
+ end
114
+ end
115
+
116
+ let(:error_flow) { ErrorFlow.new }
117
+
118
+ it "should not advance to the next state" do
119
+ error_flow.advance!
120
+ expect(error_flow.current_state).to eq :first
121
+ end
122
+
123
+ it "should call the on_error block" do
124
+ error_flow.advance!
125
+ expect(error_flow.errors).to eq({ error: RuntimeError, from: :first, to: :second, event: :advance, args: [] })
126
+ end
127
+ end
128
+
129
+ describe "on_entry and on_exit callbacks" do
130
+ class PostFlow
131
+ include Stealth::Flow
132
+
133
+ attr_reader :email_reviewer, :tweet_link
134
+
135
+ def initialize
136
+ @email_reviewer = false
137
+ @tweet_link = false
138
+ end
139
+
140
+ flow do
141
+ state :draft do
142
+ event :submit_for_review, :transitions_to => :in_review
143
+
144
+ on_exit do
145
+ @email_reviewer = true
146
+ end
147
+ end
148
+
149
+ state :in_review do
150
+ event :approve, :transitions_to => :live
151
+ event :reject, :transitions_to => :draft
152
+ end
153
+
154
+ state :live do
155
+ on_entry do
156
+ @tweet_link = true
157
+ end
158
+ end
159
+ end
160
+ end
161
+
162
+ let(:post_flow) { PostFlow.new }
163
+
164
+ it "should email the reviewer when flow transitions (on_exit) to in_review" do
165
+ expect {
166
+ post_flow.submit_for_review!
167
+ }.to change(post_flow, :email_reviewer).from(false).to(true)
168
+ end
169
+
170
+ it "should tweet the post link when flow transitions (on_entry) to live" do
171
+ post_flow.submit_for_review!
172
+
173
+ expect {
174
+ post_flow.approve!
175
+ }.to change(post_flow, :tweet_link).from(false).to(true)
176
+ end
177
+ end
178
+
179
+ describe "on_entry and on_exit method-style callbacks" do
180
+ class ConcisePostFlow
181
+ include Stealth::Flow
182
+
183
+ attr_reader :email_reviewer, :tweet_link
184
+
185
+ def initialize
186
+ @email_reviewer = false
187
+ @tweet_link = false
188
+ end
189
+
190
+ flow do
191
+ state :draft do
192
+ event :submit_for_review, :transitions_to => :in_review
193
+ end
194
+
195
+ state :in_review do
196
+ event :approve, :transitions_to => :live
197
+ event :reject, :transitions_to => :draft
198
+ end
199
+
200
+ state :live
201
+ end
202
+
203
+ def on_draft_exit(new_state, event, *args)
204
+ @email_reviewer = true
205
+ end
206
+
207
+ def on_live_entry(prior_state, event, *args)
208
+ @tweet_link = true
209
+ end
210
+ end
211
+
212
+ let(:post_flow) { ConcisePostFlow.new }
213
+
214
+ it "should email the reviewer when flow transitions (on_exit) to in_review" do
215
+ expect {
216
+ post_flow.submit_for_review!
217
+ }.to change(post_flow, :email_reviewer).from(false).to(true)
218
+ end
219
+
220
+ it "should tweet the post link when flow transitions (on_entry) to live" do
221
+ post_flow.submit_for_review!
222
+
223
+ expect {
224
+ post_flow.approve!
225
+ }.to change(post_flow, :tweet_link).from(false).to(true)
226
+ end
227
+ end
228
+ end
@@ -0,0 +1,16 @@
1
+ - reply_type: text
2
+ text: "Hi, <%= first_name %>. Welcome to Stealth bot..."
3
+ - reply-type: delay
4
+ duration: 2
5
+ - reply_type: text
6
+ text: "We offer users an awesome Ruby framework for building chat bots."
7
+ - reply-type: delay
8
+ duration: 2
9
+ - reply_type: text
10
+ text: "What do you think of our bot?"
11
+ buttons:
12
+ - text: "Cool"
13
+ payload: cool
14
+ - text: "Show me more"
15
+ payload: more
16
+
@@ -0,0 +1,31 @@
1
+ default: &default
2
+ facebook:
3
+ verify_token: c68823f4-7259-4600-b9f0-382a67260757
4
+ challenge: pOmQ7iq4ZA1hK6TbO7yrVZCmygVjfIiEIYaIZAlEveOAzY4UKb
5
+ page_access_token: EAADhbJruBbMBAKY9bnesHh9eM09ZAHjGsCQtNdvuZClZCFtyIXdFdIQI2mV0PJM910Qyn3lNNhZBPZB54zLRhDNmeIkDz9myS25CTy0kFxHjQCXJxz5oeZCD60VWdAZAFxbeDKvF8eF28qDHAI4wkGc3jvVhjFISKmmFRRM6goUeAZDZD
6
+ setup:
7
+ greeting: # Greetings are broken up by locale
8
+ - locale: default
9
+ text: "Welcome to the Stealth bot 🤖"
10
+ persistent_menu:
11
+ - type: payload
12
+ text: Main Menu
13
+ payload: main_menu
14
+ - type: url
15
+ text: Visit our website
16
+ url: https://example.com
17
+ - type: call
18
+ text: Call us
19
+ payload: "+17345551234"
20
+ twilio_sms:
21
+ account_sid: BC6f4bd46307054c84fdff70badcd9ef5d
22
+ auth_token: 4af73d27d92cff6391611a9c976725cc
23
+
24
+ production:
25
+ <<: *default
26
+
27
+ development:
28
+ <<: *default
29
+
30
+ test:
31
+ <<: *default
@@ -0,0 +1,31 @@
1
+ default: &default
2
+ facebook:
3
+ verify_token: <%= ENV['FACEBOOK_VERIFY_TOKEN'] %>
4
+ challenge: <%= ENV['FACEBOOK_CHALLENGE'] %>
5
+ page_access_token: <%= ENV['FACEBOOK_PAGE_ACCESS_TOKEN'] %>
6
+ setup:
7
+ greeting: # Greetings are broken up by locale
8
+ - locale: default
9
+ text: "Welcome to the Stealth bot 🤖"
10
+ persistent_menu:
11
+ - type: payload
12
+ text: Main Menu
13
+ payload: main_menu
14
+ - type: url
15
+ text: Visit our website
16
+ url: https://example.com
17
+ - type: call
18
+ text: Call us
19
+ payload: "+17345551234"
20
+ twilio_sms:
21
+ account_sid: <%= ENV['TWILIO_ACCOUNT_SID'] %>
22
+ auth_token: <%= ENV['TWILIO_AUTH_TOKEN'] %>
23
+
24
+ production:
25
+ <<: *default
26
+
27
+ development:
28
+ <<: *default
29
+
30
+ test:
31
+ <<: *default
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
4
+
5
+ describe "Stealth::ServiceReply" do
6
+
7
+ describe "nested reply with ERB" do
8
+ let(:recipient_id) { "8b3e0a3c-62f1-401e-8b0f-615c9d256b1f" }
9
+ let(:yaml_reply) { File.read(File.join(File.dirname(__FILE__), 'replies', 'nested_reply_with_erb.yml')) }
10
+
11
+ it "should load all the replies" do
12
+ first_name = "Presley"
13
+
14
+ service_reply = Stealth::ServiceReply.new(
15
+ recipient_id: recipient_id,
16
+ yaml_reply: yaml_reply,
17
+ context: binding
18
+ )
19
+
20
+ expect(service_reply.replies.size).to eq 5
21
+ end
22
+
23
+ it "should raise Stealth::Errors::UndefinedVariable when local variable is not available" do
24
+ expect {
25
+ service_reply = Stealth::ServiceReply.new(
26
+ recipient_id: recipient_id,
27
+ yaml_reply: yaml_reply,
28
+ context: binding
29
+ )
30
+ }.to raise_error(Stealth::Errors::UndefinedVariable)
31
+ end
32
+ end
33
+
34
+ end
@@ -0,0 +1,13 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+
5
+ require 'stealth'
6
+
7
+ # Requires supporting files with custom matchers and macros, etc,
8
+ # in ./support/ and its subdirectories.
9
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
10
+
11
+ RSpec.configure do |config|
12
+
13
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
4
+
5
+ describe "Stealth::Version" do
6
+
7
+ let(:version_in_file) { File.read(File.join(File.dirname(__FILE__), '..', 'VERSION')).strip }
8
+
9
+ it "should return the current gem version" do
10
+ expect(Stealth::Version.version).to eq version_in_file
11
+ end
12
+
13
+ it "should return the current gem version via a constant" do
14
+ expect(Stealth::VERSION).to eq version_in_file
15
+ end
16
+ end
data/stealth.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ $LOAD_PATH.push File.expand_path('../lib', __FILE__)
2
+
3
+ version = File.read(File.join(File.dirname(__FILE__), 'VERSION')).strip
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'stealth'
7
+ s.summary = 'Ruby framework for conversational bots'
8
+ s.description = 'Ruby framework for building conversational bots.'
9
+ s.homepage = 'https://github.com/whoisblackops/stealth'
10
+ s.version = version
11
+ s.author = 'Mauricio Gomes'
12
+ s.email = 'mauricio@edge14.com'
13
+
14
+ s.add_dependency 'sinatra', '~> 2.0.0'
15
+ s.add_dependency 'puma', '~> 3.10.0'
16
+ s.add_dependency 'thor', '~> 0.20'
17
+ s.add_dependency 'multi_json', '~> 1.12'
18
+ s.add_dependency 'faraday', '~> 0.13'
19
+ s.add_dependency 'sidekiq', '~> 5.0'
20
+ s.add_dependency 'activesupport', '~> 5.1'
21
+
22
+ s.add_development_dependency 'rspec', '~> 3.6.0'
23
+ s.add_development_dependency 'rspec_junit_formatter', '~> 0.3.0'
24
+ s.add_development_dependency 'rack-test', '~> 0.7.0'
25
+
26
+ s.files = `git ls-files`.split("\n")
27
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
28
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
29
+ s.require_paths = ['lib']
30
+ end