alexa-rails 0.1.1

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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +165 -0
  4. data/Rakefile +36 -0
  5. data/app/assets/config/alexa_manifest.js +2 -0
  6. data/app/assets/javascripts/alexa/application.js +13 -0
  7. data/app/assets/stylesheets/alexa/application.css +15 -0
  8. data/app/controllers/alexa/application_controller.rb +6 -0
  9. data/app/controllers/alexa/intent_handlers_controller.rb +49 -0
  10. data/app/helpers/alexa/application_helper.rb +4 -0
  11. data/app/helpers/alexa/context_helper.rb +23 -0
  12. data/app/helpers/alexa/render_helper.rb +24 -0
  13. data/app/jobs/alexa/application_job.rb +4 -0
  14. data/app/mailers/alexa/application_mailer.rb +6 -0
  15. data/app/models/alexa/application_record.rb +5 -0
  16. data/app/models/alexa/usage.rb +11 -0
  17. data/app/models/alexa/user.rb +17 -0
  18. data/app/views/alexa/_response.json.erb +18 -0
  19. data/app/views/alexa/output/_card.json.erb +13 -0
  20. data/app/views/alexa/output/_ssml.json.erb +7 -0
  21. data/app/views/alexa/output/cards/_ask_for_permissions_consent.json.erb +6 -0
  22. data/app/views/alexa/output/cards/_simple.json.erb +5 -0
  23. data/app/views/alexa/permission_consents/device_address.text.erb +7 -0
  24. data/app/views/layouts/alexa/application.html.erb +14 -0
  25. data/config/routes.rb +3 -0
  26. data/lib/alexa/context.rb +38 -0
  27. data/lib/alexa/device.rb +61 -0
  28. data/lib/alexa/engine.rb +5 -0
  29. data/lib/alexa/intent_handlers/base.rb +158 -0
  30. data/lib/alexa/request.rb +97 -0
  31. data/lib/alexa/response.rb +85 -0
  32. data/lib/alexa/responses/bye.rb +26 -0
  33. data/lib/alexa/responses/delegate.rb +18 -0
  34. data/lib/alexa/responses/permission_consents/device_address.rb +28 -0
  35. data/lib/alexa/session.rb +35 -0
  36. data/lib/alexa/slot.rb +50 -0
  37. data/lib/alexa/version.rb +3 -0
  38. data/lib/alexa-rails.rb +19 -0
  39. data/lib/generators/alexa/migrations_generator.rb +22 -0
  40. data/lib/generators/alexa/templates/create_alexa_usages.rb +13 -0
  41. data/lib/generators/alexa/templates/create_alexa_users.rb +13 -0
  42. data/lib/tasks/alexa_tasks.rake +4 -0
  43. metadata +115 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1885e37fc556e96fc1cc6d08e6643a2643e19bb9
