teams_rb 2.0.1 → 2.0.2

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.
@@ -32,6 +32,20 @@ Each invoke family has named routes — their handler return values become the i
32
32
  - Feedback: `on_message_submit_feedback` (thumbs up/down from `add_feedback` — [guide](../in-depth-guides/feedback.md)), `on_message_submit` for any `message/submitAction`
33
33
  - `on_suggested_action_submit` for suggested-action submissions
34
34
 
35
+ ## Conversation updates
36
+
37
+ `on_conversation_update` matches any `conversationUpdate` activity (members added/removed, channel and team changes). The channel/team lifecycle sub-events also have named routes, matched on `channelData.eventType`:
38
+
39
+ ```ruby
40
+ teams.on_conversation_update { |ctx| } # any conversationUpdate activity
41
+ teams.on_channel_created { |ctx| ctx.post "Welcome to #{ctx.activity.channel_data.channel.name}!" }
42
+ teams.on_channel_deleted { |ctx| } # also: on_channel_renamed, on_channel_restored
43
+ teams.on_team_renamed { |ctx| } # also: on_team_archived, on_team_unarchived,
44
+ # on_team_deleted, on_team_hard_deleted, on_team_restored
45
+ ```
46
+
47
+ A generic `on_conversation_update` registered before a specific route sees the activity first; declare `|ctx, nxt|` and call `nxt.call` to continue to the specific route (see Middleware below).
48
+
35
49
  ## Meeting events
36
50
 
37
51
  ```ruby
@@ -32,6 +32,7 @@ ctx.update sent.id, "Done!"
32
32
  ```ruby
33
33
  ctx.post Teams::Api::MessageActivity.new("**markdown**", text_format: "markdown")
34
34
  # text_format: plain | markdown | xml | extendedmarkdown
35
+ # ("extendedmarkdown" is in public preview and may be subject to change)
35
36
 
36
37
  ctx.post Teams::Api::MessageActivity.new("Quarterly numbers")
37
38
  .add_ai_generated # "AI generated" badge
@@ -5,16 +5,17 @@ Build and run your first Teams bot in Ruby.
5
5
  ## Prerequisites
6
6
 
7
7
  - Ruby 4.0+
