├── VERSION ├── test ├── test_helper.exs ├── phoenix_alexa_test.exs └── phoenix_alexa │ ├── validate_application_id_test.exs │ ├── request_test.exs │ ├── response_test.exs │ └── phoenix_alexa_controller_test.exs ├── .gitignore ├── lib ├── request │ ├── application.ex │ ├── intent.ex │ ├── user.ex │ ├── launch_request.ex │ ├── session_ended_request.ex │ ├── request.ex │ ├── intent_request.ex │ ├── generic_request.ex │ └── session.ex ├── response │ ├── ssml_output_speech.ex │ ├── text_output_speech.ex │ ├── simple_card.ex │ ├── link_account_card.ex │ ├── standard_card.ex │ └── response.ex ├── validate_application_id_plug.ex └── phoenix_alexa_controller.ex ├── .travis.yml ├── mix.exs ├── LICENSE └── README.md /VERSION: -------------------------------------------------------------------------------- 1 | 0.2.0 2 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/rel 2 | **/_build 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | -------------------------------------------------------------------------------- /test/phoenix_alexa_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixAlexa.Test do 2 | use ExUnit.Case 3 | 4 | end -------------------------------------------------------------------------------- /lib/request/application.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixAlexa.Application do 2 | 3 | defstruct applicationId: "" 4 | end -------------------------------------------------------------------------------- /lib/request/intent.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixAlexa.Intent do 2 | 3 | defstruct name: nil, 4 | slots: %{} 5 | end -------------------------------------------------------------------------------- /lib/request/user.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixAlexa.User do 2 | 3 | defstruct userId: "", 4 | accessToken: "" 5 | end -------------------------------------------------------------------------------- /lib/response/ssml_output_speech.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixAlexa.SsmlOutputSpeech do 2 | 3 | defstruct type: "SSML", 4 | ssml: nil 5 | end -------------------------------------------------------------------------------- /lib/response/text_output_speech.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixAlexa.TextOutputSpeech do 2 | 3 | defstruct type: "PlainText", 4 | text: nil 5 | end -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | elixir: 1.2.1 3 | otp_release: 4 | - 18.0 5 | sudo: false 6 | before_script: 7 | - mix deps.get --only test 8 | script: 9 | - mix test 10 | -------------------------------------------------------------------------------- /lib/request/launch_request.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixAlexa.LaunchRequest do 2 | 3 | defstruct type: "LaunchRequest", 4 | requestId: "", 5 | timestamp: "" 6 | 7 | end -------------------------------------------------------------------------------- /lib/request/session_ended_request.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixAlexa.SessionEndedRequest do 2 | 3 | defstruct type: "SessionEndedRequest", 4 | requestId: nil, 5 | timestamp: nil, 6 | reason: nil 7 | 8 | end -------------------------------------------------------------------------------- /lib/request/request.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixAlexa.Request do 2 | 3 | alias PhoenixAlexa.{Session, GenericRequest} 4 | 5 | defstruct version: "", 6 | session: %Session{}, 7 | request: %GenericRequest{} 8 | 9 | end -------------------------------------------------------------------------------- /lib/request/intent_request.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixAlexa.IntentRequest do 2 | alias PhoenixAlexa.Intent 3 | 4 | defstruct type: "IntentRequest", 5 | requestId: "", 6 | timestamp: "", 7 | intent: %Intent{} 8 | end -------------------------------------------------------------------------------- /lib/request/generic_request.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixAlexa.GenericRequest do 2 | alias PhoenixAlexa.Intent 3 | 4 | defstruct type: nil, 5 | requestId: nil, 6 | timestamp: nil, 7 | reason: nil, 8 | intent: %Intent{} 9 | end -------------------------------------------------------------------------------- /lib/request/session.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixAlexa.Session do 2 | 3 | alias PhoenixAlexa.{Application, User} 4 | 5 | defstruct new: false, 6 | sessionId: "", 7 | application: %Application{}, 8 | attributes: nil, 9 | user: %User{} 10 | end -------------------------------------------------------------------------------- /lib/response/simple_card.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixAlexa.SimpleCard do 2 | alias PhoenixAlexa.SimpleCard 3 | 4 | defstruct type: "Simple", 5 | title: "", 6 | content: "" 7 | 8 | def set_title(card, title) do 9 | %SimpleCard{card | title: title} 10 | end 11 | def set_content(card, content) do 12 | %SimpleCard{card | content: content} 13 | end 14 | end -------------------------------------------------------------------------------- /lib/response/link_account_card.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixAlexa.LinkAccountCard do 2 | alias PhoenixAlexa.LinkAccountCard 3 | 4 | defstruct type: "LinkAccount", 5 | title: "", 6 | content: "" 7 | 8 | def set_title(card, title) do 9 | %LinkAccountCard{card | title: title} 10 | end 11 | def set_content(card, content) do 12 | %LinkAccountCard{card | content: content} 13 | end 14 | end -------------------------------------------------------------------------------- /lib/validate_application_id_plug.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixAlexa.ValidateApplicationId do 2 | import Plug.Conn 3 | 4 | def init(applicationId), do: applicationId 5 | 6 | def call(conn, applicationId) do 7 | case conn.body_params["session"]["application"]["applicationId"] do 8 | ^applicationId -> 9 | conn 10 | _ -> 11 | conn 12 | |> Plug.Conn.send_resp(400, ~s({"error": "Invalid application"})) 13 | |> halt 14 | end 15 | end 16 | end -------------------------------------------------------------------------------- /lib/response/standard_card.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixAlexa.StandardCard do 2 | alias PhoenixAlexa.StandardCard 3 | 4 | defstruct type: "Standard", 5 | title: "", 6 | text: "" 7 | 8 | def set_title(card, title) do 9 | %StandardCard{card | title: title} 10 | end 11 | def set_text(card, text) do 12 | %StandardCard{card | text: text} 13 | end 14 | def set_small_image_url(card, image_url) do 15 | image = Map.get(card, :image) || %{} 16 | image = Map.put(image, :smallImageUrl, image_url) 17 | Map.put(card, :image, image) 18 | end 19 | def set_large_image_url(card, image_url) do 20 | image = Map.get(card, :image) || %{} 21 | image = Map.put(image, :largeImageUrl, image_url) 22 | Map.put(card, :image, image) 23 | end 24 | end -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixAlexa.Mixfile do 2 | use Mix.Project 3 | 4 | @version File.read!("VERSION") |> String.strip 5 | 6 | def project do 7 | [app: :phoenix_alexa, 8 | version: @version, 9 | elixir: "~> 1.2", 10 | description: "Alexa library for Phoenix", 11 | deps: deps, 12 | package: package, 13 | consolidate_protocols: Mix.env != :test] 14 | end 15 | 16 | def application do 17 | [applications: []] 18 | end 19 | 20 | defp deps do 21 | [{:poison, "~> 2.0"}, 22 | {:plug, "~> 1.1"}, 23 | ] 24 | end 25 | 26 | defp package do 27 | [files: ~w(lib test mix.exs README.md LICENSE VERSION), 28 | maintainers: ["Gabi Zuniga"], 29 | licenses: ["MIT"], 30 | links: %{"GitHub" => "https://github.com/gabiz/phoenix_alexa"}] 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Gabi Zuniga 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /test/phoenix_alexa/validate_application_id_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixAlexa.ValidateApplicationIdTest do 2 | use ExUnit.Case 3 | use Plug.Test 4 | 5 | alias PhoenixAlexa.ValidateApplicationId 6 | 7 | test "valid application id" do 8 | conn = conn(:post, "/alexa", "%{}") 9 | |> Map.put(:body_params, %{"session" => %{"application" => %{"applicationId" => "amzn1.echo-sdk-ams.app.05dcb1a4-cb45-46c5-a30e-bb3033a0770a"}}}) 10 | options = ValidateApplicationId.init("amzn1.echo-sdk-ams.app.05dcb1a4-cb45-46c5-a30e-bb3033a0770a") 11 | assert conn == ValidateApplicationId.call(conn, options) 12 | end 13 | 14 | test "invalid application id" do 15 | conn = conn(:post, "/alexa", "%{}") 16 | |> Map.put(:body_params, %{"session" => %{"application" => %{"applicationId" => "__invalid_application_id__"}}}) 17 | options = ValidateApplicationId.init("amzn1.echo-sdk-ams.app.05dcb1a4-cb45-46c5-a30e-bb3033a0770a") 18 | conn = ValidateApplicationId.call(conn, options) 19 | assert conn.state == :sent 20 | assert conn.status == 400 21 | assert conn.resp_body == ~s({"error": "Invalid application"}) 22 | assert conn.halted 23 | end 24 | end -------------------------------------------------------------------------------- /lib/phoenix_alexa_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixAlexa.Controller do 2 | 3 | defmacro __using__(method) do 4 | quote do 5 | import PhoenixAlexa.{Controller, Response} 6 | alias PhoenixAlexa.{Request, Response, TextOutputSpeech, SsmlOutputSpeech} 7 | alias PhoenixAlexa.{SimpleCard, StandardCard, LinkAccountCard} 8 | 9 | def set_response(conn, status \\ 200, response) do 10 | conn 11 | |> Plug.Conn.put_resp_content_type("application/json") 12 | |> Plug.Conn.resp(status, Poison.encode!(response)) 13 | end 14 | 15 | def handle_request(conn, request) do 16 | case request.request.type do 17 | "LaunchRequest" -> 18 | launch_request(conn, request) 19 | "IntentRequest" -> 20 | intent_request(conn, request.request.intent.name, request) 21 | "SessionEndedRequest" -> 22 | session_ended_request(conn, request) 23 | |> set_response(%{}) 24 | end 25 | |> Plug.Conn.send_resp() 26 | end 27 | 28 | def unquote(method)(conn, params) do 29 | case Poison.Decode.decode(params, as: %PhoenixAlexa.Request{}) do 30 | %PhoenixAlexa.Request{} = request -> handle_request(conn, request) 31 | _ -> 32 | conn 33 | |> Plug.Conn.put_resp_content_type("application/json") 34 | |> Plug.Conn.send_resp(500, Poison.encode!(%{error: "Internal Error"})) 35 | end 36 | 37 | end 38 | 39 | def launch_request(conn, _request) do 40 | conn |> set_response(%Response{}) 41 | end 42 | 43 | def session_ended_request(conn, request) do 44 | conn 45 | end 46 | 47 | def intent_request(conn, _, request) do 48 | conn |> set_response(%Response{}) 49 | end 50 | 51 | defoverridable [launch_request: 2, intent_request: 3, session_ended_request: 2] 52 | 53 | end 54 | end 55 | 56 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Alexa library for Phoenix 2 | 3 | [![Build Status](https://api.travis-ci.org/gabiz/phoenix_alexa.svg)](https://travis-ci.org/gabiz/phoenix_alexa) 4 | 5 | ## Usage 6 | 7 | Add phoenix_alexa as a dependency in your `mix.exs` file. 8 | 9 | ```elixir 10 | def deps do 11 | [ { :phoenix_alexa, "~> 0.2.0" } ] 12 | end 13 | ``` 14 | 15 | Update a route with a post request into your alexa controller. 16 | 17 | ```elixir 18 | scope "/", HelloAlexa do 19 | pipe_through :api 20 | 21 | post "/", AlexaController, :post 22 | end 23 | 24 | ``` 25 | 26 | In the controller add a use statement for `PhoenixAlexa.Controller` and define functions for `launch_request`, `session_end_request` and `intent_request` as follows: 27 | 28 | ```elixir 29 | 30 | defmodule HelloPhoenixAlexa.AlexaController do 31 | use HelloPhoenixAlexa.Web, :controller 32 | use PhoenixAlexa.Controller, :post # param should match route name 33 | 34 | def launch_request(conn, request) do 35 | response = %Response{} 36 | |> set_output_speech(%TextOutputSpeech{text: "Welcome to the Horoscope."}) 37 | 38 | conn 39 | |> set_response(response) 40 | end 41 | 42 | def session_end_request(conn, request) do 43 | conn 44 | end 45 | 46 | def intent_request(conn, "GetHoroscope", request) do 47 | response = case request.request.intent.slots["Sign"]["value"] do 48 | "Libra" -> 49 | card = %SimpleCard{} 50 | |> set_title("Get Horoscope") 51 | |> set_content("You are going to have an unexpected event today.") 52 | 53 | %Response{} 54 | |> set_output_speech(%TextOutputSpeech{text: "You are going to have an unexpected event today."}) 55 | |> set_card(card) 56 | |> set_session_attributes(%{my_key: "my_data"}) 57 | |> set_should_end_session(true) 58 | _ -> 59 | %Response{} 60 | |> set_output_speech(%TextOutputSpeech{text: "You are going to meet an interesting person."}) 61 | |> set_should_end_session(true) 62 | end 63 | 64 | conn |> set_response(response) 65 | end 66 | end 67 | 68 | ``` 69 | 70 | To authenticate that the request corresponds to your Alexa application add the ValidateApplicationId plug to your router as follows: 71 | 72 | ```elixir 73 | pipeline :api do 74 | plug :accepts, ["json"] 75 | plug ValidateApplicationId, "amzn1.echo-sdk-ams.app.05dcb1a4-cb45-46c5-a30e-bb3033a0770a" 76 | end 77 | ``` 78 | -------------------------------------------------------------------------------- /lib/response/response.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixAlexa.Response do 2 | alias PhoenixAlexa.{Response, TextOutputSpeech, SsmlOutputSpeech} 3 | alias PhoenixAlexa.{SimpleCard, StandardCard, LinkAccountCard} 4 | 5 | defstruct version: "1.0", 6 | sessionAttributes: %{}, 7 | response: %{ 8 | shouldEndSession: false 9 | } 10 | 11 | # def session_attributes(response) do 12 | # response[:sessionAttributes] 13 | # end 14 | 15 | def set_session_attributes(response, sessionAttributes) do 16 | # response = response || %{} 17 | # Map.put(response, :sessionAttributes, sessionAttributes) 18 | %Response{response | sessionAttributes: sessionAttributes} 19 | end 20 | 21 | def set_output_speech(response, %TextOutputSpeech{} = outputspeech) do 22 | %Response{response | response: (response.response |> Map.put(:outputSpeech, outputspeech))} 23 | end 24 | def set_output_speech(response, %SsmlOutputSpeech{} = outputspeech) do 25 | %Response{response | response: (response.response |> Map.put(:outputSpeech, outputspeech))} 26 | end 27 | 28 | def set_card(response, %SimpleCard{} = card) do 29 | %Response{response | response: (response.response |> Map.put(:card, card))} 30 | end 31 | def set_card(response, %StandardCard{} = card) do 32 | %Response{response | response: (response.response |> Map.put(:card, card))} 33 | end 34 | def set_card(response, %LinkAccountCard{} = card) do 35 | %Response{response | response: (response.response |> Map.put(:card, card))} 36 | end 37 | 38 | def set_reprompt(response, %TextOutputSpeech{} = outputspeech) do 39 | %Response{response | response: (response.response |> Map.put(:reprompt, %{outputSpeech: outputspeech}))} 40 | end 41 | def set_reprompt(response, %SsmlOutputSpeech{} = outputspeech) do 42 | %Response{response | response: (response.response |> Map.put(:reprompt, %{outputSpeech: outputspeech}))} 43 | end 44 | 45 | def set_should_end_session(response, shouldEndSession) do 46 | %Response{response | response: %{response.response | shouldEndSession: shouldEndSession}} 47 | end 48 | 49 | # Card helpers 50 | 51 | def set_title(%SimpleCard{} = card, title) do 52 | SimpleCard.set_title(card, title) 53 | end 54 | def set_title(%StandardCard{} = card, title) do 55 | StandardCard.set_title(card, title) 56 | end 57 | def set_title(%LinkAccountCard{} = card, title) do 58 | LinkAccountCard.set_title(card, title) 59 | end 60 | 61 | def set_content(%SimpleCard{} = card, content) do 62 | SimpleCard.set_content(card, content) 63 | end 64 | def set_content(%LinkAccountCard{} = card, content) do 65 | LinkAccountCard.set_content(card, content) 66 | end 67 | 68 | def set_text(%StandardCard{} = card, text) do 69 | StandardCard.set_text(card, text) 70 | end 71 | 72 | def set_small_image_url(%StandardCard{} = card, image_url) do 73 | StandardCard.set_small_image_url(card, image_url) 74 | end 75 | 76 | def set_large_image_url(%StandardCard{} = card, image_url) do 77 | StandardCard.set_large_image_url(card, image_url) 78 | end 79 | 80 | end -------------------------------------------------------------------------------- /test/phoenix_alexa/request_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixAlexa.RequestTest do 2 | use ExUnit.Case 3 | 4 | alias PhoenixAlexa.{Session, IntentRequest} 5 | 6 | test "session" do 7 | json_session = """ 8 | { 9 | "new": false, 10 | "sessionId": "amzn1.echo-api.session.0000000-0000-0000-0000-00000000000", 11 | "application": { 12 | "applicationId": "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe" 13 | }, 14 | "attributes": { 15 | "supportedHoroscopePeriods": { 16 | "daily": true, 17 | "weekly": false, 18 | "monthly": false 19 | } 20 | }, 21 | "user": { 22 | "userId": "amzn1.account.AM3B00000000000000000000000" 23 | } 24 | } 25 | """ 26 | session = Poison.decode!(json_session, as: %Session{}) 27 | 28 | assert session.new == false 29 | assert session.sessionId == "amzn1.echo-api.session.0000000-0000-0000-0000-00000000000" 30 | assert session.application.applicationId == "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe" 31 | assert session.attributes == %{ 32 | "supportedHoroscopePeriods" => %{ 33 | "daily" => true, 34 | "weekly" => false, 35 | "monthly" => false 36 | } 37 | } 38 | assert session.user.userId == "amzn1.account.AM3B00000000000000000000000" 39 | end 40 | 41 | test "request" do 42 | json_request = """ 43 | { 44 | "type": "IntentRequest", 45 | "requestId": "amzn1.echo-api.request.0000000-0000-0000-0000-00000000000", 46 | "timestamp": "2015-05-13T12:34:56Z", 47 | "intent": { 48 | "name": "GetZodiacHoroscopeIntent", 49 | "slots": { 50 | "ZodiacSign": { 51 | "name": "ZodiacSign", 52 | "value": "virgo" 53 | } 54 | } 55 | } 56 | } 57 | """ 58 | 59 | request = Poison.decode!(json_request, as: %IntentRequest{}) 60 | 61 | assert request.type == "IntentRequest" 62 | assert request.requestId == "amzn1.echo-api.request.0000000-0000-0000-0000-00000000000" 63 | assert request.timestamp == "2015-05-13T12:34:56Z" 64 | assert request.intent.name == "GetZodiacHoroscopeIntent" 65 | assert request.intent.slots["ZodiacSign"]["name"] == "ZodiacSign" 66 | assert request.intent.slots["ZodiacSign"]["value"] == "virgo" 67 | end 68 | 69 | end 70 | 71 | 72 | 73 | # { 74 | # "version": "1.0", 75 | # "session": { 76 | # "new": false, 77 | # "sessionId": "amzn1.echo-api.session.0000000-0000-0000-0000-00000000000", 78 | # "application": { 79 | # "applicationId": "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe" 80 | # }, 81 | # "attributes": { 82 | # "supportedHoroscopePeriods": { 83 | # "daily": true, 84 | # "weekly": false, 85 | # "monthly": false 86 | # } 87 | # }, 88 | # "user": { 89 | # "userId": "amzn1.account.AM3B00000000000000000000000" 90 | # } 91 | # }, 92 | # "request": { 93 | # "type": "IntentRequest", 94 | # "requestId": " amzn1.echo-api.request.0000000-0000-0000-0000-00000000000", 95 | # "timestamp": "2015-05-13T12:34:56Z", 96 | # "intent": { 97 | # "name": "GetZodiacHoroscopeIntent", 98 | # "slots": { 99 | # "ZodiacSign": { 100 | # "name": "ZodiacSign", 101 | # "value": "virgo" 102 | # } 103 | # } 104 | # } 105 | # } 106 | # } -------------------------------------------------------------------------------- /test/phoenix_alexa/response_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixAlexa.ResponseTest do 2 | use ExUnit.Case 3 | alias PhoenixAlexa.{Response, TextOutputSpeech, SsmlOutputSpeech} 4 | alias PhoenixAlexa.{SimpleCard, StandardCard, LinkAccountCard} 5 | import PhoenixAlexa.Response 6 | 7 | test "empty response" do 8 | response = %Response{} 9 | |> Poison.encode! 10 | |> Poison.decode! 11 | assert response == %{ 12 | "response" => %{ 13 | "shouldEndSession" => false 14 | }, 15 | "sessionAttributes" => %{}, 16 | "version" => "1.0" 17 | } 18 | end 19 | 20 | test "set_output_speech with TextOutputSpeech" do 21 | response = %Response{} 22 | |> set_output_speech(%TextOutputSpeech{text: "Say hello"}) 23 | |> Poison.encode! 24 | |> Poison.decode! 25 | assert response == %{ 26 | "response" => %{ 27 | "outputSpeech" => %{ 28 | "text" => "Say hello", 29 | "type" => "PlainText" 30 | }, 31 | "shouldEndSession" => false 32 | }, 33 | "sessionAttributes" => %{}, 34 | "version" => "1.0" 35 | } 36 | end 37 | 38 | test "set_output_speech with SsmlOutputSpeech" do 39 | response = %Response{} 40 | |> set_output_speech(%SsmlOutputSpeech{ssml: "This output speech uses SSML."}) 41 | |> Poison.encode! 42 | |> Poison.decode! 43 | assert response == %{ 44 | "response" => %{ 45 | "outputSpeech" => %{ 46 | "ssml" => "This output speech uses SSML.", 47 | "type" => "SSML" 48 | }, 49 | "shouldEndSession" => false 50 | }, 51 | "sessionAttributes" => %{}, 52 | "version" => "1.0" 53 | } 54 | end 55 | 56 | test "set_card with SimpleCard" do 57 | card = %SimpleCard{} 58 | |> set_title("Title") 59 | |> set_content("Content") 60 | response = %Response{} 61 | |> set_card(card) 62 | |> Poison.encode! 63 | |> Poison.decode! 64 | assert response == %{ 65 | "response" => %{ 66 | "card" => %{ 67 | "content" => "Content", 68 | "title" => "Title", 69 | "type" => "Simple" 70 | }, 71 | "shouldEndSession" => false 72 | }, 73 | "sessionAttributes" => %{}, 74 | "version" => "1.0" 75 | } 76 | end 77 | 78 | test "set_card with StandardCard" do 79 | card = %StandardCard{} 80 | |> set_title("Title") 81 | |> set_text("Text") 82 | |> set_small_image_url("http://small_image_url") 83 | |> set_large_image_url("http://large_image_url") 84 | response = %Response{} 85 | |> set_card(card) 86 | |> Poison.encode! 87 | |> Poison.decode! 88 | assert response == %{ 89 | "response" => %{ 90 | "card" => %{ 91 | "image" => %{ 92 | "smallImageUrl" => "http://small_image_url", 93 | "largeImageUrl" => "http://large_image_url" 94 | }, 95 | "text" => "Text", 96 | "title" => "Title", 97 | "type" => "Standard" 98 | }, 99 | "shouldEndSession" => false 100 | }, 101 | "sessionAttributes" => %{}, 102 | "version" => "1.0" 103 | } 104 | end 105 | 106 | test "set_card with StandardCard no images" do 107 | card = %StandardCard{} 108 | |> set_title("Title") 109 | |> set_text("Text") 110 | response = %Response{} 111 | |> set_card(card) 112 | |> Poison.encode! 113 | |> Poison.decode! 114 | assert response == %{ 115 | "response" => %{ 116 | "card" => %{ 117 | "text" => "Text", 118 | "title" => "Title", 119 | "type" => "Standard" 120 | }, 121 | "shouldEndSession" => false 122 | }, 123 | "sessionAttributes" => %{}, 124 | "version" => "1.0" 125 | } 126 | end 127 | 128 | test "set_card with LinkAccountCard" do 129 | card = %LinkAccountCard{} 130 | |> set_title("Title") 131 | |> set_content("Content") 132 | response = %Response{} 133 | |> set_card(card) 134 | |> Poison.encode! 135 | |> Poison.decode! 136 | assert response == %{ 137 | "response" => %{ 138 | "card" => %{ 139 | "content" => "Content", 140 | "title" => "Title", 141 | "type" => "LinkAccount" 142 | }, 143 | "shouldEndSession" => false 144 | }, 145 | "sessionAttributes" => %{}, 146 | "version" => "1.0" 147 | } 148 | end 149 | 150 | test "set_reprompt with TextOutputSpeech" do 151 | response = %Response{} 152 | |> set_reprompt(%TextOutputSpeech{text: "Say hello"}) 153 | |> Poison.encode! 154 | |> Poison.decode! 155 | assert response == %{ 156 | "response" => %{ 157 | "reprompt" => %{ 158 | "outputSpeech" => %{ 159 | "text" => "Say hello", 160 | "type" => "PlainText" 161 | }, 162 | }, 163 | "shouldEndSession" => false 164 | }, 165 | "sessionAttributes" => %{}, 166 | "version" => "1.0" 167 | } 168 | end 169 | 170 | test "set_reprompt with SsmlOutputSpeech" do 171 | response = %Response{} 172 | |> set_reprompt(%SsmlOutputSpeech{ssml: "This output speech uses SSML."}) 173 | |> Poison.encode! 174 | |> Poison.decode! 175 | assert response == %{ 176 | "response" => %{ 177 | "reprompt" => %{ 178 | "outputSpeech" => %{ 179 | "ssml" => "This output speech uses SSML.", 180 | "type" => "SSML" 181 | }, 182 | }, 183 | "shouldEndSession" => false 184 | }, 185 | "sessionAttributes" => %{}, 186 | "version" => "1.0" 187 | } 188 | end 189 | 190 | test "set_should_end_session" do 191 | response = %Response{} 192 | |> set_should_end_session(true) 193 | |> Poison.encode! 194 | |> Poison.decode! 195 | assert response == %{ 196 | "response" => %{ 197 | "shouldEndSession" => true}, 198 | "sessionAttributes" => %{}, 199 | "version" => "1.0" 200 | } 201 | end 202 | end -------------------------------------------------------------------------------- /test/phoenix_alexa/phoenix_alexa_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixAlexa.ControllerTest do 2 | use ExUnit.Case 3 | use Plug.Test 4 | 5 | defmodule HoroscopeController do 6 | use PhoenixAlexa.Controller, :post 7 | 8 | def launch_request(conn, request) do 9 | assert request.version == "1.0" 10 | assert request.session.new == false 11 | assert request.session.sessionId == "SessionId.80ef8951-172e-4f02-ace8-a7ec847e2d9f" 12 | assert request.session.application.applicationId == "amzn1.echo-sdk-ams.app.05dcb1a4-cb45-46c5-a30e-bb3033a0770a" 13 | assert request.session.attributes == %{} 14 | assert request.session.user.userId == "amzn1.ask.account.AFP3ZWPOS2BGJR7OWJZ3DHPKMOMNWY4AY66FUR7ILBWANIHQN73QH3G5PC2FJVGDIDA7MY54GGNRGM4SVPKTT3K53SLI232MEFI77TZN7W6LISNFZTTFDSPCLX6OB4ISJDVJB6QZO3XC74US6CH5DQXYCVOODNTUFNI5JNSUWSBDVMWB7JXPVX43P4EUIMHTPZHNRHZDUDENZVI" 15 | assert request.request.type == "LaunchRequest" 16 | assert request.request.requestId == "EdwRequestId.27142539-8af4-430c-8f22-411cfab269bd" 17 | assert request.request.timestamp == "2016-07-07T00:45:08Z" 18 | 19 | response = %Response{} 20 | |> set_output_speech(%TextOutputSpeech{text: "Welcome to the Horoscope."}) 21 | 22 | conn 23 | |> set_response(response) 24 | end 25 | 26 | def session_ended_request(conn, request) do 27 | assert request.version == "1.0" 28 | assert request.session.new == false 29 | assert request.session.sessionId == "SessionId.80ef8951-172e-4f02-ace8-a7ec847e2d9f" 30 | assert request.session.application.applicationId == "amzn1.echo-sdk-ams.app.05dcb1a4-cb45-46c5-a30e-bb3033a0770a" 31 | assert request.session.attributes == %{} 32 | assert request.session.user.userId == "amzn1.ask.account.AFP3ZWPOS2BGJR7OWJZ3DHPKMOMNWY4AY66FUR7ILBWANIHQN73QH3G5PC2FJVGDIDA7MY54GGNRGM4SVPKTT3K53SLI232MEFI77TZN7W6LISNFZTTFDSPCLX6OB4ISJDVJB6QZO3XC74US6CH5DQXYCVOODNTUFNI5JNSUWSBDVMWB7JXPVX43P4EUIMHTPZHNRHZDUDENZVI" 33 | assert request.request.type == "SessionEndedRequest" 34 | assert request.request.requestId == "EdwRequestId.27142539-8af4-430c-8f22-411cfab269bd" 35 | assert request.request.timestamp == "2016-07-07T00:45:08Z" 36 | assert request.request.reason == "USER_INITIATED" 37 | 38 | conn 39 | end 40 | 41 | def intent_request(conn, "GetHoroscope", request) do 42 | assert request.version == "1.0" 43 | assert request.session.new == false 44 | assert request.session.sessionId == "SessionId.80ef8951-172e-4f02-ace8-a7ec847e2d9f" 45 | assert request.session.application.applicationId == "amzn1.echo-sdk-ams.app.05dcb1a4-cb45-46c5-a30e-bb3033a0770a" 46 | assert request.session.attributes == %{} 47 | assert request.session.user.userId == "amzn1.ask.account.AFP3ZWPOS2BGJR7OWJZ3DHPKMOMNWY4AY66FUR7ILBWANIHQN73QH3G5PC2FJVGDIDA7MY54GGNRGM4SVPKTT3K53SLI232MEFI77TZN7W6LISNFZTTFDSPCLX6OB4ISJDVJB6QZO3XC74US6CH5DQXYCVOODNTUFNI5JNSUWSBDVMWB7JXPVX43P4EUIMHTPZHNRHZDUDENZVI" 48 | assert request.request.type == "IntentRequest" 49 | assert request.request.requestId == "EdwRequestId.27142539-8af4-430c-8f22-411cfab269bd" 50 | assert request.request.timestamp == "2016-07-07T00:45:08Z" 51 | assert request.request.intent.name == "GetHoroscope" 52 | assert request.request.intent.slots["Sign"]["name"] == "Sign" 53 | assert request.request.intent.slots["Sign"]["value"] == "Libra" 54 | assert request.request.intent.slots["Date"]["name"] == "Date" 55 | assert request.request.intent.slots["Date"]["value"] == nil 56 | 57 | card = %SimpleCard{} 58 | |> SimpleCard.set_title("Get Horoscope") 59 | |> SimpleCard.set_content("You are going to have an unexpected event today.") 60 | 61 | response = %Response{} 62 | |> set_output_speech(%TextOutputSpeech{text: "You are going to have an unexpected event today."}) 63 | |> set_card(card) 64 | |> set_session_attributes(%{my_key: "my_data"}) 65 | |> set_should_end_session(true) 66 | 67 | conn |> set_response(response) 68 | end 69 | 70 | end 71 | 72 | test "LaunchRequest" do 73 | json = """ 74 | { 75 | "session": { 76 | "sessionId": "SessionId.80ef8951-172e-4f02-ace8-a7ec847e2d9f", 77 | "application": { 78 | "applicationId": "amzn1.echo-sdk-ams.app.05dcb1a4-cb45-46c5-a30e-bb3033a0770a" 79 | }, 80 | "attributes": {}, 81 | "user": { 82 | "userId": "amzn1.ask.account.AFP3ZWPOS2BGJR7OWJZ3DHPKMOMNWY4AY66FUR7ILBWANIHQN73QH3G5PC2FJVGDIDA7MY54GGNRGM4SVPKTT3K53SLI232MEFI77TZN7W6LISNFZTTFDSPCLX6OB4ISJDVJB6QZO3XC74US6CH5DQXYCVOODNTUFNI5JNSUWSBDVMWB7JXPVX43P4EUIMHTPZHNRHZDUDENZVI" 83 | }, 84 | "new": false 85 | }, 86 | "request": { 87 | "type": "LaunchRequest", 88 | "requestId": "EdwRequestId.27142539-8af4-430c-8f22-411cfab269bd", 89 | "timestamp": "2016-07-07T00:45:08Z" 90 | }, 91 | "version": "1.0" 92 | } 93 | """ 94 | 95 | conn = conn(:post, "/alexa", "#{json}") 96 | 97 | conn = HoroscopeController.post(conn, Poison.decode!(json)) 98 | response = Poison.decode!(conn.resp_body) 99 | 100 | expected_response = %{ 101 | "version" => "1.0", 102 | "sessionAttributes" => %{}, 103 | "response" => %{ 104 | "outputSpeech" => %{ 105 | "type" => "PlainText", 106 | "text" => "Welcome to the Horoscope." 107 | }, 108 | "shouldEndSession" => false 109 | } 110 | } 111 | 112 | assert conn.status == 200 113 | assert response == expected_response 114 | end 115 | 116 | test "IntentRequest" do 117 | json = """ 118 | { 119 | "session": { 120 | "sessionId": "SessionId.80ef8951-172e-4f02-ace8-a7ec847e2d9f", 121 | "application": { 122 | "applicationId": "amzn1.echo-sdk-ams.app.05dcb1a4-cb45-46c5-a30e-bb3033a0770a" 123 | }, 124 | "attributes": {}, 125 | "user": { 126 | "userId": "amzn1.ask.account.AFP3ZWPOS2BGJR7OWJZ3DHPKMOMNWY4AY66FUR7ILBWANIHQN73QH3G5PC2FJVGDIDA7MY54GGNRGM4SVPKTT3K53SLI232MEFI77TZN7W6LISNFZTTFDSPCLX6OB4ISJDVJB6QZO3XC74US6CH5DQXYCVOODNTUFNI5JNSUWSBDVMWB7JXPVX43P4EUIMHTPZHNRHZDUDENZVI" 127 | }, 128 | "new": false 129 | }, 130 | "request": { 131 | "type": "IntentRequest", 132 | "requestId": "EdwRequestId.27142539-8af4-430c-8f22-411cfab269bd", 133 | "timestamp": "2016-07-07T00:45:08Z", 134 | "intent": { 135 | "name": "GetHoroscope", 136 | "slots": { 137 | "Sign": { 138 | "name": "Sign", 139 | "value": "Libra" 140 | }, 141 | "Date": { 142 | "name": "Date" 143 | } 144 | } 145 | }, 146 | "locale": "en-US" 147 | }, 148 | "version": "1.0" 149 | } 150 | """ 151 | 152 | conn = conn(:post, "/alexa", "#{json}") 153 | 154 | conn = HoroscopeController.post(conn, Poison.decode!(json)) 155 | 156 | response = Poison.decode!(conn.resp_body) 157 | 158 | expected_response = %{ 159 | "version" => "1.0", 160 | "response" => %{ 161 | "card" => %{ 162 | "type" => "Simple", 163 | "title" => "Get Horoscope", 164 | "content" => "You are going to have an unexpected event today." 165 | }, 166 | "outputSpeech" => %{ 167 | "type" => "PlainText", 168 | "text" => "You are going to have an unexpected event today." 169 | }, 170 | "shouldEndSession" => true 171 | }, 172 | "sessionAttributes" => %{"my_key" => "my_data"} 173 | } 174 | 175 | assert conn.status == 200 176 | assert response == expected_response 177 | end 178 | 179 | test "SessionEndedRequest" do 180 | json = """ 181 | { 182 | "session": { 183 | "sessionId": "SessionId.80ef8951-172e-4f02-ace8-a7ec847e2d9f", 184 | "application": { 185 | "applicationId": "amzn1.echo-sdk-ams.app.05dcb1a4-cb45-46c5-a30e-bb3033a0770a" 186 | }, 187 | "attributes": {}, 188 | "user": { 189 | "userId": "amzn1.ask.account.AFP3ZWPOS2BGJR7OWJZ3DHPKMOMNWY4AY66FUR7ILBWANIHQN73QH3G5PC2FJVGDIDA7MY54GGNRGM4SVPKTT3K53SLI232MEFI77TZN7W6LISNFZTTFDSPCLX6OB4ISJDVJB6QZO3XC74US6CH5DQXYCVOODNTUFNI5JNSUWSBDVMWB7JXPVX43P4EUIMHTPZHNRHZDUDENZVI" 190 | }, 191 | "new": false 192 | }, 193 | "request": { 194 | "type": "SessionEndedRequest", 195 | "requestId": "EdwRequestId.27142539-8af4-430c-8f22-411cfab269bd", 196 | "timestamp": "2016-07-07T00:45:08Z", 197 | "reason": "USER_INITIATED" 198 | }, 199 | "version": "1.0" 200 | } 201 | """ 202 | 203 | conn = conn(:post, "/alexa", "#{json}") 204 | 205 | conn = HoroscopeController.post(conn, Poison.decode!(json)) 206 | response = Poison.decode!(conn.resp_body) 207 | 208 | expected_response = %{ 209 | } 210 | 211 | assert conn.status == 200 212 | assert response == expected_response 213 | end 214 | 215 | 216 | 217 | end 218 | 219 | --------------------------------------------------------------------------------