cpaas-sdk 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +8 -5
- data/cpaas-sdk.gemspec +1 -1
- data/developer-notes.md +9 -0
- data/docs/Cpaas.html +68 -21
- data/docs/Cpaas/Conversation.html +43 -25
- data/docs/Cpaas/Notification.html +4 -4
- data/docs/Cpaas/Twofactor.html +50 -18
- data/docs/_index.html +1 -1
- data/docs/_index.md +13 -2
- data/docs/file._index.html +18 -3
- data/docs/index.html +18 -3
- data/docs/top-level-namespace.html +50 -46
- data/examples/2fa/.env.example +7 -6
- data/examples/2fa/.gitignore +159 -159
- data/examples/2fa/.ruby-gemset +1 -1
- data/examples/2fa/.ruby-version +1 -1
- data/examples/2fa/Gemfile +8 -8
- data/examples/2fa/README.md +36 -34
- data/examples/2fa/app.rb +145 -134
- data/examples/2fa/config.ru +10 -10
- data/examples/2fa/helper.rb +37 -37
- data/examples/2fa/public/stylesheets/forms.css +6 -0
- data/examples/2fa/views/alert.erb +4 -4
- data/examples/2fa/views/dashboard.erb +4 -4
- data/examples/2fa/views/index.erb +16 -16
- data/examples/2fa/views/login.erb +13 -13
- data/examples/2fa/views/verify.erb +18 -8
- data/lib/cpaas-sdk.rb +19 -6
- data/lib/cpaas-sdk/api.rb +21 -9
- data/lib/cpaas-sdk/config.rb +10 -0
- data/lib/cpaas-sdk/resources/conversation.rb +12 -11
- data/lib/cpaas-sdk/resources/notification.rb +1 -1
- data/lib/cpaas-sdk/resources/twofactor.rb +0 -1
- data/lib/cpaas-sdk/util.rb +9 -9
- data/lib/cpaas-sdk/version.rb +1 -1
- data/tutorials/2FA.md +6 -3
- data/tutorials/GetStarted.md +18 -3
- data/tutorials/SMSMessaging.md +16 -8
- metadata +7 -6
data/examples/2fa/config.ru
CHANGED
@@ -1,10 +1,10 @@
|
|
1
|
-
require 'dotenv'
|
2
|
-
require 'rubygems'
|
3
|
-
require 'bundler'
|
4
|
-
|
5
|
-
require './app'
|
6
|
-
|
7
|
-
Bundler.require
|
8
|
-
Dotenv.load
|
9
|
-
|
10
|
-
run App
|
1
|
+
require 'dotenv'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'bundler'
|
4
|
+
|
5
|
+
require './app'
|
6
|
+
|
7
|
+
Bundler.require
|
8
|
+
Dotenv.load
|
9
|
+
|
10
|
+
run App
|
data/examples/2fa/helper.rb
CHANGED
@@ -1,37 +1,37 @@
|
|
1
|
-
def valid_credentials?(params)
|
2
|
-
ENV['EMAIL'] == params['email'] && ENV['PASSWORD'] == params['password']
|
3
|
-
end
|
4
|
-
|
5
|
-
def error_message(error)
|
6
|
-
"#{error[:name]}: #{error[:message]} (#{error[:exception_id]})"
|
7
|
-
end
|
8
|
-
|
9
|
-
def set_credentials_verified(session)
|
10
|
-
session[:credentials_verified] = true
|
11
|
-
session[:code_verified] = false
|
12
|
-
end
|
13
|
-
|
14
|
-
def is_credentials_verified?(session)
|
15
|
-
session[:credentials_verified] && !session[:code_verified]
|
16
|
-
end
|
17
|
-
|
18
|
-
def is_logged_in?(session)
|
19
|
-
session[:credentials_verified] && session[:code_verified]
|
20
|
-
end
|
21
|
-
|
22
|
-
def logout(session)
|
23
|
-
session[:credentials_verified] = true
|
24
|
-
session[:code_verified] = false
|
25
|
-
session[:code_id] = nil
|
26
|
-
end
|
27
|
-
|
28
|
-
def set_default_state(session)
|
29
|
-
session[:credentials_verified] = false
|
30
|
-
session[:code_verified] = false
|
31
|
-
end
|
32
|
-
|
33
|
-
|
34
|
-
def login(session)
|
35
|
-
session[:credentials_verified] = true
|
36
|
-
session[:code_verified] = true
|
37
|
-
end
|
1
|
+
def valid_credentials?(params)
|
2
|
+
ENV['EMAIL'] == params['email'] && ENV['PASSWORD'] == params['password']
|
3
|
+
end
|
4
|
+
|
5
|
+
def error_message(error)
|
6
|
+
"#{error[:name]}: #{error[:message]} (#{error[:exception_id]})"
|
7
|
+
end
|
8
|
+
|
9
|
+
def set_credentials_verified(session)
|
10
|
+
session[:credentials_verified] = true
|
11
|
+
session[:code_verified] = false
|
12
|
+
end
|
13
|
+
|
14
|
+
def is_credentials_verified?(session)
|
15
|
+
session[:credentials_verified] && !session[:code_verified]
|
16
|
+
end
|
17
|
+
|
18
|
+
def is_logged_in?(session)
|
19
|
+
session[:credentials_verified] && session[:code_verified]
|
20
|
+
end
|
21
|
+
|
22
|
+
def logout(session)
|
23
|
+
session[:credentials_verified] = true
|
24
|
+
session[:code_verified] = false
|
25
|
+
session[:code_id] = nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def set_default_state(session)
|
29
|
+
session[:credentials_verified] = false
|
30
|
+
session[:code_verified] = false
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
def login(session)
|
35
|
+
session[:credentials_verified] = true
|
36
|
+
session[:code_verified] = true
|
37
|
+
end
|
@@ -1,5 +1,5 @@
|
|
1
|
-
<% if locals[:alert] %>
|
2
|
-
<div class="alert alert-<%= alert[:type] %>">
|
3
|
-
<p><%= alert[:message] %></p>
|
4
|
-
</div>
|
1
|
+
<% if locals[:alert] %>
|
2
|
+
<div class="alert alert-<%= alert[:type] %>">
|
3
|
+
<p><%= alert[:message] %></p>
|
4
|
+
</div>
|
5
5
|
<% end %>
|
@@ -1,4 +1,4 @@
|
|
1
|
-
<form action="/logout" method="get" class="box centered-box">
|
2
|
-
<p>You are now logged in.</p>
|
3
|
-
<button type="submit">Logout</button>
|
4
|
-
</form>
|
1
|
+
<form action="/logout" method="get" class="box centered-box">
|
2
|
+
<p>You are now logged in.</p>
|
3
|
+
<button type="submit">Logout</button>
|
4
|
+
</form>
|
@@ -1,17 +1,17 @@
|
|
1
|
-
<!DOCTYPE html>
|
2
|
-
<html lang="en">
|
3
|
-
<head>
|
4
|
-
<meta charset="UTF-8">
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6
|
-
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
7
|
-
<link rel="stylesheet" type="text/css" href="stylesheets/main.css">
|
8
|
-
<title>2FA</title>
|
9
|
-
</head>
|
10
|
-
<body>
|
11
|
-
<%= erb :alert, locals: locals %>
|
12
|
-
|
13
|
-
<div class="main">
|
14
|
-
<%= yield %>
|
15
|
-
</div>
|
16
|
-
</body>
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="UTF-8">
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6
|
+
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
7
|
+
<link rel="stylesheet" type="text/css" href="stylesheets/main.css">
|
8
|
+
<title>2FA</title>
|
9
|
+
</head>
|
10
|
+
<body>
|
11
|
+
<%= erb :alert, locals: locals %>
|
12
|
+
|
13
|
+
<div class="main">
|
14
|
+
<%= yield %>
|
15
|
+
</div>
|
16
|
+
</body>
|
17
17
|
</html>
|
@@ -1,13 +1,13 @@
|
|
1
|
-
<form action="/login" method="post" class="box centered-box">
|
2
|
-
<h2 class="text-center">Login</h2>
|
3
|
-
<div class="input-group">
|
4
|
-
<label for="email">Email</label>
|
5
|
-
<input type="text" id="email" name="email" />
|
6
|
-
</div>
|
7
|
-
<div class="input-group">
|
8
|
-
<label for="password">Password</label>
|
9
|
-
<input type="password" id="password" name="password" />
|
10
|
-
</div>
|
11
|
-
<button type="submit">Login</button>
|
12
|
-
</form>
|
13
|
-
|
1
|
+
<form action="/login" method="post" class="box centered-box">
|
2
|
+
<h2 class="text-center">Login</h2>
|
3
|
+
<div class="input-group">
|
4
|
+
<label for="email">Email</label>
|
5
|
+
<input type="text" id="email" name="email" />
|
6
|
+
</div>
|
7
|
+
<div class="input-group">
|
8
|
+
<label for="password">Password</label>
|
9
|
+
<input type="password" id="password" name="password" />
|
10
|
+
</div>
|
11
|
+
<button type="submit">Login</button>
|
12
|
+
</form>
|
13
|
+
|
@@ -1,8 +1,18 @@
|
|
1
|
-
<
|
2
|
-
<
|
3
|
-
|
4
|
-
<
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
</
|
1
|
+
<div class="box centered-box">
|
2
|
+
<form action="/verify" method="post">
|
3
|
+
<h2 class="text-center">Verify</h2>
|
4
|
+
<div class="input-group">
|
5
|
+
<label for="code">Verification code</label>
|
6
|
+
<input type="text" id="code" maxlength="6" name="code" />
|
7
|
+
<button type="submit">Verify</button>
|
8
|
+
</div>
|
9
|
+
</form>
|
10
|
+
<hr>
|
11
|
+
<form action="/sendtwofactor" method="post">
|
12
|
+
<div class="input-group:nowrap">
|
13
|
+
<input type="radio" name="otp" value="sms" checked/> 2FA via sms
|
14
|
+
<input type="radio" name="otp" value="email" /> 2FA via email
|
15
|
+
<button class='verify-button' type="submit">Send 2FA</button>
|
16
|
+
</div>
|
17
|
+
</form>
|
18
|
+
</div>
|
data/lib/cpaas-sdk.rb
CHANGED
@@ -11,20 +11,33 @@ module Cpaas
|
|
11
11
|
#
|
12
12
|
# Configure the SDK with client_id and client_secret.
|
13
13
|
#
|
14
|
-
# @param client_id [String] Private project
|
15
|
-
# @param
|
16
|
-
# @param
|
14
|
+
# @param client_id [String] Private project key / Account client ID. If Private project key is used then client_secret is mandatory. If account client ID is used then email and password are mandatory.
|
15
|
+
# @param base_url [String] URL of the server to be used.
|
16
|
+
# @param client_secret [String] +optional Private project secret
|
17
|
+
# @param email [String] +optional Account login email
|
18
|
+
# @param password [String] +optional Account login password
|
17
19
|
#
|
18
20
|
# @example
|
19
21
|
# Cpaas.configure do |config|
|
20
|
-
# config.client_id
|
21
|
-
# config.client_secret
|
22
|
-
# config.base_url
|
22
|
+
# config.client_id = '<private project key>'
|
23
|
+
# config.client_secret = '<private project secret>'
|
24
|
+
# config.base_url = 'https://$KANDYFQDN$'
|
23
25
|
# end
|
24
26
|
#
|
27
|
+
# # or
|
28
|
+
#
|
29
|
+
# Cpaas.configure do |config|
|
30
|
+
# config.client_id = '<account client ID>'
|
31
|
+
# config.email = '<account email>'
|
32
|
+
# config.password = '<account password>'
|
33
|
+
# config.base_url = 'https://$KANDYFQDN$'
|
34
|
+
# end
|
35
|
+
|
25
36
|
def self.configure
|
26
37
|
yield self.config = Cpaas::Config.new
|
27
38
|
|
39
|
+
config.validate
|
40
|
+
|
28
41
|
self.api = Cpaas::Api.new(config)
|
29
42
|
end
|
30
43
|
end
|
data/lib/cpaas-sdk/api.rb
CHANGED
@@ -12,12 +12,11 @@ module Cpaas
|
|
12
12
|
attr_accessor :user_id, :client_correlator
|
13
13
|
|
14
14
|
def initialize(config)
|
15
|
-
@
|
16
|
-
@client_secret = config.client_secret
|
15
|
+
@config = config
|
17
16
|
@id_token_parsed = nil
|
18
17
|
@access_token = nil
|
19
|
-
|
20
|
-
|
18
|
+
@user_id = nil
|
19
|
+
@client_correlator = "#{config.client_id}-ruby"
|
21
20
|
|
22
21
|
self.class.base_uri config.base_url
|
23
22
|
|
@@ -87,9 +86,7 @@ module Cpaas
|
|
87
86
|
def get_auth_token
|
88
87
|
options = {
|
89
88
|
body: {
|
90
|
-
|
91
|
-
client_id: @client_id,
|
92
|
-
client_secret: @client_secret,
|
89
|
+
client_id: @config.client_id,
|
93
90
|
scope: 'openid'
|
94
91
|
},
|
95
92
|
headers: {
|
@@ -97,6 +94,21 @@ module Cpaas
|
|
97
94
|
}
|
98
95
|
}
|
99
96
|
|
97
|
+
if !@config.client_secret.nil?
|
98
|
+
credentials = {
|
99
|
+
grant_type: 'client_credentials',
|
100
|
+
client_secret: @config.client_secret
|
101
|
+
}
|
102
|
+
else
|
103
|
+
credentials = {
|
104
|
+
grant_type: 'password',
|
105
|
+
username: @config.email,
|
106
|
+
password: @config.password
|
107
|
+
}
|
108
|
+
end
|
109
|
+
|
110
|
+
options[:body].merge!(credentials)
|
111
|
+
|
100
112
|
response = send_request('/cpaas/auth/v1/token', options, :post , false)
|
101
113
|
|
102
114
|
process_response(response, false)
|
@@ -116,13 +128,13 @@ module Cpaas
|
|
116
128
|
@access_token = nil
|
117
129
|
@id_token = nil
|
118
130
|
@id_token_parsed = nil
|
119
|
-
@
|
131
|
+
@user_id = nil
|
120
132
|
else
|
121
133
|
@access_token = tokens[:access_token]
|
122
134
|
@id_token = tokens[:id_token]
|
123
135
|
@id_token_parsed = JWT.decode(tokens[:id_token], nil, false).first
|
124
136
|
@token_parsed = JWT.decode(tokens[:access_token], nil, false).first
|
125
|
-
|
137
|
+
@user_id = @id_token_parsed['preferred_username']
|
126
138
|
end
|
127
139
|
end
|
128
140
|
|
data/lib/cpaas-sdk/config.rb
CHANGED
@@ -4,6 +4,16 @@ module Cpaas
|
|
4
4
|
class Config
|
5
5
|
attr_accessor :client_id
|
6
6
|
attr_accessor :client_secret
|
7
|
+
attr_accessor :email
|
8
|
+
attr_accessor :password
|
7
9
|
attr_accessor :base_url
|
10
|
+
|
11
|
+
def validate
|
12
|
+
raise ArgumentError.new('`client_id` cannot be nil') if client_id.nil?
|
13
|
+
|
14
|
+
raise ArgumentError.new('`clientSecret` or `email/password` cannot be nil') if client_secret.nil? && (email.nil? || password.nil?)
|
15
|
+
|
16
|
+
true
|
17
|
+
end
|
8
18
|
end
|
9
19
|
end
|
@@ -10,10 +10,10 @@ module Cpaas
|
|
10
10
|
# Send a new outbound message
|
11
11
|
#
|
12
12
|
# @param params [Hash]
|
13
|
-
# @option params [String] :type Type of conversation. Possible
|
13
|
+
# @option params [String] :type Type of conversation. Possible value(s) - 'sms'. Check Conversation.types for more options
|
14
14
|
# @option params [String] :sender_address Sender address information, basically the from address. E164 formatted DID number passed as a value, which is owned by the user. If the user wants to let CPaaS uses the default assigned DID number, this field can either has "default" value or the same value as the userId.
|
15
|
-
# @option params [Array[string]|String] :destination_address
|
16
|
-
# @option params [String] :message text message
|
15
|
+
# @option params [Array[string]|String] :destination_address Indicates which DID number(s) used as destination for this SMS.
|
16
|
+
# @option params [String] :message SMS text message
|
17
17
|
#
|
18
18
|
def self.create_message(params)
|
19
19
|
if params[:type] == types[:SMS]
|
@@ -48,7 +48,7 @@ module Cpaas
|
|
48
48
|
# Gets all messages.
|
49
49
|
#
|
50
50
|
# @param params [Hash]
|
51
|
-
# @option params [String] :type Type of conversation. Possible
|
51
|
+
# @option params [String] :type Type of conversation. Possible value(s) - 'sms'. Check Conversation.types for more options
|
52
52
|
# @option params [String] :remote_address +optional+ Remote address information while retrieving the conversation history, basically the destination telephone number that user exchanged message before. E164 formatted DID number passed as a value.
|
53
53
|
# @option params [String] :local_address +optional+ Local address information while retrieving the conversation history, basically the source telephone number that user exchanged message before.
|
54
54
|
# @option params [String] :query[:name] +optional+ - Performs search operation on firstName and lastName fields.
|
@@ -89,7 +89,7 @@ module Cpaas
|
|
89
89
|
# Delete conversation message
|
90
90
|
#
|
91
91
|
# @param params [Hash]
|
92
|
-
# @option params [String] :type Type of conversation. Possible
|
92
|
+
# @option params [String] :type Type of conversation. Possible value(s) - 'sms'. Check Conversation.types for more options
|
93
93
|
# @option params [String] :remote_address Remote address information while retrieving the conversation history, basically the destination telephone number that user exchanged message before. E164 formatted DID number passed as a value.
|
94
94
|
# @option params [String] :local_address Local address information while retrieving the conversation history, basically the source telephone number that user exchanged message before.
|
95
95
|
# @option params [String] :message_id +optional+ Identification of the message. If messeageId is not passsed then the conversation thread is deleted with all messages.
|
@@ -108,7 +108,7 @@ module Cpaas
|
|
108
108
|
# Read all messages in a thread
|
109
109
|
#
|
110
110
|
# @param params [Hash]
|
111
|
-
# @option params [String] :type Type of conversation. Possible
|
111
|
+
# @option params [String] :type Type of conversation. Possible value(s) - 'sms'. Check Conversation.types for more options
|
112
112
|
# @option params [String] :remote_address Remote address information while retrieving the conversation history, basically the destination telephone number that user exchanged message before. E164 formatted DID number passed as a value.
|
113
113
|
# @option params [String] :local_address Local address information while retrieving the conversation history, basically the source telephone number that user exchanged message before.
|
114
114
|
# @option params [String] :query[:next] +optional+ - Pointer for the next page to retrieve for the messages, provided by CPaaS in previous GET response.
|
@@ -135,7 +135,7 @@ module Cpaas
|
|
135
135
|
# Read a conversation message status
|
136
136
|
#
|
137
137
|
# @param params [Hash]
|
138
|
-
# @option params [String] :type Type of conversation. Possible
|
138
|
+
# @option params [String] :type Type of conversation. Possible value(s) - 'sms'. Check Conversation.types for more options
|
139
139
|
# @option params [String] :remote_address Remote address information while retrieving the conversation history, basically the destination telephone number that user exchanged message before. E164 formatted DID number passed as a value.
|
140
140
|
# @option params [String] :local_address Local address information while retrieving the conversation history, basically the source telephone number that user exchanged message before.
|
141
141
|
# @option params [String] :message_id Identification of the message. If messeageId is not passsed then the conversation thread is deleted with all messages.
|
@@ -149,7 +149,7 @@ module Cpaas
|
|
149
149
|
#
|
150
150
|
# Read all active subscriptions
|
151
151
|
#
|
152
|
-
# @option params [String] :type Type of conversation. Possible
|
152
|
+
# @option params [String] :type Type of conversation. Possible value(s) - 'sms'. Check Conversation.types for more options
|
153
153
|
#
|
154
154
|
|
155
155
|
def self.get_subscriptions(params)
|
@@ -173,7 +173,7 @@ module Cpaas
|
|
173
173
|
# Read active subscription
|
174
174
|
#
|
175
175
|
# @param params [Hash]
|
176
|
-
# @option params [String] :type Type of conversation. Possible
|
176
|
+
# @option params [String] :type Type of conversation. Possible value(s) - 'sms'. Check Conversation.types for more options
|
177
177
|
# @option params [String] :subscription_id Resource ID of the subscription
|
178
178
|
#
|
179
179
|
def self.get_subscription(params)
|
@@ -196,8 +196,8 @@ module Cpaas
|
|
196
196
|
# Create a new subscription
|
197
197
|
#
|
198
198
|
# @param params [Hash]
|
199
|
-
# @option params [String] :type Type of conversation. Possible
|
200
|
-
# @option params [String] :webhook_url
|
199
|
+
# @option params [String] :type Type of conversation. Possible value(s) - 'sms'. Check Conversation.types for more options
|
200
|
+
# @option params [String] :webhook_url HTTPS URL that is present in your application server which is accessible from the public web where the notifications should be sent to. Note: Should be a POST endpoint.
|
201
201
|
# @option params [String] :destination_address +optional+ The address that incoming messages are received for this subscription. If does not exist, CPaaS uses the default assigned DID number to subscribe against. It is suggested to provide the intended E164 formatted DID number within this parameter.
|
202
202
|
#
|
203
203
|
|
@@ -235,6 +235,7 @@ module Cpaas
|
|
235
235
|
# Unsubscription from conversation notification
|
236
236
|
#
|
237
237
|
# @param params [Hash]
|
238
|
+
# @option params [String] :type Type of conversation. Possible value(s) - 'sms'. Check Conversation.types for more options
|
238
239
|
# @option params [String] :subscription_id Resource ID of the subscription.
|
239
240
|
#
|
240
241
|
|
@@ -9,7 +9,7 @@ module Cpaas
|
|
9
9
|
# Parse inbound sms notification received in webhook. It parses the notification and returns
|
10
10
|
# simplified version of the response.
|
11
11
|
#
|
12
|
-
# @param notification [
|
12
|
+
# @param notification [JSON] JSON received in the subscription webhook.
|
13
13
|
#
|
14
14
|
def self.parse(notification)
|
15
15
|
parsed_notification = convert_hash_keys(notification)
|
@@ -3,7 +3,6 @@ require 'cpaas-sdk/util'
|
|
3
3
|
module Cpaas
|
4
4
|
##
|
5
5
|
# CPaaS provides Authentication API where a two-factor authentication (2FA) flow can be implemented by using that.
|
6
|
-
# Sections below describe two sample use cases, two-factor authentication via SMS and two-factor authentication via e-mail
|
7
6
|
#
|
8
7
|
|
9
8
|
class Twofactor
|
data/lib/cpaas-sdk/util.rb
CHANGED
@@ -22,26 +22,26 @@ def process_response(res, remove_outer_key = true)
|
|
22
22
|
response
|
23
23
|
end
|
24
24
|
|
25
|
-
def compose_error_from(
|
26
|
-
error_obj = deep_find(
|
25
|
+
def compose_error_from(response)
|
26
|
+
error_obj = deep_find(response, :message_id)
|
27
27
|
|
28
28
|
if (error_obj)
|
29
29
|
message = error_obj[:text]
|
30
30
|
|
31
31
|
error_obj[:variables].each_with_index { |variable, index| message.gsub!("%#{index + 1}", variable) }
|
32
32
|
|
33
|
-
|
33
|
+
return {
|
34
34
|
name: error_obj[:name],
|
35
35
|
exception_id: error_obj[:message_id],
|
36
36
|
message: message
|
37
37
|
}
|
38
|
-
else
|
39
|
-
response = {
|
40
|
-
name: err_response.keys.first,
|
41
|
-
exception_id: 'Unknown',
|
42
|
-
message: err_response[:message]
|
43
|
-
}
|
44
38
|
end
|
39
|
+
|
40
|
+
|
41
|
+
{
|
42
|
+
name: response[:error] || response.keys.first,
|
43
|
+
message: response[:error_description] || response[:message]
|
44
|
+
}
|
45
45
|
end
|
46
46
|
|
47
47
|
def id_from (url)
|