connector-ruby 0.1.1 → 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: 6c22469b3849b241124a069729e5b127cb3459d4f9f55458a79b089fc3f33a28
4
- data.tar.gz: 66fbf3cb744d58622775bed0a17840f6635984e9fc6a488ea507da5b2f5fbd6c
3
+ metadata.gz: 1de5c96a15261fbdc6a099b482fff88814b390a1d268e36b3fd0d1c50e518fb4
4
+ data.tar.gz: a78552343670e2581bfbb91dee27832f15afe8e5643a41edf26b04fa3ba26c8a
5
5
  SHA512:
6
- metadata.gz: fef0557ed46edaf27e1c50b2e28e0cde341c8343fe394b96c51d3a46e17c1b62ccd2fbc88974abd41c3b59bf37f4b20349d2cd794ede451c7074107ebdd81824
7
- data.tar.gz: 706e414863cc4f8d556166a6fd60d2a092593ded6bea22c6617768148e2c596e2121bbf6f934df5170680eeaf40d5421805db07348099a854de5466afd841d0e
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,59 +1,188 @@
1
1
  # connector-ruby
2
2
 
3
- Unified channel messaging SDK for Ruby. Framework-agnostic SDK for sending/receiving messages across chat platforms.
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", "~> 0.1"
8
+ gem "connector-ruby", "~> 0.3"
9
9
  ```
10
10
 
11
- ## Supported Channels
11
+ ```ruby
12
+ require "connector_ruby"
13
+ ```
12
14
 
13
- - WhatsApp Cloud API
14
- - Telegram Bot API
15
+ ## Channel support
15
16
 
16
- ## Usage
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)* | ✅ | ✅ |
17
25
 
18
- ### WhatsApp
26
+ ## Quickstart
19
27
 
20
28
  ```ruby
