alexa-rails 0.1.1

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