waha-ruby 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 +7 -0
- data/.github/pull_request_template.md +13 -0
- data/.github/workflows/ci.yml +32 -0
- data/.github/workflows/release.yml +457 -0
- data/.reek.yml +38 -0
- data/.rubocop.yml +73 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +34 -0
- data/CONTRIBUTING.md +53 -0
- data/LICENSE.txt +21 -0
- data/README.md +63 -0
- data/Rakefile +18 -0
- data/lib/generators/waha/install/install_generator.rb +15 -0
- data/lib/generators/waha/install/templates/waha.rb +14 -0
- data/lib/waha/client.rb +37 -0
- data/lib/waha/error.rb +36 -0
- data/lib/waha/gows.rb +103 -0
- data/lib/waha/rails/controller_concern.rb +30 -0
- data/lib/waha/rails/railtie.rb +11 -0
- data/lib/waha/rails.rb +8 -0
- data/lib/waha/resource.rb +25 -0
- data/lib/waha/resources/chats.rb +29 -0
- data/lib/waha/resources/contacts.rb +37 -0
- data/lib/waha/resources/media.rb +63 -0
- data/lib/waha/resources/messages.rb +186 -0
- data/lib/waha/resources/presence.rb +37 -0
- data/lib/waha/resources/sessions.rb +100 -0
- data/lib/waha/resources/webhooks.rb +85 -0
- data/lib/waha/support.rb +39 -0
- data/lib/waha/transport/faraday.rb +117 -0
- data/lib/waha/version.rb +5 -0
- data/lib/waha/webhook.rb +49 -0
- data/lib/waha.rb +17 -0
- metadata +274 -0
data/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Contributions must keep the public API explicit, framework-neutral, and compatible with the declared v0.1 contract. Do not add runtime dependencies without a concrete API need.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
This repository uses [asdf](https://asdf-vm.com/) and `.tool-versions`.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
asdf install
|
|
11
|
+
ruby --version
|
|
12
|
+
bundle install
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`bin/setup` is a convenience wrapper for `bundle install`.
|
|
16
|
+
|
|
17
|
+
Run focused tests while working:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bundle exec rspec spec/path/to/spec.rb
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Before submitting, run the complete repository gate:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
bin/ci
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`bin/ci` runs RSpec, RuboCop, Reek on `lib`, Flay with mass threshold 150 on `lib`, and Bundler Audit. Do not add exclusions to silence warnings in new code. Fix the design or update a rule only when it reflects an established project convention.
|
|
30
|
+
|
|
31
|
+
Use double-quoted Ruby strings and frozen string literals. Keep changes within the owned surface for the task; runtime API and API specs require their own focused review.
|
|
32
|
+
|
|
33
|
+
## Documentation
|
|
34
|
+
|
|
35
|
+
Update `README.md` for concise user-facing navigation and `docs/api.md` for signatures, return behavior, configuration, and security details. Do not copy upstream WAHA code. Link to the official Apache-2.0 project instead.
|
|
36
|
+
|
|
37
|
+
## Release setup
|
|
38
|
+
|
|
39
|
+
RubyGems trusted publishing requires no long-lived token:
|
|
40
|
+
|
|
41
|
+
1. Create a GitHub environment named `rubygems` with no secrets.
|
|
42
|
+
2. Add a pending trusted publisher for gem `waha-ruby` on RubyGems with owner `develoz-com`, repository `waha-ruby`, workflow `release.yml`, and environment `rubygems`.
|
|
43
|
+
3. Keep the repository's release workflow pinned to the reviewed action SHAs.
|
|
44
|
+
|
|
45
|
+
## Release process
|
|
46
|
+
|
|
47
|
+
1. Bump `Waha::VERSION` in `lib/waha/version.rb` and add user-facing changes under `Unreleased` in `CHANGELOG.md`.
|
|
48
|
+
2. Run `bin/ci` and review the package contents with `gem build waha-ruby.gemspec`.
|
|
49
|
+
3. Merge to `main`.
|
|
50
|
+
4. Publish a stable GitHub Release tagged `vX.Y.Z` at the current `main` commit.
|
|
51
|
+
5. Let `release.yml` validate the tag, build and inspect the exact gem, publish through RubyGems OIDC, verify API visibility/SHA, and finalize `CHANGELOG.md`.
|
|
52
|
+
|
|
53
|
+
Prereleases are not supported. If publication succeeds but changelog finalization fails, rerun the same workflow; do not create another tag or publish a second version.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Copyright (c) 2026 Mauricio Zaffari, Develoz
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
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,63 @@
|
|
|
1
|
+
# waha-ruby
|
|
2
|
+
|
|
3
|
+
Framework-neutral Ruby client for the [WAHA](https://waha.dev/) WhatsApp HTTP API. It provides resource-oriented access to sessions, messages, media, chats, contacts, presence, webhooks, and explicit GOWS helpers, with an opt-in Rails adapter.
|
|
4
|
+
|
|
5
|
+
> `waha-ruby` is an independent community project, not an official WAHA project. WAHA is documented at [waha.dev](https://waha.dev/) and developed at [devlikeapro/waha](https://github.com/devlikeapro/waha). This gem links to the Apache-2.0 upstream project and does not copy its code.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Ruby 4.0.5 or newer
|
|
10
|
+
- WAHA v2025.9 or a compatible WAHA HTTP API deployment
|
|
11
|
+
- An API key when the WAHA deployment requires one
|
|
12
|
+
|
|
13
|
+
The gem is tested against the declared v0.1 API surface. WAHA server versions can add or change response fields; the client returns parsed provider JSON without imposing a domain model.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Add the gem to your application:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
gem "waha-ruby"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Then run `bundle install`. The only runtime dependency is `faraday`.
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
require "waha"
|
|
29
|
+
|
|
30
|
+
client = Waha::Client.new(
|
|
31
|
+
base_url: ENV.fetch("WAHA_BASE_URL"),
|
|
32
|
+
api_key: ENV["WAHA_API_KEY"],
|
|
33
|
+
session: ENV.fetch("WAHA_SESSION", "default")
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
client.sessions.list
|
|
37
|
+
client.messages.send_text(chat_id: "5511999999999@c.us", text: "Olá")
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Every request uses the client's default session unless a resource method accepts an explicit `session:` override. Keep credentials in environment variables or a secret manager; do not commit them.
|
|
41
|
+
|
|
42
|
+
## API guide
|
|
43
|
+
|
|
44
|
+
The complete v0.1 resource map, argument conventions, raw return shapes, file payloads, errors, GOWS helpers, webhook verification, and Rails integration are in [docs/api.md](docs/api.md). For setup and secret handling, see [docs/installation.md](docs/installation.md).
|
|
45
|
+
|
|
46
|
+
## Security
|
|
47
|
+
|
|
48
|
+
The client sends `X-Api-Key` to WAHA and never logs credentials. Error details are bounded and redacted before they are placed in `Waha::Error` messages. Treat raw response hashes as untrusted provider data and redact message text, phone numbers, media URLs, and API keys before logging or persisting them. Webhook verification must run against the exact raw request body before JSON parsing.
|
|
49
|
+
|
|
50
|
+
## Development
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
asdf install
|
|
54
|
+
bundle install
|
|
55
|
+
bundle exec rspec path/to/spec.rb
|
|
56
|
+
bin/ci
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for quality rules and releases. See [CHANGELOG.md](CHANGELOG.md) for changes.
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
MIT. See [LICENSE.txt](LICENSE.txt).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
5
|
+
|
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
7
|
+
|
|
8
|
+
desc "Run RuboCop linter"
|
|
9
|
+
task :rubocop do
|
|
10
|
+
sh "bundle exec rubocop"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
desc "Run CI checks (RSpec, RuboCop, Reek, Flay, and Bundler Audit)"
|
|
14
|
+
task :ci do
|
|
15
|
+
sh "bin/ci"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
task default: :spec
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module Waha
|
|
6
|
+
module Generators
|
|
7
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
def create_initializer
|
|
11
|
+
copy_file "waha.rb", "config/initializers/waha.rb"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Keep WAHA credentials in the application environment. The factory creates a
|
|
4
|
+
# fresh client for each caller, so session-specific state is never shared.
|
|
5
|
+
Rails.application.config.waha.client_factory = lambda do |session: ENV.fetch("WAHA_SESSION")|
|
|
6
|
+
Waha::Client.new(
|
|
7
|
+
base_url: ENV.fetch("WAHA_BASE_URL"),
|
|
8
|
+
api_key: ENV.fetch("WAHA_API_KEY"),
|
|
9
|
+
session: session
|
|
10
|
+
)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Used by Waha::Rails::ControllerConcern#verify_waha_webhook!.
|
|
14
|
+
Rails.application.config.waha.webhook_secret = -> { ENV.fetch("WAHA_WEBHOOK_HMAC_KEY") }
|
data/lib/waha/client.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Waha
|
|
4
|
+
class Client
|
|
5
|
+
RESOURCES = {
|
|
6
|
+
sessions: Resources::Sessions,
|
|
7
|
+
messages: Resources::Messages,
|
|
8
|
+
chats: Resources::Chats,
|
|
9
|
+
contacts: Resources::Contacts,
|
|
10
|
+
presence: Resources::Presence,
|
|
11
|
+
webhooks: Resources::Webhooks,
|
|
12
|
+
media: Resources::Media
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
def initialize(base_url:, api_key:, session: nil, timeout: 30, transport: nil)
|
|
16
|
+
@base_url = base_url
|
|
17
|
+
@session = session
|
|
18
|
+
@transport = transport || Transport::Faraday.new(base_url:, api_key:, timeout:)
|
|
19
|
+
@resources = {}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
RESOURCES.each do |name, resource_class|
|
|
23
|
+
define_method(name) do
|
|
24
|
+
@resources[name] ||= build_resource(name, resource_class)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def build_resource(name, resource_class)
|
|
31
|
+
return resource_class.new(transport: @transport, base_url: @base_url) if name == :media
|
|
32
|
+
return resource_class.new(transport: @transport, session: @session, sessions:) if name == :webhooks
|
|
33
|
+
|
|
34
|
+
resource_class.new(transport: @transport, session: @session)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/waha/error.rb
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Waha
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
MESSAGE_LIMIT = 600
|
|
6
|
+
DETAILS_LIMIT = 450
|
|
7
|
+
|
|
8
|
+
attr_reader :operation, :status, :details
|
|
9
|
+
|
|
10
|
+
def initialize(operation:, status: nil, details: nil)
|
|
11
|
+
@operation = operation.to_s.slice(0, 80)
|
|
12
|
+
@status = status
|
|
13
|
+
@details = sanitize(details)
|
|
14
|
+
super(build_message.slice(0, MESSAGE_LIMIT))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def sanitize(details)
|
|
20
|
+
text = details.to_s
|
|
21
|
+
text = "operation failed" if text.empty?
|
|
22
|
+
text.gsub(%r{data:[^\s,;]+;base64,[A-Za-z0-9+/=]+}i, "[REDACTED]")
|
|
23
|
+
.slice(0, DETAILS_LIMIT)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def build_message
|
|
27
|
+
status_text = status.nil? ? "unknown" : status
|
|
28
|
+
"WAHA #{operation} failed (status #{status_text}): #{details}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class ApiError < Error; end
|
|
33
|
+
class TransportError < Error; end
|
|
34
|
+
class ValidationError < Error; end
|
|
35
|
+
class VerificationError < Error; end
|
|
36
|
+
end
|
data/lib/waha/gows.rb
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Waha
|
|
4
|
+
module Gows
|
|
5
|
+
DIRECT_CHAT_ID_PATTERN = /\A\d+@c\.us\z/
|
|
6
|
+
|
|
7
|
+
module MessageId
|
|
8
|
+
RAW_PATTERN = /\A3EB0[0-9A-F]{18}\z/
|
|
9
|
+
CANONICAL_PATTERN = /\Atrue_[^\s]+_3EB0[0-9A-F]{18}\z/
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def valid_raw?(raw_id)
|
|
14
|
+
raw_id.is_a?(String) && RAW_PATTERN.match?(raw_id)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def canonical(chat_id:, raw_id:)
|
|
18
|
+
unless chat_id.is_a?(String) && !chat_id.empty?
|
|
19
|
+
raise ValidationError.new(operation: "canonical_message_id", details: "chat_id is required")
|
|
20
|
+
end
|
|
21
|
+
unless valid_raw?(raw_id)
|
|
22
|
+
raise ValidationError.new(operation: "canonical_message_id", details: "raw_id must be a valid GOWS id")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
"true_#{chat_id}_#{raw_id}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def valid_canonical?(message_id)
|
|
29
|
+
return false unless message_id.is_a?(String)
|
|
30
|
+
return false unless CANONICAL_PATTERN.match?(message_id)
|
|
31
|
+
|
|
32
|
+
raw_id = message_id.to_s.split("_").last
|
|
33
|
+
valid_raw?(raw_id)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
module_function
|
|
38
|
+
|
|
39
|
+
def valid_direct_chat_id?(chat_id)
|
|
40
|
+
chat_id.is_a?(String) && DIRECT_CHAT_ID_PATTERN.match?(chat_id)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class EditResponseValidator
|
|
44
|
+
TARGET_ID_LENGTH = 22
|
|
45
|
+
|
|
46
|
+
EDIT_MISSING_MESSAGE = "edit protocol response missing"
|
|
47
|
+
TARGET_MISMATCH_MESSAGE = "edit protocol response targeted another message"
|
|
48
|
+
TEXT_MISMATCH_MESSAGE = "edit protocol response text mismatch"
|
|
49
|
+
|
|
50
|
+
def initialize(parsed_response:, message_id:, text:, status: nil)
|
|
51
|
+
@parsed_response = parsed_response
|
|
52
|
+
@message_id = message_id
|
|
53
|
+
@text = text
|
|
54
|
+
@status = status
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def validate!
|
|
58
|
+
protocol_message = extract_protocol_message
|
|
59
|
+
raise_error!(EDIT_MISSING_MESSAGE) unless protocol_message.is_a?(Hash)
|
|
60
|
+
|
|
61
|
+
raise_error!(TARGET_MISMATCH_MESSAGE) unless target_matches?(protocol_message)
|
|
62
|
+
raise_error!(TEXT_MISMATCH_MESSAGE) unless text_matches?(protocol_message)
|
|
63
|
+
|
|
64
|
+
@parsed_response
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def extract_protocol_message
|
|
70
|
+
data = hash_value(@parsed_response, "_data")
|
|
71
|
+
message = hash_value(data, "Message")
|
|
72
|
+
raw_message = hash_value(data, "RawMessage")
|
|
73
|
+
|
|
74
|
+
hash_value(message, "protocolMessage") || hash_value(raw_message, "protocolMessage")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def target_matches?(protocol_message)
|
|
78
|
+
target_id = nested_value(protocol_message, "key", "ID")
|
|
79
|
+
target_id.is_a?(String) &&
|
|
80
|
+
target_id.length == TARGET_ID_LENGTH &&
|
|
81
|
+
@message_id.to_s.end_with?(target_id)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def text_matches?(protocol_message)
|
|
85
|
+
nested_value(protocol_message, "editedMessage", "extendedTextMessage", "text") == @text
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def nested_value(value, *keys)
|
|
89
|
+
keys.reduce(value) { |nested, key| hash_value(nested, key) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def hash_value(value, key)
|
|
93
|
+
return unless value.is_a?(Hash)
|
|
94
|
+
|
|
95
|
+
value[key] || value[key.to_sym]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def raise_error!(details)
|
|
99
|
+
raise ApiError.new(operation: "edit_message", status: @status, details:)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/concern"
|
|
4
|
+
|
|
5
|
+
module Waha
|
|
6
|
+
module Rails
|
|
7
|
+
module ControllerConcern
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def verify_waha_webhook!
|
|
13
|
+
raw_body = request.body.read
|
|
14
|
+
Waha::Webhook.verify!(
|
|
15
|
+
body: raw_body,
|
|
16
|
+
signature: request.headers["X-Webhook-Hmac"],
|
|
17
|
+
algorithm: request.headers["X-Webhook-Hmac-Algorithm"],
|
|
18
|
+
secret: waha_webhook_secret
|
|
19
|
+
)
|
|
20
|
+
ensure
|
|
21
|
+
request.body.rewind
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def waha_webhook_secret
|
|
25
|
+
configured_secret = ::Rails.application.config.waha.webhook_secret
|
|
26
|
+
configured_secret.respond_to?(:call) ? configured_secret.call : configured_secret
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
data/lib/waha/rails.rb
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Waha
|
|
4
|
+
class Resource
|
|
5
|
+
def initialize(transport:, session:)
|
|
6
|
+
@transport = transport
|
|
7
|
+
@session = session
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
attr_reader :transport
|
|
13
|
+
|
|
14
|
+
def session_name(override, operation)
|
|
15
|
+
value = override || @session
|
|
16
|
+
return value unless value.nil? || value.to_s.empty?
|
|
17
|
+
|
|
18
|
+
raise ValidationError.new(operation:, details: "session is required")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def segment(value)
|
|
22
|
+
transport.escape_path_segment(value)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Waha
|
|
4
|
+
module Resources
|
|
5
|
+
class Chats < Resource
|
|
6
|
+
def overview(ids: nil, limit: nil, offset: nil, session: nil, **query)
|
|
7
|
+
name = session_name(session, "chats_overview")
|
|
8
|
+
transport.request(
|
|
9
|
+
method: :get,
|
|
10
|
+
path: "/api/#{segment(name)}/chats/overview",
|
|
11
|
+
operation: "chats_overview",
|
|
12
|
+
expected_status: 200,
|
|
13
|
+
query: Support.query_hash(Support.compact_hash(query.merge(ids:, limit:, offset:)))
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def messages(chat_id:, limit: 100, offset: nil, session: nil, **query)
|
|
18
|
+
name = session_name(session, "chat_messages")
|
|
19
|
+
transport.request(
|
|
20
|
+
method: :get,
|
|
21
|
+
path: "/api/#{segment(name)}/chats/#{segment(chat_id)}/messages",
|
|
22
|
+
operation: "chat_messages",
|
|
23
|
+
expected_status: 200,
|
|
24
|
+
query: Support.query_hash(Support.compact_hash(query.merge(limit:, offset:)))
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Waha
|
|
4
|
+
module Resources
|
|
5
|
+
class Contacts < Resource
|
|
6
|
+
def list(session: nil, **query)
|
|
7
|
+
transport.request(
|
|
8
|
+
method: :get,
|
|
9
|
+
path: "/api/contacts/all",
|
|
10
|
+
operation: "list_contacts",
|
|
11
|
+
expected_status: 200,
|
|
12
|
+
query: Support.compact_hash(query.merge(session: session_name(session, "list_contacts")))
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def get(contact_id:, session: nil)
|
|
17
|
+
transport.request(
|
|
18
|
+
method: :get,
|
|
19
|
+
path: "/api/contacts",
|
|
20
|
+
operation: "get_contact",
|
|
21
|
+
expected_status: 200,
|
|
22
|
+
query: { session: session_name(session, "get_contact"), contactId: contact_id }
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def check_exists(phone:, session: nil)
|
|
27
|
+
transport.request(
|
|
28
|
+
method: :get,
|
|
29
|
+
path: "/api/contacts/check-exists",
|
|
30
|
+
operation: "check_contact_exists",
|
|
31
|
+
expected_status: 200,
|
|
32
|
+
query: { session: session_name(session, "check_contact_exists"), phone: }
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Waha
|
|
6
|
+
module Resources
|
|
7
|
+
class Media < Resource
|
|
8
|
+
DOWNLOAD_TIMEOUT = 60
|
|
9
|
+
|
|
10
|
+
def initialize(transport:, base_url:)
|
|
11
|
+
super(transport:, session: nil)
|
|
12
|
+
@base_url = base_url
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def download(url:)
|
|
16
|
+
transport.request(
|
|
17
|
+
method: :get,
|
|
18
|
+
path: resolved_url(url),
|
|
19
|
+
operation: "download_media",
|
|
20
|
+
expected_status: 200,
|
|
21
|
+
headers: { "Content-Type" => nil },
|
|
22
|
+
timeout: DOWNLOAD_TIMEOUT,
|
|
23
|
+
response: :binary
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def resolved_url(url)
|
|
30
|
+
uri = parse_uri(url)
|
|
31
|
+
|
|
32
|
+
unless uri.absolute?
|
|
33
|
+
raise ValidationError.new(operation: "download_media", details: "media URL must be absolute")
|
|
34
|
+
end
|
|
35
|
+
unless uri.is_a?(URI::HTTP)
|
|
36
|
+
raise ValidationError.new(operation: "download_media", details: "unsupported media URL scheme")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
return uri.to_s unless uri.host == "localhost"
|
|
40
|
+
|
|
41
|
+
rebase_to_base_url(uri)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def rebase_to_base_url(uri)
|
|
45
|
+
base_uri = URI.parse(@base_url)
|
|
46
|
+
uri.scheme = base_uri.scheme
|
|
47
|
+
uri.host = base_uri.host
|
|
48
|
+
uri.port = base_uri.port
|
|
49
|
+
uri.to_s
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def parse_uri(url)
|
|
53
|
+
text = url.to_s
|
|
54
|
+
uri = URI.parse(text)
|
|
55
|
+
raise URI::InvalidURIError if text.include?("//") && uri.host.to_s.empty?
|
|
56
|
+
|
|
57
|
+
uri
|
|
58
|
+
rescue URI::InvalidURIError
|
|
59
|
+
raise ValidationError.new(operation: "download_media", details: "invalid media URL")
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|