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 +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +6 -2
- data/lib/sms_traffic/client/reply.rb +13 -5
- data/lib/sms_traffic/client.rb +10 -6
- data/lib/sms_traffic/errors.rb +10 -0
- data/lib/sms_traffic/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4c5c633712d477a8978bb88a14dded77e522195705ecc68e2bae2bf18078943c
|
|
4
|
+
data.tar.gz: 64b2ea39745e547e707898b6ec4778a937eff0c9c54ee2df5783b37af04d01d6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
`
|
|
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)
|
|
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
|
|
14
|
-
|
|
15
|
-
@to_h
|
|
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)
|
data/lib/sms_traffic/client.rb
CHANGED
|
@@ -10,7 +10,8 @@ module SmsTraffic
|
|
|
10
10
|
class Client
|
|
11
11
|
ENDPOINT = 'multi.php'.freeze
|
|
12
12
|
FILTERED = '[FILTERED]'.freeze
|
|
13
|
-
|
|
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
|
|
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,
|
|
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)
|
data/lib/sms_traffic/errors.rb
CHANGED
|
@@ -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
|
data/lib/sms_traffic/version.rb
CHANGED