├── config ├── docs.exs ├── config.exs ├── dev.exs └── test.exs ├── .formatter.exs ├── NOTICE ├── .env.sample ├── .travis.yml ├── lib ├── ex_twilio.ex └── ex_twilio │ ├── parent.ex │ ├── jwt │ ├── grant.ex │ ├── access_token │ │ ├── video_grant.ex │ │ ├── chat_grant.ex │ │ └── voice_grant.ex │ └── access_token.ex │ ├── resources │ ├── token.ex │ ├── conference.ex │ ├── recording.ex │ ├── studio │ │ ├── flow.ex │ │ ├── execution.ex │ │ └── step.ex │ ├── queue.ex │ ├── address.ex │ ├── task_router │ │ ├── workspace_statistic.ex │ │ ├── activity.ex │ │ ├── workflow_statistic.ex │ │ ├── task_queue_statistic.ex │ │ ├── worker_statistic.ex │ │ ├── reservation.ex │ │ ├── workspace.ex │ │ ├── workflow.ex │ │ ├── task.ex │ │ ├── event.ex │ │ ├── worker.ex │ │ └── task_queue.ex │ ├── programmable_chat │ │ ├── credential.ex │ │ ├── role.ex │ │ ├── user_channel.ex │ │ ├── user.ex │ │ ├── channel.ex │ │ ├── message.ex │ │ ├── member.ex │ │ └── service.ex │ ├── transcription.ex │ ├── authorized_connect_app.ex │ ├── outgoing_caller_id.ex │ ├── sip_credential_list.ex │ ├── member.ex │ ├── short_code.ex │ ├── available_phone_number.ex │ ├── sip_credential.ex │ ├── sip_ip_access_control_list.ex │ ├── connect_app.ex │ ├── sip_ip_address.ex │ ├── media.ex │ ├── dependent_phone_number.ex │ ├── notification.ex │ ├── video │ │ └── room.ex │ ├── sip_domain.ex │ ├── participant.ex │ ├── application.ex │ ├── feedback.ex │ ├── fax.ex │ ├── lookup.ex │ ├── call.ex │ ├── message.ex │ ├── incoming_phone_number.ex │ ├── notify │ │ ├── credential.ex │ │ ├── binding.ex │ │ ├── service.ex │ │ └── notification.ex │ └── account.ex │ ├── ext │ └── map.ex │ ├── result_stream.ex │ ├── request_validator.ex │ ├── config.ex │ ├── parser.ex │ ├── resource.ex │ ├── api.ex │ ├── url_generator.ex │ ├── capability.ex │ └── worker_capability.ex ├── bin ├── release ├── test └── setup ├── CONTRIBUTING.md ├── test ├── test_helper.exs └── ex_twilio │ ├── jwt │ ├── access_token │ │ ├── video_grant_test.exs │ │ ├── chat_grant_test.exs │ │ └── voice_grant_test.exs │ └── access_token_test.exs │ ├── parser_test.exs │ ├── resource_test.exs │ ├── result_stream_test.exs │ ├── worker_capability_test.exs │ ├── url_generator_test.exs │ ├── request_validator_test.exs │ ├── capability_test.exs │ └── api_test.exs ├── .gitignore ├── .semaphore └── semaphore.yml ├── LICENSE.md ├── mix.exs ├── mix.lock ├── CALLING_TUTORIAL.md ├── README.md └── CHANGELOG.md /config/docs.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | import_config "#{Mix.env()}.exs" 4 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :ex_twilio, 4 | account_sid: {:system, "TWILIO_ACCOUNT_SID"}, 5 | auth_token: {:system, "TWILIO_AUTH_TOKEN"}, 6 | workspace_sid: {:system, "TWILIO_WORKSPACE_SID"} 7 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :ex_twilio, 4 | account_sid: {:system, "TWILIO_TEST_ACCOUNT_SID"}, 5 | auth_token: {:system, "TWILIO_TEST_AUTH_TOKEN"}, 6 | workspace_sid: {:system, "TWILIO_TEST_WORKSPACE_SID"} 7 | 8 | config :logger, level: :info 9 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright (c) Daniel Berkompas, 2015. 2 | 3 | This product depends on the following third-party components: 4 | 5 | * elixir (https://github.com/elixir-lang/elixir) 6 | 7 | Copyright (c) 2012 Plataformatec. Elixir is open source under the 8 | Apache 2.0 license, available here: 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | export TWILIO_ACCOUNT_SID="" 2 | export TWILIO_AUTH_TOKEN="" 3 | export TWILIO_WORKSPACE_SID="" 4 | 5 | export TWILIO_TEST_ACCOUNT_SID="" 6 | export TWILIO_TEST_AUTH_TOKEN="" 7 | export TWILIO_TEST_WORKSPACE_SID="" 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | cache: 3 | directories: 4 | - $HOME/.mix 5 | elixir: 6 | - 1.8.0 7 | otp_release: 8 | - 21.0 9 | before_script: 10 | - export MIX_HOME=$HOME/.mix 11 | - export TWILIO_TEST_ACCOUNT_SID=account_sid 12 | - export TWILIO_TEST_AUTH_TOKEN=auth_token 13 | - export TWILIO_TEST_WORKSPACE_SID=workspace_sid 14 | - export MIX_ENV=test 15 | script: 16 | - bin/setup 17 | after_script: 18 | - mix inch.report 19 | -------------------------------------------------------------------------------- /lib/ex_twilio.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio do 2 | @moduledoc """ 3 | ExTwilio is a relatively full-featured API client for the Twilio API. 4 | 5 | If you're a user, take a look at the various Resource modules, such as 6 | `ExTwilio.Account` and `ExTwilio.Call`. 7 | 8 | If you want to learn more about how ExTwilio works internally, take a gander 9 | at: 10 | 11 | - `ExTwilio.Api` 12 | - `ExTwilio.Resource` 13 | - `ExTwilio.Parser` 14 | """ 15 | end 16 | -------------------------------------------------------------------------------- /lib/ex_twilio/parent.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.Parent do 2 | @moduledoc """ 3 | This module provides structure for the specification of parents to a resource. 4 | 5 | It contains a `module` which should be the full module name of the parent 6 | resource, as well as a key which represents the key that will be matched 7 | against the options list to find the `sid` of the parent resource and place it 8 | into the url correctly. 9 | """ 10 | defstruct module: nil, 11 | key: nil 12 | end 13 | -------------------------------------------------------------------------------- /lib/ex_twilio/jwt/grant.ex: -------------------------------------------------------------------------------- 1 | defprotocol ExTwilio.JWT.Grant do 2 | @moduledoc """ 3 | A protocol for converting grants into JWT claims. 4 | """ 5 | 6 | @doc """ 7 | The type of claim this grant is. 8 | 9 | ## Examples 10 | 11 | def type(_grant), do: "chat" 12 | 13 | """ 14 | def type(grant) 15 | 16 | @doc """ 17 | The attributes of the claim. 18 | 19 | ## Examples 20 | 21 | def attrs(grant) do 22 | %{"name" => grant.name} 23 | end 24 | 25 | """ 26 | def attrs(grant) 27 | end 28 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/token.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.Token do 2 | @moduledoc """ 3 | Represents a Token resource in the Twilio API. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/iam/access-tokens) 6 | """ 7 | 8 | defstruct username: nil, 9 | password: nil, 10 | ttl: nil, 11 | account_sid: nil, 12 | ice_servers: nil, 13 | date_created: nil, 14 | date_updated: nil 15 | 16 | use ExTwilio.Resource, import: [:stream, :all, :create] 17 | 18 | def parents, do: [:account] 19 | end 20 | -------------------------------------------------------------------------------- /bin/release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | puts "What version number do you want to release?" 4 | print "> " 5 | version = gets.gsub(/\n/, "") 6 | 7 | continue = system "github_changelog_generator" 8 | continue = system "git add ." 9 | continue = system "git commit -am \"Release version #{version}\"" if continue 10 | continue = system "git tag v#{version}" if continue 11 | continue = system "git push" if continue 12 | continue = system "git push -f origin v#{version}" if continue 13 | continue = system "mix hex.publish" if continue 14 | 15 | puts "Version #{version} was successfully released!" 16 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/conference.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.Conference do 2 | @moduledoc """ 3 | Represents an Conference resource in the Twilio API. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/voice/api/conference-resource) 6 | """ 7 | 8 | defstruct sid: nil, 9 | friendly_name: nil, 10 | status: nil, 11 | date_created: nil, 12 | date_updated: nil, 13 | account_sid: nil, 14 | uri: nil 15 | 16 | use ExTwilio.Resource, import: [:stream, :all, :find, :update] 17 | 18 | def parents, do: [:account] 19 | end 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | I welcome contributions to ExTwilio. Contributors should keep in mind the following rules: 4 | 5 | - Pull requests will not be accepted if the Travis build is failing. 6 | - Code changes should be accompanied with test changes or additions to validate that your changes actually work. 7 | - Code changes must update inline documentation in all relevant areas. Users rely on this documentation. 8 | - Any code you submit will be subject to the MIT license. 9 | - Discussion must be courteous at all times. Offending discussions will be closed and locked. 10 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/recording.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.Recording do 2 | @moduledoc """ 3 | Represents a Recording resource in the Twilio API. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/voice/api/recording) 6 | """ 7 | 8 | defstruct sid: nil, 9 | date_created: nil, 10 | date_updated: nil, 11 | account_sid: nil, 12 | call_sid: nil, 13 | duration: nil, 14 | api_version: nil, 15 | uri: nil 16 | 17 | use ExTwilio.Resource, import: [:stream, :all, :find, :destroy] 18 | 19 | def parents, do: [:account] 20 | end 21 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/studio/flow.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.Studio.Flow do 2 | @moduledoc """ 3 | Represents an individual workflow that you create. 4 | They can handle one or more use cases. 5 | 6 | - [Twilio docs](https://www.twilio.com/docs/studio/rest-api/flow) 7 | """ 8 | 9 | defstruct [ 10 | :sid, 11 | :account_sid, 12 | :friendly_name, 13 | :status, 14 | :version, 15 | :date_created, 16 | :date_updated, 17 | :url 18 | ] 19 | 20 | use ExTwilio.Resource, import: [:stream, :all, :find, :delete] 21 | 22 | def children, do: [:engagements, :executions] 23 | end 24 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/queue.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.Queue do 2 | @moduledoc """ 3 | Represents a Queue resource in the Twilio API. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/voice/api/queue-resource) 6 | """ 7 | 8 | defstruct sid: nil, 9 | friendly_name: nil, 10 | current_size: nil, 11 | max_size: nil, 12 | average_wait_time: nil 13 | 14 | use ExTwilio.Resource, 15 | import: [ 16 | :stream, 17 | :all, 18 | :find, 19 | :create, 20 | :update, 21 | :destroy 22 | ] 23 | 24 | def parents, do: [:account] 25 | end 26 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/address.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.Address do 2 | @moduledoc """ 3 | Represents an Address resource in the Twilio API. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/usage/api/address) 6 | """ 7 | 8 | defstruct sid: nil, 9 | account_sid: nil, 10 | friendly_name: nil, 11 | customer_name: nil, 12 | street: nil, 13 | city: nil, 14 | region: nil, 15 | postal_code: nil, 16 | iso_country: nil 17 | 18 | use ExTwilio.Resource, import: [:stream, :all, :create, :find, :update] 19 | 20 | def parents, do: [:account] 21 | end 22 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/task_router/workspace_statistic.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.TaskRouter.WorkspaceStatistic do 2 | @moduledoc """ 3 | Provides real time and historical statistics for Workspaces. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/api/taskrouter/workspace-statistics) 6 | """ 7 | 8 | defstruct realtime: nil, 9 | cumulative: nil, 10 | account_sid: nil, 11 | workspace_sid: nil, 12 | workflow_sid: nil 13 | 14 | use ExTwilio.Resource, import: [:stream, :all] 15 | 16 | def parents, do: [%ExTwilio.Parent{module: ExTwilio.TaskRouter.Workspace, key: :workspace}] 17 | def children, do: [:minutes, :start_date, :end_date] 18 | end 19 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/programmable_chat/credential.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.ProgrammableChat.Credential do 2 | @moduledoc """ 3 | Represents a Credential resource in the Twilio Programmable Chat API. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/api/chat/rest/credentials) 6 | """ 7 | 8 | defstruct sid: nil, 9 | account_sid: nil, 10 | friendly_name: nil, 11 | type: nil, 12 | sandbox: nil, 13 | date_created: nil, 14 | date_updated: nil, 15 | url: nil 16 | 17 | use ExTwilio.Resource, 18 | import: [ 19 | :stream, 20 | :all, 21 | :find, 22 | :create, 23 | :update, 24 | :destroy 25 | ] 26 | end 27 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/transcription.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.Transcription do 2 | @moduledoc """ 3 | Represents a Transcription resource in the Twilio API. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/voice/api/recording-transcription) 6 | """ 7 | 8 | defstruct sid: nil, 9 | date_created: nil, 10 | date_updated: nil, 11 | account_sid: nil, 12 | status: nil, 13 | recording_sid: nil, 14 | duration: nil, 15 | transcription_text: nil, 16 | price: nil, 17 | price_unit: nil, 18 | uri: nil 19 | 20 | use ExTwilio.Resource, import: [:stream, :all, :find, :destroy] 21 | 22 | def parents, do: [:account, :recording] 23 | end 24 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/authorized_connect_app.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.AuthorizedConnectApp do 2 | @moduledoc """ 3 | Represents an AuthorizedConnectApp resource in the Twilio API. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/iam/authorized-connect-apps/api) 6 | """ 7 | 8 | defstruct date_created: nil, 9 | date_updated: nil, 10 | account_sid: nil, 11 | permissions: nil, 12 | connect_app_sid: nil, 13 | connect_app_friendly_name: nil, 14 | connect_app_description: nil, 15 | connect_app_company_name: nil, 16 | connect_app_homepage_url: nil, 17 | uri: nil 18 | 19 | use ExTwilio.Resource, import: [:stream, :all, :find] 20 | 21 | def parents, do: [:account] 22 | end 23 | -------------------------------------------------------------------------------- /lib/ex_twilio/jwt/access_token/video_grant.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.JWT.AccessToken.VideoGrant do 2 | @moduledoc """ 3 | A JWT grant to access a given Twilio video service. 4 | 5 | ## Examples 6 | 7 | ExTwilio.JWT.AccessToken.VideoGrant.new(room: "room_id") 8 | 9 | """ 10 | 11 | @enforce_keys [:room] 12 | 13 | defstruct room: nil 14 | 15 | @type t :: %__MODULE__{ 16 | room: String.t() 17 | } 18 | 19 | @doc """ 20 | Create a new grant. 21 | """ 22 | @spec new(attrs :: Keyword.t()) :: t 23 | def new(attrs \\ []) do 24 | struct(__MODULE__, attrs) 25 | end 26 | 27 | defimpl ExTwilio.JWT.Grant do 28 | def type(_grant), do: "video" 29 | 30 | def attrs(grant) do 31 | %{"room" => grant.room} 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/outgoing_caller_id.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.OutgoingCallerId do 2 | @moduledoc """ 3 | Represents an OutgoingCallerId resource in the Twilio API. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/voice/api/outgoing-caller-ids) 6 | """ 7 | 8 | defstruct sid: nil, 9 | date_created: nil, 10 | date_updated: nil, 11 | friendly_name: nil, 12 | account_sid: nil, 13 | phone_number: nil, 14 | validation_code: nil, 15 | call_sid: nil, 16 | uri: nil 17 | 18 | use ExTwilio.Resource, 19 | import: [ 20 | :stream, 21 | :all, 22 | :find, 23 | :create, 24 | :update, 25 | :destroy 26 | ] 27 | 28 | def parents, do: [:account] 29 | end 30 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | 3 | defmodule TestHelper do 4 | use ExUnit.Case, async: false 5 | alias ExTwilio.Api 6 | import Mock 7 | 8 | def with_fixture(:get!, response, fun), 9 | do: with_fixture({:get!, fn _url, _headers -> response end}, fun) 10 | 11 | def with_fixture(:post!, response, fun), 12 | do: with_fixture({:post!, fn _url, _options, _headers -> response end}, fun) 13 | 14 | def with_fixture(:delete!, response, fun), 15 | do: with_fixture({:delete!, fn _url, _headers -> response end}, fun) 16 | 17 | def with_fixture(stub, fun) do 18 | with_mock Api, [:passthrough], [stub] do 19 | fun.() 20 | end 21 | end 22 | 23 | def json_response(map, status) do 24 | %{body: Jason.encode!(map), status_code: status} 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/task_router/activity.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.TaskRouter.Activity do 2 | @moduledoc """ 3 | Represents the current status of your workers. Workers can only have a single activity at a time. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/api/taskrouter/activities) 6 | """ 7 | 8 | defstruct sid: nil, 9 | account_sid: nil, 10 | workspace_sid: nil, 11 | friendly_name: nil, 12 | available: nil, 13 | date_created: nil, 14 | date_updated: nil, 15 | url: nil, 16 | links: nil 17 | 18 | use ExTwilio.Resource, import: [:stream, :all, :find, :create, :update, :delete] 19 | 20 | def parents, do: [%ExTwilio.Parent{module: ExTwilio.TaskRouter.Workspace, key: :workspace}] 21 | end 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | ex_twilio-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | 28 | # Misc. 29 | .env 30 | .DS_Store 31 | .elixir_ls 32 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/sip_credential_list.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.SipCredentialList do 2 | @moduledoc """ 3 | Represents an SIP CredentialList in the Twilio API. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/voice/sip/api/sip-credentiallist-resource) 6 | """ 7 | 8 | defstruct sid: nil, 9 | account_sid: nil, 10 | friendly_name: nil, 11 | date_created: nil, 12 | date_updated: nil, 13 | uri: nil 14 | 15 | use ExTwilio.Resource, 16 | import: [ 17 | :stream, 18 | :all, 19 | :find, 20 | :create, 21 | :update, 22 | :destroy 23 | ] 24 | 25 | def resource_name, do: "SIP/CredentialLists" 26 | def resource_collection_name, do: "credential_lists" 27 | def parents, do: [:account] 28 | end 29 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/member.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.Member do 2 | @moduledoc """ 3 | Represents an Member resource in the Twilio API. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/chat/rest/member-resource) 6 | 7 | ## Examples 8 | 9 | Since Members are members of a Queue in the Twilio API, you must pass a Queue 10 | SID into each function in this module. 11 | 12 | ExTwilio.Member.all(queue: "queue_sid") 13 | 14 | """ 15 | 16 | defstruct call_sid: nil, 17 | date_enqueued: nil, 18 | wait_time: nil, 19 | position: nil 20 | 21 | use ExTwilio.Resource, import: [:stream, :all, :find, :update] 22 | 23 | def parents, do: [:account, :queue] 24 | 25 | def resource_collection_name, do: Url.resource_collection_name(ExTwilio.QueueMember) 26 | end 27 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/short_code.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.ShortCode do 2 | @moduledoc """ 3 | Represents a ShortCode resource in the Twilio API. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/sms/api/short-code) 6 | """ 7 | 8 | defstruct sid: nil, 9 | date_created: nil, 10 | date_updated: nil, 11 | friendly_name: nil, 12 | account_sid: nil, 13 | short_code: nil, 14 | api_version: nil, 15 | sms_url: nil, 16 | sms_method: nil, 17 | sms_fallback_url: nil, 18 | sms_fallback_url_method: nil, 19 | uri: nil 20 | 21 | use ExTwilio.Resource, import: [:stream, :all, :find, :update] 22 | 23 | def resource_name, do: "SMS/ShortCodes" 24 | def parents, do: [:account] 25 | end 26 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/task_router/workflow_statistic.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.TaskRouter.WorkflowStatistic do 2 | @moduledoc """ 3 | Represents a resource that provides statistics on workflows. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/api/taskrouter/workflow-statistics) 6 | """ 7 | 8 | defstruct realtime: nil, 9 | cumulative: nil, 10 | account_sid: nil, 11 | workspace_sid: nil, 12 | workflow_sid: nil 13 | 14 | use ExTwilio.Resource, import: [:stream, :all] 15 | 16 | def parents, 17 | do: [ 18 | %ExTwilio.Parent{module: ExTwilio.TaskRouter.Workspace, key: :workspace}, 19 | %ExTwilio.Parent{module: ExTwilio.TaskRouter.Workflow, key: :workflow} 20 | ] 21 | 22 | def children, do: [:minutes, :start_date, :end_date] 23 | end 24 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/available_phone_number.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.AvailablePhoneNumber do 2 | @moduledoc """ 3 | Represents an AvailablePhoneNumber resource in the Twilio API. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/phone-numbers/api/availablephonenumber-resource) 6 | """ 7 | 8 | defstruct friendly_name: nil, 9 | phone_number: nil, 10 | lata: nil, 11 | rate_center: nil, 12 | latitude: nil, 13 | longitude: nil, 14 | region: nil, 15 | postal_code: nil, 16 | iso_country: nil, 17 | capabilities: nil, 18 | address_requirements: nil 19 | 20 | use ExTwilio.Resource, import: [:stream, :all] 21 | 22 | def parents, do: [:account] 23 | def children, do: [:iso_country_code, :type] 24 | end 25 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/sip_credential.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.SipCredential do 2 | @moduledoc """ 3 | Represents an SIP Credential in the Twilio API. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/voice/sip/api/sip-credential-resource) 6 | """ 7 | 8 | defstruct sid: nil, 9 | account_sid: nil, 10 | friendly_name: nil, 11 | ip_address: nil, 12 | date_created: nil, 13 | date_updated: nil, 14 | uri: nil 15 | 16 | use ExTwilio.Resource, 17 | import: [ 18 | :stream, 19 | :all, 20 | :find, 21 | :create, 22 | :update, 23 | :destroy 24 | ] 25 | 26 | def resource_name, do: "Credentials" 27 | def resource_collection_name, do: "credentials" 28 | def parents, do: [:account, :sip_credential_list] 29 | end 30 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/task_router/task_queue_statistic.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.TaskRouter.TaskQueueStatistic do 2 | @moduledoc """ 3 | Realtime and historical statistics for TaskQueues 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/api/taskrouter/taskqueue-statistics) 6 | """ 7 | 8 | defstruct realtime: nil, 9 | cumulative: nil, 10 | account_sid: nil, 11 | workspace_sid: nil, 12 | task_queue_sid: nil 13 | 14 | use ExTwilio.Resource, import: [:stream, :all] 15 | 16 | def parents, 17 | do: [ 18 | %ExTwilio.Parent{module: ExTwilio.TaskRouter.Workspace, key: :workspace}, 19 | %ExTwilio.Parent{module: ExTwilio.TaskRouter.TaskQueue, key: :task_queues} 20 | ] 21 | 22 | def children, do: [:minutes, :friendly_name, :start_date, :end_date] 23 | end 24 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/sip_ip_access_control_list.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.SipIpAccessControlList do 2 | @moduledoc """ 3 | Represents an SIP IPAccessControlList in the Twilio API. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/voice/sip/api/sip-ipaccesscontrollist-resource) 6 | """ 7 | 8 | defstruct sid: nil, 9 | account_sid: nil, 10 | friendly_name: nil, 11 | date_created: nil, 12 | date_updated: nil, 13 | uri: nil 14 | 15 | use ExTwilio.Resource, 16 | import: [ 17 | :stream, 18 | :all, 19 | :find, 20 | :create, 21 | :update, 22 | :destroy 23 | ] 24 | 25 | def resource_name, do: "SIP/IpAccessControlLists" 26 | def resource_collection_name, do: "ip_access_control_lists" 27 | def parents, do: [:account] 28 | end 29 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/studio/execution.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.Studio.Execution do 2 | @moduledoc """ 3 | Represents a specific person's run through a Flow. 4 | An execution is active while the user is in the Flow, and it is considered ended when they stop or are kicked out of the Flow. 5 | 6 | - [Twilio docs](https://www.twilio.com/docs/studio/rest-api/execution) 7 | """ 8 | 9 | defstruct [ 10 | :sid, 11 | :account_sid, 12 | :flow_sid, 13 | :context, 14 | :contact_sid, 15 | :status, 16 | :date_created, 17 | :date_updated, 18 | :url 19 | ] 20 | 21 | use ExTwilio.Resource, import: [:stream, :all, :find, :create, :delete] 22 | 23 | def parents, 24 | do: [%ExTwilio.Parent{module: ExTwilio.Studio.Flow, key: :flow}] 25 | 26 | def children, do: [:execution_context, :steps] 27 | end 28 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/connect_app.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.ConnectApp do 2 | @moduledoc """ 3 | Represents an ConnectApp resource in the Twilio API. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/iam/connect-apps/api) 6 | """ 7 | 8 | defstruct sid: nil, 9 | date_created: nil, 10 | date_updated: nil, 11 | account_sid: nil, 12 | permissions: nil, 13 | friendly_name: nil, 14 | description: nil, 15 | company_name: nil, 16 | homepage_url: nil, 17 | authorize_redirect_url: nil, 18 | deauthorize_callback_url: nil, 19 | deauthorize_callback_method: nil, 20 | uri: nil 21 | 22 | use ExTwilio.Resource, import: [:stream, :all, :find, :create, :update] 23 | 24 | def parents, do: [:account] 25 | end 26 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/programmable_chat/role.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.ProgrammableChat.Role do 2 | @moduledoc """ 3 | Represents a Message resource in the Twilio Programmable Chat API. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/api/chat/rest/roles) 6 | """ 7 | 8 | defstruct sid: nil, 9 | account_sid: nil, 10 | service_sid: nil, 11 | friendly_name: nil, 12 | type: nil, 13 | permissions: nil, 14 | date_created: nil, 15 | date_updated: nil, 16 | url: nil 17 | 18 | use ExTwilio.Resource, 19 | import: [ 20 | :stream, 21 | :all, 22 | :find, 23 | :create, 24 | :update, 25 | :destroy 26 | ] 27 | 28 | def parents, do: [%ExTwilio.Parent{module: ExTwilio.ProgrammableChat.Service, key: :service}] 29 | end 30 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/sip_ip_address.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.SipIpAddress do 2 | @moduledoc """ 3 | Represents an SIP IpAddress in the Twilio API. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/voice/sip/api/sip-ipaccesscontrollist-resource#subresources) 6 | """ 7 | 8 | defstruct sid: nil, 9 | account_sid: nil, 10 | friendly_name: nil, 11 | ip_address: nil, 12 | date_created: nil, 13 | date_updated: nil, 14 | uri: nil 15 | 16 | use ExTwilio.Resource, 17 | import: [ 18 | :stream, 19 | :all, 20 | :find, 21 | :create, 22 | :update, 23 | :destroy 24 | ] 25 | 26 | def resource_name, do: "IpAddresses" 27 | def resource_collection_name, do: "ip_addresses" 28 | def parents, do: [:account, :sip_ip_access_control_list] 29 | end 30 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/studio/step.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.Studio.Step do 2 | @moduledoc """ 3 | A Step is the runtime processing of a Widget, starting when that Widget is entered. 4 | Variables get set at the end of a Step. 5 | 6 | - [Twilio docs](https://www.twilio.com/docs/studio/rest-api/step) 7 | """ 8 | 9 | defstruct [ 10 | :sid, 11 | :account_sid, 12 | :execution_sid, 13 | :name, 14 | :context, 15 | :transitioned_from, 16 | :transitioned_to, 17 | :date_created, 18 | :date_updated, 19 | :url 20 | ] 21 | 22 | use ExTwilio.Resource, import: [:stream, :all, :find] 23 | 24 | def parents do 25 | [ 26 | %ExTwilio.Parent{module: ExTwilio.Studio.Flow, key: :flow}, 27 | %ExTwilio.Parent{module: ExTwilio.Studio.Execution, key: :execution} 28 | ] 29 | end 30 | 31 | def children, do: [:context] 32 | end 33 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/task_router/worker_statistic.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.TaskRouter.WorkerStatistic do 2 | @moduledoc """ 3 | TaskRouter provides real time and historical statistics for Workers. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/api/taskrouter/worker-statistics) 6 | """ 7 | 8 | defstruct realtime: nil, 9 | cumulative: nil, 10 | account_sid: nil, 11 | workspace_sid: nil, 12 | worker_sid: nil 13 | 14 | use ExTwilio.Resource, import: [:stream, :all, :find] 15 | 16 | def parents, 17 | do: [ 18 | %ExTwilio.Parent{module: ExTwilio.TaskRouter.Workspace, key: :workspace}, 19 | %ExTwilio.Parent{module: ExTwilio.TaskRouter.Worker, key: :workers} 20 | ] 21 | 22 | def children, 23 | do: [:minutes, :friendly_name, :start_date, :end_date, :task_queue_name, :task_queue_sid] 24 | end 25 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/media.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.Media do 2 | @moduledoc """ 3 | Represents an Media resource in the Twilio API. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/sms/api/media-resource) 6 | 7 | ## Examples 8 | 9 | Since Media belong to a Message in Twilio's API, you must pass a Message SID 10 | to each function in this module. 11 | 12 | ExTwilio.Media.all(message: "message_sid") 13 | 14 | """ 15 | 16 | defstruct sid: nil, 17 | date_created: nil, 18 | date_updated: nil, 19 | account_sid: nil, 20 | parent_sid: nil, 21 | content_type: nil, 22 | uri: nil 23 | 24 | use ExTwilio.Resource, import: [:stream, :all, :find, :destroy] 25 | 26 | def resource_name, do: "Media" 27 | def resource_collection_name, do: "media_list" 28 | def parents, do: [:account, :message] 29 | end 30 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/programmable_chat/user_channel.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.ProgrammableChat.UserChannel do 2 | @moduledoc """ 3 | Represents a User Channel resource in the Twilio Programmable Chat API. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/api/chat/rest/user-channels) 6 | """ 7 | 8 | defstruct sid: nil, 9 | account_sid: nil, 10 | service_sid: nil, 11 | unread_messages_count: nil, 12 | last_consumed_message_index: nil, 13 | channel: nil, 14 | member: nil 15 | 16 | use ExTwilio.Resource, 17 | import: [ 18 | :stream, 19 | :all 20 | ] 21 | 22 | def resource_name, do: "Channels" 23 | 24 | def parents, 25 | do: [ 26 | %ExTwilio.Parent{module: ExTwilio.ProgrammableChat.Service, key: :service}, 27 | %ExTwilio.Parent{module: ExTwilio.ProgrammableChat.User, key: :user} 28 | ] 29 | end 30 | -------------------------------------------------------------------------------- /test/ex_twilio/jwt/access_token/video_grant_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.JWT.AccessToken.VideoGrantTest do 2 | use ExUnit.Case 3 | 4 | alias ExTwilio.JWT.AccessToken.VideoGrant 5 | alias ExTwilio.JWT.Grant 6 | 7 | describe "__struct__" do 8 | test "enforces :room" do 9 | assert_raise ArgumentError, fn -> 10 | Code.eval_string("%ExTwilio.JWT.AccessToken.VideoGrant{}") 11 | end 12 | 13 | assert %VideoGrant{room: "room"} 14 | end 15 | end 16 | 17 | describe ".new/1" do 18 | test "accepts all attributes" do 19 | assert VideoGrant.new(room: "room") == %VideoGrant{ 20 | room: "room" 21 | } 22 | end 23 | end 24 | 25 | test "implements ExTwilio.JWT.Grant" do 26 | assert Grant.type(%VideoGrant{room: "room"}) == "video" 27 | 28 | assert Grant.attrs(%VideoGrant{room: "room"}) == %{ 29 | "room" => "room" 30 | } 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Continuous Integration Script 4 | # 5 | # This script contains all the test commands for this app, and should be run on 6 | # your continuous integration server. Developers can then run it locally to see 7 | # if their changes will pass on the CI server, before pushing. 8 | # 9 | # It also allows the build settings to be changed by anyone with write access to 10 | # the project repo, making them easier to manage. 11 | 12 | MIX_ENV=test mix format --check-formatted || { 13 | echo 'Please format code with `mix format`.' 14 | exit 1 15 | } 16 | 17 | MIX_ENV=test mix compile --warnings-as-errors --force || { 18 | echo 'Please fix all compiler warnings.' 19 | exit 1 20 | } 21 | 22 | MIX_ENV=test mix docs || { 23 | echo 'Elixir HTML docs were not generated!' 24 | exit 1 25 | } 26 | 27 | mix test || { 28 | echo 'Elixir tests failed!' 29 | exit 1 30 | } 31 | 32 | echo 'All tests succeeded!' 33 | exit 0 34 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/dependent_phone_number.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.DependentPhoneNumber do 2 | @moduledoc """ 3 | Represents an DependentPhoneNumber resource in the Twilio API. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/usage/api/address#list-dependent-pns) 6 | 7 | ## Examples 8 | 9 | Since DependentPhoneNumbers are a nested resource in the Twilio API, you must 10 | pass in a parent Address SID to all functions in this module. 11 | 12 | ExTwilio.DependentPhoneNumber.all(address: "address_sid") 13 | 14 | """ 15 | 16 | defstruct sid: nil, 17 | account_sid: nil, 18 | friendly_name: nil, 19 | customer_name: nil, 20 | street: nil, 21 | city: nil, 22 | region: nil, 23 | postal_code: nil, 24 | iso_country: nil 25 | 26 | use ExTwilio.Resource, import: [:stream, :all] 27 | 28 | def parents, do: [:account, :address] 29 | end 30 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/notification.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.Notification do 2 | @moduledoc """ 3 | Represents a Notification resource in the Twilio API. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/notify/api/notification-resource) 6 | """ 7 | 8 | defstruct sid: nil, 9 | date_created: nil, 10 | date_updated: nil, 11 | account_sid: nil, 12 | call_sid: nil, 13 | api_version: nil, 14 | log: nil, 15 | error_code: nil, 16 | more_info: nil, 17 | message_text: nil, 18 | message_date: nil, 19 | request_url: nil, 20 | request_method: nil, 21 | request_variables: nil, 22 | response_headers: nil, 23 | response_body: nil, 24 | uri: nil 25 | 26 | use ExTwilio.Resource, import: [:stream, :all, :find, :destroy] 27 | 28 | def parents, do: [:account, :call] 29 | end 30 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/video/room.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.Video.Room do 2 | @moduledoc """ 3 | Represents a programmable video Room. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/video/api/rooms-resource) 6 | """ 7 | 8 | defstruct [ 9 | :sid, 10 | :account_sid, 11 | :date_created, 12 | :date_updated, 13 | :unique_name, 14 | :status, 15 | :status_callback, 16 | :status_callback_method, 17 | :end_time, 18 | :duration, 19 | :type, 20 | :max_participants, 21 | :record_participant_on_connect, 22 | :video_codecs, 23 | :media_region, 24 | :url, 25 | :links 26 | ] 27 | 28 | use ExTwilio.Resource, 29 | import: [ 30 | :stream, 31 | :all, 32 | :find, 33 | :create, 34 | :update 35 | ] 36 | 37 | def complete(%{sid: sid}), do: complete(sid) 38 | def complete(sid), do: update(sid, status: "completed") 39 | 40 | def parents, do: [:account] 41 | end 42 | -------------------------------------------------------------------------------- /lib/ex_twilio/ext/map.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.Ext.Map do 2 | @moduledoc """ 3 | Additional helper functions for working with maps. 4 | """ 5 | 6 | @type key :: atom | String.t() 7 | 8 | @doc """ 9 | Puts a given key/value pair into a map, if the value is not `false` or `nil`. 10 | """ 11 | @spec put_if(map, key, any) :: map 12 | def put_if(map, _key, value) when value in [nil, false] do 13 | map 14 | end 15 | 16 | def put_if(map, key, value) do 17 | Map.put(map, key, value) 18 | end 19 | 20 | @doc """ 21 | Validates that a function returns true on the given map field, otherwise 22 | raises an error. 23 | """ 24 | @spec validate!(map, key, function, message :: String.t()) :: map | no_return 25 | def validate!(map, field, fun, message) do 26 | value = Map.get(map, field) 27 | 28 | if fun.(value) do 29 | map 30 | else 31 | raise ArgumentError, "#{inspect(field)} #{message}, was: #{inspect(value)}" 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/programmable_chat/user.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.ProgrammableChat.User do 2 | @moduledoc """ 3 | Represents a User resource in the Twilio Programmable Chat API. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/api/chat/rest/users) 6 | """ 7 | 8 | defstruct sid: nil, 9 | account_sid: nil, 10 | service_sid: nil, 11 | role_sid: nil, 12 | identity: nil, 13 | friendly_name: nil, 14 | attributes: nil, 15 | date_created: nil, 16 | date_updated: nil, 17 | is_online: nil, 18 | is_notifiable: nil, 19 | joined_channels_count: nil, 20 | url: nil 21 | 22 | use ExTwilio.Resource, 23 | import: [ 24 | :stream, 25 | :all, 26 | :find, 27 | :create, 28 | :update, 29 | :destroy 30 | ] 31 | 32 | def parents, do: [%ExTwilio.Parent{module: ExTwilio.ProgrammableChat.Service, key: :service}] 33 | end 34 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/task_router/reservation.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.TaskRouter.Reservation do 2 | @moduledoc """ 3 | TaskRouter creates a Reservation subresource whenever a Task is reserved for a a Worker. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/api/taskrouter/reservations) 6 | """ 7 | 8 | defstruct sid: nil, 9 | account_sid: nil, 10 | workspace_sid: nil, 11 | task_sid: nil, 12 | worker_sid: nil, 13 | worker_name: nil, 14 | resevation_status: nil, 15 | date_created: nil, 16 | date_updated: nil, 17 | url: nil, 18 | links: nil 19 | 20 | use ExTwilio.Resource, import: [:stream, :all, :find, :update] 21 | 22 | def parents, 23 | do: [ 24 | %ExTwilio.Parent{module: ExTwilio.TaskRouter.Workspace, key: :workspace}, 25 | %ExTwilio.Parent{module: ExTwilio.TaskRouter.Task, key: :task} 26 | ] 27 | 28 | def children, do: [:worker_sid, :reservation_status] 29 | end 30 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/programmable_chat/channel.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.ProgrammableChat.Channel do 2 | @moduledoc """ 3 | Represents a Channel resource in the Twilio Programmable Chat API. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/api/chat/rest/channels) 6 | """ 7 | defstruct sid: nil, 8 | account_sid: nil, 9 | service_sid: nil, 10 | unique_name: nil, 11 | friendly_name: nil, 12 | attributes: nil, 13 | type: nil, 14 | date_created: nil, 15 | date_updated: nil, 16 | created_by: nil, 17 | members_count: nil, 18 | messages_count: nil, 19 | url: nil, 20 | links: nil 21 | 22 | use ExTwilio.Resource, 23 | import: [ 24 | :stream, 25 | :all, 26 | :find, 27 | :create, 28 | :update, 29 | :destroy 30 | ] 31 | 32 | def parents, do: [%ExTwilio.Parent{module: ExTwilio.ProgrammableChat.Service, key: :service}] 33 | end 34 | -------------------------------------------------------------------------------- /.semaphore/semaphore.yml: -------------------------------------------------------------------------------- 1 | version: v1.0 2 | name: CI Build 3 | agent: 4 | machine: 5 | type: e1-standard-2 6 | os_image: ubuntu2004 7 | auto_cancel: 8 | running: 9 | when: "true" 10 | fail_fast: 11 | stop: 12 | when: "true" 13 | blocks: 14 | - name: Build 15 | dependencies: [] 16 | task: 17 | env_vars: 18 | - name: MIX_ENV 19 | value: test 20 | secrets: 21 | - name: ex-twilio-env 22 | jobs: 23 | - name: Build 24 | matrix: 25 | - env_var: ERLANG_VERSION 26 | values: ["26.0"] 27 | - env_var: ELIXIR_VERSION 28 | values: ["1.16.0"] 29 | commands: 30 | - sem-version erlang $ERLANG_VERSION 31 | - sem-version elixir $ELIXIR_VERSION 32 | - checkout 33 | - cache restore 34 | - mix local.hex --force 35 | - mix local.rebar --force 36 | - mix deps.get 37 | - bin/test 38 | - cache store 39 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/task_router/workspace.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.TaskRouter.Workspace do 2 | @moduledoc """ 3 | A Workspace is a container for your Tasks, Workers, TaskQueues, Workflows and Activities. 4 | 5 | Each of these items exists within a single Workspace and will not be shared across Workspaces. 6 | 7 | - [Twilio docs](https://www.twilio.com/docs/api/taskrouter/workspaces) 8 | """ 9 | 10 | defstruct sid: nil, 11 | account_sid: nil, 12 | friendly_name: nil, 13 | event_callback_url: nil, 14 | default_activity_sid: nil, 15 | date_created: nil, 16 | date_updated: nil, 17 | default_activity_name: nil, 18 | timeout_activity_sid: nil, 19 | timeout_activity_name: nil, 20 | url: nil, 21 | links: nil 22 | 23 | use ExTwilio.Resource, import: [:stream, :all, :find, :create, :update, :delete] 24 | 25 | def children, do: [:friendly_name] 26 | 27 | def resource_name, do: "Workspaces" 28 | end 29 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/task_router/workflow.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.TaskRouter.Workflow do 2 | @moduledoc """ 3 | Represents a workflow that controls how tasks will be prioritized and routed into queues. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/api/taskrouter/workflows) 6 | """ 7 | 8 | defstruct sid: nil, 9 | friendly_name: nil, 10 | account_sid: nil, 11 | workspace_sid: nil, 12 | assignment_callback_url: nil, 13 | fallback_assignment_callback_url: nil, 14 | document_content_type: nil, 15 | configuration: nil, 16 | task_reservation_timeout: nil, 17 | date_created: nil, 18 | date_updated: nil, 19 | url: nil, 20 | links: nil 21 | 22 | use ExTwilio.Resource, import: [:stream, :all, :find, :create, :update, :delete] 23 | 24 | def parents, do: [%ExTwilio.Parent{module: ExTwilio.TaskRouter.Workspace, key: :workspace}] 25 | def children, do: [:friendly_name, :configuration] 26 | end 27 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/programmable_chat/message.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.ProgrammableChat.Message do 2 | @moduledoc """ 3 | Represents a Message resource in the Twilio Programmable Chat API. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/api/chat/rest/messages) 6 | """ 7 | 8 | defstruct sid: nil, 9 | account_sid: nil, 10 | service_sid: nil, 11 | to: nil, 12 | from: nil, 13 | date_created: nil, 14 | date_updated: nil, 15 | was_edited: nil, 16 | body: nil, 17 | attributes: nil, 18 | index: nil, 19 | url: nil 20 | 21 | use ExTwilio.Resource, 22 | import: [ 23 | :stream, 24 | :all, 25 | :find, 26 | :create, 27 | :update, 28 | :destroy 29 | ] 30 | 31 | def parents, 32 | do: [ 33 | %ExTwilio.Parent{module: ExTwilio.ProgrammableChat.Service, key: :service}, 34 | %ExTwilio.Parent{module: ExTwilio.ProgrammableChat.Channel, key: :channel} 35 | ] 36 | end 37 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/programmable_chat/member.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.ProgrammableChat.Member do 2 | @moduledoc """ 3 | Represents a Member resource in the Twilio Programmable Chat API. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/api/chat/rest/members) 6 | """ 7 | 8 | defstruct sid: nil, 9 | account_sid: nil, 10 | service_sid: nil, 11 | channel_sid: nil, 12 | identity: nil, 13 | role_sid: nil, 14 | date_created: nil, 15 | date_updated: nil, 16 | last_consumed_message_index: nil, 17 | last_consumption_timestamp: nil, 18 | url: nil 19 | 20 | use ExTwilio.Resource, 21 | import: [ 22 | :stream, 23 | :all, 24 | :find, 25 | :create, 26 | :update, 27 | :destroy 28 | ] 29 | 30 | def parents, 31 | do: [ 32 | %ExTwilio.Parent{module: ExTwilio.ProgrammableChat.Service, key: :service}, 33 | %ExTwilio.Parent{module: ExTwilio.ProgrammableChat.Channel, key: :channel} 34 | ] 35 | end 36 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # This script sets up your local machine for development on ExTwilio. 4 | 5 | function no_credentials() { 6 | echo "Twilio credentials not set up. You should:" 7 | echo "" 8 | echo "$ cp .env.sample .env" 9 | echo "$ vim .env" 10 | echo "$ source .env" 11 | exit 1 12 | } 13 | 14 | if [ ! $TWILIO_TEST_ACCOUNT_SID ]; then 15 | echo 'TWILIO_TEST_ACCOUNT_SID' 16 | no_credentials 17 | fi 18 | 19 | if [ ! $TWILIO_TEST_AUTH_TOKEN ]; then 20 | echo 'TWILIO_TEST_AUTH_TOKEN' 21 | no_credentials 22 | fi 23 | 24 | if [ ! $TWILIO_TEST_WORKSPACE_SID ]; then 25 | echo 'TWILIO_TEST_WORKSPACE_SID' 26 | no_credentials 27 | fi 28 | 29 | echo "-------------------------------" 30 | echo "Installing dependencies..." 31 | echo "-------------------------------" 32 | 33 | mix local.hex --force || { echo "Could not install Hex!"; exit 1; } 34 | mix deps.get --only test || { echo "Could not install dependencies!"; exit 1;} 35 | 36 | echo "-------------------------------" 37 | echo "Running tests..." 38 | echo "-------------------------------" 39 | 40 | bin/test || { exit 1; } 41 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/sip_domain.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.SipDomain do 2 | @moduledoc """ 3 | Represents an SIP Domain resource in the Twilio API. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/voice/sip/api/sip-domain-resource) 6 | """ 7 | 8 | defstruct sid: nil, 9 | friendly_name: nil, 10 | account_sid: nil, 11 | api_version: nil, 12 | domain_name: nil, 13 | auth_type: nil, 14 | voice_url: nil, 15 | voice_method: nil, 16 | voice_fallback_url: nil, 17 | voice_fallback_method: nil, 18 | voice_status_callback_url: nil, 19 | voice_status_callback_method: nil, 20 | date_created: nil, 21 | date_updated: nil, 22 | uri: nil 23 | 24 | use ExTwilio.Resource, 25 | import: [ 26 | :stream, 27 | :all, 28 | :find, 29 | :create, 30 | :update, 31 | :destroy 32 | ] 33 | 34 | def resource_name, do: "SIP/Domains" 35 | def resource_collection_name, do: "domains" 36 | def parents, do: [:account] 37 | end 38 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/participant.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.Participant do 2 | @moduledoc """ 3 | Represents a Participant resource in the Twilio API. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/voice/api/conference-participant-resource) 6 | 7 | ## Examples 8 | 9 | Since Participants belong to Conferences in the Twilio API, you must pass a 10 | conference to each function in this module. For example: 11 | 12 | ExTwilio.Participant.all(conference: "conference_sid") 13 | 14 | """ 15 | 16 | defstruct call_sid: nil, 17 | conference_sid: nil, 18 | date_created: nil, 19 | date_updated: nil, 20 | account_sid: nil, 21 | muted: nil, 22 | start_conference_on_enter: nil, 23 | end_conference_on_exit: nil, 24 | uri: nil, 25 | label: nil, 26 | call_sid_to_coach: nil, 27 | coaching: nil, 28 | hold: nil, 29 | status: nil 30 | 31 | use ExTwilio.Resource, import: [:stream, :all, :find, :update, :create, :destroy] 32 | 33 | def parents, do: [:account, :conference] 34 | end 35 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/task_router/task.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.TaskRouter.Task do 2 | @moduledoc """ 3 | A Task instance resource represents a single item of work waiting to be processed. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/api/taskrouter/tasks) 6 | """ 7 | 8 | defstruct sid: nil, 9 | account_sid: nil, 10 | assignment_status: nil, 11 | attributes: nil, 12 | date_created: nil, 13 | date_status_changed: nil, 14 | date_updated: nil, 15 | priority: nil, 16 | age: nil, 17 | reason: nil, 18 | timeout: nil, 19 | workspace_sid: nil, 20 | workflow_sid: nil, 21 | workflow_friendly_name: nil, 22 | task_queue_sid: nil, 23 | task_queue_friendly_name: nil, 24 | url: nil, 25 | links: nil 26 | 27 | use ExTwilio.Resource, import: [:stream, :all, :find, :create, :update, :delete] 28 | 29 | def parents, do: [%ExTwilio.Parent{module: ExTwilio.TaskRouter.Workspace, key: :workspace}] 30 | def children, do: [:attributes] 31 | end 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Daniel Berkompas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/application.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.Application do 2 | @moduledoc """ 3 | Represents an Application resource in the Twilio API. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/usage/api/applications) 6 | """ 7 | 8 | defstruct sid: nil, 9 | date_created: nil, 10 | date_updated: nil, 11 | friendly_name: nil, 12 | account_sid: nil, 13 | api_version: nil, 14 | voice_url: nil, 15 | voice_method: nil, 16 | voice_fallback_url: nil, 17 | voice_fallback_method: nil, 18 | status_callback: nil, 19 | voice_caller_id_lookup: nil, 20 | sms_url: nil, 21 | sms_method: nil, 22 | sms_fallback_url: nil, 23 | sms_fallback_method: nil, 24 | sms_status_callback: nil, 25 | message_status_callback: nil, 26 | uri: nil 27 | 28 | use ExTwilio.Resource, 29 | import: [ 30 | :stream, 31 | :all, 32 | :find, 33 | :create, 34 | :update, 35 | :destroy 36 | ] 37 | 38 | def parents, do: [:account] 39 | end 40 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/task_router/event.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.TaskRouter.Event do 2 | @moduledoc """ 3 | Represents the Event logs Twilio keeps track of. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/api/taskrouter/events) 6 | """ 7 | 8 | defstruct sid: nil, 9 | description: nil, 10 | account_sid: nil, 11 | event_type: nil, 12 | resource_type: nil, 13 | resource_sid: nil, 14 | resource_url: nil, 15 | event_date: nil, 16 | source: nil, 17 | source_ip_address: nil, 18 | actor_type: nil, 19 | actor_sid: nil, 20 | actor_url: nil, 21 | event_data: nil, 22 | url: nil 23 | 24 | use ExTwilio.Resource, import: [:stream, :all, :find] 25 | 26 | def parents, do: [%ExTwilio.Parent{module: ExTwilio.TaskRouter.Workspace, key: :workspace}] 27 | 28 | def children, 29 | do: [ 30 | :minutes, 31 | :start_date, 32 | :end_date, 33 | :event_type, 34 | :worker_sid, 35 | :task_queue_sid, 36 | :workflow_sid, 37 | :task_sid, 38 | :reservation_sid 39 | ] 40 | end 41 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/feedback.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.Feedback do 2 | @moduledoc """ 3 | Represents a Call Feedback resource in the Twilio API. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/voice/api/feedback-resource) 6 | 7 | ## Examples 8 | 9 | Since Call Feedback is a nested resource in the Twilio API, you must 10 | pass in a parent Call SID to all functions in this module. 11 | 12 | ExTwilio.Feedback.create([quality_score: 5], [call: "call_sid"]) 13 | ExTwilio.Feedback.find(call: "call_sid") 14 | 15 | """ 16 | 17 | defstruct quality_score: nil, 18 | issues: nil 19 | 20 | use ExTwilio.Resource, import: [:create] 21 | 22 | @doc """ 23 | Find feedback for a given call. Any options other than `[call: "sid"]` will 24 | result in a `FunctionClauseError`. 25 | 26 | ## Examples 27 | 28 | ExTwilio.Feedback.find(call: "sid") 29 | %ExTwilio.Feedback{issues: [], quality_score: 5} 30 | """ 31 | @spec find(call: String.t()) :: Parser.success() | Parser.error() 32 | def find(call: sid) do 33 | Api.find(__MODULE__, nil, call: sid) 34 | end 35 | 36 | def parents, do: [:account, :call] 37 | def resource_name, do: "Feedback" 38 | end 39 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/fax.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.Fax do 2 | @moduledoc """ 3 | Warning! currently in Public beta. 4 | 5 | Represents a Fax resource in the Twilio API. 6 | 7 | - [Twilio docs](https://www.twilio.com/docs/fax/api) 8 | 9 | Here is an example of sending an SMS message: 10 | 11 | ExTwilio.Fax.create( 12 | to: "+15558675310", 13 | from: "+15017122661", 14 | media_url: "https://www.twilio.com/docs/documents/25/justthefaxmaam.pdf" 15 | ) 16 | 17 | """ 18 | 19 | defstruct sid: nil, 20 | date_created: nil, 21 | date_updated: nil, 22 | account_sid: nil, 23 | from: nil, 24 | to: nil, 25 | num_pages: nil, 26 | status: nil, 27 | error_code: nil, 28 | error_message: nil, 29 | direction: nil, 30 | price: nil, 31 | price_unit: nil, 32 | api_version: nil, 33 | url: nil, 34 | links: nil, 35 | media_url: nil, 36 | media_sid: nil, 37 | quality: nil, 38 | duration: nil 39 | 40 | use ExTwilio.Resource, import: [:stream, :find, :create] 41 | end 42 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/task_router/worker.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.TaskRouter.Worker do 2 | @moduledoc """ 3 | Represents a worker resource who preforms tasks. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/api/taskrouter/workers) 6 | """ 7 | 8 | defstruct sid: nil, 9 | friendly_name: nil, 10 | account_sid: nil, 11 | activity_sid: nil, 12 | activity_name: nil, 13 | workspace_sid: nil, 14 | attributes: nil, 15 | available: nil, 16 | date_created: nil, 17 | date_updated: nil, 18 | date_status_changed: nil, 19 | url: nil, 20 | links: nil 21 | 22 | use ExTwilio.Resource, import: [:stream, :all, :find, :create, :update, :delete] 23 | 24 | def parents, do: [%ExTwilio.Parent{module: ExTwilio.TaskRouter.Workspace, key: :workspace}] 25 | 26 | def children do 27 | [ 28 | :friendly_name, 29 | :target_workers_expression, 30 | :available, 31 | :activity_name, 32 | :activity_sid, 33 | :task_queue_name, 34 | :task_queue_sid, 35 | :date_created, 36 | :date_updated, 37 | :date_status_changed 38 | ] 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/programmable_chat/service.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.ProgrammableChat.Service do 2 | @moduledoc """ 3 | Represents a Service resource in the Twilio Programmable Chat API. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/api/chat/rest/services) 6 | """ 7 | defstruct sid: nil, 8 | account_sid: nil, 9 | friendly_name: nil, 10 | date_created: nil, 11 | date_updated: nil, 12 | default_service_role_sid: nil, 13 | default_channel_role_sid: nil, 14 | default_channel_creator_role_sid: nil, 15 | typing_indicator_timeout: nil, 16 | read_status_enabled: nil, 17 | consumption_report_interval: nil, 18 | reachability_enabled: nil, 19 | limits: nil, 20 | pre_webhook_url: nil, 21 | post_webhook_url: nil, 22 | webhook_method: nil, 23 | webhook_filters: nil, 24 | notifications: nil, 25 | url: nil, 26 | links: nil 27 | 28 | use ExTwilio.Resource, 29 | import: [ 30 | :stream, 31 | :all, 32 | :find, 33 | :create, 34 | :update, 35 | :destroy 36 | ] 37 | end 38 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/lookup.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.Lookup do 2 | @moduledoc """ 3 | Represents the Lookup Api provided by Twilio 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/lookup/api) 6 | """ 7 | alias ExTwilio.{Parser, Config} 8 | alias ExTwilio.UrlGenerator, as: Url 9 | 10 | @base_url "https://lookups.twilio.com/v1/PhoneNumbers/" 11 | 12 | defmodule PhoneNumber do 13 | @moduledoc false 14 | defstruct url: nil, 15 | carrier: nil, 16 | caller_name: nil, 17 | national_format: nil, 18 | phone_number: nil, 19 | country_code: nil, 20 | add_ons: nil 21 | end 22 | 23 | @doc """ 24 | Retrieves information based on the inputed phone number. Supports Twilio's add-ons. 25 | 26 | ## Examples 27 | 28 | {:ok, info} = ExTwilio.Lookup.retrieve("12345678910", [Type: carrier]) 29 | 30 | """ 31 | def retrieve(phone_number, query \\ []) do 32 | auth = [basic_auth: {Config.account_sid(), Config.auth_token()}] 33 | query_string = "?" <> Url.to_query_string(query) 34 | 35 | "#{@base_url}#{phone_number}#{query_string}" 36 | |> HTTPoison.get!([], hackney: auth) 37 | |> Parser.parse(PhoneNumber) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/task_router/task_queue.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.TaskRouter.TaskQueue do 2 | @moduledoc """ 3 | TaskQueues are the resource you use to categorize Tasks 4 | and describe which Workers are eligible to handle those Tasks 5 | 6 | - [Twilio docs](https://www.twilio.com/docs/api/taskrouter/taskqueues) 7 | """ 8 | 9 | defstruct sid: nil, 10 | account_sid: nil, 11 | workspace_sid: nil, 12 | friendly_name: nil, 13 | target_workers: nil, 14 | max_reserverd_workers: nil, 15 | reservation_activity_sid: nil, 16 | reservation_activity_name: nil, 17 | assignment_activity_sid: nil, 18 | assignment_activity_name: nil, 19 | date_created: nil, 20 | date_updated: nil, 21 | url: nil, 22 | links: nil 23 | 24 | use ExTwilio.Resource, import: [:stream, :all, :find, :create, :update, :delete] 25 | 26 | def parents, do: [%ExTwilio.Parent{module: ExTwilio.TaskRouter.Workspace, key: :workspace}] 27 | 28 | def children, 29 | do: [ 30 | :friendly_name, 31 | :evaluate_worker_attributes, 32 | :reservation_activity_sid, 33 | :assignment_activity_sid, 34 | :target_workers 35 | ] 36 | end 37 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/call.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.Call do 2 | @moduledoc """ 3 | Represents an Call resource in the Twilio API. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/voice/api/call-resource) 6 | """ 7 | defstruct sid: nil, 8 | parent_call_sid: nil, 9 | date_created: nil, 10 | date_updated: nil, 11 | account_sid: nil, 12 | to: nil, 13 | from: nil, 14 | phone_number_sid: nil, 15 | status: nil, 16 | start_time: nil, 17 | end_time: nil, 18 | duration: nil, 19 | price: nil, 20 | price_unit: nil, 21 | direction: nil, 22 | answered_by: nil, 23 | forwarded_from: nil, 24 | caller_name: nil, 25 | uri: nil 26 | 27 | use ExTwilio.Resource, 28 | import: [ 29 | :stream, 30 | :all, 31 | :find, 32 | :create, 33 | :update, 34 | :destroy 35 | ] 36 | 37 | def cancel(%{sid: sid}), do: cancel(sid) 38 | def cancel(sid), do: update(sid, status: "canceled") 39 | 40 | def complete(%{sid: sid}), do: complete(sid) 41 | def complete(sid), do: update(sid, status: "completed") 42 | 43 | def parents, do: [:account] 44 | end 45 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/message.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.Message do 2 | @moduledoc """ 3 | Represents an Message resource in the Twilio API. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/sms/api/message-resource) 6 | 7 | ## Examples 8 | 9 | Here is an example of sending an SMS message: 10 | 11 | {target_number, twilio_number_you_own, body} = {"+12223334444", "+19223334444", "Hello World"} 12 | 13 | ExTwilio.Message.create(to: target_number, from: twilio_number_you_own, body: body) 14 | 15 | """ 16 | 17 | defstruct sid: nil, 18 | date_created: nil, 19 | date_updated: nil, 20 | date_sent: nil, 21 | account_sid: nil, 22 | from: nil, 23 | to: nil, 24 | body: nil, 25 | num_media: nil, 26 | num_segments: nil, 27 | status: nil, 28 | error_code: nil, 29 | error_message: nil, 30 | direction: nil, 31 | price: nil, 32 | price_unit: nil, 33 | api_version: nil, 34 | uri: nil, 35 | subresource_uri: nil, 36 | messaging_service_sid: nil 37 | 38 | use ExTwilio.Resource, 39 | import: [ 40 | :stream, 41 | :all, 42 | :find, 43 | :create, 44 | :update, 45 | :destroy 46 | ] 47 | 48 | def parents, do: [:account] 49 | end 50 | -------------------------------------------------------------------------------- /lib/ex_twilio/jwt/access_token/chat_grant.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.JWT.AccessToken.ChatGrant do 2 | @moduledoc """ 3 | A JWT grant to access a given Twilio chat service. 4 | 5 | ## Examples 6 | 7 | ExTwilio.JWT.AccessToken.ChatGrant.new( 8 | service_sid: "sid", 9 | endpoint_id: "123", 10 | deployment_role_sid: "sid", 11 | push_credential_sid: "sid" 12 | ) 13 | 14 | """ 15 | 16 | @enforce_keys [:service_sid] 17 | 18 | defstruct service_sid: nil, endpoint_id: nil, deployment_role_sid: nil, push_credential_sid: nil 19 | 20 | @type t :: %__MODULE__{ 21 | service_sid: String.t(), 22 | endpoint_id: String.t(), 23 | deployment_role_sid: String.t(), 24 | push_credential_sid: String.t() 25 | } 26 | 27 | @doc """ 28 | Create a new grant. 29 | """ 30 | @spec new(attrs :: Keyword.t()) :: t 31 | def new(attrs \\ []) do 32 | struct(__MODULE__, attrs) 33 | end 34 | 35 | defimpl ExTwilio.JWT.Grant do 36 | alias ExTwilio.Ext 37 | 38 | def type(_grant), do: "chat" 39 | 40 | def attrs(grant) do 41 | %{"service_sid" => grant.service_sid} 42 | |> Ext.Map.put_if("endpoint_id", grant.endpoint_id) 43 | |> Ext.Map.put_if("deployment_role_sid", grant.deployment_role_sid) 44 | |> Ext.Map.put_if("push_credential_sid", grant.push_credential_sid) 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/incoming_phone_number.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.IncomingPhoneNumber do 2 | @moduledoc """ 3 | Represents an IncomingPhoneNumber resource in the Twilio API. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/phone-numbers/api/incomingphonenumber-resource) 6 | """ 7 | 8 | defstruct sid: nil, 9 | account_sid: nil, 10 | address_sid: nil, 11 | address_requirements: nil, 12 | api_version: nil, 13 | beta: nil, 14 | capabilities: nil, 15 | date_created: nil, 16 | date_updated: nil, 17 | friendly_name: nil, 18 | identity_sid: nil, 19 | phone_number: nil, 20 | origin: nil, 21 | sms_application_sid: nil, 22 | sms_fallback_method: nil, 23 | sms_fallback_url: nil, 24 | sms_method: nil, 25 | sms_url: nil, 26 | status: nil, 27 | status_callback: nil, 28 | status_callback_method: nil, 29 | trunk_sid: nil, 30 | uri: nil, 31 | voice_application_sid: nil, 32 | voice_caller_id_lookup: nil, 33 | voice_fallback_method: nil, 34 | voice_fallback_url: nil, 35 | voice_method: nil, 36 | voice_url: nil, 37 | emergency_status: nil, 38 | emergency_address_sid: nil 39 | 40 | use ExTwilio.Resource, 41 | import: [ 42 | :stream, 43 | :all, 44 | :find, 45 | :create, 46 | :update, 47 | :destroy 48 | ] 49 | 50 | def parents, do: [:account] 51 | end 52 | -------------------------------------------------------------------------------- /test/ex_twilio/parser_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.ParserTest do 2 | use ExUnit.Case 3 | import ExTwilio.Parser 4 | 5 | defmodule Resource do 6 | defstruct sid: nil 7 | end 8 | 9 | doctest ExTwilio.Parser 10 | 11 | test ".parse should decode a successful response into a named struct" do 12 | response = %{body: "{ \"sid\": \"unique_id\" }", status_code: 200} 13 | assert {:ok, %Resource{sid: "unique_id"}} == parse(response, Resource) 14 | end 15 | 16 | test ".parse should return an error when response is 400" do 17 | response = %{body: "{ \"message\": \"Error message\" }", status_code: 400} 18 | assert {:error, %{"message" => "Error message"}, 400} == parse(response, Resource) 19 | end 20 | 21 | test ".parse should return :ok when response is 204 'No Content'" do 22 | response = %{body: "", status_code: 204} 23 | assert :ok == parse(response, Resource) 24 | end 25 | 26 | test ".parse should return :ok when response is 202 'Accepted'" do 27 | response = %{body: "", status_code: 202} 28 | assert :ok == parse(response, Resource) 29 | end 30 | 31 | test ".parse_list should decode into a list of named structs" do 32 | json = """ 33 | { 34 | "resources": [{ 35 | "sid": "first" 36 | }, { 37 | "sid": "second" 38 | }], 39 | "next_page": 10 40 | } 41 | """ 42 | 43 | response = %{body: json, status_code: 200} 44 | expected = [%Resource{sid: "first"}, %Resource{sid: "second"}] 45 | metadata = %{"next_page" => 10} 46 | 47 | assert {:ok, expected, metadata} == parse_list(response, Resource, "resources") 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.Mixfile do 2 | use Mix.Project 3 | 4 | @source_url "https://github.com/danielberkompas/ex_twilio" 5 | @version "0.10.0" 6 | 7 | def project do 8 | [ 9 | app: :ex_twilio, 10 | version: @version, 11 | elixir: "~> 1.2", 12 | name: "ExTwilio", 13 | package: package(), 14 | docs: docs(), 15 | deps: deps() 16 | ] 17 | end 18 | 19 | def application do 20 | [extra_applications: [:logger]] 21 | end 22 | 23 | defp deps do 24 | [ 25 | {:httpoison, ">= 0.9.0"}, 26 | {:jason, "~> 1.2"}, 27 | {:inflex, "~> 2.0"}, 28 | {:joken, "~> 2.0"}, 29 | {:dialyze, "~> 0.2.0", only: [:dev, :test]}, 30 | {:mock, "~> 0.3", only: :test}, 31 | {:ex_doc, ">= 0.0.0", only: [:dev, :test]}, 32 | {:inch_ex, ">= 0.0.0", only: [:dev, :test]} 33 | ] 34 | end 35 | 36 | defp docs do 37 | [ 38 | extras: [ 39 | "CHANGELOG.md": [title: "Changelog"], 40 | "CONTRIBUTING.md": [title: "Contributing"], 41 | "LICENSE.md": [title: "License"], 42 | "README.md": [title: "Overview"], 43 | "CALLING_TUTORIAL.md": [title: "Calling Tutorial"] 44 | ], 45 | main: "readme", 46 | source_url: @source_url, 47 | source_ref: "v#{@version}", 48 | skip_undefined_reference_warnings_on: ["CHANGELOG.md"], 49 | formatters: ["html"] 50 | ] 51 | end 52 | 53 | defp package do 54 | [ 55 | description: "Twilio API library for Elixir", 56 | maintainers: ["Daniel Berkompas"], 57 | licenses: ["MIT"], 58 | links: %{ 59 | "Changelog" => "https://hexdocs.pm/ex_twilio/changelog.html", 60 | "Github" => @source_url 61 | } 62 | ] 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /test/ex_twilio/jwt/access_token/chat_grant_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.JWT.AccessToken.ChatGrantTest do 2 | use ExUnit.Case 3 | 4 | alias ExTwilio.JWT.AccessToken.ChatGrant 5 | alias ExTwilio.JWT.Grant 6 | 7 | describe "__struct__" do 8 | test "enforces :service_sid" do 9 | assert_raise ArgumentError, fn -> 10 | Code.eval_string("%ExTwilio.JWT.AccessToken.ChatGrant{}") 11 | end 12 | 13 | assert %ChatGrant{service_sid: "sid"} 14 | end 15 | end 16 | 17 | describe ".new/1" do 18 | test "accepts all attributes" do 19 | assert ChatGrant.new( 20 | service_sid: "sid", 21 | endpoint_id: "id", 22 | deployment_role_sid: "sid", 23 | push_credential_sid: "sid" 24 | ) == %ChatGrant{ 25 | service_sid: "sid", 26 | endpoint_id: "id", 27 | deployment_role_sid: "sid", 28 | push_credential_sid: "sid" 29 | } 30 | end 31 | end 32 | 33 | test "implements ExTwilio.JWT.Grant" do 34 | assert Grant.type(%ChatGrant{service_sid: "sid"}) == "chat" 35 | 36 | assert Grant.attrs(%ChatGrant{service_sid: "sid"}) == %{ 37 | "service_sid" => "sid" 38 | } 39 | 40 | assert Grant.attrs(%ChatGrant{service_sid: "sid", endpoint_id: "id"}) == %{ 41 | "service_sid" => "sid", 42 | "endpoint_id" => "id" 43 | } 44 | 45 | assert Grant.attrs(%ChatGrant{service_sid: "sid", deployment_role_sid: "sid"}) == %{ 46 | "service_sid" => "sid", 47 | "deployment_role_sid" => "sid" 48 | } 49 | 50 | assert Grant.attrs(%ChatGrant{service_sid: "sid", push_credential_sid: "sid"}) == %{ 51 | "service_sid" => "sid", 52 | "push_credential_sid" => "sid" 53 | } 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/notify/credential.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.Notify.Credential do 2 | @moduledoc """ 3 | Represents a Credential resource in the Twilio Notify. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/notify/api/credentials) 6 | 7 | - type Credential type, one of "gcm", "fcm", or "apn" 8 | - friendly_name Friendly name for stored credential 9 | - certificate [APN only] URL encoded representation of the certificate. Strip everything outside of the headers, e.g. `-----BEGIN 10 | CERTIFICATE-----MIIFnTCCBIWgAwIBAgIIAjy9H849+E8wDQYJKoZIhvcNAQEFBQAwgZYxCzAJBgNV.....A==-----END CERTIFICATE-----` 11 | - private_key [APN only] URL encoded representation of the private key. Strip everything outside of the headers, e.g. `-----BEGIN RSA PRIVATE 12 | KEY-----MIIEpQIBAAKCAQEAuyf/lNrH9ck8DmNyo3fGgvCI1l9s+cmBY3WIz+cUDqmxiieR\n.-----END RSA PRIVATE KEY-----` 13 | - sandbox [APN only] use this credential for sending to production or sandbox APNs (string `true` or `false`) 14 | - api_key [GCM only] This is the "Server key" of your project from 15 | Firebase console under Settings / Cloud messaging. Yes, you can use the 16 | server key from the Firebase console for GCM. 17 | - secret [FCM only] This is the "Server key" of your project from Firebase console under Settings / Cloud messaging. 18 | """ 19 | defstruct sid: nil, 20 | account_sid: nil, 21 | friendly_name: nil, 22 | type: nil, 23 | certificate: nil, 24 | private_key: nil, 25 | sandbox: nil, 26 | api_key: nil, 27 | secret: nil, 28 | date_created: nil, 29 | date_updated: nil, 30 | url: nil 31 | 32 | use ExTwilio.Resource, 33 | import: [ 34 | :stream, 35 | :all, 36 | :find, 37 | :create, 38 | :update, 39 | :destroy 40 | ] 41 | end 42 | -------------------------------------------------------------------------------- /lib/ex_twilio/result_stream.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.ResultStream do 2 | @moduledoc """ 3 | Generate a stream of results for a given Twilio API URL. Pages are lazily 4 | loaded on demand. 5 | 6 | ## Examples 7 | 8 | ExTwilio.ResultStream.new(ExTwilio.Call) 9 | |> Stream.map(fn call -> call.sid end) 10 | |> Enum.take(5) 11 | 12 | # => [%ExTwilio.Call{ ... }, %ExTwilio.Call{ ... }, ...] 13 | """ 14 | 15 | alias ExTwilio.Api 16 | alias ExTwilio.Parser 17 | alias ExTwilio.UrlGenerator 18 | alias ExTwilio.Config 19 | 20 | @type url :: String.t() 21 | 22 | @doc """ 23 | Create a new Stream. 24 | 25 | ## Parameters 26 | 27 | - `module`: The name of the module to create a Stream of results for. 28 | 29 | ## Examples 30 | 31 | ExTwilio.ResultStream.new(ExTwilio.Call) 32 | """ 33 | def new(module, options \\ []) do 34 | url = UrlGenerator.build_url(module, nil, options) 35 | 36 | Stream.resource(fn -> {url, module, options} end, &process_page/1, fn _ -> nil end) 37 | end 38 | 39 | @spec fetch_page(url, module, options :: list) :: {list, {url, module, options :: list}} 40 | defp fetch_page(url, module, options) do 41 | results = Api.get!(url, Api.auth_header(options)) 42 | {:ok, items, meta} = Parser.parse_list(results, module, module.resource_collection_name) 43 | {items, {next_page_url(meta["next_page_uri"]), module, options}} 44 | end 45 | 46 | @spec process_page({url | nil, module, options :: list}) :: 47 | {:halt, nil} 48 | | {list, {url, module}} 49 | defp process_page({nil, _module, _options}), do: {:halt, nil} 50 | 51 | defp process_page({next_page_uri, module, options}) do 52 | fetch_page(next_page_uri, module, options) 53 | end 54 | 55 | defp next_page_url(nil), do: nil 56 | defp next_page_url(uri), do: "https://#{Config.api_domain()}" <> uri 57 | end 58 | -------------------------------------------------------------------------------- /test/ex_twilio/resource_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.ResourceTest do 2 | use ExUnit.Case 3 | alias ExTwilio.Api 4 | import Mock 5 | 6 | defmodule TestResource do 7 | defstruct sid: nil 8 | use ExTwilio.Resource, import: [:stream, :all, :list, :find, :create, :update, :destroy] 9 | end 10 | 11 | test "only imports specified methods" do 12 | defmodule ExclusiveMethods do 13 | defstruct sid: nil 14 | use ExTwilio.Resource, import: [:stream] 15 | end 16 | 17 | Enum.each([:all, :list, :find, :create, :update, :destroy], fn method -> 18 | assert_raise UndefinedFunctionError, fn -> 19 | apply(ExclusiveMethods, method, ["id"]) 20 | end 21 | end) 22 | end 23 | 24 | test ".new should return the module's struct" do 25 | assert %TestResource{} == TestResource.new() 26 | assert %TestResource{sid: "hello"} == TestResource.new(sid: "hello") 27 | end 28 | 29 | test ".find should delegate to Api.find" do 30 | with_mock Api, find: fn _, _, _ -> nil end do 31 | TestResource.find("id") 32 | assert called(Api.find(TestResource, "id", [])) 33 | end 34 | end 35 | 36 | test ".create should delegate to Api.create" do 37 | with_mock Api, create: fn _, _, _ -> nil end do 38 | TestResource.create(field: "value") 39 | assert called(Api.create(TestResource, [field: "value"], [])) 40 | end 41 | end 42 | 43 | test ".update should delegate to Api.update" do 44 | with_mock Api, update: fn _, _, _, _ -> nil end do 45 | TestResource.update("id", field: "value") 46 | assert called(Api.update(TestResource, "id", [field: "value"], [])) 47 | end 48 | end 49 | 50 | test ".destroy should delegate to Api.destroy" do 51 | with_mock Api, destroy: fn _, _, _ -> nil end do 52 | TestResource.destroy("id") 53 | assert called(Api.destroy(TestResource, "id", [])) 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/ex_twilio/request_validator.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.RequestValidator do 2 | @moduledoc """ 3 | Validates the authenticity of a Twilio request. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/usage/security) 6 | """ 7 | 8 | alias ExTwilio.Config 9 | 10 | import Bitwise 11 | 12 | def valid?(url, params, signature) do 13 | valid?(url, params, signature, Config.auth_token()) 14 | end 15 | 16 | def valid?(url, params, signature, auth_token) do 17 | url 18 | |> data_for(params) 19 | |> compute_hmac(auth_token) 20 | |> Base.encode64() 21 | |> String.trim() 22 | |> secure_compare(signature) 23 | end 24 | 25 | defp data_for(url, params), do: url <> combine(params) 26 | 27 | defp combine(params) do 28 | params 29 | |> Map.keys() 30 | |> Enum.sort() 31 | |> Enum.map(fn key -> key <> Map.get(params, key) end) 32 | |> Enum.join() 33 | end 34 | 35 | defp compute_hmac(data, key), do: hmac(:sha, key, data) 36 | 37 | # TODO: remove when we require OTP 22 38 | if System.otp_release() >= "22" do 39 | defp hmac(digest, key, data), do: :crypto.mac(:hmac, digest, key, data) 40 | else 41 | defp hmac(digest, key, data), do: :crypto.hmac(digest, key, data) 42 | end 43 | 44 | # Implementation taken from Plug.Crypto 45 | # https://github.com/elixir-plug/plug/blob/master/lib/plug/crypto.ex 46 | # 47 | # Compares the two binaries in constant-time to avoid timing attacks. 48 | # See: http://codahale.com/a-lesson-in-timing-attacks/ 49 | defp secure_compare(left, right) do 50 | if byte_size(left) == byte_size(right) do 51 | secure_compare(left, right, 0) == 0 52 | else 53 | false 54 | end 55 | end 56 | 57 | defp secure_compare(<>, <>, acc) do 58 | xorred = bxor(x, y) 59 | secure_compare(left, right, acc ||| xorred) 60 | end 61 | 62 | defp secure_compare(<<>>, <<>>, acc) do 63 | acc 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /test/ex_twilio/result_stream_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.ResultStreamTest do 2 | use ExUnit.Case, async: false 3 | alias ExTwilio.ResultStream 4 | import TestHelper 5 | 6 | defmodule Resource do 7 | defstruct sid: nil, name: nil 8 | def resource_name, do: "Resources" 9 | def resource_collection_name, do: "resources" 10 | def parents, do: [] 11 | def children, do: [] 12 | end 13 | 14 | test ".new returns a new ResultStream" do 15 | assert is_function(ResultStream.new(Resource)) 16 | end 17 | 18 | test "can return all results" do 19 | with_streaming_fixture(fn -> 20 | expected = [%Resource{sid: "1", name: "first"}, %Resource{sid: "2", name: "second"}] 21 | actual = ResultStream.new(Resource) |> Enum.into([]) 22 | assert actual == expected 23 | end) 24 | end 25 | 26 | test "can filter results" do 27 | with_streaming_fixture(fn -> 28 | actual = 29 | ResultStream.new(Resource) 30 | |> Stream.filter(fn res -> res.name == "second" end) 31 | |> Enum.take(1) 32 | 33 | assert actual == [%Resource{sid: "2", name: "second"}] 34 | end) 35 | end 36 | 37 | test "can map results" do 38 | with_streaming_fixture(fn -> 39 | actual = 40 | ResultStream.new(Resource) 41 | |> Stream.map(fn res -> res.name end) 42 | |> Enum.into([]) 43 | 44 | assert actual == ["first", "second"] 45 | end) 46 | end 47 | 48 | defp with_streaming_fixture(fun) do 49 | with_fixture( 50 | {:get!, 51 | fn url, _headers -> 52 | if String.match?(url, ~r/\?Page=2/) do 53 | json_response(page2(), 200) 54 | else 55 | json_response(page1(), 200) 56 | end 57 | end}, 58 | fun 59 | ) 60 | end 61 | 62 | defp page1 do 63 | %{ 64 | "resources" => [ 65 | %{sid: "1", name: "first"} 66 | ], 67 | next_page_uri: "?Page=2" 68 | } 69 | end 70 | 71 | defp page2 do 72 | %{ 73 | "resources" => [ 74 | %{sid: "2", name: "second"} 75 | ], 76 | next_page_uri: nil 77 | } 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/ex_twilio/jwt/access_token/voice_grant.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.JWT.AccessToken.VoiceGrant do 2 | @moduledoc """ 3 | A JWT grant to access a given Twilio voice service. 4 | 5 | ## Examples 6 | 7 | ExTwilio.JWT.AccessToken.VoiceGrant.new( 8 | outgoing_application_sid: "application_sid", 9 | outgoing_application: %{"key" => "value"}, 10 | incoming_allow: true, 11 | push_credential_sid: "push_credential_sid", 12 | endpoint_id: "endpoint_id" 13 | ) 14 | 15 | """ 16 | 17 | defstruct outgoing_application_sid: nil, 18 | outgoing_application_params: nil, 19 | incoming_allow: nil, 20 | push_credential_sid: nil, 21 | endpoint_id: nil 22 | 23 | @type t :: %__MODULE__{ 24 | outgoing_application_sid: String.t(), 25 | outgoing_application_params: String.t(), 26 | incoming_allow: boolean(), 27 | push_credential_sid: String.t(), 28 | endpoint_id: String.t() 29 | } 30 | 31 | @doc """ 32 | Create a new grant. 33 | """ 34 | @spec new(attrs :: Keyword.t()) :: t 35 | def new(attrs \\ []) do 36 | struct(__MODULE__, attrs) 37 | end 38 | 39 | defimpl ExTwilio.JWT.Grant do 40 | alias ExTwilio.Ext 41 | 42 | def type(_grant), do: "voice" 43 | 44 | def attrs(grant) do 45 | %{} 46 | |> Ext.Map.put_if("outgoing", outgoing_attrs(grant)) 47 | |> Ext.Map.put_if("incoming", incoming_attrs(grant)) 48 | |> Ext.Map.put_if("push_credential_sid", grant.push_credential_sid) 49 | |> Ext.Map.put_if("endpoint_id", grant.endpoint_id) 50 | end 51 | 52 | defp outgoing_attrs(%{outgoing_application_sid: sid} = grant) 53 | when is_binary(sid) and sid != "" do 54 | Ext.Map.put_if(%{"application_sid" => sid}, "params", grant.outgoing_application_params) 55 | end 56 | 57 | defp outgoing_attrs(_grant), do: nil 58 | 59 | defp incoming_attrs(%{incoming_allow: incoming_allow}) when is_boolean(incoming_allow) do 60 | %{"allow" => incoming_allow} 61 | end 62 | 63 | defp incoming_attrs(_grant), do: nil 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /test/ex_twilio/jwt/access_token/voice_grant_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.JWT.AccessToken.VoiceGrantTest do 2 | use ExUnit.Case 3 | 4 | alias ExTwilio.JWT.AccessToken.VoiceGrant 5 | alias ExTwilio.JWT.Grant 6 | 7 | describe ".new/1" do 8 | test "accepts all attributes" do 9 | assert VoiceGrant.new( 10 | outgoing_application_sid: "outgoing_application_sid", 11 | outgoing_application_params: %{"key" => "value"}, 12 | incoming_allow: true, 13 | endpoint_id: "endpoint_id", 14 | push_credential_sid: "push_credential_sid" 15 | ) == %VoiceGrant{ 16 | outgoing_application_sid: "outgoing_application_sid", 17 | outgoing_application_params: %{"key" => "value"}, 18 | incoming_allow: true, 19 | endpoint_id: "endpoint_id", 20 | push_credential_sid: "push_credential_sid" 21 | } 22 | end 23 | end 24 | 25 | test "implements ExTwilio.JWT.Grant" do 26 | assert Grant.type(%VoiceGrant{}) == "voice" 27 | 28 | assert Grant.attrs(%VoiceGrant{ 29 | outgoing_application_sid: "sid", 30 | outgoing_application_params: %{key: "value"} 31 | }) == %{ 32 | "outgoing" => %{ 33 | "application_sid" => "sid", 34 | "params" => %{key: "value"} 35 | } 36 | } 37 | 38 | assert Grant.attrs(%VoiceGrant{incoming_allow: true}) == %{ 39 | "incoming" => %{ 40 | "allow" => true 41 | } 42 | } 43 | 44 | assert Grant.attrs(%VoiceGrant{endpoint_id: "endpoint_id"}) == %{ 45 | "endpoint_id" => "endpoint_id" 46 | } 47 | 48 | assert Grant.attrs(%VoiceGrant{push_credential_sid: "push_credential_sid"}) == %{ 49 | "push_credential_sid" => "push_credential_sid" 50 | } 51 | end 52 | 53 | test "does not include outgoing_application_params when outgoing_application_sid not defined" do 54 | assert Grant.attrs(%VoiceGrant{outgoing_application_params: %{key: "value"}}) == %{} 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /test/ex_twilio/worker_capability_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.WorkerCapabilityTest do 2 | use ExUnit.Case 3 | 4 | alias ExTwilio.Config 5 | 6 | test ".new sets the worker capability to one hour" do 7 | assert ExTwilio.WorkerCapability.new("worker_sid", "workspace_sid").ttl == 3600 8 | end 9 | 10 | test ".new sets the start time for the TTL to be the current time" do 11 | assert_in_delta ExTwilio.WorkerCapability.new("worker_sid", "workspace_sid").start_time, 12 | :erlang.system_time(:seconds), 13 | 1000 14 | end 15 | 16 | test ".new sets the account sid from the config" do 17 | assert ExTwilio.WorkerCapability.new("worker_sid", "workspace_sid").account_sid == 18 | Config.account_sid() 19 | end 20 | 21 | test ".new sets the auth token from the config" do 22 | assert ExTwilio.WorkerCapability.new("worker_sid", "workspace_sid").auth_token == 23 | Config.auth_token() 24 | end 25 | 26 | test ".new sets the worker sid" do 27 | assert ExTwilio.WorkerCapability.new("worker_sid", "workspace_sid").worker_sid == "worker_sid" 28 | end 29 | 30 | test ".new sets the workspace sid" do 31 | assert ExTwilio.WorkerCapability.new("worker_sid", "workspace_sid").workspace_sid == 32 | "workspace_sid" 33 | end 34 | 35 | test ".token sets the issuer to the account sid" do 36 | assert decoded_token(ExTwilio.WorkerCapability.new("worker_sid", "workspace_sid")).claims[ 37 | "iss" 38 | ] == Config.account_sid() 39 | end 40 | 41 | test ".token sets 9 policies" do 42 | jwt = 43 | ExTwilio.WorkerCapability.new("worker_sid", "workspace_sid") 44 | |> ExTwilio.WorkerCapability.allow_activity_updates() 45 | |> ExTwilio.WorkerCapability.allow_reservation_updates() 46 | 47 | assert length(decoded_token(jwt).claims["policies"]) == 9 48 | end 49 | 50 | defp decoded_token(capability) do 51 | signer = Joken.Signer.create("HS256", Config.auth_token()) 52 | 53 | {:ok, verified} = 54 | capability 55 | |> ExTwilio.WorkerCapability.token() 56 | |> Joken.verify(signer) 57 | 58 | %{:claims => verified} 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /test/ex_twilio/url_generator_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.UrlGeneratorTest do 2 | use ExUnit.Case 3 | 4 | import ExTwilio.UrlGenerator 5 | 6 | defmodule Resource do 7 | defstruct sid: nil, name: nil 8 | def resource_name, do: "Resources" 9 | def resource_collection_name, do: "resources" 10 | def parents, do: [:account, :sip_ip_access_control_list] 11 | def children, do: [:iso_country_code, :type] 12 | end 13 | 14 | defmodule Submodule.Parent do 15 | defstruct sid: nil, name: nil 16 | def resource_name, do: "Parents" 17 | def resource_collection_name, do: "parents" 18 | def parents, do: [:account] 19 | def children, do: [:child] 20 | end 21 | 22 | defmodule Submodule.Child do 23 | defstruct sid: nil, name: nil 24 | def resource_name, do: "Children" 25 | def resource_collection_name, do: "children" 26 | def parents, do: [:account, %ExTwilio.Parent{module: Submodule.Parent, key: :parent}] 27 | def children, do: [] 28 | end 29 | 30 | describe "query strings" do 31 | test "to_query_string can handle a value of type list without error" do 32 | params = [ 33 | status_callback: "http://example.com/status_callback", 34 | status_callback_event: ["ringing", "answered", "completed"] 35 | ] 36 | 37 | assert ExTwilio.UrlGenerator.to_query_string(params) == 38 | "StatusCallback=http%3A%2F%2Fexample.com%2Fstatus_callback&StatusCallbackEvent=ringing&StatusCallbackEvent=answered&StatusCallbackEvent=completed" 39 | end 40 | 41 | test "ignores parent keys at the root module level" do 42 | options = [account: "1234"] 43 | refute ExTwilio.UrlGenerator.build_url(Submodule.Child, nil, options) =~ "Account=1234" 44 | end 45 | 46 | test "ignores parent keys when parent in submodule" do 47 | options = [parent: "1234"] 48 | refute ExTwilio.UrlGenerator.build_url(Submodule.Child, nil, options) =~ "Parent=1234" 49 | end 50 | end 51 | 52 | describe "building urls for modules with parent in submodule" do 53 | test "builds a correct url for parent in a submodule" do 54 | options = [account: 43, parent: 4551] 55 | 56 | assert ExTwilio.UrlGenerator.build_url(Submodule.Child, nil, options) == 57 | "https://api.twilio.com/2010-04-01/Accounts/43/Parents/4551/Children.json" 58 | end 59 | end 60 | 61 | doctest ExTwilio.UrlGenerator 62 | end 63 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/notify/binding.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.Notify.Binding do 2 | @moduledoc """ 3 | Represents a Binding resource in the Twilio Notify. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/notify/api/bindings) 6 | 7 | - identity The Identity to which this Binding belongs to. Identity is defined 8 | by your application. Up to 20 Bindings can be created for the same Identity 9 | in a given Service. 10 | - binding_type The type of the Binding. This determines the transport technology to use. Allowed values: `apn`, `fcm`, `gcm`, `sms`, and `facebook-messenger`. 11 | - address The address specific to the channel. For APNS it is the device 12 | token. For FCM and GCM it is the registration token. For SMS it is a phone 13 | number in E.164 format. For Facebook Messenger it is the Messenger ID of the user or a phone number in E.164 format. 14 | - tag The list of tags associated with this Binding. Tags can be used to 15 | select the Bindings to use when sending a notification. Maximum 20 tags are 16 | allowed. 17 | - notification_protocol_version The version of the protocol (data format) 18 | used to send the notification. This defaults to the value of 19 | DefaultXXXNotificationProtocolVersion in the `ExTwilio.Notify.Service`. 20 | The current version is `"3"` for `apn`, `fcm`, and `gcm` type Bindings. The 21 | parameter is not applicable to `sms` and `facebook-messenger` type Bindings as the data format is fixed. 22 | - credential_sid The unique identifier (SID) of the 23 | `ExTwilio.Notify.Credential` resource to be used to send notifications to 24 | this Binding. If present, this overrides the Credential specified in the 25 | Service resource. Applicable only to `apn`, `fcm`, and `gcm` type Bindings. 26 | """ 27 | defstruct sid: nil, 28 | account_sid: nil, 29 | service_sid: nil, 30 | credential_sid: nil, 31 | date_created: nil, 32 | date_updated: nil, 33 | notification_protocol_version: nil, 34 | identity: nil, 35 | binding_type: nil, 36 | address: nil, 37 | tags: nil, 38 | tag: nil, 39 | url: nil, 40 | links: nil 41 | 42 | use ExTwilio.Resource, 43 | import: [ 44 | :stream, 45 | :all, 46 | :find, 47 | :create, 48 | :update, 49 | :destroy 50 | ] 51 | 52 | def parents, 53 | do: [ 54 | %ExTwilio.Parent{module: ExTwilio.Notify.Service, key: :service} 55 | ] 56 | end 57 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/account.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.Account do 2 | @moduledoc """ 3 | Represents an Account or Subaccount resource. 4 | 5 | - [Account docs](https://www.twilio.com/docs/iam/api/account) 6 | - [Subaccount docs](https://www.twilio.com/docs/api/rest/subaccounts) 7 | 8 | ## Examples 9 | 10 | An `ExTwilio.Account` can represent either an Account or a SubAccount. To see 11 | all accounts and subaccounts that your auth_token has access to, run: 12 | 13 | ExTwilio.Account.all 14 | 15 | If you want to find a SubAccount, use `find/1`. 16 | 17 | ExTwilio.Account.find("sid") 18 | 19 | If you want to see items associated with a SubAccount, you can do so by 20 | passing in an `account:` option in all other ExTwilio resources. For example: 21 | 22 | ExTwilio.Call.stream(account: "subaccount_sid") 23 | 24 | """ 25 | 26 | defstruct sid: nil, 27 | owner_account_sid: nil, 28 | date_created: nil, 29 | date_updated: nil, 30 | friendly_name: nil, 31 | type: nil, 32 | status: nil, 33 | auth_token: nil, 34 | uri: nil, 35 | subresource_uris: nil 36 | 37 | use ExTwilio.Resource, import: [:stream, :all, :find, :create, :update] 38 | 39 | @doc """ 40 | Suspend an Account by updating its status to "suspended". 41 | 42 | - [Twilio Docs](https://www.twilio.com/docs/api/rest/subaccounts#suspending-subaccounts) 43 | 44 | ## Examples 45 | 46 | {:ok, account} = ExTwilio.Account.find("") 47 | ExTwilio.Account.suspend(account) 48 | """ 49 | @spec suspend(map | String.t()) :: Parser.success() | Parser.error() 50 | def suspend(%{sid: sid}), do: suspend(sid) 51 | def suspend(sid), do: update(sid, status: "suspended") 52 | 53 | @doc """ 54 | Reactivate a suspended Account by updating its status to "active". 55 | 56 | - [Twilio Docs](https://www.twilio.com/docs/api/rest/subaccounts#suspending-subaccounts) 57 | 58 | ## Examples 59 | 60 | {:ok, account} = ExTwilio.Account.find("") 61 | ExTwilio.Account.reactivate(account) 62 | """ 63 | @spec reactivate(map | String.t()) :: Parser.success() | Parser.error() 64 | def reactivate(%{sid: sid}), do: reactivate(sid) 65 | def reactivate(sid), do: update(sid, status: "active") 66 | 67 | @doc """ 68 | Permanently close an Account by updating its status to "closed". This cannot 69 | be undone, so use it carefully! 70 | 71 | - [Twilio Docs](https://www.twilio.com/docs/api/rest/subaccounts#closing-subaccounts) 72 | 73 | ## Examples 74 | 75 | {:ok, account} = ExTwilio.Account.find("") 76 | ExTwilio.Account.close(account) 77 | """ 78 | @spec close(map | String.t()) :: Parser.success() | Parser.error() 79 | def close(%{sid: sid}), do: close(sid) 80 | def close(sid), do: update(sid, status: "closed") 81 | 82 | def parents, do: [:account] 83 | end 84 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/notify/service.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.Notify.Service do 2 | @moduledoc """ 3 | Represents a Service resource in the Twilio Notify. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/notify/api/services) 6 | 7 | - friendly_name Human-readable name for this service instance 8 | - apn_credential_sid The SID of the `ExTwilio.Notify.Credential` to be used 9 | for APN Bindings. 10 | - gcm_credential_sid The SID of the `ExTwilio.Notify.Credential` to be used for GCM Bindings. 11 | - messaging_service_sid The SID of the [Messaging Service] 12 | (https://www.twilio.com/docs/api/rest/sending-messages#messaging-services) to 13 | be used for SMS Bindings. In order to send SMS notifications this parameter 14 | has to be set. 15 | - facebook_messenger_page_id The Page ID to be used to send for Facebook 16 | Messenger Bindings. It has to match the Page ID you configured when you 17 | [enabled Facebook Messaging](https://www.twilio.com/console/sms/settings) on your account. 18 | - default_apn_notification_protocol_version The version of the protocol to be 19 | used for sending APNS notifications. Can be overriden on a Binding by Binding basis when creating a `ExTwilio.Notify.Bindings` resource. 20 | - default_gcm_notification_protocol_version The version of the protocol to be 21 | used for sending GCM notifications. Can be overriden on a Binding by Binding 22 | basis when creating a `ExTwilio.Notify.Bindings` resource. 23 | - fcm_credential_sid The SID of the `ExTwilio.Notify.Credential` to be used 24 | for FCM Bindings. 25 | - default_fcm_notification_protocol_version The version of the protocol to be 26 | used for sending FCM notifications. Can be overriden on a Binding by Binding 27 | basis when creating a `ExTwilio.Notify.Credential` resource. 28 | - log_enabled The log_enabled 29 | - alexa_skill_id The alexa_skill_id 30 | - default_alexa_notification_protocol_version The default_alexa_notification_protocol_version 31 | 32 | """ 33 | defstruct sid: nil, 34 | account_sid: nil, 35 | friendly_name: nil, 36 | date_created: nil, 37 | date_updated: nil, 38 | apn_credential_sid: nil, 39 | gcm_credential_sid: nil, 40 | fcm_credential_sid: nil, 41 | messaging_service_sid: nil, 42 | facebook_messenger_page_id: nil, 43 | default_apn_notification_protocol_version: nil, 44 | default_gcm_notification_protocol_version: nil, 45 | default_fcm_notification_protocol_version: nil, 46 | log_enabled: nil, 47 | url: nil, 48 | links: nil, 49 | alexa_skill_id: nil, 50 | default_alexa_notification_protocol_version: nil 51 | 52 | use ExTwilio.Resource, 53 | import: [ 54 | :stream, 55 | :all, 56 | :find, 57 | :create, 58 | :update, 59 | :destroy 60 | ] 61 | end 62 | -------------------------------------------------------------------------------- /lib/ex_twilio/config.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.Config do 2 | @moduledoc """ 3 | Stores configuration variables used to communicate with Twilio's API. 4 | 5 | All settings also accept `{:system, "ENV_VAR_NAME"}` to read their 6 | values from environment variables at runtime. 7 | """ 8 | 9 | @doc """ 10 | Returns the Twilio Account SID. Set it in `mix.exs`: 11 | 12 | config :ex_twilio, account_sid: "YOUR_ACCOUNT_SID" 13 | """ 14 | def account_sid, do: from_env(:ex_twilio, :account_sid) 15 | 16 | @doc """ 17 | Returns the Twilio Auth Token for your account. Set it in `mix.exs`: 18 | 19 | config :ex_twilio, auth_token: "YOUR_AUTH_TOKEN" 20 | """ 21 | def auth_token, do: from_env(:ex_twilio, :auth_token) 22 | 23 | @doc """ 24 | Returns the domain of the Twilio API. This will default to "api.twilio.com", 25 | but can be overridden using the following setting in `mix.exs`: 26 | 27 | config :ex_twilio, api_domain: "other.twilio.com" 28 | """ 29 | def api_domain, do: from_env(:ex_twilio, :api_domain, "api.twilio.com") 30 | 31 | @doc """ 32 | Returns the protocol used for the Twilio API. The default is `"https"` for 33 | interacting with the Twilio API, but when testing with Bypass, you may want 34 | this to be `"http"`. 35 | """ 36 | def protocol, do: Application.get_env(:ex_twilio, :protocol) || "https" 37 | 38 | @doc """ 39 | Options added to HTTPoison requests 40 | """ 41 | def request_options, do: from_env(:ex_twilio, :request_options, []) 42 | 43 | @doc """ 44 | Returns the version of the API that ExTwilio is going to talk to. Set it in 45 | `mix.exs`: 46 | config :ex_twilio, api_version: "2015-05-06" 47 | """ 48 | def api_version, do: Application.get_env(:ex_twilio, :api_version) || "2010-04-01" 49 | 50 | def workspace_sid, do: Application.get_env(:ex_twilio, :workspace_sid) || "12345" 51 | 52 | @doc """ 53 | Return the combined base URL of the Twilio API, using the configuration 54 | settings given. 55 | """ 56 | def base_url, do: "#{protocol()}://#{api_domain()}/#{api_version()}" 57 | 58 | def fax_url, do: "https://fax.twilio.com/v1" 59 | 60 | def task_router_url, do: "https://taskrouter.twilio.com/v1" 61 | 62 | def task_router_websocket_base_url, do: "https://event-bridge.twilio.com/v1/wschannels" 63 | 64 | def programmable_chat_url, do: "https://chat.twilio.com/v2" 65 | 66 | def notify_url, do: "https://notify.twilio.com/v1" 67 | 68 | def studio_url, do: "https://studio.twilio.com/v1" 69 | 70 | def video_url, do: "https://video.twilio.com/v1" 71 | 72 | @doc """ 73 | A light wrapper around `Application.get_env/2`, providing automatic support for 74 | `{:system, "VAR"}` tuples. 75 | """ 76 | def from_env(otp_app, key, default \\ nil) 77 | 78 | def from_env(otp_app, key, default) do 79 | otp_app 80 | |> Application.get_env(key, default) 81 | |> read_from_system(default) 82 | end 83 | 84 | defp read_from_system({:system, env}, default), do: System.get_env(env) || default 85 | defp read_from_system(value, _default), do: value 86 | end 87 | -------------------------------------------------------------------------------- /test/ex_twilio/jwt/access_token_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.JWT.AccessTokenTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias ExTwilio.JWT.AccessToken 5 | 6 | describe ".new/1" do 7 | test "accepts all struct keys" do 8 | assert AccessToken.new( 9 | token_identifier: "id", 10 | account_sid: "sid", 11 | api_key: "sid", 12 | api_secret: "secret", 13 | identity: "user@email.com", 14 | grants: [ 15 | AccessToken.ChatGrant.new(service_sid: "sid"), 16 | AccessToken.VideoGrant.new(room: "room") 17 | ], 18 | expires_in: 86_400 19 | ) == %AccessToken{ 20 | token_identifier: "id", 21 | account_sid: "sid", 22 | api_key: "sid", 23 | api_secret: "secret", 24 | identity: "user@email.com", 25 | grants: [ 26 | %AccessToken.ChatGrant{service_sid: "sid"}, 27 | %AccessToken.VideoGrant{room: "room"} 28 | ], 29 | expires_in: 86_400 30 | } 31 | end 32 | end 33 | 34 | describe ".to_jwt!/1" do 35 | test "produces a valid Twilio JWT" do 36 | token = 37 | AccessToken.new( 38 | account_sid: "sid", 39 | api_key: "sid", 40 | api_secret: "secret", 41 | identity: "user@email.com", 42 | grants: [ 43 | AccessToken.ChatGrant.new(service_sid: "sid"), 44 | AccessToken.VideoGrant.new(room: "room"), 45 | AccessToken.VoiceGrant.new( 46 | outgoing_application_sid: "sid", 47 | outgoing_application_params: %{key: "value"} 48 | ) 49 | ], 50 | expires_in: 86_400 51 | ) 52 | |> AccessToken.to_jwt!() 53 | 54 | signer = Joken.Signer.create("HS256", "secret") 55 | 56 | assert {:ok, claims} = Joken.verify(token, signer) 57 | assert claims["iss"] == "sid" 58 | assert claims["sub"] == "sid" 59 | assert_in_delta unix_now(), claims["iat"], 10 60 | assert_in_delta unix_now(), claims["nbf"], 10 61 | assert_in_delta unix_now(), claims["exp"], 86_400 62 | 63 | assert claims["grants"] == %{ 64 | "chat" => %{"service_sid" => "sid"}, 65 | "video" => %{"room" => "room"}, 66 | "voice" => %{ 67 | "outgoing" => %{"application_sid" => "sid", "params" => %{"key" => "value"}} 68 | }, 69 | "identity" => "user@email.com" 70 | } 71 | end 72 | 73 | test "validates binary keys" do 74 | for invalid <- [123, ~c"sid", nil, false], 75 | field <- [:account_sid, :api_key, :api_secret, :identity] do 76 | assert_raise ArgumentError, fn -> 77 | [{field, invalid}] 78 | |> AccessToken.new() 79 | |> AccessToken.to_jwt!() 80 | end 81 | end 82 | end 83 | 84 | test "validates :grants" do 85 | assert_raise ArgumentError, fn -> 86 | [grants: [%{}]] 87 | |> AccessToken.new() 88 | |> AccessToken.to_jwt!() 89 | end 90 | end 91 | 92 | test "validates :expires_in" do 93 | for invalid <- [nil, false, "1 hour"] do 94 | assert_raise ArgumentError, fn -> 95 | [expires_in: invalid] 96 | |> AccessToken.new() 97 | |> AccessToken.to_jwt!() 98 | end 99 | end 100 | end 101 | end 102 | 103 | defp unix_now do 104 | DateTime.utc_now() |> DateTime.to_unix() 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /lib/ex_twilio/parser.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.Parser do 2 | @moduledoc """ 3 | A JSON parser tuned specifically for Twilio API responses. Based on Poison's 4 | excellent JSON decoder. 5 | """ 6 | 7 | @type metadata :: map 8 | @type http_status_code :: number 9 | @type key :: String.t() 10 | @type success :: {:ok, map} 11 | @type success_list :: {:ok, [map], metadata} 12 | @type success_delete :: :ok 13 | @type error :: {:error, map, http_status_code} 14 | 15 | @type parsed_response :: success | error 16 | @type parsed_list_response :: success_list | error 17 | 18 | @doc """ 19 | Parse a response expected to contain a single resource. If you pass in a 20 | module as the first argument, the JSON will be parsed into that module's 21 | `__struct__`. 22 | 23 | ## Examples 24 | 25 | Given you have a module named `Resource`, defined like this: 26 | 27 | defmodule Resource do 28 | defstruct sid: nil 29 | end 30 | 31 | You can parse JSON into that module's struct like so: 32 | 33 | iex> response = %{body: "{ \\"sid\\": \\"AD34123\\" }", status_code: 200} 34 | ...> ExTwilio.Parser.parse(response, Resource) 35 | {:ok, %Resource{sid: "AD34123"}} 36 | 37 | You can also parse into a regular map if you want. 38 | 39 | iex> response = %{body: "{ \\"sid\\": \\"AD34123\\" }", status_code: 200} 40 | ...> ExTwilio.Parser.parse(response, %{}) 41 | {:ok, %{"sid" => "AD34123"}} 42 | """ 43 | @spec parse(HTTPoison.Response.t(), module) :: success | error 44 | def parse(response, map) when is_map(map) and not :erlang.is_map_key(:__struct__, map) do 45 | handle_errors(response, fn body -> Jason.decode!(body) end) 46 | end 47 | 48 | def parse(response, module) do 49 | handle_errors(response, fn body -> 50 | struct(module, Jason.decode!(body, keys: :atoms)) 51 | end) 52 | end 53 | 54 | @doc """ 55 | Parse a response expected to contain multiple resources. If you pass in a 56 | module as the first argument, the JSON will be parsed into that module's 57 | `__struct__`. 58 | 59 | ## Examples 60 | 61 | Given you have a module named `Resource`, defined like this: 62 | 63 | defmodule Resource do 64 | defstruct sid: nil 65 | end 66 | 67 | And the JSON you are parsing looks like this: 68 | 69 | { 70 | "resources": [{ 71 | "sid": "first" 72 | }, { 73 | "sid": "second" 74 | }], 75 | "next_page": 10 76 | } 77 | 78 | You can parse the the JSON like this: 79 | 80 | ExTwilio.Parser.parse_list(json, Resource, "resources") 81 | {:ok, [%Resource{sid: "first"}, %Resource{sid: "second"}], %{"next_page" => 10}} 82 | """ 83 | @spec parse_list(HTTPoison.Response.t(), module, key) :: success_list | error 84 | def parse_list(response, module, key) do 85 | case handle_errors(response, fn body -> Jason.decode!(body) end) do 86 | {:ok, json} -> {:ok, list_to_structs(json[key], module), Map.drop(json, [key])} 87 | error -> error 88 | end 89 | end 90 | 91 | defp list_to_structs(list, module) do 92 | Enum.map(list, fn item -> 93 | struct(module, Map.new(item, fn {key, value} -> {String.to_atom(key), value} end)) 94 | end) 95 | end 96 | 97 | # @spec handle_errors(response, ((String.t) -> any)) :: success | success_delete | error 98 | defp handle_errors(response, fun) do 99 | case response do 100 | %{body: body, status_code: status} when status in [200, 201] -> 101 | {:ok, fun.(body)} 102 | 103 | %{body: _, status_code: status} when status in [202, 204] -> 104 | :ok 105 | 106 | %{body: body, status_code: status} -> 107 | {:error, Jason.decode!(body), status} 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /test/ex_twilio/request_validator_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.RequestValidatorTest do 2 | alias ExTwilio.RequestValidator 3 | 4 | use ExUnit.Case, async: true 5 | 6 | describe "validating a voice request" do 7 | setup do 8 | params = %{ 9 | "ToState" => "California", 10 | "CalledState" => "California", 11 | "Direction" => "inbound", 12 | "FromState" => "CA", 13 | "AccountSid" => "ACba8bc05eacf94afdae398e642c9cc32d", 14 | "Caller" => "+14153595711", 15 | "CallerZip" => "94108", 16 | "CallerCountry" => "US", 17 | "From" => "+14153595711", 18 | "FromCity" => "SAN FRANCISCO", 19 | "CallerCity" => "SAN FRANCISCO", 20 | "To" => "+14157669926", 21 | "FromZip" => "94108", 22 | "FromCountry" => "US", 23 | "ToCity" => "", 24 | "CallStatus" => "ringing", 25 | "CalledCity" => "", 26 | "CallerState" => "CA", 27 | "CalledZip" => "", 28 | "ToZip" => "", 29 | "ToCountry" => "US", 30 | "CallSid" => "CA136d09cd59a3c0ec8dbff44da5c03f31", 31 | "CalledCountry" => "US", 32 | "Called" => "+14157669926", 33 | "ApiVersion" => "2010-04-01", 34 | "ApplicationSid" => "AP44efecad51364e80b133bb7c07eb8204" 35 | } 36 | 37 | {:ok, 38 | [ 39 | params: params, 40 | signature: "oVb2kXoVy8GEfwBDjR8bk/ZZ6eA=", 41 | token: "2bd9e9638872de601313dc77410d3b23", 42 | url: "http://twiliotests.heroku.com/validate/voice" 43 | ]} 44 | end 45 | 46 | test "validating a correct voice request", context do 47 | assert( 48 | RequestValidator.valid?( 49 | context[:url], 50 | context[:params], 51 | context[:signature], 52 | context[:token] 53 | ) 54 | ) 55 | end 56 | 57 | test "validating an incorrect voice request", context do 58 | refute( 59 | RequestValidator.valid?( 60 | context[:url], 61 | context[:params], 62 | "incorrect", 63 | context[:token] 64 | ) 65 | ) 66 | end 67 | end 68 | 69 | describe "validating a text request" do 70 | setup do 71 | params = %{ 72 | "ToState" => "CA", 73 | "FromState" => "CA", 74 | "AccountSid" => "ACba8bc05eacf94afdae398e642c9cc32d", 75 | "SmsMessageSid" => "SM2003cbd5e6a3701999aa3e5f20ff2787", 76 | "Body" => "Orly", 77 | "From" => "+14159354345", 78 | "FromCity" => "SAN FRANCISCO", 79 | "SmsStatus" => "received", 80 | "FromZip" => "94107", 81 | "FromCountry" => "US", 82 | "To" => "+14158141819", 83 | "ToCity" => "SAN FRANCISCO", 84 | "ToZip" => "94105", 85 | "ToCountry" => "US", 86 | "ApiVersion" => "2010-04-01", 87 | "SmsSid" => "SM2003cbd5e6a3701999aa3e5f20ff2787" 88 | } 89 | 90 | {:ok, 91 | [ 92 | params: params, 93 | signature: "mxeiv65lEe0b8L6LdVw2jgJi8yw=", 94 | token: "2bd9e9638872de601313dc77410d3b23", 95 | url: "http://twiliotests.heroku.com/validate/sms" 96 | ]} 97 | end 98 | 99 | test "validating a correct sms request", context do 100 | assert( 101 | RequestValidator.valid?( 102 | context[:url], 103 | context[:params], 104 | context[:signature], 105 | context[:token] 106 | ) 107 | ) 108 | end 109 | 110 | test "validating an incorrect sms request", context do 111 | refute( 112 | RequestValidator.valid?( 113 | context[:url], 114 | context[:params], 115 | "incorrect", 116 | context[:token] 117 | ) 118 | ) 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /lib/ex_twilio/jwt/access_token.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.JWT.AccessToken do 2 | @moduledoc """ 3 | A Twilio JWT access token, as described in the Twilio docs. 4 | 5 | https://www.twilio.com/docs/iam/access-tokens 6 | """ 7 | 8 | alias ExTwilio.JWT.Grant 9 | alias ExTwilio.Ext 10 | use Joken.Config 11 | 12 | @enforce_keys [:account_sid, :api_key, :api_secret, :identity, :grants, :expires_in] 13 | 14 | defstruct token_identifier: nil, 15 | account_sid: nil, 16 | api_key: nil, 17 | api_secret: nil, 18 | identity: nil, 19 | grants: [], 20 | expires_in: nil 21 | 22 | @type t :: %__MODULE__{ 23 | account_sid: String.t(), 24 | api_key: String.t(), 25 | api_secret: String.t(), 26 | identity: String.t(), 27 | grants: [ExTwilio.JWT.Grant.t()], 28 | expires_in: integer 29 | } 30 | 31 | @doc """ 32 | Creates a new JWT access token. 33 | 34 | ## Examples 35 | 36 | AccessToken.new( 37 | account_sid: "account_sid", 38 | api_key: "api_key", 39 | api_secret: "secret", 40 | identity: "user@email.com", 41 | expires_in: 86_400, 42 | grants: [AccessToken.ChatGrant.new(service_sid: "sid")] 43 | ) 44 | 45 | """ 46 | @spec new(attrs :: Keyword.t()) :: t 47 | def new(attrs \\ []) do 48 | struct(__MODULE__, attrs) 49 | end 50 | 51 | @doc """ 52 | Converts an access token into a string JWT. 53 | 54 | Will raise errors if the `token` does not have all the required fields. 55 | 56 | ## Examples 57 | 58 | token = 59 | AccessToken.new( 60 | account_sid: "account_sid", 61 | api_key: "api_key", 62 | api_secret: "secret", 63 | identity: "user@email.com", 64 | expires_in: 86_400, 65 | grants: [AccessToken.ChatGrant.new(service_sid: "sid")] 66 | ) 67 | 68 | AccessToken.to_jwt!(token) 69 | # => "eyJhbGciOiJIUzI1NiIsImN0eSI6InR3aWxpby1mcGE7dj0xIiwidHlwIjoiSldUIn0.eyJleHAiOjE1MjM5MTIxODgsImdyYW50cyI6eyJjaGF0Ijp7ImVuZHBvaW50X2lkIjpudWxsLCJzZXJ2aWNlX3NpZCI6InNpZCJ9LCJpZGVudGl0eSI6InVzZXJAZW1haWwuY29tIn0sImlhdCI6MTUyMzkwNDk4OCwibmJmIjoxNTIzOTA0OTg3fQ.M_5dsj1VWBrIZKvcIdygSpmiMsrZdkplYYNjxEhBHk0" 70 | 71 | """ 72 | @spec to_jwt!(t) :: String.t() | no_return 73 | def to_jwt!(token) do 74 | token = 75 | token 76 | |> Ext.Map.validate!(:account_sid, &is_binary/1, "must be a binary") 77 | |> Ext.Map.validate!(:api_key, &is_binary/1, "must be a binary") 78 | |> Ext.Map.validate!(:api_secret, &is_binary/1, "must be a binary") 79 | |> Ext.Map.validate!(:identity, &is_binary/1, "must be a binary") 80 | |> Ext.Map.validate!(:grants, &list_of_grants?/1, "must be a list of grants") 81 | |> Ext.Map.validate!(:expires_in, &is_integer/1, "must be an integer") 82 | 83 | token_config = 84 | %{} 85 | |> add_claim("grants", fn -> grants(token) end) 86 | |> add_claim("sub", fn -> token.account_sid end) 87 | |> add_claim("jti", fn -> token.token_identifier || "#{token.api_key}-#{random_str()}" end) 88 | |> add_claim("iss", fn -> token.api_key end) 89 | |> add_claim("nbf", fn -> DateTime.utc_now() |> DateTime.to_unix() end) 90 | |> add_claim("exp", fn -> (DateTime.utc_now() |> DateTime.to_unix()) + token.expires_in end) 91 | |> add_claim("iat", fn -> DateTime.utc_now() |> DateTime.to_unix() end) 92 | 93 | signer = 94 | Joken.Signer.create("HS256", token.api_secret, %{ 95 | "typ" => "JWT", 96 | "alg" => "HS256", 97 | "cty" => "twilio-fpa;v=1" 98 | }) 99 | 100 | Joken.generate_and_sign!(token_config, %{}, signer) 101 | end 102 | 103 | defp list_of_grants?(grants) when is_list(grants) do 104 | Enum.all?(grants, &Grant.impl_for(&1)) 105 | end 106 | 107 | defp list_of_grants?(_other), do: false 108 | 109 | defp grants(token) do 110 | grants = 111 | Enum.reduce(token.grants, %{"identity" => token.identity}, fn grant, acc -> 112 | Map.put(acc, Grant.type(grant), Grant.attrs(grant)) 113 | end) 114 | 115 | grants 116 | end 117 | 118 | defp random_str do 119 | 16 120 | |> :crypto.strong_rand_bytes() 121 | |> Base.encode16() 122 | |> String.downcase() 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /lib/ex_twilio/resource.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.Resource do 2 | @moduledoc """ 3 | Mixin to include `ExTwilio.Api` module functionality in a module with slightly 4 | prettier syntax. 5 | 6 | Under the hood, it delegates all the work to other `ExTwilio` modules, 7 | primarily `ExTwilio.Api`. 8 | 9 | ## Examples 10 | 11 | Define a module, and `use ExTwilio.Resource`. 12 | 13 | defmodule ExTwilio.Call do 14 | use ExTwilio.Resource, import: [:stream, :all] 15 | 16 | defstruct sid: nil, ... 17 | end 18 | 19 | The `import` option specifies which methods you want to be able to use. 20 | """ 21 | 22 | @doc false 23 | defmacro __using__(options) do 24 | import_functions = options[:import] || [] 25 | 26 | quote bind_quoted: [import_functions: import_functions] do 27 | alias ExTwilio.Api 28 | alias ExTwilio.Parser 29 | alias ExTwilio.UrlGenerator, as: Url 30 | alias ExTwilio.ResultStream 31 | 32 | @spec new :: %__MODULE__{} 33 | def new, do: %__MODULE__{} 34 | 35 | @spec new(list) :: %__MODULE__{} 36 | def new(attrs) do 37 | do_new(%__MODULE__{}, attrs) 38 | end 39 | 40 | @spec do_new(%__MODULE__{}, list) :: %__MODULE__{} 41 | def do_new(struct, []), do: struct 42 | 43 | def do_new(struct, [{key, val} | tail]) do 44 | do_new(Map.put(struct, key, val), tail) 45 | end 46 | 47 | if :stream in import_functions do 48 | def stream(options \\ []), do: ResultStream.new(__MODULE__, options) 49 | end 50 | 51 | if :all in import_functions do 52 | @spec all(list) :: [map] 53 | def all(options \\ []) do 54 | options 55 | |> stream 56 | |> Enum.into([]) 57 | end 58 | end 59 | 60 | if :find in import_functions do 61 | @spec find(String.t() | nil, list) :: Parser.parsed_list_response() 62 | def find(sid, options \\ []), do: Api.find(__MODULE__, sid, options) 63 | end 64 | 65 | if :create in import_functions do 66 | @spec create(Api.data(), list) :: Parser.parsed_response() 67 | def create(data, options \\ []), do: Api.create(__MODULE__, data, options) 68 | end 69 | 70 | if :update in import_functions do 71 | @spec update(String.t(), Api.data(), list) :: Parser.parsed_response() 72 | def update(sid, data, options \\ []), do: Api.update(__MODULE__, sid, data, options) 73 | end 74 | 75 | if :destroy in import_functions do 76 | @spec destroy(String.t(), list) :: Parser.success_delete() | Parser.error() 77 | def destroy(sid, options \\ []), do: Api.destroy(__MODULE__, sid, options) 78 | end 79 | 80 | @doc """ 81 | Underscored and lowercased collection name for a given resource. 82 | Delegates the real work to `ExTwilio.UrlGenerator.resource_collection_name/1` by 83 | default. 84 | 85 | Override in your module after `use ExTwilio.Resource` if you need 86 | something different. 87 | """ 88 | def resource_collection_name, do: Url.resource_collection_name(__MODULE__) 89 | 90 | @doc """ 91 | CamelCase resource name as it would be used in Twilio's API. Delegates 92 | the real work to `ExTwilio.UrlGenerator.resource_name/1` by default. 93 | 94 | Override in your module after `use ExTwilio.Resource` if you need 95 | something different. 96 | """ 97 | def resource_name, do: Url.resource_name(__MODULE__) 98 | 99 | @doc """ 100 | Parents represent path segments that precede the current resource. For example, 101 | in the path `/v2/Services/ISXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Users` "Services" is 102 | a parent. Parents will always have a key in the next segment. If your parent is under a 103 | submodule of `ExTwilio`, specify your parent using the `ExTwilio.Parent` struct. 104 | 105 | Override this method in your resource to specify parents in the order that they will appear 106 | in the path. 107 | """ 108 | @spec parents :: list 109 | def parents, do: [] 110 | 111 | @doc """ 112 | Children represent path segments that come after the current resource. For example, 113 | in the path `/v2/Services/ISXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Users/Active` "Active" is 114 | a child. Children may or may not have a key in the next segment. 115 | 116 | Override this method in your resource to specify children in the order that they will appear 117 | in the path. 118 | """ 119 | @spec children :: list 120 | def children, do: [] 121 | 122 | defoverridable Module.definitions_in(__MODULE__, :def) 123 | end 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /test/ex_twilio/capability_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.CapabilityTest do 2 | use ExUnit.Case 3 | 4 | alias ExTwilio.Config 5 | 6 | test ".new sets the TTL to one hour" do 7 | assert ExTwilio.Capability.new().ttl == 3600 8 | end 9 | 10 | test ".new sets the start time for the TTL to be the current time" do 11 | assert_in_delta ExTwilio.Capability.new().start_time, :erlang.system_time(:seconds), 1000 12 | end 13 | 14 | test ".new sets the account sid from the config" do 15 | assert ExTwilio.Capability.new().account_sid == Config.account_sid() 16 | end 17 | 18 | test ".new sets the auth token from the config" do 19 | assert ExTwilio.Capability.new().auth_token == Config.auth_token() 20 | end 21 | 22 | test ".allow_client_incoming sets the client name" do 23 | assert ExTwilio.Capability.allow_client_incoming("tommy").incoming_client_names == ["tommy"] 24 | end 25 | 26 | test ".allow_client_incoming appends additional client names" do 27 | capability = 28 | %ExTwilio.Capability{} 29 | |> ExTwilio.Capability.allow_client_incoming("tommy") 30 | |> ExTwilio.Capability.allow_client_incoming("billy") 31 | 32 | assert capability.incoming_client_names == ["tommy", "billy"] 33 | end 34 | 35 | test ".allow_client_outgoing sets the app sid" do 36 | assert ExTwilio.Capability.allow_client_outgoing("app_sid").outgoing_client_app == 37 | {"app_sid", %{}} 38 | end 39 | 40 | test ".allow_client_outgoing sets the app sid and params" do 41 | assert ExTwilio.Capability.allow_client_outgoing("app_sid", %{key: "value"}).outgoing_client_app == 42 | {"app_sid", %{key: "value"}} 43 | end 44 | 45 | test ".allow_client_outgoing overwrites the previous app sid" do 46 | capability = 47 | %ExTwilio.Capability{} 48 | |> ExTwilio.Capability.allow_client_outgoing("app_sid") 49 | |> ExTwilio.Capability.allow_client_outgoing("not_app_sid") 50 | 51 | assert capability.outgoing_client_app == {"not_app_sid", %{}} 52 | end 53 | 54 | test ".allow_client_outgoing overwrites the previous app sid and params" do 55 | capability = 56 | %ExTwilio.Capability{} 57 | |> ExTwilio.Capability.allow_client_outgoing("app_sid") 58 | |> ExTwilio.Capability.allow_client_outgoing("not_app_sid", %{key: "value"}) 59 | 60 | assert capability.outgoing_client_app == {"not_app_sid", %{key: "value"}} 61 | end 62 | 63 | test ".with_ttl sets the ttl" do 64 | assert ExTwilio.Capability.with_ttl(ExTwilio.Capability.new(), 1000).ttl == 1000 65 | end 66 | 67 | test ".starting_at sets the start_time" do 68 | assert ExTwilio.Capability.starting_at(ExTwilio.Capability.new(), 1_464_096_368).start_time == 69 | 1_464_096_368 70 | end 71 | 72 | test ".with_account_sid sets the account_sid" do 73 | assert ExTwilio.Capability.with_account_sid(ExTwilio.Capability.new(), "sid").account_sid == 74 | "sid" 75 | end 76 | 77 | test ".with_auth_token sets the auth_token" do 78 | assert ExTwilio.Capability.with_auth_token(ExTwilio.Capability.new(), "token").auth_token == 79 | "token" 80 | end 81 | 82 | test ".token sets an expiration time of one hour from now" do 83 | assert_in_delta decoded_token(ExTwilio.Capability.new()).claims["exp"], 84 | :erlang.system_time(:seconds) + 3600, 85 | 1000 86 | end 87 | 88 | test ".token sets the issuer to the account sid" do 89 | assert decoded_token(ExTwilio.Capability.new()).claims["iss"] == Config.account_sid() 90 | end 91 | 92 | test ".token sets the outgoing scope when no parameters specified" do 93 | capability = 94 | ExTwilio.Capability.new() 95 | |> ExTwilio.Capability.allow_client_outgoing("app sid") 96 | 97 | assert decoded_token(capability).claims["scope"] == "scope:client:outgoing?appSid=app%20sid" 98 | end 99 | 100 | test ".token sets the outgoing scope when parameters specified" do 101 | capability = 102 | ExTwilio.Capability.new() 103 | |> ExTwilio.Capability.allow_client_outgoing("app sid", %{user_id: "321", xargs_id: "value"}) 104 | 105 | assert decoded_token(capability).claims["scope"] == 106 | "scope:client:outgoing?appSid=app%20sid&appParams=user_id%3D321%26xargs_id%3Dvalue" 107 | end 108 | 109 | test ".token sets the incoming scope" do 110 | capability = 111 | ExTwilio.Capability.new() 112 | |> ExTwilio.Capability.allow_client_incoming("tom my") 113 | |> ExTwilio.Capability.allow_client_incoming("bil ly") 114 | 115 | assert decoded_token(capability).claims["scope"] == 116 | "scope:client:incoming?clientName=tom%20my scope:client:incoming?clientName=bil%20ly" 117 | end 118 | 119 | defp decoded_token(capability) do 120 | signer = Joken.Signer.create("HS256", Config.auth_token()) 121 | 122 | {:ok, verified} = 123 | capability 124 | |> ExTwilio.Capability.token() 125 | |> Joken.verify(signer) 126 | 127 | %{:claims => verified} 128 | end 129 | 130 | doctest ExTwilio.Capability 131 | end 132 | -------------------------------------------------------------------------------- /test/ex_twilio/api_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.ApiTest do 2 | use ExUnit.Case, async: false 3 | 4 | import TestHelper 5 | 6 | alias ExTwilio.Api 7 | 8 | defmodule Resource do 9 | defstruct sid: nil, name: nil 10 | def resource_name, do: "Resources" 11 | def resource_collection_name, do: "resources" 12 | def parents, do: [] 13 | def children, do: [] 14 | end 15 | 16 | doctest ExTwilio.Api 17 | 18 | test ".find should return the resource if it exists" do 19 | json = json_response(%{sid: "id"}, 200) 20 | 21 | with_fixture(:get!, json, fn -> 22 | assert {:ok, %Resource{sid: "id"}} == Api.find(Resource, "id") 23 | assert {:ok, %Resource{sid: "id"}} == Api.find(Resource, "id", account: "sid") 24 | end) 25 | end 26 | 27 | test ".find should return an error from Twilio if the resource does not exist" do 28 | json = json_response(%{message: "Error message"}, 404) 29 | 30 | with_fixture(:get!, json, fn -> 31 | assert {:error, %{"message" => "Error message"}, 404} == Api.find(Resource, "id") 32 | end) 33 | end 34 | 35 | test ".create should return the resource if successful" do 36 | json = json_response(%{sid: "id"}, 200) 37 | 38 | with_fixture(:post!, json, fn -> 39 | assert {:ok, %Resource{sid: "id"}} == Api.create(Resource, field: "value") 40 | assert {:ok, %Resource{sid: "id"}} == Api.create(Resource, [field: "value"], account: "sid") 41 | end) 42 | end 43 | 44 | test ".create should return an error from Twilio if the resource could not be created" do 45 | json = json_response(%{message: "Resource couldn't be created."}, 500) 46 | 47 | with_fixture(:post!, json, fn -> 48 | assert {:error, %{"message" => "Resource couldn't be created."}, 500} == 49 | Api.create(Resource, field: "value") 50 | end) 51 | end 52 | 53 | test ".update should return an updated resource if successful" do 54 | json = json_response(%{sid: "id", name: "Hello, World!"}, 200) 55 | 56 | with_fixture(:post!, json, fn -> 57 | name = "Hello, World!" 58 | expected = {:ok, %Resource{sid: "id", name: name}} 59 | data = [name: name] 60 | 61 | assert expected == Api.update(Resource, "id", data) 62 | assert expected == Api.update(Resource, "id", data, account: "sid") 63 | end) 64 | end 65 | 66 | test ".update should return an error if unsuccessful" do 67 | json = json_response(%{message: "The requested resource could not be found."}, 404) 68 | 69 | with_fixture(:post!, json, fn -> 70 | expected = {:error, %{"message" => "The requested resource could not be found."}, 404} 71 | assert expected == Api.update(Resource, "nonexistent", name: "Hello, World!") 72 | end) 73 | end 74 | 75 | test ".destroy should return :ok if successful" do 76 | with_fixture(:delete!, %{body: "", status_code: 204}, fn -> 77 | assert :ok == Api.destroy(Resource, "id") 78 | assert :ok == Api.destroy(Resource, "id", account: "sid") 79 | end) 80 | end 81 | 82 | test ".destroy should return an error if unsuccessful" do 83 | json = json_response(%{message: "not found"}, 404) 84 | 85 | with_fixture(:delete!, json, fn -> 86 | assert {:error, %{"message" => "not found"}, 404} == Api.destroy(Resource, "id") 87 | end) 88 | end 89 | 90 | ### 91 | # HTTPoison API 92 | ### 93 | 94 | test ".process_request_headers adds the correct headers" do 95 | headers = Api.process_request_headers([]) 96 | content = {:"Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"} 97 | assert content in headers 98 | assert Keyword.keys(headers) == [:Authorization, :"Content-Type"] 99 | end 100 | 101 | test ".process_request_options adds configured options if configured" do 102 | assert Api.process_request_options([]) == [] 103 | 104 | Application.put_env(:ex_twilio, :request_options, hackney: [pool: :mavis]) 105 | assert Api.process_request_options([]) == [hackney: [pool: :mavis]] 106 | assert Api.process_request_options(bob: :sue) == [bob: :sue, hackney: [pool: :mavis]] 107 | after 108 | Application.delete_env(:ex_twilio, :request_options) 109 | end 110 | 111 | test ".format_data converts data to a query string when passed a list" do 112 | assert "FieldName=value" == Api.format_data(field_name: "value") 113 | end 114 | 115 | test ".format_data does not modify the body when passed a not-list" do 116 | assert "unmodified" == Api.format_data("unmodified") 117 | end 118 | 119 | ### 120 | # Helpers 121 | ### 122 | 123 | def with_list_fixture(fun) do 124 | data = %{ 125 | "resources" => [ 126 | %{sid: "1", name: "first"}, 127 | %{sid: "2", name: "second"} 128 | ], 129 | next_page_uri: "/some/path" 130 | } 131 | 132 | json = json_response(data, 200) 133 | 134 | with_fixture(:get!, json, fn -> 135 | expected = 136 | {:ok, 137 | [ 138 | %Resource{sid: "1", name: "first"}, 139 | %Resource{sid: "2", name: "second"} 140 | ], %{"next_page_uri" => "/some/path"}} 141 | 142 | fun.(expected) 143 | end) 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, 3 | "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, 4 | "dialyze": {:hex, :dialyze, "0.2.1", "9fb71767f96649020d769db7cbd7290059daff23707d6e851e206b1fdfa92f9d", [:mix], [], "hexpm", "f485181fa53229356621261a384963cb47511cccf1454e82ca4fde53274fcd48"}, 5 | "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, 6 | "ex_doc": {:hex, :ex_doc, "0.31.2", "8b06d0a5ac69e1a54df35519c951f1f44a7b7ca9a5bb7a260cd8a174d6322ece", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "317346c14febaba9ca40fd97b5b5919f7751fb85d399cc8e7e8872049f37e0af"}, 7 | "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, 8 | "httpoison": {:hex, :httpoison, "2.2.1", "87b7ed6d95db0389f7df02779644171d7319d319178f6680438167d7b69b1f3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "51364e6d2f429d80e14fe4b5f8e39719cacd03eb3f9a9286e61e216feac2d2df"}, 9 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 10 | "inch_ex": {:hex, :inch_ex, "2.0.0", "24268a9284a1751f2ceda569cd978e1fa394c977c45c331bb52a405de544f4de", [:mix], [{:bunt, "~> 0.2", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "96d0ec5ecac8cf63142d02f16b7ab7152cf0f0f1a185a80161b758383c9399a8"}, 11 | "inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"}, 12 | "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, 13 | "joken": {:hex, :joken, "2.6.0", "b9dd9b6d52e3e6fcb6c65e151ad38bf4bc286382b5b6f97079c47ade6b1bcc6a", [:mix], [{:jose, "~> 1.11.5", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5a95b05a71cd0b54abd35378aeb1d487a23a52c324fa7efdffc512b655b5aaa7"}, 14 | "jose": {:hex, :jose, "1.11.6", "613fda82552128aa6fb804682e3a616f4bc15565a048dabd05b1ebd5827ed965", [:mix, :rebar3], [], "hexpm", "6275cb75504f9c1e60eeacb771adfeee4905a9e182103aa59b53fed651ff9738"}, 15 | "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, 16 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, 17 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.5", "e0ff5a7c708dda34311f7522a8758e23bfcd7d8d8068dc312b5eb41c6fd76eba", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"}, 18 | "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, 19 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 20 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 21 | "mock": {:hex, :mock, "0.3.8", "7046a306b71db2488ef54395eeb74df0a7f335a7caca4a3d3875d1fc81c884dd", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "7fa82364c97617d79bb7d15571193fc0c4fe5afd0c932cef09426b3ee6fe2022"}, 22 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, 23 | "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, 24 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, 25 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 26 | } 27 | -------------------------------------------------------------------------------- /lib/ex_twilio/api.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.Api do 2 | @moduledoc """ 3 | Provides a basic HTTP interface to allow easy communication with the Twilio 4 | API, by wrapping `HTTPotion`. 5 | 6 | ## Examples 7 | 8 | Requests are made to the Twilio API by passing in a resource module into one 9 | of this `Api` module's functions. The correct URL to the resource is inferred 10 | from the module name. 11 | 12 | ExTwilio.Api.find(Resource, "sid") 13 | %Resource{ sid: "sid", ... } 14 | 15 | Items are returned as instances of the given module's struct. For more 16 | details, see the documentation for each function. 17 | """ 18 | use HTTPoison.Base 19 | 20 | alias ExTwilio.Config 21 | alias ExTwilio.Parser 22 | alias ExTwilio.UrlGenerator, as: Url 23 | # Necessary for mocks in tests 24 | alias __MODULE__ 25 | 26 | @type data :: map | list 27 | 28 | @doc """ 29 | Find a given resource in the Twilio API by its SID. 30 | 31 | ## Examples 32 | 33 | If the resource was found, `find/2` will return a two-element tuple in this 34 | format, `{:ok, item}`. 35 | 36 | ExTwilio.Api.find(ExTwilio.Call, "") 37 | {:ok, %Call{ ... }} 38 | 39 | If the resource could not be loaded, `find/2` will return a 3-element tuple 40 | in this format, `{:error, error_body, code}`. The `code` is the HTTP status code 41 | returned by the Twilio API, for example, 404. 42 | 43 | ExTwilio.Api.find(ExTwilio.Call, "nonexistent sid") 44 | {:error, %{"message" => The requested resource couldn't be found..."}, 404} 45 | 46 | """ 47 | @spec find(atom, String.t() | nil, list) :: Parser.success() | Parser.error() 48 | def find(module, sid, options \\ []) do 49 | module 50 | |> Url.build_url(sid, options) 51 | |> Api.get!(auth_header(options)) 52 | |> Parser.parse(module) 53 | end 54 | 55 | @doc """ 56 | Create a new resource in the Twilio API with a POST request. 57 | 58 | ## Examples 59 | 60 | ExTwilio.Api.create(ExTwilio.Call, [to: "1112223333", from: "4445556666"]) 61 | {:ok, %Call{ ... }} 62 | 63 | ExTwilio.Api.create(ExTwilio.Call, []) 64 | {:error, %{"message" => "No 'To' number is specified"}, 400} 65 | 66 | """ 67 | @spec create(atom, data, list) :: Parser.success() | Parser.error() 68 | def create(module, data, options \\ []) do 69 | data = format_data(data) 70 | 71 | module 72 | |> Url.build_url(nil, options) 73 | |> Api.post!(data, auth_header(options)) 74 | |> Parser.parse(module) 75 | end 76 | 77 | @doc """ 78 | Update an existing resource in the Twilio Api. 79 | 80 | ## Examples 81 | 82 | ExTwilio.Api.update(ExTwilio.Call, "", [status: "canceled"]) 83 | {:ok, %Call{ status: "canceled" ... }} 84 | 85 | ExTwilio.Api.update(ExTwilio.Call, "nonexistent", [status: "complete"]) 86 | {:error, %{"message" => "The requested resource ... was not found"}, 404} 87 | 88 | """ 89 | @spec update(atom, String.t(), data, list) :: Parser.success() | Parser.error() 90 | def update(module, sid, data, options \\ []) 91 | 92 | def update(module, sid, data, options) when is_binary(sid), 93 | do: do_update(module, sid, data, options) 94 | 95 | def update(module, %{sid: sid}, data, options), do: do_update(module, sid, data, options) 96 | 97 | defp do_update(module, sid, data, options) do 98 | data = format_data(data) 99 | 100 | module 101 | |> Url.build_url(sid, options) 102 | |> Api.post!(data, auth_header(options)) 103 | |> Parser.parse(module) 104 | end 105 | 106 | @doc """ 107 | Destroy an existing resource in the Twilio Api. 108 | 109 | ## Examples 110 | 111 | ExTwilio.Api.destroy(ExTwilio.Call, "") 112 | :ok 113 | 114 | ExTwilio.Api.destroy(ExTwilio.Call, "nonexistent") 115 | {:error, %{"message" => The requested resource ... was not found"}, 404} 116 | 117 | """ 118 | @spec destroy(atom, String.t()) :: Parser.success_delete() | Parser.error() 119 | def destroy(module, sid, options \\ []) 120 | def destroy(module, sid, options) when is_binary(sid), do: do_destroy(module, sid, options) 121 | def destroy(module, %{sid: sid}, options), do: do_destroy(module, sid, options) 122 | 123 | defp do_destroy(module, sid, options) do 124 | module 125 | |> Url.build_url(sid, options) 126 | |> Api.delete!(auth_header(options)) 127 | |> Parser.parse(module) 128 | end 129 | 130 | @doc """ 131 | Builds custom auth header for subaccounts. 132 | 133 | ## Examples 134 | iex> ExTwilio.Api.auth_header([account: 123, token: 123]) 135 | ["Authorization": "Basic MTIzOjEyMw=="] 136 | 137 | iex> ExTwilio.Api.auth_header([], {nil, 2}) 138 | [] 139 | 140 | """ 141 | @spec auth_header(options :: list) :: list 142 | def auth_header(options \\ []) do 143 | auth_header([], {options[:account], options[:token]}) 144 | end 145 | 146 | @doc """ 147 | Builds custom auth header for subaccounts. 148 | 149 | Handles master account case if :"Authorization" custom header isn't present 150 | 151 | ## Examples 152 | 153 | iex> ExTwilio.Api.auth_header([], {123, 123}) 154 | ["Authorization": "Basic MTIzOjEyMw=="] 155 | 156 | iex> ExTwilio.Api.auth_header(["Authorization": "Basic BASE64=="], {123, 123}) 157 | ["Authorization": "Basic BASE64=="] 158 | 159 | """ 160 | @spec auth_header(headers :: list, auth :: tuple) :: list 161 | def auth_header(headers, {sid, token}) when not is_nil(sid) and not is_nil(token) do 162 | case Keyword.has_key?(headers, :Authorization) do 163 | true -> 164 | headers 165 | 166 | false -> 167 | auth = Base.encode64("#{sid}:#{token}") 168 | 169 | headers 170 | |> Keyword.put(:Authorization, "Basic #{auth}") 171 | end 172 | end 173 | 174 | def auth_header(headers, _), do: headers 175 | 176 | @spec format_data(any) :: binary 177 | def format_data(data) 178 | 179 | def format_data(data) when is_map(data) do 180 | data 181 | |> Map.to_list() 182 | |> Url.to_query_string() 183 | end 184 | 185 | def format_data(data) when is_list(data) do 186 | Url.to_query_string(data) 187 | end 188 | 189 | def format_data(data), do: data 190 | 191 | ### 192 | # HTTPotion API 193 | ### 194 | 195 | def process_request_headers(headers \\ []) do 196 | headers 197 | |> Keyword.put(:"Content-Type", "application/x-www-form-urlencoded; charset=UTF-8") 198 | |> auth_header({Config.account_sid(), Config.auth_token()}) 199 | end 200 | 201 | def process_request_options(options) do 202 | Keyword.merge(options, Config.request_options()) 203 | end 204 | end 205 | -------------------------------------------------------------------------------- /CALLING_TUTORIAL.md: -------------------------------------------------------------------------------- 1 | Making and Receiving Calls from the Browser 2 | ------------------------------------------- 3 | 4 | As of 2016-08-29, the Twilio [quickstart docs](https://www.twilio.com/docs/quickstart) 5 | do not contain examples for Elixir. Some examples for Elixir can be found in this tutorial. 6 | 7 | To begin, create an empty Phoenix application called `Demo` and install and configure 8 | ExTwilio per the instructions on the [README](README.md#configuration). 9 | 10 | ## Making a Call 11 | 12 | Create a controller with a `show` action, and generate a Twilio capability 13 | token with the specified application sid (it plays a welcome message) 14 | 15 | ```elixir 16 | defmodule Demo.TwilioController do 17 | use Demo.Web, :controller 18 | 19 | def show(conn, _params) do 20 | token = 21 | ExTwilio.Capability.new 22 | |> ExTwilio.Capability.allow_client_outgoing("APabe7650f654fc34655fc81ae71caa3ff") 23 | |> ExTwilio.Capability.token 24 | 25 | render(conn, "show.html", token: token) 26 | end 27 | end 28 | ``` 29 | 30 | Create a view template for the `show` action 31 | 32 | ```html 33 | 35 | 38 | 72 | 73 | 76 | 77 | 79 | 82 |
83 | ``` 84 | 85 | Start Phoenix and open the page in the browser. Click the `Call` button and you should 86 | hear the Twilio welcome message. 87 | 88 | ## Receiving a Call 89 | 90 | Create a controller with a `show` action and generate a Twilio capability 91 | token with the name _jenny_, which will allow Jenny to receive incoming calls 92 | 93 | ```elixir 94 | defmodule Demo.TwilioController do 95 | use Demo.Web, :controller 96 | 97 | def show(conn, _params) do 98 | token = 99 | ExTwilio.Capability.new 100 | |> ExTwilio.Capability.allow_client_incoming("jenny") 101 | |> ExTwilio.Capability.token 102 | 103 | render(conn, "show.html", token: token) 104 | end 105 | end 106 | ``` 107 | 108 | Create a view template for the `show` action 109 | 110 | ```html 111 | 113 | 116 | 152 | 153 | 155 | 158 |
159 | ``` 160 | 161 | Next, we need to create a controller action that will render some TwiML instructing Twilio to connect any incoming calls to the client named _jenny_. There is a library called [ExTwiml](https://github.com/danielberkompas/ex_twiml) that provides a nice DSL for generating TwiML with Elixir. Add it as a dependency with `{:ex_twiml, "~> 2.1.0"}`. 162 | 163 | Create the following controller action and module for rendering the TwiML 164 | 165 | ```elixir 166 | defmodule Demo.TwilioController do 167 | use Demo.Web, :controller 168 | 169 | def show(conn, _params) do 170 | token = 171 | ExTwilio.Capability.new 172 | |> ExTwilio.Capability.allow_client_incoming("jenny") 173 | |> ExTwilio.Capability.token 174 | 175 | render(conn, "show.html", token: token) 176 | end 177 | 178 | # Note: By default, Twilio will POST to this endpoint 179 | def voice(conn, _params) do 180 | resp = Demo.Twiml.dial_jenny 181 | conn 182 | |> put_resp_content_type("text/xml") 183 | |> text(resp) 184 | end 185 | end 186 | 187 | defmodule Demo.Twiml do 188 | import ExTwiml 189 | 190 | def dial_jenny do 191 | twiml do 192 | # This should be your Twilio Number or verified Caller ID 193 | dial callerid: "+1XXXXXXX" do 194 | client "jenny" 195 | end 196 | end 197 | end 198 | end 199 | ``` 200 | 201 | Once you've started your web server, you can expose your endpoint to the world with a tool like [Ngrok](https://ngrok.com/), pointing it to your development server on port 4000 202 | 203 | $ ngrok http 4000 204 | 205 | Create a [TwiML application](https://www.twilio.com/console/voice/dev-tools/twiml-apps) on Twilio, which will tell Twilio where to get instructions for routing incoming calls. Set the URL to point to your `voice` endpoint exposed by Ngrok, e.g. 206 | 207 | `http://xxxxxxxx.ngrok.io/voice` _Note: this URL will change every time you restart Ngrok_ 208 | 209 | In order to test making an inbound call to your client, you can use the `Call` button visible on the Twilio console page for your TwiML application. Open the page corresponding to the `show` action in your browser. Then, on a different computer, click the `Call` button on the Twilio console - you should be able to carry on a conversation through your browsers. 210 | -------------------------------------------------------------------------------- /lib/ex_twilio/url_generator.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.UrlGenerator do 2 | @moduledoc """ 3 | Generates Twilio URLs for modules. See `build_url/3` for more information. 4 | """ 5 | 6 | alias ExTwilio.Config 7 | 8 | @doc """ 9 | Infers the proper Twilio URL for a resource when given a module, an optional 10 | SID, and a list of options. 11 | 12 | Note that the module should have the following two functions: 13 | 14 | - `resource_name/0` 15 | - `resource_collection_name/0` 16 | 17 | # Examples 18 | 19 | iex> build_url(Resource) 20 | "#{Config.base_url()}/Accounts/#{Config.account_sid()}/Resources.json" 21 | 22 | iex> build_url(Resource, nil, account: 2) 23 | "#{Config.base_url()}/Accounts/2/Resources.json" 24 | 25 | iex> build_url(Resource, 1, account: 2) 26 | "#{Config.base_url()}/Accounts/2/Resources/1.json" 27 | 28 | iex> build_url(Resource, 1) 29 | "#{Config.base_url()}/Accounts/#{Config.account_sid()}/Resources/1.json" 30 | 31 | iex> build_url(Resource, nil, page: 20) 32 | "#{Config.base_url()}/Accounts/#{Config.account_sid()}/Resources.json?Page=20" 33 | 34 | iex> build_url(Resource, nil, iso_country_code: "US", type: "Mobile", page: 20) 35 | "#{Config.base_url()}/Accounts/#{Config.account_sid()}/Resources/US/Mobile.json?Page=20" 36 | 37 | iex> build_url(Resource, 1, sip_ip_access_control_list: "list", account: "account_sid") 38 | "#{Config.base_url()}/Accounts/account_sid/SIP/IpAccessControlLists/list/Resources/1.json" 39 | 40 | """ 41 | @spec build_url(atom, String.t() | nil, list) :: String.t() 42 | def build_url(module, id \\ nil, options \\ []) do 43 | {url, options} = 44 | case Module.split(module) do 45 | ["ExTwilio", "TaskRouter" | _] -> 46 | options = add_workspace_to_options(module, options) 47 | url = add_segments(Config.task_router_url(), module, id, options) 48 | {url, options} 49 | 50 | ["ExTwilio", "ProgrammableChat" | _] -> 51 | url = add_segments(Config.programmable_chat_url(), module, id, options) 52 | {url, options} 53 | 54 | ["ExTwilio", "Notify" | _] -> 55 | url = add_segments(Config.notify_url(), module, id, options) 56 | {url, options} 57 | 58 | ["ExTwilio", "Fax" | _] -> 59 | url = add_segments(Config.fax_url(), module, id, options) 60 | {url, options} 61 | 62 | ["ExTwilio", "Studio" | _] -> 63 | options = add_flow_to_options(module, options) 64 | url = add_segments(Config.studio_url(), module, id, options) 65 | {url, options} 66 | 67 | ["ExTwilio", "Video" | _] -> 68 | url = add_segments(Config.video_url(), module, id, options) 69 | {url, options} 70 | 71 | _ -> 72 | # Add Account SID segment if not already present 73 | options = add_account_to_options(module, options) 74 | url = add_segments(Config.base_url(), module, id, options) <> ".json" 75 | {url, options} 76 | end 77 | 78 | # Append querystring 79 | if Keyword.has_key?(options, :query) do 80 | url <> options[:query] 81 | else 82 | url <> build_query(module, options) 83 | end 84 | end 85 | 86 | defp add_segments(url, module, id, options) do 87 | # Append parents 88 | url = url <> build_segments(:parent, normalize_parents(module.parents), options) 89 | 90 | # Append module segment 91 | url = url <> segment(:main, {module.resource_name, id}) 92 | 93 | # Append any child segments 94 | url <> build_segments(:child, module.children, options) 95 | end 96 | 97 | @doc """ 98 | Generate a list of querystring parameters for a url from an Elixir list. 99 | 100 | ## Examples 101 | 102 | iex> ExTwilio.UrlGenerator.to_query_string([hello: "world", how_are: "you"]) 103 | "Hello=world&HowAre=you" 104 | 105 | """ 106 | @spec to_query_string(list) :: String.t() 107 | def to_query_string(list) do 108 | list 109 | |> Enum.flat_map(fn 110 | {key, value} when is_list(value) -> Enum.map(value, &{camelize(key), &1}) 111 | {key, value} -> [{camelize(key), value}] 112 | end) 113 | |> URI.encode_query() 114 | end 115 | 116 | @doc """ 117 | Converts a module name into a pluralized Twilio-compatible resource name. 118 | 119 | ## Examples 120 | 121 | iex> ExTwilio.UrlGenerator.resource_name(:"Elixir.ExTwilio.Call") 122 | "Calls" 123 | 124 | # Uses only the last segment of the module name 125 | iex> ExTwilio.UrlGenerator.resource_name(:"ExTwilio.Resources.Call") 126 | "Calls" 127 | 128 | """ 129 | @spec resource_name(atom | String.t()) :: String.t() 130 | def resource_name(module) do 131 | name = to_string(module) 132 | [[name]] = Regex.scan(~r/[a-z]+$/i, name) 133 | Inflex.pluralize(name) 134 | end 135 | 136 | @doc """ 137 | Infer a lowercase and underscore collection name for a module. 138 | 139 | ## Examples 140 | 141 | iex> ExTwilio.UrlGenerator.resource_collection_name(Resource) 142 | "resources" 143 | 144 | """ 145 | @spec resource_collection_name(atom) :: String.t() 146 | def resource_collection_name(module) do 147 | module 148 | |> resource_name 149 | |> Macro.underscore() 150 | end 151 | 152 | @spec add_account_to_options(atom, list) :: list 153 | defp add_account_to_options(module, options) 154 | 155 | defp add_account_to_options(module, options) do 156 | if module == ExTwilio.Account and options[:account] == nil do 157 | options 158 | else 159 | Keyword.put_new(options, :account, Config.account_sid()) 160 | end 161 | end 162 | 163 | defp add_flow_to_options(_module, options) do 164 | Keyword.put_new(options, :flow, Keyword.get(options, :flow_sid)) 165 | end 166 | 167 | @spec add_workspace_to_options(atom, list) :: list 168 | defp add_workspace_to_options(_module, options) do 169 | Keyword.put_new(options, :workspace, Config.workspace_sid()) 170 | end 171 | 172 | @spec normalize_parents(list) :: list 173 | defp normalize_parents(parents) do 174 | parents 175 | |> Enum.map(fn 176 | key when is_atom(key) -> 177 | %ExTwilio.Parent{module: Module.concat(ExTwilio, camelize(key)), key: key} 178 | 179 | key -> 180 | key 181 | end) 182 | end 183 | 184 | @spec build_query(atom, list) :: String.t() 185 | defp build_query(module, options) do 186 | special = 187 | module.parents 188 | |> normalize_parents() 189 | |> Enum.map(fn parent -> parent.key end) 190 | |> Enum.concat(module.children) 191 | |> Enum.concat([:token]) 192 | 193 | query = 194 | options 195 | |> Enum.reject(fn {key, _val} -> key in special end) 196 | |> to_query_string 197 | 198 | if String.length(query) > 0, do: "?" <> query, else: "" 199 | end 200 | 201 | @spec build_segments(atom, list, list) :: String.t() 202 | defp build_segments(:parent, allowed_keys, list) do 203 | for %ExTwilio.Parent{module: module, key: key} <- allowed_keys, 204 | into: "", 205 | do: segment(:parent, {%ExTwilio.Parent{module: module, key: key}, list[key]}) 206 | end 207 | 208 | defp build_segments(type, allowed_keys, list) do 209 | for key <- allowed_keys, into: "", do: segment(type, {key, list[key]}) 210 | end 211 | 212 | @spec segment(atom, {any, any}) :: String.t() 213 | defp segment(type, segment) 214 | defp segment(type, {_key, nil}) when type in [:parent, :child], do: "" 215 | defp segment(:child, {_key, value}), do: "/" <> to_string(value) 216 | defp segment(:main, {key, nil}), do: "/" <> inflect(key) 217 | defp segment(:main, {key, value}), do: "/#{inflect(key)}/#{value}" 218 | 219 | defp segment(:parent, {%ExTwilio.Parent{module: module, key: _key}, value}) do 220 | "/#{module.resource_name}/#{value}" 221 | end 222 | 223 | @spec inflect(String.t() | atom) :: String.t() 224 | defp inflect(string) when is_binary(string), do: string 225 | 226 | defp inflect(atom) when is_atom(atom) do 227 | atom |> camelize |> Inflex.pluralize() 228 | end 229 | 230 | @spec camelize(String.t() | atom) :: String.t() 231 | defp camelize(name) do 232 | name |> to_string |> Macro.camelize() 233 | end 234 | end 235 | -------------------------------------------------------------------------------- /lib/ex_twilio/resources/notify/notification.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.Notify.Notification do 2 | @moduledoc """ 3 | Represents a Notification resource in the Twilio Notify. 4 | 5 | - [Twilio docs](https://www.twilio.com/docs/notify/api/notifications) 6 | 7 | - body (optional for all except Alexa) Indicates the notification body text. 8 | Translates to `data.twi_body` for FCM and GCM, `aps.alert.body` for APNS, 9 | `Body` for SMS and Facebook Messenger and `request.message.data` for Alexa. 10 | For SMS either this, `body`, or the `media_url` attribute of the `Sms` 11 | parameter is required. For Facebook Messenger either this parameter or the 12 | body attribute in the `FacebookMessenger` parameter is required. 13 | - priority Two priorities defined: `low` and `high` (default). `low` 14 | optimizes the client app's battery consumption, and notifications may be 15 | delivered with unspecified delay. This is the same as Normal priority for FCM 16 | and GCM or priority 5 for APNS. `high` sends the notification immediately, 17 | and can wake up a sleeping device. This is the same as High priority for FCM 18 | and GCM or priority 10 for APNS. This feature is not supported by SMS and 19 | Facebook Messenger and will be ignored for deliveries via those channels. 20 | - ttl This parameter specifies how long (in seconds) the notification is 21 | valid. Delivery should be attempted if the device is offline. The maximum 22 | time to live supported is 4 weeks. The value zero means that the notification 23 | delivery is attempted immediately once but not stored for future delivery. 24 | The default value is 4 weeks. This feature is not supported by SMS and 25 | Facebook Messenger and will be ignored for deliveries via those channels. 26 | - title (optional for all except Alexa) Indicates the notification title. 27 | This field is not visible on iOS phones and tablets but it is on Apple Watch 28 | and Android devices. Translates to `data.twi_title` for FCM and GCM, 29 | `aps.alert.title` for APNS and `displayInfo.content[0].title`, 30 | `displayInfo.content[].toast.primaryText` of `request.message` for Alexa. It 31 | is not supported for SMS and Facebook Messenger and will be omitted from 32 | deliveries via those channels. 33 | - sound Indicates a sound to be played. Translates to `data.twi_sound` for 34 | FCM and GCM and `aps.sound` for APNS. This parameter is not supported by SMS 35 | and Facebook Messenger and is omitted from deliveries via those channels. 36 | - action Specifies the actions to be displayed for the notification. 37 | Translates to `data.twi_action` for GCM and `aps.category` for APNS. This 38 | parameter is not supported by SMS and Facebook Messenger and is omitted from 39 | deliveries via those channels. 40 | - data This parameter specifies the custom key-value pairs of the 41 | notification's payload. Translates to `data` dictionary in FCM and GCM 42 | payload. FCM and GCM [reserves certain keys](https://firebase.google.com/docs/cloud-messaging/http-server-ref) 43 | that cannot be used for those channels. For APNS, attributes of `Data` will be 44 | inserted into the APNS payload as custom properties outside of the `aps` 45 | dictionary. For Alexa they are added to `request.message.data`. For all 46 | channels, the `twi_` prefix is reserved for Twilio for future use. Requests 47 | including custom data with keys starting with `twi_` will be rejected as 400 48 | Bad request and no delivery will be attempted. This parameter is not 49 | supported by SMS and Facebook Messenger and is omitted from deliveries via 50 | those channels. 51 | -apn APNS specific payload that overrides corresponding attributes in a 52 | generic payload for Bindings with the apn BindingType. This value is mapped 53 | to the Payload item, therefore the `aps` key has to be used to change standard 54 | attributes. Adds custom key-value pairs to the root of the dictionary. Refer 55 | to [APNS documentation](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html) for more 56 | details. The `twi_` key prefix for custom key-value pairs is reserved for 57 | Twilio for future use. Custom data with keys starting with `twi_` is not 58 | allowed. 59 | - gcm GCM specific payload that overrides corresponding attributes in generic 60 | payload for Bindings with gcm BindingType. This value is mapped to the root 61 | json dictionary. Refer to [GCM documentation](https://developers.google.com/cloud-messaging/http-server-ref) 62 | for more details. Target parameters `to`, `registration_ids`, and 63 | `notification_key` are not allowed. The `twi_` key prefix for custom key-value 64 | pairs is reserved for Twilio for future use. Custom data with keys starting 65 | with `twi_` is not allowed. FCM and GCM 66 | [reserves certain keys](https://firebase.google.com/docs/cloud-messaging/http-server-ref) 67 | that cannot be used for those channels. 68 | - sms SMS specific payload that overrides corresponding attributes in generic 69 | payload for Bindings with sms BindingType. Each attribute in this JSON 70 | object is mapped to the corresponding form parameter of the Twilio 71 | [Message](https://www.twilio.com/docs/api/rest/sending-messages) resource. 72 | The following parameters of the Message resource are supported in snake case 73 | format: `body`, `media_urls`, `status_callback`, and `max_price`. The 74 | `status_callback` parameter overrides the corresponding parameter in the 75 | messaging service if configured. The `media_urls` expects a JSON array. 76 | - facebook_messenger Messenger specific payload that overrides corresponding 77 | attributes in generic payload for Bindings with facebook-messenger 78 | BindingType. This value is mapped to the root json dictionary of Facebook's 79 | [Send API request](https://developers.facebook.com/docs/messenger-platform/send-api-reference). 80 | Overriding the `recipient` parameter is not allowed. 81 | - fcm FCM specific payload that overrides corresponding attributes in generic 82 | payload for Bindings with fcm BindingType. This value is mapped to the root 83 | json dictionary. Refer to [FCM documentation](https://firebase.google.com/docs/cloud-messaging/http-server-ref#downstream) 84 | for more details. Target parameters `to`, `registration_ids`, `condition`, 85 | and `notification_key` are not allowed. The `twi_` key prefix for custom 86 | key-value pairs is reserved for Twilio for future use. Custom data with keys 87 | starting with `twi_` is not allowed. Custom data with keys starting with 88 | `twi_` is not allowed. FCM and GCM 89 | [reserves certain keys](https://firebase.google.com/docs/cloud-messaging/http-server-ref) 90 | that cannot be used for those channels. 91 | - segment The segment 92 | - alexa The alexa 93 | - to_binding The destination address in a JSON object (see attributes below). 94 | Multiple ToBinding parameters can be included but the total size of the 95 | request entity should not exceed 1MB. This is typically sufficient for 96 | 10,000 phone numbers. 97 | - identity Delivery will be attempted only to Bindings with an Identity in 98 | this list. Maximum 20 items allowed in this list. 99 | - tag Delivery will be attempted only to Bindings that have all of the Tags 100 | in this list. Maximum 5 items allowed in this list. The implicit tag "all" is 101 | available to notify all Bindings in a Service instance. Similarly the 102 | implicit tags "apn", "fcm", "gcm", "sms" and "facebook-messenger" are 103 | available to notify all Bindings of the given type. 104 | """ 105 | defstruct sid: nil, 106 | account_sid: nil, 107 | service_sid: nil, 108 | date_created: nil, 109 | identities: nil, 110 | tags: nil, 111 | tag: nil, 112 | segments: nil, 113 | priority: nil, 114 | ttl: nil, 115 | title: nil, 116 | body: nil, 117 | sound: nil, 118 | action: nil, 119 | data: nil, 120 | apn: nil, 121 | gcm: nil, 122 | fcm: nil, 123 | sms: nil, 124 | facebook_messenger: nil, 125 | alexa: nil, 126 | to_binding: nil, 127 | identity: nil 128 | 129 | use ExTwilio.Resource, 130 | import: [ 131 | :create 132 | ] 133 | 134 | def parents, 135 | do: [ 136 | %ExTwilio.Parent{module: ExTwilio.Notify.Service, key: :service} 137 | ] 138 | end 139 | -------------------------------------------------------------------------------- /lib/ex_twilio/capability.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.Capability do 2 | @moduledoc """ 3 | Capability tokens are used to sign communications from devices 4 | to Twilio. 5 | 6 | You create a token on your server, specify what capabilities you would like 7 | your device to have, then pass the token to your client to use. The tokens 8 | generated are JSON Web Tokens (JWT). 9 | 10 | - [Capability docs](https://www.twilio.com/docs/api/client/capability-tokens) 11 | - [JWT docs](https://jwt.io/introduction/) 12 | 13 | ## Examples 14 | 15 | ExTwilio.Capability.new 16 | |> ExTwilio.Capability.allow_client_incoming("tommy") 17 | |> ExTwilio.Capability.allow_client_outgoing("APabe7650f654fc34655fc81ae71caa3ff") 18 | |> ExTwilio.Capability.token 19 | "xxxxx.yyyyy.zzzzz" 20 | 21 | """ 22 | 23 | alias ExTwilio.Config 24 | use Joken.Config 25 | 26 | defstruct incoming_client_names: [], 27 | outgoing_client_app: nil, 28 | ttl: nil, 29 | start_time: nil, 30 | auth_token: nil, 31 | account_sid: nil 32 | 33 | @type outgoing_client_app :: {String.t(), map} 34 | 35 | @type t :: %__MODULE__{ 36 | incoming_client_names: list, 37 | outgoing_client_app: outgoing_client_app | nil, 38 | ttl: non_neg_integer | nil, 39 | start_time: non_neg_integer | nil, 40 | auth_token: String.t() | nil, 41 | account_sid: String.t() | nil 42 | } 43 | 44 | @doc """ 45 | Initialises a new capability specification with a TTL of one hour, 46 | and the account sid and auth token taken from the configuration. 47 | 48 | ## Examples 49 | 50 | ExTwilio.Capability.new 51 | 52 | """ 53 | @spec new :: t 54 | def new do 55 | %__MODULE__{} 56 | |> starting_at(:erlang.system_time(:seconds)) 57 | |> with_ttl(3600) 58 | |> with_account_sid(Config.account_sid()) 59 | |> with_auth_token(Config.auth_token()) 60 | end 61 | 62 | @doc """ 63 | Gives the device a client name allowing incoming connections 64 | to the client identified by the provided `client_name`. 65 | 66 | - [Incoming capability docs](https://www.twilio.com/docs/api/client/capability-tokens#allow-incoming-connections) 67 | 68 | ## Examples 69 | 70 | A device with this token will be identified as `tommy` 71 | 72 | ExTwilio.Capability.allow_client_incoming("tommy") 73 | 74 | """ 75 | @spec allow_client_incoming(String.t()) :: t 76 | def allow_client_incoming(client_name), do: allow_client_incoming(new(), client_name) 77 | 78 | @spec allow_client_incoming(t, String.t()) :: t 79 | def allow_client_incoming( 80 | capability_struct = %__MODULE__{incoming_client_names: client_names}, 81 | client_name 82 | ) do 83 | %{ 84 | capability_struct 85 | | incoming_client_names: client_names ++ [client_name] 86 | } 87 | end 88 | 89 | @doc """ 90 | Gives the device an application sid so that Twilio can 91 | determine the voice URL to use to handle any outgoing 92 | connection. 93 | 94 | - [Outgoing capability docs](https://www.twilio.com/docs/api/client/capability-tokens#allow-outgoing-connections) 95 | 96 | ## Examples 97 | 98 | Outgoing connections will use the Twilio application with the 99 | SID: `APabe7650f654fc34655fc81ae71caa3ff` 100 | 101 | ExTwilio.Capability.allow_client_outgoing("APabe7650f654fc34655fc81ae71caa3ff") 102 | 103 | """ 104 | @spec allow_client_outgoing(String.t()) :: t 105 | def allow_client_outgoing(app_sid), do: allow_client_outgoing(new(), app_sid) 106 | 107 | @spec allow_client_outgoing(String.t(), map) :: t 108 | def allow_client_outgoing(app_sid, app_params = %{}) when is_binary(app_sid) do 109 | allow_client_outgoing(new(), app_sid, app_params) 110 | end 111 | 112 | @spec allow_client_outgoing(t, String.t()) :: t 113 | def allow_client_outgoing(capability_struct = %__MODULE__{}, app_sid) when is_binary(app_sid) do 114 | allow_client_outgoing(capability_struct, app_sid, %{}) 115 | end 116 | 117 | @spec allow_client_outgoing(t, String.t(), map) :: t 118 | def allow_client_outgoing(capability_struct = %__MODULE__{}, app_sid, app_params = %{}) do 119 | %{capability_struct | outgoing_client_app: {app_sid, app_params}} 120 | end 121 | 122 | @doc """ 123 | Sets the time at which the TTL begins in seconds since epoch. 124 | 125 | ## Examples 126 | 127 | Sets the TTL to begin on 24th May, 2016 128 | 129 | ExTwilio.Capability.starting_at(1464096368) 130 | 131 | """ 132 | @spec starting_at(t, non_neg_integer) :: t 133 | def starting_at(capability_struct = %__MODULE__{}, start_time) do 134 | %{capability_struct | start_time: start_time} 135 | end 136 | 137 | @doc """ 138 | Sets the Twilio account sid used to issue the token. 139 | 140 | ## Examples 141 | 142 | Sets the account sid to be XXX 143 | 144 | ExTwilio.Capability.with_account_sid('XXX') 145 | 146 | """ 147 | @spec with_account_sid(t, String.t()) :: t 148 | def with_account_sid(capability_struct = %__MODULE__{}, account_sid) do 149 | %{capability_struct | account_sid: account_sid} 150 | end 151 | 152 | @doc """ 153 | Sets the Twilio account auth token used to sign the capability token. 154 | 155 | ## Examples 156 | 157 | Sets the auth token to be XXX 158 | 159 | ExTwilio.Capability.with_auth_token('XXX') 160 | 161 | """ 162 | @spec with_auth_token(t, String.t()) :: t 163 | def with_auth_token(capability_struct = %__MODULE__{}, auth_token) do 164 | %{capability_struct | auth_token: auth_token} 165 | end 166 | 167 | @doc """ 168 | Sets the TTL of the token in seconds. 169 | 170 | - [TTL docs](https://www.twilio.com/docs/api/client/capability-tokens#token-expiration) 171 | 172 | ## Examples 173 | 174 | Sets the TTL to one hour 175 | 176 | ExTwilio.Capability.with_ttl(3600) 177 | 178 | """ 179 | @spec with_ttl(t, non_neg_integer) :: t 180 | def with_ttl(capability_struct = %__MODULE__{}, ttl) do 181 | %{capability_struct | ttl: ttl} 182 | end 183 | 184 | @doc """ 185 | Generates a JWT token based on the requested capabilities 186 | that can be provided to the Twilio client. Supports clients 187 | with multiple capabilties. 188 | 189 | - [Multiple capability docs](https://www.twilio.com/docs/api/client/capability-tokens#multiple-capabilities) 190 | 191 | ## Examples 192 | 193 | Generates and signs a token with the provided capabilities 194 | 195 | ExTwilio.Capability.token 196 | 197 | """ 198 | @spec token(t) :: String.t() 199 | def token( 200 | capability_struct = %__MODULE__{ 201 | account_sid: account_sid, 202 | start_time: start_time, 203 | ttl: ttl, 204 | auth_token: auth_token 205 | } 206 | ) do 207 | capability_struct 208 | |> capabilities 209 | |> as_jwt_scope 210 | |> jwt_payload(account_sid, expiration_time(start_time, ttl)) 211 | |> generate_jwt(auth_token) 212 | end 213 | 214 | defp capabilities(capability_struct = %__MODULE__{}) do 215 | incoming_capabililities(capability_struct) ++ outgoing_capabilities(capability_struct) 216 | end 217 | 218 | defp outgoing_capabilities(%__MODULE__{outgoing_client_app: nil}) do 219 | [] 220 | end 221 | 222 | defp outgoing_capabilities(%__MODULE__{outgoing_client_app: {app_sid, app_params}}) 223 | when app_params == %{} do 224 | ["scope:client:outgoing?appSid=#{URI.encode(app_sid)}"] 225 | end 226 | 227 | defp outgoing_capabilities(%__MODULE__{outgoing_client_app: {app_sid, app_params}}) do 228 | app_sid = URI.encode(app_sid) 229 | 230 | app_params = 231 | app_params 232 | |> URI.encode_query() 233 | |> URI.encode(&(!URI.char_reserved?(&1))) 234 | 235 | ["scope:client:outgoing?appSid=#{app_sid}&appParams=#{app_params}"] 236 | end 237 | 238 | defp incoming_capabililities(%__MODULE__{incoming_client_names: client_names}) do 239 | Enum.map(client_names, &incoming_capability(&1)) 240 | end 241 | 242 | defp incoming_capability(client_name) do 243 | "scope:client:incoming?clientName=#{URI.encode(client_name)}" 244 | end 245 | 246 | defp as_jwt_scope(capabilities) do 247 | Enum.join(capabilities, " ") 248 | end 249 | 250 | defp expiration_time(start_time, ttl) do 251 | start_time + ttl 252 | end 253 | 254 | defp jwt_payload(scope, issuer, expiration_time) do 255 | %{ 256 | "scope" => scope, 257 | "iss" => issuer, 258 | "exp" => expiration_time 259 | } 260 | end 261 | 262 | defp generate_jwt(payload, secret) do 263 | signer = Joken.Signer.create("HS256", secret) 264 | Joken.generate_and_sign!(%{}, payload, signer) 265 | end 266 | end 267 | -------------------------------------------------------------------------------- /lib/ex_twilio/worker_capability.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTwilio.WorkerCapability do 2 | @moduledoc """ 3 | Capability tokens are used to sign communications from devices to Twilio. 4 | 5 | You create a token on your server, specify what capabilities you would like 6 | your device to have, then pass the token to your client to use. The tokens 7 | generated are JSON Web Tokens (JWT). 8 | 9 | - [Capability docs](https://www.twilio.com/docs/api/client/capability-tokens) 10 | - [JWT docs](https://jwt.io/introduction/) 11 | 12 | ## Examples 13 | 14 | ExTwilio.WorkerCapability.new("worker_sid", "workspace_sid") 15 | |> ExTwilio.WorkerCapability.token 16 | "xxxxx.yyyyy.zzzzz" 17 | 18 | """ 19 | 20 | alias ExTwilio.Config 21 | use Joken.Config 22 | 23 | defstruct account_sid: nil, 24 | auth_token: nil, 25 | policies: [], 26 | start_time: nil, 27 | ttl: nil, 28 | worker_sid: nil, 29 | workspace_sid: nil 30 | 31 | @type t :: %__MODULE__{ 32 | account_sid: String.t() | nil, 33 | auth_token: String.t() | nil, 34 | policies: list, 35 | start_time: non_neg_integer | nil, 36 | ttl: non_neg_integer | nil, 37 | worker_sid: String.t() | nil, 38 | workspace_sid: String.t() | nil 39 | } 40 | 41 | @doc """ 42 | Initialises a new capability specification with a TTL of one hour, 43 | and the accound sid and auth token taken from the configuration. 44 | 45 | ## Examples 46 | 47 | ExTwilio.WorkerCapability.new 48 | 49 | """ 50 | @spec new(String.t(), String.t()) :: t 51 | def new(worker_sid, workspace_sid) do 52 | %__MODULE__{} 53 | |> starting_at(:erlang.system_time(:seconds)) 54 | |> with_ttl(3600) 55 | |> with_account_sid(Config.account_sid()) 56 | |> with_auth_token(Config.auth_token()) 57 | |> with_worker_sid(worker_sid) 58 | |> with_workspace_sid(workspace_sid) 59 | end 60 | 61 | @doc """ 62 | Sets the time at which the TTL begins in seconds since epoch. 63 | 64 | ## Examples 65 | 66 | Sets the TTL to begin on 24th May, 2016: 67 | 68 | ExTwilio.WorkerCapability.starting_at(1464096368) 69 | 70 | """ 71 | @spec starting_at(t, non_neg_integer) :: t 72 | def starting_at(capability_struct = %__MODULE__{}, start_time) do 73 | %{capability_struct | start_time: start_time} 74 | end 75 | 76 | @doc """ 77 | Sets the Twilio account sid used to issue the token. 78 | 79 | ## Examples 80 | 81 | Sets the account sid to be XXX: 82 | 83 | ExTwilio.WorkerCapability.with_account_sid('XXX') 84 | 85 | """ 86 | @spec with_account_sid(t, String.t()) :: t 87 | def with_account_sid(capability_struct = %__MODULE__{}, account_sid) do 88 | %{capability_struct | account_sid: account_sid} 89 | end 90 | 91 | @doc """ 92 | Sets the Twilio account auth token used to sign the capability token. 93 | 94 | ## Examples 95 | 96 | Sets the auth token to be XXX: 97 | 98 | ExTwilio.WorkerCapability.with_auth_token('XXX') 99 | 100 | """ 101 | @spec with_auth_token(t, String.t()) :: t 102 | def with_auth_token(capability_struct = %__MODULE__{}, auth_token) do 103 | %{capability_struct | auth_token: auth_token} 104 | end 105 | 106 | @doc """ 107 | Sets the Twilio worker sid used to sign the capability token. 108 | 109 | ## Examples 110 | 111 | Sets the worker sid to be XXX: 112 | 113 | ExTwilio.WorkerCapability.with_worker_sid('XXX') 114 | 115 | """ 116 | @spec with_worker_sid(t, String.t()) :: t 117 | def with_worker_sid(capability_struct = %__MODULE__{}, worker_sid) do 118 | %{capability_struct | worker_sid: worker_sid} 119 | end 120 | 121 | @doc """ 122 | Sets the Twilio workspace sid used to sign the capability token. 123 | 124 | ## Examples 125 | 126 | Sets the workspace sid to be XXX: 127 | 128 | ExTwilio.WorkerCapability.with_workspace_sid('XXX') 129 | 130 | """ 131 | @spec with_workspace_sid(t, String.t()) :: t 132 | def with_workspace_sid(capability_struct = %__MODULE__{}, workspace_sid) do 133 | %{capability_struct | workspace_sid: workspace_sid} 134 | end 135 | 136 | @doc """ 137 | Sets the TTL of the token in seconds. 138 | 139 | - [TTL docs](https://www.twilio.com/docs/api/client/capability-tokens#token-expiration) 140 | 141 | ## Examples 142 | 143 | Sets the TTL to one hour: 144 | 145 | ExTwilio.WorkerCapability.with_ttl(3600) 146 | 147 | """ 148 | @spec with_ttl(t, non_neg_integer) :: t 149 | def with_ttl(capability_struct = %__MODULE__{}, ttl) do 150 | %{capability_struct | ttl: ttl} 151 | end 152 | 153 | def allow_activity_updates( 154 | capability_struct = %__MODULE__{ 155 | policies: policies, 156 | worker_sid: worker_sid, 157 | workspace_sid: workspace_sid 158 | } 159 | ) do 160 | policy = 161 | add_policy(worker_reservation_url(workspace_sid, worker_sid), "POST", true, nil, %{ 162 | "ActivitySid" => %{required: true} 163 | }) 164 | 165 | Map.put(capability_struct, :policies, [policy | policies]) 166 | end 167 | 168 | def allow_reservation_updates( 169 | capability_struct = %__MODULE__{ 170 | policies: policies, 171 | worker_sid: worker_sid, 172 | workspace_sid: workspace_sid 173 | } 174 | ) do 175 | policies = allow(policies, task_url(workspace_sid), "POST") 176 | policy = add_policy(worker_reservation_url(workspace_sid, worker_sid), "POST") 177 | Map.put(capability_struct, :policies, [policy | policies]) 178 | end 179 | 180 | @doc """ 181 | Generates a JWT token based on the requested policies. 182 | 183 | ## Examples 184 | 185 | Generates and signs a token with the provided capabilities: 186 | 187 | ExTwilio.WorkerCapability.token 188 | 189 | """ 190 | @spec token(t) :: String.t() 191 | def token(%__MODULE__{ 192 | account_sid: account_sid, 193 | auth_token: auth_token, 194 | policies: policies, 195 | start_time: start_time, 196 | ttl: ttl, 197 | worker_sid: worker_sid, 198 | workspace_sid: workspace_sid 199 | }) do 200 | policies 201 | |> allow(websocket_requests_url(worker_sid, account_sid), "GET") 202 | |> allow(websocket_requests_url(worker_sid, account_sid), "POST") 203 | |> allow(workspaces_base_url(workspace_sid), "GET") 204 | |> allow(activity_url(workspace_sid), "GET") 205 | |> allow(task_url(workspace_sid), "GET") 206 | |> allow(worker_reservation_url(workspace_sid, worker_sid), "GET") 207 | |> jwt_payload(account_sid, expiration_time(start_time, ttl), workspace_sid, worker_sid) 208 | |> generate_jwt(auth_token) 209 | end 210 | 211 | def allow(policies, url, method, query_filters \\ %{}, post_filters \\ %{}) do 212 | policy = add_policy(url, method, true, query_filters, post_filters) 213 | [policy | policies] 214 | end 215 | 216 | defp websocket_requests_url(worker_sid, account_sid) do 217 | "#{Config.task_router_websocket_base_url()}/#{account_sid}/#{worker_sid}" 218 | end 219 | 220 | defp expiration_time(start_time, ttl) do 221 | start_time + ttl 222 | end 223 | 224 | defp jwt_payload(policies, issuer, expiration_time, workspace_sid, worker_sid) do 225 | %{ 226 | "iss" => issuer, 227 | "exp" => expiration_time, 228 | "workspace_sid" => workspace_sid, 229 | "friendly_name" => worker_sid, 230 | "account_sid" => issuer, 231 | "version" => "v1", 232 | "policies" => policies, 233 | "channel" => worker_sid, 234 | "worker_sid" => worker_sid 235 | } 236 | end 237 | 238 | defp add_policy(url, method) do 239 | add_policy(url, method, true, %{}, %{}) 240 | end 241 | 242 | defp add_policy(url, method, allowed, query_filters, post_filters) do 243 | %{ 244 | "url" => url, 245 | "post_filter" => post_filters, 246 | "method" => method, 247 | "allowed" => allowed, 248 | "query_filter" => query_filters 249 | } 250 | end 251 | 252 | defp generate_jwt(payload, secret) do 253 | signer = Joken.Signer.create("HS256", secret) 254 | Joken.generate_and_sign!(%{}, payload, signer) 255 | end 256 | 257 | defp workspaces_base_url(workspace_sid) do 258 | "#{Config.task_router_url()}/Workspaces/#{workspace_sid}" 259 | end 260 | 261 | defp task_url(workspace_sid) do 262 | "#{workspaces_base_url(workspace_sid)}/Tasks/**" 263 | end 264 | 265 | defp activity_url(workspace_sid) do 266 | "#{workspaces_base_url(workspace_sid)}/Activities" 267 | end 268 | 269 | defp worker_reservation_url(workspace_sid, worker_sid) do 270 | "#{workspaces_base_url(workspace_sid)}/Workers/#{worker_sid}" 271 | end 272 | end 273 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ExTwilio 2 | ======== 3 | 4 | [![Hex.pm](https://img.shields.io/hexpm/v/ex_twilio.svg)](https://hex.pm/packages/ex_twilio) 5 | [![Build Status](https://danielberkompas.semaphoreci.com/badges/ex_twilio/branches/master.svg?style=shields)](https://danielberkompas.semaphoreci.com/projects/ex_twilio) 6 | [![Inline docs](http://inch-ci.org/github/danielberkompas/ex_twilio.svg?branch=master)](http://inch-ci.org/github/danielberkompas/ex_twilio) 7 | [![Module Version](https://img.shields.io/hexpm/v/ex_twilio.svg)](https://hex.pm/packages/xxx) 8 | [![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/ex_twilio/) 9 | [![Total Download](https://img.shields.io/hexpm/dt/ex_twilio.svg)](https://hex.pm/packages/xxx) 10 | [![License](https://img.shields.io/hexpm/l/ex_twilio.svg)](https://github.com/danielberkompas/xxx/blob/master/LICENSE) 11 | [![Last Updated](https://img.shields.io/github/last-commit/danielberkompas/ex_twilio.svg)](https://github.com/danielberkompas/xxx/commits/master) 12 | 13 | ExTwilio is a relatively full-featured API client for the Twilio API. 14 | 15 | ## Installation 16 | 17 | ExTwilio is currently beta software. You can install it from Hex: 18 | 19 | ```elixir 20 | def deps do 21 | [ 22 | {:ex_twilio, "~> 0.10.0"} 23 | ] 24 | end 25 | ``` 26 | 27 | Or from Github: 28 | 29 | ```elixir 30 | def deps do 31 | [ 32 | {:ex_twilio, github: "danielberkompas/ex_twilio"} 33 | ] 34 | end 35 | ``` 36 | 37 | and run `mix deps.get`. 38 | 39 | 40 | If using Elixir 1.3 or lower add `:ex_twilio` as a application dependency: 41 | 42 | ```elixir 43 | def application do 44 | [ 45 | applications: [:ex_twilio] 46 | ] 47 | end 48 | ``` 49 | 50 | ## Configuration 51 | 52 | You will need to set the following configuration variables in your 53 | `config/config.exs` file: 54 | 55 | ```elixir 56 | import Config 57 | 58 | config :ex_twilio, account_sid: {:system, "TWILIO_ACCOUNT_SID"}, 59 | auth_token: {:system, "TWILIO_AUTH_TOKEN"}, 60 | workspace_sid: {:system, "TWILIO_WORKSPACE_SID"} # optional 61 | ``` 62 | 63 | For security, I recommend that you use environment variables rather than hard 64 | coding your account credentials. If you don't already have an environment 65 | variable manager, you can create a `.env` file in your project with the 66 | following content: 67 | 68 | ```bash 69 | export TWILIO_ACCOUNT_SID= 70 | export TWILIO_AUTH_TOKEN= 71 | export TWILIO_WORKSPACE_SID= #optional 72 | ``` 73 | 74 | Then, just be sure to run `source .env` in your shell before compiling your 75 | project. 76 | 77 | ### Multiple Environments 78 | If you want to use different Twilio credentials for different environments, then 79 | create separate Mix configuration files for each environment. To do this, change 80 | `config/config.exs` to look like this: 81 | 82 | ```elixir 83 | # config/config.exs 84 | 85 | import Config 86 | 87 | # shared configuration for all environments here ... 88 | 89 | import_config "#{Mix.env}.exs" 90 | ``` 91 | 92 | Then, create a `config/#{environment_name}.exs` file for each environment. You 93 | can then set the `config :ex_twilio` variables differently in each file. 94 | 95 | ## Usage 96 | 97 | ExTwilio comes with a module for each supported Twilio API resource. For example, 98 | the "Call" resource is accessible through the `ExTwilio.Call` module. Depending 99 | on what the underlying API supports, a resource module may have the following 100 | methods: 101 | 102 | | Method | Description | 103 | |-------------|-------------------------------------------------------------------| 104 | | **all** | Eager load all of the resource items on all pages. Use with care! | 105 | | **stream** | Create a Stream of all the items. Use like any Stream. | 106 | | **find** | Find a resource given its SID. | 107 | | **create** | Create a resource. | 108 | | **update** | Update a resource. | 109 | | **destroy** | Destroy a resource. | 110 | 111 | Resource modules may contain their own custom methods. If the underlying API 112 | endpoint does not support an action, the related method will _not_ be available 113 | on that module. 114 | 115 | ### Supported Endpoints 116 | 117 | ExTwilio currently supports the following Twilio endpoints: 118 | 119 | - [Account](https://www.twilio.com/docs/api/2010-04-01/rest/account). Including SubAccounts. 120 | - [Address](https://www.twilio.com/docs/api/2010-04-01/rest/addresses) 121 | - [DependentPhoneNumber](https://www.twilio.com/docs/api/2010-04-01/rest/addresses#instance-subresources) 122 | - [Application](https://www.twilio.com/docs/api/2010-04-01/rest/applications) 123 | - [AuthorizedConnectApp](https://www.twilio.com/docs/api/2010-04-01/rest/authorized-connect-apps) 124 | - [AvailablePhoneNumber](https://www.twilio.com/docs/api/2010-04-01/rest/available-phone-numbers) 125 | - [Call](https://www.twilio.com/docs/api/2010-04-01/rest/call) 126 | - [Feedback](https://www.twilio.com/docs/api/2010-04-01/rest/call-feedback) 127 | - [Conference](https://www.twilio.com/docs/api/2010-04-01/rest/conference) 128 | - [Participant](https://www.twilio.com/docs/api/2010-04-01/rest/participant) 129 | - [ConnectApp](https://www.twilio.com/docs/api/2010-04-01/rest/connect-apps) 130 | - [IncomingPhoneNumber](https://www.twilio.com/docs/api/2010-04-01/rest/incoming-phone-numbers) 131 | - [Message](https://www.twilio.com/docs/api/2010-04-01/rest/message) 132 | - [Media](https://www.twilio.com/docs/api/2010-04-01/rest/media) 133 | - [Notification](https://www.twilio.com/docs/api/notifications/rest) 134 | - [OutgoingCallerId](https://www.twilio.com/docs/api/2010-04-01/rest/outgoing-caller-ids) 135 | - [Queue](https://www.twilio.com/docs/api/2010-04-01/rest/queue) 136 | - [Member](https://www.twilio.com/docs/api/2010-04-01/rest/member) 137 | - [Recording](https://www.twilio.com/docs/api/2010-04-01/rest/recording) 138 | - [ShortCode](https://www.twilio.com/docs/api/2010-04-01/rest/short-codes) 139 | - [Token](https://www.twilio.com/docs/api/2010-04-01/rest/token) 140 | - [Transcription](https://www.twilio.com/docs/api/2010-04-01/rest/transcription) 141 | - [SipCredentialList](https://www.twilio.com/docs/api/2010-04-01/rest/credential-list) 142 | - [SipCredential](https://www.twilio.com/docs/api/rest/credential-list#subresources) 143 | - [SipDomain](https://www.twilio.com/docs/api/2010-04-01/rest/domain) 144 | - [SipIPAccessControlList](https://www.twilio.com/docs/api/2010-04-01/rest/ip-access-control-list) 145 | - [SipIpAddress](https://www.twilio.com/docs/api/rest/ip-access-control-list#subresources) 146 | 147 | Twilio's Lookup Rest API: 148 | 149 | - [Lookup](https://www.twilio.com/docs/api/lookups) 150 | 151 | Twilio's TaskRouter API: 152 | 153 | - [Overview](https://www.twilio.com/docs/api/taskrouter/rest-api) 154 | - [Activites](https://www.twilio.com/docs/api/taskrouter/activities) 155 | - [Events](https://www.twilio.com/docs/api/taskrouter/events) 156 | - [Task Channels](https://www.twilio.com/docs/api/taskrouter/rest-api-task-channel) 157 | - [Tasks](https://www.twilio.com/docs/api/taskrouter/tasks) 158 | - [Reservations](https://www.twilio.com/docs/api/taskrouter/reservations) 159 | - [TaskQueues](https://www.twilio.com/docs/api/taskrouter/taskqueues) 160 | - [Statistics](https://www.twilio.com/docs/api/taskrouter/taskqueue-statistics) 161 | - [Workers](https://www.twilio.com/docs/api/taskrouter/workers) 162 | - [Channels](https://www.twilio.com/docs/api/taskrouter/rest-api-workerchannel) 163 | - [Statistics](https://www.twilio.com/docs/api/taskrouter/worker-statistics) 164 | - [Workflows](https://www.twilio.com/docs/api/taskrouter/workflows) 165 | - [Statistics](https://www.twilio.com/docs/api/taskrouter/workflow-statistics) 166 | - [Workspaces](https://www.twilio.com/docs/api/taskrouter/workspaces) 167 | - [Statistics](https://www.twilio.com/docs/api/taskrouter/workspace-statistics) 168 | 169 | Twilio's ProgrammableChat API: 170 | 171 | - [Overview](https://www.twilio.com/docs/api/chat/rest) 172 | - [Services](https://www.twilio.com/docs/api/chat/rest/services) 173 | - [Channels](https://www.twilio.com/docs/api/chat/rest/channels) 174 | - [Members](https://www.twilio.com/docs/api/chat/rest/members) 175 | - [Users](https://www.twilio.com/docs/api/chat/rest/users) 176 | - [UserChannels](https://www.twilio.com/docs/api/chat/rest/user-channels) 177 | - [Roles](https://www.twilio.com/docs/api/chat/rest/user-channels) 178 | - [Credentials](https://www.twilio.com/docs/api/chat/rest/credentials) 179 | 180 | Twilio Capability Tokens: 181 | - [Worker](https://www.twilio.com/docs/api/taskrouter/worker-js) 182 | - [Calling](https://www.twilio.com/docs/api/client/capability-tokens) (Deprecated, use Access Token instead) 183 | 184 | Twilio Access Token Grants: 185 | - [Chat](https://www.twilio.com/docs/chat/identity) 186 | - [Voice](https://www.twilio.com/docs/iam/access-tokens) 187 | - [Video](https://www.twilio.com/docs/video/tutorials/user-identity-access-tokens#about-access-tokens) 188 | 189 | ### Example 190 | 191 | ```elixir 192 | # Get all the calls in the Call endpoint. Be warned, this will block 193 | # until all the pages of calls have been fetched. 194 | calls = ExTwilio.Call.all 195 | 196 | # Create a stream of all the calls 197 | stream = ExTwilio.Call.stream 198 | 199 | # Lazily filter calls by duration, then map to get only their SIDs 200 | stream 201 | |> Stream.filter(fn(call) -> call.duration > 120 end) 202 | |> Stream.map(fn(call) -> call.sid end) 203 | |> Enum.into([]) # Only here does any work happen. 204 | # => ["CAc14d7...", "CA649ea861..."] 205 | 206 | # Find a call 207 | {:ok, call} = ExTwilio.Call.find("CA13a9c7f80c6f3761fabae43242b5b6c6") 208 | inspect(call) 209 | # %ExTwilio.Call{ 210 | # account_sid: "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 211 | # answered_by: nil, caller_name: "", 212 | # date_created: "Sat, 14 Mar 2015 14:27:38 +0000", 213 | # date_updated: "Sat, 14 Mar 2015 14:28:35 +0000", 214 | # direction: "outbound-api", 215 | # duration: "52", 216 | # end_time: "Sat, 14 Mar 2015 14:28:35 +0000", 217 | # forwarded_from: nil, 218 | # from: "+1xxxxxxxxxx", 219 | # parent_call_sid: nil, 220 | # phone_number_sid: "", 221 | # price: "-0.01500", 222 | # price_unit: "USD", 223 | # sid: "CA13a9c7f80c6f3761fabae43242b5b6c6", 224 | # start_time: "Sat, 14 Mar 2015 14:27:43 +0000", 225 | # status: "completed", 226 | # to: "+1xxxxxxxxxx", 227 | # uri: "/2010-04-01/Accounts/ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/Calls/CAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.json" 228 | # } 229 | 230 | # Update a call 231 | call = ExTwilio.Call.update(call, status: "canceled") 232 | 233 | # Get a call's recordings. This pattern is repeated wherever you are 234 | # getting a nested resource. 235 | recordings = ExTwilio.Recording.all(call: call.sid) 236 | 237 | # Destroy a call 238 | ExTwilio.Call.destroy(call) 239 | ``` 240 | 241 | For more in-depth documentation, see the generated docs for each module. 242 | 243 | ### Making and Receiving Calls 244 | 245 | See the [CALLING_TUTORIAL.md](CALLING_TUTORIAL.md) file for instructions on 246 | making and receiving calls from the browser with ExTwilio. 247 | 248 | 249 | ### Sending SMS messages 250 | 251 | Please look at `ExTwilio.Message` 252 | 253 | ## Contributing 254 | 255 | See the [CONTRIBUTING.md](CONTRIBUTING.md) file for contribution guidelines. 256 | 257 | ## Copyright and License 258 | 259 | Copyright (c) 2015 Daniel Berkompas 260 | 261 | ExTwilio is licensed under the MIT license. For more details, see the `LICENSE` 262 | file at the root of the repository. It depends on Elixir, which is under the 263 | Apache 2 license. 264 | 265 | Twilio™ is trademark of Twilio, Inc. 266 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.10.0](https://github.com/danielberkompas/ex_twilio/tree/0.10.0) (2024-04-05) 4 | 5 | [Full Changelog](https://github.com/danielberkompas/ex_twilio/compare/v0.9.1...0.10.0) 6 | 7 | **Closed issues:** 8 | 9 | - Add support for Proxy API [\#162](https://github.com/danielberkompas/ex_twilio/issues/162) 10 | - http client adapter/behavior [\#161](https://github.com/danielberkompas/ex_twilio/issues/161) 11 | - Allow configurable urls. [\#160](https://github.com/danielberkompas/ex_twilio/issues/160) 12 | 13 | **Merged pull requests:** 14 | 15 | - Fix deprecation warnings on modern Elixir [\#181](https://github.com/danielberkompas/ex_twilio/pull/181) ([danielberkompas](https://github.com/danielberkompas)) 16 | - Misc doc changes [\#145](https://github.com/danielberkompas/ex_twilio/pull/145) ([kianmeng](https://github.com/kianmeng)) 17 | 18 | ## [v0.9.1](https://github.com/danielberkompas/ex_twilio/tree/v0.9.1) (2021-06-05) 19 | 20 | [Full Changelog](https://github.com/danielberkompas/ex_twilio/compare/v0.9.0...v0.9.1) 21 | 22 | **Closed issues:** 23 | 24 | - Not compatible with Erlang/OTP 24 [\#155](https://github.com/danielberkompas/ex_twilio/issues/155) 25 | - HTTPoison error Fatal - Unknown CA. [\#151](https://github.com/danielberkompas/ex_twilio/issues/151) 26 | 27 | **Merged pull requests:** 28 | 29 | - Run tests against Erlang 24, Elixir 1.12 [\#156](https://github.com/danielberkompas/ex_twilio/pull/156) ([danielberkompas](https://github.com/danielberkompas)) 30 | - fix\(deprecation\): :crypto.hmac -\> :crypto.mac, Bitwise.^^^ -\> Bitwise.bxor [\#154](https://github.com/danielberkompas/ex_twilio/pull/154) ([seantanly](https://github.com/seantanly)) 31 | - Fix dialyzer errors [\#150](https://github.com/danielberkompas/ex_twilio/pull/150) ([drgmr](https://github.com/drgmr)) 32 | - Documentation Clarification [\#149](https://github.com/danielberkompas/ex_twilio/pull/149) ([imrying](https://github.com/imrying)) 33 | - Use Semaphore for CI [\#146](https://github.com/danielberkompas/ex_twilio/pull/146) ([danielberkompas](https://github.com/danielberkompas)) 34 | 35 | ## [v0.9.0](https://github.com/danielberkompas/ex_twilio/tree/v0.9.0) (2021-02-05) 36 | 37 | [Full Changelog](https://github.com/danielberkompas/ex_twilio/compare/v0.8.2...v0.9.0) 38 | 39 | **Merged pull requests:** 40 | 41 | - Replace Poison with Jason [\#143](https://github.com/danielberkompas/ex_twilio/pull/143) ([tomciopp](https://github.com/tomciopp)) 42 | - Allow request options to be configured for HTTPoison/hackney Twilio requests [\#141](https://github.com/danielberkompas/ex_twilio/pull/141) ([paulanthonywilson](https://github.com/paulanthonywilson)) 43 | 44 | ## [v0.8.2](https://github.com/danielberkompas/ex_twilio/tree/v0.8.2) (2020-10-01) 45 | 46 | [Full Changelog](https://github.com/danielberkompas/ex_twilio/compare/v0.8.1...v0.8.2) 47 | 48 | **Fixed bugs:** 49 | 50 | - problem with AvailablePhoneNumbers.all [\#96](https://github.com/danielberkompas/ex_twilio/issues/96) 51 | - Getting error after creating/sending message [\#37](https://github.com/danielberkompas/ex_twilio/issues/37) 52 | 53 | **Closed issues:** 54 | 55 | - Add :enabled option to config [\#138](https://github.com/danielberkompas/ex_twilio/issues/138) 56 | - Where is the documentation for making the webhook receive incoming SMS messages? [\#135](https://github.com/danielberkompas/ex_twilio/issues/135) 57 | - RequestValidator returns false, even when the strings both match [\#133](https://github.com/danielberkompas/ex_twilio/issues/133) 58 | - leaking `{ssl_closed, _}` messages from httpoison/hackney [\#132](https://github.com/danielberkompas/ex_twilio/issues/132) 59 | - Thank you for building this! [\#130](https://github.com/danielberkompas/ex_twilio/issues/130) 60 | - Support for multiple accounts [\#126](https://github.com/danielberkompas/ex_twilio/issues/126) 61 | - Config question with 1.9 coming - this is not an issue, just a question. [\#118](https://github.com/danielberkompas/ex_twilio/issues/118) 62 | - ExTwilio.Signature for creating a "x-twilio-signiture" string [\#108](https://github.com/danielberkompas/ex_twilio/issues/108) 63 | - Doesn't work with Dotenv [\#63](https://github.com/danielberkompas/ex_twilio/issues/63) 64 | 65 | **Merged pull requests:** 66 | 67 | - Update participant resource [\#140](https://github.com/danielberkompas/ex_twilio/pull/140) ([rrebane](https://github.com/rrebane)) 68 | - Add Rooms resource [\#139](https://github.com/danielberkompas/ex_twilio/pull/139) ([dshvimer2](https://github.com/dshvimer2)) 69 | - Fix typos [\#136](https://github.com/danielberkompas/ex_twilio/pull/136) ([yakryder](https://github.com/yakryder)) 70 | - Add voice grant [\#134](https://github.com/danielberkompas/ex_twilio/pull/134) ([ostap0207](https://github.com/ostap0207)) 71 | 72 | ## [v0.8.1](https://github.com/danielberkompas/ex_twilio/tree/v0.8.1) (2019-12-06) 73 | 74 | [Full Changelog](https://github.com/danielberkompas/ex_twilio/compare/v0.8.0...v0.8.1) 75 | 76 | **Merged pull requests:** 77 | 78 | - Update broken and redirected links [\#131](https://github.com/danielberkompas/ex_twilio/pull/131) ([dikaio](https://github.com/dikaio)) 79 | 80 | ## [v0.8.0](https://github.com/danielberkompas/ex_twilio/tree/v0.8.0) (2019-11-28) 81 | 82 | [Full Changelog](https://github.com/danielberkompas/ex_twilio/compare/v0.7.0...v0.8.0) 83 | 84 | **Closed issues:** 85 | 86 | - SMS opt-out [\#127](https://github.com/danielberkompas/ex_twilio/issues/127) 87 | - No status callbacks for sms [\#120](https://github.com/danielberkompas/ex_twilio/issues/120) 88 | 89 | **Merged pull requests:** 90 | 91 | - chore\(JSON\): Allow people to use the most recent version of Poison, r… [\#129](https://github.com/danielberkompas/ex_twilio/pull/129) ([tomciopp](https://github.com/tomciopp)) 92 | - Add Studio Flow Execution + Step [\#125](https://github.com/danielberkompas/ex_twilio/pull/125) ([jc00ke](https://github.com/jc00ke)) 93 | - Bump Inflex to 2.0 [\#123](https://github.com/danielberkompas/ex_twilio/pull/123) ([kiere](https://github.com/kiere)) 94 | - Add new fields to incoming phone number resources [\#122](https://github.com/danielberkompas/ex_twilio/pull/122) ([ferd](https://github.com/ferd)) 95 | - Add Fax resource [\#119](https://github.com/danielberkompas/ex_twilio/pull/119) ([schneiderderek](https://github.com/schneiderderek)) 96 | - Add support for Video Grants in Access tokens [\#117](https://github.com/danielberkompas/ex_twilio/pull/117) ([arielo](https://github.com/arielo)) 97 | - Add status, identity\_sid to IncomingPhoneNumber fields [\#116](https://github.com/danielberkompas/ex_twilio/pull/116) ([novaugust](https://github.com/novaugust)) 98 | - remove "list" function mentions [\#115](https://github.com/danielberkompas/ex_twilio/pull/115) ([gliush](https://github.com/gliush)) 99 | 100 | ## [v0.7.0](https://github.com/danielberkompas/ex_twilio/tree/v0.7.0) (2019-01-26) 101 | 102 | [Full Changelog](https://github.com/danielberkompas/ex_twilio/compare/v0.6.1...v0.7.0) 103 | 104 | **Closed issues:** 105 | 106 | - Upgrade to Joken 2.0.0 [\#113](https://github.com/danielberkompas/ex_twilio/issues/113) 107 | - `ExTwilio.Member.all queue: queue_sid` fails to parse correct response [\#111](https://github.com/danielberkompas/ex_twilio/issues/111) 108 | - Bypass with ExTwilio [\#109](https://github.com/danielberkompas/ex_twilio/issues/109) 109 | - Message.create\(\) works in v0.6.0 and broken in v0.6.1 [\#107](https://github.com/danielberkompas/ex_twilio/issues/107) 110 | - Requesting a release [\#106](https://github.com/danielberkompas/ex_twilio/issues/106) 111 | - 202 http status code is handled as error [\#97](https://github.com/danielberkompas/ex_twilio/issues/97) 112 | - Finding Subaccount by Friendly Name [\#93](https://github.com/danielberkompas/ex_twilio/issues/93) 113 | - Call vs. Messaging [\#84](https://github.com/danielberkompas/ex_twilio/issues/84) 114 | 115 | **Merged pull requests:** 116 | 117 | - Upgrade to Joken 2.0 [\#114](https://github.com/danielberkompas/ex_twilio/pull/114) ([lewisf](https://github.com/lewisf)) 118 | - fixing wrong resource\_collection\_name in ExTwilio.Memeber resource [\#112](https://github.com/danielberkompas/ex_twilio/pull/112) ([mjaric](https://github.com/mjaric)) 119 | - add protocol option for testing [\#110](https://github.com/danielberkompas/ex_twilio/pull/110) ([BenMorganIO](https://github.com/BenMorganIO)) 120 | - Create Notify resource [\#105](https://github.com/danielberkompas/ex_twilio/pull/105) ([MortadaAK](https://github.com/MortadaAK)) 121 | - Add docs around sending an SMS message. [\#104](https://github.com/danielberkompas/ex_twilio/pull/104) ([pdgonzalez872](https://github.com/pdgonzalez872)) 122 | - Parser: Return full error JSON [\#100](https://github.com/danielberkompas/ex_twilio/pull/100) ([xtian](https://github.com/xtian)) 123 | - Parser to handle 202 http status [\#98](https://github.com/danielberkompas/ex_twilio/pull/98) ([ins429](https://github.com/ins429)) 124 | 125 | ## [v0.6.1](https://github.com/danielberkompas/ex_twilio/tree/v0.6.1) (2018-10-03) 126 | 127 | [Full Changelog](https://github.com/danielberkompas/ex_twilio/compare/v0.6.0...v0.6.1) 128 | 129 | **Closed issues:** 130 | 131 | - Support for :create on Participant Resource [\#102](https://github.com/danielberkompas/ex_twilio/issues/102) 132 | - Utilizing subaccounts / credentials [\#95](https://github.com/danielberkompas/ex_twilio/issues/95) 133 | 134 | **Merged pull requests:** 135 | 136 | - Adds create to participant. [\#103](https://github.com/danielberkompas/ex_twilio/pull/103) ([jip1080](https://github.com/jip1080)) 137 | - add messaging\_service\_sid to the message resource [\#94](https://github.com/danielberkompas/ex_twilio/pull/94) ([swelham](https://github.com/swelham)) 138 | 139 | ## [v0.6.0](https://github.com/danielberkompas/ex_twilio/tree/v0.6.0) (2018-04-16) 140 | 141 | [Full Changelog](https://github.com/danielberkompas/ex_twilio/compare/v0.5.1...v0.6.0) 142 | 143 | **Closed issues:** 144 | 145 | - HTTPoison.Error :nxdomain in lib/ex\_twilio/api.ex:19 [\#88](https://github.com/danielberkompas/ex_twilio/issues/88) 146 | 147 | **Merged pull requests:** 148 | 149 | - Add JWT support [\#92](https://github.com/danielberkompas/ex_twilio/pull/92) ([danielberkompas](https://github.com/danielberkompas)) 150 | - fix next\_page\_url generation & run mix format [\#91](https://github.com/danielberkompas/ex_twilio/pull/91) ([techgaun](https://github.com/techgaun)) 151 | - feat\(RequestValidator\): Adds a module that handles validating request… [\#90](https://github.com/danielberkompas/ex_twilio/pull/90) ([tomciopp](https://github.com/tomciopp)) 152 | - chore\(docs\): Default to reading from system environment variables [\#89](https://github.com/danielberkompas/ex_twilio/pull/89) ([tomciopp](https://github.com/tomciopp)) 153 | 154 | ## [v0.5.1](https://github.com/danielberkompas/ex_twilio/tree/v0.5.1) (2018-02-24) 155 | 156 | [Full Changelog](https://github.com/danielberkompas/ex_twilio/compare/v0.5.0...v0.5.1) 157 | 158 | **Closed issues:** 159 | 160 | - Retrieving A Programmable Chat User. [\#85](https://github.com/danielberkompas/ex_twilio/issues/85) 161 | 162 | **Merged pull requests:** 163 | 164 | - Refactor ExTwilio.ResultStream to simplify streaming code [\#87](https://github.com/danielberkompas/ex_twilio/pull/87) ([scarfacedeb](https://github.com/scarfacedeb)) 165 | - Remove puts that could not be muted [\#86](https://github.com/danielberkompas/ex_twilio/pull/86) ([john-griffin](https://github.com/john-griffin)) 166 | - Config does not need system [\#82](https://github.com/danielberkompas/ex_twilio/pull/82) ([mahcloud](https://github.com/mahcloud)) 167 | 168 | ## [v0.5.0](https://github.com/danielberkompas/ex_twilio/tree/v0.5.0) (2017-09-21) 169 | 170 | [Full Changelog](https://github.com/danielberkompas/ex_twilio/compare/v0.4.0...v0.5.0) 171 | 172 | **Closed issues:** 173 | 174 | - Update to Poison \>= 3 [\#72](https://github.com/danielberkompas/ex_twilio/issues/72) 175 | 176 | **Merged pull requests:** 177 | 178 | - Add all endpoints for the programmable chat api. [\#81](https://github.com/danielberkompas/ex_twilio/pull/81) ([4141done](https://github.com/4141done)) 179 | - Remove someone's hardcoded workspace sid from the url\_builder [\#80](https://github.com/danielberkompas/ex_twilio/pull/80) ([4141done](https://github.com/4141done)) 180 | - Handle query string encoding of list values for the param map. [\#79](https://github.com/danielberkompas/ex_twilio/pull/79) ([m4ttsch](https://github.com/m4ttsch)) 181 | 182 | ## [v0.4.0](https://github.com/danielberkompas/ex_twilio/tree/v0.4.0) (2017-07-10) 183 | 184 | [Full Changelog](https://github.com/danielberkompas/ex_twilio/compare/v0.3.0...v0.4.0) 185 | 186 | **Closed issues:** 187 | 188 | - Subaccounts support [\#76](https://github.com/danielberkompas/ex_twilio/issues/76) 189 | - Add messaging\_service\_sid to the Message Resource [\#75](https://github.com/danielberkompas/ex_twilio/issues/75) 190 | - Setting MaxPrice on message [\#70](https://github.com/danielberkompas/ex_twilio/issues/70) 191 | - Error while creating sms [\#69](https://github.com/danielberkompas/ex_twilio/issues/69) 192 | 193 | **Merged pull requests:** 194 | 195 | - 76 - Subaccounts handling [\#78](https://github.com/danielberkompas/ex_twilio/pull/78) ([andrewshatnyy](https://github.com/andrewshatnyy)) 196 | - Upgrade Joken to 1.4.1 [\#74](https://github.com/danielberkompas/ex_twilio/pull/74) ([joshuafleck](https://github.com/joshuafleck)) 197 | - Replace 'accound' with 'account' in capability.ex [\#73](https://github.com/danielberkompas/ex_twilio/pull/73) ([yakryder](https://github.com/yakryder)) 198 | - Adds Update API action to Conference resource. [\#71](https://github.com/danielberkompas/ex_twilio/pull/71) ([m4ttsch](https://github.com/m4ttsch)) 199 | 200 | ## [v0.3.0](https://github.com/danielberkompas/ex_twilio/tree/v0.3.0) (2017-01-20) 201 | 202 | [Full Changelog](https://github.com/danielberkompas/ex_twilio/compare/v0.2.1...v0.3.0) 203 | 204 | **Closed issues:** 205 | 206 | - Configure accound\_sid and auth\_token in run time [\#65](https://github.com/danielberkompas/ex_twilio/issues/65) 207 | - protocol String.Chars not implemented for %{"mms" =\> false, "sms" =\> false, "voice" =\> true} [\#57](https://github.com/danielberkompas/ex_twilio/issues/57) 208 | - problem with lib [\#52](https://github.com/danielberkompas/ex_twilio/issues/52) 209 | - Notifications API url broken [\#46](https://github.com/danielberkompas/ex_twilio/issues/46) 210 | 211 | **Merged pull requests:** 212 | 213 | - \[\#65\] Allow runtime config with {:system} tuples [\#66](https://github.com/danielberkompas/ex_twilio/pull/66) ([danielberkompas](https://github.com/danielberkompas)) 214 | - typo: correct link to the documentation [\#64](https://github.com/danielberkompas/ex_twilio/pull/64) ([gliush](https://github.com/gliush)) 215 | - Add specifying outgoing client capability params [\#62](https://github.com/danielberkompas/ex_twilio/pull/62) ([brain-geek](https://github.com/brain-geek)) 216 | - Add caller\_name field to lookup [\#61](https://github.com/danielberkompas/ex_twilio/pull/61) ([he9lin](https://github.com/he9lin)) 217 | - Bump elixir version [\#60](https://github.com/danielberkompas/ex_twilio/pull/60) ([enilsen16](https://github.com/enilsen16)) 218 | - Implement Task Router API [\#55](https://github.com/danielberkompas/ex_twilio/pull/55) ([enilsen16](https://github.com/enilsen16)) 219 | 220 | ## [v0.2.1](https://github.com/danielberkompas/ex_twilio/tree/v0.2.1) (2016-10-31) 221 | 222 | [Full Changelog](https://github.com/danielberkompas/ex_twilio/compare/v0.2.0...v0.2.1) 223 | 224 | **Closed issues:** 225 | 226 | - where is "Api.get!" defined? [\#48](https://github.com/danielberkompas/ex_twilio/issues/48) 227 | 228 | **Merged pull requests:** 229 | 230 | - Update joken, mock [\#58](https://github.com/danielberkompas/ex_twilio/pull/58) ([danielberkompas](https://github.com/danielberkompas)) 231 | - Add instructions for receiving a call in the calling tutorial [\#54](https://github.com/danielberkompas/ex_twilio/pull/54) ([joshuafleck](https://github.com/joshuafleck)) 232 | - Add lookup rest api [\#51](https://github.com/danielberkompas/ex_twilio/pull/51) ([enilsen16](https://github.com/enilsen16)) 233 | - Add capability tokens [\#50](https://github.com/danielberkompas/ex_twilio/pull/50) ([joshuafleck](https://github.com/joshuafleck)) 234 | - Make the recommended config exrm compatible [\#49](https://github.com/danielberkompas/ex_twilio/pull/49) ([jeffrafter](https://github.com/jeffrafter)) 235 | - Update notifications url \#46 [\#47](https://github.com/danielberkompas/ex_twilio/pull/47) ([Devinsuit](https://github.com/Devinsuit)) 236 | 237 | ## [v0.2.0](https://github.com/danielberkompas/ex_twilio/tree/v0.2.0) (2016-07-20) 238 | 239 | [Full Changelog](https://github.com/danielberkompas/ex_twilio/compare/v0.1.9...v0.2.0) 240 | 241 | **Merged pull requests:** 242 | 243 | - Add Credo Linter [\#45](https://github.com/danielberkompas/ex_twilio/pull/45) ([danielberkompas](https://github.com/danielberkompas)) 244 | - Switch to HTTPoison [\#44](https://github.com/danielberkompas/ex_twilio/pull/44) ([danielberkompas](https://github.com/danielberkompas)) 245 | 246 | ## [v0.1.9](https://github.com/danielberkompas/ex_twilio/tree/v0.1.9) (2016-07-02) 247 | 248 | [Full Changelog](https://github.com/danielberkompas/ex_twilio/compare/v0.1.8...v0.1.9) 249 | 250 | **Closed issues:** 251 | 252 | - Accessing the Usage logs [\#39](https://github.com/danielberkompas/ex_twilio/issues/39) 253 | - Error on sending messages [\#36](https://github.com/danielberkompas/ex_twilio/issues/36) 254 | - Add ability to create Capability tokens [\#28](https://github.com/danielberkompas/ex_twilio/issues/28) 255 | 256 | **Merged pull requests:** 257 | 258 | - Depend on Hex version of ibrowse [\#42](https://github.com/danielberkompas/ex_twilio/pull/42) ([danielberkompas](https://github.com/danielberkompas)) 259 | - This makes ex\_twilio compatible with elixir 1.3 [\#41](https://github.com/danielberkompas/ex_twilio/pull/41) ([tokafish](https://github.com/tokafish)) 260 | 261 | ## [v0.1.8](https://github.com/danielberkompas/ex_twilio/tree/v0.1.8) (2016-06-06) 262 | 263 | [Full Changelog](https://github.com/danielberkompas/ex_twilio/compare/v0.1.7...v0.1.8) 264 | 265 | **Closed issues:** 266 | 267 | - Lack of domain when running ExTwilio.Call.all\(\) [\#38](https://github.com/danielberkompas/ex_twilio/issues/38) 268 | - Hex dependency resolution issues [\#34](https://github.com/danielberkompas/ex_twilio/issues/34) 269 | - ExTwilio.Message.create throws {:error, "The requested resource /2010-04-01/Messages.json was not found", 404} [\#33](https://github.com/danielberkompas/ex_twilio/issues/33) 270 | - 2 Factor Authentication [\#32](https://github.com/danielberkompas/ex_twilio/issues/32) 271 | - Switch out HTTPotion for Tesla? [\#10](https://github.com/danielberkompas/ex_twilio/issues/10) 272 | 273 | **Merged pull requests:** 274 | 275 | - Provide full url to process\_page when there is more than 50 entries. [\#40](https://github.com/danielberkompas/ex_twilio/pull/40) ([rlb3](https://github.com/rlb3)) 276 | - Remove unnecessary Utils module and replace its usages with Macro [\#35](https://github.com/danielberkompas/ex_twilio/pull/35) ([AvaelKross](https://github.com/AvaelKross)) 277 | 278 | ## [v0.1.7](https://github.com/danielberkompas/ex_twilio/tree/v0.1.7) (2016-04-16) 279 | 280 | [Full Changelog](https://github.com/danielberkompas/ex_twilio/compare/v0.1.6...v0.1.7) 281 | 282 | **Merged pull requests:** 283 | 284 | - Fix Parser for Poison 2.0 [\#31](https://github.com/danielberkompas/ex_twilio/pull/31) ([danielberkompas](https://github.com/danielberkompas)) 285 | 286 | ## [v0.1.6](https://github.com/danielberkompas/ex_twilio/tree/v0.1.6) (2016-04-16) 287 | 288 | [Full Changelog](https://github.com/danielberkompas/ex_twilio/compare/v0.1.5...v0.1.6) 289 | 290 | ## [v0.1.5](https://github.com/danielberkompas/ex_twilio/tree/v0.1.5) (2016-04-16) 291 | 292 | [Full Changelog](https://github.com/danielberkompas/ex_twilio/compare/v0.1.4...v0.1.5) 293 | 294 | ## [v0.1.4](https://github.com/danielberkompas/ex_twilio/tree/v0.1.4) (2016-03-29) 295 | 296 | [Full Changelog](https://github.com/danielberkompas/ex_twilio/compare/v0.1.3...v0.1.4) 297 | 298 | **Closed issues:** 299 | 300 | - Add TaskRouter support [\#27](https://github.com/danielberkompas/ex_twilio/issues/27) 301 | - next\_page and etc implementation? [\#23](https://github.com/danielberkompas/ex_twilio/issues/23) 302 | - How can I use the Message module? [\#22](https://github.com/danielberkompas/ex_twilio/issues/22) 303 | 304 | **Merged pull requests:** 305 | 306 | - Upgrade Travis to Elixir 1.2 and OTP 18 [\#30](https://github.com/danielberkompas/ex_twilio/pull/30) ([danielberkompas](https://github.com/danielberkompas)) 307 | - Allow query strings to contain multiple values for a param [\#29](https://github.com/danielberkompas/ex_twilio/pull/29) ([brentonannan](https://github.com/brentonannan)) 308 | - add fixes for Elixir 1.2 compiler warnings [\#26](https://github.com/danielberkompas/ex_twilio/pull/26) ([jeffweiss](https://github.com/jeffweiss)) 309 | - adds required steps for installation [\#25](https://github.com/danielberkompas/ex_twilio/pull/25) ([AdamBrodzinski](https://github.com/AdamBrodzinski)) 310 | - Remove deleted functions from README [\#24](https://github.com/danielberkompas/ex_twilio/pull/24) ([danielberkompas](https://github.com/danielberkompas)) 311 | 312 | ## [v0.1.3](https://github.com/danielberkompas/ex_twilio/tree/v0.1.3) (2015-10-08) 313 | 314 | [Full Changelog](https://github.com/danielberkompas/ex_twilio/compare/v0.1.2...v0.1.3) 315 | 316 | **Closed issues:** 317 | 318 | - Drop using `Mix.Utils` in runtime. [\#19](https://github.com/danielberkompas/ex_twilio/issues/19) 319 | 320 | **Merged pull requests:** 321 | 322 | - \[\#19\]: Stop using Mix.Utils [\#21](https://github.com/danielberkompas/ex_twilio/pull/21) ([danielberkompas](https://github.com/danielberkompas)) 323 | - Add missing applications to app template [\#20](https://github.com/danielberkompas/ex_twilio/pull/20) ([michalmuskala](https://github.com/michalmuskala)) 324 | 325 | ## [v0.1.2](https://github.com/danielberkompas/ex_twilio/tree/v0.1.2) (2015-09-20) 326 | 327 | [Full Changelog](https://github.com/danielberkompas/ex_twilio/compare/v0.1.1...v0.1.2) 328 | 329 | **Implemented enhancements:** 330 | 331 | - Refactor `stream` function, remove list functions [\#14](https://github.com/danielberkompas/ex_twilio/pull/14) ([danielberkompas](https://github.com/danielberkompas)) 332 | 333 | **Merged pull requests:** 334 | 335 | - Upgrade ExDoc [\#18](https://github.com/danielberkompas/ex_twilio/pull/18) ([danielberkompas](https://github.com/danielberkompas)) 336 | - Update release script [\#17](https://github.com/danielberkompas/ex_twilio/pull/17) ([danielberkompas](https://github.com/danielberkompas)) 337 | - pin Poison dependency to \>= 1.4 and \< 2 [\#16](https://github.com/danielberkompas/ex_twilio/pull/16) ([jeffweiss](https://github.com/jeffweiss)) 338 | - Upgrade ibrowse dependency for R18 support [\#15](https://github.com/danielberkompas/ex_twilio/pull/15) ([nickcampbell18](https://github.com/nickcampbell18)) 339 | 340 | ## [v0.1.1](https://github.com/danielberkompas/ex_twilio/tree/v0.1.1) (2015-05-22) 341 | 342 | [Full Changelog](https://github.com/danielberkompas/ex_twilio/compare/v0.1.0...v0.1.1) 343 | 344 | ## [v0.1.0](https://github.com/danielberkompas/ex_twilio/tree/v0.1.0) (2015-04-11) 345 | 346 | [Full Changelog](https://github.com/danielberkompas/ex_twilio/compare/v0.0.1...v0.1.0) 347 | 348 | **Implemented enhancements:** 349 | 350 | - Support all Twilio API endpoints [\#9](https://github.com/danielberkompas/ex_twilio/issues/9) 351 | - Simplify URL generation logic [\#4](https://github.com/danielberkompas/ex_twilio/issues/4) 352 | 353 | **Closed issues:** 354 | 355 | - Create tests for all Resource modules [\#3](https://github.com/danielberkompas/ex_twilio/issues/3) 356 | - Add list of supported endpoints to README [\#2](https://github.com/danielberkompas/ex_twilio/issues/2) 357 | 358 | **Merged pull requests:** 359 | 360 | - Finish documenting and tests for now [\#12](https://github.com/danielberkompas/ex_twilio/pull/12) ([danielberkompas](https://github.com/danielberkompas)) 361 | - Support All Twilio REST Endpoints [\#11](https://github.com/danielberkompas/ex_twilio/pull/11) ([danielberkompas](https://github.com/danielberkompas)) 362 | - Reduce reliance on Enum [\#8](https://github.com/danielberkompas/ex_twilio/pull/8) ([danielberkompas](https://github.com/danielberkompas)) 363 | - \[\#4\] Rework URL generation [\#7](https://github.com/danielberkompas/ex_twilio/pull/7) ([danielberkompas](https://github.com/danielberkompas)) 364 | - Run Dialyzer on Travis CI [\#6](https://github.com/danielberkompas/ex_twilio/pull/6) ([danielberkompas](https://github.com/danielberkompas)) 365 | 366 | ## [v0.0.1](https://github.com/danielberkompas/ex_twilio/tree/v0.0.1) (2015-03-31) 367 | 368 | [Full Changelog](https://github.com/danielberkompas/ex_twilio/compare/2f7ff5b721700c53eb86c5b83c63dd655d042f31...v0.0.1) 369 | 370 | **Closed issues:** 371 | 372 | - Update all inline documentation [\#1](https://github.com/danielberkompas/ex_twilio/issues/1) 373 | 374 | **Merged pull requests:** 375 | 376 | - Improve documentation and typespecs for all modules [\#5](https://github.com/danielberkompas/ex_twilio/pull/5) ([danielberkompas](https://github.com/danielberkompas)) 377 | 378 | 379 | 380 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* 381 | --------------------------------------------------------------------------------