xip 0.0.1 → 2.0.0.beta2
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 +4 -4
- data/.circleci/config.yml +116 -0
- data/.gitignore +12 -0
- data/CHANGELOG.md +135 -0
- data/Gemfile +4 -1
- data/Gemfile.lock +65 -15
- data/LICENSE +6 -4
- data/README.md +51 -1
- data/VERSION +1 -0
- data/bin/xip +3 -11
- data/lib/xip.rb +1 -3
- data/lib/xip/base.rb +189 -0
- data/lib/xip/cli.rb +273 -0
- data/lib/xip/cli_base.rb +24 -0
- data/lib/xip/commands/command.rb +13 -0
- data/lib/xip/commands/console.rb +74 -0
- data/lib/xip/commands/server.rb +63 -0
- data/lib/xip/configuration.rb +56 -0
- data/lib/xip/controller/callbacks.rb +63 -0
- data/lib/xip/controller/catch_all.rb +84 -0
- data/lib/xip/controller/controller.rb +274 -0
- data/lib/xip/controller/dev_jumps.rb +40 -0
- data/lib/xip/controller/dynamic_delay.rb +61 -0
- data/lib/xip/controller/helpers.rb +128 -0
- data/lib/xip/controller/interrupt_detect.rb +99 -0
- data/lib/xip/controller/messages.rb +283 -0
- data/lib/xip/controller/nlp.rb +49 -0
- data/lib/xip/controller/replies.rb +281 -0
- data/lib/xip/controller/unrecognized_message.rb +61 -0
- data/lib/xip/core_ext.rb +5 -0
- data/lib/xip/core_ext/numeric.rb +10 -0
- data/lib/xip/core_ext/string.rb +18 -0
- data/lib/xip/dispatcher.rb +68 -0
- data/lib/xip/errors.rb +55 -0
- data/lib/xip/flow/base.rb +69 -0
- data/lib/xip/flow/specification.rb +56 -0
- data/lib/xip/flow/state.rb +82 -0
- data/lib/xip/generators/builder.rb +41 -0
- data/lib/xip/generators/builder/.gitignore +30 -0
- data/lib/xip/generators/builder/Gemfile +19 -0
- data/lib/xip/generators/builder/Procfile.dev +2 -0
- data/lib/xip/generators/builder/README.md +9 -0
- data/lib/xip/generators/builder/Rakefile +2 -0
- data/lib/xip/generators/builder/bot/controllers/bot_controller.rb +55 -0
- data/lib/xip/generators/builder/bot/controllers/catch_alls_controller.rb +21 -0
- data/lib/xip/generators/builder/bot/controllers/concerns/.keep +0 -0
- data/lib/xip/generators/builder/bot/controllers/goodbyes_controller.rb +9 -0
- data/lib/xip/generators/builder/bot/controllers/hellos_controller.rb +9 -0
- data/lib/xip/generators/builder/bot/controllers/interrupts_controller.rb +9 -0
- data/lib/xip/generators/builder/bot/controllers/unrecognized_messages_controller.rb +9 -0
- data/lib/xip/generators/builder/bot/helpers/bot_helper.rb +2 -0
- data/lib/xip/generators/builder/bot/models/bot_record.rb +3 -0
- data/lib/xip/generators/builder/bot/models/concerns/.keep +0 -0
- data/lib/xip/generators/builder/bot/replies/catch_alls/level1.yml +2 -0
- data/lib/xip/generators/builder/bot/replies/goodbyes/say_goodbye.yml +2 -0
- data/lib/xip/generators/builder/bot/replies/hellos/say_hello.yml +2 -0
- data/lib/xip/generators/builder/config.ru +4 -0
- data/lib/xip/generators/builder/config/boot.rb +6 -0
- data/lib/xip/generators/builder/config/database.yml +25 -0
- data/lib/xip/generators/builder/config/environment.rb +2 -0
- data/lib/xip/generators/builder/config/flow_map.rb +25 -0
- data/lib/xip/generators/builder/config/initializers/autoload.rb +8 -0
- data/lib/xip/generators/builder/config/initializers/inflections.rb +16 -0
- data/lib/xip/generators/builder/config/puma.rb +25 -0
- data/lib/xip/generators/builder/config/services.yml +35 -0
- data/lib/xip/generators/builder/config/sidekiq.yml +3 -0
- data/lib/xip/generators/builder/db/seeds.rb +7 -0
- data/lib/xip/generators/generate.rb +39 -0
- data/lib/xip/generators/generate/flow/controllers/controller.tt +7 -0
- data/lib/xip/generators/generate/flow/helpers/helper.tt +3 -0
- data/lib/xip/generators/generate/flow/replies/ask_example.tt +9 -0
- data/lib/xip/helpers/redis.rb +40 -0
- data/lib/xip/jobs.rb +9 -0
- data/lib/xip/lock.rb +82 -0
- data/lib/xip/logger.rb +9 -3
- data/lib/xip/migrations/configurator.rb +73 -0
- data/lib/xip/migrations/generators.rb +16 -0
- data/lib/xip/migrations/railtie_config.rb +14 -0
- data/lib/xip/migrations/tasks.rb +43 -0
- data/lib/xip/nlp/client.rb +21 -0
- data/lib/xip/nlp/result.rb +56 -0
- data/lib/xip/reloader.rb +89 -0
- data/lib/xip/reply.rb +36 -0
- data/lib/xip/scheduled_reply.rb +18 -0
- data/lib/xip/server.rb +63 -0
- data/lib/xip/service_message.rb +17 -0
- data/lib/xip/service_reply.rb +44 -0
- data/lib/xip/services/base_client.rb +24 -0
- data/lib/xip/services/base_message_handler.rb +27 -0
- data/lib/xip/services/base_reply_handler.rb +72 -0
- data/lib/xip/services/jobs/handle_message_job.rb +21 -0
- data/lib/xip/session.rb +203 -0
- data/lib/xip/version.rb +7 -1
- data/logo.svg +17 -0
- data/spec/configuration_spec.rb +93 -0
- data/spec/controller/callbacks_spec.rb +217 -0
- data/spec/controller/catch_all_spec.rb +154 -0
- data/spec/controller/controller_spec.rb +889 -0
- data/spec/controller/dynamic_delay_spec.rb +70 -0
- data/spec/controller/helpers_spec.rb +119 -0
- data/spec/controller/interrupt_detect_spec.rb +171 -0
- data/spec/controller/messages_spec.rb +744 -0
- data/spec/controller/nlp_spec.rb +93 -0
- data/spec/controller/replies_spec.rb +694 -0
- data/spec/controller/unrecognized_message_spec.rb +168 -0
- data/spec/dispatcher_spec.rb +79 -0
- data/spec/flow/flow_spec.rb +82 -0
- data/spec/flow/state_spec.rb +109 -0
- data/spec/helpers/redis_spec.rb +77 -0
- data/spec/lock_spec.rb +100 -0
- data/spec/nlp/client_spec.rb +23 -0
- data/spec/nlp/result_spec.rb +57 -0
- data/spec/replies/hello.yml.erb +15 -0
- data/spec/replies/messages/say_hola.yml+facebook.erb +6 -0
- data/spec/replies/messages/say_hola.yml+twilio.erb +6 -0
- data/spec/replies/messages/say_hola.yml.erb +6 -0
- data/spec/replies/messages/say_howdy_with_dynamic.yml +79 -0
- data/spec/replies/messages/say_msgs_without_breaks.yml +4 -0
- data/spec/replies/messages/say_offer.yml +6 -0
- data/spec/replies/messages/say_offer_with_dynamic.yml +6 -0
- data/spec/replies/messages/say_oi.yml.erb +15 -0
- data/spec/replies/messages/say_randomize_speech.yml +10 -0
- data/spec/replies/messages/say_randomize_text.yml +10 -0
- data/spec/replies/messages/say_yo.yml +6 -0
- data/spec/replies/messages/say_yo.yml+twitter +6 -0
- data/spec/replies/messages/sub1/sub2/say_nested.yml +10 -0
- data/spec/reply_spec.rb +61 -0
- data/spec/scheduled_reply_spec.rb +23 -0
- data/spec/service_reply_spec.rb +92 -0
- data/spec/session_spec.rb +366 -0
- data/spec/spec_helper.rb +22 -66
- data/spec/support/alternate_helpers/foo_helper.rb +5 -0
- data/spec/support/controllers/vaders_controller.rb +24 -0
- data/spec/support/helpers/fun/games_helper.rb +7 -0
- data/spec/support/helpers/fun/pdf_helper.rb +7 -0
- data/spec/support/helpers/standalone_helper.rb +5 -0
- data/spec/support/helpers_typo/users_helper.rb +2 -0
- data/spec/support/nlp_clients/dialogflow.rb +9 -0
- data/spec/support/nlp_clients/luis.rb +9 -0
- data/spec/support/nlp_results/luis_result.rb +163 -0
- data/spec/support/sample_messages.rb +66 -0
- data/spec/support/services.yml +31 -0
- data/spec/support/services_with_erb.yml +31 -0
- data/spec/version_spec.rb +16 -0
- data/xip.gemspec +25 -14
- metadata +320 -18
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Xip
|
|
4
|
+
class Controller
|
|
5
|
+
module Nlp
|
|
6
|
+
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
included do
|
|
10
|
+
# Memoized in order to prevent multiple requests to the NLP provider
|
|
11
|
+
def perform_nlp!
|
|
12
|
+
Xip::Logger.l(
|
|
13
|
+
topic: :nlp,
|
|
14
|
+
message: "User #{current_session_id} -> Performing NLP."
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
unless Xip.config.nlp_integration.present?
|
|
18
|
+
raise Xip::Errors::ConfigurationError, "An NLP integration has not yet been configured (Xip.config.nlp_integration)"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
@nlp_result ||= begin
|
|
22
|
+
nlp_client = nlp_client_klass.new
|
|
23
|
+
@nlp_result = @current_message.nlp_result = nlp_client.understand(
|
|
24
|
+
query: current_message.message
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
if Xip.config.log_all_nlp_results
|
|
28
|
+
Xip::Logger.l(
|
|
29
|
+
topic: :nlp,
|
|
30
|
+
message: "User #{current_session_id} -> NLP Result: #{@nlp_result.parsed_result.inspect}"
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
@nlp_result
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def nlp_client_klass
|
|
41
|
+
integration = Xip.config.nlp_integration.to_s.titlecase
|
|
42
|
+
klass = "Xip::Nlp::#{integration}::Client"
|
|
43
|
+
klass.classify.constantize
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Xip
|
|
4
|
+
class Controller
|
|
5
|
+
module Replies
|
|
6
|
+
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
included do
|
|
10
|
+
|
|
11
|
+
class_attribute :_preprocessors, default: [:erb]
|
|
12
|
+
class_attribute :_replies_path, default: [Xip.root, 'bot', 'replies']
|
|
13
|
+
|
|
14
|
+
def send_replies(custom_reply: nil, inline: nil)
|
|
15
|
+
service_reply = load_service_reply(
|
|
16
|
+
custom_reply: custom_reply,
|
|
17
|
+
inline: inline
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Determine if we start at the beginning or somewhere else
|
|
21
|
+
reply_range = calculate_reply_range
|
|
22
|
+
offset = reply_range.first
|
|
23
|
+
|
|
24
|
+
@previous_reply = nil
|
|
25
|
+
service_reply.replies.slice(reply_range).each_with_index do |reply, i|
|
|
26
|
+
# Updates the lock with the current position of the reply
|
|
27
|
+
lock_session!(
|
|
28
|
+
session_slug: current_session.get_session,
|
|
29
|
+
position: i + offset # Otherwise this won't account for explicit starting points
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
begin
|
|
33
|
+
send_reply(reply: reply)
|
|
34
|
+
rescue Xip::Errors::UserOptOut => e
|
|
35
|
+
user_opt_out_handler(msg: e.message)
|
|
36
|
+
return
|
|
37
|
+
rescue Xip::Errors::InvalidSessionID => e
|
|
38
|
+
invalid_session_id_handler(msg: e.message)
|
|
39
|
+
return
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
@previous_reply = reply
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
@progressed = :sent_replies
|
|
46
|
+
ensure
|
|
47
|
+
release_lock!
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def send_reply(reply:)
|
|
53
|
+
if !reply.delay? && Xip.config.auto_insert_delays
|
|
54
|
+
# if it's the first reply in the service_reply or the previous reply
|
|
55
|
+
# wasn't a custom delay, then insert a delay
|
|
56
|
+
if @previous_reply.blank? || !@previous_reply.delay?
|
|
57
|
+
send_reply(reply: Reply.dynamic_delay)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Support randomized replies for text and speech replies.
|
|
62
|
+
# We select one before handing the reply off to the driver.
|
|
63
|
+
if reply['text'].is_a?(Array)
|
|
64
|
+
reply['text'] = reply['text'].sample
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
handler = reply_handler.new(
|
|
68
|
+
recipient_id: current_message.sender_id,
|
|
69
|
+
reply: reply
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
translated_reply = handler.send(reply.reply_type)
|
|
73
|
+
client = service_client.new(reply: translated_reply)
|
|
74
|
+
client.transmit
|
|
75
|
+
|
|
76
|
+
log_reply(reply) if Xip.config.transcript_logging
|
|
77
|
+
|
|
78
|
+
# If this was a 'delay' type of reply, we insert the delay
|
|
79
|
+
if reply.delay?
|
|
80
|
+
insert_delay(duration: reply['duration'])
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def insert_delay(duration:)
|
|
85
|
+
begin
|
|
86
|
+
sleep_duration = if duration == 'dynamic'
|
|
87
|
+
dyn_duration = dynamic_delay(previous_reply: @previous_reply)
|
|
88
|
+
|
|
89
|
+
Xip.config.dynamic_delay_muliplier * dyn_duration
|
|
90
|
+
else
|
|
91
|
+
Float(duration)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
sleep(sleep_duration)
|
|
95
|
+
rescue ArgumentError, TypeError
|
|
96
|
+
raise(ArgumentError, 'Invalid duration specified. Duration must be a Numeric')
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def load_service_reply(custom_reply:, inline:)
|
|
101
|
+
if inline.present?
|
|
102
|
+
Xip::ServiceReply.new(
|
|
103
|
+
recipient_id: current_session_id,
|
|
104
|
+
yaml_reply: inline,
|
|
105
|
+
preprocessor: :none,
|
|
106
|
+
context: nil
|
|
107
|
+
)
|
|
108
|
+
else
|
|
109
|
+
yaml_reply, preprocessor = action_replies(custom_reply)
|
|
110
|
+
|
|
111
|
+
Xip::ServiceReply.new(
|
|
112
|
+
recipient_id: current_session_id,
|
|
113
|
+
yaml_reply: yaml_reply,
|
|
114
|
+
preprocessor: preprocessor,
|
|
115
|
+
context: binding
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def service_client
|
|
121
|
+
begin
|
|
122
|
+
Kernel.const_get("Xip::Services::#{current_service.classify}::Client")
|
|
123
|
+
rescue NameError
|
|
124
|
+
raise(Xip::Errors::ServiceNotRecognized, "The service '#{current_service}' was not recognized")
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def reply_handler
|
|
129
|
+
begin
|
|
130
|
+
Kernel.const_get("Xip::Services::#{current_service.classify}::ReplyHandler")
|
|
131
|
+
rescue NameError
|
|
132
|
+
raise(Xip::Errors::ServiceNotRecognized, "The service '#{current_service}' was not recognized")
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def replies_folder
|
|
137
|
+
current_session.flow_string.underscore.pluralize
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def reply_dir
|
|
141
|
+
[*self._replies_path, replies_folder]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def base_reply_filename
|
|
145
|
+
"#{current_session.state_string}.yml"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def reply_filenames(custom_reply_filename=nil)
|
|
149
|
+
reply_filename = if custom_reply_filename.present?
|
|
150
|
+
custom_reply_filename
|
|
151
|
+
else
|
|
152
|
+
base_reply_filename
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
service_filename = [reply_filename, current_service].join('+')
|
|
156
|
+
|
|
157
|
+
# Service-specific filenames take precedance (returned first)
|
|
158
|
+
[service_filename, reply_filename]
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def find_reply_and_preprocessor(custom_reply)
|
|
162
|
+
selected_preprocessor = :none
|
|
163
|
+
|
|
164
|
+
if custom_reply.present?
|
|
165
|
+
dir_and_file = custom_reply.rpartition(File::SEPARATOR)
|
|
166
|
+
_dir = dir_and_file.first
|
|
167
|
+
_file = "#{dir_and_file.last}.yml"
|
|
168
|
+
_replies_dir = [*self._replies_path, _dir]
|
|
169
|
+
possible_filenames = reply_filenames(_file)
|
|
170
|
+
reply_file_path = File.join(_replies_dir, _file)
|
|
171
|
+
service_reply_path = File.join(_replies_dir, reply_filenames(_file).first)
|
|
172
|
+
else
|
|
173
|
+
_replies_dir = *reply_dir
|
|
174
|
+
possible_filenames = reply_filenames
|
|
175
|
+
reply_file_path = File.join(_replies_dir, base_reply_filename)
|
|
176
|
+
service_reply_path = File.join(_replies_dir, reply_filenames.first)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Check if the service_filename exists
|
|
180
|
+
# If so, we can skip checking for a preprocessor
|
|
181
|
+
if File.exist?(service_reply_path)
|
|
182
|
+
return service_reply_path, selected_preprocessor
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Cycles through possible preprocessor and variant combinations
|
|
186
|
+
# Early returns for performance
|
|
187
|
+
for preprocessor in self.class._preprocessors do
|
|
188
|
+
for reply_filename in possible_filenames do
|
|
189
|
+
selected_filepath = File.join(_replies_dir, [reply_filename, preprocessor.to_s].join('.'))
|
|
190
|
+
if File.exist?(selected_filepath)
|
|
191
|
+
reply_file_path = selected_filepath
|
|
192
|
+
selected_preprocessor = preprocessor
|
|
193
|
+
return reply_file_path, selected_preprocessor
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
return reply_file_path, selected_preprocessor
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def action_replies(custom_reply=nil)
|
|
202
|
+
reply_path, selected_preprocessor = find_reply_and_preprocessor(custom_reply)
|
|
203
|
+
|
|
204
|
+
begin
|
|
205
|
+
file_contents = File.read(reply_path)
|
|
206
|
+
rescue Errno::ENOENT
|
|
207
|
+
raise(Xip::Errors::ReplyNotFound, "Could not find reply: '#{reply_path}'")
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
return file_contents, selected_preprocessor
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def user_opt_out_handler(msg:)
|
|
214
|
+
if self.respond_to?(:handle_opt_out, true)
|
|
215
|
+
self.send(:handle_opt_out)
|
|
216
|
+
Xip::Logger.l(
|
|
217
|
+
topic: current_service,
|
|
218
|
+
message: "User #{current_session_id} opted out. [#{msg}]"
|
|
219
|
+
)
|
|
220
|
+
else
|
|
221
|
+
Xip::Logger.l(
|
|
222
|
+
topic: :err,
|
|
223
|
+
message: "User #{current_session_id} unhandled exception due to opt-out."
|
|
224
|
+
)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
do_nothing
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def invalid_session_id_handler(msg:)
|
|
231
|
+
if self.respond_to?(:handle_invalid_session_id, true)
|
|
232
|
+
self.send(:handle_invalid_session_id)
|
|
233
|
+
Xip::Logger.l(
|
|
234
|
+
topic: current_service,
|
|
235
|
+
message: "User #{current_session_id} has an invalid session_id. [#{msg}]"
|
|
236
|
+
)
|
|
237
|
+
else
|
|
238
|
+
Xip::Logger.l(
|
|
239
|
+
topic: :err,
|
|
240
|
+
message: "User #{current_session_id} unhandled exception due to invalid session_id."
|
|
241
|
+
)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
do_nothing
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def calculate_reply_range
|
|
248
|
+
# if an explicit starting point is specified, use that until the
|
|
249
|
+
# end of the range, otherwise start at the beginning
|
|
250
|
+
if @pos.present?
|
|
251
|
+
(@pos..-1)
|
|
252
|
+
else
|
|
253
|
+
(0..-1)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def log_reply(reply)
|
|
258
|
+
message = case reply.reply_type
|
|
259
|
+
when 'text'
|
|
260
|
+
reply['text']
|
|
261
|
+
when 'speech'
|
|
262
|
+
reply['speech']
|
|
263
|
+
when 'ssml'
|
|
264
|
+
reply['ssml']
|
|
265
|
+
when 'delay'
|
|
266
|
+
'<typing indicator>'
|
|
267
|
+
else
|
|
268
|
+
"<#{reply.reply_type}>"
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
Xip::Logger.l(
|
|
272
|
+
topic: current_service,
|
|
273
|
+
message: "User #{current_session_id} -> Sending: #{message}"
|
|
274
|
+
)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
end # instance methods
|
|
278
|
+
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Xip
|
|
4
|
+
class Controller
|
|
5
|
+
module UnrecognizedMessage
|
|
6
|
+
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
included do
|
|
10
|
+
|
|
11
|
+
def run_unrecognized_message(err:)
|
|
12
|
+
err_message = "The message \"#{current_message.message}\" was not recognized in the original context."
|
|
13
|
+
|
|
14
|
+
Xip::Logger.l(
|
|
15
|
+
topic: 'unrecognized_message',
|
|
16
|
+
message: err_message
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
unless defined?(UnrecognizedMessagesController)
|
|
20
|
+
Xip::Logger.l(
|
|
21
|
+
topic: 'unrecognized_message',
|
|
22
|
+
message: 'Running catch_all; UnrecognizedMessagesController not defined.'
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
run_catch_all(err: err)
|
|
26
|
+
return false
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
unrecognized_msg_controller = UnrecognizedMessagesController.new(
|
|
30
|
+
service_message: current_message
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
begin
|
|
34
|
+
# Run handle_unrecognized_message action
|
|
35
|
+
unrecognized_msg_controller.handle_unrecognized_message
|
|
36
|
+
|
|
37
|
+
if unrecognized_msg_controller.progressed?
|
|
38
|
+
Xip::Logger.l(
|
|
39
|
+
topic: 'unrecognized_message',
|
|
40
|
+
message: 'A match was detected. Skipping catch-all.'
|
|
41
|
+
)
|
|
42
|
+
else
|
|
43
|
+
# Log, but we don't want to run the catch_all for a poorly
|
|
44
|
+
# coded UnrecognizedMessagesController
|
|
45
|
+
Xip::Logger.l(
|
|
46
|
+
topic: 'unrecognized_message',
|
|
47
|
+
message: 'Did not send replies, update session, or step'
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
rescue StandardError => e
|
|
51
|
+
# Run the catch_all directly since we're already in an unrecognized
|
|
52
|
+
# message state
|
|
53
|
+
run_catch_all(err: e)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
data/lib/xip/core_ext.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class String
|
|
4
|
+
|
|
5
|
+
EXCLUDED_CHARS = %w[" ' . , ! ? ( ) - _ ` ‘ ’ “ ”].freeze
|
|
6
|
+
EXCLUDED_CHARS_ESC = EXCLUDED_CHARS.map { |c| "\\#{c}" }
|
|
7
|
+
EXCLUDED_CHARS_RE = /#{EXCLUDED_CHARS_ESC.join('|')}/
|
|
8
|
+
|
|
9
|
+
# Removes blank padding and double+single quotes
|
|
10
|
+
def normalize
|
|
11
|
+
self.upcase.strip
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def without_punctuation
|
|
15
|
+
self.gsub(EXCLUDED_CHARS_RE, '')
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Xip
|
|
4
|
+
|
|
5
|
+
# Responsible for coordinating incoming messages
|
|
6
|
+
# 1. Receives incoming request params
|
|
7
|
+
# 2. Initializes respective service request handler
|
|
8
|
+
# 3. Processes params through service request handler (might be async)
|
|
9
|
+
# 4. Inits base XipController with state params returned from the service
|
|
10
|
+
# request handler
|
|
11
|
+
# 5. Returns an HTTP response to be returned to the requestor
|
|
12
|
+
class Dispatcher
|
|
13
|
+
|
|
14
|
+
attr_reader :service, :params, :headers, :message_handler
|
|
15
|
+
|
|
16
|
+
def initialize(service:, params:, headers:)
|
|
17
|
+
@service = service
|
|
18
|
+
@params = params
|
|
19
|
+
@headers = headers
|
|
20
|
+
@message_handler = message_handler_klass.new(
|
|
21
|
+
params: params,
|
|
22
|
+
headers: headers
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def coordinate
|
|
27
|
+
message_handler.coordinate
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def process
|
|
31
|
+
service_message = message_handler.process
|
|
32
|
+
|
|
33
|
+
if Xip.config.transcript_logging
|
|
34
|
+
log_incoming_message(service_message)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
bot_controller = BotController.new(service_message: service_message)
|
|
38
|
+
bot_controller.route
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def message_handler_klass
|
|
44
|
+
begin
|
|
45
|
+
Kernel.const_get("Xip::Services::#{service.classify}::MessageHandler")
|
|
46
|
+
rescue NameError
|
|
47
|
+
raise(Xip::Errors::ServiceNotRecognized, "The service '#{service}' was not recognized")
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def log_incoming_message(service_message)
|
|
52
|
+
message = if service_message.location.present?
|
|
53
|
+
"Received: <user shared location>"
|
|
54
|
+
elsif service_message.attachments.present?
|
|
55
|
+
"Received: <user sent attachment>"
|
|
56
|
+
elsif service_message.payload.present?
|
|
57
|
+
"Received Payload: #{service_message.payload}"
|
|
58
|
+
else
|
|
59
|
+
"Received Message: #{service_message.message}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
Xip::Logger.l(
|
|
63
|
+
topic: 'user',
|
|
64
|
+
message: "User #{service_message.sender_id} -> #{message}"
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|