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.
Files changed (86) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +47 -0
  3. data/LICENSE +21 -0
  4. data/README.md +500 -0
  5. data/docs/README.md +40 -0
  6. data/docs/essentials/README.md +13 -0
  7. data/docs/essentials/api-client.md +62 -0
  8. data/docs/essentials/app-authentication.md +27 -0
  9. data/docs/essentials/app-basics.md +50 -0
  10. data/docs/essentials/graph.md +41 -0
  11. data/docs/essentials/on-activity.md +72 -0
  12. data/docs/essentials/on-event.md +29 -0
  13. data/docs/essentials/proactive-messaging.md +44 -0
  14. data/docs/essentials/sending-messages.md +76 -0
  15. data/docs/essentials/sovereign-cloud.md +26 -0
  16. data/docs/getting-started/README.md +7 -0
  17. data/docs/getting-started/code-basics.md +61 -0
  18. data/docs/getting-started/quickstart.md +54 -0
  19. data/docs/getting-started/running-in-teams.md +44 -0
  20. data/docs/in-depth-guides/README.md +14 -0
  21. data/docs/in-depth-guides/adaptive-cards.md +62 -0
  22. data/docs/in-depth-guides/dialogs.md +77 -0
  23. data/docs/in-depth-guides/feedback.md +29 -0
  24. data/docs/in-depth-guides/meeting-events.md +27 -0
  25. data/docs/in-depth-guides/message-extensions.md +77 -0
  26. data/docs/in-depth-guides/message-reactions.md +29 -0
  27. data/docs/in-depth-guides/observability.md +28 -0
  28. data/docs/in-depth-guides/streaming.md +52 -0
  29. data/docs/in-depth-guides/tabs.md +59 -0
  30. data/docs/in-depth-guides/user-authentication.md +60 -0
  31. data/lib/teams/activity.rb +160 -0
  32. data/lib/teams/activity_context.rb +278 -0
  33. data/lib/teams/api/account.rb +90 -0
  34. data/lib/teams/api/activity_value.rb +49 -0
  35. data/lib/teams/api/bot_sign_in_client.rb +54 -0
  36. data/lib/teams/api/channel_data.rb +86 -0
  37. data/lib/teams/api/channel_info.rb +19 -0
  38. data/lib/teams/api/citation_appearance.rb +83 -0
  39. data/lib/teams/api/client.rb +28 -0
  40. data/lib/teams/api/conversation_account.rb +40 -0
  41. data/lib/teams/api/conversation_client.rb +156 -0
  42. data/lib/teams/api/conversation_reference.rb +83 -0
  43. data/lib/teams/api/conversation_resource.rb +19 -0
  44. data/lib/teams/api/meeting_client.rb +57 -0
  45. data/lib/teams/api/meeting_info.rb +26 -0
  46. data/lib/teams/api/meeting_notification_response.rb +29 -0
  47. data/lib/teams/api/meeting_participant.rb +33 -0
  48. data/lib/teams/api/message_activity.rb +201 -0
  49. data/lib/teams/api/message_extension.rb +110 -0
  50. data/lib/teams/api/model.rb +49 -0
  51. data/lib/teams/api/notification_info.rb +28 -0
  52. data/lib/teams/api/paged_members_result.rb +16 -0
  53. data/lib/teams/api/quoted_reply_entity.rb +65 -0
  54. data/lib/teams/api/reaction_client.rb +34 -0
  55. data/lib/teams/api/sent_activity.rb +48 -0
  56. data/lib/teams/api/task_module.rb +83 -0
  57. data/lib/teams/api/team_client.rb +45 -0
  58. data/lib/teams/api/team_details.rb +36 -0
  59. data/lib/teams/api/team_info.rb +44 -0
  60. data/lib/teams/api/tenant_info.rb +11 -0
  61. data/lib/teams/api/token.rb +81 -0
  62. data/lib/teams/api/typing_activity.rb +19 -0
  63. data/lib/teams/api/user_client.rb +83 -0
  64. data/lib/teams/app.rb +602 -0
  65. data/lib/teams/auth/client_secret_credentials.rb +7 -0
  66. data/lib/teams/auth/jwt_validator.rb +148 -0
  67. data/lib/teams/auth/token.rb +39 -0
  68. data/lib/teams/auth/token_manager.rb +68 -0
  69. data/lib/teams/cards/generated.rb +1764 -0
  70. data/lib/teams/cards/generated_base.rb +105 -0
  71. data/lib/teams/cards.rb +81 -0
  72. data/lib/teams/cloud_environment.rb +41 -0
  73. data/lib/teams/common/hashes.rb +20 -0
  74. data/lib/teams/common/http_client.rb +111 -0
  75. data/lib/teams/common/retry.rb +31 -0
  76. data/lib/teams/errors.rb +50 -0
  77. data/lib/teams/function_context.rb +82 -0
  78. data/lib/teams/graph/client.rb +73 -0
  79. data/lib/teams/http_stream.rb +460 -0
  80. data/lib/teams/rack_app.rb +62 -0
  81. data/lib/teams/response.rb +9 -0
  82. data/lib/teams/router.rb +171 -0
  83. data/lib/teams/storage/memory_store.rb +27 -0
  84. data/lib/teams/version.rb +5 -0
  85. data/lib/teams.rb +70 -0
  86. 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,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teams
4
+ module Api
5
+ class ChannelInfo < Model
6
+ def id
7
+ read("id")
8
+ end
9
+
10
+ def name
11
+ read("name")
12
+ end
13
+
14
+ def type
15
+ read("type")
16
+ end
17
+ end
18
+ end
19
+ 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