smsru_ruby 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d8d52bb804fe21a23adffdcc6a2a5cea8ca864042f517b9f1258e95688d3a48c
4
+ data.tar.gz: f9e9c5cbd4ea6980885b589f07bda2b7f12bbe0dcdff2af559adfe922e4c979d
5
+ SHA512:
6
+ metadata.gz: a091a855d10c251fa1891fb440c89ccaaac9d74105006b2211a1325ac92de301cd68b00198f7e33198a4535417b3d0eddcdf64fc0a9a2f3a53ad36a7cb7d1567
7
+ data.tar.gz: 65fbe47c1243a1580795da2833df5697f628b4c9d10ece5793d1d7ac7214468468c29a9b3d31e805f1e850e9dc752b159515a30d34248bc278f206a9f672582b
data/.yardopts ADDED
@@ -0,0 +1,6 @@
1
+ --no-private
2
+ --readme README.md
3
+ lib/**/*.rb
4
+ -
5
+ CHANGELOG.md
6
+ LICENSE.txt
data/CHANGELOG.md ADDED
@@ -0,0 +1,42 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [1.0.0] - 2026-06-26
11
+
12
+ First public release. A Ruby port of the official SMS.ru PHP library covering the
13
+ same API, reworked to be idiomatic Ruby. How it differs from the original:
14
+
15
+ - **Idiomatic, namespaced API** instead of flat `get_*`/`add_*` methods: account
16
+ reads under `client.my` (`#balance`, `#limit`, `#free_limit`, `#senders`),
17
+ credential check via `client.auth.ok?`, plus `client.stoplist`,
18
+ `client.callbacks`, and `client.callcheck` sub-resources. Keyword arguments for
19
+ every optional send parameter, plus a per-client `from` default.
20
+ - **Typed, immutable `Data` results** that separate *operation outcome* from
21
+ *delivery state*: `#ok?` plus `#error_code`/`#error_text` on rejected
22
+ `Sms`/`CostItem` entries; `#delivered?`/`#pending?`/`#failed?`/`#found?` and
23
+ named `SmsRu::Statuses` constants for the delivery `status_code` on `Status`
24
+ and webhook events; `#ok?`/`#ok`/`#failed` collection helpers on `SendResult`
25
+ and `Cost`; plus `#confirmed?` and `#available_today`. No raw decoded JSON or
26
+ magic numbers.
27
+ - **Typed error hierarchy** under `SmsRu::Error` (`AuthError`,
28
+ `InsufficientFundsError`, `ResponseError`, `ConnectionError`) — errors are
29
+ raised, not returned as status codes you have to inspect.
30
+ - **First-class inbound webhooks**: `SmsRu::Webhook.parse` decodes the callback
31
+ POST into typed events (`SmsRu::Events::SmsStatus`, `CallcheckStatus`, `Test`,
32
+ `Unknown`), and `SmsRu::Webhook.valid?` verifies the signature.
33
+ - **Zero runtime dependencies** (Ruby stdlib only, no curl), TLS verified by
34
+ default, with configurable `timeout`, `retries`, global `test` mode, and an
35
+ optional `logger`.
36
+ - **Ships RBS type signatures** (`sig/`) checked at 100% coverage under Steep's
37
+ strict profile and verified against the test suite at runtime (`rbs test`);
38
+ SMS.ru's loosely-typed JSON is normalized to the declared types at the parse
39
+ boundary, so result objects never surface raw wire values.
40
+
41
+ [Unreleased]: https://github.com/svyatov/smsru_ruby/compare/v1.0.0...HEAD
42
+ [1.0.0]: https://github.com/svyatov/smsru_ruby/releases/tag/v1.0.0
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Leonid Svyatov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,376 @@
1
+ # smsru_ruby
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/smsru_ruby.svg)](https://rubygems.org/gems/smsru_ruby)
4
+ [![CI](https://github.com/svyatov/smsru_ruby/actions/workflows/main.yml/badge.svg)](https://github.com/svyatov/smsru_ruby/actions/workflows/main.yml)
5
+ [![codecov](https://codecov.io/gh/svyatov/smsru_ruby/branch/main/graph/badge.svg)](https://codecov.io/gh/svyatov/smsru_ruby)
6
+ [![Documentation](https://img.shields.io/badge/docs-rubydoc.info-blue.svg)](https://rubydoc.info/gems/smsru_ruby)
7
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.2-CC342D.svg)](https://www.ruby-lang.org)
8
+ [![Types: RBS](https://img.shields.io/badge/types-RBS-8A2BE2.svg)](https://github.com/svyatov/smsru_ruby/tree/main/sig)
9
+
10
+ A modern, **dependency-free**, **fully typed** Ruby client for the [SMS.ru](https://sms.ru) HTTP API —
11
+ typed results, typed errors, shipped RBS signatures, and first-class webhooks.
12
+
13
+ It is a clean, idiomatic Ruby port of the official [SMS.ru PHP library](https://sms.ru/php):
14
+ send single or bulk SMS, schedule delivery, check cost and delivery status, verify
15
+ users by phone call, inspect your balance/limits/senders, manage the stoplist, and
16
+ register delivery callbacks — all returning typed, immutable result objects and
17
+ raising typed errors.
18
+
19
+ ## Why smsru_ruby?
20
+
21
+ - **Zero runtime dependencies** — only Ruby's standard library (`net/http`, `json`, `openssl`).
22
+ - **Fully typed** — immutable `Data` result objects, not raw hashes, plus a typed error hierarchy: `rescue SmsRu::Error` catches everything.
23
+ - **RBS signatures shipped** (`sig/`) and Steep-checked — type-check your integration out of the box.
24
+ - **First-class webhooks** — parse signed delivery and call-authorization callbacks into typed events; the signature is verified in **constant time** (timing-attack safe).
25
+ - **Secret-safe by default** — TLS verified; the optional logger never logs your `api_id`, phone numbers, or message text. Configurable timeout and transport retries.
26
+ - **Outcome vs. delivery state** — two distinct ideas, each with its own predicates (`ok?` vs. `delivered?`/`pending?`/`failed?`), never conflated.
27
+ - **100% test & documentation coverage, enforced in CI** across Ruby 3.2–4.0.
28
+
29
+ ## What's covered
30
+
31
+ The full SMS.ru API, mapped to an idiomatic Ruby surface:
32
+
33
+ | Capability | Method |
34
+ | --- | --- |
35
+ | Send — single, bulk, or per-number text | `client.deliver` |
36
+ | Price a message before sending | `client.cost` |
37
+ | Delivery status, with state predicates | `client.status` |
38
+ | Verify by flash call (outbound) | `client.call` |
39
+ | Verify by callcheck (inbound) | `client.callcheck` |
40
+ | Balance, limits, free limit, senders | `client.my` |
41
+ | Validate credentials | `client.auth.ok?` |
42
+ | Stoplist — add, remove, list | `client.stoplist` |
43
+ | Webhook URLs — add, remove, list | `client.callbacks` |
44
+ | Parse & verify incoming webhooks | `SmsRu::Webhook` |
45
+
46
+ ## Table of contents
47
+
48
+ - [Why smsru_ruby?](#why-smsru_ruby)
49
+ - [What's covered](#whats-covered)
50
+ - [Supported Ruby versions](#supported-ruby-versions)
51
+ - [Installation](#installation)
52
+ - [Quick start](#quick-start)
53
+ - [Configuration](#configuration)
54
+ - [Sending messages](#sending-messages)
55
+ - [Cost and status](#cost-and-status)
56
+ - [Verify by phone call](#verify-by-phone-call)
57
+ - [Account information](#account-information)
58
+ - [Stoplist](#stoplist)
59
+ - [Callbacks (webhooks)](#callbacks-webhooks)
60
+ - [Error handling](#error-handling)
61
+ - [Development](#development)
62
+ - [Recording test cassettes](#recording-test-cassettes)
63
+ - [License](#license)
64
+
65
+ ## Supported Ruby versions
66
+
67
+ Ruby **3.2+** (the result objects use [`Data`](https://docs.ruby-lang.org/en/3.2/Data.html)).
68
+ CI runs against `ruby-head`, `4.0`, `3.4`, `3.3`, and `3.2`.
69
+
70
+ ## Installation
71
+
72
+ ```ruby
73
+ # Gemfile
74
+ gem "smsru_ruby"
75
+ ```
76
+
77
+ ```sh
78
+ bundle install
79
+ # or
80
+ gem install smsru_ruby
81
+ ```
82
+
83
+ ```ruby
84
+ require "smsru_ruby"
85
+ ```
86
+
87
+ ## Quick start
88
+
89
+ ```ruby
90
+ client = SmsRu.new("YOUR_API_ID")
91
+
92
+ result = client.deliver("79991234567", "Hello from Ruby!")
93
+ result.messages.first.sms_id # => "000000-10000000"
94
+ client.my.balance # => 4762.58
95
+ ```
96
+
97
+ Get your `api_id` in the SMS.ru dashboard under
98
+ [Settings → API](https://sms.ru/?panel=api).
99
+
100
+ ## Configuration
101
+
102
+ ```ruby
103
+ SmsRu.new(
104
+ "YOUR_API_ID",
105
+ timeout: 30, # open/read timeout in seconds (default: 30)
106
+ test: false, # when true, every `deliver` defaults to test mode (no charge)
107
+ retries: 5, # retries on transport failure; 0 disables (default: 5, matching the PHP lib)
108
+ from: "MyCompany", # default sender name for `deliver` (override per call)
109
+ logger: Logger.new($stdout) # optional; logs the request path + transport failures
110
+ )
111
+ ```
112
+
113
+ Retries apply only to transport-level problems (timeouts, refused connections).
114
+ API errors are never retried — they are raised immediately.
115
+
116
+ `from` is a per-client default so you don't repeat your sender name on every call;
117
+ a per-call `from:` always wins. The `logger` logs only the request path and
118
+ transport failures — never your `api_id`, phone numbers, or message text.
119
+
120
+ ## Sending messages
121
+
122
+ `#deliver` accepts the recipient(s) in three shapes:
123
+
124
+ ```ruby
125
+ # 1. One number
126
+ client.deliver("79991234567", "Hi there")
127
+
128
+ # 2. Same text to many numbers (Array)
129
+ client.deliver(["79991234567", "79991234568"], "Hi everyone")
130
+
131
+ # 3. A different text per number (Hash — do not pass a separate text).
132
+ # Use braces so Ruby treats it as a positional Hash, not keyword arguments.
133
+ client.deliver({
134
+ "79991234567" => "Hi Alice",
135
+ "79991234568" => "Hi Bob"
136
+ })
137
+ ```
138
+
139
+ Optional keyword arguments (all optional):
140
+
141
+ ```ruby
142
+ client.deliver(
143
+ "79991234567", "Hi",
144
+ from: "MyCompany", # approved sender name
145
+ time: Time.now.to_i + 3600, # scheduled send (UNIX time, up to 2 months ahead)
146
+ ttl: 60, # message lifetime in minutes (1–1440)
147
+ daytime: true, # defer night-time sends to the recipient's daytime
148
+ translit: true, # transliterate Cyrillic to Latin
149
+ test: true, # test mode for this call (overrides the client default)
150
+ ip: "192.0.2.1", # end-user IP (for auth-code anti-fraud)
151
+ partner_id: 12345 # partner program id
152
+ )
153
+ ```
154
+
155
+ The result is a `SmsRu::SendResult`. Individual recipients can fail even when the
156
+ overall request succeeds, so inspect each message:
157
+
158
+ ```ruby
159
+ result = client.deliver(["79991234567", "74993221627"], "Hi")
160
+ result.balance # => 4122.56
161
+ result.messages.each do |sms|
162
+ if sms.ok?
163
+ puts "#{sms.phone}: sent as #{sms.sms_id}"
164
+ else
165
+ puts "#{sms.phone}: rejected (#{sms.error_code}) #{sms.error_text}"
166
+ end
167
+ end
168
+
169
+ # Or use the collection helpers:
170
+ result.ok? # => true only if every recipient was accepted
171
+ result.ok # => [SmsRu::Sms, ...] accepted recipients
172
+ result.failed # => [SmsRu::Sms, ...] rejected recipients
173
+ ```
174
+
175
+ ## Cost and status
176
+
177
+ ```ruby
178
+ # Price a message before sending (text is optional; omit it for the price of 1 SMS)
179
+ cost = client.cost("79991234567", "How much?")
180
+ cost.total_cost # => 1.74
181
+ cost.total_sms # => 2
182
+
183
+ # Same collection helpers as a send result:
184
+ cost.ok? # => true only if every recipient was priced
185
+ cost.failed # => [SmsRu::CostItem, ...] recipients that errored
186
+ cost.failed.first.error_code # => 207
187
+
188
+ # Delivery status — one id or an Array of ids
189
+ status = client.status("000000-10000000")
190
+ status.status_code # => 103 (the delivery state code)
191
+ status.status_text # => "Сообщение доставлено"
192
+
193
+ # State predicates instead of memorizing codes:
194
+ status.delivered? # => true (code 103)
195
+ status.pending? # => false (codes 100–102, still in transit)
196
+ status.failed? # => false (codes 104–108, 150)
197
+ status.found? # => true (false only when the id is unknown, code -1)
198
+
199
+ statuses = client.status(["000000-10000000", "000000-10000001"]) # => [SmsRu::Status, ...]
200
+ ```
201
+
202
+ Every code has a named constant under `SmsRu::Statuses` (e.g.
203
+ `SmsRu::Statuses::DELIVERED == 103`, `::EXPIRED`, `::READ`) for the cases the
204
+ predicates don't cover. The same predicates are available on
205
+ `SmsRu::Events::SmsStatus` from webhook payloads.
206
+
207
+ > **Outcome vs. delivery state — two ideas, two names.** `ok?` (with
208
+ > `error_code`/`error_text` on a rejected `Sms`/`CostItem`) answers *did the
209
+ > request succeed for this recipient*. `status_code` (with
210
+ > `delivered?`/`pending?`/`failed?`) answers *where the message is in delivery* —
211
+ > and only `Status` and webhook events carry it.
212
+
213
+ ## Verify by phone call
214
+
215
+ Two ways to verify a user by phone call — no SMS required.
216
+
217
+ **Outbound (flash call).** SMS.ru calls the user; the last 4 digits of the
218
+ calling number are the code. You receive the expected `code` to compare against
219
+ what the user enters:
220
+
221
+ ```ruby
222
+ call = client.call("79991234567")
223
+ call.code # => "1435" — the last 4 digits the user will see
224
+ call.call_id # => "000000-10000000"
225
+ ```
226
+
227
+ **Inbound (callcheck).** The user calls a number you show them; SMS.ru drops the
228
+ call (free for the caller) and marks the check confirmed:
229
+
230
+ ```ruby
231
+ check = client.callcheck.add("79991234567")
232
+ check.call_phone_pretty # => "+7 (800) 500-8275" — show this to the user
233
+
234
+ # Poll until the user has called (or receive it via a callback/webhook):
235
+ client.callcheck.status(check.check_id).confirmed? # => true
236
+ ```
237
+
238
+ ## Account information
239
+
240
+ Account reads are grouped under `client.my`:
241
+
242
+ ```ruby
243
+ client.my.balance # => 4762.58 (a Float)
244
+
245
+ limit = client.my.limit
246
+ limit.total_limit # => 100
247
+ limit.used_today # => 7
248
+ limit.available_today # => 93
249
+
250
+ free = client.my.free_limit
251
+ free.total_free # => 5
252
+ free.used_today # => 3
253
+ free.available_today # => 2
254
+
255
+ client.my.senders # => ["MyCompany", "AnotherName"]
256
+ ```
257
+
258
+ Check that the configured `api_id` is valid:
259
+
260
+ ```ruby
261
+ client.auth.ok? # => true
262
+ ```
263
+
264
+ ## Stoplist
265
+
266
+ Numbers on the stoplist never receive messages and are never charged.
267
+
268
+ ```ruby
269
+ client.stoplist.add("79991234567", note: "spam complaint") # => true
270
+ client.stoplist.list # => [#<data SmsRu::StoplistEntry phone="79991234567", note="spam complaint">]
271
+ client.stoplist.remove("79991234567") # => true
272
+ ```
273
+
274
+ ## Callbacks (webhooks)
275
+
276
+ Register URLs that SMS.ru will POST delivery and call-authorization statuses to.
277
+ Each method returns the full list of registered URLs:
278
+
279
+ ```ruby
280
+ client.callbacks.add("https://example.com/sms/callback") # => ["https://example.com/sms/callback"]
281
+ client.callbacks.list # => [...]
282
+ client.callbacks.remove("https://example.com/sms/callback") # => [...]
283
+ ```
284
+
285
+ In your webhook handler, verify the signature, parse the payload, and
286
+ acknowledge it by replying with the string `"100"`:
287
+
288
+ ```ruby
289
+ # Reject forged callbacks: SMS.ru signs every payload with your api_id.
290
+ # The check is constant-time (timing-attack safe).
291
+ unless SmsRu::Webhook.valid?(params["data"], params["hash"], "YOUR_API_ID")
292
+ return head(:forbidden)
293
+ end
294
+
295
+ # SMS.ru sends up to 100 records as POST fields data[0]..data[N]
296
+ # (a Hash in Rack/Rails, an Array in PHP). #parse handles either shape and
297
+ # returns a typed event per record.
298
+ SmsRu::Webhook.parse(params["data"]).each do |event|
299
+ case event
300
+ when SmsRu::Events::SmsStatus # delivery report
301
+ # event.id, event.status_code, event.created_at; event.delivered? => 103
302
+ update_delivery_status(event.id, event.status_code)
303
+ when SmsRu::Events::CallcheckStatus # call-authorization result
304
+ confirm_authorization(event.id) if event.confirmed? # or event.expired?
305
+ # SmsRu::Events::Test (heartbeat) and ::Unknown (future types) fall through
306
+ end
307
+ end
308
+
309
+ # Respond with exactly "100", or SMS.ru retries every 60s for up to 5 days.
310
+ ```
311
+
312
+ ## Error handling
313
+
314
+ Every error inherits from `SmsRu::Error`:
315
+
316
+ ```ruby
317
+ SmsRu::Error # base class
318
+ ├─ SmsRu::ConnectionError # network/timeout/invalid response (after retries)
319
+ └─ SmsRu::ResponseError # API returned a non-OK status; has #code and #text
320
+ ├─ SmsRu::AuthError # invalid api_id/token/account (codes 200, 300, 301, 302)
321
+ └─ SmsRu::InsufficientFundsError # not enough money (code 201)
322
+ ```
323
+
324
+ ```ruby
325
+ begin
326
+ client.deliver("79991234567", "Hi")
327
+ rescue SmsRu::AuthError => e
328
+ warn "Check your api_id: #{e.text}"
329
+ rescue SmsRu::InsufficientFundsError
330
+ warn "Top up your balance"
331
+ rescue SmsRu::ResponseError => e
332
+ warn "SMS.ru error #{e.code}: #{e.text}"
333
+ rescue SmsRu::ConnectionError => e
334
+ warn "Could not reach SMS.ru: #{e.message}"
335
+ end
336
+ ```
337
+
338
+ Note that per-recipient failures in a bulk `deliver` are **not** raised — they are
339
+ reported on each `SmsRu::Sms` in `result.messages` (see above).
340
+
341
+ ## Development
342
+
343
+ ```sh
344
+ bin/setup # install dependencies
345
+ bundle exec rake # run RuboCop, validate RBS signatures, and the test suite
346
+ bundle exec rake steep # type-check lib/ against sig/ (Steep, strict diagnostics)
347
+ bundle exec rake steep:stats # report type coverage (typed % per file)
348
+ bundle exec rake rbs:test # run the suite verifying real values against the signatures
349
+ bin/console # an IRB session with the gem loaded
350
+ ```
351
+
352
+ The signatures are held to their own standard: Steep runs under its **strict**
353
+ diagnostics (no implicit `untyped`, no unannotated collections) at **100% type
354
+ coverage**, gated in CI. Loosely-typed JSON from SMS.ru (which returns, say,
355
+ `total_limit` as the string `"10"`) is normalized into the declared types at the
356
+ parse boundary, and `rbs:test` checks that the values flowing through the suite
357
+ actually match `sig/` at runtime — so the types can't drift from the code.
358
+
359
+ ## Recording test cassettes
360
+
361
+ End-to-end tests replay real SMS.ru responses recorded with [VCR](https://github.com/vcr/vcr).
362
+ The cassettes are not committed with secrets — your `api_id` is filtered out. To
363
+ record them once against your own account (message sends use `test=1`, so they are
364
+ free):
365
+
366
+ ```sh
367
+ SMSRU_API_ID=your_real_api_id bundle exec rake vcr:record
368
+ ```
369
+
370
+ This writes `test/cassettes/*.yml`. Commit them, then `COVERAGE=true bundle exec rake`
371
+ runs fully offline at 100% coverage. Before cassettes are recorded, the end-to-end
372
+ tests are skipped (the unit and transport tests still run).
373
+
374
+ ## License
375
+
376
+ Released under the [MIT License](LICENSE.txt).
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SmsRu
4
+ # Authentication checks against the account. Reached via SmsRu#auth, e.g.
5
+ # `client.auth.ok?`.
6
+ class Auth
7
+ # @api private
8
+ # @param request [Method] the client's bound `request` method
9
+ def initialize(request)
10
+ @request = request
11
+ end
12
+
13
+ # @return [Boolean] true when the configured api_id is valid
14
+ def ok?
15
+ @request.call("/auth/check")
16
+ true
17
+ rescue AuthError
18
+ false
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SmsRu
4
+ # Authorizes a user by an incoming call from their own number: SMS.ru hands you
5
+ # a number, the user calls it, SMS.ru drops the call (free for the caller) and
6
+ # marks the check confirmed. Reached via SmsRu#callcheck.
7
+ #
8
+ # check = client.callcheck.add("79991234567")
9
+ # # show check.call_phone_pretty to the user, then poll until confirmed:
10
+ # client.callcheck.status(check.check_id).confirmed?
11
+ class CallCheck
12
+ # @api private
13
+ # @param request [Method] the client's bound `request` method
14
+ def initialize(request)
15
+ @request = request
16
+ end
17
+
18
+ # Starts a check and returns the number the user must call to authorize.
19
+ #
20
+ # @param phone [String, Integer] the user's phone number to authorize
21
+ # @return [SmsRu::CallCheckResult]
22
+ # @raise [SmsRu::ResponseError] if SMS.ru rejects the request
23
+ def add(phone) = CallCheckResult.build(@request.call("/callcheck/add", phone: phone.to_s))
24
+
25
+ # Polls whether the user has placed the authorizing call yet.
26
+ #
27
+ # @param check_id [String, Integer] the id returned by #add
28
+ # @return [SmsRu::CallCheckStatus]
29
+ # @raise [SmsRu::ResponseError] if SMS.ru rejects the request
30
+ def status(check_id) = CallCheckStatus.build(@request.call("/callcheck/status", check_id: check_id.to_s))
31
+ end
32
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SmsRu
4
+ # Manages callback (webhook) URLs that SMS.ru notifies with delivery statuses.
5
+ # Reached via SmsRu#callbacks, e.g. `client.callbacks.add("https://...")`.
6
+ # Each method returns the full Array of registered URLs after the change.
7
+ class Callbacks
8
+ # @api private
9
+ # @param request [Method] the client's bound `request` method
10
+ def initialize(request)
11
+ @request = request
12
+ end
13
+
14
+ # Registers a callback URL.
15
+ #
16
+ # @param url [String] the webhook URL to register
17
+ # @return [Array<String>] all registered URLs after the change
18
+ # @raise [SmsRu::ResponseError] if SMS.ru rejects the request
19
+ def add(url) = urls(@request.call("/callback/add", url: url))
20
+
21
+ # Removes a callback URL.
22
+ #
23
+ # @param url [String] the webhook URL to remove
24
+ # @return [Array<String>] all registered URLs after the change
25
+ # @raise [SmsRu::ResponseError] if SMS.ru rejects the request
26
+ def remove(url) = urls(@request.call("/callback/del", url: url))
27
+
28
+ # Lists the registered callback URLs.
29
+ #
30
+ # @return [Array<String>] all registered URLs
31
+ # @raise [SmsRu::ResponseError] if SMS.ru rejects the request
32
+ def list = urls(@request.call("/callback/get"))
33
+
34
+ private
35
+
36
+ def urls(data) = data["callback"] || []
37
+ end
38
+ end