teams_rb 2.0.1 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +48 -1
- data/README.md +94 -434
- data/docs/essentials/app-authentication.md +14 -4
- data/docs/essentials/app-basics.md +2 -2
- data/docs/essentials/on-activity.md +27 -0
- data/docs/essentials/sending-messages.md +1 -0
- data/docs/getting-started/quickstart.md +6 -5
- data/docs/getting-started/running-in-teams.md +37 -14
- data/docs/in-depth-guides/README.md +1 -0
- data/docs/in-depth-guides/adaptive-cards.md +17 -0
- data/docs/in-depth-guides/agent-365.md +46 -0
- data/docs/in-depth-guides/user-authentication.md +1 -1
- data/lib/teams/activity.rb +5 -0
- data/lib/teams/activity_context.rb +17 -31
- data/lib/teams/api/account.rb +38 -0
- data/lib/teams/api/activity_value.rb +92 -0
- data/lib/teams/api/agentic_identity.rb +27 -0
- data/lib/teams/api/app_info.rb +15 -0
- data/lib/teams/api/channel_data.rb +5 -0
- data/lib/teams/api/client.rb +26 -2
- data/lib/teams/api/conversation_client.rb +3 -1
- data/lib/teams/api/message_activity.rb +1 -0
- data/lib/teams/api/search.rb +41 -0
- data/lib/teams/app.rb +184 -21
- data/lib/teams/auth/jwt_validator.rb +173 -68
- data/lib/teams/auth/token_manager.rb +132 -0
- data/lib/teams/cloud_environment.rb +5 -1
- data/lib/teams/http_stream.rb +8 -3
- data/lib/teams/router.rb +71 -0
- data/lib/teams/version.rb +1 -1
- data/lib/teams.rb +3 -0
- metadata +20 -2
data/README.md
CHANGED
|
@@ -1,40 +1,41 @@
|
|
|
1
|
-
# teams_rb
|
|
1
|
+
# teams_rb — Teams SDK for Ruby
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://rubygems.org/gems/teams_rb)
|
|
4
|
+
[](https://github.com/bavmind/teams_rb/actions/workflows/ci.yml)
|
|
5
|
+
[](docs/getting-started/README.md)
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
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
8
|
|
|
7
|
-
|
|
9
|
+
`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
10
|
|
|
9
|
-
-
|
|
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
|
|
11
|
+
> **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
12
|
|
|
19
|
-
##
|
|
20
|
-
|
|
21
|
-
Full documentation lives in [`docs/`](docs/README.md), structured like the official SDK docs:
|
|
13
|
+
## Getting started
|
|
22
14
|
|
|
23
|
-
|
|
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
|
|
15
|
+
### Prerequisites
|
|
26
16
|
|
|
27
|
-
|
|
17
|
+
- Ruby 4.0 or newer
|
|
18
|
+
- A Microsoft 365 tenant with custom app upload enabled
|
|
19
|
+
- A public HTTPS tunnel for local development, such as [Dev Tunnels](https://learn.microsoft.com/azure/developer/dev-tunnels/get-started)
|
|
28
20
|
|
|
29
|
-
|
|
21
|
+
### Install
|
|
30
22
|
|
|
31
|
-
|
|
23
|
+
Add the SDK and a Rack server for this standalone example to your `Gemfile`:
|
|
32
24
|
|
|
33
25
|
```ruby
|
|
34
|
-
gem "teams_rb"
|
|
26
|
+
gem "teams_rb"
|
|
27
|
+
gem "puma"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Then install it:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
bundle install
|
|
35
34
|
```
|
|
36
35
|
|
|
37
|
-
|
|
36
|
+
### Create your first bot
|
|
37
|
+
|
|
38
|
+
Create a `config.ru`:
|
|
38
39
|
|
|
39
40
|
```ruby
|
|
40
41
|
require "teams"
|
|
@@ -43,458 +44,117 @@ teams = Teams::App.new
|
|
|
43
44
|
|
|
44
45
|
teams.on_message do |ctx|
|
|
45
46
|
ctx.typing
|
|
46
|
-
|
|
47
|
-
ctx.reply "reply: #{ctx.activity.text.inspect}"
|
|
48
|
-
ctx.post "post: #{ctx.activity.text.inspect}"
|
|
47
|
+
ctx.reply "echo: #{ctx.activity.text}"
|
|
49
48
|
end
|
|
50
49
|
|
|
51
50
|
run teams.to_rack
|
|
52
51
|
```
|
|
53
52
|
|
|
54
|
-
|
|
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`.
|
|
53
|
+
### Register it with Teams
|
|
177
54
|
|
|
178
|
-
|
|
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:
|
|
55
|
+
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:
|
|
210
56
|
|
|
211
57
|
```sh
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
58
|
+
npm install -g @microsoft/teams.cli
|
|
59
|
+
teams login
|
|
60
|
+
teams status
|
|
215
61
|
```
|
|
216
62
|
|
|
217
|
-
|
|
63
|
+
Start your HTTPS tunnel, then let the CLI create the app, bot registration, and install link:
|
|
218
64
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
65
|
+
```sh
|
|
66
|
+
teams app create \
|
|
67
|
+
--name my-teams-ruby-bot \
|
|
68
|
+
--endpoint https://<your-tunnel>/api/messages
|
|
229
69
|
```
|
|
230
70
|
|
|
231
|
-
The
|
|
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
|
-
```
|
|
71
|
+
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).
|
|
237
72
|
|
|
238
|
-
|
|
73
|
+
### Run it
|
|
239
74
|
|
|
240
|
-
|
|
75
|
+
This standalone example uses Puma; `teams_rb` itself does not require a server gem. Start the bot with the credentials printed by the CLI:
|
|
241
76
|
|
|
242
|
-
```
|
|
243
|
-
|
|
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
|
|
77
|
+
```sh
|
|
78
|
+
CLIENT_ID=... CLIENT_SECRET=... TENANT_ID=... bundle exec puma -p 3978
|
|
249
79
|
```
|
|
250
80
|
|
|
251
|
-
|
|
81
|
+
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
82
|
|
|
253
|
-
|
|
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
|
|
83
|
+
See [Running in Teams](docs/getting-started/running-in-teams.md) for the complete registration, tunnel, and installation walkthrough.
|
|
265
84
|
|
|
266
|
-
|
|
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.
|
|
85
|
+
## Code basics
|
|
281
86
|
|
|
282
|
-
|
|
87
|
+
Handlers can match message text and respond with typed Adaptive Cards:
|
|
283
88
|
|
|
284
89
|
```ruby
|
|
285
|
-
teams.
|
|
286
|
-
|
|
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
|
|
90
|
+
teams.on_message(/^status$/i) do |ctx|
|
|
91
|
+
ctx.typing
|
|
300
92
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
Teams::Api::MessagingExtensionResult.new(type: "result", attachment_layout: "list", attachments: [card])
|
|
93
|
+
card = Teams::Cards::AdaptiveCard.new(
|
|
94
|
+
Teams::Cards::TextBlock.new("Service status", size: "Large", weight: "Bolder"),
|
|
95
|
+
Teams::Cards::TextBlock.new("All systems operational.", wrap: true)
|
|
305
96
|
)
|
|
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
97
|
|
|
355
|
-
|
|
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
|
|
98
|
+
ctx.post card
|
|
366
99
|
end
|
|
367
100
|
```
|
|
368
101
|
|
|
369
|
-
|
|
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.
|
|
102
|
+
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.
|
|
379
103
|
|
|
380
|
-
For
|
|
104
|
+
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.
|
|
381
105
|
|
|
382
|
-
|
|
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
|
-
)
|
|
106
|
+
## Feature status
|
|
394
107
|
|
|
395
|
-
|
|
396
|
-
```
|
|
108
|
+
This table tracks supported Teams SDK capabilities. Deprecated upstream AI and devtools packages are intentionally excluded.
|
|
397
109
|
|
|
398
|
-
|
|
110
|
+
| Feature | Status | Guide |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| App setup and Rack/Rails integration | ✅ Done | [App basics](docs/essentials/app-basics.md) |
|
|
113
|
+
| Activity routing and middleware | ✅ Done | [Listening to activities](docs/essentials/on-activity.md) |
|
|
114
|
+
| App and error events | ✅ Done | [Listening to events](docs/essentials/on-event.md) |
|
|
115
|
+
| Typed activity models and raw payload access | ✅ Done | [Code basics](docs/getting-started/code-basics.md) |
|
|
116
|
+
| Posts, replies, updates, typing, formatting, mentions, citations, and labels | ✅ Done | [Sending messages](docs/essentials/sending-messages.md) |
|
|
117
|
+
| Proactive messaging and conversation references | ✅ Done | [Proactive messaging](docs/essentials/proactive-messaging.md) |
|
|
118
|
+
| Conversations, teams, meetings, users, and bot sign-in APIs | ✅ Done | [API client](docs/essentials/api-client.md) |
|
|
119
|
+
| Adaptive Cards | ✅ Done | [Adaptive Cards](docs/in-depth-guides/adaptive-cards.md) |
|
|
120
|
+
| Dialogs | ✅ Done | [Dialogs](docs/in-depth-guides/dialogs.md) |
|
|
121
|
+
| Message extensions | ✅ Done | [Message extensions](docs/in-depth-guides/message-extensions.md) |
|
|
122
|
+
| Streaming responses | ✅ Done | [Streaming](docs/in-depth-guides/streaming.md) |
|
|
123
|
+
| OAuth user authentication | ✅ Done | [User authentication](docs/in-depth-guides/user-authentication.md) |
|
|
124
|
+
| Microsoft Graph | ✅ Done | [Microsoft Graph](docs/essentials/graph.md) |
|
|
125
|
+
| Tabs and remote functions | ✅ Done | [Tabs and remote functions](docs/in-depth-guides/tabs.md) |
|
|
126
|
+
| Feedback | ✅ Done | [Feedback](docs/in-depth-guides/feedback.md) |
|
|
127
|
+
| Message reactions | ✅ Done | [Message reactions](docs/in-depth-guides/message-reactions.md) |
|
|
128
|
+
| Meeting events and notifications | ✅ Done | [Meeting events](docs/in-depth-guides/meeting-events.md) |
|
|
129
|
+
| Inbound authentication and bot tokens | ✅ Done | [App authentication](docs/essentials/app-authentication.md) |
|
|
130
|
+
| Sovereign cloud support | ✅ Done | [Sovereign clouds](docs/essentials/sovereign-cloud.md) |
|
|
131
|
+
| Logging and observability | ✅ Done | [Observability](docs/in-depth-guides/observability.md) |
|
|
399
132
|
|
|
400
|
-
|
|
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
|
|
133
|
+
## Documentation
|
|
451
134
|
|
|
452
|
-
|
|
135
|
+
- [Getting started](docs/getting-started/README.md) — quickstart, code basics, and running in Teams
|
|
136
|
+
- [Essentials](docs/essentials/README.md) — app setup, activities, sending, proactive messaging, API clients, authentication, and Graph
|
|
137
|
+
- [In-depth guides](docs/in-depth-guides/README.md) — cards, dialogs, extensions, streaming, user authentication, tabs, and events
|
|
138
|
+
- [Examples](examples/README.md) — runnable Rack apps for common SDK features
|
|
139
|
+
- [Changelog](CHANGELOG.md) — release history
|
|
453
140
|
|
|
454
|
-
|
|
455
|
-
# config/initializers/teams_bot.rb
|
|
456
|
-
TEAMS_BOT = Teams::App.new
|
|
141
|
+
## Development
|
|
457
142
|
|
|
458
|
-
|
|
459
|
-
ctx.reply "Hello from Rails"
|
|
460
|
-
end
|
|
461
|
-
```
|
|
143
|
+
Install dependencies and run the Minitest suite:
|
|
462
144
|
|
|
463
|
-
```
|
|
464
|
-
|
|
465
|
-
|
|
145
|
+
```sh
|
|
146
|
+
bundle install
|
|
147
|
+
bundle exec rake test
|
|
466
148
|
```
|
|
467
149
|
|
|
468
|
-
|
|
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
|
-
```
|
|
150
|
+
GitHub Actions runs `bundle exec rake ci` on pushes and pull requests using Ruby 4.0. This runs the full test suite and builds the gem; use the same command locally to check both.
|
|
481
151
|
|
|
482
|
-
|
|
152
|
+
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
153
|
|
|
484
|
-
|
|
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.
|
|
154
|
+
## Questions and issues
|
|
488
155
|
|
|
489
|
-
|
|
156
|
+
Use [GitHub Issues](https://github.com/bavmind/teams_rb/issues) for bug reports and feature requests.
|
|
490
157
|
|
|
491
|
-
##
|
|
158
|
+
## License
|
|
492
159
|
|
|
493
|
-
|
|
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). |
|
|
160
|
+
`teams_rb` is available under the [MIT License](LICENSE).
|
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
# App authentication
|
|
2
2
|
|
|
3
|
-
How the app proves itself to Teams, and how Teams proves itself to the app.
|
|
3
|
+
How the app proves itself to Teams, and how Teams proves itself to the app.
|
|
4
4
|
|
|
5
5
|
## Inbound validation
|
|
6
6
|
|
|
7
|
-
Every `POST /api/messages` request must carry a Bot Framework JWT.
|
|
7
|
+
Every `POST /api/messages` request must carry a Bot Framework JWT. Token decoding, JWK import, RS256 signature verification, and standard claims use the established [`jwt` gem](https://github.com/jwt/ruby-jwt); Teams-specific endpoint selection, signing-key caching, and service URL checks remain in this SDK.
|
|
8
|
+
|
|
9
|
+
The app validates:
|
|
8
10
|
|
|
9
11
|
- **Signature** against Microsoft's published signing keys (RS256, JWKS fetched and cached)
|
|
10
12
|
- **Issuer** — the Bot Framework issuer, or your tenant's Entra issuers (v1 and v2 forms) when `TENANT_ID` is configured
|
|
11
13
|
- **Audience** — your app id, in any of the three accepted forms: the bare id, `api://{id}`, `api://botid-{id}`
|
|
12
|
-
- **Expiry / not-before**
|
|
14
|
+
- **Expiry / not-before** with five minutes of clock tolerance, matching the JavaScript Teams SDK
|
|
13
15
|
- **`serviceurl` claim** — must match the activity's service URL, preventing spoofed routing
|
|
14
16
|
|
|
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 `
|
|
17
|
+
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.
|
|
18
|
+
|
|
19
|
+
### Signing-key caching and rotation
|
|
20
|
+
|
|
21
|
+
The signing-key cache follows the defaults used by the [JavaScript Teams SDK](https://github.com/microsoft/teams.js/blob/3420e8899e71ed7cc74711d62f07c25e73d45b42/packages/apps/src/middleware/auth/jwt-validator.ts) and its [`jwks-rsa` 4.1.0 dependency](https://github.com/auth0/node-jwks-rsa/blob/v4.1.0/src/wrappers/cache.js): each JWKS endpoint has a least-recently-used cache of at most five public keys, indexed by JWT `kid`. Only the requested key is cached, even when a downloaded key document contains several keys.
|
|
22
|
+
|
|
23
|
+
Keys expire ten minutes after retrieval. Cache hits update recency but do not extend expiry. An uncached or expired key triggers a fresh JWKS fetch immediately, including when another key was fetched moments earlier. Misses are coordinated per JWKS endpoint, and waiting requests recheck the cache to share successful lookups. Cache reads and updates take only a brief shared mutex; HTTP requests run outside it, and Bot Framework discovery has separate synchronization. A slow key fetch or discovery request therefore does not block verification with another unexpired cached key. There is no background refresh, negative caching, retry cooldown, or fetch rate limit, matching the JavaScript SDK's current configuration. Arbitrary unknown key IDs can therefore cause repeated outbound requests; account for this when protecting your bot endpoint.
|
|
24
|
+
|
|
25
|
+
Key-fetch, discovery, and malformed key-response failures reject authentication with HTTP 401 and a warn log line through the Rack adapter. Failed lookups are not cached and may be retried immediately. Other unexpired cached keys remain usable, but expired keys are never used as a fallback during an outage. Bot Framework discovery still resolves `jwks_uri` once per validator; tenant keys still use the configured tenant endpoint. Existing issuer, audience, service URL, and RS256 checks remain in place; this matches JavaScript's cache defaults, not every feature of its authentication stack.
|
|
16
26
|
|
|
17
27
|
## Outbound bot tokens
|
|
18
28
|
|