hellio-messaging 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/CHANGELOG.md +29 -0
- data/LICENSE +21 -0
- data/README.md +144 -0
- data/lib/hellio/client.rb +266 -0
- data/lib/hellio/errors.rb +39 -0
- data/lib/hellio/http.rb +38 -0
- data/lib/hellio/version.rb +5 -0
- data/lib/hellio.rb +16 -0
- metadata +99 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 45de36c0b40d47f5073fcce914ac1f18583a7ef9a13ad128deed76abdc6205f7
|
|
4
|
+
data.tar.gz: 4e68b72f61d9c5431807c4797b81a75ad445ba1ed5b941f3fa8b69e384ebf98b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: '09058b69a323ccdafc77293a97f0fcadb8567c4bf3cfa3f01c97b675ef7690eff30515da914bdc050f637d8b25092367d3bd844c1d7898beaebded4c2fa621b3'
|
|
7
|
+
data.tar.gz: 244a062620713196303429e28b74db4073f9120b7a9d818e3bc00862198c944a4c998b5fd206cd018a74897f237945d0cca9775681e3dbceaa977f74fef9e116
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `hellio-messaging` are documented here.
|
|
4
|
+
This project follows [Semantic Versioning](https://semver.org).
|
|
5
|
+
|
|
6
|
+
## [1.0.0] - 2026-07-05
|
|
7
|
+
|
|
8
|
+
Initial release of the official Ruby SDK for the Hellio Messaging API v1.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `Hellio::Client` with Bearer-token auth and one method per endpoint:
|
|
12
|
+
- Account: `balance`, `pricing`.
|
|
13
|
+
- SMS: `sms`, `message`, `campaign`.
|
|
14
|
+
- OTP: `otp`, `verify_otp`, `verify` (channels `sms`, `email`, `voice`).
|
|
15
|
+
- Voice: `voice`, `voice_status`.
|
|
16
|
+
- Number lookup (HLR): `lookup`, `lookups`, `lookup_result`.
|
|
17
|
+
- Email verification: `verify_email`.
|
|
18
|
+
- Webhooks: `webhooks`, `create_webhook`, `delete_webhook`.
|
|
19
|
+
- Recipient normalization: methods accept a single string, a comma-separated
|
|
20
|
+
string, or an array.
|
|
21
|
+
- Typed errors: `Hellio::InvalidApiTokenError` (401),
|
|
22
|
+
`Hellio::InsufficientBalanceError` (402), `Hellio::ValidationError` (422, with
|
|
23
|
+
`#errors`), `Hellio::RateLimitError` (429), and `Hellio::Error` (base). Each
|
|
24
|
+
carries `status_code` and the parsed `response` body.
|
|
25
|
+
- Configuration via constructor or the `HELLIO_API_TOKEN`, `HELLIO_BASE_URL`,
|
|
26
|
+
and `HELLIO_DEFAULT_SENDER` environment variables.
|
|
27
|
+
- Pluggable HTTP adapter (defaults to the standard library `net/http`) so tests
|
|
28
|
+
can inject a fake transport.
|
|
29
|
+
- RSpec suite with WebMock and a GitHub Actions matrix (Ruby 3.1, 3.2, 3.3).
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Albert Ninyeh, Hellio Solutions
|
|
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,144 @@
|
|
|
1
|
+
# Hellio Messaging - Official Ruby SDK
|
|
2
|
+
|
|
3
|
+
[](https://github.com/HellioSolutions/hellio-ruby/actions/workflows/tests.yml)
|
|
4
|
+
[](https://rubygems.org/gems/hellio-messaging)
|
|
5
|
+
[](https://rubygems.org/gems/hellio-messaging)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
Ruby client for the [Hellio Messaging](https://helliomessaging.com) API v1:
|
|
9
|
+
**SMS**, **OTP** (SMS / email / voice), **Voice broadcasts**, **Number Lookup (HLR)**,
|
|
10
|
+
**Email Verification**, **Pricing**, **Balance**, and **Webhooks**. It uses only
|
|
11
|
+
the Ruby standard library (`net/http` and `json`), so there are no runtime
|
|
12
|
+
dependencies.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
gem install hellio-messaging
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Or with Bundler, add to your `Gemfile`:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
gem "hellio-messaging"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
then run `bundle install`.
|
|
27
|
+
|
|
28
|
+
## Configure
|
|
29
|
+
|
|
30
|
+
Generate a token in your dashboard (**Settings -> API -> Generate API token**),
|
|
31
|
+
then create a client. You can pass values directly or set environment variables.
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
require "hellio"
|
|
35
|
+
|
|
36
|
+
client = Hellio::Client.new(
|
|
37
|
+
token: "your-token-here",
|
|
38
|
+
default_sender: "HellioSMS"
|
|
39
|
+
)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Environment variables are read as fallbacks when an option is omitted:
|
|
43
|
+
|
|
44
|
+
```dotenv
|
|
45
|
+
HELLIO_BASE_URL=https://api.helliomessaging.com/v1
|
|
46
|
+
HELLIO_API_TOKEN=your-token-here
|
|
47
|
+
HELLIO_DEFAULT_SENDER=HellioSMS
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
client = Hellio::Client.new # reads HELLIO_API_TOKEN, HELLIO_BASE_URL, HELLIO_DEFAULT_SENDER
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Optional constructor options: `base_url` (defaults to
|
|
55
|
+
`https://api.helliomessaging.com/v1`), `timeout` (seconds, default 30), and
|
|
56
|
+
`http` (a custom HTTP adapter, mainly for tests).
|
|
57
|
+
|
|
58
|
+
Every call returns the decoded JSON as a `Hash` with string keys (payloads are
|
|
59
|
+
under the `"data"` key). Non-2xx responses raise a typed error (see below).
|
|
60
|
+
|
|
61
|
+
## Usage
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
# Account
|
|
65
|
+
client.balance # {"data" => {"balance" => "195.0000", "available" => "194.65", ...}}
|
|
66
|
+
client.pricing("GH") # optional ISO-2 country filter
|
|
67
|
+
|
|
68
|
+
# SMS (recipients: string, comma list, or array)
|
|
69
|
+
client.sms("233241234567", "Hello!")
|
|
70
|
+
client.sms(["233241234567", "233201234567"], "Hi all", sender: "HellioSMS")
|
|
71
|
+
client.message(1024) # delivery status
|
|
72
|
+
client.campaign(1024) # campaign summary
|
|
73
|
+
|
|
74
|
+
# OTP - sender (Sender ID) is REQUIRED for sms/voice and must be approved on your account.
|
|
75
|
+
# Optional length (4-10 digits) and expiry (minutes). Returns status "queued".
|
|
76
|
+
client.otp("233241234567", sender: "HellioSMS") # SMS
|
|
77
|
+
client.otp("233241234567", sender: "HellioSMS", channel: "voice") # Voice (TTS reads the code)
|
|
78
|
+
client.otp("233241234567", sender: "HellioSMS", length: 6, expiry: 10)
|
|
79
|
+
client.otp("user@example.com", channel: "email") # Email (no sender)
|
|
80
|
+
client.verify("233241234567", "123456") # true / false
|
|
81
|
+
client.verify_otp("user@example.com", "123456", channel: "email") # full response
|
|
82
|
+
|
|
83
|
+
# Voice broadcast - text (we TTS it) or a hosted audio_url
|
|
84
|
+
client.voice("233241234567", "HELLIO", text: "Your code is 1 2 3 4")
|
|
85
|
+
client.voice(["233241234567"], "HELLIO", audio_url: "https://cdn.example.com/promo.mp3")
|
|
86
|
+
|
|
87
|
+
# Number lookup (HLR) - async; poll results
|
|
88
|
+
client.lookup(["233241234567"])
|
|
89
|
+
client.lookups
|
|
90
|
+
client.lookup_result(5)
|
|
91
|
+
|
|
92
|
+
# Email verification
|
|
93
|
+
client.verify_email(["user@gmail.com", "bad@nodomain.invalid"])
|
|
94
|
+
|
|
95
|
+
# Webhooks (receive delivery reports)
|
|
96
|
+
client.create_webhook("https://your-app.com/hooks/hellio", events: ["message.delivered", "message.failed"])
|
|
97
|
+
client.webhooks
|
|
98
|
+
client.delete_webhook(1)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Recipient inputs (`sms`, `voice`, `lookup`, `verify_email`) accept a single
|
|
102
|
+
string, a comma-separated string, or an array. They are normalized to a list
|
|
103
|
+
before the request is sent.
|
|
104
|
+
|
|
105
|
+
## Error handling
|
|
106
|
+
|
|
107
|
+
Non-2xx responses raise typed errors (all subclass `Hellio::Error`). Each error
|
|
108
|
+
carries `status_code` and the parsed `response` body; `ValidationError` also
|
|
109
|
+
exposes field-level details through `#errors`.
|
|
110
|
+
|
|
111
|
+
| Error | Status |
|
|
112
|
+
|---|---|
|
|
113
|
+
| `Hellio::InvalidApiTokenError` | 401 |
|
|
114
|
+
| `Hellio::InsufficientBalanceError` | 402 |
|
|
115
|
+
| `Hellio::ValidationError` (`#errors`) | 422 |
|
|
116
|
+
| `Hellio::RateLimitError` | 429 |
|
|
117
|
+
| `Hellio::Error` | other |
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
begin
|
|
121
|
+
client.sms("233241234567", "Hi")
|
|
122
|
+
rescue Hellio::InsufficientBalanceError => e
|
|
123
|
+
# top up
|
|
124
|
+
rescue Hellio::ValidationError => e
|
|
125
|
+
e.errors # field-level messages
|
|
126
|
+
e.status_code # 422
|
|
127
|
+
e.response # full parsed body
|
|
128
|
+
end
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Rate limit: **120 requests/minute** per token.
|
|
132
|
+
|
|
133
|
+
## Development
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
bundle install
|
|
137
|
+
bundle exec rake spec
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Tests use RSpec and WebMock to mock the HTTP layer.
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
require "hellio/errors"
|
|
7
|
+
require "hellio/http"
|
|
8
|
+
|
|
9
|
+
module Hellio
|
|
10
|
+
# Hellio Messaging API v1 client. Authenticates with a Bearer token and exposes
|
|
11
|
+
# one method per endpoint. Every call returns the decoded JSON as a Hash with
|
|
12
|
+
# string keys (payloads are under "data"); non-2xx responses raise a typed
|
|
13
|
+
# Hellio::Error subclass.
|
|
14
|
+
class Client
|
|
15
|
+
DEFAULT_BASE_URL = "https://api.helliomessaging.com/v1"
|
|
16
|
+
DEFAULT_TIMEOUT = 30
|
|
17
|
+
|
|
18
|
+
attr_reader :base_url, :timeout, :default_sender
|
|
19
|
+
|
|
20
|
+
# token - API token (falls back to HELLIO_API_TOKEN).
|
|
21
|
+
# base_url - API base URL (falls back to HELLIO_BASE_URL, then default).
|
|
22
|
+
# timeout - request timeout in seconds (default 30).
|
|
23
|
+
# default_sender - Sender ID used by #sms (falls back to HELLIO_DEFAULT_SENDER).
|
|
24
|
+
# http - HTTP adapter; inject a fake in tests. Defaults to net/http.
|
|
25
|
+
def initialize(token: nil, base_url: nil, timeout: nil, default_sender: nil, http: nil)
|
|
26
|
+
@token = token || ENV["HELLIO_API_TOKEN"]
|
|
27
|
+
@base_url = (base_url || ENV["HELLIO_BASE_URL"] || DEFAULT_BASE_URL).sub(%r{/+\z}, "")
|
|
28
|
+
@timeout = timeout || DEFAULT_TIMEOUT
|
|
29
|
+
@default_sender = default_sender || ENV["HELLIO_DEFAULT_SENDER"]
|
|
30
|
+
@http = http || NetHttpAdapter.new
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------- Account
|
|
34
|
+
|
|
35
|
+
# Current account balance and available credit.
|
|
36
|
+
def balance
|
|
37
|
+
get("balance")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Per-network SMS pricing. Pass an ISO-2 country code to narrow by country.
|
|
41
|
+
def pricing(country = nil)
|
|
42
|
+
get("pricing", country.nil? ? {} : { "country" => country })
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# -------------------------------------------------------------------- SMS
|
|
46
|
+
|
|
47
|
+
# Send an SMS. recipients may be a single string, a comma-separated string,
|
|
48
|
+
# or an array of numbers.
|
|
49
|
+
def sms(recipients, message, sender: nil, gateway: nil)
|
|
50
|
+
post("sms/send", compact(
|
|
51
|
+
"recipients" => to_list(recipients),
|
|
52
|
+
"sender" => sender || default_sender,
|
|
53
|
+
"message" => message,
|
|
54
|
+
"gateway" => gateway
|
|
55
|
+
))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Delivery status for a single message.
|
|
59
|
+
def message(id)
|
|
60
|
+
get("messages/#{id}")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Summary for a single campaign.
|
|
64
|
+
def campaign(id)
|
|
65
|
+
get("campaigns/#{id}")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# -------------------------------------------------------------------- OTP
|
|
69
|
+
|
|
70
|
+
# Send a one-time passcode. `to` is a phone number (sms/voice/whatsapp) or an
|
|
71
|
+
# email (email). `sender` (Sender ID) is required for sms/voice and must be
|
|
72
|
+
# approved on your account; it is ignored for whatsapp and email. Optional
|
|
73
|
+
# `length` (4-10 digits) and `expiry` (validity in minutes).
|
|
74
|
+
def otp(to, sender: nil, channel: "sms", purpose: nil, length: nil, expiry: nil, gateway: nil)
|
|
75
|
+
destination_key = (channel == "email" ? "email" : "mobile_number")
|
|
76
|
+
|
|
77
|
+
post("otp/send", compact(
|
|
78
|
+
"channel" => channel,
|
|
79
|
+
destination_key => to,
|
|
80
|
+
"sender" => sender,
|
|
81
|
+
"purpose" => purpose,
|
|
82
|
+
"length" => length,
|
|
83
|
+
"expiry" => expiry,
|
|
84
|
+
"gateway" => gateway
|
|
85
|
+
))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Verify a one-time passcode and return the full response.
|
|
89
|
+
def verify_otp(to, code, channel: "sms")
|
|
90
|
+
destination_key = (channel == "email" ? "email" : "mobile_number")
|
|
91
|
+
|
|
92
|
+
post("otp/verify", compact(
|
|
93
|
+
"channel" => channel,
|
|
94
|
+
destination_key => to,
|
|
95
|
+
"code" => code
|
|
96
|
+
))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Convenience wrapper: true when the code is valid, false otherwise.
|
|
100
|
+
# A 422 validation failure is treated as "not verified".
|
|
101
|
+
def verify(to, code, channel: "sms")
|
|
102
|
+
result = verify_otp(to, code, channel: channel)
|
|
103
|
+
truthy?(result.is_a?(Hash) ? result.dig("data", "verified") : nil)
|
|
104
|
+
rescue ValidationError
|
|
105
|
+
false
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# ------------------------------------------------------------------ Voice
|
|
109
|
+
|
|
110
|
+
# Voice broadcast. Provide `text` (read with TTS) OR `audio_url` (fetched
|
|
111
|
+
# and played). recipients accepts the same shapes as #sms.
|
|
112
|
+
def voice(recipients, caller_id, text: nil, audio_url: nil, name: nil)
|
|
113
|
+
post("voice/send", compact(
|
|
114
|
+
"recipients" => to_list(recipients),
|
|
115
|
+
"caller_id" => caller_id,
|
|
116
|
+
"text" => text,
|
|
117
|
+
"audio_url" => audio_url,
|
|
118
|
+
"name" => name
|
|
119
|
+
))
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Status for a single voice broadcast.
|
|
123
|
+
def voice_status(id)
|
|
124
|
+
get("voice/#{id}")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# ----------------------------------------------------------- Number lookup
|
|
128
|
+
|
|
129
|
+
# Submit numbers for HLR lookup. numbers accepts a string, comma list, or array.
|
|
130
|
+
def lookup(numbers)
|
|
131
|
+
post("lookup", "numbers" => to_list(numbers))
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# List previously submitted lookups.
|
|
135
|
+
def lookups
|
|
136
|
+
get("lookups")
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Result for a single lookup.
|
|
140
|
+
def lookup_result(id)
|
|
141
|
+
get("lookup/#{id}")
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# ------------------------------------------------------- Email verification
|
|
145
|
+
|
|
146
|
+
# Verify one or more email addresses. emails accepts a string, comma list, or array.
|
|
147
|
+
def verify_email(emails)
|
|
148
|
+
post("email/verify", "emails" => to_list(emails))
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# --------------------------------------------------------------- Webhooks
|
|
152
|
+
|
|
153
|
+
# List registered webhooks.
|
|
154
|
+
def webhooks
|
|
155
|
+
get("webhooks")
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Register a webhook. Pass `events` to subscribe to specific event types.
|
|
159
|
+
def create_webhook(url, events: [])
|
|
160
|
+
post("webhooks", compact(
|
|
161
|
+
"url" => url,
|
|
162
|
+
"events" => (events.nil? || events.empty? ? nil : events)
|
|
163
|
+
))
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Delete a webhook by id.
|
|
167
|
+
def delete_webhook(id)
|
|
168
|
+
delete("webhooks/#{id}")
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# --------------------------------------------------------------- internals
|
|
172
|
+
|
|
173
|
+
private
|
|
174
|
+
|
|
175
|
+
def get(path, query = {})
|
|
176
|
+
request(:get, path, query: query)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def post(path, body = {})
|
|
180
|
+
request(:post, path, body: body)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def delete(path)
|
|
184
|
+
request(:delete, path)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def request(method, path, query: {}, body: nil)
|
|
188
|
+
response = @http.call(
|
|
189
|
+
method: method,
|
|
190
|
+
url: build_url(path, query),
|
|
191
|
+
headers: headers,
|
|
192
|
+
body: body.nil? ? nil : JSON.generate(body),
|
|
193
|
+
timeout: timeout
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
data = parse(response.body)
|
|
197
|
+
return data if response.status.to_i.between?(200, 299)
|
|
198
|
+
|
|
199
|
+
raise error_for(response.status.to_i, data)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def headers
|
|
203
|
+
{
|
|
204
|
+
"Authorization" => "Bearer #{@token}",
|
|
205
|
+
"Accept" => "application/json",
|
|
206
|
+
"Content-Type" => "application/json"
|
|
207
|
+
}
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def build_url(path, query)
|
|
211
|
+
url = "#{base_url}/#{path.sub(%r{\A/+}, "")}"
|
|
212
|
+
return url if query.nil? || query.empty?
|
|
213
|
+
|
|
214
|
+
"#{url}?#{URI.encode_www_form(query)}"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def parse(body)
|
|
218
|
+
return {} if body.nil? || body.to_s.empty?
|
|
219
|
+
|
|
220
|
+
JSON.parse(body)
|
|
221
|
+
rescue JSON::ParserError
|
|
222
|
+
{}
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def error_for(status, data)
|
|
226
|
+
message =
|
|
227
|
+
if data.is_a?(Hash) && data["message"].is_a?(String)
|
|
228
|
+
data["message"]
|
|
229
|
+
else
|
|
230
|
+
"Hellio API request failed."
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
error_class =
|
|
234
|
+
case status
|
|
235
|
+
when 401 then InvalidApiTokenError
|
|
236
|
+
when 402 then InsufficientBalanceError
|
|
237
|
+
when 422 then ValidationError
|
|
238
|
+
when 429 then RateLimitError
|
|
239
|
+
when 503 then ServiceUnavailableError
|
|
240
|
+
else Error
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
error_class.new(message, status_code: status, response: data)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Normalize a recipient/number/email input into an array of strings.
|
|
247
|
+
def to_list(value)
|
|
248
|
+
return value.map(&:to_s) if value.is_a?(Array)
|
|
249
|
+
|
|
250
|
+
value.to_s.split(",").map(&:strip).reject(&:empty?)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Drop keys whose value is nil so they are omitted from the request body.
|
|
254
|
+
def compact(hash)
|
|
255
|
+
hash.reject { |_, v| v.nil? }
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def truthy?(value)
|
|
259
|
+
return false if value.nil? || value == false
|
|
260
|
+
return false if value == 0
|
|
261
|
+
return false if value.is_a?(String) && (value.empty? || value == "0")
|
|
262
|
+
|
|
263
|
+
true
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hellio
|
|
4
|
+
# Base error for every failure returned by the Hellio Messaging API.
|
|
5
|
+
# Carries the HTTP status code and the parsed response body so callers can
|
|
6
|
+
# inspect the details (for 422, read #errors for field-level messages).
|
|
7
|
+
class Error < StandardError
|
|
8
|
+
attr_reader :status_code, :response
|
|
9
|
+
|
|
10
|
+
def initialize(message = nil, status_code: nil, response: nil)
|
|
11
|
+
super(message)
|
|
12
|
+
@status_code = status_code
|
|
13
|
+
@response = response
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Field-level validation details, present on 422 responses.
|
|
17
|
+
def errors
|
|
18
|
+
return nil unless response.is_a?(Hash)
|
|
19
|
+
|
|
20
|
+
response["errors"]
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# 401: the API token is missing, malformed, or revoked.
|
|
25
|
+
class InvalidApiTokenError < Error; end
|
|
26
|
+
|
|
27
|
+
# 402: the account balance is too low to complete the request.
|
|
28
|
+
class InsufficientBalanceError < Error; end
|
|
29
|
+
|
|
30
|
+
# 422: the request failed validation. See #errors for the details.
|
|
31
|
+
class ValidationError < Error; end
|
|
32
|
+
|
|
33
|
+
# 429: too many requests. The limit is 120 requests per minute per token.
|
|
34
|
+
class RateLimitError < Error; end
|
|
35
|
+
|
|
36
|
+
# 503: the service is temporarily unavailable, because an admin switched it off
|
|
37
|
+
# (SMS, OTP, voice, WhatsApp, lookup, email) or the API is paused. Retry later.
|
|
38
|
+
class ServiceUnavailableError < Error; end
|
|
39
|
+
end
|
data/lib/hellio/http.rb
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module Hellio
|
|
7
|
+
# A minimal HTTP response value object returned by an adapter.
|
|
8
|
+
Response = Struct.new(:status, :body)
|
|
9
|
+
|
|
10
|
+
# Default HTTP adapter built on the standard library `net/http`. Tests (or
|
|
11
|
+
# advanced users) can inject any object that responds to #call with the same
|
|
12
|
+
# keyword arguments and returns an object exposing #status and #body.
|
|
13
|
+
class NetHttpAdapter
|
|
14
|
+
def call(method:, url:, headers:, body:, timeout:)
|
|
15
|
+
uri = URI.parse(url)
|
|
16
|
+
|
|
17
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
18
|
+
http.use_ssl = (uri.scheme == "https")
|
|
19
|
+
http.open_timeout = timeout
|
|
20
|
+
http.read_timeout = timeout
|
|
21
|
+
|
|
22
|
+
request_class =
|
|
23
|
+
case method
|
|
24
|
+
when :get then Net::HTTP::Get
|
|
25
|
+
when :post then Net::HTTP::Post
|
|
26
|
+
when :delete then Net::HTTP::Delete
|
|
27
|
+
else raise ArgumentError, "Unsupported HTTP method: #{method}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
request = request_class.new(uri.request_uri)
|
|
31
|
+
headers.each { |key, value| request[key] = value }
|
|
32
|
+
request.body = body unless body.nil?
|
|
33
|
+
|
|
34
|
+
response = http.request(request)
|
|
35
|
+
Response.new(response.code.to_i, response.body.to_s)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/hellio.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "hellio/version"
|
|
4
|
+
require "hellio/errors"
|
|
5
|
+
require "hellio/http"
|
|
6
|
+
require "hellio/client"
|
|
7
|
+
|
|
8
|
+
# Official Ruby SDK for the Hellio Messaging API v1.
|
|
9
|
+
#
|
|
10
|
+
# Quick start:
|
|
11
|
+
#
|
|
12
|
+
# client = Hellio::Client.new(token: "your-token")
|
|
13
|
+
# client.sms("233241234567", "Hello!")
|
|
14
|
+
#
|
|
15
|
+
module Hellio
|
|
16
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: hellio-messaging
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Albert Ninyeh
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-07-05 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rake
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '13.0'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '13.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rspec
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '3.12'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.12'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: webmock
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '3.19'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '3.19'
|
|
55
|
+
description: 'Ruby client for the Hellio Messaging API v1: SMS, OTP (SMS / email /
|
|
56
|
+
voice), voice broadcasts, number lookup (HLR), email verification, pricing, balance,
|
|
57
|
+
and webhooks.'
|
|
58
|
+
email:
|
|
59
|
+
- eaglesecurity0@gmail.com
|
|
60
|
+
executables: []
|
|
61
|
+
extensions: []
|
|
62
|
+
extra_rdoc_files: []
|
|
63
|
+
files:
|
|
64
|
+
- CHANGELOG.md
|
|
65
|
+
- LICENSE
|
|
66
|
+
- README.md
|
|
67
|
+
- lib/hellio.rb
|
|
68
|
+
- lib/hellio/client.rb
|
|
69
|
+
- lib/hellio/errors.rb
|
|
70
|
+
- lib/hellio/http.rb
|
|
71
|
+
- lib/hellio/version.rb
|
|
72
|
+
homepage: https://github.com/HellioSolutions/hellio-ruby
|
|
73
|
+
licenses:
|
|
74
|
+
- MIT
|
|
75
|
+
metadata:
|
|
76
|
+
source_code_uri: https://github.com/HellioSolutions/hellio-ruby
|
|
77
|
+
changelog_uri: https://github.com/HellioSolutions/hellio-ruby/blob/main/CHANGELOG.md
|
|
78
|
+
bug_tracker_uri: https://github.com/HellioSolutions/hellio-ruby/issues
|
|
79
|
+
documentation_uri: https://helliomessaging.com
|
|
80
|
+
post_install_message:
|
|
81
|
+
rdoc_options: []
|
|
82
|
+
require_paths:
|
|
83
|
+
- lib
|
|
84
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '3.1'
|
|
89
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
90
|
+
requirements:
|
|
91
|
+
- - ">="
|
|
92
|
+
- !ruby/object:Gem::Version
|
|
93
|
+
version: '0'
|
|
94
|
+
requirements: []
|
|
95
|
+
rubygems_version: 3.0.3.1
|
|
96
|
+
signing_key:
|
|
97
|
+
specification_version: 4
|
|
98
|
+
summary: Official Ruby SDK for the Hellio Messaging API v1.
|
|
99
|
+
test_files: []
|