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