teams_rb 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +47 -0
  3. data/LICENSE +21 -0
  4. data/README.md +500 -0
  5. data/docs/README.md +40 -0
  6. data/docs/essentials/README.md +13 -0
  7. data/docs/essentials/api-client.md +62 -0
  8. data/docs/essentials/app-authentication.md +27 -0
  9. data/docs/essentials/app-basics.md +50 -0
  10. data/docs/essentials/graph.md +41 -0
  11. data/docs/essentials/on-activity.md +72 -0
  12. data/docs/essentials/on-event.md +29 -0
  13. data/docs/essentials/proactive-messaging.md +44 -0
  14. data/docs/essentials/sending-messages.md +76 -0
  15. data/docs/essentials/sovereign-cloud.md +26 -0
  16. data/docs/getting-started/README.md +7 -0
  17. data/docs/getting-started/code-basics.md +61 -0
  18. data/docs/getting-started/quickstart.md +54 -0
  19. data/docs/getting-started/running-in-teams.md +44 -0
  20. data/docs/in-depth-guides/README.md +14 -0
  21. data/docs/in-depth-guides/adaptive-cards.md +62 -0
  22. data/docs/in-depth-guides/dialogs.md +77 -0
  23. data/docs/in-depth-guides/feedback.md +29 -0
  24. data/docs/in-depth-guides/meeting-events.md +27 -0
  25. data/docs/in-depth-guides/message-extensions.md +77 -0
  26. data/docs/in-depth-guides/message-reactions.md +29 -0
  27. data/docs/in-depth-guides/observability.md +28 -0
  28. data/docs/in-depth-guides/streaming.md +52 -0
  29. data/docs/in-depth-guides/tabs.md +59 -0
  30. data/docs/in-depth-guides/user-authentication.md +60 -0
  31. data/lib/teams/activity.rb +160 -0
  32. data/lib/teams/activity_context.rb +278 -0
  33. data/lib/teams/api/account.rb +90 -0
  34. data/lib/teams/api/activity_value.rb +49 -0
  35. data/lib/teams/api/bot_sign_in_client.rb +54 -0
  36. data/lib/teams/api/channel_data.rb +86 -0
  37. data/lib/teams/api/channel_info.rb +19 -0
  38. data/lib/teams/api/citation_appearance.rb +83 -0
  39. data/lib/teams/api/client.rb +28 -0
  40. data/lib/teams/api/conversation_account.rb +40 -0
  41. data/lib/teams/api/conversation_client.rb +156 -0
  42. data/lib/teams/api/conversation_reference.rb +83 -0
  43. data/lib/teams/api/conversation_resource.rb +19 -0
  44. data/lib/teams/api/meeting_client.rb +57 -0
  45. data/lib/teams/api/meeting_info.rb +26 -0
  46. data/lib/teams/api/meeting_notification_response.rb +29 -0
  47. data/lib/teams/api/meeting_participant.rb +33 -0
  48. data/lib/teams/api/message_activity.rb +201 -0
  49. data/lib/teams/api/message_extension.rb +110 -0
  50. data/lib/teams/api/model.rb +49 -0
  51. data/lib/teams/api/notification_info.rb +28 -0
  52. data/lib/teams/api/paged_members_result.rb +16 -0
  53. data/lib/teams/api/quoted_reply_entity.rb +65 -0
  54. data/lib/teams/api/reaction_client.rb +34 -0
  55. data/lib/teams/api/sent_activity.rb +48 -0
  56. data/lib/teams/api/task_module.rb +83 -0
  57. data/lib/teams/api/team_client.rb +45 -0
  58. data/lib/teams/api/team_details.rb +36 -0
  59. data/lib/teams/api/team_info.rb +44 -0
  60. data/lib/teams/api/tenant_info.rb +11 -0
  61. data/lib/teams/api/token.rb +81 -0
  62. data/lib/teams/api/typing_activity.rb +19 -0
  63. data/lib/teams/api/user_client.rb +83 -0
  64. data/lib/teams/app.rb +602 -0
  65. data/lib/teams/auth/client_secret_credentials.rb +7 -0
  66. data/lib/teams/auth/jwt_validator.rb +148 -0
  67. data/lib/teams/auth/token.rb +39 -0
  68. data/lib/teams/auth/token_manager.rb +68 -0
  69. data/lib/teams/cards/generated.rb +1764 -0
  70. data/lib/teams/cards/generated_base.rb +105 -0
  71. data/lib/teams/cards.rb +81 -0
  72. data/lib/teams/cloud_environment.rb +41 -0
  73. data/lib/teams/common/hashes.rb +20 -0
  74. data/lib/teams/common/http_client.rb +111 -0
  75. data/lib/teams/common/retry.rb +31 -0
  76. data/lib/teams/errors.rb +50 -0
  77. data/lib/teams/function_context.rb +82 -0
  78. data/lib/teams/graph/client.rb +73 -0
  79. data/lib/teams/http_stream.rb +460 -0
  80. data/lib/teams/rack_app.rb +62 -0
  81. data/lib/teams/response.rb +9 -0
  82. data/lib/teams/router.rb +171 -0
  83. data/lib/teams/storage/memory_store.rb +27 -0
  84. data/lib/teams/version.rb +5 -0
  85. data/lib/teams.rb +70 -0
  86. metadata +217 -0
@@ -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