sms_traffic_sdk 0.0.8 → 0.2.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 +87 -0
- data/README.md +104 -39
- data/lib/sms_traffic/client/account_reply.rb +14 -0
- data/lib/sms_traffic/client/deliver_reply.rb +19 -38
- data/lib/sms_traffic/client/reply.rb +28 -7
- data/lib/sms_traffic/client/status_reply.rb +77 -24
- data/lib/sms_traffic/client.rb +110 -40
- data/lib/sms_traffic/configuration.rb +15 -56
- data/lib/sms_traffic/errors.rb +45 -0
- data/lib/sms_traffic/sms.rb +50 -25
- data/lib/sms_traffic/status.rb +26 -0
- data/lib/sms_traffic/version.rb +1 -1
- data/lib/sms_traffic.rb +7 -9
- data/lib/sms_traffic_sdk.rb +1 -0
- metadata +28 -33
- data/.env.template +0 -4
- data/.fasterer.yml +0 -2
- data/.github/workflows/ci.yml +0 -72
- data/.gitignore +0 -12
- data/.rspec +0 -2
- data/.rubocop.yml +0 -20
- data/.tool-versions +0 -1
- data/Gemfile +0 -17
- data/Rakefile +0 -10
- data/bin/console +0 -18
- data/bin/setup +0 -7
- data/exe/konsierge-sms.rb +0 -2
- data/lib/sms_traffic/client/response.rb +0 -39
- data/sms_traffic_sdk.gemspec +0 -31
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7073f3fdf9537cf98008d5c6fac2bbc28457fbe4ef05852ccfd4e3b3a4606fbc
|
|
4
|
+
data.tar.gz: 7ad131b8ca90beebf7d4e227fa4031655bdc49d7e5db4f12c7ee11763cde8e0a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f22009b2e84816ea0ad7294fd80ec8810dfc99c4ad46a7c66e9f9604f837c31fe5d6ae996639e776cb79501523b50257d20390069412993c704b2456ed3b5fb7
|
|
7
|
+
data.tar.gz: 4dc45cb30d25dd521d716f16bcb7a4ae530ee9fa3b8d14f0b8ec0cfde6a479c81d41268b1015a7518bdd16d79a11fc4f8c5663d2e8266dc7f34c1d0415202843
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0 - 2026-11-03
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `Configuration#fallback_host` (default `nil`). When set, the client repeats a request once on that host after a
|
|
8
|
+
connection failure that proves the server was not reached (`SocketError`, `Errno::ECONNREFUSED`,
|
|
9
|
+
`Errno::EHOSTUNREACH`, `Errno::ENETUNREACH`, `Net::OpenTimeout`) or after API error 1000. `Client.status` and
|
|
10
|
+
`Client.balance` also retry after any `ConnectionError` and HTTP 5xx; `Client.deliver` never does, because the
|
|
11
|
+
provider may already have queued the SMS. With `debug`, the retry is logged in one line without credentials.
|
|
12
|
+
- `Client.balance` returns an `AccountReply`; `AccountReply#balance` is a `BigDecimal` (units as sent by the
|
|
13
|
+
provider). `bigdecimal` is a runtime dependency.
|
|
14
|
+
- `SmsTraffic::Status` (`FINAL`, `INTERMEDIATE`), `StatusReply::Entry#final?` / `#delivered?`, the same methods on
|
|
15
|
+
`StatusReply` (optional `sms_id`) and `Sms#final?` / `#delivered?`.
|
|
16
|
+
- `ApiError#retryable?`, `#access_denied?`, `#insufficient_funds?` and `#blocked_phone?`.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- A provider reply with `<result>ERRO</result>` (the spelling in the provider's own example) is now an
|
|
21
|
+
`ApiError`. `Reply#api_error?` is true for any present `result` other than `OK`.
|
|
22
|
+
- `Sms#update_status` no longer records "No status returned" for an empty `<status>` element (an intermediate
|
|
23
|
+
provider status); the status stays `sent`. `StatusReply::Entry#status_reported?` tells an empty status from an
|
|
24
|
+
absent one.
|
|
25
|
+
|
|
26
|
+
## 0.1.0 - 2026-11-03
|
|
27
|
+
|
|
28
|
+
### Breaking changes
|
|
29
|
+
|
|
30
|
+
- Ruby 3.1 or newer is required.
|
|
31
|
+
- Zeitwerk is no longer a dependency; files are loaded with `require_relative`.
|
|
32
|
+
- `SmsTraffic::Configuration` is a plain class, not a `Singleton`. Defaults changed: `logger` is `nil` (was `$stdout`),
|
|
33
|
+
new `open_timeout` (10) and `read_timeout` (30). `originator` is no longer required by `configure`.
|
|
34
|
+
`Configuration#values_at` is removed.
|
|
35
|
+
- `SmsTraffic::Client::Response` is removed. `Client.deliver` returns a `DeliverReply` and `Client.status` a
|
|
36
|
+
`StatusReply` directly, and raise `SmsTraffic::ConnectionError`, `SmsTraffic::HttpError` or `SmsTraffic::ApiError`
|
|
37
|
+
(all `SmsTraffic::Error`) instead of returning a failed response.
|
|
38
|
+
- `Reply#hash` no longer returns the parsed reply (it is `Object#hash` again); use `Reply#to_h`.
|
|
39
|
+
- `DeliverReply#ok?` is strictly boolean and requires a queued message. `#sms_ids` is an Array of every sms id (it was
|
|
40
|
+
a Hash keyed by phone). `#message_infos` is replaced by `#messages`. `#error_description` reads
|
|
41
|
+
`"Sms has not been enqueued: <description>"`.
|
|
42
|
+
- `StatusReply#sms_array` is replaced by `#entries` (`StatusReply::Entry`); `#entry`, `#error?`, `#not_found?`
|
|
43
|
+
and `#send_date` are new.
|
|
44
|
+
- `Sms#response` is replaced by `Sms#reply`. `Sms#update_status` always returns the status String, and `Sms#deliver`
|
|
45
|
+
records `SmsTraffic::Error`s in `errors` instead of the HTTP code.
|
|
46
|
+
- The `konsierge-sms.rb` executable is removed.
|
|
47
|
+
- The default XML parser no longer typecasts (`Nori.new(advanced_typecasting: false)`): dates and `true` stay
|
|
48
|
+
provider strings.
|
|
49
|
+
- `nori` 2.8 or newer is required.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- `Sms` phone validation no longer accepts a newline-separated value (`\A`/`\z` anchors).
|
|
54
|
+
- `Client.status` with several ids sends a flat comma-separated `sms_id` list.
|
|
55
|
+
- The client builds its URI from the current `host` on every request instead of memoizing the first one.
|
|
56
|
+
- `open_timeout` and `read_timeout` are applied to every request.
|
|
57
|
+
- `Reply` no longer mutates (or fails on) frozen input.
|
|
58
|
+
- A multi-part SMS keeps every `sms_id`; a single `<message_info>` with a phone is parsed.
|
|
59
|
+
- `require 'sms_traffic_sdk'` works with `Bundler.require`.
|
|
60
|
+
- Loading the gem without a preloaded `StringIO` no longer fails with `NameError` (nori 2.7.0 missed
|
|
61
|
+
`require 'stringio'`; fixed in nori 2.8).
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
|
|
65
|
+
- CI runs lint and RSpec on Ruby 3.1-3.4, smoke-tests the built gem in a clean Ruby process, uploads Cobertura
|
|
66
|
+
coverage to Codecov, and publishes to RubyGems through trusted publishing (OIDC) from a published release whose tag
|
|
67
|
+
matches the gem version. The gem package contains only `lib`, `README.md`, `LICENSE` and `CHANGELOG.md`.
|
|
68
|
+
|
|
69
|
+
### Security
|
|
70
|
+
|
|
71
|
+
- `Net::HTTP#set_debug_output` is no longer used, so the login, password and SMS text are never written to the logger.
|
|
72
|
+
Debug logging emits one request line with `login`, `password` and `message` filtered, and one response line with
|
|
73
|
+
the HTTP status and body size only.
|
|
74
|
+
|
|
75
|
+
### Migration
|
|
76
|
+
|
|
77
|
+
- Replace `Client.deliver(...).reply` / `.success?` with the returned reply and `rescue SmsTraffic::Error`.
|
|
78
|
+
- Replace `reply.hash` with `reply.to_h`, and `sms.response` with `sms.reply`.
|
|
79
|
+
- Treat `reply.sms_ids` as an Array; use `reply.messages` for per-phone data.
|
|
80
|
+
- Set `config.logger` explicitly if you relied on the `$stdout` default with `debug = true`.
|
|
81
|
+
- `Client.deliver` and `Client.status` no longer raise `SocketError`, `SystemCallError` (`Errno::*`), `IOError`,
|
|
82
|
+
`Net::OpenTimeout`, `Net::ReadTimeout` or `OpenSSL::SSL::SSLError`; rescue `SmsTraffic::ConnectionError` (the
|
|
83
|
+
original exception is in `cause`). `Sms#deliver` and `Sms#update_status` already rescue `SmsTraffic::Error` into
|
|
84
|
+
`errors`.
|
|
85
|
+
- Replace `response.as_json` / `sms.response.as_json` with `reply.to_h` / `sms.reply&.to_h`.
|
|
86
|
+
- Replace `response.success?` checks with `rescue SmsTraffic::Error`.
|
|
87
|
+
- Replace `reply.hash["send_date"]` / `reply.hash["status"]` with `reply.send_date` / `reply.status`.
|
data/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SmsTraffic
|
|
2
2
|
|
|
3
3
|
[](https://badge.fury.io/rb/sms_traffic_sdk)
|
|
4
|
-
[](https://codeclimate.com/github/golifox/
|
|
5
|
-
[](https://codeclimate.com/github/golifox/sms_traffic_sdk/maintainability)
|
|
5
|
+
[](https://codecov.io/github/golifox/sms_traffic_sdk)
|
|
6
|
+
[](https://github.com/golifox/sms_traffic_sdk/actions/workflows/ci.yml)
|
|
7
7
|
|
|
8
8
|
Ruby Gem as a software development kit (SDK) that facilitates interaction with the SMS Traffic HTTP API (smstraffic.ru/api).
|
|
9
9
|
This gem provides a convenient wrapper to integrate SMS Traffic services within Ruby applications, allowing for easy
|
|
@@ -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
|
|
|
@@ -37,63 +39,126 @@ Or install it yourself as:
|
|
|
37
39
|
Define settings:
|
|
38
40
|
|
|
39
41
|
```ruby
|
|
40
|
-
# initializers/sms_traffic.rb
|
|
41
|
-
require '
|
|
42
|
+
# config/initializers/sms_traffic.rb
|
|
43
|
+
require 'sms_traffic'
|
|
42
44
|
|
|
43
45
|
SmsTraffic.configure do |config|
|
|
44
|
-
config.login
|
|
45
|
-
config.password
|
|
46
|
-
config.
|
|
47
|
-
config.
|
|
48
|
-
config.
|
|
49
|
-
config.
|
|
46
|
+
config.login = 'login'
|
|
47
|
+
config.password = 'password'
|
|
48
|
+
config.host = 'https://api.smstraffic.ru'
|
|
49
|
+
config.fallback_host = 'https://api2.smstraffic.ru' # optional backup host, default nil
|
|
50
|
+
config.originator = 'default_originator' # optional, can be given per SMS
|
|
51
|
+
config.open_timeout = 10 # seconds, default 10
|
|
52
|
+
config.read_timeout = 30 # seconds, default 30
|
|
53
|
+
config.validate_phone = true # default true: 11+ digits
|
|
54
|
+
config.debug = true # default false
|
|
55
|
+
config.logger = Rails.logger # required when debug is true
|
|
50
56
|
end
|
|
51
57
|
```
|
|
52
58
|
|
|
53
|
-
|
|
59
|
+
`login`, `password` and `host` are required; `logger` is required when `debug` is enabled.
|
|
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]`.
|
|
62
|
+
|
|
63
|
+
Send an SMS:
|
|
64
|
+
|
|
54
65
|
```ruby
|
|
55
|
-
sms = SmsTraffic::Sms.new('
|
|
56
|
-
# => #<SmsTraffic::Sms:0x0000 @errors=[], @message="text", @originator="default_originator", @phone="phone", @status="not-sent">
|
|
66
|
+
sms = SmsTraffic::Sms.new('79991112233', 'text', originator: 'my_brand') # originator defaults to the configured one
|
|
57
67
|
|
|
68
|
+
sms.deliver # => true when the provider enqueued it, false otherwise
|
|
69
|
+
sms.status # => 'sent' ('not-sent' until delivered)
|
|
70
|
+
sms.id # => '123456789' (the first sms id of the reply)
|
|
71
|
+
sms.reply # => #<SmsTraffic::Client::DeliverReply>
|
|
72
|
+
sms.errors # => [] (messages of failed attempts)
|
|
58
73
|
```
|
|
59
74
|
|
|
60
|
-
|
|
75
|
+
`deliver` and `update_status` never raise on provider or network failures; they append the message to `errors`
|
|
76
|
+
and keep the current `status`.
|
|
77
|
+
|
|
78
|
+
Refresh the status:
|
|
79
|
+
|
|
61
80
|
```ruby
|
|
62
|
-
sms.
|
|
63
|
-
# => true
|
|
64
|
-
sms.id # get sms id
|
|
65
|
-
# => '123456789'
|
|
81
|
+
sms.update_status # => 'delivered' (downcased provider status, or the unchanged status on failure)
|
|
66
82
|
```
|
|
67
83
|
|
|
68
|
-
|
|
84
|
+
Use the client directly. It raises typed errors:
|
|
85
|
+
|
|
69
86
|
```ruby
|
|
70
|
-
|
|
71
|
-
# =>
|
|
72
|
-
|
|
73
|
-
# => '
|
|
87
|
+
reply = SmsTraffic::Client.deliver('79991112233,79261112233', 'text', 'my_brand')
|
|
88
|
+
reply.ok? # => true
|
|
89
|
+
reply.sms_ids # => ['8287366071', '8287366073']
|
|
90
|
+
reply.messages # => [{ phone: '79991112233', sms_id: '8287366071', push_id: 'a' }, ...]
|
|
91
|
+
reply.sms_id('79261112233') # first sms id for the phone
|
|
92
|
+
|
|
93
|
+
reply = SmsTraffic::Client.status('8287713301', '8287713303')
|
|
94
|
+
reply.entries # => [#<struct SmsTraffic::Client::StatusReply::Entry sms_id="8287713301", status="Delivered", ...>]
|
|
95
|
+
reply.statuses # => { '8287713301' => 'Delivered' }
|
|
96
|
+
reply.errors # => { '8287713303' => 'no such message or this message does not belong to you' }
|
|
97
|
+
reply.entry('8287713303').not_found? # => true
|
|
98
|
+
reply.status('8287713301') # => 'Delivered'; the id may be omitted when there is one entry
|
|
74
99
|
```
|
|
75
100
|
|
|
76
|
-
|
|
101
|
+
Each entry has `sms_id`, `status`, `error`, `submission_date`, `send_date`, `last_status_change_date`
|
|
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
|
+
|
|
77
111
|
```ruby
|
|
78
|
-
|
|
79
|
-
reply
|
|
80
|
-
# =>
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
# =>
|
|
85
|
-
#
|
|
86
|
-
|
|
87
|
-
# "send_date"=>"2024-07-15 11:36:25",
|
|
88
|
-
# "last_status_change_date"=>"2024-07-15 11:36:25",
|
|
89
|
-
# "sms_id"=>"865149797164801235",
|
|
90
|
-
# "status"=>"Delivered"}
|
|
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
|
+
```
|
|
91
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
|
+
### Errors
|
|
140
|
+
|
|
141
|
+
All errors inherit from `SmsTraffic::Error`:
|
|
142
|
+
|
|
143
|
+
- `SmsTraffic::ConnectionError` - network, timeout or TLS failure (the original exception is `cause`).
|
|
144
|
+
- `SmsTraffic::HttpError` - non-2xx response; has `status` and `body`.
|
|
145
|
+
- `SmsTraffic::ApiError` - the provider replied `ERROR` (or `ERRO`); has `code` and `description`, and the
|
|
146
|
+
predicates `retryable?` (1000), `access_denied?` (411, 412), `insufficient_funds?` (415) and `blocked_phone?` (432).
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
begin
|
|
150
|
+
SmsTraffic::Client.deliver('79991112233', 'text')
|
|
151
|
+
rescue SmsTraffic::ApiError => e
|
|
152
|
+
e.code # => '401'
|
|
153
|
+
e.description # => 'login param is missing'
|
|
154
|
+
rescue SmsTraffic::Error => e
|
|
155
|
+
e.message
|
|
156
|
+
end
|
|
92
157
|
```
|
|
93
158
|
|
|
94
159
|
# Contributing
|
|
95
160
|
|
|
96
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/golifox/sms_traffic_sdk.
|
|
161
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/golifox/sms_traffic_sdk.
|
|
97
162
|
|
|
98
163
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
99
164
|
|
|
@@ -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,62 +1,43 @@
|
|
|
1
1
|
module SmsTraffic
|
|
2
2
|
class Client
|
|
3
|
-
class DeliverReply <
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
class DeliverReply < Reply
|
|
4
|
+
# @return [Boolean] result is OK and at least one message was queued
|
|
6
5
|
def ok?
|
|
7
|
-
result ==
|
|
6
|
+
result == OK_RESULT && !messages.empty?
|
|
8
7
|
end
|
|
9
8
|
|
|
10
9
|
def error?
|
|
11
10
|
!ok?
|
|
12
11
|
end
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def code
|
|
19
|
-
fetch_value(hash, :code)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def description
|
|
23
|
-
fetch_value(hash, :description)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def message_infos
|
|
27
|
-
fetch_value(hash, :message_infos)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def sms_id(phone = nil)
|
|
31
|
-
message_info = fetch_value(message_infos, :message_info)
|
|
32
|
-
return fetch_value(message_info, :sms_id) if message_info.is_a?(Hash) || message_info.nil?
|
|
33
|
-
|
|
34
|
-
if phone
|
|
35
|
-
fetch_value(find_message_info(phone), :sms_id)
|
|
36
|
-
elsif message_info.is_a?(Array)
|
|
37
|
-
fetch_value(message_info.first, :sms_id)
|
|
13
|
+
# @return [Array<Hash>] queued messages as { phone:, sms_id:, push_id: }
|
|
14
|
+
def messages
|
|
15
|
+
@messages ||= message_infos.map do |info|
|
|
16
|
+
{ phone: fetch_value(info, :phone), sms_id: fetch_value(info, :sms_id), push_id: fetch_value(info, :push_id) }
|
|
38
17
|
end
|
|
39
18
|
end
|
|
40
19
|
|
|
20
|
+
# @return [Array<String>] every sms id in reply order
|
|
41
21
|
def sms_ids
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return [fetch_value(message_info, :sms_id)] if message_info&.size == 1
|
|
45
|
-
return [] if message_info.nil?
|
|
22
|
+
messages.filter_map { |message| message[:sms_id] }
|
|
23
|
+
end
|
|
46
24
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
25
|
+
# @param phone [String, nil]
|
|
26
|
+
# @return [String, nil] the first sms id, or the first one for the phone
|
|
27
|
+
def sms_id(phone = nil)
|
|
28
|
+
message = phone ? messages.find { |item| item[:phone].to_s == phone.to_s } : messages.first
|
|
29
|
+
message&.fetch(:sms_id)
|
|
50
30
|
end
|
|
51
31
|
|
|
52
32
|
def error_description
|
|
53
|
-
"
|
|
33
|
+
"Sms has not been enqueued: #{description}" if error?
|
|
54
34
|
end
|
|
55
35
|
|
|
56
36
|
private
|
|
57
37
|
|
|
58
|
-
def
|
|
59
|
-
fetch_value(
|
|
38
|
+
def message_infos
|
|
39
|
+
info = fetch_value(fetch_value(to_h, :message_infos), :message_info)
|
|
40
|
+
info.is_a?(Array) ? info : [info].compact
|
|
60
41
|
end
|
|
61
42
|
end
|
|
62
43
|
end
|
|
@@ -1,19 +1,40 @@
|
|
|
1
1
|
module SmsTraffic
|
|
2
2
|
class Client
|
|
3
3
|
class Reply
|
|
4
|
-
|
|
4
|
+
OK_RESULT = 'OK'.freeze
|
|
5
|
+
ERROR_RESULT = 'ERROR'.freeze
|
|
6
|
+
|
|
7
|
+
attr_reader :xml, :to_h
|
|
5
8
|
|
|
6
9
|
def initialize(xml, xml_parser: SmsTraffic.configuration.xml_parser)
|
|
7
|
-
@xml
|
|
8
|
-
|
|
10
|
+
@xml = xml.gsub(/\s+/, ' ')
|
|
11
|
+
parsed = fetch_value(xml_parser.parse(@xml), :reply)
|
|
12
|
+
@to_h = parsed.is_a?(Hash) ? parsed : {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def result
|
|
16
|
+
fetch_value(to_h, :result)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def code
|
|
20
|
+
fetch_value(to_h, :code)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def description
|
|
24
|
+
fetch_value(to_h, :description)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The provider documents `ERROR` but its example replies `ERRO`, so any result other than OK is an error.
|
|
28
|
+
def api_error?
|
|
29
|
+
!result.nil? && result != OK_RESULT
|
|
9
30
|
end
|
|
10
31
|
|
|
11
32
|
private
|
|
12
33
|
|
|
13
|
-
def fetch_value(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
34
|
+
def fetch_value(source, key)
|
|
35
|
+
return unless source.is_a?(Hash)
|
|
36
|
+
|
|
37
|
+
source.fetch(key.to_s) { source[key.to_sym] }
|
|
17
38
|
end
|
|
18
39
|
end
|
|
19
40
|
end
|
|
@@ -1,43 +1,96 @@
|
|
|
1
1
|
module SmsTraffic
|
|
2
2
|
class Client
|
|
3
3
|
class StatusReply < Reply
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
NOT_FOUND = /no such message/i
|
|
5
|
+
|
|
6
|
+
Entry = Struct.new(
|
|
7
|
+
:sms_id, :status, :status_reported, :error, :submission_date, :send_date, :last_status_change_date,
|
|
8
|
+
keyword_init: true
|
|
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
|
+
|
|
15
|
+
def error?
|
|
16
|
+
!error.nil?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def not_found?
|
|
20
|
+
error? && error.match?(NOT_FOUND)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def final?
|
|
24
|
+
Status.final?(status)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def delivered?
|
|
28
|
+
Status.delivered?(status)
|
|
29
|
+
end
|
|
6
30
|
end
|
|
7
31
|
|
|
8
|
-
|
|
9
|
-
|
|
32
|
+
# @return [Array<Entry>]
|
|
33
|
+
def entries
|
|
34
|
+
@entries ||= entry_sources.map { |source| build_entry(source) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @param sms_id [String, nil] required unless the reply has exactly one entry
|
|
38
|
+
# @return [Entry, nil]
|
|
39
|
+
def entry(sms_id = nil)
|
|
40
|
+
return entries.find { |item| item.sms_id.to_s == sms_id.to_s } if sms_id
|
|
41
|
+
return entries.first if entries.size == 1
|
|
42
|
+
|
|
43
|
+
raise ArgumentError, 'sms_id is required'
|
|
44
|
+
end
|
|
10
45
|
|
|
11
|
-
|
|
46
|
+
%i[status error send_date].each do |name|
|
|
47
|
+
define_method(name) { |sms_id = nil| entry(sms_id)&.public_send(name) }
|
|
48
|
+
end
|
|
12
49
|
|
|
13
|
-
|
|
50
|
+
%i[error? not_found? final? delivered?].each do |name|
|
|
51
|
+
define_method(name) { |sms_id = nil| entry(sms_id)&.public_send(name) || false }
|
|
14
52
|
end
|
|
15
53
|
|
|
16
|
-
# @return [Hash]
|
|
17
|
-
# @example
|
|
18
|
-
#
|
|
19
|
-
# {
|
|
20
|
-
# '1234567890': 'DELIVERED',
|
|
21
|
-
# '0987654321': 'FAILED'
|
|
22
|
-
# }
|
|
54
|
+
# @return [Hash{String => String}] sms_id => status, without nil values
|
|
23
55
|
def statuses
|
|
24
|
-
|
|
25
|
-
hash[fetch_value(sms, :sms_id)] = fetch_value(sms, :status)
|
|
26
|
-
end.compact
|
|
56
|
+
entries.to_h { |item| [item.sms_id, item.status] }.compact
|
|
27
57
|
end
|
|
28
58
|
|
|
29
|
-
|
|
30
|
-
|
|
59
|
+
# @return [Hash{String => String}] sms_id => error, without nil values
|
|
60
|
+
def errors
|
|
61
|
+
entries.to_h { |item| [item.sms_id, item.error] }.compact
|
|
62
|
+
end
|
|
31
63
|
|
|
32
|
-
|
|
64
|
+
private
|
|
33
65
|
|
|
34
|
-
|
|
66
|
+
def entry_sources
|
|
67
|
+
sms = fetch_value(to_h, :sms)
|
|
68
|
+
return sms if sms.is_a?(Array)
|
|
69
|
+
return [sms] if sms.is_a?(Hash)
|
|
70
|
+
|
|
71
|
+
to_h.empty? ? [] : [to_h]
|
|
35
72
|
end
|
|
36
73
|
|
|
37
|
-
def
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
74
|
+
def key?(source, key)
|
|
75
|
+
source.is_a?(Hash) && (source.key?(key.to_s) || source.key?(key.to_sym))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def blank_to_nil(value)
|
|
79
|
+
stripped = value&.strip
|
|
80
|
+
stripped unless stripped.nil? || stripped.empty?
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def build_entry(source)
|
|
84
|
+
Entry.new(
|
|
85
|
+
sms_id: fetch_value(source, :sms_id),
|
|
86
|
+
status: fetch_value(source, :status),
|
|
87
|
+
status_reported: key?(source, :status),
|
|
88
|
+
error: blank_to_nil(fetch_value(source, :error)),
|
|
89
|
+
# The provider spells this field "submition_date".
|
|
90
|
+
submission_date: fetch_value(source, :submition_date),
|
|
91
|
+
send_date: fetch_value(source, :send_date),
|
|
92
|
+
last_status_change_date: fetch_value(source, :last_status_change_date)
|
|
93
|
+
)
|
|
41
94
|
end
|
|
42
95
|
end
|
|
43
96
|
end
|