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: []