diff --git a/.gitignore b/.gitignore index bd1af06b..94fa0e73 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ .yardoc doc/ *.gem +/test/dummy +.idea diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 00000000..a0891f56 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.3.4 diff --git a/Gemfile b/Gemfile index 4a505fce..e92c4775 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,12 @@ -source 'https://rubygems.org' +source "https://rubygems.org" +git_source(:github) { |repo| "https://github.com/#{repo}.git" } + +# Specify your gem's dependencies in stealth.gemspec. gemspec -platforms :mri do - gem 'oj', '~> 3.14' -end +gem "puma" + +gem "sprockets-rails" + +# Start debugger with binding.b [https://github.com/ruby/debug] +# gem "debug", ">= 1.0.0" diff --git a/Gemfile.lock b/Gemfile.lock index c223de60..9c9accdd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,87 +1,236 @@ PATH remote: . specs: - stealth (2.0.0.beta7) - activesupport (~> 7.0) - multi_json (~> 1.12) - puma (~> 6.0) + stealth (3.0.0.alpha1) + rails (>= 7.1.3.4) redis (~> 5.0) sidekiq (~> 7.0) - sinatra (>= 2, < 4) - thor (~> 1.0) - zeitwerk (~> 2.6) + spectre_ai (~> 1.1.2) GEM remote: https://rubygems.org/ specs: - activesupport (7.0.5) + actioncable (7.1.3.4) + actionpack (= 7.1.3.4) + activesupport (= 7.1.3.4) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (7.1.3.4) + actionpack (= 7.1.3.4) + activejob (= 7.1.3.4) + activerecord (= 7.1.3.4) + activestorage (= 7.1.3.4) + activesupport (= 7.1.3.4) + mail (>= 2.7.1) + net-imap + net-pop + net-smtp + actionmailer (7.1.3.4) + actionpack (= 7.1.3.4) + actionview (= 7.1.3.4) + activejob (= 7.1.3.4) + activesupport (= 7.1.3.4) + mail (~> 2.5, >= 2.5.4) + net-imap + net-pop + net-smtp + rails-dom-testing (~> 2.2) + actionpack (7.1.3.4) + actionview (= 7.1.3.4) + activesupport (= 7.1.3.4) + nokogiri (>= 1.8.5) + racc + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + actiontext (7.1.3.4) + actionpack (= 7.1.3.4) + activerecord (= 7.1.3.4) + activestorage (= 7.1.3.4) + activesupport (= 7.1.3.4) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (7.1.3.4) + activesupport (= 7.1.3.4) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (7.1.3.4) + activesupport (= 7.1.3.4) + globalid (>= 0.3.6) + activemodel (7.1.3.4) + activesupport (= 7.1.3.4) + activerecord (7.1.3.4) + activemodel (= 7.1.3.4) + activesupport (= 7.1.3.4) + timeout (>= 0.4.0) + activestorage (7.1.3.4) + actionpack (= 7.1.3.4) + activejob (= 7.1.3.4) + activerecord (= 7.1.3.4) + activesupport (= 7.1.3.4) + marcel (~> 1.0) + activesupport (7.1.3.4) + base64 + bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) minitest (>= 5.1) + mutex_m tzinfo (~> 2.0) - concurrent-ruby (1.2.2) + base64 (0.2.0) + bigdecimal (3.1.6) + builder (3.2.4) + concurrent-ruby (1.3.3) connection_pool (2.4.1) - diff-lcs (1.5.0) - i18n (1.13.0) - concurrent-ruby (~> 1.0) - minitest (5.18.0) - mock_redis (0.36.0) + crass (1.0.6) + date (3.3.4) + diff-lcs (1.5.1) + drb (2.2.0) ruby2_keywords - multi_json (1.15.0) - mustermann (3.0.0) - ruby2_keywords (~> 0.0.1) - nio4r (2.5.9) - oj (3.14.3) - puma (6.3.0) + erubi (1.12.0) + globalid (1.2.1) + activesupport (>= 6.1) + i18n (1.14.1) + concurrent-ruby (~> 1.0) + io-console (0.7.2) + irb (1.14.1) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + logger (1.6.1) + loofah (2.22.0) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.8.1) + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.0.4) + mini_mime (1.1.5) + minitest (5.24.0) + mock_redis (0.44.0) + mutex_m (0.2.0) + net-imap (0.4.16) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.5.0) + net-protocol + nio4r (2.7.0) + nokogiri (1.16.2-arm64-darwin) + racc (~> 1.4) + psych (5.1.2) + stringio + puma (6.4.2) nio4r (~> 2.0) - rack (2.2.7) - rack-protection (3.0.6) - rack + racc (1.8.0) + rack (3.1.4) + rack-session (2.0.0) + rack (>= 3.0.0) rack-test (2.1.0) rack (>= 1.3) - redis (5.0.6) - redis-client (>= 0.9.0) - redis-client (0.14.1) + rackup (2.1.0) + rack (>= 3) + webrick (~> 1.8) + rails (7.1.3.4) + actioncable (= 7.1.3.4) + actionmailbox (= 7.1.3.4) + actionmailer (= 7.1.3.4) + actionpack (= 7.1.3.4) + actiontext (= 7.1.3.4) + actionview (= 7.1.3.4) + activejob (= 7.1.3.4) + activemodel (= 7.1.3.4) + activerecord (= 7.1.3.4) + activestorage (= 7.1.3.4) + activesupport (= 7.1.3.4) + bundler (>= 1.15.0) + railties (= 7.1.3.4) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) + railties (7.1.3.4) + actionpack (= 7.1.3.4) + activesupport (= 7.1.3.4) + irb + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) + rake (13.2.1) + rdoc (6.7.0) + psych (>= 4.0.0) + redis (5.3.0) + redis-client (>= 0.22.0) + redis-client (0.22.2) connection_pool - rspec (3.12.0) - rspec-core (~> 3.12.0) - rspec-expectations (~> 3.12.0) - rspec-mocks (~> 3.12.0) - rspec-core (3.12.0) - rspec-support (~> 3.12.0) - rspec-expectations (3.12.0) + reline (0.5.10) + io-console (~> 0.5) + rspec (3.13.0) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.1) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.3) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-mocks (3.12.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-support (3.12.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.1) ruby2_keywords (0.0.5) - sidekiq (7.1.2) + sidekiq (7.3.2) concurrent-ruby (< 2) connection_pool (>= 2.3.0) + logger rack (>= 2.2.4) - redis-client (>= 0.14.0) - sinatra (3.0.6) - mustermann (~> 3.0) - rack (~> 2.2, >= 2.2.4) - rack-protection (= 3.0.6) - tilt (~> 2.0) - thor (1.2.2) - tilt (2.2.0) + redis-client (>= 0.22.2) + spectre_ai (1.1.2) + sprockets (4.2.1) + concurrent-ruby (~> 1.0) + rack (>= 2.2.4, < 4) + sprockets-rails (3.4.2) + actionpack (>= 5.2) + activesupport (>= 5.2) + sprockets (>= 3.0.0) + stringio (3.1.1) + thor (1.3.2) + timeout (0.4.1) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - zeitwerk (2.6.8) + webrick (1.8.2) + websocket-driver (0.7.6) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + zeitwerk (2.7.0) PLATFORMS - ruby + arm64-darwin-21 + arm64-darwin-22 + arm64-darwin-23 DEPENDENCIES mock_redis (~> 0.22) - oj (~> 3.14) + puma rack-test (~> 2.0) rspec (~> 3.9) + sprockets-rails stealth! BUNDLED WITH - 2.3.26 + 2.2.3 diff --git a/MIT-LICENSE b/MIT-LICENSE new file mode 100644 index 00000000..66142af9 --- /dev/null +++ b/MIT-LICENSE @@ -0,0 +1,20 @@ +Copyright Mav Automation Ventures, Inc + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 719bde41..0f452b8a 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,28 @@ -# Stealth Logo +# Stealth +Short description and motivation. -Stealth is a Ruby framework for creating text and voice chatbots. It's design is inspired by Ruby on Rails's philosophy of convention over configuration. It has an MVC architecture with the slight caveat that `views` are aptly named `replies`. +## Usage +How to use my plugin. -![CircleCI](https://img.shields.io/circleci/build/github/hellostealth/stealth?style=for-the-badge) -![Gem (including prereleases)](https://img.shields.io/gem/v/stealth?include_prereleases&style=for-the-badge) - -## Features - -* Deploy anywhere, it's just a Rack app -* Variants allow you to use a single codebase on multiple messaging platforms -* Structured, universal reply format -* Sessions utilize a state-machine concept and are Redis backed -* Highly scalable. Incoming webhooks are processed via a Sidekiq queue -* Built-in best practices: catch-alls (error handling), hello flows, goodbye flows - -## Getting Started - -Getting started with Stealth is simple: +## Installation +Add this line to your application's Gemfile: +```ruby +gem "stealth" ``` -> gem install stealth -> stealth new -``` - -## Service Integrations - -Stealth is extensible. All service integrations are split out into separate Ruby Gems. Things like analytics and natural language processing ([NLP](https://en.wikipedia.org/wiki/Natural-language_processing)) can be added in as gems as well. - -Currently, there are gems for: -### Messaging -* [Facebook Messenger](https://github.com/hellostealth/stealth-facebook) -* [Twilio SMS](https://github.com/hellostealth/stealth-twilio) -* [Bandwidth](https://github.com/hellostealth/stealth-bandwidth) -* [Smooch](https://github.com/hellostealth/stealth-smooch) - -### Voice -* [Alexa Skill](https://github.com/hellostealth/stealth-alexa) (Early alpha) - -### Natural Language Processing -* [Microsoft LUIS](https://github.com/hellostealth/stealth-luis) -* [AWS Comprehend](https://github.com/hellostealth/stealth-aws-comprehend) - -### Analytics -* [Mixpanel](https://github.com/hellostealth/stealth-mixpanel) - -## Docs - -You can find our full docs [here](https://github.com/hellostealth/stealth/wiki). If something is not clear in the docs, please file an issue! We consider all shortcomings in the docs as bugs. +And then execute: +```bash +$ bundle +``` -## Versioning +Or install it yourself as: +```bash +$ gem install stealth +``` -Stealth is versioned using [Semantic Versioning](https://semver.org), but it's more like the Linux Kernel. Major version releases are just as arbitrary as minor version releases. We strive to never break anything with any version change. Patches are still issues as the "third dot" in the version string. +## Contributing +Contribution directions go here. ## License - -"Stealth" and the Stealth logo are Copyright (c) 2017-2024 MAV Automated Ventures Inc. +The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). diff --git a/Rakefile b/Rakefile new file mode 100644 index 00000000..e7793b5c --- /dev/null +++ b/Rakefile @@ -0,0 +1,8 @@ +require "bundler/setup" + +APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__) +load "rails/tasks/engine.rake" + +load "rails/tasks/statistics.rake" + +require "bundler/gem_tasks" diff --git a/VERSION b/VERSION deleted file mode 100644 index d3dd3989..00000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -2.0.0.beta7 diff --git a/app/assets/config/stealth_manifest.js b/app/assets/config/stealth_manifest.js new file mode 100644 index 00000000..eba09c6f --- /dev/null +++ b/app/assets/config/stealth_manifest.js @@ -0,0 +1 @@ +//= link_directory ../stylesheets/stealth .css diff --git a/lib/stealth/generators/builder/bot/controllers/concerns/.keep b/app/assets/images/stealth/.keep similarity index 100% rename from lib/stealth/generators/builder/bot/controllers/concerns/.keep rename to app/assets/images/stealth/.keep diff --git a/app/assets/stylesheets/stealth/application.css b/app/assets/stylesheets/stealth/application.css new file mode 100644 index 00000000..0ebd7fe8 --- /dev/null +++ b/app/assets/stylesheets/stealth/application.css @@ -0,0 +1,15 @@ +/* + * This is a manifest file that'll be compiled into application.css, which will include all the files + * listed below. + * + * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, + * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. + * + * You're free to add application-wide styles to this file and they'll appear at the bottom of the + * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS + * files in this directory. Styles in this file should be added after the last require_* statement. + * It is generally better to create a new file per style scope. + * + *= require_tree . + *= require_self + */ diff --git a/lib/stealth/generators/builder/bot/models/concerns/.keep b/app/controllers/concerns/.keep similarity index 100% rename from lib/stealth/generators/builder/bot/models/concerns/.keep rename to app/controllers/concerns/.keep diff --git a/app/controllers/stealth/application_controller.rb b/app/controllers/stealth/application_controller.rb new file mode 100644 index 00000000..9b027aa2 --- /dev/null +++ b/app/controllers/stealth/application_controller.rb @@ -0,0 +1,4 @@ +module Stealth + class ApplicationController < ActionController::Base + end +end diff --git a/lib/stealth/controller/controller.rb b/app/controllers/stealth/controller.rb similarity index 56% rename from lib/stealth/controller/controller.rb rename to app/controllers/stealth/controller.rb index b52ca2ca..a46bc703 100644 --- a/lib/stealth/controller/controller.rb +++ b/app/controllers/stealth/controller.rb @@ -1,29 +1,20 @@ -# coding: utf-8 -# frozen_string_literal: true - module Stealth - class Controller + class Controller < ApplicationController - include Stealth::Controller::Callbacks - include Stealth::Controller::DynamicDelay - include Stealth::Controller::Replies - include Stealth::Controller::Messages - include Stealth::Controller::UnrecognizedMessage - include Stealth::Controller::CatchAll - include Stealth::Controller::Helpers include Stealth::Controller::InterruptDetect include Stealth::Controller::DevJumps - include Stealth::Controller::Nlp + include Stealth::Controller::Replies + include Stealth::Controller::IntentClassifier - attr_reader :current_message, :current_service, :flow_controller, - :action_name, :current_session_id + attr_reader :current_message, :current_service, :current_session_id attr_accessor :nlp_result, :pos - def initialize(service_message:, pos: nil) - @current_message = service_message - @current_service = service_message.service - @current_session_id = service_message.sender_id - @nlp_result = service_message.nlp_result + def initialize(service_event:, pos: nil) + super() + @current_message = service_event + @current_service = service_event.service + @current_session_id = service_event.sender_id + # @nlp_result = service_event.nlp_result @pos = pos @progressed = false end @@ -40,16 +31,12 @@ def progressed? @progressed end - def route - raise(Stealth::Errors::ControllerRoutingNotImplemented, "Please implement `route` method in BotController") - end - - def flow_controller - @flow_controller ||= begin - flow_controller = [current_session.flow_string.pluralize, 'controller'].join('_').classify.constantize - flow_controller.new(service_message: @current_message, pos: @pos) - end - end + # def flow_controller + # @flow_controller ||= begin + # flow_controller = [current_session.flow_string, 'controller'].join('_').classify.constantize + # flow_controller.new(service_message: @current_message, pos: @pos) + # end + # end def current_session @current_session ||= Stealth::Session.new(id: current_session_id) @@ -62,53 +49,53 @@ def previous_session ) end - def action(action: nil) - begin - # Grab a mutual exclusion lock on the session - lock_session!( - session_slug: Session.slugify( - flow: current_session.flow_string, - state: current_session.state_string - ) - ) - - @action_name = action - @action_name ||= current_session.state_string - - # Check if the user needs to be redirected - if current_session.flow.current_state.redirects_to.present? - Stealth::Logger.l( - topic: "redirect", - message: "From #{current_session.session} to #{current_session.flow.current_state.redirects_to.session}" - ) - step_to(session: current_session.flow.current_state.redirects_to, pos: @pos) - return - end - - run_callbacks :action do - begin - flow_controller.send(@action_name) - unless flow_controller.progressed? - run_catch_all(reason: 'Did not send replies, update session, or step') - end - rescue Stealth::Errors::Halted - Stealth::Logger.l( - topic: "session", - message: "User #{current_session_id}: session halted." - ) - rescue StandardError => e - if e.class == Stealth::Errors::UnrecognizedMessage - run_unrecognized_message(err: e) - else - run_catch_all(err: e) - end - end - end - ensure - # Release mutual exclusion lock on the session - release_lock! - end - end + # def action(action: nil) + # begin + # # Grab a mutual exclusion lock on the session + # lock_session!( + # session_slug: Session.slugify( + # flow: current_session.flow_string, + # state: current_session.state_string + # ) + # ) + + # @action_name = action + # @action_name ||= current_session.state_string + + # # Check if the user needs to be redirected + # if current_session.flow.current_state.redirects_to.present? + # Stealth::Logger.l( + # topic: "redirect", + # message: "From #{current_session.session} to #{current_session.flow.current_state.redirects_to.session}" + # ) + # step_to(session: current_session.flow.current_state.redirects_to, pos: @pos) + # return + # end + + # run_callbacks :action do + # begin + # flow_controller.send(@action_name) + # unless flow_controller.progressed? + # run_catch_all(reason: 'Did not send replies, update session, or step') + # end + # rescue Stealth::Errors::Halted + # Stealth::Logger.l( + # topic: "session", + # message: "User #{current_session_id}: session halted." + # ) + # rescue StandardError => e + # if e.class == Stealth::Errors::UnrecognizedMessage + # run_unrecognized_message(err: e) + # else + # run_catch_all(err: e) + # end + # end + # end + # ensure + # # Release mutual exclusion lock on the session + # release_lock! + # end + # end def step_to_in(delay, session: nil, flow: nil, state: nil, slug: nil) if interrupt_detected? @@ -127,7 +114,7 @@ def step_to_in(delay, session: nil, flow: nil, state: nil, slug: nil) raise ArgumentError, "Please specify your step_to_in `delay` parameter using ActiveSupport::Duration, e.g. `1.day` or `5.hours`" end - Stealth::ScheduledReplyJob.perform_in(delay, current_service, current_session_id, flow, state, current_message.target_id) + Stealth::Services::ScheduledReplyJob.perform_in(delay, current_service, current_session_id, flow, state, current_message.target_id) Stealth::Logger.l(topic: "session", message: "User #{current_session_id}: scheduled session step to #{flow}->#{state} in #{delay} seconds") end @@ -148,11 +135,11 @@ def step_to_at(timestamp, session: nil, flow: nil, state: nil, slug: nil) raise ArgumentError, "Please specify your step_to_at `timestamp` parameter as a DateTime" end - Stealth::ScheduledReplyJob.perform_at(timestamp, current_service, current_session_id, flow, state, current_message.target_id) + Stealth::Services::ScheduledReplyJob.perform_at(timestamp, current_service, current_session_id, flow, state, current_message.target_id) Stealth::Logger.l(topic: "session", message: "User #{current_session_id}: scheduled session step to #{flow}->#{state} at #{timestamp.iso8601}") end - def step_to(session: nil, flow: nil, state: nil, slug: nil, pos: nil) + def step_to(session: nil, flow: nil, state: nil, slug: nil, pos: nil, locals: nil) if interrupt_detected? run_interrupt_action return :interrupted @@ -164,11 +151,10 @@ def step_to(session: nil, flow: nil, state: nil, slug: nil, pos: nil) state: state, slug: slug ) - - step(flow: flow, state: state, pos: pos) + step(flow: flow, state: state, pos: pos, locals: locals) end - def update_session_to(session: nil, flow: nil, state: nil, slug: nil) + def update_session_to(session: nil, flow: nil, state: nil, slug: nil, locals: nil) if interrupt_detected? run_interrupt_action return :interrupted @@ -181,7 +167,7 @@ def update_session_to(session: nil, flow: nil, state: nil, slug: nil) slug: slug ) - update_session(flow: flow, state: state) + update_session(flow: flow, state: state, locals: locals) end def set_back_to(session: nil, flow: nil, state: nil, slug: nil) @@ -226,13 +212,15 @@ def halt! private - def update_session(flow:, state:) + def update_session(flow:, state:, locals: nil) @progressed = :updated_session @current_session = Session.new(id: current_session_id) unless current_session.flow_string == flow.to_s && current_session.state_string == state.to_s + @current_session.locals = locals @current_session.set_session(new_flow: flow, new_state: state) end + end def store_back_to_session(flow:, state:) @@ -243,14 +231,16 @@ def store_back_to_session(flow:, state:) back_to_session.set_session(new_flow: flow, new_state: state) end - def step(flow:, state:, pos: nil) - update_session(flow: flow, state: state) + def step(flow:, state:, pos: nil, locals: nil) + update_session(flow: flow, state: state, locals: locals) + Stealth.trigger_flow(flow, state, @current_message) + @progressed = :stepped - @flow_controller = nil - @current_flow = current_session.flow + # @flow_controller = nil + # @current_flow = current_session.flow @pos = pos - flow_controller.action(action: state) + # flow_controller.action(action: state) end def get_flow_and_state(session: nil, flow: nil, state: nil, slug: nil) @@ -268,8 +258,23 @@ def get_flow_and_state(session: nil, flow: nil, state: nil, slug: nil) end if flow.present? + # Deprecated + # if state.blank? + # state = FlowMap.flow_spec[flow.to_sym].states.keys.first.to_s + # end + if state.blank? - state = FlowMap.flow_spec[flow.to_sym].states.keys.first.to_s + # Access the existing FlowManager instance that has the registered flows + flow_manager = Stealth::FlowManager.instance + + # Retrieve the flow states for the specified flow + flow_states = flow_manager.instance_variable_get(:@flows)[flow.to_sym] + + if flow_states.present? + state = flow_states.keys.first.to_s + else + raise ArgumentError, "No states defined for flow: #{flow}" + end end return flow.to_s, state.to_s diff --git a/lib/stealth/controller/dev_jumps.rb b/app/controllers/stealth/controller/dev_jumps.rb similarity index 97% rename from lib/stealth/controller/dev_jumps.rb rename to app/controllers/stealth/controller/dev_jumps.rb index edc1a80b..e4bd28bb 100644 --- a/lib/stealth/controller/dev_jumps.rb +++ b/app/controllers/stealth/controller/dev_jumps.rb @@ -10,8 +10,7 @@ module DevJumps extend ActiveSupport::Concern included do - private - + def dev_jump_detected? if Stealth.env.development? if current_message.message&.match(DEV_JUMP_REGEX) @@ -38,4 +37,4 @@ def handle_dev_jump end end -end +end \ No newline at end of file diff --git a/app/controllers/stealth/controller/intent_classifier.rb b/app/controllers/stealth/controller/intent_classifier.rb new file mode 100644 index 00000000..1bfb647e --- /dev/null +++ b/app/controllers/stealth/controller/intent_classifier.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'spectre/prompt' + +module Stealth + class Controller + module IntentClassifier + extend ActiveSupport::Concern + + included do + def get_intent(message, categories: []) + return nil if message.nil? + + intents = self.class.get_intents_file + intents = if categories.present? + intents.select { |intent| categories.include?(intent[:category]) } + end + + system_prompt = Spectre::Prompt.render(template: 'intent_classifier/system', locals: { intents: intents }) + messages = [{ role: 'system', content: system_prompt }, { role: 'user', content: message }] + json_schema = { + name: "intent_response", + schema: { + type: "object", + properties: { + intent: { type: "string", description: "The name of the identified intent from the conversation message." } + }, + required: ["intent"], + additionalProperties: false + }, + strict: true + } + + response = Spectre::provider_module::Completions.create(messages: messages, json_schema: json_schema) + response_intent = JSON.parse(response[:content])['intent'] + if response_intent.present? + { intent: response_intent, tool: intents.select{ |intent| intent[:name] == response_intent }.first[:tool] } + end + end + + private + + def self.get_intents_file + return Stealth::Intents::INTENTS if defined?(Stealth::Intents::INTENTS) + + file_path = Pathname.new(Stealth.root).join('stealth', 'intents.rb') + + raise "Intents file not found: #{file_path}" unless File.exist?(file_path) + + load file_path + + Stealth::Intents::INTENTS + rescue LoadError => e + raise "Error loading Intents file #{file_path}: #{e.message}" + end + end + + end + end +end diff --git a/lib/stealth/controller/interrupt_detect.rb b/app/controllers/stealth/controller/interrupt_detect.rb similarity index 100% rename from lib/stealth/controller/interrupt_detect.rb rename to app/controllers/stealth/controller/interrupt_detect.rb diff --git a/app/controllers/stealth/controller/replies.rb b/app/controllers/stealth/controller/replies.rb new file mode 100644 index 00000000..108da489 --- /dev/null +++ b/app/controllers/stealth/controller/replies.rb @@ -0,0 +1,318 @@ +# coding: utf-8 +# frozen_string_literal: true + +module Stealth + class Controller + module Replies + + extend ActiveSupport::Concern + + included do + + # class_attribute :_preprocessors, default: [:erb] + # class_attribute :_replies_path, default: [Stealth.root, 'bot', 'replies'] + + # def send_replies(custom_reply: nil, inline: nil) + # service_reply = load_service_reply( + # custom_reply: custom_reply, + # inline: inline + # ) + + # # Determine if we start at the beginning or somewhere else + # reply_range = calculate_reply_range + # offset = reply_range.first + + # @previous_reply = nil + # service_reply.replies.slice(reply_range).each_with_index do |reply, i| + # # Updates the lock with the current position of the reply + # lock_session!( + # session_slug: current_session.get_session, + # position: i + offset # Otherwise this won't account for explicit starting points + # ) + + # begin + # send_reply(reply: reply) + # rescue Stealth::Errors::UserOptOut => e + # msg = "User #{current_session_id} opted out. [#{e.message}]" + # service_error_dispatcher( + # handler_method: :handle_opt_out, + # error_msg: msg + # ) + # return + # rescue Stealth::Errors::InvalidSessionID => e + # msg = "User #{current_session_id} has an invalid session_id. [#{e.message}]" + # service_error_dispatcher( + # handler_method: :handle_invalid_session_id, + # error_msg: msg + # ) + # return + # rescue Stealth::Errors::MessageFiltered => e + # msg = "Message to user #{current_session_id} was filtered. [#{e.message}]" + # service_error_dispatcher( + # handler_method: :handle_message_filtered, + # error_msg: msg + # ) + # return + # rescue Stealth::Errors::UnknownServiceError => e + # msg = "User #{current_session_id} had an unknown error. [#{e.message}]" + # service_error_dispatcher( + # handler_method: :handle_unknown_error, + # error_msg: msg + # ) + # return + # end + + # @previous_reply = reply + # end + + # @progressed = :sent_replies + # ensure + # release_lock! + # end + + def send_replies + flow = current_session.flow_string + state = current_session.state_string + Stealth.trigger_reply(flow, state, current_message) + end + + def say(reply) + reply_instance = Stealth::Reply.new(unstructured_reply: reply) + + handler = reply_handler.new( + recipient_id: current_message.sender_id, + reply: reply_instance.reply + ) + + formatted_reply = handler.send(reply_instance.reply_type) + + client = service_client.new(reply: formatted_reply) + client.transmit + end + + + # private + + # def voice_service? + # current_service.match?(/voice/) + # end + + # def send_reply(reply:) + # if !reply.delay? && Stealth.config.auto_insert_delays && !voice_service? + # # if it's the first reply in the service_reply or the previous reply + # # wasn't a custom delay, then insert a delay + # if @previous_reply.blank? || !@previous_reply.delay? + # send_reply(reply: Reply.dynamic_delay) + # end + # end + + # # Support randomized replies for text and speech replies. + # # We select one before handing the reply off to the driver. + # if reply['text'].is_a?(Array) + # reply['text'] = reply['text'].sample + # end + + # handler = reply_handler.new( + # recipient_id: current_message.sender_id, + # reply: reply + # ) + + # formatted_reply = handler.send(reply.reply_type) + # client = service_client.new(reply: formatted_reply) + # client.transmit + + # log_reply(reply, handler) if Stealth.config.transcript_logging + + # # If this was a 'delay' type of reply, we insert the delay + # if reply.delay? + # insert_delay(duration: reply['duration']) + # end + # end + + # def insert_delay(duration:) + # begin + # sleep_duration = if duration == 'dynamic' + # dyn_duration = dynamic_delay(previous_reply: @previous_reply) + + # Stealth.config.dynamic_delay_muliplier * dyn_duration + # else + # Float(duration) + # end + + # sleep(sleep_duration) + # rescue ArgumentError, TypeError + # raise(ArgumentError, 'Invalid duration specified. Duration must be a Numeric') + # end + # end + + # def load_service_reply(custom_reply:, inline:) + # if inline.present? + # Stealth::ServiceReply.new( + # recipient_id: current_session_id, + # yaml_reply: inline, + # preprocessor: :none, + # context: nil + # ) + # else + # yaml_reply, preprocessor = action_replies(custom_reply) + + # Stealth::ServiceReply.new( + # recipient_id: current_session_id, + # yaml_reply: yaml_reply, + # preprocessor: preprocessor, + # context: binding + # ) + # end + # end + + def service_client + begin + Kernel.const_get("Stealth::Services::#{current_service.classify}::Client") + rescue NameError + raise(Stealth::Errors::ServiceNotRecognized, "The service '#{current_service}' was not recognized") + end + end + + def reply_handler + begin + Kernel.const_get("Stealth::Services::#{current_service.classify}::ReplyHandler") + rescue NameError + raise(Stealth::Errors::ServiceNotRecognized, "The service '#{current_service}' was not recognized") + end + end + + # def replies_folder + # current_session.flow_string.underscore.pluralize + # end + + # def reply_dir + # [*self._replies_path, replies_folder] + # end + + # def base_reply_filename + # "#{current_session.state_string}.yml" + # end + + # def reply_filenames(custom_reply_filename=nil) + # reply_filename = if custom_reply_filename.present? + # custom_reply_filename + # else + # base_reply_filename + # end + + # service_filename = [reply_filename, current_service].join('+') + + # # Service-specific filenames take precedance (returned first) + # [service_filename, reply_filename] + # end + + # def find_reply_and_preprocessor(custom_reply) + # selected_preprocessor = :none + + # if custom_reply.present? + # dir_and_file = custom_reply.rpartition(File::SEPARATOR) + # _dir = dir_and_file.first + # _file = "#{dir_and_file.last}.yml" + # _replies_dir = [*self._replies_path, _dir] + # possible_filenames = reply_filenames(_file) + # reply_file_path = File.join(_replies_dir, _file) + # service_reply_path = File.join(_replies_dir, reply_filenames(_file).first) + # else + # _replies_dir = *reply_dir + # possible_filenames = reply_filenames + # reply_file_path = File.join(_replies_dir, base_reply_filename) + # service_reply_path = File.join(_replies_dir, reply_filenames.first) + # end + + # # Check if the service_filename exists + # # If so, we can skip checking for a preprocessor + # if File.exist?(service_reply_path) + # return service_reply_path, selected_preprocessor + # end + + # # Cycles through possible preprocessor and variant combinations + # # Early returns for performance + # for preprocessor in self.class._preprocessors do + # for reply_filename in possible_filenames do + # selected_filepath = File.join(_replies_dir, [reply_filename, preprocessor.to_s].join('.')) + # if File.exist?(selected_filepath) + # reply_file_path = selected_filepath + # selected_preprocessor = preprocessor + # return reply_file_path, selected_preprocessor + # end + # end + # end + + # return reply_file_path, selected_preprocessor + # end + + # def action_replies(custom_reply=nil) + # reply_path, selected_preprocessor = find_reply_and_preprocessor(custom_reply) + + # begin + # file_contents = File.read(reply_path) + # rescue Errno::ENOENT + # raise(Stealth::Errors::ReplyNotFound, "Could not find reply: '#{reply_path}'") + # end + + # return file_contents, selected_preprocessor + # end + + # def service_error_dispatcher(handler_method:, error_msg:) + # if self.respond_to?(handler_method, true) + # Stealth::Logger.l( + # topic: current_service, + # message: error_msg + # ) + # self.send(handler_method) + # else + # Stealth::Logger.l( + # topic: :err, + # message: "Unhandled service exception for user #{current_session_id}. No error handler for `#{handler_method}` found." + # ) + # end + + # do_nothing + # end + + # def calculate_reply_range + # # if an explicit starting point is specified, use that until the + # # end of the range, otherwise start at the beginning + # if @pos.present? + # (@pos..-1) + # else + # (0..-1) + # end + # end + + # def log_reply(reply, reply_handler) + # message = case reply.reply_type + # when 'text' + # if reply_handler.respond_to?(:translated_reply) + # reply_handler.translated_reply + # else + # reply['text'] + # end + # when 'speech' + # reply['speech'] + # when 'ssml' + # reply['ssml'] + # when 'delay' + # '' + # else + # "<#{reply.reply_type}>" + # end + + # Stealth::Logger.l( + # topic: current_service, + # message: "User #{current_session_id} -> Sending: #{message}" + # ) + + # message + # end + + end # instance methods + + end + end +end diff --git a/app/controllers/stealth/event_controller.rb b/app/controllers/stealth/event_controller.rb new file mode 100644 index 00000000..b21a4af7 --- /dev/null +++ b/app/controllers/stealth/event_controller.rb @@ -0,0 +1,49 @@ +module Stealth + class EventController < ApplicationController + # skip the default Rails CSRF protection for webhook calls... + skip_before_action :verify_authenticity_token, only: [:dispatch_event] + + def dispatch_event + Stealth::Logger.l(topic: params[:service], message: 'Received webhook.') + + # Convert params to a JSON-serializable plain Ruby hash + plain_params = JSON.parse(params.to_json) + + if request.env['CONTENT_TYPE']&.match(/application\/json/i) + json_params = MultiJson.load(request.body.read) + plain_params.merge!(json_params) + end + + # headers 'Access-Control-Allow-Origin' => '*', + # 'Access-Control-Allow-Methods' => ['OPTIONS', 'GET', 'POST'] + + # content_type 'audio/mp3' + # content_type 'application/octet-stream' + + if webhook_subscription?(plain_params) + render plain: plain_params['challenge'] + else + dispatcher = Stealth::Dispatcher.new( + service: plain_params["service"], + params: plain_params, + headers: get_headers_from_request(request) + ) + + dispatcher.coordinate + end + end + + private + + def get_headers_from_request(request) + request.env.select do |header, value| + %w[HTTP_HOST].include?(header) + end + end + + def webhook_subscription?(plain_params) + plain_params['type'] == 'url_verification' + end + + end +end diff --git a/app/helpers/stealth/application_helper.rb b/app/helpers/stealth/application_helper.rb new file mode 100644 index 00000000..6087e394 --- /dev/null +++ b/app/helpers/stealth/application_helper.rb @@ -0,0 +1,4 @@ +module Stealth + module ApplicationHelper + end +end diff --git a/app/jobs/stealth/application_job.rb b/app/jobs/stealth/application_job.rb new file mode 100644 index 00000000..ddf7ecac --- /dev/null +++ b/app/jobs/stealth/application_job.rb @@ -0,0 +1,4 @@ +module Stealth + class ApplicationJob < ActiveJob::Base + end +end diff --git a/app/mailers/stealth/application_mailer.rb b/app/mailers/stealth/application_mailer.rb new file mode 100644 index 00000000..cd42d961 --- /dev/null +++ b/app/mailers/stealth/application_mailer.rb @@ -0,0 +1,6 @@ +module Stealth + class ApplicationMailer < ActionMailer::Base + default from: "from@example.com" + layout "mailer" + end +end diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep new file mode 100644 index 00000000..e69de29b diff --git a/app/models/stealth/application_record.rb b/app/models/stealth/application_record.rb new file mode 100644 index 00000000..396959f3 --- /dev/null +++ b/app/models/stealth/application_record.rb @@ -0,0 +1,4 @@ +module Stealth + class ApplicationRecord + end +end diff --git a/app/spectre/prompts/intent_classifier/system.yml.erb b/app/spectre/prompts/intent_classifier/system.yml.erb new file mode 100644 index 00000000..35cf46b0 --- /dev/null +++ b/app/spectre/prompts/intent_classifier/system.yml.erb @@ -0,0 +1,11 @@ +system: | + Label a users message from a conversation with an intent. + Reply ONLY with the name of the intent. + + The intent should be one of the following: + <% @intents.each do |intent| %> + - <%= intent[:name] %> + <%= "Description: #{intent[:description]}" %> + <%= "Examples: #{intent[:examples]}" %> + <% end %> + If the intent is not in the list, reply with nothing. diff --git a/app/views/layouts/stealth/application.html.erb b/app/views/layouts/stealth/application.html.erb new file mode 100644 index 00000000..f22e7939 --- /dev/null +++ b/app/views/layouts/stealth/application.html.erb @@ -0,0 +1,15 @@ + + + + Stealth + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= stylesheet_link_tag "stealth/application", media: "all" %> + + + +<%= yield %> + + + diff --git a/bin/rails b/bin/rails new file mode 100755 index 00000000..6df0846b --- /dev/null +++ b/bin/rails @@ -0,0 +1,14 @@ +#!/usr/bin/env ruby +# This command will automatically be run when you run "rails" with Rails gems +# installed from the root of your application. + +ENGINE_ROOT = File.expand_path("..", __dir__) +ENGINE_PATH = File.expand_path("../lib/stealth/engine", __dir__) +APP_PATH = File.expand_path("../test/dummy/config/application", __dir__) + +# Set up gems listed in the Gemfile. +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) +require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"]) + +require "rails/all" +require "rails/engine/commands" diff --git a/bin/stealth b/bin/stealth deleted file mode 100755 index 0e889408..00000000 --- a/bin/stealth +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env ruby - -require 'bundler' -require 'stealth/cli' -Stealth::Cli.start diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 00000000..beda63ba --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,6 @@ +# require 'multi_json' + +Stealth::Engine.routes.draw do + # Stealth Default Service Routes + post ':service', to: 'event#dispatch_event' +end diff --git a/docs/.gitbook/assets/2880px-Turnstile_state_machine_colored.svg.png b/docs/.gitbook/assets/2880px-Turnstile_state_machine_colored.svg.png deleted file mode 100644 index 1fc4ede4..00000000 Binary files a/docs/.gitbook/assets/2880px-Turnstile_state_machine_colored.svg.png and /dev/null differ diff --git a/docs/.gitbook/assets/Torniqueterevolution.jpg b/docs/.gitbook/assets/Torniqueterevolution.jpg deleted file mode 100644 index a40b7320..00000000 Binary files a/docs/.gitbook/assets/Torniqueterevolution.jpg and /dev/null differ diff --git a/docs/.gitbook/assets/logo.svg b/docs/.gitbook/assets/logo.svg deleted file mode 100644 index 9e42161e..00000000 --- a/docs/.gitbook/assets/logo.svg +++ /dev/null @@ -1,28 +0,0 @@ - - - stealth_app_logo - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/.gitbook/assets/ruby.png b/docs/.gitbook/assets/ruby.png deleted file mode 100644 index 33274b40..00000000 Binary files a/docs/.gitbook/assets/ruby.png and /dev/null differ diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 6e8abb65..00000000 --- a/docs/README.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -description: >- - Stealth includes everything you need to build amazing conversational bots with - tools you know and love. From two-way SMS-based bots to Facebook Messenger, - Stealth offers the best developer experience. ---- - -# Intro - -## ![](.gitbook/assets/logo.svg) - -## What's Stealth? - -Stealth is an open source, Ruby framework for conversational voice and text chatbots. - -Stealth is inspired by the Model-View-Controller (MVC) pattern. However, instead of calling them _Views,_ Stealth refers to them as _Replies_ to better match the chatbot domain. - -* The [Model](models/overview.md) layer represents your data model (such as Account, User, Quote, etc.) and encapsulates the business logic that is specific to your bot. By default, Stealth uses [ActiveRecord](models/activerecord.md), but you can use any library that you prefer. -* The [Controller](controllers/controller-overview.md) layer is responsible for handling incoming requests from messaging platforms and providing and transmitting the response (reply). -* The [Reply](replies/reply-overview.md) layer is composed of “templates” that are responsible for constructing the respective reply. - -In addition to being inspired by Model-View-Controller (MVC) pattern, Stealth has a few other awesome things built in for you. - -* **Plug and play components.** Every service integration in Stealth is a Ruby gem. One bot can support multiple [messaging platforms](platforms/overview.md) (i.e. Facebook Messenger, SMS, Alexa, and more) and multiple NLP/NLU services. -* **Innovative.** Stealth is constantly improving and evolving. There are many innovations in Stealth such as: [interrupt detection](controllers/interrupt-detection.md), [homophone detection](controllers/handle\_message/homophone-detection.md), [hot-code reloading](dev-environment/hot-code-reloading.md), [multi-level catch-all handling](controllers/catch-alls.md), and more that make your bots perform better. -* **Advanced tooling.** From web servers to continuous integration testing, Stealth is built to take advantage of all the great work done by the web development community. -* **Hosting you trust.** Stealth bots are Rack applications. That means your bots can be [deployed](deployment/overview.md) using familiar services like Docker and Heroku. -* **Ready for production.** Stealth already powers bots for large, well-known brands. You can rest assured your bot will be in good hands with Stealth. -* **Open source.** Stealth is MIT licensed to ensure you own your bot's source code. More importantly, we welcome contributors to help make Stealth even better for everyone. - -## Prerequisites - -While it's helpful to have some familiarity with Ruby, we think you can get started with Stealth without yet knowing Ruby or even programming. Building text-based bots or Alexa Skills is a great starting point into the world of programming and we think Ruby is an excellent first programming language. - -We welcome contributors and questions from programmers of all experience levels. diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md deleted file mode 100644 index 88b42de1..00000000 --- a/docs/SUMMARY.md +++ /dev/null @@ -1,99 +0,0 @@ -# Table of contents - -* [Intro](README.md) -* [Getting Started](getting-started.md) -* [Basics](basics.md) -* [Dev Environment](dev-environment/README.md) - * [Booting Up](dev-environment/booting-up.md) - * [Hot-Code Reloading](dev-environment/hot-code-reloading.md) - * [Procfile](dev-environment/procfile.md) - * [Tunnels](dev-environment/tunnels.md) - * [Environment Variables](dev-environment/environment-variables.md) - * [Logs](dev-environment/logs.md) -* [Glossary](glossary.md) - -## Flows - -* [Flows & States](flows/overview.md) -* [State Naming](flows/state-naming.md) -* [FlowMap](flows/flowmap.md) -* [State Options](flows/state-options.md) - -## Controllers - -* [Controller Overview](controllers/controller-overview.md) -* [Sessions](controllers/sessions/README.md) - * [Session Overview](controllers/sessions/intro.md) - * [step\_to](controllers/sessions/step\_to.md) - * [step\_to\_in](controllers/sessions/step\_to\_in.md) - * [step\_to\_at](controllers/sessions/step\_to\_at.md) - * [update\_session\_to](controllers/sessions/update\_session\_to.md) - * [step\_back](controllers/sessions/step\_back.md) - * [do\_nothing](controllers/sessions/do\_nothing.md) -* [route](controllers/route.md) -* [Available Data](controllers/available-data.md) -* [handle\_message](controllers/handle\_message/README.md) - * [String Matcher](controllers/handle\_message/string-mather.md) - * [Alpha Ordinal Matcher](controllers/handle\_message/alpha-ordinal-matcher.md) - * [Homophone Detection](controllers/handle\_message/homophone-detection.md) - * [NLP Matcher](controllers/handle\_message/nlp-matcher.md) - * [Regex Matcher](controllers/handle\_message/regex-matcher.md) - * [Nil Matcher](controllers/handle\_message/nil-matcher.md) -* [get\_match](controllers/get\_match/README.md) - * [Exact Match](controllers/get\_match/exact-match.md) - * [Alpha Ordinals](controllers/get\_match/alpha-ordinals.md) - * [Entity Match](controllers/get\_match/entity-match.md) -* [Catch-Alls](controllers/catch-alls.md) -* [Dev Jumps](controllers/dev-jumps.md) -* [Interrupt Detection](controllers/interrupt-detection.md) -* [Unrecognized Messages](controllers/unrecognized-messages.md) -* [Platform Errors](controllers/platform-errors.md) - -## Replies - -* [Reply Overview](replies/reply-overview.md) -* [YAML Replies](replies/yaml-replies.md) -* [ERB](replies/erb.md) -* [Delays](replies/delays.md) -* [Variants](replies/variants.md) -* [Inline Replies](replies/inline-replies.md) - -## Models - -* [Model Overview](models/overview.md) -* [ActiveRecord](models/activerecord.md) -* [Mongoid](models/mongoid.md) - -## Platforms - -* [Platform Overview](platforms/overview.md) -* [Facebook Messenger](platforms/facebook-messenger.md) -* [SMS/Whatsapp](platforms/sms-whatsapp.md) -* [Alexa Skills](platforms/alexa-skills.md) -* [Voice](platforms/voice.md) - -## NLP/NLU - -* [NLP Overview](nlp-nlu/overview.md) -* [Microsoft LUIS](nlp-nlu/microsoft-luis.md) -* [OpenAI](nlp-nlu/openai.md) - -## Config - -* [Settings](config/settings.md) -* [services.yml](config/services.yml.md) - -## Testing - -* [Specs](testing/untitled.md) -* [Integration Testing](testing/integration-testing.md) - -## Deployment - -* [Deployment Overview](deployment/overview.md) -* [Heroku](deployment/heroku.md) - -## Building Components - -* [Message Services](building-components/message-services.md) -* [NLP](building-components/nlp.md) diff --git a/docs/basics.md b/docs/basics.md deleted file mode 100644 index a595099f..00000000 --- a/docs/basics.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -description: A quick primer of the various pieces that comprise Stealth. ---- - -# Basics - -## Anatomy of a Stealth Bot - -A Stealth bot has two primary processes: a _web server_, and a _background job processer_. If the messaging platform being used supports it, Stealth will push a message to a queue where a reply will be constructured by a background job. This allows you to easily scale your Stealth bot across many threads and processes. - -### Data Store - -Stealth requires Redis. Redis is used for [session storage](controllers/sessions/intro.md) as well as the queue for background jobs. You can access the Redis store yourself via the global variable: `$redis`. You can use it as your primary data store if you are building a simple bot, but you'll likely want to use a SQL or NoSQL database for more complex bots. - -### Environments - -Stealth bots can be booted into three environment types: `development`, `testing`, `production`. By default, if an environment is not specified via the `STEALTH_ENV` environment variable, the `development` environment will be used. The `testing` environment is automatically used when running your specs. - -### ActiveSupport - -Stealth automatically includes [active\_support](https://guides.rubyonrails.org/active\_support\_core\_extensions.html). So if you're used to using certain core extensions in Ruby on Rails, you can continue to use them in your Stealth bots! - -## Lifecycle of a Message - -This is just a brief outline of the lifecycle of a message to help you understand how Stealth processes messages. For more detailed information that you can use to build your own message platform components, check out [those docs](building-components/message-services.md). - -1. A message is received by the web server. -2. If the message platform supports it, the message is backgrounded to be processed by a background job. If the message platform does not support it ([Alexa Skill](platforms/alexa-skills.md) or [Voice](platforms/voice.md)), the message is processed inline by the web server process. -3. Stealth uses the respective message platform component to normalize the message into a standard format. -4. Stealth calls the [route](controllers/route.md) method in `BotController`. If a session exists for the user, they are routed to their current state. If a session does not exist, by default the `route` method will route the user to the `HellosController#say_hello` method. -5. The controller action will either do nothing, step to another state, update the session, or generate a reply. In the latter case, the reply will be delivered via the message platform component. - -## Directory Structure - -When you use the generator `stealth new` to instantiate a new bot, here is the directory structure that will be created: - -``` -├── Gemfile -├── Procfile.dev -├── README.md -├── Rakefile -├── bot -│   ├── controllers -│   │   ├── bot_controller.rb -│   │   ├── catch_alls_controller.rb -│   │   ├── concerns -│   │   ├── goodbyes_controller.rb -│   │   ├── hellos_controller.rb -│   │   ├── interrupts_controller.rb -│   │   └── unrecognized_messages_controller.rb -│   ├── helpers -│   │   └── bot_helper.rb -│   ├── models -│   │   ├── bot_record.rb -│   │   └── concerns -│   └── replies -│   ├── catch_alls -│   │   └── level1.yml -│   ├── goodbyes -│   │   └── say_goodbye.yml -│   └── hellos -│   └── say_hello.yml -├── config -│   ├── boot.rb -│   ├── database.yml -│   ├── environment.rb -│   ├── flow_map.rb -│   ├── initializers -│   │   ├── autoload.rb -│   │   └── inflections.rb -│   ├── puma.rb -│   ├── services.yml -│   └── sidekiq.yml -├── config.ru -└── db - └── seeds.rb -``` diff --git a/docs/building-components/message-services.md b/docs/building-components/message-services.md deleted file mode 100644 index b6a9b2f8..00000000 --- a/docs/building-components/message-services.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -description: Guide to creating your own message platform component. ---- - -# Message Services - -## Coming Soon... diff --git a/docs/building-components/nlp.md b/docs/building-components/nlp.md deleted file mode 100644 index 1ec9f8c0..00000000 --- a/docs/building-components/nlp.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -description: Guide to creating your own NLP component. ---- - -# NLP - -## Coming Soon... diff --git a/docs/config/services.yml.md b/docs/config/services.yml.md deleted file mode 100644 index 53340fd7..00000000 --- a/docs/config/services.yml.md +++ /dev/null @@ -1,2 +0,0 @@ -# services.yml - diff --git a/docs/config/settings.md b/docs/config/settings.md deleted file mode 100644 index 29e82f8c..00000000 --- a/docs/config/settings.md +++ /dev/null @@ -1,2 +0,0 @@ -# Settings - diff --git a/docs/controllers/available-data.md b/docs/controllers/available-data.md deleted file mode 100644 index da61f2ec..00000000 --- a/docs/controllers/available-data.md +++ /dev/null @@ -1,77 +0,0 @@ -# Available Data - -Within each controller action, you have access to a few objects containing information about the session and the received message. - -{% hint style="info" %} -Other Stealth components might make additional data available (e.g., sentiment analysis, etc). -{% endhint %} - -## `current_message` - -The current message being processed is available via `current_message`. This is a `Stealth::ServiceMessage` object. It has a few important methods: - -`sender_id`: The ID of the user sending the message. This will vary based on the message service component. This is also the ID that will be used as the user's session ID. - -`target_id`: The ID of the target. This will vary based on the service sending the message, but for Facebook it will be the `page_id` of the Facebook page receiving the message and for SMS will be the number receiving the SMS message. For other services, this may be `nil`. - -`timestamp`: Ruby `DateTime` object containing the timestamp of when the message was transmitted. This might differ from the current time. - -`service`: String indicating the message service from where the message originated (i.e., `facebook`, or `twilio`). - -`message`: String of the message contents. - -`payload`: This will vary per message service component. - -`nlp_result`: The raw result of the NLP performed on this message. This will vary per NLP component. - -`catch_all_reason`: This is a hash that contains two keys: `:err` and `:err_msg`. The `:err` key is a string of the exception class that was raised and the `:err_msg` is the message associated with that exception. See [Catch-All Reasons](catch-alls.md#catch-all-reasons) for more info. - -`location`: This will vary per message service component. - -`attachments`: This will vary per message service component. - -`referral`: This will vary per message service component. - -## `current_session` - -The user's session is accessible via `current_session`. This is a `Stealth::Session` object. It has a few important methods: - -`flow_string`: Returns the name of the flow. - -`state_string`: Returns the name of the state. - -`to_s`: Returns the session canonical session slug string. - -`current_session + 2.states`: Returns a new session object 2 states after the current state. If we've exceeded the last state in flow (as defined in the [FlowMap](../flows/flowmap.md)), the last state is returned. - -`current_session - 2.states`: Returns a new session object 2 states before the current state. If we've exceeded the first state in the flow (as defined in the [FlowMap](../flows/flowmap.md)), the first state is returned. - -`==`: Compare two sessions and returns `true` if they point to the same flow and state and `false` if they do not. - -{% hint style="warning" %} -Use the session arithmetic operators (`+` and `-`) sparingly. They are primarily designed for use in [Catch](catch-alls.md)[Alls](catch-alls.md) when a `fails_to` state has not been specified. -{% endhint %} - -## `current_service` - -Returns a string indicating the message platform from where the message originated (i.e., `facebook`, or `twilio`). - -{% hint style="info" %} -This is an alias of `current_message.service` -{% endhint %} - -## `current_session_id` - -Returns the session ID. This will vary by message service, but for Facebook Messenger this will be the user's PSID and for SMS and Whatsapp this will be the user's phone number in [E.164](https://en.m.wikipedia.org/wiki/E.164) format. - -{% hint style="info" %} -This is an alias of `current_message.sender_id` -{% endhint %} - -## `has_location?` - -Returns `true` or `false` depending on whether or not the `current_message` contains location data. - -## `has_attachments?` - -Returns `true` or `false` depending on whether or not the `current_message` contains file or media attachments. diff --git a/docs/controllers/catch-alls.md b/docs/controllers/catch-alls.md deleted file mode 100644 index f96ebb00..00000000 --- a/docs/controllers/catch-alls.md +++ /dev/null @@ -1,135 +0,0 @@ -# Catch-Alls - -Stealth Catch-Alls are designed to handle the error cases within bots. Either the bot doesn't understand what a user typed or an error occurred. If this were a webpage, we'd see the dreaded HTTP 500 error page. - -Catch-Alls are designed to move beyond simple "I don't understand" messages and help users get back on track. The better your CatchAlls, the better your bot. - -### Triggering - -The `catch_all` flow is automatically triggered when either of two things happens within a controller action: - -1. The action [fails to progress a user](controller-overview.md#failing-to-progress-a-user). -2. An Exception is raised. - -{% hint style="info" %} -If an action within `CatchAllsController` raises an exception, it won't fire another Catch-All to prevent loops. -{% endhint %} - -### Multi-Level - -Stealth keeps track of how many times a Catch-All is triggered for a given session. This allows you to build experiences in which the user is provided different responses for subsequent failures. - -So for example, if in the `hello` flow and `say_hello` state an exception is raised, then Catch-All _Level 1_ will be called. If the user is return to that same flow and state and another exception is raised, Catch-All _Level 2_ will be called. This continues until you either run out of Catch-All states or if the Catch-All counter resets. - -{% hint style="info" %} -The Catch-All counter currently resets after 15 minutes. This is per flow and state. So a user may encounter a Catch-All elsewhere in your bot and it will utilize a separate Catch-All counter. -{% endhint %} - -### Retrying - -By default, a Stealth bot comes with Catch-All Level 1 already defined. Here is the default `CatchAllsController` and associated reply: - -```ruby -class CatchAllsController < BotController - - def level1 - send_replies - - if fail_session.present? - step_to session: fail_session - else - step_to session: previous_session - 2.states - end - end - -private - - def fail_session - previous_session.flow.current_state.fails_to - end - -end -``` - -```yaml -- reply_type: text - text: Oops. It looks like something went wrong. Let's try that again -``` - -In the controller action, we check if the `previous_session` (the one that failed) specified a `fails_to` state. If so, we send the user there. Otherwise, we send the user back 2 states. - -Sending a user back two states is a pretty good generic action. Going back 1 state takes us back to the action that failed. Since the actions most likely to fail are `get` actions, or actions that deal with user responses, going back 2 states usually takes us back to the original "question". - -{% hint style="info" %} -Where possible, it's better to specify a `fails_to` state so Stealth doesn't incorrectly guess where to send your user back. -{% endhint %} - -### Adding More Levels - -If you would like to extend the experience, add a `level2` controller action and associated reply (and update the `FlowMap`). You can go as far as you want. CatchAlls have no limit, just make sure you increment using the standardized method names of `level1`, `level2`, `level3`, `level4`, etc. - -If a user has encountered the maximum number of CatchAll levels that have been defined, it won't attempt to call any more levels. - -{% hint style="warning" %} -For the last Catch-All state, you'll probably want to prompt the user to contact support or send them to a special menu to choose from. -{% endhint %} - -### Catch-All Reasons - -As mentioned in the [Triggering](catch-alls.md#triggering) section above, there are two reasons a Catch-All triggers. Stealth will provide the `CatchAllsController` with that reason so you can customize your messages and take the appropriate action. - -So if for example your bot just didn't recognize the message sent by the user, you may ask the user to repeat. If however, your database is down, you might other action. - -Here is an example usage: - -```ruby -class CatchAllsController < BotController - - before_action :set_catch_all_reason - - def level1 - send_catch_all_replies('level1') - - if fail_session.present? - step_to session: fail_session, pos: -1 - else - step_to session: previous_session - 2.states, pos: -1 - end - end - - def level2 - send_catch_all_replies('level2') - end - - def level3 - send_catch_all_replies('level3') - end - - private - - def fail_session - previous_session.flow.current_state.fails_to - end - - def send_catch_all_replies(level) - if @reason == :unrecognized_message - send_replies(custom_reply: "catch_alls/#{level}_unrecognized") - else - send_replies(custom_reply: "catch_alls/#{level}") - end - end - - def set_catch_all_reason - @reason = case current_message.catch_all_reason[:err].to_s - when 'Stealth::Errors::UnrecognizedMessage' - :unrecognized_message - else - :system_error - end - end - -end - -``` - -In `CatchAllsController` we have two sets of Catch-All replies. One for when the message was unrecognized and another for when we've encountered a system error. We dynamically send the appropriate reply based on the `@reason` instance variable that we set with the `before_action` in the controller. diff --git a/docs/controllers/controller-overview.md b/docs/controllers/controller-overview.md deleted file mode 100644 index 3c92ebd2..00000000 --- a/docs/controllers/controller-overview.md +++ /dev/null @@ -1,130 +0,0 @@ -# Controller Overview - -Controllers are responsible for handling incoming requests and getting a response back to the user via replies. They also perform all state transitions. - -## Naming Conventions - -The controller's methods, also referred to as actions, must be named after the flow's states. So for example, given the flow: - -```ruby -flow :onboard do - state :say_welcome - state :ask_for_phone - state :get_phone, fails_to: :ask_for_phone -end -``` - -The corresponding controller would be: - -```ruby -class OnboardsController < BotController - def say_welcome - - end - - def ask_for_phone - - end - - def get_phone - - end -end -``` - -## BotController - -Every Stealth bot comes with a default `bot_controller.rb`. You don't have to know what each method does yet, we'll cover each in their respective doc sections. - -```ruby -# frozen_string_literal: true - -class BotController < Stealth::Controller - - helper :all - - def route - if current_message.payload.present? - handle_payloads - # Clear out the payload to prevent duplicate handling - current_message.payload = nil - return - end - - # Allow devs to jump around flows and states by typing: - # /flow_name/state_name or - # /flow_name (jumps to first state) or - # //state_name (jumps to state in current flow) - # (only works for bots in development) - return if dev_jump_detected? - - if current_session.present? - step_to session: current_session - else - step_to flow: 'hello', state: 'say_hello' - end - end - -private - - # Handle payloads globally since payload buttons remain in the chat - # and we cannot guess in which states they will be tapped. - def handle_payloads - case current_message.payload - when 'developer_restart', 'new_user' - step_to flow: 'hello', state: 'say_hello' - when 'goodbye' - step_to flow: 'goodbye' - end - end - - # Automatically called when clients receive an opt-out error from - # the platform. You can write your own steps for handling. - def handle_opt_out - do_nothing - end - - # Automatically called when clients receive an invalid session_id error from - # the platform. For example, attempting to text a landline. - # You can write your own steps for handling. - def handle_invalid_session_id - do_nothing - end - -end - -``` - -All of your controllers will inherit from this `BotController`: - -```ruby -class QuotesController < BotController - -end -``` - -## Failing to Progress a User - -One of the primary responsibilities of a controller is to update a user's session. The other responsibility is sending replies to a user. If you fail to do either of these things, essentially the user at the other end won't have any feedback. - -If a controller action fails to update the state or send any replies, Stealth will automatically fire a [CatchAll](catch-alls.md). This is designed to catch errors during development. If you are certain you don't want to send any feedback to the user for a specific action you can call [do\_nothing](sessions/do\_nothing.md) to override Stealth's default behavior. - -## Before/After/Around Filters - -Like Ruby on Rails controllers, Stealth controllers support `before_action`, `after_action`, and `around_action` filters. - -Given a `BotController` that loads a user: - -```ruby -class BotController < Stealth::Controller - - before_action :current_user - - private def current_user - @current_user ||= User.find_by_session_id(current_session_id) - end - -end -``` - -The `current_user` method will be run on all controllers that inherit from `BotController`. Similarly, if you add a `before_action` to a child controller, only that controller's actions will run that filter. diff --git a/docs/controllers/dev-jumps.md b/docs/controllers/dev-jumps.md deleted file mode 100644 index 67445653..00000000 --- a/docs/controllers/dev-jumps.md +++ /dev/null @@ -1,47 +0,0 @@ -# Dev Jumps - -Dev Jumps are a feature of Stealth that makes you and your team more productive during development. It enables you to jump between flows and states while interacting with your bot. As you develop your bot, you can avoid having to restart the conversation each time. - -{% hint style="warning" %} -Dev Jumps will only work while your bot is in the `development` environment. Dev jumps in other environments will be ignored. -{% endhint %} - -## Usage - -You can specify Dev Jumps in one of three ways: - -1. Flow and state. -2. Just a flow name. -3. Just a state name. - -{% hint style="info" %} -You can text these at any time to your bot. -{% endhint %} - -### Flow and State - -``` -/flow_name/state_name -``` - -This will immediately step to the `flow_name` and `state_name` that you specified. - -### Flow Name - -``` -/flow_name -``` - -This will jump the the first state declared in the [FlowMap](../flows/flowmap.md) for the flow. - -### State Name - -``` -//state_name -``` - -This will jump to specified `state_name` within the current flow. - -{% hint style="info" %} -Note the double forward slash `//`. This is essentially because the `flow_name` has been explicitly omitted. -{% endhint %} diff --git a/docs/controllers/get_match/README.md b/docs/controllers/get_match/README.md deleted file mode 100644 index a2a213c6..00000000 --- a/docs/controllers/get_match/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# get\_match - diff --git a/docs/controllers/get_match/alpha-ordinals.md b/docs/controllers/get_match/alpha-ordinals.md deleted file mode 100644 index 6ca7a04d..00000000 --- a/docs/controllers/get_match/alpha-ordinals.md +++ /dev/null @@ -1,2 +0,0 @@ -# Alpha Ordinals - diff --git a/docs/controllers/get_match/entity-match.md b/docs/controllers/get_match/entity-match.md deleted file mode 100644 index ef2f90dd..00000000 --- a/docs/controllers/get_match/entity-match.md +++ /dev/null @@ -1,2 +0,0 @@ -# Entity Match - diff --git a/docs/controllers/get_match/exact-match.md b/docs/controllers/get_match/exact-match.md deleted file mode 100644 index 7fd9c932..00000000 --- a/docs/controllers/get_match/exact-match.md +++ /dev/null @@ -1,2 +0,0 @@ -# Exact Match - diff --git a/docs/controllers/handle_message/README.md b/docs/controllers/handle_message/README.md deleted file mode 100644 index 99d2c663..00000000 --- a/docs/controllers/handle_message/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# handle\_message - -The `handle_message` method is one of the most fundamental controller methods in Stealth. It enables you to succinctly handle messages without needing long `if/else` or `case` statements. - -## Example - -Here's an example of what it looks like: - -```ruby -def get_call_response - handle_message( - :yes => proc { step_to state: :say_yes }, - 'Not available' => proc { step_to state: :say_no_problem }, - :no => proc { step_to state: :say_no_problem }, - :call => proc { step_to state: :say_call_later } - ) -end -``` - -In this example, the `handle_message` method has four different "match arms". Each arm is just `key => value` pair since it's all just a hash. The keys are the `match expressions`. The values are `procs` that serve as the action to take if the match expression is matched. - -Stealth accepts match expressions in five different ways: - -1. [A string](string-mather.md) (like Line 4) -2. An [alpha ordinal](alpha-ordinal-matcher.md) -3. A [symbol](nlp-matcher.md) (like Lines 3, 5, and 6) -4. A [regex](regex-matcher.md) -5. [nil](nil-matcher.md) - -{% hint style="info" %} -The `procs` can be multi-line and thus perform more than one action. In the docs for the match expressions we'll include some of those examples. -{% endhint %} - -## Proc Scope - -If you are new to Ruby, you might be wondering about `procs`. You can think of them as anonymous functions from Javascript or closures. - -The code inside of the `proc` is executed if the match expression is matched. That code has access to the same variables as the containing method's scope. So for example: - -```ruby -def get_response - x = 15 - handle_message( - 'Buy' => proc { - x += 1 - }, - 'Refinance' => proc { - x += 2 - } - ) -end -``` - -In this example if a user sends the message "Buy", the value of `x` will be `16`. Similarly, if the user sends the message "Refinance", the value of `x` will be `17`. diff --git a/docs/controllers/handle_message/alpha-ordinal-matcher.md b/docs/controllers/handle_message/alpha-ordinal-matcher.md deleted file mode 100644 index 9283d045..00000000 --- a/docs/controllers/handle_message/alpha-ordinal-matcher.md +++ /dev/null @@ -1,40 +0,0 @@ -# Alpha Ordinal Matcher - -Alpha ordinals are a way of providing "quick replies" for messaging services that do not support them natively, such as SMS and Whatsapp. With `handle_message`, they are supported directly. - -## Example - -Imagine you send a user this reply: - -``` -What's your favorite color? - -Reply with: -"A" for Red -"B" for Blue -"C" for Green -"D" for Yellow -``` - -{% hint style="info" %} -Stealth's Twilio component can automatically generate these for you. This is covered in depth in the [YAML Replies docs](../../replies/yaml-replies.md). -{% endhint %} - -The corresponding `handle_message` method would look like this: - -```ruby -def get_response - handle_message( - 'Red' => proc { current_user.update_attributes!(favorite_color: 'red') }, - 'Blue' => proc { current_user.update_attributes!(favorite_color: 'blue') }, - 'Green' => proc { current_user.update_attributes!(favorite_color: 'green') }, - 'Yellow' => proc { current_user.update_attributes!(favorite_color: 'yellow') } - ) -end -``` - -With alpha ordinals, if a user types "C", then the `Green` match arm is automatically selected since it is the 3rd match expression (Line 5) specified in `handle_message`. Similarly, the user could still reply with "green" directly and that will also match the 3rd match expression (Line 5). - -{% hint style="warning" %} -If a user were to type, "E", then Stealth will still raise a `Stealth::Errors::UnrecognizedMessage` exception. -{% endhint %} diff --git a/docs/controllers/handle_message/homophone-detection.md b/docs/controllers/handle_message/homophone-detection.md deleted file mode 100644 index 9d7917ba..00000000 --- a/docs/controllers/handle_message/homophone-detection.md +++ /dev/null @@ -1,36 +0,0 @@ -# Homophone Detection - -One of the problems that arises with [alpha ordinals](alpha-ordinal-matcher.md) is that some English letters sound like other words. So for example, pronouncing "C" sounds like "see" or "sea". The letter "B" sounds like "bee" or "be". These are called homophones. - -If your user is using a voice assistant to dictate their responses to your bot, it's likely the voice assistant will substitute one of these homophones for the actual letter and your bot would fail to match the user's message. - -This is where homophone detection helps. - -{% hint style="info" %} -Homophone detection is currently only supported for the English language. -{% endhint %} - -## Example - -Given this code snippet from the [Alpha Ordinal Matcher docs](alpha-ordinal-matcher.md): - -```ruby -def get_response - handle_message( - 'Red' => proc { current_user.update_attributes!(favorite_color: 'red') }, - 'Blue' => proc { current_user.update_attributes!(favorite_color: 'blue') }, - 'Green' => proc { current_user.update_attributes!(favorite_color: 'green') }, - 'Yellow' => proc { current_user.update_attributes!(favorite_color: 'yellow') } - ) -end -``` - -If a user enters the letter "b", then the "Blue" match arm will automatically be selected as expected. However, Stealth will also natively accept the input "be" and "bee" from the user and the "Blue" match arm will still be selected. Similarly, "see" and "sea" will select the "Green" match arm. - -{% hint style="info" %} -For a full list of homophones Stealth detects, you can inspect the array constant `Stealth::Controller::Messages::HOMOPHONES` -{% endhint %} - -{% hint style="danger" %} -If you attempt to use a homophone as your match expression, Stealth will raise a `Stealth::Errors::ReservedHomophoneUsed` exception. -{% endhint %} diff --git a/docs/controllers/handle_message/nil-matcher.md b/docs/controllers/handle_message/nil-matcher.md deleted file mode 100644 index 83d9323a..00000000 --- a/docs/controllers/handle_message/nil-matcher.md +++ /dev/null @@ -1,44 +0,0 @@ -# Nil Matcher - -When none of the match expressions are matched in a `handle_message` method call, by default Stealth will raise a `Stealth::Errors::UnrecognizedMessage` exception. This is typically the desired behavior because it allows the [UnrecognizedMessagesController](../unrecognized-messages.md) to run. - -In the event that you don't want to raise an error, like in the case where you want to just save what the user typed in and move on, you can use the nil matcher. - -## Example - -Given this reply to a user: - -``` -How much is your property worth? - -Reply with: -"A" for $1 - $100 -"B" for $101 - $999 -"C" for $1000 - $9999 -``` - -```ruby -def get_response - handle_message( - '$1 - $100' => proc { - current_user.update_attributes!(property_value: '$1 - $100') - }, - '$101 - $999' => proc { - current_user.update_attributes!(property_value: '$101 - $999') - }, - '$1000 - $9999' => proc { - current_user.update_attributes!(property_value: '$1000 - $9999') - }, - nil => proc { - amount = current_user.message - current_user.update_attributes!(property_value: amount) - } - ) -end -``` - -In the above example, if a user enters a specific amount instead of choosing one of the ranges provided, we just store that amount and don't raise an error. - -{% hint style="warning" %} -You may likely still want to verify the input they entered is a Numeric. There are also better ways to handle an example like this using [get\_match](../get\_match/). -{% endhint %} diff --git a/docs/controllers/handle_message/nlp-matcher.md b/docs/controllers/handle_message/nlp-matcher.md deleted file mode 100644 index cb91f3cc..00000000 --- a/docs/controllers/handle_message/nlp-matcher.md +++ /dev/null @@ -1,51 +0,0 @@ -# NLP Matcher - -NLP is a very important part of creating powerful bots. Stealth seamlessly integrates with NLP services to provide NLP matching from within the same `handle_message` method. - -{% hint style="info" %} -Check out the [NLP section](../../nlp-nlu/overview.md) for more information on how to configure your NLP service to work with Stealth's `handle_message`. -{% endhint %} - -## Example - -Let's pretend you've trained your NLP service with some examples of "Yes" and some examples of "No". These are two common intents that you'll likely want to train for your bot. Let's also assume that we've named the "Yes" intent as `yes` and the "No" intent as `no` - -{% hint style="warning" %} -Make sure you name your intents using Ruby's `snake_casing` so they easily be used in `handle_message`. -{% endhint %} - -Given this reply to the user: - -``` -Are you interested in learning more about Stealth? - -Reply with: -"A" for Yes -"B" for Remind me later -"C" for No longer interested -``` - -Here is what our controller action that handles this message can look like with NLP matchers: - -```ruby -def get_response - handle_message( - :yes => proc { - step_to state: :say_proceed - }, - 'Remind me later' => proc { step_to state: :say_no_problem }, - :no => proc { step_to state: :say_goodbye }, - :call => proc { - step_to state: :ask_when_to_call - } - ) -end -``` - -Here, we are using NLP matchers on Lines 3, 7, and 8. NLP matchers have specify match expression as a Ruby symbol. - -When Stealth encounters an NLP matcher as a match expression, it will perform NLP using your configured NLP service. The result is automatically cached so that subsequent NLP matchers don't trigger another NLP lookup. The raw result of the NLP query will be stored in `current_message.nlp_result`, but `handle_message` will automatically make use of that without you having to parse it yourself. - -If the resulting NLP intent is `yes` then the `:yes` match arm will be matched. The same for `:no` and `:call`. So for example, if a user type "Nah, not interested" it's likely or `:no` match arm will be called. Similarly, if a user writes "Sure!!" the `:yes` match arm will be called. - -Another interesting thing to note about this example is that we have a 4th option (`:call`) that isn't explicitly mentioned in the reply to the user. With NLP matchers, it's sometimes useful to do this when your data shows that a lot users are manually typing in a custom response for a specific question. So in this case, we've asked the user if they are interested in learning more about Stealth, but some users will ask to jump on a phone call. So we've trained an NLP intent for "calls" and it will match the cases where a user requests a call for this question. diff --git a/docs/controllers/handle_message/regex-matcher.md b/docs/controllers/handle_message/regex-matcher.md deleted file mode 100644 index 38622e1c..00000000 --- a/docs/controllers/handle_message/regex-matcher.md +++ /dev/null @@ -1,41 +0,0 @@ -# Regex Matcher - -When using the [string matcher](string-mather.md), you might have a scenario where the option you present to user for selection is longer or contains more detail than the answer they type. In these cases, it's useful to be able to use a regex to match a just a part of a message. - -{% hint style="info" %} -The regex matcher is not limited to this use case. You can use the full power of Ruby regexes. -{% endhint %} - -## Example - -Given this reply to a user: - -``` -What would you like to do? - -Reply with: -"A" for I'd like to restart -"B" for Just stop -"C" for Repeat -``` - -We can take advantage of the regex matcher for options "A" and "B" since it's unlikely a user would type that entire string with the formatting we expect. - -```ruby -def get_response - handle_message( - /restart/ => proc { step_to flow: :hello }, - /stop/ => proc { - current_user.opt_out! - step_to flow: :opt_out - }, - 'Repeat' => proc { step_to session: previous_session } - ) -end -``` - -Now if a user types "restart" or "restart plz" it will still match the first match arm. Similarly, if they type "stop" it will match the second match arm. But just as before, typing "just stop" and "B" will also match the second match arm. - -{% hint style="info" %} -Notice how we are able to mix matchers in the same `handle_message` method. -{% endhint %} diff --git a/docs/controllers/handle_message/string-mather.md b/docs/controllers/handle_message/string-mather.md deleted file mode 100644 index 5268407e..00000000 --- a/docs/controllers/handle_message/string-mather.md +++ /dev/null @@ -1,23 +0,0 @@ -# String Matcher - -The string matcher matches exact string responses. It will however automatically ignore case and also ignore blank padding preceding and trailing a string. These blank spaces occur frequently with texting apps and autocomplete. - -## Example - -```ruby -def get_response - handle_message( - 'Sure' => proc { - current_user.update_attributes!(interested: true) - step_to state: :say_yes - }, - 'Nope' => proc { step_to state: :say_no_problem } - ) -end -``` - -In this example, if a user types in `SURE` or `SuRE` or `sure`, it will match the first match arm and the corresponding proc will be executed. - -{% hint style="warning" %} -If none of the match expressions are matched, Stealth will raise a `Stealth::Errors::UnrecognizedMessage` exception unless the [nil matcher](nil-matcher.md) is included. -{% endhint %} diff --git a/docs/controllers/interrupt-detection.md b/docs/controllers/interrupt-detection.md deleted file mode 100644 index 8d615000..00000000 --- a/docs/controllers/interrupt-detection.md +++ /dev/null @@ -1,2 +0,0 @@ -# Interrupt Detection - diff --git a/docs/controllers/platform-errors.md b/docs/controllers/platform-errors.md deleted file mode 100644 index 10a18bb7..00000000 --- a/docs/controllers/platform-errors.md +++ /dev/null @@ -1,2 +0,0 @@ -# Platform Errors - diff --git a/docs/controllers/route.md b/docs/controllers/route.md deleted file mode 100644 index 6fac1924..00000000 --- a/docs/controllers/route.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -description: Describes the route method in BotController. ---- - -# route - -The `route` method in `BotController` is the primary entry point for messages into your bot. This method is left open for you to edit in order for you to customize that process. Here is the method after having been generated: - -```ruby -def route - if current_message.payload.present? - handle_payloads - # Clear out the payload to prevent duplicate handling - current_message.payload = nil - return - end - - # Allow devs to jump around flows and states by typing: - # /flow_name/state_name or - # /flow_name (jumps to first state) or - # //state_name (jumps to state in current flow) - # (only works for bots in development) - return if dev_jump_detected? - - if current_session.present? - step_to session: current_session - else - step_to flow: 'hello', state: 'say_hello' - end -end -``` - -The method, by default, performs three tasks: - -1. Handles payloads. -2. Handles [Dev Jumps](dev-jumps.md). -3. Routes a user to their existing location based on their session or starts a new session if one does not already exist. - -We'll cover 1 and 3 in more detail below. You can learn more about Dev Jumps via the [Dev Jumps docs](dev-jumps.md). - -### Payloads - -Payloads are used to handle things like buttons in Facebook Messenger. On other platforms, like SMS and Whatsapp, payloads might be global keywords your bot is configured to support. - -Payloads have to be handled globally because a button may be tapped (or keyword typed in) at any point during a conversation. Your bot, therefore, needs to be able to handle these in any flow and state. - -Line 2 in the code sample above checks if the payload field of a message is present, and if so, calls the `handle_payloads` method. Here is that method: - -```ruby -# Handle payloads globally since payload buttons remain in the chat -# and we cannot guess in which states they will be tapped. -def handle_payloads - case current_message.payload - when 'developer_restart', 'new_user' - step_to flow: 'hello', state: 'say_hello' - when 'goodbye' - step_to flow: 'goodbye' - end -end -``` - -By setting the payload value of a Facebook Messenger button to `developer_restart`, for example, you can trigger the conversation to restart. - -{% hint style="info" %} -More information for handling global SMS or Whatsapp keywords and button payloads can be found in the respective documentation for each message platform component. -{% endhint %} - -### Routing Based on Session - -In the first code sample, Lines 16-20 handle routing a user to their existing session or starting a new one. For users with an existing session, you'll likely want to keep that code the same. For users without session, you may want to customize the starting flow. diff --git a/docs/controllers/sessions/README.md b/docs/controllers/sessions/README.md deleted file mode 100644 index fd8b004c..00000000 --- a/docs/controllers/sessions/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# Sessions - diff --git a/docs/controllers/sessions/do_nothing.md b/docs/controllers/sessions/do_nothing.md deleted file mode 100644 index 5bded025..00000000 --- a/docs/controllers/sessions/do_nothing.md +++ /dev/null @@ -1,17 +0,0 @@ -# do\_nothing - -This method is available for overriding the default behavior of Stealth which fires a [CatchAll](../catch-alls.md) in cases where a controller action fails to the update a session or send replies. See the documentation on [failing to progress a user](../controller-overview.md#failing-to-progress-a-user) for more information. - -It's primarily used in states that "trap" the user (like your bot's last state or the last level of your `catch_all` flow). - -It's usage is straightforward: - -```ruby -def - do_nothing -end -``` - -{% hint style="info" %} -You might also use `do_nothing` within an if/else block. -{% endhint %} diff --git a/docs/controllers/sessions/intro.md b/docs/controllers/sessions/intro.md deleted file mode 100644 index 8af3ecb7..00000000 --- a/docs/controllers/sessions/intro.md +++ /dev/null @@ -1,37 +0,0 @@ -# Session Overview - -Sessions in Stealth allow your bot to maintain a state for each user. If you come from the web development world, they are very similar to HTTP sessions. If you don't come from that world, no worries, we'll explain how sessions work without needing to know. If you haven't yet read the primer on [Flows and States](../../flows/overview.md), we recommend you do that first. - -### How are sessions stored? - -Sessions in Stealth are backed by Redis. Each user interacting with your bot has a unique ID assigned by the messaging platform that identifies them. With Facebook Messenger, it's a page-scoped ID (PSID). With SMS and Whatsapp it's a phone number. These unique IDs are used as the key in Redis. - -Regardless of what it is, it allows Stealth to find and load a user's session each time a message is received. - -#### Session Slugs - -With the unique messaging platform ID used as the Redis key, the value is the session slug. This is a canonical string that represents a user's current flow and state. A session slug looks like this: `flow->state`. So if a user's session is currently pointing to the `hello` flow and the `say_hola` state, then the slug would be `hello->say_hola`. - -{% hint style="info" %} -If a user has not interacted with your bot before, the key will therefore be `nil` indicating there is no session for the user. -{% endhint %} - -#### Session Expiration - -By default, sessions do not expire. This is however configurable as a [setting](../../config/settings.md). - -{% hint style="warning" %} -If your bot sends re-engagements, make sure your session's expiration is set to be _after_ the last re-engagement message is sent. -{% endhint %} - -#### Previous Session - -In addition to storing a user's current session, Stealth also automatically maintains a copy of the previous session. This allows you to send a user "back" from a catch-all scenario. - -{% hint style="info" %} -Previous sessions also expire at the same time as primary sessions. -{% endhint %} - -#### State Transitions - -State transitions are performed via [step\_to](step\_to.md), [step\_to\_in](step\_to\_in.md), [step\_to\_at](step\_to\_at.md), [step\_back](step\_back.md), and [update\_session\_to](update\_session\_to.md). You can also manually manipulate the key in Redis, but these are currently the only ways to alter a user's session via Stealth. diff --git a/docs/controllers/sessions/step_back.md b/docs/controllers/sessions/step_back.md deleted file mode 100644 index c97c57f2..00000000 --- a/docs/controllers/sessions/step_back.md +++ /dev/null @@ -1,88 +0,0 @@ -# step\_back - -Used in conjunction with `set_back_to`, `step_back` will step the user to the flow and state specified by `set_back_to`. This is a useful feature when you're building shared flows. Like for example, if you ask a user for their name in multiple places throughout your bot. This allows you to keep your flows [DRY](https://en.m.wikipedia.org/wiki/Don't\_repeat\_yourself). - -{% hint style="warning" %} -If a `back_to` session has not been set before `step_back` is called, Stealth will raise a `Stealth::Errors::InvalidStateTransition` exception. -{% endhint %} - -## set\_back\_to - -`set_back_to` serves a similar purpose as [previous\_session](intro.md#previous-session), however, instead of being automatically set by Stealth, `set_back_to` is user configurable. - -It takes the same parameters as `step_to`: - -```ruby -set_back_to state: 'say_hello' -set_back_to flow: 'hello', state: 'say_hello' -set_back_to session: previous_session -``` - -All three commands above are valid and will store the `back_to` session. - -{% hint style="info" %} -`back_to` sessions also expire along with the primary session and previous session (if an expiration has been set). -{% endhint %} - -## Example - -```ruby -class DataHelpersController < BotController - - def ask_for_email_address - send_replies - update_session_to state: :get_email_address - end - - def get_email_address - unless message_is_a_valid_email? - step_to state: :say_invalid_email - return - end - - current_user.store(email: current_message.message) - - step_back - end - - def say_invalid_email - send_replies - update_session_to state: :get_email_address - end - -end - - -class QuestionsController < BotController - - def ask_if_interested - send_replies - update_session_to state: :get_interest_response - end - - def get_interest_response - handle_message( - :yes => proc { - set_back_to state: :say_thanks - step_to flow: :data_helper, state: :ask_for_email_address - }, - :no => proc { - step_to state: :say_no_worries - } - ) - end - - def say_thanks - send_replies - end - - def say_no_worries - send_replies - end - -end -``` - -In the above example, we have two flows/controllers: `data_helper` and `question`. The `DataHelpersController` contains a few states for asking for an email address for the user and verifying that it looks like a legit email address. Setting it up this way allows any of our other controllers to also ask for an email address without having to duplicate these states. - -On Lines 37-38, you see that we set the `back_to` session to be the `say_thanks` state. Then we step to the `data_helper` state directly via `step_to`. From the `DataHelpersController` we can continue to ask questions and update the states as needed like normal. Once we've collected the email address, we send the user "back" to the `back_to` session by calling `step_back` on Line 16. diff --git a/docs/controllers/sessions/step_to.md b/docs/controllers/sessions/step_to.md deleted file mode 100644 index c41a5f1b..00000000 --- a/docs/controllers/sessions/step_to.md +++ /dev/null @@ -1,47 +0,0 @@ -# step\_to - -The `step_to` method is used to update the session and immediately move the user to the specified flow and state. `step_to` can accept a _flow_, a _state_, or both. `step_to` is often used after a `say` action where the next action typically doesn't require user input. - -{% hint style="warning" %} -If the flow and/or action specified in the `step_to` is not declared in the [FlowMap](../../flows/flowmap.md), Stealth will raise an exception. -{% endhint %} - -## Flow Example - -```ruby -step_to flow: 'hello' -``` - -Sets the session's flow to `hello` and the state will be set to the **first** state in that flow (as defined by the [FlowMap](../../flows/flowmap.md)). The corresponding controller action in the `HellosController` will also be immediately called. - -{% hint style="info" %} -The flow name can also be specified as a symbol. -{% endhint %} - -## State Example - -```ruby -step_to state: 'say_hello' -``` - -Sets the session's state to `say_hello` and keeps the flow the same. The `say_hello` controller action will also be immediately called. - -{% hint style="info" %} -The state name can also be specified as a symbol. -{% endhint %} - -## Flow and State Example - -```ruby -step_to flow: :hello, state: :say_hello -``` - -Sets the session's flow to `hello` and the state to `say_hello`. The `say_hello` controller action of the `HellosController` controller will also be immediately called. - -## Session Example - -```ruby -step_to session: previous_session -``` - -Sets the session to the `previous_session` and immediately calls the respective controller action. This is useful for sending a user "back". diff --git a/docs/controllers/sessions/step_to_at.md b/docs/controllers/sessions/step_to_at.md deleted file mode 100644 index 92a5d66a..00000000 --- a/docs/controllers/sessions/step_to_at.md +++ /dev/null @@ -1,43 +0,0 @@ -# step\_to\_at - -The `step_to_at` method is used to update the session and move the user to the specified flow and state **at the specified date and time**. `step_to_at` can accept a _flow_, a _state_, or both. `step_to_at` is often used as a tool for re-engaging a user at a specific time. - -{% hint style="info" %} -The session will only be updated and the controller action called **at the time specified**. -{% endhint %} - -{% hint style="warning" %} -If the flow and/or action specified in the `step_to_at` is not declared in the [FlowMap](../../flows/flowmap.md) (at the specified date and time), Stealth will raise an exception. -{% endhint %} - -## Flow Example - -```ruby -step_to_at Time.now.next_week, flow: 'hello' -``` - -At the specified time (next week in this case), Stealth will set the session's flow to `hello` and the state will be set to the **first** state in that flow (as defined by the [FlowMap](../../flows/flowmap.md)). The corresponding controller action in the `HellosController` will also be called. - -{% hint style="info" %} -The flow name can also be specified as a symbol. -{% endhint %} - -## State Example - -```ruby -step_to_at Time.now.next_week, state: 'say_hello' -``` - -At the specified time (next week in this case), Stealth will set the session's state to `say_hello` and keeps the flow the same. The `say_hello` controller action will also be called. - -{% hint style="info" %} -The state name can also be specified as a symbol. -{% endhint %} - -## Flow and State Example - -```ruby -step_to_at Time.now.next_week, flow: :hello, state: :say_hello -``` - -At the specified time (next week in this case), Stealth will set the session's flow to `hello` and the state to `say_hello`. The `say_hello` controller action of the `HellosController` controller will also be called. diff --git a/docs/controllers/sessions/step_to_in.md b/docs/controllers/sessions/step_to_in.md deleted file mode 100644 index e07c3d56..00000000 --- a/docs/controllers/sessions/step_to_in.md +++ /dev/null @@ -1,43 +0,0 @@ -# step\_to\_in - -The `step_to_in` method is used to update the session and move the user to the specified flow and state **after a specified duration**. `step_to_in` can accept a _flow_, a _state_, or both. `step_to_in` is often used as a tool for re-engaging a user after a specified duration. - -{% hint style="info" %} -The session will only be updated and the controller action called **after the specified duration has elapsed**. -{% endhint %} - -{% hint style="warning" %} -If the flow and/or action specified in the `step_to_in` is not declared in the [FlowMap](../../flows/flowmap.md) (after the specified duration), Stealth will raise an exception. -{% endhint %} - -## Flow Example - -```ruby -step_to_in 8.hours, flow: 'hello' -``` - -After the specified duration (8 hours in this case), Stealth will set the session's flow to `hello` and the state will be set to the **first** state in that flow (as defined by the [FlowMap](../../flows/flowmap.md)). The corresponding controller action in the `HellosController` will also be called. - -{% hint style="info" %} -The flow name can also be specified as a symbol. -{% endhint %} - -## State Example - -```ruby -step_to_in 8.hours, state: 'say_hello' -``` - -After the specified duration (8 hours in this case), Stealth will set the session's state to `say_hello` and keeps the flow the same. The `say_hello` controller action will also be called. - -{% hint style="info" %} -The state name can also be specified as a symbol. -{% endhint %} - -## Flow and State Example - -```ruby -step_to_in 8.hours, flow: :hello, state: :say_hello -``` - -After the specified duration (8 hours in this case), Stealth will set the session's flow to `hello` and the state to `say_hello`. The `say_hello` controller action of the `HellosController` controller will also be called. diff --git a/docs/controllers/sessions/update_session_to.md b/docs/controllers/sessions/update_session_to.md deleted file mode 100644 index a8282cc2..00000000 --- a/docs/controllers/sessions/update_session_to.md +++ /dev/null @@ -1,47 +0,0 @@ -# update\_session\_to - -Similar to [step\_to](step\_to.md), `update_session_to` is used to update the user's session to a flow and state. It also accepts the same arguments. However, `update_session_to` does not immediately call the respective controller action. `update_session_to` is typically used after an `ask` action where the next action is waiting for user input. It allows you to set the state that will be responsible for handling that user input, like a `get` action. - -{% hint style="warning" %} -If the flow and/or action specified in the `update_session_to` is not declared in the [FlowMap](../../flows/flowmap.md), Stealth will raise an exception. -{% endhint %} - -## Flow Example - -```ruby -update_session_to flow: 'hello' -``` - -Sets the session's flow to `hello` and the state will be set to the **first** state in that flow (as defined by the [FlowMap](../../flows/flowmap.md)). The corresponding controller action in the `HellosController` will **not** be called. - -{% hint style="info" %} -The flow name can also be specified as a symbol. -{% endhint %} - -## State Example - -```ruby -update_session_to state: 'get_hello_response' -``` - -Sets the session's state to `get_hello_response` and keeps the flow the same. The `get_hello_response` controller action will **not** called. - -{% hint style="info" %} -The state name can also be specified as a symbol. -{% endhint %} - -## Flow and State Example - -```ruby -step_to flow: :hello, state: :say_hello -``` - -Sets the session's flow to `hello` and the state to `say_hello`. The `say_hello` controller action of the `HellosController` controller will also be immediately called. - -## Session Example - -```ruby -update_session_to session: previous_session -``` - -Sets the session to the `previous_session` and but does **not** call the respective controller action. This is useful for updating a user's session to the previous value. diff --git a/docs/controllers/unrecognized-messages.md b/docs/controllers/unrecognized-messages.md deleted file mode 100644 index 25134419..00000000 --- a/docs/controllers/unrecognized-messages.md +++ /dev/null @@ -1,2 +0,0 @@ -# Unrecognized Messages - diff --git a/docs/deployment/heroku.md b/docs/deployment/heroku.md deleted file mode 100644 index 832757ae..00000000 --- a/docs/deployment/heroku.md +++ /dev/null @@ -1,2 +0,0 @@ -# Heroku - diff --git a/docs/deployment/overview.md b/docs/deployment/overview.md deleted file mode 100644 index 62a4602b..00000000 --- a/docs/deployment/overview.md +++ /dev/null @@ -1,2 +0,0 @@ -# Deployment Overview - diff --git a/docs/dev-environment/README.md b/docs/dev-environment/README.md deleted file mode 100644 index 7dbb16dc..00000000 --- a/docs/dev-environment/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# Dev Environment - diff --git a/docs/dev-environment/booting-up.md b/docs/dev-environment/booting-up.md deleted file mode 100644 index 8a3525ca..00000000 --- a/docs/dev-environment/booting-up.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -description: Steps for booting up your bot. ---- - -# Booting Up - -While you can use Docker or anything else to boot up your bot, there is a built-in command that utilizes [foreman](https://github.com/ddollar/foreman) to ensure your _web server_ and your _background job processor_ both boot up. If you boot only the web server bot the the background job processor, your bot will receive message but will fail to reply. - -For more info about the different process types, check out [Anatomy of a Stealth bot](../basics.md#anatomy-of-a-stealth-bot). - -### Install Gems - -``` -bundle install -``` - -### Boot Your Bot - -``` -stealth s -``` - -Or for the full command: - -``` -stealth server -``` - -### Tunnels to Localhost - -After you boot your server, you'll likely want to use a service to create a tunnel to your localhost. This allows message platform like Facebook Messenger and Whatsapp to deliver messages to your laptop. - -Check out the [docs for creating your own tunnel](tunnels.md). diff --git a/docs/dev-environment/environment-variables.md b/docs/dev-environment/environment-variables.md deleted file mode 100644 index 936f3bb9..00000000 --- a/docs/dev-environment/environment-variables.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -description: Tips for streamlining your environment variable management during development. ---- - -# Environment Variables - -Environment variables (ENV Vars) will likely be an important part of your configuration. Each message platform component, NLP component, and others will likely require one or more API keys. - -Most production environments will provide a way for you to specify your production keys. But for development, we recommend using the [dotenv](https://github.com/bkeepers/dotenv) gem. This gem will allow you to specify a `.env` file in your bot repo from where you can set all of your environment variables. - -{% hint style="info" %} -Stealth will automatically exclude the `.env` file from git. -{% endhint %} - -### Configuring dotenv in Stealth - -Add the [dotenv](https://github.com/bkeepers/dotenv) gem to your `Gemfile`: - -```ruby -group :development do - gem 'foreman' - gem 'listen', '~> 3.3' - gem 'dotenv' -end -``` - -Install the gem: - -```ruby -bundle install -``` - -Load dotenv on boot via `boot.rb`: - -```ruby -require 'stealth/base' -if %w(development test).include?(Stealth.env) - require 'dotenv/load' -end -require_relative './environment' -``` - -{% hint style="info" %} -You'll be adding Lines 2-4 right below Line 1 which will already be there. -{% endhint %} - -That's it! Now you can specify your environment variables via the `.env` file: - -``` -FACEBOOK_VERIFY_TOKEN=some_value -LUIS_APP_ID=1234 -LUIS_ENDPOINT=your_endpoint.cognitiveservices.azure.com -LUIS_SUBSCRIPTION_KEY=xyz1234 -``` diff --git a/docs/dev-environment/hot-code-reloading.md b/docs/dev-environment/hot-code-reloading.md deleted file mode 100644 index 5c2d96cf..00000000 --- a/docs/dev-environment/hot-code-reloading.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -description: ✨✨✨ ---- - -# Hot-Code Reloading - -Hot-code reloading is one of the more enjoyable features of using Stealth to create your bots. As you make changes to your bot, your source code is automatically loaded into memory without having to stop and start your bot! - -There is nothing you need to turn on to start using this feature. As long as you are in the `development` [environment](../basics.md#environments). You may however wish to customize which files are watched for changes as you add your own custom directories, service objects, etc. - -## Customizing Hot-Reload Paths - -By default, these are paths and files that Stealth will watch for changes: - -``` -bot/controllers/conerns/*.* -bot/controllers/*.* -bot/models/concerns/*.* -bot/models/*.* -bot/helpers/*.* -config/*.* -``` - -In addition to your Ruby code in these directories, all reply files are automatically included since Stealth reads their contents for each message. - -### Adding a File or Path to Watch - -As you add your own directories to your bot, you'll want to add them to your `autoload_path` so that they can be hot-reloaded while in `development`. - -{% hint style="info" %} -In `production`Stealth will pre-load all files in the `autoload_paths` array to improve performance. -{% endhint %} - -#### Adding a Single File - -To add the file `lib/some_file.rb` to the `autoload_path`, add this line to `config/autoload.rb`: - -```ruby -Stealth.config.autoload_paths << File.join(Stealth.root, 'lib', 'some_file') -``` - -{% hint style="warning" %} -Don't include the `.rb` extension to the filename. -{% endhint %} - -#### Adding a Directory - -To add the entire `lib` directory to the `autoload_path`, add this line to `config/autoload.rb`: - -```ruby -Stealth.config.autoload_paths << File.join(Stealth.root, 'lib') -``` diff --git a/docs/dev-environment/logs.md b/docs/dev-environment/logs.md deleted file mode 100644 index 9192af86..00000000 --- a/docs/dev-environment/logs.md +++ /dev/null @@ -1,74 +0,0 @@ -# Logs - -Logs are the primary visibility mechanism into your bot. Stealth logs are designed to help you debug during development and also in production. - -Stealth logs all events to `stdout` instead of a log file. This avoids having to routinely clean your log files and also mimics typical production deployments. - -### Interpreting Logs - -Here are some sample log entries from the [Facebook Messenger](../platforms/facebook-messenger.md) platform: - -``` -Dec 28 18:31:33 sidekiq.1 info pid=4 tid=590 class=Stealth::Services::HandleMessageJob jid=b08b6721a327c72aa5baa09f INFO: start -Dec 28 18:31:33 sidekiq.1 info TID-58w [user] User 3772279279459415 -> Received Message: What is Stealth? -Dec 28 18:31:33 sidekiq.1 info TID-58w [facebook] Requested user profile for 3772279279459415. Response: 200: {"id":"3772279279459415","name":"Leroy Jenkins","first_name":"Leroy","last_name":"Jenkins","profile_pic":"https:\/\/picsum.photos\/400"} -Dec 28 18:31:33 sidekiq.1 info TID-58w [primary_session] User 3772279279459415: setting session to hellos->say_hello -Dec 28 18:31:33 sidekiq.1 info TID-58w [previous_session] User 3772279279459415: setting to -Dec 28 18:31:33 web.1 info TID-5hs [facebook] Received webhook. -Dec 28 18:31:34 sidekiq.1 info TID-58w [facebook] Transmitted. Response: 200: {"recipient_id":"3772279279459415"} -Dec 28 18:31:34 sidekiq.1 info TID-58w [facebook] User 3772279279459415 -> Sending: -Dec 28 18:31:37 sidekiq.1 info TID-58w [facebook] Transmitted. Response: 200: {"recipient_id":"3772279279459415","message_id":"m_aukUQrqKbnxqm9FT6nBdZuuz3dIn4BVeTi4JX4XD8lJ2fSuuNXpN7SZkouaoC7SRrdrSDsqF--2Gi0KFZHkFNg"} -Dec 28 18:31:37 sidekiq.1 info TID-58w [facebook] User 3772279279459415 -> Sending: Hey Leroy, 👋 welcome to Stealth! -Dec 28 18:31:37 sidekiq.1 info TID-58w [facebook] Transmitted. Response: 200: {"recipient_id":"3772279279459415"} -Dec 28 18:31:37 sidekiq.1 info TID-58w [facebook] User 3772279279459415 -> Sending: -Dec 28 18:31:42 sidekiq.1 info TID-58w [facebook] Transmitted. Response: 200: {"recipient_id":"3772279279459415","message_id":"m_5Dm01zkLp0Fj44QhvWYpq-uz3dIn4BVeTi4JX4XD8lLRkdqJ3g0yw0RA30Cpb06rkJPfDNGYs6BCV769e-nb7Q"} -Dec 28 18:31:42 sidekiq.1 info TID-58w [facebook] User 3772279279459415 -> Sending: Stealth is one of the fastest ways to create a bot. -Dec 28 18:31:42 sidekiq.1 info TID-58w [facebook] Transmitted. Response: 200: {"recipient_id":"3772279279459415"} -Dec 28 18:31:42 sidekiq.1 info TID-58w [facebook] User 3772279279459415 -> Sending: -Dec 28 18:31:49 sidekiq.1 info TID-58w [facebook] Transmitted. Response: 200: {"recipient_id":"3772279279459415","message_id":"m_s9FICmzHaIi2hEVm2NDDvOuz3dIn4BVeTi4JX4XD8lJlfF98sBAAIos0GgjxrV4tNvIxq9_MW9qqPTO45e6gIA"} -Dec 28 18:31:49 sidekiq.1 info TID-58w [facebook] User 3772279279459415 -> Sending: Ready to get started? -Dec 28 18:31:49 sidekiq.1 info TID-58w [primary_session] User 3772279279459415: setting session to hellos->get_hello_response -Dec 28 18:31:49 sidekiq.1 info TID-58w [previous_session] User 3772279279459415: setting to hellos->say_hello -``` - -{% hint style="info" %} -Transcript logging is enabled for the sample entries above. -{% endhint %} - -#### Session Updates - -Each time Stealth changes the session for a user, the `previous_session` is stored as well. On Line 5 above, you can see the user does not have a previous session (`nil`) and so on Line 4 they are being sent to `hellos->say_hello`. - -After the replies are sent, you can see the session is updated again on Line 19 to `hellos->get_hello_response`. This time on Line 20 you can see there is a previous session to set and so we do. - -For more info about sessions and why Stealth stores the previous session, check out the [Session docs](../controllers/sessions/intro.md). - -#### Thread IDs - -Often times in production, you'll see a lot of entries all interlaced together depending on your bot's load. In the example above, there isn't a lot going on, but nonetheless you can see a couple different threads logging their events. - -Thread IDs begin with the prefix `TID-` followed by an alphanumeric string. Threads that have the same ID are the same thread. So in the example above, `TID-58w` are all the same background job started on Line 2. Following this thread ID will allow us to follow all the steps taken by this background job without getting confused by unrelated events. - -#### Event Topics - -After the thread ID, you can see the event topic wrapped in brackets (`[facebook]`). These will be color coded in your console. Session change events, component events, user events, etc are all labeled accordingly. - -#### Finding Events For a User - -Each Stealth component is designed to output the user's `session_id` where applicable. In the example above, you can see each Facebook entry is prefixed with `User 3772279279459415`. - -In production this would allow you to search for the user's ID (`3772279279459415`) and you would be able to see all events for the user. This is really helpful for debugging. - -### Transcript Logging - -In order to see what your users type, in addition to what your bot sends out, you'll need to enable the transcript logging config setting. Check out the [docs for config settings](../config/settings.md). - -### Logging Custom Events - -If you want to add your own custom events to the log stream, you can use the Stealth`::Logger` class to log those events. This ensures your events will appear formatted like the stock Stealth events. The API for event logging is: - -```ruby -Stealth::Logger.l(topic: 'your_topic', message: 'Your message.') -``` - -The `topic` can be any topic of your choosing and `message` is the string you want to log. If available, you'll want to include the user's `session_id` in the `message`. This will help you tail your logs for events related to particular user. diff --git a/docs/dev-environment/procfile.md b/docs/dev-environment/procfile.md deleted file mode 100644 index 826cb11a..00000000 --- a/docs/dev-environment/procfile.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -description: Examining the contents of the Procfile ---- - -# Procfile - -If you used the generator to instantiate your bot, you'll find a `Procfile.dev` in the root directory of your bot. Here are the contents: - -``` -web: bundle exec puma -C config/puma.rb -sidekiq: bundle exec sidekiq -C config/sidekiq.yml -q stealth_webhooks -q stealth_replies -r ./config/boot.rb -``` - -If you're not familiar with Procfiles, each line specifies a process name and command for the process. So in this default Procfile, we've specified 2 processes, `web` and `sidekiq`. - -### Sidekiq - -Currently, Stealth uses Sidekiq for processing background jobs. It is configured to monitor 2 queues by default: `stealth_webhooks` and `stealth_replies`. You can specify additional queues as needed by your bot by adding `-q ` to the `sidekiq` Procfile entry. - -{% hint style="success" %} -You can even use Sidekiq Pro and Sidekiq Enterprise if you have a license -{% endhint %} diff --git a/docs/dev-environment/tunnels.md b/docs/dev-environment/tunnels.md deleted file mode 100644 index 0303309b..00000000 --- a/docs/dev-environment/tunnels.md +++ /dev/null @@ -1,18 +0,0 @@ -# Tunnels - -When developing locally, message platforms require access to the Stealth server running on your machine in order to transmit user messages. - -Here are some options you can use: - -### ngrok - -1. Download [ngrok](https://ngrok.com/download) -2. Start your Stealth server as detailed in [Booting Up](booting-up.md#boot-your-bot). -3. Open up an ngrok tunnel to your Stealth server and port (default 5000) like this: `ngrok http 5000`. ngrok will output a unique ngrok local tunnel URL to your machine. - -When you provide your local ngrok URL to a messaging service, you will have to add `/incoming/`. For example: - -* `https://abc1234.ngrok.io/incoming/facebook` -* `https://abc1234.ngrok.io/incoming/twilio` - -More details on service specific settings can be found on the GitHub page for each service gem. diff --git a/docs/flows/flowmap.md b/docs/flows/flowmap.md deleted file mode 100644 index 136f905f..00000000 --- a/docs/flows/flowmap.md +++ /dev/null @@ -1,40 +0,0 @@ -# FlowMap - -The `FlowMap` is the file that contains your flow and state declarations. It's stored in `config/flow_map.rb` and will be generated by Stealth when you instantiate your bot. - -Here is a `FlowMap` similar to the one that is generated for a new bot: - -```ruby -class FlowMap - - include Stealth::Flow - - flow :hello do - state :say_hello - state :get_hello_response, fails_to: :say_hello - end - - flow :goodbye do - state :say_goodbye - end - - flow :interrupt do - state :say_interrupted - end - - flow :unrecognized_message do - state :handle_unrecognized_message - end - - flow :catch_all do - state :level1 - end - -end -``` - -In this example, we've declared five flows: `hello`, `goodbye`, `interrupt`, `unrecognized_message`, and `catch_all`. These are the default flows that are generated for you when you create a new bot. - -Each flow consists of an arbitrary number of states. All of the above flows only have a single state, but the `hello` flow has two. As you build out your bot and add functionality, you'll need to keep the `FlowMap` updated. If you attempt to transition to a flow or state that hasn't yet been declared in the `FlowMap`, you'll encounter a Stealth`::Errors::InvalidStateTransition` exception. - -States also support additional options like the `fails_to` option for the `get_hello_response` state. We'll cover these state options in the [State Option docs](state-options.md) section. diff --git a/docs/flows/overview.md b/docs/flows/overview.md deleted file mode 100644 index 8dd3b102..00000000 --- a/docs/flows/overview.md +++ /dev/null @@ -1,43 +0,0 @@ -# Flows & States - -## Overview - -Flows and states are the primary building blocks for Stealth bots. Your bot's users can only be in a single flow and state at any given moment. The relationship between flows and states is one of parent and child, respectively. So a flow can _have many_ states and a state always _belongs to_ a single flow. - -The concept is modeled after [finite-state machines](https://en.m.wikipedia.org/wiki/Finite-state\_machine), though you don't need to familiarize yourself with all of that knowledge. The outline we provide in these docs will be sufficient. - -Finite-state machines, or more simply state machines, are used throughout engineering to model states within a given machine. Imagine a coin-operated, turnstile you might find in a subway or airport. You insert a coin and the mechanism unlocks to allow you to rotate the arms and pass through. - -![Figure 1: A simple, coin-operated turnstile](../../.gitbook/assets/torniqueterevolution.jpg) - -The operation of this turnstile can (and probably is) modeled as a state machine. Here is an example of what that model looks like: - -![Figure 2: Finite-state machine model for the simple, coin-operated turnstile.](../../.gitbook/assets/2880px-turnstile\_state\_machine\_colored.svg.png) - -In Figure 2, the "starting" state is _Locked_ and if someone attempts to _Push_ the turnstile arms while it is in the _Locked_ state it will indefinitely remain in the _Locked_ state. That's what the self-referencing _Push_ action in Figure 2 is showing. Similarly, in Stealth, states can transition a user to a new state or it can keep a user in the same state either indefinitely or until some specific action is taken. - -When a user inserts a _Coin_, the state machine in Figure 2 transitions the machine to the _Unlocked_ state. If a user inserts more coins while in this state, the machine just remains in the _Unlocked_ state. When the turnstile arms are _Pushed_, then the machine transitions back to the _Locked_ state. - -This turnstile example highlights the mental model of flows and states in Stealth quite well. Specifically, states can transition your users to other states or they can keep your user in the same state. In the section about [Sessions](../controllers/sessions/), we'll cover all the ways these transitions can happen. - -## Flows - -A **flow** is the term used to describe a complete interaction between a user and the bot. Flows are comprised of `states`, like a finite state machine. In Figure 2 above, the entire finite-state machine is the flow. - -For example, if a user is using your bot to receive an insurance quote, the flow might be named `quote`. - -{% hint style="warning" %} -Stealth requires that flows be named in the singular form, like Ruby on Rails. -{% endhint %} - -A flow consists of the following components: - -1. A controller file, named in the plural form. For example, a `quote` flow would have a corresponding `QuotesController`. One controller maps to one flow. -2. Replies. Each flow will have a directory in the `replies` directory in plural form. Again using the `quote` flow example, the directory would named `quotes`. -3. An entry in the `FlowMap`. The `FlowMap` file is where each flow and it's respective states are defined. We'll cover the FlowMap file in [FlowMap docs](flowmap.md) section. - -## States - -A **state** is the logical division of flows. Just like in finite-state machines, users can transition between states. In Stealth, users can even transition between states from different flows. There are no naming conventions enforced by Stealth for states, but in [State Naming section](state-naming.md) we'll cover some best practices. - -As mentioned in the above, a user can at most be in a single flow and state at any given moment. diff --git a/docs/flows/state-naming.md b/docs/flows/state-naming.md deleted file mode 100644 index b1b1ddc1..00000000 --- a/docs/flows/state-naming.md +++ /dev/null @@ -1,53 +0,0 @@ -# State Naming - -While Stealth doesn't enforce any naming requirements for your states, we do recommend following the naming conventions outlined below. It provides continuity across your team and across bots. - -Most of your states will fall into the `say`, `get`, and `ask` buckets. On the rare occasion that it does not, feel free to select a name that best describes the state. - -## Say, Ask, Get - -#### Say - -_Say_ actions are for _saying_ something to the user. - -For example: - -```ruby -def say_hello - send_replies -end -``` - -Typically we'd send the user to a new state, but sometimes it's as simple as just saying something like in the case of the end of a flow or conversation. - -#### Ask - -_Ask_ actions are for _asking_ something from the user. - -For example: - -```ruby -def ask_weather - send_replies - update_session_to state: 'get_weather_response' -end -``` - -In the above example, we've asked a question via `send_replies` and we've updated the session to a new state. This is the state that will be receiving the response. We'll cover state transitions in detail in the [Sessions Overview](../controllers/sessions/intro.md) section. - -#### Get - -_Get_ actions are for _getting_ and parsing a message from the user. - -For example: - -```ruby -def get_weather_response - handle_message( - 'Sunny' => proc { step_to state: 'say_wear_sunglasses' }, - 'Raining' => proc { step_to state: 'say_dont_forget_umbrella' } - ) -end -``` - -In the example above, we're handling two responses by the user. When they say "Sunny" or "Raining". Don't worry too much about the format of `handle_message`. We cover its usage in the [handle\_message docs](../controllers/handle\_message/) section. diff --git a/docs/flows/state-options.md b/docs/flows/state-options.md deleted file mode 100644 index 941a50f1..00000000 --- a/docs/flows/state-options.md +++ /dev/null @@ -1,70 +0,0 @@ -# State Options - -In your `FlowMap`, each state may also specify certain options. Some options expose built-in Stealth functionality, while others are completely custom and can be referenced by your code. - -```ruby -class FlowMap - - include Stealth::Flow - - flow :hello do - state :say_hello - state :get_hello_response, fails_to: :say_hello - state :say_hola, redirects_to: :say_hello - end - - flow :goodbye do - state :say_goodbye, re_engage: false - end - - flow :interrupt do - state :say_interrupted - end - - flow :unrecognized_message do - state :handle_unrecognized_message - end - - flow :catch_all do - state :level1 - end - -end -``` - -We see three states have options defined: `get_hello_response`, `say_hola`, and `say_goodbye`. - -#### fails\_to - -The `fails_to` option is one of the built-in Stealth state options. By default, it's used in the `CatchAllsController` to specify where a user should be sent in the event of an error. We cover this more in the [CatchAll docs](../controllers/catch-alls.md), but in the `get_hello_response` state above, if Stealth encounters an error the `fails_to` option declares the user to be sent to the `say_hello` state of the same flow. - -The `fails_to` value can also be a string if you wish to specify a different flow. So for example: - -```ruby -state :get_hello_response, fails_to: 'goodbye->say_goodbye' -``` - -If Stealth encounters an error in this state, it will be sent to the `say_goodbye` state of the `goodbye` flow. - -#### redirects\_to - -The `redirects_to` option is useful when you're performing a rename of a state and the bot has already been deployed to production. Your production users may have existing sessions attached to the state you are renaming. If you were to perform a state rename without attaching a `redirects_to` to the old state name, the user will receive an error the next time they message your bot. - -{% hint style="info" %} -For the `redirects_to`values, you can use state names as well as the "flow->state\_name" convention like in `fails_to`. -{% endhint %} - -#### Custom Options - -In addition to the built-in Stealth state options, you are able to define your own. This is helpful for cases where you want to define metadata for a set of states but don't want to define that logic within the controllers. - -In the example `FlowMap` above, we've defined a `re_engage` option on the `say_goodbye` state. If we pretend our bot re-engages leads after a period of time, this option would be useful for allowing us to declare states for which we do not want re-engagements to be sent. In this case, the user has reached the end of the bot and so we don't want to send them any re-engagements. - -You can access these custom state options via the `opts` attribute for the state specification. - -```ruby -state_spec = FlowMap.flow_spec[:goodbye].states[:say_goodbye] -state_spec.opts.present? && state_spec.opts[:re_engage] -``` - -Here `state_spec.opts[:re_engage]` contains the value `true`. The hash key will correspond to what you named your option in the `FlowMap`. diff --git a/docs/getting-started.md b/docs/getting-started.md deleted file mode 100644 index 860c6573..00000000 --- a/docs/getting-started.md +++ /dev/null @@ -1,19 +0,0 @@ -# Getting Started - -Stealth is designed to run on Ruby (MRI) 2.5+ - -While we don't require any C-based Ruby gems, we haven't yet certified Stealth on other VMs (such as JRuby). However, we do intend to provide official support for JRuby and TruffleRuby soon. - -### Installation - -You can install Stealth via RubyGems: - -```ruby -gem install stealth -``` - -Next, create your new bot: - -``` -stealth new -``` diff --git a/docs/glossary.md b/docs/glossary.md deleted file mode 100644 index ab3986d5..00000000 --- a/docs/glossary.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -description: >- - Some common terms you may read throughout this doc or as you interact with the - Stealth community. ---- - -# Glossary - -* **message** - An _incoming_ message from a user. A _message_ and a _reply_ are counterparts. -* **reply** - An _outgoing_ message from your bot. A _message_ and a _reply_ are counterparts. -* **service message** - This is the long version of _message_. You will likely only see this referenced when developing your own Stealth components. -* **`current_message`** - This is object that contains the service message. It's available within all controller actions. More info can be found in the [controller docs](controllers/controller-overview.md). -* **component** - Components are the individual building blocks of Stealth. Stealth itself is the core framework that handles webhooks, replies, etc. Components allow Stealth to connect to messaging platforms, NLP providers, and more. Each component is offered as a separate Ruby gem. -* **message platform** - Message platforms are the platforms where your bot interacts with its users. E.g., Facebook Messenger, SMS, Whatsapp, Slack, etc. -* **NLP** - natural language processing. This is the AI subfield that encompasses taking unstructured text (like messages from users) and extracting structured concepts. NLP in Stealth is achieved through components. -* **NLU** - A subclass of NLP. More aptly describes the type of NLP you'll want to perform with Stealth, but NLP is the more commonly used term. -* **session** - Sessions allow your Stealth bot to recognize subsequent messages from users. It keeps track of where in the conversation each of your users currently reside. -* **MVC** - A software design pattern. It's not critical to understand this to get going, but if you're interested you can learn more [here](https://www.google.com/url?sa=t\&rct=j\&q=\&esrc=s\&source=web\&cd=\&cad=rja\&uact=8\&ved=2ahUKEwiGt\_XpzPHtAhXNVc0KHWjiDG8QFjAAegQIBRAC\&url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FModel%25E2%2580%2593view%25E2%2580%2593controller\&usg=AOvVaw1wpuCUJRz1WxG51eRibnYX). -* **intent** - **** Intents are one of the main things NLP components extract from a message. They are a type of classification. We talk about them in more detail in the `handle_message` [docs](controllers/handle\_message/nlp-matcher.md). -* **entity** - Entities are individual tokens within a message that an NLP component will extract. So for example, a number or some other trained entity specific to your bot (like car models and makes). More info about these can be found in the `get_match` [docs](controllers/get\_match/entity-match.md). -* **regex** - A regular expression. These are a programming concept used in string matching. In Stealth, these are most often used in `handle_message` and there are [docs](controllers/handle\_message/regex-matcher.md) for their usage. diff --git a/docs/models/activerecord.md b/docs/models/activerecord.md deleted file mode 100644 index d3ad9b32..00000000 --- a/docs/models/activerecord.md +++ /dev/null @@ -1,2 +0,0 @@ -# ActiveRecord - diff --git a/docs/models/mongoid.md b/docs/models/mongoid.md deleted file mode 100644 index 5841ed55..00000000 --- a/docs/models/mongoid.md +++ /dev/null @@ -1,2 +0,0 @@ -# Mongoid - diff --git a/docs/models/overview.md b/docs/models/overview.md deleted file mode 100644 index 7b904848..00000000 --- a/docs/models/overview.md +++ /dev/null @@ -1,2 +0,0 @@ -# Model Overview - diff --git a/docs/nlp-nlu/microsoft-luis.md b/docs/nlp-nlu/microsoft-luis.md deleted file mode 100644 index a438f7f0..00000000 --- a/docs/nlp-nlu/microsoft-luis.md +++ /dev/null @@ -1,2 +0,0 @@ -# Microsoft LUIS - diff --git a/docs/nlp-nlu/openai.md b/docs/nlp-nlu/openai.md deleted file mode 100644 index fdf22f6c..00000000 --- a/docs/nlp-nlu/openai.md +++ /dev/null @@ -1,2 +0,0 @@ -# OpenAI - diff --git a/docs/nlp-nlu/overview.md b/docs/nlp-nlu/overview.md deleted file mode 100644 index 443e15e8..00000000 --- a/docs/nlp-nlu/overview.md +++ /dev/null @@ -1,2 +0,0 @@ -# NLP Overview - diff --git a/docs/platforms/alexa-skills.md b/docs/platforms/alexa-skills.md deleted file mode 100644 index e3397da1..00000000 --- a/docs/platforms/alexa-skills.md +++ /dev/null @@ -1,2 +0,0 @@ -# Alexa Skills - diff --git a/docs/platforms/facebook-messenger.md b/docs/platforms/facebook-messenger.md deleted file mode 100644 index e8e26ecb..00000000 --- a/docs/platforms/facebook-messenger.md +++ /dev/null @@ -1,2 +0,0 @@ -# Facebook Messenger - diff --git a/docs/platforms/overview.md b/docs/platforms/overview.md deleted file mode 100644 index 70483484..00000000 --- a/docs/platforms/overview.md +++ /dev/null @@ -1,2 +0,0 @@ -# Platform Overview - diff --git a/docs/platforms/sms-whatsapp.md b/docs/platforms/sms-whatsapp.md deleted file mode 100644 index dec9e5c1..00000000 --- a/docs/platforms/sms-whatsapp.md +++ /dev/null @@ -1,2 +0,0 @@ -# SMS/Whatsapp - diff --git a/docs/platforms/voice.md b/docs/platforms/voice.md deleted file mode 100644 index ea4e1f82..00000000 --- a/docs/platforms/voice.md +++ /dev/null @@ -1,2 +0,0 @@ -# Voice - diff --git a/docs/replies/delays.md b/docs/replies/delays.md deleted file mode 100644 index 7188b3cc..00000000 --- a/docs/replies/delays.md +++ /dev/null @@ -1,2 +0,0 @@ -# Delays - diff --git a/docs/replies/erb.md b/docs/replies/erb.md deleted file mode 100644 index dd46817c..00000000 --- a/docs/replies/erb.md +++ /dev/null @@ -1,2 +0,0 @@ -# ERB - diff --git a/docs/replies/inline-replies.md b/docs/replies/inline-replies.md deleted file mode 100644 index 82576e3b..00000000 --- a/docs/replies/inline-replies.md +++ /dev/null @@ -1,2 +0,0 @@ -# Inline Replies - diff --git a/docs/replies/reply-overview.md b/docs/replies/reply-overview.md deleted file mode 100644 index c9c6f3f7..00000000 --- a/docs/replies/reply-overview.md +++ /dev/null @@ -1,2 +0,0 @@ -# Reply Overview - diff --git a/docs/replies/variants.md b/docs/replies/variants.md deleted file mode 100644 index ad9d1a7f..00000000 --- a/docs/replies/variants.md +++ /dev/null @@ -1,2 +0,0 @@ -# Variants - diff --git a/docs/replies/yaml-replies.md b/docs/replies/yaml-replies.md deleted file mode 100644 index 63ae7177..00000000 --- a/docs/replies/yaml-replies.md +++ /dev/null @@ -1,2 +0,0 @@ -# YAML Replies - diff --git a/docs/testing/integration-testing.md b/docs/testing/integration-testing.md deleted file mode 100644 index f7e17b0d..00000000 --- a/docs/testing/integration-testing.md +++ /dev/null @@ -1,2 +0,0 @@ -# Integration Testing - diff --git a/docs/testing/untitled.md b/docs/testing/untitled.md deleted file mode 100644 index e5bea2a9..00000000 --- a/docs/testing/untitled.md +++ /dev/null @@ -1,2 +0,0 @@ -# Specs - diff --git a/lib/generators/stealth/install_generator.rb b/lib/generators/stealth/install_generator.rb new file mode 100644 index 00000000..67efffc0 --- /dev/null +++ b/lib/generators/stealth/install_generator.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'rails/generators/base' +require 'securerandom' + +module Stealth + module Generators + + class InstallGenerator < Rails::Generators::Base + source_root File.expand_path("../../templates", __FILE__) + + desc "Creates a Stealth folder and copies Stealth template files to your application." + + def copy_initializer + template "stealth.rb", "config/initializers/stealth.rb" + end + + def create_stealth_folder + empty_directory "stealth" + end + + def copy_stealth_folders + directory "events", "stealth/events" + directory "flows", "stealth/flows" + directory "models", "stealth/models" + directory "replies", "stealth/replies" + template "intents.rb", "stealth" + end + + def show_readme + readme "README" if behavior == :invoke + end + end + end +end \ No newline at end of file diff --git a/lib/stealth/commands/server.rb b/lib/generators/templates/README similarity index 59% rename from lib/stealth/commands/server.rb rename to lib/generators/templates/README index eca1b673..e25af269 100644 --- a/lib/stealth/commands/server.rb +++ b/lib/generators/templates/README @@ -1,27 +1,7 @@ -# coding: utf-8 -# frozen_string_literal: true +===================================================================================== -require 'rack/handler/puma' -require 'stealth/commands/command' +Stealth 3.0 Installed -module Stealth - module Commands - class Server < Command - def initialize(port:) - @port = port - $stdout.sync = true - end - - def start - # Rack::Handler::Puma.run(Stealth::Server) - puts ascii_art - exec "foreman start -f Procfile.dev -p #{@port}" - end - - private - - def ascii_art - <<~ART -- -yooy- -yo` `oy- @@ -40,10 +20,6 @@ def ascii_art -yh/ :yy: /hy- - Stealth v#{Stealth::VERSION} +Visit https://localhost:5100/stealth for a tutorial on how to build your first bot 🚀 - ART - end - end - end -end +===================================================================================== \ No newline at end of file diff --git a/lib/generators/templates/events/phone_calls.rb b/lib/generators/templates/events/phone_calls.rb new file mode 100644 index 00000000..a4cc2390 --- /dev/null +++ b/lib/generators/templates/events/phone_calls.rb @@ -0,0 +1,9 @@ +Stealth.event :phone_call do + on :call_receive do + + end + + on :hang_up do + + end +end diff --git a/lib/generators/templates/events/text_messages.rb b/lib/generators/templates/events/text_messages.rb new file mode 100644 index 00000000..721fbfcd --- /dev/null +++ b/lib/generators/templates/events/text_messages.rb @@ -0,0 +1,9 @@ +Stealth.event :text_message do + on :receive do + + end + + on :unsubscribe do + + end +end diff --git a/lib/generators/templates/flows/catch_all_flow.rb b/lib/generators/templates/flows/catch_all_flow.rb new file mode 100644 index 00000000..1d5e5d2f --- /dev/null +++ b/lib/generators/templates/flows/catch_all_flow.rb @@ -0,0 +1,15 @@ +Stealth.flow :catch_all do + before_state :set_catch_all_reason + + state :level1, reengage: false do + say "Uh oh, let's try that again!" + end + + state :level2, reengage: false do + send_replies + end + + state :level3, reenage: false do + send_replies + end +end diff --git a/lib/generators/templates/flows/driving_flow.rb b/lib/generators/templates/flows/driving_flow.rb new file mode 100644 index 00000000..55bd9d24 --- /dev/null +++ b/lib/generators/templates/flows/driving_flow.rb @@ -0,0 +1,31 @@ +Stealth.flow :driving do + state :schedule_checkin do + step_to_in 1.hour, state: :ask_to_continue + update_session_to state: :say_safe_drive + end + + state :say_safe_drive do + send_replies + step_to slug: current_lead.return_to_slug + end + + state :ask_to_continue do + if previous_session.flow_string == 'driving' + send_replies + update_session_to state: :get_continue_response + else + update_session_to session: previous_session + end + end + + state :get_continue_response, fails_to: :ask_to_continue, reengage: true do + handle_message( + :yes => proc { + step_to slug: current_lead.return_to_slug + }, + :no => proc { + do_nothing + } + ) + end +end diff --git a/lib/generators/templates/flows/goodbye_flow.rb b/lib/generators/templates/flows/goodbye_flow.rb new file mode 100644 index 00000000..b49b5775 --- /dev/null +++ b/lib/generators/templates/flows/goodbye_flow.rb @@ -0,0 +1,5 @@ +Stealth.flow :goodbye do + state :say_goodbye do + say "Goodbye!" + end +end diff --git a/lib/generators/templates/flows/hello_flow.rb b/lib/generators/templates/flows/hello_flow.rb new file mode 100644 index 00000000..c9796271 --- /dev/null +++ b/lib/generators/templates/flows/hello_flow.rb @@ -0,0 +1,5 @@ +Stealth.flow :hello do + state :say_hello do + say "Hello world!" + end +end diff --git a/lib/generators/templates/flows/helpers/catch_all_helper.rb b/lib/generators/templates/flows/helpers/catch_all_helper.rb new file mode 100644 index 00000000..c9e89c47 --- /dev/null +++ b/lib/generators/templates/flows/helpers/catch_all_helper.rb @@ -0,0 +1,11 @@ +module CatchAllHelper + def set_catch_all_reason + @reason = case current_message.catch_all_reason[:err].to_s + when 'Stealth::Errors::UnrecognizedMessage' + Message.log_catch_all + :unrecognized_message + else + :system_error + end + end +end diff --git a/lib/generators/templates/intents.rb b/lib/generators/templates/intents.rb new file mode 100644 index 00000000..431a1429 --- /dev/null +++ b/lib/generators/templates/intents.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Stealth + module Intents + INTENTS = [ + # { + # name: 'get_leads_count', + # category: 'leads_query', + # description: 'Get the number of leads in the system for time period', + # examples: ['How many leads do we have today?', 'How many leads are there?', 'How many leads are in the system overall?'], + # tool: { + # type: "function", + # function: { + # name: "get_lead_count", + # description: "Get leads count for provided time span", + # parameters: { + # type: "object", + # properties: { + # start_date: { type: "string", description: "The start date with time." }, + # end_date: { type: "string", description: "The end date with time." } + # }, + # required: %w[start_date end_date], + # additionalProperties: false + # }, + # } + # } + # }, + ] + end +end diff --git a/lib/generators/templates/models/.gitkeep b/lib/generators/templates/models/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/lib/generators/templates/replies/catch_all/level2.rb b/lib/generators/templates/replies/catch_all/level2.rb new file mode 100644 index 00000000..a2a31599 --- /dev/null +++ b/lib/generators/templates/replies/catch_all/level2.rb @@ -0,0 +1,7 @@ +Stealth.reply do + if @reason == :unrecognized_message + say "I'm sorry, I didn't understand that." + else + say "Uh oh, something went wrong." + end +end diff --git a/lib/generators/templates/replies/catch_all/level3.rb b/lib/generators/templates/replies/catch_all/level3.rb new file mode 100644 index 00000000..3fd315d5 --- /dev/null +++ b/lib/generators/templates/replies/catch_all/level3.rb @@ -0,0 +1,8 @@ +Steath.reply do + if @reason == :unrecognized_message + say "I'm sorry, I still didn't understand that." + say "Please hold while I transfer you to a human." + else + say "Uh oh, something went wrong." + end +end diff --git a/lib/generators/templates/replies/driving/ask_to_continue.rb b/lib/generators/templates/replies/driving/ask_to_continue.rb new file mode 100644 index 00000000..1ac7d94d --- /dev/null +++ b/lib/generators/templates/replies/driving/ask_to_continue.rb @@ -0,0 +1,13 @@ +Stealth.reply do + if current_lead[:first_name].present? + say( + text: "Hey #{current_lead[:first_name]}. Hope you had a safe drive 🚗. Are you ready to continue?" + suggestions: ["Yes", "No"] + ) + else + say( + text: "Hope you had a safe drive 🚗. Are you ready to continue?" + suggestions: ["Yes"] + ) + end +end diff --git a/lib/generators/templates/replies/driving/say_safe_drive.rb b/lib/generators/templates/replies/driving/say_safe_drive.rb new file mode 100644 index 00000000..37853ba6 --- /dev/null +++ b/lib/generators/templates/replies/driving/say_safe_drive.rb @@ -0,0 +1,7 @@ +Stealth.reply do + if current_lead[:first_name].present? + say "Hey #{current_lead[:first_name]}. Hope you had a safe drive 🚗." + else + say "Hope you had a safe drive 🚗." + end +end diff --git a/lib/generators/templates/stealth.rb b/lib/generators/templates/stealth.rb new file mode 100644 index 00000000..b2aa81cb --- /dev/null +++ b/lib/generators/templates/stealth.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +Stealth.setup do |config| +end diff --git a/lib/stealth.rb b/lib/stealth.rb index 7017cfb5..5a5572b8 100644 --- a/lib/stealth.rb +++ b/lib/stealth.rb @@ -1 +1,2 @@ require 'stealth/base' + diff --git a/lib/stealth/base.rb b/lib/stealth/base.rb index 0f0585d4..38829855 100644 --- a/lib/stealth/base.rb +++ b/lib/stealth/base.rb @@ -6,27 +6,43 @@ require 'sidekiq' require 'redis' require 'active_support/all' - -begin - require "rails" - require "active_record" -rescue LoadError - # Don't require ActiveRecord -end +require 'spectre' # core require 'stealth/version' -require 'stealth/errors' -require 'stealth/core_ext' -require 'stealth/logger' -require 'stealth/configuration' -require 'stealth/reloader' +require 'stealth/engine' +require 'stealth/configuration/configuration' +require 'stealth/configuration/bandwidth' +require 'stealth/configuration/slack' +require 'stealth/configuration/spectre_configuration' +# require 'stealth/core_ext' +# require 'stealth/reloader' # helpers require 'stealth/helpers/redis' -module Stealth +require 'stealth/event_mapping' +require 'stealth/event_manager' +require 'stealth/event_triggers' +require 'stealth/flow_manager' +require 'stealth/flow_triggers' +require 'stealth/reply_manager' +require 'stealth/reply_triggers' +require 'stealth/reply' +require 'stealth/jobs' +require 'stealth/lock' +require 'stealth/dispatcher' +require 'stealth/session' +require 'stealth/errors' +require 'stealth/logger' + +# services +require 'stealth/services/base_client' +require 'stealth/services/jobs/handle_event_job' +require 'stealth/services/jobs/scheduled_reply_job' +require 'stealth/service_event' +module Stealth def self.env @env ||= ActiveSupport::StringInquirer.new(ENV['STEALTH_ENV'] || 'development') end @@ -50,25 +66,25 @@ def self.configuration=(config) def self.default_autoload_paths [ - File.join(Stealth.root, 'bot', 'controllers', 'concerns'), - File.join(Stealth.root, 'bot', 'controllers'), - File.join(Stealth.root, 'bot', 'models', 'concerns'), - File.join(Stealth.root, 'bot', 'models'), - File.join(Stealth.root, 'bot', 'helpers'), + # File.join(Stealth.root, 'bot', 'controllers', 'concerns'), + # File.join(Stealth.root, 'bot', 'controllers'), + # File.join(Stealth.root, 'bot', 'models', 'concerns'), + # File.join(Stealth.root, 'bot', 'models'), + # File.join(Stealth.root, 'bot', 'helpers'), File.join(Stealth.root, 'config') ] end - def self.bot_reloader - @bot_reloader - end + # def self.bot_reloader + # @bot_reloader + # end def self.set_config_defaults(config) defaults = { dynamic_delay_muliplier: 1.0, # values > 1 increase, values < 1 decrease delay session_ttl: 0, # 0 seconds; don't expire sessions lock_autorelease: 30, # 30 seconds - transcript_logging: false, # show user replies in the logs + transcript_logging: true, # show user replies in the logs hot_reload: Stealth.env.development?, # hot reload bot files on change (dev only) eager_load: Stealth.env.production?, # eager load bot files for performance (prod only) autoload_paths: Stealth.default_autoload_paths, # array of autoload paths used in eager and hot reloading @@ -140,15 +156,11 @@ def self.load_environment end end - def self.tid - Thread.current.object_id.to_s(36) - end - def self.require_directory(directory) for_each_file_in(directory) { |file| require_relative(file) } end -private + private def self.for_each_file_in(directory, &blk) directory = directory.to_s.gsub(%r{(\/|\\)}, File::SEPARATOR) @@ -158,36 +170,74 @@ def self.for_each_file_in(directory, &blk) Dir.glob(directory).sort.each(&blk) end -end + # Thread Management + def self.tid + Thread.current.object_id.to_s(36) + end -require 'stealth/jobs' -require 'stealth/dispatcher' -require 'stealth/server' -require 'stealth/reply' -require 'stealth/scheduled_reply' -require 'stealth/service_reply' -require 'stealth/service_message' -require 'stealth/session' -require 'stealth/lock' -require 'stealth/nlp/result' -require 'stealth/nlp/client' -require 'stealth/controller/callbacks' -require 'stealth/controller/replies' -require 'stealth/controller/messages' -require 'stealth/controller/unrecognized_message' -require 'stealth/controller/catch_all' -require 'stealth/controller/helpers' -require 'stealth/controller/dynamic_delay' -require 'stealth/controller/interrupt_detect' -require 'stealth/controller/dev_jumps' -require 'stealth/controller/nlp' -require 'stealth/controller/controller' -require 'stealth/flow/base' -require 'stealth/services/base_client' + class << self + include Stealth::EventTriggers + include Stealth::FlowTriggers + include Stealth::ReplyTriggers -if defined?(ActiveRecord) - require 'stealth/migrations/configurator' - require 'stealth/migrations/generators' - require 'stealth/migrations/railtie_config' - require 'stealth/migrations/tasks' + attr_accessor :configurations + + # Setup & Service Driver Configuration + def setup + self.configurations ||= {} + yield(self) + end + + def configure_bandwidth + self.configurations[:bandwidth] ||= Bandwidth.new + yield(configurations[:bandwidth]) + end + + def configure_slack + self.configurations[:slack] ||= Slack.new + yield(configurations[:slack]) + end + + def configure_spectre + self.configurations[:spectre] ||= SpectreConfiguration.new + yield(configurations[:spectre]) + + Spectre.setup do |config| + config.api_key = configurations[:spectre].api_key + config.llm_provider = configurations[:spectre].llm_provider + end + end + end end + +# require 'stealth/jobs' +# require 'stealth/dispatcher' +# require 'stealth/server' +# require 'stealth/reply' +# require 'stealth/scheduled_reply' +# require 'stealth/service_reply' +# require 'stealth/service_message' +# require 'stealth/session' +# require 'stealth/lock' +# require 'stealth/nlp/result' +# require 'stealth/nlp/client' +# require 'stealth/controller/callbacks' +# require 'stealth/controller/replies' +# require 'stealth/controller/messages' +# require 'stealth/controller/unrecognized_message' +# require 'stealth/controller/catch_all' +# require 'stealth/controller/helpers' +# require 'stealth/controller/dynamic_delay' +# require 'stealth/controller/interrupt_detect' +# require 'stealth/controller/dev_jumps' +# require 'stealth/controller/nlp' +# require 'stealth/controller/controller' +# require 'stealth/flow/base' +# require 'stealth/services/base_client' + +# if defined?(ActiveRecord) +# require 'stealth/migrations/configurator' +# require 'stealth/migrations/generators' +# require 'stealth/migrations/railtie_config' +# require 'stealth/migrations/tasks' +# end diff --git a/lib/stealth/cli.rb b/lib/stealth/cli.rb deleted file mode 100644 index fd4cf09d..00000000 --- a/lib/stealth/cli.rb +++ /dev/null @@ -1,274 +0,0 @@ -# coding: utf-8 -# frozen_string_literal: true - -require 'thor' -require 'stealth/cli_base' -require 'stealth/commands/console' -require 'stealth/generators/builder' -require 'stealth/generators/generate' - -module Stealth - class Cli < Thor - extend CliBase - - desc 'new', 'Creates a new Stealth bot' - long_desc <<-EOS - `stealth new ` creates a new Stealth both with the given name. - - $ > stealth new new_bot - EOS - def new(name) - Stealth::Generators::Builder.start([name]) - end - - - desc 'generate', 'Generates scaffold Stealth files' - long_desc <<-EOS - `stealth generate ` generates scaffold Stealth files - - $ > stealth generate flow quote - EOS - def generate(generator, name) - case generator - when 'migration' - Stealth::Migrations::Generator.migration(name) - when 'flow' - Stealth::Generators::Generate.start([generator, name]) - else - puts "Could not find generator '#{generator}'." - puts "Run `stealth help generate` for more options." - end - end - map 'g' => 'generate' - - - desc 'version', 'Prints stealth version' - long_desc <<-EOS - `stealth version` prints the version of the bundled stealth gem. - EOS - def version - require 'stealth/version' - puts "#{ Stealth::VERSION }" - end - map %w{--version -v} => :version - - - desc 'server', 'Starts a stealth server' - long_desc <<-EOS - `stealth server` starts a server for the current stealth project. - - $ > stealth server - - $ > stealth server -p 4500 - EOS - method_option :port, aliases: '-p', desc: 'The port to run the server on' - method_option :help, desc: 'Displays the usage message' - def server - if options[:help] - invoke :help, ['server'] - else - require 'stealth/commands/server' - Stealth::Commands::Server.new(port: options.fetch(:port) { 5000 }).start - end - end - map 's' => 'server' - - - desc 'console', 'Starts a stealth console' - long_desc <<-EOS - `stealth console` starts the interactive stealth console. - - $ > stealth console --engine=pry - EOS - method_option :engine, desc: "Choose a specific console engine: (#{Stealth::Commands::Console::ENGINES.keys.join('/')})" - method_option :help, desc: 'Displays the usage method' - def console - if options[:help] - invoke :help, ['console'] - else - Stealth::Commands::Console.new(options).start - end - end - map 'c' => 'console' - - - desc 'setup', 'Runs setup tasks for a specified service' - long_desc <<-EOS - `stealth setup ` runs setup tasks for the specified service. - - $ > stealth setup facebook - EOS - def setup(service) - Stealth.load_environment - service_setup_klass = "Stealth::Services::#{service.classify}::Setup".constantize - service_setup_klass.trigger - end - - - desc 'sessions:clear', 'Clears all sessions in development' - long_desc <<-EOS - `stealth sessions:clear` clears all sessions from Redis in development. - - $ > stealth sessions:clear - EOS - define_method 'sessions:clear' do - Stealth.load_environment - $redis.flushdb if Stealth.env.development? - end - - - desc 'db:create', 'Creates the database from DATABASE_URL or config/database.yml for the current STEALTH_ENV' - long_desc <<-EOS - `stealth db:create` Creates the database from DATABASE_URL or config/database.yml for the current STEALTH_ENV (use db:create:all to create all databases in the config). Without STEALTH_ENV or when STEALTH_ENV is development, it defaults to creating the development and test databases. - - $ > stealth db:create - EOS - define_method 'db:create' do - Kernel.exec('bundle exec rake db:create') - end - - - desc 'db:create:all', 'Creates all databases from DATABASE_URL or config/database.yml' - long_desc <<-EOS - `stealth db:create:all` Creates all databases from DATABASE_URL or config/database.yml regardless of the enviornment specified in STEALTH_ENV - - $ > stealth db:create:all - EOS - define_method 'db:create:all' do - Kernel.exec('bundle exec rake db:create:all') - end - - - desc 'db:drop', 'Drops the database from DATABASE_URL or config/database.yml for the current STEALTH_ENV' - long_desc <<-EOS - `stealth db:drop` Drops the database from DATABASE_URL or config/database.yml for the current STEALTH_ENV (use db:drop:all to drop all databases in the config). Without STEALTH_ENV or when STEALTH_ENV is development, it defaults to dropping the development and test databases. - - $ > stealth db:drop - EOS - define_method 'db:drop' do - Kernel.exec('bundle exec rake db:drop') - end - - - desc 'db:drop:all', 'Drops all databases from DATABASE_URL or config/database.yml' - long_desc <<-EOS - `stealth db:drop:all` Drops all databases from DATABASE_URL or config/database.yml - - $ > stealth db:drop:all - EOS - define_method 'db:drop:all' do - Kernel.exec('bundle exec rake db:drop:all') - end - - - desc 'db:environment:set', 'Set the environment value for the database' - long_desc <<-EOS - `stealth db:environment:set` Set the environment value for the database - - $ > stealth db:environment:set - EOS - define_method 'db:environment:set' do - Kernel.exec('bundle exec rake db:enviornment:set') - end - - - desc 'db:migrate', 'Migrate the database' - long_desc <<-EOS - `stealth db:migrate` Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog). - - $ > stealth db:migrate - EOS - define_method 'db:migrate' do - Kernel.exec('bundle exec rake db:migrate') - end - - - desc 'db:rollback', 'Rolls the schema back to the previous version' - long_desc <<-EOS - `stealth db:rollback` Rolls the schema back to the previous version (specify steps w/ STEP=n). - - $ > stealth db:rollback - EOS - define_method 'db:rollback' do - Kernel.exec('bundle exec rake db:rollback') - end - - - desc 'db:schema:load', 'Loads a schema.rb file into the database' - long_desc <<-EOS - `stealth db:schema:load` Loads a schema.rb file into the database - - $ > stealth db:schema:load - EOS - define_method 'db:schema:load' do - Kernel.exec('bundle exec rake db:schema:load') - end - - - desc 'db:schema:dump', 'Creates a db/schema.rb file that is portable against any DB supported by Active Record' - long_desc <<-EOS - `stealth db:schema:dump` Creates a db/schema.rb file that is portable against any DB supported by Active Record - - $ > stealth db:schema:dump - EOS - define_method 'db:schema:dump' do - Kernel.exec('bundle exec rake db:schema:dump') - end - - - desc 'db:seed', 'Seeds the database with data from db/seeds.rb' - long_desc <<-EOS - `stealth db:seed` Seeds the database with data from db/seeds.rb - - $ > stealth db:seed - EOS - define_method 'db:seed' do - Kernel.exec('bundle exec rake db:seed') - end - - - desc 'db:version', 'Retrieves the current schema version number' - long_desc <<-EOS - `stealth db:version` Retrieves the current schema version number - - $ > stealth db:version - EOS - define_method 'db:version' do - Kernel.exec('bundle exec rake db:version') - end - - - desc 'db:setup', 'Creates the database, loads the schema, and initializes with the seed data (use db:reset to also drop the database first)' - long_desc <<-EOS - `stealth db:setup` Creates the database, loads the schema, and initializes with the seed data (use db:reset to also drop the database first) - - $ > stealth db:setup - EOS - define_method 'db:setup' do - Kernel.exec('bundle exec rake db:setup') - end - - - desc 'db:structure:dump', 'Dumps the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql' - long_desc <<-EOS - `stealth db:structure:dump` Dumps the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql - - $ > stealth db:structure:dump - EOS - define_method 'db:structure:dump' do - Kernel.exec('bundle exec rake db:structure:dump') - end - - - desc 'db:structure:load', 'Recreates the databases from the structure.sql file' - long_desc <<-EOS - `stealth db:structure:load` Recreates the databases from the structure.sql file - - $ > stealth db:structure:load - EOS - define_method 'db:structure:load' do - Kernel.exec('bundle exec rake db:structure:load') - end - - end -end diff --git a/lib/stealth/cli_base.rb b/lib/stealth/cli_base.rb deleted file mode 100644 index 69a560e1..00000000 --- a/lib/stealth/cli_base.rb +++ /dev/null @@ -1,25 +0,0 @@ -# coding: utf-8 -# frozen_string_literal: true - -module Stealth - module CliBase - def define_commands(&blk) - class_eval(&blk) if block_given? - end - - def banner(command, nspace = true, subcommand = false) - super(command, nspace, namespace != 'stealth:cli') - end - - def handle_argument_error(command, error, args, arity) - name = [(namespace == 'stealth:cli' ? nil : namespace), command.name].compact.join(" ") - - msg = "ERROR: \"#{basename} #{name}\" was called with " - msg << "no arguments" if args.empty? - msg << "arguments " << args.inspect unless args.empty? - msg << "\nUsage: #{banner(command).inspect}" - - raise Thor::InvocationError, msg - end - end -end diff --git a/lib/stealth/commands/command.rb b/lib/stealth/commands/command.rb deleted file mode 100644 index 3745cb21..00000000 --- a/lib/stealth/commands/command.rb +++ /dev/null @@ -1,14 +0,0 @@ -# coding: utf-8 -# frozen_string_literal: true - -require 'stealth' - -module Stealth - module Commands - class Command - def initialize(options) - - end - end - end -end diff --git a/lib/stealth/commands/console.rb b/lib/stealth/commands/console.rb deleted file mode 100644 index e13b8a8e..00000000 --- a/lib/stealth/commands/console.rb +++ /dev/null @@ -1,75 +0,0 @@ -# coding: utf-8 -# frozen_string_literal: true - -require 'stealth/commands/command' - -module Stealth - module Commands - # REPL that supports different engines. - # - # It is run with: - # - # `bundle exec stealth console` - class Console < Command - module CodeReloading - def reload! - puts 'Reloading...' - Kernel.exec "#{$PROGRAM_NAME} console" - end - end - - # Supported engines - ENGINES = { - 'pry' => 'Pry', - 'ripl' => 'Ripl', - 'irb' => 'IRB' - }.freeze - - DEFAULT_ENGINE = ['irb'].freeze - - attr_reader :options - - def initialize(options) - super(options) - - @options = options - end - - def start - prepare - engine.start - end - - def engine - load_engine options.fetch(:engine) { engine_lookup } - end - - private - - def prepare - # Clear out ARGV so Pry/IRB don't attempt to parse the rest - ARGV.shift until ARGV.empty? - - # Add convenience methods to the main:Object binding - TOPLEVEL_BINDING.eval('self').__send__(:include, CodeReloading) - - Stealth.boot - end - - def engine_lookup - (ENGINES.find { |_, klass| Object.const_defined?(klass) } || DEFAULT_ENGINE).first - end - - def load_engine(engine) - require engine - rescue LoadError - ensure - return Object.const_get( - ENGINES.fetch(engine) do - raise ArgumentError.new("Unknown console engine: `#{engine}'") - end - ) - end - end - end -end diff --git a/lib/stealth/configuration/bandwidth.rb b/lib/stealth/configuration/bandwidth.rb new file mode 100644 index 00000000..36e2e4b3 --- /dev/null +++ b/lib/stealth/configuration/bandwidth.rb @@ -0,0 +1,12 @@ +module Stealth + class Bandwidth + attr_accessor :account_id, :api_username, :api_password, :application_id + + def initialize + @account_id = nil + @api_username = nil + @api_password = nil + @application_id = nil + end + end +end diff --git a/lib/stealth/configuration.rb b/lib/stealth/configuration/configuration.rb similarity index 100% rename from lib/stealth/configuration.rb rename to lib/stealth/configuration/configuration.rb diff --git a/lib/stealth/configuration/slack.rb b/lib/stealth/configuration/slack.rb new file mode 100644 index 00000000..e0746032 --- /dev/null +++ b/lib/stealth/configuration/slack.rb @@ -0,0 +1,9 @@ +module Stealth + class Slack + attr_accessor :webhook_url + + def initialize + @webhook_url = nil + end + end +end diff --git a/lib/stealth/configuration/spectre_configuration.rb b/lib/stealth/configuration/spectre_configuration.rb new file mode 100644 index 00000000..7520459c --- /dev/null +++ b/lib/stealth/configuration/spectre_configuration.rb @@ -0,0 +1,10 @@ +module Stealth + class SpectreConfiguration + attr_accessor :api_key, :llm_provider + + def initialize + @api_key = nil + @llm_provider = nil + end + end +end diff --git a/lib/stealth/controller/callbacks.rb b/lib/stealth/controller/callbacks.rb deleted file mode 100644 index cf350444..00000000 --- a/lib/stealth/controller/callbacks.rb +++ /dev/null @@ -1,64 +0,0 @@ -# coding: utf-8 -# frozen_string_literal: true - -module Stealth - class Controller - module Callbacks - - extend ActiveSupport::Concern - - include ActiveSupport::Callbacks - - included do - define_callbacks :action, skip_after_callbacks_if_terminated: true - end - - class_methods do - def _normalize_callback_options(options) - _normalize_callback_option(options, :only, :if) - _normalize_callback_option(options, :except, :unless) - end - - def _normalize_callback_option(options, from, to) - if from = options[from] - _from = Array(from).map(&:to_s).to_set - from = proc { |c| _from.include?(c.action_name) } - options[to] = Array(options[to]).unshift(from) - end - end - - def _insert_callbacks(callbacks, block = nil) - options = callbacks.extract_options! - _normalize_callback_options(options) - callbacks.push(block) if block - callbacks.each do |callback| - yield callback, options - end - end - - [:before, :after, :around].each do |callback| - define_method "#{callback}_action" do |*names, &blk| - _insert_callbacks(names, blk) do |name, options| - set_callback(:action, callback, name, options) - end - end - - define_method "prepend_#{callback}_action" do |*names, &blk| - _insert_callbacks(names, blk) do |name, options| - set_callback(:action, callback, name, options.merge(prepend: true)) - end - end - - define_method "skip_#{callback}_action" do |*names| - _insert_callbacks(names) do |name, options| - skip_callback(:action, callback, name, options) - end - end - - alias_method :"append_#{callback}_action", :"#{callback}_action" - end - end - - end - end -end diff --git a/lib/stealth/controller/catch_all.rb b/lib/stealth/controller/catch_all.rb deleted file mode 100644 index d7ec5039..00000000 --- a/lib/stealth/controller/catch_all.rb +++ /dev/null @@ -1,85 +0,0 @@ -# coding: utf-8 -# frozen_string_literal: true - -module Stealth - class Controller - module CatchAll - - extend ActiveSupport::Concern - - included do - - def run_catch_all(err:) - error_level = fetch_error_level - - if err.class == Stealth::Errors::UnrecognizedMessage - Stealth::Logger.l( - topic: 'catch_all', - message: "[Level #{error_level}] for user #{current_session_id} #{err.message}" - ) - else - Stealth::Logger.l( - topic: 'catch_all', - message: "[Level #{error_level}] for user #{current_session_id} #{[err.class, err.message, err.backtrace.join("\n")].join("\n")}" - ) - end - - # Store the reason so it can be accessed by the CatchAllsController - current_message.catch_all_reason = { - err: err.class, - err_msg: err.message - } - - # Don't run catch_all from the catch_all controller - if current_session.flow_string == 'catch_all' - Stealth::Logger.l(topic: 'catch_all', message: "CatchAll triggered for user #{current_session_id} from within CatchAll; ignoring.") - return false - end - - if defined?(CatchAllsController) && FlowMap.flow_spec[:catch_all].present? - catch_all_state = calculate_catch_all_state(error_level) - - if FlowMap.flow_spec[:catch_all].states.keys.include?(catch_all_state.to_sym) - step_to flow: :catch_all, state: catch_all_state - else - # We are out of bounds, do nothing to prevent an infinite loop - Stealth::Logger.l(topic: 'catch_all', message: "Stopping; we\'ve exceeded the number of defined catch_all states for user #{current_session_id}.") - return false - end - end - end - - private - - def fetch_error_level - if fail_attempts = $redis.get(error_slug) - begin - fail_attempts = Integer(fail_attempts) - rescue ArgumentError - fail_attempts = 1 - end - - fail_attempts += 1 - else - fail_attempts = 1 - end - - # Set the error with an expiration to avoid filling Redis - $redis.setex(error_slug, 15.minutes.to_i, fail_attempts) - - fail_attempts - end - - def error_slug - ['error', current_session_id, current_session.flow_string, current_session.state_string].join('-') - end - - def calculate_catch_all_state(error_level) - "level#{error_level}" - end - - end - - end - end -end diff --git a/lib/stealth/controller/dynamic_delay.rb b/lib/stealth/controller/dynamic_delay.rb deleted file mode 100644 index e026a249..00000000 --- a/lib/stealth/controller/dynamic_delay.rb +++ /dev/null @@ -1,62 +0,0 @@ -# coding: utf-8 -# frozen_string_literal: true - -module Stealth - class Controller - - module DynamicDelay - extend ActiveSupport::Concern - - SHORT_DELAY = 3.0 - STANDARD_DELAY = 4.3 - LONG_DELAY = 7.0 - - included do - def dynamic_delay(previous_reply:) - calculate_delay(previous_reply: previous_reply) - end - - private - - def calculate_delay(previous_reply:) - return SHORT_DELAY if previous_reply.blank? - - case previous_reply['reply_type'] - when 'text' - calculate_delay_from_text(previous_reply['text']) - when 'image' - STANDARD_DELAY - when 'audio' - STANDARD_DELAY - when 'video' - STANDARD_DELAY - when 'file' - STANDARD_DELAY - when 'cards' - STANDARD_DELAY - when 'list' - STANDARD_DELAY - when nil - SHORT_DELAY - else - SHORT_DELAY - end - end - end - - def calculate_delay_from_text(text) - case text.size - when 0..55 - SHORT_DELAY - when 56..140 - STANDARD_DELAY - when 141..256 - STANDARD_DELAY * 1.5 - else - LONG_DELAY - end - end - end - - end -end diff --git a/lib/stealth/controller/helpers.rb b/lib/stealth/controller/helpers.rb deleted file mode 100644 index fe6e910b..00000000 --- a/lib/stealth/controller/helpers.rb +++ /dev/null @@ -1,129 +0,0 @@ -# coding: utf-8 -# frozen_string_literal: true - -require "active_support/dependencies" - -module Stealth - class Controller - module Helpers - - extend ActiveSupport::Concern - - class MissingHelperError < LoadError - def initialize(error, path) - @error = error - @path = "helpers/#{path}.rb" - set_backtrace error.backtrace - - if error.path =~ /^#{path}(\.rb)?$/ - super("Missing helper file helpers/%s.rb" % path) - else - raise error - end - end - end - - # class << self; attr_accessor :helpers_path; end - - included do - class_attribute :_helpers, default: Module.new - class_attribute :helpers_path, default: ["bot/helpers"] - class_attribute :include_all_helpers, default: true - end - - class_methods do - # When a class is inherited, wrap its helper module in a new module. - # This ensures that the parent class's module can be changed - # independently of the child class's. - def inherited(subclass) - helpers = _helpers - subclass._helpers = Module.new { include helpers } - - if subclass.superclass == Stealth::Controller && Stealth::Controller.include_all_helpers - subclass.helper :all - else - subclass.class_eval { default_helper_module! } unless subclass.anonymous? - end - - include subclass._helpers - - super - end - - def modules_for_helpers(args) - # Allow all helpers to be included - args += all_bot_helpers if args.delete(:all) - - # Add each helper_path to the LOAD_PATH - Array(helpers_path).each {|path| $:.unshift(path) } - - args.flatten.map! do |arg| - case arg - when String, Symbol - file_name = "#{arg.to_s.underscore}_helper" - begin - require_dependency(file_name) - rescue LoadError => e - raise Stealth::Controller::Helpers::MissingHelperError.new(e, file_name) - end - - mod_name = file_name.camelize - begin - mod_name.constantize - rescue LoadError - raise NameError, "Couldn't find #{mod_name}, expected it to be defined in helpers/#{file_name}.rb" - end - when Module - arg - else - raise ArgumentError, "helper must be a String, Symbol, or Module" - end - end - end - - def helper(*args, &block) - modules_for_helpers(args).each do |mod| - add_template_helper(mod) - end - - _helpers.module_eval(&block) if block_given? - end - - def default_helper_module! - module_name = name.sub(/Controller$/, "".freeze) - module_path = module_name.underscore - helper module_path - rescue LoadError => e - raise e unless e.is_missing? "helpers/#{module_path}_helper" - rescue NameError => e - raise e unless e.missing_name? "#{module_name}Helper" - end - - # Returns a list of helper names in a given path. - # - # Stealth::Controller.all_helpers_from_path 'bot/helpers' - # # => ["bot", "estimates", "tickets"] - def all_helpers_from_path(path) - helpers = Array(path).flat_map do |_path| - extract = /^#{Regexp.quote(_path.to_s)}\/?(.*)_helper.rb$/ - names = Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1'.freeze) } - names.sort! - end - helpers.uniq! - helpers - end - - private - def add_template_helper(mod) - _helpers.module_eval { include mod } - end - - # Extract helper names from files in "bot/helpers/**/*_helper.rb" - def all_bot_helpers - all_helpers_from_path(helpers_path) - end - end - - end - end -end diff --git a/lib/stealth/controller/messages.rb b/lib/stealth/controller/messages.rb deleted file mode 100644 index abcea39d..00000000 --- a/lib/stealth/controller/messages.rb +++ /dev/null @@ -1,283 +0,0 @@ -# frozen_string_literal: true - -module Stealth - class Controller - module Messages - extend ActiveSupport::Concern - - included do - attr_accessor :normalized_msg, :homophone_translated_msg - - unless defined?(ALPHA_ORDINALS) - ALPHA_ORDINALS = ('A'..'Z').to_a.freeze - end - - unless defined?(NO_MATCH) - NO_MATCH = 0xdeadbeef - end - - unless defined?(HOMOPHONES) - HOMOPHONES = { - 'EH' => 'A', - 'BE' => 'B', - 'BEE' => 'B', - 'CEE' => 'C', - 'SEA' => 'C', - 'SEE' => 'C', - 'DEE' => 'D', - 'GEE' => 'G', - 'EYE' => 'I', - 'AYE' => 'I', - 'JAY' => 'J', - 'KAY' => 'K', - 'KAYE' => 'K', - 'OH' => 'O', - 'OWE' => 'O', - 'PEA' => 'P', - 'PEE' => 'P', - 'CUE' => 'Q', - 'QUEUE' => 'Q', - 'ARR' => 'R', - 'YOU' => 'U', - 'YEW' => 'U', - 'EX' => 'X', - 'WHY' => 'Y', - 'ZEE' => 'Z' - } - end - - def normalized_msg - @normalized_msg ||= current_message.message.normalize - end - - # Converts homophones into alpha-ordinals - def homophone_translated_msg - @homophone_translated_msg ||= begin - ord = normalized_msg.without_punctuation - if HOMOPHONES[ord].present? - HOMOPHONES[ord] - else - ord - end - end - end - - # Hash for message and lambda pairs. If the message is matched, the - # lambda will be called. - # - # Example: { - # "100k" => proc { step_back }, "200k" => proc { step_to flow :hello } - # } - def handle_message(message_tuples) - match = NO_MATCH # dummy value since nils are used for matching - - if reserved_homophones_used = contains_homophones?(message_tuples.keys) - raise( - Stealth::Errors::ReservedHomophoneUsed, - "Cannot use `#{reserved_homophones_used.join(', ')}`. Reserved for homophones." - ) - end - - # Before checking content, match against our ordinals - if idx = message_is_an_ordinal? - # find the value stored in the message tuple via the index - matched_value = message_tuples.keys[idx] - match = matched_value unless matched_value.nil? - end - - if match == NO_MATCH - message_tuples.keys.each_with_index do |msg, i| - # intent detection - if msg.is_a?(Symbol) - perform_nlp! unless nlp_result.present? - - if intent_matched?(msg) - match = msg - break - else - next - end - end - - if msg.is_a?(Regexp) - if normalized_msg =~ msg - match = msg - break - else - next - end - end - - # custom mismatch handler; any nil key results in a match - if msg.nil? - match = msg - break - end - - # check if the normalized message matches exactly - if message_matches?(msg) - match = msg - break - end - end - end - - if match != NO_MATCH - instance_eval(&message_tuples[match]) - else - handle_mismatch(true) - end - end - - # Matches the message or the oridinal value entered (via SMS) - # Ignores case and strips leading and trailing whitespace before matching. - def get_match(messages, raise_on_mismatch: true, fuzzy_match: true) - if reserved_homophones_used = contains_homophones?(messages) - raise( - Stealth::Errors::ReservedHomophoneUsed, - "Cannot use `#{reserved_homophones_used.join(', ')}`. Reserved for homophones." - ) - end - - # Before checking content, match against our ordinals - if idx = message_is_an_ordinal? - return messages[idx] unless messages[idx].nil? - end - - messages.each_with_index do |msg, i| - # entity detection - if msg.is_a?(Symbol) - perform_nlp! unless nlp_result.present? - - if match = entity_matched?(msg, fuzzy_match) - return match - else - next - end - end - - # multi-entity detection - if msg.is_a?(Array) - perform_nlp! unless nlp_result.present? - - if match = entities_matched?(msg, fuzzy_match) - return match - else - next - end - end - - if message_matches?(msg) - return msg - end - end - - handle_mismatch(raise_on_mismatch) - end - - private - - def handle_mismatch(raise_on_mismatch) - log_nlp_result unless Stealth.config.log_all_nlp_results # Already logged - - if raise_on_mismatch - raise( - Stealth::Errors::UnrecognizedMessage, - "The reply '#{current_message.message}' was not recognized." - ) - else - current_message.message - end - end - - def contains_homophones?(arr) - arr = arr.map do |elem| - elem.normalize if elem.is_a?(String) - end.compact - - homophones = arr & HOMOPHONES.keys - homophones.any? ? homophones : false - end - - # Returns the index of the ordinal, nil if not found - def message_is_an_ordinal? - ALPHA_ORDINALS.index(homophone_translated_msg) - end - - def message_matches?(msg) - normalized_msg == msg.upcase - end - - def intent_matched?(intent) - nlp_result.intent == intent - end - - def entity_matched?(entity, fuzzy_match) - if nlp_result.entities.has_key?(entity) - match_count = nlp_result.entities[entity].size - if match_count > 1 && !fuzzy_match - log_nlp_result unless Stealth.config.log_all_nlp_results # Already logged - - raise( - Stealth::Errors::UnrecognizedMessage, - "Encountered #{match_count} entity matches of type #{entity.inspect} and expected 1. To allow, set fuzzy_match to true." - ) - else - # For single entity matches, just return the value - # rather than a single-element array - matched_entity = nlp_result.entities[entity].first - - # Custom LUIS List entities return a single element array for some - # reason - if matched_entity.is_a?(Array) && matched_entity.size == 1 - matched_entity.first - else - matched_entity - end - end - end - end - - def entities_matched?(entities, fuzzy_match) - nlp_entities = nlp_result.entities.deep_dup - results = [] - - entities.each do |entity| - # If we run out of matches for the entity type - # (or never had any to begin with) - return false if nlp_entities[entity].blank? - - results << nlp_entities[entity].shift - end - - # Check for leftover entities for the types we were looking for - unless fuzzy_match - entities.each do |entity| - unless nlp_entities[entity].blank? - log_nlp_result unless Stealth.config.log_all_nlp_results # Already logged - leftover_count = nlp_entities[entity].size - raise( - Stealth::Errors::UnrecognizedMessage, - "Encountered #{leftover_count} additional entity matches of type #{entity.inspect} for match #{entities.inspect}. To allow, set fuzzy_match to true." - ) - end - end - end - - results - end - - def log_nlp_result - # Log the results from the nlp_result if NLP was performed - if nlp_result.present? - Stealth::Logger.l( - topic: :nlp, - message: "User #{current_session_id} -> NLP Result: #{nlp_result.parsed_result.inspect}" - ) - end - end - - end - end - end -end diff --git a/lib/stealth/controller/nlp.rb b/lib/stealth/controller/nlp.rb deleted file mode 100644 index 146b5672..00000000 --- a/lib/stealth/controller/nlp.rb +++ /dev/null @@ -1,50 +0,0 @@ -# coding: utf-8 -# frozen_string_literal: true - -module Stealth - class Controller - module Nlp - - extend ActiveSupport::Concern - - included do - # Memoized in order to prevent multiple requests to the NLP provider - def perform_nlp! - Stealth::Logger.l( - topic: :nlp, - message: "User #{current_session_id} -> Performing NLP." - ) - - unless Stealth.config.nlp_integration.present? - raise Stealth::Errors::ConfigurationError, "An NLP integration has not yet been configured (Stealth.config.nlp_integration)" - end - - @nlp_result ||= begin - nlp_client = nlp_client_klass.new - @nlp_result = @current_message.nlp_result = nlp_client.understand( - query: current_message.message - ) - - if Stealth.config.log_all_nlp_results - Stealth::Logger.l( - topic: :nlp, - message: "User #{current_session_id} -> NLP Result: #{@nlp_result.parsed_result.inspect}" - ) - end - - @nlp_result - end - end - - private - - def nlp_client_klass - integration = Stealth.config.nlp_integration.to_s.titlecase - klass = "Stealth::Nlp::#{integration}::Client" - klass.classify.constantize - end - end - - end - end -end diff --git a/lib/stealth/controller/replies.rb b/lib/stealth/controller/replies.rb deleted file mode 100644 index 93910f83..00000000 --- a/lib/stealth/controller/replies.rb +++ /dev/null @@ -1,297 +0,0 @@ -# coding: utf-8 -# frozen_string_literal: true - -module Stealth - class Controller - module Replies - - extend ActiveSupport::Concern - - included do - - class_attribute :_preprocessors, default: [:erb] - class_attribute :_replies_path, default: [Stealth.root, 'bot', 'replies'] - - def send_replies(custom_reply: nil, inline: nil) - service_reply = load_service_reply( - custom_reply: custom_reply, - inline: inline - ) - - # Determine if we start at the beginning or somewhere else - reply_range = calculate_reply_range - offset = reply_range.first - - @previous_reply = nil - service_reply.replies.slice(reply_range).each_with_index do |reply, i| - # Updates the lock with the current position of the reply - lock_session!( - session_slug: current_session.get_session, - position: i + offset # Otherwise this won't account for explicit starting points - ) - - begin - send_reply(reply: reply) - rescue Stealth::Errors::UserOptOut => e - msg = "User #{current_session_id} opted out. [#{e.message}]" - service_error_dispatcher( - handler_method: :handle_opt_out, - error_msg: msg - ) - return - rescue Stealth::Errors::InvalidSessionID => e - msg = "User #{current_session_id} has an invalid session_id. [#{e.message}]" - service_error_dispatcher( - handler_method: :handle_invalid_session_id, - error_msg: msg - ) - return - rescue Stealth::Errors::MessageFiltered => e - msg = "Message to user #{current_session_id} was filtered. [#{e.message}]" - service_error_dispatcher( - handler_method: :handle_message_filtered, - error_msg: msg - ) - return - rescue Stealth::Errors::UnknownServiceError => e - msg = "User #{current_session_id} had an unknown error. [#{e.message}]" - service_error_dispatcher( - handler_method: :handle_unknown_error, - error_msg: msg - ) - return - end - - @previous_reply = reply - end - - @progressed = :sent_replies - ensure - release_lock! - end - - private - - def voice_service? - current_service.match?(/voice/) - end - - def send_reply(reply:) - if !reply.delay? && Stealth.config.auto_insert_delays && !voice_service? - # if it's the first reply in the service_reply or the previous reply - # wasn't a custom delay, then insert a delay - if @previous_reply.blank? || !@previous_reply.delay? - send_reply(reply: Reply.dynamic_delay) - end - end - - # Support randomized replies for text and speech replies. - # We select one before handing the reply off to the driver. - if reply['text'].is_a?(Array) - reply['text'] = reply['text'].sample - end - - handler = reply_handler.new( - recipient_id: current_message.sender_id, - reply: reply - ) - - formatted_reply = handler.send(reply.reply_type) - client = service_client.new(reply: formatted_reply) - client.transmit - - log_reply(reply, handler) if Stealth.config.transcript_logging - - # If this was a 'delay' type of reply, we insert the delay - if reply.delay? - insert_delay(duration: reply['duration']) - end - end - - def insert_delay(duration:) - begin - sleep_duration = if duration == 'dynamic' - dyn_duration = dynamic_delay(previous_reply: @previous_reply) - - Stealth.config.dynamic_delay_muliplier * dyn_duration - else - Float(duration) - end - - sleep(sleep_duration) - rescue ArgumentError, TypeError - raise(ArgumentError, 'Invalid duration specified. Duration must be a Numeric') - end - end - - def load_service_reply(custom_reply:, inline:) - if inline.present? - Stealth::ServiceReply.new( - recipient_id: current_session_id, - yaml_reply: inline, - preprocessor: :none, - context: nil - ) - else - yaml_reply, preprocessor = action_replies(custom_reply) - - Stealth::ServiceReply.new( - recipient_id: current_session_id, - yaml_reply: yaml_reply, - preprocessor: preprocessor, - context: binding - ) - end - end - - def service_client - begin - Kernel.const_get("Stealth::Services::#{current_service.classify}::Client") - rescue NameError - raise(Stealth::Errors::ServiceNotRecognized, "The service '#{current_service}' was not recognized") - end - end - - def reply_handler - begin - Kernel.const_get("Stealth::Services::#{current_service.classify}::ReplyHandler") - rescue NameError - raise(Stealth::Errors::ServiceNotRecognized, "The service '#{current_service}' was not recognized") - end - end - - def replies_folder - current_session.flow_string.underscore.pluralize - end - - def reply_dir - [*self._replies_path, replies_folder] - end - - def base_reply_filename - "#{current_session.state_string}.yml" - end - - def reply_filenames(custom_reply_filename=nil) - reply_filename = if custom_reply_filename.present? - custom_reply_filename - else - base_reply_filename - end - - service_filename = [reply_filename, current_service].join('+') - - # Service-specific filenames take precedance (returned first) - [service_filename, reply_filename] - end - - def find_reply_and_preprocessor(custom_reply) - selected_preprocessor = :none - - if custom_reply.present? - dir_and_file = custom_reply.rpartition(File::SEPARATOR) - _dir = dir_and_file.first - _file = "#{dir_and_file.last}.yml" - _replies_dir = [*self._replies_path, _dir] - possible_filenames = reply_filenames(_file) - reply_file_path = File.join(_replies_dir, _file) - service_reply_path = File.join(_replies_dir, reply_filenames(_file).first) - else - _replies_dir = *reply_dir - possible_filenames = reply_filenames - reply_file_path = File.join(_replies_dir, base_reply_filename) - service_reply_path = File.join(_replies_dir, reply_filenames.first) - end - - # Check if the service_filename exists - # If so, we can skip checking for a preprocessor - if File.exist?(service_reply_path) - return service_reply_path, selected_preprocessor - end - - # Cycles through possible preprocessor and variant combinations - # Early returns for performance - for preprocessor in self.class._preprocessors do - for reply_filename in possible_filenames do - selected_filepath = File.join(_replies_dir, [reply_filename, preprocessor.to_s].join('.')) - if File.exist?(selected_filepath) - reply_file_path = selected_filepath - selected_preprocessor = preprocessor - return reply_file_path, selected_preprocessor - end - end - end - - return reply_file_path, selected_preprocessor - end - - def action_replies(custom_reply=nil) - reply_path, selected_preprocessor = find_reply_and_preprocessor(custom_reply) - - begin - file_contents = File.read(reply_path) - rescue Errno::ENOENT - raise(Stealth::Errors::ReplyNotFound, "Could not find reply: '#{reply_path}'") - end - - return file_contents, selected_preprocessor - end - - def service_error_dispatcher(handler_method:, error_msg:) - if self.respond_to?(handler_method, true) - Stealth::Logger.l( - topic: current_service, - message: error_msg - ) - self.send(handler_method) - else - Stealth::Logger.l( - topic: :err, - message: "Unhandled service exception for user #{current_session_id}. No error handler for `#{handler_method}` found." - ) - end - - do_nothing - end - - def calculate_reply_range - # if an explicit starting point is specified, use that until the - # end of the range, otherwise start at the beginning - if @pos.present? - (@pos..-1) - else - (0..-1) - end - end - - def log_reply(reply, reply_handler) - message = case reply.reply_type - when 'text' - if reply_handler.respond_to?(:translated_reply) - reply_handler.translated_reply - else - reply['text'] - end - when 'speech' - reply['speech'] - when 'ssml' - reply['ssml'] - when 'delay' - '' - else - "<#{reply.reply_type}>" - end - - Stealth::Logger.l( - topic: current_service, - message: "User #{current_session_id} -> Sending: #{message}" - ) - - message - end - - end # instance methods - - end - end -end diff --git a/lib/stealth/controller/unrecognized_message.rb b/lib/stealth/controller/unrecognized_message.rb deleted file mode 100644 index 881d7133..00000000 --- a/lib/stealth/controller/unrecognized_message.rb +++ /dev/null @@ -1,62 +0,0 @@ -# coding: utf-8 -# frozen_string_literal: true - -module Stealth - class Controller - module UnrecognizedMessage - - extend ActiveSupport::Concern - - included do - - def run_unrecognized_message(err:) - err_message = "The message \"#{current_message.message}\" was not recognized in the original context." - - Stealth::Logger.l( - topic: 'unrecognized_message', - message: err_message - ) - - unless defined?(UnrecognizedMessagesController) - Stealth::Logger.l( - topic: 'unrecognized_message', - message: 'Running catch_all; UnrecognizedMessagesController not defined.' - ) - - run_catch_all(err: err) - return false - end - - unrecognized_msg_controller = UnrecognizedMessagesController.new( - service_message: current_message - ) - - begin - # Run handle_unrecognized_message action - unrecognized_msg_controller.handle_unrecognized_message - - if unrecognized_msg_controller.progressed? - Stealth::Logger.l( - topic: 'unrecognized_message', - message: 'A match was detected. Skipping catch-all.' - ) - else - # Log, but we don't want to run the catch_all for a poorly - # coded UnrecognizedMessagesController - Stealth::Logger.l( - topic: 'unrecognized_message', - message: 'Did not send replies, update session, or step' - ) - end - rescue StandardError => e - # Run the catch_all directly since we're already in an unrecognized - # message state - run_catch_all(err: e) - end - end - - end - - end - end -end diff --git a/lib/stealth/core_ext.rb b/lib/stealth/core_ext.rb deleted file mode 100644 index 10f23551..00000000 --- a/lib/stealth/core_ext.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -Dir.glob(File.expand_path('core_ext/*.rb', __dir__)).each do |path| - require path -end diff --git a/lib/stealth/core_ext/numeric.rb b/lib/stealth/core_ext/numeric.rb deleted file mode 100644 index f98125c3..00000000 --- a/lib/stealth/core_ext/numeric.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -class Numeric - - def states - self - end - alias :state :states - -end diff --git a/lib/stealth/core_ext/string.rb b/lib/stealth/core_ext/string.rb deleted file mode 100644 index ccf6cd60..00000000 --- a/lib/stealth/core_ext/string.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -class String - - EXCLUDED_CHARS = %w[" ' . , ! ? ( ) - _ ` ‘ ’ “ ”].freeze - EXCLUDED_CHARS_ESC = EXCLUDED_CHARS.map { |c| "\\#{c}" } - EXCLUDED_CHARS_RE = /#{EXCLUDED_CHARS_ESC.join('|')}/ - - # Removes blank padding and double+single quotes - def normalize - self.upcase.strip - end - - def without_punctuation - self.gsub(EXCLUDED_CHARS_RE, '') - end - -end diff --git a/lib/stealth/dispatcher.rb b/lib/stealth/dispatcher.rb index 830bab3d..429ba6d6 100644 --- a/lib/stealth/dispatcher.rb +++ b/lib/stealth/dispatcher.rb @@ -3,7 +3,7 @@ module Stealth - # Responsible for coordinating incoming messages + # Responsible for coordinating incoming events # 1. Receives incoming request params # 2. Initializes respective service request handler # 3. Processes params through service request handler (might be async) @@ -12,57 +12,61 @@ module Stealth # 5. Returns an HTTP response to be returned to the requestor class Dispatcher - attr_reader :service, :params, :headers, :message_handler + attr_reader :service, :params, :headers, :service_handler def initialize(service:, params:, headers:) @service = service @params = params @headers = headers - @message_handler = message_handler_klass.new( + @service_handler = service_handler_klass.new( params: params, headers: headers ) end def coordinate - message_handler.coordinate + service_handler.coordinate end def process - service_message = message_handler.process + service_event = service_handler.process if Stealth.config.transcript_logging - log_incoming_message(service_message) + log_incoming_message(service_event) end - bot_controller = BotController.new(service_message: service_message) - bot_controller.route + # This is where we will need to route to the new eventing system + # event_type -> text_message, event -> receive + Stealth.trigger_event(service_event.event_type, service_event.event, service_event) + + # bot_controller = BotController.new(service_message: service_message) + # bot_controller.route end private - def message_handler_klass + def service_handler_klass begin - Kernel.const_get("Stealth::Services::#{service.classify}::MessageHandler") + Kernel.const_get("Stealth::Services::#{service.classify}::EventHandler") rescue NameError raise(Stealth::Errors::ServiceNotRecognized, "The service '#{service}' was not recognized") end end - def log_incoming_message(service_message) - message = if service_message.location.present? + def log_incoming_message(service_event) + event = if service_event.location.present? "Received: " - elsif service_message.attachments.present? + elsif service_event.attachments.present? "Received: " - elsif service_message.payload.present? - "Received Payload: #{service_message.payload}" + elsif service_event.payload.present? + "Received Payload: #{service_event.payload}" else - "Received Message: #{service_message.message}" + "Received Message: #{service_event.message}" end Stealth::Logger.l( topic: 'user', - message: "User #{service_message.sender_id} -> #{message}" + message: "User #{service_event.sender_id} -> #{event}" ) end end diff --git a/lib/stealth/engine.rb b/lib/stealth/engine.rb new file mode 100644 index 00000000..7008a1c6 --- /dev/null +++ b/lib/stealth/engine.rb @@ -0,0 +1,14 @@ +module Stealth + class Engine < ::Rails::Engine + isolate_namespace Stealth + + # Will have to avoid loading driver files in the engine + initializer 'stealth' do + # Not sure why we need these. + # require 'stealth/services/bandwidth/message_event_handler' + # require 'stealth/services/bandwidth/call_event_handler' + # require 'stealth/services/bandwidth/service_message' + # require 'stealth/services/bandwidth/service_call' + end + end +end diff --git a/lib/stealth/errors.rb b/lib/stealth/errors.rb index 12656b01..019f35d5 100644 --- a/lib/stealth/errors.rb +++ b/lib/stealth/errors.rb @@ -64,4 +64,4 @@ class Halted < Errors end end -end +end \ No newline at end of file diff --git a/lib/stealth/event_manager.rb b/lib/stealth/event_manager.rb new file mode 100644 index 00000000..99ff586c --- /dev/null +++ b/lib/stealth/event_manager.rb @@ -0,0 +1,60 @@ +module Stealth + class EventManager + def initialize + @events = Hash.new { |hash, key| hash[key] = {} } + end + + def register_event(event_name, &block) + @current_event = event_name + instance_eval(&block) + @current_event = nil + end + + def on(action_name, &block) + @events[@current_event][action_name] = block + end + + def trigger_event(event_name, action_name, service_event) + if @events[event_name] && @events[event_name][action_name] + + # Always use DslEventContext if defined in the Rails app + context_class = defined?(Stealth::DslEventContext) ? Stealth::DslEventContext : Stealth::Controller + context = context_class.new(service_event: service_event) + + block_context = Class.new do + def initialize(context) + @context = context + end + + def method_missing(method_name, *args, **kwargs, &block) + if @context.respond_to?(method_name) + @context.public_send(method_name, *args, **kwargs, &block) + else + super + end + end + + def respond_to_missing?(method_name, include_private = false) + @context.respond_to?(method_name) || super + end + end.new(context) + + block_context.instance_exec(service_event, &@events[event_name][action_name]) + else + Stealth::Logger.l(topic: 'user', message: "No handler for #{event_name} #{action_name}") + end + end + + def self.instance + @instance ||= new + end + + def self.event(event_name, &block) + instance.register_event(event_name, &block) + end + + def self.trigger_event(event_name, action_name, service_event) + instance.trigger_event(event_name, action_name, service_event) + end + end +end diff --git a/lib/stealth/event_mapping.rb b/lib/stealth/event_mapping.rb new file mode 100644 index 00000000..c0ba1629 --- /dev/null +++ b/lib/stealth/event_mapping.rb @@ -0,0 +1,16 @@ +module Stealth + class EventMapping + MAPPING = { + # slack-specific event mappings + 'slack' => { + 'text_received' => { event_type: :slack, event: :receive }, + 'reaction_received' => { event_type: :slack, event: :reaction }, + 'interactive_response_received' => { event_type: :slack, event: :interactive_response } + } + }.freeze + + def self.map_event(service:, event_type:) + MAPPING.dig(service, event_type) + end + end +end diff --git a/lib/stealth/event_triggers.rb b/lib/stealth/event_triggers.rb new file mode 100644 index 00000000..a171ed11 --- /dev/null +++ b/lib/stealth/event_triggers.rb @@ -0,0 +1,11 @@ +module Stealth + module EventTriggers + def trigger_event(event_name, action_name, service_event) + EventManager.trigger_event(event_name, action_name, service_event) + end + + def event(event_name, &block) + EventManager.event(event_name, &block) + end + end +end diff --git a/lib/stealth/flow/base.rb b/lib/stealth/flow/base.rb deleted file mode 100644 index eea2562c..00000000 --- a/lib/stealth/flow/base.rb +++ /dev/null @@ -1,70 +0,0 @@ -# coding: utf-8 -# frozen_string_literal: true - -require 'stealth/flow/specification' -require 'stealth/flow/state' - -module Stealth - module Flow - - extend ActiveSupport::Concern - - class_methods do - def flow(flow_name, &specification) - flow_spec[flow_name.to_sym] = Specification.new(flow_name, &specification) - end - end - - included do - class_attribute :flow_spec, default: {} - - attr_accessor :flow, :flow_state, :user_id - - def current_state - res = self.spec.states[@flow_state.to_sym] if @flow_state - res || self.spec.initial_state - end - - def current_flow - @flow || self.class.flow_spec.keys.first - end - - def spec - self.class.flow_spec[current_flow] - end - - def states - self.spec.states.keys - end - - def init(flow:, state:) - new_flow = flow.to_sym - new_state = state.to_sym - - unless state_exists?(potential_flow: new_flow, potential_state: new_state) - raise(Stealth::Errors::InvalidStateTransition, "Unknown state '#{new_state}' for '#{new_flow}' flow") - end - - @flow = new_flow - @flow_state = new_state - - self - end - - private - - def flow_and_state - [current_flow, current_state].join(Stealth::Session::SLUG_SEPARATOR) - end - - def state_exists?(potential_flow:, potential_state:) - if self.class.flow_spec[potential_flow].present? - self.class.flow_spec[potential_flow].states.include?(potential_state) - else - raise(Stealth::Errors::InvalidStateTransition, "Unknown flow '#{potential_flow}'") - end - end - end - - end -end diff --git a/lib/stealth/flow/specification.rb b/lib/stealth/flow/specification.rb deleted file mode 100644 index 02b58a20..00000000 --- a/lib/stealth/flow/specification.rb +++ /dev/null @@ -1,57 +0,0 @@ -# coding: utf-8 -# frozen_string_literal: true - -module Stealth - module Flow - class Specification - attr_reader :flow_name - attr_accessor :states, :initial_state - - def initialize(flow_name, &specification) - @states = Hash.new - @flow_name = flow_name - instance_eval(&specification) - end - - def state_names - states.keys - end - - private - - def state(name, fails_to: nil, redirects_to: nil, **opts) - fail_state = get_fail_or_redirect_state(fails_to) - redirect_state = get_fail_or_redirect_state(redirects_to) - - new_state = Stealth::Flow::State.new( - name: name, - spec: self, - fails_to: fail_state, - redirects_to: redirect_state, - opts: opts - ) - - @initial_state = new_state if @states.empty? - @states[name.to_sym] = new_state - end - - def get_fail_or_redirect_state(specified_state) - if specified_state.present? - session = Stealth::Session.new - - if Stealth::Session.is_a_session_string?(specified_state) - session.session = specified_state - else - session.session = Stealth::Session.canonical_session_slug( - flow: flow_name, - state: specified_state - ) - end - - return session - end - end - - end - end -end diff --git a/lib/stealth/flow/state.rb b/lib/stealth/flow/state.rb deleted file mode 100644 index 0bcc1ede..00000000 --- a/lib/stealth/flow/state.rb +++ /dev/null @@ -1,83 +0,0 @@ -# coding: utf-8 -# frozen_string_literal: true - -module Stealth - module Flow - class State - - include Comparable - - attr_accessor :name - attr_reader :spec, :fails_to, :redirects_to, :opts - - def initialize(name:, spec:, fails_to: nil, redirects_to: nil, opts:) - if fails_to.present? && !fails_to.is_a?(Stealth::Session) - raise(ArgumentError, 'fails_to state should be a Stealth::Session') - end - - if redirects_to.present? && !redirects_to.is_a?(Stealth::Session) - raise(ArgumentError, 'redirects_to state should be a Stealth::Session') - end - - @name, @spec = name, spec - @fails_to, @redirects_to, @opts = fails_to, redirects_to, opts - end - - def <=>(other_state) - state_position(self) <=> state_position(other_state) - end - - def +(steps) - if steps < 0 - new_position = state_position(self) + steps - - # we don't want to allow the array index to wrap here so we return - # the first state instead - if new_position < 0 - new_state = spec.states.keys.first - else - new_state = spec.states.keys.at(new_position) - end - else - new_state = spec.states.keys[state_position(self) + steps] - - # we may have been told to access an out-of-bounds state - # return the last state - if new_state.blank? - new_state = spec.states.keys.last - end - end - - new_state - end - - def -(steps) - if steps < 0 - return self + steps.abs - else - return self + (-steps) - end - end - - def to_s - "#{name}" - end - - def to_sym - name.to_sym - end - - private - - def state_position(state) - states = spec.states.keys - - unless states.include?(state.to_sym) - raise(ArgumentError, "state `#{state}' does not exist") - end - - states.index(state.to_sym) - end - end - end -end diff --git a/lib/stealth/flow_manager.rb b/lib/stealth/flow_manager.rb new file mode 100644 index 00000000..2fc876b4 --- /dev/null +++ b/lib/stealth/flow_manager.rb @@ -0,0 +1,65 @@ +module Stealth + class FlowManager + def initialize + @flows = Hash.new { |hash, key| hash[key] = {} } + end + + def register_flow(flow_name, &block) + @current_flow = flow_name.to_sym + instance_eval(&block) + @current_flow = nil + end + + def state(state_name, &block) + @flows[@current_flow] ||= {} + @flows[@current_flow][state_name.to_sym] = block + end + + def trigger_flow(flow_name, state_name, service_event) + flow_name = flow_name.to_sym + state_name = state_name.to_sym + + if @flows[flow_name] && @flows[flow_name][state_name] + block = @flows[flow_name][state_name] + + # Always use DslEventContext if defined in the Rails app + context_class = defined?(Stealth::DslEventContext) ? Stealth::DslEventContext : Stealth::Controller + context = context_class.new(service_event: service_event) + + block_context = Class.new do + def initialize(context) + @context = context + end + + def method_missing(method_name, *args, **kwargs, &block) + if @context.respond_to?(method_name) + @context.public_send(method_name, *args, **kwargs, &block) + else + super + end + end + + def respond_to_missing?(method_name, include_private = false) + @context.respond_to?(method_name) || super + end + end.new(context) + + block_context.instance_exec(service_event, &block) + else + Stealth::Logger.l(topic: 'user', message: "No flow found for #{flow_name} with state #{state_name}") + end + end + + def self.instance + @instance ||= new + end + + def self.flow(flow_name, &block) + instance.register_flow(flow_name, &block) + end + + def self.trigger_flow(flow_name, state_name, service_event) + instance.trigger_flow(flow_name, state_name, service_event) + end + end +end diff --git a/lib/stealth/flow_triggers.rb b/lib/stealth/flow_triggers.rb new file mode 100644 index 00000000..e4ed7c9d --- /dev/null +++ b/lib/stealth/flow_triggers.rb @@ -0,0 +1,11 @@ +module Stealth + module FlowTriggers + def trigger_flow(flow_name, state_name, service_event) + FlowManager.trigger_flow(flow_name, state_name, service_event) + end + + def flow(flow_name, &block) + FlowManager.flow(flow_name, &block) + end + end +end diff --git a/lib/stealth/generators/builder.rb b/lib/stealth/generators/builder.rb deleted file mode 100644 index 432db92c..00000000 --- a/lib/stealth/generators/builder.rb +++ /dev/null @@ -1,41 +0,0 @@ -# coding: utf-8 -# frozen_string_literal: true - -require 'thor/group' - -module Stealth - module Generators - class Builder < Thor::Group - include Thor::Actions - - argument :name - - def self.source_root - File.dirname(__FILE__) + "/builder" - end - - def create_bot_directory - empty_directory(name) - end - - def create_bot_structure - directory('bot', "#{name}/bot") - directory('config', "#{name}/config") - directory('db', "#{name}/db") - - # Miscellaneous Files - copy_file "config.ru", "#{name}/config.ru" - copy_file "Rakefile", "#{name}/Rakefile" - copy_file "Gemfile", "#{name}/Gemfile" - copy_file "README.md", "#{name}/README.md" - copy_file "Procfile.dev", "#{name}/Procfile.dev" - copy_file ".gitignore", "#{name}/.gitignore" - end - - def change_directory_bundle - puts run("cd #{name} && bundle install") - end - - end - end -end diff --git a/lib/stealth/generators/builder/.gitignore b/lib/stealth/generators/builder/.gitignore deleted file mode 100644 index ec0b1960..00000000 --- a/lib/stealth/generators/builder/.gitignore +++ /dev/null @@ -1,30 +0,0 @@ -# See https://help.github.com/articles/ignoring-files for more about ignoring files. -# -# If you find yourself ignoring temporary files generated by your text editor -# or operating system, you probably want to add a global ignore instead: -# git config --global core.excludesfile '~/.gitignore_global' - -# Ignore bundler config. -/.bundle - -# Ignore the default SQLite database. -/db/*.sqlite3 -/db/*.sqlite3-journal - -# Ignore all logfiles and tempfiles. -/log/* -/tmp/* -!/log/.keep -!/tmp/.keep - -# Ignore uploaded files in development -/storage/* - -/node_modules -/yarn-error.log - -/public/assets -.byebug_history - -# Ignore master key for decrypting credentials and more. -/config/master.key diff --git a/lib/stealth/generators/builder/Gemfile b/lib/stealth/generators/builder/Gemfile deleted file mode 100644 index b6b64579..00000000 --- a/lib/stealth/generators/builder/Gemfile +++ /dev/null @@ -1,19 +0,0 @@ -source 'https://rubygems.org' - -gem 'stealth', '~> 2.0' - -gem 'railties', '~> 7.0' -gem 'activerecord', '~> 7.0' -# Use sqlite3 as the database for Active Record -gem 'sqlite3' - -# Uncomment to enable the Stealth Facebook Driver -# gem 'stealth-facebook' - -# Uncomment to enable the Stealth Twilio SMS Driver -# gem 'stealth-twilio' - -group :development do - gem 'foreman' - gem 'listen', '~> 3.3' -end diff --git a/lib/stealth/generators/builder/Procfile.dev b/lib/stealth/generators/builder/Procfile.dev deleted file mode 100644 index 6e85f3fb..00000000 --- a/lib/stealth/generators/builder/Procfile.dev +++ /dev/null @@ -1,2 +0,0 @@ -web: bundle exec puma -C config/puma.rb -sidekiq: bundle exec sidekiq -C config/sidekiq.yml -q stealth_webhooks -q stealth_replies -r ./config/boot.rb diff --git a/lib/stealth/generators/builder/README.md b/lib/stealth/generators/builder/README.md deleted file mode 100644 index d54a366a..00000000 --- a/lib/stealth/generators/builder/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Stealth Logo - -To boot this bot locally, we recommend the following: - -1. `bundle` -2. Start your local Redis server -3. `stealth s` - -For more information, please check out the [Stealth documentation](https://hellostealth.org/docs). diff --git a/lib/stealth/generators/builder/Rakefile b/lib/stealth/generators/builder/Rakefile deleted file mode 100644 index 5ed39b7c..00000000 --- a/lib/stealth/generators/builder/Rakefile +++ /dev/null @@ -1,2 +0,0 @@ -require_relative 'config/boot' -Stealth::Migrations::Tasks.load_tasks diff --git a/lib/stealth/generators/builder/bot/controllers/bot_controller.rb b/lib/stealth/generators/builder/bot/controllers/bot_controller.rb deleted file mode 100644 index 0b2c6572..00000000 --- a/lib/stealth/generators/builder/bot/controllers/bot_controller.rb +++ /dev/null @@ -1,67 +0,0 @@ -# frozen_string_literal: true - -class BotController < Stealth::Controller - - helper :all - - def route - if current_message.payload.present? - handle_payloads - # Clear out the payload to prevent duplicate handling - current_message.payload = nil - return - end - - # Allow devs to jump around flows and states by typing: - # /flow_name/state_name or - # /flow_name (jumps to first state) or - # //state_name (jumps to state in current flow) - # (only works for bots in development) - return if dev_jump_detected? - - if current_session.present? - step_to session: current_session - else - step_to flow: 'hello', state: 'say_hello' - end - end - -private - - # Handle payloads globally since payload buttons remain in the chat - # and we cannot guess in which states they will be tapped. - def handle_payloads - case current_message.payload - when 'developer_restart', 'new_user' - step_to flow: 'hello', state: 'say_hello' - when 'goodbye' - step_to flow: 'goodbye' - end - end - - # Automatically called when clients receive an opt-out error from - # the platform. You may write your own steps for handling. - def handle_opt_out - do_nothing - end - - # Automatically called when clients receive an invalid session_id error from - # the platform. For example, attempting to text a landline. - # You may write your own steps for handling. - def handle_invalid_session_id - do_nothing - end - - # Automatically called when a transmitted message is filtered/marked as spam. - # You may write your own steps for handling. - def handle_message_filtered - do_nothing - end - - # Automatically called when an unknown error is returned by the platform. - # You may write your own steps for handling. - def handle_unknown_error - do_nothing - end - -end diff --git a/lib/stealth/generators/builder/bot/controllers/catch_alls_controller.rb b/lib/stealth/generators/builder/bot/controllers/catch_alls_controller.rb deleted file mode 100644 index 9d4b01a1..00000000 --- a/lib/stealth/generators/builder/bot/controllers/catch_alls_controller.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -class CatchAllsController < BotController - - def level1 - send_replies - - if fail_session.present? - step_to session: fail_session - else - step_to session: previous_session - 2.states - end - end - -private - - def fail_session - previous_session.flow.current_state.fails_to - end - -end diff --git a/lib/stealth/generators/builder/bot/controllers/goodbyes_controller.rb b/lib/stealth/generators/builder/bot/controllers/goodbyes_controller.rb deleted file mode 100644 index 9ebefa38..00000000 --- a/lib/stealth/generators/builder/bot/controllers/goodbyes_controller.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -class GoodbyesController < BotController - - def say_goodbye - send_replies - end - -end diff --git a/lib/stealth/generators/builder/bot/controllers/hellos_controller.rb b/lib/stealth/generators/builder/bot/controllers/hellos_controller.rb deleted file mode 100644 index 4639a752..00000000 --- a/lib/stealth/generators/builder/bot/controllers/hellos_controller.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -class HellosController < BotController - - def say_hello - send_replies - end - -end diff --git a/lib/stealth/generators/builder/bot/controllers/interrupts_controller.rb b/lib/stealth/generators/builder/bot/controllers/interrupts_controller.rb deleted file mode 100644 index 90698ac7..00000000 --- a/lib/stealth/generators/builder/bot/controllers/interrupts_controller.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -class InterruptsController < BotController - - def say_interrupted - do_nothing - end - -end diff --git a/lib/stealth/generators/builder/bot/controllers/unrecognized_messages_controller.rb b/lib/stealth/generators/builder/bot/controllers/unrecognized_messages_controller.rb deleted file mode 100644 index f4b1458e..00000000 --- a/lib/stealth/generators/builder/bot/controllers/unrecognized_messages_controller.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -class UnrecognizedMessagesController < BotController - - def handle_unrecognized_message - do_nothing - end - -end diff --git a/lib/stealth/generators/builder/bot/helpers/bot_helper.rb b/lib/stealth/generators/builder/bot/helpers/bot_helper.rb deleted file mode 100644 index 1ea1fe0a..00000000 --- a/lib/stealth/generators/builder/bot/helpers/bot_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module BotHelper -end diff --git a/lib/stealth/generators/builder/bot/models/bot_record.rb b/lib/stealth/generators/builder/bot/models/bot_record.rb deleted file mode 100644 index 2cdad92e..00000000 --- a/lib/stealth/generators/builder/bot/models/bot_record.rb +++ /dev/null @@ -1,3 +0,0 @@ -class BotRecord < ActiveRecord::Base - self.abstract_class = true -end diff --git a/lib/stealth/generators/builder/bot/replies/catch_alls/level1.yml b/lib/stealth/generators/builder/bot/replies/catch_alls/level1.yml deleted file mode 100644 index 5a7b0eb1..00000000 --- a/lib/stealth/generators/builder/bot/replies/catch_alls/level1.yml +++ /dev/null @@ -1,2 +0,0 @@ -- reply_type: text - text: Oops. It looks like something went wrong. Let's try that again diff --git a/lib/stealth/generators/builder/bot/replies/goodbyes/say_goodbye.yml b/lib/stealth/generators/builder/bot/replies/goodbyes/say_goodbye.yml deleted file mode 100644 index 95da6881..00000000 --- a/lib/stealth/generators/builder/bot/replies/goodbyes/say_goodbye.yml +++ /dev/null @@ -1,2 +0,0 @@ -- reply_type: text - text: Goodbye World! diff --git a/lib/stealth/generators/builder/bot/replies/hellos/say_hello.yml b/lib/stealth/generators/builder/bot/replies/hellos/say_hello.yml deleted file mode 100644 index 05ab737f..00000000 --- a/lib/stealth/generators/builder/bot/replies/hellos/say_hello.yml +++ /dev/null @@ -1,2 +0,0 @@ -- reply_type: text - text: Hello World! diff --git a/lib/stealth/generators/builder/config.ru b/lib/stealth/generators/builder/config.ru deleted file mode 100644 index 9ed1b4a2..00000000 --- a/lib/stealth/generators/builder/config.ru +++ /dev/null @@ -1,4 +0,0 @@ -require 'rack/handler/puma' -require_relative 'config/boot' - -Rack::Handler::Puma.run(Stealth::Server) diff --git a/lib/stealth/generators/builder/config/boot.rb b/lib/stealth/generators/builder/config/boot.rb deleted file mode 100644 index db300efa..00000000 --- a/lib/stealth/generators/builder/config/boot.rb +++ /dev/null @@ -1,6 +0,0 @@ -require 'stealth' -require_relative './environment' - -Bundler.require(:default, Stealth.env) - -Stealth.boot diff --git a/lib/stealth/generators/builder/config/environment.rb b/lib/stealth/generators/builder/config/environment.rb deleted file mode 100644 index 85d9b586..00000000 --- a/lib/stealth/generators/builder/config/environment.rb +++ /dev/null @@ -1,2 +0,0 @@ -REDIS_URL = ENV['REDIS_URL'] || 'redis://localhost:6379/0' -$redis = Redis.new(:url => REDIS_URL) diff --git a/lib/stealth/generators/builder/config/flow_map.rb b/lib/stealth/generators/builder/config/flow_map.rb deleted file mode 100644 index 41042097..00000000 --- a/lib/stealth/generators/builder/config/flow_map.rb +++ /dev/null @@ -1,25 +0,0 @@ -class FlowMap - - include Stealth::Flow - - flow :hello do - state :say_hello - end - - flow :goodbye do - state :say_goodbye - end - - flow :interrupt do - state :say_interrupted - end - - flow :unrecognized_message do - state :handle_unrecognized_message - end - - flow :catch_all do - state :level1 - end - -end diff --git a/lib/stealth/generators/builder/config/initializers/autoload.rb b/lib/stealth/generators/builder/config/initializers/autoload.rb deleted file mode 100644 index a909e64e..00000000 --- a/lib/stealth/generators/builder/config/initializers/autoload.rb +++ /dev/null @@ -1,8 +0,0 @@ -# Add additional directories below for hot-reloading during development. -# You'll want to include any custom directories you create for your code. -# To stop certain files or directories from autoloading, use autoload_ignore_paths - -# Stealth.config.autoload_paths << File.join(Stealth.root, 'bot', 'services') -# Stealth.config.autoload_paths << File.join(Stealth.root, 'bot', 'jobs') - -# Stealth.config.autoload_ignore_paths << File.join(Stealth.root, 'bot', 'overrides') diff --git a/lib/stealth/generators/builder/config/puma.rb b/lib/stealth/generators/builder/config/puma.rb deleted file mode 100644 index 1f451118..00000000 --- a/lib/stealth/generators/builder/config/puma.rb +++ /dev/null @@ -1,25 +0,0 @@ -threads_count = ENV.fetch("STEALTH_MAX_THREADS") { 5 }.to_i -threads threads_count, threads_count - -# Specifies the `port` that Puma will listen on to receive requests, default is 3000. -# -port ENV.fetch("PORT") { 3000 } - -# Specifies the number of `workers` to boot in clustered mode. -# Workers are forked webserver processes. If using threads and workers together -# the concurrency of the application would be max `threads` * `workers`. -# Workers do not work on JRuby or Windows (both of which do not support -# processes). -# -# workers ENV.fetch("WEB_CONCURRENCY") { 2 } - -# Use the `preload_app!` method when specifying a `workers` number. -# This directive tells Puma to first boot the application and load code -# before forking the application. This takes advantage of Copy On Write -# process behavior so workers use less memory. -# -# preload_app! - -# Specifies the `environment` that Puma will run in. -# -environment ENV.fetch("STEALTH_ENV") { "development" } diff --git a/lib/stealth/generators/builder/config/services.yml b/lib/stealth/generators/builder/config/services.yml deleted file mode 100644 index d2000131..00000000 --- a/lib/stealth/generators/builder/config/services.yml +++ /dev/null @@ -1,35 +0,0 @@ -default: &default - # ========================================== - # ===== Example Facebook Service Setup ===== - # ========================================== - # facebook: - # verify_token: XXXFACEBOOK_VERIFY_TOKENXXX - # page_access_token: XXXFACEBOOK_ACCESS_TOKENXXX - # setup: - # greeting: # Greetings are broken up by locale - # - locale: default - # text: "Welcome to my Facebook Bot." - # get_started: - # payload: new_user - # persistent_menu: - # - locale: default - # composer_input_disabled: false - # call_to_actions: - # - type: payload - # text: Some Button - # payload: some_button - # - # =========================================== - # ======== Example SMS Service Setup ======== - # =========================================== - # twilio: - # account_sid: XXXTWILIO_ACCOUNT_SIDXXX - # auth_token: XXXTWILIO_AUTH_TOKENXXX - # from_phone: +14155330000 - -production: - <<: *default -development: - <<: *default -test: - <<: *default diff --git a/lib/stealth/generators/builder/config/sidekiq.yml b/lib/stealth/generators/builder/config/sidekiq.yml deleted file mode 100644 index b0f6a5f3..00000000 --- a/lib/stealth/generators/builder/config/sidekiq.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -:verbose: false -:concurrency: <%= ENV.fetch('SIDEKIQ_CONCURRENCY', 5).to_i %> diff --git a/lib/stealth/generators/builder/db/seeds.rb b/lib/stealth/generators/builder/db/seeds.rb deleted file mode 100644 index 5760a269..00000000 --- a/lib/stealth/generators/builder/db/seeds.rb +++ /dev/null @@ -1,7 +0,0 @@ -# This file should contain all the record creation needed to seed the database with its default values. -# The data can then be loaded with the stealth db:seed command (or created alongside the database with db:setup). -# -# Examples: -# -# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) -# Character.create(name: 'Luke', movie: movies.first) diff --git a/lib/stealth/generators/generate.rb b/lib/stealth/generators/generate.rb deleted file mode 100644 index 34bc26c3..00000000 --- a/lib/stealth/generators/generate.rb +++ /dev/null @@ -1,39 +0,0 @@ -# coding: utf-8 -# frozen_string_literal: true - -require 'thor/group' - -module Stealth - module Generators - class Generate < Thor::Group - include Thor::Actions - - argument :generator - argument :name - - def self.source_root - File.dirname(__FILE__) + "/generate/flow" - end - - def create_controller - template('controllers/controller.tt', "bot/controllers/#{name.pluralize}_controller.rb") - end - - def create_replies - # Sample Ask Reply - template('replies/ask_example.tt', "bot/replies/#{name.pluralize}/ask_example.yml.erb") - end - - def create_helper - template('helpers/helper.tt', "bot/helpers/#{name}_helper.rb") - end - - def edit_flow_map - inject_into_file "config/flow_map.rb", after: "include Stealth::Flow\n" do - "\n\tflow :#{name} do\n\t\tstate :ask_example\n\tend\n" - end - end - - end - end -end diff --git a/lib/stealth/generators/generate/flow/controllers/controller.tt b/lib/stealth/generators/generate/flow/controllers/controller.tt deleted file mode 100644 index 81f672f6..00000000 --- a/lib/stealth/generators/generate/flow/controllers/controller.tt +++ /dev/null @@ -1,7 +0,0 @@ -class <%= name.classify.pluralize %>Controller < BotController - - def ask_example - send_replies - end - -end diff --git a/lib/stealth/generators/generate/flow/helpers/helper.tt b/lib/stealth/generators/generate/flow/helpers/helper.tt deleted file mode 100644 index 7df92b5d..00000000 --- a/lib/stealth/generators/generate/flow/helpers/helper.tt +++ /dev/null @@ -1,3 +0,0 @@ -module <%= name.capitalize.underscore.camelize %>Helper - -end diff --git a/lib/stealth/generators/generate/flow/replies/ask_example.tt b/lib/stealth/generators/generate/flow/replies/ask_example.tt deleted file mode 100644 index 3bcfd1f8..00000000 --- a/lib/stealth/generators/generate/flow/replies/ask_example.tt +++ /dev/null @@ -1,9 +0,0 @@ -- reply_type: text - text: "Welcome to your brand new Stealth flow." -- reply_type: delay - duration: 2 -- reply_type: text - text: 'That was pretty easy huh?' - suggestions: - - text: "Yes" - - text: "No" diff --git a/lib/stealth/jobs.rb b/lib/stealth/jobs.rb index 5c9186a0..0cabc1b4 100644 --- a/lib/stealth/jobs.rb +++ b/lib/stealth/jobs.rb @@ -1,6 +1,3 @@ -# coding: utf-8 -# frozen_string_literal: true - module Stealth class Jobs diff --git a/lib/stealth/logger.rb b/lib/stealth/logger.rb index 621c09f3..7514187c 100644 --- a/lib/stealth/logger.rb +++ b/lib/stealth/logger.rb @@ -1,6 +1,3 @@ -# coding: utf-8 -# frozen_string_literal: true - module Stealth class Logger @@ -65,4 +62,4 @@ class << self end end -end +end \ No newline at end of file diff --git a/lib/stealth/migrations/configurator.rb b/lib/stealth/migrations/configurator.rb deleted file mode 100644 index 509139e2..00000000 --- a/lib/stealth/migrations/configurator.rb +++ /dev/null @@ -1,74 +0,0 @@ -# coding: utf-8 -# frozen_string_literal: true - -module Stealth - module Migrations - class InternalConfigurationsProxy - - attr_reader :configurations - - def initialize(configurations) - @configurations = configurations - end - - def on(config_key) - if @configurations[config_key] && block_given? - @configurations[config_key] = yield(@configurations[config_key]) || @configurations[config_key] - end - @configurations[config_key] - end - end - - class Configurator - def self.load_configurations - self.new - @env_config ||= Rails.application.config.database_configuration - ActiveRecord::Base.configurations = @env_config - @env_config - end - - def self.environments_config - proxy = InternalConfigurationsProxy.new(load_configurations) - yield(proxy) if block_given? - end - - def initialize(options = {}) - default_schema = ENV['SCHEMA'] || ActiveRecord::Tasks::DatabaseTasks.schema_file(ActiveRecord::Base.schema_format) - defaults = { - config: "config/database.yml", - migrate_dir: "db/migrate", - seeds: "db/seeds.rb", - schema: default_schema - } - @options = defaults.merge(options) - - Rails.application.config.root = Pathname.pwd - Rails.application.config.paths["config/database"] = config - end - - def config - @options[:config] - end - - def migrate_dir - @options[:migrate_dir] - end - - def seeds - @options[:seeds] - end - - def schema - @options[:schema] - end - - def config_for_all - Configurator.load_configurations.dup - end - - def config_for(environment) - config_for_all[environment.to_s] - end - end - end -end diff --git a/lib/stealth/migrations/generators.rb b/lib/stealth/migrations/generators.rb deleted file mode 100644 index c2aaf5d9..00000000 --- a/lib/stealth/migrations/generators.rb +++ /dev/null @@ -1,17 +0,0 @@ -# coding: utf-8 -# frozen_string_literal: true - -require "rails/generators" - -module Stealth - module Migrations - class Generator - def self.migration(name, options="") - generator_params = [name] + options.split(" ") - Rails::Generators.invoke("active_record:migration", generator_params, - destination_root: Stealth.root - ) - end - end - end -end diff --git a/lib/stealth/migrations/railtie_config.rb b/lib/stealth/migrations/railtie_config.rb deleted file mode 100644 index 5352e1c3..00000000 --- a/lib/stealth/migrations/railtie_config.rb +++ /dev/null @@ -1,15 +0,0 @@ -# coding: utf-8 -# frozen_string_literal: true - -module Stealth - module Migrations - class RailtieConfig < Rails::Application - config.generators.options[:rails] = { orm: :active_record } - - config.generators.options[:active_record] = { - :migration => true, - :timestamps => true - } - end - end -end diff --git a/lib/stealth/migrations/tasks.rb b/lib/stealth/migrations/tasks.rb deleted file mode 100644 index 4a594343..00000000 --- a/lib/stealth/migrations/tasks.rb +++ /dev/null @@ -1,44 +0,0 @@ -# coding: utf-8 -# frozen_string_literal: true - -module Stealth - module Migrations - class Tasks - - class << self - def configure - configurator = Configurator.new - - paths = Rails.application.config.paths - - paths.add "config/database", with: configurator.config - paths.add "db/migrate", with: configurator.migrate_dir - paths.add "db/seeds.rb", with: configurator.seeds - end - - def load_tasks - return unless defined?(ActiveRecord) - - configure - - Configurator.environments_config do |proxy| - ActiveRecord::Tasks::DatabaseTasks.database_configuration = proxy.configurations - end - - RailtieConfig.load_tasks - - # %w( - # connection - # environment - # db/new_migration - # ).each do - # |task| load "stealth/migrations/tasks/#{task}.rake" - # end - - load "active_record/railties/databases.rake" - end - end - - end - end -end diff --git a/lib/stealth/nlp/client.rb b/lib/stealth/nlp/client.rb deleted file mode 100644 index 06a55b84..00000000 --- a/lib/stealth/nlp/client.rb +++ /dev/null @@ -1,22 +0,0 @@ -# coding: utf-8 -# frozen_string_literal: true - -module Stealth - module Nlp - class Client - - def client - nil - end - - def understand(query:) - nil - end - - def understand_speech(audio_file:, audio_config: nil) - nil - end - end - - end -end diff --git a/lib/stealth/nlp/result.rb b/lib/stealth/nlp/result.rb deleted file mode 100644 index dd77a99e..00000000 --- a/lib/stealth/nlp/result.rb +++ /dev/null @@ -1,57 +0,0 @@ -# coding: utf-8 -# frozen_string_literal: true - -module Stealth - module Nlp - class Result - - ENTITY_TYPES = %i(number currency email percentage phone age - url ordinal geo dimension temp datetime duration - key_phrase name) - - attr_reader :result - - def initialize(result:) - @result = result - end - - def parsed_result - nil - end - - def intent_id - nil - end - - def intent - nil - end - - def intent_score - nil - end - - def raw_entities - {} - end - - def entities - {} - end - - # :postive, :negative, :neutral - def sentiment - nil - end - - def sentiment_score - nil - end - - def present? - parsed_result.present? - end - - end - end -end diff --git a/lib/stealth/reloader.rb b/lib/stealth/reloader.rb deleted file mode 100644 index 869d3fd3..00000000 --- a/lib/stealth/reloader.rb +++ /dev/null @@ -1,91 +0,0 @@ -# coding: utf-8 -# frozen_string_literal: true - -require 'zeitwerk' - -module Stealth - class Reloader - - def initialize - @reloader = Class.new(ActiveSupport::Reloader) - @loader = Zeitwerk::Loader.new - # @loader.logger = method(:puts) - @loader - end - - def load_bot! - load_autoload_paths! - enable_reloading! - enable_eager_load! - @loader.setup - end - - def load_autoload_paths! - if Stealth.config.autoload_paths.present? - Stealth.config.autoload_paths.each do |autoload_path| - @loader.push_dir(autoload_path) - end - - # Bot-specific ignores - Stealth.config.autoload_ignore_paths.each do |autoload_ignore_path| - @loader.ignore(autoload_ignore_path) - end - - # Ignore setup files - @loader.ignore(File.join(Stealth.root, 'config', 'initializers')) - @loader.ignore(File.join(Stealth.root, 'config', 'boot.rb')) - @loader.ignore(File.join(Stealth.root, 'config', 'environment.rb')) - @loader.ignore(File.join(Stealth.root, 'config', 'puma.rb')) - end - end - - def enable_eager_load! - if Stealth.config.eager_load - @loader.setup - @loader.eager_load - Stealth::Logger.l(topic: 'stealth', message: 'Eager loading enabled.') - end - end - - def enable_reloading! - if Stealth.config.hot_reload - @checker = ActiveSupport::EventedFileUpdateChecker.new([], files_to_watch) do - reload! - end - - @loader.enable_reloading - Stealth::Logger.l(topic: 'stealth', message: 'Hot reloading enabled.') - end - end - - # Only reloads if a change has been detected in one of the autoload files` - def reload - if Stealth.config.hot_reload - @checker.execute_if_updated - end - end - - # Force reloads reglardless of filesystem changes - def reload! - if Stealth.config.hot_reload - @loader.reload - end - end - - def call - @reloader.wrap do - reload - yield - end - end - - private - - def files_to_watch - Stealth.config.autoload_paths.map do |path| - [path, 'rb'] - end.to_h - end - - end -end diff --git a/lib/stealth/reply.rb b/lib/stealth/reply.rb index b1525fd2..3bf3d5ea 100644 --- a/lib/stealth/reply.rb +++ b/lib/stealth/reply.rb @@ -3,12 +3,11 @@ module Stealth class Reply - - attr_accessor :reply_type, :reply + attr_reader :reply_type, :reply def initialize(unstructured_reply:) - @reply_type = unstructured_reply["reply_type"] - @reply = unstructured_reply + @reply = sanitize_reply(unstructured_reply) + @reply_type = determine_reply_type end def [](key) @@ -23,14 +22,69 @@ def delay? reply_type == 'delay' end - def self.dynamic_delay - self.new( - unstructured_reply: { - 'reply_type' => 'delay', - 'duration' => 'dynamic' - } - ) + private + + def determine_reply_type + # WIP + # makes lighter synthax for text reply type + if @reply.key?(:text) || @reply[:reply_type] == 'text' + 'text' + elsif @reply[:reply_type] == 'image' + 'image' + elsif @reply[:reply_type] == 'dropdown' + 'dropdown' + else + raise "No valid reply type found." + end + end + + def sanitize_reply(reply) + # If the reply is a String, default it to a text reply + if reply.is_a?(String) + { text: sanitize(reply) } + elsif reply.is_a?(Hash) + reply.transform_values { |value| sanitize(value) } + else + raise "Invalid reply type. Must be a String or Hash." + end end + def sanitize(value) + ActionView::Base.full_sanitizer.sanitize(value) + end end end + +# module Stealth +# class Reply + +# attr_accessor :reply_type, :reply + +# def initialize(unstructured_reply:) +# @reply_type = unstructured_reply["reply_type"] +# @reply = unstructured_reply +# end + +# def [](key) +# @reply[key] +# end + +# def []=(key, value) +# @reply[key] = value +# end + +# def delay? +# reply_type == 'delay' +# end + +# def self.dynamic_delay +# self.new( +# unstructured_reply: { +# 'reply_type' => 'delay', +# 'duration' => 'dynamic' +# } +# ) +# end + +# end +# end diff --git a/lib/stealth/reply_manager.rb b/lib/stealth/reply_manager.rb new file mode 100644 index 00000000..6e70668d --- /dev/null +++ b/lib/stealth/reply_manager.rb @@ -0,0 +1,88 @@ +module Stealth + class ReplyManager + def initialize + @replies = {} + end + + def register_reply(flow_name, state_name, &block) + flow_state_key = "#{flow_name}/#{state_name}".to_sym + @replies[flow_state_key] = block + end + + def trigger_reply(flow_name, state_name, service_event) + flow_state_key = "#{flow_name}/#{state_name}".to_sym + + # Check if reply is already registered + unless @replies.key?(flow_state_key) + # Load reply file based on flow and state if not registered + load_reply_file(flow_name, state_name) + end + + if @replies.key?(flow_state_key) + block = @replies[flow_state_key] + + # Always use DslEventContext if defined in the Rails app + context_class = defined?(Stealth::DslEventContext) ? Stealth::DslEventContext : Stealth::Controller + context = context_class.new(service_event: service_event) + + block_context = Class.new do + def initialize(context) + @context = context + end + + def method_missing(method_name, *args, **kwargs, &block) + if @context.respond_to?(method_name) + @context.public_send(method_name, *args, **kwargs, &block) + else + super + end + end + + def respond_to_missing?(method_name, include_private = false) + @context.respond_to?(method_name) || super + end + end.new(context) + + block_context.instance_exec(service_event, &block) + else + Stealth::Logger.l(topic: 'reply', message: "No reply found for #{flow_name} with state #{state_name}") + end + end + + def load_reply_file(flow_name, state_name) + reply_file_path = "stealth/replies/#{flow_name}/#{state_name}.rb" + + if File.exist?(reply_file_path) + load reply_file_path + else + Stealth::Logger.l(topic: 'reply', message: "Reply file not found: #{reply_file_path}") + end + end + + def self.instance + @instance ||= new + end + + def self.reply(&block) + # Search through the call stack to find the Rails app reply file path + file_path = caller_locations.find { |location| location.path.include?("stealth/replies") }.path + # Extract flow and state from the file path, assuming the path structure matches "stealth/replies/flow_name/state_name.rb" + match_data = file_path.match(%r{stealth/replies/(?[^/]+)/(?[^/]+)\.rb}) + + if match_data + flow_name = match_data[:flow_name] + state_name = match_data[:state_name] + + # Register the reply with inferred flow and state + instance.register_reply(flow_name, state_name, &block) + else + raise "Unable to determine flow and state from file path: #{file_path}" + end + end + + def self.trigger_reply(flow_name, state_name, service_event) + instance.trigger_reply(flow_name, state_name, service_event) + end + + end +end diff --git a/lib/stealth/reply_triggers.rb b/lib/stealth/reply_triggers.rb new file mode 100644 index 00000000..b6665fd0 --- /dev/null +++ b/lib/stealth/reply_triggers.rb @@ -0,0 +1,11 @@ +module Stealth + module ReplyTriggers + def trigger_reply(flow_name, state_name, service_event) + ReplyManager.trigger_reply(flow_name, state_name, service_event) + end + + def reply(&block) + ReplyManager.reply(&block) + end + end +end diff --git a/lib/stealth/scheduled_reply.rb b/lib/stealth/scheduled_reply.rb deleted file mode 100644 index f0a89adc..00000000 --- a/lib/stealth/scheduled_reply.rb +++ /dev/null @@ -1,18 +0,0 @@ -# coding: utf-8 -# frozen_string_literal: true - -module Stealth - - class ScheduledReplyJob < Stealth::Jobs - sidekiq_options queue: :stealth_replies, retry: false - - def perform(service, user_id, flow, state, target_id=nil) - service_message = ServiceMessage.new(service: service) - service_message.sender_id = user_id - service_message.target_id = target_id - controller = BotController.new(service_message: service_message) - controller.step_to(flow: flow, state: state) - end - end - -end diff --git a/lib/stealth/server.rb b/lib/stealth/server.rb deleted file mode 100644 index bcae5a02..00000000 --- a/lib/stealth/server.rb +++ /dev/null @@ -1,77 +0,0 @@ -# coding: utf-8 -# frozen_string_literal: true - -require 'sinatra/base' -require 'multi_json' - -module Stealth - class Server < Sinatra::Base - - def self.get_or_post(url, &block) - get(url, &block) - post(url, &block) - end - - get '/' do - <<~WELCOME - - - Stealth - - -
- - Stealth Logo - -
- - - WELCOME - end - - get_or_post '/incoming/:service' do - Stealth::Logger.l(topic: params[:service], message: 'Received webhook.') - - # JSON params need to be parsed and added to the params - if request.env['CONTENT_TYPE']&.match(/application\/json/i) - json_params = MultiJson.load(request.body.read) - - if bandwidth? - if json_params.is_a?(Array) - params.merge!(json_params.first) - else - return [200, 'Ok'] - end - else - params.merge!(json_params) - end - end - - dispatcher = Stealth::Dispatcher.new( - service: params[:service], - params: params, - headers: get_helpers_from_request(request) - ) - - headers 'Access-Control-Allow-Origin' => '*', - 'Access-Control-Allow-Methods' => ['OPTIONS', 'GET', 'POST'] - # content_type 'audio/mp3' - content_type 'application/octet-stream' - - dispatcher.coordinate - end - - private - - def get_helpers_from_request(request) - request.env.select do |header, value| - %w[HTTP_HOST].include?(header) - end - end - - def bandwidth? - params[:service] == "bandwidth" - end - - end -end diff --git a/lib/stealth/service_event.rb b/lib/stealth/service_event.rb new file mode 100644 index 00000000..f593c5ca --- /dev/null +++ b/lib/stealth/service_event.rb @@ -0,0 +1,24 @@ +module Stealth + class ServiceEvent + + attr_accessor :sender_id, # ID of the sender + :target_id, # ID of the target recipient + :timestamp, # Time when the event occurred + :service, # Service associated with the event + :event_type, # Type of event (phone, message, reaction, etc. WIP: We need to standardize these) + :event, # Name of event (call, hangup, message, etc. WIP: We need to standardize these) + :message, # Message content + :payload, # Payload information + :attachments, # Attachments + :location, # Location information + :reaction # Reaction to a message + :selected_option # Selected action from an interactive message + + def initialize(service:) + @service = service + @attachments = [] + @location = {} + end + + end +end diff --git a/lib/stealth/service_message.rb b/lib/stealth/service_message.rb deleted file mode 100644 index 4f4b7554..00000000 --- a/lib/stealth/service_message.rb +++ /dev/null @@ -1,18 +0,0 @@ -# coding: utf-8 -# frozen_string_literal: true - -module Stealth - class ServiceMessage - - attr_accessor :sender_id, :target_id, :timestamp, :service, :message, - :location, :attachments, :payload, :referral, :nlp_result, - :catch_all_reason, :confidence - - def initialize(service:) - @service = service - @attachments = [] - @location = {} - end - - end -end diff --git a/lib/stealth/service_reply.rb b/lib/stealth/service_reply.rb deleted file mode 100644 index 03b1debd..00000000 --- a/lib/stealth/service_reply.rb +++ /dev/null @@ -1,45 +0,0 @@ -# coding: utf-8 -# frozen_string_literal: true - -module Stealth - class ServiceReply - - attr_accessor :recipient_id, :replies, :yaml_reply, :context - - def initialize(recipient_id:, yaml_reply:, context:, preprocessor: :none) - @recipient_id = recipient_id - @yaml_reply = yaml_reply - @context = context - - processed_reply = case preprocessor - when :erb - preprocess_erb - when :none - @yaml_reply - end - - if yaml_reply.is_a?(Array) - @replies = load_replies(@yaml_reply) - else - @replies = load_replies(YAML.load(processed_reply)) - end - end - - private - - def load_replies(unstructured_replies) - unstructured_replies.collect do |reply| - Stealth::Reply.new(unstructured_reply: reply) - end - end - - def preprocess_erb - begin - ERB.new(yaml_reply).result(context) - rescue NameError => e - raise(Stealth::Errors::UndefinedVariable, e.message) - end - end - - end -end diff --git a/lib/stealth/services/base_client.rb b/lib/stealth/services/base_client.rb index 0520175d..f156b372 100644 --- a/lib/stealth/services/base_client.rb +++ b/lib/stealth/services/base_client.rb @@ -4,7 +4,7 @@ require 'stealth/services/base_reply_handler' require 'stealth/services/base_message_handler' -require 'stealth/services/jobs/handle_message_job' +# require 'stealth/services/jobs/handle_message_job' module Stealth module Services @@ -22,4 +22,4 @@ def transmit end end -end +end \ No newline at end of file diff --git a/lib/stealth/services/base_message_handler.rb b/lib/stealth/services/base_message_handler.rb index ff2ef9a3..58333d87 100644 --- a/lib/stealth/services/base_message_handler.rb +++ b/lib/stealth/services/base_message_handler.rb @@ -25,4 +25,4 @@ def process end end -end +end \ No newline at end of file diff --git a/lib/stealth/services/base_reply_handler.rb b/lib/stealth/services/base_reply_handler.rb index cd924c30..2b1396ea 100644 --- a/lib/stealth/services/base_reply_handler.rb +++ b/lib/stealth/services/base_reply_handler.rb @@ -70,4 +70,4 @@ def ssml end end -end +end \ No newline at end of file diff --git a/lib/stealth/services/jobs/handle_message_job.rb b/lib/stealth/services/jobs/handle_event_job.rb similarity index 88% rename from lib/stealth/services/jobs/handle_message_job.rb rename to lib/stealth/services/jobs/handle_event_job.rb index b3996b2b..bc8ab53d 100644 --- a/lib/stealth/services/jobs/handle_message_job.rb +++ b/lib/stealth/services/jobs/handle_event_job.rb @@ -4,7 +4,7 @@ module Stealth module Services - class HandleMessageJob < Stealth::Jobs + class HandleEventJob < Stealth::Jobs sidekiq_options queue: :stealth_webhooks, retry: false def perform(service, params, headers) @@ -19,4 +19,4 @@ def perform(service, params, headers) end end -end +end \ No newline at end of file diff --git a/lib/stealth/services/jobs/scheduled_reply_job.rb b/lib/stealth/services/jobs/scheduled_reply_job.rb new file mode 100644 index 00000000..64dceae4 --- /dev/null +++ b/lib/stealth/services/jobs/scheduled_reply_job.rb @@ -0,0 +1,33 @@ +# coding: utf-8 +# frozen_string_literal: true + +module Stealth + module Services + + # class ScheduledReplyJob < Stealth::Jobs + # sidekiq_options queue: :stealth_replies, retry: false + + # def perform(service, user_id, flow, state, target_id=nil) + # service_message = ServiceMessage.new(service: service) + # service_message.sender_id = user_id + # service_message.target_id = target_id + # controller = BotController.new(service_message: service_message) + # controller.step_to(flow: flow, state: state) + # end + # end + + class ScheduledReplyJob < Stealth::Jobs + sidekiq_options queue: :stealth_replies, retry: false + + def perform(service, user_id, flow, state, target_id=nil) + service_event = ServiceEvent.new(service: service) + service_event.sender_id = user_id + service_event.target_id = target_id + + controller = Stealth::Controller.new(service_event: service_event) + controller.step_to(flow: flow, state: state) + end + end + end + +end diff --git a/lib/stealth/session.rb b/lib/stealth/session.rb index 55562b9a..e4ef37bc 100644 --- a/lib/stealth/session.rb +++ b/lib/stealth/session.rb @@ -9,7 +9,7 @@ class Session SLUG_SEPARATOR = '->' attr_reader :flow, :state, :id, :type - attr_accessor :session + attr_accessor :session, :locals # Session types: # - :primary @@ -27,6 +27,7 @@ def initialize(id: nil, type: :primary) ) end + load_previous_locals get_session end @@ -48,15 +49,15 @@ def self.slugify(flow:, state:) [flow, state].join(SLUG_SEPARATOR) end - def flow - return nil if flow_string.blank? + # def flow + # return nil if flow_string.blank? - @flow ||= FlowMap.new.init(flow: flow_string, state: state_string) - end + # @flow ||= FlowMap.new.init(flow: flow_string, state: state_string) + # end - def state - flow&.current_state - end + # def state + # flow&.current_state + # end def flow_string session&.split(SLUG_SEPARATOR)&.first @@ -66,6 +67,23 @@ def state_string session&.split(SLUG_SEPARATOR)&.last end + def load_previous_locals + return if primary_session? + + @locals = get_key(previous_locals_key) + + if @locals.present? && @locals.is_a?(String) + begin + @locals = JSON.parse(@locals) + rescue JSON::ParserError => e + Stealth::Logger.l( + topic: "session", + message: "User #{id}: failed to parse locals from Redis -> #{@locals}, error: #{e.message}" + ) + end + end + end + def get_session @session ||= get_key(session_key) end @@ -175,6 +193,10 @@ def previous_session_key [id, 'previous'].join('-') end + def previous_locals_key + [id, 'previous', 'locals'].join('-') + end + def back_to_key [id, 'back_to'].join('-') end @@ -193,10 +215,8 @@ def store_current_to_previous(existing_session:) message: "User #{id}: setting to #{existing_session}" ) - persist_key( - key: previous_session_key, - value: existing_session - ) + persist_key(key: previous_session_key, value: existing_session) + persist_key(key: previous_locals_key, value: @locals.to_json) end end diff --git a/lib/stealth/version.rb b/lib/stealth/version.rb index 651a104a..aa125e47 100644 --- a/lib/stealth/version.rb +++ b/lib/stealth/version.rb @@ -1,12 +1,3 @@ -# coding: utf-8 -# frozen_string_literal: true - module Stealth - module Version - def self.version - File.read(File.join(File.dirname(__FILE__), '../..', 'VERSION')).strip - end - end - - VERSION = Version.version + VERSION = "3.0.0" end diff --git a/lib/tasks/stealth_tasks.rake b/lib/tasks/stealth_tasks.rake new file mode 100644 index 00000000..e8be2817 --- /dev/null +++ b/lib/tasks/stealth_tasks.rake @@ -0,0 +1,4 @@ +# desc "Explaining what the task does" +task :stealth do + # Task goes here +end diff --git a/spec/configuration_spec.rb b/spec/configuration_spec.rb deleted file mode 100644 index da998996..00000000 --- a/spec/configuration_spec.rb +++ /dev/null @@ -1,93 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe "Stealth::Configuration" do - - describe "accessing via method calling" do - let(:services_yml) { File.read(File.join(File.dirname(__FILE__), 'support', 'services.yml')) } - let(:parsed_config) { YAML.safe_load(ERB.new(services_yml).result, aliases: true)[Stealth.env] } - let(:config) { Stealth.load_services_config!(services_yml) } - - it "should return the root node" do - expect(config.facebook).to eq parsed_config['facebook'] - end - - it "should access deeply nested nodes" do - expect(config.facebook.setup.greeting).to eq parsed_config['facebook']['setup']['greeting'] - end - - it "should handle values that are arrays correctly" do - expect(config.facebook.setup.persistent_menu).to be_a(Array) - end - - it "should retain the configuration at the class level" do - expect(Stealth.config.facebook.setup.greeting).to eq parsed_config['facebook']['setup']['greeting'] - end - - it "should handle multiple keys at the root level" do - expect(config.twilio_sms.account_sid).to eq parsed_config['twilio_sms']['account_sid'] - end - - it "should return nil if the key is not present at the node" do - expect(config.twilio_sms.api_key).to be nil - end - - it "should raise a NoMethodError when accessing multi-levels of missing nodes" do - expect { config.slack.api_key }.to raise_error(NoMethodError) - end - end - - describe "config files with ERB" do - let(:services_yml) { File.read(File.join(File.dirname(__FILE__), 'support', 'services_with_erb.yml')) } - let(:config) { Stealth.load_services_config!(services_yml) } - - it "should replace available embedded env vars" do - ENV['FACEBOOK_VERIFY_TOKEN'] = 'it works' - expect(config.facebook.verify_token).to eq 'it works' - end - - it "should replace unavailable embedded env vars with nil" do - expect(config.facebook.challenge).to be_nil - end - - it "should not reload the configuration file if one already exists" do - Stealth.load_services_config(services_yml) - expect(config.facebook.challenge).to be_nil - end - end - - describe "configuring with default values" do - let(:config) { - Stealth::Configuration.new( - { 'a' => nil, 'x' => 0, 'y' => false, 'z' => '' } - ) - } - - it 'should replace a nil value' do - config.set_default('a', 99) - expect(config.a).to eq 99 - end - - it 'should NOT replace a zero value' do - config.set_default('x', 99) - expect(config.x).to eq 0 - end - - it 'should NOT replace a false value' do - config.set_default('y', 99) - expect(config.y).to be false - end - - it 'should NOT replace a blank string value' do - config.set_default('z', 99) - expect(config.z).to eq '' - end - - it 'should replace a not-set key' do - config.set_default('zz', 99) - expect(config.zz).to eq 99 - end - end - -end diff --git a/spec/controller/callbacks_spec.rb b/spec/controller/callbacks_spec.rb deleted file mode 100644 index 8eeef407..00000000 --- a/spec/controller/callbacks_spec.rb +++ /dev/null @@ -1,217 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -$history = [] - -class BotController < Stealth::Controller - before_action :fetch_user_name - - def some_action - step_to flow: 'flow_tester', state: 'my_action' - end - - def other_action - step_to flow: 'other_flow_tester', state: 'other_action' - end - - def halted_action - step_to flow: 'flow_tester', state: 'my_action2' - end - - def filtered_action - step_to flow: 'flow_tester', state: 'my_action3' - end - - def some_other_action2 - step_to flow: 'other_flow_tester', state: 'other_action2' - end - - def some_other_action3 - step_to flow: 'other_flow_tester', state: 'other_action3' - end - - def some_other_action4 - step_to flow: 'other_flow_tester', state: 'other_action4' - end - - def some_other_action5 - step_to flow: 'other_flow_tester', state: 'other_action5' - end - - private - - def fetch_user_name - $history << "fetched user name" - end -end - -class FlowTestersController < BotController - before_action :test_before_halting, only: :my_action2 - before_action :test_action - before_action :test_filtering, except: [:my_action, :my_action2] - - attr_reader :action_ran - - def my_action - - end - - def my_action2 - - end - - def my_action3 - - end - - protected - - def test_action - $history << "tested action" - end - - def test_before_halting - throw(:abort) - end - - def test_filtering - $history << "filtered" - end - - def test_after_halting - $history << "after action ran" - end -end - -class OtherFlowTestersController < BotController - after_action :after_action1, only: :other_action2 - after_action :after_action2, only: :other_action2 - - before_action :run_halt, only: [:other_action3, :other_action5] - after_action :after_action3, only: :other_action3 - - around_action :run_around_filter, only: [:other_action4, :other_action5] - - def other_action - - end - - def other_action2 - - end - - def other_action3 - - end - - def other_action4 - - end - - def other_action5 - - end - - private - - def after_action1 - $history << "after action 1" - end - - def after_action2 - $history << "after action 2" - end - - def run_halt - throw(:abort) - end - - def run_around_filter - $history << "around before" - yield - $history << "around after" - end -end - -class FlowMap - include Stealth::Flow - - flow :flow_tester do - state :my_action - state :my_action2 - state :my_action3 - end - - flow :other_flow_tester do - state :other_action - state :other_action2 - state :other_action3 - state :other_action4 - state :other_action5 - end -end - -describe "Stealth::Controller callbacks" do - - let(:facebook_message) { SampleMessage.new(service: 'facebook') } - - before(:each) do - $history = [] - end - - describe "before_action" do - it "should fire the callback on the parent class" do - controller = BotController.new(service_message: facebook_message.message_with_text) - controller.other_action - expect($history).to eq ["fetched user name"] - end - - it "should fire the callback on a child class" do - controller = FlowTestersController.new(service_message: facebook_message.message_with_text) - controller.some_action - expect($history).to eq ["fetched user name", "tested action"] - end - - it "should halt the callback chain when :abort is thrown" do - controller = FlowTestersController.new(service_message: facebook_message.message_with_text) - controller.halted_action - expect($history).to eq ["fetched user name"] - end - - it "should respect 'unless' filter" do - controller = FlowTestersController.new(service_message: facebook_message.message_with_text) - controller.filtered_action - expect($history).to eq ["fetched user name", "tested action", "filtered"] - end - end - - describe "after_action" do - it "should fire the after callbacks in reverse order" do - controller = OtherFlowTestersController.new(service_message: facebook_message.message_with_text) - controller.some_other_action2 - expect($history).to eq ["fetched user name", "after action 2", "after action 1"] - end - - it "should not fire after callbacks if a before callback throws an :abort" do - controller = OtherFlowTestersController.new(service_message: facebook_message.message_with_text) - controller.some_other_action3 - expect($history).to eq ["fetched user name"] - end - end - - describe "around_action" do - it "should fire the around callback before and after" do - controller = OtherFlowTestersController.new(service_message: facebook_message.message_with_text) - controller.some_other_action4 - expect($history).to eq ["fetched user name", "around before", "around after"] - end - - it "should not fire the around callback if a before callback throws abort" do - controller = OtherFlowTestersController.new(service_message: facebook_message.message_with_text) - controller.some_other_action5 - expect($history).to eq ["fetched user name"] - end - end - -end diff --git a/spec/controller/catch_all_spec.rb b/spec/controller/catch_all_spec.rb deleted file mode 100644 index 8863d50a..00000000 --- a/spec/controller/catch_all_spec.rb +++ /dev/null @@ -1,160 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe "Stealth::Controller::CatchAll" do - $msg = nil - - class StubbedCatchAllsController < Stealth::Controller - def level1 - $msg = current_message - do_nothing - end - - def level2 - do_nothing - end - - def level3 - do_nothing - end - end - - class FlowMap - include Stealth::Flow - - flow :vader do - state :my_action - state :my_action2 - state :my_action3 - state :action_with_unrecognized_msg - state :action_with_unrecognized_match - end - - flow :catch_all do - state :level1 - state :level2 - state :level3 - end - end - - let(:facebook_message) { SampleMessage.new(service: 'facebook') } - let(:controller) { VadersController.new(service_message: facebook_message.message_with_text) } - - describe "when a CatchAll flow is defined" do - before(:each) do - stub_const("CatchAllsController", StubbedCatchAllsController) - end - - after(:each) do - $redis.flushdb - end - - it "should step_to catch_all->level1 when a StandardError is raised" do - controller.current_session.session = Stealth::Session.canonical_session_slug(flow: 'vader', state: 'my_action') - controller.action(action: :my_action) - expect($redis.get(controller.current_session.session_key)).to eq('catch_all->level1') - end - - it "should step_to catch_all->level1 when an action doesn't progress the flow" do - controller.current_session.session = Stealth::Session.canonical_session_slug(flow: 'vader', state: 'my_action2') - controller.action(action: :my_action2) - expect($redis.get(controller.current_session.session_key)).to eq('catch_all->level1') - end - - it "should step_to catch_all->level2 when an action raises back to back" do - controller.step_to flow: :vader, state: :my_action - controller.step_to flow: :vader, state: :my_action - expect($redis.get(controller.current_session.session_key)).to eq('catch_all->level2') - end - - it "should step_to catch_all->level3 when an action raises back to back to back" do - controller.step_to flow: :vader, state: :my_action - controller.step_to flow: :vader, state: :my_action - controller.step_to flow: :vader, state: :my_action - expect($redis.get(controller.current_session.session_key)).to eq('catch_all->level3') - end - - it "should just stop after the maximum number of catch_all levels have been reached" do - controller.step_to flow: :vader, state: :my_action - controller.step_to flow: :vader, state: :my_action - controller.step_to flow: :vader, state: :my_action - controller.step_to flow: :vader, state: :my_action - expect($redis.get(controller.current_session.session_key)).to eq('vader->my_action') - end - - it "should NOT run the catch_all if do_nothing is called" do - controller.current_session.set_session(new_flow: 'vader', new_state: 'my_action3') - controller.action(action: :my_action3) - expect($redis.get(controller.current_session.session_key)).to eq('vader->my_action3') - end - - describe "catch_alls from within catch_all flow" do - let(:e) { - e = OpenStruct.new - e.class = RuntimeError - e.message = 'oops' - e.backtrace = [ - '/stealth/lib/stealth/controller/controller.rb', - '/stealth/lib/stealth/controller/catch_all.rb', - ] - e - } - - before(:each) do - controller.current_session.session = Stealth::Session.canonical_session_slug(flow: 'catch_all', state: 'level1') - end - - it "should not step_to to catch_all" do - expect(controller).to_not receive(:step_to) - controller.run_catch_all(err: e) - end - - it "should return false" do - expect(controller.run_catch_all(err: e)).to be false - end - - it "should log the error message" do - err_klass = if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.0.0') - "RuntimeError" - else - "OpenStruct" - end - - expect(Stealth::Logger).to receive(:l).with(topic: 'catch_all', message: "[Level 1] for user #{facebook_message.sender_id} #{err_klass}\noops\n/stealth/lib/stealth/controller/controller.rb\n/stealth/lib/stealth/controller/catch_all.rb") - expect(Stealth::Logger).to receive(:l).with(topic: 'catch_all', message: "CatchAll triggered for user #{facebook_message.sender_id} from within CatchAll; ignoring.") - controller.run_catch_all(err: e) - end - end - - describe "catch_all_reason" do - before(:each) do - @session = Stealth::Session.new(id: controller.current_session_id) - @session.set_session(new_flow: 'vader', new_state: 'my_action2') - end - - after(:each) do - $msg = nil - end - - it 'should have access to the error raised in current_message.catch_all_reason' do - controller.action(action: :my_action) - expect($msg.catch_all_reason).to be_a(Hash) - expect($msg.catch_all_reason[:err]).to eq(RuntimeError) - expect($msg.catch_all_reason[:err_msg]).to eq('oops') - end - - it 'should have the correct error when handle_message fails to recognize a message' do - controller.action(action: :action_with_unrecognized_msg) - expect($msg.catch_all_reason[:err]).to eq(Stealth::Errors::UnrecognizedMessage) - expect($msg.catch_all_reason[:err_msg]).to eq("The reply '#{facebook_message.message_with_text.message}' was not recognized.") - end - - it 'should have the correct error when get_match fails to recognize a message' do - controller.action(action: :action_with_unrecognized_match) - expect($msg.catch_all_reason[:err]).to eq(Stealth::Errors::UnrecognizedMessage) - expect($msg.catch_all_reason[:err_msg]).to eq("The reply '#{facebook_message.message_with_text.message}' was not recognized.") - end - end - end -end diff --git a/spec/controller/controller_spec.rb b/spec/controller/controller_spec.rb deleted file mode 100644 index de3b6d61..00000000 --- a/spec/controller/controller_spec.rb +++ /dev/null @@ -1,923 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe "Stealth::Controller" do - - class MrRobotsController < Stealth::Controller - def my_action - [:success, :my_action] - end - - def my_action2 - [:success, :my_action2] - end - - def my_action3 - [:success, :my_action3] - end - end - - class MrTronsController < Stealth::Controller - def other_action - - end - - def other_action2 - step_to state: :other_action4 - end - - def other_action3 - - end - - def other_action4 - do_nothing - end - - def broken_action - raise StandardError - end - - def parts_unknown - step_to flow: :parts, state: :unknown - end - - def halted - halt! - step_to state: :other_action2 - end - end - - class FlowMap - include Stealth::Flow - - flow :mr_robot do - state :my_action - state :my_action2 - state :my_action3 - end - - flow :mr_tron do - state :other_action - state :other_action2 - state :other_action3 - state :other_action4 - state :broken_action - state :part_unknown - state :halted - state :deprecated_action, redirects_to: :other_action - state :deprecated_action2, redirects_to: 'mr_robot->my_action' - end - end - - let(:facebook_message) { SampleMessage.new(service: 'facebook') } - let(:controller) { - MrTronsController.new(service_message: facebook_message.message_with_text) - } - - describe "convenience methods" do - it "should make the session ID accessible via current_session_id" do - controller.current_session.set_session(new_flow: 'mr_tron', new_state: 'other_action') - - expect(controller.current_session_id).to eq(facebook_message.sender_id) - end - - it "should make the message available in current_message.message" do - expect(controller.current_message.message).to eq(facebook_message.message) - end - - it "should make the payload available in current_message.payload" do - message_with_payload = facebook_message.message_with_payload - expect(controller.current_message.payload).to eq(message_with_payload.payload) - end - - describe "current_service" do - let(:twilio_message) { SampleMessage.new(service: 'twilio') } - let(:controller_with_twilio_message) { MrTronsController.new(service_message: twilio_message.message_with_text) } - - it "should detect a Facebook message" do - expect(controller.current_service).to eq('facebook') - end - - it "should detect a Twilio message" do - expect(controller_with_twilio_message.current_service).to eq('twilio') - end - end - - describe "messages with location" do - let(:message_with_location) { facebook_message.message_with_location } - let(:controller_with_location) { MrTronsController.new(service_message: message_with_location) } - - it "should make the location available in current_message.location" do - expect(controller_with_location.current_message.location).to eq(message_with_location.location) - end - - it "should return true for current_message.has_location?" do - expect(controller_with_location.has_location?).to be true - end - end - - describe "messages with attachments" do - let(:message_with_attachments) { facebook_message.message_with_attachments } - let(:controller_with_attachment) { MrTronsController.new(service_message: message_with_attachments) } - - it "should make the attachments available in current_message.attachments" do - expect(controller_with_attachment.current_message.attachments).to eq(message_with_attachments.attachments) - end - - it "should return true for current_message.has_attachments?" do - expect(controller_with_attachment.has_attachments?).to be true - end - end - end - - describe "states with redirect_to specified" do - it "should step_to the specified redirect state when only a state is specified" do - controller.current_session.session = Stealth::Session.canonical_session_slug(flow: 'mr_tron', state: 'deprecated_action') - expect(MrTronsController).to receive(:new).and_return(controller) - expect(controller).to receive(:other_action) - controller.action(action: :deprecated_action) - end - - it "should step_to the specified redirect flow and state when a session is specified" do - controller.current_session.session = Stealth::Session.canonical_session_slug(flow: 'mr_tron', state: 'deprecated_action2') - mr_robot_controller = MrRobotsController.new(service_message: facebook_message.message_with_text) - - allow(MrRobotsController).to receive(:new).and_return(mr_robot_controller) - expect(mr_robot_controller).to receive(:my_action) - controller.action(action: :deprecated_action2) - end - - it "should NOT call the redirected controller action method" do - controller.current_session.session = Stealth::Session.canonical_session_slug(flow: 'mr_tron', state: 'deprecated_action') - expect(MrTronsController).to receive(:new).and_return(controller) - expect(controller).to_not receive(:deprecated_action) - controller.action(action: :deprecated_action) - end - end - - describe "step_to" do - it "should raise an ArgumentError if a session, flow, or state is not specified" do - expect { - controller.step_to - }.to raise_error(ArgumentError) - end - - it "should call the flow's first state's controller action when only a flow is provided" do - expect_any_instance_of(MrRobotsController).to receive(:my_action) - controller.step_to flow: "mr_robot" - end - - it "should call a controller's corresponding action when only a state is provided" do - expect_any_instance_of(MrTronsController).to receive(:other_action3) - - controller.current_session.set_session(new_flow: 'mr_tron', new_state: 'other_action') - - controller.step_to state: "other_action3" - end - - it "should call a controller's corresponding action when a state and flow is provided" do - expect_any_instance_of(MrRobotsController).to receive(:my_action3) - controller.step_to flow: "mr_robot", state: "my_action3" - end - - it "should call a controller's corresponding action when a session is provided" do - expect_any_instance_of(MrRobotsController).to receive(:my_action3) - - allow(controller.current_session).to receive(:flow_string).and_return("mr_robot") - allow(controller.current_session).to receive(:state_string).and_return("my_action3") - - controller.step_to session: controller.current_session - end - - it "should call a controller's corresponding action when a session slug is provided" do - expect_any_instance_of(MrRobotsController).to receive(:my_action3) - controller.step_to slug: 'mr_robot->my_action3' - end - - it "should pass along the service_message" do - robot_controller_dbl = double('MrRobotsController').as_null_object - expect(MrRobotsController).to receive(:new).with(service_message: controller.current_message, pos: nil).and_return(robot_controller_dbl) - controller.step_to flow: :mr_robot, state: :my_action3 - end - - it "should accept flow and string specified as symbols" do - expect_any_instance_of(MrRobotsController).to receive(:my_action3) - controller.step_to flow: :mr_robot, state: :my_action3 - end - - it "should check if an interruption occured" do - expect(controller).to receive(:interrupt_detected?).and_return(false) - controller.step_to flow: :mr_robot, state: :my_action3 - end - - it "should call run_interrupt_action if an interruption occured and return" do - expect(controller).to receive(:interrupt_detected?).and_return(true) - expect(controller).to receive(:run_interrupt_action) - expect(controller.step_to(flow: :mr_robot, state: :my_action3)).to eq :interrupted - end - - it "should set @pos if it is specified in the arguments" do - expect_any_instance_of(MrRobotsController).to receive(:my_action3) - controller.step_to flow: :mr_robot, state: :my_action3, pos: -1 - expect(controller.pos).to eq -1 - end - - it "should leave @pos as nil if the pos argument is not specified" do - expect_any_instance_of(MrRobotsController).to receive(:my_action3) - controller.step_to flow: :mr_robot, state: :my_action3 - expect(controller.pos).to be_nil - end - end - - describe "update_session_to" do - it "should raise an ArgumentError if a session, flow, or state is not specified" do - expect { - controller.update_session_to - }.to raise_error(ArgumentError) - end - - it "should update session to flow's first state's controller action when only a flow is provided" do - expect_any_instance_of(MrRobotsController).to_not receive(:my_action) - - controller.update_session_to flow: "mr_robot" - expect(controller.current_session.flow_string).to eq('mr_robot') - expect(controller.current_session.state_string).to eq('my_action') - end - - it "should update session to controller's corresponding action when only a state is provided" do - expect_any_instance_of(MrTronsController).to_not receive(:other_action3) - - controller.current_session.set_session(new_flow: 'mr_tron', new_state: 'other_action') - - controller.update_session_to state: "other_action3" - expect(controller.current_session.flow_string).to eq('mr_tron') - expect(controller.current_session.state_string).to eq('other_action3') - end - - it "should update session to controller's corresponding action when a state and flow is provided" do - expect_any_instance_of(MrRobotsController).to_not receive(:my_action3) - - controller.update_session_to flow: "mr_robot", state: "my_action3" - expect(controller.current_session.flow_string).to eq('mr_robot') - expect(controller.current_session.state_string).to eq('my_action3') - end - - it "should update session to controller's corresponding action when a session is provided" do - expect_any_instance_of(MrRobotsController).to_not receive(:my_action3) - - session = Stealth::Session.new(id: controller.current_session_id) - session.set_session(new_flow: 'mr_robot', new_state: 'my_action3') - - controller.update_session_to session: session - expect(controller.current_session.flow_string).to eq('mr_robot') - expect(controller.current_session.state_string).to eq('my_action3') - end - - it "should update session to controller's corresponding action when a session slug is provided" do - expect_any_instance_of(MrRobotsController).to_not receive(:my_action3) - expect(controller.current_session.flow_string).to eq('mr_robot') - expect(controller.current_session.state_string).to eq('my_action3') - - controller.update_session_to slug: 'mr_robot->my_action3' - end - - it "should accept flow and string specified as symbols" do - expect_any_instance_of(MrRobotsController).to_not receive(:my_action3) - - controller.update_session_to flow: :mr_robot, state: :my_action3 - expect(controller.current_session.flow_string).to eq('mr_robot') - expect(controller.current_session.state_string).to eq('my_action3') - end - - it "should check if an interruption occured" do - expect(controller).to receive(:interrupt_detected?).and_return(false) - controller.update_session_to flow: :mr_robot, state: :my_action3 - end - - it "should call run_interrupt_action if an interruption occured and return" do - expect(controller).to receive(:interrupt_detected?).and_return(true) - expect(controller).to receive(:run_interrupt_action) - expect(controller.update_session_to(flow: :mr_robot, state: :my_action3)).to eq :interrupted - end - end - - describe "step_to_in" do - it "should raise an ArgumentError if a session, flow, or state is not specified" do - expect { - controller.step_to_in - }.to raise_error(ArgumentError) - end - - it "should raise an ArgumentError if delay is not specifed as an ActiveSupport::Duration" do - expect { - controller.step_to_in DateTime.now, flow: 'mr_robot' - }.to raise_error(ArgumentError) - end - - it "should schedule a transition to flow's first state's controller action when only a flow is provided" do - expect_any_instance_of(MrRobotsController).to_not receive(:my_action) - - expect(Stealth::ScheduledReplyJob).to receive(:perform_in).with( - 100.seconds, - controller.current_service, - controller.current_session_id, - 'mr_robot', - 'my_action', - nil - ) - - expect { - controller.step_to_in 100.seconds, flow: "mr_robot" - }.to_not change(controller.current_session, :get_session) - end - - it "should schedule a transition to controller's corresponding action when only a state is provided" do - expect_any_instance_of(MrRobotsController).to_not receive(:my_action) - - controller.current_session.set_session(new_flow: 'mr_tron', new_state: 'other_action') - - expect(Stealth::ScheduledReplyJob).to receive(:perform_in).with( - 100.seconds, - controller.current_service, - controller.current_session_id, - 'mr_tron', - 'other_action3', - nil - ) - - expect { - controller.step_to_in 100.seconds, state: "other_action3" - }.to_not change(controller.current_session, :get_session) - end - - it "should update session to controller's corresponding action when a state and flow is provided" do - expect_any_instance_of(MrRobotsController).to_not receive(:my_action) - - expect(Stealth::ScheduledReplyJob).to receive(:perform_in).with( - 100.seconds, - controller.current_service, - controller.current_session_id, - 'mr_robot', - 'my_action3', - nil - ) - - expect { - controller.step_to_in 100.seconds, flow: 'mr_robot', state: "my_action3" - }.to_not change(controller.current_session, :get_session) - end - - it "should update session to controller's corresponding action when a session is provided" do - expect_any_instance_of(MrRobotsController).to_not receive(:my_action) - - session = Stealth::Session.new(id: controller.current_session_id) - session.set_session(new_flow: 'mr_robot', new_state: 'my_action3') - - expect(Stealth::ScheduledReplyJob).to receive(:perform_in).with( - 100.seconds, - controller.current_service, - controller.current_session_id, - 'mr_robot', - 'my_action3', - nil - ) - - expect { - controller.step_to_in 100.seconds, session: session - }.to_not change(controller.current_session, :get_session) - end - - it "should update session to controller's corresponding action when a session slug is provided" do - expect_any_instance_of(MrRobotsController).to_not receive(:my_action) - - expect(Stealth::ScheduledReplyJob).to receive(:perform_in).with( - 100.seconds, - controller.current_service, - controller.current_session_id, - 'mr_robot', - 'my_action3', - nil - ) - - expect { - controller.step_to_in 100.seconds, slug: 'mr_robot->my_action3' - }.to_not change(controller.current_session, :get_session) - end - - it "should accept flow and string specified as symbols" do - expect_any_instance_of(MrRobotsController).to_not receive(:my_action) - - expect(Stealth::ScheduledReplyJob).to receive(:perform_in).with( - 100.seconds, - controller.current_service, - controller.current_session_id, - 'mr_robot', - 'my_action3', - nil - ) - - expect { - controller.step_to_in 100.seconds, flow: :mr_robot, state: :my_action3 - }.to_not change(controller.current_session, :get_session) - end - - it "should pass along the target_id if set on the message" do - expect(Stealth::ScheduledReplyJob).to receive(:perform_in).with( - 100.seconds, - controller.current_service, - controller.current_session_id, - 'mr_robot', - 'my_action3', - '+18885551212' - ) - - controller.current_message.target_id = '+18885551212' - controller.step_to_in 100.seconds, flow: :mr_robot, state: :my_action3 - end - - it "should check if an interruption occured" do - expect(controller).to receive(:interrupt_detected?).and_return(false) - controller.step_to_in 100.seconds, flow: :mr_robot, state: :my_action3 - end - - it "should call run_interrupt_action if an interruption occured and return" do - expect(controller).to receive(:interrupt_detected?).and_return(true) - expect(controller).to receive(:run_interrupt_action) - expect(controller.step_to_in(100.seconds, flow: :mr_robot, state: :my_action3)).to eq :interrupted - end - end - - describe "step_to_at" do - let(:future_timestamp) { DateTime.now + 10.hours } - - it "should raise an ArgumentError if a session, flow, or state is not specified" do - expect { - controller.step_to_at - }.to raise_error(ArgumentError) - end - - it "should raise an ArgumentError if delay is not specifed as a DateTime" do - expect { - controller.step_to_at 100.seconds, flow: 'mr_robot' - }.to raise_error(ArgumentError) - end - - it "should schedule a transition to flow's first state's controller action when only a flow is provided" do - expect_any_instance_of(MrRobotsController).to_not receive(:my_action) - - expect(Stealth::ScheduledReplyJob).to receive(:perform_at).with( - future_timestamp, - controller.current_service, - controller.current_session_id, - 'mr_robot', - 'my_action', - nil - ) - - expect { - controller.step_to_at future_timestamp, flow: "mr_robot" - }.to_not change(controller.current_session, :get_session) - end - - it "should schedule a transition to controller's corresponding action when only a state is provided" do - expect_any_instance_of(MrRobotsController).to_not receive(:my_action) - - controller.current_session.set_session(new_flow: 'mr_tron', new_state: 'other_action') - - expect(Stealth::ScheduledReplyJob).to receive(:perform_at).with( - future_timestamp, - controller.current_service, - controller.current_session_id, - 'mr_tron', - 'other_action3', - nil - ) - - expect { - controller.step_to_at future_timestamp, state: "other_action3" - }.to_not change(controller.current_session, :get_session) - end - - it "should update session to controller's corresponding action when a state and flow is provided" do - expect_any_instance_of(MrRobotsController).to_not receive(:my_action) - - expect(Stealth::ScheduledReplyJob).to receive(:perform_at).with( - future_timestamp, - controller.current_service, - controller.current_session_id, - 'mr_robot', - 'my_action3', - nil - ) - - expect { - controller.step_to_at future_timestamp, flow: 'mr_robot', state: "my_action3" - }.to_not change(controller.current_session, :get_session) - end - - it "should update session to controller's corresponding action when a session is provided" do - expect_any_instance_of(MrRobotsController).to_not receive(:my_action) - - session = Stealth::Session.new(id: controller.current_session_id) - session.set_session(new_flow: 'mr_robot', new_state: 'my_action3') - - expect(Stealth::ScheduledReplyJob).to receive(:perform_at).with( - future_timestamp, - controller.current_service, - controller.current_session_id, - 'mr_robot', - 'my_action3', - nil - ) - - expect { - controller.step_to_at future_timestamp, session: session - }.to_not change(controller.current_session, :get_session) - end - - it "should update session to controller's corresponding action when a session slug is provided" do - expect_any_instance_of(MrRobotsController).to_not receive(:my_action) - - expect(Stealth::ScheduledReplyJob).to receive(:perform_at).with( - future_timestamp, - controller.current_service, - controller.current_session_id, - 'mr_robot', - 'my_action3', - nil - ) - - expect { - controller.step_to_at future_timestamp, slug: 'mr_robot->my_action3' - }.to_not change(controller.current_session, :get_session) - end - - it "should accept flow and string specified as symbols" do - expect_any_instance_of(MrRobotsController).to_not receive(:my_action) - - expect(Stealth::ScheduledReplyJob).to receive(:perform_at).with( - future_timestamp, - controller.current_service, - controller.current_session_id, - 'mr_robot', - 'my_action3', - nil - ) - - expect { - controller.step_to_at future_timestamp, flow: :mr_robot, state: :my_action3 - }.to_not change(controller.current_session, :get_session) - end - - it "should pass along the target_id if set on the message" do - expect(Stealth::ScheduledReplyJob).to receive(:perform_at).with( - future_timestamp, - controller.current_service, - controller.current_session_id, - 'mr_robot', - 'my_action3', - '+18885551212' - ) - - controller.current_message.target_id = '+18885551212' - controller.step_to_at future_timestamp, flow: :mr_robot, state: :my_action3 - end - - it "should check if an interruption occured" do - expect(controller).to receive(:interrupt_detected?).and_return(false) - controller.step_to_at future_timestamp, flow: :mr_robot, state: :my_action3 - end - - it "should call run_interrupt_action if an interruption occured and return" do - expect(controller).to receive(:interrupt_detected?).and_return(true) - expect(controller).to receive(:run_interrupt_action) - expect(controller.step_to_at(future_timestamp, flow: :mr_robot, state: :my_action3)).to eq :interrupted - end - end - - describe "set_back_to" do - it "should raise an ArgumentError if a session, flow, or state is not specified" do - expect { - controller.set_back_to - }.to raise_error(ArgumentError) - end - - it "should call the flow's first state's controller action when only a flow is provided" do - expect { - controller.set_back_to(flow: :mr_robot) - }.to change{ $redis.get([controller.current_session_id, 'back_to'].join('-')) }.to('mr_robot->my_action') - end - - it "should call a controller's corresponding action when only a state is provided" do - controller.current_session.set_session(new_flow: 'mr_tron', new_state: 'other_action') - - expect { - controller.set_back_to(state: :other_action3) - }.to change{ $redis.get([controller.current_session_id, 'back_to'].join('-')) }.to('mr_tron->other_action3') - end - - it "should call a controller's corresponding action when a state and flow is provided" do - expect { - controller.set_back_to(flow: 'marco', state: 'polo') - }.to change{ $redis.get([controller.current_session_id, 'back_to'].join('-')) }.to('marco->polo') - end - - it "should call a controller's corresponding action when a session is provided" do - allow(controller.current_session).to receive(:flow_string).and_return("mr_robot") - allow(controller.current_session).to receive(:state_string).and_return("my_action3") - - expect { - controller.set_back_to(session: controller.current_session) - }.to change{ $redis.get([controller.current_session_id, 'back_to'].join('-')) }.to('mr_robot->my_action3') - end - - it "should call a controller's corresponding action when a session slug is provided" do - expect { - controller.set_back_to(slug: 'marco->polo') - }.to change{ $redis.get([controller.current_session_id, 'back_to'].join('-')) }.to('marco->polo') - end - - it "should default to the scoped flow if one is not specified" do - controller.current_session.set_session(new_flow: :mr_tron, new_state: :other_action) - expect { - controller.set_back_to(state: 'polo') - }.to change{ $redis.get([controller.current_session_id, 'back_to'].join('-')) }.to('mr_tron->polo') - end - - it "should overwrite the existing back_to_session if one is already present" do - $redis.set([controller.current_session_id, 'back_to'].join('-'), 'marco->polo') - controller.current_session.set_session(new_flow: :mr_tron, new_state: :other_action) - expect { - controller.set_back_to(state: 'other_action') - }.to change{ $redis.get([controller.current_session_id, 'back_to'].join('-')) }.from('marco->polo').to('mr_tron->other_action') - end - - it "should check if an interruption occured" do - expect(controller).to receive(:interrupt_detected?).and_return(false) - controller.set_back_to flow: :mr_robot, state: :my_action3 - end - - it "should call run_interrupt_action if an interruption occured and return" do - expect(controller).to receive(:interrupt_detected?).and_return(true) - expect(controller).to receive(:run_interrupt_action) - expect(controller.set_back_to(flow: :mr_robot, state: :my_action3)).to eq :interrupted - end - end - - describe "step_back" do - let(:back_to_slug) { [controller.current_session_id, 'back_to'].join('-') } - - it "should raise Stealth::Errors::InvalidStateTransition if back_to_session is not set" do - $redis.del(back_to_slug) - expect { - controller.step_back - }.to raise_error(Stealth::Errors::InvalidStateTransition) - end - - it "should step_to the stored back_to_session" do - controller.set_back_to(flow: 'marco', state: 'polo') - back_to_session = Stealth::Session.new( - id: controller.current_session_id, - type: :back_to - ) - - # We need to control the returned session object so the IDs match - expect(Stealth::Session).to receive(:new).with( - id: controller.current_session_id, - type: :back_to - ).and_return(back_to_session) - expect(controller).to receive(:step_to).with(session: back_to_session) - - controller.step_back - end - - it "should check if an interruption occured" do - controller.set_back_to(flow: :mr_robot, state: :my_action3) - expect(controller).to receive(:interrupt_detected?).and_return(false) - controller.step_back - end - - it "should call run_interrupt_action if an interruption occured and return" do - controller.set_back_to(flow: :mr_robot, state: :my_action3) - expect(controller).to receive(:interrupt_detected?).and_return(true) - expect(controller).to receive(:run_interrupt_action) - expect(controller.step_back).to eq :interrupted - end - end - - describe "progressed?" do - it "should be truthy if an action calls step_to" do - expect(controller.progressed?).to be_falsey - controller.step_to flow: "mr_robot" - expect(controller.progressed?).to be_truthy - end - - it "should be falsey if an action only calls step_to_at" do - expect(controller.progressed?).to be_falsey - - expect(Stealth::ScheduledReplyJob).to receive(:perform_at) - controller.step_to_at (DateTime.now + 10.hours), flow: 'mr_robot' - - expect(controller.progressed?).to be_falsey - end - - it "should be falsey if an action only calls step_to_in" do - expect(controller.progressed?).to be_falsey - - expect(Stealth::ScheduledReplyJob).to receive(:perform_in) - controller.step_to_in 100.seconds, flow: 'mr_robot' - - expect(controller.progressed?).to be_falsey - end - - it "should be truthy if an action calls update_session_to" do - expect(controller.progressed?).to be_falsey - controller.update_session_to flow: "mr_robot" - expect(controller.progressed?).to be_truthy - end - - it "should be truthy if an action sends replies" do - expect(controller.progressed?).to be_falsey - - # Stub out a service reply -- we just want send_replies to succeed here - stubbed_service_reply = double("service_reply") - allow(controller).to receive(:action_replies).and_return([], :erb) - allow(stubbed_service_reply).to receive(:replies).and_return([]) - allow(Stealth::ServiceReply).to receive(:new).and_return(stubbed_service_reply) - - controller.send_replies - expect(controller.progressed?).to be_truthy - end - - it "should be falsey otherwise" do - allow(controller).to receive(:flow_controller).and_return(controller) - expect(controller.progressed?).to be_falsey - controller.action(action: :other_action) - expect(controller.progressed?).to be_falsey - end - end - - describe "do_nothing" do - it "should set progressed to truthy when called" do - allow(controller).to receive(:flow_controller).and_return(controller) - expect(controller.progressed?).to be_falsey - controller.action(action: :other_action4) - expect(controller.progressed?).to be_truthy - end - end - - describe "update_session" do - before(:each) do - controller.current_session.set_session(new_flow: 'mr_tron', new_state: 'other_action') - end - - it "should set progressed to :updated_session" do - controller.send(:update_session, flow: :mr_tron, state: :other_action) - expect(controller.progressed?).to eq :updated_session - end - - it "call set_session on the current_session with the new flow and state" do - controller.send(:update_session, flow: :mr_robot, state: :my_action) - expect(controller.current_session.flow_string).to eq 'mr_robot' - expect(controller.current_session.state_string).to eq 'my_action' - end - - it "should not call set_session on current_session if the flow and state match" do - expect_any_instance_of(Stealth::Session).to_not receive(:set_session) - controller.send(:update_session, flow: :mr_tron, state: :other_action) - end - end - - describe "dev jumps" do - let!(:dev_env) { ActiveSupport::StringInquirer.new('development') } - - describe "dev_jump_detected?" do - it "should return false if the enviornment is not 'development'" do - expect(Stealth.env).to eq 'test' - expect(controller.send(:dev_jump_detected?)).to be false - end - - it "should return false if the message does not match the jump format" do - allow(Stealth).to receive(:env).and_return(dev_env) - controller.current_message.message = 'hello world' - expect(Stealth.env.development?).to be true - expect(controller.send(:dev_jump_detected?)).to be false - end - - it "should return false if the message looks like an American date" do - allow(Stealth).to receive(:env).and_return(dev_env) - controller.current_message.message = '1/23/84' - expect(Stealth.env.development?).to be true - expect(controller.send(:dev_jump_detected?)).to be false - end - - it "should return false if the message looks like an American date that is zero padded" do - allow(Stealth).to receive(:env).and_return(dev_env) - controller.current_message.message = '01/23/1984' - expect(Stealth.env.development?).to be true - expect(controller.send(:dev_jump_detected?)).to be false - end - - describe "with a dev jump message" do - before(:each) do - expect(controller).to receive(:handle_dev_jump).and_return(true) - expect(Stealth).to receive(:env).and_return(dev_env) - end - - it "should return true if the message is in the format /flow/state" do - controller.current_message.message = '/mr_robot/my_action' - expect(controller.send(:dev_jump_detected?)).to be true - end - - it "should return true if the message is in the format /flow" do - controller.current_message.message = '/mr_robot' - expect(controller.send(:dev_jump_detected?)).to be true - end - - it "should return true if the message is in the format //state" do - controller.current_message.message = '//my_action' - expect(controller.send(:dev_jump_detected?)).to be true - end - end - end - - describe "handle_dev_jump" do - it "should handle messages in the format /flow/state" do - controller.current_message.message = '/mr_robot/my_action' - expect(controller).to receive(:step_to).with(flow: 'mr_robot', state: 'my_action') - controller.send(:handle_dev_jump) - end - - it "should handle messages in the format /flow" do - controller.current_message.message = '/mr_robot' - expect(controller).to receive(:step_to).with(flow: 'mr_robot', state: nil) - controller.send(:handle_dev_jump) - end - - it "should handle messages in the format //state" do - controller.current_message.message = '//my_action' - expect(controller).to receive(:step_to).with(flow: nil, state: 'my_action') - controller.send(:handle_dev_jump) - end - end - - describe "session locking" do - before(:each) do - allow(MrTronsController).to receive(:new).and_return(controller) - end - - it "should lock and then unlock a session when a do_nothing action is called" do - expect(controller).to receive(:lock_session!).once - expect(controller).to receive(:release_lock!).once - controller.action(action: :other_action4) - end - - it "should lock and then unlock a session twice when an action steps to another" do - expect(controller).to receive(:lock_session!).twice - expect(controller).to receive(:release_lock!).twice - controller.action(action: :other_action2) - end - - it 'should still release the lock even if an action raises' do - expect(controller).to receive(:release_lock!).once - controller.action(action: :broken_action) - end - - it 'should still release the lock if an action steps to an unknown flow->state' do - expect(controller).to receive(:release_lock!).once - controller.action(action: :parts_unknown) - end - end - - describe "halt!" do - it "should catch the error and log the sessio halt" do - ### It's lame we have to include these two - expect(Stealth::Logger).to receive(:l).with( - topic: "primary_session", - message: "User #{facebook_message.sender_id}: setting session to mr_tron->halted" - ) - expect(Stealth::Logger).to receive(:l).with( - topic: "previous_session", - message: "User #{facebook_message.sender_id}: setting to parts->unknown" - ) - ### - - expect(Stealth::Logger).to receive(:l).with( - topic: "session", - message: "User #{facebook_message.sender_id}: session halted." - ) - - controller.step_to(flow: :mr_tron, state: :halted) - end - - it "should NOT continue with the rest of the controller code" do - expect_any_instance_of(MrTronsController).to_not receive(:other_action2) - expect_any_instance_of(MrTronsController).to_not receive(:step_to).with(state: :other_action2) - controller.step_to(flow: :mr_tron, state: :halted) - end - end - end - -end diff --git a/spec/controller/dynamic_delay_spec.rb b/spec/controller/dynamic_delay_spec.rb deleted file mode 100644 index b5560a3b..00000000 --- a/spec/controller/dynamic_delay_spec.rb +++ /dev/null @@ -1,70 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe "Stealth::Controller::DynamicDelay" do - - let(:facebook_message) { SampleMessage.new(service: 'facebook') } - let(:controller) { VadersController.new(service_message: facebook_message.message_with_text) } - let!(:service_replies) { YAML.load(File.read(File.expand_path("../replies/messages/say_howdy_with_dynamic.yml", __dir__))) } - - it "should return a SHORT_DELAY for a dynamic delay at position 0" do - delay = controller.dynamic_delay(previous_reply: nil) - expect(delay).to eq(Stealth::Controller::DynamicDelay::SHORT_DELAY) - end - - it "should return a STANDARD_DELAY for a dynamic delay at position -2" do - delay = controller.dynamic_delay(previous_reply: service_replies[-2]) - expect(delay).to eq(Stealth::Controller::DynamicDelay::STANDARD_DELAY) - end - - it "should return a SHORT_DELAY for text 35 chars long" do - delay = controller.dynamic_delay(previous_reply: service_replies[1]) - expect(delay).to eq(Stealth::Controller::DynamicDelay::SHORT_DELAY) - end - - it "should return a STANDARD_DELAY for text 120 chars long" do - delay = controller.dynamic_delay(previous_reply: service_replies[3]) - expect(delay).to eq(Stealth::Controller::DynamicDelay::STANDARD_DELAY) - end - - it "should return a (STANDARD_DELAY * 1.5) for text 230 chars long" do - delay = controller.dynamic_delay(previous_reply: service_replies[5]) - expect(delay).to eq(Stealth::Controller::DynamicDelay::STANDARD_DELAY * 1.5) - end - - it "should return a LONG_DELAY for text 350 chars long" do - delay = controller.dynamic_delay(previous_reply: service_replies[7]) - expect(delay).to eq(Stealth::Controller::DynamicDelay::LONG_DELAY) - end - - it "should return a STANDARD_DELAY for an image" do - delay = controller.dynamic_delay(previous_reply: service_replies[9]) - expect(delay).to eq(Stealth::Controller::DynamicDelay::STANDARD_DELAY) - end - - it "should return a STANDARD_DELAY for a video" do - delay = controller.dynamic_delay(previous_reply: service_replies[11]) - expect(delay).to eq(Stealth::Controller::DynamicDelay::STANDARD_DELAY) - end - - it "should return a STANDARD_DELAY for an audio" do - delay = controller.dynamic_delay(previous_reply: service_replies[13]) - expect(delay).to eq(Stealth::Controller::DynamicDelay::STANDARD_DELAY) - end - - it "should return a STANDARD_DELAY for a file" do - delay = controller.dynamic_delay(previous_reply: service_replies[15]) - expect(delay).to eq(Stealth::Controller::DynamicDelay::STANDARD_DELAY) - end - - it "should return a STANDARD_DELAY for cards" do - delay = controller.dynamic_delay(previous_reply: service_replies[17]) - expect(delay).to eq(Stealth::Controller::DynamicDelay::STANDARD_DELAY) - end - - it "should return a STANDARD_DELAY for a list" do - delay = controller.dynamic_delay(previous_reply: service_replies[19]) - expect(delay).to eq(Stealth::Controller::DynamicDelay::STANDARD_DELAY) - end -end diff --git a/spec/controller/helpers_spec.rb b/spec/controller/helpers_spec.rb deleted file mode 100644 index a7b1bb7f..00000000 --- a/spec/controller/helpers_spec.rb +++ /dev/null @@ -1,119 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -$:.unshift File.expand_path("../support/helpers", __dir__) - -describe "Stealth::Controller helpers" do - - Stealth::Controller.helpers_path = File.expand_path("../support/helpers", __dir__) - - module Fun - class GamesController < Stealth::Controller - helper :all - - def say_hello_world - hello_world - end - - def say_kaboom - hello_world2 - end - end - - class PdfController < Stealth::Controller - def say_pdf_name - generate_pdf_name - end - end - end - - class BaseController < Stealth::Controller - - end - - class AllHelpersController < Stealth::Controller - helper :all - end - - class InheritedHelpersController < AllHelpersController - def say_hello_world - hello_world - end - end - - class SizzleController < Stealth::Controller - helper :standalone - - def say_sizzle - - end - end - - class HelpersTypoController < Stealth::Controller - path = File.expand_path("../support/helpers_typo", __dir__) - $:.unshift(path) - self.helpers_path = path - end - - class VoodooController < Stealth::Controller - helpers_path = File.expand_path("../support/alternate_helpers", __dir__) - - # Reload helpers - _helpers = Module.new - helper :all - - def zoom - - end - end - - let(:facebook_message) { SampleMessage.new(service: 'facebook') } - let(:all_helper_methods) { [:hello_world, :baz, :generate_pdf_name] } - - describe "loading" do - - it "should load all helpers if none are specified by default" do - expect(BaseController._helpers.instance_methods).to match_array(all_helper_methods) - end - - it "should not load helpers if none are specified by default and include_all_helpers = false" do - Stealth::Controller.include_all_helpers = false - class HelperlessController < Stealth::Controller; end - expect(HelperlessController._helpers.instance_methods).to eq [] - end - - it "should load all helpers if :all is used" do - expect(AllHelpersController._helpers.instance_methods).to match_array(all_helper_methods) - end - - it "should load all helpers if parent class inherits all helpers" do - expect(InheritedHelpersController._helpers.instance_methods).to match_array(all_helper_methods) - end - - it "should allow a controller that has inherited all helpers to access a helper method" do - expect { - InheritedHelpersController.new(service_message: facebook_message.message_with_text).say_hello_world - }.to_not raise_error - end - - it "should allow a controller that has loaded all helpers to access a helper method" do - expect { - Fun::GamesController.new(service_message: facebook_message.message_with_text).say_hello_world - }.to_not raise_error - end - - it "should raise an error if a helper method does not exist" do - expect { - Fun::GamesController.new(service_message: facebook_message.message_with_text).say_kaboom - }.to raise_error(NameError) - end - - it "should allow a controller action to access a helper method" do - expect { - Fun::PdfController.new(service_message: facebook_message.message_with_text).say_pdf_name - }.to_not raise_error - end - end - -end diff --git a/spec/controller/interrupt_detect_spec.rb b/spec/controller/interrupt_detect_spec.rb deleted file mode 100644 index 209eda24..00000000 --- a/spec/controller/interrupt_detect_spec.rb +++ /dev/null @@ -1,171 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe "Stealth::Controller::InterruptDetect" do - - let(:fb_message) { SampleMessage.new(service: 'facebook') } - let(:controller) { VadersController.new(service_message: fb_message.message_with_text) } - let(:lock_key) { "#{fb_message.sender_id}-lock" } - let(:example_tid) { 'ovefhgJvx' } - let(:example_session) { 'goodbye->say_goodbye' } - let(:example_position) { 2 } - let(:example_lock) { "#{example_tid}##{example_session}:#{example_position}" } - - describe 'current_lock' do - it "should return the current lock for the session if it is locked" do - $redis.set(lock_key, example_lock) - current_lock = controller.current_lock - expect(current_lock).to be_a(Stealth::Lock) - expect(current_lock.session_id).to eq fb_message.sender_id - end - - it "should return nil if the current session is not locked" do - random_lock_key = "xyz123-lock" - - # clear the memoization - controller.instance_eval do - @current_lock = nil - @current_session_id = random_lock_key - end - - expect($redis.get(random_lock_key)).to be_nil - expect(controller.current_lock).to be_nil - end - end - - describe 'run_interrupt_action' do - let(:interrupts_controller) { InterruptController.new(service_message: fb_message) } - - it "should return false if an InterruptsController is not defined" do - expect(Stealth::Logger).to receive(:l).with( - topic: 'interrupt', - message: "Interrupt detected for session #{fb_message.sender_id}" - ).ordered - - expect(Stealth::Logger).to receive(:l).with( - topic: 'interrupt', - message: 'Ignoring interrupt; InterruptsController not defined.' - ).ordered - - expect(controller.run_interrupt_action).to be false - end - - it "should call say_interrupted on the InterruptsController" do - class InterruptsController < Stealth::Controller - def say_interrupted - end - end - - expect_any_instance_of(InterruptsController).to receive(:say_interrupted) - controller.run_interrupt_action - end - - it "should log if the InterruptsController#say_interrupted does not progress the session" do - class InterruptsController < Stealth::Controller - def say_interrupted - end - end - - expect(Stealth::Logger).to receive(:l).with( - topic: 'interrupt', - message: "Interrupt detected for session #{fb_message.sender_id}" - ).ordered - - expect(Stealth::Logger).to receive(:l).with( - topic: 'interrupt', - message: 'Did not send replies, update session, or step' - ).ordered - - controller.run_interrupt_action - end - - it "should catch StandardError from within InterruptController and log it" do - class InterruptsController < Stealth::Controller - def say_interrupted - raise Stealth::Errors::ReplyNotFound - end - end - - # Once for the interrupt detection, once for the error - expect(Stealth::Logger).to receive(:l).exactly(2).times - - controller.run_interrupt_action - end - end - - describe 'interrupt_detected?' do - it "should return false if there is not a lock on the session" do - random_lock_key = "xyz123-lock" - - # clear the memoization - controller.instance_eval do - @current_lock = nil - @current_session_id = random_lock_key - end - - expect(controller.send(:interrupt_detected?)).to be false - end - - it "should return false if the current thread owns the lock" do - $redis.set(lock_key, example_lock) - lock = controller.current_lock - expect(lock).to receive(:tid).and_return(Stealth.tid) - - expect(controller.send(:interrupt_detected?)).to be false - end - - it 'should return true if the session is locked by another thread' do - $redis.set(lock_key, example_lock) - # our mock tid will not match the real tid for this test - expect(controller.send(:interrupt_detected?)).to be true - end - end - - describe 'current_thread_has_control?' do - it "should return true if the current tid matches the lock tid" do - $redis.set(lock_key, example_lock) - lock = controller.current_lock - expect(lock).to receive(:tid).and_return(Stealth.tid) - expect(controller.send(:current_thread_has_control?)).to be true - end - - it "should return false if the current tid does not match the lock tid" do - $redis.set(lock_key, example_lock) - lock = controller.current_lock - expect(controller.send(:current_thread_has_control?)).to be false - end - end - - describe 'lock_session!' do - it "should create a lock for the session" do - $redis.del(lock_key) - controller.send(:lock_session!, session_slug: example_session, position: example_position) - expect($redis.get(lock_key)).to match(/goodbye\-\>say_goodbye\:2/) - end - end - - describe 'release_lock!' do - it "should not raise an error if current_lock is nil" do - expect(controller).to receive(:current_lock).and_return(nil) - controller.send(:release_lock!) - end - - it "should not release the lock if we are in the InterruptsController" do - class InterruptsController - end - - lock = controller.current_lock - expect(controller).to receive(:class).and_return InterruptsController - expect(lock).to_not receive(:release) - controller.send(:release_lock!) - end - - it "should release the lock if there is one and we are not in the InterruptsController" do - $redis.set(lock_key, example_lock) - controller.send(:release_lock!) - expect($redis.get(lock_key)).to be_nil - end - end - -end diff --git a/spec/controller/messages_spec.rb b/spec/controller/messages_spec.rb deleted file mode 100644 index adfd0f56..00000000 --- a/spec/controller/messages_spec.rb +++ /dev/null @@ -1,744 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe Stealth::Controller::Messages do - - class MrTronsController < Stealth::Controller - - end - - let(:facebook_message) { SampleMessage.new(service: 'facebook') } - let(:test_controller) { - MrTronsController.new(service_message: facebook_message.message_with_text) - } - - describe "normalized_msg" do - let(:padded_msg) { ' Hello World! 👋 ' } - let(:weird_case_msg) { 'Oh BaBy Oh BaBy' } - - it 'should normalize blank-padded messages' do - test_controller.current_message.message = padded_msg - expect(test_controller.normalized_msg).to eq('HELLO WORLD! 👋') - end - - it 'should normalize differently cased messages' do - test_controller.current_message.message = weird_case_msg - expect(test_controller.normalized_msg).to eq('OH BABY OH BABY') - end - end - - describe "homophone_translated_msg" do - it 'should convert homophones to their respective alpha ordinal' do - Stealth::Controller::Messages::HOMOPHONES.each do |homophone, ordinal| - test_controller.current_message.message = homophone - test_controller.normalized_msg = test_controller.homophone_translated_msg = nil - expect(test_controller.homophone_translated_msg).to eq(ordinal) - end - end - end - - describe "get_match" do - it "should match messages with different casing" do - test_controller.current_message.message = "NICE" - expect( - test_controller.get_match(['nice', 'woot']) - ).to eq('nice') - end - - it "should match messages with blank padding" do - test_controller.current_message.message = " NiCe " - expect( - test_controller.get_match(['nice', 'woot']) - ).to eq('nice') - end - - it "should match messages utilizing a lower case SMS quick reply" do - test_controller.current_message.message = "a " - expect( - test_controller.get_match(['nice', 'woot']) - ).to eq('nice') - end - - it "should match messages utilizing an upper case SMS quick reply" do - test_controller.current_message.message = " B " - expect( - test_controller.get_match(['nice', 'woot']) - ).to eq('woot') - end - - it "should match messages utilizing a single-quoted SMS quick reply" do - test_controller.current_message.message = "'B'" - expect( - test_controller.get_match(['nice', 'woot']) - ).to eq('woot') - end - - it "should match messages utilizing a double-quoted SMS quick reply" do - test_controller.current_message.message = '"A"' - expect( - test_controller.get_match(['nice', 'woot']) - ).to eq('nice') - end - - it "should match messages utilizing a double-smartquoted SMS quick reply" do - test_controller.current_message.message = '“A”' - expect( - test_controller.get_match(['nice', 'woot']) - ).to eq('nice') - end - - it "should match messages utilizing a single-smartquoted SMS quick reply" do - test_controller.current_message.message = '‘A’' - expect( - test_controller.get_match(['nice', 'woot']) - ).to eq('nice') - end - - it "should match messages with a period in the SMS quick reply" do - test_controller.current_message.message = 'A.' - expect( - test_controller.get_match(['nice', 'woot']) - ).to eq('nice') - end - - it "should match messages with a question mark in the SMS quick reply" do - test_controller.current_message.message = 'B?' - expect( - test_controller.get_match(['nice', 'woot']) - ).to eq('woot') - end - - it "should match messages in parens in the SMS quick reply" do - test_controller.current_message.message = '(B)' - expect( - test_controller.get_match(['nice', 'woot']) - ).to eq('woot') - end - - it "should match messages with backticks in the SMS quick reply" do - test_controller.current_message.message = '`B`' - expect( - test_controller.get_match(['nice', 'woot']) - ).to eq('woot') - end - - it "should match messages utilizing a homophone" do - test_controller.current_message.message = " bee " - expect( - test_controller.get_match(['nice', 'woot']) - ).to eq('woot') - end - - it "should raise ReservedHomophoneUsed if a homophone is used" do - test_controller.current_message.message = " B " - expect { - test_controller.get_match(['nice', 'woot', 'sea', 'bee']) - }.to raise_error(Stealth::Errors::ReservedHomophoneUsed, 'Cannot use `SEA, BEE`. Reserved for homophones.') - end - - it "should raise Stealth::Errors::UnrecognizedMessage if a response was not matched" do - test_controller.current_message.message = "uh oh" - expect { - test_controller.get_match(['nice', 'woot']) - }.to raise_error(Stealth::Errors::UnrecognizedMessage) - end - - it "should raise Stealth::Errors::UnrecognizedMessage if an SMS quick reply was not matched" do - test_controller.current_message.message = "C" - expect { - test_controller.get_match(['nice', 'woot']) - }.to raise_error(Stealth::Errors::UnrecognizedMessage) - end - - it "should not run NLP entity detection if an ordinal is entered by the user" do - test_controller.current_message.message = "C" - - expect(test_controller).to_not receive(:perform_nlp!) - expect( - test_controller.get_match([:yes, :no, 'unsubscribe']) - ).to eq('unsubscribe') - end - - describe "entity detection" do - let(:no_intent) { :no } - let(:yes_intent) { :yes } - let(:single_number_nlp_result) { TestNlpResult::Luis.new(intent: yes_intent, entity: :single_number_entity) } - let(:double_number_nlp_result) { TestNlpResult::Luis.new(intent: no_intent, entity: :double_number_entity) } - let(:triple_number_nlp_result) { TestNlpResult::Luis.new(intent: yes_intent, entity: :triple_number_entity) } - - describe 'single nlp_result entity' do - it 'should return the :number entity' do - allow(test_controller).to receive(:perform_nlp!).and_return(single_number_nlp_result) - test_controller.nlp_result = single_number_nlp_result - - test_controller.current_message.message = "hi" - expect( - test_controller.get_match(['nice', :number]) - ).to eq(test_controller.nlp_result.entities[:number].first) - end - - it 'should return the first :number entity if fuzzy_match=true' do - allow(test_controller).to receive(:perform_nlp!).and_return(double_number_nlp_result) - test_controller.nlp_result = double_number_nlp_result - - test_controller.current_message.message = "hi" - expect( - test_controller.get_match(['nice', :number]) - ).to eq(test_controller.nlp_result.entities[:number].first) - end - - it 'should raise Stealth::Errors::UnrecognizedMessage if more than one :number entity is returned and fuzzy_match=false' do - allow(test_controller).to receive(:perform_nlp!).and_return(double_number_nlp_result) - test_controller.nlp_result = double_number_nlp_result - - test_controller.current_message.message = "hi" - expect { - test_controller.get_match(['nice', :number], fuzzy_match: false) - }.to raise_error(Stealth::Errors::UnrecognizedMessage, "Encountered 2 entity matches of type :number and expected 1. To allow, set fuzzy_match to true.") - end - - it 'should log the NLP result if log_all_nlp_results=true' do - Stealth.config.log_all_nlp_results = true - Stealth.config.nlp_integration = :luis - - luis_client = double('luis_client') - allow(luis_client).to receive(:understand).and_return(single_number_nlp_result) - allow(Stealth::Nlp::Luis::Client).to receive(:new).and_return(luis_client) - - expect(Stealth::Logger).to receive(:l).with( - topic: :nlp, - message: "User 8b3e0a3c-62f1-401e-8b0f-615c9d256b1f -> Performing NLP." - ) - expect(Stealth::Logger).to receive(:l).with( - topic: :nlp, - message: "User 8b3e0a3c-62f1-401e-8b0f-615c9d256b1f -> NLP Result: #{single_number_nlp_result.parsed_result.inspect}" - ) - test_controller.current_message.message = "hi" - test_controller.get_match(['nice', :number]) - - Stealth.config.log_all_nlp_results = false - Stealth.config.nlp_integration = nil - end - end - - describe 'multiple nlp_result entity matches' do - it 'should return the [:number, :number] entity' do - allow(test_controller).to receive(:perform_nlp!).and_return(double_number_nlp_result) - test_controller.nlp_result = double_number_nlp_result - - test_controller.current_message.message = "hi" - expect( - test_controller.get_match(['nice', [:number, :number]]) - ).to eq(double_number_nlp_result.entities[:number]) - end - - it 'should return the [:number, :number, :number] entity' do - allow(test_controller).to receive(:perform_nlp!).and_return(triple_number_nlp_result) - test_controller.nlp_result = triple_number_nlp_result - - test_controller.current_message.message = "hi" - expect( - test_controller.get_match(['nice', [:number, :number, :number]]) - ).to eq(triple_number_nlp_result.entities[:number]) - end - - it 'should return the [:number, :number] entity from a triple :number entity result' do - allow(test_controller).to receive(:perform_nlp!).and_return(triple_number_nlp_result) - test_controller.nlp_result = triple_number_nlp_result - - test_controller.current_message.message = "hi" - expect( - test_controller.get_match(['nice', [:number, :number]]) - ).to eq(triple_number_nlp_result.entities[:number].slice(0, 2)) - end - - it 'should return the :number entity from a triple :number entity result' do - allow(test_controller).to receive(:perform_nlp!).and_return(triple_number_nlp_result) - test_controller.nlp_result = triple_number_nlp_result - - test_controller.current_message.message = "hi" - expect( - test_controller.get_match(['nice', :number]) - ).to eq(triple_number_nlp_result.entities[:number].first) - end - - it 'should return the [:number, :key_phrase] entities' do - allow(test_controller).to receive(:perform_nlp!).and_return(triple_number_nlp_result) - test_controller.nlp_result = triple_number_nlp_result - - test_controller.current_message.message = "hi" - expect( - test_controller.get_match(['nice', [:number, :key_phrase]]) - ).to eq([89, 'scores']) - end - - it 'should raise Stealth::Errors::UnrecognizedMessage if more than one :number entity is returned and fuzzy_match=false' do - allow(test_controller).to receive(:perform_nlp!).and_return(triple_number_nlp_result) - test_controller.nlp_result = triple_number_nlp_result - - test_controller.current_message.message = "hi" - expect { - test_controller.get_match(['nice', :number], fuzzy_match: false) - }.to raise_error(Stealth::Errors::UnrecognizedMessage, "Encountered 3 entity matches of type :number and expected 1. To allow, set fuzzy_match to true.") - end - - it 'should raise Stealth::Errors::UnrecognizedMessage if more than two :number entities are returned and fuzzy_match=false' do - allow(test_controller).to receive(:perform_nlp!).and_return(triple_number_nlp_result) - test_controller.nlp_result = triple_number_nlp_result - - test_controller.current_message.message = "hi" - expect { - test_controller.get_match(['nice', [:number, :number]], fuzzy_match: false) - }.to raise_error(Stealth::Errors::UnrecognizedMessage, "Encountered 1 additional entity matches of type :number for match [:number, :number]. To allow, set fuzzy_match to true.") - end - - it 'should log the NLP result if log_all_nlp_results=true' do - Stealth.config.log_all_nlp_results = true - Stealth.config.nlp_integration = :luis - - luis_client = double('luis_client') - allow(luis_client).to receive(:understand).and_return(triple_number_nlp_result) - allow(Stealth::Nlp::Luis::Client).to receive(:new).and_return(luis_client) - - expect(Stealth::Logger).to receive(:l).with( - topic: :nlp, - message: "User 8b3e0a3c-62f1-401e-8b0f-615c9d256b1f -> Performing NLP." - ) - expect(Stealth::Logger).to receive(:l).with( - topic: :nlp, - message: "User 8b3e0a3c-62f1-401e-8b0f-615c9d256b1f -> NLP Result: #{triple_number_nlp_result.parsed_result.inspect}" - ) - test_controller.current_message.message = "hi" - test_controller.get_match(['nice', [:number, :number]]) - - Stealth.config.log_all_nlp_results = false - Stealth.config.nlp_integration = nil - end - end - - describe 'custom entities' do - let(:custom_entity_nlp_result) { TestNlpResult::Luis.new(intent: yes_intent, entity: :custom_entity) } - - it 'should return the text matched by the custom entity' do - allow(test_controller).to receive(:perform_nlp!).and_return(custom_entity_nlp_result) - test_controller.nlp_result = custom_entity_nlp_result - - test_controller.current_message.message = "call me right away" - expect( - test_controller.get_match(['nice', :asap]) - ).to eq 'right away' - end - end - end - - describe "mismatch" do - describe 'raise_on_mismatch: true' do - it "should raise a Stealth::Errors::UnrecognizedMessage" do - test_controller.current_message.message = 'C' - expect { - test_controller.get_match(['nice', 'woot']) - }.to raise_error(Stealth::Errors::UnrecognizedMessage) - end - - it "should NOT log if an nlp_result is not present" do - test_controller.current_message.message = 'spicy' - expect(Stealth::Logger).to_not receive(:l) - expect { - test_controller.get_match(['nice', 'woot']) - }.to raise_error(Stealth::Errors::UnrecognizedMessage) - end - - it "should log if an nlp_result is present" do - test_controller.current_message.message = 'spicy' - nlp_result = double('nlp_result') - allow(nlp_result).to receive(:parsed_result).and_return({}) - - expect(Stealth::Logger).to receive(:l).with( - topic: :nlp, - message: "User 8b3e0a3c-62f1-401e-8b0f-615c9d256b1f -> NLP Result: {}" - ) - - test_controller.nlp_result = nlp_result - - expect { - test_controller.get_match(['nice', 'woot']) - }.to raise_error(Stealth::Errors::UnrecognizedMessage) - end - end - - describe 'raise_on_mismatch: false' do - it "should not raise a Stealth::Errors::UnrecognizedMessage" do - test_controller.current_message.message = 'C' - expect { - test_controller.get_match(['nice', 'woot'], raise_on_mismatch: false) - }.to_not raise_error(Stealth::Errors::UnrecognizedMessage) - end - - it "should return the original message" do - test_controller.current_message.message = 'spicy' - expect( - test_controller.get_match(['nice', 'woot'], raise_on_mismatch: false) - ).to eq 'spicy' - end - - it "should NOT log if an nlp_result is not present" do - test_controller.current_message.message = 'spicy' - expect(Stealth::Logger).to_not receive(:l) - test_controller.get_match(['nice', 'woot'], raise_on_mismatch: false) - end - - it "should log if an nlp_result is present" do - test_controller.current_message.message = 'spicy' - nlp_result = double('nlp_result') - allow(nlp_result).to receive(:parsed_result).and_return({}) - - expect(Stealth::Logger).to receive(:l).with( - topic: :nlp, - message: "User 8b3e0a3c-62f1-401e-8b0f-615c9d256b1f -> NLP Result: {}" - ) - - test_controller.nlp_result = nlp_result - - test_controller.get_match(['nice', 'woot'], raise_on_mismatch: false) - end - end - end - end - - describe "handle_message" do - it "should run the proc of the matched reply" do - expect(STDOUT).to receive(:puts).with('Cool, Refinance 👍') - - test_controller.current_message.message = "B" - test_controller.handle_message( - 'Buy' => proc { puts 'Buy' }, - 'Refinance' => proc { puts 'Cool, Refinance 👍' } - ) - end - - it "should run proc in the binding of the calling instance" do - test_controller.current_message.message = "B" - x = 0 - test_controller.handle_message( - 'Buy' => proc { x += 1 }, - 'Refinance' => proc { x += 2 } - ) - - expect(x).to eq 2 - end - - it "should match against single-quoted ordinals" do - test_controller.current_message.message = "'B'" - x = 0 - test_controller.handle_message( - 'Buy' => proc { x += 1 }, - 'Refinance' => proc { x += 2 } - ) - - expect(x).to eq 2 - end - - it "should match against double-quoted ordinals" do - test_controller.current_message.message = '"A"' - x = 0 - test_controller.handle_message( - 'Buy' => proc { x += 1 }, - 'Refinance' => proc { x += 2 } - ) - - expect(x).to eq 1 - end - - it "should match against double-smartquoted ordinals" do - test_controller.current_message.message = '“A”' - x = 0 - test_controller.handle_message( - 'Buy' => proc { x += 1 }, - 'Refinance' => proc { x += 2 } - ) - - expect(x).to eq 1 - end - - it "should match against single-smartquoted ordinals" do - test_controller.current_message.message = '‘A’' - x = 0 - test_controller.handle_message( - 'Buy' => proc { x += 1 }, - 'Refinance' => proc { x += 2 } - ) - - expect(x).to eq 1 - end - - it "should match against ordinals with periods" do - test_controller.current_message.message = 'A.' - x = 0 - test_controller.handle_message( - 'Buy' => proc { x += 1 }, - 'Refinance' => proc { x += 2 } - ) - - expect(x).to eq 1 - end - - it "should match against ordinals with question marks" do - test_controller.current_message.message = 'A?' - x = 0 - test_controller.handle_message( - 'Buy' => proc { x += 1 }, - 'Refinance' => proc { x += 2 } - ) - - expect(x).to eq 1 - end - - it "should match against ordinals with parens" do - test_controller.current_message.message = '(A)' - x = 0 - test_controller.handle_message( - 'Buy' => proc { x += 1 }, - 'Refinance' => proc { x += 2 } - ) - - expect(x).to eq 1 - end - - it "should match against ordinals with backticks" do - test_controller.current_message.message = '`A`' - x = 0 - test_controller.handle_message( - 'Buy' => proc { x += 1 }, - 'Refinance' => proc { x += 2 } - ) - - expect(x).to eq 1 - end - - it "should match homophones" do - test_controller.current_message.message = 'sea' - x = 0 - test_controller.handle_message( - 'Buy' => proc { x += 1 }, - 'Refinance' => proc { x += 2 }, - 'Other' => proc { x += 3 } - ) - - expect(x).to eq 3 - end - - it "should raise ReservedHomophoneUsed error if an arm contains a reserved homophone" do - test_controller.current_message.message = "B" - x = 0 - - expect { - test_controller.handle_message( - 'Buy' => proc { x += 1 }, - :woot => proc { x += 2 }, - 'Sea' => proc { x += 3 } - ) - }.to raise_error(Stealth::Errors::ReservedHomophoneUsed, 'Cannot use `SEA`. Reserved for homophones.') - end - - it "should not run NLP if an ordinal is entered by the user" do - test_controller.current_message.message = "C" - x = 0 - test_controller.handle_message( - :yes => proc { x += 1 }, - :no => proc { x += 2 }, - 'Unsubscribe' => proc { x += 3 } - ) - - expect(test_controller).to_not receive(:perform_nlp!) - expect(x).to eq 3 - end - - describe "intent detection" do - let(:no_intent) { :no } - let(:yes_intent) { :yes } - let(:yes_intent_nlp_result) { TestNlpResult::Luis.new(intent: yes_intent, entity: :single_number_entity) } - let(:no_intent_nlp_result) { TestNlpResult::Luis.new(intent: no_intent, entity: :double_number_entity) } - - it 'should support :yes intent' do - test_controller.current_message.message = "YAS" - allow(test_controller).to receive(:perform_nlp!).and_return(yes_intent_nlp_result) - test_controller.nlp_result = yes_intent_nlp_result - - x = 0 - test_controller.send( - :handle_message, { - 'Buy' => proc { x += 1 }, - :yes => proc { x += 9 }, - :no => proc { x += 8 } - } - ) - - expect(x).to eq 9 - end - - it 'should support :no intent' do - test_controller.current_message.message = "NAH" - allow(test_controller).to receive(:perform_nlp!).and_return(no_intent_nlp_result) - test_controller.nlp_result = no_intent_nlp_result - - x = 0 - test_controller.send( - :handle_message, { - 'Buy' => proc { x += 1 }, - :yes => proc { x += 9 }, - :no => proc { x += 8 } - } - ) - - expect(x).to eq 8 - end - - it 'should log the NLP result if log_all_nlp_results=true' do - Stealth.config.log_all_nlp_results = true - Stealth.config.nlp_integration = :luis - - luis_client = double('luis_client') - allow(luis_client).to receive(:understand).and_return(yes_intent_nlp_result) - allow(Stealth::Nlp::Luis::Client).to receive(:new).and_return(luis_client) - - expect(Stealth::Logger).to receive(:l).with( - topic: :nlp, - message: "User 8b3e0a3c-62f1-401e-8b0f-615c9d256b1f -> Performing NLP." - ) - expect(Stealth::Logger).to receive(:l).with( - topic: :nlp, - message: "User 8b3e0a3c-62f1-401e-8b0f-615c9d256b1f -> NLP Result: #{yes_intent_nlp_result.parsed_result.inspect}" - ) - test_controller.current_message.message = "YAS" - x = 0 - test_controller.send( - :handle_message, { - 'Buy' => proc { x += 1 }, - :yes => proc { x += 9 }, - :no => proc { x += 8 } - } - ) - - Stealth.config.log_all_nlp_results = false - Stealth.config.nlp_integration = nil - end - end - - describe 'Regexp matcher' do - it "should match when the Regexp matches" do - test_controller.current_message.message = "About Encom" - x = 0 - test_controller.handle_message( - 'Buy' => proc { x += 1 }, - 'Refinance' => proc { x += 2 }, - /about/i => proc { x += 10 } - ) - expect(x).to eq 10 - end - - it "should match positional Regexes" do - test_controller.current_message.message = "Jump about" - x = 0 - test_controller.handle_message( - 'Buy' => proc { x += 1 }, - /\Aabout/i => proc { x += 2 }, - /about/i => proc { x += 10 } - ) - expect(x).to eq 10 - end - - it "should match as an alpha ordinal" do - test_controller.current_message.message = "C" - x = 0 - test_controller.handle_message( - 'Buy' => proc { x += 1 }, - 'Refinance' => proc { x += 2 }, - /about/i => proc { x += 10 } - ) - expect(x).to eq 10 - end - end - - describe 'nil matcher' do - it "should match the respective ordinal" do - test_controller.current_message.message = "C" - x = 0 - test_controller.handle_message( - 'Buy' => proc { x += 1 }, - 'Refinance' => proc { x += 2 }, - nil => proc { x += 10 } - ) - expect(x).to eq 10 - end - - it "should match an unknown ordinal" do - test_controller.current_message.message = "D" - x = 0 - test_controller.handle_message( - 'Buy' => proc { x += 1 }, - 'Refinance' => proc { x += 2 }, - nil => proc { x += 10 } - ) - expect(x).to eq 10 - end - - it "should match free-form text" do - test_controller.current_message.message = "Hello world!" - x = 0 - test_controller.handle_message( - 'Buy' => proc { x += 1 }, - 'Refinance' => proc { x += 2 }, - nil => proc { x += 10 } - ) - expect(x).to eq 10 - end - end - - it "should raise Stealth::Errors::UnrecognizedMessage if the reply does not match" do - test_controller.current_message.message = "C" - x = 0 - expect { - test_controller.handle_message( - 'Buy' => proc { x += 1 }, - 'Refinance' => proc { x += 2 } - ) - }.to raise_error(Stealth::Errors::UnrecognizedMessage) - end - - it "should NOT log if an nlp_result is not present" do - test_controller.current_message.message = 'spicy' - expect(Stealth::Logger).to_not receive(:l) - - x = 0 - expect { - test_controller.handle_message( - 'Buy' => proc { x += 1 }, - 'Refinance' => proc { x += 2 } - ) - }.to raise_error(Stealth::Errors::UnrecognizedMessage) - end - - it "should log if an nlp_result is present" do - test_controller.current_message.message = 'spicy' - nlp_result = double('nlp_result') - allow(nlp_result).to receive(:parsed_result).and_return({}) - - expect(Stealth::Logger).to receive(:l).with( - topic: :nlp, - message: "User 8b3e0a3c-62f1-401e-8b0f-615c9d256b1f -> NLP Result: {}" - ) - - test_controller.nlp_result = nlp_result - - x = 0 - expect { - test_controller.handle_message( - 'Buy' => proc { x += 1 }, - 'Refinance' => proc { x += 2 } - ) - }.to raise_error(Stealth::Errors::UnrecognizedMessage) - end - end - -end diff --git a/spec/controller/nlp_spec.rb b/spec/controller/nlp_spec.rb deleted file mode 100644 index 5d232c31..00000000 --- a/spec/controller/nlp_spec.rb +++ /dev/null @@ -1,93 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe Stealth::Controller::Nlp do - - let(:fb_message) { SampleMessage.new(service: 'facebook') } - let(:controller) { VadersController.new(service_message: fb_message.message_with_text) } - - describe 'nlp_client_klass' do - it 'should return the correct class for LUIS' do - config_dbl = double('Stealth Config', nlp_integration: :luis).as_null_object - allow(Stealth).to receive(:config).and_return(config_dbl) - expect(controller.send(:nlp_client_klass)).to eq Stealth::Nlp::Luis::Client - end - - it 'should return the correct class for Dialogflow' do - config_dbl = double('Stealth Config', nlp_integration: :dialogflow).as_null_object - allow(Stealth).to receive(:config).and_return(config_dbl) - expect(controller.send(:nlp_client_klass)).to eq Stealth::Nlp::Dialogflow::Client - end - - it 'should raise an error if it cannot locate a class' do - config_dbl = double('Stealth Config', nlp_integration: :unknown).as_null_object - allow(Stealth).to receive(:config).and_return(config_dbl) - expect { - controller.send(:nlp_client_klass) - }.to raise_error(NameError) - end - end - - describe 'perform_nlp!' do - - describe 'NLP has not yet been configured' do - it 'should raise Stealth::Errors::ConfigurationError' do - config_dbl = double('Stealth Config', nlp_integration: nil).as_null_object - allow(Stealth).to receive(:config).and_return(config_dbl) - - expect { - controller.perform_nlp! - }.to raise_error(Stealth::Errors::ConfigurationError) - end - end - - describe 'NLP has been configured' do - before(:each) do - config_dbl = double('Stealth Config', nlp_integration: :luis).as_null_object - @luis_client_dbl = double('LUIS Client') - allow(Stealth).to receive(:config).and_return(config_dbl) - allow(Stealth::Nlp::Luis::Client).to receive(:new).and_return(@luis_client_dbl) - end - - let(:nlp_result) { Stealth::Nlp::Result.new(result: {}) } - - it 'should call understand on the NLP client' do - expect(@luis_client_dbl).to receive(:understand).with(query: 'Hello World!').and_return(nlp_result) - controller.perform_nlp! - end - - it 'should return an Nlp::Result object' do - expect(@luis_client_dbl).to receive(:understand).with(query: 'Hello World!').and_return(nlp_result) - expect(controller.perform_nlp!).to eq nlp_result - end - - it 'should memoize the understand call' do - expect(@luis_client_dbl).to receive(:understand).once.with(query: 'Hello World!').and_return(nlp_result) - controller.perform_nlp! - controller.perform_nlp! - controller.perform_nlp! - end - - it 'should store the nlp_result for the current controller' do - expect(@luis_client_dbl).to receive(:understand).once.with(query: 'Hello World!').and_return(nlp_result) - controller.perform_nlp! - expect(controller.nlp_result).to eq nlp_result - end - - it 'should store the nlp_result for the current service_message' do - expect(@luis_client_dbl).to receive(:understand).once.with(query: 'Hello World!').and_return(nlp_result) - controller.perform_nlp! - expect(controller.current_message.nlp_result).to eq nlp_result - end - - it 'should perform the NLP query if it has been cleared out' do - expect(@luis_client_dbl).to receive(:understand).exactly(2).times.with(query: 'Hello World!').and_return(nlp_result) - controller.perform_nlp! - controller.nlp_result = nil - controller.perform_nlp! - end - end - end - -end diff --git a/spec/controller/replies_spec.rb b/spec/controller/replies_spec.rb deleted file mode 100644 index 2c478b77..00000000 --- a/spec/controller/replies_spec.rb +++ /dev/null @@ -1,796 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe "Stealth::Controller replies" do - - Stealth::Controller._replies_path = File.expand_path("../replies", __dir__) - - let(:facebook_message) { SampleMessage.new(service: 'facebook') } - let(:controller) { MessagesController.new(service_message: facebook_message.message_with_text) } - - # Stub out base Facebook integration - module Stealth - module Services - module Facebook - class ReplyHandler - - end - - class Client - - end - end - end - end - - class MessagesController < Stealth::Controller - def say_oi - @first_name = "Presley" - send_replies - end - - def say_offer - send_replies - end - - def say_offer_with_dynamic - send_replies - end - - def say_msgs_without_breaks - send_replies - end - - def say_uh_oh - send_replies - end - - def say_randomize_text - send_replies - end - - def say_randomize_speech - send_replies - end - - def say_custom_reply - send_replies custom_reply: 'messages/say_offer' - end - - def say_howdy_with_dynamic - send_replies - end - - def say_nested_custom_reply - send_replies custom_reply: 'messages/sub1/sub2/say_nested' - end - - def say_simple_hello - send_replies - end - - def say_inline_reply - reply = [ - { 'reply_type' => 'text', 'text' => 'Hi, Morty. Welcome to Stealth bot...' }, - { 'reply_type' => 'delay', 'duration' => 2 }, - { 'reply_type' => 'text', 'text' => 'We offer users an awesome Ruby framework for building chat bots.' } - ] - - send_replies inline: reply - end - end - - describe "missing reply" do - it "should raise a Stealth::Errors::ReplyNotFound" do - allow(controller.current_session).to receive(:flow_string).and_return("message") - allow(controller.current_session).to receive(:state_string).and_return("say_uh_oh") - - expect { - controller.send_replies - }.to raise_error(Stealth::Errors::ReplyNotFound) - end - end - - describe "class attributes" do - it "should have altered the _replies_path class attribute" do - expect(MessagesController._replies_path).to eq(File.expand_path("../replies", __dir__)) - end - - it "should have altered the _preprocessors class attribute" do - expect(MessagesController._preprocessors).to eq([:erb]) - end - end - - describe "action_replies" do - it "should select the :erb preprocessor when reply extension is .yml" do - allow(controller.current_session).to receive(:flow_string).and_return("message") - allow(controller.current_session).to receive(:state_string).and_return("say_oi") - file_contents, selected_preprocessor = controller.send(:action_replies) - - expect(selected_preprocessor).to eq(:erb) - end - - it "should select the :none preprocessor when there is no reply extension" do - allow(controller.current_session).to receive(:flow_string).and_return("message") - allow(controller.current_session).to receive(:state_string).and_return("say_offer") - file_contents, selected_preprocessor = controller.send(:action_replies) - - expect(selected_preprocessor).to eq(:none) - end - - it "should read the reply's file contents" do - allow(controller.current_session).to receive(:flow_string).and_return("message") - allow(controller.current_session).to receive(:state_string).and_return("say_offer") - file_contents, selected_preprocessor = controller.send(:action_replies) - - expect(file_contents).to eq(File.read(File.expand_path("../replies/messages/say_offer.yml", __dir__))) - end - end - - describe "reply with ERB" do - let(:stubbed_handler) { double("handler") } - let(:stubbed_client) { double("client") } - - before(:each) do - allow(Stealth::Services::Facebook::ReplyHandler).to receive(:new).and_return(stubbed_handler) - allow(Stealth::Services::Facebook::Client).to receive(:new).and_return(stubbed_client) - allow(controller.current_session).to receive(:flow_string).and_return("message") - allow(controller.current_session).to receive(:state_string).and_return("say_oi") - Stealth.config.auto_insert_delays = false - end - - after(:each) do - Stealth.config.auto_insert_delays = true - end - - it "should translate each reply_type in the reply" do - allow(stubbed_client).to receive(:transmit).and_return(true) - allow(controller).to receive(:sleep).and_return(true) - - expect(stubbed_handler).to receive(:text).exactly(3).times - expect(stubbed_handler).to receive(:delay).exactly(2).times - controller.say_oi - end - - it "should transmit each reply_type in the reply" do - allow(stubbed_handler).to receive(:text).exactly(3).times - allow(stubbed_handler).to receive(:delay).exactly(2).times - allow(controller).to receive(:sleep).and_return(true) - - expect(stubbed_client).to receive(:transmit).exactly(5).times - controller.say_oi - end - - it "should sleep on delays" do - allow(stubbed_handler).to receive(:text).exactly(3).times - allow(stubbed_handler).to receive(:delay).exactly(2).times - allow(stubbed_client).to receive(:transmit).exactly(5).times - - expect(controller).to receive(:sleep).exactly(2).times - controller.say_oi - end - end - - describe "plain reply" do - let(:stubbed_handler) { double("handler") } - let(:stubbed_client) { double("client") } - - before(:each) do - allow(Stealth::Services::Facebook::ReplyHandler).to receive(:new).and_return(stubbed_handler) - allow(Stealth::Services::Facebook::Client).to receive(:new).and_return(stubbed_client) - allow(controller.current_session).to receive(:flow_string).and_return("message") - allow(controller.current_session).to receive(:state_string).and_return("say_offer") - Stealth.config.auto_insert_delays = false - end - - after(:each) do - Stealth.config.auto_insert_delays = true - end - - it "should translate each reply_type in the reply" do - allow(stubbed_client).to receive(:transmit).and_return(true) - allow(controller).to receive(:sleep).and_return(true).with(2.0) - - expect(stubbed_handler).to receive(:text).exactly(2).times - expect(stubbed_handler).to receive(:delay).exactly(1).times - controller.say_offer - end - - it "should transmit each reply_type in the reply" do - allow(stubbed_handler).to receive(:text).exactly(2).times - allow(stubbed_handler).to receive(:delay).exactly(1).times - allow(controller).to receive(:sleep).and_return(true).with(2.0) - - expect(stubbed_client).to receive(:transmit).exactly(3).times - controller.say_offer - end - - it "should sleep on delays" do - allow(stubbed_handler).to receive(:text).exactly(2).times - allow(stubbed_handler).to receive(:delay).exactly(1).times - allow(stubbed_client).to receive(:transmit).exactly(3).times - - expect(controller).to receive(:sleep).exactly(1).times.with(2.0) - controller.say_offer - end - end - - describe "custom_reply" do - let(:stubbed_handler) { double("handler") } - let(:stubbed_client) { double("client") } - - before(:each) do - allow(Stealth::Services::Facebook::ReplyHandler).to receive(:new).and_return(stubbed_handler) - allow(Stealth::Services::Facebook::Client).to receive(:new).and_return(stubbed_client) - allow(controller.current_session).to receive(:flow_string).and_return("message") - allow(controller.current_session).to receive(:state_string).and_return("say_custom_reply") - Stealth.config.auto_insert_delays = false - end - - after(:each) do - Stealth.config.auto_insert_delays = true - end - - it "should translate each reply_type in the reply" do - allow(stubbed_client).to receive(:transmit).and_return(true) - allow(controller).to receive(:sleep).and_return(true).with(2.0) - - expect(stubbed_handler).to receive(:text).exactly(2).times - expect(stubbed_handler).to receive(:delay).exactly(1).times - controller.say_custom_reply - end - - it "should transmit each reply_type in the reply" do - allow(stubbed_handler).to receive(:text).exactly(2).times - allow(stubbed_handler).to receive(:delay).exactly(1).times - allow(controller).to receive(:sleep).and_return(true).with(2.0) - - expect(stubbed_client).to receive(:transmit).exactly(3).times - controller.say_custom_reply - end - - it "should sleep on delays" do - allow(stubbed_handler).to receive(:text).exactly(2).times - allow(stubbed_handler).to receive(:delay).exactly(1).times - allow(stubbed_client).to receive(:transmit).exactly(3).times - - expect(controller).to receive(:sleep).exactly(1).times.with(2.0) - controller.say_custom_reply - end - - it "should correctly load from sub-dirs" do - expect(stubbed_handler).to receive(:text).exactly(3).times - expect(stubbed_handler).to receive(:delay).exactly(2).times - expect(stubbed_client).to receive(:transmit).exactly(5).times - - expect(controller).to receive(:sleep).exactly(2).times.with(2.0) - controller.say_nested_custom_reply - end - end - - describe "inline replies" do - let(:stubbed_handler) { double("handler") } - let(:stubbed_client) { double("client") } - - before(:each) do - allow(Stealth::Services::Facebook::ReplyHandler).to receive(:new).and_return(stubbed_handler) - allow(Stealth::Services::Facebook::Client).to receive(:new).and_return(stubbed_client) - allow(controller.current_session).to receive(:flow_string).and_return("message") - allow(controller.current_session).to receive(:state_string).and_return("say_inline_reply") - Stealth.config.auto_insert_delays = false - end - - after(:each) do - Stealth.config.auto_insert_delays = true - end - - it "should translate each reply_type in the reply" do - allow(stubbed_client).to receive(:transmit).and_return(true) - allow(controller).to receive(:sleep).and_return(true).with(2.0) - - expect(stubbed_handler).to receive(:text).exactly(2).times - expect(stubbed_handler).to receive(:delay).exactly(1).times - controller.say_inline_reply - end - - it "should transmit each reply_type in the reply" do - allow(stubbed_handler).to receive(:text).exactly(2).times - allow(stubbed_handler).to receive(:delay).exactly(1).times - allow(controller).to receive(:sleep).and_return(true).with(2.0) - - expect(stubbed_client).to receive(:transmit).exactly(3).times - controller.say_inline_reply - end - - it "should sleep on delays" do - allow(stubbed_handler).to receive(:text).exactly(2).times - allow(stubbed_handler).to receive(:delay).exactly(1).times - allow(stubbed_client).to receive(:transmit).exactly(3).times - - expect(controller).to receive(:sleep).exactly(1).times.with(2.0) - controller.say_inline_reply - end - end - - describe "auto delays" do - let(:stubbed_handler) { double("handler") } - let(:stubbed_client) { double("client") } - - before(:each) do - allow(Stealth::Services::Facebook::ReplyHandler).to receive(:new).and_return(stubbed_handler) - allow(Stealth::Services::Facebook::Client).to receive(:new).and_return(stubbed_client) - allow(controller.current_session).to receive(:flow_string).and_return("message") - allow(controller.current_session).to receive(:state_string).and_return("say_msgs_without_breaks") - end - - it "should add two additional delays to a reply without delays" do - allow(stubbed_client).to receive(:transmit).and_return(true) - allow(controller).to receive(:sleep).and_return(true) - - expect(stubbed_handler).to receive(:text).exactly(2).times - expect(stubbed_handler).to receive(:delay).exactly(2).times - controller.say_msgs_without_breaks - end - - it "should only add a single delay to a reply that already contains a delay" do - allow(stubbed_client).to receive(:transmit).and_return(true) - allow(controller).to receive(:sleep).and_return(true) - - expect(stubbed_handler).to receive(:text).exactly(2).times - expect(stubbed_handler).to receive(:delay).exactly(2).times - controller.say_offer - end - - it "should not add delays if auto_insert_delays = false" do - allow(stubbed_client).to receive(:transmit).and_return(true) - allow(controller).to receive(:sleep).and_return(true) - - expect(stubbed_handler).to receive(:text).exactly(2).times - expect(stubbed_handler).to_not receive(:delay) - - Stealth.config.auto_insert_delays = false - controller.say_msgs_without_breaks - Stealth.config.auto_insert_delays = true - end - end - - describe "session locking" do - let(:stubbed_handler) { double("handler") } - let(:stubbed_client) { double("client") } - - before(:each) do - allow(Stealth::Services::Facebook::ReplyHandler).to receive(:new).and_return(stubbed_handler) - allow(Stealth::Services::Facebook::Client).to receive(:new).and_return(stubbed_client) - allow(controller.current_session).to receive(:flow_string).and_return("message") - allow(controller.current_session).to receive(:state_string).and_return("say_offer") - Stealth.config.auto_insert_delays = false - end - - after(:each) do - Stealth.config.auto_insert_delays = true - end - - it "should update the lock for each reply_type in the reply" do - allow(stubbed_client).to receive(:transmit).and_return(true) - allow(controller).to receive(:sleep).and_return(true).with(2.0) - - expect(controller).to receive(:lock_session!).exactly(3).times - expect(stubbed_handler).to receive(:text).exactly(2).times - expect(stubbed_handler).to receive(:delay).exactly(1).times - controller.say_offer - end - - it "should update the lock position for each reply_type in the reply" do - allow(stubbed_client).to receive(:transmit).and_return(true) - allow(controller).to receive(:sleep).and_return(true).with(2.0) - - expect(controller).to receive( - :lock_session! - ).with( - session_slug: controller.current_session.get_session, - position: 0 - ).exactly(1).times - - expect(controller).to receive( - :lock_session! - ).with( - session_slug: controller.current_session.get_session, - position: 1 - ).exactly(1).times - - expect(controller).to receive( - :lock_session! - ).with( - session_slug: controller.current_session.get_session, - position: 2 - ).exactly(1).times - - expect(stubbed_handler).to receive(:text).exactly(2).times - expect(stubbed_handler).to receive(:delay).exactly(1).times - controller.say_offer - end - - it "should update the lock position with an offset for each reply_type in the reply" do - allow(stubbed_client).to receive(:transmit).and_return(true) - allow(controller).to receive(:sleep).and_return(true) - - controller.pos = 17 # set the offset - - expect(controller).to receive( - :lock_session! - ).with( - session_slug: controller.current_session.get_session, - position: 17 - ).exactly(1).times - - expect(controller).to receive( - :lock_session! - ).with( - session_slug: controller.current_session.get_session, - position: 18 - ).exactly(1).times - - expect(controller).to receive( - :lock_session! - ).with( - session_slug: controller.current_session.get_session, - position: 19 - ).exactly(1).times - - expect(controller).to receive( - :lock_session! - ).with( - session_slug: controller.current_session.get_session, - position: 20 - ).exactly(1).times - - expect(stubbed_handler).to receive(:cards).exactly(1).time - expect(stubbed_handler).to receive(:list).exactly(1).time - expect(stubbed_handler).to receive(:delay).exactly(2).times - allow(controller.current_session).to receive(:state_string).and_return("say_howdy_with_dynamic") - controller.say_howdy_with_dynamic - end - end - - describe "dynamic delays" do - let(:stubbed_handler) { double("handler") } - let(:stubbed_client) { double("client") } - - before(:each) do - allow(Stealth::Services::Facebook::ReplyHandler).to receive(:new).and_return(stubbed_handler) - allow(Stealth::Services::Facebook::Client).to receive(:new).and_return(stubbed_client) - allow(controller.current_session).to receive(:flow_string).and_return("message") - allow(controller.current_session).to receive(:state_string).and_return("say_offer_with_dynamic") - end - - it "should use the default multiplier if none is set" do - allow(stubbed_handler).to receive(:text).exactly(2).times - allow(stubbed_handler).to receive(:delay).exactly(2).times - allow(stubbed_client).to receive(:transmit).exactly(4).times - - delay = Stealth.config.dynamic_delay_muliplier * Stealth::Controller::DynamicDelay::SHORT_DELAY - expect(controller).to receive(:sleep).exactly(2).times.with(delay) - controller.say_offer_with_dynamic - end - - it "should slow down SHORT_DELAY if dynamic_delay_muliplier > 1" do - allow(stubbed_handler).to receive(:text).exactly(2).times - allow(stubbed_handler).to receive(:delay).exactly(2).times - allow(stubbed_client).to receive(:transmit).exactly(4).times - - Stealth.config.dynamic_delay_muliplier = 5 - delay = Stealth.config.dynamic_delay_muliplier * Stealth::Controller::DynamicDelay::SHORT_DELAY - expect(controller).to receive(:sleep).exactly(2).times.with(delay) - controller.say_offer_with_dynamic - end - - it "should speed up SHORT_DELAY if dynamic_delay_muliplier < 1" do - allow(stubbed_handler).to receive(:text).exactly(2).times - allow(stubbed_handler).to receive(:delay).exactly(2).times - allow(stubbed_client).to receive(:transmit).exactly(4).times - - Stealth.config.dynamic_delay_muliplier = 0.1 - delay = Stealth.config.dynamic_delay_muliplier * Stealth::Controller::DynamicDelay::SHORT_DELAY - expect(controller).to receive(:sleep).exactly(2).times.with(delay) - controller.say_offer_with_dynamic - end - end - - describe "variants" do - let(:twilio_message) { SampleMessage.new(service: 'twilio') } - let(:twilio_controller) { MessagesController.new(service_message: twilio_message.message_with_text) } - - let(:epsilon_message) { SampleMessage.new(service: 'epsilon') } - let(:epsilon_controller) { MessagesController.new(service_message: epsilon_message.message_with_text) } - - let(:gamma_message) { SampleMessage.new(service: 'twitter') } - let(:gamma_controller) { MessagesController.new(service_message: gamma_message.message_with_text) } - - it "should load the Facebook reply variant if current_service == facebook" do - allow(controller.current_session).to receive(:flow_string).and_return("message") - allow(controller.current_session).to receive(:state_string).and_return("say_hola") - file_contents, selected_preprocessor = controller.send(:action_replies) - - expect(file_contents).to eq(File.read(File.expand_path("../replies/messages/say_hola.yml+facebook.erb", __dir__))) - end - - it "should load the Twilio reply variant if current_service == twilio" do - allow(twilio_controller.current_session).to receive(:flow_string).and_return("message") - allow(twilio_controller.current_session).to receive(:state_string).and_return("say_hola") - file_contents, selected_preprocessor = twilio_controller.send(:action_replies) - - expect(file_contents).to eq(File.read(File.expand_path("../replies/messages/say_hola.yml+twilio.erb", __dir__))) - end - - it "should load the base reply variant if current_service does not have a custom variant" do - allow(epsilon_controller.current_session).to receive(:flow_string).and_return("message") - allow(epsilon_controller.current_session).to receive(:state_string).and_return("say_hola") - file_contents, selected_preprocessor = epsilon_controller.send(:action_replies) - - expect(file_contents).to eq(File.read(File.expand_path("../replies/messages/say_hola.yml.erb", __dir__))) - end - - it "should load the correct variant when there is no preprocessor" do - allow(gamma_controller.current_session).to receive(:flow_string).and_return("message") - allow(gamma_controller.current_session).to receive(:state_string).and_return("say_yo") - file_contents, selected_preprocessor = gamma_controller.send(:action_replies) - - expect(file_contents).to eq(File.read(File.expand_path("../replies/messages/say_yo.yml+twitter", __dir__))) - end - end - - describe "randomized replies" do - let(:stubbed_handler) { double("handler") } - let(:stubbed_client) { double("client") } - - before(:each) do - allow(Stealth::Services::Facebook::Client).to receive(:new).and_return(stubbed_client) - end - - describe "text replies" do - before(:each) do - allow(controller.current_session).to receive(:flow_string).and_return("message") - allow(controller.current_session).to receive(:state_string).and_return("say_randomize_text") - Stealth.config.auto_insert_delays = false - end - - after(:each) do - Stealth.config.auto_insert_delays = true - end - - it "should receive a single text string" do - allow(Stealth::Services::Facebook::ReplyHandler).to receive(:new) do |*args| - expect(args.first[:reply]['text']).to be_a(String) - stubbed_handler - end - allow(stubbed_handler).to receive(:text).exactly(1).time - expect(stubbed_client).to receive(:transmit).exactly(1).time - controller.say_randomize_text - end - end - end - - describe "sub-state replies" do - let(:stubbed_handler) { double("handler") } - let(:stubbed_client) { double("client") } - - before(:each) do - allow(Stealth::Services::Facebook::ReplyHandler).to receive(:new).and_return(stubbed_handler) - allow(Stealth::Services::Facebook::Client).to receive(:new).and_return(stubbed_client) - allow(controller.current_session).to receive(:flow_string).and_return("message") - allow(controller.current_session).to receive(:state_string).and_return("say_offer") - allow(stubbed_client).to receive(:transmit).and_return(true) - allow(controller).to receive(:sleep).and_return(true) - end - - it "should transmit only the last reply in the file when @pos = -1" do - expect(stubbed_handler).to receive(:text).exactly(1).time - expect(stubbed_handler).to receive(:delay).exactly(1).time # auto-delay - controller.pos = -1 - controller.say_offer - end - - it "should transmit the last two replies in the file when @pos = -2" do - expect(stubbed_handler).to receive(:text).exactly(1).time - expect(stubbed_handler).to receive(:delay).exactly(1).time - controller.pos = -2 - controller.say_offer - end - - it "should transmit all the replies in the file when @pos = 0" do - expect(stubbed_handler).to receive(:text).exactly(2).times - expect(stubbed_handler).to receive(:delay).exactly(2).times - controller.pos = 0 - controller.say_offer - end - - it "should transmit all the replies in the file when @pos = nil" do - expect(stubbed_handler).to receive(:text).exactly(2).times - expect(stubbed_handler).to receive(:delay).exactly(2).times - expect(controller.pos).to be_nil - controller.say_offer - end - end - - describe "Logging replies" do - let(:stubbed_handler) { double("handler") } - let(:stubbed_client) { double("client") } - - before(:each) do - allow(Stealth::Services::Facebook::ReplyHandler).to receive(:new).and_return(stubbed_handler) - allow(Stealth::Services::Facebook::Client).to receive(:new).and_return(stubbed_client) - allow(controller.current_session).to receive(:flow_string).and_return("message") - allow(controller.current_session).to receive(:state_string).and_return("say_simple_hello") - Stealth.config.auto_insert_delays = false - Stealth.config.transcript_logging = true - end - - after(:each) do - Stealth.config.auto_insert_delays = true - Stealth.config.transcript_logging = false - end - - it "should log replies if transcript_logging is enabled" do - allow(stubbed_client).to receive(:transmit).and_return(true) - allow(controller).to receive(:sleep).and_return(true).with(2.0) - - allow(stubbed_handler).to receive(:text).exactly(1).times - expect(Stealth::Logger).to receive(:l).with( - topic: 'facebook', - message: "User #{controller.current_session_id} -> Sending: Hello" - ) - controller.say_simple_hello - end - - it "should log translated replies if transcript_logging is enabled and the driver supports it" do - allow(stubbed_client).to receive(:transmit).and_return(true) - allow(controller).to receive(:sleep).and_return(true).with(2.0) - - allow(stubbed_handler).to receive(:text).exactly(1).times - allow(stubbed_handler).to receive(:translated_reply).and_return("Bonjour") - expect(Stealth::Logger).to receive(:l).with( - topic: 'facebook', - message: "User #{controller.current_session_id} -> Sending: Bonjour" - ) - controller.say_simple_hello - end - end - - describe "client errors" do - let(:stubbed_handler) { double("handler") } - let(:stubbed_client) { double("client") } - - before(:each) do - allow(Stealth::Services::Facebook::ReplyHandler).to receive(:new).and_return(stubbed_handler) - allow(Stealth::Services::Facebook::Client).to receive(:new).and_return(stubbed_client) - allow(controller.current_session).to receive(:flow_string).and_return("message") - allow(controller.current_session).to receive(:state_string).and_return("say_offer") - allow(stubbed_handler).to receive(:delay).exactly(1).time - allow(stubbed_handler).to receive(:text).exactly(1).time - end - - describe "Stealth::Errors::UserOptOut" do - before(:each) do - expect(stubbed_client).to receive(:transmit).and_raise( - Stealth::Errors::UserOptOut.new('boom') - ).once # Retuns early; doesn't send the remaining replies in the file - end - - it "should log the unhandled exception if the controller does not have a handle_opt_out method" do - expect(Stealth::Logger).to receive(:l).with( - topic: :err, - message: "Unhandled service exception for user #{facebook_message.sender_id}. No error handler for `handle_opt_out` found." - ) - expect(controller).to receive(:do_nothing) - controller.say_offer - end - - it "should call handle_opt_out method" do - expect(controller).to receive(:handle_opt_out) - expect(Stealth::Logger).to receive(:l).with( - topic: 'facebook', - message: "User #{facebook_message.sender_id} opted out. [boom]" - ) - expect(controller).to receive(:do_nothing) - controller.say_offer - end - end - - describe "Stealth::Errors::InvalidSessionID" do - before(:each) do - expect(stubbed_client).to receive(:transmit).and_raise( - Stealth::Errors::InvalidSessionID.new('boom') - ).once # Retuns early; doesn't send the remaining replies in the file - end - - it "should log the unhandled exception if the controller does not have a handle_invalid_session_id method" do - expect(Stealth::Logger).to receive(:l).with( - topic: :err, - message: "Unhandled service exception for user #{facebook_message.sender_id}. No error handler for `handle_invalid_session_id` found." - ) - expect(controller).to receive(:do_nothing) - controller.say_offer - end - - it "should call handle_invalid_session_id method" do - expect(controller).to receive(:handle_invalid_session_id) - expect(Stealth::Logger).to receive(:l).with( - topic: 'facebook', - message: "User #{facebook_message.sender_id} has an invalid session_id. [boom]" - ) - expect(controller).to receive(:do_nothing) - controller.say_offer - end - end - - describe "Stealth::Errors::MessageFiltered" do - before(:each) do - expect(stubbed_client).to receive(:transmit).and_raise( - Stealth::Errors::MessageFiltered.new('boom') - ).once # Retuns early; doesn't send the remaining replies in the file - end - - it "should log the unhandled exception if the controller does not have a handle_message_filtered method" do - expect(Stealth::Logger).to receive(:l).with( - topic: :err, - message: "Unhandled service exception for user #{facebook_message.sender_id}. No error handler for `handle_message_filtered` found." - ) - expect(controller).to receive(:do_nothing) - controller.say_offer - end - - it "should call handle_message_filtered method" do - expect(controller).to receive(:handle_message_filtered) - expect(Stealth::Logger).to receive(:l).with( - topic: 'facebook', - message: "Message to user #{facebook_message.sender_id} was filtered. [boom]" - ) - expect(controller).to receive(:do_nothing) - controller.say_offer - end - end - - describe "Stealth::Errors::UnknownServiceError" do - before(:each) do - expect(stubbed_client).to receive(:transmit).and_raise( - Stealth::Errors::UnknownServiceError.new('boom') - ).once # Retuns early; doesn't send the remaining replies in the file - end - - it "should log the unhandled exception if the controller does not have a handle_unknown_error method" do - expect(Stealth::Logger).to receive(:l).with( - topic: :err, - message: "Unhandled service exception for user #{facebook_message.sender_id}. No error handler for `handle_unknown_error` found." - ) - expect(controller).to receive(:do_nothing) - controller.say_offer - end - - it "should call handle_unknown_error method" do - expect(controller).to receive(:handle_unknown_error) - expect(Stealth::Logger).to receive(:l).with( - topic: 'facebook', - message: "User #{facebook_message.sender_id} had an unknown error. [boom]" - ) - expect(controller).to receive(:do_nothing) - controller.say_offer - end - end - - describe 'an unknown client error' do - before(:each) do - allow(stubbed_client).to receive(:transmit).and_raise( - StandardError - ) - end - - it 'should raise the error' do - expect { - controller.say_offer - }.to raise_error(StandardError) - end - end - end - -end diff --git a/spec/controller/unrecognized_message_spec.rb b/spec/controller/unrecognized_message_spec.rb deleted file mode 100644 index 77d95092..00000000 --- a/spec/controller/unrecognized_message_spec.rb +++ /dev/null @@ -1,168 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe "Stealth::Controller::UnrecognizedMessage" do - - let(:fb_message) { SampleMessage.new(service: 'facebook') } - let(:controller) { VadersController.new(service_message: fb_message.message_with_text) } - - describe 'run_unrecognized_message' do - let(:e) { - e = OpenStruct.new - e.class = RuntimeError - e.message = 'oops' - e.backtrace = [ - '/stealth/lib/stealth/controller/controller.rb', - '/stealth/lib/stealth/controller/catch_all.rb', - ] - e - } - - describe 'when UnrecognizedMessagesController is not defined' do - before(:each) do - Object.send(:remove_const, :UnrecognizedMessagesController) - end - - it "should log and run catch_all" do - expect(Stealth::Logger).to receive(:l).with( - topic: 'unrecognized_message', - message: "The message \"Hello World!\" was not recognized in the original context." - ).ordered - - expect(Stealth::Logger).to receive(:l).with( - topic: 'unrecognized_message', - message: 'Running catch_all; UnrecognizedMessagesController not defined.' - ).ordered - - expect(controller).to receive(:run_catch_all).with(err: e) - controller.run_unrecognized_message(err: e) - end - end - - it "should call handle_unrecognized_message on the UnrecognizedMessagesController" do - class UnrecognizedMessagesController < Stealth::Controller - def handle_unrecognized_message - do_nothing - end - end - - expect(Stealth::Logger).to receive(:l).with( - topic: 'unrecognized_message', - message: "The message \"Hello World!\" was not recognized in the original context." - ).ordered - - expect(Stealth::Logger).to receive(:l).with( - topic: 'unrecognized_message', - message: 'A match was detected. Skipping catch-all.' - ).ordered - - controller.run_unrecognized_message(err: e) - end - - it "should log if the UnrecognizedMessagesController#handle_unrecognized_message does not progress the session" do - class UnrecognizedMessagesController < Stealth::Controller - def handle_unrecognized_message - # Oops - end - end - - expect(Stealth::Logger).to receive(:l).with( - topic: 'unrecognized_message', - message: "The message \"Hello World!\" was not recognized in the original context." - ).ordered - - expect(Stealth::Logger).to receive(:l).with( - topic: 'unrecognized_message', - message: 'Did not send replies, update session, or step' - ).ordered - - expect(controller).to_not receive(:run_catch_all) - - controller.run_unrecognized_message(err: e) - end - - describe 'handoff to catch_all' do - before(:each) do - @session = Stealth::Session.new(id: controller.current_session_id) - @session.set_session(new_flow: 'vader', new_state: 'action_with_unrecognized_msg') - - @error_slug = [ - 'error', - controller.current_session_id, - 'vader', - 'action_with_unrecognized_msg' - ].join('-') - - $redis.del(@error_slug) - end - - it "should catch StandardError within UnrecognizedMessagesController and run catch_all" do - $err = Stealth::Errors::ReplyNotFound.new('oops') - - class UnrecognizedMessagesController < Stealth::Controller - def handle_unrecognized_message - raise $err - end - end - - expect(Stealth::Logger).to receive(:l).with( - topic: 'unrecognized_message', - message: "The message \"Hello World!\" was not recognized in the original context." - ).ordered - - expect(controller).to receive(:run_catch_all).with(err: $err) - - controller.run_unrecognized_message(err: e) - end - - it "should track the catch_all level against the original session during exceptions" do - class UnrecognizedMessagesController < Stealth::Controller - def handle_unrecognized_message - raise 'oops' - end - end - - expect($redis.get(@error_slug)).to be_nil - controller.run_unrecognized_message(err: e) - expect($redis.get(@error_slug)).to eq '1' - end - - it "should track the catch_all level against the original session for UnrecognizedMessage errors" do - class UnrecognizedMessagesController < Stealth::Controller - def handle_unrecognized_message - handle_message( - 'x' => proc { do_nothing }, - 'y' => proc { do_nothing } - ) - end - end - - expect($redis.get(@error_slug)).to be_nil - controller.action(action: :action_with_unrecognized_msg) - expect($redis.get(@error_slug)).to eq '1' - end - - it "should NOT run catch_all if UnrecognizedMessagesController handles the message" do - $x = 0 - class UnrecognizedMessagesController < Stealth::Controller - def handle_unrecognized_message - handle_message( - 'Hello World!' => proc { - $x = 1 - do_nothing - }, - 'y' => proc { do_nothing } - ) - end - end - - expect($redis.get(@error_slug)).to be_nil - controller.action(action: :action_with_unrecognized_msg) - expect($redis.get(@error_slug)).to be_nil - expect($x).to eq 1 - end - end - end - -end diff --git a/spec/dispatcher_spec.rb b/spec/dispatcher_spec.rb deleted file mode 100644 index 173c755c..00000000 --- a/spec/dispatcher_spec.rb +++ /dev/null @@ -1,79 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe "Stealth::Dispatcher" do - - class Stealth::Services::Facebook::MessageHandler - - end - - describe 'coordinate' do - let(:dispatcher) { - Stealth::Dispatcher.new( - service: 'facebook', - params: {}, - headers: {} - ) - } - - it 'should call coordinate on the message handler' do - message_handler = double - expect(Stealth::Services::Facebook::MessageHandler).to receive(:new).and_return(message_handler) - expect(message_handler).to receive(:coordinate) - - dispatcher.coordinate - end - end - - describe 'process' do - class StubbedBotController < Stealth::Controller - def route - true - end - end - - let(:dispatcher) { - Stealth::Dispatcher.new( - service: 'facebook', - params: {}, - headers: {} - ) - } - let(:fb_message) { SampleMessage.new(service: 'facebook') } - let(:stubbed_controller) { - StubbedBotController.new(service_message: fb_message.message_with_text) - } - - it 'should call process on the message handler' do - message_handler = double - - # Stub out the message handler to return a service_message - expect(Stealth::Services::Facebook::MessageHandler).to receive(:new).and_return(message_handler) - expect(message_handler).to receive(:process).and_return(fb_message.message_with_text) - - # Stub out BotController and set session - expect(BotController).to receive(:new).and_return(stubbed_controller) - stubbed_controller.current_session.set_session(new_flow: 'mr_tron', new_state: 'other_action') - - dispatcher.process - end - - it 'should log the incoming message if transcript_logging is enabled' do - message_handler = double - - # Stub out the message handler to return a service_message - expect(Stealth::Services::Facebook::MessageHandler).to receive(:new).and_return(message_handler) - expect(message_handler).to receive(:process).and_return(fb_message.message_with_text) - - # Stub out BotController and set session - expect(BotController).to receive(:new).and_return(stubbed_controller) - stubbed_controller.current_session.set_session(new_flow: 'mr_tron', new_state: 'other_action') - - Stealth.config.transcript_logging = true - expect(dispatcher).to receive(:log_incoming_message).with(fb_message.message_with_text) - dispatcher.process - end - end - -end diff --git a/spec/flow/flow_spec.rb b/spec/flow/flow_spec.rb deleted file mode 100644 index 31582f0b..00000000 --- a/spec/flow/flow_spec.rb +++ /dev/null @@ -1,82 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe Stealth::Flow do - - class CustomFlowMap - include Stealth::Flow - - flow :new_todo do - state :new - state :get_due_date - state :created - state :error - end - - flow :hello do - state :say_hello - state :say_oi - end - - flow "howdy" do - state :say_howdy - end - end - - let(:flow_map) { CustomFlowMap.new } - - describe "inititating with states" do - it "should init a state given a state name" do - flow_map.init(flow: 'new_todo', state: 'created') - expect(flow_map.current_state).to eq :created - - flow_map.init(flow: 'new_todo', state: 'error') - expect(flow_map.current_state).to eq :error - end - - it "should raise an error if an invalid state is specified" do - expect { - flow_map.init(flow: 'new_todo', state: 'invalid') - }.to raise_error(Stealth::Errors::InvalidStateTransition) - end - end - - describe "accessing states" do - it "should default to the first flow and state" do - expect(flow_map.current_flow).to eq(:new_todo) - expect(flow_map.current_state).to eq(:new) - end - - it "should support comparing states" do - first_state = CustomFlowMap.flow_spec[:new_todo].states[:new] - last_state = CustomFlowMap.flow_spec[:new_todo].states[:error] - expect(first_state < last_state).to be true - expect(last_state > first_state).to be true - end - - it "should allow every state to be fetched for a flow" do - expect(CustomFlowMap.flow_spec[:new_todo].states.length).to eq 4 - expect(CustomFlowMap.flow_spec[:hello].states.length).to eq 2 - expect(CustomFlowMap.flow_spec[:new_todo].states.keys).to eq([:new, :get_due_date, :created, :error]) - expect(CustomFlowMap.flow_spec[:hello].states.keys).to eq([:say_hello, :say_oi]) - end - - it "should return the states in an array for a given FlowMap instance" do - expect(flow_map.states).to eq [:new, :get_due_date, :created, :error] - flow_map.init(flow: :hello, state: :say_oi) - expect(flow_map.states).to eq [:say_hello, :say_oi] - end - - it "should allow flows to be specified with strings" do - expect(CustomFlowMap.flow_spec[:howdy].states.length).to eq 1 - expect(CustomFlowMap.flow_spec[:howdy].states.keys).to eq([:say_howdy]) - end - - it "should allow FlowMaps to be intialized with strings" do - flow_map.init(flow: "hello", state: "say_oi") - expect(flow_map.states).to eq [:say_hello, :say_oi] - end - end - -end diff --git a/spec/flow/state_spec.rb b/spec/flow/state_spec.rb deleted file mode 100644 index f9d9d152..00000000 --- a/spec/flow/state_spec.rb +++ /dev/null @@ -1,109 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe Stealth::Flow::State do - - class SuperFlowMap - include Stealth::Flow - - flow :new_todo do - state :new - state :get_due_date - state :created, fails_to: :new - state :created2, fails_to: 'new_todo->new' - state :deprecated, redirects_to: 'new' - state :deprecated2, redirects_to: 'other_flow->say_hi' - state :new_opts, opt1: 'hello', opt2: 1 - state :error - end - end - - let(:flow_map) { SuperFlowMap.new } - - describe "flow states" do - it "should convert itself to a string" do - expect(flow_map.current_state.to_s).to be_a(String) - end - - it "should convert itself to a symbol" do - expect(flow_map.current_state.to_sym).to be_a(Symbol) - end - end - - describe "fails_to" do - it "should be nil for a state that has not specified a fails_to" do - expect(flow_map.current_state.fails_to).to be_nil - end - - it "should return the fail_state if a fails_to was specified" do - flow_map.init(flow: :new_todo, state: :created) - expect(flow_map.current_state.fails_to).to be_a(Stealth::Session) - expect(flow_map.current_state.fails_to.state_string).to eq 'new' - end - - it "should return the fail_state if a fails_to was specified as a session" do - flow_map.init(flow: :new_todo, state: :created2) - expect(flow_map.current_state.fails_to).to be_a(Stealth::Session) - expect(flow_map.current_state.fails_to.state_string).to eq 'new' - expect(flow_map.current_state.fails_to.flow_string).to eq 'new_todo' - end - end - - describe "redirects_to" do - it "should be nil for a state that has not specified a fails_to" do - expect(flow_map.current_state.redirects_to).to be_nil - end - - it "should return the redirects_to state if a redirects_to was specified" do - flow_map.init(flow: :new_todo, state: :deprecated) - expect(flow_map.current_state.redirects_to).to be_a(Stealth::Session) - expect(flow_map.current_state.redirects_to.state_string).to eq 'new' - end - - it "should return the redirects_to state if a redirects_to was specified as a session" do - flow_map.init(flow: :new_todo, state: :deprecated2) - expect(flow_map.current_state.redirects_to).to be_a(Stealth::Session) - expect(flow_map.current_state.redirects_to.state_string).to eq 'say_hi' - expect(flow_map.current_state.redirects_to.flow_string).to eq 'other_flow' - end - end - - describe "opts" do - it "should return {} for a state that has not specified any opts" do - expect(flow_map.current_state.opts).to eq({}) - end - - it "should return the opts if they were specified" do - flow_map.init(flow: :new_todo, state: :new_opts) - expect(flow_map.current_state.opts).to eq({ opt1: 'hello', opt2: 1 }) - end - end - - describe "state incrementing and decrementing" do - it "should increment the state" do - flow_map.init(flow: :new_todo, state: :get_due_date) - new_state = flow_map.current_state + 1.state - expect(new_state).to eq(:created) - end - - it "should decrement the state" do - flow_map.init(flow: :new_todo, state: :error) - new_state = flow_map.current_state - 6.states - expect(new_state).to eq(:get_due_date) - end - - it "should return the first state if the decrement is out of bounds" do - flow_map.init(flow: :new_todo, state: :get_due_date) - new_state = flow_map.current_state - 5.states - expect(new_state).to eq(:new) - end - - it "should return the last state if the increment is out of bounds" do - flow_map.init(flow: :new_todo, state: :created) - new_state = flow_map.current_state + 10.states - expect(new_state).to eq(:error) - end - end - -end diff --git a/spec/helpers/redis_spec.rb b/spec/helpers/redis_spec.rb deleted file mode 100644 index eeef03ff..00000000 --- a/spec/helpers/redis_spec.rb +++ /dev/null @@ -1,77 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe "Stealth::Redis" do - - class RedisTester - include Stealth::Redis - end - - let(:redis_tester) { RedisTester.new } - let(:key) { 'xyz' } - - describe "get_key" do - it "should return the key from Redis if an expiration is not set" do - $redis.set(key, 'abc') - expect(redis_tester.send(:get_key, key)).to eq 'abc' - end - - it "should call getex if an expiration is set" do - expect(redis_tester).to receive(:getex).with(key, 30) - redis_tester.send(:get_key, key, expiration: 30) - end - end - - describe "delete_key" do - it 'should delete the key from Redis' do - $redis.set(key, 'abc') - expect(redis_tester.send(:get_key, key)).to eq 'abc' - redis_tester.send(:delete_key, key) - expect(redis_tester.send(:get_key, key)).to be_nil - end - end - - describe "getex" do - it "should return the key from Redis" do - Stealth.config.session_ttl = 50 - $redis.set(key, 'abc') - expect(redis_tester.send(:getex, key)).to eq 'abc' - end - - it "should set the expiration of a key in Redis" do - Stealth.config.session_ttl = 50 - $redis.set(key, 'abc') - redis_tester.send(:getex, key) - expect($redis.ttl(key)).to be_between(0, 50).inclusive - end - - it "should update the expiration of a key in Redis" do - Stealth.config.session_ttl = 500 - $redis.setex(key, 50, 'abc') - redis_tester.send(:getex, key) - expect($redis.ttl(key)).to be_between(400, 500).inclusive - end - end - - describe "persist_key" do - it "should set the key in Redis" do - Stealth.config.session_ttl = 50 - redis_tester.send(:persist_key, key: key, value: 'zzz') - expect($redis.get(key)).to eq 'zzz' - end - - it "should set the expiration to session_ttl if none specified" do - Stealth.config.session_ttl = 50 - redis_tester.send(:persist_key, key: key, value: 'zzz') - expect($redis.ttl(key)).to be_between(0, 50).inclusive - end - - it "should set the expiration to the specified value when provided" do - Stealth.config.session_ttl = 50 - redis_tester.send(:persist_key, key: key, value: 'zzz', expiration: 500) - expect($redis.ttl(key)).to be_between(400, 500).inclusive - end - end - -end diff --git a/spec/lock_spec.rb b/spec/lock_spec.rb deleted file mode 100644 index c6698cef..00000000 --- a/spec/lock_spec.rb +++ /dev/null @@ -1,100 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe "Stealth::Lock" do - let(:session_id) { SecureRandom.hex(14) } - let(:session_slug) { 'hello->say_hello' } - - before(:each) do - Stealth.config.lock_autorelease = 30 - end - - describe "create" do - it "should raise an ArgumentError if the session_slug was not provided" do - lock = Stealth::Lock.new(session_id: session_id) - expect { - lock.create - }.to raise_error(ArgumentError) - end - - it "should save the lock using a canonical key and value" do - lock = Stealth::Lock.new(session_id: session_id, session_slug: session_slug) - canonical_key = "#{session_id}-lock" - expected_value = "#{lock.tid}##{session_slug}" - lock.create - expect($redis.get(canonical_key)).to eq expected_value - end - - it "should include the reply file position in the lock" do - lock = Stealth::Lock.new( - session_id: session_id, - session_slug: session_slug, - position: 3 - ) - canonical_key = "#{session_id}-lock" - expected_value = "#{lock.tid}##{session_slug}:3" - lock.create - expect($redis.get(canonical_key)).to eq expected_value - end - - it "should set the lock expiration to lock_autorelease" do - lock = Stealth::Lock.new(session_id: session_id, session_slug: session_slug) - canonical_key = "#{session_id}-lock" - expected_value = "#{lock.tid}##{session_slug}" - lock.create - expect($redis.ttl(canonical_key)).to be_between(1, 30).inclusive - end - end - - describe "release" do - it "should delete the key in Redis" do - lock = Stealth::Lock.new(session_id: session_id, session_slug: session_slug) - canonical_key = "#{session_id}-lock" - lock.create - expect($redis.get(canonical_key)).to_not be_nil - lock.release - expect($redis.get(canonical_key)).to be_nil - end - end - - describe "slug" do - it "should return the lock slug from Redis" do - lock = Stealth::Lock.new(session_id: session_id, session_slug: session_slug) - lock.create - canonical_key = "#{session_id}-lock" - expect(lock.slug).to eq "#{lock.tid}##{session_slug}" - end - end - - describe "flow_and_state" do - it "should return a hash containing the flow and state" do - lock = Stealth::Lock.new(session_id: session_id, session_slug: session_slug) - expect(lock.flow_and_state[:flow]).to eq 'hello' - expect(lock.flow_and_state[:state]).to eq 'say_hello' - end - end - - describe "self.find_lock" do - it "should load the lock from Redis" do - lock_key = "#{session_id}-lock" - example_tid = 'ovefhgJvx' - example_session = 'goodbye->say_goodbye' - example_position = 2 - example_lock = "#{example_tid}##{example_session}:#{example_position}" - $redis.set(lock_key, example_lock) - - lock = Stealth::Lock.find_lock(session_id: session_id) - expect(lock.tid).to eq example_tid - expect(lock.session_slug).to eq example_session - expect(lock.position).to eq example_position - end - - it "should return nil if the lock is not found" do - lock_key = "#{session_id}-lock" - lock = Stealth::Lock.find_lock(session_id: session_id) - expect($redis.get(lock_key)).to be_nil - expect(lock).to be_nil - end - end -end diff --git a/spec/nlp/client_spec.rb b/spec/nlp/client_spec.rb deleted file mode 100644 index b78c50e9..00000000 --- a/spec/nlp/client_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe "Stealth::Nlp::Client" do - - describe 'blank client' do - let(:nlp_client) { Stealth::Nlp::Client.new } - - it 'should return nil for client' do - expect(nlp_client.client).to be_nil - end - - it 'should return nil for the understand call' do - expect(nlp_client.understand(query: 'hello world!')).to be_nil - end - - it 'should return nil for the understand_speec call' do - expect(nlp_client.understand_speech(audio_file: 'https://path.to/audio.mp3')).to be_nil - end - end - -end diff --git a/spec/nlp/result_spec.rb b/spec/nlp/result_spec.rb deleted file mode 100644 index a6f32e8b..00000000 --- a/spec/nlp/result_spec.rb +++ /dev/null @@ -1,57 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe "Stealth::Nlp::Result" do - - it 'should return the built-in entity types' do - expect(Stealth::Nlp::Result::ENTITY_TYPES).to eq %i(number currency email percentage phone age - url ordinal geo dimension temp datetime duration - key_phrase name) - end - - describe 'blank result' do - let(:stealth_result) { Stealth::Nlp::Result.new(result: 1234) } - - it 'should initialize @result to the value provided during instantiation' do - expect(stealth_result.result).to eq 1234 - end - - it 'should return nil for parsed_result' do - expect(stealth_result.parsed_result).to be_nil - end - - it 'should return nil for intent_id' do - expect(stealth_result.intent_id).to be_nil - end - - it 'should return nil for intent' do - expect(stealth_result.intent).to be_nil - end - - it 'should return nil for intent_score' do - expect(stealth_result.intent_score).to be_nil - end - - it 'should return {} for raw_entities' do - expect(stealth_result.raw_entities).to eq({}) - end - - it 'should return {} for entities' do - expect(stealth_result.entities).to eq({}) - end - - it 'should return nil for sentiment' do - expect(stealth_result.sentiment).to be_nil - end - - it 'should return nil for sentiment_score' do - expect(stealth_result.sentiment_score).to be_nil - end - - it 'should return false for present?' do - expect(stealth_result.present?).to be false - end - end - -end diff --git a/spec/replies/hello.yml.erb b/spec/replies/hello.yml.erb deleted file mode 100644 index 5b1d698e..00000000 --- a/spec/replies/hello.yml.erb +++ /dev/null @@ -1,15 +0,0 @@ -- reply_type: text - text: "Hi, <%= first_name %>. Welcome to Stealth bot..." -- reply_type: delay - duration: 2 -- reply_type: text - text: "We offer users an awesome Ruby framework for building chat bots." -- reply_type: delay - duration: 2 -- reply_type: text - text: "What do you think of our bot?" - buttons: - - text: "Cool" - payload: cool - - text: "Show me more" - payload: more diff --git a/spec/replies/messages/say_hola.yml+facebook.erb b/spec/replies/messages/say_hola.yml+facebook.erb deleted file mode 100644 index f7d2de02..00000000 --- a/spec/replies/messages/say_hola.yml+facebook.erb +++ /dev/null @@ -1,6 +0,0 @@ -- reply_type: text - text: "Hi, Facebook. Welcome to Stealth bot..." -- reply_type: delay - duration: 2 -- reply_type: text - text: "We offer users an awesome Ruby framework for building chat bots." diff --git a/spec/replies/messages/say_hola.yml+twilio.erb b/spec/replies/messages/say_hola.yml+twilio.erb deleted file mode 100644 index afce1718..00000000 --- a/spec/replies/messages/say_hola.yml+twilio.erb +++ /dev/null @@ -1,6 +0,0 @@ -- reply_type: text - text: "Hi, Twilio. Welcome to Stealth bot..." -- reply_type: delay - duration: 2 -- reply_type: text - text: "We offer users an awesome Ruby framework for building chat bots." diff --git a/spec/replies/messages/say_hola.yml.erb b/spec/replies/messages/say_hola.yml.erb deleted file mode 100644 index bf4d4c11..00000000 --- a/spec/replies/messages/say_hola.yml.erb +++ /dev/null @@ -1,6 +0,0 @@ -- reply_type: text - text: "Hi, Morty. Welcome to Stealth bot..." -- reply_type: delay - duration: 2 -- reply_type: text - text: "We offer users an awesome Ruby framework for building chat bots." diff --git a/spec/replies/messages/say_howdy_with_dynamic.yml b/spec/replies/messages/say_howdy_with_dynamic.yml deleted file mode 100644 index e0bd33bd..00000000 --- a/spec/replies/messages/say_howdy_with_dynamic.yml +++ /dev/null @@ -1,79 +0,0 @@ -- reply_type: delay # position 0 - duration: dynamic -- reply_type: text - text: "Lorem ipsum dolor sit amet posuere." -- reply_type: delay # position 2 - duration: dynamic -- reply_type: text - text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin in lobortis ante. Duis elementum lacus sit amet volutpat." -- reply_type: delay # position 4 - duration: dynamic -- reply_type: text - text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer quis fermentum arcu, eget maximus metus. Vivamus ante tortor, scelerisque vel laoreet sit amet, sodales in augue. Nam vel quam a tellus mattis vestibulum non a amet." - buttons: - - text: "Cool" - payload: cool - - text: "Show me more" - payload: more -- reply_type: delay # position 6 - duration: dynamic -- reply_type: text - text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec quis consequat dolor. Suspendisse sed egestas lacus. Nam a viverra risus. Aliquam erat volutpat. Nunc molestie, metus ut cursus varius, purus quam pharetra nisi, sit amet porttitor tellus nisl sit amet est. Nam volutpat id lorem a condimentum. Sed quam mauris, faucibus sed pharetra sed." -- reply_type: delay # position 8 - duration: dynamic -- reply_type: image - text: "https://via.placeholder.com/350x150" -- reply_type: delay # position 10 - duration: dynamic -- reply_type: video - text: "https://via.placeholder.com/350x150" -- reply_type: delay # position 12 - duration: dynamic -- reply_type: audio - text: "https://via.placeholder.com/350x150" -- reply_type: delay # position 14 - duration: dynamic -- reply_type: file - text: "https://via.placeholder.com/350x150" -- reply_type: delay # position 16 - duration: dynamic -- reply_type: cards - sharable: true - aspect_ratio: horizontal - elements: - - title: My App - subtitle: Download our app below or visit our website for more info. - image_url: "https://my-app.com/app-image.png" - buttons: - - type: url - url: "https://my-app.com" - text: 'View' - webview_height: 'tall' - - type: url - url: "https://itunes.apple.com/us/app/my-app" - text: 'Download iOS App' -- reply_type: delay # position 18 - duration: dynamic -- reply_type: list - top_element_style: large - buttons: - - type: payload - text: View More - payload: view_more - elements: - - title: Your Daily News Update - subtitle: The following stories have been curated just for you. - image_url: "https://loremflickr.com/320/240" - buttons: - - type: url - url: "https://news-articles.com/199" - text: 'View' - webview_height: 'tall' - - title: Breakthrough in AI - subtitle: Major breakthrough in the AI space. - image_url: "https://loremflickr.com/320/320" - default_action: - - url: "https://news-articles.com/232" - webview_height: 'tall' -- reply_type: delay # position 20 - duration: dynamic diff --git a/spec/replies/messages/say_msgs_without_breaks.yml b/spec/replies/messages/say_msgs_without_breaks.yml deleted file mode 100644 index 22d11daa..00000000 --- a/spec/replies/messages/say_msgs_without_breaks.yml +++ /dev/null @@ -1,4 +0,0 @@ -- reply_type: text - text: "Hi, Morty. Welcome to Stealth bot..." -- reply_type: text - text: "We offer users an awesome Ruby framework for building chat bots." diff --git a/spec/replies/messages/say_offer.yml b/spec/replies/messages/say_offer.yml deleted file mode 100644 index bf4d4c11..00000000 --- a/spec/replies/messages/say_offer.yml +++ /dev/null @@ -1,6 +0,0 @@ -- reply_type: text - text: "Hi, Morty. Welcome to Stealth bot..." -- reply_type: delay - duration: 2 -- reply_type: text - text: "We offer users an awesome Ruby framework for building chat bots." diff --git a/spec/replies/messages/say_offer_with_dynamic.yml b/spec/replies/messages/say_offer_with_dynamic.yml deleted file mode 100644 index c55273d3..00000000 --- a/spec/replies/messages/say_offer_with_dynamic.yml +++ /dev/null @@ -1,6 +0,0 @@ -- reply_type: text - text: "Hi, Morty. Welcome to Stealth bot..." -- reply_type: delay - duration: dynamic -- reply_type: text - text: "We offer users an awesome Ruby framework for building chat bots." diff --git a/spec/replies/messages/say_oi.yml.erb b/spec/replies/messages/say_oi.yml.erb deleted file mode 100644 index 9904acdb..00000000 --- a/spec/replies/messages/say_oi.yml.erb +++ /dev/null @@ -1,15 +0,0 @@ -- reply_type: text - text: "Hi, <%= @first_name %>. Welcome to Stealth bot..." -- reply_type: delay - duration: 2 -- reply_type: text - text: "We offer users an awesome Ruby framework for building chat bots." -- reply_type: delay - duration: 2 -- reply_type: text - text: "What do you think of our bot?" - buttons: - - text: "Cool" - payload: cool - - text: "Show me more" - payload: more diff --git a/spec/replies/messages/say_randomize_speech.yml b/spec/replies/messages/say_randomize_speech.yml deleted file mode 100644 index e97f9366..00000000 --- a/spec/replies/messages/say_randomize_speech.yml +++ /dev/null @@ -1,10 +0,0 @@ -- reply_type: speech - text: - - Build amazing chatbots with tools you know and love. - - Stealth is an open source Ruby framework for voice and text chatbots. - - From our MVC architecture to our convention over configuration philosophy, you'll feel right at home with Stealth. - - Every service integration in Stealth is a Ruby gem. - - From web servers to continuous integration testing, Stealth is built to take advantage of all the great work done by the web development community. - - Stealth is a Rack application. - - Stealth already powers bots for large, well-known brands. - - Stealth is MIT licensed to ensure you own your bot. diff --git a/spec/replies/messages/say_randomize_text.yml b/spec/replies/messages/say_randomize_text.yml deleted file mode 100644 index 22d31b02..00000000 --- a/spec/replies/messages/say_randomize_text.yml +++ /dev/null @@ -1,10 +0,0 @@ -- reply_type: text - text: - - Build amazing chatbots with tools you know and love. - - Stealth is an open source Ruby framework for voice and text chatbots. - - From our MVC architecture to our convention over configuration philosophy, you'll feel right at home with Stealth. - - Every service integration in Stealth is a Ruby gem. - - From web servers to continuous integration testing, Stealth is built to take advantage of all the great work done by the web development community. - - Stealth is a Rack application. - - Stealth already powers bots for large, well-known brands. - - Stealth is MIT licensed to ensure you own your bot. diff --git a/spec/replies/messages/say_simple_hello.yml b/spec/replies/messages/say_simple_hello.yml deleted file mode 100644 index 33ef46d0..00000000 --- a/spec/replies/messages/say_simple_hello.yml +++ /dev/null @@ -1,2 +0,0 @@ -- reply_type: text - text: "Hello" diff --git a/spec/replies/messages/say_yo.yml b/spec/replies/messages/say_yo.yml deleted file mode 100644 index ed0bbb5e..00000000 --- a/spec/replies/messages/say_yo.yml +++ /dev/null @@ -1,6 +0,0 @@ -- reply_type: text - text: "Yo, Morty! Welcome to Stealth bot..." -- reply_type: delay - duration: 2 -- reply_type: text - text: "We offer users an awesome Ruby framework for building chat bots." diff --git a/spec/replies/messages/say_yo.yml+twitter b/spec/replies/messages/say_yo.yml+twitter deleted file mode 100644 index 4509b6b1..00000000 --- a/spec/replies/messages/say_yo.yml+twitter +++ /dev/null @@ -1,6 +0,0 @@ -- reply_type: text - text: "Yo, Morty from Twitter! Welcome to Stealth bot..." -- reply_type: delay - duration: 2 -- reply_type: text - text: "We offer users an awesome Ruby framework for building chat bots." diff --git a/spec/replies/messages/sub1/sub2/say_nested.yml b/spec/replies/messages/sub1/sub2/say_nested.yml deleted file mode 100644 index e84c58ac..00000000 --- a/spec/replies/messages/sub1/sub2/say_nested.yml +++ /dev/null @@ -1,10 +0,0 @@ -- reply_type: text - text: "Hi, Morty. Welcome to Stealth bot..." -- reply_type: delay - duration: 2 -- reply_type: text - text: "We offer users an awesome Ruby framework for building chat bots." -- reply_type: delay - duration: 2 -- reply_type: text - text: "This reply was nested." diff --git a/spec/reply_spec.rb b/spec/reply_spec.rb deleted file mode 100644 index 863a855b..00000000 --- a/spec/reply_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe "Stealth::Reply" do - - let!(:unstructured_text) { - { 'reply_type' => 'text', 'text' => 'Hello World!' } - } - let!(:unstructured_delay) { - { 'reply_type' => 'delay', 'duration' => 'dynamic' } - } - let(:text_reply) { Stealth::Reply.new(unstructured_reply: unstructured_text) } - let(:delay_reply) { Stealth::Reply.new(unstructured_reply: unstructured_delay) } - - describe 'hash-like [] getter' do - it 'should return the values' do - expect(text_reply['text']).to eq 'Hello World!' - expect(delay_reply['duration']).to eq 'dynamic' - end - end - - describe 'hash-like []= setter' do - it 'should return the values' do - text_reply['woot'] = 'root' - delay_reply['duration'] = 4.3 - expect(text_reply['woot']).to eq 'root' - expect(delay_reply['duration']).to eq 4.3 - end - end - - describe 'reply_type' do - it 'should act as a getter method for reply_type' do - expect(text_reply.reply_type).to eq 'text' - expect(delay_reply.reply_type).to eq 'delay' - end - end - - describe 'delay?' do - it 'should return false for a text reply' do - expect(text_reply.delay?).to be false - end - - it 'should return true for a delay reply' do - expect(delay_reply.delay?).to be true - end - end - - describe 'self.dynamic_delay' do - it 'should return a new Stealth::Reply' do - expect(Stealth::Reply.dynamic_delay).to be_a(Stealth::Reply) - end - - it 'should be a dynamic delay' do - expect(Stealth::Reply.dynamic_delay.delay?).to be true - expect(Stealth::Reply.dynamic_delay.reply_type).to eq 'delay' - expect(Stealth::Reply.dynamic_delay['duration']).to eq 'dynamic' - end - end - -end diff --git a/spec/scheduled_reply_spec.rb b/spec/scheduled_reply_spec.rb deleted file mode 100644 index 8065862d..00000000 --- a/spec/scheduled_reply_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe "Stealth::ScheduledReplyJob" do - - let(:scheduled_reply_job) { Stealth::ScheduledReplyJob.new } - - it "should instantiate BotController with service_message, set flow and state, and route" do - service_msg_double = double('service_message') - expect(Stealth::ServiceMessage).to receive(:new).with(service: 'twilio').and_return(service_msg_double) - expect(service_msg_double).to receive(:sender_id=).with('+18885551212') - expect(service_msg_double).to receive(:target_id=).with('33322') - - bot_controller_double = double('bot_controller') - expect(BotController).to receive(:new).with(service_message: service_msg_double).and_return(bot_controller_double) - expect(bot_controller_double).to receive(:step_to).with(flow: 'my_flow', state: 'say_hi') - - scheduled_reply_job = Stealth::ScheduledReplyJob.new - scheduled_reply_job.perform('twilio', '+18885551212', 'my_flow', 'say_hi', '33322') - end - -end diff --git a/spec/service_reply_spec.rb b/spec/service_reply_spec.rb deleted file mode 100644 index f00e1280..00000000 --- a/spec/service_reply_spec.rb +++ /dev/null @@ -1,92 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe "Stealth::ServiceReply" do - - let(:recipient_id) { "8b3e0a3c-62f1-401e-8b0f-615c9d256b1f" } - let(:yaml_reply) { File.read(File.join(File.dirname(__FILE__), 'replies', 'hello.yml.erb')) } - - describe "nested reply with ERB" do - it "should load all the replies" do - first_name = "Presley" - - service_reply = Stealth::ServiceReply.new( - recipient_id: recipient_id, - yaml_reply: yaml_reply, - context: binding, - preprocessor: :erb - ) - - expect(service_reply.replies.size).to eq 5 - end - - it "should load all replies as Stealth::Reply objects" do - first_name = "Presley" - - service_reply = Stealth::ServiceReply.new( - recipient_id: recipient_id, - yaml_reply: yaml_reply, - context: binding, - preprocessor: :erb - ) - - expect(service_reply.replies).to all(be_an(Stealth::Reply)) - end - - it "should replace the ERB tag" do - first_name = "Presley" - - service_reply = Stealth::ServiceReply.new( - recipient_id: recipient_id, - yaml_reply: yaml_reply, - context: binding, - preprocessor: :erb - ) - - phrase_in_reply = service_reply.replies.first['text'] - expect(phrase_in_reply).to eq "Hi, Presley. Welcome to Stealth bot..." - end - - it "should raise Stealth::Errors::UndefinedVariable when local variable is not available" do - expect { - service_reply = Stealth::ServiceReply.new( - recipient_id: recipient_id, - yaml_reply: yaml_reply, - context: binding, - preprocessor: :erb - ) - }.to raise_error(Stealth::Errors::UndefinedVariable) - end - end - - describe "processing a reply without a preprocessor specified" do - it "should not replace the ERB tag when no preprocessor is specified" do - first_name = "Gisele" - - service_reply = Stealth::ServiceReply.new( - recipient_id: recipient_id, - yaml_reply: yaml_reply, - context: binding - ) - - phrase_in_reply = service_reply.replies.first['text'] - expect(phrase_in_reply).to eq "Hi, <%= first_name %>. Welcome to Stealth bot..." - end - - it "should not replace the ERB tag when :none is specified as the preprocessor" do - first_name = "Gisele" - - service_reply = Stealth::ServiceReply.new( - recipient_id: recipient_id, - yaml_reply: yaml_reply, - context: binding, - preprocessor: :none - ) - - phrase_in_reply = service_reply.replies.first['text'] - expect(phrase_in_reply).to eq "Hi, <%= first_name %>. Welcome to Stealth bot..." - end - end - -end diff --git a/spec/session_spec.rb b/spec/session_spec.rb deleted file mode 100644 index 5355aa22..00000000 --- a/spec/session_spec.rb +++ /dev/null @@ -1,366 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -class FlowMap - include Stealth::Flow - - flow :new_todo do - state :new - state :get_due_date - state :created, fails_to: :new - state :error - end - - flow :marco do - state :polo - end -end - -describe "Stealth::Session" do - let(:id) { '0xDEADBEEF' } - - it "should raise an error if $redis is not set" do - $redis = nil - - expect { - Stealth::Session.new(id: id) - }.to raise_error(Stealth::Errors::RedisNotConfigured) - - $redis = MockRedis.new - end - - describe "without a session" do - let(:session) { Stealth::Session.new(id: id) } - - it "should have nil flow and state" do - expect(session.flow).to be_nil - expect(session.state).to be_nil - end - - it "should have nil flow_string and state_string" do - expect(session.flow_string).to be_nil - expect(session.state_string).to be_nil - end - - it "should respond to present? and blank?" do - expect(session.present?).to be false - expect(session.blank?).to be true - end - end - - describe "with a session" do - let(:session) do - session = Stealth::Session.new(id: id) - session.set_session(new_flow: 'marco', new_state: 'polo') - session - end - - it "should return the FlowMap" do - expect(session.flow).to be_a(FlowMap) - end - - it "should return the state" do - expect(session.state).to be_a(Stealth::Flow::State) - expect(session.state).to eq :polo - end - - it "should return the flow_string" do - expect(session.flow_string).to eq "marco" - end - - it "should return the state_string" do - expect(session.state_string).to eq "polo" - end - - it "should respond to present? and blank?" do - expect(session.present?).to be true - expect(session.blank?).to be false - end - end - - describe "incrementing and decrementing" do - let(:session) { Stealth::Session.new(id: id) } - - it "should increment the state" do - session.set_session(new_flow: 'new_todo', new_state: 'get_due_date') - new_session = session + 1.state - expect(new_session.state_string).to eq('created') - end - - it "should decrement the state" do - session.set_session(new_flow: 'new_todo', new_state: 'error') - new_session = session - 2.states - expect(new_session.state_string).to eq('get_due_date') - end - - it "should return the first state if the decrement is out of bounds" do - session.set_session(new_flow: 'new_todo', new_state: 'get_due_date') - new_session = session - 5.states - expect(new_session.state_string).to eq('new') - end - - it "should return the last state if the increment is out of bounds" do - session.set_session(new_flow: 'new_todo', new_state: 'created') - new_session = session + 5.states - expect(new_session.state_string).to eq('error') - end - end - - describe "==" do - let(:session) { - _session = Stealth::Session.new(id: id, type: :primary) - _session.set_session(new_flow: 'hello', new_state: 'say_hola') - _session - } - - it "should return false if the session ids do not" do - other_session = Stealth::Session.new(id: 'xyz123', type: :primary) - other_session.set_session(new_flow: 'hello', new_state: 'say_hola') - expect(session == other_session).to be false - end - - it "should return false if the states do not" do - other_session = Stealth::Session.new(id: id, type: :primary) - other_session.set_session(new_flow: 'hello', new_state: 'say_hola2') - expect(session == other_session).to be false - end - - it "should return false if flows do not match" do - other_session = Stealth::Session.new(id: id, type: :primary) - other_session.set_session(new_flow: 'hello2', new_state: 'say_hola') - expect(session == other_session).to be false - end - - it 'should return false if the session type does not match' do - other_session = Stealth::Session.new(id: id, type: :back_to) - other_session.set_session(new_flow: 'hello', new_state: 'say_hola') - expect(session == other_session).to be false - end - - it 'should return true if the session id, flow, state, and session types match' do - other_session = Stealth::Session.new(id: id, type: :primary) - other_session.set_session(new_flow: 'hello', new_state: 'say_hola') - expect(session == other_session).to be true - end - end - - describe "self.is_a_session_string?" do - it "should return false for state strings" do - session_string = 'say_hello' - expect(Stealth::Session.is_a_session_string?(session_string)).to be false - end - - it "should return false for an incomplete session string" do - session_string = 'hello->' - expect(Stealth::Session.is_a_session_string?(session_string)).to be false - end - - it "should return true for a complete session string" do - session_string = 'hello->say_hello' - expect(Stealth::Session.is_a_session_string?(session_string)).to be true - end - end - - describe "self.canonical_session_slug" do - it "should generate a canonical session slug given a flow and state as symbols" do - expect( - Stealth::Session.canonical_session_slug(flow: :hello, state: :say_hello) - ).to eq 'hello->say_hello' - end - - it "should generate a canonical session slug given a flow and state as strings" do - expect( - Stealth::Session.canonical_session_slug(flow: 'hello', state: 'say_hello') - ).to eq 'hello->say_hello' - end - end - - describe "self.flow_and_state_from_session_slug" do - it "should return the flow and string as a hash with symbolized keys" do - slug = 'hello->say_hello' - expect( - Stealth::Session.flow_and_state_from_session_slug(slug: slug) - ).to eq({ flow: 'hello', state: 'say_hello' }) - end - - it "should not raise if slug is nil" do - slug = nil - expect( - Stealth::Session.flow_and_state_from_session_slug(slug: slug) - ).to eq({ flow: nil, state: nil }) - end - end - - describe "setting sessions" do - let(:session) { Stealth::Session.new(id: id) } - let(:previous_session) { Stealth::Session.new(id: id, type: :previous) } - let(:back_to_session) { Stealth::Session.new(id: id, type: :back_to) } - - before(:each) do - $redis.del(id) - $redis.del([id, 'previous'].join('-')) - $redis.del([id, 'back_to'].join('-')) - end - - it "should store the new session" do - session.set_session(new_flow: 'marco', new_state: 'polo') - expect($redis.get(id)).to eq 'marco->polo' - end - - it "should store the current_session to previous_session" do - $redis.set(id, 'new_todo->new') - $redis.set([id, 'previous'].join('-'), 'new_todo->error') - session.set_session(new_flow: 'marco', new_state: 'polo') - expect(previous_session.get_session).to eq 'new_todo->new' - end - - it "should not update previous_session if it matches current_session" do - $redis.set(id, 'marco->polo') - $redis.set([id, 'previous'].join('-'), 'new_todo->new') - session.set_session(new_flow: 'marco', new_state: 'polo') - expect(previous_session.get_session).to eq 'new_todo->new' - end - - it "should set an expiration for current_session if session_ttl is specified" do - Stealth.config.session_ttl = 500 - session.set_session(new_flow: 'marco', new_state: 'polo') - expect($redis.ttl(id)).to be > 0 - Stealth.config.session_ttl = 0 - end - - it "should set an expiration for previous_session if session_ttl is specified" do - Stealth.config.session_ttl = 500 - $redis.set(id, 'new_todo->new') - session.set_session(new_flow: 'marco', new_state: 'polo') - expect($redis.ttl([id, 'previous'].join('-'))).to be > 0 - Stealth.config.session_ttl = 0 - end - - it "should NOT set an expiration if session_ttl is not specified" do - Stealth.config.session_ttl = 0 - session.set_session(new_flow: 'new_todo', new_state: 'get_due_date') - expect($redis.ttl(id)).to eq -1 # Does not expire - end - - it "should set the session for back_to" do - back_to_session.set_session(new_flow: 'marco', new_state: 'polo') - expect($redis.get([id, 'back_to'].join('-'))).to eq 'marco->polo' - end - - it "should set an expiration for back_to if session_ttl is specified" do - Stealth.config.session_ttl = 500 - back_to_session.set_session(new_flow: 'marco', new_state: 'polo') - expect($redis.ttl([id, 'back_to'].join('-'))).to be > 0 - Stealth.config.session_ttl = 0 - end - end - - describe "getting sessions" do - let(:session) { Stealth::Session.new(id: id) } - let(:previous_session) { Stealth::Session.new(id: id, type: :previous) } - let(:back_to_session) { Stealth::Session.new(id: id, type: :back_to) } - - before(:each) do - $redis.del(id) - $redis.del([id, 'previous'].join('-')) - $redis.del([id, 'back_to'].join('-')) - end - - it "should return the stored current_session" do - session.set_session(new_flow: 'marco', new_state: 'polo') - expect(session.get_session).to eq 'marco->polo' - end - - it "should return the stored previous_session if previous is requested" do - $redis.set(id, 'new_todo->new') - session.set_session(new_flow: 'marco', new_state: 'polo') - expect(previous_session.get_session).to eq 'new_todo->new' - end - - it "should update the expiration of current_session if session_ttl is set" do - Stealth.config.session_ttl = 50 - session.set_session(new_flow: 'marco', new_state: 'polo') - expect($redis.ttl(id)).to be_between(0, 50).inclusive - - Stealth.config.session_ttl = 500 - session.session = nil # reset memoization - session.get_session - expect($redis.ttl(id)).to be > 100 - - Stealth.config.session_ttl = 0 - end - - it "should return the stored back_to_session" do - back_to_session.set_session(new_flow: 'marco', new_state: 'polo') - expect(back_to_session.get_session).to eq 'marco->polo' - end - - it "should update the expiration of back_to_session if session_ttl is set" do - Stealth.config.session_ttl = 50 - back_to_session.set_session(new_flow: 'marco', new_state: 'polo') - expect($redis.ttl([id, 'back_to'].join('-'))).to be_between(0, 50).inclusive - - Stealth.config.session_ttl = 500 - back_to_session.session = nil # reset memoization - back_to_session.get_session - expect($redis.ttl([id, 'back_to'].join('-'))).to be > 100 - - Stealth.config.session_ttl = 0 - end - end - - describe "clearing sessions" do - let(:session) { Stealth::Session.new(id: id) } - let(:previous_session) { Stealth::Session.new(id: id, type: :previous) } - let(:back_to_session) { Stealth::Session.new(id: id, type: :back_to) } - - before(:each) do - session.send(:persist_key, key: session.session_key, value: '1') - previous_session.send(:persist_key, key: previous_session.session_key, value: '1') - back_to_session.send(:persist_key, key: back_to_session.session_key, value: '1') - end - - it "should remove a default session from Redis" do - expect($redis.get(session.session_key)).to eq '1' - session.clear_session - expect($redis.get(session.session_key)).to be_nil - end - - it "should remove a previous session from Redis" do - expect($redis.get(previous_session.session_key)).to eq '1' - previous_session.clear_session - expect($redis.get(previous_session.session_key)).to be_nil - end - - it "should remove a back_to session from Redis" do - expect($redis.get(back_to_session.session_key)).to eq '1' - back_to_session.clear_session - expect($redis.get(back_to_session.session_key)).to be_nil - end - end - - describe "self.slugify" do - it "should return a session slug given a flow and state" do - expect(Stealth::Session.slugify(flow: 'hello', state: 'world')).to eq 'hello->world' - end - - it "should raise an ArgumentError if flow is blank" do - expect { - Stealth::Session.slugify(flow: '', state: 'world') - }.to raise_error(ArgumentError) - end - - it "should raise an ArgumentError if state is blank" do - expect { - Stealth::Session.slugify(flow: 'hello', state: nil) - }.to raise_error(ArgumentError) - end - - it "should raise an ArgumentError if flow and state are blank" do - expect { - Stealth::Session.slugify(flow: nil, state: nil) - }.to raise_error(ArgumentError) - end - end -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb deleted file mode 100644 index 83eef3cc..00000000 --- a/spec/spec_helper.rb +++ /dev/null @@ -1,42 +0,0 @@ -# coding: utf-8 -# frozen_string_literal: true - -$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) -$LOAD_PATH.unshift(File.dirname(__FILE__)) -require 'rspec' - -require 'stealth' -require 'sidekiq/testing' -require 'mock_redis' - -# Requires supporting files with custom matchers and macros, etc, -# in ./support/ and its subdirectories. -Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } - -$redis = MockRedis.new -$services_yml = File.read(File.join(File.dirname(__FILE__), 'support', 'services.yml')) - -Sidekiq.logger.level = Logger::ERROR - -RSpec.configure do |config| - ENV['STEALTH_ENV'] = 'test' - - config.before(:each) do |example| - Sidekiq::Testing.fake! - - Stealth.load_services_config!($services_yml) - end - - config.expect_with :rspec do |expectations| - # This option will default to `true` in RSpec 4. It makes the `description` - # and `failure_message` of custom matchers include text for helper methods - # defined using `chain`, e.g.: - # be_bigger_than(2).and_smaller_than(4).description - # # => "be bigger than 2 and smaller than 4" - # ...rather than: - # # => "be bigger than 2" - expectations.include_chain_clauses_in_custom_matcher_descriptions = true - - expectations.on_potential_false_positives = :nothing - end -end diff --git a/spec/support/alternate_helpers/foo_helper.rb b/spec/support/alternate_helpers/foo_helper.rb deleted file mode 100644 index ed57b50f..00000000 --- a/spec/support/alternate_helpers/foo_helper.rb +++ /dev/null @@ -1,5 +0,0 @@ -module FooHelper - def foo - - end -end diff --git a/spec/support/controllers/vaders_controller.rb b/spec/support/controllers/vaders_controller.rb deleted file mode 100644 index 091238f8..00000000 --- a/spec/support/controllers/vaders_controller.rb +++ /dev/null @@ -1,24 +0,0 @@ -class VadersController < Stealth::Controller - def my_action - raise "oops" - end - - def my_action2 - - end - - def my_action3 - do_nothing - end - - def action_with_unrecognized_msg - handle_message( - 'hello' => proc { puts "Hello world!" }, - 'bye' => proc { puts "Goodbye world!" } - ) - end - - def action_with_unrecognized_match - match = get_match(['hello', 'bye']) - end -end diff --git a/spec/support/helpers/fun/games_helper.rb b/spec/support/helpers/fun/games_helper.rb deleted file mode 100644 index ad02ea38..00000000 --- a/spec/support/helpers/fun/games_helper.rb +++ /dev/null @@ -1,7 +0,0 @@ -module Fun - module GamesHelper - def hello_world - - end - end -end diff --git a/spec/support/helpers/fun/pdf_helper.rb b/spec/support/helpers/fun/pdf_helper.rb deleted file mode 100644 index dafde600..00000000 --- a/spec/support/helpers/fun/pdf_helper.rb +++ /dev/null @@ -1,7 +0,0 @@ -module Fun - module PdfHelper - def generate_pdf_name - - end - end -end diff --git a/spec/support/helpers/standalone_helper.rb b/spec/support/helpers/standalone_helper.rb deleted file mode 100644 index a5330541..00000000 --- a/spec/support/helpers/standalone_helper.rb +++ /dev/null @@ -1,5 +0,0 @@ -module StandaloneHelper - def baz - - end -end diff --git a/spec/support/helpers_typo/users_helper.rb b/spec/support/helpers_typo/users_helper.rb deleted file mode 100644 index 285a72dc..00000000 --- a/spec/support/helpers_typo/users_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module UsersHelpeR -end diff --git a/spec/support/nlp_clients/dialogflow.rb b/spec/support/nlp_clients/dialogflow.rb deleted file mode 100644 index 45dd6fd4..00000000 --- a/spec/support/nlp_clients/dialogflow.rb +++ /dev/null @@ -1,9 +0,0 @@ -module Stealth - module Nlp - module Dialogflow - class Client < Stealth::Nlp::Client - - end - end - end -end diff --git a/spec/support/nlp_clients/luis.rb b/spec/support/nlp_clients/luis.rb deleted file mode 100644 index d21355fc..00000000 --- a/spec/support/nlp_clients/luis.rb +++ /dev/null @@ -1,9 +0,0 @@ -module Stealth - module Nlp - module Luis - class Client < Stealth::Nlp::Client - - end - end - end -end diff --git a/spec/support/nlp_results/luis_result.rb b/spec/support/nlp_results/luis_result.rb deleted file mode 100644 index 4040355d..00000000 --- a/spec/support/nlp_results/luis_result.rb +++ /dev/null @@ -1,163 +0,0 @@ -module TestNlpResult - class Luis < Stealth::Nlp::Result - - ENTITY_MAP = { - 'money' => :currency, 'number' => :number, 'email' => :email, - 'percentage' => :percentage, 'Calendar.Duration' => :duration, - 'geographyV2' => :geo, 'age' => :age, 'phonenumber' => :phone, - 'ordinalV2' => :ordinal, 'url' => :url, 'dimension' => :dimension, - 'temperature' => :temp, 'keyPhrase' => :key_phrase, 'name' => :name, - 'datetimeV2' => :datetime - } - - attr_reader :result, :intent - - def initialize(intent:, entity: :single_number_entity) - @result = test_responses[entity] - @intent = intent - end - - def parsed_result - @result - end - - def intent_score - rand - end - - def raw_entities - @result.dig('prediction', 'entities') - end - - def entities - return {} if raw_entities.blank? - _entities = {} - - raw_entities.each do |type, values| - if ENTITY_MAP[type] - _entities[ENTITY_MAP[type]] = values - else - # A custom entity - _entities[type.to_sym] = values - end - end - - _entities - end - - def sentiment - %i(positive neutral negative).sample - end - - def sentiment_score - rand - end - - def present? - parsed_result.present? - end - - private - - def test_responses - { - single_number_entity: { - "query" => "My score was 78", - "prediction" => { - "topIntent" => "None", - "intents" => { - "None" => { - "score" => 0.170594558 - } - }, - "entities" => { - "keyPhrase" => [ - "score" - ], - "number" => [ - 78 - ] - }, - "sentiment" => { - "label" => "neutral", - "score" => 0.5 - } - } - }, - - double_number_entity: { - "query" => "Their scores were 89 and 97, respectively", - "prediction" => { - "topIntent" => "None", - "intents" => { - "None" => { - "score" => 0.5280223 - } - }, - "entities" => { - "keyPhrase" => [ - "scores" - ], - "number" => [ - 89, - 97 - ] - }, - "sentiment" => { - "label" => "negative", - "score" => 0.309174955 - } - } - }, - - triple_number_entity: { - "query" => "Their scores were 89, 65, and 97, respectively", - "prediction" => { - "topIntent" => "None", - "intents" => { - "None" => { - "score" => 0.6703843 - } - }, - "entities" => { - "keyPhrase" => [ - "scores" - ], - "number" => [ - 89, - 65, - 97 - ] - }, - "sentiment" => { - "label" => "negative", - "score" => 0.309174955 - } - } - }, - - custom_entity: { - "query" => "call me right away", - "prediction" => { - "topIntent" => "now", - "intents" => { - "now" => { - "score" => 0.781227 - } - }, - "entities" => { - "asap" => [ - ["right away"] - ] - }, - "sentiment" => { - "label" => "neutral", - "score" => 0.5 - } - } - } - } - end - - end -end diff --git a/spec/support/sample_messages.rb b/spec/support/sample_messages.rb deleted file mode 100644 index 9316a92c..00000000 --- a/spec/support/sample_messages.rb +++ /dev/null @@ -1,66 +0,0 @@ -# coding: utf-8 -# frozen_string_literal: true - -class SampleMessage - - def initialize(service:) - @service = service - @base_message = Stealth::ServiceMessage.new(service: @service) - @base_message.sender_id = sender_id - @base_message.timestamp = timestamp - @base_message - end - - def message_with_text - @base_message.message = message - @base_message - end - - def message_with_payload - @base_message.payload = payload - @base_message - end - - def message_with_location - @base_message.location = location - @base_message - end - - def message_with_attachments - @base_message.attachments = attachments - @base_message - end - - def sender_id - if @service == 'twilio' - '+15554561212' - else - "8b3e0a3c-62f1-401e-8b0f-615c9d256b1f" - end - end - - def timestamp - Time.now - end - - def message - "Hello World!" - end - - def payload - "some_payload" - end - - def location - { lat: '42.323724' , lng: '-83.047543' } - end - - def attachments - [ { type: 'image', url: 'https://domain.none/image.jpg' } ] - end - - def referral - {} - end - -end diff --git a/spec/support/services.yml b/spec/support/services.yml deleted file mode 100644 index ae138642..00000000 --- a/spec/support/services.yml +++ /dev/null @@ -1,31 +0,0 @@ -default: &default - facebook: - verify_token: c68823f4-7259-4600-b9f0-382a67260757 - challenge: pOmQ7iq4ZA1hK6TbO7yrVZCmygVjfIiEIYaIZAlEveOAzY4UKb - page_access_token: EAADhbJruBbMBAKY9bnesHh9eM09ZAHjGsCQtNdvuZClZCFtyIXdFdIQI2mV0PJM910Qyn3lNNhZBPZB54zLRhDNmeIkDz9myS25CTy0kFxHjQCXJxz5oeZCD60VWdAZAFxbeDKvF8eF28qDHAI4wkGc3jvVhjFISKmmFRRM6goUeAZDZD - setup: - greeting: # Greetings are broken up by locale - - locale: default - text: "Welcome to the Stealth bot 🤖" - persistent_menu: - - type: payload - text: Main Menu - payload: main_menu - - type: url - text: Visit our website - url: https://example.com - - type: call - text: Call us - payload: "+17345551234" - twilio_sms: - account_sid: BC6f4bd46307054c84fdff70badcd9ef5d - auth_token: 4af73d27d92cff6391611a9c976725cc - -production: - <<: *default - -development: - <<: *default - -test: - <<: *default diff --git a/spec/support/services_with_erb.yml b/spec/support/services_with_erb.yml deleted file mode 100644 index 2999bc6d..00000000 --- a/spec/support/services_with_erb.yml +++ /dev/null @@ -1,31 +0,0 @@ -default: &default - facebook: - verify_token: <%= ENV['FACEBOOK_VERIFY_TOKEN'] %> - challenge: <%= ENV['FACEBOOK_CHALLENGE'] %> - page_access_token: <%= ENV['FACEBOOK_PAGE_ACCESS_TOKEN'] %> - setup: - greeting: # Greetings are broken up by locale - - locale: default - text: "Welcome to the Stealth bot 🤖" - persistent_menu: - - type: payload - text: Main Menu - payload: main_menu - - type: url - text: Visit our website - url: https://example.com - - type: call - text: Call us - payload: "+17345551234" - twilio_sms: - account_sid: <%= ENV['TWILIO_ACCOUNT_SID'] %> - auth_token: <%= ENV['TWILIO_AUTH_TOKEN'] %> - -production: - <<: *default - -development: - <<: *default - -test: - <<: *default diff --git a/spec/version_spec.rb b/spec/version_spec.rb deleted file mode 100644 index d1df3f6f..00000000 --- a/spec/version_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe "Stealth::Version" do - - let(:version_in_file) { File.read(File.join(File.dirname(__FILE__), '..', 'VERSION')).strip } - - it "should return the current gem version" do - expect(Stealth::Version.version).to eq version_in_file - end - - it "should return the current gem version via a constant" do - expect(Stealth::VERSION).to eq version_in_file - end -end diff --git a/stealth.gemspec b/stealth.gemspec index 67427d1e..2b9daf91 100644 --- a/stealth.gemspec +++ b/stealth.gemspec @@ -1,6 +1,4 @@ -$LOAD_PATH.push File.expand_path('../lib', __FILE__) - -version = File.read(File.join(File.dirname(__FILE__), 'VERSION')).strip +require_relative "lib/stealth/version" Gem::Specification.new do |s| s.name = 'stealth' @@ -8,25 +6,29 @@ Gem::Specification.new do |s| s.description = 'Ruby framework for building conversational bots.' s.homepage = 'https://github.com/hellostealth/stealth' s.licenses = ['MIT'] - s.version = version - s.authors = ['Mauricio Gomes', 'Matthew Black'] - s.email = 'mauricio@edge14.com' + s.version = '3.0.0.alpha1' + s.authors = ['Matthew Black'] + s.email = 'm@hiremav.com' - s.add_dependency 'activesupport', '~> 7.0' - s.add_dependency 'multi_json', '~> 1.12' - s.add_dependency 'puma', '~> 6.0' s.add_dependency 'redis', '~> 5.0' s.add_dependency 'sidekiq', '~> 7.0' - s.add_dependency 'sinatra', '>= 2', '< 4' - s.add_dependency 'thor', '~> 1.0' - s.add_dependency 'zeitwerk', '~> 2.6' + s.add_dependency 'spectre_ai', '~> 1.1.2' s.add_development_dependency 'rspec', '~> 3.9' s.add_development_dependency 'rack-test', '~> 2.0' s.add_development_dependency 'mock_redis', '~> 0.22' - s.files = `git ls-files`.split("\n") - s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") - s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) } - s.require_paths = ['lib'] + # Prevent pushing this gem to RubyGems.org. To allow pushes either set the "allowed_push_host" + # to allow pushing to a single host or delete this section to allow pushing to any host. + # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'" + + # spec.metadata["homepage_uri"] = spec.homepage + # spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here." + # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here." + + s.files = Dir.chdir(File.expand_path(__dir__)) do + Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"] + end + + s.add_dependency "rails", ">= 7.1.3.4" end diff --git a/test/controllers/.keep b/test/controllers/.keep new file mode 100644 index 00000000..e69de29b diff --git a/test/dummy/Rakefile b/test/dummy/Rakefile new file mode 100644 index 00000000..9a5ea738 --- /dev/null +++ b/test/dummy/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative "config/application" + +Rails.application.load_tasks diff --git a/test/dummy/app/assets/config/manifest.js b/test/dummy/app/assets/config/manifest.js new file mode 100644 index 00000000..3d3a8c30 --- /dev/null +++ b/test/dummy/app/assets/config/manifest.js @@ -0,0 +1,3 @@ +//= link_tree ../images +//= link_directory ../stylesheets .css +//= link stealth_manifest.js diff --git a/test/dummy/app/assets/images/.keep b/test/dummy/app/assets/images/.keep new file mode 100644 index 00000000..e69de29b diff --git a/test/dummy/app/assets/stylesheets/application.css b/test/dummy/app/assets/stylesheets/application.css new file mode 100644 index 00000000..0ebd7fe8 --- /dev/null +++ b/test/dummy/app/assets/stylesheets/application.css @@ -0,0 +1,15 @@ +/* + * This is a manifest file that'll be compiled into application.css, which will include all the files + * listed below. + * + * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, + * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. + * + * You're free to add application-wide styles to this file and they'll appear at the bottom of the + * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS + * files in this directory. Styles in this file should be added after the last require_* statement. + * It is generally better to create a new file per style scope. + * + *= require_tree . + *= require_self + */ diff --git a/test/dummy/app/channels/application_cable/channel.rb b/test/dummy/app/channels/application_cable/channel.rb new file mode 100644 index 00000000..d6726972 --- /dev/null +++ b/test/dummy/app/channels/application_cable/channel.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Channel < ActionCable::Channel::Base + end +end diff --git a/test/dummy/app/channels/application_cable/connection.rb b/test/dummy/app/channels/application_cable/connection.rb new file mode 100644 index 00000000..0ff5442f --- /dev/null +++ b/test/dummy/app/channels/application_cable/connection.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Connection < ActionCable::Connection::Base + end +end diff --git a/test/dummy/app/controllers/application_controller.rb b/test/dummy/app/controllers/application_controller.rb new file mode 100644 index 00000000..09705d12 --- /dev/null +++ b/test/dummy/app/controllers/application_controller.rb @@ -0,0 +1,2 @@ +class ApplicationController < ActionController::Base +end diff --git a/test/dummy/app/controllers/concerns/.keep b/test/dummy/app/controllers/concerns/.keep new file mode 100644 index 00000000..e69de29b diff --git a/test/dummy/app/helpers/application_helper.rb b/test/dummy/app/helpers/application_helper.rb new file mode 100644 index 00000000..de6be794 --- /dev/null +++ b/test/dummy/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +module ApplicationHelper +end diff --git a/test/dummy/app/jobs/application_job.rb b/test/dummy/app/jobs/application_job.rb new file mode 100644 index 00000000..d394c3d1 --- /dev/null +++ b/test/dummy/app/jobs/application_job.rb @@ -0,0 +1,7 @@ +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/test/dummy/app/mailers/application_mailer.rb b/test/dummy/app/mailers/application_mailer.rb new file mode 100644 index 00000000..3c34c814 --- /dev/null +++ b/test/dummy/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +class ApplicationMailer < ActionMailer::Base + default from: "from@example.com" + layout "mailer" +end diff --git a/test/dummy/app/models/application_record.rb b/test/dummy/app/models/application_record.rb new file mode 100644 index 00000000..b63caeb8 --- /dev/null +++ b/test/dummy/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + primary_abstract_class +end diff --git a/test/dummy/app/models/concerns/.keep b/test/dummy/app/models/concerns/.keep new file mode 100644 index 00000000..e69de29b diff --git a/test/dummy/app/views/layouts/application.html.erb b/test/dummy/app/views/layouts/application.html.erb new file mode 100644 index 00000000..f72b4ef0 --- /dev/null +++ b/test/dummy/app/views/layouts/application.html.erb @@ -0,0 +1,15 @@ + + + + Dummy + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= stylesheet_link_tag "application" %> + + + + <%= yield %> + + diff --git a/test/dummy/app/views/layouts/mailer.html.erb b/test/dummy/app/views/layouts/mailer.html.erb new file mode 100644 index 00000000..3aac9002 --- /dev/null +++ b/test/dummy/app/views/layouts/mailer.html.erb @@ -0,0 +1,13 @@ + + + + + + + + + <%= yield %> + + diff --git a/test/dummy/app/views/layouts/mailer.text.erb b/test/dummy/app/views/layouts/mailer.text.erb new file mode 100644 index 00000000..37f0bddb --- /dev/null +++ b/test/dummy/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/test/dummy/bin/rails b/test/dummy/bin/rails new file mode 100755 index 00000000..efc03774 --- /dev/null +++ b/test/dummy/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +APP_PATH = File.expand_path("../config/application", __dir__) +require_relative "../config/boot" +require "rails/commands" diff --git a/test/dummy/bin/rake b/test/dummy/bin/rake new file mode 100755 index 00000000..4fbf10b9 --- /dev/null +++ b/test/dummy/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "rake" +Rake.application.run diff --git a/test/dummy/bin/setup b/test/dummy/bin/setup new file mode 100755 index 00000000..3cd5a9d7 --- /dev/null +++ b/test/dummy/bin/setup @@ -0,0 +1,33 @@ +#!/usr/bin/env ruby +require "fileutils" + +# path to your application root. +APP_ROOT = File.expand_path("..", __dir__) + +def system!(*args) + system(*args, exception: true) +end + +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. + # Add necessary setup steps to this file. + + puts "== Installing dependencies ==" + system! "gem install bundler --conservative" + system("bundle check") || system!("bundle install") + + # puts "\n== Copying sample files ==" + # unless File.exist?("config/database.yml") + # FileUtils.cp "config/database.yml.sample", "config/database.yml" + # end + + puts "\n== Preparing database ==" + system! "bin/rails db:prepare" + + puts "\n== Removing old logs and tempfiles ==" + system! "bin/rails log:clear tmp:clear" + + puts "\n== Restarting application server ==" + system! "bin/rails restart" +end diff --git a/test/dummy/config.ru b/test/dummy/config.ru new file mode 100644 index 00000000..4a3c09a6 --- /dev/null +++ b/test/dummy/config.ru @@ -0,0 +1,6 @@ +# This file is used by Rack-based servers to start the application. + +require_relative "config/environment" + +run Rails.application +Rails.application.load_server diff --git a/test/dummy/config/application.rb b/test/dummy/config/application.rb new file mode 100644 index 00000000..67cc1408 --- /dev/null +++ b/test/dummy/config/application.rb @@ -0,0 +1,29 @@ +require_relative "boot" + +require "rails/all" + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module Dummy + class Application < Rails::Application + config.load_defaults Rails::VERSION::STRING.to_f + + # For compatibility with applications that use this config + config.action_controller.include_all_helpers = false + + # Please, add to the `ignore` list any other `lib` subdirectories that do + # not contain `.rb` files, or that should not be reloaded or eager loaded. + # Common ones are `templates`, `generators`, or `middleware`, for example. + config.autoload_lib(ignore: %w(assets tasks)) + + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + # config.time_zone = "Central Time (US & Canada)" + # config.eager_load_paths << Rails.root.join("extras") + end +end diff --git a/test/dummy/config/boot.rb b/test/dummy/config/boot.rb new file mode 100644 index 00000000..116591a4 --- /dev/null +++ b/test/dummy/config/boot.rb @@ -0,0 +1,5 @@ +# Set up gems listed in the Gemfile. +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../Gemfile", __dir__) + +require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"]) +$LOAD_PATH.unshift File.expand_path("../../../lib", __dir__) diff --git a/test/dummy/config/cable.yml b/test/dummy/config/cable.yml new file mode 100644 index 00000000..98367f89 --- /dev/null +++ b/test/dummy/config/cable.yml @@ -0,0 +1,10 @@ +development: + adapter: async + +test: + adapter: test + +production: + adapter: redis + url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> + channel_prefix: dummy_production diff --git a/lib/stealth/generators/builder/config/database.yml b/test/dummy/config/database.yml similarity index 63% rename from lib/stealth/generators/builder/config/database.yml rename to test/dummy/config/database.yml index e5c7997c..796466ba 100644 --- a/lib/stealth/generators/builder/config/database.yml +++ b/test/dummy/config/database.yml @@ -1,25 +1,25 @@ -# SQLite version 3.x +# SQLite. Versions 3.8.0 and up are supported. # gem install sqlite3 # # Ensure the SQLite 3 gem is defined in your Gemfile -# gem 'sqlite3' +# gem "sqlite3" # default: &default adapter: sqlite3 - pool: <%= ENV.fetch("STEALTH_MAX_THREADS") { 5 } %> + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> timeout: 5000 development: <<: *default - database: db/development.sqlite3 + database: storage/development.sqlite3 # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: <<: *default - database: db/test.sqlite3 + database: storage/test.sqlite3 production: <<: *default - database: db/production.sqlite3 + database: storage/production.sqlite3 diff --git a/test/dummy/config/environment.rb b/test/dummy/config/environment.rb new file mode 100644 index 00000000..cac53157 --- /dev/null +++ b/test/dummy/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative "application" + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/test/dummy/config/environments/development.rb b/test/dummy/config/environments/development.rb new file mode 100644 index 00000000..2e7fb486 --- /dev/null +++ b/test/dummy/config/environments/development.rb @@ -0,0 +1,76 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # In the development environment your application's code is reloaded any time + # it changes. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.enable_reloading = true + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable server timing + config.server_timing = true + + # Enable/disable caching. By default caching is disabled. + # Run rails dev:cache to toggle caching. + if Rails.root.join("tmp/caching-dev.txt").exist? + config.action_controller.perform_caching = true + config.action_controller.enable_fragment_cache_logging = true + + config.cache_store = :memory_store + config.public_file_server.headers = { + "Cache-Control" => "public, max-age=#{2.days.to_i}" + } + else + config.action_controller.perform_caching = false + + config.cache_store = :null_store + end + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = false + + config.action_mailer.perform_caching = false + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + + # Highlight code that enqueued background job in logs. + config.active_job.verbose_enqueue_logs = true + + # Suppress logger output for asset requests. + config.assets.quiet = true + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Uncomment if you wish to allow Action Cable access from any origin. + # config.action_cable.disable_request_forgery_protection = true + + # Raise error when a before_action's only/except options reference missing actions + config.action_controller.raise_on_missing_callback_actions = true +end diff --git a/test/dummy/config/environments/production.rb b/test/dummy/config/environments/production.rb new file mode 100644 index 00000000..f25aaafe --- /dev/null +++ b/test/dummy/config/environments/production.rb @@ -0,0 +1,97 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.enable_reloading = false + + # Eager load code on boot. This eager loads most of Rails and + # your application in memory, allowing both threaded web servers + # and those relying on copy on write to perform better. + # Rake tasks automatically ignore this option for performance. + config.eager_load = true + + # Full error reports are disabled and caching is turned on. + config.consider_all_requests_local = false + config.action_controller.perform_caching = true + + # Ensures that a master key has been made available in ENV["RAILS_MASTER_KEY"], config/master.key, or an environment + # key such as config/credentials/production.key. This key is used to decrypt credentials (and other encrypted files). + # config.require_master_key = true + + # Disable serving static files from `public/`, relying on NGINX/Apache to do so instead. + # config.public_file_server.enabled = false + + # Compress CSS using a preprocessor. + # config.assets.css_compressor = :sass + + # Do not fall back to assets pipeline if a precompiled asset is missed. + config.assets.compile = false + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.asset_host = "http://assets.example.com" + + # Specifies the header that your server uses for sending files. + # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache + # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Mount Action Cable outside main process or domain. + # config.action_cable.mount_path = nil + # config.action_cable.url = "wss://example.com/cable" + # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] + + # Assume all access to the app is happening through a SSL-terminating reverse proxy. + # Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies. + # config.assume_ssl = true + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + config.force_ssl = true + + # Log to STDOUT by default + config.logger = ActiveSupport::Logger.new(STDOUT) + .tap { |logger| logger.formatter = ::Logger::Formatter.new } + .then { |logger| ActiveSupport::TaggedLogging.new(logger) } + + # Prepend all log lines with the following tags. + config.log_tags = [ :request_id ] + + # "info" includes generic and useful information about system operation, but avoids logging too much + # information to avoid inadvertent exposure of personally identifiable information (PII). If you + # want to log everything, set the level to "debug". + config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") + + # Use a different cache store in production. + # config.cache_store = :mem_cache_store + + # Use a real queuing backend for Active Job (and separate queues per environment). + # config.active_job.queue_adapter = :resque + # config.active_job.queue_name_prefix = "dummy_production" + + config.action_mailer.perform_caching = false + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Don't log any deprecations. + config.active_support.report_deprecations = false + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false + + # Enable DNS rebinding protection and other `Host` header attacks. + # config.hosts = [ + # "example.com", # Allow requests from example.com + # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` + # ] + # Skip DNS rebinding protection for the default health check endpoint. + # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } +end diff --git a/test/dummy/config/environments/test.rb b/test/dummy/config/environments/test.rb new file mode 100644 index 00000000..adbb4a6f --- /dev/null +++ b/test/dummy/config/environments/test.rb @@ -0,0 +1,64 @@ +require "active_support/core_ext/integer/time" + +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # While tests run files are not watched, reloading is not necessary. + config.enable_reloading = false + + # Eager loading loads your entire application. When running a single test locally, + # this is usually not necessary, and can slow down your test suite. However, it's + # recommended that you enable it in continuous integration systems to ensure eager + # loading is working properly before deploying your code. + config.eager_load = ENV["CI"].present? + + # Configure public file server for tests with Cache-Control for performance. + config.public_file_server.enabled = true + config.public_file_server.headers = { + "Cache-Control" => "public, max-age=#{1.hour.to_i}" + } + + # Show full error reports and disable caching. + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + config.cache_store = :null_store + + # Render exception templates for rescuable exceptions and raise for other exceptions. + config.action_dispatch.show_exceptions = :rescuable + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + # Store uploaded files on the local file system in a temporary directory. + config.active_storage.service = :test + + config.action_mailer.perform_caching = false + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Raise error when a before_action's only/except options reference missing actions + config.action_controller.raise_on_missing_callback_actions = true +end diff --git a/test/dummy/config/initializers/assets.rb b/test/dummy/config/initializers/assets.rb new file mode 100644 index 00000000..2eeef966 --- /dev/null +++ b/test/dummy/config/initializers/assets.rb @@ -0,0 +1,12 @@ +# Be sure to restart your server when you modify this file. + +# Version of your assets, change this if you want to expire all your assets. +Rails.application.config.assets.version = "1.0" + +# Add additional assets to the asset load path. +# Rails.application.config.assets.paths << Emoji.images_path + +# Precompile additional assets. +# application.js, application.css, and all non-JS/CSS in the app/assets +# folder are already added. +# Rails.application.config.assets.precompile += %w( admin.js admin.css ) diff --git a/test/dummy/config/initializers/content_security_policy.rb b/test/dummy/config/initializers/content_security_policy.rb new file mode 100644 index 00000000..b3076b38 --- /dev/null +++ b/test/dummy/config/initializers/content_security_policy.rb @@ -0,0 +1,25 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide content security policy. +# See the Securing Rails Applications Guide for more information: +# https://guides.rubyonrails.org/security.html#content-security-policy-header + +# Rails.application.configure do +# config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end +# +# # Generate session nonces for permitted importmap, inline scripts, and inline styles. +# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } +# config.content_security_policy_nonce_directives = %w(script-src style-src) +# +# # Report violations without enforcing the policy. +# # config.content_security_policy_report_only = true +# end diff --git a/test/dummy/config/initializers/filter_parameter_logging.rb b/test/dummy/config/initializers/filter_parameter_logging.rb new file mode 100644 index 00000000..c2d89e28 --- /dev/null +++ b/test/dummy/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. +# Use this to limit dissemination of sensitive information. +# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. +Rails.application.config.filter_parameters += [ + :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn +] diff --git a/lib/stealth/generators/builder/config/initializers/inflections.rb b/test/dummy/config/initializers/inflections.rb similarity index 77% rename from lib/stealth/generators/builder/config/initializers/inflections.rb rename to test/dummy/config/initializers/inflections.rb index ac033bf9..3860f659 100644 --- a/lib/stealth/generators/builder/config/initializers/inflections.rb +++ b/test/dummy/config/initializers/inflections.rb @@ -4,13 +4,13 @@ # are locale specific, and you may define rules for as many different # locales as you wish. All of these examples are active by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| -# inflect.plural /^(ox)$/i, '\1en' -# inflect.singular /^(ox)en/i, '\1' -# inflect.irregular 'person', 'people' +# inflect.plural /^(ox)$/i, "\\1en" +# inflect.singular /^(ox)en/i, "\\1" +# inflect.irregular "person", "people" # inflect.uncountable %w( fish sheep ) # end # These inflection rules are supported but not enabled by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| -# inflect.acronym 'RESTful' +# inflect.acronym "RESTful" # end diff --git a/test/dummy/config/initializers/permissions_policy.rb b/test/dummy/config/initializers/permissions_policy.rb new file mode 100644 index 00000000..7db3b957 --- /dev/null +++ b/test/dummy/config/initializers/permissions_policy.rb @@ -0,0 +1,13 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide HTTP permissions policy. For further +# information see: https://developers.google.com/web/updates/2018/06/feature-policy + +# Rails.application.config.permissions_policy do |policy| +# policy.camera :none +# policy.gyroscope :none +# policy.microphone :none +# policy.usb :none +# policy.fullscreen :self +# policy.payment :self, "https://secure.example.com" +# end diff --git a/test/dummy/config/locales/en.yml b/test/dummy/config/locales/en.yml new file mode 100644 index 00000000..6c349ae5 --- /dev/null +++ b/test/dummy/config/locales/en.yml @@ -0,0 +1,31 @@ +# Files in the config/locales directory are used for internationalization and +# are automatically loaded by Rails. If you want to use locales other than +# English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t "hello" +# +# In views, this is aliased to just `t`: +# +# <%= t("hello") %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# To learn more about the API, please read the Rails Internationalization guide +# at https://guides.rubyonrails.org/i18n.html. +# +# Be aware that YAML interprets the following case-insensitive strings as +# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings +# must be quoted to be interpreted as strings. For example: +# +# en: +# "yes": yup +# enabled: "ON" + +en: + hello: "Hello world" diff --git a/test/dummy/config/puma.rb b/test/dummy/config/puma.rb new file mode 100644 index 00000000..afa809b4 --- /dev/null +++ b/test/dummy/config/puma.rb @@ -0,0 +1,35 @@ +# This configuration file will be evaluated by Puma. The top-level methods that +# are invoked here are part of Puma's configuration DSL. For more information +# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. + +# Puma can serve each request in a thread from an internal thread pool. +# The `threads` method setting takes two numbers: a minimum and maximum. +# Any libraries that use thread pools should be configured to match +# the maximum value specified for Puma. Default is set to 5 threads for minimum +# and maximum; this matches the default thread size of Active Record. +max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } +min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } +threads min_threads_count, max_threads_count + +# Specifies that the worker count should equal the number of processors in production. +if ENV["RAILS_ENV"] == "production" + require "concurrent-ruby" + worker_count = Integer(ENV.fetch("WEB_CONCURRENCY") { Concurrent.physical_processor_count }) + workers worker_count if worker_count > 1 +end + +# Specifies the `worker_timeout` threshold that Puma will use to wait before +# terminating a worker in development environments. +worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +port ENV.fetch("PORT") { 3000 } + +# Specifies the `environment` that Puma will run in. +environment ENV.fetch("RAILS_ENV") { "development" } + +# Specifies the `pidfile` that Puma will use. +pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } + +# Allow puma to be restarted by `bin/rails restart` command. +plugin :tmp_restart diff --git a/test/dummy/config/routes.rb b/test/dummy/config/routes.rb new file mode 100644 index 00000000..b58be6f8 --- /dev/null +++ b/test/dummy/config/routes.rb @@ -0,0 +1,3 @@ +Rails.application.routes.draw do + mount Stealth::Engine => "/stealth" +end diff --git a/test/dummy/config/storage.yml b/test/dummy/config/storage.yml new file mode 100644 index 00000000..4942ab66 --- /dev/null +++ b/test/dummy/config/storage.yml @@ -0,0 +1,34 @@ +test: + service: Disk + root: <%= Rails.root.join("tmp/storage") %> + +local: + service: Disk + root: <%= Rails.root.join("storage") %> + +# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) +# amazon: +# service: S3 +# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> +# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> +# region: us-east-1 +# bucket: your_own_bucket-<%= Rails.env %> + +# Remember not to checkin your GCS keyfile to a repository +# google: +# service: GCS +# project: your_project +# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> +# bucket: your_own_bucket-<%= Rails.env %> + +# Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) +# microsoft: +# service: AzureStorage +# storage_account_name: your_account_name +# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> +# container: your_container_name-<%= Rails.env %> + +# mirror: +# service: Mirror +# primary: local +# mirrors: [ amazon, google, microsoft ] diff --git a/test/dummy/lib/assets/.keep b/test/dummy/lib/assets/.keep new file mode 100644 index 00000000..e69de29b diff --git a/test/dummy/log/.keep b/test/dummy/log/.keep new file mode 100644 index 00000000..e69de29b diff --git a/test/dummy/public/404.html b/test/dummy/public/404.html new file mode 100644 index 00000000..2be3af26 --- /dev/null +++ b/test/dummy/public/404.html @@ -0,0 +1,67 @@ + + + + The page you were looking for doesn't exist (404) + + + + + + +
+
+

The page you were looking for doesn't exist.

+

You may have mistyped the address or the page may have moved.

+
+

If you are the application owner check the logs for more information.

+
+ + diff --git a/test/dummy/public/422.html b/test/dummy/public/422.html new file mode 100644 index 00000000..c08eac0d --- /dev/null +++ b/test/dummy/public/422.html @@ -0,0 +1,67 @@ + + + + The change you wanted was rejected (422) + + + + + + +
+
+

The change you wanted was rejected.

+

Maybe you tried to change something you didn't have access to.

+
+

If you are the application owner check the logs for more information.

+
+ + diff --git a/test/dummy/public/500.html b/test/dummy/public/500.html new file mode 100644 index 00000000..78a030af --- /dev/null +++ b/test/dummy/public/500.html @@ -0,0 +1,66 @@ + + + + We're sorry, but something went wrong (500) + + + + + + +
+
+

We're sorry, but something went wrong.

+
+

If you are the application owner check the logs for more information.

+
+ + diff --git a/test/dummy/public/apple-touch-icon-precomposed.png b/test/dummy/public/apple-touch-icon-precomposed.png new file mode 100644 index 00000000..e69de29b diff --git a/test/dummy/public/apple-touch-icon.png b/test/dummy/public/apple-touch-icon.png new file mode 100644 index 00000000..e69de29b diff --git a/test/dummy/public/favicon.ico b/test/dummy/public/favicon.ico new file mode 100644 index 00000000..e69de29b diff --git a/test/dummy/storage/.keep b/test/dummy/storage/.keep new file mode 100644 index 00000000..e69de29b diff --git a/test/dummy/tmp/.keep b/test/dummy/tmp/.keep new file mode 100644 index 00000000..e69de29b diff --git a/test/dummy/tmp/pids/.keep b/test/dummy/tmp/pids/.keep new file mode 100644 index 00000000..e69de29b diff --git a/test/dummy/tmp/storage/.keep b/test/dummy/tmp/storage/.keep new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/files/.keep b/test/fixtures/files/.keep new file mode 100644 index 00000000..e69de29b diff --git a/test/helpers/.keep b/test/helpers/.keep new file mode 100644 index 00000000..e69de29b diff --git a/test/integration/.keep b/test/integration/.keep new file mode 100644 index 00000000..e69de29b diff --git a/test/integration/navigation_test.rb b/test/integration/navigation_test.rb new file mode 100644 index 00000000..ebbc098a --- /dev/null +++ b/test/integration/navigation_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class NavigationTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/mailers/.keep b/test/mailers/.keep new file mode 100644 index 00000000..e69de29b diff --git a/test/models/.keep b/test/models/.keep new file mode 100644 index 00000000..e69de29b diff --git a/test/stealth_test.rb b/test/stealth_test.rb new file mode 100644 index 00000000..4591c176 --- /dev/null +++ b/test/stealth_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class StealthTest < ActiveSupport::TestCase + test "it has a version number" do + assert Stealth::VERSION + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 00000000..9d47a1bf --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,15 @@ +# Configure Rails Environment +ENV["RAILS_ENV"] = "test" + +require_relative "../test/dummy/config/environment" +ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)] +ActiveRecord::Migrator.migrations_paths << File.expand_path("../db/migrate", __dir__) +require "rails/test_help" + +# Load fixtures from the engine +if ActiveSupport::TestCase.respond_to?(:fixture_paths=) + ActiveSupport::TestCase.fixture_paths = [File.expand_path("fixtures", __dir__)] + ActionDispatch::IntegrationTest.fixture_paths = ActiveSupport::TestCase.fixture_paths + ActiveSupport::TestCase.file_fixture_path = File.expand_path("fixtures", __dir__) + "/files" + ActiveSupport::TestCase.fixtures :all +end