chat_sdk-teams 1.1.0 → 1.2.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 +4 -4
- data/lib/chat_sdk/teams/activity_parser.rb +100 -13
- data/lib/chat_sdk/teams/adapter.rb +106 -22
- data/lib/chat_sdk/teams/adaptive_card_renderer.rb +53 -10
- data/lib/chat_sdk/teams/graph_client.rb +74 -2
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7a6410c72972c0210380902fcf4a176c992c68d7c18154ff55f2a286fe1823a2
|
|
4
|
+
data.tar.gz: 59897106f6792d7ec3c73b76de36019fa1eb40126ec120bf28060fe35cc47cc2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 485f8fea6b1f2702b71a7b1a78cbef630255c0a88d92d5b8e04a723bc30fd7629746eb9a84fc6e99871baec9dc488aeb7ee2bfebfaacf60a95e298f48a3b4571
|
|
7
|
+
data.tar.gz: f25e032254fee24a840cd7c14afd317ea45c279e404f2bfb6cf5a494d20803f980f2452d7e6a99e6f3d7b485a435c6cf4316682b3cdc3737fcd45eb5072ef8ab
|
|
@@ -9,11 +9,17 @@ module ChatSDK
|
|
|
9
9
|
|
|
10
10
|
case activity["type"]
|
|
11
11
|
when "message"
|
|
12
|
-
|
|
12
|
+
if action_submission?(activity)
|
|
13
|
+
parse_invoke(activity, bot_app_id)
|
|
14
|
+
else
|
|
15
|
+
parse_message(activity, bot_app_id)
|
|
16
|
+
end
|
|
13
17
|
when "messageReaction"
|
|
14
18
|
parse_message_reaction(activity)
|
|
15
19
|
when "invoke"
|
|
16
|
-
parse_invoke(activity)
|
|
20
|
+
parse_invoke(activity, bot_app_id)
|
|
21
|
+
when "installationUpdate"
|
|
22
|
+
parse_installation(activity, bot_app_id)
|
|
17
23
|
else
|
|
18
24
|
[]
|
|
19
25
|
end
|
|
@@ -22,9 +28,9 @@ module ChatSDK
|
|
|
22
28
|
private
|
|
23
29
|
|
|
24
30
|
def parse_message(activity, bot_app_id)
|
|
25
|
-
return [] if activity
|
|
31
|
+
return [] if self_message?(activity, bot_app_id)
|
|
26
32
|
|
|
27
|
-
author = build_author(activity)
|
|
33
|
+
author = build_author(activity, bot_app_id)
|
|
28
34
|
conversation_id = activity.dig("conversation", "id")
|
|
29
35
|
thread_id = activity.dig("conversation", "id")
|
|
30
36
|
|
|
@@ -103,19 +109,37 @@ module ChatSDK
|
|
|
103
109
|
events
|
|
104
110
|
end
|
|
105
111
|
|
|
106
|
-
def parse_invoke(activity)
|
|
112
|
+
def parse_invoke(activity, bot_app_id)
|
|
107
113
|
return [] unless activity.dig("value")
|
|
108
114
|
|
|
109
115
|
conversation_id = activity.dig("conversation", "id")
|
|
110
|
-
user = build_author(activity)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
116
|
+
user = build_author(activity, bot_app_id)
|
|
117
|
+
|
|
118
|
+
value_data = activity["value"]
|
|
119
|
+
nested_data = value_data["data"] if value_data.is_a?(Hash)
|
|
120
|
+
action = value_data["action"] if value_data.is_a?(Hash)
|
|
121
|
+
action_data = action.is_a?(Hash) ? action["data"] : nil
|
|
122
|
+
data = if nested_data.is_a?(Hash)
|
|
123
|
+
nested_data
|
|
124
|
+
elsif action_data.is_a?(Hash)
|
|
125
|
+
action_data
|
|
126
|
+
else
|
|
127
|
+
value_data
|
|
128
|
+
end
|
|
129
|
+
data = {} unless data.is_a?(Hash)
|
|
130
|
+
action_id = if action_data.is_a?(Hash)
|
|
131
|
+
action_data["actionId"] || action_data["action"]
|
|
132
|
+
end
|
|
133
|
+
action_id ||= action["verb"] if action.is_a?(Hash)
|
|
134
|
+
action_id ||= data["actionId"] || data["action"] ||
|
|
135
|
+
(action unless action.is_a?(Hash)) || activity["name"] || "invoke"
|
|
136
|
+
value = data["value"] || (nested_data unless nested_data.is_a?(Hash)) || data
|
|
114
137
|
value = value.is_a?(Hash) ? JSON.generate(value) : value.to_s
|
|
115
138
|
|
|
116
139
|
[ChatSDK::Events::Action.new(
|
|
117
140
|
action_id: action_id,
|
|
118
141
|
value: value,
|
|
142
|
+
values: action_values(data),
|
|
119
143
|
user: user,
|
|
120
144
|
thread_id: conversation_id,
|
|
121
145
|
channel_id: conversation_id,
|
|
@@ -125,17 +149,80 @@ module ChatSDK
|
|
|
125
149
|
)]
|
|
126
150
|
end
|
|
127
151
|
|
|
128
|
-
def
|
|
152
|
+
def action_submission?(activity)
|
|
153
|
+
value = activity["value"]
|
|
154
|
+
value.is_a?(Hash) && (value.key?("data") || value.key?("action") || value.key?("actionId"))
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def parse_installation(activity, bot_app_id)
|
|
158
|
+
return [] unless app_id?(activity.dig("recipient", "id"), bot_app_id)
|
|
159
|
+
|
|
160
|
+
action = activity["action"]
|
|
161
|
+
event_class = case action
|
|
162
|
+
when "add", "add-upgrade" then ChatSDK::Events::Installed
|
|
163
|
+
when "remove", "remove-upgrade" then ChatSDK::Events::Uninstalled
|
|
164
|
+
else return []
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
conversation_id = activity.dig("conversation", "id")
|
|
168
|
+
return [] unless conversation_id
|
|
169
|
+
|
|
170
|
+
team_id = activity.dig("channelData", "team", "id")
|
|
171
|
+
tenant_id = activity.dig("conversation", "tenantId") ||
|
|
172
|
+
activity.dig("channelData", "tenant", "id")
|
|
173
|
+
user_id = activity.dig("from", "aadObjectId") || activity.dig("from", "id")
|
|
174
|
+
installation_id = if team_id
|
|
175
|
+
team_id
|
|
176
|
+
elsif tenant_id && user_id
|
|
177
|
+
"#{tenant_id}:#{user_id}"
|
|
178
|
+
else
|
|
179
|
+
tenant_id || conversation_id
|
|
180
|
+
end
|
|
181
|
+
[event_class.new(
|
|
182
|
+
installation_id: installation_id,
|
|
183
|
+
team_id: team_id,
|
|
184
|
+
tenant_id: tenant_id,
|
|
185
|
+
conversation_id: conversation_id,
|
|
186
|
+
action: action,
|
|
187
|
+
user: build_author(activity, bot_app_id),
|
|
188
|
+
platform: :teams,
|
|
189
|
+
adapter_name: :teams,
|
|
190
|
+
raw: activity
|
|
191
|
+
)]
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def build_author(activity, bot_app_id)
|
|
129
195
|
from = activity["from"] || {}
|
|
196
|
+
from_id = from["id"] || "unknown"
|
|
130
197
|
ChatSDK::Author.new(
|
|
131
|
-
id:
|
|
132
|
-
name: from["name"] ||
|
|
198
|
+
id: from_id,
|
|
199
|
+
name: from["name"] || from_id,
|
|
133
200
|
platform: :teams,
|
|
134
|
-
bot: from["role"] == "bot",
|
|
201
|
+
bot: from["role"].to_s.downcase == "bot" || from_id.start_with?("28:"),
|
|
202
|
+
me: self_message?(activity, bot_app_id),
|
|
135
203
|
locale: activity["locale"]
|
|
136
204
|
)
|
|
137
205
|
end
|
|
138
206
|
|
|
207
|
+
def self_message?(activity, bot_app_id)
|
|
208
|
+
from_id = activity.dig("from", "id")
|
|
209
|
+
return true if app_id?(from_id, bot_app_id)
|
|
210
|
+
|
|
211
|
+
recipient_id = activity.dig("recipient", "id")
|
|
212
|
+
from_id && recipient_id && from_id == recipient_id
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def app_id?(id, bot_app_id)
|
|
216
|
+
return false unless id && bot_app_id
|
|
217
|
+
|
|
218
|
+
id == bot_app_id || id == "28:#{bot_app_id}"
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def action_values(data)
|
|
222
|
+
values = data.except("action", "actionId", "value", "msteams")
|
|
223
|
+
values.empty? ? nil : values
|
|
224
|
+
end
|
|
225
|
+
|
|
139
226
|
def bot_mentioned?(activity, bot_app_id)
|
|
140
227
|
return false unless bot_app_id
|
|
141
228
|
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
3
5
|
module ChatSDK
|
|
4
6
|
module Teams
|
|
5
7
|
class Adapter < ChatSDK::Adapter::Base
|
|
8
|
+
DEFAULT_SERVICE_URL = "https://smba.trafficmanager.net/teams/"
|
|
9
|
+
VERIFIED_JWT_CLAIMS_ENV_KEY = "chat_sdk.teams.verified_jwt_claims"
|
|
10
|
+
|
|
6
11
|
capabilities :edit_messages, :delete_messages, :threads, :direct_messages,
|
|
7
12
|
:file_uploads, :streaming_edit, :typing_indicator, :message_history
|
|
8
13
|
|
|
9
14
|
attr_reader :client
|
|
10
15
|
|
|
11
16
|
def initialize(app_id: nil, app_password: nil, tenant_id: nil,
|
|
12
|
-
graph_client_id: nil, graph_client_secret: nil, graph_tenant_id: nil
|
|
17
|
+
graph_client_id: nil, graph_client_secret: nil, graph_tenant_id: nil,
|
|
18
|
+
service_url: nil)
|
|
13
19
|
@app_id = app_id || ENV["TEAMS_APP_ID"]
|
|
14
20
|
@app_password = app_password || ENV["TEAMS_APP_PASSWORD"]
|
|
15
21
|
@tenant_id = tenant_id || ENV["TEAMS_TENANT_ID"]
|
|
@@ -21,6 +27,7 @@ module ChatSDK
|
|
|
21
27
|
@graph_client_secret = graph_client_secret || ENV["TEAMS_GRAPH_CLIENT_SECRET"]
|
|
22
28
|
@graph_tenant_id = graph_tenant_id || ENV["TEAMS_GRAPH_TENANT_ID"]
|
|
23
29
|
@graph_client = GraphClient.new(@graph_client_id, @graph_client_secret, @graph_tenant_id) if @graph_client_id
|
|
30
|
+
@default_service_url = service_url || ENV["TEAMS_SERVICE_URL"] || DEFAULT_SERVICE_URL
|
|
24
31
|
|
|
25
32
|
@client = BotFrameworkClient.new(app_id: @app_id, app_password: @app_password)
|
|
26
33
|
@jwt_verifier = JwtVerifier.new(app_id: @app_id)
|
|
@@ -28,6 +35,10 @@ module ChatSDK
|
|
|
28
35
|
@service_urls = {}
|
|
29
36
|
end
|
|
30
37
|
|
|
38
|
+
def set_state(state)
|
|
39
|
+
@state = state
|
|
40
|
+
end
|
|
41
|
+
|
|
31
42
|
def name
|
|
32
43
|
:teams
|
|
33
44
|
end
|
|
@@ -38,16 +49,17 @@ module ChatSDK
|
|
|
38
49
|
raise ChatSDK::SignatureVerificationError, "Missing authorization header" unless auth_header
|
|
39
50
|
|
|
40
51
|
token = auth_header.sub(/^Bearer\s+/i, "")
|
|
41
|
-
@jwt_verifier.verify!(token)
|
|
52
|
+
rack_request.env[VERIFIED_JWT_CLAIMS_ENV_KEY] = @jwt_verifier.verify!(token)
|
|
42
53
|
true
|
|
43
54
|
end
|
|
44
55
|
|
|
45
56
|
def parse_events(rack_request)
|
|
46
57
|
activity = read_json_body(rack_request)
|
|
47
58
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
59
|
+
conversation_id = activity.dig("conversation", "id")
|
|
60
|
+
claims = rack_request.env[VERIFIED_JWT_CLAIMS_ENV_KEY]
|
|
61
|
+
if conversation_id && trusted_service_url?(activity["serviceUrl"], claims)
|
|
62
|
+
register_service_url(conversation_id, activity["serviceUrl"])
|
|
51
63
|
end
|
|
52
64
|
|
|
53
65
|
ActivityParser.parse(activity, bot_app_id: @app_id)
|
|
@@ -61,7 +73,7 @@ module ChatSDK
|
|
|
61
73
|
service_url = service_url_for(channel_id)
|
|
62
74
|
|
|
63
75
|
activity = build_activity(msg)
|
|
64
|
-
activity["replyToId"] = thread_id if thread_id
|
|
76
|
+
activity["replyToId"] = activity_reply_id(channel_id, thread_id) if thread_id
|
|
65
77
|
|
|
66
78
|
result = @client.send_activity(
|
|
67
79
|
service_url: service_url,
|
|
@@ -73,14 +85,14 @@ module ChatSDK
|
|
|
73
85
|
id: result["id"],
|
|
74
86
|
text: msg.text || "",
|
|
75
87
|
author: ChatSDK::Author.new(id: @app_id, name: "bot", platform: :teams, bot: true),
|
|
76
|
-
thread_id: thread_id || result["id"],
|
|
88
|
+
thread_id: thread_id || channel_thread_id(channel_id, result["id"]),
|
|
77
89
|
channel_id: channel_id,
|
|
78
90
|
platform: :teams,
|
|
79
91
|
raw: result
|
|
80
92
|
)
|
|
81
93
|
end
|
|
82
94
|
|
|
83
|
-
def edit_message(channel_id:, message_id:, message
|
|
95
|
+
def edit_message(channel_id:, message_id:, message:, **_options)
|
|
84
96
|
require_capability!(:edit_messages)
|
|
85
97
|
msg = ChatSDK::PostableMessage.from(message)
|
|
86
98
|
service_url = service_url_for(channel_id)
|
|
@@ -96,7 +108,7 @@ module ChatSDK
|
|
|
96
108
|
)
|
|
97
109
|
end
|
|
98
110
|
|
|
99
|
-
def delete_message(channel_id:, message_id
|
|
111
|
+
def delete_message(channel_id:, message_id:, **_options)
|
|
100
112
|
require_capability!(:delete_messages)
|
|
101
113
|
service_url = service_url_for(channel_id)
|
|
102
114
|
|
|
@@ -126,7 +138,7 @@ module ChatSDK
|
|
|
126
138
|
"contentUrl" => "data:application/octet-stream;base64,#{content}"
|
|
127
139
|
}]
|
|
128
140
|
}
|
|
129
|
-
activity["replyToId"] = thread_id if thread_id
|
|
141
|
+
activity["replyToId"] = activity_reply_id(channel_id, thread_id) if thread_id
|
|
130
142
|
|
|
131
143
|
@client.send_activity(
|
|
132
144
|
service_url: service_url,
|
|
@@ -137,14 +149,7 @@ module ChatSDK
|
|
|
137
149
|
|
|
138
150
|
def open_dm(user_id)
|
|
139
151
|
require_capability!(:direct_messages)
|
|
140
|
-
|
|
141
|
-
service_url = @service_urls.values.first
|
|
142
|
-
unless service_url
|
|
143
|
-
raise ChatSDK::PlatformError.new(
|
|
144
|
-
"No service URL available. A Teams activity must be received first.",
|
|
145
|
-
adapter_name: :teams
|
|
146
|
-
)
|
|
147
|
-
end
|
|
152
|
+
service_url = @service_urls.values.first || @default_service_url
|
|
148
153
|
|
|
149
154
|
payload = {
|
|
150
155
|
"bot" => {"id" => @app_id},
|
|
@@ -154,7 +159,7 @@ module ChatSDK
|
|
|
154
159
|
|
|
155
160
|
result = @client.create_conversation(service_url: service_url, payload: payload)
|
|
156
161
|
conversation_id = result["id"]
|
|
157
|
-
|
|
162
|
+
register_service_url(conversation_id, service_url)
|
|
158
163
|
conversation_id
|
|
159
164
|
end
|
|
160
165
|
|
|
@@ -162,7 +167,7 @@ module ChatSDK
|
|
|
162
167
|
super
|
|
163
168
|
end
|
|
164
169
|
|
|
165
|
-
def start_typing(channel_id:, thread_id: nil)
|
|
170
|
+
def start_typing(channel_id:, thread_id: nil, message_id: nil)
|
|
166
171
|
service_url = service_url_for(channel_id)
|
|
167
172
|
@client.send_typing(
|
|
168
173
|
service_url: service_url,
|
|
@@ -191,7 +196,7 @@ module ChatSDK
|
|
|
191
196
|
"(TEAMS_GRAPH_CLIENT_ID, TEAMS_GRAPH_CLIENT_SECRET, TEAMS_GRAPH_TENANT_ID)"
|
|
192
197
|
end
|
|
193
198
|
|
|
194
|
-
result = @graph_client.fetch_messages(chat_id: channel_id, limit: limit)
|
|
199
|
+
result = @graph_client.fetch_messages(chat_id: channel_id, limit: limit, cursor: cursor)
|
|
195
200
|
messages = (result["value"] || []).map { |m| parse_graph_message(m, channel_id) }
|
|
196
201
|
next_link = result["@odata.nextLink"]
|
|
197
202
|
[messages, next_link]
|
|
@@ -200,6 +205,18 @@ module ChatSDK
|
|
|
200
205
|
# Store a service URL for a conversation (useful when sending proactive messages)
|
|
201
206
|
def register_service_url(conversation_id, service_url)
|
|
202
207
|
@service_urls[conversation_id] = service_url
|
|
208
|
+
@state&.set(service_url_key(conversation_id), service_url)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def webhook_response(events)
|
|
212
|
+
return unless events.any? { |event| event.raw.is_a?(Hash) && event.raw["type"] == "invoke" }
|
|
213
|
+
|
|
214
|
+
body = {
|
|
215
|
+
"statusCode" => 200,
|
|
216
|
+
"type" => "application/vnd.microsoft.activity.message",
|
|
217
|
+
"value" => "OK"
|
|
218
|
+
}
|
|
219
|
+
[200, {"content-type" => "application/json"}, [JSON.generate(body)]]
|
|
203
220
|
end
|
|
204
221
|
|
|
205
222
|
private
|
|
@@ -218,11 +235,25 @@ module ChatSDK
|
|
|
218
235
|
activity["text"] = postable_message.text
|
|
219
236
|
end
|
|
220
237
|
|
|
238
|
+
if postable_message.ai_generated
|
|
239
|
+
activity["entities"] = [{
|
|
240
|
+
"type" => "https://schema.org/Message",
|
|
241
|
+
"@type" => "Message",
|
|
242
|
+
"@context" => "https://schema.org",
|
|
243
|
+
"additionalType" => ["AIGeneratedContent"]
|
|
244
|
+
}]
|
|
245
|
+
end
|
|
246
|
+
|
|
221
247
|
activity
|
|
222
248
|
end
|
|
223
249
|
|
|
224
250
|
def service_url_for(channel_id)
|
|
225
251
|
url = @service_urls[channel_id]
|
|
252
|
+
if !url && @state
|
|
253
|
+
url = @state.get(service_url_key(channel_id))
|
|
254
|
+
@service_urls[channel_id] = url if url
|
|
255
|
+
end
|
|
256
|
+
url ||= @default_service_url
|
|
226
257
|
unless url
|
|
227
258
|
raise ChatSDK::PlatformError.new(
|
|
228
259
|
"No service URL for conversation #{channel_id}. Register it first or receive an activity.",
|
|
@@ -232,10 +263,61 @@ module ChatSDK
|
|
|
232
263
|
url
|
|
233
264
|
end
|
|
234
265
|
|
|
266
|
+
def service_url_key(conversation_id)
|
|
267
|
+
"chat_sdk:teams:service_url:#{conversation_id}"
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def trusted_service_url?(service_url, claims)
|
|
271
|
+
return false unless service_url.is_a?(String)
|
|
272
|
+
|
|
273
|
+
uri = URI.parse(service_url)
|
|
274
|
+
return false unless uri.scheme&.casecmp?("https") && uri.host
|
|
275
|
+
|
|
276
|
+
service_url_claim = claims.is_a?(Hash) ? claims["serviceurl"] : nil
|
|
277
|
+
service_url_claim.nil? ||
|
|
278
|
+
(service_url_claim.is_a?(String) &&
|
|
279
|
+
normalize_service_url(service_url) == normalize_service_url(service_url_claim))
|
|
280
|
+
rescue URI::InvalidURIError
|
|
281
|
+
false
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def normalize_service_url(service_url)
|
|
285
|
+
uri = URI.parse(service_url)
|
|
286
|
+
return unless uri.scheme && uri.host
|
|
287
|
+
|
|
288
|
+
uri.scheme = uri.scheme.downcase
|
|
289
|
+
uri.host = uri.host.downcase
|
|
290
|
+
uri.path = uri.path.delete_suffix("/")
|
|
291
|
+
uri.to_s
|
|
292
|
+
rescue URI::InvalidURIError
|
|
293
|
+
nil
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def channel_thread_id(channel_id, activity_id)
|
|
297
|
+
return activity_id unless channel_id.to_s.include?("@thread.tacv2") && activity_id
|
|
298
|
+
|
|
299
|
+
"#{channel_id};messageid=#{activity_id}"
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def activity_reply_id(channel_id, thread_id)
|
|
303
|
+
prefix = "#{channel_id};messageid="
|
|
304
|
+
thread_id.start_with?(prefix) ? thread_id.delete_prefix(prefix) : thread_id
|
|
305
|
+
end
|
|
306
|
+
|
|
235
307
|
def parse_graph_message(data, channel_id)
|
|
236
308
|
from = data["from"] || {}
|
|
237
309
|
user = from["user"] || from["application"] || {}
|
|
238
310
|
is_bot = !from["application"].nil?
|
|
311
|
+
attachments = Array(data["attachments"]).filter_map do |attachment|
|
|
312
|
+
next unless attachment["contentType"] == "reference" && attachment["contentUrl"]
|
|
313
|
+
|
|
314
|
+
begin
|
|
315
|
+
resolved = @graph_client.resolve_reference_attachment(content_url: attachment["contentUrl"])
|
|
316
|
+
resolved.merge("filename" => resolved["filename"] || attachment["name"])
|
|
317
|
+
rescue ChatSDK::PlatformError, ArgumentError, Faraday::Error
|
|
318
|
+
nil
|
|
319
|
+
end
|
|
320
|
+
end
|
|
239
321
|
|
|
240
322
|
ChatSDK::Message.new(
|
|
241
323
|
id: data["id"],
|
|
@@ -244,11 +326,13 @@ module ChatSDK
|
|
|
244
326
|
id: user["id"] || "unknown",
|
|
245
327
|
name: user["displayName"] || "unknown",
|
|
246
328
|
platform: :teams,
|
|
247
|
-
bot: is_bot
|
|
329
|
+
bot: is_bot,
|
|
330
|
+
me: is_bot && user["id"] == @app_id
|
|
248
331
|
),
|
|
249
332
|
thread_id: data["replyToId"] || data["id"],
|
|
250
333
|
channel_id: channel_id,
|
|
251
334
|
platform: :teams,
|
|
335
|
+
attachments: attachments,
|
|
252
336
|
timestamp: data["createdDateTime"],
|
|
253
337
|
raw: data
|
|
254
338
|
)
|
|
@@ -62,6 +62,7 @@ module ChatSDK
|
|
|
62
62
|
when :button then render_action_submit(node)
|
|
63
63
|
when :link_button then render_action_open_url(node)
|
|
64
64
|
when :select then render_select(node)
|
|
65
|
+
when :text_input then render_text_input(node)
|
|
65
66
|
when :table then render_table(node)
|
|
66
67
|
when :chart then render_chart(node)
|
|
67
68
|
end
|
|
@@ -142,7 +143,7 @@ module ChatSDK
|
|
|
142
143
|
action = {
|
|
143
144
|
"type" => "Action.Submit",
|
|
144
145
|
"title" => node.attributes[:text],
|
|
145
|
-
"data" => {"
|
|
146
|
+
"data" => {"actionId" => node.attributes[:id]}
|
|
146
147
|
}
|
|
147
148
|
action["data"]["value"] = node.attributes[:value] if node.attributes[:value]
|
|
148
149
|
action["tooltip"] = node.attributes[:tooltip] if node.attributes[:tooltip]
|
|
@@ -180,23 +181,65 @@ module ChatSDK
|
|
|
180
181
|
input
|
|
181
182
|
end
|
|
182
183
|
|
|
184
|
+
def render_text_input(node)
|
|
185
|
+
input = {
|
|
186
|
+
"type" => "Input.Text",
|
|
187
|
+
"id" => node.attributes[:id],
|
|
188
|
+
"label" => node.attributes[:label],
|
|
189
|
+
"isMultiline" => node.attributes[:multiline],
|
|
190
|
+
"isRequired" => !node.attributes[:optional]
|
|
191
|
+
}
|
|
192
|
+
input["placeholder"] = node.attributes[:placeholder] if node.attributes[:placeholder]
|
|
193
|
+
input["value"] = node.attributes[:value] unless node.attributes[:value].nil?
|
|
194
|
+
input
|
|
195
|
+
end
|
|
196
|
+
|
|
183
197
|
def render_table(node)
|
|
184
|
-
|
|
198
|
+
headers = Array(node.attributes[:headers])
|
|
199
|
+
data_rows = Array(node.attributes[:rows])
|
|
200
|
+
column_count = data_rows.reduce(headers.length) { |count, row| [count, Array(row).length].max }
|
|
201
|
+
return if column_count.zero?
|
|
202
|
+
|
|
203
|
+
alignment = {left: "Left", center: "Center", right: "Right"}
|
|
204
|
+
vertical_alignment = {top: "Top", center: "Center", bottom: "Bottom"}
|
|
205
|
+
widths = Array(node.attributes[:widths])
|
|
206
|
+
aligns = Array(node.attributes[:align])
|
|
207
|
+
rows = data_rows.map { |row| Array(row) }
|
|
208
|
+
rows.unshift(headers) unless headers.empty?
|
|
209
|
+
|
|
185
210
|
{
|
|
186
|
-
"type" => "
|
|
187
|
-
"
|
|
211
|
+
"type" => "Table",
|
|
212
|
+
"firstRowAsHeaders" => !headers.empty?,
|
|
213
|
+
"showGridLines" => node.attributes.fetch(:grid_lines, true),
|
|
214
|
+
"columns" => Array.new(column_count) do |index|
|
|
215
|
+
width = widths[index]
|
|
216
|
+
column_width = width
|
|
217
|
+
column_width = 1 unless column_width.is_a?(Integer) && column_width.positive?
|
|
218
|
+
column = {"width" => column_width}
|
|
219
|
+
column_alignment = alignment[aligns[index]&.to_sym]
|
|
220
|
+
column["horizontalCellContentAlignment"] = column_alignment if column_alignment
|
|
221
|
+
column
|
|
222
|
+
end,
|
|
223
|
+
"rows" => rows.each_with_index.map do |row, index|
|
|
188
224
|
{
|
|
189
|
-
"type" => "
|
|
190
|
-
"
|
|
225
|
+
"type" => "TableRow",
|
|
226
|
+
"cells" => Array.new(column_count) do |cell_index|
|
|
191
227
|
{
|
|
192
|
-
"type" => "
|
|
193
|
-
"
|
|
194
|
-
|
|
228
|
+
"type" => "TableCell",
|
|
229
|
+
"items" => [{
|
|
230
|
+
"type" => "TextBlock",
|
|
231
|
+
"text" => Array(row)[cell_index].to_s,
|
|
232
|
+
"weight" => ("Bolder" if index.zero? && !headers.empty?),
|
|
233
|
+
"wrap" => true
|
|
234
|
+
}.compact]
|
|
195
235
|
}
|
|
196
236
|
end
|
|
197
237
|
}
|
|
198
|
-
end
|
|
238
|
+
end,
|
|
239
|
+
"gridStyle" => node.attributes[:grid_style],
|
|
240
|
+
"verticalCellContentAlignment" => vertical_alignment[node.attributes[:vertical_align]&.to_sym]
|
|
199
241
|
}
|
|
242
|
+
.compact
|
|
200
243
|
end
|
|
201
244
|
|
|
202
245
|
def render_chart(node)
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "base64"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
3
6
|
module ChatSDK
|
|
4
7
|
module Teams
|
|
5
8
|
class GraphClient
|
|
@@ -15,9 +18,11 @@ module ChatSDK
|
|
|
15
18
|
@token_expires_at = nil
|
|
16
19
|
end
|
|
17
20
|
|
|
18
|
-
def fetch_messages(chat_id:, limit: 50)
|
|
21
|
+
def fetch_messages(chat_id:, limit: 50, cursor: nil)
|
|
22
|
+
messages_url = chat_messages_url(chat_id)
|
|
23
|
+
url = cursor || "#{messages_url}?$top=#{limit}&$orderby=createdDateTime desc"
|
|
24
|
+
validate_graph_url!(url, chat_id) if cursor
|
|
19
25
|
token = fetch_token
|
|
20
|
-
url = "#{GRAPH_BASE_URL}/chats/#{chat_id}/messages?$top=#{limit}&$orderby=createdDateTime desc"
|
|
21
26
|
|
|
22
27
|
response = api_connection.get(url) do |req|
|
|
23
28
|
req.headers["Authorization"] = "Bearer #{token}"
|
|
@@ -35,6 +40,27 @@ module ChatSDK
|
|
|
35
40
|
response.body
|
|
36
41
|
end
|
|
37
42
|
|
|
43
|
+
def resolve_reference_attachment(content_url:)
|
|
44
|
+
item = fetch_reference_item(content_url)
|
|
45
|
+
{
|
|
46
|
+
"url" => item["@microsoft.graph.downloadUrl"],
|
|
47
|
+
"mime_type" => item.dig("file", "mimeType") || "application/octet-stream",
|
|
48
|
+
"filename" => item["name"]
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def resolve_reference_url(content_url:)
|
|
53
|
+
resolve_reference_attachment(content_url: content_url).fetch("url")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def download_reference_attachment(content_url:)
|
|
57
|
+
download_url = resolve_reference_url(content_url: content_url)
|
|
58
|
+
uri = URI.parse(download_url)
|
|
59
|
+
raise ArgumentError, "Teams download URL must use HTTPS" unless uri.is_a?(URI::HTTPS)
|
|
60
|
+
|
|
61
|
+
Faraday.get(download_url)
|
|
62
|
+
end
|
|
63
|
+
|
|
38
64
|
def fetch_token
|
|
39
65
|
return @access_token if @access_token && @token_expires_at && Time.now < @token_expires_at
|
|
40
66
|
|
|
@@ -78,6 +104,52 @@ module ChatSDK
|
|
|
78
104
|
f.adapter :net_http
|
|
79
105
|
end
|
|
80
106
|
end
|
|
107
|
+
|
|
108
|
+
def fetch_reference_item(content_url)
|
|
109
|
+
uri = URI.parse(content_url)
|
|
110
|
+
raise ArgumentError, "Teams attachment URL must use HTTPS" unless uri.is_a?(URI::HTTPS)
|
|
111
|
+
|
|
112
|
+
token = fetch_token
|
|
113
|
+
share_id = "u!#{Base64.urlsafe_encode64(content_url, padding: false)}"
|
|
114
|
+
response = api_connection.get("#{GRAPH_BASE_URL}/shares/#{share_id}/driveItem") do |req|
|
|
115
|
+
req.headers["Authorization"] = "Bearer #{token}"
|
|
116
|
+
end
|
|
117
|
+
unless response.success?
|
|
118
|
+
raise ChatSDK::PlatformError.new(
|
|
119
|
+
"Graph API error: #{response.status}",
|
|
120
|
+
status: response.status,
|
|
121
|
+
body: response.body,
|
|
122
|
+
adapter_name: :teams
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
item = response.body
|
|
127
|
+
unless item["@microsoft.graph.downloadUrl"]
|
|
128
|
+
raise ChatSDK::PlatformError.new("Graph API did not return an attachment download URL", adapter_name: :teams)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
item
|
|
132
|
+
rescue URI::InvalidURIError
|
|
133
|
+
raise ArgumentError, "Invalid Teams attachment URL"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def chat_messages_url(chat_id)
|
|
137
|
+
"#{GRAPH_BASE_URL}/chats/#{URI::DEFAULT_PARSER.escape(chat_id.to_s, /[^A-Za-z0-9._~-]/)}/messages"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def validate_graph_url!(url, chat_id)
|
|
141
|
+
uri = URI.parse(url)
|
|
142
|
+
expected_path = URI::DEFAULT_PARSER.unescape(URI.parse(chat_messages_url(chat_id)).path)
|
|
143
|
+
actual_path = URI::DEFAULT_PARSER.unescape(uri.path)
|
|
144
|
+
valid = uri.is_a?(URI::HTTPS) && uri.host == "graph.microsoft.com" &&
|
|
145
|
+
uri.port == 443 && !uri.userinfo && !uri.fragment && actual_path == expected_path
|
|
146
|
+
unless valid
|
|
147
|
+
raise ArgumentError,
|
|
148
|
+
"Graph pagination cursor must use https://graph.microsoft.com and the requested chat path"
|
|
149
|
+
end
|
|
150
|
+
rescue URI::InvalidURIError
|
|
151
|
+
raise ArgumentError, "Invalid Graph pagination cursor"
|
|
152
|
+
end
|
|
81
153
|
end
|
|
82
154
|
end
|
|
83
155
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: chat_sdk-teams
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Quentin Rousseau
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '
|
|
18
|
+
version: '1.2'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '
|
|
25
|
+
version: '1.2'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: faraday
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|