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.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/.rspec +3 -0
- data/.rubocop.yml +16 -0
- data/.ruby-version +1 -0
- data/.tool-versions +1 -0
- data/.travis.yml +12 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/CONTRIBUTING.md +19 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +294 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/ralyxa.rb +36 -0
- data/lib/ralyxa/configuration.rb +18 -0
- data/lib/ralyxa/errors.rb +4 -0
- data/lib/ralyxa/handler.rb +47 -0
- data/lib/ralyxa/register_intents.rb +45 -0
- data/lib/ralyxa/request_entities/request.rb +63 -0
- data/lib/ralyxa/request_entities/user.rb +25 -0
- data/lib/ralyxa/response_builder.rb +65 -0
- data/lib/ralyxa/response_entities/card.rb +89 -0
- data/lib/ralyxa/response_entities/directives.rb +9 -0
- data/lib/ralyxa/response_entities/directives/audio.rb +11 -0
- data/lib/ralyxa/response_entities/directives/audio/audio_item.rb +23 -0
- data/lib/ralyxa/response_entities/directives/audio/stream.rb +37 -0
- data/lib/ralyxa/response_entities/directives/audio_player.rb +46 -0
- data/lib/ralyxa/response_entities/directives/audio_player/clear_queue.rb +27 -0
- data/lib/ralyxa/response_entities/directives/audio_player/play.rb +32 -0
- data/lib/ralyxa/response_entities/directives/audio_player/stop.rb +19 -0
- data/lib/ralyxa/response_entities/output_speech.rb +35 -0
- data/lib/ralyxa/response_entities/reprompt.rb +20 -0
- data/lib/ralyxa/response_entities/response.rb +54 -0
- data/lib/ralyxa/skill.rb +53 -0
- data/lib/ralyxa/version.rb +3 -0
- data/pkg/ralyxa-1.0.0.gem +0 -0
- data/ralyxa.gemspec +32 -0
- metadata +195 -0
data/Rakefile
ADDED
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
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,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
|