ralyxa-lambda 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +16 -0
  5. data/.ruby-version +1 -0
  6. data/.tool-versions +1 -0
  7. data/.travis.yml +12 -0
  8. data/CODE_OF_CONDUCT.md +74 -0
  9. data/CONTRIBUTING.md +19 -0
  10. data/Gemfile +3 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +294 -0
  13. data/Rakefile +6 -0
  14. data/bin/console +14 -0
  15. data/bin/setup +8 -0
  16. data/lib/ralyxa.rb +36 -0
  17. data/lib/ralyxa/configuration.rb +18 -0
  18. data/lib/ralyxa/errors.rb +4 -0
  19. data/lib/ralyxa/handler.rb +47 -0
  20. data/lib/ralyxa/register_intents.rb +45 -0
  21. data/lib/ralyxa/request_entities/request.rb +63 -0
  22. data/lib/ralyxa/request_entities/user.rb +25 -0
  23. data/lib/ralyxa/response_builder.rb +65 -0
  24. data/lib/ralyxa/response_entities/card.rb +89 -0
  25. data/lib/ralyxa/response_entities/directives.rb +9 -0
  26. data/lib/ralyxa/response_entities/directives/audio.rb +11 -0
  27. data/lib/ralyxa/response_entities/directives/audio/audio_item.rb +23 -0
  28. data/lib/ralyxa/response_entities/directives/audio/stream.rb +37 -0
  29. data/lib/ralyxa/response_entities/directives/audio_player.rb +46 -0
  30. data/lib/ralyxa/response_entities/directives/audio_player/clear_queue.rb +27 -0
  31. data/lib/ralyxa/response_entities/directives/audio_player/play.rb +32 -0
  32. data/lib/ralyxa/response_entities/directives/audio_player/stop.rb +19 -0
  33. data/lib/ralyxa/response_entities/output_speech.rb +35 -0
  34. data/lib/ralyxa/response_entities/reprompt.rb +20 -0
  35. data/lib/ralyxa/response_entities/response.rb +54 -0
  36. data/lib/ralyxa/skill.rb +53 -0
  37. data/lib/ralyxa/version.rb +3 -0
  38. data/pkg/ralyxa-1.0.0.gem +0 -0
  39. data/ralyxa.gemspec +32 -0
  40. metadata +195 -0
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'ralyxa'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require 'irb'
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/lib/ralyxa.rb ADDED
@@ -0,0 +1,36 @@
1
+ require 'ralyxa/version'
2
+ require 'ralyxa/configuration'
3
+ require 'ralyxa/register_intents'
4
+ require 'ralyxa/skill'
5
+
6
+ module Ralyxa
7
+ class << self
8
+ attr_accessor :configuration
9
+
10
+ def configure
11
+ yield configuration if block_given?
12
+ end
13
+
14
+ def method_missing(m, *args, &block)
15
+ if configuration.respond_to?(m)
16
+ configuration.send(m, *args, &block)
17
+ else
18
+ super
19
+ end
20
+ end
21
+
22
+ def respond_to_missing?(m, include_private = false)
23
+ configuration.respond_to?(m) || super
24
+ end
25
+
26
+ private
27
+
28
+ def setup_configuration
29
+ @configuration = Ralyxa::Configuration.new
30
+ end
31
+ end
32
+
33
+ setup_configuration
34
+ end
35
+
36
+ Ralyxa::RegisterIntents.run
@@ -0,0 +1,18 @@
1
+ module Ralyxa
2
+ class Configuration
3
+ attr_accessor :validate_requests, :require_secure_urls
4
+
5
+ def initialize
6
+ @validate_requests = true
7
+ @require_secure_urls = true
8
+ end
9
+
10
+ def validate_requests?
11
+ validate_requests
12
+ end
13
+
14
+ def require_secure_urls?
15
+ require_secure_urls
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,4 @@
1
+ module Ralyxa
2
+ class UnsecureUrlError < StandardError
3
+ end
4
+ end
@@ -0,0 +1,47 @@
1
+ require_relative './response_builder'
2
+ require_relative './response_entities/card'
3
+
4
+ # Handler Base Class. Each Intent Handler inherits from this, and overwrites the #handle method.
5
+ module Ralyxa
6
+ class Handler
7
+ def initialize(request)
8
+ @request = request
9
+ end
10
+
11
+ def handle
12
+ raise NotImplementedError
13
+ end
14
+
15
+ def respond(response_text = '', response_details = {}, response_builder = Ralyxa::ResponseBuilder)
16
+ options = response_details
17
+ options[:response_text] = response_text if response_text
18
+
19
+ response_builder.build(options)
20
+ end
21
+
22
+ def tell(response_text = '', response_details = {})
23
+ respond(response_text, response_details.merge(end_session: true))
24
+ end
25
+
26
+ def card(title, body, image_url = nil, card_class = Ralyxa::ResponseEntities::Card)
27
+ card_class.as_hash(title, body, image_url)
28
+ end
29
+
30
+ def audio_player
31
+ Ralyxa::ResponseEntities::Directives::AudioPlayer
32
+ end
33
+
34
+ def link_account_card(card_class = Ralyxa::ResponseEntities::Card)
35
+ card_class.link_account
36
+ end
37
+
38
+ def log(level, message)
39
+ puts "[#{Time.new}] [#{@request.user_id}] #{level} - #{message}"
40
+ end
41
+
42
+ alias ask respond
43
+
44
+ attr_reader :request
45
+ private :request, :respond, :tell, :ask
46
+ end
47
+ end
@@ -0,0 +1,45 @@
1
+ require_relative './skill'
2
+
3
+ module Ralyxa
4
+ class RegisterIntents
5
+ DEFAULT_INTENTS_DIRECTORY = './intents'.freeze
6
+
7
+ def initialize(intents_directory, alexa_skill_class)
8
+ @intents_directory = intents_directory
9
+ @alexa_skill_class = alexa_skill_class
10
+ end
11
+
12
+ def self.run(intents_directory = DEFAULT_INTENTS_DIRECTORY, alexa_skill_class = Ralyxa::Skill)
13
+ new(intents_directory, alexa_skill_class).run
14
+ end
15
+
16
+ def run
17
+ warn NO_INTENT_DECLARATIONS_FOUND if intent_declarations.empty?
18
+
19
+ intent_declarations.each do |intent_declaration|
20
+ alexa_skill_class.class_eval intent_declaration
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :intents_directory, :alexa_skill_class
27
+
28
+ def intent_declarations
29
+ @intent_declarations ||=
30
+ Dir.glob("#{intents_directory}/*.rb")
31
+ .map { |relative_path| File.expand_path(relative_path) }
32
+ .map { |intent_declaration_path| File.open(intent_declaration_path).read }
33
+ end
34
+
35
+ heredoc = <<~HEREDOC
36
+ \e[33m
37
+ WARNING: You haven't defined any intent declarations.
38
+
39
+ Please define intent declarations inside a directory called 'intents',
40
+ on the same directory level as the runfile for your server application.
41
+ \e[0m
42
+ HEREDOC
43
+ NO_INTENT_DECLARATIONS_FOUND = heredoc.freeze
44
+ end
45
+ end
@@ -0,0 +1,63 @@
1
+ require 'json'
2
+ require 'forwardable'
3
+ require 'alexa_verifier'
4
+ require_relative './user'
5
+
6
+ module Ralyxa
7
+ module RequestEntities
8
+ class Request
9
+ extend Forwardable
10
+ INTENT_REQUEST_TYPE = 'IntentRequest'.freeze
11
+
12
+ def_delegator :@user, :id, :user_id
13
+ def_delegator :@user, :access_token, :user_access_token
14
+ def_delegator :@user, :access_token_exists?, :user_access_token_exists?
15
+
16
+ attr_reader :request
17
+
18
+ def initialize(original_request, user_class = Ralyxa::RequestEntities::User)
19
+ validate_request(original_request) if Ralyxa.configuration.validate_requests?
20
+
21
+ @request = JSON.parse(original_request.body.read)
22
+ attempt_to_rewind_request_body(original_request)
23
+
24
+ @user = user_class.build(@request)
25
+ end
26
+
27
+ def intent_name
28
+ return @request['request']['type'] unless intent_request?
29
+ @request['request']['intent']['name']
30
+ end
31
+
32
+ def slot_value(slot_name)
33
+ @request['request']['intent']['slots'][slot_name]['value']
34
+ end
35
+
36
+ def new_session?
37
+ @request['session']['new']
38
+ end
39
+
40
+ def session_attributes
41
+ @request['session']['attributes']
42
+ end
43
+
44
+ def session_attribute(attribute_name)
45
+ session_attributes[attribute_name]
46
+ end
47
+
48
+ private
49
+
50
+ def intent_request?
51
+ @request['request']['type'] == INTENT_REQUEST_TYPE
52
+ end
53
+
54
+ def validate_request(request)
55
+ AlexaVerifier.valid!(request)
56
+ end
57
+
58
+ def attempt_to_rewind_request_body(original_request)
59
+ original_request.body&.rewind
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,25 @@
1
+ module Ralyxa
2
+ module RequestEntities
3
+ class User
4
+ attr_reader :id, :access_token
5
+
6
+ def initialize(id:, access_token: nil)
7
+ @id = id
8
+ @access_token = access_token
9
+ end
10
+
11
+ def self.build(request)
12
+ user_hash = request.dig('session', 'user') || request.dig('context', 'System', 'user') || {}
13
+
14
+ new(
15
+ id: user_hash['userId'],
16
+ access_token: user_hash['accessToken']
17
+ )
18
+ end
19
+
20
+ def access_token_exists?
21
+ !@access_token.nil?
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,65 @@
1
+ require 'json'
2
+ require_relative './response_entities/response'
3
+
4
+ module Ralyxa
5
+ class ResponseBuilder
6
+ def initialize(response_class, output_speech_class, reprompt_class, options)
7
+ @response_class = response_class
8
+ @output_speech_class = output_speech_class
9
+ @reprompt_class = reprompt_class
10
+ @options = options
11
+ end
12
+
13
+ def self.build(options = {}, response_class = Ralyxa::ResponseEntities::Response, output_speech_class = Ralyxa::ResponseEntities::OutputSpeech, reprompt_class = Ralyxa::ResponseEntities::Reprompt)
14
+ new(response_class, output_speech_class, reprompt_class, options).build
15
+ end
16
+
17
+ def build
18
+ merge_output_speech if response_text_exists?
19
+ merge_reprompt if reprompt_exists?
20
+ merge_card if card_exists?
21
+
22
+ @response_class.as_hash(@options).to_json
23
+ end
24
+
25
+ private
26
+
27
+ def merge_output_speech
28
+ @options.merge!(output_speech: output_speech)
29
+ end
30
+
31
+ def merge_reprompt
32
+ @options.merge!(reprompt: reprompt)
33
+ end
34
+
35
+ def merge_card
36
+ @options[:card] = @options[:card].to_h
37
+ end
38
+
39
+ def card_exists?
40
+ @options[:card]
41
+ end
42
+
43
+ def response_text_exists?
44
+ @options[:response_text]
45
+ end
46
+
47
+ def reprompt_exists?
48
+ @options[:reprompt]
49
+ end
50
+
51
+ def output_speech
52
+ output_speech_params = { speech: @options.delete(:response_text) }
53
+ output_speech_params[:ssml] = @options.delete(:ssml) if @options[:ssml]
54
+
55
+ @output_speech_class.as_hash(output_speech_params)
56
+ end
57
+
58
+ def reprompt
59
+ reprompt_params = { reprompt_speech: @options.delete(:reprompt) }
60
+ reprompt_params[:reprompt_ssml] = @options.delete(:reprompt_ssml) if @options[:reprompt_ssml]
61
+
62
+ @reprompt_class.as_hash(reprompt_params)
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,89 @@
1
+ require_relative '../errors'
2
+
3
+ module Ralyxa
4
+ module ResponseEntities
5
+ class Card
6
+ LINK_ACCOUNT_CARD_TYPE = 'LinkAccount'.freeze
7
+ SIMPLE_CARD_TYPE = 'Simple'.freeze
8
+ STANDARD_CARD_TYPE = 'Standard'.freeze
9
+
10
+ def initialize(options)
11
+ @options = options
12
+ end
13
+
14
+ def self.as_hash(title, body, small_image_url = nil, large_image_url = small_image_url)
15
+ new(title: title, body: body, small_image_url: small_image_url, large_image_url: large_image_url).to_h
16
+ end
17
+
18
+ def self.link_account
19
+ new(link_account: true).to_h
20
+ end
21
+
22
+ def to_h
23
+ {}.tap do |card|
24
+ add_type(card)
25
+ add_title(card) if @options[:title]
26
+ add_body(card) if @options[:body]
27
+ add_image(card) if standard?
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def add_type(card)
34
+ return card[:type] = LINK_ACCOUNT_CARD_TYPE if link_account?
35
+ card[:type] = SIMPLE_CARD_TYPE if simple?
36
+ card[:type] = STANDARD_CARD_TYPE if standard?
37
+ end
38
+
39
+ def add_title(card)
40
+ card[:title] = @options[:title]
41
+ end
42
+
43
+ def add_body(card)
44
+ card[:content] = @options[:body] if simple?
45
+ card[:text] = @options[:body] if standard?
46
+ end
47
+
48
+ def add_image(card)
49
+ raise Ralyxa::UnsecureUrlError, "Card images must be available at an SSL-enabled (HTTPS) endpoint. Your current image urls are: (small: #{@options[:small_image_url]}, large: #{@options[:large_image_url]}" unless secure_images?
50
+ card[:image] = {}
51
+
52
+ small_image = @options[:small_image_url] || @options[:large_image_url]
53
+ large_image = @options[:large_image_url] || @options[:small_image_url]
54
+
55
+ card[:image][:smallImageUrl] = small_image if small_image
56
+ card[:image][:largeImageUrl] = large_image if large_image
57
+ end
58
+
59
+ def link_account?
60
+ !@options[:link_account].nil?
61
+ end
62
+
63
+ def simple?
64
+ !@options[:small_image_url] && !@options[:large_image_url]
65
+ end
66
+
67
+ def standard?
68
+ @options[:small_image_url] || @options[:large_image_url]
69
+ end
70
+
71
+ def secure_images?
72
+ small_secure = secure_uri?(@options[:small_image_url])
73
+ large_secure = secure_uri?(@options[:large_image_url])
74
+
75
+ small_secure && large_secure
76
+ end
77
+
78
+ # Given a uri string, retutrn true if:
79
+ # * the scheme is https
80
+ # * or if we are not interested in the uri being secure
81
+ # * or if the value that has been passed is nil
82
+ def secure_uri?(uri_string)
83
+ return true if uri_string.nil?
84
+
85
+ (URI.parse(uri_string).scheme == 'https' || !Ralyxa.require_secure_urls?)
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,9 @@
1
+ require_relative './directives/audio'
2
+ require_relative './directives/audio_player'
3
+
4
+ module Ralyxa
5
+ module ResponseEntities
6
+ module Directives
7
+ end
8
+ end
9
+ end