sms_traffic_sdk 0.3.0 → 0.4.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 73b544fc8bce8a1627dca1e7be082da47adbb4116bc6d47a4fdb9cd50bbc2e3c
4
- data.tar.gz: d653685da42c1e33a1d06a300edf98d8f5f83e5b6da969f57e41eb4b444692b2
3
+ metadata.gz: 4c5c633712d477a8978bb88a14dded77e522195705ecc68e2bae2bf18078943c
4
+ data.tar.gz: 64b2ea39745e547e707898b6ec4778a937eff0c9c54ee2df5783b37af04d01d6
5
5
  SHA512:
6
- metadata.gz: 75efe5af30bde21435b11fde083721a9cc9b701dbb3f3eab506670694a9b6b863b2c143a288793b27e63607ef138c2ffd8b31d7a3387821ac439b2765ffeed16
7
- data.tar.gz: 640e577af992334877dcaf3c0839e005cf2929e5fa21c9ac9bb0f4fb4f94a64f446ad428e3764fdaf152444bf7e33b01f5b967414e1a78928bc39c502aab9895
6
+ metadata.gz: 4f02913510ac31a7b1b7f8f61cb503ca10332ec04a6bc2281c90da2598444ad9a940b329948b6759a79be48842a918299936b74e5b0b7c2b986fe5cf6b5febd8
7
+ data.tar.gz: 7a708811f729349e37db2c75c68c636390f147ef3facff7cfcaf7b200ef57d6538a20b64c0dde0a93af71066f7d632d747127f10ebabf50eab874ac51b3f8b3c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0 - 2026-11-03
4
+
5
+ ### Breaking changes
6
+
7
+ - An HTTP 200 body that is not an XML `<reply>` element (an HTML error page, plain text, a truncated or empty body)
8
+ raises `SmsTraffic::MalformedReplyError` instead of returning an empty reply. Its message is
9
+ `"Malformed reply (N bytes)"`; `#body` keeps the body. `Sms#deliver` returns `false` and records that message
10
+ instead of `"Sms has not been enqueued: "`. An empty `<reply/>` is still an empty reply.
11
+ - The debug request line shows only `originator`, `want_sms_ids`, `rus`, `operation` and `sms_id`; `phones` and any
12
+ extra `deliver` option are now `[FILTERED]` too.
13
+
14
+ ### Changed
15
+
16
+ - `Client.status` and `Client.balance` repeat on the fallback host after `MalformedReplyError`; `Client.deliver`
17
+ does not, since the provider may already have queued the SMS.
18
+
3
19
  ## 0.3.0 - 2026-11-03
4
20
 
5
21
  ### Breaking changes
data/README.md CHANGED
@@ -58,7 +58,8 @@ end
58
58
 
59
59
  `login`, `password` and `host` are required; `logger` is required when `debug` is enabled.
60
60
  With `debug` on, one request line and one response line are written through `logger.debug`;
61
- `login`, `password` and `message` are replaced with `[FILTERED]`.
61
+ only `originator`, `want_sms_ids`, `rus`, `operation` and `sms_id` are shown, every other param (credentials,
62
+ `phones`, `message`, extra options) is replaced with `[FILTERED]`.
62
63
 
63
64
  Send an SMS:
64
65
 
@@ -132,7 +133,8 @@ The value is returned as the provider sends it; its units are not documented and
132
133
  Set `config.fallback_host` (the provider documents `https://api2.smstraffic.ru`) to repeat a failed request once
133
134
  on the backup host. The request is repeated after `SocketError`, `Errno::ECONNREFUSED`, `Errno::EHOSTUNREACH`,
134
135
  `Errno::ENETUNREACH`, `Net::OpenTimeout`, or API error 1000 (temporary server problem). `Client.status` and
135
- `Client.balance` also retry after any `ConnectionError` (read timeout, reset connection, TLS failure) and HTTP 5xx.
136
+ `Client.balance` also retry after any `ConnectionError` (read timeout, reset connection, TLS failure), HTTP 5xx and
137
+ `MalformedReplyError`.
136
138
  `Client.deliver` does not, since the provider may already have queued the SMS. The error of the last attempt is raised. With `debug`, the retry is
137
139
  logged in one line without credentials.
138
140
 
@@ -147,6 +149,8 @@ All errors inherit from `SmsTraffic::Error`:
147
149
 
148
150
  - `SmsTraffic::ConnectionError` - network, timeout or TLS failure (the original exception is `cause`).
149
151
  - `SmsTraffic::HttpError` - non-2xx response; has `status` and `body`.
152
+ - `SmsTraffic::MalformedReplyError` - HTTP 200 whose body is not an XML `<reply>` element (an HTML error page, plain
153
+ text, a truncated reply); has `body`, the message gives only its size.
150
154
  - `SmsTraffic::ApiError` - the provider replied `ERROR` (or `ERRO`); has `code` and `description`, and the
151
155
  predicates `retryable?` (1000), `access_denied?` (411, 412), `insufficient_funds?` (415) and `blocked_phone?` (432).
152
156
 
@@ -9,10 +9,11 @@ module SmsTraffic
9
9
 
10
10
  attr_reader :xml, :to_h
11
11
 
12
+ # @raise [MalformedReplyError] the body is not an XML `<reply>` element
12
13
  def initialize(xml, xml_parser: SmsTraffic.configuration.xml_parser)
