182 lines
6.3 KiB
Ruby
182 lines
6.3 KiB
Ruby
require 'net/http'
|
|
require 'json'
|
|
|
|
module Wuzapi
|
|
class Client
|
|
class Error < StandardError; end
|
|
class AuthenticationError < Error; end
|
|
class ConnectionError < Error; end
|
|
|
|
attr_reader :base_url
|
|
|
|
def initialize(base_url)
|
|
@base_url = normalize_url(base_url)
|
|
end
|
|
|
|
# Admin Endpoints (Use Authorization header)
|
|
def create_user(admin_token, name, user_token)
|
|
payload = { name: name, token: user_token }
|
|
request(:post, '/admin/users', payload, admin_auth_headers(admin_token))
|
|
end
|
|
|
|
def delete_user(admin_token, user_id)
|
|
request(:delete, "/admin/users/#{user_id}", nil, admin_auth_headers(admin_token))
|
|
end
|
|
|
|
# User Endpoints (Use token header)
|
|
def send_text(user_token, phone_number, body)
|
|
# Payload MUST be Case-Sensitive: Key 'Phone' and 'Body'
|
|
payload = { 'Phone' => phone_number, 'Body' => body }
|
|
request(:post, '/chat/send/text', payload, user_auth_headers(user_token))
|
|
end
|
|
|
|
def send_image(user_token, phone_number, base64_data, caption = nil)
|
|
payload = { 'Phone' => phone_number, 'Body' => base64_data, 'Caption' => caption }
|
|
request(:post, '/chat/send/image', payload, user_auth_headers(user_token))
|
|
end
|
|
|
|
def send_file(user_token, phone_number, base64_data, filename)
|
|
payload = { 'Phone' => phone_number, 'Body' => base64_data, 'Filename' => filename }
|
|
request(:post, '/chat/send/file', payload, user_auth_headers(user_token))
|
|
end
|
|
|
|
def send_reaction(user_token, phone_number, message_id, emoji)
|
|
payload = { 'Phone' => phone_number, 'Body' => emoji, 'Id' => message_id }
|
|
request(:post, '/chat/react', payload, user_auth_headers(user_token))
|
|
end
|
|
|
|
def send_chat_presence(user_token, phone_number, state, media = nil)
|
|
# State: "composing" or "paused"
|
|
# Media: "audio" (optional)
|
|
payload = { 'Phone' => phone_number, 'State' => state }
|
|
payload['Media'] = media if media
|
|
request(:post, '/chat/presence', payload, user_auth_headers(user_token))
|
|
end
|
|
|
|
def session_status(user_token)
|
|
request(:get, '/session/status', nil, user_auth_headers(user_token))
|
|
end
|
|
|
|
def get_qr_code(user_token)
|
|
request(:get, '/session/qr', nil, user_auth_headers(user_token))
|
|
end
|
|
|
|
def session_connect(user_token)
|
|
request(:post, '/session/connect', {}, user_auth_headers(user_token))
|
|
end
|
|
|
|
def session_disconnect(user_token)
|
|
request(:post, '/session/disconnect', nil, user_auth_headers(user_token))
|
|
end
|
|
|
|
def session_logout(user_token)
|
|
request(:get, '/session/logout', nil, user_auth_headers(user_token))
|
|
end
|
|
|
|
def set_webhook(user_token, webhook_url)
|
|
# Wuzapi expects PascalCase keys 'WebhookURL' and 'Events' with 'All' per user verification.
|
|
payload = { 'WebhookURL' => webhook_url, 'Events' => ['All'] }
|
|
request(:post, '/webhook', payload, user_auth_headers(user_token))
|
|
end
|
|
|
|
def update_webhook(user_token, webhook_url)
|
|
payload = { 'WebhookURL' => webhook_url, 'Events' => ['All'] }
|
|
request(:put, '/webhook', payload, user_auth_headers(user_token))
|
|
end
|
|
|
|
def get_webhook(user_token)
|
|
request(:get, '/webhook', nil, user_auth_headers(user_token))
|
|
end
|
|
|
|
private
|
|
|
|
def normalize_url(url)
|
|
url.to_s.gsub(%r{/$}, '')
|
|
end
|
|
|
|
def admin_auth_headers(token)
|
|
{ 'Authorization' => token }
|
|
end
|
|
|
|
def user_auth_headers(token)
|
|
{ 'token' => token }
|
|
end
|
|
|
|
def request(method, path, payload, headers)
|
|
uri = URI.parse("#{base_url}#{path}")
|
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
|
|
if uri.scheme == 'https'
|
|
http.use_ssl = true
|
|
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
end
|
|
|
|
request_obj = case method
|
|
when :get
|
|
Net::HTTP::Get.new(uri.request_uri)
|
|
when :post
|
|
Net::HTTP::Post.new(uri.request_uri)
|
|
when :put
|
|
Net::HTTP::Put.new(uri.request_uri)
|
|
when :delete
|
|
Net::HTTP::Delete.new(uri.request_uri)
|
|
end
|
|
|
|
# Common headers
|
|
request_obj['Content-Type'] = 'application/json'
|
|
request_obj['Accept'] = 'application/json'
|
|
|
|
# Auth headers
|
|
headers.each { |k, v| request_obj[k] = v }
|
|
|
|
request_obj.body = payload.to_json if payload
|
|
|
|
begin
|
|
response = http.request(request_obj)
|
|
handle_response(response)
|
|
rescue Errno::ECONNREFUSED, Net::OpenTimeout, Net::ReadTimeout => e
|
|
raise ConnectionError, "Could not connect to Wuzapi: #{e.message}"
|
|
end
|
|
end
|
|
|
|
def handle_response(response)
|
|
Rails.logger.info "WUZAPI RAW RESPONSE: status=#{response.code} ct=#{response['content-type']} body=#{response.body.to_s.truncate(1000)}"
|
|
|
|
if response.code.to_i >= 200 && response.code.to_i < 300
|
|
content_type = response['content-type'] || ''
|
|
|
|
if content_type.include?('image/')
|
|
require 'base64'
|
|
base64_image = Base64.strict_encode64(response.body)
|
|
return { 'qrcode' => "data:#{content_type};base64,#{base64_image}" }
|
|
end
|
|
|
|
begin
|
|
body = JSON.parse(response.body)
|
|
# Normalize keys to 'qrcode'
|
|
# Check nested data object
|
|
if body['data'].is_a?(Hash)
|
|
found = body['data']['qrcode'] || body['data']['qr'] || body['data']['QRCode'] || body['data']['QR'] || body['data']['base64'] || body['data']['image']
|
|
body['qrcode'] = found if found
|
|
# Check if data is the string itself
|
|
elsif body['data'].is_a?(String) && (body['data'].start_with?('data:') || body['data'].length > 50)
|
|
body['qrcode'] = body['data']
|
|
end
|
|
|
|
# Check root keys if still not found
|
|
body['qrcode'] = body['qr'] || body['QRCode'] || body['QR'] || body['base64'] || body['image'] unless body['qrcode']
|
|
|
|
return body
|
|
rescue JSON::ParserError
|
|
Rails.logger.warn "Wuzapi response parse error or non-JSON: #{response.body}"
|
|
return { 'raw_body' => response.body }
|
|
end
|
|
elsif response.code.to_i == 401 || response.code.to_i == 403
|
|
raise AuthenticationError, "Authentication failed: #{response.code} #{response.body}"
|
|
else
|
|
raise Error, "API Error: #{response.code} #{response.body}"
|
|
end
|
|
end
|
|
end
|
|
end
|