From ae9d8229100e0a4525b78e69ade8aea72cc48d47 Mon Sep 17 00:00:00 2001 From: Mauricio Gomes Date: Tue, 8 Mar 2022 18:18:07 -0500 Subject: [PATCH] Initial import --- .circleci/config.yml | 60 +++++++++ .gitignore | 12 ++ Gemfile | 3 + Gemfile.lock | 117 ++++++++++++++++++ README.md | 5 + VERSION | 1 + lib/stealth-twilio_voice.rb | 1 + lib/stealth/services/twilio_voice/client.rb | 32 +++++ .../services/twilio_voice/message_handler.rb | 72 +++++++++++ .../services/twilio_voice/reply_handler.rb | 73 +++++++++++ lib/stealth/services/twilio_voice/setup.rb | 24 ++++ lib/stealth/services/twilio_voice/version.rb | 16 +++ lib/stealth/twilio_voice.rb | 2 + spec/spec_helper.rb | 14 +++ spec/version_spec.rb | 16 +++ stealth-twilio_voice.gemspec | 26 ++++ 16 files changed, 474 insertions(+) create mode 100644 .circleci/config.yml create mode 100644 .gitignore create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 README.md create mode 100644 VERSION create mode 100644 lib/stealth-twilio_voice.rb create mode 100644 lib/stealth/services/twilio_voice/client.rb create mode 100644 lib/stealth/services/twilio_voice/message_handler.rb create mode 100644 lib/stealth/services/twilio_voice/reply_handler.rb create mode 100644 lib/stealth/services/twilio_voice/setup.rb create mode 100644 lib/stealth/services/twilio_voice/version.rb create mode 100644 lib/stealth/twilio_voice.rb create mode 100644 spec/spec_helper.rb create mode 100644 spec/version_spec.rb create mode 100644 stealth-twilio_voice.gemspec diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..543ba4e --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,60 @@ +# Ruby CircleCI 2.0 configuration file +# +# Check https://circleci.com/docs/2.0/language-ruby/ for more details +# +version: 2 +jobs: + build: + docker: + # specify the version you desire here + - image: circleci/ruby:2.4.1-node-browsers + environment: + STEALTH_ENV: test + + # Specify service dependencies here if necessary + # CircleCI maintains a library of pre-built images + # documented at https://circleci.com/docs/2.0/circleci-images/ + # - image: circleci/postgres:9.4 + + working_directory: ~/repo + + steps: + - checkout + + # Download and cache dependencies + - restore_cache: + keys: + - v1-dependencies-{{ checksum "Gemfile.lock" }} + # fallback to using the latest cache if no exact match is found + - v1-dependencies- + + - run: + name: install dependencies + command: | + bundle install --jobs=4 --retry=3 --path vendor/bundle + + - save_cache: + paths: + - ./vendor/bundle + key: v1-dependencies-{{ checksum "Gemfile.lock" }} + + # run tests! + - run: + name: run tests + command: | + mkdir /tmp/test-results + TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)" + + bundle exec rspec --format progress \ + --format RspecJunitFormatter \ + --out /tmp/test-results/rspec.xml \ + --format progress \ + -- \ + $TEST_FILES + + # collect reports + - store_test_results: + path: /tmp/test-results + - store_artifacts: + path: /tmp/test-results + destination: test-results diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bd1af06 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# please add general patterns to your global ignore list +# see https://github.com/github/gitignore#readme +.DS_STORE +*.swp +*.rbc +*.sass-cache +/pkg +/doc/api +/coverage +.yardoc +doc/ +*.gem diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..fa75df1 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source 'https://rubygems.org' + +gemspec diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..0adf684 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,117 @@ +PATH + remote: . + specs: + stealth-twilio_voice (1.3.1) + stealth (>= 2.0.0.beta4) + twilio-ruby (~> 5.65) + +GEM + remote: https://rubygems.org/ + specs: + activesupport (6.1.4.6) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + zeitwerk (~> 2.3) + concurrent-ruby (1.1.9) + connection_pool (2.2.5) + diff-lcs (1.3) + faraday (1.10.0) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.0.3) + multipart-post (>= 1.2, < 3) + faraday-net_http (1.0.1) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.3) + i18n (1.10.0) + concurrent-ruby (~> 1.0) + jwt (2.3.0) + mini_portile2 (2.8.0) + minitest (5.15.0) + multi_json (1.15.0) + multipart-post (2.1.1) + mustermann (1.1.1) + ruby2_keywords (~> 0.0.1) + nio4r (2.5.8) + nokogiri (1.13.3) + mini_portile2 (~> 2.8.0) + racc (~> 1.4) + puma (5.6.2) + nio4r (~> 2.0) + racc (1.6.0) + rack (2.2.2) + rack-protection (2.2.0) + rack + rack-test (1.1.0) + rack (>= 1.0, < 3) + redis (4.6.0) + rspec (3.9.0) + rspec-core (~> 3.9.0) + rspec-expectations (~> 3.9.0) + rspec-mocks (~> 3.9.0) + rspec-core (3.9.1) + rspec-support (~> 3.9.1) + rspec-expectations (3.9.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.9.0) + rspec-mocks (3.9.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.9.0) + rspec-support (3.9.2) + rspec_junit_formatter (0.4.1) + rspec-core (>= 2, < 4, != 2.12.0) + ruby2_keywords (0.0.5) + sidekiq (6.4.1) + connection_pool (>= 2.2.2) + rack (~> 2.0) + redis (>= 4.2.0) + sinatra (2.2.0) + mustermann (~> 1.0) + rack (~> 2.2) + rack-protection (= 2.2.0) + tilt (~> 2.0) + stealth (2.0.0.beta4) + activesupport (~> 6.0) + multi_json (~> 1.12) + puma (>= 4.2, < 6.0) + sidekiq (~> 6.0) + sinatra (~> 2.0) + thor (~> 1.0) + thor (1.2.1) + tilt (2.0.10) + twilio-ruby (5.65.0) + faraday (>= 0.9, < 2.0) + jwt (>= 1.5, <= 2.5) + nokogiri (>= 1.6, < 2.0) + tzinfo (2.0.4) + concurrent-ruby (~> 1.0) + zeitwerk (2.5.4) + +PLATFORMS + ruby + +DEPENDENCIES + rack-test (~> 1.1) + rspec (~> 3.6) + rspec_junit_formatter (~> 0.3) + stealth-twilio_voice! + +BUNDLED WITH + 2.2.32 diff --git a/README.md b/README.md new file mode 100644 index 0000000..4c6fcf0 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# Stealth Twilio Phone + +The [Stealth](https://github.com/hellostealth/stealth) Twilio phone driver adds the ability to build your bot using voice over a standard phone call. + +[![Gem Version](https://badge.fury.io/rb/stealth-twilio.svg)](https://badge.fury.io/rb/stealth-twilio) diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..3a3cd8c --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.3.1 diff --git a/lib/stealth-twilio_voice.rb b/lib/stealth-twilio_voice.rb new file mode 100644 index 0000000..ee87ab0 --- /dev/null +++ b/lib/stealth-twilio_voice.rb @@ -0,0 +1 @@ +require 'stealth/twilio_voice' diff --git a/lib/stealth/services/twilio_voice/client.rb b/lib/stealth/services/twilio_voice/client.rb new file mode 100644 index 0000000..fa6339b --- /dev/null +++ b/lib/stealth/services/twilio_voice/client.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'twilio-ruby' + +require 'stealth/services/twilio_voice/message_handler' +require 'stealth/services/twilio_voice/reply_handler' +require 'stealth/services/twilio_voice/setup' + +module Stealth + module Services + module TwilioVoice + class Client < Stealth::Services::BaseClient + + attr_reader :reply + + def initialize(reply:) + @reply = reply + end + + def transmit + Thread.current[:voice_reply] = reply[:msg] + + Stealth::Logger.l( + topic: :twilio_voice, + message: "Audio reply sent." + ) + end + + end + end + end +end diff --git a/lib/stealth/services/twilio_voice/message_handler.rb b/lib/stealth/services/twilio_voice/message_handler.rb new file mode 100644 index 0000000..5a88286 --- /dev/null +++ b/lib/stealth/services/twilio_voice/message_handler.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'twilio-ruby' + +module Stealth + module Services + module TwilioVoice + class MessageHandler < Stealth::Services::BaseMessageHandler + attr_reader :service_message, :params, :headers + + def initialize(params:, headers:) + @service = "twilio_voice" + @params = params + @headers = headers + end + + def coordinate + dispatcher = Stealth::Dispatcher.new( + service: @service, + params: params, + headers: headers + ) + + dispatcher.process + + puts "SENDING: #{Thread.current[:voice_reply]}" + + send_voice_reply + end + + def process + @service_message = ServiceMessage.new(service: 'twilio_voice') + service_message.sender_id = params['From'] + service_message.target_id = params['To'] + if params["Digits"].present? + service_message.message = parse_int(params["Digits"]) + elsif params["SpeechResult"].present? + service_message.message = params["SpeechResult"] + service_message.confidence = parse_float(params["Confidence"]) + end + + puts "Webhook from Twilio: #{params.inspect}" + + service_message + end + + private + + def send_voice_reply + response_headers = { 'Content-Type' => 'text/xml' } + [200, response_headers, Thread.current[:voice_reply]] + end + + def parse_int(int) + Integer(int) + rescue TypeError + return nil + rescue ArgumentError + return nil + end + + def parse_float(float) + Float(float) + rescue TypeError + return nil + rescue ArgumentError + return nil + end + end + end + end +end diff --git a/lib/stealth/services/twilio_voice/reply_handler.rb b/lib/stealth/services/twilio_voice/reply_handler.rb new file mode 100644 index 0000000..f1902c6 --- /dev/null +++ b/lib/stealth/services/twilio_voice/reply_handler.rb @@ -0,0 +1,73 @@ +# coding: utf-8 +# frozen_string_literal: true + +module Stealth + module Services + module TwilioVoice + class ReplyHandler < Stealth::Services::BaseReplyHandler + + attr_reader :recipient_id, :replies + + def initialize(recipient_id: nil, replies: nil) + @recipient_id = recipient_id + @replies = replies + end + + def build_reply + voice_reply = ::Twilio::TwiML::VoiceResponse.new do |r| + replies.each do |reply| + case reply["reply_type"] + when "speech" + # Say options: https://www.twilio.com/docs/voice/twiml/say + r.say( + message: reply["speech"], + voice: Stealth.config.twilio_voice.voice + ) + when "delay" + # Pause options: https://www.twilio.com/docs/voice/twiml/pause + r.pause(length: reply["duration"]) + when "play" + # Play options: https://www.twilio.com/docs/voice/twiml/play + loop_val = reply["loop"] || 1 + r.play(loop: loop_val, url: reply["url"]) + when "gather" + # Gather options: https://www.twilio.com/docs/voice/twiml/gather + finish_key = reply["finish_key"] || "#" + timeout = reply["timeout"] || 5 + speech_timeout = reply["speech_timeout"] || "auto" + num_digits = reply["num_digits"] || 5 + profanity_filter = reply["profanity_filter"] || true + language = reply["language"] || "en-US" + hints = reply["hints"]&.join(",") || "" + # dtmf, speech, dtmf speech + input = reply["input"] || "dtmf" + # Must utilize speech_model "phone_call" + enhanced = reply["enhanced"] || false + # default, number_and_commands, phone_call + speech_model = reply["speech_model"] || "default" + + r.gather( + input: input, + finish_on_key: finish_key, + timeout: timeout, + speech_timeout: speech_timeout, + num_digits: num_digits, + profanity_filter: profanity_filter, + language: language, + hints: hints, + enhanced: enhanced, + speech_model: speech_model + ) + end + end + end.to_s + + { + type: :speech, + msg: voice_reply + } + end + end + end + end +end diff --git a/lib/stealth/services/twilio_voice/setup.rb b/lib/stealth/services/twilio_voice/setup.rb new file mode 100644 index 0000000..29d7659 --- /dev/null +++ b/lib/stealth/services/twilio_voice/setup.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'stealth/services/twilio_voice/client' + +module Stealth + module Services + module TwilioVoice + + class Setup + + class << self + def trigger + Stealth::Logger.l( + topic: "twilio", + message: "There is no setup needed!" + ) + end + end + + end + + end + end +end diff --git a/lib/stealth/services/twilio_voice/version.rb b/lib/stealth/services/twilio_voice/version.rb new file mode 100644 index 0000000..f565037 --- /dev/null +++ b/lib/stealth/services/twilio_voice/version.rb @@ -0,0 +1,16 @@ +# coding: utf-8 +# frozen_string_literal: true + +module Stealth + module Services + module TwilioVoice + module Version + def self.version + File.read(File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'VERSION')).strip + end + end + + VERSION = Version.version + end + end +end diff --git a/lib/stealth/twilio_voice.rb b/lib/stealth/twilio_voice.rb new file mode 100644 index 0000000..6367ad1 --- /dev/null +++ b/lib/stealth/twilio_voice.rb @@ -0,0 +1,2 @@ +require 'stealth/services/twilio_voice/version' +require 'stealth/services/twilio_voice/client' diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..c9c9f5e --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,14 @@ +$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) +$LOAD_PATH.unshift(File.dirname(__FILE__)) +require 'rspec' + +require 'stealth' +require 'stealth-twilio_voice' + +# Requires supporting files with custom matchers and macros, etc, +# in ./support/ and its subdirectories. +Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } + +RSpec.configure do |config| + +end diff --git a/spec/version_spec.rb b/spec/version_spec.rb new file mode 100644 index 0000000..91caebc --- /dev/null +++ b/spec/version_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require File.expand_path(File.dirname(__FILE__) + '/spec_helper') + +describe "Stealth::Services::Twilio::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::Services::Twilio::Version.version).to eq version_in_file + end + + it "should return the current gem version via a constant" do + expect(Stealth::Services::Twilio::VERSION).to eq version_in_file + end +end diff --git a/stealth-twilio_voice.gemspec b/stealth-twilio_voice.gemspec new file mode 100644 index 0000000..1ba4fd0 --- /dev/null +++ b/stealth-twilio_voice.gemspec @@ -0,0 +1,26 @@ +$LOAD_PATH.push File.expand_path('../lib', __FILE__) + +version = File.read(File.join(File.dirname(__FILE__), 'VERSION')).strip + +Gem::Specification.new do |s| + s.name = 'stealth-twilio_voice' + s.summary = 'Stealth Twilio phone (voice) driver' + s.description = 'Twilio phone call (voice) driver for Stealth.' + s.homepage = 'https://github.com/hiremav/stealth-twilio_voice' + s.licenses = ['MIT'] + s.version = version + s.author = 'Mauricio Gomes' + s.email = 'mauricio@edge14.com' + + s.add_dependency 'stealth', '>= 2.0.0.beta4' + s.add_dependency 'twilio-ruby', '~> 5.65' + + s.add_development_dependency 'rspec', '~> 3.6' + s.add_development_dependency 'rspec_junit_formatter', '~> 0.3' + s.add_development_dependency 'rack-test', '~> 1.1' + + 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'] +end