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 +7 -0
- data/.yardopts +6 -0
- data/CHANGELOG.md +42 -0
- data/LICENSE.txt +21 -0
- data/README.md +376 -0
- data/lib/sms_ru/auth.rb +21 -0
- data/lib/sms_ru/call_check.rb +32 -0
- data/lib/sms_ru/callbacks.rb +38 -0
- data/lib/sms_ru/client.rb +190 -0
- data/lib/sms_ru/coerce.rb +51 -0
- data/lib/sms_ru/data.rb +263 -0
- data/lib/sms_ru/errors.rb +35 -0
- data/lib/sms_ru/events.rb +65 -0
- data/lib/sms_ru/my.rb +29 -0
- data/lib/sms_ru/statuses.rb +50 -0
- data/lib/sms_ru/stoplist.rb +43 -0
- data/lib/sms_ru/version.rb +6 -0
- data/lib/sms_ru/webhook.rb +74 -0
- data/lib/smsru_ruby.rb +19 -0
- data/sig/manifest.yaml +9 -0
- data/sig/sms_ru/auth.rbs +9 -0
- data/sig/sms_ru/call_check.rbs +10 -0
- data/sig/sms_ru/callbacks.rbs +15 -0
- data/sig/sms_ru/client.rbs +53 -0
- data/sig/sms_ru/coerce.rbs +15 -0
- data/sig/sms_ru/data.rbs +141 -0
- data/sig/sms_ru/errors.rbs +25 -0
- data/sig/sms_ru/events.rbs +49 -0
- data/sig/sms_ru/my.rbs +12 -0
- data/sig/sms_ru/statuses.rbs +35 -0
- data/sig/sms_ru/stoplist.rbs +11 -0
- data/sig/sms_ru/version.rbs +3 -0
- data/sig/sms_ru/webhook.rbs +17 -0
- data/smsru_ruby.gemspec +29 -0
- metadata +81 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class SmsRu
|
|
4
|
+
# Manages the account stoplist (numbers that never receive messages and are
|
|
5
|
+
# never charged). Reached via SmsRu#stoplist, e.g. `client.stoplist.add(...)`.
|
|
6
|
+
class Stoplist
|
|
7
|
+
# @api private
|
|
8
|
+
# @param request [Method] the client's bound `request` method
|
|
9
|
+
def initialize(request)
|
|
10
|
+
@request = request
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Adds a number to the stoplist. `note` is visible only to you.
|
|
14
|
+
#
|
|
15
|
+
# @param phone [String, Integer] the number to stoplist
|
|
16
|
+
# @param note [String, nil] an optional private note
|
|
17
|
+
# @return [Boolean] true on success
|
|
18
|
+
# @raise [SmsRu::ResponseError] if SMS.ru rejects the request
|
|
19
|
+
def add(phone, note: nil)
|
|
20
|
+
@request.call("/stoplist/add", stoplist_phone: phone.to_s, stoplist_text: note.to_s)
|
|
21
|
+
true
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Removes a number from the stoplist.
|
|
25
|
+
#
|
|
26
|
+
# @param phone [String, Integer] the number to remove
|
|
27
|
+
# @return [Boolean] true on success
|
|
28
|
+
# @raise [SmsRu::ResponseError] if SMS.ru rejects the request
|
|
29
|
+
def remove(phone)
|
|
30
|
+
@request.call("/stoplist/del", stoplist_phone: phone.to_s)
|
|
31
|
+
true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Returns every stoplisted number as an Array of SmsRu::StoplistEntry.
|
|
35
|
+
#
|
|
36
|
+
# @return [Array<SmsRu::StoplistEntry>]
|
|
37
|
+
# @raise [SmsRu::ResponseError] if SMS.ru rejects the request
|
|
38
|
+
def list
|
|
39
|
+
data = @request.call("/stoplist/get")
|
|
40
|
+
Coerce.records(data["stoplist"]).map { |phone, note| StoplistEntry.new(phone: String(phone), note: String(note)) }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class SmsRu
|
|
4
|
+
# Parses the inbound webhook payload SMS.ru POSTs to your callback URL and
|
|
5
|
+
# verifies its signature. SMS.ru sends the records as POST fields
|
|
6
|
+
# `data[0]..data[N]` (so `params["data"]` is a Hash in Rack/Rails, an Array
|
|
7
|
+
# in PHP) plus a `hash` field; acknowledge the webhook by replying with "100".
|
|
8
|
+
#
|
|
9
|
+
# {parse} returns one typed event per record — a {SmsRu::Events::SmsStatus},
|
|
10
|
+
# {SmsRu::Events::CallcheckStatus}, {SmsRu::Events::Test}, or
|
|
11
|
+
# {SmsRu::Events::Unknown} — best handled with a case match:
|
|
12
|
+
#
|
|
13
|
+
# return head(:forbidden) unless SmsRu::Webhook.valid?(params["data"], params["hash"], api_id)
|
|
14
|
+
# SmsRu::Webhook.parse(params["data"]).each do |event|
|
|
15
|
+
# case event
|
|
16
|
+
# when SmsRu::Events::SmsStatus then update_delivery(event.id, event.status_code)
|
|
17
|
+
# when SmsRu::Events::CallcheckStatus then confirm(event.id) if event.confirmed?
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
module Webhook
|
|
21
|
+
# @param data [Hash, Array<String>, String, nil] the POST "data" parameter
|
|
22
|
+
# @return [Array<SmsRu::Events::SmsStatus, SmsRu::Events::CallcheckStatus,
|
|
23
|
+
# SmsRu::Events::Test, SmsRu::Events::Unknown>] one event per record
|
|
24
|
+
def self.parse(data)
|
|
25
|
+
entries(data).map do |entry|
|
|
26
|
+
lines = entry.to_s.split("\n")
|
|
27
|
+
case lines[0]
|
|
28
|
+
when "sms_status" then Events::SmsStatus.new(**status_fields(lines))
|
|
29
|
+
when "callcheck_status" then Events::CallcheckStatus.new(**status_fields(lines))
|
|
30
|
+
when "test" then Events::Test.new(created_at: time(lines[1]), raw: lines)
|
|
31
|
+
else Events::Unknown.new(type: lines[0].to_s, raw: lines)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Verifies the payload genuinely came from SMS.ru (constant-time compare of
|
|
37
|
+
# SMS.ru's `hash` against `sha256(api_id + concatenated data entries)`).
|
|
38
|
+
#
|
|
39
|
+
# @param data [Hash, Array<String>, String, nil] the POST "data" parameter
|
|
40
|
+
# @param hash [String, nil] the POST "hash" parameter
|
|
41
|
+
# @param api_id [String] your SMS.ru API id
|
|
42
|
+
# @return [Boolean] true when the signature matches
|
|
43
|
+
def self.valid?(data, hash, api_id)
|
|
44
|
+
return false unless hash.is_a?(String)
|
|
45
|
+
|
|
46
|
+
expected = OpenSSL::Digest::SHA256.hexdigest("#{api_id}#{entries(data).join}")
|
|
47
|
+
expected.bytesize == hash.bytesize && OpenSSL.fixed_length_secure_compare(expected, hash)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Common fields of the "type / id / status / timestamp" status records.
|
|
51
|
+
# @api private
|
|
52
|
+
def self.status_fields(lines)
|
|
53
|
+
{ id: lines[1].to_s, status_code: Coerce.integer?(lines[2]), created_at: time(lines[3]), raw: lines }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Normalizes the `data` param to an ordered Array of record strings. SMS.ru
|
|
57
|
+
# numbers the fields data[0..N]; Rack delivers them as a Hash, so sort by
|
|
58
|
+
# the numeric key to preserve SMS.ru's order (the signature depends on it).
|
|
59
|
+
#
|
|
60
|
+
# @api private
|
|
61
|
+
# @param data [Hash, Array<String>, String, nil] the POST "data" parameter
|
|
62
|
+
# @return [Array<String>] the records in the order SMS.ru sent them
|
|
63
|
+
def self.entries(data)
|
|
64
|
+
data.is_a?(Hash) ? data.sort_by { |k, _| Coerce.integer(k) }.map(&:last) : Array(data)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Converts a unix-timestamp line into a Time, or nil when absent.
|
|
68
|
+
# @api private
|
|
69
|
+
def self.time(str)
|
|
70
|
+
unix = Coerce.integer?(str)
|
|
71
|
+
unix && Time.at(unix)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
data/lib/smsru_ruby.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "openssl"
|
|
6
|
+
|
|
7
|
+
require_relative "sms_ru/version"
|
|
8
|
+
require_relative "sms_ru/errors"
|
|
9
|
+
require_relative "sms_ru/statuses"
|
|
10
|
+
require_relative "sms_ru/coerce"
|
|
11
|
+
require_relative "sms_ru/data"
|
|
12
|
+
require_relative "sms_ru/events"
|
|
13
|
+
require_relative "sms_ru/webhook"
|
|
14
|
+
require_relative "sms_ru/my"
|
|
15
|
+
require_relative "sms_ru/auth"
|
|
16
|
+
require_relative "sms_ru/stoplist"
|
|
17
|
+
require_relative "sms_ru/callbacks"
|
|
18
|
+
require_relative "sms_ru/call_check"
|
|
19
|
+
require_relative "sms_ru/client"
|
data/sig/manifest.yaml
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Standard-library signatures these RBS files reference. Consumers' RBS/Steep
|
|
2
|
+
# load these automatically when they depend on smsru_ruby. (The gem has no
|
|
3
|
+
# runtime gem dependencies, so none are auto-derived from the gemspec.)
|
|
4
|
+
dependencies:
|
|
5
|
+
- name: logger # Logger? in SmsRu#initialize
|
|
6
|
+
- name: openssl # Webhook signature verification
|
|
7
|
+
- name: json # client request parsing
|
|
8
|
+
- name: net-http # client transport
|
|
9
|
+
- name: uri # client transport
|
data/sig/sms_ru/auth.rbs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
class SmsRu
|
|
2
|
+
# Authorizes a user by an incoming call from their own number.
|
|
3
|
+
class CallCheck
|
|
4
|
+
@request: _Request
|
|
5
|
+
|
|
6
|
+
def initialize: (_Request request) -> void
|
|
7
|
+
def add: (String | Integer phone) -> CallCheckResult
|
|
8
|
+
def status: (String | Integer check_id) -> CallCheckStatus
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class SmsRu
|
|
2
|
+
# Manages callback (webhook) URLs that SMS.ru notifies with delivery statuses.
|
|
3
|
+
class Callbacks
|
|
4
|
+
@request: _Request
|
|
5
|
+
|
|
6
|
+
def initialize: (_Request request) -> void
|
|
7
|
+
def add: (String url) -> Array[String]
|
|
8
|
+
def remove: (String url) -> Array[String]
|
|
9
|
+
def list: () -> Array[String]
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def urls: (Hash[String, untyped] data) -> Array[String]
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Ruby client for the SMS.ru HTTP API (https://sms.ru/api).
|
|
2
|
+
class SmsRu
|
|
3
|
+
# The client's bound `request` method as handed to each sub-resource. A
|
|
4
|
+
# `Method` conforms structurally, so `method(:request)` is accepted here while
|
|
5
|
+
# callers still see a typed `Hash[String, untyped]` return instead of untyped.
|
|
6
|
+
interface _Request
|
|
7
|
+
def call: (String path, **untyped params) -> Hash[String, untyped]
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
BASE_URL: String
|
|
11
|
+
# Singletons (not bare Class) so `rescue *RETRIABLE => e` types e as StandardError.
|
|
12
|
+
RETRIABLE: Array[singleton(StandardError)]
|
|
13
|
+
|
|
14
|
+
@api_id: String
|
|
15
|
+
@timeout: Integer
|
|
16
|
+
@test: bool
|
|
17
|
+
@retries: Integer
|
|
18
|
+
@from: String?
|
|
19
|
+
@logger: Logger?
|
|
20
|
+
@my: My?
|
|
21
|
+
@auth: Auth?
|
|
22
|
+
@stoplist: Stoplist?
|
|
23
|
+
@callbacks: Callbacks?
|
|
24
|
+
@callcheck: CallCheck?
|
|
25
|
+
|
|
26
|
+
def initialize: (String api_id, ?timeout: Integer, ?test: bool, ?retries: Integer, ?from: String?, ?logger: Logger?) -> void
|
|
27
|
+
|
|
28
|
+
def deliver: (String | Array[String] | Hash[String, String] to, ?String? text,
|
|
29
|
+
?from: String?, ?time: Integer?, ?ttl: Integer?, ?daytime: bool,
|
|
30
|
+
?translit: bool, ?test: bool?, ?ip: String?, ?partner_id: Integer?) -> SendResult
|
|
31
|
+
|
|
32
|
+
def cost: (String | Array[String] to, ?String? text, ?translit: bool) -> Cost
|
|
33
|
+
|
|
34
|
+
def status: (String | Array[String] sms_id) -> (Status | Array[Status])
|
|
35
|
+
|
|
36
|
+
def call: (String | Integer phone, ?ip: String, ?partner_id: Integer?) -> Call
|
|
37
|
+
|
|
38
|
+
def my: () -> My
|
|
39
|
+
def auth: () -> Auth
|
|
40
|
+
def stoplist: () -> Stoplist
|
|
41
|
+
def callbacks: () -> Callbacks
|
|
42
|
+
def callcheck: () -> CallCheck
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def add_recipients: (Hash[untyped, untyped] params, String | Array[String] | Hash[String, String] to, String? text) -> void
|
|
47
|
+
def request: (String path, **untyped params) -> Hash[String, untyped]
|
|
48
|
+
def perform: (URI::HTTP uri, String body) -> Hash[String, untyped]
|
|
49
|
+
def http: (URI::HTTP uri) -> Net::HTTP
|
|
50
|
+
def parse: (String? raw) -> Hash[String, untyped]
|
|
51
|
+
def error_for: (Hash[String, untyped] data) -> ResponseError
|
|
52
|
+
def error_class: (Integer code) -> singleton(ResponseError)
|
|
53
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class SmsRu
|
|
2
|
+
# Normalizes loosely-typed SMS.ru JSON values into the result objects' types.
|
|
3
|
+
# `self?.` mirrors `module_function`: each helper is both a private instance
|
|
4
|
+
# method and a public singleton method. The `?` variant is nullable; the plain
|
|
5
|
+
# variant falls back to a (overridable) default, so it never returns nil.
|
|
6
|
+
module Coerce
|
|
7
|
+
def self?.string?: (untyped value) -> String?
|
|
8
|
+
def self?.string: (untyped value, ?String default) -> String
|
|
9
|
+
def self?.integer?: (untyped value) -> Integer?
|
|
10
|
+
def self?.integer: (untyped value, ?Integer default) -> Integer
|
|
11
|
+
def self?.float?: (untyped value) -> Float?
|
|
12
|
+
def self?.float: (untyped value, ?Float default) -> Float
|
|
13
|
+
def self?.records: (untyped value) -> Hash[untyped, untyped]
|
|
14
|
+
end
|
|
15
|
+
end
|
data/sig/sms_ru/data.rbs
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
class SmsRu
|
|
2
|
+
# Self-type element for MessageCollection: each entry responds to #ok?.
|
|
3
|
+
interface _OkItem
|
|
4
|
+
def ok?: () -> bool
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
# Self-type host for MessageCollection: exposes a `messages` array of _OkItem.
|
|
8
|
+
interface _MessageHost[E]
|
|
9
|
+
def messages: () -> Array[E]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Collection helpers shared by results wrapping a `messages` array.
|
|
13
|
+
module MessageCollection[E < _OkItem] : _MessageHost[E]
|
|
14
|
+
def ok?: () -> bool
|
|
15
|
+
def ok: () -> Array[E]
|
|
16
|
+
def failed: () -> Array[E]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# A single message inside a send response.
|
|
20
|
+
class Sms
|
|
21
|
+
attr_reader phone: String
|
|
22
|
+
attr_reader sms_id: String?
|
|
23
|
+
attr_reader error_code: Integer?
|
|
24
|
+
attr_reader error_text: String?
|
|
25
|
+
|
|
26
|
+
def initialize: (phone: String, sms_id: String?, error_code: Integer?, error_text: String?) -> void
|
|
27
|
+
def self.build: (String phone, Hash[String, untyped] hash) -> Sms
|
|
28
|
+
def ok?: () -> bool
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Result of SmsRu#deliver.
|
|
32
|
+
class SendResult
|
|
33
|
+
include MessageCollection[Sms]
|
|
34
|
+
|
|
35
|
+
attr_reader balance: Float
|
|
36
|
+
attr_reader messages: Array[Sms]
|
|
37
|
+
|
|
38
|
+
def initialize: (balance: Float, messages: Array[Sms]) -> void
|
|
39
|
+
def self.build: (Hash[String, untyped] hash) -> SendResult
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Delivery status of one message.
|
|
43
|
+
class Status
|
|
44
|
+
include DeliveryStatus
|
|
45
|
+
|
|
46
|
+
attr_reader sms_id: String
|
|
47
|
+
attr_reader status_code: Integer
|
|
48
|
+
attr_reader status_text: String
|
|
49
|
+
attr_reader cost: Float?
|
|
50
|
+
|
|
51
|
+
def initialize: (sms_id: String, status_code: Integer, status_text: String, cost: Float?) -> void
|
|
52
|
+
def self.build: (String sms_id, Hash[String, untyped] hash) -> Status
|
|
53
|
+
def self.build_all: (Hash[String, untyped] hash) -> Array[Status]
|
|
54
|
+
def found?: () -> bool
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Per-recipient cost (one entry of a /sms/cost response).
|
|
58
|
+
class CostItem
|
|
59
|
+
attr_reader phone: String
|
|
60
|
+
attr_reader cost: Float?
|
|
61
|
+
attr_reader sms_count: Integer?
|
|
62
|
+
attr_reader error_code: Integer?
|
|
63
|
+
attr_reader error_text: String?
|
|
64
|
+
|
|
65
|
+
def initialize: (phone: String, cost: Float?, sms_count: Integer?, error_code: Integer?, error_text: String?) -> void
|
|
66
|
+
def self.build: (String phone, Hash[String, untyped] hash) -> CostItem
|
|
67
|
+
def ok?: () -> bool
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Result of SmsRu#cost.
|
|
71
|
+
class Cost
|
|
72
|
+
include MessageCollection[CostItem]
|
|
73
|
+
|
|
74
|
+
attr_reader total_cost: Float
|
|
75
|
+
attr_reader total_sms: Integer
|
|
76
|
+
attr_reader messages: Array[CostItem]
|
|
77
|
+
|
|
78
|
+
def initialize: (total_cost: Float, total_sms: Integer, messages: Array[CostItem]) -> void
|
|
79
|
+
def self.build: (Hash[String, untyped] hash) -> Cost
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Result of SmsRu#call (flash call).
|
|
83
|
+
class Call
|
|
84
|
+
attr_reader code: String
|
|
85
|
+
attr_reader call_id: String
|
|
86
|
+
attr_reader cost: Float
|
|
87
|
+
attr_reader balance: Float
|
|
88
|
+
|
|
89
|
+
def initialize: (code: String, call_id: String, cost: Float, balance: Float) -> void
|
|
90
|
+
def self.build: (Hash[String, untyped] hash) -> Call
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Result of SmsRu::My#limit (daily sending limit).
|
|
94
|
+
class Limit
|
|
95
|
+
attr_reader total_limit: Integer
|
|
96
|
+
attr_reader used_today: Integer
|
|
97
|
+
|
|
98
|
+
def initialize: (total_limit: Integer, used_today: Integer) -> void
|
|
99
|
+
def self.build: (Hash[String, untyped] hash) -> Limit
|
|
100
|
+
def available_today: () -> Integer
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Result of SmsRu::My#free_limit (free daily messages).
|
|
104
|
+
class FreeLimit
|
|
105
|
+
attr_reader total_free: Integer
|
|
106
|
+
attr_reader used_today: Integer
|
|
107
|
+
|
|
108
|
+
def initialize: (total_free: Integer, used_today: Integer) -> void
|
|
109
|
+
def self.build: (Hash[String, untyped] hash) -> FreeLimit
|
|
110
|
+
def available_today: () -> Integer
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Result of SmsRu::CallCheck#add.
|
|
114
|
+
class CallCheckResult
|
|
115
|
+
attr_reader check_id: String
|
|
116
|
+
attr_reader call_phone: String
|
|
117
|
+
attr_reader call_phone_pretty: String
|
|
118
|
+
attr_reader call_phone_html: String
|
|
119
|
+
|
|
120
|
+
def initialize: (check_id: String, call_phone: String, call_phone_pretty: String, call_phone_html: String) -> void
|
|
121
|
+
def self.build: (Hash[String, untyped] hash) -> CallCheckResult
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Result of SmsRu::CallCheck#status.
|
|
125
|
+
class CallCheckStatus
|
|
126
|
+
attr_reader status_code: Integer
|
|
127
|
+
attr_reader status_text: String
|
|
128
|
+
|
|
129
|
+
def initialize: (status_code: Integer, status_text: String) -> void
|
|
130
|
+
def self.build: (Hash[String, untyped] hash) -> CallCheckStatus
|
|
131
|
+
def confirmed?: () -> bool
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# One stoplist entry returned by SmsRu::Stoplist#list.
|
|
135
|
+
class StoplistEntry
|
|
136
|
+
attr_reader phone: String
|
|
137
|
+
attr_reader note: String
|
|
138
|
+
|
|
139
|
+
def initialize: (phone: String, note: String) -> void
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
class SmsRu
|
|
2
|
+
# Base class for every error raised by the gem.
|
|
3
|
+
class Error < StandardError
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
# Raised when SMS.ru cannot be reached or returns an unparseable body.
|
|
7
|
+
class ConnectionError < Error
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Raised when SMS.ru replies with a non-OK status.
|
|
11
|
+
class ResponseError < Error
|
|
12
|
+
attr_reader code: Integer
|
|
13
|
+
attr_reader text: String
|
|
14
|
+
|
|
15
|
+
def initialize: (code: Integer, text: String) -> void
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Invalid api_id / token / unconfirmed account (codes 200, 300, 301, 302).
|
|
19
|
+
class AuthError < ResponseError
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Not enough money on the account (code 201).
|
|
23
|
+
class InsufficientFundsError < ResponseError
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
class SmsRu
|
|
2
|
+
# Typed events decoded from an inbound SMS.ru webhook payload.
|
|
3
|
+
module Events
|
|
4
|
+
# A delivery-status notification (record type "sms_status"). Its `status_code`
|
|
5
|
+
# is nullable (a malformed line yields nil); DeliveryStatus's predicates
|
|
6
|
+
# nil-guard, so the module is included directly here as it is at runtime.
|
|
7
|
+
class SmsStatus
|
|
8
|
+
include DeliveryStatus
|
|
9
|
+
|
|
10
|
+
attr_reader id: String
|
|
11
|
+
attr_reader status_code: Integer?
|
|
12
|
+
attr_reader created_at: Time?
|
|
13
|
+
attr_reader raw: Array[String]
|
|
14
|
+
|
|
15
|
+
def initialize: (id: String, status_code: Integer?, created_at: Time?, raw: Array[String]) -> void
|
|
16
|
+
def type: () -> String
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# A call-authorization notification (record type "callcheck_status").
|
|
20
|
+
class CallcheckStatus
|
|
21
|
+
attr_reader id: String
|
|
22
|
+
attr_reader status_code: Integer?
|
|
23
|
+
attr_reader created_at: Time?
|
|
24
|
+
attr_reader raw: Array[String]
|
|
25
|
+
|
|
26
|
+
def initialize: (id: String, status_code: Integer?, created_at: Time?, raw: Array[String]) -> void
|
|
27
|
+
def type: () -> String
|
|
28
|
+
def confirmed?: () -> bool
|
|
29
|
+
def expired?: () -> bool
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# SMS.ru's periodic heartbeat record (record type "test").
|
|
33
|
+
class Test
|
|
34
|
+
attr_reader created_at: Time?
|
|
35
|
+
attr_reader raw: Array[String]
|
|
36
|
+
|
|
37
|
+
def initialize: (created_at: Time?, raw: Array[String]) -> void
|
|
38
|
+
def type: () -> String
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Any record type this gem does not model explicitly.
|
|
42
|
+
class Unknown
|
|
43
|
+
attr_reader type: String
|
|
44
|
+
attr_reader raw: Array[String]
|
|
45
|
+
|
|
46
|
+
def initialize: (type: String, raw: Array[String]) -> void
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/sig/sms_ru/my.rbs
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
class SmsRu
|
|
2
|
+
# Account information: balance, daily limit, free quota, approved sender names.
|
|
3
|
+
class My
|
|
4
|
+
@request: _Request
|
|
5
|
+
|
|
6
|
+
def initialize: (_Request request) -> void
|
|
7
|
+
def balance: () -> Float
|
|
8
|
+
def limit: () -> Limit
|
|
9
|
+
def free_limit: () -> FreeLimit
|
|
10
|
+
def senders: () -> Array[String]
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
class SmsRu
|
|
2
|
+
# Delivery status codes returned by /sms/status and carried in `sms_status` webhooks.
|
|
3
|
+
module Statuses
|
|
4
|
+
NOT_FOUND: Integer
|
|
5
|
+
QUEUED: Integer
|
|
6
|
+
SENT_TO_OPERATOR: Integer
|
|
7
|
+
IN_TRANSIT: Integer
|
|
8
|
+
DELIVERED: Integer
|
|
9
|
+
EXPIRED: Integer
|
|
10
|
+
DELETED: Integer
|
|
11
|
+
PHONE_FAILURE: Integer
|
|
12
|
+
UNKNOWN_FAILURE: Integer
|
|
13
|
+
REJECTED: Integer
|
|
14
|
+
READ: Integer
|
|
15
|
+
NO_ROUTE: Integer
|
|
16
|
+
|
|
17
|
+
PENDING: Array[Integer]
|
|
18
|
+
FAILED: Array[Integer]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Self-type for DeliveryStatus: the including object must expose `status_code`.
|
|
22
|
+
# Nullable so both SmsRu::Status (always present) and SmsRu::Events::SmsStatus
|
|
23
|
+
# (nil for a malformed webhook line) can include the module; the predicates
|
|
24
|
+
# nil-guard before the Array#include? lookups.
|
|
25
|
+
interface _HasStatusCode
|
|
26
|
+
def status_code: () -> Integer?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Delivery-state predicates shared by SmsRu::Status and SmsRu::Events::SmsStatus.
|
|
30
|
+
module DeliveryStatus : _HasStatusCode
|
|
31
|
+
def delivered?: () -> bool
|
|
32
|
+
def pending?: () -> bool
|
|
33
|
+
def failed?: () -> bool
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
class SmsRu
|
|
2
|
+
# Manages the account stoplist.
|
|
3
|
+
class Stoplist
|
|
4
|
+
@request: _Request
|
|
5
|
+
|
|
6
|
+
def initialize: (_Request request) -> void
|
|
7
|
+
def add: (String | Integer phone, ?note: String?) -> bool
|
|
8
|
+
def remove: (String | Integer phone) -> bool
|
|
9
|
+
def list: () -> Array[StoplistEntry]
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class SmsRu
|
|
2
|
+
# Parses the inbound webhook payload SMS.ru POSTs to your callback URL and
|
|
3
|
+
# verifies its signature.
|
|
4
|
+
module Webhook
|
|
5
|
+
type payload = Hash[untyped, untyped] | Array[String] | String | nil
|
|
6
|
+
type event = Events::SmsStatus | Events::CallcheckStatus | Events::Test | Events::Unknown
|
|
7
|
+
|
|
8
|
+
def self.parse: (payload data) -> Array[event]
|
|
9
|
+
def self.valid?: (payload data, String? hash, String api_id) -> bool
|
|
10
|
+
|
|
11
|
+
# @api private helpers. status_fields returns a record so Steep can expand it
|
|
12
|
+
# into the SmsStatus/CallcheckStatus keyword constructors via `**`.
|
|
13
|
+
def self.status_fields: (Array[String] lines) -> { id: String, status_code: Integer?, created_at: Time?, raw: Array[String] }
|
|
14
|
+
def self.entries: (payload data) -> Array[String]
|
|
15
|
+
def self.time: (String? str) -> Time?
|
|
16
|
+
end
|
|
17
|
+
end
|
data/smsru_ruby.gemspec
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/sms_ru/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "smsru_ruby"
|
|
7
|
+
spec.version = SmsRu::VERSION
|
|
8
|
+
spec.authors = ["Leonid Svyatov"]
|
|
9
|
+
spec.email = ["leonid@svyatov.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Modern, dependency-free Ruby client for the SMS.ru API."
|
|
12
|
+
spec.description = "A modern, dependency-free Ruby client for the SMS.ru HTTP API. Send single or bulk SMS, " \
|
|
13
|
+
"schedule delivery, check cost and delivery status, verify users by phone call, inspect " \
|
|
14
|
+
"balance/limits/senders, manage the stoplist, and register delivery callbacks."
|
|
15
|
+
spec.homepage = "https://github.com/svyatov/smsru_ruby"
|
|
16
|
+
spec.license = "MIT"
|
|
17
|
+
|
|
18
|
+
spec.required_ruby_version = ">= 3.2.0"
|
|
19
|
+
|
|
20
|
+
spec.require_paths = ["lib"]
|
|
21
|
+
spec.files = Dir["lib/**/*.rb"] + Dir["sig/**/*"] +
|
|
22
|
+
%w[.yardopts CHANGELOG.md LICENSE.txt README.md smsru_ruby.gemspec]
|
|
23
|
+
|
|
24
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
25
|
+
spec.metadata["documentation_uri"] = "https://rubydoc.info/gems/smsru_ruby"
|
|
26
|
+
spec.metadata["source_code_uri"] = "https://github.com/svyatov/smsru_ruby"
|
|
27
|
+
spec.metadata["changelog_uri"] = "https://github.com/svyatov/smsru_ruby/blob/main/CHANGELOG.md"
|
|
28
|
+
spec.metadata["bug_tracker_uri"] = "https://github.com/svyatov/smsru_ruby/issues"
|
|
29
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: smsru_ruby
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Leonid Svyatov
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: A modern, dependency-free Ruby client for the SMS.ru HTTP API. Send single
|
|
13
|
+
or bulk SMS, schedule delivery, check cost and delivery status, verify users by
|
|
14
|
+
phone call, inspect balance/limits/senders, manage the stoplist, and register delivery
|
|
15
|
+
callbacks.
|
|
16
|
+
email:
|
|
17
|
+
- leonid@svyatov.com
|
|
18
|
+
executables: []
|
|
19
|
+
extensions: []
|
|
20
|
+
extra_rdoc_files: []
|
|
21
|
+
files:
|
|
22
|
+
- ".yardopts"
|
|
23
|
+
- CHANGELOG.md
|
|
24
|
+
- LICENSE.txt
|
|
25
|
+
- README.md
|
|
26
|
+
- lib/sms_ru/auth.rb
|
|
27
|
+
- lib/sms_ru/call_check.rb
|
|
28
|
+
- lib/sms_ru/callbacks.rb
|
|
29
|
+
- lib/sms_ru/client.rb
|
|
30
|
+
- lib/sms_ru/coerce.rb
|
|
31
|
+
- lib/sms_ru/data.rb
|
|
32
|
+
- lib/sms_ru/errors.rb
|
|
33
|
+
- lib/sms_ru/events.rb
|
|
34
|
+
- lib/sms_ru/my.rb
|
|
35
|
+
- lib/sms_ru/statuses.rb
|
|
36
|
+
- lib/sms_ru/stoplist.rb
|
|
37
|
+
- lib/sms_ru/version.rb
|
|
38
|
+
- lib/sms_ru/webhook.rb
|
|
39
|
+
- lib/smsru_ruby.rb
|
|
40
|
+
- sig/manifest.yaml
|
|
41
|
+
- sig/sms_ru/auth.rbs
|
|
42
|
+
- sig/sms_ru/call_check.rbs
|
|
43
|
+
- sig/sms_ru/callbacks.rbs
|
|
44
|
+
- sig/sms_ru/client.rbs
|
|
45
|
+
- sig/sms_ru/coerce.rbs
|
|
46
|
+
- sig/sms_ru/data.rbs
|
|
47
|
+
- sig/sms_ru/errors.rbs
|
|
48
|
+
- sig/sms_ru/events.rbs
|
|
49
|
+
- sig/sms_ru/my.rbs
|
|
50
|
+
- sig/sms_ru/statuses.rbs
|
|
51
|
+
- sig/sms_ru/stoplist.rbs
|
|
52
|
+
- sig/sms_ru/version.rbs
|
|
53
|
+
- sig/sms_ru/webhook.rbs
|
|
54
|
+
- smsru_ruby.gemspec
|
|
55
|
+
homepage: https://github.com/svyatov/smsru_ruby
|
|
56
|
+
licenses:
|
|
57
|
+
- MIT
|
|
58
|
+
metadata:
|
|
59
|
+
rubygems_mfa_required: 'true'
|
|
60
|
+
documentation_uri: https://rubydoc.info/gems/smsru_ruby
|
|
61
|
+
source_code_uri: https://github.com/svyatov/smsru_ruby
|
|
62
|
+
changelog_uri: https://github.com/svyatov/smsru_ruby/blob/main/CHANGELOG.md
|
|
63
|
+
bug_tracker_uri: https://github.com/svyatov/smsru_ruby/issues
|
|
64
|
+
rdoc_options: []
|
|
65
|
+
require_paths:
|
|
66
|
+
- lib
|
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
68
|
+
requirements:
|
|
69
|
+
- - ">="
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: 3.2.0
|
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
73
|
+
requirements:
|
|
74
|
+
- - ">="
|
|
75
|
+
- !ruby/object:Gem::Version
|
|
76
|
+
version: '0'
|
|
77
|
+
requirements: []
|
|
78
|
+
rubygems_version: 4.0.12
|
|
79
|
+
specification_version: 4
|
|
80
|
+
summary: Modern, dependency-free Ruby client for the SMS.ru API.
|
|
81
|
+
test_files: []
|