waservix 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +116 -0
- data/lib/waservix.rb +196 -0
- metadata +52 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c3051c5426d31968d5f9c5fb891871698ec55b7c9f8efbbd91cd4b1f6f17dc2d
|
|
4
|
+
data.tar.gz: e45c7a50952b53cc4d9ddb756f9abbce88424cd8aa2af374bba63fa6be1b931c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 7f035fc9c42f8ca4699a738efaf5f13fb1ba3cd8d152a2b9b8a4337e94ea5cfc2a2d67cb68d52957678e8b4b7ba9441fc4433ffa83c155f1d8d4fd1aee0d9f6b
|
|
7
|
+
data.tar.gz: 752ff0e00f931b4e02242faf9c2bc9af29a921ca25c08090b0ef42f0ee2816d33a3c8d7bcbfecd0e14d90b4e8d69e504fa43214b2be4d34ce4a966b582aec74e
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tekservix
|
|
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,116 @@
|
|
|
1
|
+
# WAservix Ruby client
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/waservix)
|
|
4
|
+
|
|
5
|
+
Ruby client for the [WAservix WhatsApp API Gateway](https://tekservix.com/waservix/).
|
|
6
|
+
|
|
7
|
+
WAservix links your own WhatsApp number by QR code (the same way WhatsApp Web does) and gives you a REST API to send text, images, video, audio, documents, locations, polls and more, with webhooks for incoming messages and status updates.
|
|
8
|
+
|
|
9
|
+
- Ruby 2.7+, no dependencies (standard library only)
|
|
10
|
+
- Works in Rails, Sinatra, Hanami, Sidekiq jobs and scripts
|
|
11
|
+
- Webhook check built in
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
gem install waservix
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
or in your Gemfile:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
gem 'waservix'
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
Copy your device's API key from the WAservix dashboard (Devices -> API key) and keep it on your server.
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
require 'waservix'
|
|
31
|
+
|
|
32
|
+
wa = WAservix::Client.new(api_key: ENV['WASERVIX_KEY'])
|
|
33
|
+
|
|
34
|
+
res = wa.send_text('+15551234567', 'Hello Aline, your order is ready')
|
|
35
|
+
res['msgId'] # => 58213
|
|
36
|
+
res['status'] # => "in_progress"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Sending
|
|
40
|
+
|
|
41
|
+
Messages are queued and sent one at a time per number, so each call returns right away with `status: "in_progress"` and a `msgId`.
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
# Image with caption
|
|
45
|
+
wa.send_image('+15551234567', 'https://example.com/product.jpg', caption: 'Back in stock')
|
|
46
|
+
|
|
47
|
+
# Document
|
|
48
|
+
wa.send_document('+15551234567', 'https://example.com/inv-1042.pdf', file_name: 'Invoice-1042.pdf', caption: 'Your invoice')
|
|
49
|
+
|
|
50
|
+
# Anything the API supports, including groups (JID ending in @g.us)
|
|
51
|
+
wa.send_message('120363040000000000@g.us', text: 'Team meeting at 3pm')
|
|
52
|
+
wa.send_message('+15551234567', location: { latitude: -1.9441, longitude: 30.0619, name: 'Our shop' })
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Status, contacts and groups
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
info = wa.get_message_info(58213) # queued, sent, delivered, read, played or failed
|
|
59
|
+
check = wa.on_whatsapp('+15551234567') # { "exists" => true, "jid" => "..." }
|
|
60
|
+
conn = wa.get_status # { "status" => "connected" }
|
|
61
|
+
|
|
62
|
+
wa.edit_message(58213, 'Corrected text') # own text messages, about 15 minutes
|
|
63
|
+
groups = wa.list_groups
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Anything else in the [API reference](https://tekservix.com/waservix/docs/) is one call away:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
wa.request(:post, '/messages/read', { msgId: '3EB0X123456789' })
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Webhooks
|
|
73
|
+
|
|
74
|
+
Set a webhook URL and events for the number in the dashboard. Each request carries `X-Webhook-Signature`, which equals the device's webhook secret. Rails example:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
class WaWebhooksController < ActionController::API
|
|
78
|
+
def create
|
|
79
|
+
unless WAservix.verify_webhook(request.headers['X-Webhook-Signature'], ENV['WASERVIX_WEBHOOK_SECRET'])
|
|
80
|
+
return head :unauthorized
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
payload = JSON.parse(request.raw_post)
|
|
84
|
+
if payload['event'] == 'messages.received'
|
|
85
|
+
m = payload['data']['messages']
|
|
86
|
+
IncomingWhatsAppJob.perform_later(m['key']['cleanedSenderPn'], m['messageBody'])
|
|
87
|
+
end
|
|
88
|
+
head :ok # acknowledge fast; do slow work in a job
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Errors
|
|
94
|
+
|
|
95
|
+
Failed calls raise `WAservix::Error` with `status`, `message`, field `errors` and, on 429, `retry_after` in seconds. A 503 means the device is reconnecting: retry shortly.
|
|
96
|
+
|
|
97
|
+
Never put the key in JavaScript or a mobile app: anyone could read it. Call the API from your server.
|
|
98
|
+
|
|
99
|
+
## Migrating from WaSender
|
|
100
|
+
|
|
101
|
+
WAservix accepts the same `/send-message` body and webhook fields as WaSender, so most integrations switch by changing the base URL and key. See the [compatibility notes](https://tekservix.com/waservix/docs/).
|
|
102
|
+
|
|
103
|
+
## Use it responsibly
|
|
104
|
+
|
|
105
|
+
WAservix is not Meta's official WhatsApp Business Platform. There's no business verification and no per-conversation fee, but WhatsApp's spam rules apply to your number directly. Message people who expect to hear from you, warm up new numbers slowly and keep the built-in account protection on.
|
|
106
|
+
|
|
107
|
+
## More from Tekservix
|
|
108
|
+
|
|
109
|
+
- [SMS Gora SMS Gateway](https://tekservix.com/smsgora/): send SMS through your own Android phones (`gem install smsgora`)
|
|
110
|
+
- [AI Sales Agent](https://tekservix.com/ai-sales-agent): answers customers on WhatsApp and SMS using your website's content
|
|
111
|
+
- [Website to App](https://tekservix.com/mobile-apps): turn your website into Android and iOS apps
|
|
112
|
+
- WAservix is also available for Node.js, Python, PHP, Go, Java/Kotlin and .NET: see [github.com/TekservixHQ](https://github.com/TekservixHQ)
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT
|
data/lib/waservix.rb
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'openssl'
|
|
6
|
+
require 'uri'
|
|
7
|
+
|
|
8
|
+
# Ruby client for the WAservix WhatsApp API Gateway.
|
|
9
|
+
#
|
|
10
|
+
# wa = WAservix::Client.new(api_key: ENV['WASERVIX_KEY'])
|
|
11
|
+
# wa.send_text('+15551234567', 'Hello Aline, your order is ready')
|
|
12
|
+
#
|
|
13
|
+
# Docs: https://tekservix.com/waservix/docs/
|
|
14
|
+
module WAservix
|
|
15
|
+
VERSION = '1.0.0'
|
|
16
|
+
DEFAULT_BASE_URL = 'https://tekservix.com/waservix/api'
|
|
17
|
+
|
|
18
|
+
# Raised when the API answers with an error. A 503 means the device is reconnecting.
|
|
19
|
+
class Error < StandardError
|
|
20
|
+
attr_reader :status, :errors, :body, :retry_after
|
|
21
|
+
|
|
22
|
+
def initialize(message, status: nil, errors: nil, body: nil, retry_after: nil)
|
|
23
|
+
super(message)
|
|
24
|
+
@status = status
|
|
25
|
+
@errors = errors
|
|
26
|
+
@body = body
|
|
27
|
+
@retry_after = retry_after
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class Client
|
|
32
|
+
attr_reader :base_url
|
|
33
|
+
|
|
34
|
+
# api_key: device API key (wpk_...) or a personal access token for session management.
|
|
35
|
+
def initialize(api_key:, base_url: DEFAULT_BASE_URL, timeout: 30)
|
|
36
|
+
raise ArgumentError, 'WAservix: api_key is required (Devices -> API key in the WAservix dashboard)' if api_key.nil? || api_key.empty?
|
|
37
|
+
|
|
38
|
+
@api_key = api_key
|
|
39
|
+
@base_url = base_url.sub(%r{/+\z}, '')
|
|
40
|
+
@timeout = timeout
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Call any documented endpoint, e.g. request(:get, '/groups'). Returns the `data` field.
|
|
44
|
+
def request(method, path, body = nil)
|
|
45
|
+
uri = URI(@base_url + path)
|
|
46
|
+
klass = { get: Net::HTTP::Get, post: Net::HTTP::Post, put: Net::HTTP::Put, delete: Net::HTTP::Delete }.fetch(method.to_s.downcase.to_sym)
|
|
47
|
+
req = klass.new(uri)
|
|
48
|
+
req['Authorization'] = "Bearer #{@api_key}"
|
|
49
|
+
req['Accept'] = 'application/json'
|
|
50
|
+
req['User-Agent'] = "waservix-ruby/#{VERSION}"
|
|
51
|
+
unless body.nil?
|
|
52
|
+
req['Content-Type'] = 'application/json'
|
|
53
|
+
req.body = JSON.generate(body)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https',
|
|
57
|
+
open_timeout: @timeout, read_timeout: @timeout) { |http| http.request(req) }
|
|
58
|
+
status = res.code.to_i
|
|
59
|
+
text = res.body.to_s
|
|
60
|
+
json = begin
|
|
61
|
+
text.empty? ? nil : JSON.parse(text)
|
|
62
|
+
rescue JSON::ParserError
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
if status >= 400 || (json.is_a?(Hash) && json['success'] == false)
|
|
67
|
+
retry_after = res['Retry-After']
|
|
68
|
+
raise Error.new((json.is_a?(Hash) && json['message']) || "HTTP #{status}",
|
|
69
|
+
status: status,
|
|
70
|
+
errors: json.is_a?(Hash) ? json['errors'] : nil,
|
|
71
|
+
body: json || text,
|
|
72
|
+
retry_after: retry_after && Float(retry_after, exception: false))
|
|
73
|
+
end
|
|
74
|
+
json.is_a?(Hash) && json.key?('data') ? json['data'] : json
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Send any message type to a number or group JID (...@g.us). Fields use the API names:
|
|
78
|
+
# text, imageUrl, videoUrl, audioUrl, documentUrl, fileName, stickerUrl, contact, location,
|
|
79
|
+
# poll, replyTo, mentions, viewOnce. Returns { 'msgId' => ..., 'jid' => ..., 'status' => 'in_progress' }.
|
|
80
|
+
def send_message(to, **fields)
|
|
81
|
+
raise ArgumentError, 'send_message: "to" is required (international number or group JID)' if to.nil? || to.empty?
|
|
82
|
+
|
|
83
|
+
request(:post, '/send-message', { to: to }.merge(fields.reject { |_, v| v.nil? }))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def send_text(to, text)
|
|
87
|
+
send_message(to, text: text)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def send_image(to, image_url, caption: nil)
|
|
91
|
+
send_message(to, imageUrl: image_url, text: caption)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def send_document(to, document_url, file_name: nil, caption: nil)
|
|
95
|
+
send_message(to, documentUrl: document_url, fileName: file_name, text: caption)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Status: queued, sent, delivered, read, played or failed.
|
|
99
|
+
def get_message_info(msg_id)
|
|
100
|
+
request(:get, "/messages/#{esc(msg_id)}/info")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Edit one of your own text messages (about 15 minutes after sending).
|
|
104
|
+
def edit_message(msg_id, text)
|
|
105
|
+
request(:put, "/messages/#{esc(msg_id)}", { text: text })
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def delete_message(msg_id)
|
|
109
|
+
request(:delete, "/messages/#{esc(msg_id)}")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def resend_message(msg_id)
|
|
113
|
+
request(:post, "/messages/#{esc(msg_id)}/resend")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Is this number on WhatsApp? Returns { 'exists' => true/false, 'jid' => ... }.
|
|
117
|
+
def on_whatsapp(phone)
|
|
118
|
+
request(:get, "/on-whatsapp/#{esc(phone)}")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# connected, connecting, need_scan, disconnected, logged_out or expired.
|
|
122
|
+
def get_status
|
|
123
|
+
request(:get, '/status')
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def get_user
|
|
127
|
+
request(:get, '/user')
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def list_contacts
|
|
131
|
+
request(:get, '/contacts')
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def get_contact(phone)
|
|
135
|
+
request(:get, "/contacts/#{esc(phone)}")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def save_contact(phone, name)
|
|
139
|
+
request(:put, '/contacts', { phone: phone, name: name })
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def list_groups
|
|
143
|
+
request(:get, '/groups')
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def get_group_metadata(group_jid)
|
|
147
|
+
request(:get, "/groups/#{esc(group_jid)}/metadata")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Session management (needs a personal access token, not a device key).
|
|
151
|
+
def list_sessions
|
|
152
|
+
request(:get, '/whatsapp-sessions')
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def get_session(id)
|
|
156
|
+
request(:get, "/whatsapp-sessions/#{esc(id)}")
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def create_session(**params)
|
|
160
|
+
request(:post, '/whatsapp-sessions', params)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# method: 'qr', or 'code' with phone: for pairing codes.
|
|
164
|
+
def connect_session(id, method: 'qr', phone: nil)
|
|
165
|
+
body = { method: method }
|
|
166
|
+
body[:phone] = phone if phone
|
|
167
|
+
request(:post, "/whatsapp-sessions/#{esc(id)}/connect", body)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def get_session_qr_code(id)
|
|
171
|
+
request(:get, "/whatsapp-sessions/#{esc(id)}/qrcode")
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
private
|
|
175
|
+
|
|
176
|
+
def esc(value)
|
|
177
|
+
URI.encode_www_form_component(value.to_s).gsub('+', '%20')
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Constant-time check of the X-Webhook-Signature header against your device's webhook secret.
|
|
182
|
+
def self.verify_webhook(signature, secret)
|
|
183
|
+
return false if signature.nil? || signature.empty? || secret.nil? || secret.empty?
|
|
184
|
+
secure_compare(signature.to_s, secret.to_s)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Constant-time string comparison (works on every Ruby version).
|
|
188
|
+
def self.secure_compare(a, b)
|
|
189
|
+
return false unless a.bytesize == b.bytesize
|
|
190
|
+
|
|
191
|
+
diff = 0
|
|
192
|
+
a.bytes.zip(b.bytes) { |x, y| diff |= x ^ y }
|
|
193
|
+
diff.zero?
|
|
194
|
+
end
|
|
195
|
+
private_class_method :secure_compare
|
|
196
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: waservix
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Tekservix
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-11-10 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: 'Send WhatsApp messages, images and documents from your own number with
|
|
14
|
+
WAservix: status tracking, contacts, groups and webhooks. Works in Rails, Sinatra
|
|
15
|
+
and plain Ruby.'
|
|
16
|
+
email: support@tekservix.com
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- LICENSE
|
|
22
|
+
- README.md
|
|
23
|
+
- lib/waservix.rb
|
|
24
|
+
homepage: https://tekservix.com/waservix/
|
|
25
|
+
licenses:
|
|
26
|
+
- MIT
|
|
27
|
+
metadata:
|
|
28
|
+
homepage_uri: https://tekservix.com/waservix/
|
|
29
|
+
documentation_uri: https://tekservix.com/waservix/docs/
|
|
30
|
+
source_code_uri: https://github.com/TekservixHQ/waservix-ruby
|
|
31
|
+
bug_tracker_uri: https://github.com/TekservixHQ/waservix-ruby/issues
|
|
32
|
+
rubygems_mfa_required: 'true'
|
|
33
|
+
post_install_message:
|
|
34
|
+
rdoc_options: []
|
|
35
|
+
require_paths:
|
|
36
|
+
- lib
|
|
37
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
38
|
+
requirements:
|
|
39
|
+
- - ">="
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '2.7'
|
|
42
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
requirements: []
|
|
48
|
+
rubygems_version: 3.5.22
|
|
49
|
+
signing_key:
|
|
50
|
+
specification_version: 4
|
|
51
|
+
summary: Official Ruby client for the WAservix WhatsApp API Gateway
|
|
52
|
+
test_files: []
|