Skip to content
This repository was archived by the owner on Feb 10, 2021. It is now read-only.

Commit 001de83

Browse files
avbelDaniel Tolbert
authored andcommitted
Added Message API v2 support (#13)
* Added Message API v2 support * Added sample * Implemented missing apis (not tested) * Added search query serialization support * Fixed found issues * Added tests. fixed errors * Added demo to README
1 parent ca8e0f5 commit 001de83

6 files changed

Lines changed: 458 additions & 4 deletions

File tree

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,35 @@ Send some SMSes
102102
client = Bandwidth::Client.new(:user_id => "userId", :api_token => "token", :api_secret => "secret")
103103
statuses = Bandwidth::Message.create(client, [{:from => "+19195551212", :to => "+191955512142", :text => "Test"}, {:from => "+19195551212", :to => "+191955512143", :text => "Test2"}])
104104
```
105+
106+
Send SMS (v2)
107+
108+
```ruby
109+
auth_data = {user_name: 'user', password: 'password', account_id: 'accountId', subaccount_id: 'subaccountId'}
110+
111+
# Before sending sms you should have nessagin application on Bandwidth dashboard. You should create it by next call
112+
application = Bandwidth::V2::Message.create_messaging_application(auth_data, {
113+
:name => 'My messaging application',
114+
:callback_url => 'http://server/to/handle/messages/events',
115+
:location_name => 'current',
116+
:is_default_location => false,
117+
:sms_options => {:toll_free_enabled => true},
118+
:mms_options => {:enabled => true}
119+
})
120+
121+
# After that you should reserve 1 or some phone nubmers on Bandwidth Dashboard
122+
numbers = Message.search_and_order_numbers(auth_data, application) do |query|
123+
query.AreaCodeSearchAndOrderType do |b|
124+
b.AreaCode("910")
125+
b.Quantity(1)
126+
end
127+
end
128+
129+
# Now you can send messages from reserved numbers. Don't forget to pass :application_id
130+
message = Bandwidth::V2::Message.create({:from => numbers[0], :to => ["+191955512142"], :text => "Test", :application_id => application[:application_id]})
131+
```
132+
133+
105134
Upload file
106135

107136
```ruby

lib/bandwidth/client.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ def initialize (user_id = nil, api_token = nil, api_secret = nil, api_endpoint =
3838
end
3939
raise Errors::MissingCredentialsError.new() if (user_id || '').length == 0 || (api_token || '').length == 0 || (api_secret || '').length == 0
4040
@concat_user_path = lambda {|path| "/users/#{user_id}" + (if path[0] == "/" then path else "/#{path}" end) }
41-
@build_path = lambda {|path| "/#{api_version}" + (if path[0] == "/" then path else "/#{path}" end) }
4241
@set_adapter = lambda {|faraday| faraday.adapter(Faraday.default_adapter)}
4342
@create_connection = lambda{||
4443
Faraday.new(api_endpoint) { |faraday|
@@ -83,15 +82,16 @@ def Client.get_id_from_location_header(location)
8382
# @param path [String] path of url (exclude api verion and endpoint) to make call
8483
# @param data [Hash] data which will be sent with request (for :get and :delete request they will be sent with query in url)
8584
# @return [Array] array with 2 elements: parsed json data of response and response headers
86-
def make_request(method, path, data = {})
85+
def make_request(method, path, data = {}, api_version = 'v1')
8786
d = camelcase(data)
87+
build_path = lambda {|path| "/#{api_version}" + (if path[0] == "/" then path else "/#{path}" end) }
8888
connection = @create_connection.call()
8989
response = if method == :get || method == :delete
90-
connection.run_request(method, @build_path.call(path), nil, nil) do |req|
90+
connection.run_request(method, build_path.call(path), nil, nil) do |req|
9191
req.params = d unless d == nil || d.empty?
9292
end
9393
else
94-
connection.run_request(method, @build_path.call(path), d.to_json(), {'Content-Type' => 'application/json'})
94+
connection.run_request(method, build_path.call(path), d.to_json(), {'Content-Type' => 'application/json'})
9595
end
9696
check_response(response)
9797
r = if response.body.strip().size > 0 then symbolize(JSON.parse(response.body)) else {} end

lib/bandwidth/errors.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,34 @@ def initialize code, message
1111
end
1212
end
1313

14+
# Dashboard error class
15+
class GenericIrisError < StandardError
16+
# @return [String] Error code
17+
attr_reader :code
18+
19+
# @return [String] Http status code
20+
attr_reader :http_status
21+
22+
# @api private
23+
def initialize code, message, http_status
24+
super message
25+
@code = code
26+
@http_status = http_status
27+
end
28+
end
29+
30+
# Agregate error class
31+
class AgregateError < StandardError
32+
# @return [Array] errors
33+
attr_reader :errors
34+
35+
# @api private
36+
def initialize errorss
37+
super "Multiple errors"
38+
@errors = errors
39+
end
40+
end
41+
1442
# Missing Credentials error class
1543
class MissingCredentialsError < StandardError
1644

lib/bandwidth/v2/message.rb

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
require 'timeout'
2+
require 'active_support/xml_mini'
3+
4+
module Bandwidth
5+
module V2
6+
MESSAGE_PATH = 'messages'
7+
# The Messages resource lets you send SMS text messages and view messages that were previously sent or received.
8+
class Message
9+
extend ClientWrapper
10+
# Send text messages
11+
# @param client [Client] optional client instance to make requests
12+
# @param data [Hash] options of new message or list of messages
13+
# @return [Hash] created message or statuses of list of messages
14+
# @example
15+
# message = Message.create(client, {:from=>"from", :to=>["to"], :text=>"text", :application_id=>"messagingApplicationId"})
16+
def self.create(client, data)
17+
client.make_request(:post, client.concat_user_path(MESSAGE_PATH), data, 'v2')[0]
18+
end
19+
wrap_client_arg :create
20+
21+
# Create messaging application
22+
# @param auth_data [Hash] bandwidth dashboard auth data
23+
# @param data [Hash] options to create a messaging application
24+
# @return [Hash] created application data
25+
# @example
26+
# application = Message.create_messaging_application(auth_data, {
27+
# name: 'My messaging application',
28+
# callback_url: 'http://server/to/handle/messages/events',
29+
# location_name: 'current',
30+
# sms_options: {toll_free_enabled: true},
31+
# mms_options: {enabled: true}
32+
# })
33+
def self.create_messaging_application(auth_data, data)
34+
app = {
35+
:application_id => self.create_application(auth_data, data),
36+
:location_id => self.create_location(auth_data, data)
37+
}
38+
self.enable_sms(auth_data, data[:sms_options], app)
39+
self.enable_mms(auth_data, data[:mms_options], app)
40+
self.assign_application_to_location(auth_data, app)
41+
app
42+
end
43+
44+
# Look for and reserve phone numbers for messaging application
45+
# @param auth_data [Hash] bandwidth dashboard auth data
46+
# @param application [Hash] messaging application data
47+
# @param query [Hash] search parameters
48+
# @return [Array] list of reserved phone numbers
49+
# @example
50+
# numbers = Message.search_and_order_numbers(auth_data, application) do |query|
51+
# query.AreaCodeSearchAndOrderType do |b|
52+
# b.AreaCode("910")
53+
# b.Quantity(10)
54+
# end
55+
# end
56+
def self.search_and_order_numbers(auth_data, application, timeout = 60, &query_builder)
57+
builder = Builder::XmlMarkup.new()
58+
builder.Order do |b|
59+
query_builder.call(b) if query_builder
60+
b.SiteId(auth_data[:subaccount_id])
61+
b.PeerId(application[:location_id])
62+
end
63+
resp = self.make_iris_request(auth_data, :post, "/orders", builder.target!)
64+
order_id = self.find_first_descendant(resp[0], :id)
65+
success_statuses = ["COMPLETE", "PARTIAL"]
66+
Timeout::timeout(timeout || 60) do
67+
while true do
68+
sleep 0.5
69+
resp = self.make_iris_request(auth_data, :get, "/orders/#{order_id}")
70+
status = self.find_first_descendant(resp[0], :order_status)
71+
numbers = self.find_first_descendant(resp[0], :completed_numbers)[:telephone_number]
72+
numbers = [numbers] unless numbers.is_a?(Array)
73+
return numbers.map {|n| n[:full_number]} if success_statuses.include?(status)
74+
end
75+
end
76+
end
77+
78+
private
79+
def self.create_application(auth_data, data)
80+
builder = Builder::XmlMarkup.new()
81+
builder.Application do |b|
82+
b.AppName(data[:name])
83+
b.CallbackUrl(data[:callback_url])
84+
b.CallBackCreds do |bb|
85+
if data[:callback_auth_data] then
86+
bb.UserId(data[:callback_auth_data][:user_name])
87+
bb.Password(data[:callback_auth_data][:password])
88+
end
89+
end
90+
end
91+
resp = self.make_iris_request(auth_data, :post, "/applications", builder.target!)
92+
self.find_first_descendant(resp[0], :application_id)
93+
end
94+
95+
def self.create_location(auth_data, data)
96+
builder = Builder::XmlMarkup.new()
97+
builder.SipPeer do |b|
98+
b.PeerName(data[:location_name])
99+
b.IsDefaultPeer(data[:is_default_location])
100+
end
101+
resp = self.make_iris_request(auth_data, :post, "/sites/#{auth_data[:subaccount_id]}/sippeers", builder.target!)
102+
(resp[1]["Location"] || "").split("/").last
103+
end
104+
105+
def self.enable_sms(auth_data, options, application)
106+
return if !options || options[:enabled] == false
107+
builder = Builder::XmlMarkup.new()
108+
builder.SipPeerSmsFeature do |b|
109+
b.SipPeerSmsFeatureSettings do |bb|
110+
bb.TollFree(options[:toll_free_enabled] || false)
111+
bb.ShortCode(options[:short_code_enabled] || false)
112+
bb.Protocol("HTTP")
113+
bb.Zone1(true)
114+
bb.Zone2(false)
115+
bb.Zone3(false)
116+
bb.Zone4(false)
117+
bb.Zone5(false)
118+
end
119+
b.HttpSettings do |bb|
120+
bb.ProxyPeerId("539692")
121+
end
122+
end
123+
self.make_iris_request(auth_data, :post, "/sites/#{auth_data[:subaccount_id]}/sippeers/#{application[:location_id]}/products/messaging/features/sms", builder.target!)
124+
end
125+
126+
def self.enable_mms(auth_data, options, application)
127+
return if !options || options[:enabled] == false
128+
builder = Builder::XmlMarkup.new()
129+
builder.MmsFeature do |b|
130+
b.MmsSettings do |bb|
131+
bb.protocol("HTTP")
132+
end
133+
b.Protocols do |bb|
134+
bb.HTTP do |bbb|
135+
bbb.HttpSettings do |bbbb|
136+
bbbb.ProxyPeerId("539692")
137+
end
138+
end
139+
end
140+
end
141+
self.make_iris_request(auth_data, :post, "/sites/#{auth_data[:subaccount_id]}/sippeers/#{application[:location_id]}/products/messaging/features/mms", builder.target!)
142+
end
143+
144+
def self.assign_application_to_location(auth_data, application)
145+
builder = Builder::XmlMarkup.new()
146+
builder.ApplicationsSettings do |b|
147+
b.HttpMessagingV2AppId(application[:application_id])
148+
end
149+
self.make_iris_request(auth_data, :put, "/sites/#{auth_data[:subaccount_id]}/sippeers/#{application[:location_id]}/products/messaging/applicationSettings", builder.target!)
150+
end
151+
152+
def self.create_iris_request(auth_data)
153+
Faraday.new("https://dashboard.bandwidth.com") { |faraday|
154+
faraday.basic_auth(auth_data[:user_name], auth_data[:password])
155+
faraday.headers['Accept'] = 'application/xml'
156+
faraday.headers['User-Agent'] = "ruby-bandwidth/v#{Bandwidth::VERSION}"
157+
if @@configure_connection
158+
@@configure_connection.call(faraday)
159+
else
160+
faraday.adapter(Faraday.default_adapter)
161+
end
162+
}
163+
end
164+
165+
def self.configure_connection(handler)
166+
@@configure_connection = handler
167+
end
168+
169+
def self.make_iris_request(auth_data, method, path, xml = nil)
170+
connection = self.create_iris_request(auth_data)
171+
full_path = "/api/accounts/#{auth_data[:account_id]}#{path}"
172+
response = connection.run_request(method, full_path, xml, if xml then {'Content-Type' => 'application/xml'} else nil end)
173+
body = self.check_response(response)
174+
[body || {}, response.headers || {}]
175+
end
176+
177+
def self.check_response(response)
178+
doc = ActiveSupport::XmlMini.parse(response.body || '')
179+
parsed_body = self.process_parsed_doc(doc.values.first)
180+
code = self.find_first_descendant(parsed_body, :error_code)
181+
description = self.find_first_descendant(parsed_body, :description)
182+
unless code
183+
error = self.find_first_descendant(parsed_body, :error)
184+
if error
185+
code = error[:code]
186+
description = error[:description]
187+
else
188+
errors = self.find_first_descendant(parsed_body, :errors)
189+
if errors == nil || errors.length == 0
190+
code = self.find_first_descendant(parsed_body, :result_code)
191+
description = self.find_first_descendant(parsed_body, :result_message)
192+
else
193+
errors = [errors] if errors.is_a?(Hash)
194+
raise Errors::AgregateError.new(errors.map {|e| Errors::GenericIrisError.new(e[:code], e[:description], response.status)})
195+
end
196+
end
197+
end
198+
raise Errors::GenericIrisError.new(code, description, response.status) if code && description && code != '0' && code != 0
199+
raise Errors::GenericIrisError.new('', "Http code #{response.status}", response.status) if response.status >= 400
200+
parsed_body
201+
end
202+
203+
def self.find_first_descendant v, name
204+
result = nil
205+
case
206+
when v.is_a?(Array)
207+
v.each do |val|
208+
result = self.find_first_descendant(val, name)
209+
break if result
210+
end
211+
when v.is_a?(Hash)
212+
v.each do |k, val|
213+
if k == name
214+
result = val
215+
break
216+
else
217+
result = self.find_first_descendant(val, name)
218+
break if result
219+
end
220+
end
221+
end
222+
result
223+
end
224+
225+
def self.process_parsed_doc(v)
226+
case
227+
when v.is_a?(Array)
228+
v.map {|i| self.process_parsed_doc(i)}
229+
when v.is_a?(Hash)
230+
return self.process_parsed_doc(v['__content__']) if v.keys.length == 1 && v['__content__']
231+
result = {}
232+
v.each do |k, val|
233+
key = if k.downcase() == 'lata' then :lata else k.underscore().to_sym() end
234+
result[key] = self.process_parsed_doc(val)
235+
end
236+
result
237+
when v == "true" || v == "false"
238+
v == "true"
239+
when /^\d{4}\-\d{2}-\d{2}T\d{2}\:\d{2}\:\d{2}(\.\d{3})?Z$/.match(v)
240+
DateTime.iso8601(v)
241+
when /\A\d{9}\d?\Z/.match(v)
242+
v
243+
when /\A[1-9]\d*\Z/.match(v)
244+
Integer(v)
245+
when /\A[-+]?[0-9]*\.?[0-9]+\Z/.match(v)
246+
Float(v)
247+
else
248+
v
249+
end
250+
end
251+
end
252+
end
253+
end

lib/ruby-bandwidth.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
require 'bandwidth/phone_number'
2121
require 'bandwidth/recording'
2222

23+
require 'bandwidth/v2/message'
2324

2425
require 'bandwidth/xml/response'
2526
require 'bandwidth/xml/xml_verb'

0 commit comments

Comments
 (0)