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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +47 -0
- data/LICENSE +21 -0
- data/README.md +500 -0
- data/docs/README.md +40 -0
- data/docs/essentials/README.md +13 -0
- data/docs/essentials/api-client.md +62 -0
- data/docs/essentials/app-authentication.md +27 -0
- data/docs/essentials/app-basics.md +50 -0
- data/docs/essentials/graph.md +41 -0
- data/docs/essentials/on-activity.md +72 -0
- data/docs/essentials/on-event.md +29 -0
- data/docs/essentials/proactive-messaging.md +44 -0
- data/docs/essentials/sending-messages.md +76 -0
- data/docs/essentials/sovereign-cloud.md +26 -0
- data/docs/getting-started/README.md +7 -0
- data/docs/getting-started/code-basics.md +61 -0
- data/docs/getting-started/quickstart.md +54 -0
- data/docs/getting-started/running-in-teams.md +44 -0
- data/docs/in-depth-guides/README.md +14 -0
- data/docs/in-depth-guides/adaptive-cards.md +62 -0
- data/docs/in-depth-guides/dialogs.md +77 -0
- data/docs/in-depth-guides/feedback.md +29 -0
- data/docs/in-depth-guides/meeting-events.md +27 -0
- data/docs/in-depth-guides/message-extensions.md +77 -0
- data/docs/in-depth-guides/message-reactions.md +29 -0
- data/docs/in-depth-guides/observability.md +28 -0
- data/docs/in-depth-guides/streaming.md +52 -0
- data/docs/in-depth-guides/tabs.md +59 -0
- data/docs/in-depth-guides/user-authentication.md +60 -0
- data/lib/teams/activity.rb +160 -0
- data/lib/teams/activity_context.rb +278 -0
- data/lib/teams/api/account.rb +90 -0
- data/lib/teams/api/activity_value.rb +49 -0
- data/lib/teams/api/bot_sign_in_client.rb +54 -0
- data/lib/teams/api/channel_data.rb +86 -0
- data/lib/teams/api/channel_info.rb +19 -0
- data/lib/teams/api/citation_appearance.rb +83 -0
- data/lib/teams/api/client.rb +28 -0
- data/lib/teams/api/conversation_account.rb +40 -0
- data/lib/teams/api/conversation_client.rb +156 -0
- data/lib/teams/api/conversation_reference.rb +83 -0
- data/lib/teams/api/conversation_resource.rb +19 -0
- data/lib/teams/api/meeting_client.rb +57 -0
- data/lib/teams/api/meeting_info.rb +26 -0
- data/lib/teams/api/meeting_notification_response.rb +29 -0
- data/lib/teams/api/meeting_participant.rb +33 -0
- data/lib/teams/api/message_activity.rb +201 -0
- data/lib/teams/api/message_extension.rb +110 -0
- data/lib/teams/api/model.rb +49 -0
- data/lib/teams/api/notification_info.rb +28 -0
- data/lib/teams/api/paged_members_result.rb +16 -0
- data/lib/teams/api/quoted_reply_entity.rb +65 -0
- data/lib/teams/api/reaction_client.rb +34 -0
- data/lib/teams/api/sent_activity.rb +48 -0
- data/lib/teams/api/task_module.rb +83 -0
- data/lib/teams/api/team_client.rb +45 -0
- data/lib/teams/api/team_details.rb +36 -0
- data/lib/teams/api/team_info.rb +44 -0
- data/lib/teams/api/tenant_info.rb +11 -0
- data/lib/teams/api/token.rb +81 -0
- data/lib/teams/api/typing_activity.rb +19 -0
- data/lib/teams/api/user_client.rb +83 -0
- data/lib/teams/app.rb +602 -0
- data/lib/teams/auth/client_secret_credentials.rb +7 -0
- data/lib/teams/auth/jwt_validator.rb +148 -0
- data/lib/teams/auth/token.rb +39 -0
- data/lib/teams/auth/token_manager.rb +68 -0
- data/lib/teams/cards/generated.rb +1764 -0
- data/lib/teams/cards/generated_base.rb +105 -0
- data/lib/teams/cards.rb +81 -0
- data/lib/teams/cloud_environment.rb +41 -0
- data/lib/teams/common/hashes.rb +20 -0
- data/lib/teams/common/http_client.rb +111 -0
- data/lib/teams/common/retry.rb +31 -0
- data/lib/teams/errors.rb +50 -0
- data/lib/teams/function_context.rb +82 -0
- data/lib/teams/graph/client.rb +73 -0
- data/lib/teams/http_stream.rb +460 -0
- data/lib/teams/rack_app.rb +62 -0
- data/lib/teams/response.rb +9 -0
- data/lib/teams/router.rb +171 -0
- data/lib/teams/storage/memory_store.rb +27 -0
- data/lib/teams/version.rb +5 -0
- data/lib/teams.rb +70 -0
- metadata +217 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# The API client
|
|
2
|
+
|
|
3
|
+
`teams.api` (also `ctx.api`) is the low-level Bot Framework client, structured like the other SDKs' API hub. Higher-level helpers (`ctx.reply`, `teams.post`, `ctx.sign_in`) route through it; use it directly when you need the raw surface.
|
|
4
|
+
|
|
5
|
+
## Conversations
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
api.conversations.create(members:, tenant_id:, activity: nil, channel_data: nil) # => ConversationResource
|
|
9
|
+
api.conversations.create_activity(conversation_id, activity)
|
|
10
|
+
api.conversations.reply_to_activity(conversation_id, activity_id, activity)
|
|
11
|
+
api.conversations.update_activity(conversation_id, activity_id, activity)
|
|
12
|
+
api.conversations.delete_activity(conversation_id, activity_id)
|
|
13
|
+
|
|
14
|
+
# targeted (single-recipient) variants
|
|
15
|
+
api.conversations.create_targeted_activity(conversation_id, activity)
|
|
16
|
+
api.conversations.update_targeted_activity(conversation_id, activity_id, activity)
|
|
17
|
+
api.conversations.delete_targeted_activity(conversation_id, activity_id)
|
|
18
|
+
|
|
19
|
+
# members — typed Teams::Api::Account results, aadObjectId normalized
|
|
20
|
+
api.conversations.get_members(conversation_id)
|
|
21
|
+
api.conversations.get_member_by_id(conversation_id, member_id)
|
|
22
|
+
api.conversations.get_paged_members(conversation_id, page_size: 200, continuation_token: nil) # => PagedMembersResult
|
|
23
|
+
api.conversations.get_activity_members(conversation_id, activity_id)
|
|
24
|
+
|
|
25
|
+
# reactions
|
|
26
|
+
api.conversations.add_reaction(conversation_id, activity_id, "like")
|
|
27
|
+
api.conversations.delete_reaction(conversation_id, activity_id, "like")
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
For large rosters use `get_paged_members` and feed `continuation_token` back until it returns `nil`.
|
|
31
|
+
|
|
32
|
+
## Teams and meetings
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
api.teams.get_by_id(team_id) # => TeamDetails
|
|
36
|
+
api.teams.get_conversations(team_id) # => [ChannelInfo] — the team's channels
|
|
37
|
+
|
|
38
|
+
api.meetings.get_by_id(meeting_id) # => MeetingInfo
|
|
39
|
+
api.meetings.get_participant(meeting_id, aad_object_id, tenant_id) # => MeetingParticipant
|
|
40
|
+
api.meetings.send_notification(meeting_id, { value: { recipients: [aad_object_id], surfaces: surfaces } })
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`send_notification` returns `nil` when every recipient succeeded (202) and a `MeetingNotificationResponse` with per-recipient failures on partial success (207).
|
|
44
|
+
|
|
45
|
+
## Users and bot sign-in
|
|
46
|
+
|
|
47
|
+
The token-service clients behind [user authentication](../in-depth-guides/user-authentication.md):
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
api.users.get_token(user_id:, connection_name:, channel_id: nil, code: nil) # => TokenResponse
|
|
51
|
+
api.users.get_token_status(user_id:, channel_id:) # => [TokenStatus]
|
|
52
|
+
api.users.sign_out(user_id:, connection_name:, channel_id:)
|
|
53
|
+
api.users.exchange_token(user_id:, connection_name:, channel_id:, exchange_request:)
|
|
54
|
+
api.users.get_aad_tokens(user_id:, connection_name:, resource_urls:, channel_id:)
|
|
55
|
+
|
|
56
|
+
api.bots.sign_in.get_url(state:) # => String
|
|
57
|
+
api.bots.sign_in.get_resource(state:) # => SignInUrlResponse
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Errors
|
|
61
|
+
|
|
62
|
+
Failed requests raise `Teams::HttpError` with `status`, `headers`, `body` (parsed JSON when possible, raw string otherwise), and the request. Streaming sends additionally classify their 403s into typed stream errors ([Streaming](../in-depth-guides/streaming.md)).
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# App authentication
|
|
2
|
+
|
|
3
|
+
How the app proves itself to Teams, and how Teams proves itself to the app. All of it matches the TypeScript, Python, and .NET SDKs.
|
|
4
|
+
|
|
5
|
+
## Inbound validation
|
|
6
|
+
|
|
7
|
+
Every `POST /api/messages` request must carry a Bot Framework JWT. The app validates:
|
|
8
|
+
|
|
9
|
+
- **Signature** against Microsoft's published signing keys (RS256, JWKS fetched and cached)
|
|
10
|
+
- **Issuer** — the Bot Framework issuer, or your tenant's Entra issuers (v1 and v2 forms) when `TENANT_ID` is configured
|
|
11
|
+
- **Audience** — your app id, in any of the three accepted forms: the bare id, `api://{id}`, `api://botid-{id}`
|
|
12
|
+
- **Expiry / not-before**
|
|
13
|
+
- **`serviceurl` claim** — must match the activity's service URL, preventing spoofed routing
|
|
14
|
+
|
|
15
|
+
Rejected requests get a 401 with a warn log line. Requests are rejected wholesale when no credentials are configured (with a loud startup warning) unless `skip_auth: true` is set explicitly — which is for local development only.
|
|
16
|
+
|
|
17
|
+
## Outbound bot tokens
|
|
18
|
+
|
|
19
|
+
Replies and proactive sends authenticate with an app-only token from the Entra client-credentials flow, acquired and cached automatically (single-flight refresh with expiry skew — the other SDKs get the equivalent from MSAL). Nothing to configure beyond the three credentials.
|
|
20
|
+
|
|
21
|
+
## The trust model in one paragraph
|
|
22
|
+
|
|
23
|
+
Teams never talks to your bot directly — the Bot Framework service does, signing every request with Microsoft-issued tokens audienced to *your* app id. Your bot talks back through the service URL from the validated activity, authenticating with *your* app's token. User identity inside activities (`ctx.activity.from`) is asserted by Teams and safe to trust once the JWT validates; user identity for *external* calls (Graph, your own API) comes from [user authentication](../in-depth-guides/user-authentication.md) instead.
|
|
24
|
+
|
|
25
|
+
## Sovereign clouds
|
|
26
|
+
|
|
27
|
+
Pass a `cloud:` environment to route logins, token services, and Graph to sovereign endpoints. The public cloud is the default.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# App basics
|
|
2
|
+
|
|
3
|
+
## Construction
|
|
4
|
+
|
|
5
|
+
`Teams::App.new` reads configuration from the environment by default; every value can be passed explicitly:
|
|
6
|
+
|
|
7
|
+
| Env var | Keyword | Required | Purpose |
|
|
8
|
+
|---|---|---|---|
|
|
9
|
+
| `CLIENT_ID` | `client_id:` | production | Bot's Microsoft App ID — bot tokens and inbound JWT audiences |
|
|
10
|
+
| `CLIENT_SECRET` | `client_secret:` | production | Client secret for the client-credentials flow |
|
|
11
|
+
| `TENANT_ID` | `tenant_id:` | single-tenant | Entra tenant for bot tokens and tenant-issuer validation |
|
|
12
|
+
| `SERVICE_URL` | `service_url:` | no | Default Bot Framework URL for proactive sends (inbound requests always use the activity's own service URL) |
|
|
13
|
+
| — | `skip_auth:` | no | Disables inbound validation — local development only |
|
|
14
|
+
| — | `messaging_endpoint:` | no | Inbound path, default `/api/messages` |
|
|
15
|
+
| — | `default_connection_name:` | no | OAuth connection name for user sign-in, default `"graph"` |
|
|
16
|
+
| — | `logger:`, `storage:`, `cloud:` | no | Logger (stdout default), state store (in-memory default), cloud environment for sovereign clouds |
|
|
17
|
+
|
|
18
|
+
Without credentials the app logs a startup warning and rejects every inbound request unless `skip_auth: true` was set explicitly — the same behavior as the TypeScript, Python, and .NET SDKs.
|
|
19
|
+
|
|
20
|
+
## One app instance
|
|
21
|
+
|
|
22
|
+
Construct the app once at boot and share it; it is safe under multi-threaded servers (Puma etc.) — the token cache, storage, and JWKS caches are lock-protected, and per-request state lives on the context.
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
# config/initializers/teams_bot.rb (Rails) or config.ru (Rack)
|
|
26
|
+
TEAMS_BOT = Teams::App.new
|
|
27
|
+
|
|
28
|
+
TEAMS_BOT.on_message do |ctx|
|
|
29
|
+
ctx.reply "hello"
|
|
30
|
+
end
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Serving
|
|
34
|
+
|
|
35
|
+
`to_rack` produces the Rack app:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
# Rack
|
|
39
|
+
run TEAMS_BOT.to_rack
|
|
40
|
+
|
|
41
|
+
# Rails — route the endpoint path; this preserves the request path,
|
|
42
|
+
# which the endpoint check relies on
|
|
43
|
+
post "/api/messages" => TEAMS_BOT.to_rack
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Handlers run inside the web request. Keep them fast; move slow work (LLM calls, long queries) to a job and deliver results later via [proactive messaging](proactive-messaging.md).
|
|
47
|
+
|
|
48
|
+
## Storage
|
|
49
|
+
|
|
50
|
+
`ctx.storage` (and `app.storage`) is a simple key-value store with `get`/`set`/`delete`. The default `Teams::Storage::MemoryStore` is thread-safe but process-local — substitute anything answering the same three methods (backed by Redis, Active Record, …) via `storage:`.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Microsoft Graph
|
|
2
|
+
|
|
3
|
+
A thin Graph request client, following the TypeScript SDK's core Graph client. The raw request surface is the API (like TypeScript's `client.http` escape hatch); there are no generated endpoint wrappers, and Ruby has no official Graph SDK to delegate to.
|
|
4
|
+
|
|
5
|
+
## Two identities
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
teams.graph # the app's own identity (app-only tokens) — also ctx.app_graph
|
|
9
|
+
ctx.user_graph # the signed-in user's identity
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
- **App identity** uses the client-credentials flow with the Graph scope. Grant the app **application permissions** in Entra for whatever it reads/writes.
|
|
13
|
+
- **User identity** uses the token from [user sign-in](../in-depth-guides/user-authentication.md) and raises `Teams::Error` when the user hasn't signed in. The token carries the OAuth connection's **delegated** scopes.
|
|
14
|
+
|
|
15
|
+
## Requests
|
|
16
|
+
|
|
17
|
+
`get` / `post` / `patch` / `put` / `delete`, paths relative to `/v1.0`, parsed-hash returns:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
me = ctx.user_graph.get("/me")
|
|
21
|
+
ctx.reply "You are #{me["displayName"]} (#{me["userPrincipalName"]})"
|
|
22
|
+
|
|
23
|
+
teams.graph.get("/users", params: { "$top" => 5, "$select" => "displayName" })
|
|
24
|
+
teams.graph.post("/users/#{user_id}/sendMail", json: { message: { subject: "Hi" } })
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Errors
|
|
28
|
+
|
|
29
|
+
Failures raise `Teams::GraphError` with `status`, the Graph error `code` (e.g. `Authorization_RequestDenied`), and the full response `body`:
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
begin
|
|
33
|
+
teams.graph.get("/users")
|
|
34
|
+
rescue Teams::GraphError => e
|
|
35
|
+
ctx.log.warn("Graph denied: #{e.code}") if e.status == 403
|
|
36
|
+
end
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Sovereign clouds
|
|
40
|
+
|
|
41
|
+
The Graph host derives from the configured cloud's graph scope automatically; `Teams::Graph::Client.new(token:, base_url_root: "https://graph.microsoft.us")` overrides it for hand-built clients.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Listening to activities
|
|
2
|
+
|
|
3
|
+
Handlers register against named routes, matching the Python SDK's handler names.
|
|
4
|
+
|
|
5
|
+
## Messages
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
teams.on_message do |ctx| # every message
|
|
9
|
+
ctx.reply "you said: #{ctx.activity.text}"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
teams.on_message(/^help$/i) do |ctx| # pattern-filtered (Regexp or exact String)
|
|
13
|
+
ctx.reply "try: search, create, login"
|
|
14
|
+
end
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Message edits and restores route separately:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
teams.on_message_update { |ctx| } # any messageUpdate activity
|
|
21
|
+
teams.on_edit_message { |ctx| } # user edited a message
|
|
22
|
+
teams.on_undelete_message { |ctx| } # user restored a deleted message
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Invokes
|
|
26
|
+
|
|
27
|
+
Each invoke family has named routes — their handler return values become the invoke's HTTP response body:
|
|
28
|
+
|
|
29
|
+
- Dialogs: `on_dialog_open(dialog_id = nil)`, `on_dialog_submit(action = nil)` — [guide](../in-depth-guides/dialogs.md)
|
|
30
|
+
- Message extensions: `on_message_ext_query`, `on_message_ext_submit`, `on_message_ext_open`, `on_message_ext_query_link`, and five more — [guide](../in-depth-guides/message-extensions.md)
|
|
31
|
+
- Sign-in: `on_signin_token_exchange`, `on_signin_verify_state`, `on_signin_failure` (defaults provided — [guide](../in-depth-guides/user-authentication.md))
|
|
32
|
+
- Feedback: `on_message_submit_feedback` (thumbs up/down from `add_feedback` — [guide](../in-depth-guides/feedback.md)), `on_message_submit` for any `message/submitAction`
|
|
33
|
+
- `on_suggested_action_submit` for suggested-action submissions
|
|
34
|
+
|
|
35
|
+
## Meeting events
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
teams.on_meeting_start { |ctx| ctx.post "Meeting #{ctx.activity.value.title} started" }
|
|
39
|
+
teams.on_meeting_end { |ctx| }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
See [Meeting events](../in-depth-guides/meeting-events.md) for the payload details.
|
|
43
|
+
|
|
44
|
+
## Middleware
|
|
45
|
+
|
|
46
|
+
`use` registers a handler that sees every activity; calling the second parameter continues the chain. Any handler that declares two parameters participates in chaining:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
teams.use do |ctx, nxt|
|
|
50
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
51
|
+
nxt.call
|
|
52
|
+
ensure
|
|
53
|
+
ctx.log.info("#{ctx.activity.type} handled in #{(Process.clock_gettime(Process::CLOCK_MONOTONIC) - started).round(3)}s")
|
|
54
|
+
end
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Handlers run in registration order among those whose route matches the activity; without `nxt.call`, the chain stops.
|
|
58
|
+
|
|
59
|
+
## The escape hatch
|
|
60
|
+
|
|
61
|
+
`teams.on(type)` matches raw activity types (`"message"`, `"invoke"`, `"messageReaction"`, `"conversationUpdate"`, …) for anything without a named route. Prefer the named routes when one exists.
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
teams.on("messageReaction") do |ctx|
|
|
65
|
+
reaction = ctx.activity.raw["reactionsAdded"]&.first
|
|
66
|
+
ctx.post "thanks for the #{reaction["type"]}!" if reaction
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Error semantics
|
|
71
|
+
|
|
72
|
+
A handler that raises produces a 500 response, and Bot Framework then **redelivers the activity** (same `ctx.activity.id`) — this is SDK-family behavior. Deduplicate side effects by activity id when that matters.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Listening to events
|
|
2
|
+
|
|
3
|
+
Beyond activity routes, the app emits two application-level events, registered with named methods (the Ruby counterpart of the TypeScript/Python event emitters).
|
|
4
|
+
|
|
5
|
+
## Sign-in
|
|
6
|
+
|
|
7
|
+
Fires whenever a user sign-in completes through the default OAuth handlers — both the silent token-exchange path and the interactive card path:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
teams.on_sign_in do |ctx, token_response|
|
|
11
|
+
ctx.post "Welcome! You're signed in."
|
|
12
|
+
# token_response.token is the user's access token for the
|
|
13
|
+
# OAuth connection's scopes; ctx is the invoke's activity context
|
|
14
|
+
end
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
See [User authentication](../in-depth-guides/user-authentication.md) for the full flow.
|
|
18
|
+
|
|
19
|
+
## Errors
|
|
20
|
+
|
|
21
|
+
Fires when a default OAuth handler hits an unexpected failure, or when the Teams client reports a sign-in failure:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
teams.on_error do |error, activity|
|
|
25
|
+
Honeybadger.notify(error, context: { activity_id: activity&.id })
|
|
26
|
+
end
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Handler errors during normal activity processing are not routed here — they surface as 500 responses (and Bot Framework redelivery); log-based monitoring catches those via the app's error log line.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Proactive messaging
|
|
2
|
+
|
|
3
|
+
Sending outside an inbound request — from a job, a webhook, your product's backend.
|
|
4
|
+
|
|
5
|
+
## Conversation references
|
|
6
|
+
|
|
7
|
+
Inside a handler, `ctx.ref` is a `Teams::Api::ConversationReference` for the current conversation. Persist it, restore it later:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# during a handler
|
|
11
|
+
ConversationRecord.create!(user: current_user, ref: ctx.ref.to_h)
|
|
12
|
+
|
|
13
|
+
# later, from a job
|
|
14
|
+
ref = Teams::Api::ConversationReference.from_h(record.ref)
|
|
15
|
+
teams.post(ref.conversation_id, "Your report is ready", service_url: ref.service_url)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The proactive surface on the app mirrors the SDK family:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
teams.post(conversation_id, activity) # send
|
|
22
|
+
teams.reply(conversation_id, activity_id, activity) # threaded reply
|
|
23
|
+
teams.update(conversation_id, activity_id, activity) # edit
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`teams.reply` threads through the conversation id (`;messageid=...`), like TypeScript and Python.
|
|
27
|
+
|
|
28
|
+
## Messaging a user without a stored conversation
|
|
29
|
+
|
|
30
|
+
Create (or re-fetch) the 1:1 conversation — Teams returns the existing conversation when one exists for the same members:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
conversation = teams.api.conversations.create(
|
|
34
|
+
members: [{ id: user_teams_id }], # the user's Teams id ("29:...")
|
|
35
|
+
tenant_id: tenant_id
|
|
36
|
+
)
|
|
37
|
+
teams.post(conversation.id, "Hello from your backend")
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This is the standard SaaS pattern: notify any user who has the app installed, no prior chat required.
|
|
41
|
+
|
|
42
|
+
## Service URLs
|
|
43
|
+
|
|
44
|
+
Proactive sends default to the app's configured `SERVICE_URL`; pass `service_url:` from a stored reference to target the user's regional endpoint (references captured from real activities carry it).
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Sending messages
|
|
2
|
+
|
|
3
|
+
## The four verbs
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
ctx.post "a plain message in the conversation"
|
|
7
|
+
ctx.reply "a threaded reply, quoting the inbound message"
|
|
8
|
+
ctx.quote "message-id", "a reply quoting a specific message"
|
|
9
|
+
ctx.update "activity-id", "replaces a previous bot message"
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`post` is Ruby's spelling of the SDKs' `send` (Ruby reserves `Object#send`), with identical semantics — including: an activity that already carries an `id` is **updated** instead of created. `ctx.update` is sugar over exactly that. `reply` adds the Teams quoted-reply entity and placeholder, matching the other SDKs.
|
|
13
|
+
|
|
14
|
+
All sends accept a String, a `Teams::Api::MessageActivity`, an Adaptive Card, or a raw activity Hash (symbol or string keys — they're normalized).
|
|
15
|
+
|
|
16
|
+
## SentActivity
|
|
17
|
+
|
|
18
|
+
Every send returns a `Teams::Api::SentActivity` — the outbound activity merged with the server's response. Keep its `id` to edit later:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
sent = ctx.post "Working on it..."
|
|
22
|
+
# ...later...
|
|
23
|
+
ctx.update sent.id, "Done!"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
> **Updates replace the activity entirely.** Metadata like the AI-generated label, sensitivity labels, citations, and mentions must be re-attached on every update or they disappear.
|
|
27
|
+
|
|
28
|
+
## Message activities
|
|
29
|
+
|
|
30
|
+
`Teams::Api::MessageActivity` is the builder for anything beyond plain text:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
ctx.post Teams::Api::MessageActivity.new("**markdown**", text_format: "markdown")
|
|
34
|
+
# text_format: plain | markdown | xml | extendedmarkdown
|
|
35
|
+
|
|
36
|
+
ctx.post Teams::Api::MessageActivity.new("Quarterly numbers")
|
|
37
|
+
.add_ai_generated # "AI generated" badge
|
|
38
|
+
.add_sensitivity_label("Confidential", description: "Internal only")
|
|
39
|
+
.add_feedback # thumbs up/down feedback buttons
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Mentions:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
ctx.post Teams::Api::MessageActivity.new("ping ").add_mention(ctx.activity.from.to_h)
|
|
46
|
+
|
|
47
|
+
if ctx.activity.recipient_mentioned?
|
|
48
|
+
ctx.reply "you said: #{ctx.activity.strip_mentions_text}"
|
|
49
|
+
end
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Citations (for AI answers) attach numbered references — `[1]` in the text links to the citation:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
message = Teams::Api::MessageActivity.new("Revenue grew 40% [1].").add_ai_generated
|
|
56
|
+
message.add_citation(1, { "name" => "Q3 Report", "url" => "https://example.com/q3" })
|
|
57
|
+
ctx.post message
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
> A citation's inline `text` content must be a **stringified Adaptive Card** — Teams rejects the whole message with a 400 otherwise.
|
|
61
|
+
|
|
62
|
+
## Typing
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
ctx.typing # animated ellipsis in the chat
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`ctx.typing(text)` exists for wire parity, but the Teams client doesn't render text on a plain typing activity — for a visible status line use `ctx.stream.update("Thinking...")` ([Streaming](../in-depth-guides/streaming.md)).
|
|
69
|
+
|
|
70
|
+
## Targeted messages
|
|
71
|
+
|
|
72
|
+
When the inbound message targets the bot (`recipient.isTargeted`), `ctx.post` and `ctx.reply` automatically answer as targeted messages visible only to that user, matching TypeScript/Python — including the `targetedMessageInfo` entity and the targeted API routes. Targeted messages aren't supported in 1:1 chats (the SDK raises).
|
|
73
|
+
|
|
74
|
+
## Delivery semantics
|
|
75
|
+
|
|
76
|
+
Bot Framework delivers **at least once**: a slow or failing bot sees the same activity again with the same `ctx.activity.id`. The SDK doesn't deduplicate (none of the SDKs do) — if a side effect must not repeat, guard it by activity id.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Sovereign clouds
|
|
2
|
+
|
|
3
|
+
Government and sovereign cloud tenants use different login, Bot Framework, and Graph endpoints. The app routes everything from a single `cloud:` configuration:
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
gcch = Teams::CloudEnvironment.new(
|
|
7
|
+
login_endpoint: "https://login.microsoftonline.us",
|
|
8
|
+
login_tenant: "botframework.com",
|
|
9
|
+
bot_scope: "https://api.botframework.us/.default",
|
|
10
|
+
token_service_url: "https://tokengcch.botframework.azure.us",
|
|
11
|
+
open_id_metadata_url: "https://login.botframework.azure.us/v1/.well-known/openidconfiguration",
|
|
12
|
+
token_issuer: "https://api.botframework.us",
|
|
13
|
+
graph_scope: "https://graph.microsoft.us/.default"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
teams = Teams::App.new(cloud: gcch)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Everything derives from the cloud environment automatically:
|
|
20
|
+
|
|
21
|
+
- **Bot tokens** — requested from the cloud's login endpoint with its bot scope
|
|
22
|
+
- **Inbound validation** — issuer and signing keys from the cloud's metadata
|
|
23
|
+
- **User tokens / sign-in** — the cloud's token service
|
|
24
|
+
- **Graph** — the host is derived from the cloud's graph scope (`https://graph.microsoft.us` above)
|
|
25
|
+
|
|
26
|
+
The public cloud is the default; nothing to configure outside sovereign deployments. Endpoint values for the specific sovereign clouds are in [Microsoft's sovereign cloud documentation](https://learn.microsoft.com/azure/bot-service/how-to-deploy-gov-cloud-high).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
Everything to go from zero to a bot running inside Teams:
|
|
4
|
+
|
|
5
|
+
1. [Quickstart](quickstart.md) — install the gem and write an echo bot
|
|
6
|
+
2. [Code basics](code-basics.md) — the `App`, handlers, and the activity context
|
|
7
|
+
3. [Running in Teams](running-in-teams.md) — registration, tunnel, manifest, troubleshooting
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Code basics
|
|
2
|
+
|
|
3
|
+
The quickstart's `config.ru` is a complete Teams app. Here's what each piece does, and the concepts you'll use everywhere else.
|
|
4
|
+
|
|
5
|
+
## The App
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
teams = Teams::App.new
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`Teams::App` is the heart of the SDK — it owns the route table, the API client, token management, and inbound request validation. By default it reads `CLIENT_ID`, `CLIENT_SECRET`, and `TENANT_ID` from the environment; every option can also be passed explicitly ([App basics](../essentials/app-basics.md) lists them all).
|
|
12
|
+
|
|
13
|
+
## Handlers
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
teams.on_message do |ctx|
|
|
17
|
+
ctx.typing
|
|
18
|
+
ctx.reply "echo: #{ctx.activity.text}"
|
|
19
|
+
end
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Handlers register against activity routes: `on_message` for messages (optionally filtered by a pattern), `on_dialog_open` for dialog invokes, `on_message_ext_query` for message extension searches, and so on — [Listening to activities](../essentials/on-activity.md) has the full list. Multiple handlers can match one activity; a handler with a second block parameter controls chaining:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
teams.use do |ctx, nxt|
|
|
26
|
+
puts "inbound #{ctx.activity.type}"
|
|
27
|
+
nxt.call # continue to the next matching handler
|
|
28
|
+
end
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## The activity context
|
|
32
|
+
|
|
33
|
+
Every handler receives a `ctx` with everything about the current activity:
|
|
34
|
+
|
|
35
|
+
| Member | What it is |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `ctx.activity` | The inbound activity model — `type`, `text`, `from`, `conversation`, `value`, plus raw payload access via `ctx.activity.raw` |
|
|
38
|
+
| `ctx.ref` | The conversation reference — persist `ctx.ref.to_h` to message this conversation later |
|
|
39
|
+
| `ctx.post` / `ctx.reply` / `ctx.quote` / `ctx.update` / `ctx.typing` | Sending — see [Sending messages](../essentials/sending-messages.md) |
|
|
40
|
+
| `ctx.stream` | Chunked streaming responses — see [Streaming](../in-depth-guides/streaming.md) |
|
|
41
|
+
| `ctx.sign_in` / `ctx.sign_out` | User OAuth — see [User authentication](../in-depth-guides/user-authentication.md) |
|
|
42
|
+
| `ctx.api` | The low-level [API client](../essentials/api-client.md) |
|
|
43
|
+
| `ctx.app_graph` / `ctx.user_graph` | [Microsoft Graph](../essentials/graph.md) clients |
|
|
44
|
+
| `ctx.storage`, `ctx.log` | The app's state store and logger |
|
|
45
|
+
|
|
46
|
+
## The Rack endpoint
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
run teams.to_rack
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`to_rack` returns a plain Rack app serving the Teams messaging endpoint (default `/api/messages`) — mount it in `config.ru`, or route to it from Rails:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
# config/routes.rb
|
|
56
|
+
post "/api/messages" => TEAMS_BOT.to_rack
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## One naming note
|
|
60
|
+
|
|
61
|
+
The Microsoft SDKs call their send operation `send`; Ruby uses `post` because `Object#send` is core Ruby. Treat `post` as Ruby's spelling of SDK `send` — including its update-when-id-present behavior. `reply` keeps full SDK reply semantics (reply threading plus Teams quoted-reply markup).
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Quickstart
|
|
2
|
+
|
|
3
|
+
Build and run your first Teams bot in Ruby.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- Ruby 4.0+
|
|
8
|
+
- A Microsoft 365 tenant where you can register a Teams app ([developer program](https://developer.microsoft.com/microsoft-365/dev-program) tenants work)
|
|
9
|
+
- A tunnel for local development ([Dev Tunnels](https://learn.microsoft.com/azure/developer/dev-tunnels/get-started), ngrok, or similar)
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
Add the gem to your project:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
# Gemfile
|
|
17
|
+
gem "teams_rb"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
bundle install
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Your first bot
|
|
25
|
+
|
|
26
|
+
Create a `config.ru`:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
require "teams"
|
|
30
|
+
|
|
31
|
+
teams = Teams::App.new
|
|
32
|
+
|
|
33
|
+
teams.on_message do |ctx|
|
|
34
|
+
ctx.typing
|
|
35
|
+
ctx.reply "echo: #{ctx.activity.text}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
run teams.to_rack
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
And run it:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
CLIENT_ID=... CLIENT_SECRET=... TENANT_ID=... bundle exec rackup -p 3978 -o 0.0.0.0
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The app listens for Teams activities on `POST /api/messages`, validates every inbound request against Microsoft's signing keys, and echoes any message back as a quoted reply.
|
|
48
|
+
|
|
49
|
+
The three environment variables come from your bot registration — [Running in Teams](running-in-teams.md) walks through creating one and connecting Teams to your locally running bot. For local experiments without credentials, `Teams::App.new(skip_auth: true)` disables inbound validation (never use it beyond local testing; the app logs a loud warning when you do).
|
|
50
|
+
|
|
51
|
+
## Where to go next
|
|
52
|
+
|
|
53
|
+
- [Code basics](code-basics.md) — what each part of this file does
|
|
54
|
+
- [Running in Teams](running-in-teams.md) — see it live inside the Teams client
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Running in Teams
|
|
2
|
+
|
|
3
|
+
Connect your locally running bot to a real Teams client.
|
|
4
|
+
|
|
5
|
+
## 1. Register a bot
|
|
6
|
+
|
|
7
|
+
Create a Teams app with a bot in the [Teams Developer Portal](https://dev.teams.microsoft.com) (or with Microsoft's Teams CLI). You need three values for the app's environment:
|
|
8
|
+
|
|
9
|
+
- `CLIENT_ID` — the bot's Microsoft App ID
|
|
10
|
+
- `CLIENT_SECRET` — a client secret for that app registration
|
|
11
|
+
- `TENANT_ID` — your Entra tenant ID (single-tenant bots)
|
|
12
|
+
|
|
13
|
+
> If you plan to use [user authentication](../in-depth-guides/user-authentication.md), register the bot as an **Azure Bot resource** from the start (Azure portal → Create resource → Azure Bot → "Use existing app registration"). OAuth connection settings only exist there, and converting a Developer Portal registration later means deleting and recreating it.
|
|
14
|
+
|
|
15
|
+
## 2. Start a tunnel
|
|
16
|
+
|
|
17
|
+
Teams must reach your machine over HTTPS. With Dev Tunnels:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
devtunnel create teams-bot -a
|
|
21
|
+
devtunnel port create teams-bot -p 3978
|
|
22
|
+
devtunnel host teams-bot
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Note the tunnel URL, e.g. `https://abc123-3978.euw.devtunnels.ms`. Dev Tunnels URLs are stable across restarts, so this is one-time setup.
|
|
26
|
+
|
|
27
|
+
## 3. Point the bot at the tunnel
|
|
28
|
+
|
|
29
|
+
In the bot registration, set the **messaging endpoint** to `https://<your-tunnel>/api/messages`, and make sure the **Microsoft Teams channel** is enabled.
|
|
30
|
+
|
|
31
|
+
## 4. Run and install
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
bundle exec rackup -p 3978 -o 0.0.0.0
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Install the app in Teams (Developer Portal → Preview in Teams) and send it a message. Inbound requests are JWT-validated with your real credentials — no `skip_auth` needed behind a tunnel.
|
|
38
|
+
|
|
39
|
+
## Troubleshooting
|
|
40
|
+
|
|
41
|
+
- **No response in Teams**: check the messaging endpoint URL, the Teams channel, and that the tunnel and server are both running. The app logs every inbound activity at debug level and every rejected request at warn level.
|
|
42
|
+
- **401s in your logs**: the requests are reaching you but failing validation — usually a `CLIENT_ID`/`TENANT_ID` mismatch with the registration.
|
|
43
|
+
- **Teams caches aggressively**: after manifest changes, fully quit and reopen the Teams client.
|
|
44
|
+
- **At-least-once delivery**: Bot Framework redelivers activities when your bot errors or responds slowly. If a handler's side effects must not repeat, deduplicate by `ctx.activity.id`.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# In-depth guides
|
|
2
|
+
|
|
3
|
+
Feature guides for building richer Teams apps:
|
|
4
|
+
|
|
5
|
+
- [Adaptive Cards](adaptive-cards.md) — the generated card classes, actions, submissions
|
|
6
|
+
- [Dialogs](dialogs.md) — task modules: opening, submissions, multi-step forms
|
|
7
|
+
- [Message extensions](message-extensions.md) — search commands, action commands, link unfurling
|
|
8
|
+
- [Feedback](feedback.md) — thumbs up/down on bot messages
|
|
9
|
+
- [Message reactions](message-reactions.md) — sending and receiving reactions
|
|
10
|
+
- [Streaming](streaming.md) — chunked responses, informative updates, events, typed errors
|
|
11
|
+
- [User authentication](user-authentication.md) — OAuth sign-in, token exchange, Azure setup
|
|
12
|
+
- [Tabs and remote functions](tabs.md) — calling the bot backend from a tab with SSO
|
|
13
|
+
- [Meeting events](meeting-events.md) — meeting start/end
|
|
14
|
+
- [Observability](observability.md) — logging and middleware
|