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