21
- client = ConnectorRuby::WhatsApp.new(
22
- phone_number_id: "...",
23
- access_token: "..."
29
+ # WhatsApp Cloud API
30
+ wa = ConnectorRuby::WhatsApp.new(
31
+ access_token: ENV["WHATSAPP_TOKEN"],
32
+ phone_number_id: ENV["WHATSAPP_PHONE_ID"]
24
33
  )
34
+ wa.send_text(to: "+6281234567890", text: "Hello!")
35
+
36
+ # Telegram Bot API
37
+ tg = ConnectorRuby::Telegram.new(bot_token: ENV["TELEGRAM_BOT_TOKEN"])
38
+ tg.send_text(to: "chat_id", text: "Hello!")
39
+
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
+ ```
25
59
 
26
- client.send_text(to: "+62812...", text: "Hello!")
27
- client.send_buttons(to: "+62812...", body: "Choose:", buttons: [
28
- { id: "opt1", title: "Option 1" },
29
- { id: "opt2", title: "Option 2" }
30
- ])
31
- client.send_image(to: "+62812...", url: "https://...")
60
+ ## Global configuration
32
61
 
33
- event = ConnectorRuby::WhatsApp.parse_webhook(request_body)
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
34
85
  ```
35
86
 
36
- ### Telegram
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.
37
107
 
38
108
  ```ruby
39
- client = ConnectorRuby::Telegram.new(bot_token: "...")
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
+ )
40
146
 
41
- client.send_text(chat_id: 12345, text: "Hello!")
42
- event = ConnectorRuby::Telegram.parse_webhook(request_body)
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
+ )
43
152
  ```
44
153
 
45
- ### Configuration
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.
46
169
 
47
170
  ```ruby
48
- ConnectorRuby.configure do |config|
49
- config.whatsapp_phone_number_id = ENV["WA_PHONE_ID"]
50
- config.whatsapp_access_token = ENV["WA_TOKEN"]
51
- config.telegram_bot_token = ENV["TG_TOKEN"]
52
- config.http_timeout = 30
53
- config.http_retries = 3
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
54
179
  end
55
180
  ```
56
181
 
182
+ ## Changelog
183
+
184
+ See [CHANGELOG.md](CHANGELOG.md) for release history.
185
+
57
186
  ## License
58
187
 
59
188
  MIT
@@ -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,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConnectorRuby
4
+ class BatchSender
5
+ RATE_LIMITS = {
6
+ whatsapp: { messages_per_second: 80 },
7
+ telegram: { messages_per_second: 30 },
8
+ messenger: { messages_per_second: 200 },
9
+ line: { messages_per_second: 100 },
10
+ slack: { messages_per_second: 1 }
11
+ }.freeze
12
+
13
+ def initialize(channel:)
14
+ @channel = channel
15
+ @results = []
16
+ @errors = []
17
+ end
18
+
19
+ def send_batch(messages)
20
+ rate = rate_limit_for(@channel)
21
+ delay = 1.0 / rate
22
+
23
+ messages.each_with_index do |msg, i|
24
+ sleep(delay) if i > 0
25
+ begin
26
+ result = yield msg
27
+ @results << { index: i, status: :sent, result: result }
28
+ rescue => e
29
+ @errors << { index: i, status: :failed, error: e.message }
30
+ end
31
+ end
32
+
33
+ { sent: @results, failed: @errors, total: messages.size }
34
+ end
35
+
36
+ private
37
+
38
+ def rate_limit_for(channel)
39
+ channel_sym = channel.class.name.split("::").last.downcase.to_sym
40
+ config = RATE_LIMITS[channel_sym] || { messages_per_second: 10 }
41
+ config[:messages_per_second]
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConnectorRuby
4
+ module Channels
5
+ class Line < Base
6
+ BASE_URL = "https://api.line.me/v2/bot/message"
7
+
8
+ def initialize(channel_access_token: nil)
9
+ @channel_access_token = channel_access_token || ConnectorRuby.configuration.line_channel_access_token
10
+ raise ConfigurationError, "LINE channel_access_token is required" unless @channel_access_token
11
+ end
12
+
13
+ def send_text(to:, text:)
14
+ validate_send!(to: to, text: text)
15
+ push_message(to, [{ type: "text", text: text }])
16
+ end
17
+
18
+ def send_buttons(to:, text:, buttons:)
19
+ validate_send!(to: to, text: text)
20
+ actions = buttons.map do |btn|
21
+ { type: "postback", label: btn[:title], data: btn[:id] }
22
+ end
23
+
24
+ template = {
25
+ type: "template",
26
+ altText: text,
27
+ template: {
28
+ type: "buttons",
29
+ text: text,
30
+ actions: actions
31
+ }
32
+ }
33
+ push_message(to, [template])
34
+ end
35
+
36
+ def send_image(to:, url:, caption: nil)
37
+ validate_send!(to: to)
38
+ messages = [{ type: "image", originalContentUrl: url, previewImageUrl: url }]
39
+ messages << { type: "text", text: caption } if caption
40
+ push_message(to, messages)
41
+ end
42
+
43
+ def send_flex(to:, alt_text:, contents:)
44
+ validate_send!(to: to)
45
+ flex = {
46
+ type: "flex",
47
+ altText: alt_text,
48
+ contents: contents
49
+ }
50
+ push_message(to, [flex])
51
+ end
52
+
53
+ def self.parse_webhook(body)
54
+ data = body.is_a?(String) ? JSON.parse(body) : body
55
+
56
+ events = data["events"]
57
+ return nil unless events&.any?
58
+
59
+ event_data = events[0]
60
+ case event_data["type"]
61
+ when "message"
62
+ parse_message(event_data)
63
+ when "postback"
64
+ parse_postback(event_data)
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def push_message(to, messages)
71
+ payload = { to: to, messages: messages }
72
+ http_client.post("#{BASE_URL}/push", body: payload, headers: auth_headers)
73
+ end
74
+
75
+ def auth_headers
76
+ { "Authorization" => "Bearer #{@channel_access_token}" }
77
+ end
78
+
79
+ def validate_send!(to:, text: nil)
80
+ raise ConnectorRuby::Error, "Recipient 'to' cannot be nil or empty" if to.nil? || to.to_s.strip.empty?
81
+ if text
82
+ raise ConnectorRuby::Error, "Text cannot be nil or empty" if text.nil? || text.to_s.strip.empty?
83
+ raise ConnectorRuby::Error, "Text exceeds 5000 character limit" if text.length > 5000
84
+ end
85
+ end
86
+
87
+ def self.parse_message(event_data)
88
+ msg = event_data["message"]
89
+ Event.new(
90
+ type: :message,
91
+ channel: :line,
92
+ from: event_data.dig("source", "userId"),
93
+ text: msg["text"],
94
+ timestamp: event_data["timestamp"] ? Time.at(event_data["timestamp"].to_i / 1000) : nil,
95
+ message_id: msg["id"],
96
+ metadata: {
97
+ reply_token: event_data["replyToken"],
98
+ source_type: event_data.dig("source", "type"),
99
+ message_type: msg["type"]
100
+ }
101
+ )
102
+ end
103
+
104
+ def self.parse_postback(event_data)
105
+ Event.new(
106
+ type: :callback,
107
+ channel: :line,
108
+ from: event_data.dig("source", "userId"),
109
+ text: event_data.dig("postback", "data"),
110
+ timestamp: event_data["timestamp"] ? Time.at(event_data["timestamp"].to_i / 1000) : nil,
111
+ metadata: {
112
+ reply_token: event_data["replyToken"],
113
+ source_type: event_data.dig("source", "type")
114
+ }
115
+ )
116
+ end
117
+ end
118
+ end
119
+ end
@@ -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