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.
data/README.md CHANGED
@@ -1,40 +1,40 @@
1
- # teams_rb
1
+ # teams_rb — Teams SDK for Ruby
2
2
 
3
- A Ruby-native port of the Microsoft Teams SDKs ([TypeScript](https://microsoft.github.io/teams-sdk/typescript/getting-started), [Python](https://microsoft.github.io/teams-sdk/python/getting-started), [C#](https://microsoft.github.io/teams-sdk/csharp/getting-started)) for building Teams bots and apps from Rack/Rails.
3
+ [![Gem Version](https://img.shields.io/gem/v/teams_rb)](https://rubygems.org/gems/teams_rb)
4
+ [![Documentation](https://img.shields.io/badge/docs-getting_started-blue)](docs/getting-started/README.md)
4
5
 
5
- > **Unofficial.** `teams_rb` is an independent, community-maintained project. It is not affiliated with, endorsed by, or sponsored by Microsoft. The official Teams SDKs (TypeScript, Python, C#) are developed by Microsoft at [github.com/microsoft/teams-sdk](https://github.com/microsoft/teams-sdk). "Microsoft", "Microsoft Teams", and "Microsoft 365" are trademarks of the Microsoft group of companies.
6
+ A Ruby-native port of the Microsoft Teams SDKs for building Teams bots and apps with Rack or Rails. It includes message routing, proactive messaging, the Bot Framework API client, typed Adaptive Cards, dialogs, message extensions, streaming, meetings, OAuth, Microsoft Graph, tabs, and remote functions.
6
7
 
7
- It preserves the Teams SDK concepts and wire behavior while using Ruby idioms for the public API, and versions alongside the Teams SDK v2 generation it ports. Every capability is verified against all three upstream SDKs and live-tested in Teams:
8
+ `teams_rb` preserves the concepts and wire behavior of the official [TypeScript](https://microsoft.github.io/teams-sdk/typescript/getting-started), [Python](https://microsoft.github.io/teams-sdk/python/getting-started), and [C#](https://microsoft.github.io/teams-sdk/csharp/getting-started) SDKs while providing an idiomatic Ruby API. Inbound JWT validation and outbound bot token management are built in.
8
9
 
9
- - Message routing, middleware, and the activity context
10
- - Sending: post, reply, quote, update, typing, formatting, mentions, sensitivity labels, citations
11
- - Proactive messaging and conversation creation
12
- - The full API client — conversations, teams, meetings, users, bot sign-in
13
- - Adaptive Cards (all 112 element classes, generated from the SDK card model)
14
- - Dialogs, message extensions, streaming, meeting events
15
- - OAuth user sign-in and Microsoft Graph (app and user identity)
16
- - Tabs and remote functions
17
- - Inbound JWT validation, bot token management, thread-safe by design
10
+ > **Unofficial.** `teams_rb` is an independent, community-maintained project. It is not affiliated with, endorsed by, or sponsored by Microsoft. The official Teams SDKs are developed by Microsoft at [github.com/microsoft/teams-sdk](https://github.com/microsoft/teams-sdk).
18
11
 
19
- ## Documentation
20
-
21
- Full documentation lives in [`docs/`](docs/README.md), structured like the official SDK docs:
12
+ ## Getting started
22
13
 
23
- - **[Getting started](docs/getting-started/README.md)** — quickstart, code basics, running in Teams
24
- - **[Essentials](docs/essentials/README.md)** — app, activities, sending, proactive, API client, auth, Graph
25
- - **[In-depth guides](docs/in-depth-guides/README.md)** — cards, dialogs, message extensions, streaming, user auth, tabs
14
+ ### Prerequisites
26
15
 
27
- The rest of this file is a condensed reference; the docs are the primary source.
16
+ - Ruby 4.0 or newer
17
+ - A Microsoft 365 tenant with custom app upload enabled
18
+ - A public HTTPS tunnel for local development, such as [Dev Tunnels](https://learn.microsoft.com/azure/developer/dev-tunnels/get-started)
28
19
 
29
- ## Local Usage
20
+ ### Install
30
21
 
31
- From another Ruby app:
22
+ Add the SDK and a Rack server for this standalone example to your `Gemfile`:
32
23
 
33
24
  ```ruby
34
- gem "teams_rb", path: "../teams_rb"
25
+ gem "teams_rb"
26
+ gem "puma"
27
+ ```
28
+
29
+ Then install it:
30
+
31
+ ```sh
32
+ bundle install
35
33
  ```
36
34
 
37
- Then:
35
+ ### Create your first bot
36
+
37
+ Create a `config.ru`:
38
38
 
39
39
  ```ruby
40
40
  require "teams"
@@ -43,458 +43,115 @@ teams = Teams::App.new
43
43
 
44
44
  teams.on_message do |ctx|
45
45
  ctx.typing
46
- puts ctx.ref.conversation_id
47
- ctx.reply "reply: #{ctx.activity.text.inspect}"
48
- ctx.post "post: #{ctx.activity.text.inspect}"
46
+ ctx.reply "echo: #{ctx.activity.text}"
49
47
  end
50
48
 
51
49
  run teams.to_rack
52
50
  ```
53
51
 
54
- Suggested action submit invokes use the SDK route name and expose the submitted payload through `ctx.activity.value`:
55
-
56
- ```ruby
57
- teams.on_suggested_action_submit do |ctx|
58
- ctx.post "submitted: #{ctx.activity.value.to_h.inspect}"
59
- end
60
- ```
61
-
62
- Message update events fire when a user edits or restores a Teams message:
63
-
64
- ```ruby
65
- teams.on_edit_message do |ctx|
66
- puts "edited text: #{ctx.activity.text}"
67
- end
68
-
69
- teams.on_undelete_message do |ctx|
70
- puts "restored text: #{ctx.activity.text}"
71
- end
72
- ```
73
-
74
- Reactions use the API client shape from the Microsoft SDKs:
75
-
76
- ```ruby
77
- teams.api.conversations.add_reaction(conversation_id, activity_id, "like")
78
- teams.api.conversations.delete_reaction(conversation_id, activity_id, "like")
79
- ```
80
-
81
- To update a message later, keep the activity id returned from the original send:
82
-
83
- ```ruby
84
- sent = teams.post(conversation_id, "We have 2 Free iPhones, ready to pick up. While supplies last")
85
-
86
- teams.update(conversation_id, sent.fetch("id"), "Free phones gone now.")
87
- # equivalent SDK-send style:
88
- teams.post(conversation_id, Teams::Api::MessageActivity.new("Free phones gone now.").with_id(sent.fetch("id")))
89
- # lower-level parity surface:
90
- teams.api.conversations.update_activity(conversation_id, sent.fetch("id"), Teams::Api::MessageActivity.new("Free phones gone now."))
91
- ```
92
-
93
- More Rack examples live in `examples/`.
94
-
95
- The Teams messaging endpoint defaults to `/api/messages`, matching the TypeScript and Python SDK defaults. If your app needs another path, configure it on the app and register the same full URL with Teams:
96
-
97
- ```ruby
98
- teams = Teams::App.new(messaging_endpoint: "/bot/incoming")
99
- run teams.to_rack
100
- ```
101
-
102
- Use `ctx.post` for a plain message in the conversation. The Microsoft Teams SDKs call this `send`, but Ruby already defines `Object#send` for dynamic dispatch, so this SDK uses `post` for the public Ruby API. Treat `post` as Ruby's spelling of SDK `send`: if the activity already has an `id`, it updates that activity instead of creating a new one. Use `ctx.reply` when you want Teams reply semantics: `replyToId` plus the Teams `quotedReply` entity and quote placeholder, matching the Microsoft SDK behavior. Use `ctx.update(activity_id, activity)` to replace a previous bot message in the current conversation.
103
-
104
- `ctx.ref` returns a `Teams::Api::ConversationReference`, matching the Teams SDK concept used for the current conversation. The same object is also available as `ctx.conversation_reference`. Store `ctx.ref.to_h` from a validated inbound activity if you need to post, reply, or update later from a job, then restore it with `Teams::Api::ConversationReference.from_h` and pass its `conversation_id` and `service_url` to `teams.post` / `teams.reply` / `teams.update`.
52
+ ### Register it with Teams
105
53
 
106
- To message a user without a stored conversation, create (or re-fetch) the 1:1 conversation first. Teams returns the existing conversation if one already exists for the same members:
54
+ The easiest registration path is the optional [Teams CLI](https://www.npmjs.com/package/@microsoft/teams.cli), which requires Node.js 20 or newer. Install it, sign in, and check that your tenant allows sideloading:
107
55
 
108
- ```ruby
109
- conversation = teams.api.conversations.create(
110
- members: [{ id: user_id }], # the user's Teams/Bot Framework id, e.g. "29:..."
111
- tenant_id: tenant_id
112
- )
113
- teams.post(conversation.id, "Hello from your SaaS backend")
114
- ```
115
-
116
- Conversation rosters come from the members APIs, which return `Teams::Api::Account` objects (with `aadObjectId` normalized, matching the other SDKs):
117
-
118
- ```ruby
119
- teams.api.conversations.get_members(conversation_id)
120
- teams.api.conversations.get_member_by_id(conversation_id, member_id)
121
- teams.api.conversations.get_paged_members(conversation_id, page_size: 200) # => Teams::Api::PagedMembersResult
122
- teams.api.conversations.get_activity_members(conversation_id, activity_id)
123
- ```
124
-
125
- Use `get_paged_members` for large rosters: pass the result's `continuation_token` back in until it returns `nil`.
126
-
127
- Team and meeting lookups follow the same client shape:
128
-
129
- ```ruby
130
- teams.api.teams.get_by_id(team_id) # => Teams::Api::TeamDetails
131
- teams.api.teams.get_conversations(team_id) # => [Teams::Api::ChannelInfo] (the team's channels)
132
- teams.api.meetings.get_by_id(meeting_id) # => Teams::Api::MeetingInfo
133
- teams.api.meetings.get_participant(meeting_id, aad_object_id, tenant_id)
134
- teams.api.meetings.send_notification(meeting_id, { value: { recipients: [aad_object_id], surfaces: [{ surface: "meetingStage", contentType: "task", content: { ... } }] } })
135
- ```
136
-
137
- `send_notification` returns `nil` when every recipient was notified (HTTP 202) and a `Teams::Api::MeetingNotificationResponse` with `recipients_failure_info` on partial success (HTTP 207).
138
-
139
- For modeled Ruby object access, use snake_case field names:
140
-
141
- ```ruby
142
- ctx.activity.service_url
143
- ctx.activity.reply_to_id
144
- ctx.activity.from.aad_object_id
145
- ctx.activity.conversation.conversation_type
146
- ```
147
-
148
- Raw payload access stays unchanged through `raw` / `to_h`:
149
-
150
- ```ruby
151
- ctx.activity.raw["serviceUrl"]
152
- ctx.activity.raw.dig("from", "aadObjectId")
153
- ```
154
-
155
- Quoted replies use the same SDK concepts as TypeScript, Python, and .NET:
156
-
157
- ```ruby
158
- ctx.reply "auto-quotes the inbound activity"
159
- ctx.quote "message-id", "quotes a specific activity"
160
-
161
- message = Teams::Api::MessageActivity.new
162
- .add_quote("message-id", "builder response")
163
-
164
- quotes = ctx.activity.get_quoted_messages
165
- ```
166
-
167
- For formatted text, use a message activity with `text_format`:
168
-
169
- ```ruby
170
- ctx.post Teams::Api::MessageActivity.new("plain text", text_format: "plain")
171
- ctx.post Teams::Api::MessageActivity.new("**markdown**", text_format: "markdown")
172
- ctx.post Teams::Api::MessageActivity.new("line 1<br>line 2", text_format: "xml")
173
- ctx.post Teams::Api::MessageActivity.new("extended markdown", text_format: "extendedmarkdown")
174
- ```
175
-
176
- Teams delivers activities with at-least-once semantics, so a message can occasionally reach your bot twice. Like the other Teams SDKs, `teams_rb` does not deduplicate inbound activities; if a handler performs side effects that must not repeat, deduplicate by `ctx.activity.id`.
177
-
178
- For a typing indicator, use `ctx.typing`. Teams renders it as an animated ellipsis in the chat. It accepts optional text for wire parity with the other Teams SDKs, but the Teams client does not display that text on a plain typing activity — for a visible status line, use `ctx.stream.update` instead:
179
-
180
- ```ruby
181
- ctx.typing # animated ellipsis
182
- ctx.stream.update("Thinking...") # visible status line above the streamed response
183
- ```
184
-
185
- Updating an activity replaces it entirely: Teams does not merge with the previous version, so metadata such as the AI-generated label, sensitivity label, citations, and mentions must be re-attached on every update or they disappear (verified live). For example:
186
-
187
- ```ruby
188
- sent = ctx.post Teams::Api::MessageActivity.new("Thinking...").add_ai_generated
189
- ctx.update sent.id, Teams::Api::MessageActivity.new("Final answer.").add_ai_generated
190
- ```
191
-
192
- Every send returns a `Teams::Api::SentActivity` carrying the outbound activity merged with the server response, so the sent message id is always available:
193
-
194
- ```ruby
195
- sent = ctx.reply("hello")
196
- sent.id # server-assigned activity id
197
- sent.text # "hello"
198
- sent.to_h # full merged activity hash
199
- ```
200
-
201
- When the inbound message is targeted (visible only to the sender), `ctx.post` and `ctx.reply` automatically respond as targeted messages to that sender, matching the other Teams SDKs: the recipient is inferred, a `targetedMessageInfo` entity is attached for prompt preview, and the send routes through the targeted activity endpoints. Pass an explicit recipient to opt out, or send an explicitly targeted message from any handler:
202
-
203
- ```ruby
204
- ctx.post Teams::Api::MessageActivity.new("Only for you").with_recipient(account, is_targeted: true)
56
+ ```sh
57
+ npm install -g @microsoft/teams.cli
58
+ teams login
59
+ teams status
205
60
  ```
206
61
 
207
- Targeted messages are rejected in 1:1 (personal) chats, where every message is already private.
208
-
209
- This repository is self-contained: the generated card classes and their golden fixtures are committed, so the gem and its test suite need nothing beyond Ruby. Regenerating the card classes requires a clone of Microsoft's Python SDK and [uv](https://docs.astral.sh/uv/); by default a checkout next to this repository is used, and `TEAMS_PY_PATH` points anywhere else:
62
+ Start your HTTPS tunnel, then let the CLI create the app, bot registration, and install link:
210
63
 
211
64
  ```sh
212
- bundle exec rake cards:generate
213
- # or with a custom checkout location:
214
- TEAMS_PY_PATH=/path/to/teams.py bundle exec rake cards:generate
65
+ teams app create \
66
+ --name my-teams-ruby-bot \
67
+ --endpoint https://<your-tunnel>/api/messages
215
68
  ```
216
69
 
217
- The SDK-parity comparison workflow additionally uses sibling clones of `teams.ts` and `teams.net`, as described in the porting workspace's `AGENTS.md`.
70
+ The command prints `CLIENT_ID`, `CLIENT_SECRET`, `TENANT_ID`, and an **Install in Teams** link. You can also create the app manually in the [Teams Developer Portal](https://dev.teams.microsoft.com/apps).
218
71
 
219
- The full Adaptive Card schema (112 typed classes) is available under `Teams::Cards`, generated from the Python SDK's card models and golden-tested to serialize identically. Cards serialize with the same defaults the other SDKs emit. Raw card JSON via `add_card(hash)` remains available as an escape hatch. Two live-verified gotchas: some card fields are server-side enums (for example `CodeBlock` `language:` — Teams rejects the whole message for values outside the enum), and a card `Action.Submit` arrives as a message activity with `nil` text, an ephemeral id, and the inputs in `value` — answer it with `ctx.post`, since quote-replying to the invisible submit activity is rejected by Teams.
72
+ ### Run it
220
73
 
221
- For streamed responses, use `ctx.stream`:
74
+ This standalone example uses Puma; `teams_rb` itself does not require a server gem. Start the bot with the credentials printed by the CLI:
222
75
 
223
- ```ruby
224
- teams.on_message do |ctx|
225
- ctx.stream.update("Thinking...")
226
- ctx.stream.emit("Hello")
227
- ctx.stream.emit(", world")
228
- end
229
- ```
230
-
231
- The stream emits events: `on_chunk` fires with the `SentActivity` of every sent chunk, and `on_close` fires with the final `SentActivity` when the stream finalizes. Handlers persist across stream reuse:
232
-
233
- ```ruby
234
- ctx.stream.on_chunk { |sent| logger.debug("chunk #{sent.id}") }
235
- ctx.stream.on_close { |sent| MessageLog.record(sent.id) }
236
- ```
237
-
238
- Emits are queued and flushed by a background thread, matching the TypeScript and Python streamers: rapid emits coalesce into fewer chunks (spaced to respect Teams rate limits), transient send failures retry with backoff, and `close` waits for the queue to drain before sending the final message. Emitting again after `ctx.stream.close` starts a new streamed message on the same stream. If Teams stops a stream, the SDK raises typed errors: `Teams::StreamCancelledError` when the user cancels (sets the sticky `canceled` flag and makes the next `emit` raise), and `Teams::StreamNotAllowedError` or `Teams::TerminalStreamError` for terminal streaming failures — chunk-send errors are recorded on the stream and surface when `close` sends the final message. A stream that exceeds the Teams two-minute streaming limit finalizes automatically by updating the streamed message in place. Note that a card-only stream (no text ever emitted) sends nothing, like the other SDKs: emit text chunks first, then `clear_text` and emit the card as the final message.
239
-
240
- To mark a final message as AI-generated, use `add_ai_generated` on `MessageActivity`. This also works as the final streamed message metadata:
241
-
242
- ```ruby
243
- teams.on_message do |ctx|
244
- ctx.stream.update("Thinking...")
245
- ctx.stream.emit("Hello")
246
- ctx.stream.emit("! I'm a friendly AI bot. ")
247
- ctx.stream.emit(Teams::Api::MessageActivity.new.add_ai_generated)
248
- end
76
+ ```sh
77
+ CLIENT_ID=... CLIENT_SECRET=... TENANT_ID=... bundle exec puma -p 3978
249
78
  ```
250
79
 
251
- Dialogs (Teams task modules) open from a card action whose data carries `msteams: { type: "task/fetch" }`. Route them with `on_dialog_open` / `on_dialog_submit`; the reserved `dialog_id` and `action` data keys select specific handlers, matching the other SDKs. The handler's return value — a `Teams::Api::TaskModuleResponse` (or an equivalent hash) — becomes the invoke response Teams renders:
80
+ Open the install link and send the bot a message. It receives Teams activities at `POST /api/messages`, validates each request, and replies to incoming messages.
252
81
 
253
- ```ruby
254
- teams.on_message(/^form$/i) do |ctx|
255
- ctx.post Teams::Api::MessageActivity.new.add_card(
256
- Teams::Cards::AdaptiveCard.new(
257
- Teams::Cards::TextBlock.new("Open the form"),
258
- actions: [Teams::Cards::SubmitAction.new(
259
- title: "Open",
260
- data: { "msteams" => { "type" => "task/fetch" }, "dialog_id" => "simple_form" }
261
- )]
262
- )
263
- )
264
- end
82
+ See [Running in Teams](docs/getting-started/running-in-teams.md) for the complete registration, tunnel, and installation walkthrough.
265
83
 
266
- teams.on_dialog_open("simple_form") do |ctx|
267
- Teams::Api::TaskModuleResponse.new(
268
- Teams::Api::TaskModuleContinueResponse.new(
269
- Teams::Api::TaskModuleTaskInfo.new(title: "Simple Form", card: dialog_card)
270
- )
271
- )
272
- end
84
+ ## Code basics
273
85
 
274
- teams.on_dialog_submit("submit_simple_form") do |ctx|
275
- ctx.post "Hi #{ctx.activity.value.data["name"]}!"
276
- Teams::Api::TaskModuleResponse.new(Teams::Api::TaskModuleMessageResponse.new("Form was submitted"))
277
- end
278
- ```
279
-
280
- `TaskModuleTaskInfo` takes `card:` (an `AdaptiveCard`, card hash, or ready attachment — cards are wrapped into an attachment automatically) or `url:` for webpage dialogs, plus `title:`, `height:`/`width:` (`"small"`/`"medium"`/`"large"` or pixels), `fallback_url:`, and `completion_bot_id:`. Returning a `TaskModuleContinueResponse` from a submit handler chains multi-step dialogs; a `TaskModuleMessageResponse` shows a message and closes.
281
-
282
- Message extensions (compose extensions) route the `composeExtension/*` invokes with the same handler names as the Python SDK: `on_message_ext_query`, `on_message_ext_select_item`, `on_message_ext_submit`, `on_message_ext_open` (fetchTask), `on_message_ext_query_link`, `on_message_ext_anon_query_link`, `on_message_ext_query_settings_url`, `on_message_ext_setting`, and `on_message_ext_card_button_clicked`. The commands themselves are declared in the Teams app manifest; query handlers return a `MessagingExtensionResponse`, action handlers a `MessagingExtensionActionResponse` (which can open a dialog via `task:`, reusing the task module responses):
86
+ Handlers can match message text and respond with typed Adaptive Cards:
283
87
 
284
88
  ```ruby
285
- teams.on_message_ext_query do |ctx|
286
- query = ctx.activity.value.parameters.find { |p| p["name"] == "searchQuery" }&.dig("value")
287
- results = Item.search(query).map do |item|
288
- Teams::Api::MessagingExtensionAttachment.new(
289
- content_type: "application/vnd.microsoft.card.adaptive",
290
- content: item.to_card,
291
- preview: { "contentType" => "application/vnd.microsoft.card.thumbnail",
292
- "content" => { "title" => item.title } }
293
- )
294
- end
295
-
296
- Teams::Api::MessagingExtensionResponse.new(
297
- Teams::Api::MessagingExtensionResult.new(type: "result", attachment_layout: "list", attachments: results)
298
- )
299
- end
89
+ teams.on_message(/^status$/i) do |ctx|
90
+ ctx.typing
300
91
 
301
- teams.on_message_ext_query_link do |ctx|
302
- card = unfurl(ctx.activity.value.raw["url"])
303
- Teams::Api::MessagingExtensionResponse.new(
304
- Teams::Api::MessagingExtensionResult.new(type: "result", attachment_layout: "list", attachments: [card])
92
+ card = Teams::Cards::AdaptiveCard.new(
93
+ Teams::Cards::TextBlock.new("Service status", size: "Large", weight: "Bolder"),
94
+ Teams::Cards::TextBlock.new("All systems operational.", wrap: true)
305
95
  )
306
- end
307
- ```
308
-
309
- User sign-in (OAuth) needs an OAuth connection configured on the bot's Azure registration (name it to match `default_connection_name`, default `"graph"`). `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 — token exchange for silent SSO, verify-state for the interactive card — and hand the token to `on_sign_in`:
310
-
311
- ```ruby
312
- teams = Teams::App.new(default_connection_name: "graph")
313
-
314
- teams.on_message(/^login$/i) do |ctx|
315
- token = ctx.sign_in
316
- ctx.reply "Already signed in!" if token
317
- end
318
-
319
- teams.on_sign_in do |ctx, token|
320
- ctx.post "Welcome! You are signed in."
321
- # token.token is the user's access token for the connection's scopes
322
- end
323
-
324
- teams.on_error do |error, activity|
325
- # unexpected OAuth failures and client-reported sign-in failures
326
- end
327
- ```
328
-
329
- `ctx.sign_out` clears the token. Handlers registered on `on_signin_token_exchange` / `on_signin_verify_state` / `on_signin_failure` run after the defaults for custom behavior. The lower-level surface lives on `teams.api.users` (`get_token`, `get_aad_tokens`, `get_token_status`, `sign_out`, `exchange_token`) and `teams.api.bots.sign_in` (`get_url`, `get_resource`) against the Bot Framework token service.
330
-
331
- Microsoft Graph is available through a thin request client (following the TypeScript SDK's core Graph client; the generated endpoint packages are not ported — the raw request surface is the API, like TypeScript's `client.http` escape hatch). `teams.graph` / `ctx.app_graph` use the app's own identity (app-only tokens via the client-credentials flow — grant the app *application* permissions in Entra for these). `ctx.user_graph` uses the signed-in user's token and raises if the user hasn't signed in:
332
-
333
- ```ruby
334
- teams.on_message(/^whoami$/i) do |ctx|
335
- me = ctx.user_graph.get("/me") # requires prior ctx.sign_in
336
- ctx.reply "You are #{me["displayName"]} (#{me["userPrincipalName"]})"
337
- end
338
-
339
- app_info = teams.graph.get("/applications", params: { "$top" => 1 })
340
- teams.graph.post("/users/#{user_id}/sendMail", json: { message: { subject: "Hi" } })
341
- ```
342
-
343
- `get`/`post`/`patch`/`put`/`delete` take a path relative to `/v1.0`, return parsed hashes, and raise `Teams::GraphError` (with `status`, the Graph error `code`, and the full `body`) on failure. Sovereign clouds route automatically from the configured cloud's graph scope.
344
-
345
- Remote functions let a tab (or any Teams-hosted web page) call your bot backend as the signed-in user. The page acquires an Entra token through the Teams JS SDK and POSTs to `/api/functions/{name}` with the token plus the Teams client-context headers; the SDK validates the token against your app registration (client id audience forms, tenant issuer) and requires the `oid`/`tid`/`name` claims:
346
-
347
- ```ruby
348
- teams.on_function("create-ticket") do |ctx|
349
- ticket = Ticket.create!(title: ctx.data["title"], creator_oid: ctx.user_id)
350
- ctx.post "#{ctx.user_name} created ticket #{ticket.id} from the tab"
351
- { "id" => ticket.id }
352
- end
353
- ```
354
-
355
- `ctx.data` is the parsed JSON body; identity (`user_id`, `tenant_id`, `user_name`) comes from the validated token; the client context (`chat_id`, `channel_id`, `meeting_id`, `team_id`, `page_id`, `app_session_id`, …) comes from the `X-Teams-*` headers. `ctx.conversation_id` resolves the chat/channel after validating the user's membership — or creates the 1:1 conversation in personal scope — and `ctx.post` sends into it proactively. The handler's return value becomes the JSON response body; invalid requests get `401` with a `detail` message.
356
96
 
357
- For @mentions, use `add_mention` on the outbound message and the mention readers on inbound activities:
358
-
359
- ```ruby
360
- ctx.post Teams::Api::MessageActivity.new("ping ").add_mention(ctx.activity.from.to_h)
361
-
362
- teams.on_message do |ctx|
363
- if ctx.activity.recipient_mentioned?
364
- ctx.reply "You said: #{ctx.activity.strip_mentions_text}"
365
- end
97
+ ctx.post card
366
98
  end
367
99
  ```
368
100
 
369
- To mark a message with a content sensitivity label:
370
-
371
- ```ruby
372
- ctx.post Teams::Api::MessageActivity.new("Q3 numbers...").add_sensitivity_label(
373
- "Confidential",
374
- description: "Internal use only"
375
- )
376
- ```
377
-
378
- The label is informational: Teams renders a shield icon whose popup shows the name in bold, the description underneath, and an automatic "Sensitivity set by {bot}" attribution. Name and description are free text. The optional `pattern:` (a schema.org DefinedTerm hash) is carried on the wire but not rendered by the Teams client. No enforcement or Microsoft Purview integration is attached.
379
-
380
- For citations, include the matching inline position marker in the text and add the citation to the message activity. The optional citation `text:` must be a stringified Adaptive Card (it renders in a modal when the citation is clicked); passing plain prose makes Teams reject the whole message with `400 BadSyntax`:
101
+ The official SDKs call plain message sending `send`. Ruby already defines `Object#send`, so `teams_rb` intentionally uses `post`. This is the one deliberate public API naming difference.
381
102
 
382
- ```ruby
383
- message = Teams::Api::MessageActivity.new("The policy allows this [1].")
384
- .add_ai_generated
385
- .add_citation(
386
- 1,
387
- Teams::Api::CitationAppearance.new(
388
- name: "Policy Guide",
389
- abstract: "Relevant policy excerpt",
390
- url: "https://example.com/policy",
391
- icon: "PDF"
392
- )
393
- )
103
+ For Rails, define one `Teams::App` instance during boot and route `POST /api/messages` to `app.to_rack`. See [App basics](docs/essentials/app-basics.md#serving) for the Rack and Rails forms.
394
104
 
395
- ctx.post message
396
- ```
105
+ ## Feature status
397
106
 
398
- Citation `name` and `abstract` are required. Teams expects `name` to be at most 80 characters and `abstract` to be at most 160 characters. Keywords are documented as limited to 3 items, each at most 28 characters.
107
+ This table tracks supported Teams SDK capabilities. Deprecated upstream AI and devtools packages are intentionally excluded.
399
108
 
400
- To show Teams' built-in feedback controls on a message:
401
-
402
- ```ruby
403
- ctx.post Teams::Api::MessageActivity.new("Was this helpful?").add_feedback
404
- ```
405
-
406
- For a custom feedback dialog flow, use `custom`:
407
-
408
- ```ruby
409
- ctx.post Teams::Api::MessageActivity.new("Was this helpful?").add_feedback("custom")
410
- ```
411
-
412
- For Adaptive Cards, use `Teams::Cards` objects directly or wrap them in a message activity:
413
-
414
- ```ruby
415
- card = Teams::Cards::AdaptiveCard.new(
416
- Teams::Cards::TextBlock.new("Create ticket", weight: "Bolder", size: "Large", wrap: true),
417
- Teams::Cards::TextInput.new(id: "title", label: "Title", is_required: true),
418
- actions: [
419
- Teams::Cards::SubmitAction.new(title: "Create", data: { action: "create_ticket" })
420
- ]
421
- )
422
-
423
- ctx.post card
424
- ctx.reply card
425
- ctx.post Teams::Api::MessageActivity.new.add_card(card)
426
- ```
109
+ | Feature | Status | Guide |
110
+ |---|---|---|
111
+ | App setup and Rack/Rails integration | ✅ Done | [App basics](docs/essentials/app-basics.md) |
112
+ | Activity routing and middleware | ✅ Done | [Listening to activities](docs/essentials/on-activity.md) |
113
+ | App and error events | ✅ Done | [Listening to events](docs/essentials/on-event.md) |
114
+ | Typed activity models and raw payload access | ✅ Done | [Code basics](docs/getting-started/code-basics.md) |
115
+ | Posts, replies, updates, typing, formatting, mentions, citations, and labels | ✅ Done | [Sending messages](docs/essentials/sending-messages.md) |
116
+ | Proactive messaging and conversation references | ✅ Done | [Proactive messaging](docs/essentials/proactive-messaging.md) |
117
+ | Conversations, teams, meetings, users, and bot sign-in APIs | ✅ Done | [API client](docs/essentials/api-client.md) |
118
+ | Adaptive Cards | ✅ Done | [Adaptive Cards](docs/in-depth-guides/adaptive-cards.md) |
119
+ | Dialogs | ✅ Done | [Dialogs](docs/in-depth-guides/dialogs.md) |
120
+ | Message extensions | ✅ Done | [Message extensions](docs/in-depth-guides/message-extensions.md) |
121
+ | Streaming responses | Done | [Streaming](docs/in-depth-guides/streaming.md) |
122
+ | OAuth user authentication | ✅ Done | [User authentication](docs/in-depth-guides/user-authentication.md) |
123
+ | Microsoft Graph | ✅ Done | [Microsoft Graph](docs/essentials/graph.md) |
124
+ | Tabs and remote functions | ✅ Done | [Tabs and remote functions](docs/in-depth-guides/tabs.md) |
125
+ | Feedback | Done | [Feedback](docs/in-depth-guides/feedback.md) |
126
+ | Message reactions | Done | [Message reactions](docs/in-depth-guides/message-reactions.md) |
127
+ | Meeting events and notifications | ✅ Done | [Meeting events](docs/in-depth-guides/meeting-events.md) |
128
+ | Inbound authentication and bot tokens | ✅ Done | [App authentication](docs/essentials/app-authentication.md) |
129
+ | Sovereign cloud support | ✅ Done | [Sovereign clouds](docs/essentials/sovereign-cloud.md) |
130
+ | Logging and observability | ✅ Done | [Observability](docs/in-depth-guides/observability.md) |
427
131
 
428
- ## Configuration
429
-
430
- `Teams::App.new` reads its configuration from the environment by default; every value can also be passed explicitly as a keyword argument:
431
-
432
- | Env var | Keyword | Required | Purpose |
433
- |---|---|---|---|
434
- | `CLIENT_ID` | `client_id:` | production | The bot's Microsoft App ID. Used for bot token requests and to validate inbound JWT audiences. |
435
- | `CLIENT_SECRET` | `client_secret:` | production | The bot's client secret for the client-credentials token flow. |
436
- | `TENANT_ID` | `tenant_id:` | single-tenant bots | Entra tenant for bot tokens and tenant-issuer JWT validation. |
437
- | `SERVICE_URL` | `service_url:` | no | Default Bot Framework service URL for proactive sends (defaults to `https://smba.trafficmanager.net/teams`). Inbound requests always use the service URL from the activity. |
438
- | — | `skip_auth:` | no | Disables inbound request validation. Local development only. |
439
- | — | `messaging_endpoint:` | no | Inbound path, defaults to `/api/messages`. |
440
- | — | `logger:`, `storage:`, `cloud:` | no | Logger (defaults to stdout), state store (defaults to the in-memory store), and cloud environment for sovereign clouds. |
441
-
442
- For local tests only:
443
-
444
- ```ruby
445
- teams = Teams::App.new(skip_auth: true)
446
- ```
447
-
448
- Production apps must provide `CLIENT_ID`, `CLIENT_SECRET`, and `TENANT_ID`. Without credentials the app logs a startup warning and rejects every inbound request unless `skip_auth: true` was set explicitly — the same behavior as the TypeScript, Python, and .NET SDKs.
449
-
450
- ## Rails
451
-
452
- Define the app once (an initializer works well) and route the messaging endpoint to it:
453
-
454
- ```ruby
455
- # config/initializers/teams_bot.rb
456
- TEAMS_BOT = Teams::App.new
457
-
458
- TEAMS_BOT.on_message do |ctx|
459
- ctx.reply "Hello from Rails"
460
- end
461
- ```
462
-
463
- ```ruby
464
- # config/routes.rb
465
- post "/api/messages" => TEAMS_BOT.to_rack
466
- ```
132
+ ## Documentation
467
133
 
468
- Routing the exact path (rather than `mount`) keeps the request's full path intact, which the endpoint check relies on. If you prefer `mount`, mount at root — `mount TEAMS_BOT.to_rack => "/"` — and let the SDK's own endpoint matching answer 404 for everything else; register whichever full URL you chose with Teams.
134
+ - [Getting started](docs/getting-started/README.md) quickstart, code basics, and running in Teams
135
+ - [Essentials](docs/essentials/README.md) — app setup, activities, sending, proactive messaging, API clients, authentication, and Graph
136
+ - [In-depth guides](docs/in-depth-guides/README.md) — cards, dialogs, extensions, streaming, user authentication, tabs, and events
137
+ - [Examples](examples/README.md) — runnable Rack apps for common SDK features
138
+ - [Changelog](CHANGELOG.md) — release history
469
139
 
470
- Handlers run inside the web request, so treat them like controller actions: keep them fast, and push slow work (LLM calls, big queries) to a job, then deliver the result proactively with the stored conversation reference. Remember that Bot Framework delivers at-least-once — if a handler's side effect must not repeat, dedupe by `ctx.activity.id` before performing it:
140
+ ## Development
471
141
 
472
- ```ruby
473
- teams.on_message do |ctx|
474
- next if ProcessedActivity.exists?(activity_id: ctx.activity.id)
475
- ProcessedActivity.create!(activity_id: ctx.activity.id)
142
+ Install dependencies and run the Minitest suite:
476
143
 
477
- answer = Assistant.answer(user: ctx.activity.from.aad_object_id, text: ctx.activity.text)
478
- ctx.stream.emit(answer)
479
- end
144
+ ```sh
145
+ bundle install
146
+ bundle exec rake test
480
147
  ```
481
148
 
482
- ## Local development
149
+ The gem is self-contained. Regenerating the typed Adaptive Card classes requires a sibling checkout of Microsoft's Python SDK; see the [Adaptive Cards guide](docs/in-depth-guides/adaptive-cards.md#regenerating).
483
150
 
484
- 1. Register a bot (Microsoft's [Teams CLI](https://github.com/microsoft/teams-sdk) or the Developer Portal) and note the client id, client secret, and tenant id. Put them in `.env` as `CLIENT_ID`, `CLIENT_SECRET`, `TENANT_ID`.
485
- 2. Expose your local port with a persistent tunnel, e.g. Dev Tunnels: `devtunnel create teams-bot -a && devtunnel port create teams-bot -p 3978`, then `devtunnel host teams-bot`.
486
- 3. Set the bot's messaging endpoint to `https://<your-tunnel>/api/messages` in the bot registration, and install the app in Teams.
487
- 4. Run the app: `bundle exec rackup -p 3978 -o 0.0.0.0`. Inbound requests are JWT-validated with your real credentials — no `skip_auth` needed behind a tunnel.
151
+ ## Questions and issues
488
152
 
489
- If the tunnel URL is stable (Dev Tunnels URLs are), steps 1–3 are one-time setup.
153
+ Use [GitHub Issues](https://github.com/bavmind/teams_rb/issues) for bug reports and feature requests.
490
154
 
491
- ## API reference
155
+ ## License
492
156
 
493
- | Surface | Methods |
494
- |---|---|
495
- | Routing | `teams.on_message(pattern = nil)`, `on_message_update`, `on_edit_message`, `on_undelete_message`, `on_dialog_open(dialog_id = nil)`, `on_dialog_submit(action = nil)`, `on_message_ext_*` (nine composeExtension routes), `on_meeting_start`, `on_meeting_end`, `on_message_submit_feedback`, `on_suggested_action_submit`, `on(type)` (escape hatch), `use` (middleware, `(ctx, next)`) |
496
- | Context (`ctx`) | `activity`, `ref` / `conversation_reference`, `post`, `reply`, `quote(message_id, ...)`, `update(activity_id, ...)`, `typing(text = nil)`, `stream` (`emit`, `update`, `clear_text`, `close`, `on_chunk`, `on_close`), `api`, `storage`, `log` |
497
- | Proactive (`teams`) | `post(conversation_id, activity)`, `reply(conversation_id, activity_id, activity)`, `update(conversation_id, activity_id, activity)`, `send_activity(reference, activity)` |
498
- | API client (`teams.api`) | `conversations` (`create`, `create_activity`, `reply_to_activity`, `update_activity`, `delete_activity`, targeted variants, `get_members`, `get_member_by_id`, `get_paged_members`, `get_activity_members`, `add_reaction`, `delete_reaction`), `teams` (`get_by_id`, `get_conversations`), `meetings` (`get_by_id`, `get_participant`, `send_notification`) |
499
- | Sends return | `Teams::Api::SentActivity` (outbound activity merged with the server response; `#id`, `#[]`, `#to_h`) |
500
- | Auth | Inbound JWTs validated against Bot Framework/Entra issuers, the three audience forms, expiry/nbf, signature, and the `serviceurl` claim. Outbound bot tokens via client-credentials flow, cached with refresh skew. `AuthenticationError` → 401, `BadRequestError` → 400, handler errors → 500 (Bot Framework then redelivers). |
157
+ `teams_rb` is available under the [MIT License](LICENSE).
@@ -12,7 +12,7 @@ Every `POST /api/messages` request must carry a Bot Framework JWT. The app valid
12
12
  - **Expiry / not-before**
13
13
  - **`serviceurl` claim** — must match the activity's service URL, preventing spoofed routing
14
14
 
15
- Rejected requests get a 401 with a warn log line. Requests are rejected wholesale when no credentials are configured (with a loud startup warning) unless `skip_auth: true` is set explicitly — which is for local development only.
15
+ Rejected requests get a 401 with a warn log line. Requests are rejected wholesale when no credentials are configured (with a loud startup warning) unless `dangerously_allow_unauthenticated_requests: true` is set explicitly — which is for local development only. The option can also come from the `DANGEROUSLY_ALLOW_UNAUTHENTICATED_REQUESTS` environment variable (`true`/`false`, `1`/`0`, `yes`/`no`, or `on`/`off`); the older `skip_auth:` name still works as a deprecated alias.
16
16
 
17
17
  ## Outbound bot tokens
18
18
 
@@ -10,12 +10,12 @@
10
10
  | `CLIENT_SECRET` | `client_secret:` | production | Client secret for the client-credentials flow |
11
11
  | `TENANT_ID` | `tenant_id:` | single-tenant | Entra tenant for bot tokens and tenant-issuer validation |
12
12
  | `SERVICE_URL` | `service_url:` | no | Default Bot Framework URL for proactive sends (inbound requests always use the activity's own service URL) |
13
- | | `skip_auth:` | no | Disables inbound validation — local development only |
13
+ | `DANGEROUSLY_ALLOW_UNAUTHENTICATED_REQUESTS` | `dangerously_allow_unauthenticated_requests:` | no | Disables inbound validation — local development only (`skip_auth:` is a deprecated alias) |
14
14
  | — | `messaging_endpoint:` | no | Inbound path, default `/api/messages` |
15
15
  | — | `default_connection_name:` | no | OAuth connection name for user sign-in, default `"graph"` |
16
16
  | — | `logger:`, `storage:`, `cloud:` | no | Logger (stdout default), state store (in-memory default), cloud environment for sovereign clouds |
17
17
 
18
- Without credentials the app logs a startup warning and rejects every inbound request unless `skip_auth: true` was set explicitly — the same behavior as the TypeScript, Python, and .NET SDKs.
18
+ Without credentials the app logs a startup warning and rejects every inbound request unless `dangerously_allow_unauthenticated_requests: true` was set explicitly — the same behavior as the TypeScript, Python, and .NET SDKs.
19
19
 
20
20
  ## One app instance
21
21