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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b902f0ff9e413ee603c1f86505fbc6bc3160162c01502479b73cc58af1d0a85c
4
+ data.tar.gz: fd067a9990eed609b824d80cf5591f258964455f82c99657dd8e5492c7daed53
5
+ SHA512:
6
+ metadata.gz: f8bd5a77a2e98ffdaafd9201a0b1325b46305202f720d2632789b1a987e83307435a2645949f305e62435845dbeacab7c71c4b3fd6753d321a368b00e7b189a3
7
+ data.tar.gz: b026cd3ac95866f35ec8454308661b6f3d52c83b8aac2b3ea56327b510372536d67c1a823070f9492c4f085921c92f7a18ccf388c6afeffc8f56a82dc5cf8688
data/CHANGELOG.md ADDED
@@ -0,0 +1,47 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project adheres to
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [2.0.1] - 2026-07-13
10
+
11
+ ### Fixed
12
+
13
+ - `ctx.user_graph` now caches per connection name; requesting a different `connection_name:` returns a client for that connection instead of the first one fetched
14
+ - Remote function handlers returning `nil` now serialize as `{}`, keeping function responses valid JSON for `fetch(...).json()` callers
15
+ - Stream sequence/id updates are written under the stream mutex so `close` reliably observes the assigned stream id on non-GIL Ruby implementations
16
+ - Documented (in code) that the group-chat sign-in notice is deliberately posted to the group while the OAuth card goes to the 1:1 conversation, matching the TypeScript and Python SDKs
17
+
18
+ ## [2.0.0] - 2026-07-13
19
+
20
+ Initial release. A Ruby-native port of the Microsoft Teams SDK concepts, verified
21
+ against the TypeScript, Python, and .NET SDKs and live-tested in Microsoft Teams.
22
+
23
+ Versioning starts at 2.0.0 to align with the Teams SDK v2 generation that this
24
+ project ports (TypeScript, Python, and C# all release as 2.x) — the same choice
25
+ the Python SDK made when it joined the family.
26
+
27
+ ### Added
28
+
29
+ - Rack endpoint with inbound Bot Framework JWT validation (issuers, audiences, signature, `serviceurl` claim)
30
+ - Activity routing: messages (with patterns), message updates/edits, invokes, meeting events, middleware chain
31
+ - Sending: `post`, `reply`, `quote`, `update`, `typing`; text formats, mentions, sensitivity labels, citations, AI-generated labels, feedback; typed `SentActivity` returns
32
+ - Targeted (single-recipient) message support with automatic defaulting
33
+ - Proactive messaging with conversation references and conversation creation
34
+ - Streaming responses with background flushing, informative updates, stream events, typed stream errors, and reuse after close
35
+ - Adaptive Cards: all 112 element classes generated from the SDK card model, golden-tested for byte-identical serialization
36
+ - Dialogs (task modules): open/submit routing with dialog-id and action filters, multi-step forms
37
+ - Message extensions: all nine `composeExtension/*` routes with typed responses
38
+ - Full API client: conversations (activities, members, reactions, create), teams, meetings, user tokens, bot sign-in
39
+ - OAuth user sign-in: `ctx.sign_in`/`ctx.sign_out`, default token-exchange and verify-state handlers, `on_sign_in`/`on_error` events
40
+ - Microsoft Graph client with app identity (`teams.graph`) and user identity (`ctx.user_graph`)
41
+ - Remote functions callable from tabs with Entra token validation and client-context resolution
42
+ - Thread-safe token management, storage, and JWKS caching for multi-threaded Rack servers
43
+ - Documentation mirroring the official teams-sdk docs structure (getting started, essentials, in-depth guides)
44
+
45
+ [Unreleased]: https://github.com/bavmind/teams_rb/compare/v2.0.1...HEAD
46
+ [2.0.1]: https://github.com/bavmind/teams_rb/compare/v2.0.0...v2.0.1
47
+ [2.0.0]: https://github.com/bavmind/teams_rb/releases/tag/v2.0.0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Devran Cosmo Uenal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,500 @@
1
+ # teams_rb
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.
4
+
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
+
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
+
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
18
+
19
+ ## Documentation
20
+
21
+ Full documentation lives in [`docs/`](docs/README.md), structured like the official SDK docs:
22
+
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
26
+
27
+ The rest of this file is a condensed reference; the docs are the primary source.
28
+
29
+ ## Local Usage
30
+
31
+ From another Ruby app:
32
+
33
+ ```ruby
34
+ gem "teams_rb", path: "../teams_rb"
35
+ ```
36
+
37
+ Then:
38
+
39
+ ```ruby
40
+ require "teams"
41
+
42
+ teams = Teams::App.new
43
+
44
+ teams.on_message do |ctx|
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}"
49
+ end
50
+
51
+ run teams.to_rack
52
+ ```
53
+
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`.
105
+
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:
107
+
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)
205
+ ```
206
+
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:
210
+
211
+ ```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
215
+ ```
216
+
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`.
218
+
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.
220
+
221
+ For streamed responses, use `ctx.stream`:
222
+
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
249
+ ```
250
+
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:
252
+
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
265
+
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
273
+
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):
283
+
284
+ ```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
300
+
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])
305
+ )
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
+
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
366
+ end
367
+ ```
368
+
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`:
381
+
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
+ )
394
+
395
+ ctx.post message
396
+ ```
397
+
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.
399
+
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
+ ```
427
+
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
+ ```
467
+
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.
469
+
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:
471
+
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)
476
+
477
+ answer = Assistant.answer(user: ctx.activity.from.aad_object_id, text: ctx.activity.text)
478
+ ctx.stream.emit(answer)
479
+ end
480
+ ```
481
+
482
+ ## Local development
483
+
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.
488
+
489
+ If the tunnel URL is stable (Dev Tunnels URLs are), steps 1–3 are one-time setup.
490
+
491
+ ## API reference
492
+
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). |
data/docs/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # teams_rb documentation
2
+
3
+ `teams_rb` is 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)). It preserves the Teams SDK concepts and wire behavior while using Ruby idioms for the public API, and these docs follow the same structure as the official SDK documentation.
4
+
5
+ > **Unofficial.** This is an independent, community-maintained project, not affiliated with, endorsed by, or sponsored by Microsoft. For the official SDKs, see [github.com/microsoft/teams-sdk](https://github.com/microsoft/teams-sdk).
6
+
7
+ ## Getting started
8
+
9
+ 1. [Quickstart](getting-started/quickstart.md) — install the gem and run your first echo bot
10
+ 2. [Code basics](getting-started/code-basics.md) — the `App`, handlers, and the activity context
11
+ 3. [Running in Teams](getting-started/running-in-teams.md) — registration, tunnel, and manifest
12
+
13
+ ## Essentials
14
+
15
+ - [App basics](essentials/app-basics.md) — configuration, credentials, lifecycle
16
+ - [Listening to activities](essentials/on-activity.md) — routing, middleware, invokes, meeting events
17
+ - [Listening to events](essentials/on-event.md) — `on_sign_in`, `on_error`
18
+ - [Sending messages](essentials/sending-messages.md) — `post`, `reply`, `quote`, `update`, `typing`, formatting, mentions
19
+ - [Proactive messaging](essentials/proactive-messaging.md) — conversation references, creating conversations, threading
20
+ - [The API client](essentials/api-client.md) — `api.conversations`, `api.teams`, `api.meetings`, `api.users`, `api.bots`
21
+ - [App authentication](essentials/app-authentication.md) — inbound validation, bot tokens, the trust model
22
+ - [Sovereign clouds](essentials/sovereign-cloud.md) — government cloud endpoint routing
23
+ - [Microsoft Graph](essentials/graph.md) — app and user identity Graph clients
24
+
25
+ ## In-depth guides
26
+
27
+ - [Adaptive Cards](in-depth-guides/adaptive-cards.md) — the generated card classes, actions, and submissions
28
+ - [Dialogs](in-depth-guides/dialogs.md) — task modules: opening, submissions, multi-step forms
29
+ - [Message extensions](in-depth-guides/message-extensions.md) — search commands, action commands, link unfurling
30
+ - [Streaming](in-depth-guides/streaming.md) — chunked responses, informative updates, stream events
31
+ - [User authentication](in-depth-guides/user-authentication.md) — OAuth sign-in, token exchange, Azure setup
32
+ - [Tabs and remote functions](in-depth-guides/tabs.md) — calling your bot backend from a tab with SSO
33
+ - [Feedback](in-depth-guides/feedback.md) — thumbs up/down on bot messages
34
+ - [Message reactions](in-depth-guides/message-reactions.md) — sending and receiving reactions
35
+ - [Meeting events](in-depth-guides/meeting-events.md) — meeting start/end
36
+ - [Observability](in-depth-guides/observability.md) — logging and middleware
37
+
38
+ ## A note on naming
39
+
40
+ The Microsoft SDKs call their plain send operation `send`. Ruby already defines `Object#send` for dynamic dispatch, so `teams_rb` uses `post` — the one deliberate public API deviation. Everything else keeps the SDK family's shapes: Python-style snake_case method names, the same wire behavior, the same concepts.
@@ -0,0 +1,13 @@
1
+ # Essentials
2
+
3
+ The concepts every Teams app uses:
4
+
5
+ - [App basics](app-basics.md) — configuration, credentials, lifecycle, storage
6
+ - [Listening to activities](on-activity.md) — routing, middleware, invokes, the escape hatch
7
+ - [Listening to events](on-event.md) — `on_sign_in`, `on_error`
8
+ - [Sending messages](sending-messages.md) — the send verbs, `SentActivity`, formatting, mentions, labels, citations
9
+ - [Proactive messaging](proactive-messaging.md) — conversation references, creating conversations
10
+ - [The API client](api-client.md) — the full `teams.api` surface
11
+ - [App authentication](app-authentication.md) — inbound validation and bot tokens
12
+ - [Sovereign clouds](sovereign-cloud.md) — routing every endpoint from one cloud config
13
+ - [Microsoft Graph](graph.md) — app- and user-identity Graph access