sms_traffic_sdk 0.1.0 → 0.3.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 +41 -0
- data/README.md +46 -2
- data/lib/sms_traffic/client/account_reply.rb +14 -0
- data/lib/sms_traffic/client/deliver_reply.rb +0 -2
- data/lib/sms_traffic/client/reply.rb +14 -2
- data/lib/sms_traffic/client/status_reply.rb +21 -2
- data/lib/sms_traffic/client.rb +55 -12
- data/lib/sms_traffic/configuration.rb +6 -11
- data/lib/sms_traffic/errors.rb +18 -1
- data/lib/sms_traffic/sms.rb +13 -0
- data/lib/sms_traffic/status.rb +26 -0
- data/lib/sms_traffic/version.rb +1 -1
- data/lib/sms_traffic.rb +1 -0
- metadata +22 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 73b544fc8bce8a1627dca1e7be082da47adbb4116bc6d47a4fdb9cd50bbc2e3c
|
|
4
|
+
data.tar.gz: d653685da42c1e33a1d06a300edf98d8f5f83e5b6da969f57e41eb4b444692b2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 75efe5af30bde21435b11fde083721a9cc9b701dbb3f3eab506670694a9b6b863b2c143a288793b27e63607ef138c2ffd8b31d7a3387821ac439b2765ffeed16
|
|
7
|
+
data.tar.gz: 640e577af992334877dcaf3c0839e005cf2929e5fa21c9ac9bb0f4fb4f94a64f446ad428e3764fdaf152444bf7e33b01f5b967414e1a78928bc39c502aab9895
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0 - 2026-11-03
|
|
4
|
+
|
|
5
|
+
### Breaking changes
|
|
6
|
+
|
|
7
|
+
- `nokogiri` is no longer a dependency. Applications that relied on Nokogiri being installed through this gem must
|
|
8
|
+
depend on it themselves.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- The default XML parser is `Nori.new(advanced_typecasting: false, parser: :rexml)`. Every fixture parses to the
|
|
13
|
+
same hash as with Nokogiri. `rexml` (`>= 3.3.9`, after the 2024 DoS fixes) is a new runtime dependency; a custom
|
|
14
|
+
`xml_parser` is still possible.
|
|
15
|
+
- A body that is not well-formed XML (an HTML error page, plain text, a truncated reply) keeps the 0.2.0 behavior:
|
|
16
|
+
the reply is empty, so `Sms#deliver` returns `false` and `Client.*` return a reply with an empty hash.
|
|
17
|
+
- Runtime dependency lower bounds are tested: `gemfiles/minimum.gemfile` pins `nori` 2.8.0, `rexml` 3.3.9 and
|
|
18
|
+
`bigdecimal` 3.1.0, and the suite passes with them. CI runs it as `test-minimum` on Ruby 3.1 and 3.4; `publish`
|
|
19
|
+
waits for it.
|
|
20
|
+
|
|
21
|
+
## 0.2.0 - 2026-11-03
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- `Configuration#fallback_host` (default `nil`). When set, the client repeats a request once on that host after a
|
|
26
|
+
connection failure that proves the server was not reached (`SocketError`, `Errno::ECONNREFUSED`,
|
|
27
|
+
`Errno::EHOSTUNREACH`, `Errno::ENETUNREACH`, `Net::OpenTimeout`) or after API error 1000. `Client.status` and
|
|
28
|
+
`Client.balance` also retry after any `ConnectionError` and HTTP 5xx; `Client.deliver` never does, because the
|
|
29
|
+
provider may already have queued the SMS. With `debug`, the retry is logged in one line without credentials.
|
|
30
|
+
- `Client.balance` returns an `AccountReply`; `AccountReply#balance` is a `BigDecimal` (units as sent by the
|
|
31
|
+
provider). `bigdecimal` is a runtime dependency.
|
|
32
|
+
- `SmsTraffic::Status` (`FINAL`, `INTERMEDIATE`), `StatusReply::Entry#final?` / `#delivered?`, the same methods on
|
|
33
|
+
`StatusReply` (optional `sms_id`) and `Sms#final?` / `#delivered?`.
|
|
34
|
+
- `ApiError#retryable?`, `#access_denied?`, `#insufficient_funds?` and `#blocked_phone?`.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- A provider reply with `<result>ERRO</result>` (the spelling in the provider's own example) is now an
|
|
39
|
+
`ApiError`. `Reply#api_error?` is true for any present `result` other than `OK`.
|
|
40
|
+
- `Sms#update_status` no longer records "No status returned" for an empty `<status>` element (an intermediate
|
|
41
|
+
provider status); the status stays `sent`. `StatusReply::Entry#status_reported?` tells an empty status from an
|
|
42
|
+
absent one.
|
|
43
|
+
|
|
3
44
|
## 0.1.0 - 2026-11-03
|
|
4
45
|
|
|
5
46
|
### Breaking changes
|
data/README.md
CHANGED
|
@@ -13,6 +13,8 @@ sending of SMS messages and checking delivery statuses.
|
|
|
13
13
|
|
|
14
14
|
- Send SMS messages to single or multiple recipients.
|
|
15
15
|
- Check the delivery status of sent messages.
|
|
16
|
+
- Read the account balance.
|
|
17
|
+
- Fail over to the provider's backup host.
|
|
16
18
|
- **TODO:** Support for various message formats and encodings.
|
|
17
19
|
|
|
18
20
|
|
|
@@ -44,6 +46,7 @@ SmsTraffic.configure do |config|
|
|
|
44
46
|
config.login = 'login'
|
|
45
47
|
config.password = 'password'
|
|
46
48
|
config.host = 'https://api.smstraffic.ru'
|
|
49
|
+
config.fallback_host = 'https://api2.smstraffic.ru' # optional backup host, default nil
|
|
47
50
|
config.originator = 'default_originator' # optional, can be given per SMS
|
|
48
51
|
config.open_timeout = 10 # seconds, default 10
|
|
49
52
|
config.read_timeout = 30 # seconds, default 30
|
|
@@ -96,7 +99,47 @@ reply.status('8287713301') # => 'Delivered'; the id may be omitted whe
|
|
|
96
99
|
```
|
|
97
100
|
|
|
98
101
|
Each entry has `sms_id`, `status`, `error`, `submission_date`, `send_date`, `last_status_change_date`
|
|
99
|
-
(strings as sent by the provider), `error?` and `
|
|
102
|
+
(strings as sent by the provider), `error?`, `not_found?`, `final?`, `delivered?` and `status_reported?`
|
|
103
|
+
(true when the provider sent a status element, even an empty one).
|
|
104
|
+
|
|
105
|
+
### Final and delivered statuses
|
|
106
|
+
|
|
107
|
+
Provider statuses `Delivered`, `Non Delivered`, `Rejected`, `Expired`, `Deleted` and `Unknown status` are final;
|
|
108
|
+
an empty status and `Buffered SMSC` are intermediate (`SmsTraffic::Status::FINAL` / `INTERMEDIATE`).
|
|
109
|
+
Comparison is case-insensitive.
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
reply = SmsTraffic::Client.status('8287713301')
|
|
113
|
+
reply.final? # => true
|
|
114
|
+
reply.delivered? # => true; pass an sms id when the reply has several entries
|
|
115
|
+
reply.entry.final?
|
|
116
|
+
|
|
117
|
+
sms.update_status
|
|
118
|
+
sms.final? # => true for a final stored status; false for 'not-sent' and 'sent'
|
|
119
|
+
sms.delivered? # => true only for 'delivered'
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Balance
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
SmsTraffic::Client.balance.balance # => 0.10025e5 (BigDecimal), nil when absent or not a number
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The value is returned as the provider sends it; its units are not documented and are not converted.
|
|
129
|
+
|
|
130
|
+
### Fallback host
|
|
131
|
+
|
|
132
|
+
Set `config.fallback_host` (the provider documents `https://api2.smstraffic.ru`) to repeat a failed request once
|
|
133
|
+
on the backup host. The request is repeated after `SocketError`, `Errno::ECONNREFUSED`, `Errno::EHOSTUNREACH`,
|
|
134
|
+
`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.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
|
+
logged in one line without credentials.
|
|
138
|
+
|
|
139
|
+
### XML parser
|
|
140
|
+
|
|
141
|
+
Replies are parsed with Nori and its pure-Ruby REXML backend, so no native extension is needed. To use another
|
|
142
|
+
parser, set `config.xml_parser` to any object that responds to `parse(xml)` and returns a Hash.
|
|
100
143
|
|
|
101
144
|
### Errors
|
|
102
145
|
|
|
@@ -104,7 +147,8 @@ All errors inherit from `SmsTraffic::Error`:
|
|
|
104
147
|
|
|
105
148
|
- `SmsTraffic::ConnectionError` - network, timeout or TLS failure (the original exception is `cause`).
|
|
106
149
|
- `SmsTraffic::HttpError` - non-2xx response; has `status` and `body`.
|
|
107
|
-
- `SmsTraffic::ApiError` - the provider replied `ERROR
|
|
150
|
+
- `SmsTraffic::ApiError` - the provider replied `ERROR` (or `ERRO`); has `code` and `description`, and the
|
|
151
|
+
predicates `retryable?` (1000), `access_denied?` (411, 412), `insufficient_funds?` (415) and `blocked_phone?` (432).
|
|
108
152
|
|
|
109
153
|
```ruby
|
|
110
154
|
begin
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
require 'bigdecimal'
|
|
2
|
+
|
|
3
|
+
module SmsTraffic
|
|
4
|
+
class Client
|
|
5
|
+
class AccountReply < Reply
|
|
6
|
+
# @return [BigDecimal, nil] account balance as sent by the provider (units are not converted); nil when absent
|
|
7
|
+
# or not a number
|
|
8
|
+
def balance
|
|
9
|
+
value = fetch_value(to_h, :account).to_s.strip
|
|
10
|
+
BigDecimal(value, exception: false) unless value.empty?
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -1,13 +1,17 @@
|
|
|
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
|
|
7
|
+
OK_RESULT = 'OK'.freeze
|
|
4
8
|
ERROR_RESULT = 'ERROR'.freeze
|
|
5
9
|
|
|
6
10
|
attr_reader :xml, :to_h
|
|
7
11
|
|
|
8
12
|
def initialize(xml, xml_parser: SmsTraffic.configuration.xml_parser)
|
|
9
13
|
@xml = xml.gsub(/\s+/, ' ')
|
|
10
|
-
parsed = fetch_value(
|
|
14
|
+
parsed = fetch_value(parse(xml_parser), :reply)
|
|
11
15
|
@to_h = parsed.is_a?(Hash) ? parsed : {}
|
|
12
16
|
end
|
|
13
17
|
|
|
@@ -23,12 +27,20 @@ module SmsTraffic
|
|
|
23
27
|
fetch_value(to_h, :description)
|
|
24
28
|
end
|
|
25
29
|
|
|
30
|
+
# The provider documents `ERROR` but its example replies `ERRO`, so any result other than OK is an error.
|
|
26
31
|
def api_error?
|
|
27
|
-
result
|
|
32
|
+
!result.nil? && result != OK_RESULT
|
|
28
33
|
end
|
|
29
34
|
|
|
30
35
|
private
|
|
31
36
|
|
|
37
|
+
# A body that is not well-formed XML is an empty reply, as before the REXML parser.
|
|
38
|
+
def parse(xml_parser)
|
|
39
|
+
xml_parser.parse(@xml)
|
|
40
|
+
rescue REXML::ParseException
|
|
41
|
+
{}
|
|
42
|
+
end
|
|
43
|
+
|
|
32
44
|
def fetch_value(source, key)
|
|
33
45
|
return unless source.is_a?(Hash)
|
|
34
46
|
|
|
@@ -4,8 +4,14 @@ module SmsTraffic
|
|
|
4
4
|
NOT_FOUND = /no such message/i
|
|
5
5
|
|
|
6
6
|
Entry = Struct.new(
|
|
7
|
-
:sms_id, :status, :error, :submission_date, :send_date, :last_status_change_date,
|
|
7
|
+
:sms_id, :status, :status_reported, :error, :submission_date, :send_date, :last_status_change_date,
|
|
8
|
+
keyword_init: true
|
|
8
9
|
) do
|
|
10
|
+
# @return [Boolean] the provider sent a status element, even an empty one (an intermediate status)
|
|
11
|
+
def status_reported?
|
|
12
|
+
status_reported == true
|
|
13
|
+
end
|
|
14
|
+
|
|
9
15
|
def error?
|
|
10
16
|
!error.nil?
|
|
11
17
|
end
|
|
@@ -13,6 +19,14 @@ module SmsTraffic
|
|
|
13
19
|
def not_found?
|
|
14
20
|
error? && error.match?(NOT_FOUND)
|
|
15
21
|
end
|
|
22
|
+
|
|
23
|
+
def final?
|
|
24
|
+
Status.final?(status)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def delivered?
|
|
28
|
+
Status.delivered?(status)
|
|
29
|
+
end
|
|
16
30
|
end
|
|
17
31
|
|
|
18
32
|
# @return [Array<Entry>]
|
|
@@ -33,7 +47,7 @@ module SmsTraffic
|
|
|
33
47
|
define_method(name) { |sms_id = nil| entry(sms_id)&.public_send(name) }
|
|
34
48
|
end
|
|
35
49
|
|
|
36
|
-
%i[error? not_found?].each do |name|
|
|
50
|
+
%i[error? not_found? final? delivered?].each do |name|
|
|
37
51
|
define_method(name) { |sms_id = nil| entry(sms_id)&.public_send(name) || false }
|
|
38
52
|
end
|
|
39
53
|
|
|
@@ -57,6 +71,10 @@ module SmsTraffic
|
|
|
57
71
|
to_h.empty? ? [] : [to_h]
|
|
58
72
|
end
|
|
59
73
|
|
|
74
|
+
def key?(source, key)
|
|
75
|
+
source.is_a?(Hash) && (source.key?(key.to_s) || source.key?(key.to_sym))
|
|
76
|
+
end
|
|
77
|
+
|
|
60
78
|
def blank_to_nil(value)
|
|
61
79
|
stripped = value&.strip
|
|
62
80
|
stripped unless stripped.nil? || stripped.empty?
|
|
@@ -66,6 +84,7 @@ module SmsTraffic
|
|
|
66
84
|
Entry.new(
|
|
67
85
|
sms_id: fetch_value(source, :sms_id),
|
|
68
86
|
status: fetch_value(source, :status),
|
|
87
|
+
status_reported: key?(source, :status),
|
|
69
88
|
error: blank_to_nil(fetch_value(source, :error)),
|
|
70
89
|
# The provider spells this field "submition_date".
|
|
71
90
|
submission_date: fetch_value(source, :submition_date),
|
data/lib/sms_traffic/client.rb
CHANGED
|
@@ -4,6 +4,7 @@ require 'uri'
|
|
|
4
4
|
require_relative 'client/reply'
|
|
5
5
|
require_relative 'client/deliver_reply'
|
|
6
6
|
require_relative 'client/status_reply'
|
|
7
|
+
require_relative 'client/account_reply'
|
|
7
8
|
|
|
8
9
|
module SmsTraffic
|
|
9
10
|
class Client
|
|
@@ -13,6 +14,10 @@ module SmsTraffic
|
|
|
13
14
|
CONNECTION_ERRORS = [
|
|
14
15
|
SocketError, SystemCallError, IOError, Net::OpenTimeout, Net::ReadTimeout, OpenSSL::SSL::SSLError
|
|
15
16
|
].freeze
|
|
17
|
+
# Failures that prove the request never reached the server.
|
|
18
|
+
UNREACHABLE_ERRORS = [
|
|
19
|
+
SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ENETUNREACH, Net::OpenTimeout
|
|
20
|
+
].freeze
|
|
16
21
|
|
|
17
22
|
class << self
|
|
18
23
|
# @param phone [String] phone number or phone numbers separated by comma
|
|
@@ -23,30 +28,65 @@ module SmsTraffic
|
|
|
23
28
|
# @raise [ConnectionError, HttpError, ApiError]
|
|
24
29
|
def deliver(phone, message, originator = nil, **options)
|
|
25
30
|
originator ||= SmsTraffic.configuration.originator
|
|
26
|
-
|
|
31
|
+
request(DeliverReply, deliver_params(phone, message, originator, **options), read_only: false)
|
|
27
32
|
end
|
|
28
33
|
|
|
29
34
|
# @param sms_ids [Array<String>] sms ids
|
|
30
35
|
# @return [StatusReply]
|
|
31
36
|
# @raise [ConnectionError, HttpError, ApiError]
|
|
32
37
|
def status(*sms_ids)
|
|
33
|
-
|
|
38
|
+
request(StatusReply, status_params(sms_ids), read_only: true)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @return [AccountReply]
|
|
42
|
+
# @raise [ConnectionError, HttpError, ApiError]
|
|
43
|
+
def balance
|
|
44
|
+
request(AccountReply, account_params, read_only: true)
|
|
34
45
|
end
|
|
35
46
|
|
|
36
47
|
private
|
|
37
48
|
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
# Sends the request to the configured host and, when a fallback host is set, repeats it there once if the
|
|
50
|
+
# failure allows it.
|
|
51
|
+
def request(reply_class, form_data, read_only:)
|
|
52
|
+
config = SmsTraffic.configuration
|
|
53
|
+
attempt(reply_class, form_data, config.host)
|
|
54
|
+
rescue Error => e
|
|
55
|
+
raise unless config.fallback_host && retry_on_fallback?(e, read_only:)
|
|
56
|
+
|
|
57
|
+
log("retry on #{config.fallback_host} after #{retry_reason(e)}")
|
|
58
|
+
attempt(reply_class, form_data, config.fallback_host)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Names the root cause and, for an API error, its code; never the message text.
|
|
62
|
+
def retry_reason(error)
|
|
63
|
+
reason = (error.cause&.class || error.class).to_s
|
|
64
|
+
error.is_a?(ApiError) ? "#{reason} #{error.code}" : reason
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def attempt(reply_class, form_data, host)
|
|
68
|
+
reply = reply_class.new(post(form_data, host))
|
|
40
69
|
raise ApiError.new(reply.code, reply.description) if reply.api_error?
|
|
41
70
|
|
|
42
71
|
reply
|
|
43
72
|
end
|
|
44
73
|
|
|
74
|
+
# A read-only request is safe to repeat after any transport failure. A deliver is repeated only when the
|
|
75
|
+
# request never reached the server, as the provider may already have queued the SMS otherwise.
|
|
76
|
+
def retry_on_fallback?(error, read_only:)
|
|
77
|
+
case error
|
|
78
|
+
when ApiError then error.retryable?
|
|
79
|
+
when ConnectionError then read_only || UNREACHABLE_ERRORS.any? { |klass| error.cause.is_a?(klass) }
|
|
80
|
+
when HttpError then read_only && error.status >= 500
|
|
81
|
+
else false
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
45
85
|
# @param form_data [Hash<Symbol, String|Integer>]
|
|
46
86
|
# @return [String] response body
|
|
47
|
-
def post(form_data)
|
|
87
|
+
def post(form_data, host)
|
|
48
88
|
config = SmsTraffic.configuration
|
|
49
|
-
uri = URI.join(
|
|
89
|
+
uri = URI.join(host, ENDPOINT)
|
|
50
90
|
log("request POST #{uri} #{filter(form_data)}")
|
|
51
91
|
|
|
52
92
|
response = perform(uri, form_data, config)
|
|
@@ -94,12 +134,15 @@ module SmsTraffic
|
|
|
94
134
|
end
|
|
95
135
|
|
|
96
136
|
def status_params(sms_ids)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
137
|
+
operation_params('status').merge(sms_id: sms_ids.join(','))
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def account_params
|
|
141
|
+
operation_params('account')
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def operation_params(operation)
|
|
145
|
+
{ login: SmsTraffic.configuration.login, password: SmsTraffic.configuration.password, operation: }
|
|
103
146
|
end
|
|
104
147
|
end
|
|
105
148
|
end
|
|
@@ -3,9 +3,12 @@ require 'nori'
|
|
|
3
3
|
module SmsTraffic
|
|
4
4
|
class Configuration
|
|
5
5
|
ATTRIBUTES = %i[
|
|
6
|
-
login password host originator debug logger open_timeout read_timeout validate_phone xml_parser
|
|
6
|
+
login password host fallback_host originator debug logger open_timeout read_timeout validate_phone xml_parser
|
|
7
7
|
].freeze
|
|
8
8
|
|
|
9
|
+
# Attributes not listed here default to nil.
|
|
10
|
+
DEFAULTS = { debug: false, open_timeout: 10, read_timeout: 30, validate_phone: true }.freeze
|
|
11
|
+
|
|
9
12
|
attr_accessor(*ATTRIBUTES)
|
|
10
13
|
|
|
11
14
|
def initialize
|
|
@@ -13,16 +16,8 @@ module SmsTraffic
|
|
|
13
16
|
end
|
|
14
17
|
|
|
15
18
|
def reset!
|
|
16
|
-
|
|
17
|
-
self.
|
|
18
|
-
self.host = nil
|
|
19
|
-
self.originator = nil
|
|
20
|
-
self.debug = false
|
|
21
|
-
self.logger = nil
|
|
22
|
-
self.open_timeout = 10
|
|
23
|
-
self.read_timeout = 30
|
|
24
|
-
self.validate_phone = true
|
|
25
|
-
self.xml_parser = Nori.new(advanced_typecasting: false)
|
|
19
|
+
ATTRIBUTES.each { |attribute| public_send("#{attribute}=", DEFAULTS[attribute]) }
|
|
20
|
+
self.xml_parser = Nori.new(advanced_typecasting: false, parser: :rexml)
|
|
26
21
|
end
|
|
27
22
|
|
|
28
23
|
def validate!
|
data/lib/sms_traffic/errors.rb
CHANGED
|
@@ -15,7 +15,7 @@ module SmsTraffic
|
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
# HTTP 200 reply whose result is ERROR.
|
|
18
|
+
# HTTP 200 reply whose `result` is not `OK` (`ERROR`, `ERRO`).
|
|
19
19
|
class ApiError < Error
|
|
20
20
|
attr_reader :code, :description
|
|
21
21
|
|
|
@@ -24,5 +24,22 @@ module SmsTraffic
|
|
|
24
24
|
@description = description
|
|
25
25
|
super("#{code}: #{description}")
|
|
26
26
|
end
|
|
27
|
+
|
|
28
|
+
# Temporary server problem: retry later or use the backup host.
|
|
29
|
+
def retryable?
|
|
30
|
+
code.to_i == 1000
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def access_denied?
|
|
34
|
+
[411, 412].include?(code.to_i)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def insufficient_funds?
|
|
38
|
+
code.to_i == 415
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def blocked_phone?
|
|
42
|
+
code.to_i == 432
|
|
43
|
+
end
|
|
27
44
|
end
|
|
28
45
|
end
|
data/lib/sms_traffic/sms.rb
CHANGED
|
@@ -37,6 +37,16 @@ module SmsTraffic
|
|
|
37
37
|
status
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
# @return [Boolean] the stored status is a final provider status
|
|
41
|
+
def final?
|
|
42
|
+
Status.final?(status)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @return [Boolean] the stored status is Delivered
|
|
46
|
+
def delivered?
|
|
47
|
+
Status.delivered?(status)
|
|
48
|
+
end
|
|
49
|
+
|
|
40
50
|
private
|
|
41
51
|
|
|
42
52
|
def mark_sent
|
|
@@ -53,6 +63,9 @@ module SmsTraffic
|
|
|
53
63
|
add_error(entry.error)
|
|
54
64
|
elsif entry&.status
|
|
55
65
|
@status = entry.status.downcase
|
|
66
|
+
elsif entry&.status_reported?
|
|
67
|
+
# An empty status element is an intermediate provider status: keep the current one.
|
|
68
|
+
nil
|
|
56
69
|
else
|
|
57
70
|
add_error("No status returned for sms #{id}")
|
|
58
71
|
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module SmsTraffic
|
|
2
|
+
# Provider delivery statuses (HTTP API v1, operation=status).
|
|
3
|
+
module Status
|
|
4
|
+
INTERMEDIATE = ['', 'Buffered SMSC'].freeze
|
|
5
|
+
DELIVERED = 'Delivered'.freeze
|
|
6
|
+
FINAL = [DELIVERED, 'Non Delivered', 'Rejected', 'Expired', 'Deleted', 'Unknown status'].freeze
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
# @param status [String, nil] provider status, compared case-insensitively
|
|
10
|
+
def final?(status)
|
|
11
|
+
matches?(FINAL, status)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def delivered?(status)
|
|
15
|
+
matches?([DELIVERED], status)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def matches?(list, status)
|
|
21
|
+
value = status.to_s.strip
|
|
22
|
+
list.any? { |item| item.casecmp?(value) }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/sms_traffic/version.rb
CHANGED
data/lib/sms_traffic.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.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Rybolovlev
|
|
@@ -11,19 +11,19 @@ cert_chain: []
|
|
|
11
11
|
date: 2026-11-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
|
-
name:
|
|
14
|
+
name: bigdecimal
|
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
|
16
16
|
requirements:
|
|
17
|
-
- - "
|
|
17
|
+
- - ">="
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '1
|
|
19
|
+
version: '3.1'
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
|
-
- - "
|
|
24
|
+
- - ">="
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '1
|
|
26
|
+
version: '3.1'
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
28
|
name: nori
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -38,6 +38,20 @@ dependencies:
|
|
|
38
38
|
- - "~>"
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
40
|
version: '2.8'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rexml
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: 3.3.9
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: 3.3.9
|
|
41
55
|
description: |-
|
|
42
56
|
Ruby Gem as a SDK for interacting with the SMS Traffic HTTP API (smstraffic.ru/api).
|
|
43
57
|
This gem provides a convenient way to integrate SMS Traffic services into Ruby applications,
|
|
@@ -53,12 +67,14 @@ files:
|
|
|
53
67
|
- README.md
|
|
54
68
|
- lib/sms_traffic.rb
|
|
55
69
|
- lib/sms_traffic/client.rb
|
|
70
|
+
- lib/sms_traffic/client/account_reply.rb
|
|
56
71
|
- lib/sms_traffic/client/deliver_reply.rb
|
|
57
72
|
- lib/sms_traffic/client/reply.rb
|
|
58
73
|
- lib/sms_traffic/client/status_reply.rb
|
|
59
74
|
- lib/sms_traffic/configuration.rb
|
|
60
75
|
- lib/sms_traffic/errors.rb
|
|
61
76
|
- lib/sms_traffic/sms.rb
|
|
77
|
+
- lib/sms_traffic/status.rb
|
|
62
78
|
- lib/sms_traffic/version.rb
|
|
63
79
|
- lib/sms_traffic_sdk.rb
|
|
64
80
|
homepage: https://github.com/golifox/sms_traffic_sdk
|