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