class SmsPilot::Client

@!attribute [r] api_key

@return [String] your API key
@example
  client.api_key #=> "XXX..."

@!attribute [r] error

Error message returned from the API, combined with the error code
@example
  client.error #=> "Пользователь временно блокирован (спорная ситуация) (error code: 122)"
@return [nil, String]
@see #error_code
@see #error_description

@!attribute [r] locale

Chosen locale (affects only the language of errors)

@return [Symbol]
@example
  client.locale #=> :ru

@!attribute [r] phone

@return [nil, String] phone after normalization
@example
  client.phone #=> "79021234567"

@!attribute [r] response_body

Response format is JSON (because we request it that way in {#build_uri}).
@example
  "{\"send\":[{\"server_id\":\"10000\",\"phone\":\"79021234567\",\"price\":\"1.68\",\"status\":\"0\"}],\"balance\":\"20006.97\",\"cost\":\"1.68\"}"
@return [nil, String] Unmodified HTTP resonse body that API returned
@see #response_data
@see #response_headers
@see #response_status

@!attribute [r] response_headers

@example
  client.response_headers #=>
  {
    "Access-Control-Allow-Origin" => "*",
    "Connection" => "close",
    "Content-Length" => "179",
    "Content-Type" => "application/json; charset=utf-8",
    "Date" => "Thu, 06 May 2021 04:52:58 GMT",
    "Server" => "nginx"
  }
@return [nil, String] Unmodified HTTP resonse headers that API returned.
@see #response_body
@see #response_data
@see #response_status

@!attribute [r] response_status

HTTP status of the request to the API. 200 in case of success.
@example
  client.response_status #=> 200

@return [nil, Integer]
@see #response_body
@see #response_data
@see #response_headers

Constants

API_ENDPOINT

Check current API endpoint URL at {smspilot.ru/apikey.php#api1}

AVAILABLE_LOCALES

Locale influences only the language of API errors

REQUEST_ACCEPT_FORMAT
REQUEST_CHARSET

Attributes

api_key[R]
error[R]
locale[R]
phone[R]
response_body[R]
response_headers[R]
response_status[R]

Public Class Methods

new(api_key:, locale: AVAILABLE_LOCALES[0]) click to toggle source

@param api_key [String] @param locale [Symbol]

@return [SmsPilot::Client] @raise [SmsPilot::InvalidAPIkeyError] if you pass anything but a non-empty String @raise [SmsPilot::InvalidLocaleError] if you pass anything but :ru or :en

@see smspilot.ru/my-settings.php Get your production API key here @see smspilot.ru/apikey.php Get your development API key here @note Current development API key is "XXXXXXXXXXXXYYYYYYYYYYYYZZZZZZZZXXXXXXXXXXXXYYYYYYYYYYYYZZZZZZZZ"

@example

client = SmsPilot::Client.new(api_key: ENV["SMS_PILOT_API_KEY"])
client = SmsPilot::Client.new(api_key: ENV["SMS_PILOT_API_KEY"], locale: :en)
# File lib/sms_pilot/client.rb, line 106
def initialize(api_key:, locale: AVAILABLE_LOCALES[0])
  @api_key          = validate_api_key!(api_key)
  @error            = nil
  @locale           = validate_locale!(locale)
  @response_status  = nil
  @response_headers = {}
  @response_body    = nil
end

Public Instance Methods

balance() click to toggle source

Your current balance, remaining after sending that latest SMS.

@return [nil, Float] Always nil before you send SMS and if the SMS was not sent, always Float after successfull SMS transmission. @example

client.balance #=> 20215.25
# File lib/sms_pilot/client.rb, line 173
def balance
  response_data["balance"]&.to_f if sms_sent?
end
broadcast_id() click to toggle source

SMS broadcast ID (API documentation calls it “server ID” but it makes no sense, as it is clearly the ID of the transmission, not of a server)

@example

client.broadcast_id #=> 10000

@return [nil, Integer]

@see response_data

# File lib/sms_pilot/client.rb, line 187
def broadcast_id
  @response_data.dig("send", 0, "server_id")&.to_i if sms_sent?
end
broadcast_status() click to toggle source

SMS delivery status, as returned by the API

@return [nil, Integer] nil is returned before sending SMS or if the request was rejected. Otherwise an Integer in the range of [-2..3] is returned. @see smspilot.ru/apikey.php#status List of available statuses at API documentation website

Code | Name | Final? | Description —-:|:————–|:——-|:————- -2 | Ошибка | Да | Ошибка, неправильные параметры запроса -1 | Не доставлено | Да | Сообщение не доставлено (не в сети, заблокирован, не взял трубку), PING — не в сети, HLR — не обслуживается (заблокирован)

0   | Новое         | Нет    | Новое сообщение/запрос, ожидает обработки у нас на сервере
1   | В очереди     | Нет    | Сообщение или запрос ожидают отправки на сервере оператора
2   | Доставлено    | Да     | Доставлено, звонок совершен, PING — в сети, HLR — обслуживается
3   | Отложено      | Нет    | Отложенная отправка, отправка сообщения/запроса запланирована на другое время

@example

client.broadcast_status #=> 2

@see sms_status

# File lib/sms_pilot/client.rb, line 211
def broadcast_status
  @response_data.dig("send", 0, "status")&.to_i if sms_sent?
end
error_code() click to toggle source

Numerical code of the error that occured when sending the SMS. In the range from 0 to 715 (which may change).

@return [nil, Integer] nil is returned before sending SMS. Otherwise Integer @example

client.error_code #=> 122

@see error @see error_description @see smspilot.ru/apikey.php#err Error codes at the API documentation website

# File lib/sms_pilot/client.rb, line 225
def error_code
  @response_data.dig("error", "code")&.to_i if rejected?
end
error_description() click to toggle source

Description of the error that occured when sending the SMS

@return [nil, String] nil is returned before sending SMS. Otherwise String @example

client.error_description #=> "Пользователь временно блокирован (спорная ситуация)"

@see error @see error_code @see smspilot.ru/apikey.php#err Error codes at the API documentation website

# File lib/sms_pilot/client.rb, line 239
def error_description
  method_name = (@locale == :ru) ? "description_ru" : "description"
  @response_data.dig("error", method_name) if rejected?
end
rejected?() click to toggle source

Did the API reject your request to send that SMS

@return [Boolean] false is returned before sending SMS. Otherwise the Boolean corresponds to whether your request to send an SMS was rejected. @example

client.rejected? #=> false
# File lib/sms_pilot/client.rb, line 251
def rejected?
  return false if sms_sent?
  response_data["error"].is_a? Hash
end
response_data() click to toggle source

Parses @response_body and memoizes result in @response_data

@example

{
  "balance" => "20006.97",
  "cost" => "1.68",
  "send" => [
    {
      "phone" => "79021234567",
      "price" => "1.68",
      "server_id" => "10000",
      "status" => "0"
    }
  ]
}

@return [Hash] @raise [JSON::ParserError] which is rescued in {#send_sms}

@see response_body @see response_headers @see response_status

# File lib/sms_pilot/client.rb, line 280
def response_data
  return {} unless @response_body
  @response_data ||= JSON.parse @response_body
end
send_sms(phone, message, sender_name = nil) click to toggle source

Send HTTP request to the API to ask them to transmit your SMS

@return [Boolean] true if the SMS has been sent, false otherwise

@param [String] phone The phone to send the SMS to. In free-form, will be sanitized. @param [String] message The text of your message.

@raise [SmsPilot::InvalidPhoneError] if you pass anythig but a String with the phone argument @raise [SmsPilot::InvalidMessageError] if you pass anythig but a String with the message argument @raise [SmsPilot::InvalidMessageError] if your message is empty @raise [SmsPilot::InvalidPhoneError] if your phone is empty @raise [SmsPilot::InvalidPhoneError] if your phone has no digits @raise [URI::InvalidURIError] but is almost impossible, because we provide the URL ourselves

@example

client.send_sms("+7 (902) 123-45-67", "Привет, мир!") # => true
client.send_sms("+7 (902) 123-45-67", "Привет, мир!", "ФССПРФ") # => true
# File lib/sms_pilot/client.rb, line 136
def send_sms(phone, message, sender_name = nil)
  validate_phone! phone
  validate_message! message
  validate_sender_name! sender_name

  @phone = normalize_phone(phone)
  @uri   = build_uri(@phone, message, sender_name)

  response = persist_response_details Net::HTTP.get_response(@uri)

  @error = "HTTP request failed with code #{response.code}"   and return false unless response.is_a?(Net::HTTPSuccess)
  @error = "#{error_description} (error code: #{error_code})" and return false if rejected?

  true

rescue JSON::ParserError => error
  @error = "API returned invalid JSON. #{error.message}"
  return false

rescue SocketError, EOFError, IOError, SystemCallError,
       Timeout::Error, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError,
       Net::ProtocolError, OpenSSL::SSL::SSLError => error
  @error = error.message
  return false
end
sender_blocked?() click to toggle source

Did the API block you

Error code | Description :—|:—————— 105 | из-за низкого баланса 106 | за спам/ошибки 107 | за недостоверные учетные данные / недоступна эл. почта / проблемы с телефоном 122 | спорная ситуация

@return [Boolean] nil is returned before sending SMS. Otherwise the Boolean corresponds to whether the API has blocked you. @example

client.sender_blocked? #=> false

@see error @see smspilot.ru/apikey.php#err Error codes at the API documentation website

# File lib/sms_pilot/client.rb, line 301
def sender_blocked?
  [105, 106, 107, 122].include? error_code
end
sms_cost() click to toggle source

The cost of the SMS that has just been sent, in RUB

@return [nil, Float] @example

client.sms_cost #=> 2.63
# File lib/sms_pilot/client.rb, line 312
def sms_cost
  response_data["cost"]&.to_f if sms_sent?
end
sms_sent?() click to toggle source

Has the SMS transmission been a success.

@return [Boolean] nil is returned before sending SMS. Otherwise the Boolean corresponds to the result of SMS transmission. @see sms_status @see rejected? @see error

@example

client.sms_sent? #=> true
# File lib/sms_pilot/client.rb, line 327
def sms_sent?
  response_data["send"] != nil
end
sms_status() click to toggle source

@deprecated (in favor of {#broadcast_status})

# File lib/sms_pilot/client.rb, line 334
def sms_status
  broadcast_status
end
url() click to toggle source

URL generated by combining API_ENDPOINT, your API key, SMS text & phone

@example

client.url #=> "https://smspilot.ru/api.php?api_key=XXX&format=json&send=TEXT&to=79021234567"

@return [nil, String]

# File lib/sms_pilot/client.rb, line 346
def url
  @uri&.to_s
end

Private Instance Methods

build_uri(phone, text, sender_name) click to toggle source

The URI we will send an HTTP request to @private

@example

build_uri("79021234567", "Hello, World!")
#=> #<URI::HTTPS https://smspilot.ru/api.php?apikey=XXX…&format=json&send=Hello%2C+World%21&to=79021234567>

@return [URI] @raise [URI::InvalidURIError] but is almost impossible, because we provide the URL ourselves

@param [String] phone @param [String] text @param [nil, String] sender_name

@see api_key @see phone @see validate_phone! @see validate_message! @see validate_sender_name!

# File lib/sms_pilot/client.rb, line 373
        def build_uri(phone, text, sender_name)
  attributes = {
    apikey:  @api_key,
    charset: REQUEST_CHARSET,
    format:  REQUEST_ACCEPT_FORMAT,
    lang:    @locale,
    send:    text,
    to:      phone
  }
  attributes = attributes.merge({ sender: sender_name }) if sender_name

  URI.parse(API_ENDPOINT).tap do |uri|
    uri.query = URI.encode_www_form(attributes)
  end
end
normalize_phone(phone) click to toggle source

Cleans up your phone from anything but digits. Also replaces 8 to 7 if it is the first digit.

@private @param [String] phone @return [String]

@example

normalize_phone("8 (902) 123-45-67") #=> 79021234567
normalize_phone("+7-902-123-45-67")  #=> 79021234567
# File lib/sms_pilot/client.rb, line 402
        def normalize_phone(phone)
  phone.gsub(/[^0-9]/, '').sub(/^8/, '7').gsub('+7', '8')
end
persist_response_details(response) click to toggle source

Saves response details into instance variables @private

@return [response] @raise [TypeError] unless a Net::HTTPResponse passed

# File lib/sms_pilot/client.rb, line 413
        def persist_response_details(response)
  fail TypeError, "Net::HTTPResponse expected, you pass a #{response.class}" unless response.is_a? Net::HTTPResponse
  @response_body    = response.body
  @response_status  = response.code.to_i
  @response_headers = response.each_capitalized.to_h
  response
end
validate_api_key!(api_key) click to toggle source

Validates api_key

@private @return [String] the original value passed into the method, only if it was valid @param [String] api_key

@raise [SmsPilot::InvalidError] if api_key is not a String @raise [SmsPilot::InvalidError] if api_key is an empty String

# File lib/sms_pilot/client.rb, line 433
        def validate_api_key!(api_key)
  fail SmsPilot::InvalidAPIkeyError, "API key must be a String, you pass a #{api_key.class} (#{api_key})" unless api_key.is_a? String
  fail SmsPilot::InvalidAPIkeyError, "API key cannot be empty" if api_key == ""
  return api_key
end
validate_locale!(locale) click to toggle source

Validates locale

@private @return [Symbol] the original value passed into the method, only if it was valid @param [Symbol] locale

@raise [SmsPilot::InvalidError] if locale is not a Symbol @raise [SmsPilot::InvalidError] if locale is unrecognized

# File lib/sms_pilot/client.rb, line 449
        def validate_locale!(locale)
  fail SmsPilot::InvalidLocaleError, "locale must be a Symbol" unless locale.is_a? Symbol
  fail SmsPilot::InvalidLocaleError, "API does not support locale :#{locale}; choose one of #{AVAILABLE_LOCALES.inspect}" unless AVAILABLE_LOCALES.include? locale
  return locale
end
validate_message!(message) click to toggle source

Validates message @private

@param [String] message @return [String] the original value passed into the method, only if it was valid

@raise [SmsPilot::InvalidMessageError] if you pass anythig but a String with the message argument @raise [SmsPilot::InvalidMessageError] if your message is empty

# File lib/sms_pilot/client.rb, line 465
        def validate_message!(message)
  fail SmsPilot::InvalidMessageError, "SMS message must be a String, you pass a #{ message.class} (#{ message})" unless message.is_a? String
  fail SmsPilot::InvalidMessageError, "SMS message cannot be empty" if  message == ""
  message
end
validate_phone!(phone) click to toggle source

Validates phone @private

@param [String] phone @return [String] the original value passed into the method, only if it was valid

@raise [SmsPilot::InvalidPhoneError] if you pass anythig but a String with the phone argument @raise [SmsPilot::InvalidPhoneError] if your phone is empty @raise [SmsPilot::InvalidPhoneError] if your phone has no digits

# File lib/sms_pilot/client.rb, line 482
        def validate_phone!(phone)
  fail SmsPilot::InvalidPhoneError, "phone must be a String, you pass a #{phone.class} (#{phone})" unless phone.is_a? String
  fail SmsPilot::InvalidPhoneError, "phone cannot be empty" if phone == ""
  fail SmsPilot::InvalidPhoneError, "phone must contain digits" if phone.scan(/\d/).none?
  phone
end
validate_sender_name!(sender_name) click to toggle source

Validates sender name @private

@param [nil, String] sender_name @return [String] the original value passed into the method, only if it was valid

@raise [SmsPilot::InvalidSenderNameError] if you pass anything but nil or non-empty String

# File lib/sms_pilot/client.rb, line 498
        def validate_sender_name!(sender_name)
  fail SmsPilot::InvalidSenderNameError, "sender name must be either nil or String" unless [NilClass, String].include? sender_name.class
  fail SmsPilot::InvalidSenderNameError, "sender name cannot be empty" if sender_name == ""
  sender_name
end