kwtsms 0.3.0 → 0.4.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 +4 -4
- data/CHANGELOG.md +38 -1
- data/LICENSE +1 -1
- data/README.md +43 -31
- data/lib/kwtsms/client.rb +37 -16
- data/lib/kwtsms/env_loader.rb +1 -0
- data/lib/kwtsms/errors.rb +10 -3
- data/lib/kwtsms/logger.rb +17 -0
- data/lib/kwtsms/phone.rb +8 -4
- data/lib/kwtsms/request.rb +14 -1
- data/lib/kwtsms/version.rb +1 -1
- data/lib/kwtsms.rb +5 -3
- metadata +10 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8d0309851d3d99cd00dc43e57fcc1a6a3c325573a3ef5f03cc605b6155e2f8c5
|
|
4
|
+
data.tar.gz: ded82b65342f4b32e9aa42ced78bcdeeeb496efb2865b543ec4dfef1d8e8bc46
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: abf1aab66a4eef8ce7254f8a7963992b65a1dd70b527b893181f7b72a29f836aca7a32f76a01fe5507bc557fc8a82c4c9244b10ad9d4c0cee8a69b30327e7075
|
|
7
|
+
data.tar.gz: 5551f9ce6129a20e7f2c33c23395acf89910db177362f097f048618621624365c4b9a3add2efd8a82359f5af0f40597230dc48d3d1ca6ef96d8a17241ef8ef1b
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,42 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.4.0] - 2026-11-07
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- The client sends a User-Agent `kwtsms-ruby/<version>` (name and version only, no personal data) so kwtSMS can see which integrations are in use. An optional `app_name:` keyword on `KwtSMS::Client.new` puts your app's token in front, e.g. `kwtsms-wordpress/3.5.2 kwtsms-ruby/0.3.1`. New `KwtSMS.build_user_agent` and `client.user_agent`.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- Package author and license holder are now kwtSMS; contact email support@kwtsms.com.
|
|
17
|
+
|
|
18
|
+
- `send_with_retry`: ERR028 retry is now off by default (`max_retries: 0`), and the wait is a new `wait_seconds:` keyword (default 10) instead of a hardcoded 16 seconds. Duplicate SMS protection (ERR028) is opt-in per account and its window is configurable per account (10 seconds by default); when on, the whole request is rejected and nothing is charged.
|
|
19
|
+
- ERR028 action text no longer states a fixed 15 seconds.
|
|
20
|
+
- ERR006 and ERR025 action text, docs and examples use the placeholder `965XXXXXXXX` instead of a sample mobile number; tests read the phone number from `KWTSMS_TEST_NUMBER` and skip the tests that need it when it is unset.
|
|
21
|
+
- The live 250-number bulk test is replaced by an offline mocked test of the 200 + 50 batching; live tests send only to `KWTSMS_TEST_NUMBER`.
|
|
22
|
+
|
|
23
|
+
### Docs
|
|
24
|
+
|
|
25
|
+
- Test mode: test messages are queued but not delivered. Their credits are held until you delete them from the Sending Queue in your kwtsms.com account.
|
|
26
|
+
|
|
27
|
+
## [0.3.1] - 2026-03-15
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- Password no longer visible in `Client#inspect` or `#to_s` output (redacted)
|
|
32
|
+
- Sanitize raw input in phone validation errors to prevent log injection
|
|
33
|
+
- Command injection risk in dependabot auto-merge workflow (use PR number not URL)
|
|
34
|
+
- Explicit SSL `VERIFY_PEER` on all HTTP connections
|
|
35
|
+
- Log file path traversal protection: rejects `..`, absolute paths, null bytes, pipes
|
|
36
|
+
- `.env` parser now handles `export KEY=VALUE` syntax
|
|
37
|
+
- CI workflow scoped to `permissions: contents: read`
|
|
38
|
+
- Missing error codes ERR019-ERR023 added to `API_ERRORS`
|
|
39
|
+
- Em dashes replaced with commas/colons per style rules
|
|
40
|
+
- SECURITY.md updated to reflect 0.3.x support
|
|
41
|
+
- CONTRIBUTING.md expanded with all required sections
|
|
42
|
+
- Error code test coverage includes ERR019-ERR023
|
|
43
|
+
|
|
8
44
|
## [0.3.0] - 2026-03-15
|
|
9
45
|
|
|
10
46
|
### Added
|
|
@@ -31,7 +67,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
31
67
|
|
|
32
68
|
### Added
|
|
33
69
|
|
|
34
|
-
- Raw API example (`examples/00_raw_api.rb`)
|
|
70
|
+
- Raw API example (`examples/00_raw_api.rb`): call all 7 kwtSMS endpoints using only Ruby stdlib, no gem needed
|
|
35
71
|
- Step-by-step guide (`examples/00_raw_api.md`) with helper function reference, endpoints table, key rules, and going-live checklist
|
|
36
72
|
|
|
37
73
|
### Removed
|
|
@@ -69,6 +105,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
69
105
|
- Comprehensive test suite: unit tests, mocked API tests, and real integration tests
|
|
70
106
|
- Examples: basic usage, OTP flow, bulk SMS, Rails endpoint, error handling, production OTP
|
|
71
107
|
|
|
108
|
+
[0.3.1]: https://github.com/boxlinknet/kwtsms-ruby/releases/tag/v0.3.1
|
|
72
109
|
[0.3.0]: https://github.com/boxlinknet/kwtsms-ruby/releases/tag/v0.3.0
|
|
73
110
|
[0.2.0]: https://github.com/boxlinknet/kwtsms-ruby/releases/tag/v0.2.0
|
|
74
111
|
[0.1.0]: https://github.com/boxlinknet/kwtsms-ruby/releases/tag/v0.1.0
|
data/LICENSE
CHANGED
data/README.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://rubygems.org/gems/kwtsms)
|
|
4
4
|
[](https://rubygems.org/gems/kwtsms)
|
|
5
|
-
[](https://github.com/kwtsms/kwtsms-ruby/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/kwtsms/kwtsms-ruby/actions/workflows/codeql.yml)
|
|
7
|
+
[](https://github.com/kwtsms/kwtsms-ruby/actions/workflows/audit.yml)
|
|
8
|
+
[](https://github.com/kwtsms/kwtsms-ruby/actions/workflows/gitguardian.yml)
|
|
9
|
+
[](https://scorecard.dev/viewer/?uri=github.com/kwtsms/kwtsms-ruby)
|
|
10
10
|
[](https://www.ruby-lang.org/)
|
|
11
11
|
[](LICENSE)
|
|
12
12
|
|
|
@@ -52,7 +52,7 @@ ok, balance, err = sms.verify
|
|
|
52
52
|
puts "Balance: #{balance}" if ok
|
|
53
53
|
|
|
54
54
|
# Send SMS
|
|
55
|
-
result = sms.send_sms("
|
|
55
|
+
result = sms.send_sms("965XXXXXXXX", "Your OTP for MyApp is: 123456")
|
|
56
56
|
puts "msg-id: #{result['msg-id']}" if result["result"] == "OK"
|
|
57
57
|
```
|
|
58
58
|
|
|
@@ -82,6 +82,16 @@ sms = KwtSMS::Client.new(
|
|
|
82
82
|
)
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
+
### User-Agent
|
|
86
|
+
|
|
87
|
+
Every API request carries the User-Agent `kwtsms-ruby/<version>`: the library name and version only, no
|
|
88
|
+
personal or customer data. Pass your own product token as `app_name:` to put it in front:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
sms = KwtSMS::Client.new("ruby_username", "ruby_password", app_name: "my-shop/2.1.0")
|
|
92
|
+
# User-Agent: my-shop/2.1.0 kwtsms-ruby/0.3.1
|
|
93
|
+
```
|
|
94
|
+
|
|
85
95
|
## API Reference
|
|
86
96
|
|
|
87
97
|
### verify
|
|
@@ -109,13 +119,13 @@ Send SMS to one or more numbers.
|
|
|
109
119
|
|
|
110
120
|
```ruby
|
|
111
121
|
# Single number
|
|
112
|
-
result = sms.send_sms("
|
|
122
|
+
result = sms.send_sms("965XXXXXXXX", "Hello!")
|
|
113
123
|
|
|
114
124
|
# Multiple numbers
|
|
115
|
-
result = sms.send_sms(["
|
|
125
|
+
result = sms.send_sms(["965XXXXXXXX", "96512345678"], "Bulk message")
|
|
116
126
|
|
|
117
127
|
# Override sender ID
|
|
118
|
-
result = sms.send_sms("
|
|
128
|
+
result = sms.send_sms("965XXXXXXXX", "Hello!", sender: "MY-APP")
|
|
119
129
|
```
|
|
120
130
|
|
|
121
131
|
Response on success:
|
|
@@ -133,10 +143,10 @@ Response on success:
|
|
|
133
143
|
|
|
134
144
|
### send_with_retry
|
|
135
145
|
|
|
136
|
-
Send with
|
|
146
|
+
Send with optional retry on ERR028. ERR028 comes from Duplicate SMS protection, which is opt-in per account (in the account's API settings). When it is on and any number in the request was sent the same message within the account's window (10 seconds by default, configurable per account), the whole request is rejected with ERR028 and nothing is charged. Retry is off by default (`max_retries: 0`); set `wait_seconds:` to your account's window.
|
|
137
147
|
|
|
138
148
|
```ruby
|
|
139
|
-
result = sms.send_with_retry("
|
|
149
|
+
result = sms.send_with_retry("965XXXXXXXX", "Hello!", max_retries: 1, wait_seconds: 10)
|
|
140
150
|
```
|
|
141
151
|
|
|
142
152
|
### senderids
|
|
@@ -161,7 +171,7 @@ result = sms.coverage
|
|
|
161
171
|
Validate phone numbers.
|
|
162
172
|
|
|
163
173
|
```ruby
|
|
164
|
-
result = sms.validate(["
|
|
174
|
+
result = sms.validate(["965XXXXXXXX", "invalid", "+96512345678"])
|
|
165
175
|
puts result["ok"] # valid numbers
|
|
166
176
|
puts result["er"] # error numbers
|
|
167
177
|
puts result["rejected"] # locally rejected with error messages
|
|
@@ -171,7 +181,7 @@ puts result["rejected"] # locally rejected with error messages
|
|
|
171
181
|
|
|
172
182
|
```ruby
|
|
173
183
|
# Normalize phone number: Arabic digits, strip non-digits, strip leading zeros
|
|
174
|
-
KwtSMS.normalize_phone("+965
|
|
184
|
+
KwtSMS.normalize_phone("+965 XXXX XXXX") # => "965XXXXXXXX"
|
|
175
185
|
|
|
176
186
|
# Validate phone input (returns [valid, error, normalized])
|
|
177
187
|
valid, error, normalized = KwtSMS.validate_phone_input("user@email.com")
|
|
@@ -212,22 +222,24 @@ All formats are accepted and normalized automatically:
|
|
|
212
222
|
|
|
213
223
|
| Input | Normalized | Valid? |
|
|
214
224
|
|-------|-----------|--------|
|
|
215
|
-
| `
|
|
216
|
-
| `+
|
|
217
|
-
| `
|
|
218
|
-
| `965
|
|
219
|
-
| `965-
|
|
220
|
-
| `(965)
|
|
221
|
-
|
|
|
222
|
-
|
|
|
223
|
-
|
|
|
224
|
-
|
|
|
225
|
-
| `٩٦٥
|
|
226
|
-
|
|
|
227
|
-
| `965
|
|
225
|
+
| `965XXXXXXXX` | `965XXXXXXXX` | Yes |
|
|
226
|
+
| `+965XXXXXXXX` | `965XXXXXXXX` | Yes |
|
|
227
|
+
| `00965XXXXXXXX` | `965XXXXXXXX` | Yes |
|
|
228
|
+
| `965 XXXX XXXX` | `965XXXXXXXX` | Yes |
|
|
229
|
+
| `965-XXXX-XXXX` | `965XXXXXXXX` | Yes |
|
|
230
|
+
| `(965) XXXXXXXX` | `965XXXXXXXX` | Yes |
|
|
231
|
+
| `٩٦٥XXXXXXXX` | `965XXXXXXXX` | Yes |
|
|
232
|
+
| `۹۶۵XXXXXXXX` | `965XXXXXXXX` | Yes |
|
|
233
|
+
| `+٩٦٥XXXXXXXX` | `965XXXXXXXX` | Yes |
|
|
234
|
+
| `٠٠٩٦٥XXXXXXXX` | `965XXXXXXXX` | Yes |
|
|
235
|
+
| `٩٦٥ XXXX XXXX` | `965XXXXXXXX` | Yes |
|
|
236
|
+
| `٩٦٥-XXXX-XXXX` | `965XXXXXXXX` | Yes |
|
|
237
|
+
| `965` + 8 Arabic-Indic digits | `965XXXXXXXX` | Yes |
|
|
228
238
|
| `123456` (too short) | rejected | No |
|
|
229
239
|
| `user@gmail.com` | rejected | No |
|
|
230
240
|
|
|
241
|
+
`X` stands for any digit, in Latin, Arabic-Indic or Persian digits.
|
|
242
|
+
|
|
231
243
|
Normalization rules:
|
|
232
244
|
- Arabic-Indic and Extended Arabic-Indic digits converted to Latin
|
|
233
245
|
- Non-digit characters stripped (`+`, spaces, dashes, dots, brackets)
|
|
@@ -247,7 +259,7 @@ Messages are cleaned automatically before sending to prevent silent delivery fai
|
|
|
247
259
|
|
|
248
260
|
## CLI
|
|
249
261
|
|
|
250
|
-
For a standalone CLI tool that works on all operating systems, see [kwtsms-cli](https://github.com/
|
|
262
|
+
For a standalone CLI tool that works on all operating systems, see [kwtsms-cli](https://github.com/kwtsms/kwtsms-cli).
|
|
251
263
|
|
|
252
264
|
## Credential Management
|
|
253
265
|
|
|
@@ -331,7 +343,7 @@ Before going live:
|
|
|
331
343
|
Every API call is logged to a JSONL file (default: `kwtsms.log`):
|
|
332
344
|
|
|
333
345
|
```json
|
|
334
|
-
{"ts":"2026-03-06T12:00:00Z","endpoint":"send","request":{"username":"ruby_username","password":"***","mobile":"
|
|
346
|
+
{"ts":"2026-03-06T12:00:00Z","endpoint":"send","request":{"username":"ruby_username","password":"***","mobile":"965XXXXXXXX","message":"Hello"},"response":{"result":"OK","msg-id":"12345"},"ok":true,"error":null}
|
|
335
347
|
```
|
|
336
348
|
|
|
337
349
|
Passwords are always masked as `***`. Logging never crashes the main flow.
|
|
@@ -344,7 +356,7 @@ See the [examples/](examples/) directory:
|
|
|
344
356
|
|
|
345
357
|
| # | Example | Description |
|
|
346
358
|
|---|---------|-------------|
|
|
347
|
-
| 00 | [Raw API](examples/00_raw_api.rb) | Call all 7 endpoints directly
|
|
359
|
+
| 00 | [Raw API](examples/00_raw_api.rb) | Call all 7 endpoints directly, no gem needed |
|
|
348
360
|
| 01 | [Basic Usage](examples/01_basic_usage.rb) | Connect, verify, send SMS, validate |
|
|
349
361
|
| 02 | [OTP Flow](examples/02_otp_flow.rb) | Send OTP codes |
|
|
350
362
|
| 03 | [Bulk SMS](examples/03_bulk_sms.rb) | Send to many recipients |
|
|
@@ -381,7 +393,7 @@ Check the **Sending Queue** at [kwtsms.com](https://www.kwtsms.com/login/). If y
|
|
|
381
393
|
|
|
382
394
|
**2. What is the difference between Test mode and Live mode?**
|
|
383
395
|
|
|
384
|
-
**Test mode** (`KWTSMS_TEST_MODE=1`) sends your message to the kwtSMS queue but does NOT deliver it to the handset.
|
|
396
|
+
**Test mode** (`KWTSMS_TEST_MODE=1`) sends your message to the kwtSMS queue but does NOT deliver it to the handset. Its credits are held until you delete it from the Sending Queue in your kwtsms.com account. Use this during development. **Live mode** (`KWTSMS_TEST_MODE=0`) delivers the message for real and deducts credits. Always develop in test mode and switch to live only when ready for production.
|
|
385
397
|
|
|
386
398
|
**3. What is a Sender ID and why should I not use "KWT-SMS" in production?**
|
|
387
399
|
|
|
@@ -407,7 +419,7 @@ International sending is **disabled by default** on kwtSMS accounts. Log in to y
|
|
|
407
419
|
- **[kwtSMS Dashboard](https://www.kwtsms.com/login/)**: Recharge credits, buy Sender IDs, view message logs, manage coverage
|
|
408
420
|
- **[Other Integrations](https://www.kwtsms.com/integrations.html)**: Plugins and integrations for other platforms and languages
|
|
409
421
|
- **[RubyGems](https://rubygems.org/gems/kwtsms)**: Package on RubyGems.org
|
|
410
|
-
- **[GitHub](https://github.com/
|
|
422
|
+
- **[GitHub](https://github.com/kwtsms/kwtsms-ruby)**: Source code and issue tracker
|
|
411
423
|
- **[Changelog](CHANGELOG.md)** | **[Contributing](CONTRIBUTING.md)** | **[Security](SECURITY.md)**
|
|
412
424
|
|
|
413
425
|
## License
|
data/lib/kwtsms/client.rb
CHANGED
|
@@ -19,21 +19,27 @@ module KwtSMS
|
|
|
19
19
|
# Quick start:
|
|
20
20
|
# sms = KwtSMS::Client.from_env
|
|
21
21
|
# ok, balance, err = sms.verify
|
|
22
|
-
# result = sms.send_sms("
|
|
23
|
-
# result = sms.send_sms("
|
|
22
|
+
# result = sms.send_sms("965XXXXXXXX", "Your OTP for MYAPP is: 123456")
|
|
23
|
+
# result = sms.send_sms("965XXXXXXXX", "Hello", sender: "OTHER-ID")
|
|
24
|
+
# 2026-11-05 08:07 +03: placeholder instead of a sample mobile number (it belongs to a real person), int-testnumber.
|
|
24
25
|
# balance = sms.balance
|
|
25
26
|
class Client
|
|
27
|
+
# 2026-11-05 14:19 +03: user_agent reader added, the User-Agent sent on every call (#939).
|
|
26
28
|
attr_reader :username, :sender_id, :test_mode, :log_file,
|
|
27
|
-
:cached_balance, :cached_purchased
|
|
29
|
+
:cached_balance, :cached_purchased, :user_agent
|
|
28
30
|
|
|
29
31
|
# @param username [String] API username (not your account phone number)
|
|
30
32
|
# @param password [String] API password
|
|
31
33
|
# @param sender_id [String] Sender ID shown on recipient's phone (default: "KWT-SMS")
|
|
32
34
|
# @param test_mode [Boolean] When true, messages are queued but not delivered (default: false)
|
|
33
35
|
# @param log_file [String] Path to JSONL log file. Set to "" to disable logging.
|
|
34
|
-
|
|
36
|
+
# @param app_name [String] Optional product token of the calling app or plugin, e.g. "kwtsms-wordpress/3.5.2",
|
|
37
|
+
# sent before the library token in the User-Agent (2026-11-05 14:19 +03, #939).
|
|
38
|
+
def initialize(username, password, sender_id: "KWT-SMS", test_mode: false, log_file: "kwtsms.log", app_name: "")
|
|
35
39
|
raise ArgumentError, "username and password are required" if username.nil? || username.empty? || password.nil? || password.empty?
|
|
36
40
|
|
|
41
|
+
# 2026-11-05 14:19 +03: identify the caller to the API (name and version only, no personal data), #939.
|
|
42
|
+
@user_agent = KwtSMS.build_user_agent(app_name)
|
|
37
43
|
@username = username
|
|
38
44
|
@password = password
|
|
39
45
|
@sender_id = sender_id
|
|
@@ -43,6 +49,13 @@ module KwtSMS
|
|
|
43
49
|
@cached_purchased = nil
|
|
44
50
|
end
|
|
45
51
|
|
|
52
|
+
# Redact password from inspect/to_s output to prevent accidental exposure
|
|
53
|
+
# in logs, error trackers (Sentry, Bugsnag), or REPL sessions.
|
|
54
|
+
def inspect
|
|
55
|
+
"#<#{self.class} @username=#{@username.inspect} @sender_id=#{@sender_id.inspect} @test_mode=#{@test_mode}>"
|
|
56
|
+
end
|
|
57
|
+
alias_method :to_s, :inspect
|
|
58
|
+
|
|
46
59
|
# Load credentials from environment variables, falling back to .env file.
|
|
47
60
|
#
|
|
48
61
|
# Required env vars:
|
|
@@ -86,7 +99,7 @@ module KwtSMS
|
|
|
86
99
|
# balance: Float or nil
|
|
87
100
|
# error: nil or error message string
|
|
88
101
|
def verify
|
|
89
|
-
data = KwtSMS.api_request("balance", creds, @log_file)
|
|
102
|
+
data = KwtSMS.api_request("balance", creds, @log_file, @user_agent) # 2026-11-05 14:19 +03: UA (#939)
|
|
90
103
|
if data["result"] == "OK"
|
|
91
104
|
@cached_balance = data["available"].to_f
|
|
92
105
|
@cached_purchased = data["purchased"].to_f
|
|
@@ -112,7 +125,7 @@ module KwtSMS
|
|
|
112
125
|
# List sender IDs registered on this account via /senderid/.
|
|
113
126
|
# Returns a consistent hash. Never raises, never crashes.
|
|
114
127
|
def senderids
|
|
115
|
-
data = KwtSMS.api_request("senderid", creds, @log_file)
|
|
128
|
+
data = KwtSMS.api_request("senderid", creds, @log_file, @user_agent) # 2026-11-05 14:19 +03: UA (#939)
|
|
116
129
|
if data["result"] == "OK"
|
|
117
130
|
{ "result" => "OK", "senderids" => data["senderid"] || [] }
|
|
118
131
|
else
|
|
@@ -126,7 +139,7 @@ module KwtSMS
|
|
|
126
139
|
# List active country prefixes via /coverage/.
|
|
127
140
|
# Returns the full API response hash with error enrichment.
|
|
128
141
|
def coverage
|
|
129
|
-
data = KwtSMS.api_request("coverage", creds, @log_file)
|
|
142
|
+
data = KwtSMS.api_request("coverage", creds, @log_file, @user_agent) # 2026-11-05 14:19 +03: UA (#939)
|
|
130
143
|
KwtSMS.enrich_error(data)
|
|
131
144
|
rescue RuntimeError => e
|
|
132
145
|
{ "result" => "ERROR", "code" => "NETWORK", "description" => e.message,
|
|
@@ -173,7 +186,7 @@ module KwtSMS
|
|
|
173
186
|
|
|
174
187
|
payload = creds.merge("mobile" => valid_normalized.join(","))
|
|
175
188
|
begin
|
|
176
|
-
data = KwtSMS.api_request("validate", payload, @log_file)
|
|
189
|
+
data = KwtSMS.api_request("validate", payload, @log_file, @user_agent) # 2026-11-05 14:19 +03: UA (#939)
|
|
177
190
|
if data["result"] == "OK"
|
|
178
191
|
mobile = data["mobile"] || {}
|
|
179
192
|
result["ok"] = mobile["OK"] || []
|
|
@@ -255,7 +268,7 @@ module KwtSMS
|
|
|
255
268
|
"test" => @test_mode ? "1" : "0"
|
|
256
269
|
)
|
|
257
270
|
begin
|
|
258
|
-
result = KwtSMS.api_request("send", payload, @log_file)
|
|
271
|
+
result = KwtSMS.api_request("send", payload, @log_file, @user_agent) # 2026-11-05 14:19 +03: UA (#939)
|
|
259
272
|
rescue RuntimeError => e
|
|
260
273
|
return {
|
|
261
274
|
"result" => "ERROR",
|
|
@@ -275,21 +288,29 @@ module KwtSMS
|
|
|
275
288
|
result
|
|
276
289
|
end
|
|
277
290
|
|
|
278
|
-
# Send SMS, retrying
|
|
291
|
+
# Send SMS, optionally retrying on ERR028 (duplicate SMS protection).
|
|
292
|
+
#
|
|
293
|
+
# ERR028 is opt-in per account (Duplicate SMS protection in the account's
|
|
294
|
+
# API settings). When it is on and any number in the request was sent the
|
|
295
|
+
# same message within the account's window (10 seconds by default,
|
|
296
|
+
# configurable per account), the whole request is rejected and nothing is
|
|
297
|
+
# charged. Retry is off by default: ERR028 is returned like any other error
|
|
298
|
+
# unless max_retries > 0. All other errors are returned immediately.
|
|
279
299
|
#
|
|
280
|
-
#
|
|
281
|
-
#
|
|
300
|
+
# 2026-10-22 15:34 +03: retry off by default and wait is a parameter (default 10),
|
|
301
|
+
# was a hardcoded 3 retries x 16 s. The ERR028 window is per account, not a fixed 15 s.
|
|
282
302
|
#
|
|
283
303
|
# @param mobile [String, Array<String>] Phone number(s)
|
|
284
304
|
# @param message [String] Message text
|
|
285
305
|
# @param sender [String, nil] Sender ID override
|
|
286
|
-
# @param max_retries [Integer] Max ERR028 retries after first attempt (default
|
|
306
|
+
# @param max_retries [Integer] Max ERR028 retries after first attempt (default 0, no retry)
|
|
307
|
+
# @param wait_seconds [Numeric] Seconds to wait before each retry (default 10)
|
|
287
308
|
# @return [Hash] Same shape as send_sms. Never raises.
|
|
288
|
-
def send_with_retry(mobile, message, sender: nil, max_retries:
|
|
309
|
+
def send_with_retry(mobile, message, sender: nil, max_retries: 0, wait_seconds: 10)
|
|
289
310
|
result = send_sms(mobile, message, sender: sender)
|
|
290
311
|
retries = 0
|
|
291
312
|
while result["code"] == "ERR028" && retries < max_retries
|
|
292
|
-
sleep(
|
|
313
|
+
sleep(wait_seconds)
|
|
293
314
|
result = send_sms(mobile, message, sender: sender)
|
|
294
315
|
retries += 1
|
|
295
316
|
end
|
|
@@ -330,7 +351,7 @@ module KwtSMS
|
|
|
330
351
|
waits.each_with_index do |wait, attempt|
|
|
331
352
|
sleep(wait) if wait > 0
|
|
332
353
|
begin
|
|
333
|
-
data = KwtSMS.api_request("send", payload, @log_file)
|
|
354
|
+
data = KwtSMS.api_request("send", payload, @log_file, @user_agent) # 2026-11-05 14:19 +03: UA (#939)
|
|
334
355
|
rescue RuntimeError => e
|
|
335
356
|
errors << { "batch" => i + 1, "code" => "NETWORK", "description" => e.message }
|
|
336
357
|
data = nil
|
data/lib/kwtsms/env_loader.rb
CHANGED
data/lib/kwtsms/errors.rb
CHANGED
|
@@ -9,7 +9,8 @@ module KwtSMS
|
|
|
9
9
|
"ERR003" => "Wrong API username or password. Check KWTSMS_USERNAME and KWTSMS_PASSWORD. These are your API credentials, not your account mobile number.",
|
|
10
10
|
"ERR004" => "This account does not have API access. Contact kwtSMS support to enable it.",
|
|
11
11
|
"ERR005" => "This account is blocked. Contact kwtSMS support.",
|
|
12
|
-
|
|
12
|
+
# 2026-11-05 08:07 +03: placeholder instead of a sample mobile number (it belongs to a real person), int-testnumber.
|
|
13
|
+
"ERR006" => "No valid phone numbers. Make sure each number includes the country code (e.g., 965XXXXXXXX for Kuwait, not XXXXXXXX).",
|
|
13
14
|
"ERR007" => "Too many numbers in a single request (maximum 200). Split into smaller batches.",
|
|
14
15
|
"ERR008" => "This sender ID is banned or not found. Sender IDs are case sensitive (\"Kuwait\" is not the same as \"KUWAIT\"). Check your registered sender IDs at kwtsms.com.",
|
|
15
16
|
"ERR009" => "Message is empty. Provide a non-empty message text.",
|
|
@@ -17,11 +18,17 @@ module KwtSMS
|
|
|
17
18
|
"ERR011" => "Insufficient balance for this send. Buy more credits at kwtsms.com.",
|
|
18
19
|
"ERR012" => "Message is too long (over 6 SMS pages). Shorten your message.",
|
|
19
20
|
"ERR013" => "Send queue is full (1000 messages). Wait a moment and try again.",
|
|
21
|
+
"ERR019" => "No delivery reports found for this message.",
|
|
22
|
+
"ERR020" => "Message ID does not exist. Make sure you saved the msg-id from the send response.",
|
|
23
|
+
"ERR021" => "No delivery report available for this message yet.",
|
|
24
|
+
"ERR022" => "Delivery reports are not ready yet. Try again after 24 hours.",
|
|
25
|
+
"ERR023" => "Unknown delivery report error. Contact kwtSMS support.",
|
|
20
26
|
"ERR024" => "Your IP address is not in the API whitelist. Add it at kwtsms.com > Account > API > IP Lockdown, or disable IP lockdown.",
|
|
21
|
-
"ERR025" => "Invalid phone number. Make sure the number includes the country code (e.g.,
|
|
27
|
+
"ERR025" => "Invalid phone number. Make sure the number includes the country code (e.g., 965XXXXXXXX for Kuwait, not XXXXXXXX).",
|
|
22
28
|
"ERR026" => "This country is not activated on your account. Contact kwtSMS support to enable the destination country.",
|
|
23
29
|
"ERR027" => "HTML tags are not allowed in the message. Remove any HTML content and try again.",
|
|
24
|
-
|
|
30
|
+
# 2026-10-22 15:34 +03: ERR028 text corrected, the window is opt-in and per account (10 s default), never a fixed 15 s.
|
|
31
|
+
"ERR028" => "Sent to the same number too soon. Duplicate SMS protection is on for this account, so the whole request was rejected and nothing was charged. Wait for the account's window (10 seconds by default) before resending.",
|
|
25
32
|
"ERR029" => "Message ID does not exist or is incorrect.",
|
|
26
33
|
"ERR030" => "Message is stuck in the send queue with an error. Delete it at kwtsms.com > Queue to recover credits.",
|
|
27
34
|
"ERR031" => "Message rejected: bad language detected.",
|
data/lib/kwtsms/logger.rb
CHANGED
|
@@ -4,10 +4,27 @@ require "json"
|
|
|
4
4
|
require "time"
|
|
5
5
|
|
|
6
6
|
module KwtSMS
|
|
7
|
+
# Validate a log file path. Returns [valid, error].
|
|
8
|
+
# Rejects path traversal, absolute paths outside CWD, and device paths.
|
|
9
|
+
def self.validate_log_path(log_file)
|
|
10
|
+
return [false, "log_file must be a String"] unless log_file.is_a?(String)
|
|
11
|
+
return [false, "log_file must not be empty"] if log_file.strip.empty?
|
|
12
|
+
return [false, "log_file must not contain '..'"] if log_file.include?("..")
|
|
13
|
+
return [false, "log_file must not contain null bytes"] if log_file.include?("\0")
|
|
14
|
+
return [false, "log_file must not start with /"] if log_file.start_with?("/")
|
|
15
|
+
return [false, "log_file must not start with ~"] if log_file.start_with?("~")
|
|
16
|
+
return [false, "log_file must not be a pipe or device"] if log_file.start_with?("|") || log_file.match?(%r{\A/dev/})
|
|
17
|
+
|
|
18
|
+
[true, nil]
|
|
19
|
+
end
|
|
20
|
+
|
|
7
21
|
# Append a JSONL log entry. Never raises. Logging must not break main flow.
|
|
8
22
|
def self.write_log(log_file, entry)
|
|
9
23
|
return if log_file.nil? || log_file.empty?
|
|
10
24
|
|
|
25
|
+
valid, = validate_log_path(log_file)
|
|
26
|
+
return unless valid
|
|
27
|
+
|
|
11
28
|
begin
|
|
12
29
|
File.open(log_file, "a", encoding: "utf-8") do |f|
|
|
13
30
|
f.puts(JSON.generate(entry))
|
data/lib/kwtsms/phone.rb
CHANGED
|
@@ -222,24 +222,28 @@ module KwtSMS
|
|
|
222
222
|
# 1. Empty / blank
|
|
223
223
|
return [false, "Phone number is required", ""] if raw.empty?
|
|
224
224
|
|
|
225
|
+
# Sanitize raw input for safe interpolation in error messages:
|
|
226
|
+
# strip control chars and truncate to prevent log injection
|
|
227
|
+
safe = raw.gsub(/[[:cntrl:]]/, "")[0, 50]
|
|
228
|
+
|
|
225
229
|
# 2. Email address entered by mistake
|
|
226
|
-
return [false, "'#{
|
|
230
|
+
return [false, "'#{safe}' is an email address, not a phone number", ""] if raw.include?("@")
|
|
227
231
|
|
|
228
232
|
# 3. Normalize
|
|
229
233
|
normalized = normalize_phone(raw)
|
|
230
234
|
|
|
231
235
|
# 4. No digits survived normalization
|
|
232
|
-
return [false, "'#{
|
|
236
|
+
return [false, "'#{safe}' is not a valid phone number, no digits found", ""] if normalized.empty?
|
|
233
237
|
|
|
234
238
|
# 5. Too short
|
|
235
239
|
if normalized.length < 7
|
|
236
240
|
digit_word = normalized.length == 1 ? "digit" : "digits"
|
|
237
|
-
return [false, "'#{
|
|
241
|
+
return [false, "'#{safe}' is too short to be a valid phone number (#{normalized.length} #{digit_word}, minimum is 7)", normalized]
|
|
238
242
|
end
|
|
239
243
|
|
|
240
244
|
# 6. Too long
|
|
241
245
|
if normalized.length > 15
|
|
242
|
-
return [false, "'#{
|
|
246
|
+
return [false, "'#{safe}' is too long to be a valid phone number (#{normalized.length} digits, maximum is 15)", normalized]
|
|
243
247
|
end
|
|
244
248
|
|
|
245
249
|
# 7. Country-specific format validation
|
data/lib/kwtsms/request.rb
CHANGED
|
@@ -12,13 +12,24 @@ module KwtSMS
|
|
|
12
12
|
# unix-timestamp values in API responses are server time, not UTC.
|
|
13
13
|
SERVER_TIMEZONE = "Asia/Kuwait (GMT+3)"
|
|
14
14
|
|
|
15
|
+
# Build the User-Agent "[app token ]kwtsms-ruby/<VERSION>". The app token is caller input: anything
|
|
16
|
+
# outside printable ASCII is removed (CR/LF would split the header), whitespace runs become "-",
|
|
17
|
+
# leading/trailing "-" are trimmed, and it is cut to 64 characters. Empty means the library token only.
|
|
18
|
+
# 2026-11-05 14:19 +03: added so kwtSMS can count accounts per integration (#939).
|
|
19
|
+
def self.build_user_agent(app_name = "")
|
|
20
|
+
app = app_name.to_s.gsub(/[^\x20-\x7e]+/, "").strip.gsub(/\s+/, "-").gsub(/\A-+|-+\z/, "")[0, 64]
|
|
21
|
+
lib = "kwtsms-ruby/#{VERSION}"
|
|
22
|
+
app.empty? ? lib : "#{app} #{lib}"
|
|
23
|
+
end
|
|
24
|
+
|
|
15
25
|
# POST to a kwtSMS REST/JSON API endpoint.
|
|
16
26
|
#
|
|
17
27
|
# Always sets Content-Type and Accept: application/json.
|
|
18
28
|
# Strips password from log entry.
|
|
19
29
|
# Returns parsed JSON hash.
|
|
20
30
|
# Raises RuntimeError on network / HTTP / parse failure.
|
|
21
|
-
|
|
31
|
+
# 2026-11-05 14:19 +03: user_agent parameter added, sent on every call (#939).
|
|
32
|
+
def self.api_request(endpoint, payload, log_file = "", user_agent = nil)
|
|
22
33
|
url = URI.parse("#{BASE_URL}#{endpoint}/")
|
|
23
34
|
|
|
24
35
|
safe_payload = payload.transform_keys(&:to_s).each_with_object({}) do |(k, v), h|
|
|
@@ -37,12 +48,14 @@ module KwtSMS
|
|
|
37
48
|
begin
|
|
38
49
|
http = Net::HTTP.new(url.host, url.port)
|
|
39
50
|
http.use_ssl = true
|
|
51
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
40
52
|
http.open_timeout = 15
|
|
41
53
|
http.read_timeout = 15
|
|
42
54
|
|
|
43
55
|
request = Net::HTTP::Post.new(url.path)
|
|
44
56
|
request["Content-Type"] = "application/json"
|
|
45
57
|
request["Accept"] = "application/json"
|
|
58
|
+
request["User-Agent"] = user_agent || build_user_agent # 2026-11-05 14:19 +03: kwtsms-ruby/<version> (#939)
|
|
46
59
|
request.body = JSON.generate(payload)
|
|
47
60
|
|
|
48
61
|
response = http.request(request)
|
data/lib/kwtsms/version.rb
CHANGED
data/lib/kwtsms.rb
CHANGED
|
@@ -7,12 +7,14 @@
|
|
|
7
7
|
#
|
|
8
8
|
# sms = KwtSMS::Client.from_env # reads .env / env vars
|
|
9
9
|
# ok, balance, err = sms.verify
|
|
10
|
-
# result = sms.send_sms("
|
|
11
|
-
# result = sms.send_sms("
|
|
10
|
+
# result = sms.send_sms("965XXXXXXXX", "Your OTP is: 123456")
|
|
11
|
+
# result = sms.send_sms("965XXXXXXXX", "Hello", sender: "MY-APP")
|
|
12
12
|
# balance = sms.balance
|
|
13
13
|
#
|
|
14
14
|
# Utility functions:
|
|
15
|
-
# KwtSMS.normalize_phone("+965
|
|
15
|
+
# KwtSMS.normalize_phone("+965 XXXX XXXX")
|
|
16
|
+
#
|
|
17
|
+
# 2026-11-05 08:07 +03: placeholder instead of a sample mobile number (it belongs to a real person), int-testnumber.
|
|
16
18
|
# KwtSMS.validate_phone_input("user@email.com")
|
|
17
19
|
# KwtSMS.clean_message("Hello \u{1F600} world")
|
|
18
20
|
|
metadata
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kwtsms
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
-
|
|
7
|
+
- kwtSMS
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-11-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Official Ruby client library for the kwtSMS SMS gateway. Send SMS, check
|
|
14
14
|
balance, validate phone numbers, check delivery status, and manage sender IDs. Zero
|
|
15
15
|
external dependencies.
|
|
16
16
|
email:
|
|
17
|
-
-
|
|
17
|
+
- support@kwtsms.com
|
|
18
18
|
executables: []
|
|
19
19
|
extensions: []
|
|
20
20
|
extra_rdoc_files: []
|
|
@@ -31,15 +31,15 @@ files:
|
|
|
31
31
|
- lib/kwtsms/phone.rb
|
|
32
32
|
- lib/kwtsms/request.rb
|
|
33
33
|
- lib/kwtsms/version.rb
|
|
34
|
-
homepage: https://github.com/
|
|
34
|
+
homepage: https://github.com/kwtsms/kwtsms-ruby
|
|
35
35
|
licenses:
|
|
36
36
|
- MIT
|
|
37
37
|
metadata:
|
|
38
|
-
homepage_uri: https://github.com/
|
|
39
|
-
source_code_uri: https://github.com/
|
|
40
|
-
changelog_uri: https://github.com/
|
|
41
|
-
bug_tracker_uri: https://github.com/
|
|
42
|
-
documentation_uri: https://github.com/
|
|
38
|
+
homepage_uri: https://github.com/kwtsms/kwtsms-ruby
|
|
39
|
+
source_code_uri: https://github.com/kwtsms/kwtsms-ruby
|
|
40
|
+
changelog_uri: https://github.com/kwtsms/kwtsms-ruby/blob/main/CHANGELOG.md
|
|
41
|
+
bug_tracker_uri: https://github.com/kwtsms/kwtsms-ruby/issues
|
|
42
|
+
documentation_uri: https://github.com/kwtsms/kwtsms-ruby#readme
|
|
43
43
|
rubygems_mfa_required: 'true'
|
|
44
44
|
post_install_message:
|
|
45
45
|
rdoc_options: []
|