ralyxa-lambda 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
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