13
- @xml = xml.gsub(/\s+/, ' ')
14
- parsed = fetch_value(parse(xml_parser), :reply)
15
- @to_h = parsed.is_a?(Hash) ? parsed : {}
14
+ @xml = xml.to_s.gsub(/\s+/, ' ')
15
+ @to_h = reply_hash(parse(xml_parser))
16
+ raise MalformedReplyError, xml if @to_h.nil?
16
17
  end
17
18
 
18
19
  def result
@@ -34,11 +35,18 @@ module SmsTraffic
34
35
 
35
36
  private
36
37
 
37
- # A body that is not well-formed XML is an empty reply, as before the REXML parser.
38
38
  def parse(xml_parser)
39
39
  xml_parser.parse(@xml)
40
40
  rescue REXML::ParseException
41
- {}
41
+ nil
42
+ end
43
+
44
+ # An empty `<reply/>` carries no data; a missing `<reply>` element or one holding only text is malformed.
45
+ def reply_hash(parsed)
46
+ return unless parsed.is_a?(Hash) && (parsed.key?('reply') || parsed.key?(:reply))
47
+
48
+ reply = fetch_value(parsed, :reply) || {}
49
+ reply if reply.is_a?(Hash)
42
50
  end
43
51
 
44
52
  def fetch_value(source, key)
@@ -10,7 +10,8 @@ module SmsTraffic
10
10
  class Client
11
11
  ENDPOINT = 'multi.php'.freeze
12
12
  FILTERED = '[FILTERED]'.freeze
13
- FILTERED_PARAMS = %i[login password message].freeze
13
+ # The only request params the debug log shows; every other value, phones and options included, is FILTERED.
14
+ LOGGED_PARAMS = %i[originator want_sms_ids rus operation sms_id].freeze
14
15
  CONNECTION_ERRORS = [
15
16
  SocketError, SystemCallError, IOError, Net::OpenTimeout, Net::ReadTimeout, OpenSSL::SSL::SSLError
16
17
  ].freeze
@@ -25,7 +26,7 @@ module SmsTraffic
25
26
  # @param originator [String] originator (subject), defaults to the configured one
26
27
  # @param options [Hash] additional options
27
28
  # @return [DeliverReply]
28
- # @raise [ConnectionError, HttpError, ApiError]
29
+ # @raise [ConnectionError, HttpError, MalformedReplyError, ApiError]
29
30
  def deliver(phone, message, originator = nil, **options)
30
31
  originator ||= SmsTraffic.configuration.originator
31
32
  request(DeliverReply, deliver_params(phone, message, originator, **options), read_only: false)
@@ -33,13 +34,13 @@ module SmsTraffic
33
34
 
34
35
  # @param sms_ids [Array<String>] sms ids
35
36
  # @return [StatusReply]
36
- # @raise [ConnectionError, HttpError, ApiError]
37
+ # @raise [ConnectionError, HttpError, MalformedReplyError, ApiError]
37
38
  def status(*sms_ids)
38
39
  request(StatusReply, status_params(sms_ids), read_only: true)
39
40
  end
40
41
 
41
42
  # @return [AccountReply]
42
- # @raise [ConnectionError, HttpError, ApiError]
43
+ # @raise [ConnectionError, HttpError, MalformedReplyError, ApiError]
43
44
  def balance
44
45
  request(AccountReply, account_params, read_only: true)
45
46
  end
@@ -76,12 +77,15 @@ module SmsTraffic
76
77
  def retry_on_fallback?(error, read_only:)
77
78
  case error
78
79
  when ApiError then error.retryable?
79
- when ConnectionError then read_only || UNREACHABLE_ERRORS.any? { |klass| error.cause.is_a?(klass) }
80
+ when MalformedReplyError then read_only
81
+ when ConnectionError then read_only || unreachable?(error)
80
82
  when HttpError then read_only && error.status >= 500
81
83
  else false
82
84
  end
83
85
  end
84
86
 
87
+ def unreachable?(error) = UNREACHABLE_ERRORS.any? { |klass| error.cause.is_a?(klass) }
88
+
85
89
  # @param form_data [Hash<Symbol, String|Integer>]
86
90
  # @return [String] response body
87
91
  def post(form_data, host)
@@ -118,7 +122,7 @@ module SmsTraffic
118
122
  end
119
123
 
120
124
  def filter(form_data)
121
- form_data.to_h { |key, value| [key, FILTERED_PARAMS.include?(key) ? FILTERED : value] }
125
+ form_data.to_h { |key, value| [key, LOGGED_PARAMS.include?(key.to_sym) ? value : FILTERED] }
122
126
  end
123
127
 
124
128
  def deliver_params(phone, message, originator, **options)
@@ -15,6 +15,16 @@ module SmsTraffic
15
15
  end
16
16
  end
17
17
 
18
+ # HTTP 200 body that is not an XML `<reply>` element: an HTML error page, plain text, a truncated reply.
19
+ class MalformedReplyError < Error
20
+ attr_reader :body
21
+
22
+ def initialize(body)
23
+ @body = body
24
+ super("Malformed reply (#{body.to_s.bytesize} bytes)")
25
+ end
26
+ end
27
+
18
28
  # HTTP 200 reply whose `result` is not `OK` (`ERROR`, `ERRO`).
19
29
  class ApiError < Error
20
30
  attr_reader :code, :description
@@ -1,3 +1,3 @@
1
1
  module SmsTraffic
2
- VERSION = '0.3.0'.freeze
2
+ VERSION = '0.4.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sms_traffic_sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Rybolovlev