chat_sdk-whatsapp 0.2.1
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/lib/chat_sdk/whatsapp/adapter.rb +182 -0
- data/lib/chat_sdk/whatsapp/api_client.rb +95 -0
- data/lib/chat_sdk/whatsapp/event_parser.rb +87 -0
- data/lib/chat_sdk/whatsapp/interactive_renderer.rb +114 -0
- data/lib/chat_sdk/whatsapp.rb +25 -0
- metadata +75 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 5a9c659c8408890113cd3a76002744ec05805a84b7f931bc3f5af5d3c8f12b92
|
|
4
|
+
data.tar.gz: f75e6904f0de6b2f29b3a7b069abc773044a31cfb189aa4f92ba9b25a9a1ef62
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 2a38e860754f4f1950c9e1353e46a1ab26bd16ab2e8ef4c18861ec79fc2a4a8d17c9a1e79b8e3a2bd8e6b2d305f74f6c08ebbb1a83c6dabc5274c5ab2c841113
|
|
7
|
+
data.tar.gz: 7b1be833a9084bd3e65b7a9ba27c795b61323115ae7c0457a3ab24bec9a26e95375f6de6d638f3c56def3895362ebee343df067aa555d0f3c87c21dbdf4569e1
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "rack/utils"
|
|
5
|
+
|
|
6
|
+
module ChatSDK
|
|
7
|
+
module WhatsApp
|
|
8
|
+
class Adapter < ChatSDK::Adapter::Base
|
|
9
|
+
capabilities :direct_messages, :file_uploads, :reactions
|
|
10
|
+
|
|
11
|
+
attr_reader :client
|
|
12
|
+
|
|
13
|
+
def initialize(access_token: nil, app_secret: nil, phone_number_id: nil, verify_token: nil)
|
|
14
|
+
@access_token = access_token || ENV["WHATSAPP_ACCESS_TOKEN"]
|
|
15
|
+
@app_secret = app_secret || ENV["WHATSAPP_APP_SECRET"]
|
|
16
|
+
@phone_number_id = phone_number_id || ENV["WHATSAPP_PHONE_NUMBER_ID"]
|
|
17
|
+
@verify_token = verify_token || ENV["WHATSAPP_VERIFY_TOKEN"]
|
|
18
|
+
|
|
19
|
+
raise ChatSDK::ConfigurationError, "WhatsApp access_token required" unless @access_token
|
|
20
|
+
raise ChatSDK::ConfigurationError, "WhatsApp phone_number_id required" unless @phone_number_id
|
|
21
|
+
|
|
22
|
+
@client = ApiClient.new(@access_token, @phone_number_id)
|
|
23
|
+
@renderer = InteractiveRenderer.new
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def name
|
|
27
|
+
:whatsapp
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Inbound
|
|
31
|
+
def verify_request!(rack_request)
|
|
32
|
+
signature = rack_request.get_header("HTTP_X_HUB_SIGNATURE_256")
|
|
33
|
+
|
|
34
|
+
unless signature
|
|
35
|
+
raise ChatSDK::SignatureVerificationError, "Missing WhatsApp signature header"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
body = rack_request.body.read
|
|
39
|
+
rack_request.body.rewind
|
|
40
|
+
|
|
41
|
+
expected = "sha256=#{OpenSSL::HMAC.hexdigest("SHA256", @app_secret, body)}"
|
|
42
|
+
|
|
43
|
+
unless Rack::Utils.secure_compare(signature, expected)
|
|
44
|
+
raise ChatSDK::SignatureVerificationError, "Invalid WhatsApp signature"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
true
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def ack_response(rack_request)
|
|
51
|
+
return nil unless rack_request.get?
|
|
52
|
+
|
|
53
|
+
params = rack_request.params
|
|
54
|
+
mode = params["hub.mode"]
|
|
55
|
+
token = params["hub.verify_token"]
|
|
56
|
+
challenge = params["hub.challenge"]
|
|
57
|
+
|
|
58
|
+
if mode == "subscribe" && token == @verify_token
|
|
59
|
+
[200, {}, [challenge.to_s]]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def parse_events(rack_request)
|
|
64
|
+
body = rack_request.body.read
|
|
65
|
+
rack_request.body.rewind
|
|
66
|
+
|
|
67
|
+
payload = JSON.parse(body)
|
|
68
|
+
EventParser.parse(payload, @phone_number_id)
|
|
69
|
+
rescue JSON::ParserError
|
|
70
|
+
[]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Outbound
|
|
74
|
+
def post_message(channel_id:, message:, thread_id: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
75
|
+
payload = prepare_message_payload(message)
|
|
76
|
+
|
|
77
|
+
result = @client.send_message(to: channel_id, **payload)
|
|
78
|
+
|
|
79
|
+
parse_whatsapp_message(result, channel_id)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def upload_file(channel_id:, io:, filename:, thread_id: nil, comment: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
83
|
+
content_type = detect_content_type(filename)
|
|
84
|
+
media_type = media_type_for(content_type)
|
|
85
|
+
|
|
86
|
+
# Upload media first
|
|
87
|
+
media_result = @client.upload_media(io: io, filename: filename, content_type: content_type)
|
|
88
|
+
media_id = media_result["id"]
|
|
89
|
+
|
|
90
|
+
# Send media message
|
|
91
|
+
media_payload = {caption: comment}.compact
|
|
92
|
+
result = @client.send_message(to: channel_id, type: media_type, **{media_type => media_payload.merge(id: media_id)})
|
|
93
|
+
|
|
94
|
+
parse_whatsapp_message(result, channel_id)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def add_reaction(channel_id:, message_id:, emoji:)
|
|
98
|
+
@client.send_reaction(to: channel_id, message_id: message_id, emoji: emoji)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def remove_reaction(channel_id:, message_id:, emoji: "") # rubocop:disable Lint/UnusedMethodArgument
|
|
102
|
+
@client.send_reaction(to: channel_id, message_id: message_id, emoji: "")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def open_dm(user_id)
|
|
106
|
+
user_id
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def mention(user_id)
|
|
110
|
+
user_id
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def render(postable_message)
|
|
114
|
+
if postable_message.card?
|
|
115
|
+
@renderer.render(postable_message.card)
|
|
116
|
+
else
|
|
117
|
+
postable_message.text
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def prepare_message_payload(message)
|
|
124
|
+
msg = ChatSDK::PostableMessage.from(message)
|
|
125
|
+
|
|
126
|
+
if msg.card?
|
|
127
|
+
rendered = @renderer.render(msg.card)
|
|
128
|
+
if rendered.is_a?(Hash) && rendered[:type] == "interactive"
|
|
129
|
+
{type: "interactive", interactive: rendered[:interactive]}
|
|
130
|
+
else
|
|
131
|
+
{type: "text", text: {body: rendered[:text] || msg.text || msg.card.fallback_text || ""}}
|
|
132
|
+
end
|
|
133
|
+
else
|
|
134
|
+
{type: "text", text: {body: msg.text || ""}}
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def parse_whatsapp_message(data, channel_id)
|
|
139
|
+
message_id = data.dig("messages", 0, "id")
|
|
140
|
+
|
|
141
|
+
ChatSDK::Message.new(
|
|
142
|
+
id: message_id,
|
|
143
|
+
text: "",
|
|
144
|
+
author: ChatSDK::Author.new(
|
|
145
|
+
id: "bot",
|
|
146
|
+
name: "bot",
|
|
147
|
+
platform: :whatsapp,
|
|
148
|
+
bot: true
|
|
149
|
+
),
|
|
150
|
+
thread_id: "whatsapp:#{@phone_number_id}:#{channel_id}",
|
|
151
|
+
channel_id: channel_id,
|
|
152
|
+
platform: :whatsapp,
|
|
153
|
+
raw: data
|
|
154
|
+
)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
CONTENT_TYPES = {
|
|
158
|
+
".jpg" => "image/jpeg", ".jpeg" => "image/jpeg", ".png" => "image/png",
|
|
159
|
+
".gif" => "image/gif", ".webp" => "image/webp",
|
|
160
|
+
".mp4" => "video/mp4", ".3gp" => "video/3gpp",
|
|
161
|
+
".mp3" => "audio/mpeg", ".ogg" => "audio/ogg", ".amr" => "audio/amr",
|
|
162
|
+
".pdf" => "application/pdf", ".doc" => "application/msword",
|
|
163
|
+
".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
164
|
+
".xls" => "application/vnd.ms-excel",
|
|
165
|
+
".xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
166
|
+
}.freeze
|
|
167
|
+
|
|
168
|
+
def detect_content_type(filename)
|
|
169
|
+
CONTENT_TYPES.fetch(File.extname(filename).downcase, "application/octet-stream")
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def media_type_for(content_type)
|
|
173
|
+
case content_type
|
|
174
|
+
when %r{^image/} then "image"
|
|
175
|
+
when %r{^video/} then "video"
|
|
176
|
+
when %r{^audio/} then "audio"
|
|
177
|
+
else "document"
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module WhatsApp
|
|
5
|
+
class ApiClient
|
|
6
|
+
BASE_URL = "https://graph.facebook.com/v21.0"
|
|
7
|
+
|
|
8
|
+
def initialize(access_token, phone_number_id)
|
|
9
|
+
@access_token = access_token
|
|
10
|
+
@phone_number_id = phone_number_id
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def send_message(to:, type:, **payload)
|
|
14
|
+
body = {
|
|
15
|
+
"messaging_product" => "whatsapp",
|
|
16
|
+
"recipient_type" => "individual",
|
|
17
|
+
"to" => to,
|
|
18
|
+
"type" => type
|
|
19
|
+
}.merge(payload)
|
|
20
|
+
|
|
21
|
+
request(:post, "#{@phone_number_id}/messages", body)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def send_reaction(to:, message_id:, emoji:)
|
|
25
|
+
send_message(
|
|
26
|
+
to: to,
|
|
27
|
+
type: "reaction",
|
|
28
|
+
reaction: {message_id: message_id, emoji: emoji}
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def upload_media(io:, filename:, content_type:)
|
|
33
|
+
response = media_connection.post("#{@phone_number_id}/media") do |req|
|
|
34
|
+
req.body = {
|
|
35
|
+
"messaging_product" => "whatsapp",
|
|
36
|
+
"file" => Faraday::Multipart::FilePart.new(io, content_type, filename),
|
|
37
|
+
"type" => content_type
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
handle_response(response)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def connection
|
|
47
|
+
@connection ||= build_connection { |f| f.request :json }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def media_connection
|
|
51
|
+
@media_connection ||= build_connection { |f| f.request :multipart }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def build_connection
|
|
55
|
+
Faraday.new(url: BASE_URL) do |f|
|
|
56
|
+
yield f
|
|
57
|
+
f.response :json
|
|
58
|
+
f.adapter :net_http
|
|
59
|
+
f.headers["Authorization"] = "Bearer #{@access_token}"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def request(method, path, body = nil)
|
|
64
|
+
response = connection.public_send(method, path) do |req|
|
|
65
|
+
req.body = body if body && method != :get
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
handle_response(response)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def handle_response(response)
|
|
72
|
+
body = response.body
|
|
73
|
+
return body.is_a?(Hash) ? body : {} if response.success?
|
|
74
|
+
|
|
75
|
+
if response.status == 429
|
|
76
|
+
raise ChatSDK::RateLimitedError.new(
|
|
77
|
+
"WhatsApp API rate limited",
|
|
78
|
+
retry_after: nil,
|
|
79
|
+
status: response.status,
|
|
80
|
+
body: body,
|
|
81
|
+
adapter_name: :whatsapp
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
error_message = body.is_a?(Hash) ? body.dig("error", "message") : response.status
|
|
86
|
+
raise ChatSDK::PlatformError.new(
|
|
87
|
+
"WhatsApp API error: #{error_message}",
|
|
88
|
+
status: response.status,
|
|
89
|
+
body: body,
|
|
90
|
+
adapter_name: :whatsapp
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module WhatsApp
|
|
5
|
+
class EventParser
|
|
6
|
+
class << self
|
|
7
|
+
def parse(payload, phone_number_id)
|
|
8
|
+
return [] unless payload.is_a?(Hash)
|
|
9
|
+
return [] unless payload["object"] == "whatsapp_business_account"
|
|
10
|
+
|
|
11
|
+
(payload["entry"] || [])
|
|
12
|
+
.flat_map { |entry| entry["changes"] || [] }
|
|
13
|
+
.select { |change| change["field"] == "messages" }
|
|
14
|
+
.flat_map { |change| change.dig("value", "messages") || [] }
|
|
15
|
+
.filter_map { |msg| parse_message(msg, phone_number_id) }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def parse_message(msg, phone_number_id)
|
|
21
|
+
from = msg["from"]&.to_s
|
|
22
|
+
return nil unless from
|
|
23
|
+
|
|
24
|
+
author = ChatSDK::Author.new(id: from, name: from, platform: :whatsapp, bot: false)
|
|
25
|
+
thread_id = "whatsapp:#{phone_number_id}:#{from}"
|
|
26
|
+
|
|
27
|
+
case msg["type"]
|
|
28
|
+
when "text"
|
|
29
|
+
build_direct_message(msg, msg.dig("text", "body") || "", author, from, thread_id)
|
|
30
|
+
when "interactive"
|
|
31
|
+
parse_interactive_message(msg, author, from, thread_id)
|
|
32
|
+
when "image", "document", "audio", "video"
|
|
33
|
+
parse_media_message(msg, author, from, thread_id)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def parse_interactive_message(msg, author, channel_id, thread_id)
|
|
38
|
+
interactive = msg["interactive"] || {}
|
|
39
|
+
action_id = interactive.dig("button_reply", "id") || interactive.dig("list_reply", "id") || ""
|
|
40
|
+
|
|
41
|
+
ChatSDK::Events::Action.new(
|
|
42
|
+
action_id: action_id,
|
|
43
|
+
value: action_id,
|
|
44
|
+
user: author,
|
|
45
|
+
thread_id: thread_id,
|
|
46
|
+
channel_id: channel_id,
|
|
47
|
+
platform: :whatsapp,
|
|
48
|
+
adapter_name: :whatsapp,
|
|
49
|
+
raw: msg
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def parse_media_message(msg, author, channel_id, thread_id)
|
|
54
|
+
media_type = msg["type"]
|
|
55
|
+
media_data = msg[media_type] || {}
|
|
56
|
+
caption = media_data["caption"] || ""
|
|
57
|
+
mime_type = media_data["mime_type"] || ""
|
|
58
|
+
media_id = media_data["id"] || ""
|
|
59
|
+
|
|
60
|
+
text = [caption, "[#{media_type}: #{mime_type} #{media_id}]"].reject(&:empty?).join("\n")
|
|
61
|
+
build_direct_message(msg, text, author, channel_id, thread_id)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def build_direct_message(msg, text, author, channel_id, thread_id)
|
|
65
|
+
message = ChatSDK::Message.new(
|
|
66
|
+
id: msg["id"],
|
|
67
|
+
text: text,
|
|
68
|
+
author: author,
|
|
69
|
+
thread_id: thread_id,
|
|
70
|
+
channel_id: channel_id,
|
|
71
|
+
platform: :whatsapp,
|
|
72
|
+
raw: msg
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
ChatSDK::Events::DirectMessage.new(
|
|
76
|
+
message: message,
|
|
77
|
+
thread_id: thread_id,
|
|
78
|
+
channel_id: channel_id,
|
|
79
|
+
platform: :whatsapp,
|
|
80
|
+
adapter_name: :whatsapp,
|
|
81
|
+
raw: msg
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module WhatsApp
|
|
5
|
+
class InteractiveRenderer
|
|
6
|
+
def render(node)
|
|
7
|
+
case node.type
|
|
8
|
+
when :card then render_card(node)
|
|
9
|
+
else render_single(node)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def render_card(node)
|
|
16
|
+
buttons = collect_reply_buttons(node)
|
|
17
|
+
title = node.attributes[:title]
|
|
18
|
+
subtitle = node.attributes[:subtitle]
|
|
19
|
+
text_parts = collect_text_parts(node)
|
|
20
|
+
body_text = [subtitle, *text_parts].compact.reject(&:empty?).join("\n")
|
|
21
|
+
body_text = title if body_text.empty?
|
|
22
|
+
|
|
23
|
+
if title && buttons.any? && buttons.length <= 3
|
|
24
|
+
interactive = {
|
|
25
|
+
"type" => "button",
|
|
26
|
+
"body" => {"text" => body_text}
|
|
27
|
+
}
|
|
28
|
+
interactive["header"] = {"type" => "text", "text" => truncate(title, 60)} if title
|
|
29
|
+
interactive["action"] = {
|
|
30
|
+
"buttons" => buttons.first(3).map do |btn|
|
|
31
|
+
{
|
|
32
|
+
"type" => "reply",
|
|
33
|
+
"reply" => {
|
|
34
|
+
"id" => btn[:id],
|
|
35
|
+
"title" => truncate(btn[:text], 20)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
{type: "interactive", interactive: interactive}
|
|
42
|
+
else
|
|
43
|
+
# Fallback to plain text
|
|
44
|
+
fallback = [title, subtitle, *text_parts].compact.reject(&:empty?).join("\n")
|
|
45
|
+
if buttons.any?
|
|
46
|
+
button_text = buttons.map { |b| b[:text] }.join(", ")
|
|
47
|
+
fallback = "#{fallback}\n[#{button_text}]" unless button_text.empty?
|
|
48
|
+
end
|
|
49
|
+
{text: fallback}
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def collect_reply_buttons(node)
|
|
54
|
+
buttons = []
|
|
55
|
+
node.children.each do |child|
|
|
56
|
+
case child.type
|
|
57
|
+
when :button
|
|
58
|
+
buttons << {id: child.attributes[:id] || "button", text: child.attributes[:text] || ""}
|
|
59
|
+
when :link_button
|
|
60
|
+
# WhatsApp reply buttons can't have URLs; skip link buttons
|
|
61
|
+
next
|
|
62
|
+
when :actions
|
|
63
|
+
child.children.each do |action_child|
|
|
64
|
+
case action_child.type
|
|
65
|
+
when :button
|
|
66
|
+
buttons << {id: action_child.attributes[:id] || "button", text: action_child.attributes[:text] || ""}
|
|
67
|
+
when :link_button
|
|
68
|
+
next
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
when :section
|
|
72
|
+
buttons.concat(collect_reply_buttons(child))
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
buttons
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def collect_text_parts(node)
|
|
79
|
+
parts = []
|
|
80
|
+
node.children.each do |child|
|
|
81
|
+
case child.type
|
|
82
|
+
when :text
|
|
83
|
+
parts << child.attributes[:content]
|
|
84
|
+
when :divider
|
|
85
|
+
parts << "---"
|
|
86
|
+
when :fields
|
|
87
|
+
child.children.each do |field|
|
|
88
|
+
parts << "#{field.attributes[:label]}: #{field.attributes[:value]}"
|
|
89
|
+
end
|
|
90
|
+
when :section
|
|
91
|
+
parts << child.attributes[:title] if child.attributes[:title]
|
|
92
|
+
parts.concat(collect_text_parts(child))
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
parts
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def render_single(node)
|
|
99
|
+
case node.type
|
|
100
|
+
when :text
|
|
101
|
+
{text: node.attributes[:content]}
|
|
102
|
+
else
|
|
103
|
+
{text: node.fallback_text}
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def truncate(text, max)
|
|
108
|
+
return "" unless text
|
|
109
|
+
|
|
110
|
+
(text.length > max) ? "#{text[0..max - 4]}..." : text
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "chat_sdk"
|
|
4
|
+
require "faraday"
|
|
5
|
+
require "faraday/net_http"
|
|
6
|
+
require "zeitwerk"
|
|
7
|
+
|
|
8
|
+
module ChatSDK
|
|
9
|
+
module WhatsApp
|
|
10
|
+
class << self
|
|
11
|
+
def loader
|
|
12
|
+
@loader ||= begin
|
|
13
|
+
loader = Zeitwerk::Loader.new
|
|
14
|
+
loader.tag = "chat_sdk-whatsapp"
|
|
15
|
+
loader.inflector.inflect("chat_sdk" => "ChatSDK", "whatsapp" => "WhatsApp")
|
|
16
|
+
loader.push_dir("#{__dir__}/whatsapp", namespace: ChatSDK::WhatsApp)
|
|
17
|
+
loader.setup
|
|
18
|
+
loader
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
ChatSDK::WhatsApp.loader
|
metadata
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: chat_sdk-whatsapp
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Quentin Rousseau
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: chat_sdk
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: faraday
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.0'
|
|
40
|
+
description: WhatsApp Business Cloud API adapter for the ChatSDK framework with HMAC-SHA256
|
|
41
|
+
verification and interactive message rendering
|
|
42
|
+
email:
|
|
43
|
+
- quentin@rootly.com
|
|
44
|
+
executables: []
|
|
45
|
+
extensions: []
|
|
46
|
+
extra_rdoc_files: []
|
|
47
|
+
files:
|
|
48
|
+
- lib/chat_sdk/whatsapp.rb
|
|
49
|
+
- lib/chat_sdk/whatsapp/adapter.rb
|
|
50
|
+
- lib/chat_sdk/whatsapp/api_client.rb
|
|
51
|
+
- lib/chat_sdk/whatsapp/event_parser.rb
|
|
52
|
+
- lib/chat_sdk/whatsapp/interactive_renderer.rb
|
|
53
|
+
homepage: https://github.com/rootlyhq/chat-sdk
|
|
54
|
+
licenses:
|
|
55
|
+
- MIT
|
|
56
|
+
metadata:
|
|
57
|
+
rubygems_mfa_required: 'true'
|
|
58
|
+
rdoc_options: []
|
|
59
|
+
require_paths:
|
|
60
|
+
- lib
|
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
62
|
+
requirements:
|
|
63
|
+
- - ">="
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: 3.2.0
|
|
66
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
67
|
+
requirements:
|
|
68
|
+
- - ">="
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: '0'
|
|
71
|
+
requirements: []
|
|
72
|
+
rubygems_version: 4.0.16
|
|
73
|
+
specification_version: 4
|
|
74
|
+
summary: WhatsApp Business Cloud API adapter for ChatSDK
|
|
75
|
+
test_files: []
|