Module: SmsRu::Webhook
- Defined in:
- lib/sms_ru/webhook.rb
Overview
Parses the inbound webhook payload SMS.ru POSTs to your callback URL and verifies its signature. SMS.ru sends the records as POST fields ‘data..data` (so `params` is a Hash in Rack/Rails, an Array in PHP) plus a `hash` field; acknowledge the webhook by replying with “100”.
Webhook.parse returns one typed event per record — a Events::SmsStatus, Events::CallcheckStatus, Events::Test, or Events::Unknown — best handled with a case match:
return head(:forbidden) unless SmsRu::Webhook.valid?(params["data"], params["hash"], api_id)
SmsRu::Webhook.parse(params["data"]).each do |event|
case event
when SmsRu::Events::SmsStatus then update_delivery(event.id, event.status_code)
when SmsRu::Events::CallcheckStatus then confirm(event.id) if event.confirmed?
end
end
Class Method Summary collapse
-
.entries(data) ⇒ Array<String>
private
Normalizes the ‘data` param to an ordered Array of record strings.
-
.parse(data) ⇒ Array<SmsRu::Events::SmsStatus, SmsRu::Events::CallcheckStatus, SmsRu::Events::Test, SmsRu::Events::Unknown>
One event per record.
-
.status_fields(lines) ⇒ Object
private
Common fields of the “type / id / status / timestamp” status records.
-
.time(str) ⇒ Object
private
Converts a unix-timestamp line into a Time, or nil when absent.
-
.valid?(data, hash, api_id) ⇒ Boolean
Verifies the payload genuinely came from SMS.ru (constant-time compare of SMS.ru’s ‘hash` against `sha256(api_id + concatenated data entries)`).
Class Method Details
.entries(data) ⇒ Array<String>
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Normalizes the ‘data` param to an ordered Array of record strings. SMS.ru numbers the fields data; Rack delivers them as a Hash, so sort by the numeric key to preserve SMS.ru’s order (the signature depends on it).
63 64 65 |
# File 'lib/sms_ru/webhook.rb', line 63 def self.entries(data) data.is_a?(Hash) ? data.sort_by { |k, _| Coerce.integer(k) }.map(&:last) : Array(data) end |
.parse(data) ⇒ Array<SmsRu::Events::SmsStatus, SmsRu::Events::CallcheckStatus, SmsRu::Events::Test, SmsRu::Events::Unknown>
Returns one event per record.
24 25 26 27 28 29 30 31 32 33 34 |
# File 'lib/sms_ru/webhook.rb', line 24 def self.parse(data) entries(data).map do |entry| lines = entry.to_s.split("\n") case lines[0] when "sms_status" then Events::SmsStatus.new(**status_fields(lines)) when "callcheck_status" then Events::CallcheckStatus.new(**status_fields(lines)) when "test" then Events::Test.new(created_at: time(lines[1]), raw: lines) else Events::Unknown.new(type: lines[0].to_s, raw: lines) end end end |
.status_fields(lines) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Common fields of the “type / id / status / timestamp” status records.
52 53 54 |
# File 'lib/sms_ru/webhook.rb', line 52 def self.status_fields(lines) { id: lines[1].to_s, status_code: Coerce.integer?(lines[2]), created_at: time(lines[3]), raw: lines } end |
.time(str) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Converts a unix-timestamp line into a Time, or nil when absent.
69 70 71 72 |
# File 'lib/sms_ru/webhook.rb', line 69 def self.time(str) unix = Coerce.integer?(str) unix && Time.at(unix) end |
.valid?(data, hash, api_id) ⇒ Boolean
Verifies the payload genuinely came from SMS.ru (constant-time compare of SMS.ru’s ‘hash` against `sha256(api_id + concatenated data entries)`).
43 44 45 46 47 48 |
# File 'lib/sms_ru/webhook.rb', line 43 def self.valid?(data, hash, api_id) return false unless hash.is_a?(String) expected = OpenSSL::Digest::SHA256.hexdigest("#{api_id}#{entries(data).join}") expected.bytesize == hash.bytesize && OpenSSL.fixed_length_secure_compare(expected, hash) end |