teams_rb 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +47 -0
- data/LICENSE +21 -0
- data/README.md +500 -0
- data/docs/README.md +40 -0
- data/docs/essentials/README.md +13 -0
- data/docs/essentials/api-client.md +62 -0
- data/docs/essentials/app-authentication.md +27 -0
- data/docs/essentials/app-basics.md +50 -0
- data/docs/essentials/graph.md +41 -0
- data/docs/essentials/on-activity.md +72 -0
- data/docs/essentials/on-event.md +29 -0
- data/docs/essentials/proactive-messaging.md +44 -0
- data/docs/essentials/sending-messages.md +76 -0
- data/docs/essentials/sovereign-cloud.md +26 -0
- data/docs/getting-started/README.md +7 -0
- data/docs/getting-started/code-basics.md +61 -0
- data/docs/getting-started/quickstart.md +54 -0
- data/docs/getting-started/running-in-teams.md +44 -0
- data/docs/in-depth-guides/README.md +14 -0
- data/docs/in-depth-guides/adaptive-cards.md +62 -0
- data/docs/in-depth-guides/dialogs.md +77 -0
- data/docs/in-depth-guides/feedback.md +29 -0
- data/docs/in-depth-guides/meeting-events.md +27 -0
- data/docs/in-depth-guides/message-extensions.md +77 -0
- data/docs/in-depth-guides/message-reactions.md +29 -0
- data/docs/in-depth-guides/observability.md +28 -0
- data/docs/in-depth-guides/streaming.md +52 -0
- data/docs/in-depth-guides/tabs.md +59 -0
- data/docs/in-depth-guides/user-authentication.md +60 -0
- data/lib/teams/activity.rb +160 -0
- data/lib/teams/activity_context.rb +278 -0
- data/lib/teams/api/account.rb +90 -0
- data/lib/teams/api/activity_value.rb +49 -0
- data/lib/teams/api/bot_sign_in_client.rb +54 -0
- data/lib/teams/api/channel_data.rb +86 -0
- data/lib/teams/api/channel_info.rb +19 -0
- data/lib/teams/api/citation_appearance.rb +83 -0
- data/lib/teams/api/client.rb +28 -0
- data/lib/teams/api/conversation_account.rb +40 -0
- data/lib/teams/api/conversation_client.rb +156 -0
- data/lib/teams/api/conversation_reference.rb +83 -0
- data/lib/teams/api/conversation_resource.rb +19 -0
- data/lib/teams/api/meeting_client.rb +57 -0
- data/lib/teams/api/meeting_info.rb +26 -0
- data/lib/teams/api/meeting_notification_response.rb +29 -0
- data/lib/teams/api/meeting_participant.rb +33 -0
- data/lib/teams/api/message_activity.rb +201 -0
- data/lib/teams/api/message_extension.rb +110 -0
- data/lib/teams/api/model.rb +49 -0
- data/lib/teams/api/notification_info.rb +28 -0
- data/lib/teams/api/paged_members_result.rb +16 -0
- data/lib/teams/api/quoted_reply_entity.rb +65 -0
- data/lib/teams/api/reaction_client.rb +34 -0
- data/lib/teams/api/sent_activity.rb +48 -0
- data/lib/teams/api/task_module.rb +83 -0
- data/lib/teams/api/team_client.rb +45 -0
- data/lib/teams/api/team_details.rb +36 -0
- data/lib/teams/api/team_info.rb +44 -0
- data/lib/teams/api/tenant_info.rb +11 -0
- data/lib/teams/api/token.rb +81 -0
- data/lib/teams/api/typing_activity.rb +19 -0
- data/lib/teams/api/user_client.rb +83 -0
- data/lib/teams/app.rb +602 -0
- data/lib/teams/auth/client_secret_credentials.rb +7 -0
- data/lib/teams/auth/jwt_validator.rb +148 -0
- data/lib/teams/auth/token.rb +39 -0
- data/lib/teams/auth/token_manager.rb +68 -0
- data/lib/teams/cards/generated.rb +1764 -0
- data/lib/teams/cards/generated_base.rb +105 -0
- data/lib/teams/cards.rb +81 -0
- data/lib/teams/cloud_environment.rb +41 -0
- data/lib/teams/common/hashes.rb +20 -0
- data/lib/teams/common/http_client.rb +111 -0
- data/lib/teams/common/retry.rb +31 -0
- data/lib/teams/errors.rb +50 -0
- data/lib/teams/function_context.rb +82 -0
- data/lib/teams/graph/client.rb +73 -0
- data/lib/teams/http_stream.rb +460 -0
- data/lib/teams/rack_app.rb +62 -0
- data/lib/teams/response.rb +9 -0
- data/lib/teams/router.rb +171 -0
- data/lib/teams/storage/memory_store.rb +27 -0
- data/lib/teams/version.rb +5 -0
- data/lib/teams.rb +70 -0
- metadata +217 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Teams
|
|
7
|
+
class ActivityContext
|
|
8
|
+
attr_reader :app, :activity, :conversation_reference, :extra, :stream
|
|
9
|
+
|
|
10
|
+
def initialize(app:, activity:, conversation_reference:, extra: {}, stream: nil)
|
|
11
|
+
@app = app
|
|
12
|
+
@activity = activity
|
|
13
|
+
@conversation_reference = conversation_reference
|
|
14
|
+
@extra = extra
|
|
15
|
+
@stream = stream
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def ref
|
|
19
|
+
conversation_reference
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def reference
|
|
23
|
+
conversation_reference
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def log
|
|
27
|
+
app.logger
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def storage
|
|
31
|
+
app.storage
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def api
|
|
35
|
+
app.api
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The TypeScript, Python, and .NET SDKs call this operation `send`.
|
|
39
|
+
# Ruby already defines Object#send for dynamic dispatch, so the public
|
|
40
|
+
# Ruby API uses `post` to avoid shadowing a core language method.
|
|
41
|
+
def post(activity_or_text)
|
|
42
|
+
app.send_activity(conversation_reference, apply_targeted_defaults(activity_or_text))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def reply(activity_or_text)
|
|
46
|
+
return post(activity_or_text) unless activity.id
|
|
47
|
+
# Replies to targeted inbound messages are targeted sends: no quoted
|
|
48
|
+
# reply and no replyToId, matching the TypeScript and Python SDKs.
|
|
49
|
+
return post(activity_or_text) if targeted_reply?(activity_or_text)
|
|
50
|
+
|
|
51
|
+
quote(activity.id, activity_or_text)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Quoted replies are plain sends carrying the quote placeholder and
|
|
55
|
+
# entity, matching the TypeScript and Python SDKs; no replyToId is set.
|
|
56
|
+
def quote(message_id, activity_or_text)
|
|
57
|
+
post(quoted_activity(message_id, activity_or_text))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Sugar over post: the SDKs update by sending an activity that already
|
|
61
|
+
# carries an id, and post does exactly that. See AGENTS.md.
|
|
62
|
+
def update(activity_id, activity_or_text)
|
|
63
|
+
post(activity_with_id(activity_id, activity_or_text))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def typing(text = nil)
|
|
67
|
+
post(Api::TypingActivity.new(text))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Microsoft Graph client with the app's own identity (app-only tokens).
|
|
71
|
+
def app_graph
|
|
72
|
+
app.graph
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Microsoft Graph client with the signed-in user's token. Raises when
|
|
76
|
+
# the user is not signed in, like the Python SDK's user_graph.
|
|
77
|
+
def user_graph(connection_name: nil)
|
|
78
|
+
connection = connection_name || app.default_connection_name
|
|
79
|
+
@user_graph ||= {}
|
|
80
|
+
@user_graph[connection] ||= begin
|
|
81
|
+
response = api.users.get_token(
|
|
82
|
+
user_id: activity.from.id,
|
|
83
|
+
connection_name: connection,
|
|
84
|
+
channel_id: activity.channel_id
|
|
85
|
+
)
|
|
86
|
+
raise Error, "User must be signed in to access the Graph client" if response.token.to_s.empty?
|
|
87
|
+
|
|
88
|
+
Graph::Client.new(token: response.token, base_url_root: app.graph.base_url_root)
|
|
89
|
+
rescue HttpError
|
|
90
|
+
raise Error, "User must be signed in to access the Graph client"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Starts the user sign-in flow: returns the token if the user is already
|
|
95
|
+
# signed in, otherwise sends an OAuth card and returns nil. In group
|
|
96
|
+
# conversations the card goes to a 1:1 conversation with the user (group
|
|
97
|
+
# OAuth is not supported by Teams), like the TypeScript/Python SDKs.
|
|
98
|
+
def sign_in(connection_name: nil, oauth_card_text: "Please Sign In...", sign_in_button_text: "Sign In")
|
|
99
|
+
connection_name ||= app.default_connection_name
|
|
100
|
+
|
|
101
|
+
begin
|
|
102
|
+
response = api.users.get_token(
|
|
103
|
+
user_id: activity.from.id,
|
|
104
|
+
connection_name:,
|
|
105
|
+
channel_id: activity.channel_id
|
|
106
|
+
)
|
|
107
|
+
return response.token if response.token && !response.token.to_s.empty?
|
|
108
|
+
rescue HttpError
|
|
109
|
+
# No token yet; continue with the OAuth card flow.
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
conversation_id = conversation_reference.conversation_id
|
|
113
|
+
if activity.conversation.is_group
|
|
114
|
+
one_on_one = api.conversations.create(
|
|
115
|
+
members: [activity.from.to_h],
|
|
116
|
+
tenant_id: activity.conversation.tenant_id
|
|
117
|
+
)
|
|
118
|
+
conversation_id = one_on_one.id
|
|
119
|
+
# Deliberately posts the plain text notice into the group (matching
|
|
120
|
+
# TypeScript and Python): group chats don't support SSO, so the card
|
|
121
|
+
# itself goes to the 1:1 below while the group sees only the notice.
|
|
122
|
+
post(oauth_card_text)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
state = Base64.strict_encode64(JSON.generate(
|
|
126
|
+
"connectionName" => connection_name,
|
|
127
|
+
"conversation" => conversation_reference.to_h,
|
|
128
|
+
"msAppId" => app.client_id
|
|
129
|
+
))
|
|
130
|
+
resource = api.bots.sign_in.get_resource(state:)
|
|
131
|
+
|
|
132
|
+
card = {
|
|
133
|
+
"text" => oauth_card_text,
|
|
134
|
+
"connectionName" => connection_name,
|
|
135
|
+
"tokenExchangeResource" => resource.token_exchange_resource&.to_h,
|
|
136
|
+
"tokenPostResource" => resource.token_post_resource&.to_h,
|
|
137
|
+
"buttons" => [
|
|
138
|
+
{ "type" => "signin", "title" => sign_in_button_text, "value" => resource.sign_in_link }
|
|
139
|
+
]
|
|
140
|
+
}.compact
|
|
141
|
+
|
|
142
|
+
payload = {
|
|
143
|
+
"type" => "message",
|
|
144
|
+
"recipient" => activity.from.to_h,
|
|
145
|
+
"attachments" => [
|
|
146
|
+
{ "contentType" => "application/vnd.microsoft.card.oauth", "content" => card }
|
|
147
|
+
]
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
app.send_activity(sign_in_reference(conversation_id), payload)
|
|
151
|
+
nil
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Clears the user's token for the connection. Failures are logged, not
|
|
155
|
+
# raised, matching the TypeScript/Python SDKs.
|
|
156
|
+
def sign_out(connection_name: nil)
|
|
157
|
+
api.users.sign_out(
|
|
158
|
+
user_id: activity.from.id,
|
|
159
|
+
connection_name: connection_name || app.default_connection_name,
|
|
160
|
+
channel_id: activity.channel_id
|
|
161
|
+
)
|
|
162
|
+
nil
|
|
163
|
+
rescue HttpError => error
|
|
164
|
+
log&.error("Failed to sign out user: #{error.message}")
|
|
165
|
+
nil
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
private
|
|
169
|
+
|
|
170
|
+
def sign_in_reference(conversation_id)
|
|
171
|
+
return conversation_reference if conversation_id == conversation_reference.conversation_id
|
|
172
|
+
|
|
173
|
+
Api::ConversationReference.from_h(
|
|
174
|
+
conversation_reference.to_h.merge(
|
|
175
|
+
"conversation" => conversation_reference.conversation.to_h.merge("id" => conversation_id)
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def incoming_targeted_sender
|
|
181
|
+
return nil unless activity.message?
|
|
182
|
+
return nil unless activity.recipient.is_targeted == true
|
|
183
|
+
|
|
184
|
+
activity.from
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def apply_targeted_defaults(activity_or_text)
|
|
188
|
+
sender = incoming_targeted_sender
|
|
189
|
+
return activity_or_text unless sender
|
|
190
|
+
|
|
191
|
+
body = activity_body(activity_or_text)
|
|
192
|
+
return activity_or_text unless body
|
|
193
|
+
|
|
194
|
+
if body["type"] == "message" && !body["id"] && !body.key?("recipient")
|
|
195
|
+
body["recipient"] = sender.to_h.merge("isTargeted" => true)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
return body unless targeted_outbound?(body)
|
|
199
|
+
|
|
200
|
+
add_targeted_message_info(body, activity.id)
|
|
201
|
+
body
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def targeted_reply?(activity_or_text)
|
|
205
|
+
return false unless incoming_targeted_sender
|
|
206
|
+
|
|
207
|
+
body = activity_body(activity_or_text)
|
|
208
|
+
return false unless body
|
|
209
|
+
return false unless body["type"] == "message"
|
|
210
|
+
|
|
211
|
+
(!body["id"] && !body.key?("recipient")) || targeted_outbound?(body)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def targeted_outbound?(body)
|
|
215
|
+
body["type"] == "message" && body.dig("recipient", "isTargeted") == true
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def activity_body(activity_or_text)
|
|
219
|
+
outbound = normalize_activity(activity_or_text)
|
|
220
|
+
body = outbound.respond_to?(:to_h) ? outbound.to_h : outbound
|
|
221
|
+
body.is_a?(Hash) ? body.dup : nil
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Hash counterpart of MessageActivity#add_targeted_message_info: strips
|
|
225
|
+
# quotedReply artifacts and adds the prompt preview entity once.
|
|
226
|
+
def add_targeted_message_info(body, message_id)
|
|
227
|
+
entities = Array(body["entities"]).reject do |entity|
|
|
228
|
+
entity.is_a?(Hash) && entity["type"] == "quotedReply"
|
|
229
|
+
end
|
|
230
|
+
body["text"] = body["text"].gsub(%(<quoted messageId="#{message_id}"/>), "").strip if body["text"]
|
|
231
|
+
|
|
232
|
+
unless entities.any? { |entity| entity.is_a?(Hash) && entity["type"] == "targetedMessageInfo" }
|
|
233
|
+
entities << { "type" => "targetedMessageInfo", "messageId" => message_id }
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
body["entities"] = entities
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def activity_with_id(activity_id, activity_or_text)
|
|
240
|
+
outbound = normalize_activity(activity_or_text)
|
|
241
|
+
body = outbound.respond_to?(:to_h) ? outbound.to_h : outbound
|
|
242
|
+
body.merge("id" => activity_id)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def quoted_activity(message_id, activity_or_text)
|
|
246
|
+
outbound = normalize_activity(activity_or_text)
|
|
247
|
+
return outbound.prepend_quote(message_id) if outbound.is_a?(Api::MessageActivity)
|
|
248
|
+
|
|
249
|
+
body = outbound.respond_to?(:to_h) ? outbound.to_h : outbound
|
|
250
|
+
return body unless body.is_a?(Hash)
|
|
251
|
+
|
|
252
|
+
body = body.dup
|
|
253
|
+
prepend_quote_to_hash(body, message_id) if body["type"] == "message"
|
|
254
|
+
body
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def prepend_quote_to_hash(body, message_id)
|
|
258
|
+
body["entities"] = Array(body["entities"]) + [
|
|
259
|
+
Api::QuotedReplyEntity.new("quotedReply" => { "messageId" => message_id }).to_h
|
|
260
|
+
]
|
|
261
|
+
placeholder = %(<quoted messageId="#{message_id}"/>)
|
|
262
|
+
body["text"] = body["text"].to_s.strip.empty? ? placeholder : "#{placeholder} #{body["text"]}"
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def normalize_activity(activity_or_text)
|
|
266
|
+
case activity_or_text
|
|
267
|
+
when String
|
|
268
|
+
Api::MessageActivity.new(activity_or_text)
|
|
269
|
+
when Cards::AdaptiveCard
|
|
270
|
+
Api::MessageActivity.new.add_card(activity_or_text)
|
|
271
|
+
when Hash
|
|
272
|
+
Common::Hashes.deep_stringify_keys(activity_or_text)
|
|
273
|
+
else
|
|
274
|
+
activity_or_text
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teams
|
|
4
|
+
module Api
|
|
5
|
+
class Account < Model
|
|
6
|
+
def id
|
|
7
|
+
read("id")
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def name
|
|
11
|
+
read("name")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def aad_object_id
|
|
15
|
+
read("aadObjectId", "objectId", "aad_object_id")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def type
|
|
19
|
+
read("type")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def properties
|
|
23
|
+
read("properties")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def is_targeted
|
|
27
|
+
read("isTargeted", "is_targeted")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def role
|
|
31
|
+
read("role")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def user_role
|
|
35
|
+
read("userRole", "user_role")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def given_name
|
|
39
|
+
read("givenName", "given_name")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def surname
|
|
43
|
+
read("surname")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def email
|
|
47
|
+
read("email")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def user_principal_name
|
|
51
|
+
read("userPrincipalName", "user_principal_name")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def tenant_id
|
|
55
|
+
read("tenantId", "tenant_id")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def to_h
|
|
59
|
+
body = raw.dup
|
|
60
|
+
body["id"] = id if id
|
|
61
|
+
body["name"] = name if name
|
|
62
|
+
body["aadObjectId"] = aad_object_id if aad_object_id
|
|
63
|
+
body["type"] = type if type
|
|
64
|
+
body["properties"] = properties if properties
|
|
65
|
+
body["isTargeted"] = is_targeted unless is_targeted.nil?
|
|
66
|
+
body["role"] = role if role
|
|
67
|
+
body["userRole"] = user_role if user_role
|
|
68
|
+
body["givenName"] = given_name if given_name
|
|
69
|
+
body["surname"] = surname if surname
|
|
70
|
+
body["email"] = email if email
|
|
71
|
+
body["userPrincipalName"] = user_principal_name if user_principal_name
|
|
72
|
+
body["tenantId"] = tenant_id if tenant_id
|
|
73
|
+
remove_aliases(body)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def remove_aliases(body)
|
|
79
|
+
body.delete("objectId")
|
|
80
|
+
body.delete("aad_object_id")
|
|
81
|
+
body.delete("is_targeted")
|
|
82
|
+
body.delete("user_role")
|
|
83
|
+
body.delete("given_name")
|
|
84
|
+
body.delete("user_principal_name")
|
|
85
|
+
body.delete("tenant_id")
|
|
86
|
+
body
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teams
|
|
4
|
+
module Api
|
|
5
|
+
class ActivityValue < Model
|
|
6
|
+
# The invoke payload's data, e.g. dialog submit form values plus the
|
|
7
|
+
# card action's data (dialog_id / action routing keys).
|
|
8
|
+
def data
|
|
9
|
+
read("data")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# The message extension command that fired (query/submit/fetchTask).
|
|
13
|
+
def command_id
|
|
14
|
+
read("commandId", "command_id")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Message extension query parameters: [{"name" => ..., "value" => ...}].
|
|
18
|
+
def parameters
|
|
19
|
+
Array(read("parameters"))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Meeting start/end event fields. Teams sends these PascalCase on the
|
|
23
|
+
# wire (Id, JoinUrl, Title, MeetingType, StartTime, EndTime).
|
|
24
|
+
def id
|
|
25
|
+
read("Id", "id")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def title
|
|
29
|
+
read("Title", "title")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def meeting_type
|
|
33
|
+
read("MeetingType", "meetingType", "meeting_type")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def join_url
|
|
37
|
+
read("JoinUrl", "joinUrl", "join_url")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def start_time
|
|
41
|
+
read("StartTime", "startTime", "start_time")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def end_time
|
|
45
|
+
read("EndTime", "endTime", "end_time")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Teams
|
|
6
|
+
module Api
|
|
7
|
+
# Bot sign-in resources from the Bot Framework token service, used to
|
|
8
|
+
# build OAuth cards. Exposed as api.bots.sign_in, following .NET's
|
|
9
|
+
# non-deprecated Bots client (TypeScript/Python deprecate their bot
|
|
10
|
+
# client but still call sign-in through it internally).
|
|
11
|
+
class BotSignInClient
|
|
12
|
+
attr_reader :oauth_url, :http
|
|
13
|
+
|
|
14
|
+
def initialize(oauth_url:, http:, logger: nil)
|
|
15
|
+
@oauth_url = oauth_url.sub(%r{/+\z}, "")
|
|
16
|
+
@http = http
|
|
17
|
+
@logger = logger
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Returns the sign-in URL as a plain string.
|
|
21
|
+
def get_url(state:, code_challenge: nil, emulator_url: nil, final_redirect: nil)
|
|
22
|
+
url = endpoint("api/botsignin/GetSignInUrl", state:, code_challenge:, emulator_url:, final_redirect:)
|
|
23
|
+
@logger&.debug("Teams API GET #{url}")
|
|
24
|
+
http.get(url)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def get_resource(state:, code_challenge: nil, emulator_url: nil, final_redirect: nil)
|
|
28
|
+
url = endpoint("api/botsignin/GetSignInResource", state:, code_challenge:, emulator_url:, final_redirect:)
|
|
29
|
+
@logger&.debug("Teams API GET #{url}")
|
|
30
|
+
SignInUrlResponse.new(http.get(url))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def endpoint(path, state:, code_challenge:, emulator_url:, final_redirect:)
|
|
36
|
+
params = {
|
|
37
|
+
"state" => state,
|
|
38
|
+
"codeChallenge" => code_challenge,
|
|
39
|
+
"emulatorUrl" => emulator_url,
|
|
40
|
+
"finalRedirect" => final_redirect
|
|
41
|
+
}.compact
|
|
42
|
+
"#{oauth_url}/#{path}?#{URI.encode_www_form(params)}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class BotClient
|
|
47
|
+
attr_reader :sign_in
|
|
48
|
+
|
|
49
|
+
def initialize(oauth_url:, http:, logger: nil)
|
|
50
|
+
@sign_in = BotSignInClient.new(oauth_url:, http:, logger:)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teams
|
|
4
|
+
module Api
|
|
5
|
+
class ChannelData < Model
|
|
6
|
+
def tenant
|
|
7
|
+
wrap(read("tenant"), TenantInfo)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def team
|
|
11
|
+
wrap(read("team"), TeamInfo)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def channel
|
|
15
|
+
wrap(read("channel"), ChannelInfo)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def meeting
|
|
19
|
+
wrap(read("meeting"), MeetingInfo)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def notification
|
|
23
|
+
wrap(read("notification"), NotificationInfo)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def event_type
|
|
27
|
+
read("eventType", "event_type")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def settings
|
|
31
|
+
value = read("settings")
|
|
32
|
+
value.is_a?(Hash) ? ActivityValue.new(value) : value
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def feedback_loop_enabled
|
|
36
|
+
read("feedbackLoopEnabled", "feedback_loop_enabled")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def feedback_loop
|
|
40
|
+
value = read("feedbackLoop", "feedback_loop")
|
|
41
|
+
value.is_a?(Hash) ? ActivityValue.new(value) : value
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def stream_id
|
|
45
|
+
read("streamId", "stream_id")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def stream_type
|
|
49
|
+
read("streamType", "stream_type")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def stream_sequence
|
|
53
|
+
read("streamSequence", "stream_sequence")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def to_h
|
|
57
|
+
body = raw.dup
|
|
58
|
+
body["tenant"] = tenant.to_h if tenant
|
|
59
|
+
body["team"] = team.to_h if team
|
|
60
|
+
body["channel"] = channel.to_h if channel
|
|
61
|
+
body["meeting"] = meeting.to_h if meeting
|
|
62
|
+
body["notification"] = notification.to_h if notification
|
|
63
|
+
body["eventType"] = event_type if event_type
|
|
64
|
+
body["settings"] = settings.to_h if settings.respond_to?(:to_h)
|
|
65
|
+
body["feedbackLoopEnabled"] = feedback_loop_enabled unless feedback_loop_enabled.nil?
|
|
66
|
+
body["feedbackLoop"] = feedback_loop.to_h if feedback_loop.respond_to?(:to_h)
|
|
67
|
+
body["streamId"] = stream_id if stream_id
|
|
68
|
+
body["streamType"] = stream_type if stream_type
|
|
69
|
+
body["streamSequence"] = stream_sequence if stream_sequence
|
|
70
|
+
body.delete("event_type")
|
|
71
|
+
body.delete("feedback_loop_enabled")
|
|
72
|
+
body.delete("feedback_loop")
|
|
73
|
+
body.delete("stream_id")
|
|
74
|
+
body.delete("stream_type")
|
|
75
|
+
body.delete("stream_sequence")
|
|
76
|
+
body
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def wrap(value, klass)
|
|
82
|
+
value ? klass.new(value) : nil
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teams
|
|
4
|
+
module Api
|
|
5
|
+
class CitationAppearance < Model
|
|
6
|
+
ADAPTIVE_CARD_CONTENT_TYPE = "application/vnd.microsoft.card.adaptive"
|
|
7
|
+
|
|
8
|
+
def initialize(raw = nil, name: nil, text: nil, url: nil, abstract: nil, icon: nil, keywords: nil, usage_info: nil)
|
|
9
|
+
body = raw || {}
|
|
10
|
+
body = body.to_h if body.respond_to?(:to_h)
|
|
11
|
+
body = body.merge({
|
|
12
|
+
"name" => name,
|
|
13
|
+
"text" => text,
|
|
14
|
+
"url" => url,
|
|
15
|
+
"abstract" => abstract,
|
|
16
|
+
"icon" => icon,
|
|
17
|
+
"keywords" => keywords,
|
|
18
|
+
"usageInfo" => usage_info
|
|
19
|
+
}.compact)
|
|
20
|
+
super(body)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def name
|
|
24
|
+
read("name")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def text
|
|
28
|
+
read("text")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def url
|
|
32
|
+
read("url")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def abstract
|
|
36
|
+
read("abstract")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def icon
|
|
40
|
+
read("icon")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def keywords
|
|
44
|
+
read("keywords")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def usage_info
|
|
48
|
+
read("usageInfo", "usage_info")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def to_h
|
|
52
|
+
validate!
|
|
53
|
+
|
|
54
|
+
body = {
|
|
55
|
+
"@type" => "DigitalDocument",
|
|
56
|
+
"name" => name,
|
|
57
|
+
"abstract" => abstract
|
|
58
|
+
}
|
|
59
|
+
body["text"] = text if text
|
|
60
|
+
body["url"] = url if url
|
|
61
|
+
body["encodingFormat"] = ADAPTIVE_CARD_CONTENT_TYPE if text && !text.empty?
|
|
62
|
+
body["image"] = { "@type" => "ImageObject", "name" => icon } if icon
|
|
63
|
+
body["keywords"] = keywords if keywords
|
|
64
|
+
body["usageInfo"] = usage_info.to_h if usage_info && usage_info.respond_to?(:to_h)
|
|
65
|
+
body["usageInfo"] = usage_info if usage_info && !usage_info.respond_to?(:to_h)
|
|
66
|
+
body
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def validate!
|
|
72
|
+
validate_required!("name", name)
|
|
73
|
+
validate_required!("abstract", abstract)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def validate_required!(field, value)
|
|
77
|
+
return unless value.nil?
|
|
78
|
+
|
|
79
|
+
raise ArgumentError, "citation #{field} is required"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teams
|
|
4
|
+
module Api
|
|
5
|
+
class Client
|
|
6
|
+
attr_reader :service_url, :http
|
|
7
|
+
|
|
8
|
+
DEFAULT_OAUTH_URL = "https://token.botframework.com"
|
|
9
|
+
|
|
10
|
+
attr_reader :conversations, :teams, :meetings, :users, :bots
|
|
11
|
+
|
|
12
|
+
# Sub-clients are constructed eagerly like the other SDKs' ApiClient
|
|
13
|
+
# constructors, which also keeps the shared client thread-safe.
|
|
14
|
+
# oauth_url is the Bot Framework token service (cloud-dependent), used
|
|
15
|
+
# by the user token and bot sign-in clients.
|
|
16
|
+
def initialize(service_url:, http:, logger: nil, oauth_url: DEFAULT_OAUTH_URL)
|
|
17
|
+
@service_url = service_url.sub(%r{/+\z}, "")
|
|
18
|
+
@http = http
|
|
19
|
+
@logger = logger
|
|
20
|
+
@conversations = ConversationClient.new(service_url: @service_url, http:, logger:)
|
|
21
|
+
@teams = TeamClient.new(service_url: @service_url, http:, logger:)
|
|
22
|
+
@meetings = MeetingClient.new(service_url: @service_url, http:, logger:)
|
|
23
|
+
@users = UserClient.new(oauth_url:, http:, logger:)
|
|
24
|
+
@bots = BotClient.new(oauth_url:, http:, logger:)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|