sms_traffic_sdk 0.2.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 +34 -0
- data/README.md +11 -2
- data/lib/sms_traffic/client/reply.rb +21 -3
- data/lib/sms_traffic/client.rb +10 -6
- data/lib/sms_traffic/configuration.rb +1 -1
- data/lib/sms_traffic/errors.rb +10 -0
- data/lib/sms_traffic/version.rb +1 -1
- metadata +9 -9
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,39 @@
|
|
|
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
|
+
|
|
19
|
+
## 0.3.0 - 2026-11-03
|
|
20
|
+
|
|
21
|
+
### Breaking changes
|
|
22
|
+
|
|
23
|
+
- `nokogiri` is no longer a dependency. Applications that relied on Nokogiri being installed through this gem must
|
|
24
|
+
depend on it themselves.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- The default XML parser is `Nori.new(advanced_typecasting: false, parser: :rexml)`. Every fixture parses to the
|
|
29
|
+
same hash as with Nokogiri. `rexml` (`>= 3.3.9`, after the 2024 DoS fixes) is a new runtime dependency; a custom
|
|
30
|
+
`xml_parser` is still possible.
|
|
31
|
+
- A body that is not well-formed XML (an HTML error page, plain text, a truncated reply) keeps the 0.2.0 behavior:
|
|
32
|
+
the reply is empty, so `Sms#deliver` returns `false` and `Client.*` return a reply with an empty hash.
|
|
33
|
+
- Runtime dependency lower bounds are tested: `gemfiles/minimum.gemfile` pins `nori` 2.8.0, `rexml` 3.3.9 and
|
|
34
|
+
`bigdecimal` 3.1.0, and the suite passes with them. CI runs it as `test-minimum` on Ruby 3.1 and 3.4; `publish`
|
|
35
|
+
waits for it.
|
|
36
|
+
|
|
3
37
|
## 0.2.0 - 2026-11-03
|
|
4
38
|
|
|
5
39
|
### Added
|
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,16 +133,24 @@ 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
|
|
|
141
|
+
### XML parser
|
|
142
|
+
|
|
143
|
+
Replies are parsed with Nori and its pure-Ruby REXML backend, so no native extension is needed. To use another
|
|
144
|
+
parser, set `config.xml_parser` to any object that responds to `parse(xml)` and returns a Hash.
|
|
145
|
+
|
|
139
146
|
### Errors
|
|
140
147
|
|
|
141
148
|
All errors inherit from `SmsTraffic::Error`:
|
|
142
149
|
|
|
143
150
|
- `SmsTraffic::ConnectionError` - network, timeout or TLS failure (the original exception is `cause`).
|
|
144
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.
|
|
145
154
|
- `SmsTraffic::ApiError` - the provider replied `ERROR` (or `ERRO`); has `code` and `description`, and the
|
|
146
155
|
predicates `retryable?` (1000), `access_denied?` (411, 412), `insufficient_funds?` (415) and `blocked_phone?` (432).
|
|
147
156
|
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
# Nori loads REXML lazily on the first parse; Reply#parse rescues its exception even with a custom parser.
|
|
2
|
+
require 'rexml/parsers/baseparser'
|
|
3
|
+
|
|
1
4
|
module SmsTraffic
|
|
2
5
|
class Client
|
|
3
6
|
class Reply
|
|
@@ -6,10 +9,11 @@ module SmsTraffic
|
|
|
6
9
|
|
|
7
10
|
attr_reader :xml, :to_h
|
|
8
11
|
|
|
12
|
+
# @raise [MalformedReplyError] the body is not an XML `<reply>` element
|
|
9
13
|
def initialize(xml, xml_parser: SmsTraffic.configuration.xml_parser)
|
|
10
|
-
@xml
|
|
11
|
-
|
|
12
|
-
@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?
|
|
13
17
|
end
|
|
14
18
|
|
|
15
19
|
def result
|
|
@@ -31,6 +35,20 @@ module SmsTraffic
|
|
|
31
35
|
|
|
32
36
|
private
|
|
33
37
|
|
|
38
|
+
def parse(xml_parser)
|
|
39
|
+
xml_parser.parse(@xml)
|
|
40
|
+
rescue REXML::ParseException
|
|
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)
|
|
50
|
+
end
|
|
51
|
+
|
|
34
52
|
def fetch_value(source, key)
|
|
35
53
|
return unless source.is_a?(Hash)
|
|
36
54
|
|
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)
|
|
@@ -17,7 +17,7 @@ module SmsTraffic
|
|
|
17
17
|
|
|
18
18
|
def reset!
|
|
19
19
|
ATTRIBUTES.each { |attribute| public_send("#{attribute}=", DEFAULTS[attribute]) }
|
|
20
|
-
self.xml_parser = Nori.new(advanced_typecasting: false)
|
|
20
|
+
self.xml_parser = Nori.new(advanced_typecasting: false, parser: :rexml)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def validate!
|
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
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.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Rybolovlev
|
|
@@ -25,33 +25,33 @@ dependencies:
|
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '3.1'
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
|
-
name:
|
|
28
|
+
name: nori
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
30
30
|
requirements:
|
|
31
31
|
- - "~>"
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '
|
|
33
|
+
version: '2.8'
|
|
34
34
|
type: :runtime
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
38
|
- - "~>"
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: '
|
|
40
|
+
version: '2.8'
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
|
-
name:
|
|
42
|
+
name: rexml
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
44
44
|
requirements:
|
|
45
|
-
- - "
|
|
45
|
+
- - ">="
|
|
46
46
|
- !ruby/object:Gem::Version
|
|
47
|
-
version:
|
|
47
|
+
version: 3.3.9
|
|
48
48
|
type: :runtime
|
|
49
49
|
prerelease: false
|
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
51
|
requirements:
|
|
52
|
-
- - "
|
|
52
|
+
- - ">="
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
|
-
version:
|
|
54
|
+
version: 3.3.9
|
|
55
55
|
description: |-
|
|
56
56
|
Ruby Gem as a SDK for interacting with the SMS Traffic HTTP API (smstraffic.ru/api).
|
|
57
57
|
This gem provides a convenient way to integrate SMS Traffic services into Ruby applications,
|