chat_sdk-messenger 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/messenger/adapter.rb +147 -0
- data/lib/chat_sdk/messenger/api_client.rb +76 -0
- data/lib/chat_sdk/messenger/event_parser.rb +105 -0
- data/lib/chat_sdk/messenger/template_renderer.rb +152 -0
- data/lib/chat_sdk/messenger.rb +25 -0
- metadata +75 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: ddb5167e642f3d5d79effdd65467cbfd7f1c4afae486a95204bf64708ff3019b
|
|
4
|
+
data.tar.gz: aa0f9eae474adae2691643fe4783ddff7c25894f2418036df6651642c6afc882
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8fb24bc5b9428c7d1575bd669a397215a465a78cf5f1ba98b13e1f500a04b89eeeed3471326584d612fa109e1417c227ad90e3f118c28596896f50aa12a83f60
|
|
7
|
+
data.tar.gz: f20ddad5394606ea1685b4ba9a70de96db0094d362dde49674a9a2a8f920802cf5d7631017cb952efeced188e72eb6118c1a6c9f2b01e4b877874530e605707f
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "rack/utils"
|
|
5
|
+
|
|
6
|
+
module ChatSDK
|
|
7
|
+
module Messenger
|
|
8
|
+
class Adapter < ChatSDK::Adapter::Base
|
|
9
|
+
capabilities :typing_indicator, :direct_messages, :file_uploads
|
|
10
|
+
|
|
11
|
+
attr_reader :client
|
|
12
|
+
|
|
13
|
+
def initialize(app_secret: nil, page_access_token: nil, verify_token: nil)
|
|
14
|
+
@app_secret = app_secret || ENV["FACEBOOK_APP_SECRET"]
|
|
15
|
+
@page_access_token = page_access_token || ENV["FACEBOOK_PAGE_ACCESS_TOKEN"]
|
|
16
|
+
@verify_token = verify_token || ENV["FACEBOOK_VERIFY_TOKEN"]
|
|
17
|
+
|
|
18
|
+
raise ChatSDK::ConfigurationError, "Messenger app_secret required" unless @app_secret
|
|
19
|
+
raise ChatSDK::ConfigurationError, "Messenger page_access_token required" unless @page_access_token
|
|
20
|
+
|
|
21
|
+
@client = ApiClient.new(@page_access_token)
|
|
22
|
+
@renderer = TemplateRenderer.new
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def name
|
|
26
|
+
:messenger
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Inbound
|
|
30
|
+
def verify_request!(rack_request)
|
|
31
|
+
signature = rack_request.get_header("HTTP_X_HUB_SIGNATURE_256")
|
|
32
|
+
|
|
33
|
+
unless signature
|
|
34
|
+
raise ChatSDK::SignatureVerificationError, "Missing Facebook signature header"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
body = rack_request.body.read
|
|
38
|
+
rack_request.body.rewind
|
|
39
|
+
|
|
40
|
+
expected = "sha256=#{OpenSSL::HMAC.hexdigest("SHA256", @app_secret, body)}"
|
|
41
|
+
|
|
42
|
+
unless Rack::Utils.secure_compare(signature, expected)
|
|
43
|
+
raise ChatSDK::SignatureVerificationError, "Invalid Facebook signature"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def ack_response(rack_request)
|
|
50
|
+
return nil unless rack_request.get?
|
|
51
|
+
|
|
52
|
+
params = rack_request.params
|
|
53
|
+
mode = params["hub.mode"]
|
|
54
|
+
token = params["hub.verify_token"]
|
|
55
|
+
challenge = params["hub.challenge"]
|
|
56
|
+
|
|
57
|
+
if mode == "subscribe" && token == @verify_token
|
|
58
|
+
[200, {}, [challenge.to_s]]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def parse_events(rack_request)
|
|
63
|
+
body = rack_request.body.read
|
|
64
|
+
rack_request.body.rewind
|
|
65
|
+
|
|
66
|
+
payload = JSON.parse(body)
|
|
67
|
+
EventParser.parse(payload)
|
|
68
|
+
rescue JSON::ParserError
|
|
69
|
+
[]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Outbound
|
|
73
|
+
def post_message(channel_id:, message:, thread_id: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
74
|
+
payload = prepare_message_payload(message)
|
|
75
|
+
|
|
76
|
+
result = @client.send_message(
|
|
77
|
+
recipient_id: channel_id,
|
|
78
|
+
message: payload
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
parse_messenger_message(result, channel_id)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def upload_file(channel_id:, io:, filename:, thread_id: nil, comment: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
85
|
+
raise ChatSDK::PlatformError.new(
|
|
86
|
+
"Messenger file uploads require a publicly accessible URL. Binary upload is not supported. " \
|
|
87
|
+
"Host the file at a public URL and send it as an attachment.",
|
|
88
|
+
adapter_name: :messenger
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def open_dm(user_id)
|
|
93
|
+
user_id
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def start_typing(channel_id:, thread_id: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
97
|
+
@client.send_action(recipient_id: channel_id, action: "typing_on")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def mention(user_id)
|
|
101
|
+
user_id
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def render(postable_message)
|
|
105
|
+
if postable_message.card?
|
|
106
|
+
@renderer.render(postable_message.card)
|
|
107
|
+
else
|
|
108
|
+
postable_message.text
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
def prepare_message_payload(message)
|
|
115
|
+
msg = ChatSDK::PostableMessage.from(message)
|
|
116
|
+
|
|
117
|
+
if msg.card?
|
|
118
|
+
rendered = @renderer.render(msg.card)
|
|
119
|
+
if rendered.is_a?(Hash) && rendered[:attachment]
|
|
120
|
+
rendered
|
|
121
|
+
else
|
|
122
|
+
{"text" => rendered[:text] || msg.text || msg.card.fallback_text || ""}
|
|
123
|
+
end
|
|
124
|
+
else
|
|
125
|
+
{"text" => msg.text || ""}
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def parse_messenger_message(data, channel_id)
|
|
130
|
+
ChatSDK::Message.new(
|
|
131
|
+
id: data["message_id"],
|
|
132
|
+
text: "",
|
|
133
|
+
author: ChatSDK::Author.new(
|
|
134
|
+
id: "bot",
|
|
135
|
+
name: "bot",
|
|
136
|
+
platform: :messenger,
|
|
137
|
+
bot: true
|
|
138
|
+
),
|
|
139
|
+
thread_id: "messenger:#{data.dig("recipient_id") || channel_id}",
|
|
140
|
+
channel_id: channel_id,
|
|
141
|
+
platform: :messenger,
|
|
142
|
+
raw: data
|
|
143
|
+
)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Messenger
|
|
5
|
+
class ApiClient
|
|
6
|
+
BASE_URL = "https://graph.facebook.com/v21.0/"
|
|
7
|
+
|
|
8
|
+
def initialize(page_access_token)
|
|
9
|
+
@page_access_token = page_access_token
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def send_message(recipient_id:, message:)
|
|
13
|
+
body = {
|
|
14
|
+
"recipient" => {"id" => recipient_id},
|
|
15
|
+
"message" => message
|
|
16
|
+
}
|
|
17
|
+
request(:post, "me/messages", body)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def send_action(recipient_id:, action:)
|
|
21
|
+
body = {
|
|
22
|
+
"recipient" => {"id" => recipient_id},
|
|
23
|
+
"sender_action" => action
|
|
24
|
+
}
|
|
25
|
+
request(:post, "me/messages", body)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def connection
|
|
31
|
+
@connection ||= Faraday.new(url: BASE_URL) do |f|
|
|
32
|
+
f.request :json
|
|
33
|
+
f.response :json
|
|
34
|
+
f.adapter :net_http
|
|
35
|
+
f.params["access_token"] = @page_access_token
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def request(method, path, body = nil)
|
|
40
|
+
response = connection.public_send(method, path) do |req|
|
|
41
|
+
req.body = body if body && method != :get
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
handle_response(response)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def handle_response(response)
|
|
48
|
+
body = response.body
|
|
49
|
+
|
|
50
|
+
if response.success?
|
|
51
|
+
return body if body.is_a?(Hash)
|
|
52
|
+
|
|
53
|
+
return {}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
if response.status == 429
|
|
57
|
+
raise ChatSDK::RateLimitedError.new(
|
|
58
|
+
"Messenger API rate limited",
|
|
59
|
+
retry_after: nil,
|
|
60
|
+
status: response.status,
|
|
61
|
+
body: body,
|
|
62
|
+
adapter_name: :messenger
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
error_message = body.is_a?(Hash) ? body.dig("error", "message") : response.status
|
|
67
|
+
raise ChatSDK::PlatformError.new(
|
|
68
|
+
"Messenger API error: #{error_message}",
|
|
69
|
+
status: response.status,
|
|
70
|
+
body: body,
|
|
71
|
+
adapter_name: :messenger
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Messenger
|
|
5
|
+
class EventParser
|
|
6
|
+
class << self
|
|
7
|
+
def parse(payload)
|
|
8
|
+
return [] unless payload.is_a?(Hash)
|
|
9
|
+
return [] unless payload["object"] == "page"
|
|
10
|
+
|
|
11
|
+
events = []
|
|
12
|
+
|
|
13
|
+
(payload["entry"] || []).each do |entry|
|
|
14
|
+
(entry["messaging"] || []).each do |messaging|
|
|
15
|
+
event = parse_messaging(messaging)
|
|
16
|
+
events << event if event
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
events
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def parse_messaging(messaging)
|
|
26
|
+
sender_id = messaging.dig("sender", "id")&.to_s
|
|
27
|
+
return nil unless sender_id
|
|
28
|
+
|
|
29
|
+
channel_id = sender_id
|
|
30
|
+
thread_id = "messenger:#{sender_id}"
|
|
31
|
+
|
|
32
|
+
if messaging["postback"]
|
|
33
|
+
parse_postback(messaging, sender_id, channel_id, thread_id)
|
|
34
|
+
elsif messaging["message"]
|
|
35
|
+
parse_message(messaging, sender_id, channel_id, thread_id)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def parse_message(messaging, sender_id, channel_id, thread_id)
|
|
40
|
+
message_data = messaging["message"]
|
|
41
|
+
text = message_data["text"] || ""
|
|
42
|
+
|
|
43
|
+
attachments = extract_attachments(message_data["attachments"])
|
|
44
|
+
|
|
45
|
+
if !attachments.empty?
|
|
46
|
+
text = [text, *attachments.map { |a| a[:url] }].reject(&:empty?).join("\n")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
msg = ChatSDK::Message.new(
|
|
50
|
+
id: message_data["mid"],
|
|
51
|
+
text: text,
|
|
52
|
+
author: ChatSDK::Author.new(id: sender_id, name: sender_id, platform: :messenger, bot: false),
|
|
53
|
+
thread_id: thread_id,
|
|
54
|
+
channel_id: channel_id,
|
|
55
|
+
platform: :messenger,
|
|
56
|
+
raw: messaging
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
ChatSDK::Events::DirectMessage.new(
|
|
60
|
+
message: msg,
|
|
61
|
+
thread_id: thread_id,
|
|
62
|
+
channel_id: channel_id,
|
|
63
|
+
platform: :messenger,
|
|
64
|
+
adapter_name: :messenger,
|
|
65
|
+
raw: messaging
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def parse_postback(messaging, sender_id, channel_id, thread_id)
|
|
70
|
+
postback = messaging["postback"]
|
|
71
|
+
payload = postback["payload"] || ""
|
|
72
|
+
|
|
73
|
+
user = ChatSDK::Author.new(
|
|
74
|
+
id: sender_id,
|
|
75
|
+
name: sender_id,
|
|
76
|
+
platform: :messenger,
|
|
77
|
+
bot: false
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
ChatSDK::Events::Action.new(
|
|
81
|
+
action_id: payload,
|
|
82
|
+
value: payload,
|
|
83
|
+
user: user,
|
|
84
|
+
thread_id: thread_id,
|
|
85
|
+
channel_id: channel_id,
|
|
86
|
+
platform: :messenger,
|
|
87
|
+
adapter_name: :messenger,
|
|
88
|
+
raw: messaging
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def extract_attachments(attachments)
|
|
93
|
+
return [] unless attachments.is_a?(Array)
|
|
94
|
+
|
|
95
|
+
attachments.filter_map do |attachment|
|
|
96
|
+
url = attachment.dig("payload", "url")
|
|
97
|
+
next unless url
|
|
98
|
+
|
|
99
|
+
{type: attachment["type"], url: url}
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Messenger
|
|
5
|
+
class TemplateRenderer
|
|
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_buttons(node)
|
|
17
|
+
title = node.attributes[:title]
|
|
18
|
+
subtitle = node.attributes[:subtitle]
|
|
19
|
+
image_url = find_image_url(node)
|
|
20
|
+
text_parts = collect_text_parts(node)
|
|
21
|
+
body_text = text_parts.join("\n")
|
|
22
|
+
|
|
23
|
+
if title && buttons.any?
|
|
24
|
+
# Generic Template: has title and buttons
|
|
25
|
+
element = {"title" => truncate(title, 80)}
|
|
26
|
+
element["subtitle"] = truncate(subtitle || body_text, 80) if subtitle || body_text.length.positive?
|
|
27
|
+
element["image_url"] = image_url if image_url
|
|
28
|
+
element["buttons"] = buttons.first(3)
|
|
29
|
+
|
|
30
|
+
{
|
|
31
|
+
attachment: {
|
|
32
|
+
"type" => "template",
|
|
33
|
+
"payload" => {
|
|
34
|
+
"template_type" => "generic",
|
|
35
|
+
"elements" => [element]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
elsif buttons.any? && buttons.length <= 3 && !image_url
|
|
40
|
+
# Button Template: text + buttons, no image
|
|
41
|
+
fallback_text = [title, subtitle, body_text].compact.reject(&:empty?).join(" - ")
|
|
42
|
+
fallback_text = "Choose an option" if fallback_text.empty?
|
|
43
|
+
|
|
44
|
+
{
|
|
45
|
+
attachment: {
|
|
46
|
+
"type" => "template",
|
|
47
|
+
"payload" => {
|
|
48
|
+
"template_type" => "button",
|
|
49
|
+
"text" => truncate(fallback_text, 640),
|
|
50
|
+
"buttons" => buttons.first(3)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else
|
|
55
|
+
# Fallback: plain text
|
|
56
|
+
fallback = [title, subtitle, body_text].compact.reject(&:empty?).join("\n")
|
|
57
|
+
if buttons.any?
|
|
58
|
+
button_text = buttons.map { |b| b["title"] }.join(", ")
|
|
59
|
+
fallback = "#{fallback}\n[#{button_text}]" unless button_text.empty?
|
|
60
|
+
end
|
|
61
|
+
{text: fallback}
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def collect_buttons(node)
|
|
66
|
+
buttons = []
|
|
67
|
+
node.children.each do |child|
|
|
68
|
+
case child.type
|
|
69
|
+
when :button
|
|
70
|
+
buttons << render_postback_button(child)
|
|
71
|
+
when :link_button
|
|
72
|
+
buttons << render_url_button(child)
|
|
73
|
+
when :actions
|
|
74
|
+
child.children.each do |action_child|
|
|
75
|
+
case action_child.type
|
|
76
|
+
when :button
|
|
77
|
+
buttons << render_postback_button(action_child)
|
|
78
|
+
when :link_button
|
|
79
|
+
buttons << render_url_button(action_child)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
when :section
|
|
83
|
+
buttons.concat(collect_buttons(child))
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
buttons
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def collect_text_parts(node)
|
|
90
|
+
parts = []
|
|
91
|
+
node.children.each do |child|
|
|
92
|
+
case child.type
|
|
93
|
+
when :text
|
|
94
|
+
parts << child.attributes[:content]
|
|
95
|
+
when :divider
|
|
96
|
+
parts << "---"
|
|
97
|
+
when :fields
|
|
98
|
+
child.children.each do |field|
|
|
99
|
+
parts << "#{field.attributes[:label]}: #{field.attributes[:value]}"
|
|
100
|
+
end
|
|
101
|
+
when :section
|
|
102
|
+
parts << child.attributes[:title] if child.attributes[:title]
|
|
103
|
+
parts.concat(collect_text_parts(child))
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
parts
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def find_image_url(node)
|
|
110
|
+
node.children.each do |child|
|
|
111
|
+
return child.attributes[:url] if child.type == :image
|
|
112
|
+
if child.type == :section
|
|
113
|
+
url = find_image_url(child)
|
|
114
|
+
return url if url
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
nil
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def render_postback_button(node)
|
|
121
|
+
{
|
|
122
|
+
"type" => "postback",
|
|
123
|
+
"title" => truncate(node.attributes[:text], 20),
|
|
124
|
+
"payload" => node.attributes[:id] || "button"
|
|
125
|
+
}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def render_url_button(node)
|
|
129
|
+
{
|
|
130
|
+
"type" => "web_url",
|
|
131
|
+
"title" => truncate(node.attributes[:text], 20),
|
|
132
|
+
"url" => node.attributes[:url]
|
|
133
|
+
}
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def render_single(node)
|
|
137
|
+
case node.type
|
|
138
|
+
when :text
|
|
139
|
+
{text: node.attributes[:content]}
|
|
140
|
+
else
|
|
141
|
+
{text: node.fallback_text}
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def truncate(text, max)
|
|
146
|
+
return "" unless text
|
|
147
|
+
|
|
148
|
+
(text.length > max) ? "#{text[0..max - 4]}..." : text
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
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 Messenger
|
|
10
|
+
class << self
|
|
11
|
+
def loader
|
|
12
|
+
@loader ||= begin
|
|
13
|
+
loader = Zeitwerk::Loader.new
|
|
14
|
+
loader.tag = "chat_sdk-messenger"
|
|
15
|
+
loader.inflector.inflect("chat_sdk" => "ChatSDK")
|
|
16
|
+
loader.push_dir("#{__dir__}/messenger", namespace: ChatSDK::Messenger)
|
|
17
|
+
loader.setup
|
|
18
|
+
loader
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
ChatSDK::Messenger.loader
|
metadata
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: chat_sdk-messenger
|
|
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: Facebook Messenger adapter for the ChatSDK framework with HMAC-SHA256
|
|
41
|
+
verification and template rendering
|
|
42
|
+
email:
|
|
43
|
+
- quentin@rootly.com
|
|
44
|
+
executables: []
|
|
45
|
+
extensions: []
|
|
46
|
+
extra_rdoc_files: []
|
|
47
|
+
files:
|
|
48
|
+
- lib/chat_sdk/messenger.rb
|
|
49
|
+
- lib/chat_sdk/messenger/adapter.rb
|
|
50
|
+
- lib/chat_sdk/messenger/api_client.rb
|
|
51
|
+
- lib/chat_sdk/messenger/event_parser.rb
|
|
52
|
+
- lib/chat_sdk/messenger/template_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: Facebook Messenger adapter for ChatSDK
|
|
75
|
+
test_files: []
|