xip 0.0.1 → 2.0.0.beta2
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/lib/xip/cli_base.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Xip
|
4
|
+
module CliBase
|
5
|
+
def define_commands(&blk)
|
6
|
+
class_eval(&blk) if block_given?
|
7
|
+
end
|
8
|
+
|
9
|
+
def banner(command, nspace = true, subcommand = false)
|
10
|
+
super(command, nspace, namespace != 'xip:cli')
|
11
|
+
end
|
12
|
+
|
13
|
+
def handle_argument_error(command, error, args, arity)
|
14
|
+
name = [(namespace == 'xip:cli' ? nil : namespace), command.name].compact.join(" ")
|
15
|
+
|
16
|
+
msg = "ERROR: \"#{basename} #{name}\" was called with "
|
17
|
+
msg << "no arguments" if args.empty?
|
18
|
+
msg << "arguments " << args.inspect unless args.empty?
|
19
|
+
msg << "\nUsage: #{banner(command).inspect}"
|
20
|
+
|
21
|
+
raise Thor::InvocationError, msg
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'xip/commands/command'
|
4
|
+
|
5
|
+
module Xip
|
6
|
+
module Commands
|
7
|
+
# REPL that supports different engines.
|
8
|
+
#
|
9
|
+
# It is run with:
|
10
|
+
#
|
11
|
+
# `bundle exec xip console`
|
12
|
+
class Console < Command
|
13
|
+
module CodeReloading
|
14
|
+
def reload!
|
15
|
+
puts 'Reloading...'
|
16
|
+
Kernel.exec "#{$PROGRAM_NAME} console"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Supported engines
|
21
|
+
ENGINES = {
|
22
|
+
'pry' => 'Pry',
|
23
|
+
'ripl' => 'Ripl',
|
24
|
+
'irb' => 'IRB'
|
25
|
+
}.freeze
|
26
|
+
|
27
|
+
DEFAULT_ENGINE = ['irb'].freeze
|
28
|
+
|
29
|
+
attr_reader :options
|
30
|
+
|
31
|
+
def initialize(options)
|
32
|
+
super(options)
|
33
|
+
|
34
|
+
@options = options
|
35
|
+
end
|
36
|
+
|
37
|
+
def start
|
38
|
+
prepare
|
39
|
+
engine.start
|
40
|
+
end
|
41
|
+
|
42
|
+
def engine
|
43
|
+
load_engine options.fetch(:engine) { engine_lookup }
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def prepare
|
49
|
+
# Clear out ARGV so Pry/IRB don't attempt to parse the rest
|
50
|
+
ARGV.shift until ARGV.empty?
|
51
|
+
|
52
|
+
# Add convenience methods to the main:Object binding
|
53
|
+
TOPLEVEL_BINDING.eval('self').__send__(:include, CodeReloading)
|
54
|
+
|
55
|
+
Xip.boot
|
56
|
+
end
|
57
|
+
|
58
|
+
def engine_lookup
|
59
|
+
(ENGINES.find { |_, klass| Object.const_defined?(klass) } || DEFAULT_ENGINE).first
|
60
|
+
end
|
61
|
+
|
62
|
+
def load_engine(engine)
|
63
|
+
require engine
|
64
|
+
rescue LoadError
|
65
|
+
ensure
|
66
|
+
return Object.const_get(
|
67
|
+
ENGINES.fetch(engine) do
|
68
|
+
raise ArgumentError.new("Unknown console engine: `#{engine}'")
|
69
|
+
end
|
70
|
+
)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rack/handler/puma'
|
4
|
+
require 'xip/commands/command'
|
5
|
+
|
6
|
+
module Xip
|
7
|
+
module Commands
|
8
|
+
class Server < Command
|
9
|
+
def initialize(port:)
|
10
|
+
@port = port
|
11
|
+
$stdout.sync = true
|
12
|
+
end
|
13
|
+
|
14
|
+
def start
|
15
|
+
# Rack::Handler::Puma.run(Xip::Server)
|
16
|
+
puts ascii_art
|
17
|
+
exec "foreman start -f Procfile.dev -p #{@port}"
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def ascii_art
|
23
|
+
<<~ART
|
24
|
+
\e[36m,,,,,,,,,,,\e[0m \e[32m,,,,,,..,,,,,\e[0m
|
25
|
+
\e[36m░░░░░░░░░░░░░░≥\e[0m \e[32m╔╬╠╠╠╠╠╠╠╠╠╠╠╠╠⌐\e[0m
|
26
|
+
\e[36m░░░░░░░░░░░░░░░░≥,\e[0m \e[32mé╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠⌐\e[0m
|
27
|
+
\e[36m░░░░░░░░░░░░░░░░░░░,\e[0m \e[32m,@╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠⌐\e[0m
|
28
|
+
\e[36m░░░░░░░░░░░░░░░░░░░░▒≥\e[0m \e[32m╓╬╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠⌐\e[0m
|
29
|
+
\e[36m░░░░░░░░░░░░░░░░░░░░░░░≥\e[0m \e[32m╓╬╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠⌐\e[0m
|
30
|
+
\e[36m░░░░░░░░░░░░░░░░░░░░░░░░░≥\e[0m \e[32m╔╬╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠⌐\e[0m
|
31
|
+
\e[36m`╚░░░░░░░░░░░░░░░░░░░░░░░░≥,\e[0m \e[32mé╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╜\e[0m
|
32
|
+
\e[36m`╙░░░░░░░░░░░░░░░░░░░░░░░░\e[0m \e[32m╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╙\e[0m
|
33
|
+
\e[36m%░░░░░░░░░░░░░░░░░░░░░░\e[0m \e[32m╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╬"\e[0m
|
34
|
+
\e[36m"░░░░░░░░░░░░░░░░░░░░\e[0m \e[32m╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╩\e[0m
|
35
|
+
\e[36m"░░░░░░░░░░░░░░░░░░\e[0m \e[32m╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╩²\e[0m
|
36
|
+
\e[36m`╚░░░░░░░░░░░░░░░\e[0m \e[32m╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╜\e[0m
|
37
|
+
\e[36m`╙░░░░░░░░░░░░░\e[0m \e[32m╠╠╠╠╠╠╠╠╠╠╠╠╠╠╙\e[0m
|
38
|
+
|
39
|
+
|
40
|
+
\e[32m╔φφφφφφφφφφφφφ\e[0m \e[36m≥≥≥≥≥≥≥≥≥≥≥≥≥≥\e[0m
|
41
|
+
\e[32m╔╬╠╠╠╠╠╠╠╠╠╠╠╠╠╠\e[0m \e[36m░░░░░░░░░░░░░░░≥\e[0m
|
42
|
+
\e[32m,#╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠\e[0m \e[36m░░░░░░░░░░░░░░░░░≥\e[0m
|
43
|
+
\e[32m,ê╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠\e[0m \e[36m░░░░░░░░░░░░░░░░░░░≥,\e[0m
|
44
|
+
\e[32m²╬╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠\e[0m \e[36m░░░░░░░░░░░░░░░░░░░░░░,\e[0m
|
45
|
+
\e[32m╔╬╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠\e[0m \e[36m░░░░░░░░░░░░░░░░░░░░░░░▒≥\e[0m
|
46
|
+
\e[32m╔╬╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╩\e[0m \e[36m"░░░░░░░░░░░░░░░░░░░░░░░░░≥\e[0m
|
47
|
+
\e[32m#╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╜ \e[0m \e[36m"░░░░░░░░░░░░░░░░░░░░░░░░░≥\e[0m
|
48
|
+
\e[32m╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╙ \e[0m \e[36m`≥░░░░░░░░░░░░░░░░░░░░░░░\e[0m
|
49
|
+
\e[32m╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╩" \e[0m \e[36m░░░░░░░░░░░░░░░░░░░░░░\e[0m
|
50
|
+
\e[32m╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╩ \e[0m \e[36m²░░░░░░░░░░░░░░░░░░░\e[0m
|
51
|
+
\e[32m╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╩ \e[0m \e[36m"░░░░░░░░░░░░░░░░░\e[0m
|
52
|
+
\e[32m╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╜ \e[0m \e[36m"░░░░░░░░░░░░░░░\e[0m
|
53
|
+
\e[32m╚╚╚╚╚╩╩╩╩╩╩╩╩╙ \e[0m \e[36m`============²\e[0m
|
54
|
+
|
55
|
+
|
56
|
+
|
57
|
+
Xip v#{Xip::VERSION}
|
58
|
+
|
59
|
+
ART
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Xip
|
4
|
+
class Configuration < Hash
|
5
|
+
|
6
|
+
def initialize(hash)
|
7
|
+
hash.each do |k, v|
|
8
|
+
self[k] = store(v)
|
9
|
+
end
|
10
|
+
|
11
|
+
self
|
12
|
+
end
|
13
|
+
|
14
|
+
def method_missing(method, *args)
|
15
|
+
key = create_config_attribute(method)
|
16
|
+
|
17
|
+
if setter?(method)
|
18
|
+
self[key] = args.first
|
19
|
+
else
|
20
|
+
self[key]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def set_default(key, default_value)
|
25
|
+
if self[key.to_s] == nil
|
26
|
+
self[key.to_s] = store(default_value)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def store(value)
|
33
|
+
if value.is_a?(Hash)
|
34
|
+
Xip::Configuration.new(value)
|
35
|
+
else
|
36
|
+
value
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def setter?(method)
|
41
|
+
method.slice(-1, 1) == "="
|
42
|
+
end
|
43
|
+
|
44
|
+
def create_config_attribute(method)
|
45
|
+
key = basic_config_attribute_from_method(method)
|
46
|
+
|
47
|
+
key?(key.to_s) ? key.to_s : key
|
48
|
+
end
|
49
|
+
|
50
|
+
def basic_config_attribute_from_method(method)
|
51
|
+
setter?(method) ? method.to_s.chop : method
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Xip
|
4
|
+
class Controller
|
5
|
+
module Callbacks
|
6
|
+
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
include ActiveSupport::Callbacks
|
10
|
+
|
11
|
+
included do
|
12
|
+
define_callbacks :action, skip_after_callbacks_if_terminated: true
|
13
|
+
end
|
14
|
+
|
15
|
+
class_methods do
|
16
|
+
def _normalize_callback_options(options)
|
17
|
+
_normalize_callback_option(options, :only, :if)
|
18
|
+
_normalize_callback_option(options, :except, :unless)
|
19
|
+
end
|
20
|
+
|
21
|
+
def _normalize_callback_option(options, from, to)
|
22
|
+
if from = options[from]
|
23
|
+
_from = Array(from).map(&:to_s).to_set
|
24
|
+
from = proc { |c| _from.include?(c.action_name) }
|
25
|
+
options[to] = Array(options[to]).unshift(from)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def _insert_callbacks(callbacks, block = nil)
|
30
|
+
options = callbacks.extract_options!
|
31
|
+
_normalize_callback_options(options)
|
32
|
+
callbacks.push(block) if block
|
33
|
+
callbacks.each do |callback|
|
34
|
+
yield callback, options
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
[:before, :after, :around].each do |callback|
|
39
|
+
define_method "#{callback}_action" do |*names, &blk|
|
40
|
+
_insert_callbacks(names, blk) do |name, options|
|
41
|
+
set_callback(:action, callback, name, options)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
define_method "prepend_#{callback}_action" do |*names, &blk|
|
46
|
+
_insert_callbacks(names, blk) do |name, options|
|
47
|
+
set_callback(:action, callback, name, options.merge(prepend: true))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
define_method "skip_#{callback}_action" do |*names|
|
52
|
+
_insert_callbacks(names) do |name, options|
|
53
|
+
skip_callback(:action, callback, name, options)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
alias_method :"append_#{callback}_action", :"#{callback}_action"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Xip
|
4
|
+
class Controller
|
5
|
+
module CatchAll
|
6
|
+
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
|
11
|
+
def run_catch_all(err:)
|
12
|
+
error_level = fetch_error_level
|
13
|
+
|
14
|
+
if err.class == Xip::Errors::UnrecognizedMessage
|
15
|
+
Xip::Logger.l(
|
16
|
+
topic: 'catch_all',
|
17
|
+
message: "[Level #{error_level}] for user #{current_session_id} #{err.message}"
|
18
|
+
)
|
19
|
+
else
|
20
|
+
Xip::Logger.l(
|
21
|
+
topic: 'catch_all',
|
22
|
+
message: "[Level #{error_level}] for user #{current_session_id} #{[err.class, err.message, err.backtrace.join("\n")].join("\n")}"
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Store the reason so it can be accessed by the CatchAllsController
|
27
|
+
current_message.catch_all_reason = {
|
28
|
+
err: err.class,
|
29
|
+
err_msg: err.message
|
30
|
+
}
|
31
|
+
|
32
|
+
# Don't run catch_all from the catch_all controller
|
33
|
+
if current_session.flow_string == 'catch_all'
|
34
|
+
Xip::Logger.l(topic: 'catch_all', message: "CatchAll triggered for user #{current_session_id} from within CatchAll; ignoring.")
|
35
|
+
return false
|
36
|
+
end
|
37
|
+
|
38
|
+
if defined?(CatchAllsController) && FlowMap.flow_spec[:catch_all].present?
|
39
|
+
catch_all_state = calculate_catch_all_state(error_level)
|
40
|
+
|
41
|
+
if FlowMap.flow_spec[:catch_all].states.keys.include?(catch_all_state.to_sym)
|
42
|
+
step_to flow: :catch_all, state: catch_all_state
|
43
|
+
else
|
44
|
+
# We are out of bounds, do nothing to prevent an infinite loop
|
45
|
+
Xip::Logger.l(topic: 'catch_all', message: "Stopping; we\'ve exceeded the number of defined catch_all states for user #{current_session_id}.")
|
46
|
+
return false
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def fetch_error_level
|
54
|
+
if fail_attempts = $redis.get(error_slug)
|
55
|
+
begin
|
56
|
+
fail_attempts = Integer(fail_attempts)
|
57
|
+
rescue ArgumentError
|
58
|
+
fail_attempts = 1
|
59
|
+
end
|
60
|
+
|
61
|
+
fail_attempts += 1
|
62
|
+
else
|
63
|
+
fail_attempts = 1
|
64
|
+
end
|
65
|
+
|
66
|
+
# Set the error with an expiration to avoid filling Redis
|
67
|
+
$redis.setex(error_slug, 15.minutes.to_i, fail_attempts)
|
68
|
+
|
69
|
+
fail_attempts
|
70
|
+
end
|
71
|
+
|
72
|
+
def error_slug
|
73
|
+
['error', current_session_id, current_session.flow_string, current_session.state_string].join('-')
|
74
|
+
end
|
75
|
+
|
76
|
+
def calculate_catch_all_state(error_level)
|
77
|
+
"level#{error_level}"
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,274 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Xip
|
4
|
+
class Controller
|
5
|
+
|
6
|
+
include Xip::Controller::Callbacks
|
7
|
+
include Xip::Controller::DynamicDelay
|
8
|
+
include Xip::Controller::Replies
|
9
|
+
include Xip::Controller::Messages
|
10
|
+
include Xip::Controller::UnrecognizedMessage
|
11
|
+
include Xip::Controller::CatchAll
|
12
|
+
include Xip::Controller::Helpers
|
13
|
+
include Xip::Controller::InterruptDetect
|
14
|
+
include Xip::Controller::DevJumps
|
15
|
+
include Xip::Controller::Nlp
|
16
|
+
|
17
|
+
attr_reader :current_message, :current_service, :flow_controller,
|
18
|
+
:action_name, :current_session_id
|
19
|
+
attr_accessor :nlp_result, :pos
|
20
|
+
|
21
|
+
def initialize(service_message:, pos: nil)
|
22
|
+
@current_message = service_message
|
23
|
+
@current_service = service_message.service
|
24
|
+
@current_session_id = service_message.sender_id
|
25
|
+
@nlp_result = service_message.nlp_result
|
26
|
+
@pos = pos
|
27
|
+
@progressed = false
|
28
|
+
end
|
29
|
+
|
30
|
+
def has_location?
|
31
|
+
current_message.location.present?
|
32
|
+
end
|
33
|
+
|
34
|
+
def has_attachments?
|
35
|
+
current_message.attachments.present?
|
36
|
+
end
|
37
|
+
|
38
|
+
def progressed?
|
39
|
+
@progressed
|
40
|
+
end
|
41
|
+
|
42
|
+
def route
|
43
|
+
raise(Xip::Errors::ControllerRoutingNotImplemented, "Please implement `route` method in BotController")
|
44
|
+
end
|
45
|
+
|
46
|
+
def flow_controller
|
47
|
+
@flow_controller ||= begin
|
48
|
+
flow_controller = [current_session.flow_string.pluralize, 'controller'].join('_').classify.constantize
|
49
|
+
flow_controller.new(service_message: @current_message, pos: @pos)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def current_session
|
54
|
+
@current_session ||= Xip::Session.new(id: current_session_id)
|
55
|
+
end
|
56
|
+
|
57
|
+
def previous_session
|
58
|
+
@previous_session ||= Xip::Session.new(
|
59
|
+
id: current_session_id,
|
60
|
+
type: :previous
|
61
|
+
)
|
62
|
+
end
|
63
|
+
|
64
|
+
def action(action: nil)
|
65
|
+
begin
|
66
|
+
# Grab a mutual exclusion lock on the session
|
67
|
+
lock_session!(
|
68
|
+
session_slug: Session.slugify(
|
69
|
+
flow: current_session.flow_string,
|
70
|
+
state: current_session.state_string
|
71
|
+
)
|
72
|
+
)
|
73
|
+
|
74
|
+
@action_name = action
|
75
|
+
@action_name ||= current_session.state_string
|
76
|
+
|
77
|
+
# Check if the user needs to be redirected
|
78
|
+
if current_session.flow.current_state.redirects_to.present?
|
79
|
+
Xip::Logger.l(
|
80
|
+
topic: "redirect",
|
81
|
+
message: "From #{current_session.session} to #{current_session.flow.current_state.redirects_to.session}"
|
82
|
+
)
|
83
|
+
step_to(session: current_session.flow.current_state.redirects_to, pos: @pos)
|
84
|
+
return
|
85
|
+
end
|
86
|
+
|
87
|
+
run_callbacks :action do
|
88
|
+
begin
|
89
|
+
flow_controller.send(@action_name)
|
90
|
+
unless flow_controller.progressed?
|
91
|
+
run_catch_all(reason: 'Did not send replies, update session, or step')
|
92
|
+
end
|
93
|
+
rescue StandardError => e
|
94
|
+
if e.class == Xip::Errors::UnrecognizedMessage
|
95
|
+
run_unrecognized_message(err: e)
|
96
|
+
else
|
97
|
+
run_catch_all(err: e)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
ensure
|
102
|
+
# Release mutual exclusion lock on the session
|
103
|
+
release_lock!
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def step_to_in(delay, session: nil, flow: nil, state: nil, slug: nil)
|
108
|
+
if interrupt_detected?
|
109
|
+
run_interrupt_action
|
110
|
+
return :interrupted
|
111
|
+
end
|
112
|
+
|
113
|
+
flow, state = get_flow_and_state(
|
114
|
+
session: session,
|
115
|
+
flow: flow,
|
116
|
+
state: state,
|
117
|
+
slug: slug
|
118
|
+
)
|
119
|
+
|
120
|
+
unless delay.is_a?(ActiveSupport::Duration)
|
121
|
+
raise ArgumentError, "Please specify your step_to_in `delay` parameter using ActiveSupport::Duration, e.g. `1.day` or `5.hours`"
|
122
|
+
end
|
123
|
+
|
124
|
+
Xip::ScheduledReplyJob.perform_in(delay, current_service, current_session_id, flow, state, current_message.target_id)
|
125
|
+
Xip::Logger.l(topic: "session", message: "User #{current_session_id}: scheduled session step to #{flow}->#{state} in #{delay} seconds")
|
126
|
+
end
|
127
|
+
|
128
|
+
def step_to_at(timestamp, session: nil, flow: nil, state: nil, slug: nil)
|
129
|
+
if interrupt_detected?
|
130
|
+
run_interrupt_action
|
131
|
+
return :interrupted
|
132
|
+
end
|
133
|
+
|
134
|
+
flow, state = get_flow_and_state(
|
135
|
+
session: session,
|
136
|
+
flow: flow,
|
137
|
+
state: state,
|
138
|
+
slug: slug
|
139
|
+
)
|
140
|
+
|
141
|
+
unless timestamp.is_a?(DateTime)
|
142
|
+
raise ArgumentError, "Please specify your step_to_at `timestamp` parameter as a DateTime"
|
143
|
+
end
|
144
|
+
|
145
|
+
Xip::ScheduledReplyJob.perform_at(timestamp, current_service, current_session_id, flow, state, current_message.target_id)
|
146
|
+
Xip::Logger.l(topic: "session", message: "User #{current_session_id}: scheduled session step to #{flow}->#{state} at #{timestamp.iso8601}")
|
147
|
+
end
|
148
|
+
|
149
|
+
def step_to(session: nil, flow: nil, state: nil, slug: nil, pos: nil)
|
150
|
+
if interrupt_detected?
|
151
|
+
run_interrupt_action
|
152
|
+
return :interrupted
|
153
|
+
end
|
154
|
+
|
155
|
+
flow, state = get_flow_and_state(
|
156
|
+
session: session,
|
157
|
+
flow: flow,
|
158
|
+
state: state,
|
159
|
+
slug: slug
|
160
|
+
)
|
161
|
+
|
162
|
+
step(flow: flow, state: state, pos: pos)
|
163
|
+
end
|
164
|
+
|
165
|
+
def update_session_to(session: nil, flow: nil, state: nil, slug: nil)
|
166
|
+
if interrupt_detected?
|
167
|
+
run_interrupt_action
|
168
|
+
return :interrupted
|
169
|
+
end
|
170
|
+
|
171
|
+
flow, state = get_flow_and_state(
|
172
|
+
session: session,
|
173
|
+
flow: flow,
|
174
|
+
state: state,
|
175
|
+
slug: slug
|
176
|
+
)
|
177
|
+
|
178
|
+
update_session(flow: flow, state: state)
|
179
|
+
end
|
180
|
+
|
181
|
+
def set_back_to(session: nil, flow: nil, state: nil, slug: nil)
|
182
|
+
if interrupt_detected?
|
183
|
+
run_interrupt_action
|
184
|
+
return :interrupted
|
185
|
+
end
|
186
|
+
|
187
|
+
flow, state = get_flow_and_state(
|
188
|
+
session: session,
|
189
|
+
flow: flow,
|
190
|
+
state: state,
|
191
|
+
slug: slug
|
192
|
+
)
|
193
|
+
|
194
|
+
store_back_to_session(flow: flow, state: state)
|
195
|
+
end
|
196
|
+
|
197
|
+
def step_back
|
198
|
+
back_to_session = Xip::Session.new(
|
199
|
+
id: current_session_id,
|
200
|
+
type: :back_to
|
201
|
+
)
|
202
|
+
|
203
|
+
if back_to_session.blank?
|
204
|
+
raise(
|
205
|
+
Xip::Errors::InvalidStateTransition,
|
206
|
+
'back_to_session not found; make sure set_back_to was called first'
|
207
|
+
)
|
208
|
+
end
|
209
|
+
|
210
|
+
step_to(session: back_to_session)
|
211
|
+
end
|
212
|
+
|
213
|
+
def do_nothing
|
214
|
+
@progressed = :do_nothing
|
215
|
+
end
|
216
|
+
|
217
|
+
private
|
218
|
+
|
219
|
+
def update_session(flow:, state:)
|
220
|
+
@progressed = :updated_session
|
221
|
+
@current_session = Session.new(id: current_session_id)
|
222
|
+
|
223
|
+
unless current_session.flow_string == flow.to_s && current_session.state_string == state.to_s
|
224
|
+
@current_session.set_session(new_flow: flow, new_state: state)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def store_back_to_session(flow:, state:)
|
229
|
+
back_to_session = Session.new(
|
230
|
+
id: current_session_id,
|
231
|
+
type: :back_to
|
232
|
+
)
|
233
|
+
back_to_session.set_session(new_flow: flow, new_state: state)
|
234
|
+
end
|
235
|
+
|
236
|
+
def step(flow:, state:, pos: nil)
|
237
|
+
update_session(flow: flow, state: state)
|
238
|
+
@progressed = :stepped
|
239
|
+
@flow_controller = nil
|
240
|
+
@current_flow = current_session.flow
|
241
|
+
@pos = pos
|
242
|
+
|
243
|
+
flow_controller.action(action: state)
|
244
|
+
end
|
245
|
+
|
246
|
+
def get_flow_and_state(session: nil, flow: nil, state: nil, slug: nil)
|
247
|
+
if session.nil? && flow.nil? && state.nil? && slug.nil?
|
248
|
+
raise(ArgumentError, "A session, flow, state, or slug must be specified")
|
249
|
+
end
|
250
|
+
|
251
|
+
if session.present?
|
252
|
+
return session.flow_string, session.state_string
|
253
|
+
end
|
254
|
+
|
255
|
+
if slug.present?
|
256
|
+
flow_state = Session.flow_and_state_from_session_slug(slug: slug)
|
257
|
+
return flow_state[:flow], flow_state[:state]
|
258
|
+
end
|
259
|
+
|
260
|
+
if flow.present?
|
261
|
+
if state.blank?
|
262
|
+
state = FlowMap.flow_spec[flow.to_sym].states.keys.first.to_s
|
263
|
+
end
|
264
|
+
|
265
|
+
return flow.to_s, state.to_s
|
266
|
+
end
|
267
|
+
|
268
|
+
if state.present?
|
269
|
+
return current_session.flow_string, state.to_s
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
end
|
274
|
+
end
|