telnyx 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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