8
- - A Microsoft 365 tenant where you can register a Teams app ([developer program](https://developer.microsoft.com/microsoft-365/dev-program) tenants work)
8
+ - A Microsoft 365 tenant with custom app upload enabled ([developer program](https://developer.microsoft.com/microsoft-365/dev-program) tenants work)
9
9
  - A tunnel for local development ([Dev Tunnels](https://learn.microsoft.com/azure/developer/dev-tunnels/get-started), ngrok, or similar)
10
10
 
11
11
  ## Install
12
12
 
13
- Add the gem to your project:
13
+ Add the SDK and a Rack server for this standalone example:
14
14
 
15
15
  ```ruby
16
16
  # Gemfile
17
17
  gem "teams_rb"
18
+ gem "puma"
18
19
  ```
19
20
 
20
21
  ```sh
@@ -38,15 +39,15 @@ end
38
39
  run teams.to_rack
39
40
  ```
40
41
 
41
- And run it:
42
+ This example uses Puma; `teams_rb` itself does not require a server gem. Run it with:
42
43
 
43
44
  ```sh
44
- CLIENT_ID=... CLIENT_SECRET=... TENANT_ID=... bundle exec rackup -p 3978 -o 0.0.0.0
45
+ CLIENT_ID=... CLIENT_SECRET=... TENANT_ID=... bundle exec puma -p 3978
45
46
  ```
46
47
 
47
48
  The app listens for Teams activities on `POST /api/messages`, validates every inbound request against Microsoft's signing keys, and echoes any message back as a quoted reply.
48
49
 
49
- The three environment variables come from your bot registration — [Running in Teams](running-in-teams.md) walks through creating one and connecting Teams to your locally running bot. For local experiments without credentials, `Teams::App.new(skip_auth: true)` disables inbound validation (never use it beyond local testing; the app logs a loud warning when you do).
50
+ The three environment variables come from your bot registration — [Running in Teams](running-in-teams.md) walks through creating one and connecting Teams to your locally running bot. For local experiments without credentials, `Teams::App.new(dangerously_allow_unauthenticated_requests: true)` disables inbound validation (never use it beyond local testing; the app logs a loud warning when you do). The `DANGEROUSLY_ALLOW_UNAUTHENTICATED_REQUESTS` environment variable sets the same option.
50
51
 
51
52
  ## Where to go next
52
53
 
@@ -1,16 +1,18 @@
1
1
  # Running in Teams
2
2
 
3
- Connect your locally running bot to a real Teams client.
3
+ Connect your locally running bot to a real Teams client with the [Teams CLI](https://www.npmjs.com/package/@microsoft/teams.cli).
4
4
 
5
- ## 1. Register a bot
5
+ ## 1. Install and sign in
6
6
 
7
- Create a Teams app with a bot in the [Teams Developer Portal](https://dev.teams.microsoft.com) (or with Microsoft's Teams CLI). You need three values for the app's environment:
7
+ The CLI requires Node.js 20 or newer:
8
8
 
9
- - `CLIENT_ID` — the bot's Microsoft App ID
10
- - `CLIENT_SECRET` a client secret for that app registration
11
- - `TENANT_ID` — your Entra tenant ID (single-tenant bots)
9
+ ```sh
10
+ npm install -g @microsoft/teams.cli
11
+ teams login
12
+ teams status
13
+ ```
12
14
 
13
- > If you plan to use [user authentication](../in-depth-guides/user-authentication.md), register the bot as an **Azure Bot resource** from the start (Azure portal → Create resource → Azure Bot → "Use existing app registration"). OAuth connection settings only exist there, and converting a Developer Portal registration later means deleting and recreating it.
15
+ `teams status` should report that sideloading is enabled. If it is disabled, your tenant administrator must enable custom app upload before you can install the bot.
14
16
 
15
17
  ## 2. Start a tunnel
16
18
 
@@ -22,23 +24,44 @@ devtunnel port create teams-bot -p 3978
22
24
  devtunnel host teams-bot
23
25
  ```
24
26
 
25
- Note the tunnel URL, e.g. `https://abc123-3978.euw.devtunnels.ms`. Dev Tunnels URLs are stable across restarts, so this is one-time setup.
27
+ Note the tunnel URL, such as `https://abc123-3978.euw.devtunnels.ms`. Dev Tunnels URLs are stable across restarts, so creation is a one-time step.
28
+
29
+ ## 3. Register the app and bot
26
30
 
27
- ## 3. Point the bot at the tunnel
31
+ From your project directory, pass the public endpoint to the CLI:
32
+
33
+ ```sh
34
+ teams app create \
35
+ --name my-teams-ruby-bot \
36
+ --endpoint https://<your-tunnel>/api/messages
37
+ ```
28
38
 
29
- In the bot registration, set the **messaging endpoint** to `https://<your-tunnel>/api/messages`, and make sure the **Microsoft Teams channel** is enabled.
39
+ The CLI creates the app registration, manifest, bot, and Teams app. It prints the Teams App ID, an **Install in Teams** link, and the three values your Ruby app needs:
40
+
41
+ - `CLIENT_ID` — the bot's Microsoft App ID
42
+ - `CLIENT_SECRET` — the bot's client secret
43
+ - `TENANT_ID` — the Entra tenant ID
44
+
45
+ > If you plan to use [user authentication](../in-depth-guides/user-authentication.md), create an Azure-hosted bot with the CLI's `--azure`, `--subscription`, and `--resource-group` options. OAuth connection settings are not available for Teams-managed bots.
46
+
47
+ You can instead configure the same resources manually in the [Teams Developer Portal](https://dev.teams.microsoft.com/apps).
30
48
 
31
49
  ## 4. Run and install
32
50
 
51
+ Start the bot with the credentials printed by the CLI:
52
+
33
53
  ```sh
34
- bundle exec rackup -p 3978 -o 0.0.0.0
54
+ CLIENT_ID=... CLIENT_SECRET=... TENANT_ID=... bundle exec puma -p 3978
35
55
  ```
36
56
 
37
- Install the app in Teams (Developer Portal → Preview in Teams) and send it a message. Inbound requests are JWT-validated with your real credentials — no `skip_auth` needed behind a tunnel.
57
+ Open the **Install in Teams** link and send the bot a message. If you need the link again, run `teams app get <teams-app-id> --install-link`.
58
+
59
+ Inbound requests are JWT-validated with your real credentials; no `dangerously_allow_unauthenticated_requests` is needed behind the tunnel.
38
60
 
39
61
  ## Troubleshooting
40
62
 
41
- - **No response in Teams**: check the messaging endpoint URL, the Teams channel, and that the tunnel and server are both running. The app logs every inbound activity at debug level and every rejected request at warn level.
42
- - **401s in your logs**: the requests are reaching you but failing validation usually a `CLIENT_ID`/`TENANT_ID` mismatch with the registration.
63
+ - **No response in Teams**: run `teams app doctor <teams-app-id>`, then check that the tunnel and server are running.
64
+ - **401s in your logs**: the requests are reaching you but failing validation, usually because the running app has credentials from a different registration.
65
+ - **Sideloading disabled**: ask your tenant administrator to enable custom app upload.
43
66
  - **Teams caches aggressively**: after manifest changes, fully quit and reopen the Teams client.
44
67
  - **At-least-once delivery**: Bot Framework redelivers activities when your bot errors or responds slowly. If a handler's side effects must not repeat, deduplicate by `ctx.activity.id`.
@@ -48,6 +48,23 @@ end
48
48
 
49
49
  For richer action flows — opening a modal form from a card button — see [Dialogs](dialogs.md).
50
50
 
51
+ ## Dynamic typeahead search
52
+
53
+ An `Input.ChoiceSet` with `choices.data` (`Data.Query`) fetches its choices from your bot as the user types. Teams sends an `application/search` invoke; answer it with `on_card_search`:
54
+
55
+ ```ruby
56
+ teams.on_card_search do |ctx|
57
+ query = ctx.activity.value.query_text
58
+ Teams::Api::SearchResponse.new(
59
+ CITIES.grep(/#{Regexp.escape(query)}/i).map do |city|
60
+ Teams::Api::SearchInvokeResult.new(title: city, value: city)
61
+ end
62
+ )
63
+ end
64
+ ```
65
+
66
+ `ctx.activity.value` also carries `dataset` (the `Data.Query` dataset id, for cards with several dynamic inputs) and `query_options` with `skip`/`top` for paging large result sets.
67
+
51
68
  ## Regenerating
52
69
 
53
70
  The card classes are generated; don't hand-edit `lib/teams/cards/generated.rb`. To regenerate after an upstream card-model change:
@@ -31,7 +31,7 @@ teams.on_sign_in do |ctx, token_response|
31
31
  end
32
32
  ```
33
33
 
34
- `ctx.sign_in` returns the token when the user is already signed in; otherwise it sends an OAuth card (to a 1:1 conversation when invoked from a group chat) and returns `nil`. The SDK's **default handlers** then complete the sign-in invokes automatically — both the interactive card path (`signin/verifyState`) and silent SSO (`signin/tokenExchange`) — and fire `on_sign_in`. You don't handle the invokes yourself.
34
+ `ctx.sign_in` returns the token when the user is already signed in; otherwise it sends an OAuth card and returns `nil`. In group chats and channels the card is delivered in the conversation as a targeted message only the requesting user sees; in channels the card always uses the sign-in button (silent SSO is not supported in channel scope). The SDK's **default handlers** then complete the sign-in invokes automatically — both the interactive card path (`signin/verifyState`) and silent SSO (`signin/tokenExchange`) — and fire `on_sign_in`. You don't handle the invokes yourself.
35
35
 
36
36
  `ctx.sign_out` clears the stored token.
37
37
 
@@ -93,8 +93,10 @@ module Teams
93
93
 
94
94
  # Starts the user sign-in flow: returns the token if the user is already
95
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.
96
+ # chats and channels the card is a targeted message visible only to the
97
+ # requesting user; channels omit the token exchange resource because
98
+ # channel scope cannot do the silent SSO exchange, so the card renders
99
+ # the sign-in button instead.
98
100
  def sign_in(connection_name: nil, oauth_card_text: "Please Sign In...", sign_in_button_text: "Sign In")
99
101
  connection_name ||= app.default_connection_name
100
102
 
@@ -109,19 +111,6 @@ module Teams
109
111
  # No token yet; continue with the OAuth card flow.
110
112
  end
111
113
 
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
114
  state = Base64.strict_encode64(JSON.generate(
126
115
  "connectionName" => connection_name,
127
116
  "conversation" => conversation_reference.to_h,
@@ -129,10 +118,14 @@ module Teams
129
118
  ))
130
119
  resource = api.bots.sign_in.get_resource(state:)
131
120
 
121
+ is_channel = activity.conversation.conversation_type == "channel"
122
+ recipient = activity.from.to_h
123
+ recipient = recipient.merge("isTargeted" => true) if activity.conversation.is_group
124
+
132
125
  card = {
133
126
  "text" => oauth_card_text,
134
127
  "connectionName" => connection_name,
135
- "tokenExchangeResource" => resource.token_exchange_resource&.to_h,
128
+ "tokenExchangeResource" => (resource.token_exchange_resource&.to_h unless is_channel),
136
129
  "tokenPostResource" => resource.token_post_resource&.to_h,
137
130
  "buttons" => [
138
131
  { "type" => "signin", "title" => sign_in_button_text, "value" => resource.sign_in_link }
@@ -141,13 +134,13 @@ module Teams
141
134
 
142
135
  payload = {
143
136
  "type" => "message",
144
- "recipient" => activity.from.to_h,
137
+ "recipient" => recipient,
145
138
  "attachments" => [
146
139
  { "contentType" => "application/vnd.microsoft.card.oauth", "content" => card }
147
140
  ]
148
141
  }
149
142
 
150
- app.send_activity(sign_in_reference(conversation_id), payload)
143
+ app.send_activity(conversation_reference, payload)
151
144
  nil
152
145
  end
153
146
 
@@ -167,16 +160,6 @@ module Teams
167
160
 
168
161
  private
169
162
 
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
163
  def incoming_targeted_sender
181
164
  return nil unless activity.message?
182
165
  return nil unless activity.recipient.is_targeted == true
@@ -44,6 +44,35 @@ module Teams
44
44
  def end_time
45
45
  read("EndTime", "endTime", "end_time")
46
46
  end
47
+
48
+ # application/search invoke fields (Adaptive Card dynamic typeahead
49
+ # Input.ChoiceSet queries via choices.data / Data.Query).
50
+ def query_text
51
+ read("queryText", "query_text")
52
+ end
53
+
54
+ # Pagination options; skip and top read from the nested wrapper.
55
+ def query_options
56
+ value = read("queryOptions", "query_options")
57
+ value.is_a?(Hash) ? ActivityValue.new(value) : value
58
+ end
59
+
60
+ def kind
61
+ read("kind")
62
+ end
63
+
64
+ # The Data.Query dataset id authored on the Adaptive Card.
65
+ def dataset
66
+ read("dataset")
67
+ end
68
+
69
+ def skip
70
+ read("skip")
71
+ end
72
+
73
+ def top
74
+ read("top")
75
+ end
47
76
  end
48
77
  end
49
78
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teams
4
+ module Api
5
+ class AppInfo < Model
6
+ def id
7
+ read("id")
8
+ end
9
+
10
+ def version
11
+ read("version")
12
+ end
13
+ end
14
+ end
15
+ end
@@ -3,6 +3,10 @@
3
3
  module Teams
4
4
  module Api
5
5
  class ChannelData < Model
6
+ def app
7
+ wrap(read("app"), AppInfo)
8
+ end
9
+
6
10
  def tenant
7
11
  wrap(read("tenant"), TenantInfo)
8
12
  end
@@ -55,6 +59,7 @@ module Teams
55
59
 
56
60
  def to_h
57
61
  body = raw.dup
62
+ body["app"] = app.to_h if app
58
63
  body["tenant"] = tenant.to_h if tenant
59
64
  body["team"] = team.to_h if team
60
65
  body["channel"] = channel.to_h if channel
@@ -41,7 +41,9 @@ module Teams
41
41
  def reply_to_activity(conversation_id, activity_id, activity, service_url: nil)
42
42
  body = activity_to_h(activity)
43
43
  body = body.merge("replyToId" => activity_id) if body.is_a?(Hash)
44
- path = "/v3/conversations/#{escape(conversation_id)}/activities/#{escape(activity_id)}"
44
+ # Replies POST to the base activities collection; the reply
45
+ # semantics ride entirely in the body's replyToId.
46
+ path = "/v3/conversations/#{escape(conversation_id)}/activities"
45
47
  url = absolute(path, service_url:)
46
48
  @logger&.debug("Teams API POST #{url}")
47
49
  http.post(url, json: body)
@@ -3,6 +3,7 @@
3
3
  module Teams
4
4
  module Api
5
5
  class MessageActivity
6
+ # "extendedmarkdown" is in public preview and may be subject to change.
6
7
  TEXT_FORMATS = %w[plain markdown xml extendedmarkdown].freeze
7
8
  FEEDBACK_MODES = %w[default custom].freeze
8
9
  AI_MESSAGE_ENTITY_TYPE = "https://schema.org/Message"
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teams
4
+ module Api
5
+ # A single application/search result: title is the display text, value
6
+ # is submitted when the user selects it.
7
+ class SearchInvokeResult
8
+ def initialize(title:, value:)
9
+ @title = title
10
+ @value = value
11
+ end
12
+
13
+ def to_h
14
+ { "title" => @title, "value" => @value }
15
+ end
16
+ end
17
+
18
+ # Response body for application/search invokes (Adaptive Card dynamic
19
+ # typeahead Input.ChoiceSet queries). results accepts SearchInvokeResult
20
+ # objects or {title:, value:} hashes.
21
+ class SearchResponse
22
+ def initialize(results, status_code: 200)
23
+ @results = results
24
+ @status_code = status_code
25
+ end
26
+
27
+ def to_h
28
+ {
29
+ "statusCode" => @status_code,
30
+ "type" => "application/vnd.microsoft.search.searchResponse",
31
+ "value" => {
32
+ "results" => Array(@results).map do |result|
33
+ body = result.is_a?(Hash) ? result : result.to_h
34
+ Common::Hashes.deep_stringify_keys(body)
35
+ end
36
+ }
37
+ }
38
+ end
39
+ end
40
+ end
41
+ end
data/lib/teams/app.rb CHANGED
@@ -5,6 +5,9 @@ require "logger"
5
5
  module Teams
6
6
  class App
7
7
  DEFAULT_MESSAGING_ENDPOINT = "/api/messages"
8
+ DANGEROUSLY_ALLOW_UNAUTHENTICATED_REQUESTS_ENV_VAR = "DANGEROUSLY_ALLOW_UNAUTHENTICATED_REQUESTS"
9
+ TRUE_ENV_VALUES = %w[1 true yes on].freeze
10
+ FALSE_ENV_VALUES = %w[0 false no off].freeze
8
11
 
9
12
  attr_reader :api, :logger, :storage, :messaging_endpoint, :default_connection_name, :graph
10
13
 
@@ -22,7 +25,8 @@ module Teams
22
25
  storage: Storage::MemoryStore.new,
23
26
  api: nil,
24
27
  token_manager: nil,
25
- skip_auth: false,
28
+ dangerously_allow_unauthenticated_requests: nil,
29
+ skip_auth: nil,
26
30
  messaging_endpoint: DEFAULT_MESSAGING_ENDPOINT,
27
31
  default_connection_name: "graph"
28
32
  )
@@ -33,7 +37,9 @@ module Teams
33
37
  @logger = logger
34
38
  @storage = storage
35
39
  @cloud = cloud
36
- @skip_auth = skip_auth
40
+ @dangerously_allow_unauthenticated_requests = resolve_unauthenticated_requests_option(
41
+ dangerously_allow_unauthenticated_requests, skip_auth
42
+ )
37
43
  @messaging_endpoint = normalize_messaging_endpoint(messaging_endpoint)
38
44
  @router = Router.new
39
45
 
@@ -140,6 +146,14 @@ module Teams
140
146
  self
141
147
  end
142
148
 
149
+ # application/search invokes from Adaptive Card dynamic typeahead
150
+ # Input.ChoiceSet queries; the handler's return (Api::SearchResponse or
151
+ # hash) becomes the invoke response body.
152
+ def on_card_search(&block)
153
+ @router.on_card_search(&block)
154
+ self
155
+ end
156
+
143
157
  # Called with (ctx, token_response) whenever a sign-in completes through
144
158
  # the default token-exchange or verify-state handlers.
145
159
  def on_sign_in(&block)
@@ -206,6 +220,21 @@ module Teams
206
220
  self
207
221
  end
208
222
 
223
+ # conversationUpdate activities plus their channel/team lifecycle
224
+ # sub-events (on_channel_created, on_team_renamed, ...), routed by
225
+ # channelData.eventType with the Python method names.
226
+ def on_conversation_update(&block)
227
+ @router.on_conversation_update(&block)
228
+ self
229
+ end
230
+
231
+ Router::CONVERSATION_UPDATE_EVENTS.each_key do |method_name|
232
+ define_method(method_name) do |&block|
233
+ @router.public_send(method_name, &block)
234
+ self
235
+ end
236
+ end
237
+
209
238
  # Message extension handlers (on_message_ext_query, on_message_ext_submit,
210
239
  # on_message_ext_open, ...) route the composeExtension/* invokes with the
211
240
  # TypeScript/Python route names. Handler return values (typed responses
@@ -326,7 +355,8 @@ module Teams
326
355
  # ending in ctx.post must not leak the SentActivity into the response.
327
356
  def invoke_response_body(result)
328
357
  case result
329
- when Api::TaskModuleResponse, Api::MessagingExtensionResponse, Api::MessagingExtensionActionResponse
358
+ when Api::TaskModuleResponse, Api::MessagingExtensionResponse, Api::MessagingExtensionActionResponse,
359
+ Api::SearchResponse
330
360
  result.to_h
331
361
  when Hash
332
362
  Common::Hashes.deep_stringify_keys(result)
@@ -487,28 +517,53 @@ module Teams
487
517
  end
488
518
  end
489
519
 
520
+ # Resolves the auth-bypass setting like the Python SDK: the explicit
521
+ # option wins, then the deprecated skip_auth alias, then the env var,
522
+ # defaulting to false. Passing skip_auth always warns, even when the
523
+ # new option overrides it.
524
+ def resolve_unauthenticated_requests_option(explicit, skip_auth)
525
+ unless skip_auth.nil?
526
+ warn("skip_auth is deprecated; use dangerously_allow_unauthenticated_requests instead.", uplevel: 2)
527
+ end
528
+ return explicit unless explicit.nil?
529
+ return skip_auth unless skip_auth.nil?
530
+
531
+ parse_bool_env_var(DANGEROUSLY_ALLOW_UNAUTHENTICATED_REQUESTS_ENV_VAR) || false
532
+ end
533
+
534
+ def parse_bool_env_var(name)
535
+ value = ENV[name].to_s.strip.downcase
536
+ return nil if value.empty?
537
+ return true if TRUE_ENV_VALUES.include?(value)
538
+ return false if FALSE_ENV_VALUES.include?(value)
539
+
540
+ raise ArgumentError, "#{name} must be a boolean value: true/false, 1/0, yes/no, or on/off."
541
+ end
542
+
490
543
  # The same two startup warnings the TypeScript, Python, and .NET SDKs
491
544
  # log when no credentials are configured. Settled 2026-07-12: exact
492
- # upstream branches only; credentials-plus-skip_auth stays silent like
545
+ # upstream branches only; credentials-plus-bypass stays silent like
493
546
  # the other SDKs.
494
547
  def warn_missing_credentials
495
548
  return if @token_manager.client_id
496
549
 
497
- if @skip_auth
550
+ if @dangerously_allow_unauthenticated_requests
498
551
  logger&.warn(
499
552
  "No credentials configured (CLIENT_ID / CLIENT_SECRET / TENANT_ID), " \
500
- "but skip_auth is enabled. Bot will accept unauthenticated requests on #{@messaging_endpoint}."
553
+ "but dangerously_allow_unauthenticated_requests is enabled. " \
554
+ "Bot will accept unauthenticated requests on #{@messaging_endpoint}."
501
555
  )
502
556
  else
503
557
  logger&.warn(
504
- "No credentials configured and skip_auth is not enabled. All incoming requests will be rejected. " \
505
- "Configure client authentication to securely receive messages, or set skip_auth: true for local development."
558
+ "No credentials configured and dangerously_allow_unauthenticated_requests is not enabled. " \
559
+ "All incoming requests will be rejected. Configure client authentication to securely receive " \
560
+ "messages, or set dangerously_allow_unauthenticated_requests: true for local development."
506
561
  )
507
562
  end
508
563
  end
509
564
 
510
565
  def validate_inbound!(env, activity)
511
- return if @skip_auth
566
+ return if @dangerously_allow_unauthenticated_requests
512
567
 
513
568
  raise AuthenticationError, "CLIENT_ID is required for inbound validation" unless @jwt_validator
514
569
 
@@ -356,6 +356,9 @@ module Teams
356
356
  body = activity.dup
357
357
  body["from"] = conversation_reference.bot.to_h if conversation_reference.bot
358
358
  body["conversation"] = conversation_reference.conversation.to_h
359
+ # Every streamed activity replies to the inbound message, restoring
360
+ # Bot Framework v1 threading behavior.
361
+ body["replyToId"] = conversation_reference.activity_id if conversation_reference.activity_id
359
362
 
360
363
  # Stream chunks and the streamed final carry a streaminfo entity and are
361
364
  # always created; only the timed-out in-place final routes through update.
data/lib/teams/router.rb CHANGED
@@ -72,6 +72,12 @@ module Teams
72
72
  register("message.submit", invoke_selector("message/submitAction"), &block)
73
73
  end
74
74
 
75
+ # application/search invokes - Adaptive Card dynamic typeahead
76
+ # Input.ChoiceSet queries (choices.data / Data.Query).
77
+ def on_card_search(&block)
78
+ register("card.search", invoke_selector("application/search"), &block)
79
+ end
80
+
75
81
  # message/submitAction invokes whose actionName is "feedback" - the
76
82
  # submissions from add_feedback's thumbs up/down UI.
77
83
  def on_message_submit_feedback(&block)
@@ -90,6 +96,32 @@ module Teams
90
96
  register("meeting_end", event_selector("application/vnd.microsoft.meetingEnd"), &block)
91
97
  end
92
98
 
99
+ # conversationUpdate activities and their channel/team lifecycle
100
+ # sub-events, routed by channelData.eventType with the Python method
101
+ # names (the eventType literals are shared by all three SDKs).
102
+ def on_conversation_update(&block)
103
+ register("conversation_update", ->(activity) { activity.type == "conversationUpdate" }, &block)
104
+ end
105
+
106
+ CONVERSATION_UPDATE_EVENTS = {
107
+ "on_channel_created" => "channelCreated",
108
+ "on_channel_deleted" => "channelDeleted",
109
+ "on_channel_renamed" => "channelRenamed",
110
+ "on_channel_restored" => "channelRestored",
111
+ "on_team_archived" => "teamArchived",
112
+ "on_team_deleted" => "teamDeleted",
113
+ "on_team_hard_deleted" => "teamHardDeleted",
114
+ "on_team_renamed" => "teamRenamed",
115
+ "on_team_restored" => "teamRestored",
116
+ "on_team_unarchived" => "teamUnarchived"
117
+ }.freeze
118
+
119
+ CONVERSATION_UPDATE_EVENTS.each do |method_name, event_type|
120
+ define_method(method_name) do |&block|
121
+ register(event_type, conversation_update_selector(event_type), &block)
122
+ end
123
+ end
124
+
93
125
  # Message extension (compose extension) invoke routes, using the same
94
126
  # route names as the TypeScript and Python SDKs.
95
127
  MESSAGE_EXTENSION_ROUTES = {
@@ -152,6 +184,12 @@ module Teams
152
184
  ->(activity) { activity.type == "event" && activity.name == event_name }
153
185
  end
154
186
 
187
+ def conversation_update_selector(event_type)
188
+ lambda do |activity|
189
+ activity.type == "conversationUpdate" && activity.channel_data.event_type == event_type
190
+ end
191
+ end
192
+
155
193
  def dialog_selector(invoke_name, key, expected)
156
194
  lambda do |activity|
157
195
  next false unless activity.invoke? && activity.name == invoke_name
data/lib/teams/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Teams
4
- VERSION = "2.0.1"
4
+ VERSION = "2.0.2"
5
5
  end
data/lib/teams.rb CHANGED
@@ -12,6 +12,7 @@ require_relative "teams/api/account"
12
12
  require_relative "teams/api/conversation_account"
13
13
  require_relative "teams/api/conversation_resource"
14
14
  require_relative "teams/api/paged_members_result"
15
+ require_relative "teams/api/app_info"
15
16
  require_relative "teams/api/tenant_info"
16
17
  require_relative "teams/api/team_info"
17
18
  require_relative "teams/api/team_details"
@@ -27,6 +28,7 @@ require_relative "teams/api/sent_activity"
27
28
  require_relative "teams/api/citation_appearance"
28
29
  require_relative "teams/api/task_module"
29
30
  require_relative "teams/api/message_extension"
31
+ require_relative "teams/api/search"
30
32
  require_relative "teams/activity"
31
33
  require_relative "teams/activity_context"
32
34
  require_relative "teams/function_context"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: teams_rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.1
4
+ version: 2.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Devran Cosmo Uenal
@@ -138,6 +138,7 @@ files:
138
138
  - lib/teams/activity_context.rb
139
139
  - lib/teams/api/account.rb
140
140
  - lib/teams/api/activity_value.rb
141
+ - lib/teams/api/app_info.rb
141
142
  - lib/teams/api/bot_sign_in_client.rb
142
143
  - lib/teams/api/channel_data.rb
143
144
  - lib/teams/api/channel_info.rb
@@ -158,6 +159,7 @@ files:
158
159
  - lib/teams/api/paged_members_result.rb
159
160
  - lib/teams/api/quoted_reply_entity.rb
160
161
  - lib/teams/api/reaction_client.rb
162
+ - lib/teams/api/search.rb
161
163
  - lib/teams/api/sent_activity.rb
162
164
  - lib/teams/api/task_module.rb
163
165
  - lib/teams/api/team_client.rb