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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d3564fee7248f47cf5a37285ef65601563ad281f509ac7e62a7a990219d81a87
4
- data.tar.gz: 1edc94f66291c7d36116fb37af9d7546d3d275993e56ecdb49bac5b9afc1775e
3
+ metadata.gz: 1de5c96a15261fbdc6a099b482fff88814b390a1d268e36b3fd0d1c50e518fb4
4
+ data.tar.gz: a78552343670e2581bfbb91dee27832f15afe8e5643a41edf26b04fa3ba26c8a
5
5
  SHA512:
6
- metadata.gz: 7b1f6ee12c630043c864c2a60e0684e388e50e2cbb53ce868377e6fb5254ce93fc2927dec99efb3841a960316a2924e8a47348d6dee1909f746098065973c3d3
7
- data.tar.gz: 168d00e2d6bb2779f8e32a47224bd390a08e70cf13faa05eefcb44f8748aff2deb9e96b19e2a415bff468e3722122348537daae1d0a3e575252133fe781b913b
6
+ metadata.gz: a148220f6acd6dc59d030e43eb7ff52de63d99d377f21b799d00af7c1f416c57633618392cdc418f12d427d4811846c9efc0d06bb36215e5d5dca54595ad08cd
7
+ data.tar.gz: f508ead3c9681cd3f5787a7835cf33280c7534cf7f0d194bae36912f9ec421df831d0daa57556c95272ad912817924799a9ccf83cb9eed97bddc4ead1bf54b19
data/CHANGELOG.md CHANGED
@@ -1,11 +1,63 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.0 (2026-03-09)
4
-
5
- - Initial release
6
- - WhatsApp Cloud API support (send text, buttons, image; parse webhooks)
7
- - Telegram Bot API support (send text, buttons, image; parse webhooks)
8
- - Unified Event model for normalized inbound messages
9
- - Webhook signature verification (WhatsApp HMAC-SHA256)
10
- - HTTP client with retry and timeout support
11
- - Configuration DSL
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 Telegram with a consistent API.
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
- # WhatsApp
17
- wa = ConnectorRuby::Channels::WhatsApp.new(
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: "+1234567890", text: "Hello!")
34
+ wa.send_text(to: "+6281234567890", text: "Hello!")
22
35
 
23
- # Telegram
24
- tg = ConnectorRuby::Channels::Telegram.new(bot_token: ENV["TELEGRAM_TOKEN"])
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
- # Webhook verification
28
- verifier = ConnectorRuby::WebhookVerifier.new(secret_token: "secret")
29
- verifier.verify!(request_body, signature_header)
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
- ## Features
182
+ ## Changelog
33
183
 
34
- - WhatsApp Business API (text, buttons, images)
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
 
@@ -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 more."
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,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ConnectorRuby
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
@@ -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
- def self.secure_compare(a, b)
19
- a = a.to_s.downcase
20
- b = b.to_s.downcase
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)
@@ -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.2.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, and more.
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