smsgora 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/LICENSE +21 -0
- data/README.md +128 -0
- data/lib/smsgora.rb +178 -0
- metadata +52 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 51014d836b73e44b96133370765f4f0169def6197d5d0472fe8ef4056140c4e6
|
|
4
|
+
data.tar.gz: 6e44f8be098a567fe005026a7addc4b9c47aeeb5ef84c2f8b84114de53bae8e7
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 14af794258be6bbcd416d3945fbec73db9af1fd88e35eb8ca8f91983cb768c547ef5ed67839d67659f4dbb52df84e5e4f293888c47ceaf4b9681d8904ec22eab
|
|
7
|
+
data.tar.gz: 9bd8449a841c46cee563ce97411b447c6534f42a8127b964e0fc2783136d0c7c32ae4eb5291f8a391d46c00b7e454b68165731287d2d7c5115b2f5e344897265
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tekservix
|
|
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,128 @@
|
|
|
1
|
+
# SMS Gora Ruby client
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/smsgora)
|
|
4
|
+
|
|
5
|
+
Ruby client for the [SMS Gora SMS Gateway](https://tekservix.com/smsgora/) API.
|
|
6
|
+
|
|
7
|
+
SMS Gora turns Android phones with ordinary SIM cards into an SMS gateway. Your server calls the API, a linked phone sends the text, and replies and delivery reports come back to you. There's no sender ID registration and no per-message fee from us: your mobile operator bills the texts like any SMS.
|
|
8
|
+
|
|
9
|
+
- Ruby 2.7+, no dependencies (standard library only)
|
|
10
|
+
- Works in Rails, Sinatra, Hanami, Sidekiq jobs and scripts
|
|
11
|
+
- Webhook signature check built in
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
gem install smsgora
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
or in your Gemfile:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
gem 'smsgora'
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
Create an API key in the SMS Gora dashboard (SMS Gora -> API keys) and keep it on your server.
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
require 'smsgora'
|
|
31
|
+
|
|
32
|
+
sms = SmsGora::Client.new(api_key: ENV['SMSGORA_KEY'])
|
|
33
|
+
|
|
34
|
+
result = sms.send_sms(to: '+15551234567', message: 'Your verification code is 482913')
|
|
35
|
+
result['message_ids'] # => [1873]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Sending
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
# Several recipients (up to 1,000 per call)
|
|
42
|
+
sms.send_sms(to: ['+15551234567', '+447700900123'], message: 'Your order has shipped')
|
|
43
|
+
|
|
44
|
+
# From a chosen phone and SIM, scheduled for later
|
|
45
|
+
sms.send_sms(
|
|
46
|
+
to: '+15551234567',
|
|
47
|
+
message: 'Reminder: your appointment is tomorrow at 10:00',
|
|
48
|
+
device_id: 42,
|
|
49
|
+
sim_slot: 1, # 0 = SIM 1, 1 = SIM 2
|
|
50
|
+
schedule_at: Time.utc(2026, 12, 1, 8, 0)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Different texts for different people in one call (up to 5,000 recipients)
|
|
54
|
+
sms.send_bulk_sms(42,
|
|
55
|
+
message_template: 'Hello, your parcel is ready for pickup.',
|
|
56
|
+
messages: [
|
|
57
|
+
{ recipients: ['+15551234567'], message: 'Hi Aline, parcel #1042 is ready.' },
|
|
58
|
+
{ recipients: ['+15557654321'] }
|
|
59
|
+
])
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Delivery status and incoming SMS
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
msg = sms.get_message(1873)
|
|
66
|
+
msg['status'] # queued -> dispatched -> sent -> delivered (or failed)
|
|
67
|
+
|
|
68
|
+
sms.cancel_message(1874) # works while queued or scheduled
|
|
69
|
+
|
|
70
|
+
inbox = sms.list_received_sms(since: '2026-11-01', limit: 50)
|
|
71
|
+
phones = sms.list_devices
|
|
72
|
+
account = sms.get_account
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Anything else in the [API reference](https://tekservix.com/smsgora/docs/) is one call away:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
batch = sms.request(:get, '/batches/311')
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Webhooks
|
|
82
|
+
|
|
83
|
+
Add your HTTPS URL in SMS Gora -> Webhooks (or with `create_webhook`) and keep the secret. Each request carries `X-SmsGora-Signature: sha256=...`, an HMAC-SHA256 of the raw body. Rails example:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
class SmsWebhooksController < ActionController::API
|
|
87
|
+
def create
|
|
88
|
+
unless SmsGora.verify_webhook(request.raw_post, request.headers['X-SmsGora-Signature'], ENV['SMSGORA_WEBHOOK_SECRET'])
|
|
89
|
+
return head :unauthorized
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
event = JSON.parse(request.raw_post)
|
|
93
|
+
if event['event'] == 'sms.received'
|
|
94
|
+
IncomingSmsJob.perform_later(event['data']['from'], event['data']['message'])
|
|
95
|
+
end
|
|
96
|
+
head :ok # answer within 10 seconds; do slow work in a job
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Errors
|
|
102
|
+
|
|
103
|
+
Failed calls raise `SmsGora::Error` with `status`, `message`, field `errors` and, on 429, `retry_after` in seconds.
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
begin
|
|
107
|
+
sms.send_sms(to: '+15551234567', message: 'Hi')
|
|
108
|
+
rescue SmsGora::Error => e
|
|
109
|
+
sleep(e.retry_after || 5) if e.status == 429 # 300 requests/minute per key, or plan limit
|
|
110
|
+
end
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Never put the key in JavaScript or a mobile app: anyone could read it. Call the API from your server.
|
|
114
|
+
|
|
115
|
+
## Migrating from TextBee
|
|
116
|
+
|
|
117
|
+
SMS Gora accepts TextBee-style requests, so most integrations switch by changing the base URL and key. See the [compatibility notes](https://tekservix.com/smsgora/docs/).
|
|
118
|
+
|
|
119
|
+
## More from Tekservix
|
|
120
|
+
|
|
121
|
+
- [WAservix WhatsApp API Gateway](https://tekservix.com/waservix/): send WhatsApp messages from your own number (`gem install waservix`)
|
|
122
|
+
- [AI Sales Agent](https://tekservix.com/ai-sales-agent): answers customers on WhatsApp and SMS using your website's content
|
|
123
|
+
- [Website to App](https://tekservix.com/mobile-apps): turn your website into Android and iOS apps
|
|
124
|
+
- SMS Gora is also available for Node.js, Python, PHP, Go, Java/Kotlin and .NET: see [github.com/TekservixHQ](https://github.com/TekservixHQ)
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
MIT
|
data/lib/smsgora.rb
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'openssl'
|
|
6
|
+
require 'time'
|
|
7
|
+
require 'uri'
|
|
8
|
+
|
|
9
|
+
# Ruby client for the SMS Gora SMS Gateway API.
|
|
10
|
+
#
|
|
11
|
+
# sms = SmsGora::Client.new(api_key: ENV['SMSGORA_KEY'])
|
|
12
|
+
# sms.send_sms(to: '+15551234567', message: 'Your verification code is 482913')
|
|
13
|
+
#
|
|
14
|
+
# Docs: https://tekservix.com/smsgora/docs/
|
|
15
|
+
module SmsGora
|
|
16
|
+
VERSION = '1.0.0'
|
|
17
|
+
DEFAULT_BASE_URL = 'https://tekservix.com/smsgora/api/v1'
|
|
18
|
+
|
|
19
|
+
# Raised when the API answers with an error.
|
|
20
|
+
class Error < StandardError
|
|
21
|
+
attr_reader :status, :errors, :body, :retry_after
|
|
22
|
+
|
|
23
|
+
def initialize(message, status: nil, errors: nil, body: nil, retry_after: nil)
|
|
24
|
+
super(message)
|
|
25
|
+
@status = status
|
|
26
|
+
@errors = errors
|
|
27
|
+
@body = body
|
|
28
|
+
@retry_after = retry_after
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class Client
|
|
33
|
+
attr_reader :base_url
|
|
34
|
+
|
|
35
|
+
# api_key: your SMS Gora API key (starts with sgk_). Keep it on your server.
|
|
36
|
+
def initialize(api_key:, base_url: DEFAULT_BASE_URL, timeout: 30)
|
|
37
|
+
raise ArgumentError, 'SmsGora: api_key is required (create one in SMS Gora -> API keys)' if api_key.nil? || api_key.empty?
|
|
38
|
+
|
|
39
|
+
@api_key = api_key
|
|
40
|
+
@base_url = base_url.sub(%r{/+\z}, '')
|
|
41
|
+
@timeout = timeout
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Call any documented endpoint, e.g. request(:get, '/devices'). Returns the `data` field.
|
|
45
|
+
def request(method, path, body = nil)
|
|
46
|
+
uri = URI(@base_url + path)
|
|
47
|
+
klass = { get: Net::HTTP::Get, post: Net::HTTP::Post, put: Net::HTTP::Put, delete: Net::HTTP::Delete }.fetch(method.to_s.downcase.to_sym)
|
|
48
|
+
req = klass.new(uri)
|
|
49
|
+
req['x-api-key'] = @api_key
|
|
50
|
+
req['Accept'] = 'application/json'
|
|
51
|
+
req['User-Agent'] = "smsgora-ruby/#{VERSION}"
|
|
52
|
+
unless body.nil?
|
|
53
|
+
req['Content-Type'] = 'application/json'
|
|
54
|
+
req.body = JSON.generate(body)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https',
|
|
58
|
+
open_timeout: @timeout, read_timeout: @timeout) { |http| http.request(req) }
|
|
59
|
+
status = res.code.to_i
|
|
60
|
+
text = res.body.to_s
|
|
61
|
+
json = begin
|
|
62
|
+
text.empty? ? nil : JSON.parse(text)
|
|
63
|
+
rescue JSON::ParserError
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
if status >= 400 || (json.is_a?(Hash) && json['success'] == false)
|
|
68
|
+
retry_after = res['Retry-After']
|
|
69
|
+
raise Error.new((json.is_a?(Hash) && json['message']) || "HTTP #{status}",
|
|
70
|
+
status: status,
|
|
71
|
+
errors: json.is_a?(Hash) ? json['errors'] : nil,
|
|
72
|
+
body: json || text,
|
|
73
|
+
retry_after: retry_after && Float(retry_after, exception: false))
|
|
74
|
+
end
|
|
75
|
+
json.is_a?(Hash) && json.key?('data') ? json['data'] : json
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Send an SMS to one number or up to 1,000. Picks your best online phone unless device_id is given.
|
|
79
|
+
# sim_slot: 0 = SIM 1, 1 = SIM 2. schedule_at: Time or ISO 8601 string.
|
|
80
|
+
def send_sms(to:, message:, device_id: nil, sim_slot: nil, schedule_at: nil)
|
|
81
|
+
body = { recipients: Array(to), message: message }
|
|
82
|
+
body[:sim_slot] = sim_slot unless sim_slot.nil?
|
|
83
|
+
body[:schedule_at] = schedule_at.respond_to?(:utc) ? schedule_at.utc.iso8601 : schedule_at if schedule_at
|
|
84
|
+
path = device_id ? "/devices/#{esc(device_id)}/send-sms" : '/send-sms'
|
|
85
|
+
request(:post, path, body)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Different texts for different people in one call (up to 5,000 recipients).
|
|
89
|
+
# messages: [{ recipients: ['+1555...'], message: 'optional own text' }, ...]
|
|
90
|
+
def send_bulk_sms(device_id, messages:, message_template: nil)
|
|
91
|
+
body = { messages: messages }
|
|
92
|
+
body[:messageTemplate] = message_template if message_template
|
|
93
|
+
request(:post, "/devices/#{esc(device_id)}/send-bulk-sms", body)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Status is queued, dispatched, sent, delivered, failed or cancelled.
|
|
97
|
+
def get_message(id)
|
|
98
|
+
request(:get, "/messages/#{esc(id)}")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Cancel a queued or scheduled message.
|
|
102
|
+
def cancel_message(id)
|
|
103
|
+
request(:post, "/messages/#{esc(id)}/cancel")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Filters: direction (in/out), status, device_id, phone, since, after_id, page, limit.
|
|
107
|
+
def list_messages(**filters)
|
|
108
|
+
request(:get, "/messages#{query(filters)}")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Incoming SMS for all phones, or one phone with device_id.
|
|
112
|
+
def list_received_sms(device_id: nil, **filters)
|
|
113
|
+
path = device_id ? "/devices/#{esc(device_id)}/get-received-sms" : '/received-sms'
|
|
114
|
+
request(:get, path + query(filters))
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def get_batch(id)
|
|
118
|
+
request(:get, "/batches/#{esc(id)}")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Your linked Android phones.
|
|
122
|
+
def list_devices
|
|
123
|
+
request(:get, '/devices')
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def get_device(id)
|
|
127
|
+
request(:get, "/devices/#{esc(id)}")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Account, plan and usage.
|
|
131
|
+
def get_account
|
|
132
|
+
request(:get, '/account')
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def list_webhooks
|
|
136
|
+
request(:get, '/webhooks')
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Register a webhook. The answer includes the secret (only returned once).
|
|
140
|
+
def create_webhook(url:, events: ['sms.received'], device_id: nil)
|
|
141
|
+
request(:post, '/webhooks', { url: url, events: events, device_id: device_id })
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def delete_webhook(id)
|
|
145
|
+
request(:delete, "/webhooks/#{esc(id)}")
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
def esc(value)
|
|
151
|
+
URI.encode_www_form_component(value.to_s).gsub('+', '%20')
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def query(params)
|
|
155
|
+
clean = params.reject { |_, v| v.nil? || v.to_s.empty? }
|
|
156
|
+
clean.empty? ? '' : "?#{URI.encode_www_form(clean)}"
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Check the X-SmsGora-Signature header (sha256=<hex HMAC-SHA256 of the raw body>).
|
|
161
|
+
# Pass the raw request body (e.g. request.raw_post in Rails), before JSON parsing.
|
|
162
|
+
def self.verify_webhook(raw_body, signature, secret)
|
|
163
|
+
return false if raw_body.nil? || raw_body.empty? || signature.nil? || signature.empty? || secret.nil? || secret.empty?
|
|
164
|
+
|
|
165
|
+
expected = "sha256=#{OpenSSL::HMAC.hexdigest('SHA256', secret, raw_body)}"
|
|
166
|
+
secure_compare(expected, signature.to_s)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Constant-time string comparison (works on every Ruby version).
|
|
170
|
+
def self.secure_compare(a, b)
|
|
171
|
+
return false unless a.bytesize == b.bytesize
|
|
172
|
+
|
|
173
|
+
diff = 0
|
|
174
|
+
a.bytes.zip(b.bytes) { |x, y| diff |= x ^ y }
|
|
175
|
+
diff.zero?
|
|
176
|
+
end
|
|
177
|
+
private_class_method :secure_compare
|
|
178
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: smsgora
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Tekservix
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-11-10 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: 'Send and receive SMS through your own Android phones with SMS Gora:
|
|
14
|
+
delivery reports, scheduled and bulk sending, and signed webhooks. Works in Rails,
|
|
15
|
+
Sinatra and plain Ruby.'
|
|
16
|
+
email: support@tekservix.com
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- LICENSE
|
|
22
|
+
- README.md
|
|
23
|
+
- lib/smsgora.rb
|
|
24
|
+
homepage: https://tekservix.com/smsgora/
|
|
25
|
+
licenses:
|
|
26
|
+
- MIT
|
|
27
|
+
metadata:
|
|
28
|
+
homepage_uri: https://tekservix.com/smsgora/
|
|
29
|
+
documentation_uri: https://tekservix.com/smsgora/docs/
|
|
30
|
+
source_code_uri: https://github.com/TekservixHQ/smsgora-ruby
|
|
31
|
+
bug_tracker_uri: https://github.com/TekservixHQ/smsgora-ruby/issues
|
|
32
|
+
rubygems_mfa_required: 'true'
|
|
33
|
+
post_install_message:
|
|
34
|
+
rdoc_options: []
|
|
35
|
+
require_paths:
|
|
36
|
+
- lib
|
|
37
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
38
|
+
requirements:
|
|
39
|
+
- - ">="
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '2.7'
|
|
42
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
requirements: []
|
|
48
|
+
rubygems_version: 3.5.22
|
|
49
|
+
signing_key:
|
|
50
|
+
specification_version: 4
|
|
51
|
+
summary: Official Ruby client for the SMS Gora SMS Gateway API
|
|
52
|
+
test_files: []
|