connector-ruby 0.2.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +61 -9
- data/README.md +164 -19
- data/connector-ruby.gemspec +5 -1
- data/lib/connector_ruby/channels/livechat.rb +88 -0
- data/lib/connector_ruby/configuration.rb +3 -0
- data/lib/connector_ruby/version.rb +1 -1
- data/lib/connector_ruby/webhook_verifier.rb +97 -3
- data/lib/connector_ruby.rb +2 -0
- metadata +18 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1de5c96a15261fbdc6a099b482fff88814b390a1d268e36b3fd0d1c50e518fb4
|
|
4
|
+
data.tar.gz: a78552343670e2581bfbb91dee27832f15afe8e5643a41edf26b04fa3ba26c8a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a148220f6acd6dc59d030e43eb7ff52de63d99d377f21b799d00af7c1f416c57633618392cdc418f12d427d4811846c9efc0d06bb36215e5d5dca54595ad08cd
|
|
7
|
+
data.tar.gz: f508ead3c9681cd3f5787a7835cf33280c7534cf7f0d194bae36912f9ec421df831d0daa57556c95272ad912817924799a9ccf83cb9eed97bddc4ead1bf54b19
|
data/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,63 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
5
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.3.0] - 2026-04-11
|
|
8
|
+
|
|
9
|
+
Reshape release — the original v0.3.0 Rails-integration plan was redistributed
|
|
10
|
+
to v0.4.0 (Rails integration) and v0.5.0 (channel expansion + advanced features)
|
|
11
|
+
to unblock the downstream omnibot Phase 2.4 channel rollout, which needed
|
|
12
|
+
webhook verification for all shipped channels plus a LiveChat transport.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- `WebhookVerifier.verify_messenger(payload, signature:, app_secret:)` — HMAC-SHA256 signature verification for Meta Messenger webhooks (`X-Hub-Signature-256` header).
|
|
16
|
+
- `WebhookVerifier.verify_line(payload, signature:, channel_secret:)` — Base64 HMAC-SHA256 signature verification for LINE Messaging API webhooks (`X-Line-Signature` header), with case-sensitive comparison.
|
|
17
|
+
- `WebhookVerifier.verify_slack(payload, timestamp:, signature:, signing_secret:, tolerance: 300)` — Slack v0 signature verification with replay protection. Rejects requests whose `X-Slack-Request-Timestamp` is outside the tolerance window (default 5 minutes).
|
|
18
|
+
- `WebhookVerifier.verify_livechat(payload, expected_secret:)` — shared-secret verification for LiveChat webhooks. LiveChat does not use HMAC; it embeds a `secret_key` field in the webhook body.
|
|
19
|
+
- `Channels::LiveChat` — minimal LiveChat (livechatinc.com / text.com) Agent API support: `send_text` + `parse_webhook`. Basic auth with a PAT (`base64(account_id:region:pat_value)`) and optional `X-Region` header.
|
|
20
|
+
- `Configuration#livechat_pat` and `Configuration#livechat_region`.
|
|
21
|
+
- `WebhookVerifier.secure_compare` now accepts `case_sensitive:` keyword (default `false` for hex signatures; pass `true` for Base64 or shared-secret comparisons).
|
|
22
|
+
- Runtime dependency on the `base64` gem so `verify_line` works on Ruby 3.4+ (where `base64` was removed from default gems).
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- README rewritten to cover all 6 channels with per-channel webhook verification examples.
|
|
26
|
+
|
|
27
|
+
### Notes
|
|
28
|
+
- Rails integration work (Railtie, mountable WebhookController, install generator, ActiveJob integration, omnibot helper) has been moved to milestone v0.4.0.
|
|
29
|
+
- Instagram, Discord, Email, conversation state, multi-tenancy, outbox pattern, signature rotation, and rich message adapters have been moved to milestone v0.5.0.
|
|
30
|
+
|
|
31
|
+
## [0.2.0] - 2026-03-17
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
- **Channels:** Facebook Messenger (`Channels::Messenger`), LINE Messaging API (`Channels::Line`), Slack Web API (`Channels::Slack`). Each supports `send_text`, `send_buttons`, `send_image`, and `parse_webhook`, plus channel-specific rich types (Messenger quick replies, LINE flex, Slack blocks).
|
|
35
|
+
- **WhatsApp rich message types:** templates, documents, location, contacts, reactions, interactive lists.
|
|
36
|
+
- **Message builder DSL:** fluent API for constructing outbound messages.
|
|
37
|
+
- **Batch sending:** `BatchSender` with rate limiting.
|
|
38
|
+
- **Delivery tracking:** correlate sent message IDs with status webhooks.
|
|
39
|
+
- **Typing indicators** and `mark_as_read` where the platform supports them.
|
|
40
|
+
|
|
41
|
+
## [0.1.1] - 2026-03-09
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
- Telegram webhook verification now enforces the `X-Telegram-Bot-Api-Secret-Token` header set via `setWebhook`.
|
|
45
|
+
- `parse_webhook` no longer crashes on malformed payloads with missing nested keys; nil guards throughout.
|
|
46
|
+
- WhatsApp signature comparison handles differing byte lengths gracefully.
|
|
47
|
+
- HTTP retry now applies exponential backoff for 429 responses (previously `RateLimitError` was raised but never retried).
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
- Input validation: reject empty `to`/`chat_id`, enforce WhatsApp/Telegram 4096-char text limits.
|
|
51
|
+
- Phone number normalization for WhatsApp (strip spaces, enforce `+` prefix).
|
|
52
|
+
- Raw response access on `ApiError` (`error.response`).
|
|
53
|
+
- Logging hooks: `on_request`, `on_response`, `on_error` callbacks in `Configuration`.
|
|
54
|
+
|
|
55
|
+
## [0.1.0] - 2026-03-09
|
|
56
|
+
|
|
57
|
+
- Initial release.
|
|
58
|
+
- WhatsApp Cloud API support (send text, buttons, image; parse webhooks).
|
|
59
|
+
- Telegram Bot API support (send text, buttons, image; parse webhooks).
|
|
60
|
+
- Unified `Event` model for normalized inbound messages.
|
|
61
|
+
- Webhook signature verification (WhatsApp HMAC-SHA256).
|
|
62
|
+
- HTTP client with retry and timeout support.
|
|
63
|
+
- Configuration DSL.
|
data/README.md
CHANGED
|
@@ -1,42 +1,187 @@
|
|
|
1
1
|
# connector-ruby
|
|
2
2
|
|
|
3
|
-
Unified channel messaging SDK for Ruby. Send and receive messages across WhatsApp and
|
|
3
|
+
Unified channel messaging SDK for Ruby. Send and receive messages across **WhatsApp, Telegram, Facebook Messenger, LINE, Slack, and LiveChat** with a consistent API, normalized webhook events, and first-class signature verification for every channel.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```ruby
|
|
8
|
-
gem "connector-ruby"
|
|
8
|
+
gem "connector-ruby", "~> 0.3"
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
## Usage
|
|
12
|
-
|
|
13
11
|
```ruby
|
|
14
12
|
require "connector_ruby"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Channel support
|
|
16
|
+
|
|
17
|
+
| Channel | Send text | Send buttons | Send image | Rich messages | Parse webhook | Verify webhook |
|
|
18
|
+
|---|:---:|:---:|:---:|:---:|:---:|:---:|
|
|
19
|
+
| WhatsApp | ✅ | ✅ | ✅ | templates, documents, location, contacts, reactions, lists | ✅ | ✅ |
|
|
20
|
+
| Telegram | ✅ | ✅ | ✅ | documents, location | ✅ | ✅ |
|
|
21
|
+
| Messenger | ✅ | ✅ | ✅ | quick replies | ✅ | ✅ |
|
|
22
|
+
| LINE | ✅ | ✅ | ✅ | flex messages | ✅ | ✅ |
|
|
23
|
+
| Slack | ✅ | ✅ | ✅ | blocks | ✅ | ✅ |
|
|
24
|
+
| LiveChat | ✅ | — | — | *(minimal in v0.3.0)* | ✅ | ✅ |
|
|
15
25
|
|
|
16
|
-
|
|
17
|
-
|
|
26
|
+
## Quickstart
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
# WhatsApp Cloud API
|
|
30
|
+
wa = ConnectorRuby::WhatsApp.new(
|
|
18
31
|
access_token: ENV["WHATSAPP_TOKEN"],
|
|
19
32
|
phone_number_id: ENV["WHATSAPP_PHONE_ID"]
|
|
20
33
|
)
|
|
21
|
-
wa.send_text(to: "+
|
|
34
|
+
wa.send_text(to: "+6281234567890", text: "Hello!")
|
|
22
35
|
|
|
23
|
-
# Telegram
|
|
24
|
-
tg = ConnectorRuby::
|
|
36
|
+
# Telegram Bot API
|
|
37
|
+
tg = ConnectorRuby::Telegram.new(bot_token: ENV["TELEGRAM_BOT_TOKEN"])
|
|
25
38
|
tg.send_text(to: "chat_id", text: "Hello!")
|
|
26
39
|
|
|
27
|
-
#
|
|
28
|
-
|
|
29
|
-
|
|
40
|
+
# Facebook Messenger
|
|
41
|
+
fb = ConnectorRuby::Messenger.new(page_access_token: ENV["MESSENGER_PAGE_TOKEN"])
|
|
42
|
+
fb.send_text(to: "psid", text: "Hello!")
|
|
43
|
+
|
|
44
|
+
# LINE Messaging API
|
|
45
|
+
line = ConnectorRuby::Line.new(channel_access_token: ENV["LINE_CHANNEL_TOKEN"])
|
|
46
|
+
line.send_text(to: "user_id", text: "Hello!")
|
|
47
|
+
|
|
48
|
+
# Slack Web API
|
|
49
|
+
slack = ConnectorRuby::Slack.new(bot_token: ENV["SLACK_BOT_TOKEN"])
|
|
50
|
+
slack.send_text(channel: "C0123456", text: "Hello!")
|
|
51
|
+
|
|
52
|
+
# LiveChat Agent API (Basic auth with a PAT)
|
|
53
|
+
lc = ConnectorRuby::LiveChat.new(
|
|
54
|
+
pat: ENV["LIVECHAT_PAT"], # base64(account_id:region:pat_value)
|
|
55
|
+
region: ENV["LIVECHAT_REGION"] # e.g. "dal" — extracted from the PAT
|
|
56
|
+
)
|
|
57
|
+
lc.send_text(to: "CHAT_abc123", text: "Hello!")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Global configuration
|
|
61
|
+
|
|
62
|
+
Configure credentials once and every channel picks them up automatically:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
ConnectorRuby.configure do |c|
|
|
66
|
+
c.whatsapp_phone_number_id = ENV["WHATSAPP_PHONE_ID"]
|
|
67
|
+
c.whatsapp_access_token = ENV["WHATSAPP_TOKEN"]
|
|
68
|
+
c.telegram_bot_token = ENV["TELEGRAM_BOT_TOKEN"]
|
|
69
|
+
c.messenger_page_access_token = ENV["MESSENGER_PAGE_TOKEN"]
|
|
70
|
+
c.line_channel_access_token = ENV["LINE_CHANNEL_TOKEN"]
|
|
71
|
+
c.slack_bot_token = ENV["SLACK_BOT_TOKEN"]
|
|
72
|
+
c.livechat_pat = ENV["LIVECHAT_PAT"]
|
|
73
|
+
c.livechat_region = ENV["LIVECHAT_REGION"]
|
|
74
|
+
|
|
75
|
+
# HTTP client
|
|
76
|
+
c.http_timeout = 30
|
|
77
|
+
c.http_retries = 3
|
|
78
|
+
c.http_open_timeout = 10
|
|
79
|
+
|
|
80
|
+
# Instrumentation callbacks
|
|
81
|
+
c.on_request = ->(method:, url:, headers:, body:) { Rails.logger.info("[connector] → #{method} #{url}") }
|
|
82
|
+
c.on_response = ->(status:, body:) { Rails.logger.info("[connector] ← #{status}") }
|
|
83
|
+
c.on_error = ->(error:) { Sentry.capture_exception(error) }
|
|
84
|
+
end
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Parsing inbound webhooks
|
|
88
|
+
|
|
89
|
+
Every channel exposes a `parse_webhook` class method that returns a normalized `ConnectorRuby::Event`:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
event = ConnectorRuby::WhatsApp.parse_webhook(request.raw_post)
|
|
93
|
+
# => #<ConnectorRuby::Event type=:message channel=:whatsapp from="+6281..." text="Hi">
|
|
94
|
+
|
|
95
|
+
event.message? # => true
|
|
96
|
+
event.text # => "Hi"
|
|
97
|
+
event.from # => "+6281234567890"
|
|
98
|
+
event.channel # => :whatsapp
|
|
99
|
+
event.metadata # => channel-specific extras
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Same shape for `Telegram`, `Messenger`, `Line`, `Slack`, `LiveChat`.
|
|
103
|
+
|
|
104
|
+
## Webhook verification
|
|
105
|
+
|
|
106
|
+
Each channel has its own verification contract. `WebhookVerifier` has a dedicated method per provider — don't hand-roll HMACs in your controllers.
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
# WhatsApp — X-Hub-Signature-256: sha256=<hex>
|
|
110
|
+
ConnectorRuby::WebhookVerifier.verify_whatsapp(
|
|
111
|
+
request.raw_post,
|
|
112
|
+
signature: request.headers["X-Hub-Signature-256"],
|
|
113
|
+
app_secret: ENV["WHATSAPP_APP_SECRET"]
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Telegram — X-Telegram-Bot-Api-Secret-Token (configured via setWebhook)
|
|
117
|
+
ConnectorRuby::WebhookVerifier.verify_telegram(
|
|
118
|
+
token: ENV["TELEGRAM_BOT_TOKEN"],
|
|
119
|
+
payload: request.raw_post,
|
|
120
|
+
secret_token: ENV["TELEGRAM_WEBHOOK_SECRET"],
|
|
121
|
+
header_value: request.headers["X-Telegram-Bot-Api-Secret-Token"]
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Messenger — X-Hub-Signature-256: sha256=<hex>
|
|
125
|
+
ConnectorRuby::WebhookVerifier.verify_messenger(
|
|
126
|
+
request.raw_post,
|
|
127
|
+
signature: request.headers["X-Hub-Signature-256"],
|
|
128
|
+
app_secret: ENV["FB_APP_SECRET"]
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# LINE — X-Line-Signature: <base64 HMAC-SHA256>
|
|
132
|
+
ConnectorRuby::WebhookVerifier.verify_line(
|
|
133
|
+
request.raw_post,
|
|
134
|
+
signature: request.headers["X-Line-Signature"],
|
|
135
|
+
channel_secret: ENV["LINE_CHANNEL_SECRET"]
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Slack — X-Slack-Signature + X-Slack-Request-Timestamp (with replay protection)
|
|
139
|
+
ConnectorRuby::WebhookVerifier.verify_slack(
|
|
140
|
+
request.raw_post,
|
|
141
|
+
timestamp: request.headers["X-Slack-Request-Timestamp"],
|
|
142
|
+
signature: request.headers["X-Slack-Signature"],
|
|
143
|
+
signing_secret: ENV["SLACK_SIGNING_SECRET"]
|
|
144
|
+
# tolerance: 300 # default; override if your clock skew demands it
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# LiveChat — shared secret_key embedded IN the JSON body (no header, no HMAC)
|
|
148
|
+
ConnectorRuby::WebhookVerifier.verify_livechat(
|
|
149
|
+
request.raw_post,
|
|
150
|
+
expected_secret: ENV["LIVECHAT_WEBHOOK_SECRET"]
|
|
151
|
+
)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
All verifiers return `true`/`false` and use a constant-time comparison internally. Slack verification additionally rejects timestamps outside a 5-minute tolerance window (configurable via `tolerance:`) to prevent replay attacks.
|
|
155
|
+
|
|
156
|
+
> **Why LiveChat is different:** LiveChat does not sign webhooks with HMAC. Every webhook body contains a `secret_key` field, and verification is a constant-time compare between that field and the shared secret configured in your LiveChat webhook settings. See `WebhookVerifier.verify_livechat` for the production-tested implementation.
|
|
157
|
+
|
|
158
|
+
## Additional features (v0.2.0)
|
|
159
|
+
|
|
160
|
+
- **Message builder DSL** — fluent API: `ConnectorRuby::Message.to("+62...").text("Hi").buttons([...]).send_via(wa)`
|
|
161
|
+
- **Batch sending** — `BatchSender` with rate limiting for bulk outbound
|
|
162
|
+
- **Delivery tracking** — correlate sent message IDs with status webhooks
|
|
163
|
+
- **Rich WhatsApp types** — templates, documents, location, contacts, reactions, interactive lists
|
|
164
|
+
- **Typing indicators** — `send_typing` / `mark_as_read` where supported
|
|
165
|
+
|
|
166
|
+
## Error handling
|
|
167
|
+
|
|
168
|
+
All API failures raise `ConnectorRuby::ApiError` (or its subclasses `AuthenticationError`, `RateLimitError`). Rate limits are automatically retried with exponential backoff; after `http_retries` attempts the error propagates so you can observe and alert.
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
begin
|
|
172
|
+
wa.send_text(to: "+6281...", text: "Hi")
|
|
173
|
+
rescue ConnectorRuby::AuthenticationError => e
|
|
174
|
+
# 401 — bad token
|
|
175
|
+
rescue ConnectorRuby::RateLimitError => e
|
|
176
|
+
# 429 after retries exhausted
|
|
177
|
+
rescue ConnectorRuby::ApiError => e
|
|
178
|
+
# other 4xx/5xx; inspect e.status, e.body, e.response
|
|
179
|
+
end
|
|
30
180
|
```
|
|
31
181
|
|
|
32
|
-
##
|
|
182
|
+
## Changelog
|
|
33
183
|
|
|
34
|
-
|
|
35
|
-
- Telegram Bot API (text, callbacks)
|
|
36
|
-
- HMAC-SHA256 webhook verification
|
|
37
|
-
- HTTP retry with exponential backoff for 429/5xx
|
|
38
|
-
- Input validation and error handling
|
|
39
|
-
- Logging hooks (on_request, on_response, on_error)
|
|
184
|
+
See [CHANGELOG.md](CHANGELOG.md) for release history.
|
|
40
185
|
|
|
41
186
|
## License
|
|
42
187
|
|
data/connector-ruby.gemspec
CHANGED
|
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
|
|
|
8
8
|
spec.authors = ["Johannes Dwi Cahyo"]
|
|
9
9
|
spec.email = ["johannes@example.com"]
|
|
10
10
|
spec.summary = "Unified channel messaging SDK for Ruby"
|
|
11
|
-
spec.description = "Framework-agnostic SDK for sending/receiving messages across chat platforms. Supports WhatsApp Cloud API, Telegram Bot API, and
|
|
11
|
+
spec.description = "Framework-agnostic SDK for sending/receiving messages across chat platforms. Supports WhatsApp Cloud API, Telegram Bot API, Facebook Messenger, LINE Messaging API, Slack Web API, and LiveChat Agent API."
|
|
12
12
|
spec.homepage = "https://github.com/johannesdwicahyo/connector-ruby"
|
|
13
13
|
spec.license = "MIT"
|
|
14
14
|
spec.required_ruby_version = ">= 3.0.0"
|
|
@@ -27,6 +27,10 @@ Gem::Specification.new do |spec|
|
|
|
27
27
|
]
|
|
28
28
|
spec.require_paths = ["lib"]
|
|
29
29
|
|
|
30
|
+
# base64 was removed from Ruby's default gems in 3.4; add as a runtime
|
|
31
|
+
# dependency so verify_line can Base64-encode HMAC digests on any Ruby.
|
|
32
|
+
spec.add_dependency "base64", "~> 0.2"
|
|
33
|
+
|
|
30
34
|
spec.add_development_dependency "minitest", "~> 5.0"
|
|
31
35
|
spec.add_development_dependency "rake", "~> 13.0"
|
|
32
36
|
spec.add_development_dependency "webmock", "~> 3.0"
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module ConnectorRuby
|
|
6
|
+
module Channels
|
|
7
|
+
# LiveChat (livechatinc.com / text.com) Agent API channel.
|
|
8
|
+
#
|
|
9
|
+
# Minimal v0.3.0 surface: send_text + parse_webhook. Buttons, images,
|
|
10
|
+
# and rich events are intentionally deferred to a later release.
|
|
11
|
+
#
|
|
12
|
+
# Authentication uses a Personal Access Token (PAT) with Basic auth.
|
|
13
|
+
# The PAT is typically stored as `base64(account_id:region:pat_value)`,
|
|
14
|
+
# and an `X-Region` header should be sent alongside it. Pass the
|
|
15
|
+
# pre-encoded PAT as `pat:` and the region string as `region:`.
|
|
16
|
+
#
|
|
17
|
+
# Webhook verification is handled separately by
|
|
18
|
+
# `ConnectorRuby::WebhookVerifier.verify_livechat`, because LiveChat
|
|
19
|
+
# authenticates webhooks via a `secret_key` field in the JSON body
|
|
20
|
+
# rather than via HMAC signatures.
|
|
21
|
+
class LiveChat < Base
|
|
22
|
+
BASE_URL = "https://api.livechatinc.com/v3.5/agent/action"
|
|
23
|
+
|
|
24
|
+
def initialize(pat: nil, region: nil)
|
|
25
|
+
@pat = pat || ConnectorRuby.configuration.livechat_pat
|
|
26
|
+
@region = region || ConnectorRuby.configuration.livechat_region
|
|
27
|
+
raise ConfigurationError, "LiveChat pat is required" unless @pat
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def send_text(to:, text:)
|
|
31
|
+
validate_send!(to: to, text: text)
|
|
32
|
+
payload = {
|
|
33
|
+
chat_id: to,
|
|
34
|
+
event: { type: "message", text: text }
|
|
35
|
+
}
|
|
36
|
+
http_client.post("#{BASE_URL}/send_event", body: payload, headers: auth_headers)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.parse_webhook(body)
|
|
40
|
+
data = body.is_a?(String) ? JSON.parse(body) : body
|
|
41
|
+
return nil unless data.is_a?(Hash)
|
|
42
|
+
return nil unless data["action"] == "incoming_event"
|
|
43
|
+
|
|
44
|
+
payload = data["payload"]
|
|
45
|
+
return nil unless payload.is_a?(Hash)
|
|
46
|
+
|
|
47
|
+
event = payload["event"]
|
|
48
|
+
return nil unless event.is_a?(Hash) && event["type"] == "message"
|
|
49
|
+
|
|
50
|
+
Event.new(
|
|
51
|
+
type: :message,
|
|
52
|
+
channel: :livechat,
|
|
53
|
+
from: event["author_id"],
|
|
54
|
+
text: event["text"],
|
|
55
|
+
timestamp: parse_timestamp(event["created_at"]),
|
|
56
|
+
message_id: event["id"],
|
|
57
|
+
metadata: {
|
|
58
|
+
chat_id: payload["chat_id"],
|
|
59
|
+
thread_id: payload["thread_id"],
|
|
60
|
+
organization_id: data["organization_id"]
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
rescue JSON::ParserError
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.parse_timestamp(value)
|
|
68
|
+
return nil unless value
|
|
69
|
+
Time.parse(value.to_s)
|
|
70
|
+
rescue ArgumentError
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def auth_headers
|
|
77
|
+
headers = { "Authorization" => "Basic #{@pat}" }
|
|
78
|
+
headers["X-Region"] = @region if @region
|
|
79
|
+
headers
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def validate_send!(to:, text:)
|
|
83
|
+
raise ConnectorRuby::Error, "Recipient 'to' cannot be nil or empty" if to.nil? || to.to_s.strip.empty?
|
|
84
|
+
raise ConnectorRuby::Error, "Text cannot be nil or empty" if text.nil? || text.to_s.strip.empty?
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -7,6 +7,7 @@ module ConnectorRuby
|
|
|
7
7
|
:messenger_page_access_token,
|
|
8
8
|
:line_channel_access_token,
|
|
9
9
|
:slack_bot_token,
|
|
10
|
+
:livechat_pat, :livechat_region,
|
|
10
11
|
:http_timeout, :http_retries, :http_open_timeout,
|
|
11
12
|
:on_request, :on_response, :on_error
|
|
12
13
|
|
|
@@ -17,6 +18,8 @@ module ConnectorRuby
|
|
|
17
18
|
@messenger_page_access_token = nil
|
|
18
19
|
@line_channel_access_token = nil
|
|
19
20
|
@slack_bot_token = nil
|
|
21
|
+
@livechat_pat = nil
|
|
22
|
+
@livechat_region = nil
|
|
20
23
|
@http_timeout = 30
|
|
21
24
|
@http_retries = 3
|
|
22
25
|
@http_open_timeout = 10
|
|
@@ -1,23 +1,117 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "openssl"
|
|
4
|
+
require "base64"
|
|
5
|
+
require "json"
|
|
4
6
|
|
|
5
7
|
module ConnectorRuby
|
|
6
8
|
class WebhookVerifier
|
|
9
|
+
# Verify a WhatsApp Cloud API webhook signature.
|
|
10
|
+
#
|
|
11
|
+
# WhatsApp sends the signature in the `X-Hub-Signature-256` header as
|
|
12
|
+
# `sha256=<hex>`, computed with HMAC-SHA256 over the raw request body
|
|
13
|
+
# using the app secret as the key.
|
|
7
14
|
def self.verify_whatsapp(payload, signature:, app_secret:)
|
|
8
15
|
expected = "sha256=#{OpenSSL::HMAC.hexdigest("SHA256", app_secret, payload)}"
|
|
9
16
|
secure_compare(expected, signature)
|
|
10
17
|
end
|
|
11
18
|
|
|
19
|
+
# Verify a Telegram webhook using the `X-Telegram-Bot-Api-Secret-Token`
|
|
20
|
+
# header configured via `setWebhook`.
|
|
12
21
|
def self.verify_telegram(token:, payload:, secret_token: nil, header_value: nil)
|
|
13
22
|
return false unless secret_token && header_value
|
|
14
23
|
computed = OpenSSL::HMAC.hexdigest("SHA256", secret_token, payload.to_s)
|
|
15
24
|
secure_compare(computed, header_value.to_s)
|
|
16
25
|
end
|
|
17
26
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
27
|
+
# Verify a Meta Messenger webhook signature.
|
|
28
|
+
#
|
|
29
|
+
# Messenger sends the signature in the `X-Hub-Signature-256` header as
|
|
30
|
+
# `sha256=<hex>`, computed with HMAC-SHA256 over the raw request body
|
|
31
|
+
# using the Facebook app secret as the key.
|
|
32
|
+
#
|
|
33
|
+
# Reference: https://developers.facebook.com/docs/messenger-platform/webhooks#security
|
|
34
|
+
def self.verify_messenger(payload, signature:, app_secret:)
|
|
35
|
+
return false if signature.nil? || app_secret.nil?
|
|
36
|
+
expected = "sha256=#{OpenSSL::HMAC.hexdigest("SHA256", app_secret, payload.to_s)}"
|
|
37
|
+
secure_compare(expected, signature)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Verify a LINE Messaging API webhook signature.
|
|
41
|
+
#
|
|
42
|
+
# LINE sends the signature in the `X-Line-Signature` header as the
|
|
43
|
+
# Base64-encoded HMAC-SHA256 digest of the raw request body, using the
|
|
44
|
+
# channel secret as the key.
|
|
45
|
+
#
|
|
46
|
+
# Because the signature is Base64 (which contains uppercase letters),
|
|
47
|
+
# the comparison is case-sensitive.
|
|
48
|
+
#
|
|
49
|
+
# Reference: https://developers.line.biz/en/reference/messaging-api/#signature-validation
|
|
50
|
+
def self.verify_line(payload, signature:, channel_secret:)
|
|
51
|
+
return false if signature.nil? || channel_secret.nil?
|
|
52
|
+
digest = OpenSSL::HMAC.digest("SHA256", channel_secret, payload.to_s)
|
|
53
|
+
expected = Base64.strict_encode64(digest)
|
|
54
|
+
secure_compare(expected, signature, case_sensitive: true)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Verify a Slack webhook signature with replay protection.
|
|
58
|
+
#
|
|
59
|
+
# Slack sends the signature in `X-Slack-Signature` as `v0=<hex>`, computed
|
|
60
|
+
# as HMAC-SHA256 over the base string `v0:{timestamp}:{body}` using the
|
|
61
|
+
# signing secret as the key. The `X-Slack-Request-Timestamp` value must
|
|
62
|
+
# also be checked for freshness to prevent replay attacks.
|
|
63
|
+
#
|
|
64
|
+
# @param tolerance [Integer] max allowed delta between the timestamp and
|
|
65
|
+
# the current time, in seconds (default 300 = 5 minutes, matching
|
|
66
|
+
# Slack's own recommendation).
|
|
67
|
+
#
|
|
68
|
+
# Reference: https://api.slack.com/authentication/verifying-requests-from-slack
|
|
69
|
+
def self.verify_slack(payload, timestamp:, signature:, signing_secret:, tolerance: 300)
|
|
70
|
+
return false if timestamp.nil? || signature.nil? || signing_secret.nil?
|
|
71
|
+
ts = timestamp.to_i
|
|
72
|
+
return false if ts.zero?
|
|
73
|
+
return false if (Time.now.to_i - ts).abs > tolerance
|
|
74
|
+
|
|
75
|
+
basestring = "v0:#{ts}:#{payload}"
|
|
76
|
+
expected = "v0=#{OpenSSL::HMAC.hexdigest("SHA256", signing_secret, basestring)}"
|
|
77
|
+
secure_compare(expected, signature)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Verify a LiveChat webhook using the shared secret embedded in the body.
|
|
81
|
+
#
|
|
82
|
+
# LiveChat does NOT sign webhooks with HMAC and does NOT use a signature
|
|
83
|
+
# header. Instead, every webhook body contains a `secret_key` field;
|
|
84
|
+
# verification is a constant-time compare between that field and the
|
|
85
|
+
# shared secret you configured in your LiveChat webhook settings.
|
|
86
|
+
#
|
|
87
|
+
# Reference: https://platform.text.com/docs/messaging/webhooks
|
|
88
|
+
# and (production reference) chatbotlic's webhooks_controller.rb
|
|
89
|
+
def self.verify_livechat(payload, expected_secret:)
|
|
90
|
+
return false if expected_secret.nil?
|
|
91
|
+
|
|
92
|
+
data = payload.is_a?(String) ? JSON.parse(payload) : payload
|
|
93
|
+
return false unless data.is_a?(Hash)
|
|
94
|
+
|
|
95
|
+
received = data["secret_key"]
|
|
96
|
+
return false if received.nil?
|
|
97
|
+
|
|
98
|
+
secure_compare(received.to_s, expected_secret.to_s, case_sensitive: true)
|
|
99
|
+
rescue JSON::ParserError
|
|
100
|
+
false
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Constant-time string comparison.
|
|
104
|
+
#
|
|
105
|
+
# By default, comparisons are case-insensitive (safe for hex signatures).
|
|
106
|
+
# Pass `case_sensitive: true` for Base64 or shared-secret comparisons where
|
|
107
|
+
# case carries meaning.
|
|
108
|
+
def self.secure_compare(a, b, case_sensitive: false)
|
|
109
|
+
a = a.to_s
|
|
110
|
+
b = b.to_s
|
|
111
|
+
unless case_sensitive
|
|
112
|
+
a = a.downcase
|
|
113
|
+
b = b.downcase
|
|
114
|
+
end
|
|
21
115
|
return false unless a.bytesize == b.bytesize
|
|
22
116
|
|
|
23
117
|
OpenSSL.fixed_length_secure_compare(a, b)
|
data/lib/connector_ruby.rb
CHANGED
|
@@ -13,6 +13,7 @@ require_relative "connector_ruby/channels/telegram"
|
|
|
13
13
|
require_relative "connector_ruby/channels/messenger"
|
|
14
14
|
require_relative "connector_ruby/channels/line"
|
|
15
15
|
require_relative "connector_ruby/channels/slack"
|
|
16
|
+
require_relative "connector_ruby/channels/livechat"
|
|
16
17
|
require_relative "connector_ruby/batch_sender"
|
|
17
18
|
require_relative "connector_ruby/delivery_tracker"
|
|
18
19
|
|
|
@@ -37,4 +38,5 @@ module ConnectorRuby
|
|
|
37
38
|
Messenger = Channels::Messenger
|
|
38
39
|
Line = Channels::Line
|
|
39
40
|
Slack = Channels::Slack
|
|
41
|
+
LiveChat = Channels::LiveChat
|
|
40
42
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: connector-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Johannes Dwi Cahyo
|
|
@@ -9,6 +9,20 @@ bindir: bin
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: base64
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.2'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.2'
|
|
12
26
|
- !ruby/object:Gem::Dependency
|
|
13
27
|
name: minitest
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -52,7 +66,8 @@ dependencies:
|
|
|
52
66
|
- !ruby/object:Gem::Version
|
|
53
67
|
version: '3.0'
|
|
54
68
|
description: Framework-agnostic SDK for sending/receiving messages across chat platforms.
|
|
55
|
-
Supports WhatsApp Cloud API, Telegram Bot API,
|
|
69
|
+
Supports WhatsApp Cloud API, Telegram Bot API, Facebook Messenger, LINE Messaging
|
|
70
|
+
API, Slack Web API, and LiveChat Agent API.
|
|
56
71
|
email:
|
|
57
72
|
- johannes@example.com
|
|
58
73
|
executables: []
|
|
@@ -68,6 +83,7 @@ files:
|
|
|
68
83
|
- lib/connector_ruby/batch_sender.rb
|
|
69
84
|
- lib/connector_ruby/channels/base.rb
|
|
70
85
|
- lib/connector_ruby/channels/line.rb
|
|
86
|
+
- lib/connector_ruby/channels/livechat.rb
|
|
71
87
|
- lib/connector_ruby/channels/messenger.rb
|
|
72
88
|
- lib/connector_ruby/channels/slack.rb
|
|
73
89
|
- lib/connector_ruby/channels/telegram.rb
|