telnyx 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/.gitattributes +4 -0
  3. data/.github/ISSUE_TEMPLATE.md +5 -0
  4. data/.gitignore +9 -0
  5. data/.rubocop.yml +32 -0
  6. data/.rubocop_todo.yml +50 -0
  7. data/.travis.yml +42 -0
  8. data/CHANGELOG.md +2 -0
  9. data/CONTRIBUTORS +0 -0
  10. data/Gemfile +40 -0
  11. data/Guardfile +8 -0
  12. data/LICENSE +22 -0
  13. data/README.md +173 -0
  14. data/Rakefile +28 -0
  15. data/VERSION +1 -0
  16. data/bin/telnyx-console +16 -0
  17. data/lib/telnyx.rb +151 -0
  18. data/lib/telnyx/api_operations/create.rb +12 -0
  19. data/lib/telnyx/api_operations/delete.rb +13 -0
  20. data/lib/telnyx/api_operations/list.rb +29 -0
  21. data/lib/telnyx/api_operations/nested_resource.rb +63 -0
  22. data/lib/telnyx/api_operations/request.rb +57 -0
  23. data/lib/telnyx/api_operations/save.rb +103 -0
  24. data/lib/telnyx/api_resource.rb +69 -0
  25. data/lib/telnyx/available_phone_number.rb +9 -0
  26. data/lib/telnyx/errors.rb +166 -0
  27. data/lib/telnyx/event.rb +9 -0
  28. data/lib/telnyx/list_object.rb +155 -0
  29. data/lib/telnyx/message.rb +9 -0
  30. data/lib/telnyx/messaging_phone_number.rb +10 -0
  31. data/lib/telnyx/messaging_profile.rb +32 -0
  32. data/lib/telnyx/messaging_sender_id.rb +12 -0
  33. data/lib/telnyx/messaging_short_code.rb +10 -0
  34. data/lib/telnyx/number_order.rb +11 -0
  35. data/lib/telnyx/number_reservation.rb +11 -0
  36. data/lib/telnyx/public_key.rb +7 -0
  37. data/lib/telnyx/singleton_api_resource.rb +24 -0
  38. data/lib/telnyx/telnyx_client.rb +545 -0
  39. data/lib/telnyx/telnyx_object.rb +521 -0
  40. data/lib/telnyx/telnyx_response.rb +50 -0
  41. data/lib/telnyx/util.rb +328 -0
  42. data/lib/telnyx/version.rb +5 -0
  43. data/lib/telnyx/webhook.rb +66 -0
  44. data/telnyx.gemspec +25 -0
  45. data/test/api_stub_helpers.rb +1 -0
  46. data/test/openapi/README.md +9 -0
  47. data/test/telnyx/api_operations_test.rb +85 -0
  48. data/test/telnyx/api_resource_test.rb +293 -0
  49. data/test/telnyx/available_phone_number_test.rb +14 -0
  50. data/test/telnyx/errors_test.rb +23 -0
  51. data/test/telnyx/list_object_test.rb +244 -0
  52. data/test/telnyx/message_test.rb +19 -0
  53. data/test/telnyx/messaging_phone_number_test.rb +33 -0
  54. data/test/telnyx/messaging_profile_test.rb +70 -0
  55. data/test/telnyx/messaging_sender_id_test.rb +46 -0
  56. data/test/telnyx/messaging_short_code_test.rb +33 -0
  57. data/test/telnyx/number_order_test.rb +39 -0
  58. data/test/telnyx/number_reservation_test.rb +12 -0
  59. data/test/telnyx/public_key_test.rb +13 -0
  60. data/test/telnyx/telnyx_client_test.rb +631 -0
  61. data/test/telnyx/telnyx_object_test.rb +497 -0
  62. data/test/telnyx/telnyx_response_test.rb +49 -0
  63. data/test/telnyx/util_test.rb +380 -0
  64. data/test/telnyx/webhook_test.rb +108 -0
  65. data/test/telnyx_mock.rb +78 -0
  66. data/test/telnyx_test.rb +40 -0
  67. data/test/test_data.rb +149 -0
  68. data/test/test_helper.rb +73 -0
  69. metadata +162 -0
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Telnyx
4
+ class MessagingSenderId < APIResource
5
+ include Telnyx::APIOperations::Save
6
+ include Telnyx::APIOperations::Delete
7
+ extend Telnyx::APIOperations::List
8
+ extend Telnyx::APIOperations::Create
9
+
10
+ OBJECT_NAME = "messaging_sender_id".freeze
11
+ end
12
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Telnyx
4
+ class MessagingShortCode < APIResource
5
+ include Telnyx::APIOperations::Save
6
+ extend Telnyx::APIOperations::List
7
+
8
+ OBJECT_NAME = "messaging_short_code".freeze
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Telnyx
4
+ class NumberOrder < APIResource
5
+ extend Telnyx::APIOperations::Create
6
+ extend Telnyx::APIOperations::List
7
+ include Telnyx::APIOperations::Save
8
+
9
+ OBJECT_NAME = "number_order".freeze
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Telnyx
4
+ class NumberReservation < APIResource
5
+ extend Telnyx::APIOperations::List
6
+ extend Telnyx::APIOperations::Create
7
+ include Telnyx::APIOperations::Save
8
+
9
+ OBJECT_NAME = "number_reservation".freeze
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Telnyx
4
+ class PublicKey < SingletonAPIResource
5
+ OBJECT_NAME = "public_key".freeze
6
+ end
7
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Telnyx
4
+ class SingletonAPIResource < APIResource
5
+ def self.resource_url
6
+ if self == SingletonAPIResource
7
+ raise NotImplementedError, "SingletonAPIResource is an abstract class. You should perform actions on its subclasses (Account, etc.)"
8
+ end
9
+ # Namespaces are separated in object names with periods (.) and in URLs
10
+ # with forward slashes (/), so replace the former with the latter.
11
+ "/v2/#{self::OBJECT_NAME.downcase.tr('.', '/')}"
12
+ end
13
+
14
+ def resource_url
15
+ self.class.resource_url
16
+ end
17
+
18
+ def self.retrieve(opts = {})
19
+ instance = new(nil, Util.normalize_opts(opts))
20
+ instance.refresh
21
+ instance
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,545 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Telnyx
4
+ # TelnyxClient executes requests against the Telnyx API and allows a user to
5
+ # recover both a resource a call returns as well as a response object that
6
+ # contains information on the HTTP call.
7
+ class TelnyxClient
8
+ attr_accessor :conn
9
+
10
+ # Initializes a new TelnyxClient. Expects a Faraday connection object, and
11
+ # uses a default connection unless one is passed.
12
+ def initialize(conn = nil)
13
+ self.conn = conn || self.class.default_conn
14
+ @system_profiler = SystemProfiler.new
15
+ @last_request_metrics = nil
16
+ end
17
+
18
+ def self.active_client
19
+ Thread.current[:telnyx_client] || default_client
20
+ end
21
+
22
+ def self.default_client
23
+ Thread.current[:telnyx_client_default_client] ||= TelnyxClient.new(default_conn)
24
+ end
25
+
26
+ # A default Faraday connection to be used when one isn't configured. This
27
+ # object should never be mutated, and instead instantiating your own
28
+ # connection and wrapping it in a TelnyxClient object should be preferred.
29
+ def self.default_conn
30
+ # We're going to keep connections around so that we can take advantage
31
+ # of connection re-use, so make sure that we have a separate connection
32
+ # object per thread.
33
+ Thread.current[:telnyx_client_default_conn] ||= begin
34
+ conn = Faraday.new do |builder|
35
+ builder.use Faraday::Request::Multipart
36
+ builder.use Faraday::Request::UrlEncoded
37
+ builder.use Faraday::Response::RaiseError
38
+
39
+ # Net::HTTP::Persistent doesn't seem to do well on Windows or JRuby,
40
+ # so fall back to default there.
41
+ if Gem.win_platform? || RUBY_PLATFORM == "java"
42
+ builder.adapter :net_http
43
+ else
44
+ builder.adapter :net_http_persistent
45
+ end
46
+ end
47
+
48
+ if Telnyx.verify_ssl_certs
49
+ conn.ssl.verify = true
50
+ else
51
+ conn.ssl.verify = false
52
+
53
+ unless @verify_ssl_warned
54
+ @verify_ssl_warned = true
55
+ $stderr.puts("WARNING: Running without SSL cert verification. " \
56
+ "You should never do this in production. " \
57
+ "Execute 'Telnyx.verify_ssl_certs = true' to enable verification.")
58
+ end
59
+ end
60
+
61
+ conn
62
+ end
63
+ end
64
+
65
+ # Checks if an error is a problem that we should retry on. This includes both
66
+ # socket errors that may represent an intermittent problem and some special
67
+ # HTTP statuses.
68
+ def self.should_retry?(e, num_retries)
69
+ return false if num_retries >= Telnyx.max_network_retries
70
+
71
+ # Retry on timeout-related problems (either on open or read).
72
+ return true if e.is_a?(Faraday::TimeoutError)
73
+
74
+ # Destination refused the connection, the connection was reset, or a
75
+ # variety of other connection failures. This could occur from a single
76
+ # saturated server, so retry in case it's intermittent.
77
+ return true if e.is_a?(Faraday::ConnectionFailed)
78
+
79
+ false
80
+ end
81
+
82
+ def self.sleep_time(num_retries)
83
+ # Apply exponential backoff with initial_network_retry_delay on the
84
+ # number of num_retries so far as inputs. Do not allow the number to exceed
85
+ # max_network_retry_delay.
86
+ sleep_seconds = [Telnyx.initial_network_retry_delay * (2**(num_retries - 1)), Telnyx.max_network_retry_delay].min
87
+
88
+ # Apply some jitter by randomizing the value in the range of (sleep_seconds
89
+ # / 2) to (sleep_seconds).
90
+ sleep_seconds *= (0.5 * (1 + rand))
91
+
92
+ # But never sleep less than the base sleep seconds.
93
+ sleep_seconds = [Telnyx.initial_network_retry_delay, sleep_seconds].max
94
+
95
+ sleep_seconds
96
+ end
97
+
98
+ # Executes the API call within the given block. Usage looks like:
99
+ #
100
+ # client = TelnyxClient.new
101
+ # messaging_profile, resp = client.request { MessagingProfile.create }
102
+ #
103
+ def request
104
+ @last_response = nil
105
+ old_telnyx_client = Thread.current[:telnyx_client]
106
+ Thread.current[:telnyx_client] = self
107
+
108
+ begin
109
+ res = yield
110
+ [res, @last_response]
111
+ ensure
112
+ Thread.current[:telnyx_client] = old_telnyx_client
113
+ end
114
+ end
115
+
116
+ def execute_request(method, path,
117
+ api_base: nil, api_key: nil, headers: {}, params: {})
118
+ api_base ||= Telnyx.api_base
119
+ api_key ||= Telnyx.api_key
120
+
121
+ check_api_key!(api_key)
122
+
123
+ params = Util.objects_to_ids(params)
124
+ url = api_url(path, api_base)
125
+
126
+ body = nil
127
+ query_params = nil
128
+
129
+ case method.to_s.downcase.to_sym
130
+ when :get, :head, :delete
131
+ query_params = params
132
+ else
133
+ body = if headers[:content_type] && headers[:content_type] == "multipart/form-data"
134
+ params
135
+ else
136
+ JSON.generate(params)
137
+ end
138
+ end
139
+
140
+ # This works around an edge case where we end up with both query
141
+ # parameters in `query_params` and query parameters that are appended
142
+ # onto the end of the given path. In this case, Faraday will silently
143
+ # discard the URL's parameters which may break a request.
144
+ #
145
+ # Here we decode any parameters that were added onto the end of a path
146
+ # and add them to `query_params` so that all parameters end up in one
147
+ # place and all of them are correctly included in the final request.
148
+ u = URI.parse(path)
149
+ unless u.query.nil?
150
+ query_params ||= {}
151
+ query_params = Hash[URI.decode_www_form(u.query)].merge(query_params)
152
+
153
+ # Reset the path minus any query parameters that were specified.
154
+ path = u.path
155
+ end
156
+
157
+ headers = request_headers(api_key, method)
158
+ .update(Util.normalize_headers(headers))
159
+
160
+ # stores information on the request we're about to make so that we don't
161
+ # have to pass as many parameters around for logging.
162
+ context = RequestLogContext.new
163
+ context.api_key = api_key
164
+ context.body = body
165
+ context.method = method
166
+ context.path = path
167
+ context.query_params = query_params ? Util.encode_parameters(query_params) : nil
168
+
169
+ http_resp = execute_request_with_rescues(api_base, context) do
170
+ conn.run_request(method, url, body, headers) do |req|
171
+ req.options.open_timeout = Telnyx.open_timeout
172
+ req.options.timeout = Telnyx.read_timeout
173
+ req.params = query_params unless query_params.nil?
174
+ end
175
+ end
176
+
177
+ begin
178
+ resp = TelnyxResponse.from_faraday_response(http_resp)
179
+ rescue JSON::ParserError
180
+ raise general_api_error(http_resp.status, http_resp.body)
181
+ end
182
+
183
+ # Allows TelnyxClient#request to return a response object to a caller.
184
+ @last_response = resp
185
+ [resp, api_key]
186
+ end
187
+
188
+ private
189
+
190
+ def api_url(url = "", api_base = nil)
191
+ (api_base || Telnyx.api_base) + url
192
+ end
193
+
194
+ def check_api_key!(api_key)
195
+ unless api_key
196
+ raise AuthenticationError, "No API key provided. " \
197
+ 'Set your API key using "Telnyx.api_key = <API-KEY>". ' \
198
+ "You can generate API keys from the Telnyx web interface. " \
199
+ "See https://telnyx.com/api for details, or email support@telnyx.com " \
200
+ "if you have any questions."
201
+ end
202
+
203
+ return unless api_key =~ /\s/
204
+
205
+ raise AuthenticationError, "Your API key is invalid, as it contains " \
206
+ "whitespace. (HINT: You can double-check your API key from the " \
207
+ "Telnyx web interface. See https://telnyx.com/api for details, or " \
208
+ "email support@telnyx.com if you have any questions.)"
209
+ end
210
+
211
+ def execute_request_with_rescues(api_base, context)
212
+ num_retries = 0
213
+ begin
214
+ request_start = Time.now
215
+ log_request(context, num_retries)
216
+ resp = yield
217
+ context = context.dup_from_response(resp)
218
+ log_response(context, request_start, resp.status, resp.body)
219
+
220
+ # We rescue all exceptions from a request so that we have an easy spot to
221
+ # implement our retry logic across the board. We'll re-raise if it's a type
222
+ # of exception that we didn't expect to handle.
223
+ rescue StandardError => e
224
+ # If we modify context we copy it into a new variable so as not to
225
+ # taint the original on a retry.
226
+ error_context = context
227
+
228
+ if e.respond_to?(:response) && e.response
229
+ error_context = context.dup_from_response(e.response)
230
+ log_response(error_context, request_start,
231
+ e.response[:status], e.response[:body])
232
+ else
233
+ log_response_error(error_context, request_start, e)
234
+ end
235
+
236
+ if self.class.should_retry?(e, num_retries)
237
+ num_retries += 1
238
+ sleep self.class.sleep_time(num_retries)
239
+ retry
240
+ end
241
+
242
+ case e
243
+ when Faraday::ClientError
244
+ if e.response
245
+ handle_error_response(e.response, error_context)
246
+ else
247
+ handle_network_error(e, error_context, num_retries, api_base)
248
+ end
249
+
250
+ # Only handle errors when we know we can do so, and re-raise otherwise.
251
+ # This should be pretty infrequent.
252
+ else
253
+ raise
254
+ end
255
+ end
256
+
257
+ resp
258
+ end
259
+
260
+ def general_api_error(status, body)
261
+ APIError.new("Invalid response object from API: #{body.inspect} " \
262
+ "(HTTP response code was #{status})",
263
+ http_status: status, http_body: body)
264
+ end
265
+
266
+ # Formats a plugin "app info" hash into a string that we can tack onto the
267
+ # end of a User-Agent string where it'll be fairly prominent in places like
268
+ # the Dashboard. Note that this formatting has been implemented to match
269
+ # other libraries, and shouldn't be changed without universal consensus.
270
+ def format_app_info(info)
271
+ str = info[:name]
272
+ str = "#{str}/#{info[:version]}" unless info[:version].nil?
273
+ str = "#{str} (#{info[:url]})" unless info[:url].nil?
274
+ str
275
+ end
276
+
277
+ def handle_error_response(http_resp, context)
278
+ begin
279
+ resp = TelnyxResponse.from_faraday_hash(http_resp)
280
+ error_list = resp.data[:errors]
281
+
282
+ raise TelnyxError, "Indeterminate error" unless error_list
283
+ rescue JSON::ParserError, TelnyxError
284
+ raise general_api_error(http_resp[:status], http_resp[:body])
285
+ end
286
+
287
+ error = specific_api_error(resp, error_list, context)
288
+
289
+ error.response = resp
290
+ raise(error)
291
+ end
292
+
293
+ def specific_api_error(resp, error_list, context)
294
+ error_list.each do |error|
295
+ Util.log_error("Telnyx API error",
296
+ status: resp.http_status,
297
+ error_code: error[:code],
298
+ error_detail: error[:detail],
299
+ error_source: error[:source],
300
+ error_title: error[:title],
301
+ request_id: context.request_id)
302
+ end
303
+
304
+ # The standard set of arguments that can be used to initialize most of
305
+ # the exceptions.
306
+ opts = {
307
+ http_body: resp.http_body,
308
+ http_headers: resp.http_headers,
309
+ http_status: resp.http_status,
310
+ json_body: resp.data,
311
+ }
312
+
313
+ case resp.http_status
314
+ when 400
315
+ InvalidRequestError.new(error_list, opts)
316
+ when 401
317
+ AuthenticationError.new(error_list, opts)
318
+ when 403
319
+ PermissionError.new(error_list, opts)
320
+ when 404
321
+ ResourceNotFoundError.new(error_list, opts)
322
+ when 405
323
+ MethodNotSupportedError.new(error_list, opts)
324
+ when 408
325
+ TimeoutError.new(error_list, opts)
326
+ when 422
327
+ InvalidParametersError.new(error_list, opts)
328
+ when 429
329
+ RateLimitError.new(error_list, opts)
330
+ when 500
331
+ APIError.new(error_list, opts)
332
+ when 503
333
+ ServiceUnavailableError.new(error_list, opts)
334
+ else
335
+ APIError.new(error_list, opts)
336
+ end
337
+ end
338
+
339
+ def handle_network_error(e, context, num_retries, api_base = nil)
340
+ Util.log_error("Telnyx network error",
341
+ error_message: e.message,
342
+ request_id: context.request_id)
343
+
344
+ case e
345
+ when Faraday::ConnectionFailed
346
+ message = "Unexpected error communicating when trying to connect to Telnyx. " \
347
+ "You may be seeing this message because your DNS is not working. " \
348
+ "To check, try running 'host telnyx.com' from the command line."
349
+
350
+ when Faraday::SSLError
351
+ message = "Could not establish a secure connection to Telnyx, you may " \
352
+ "need to upgrade your OpenSSL version. To check, try running " \
353
+ "'openssl s_client -connect api.telnyx.com:443' from the " \
354
+ "command line."
355
+
356
+ when Faraday::TimeoutError
357
+ api_base ||= Telnyx.api_base
358
+ message = "Could not connect to Telnyx (#{api_base}). " \
359
+ "Please check your internet connection and try again. " \
360
+ "If this problem persists, you should check Telnyx's service status at " \
361
+ "https://twitter.com/telnyxstatus, or let us know at support@telnyx.com."
362
+
363
+ else
364
+ message = "Unexpected error communicating with Telnyx. " \
365
+ "If this problem persists, let us know at support@telnyx.com."
366
+
367
+ end
368
+
369
+ message += " Request was retried #{num_retries} times." if num_retries > 0
370
+
371
+ raise APIConnectionError, message + "\n\n(Network error: #{e.message})"
372
+ end
373
+
374
+ def request_headers(api_key, _method)
375
+ user_agent = "Telnyx/v2 RubyBindings/#{Telnyx::VERSION}"
376
+ unless Telnyx.app_info.nil?
377
+ user_agent += " " + format_app_info(Telnyx.app_info)
378
+ end
379
+
380
+ headers = {
381
+ "User-Agent" => user_agent,
382
+ "Authorization" => "Bearer #{api_key}",
383
+ "Content-Type" => "application/json",
384
+ }
385
+
386
+ user_agent = @system_profiler.user_agent
387
+ begin
388
+ headers.update(
389
+ "X-Telnyx-Client-User-Agent" => JSON.generate(user_agent)
390
+ )
391
+ rescue StandardError => e
392
+ headers.update(
393
+ "X-Telnyx-Client-Raw-User-Agent" => user_agent.inspect,
394
+ :error => "#{e} (#{e.class})"
395
+ )
396
+ end
397
+
398
+ headers
399
+ end
400
+
401
+ def log_request(context, num_retries)
402
+ Util.log_info("Request to Telnyx API",
403
+ method: context.method,
404
+ num_retries: num_retries,
405
+ path: context.path)
406
+ Util.log_debug("Request details",
407
+ body: context.body,
408
+ query_params: context.query_params)
409
+ end
410
+ private :log_request
411
+
412
+ def log_response(context, request_start, status, body)
413
+ Util.log_info("Response from Telnyx API",
414
+ elapsed: Time.now - request_start,
415
+ method: context.method,
416
+ path: context.path,
417
+ request_id: context.request_id,
418
+ status: status)
419
+ Util.log_debug("Response details",
420
+ body: body,
421
+ request_id: context.request_id)
422
+
423
+ return unless context.request_id
424
+ end
425
+ private :log_response
426
+
427
+ def log_response_error(context, request_start, e)
428
+ Util.log_error("Request error",
429
+ elapsed: Time.now - request_start,
430
+ error_message: e.message,
431
+ method: context.method,
432
+ path: context.path)
433
+ end
434
+ private :log_response_error
435
+
436
+ # RequestLogContext stores information about a request that's begin made so
437
+ # that we can log certain information. It's useful because it means that we
438
+ # don't have to pass around as many parameters.
439
+ class RequestLogContext
440
+ attr_accessor :body
441
+ attr_accessor :api_key
442
+ attr_accessor :method
443
+ attr_accessor :path
444
+ attr_accessor :query_params
445
+ attr_accessor :request_id
446
+
447
+ # The idea with this method is that we might want to update some of
448
+ # context information because a response that we've received from the API
449
+ # contains information that's more authoritative than what we started
450
+ # with for a request. For example, we should trust whatever came back in
451
+ # a `Telnyx-Version` header beyond what configuration information that we
452
+ # might have had available.
453
+ def dup_from_response(resp)
454
+ return self if resp.nil?
455
+
456
+ # Faraday's API is a little unusual. Normally it'll produce a response
457
+ # object with a `headers` method, but on error what it puts into
458
+ # `e.response` is an untyped `Hash`.
459
+ headers = if resp.is_a?(Faraday::Response)
460
+ resp.headers
461
+ else
462
+ resp[:headers]
463
+ end
464
+
465
+ context = dup
466
+ context.request_id = headers["X-Request-Id"]
467
+ context
468
+ end
469
+ end
470
+
471
+ # SystemProfiler extracts information about the system that we're running
472
+ # in so that we can generate a rich user agent header to help debug
473
+ # integrations.
474
+ class SystemProfiler
475
+ def self.uname
476
+ if ::File.exist?("/proc/version")
477
+ ::File.read("/proc/version").strip
478
+ else
479
+ case RbConfig::CONFIG["host_os"]
480
+ when /linux|darwin|bsd|sunos|solaris|cygwin/i
481
+ uname_from_system
482
+ when /mswin|mingw/i
483
+ uname_from_system_ver
484
+ else
485
+ "unknown platform"
486
+ end
487
+ end
488
+ end
489
+
490
+ def self.uname_from_system
491
+ (`uname -a 2>/dev/null` || "").strip
492
+ rescue Errno::ENOENT
493
+ "uname executable not found"
494
+ rescue Errno::ENOMEM # couldn't create subprocess
495
+ "uname lookup failed"
496
+ end
497
+
498
+ def self.uname_from_system_ver
499
+ (`ver` || "").strip
500
+ rescue Errno::ENOENT
501
+ "ver executable not found"
502
+ rescue Errno::ENOMEM # couldn't create subprocess
503
+ "uname lookup failed"
504
+ end
505
+
506
+ def initialize
507
+ @uname = self.class.uname
508
+ end
509
+
510
+ def user_agent
511
+ lang_version = "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})"
512
+
513
+ {
514
+ application: Telnyx.app_info,
515
+ bindings_version: Telnyx::VERSION,
516
+ lang: "ruby",
517
+ lang_version: lang_version,
518
+ platform: RUBY_PLATFORM,
519
+ engine: defined?(RUBY_ENGINE) ? RUBY_ENGINE : "",
520
+ publisher: "telnyx",
521
+ uname: @uname,
522
+ hostname: Socket.gethostname,
523
+ }.delete_if { |_k, v| v.nil? }
524
+ end
525
+ end
526
+
527
+ # TelnyxRequestMetrics tracks metadata to be reported to telnyx for metrics collection
528
+ class TelnyxRequestMetrics
529
+ # The Telnyx request ID of the response.
530
+ attr_accessor :request_id
531
+
532
+ # Request duration in milliseconds
533
+ attr_accessor :request_duration_ms
534
+
535
+ def initialize(request_id, request_duration_ms)
536
+ self.request_id = request_id
537
+ self.request_duration_ms = request_duration_ms
538
+ end
539
+
540
+ def payload
541
+ { request_id: request_id, request_duration_ms: request_duration_ms }
542
+ end
543
+ end
544
+ end
545
+ end