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
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
|