diff --git a/examples/conference_bridge.rb b/examples/conference_bridge.rb new file mode 100644 index 00000000..6b26146f --- /dev/null +++ b/examples/conference_bridge.rb @@ -0,0 +1,108 @@ +require 'rubygems' +require 'plivo' + +include Plivo + +AUTH_ID = 'AUTH_ID' +AUTH_TOKEN = 'AUTH_TOKEN' + +client = Phlo.new(AUTH_ID, AUTH_TOKEN) + +# if credentials are stored in the PLIVO_AUTH_ID and the PLIVO_AUTH_TOKEN environment variables +# then initialize client as: +# client = Phlo.new +# + +# provide the phlo_id in params +begin + phlo = client.phlo.get('11a5f5cf-b6cd-419b-8ada-e41ef073de74') + conference_bridge = phlo.conference_bridge('b24d49ea-fb62-4612-9d98-4565c67f0bdc') + puts conference_bridge +rescue PlivoRESTError => e + puts 'Exception: ' + e.message +end + +# Response: +# { +# "api_id"=>"1bcee5a2-f2db-47c3-b424-49f09bc9c62a", +# "node_id"=>"b24d49ea-fb62-4612-9d98-4565c67f0bdc", +# "phlo_id"=>"11a5f5cf-b6cd-419b-8ada-e41ef073de74", +# "name"=>"Conference_1", +# "node_type"=>"conference", +# "created_on"=>"2018-12-04 13:51:22.796041+00:00" +# } + + + +# 1. member leaves the call [HANGUP]: +# +# member_address => phone number of the member +# +# conference_bridge.member().hangup + +begin + response = conference_bridge.member('0000000000').hangup + puts response +rescue PlivoRESTError => e + puts 'Exception: ' + e.message +end + +# Response: +# { +# "api_id"=>"1bcee5a2-f2db-47c3-b424-49f09bc9c62a", +# "node_id"=>"b24d49ea-fb62-4612-9d98-4565c67f0bdc", +# "phlo_id"=>"11a5f5cf-b6cd-419b-8ada-e41ef073de74", +# "member_address"=>"0000000000", +# "node_type"=>"conference", +# "created_on"=>"2018-12-04 13:51:22.796041+00:00" +# } + + + +# 2. Mute a member in the conference bridge: +# +# member_address => phone number of the member +# +# conference_bridge.member().mute + +begin + response = conference_bridge.member('0000000000').mute + puts response +rescue PlivoRESTError => e + puts 'Exception: ' + e.message +end + +# Response: +# { +# "api_id"=>"1bcee5a2-f2db-47c3-b424-49f09bc9c62a", +# "node_id"=>"b24d49ea-fb62-4612-9d98-4565c67f0bdc", +# "phlo_id"=>"11a5f5cf-b6cd-419b-8ada-e41ef073de74", +# "member_address"=>"0000000000", +# "node_type"=>"conference", +# "created_on"=>"2018-12-04 13:51:22.796041+00:00" +# } + + + +# 3. Unmute a member in the conference bridge: +# +# member_address => phone number of the member +# +# conference_bridge.member().unmute + +begin + response = conference_bridge.member('0000000000').unmute + puts response +rescue PlivoRESTError => e + puts 'Exception: ' + e.message +end + +# Response: +# { +# "api_id"=>"1bcee5a2-f2db-47c3-b424-49f09bc9c62a", +# "node_id"=>"b24d49ea-fb62-4612-9d98-4565c67f0bdc", +# "phlo_id"=>"11a5f5cf-b6cd-419b-8ada-e41ef073de74", +# "member_address"=>"0000000000", +# "node_type"=>"conference", +# "created_on"=>"2018-12-04 13:51:22.796041+00:00" +# } \ No newline at end of file diff --git a/examples/multi_party_call.rb b/examples/multi_party_call.rb new file mode 100644 index 00000000..ebcffa5a --- /dev/null +++ b/examples/multi_party_call.rb @@ -0,0 +1,295 @@ +require 'rubygems' +require 'plivo' + +include Plivo + +AUTH_ID = 'AUTH_ID' +AUTH_TOKEN = 'AUTH_TOKEN' + +client = Phlo.new(AUTH_ID, AUTH_TOKEN) + +# if credentials are stored in the PLIVO_AUTH_ID and the PLIVO_AUTH_TOKEN environment variables +# then initialize client as: +# client = Phlo.new + +# provide the phlo_id in params +phlo = client.phlo.get('phlo_id') + +# provide multi_party_call node id in params: +begin + multi_party_call = phlo.multi_party_call('node_id') + puts multi_party_call +rescue PlivoRESTError => e + puts 'Exception: ' + e.message +end + +# Response: +# { +# "api_id": "36989807-a76f-4555-84d1-9dfdccca7a80", +# "node_id": "36989807-a76f-4555-84d1-9dfdccca7a80", +# "name": "Multi-Party Call_1", +# "node_type": "multi_party_call", +# "phlo_id": 'e564a84a-7910-4447-b16f-65c541dd552c', +# "created_on": '2018-11-03 19:32:33.240504+00:00' +# } +#------------------------------------------------------------------------ + +# 1. Agent makes outbound call to customer: +# +# 'trigger_source' => Caller Id to be set when an outbound call is made to the users to be added to the multi-party call +# 'to' => 'List of phone numbers and endpoints to which an outbound call should be initiated' +# 'role' => 'customer'/'agent'/'supervisor' +# multi_party_call.call(, , ) + +begin + response = multi_party_call.call('9999999999', %w(0000000000 8888888888), 'customer') + puts response +rescue PlivoRESTError => e + puts 'Exception: ' + e.message +end + +# Response: +# { +# "api_id": "36989807-a76f-4555-84d1-9dfdccca7a80", +# "node_id": "36989807-a76f-4555-84d1-9dfdccca7a80", +# "name": "Multi-Party Call_1", +# "node_type": "multi_party_call", +# "phlo_id": 'e564a84a-7910-4447-b16f-65c541dd552c', +# "created_on": '2018-11-03 19:32:33.240504+00:00' +# } + + +# 2. Agent initiates warm transfer: +# +# 'trigger_source' => number of the agent initiating warm transfer +# 'to' => number of another agent to be added to the multi-party call +# +# multi_party_call.warm_transfer(, ) + +begin + response = multi_party_call.warm_transfer('9999999999', '0000000000') + puts response +rescue PlivoRESTError => e + puts 'Exception: ' + e.message +end + +# Response: +# { +# "api_id": "36989807-a76f-4555-84d1-9dfdccca7a80", +# "node_id": "36989807-a76f-4555-84d1-9dfdccca7a80", +# "name": "Multi-Party Call_1", +# "node_type": "multi_party_call", +# "phlo_id": 'e564a84a-7910-4447-b16f-65c541dd552c', +# "created_on": '2018-11-03 19:32:33.240504+00:00' +# } + + +# 3. Agent initiates cold transfer: +# +# 'trigger_source' => number of the agent initiating cold transfer +# 'to' => number of another agent to be added to the multi-party call +# +# multi_party_call.cold_transfer(, ) + +begin + response = multi_party_call.cold_transfer('9999999999', '0000000000') + puts response +rescue PlivoRESTError => e + puts 'Exception: ' + e.message +end + +# Response: +# { +# "api_id": "36989807-a76f-4555-84d1-9dfdccca7a80", +# "node_id": "36989807-a76f-4555-84d1-9dfdccca7a80", +# "name": "Multi-Party Call_1", +# "node_type": "multi_party_call", +# "phlo_id": 'e564a84a-7910-4447-b16f-65c541dd552c', +# "created_on": '2018-11-03 19:32:33.240504+00:00' +# } + + +# 4. Agent abort transfer: +# +# agent_address => number of the agent +# +# multi_party_call.member().abort_transfer + + +begin + response = multi_party_call.member('0000000000').abort_transfer + puts response +rescue PlivoRESTError => e + puts 'Exception: ' + e.message +end + +# Response: +# { +# "api_id": "36989807-a76f-4555-84d1-9dfdccca7a80", +# "node_id": "36989807-a76f-4555-84d1-9dfdccca7a80", +# "node_type": "multi_party_call", +# "phlo_id": 'e564a84a-7910-4447-b16f-65c541dd552c', +# "member_address": '0000000000', +# "created_on": '2018-11-03 19:32:33.240504+00:00' +# } + + +# 5. Agent places customer on hold: +# +# member_address => phone number of the member who is being put on hold +# +# multi_party_call.member().hold + +begin + response = multi_party_call.member('9999999999').hold + puts response +rescue PlivoRESTError => e + puts 'Exception: ' + e.message +end + +# Response: +# { +# "api_id": "36989807-a76f-4555-84d1-9dfdccca7a80", +# "node_id": "36989807-a76f-4555-84d1-9dfdccca7a80", +# "node_type": "multi_party_call", +# "phlo_id": 'e564a84a-7910-4447-b16f-65c541dd552c', +# "member_address": '9999999999', +# "created_on": '2018-11-03 19:32:33.240504+00:00' +# } + + +# 6. Resume call after hold: +# +# member_address => phone number of the member +# +# multi_party_call.member().unhold + +begin + response = multi_party_call.member('9999999999').unhold + puts response +rescue PlivoRESTError => e + puts 'Exception: ' + e.message +end + + +# Response: +# { +# "api_id": "36989807-a76f-4555-84d1-9dfdccca7a80", +# "node_id": "36989807-a76f-4555-84d1-9dfdccca7a80", +# "node_type": "multi_party_call", +# "phlo_id": 'e564a84a-7910-4447-b16f-65c541dd552c', +# "member_address": '9999999999', +# "created_on": '2018-11-03 19:32:33.240504+00:00' +# } + + +# 7. Agent initiates voicemail drop: +# +# member_address => customer's number/endpoint +# +# multi_party_call.member().voicemail_drop + +begin + response = multi_party_call.member('9999999999').voicemail_drop + puts response +rescue PlivoRESTError => e + puts 'Exception: ' + e.message +end + + +# Response: +# { +# "api_id": "36989807-a76f-4555-84d1-9dfdccca7a80", +# "node_id": "36989807-a76f-4555-84d1-9dfdccca7a80", +# "node_type": "multi_party_call", +# "phlo_id": 'e564a84a-7910-4447-b16f-65c541dd552c', +# "member_address": '9999999999', +# "created_on": '2018-11-03 19:32:33.240504+00:00' +# } + + +# 8. Rejoin call on warm transfer: +# +# agent_address => number of the agent to be added to the original conference on completion of call between agents +# +# multi_party_call.member().resume_call + +begin + response = multi_party_call.member('0000000000').resume_call + puts response +rescue PlivoRESTError => e + puts 'Exception: ' + e.message +end + + +# Response: +# { +# "api_id": "36989807-a76f-4555-84d1-9dfdccca7a80", +# "node_id": "36989807-a76f-4555-84d1-9dfdccca7a80", +# "node_type": "multi_party_call", +# "phlo_id": 'e564a84a-7910-4447-b16f-65c541dd552c', +# "member_address": '9999999999', +# "created_on": '2018-11-03 19:32:33.240504+00:00' +# } + + +# 9. Agent leaves of the call [HANGUP]: +# +# agent_address => phone number of the agent +# +# multi_party_call.member().hangup + +begin + response = multi_party_call.member('0000000000').hangup + puts response +rescue PlivoRESTError => e + puts 'Exception: ' + e.message +end + + +# Response: +# { +# "api_id": "36989807-a76f-4555-84d1-9dfdccca7a80", +# "node_id": "36989807-a76f-4555-84d1-9dfdccca7a80", +# "node_type": "multi_party_call", +# "phlo_id": 'e564a84a-7910-4447-b16f-65c541dd552c', +# "member_address": '9999999999', +# "created_on": '2018-11-03 19:32:33.240504+00:00' +# } + + +# 10. Customer is removed from the conference call [HANGUP]: +# +# customer_address => phone number of the customer +# +# multi_party_call.member().hangup + +# Response: +# { +# "api_id": "36989807-a76f-4555-84d1-9dfdccca7a80", +# "node_id": "36989807-a76f-4555-84d1-9dfdccca7a80", +# "node_type": "multi_party_call", +# "phlo_id": 'e564a84a-7910-4447-b16f-65c541dd552c', +# "member_address": '9999999999', +# "created_on": '2018-11-03 19:32:33.240504+00:00' +# } + + +# 11. Remove a member from the multi-party call: +# +# member_address => phone number of the member +# +# multi_party_call.member().remove + +begin + response = multi_party_call.member('9999999999').remove + puts response +rescue PlivoRESTError => e + puts 'Exception: ' + e.message +end + +# Response: +# { +# "api_id": "36989807-a76f-4555-84d1-9dfdccca7a80", +# "id": "9999999999" +# } \ No newline at end of file diff --git a/examples/phlos.rb b/examples/phlos.rb new file mode 100644 index 00000000..4c1d0898 --- /dev/null +++ b/examples/phlos.rb @@ -0,0 +1,50 @@ +require 'rubygems' +require 'plivo' + +include Plivo + +AUTH_ID = 'AUTH_ID' +AUTH_TOKEN = 'AUTH_TOKEN' + +client = Phlo.new(AUTH_ID, AUTH_TOKEN) + +# if credentials are stored in the PLIVO_AUTH_ID and the PLIVO_AUTH_TOKEN environment variables +# then initialize client as: +# client = Phlo.new +# + +# provide the phlo_id in params +begin + phlo = client.phlo.get('phlo_id') + puts phlo +rescue PlivoRESTError => e + puts 'Exception: ' + e.message +end + + +# Sample Response: +# { +# "api_id": '36989807-a76f-4555-84d1-9dfdccca7a80', +# "phlo_id": 'e564a84a-7910-4447-b16f-65c541dd552c', +# "name": 'assignment_mpc', +# "created_on": '2018-11-03 19:32:33.240504+00:00' +# } + + +# initiate phlo via API request: +begin + response = phlo.run() + puts response +rescue PlivoRESTError => e + puts 'Exception: ' + e.message +end + + +# Sample Response: +# { +# :api_id=>"ff25223a-1c9f-11e4-80aa-12313f048015", +# :phlo_id=>"ff25223a-1c9f-11e4-80aa-12313f048015", +# :name=>"assignment_mpc", +# :created_on=>"2018-11-03 19:32:33.210714+00:00", +# :phlo_run_id=>"ff25223a-1c9f-11e4-80aa-12313f048015" +# } diff --git a/lib/plivo.rb b/lib/plivo.rb index f4092792..e1eb9714 100644 --- a/lib/plivo.rb +++ b/lib/plivo.rb @@ -5,5 +5,7 @@ require_relative 'plivo/resources' require_relative 'plivo/rest_client' require_relative 'plivo/xml' +require_relative 'plivo/phlo_client' +require_relative 'plivo/base_client' module Plivo; end diff --git a/lib/plivo/base.rb b/lib/plivo/base.rb index acb93e2f..ae7c3c37 100644 --- a/lib/plivo/base.rb +++ b/lib/plivo/base.rb @@ -5,5 +5,6 @@ module Plivo module Base PLIVO_API_URL = 'https://api.plivo.com'.freeze + PHLO_API_URL = 'https://phlorunner.plivo.com'.freeze end end diff --git a/lib/plivo/base/resource.rb b/lib/plivo/base/resource.rb index b5e25e1e..5f74a639 100644 --- a/lib/plivo/base/resource.rb +++ b/lib/plivo/base/resource.rb @@ -13,7 +13,7 @@ def initialize(client, options = nil) private def configure_client(client) - valid_param?(:client, client, RestClient, true) + valid_param?(:client, client, [RestClient, Phlo], true) @_client = client end diff --git a/lib/plivo/base/resource_interface.rb b/lib/plivo/base/resource_interface.rb index 6dfda7e2..55d295fd 100644 --- a/lib/plivo/base/resource_interface.rb +++ b/lib/plivo/base/resource_interface.rb @@ -11,14 +11,14 @@ def initialize(client, resource_list_json = nil) private def configure_client(client) - valid_param?(:client, client, RestClient, true) + valid_param?(:client, client, [RestClient, Phlo], true) @_client = client end def configure_resource_uri to_join = ['', 'v1', 'Account', @_client.auth_id, @_name, ''] to_join = ['', 'v1', 'Account', ''] if @_name == 'Account' - + to_join = ['', 'v1', @_name, ''] if @_name == 'phlo' @_resource_uri = to_join.join('/') end diff --git a/lib/plivo/base_client.rb b/lib/plivo/base_client.rb new file mode 100644 index 00000000..b0e0d3cb --- /dev/null +++ b/lib/plivo/base_client.rb @@ -0,0 +1,213 @@ +require 'json' +require 'faraday' +require 'faraday_middleware' + +require_relative 'exceptions' +require_relative 'utils' +require_relative 'resources' +require_relative 'base' + +module Plivo + # Core client, used for all API requests + include Utils + class BaseClient + # Base stuff + attr_reader :headers, :auth_credentials + + def initialize(auth_id = nil, auth_token = nil, proxy_options = nil, timeout=5) + configure_credentials(auth_id, auth_token) + configure_proxies(proxy_options) + configure_timeout(timeout) + configure_headers + configure_connection + end + + def auth_id + @auth_credentials[:auth_id] + end + + def process_response(method, response) + handle_response_exceptions(response) + if method == 'DELETE' + if response[:status] != 204 + raise Exceptions::PlivoRESTError, "Resource at #{response[:url]} "\ + 'couldn\'t be deleted' + end + elsif !([200, 201, 202].include? response[:status]) + raise Exceptions::PlivoRESTError, "Received #{response[:status]} for #{method}" + end + + response[:body] + end + + def send_request(resource_path, method = 'GET', data = {}, timeout = nil, use_multipart_conn = false) + timeout ||= @timeout + + response = case method + when 'GET' then send_get(resource_path, data, timeout) + when 'POST' then send_post(resource_path, data, timeout, use_multipart_conn) + when 'DELETE' then send_delete(resource_path, timeout) + else raise_invalid_request("#{method} not supported by Plivo, yet") + end + + process_response(method, response.to_hash) + end + + private + + def auth_token + @auth_credentials[:auth_token] + end + + def configure_credentials(auth_id, auth_token) + # Fetches and sets the right credentials + auth_id ||= ENV['PLIVO_AUTH_ID'] + auth_token ||= ENV['PLIVO_AUTH_TOKEN'] + + raise Exceptions::AuthenticationError, 'Couldn\'t find auth credentials' unless + auth_id && auth_token + + raise Exceptions::AuthenticationError, "Invalid auth_id: '#{auth_id}'" unless + Utils.valid_account?(auth_id) + + @auth_credentials = { + auth_id: auth_id, + auth_token: auth_token + } + end + + def configure_proxies(proxy_dict) + @proxy_hash = nil + return unless proxy_dict + + @proxy_hash = { + uri: "#{proxy_dict[:proxy_host]}:#{proxy_dict[:proxy_port]}", + user: proxy_dict[:proxy_user], + password: proxy_dict[:proxy_pass] + } + end + + def configure_timeout(timeout) + @timeout = timeout + end + + def user_agent + "plivo-ruby/#{Plivo::VERSION} (Ruby #{RUBY_VERSION})" + end + + def configure_headers + @headers = { + 'User-Agent' => user_agent, + 'Content-Type' => 'application/json', + 'Accept' => 'application/json' + } + end + + + def configure_connection + @conn = Faraday.new(@base_uri) do |faraday| + faraday.headers = @headers + + # DANGER: Basic auth should always come after headers, else + # The headers will replace the basic_auth + + faraday.basic_auth(auth_id, auth_token) + + faraday.proxy=@proxy_hash if @proxy_hash + faraday.response :json, content_type: /\bjson$/ + faraday.adapter Faraday.default_adapter + end + end + + def send_get(resource_path, data, timeout) + response = @conn.get do |req| + req.url resource_path, data + req.options.timeout = timeout if timeout + end + response + end + + def send_post(resource_path, data, timeout, use_multipart_conn) + if use_multipart_conn + multipart_conn = Faraday.new(@base_uri) do |faraday| + faraday.headers = { + 'User-Agent' => @headers['User-Agent'], + 'Accept' => @headers['Accept'] + } + + # DANGER: Basic auth should always come after headers, else + # The headers will replace the basic_auth + + faraday.request :multipart + faraday.request :url_encoded + faraday.basic_auth(auth_id, auth_token) + + faraday.proxy=@proxy_hash if @proxy_hash + faraday.response :json, content_type: /\bjson$/ + faraday.adapter Faraday.default_adapter + end + + response = multipart_conn.post do |req| + req.url resource_path + req.options.timeout = timeout if timeout + req.body = data + end + else + response = @conn.post do |req| + req.url resource_path + req.options.timeout = timeout if timeout + req.body = JSON.generate(data) if data + end + end + response + end + + def send_delete(resource_path, timeout) + response = @conn.delete do |req| + req.url resource_path + req.options.timeout = timeout if timeout + end + response + end + + def handle_response_exceptions(response) + exception_mapping = { + 400 => [ + Exceptions::ValidationError, + 'A parameter is missing or is invalid while accessing resource' + ], + 401 => [ + Exceptions::AuthenticationError, + 'Failed to authenticate while accessing resource' + ], + 404 => [ + Exceptions::ResourceNotFoundError, + 'Resource not found' + ], + 405 => [ + Exceptions::InvalidRequestError, + 'HTTP method used is not allowed to access resource' + ], + 500 => [ + Exceptions::PlivoServerError, + 'A server error occurred while accessing resource' + ] + } + + response_json = response[:body] + return unless exception_mapping.key? response[:status] + + exception_now = exception_mapping[response[:status]] + error_message = if (response_json.is_a? Hash) && (response_json.key? 'error') + response_json['error'] + else + exception_now[1] + " at: #{response[:url]}" + end + if error_message.is_a?(Hash) && error_message.key?('error') + error_message = error_message['error'] + end + + raise exception_now[0], error_message.to_s + end + end +end diff --git a/lib/plivo/phlo_client.rb b/lib/plivo/phlo_client.rb new file mode 100644 index 00000000..5d38ceea --- /dev/null +++ b/lib/plivo/phlo_client.rb @@ -0,0 +1,29 @@ +require_relative 'resources' +require_relative 'base_client' +require_relative 'base' + +module Plivo + + class Phlo < BaseClient + + # Resources + attr_reader :phlo + + def initialize(auth_id = nil, auth_token = nil, proxy_options = nil, timeout=5) + configure_base_uri + super + configure_interfaces + end + + private + + def configure_base_uri + @base_uri = Base::PHLO_API_URL + end + + def configure_interfaces + @phlo = Resources::PhloInterface.new(self) + end + + end +end diff --git a/lib/plivo/resources.rb b/lib/plivo/resources.rb index feeb08f1..f355cef0 100644 --- a/lib/plivo/resources.rb +++ b/lib/plivo/resources.rb @@ -9,6 +9,9 @@ require_relative 'resources/endpoints' require_relative 'resources/addresses' require_relative 'resources/identities' +require_relative 'resources/phlos' +require_relative 'resources/nodes' +require_relative 'resources/member' module Plivo module Resources diff --git a/lib/plivo/resources/member.rb b/lib/plivo/resources/member.rb new file mode 100644 index 00000000..f49f44f5 --- /dev/null +++ b/lib/plivo/resources/member.rb @@ -0,0 +1,64 @@ +module Plivo + module Resources + class Member < Base::Resource + def initialize(client, options) + @_name = 'member' + @_identifier_string = 'member_address' + super + configure_resource_uri + end + + def to_s + { + api_id: @api_id, + node_id: @node_id, + phlo_id: @phlo_id, + node_type: @node_type, + member_address: @member_address, + created_on: @created_on + }.to_s + end + + def hold + perform_update({action: 'hold'}) + end + + def unhold + perform_update({action: 'unhold'}) + end + + def voicemail_drop + perform_update({action: 'voicemail_drop'}) + end + + def resume_call + perform_update({action: 'resume_call'}) + end + + def hangup + perform_update({action: 'hangup'}) + end + + def remove + perform_delete + end + + def mute + perform_update({action: 'mute'}) + end + + def unmute + perform_update({action: 'unmute'}) + end + + def abort_transfer + perform_update({action: 'abort_transfer'}) + end + + private + def configure_resource_uri + @_resource_uri = ['', 'v1', 'phlo', @phlo_id, @node_type, @node_id, 'members', @id, ''].join('/') + end + end + end +end diff --git a/lib/plivo/resources/nodes.rb b/lib/plivo/resources/nodes.rb new file mode 100644 index 00000000..bcaa49a2 --- /dev/null +++ b/lib/plivo/resources/nodes.rb @@ -0,0 +1,83 @@ +module Plivo + module Resources + class NodeInterface < Base::ResourceInterface + def initialize(client, resource_list_json=nil) + super + end + + def getNode(node_id, node_type) + @_resource_uri = ['', 'v1', 'phlo', @_phlo_id, node_type, ''].join('/') + @_resource_type = configure_node_type(node_type) + perform_get(node_id) + end + + private + def configure_node_type(node_type) + case node_type + when 'multi_party_call' + MultiPartyCall + when 'conference_bridge' + ConferenceBridge + end + end + end + + class Node < Base::Resource + def initialize(client,options=nil) + @_identifier_string = 'node_id' + super + configure_resource_uri + end + + def to_s + { + api_id: @api_id, + node_id: @node_id, + phlo_id: @phlo_id, + name: @name, + node_type: @node_type, + created_on: @created_on + }.to_s + end + + def member(member_address) + options = {'member_address' => member_address, 'node_id' => @id, 'phlo_id' => @phlo_id, 'node_type' => @node_type} + Member.new(@_client, {resource_json: options}) + end + + private + def configure_resource_uri + @_resource_uri = ['', 'v1', 'phlo', @phlo_id, @node_type, @id, ''].join('/') + end + end + + class MultiPartyCall < Node + def initialize(client,options=nil) + @_name = 'multi_party_call' + super + end + + def call(trigger_source, to, role) + payload = {action: 'call', trigger_source: trigger_source, to: to, role: role} + perform_update(payload) + end + + def warm_transfer(trigger_source, to, role='agent') + payload = {action: 'warm_transfer', trigger_source: trigger_source, to: to, role: role} + perform_update(payload) + end + + def cold_transfer(trigger_source, to, role='agent') + payload = {action: 'cold_transfer', trigger_source: trigger_source, to: to, role: role} + perform_update(payload) + end + end + + class ConferenceBridge < Node + def initialize(client,options=nil) + @_name = 'conference_bridge' + super + end + end + end +end diff --git a/lib/plivo/resources/phlos.rb b/lib/plivo/resources/phlos.rb new file mode 100644 index 00000000..1d79b812 --- /dev/null +++ b/lib/plivo/resources/phlos.rb @@ -0,0 +1,55 @@ +module Plivo + module Resources + include Plivo::Utils + + class Phlo < Base::Resource + + def initialize(client, options = nil) + @_name = 'phlo' + @_identifier_string = 'phlo_id' + super + end + + def to_s + { + api_id: @api_id, + phlo_id: @phlo_id, + name: @name, + created_on: @created_on, + phlo_run_id: @phlo_run_id + }.to_s + end + + def multi_party_call(node_id) + nodeInterface = NodeInterface.new(@_client, {_phlo_id: @id}) + nodeInterface.getNode(node_id, 'multi_party_call') + end + + def conference_bridge(node_id) + nodeInterface = NodeInterface.new(@_client, {_phlo_id: @id}) + nodeInterface.getNode(node_id, 'conference_bridge') + end + + def run(params=nil) + @_resource_uri = ['', 'v1', 'account', @_client.auth_id, @_name, @id, ''].join('/') + perform_update(params) + end + end + + + class PhloInterface < Base::ResourceInterface + + def initialize(client, resource_list_json = nil) + @_name = 'phlo' + @_resource_type = Phlo + super + end + + def get(phlo_id) + perform_get(phlo_id) + end + + end + + end +end diff --git a/lib/plivo/rest_client.rb b/lib/plivo/rest_client.rb index bc176191..9d78e448 100644 --- a/lib/plivo/rest_client.rb +++ b/lib/plivo/rest_client.rb @@ -1,18 +1,9 @@ -require 'json' -require 'faraday' -require 'faraday_middleware' - -require_relative 'exceptions' -require_relative 'utils' require_relative 'resources' +require_relative 'base_client' require_relative 'base' module Plivo - # Core client, used for all API requests - include Utils - class RestClient - # Base stuff - attr_reader :headers, :auth_credentials + class RestClient < BaseClient # Resources attr_reader :messages, :account, :subaccounts, :recordings @@ -21,93 +12,15 @@ class RestClient attr_reader :addresses, :identities def initialize(auth_id = nil, auth_token = nil, proxy_options = nil, timeout=5) - configure_credentials(auth_id, auth_token) - configure_proxies(proxy_options) - configure_timeout(timeout) - configure_headers - configure_connection + configure_base_uri + super configure_interfaces end - def auth_id - @auth_credentials[:auth_id] - end - - def process_response(method, response) - handle_response_exceptions(response) - if method == 'DELETE' - if response[:status] != 204 - raise Exceptions::PlivoRESTError, "Resource at #{response[:url]} "\ - 'couldn\'t be deleted' - end - elsif !([200, 201, 202].include? response[:status]) - raise Exceptions::PlivoRESTError, "Received #{response[:status]} for #{method}" - end - - response[:body] - end - - def send_request(resource_path, method = 'GET', data = {}, timeout = nil, use_multipart_conn = false) - timeout ||= @timeout - - response = case method - when 'GET' then send_get(resource_path, data, timeout) - when 'POST' then send_post(resource_path, data, timeout, use_multipart_conn) - when 'DELETE' then send_delete(resource_path, data, timeout) - else raise_invalid_request("#{method} not supported by Plivo, yet") - end - - process_response(method, response.to_hash) - end - private - def auth_token - @auth_credentials[:auth_token] - end - - def configure_credentials(auth_id, auth_token) - # Fetches and sets the right credentials - auth_id ||= ENV['PLIVO_AUTH_ID'] - auth_token ||= ENV['PLIVO_AUTH_TOKEN'] - - raise Exceptions::AuthenticationError, 'Couldn\'t find auth credentials' unless - auth_id && auth_token - - raise Exceptions::AuthenticationError, "Invalid auth_id: '#{auth_id}'" unless - Utils.valid_account?(auth_id) - - @auth_credentials = { - auth_id: auth_id, - auth_token: auth_token - } - end - - def configure_proxies(proxy_dict) - @proxy_hash = nil - return unless proxy_dict - - @proxy_hash = { - uri: "#{proxy_dict[:proxy_host]}:#{proxy_dict[:proxy_port]}", - user: proxy_dict[:proxy_user], - password: proxy_dict[:proxy_pass] - } - end - - def configure_timeout(timeout) - @timeout = timeout - end - - def user_agent - "plivo-ruby/#{Plivo::VERSION} (Ruby #{RUBY_VERSION})" - end - - def configure_headers - @headers = { - 'User-Agent' => user_agent, - 'Content-Type' => 'application/json', - 'Accept' => 'application/json' - } + def configure_base_uri + @base_uri = Base::PLIVO_API_URL end def configure_interfaces @@ -125,112 +38,5 @@ def configure_interfaces @addresses = Resources::AddressInterface.new(self) @identities = Resources::IdentityInterface.new(self) end - - def configure_connection - @conn = Faraday.new(Base::PLIVO_API_URL) do |faraday| - faraday.headers = @headers - - # DANGER: Basic auth should always come after headers, else - # The headers will replace the basic_auth - - faraday.basic_auth(auth_id, auth_token) - - faraday.proxy=@proxy_hash if @proxy_hash - faraday.response :json, content_type: /\bjson$/ - faraday.adapter Faraday.default_adapter - end - end - - def send_get(resource_path, data, timeout) - response = @conn.get do |req| - req.url resource_path, data - req.options.timeout = timeout if timeout - end - response - end - - def send_post(resource_path, data, timeout, use_multipart_conn) - if use_multipart_conn - multipart_conn = Faraday.new(Base::PLIVO_API_URL) do |faraday| - faraday.headers = { - 'User-Agent' => @headers['User-Agent'], - 'Accept' => @headers['Accept'] - } - - # DANGER: Basic auth should always come after headers, else - # The headers will replace the basic_auth - - faraday.request :multipart - faraday.request :url_encoded - faraday.basic_auth(auth_id, auth_token) - - faraday.proxy=@proxy_hash if @proxy_hash - faraday.response :json, content_type: /\bjson$/ - faraday.adapter Faraday.default_adapter - end - - response = multipart_conn.post do |req| - req.url resource_path - req.options.timeout = timeout if timeout - req.body = data - end - else - response = @conn.post do |req| - req.url resource_path - req.options.timeout = timeout if timeout - req.body = JSON.generate(data) if data - end - end - response - end - - def send_delete(resource_path, data, timeout) - response = @conn.delete do |req| - req.url resource_path - req.options.timeout = timeout if timeout - req.body = JSON.generate(data) if data - end - response - end - - def handle_response_exceptions(response) - exception_mapping = { - 400 => [ - Exceptions::ValidationError, - 'A parameter is missing or is invalid while accessing resource' - ], - 401 => [ - Exceptions::AuthenticationError, - 'Failed to authenticate while accessing resource' - ], - 404 => [ - Exceptions::ResourceNotFoundError, - 'Resource not found' - ], - 405 => [ - Exceptions::InvalidRequestError, - 'HTTP method used is not allowed to access resource' - ], - 500 => [ - Exceptions::PlivoServerError, - 'A server error occurred while accessing resource' - ] - } - - response_json = response[:body] - return unless exception_mapping.key? response[:status] - - exception_now = exception_mapping[response[:status]] - error_message = if (response_json.is_a? Hash) && (response_json.key? 'error') - response_json['error'] - else - exception_now[1] + " at: #{response[:url]}" - end - if error_message.is_a?(Hash) && error_message.key?('error') - error_message = error_message['error'] - end - - raise exception_now[0], error_message.to_s - end end end diff --git a/spec/mocks/conferenceBridgeGetResponse.json b/spec/mocks/conferenceBridgeGetResponse.json new file mode 100644 index 00000000..3a8a5bb3 --- /dev/null +++ b/spec/mocks/conferenceBridgeGetResponse.json @@ -0,0 +1,8 @@ +{ + "api_id": "36989807-a76f-4555-84d1-9dfdccca7a80", + "node_id": "36989807-a76f-4555-84d1-9dfdccca7a80", + "phlo_id": "e564a84a-7910-4447-b16f-65c541dd552c", + "name": "Conference Bridge_1", + "node_type": "conference_bridge", + "created_on": "2018-11-03 19:32:33.240504+00:00" +} diff --git a/spec/mocks/memberActionResponse.json b/spec/mocks/memberActionResponse.json new file mode 100644 index 00000000..13c57eae --- /dev/null +++ b/spec/mocks/memberActionResponse.json @@ -0,0 +1,7 @@ +{ + "api_id": "36989807-a76f-4555-84d1-9dfdccca7a80", + "node_id": "36989807-a76f-4555-84d1-9dfdccca7a80", + "phlo_id": "e564a84a-7910-4447-b16f-65c541dd552c", + "node_type": "multi_party_call", + "member_address": "0000000000" +} diff --git a/spec/mocks/multiPartyCallActionResponse.json b/spec/mocks/multiPartyCallActionResponse.json new file mode 100644 index 00000000..cb01ab94 --- /dev/null +++ b/spec/mocks/multiPartyCallActionResponse.json @@ -0,0 +1,7 @@ +{ + "api_id": "36989807-a76f-4555-84d1-9dfdccca7a80", + "node_id": "36989807-a76f-4555-84d1-9dfdccca7a80", + "phlo_id": "e564a84a-7910-4447-b16f-65c541dd552c", + "name": "Multi-Party Call_1", + "node_type": "multi_party_call" +} diff --git a/spec/mocks/multiPartyCallGetResponse.json b/spec/mocks/multiPartyCallGetResponse.json new file mode 100644 index 00000000..468dfbfe --- /dev/null +++ b/spec/mocks/multiPartyCallGetResponse.json @@ -0,0 +1,8 @@ +{ + "api_id": "36989807-a76f-4555-84d1-9dfdccca7a80", + "node_id": "36989807-a76f-4555-84d1-9dfdccca7a80", + "phlo_id": "e564a84a-7910-4447-b16f-65c541dd552c", + "name": "Multi-Party Call_1", + "node_type": "multi_party_call", + "created_on": "2018-11-03 19:32:33.240504+00:00" +} diff --git a/spec/mocks/phloGetResponse.json b/spec/mocks/phloGetResponse.json new file mode 100644 index 00000000..1e79a8ab --- /dev/null +++ b/spec/mocks/phloGetResponse.json @@ -0,0 +1,6 @@ +{ + "api_id": "e564a84a-7910-4447-b16f-65c541dd552c", + "phlo_id": "e564a84a-7910-4447-b16f-65c541dd552c", + "name": "assignment_mpc", + "created_on": "2018-11-03 19:32:33.210714+00:00" +} diff --git a/spec/mocks/phloRunResponse.json b/spec/mocks/phloRunResponse.json new file mode 100644 index 00000000..58873ae0 --- /dev/null +++ b/spec/mocks/phloRunResponse.json @@ -0,0 +1,6 @@ +{ + "api_id": "ff25223a-1c9f-11e4-80aa-12313f048015", + "phlo_run_id": "ff25223a-1c9f-11e4-80aa-12313f048015", + "phlo_id": "e564a84a-7910-4447-b16f-65c541dd552c", + "resource_uri": "/Account/MAXXXXXXXXXXXXXXXXXX/Phlo/e564a84a-7910-4447-b16f-65c541dd552c/" +} diff --git a/spec/resource_nodes_spec.rb b/spec/resource_nodes_spec.rb new file mode 100644 index 00000000..d3cca5c2 --- /dev/null +++ b/spec/resource_nodes_spec.rb @@ -0,0 +1,214 @@ +require 'rspec' + +describe 'node tests' do + before :each do + contents = File.read(Dir.pwd + '/spec/mocks/phloGetResponse.json') + mock(200, JSON.parse(contents)) + @phlo = @phlo_client.phlo.get('e564a84a-7910-4447-b16f-65c541dd552c') + end + + describe 'multi party call tests' do + + before :each do + contents = File.read(Dir.pwd + '/spec/mocks/multiPartyCallGetResponse.json') + mock(200, JSON.parse(contents)) + @multi_party_call = @phlo.multi_party_call('36989807-a76f-4555-84d1-9dfdccca7a80') + end + + def to_json(node) + { + api_id: node.api_id, + phlo_id: node.phlo_id, + node_id: node.node_id, + node_type: node.node_type, + name: node.name, + }.to_json + end + + it 'agent makes outbound call to customer' do + contents = File.read(Dir.pwd + '/spec/mocks/multiPartyCallActionResponse.json') + mock(201, JSON.parse(contents)) + expect(JSON.parse(to_json(@multi_party_call.call('9090909090', '9090909090', 'customer')))) + .to eql(JSON.parse(contents)) + compare_requests(uri: '/v1/phlo/e564a84a-7910-4447-b16f-65c541dd552c/multi_party_call/36989807-a76f-4555-84d1-9dfdccca7a80/', + method: 'POST', + data: {:action=>"call", :trigger_source=>"9090909090", :to=>"9090909090", :role=>"customer"}) + + + end + + it 'agent initiates warm transfer' do + contents = File.read(Dir.pwd + '/spec/mocks/multiPartyCallActionResponse.json') + mock(201, JSON.parse(contents)) + expect(JSON.parse(to_json(@multi_party_call.warm_transfer('9090909090', '9090909090')))) + .to eql(JSON.parse(contents)) + compare_requests(uri: '/v1/phlo/e564a84a-7910-4447-b16f-65c541dd552c/multi_party_call/36989807-a76f-4555-84d1-9dfdccca7a80/', + method: 'POST', + data: {:action=>"warm_transfer", :trigger_source=>"9090909090", :to=>"9090909090", :role=>"agent"}) + + end + + it 'agent initiates cold transfer' do + contents = File.read(Dir.pwd + '/spec/mocks/multiPartyCallActionResponse.json') + mock(201, JSON.parse(contents)) + expect(JSON.parse(to_json(@multi_party_call.cold_transfer('9090909090', '9090909090', 'customer')))) + .to eql(JSON.parse(contents)) + compare_requests(uri: '/v1/phlo/e564a84a-7910-4447-b16f-65c541dd552c/multi_party_call/36989807-a76f-4555-84d1-9dfdccca7a80/', + method: 'POST', + data: {:action=>"cold_transfer", :trigger_source=>"9090909090", :to=>"9090909090", :role=>"customer"}) + + end + + end + + describe 'member tests: for multi party call node' do + before do + contents = File.read(Dir.pwd + '/spec/mocks/multiPartyCallGetResponse.json') + mock(200, JSON.parse(contents)) + @multi_party_call = @phlo.multi_party_call('36989807-a76f-4555-84d1-9dfdccca7a80') + @member = @multi_party_call.member('0000000000') + end + + def to_json(member) + { + api_id: member.api_id, + phlo_id: member.phlo_id, + node_id: member.node_id, + node_type: member.node_type, + member_address: member.member_address + }.to_json + end + + it 'Agent places Customer on hold' do + contents = File.read(Dir.pwd + '/spec/mocks/memberActionResponse.json') + mock(201, JSON.parse(contents)) + expect(JSON.parse(to_json(@member.hold))) + .to eql(JSON.parse(contents)) + compare_requests(uri: '/v1/phlo/e564a84a-7910-4447-b16f-65c541dd552c/multi_party_call/36989807-a76f-4555-84d1-9dfdccca7a80/members/0000000000/', + method: 'POST', + data: {:action=>"hold"}) + + end + + it 'resume call after hold' do + contents = File.read(Dir.pwd + '/spec/mocks/memberActionResponse.json') + mock(201, JSON.parse(contents)) + expect(JSON.parse(to_json(@member.unhold))) + .to eql(JSON.parse(contents)) + compare_requests(uri: '/v1/phlo/e564a84a-7910-4447-b16f-65c541dd552c/multi_party_call/36989807-a76f-4555-84d1-9dfdccca7a80/members/0000000000/', + method: 'POST', + data: {:action=>"unhold"}) + + end + + it 'agent initiates Voicemail Drop' do + contents = File.read(Dir.pwd + '/spec/mocks/memberActionResponse.json') + mock(201, JSON.parse(contents)) + expect(JSON.parse(to_json(@member.voicemail_drop))) + .to eql(JSON.parse(contents)) + compare_requests(uri: '/v1/phlo/e564a84a-7910-4447-b16f-65c541dd552c/multi_party_call/36989807-a76f-4555-84d1-9dfdccca7a80/members/0000000000/', + method: 'POST', + data: {:action=>"voicemail_drop"}) + + end + + it 'Rejoin call on warm transfer' do + contents = File.read(Dir.pwd + '/spec/mocks/memberActionResponse.json') + mock(201, JSON.parse(contents)) + expect(JSON.parse(to_json(@member.resume_call))) + .to eql(JSON.parse(contents)) + compare_requests(uri: '/v1/phlo/e564a84a-7910-4447-b16f-65c541dd552c/multi_party_call/36989807-a76f-4555-84d1-9dfdccca7a80/members/0000000000/', + method: 'POST', + data: {:action=>"resume_call"}) + + end + + it 'Agent 1 leaves of the call ' do + contents = File.read(Dir.pwd + '/spec/mocks/memberActionResponse.json') + mock(201, JSON.parse(contents)) + expect(JSON.parse(to_json(@member.hangup))) + .to eql(JSON.parse(contents)) + compare_requests(uri: '/v1/phlo/e564a84a-7910-4447-b16f-65c541dd552c/multi_party_call/36989807-a76f-4555-84d1-9dfdccca7a80/members/0000000000/', + method: 'POST', + data: {:action=>"hangup"}) + + end + + it 'agent abort transfer' do + contents = File.read(Dir.pwd + '/spec/mocks/memberActionResponse.json') + mock(201, JSON.parse(contents)) + expect(JSON.parse(to_json(@member.abort_transfer))) + .to eql(JSON.parse(contents)) + compare_requests(uri: '/v1/phlo/e564a84a-7910-4447-b16f-65c541dd552c/multi_party_call/36989807-a76f-4555-84d1-9dfdccca7a80/members/0000000000/', + method: 'POST', + data: {:action=>"abort_transfer"}) + + end + + it 'Remove a member from the Multi-Party Call' do + contents = File.read(Dir.pwd + '/spec/mocks/memberActionResponse.json') + mock(204, JSON.parse(contents)) + expect(JSON.parse(to_json(@member.remove))) + .to eql(JSON.parse(contents)) + compare_requests(uri: '/v1/phlo/e564a84a-7910-4447-b16f-65c541dd552c/multi_party_call/36989807-a76f-4555-84d1-9dfdccca7a80/members/0000000000/', + method: 'DELETE', + data: nil) + + end + + end + + describe 'member tests: for conference bridge node' do + before :each do + contents = File.read(Dir.pwd + '/spec/mocks/conferenceBridgeGetResponse.json') + mock(200, JSON.parse(contents)) + @conference_bridge = @phlo.conference_bridge('36989807-a76f-4555-84d1-9dfdccca7a80') + @member = @conference_bridge.member('0000000000') + end + + def to_json(member) + { + api_id: member.api_id, + phlo_id: member.phlo_id, + node_id: member.node_id, + node_type: member.node_type, + member_address: member.member_address + }.to_json + end + + describe 'update a member in the conference bridge' do + it 'mutes the member' do + contents = File.read(Dir.pwd + '/spec/mocks/memberActionResponse.json') + mock(201, JSON.parse(contents)) + expect(JSON.parse(to_json(@member.mute))) + .to eql(JSON.parse(contents)) + compare_requests(uri: '/v1/phlo/e564a84a-7910-4447-b16f-65c541dd552c/conference_bridge/36989807-a76f-4555-84d1-9dfdccca7a80/members/0000000000/', + method: 'POST', + data: {:action=>"mute"}) + + end + + it 'unmutes the member' do + contents = File.read(Dir.pwd + '/spec/mocks/memberActionResponse.json') + mock(201, JSON.parse(contents)) + expect(JSON.parse(to_json(@member.unmute))) + .to eql(JSON.parse(contents)) + compare_requests(uri: '/v1/phlo/e564a84a-7910-4447-b16f-65c541dd552c/conference_bridge/36989807-a76f-4555-84d1-9dfdccca7a80/members/0000000000/', + method: 'POST', + data: {:action=>"unmute"}) + + end + + it 'member leaves the call' do + contents = File.read(Dir.pwd + '/spec/mocks/memberActionResponse.json') + mock(201, JSON.parse(contents)) + expect(JSON.parse(to_json(@member.hangup))) + .to eql(JSON.parse(contents)) + compare_requests(uri: '/v1/phlo/e564a84a-7910-4447-b16f-65c541dd552c/conference_bridge/36989807-a76f-4555-84d1-9dfdccca7a80/members/0000000000/', + method: 'POST', + data: {:action=>"hangup"}) + + end + end + end +end diff --git a/spec/resource_phlos_spec.rb b/spec/resource_phlos_spec.rb new file mode 100644 index 00000000..59b6194e --- /dev/null +++ b/spec/resource_phlos_spec.rb @@ -0,0 +1,33 @@ +require 'rspec' + +describe 'phlos test' do + + before :each do + contents = File.read(Dir.pwd + '/spec/mocks/phloGetResponse.json') + mock(200, JSON.parse(contents)) + @phlo = @phlo_client.phlo.get('e564a84a-7910-4447-b16f-65c541dd552c') + end + + def to_json(phlo) + { + api_id: phlo.api_id, + resource_uri: phlo.resource_uri, + phlo_id: phlo.phlo_id, + phlo_run_id: phlo.phlo_run_id + }.to_json + end + + it 'initiates a phlo' do + contents = File.read(Dir.pwd + '/spec/mocks/phloRunResponse.json') + mock(200, JSON.parse(contents)) + expect(JSON.parse(to_json(@phlo.run()))) + .to eql(JSON.parse(contents)) + compare_requests(uri: '/v1/account/MAXXXXXXXXXXXXXXXXXX/phlo/'\ + 'e564a84a-7910-4447-b16f-65c541dd552c/', + method: 'POST', + data: nil) + + + end + +end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 27c6a53c..87f7e3bd 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -21,6 +21,18 @@ def @api.send_request(uri, method, data = nil, timeout=nil, use_multipart_conn = body: $response) end + @phlo_client = Plivo::Phlo.new('MAXXXXXXXXXXXXXXXXXX', 'MjEyOWU5MGVlM2NjZDY1ZTNmZTU2NjZhZGNjMTc5') + def @phlo_client.send_request(uri, method, data = nil, timeout=nil, use_multipart_conn = false) + $request = { + uri: uri, + method: method, + data: data + } + process_response(method, + status: $status, + body: $response) + end + def mock(status, response) $status = status $response = response