kwtsms 0.1.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 +37 -0
- data/LICENSE +21 -0
- data/README.md +386 -0
- data/exe/kwtsms +222 -0
- data/lib/kwtsms/client.rb +395 -0
- data/lib/kwtsms/env_loader.rb +34 -0
- data/lib/kwtsms/errors.rb +51 -0
- data/lib/kwtsms/logger.rb +19 -0
- data/lib/kwtsms/message.rb +86 -0
- data/lib/kwtsms/phone.rb +64 -0
- data/lib/kwtsms/request.rb +80 -0
- data/lib/kwtsms/version.rb +5 -0
- data/lib/kwtsms.rb +30 -0
- metadata +65 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c507b4a27f1627ba000c997feb953c77a79d2f3328848c251d3159e18cc9007c
|
|
4
|
+
data.tar.gz: e645bfa974ec35ab4ed008258aa255a05fe3153ed433658c8fbcc4a3c88413bf
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: fbe4ee64c98c0335646327309248eebe99336e96da957fc23341de1ddd0801957a98306a57c33d7043ac21c4ef8e843c91d804e7354b3ab2eeb61430b1cc6a00
|
|
7
|
+
data.tar.gz: 37a77cce2060b5a97682e7dc74b5c13c80cfc1d5dfb8f8ba8fae7206ce402e97bfce0ff5f90b0a648bf139bc6878bbf84e94979d876b1a2625877dc2c4f22f32
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-03-06
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Initial release of the kwtsms Ruby gem
|
|
13
|
+
- `KwtSMS::Client` class with full API coverage:
|
|
14
|
+
- `verify` : test credentials and check balance
|
|
15
|
+
- `balance` : get current balance
|
|
16
|
+
- `send_sms` : send SMS to one or more numbers
|
|
17
|
+
- `send_with_retry` : send with automatic ERR028 retry
|
|
18
|
+
- `status` : get delivery report for a message
|
|
19
|
+
- `senderids` : list registered sender IDs
|
|
20
|
+
- `coverage` : list active country prefixes
|
|
21
|
+
- `validate` : validate phone numbers
|
|
22
|
+
- `KwtSMS::Client.from_env` factory method for loading credentials from environment variables or `.env` file
|
|
23
|
+
- Utility functions:
|
|
24
|
+
- `KwtSMS.normalize_phone` : normalize phone numbers (Arabic digits, strip non-digits, strip leading zeros)
|
|
25
|
+
- `KwtSMS.validate_phone_input` : validate phone input with detailed error messages
|
|
26
|
+
- `KwtSMS.clean_message` : clean SMS text (strip emojis, HTML, hidden chars, convert Arabic digits)
|
|
27
|
+
- `KwtSMS.enrich_error` : add developer-friendly action messages to API errors
|
|
28
|
+
- `KwtSMS::API_ERRORS` constant with all 33 kwtSMS error codes mapped to action messages
|
|
29
|
+
- Bulk send support: auto-batching for >200 numbers with ERR013 retry and backoff
|
|
30
|
+
- Phone number deduplication before sending
|
|
31
|
+
- JSONL logging with password masking
|
|
32
|
+
- CLI tool (`kwtsms`) with setup, verify, balance, senderid, coverage, send, and validate commands
|
|
33
|
+
- Zero external runtime dependencies (uses Ruby stdlib only)
|
|
34
|
+
- Comprehensive test suite: unit tests, mocked API tests, and real integration tests
|
|
35
|
+
- Examples: basic usage, OTP flow, bulk SMS, Rails endpoint, error handling, production OTP
|
|
36
|
+
|
|
37
|
+
[0.1.0]: https://github.com/boxlinknet/kwtsms-ruby/releases/tag/v0.1.0
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 boxlink
|
|
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,386 @@
|
|
|
1
|
+
# kwtSMS Ruby Client
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/kwtsms)
|
|
4
|
+
[](https://github.com/boxlinknet/kwtsms-ruby/actions/workflows/ci.yml)
|
|
5
|
+
[](https://github.com/boxlinknet/kwtsms-ruby/actions/workflows/codeql.yml)
|
|
6
|
+
[](https://www.ruby-lang.org/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
Ruby client for the [kwtSMS API](https://www.kwtsms.com). Send SMS, check balance, validate numbers, list sender IDs, check coverage, get delivery reports.
|
|
10
|
+
|
|
11
|
+
## About kwtSMS
|
|
12
|
+
|
|
13
|
+
kwtSMS is a Kuwaiti SMS gateway trusted by top businesses to deliver messages anywhere in the world, with private Sender ID, free API testing, non-expiring credits, and competitive flat-rate pricing. Secure, simple to integrate, built to last. Open a free account in under 1 minute, no paperwork or payment required. [Click here to get started](https://www.kwtsms.com/signup/)
|
|
14
|
+
|
|
15
|
+
## Prerequisites
|
|
16
|
+
|
|
17
|
+
You need **Ruby** (>= 2.7) installed.
|
|
18
|
+
|
|
19
|
+
### Check if Ruby is installed
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
ruby -v
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
If not installed, see [ruby-lang.org/en/downloads](https://www.ruby-lang.org/en/downloads/).
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
gem install kwtsms
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or add to your `Gemfile`:
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
gem "kwtsms"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
require "kwtsms"
|
|
43
|
+
|
|
44
|
+
sms = KwtSMS::Client.from_env
|
|
45
|
+
|
|
46
|
+
# Verify credentials
|
|
47
|
+
ok, balance, err = sms.verify
|
|
48
|
+
puts "Balance: #{balance}" if ok
|
|
49
|
+
|
|
50
|
+
# Send SMS
|
|
51
|
+
result = sms.send_sms("96598765432", "Your OTP for MyApp is: 123456")
|
|
52
|
+
puts "msg-id: #{result['msg-id']}" if result["result"] == "OK"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Configuration
|
|
56
|
+
|
|
57
|
+
### Environment Variables
|
|
58
|
+
|
|
59
|
+
Create a `.env` file or set environment variables:
|
|
60
|
+
|
|
61
|
+
```ini
|
|
62
|
+
KWTSMS_USERNAME=ruby_username
|
|
63
|
+
KWTSMS_PASSWORD=ruby_password
|
|
64
|
+
KWTSMS_SENDER_ID=YOUR-SENDER
|
|
65
|
+
KWTSMS_TEST_MODE=1
|
|
66
|
+
KWTSMS_LOG_FILE=kwtsms.log
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Direct Construction
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
sms = KwtSMS::Client.new(
|
|
73
|
+
"ruby_username",
|
|
74
|
+
"ruby_password",
|
|
75
|
+
sender_id: "YOUR-SENDER",
|
|
76
|
+
test_mode: true,
|
|
77
|
+
log_file: "kwtsms.log"
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## API Reference
|
|
82
|
+
|
|
83
|
+
### verify
|
|
84
|
+
|
|
85
|
+
Test credentials and check balance.
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
ok, balance, err = sms.verify
|
|
89
|
+
# ok: true/false
|
|
90
|
+
# balance: Float or nil
|
|
91
|
+
# err: nil or error message string
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### balance
|
|
95
|
+
|
|
96
|
+
Get current balance.
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
balance = sms.balance # Float or nil
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### send_sms
|
|
103
|
+
|
|
104
|
+
Send SMS to one or more numbers.
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
# Single number
|
|
108
|
+
result = sms.send_sms("96598765432", "Hello!")
|
|
109
|
+
|
|
110
|
+
# Multiple numbers
|
|
111
|
+
result = sms.send_sms(["96598765432", "96512345678"], "Bulk message")
|
|
112
|
+
|
|
113
|
+
# Override sender ID
|
|
114
|
+
result = sms.send_sms("96598765432", "Hello!", sender: "MY-APP")
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Response on success:
|
|
118
|
+
```ruby
|
|
119
|
+
{
|
|
120
|
+
"result" => "OK",
|
|
121
|
+
"msg-id" => "12345",
|
|
122
|
+
"numbers" => 1,
|
|
123
|
+
"points-charged" => 1,
|
|
124
|
+
"balance-after" => 149.0
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Always save `msg-id` immediately after a successful send.** You need it for status checks and delivery reports.
|
|
129
|
+
|
|
130
|
+
**Never call `balance` after `send_sms`.** The send response already includes your updated balance in `balance-after`.
|
|
131
|
+
|
|
132
|
+
### send_with_retry
|
|
133
|
+
|
|
134
|
+
Send with automatic retry on ERR028 (15-second rate limit).
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
result = sms.send_with_retry("96598765432", "Hello!", max_retries: 3)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### status
|
|
141
|
+
|
|
142
|
+
Get delivery report for a message.
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
result = sms.status("12345") # pass the msg-id from send_sms
|
|
146
|
+
# result["status"] => "DELIVERED", "FAILED", "PENDING", "REJECTED"
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### senderids
|
|
150
|
+
|
|
151
|
+
List sender IDs registered on your account.
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
result = sms.senderids
|
|
155
|
+
puts result["senderids"] # => ["KWT-SMS", "MY-APP"]
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### coverage
|
|
159
|
+
|
|
160
|
+
List active country prefixes.
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
result = sms.coverage
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### validate
|
|
167
|
+
|
|
168
|
+
Validate phone numbers.
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
result = sms.validate(["96598765432", "invalid", "+96512345678"])
|
|
172
|
+
puts result["ok"] # valid numbers
|
|
173
|
+
puts result["er"] # error numbers
|
|
174
|
+
puts result["rejected"] # locally rejected with error messages
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Utility Functions
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
# Normalize phone number: Arabic digits, strip non-digits, strip leading zeros
|
|
181
|
+
KwtSMS.normalize_phone("+965 9876 5432") # => "96598765432"
|
|
182
|
+
|
|
183
|
+
# Validate phone input (returns [valid, error, normalized])
|
|
184
|
+
valid, error, normalized = KwtSMS.validate_phone_input("user@email.com")
|
|
185
|
+
# => [false, "'user@email.com' is an email address, not a phone number", ""]
|
|
186
|
+
|
|
187
|
+
# Clean message: strip emojis, HTML, hidden chars, convert Arabic digits
|
|
188
|
+
KwtSMS.clean_message("Hello \u{1F600} <b>world</b>") # => "Hello world"
|
|
189
|
+
|
|
190
|
+
# Enrich error with developer-friendly action message
|
|
191
|
+
KwtSMS.enrich_error({"result" => "ERROR", "code" => "ERR003"})
|
|
192
|
+
# => adds "action" key with guidance
|
|
193
|
+
|
|
194
|
+
# Access all error codes
|
|
195
|
+
KwtSMS::API_ERRORS # => Hash of all 29 error codes with action messages
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Bulk Send (>200 Numbers)
|
|
199
|
+
|
|
200
|
+
When passing more than 200 numbers to `send_sms`, the library automatically:
|
|
201
|
+
|
|
202
|
+
1. Splits into batches of 200
|
|
203
|
+
2. Sends each batch with a 0.5s delay
|
|
204
|
+
3. Retries ERR013 (queue full) up to 3 times with 30s/60s/120s backoff
|
|
205
|
+
4. Returns aggregated result: `OK`, `PARTIAL`, or `ERROR`
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
result = sms.send_sms(large_number_list, "Announcement")
|
|
209
|
+
puts result["batches"] # number of batches
|
|
210
|
+
puts result["msg-ids"] # array of message IDs
|
|
211
|
+
puts result["points-charged"] # total points
|
|
212
|
+
puts result["balance-after"] # final balance
|
|
213
|
+
puts result["errors"] # any batch errors
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Phone Number Handling
|
|
217
|
+
|
|
218
|
+
Phone numbers are normalized automatically before sending:
|
|
219
|
+
|
|
220
|
+
- Arabic-Indic and Extended Arabic-Indic digits converted to Latin
|
|
221
|
+
- Non-digit characters stripped (`+`, spaces, dashes, dots, brackets)
|
|
222
|
+
- Leading zeros stripped (handles `00` country code prefix)
|
|
223
|
+
- Duplicate numbers deduplicated before sending
|
|
224
|
+
- Invalid numbers rejected locally with clear error messages
|
|
225
|
+
|
|
226
|
+
## Message Cleaning
|
|
227
|
+
|
|
228
|
+
Messages are cleaned automatically before sending to prevent silent delivery failures:
|
|
229
|
+
|
|
230
|
+
- Emojis stripped (cause messages to get stuck in queue)
|
|
231
|
+
- HTML tags stripped (causes ERR027)
|
|
232
|
+
- Hidden characters stripped (BOM, zero-width spaces, soft hyphens, directional marks)
|
|
233
|
+
- Arabic-Indic digits converted to Latin
|
|
234
|
+
- C0/C1 control characters removed (except `\n` and `\t`)
|
|
235
|
+
|
|
236
|
+
## CLI
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
kwtsms setup # Interactive credential wizard
|
|
240
|
+
kwtsms verify # Test credentials, show balance
|
|
241
|
+
kwtsms balance # Show available + purchased credits
|
|
242
|
+
kwtsms senderid # List sender IDs
|
|
243
|
+
kwtsms coverage # List active country prefixes
|
|
244
|
+
kwtsms send 96598765432 "Hello!" # Send SMS
|
|
245
|
+
kwtsms send 965xxx,965yyy "Bulk message" # Multiple numbers
|
|
246
|
+
kwtsms send 96598765432 "Hi" --sender X # Override sender ID
|
|
247
|
+
kwtsms validate 96598765432 96512345678 # Validate numbers
|
|
248
|
+
kwtsms version # Show version
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Credential Management
|
|
252
|
+
|
|
253
|
+
**Never hardcode credentials in source code.** Credentials must be changeable without recompiling or redeploying.
|
|
254
|
+
|
|
255
|
+
### Environment variables (recommended for servers)
|
|
256
|
+
|
|
257
|
+
```ruby
|
|
258
|
+
sms = KwtSMS::Client.from_env # reads KWTSMS_USERNAME, KWTSMS_PASSWORD, etc.
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Rails initializer
|
|
262
|
+
|
|
263
|
+
```ruby
|
|
264
|
+
# config/initializers/kwtsms.rb
|
|
265
|
+
KWTSMS_CLIENT = KwtSMS::Client.from_env
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Constructor injection (for custom config systems)
|
|
269
|
+
|
|
270
|
+
```ruby
|
|
271
|
+
sms = KwtSMS::Client.new(
|
|
272
|
+
config[:username],
|
|
273
|
+
config[:password],
|
|
274
|
+
sender_id: config[:sender_id]
|
|
275
|
+
)
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Best Practices
|
|
279
|
+
|
|
280
|
+
### Validate before calling the API
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
valid, error, normalized = KwtSMS.validate_phone_input(user_input)
|
|
284
|
+
unless valid
|
|
285
|
+
# Don't waste an API call on invalid input
|
|
286
|
+
return { error: error }
|
|
287
|
+
end
|
|
288
|
+
result = sms.send_sms(normalized, message)
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### User-facing error messages
|
|
292
|
+
|
|
293
|
+
Never expose raw API errors to end users:
|
|
294
|
+
|
|
295
|
+
| Situation | API Code | Show to User |
|
|
296
|
+
|-----------|----------|--------------|
|
|
297
|
+
| Invalid phone | ERR006, ERR025 | "Please enter a valid phone number in international format." |
|
|
298
|
+
| Auth error | ERR003 | "SMS service temporarily unavailable. Please try again later." |
|
|
299
|
+
| No balance | ERR010, ERR011 | "SMS service temporarily unavailable. Please try again later." |
|
|
300
|
+
| Rate limited | ERR028 | "Please wait a moment before requesting another code." |
|
|
301
|
+
| Content rejected | ERR031, ERR032 | "Your message could not be sent. Please try again with different content." |
|
|
302
|
+
|
|
303
|
+
### Sender ID
|
|
304
|
+
|
|
305
|
+
- `KWT-SMS` is a shared test sender: delays, blocked on some carriers. Never use in production.
|
|
306
|
+
- Register a private Sender ID at kwtsms.com (takes ~5 working days for Kuwait).
|
|
307
|
+
- **Sender ID is case sensitive:** `Kuwait` is not the same as `KUWAIT`.
|
|
308
|
+
- **For OTP, use Transactional Sender ID.** Promotional IDs are filtered by DND on Zain and Ooredoo.
|
|
309
|
+
|
|
310
|
+
### Timezone
|
|
311
|
+
|
|
312
|
+
`unix-timestamp` in API responses is GMT+3 (Asia/Kuwait server time), not UTC. Always convert when storing or displaying. Log timestamps written by this client are UTC.
|
|
313
|
+
|
|
314
|
+
### Security Checklist
|
|
315
|
+
|
|
316
|
+
Before going live:
|
|
317
|
+
|
|
318
|
+
- [ ] Bot protection enabled (CAPTCHA for web)
|
|
319
|
+
- [ ] Rate limit per phone number (max 3-5/hour)
|
|
320
|
+
- [ ] Rate limit per IP address (max 10-20/hour)
|
|
321
|
+
- [ ] Rate limit per user/session if authenticated
|
|
322
|
+
- [ ] Monitoring/alerting on abuse patterns
|
|
323
|
+
- [ ] Admin notification on low balance
|
|
324
|
+
- [ ] Test mode OFF (`KWTSMS_TEST_MODE=0`)
|
|
325
|
+
- [ ] Private Sender ID registered (not KWT-SMS)
|
|
326
|
+
- [ ] Transactional Sender ID for OTP (not promotional)
|
|
327
|
+
|
|
328
|
+
## JSONL Logging
|
|
329
|
+
|
|
330
|
+
Every API call is logged to a JSONL file (default: `kwtsms.log`):
|
|
331
|
+
|
|
332
|
+
```json
|
|
333
|
+
{"ts":"2026-03-06T12:00:00Z","endpoint":"send","request":{"username":"ruby_username","password":"***","mobile":"96598765432","message":"Hello"},"response":{"result":"OK","msg-id":"12345"},"ok":true,"error":null}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Passwords are always masked as `***`. Logging never crashes the main flow.
|
|
337
|
+
|
|
338
|
+
Disable logging by setting `log_file: ""` in the constructor.
|
|
339
|
+
|
|
340
|
+
## Examples
|
|
341
|
+
|
|
342
|
+
See the [examples/](examples/) directory:
|
|
343
|
+
|
|
344
|
+
| # | Example | Description |
|
|
345
|
+
|---|---------|-------------|
|
|
346
|
+
| 01 | [Basic Usage](examples/01_basic_usage.rb) | Connect, verify, send SMS, validate |
|
|
347
|
+
| 02 | [OTP Flow](examples/02_otp_flow.rb) | Send OTP codes |
|
|
348
|
+
| 03 | [Bulk SMS](examples/03_bulk_sms.rb) | Send to many recipients |
|
|
349
|
+
| 04 | [Rails Endpoint](examples/04_rails_endpoint.rb) | Rails controller |
|
|
350
|
+
| 05 | [Error Handling](examples/05_error_handling.rb) | Handle every error type |
|
|
351
|
+
| 06 | [OTP Production](examples/06-otp-production/) | Production OTP with rate limiting, CAPTCHA, Redis |
|
|
352
|
+
|
|
353
|
+
## Requirements
|
|
354
|
+
|
|
355
|
+
- Ruby >= 2.7
|
|
356
|
+
- No external runtime dependencies
|
|
357
|
+
|
|
358
|
+
## Publishing to RubyGems
|
|
359
|
+
|
|
360
|
+
```bash
|
|
361
|
+
# 1. Create account at https://rubygems.org/sign_up
|
|
362
|
+
# 2. Build the gem
|
|
363
|
+
gem build kwtsms.gemspec
|
|
364
|
+
|
|
365
|
+
# 3. Push to RubyGems
|
|
366
|
+
gem push kwtsms-0.1.0.gem
|
|
367
|
+
|
|
368
|
+
# 4. Or use the automated GitHub Actions workflow:
|
|
369
|
+
# Push a tag and it publishes automatically
|
|
370
|
+
git tag v0.1.0
|
|
371
|
+
git push origin v0.1.0
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
## License
|
|
375
|
+
|
|
376
|
+
MIT License. See [LICENSE](LICENSE).
|
|
377
|
+
|
|
378
|
+
## Links
|
|
379
|
+
|
|
380
|
+
- [kwtSMS Website](https://www.kwtsms.com)
|
|
381
|
+
- [kwtSMS API Best Practices](https://www.kwtsms.com/articles/sms-api-implementation-best-practices.html)
|
|
382
|
+
- [RubyGems](https://rubygems.org/gems/kwtsms)
|
|
383
|
+
- [GitHub](https://github.com/boxlinknet/kwtsms-ruby)
|
|
384
|
+
- [Changelog](CHANGELOG.md)
|
|
385
|
+
- [Contributing](CONTRIBUTING.md)
|
|
386
|
+
- [Security](SECURITY.md)
|
data/exe/kwtsms
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "kwtsms"
|
|
5
|
+
require "optparse"
|
|
6
|
+
|
|
7
|
+
module KwtSMS
|
|
8
|
+
module CLI
|
|
9
|
+
def self.run(args = ARGV)
|
|
10
|
+
command = args.shift
|
|
11
|
+
|
|
12
|
+
case command
|
|
13
|
+
when "setup"
|
|
14
|
+
setup
|
|
15
|
+
when "verify"
|
|
16
|
+
verify
|
|
17
|
+
when "balance"
|
|
18
|
+
show_balance
|
|
19
|
+
when "senderid"
|
|
20
|
+
list_senderids
|
|
21
|
+
when "coverage"
|
|
22
|
+
list_coverage
|
|
23
|
+
when "send"
|
|
24
|
+
send_sms(args)
|
|
25
|
+
when "validate"
|
|
26
|
+
validate_numbers(args)
|
|
27
|
+
when "version", "--version", "-v"
|
|
28
|
+
puts "kwtsms #{KwtSMS::VERSION}"
|
|
29
|
+
when "help", "--help", "-h", nil
|
|
30
|
+
print_help
|
|
31
|
+
else
|
|
32
|
+
$stderr.puts "Unknown command: #{command}"
|
|
33
|
+
$stderr.puts "Run 'kwtsms help' for usage."
|
|
34
|
+
exit 1
|
|
35
|
+
end
|
|
36
|
+
rescue => e
|
|
37
|
+
$stderr.puts "Error: #{e.message}"
|
|
38
|
+
exit 1
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.print_help
|
|
42
|
+
puts <<~HELP
|
|
43
|
+
kwtsms #{KwtSMS::VERSION} - kwtSMS API client
|
|
44
|
+
|
|
45
|
+
Usage:
|
|
46
|
+
kwtsms setup Interactive credential wizard
|
|
47
|
+
kwtsms verify Test credentials, show balance
|
|
48
|
+
kwtsms balance Show available + purchased credits
|
|
49
|
+
kwtsms senderid List sender IDs
|
|
50
|
+
kwtsms coverage List active country prefixes
|
|
51
|
+
kwtsms send <mobile> <message> [options] Send SMS
|
|
52
|
+
kwtsms validate <number> [number2 ...] Validate numbers
|
|
53
|
+
kwtsms version Show version
|
|
54
|
+
|
|
55
|
+
Send options:
|
|
56
|
+
--sender SENDER_ID Override sender ID
|
|
57
|
+
|
|
58
|
+
Environment variables:
|
|
59
|
+
KWTSMS_USERNAME API username (required)
|
|
60
|
+
KWTSMS_PASSWORD API password (required)
|
|
61
|
+
KWTSMS_SENDER_ID Sender ID (default: KWT-SMS)
|
|
62
|
+
KWTSMS_TEST_MODE Set to 1 for test mode
|
|
63
|
+
KWTSMS_LOG_FILE Log file path (default: kwtsms.log)
|
|
64
|
+
|
|
65
|
+
Or create a .env file with these variables.
|
|
66
|
+
HELP
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.client
|
|
70
|
+
KwtSMS::Client.from_env
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def self.setup
|
|
74
|
+
puts "kwtsms setup wizard"
|
|
75
|
+
puts "=" * 40
|
|
76
|
+
print "API Username: "
|
|
77
|
+
username = $stdin.gets&.strip
|
|
78
|
+
print "API Password: "
|
|
79
|
+
password = $stdin.gets&.strip
|
|
80
|
+
print "Sender ID (default: KWT-SMS): "
|
|
81
|
+
sender = $stdin.gets&.strip
|
|
82
|
+
sender = "KWT-SMS" if sender.nil? || sender.empty?
|
|
83
|
+
print "Test mode? (y/N): "
|
|
84
|
+
test = $stdin.gets&.strip&.downcase == "y"
|
|
85
|
+
|
|
86
|
+
env_content = <<~ENV
|
|
87
|
+
KWTSMS_USERNAME=#{username}
|
|
88
|
+
KWTSMS_PASSWORD=#{password}
|
|
89
|
+
KWTSMS_SENDER_ID=#{sender}
|
|
90
|
+
KWTSMS_TEST_MODE=#{test ? '1' : '0'}
|
|
91
|
+
KWTSMS_LOG_FILE=kwtsms.log
|
|
92
|
+
ENV
|
|
93
|
+
|
|
94
|
+
File.write(".env", env_content)
|
|
95
|
+
puts "\n.env file created. Testing credentials..."
|
|
96
|
+
|
|
97
|
+
sms = KwtSMS::Client.new(username, password, sender_id: sender, test_mode: test)
|
|
98
|
+
ok, balance, err = sms.verify
|
|
99
|
+
if ok
|
|
100
|
+
puts "Credentials verified. Balance: #{balance} credits."
|
|
101
|
+
else
|
|
102
|
+
puts "Credential verification failed: #{err}"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def self.verify
|
|
107
|
+
sms = client
|
|
108
|
+
ok, balance, err = sms.verify
|
|
109
|
+
if ok
|
|
110
|
+
puts "Credentials: OK"
|
|
111
|
+
puts "Balance: #{balance} credits"
|
|
112
|
+
puts "Purchased: #{sms.cached_purchased} credits" if sms.cached_purchased
|
|
113
|
+
else
|
|
114
|
+
$stderr.puts "Verification failed: #{err}"
|
|
115
|
+
exit 1
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def self.show_balance
|
|
120
|
+
sms = client
|
|
121
|
+
ok, balance, err = sms.verify
|
|
122
|
+
if ok
|
|
123
|
+
puts "Available: #{balance} credits"
|
|
124
|
+
puts "Purchased: #{sms.cached_purchased} credits" if sms.cached_purchased
|
|
125
|
+
else
|
|
126
|
+
$stderr.puts "Failed to get balance: #{err}"
|
|
127
|
+
exit 1
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def self.list_senderids
|
|
132
|
+
sms = client
|
|
133
|
+
result = sms.senderids
|
|
134
|
+
if result["result"] == "OK"
|
|
135
|
+
ids = result["senderids"]
|
|
136
|
+
if ids.empty?
|
|
137
|
+
puts "No sender IDs registered."
|
|
138
|
+
else
|
|
139
|
+
puts "Sender IDs:"
|
|
140
|
+
ids.each { |id| puts " - #{id}" }
|
|
141
|
+
end
|
|
142
|
+
else
|
|
143
|
+
$stderr.puts "Error: #{result['description']}"
|
|
144
|
+
$stderr.puts result["action"] if result["action"]
|
|
145
|
+
exit 1
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def self.list_coverage
|
|
150
|
+
sms = client
|
|
151
|
+
result = sms.coverage
|
|
152
|
+
if result["result"] == "OK"
|
|
153
|
+
puts "Active coverage:"
|
|
154
|
+
puts JSON.pretty_generate(result)
|
|
155
|
+
else
|
|
156
|
+
$stderr.puts "Error: #{result['description']}"
|
|
157
|
+
$stderr.puts result["action"] if result["action"]
|
|
158
|
+
exit 1
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def self.send_sms(args)
|
|
163
|
+
sender = nil
|
|
164
|
+
opts = OptionParser.new do |o|
|
|
165
|
+
o.on("--sender SENDER_ID", "Override sender ID") { |s| sender = s }
|
|
166
|
+
end
|
|
167
|
+
opts.parse!(args)
|
|
168
|
+
|
|
169
|
+
if args.length < 2
|
|
170
|
+
$stderr.puts "Usage: kwtsms send <mobile> <message> [--sender SENDER_ID]"
|
|
171
|
+
$stderr.puts "Multiple numbers: kwtsms send 965xxx,965yyy \"message\""
|
|
172
|
+
exit 1
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
mobile_input = args[0]
|
|
176
|
+
message = args[1]
|
|
177
|
+
|
|
178
|
+
mobiles = mobile_input.include?(",") ? mobile_input.split(",").map(&:strip) : mobile_input
|
|
179
|
+
|
|
180
|
+
sms = client
|
|
181
|
+
|
|
182
|
+
if sms.test_mode
|
|
183
|
+
$stderr.puts "WARNING: Test mode is ON. Messages will be queued but NOT delivered."
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
result = sms.send_sms(mobiles, message, sender: sender)
|
|
187
|
+
|
|
188
|
+
if result["result"] == "OK"
|
|
189
|
+
puts "Message sent successfully."
|
|
190
|
+
puts " msg-id: #{result['msg-id']}" if result["msg-id"]
|
|
191
|
+
puts " Numbers: #{result['numbers']}" if result["numbers"]
|
|
192
|
+
puts " Points charged: #{result['points-charged']}" if result["points-charged"]
|
|
193
|
+
puts " Balance after: #{result['balance-after']}" if result["balance-after"]
|
|
194
|
+
else
|
|
195
|
+
$stderr.puts "Send failed: #{result['description']}"
|
|
196
|
+
$stderr.puts "Action: #{result['action']}" if result["action"]
|
|
197
|
+
exit 1
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def self.validate_numbers(args)
|
|
202
|
+
if args.empty?
|
|
203
|
+
$stderr.puts "Usage: kwtsms validate <number> [number2 ...]"
|
|
204
|
+
exit 1
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
sms = client
|
|
208
|
+
result = sms.validate(args)
|
|
209
|
+
|
|
210
|
+
puts "Valid (OK): #{result['ok'].inspect}" unless result["ok"].empty?
|
|
211
|
+
puts "Errors (ER): #{result['er'].inspect}" unless result["er"].empty?
|
|
212
|
+
puts "No route (NR): #{result['nr'].inspect}" unless result["nr"].empty?
|
|
213
|
+
if result["rejected"] && !result["rejected"].empty?
|
|
214
|
+
puts "Locally rejected:"
|
|
215
|
+
result["rejected"].each { |r| puts " #{r['input']}: #{r['error']}" }
|
|
216
|
+
end
|
|
217
|
+
puts "Error: #{result['error']}" if result["error"]
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
KwtSMS::CLI.run
|