alexa_ruby 0.2.0 → 1.0.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 +4 -4
- data/Gemfile +0 -1
- data/Gemfile.lock +24 -21
- data/README.md +26 -14
- data/Rakefile +6 -4
- data/lib/alexa_ruby/alexa.rb +52 -0
- data/lib/alexa_ruby/request/audio_player_request.rb +18 -25
- data/lib/alexa_ruby/request/base_request/context/device.rb +28 -0
- data/lib/alexa_ruby/request/base_request/context/user.rb +28 -0
- data/lib/alexa_ruby/request/base_request/context.rb +45 -0
- data/lib/alexa_ruby/request/base_request/session.rb +60 -0
- data/lib/alexa_ruby/request/base_request.rb +53 -0
- data/lib/alexa_ruby/request/intent_request/slot.rb +28 -11
- data/lib/alexa_ruby/request/intent_request.rb +41 -42
- data/lib/alexa_ruby/request/launch_request.rb +3 -10
- data/lib/alexa_ruby/request/session_ended_request.rb +12 -12
- data/lib/alexa_ruby/response/audio_player.rb +25 -32
- data/lib/alexa_ruby/response/card.rb +67 -12
- data/lib/alexa_ruby/response.rb +155 -127
- data/lib/alexa_ruby/version.rb +1 -1
- data/lib/alexa_ruby.rb +40 -66
- data/spec/request_spec.rb +197 -0
- data/spec/response_spec.rb +201 -0
- data/spec/spec_helper.rb +7 -0
- metadata +57 -19
- data/lib/alexa_ruby/request.rb +0 -28
- data/lib/alexa_ruby/session.rb +0 -55
@@ -1,19 +1,12 @@
|
|
1
1
|
module AlexaRuby
|
2
2
|
# LaunchRequest class implements Alexa "LaunchRequest" request type
|
3
|
-
class LaunchRequest <
|
3
|
+
class LaunchRequest < BaseRequest
|
4
4
|
# Initialize new launch request
|
5
5
|
#
|
6
|
-
# @param
|
7
|
-
def initialize(
|
6
|
+
# @param request [Hash] valid request from Amazon Alexa service
|
7
|
+
def initialize(request)
|
8
8
|
@type = :launch
|
9
9
|
super
|
10
10
|
end
|
11
|
-
|
12
|
-
# Outputs the launch requestID
|
13
|
-
#
|
14
|
-
# @return [String] launch request ID
|
15
|
-
def to_s
|
16
|
-
"LaunchRequest requestID: #{request_id}"
|
17
|
-
end
|
18
11
|
end
|
19
12
|
end
|
@@ -1,23 +1,23 @@
|
|
1
1
|
# Session end request class.
|
2
2
|
module AlexaRuby
|
3
|
-
#
|
4
|
-
class SessionEndedRequest <
|
5
|
-
attr_accessor :reason
|
6
|
-
|
3
|
+
# Amazon "SessionEndedRequest" request type
|
4
|
+
class SessionEndedRequest < BaseRequest
|
7
5
|
# Initialize new SessionEndedRequest
|
8
6
|
#
|
9
|
-
# @param
|
10
|
-
def initialize(
|
7
|
+
# @param request [Hash] valid request from Amazon Alexa service
|
8
|
+
def initialize(request)
|
11
9
|
@type = :session_ended
|
12
|
-
@reason = json[:request][:reason]
|
13
10
|
super
|
11
|
+
finalize_session
|
14
12
|
end
|
15
13
|
|
16
|
-
|
17
|
-
|
18
|
-
#
|
19
|
-
def
|
20
|
-
|
14
|
+
private
|
15
|
+
|
16
|
+
# Set final session params
|
17
|
+
def finalize_session
|
18
|
+
@session.state = :ended
|
19
|
+
@session.end_reason = @req[:request][:reason]
|
20
|
+
@session.error = @req[:request][:error] unless @req[:request][:error].nil?
|
21
21
|
end
|
22
22
|
end
|
23
23
|
end
|
@@ -1,51 +1,44 @@
|
|
1
|
-
require 'securerandom'
|
2
|
-
require 'uri'
|
3
|
-
|
4
1
|
module AlexaRuby
|
5
2
|
# AudioPlayer class encapsulates all Alexa audio player directives
|
6
3
|
class AudioPlayer
|
7
|
-
attr_accessor :directive
|
8
|
-
|
9
|
-
# Initialize new directive object
|
10
|
-
def initialize
|
11
|
-
@directive = {}
|
12
|
-
end
|
13
|
-
|
14
4
|
# Build an AudioPlayer.Play directive
|
15
5
|
#
|
16
|
-
# @param
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
# - playback offset
|
6
|
+
# @param params [Hash] optional request parameters:
|
7
|
+
# url [String] streaming URL
|
8
|
+
# token [String] streaming service token
|
9
|
+
# offset [Integer] playback offset
|
21
10
|
# @return [Hash] AudioPlayer.Play directive
|
22
|
-
def
|
23
|
-
|
24
|
-
token =
|
25
|
-
offset =
|
26
|
-
|
27
|
-
@directive[:playBehavior] = 'REPLACE_ALL'
|
28
|
-
@directive[:audioItem] = { stream: {} }
|
29
|
-
@directive[:audioItem][:stream][:url] = opts[:url]
|
30
|
-
@directive[:audioItem][:stream][:token] = token
|
31
|
-
@directive[:audioItem][:stream][:offsetInMilliseconds] = offset
|
11
|
+
def play_directive(params)
|
12
|
+
url = params[:url]
|
13
|
+
token = params[:token] || SecureRandom.uuid
|
14
|
+
offset = params[:offset] || 0
|
15
|
+
build_directive('AudioPlayer.Play', url, token, offset)
|
32
16
|
end
|
33
17
|
|
34
18
|
# Build AudioPlayer.Stop directive
|
35
19
|
#
|
36
20
|
# @return [Hash] AudioPlayer.Stop directive
|
37
|
-
def
|
38
|
-
|
21
|
+
def stop_directive
|
22
|
+
build_directive('AudioPlayer.Stop')
|
39
23
|
end
|
40
24
|
|
41
25
|
private
|
42
26
|
|
43
|
-
#
|
27
|
+
# Set play directive parameters
|
44
28
|
#
|
45
|
-
# @param
|
46
|
-
# @
|
47
|
-
|
48
|
-
|
29
|
+
# @param type [String] directive type, can be Play or Stop
|
30
|
+
# @param url [String] streaming service URL
|
31
|
+
# @param token [String] streaming service token
|
32
|
+
# @param offset [Integer] playback offset
|
33
|
+
def build_directive(type, url = nil, token = nil, offset = nil)
|
34
|
+
directive = { type: type }
|
35
|
+
return directive if type == 'AudioPlayer.Stop'
|
36
|
+
directive[:playBehavior] = 'REPLACE_ALL'
|
37
|
+
directive[:audioItem] = { stream: {} }
|
38
|
+
directive[:audioItem][:stream][:url] = url
|
39
|
+
directive[:audioItem][:stream][:token] = token
|
40
|
+
directive[:audioItem][:stream][:offsetInMilliseconds] = offset
|
41
|
+
directive
|
49
42
|
end
|
50
43
|
end
|
51
44
|
end
|
@@ -1,25 +1,80 @@
|
|
1
1
|
module AlexaRuby
|
2
2
|
# Class for Amazon Alexa app cards
|
3
3
|
class Card
|
4
|
-
|
4
|
+
attr_reader :obj
|
5
5
|
|
6
6
|
# Initialize new card object
|
7
|
-
|
7
|
+
#
|
8
|
+
# @param params [Hash] card parameters:
|
9
|
+
# type [String] card type, can be "Simple", "Standard" or "LinkAccount"
|
10
|
+
# title [String] card title
|
11
|
+
# content [String] card content (line breaks must be already included)
|
12
|
+
# small_image_url [String] an URL for small card image
|
13
|
+
# large_image_url [String] an URL for large card image
|
14
|
+
# @return [Hash] ready to use card object
|
15
|
+
# @raise [ArgumentError] if card type is unknown
|
16
|
+
def initialize(params)
|
8
17
|
@obj = {}
|
18
|
+
@obj[:type] = params[:type] || 'Simple'
|
19
|
+
raise ArgumentError, 'Unknown card type' unless valid_type?
|
20
|
+
@params = params
|
21
|
+
build
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# Check if card type is valid
|
27
|
+
#
|
28
|
+
# @return [Boolean]
|
29
|
+
def valid_type?
|
30
|
+
%w[Simple Standard LinkAccount].include? @obj[:type]
|
9
31
|
end
|
10
32
|
|
11
33
|
# Build card object
|
12
34
|
#
|
13
|
-
# @
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
@
|
20
|
-
|
21
|
-
|
22
|
-
|
35
|
+
# @return [Hash] ready to use card object
|
36
|
+
def build
|
37
|
+
return if @obj[:type] == 'LinkAccount'
|
38
|
+
add_title
|
39
|
+
add_content unless @params[:content].nil?
|
40
|
+
return unless @obj[:type] == 'Standard'
|
41
|
+
return if @params[:small_image_url].nil? &&
|
42
|
+
@params[:large_image_url].nil?
|
43
|
+
add_images
|
44
|
+
end
|
45
|
+
|
46
|
+
# Add card title to object
|
47
|
+
#
|
48
|
+
# @raise [ArgumentError] if not title found
|
49
|
+
def add_title
|
50
|
+
raise ArgumentError, 'Card need a title' if @params[:title].nil?
|
51
|
+
@obj[:title] = @params[:title]
|
52
|
+
end
|
53
|
+
|
54
|
+
# Add content to card body.
|
55
|
+
# Content must be already prepared and contain needed line breaks.
|
56
|
+
# \r\n or \n can be used for line breaks
|
57
|
+
def add_content
|
58
|
+
type =
|
59
|
+
case @obj[:type]
|
60
|
+
when 'Simple'
|
61
|
+
:content
|
62
|
+
when 'Standard'
|
63
|
+
:text
|
64
|
+
end
|
65
|
+
@obj[type] = @params[:content]
|
66
|
+
end
|
67
|
+
|
68
|
+
# Add images to card
|
69
|
+
def add_images
|
70
|
+
@obj[:image] = {}
|
71
|
+
|
72
|
+
if @params[:small_image_url]
|
73
|
+
@obj[:image][:smallImageUrl] = @params[:small_image_url]
|
74
|
+
end
|
75
|
+
|
76
|
+
return unless @params[:large_image_url]
|
77
|
+
@obj[:image][:largeImageUrl] = @params[:large_image_url]
|
23
78
|
end
|
24
79
|
end
|
25
80
|
end
|
data/lib/alexa_ruby/response.rb
CHANGED
@@ -1,176 +1,204 @@
|
|
1
|
-
require 'alexa_ruby/response/audio_player'
|
2
|
-
require 'alexa_ruby/response/card'
|
3
|
-
|
4
1
|
module AlexaRuby
|
5
|
-
#
|
2
|
+
# Response for Amazon Alexa service request
|
6
3
|
class Response
|
7
|
-
|
8
|
-
:speech, :reprompt, :response, :card
|
9
|
-
|
10
|
-
# Initialize new response and set response version
|
4
|
+
# Initialize new response
|
11
5
|
#
|
12
|
-
# @param
|
13
|
-
|
14
|
-
|
15
|
-
@
|
16
|
-
@
|
6
|
+
# @param request_type [Symbol] AlexaRuby::Request type
|
7
|
+
# @param version [String] Amazon Alexa SDK version
|
8
|
+
def initialize(request_type, version = '1.0')
|
9
|
+
@req_type = request_type
|
10
|
+
@resp = {
|
11
|
+
version: version,
|
12
|
+
sessionAttributes: {},
|
13
|
+
response: { shouldEndSession: true }
|
14
|
+
}
|
17
15
|
end
|
18
16
|
|
19
|
-
#
|
17
|
+
# Add one session attribute
|
20
18
|
#
|
21
|
-
# @param key [String]
|
19
|
+
# @param key [String] atrribute key
|
22
20
|
# @param value [String] attribute value
|
23
|
-
|
24
|
-
|
21
|
+
# @raise [ArgumentError] if session key is already added and
|
22
|
+
# rewrite is set to false
|
23
|
+
def add_session_attribute(key, value, rewrite = false)
|
24
|
+
unless rewrite
|
25
|
+
if @resp[:sessionAttributes].key?(key)
|
26
|
+
raise ArgumentError, 'Duplicate session attributes not allowed'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
@resp[:sessionAttributes][key] = value
|
25
30
|
end
|
26
31
|
|
27
|
-
# Add
|
28
|
-
# Speech can be plain text or SSML text, default type is plain text
|
32
|
+
# Add pack of session attributes and overwrite all existing ones
|
29
33
|
#
|
30
|
-
# @param
|
31
|
-
# @
|
32
|
-
def
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
34
|
+
# @param attributes [Hash] pack of session attributes
|
35
|
+
# @raise [ArgumentError] if given paramter is not a Hash object
|
36
|
+
def add_session_attributes(attributes)
|
37
|
+
unless attributes.is_a? Hash
|
38
|
+
raise ArgumentError, 'Attributes must be a Hash'
|
39
|
+
end
|
40
|
+
session_attributes(attributes, false)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Add pack of session attributes to existing ones
|
44
|
+
#
|
45
|
+
# @param attributes [Hash] pack of session attributes
|
46
|
+
# @raise [ArgumentError] if given paramter is not a Hash object
|
47
|
+
def merge_session_attributes(attributes)
|
48
|
+
unless attributes.is_a? Hash
|
49
|
+
raise ArgumentError, 'Attributes must be a Hash'
|
50
|
+
end
|
51
|
+
session_attributes(attributes, true)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Add card to response object
|
55
|
+
#
|
56
|
+
# @param params [Hash] card parameters:
|
57
|
+
# type [String] card type, can be "Simple", "Standard" or "LinkAccount"
|
58
|
+
# title [String] card title
|
59
|
+
# content [String] card content (line breaks must be already included)
|
60
|
+
# small_image_url [String] an URL for small card image
|
61
|
+
# large_image_url [String] an URL for large card image
|
62
|
+
# @raise [NoMethodError] if card is not allowed
|
63
|
+
def add_card(params = {})
|
64
|
+
card_exception unless %i[launch intent].include? @req_type
|
65
|
+
card = Card.new(params)
|
66
|
+
@resp[:response][:card] = card.obj
|
40
67
|
end
|
41
68
|
|
42
69
|
# Add AudioPlayer directive
|
43
70
|
#
|
44
71
|
# @param directive [String] audio player directive type,
|
45
72
|
# can be :start or :stop
|
46
|
-
# @param
|
47
|
-
|
73
|
+
# @param params [Hash] optional request parameters:
|
74
|
+
# url [String] streaming URL
|
75
|
+
# token [String] streaming service token
|
76
|
+
# offset [Integer] playback offset
|
77
|
+
def add_audio_player_directive(directive, params = {})
|
48
78
|
player = AudioPlayer.new
|
49
|
-
@directives = [
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
79
|
+
@resp[:response][:directives] = [
|
80
|
+
case directive.to_sym
|
81
|
+
when :start
|
82
|
+
player.play_directive(params)
|
83
|
+
when :stop
|
84
|
+
player.stop_directive
|
85
|
+
end
|
86
|
+
]
|
57
87
|
end
|
58
88
|
|
59
|
-
#
|
89
|
+
# Return JSON version of current response state
|
60
90
|
#
|
61
|
-
# @
|
62
|
-
|
63
|
-
|
64
|
-
@reprompt =
|
65
|
-
if ssml
|
66
|
-
{ outputSpeech: { type: 'SSML', ssml: check_ssml(speech_text) } }
|
67
|
-
else
|
68
|
-
{ outputSpeech: { type: 'PlainText', text: speech_text } }
|
69
|
-
end
|
70
|
-
@reprompt
|
91
|
+
# @return [JSON] response object
|
92
|
+
def json
|
93
|
+
Oj.to_json(@resp)
|
71
94
|
end
|
72
95
|
|
73
|
-
#
|
96
|
+
# Tell something to Alexa user and close conversation.
|
97
|
+
# Method will only add a given speech to response object
|
74
98
|
#
|
75
|
-
# @param
|
76
|
-
# @
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
99
|
+
# @param speech [Sring] output speech
|
100
|
+
# @param reprompt_speech [String] output speech if user remains idle
|
101
|
+
# @param ssml [Boolean] is it an SSML speech or not
|
102
|
+
def tell(speech, reprompt_speech = nil, ssml = false)
|
103
|
+
obj = { outputSpeech: build_speech(speech, ssml) }
|
104
|
+
if reprompt_speech
|
105
|
+
obj[:reprompt] = { outputSpeech: build_speech(reprompt_speech, ssml) }
|
106
|
+
end
|
107
|
+
@resp[:response].merge!(obj)
|
82
108
|
end
|
83
109
|
|
84
|
-
#
|
110
|
+
# Tell something to Alexa user and close conversation.
|
111
|
+
# Method will add given sppech to response object and
|
112
|
+
# immediately return its JSON implementation
|
85
113
|
#
|
86
|
-
# @param speech [
|
87
|
-
# @param
|
114
|
+
# @param speech [Sring] output speech
|
115
|
+
# @param reprompt_speech [String] output speech if user remains idle
|
88
116
|
# @param ssml [Boolean] is it an SSML speech or not
|
89
|
-
|
90
|
-
|
91
|
-
{ outputSpeech:
|
117
|
+
# @return [JSON] ready to use response object
|
118
|
+
def tell!(speech, reprompt_speech = nil, ssml = false)
|
119
|
+
obj = { outputSpeech: build_speech(speech, ssml) }
|
120
|
+
if reprompt_speech
|
121
|
+
obj[:reprompt][:outputSpeech] = build_speech(reprompt_speech, ssml)
|
122
|
+
end
|
123
|
+
@resp[:response].merge!(obj)
|
124
|
+
json
|
92
125
|
end
|
93
126
|
|
94
|
-
#
|
127
|
+
# Ask something from user and wait for further information.
|
128
|
+
# Method will only add given sppech to response object and
|
129
|
+
# set "shouldEndSession" parameter to false
|
95
130
|
#
|
96
|
-
# @param speech [
|
97
|
-
# @param reprompt_speech [String] output
|
98
|
-
# @param
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
speech_ssml = false, reprompt_ssml = false)
|
103
|
-
output_speech = add_speech(speech, speech_ssml)
|
104
|
-
reprompt_speech = add_reprompt(reprompt_speech, reprompt_ssml)
|
105
|
-
{
|
106
|
-
outputSpeech: output_speech,
|
107
|
-
reprompt: reprompt_speech,
|
108
|
-
shouldEndSession: end_session
|
109
|
-
}
|
131
|
+
# @param speech [Sring] output speech
|
132
|
+
# @param reprompt_speech [String] output speech if user remains idle
|
133
|
+
# @param ssml [Boolean] is it an SSML speech or not
|
134
|
+
def ask(speech, reprompt_speech = nil, ssml = false)
|
135
|
+
@resp[:response][:shouldEndSession] = false
|
136
|
+
tell(speech, reprompt_speech, ssml)
|
110
137
|
end
|
111
138
|
|
112
|
-
#
|
113
|
-
#
|
139
|
+
# Ask something from user and wait for further information.
|
140
|
+
# Method will only add given sppech to response object,
|
141
|
+
# set "shouldEndSession" parameter to false and
|
142
|
+
# immediately return response JSON implementation
|
114
143
|
#
|
115
|
-
# @
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
144
|
+
# @param speech [Sring] output speech
|
145
|
+
# @param reprompt_speech [String] output speech if user remains idle
|
146
|
+
# @param ssml [Boolean] is it an SSML speech or not
|
147
|
+
# @return [JSON] ready to use response object
|
148
|
+
def ask!(speech, reprompt_speech = nil, ssml = false)
|
149
|
+
@resp[:response][:shouldEndSession] = false
|
150
|
+
tell!(speech, reprompt_speech, ssml)
|
120
151
|
end
|
121
152
|
|
122
|
-
|
123
|
-
|
124
|
-
#
|
153
|
+
private
|
154
|
+
|
155
|
+
# Add pack of session attributes.
|
156
|
+
# By default all existing session attributes would be overwritten
|
125
157
|
#
|
126
|
-
# @param
|
127
|
-
# @
|
128
|
-
def
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
@response
|
158
|
+
# @param attributes [Hash] pack of session attributes
|
159
|
+
# @param merge [Boolean] merge attributes with existing ones?
|
160
|
+
def session_attributes(attributes, merge)
|
161
|
+
if merge
|
162
|
+
attributes.each { |k, v| add_session_attribute(k, v, true) }
|
163
|
+
else
|
164
|
+
@resp[:sessionAttributes] = {}
|
165
|
+
attributes.each { |k, v| add_session_attribute(k, v, false) }
|
166
|
+
end
|
136
167
|
end
|
137
168
|
|
138
|
-
#
|
139
|
-
# Takes the version, response, should_end_session variables
|
140
|
-
# and builds a JSON object
|
169
|
+
# Build speech object
|
141
170
|
#
|
142
|
-
# @param
|
143
|
-
# @
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
171
|
+
# @param speech [Sring] output speech
|
172
|
+
# @param ssml [Boolean] is it an SSML speech or not
|
173
|
+
# @return [Hash] speech object
|
174
|
+
def build_speech(speech, ssml)
|
175
|
+
obj = {}
|
176
|
+
obj[:type] = ssml ? 'SSML' : 'PlainText'
|
177
|
+
if ssml
|
178
|
+
obj[:ssml] = fix_ssml(speech)
|
179
|
+
else
|
180
|
+
obj[:text] = speech
|
150
181
|
end
|
151
|
-
|
152
|
-
Oj.to_json(response)
|
182
|
+
obj
|
153
183
|
end
|
154
184
|
|
155
|
-
#
|
185
|
+
# Forced fix of SSML speech - manually check and fix open and close tags
|
156
186
|
#
|
157
|
-
# @
|
158
|
-
|
159
|
-
|
160
|
-
|
187
|
+
# @param text [String] SSML response speech
|
188
|
+
# @return [String] fixed SSML speech
|
189
|
+
def fix_ssml(text)
|
190
|
+
open_tag = text.strip[0..6]
|
191
|
+
close_tag = text.strip[-8..1]
|
192
|
+
text = open_tag == '<speak>' ? text : "<speak>#{text}"
|
193
|
+
close_tag == '</speak>' ? text : "#{text}</speak>"
|
161
194
|
end
|
162
195
|
|
163
|
-
|
164
|
-
|
165
|
-
# Check and fix SSML speech
|
196
|
+
# Raise card exception
|
166
197
|
#
|
167
|
-
# @
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
close_tag = ssml.strip[-8..1]
|
172
|
-
ssml = open_tag == '<speak>' ? ssml : "<speak>#{ssml}"
|
173
|
-
close_tag == '</speak>' ? ssml : "#{ssml}</speak>"
|
198
|
+
# @raise [NoMethodError] if card is not allowed
|
199
|
+
def card_exception
|
200
|
+
raise NoMethodError, 'Card can only be included in response ' \
|
201
|
+
'to a "LaunchRequest" or "IntentRequest"'
|
174
202
|
end
|
175
203
|
end
|
176
204
|
end
|
data/lib/alexa_ruby/version.rb
CHANGED