4
+ data.tar.gz: ab032623d44c9c2fbdfa85f75d437ae17dcda44d
5
+ SHA512:
6
+ metadata.gz: 0e86743175bfc943c911c35aa6d079475501ea3a8c3a02958fd749b2358b3445b6172a0bfd3fad58e075470e5bcaa668371caf709ec197a141c40d416019e8ec
7
+ data.tar.gz: 2eee6244d1f954b2b86e03af208a198e5051459857b1578a62ab1c1954c058c08a73798877622a91bc8495a659119db2e861f51563102ea4b94cb5f0888b4f75
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2018
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,165 @@
1
+ # Alexa
2
+ `alexa-rails` is a ruby gem which is a mountable rails engine that will add abilities to your Ruby on Rails application to handle Amazon alexa requests and responses.
3
+
4
+ ## Intallation/Usage
5
+
6
+ Do the usual by adding the following to your Gemfile:
7
+
8
+ ```ruby
9
+ gem install alexa-rails
10
+ ```
11
+
12
+ ### Migrations
13
+
14
+ The gem provides migrations that are needed to use few features of the gem.
15
+ For example: Saving or reading the user's skill usage count.
16
+ To generate the migrations, run the following
17
+
18
+ ```ruby
19
+ $ rails generate alexa:migrations
20
+ $ rake db:migrate
21
+ ```
22
+
23
+ ### Configuration
24
+
25
+ Set alexa skill IDs in environment config (ex: config/environments/development.rb).
26
+
27
+
28
+ ```ruby
29
+ # config/environments/development.rb
30
+
31
+ config.x.alexa.skill_ids = [
32
+ "amzn1.ask.skill.xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx"
33
+ ]
34
+ ```
35
+
36
+ Mount the engine for routes handling in your routes
37
+
38
+ ```ruby
39
+ # config/routes.rb
40
+
41
+ Rails.application.routes.draw do
42
+ ...
43
+ mount Alexa::Engine, at: "/alexa"
44
+ end
45
+ ```
46
+
47
+ ### Request handling
48
+
49
+ After the above steps, your application is ready to accept requests from Alexa
50
+ servers at `/alexa/intent_handlers`.
51
+ You will have to provide that in the HTTPS endpoint URL for your skill.
52
+
53
+ To handle an intent, you will have to create an intent handler class.
54
+ For example, if your intent is named `PlaceOrder`, you will have to create
55
+ the following file under you `app/lib/intent_handlers` directory.
56
+
57
+ ```ruby
58
+ module Alexa
59
+ module IntentHandlers
60
+ class PlaceOrder < Alexa::IntentHandlers::Base
61
+ def handle
62
+ ...
63
+ end
64
+ end
65
+ end
66
+ end
67
+ ```
68
+
69
+ All intent handlers should contain a `#handle` method that has required logic
70
+ as to how to handle the intent request. For example, adding session variables,
71
+ setting response to elicit slots, etc.
72
+
73
+ Adding session variable:
74
+
75
+ ```ruby
76
+ session.merge!(my_var: value)
77
+ ```
78
+
79
+ #### Slot elicitations
80
+
81
+ Depending on your conditions, you can set the reponse to elicit a specific
82
+ slot and the respecitve views are used.
83
+
84
+ ```ruby
85
+ response.elicit_slot!(:SlotName)
86
+ ```
87
+
88
+ ### Views
89
+
90
+ The content for speech and display cards is not set in the intent handler
91
+ classes.
92
+ We follow rails convention and expect all response content for intents to be
93
+ in their respective view files.
94
+
95
+ Also, the views are context locale dependant.
96
+
97
+ Given an intent named `PlaceOrder`, you view files would be
98
+
99
+ * SSML: `views/alexa/en_us/intent_handlers/place_order/speech.ssml.erb`
100
+ * Card: `views/alexa/en_us/intent_handlers/place_order/display.text.erb`
101
+
102
+ In case of slot elicitations, follow a similar convention but make sure you
103
+ name the `ssml` and `text` files with the same name as the slot that is being
104
+ elicited. For example, in the `PlaceOrder` intent, the elicatation for `Address`
105
+ slot would have the following views
106
+
107
+ * SSML: `views/alexa/en_us/intent_handlers/place_order/elicitations/address.ssml.erb`
108
+ * Card: `views/alexa/en_us/intent_handlers/place_order/elicitations/address.text.erb`
109
+
110
+ #### SSML
111
+
112
+ ##### Re-prompts
113
+
114
+ By default, there is no re-prompt SSML is added to the response.
115
+ However, re-prompt SSML can be set in the ssml view of the intent response or
116
+ a slot elicitation view with a `content_for :repromt_ssml` like this:
117
+
118
+ ```erb
119
+ What is your address?
120
+
121
+ <% content_for :repromt_ssml do %>
122
+ Where would you like the pizza to be delivered?
123
+ <% end %>
124
+ ```
125
+
126
+ #### Cards
127
+
128
+ ##### Type & Title
129
+
130
+ By default, the card type is set to `Simple`.
131
+ To change the card type and title, use the `content_for` blocks in the `text`
132
+ view file for the response as follows:
133
+
134
+ ```erb
135
+ <% content_for :card_type do %>
136
+ Simple
137
+ <% end %>
138
+ <% content_for :card_title do %>
139
+ Get your pizza
140
+ <% end %>
141
+
142
+ ```
143
+
144
+ ## Installation
145
+ Add this line to your application's Gemfile:
146
+
147
+ ```ruby
148
+ gem 'alexa'
149
+ ```
150
+
151
+ And then execute:
152
+ ```bash
153
+ $ bundle
154
+ ```
155
+
156
+ Or install it yourself as:
157
+ ```bash
158
+ $ gem install alexa
159
+ ```
160
+
161
+ ## Contributing
162
+ Contribution directions go here.
163
+
164
+ ## License
165
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,36 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Alexa'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+
21
+ load 'rails/tasks/statistics.rake'
22
+
23
+
24
+
25
+ require 'bundler/gem_tasks'
26
+
27
+ require 'rake/testtask'
28
+
29
+ Rake::TestTask.new(:test) do |t|
30
+ t.libs << 'test'
31
+ t.pattern = 'test/**/*_test.rb'
32
+ t.verbose = false
33
+ end
34
+
35
+
36
+ task default: :test
@@ -0,0 +1,2 @@
1
+ //= link_directory ../javascripts/alexa .js
2
+ //= link_directory ../stylesheets/alexa .css
@@ -0,0 +1,13 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // compiled file. JavaScript code in this file should be added after the last require_* statement.
9
+ //
10
+ // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require_tree .
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,6 @@
1
+ module Alexa
2
+ class ApplicationController < ActionController::Base
3
+ protect_from_forgery with: :exception
4
+
5
+ end
6
+ end
@@ -0,0 +1,49 @@
1
+ require_dependency "alexa/application_controller"
2
+
3
+ module Alexa
4
+ class IntentHandlersController < ApplicationController
5
+ include Alexa::ContextHelper
6
+ include Alexa::RenderHelper
7
+ skip_before_action :verify_authenticity_token
8
+
9
+ def create
10
+ @resp = nil
11
+ if alexa_request.valid?
12
+ if alexa_request.intent_request?
13
+ case alexa_request.intent_name
14
+ when 'AMAZON.CancelIntent'
15
+ @resp = Alexa::IntentHandlers::GoodBye.new(alexa_context).handle
16
+ when 'AMAZON.StopIntent'
17
+ @resp = Alexa::IntentHandlers::GoodBye.new(alexa_context).handle
18
+ when 'AMAZON.HelpIntent'
19
+ @resp = Alexa::IntentHandlers::Help.new(alexa_context).handle
20
+ else
21
+ @resp = "Alexa::IntentHandlers::#{alexa_request.intent_name}"
22
+ .constantize
23
+ .new(alexa_context)
24
+ .handle
25
+ end
26
+ elsif alexa_request.launch_request?
27
+ @resp = Alexa::IntentHandlers::LaunchApp.new(alexa_context).handle
28
+ elsif alexa_request.session_ended_request?
29
+ @resp = Alexa::IntentHandlers::SessionEnd.new(alexa_context).handle
30
+ end
31
+ end
32
+
33
+ alexa_response = @resp
34
+ respond_for_alexa_with(alexa_response)
35
+ end
36
+
37
+ helper_method def intent
38
+ @resp.intent
39
+ end
40
+
41
+ helper_method def slots
42
+ intent.slots
43
+ end
44
+
45
+ helper_method def context
46
+ alexa_context
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,4 @@
1
+ module Alexa
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,23 @@
1
+ module Alexa
2
+ module ContextHelper
3
+ def alexa_response
4
+ @_alexa_response
5
+ end
6
+
7
+ def alexa_response=(resp)
8
+ @_alexa_response = resp
9
+ end
10
+
11
+ def context_alexa_user
12
+ alexa_context.user
13
+ end
14
+
15
+ def alexa_context
16
+ @_alexa_context ||= Alexa::Context.new(alexa_request)
17
+ end
18
+
19
+ def alexa_request
20
+ @_alexa_request ||= Alexa::Request.new(request)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,24 @@
1
+ module Alexa
2
+ module RenderHelper
3
+ def respond_for_alexa_with(alexa_response)
4
+ if alexa_response.nil?
5
+ render :not_found
6
+ else
7
+ # render json: alexa_response
8
+ if alexa_response.is_a?(Alexa::Responses::Delegate)
9
+ render json: alexa_response
10
+ else
11
+ render partial: 'alexa/response.json', locals: { response: alexa_response }
12
+ end
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def render(*args)
19
+ options = args.extract_options!
20
+ options[:template] = "/app/views/"
21
+ super(*(args << options))
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,4 @@
1
+ module Alexa
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module Alexa
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: 'from@example.com'
4
+ layout 'mailer'
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module Alexa
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,11 @@
1
+ module Alexa
2
+ class Usage < ActiveRecord::Base
3
+ self.table_name = 'alexa_usages'
4
+ belongs_to :alexa_user
5
+
6
+ def increment_count!
7
+ self.count = self.count + 1
8
+ save!
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ module Alexa
2
+ class User < ActiveRecord::Base
3
+ self.table_name = 'alexa_users'
4
+ has_many :usages, class_name: "Alexa::Usage", foreign_key: "alexa_user_id"
5
+
6
+ def update_usage(intent_name:)
7
+ usage = usages.where(intent_name: intent_name).first_or_initialize
8
+ usage.increment_count!
9
+ end
10
+
11
+ def usage_count_for(intent_name:)
12
+ usage = usages.where(intent_name: intent_name).first
13
+ return 0 if usage.nil?
14
+ usage.count
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ {
2
+ "version": "1.0",
3
+ "sessionAttributes": <%= raw (response.intent.session || {}).to_json %>,
4
+ "response": {
5
+ "directives": <%= raw response.directives.to_json %>,
6
+ "outputSpeech": <%= raw render(partial: 'alexa/output/ssml', locals: { response: response }) %>,
7
+ "card": <%= raw render(partial: 'alexa/output/card', locals: { response: response }) %>,
8
+ <% if content_for(:repromt_ssml).present? %>
9
+ "reprompt": {
10
+ "outputSpeech": {
11
+ "type": "SSML",
12
+ "ssml": <%= raw content_for(:repromt_ssml).strip.gsub(/\n/, "").to_json %>
13
+ }
14
+ },
15
+ <% end %>
16
+ "shouldEndSession": <%= response.end_session? %>
17
+ }
18
+ }
@@ -0,0 +1,13 @@
1
+ <% content = render(
2
+ file: response.partial_path(format: :text),
3
+ locals: { response: response }
4
+ ) %>
5
+ <% card_type = (content_for(:card_type) || "Simple").strip %>
6
+
7
+ <%= raw render(
8
+ partial: "alexa/output/cards/#{card_type.downcase}",
9
+ locals: {
10
+ response: response,
11
+ content: content
12
+ }
13
+ ) %>
@@ -0,0 +1,7 @@
1
+ {
2
+ "type": "SSML",
3
+ "ssml": "<%= raw render(
4
+ file: response.partial_path(format: :ssml),
5
+ locals: { response: response }
6
+ ).gsub(/\n/, "").gsub(/\"/, '\"') %>"
7
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "type": "AskForPermissionsConsent",
3
+ "permissions": [
4
+ "<%= content_for(:permissions_scope).strip %>"
5
+ ]
6
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "type": "Simple",
3
+ "title": "<%= (content_for(:card_title) || "Card Title").strip %>",
4
+ "content": <%= raw content.strip.to_json %>
5
+ }
@@ -0,0 +1,7 @@
1
+ <% content_for :card_type do %>
2
+ ask_for_permissions_consent
3
+ <% end %>
4
+
5
+ <% content_for :permissions_scope do %>
6
+ read::alexa:device:all:address
7
+ <% end %>
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Alexa</title>
5
+ <%= stylesheet_link_tag "alexa/application", media: "all" %>
6
+ <%= javascript_include_tag "alexa/application" %>
7
+ <%= csrf_meta_tags %>
8
+ </head>
9
+ <body>
10
+
11
+ <%= yield %>
12
+
13
+ </body>
14
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,3 @@
1
+ Alexa::Engine.routes.draw do
2
+ resources :intent_handlers, only: [:create]
3
+ end
@@ -0,0 +1,38 @@
1
+ module Alexa
2
+ class Context
3
+ attr_accessor :request
4
+
5
+ def initialize(alexa_request)
6
+ @request = alexa_request
7
+ end
8
+
9
+ def user
10
+ @_user ||= Alexa::User.where(
11
+ amazon_id: request.user_id
12
+ ).first_or_create
13
+ end
14
+
15
+ def session
16
+ request.session
17
+ end
18
+
19
+ def locale
20
+ request.locale
21
+ end
22
+
23
+ def device
24
+ @_device ||= Alexa::Device.new(
25
+ attributes: request.params["context"]["System"]["device"],
26
+ context: self
27
+ )
28
+ end
29
+
30
+ def api_endpoint
31
+ request.params["context"]["System"]["apiEndpoint"]
32
+ end
33
+
34
+ def api_access_token
35
+ request.params["context"]["System"]["apiAccessToken"]
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,61 @@
1
+ ##
2
+ # This class represents the +device+ part in the request.
3
+ #
4
+ module Alexa
5
+ class Device
6
+ attr_accessor :attributes
7
+ def initialize(attributes: {}, context:)
8
+ @attributes = attributes
9
+ @context = context
10
+ end
11
+
12
+ ##
13
+ # Return device id
14
+ def id
15
+ attributes["deviceId"]
16
+ end
17
+
18
+ def audio_supported?
19
+ attributes["supportedInterfaces"].keys.include?("AudioPlayer")
20
+ end
21
+
22
+ def video_supported?
23
+ attributes["supportedInterfaces"].keys.include?("VideoApp")
24
+ end
25
+
26
+ ##
27
+ # Return device location from amazon.
28
+ # Makes an API to amazon alexa's device location service and returns the
29
+ # location hash
30
+ def location
31
+ @_location ||= get_location
32
+ end
33
+
34
+ private
35
+
36
+ def get_location
37
+ url = "#{@context.api_endpoint}/v1/devices/#{id}/settings/address"
38
+ conn = Faraday.new(url: url) do |conn|
39
+ conn.options["open_timeout"] = 2
40
+ conn.options["timeout"] = 3
41
+ conn.adapter :net_http
42
+ conn.headers["Authorization"] = "Bearer #{@context.api_access_token}"
43
+ end
44
+ begin
45
+ resp = conn.get
46
+ if resp.status == 200
47
+ return JSON.parse(resp.body)
48
+ end
49
+ rescue Faraday::ConnectionFailed, JSON::ParserError => e
50
+ Raven.capture_exception(
51
+ e,
52
+ extra: {
53
+ deviceId: id,
54
+ apiEndPoint: @context.api_endpoint
55
+ }
56
+ )
57
+ return {}
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,5 @@
1
+ module Alexa
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Alexa
4
+ end
5
+ end