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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -1
- data/README.md +92 -435
- data/docs/essentials/app-authentication.md +1 -1
- data/docs/essentials/app-basics.md +2 -2
- data/docs/essentials/on-activity.md +14 -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/adaptive-cards.md +17 -0
- data/docs/in-depth-guides/user-authentication.md +1 -1
- data/lib/teams/activity_context.rb +11 -28
- data/lib/teams/api/activity_value.rb +29 -0
- data/lib/teams/api/app_info.rb +15 -0
- data/lib/teams/api/channel_data.rb +5 -0
- 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 +64 -9
- data/lib/teams/http_stream.rb +3 -0
- data/lib/teams/router.rb +38 -0
- data/lib/teams/version.rb +1 -1
- data/lib/teams.rb +2 -0
- metadata +3 -1
data/README.md
CHANGED
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
# teams_rb
|
|
1
|
+
# teams_rb — Teams SDK for Ruby
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://rubygems.org/gems/teams_rb)
|
|
4
|
+
[](docs/getting-started/README.md)
|
|
4
5
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
##
|
|
20
|
-
|
|
21
|
-
Full documentation lives in [`docs/`](docs/README.md), structured like the official SDK docs:
|
|
12
|
+
## Getting started
|
|
22
13
|
|
|
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
|
|
14
|
+
### Prerequisites
|
|
26
15
|
|
|
27
|
-
|
|
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
|
-
|
|
20
|
+
### Install
|
|
30
21
|
|
|
31
|
-
|
|
22
|
+
Add the SDK and a Rack server for this standalone example to your `Gemfile`:
|
|
32
23
|
|
|
33
24
|
```ruby
|
|
34
|
-
gem "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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
65
|
+
teams app create \
|
|
66
|
+
--name my-teams-ruby-bot \
|
|
67
|
+
--endpoint https://<your-tunnel>/api/messages
|
|
215
68
|
```
|
|
216
69
|
|
|
217
|
-
The
|
|
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
|
-
|
|
72
|
+
### Run it
|
|
220
73
|
|
|
221
|
-
|
|
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
|
-
```
|
|
224
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
89
|
+
teams.on_message(/^status$/i) do |ctx|
|
|
90
|
+
ctx.typing
|
|
300
91
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
396
|
-
```
|
|
105
|
+
## Feature status
|
|
397
106
|
|
|
398
|
-
|
|
107
|
+
This table tracks supported Teams SDK capabilities. Deprecated upstream AI and devtools packages are intentionally excluded.
|
|
399
108
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
140
|
+
## Development
|
|
471
141
|
|
|
472
|
-
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
144
|
+
```sh
|
|
145
|
+
bundle install
|
|
146
|
+
bundle exec rake test
|
|
480
147
|
```
|
|
481
148
|
|
|
482
|
-
|
|
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
|
-
|
|
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
|
-
|
|
153
|
+
Use [GitHub Issues](https://github.com/bavmind/teams_rb/issues) for bug reports and feature requests.
|
|
490
154
|
|
|
491
|
-
##
|
|
155
|
+
## License
|
|
492
156
|
|
|
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). |
|
|
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 `
|
|
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
|
-
|
|
|
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 `
|
|
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
|
|