chat_sdk-mattermost 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
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 858c54b49dd67ec3850a0d5896598be9c58794a2f2fdd6ee07233f7f20ff13fb
|
|
4
|
+
data.tar.gz: 0071f01c237e90e2ac85a2d12cb3917b83869a75a4115fa11a6aa22f0ee476dc
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8100e0c6256b3e21dd9a1bbf4a16f4c7acd61130b4df064a1142148876456ea2c1e25a1dcd7716bca9bfbe51a068300d773175aa6b20c0872fd9000472eee1d6
|
|
7
|
+
data.tar.gz: a05820710ee8e3fbdfdc74eb9f45e40c123212b870f1b57a1c56ac0267146073dc0756f4b9d738bcd401ebaed311916fb4f146342b49555457f4f07936422fd4
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rack/utils"
|
|
4
|
+
|
|
5
|
+
module ChatSDK
|
|
6
|
+
module Mattermost
|
|
7
|
+
class Adapter < ChatSDK::Adapter::Base
|
|
8
|
+
capabilities :edit_messages, :delete_messages, :ephemeral_messages, :reactions,
|
|
9
|
+
:typing_indicator, :streaming_edit, :threads, :direct_messages,
|
|
10
|
+
:message_history, :file_uploads
|
|
11
|
+
|
|
12
|
+
attr_reader :client
|
|
13
|
+
|
|
14
|
+
def initialize(base_url: nil, bot_token: nil, webhook_token: nil, bot_user_id: nil)
|
|
15
|
+
@base_url = base_url || ENV["MATTERMOST_BASE_URL"]
|
|
16
|
+
@bot_token = bot_token || ENV["MATTERMOST_BOT_TOKEN"]
|
|
17
|
+
@webhook_token = webhook_token || ENV["MATTERMOST_WEBHOOK_TOKEN"]
|
|
18
|
+
@bot_user_id = bot_user_id || ENV["MATTERMOST_BOT_USER_ID"]
|
|
19
|
+
|
|
20
|
+
raise ChatSDK::ConfigurationError, "Mattermost base_url required" unless @base_url
|
|
21
|
+
raise ChatSDK::ConfigurationError, "Mattermost bot_token required" unless @bot_token
|
|
22
|
+
|
|
23
|
+
@client = ApiClient.new(base_url: @base_url, bot_token: @bot_token)
|
|
24
|
+
@renderer = AttachmentRenderer.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def name
|
|
28
|
+
:mattermost
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Inbound
|
|
32
|
+
def verify_request!(rack_request)
|
|
33
|
+
body = rack_request.body.read
|
|
34
|
+
rack_request.body.rewind
|
|
35
|
+
|
|
36
|
+
payload = begin
|
|
37
|
+
JSON.parse(body)
|
|
38
|
+
rescue JSON::ParserError
|
|
39
|
+
raise ChatSDK::SignatureVerificationError, "Invalid JSON payload"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
return true unless @webhook_token
|
|
43
|
+
|
|
44
|
+
token = payload["token"]
|
|
45
|
+
unless token && Rack::Utils.secure_compare(token, @webhook_token)
|
|
46
|
+
raise ChatSDK::SignatureVerificationError, "Invalid webhook token"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
true
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def parse_events(rack_request)
|
|
53
|
+
body = rack_request.body.read
|
|
54
|
+
rack_request.body.rewind
|
|
55
|
+
|
|
56
|
+
payload = JSON.parse(body)
|
|
57
|
+
EventParser.parse(payload, bot_user_id: @bot_user_id)
|
|
58
|
+
rescue JSON::ParserError
|
|
59
|
+
[]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Outbound
|
|
63
|
+
def post_message(channel_id:, message:, thread_id: nil)
|
|
64
|
+
msg = ChatSDK::PostableMessage.from(message)
|
|
65
|
+
|
|
66
|
+
props = nil
|
|
67
|
+
if msg.card?
|
|
68
|
+
attachments = @renderer.render(msg.card)
|
|
69
|
+
props = {"attachments" => attachments}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
result = @client.create_post(
|
|
73
|
+
channel_id: channel_id,
|
|
74
|
+
message: msg.text || msg.card&.fallback_text || "",
|
|
75
|
+
root_id: thread_id,
|
|
76
|
+
props: props
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
ChatSDK::Message.new(
|
|
80
|
+
id: result["id"],
|
|
81
|
+
text: msg.text || "",
|
|
82
|
+
author: ChatSDK::Author.new(id: @bot_user_id || "bot", name: "bot", platform: :mattermost, bot: true),
|
|
83
|
+
thread_id: thread_id || result["id"],
|
|
84
|
+
channel_id: channel_id,
|
|
85
|
+
platform: :mattermost,
|
|
86
|
+
raw: result
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def edit_message(channel_id:, message_id:, message:)
|
|
91
|
+
require_capability!(:edit_messages)
|
|
92
|
+
msg = ChatSDK::PostableMessage.from(message)
|
|
93
|
+
|
|
94
|
+
props = nil
|
|
95
|
+
if msg.card?
|
|
96
|
+
attachments = @renderer.render(msg.card)
|
|
97
|
+
props = {"attachments" => attachments}
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
@client.update_post(
|
|
101
|
+
post_id: message_id,
|
|
102
|
+
message: msg.text || msg.card&.fallback_text || "",
|
|
103
|
+
props: props
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def delete_message(channel_id:, message_id:)
|
|
108
|
+
require_capability!(:delete_messages)
|
|
109
|
+
@client.delete_post(post_id: message_id)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def post_ephemeral(channel_id:, user_id:, message:, thread_id: nil)
|
|
113
|
+
require_capability!(:ephemeral_messages)
|
|
114
|
+
msg = ChatSDK::PostableMessage.from(message)
|
|
115
|
+
@client.create_ephemeral_post(
|
|
116
|
+
user_id: user_id,
|
|
117
|
+
channel_id: channel_id,
|
|
118
|
+
message: msg.text || msg.card&.fallback_text || ""
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def upload_file(channel_id:, io:, filename:, thread_id: nil, comment: nil)
|
|
123
|
+
require_capability!(:file_uploads)
|
|
124
|
+
|
|
125
|
+
file_result = @client.upload_file(channel_id: channel_id, io: io, filename: filename)
|
|
126
|
+
file_ids = file_result.dig("file_infos")&.map { |fi| fi["id"] } || []
|
|
127
|
+
|
|
128
|
+
@client.create_post(
|
|
129
|
+
channel_id: channel_id,
|
|
130
|
+
message: comment || "",
|
|
131
|
+
root_id: thread_id,
|
|
132
|
+
props: nil,
|
|
133
|
+
file_ids: file_ids
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def add_reaction(channel_id:, message_id:, emoji:)
|
|
138
|
+
require_capability!(:reactions)
|
|
139
|
+
raise ChatSDK::ConfigurationError, "bot_user_id required for reactions" unless @bot_user_id
|
|
140
|
+
|
|
141
|
+
@client.add_reaction(
|
|
142
|
+
user_id: @bot_user_id,
|
|
143
|
+
post_id: message_id,
|
|
144
|
+
emoji_name: emoji
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def remove_reaction(channel_id:, message_id:, emoji:)
|
|
149
|
+
require_capability!(:reactions)
|
|
150
|
+
raise ChatSDK::ConfigurationError, "bot_user_id required for reactions" unless @bot_user_id
|
|
151
|
+
|
|
152
|
+
@client.remove_reaction(
|
|
153
|
+
user_id: @bot_user_id,
|
|
154
|
+
post_id: message_id,
|
|
155
|
+
emoji_name: emoji
|
|
156
|
+
)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def open_dm(user_id)
|
|
160
|
+
require_capability!(:direct_messages)
|
|
161
|
+
raise ChatSDK::ConfigurationError, "bot_user_id required for DMs" unless @bot_user_id
|
|
162
|
+
|
|
163
|
+
result = @client.create_direct_channel([@bot_user_id, user_id])
|
|
164
|
+
result["id"]
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def fetch_messages(channel_id:, thread_id: nil, cursor: nil, limit: 50)
|
|
168
|
+
require_capability!(:message_history)
|
|
169
|
+
|
|
170
|
+
if thread_id
|
|
171
|
+
result = @client.get_post_thread(post_id: thread_id)
|
|
172
|
+
else
|
|
173
|
+
page = cursor&.to_i || 0
|
|
174
|
+
result = @client.get_channel_posts(channel_id: channel_id, page: page, per_page: limit)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
order = result["order"] || []
|
|
178
|
+
posts = result["posts"] || {}
|
|
179
|
+
|
|
180
|
+
messages = order.map do |post_id|
|
|
181
|
+
post = posts[post_id]
|
|
182
|
+
next unless post
|
|
183
|
+
|
|
184
|
+
ChatSDK::Message.new(
|
|
185
|
+
id: post["id"],
|
|
186
|
+
text: post["message"] || "",
|
|
187
|
+
author: ChatSDK::Author.new(
|
|
188
|
+
id: post["user_id"] || "unknown",
|
|
189
|
+
name: post["user_id"] || "unknown",
|
|
190
|
+
platform: :mattermost
|
|
191
|
+
),
|
|
192
|
+
thread_id: post["root_id"].to_s.empty? ? post["id"] : post["root_id"],
|
|
193
|
+
channel_id: post["channel_id"],
|
|
194
|
+
platform: :mattermost,
|
|
195
|
+
raw: post
|
|
196
|
+
)
|
|
197
|
+
end.compact
|
|
198
|
+
|
|
199
|
+
next_cursor = thread_id ? nil : ((cursor&.to_i || 0) + 1).to_s
|
|
200
|
+
[messages, next_cursor]
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def open_modal(trigger_id:, modal:)
|
|
204
|
+
super # raises NotSupportedError
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def start_typing(channel_id:, thread_id: nil)
|
|
208
|
+
require_capability!(:typing_indicator)
|
|
209
|
+
@client.send_typing(channel_id: channel_id)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def mention(user_id)
|
|
213
|
+
"@#{user_id}"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def render(postable_message)
|
|
217
|
+
msg = ChatSDK::PostableMessage.from(postable_message)
|
|
218
|
+
if msg.card?
|
|
219
|
+
@renderer.render(msg.card)
|
|
220
|
+
else
|
|
221
|
+
msg.text
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Mattermost
|
|
5
|
+
class ApiClient
|
|
6
|
+
attr_reader :base_url
|
|
7
|
+
|
|
8
|
+
def initialize(base_url:, bot_token:)
|
|
9
|
+
@base_url = base_url.chomp("/")
|
|
10
|
+
@bot_token = bot_token
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Posts
|
|
14
|
+
def create_post(channel_id:, message:, root_id: nil, props: nil, file_ids: nil)
|
|
15
|
+
body = {"channel_id" => channel_id, "message" => message}
|
|
16
|
+
body["root_id"] = root_id if root_id
|
|
17
|
+
body["props"] = props if props
|
|
18
|
+
body["file_ids"] = file_ids if file_ids
|
|
19
|
+
request(:post, "/api/v4/posts", body)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def update_post(post_id:, message:, props: nil)
|
|
23
|
+
body = {"id" => post_id, "message" => message}
|
|
24
|
+
body["props"] = props if props
|
|
25
|
+
request(:put, "/api/v4/posts/#{post_id}", body)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def delete_post(post_id:)
|
|
29
|
+
request(:delete, "/api/v4/posts/#{post_id}")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def create_ephemeral_post(user_id:, channel_id:, message:)
|
|
33
|
+
body = {
|
|
34
|
+
"user_id" => user_id,
|
|
35
|
+
"post" => {"channel_id" => channel_id, "message" => message}
|
|
36
|
+
}
|
|
37
|
+
request(:post, "/api/v4/posts/ephemeral", body)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Reactions
|
|
41
|
+
def add_reaction(user_id:, post_id:, emoji_name:)
|
|
42
|
+
body = {"user_id" => user_id, "post_id" => post_id, "emoji_name" => emoji_name}
|
|
43
|
+
request(:post, "/api/v4/reactions", body)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def remove_reaction(user_id:, post_id:, emoji_name:)
|
|
47
|
+
request(:delete, "/api/v4/reactions/#{user_id}/#{post_id}/#{emoji_name}")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Channels
|
|
51
|
+
def create_direct_channel(user_ids)
|
|
52
|
+
request(:post, "/api/v4/channels/direct", user_ids)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def get_channel_posts(channel_id:, page: 0, per_page: 50)
|
|
56
|
+
request(:get, "/api/v4/channels/#{channel_id}/posts?page=#{page}&per_page=#{per_page}")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Threads
|
|
60
|
+
def get_post_thread(post_id:)
|
|
61
|
+
request(:get, "/api/v4/posts/#{post_id}/thread")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Files
|
|
65
|
+
def upload_file(channel_id:, io:, filename:)
|
|
66
|
+
conn = Faraday.new(url: @base_url) do |f|
|
|
67
|
+
f.request :multipart
|
|
68
|
+
f.response :json
|
|
69
|
+
f.adapter :net_http
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
payload = {
|
|
73
|
+
files: Faraday::Multipart::FilePart.new(io, "application/octet-stream", filename),
|
|
74
|
+
channel_id: channel_id
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
response = conn.post("/api/v4/files", payload) do |req|
|
|
78
|
+
req.headers["Authorization"] = "Bearer #{@bot_token}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
handle_response(response)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Typing indicator
|
|
85
|
+
def send_typing(channel_id:)
|
|
86
|
+
request(:post, "/api/v4/users/me/typing", {"channel_id" => channel_id})
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def connection
|
|
92
|
+
@connection ||= Faraday.new(url: @base_url) do |f|
|
|
93
|
+
f.request :json
|
|
94
|
+
f.response :json
|
|
95
|
+
f.adapter :net_http
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def request(method, path, body = nil)
|
|
100
|
+
response = connection.public_send(method, path) do |req|
|
|
101
|
+
req.headers["Authorization"] = "Bearer #{@bot_token}"
|
|
102
|
+
req.body = body if body && method != :get
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
handle_response(response)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def handle_response(response)
|
|
109
|
+
return response.body if response.success?
|
|
110
|
+
|
|
111
|
+
if response.status == 429
|
|
112
|
+
retry_after = response.headers["Retry-After"]&.to_i
|
|
113
|
+
raise ChatSDK::RateLimitedError.new(
|
|
114
|
+
"Mattermost API rate limited",
|
|
115
|
+
retry_after: retry_after,
|
|
116
|
+
status: response.status,
|
|
117
|
+
body: response.body,
|
|
118
|
+
adapter_name: :mattermost
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
raise ChatSDK::PlatformError.new(
|
|
123
|
+
"Mattermost API error: #{response.status}",
|
|
124
|
+
status: response.status,
|
|
125
|
+
body: response.body,
|
|
126
|
+
adapter_name: :mattermost
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Mattermost
|
|
5
|
+
class AttachmentRenderer
|
|
6
|
+
attr_writer :integration_url
|
|
7
|
+
|
|
8
|
+
def initialize(integration_url: nil)
|
|
9
|
+
@integration_url = integration_url
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def render(node)
|
|
13
|
+
case node.type
|
|
14
|
+
when :card then render_card(node)
|
|
15
|
+
else wrap_attachment(render_node(node))
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def render_card(node)
|
|
22
|
+
attachment = {}
|
|
23
|
+
fields = []
|
|
24
|
+
actions = []
|
|
25
|
+
text_parts = []
|
|
26
|
+
|
|
27
|
+
node.children.each do |child|
|
|
28
|
+
case child.type
|
|
29
|
+
when :text
|
|
30
|
+
text_parts << child.attributes[:content]
|
|
31
|
+
when :divider
|
|
32
|
+
text_parts << "---"
|
|
33
|
+
when :image
|
|
34
|
+
attachment["image_url"] = child.attributes[:url]
|
|
35
|
+
when :fields
|
|
36
|
+
child.children.each do |field|
|
|
37
|
+
fields << {
|
|
38
|
+
"title" => field.attributes[:label],
|
|
39
|
+
"value" => field.attributes[:value],
|
|
40
|
+
"short" => true
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
when :section
|
|
44
|
+
render_section_into(child, text_parts, fields, actions)
|
|
45
|
+
when :actions
|
|
46
|
+
child.children.each do |action_node|
|
|
47
|
+
action = render_action(action_node)
|
|
48
|
+
actions << action if action
|
|
49
|
+
end
|
|
50
|
+
when :button
|
|
51
|
+
action = render_action(child)
|
|
52
|
+
actions << action if action
|
|
53
|
+
when :select
|
|
54
|
+
action = render_action(child)
|
|
55
|
+
actions << action if action
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
attachment["text"] = text_parts.join("\n") unless text_parts.empty?
|
|
60
|
+
attachment["fields"] = fields unless fields.empty?
|
|
61
|
+
attachment["actions"] = actions unless actions.empty?
|
|
62
|
+
|
|
63
|
+
if node.attributes[:title]
|
|
64
|
+
attachment["title"] = node.attributes[:title]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
[attachment]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def render_section_into(node, text_parts, fields, actions)
|
|
71
|
+
text_parts << "**#{node.attributes[:title]}**" if node.attributes[:title]
|
|
72
|
+
|
|
73
|
+
node.children.each do |child|
|
|
74
|
+
case child.type
|
|
75
|
+
when :text
|
|
76
|
+
text_parts << child.attributes[:content]
|
|
77
|
+
when :fields
|
|
78
|
+
child.children.each do |field|
|
|
79
|
+
fields << {
|
|
80
|
+
"title" => field.attributes[:label],
|
|
81
|
+
"value" => field.attributes[:value],
|
|
82
|
+
"short" => true
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
when :actions
|
|
86
|
+
child.children.each do |action_node|
|
|
87
|
+
action = render_action(action_node)
|
|
88
|
+
actions << action if action
|
|
89
|
+
end
|
|
90
|
+
when :button, :select
|
|
91
|
+
action = render_action(child)
|
|
92
|
+
actions << action if action
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def render_action(node)
|
|
98
|
+
case node.type
|
|
99
|
+
when :button then render_button(node)
|
|
100
|
+
when :link_button then render_link_button(node)
|
|
101
|
+
when :select then render_select(node)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def render_button(node)
|
|
106
|
+
action = {
|
|
107
|
+
"id" => node.attributes[:id] || "button",
|
|
108
|
+
"name" => node.attributes[:text],
|
|
109
|
+
"type" => "button"
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if @integration_url
|
|
113
|
+
action["integration"] = {
|
|
114
|
+
"url" => @integration_url,
|
|
115
|
+
"context" => {
|
|
116
|
+
"action" => node.attributes[:id],
|
|
117
|
+
"value" => node.attributes[:value]
|
|
118
|
+
}.compact
|
|
119
|
+
}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
if node.attributes[:style] == :primary
|
|
123
|
+
action["style"] = "primary"
|
|
124
|
+
elsif node.attributes[:style] == :danger
|
|
125
|
+
action["style"] = "danger"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
action
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def render_link_button(node)
|
|
132
|
+
# Mattermost does not have native link buttons in attachments.
|
|
133
|
+
# Render as a button with a URL in the integration context.
|
|
134
|
+
{
|
|
135
|
+
"id" => node.attributes[:id] || "link",
|
|
136
|
+
"name" => node.attributes[:text],
|
|
137
|
+
"type" => "button",
|
|
138
|
+
"integration" => {
|
|
139
|
+
"url" => node.attributes[:url],
|
|
140
|
+
"context" => {"action" => "open_url", "url" => node.attributes[:url]}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def render_select(node)
|
|
146
|
+
action = {
|
|
147
|
+
"id" => node.attributes[:id] || "select",
|
|
148
|
+
"name" => node.attributes[:placeholder] || "Select",
|
|
149
|
+
"type" => "select",
|
|
150
|
+
"options" => node.children.map do |opt|
|
|
151
|
+
{"text" => opt.attributes[:text], "value" => opt.attributes[:value]}
|
|
152
|
+
end
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if @integration_url
|
|
156
|
+
action["integration"] = {
|
|
157
|
+
"url" => @integration_url,
|
|
158
|
+
"context" => {"action" => node.attributes[:id]}
|
|
159
|
+
}
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
action
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def render_node(node)
|
|
166
|
+
case node.type
|
|
167
|
+
when :text
|
|
168
|
+
{"text" => node.attributes[:content]}
|
|
169
|
+
when :fields
|
|
170
|
+
fields = node.children.map do |field|
|
|
171
|
+
{"title" => field.attributes[:label], "value" => field.attributes[:value], "short" => true}
|
|
172
|
+
end
|
|
173
|
+
{"fields" => fields}
|
|
174
|
+
when :actions
|
|
175
|
+
actions = node.children.filter_map { |child| render_action(child) }
|
|
176
|
+
{"actions" => actions}
|
|
177
|
+
else
|
|
178
|
+
{"text" => node.fallback_text}
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def wrap_attachment(content)
|
|
183
|
+
[content]
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Mattermost
|
|
5
|
+
class EventParser
|
|
6
|
+
class << self
|
|
7
|
+
def parse(payload, bot_user_id: nil)
|
|
8
|
+
return [] unless payload.is_a?(Hash)
|
|
9
|
+
|
|
10
|
+
if payload.key?("trigger_word") || payload.key?("token")
|
|
11
|
+
parse_outgoing_webhook(payload, bot_user_id)
|
|
12
|
+
elsif payload.key?("context") && (payload.key?("type") || payload.key?("action"))
|
|
13
|
+
parse_interactive_action(payload)
|
|
14
|
+
else
|
|
15
|
+
[]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def parse_outgoing_webhook(payload, bot_user_id)
|
|
22
|
+
return [] if bot_user_id && payload["user_id"] == bot_user_id
|
|
23
|
+
|
|
24
|
+
author = ChatSDK::Author.new(
|
|
25
|
+
id: payload["user_id"] || "unknown",
|
|
26
|
+
name: payload["user_name"] || payload["user_id"] || "unknown",
|
|
27
|
+
platform: :mattermost,
|
|
28
|
+
bot: false
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
channel_id = payload["channel_id"]
|
|
32
|
+
thread_id = payload["root_id"].to_s.empty? ? nil : payload["root_id"]
|
|
33
|
+
|
|
34
|
+
message = ChatSDK::Message.new(
|
|
35
|
+
id: payload["post_id"] || "unknown",
|
|
36
|
+
text: payload["text"] || "",
|
|
37
|
+
author: author,
|
|
38
|
+
thread_id: thread_id || payload["post_id"],
|
|
39
|
+
channel_id: channel_id,
|
|
40
|
+
platform: :mattermost,
|
|
41
|
+
raw: payload
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if payload["trigger_word"].to_s.start_with?("@")
|
|
45
|
+
[ChatSDK::Events::Mention.new(
|
|
46
|
+
message: message,
|
|
47
|
+
thread_id: thread_id || payload["post_id"],
|
|
48
|
+
channel_id: channel_id,
|
|
49
|
+
platform: :mattermost,
|
|
50
|
+
adapter_name: :mattermost,
|
|
51
|
+
raw: payload
|
|
52
|
+
)]
|
|
53
|
+
else
|
|
54
|
+
[ChatSDK::Events::SubscribedMessage.new(
|
|
55
|
+
message: message,
|
|
56
|
+
thread_id: thread_id || payload["post_id"],
|
|
57
|
+
channel_id: channel_id,
|
|
58
|
+
platform: :mattermost,
|
|
59
|
+
adapter_name: :mattermost,
|
|
60
|
+
raw: payload
|
|
61
|
+
)]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def parse_interactive_action(payload)
|
|
66
|
+
user = ChatSDK::Author.new(
|
|
67
|
+
id: payload["user_id"] || "unknown",
|
|
68
|
+
name: payload["user_name"] || payload["user_id"] || "unknown",
|
|
69
|
+
platform: :mattermost,
|
|
70
|
+
bot: false
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
channel_id = payload["channel_id"]
|
|
74
|
+
context = payload["context"] || {}
|
|
75
|
+
action_id = context["action"] || payload["action"] || "unknown"
|
|
76
|
+
|
|
77
|
+
value = if payload["type"] == "select"
|
|
78
|
+
payload["selected_option"]
|
|
79
|
+
else
|
|
80
|
+
context["value"]
|
|
81
|
+
end
|
|
82
|
+
value = value.is_a?(Hash) ? JSON.generate(value) : value.to_s
|
|
83
|
+
|
|
84
|
+
[ChatSDK::Events::Action.new(
|
|
85
|
+
action_id: action_id,
|
|
86
|
+
value: value,
|
|
87
|
+
user: user,
|
|
88
|
+
thread_id: payload["post_id"],
|
|
89
|
+
channel_id: channel_id,
|
|
90
|
+
platform: :mattermost,
|
|
91
|
+
adapter_name: :mattermost,
|
|
92
|
+
raw: payload
|
|
93
|
+
)]
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
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 Mattermost
|
|
10
|
+
class << self
|
|
11
|
+
def loader
|
|
12
|
+
@loader ||= begin
|
|
13
|
+
loader = Zeitwerk::Loader.new
|
|
14
|
+
loader.tag = "chat_sdk-mattermost"
|
|
15
|
+
loader.inflector.inflect("chat_sdk" => "ChatSDK")
|
|
16
|
+
loader.push_dir("#{__dir__}/mattermost", namespace: ChatSDK::Mattermost)
|
|
17
|
+
loader.setup
|
|
18
|
+
loader
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
ChatSDK::Mattermost.loader
|
metadata
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: chat_sdk-mattermost
|
|
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: 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: Mattermost bot adapter for the ChatSDK framework
|
|
41
|
+
email:
|
|
42
|
+
- eng@rootly.com
|
|
43
|
+
executables: []
|
|
44
|
+
extensions: []
|
|
45
|
+
extra_rdoc_files: []
|
|
46
|
+
files:
|
|
47
|
+
- lib/chat_sdk/mattermost.rb
|
|
48
|
+
- lib/chat_sdk/mattermost/adapter.rb
|
|
49
|
+
- lib/chat_sdk/mattermost/api_client.rb
|
|
50
|
+
- lib/chat_sdk/mattermost/attachment_renderer.rb
|
|
51
|
+
- lib/chat_sdk/mattermost/event_parser.rb
|
|
52
|
+
homepage: https://github.com/rootlyhq/rootly-chat-sdk
|
|
53
|
+
licenses:
|
|
54
|
+
- MIT
|
|
55
|
+
metadata:
|
|
56
|
+
rubygems_mfa_required: 'true'
|
|
57
|
+
rdoc_options: []
|
|
58
|
+
require_paths:
|
|
59
|
+
- lib
|
|
60
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
61
|
+
requirements:
|
|
62
|
+
- - ">="
|
|
63
|
+
- !ruby/object:Gem::Version
|
|
64
|
+
version: 3.2.0
|
|
65
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '0'
|
|
70
|
+
requirements: []
|
|
71
|
+
rubygems_version: 4.0.16
|
|
72
|
+
specification_version: 4
|
|
73
|
+
summary: Mattermost adapter for ChatSDK
|
|
74
|
+
test_files: []
|