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.
Files changed (146) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +116 -0
  3. data/.gitignore +12 -0
  4. data/CHANGELOG.md +135 -0
  5. data/Gemfile +4 -1
  6. data/Gemfile.lock +65 -15
  7. data/LICENSE +6 -4
  8. data/README.md +51 -1
  9. data/VERSION +1 -0
  10. data/bin/xip +3 -11
  11. data/lib/xip.rb +1 -3
  12. data/lib/xip/base.rb +189 -0
  13. data/lib/xip/cli.rb +273 -0
  14. data/lib/xip/cli_base.rb +24 -0
  15. data/lib/xip/commands/command.rb +13 -0
  16. data/lib/xip/commands/console.rb +74 -0
  17. data/lib/xip/commands/server.rb +63 -0
  18. data/lib/xip/configuration.rb +56 -0
  19. data/lib/xip/controller/callbacks.rb +63 -0
  20. data/lib/xip/controller/catch_all.rb +84 -0
  21. data/lib/xip/controller/controller.rb +274 -0
  22. data/lib/xip/controller/dev_jumps.rb +40 -0
  23. data/lib/xip/controller/dynamic_delay.rb +61 -0
  24. data/lib/xip/controller/helpers.rb +128 -0
  25. data/lib/xip/controller/interrupt_detect.rb +99 -0
  26. data/lib/xip/controller/messages.rb +283 -0
  27. data/lib/xip/controller/nlp.rb +49 -0
  28. data/lib/xip/controller/replies.rb +281 -0
  29. data/lib/xip/controller/unrecognized_message.rb +61 -0
  30. data/lib/xip/core_ext.rb +5 -0
  31. data/lib/xip/core_ext/numeric.rb +10 -0
  32. data/lib/xip/core_ext/string.rb +18 -0
  33. data/lib/xip/dispatcher.rb +68 -0
  34. data/lib/xip/errors.rb +55 -0
  35. data/lib/xip/flow/base.rb +69 -0
  36. data/lib/xip/flow/specification.rb +56 -0
  37. data/lib/xip/flow/state.rb +82 -0
  38. data/lib/xip/generators/builder.rb +41 -0
  39. data/lib/xip/generators/builder/.gitignore +30 -0
  40. data/lib/xip/generators/builder/Gemfile +19 -0
  41. data/lib/xip/generators/builder/Procfile.dev +2 -0
  42. data/lib/xip/generators/builder/README.md +9 -0
  43. data/lib/xip/generators/builder/Rakefile +2 -0
  44. data/lib/xip/generators/builder/bot/controllers/bot_controller.rb +55 -0
  45. data/lib/xip/generators/builder/bot/controllers/catch_alls_controller.rb +21 -0
  46. data/lib/xip/generators/builder/bot/controllers/concerns/.keep +0 -0
  47. data/lib/xip/generators/builder/bot/controllers/goodbyes_controller.rb +9 -0
  48. data/lib/xip/generators/builder/bot/controllers/hellos_controller.rb +9 -0
  49. data/lib/xip/generators/builder/bot/controllers/interrupts_controller.rb +9 -0
  50. data/lib/xip/generators/builder/bot/controllers/unrecognized_messages_controller.rb +9 -0
  51. data/lib/xip/generators/builder/bot/helpers/bot_helper.rb +2 -0
  52. data/lib/xip/generators/builder/bot/models/bot_record.rb +3 -0
  53. data/lib/xip/generators/builder/bot/models/concerns/.keep +0 -0
  54. data/lib/xip/generators/builder/bot/replies/catch_alls/level1.yml +2 -0
  55. data/lib/xip/generators/builder/bot/replies/goodbyes/say_goodbye.yml +2 -0
  56. data/lib/xip/generators/builder/bot/replies/hellos/say_hello.yml +2 -0
  57. data/lib/xip/generators/builder/config.ru +4 -0
  58. data/lib/xip/generators/builder/config/boot.rb +6 -0
  59. data/lib/xip/generators/builder/config/database.yml +25 -0
  60. data/lib/xip/generators/builder/config/environment.rb +2 -0
  61. data/lib/xip/generators/builder/config/flow_map.rb +25 -0
  62. data/lib/xip/generators/builder/config/initializers/autoload.rb +8 -0
  63. data/lib/xip/generators/builder/config/initializers/inflections.rb +16 -0
  64. data/lib/xip/generators/builder/config/puma.rb +25 -0
  65. data/lib/xip/generators/builder/config/services.yml +35 -0
  66. data/lib/xip/generators/builder/config/sidekiq.yml +3 -0
  67. data/lib/xip/generators/builder/db/seeds.rb +7 -0
  68. data/lib/xip/generators/generate.rb +39 -0
  69. data/lib/xip/generators/generate/flow/controllers/controller.tt +7 -0
  70. data/lib/xip/generators/generate/flow/helpers/helper.tt +3 -0
  71. data/lib/xip/generators/generate/flow/replies/ask_example.tt +9 -0
  72. data/lib/xip/helpers/redis.rb +40 -0
  73. data/lib/xip/jobs.rb +9 -0
  74. data/lib/xip/lock.rb +82 -0
  75. data/lib/xip/logger.rb +9 -3
  76. data/lib/xip/migrations/configurator.rb +73 -0
  77. data/lib/xip/migrations/generators.rb +16 -0
  78. data/lib/xip/migrations/railtie_config.rb +14 -0
  79. data/lib/xip/migrations/tasks.rb +43 -0
  80. data/lib/xip/nlp/client.rb +21 -0
  81. data/lib/xip/nlp/result.rb +56 -0
  82. data/lib/xip/reloader.rb +89 -0
  83. data/lib/xip/reply.rb +36 -0
  84. data/lib/xip/scheduled_reply.rb +18 -0
  85. data/lib/xip/server.rb +63 -0
  86. data/lib/xip/service_message.rb +17 -0
  87. data/lib/xip/service_reply.rb +44 -0
  88. data/lib/xip/services/base_client.rb +24 -0
  89. data/lib/xip/services/base_message_handler.rb +27 -0
  90. data/lib/xip/services/base_reply_handler.rb +72 -0
  91. data/lib/xip/services/jobs/handle_message_job.rb +21 -0
  92. data/lib/xip/session.rb +203 -0
  93. data/lib/xip/version.rb +7 -1
  94. data/logo.svg +17 -0
  95. data/spec/configuration_spec.rb +93 -0
  96. data/spec/controller/callbacks_spec.rb +217 -0
  97. data/spec/controller/catch_all_spec.rb +154 -0
  98. data/spec/controller/controller_spec.rb +889 -0
  99. data/spec/controller/dynamic_delay_spec.rb +70 -0
  100. data/spec/controller/helpers_spec.rb +119 -0
  101. data/spec/controller/interrupt_detect_spec.rb +171 -0
  102. data/spec/controller/messages_spec.rb +744 -0
  103. data/spec/controller/nlp_spec.rb +93 -0
  104. data/spec/controller/replies_spec.rb +694 -0
  105. data/spec/controller/unrecognized_message_spec.rb +168 -0
  106. data/spec/dispatcher_spec.rb +79 -0
  107. data/spec/flow/flow_spec.rb +82 -0
  108. data/spec/flow/state_spec.rb +109 -0
  109. data/spec/helpers/redis_spec.rb +77 -0
  110. data/spec/lock_spec.rb +100 -0
  111. data/spec/nlp/client_spec.rb +23 -0
  112. data/spec/nlp/result_spec.rb +57 -0
  113. data/spec/replies/hello.yml.erb +15 -0
  114. data/spec/replies/messages/say_hola.yml+facebook.erb +6 -0
  115. data/spec/replies/messages/say_hola.yml+twilio.erb +6 -0
  116. data/spec/replies/messages/say_hola.yml.erb +6 -0
  117. data/spec/replies/messages/say_howdy_with_dynamic.yml +79 -0
  118. data/spec/replies/messages/say_msgs_without_breaks.yml +4 -0
  119. data/spec/replies/messages/say_offer.yml +6 -0
  120. data/spec/replies/messages/say_offer_with_dynamic.yml +6 -0
  121. data/spec/replies/messages/say_oi.yml.erb +15 -0
  122. data/spec/replies/messages/say_randomize_speech.yml +10 -0
  123. data/spec/replies/messages/say_randomize_text.yml +10 -0
  124. data/spec/replies/messages/say_yo.yml +6 -0
  125. data/spec/replies/messages/say_yo.yml+twitter +6 -0
  126. data/spec/replies/messages/sub1/sub2/say_nested.yml +10 -0
  127. data/spec/reply_spec.rb +61 -0
  128. data/spec/scheduled_reply_spec.rb +23 -0
  129. data/spec/service_reply_spec.rb +92 -0
  130. data/spec/session_spec.rb +366 -0
  131. data/spec/spec_helper.rb +22 -66
  132. data/spec/support/alternate_helpers/foo_helper.rb +5 -0
  133. data/spec/support/controllers/vaders_controller.rb +24 -0
  134. data/spec/support/helpers/fun/games_helper.rb +7 -0
  135. data/spec/support/helpers/fun/pdf_helper.rb +7 -0
  136. data/spec/support/helpers/standalone_helper.rb +5 -0
  137. data/spec/support/helpers_typo/users_helper.rb +2 -0
  138. data/spec/support/nlp_clients/dialogflow.rb +9 -0
  139. data/spec/support/nlp_clients/luis.rb +9 -0
  140. data/spec/support/nlp_results/luis_result.rb +163 -0
  141. data/spec/support/sample_messages.rb +66 -0
  142. data/spec/support/services.yml +31 -0
  143. data/spec/support/services_with_erb.yml +31 -0
  144. data/spec/version_spec.rb +16 -0
  145. data/xip.gemspec +25 -14
  146. metadata +320 -18
@@ -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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'xip'
4
+
5
+ module Xip
6
+ module Commands
7
+ class Command
8
+ def initialize(options)
9
+
10
+ end
11
+ end
12
+ end
13
+ 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