kirimi 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: 0bc013f5f011e16f83060dc902e32b6cf6c45312c2d6c443cb882503babed732
4
+ data.tar.gz: '082c9dcd26bee5df651c865dc77c93222c49f8fa70e68d4e5970ef9013efad17'
5
+ SHA512:
6
+ metadata.gz: 487b38bee90b1463c0550efdb36a7646e0bdfd50ec2f0e598407bf4aacf3b0a3a770f0c0fead719b0fb98ad68debfa54e8b26574a538c5b76b4ff1cefa94d0a4
7
+ data.tar.gz: cc5f8545838710df3641cf0207aa2dc9d168c906f76a2edc1d288942f4ce05677ed9ffa50e7c2c45c5b6128dcafec5950cf0198bc821cf989fcd66cf5d7fafe2
data/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - 2026-04-14
4
+
5
+ ### Added
6
+ - Initial release
7
+ - 16 Kirimi API endpoints supported
8
+ - Zero runtime dependencies (uses Ruby stdlib `net/http`)
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kirimi
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,235 @@
1
+ # Kirimi Ruby SDK
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/kirimi.svg)](https://badge.fury.io/rb/kirimi)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
5
+
6
+ Official Ruby SDK for the [Kirimi WhatsApp API](https://kirimi.id). Zero runtime dependencies — uses only Ruby's built-in `net/http`.
7
+
8
+ ## Installation
9
+
10
+ Add to your Gemfile:
11
+
12
+ ```ruby
13
+ gem 'kirimi'
14
+ ```
15
+
16
+ Then run:
17
+
18
+ ```bash
19
+ bundle install
20
+ ```
21
+
22
+ Or install directly:
23
+
24
+ ```bash
25
+ gem install kirimi
26
+ ```
27
+
28
+ ## Quick Start
29
+
30
+ ```ruby
31
+ require 'kirimi'
32
+
33
+ client = Kirimi::Client.new(
34
+ user_code: 'YOUR_USER_CODE',
35
+ secret: 'YOUR_SECRET'
36
+ )
37
+
38
+ resp = client.send_message(
39
+ device_id: 'YOUR_DEVICE_ID',
40
+ phone: '628111222333',
41
+ message: 'Hello from Kirimi!'
42
+ )
43
+
44
+ puts resp.success? # => true
45
+ puts resp.message # => "Message sent"
46
+ puts resp.data # => { ... }
47
+ ```
48
+
49
+ ## Constructor
50
+
51
+ ```ruby
52
+ Kirimi::Client.new(
53
+ user_code: 'YOUR_USER_CODE', # required
54
+ secret: 'YOUR_SECRET', # required
55
+ base_url: 'https://api.kirimi.id', # optional, default
56
+ timeout: 30 # optional, seconds
57
+ )
58
+ ```
59
+
60
+ ## All Methods
61
+
62
+ ### WhatsApp Unofficial
63
+
64
+ ```ruby
65
+ # Send text/media message
66
+ client.send_message(device_id:, phone:, message:, media_url: nil)
67
+
68
+ # Send file via multipart (accepts File, StringIO, or file path string)
69
+ client.send_message_file(device_id:, phone:, file:, file_name:, message: nil)
70
+
71
+ # Send message without typing indicator
72
+ client.send_message_fast(device_id:, phone:, message:, media_url: nil)
73
+ ```
74
+
75
+ ### WABA (WhatsApp Business API)
76
+
77
+ ```ruby
78
+ client.send_waba_message(device_id:, phone:, message:)
79
+ ```
80
+
81
+ ### Devices
82
+
83
+ ```ruby
84
+ client.list_devices
85
+ client.device_status(device_id:)
86
+ client.device_status_enhanced(device_id:)
87
+ ```
88
+
89
+ ### User
90
+
91
+ ```ruby
92
+ client.user_info
93
+ ```
94
+
95
+ ### Contacts
96
+
97
+ ```ruby
98
+ client.save_contact(phone:, name: nil, email: nil)
99
+ ```
100
+
101
+ ### OTP (V1)
102
+
103
+ ```ruby
104
+ # Generate & send OTP
105
+ client.generate_otp(
106
+ device_id:,
107
+ phone:,
108
+ otp_length: 6, # optional
109
+ otp_type: 'numeric', # optional: numeric, alphabetic, alphanumeric
110
+ custom_otp_message: nil # optional
111
+ )
112
+
113
+ # Validate OTP
114
+ client.validate_otp(device_id:, phone:, otp:)
115
+ ```
116
+
117
+ ### OTP (V2)
118
+
119
+ ```ruby
120
+ # Send OTP via WABA template or device
121
+ client.send_otp_v2(
122
+ phone:,
123
+ device_id:,
124
+ method: 'device', # optional: device, waba
125
+ app_name: 'MyApp', # optional
126
+ template_code: nil, # optional, for waba method
127
+ custom_message: nil # optional, for device method
128
+ )
129
+
130
+ # Verify OTP
131
+ client.verify_otp_v2(phone:, otp_code:)
132
+ ```
133
+
134
+ ### Broadcast
135
+
136
+ ```ruby
137
+ # phones accepts String or Array (Array auto-joined with comma)
138
+ client.broadcast_message(
139
+ device_id:,
140
+ phones: ['628111', '628222', '628333'],
141
+ message: 'Promo!',
142
+ delay: 2 # optional, seconds between messages
143
+ )
144
+ ```
145
+
146
+ ### Deposits
147
+
148
+ ```ruby
149
+ client.list_deposits(status: nil) # status: nil, 'paid', 'unpaid', 'expired'
150
+ client.list_packages
151
+ ```
152
+
153
+ ## Response Object
154
+
155
+ All methods return a `Kirimi::Response` instance:
156
+
157
+ ```ruby
158
+ resp = client.send_message(device_id: 'DEV', phone: '628xxx', message: 'hi')
159
+
160
+ resp.success? # => true / false (boolean method)
161
+ resp.success # => true / false (raw value)
162
+ resp.data # => Hash or nil
163
+ resp.message # => String
164
+ resp.raw # => full parsed Hash
165
+ ```
166
+
167
+ ## Error Handling
168
+
169
+ ```ruby
170
+ begin
171
+ resp = client.send_message(device_id: 'DEV', phone: '628xxx', message: 'hi')
172
+ rescue Kirimi::ApiError => e
173
+ puts e.status_code # => 401
174
+ puts e.message # => "Unauthorized"
175
+ puts e.response_data # => parsed response body
176
+ rescue Kirimi::NetworkError => e
177
+ puts "Network problem: #{e.message}"
178
+ rescue Kirimi::Error => e
179
+ puts "SDK error: #{e.message}"
180
+ end
181
+ ```
182
+
183
+ ## Rails Integration
184
+
185
+ ### Application configuration
186
+
187
+ ```ruby
188
+ # config/initializers/kirimi.rb
189
+ KIRIMI_CLIENT = Kirimi::Client.new(
190
+ user_code: ENV.fetch('KIRIMI_USER_CODE'),
191
+ secret: ENV.fetch('KIRIMI_SECRET')
192
+ )
193
+ ```
194
+
195
+ ### Controller example
196
+
197
+ ```ruby
198
+ class NotificationsController < ApplicationController
199
+ def send_otp
200
+ resp = KIRIMI_CLIENT.generate_otp(
201
+ device_id: params[:device_id],
202
+ phone: params[:phone],
203
+ otp_length: 6,
204
+ otp_type: 'numeric'
205
+ )
206
+
207
+ if resp.success?
208
+ render json: { sent: true }
209
+ else
210
+ render json: { sent: false, error: resp.message }, status: :unprocessable_entity
211
+ end
212
+ rescue Kirimi::ApiError => e
213
+ render json: { error: e.message }, status: e.status_code
214
+ end
215
+ end
216
+ ```
217
+
218
+ ### Background Job example
219
+
220
+ ```ruby
221
+ class SendWhatsAppJob < ApplicationJob
222
+ queue_as :default
223
+
224
+ def perform(device_id, phone, message)
225
+ KIRIMI_CLIENT.send_message(device_id: device_id, phone: phone, message: message)
226
+ rescue Kirimi::ApiError, Kirimi::NetworkError => e
227
+ Rails.logger.error "[Kirimi] #{e.class}: #{e.message}"
228
+ raise # re-raise to trigger job retry
229
+ end
230
+ end
231
+ ```
232
+
233
+ ## License
234
+
235
+ MIT — see [LICENSE](LICENSE).
data/kirimi.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/kirimi/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'kirimi'
7
+ spec.version = Kirimi::VERSION
8
+ spec.authors = ['Kirimi']
9
+ spec.email = ['support@kirimi.id']
10
+
11
+ spec.summary = 'Official Ruby SDK for Kirimi WhatsApp API'
12
+ spec.description = 'Send WhatsApp messages, OTP, broadcasts, and more via the Kirimi API.'
13
+ spec.homepage = 'https://github.com/kiriminow/kirimi-ruby'
14
+ spec.license = 'MIT'
15
+
16
+ spec.required_ruby_version = '>= 2.6.0'
17
+
18
+ spec.metadata['homepage_uri'] = spec.homepage
19
+ spec.metadata['source_code_uri'] = spec.homepage
20
+ spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/main/CHANGELOG.md"
21
+
22
+ spec.files = Dir.glob(%w[lib/**/* LICENSE README.md CHANGELOG.md kirimi.gemspec])
23
+ spec.test_files = Dir.glob('test/**/*_test.rb')
24
+ spec.require_paths = ['lib']
25
+
26
+ # No runtime dependencies — uses only Ruby stdlib (net/http, json, securerandom)
27
+
28
+ spec.add_development_dependency 'rake', '~> 13.0'
29
+ end
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'uri'
6
+
7
+ module Kirimi
8
+ class Client
9
+ DEFAULT_BASE_URL = 'https://api.kirimi.id'
10
+ DEFAULT_TIMEOUT = 30
11
+
12
+ def initialize(user_code:, secret:, base_url: DEFAULT_BASE_URL, timeout: DEFAULT_TIMEOUT)
13
+ @user_code = user_code
14
+ @secret = secret
15
+ @base_url = base_url.chomp('/')
16
+ @timeout = timeout
17
+ end
18
+
19
+ # --- WhatsApp Unofficial ---
20
+
21
+ def send_message(device_id:, phone:, message:, media_url: nil)
22
+ body = { device_id: device_id, phone: phone, message: message }
23
+ body[:media_url] = media_url if media_url
24
+ post('/v1/send-message', body)
25
+ end
26
+
27
+ def send_message_file(device_id:, phone:, file:, file_name:, message: nil)
28
+ file_io = file.is_a?(String) ? File.open(file, 'rb') : file
29
+ fields = {
30
+ 'user_code' => @user_code,
31
+ 'secret' => @secret,
32
+ 'device_id' => device_id,
33
+ 'phone' => phone,
34
+ 'fileName' => file_name
35
+ }
36
+ fields['message'] = message if message
37
+ post_multipart('/v1/send-message-file', fields, file_io, file_name)
38
+ ensure
39
+ file_io&.close if file.is_a?(String)
40
+ end
41
+
42
+ def send_message_fast(device_id:, phone:, message:, media_url: nil)
43
+ body = { device_id: device_id, phone: phone, message: message }
44
+ body[:media_url] = media_url if media_url
45
+ post('/v1/send-message-fast', body)
46
+ end
47
+
48
+ # --- WABA ---
49
+
50
+ def send_waba_message(device_id:, phone:, message:)
51
+ post('/v1/waba/send-message', { device_id: device_id, phone: phone, message: message })
52
+ end
53
+
54
+ # --- Devices ---
55
+
56
+ def list_devices
57
+ post('/v1/list-devices', {})
58
+ end
59
+
60
+ def device_status(device_id:)
61
+ post('/v1/device-status', { device_id: device_id })
62
+ end
63
+
64
+ def device_status_enhanced(device_id:)
65
+ post('/v1/device-status-enhanced', { device_id: device_id })
66
+ end
67
+
68
+ # --- User ---
69
+
70
+ def user_info
71
+ post('/v1/user-info', {})
72
+ end
73
+
74
+ # --- Contacts ---
75
+
76
+ def save_contact(phone:, name: nil, email: nil)
77
+ body = { phone: phone }
78
+ body[:name] = name if name
79
+ body[:email] = email if email
80
+ post('/v1/save-contact', body)
81
+ end
82
+
83
+ # --- OTP ---
84
+
85
+ def generate_otp(device_id:, phone:, otp_length: nil, otp_type: nil, custom_otp_message: nil)
86
+ body = { device_id: device_id, phone: phone }
87
+ body[:otp_length] = otp_length if otp_length
88
+ body[:otp_type] = otp_type if otp_type
89
+ body[:customOtpMessage] = custom_otp_message if custom_otp_message
90
+ post('/v1/generate-otp', body)
91
+ end
92
+
93
+ def validate_otp(device_id:, phone:, otp:)
94
+ post('/v1/validate-otp', { device_id: device_id, phone: phone, otp: otp })
95
+ end
96
+
97
+ # --- OTP V2 ---
98
+
99
+ def send_otp_v2(phone:, device_id:, method: nil, app_name: nil, template_code: nil, custom_message: nil)
100
+ body = { phone: phone, device_id: device_id }
101
+ body[:method] = method if method
102
+ body[:app_name] = app_name if app_name
103
+ body[:template_code] = template_code if template_code
104
+ body[:custom_message] = custom_message if custom_message
105
+ post('/v2/otp/send', body)
106
+ end
107
+
108
+ def verify_otp_v2(phone:, otp_code:)
109
+ post('/v2/otp/verify', { phone: phone, otp_code: otp_code })
110
+ end
111
+
112
+ # --- Broadcast ---
113
+
114
+ def broadcast_message(device_id:, phones:, message:, delay: nil)
115
+ phones_str = phones.is_a?(Array) ? phones.join(',') : phones
116
+ body = { device_id: device_id, phones: phones_str, message: message }
117
+ body[:delay] = delay if delay
118
+ post('/v1/broadcast-message', body)
119
+ end
120
+
121
+ # --- Deposits ---
122
+
123
+ def list_deposits(status: nil)
124
+ body = {}
125
+ body[:status] = status if status
126
+ post('/v1/list-deposits', body)
127
+ end
128
+
129
+ def list_packages
130
+ post('/v1/list-packages', {})
131
+ end
132
+
133
+ private
134
+
135
+ def auth_params
136
+ { user_code: @user_code, secret: @secret }
137
+ end
138
+
139
+ def post(path, body)
140
+ uri = URI("#{@base_url}#{path}")
141
+ http = build_http(uri)
142
+
143
+ request = Net::HTTP::Post.new(uri)
144
+ request['Content-Type'] = 'application/json'
145
+ request['Accept'] = 'application/json'
146
+ request.body = JSON.generate(auth_params.merge(body))
147
+
148
+ execute(http, request)
149
+ end
150
+
151
+ def post_multipart(path, fields, file_io, file_name)
152
+ uri = URI("#{@base_url}#{path}")
153
+ http = build_http(uri)
154
+ boundary = "KirimiRubySDK#{SecureRandom.hex(8)}"
155
+
156
+ body_parts = []
157
+ fields.each do |key, value|
158
+ body_parts << "--#{boundary}\r\n"
159
+ body_parts << "Content-Disposition: form-data; name=\"#{key}\"\r\n\r\n"
160
+ body_parts << "#{value}\r\n"
161
+ end
162
+
163
+ # File part
164
+ file_data = file_io.read
165
+ body_parts << "--#{boundary}\r\n"
166
+ body_parts << "Content-Disposition: form-data; name=\"file\"; filename=\"#{file_name}\"\r\n"
167
+ body_parts << "Content-Type: application/octet-stream\r\n\r\n"
168
+ body_parts << file_data
169
+ body_parts << "\r\n--#{boundary}--\r\n"
170
+
171
+ request = Net::HTTP::Post.new(uri)
172
+ request['Content-Type'] = "multipart/form-data; boundary=#{boundary}"
173
+ request['Accept'] = 'application/json'
174
+ request.body = body_parts.join
175
+
176
+ execute(http, request)
177
+ end
178
+
179
+ def build_http(uri)
180
+ http = Net::HTTP.new(uri.host, uri.port)
181
+ http.use_ssl = (uri.scheme == 'https')
182
+ http.read_timeout = @timeout
183
+ http.open_timeout = @timeout
184
+ http
185
+ end
186
+
187
+ def execute(http, request)
188
+ response = http.request(request)
189
+ parse_response(response)
190
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ETIMEDOUT,
191
+ Net::OpenTimeout, Net::ReadTimeout, SocketError => e
192
+ raise Kirimi::NetworkError, "Network error: #{e.message}"
193
+ end
194
+
195
+ def parse_response(response)
196
+ status = response.code.to_i
197
+ body = parse_body(response.body)
198
+
199
+ unless (200..299).cover?(status)
200
+ msg = body.is_a?(Hash) ? (body['message'] || response.message) : response.message
201
+ raise Kirimi::ApiError.new(status, msg, body)
202
+ end
203
+
204
+ Kirimi::Response.new(body)
205
+ end
206
+
207
+ def parse_body(body)
208
+ return {} if body.nil? || body.empty?
209
+
210
+ JSON.parse(body)
211
+ rescue JSON::ParserError
212
+ { 'message' => body }
213
+ end
214
+ end
215
+ end
216
+
217
+ # Lazy require SecureRandom (part of stdlib, always available)
218
+ require 'securerandom'
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kirimi
4
+ class Error < StandardError; end
5
+
6
+ class ApiError < Error
7
+ attr_reader :status_code, :response_data
8
+
9
+ def initialize(status_code, message, response_data = nil)
10
+ super(message)
11
+ @status_code = status_code
12
+ @response_data = response_data
13
+ end
14
+ end
15
+
16
+ class NetworkError < Error; end
17
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kirimi
4
+ class Response
5
+ attr_reader :success, :data, :message, :raw
6
+
7
+ def initialize(hash)
8
+ @raw = hash
9
+ @success = hash['success']
10
+ @data = hash['data']
11
+ @message = hash['message']
12
+ end
13
+
14
+ def success?
15
+ @success == true
16
+ end
17
+
18
+ def to_s
19
+ "#<Kirimi::Response success=#{@success} message=#{@message.inspect}>"
20
+ end
21
+
22
+ def inspect
23
+ to_s
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kirimi
4
+ VERSION = '0.1.0'
5
+ end
data/lib/kirimi.rb ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'kirimi/version'
4
+ require_relative 'kirimi/errors'
5
+ require_relative 'kirimi/response'
6
+ require_relative 'kirimi/client'
7
+
8
+ # Kirimi Ruby SDK — Official client for the Kirimi WhatsApp API.
9
+ #
10
+ # Usage:
11
+ # client = Kirimi::Client.new(user_code: 'USER', secret: 'SECRET')
12
+ # resp = client.send_message(device_id: 'DEV', phone: '628xxx', message: 'Hello!')
13
+ # puts resp.success? # => true
14
+ # puts resp.data # => { ... }
15
+ module Kirimi
16
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require 'minitest/pride'
5
+ require 'json'
6
+ require 'ostruct'
7
+
8
+ $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
9
+ require 'kirimi'
10
+
11
+ # Minimal HTTP response stub
12
+ FakeResponse = Struct.new(:code, :body, :message) do
13
+ def [](key); nil; end
14
+ end
15
+
16
+ # Captures the last request body sent via Net::HTTP
17
+ module NetHTTPStub
18
+ @stub_status = 200
19
+ @stub_body = {}
20
+ @last_body = nil
21
+ @last_path = nil
22
+
23
+ class << self
24
+ attr_accessor :stub_status, :stub_body, :last_body, :last_path
25
+
26
+ def setup(status:, body:)
27
+ @stub_status = status
28
+ @stub_body = body
29
+ @last_body = nil
30
+ @last_path = nil
31
+ end
32
+ end
33
+ end
34
+
35
+ # Monkey-patch Net::HTTP for tests only
36
+ module Net
37
+ class HTTP
38
+ def request(req)
39
+ NetHTTPStub.last_body = JSON.parse(req.body) rescue req.body
40
+ NetHTTPStub.last_path = req.path
41
+ FakeResponse.new(
42
+ NetHTTPStub.stub_status.to_s,
43
+ NetHTTPStub.stub_body.to_json,
44
+ 'OK'
45
+ )
46
+ end
47
+ end
48
+ end
49
+
50
+ class ClientTest < Minitest::Test
51
+ def setup
52
+ @client = Kirimi::Client.new(user_code: 'USER', secret: 'SECRET')
53
+ end
54
+
55
+ def stub(status: 200, body: { 'success' => true, 'data' => nil, 'message' => 'ok' })
56
+ NetHTTPStub.setup(status: status, body: body)
57
+ end
58
+
59
+ # ---- send_message ----
60
+
61
+ def test_send_message_correct_body
62
+ stub(body: { 'success' => true, 'data' => { 'id' => 'msg_1' }, 'message' => 'sent' })
63
+
64
+ resp = @client.send_message(device_id: 'DEV1', phone: '628111', message: 'Hello!')
65
+
66
+ assert_equal 'USER', NetHTTPStub.last_body['user_code']
67
+ assert_equal 'SECRET', NetHTTPStub.last_body['secret']
68
+ assert_equal 'DEV1', NetHTTPStub.last_body['device_id']
69
+ assert_equal '628111', NetHTTPStub.last_body['phone']
70
+ assert_equal 'Hello!', NetHTTPStub.last_body['message']
71
+ assert_instance_of Kirimi::Response, resp
72
+ assert resp.success?
73
+ assert_equal 'msg_1', resp.data['id']
74
+ end
75
+
76
+ def test_send_message_includes_media_url
77
+ stub
78
+ @client.send_message(device_id: 'DEV1', phone: '628111', message: 'pic', media_url: 'https://img.example.com/a.jpg')
79
+ assert_equal 'https://img.example.com/a.jpg', NetHTTPStub.last_body['media_url']
80
+ end
81
+
82
+ def test_send_message_omits_media_url_when_nil
83
+ stub
84
+ @client.send_message(device_id: 'DEV1', phone: '628111', message: 'hi')
85
+ refute NetHTTPStub.last_body.key?('media_url')
86
+ end
87
+
88
+ # ---- generate_otp ----
89
+
90
+ def test_generate_otp_with_optional_params
91
+ stub(body: { 'success' => true, 'data' => { 'otp' => '123456' }, 'message' => 'OTP sent' })
92
+
93
+ resp = @client.generate_otp(
94
+ device_id: 'DEV1',
95
+ phone: '628111',
96
+ otp_length: 6,
97
+ otp_type: 'numeric',
98
+ custom_otp_message: 'Your code is {otp}'
99
+ )
100
+
101
+ assert_equal 6, NetHTTPStub.last_body['otp_length']
102
+ assert_equal 'numeric', NetHTTPStub.last_body['otp_type']
103
+ assert_equal 'Your code is {otp}', NetHTTPStub.last_body['customOtpMessage']
104
+ assert resp.success?
105
+ assert_equal '123456', resp.data['otp']
106
+ end
107
+
108
+ def test_generate_otp_omits_optional_when_nil
109
+ stub
110
+ @client.generate_otp(device_id: 'DEV1', phone: '628111')
111
+ refute NetHTTPStub.last_body.key?('otp_length')
112
+ refute NetHTTPStub.last_body.key?('customOtpMessage')
113
+ end
114
+
115
+ # ---- error handling ----
116
+
117
+ def test_raises_api_error_on_401
118
+ stub(status: 401, body: { 'success' => false, 'message' => 'Unauthorized' })
119
+
120
+ err = assert_raises(Kirimi::ApiError) { @client.user_info }
121
+ assert_equal 401, err.status_code
122
+ assert_equal 'Unauthorized', err.message
123
+ end
124
+
125
+ def test_raises_api_error_on_500
126
+ stub(status: 500, body: { 'success' => false, 'message' => 'Server Error' })
127
+
128
+ err = assert_raises(Kirimi::ApiError) { @client.list_devices }
129
+ assert_equal 500, err.status_code
130
+ end
131
+
132
+ # ---- response wrapping ----
133
+
134
+ def test_response_wraps_data_correctly
135
+ stub(body: { 'success' => true, 'data' => { 'name' => 'John' }, 'message' => 'OK' })
136
+
137
+ resp = @client.user_info
138
+
139
+ assert_instance_of Kirimi::Response, resp
140
+ assert resp.success?
141
+ assert_equal true, resp.success
142
+ assert_equal 'John', resp.data['name']
143
+ assert_equal 'OK', resp.message
144
+ assert_instance_of Hash, resp.raw
145
+ end
146
+
147
+ # ---- broadcast phones array ----
148
+
149
+ def test_broadcast_joins_phones_array
150
+ stub
151
+ @client.broadcast_message(device_id: 'DEV1', phones: %w[628111 628222 628333], message: 'Promo!')
152
+ assert_equal '628111,628222,628333', NetHTTPStub.last_body['phones']
153
+ end
154
+
155
+ def test_broadcast_accepts_phones_string
156
+ stub
157
+ @client.broadcast_message(device_id: 'DEV1', phones: '628111,628222', message: 'Hi')
158
+ assert_equal '628111,628222', NetHTTPStub.last_body['phones']
159
+ end
160
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kirimi
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Kirimi
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-04-14 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
+ description: Send WhatsApp messages, OTP, broadcasts, and more via the Kirimi API.
28
+ email:
29
+ - support@kirimi.id
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - CHANGELOG.md
35
+ - LICENSE
36
+ - README.md
37
+ - kirimi.gemspec
38
+ - lib/kirimi.rb
39
+ - lib/kirimi/client.rb
40
+ - lib/kirimi/errors.rb
41
+ - lib/kirimi/response.rb
42
+ - lib/kirimi/version.rb
43
+ - test/client_test.rb
44
+ homepage: https://github.com/kiriminow/kirimi-ruby
45
+ licenses:
46
+ - MIT
47
+ metadata:
48
+ homepage_uri: https://github.com/kiriminow/kirimi-ruby
49
+ source_code_uri: https://github.com/kiriminow/kirimi-ruby
50
+ changelog_uri: https://github.com/kiriminow/kirimi-ruby/blob/main/CHANGELOG.md
51
+ post_install_message:
52
+ rdoc_options: []
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: 2.6.0
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ requirements: []
66
+ rubygems_version: 3.0.3.1
67
+ signing_key:
68
+ specification_version: 4
69
+ summary: Official Ruby SDK for Kirimi WhatsApp API
70
+ test_files:
71
+ - test/client_test.rb