chat_sdk-gchat 0.1.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 +7 -0
- data/lib/chat_sdk/gchat/adapter.rb +239 -0
- data/lib/chat_sdk/gchat/card_v2_renderer.rb +188 -0
- data/lib/chat_sdk/gchat/event_parser.rb +107 -0
- data/lib/chat_sdk/gchat/token_verifier.rb +32 -0
- data/lib/chat_sdk/gchat.rb +25 -0
- metadata +88 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3bfec5f052031774aa8db868e443e960182310d8af06672d7da4a9ea6e5816f8
|
|
4
|
+
data.tar.gz: 53dcace115831d3816b876f4048b3a693e72a46758c9e133dd24c37d8cad7f9d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1930e5a33ef72f960c3368190b578c963f79e384aad21c39950657e96d2a9c8efcedba418d4b922527198668a76083af503230c0c2f09f92c03692ee40c31529
|
|
7
|
+
data.tar.gz: '08c9d6e254f0d552c94e70f2210b8304159ff02578989f14a1502e8f6fb1df4930eaceed9f891fdeb500879919022331ad69545ceb9c559f89e1d35ccc28f62d'
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module GChat
|
|
5
|
+
class Adapter < ChatSDK::Adapter::Base
|
|
6
|
+
capabilities :edit_messages, :delete_messages, :ephemeral_messages,
|
|
7
|
+
:threads, :direct_messages, :message_history,
|
|
8
|
+
:reactions, :streaming_edit
|
|
9
|
+
|
|
10
|
+
attr_reader :client
|
|
11
|
+
|
|
12
|
+
def initialize(project_number:, credentials: nil)
|
|
13
|
+
@project_number = project_number.to_s
|
|
14
|
+
|
|
15
|
+
raise ChatSDK::ConfigurationError, "Google Chat project_number required" if @project_number.empty?
|
|
16
|
+
|
|
17
|
+
@credentials = build_credentials(credentials)
|
|
18
|
+
@client = Google::Apps::Chat::V1::ChatService::Client.new do |config|
|
|
19
|
+
config.credentials = @credentials if @credentials
|
|
20
|
+
end
|
|
21
|
+
@verifier = TokenVerifier.new(@project_number)
|
|
22
|
+
@renderer = CardV2Renderer.new
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def name
|
|
26
|
+
:gchat
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Inbound
|
|
30
|
+
def verify_request!(rack_request)
|
|
31
|
+
auth_header = rack_request.env["HTTP_AUTHORIZATION"] || ""
|
|
32
|
+
token = auth_header.sub(/\ABearer\s+/i, "")
|
|
33
|
+
@verifier.verify!(token)
|
|
34
|
+
true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def parse_events(rack_request)
|
|
38
|
+
body = rack_request.body.read
|
|
39
|
+
rack_request.body.rewind
|
|
40
|
+
|
|
41
|
+
payload = JSON.parse(body)
|
|
42
|
+
EventParser.parse(payload)
|
|
43
|
+
rescue JSON::ParserError
|
|
44
|
+
[]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Outbound
|
|
48
|
+
def post_message(channel_id:, message:, thread_id: nil)
|
|
49
|
+
msg = ChatSDK::PostableMessage.from(message)
|
|
50
|
+
request_body = build_message_body(msg)
|
|
51
|
+
|
|
52
|
+
if thread_id
|
|
53
|
+
request_body[:thread] = {name: "spaces/#{channel_id}/threads/#{thread_id}"}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
result = @client.create_message(
|
|
57
|
+
parent: "spaces/#{channel_id}",
|
|
58
|
+
message: request_body
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
build_response_message(result, channel_id, msg)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def edit_message(channel_id:, message_id:, message:)
|
|
65
|
+
require_capability!(:edit_messages)
|
|
66
|
+
msg = ChatSDK::PostableMessage.from(message)
|
|
67
|
+
request_body = build_message_body(msg)
|
|
68
|
+
request_body[:name] = "spaces/#{channel_id}/messages/#{message_id}"
|
|
69
|
+
|
|
70
|
+
@client.update_message(
|
|
71
|
+
message: request_body,
|
|
72
|
+
update_mask: Google::Protobuf::FieldMask.new(paths: ["text", "cards_v2"])
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def delete_message(channel_id:, message_id:)
|
|
77
|
+
require_capability!(:delete_messages)
|
|
78
|
+
@client.delete_message(name: "spaces/#{channel_id}/messages/#{message_id}")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def post_ephemeral(channel_id:, user_id:, message:, thread_id: nil)
|
|
82
|
+
require_capability!(:ephemeral_messages)
|
|
83
|
+
msg = ChatSDK::PostableMessage.from(message)
|
|
84
|
+
request_body = build_message_body(msg)
|
|
85
|
+
request_body[:private_message_viewer] = {name: "users/#{user_id}"}
|
|
86
|
+
|
|
87
|
+
if thread_id
|
|
88
|
+
request_body[:thread] = {name: "spaces/#{channel_id}/threads/#{thread_id}"}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
result = @client.create_message(
|
|
92
|
+
parent: "spaces/#{channel_id}",
|
|
93
|
+
message: request_body
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
build_response_message(result, channel_id, msg)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def upload_file(channel_id:, io:, filename:, thread_id: nil, comment: nil)
|
|
100
|
+
super
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def add_reaction(channel_id:, message_id:, emoji:)
|
|
104
|
+
require_capability!(:reactions)
|
|
105
|
+
@client.create_reaction(
|
|
106
|
+
parent: "spaces/#{channel_id}/messages/#{message_id}",
|
|
107
|
+
reaction: {
|
|
108
|
+
emoji: {unicode: emoji}
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def remove_reaction(channel_id:, message_id:, emoji:)
|
|
114
|
+
require_capability!(:reactions)
|
|
115
|
+
# Google Chat requires the reaction resource name to delete
|
|
116
|
+
# We construct it from the emoji unicode
|
|
117
|
+
@client.delete_reaction(
|
|
118
|
+
name: "spaces/#{channel_id}/messages/#{message_id}/reactions/#{emoji}"
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def open_dm(user_id)
|
|
123
|
+
require_capability!(:direct_messages)
|
|
124
|
+
result = @client.setup_space(
|
|
125
|
+
space: {space_type: "DIRECT_MESSAGE"},
|
|
126
|
+
memberships: [{member: {name: "users/#{user_id}", type: "HUMAN"}}]
|
|
127
|
+
)
|
|
128
|
+
extract_id(result.name)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def fetch_messages(channel_id:, thread_id: nil, cursor: nil, limit: 50)
|
|
132
|
+
require_capability!(:message_history)
|
|
133
|
+
result = @client.list_messages(
|
|
134
|
+
parent: "spaces/#{channel_id}",
|
|
135
|
+
page_size: limit,
|
|
136
|
+
page_token: cursor
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
messages = result.messages.map { |m| parse_gchat_message(m, channel_id) }
|
|
140
|
+
[messages, result.next_page_token.to_s.empty? ? nil : result.next_page_token]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def open_modal(trigger_id:, modal:)
|
|
144
|
+
super
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def start_typing(channel_id:, thread_id: nil)
|
|
148
|
+
super
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def mention(user_id)
|
|
152
|
+
"<users/#{user_id}>"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def render(postable_message)
|
|
156
|
+
if postable_message.card?
|
|
157
|
+
@renderer.render(postable_message.card)
|
|
158
|
+
else
|
|
159
|
+
postable_message.text
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
private
|
|
164
|
+
|
|
165
|
+
def build_credentials(credentials)
|
|
166
|
+
case credentials
|
|
167
|
+
when nil
|
|
168
|
+
# Use Application Default Credentials
|
|
169
|
+
Google::Auth.get_application_default(
|
|
170
|
+
["https://www.googleapis.com/auth/chat.bot"]
|
|
171
|
+
)
|
|
172
|
+
when String
|
|
173
|
+
# Path to service account JSON
|
|
174
|
+
Google::Auth::ServiceAccountCredentials.make_creds(
|
|
175
|
+
json_key_io: File.open(credentials),
|
|
176
|
+
scope: "https://www.googleapis.com/auth/chat.bot"
|
|
177
|
+
)
|
|
178
|
+
when Hash
|
|
179
|
+
# Service account JSON as a hash
|
|
180
|
+
Google::Auth::ServiceAccountCredentials.make_creds(
|
|
181
|
+
json_key_io: StringIO.new(JSON.generate(credentials)),
|
|
182
|
+
scope: "https://www.googleapis.com/auth/chat.bot"
|
|
183
|
+
)
|
|
184
|
+
else
|
|
185
|
+
credentials
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def build_message_body(msg)
|
|
190
|
+
body = {}
|
|
191
|
+
|
|
192
|
+
if msg.card?
|
|
193
|
+
rendered = @renderer.render(msg.card)
|
|
194
|
+
body[:cards_v2] = rendered[:cards_v2] if rendered[:cards_v2]
|
|
195
|
+
body[:text] = msg.text || msg.card.fallback_text
|
|
196
|
+
else
|
|
197
|
+
body[:text] = msg.text
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
body
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def build_response_message(result, channel_id, msg)
|
|
204
|
+
ChatSDK::Message.new(
|
|
205
|
+
id: extract_id(result.name),
|
|
206
|
+
text: msg.text || "",
|
|
207
|
+
author: ChatSDK::Author.new(id: "bot", name: "bot", platform: :gchat, bot: true),
|
|
208
|
+
thread_id: extract_id(result.thread&.name),
|
|
209
|
+
channel_id: channel_id,
|
|
210
|
+
platform: :gchat,
|
|
211
|
+
raw: result
|
|
212
|
+
)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def parse_gchat_message(msg, channel_id)
|
|
216
|
+
sender = msg.sender
|
|
217
|
+
ChatSDK::Message.new(
|
|
218
|
+
id: extract_id(msg.name),
|
|
219
|
+
text: msg.text || "",
|
|
220
|
+
author: ChatSDK::Author.new(
|
|
221
|
+
id: extract_id(sender&.name || "unknown"),
|
|
222
|
+
name: sender&.display_name || "unknown",
|
|
223
|
+
platform: :gchat,
|
|
224
|
+
bot: sender&.type == :BOT
|
|
225
|
+
),
|
|
226
|
+
thread_id: extract_id(msg.thread&.name),
|
|
227
|
+
channel_id: channel_id,
|
|
228
|
+
platform: :gchat,
|
|
229
|
+
raw: msg
|
|
230
|
+
)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def extract_id(resource_name)
|
|
234
|
+
return resource_name unless resource_name.is_a?(String)
|
|
235
|
+
resource_name.split("/").last || resource_name
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module GChat
|
|
5
|
+
class CardV2Renderer
|
|
6
|
+
def render(node)
|
|
7
|
+
case node.type
|
|
8
|
+
when :card then render_card(node)
|
|
9
|
+
else {cards_v2: [{card: {sections: [{widgets: [render_widget(node)]}]}}]}
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def render_card(node)
|
|
16
|
+
header = build_header(node)
|
|
17
|
+
sections = build_sections(node.children)
|
|
18
|
+
|
|
19
|
+
card = {}
|
|
20
|
+
card[:header] = header if header
|
|
21
|
+
card[:sections] = sections unless sections.empty?
|
|
22
|
+
|
|
23
|
+
{cards_v2: [{card: card}]}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def build_header(node)
|
|
27
|
+
return nil unless node.attributes[:title]
|
|
28
|
+
|
|
29
|
+
header = {title: node.attributes[:title]}
|
|
30
|
+
header[:subtitle] = node.attributes[:subtitle] if node.attributes[:subtitle]
|
|
31
|
+
header
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def build_sections(children)
|
|
35
|
+
sections = []
|
|
36
|
+
current_widgets = []
|
|
37
|
+
|
|
38
|
+
children.each do |child|
|
|
39
|
+
case child.type
|
|
40
|
+
when :divider
|
|
41
|
+
sections << {widgets: current_widgets} unless current_widgets.empty?
|
|
42
|
+
current_widgets = []
|
|
43
|
+
sections << {widgets: [{divider: {}}]}
|
|
44
|
+
when :section
|
|
45
|
+
sections << {widgets: current_widgets} unless current_widgets.empty?
|
|
46
|
+
current_widgets = []
|
|
47
|
+
section = {}
|
|
48
|
+
section[:header] = child.attributes[:title] if child.attributes[:title]
|
|
49
|
+
section[:widgets] = child.children.flat_map { |c| render_widgets(c) }.compact
|
|
50
|
+
sections << section
|
|
51
|
+
else
|
|
52
|
+
render_widgets(child).each { |w| current_widgets << w }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
sections << {widgets: current_widgets} unless current_widgets.empty?
|
|
57
|
+
sections
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def render_widget(node)
|
|
61
|
+
case node.type
|
|
62
|
+
when :text then render_text(node)
|
|
63
|
+
when :fields then render_fields(node)
|
|
64
|
+
when :image then render_image(node)
|
|
65
|
+
when :actions then render_actions(node)
|
|
66
|
+
when :divider then {divider: {}}
|
|
67
|
+
when :select then render_select(node)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def render_widgets(node)
|
|
72
|
+
case node.type
|
|
73
|
+
when :actions then render_actions_as_widgets(node)
|
|
74
|
+
else
|
|
75
|
+
widget = render_widget(node)
|
|
76
|
+
widget ? [widget] : []
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def render_text(node)
|
|
81
|
+
{textParagraph: {text: node.attributes[:content]}}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def render_fields(node)
|
|
85
|
+
{
|
|
86
|
+
decoratedText: {
|
|
87
|
+
topLabel: node.children.map { |f| f.attributes[:label] }.join(" | "),
|
|
88
|
+
text: node.children.map { |f| f.attributes[:value] }.join(" | ")
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def render_image(node)
|
|
94
|
+
img = {imageUrl: node.attributes[:url]}
|
|
95
|
+
img[:altText] = node.attributes[:alt] if node.attributes[:alt]
|
|
96
|
+
{image: img}
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def render_actions(node)
|
|
100
|
+
widgets = render_actions_as_widgets(node)
|
|
101
|
+
# For single-widget render path, return the first widget (buttonList or select)
|
|
102
|
+
widgets.first
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def render_actions_as_widgets(node)
|
|
106
|
+
widgets = []
|
|
107
|
+
buttons = []
|
|
108
|
+
node.children.each do |child|
|
|
109
|
+
if child.type == :select
|
|
110
|
+
widgets << render_button_list(buttons) unless buttons.empty?
|
|
111
|
+
buttons = []
|
|
112
|
+
widgets << render_select(child)
|
|
113
|
+
else
|
|
114
|
+
btn = render_action_element(child)
|
|
115
|
+
buttons << btn if btn
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
widgets << render_button_list(buttons) unless buttons.empty?
|
|
119
|
+
widgets
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def render_button_list(buttons)
|
|
123
|
+
{buttonList: {buttons: buttons}}
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def render_action_element(node)
|
|
127
|
+
case node.type
|
|
128
|
+
when :button then render_button(node)
|
|
129
|
+
when :link_button then render_link_button(node)
|
|
130
|
+
when :select then nil # selects are rendered as their own widget
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def render_button(node)
|
|
135
|
+
btn = {
|
|
136
|
+
text: node.attributes[:text],
|
|
137
|
+
onClick: {
|
|
138
|
+
action: {
|
|
139
|
+
actionMethodName: node.attributes[:id]
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if node.attributes[:value]
|
|
145
|
+
btn[:onClick][:action][:parameters] = [
|
|
146
|
+
{key: "value", value: node.attributes[:value]}
|
|
147
|
+
]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
if node.attributes[:style] == :primary
|
|
151
|
+
btn[:color] = {red: 0.0, green: 0.53, blue: 0.87, alpha: 1.0}
|
|
152
|
+
elsif node.attributes[:style] == :danger
|
|
153
|
+
btn[:color] = {red: 0.87, green: 0.17, blue: 0.17, alpha: 1.0}
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
btn
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def render_link_button(node)
|
|
160
|
+
{
|
|
161
|
+
text: node.attributes[:text],
|
|
162
|
+
onClick: {
|
|
163
|
+
openLink: {url: node.attributes[:url]}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def render_select(node)
|
|
169
|
+
{
|
|
170
|
+
selectionInput: {
|
|
171
|
+
name: node.attributes[:id],
|
|
172
|
+
label: node.attributes[:placeholder] || "Select",
|
|
173
|
+
type: "DROPDOWN",
|
|
174
|
+
items: node.children.map { |opt| render_select_option(opt) }
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def render_select_option(node)
|
|
180
|
+
{
|
|
181
|
+
text: node.attributes[:text],
|
|
182
|
+
value: node.attributes[:value],
|
|
183
|
+
selected: false
|
|
184
|
+
}
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module GChat
|
|
5
|
+
class EventParser
|
|
6
|
+
class << self
|
|
7
|
+
def parse(payload)
|
|
8
|
+
type = payload["type"]
|
|
9
|
+
|
|
10
|
+
case type
|
|
11
|
+
when "MESSAGE"
|
|
12
|
+
parse_message(payload)
|
|
13
|
+
when "CARD_CLICKED"
|
|
14
|
+
parse_card_clicked(payload)
|
|
15
|
+
when "ADDED_TO_SPACE", "REMOVED_FROM_SPACE"
|
|
16
|
+
[]
|
|
17
|
+
else
|
|
18
|
+
[]
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def parse_message(payload)
|
|
25
|
+
msg_data = payload["message"] || {}
|
|
26
|
+
sender = msg_data["sender"] || {}
|
|
27
|
+
space = msg_data["space"] || {}
|
|
28
|
+
thread_data = msg_data["thread"] || {}
|
|
29
|
+
|
|
30
|
+
author = ChatSDK::Author.new(
|
|
31
|
+
id: extract_id(sender["name"]),
|
|
32
|
+
name: sender["displayName"] || sender["name"] || "unknown",
|
|
33
|
+
platform: :gchat,
|
|
34
|
+
bot: sender["type"] == "BOT"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
message = ChatSDK::Message.new(
|
|
38
|
+
id: extract_id(msg_data["name"]),
|
|
39
|
+
text: msg_data["text"] || msg_data["argumentText"] || "",
|
|
40
|
+
author: author,
|
|
41
|
+
thread_id: extract_id(thread_data["name"]),
|
|
42
|
+
channel_id: extract_id(space["name"]),
|
|
43
|
+
platform: :gchat,
|
|
44
|
+
raw: msg_data
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if bot_mentioned?(msg_data)
|
|
48
|
+
[ChatSDK::Events::Mention.new(
|
|
49
|
+
message: message,
|
|
50
|
+
thread_id: message.thread_id,
|
|
51
|
+
channel_id: message.channel_id,
|
|
52
|
+
platform: :gchat,
|
|
53
|
+
adapter_name: :gchat,
|
|
54
|
+
raw: payload
|
|
55
|
+
)]
|
|
56
|
+
else
|
|
57
|
+
[ChatSDK::Events::SubscribedMessage.new(
|
|
58
|
+
message: message,
|
|
59
|
+
thread_id: message.thread_id,
|
|
60
|
+
channel_id: message.channel_id,
|
|
61
|
+
platform: :gchat,
|
|
62
|
+
adapter_name: :gchat,
|
|
63
|
+
raw: payload
|
|
64
|
+
)]
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def parse_card_clicked(payload)
|
|
69
|
+
action = payload.dig("action", "actionMethodName") || payload.dig("common", "invokedFunction")
|
|
70
|
+
params = payload.dig("action", "parameters") || []
|
|
71
|
+
value = params.first&.dig("value")
|
|
72
|
+
|
|
73
|
+
user_data = payload["user"] || {}
|
|
74
|
+
space = payload.dig("message", "space") || payload["space"] || {}
|
|
75
|
+
thread_data = payload.dig("message", "thread") || {}
|
|
76
|
+
|
|
77
|
+
user = ChatSDK::Author.new(
|
|
78
|
+
id: extract_id(user_data["name"]),
|
|
79
|
+
name: user_data["displayName"] || user_data["name"] || "unknown",
|
|
80
|
+
platform: :gchat
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
[ChatSDK::Events::Action.new(
|
|
84
|
+
action_id: action || "unknown",
|
|
85
|
+
value: value,
|
|
86
|
+
user: user,
|
|
87
|
+
thread_id: extract_id(thread_data["name"]),
|
|
88
|
+
channel_id: extract_id(space["name"]),
|
|
89
|
+
platform: :gchat,
|
|
90
|
+
adapter_name: :gchat,
|
|
91
|
+
raw: payload
|
|
92
|
+
)]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def bot_mentioned?(msg_data)
|
|
96
|
+
annotations = msg_data["annotations"] || []
|
|
97
|
+
annotations.any? { |a| a["type"] == "USER_MENTION" && a.dig("userMention", "type") == "MENTION" }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def extract_id(resource_name)
|
|
101
|
+
return resource_name unless resource_name.is_a?(String)
|
|
102
|
+
resource_name.split("/").last || resource_name
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module GChat
|
|
5
|
+
class TokenVerifier
|
|
6
|
+
EXPECTED_ISSUER = "chat@system.gserviceaccount.com"
|
|
7
|
+
|
|
8
|
+
def initialize(project_number)
|
|
9
|
+
@project_number = project_number.to_s
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def verify!(token)
|
|
13
|
+
raise ChatSDK::SignatureVerificationError, "Missing bearer token" if token.nil? || token.empty?
|
|
14
|
+
|
|
15
|
+
payload = Google::Auth::IDTokens.verify_oidc(
|
|
16
|
+
token,
|
|
17
|
+
aud: @project_number
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
issuer = payload["iss"] || payload["email"]
|
|
21
|
+
unless issuer == EXPECTED_ISSUER
|
|
22
|
+
raise ChatSDK::SignatureVerificationError,
|
|
23
|
+
"Unexpected issuer: #{issuer} (expected #{EXPECTED_ISSUER})"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
payload
|
|
27
|
+
rescue Google::Auth::IDTokens::VerificationError => e
|
|
28
|
+
raise ChatSDK::SignatureVerificationError, "Google ID token verification failed: #{e.message}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "chat_sdk"
|
|
4
|
+
require "google/apps/chat/v1"
|
|
5
|
+
require "googleauth"
|
|
6
|
+
require "zeitwerk"
|
|
7
|
+
|
|
8
|
+
module ChatSDK
|
|
9
|
+
module GChat
|
|
10
|
+
class << self
|
|
11
|
+
def loader
|
|
12
|
+
@loader ||= begin
|
|
13
|
+
loader = Zeitwerk::Loader.new
|
|
14
|
+
loader.tag = "chat_sdk-gchat"
|
|
15
|
+
loader.inflector.inflect("chat_sdk" => "ChatSDK", "gchat" => "GChat")
|
|
16
|
+
loader.push_dir("#{__dir__}/gchat", namespace: ChatSDK::GChat)
|
|
17
|
+
loader.setup
|
|
18
|
+
loader
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
ChatSDK::GChat.loader
|
metadata
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: chat_sdk-gchat
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Rootly
|
|
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: google-apps-chat-v1
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: googleauth
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.0'
|
|
54
|
+
description: Google Chat bot adapter for the ChatSDK framework
|
|
55
|
+
email:
|
|
56
|
+
- eng@rootly.com
|
|
57
|
+
executables: []
|
|
58
|
+
extensions: []
|
|
59
|
+
extra_rdoc_files: []
|
|
60
|
+
files:
|
|
61
|
+
- lib/chat_sdk/gchat.rb
|
|
62
|
+
- lib/chat_sdk/gchat/adapter.rb
|
|
63
|
+
- lib/chat_sdk/gchat/card_v2_renderer.rb
|
|
64
|
+
- lib/chat_sdk/gchat/event_parser.rb
|
|
65
|
+
- lib/chat_sdk/gchat/token_verifier.rb
|
|
66
|
+
homepage: https://github.com/rootlyhq/rootly-chat-sdk
|
|
67
|
+
licenses:
|
|
68
|
+
- MIT
|
|
69
|
+
metadata:
|
|
70
|
+
rubygems_mfa_required: 'true'
|
|
71
|
+
rdoc_options: []
|
|
72
|
+
require_paths:
|
|
73
|
+
- lib
|
|
74
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
75
|
+
requirements:
|
|
76
|
+
- - ">="
|
|
77
|
+
- !ruby/object:Gem::Version
|
|
78
|
+
version: 3.2.0
|
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
80
|
+
requirements:
|
|
81
|
+
- - ">="
|
|
82
|
+
- !ruby/object:Gem::Version
|
|
83
|
+
version: '0'
|
|
84
|
+
requirements: []
|
|
85
|
+
rubygems_version: 4.0.16
|
|
86
|
+
specification_version: 4
|
|
87
|
+
summary: Google Chat adapter for ChatSDK
|
|
88
|
+
test_files: []
|