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.
Files changed (5) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +128 -0
  4. data/lib/smsgora.rb +178 -0
  5. 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
+ [![Gem Version](https://badge.fury.io/rb/smsgora.svg)](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: []