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 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
+ [![Gem Version](https://badge.fury.io/rb/kwtsms.svg)](https://rubygems.org/gems/kwtsms)
4
+ [![CI](https://github.com/boxlinknet/kwtsms-ruby/actions/workflows/ci.yml/badge.svg)](https://github.com/boxlinknet/kwtsms-ruby/actions/workflows/ci.yml)
5
+ [![Security Audit](https://github.com/boxlinknet/kwtsms-ruby/actions/workflows/codeql.yml/badge.svg)](https://github.com/boxlinknet/kwtsms-ruby/actions/workflows/codeql.yml)
6
+ [![Ruby](https://img.shields.io/badge/Ruby-%3E%3D%202.7-red.svg)](https://www.ruby-lang.org/)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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