teams_rb 2.0.1 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +48 -1
- data/README.md +94 -434
- data/docs/essentials/app-authentication.md +14 -4
- data/docs/essentials/app-basics.md +2 -2
- data/docs/essentials/on-activity.md +27 -0
- data/docs/essentials/sending-messages.md +1 -0
- data/docs/getting-started/quickstart.md +6 -5
- data/docs/getting-started/running-in-teams.md +37 -14
- data/docs/in-depth-guides/README.md +1 -0
- data/docs/in-depth-guides/adaptive-cards.md +17 -0
- data/docs/in-depth-guides/agent-365.md +46 -0
- data/docs/in-depth-guides/user-authentication.md +1 -1
- data/lib/teams/activity.rb +5 -0
- data/lib/teams/activity_context.rb +17 -31
- data/lib/teams/api/account.rb +38 -0
- data/lib/teams/api/activity_value.rb +92 -0
- data/lib/teams/api/agentic_identity.rb +27 -0
- data/lib/teams/api/app_info.rb +15 -0
- data/lib/teams/api/channel_data.rb +5 -0
- data/lib/teams/api/client.rb +26 -2
- data/lib/teams/api/conversation_client.rb +3 -1
- data/lib/teams/api/message_activity.rb +1 -0
- data/lib/teams/api/search.rb +41 -0
- data/lib/teams/app.rb +184 -21
- data/lib/teams/auth/jwt_validator.rb +173 -68
- data/lib/teams/auth/token_manager.rb +132 -0
- data/lib/teams/cloud_environment.rb +5 -1
- data/lib/teams/http_stream.rb +8 -3
- data/lib/teams/router.rb +71 -0
- data/lib/teams/version.rb +1 -1
- data/lib/teams.rb +3 -0
- metadata +20 -2
|
@@ -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
|
|
|
@@ -32,6 +32,33 @@ Each invoke family has named routes — their handler return values become the i
|
|
|
32
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
33
|
- `on_suggested_action_submit` for suggested-action submissions
|
|
34
34
|
|
|
35
|
+
## Conversation updates
|
|
36
|
+
|
|
37
|
+
`on_conversation_update` matches any `conversationUpdate` activity (members added/removed, channel and team changes). The channel/team lifecycle sub-events also have named routes, matched on `channelData.eventType`:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
teams.on_conversation_update { |ctx| } # any conversationUpdate activity
|
|
41
|
+
teams.on_channel_created { |ctx| ctx.post "Welcome to #{ctx.activity.channel_data.channel.name}!" }
|
|
42
|
+
teams.on_channel_deleted { |ctx| } # also: on_channel_renamed, on_channel_restored
|
|
43
|
+
teams.on_team_renamed { |ctx| } # also: on_team_archived, on_team_unarchived,
|
|
44
|
+
# on_team_deleted, on_team_hard_deleted, on_team_restored
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
A generic `on_conversation_update` registered before a specific route sees the activity first; declare `|ctx, nxt|` and call `nxt.call` to continue to the specific route (see Middleware below).
|
|
48
|
+
|
|
49
|
+
## Agent 365 lifecycle events
|
|
50
|
+
|
|
51
|
+
`on_agent_lifecycle` matches any `agentLifecycle` event (sent when this app runs as an Agent 365 agentic user); the variants also have named routes, matched on the activity's `valueType`:
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
teams.on_agent_lifecycle { |ctx| } # any agentLifecycle event
|
|
55
|
+
teams.on_agentic_user_identity_created { |ctx| } # also: identity_updated, manager_updated,
|
|
56
|
+
teams.on_agentic_user_enabled { |ctx| } # disabled, deleted, undeleted,
|
|
57
|
+
# workload_onboarding_updated
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The event value exposes `tenant_id`, `agentic_user_id`, `agentic_app_instance_id`, `agent_identity_blueprint_id`, `version`, and per-variant fields (`manager`, `deletion_reason`, `workload_name`, …).
|
|
61
|
+
|
|
35
62
|
## Meeting events
|
|
36
63
|
|
|
37
64
|
```ruby
|
|
@@ -32,6 +32,7 @@ ctx.update sent.id, "Done!"
|
|
|
32
32
|
```ruby
|
|
33
33
|
ctx.post Teams::Api::MessageActivity.new("**markdown**", text_format: "markdown")
|
|
34
34
|
# text_format: plain | markdown | xml | extendedmarkdown
|
|
35
|
+
# ("extendedmarkdown" is in public preview and may be subject to change)
|
|
35
36
|
|
|
36
37
|
ctx.post Teams::Api::MessageActivity.new("Quarterly numbers")
|
|
37
38
|
.add_ai_generated # "AI generated" badge
|
|
@@ -5,16 +5,17 @@ Build and run your first Teams bot in Ruby.
|
|
|
5
5
|
## Prerequisites
|
|
6
6
|
|
|
7
7
|
- Ruby 4.0+
|
|
8
|
-
- A Microsoft 365 tenant
|
|
8
|
+
- A Microsoft 365 tenant with custom app upload enabled ([developer program](https://developer.microsoft.com/microsoft-365/dev-program) tenants work)
|
|
9
9
|
- A tunnel for local development ([Dev Tunnels](https://learn.microsoft.com/azure/developer/dev-tunnels/get-started), ngrok, or similar)
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
13
|
-
Add the
|
|
13
|
+
Add the SDK and a Rack server for this standalone example:
|
|
14
14
|
|
|
15
15
|
```ruby
|
|
16
16
|
# Gemfile
|
|
17
17
|
gem "teams_rb"
|
|
18
|
+
gem "puma"
|
|
18
19
|
```
|
|
19
20
|
|
|
20
21
|
```sh
|
|
@@ -38,15 +39,15 @@ end
|
|
|
38
39
|
run teams.to_rack
|
|
39
40
|
```
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
This example uses Puma; `teams_rb` itself does not require a server gem. Run it with:
|
|
42
43
|
|
|
43
44
|
```sh
|
|
44
|
-
CLIENT_ID=... CLIENT_SECRET=... TENANT_ID=... bundle exec
|
|
45
|
+
CLIENT_ID=... CLIENT_SECRET=... TENANT_ID=... bundle exec puma -p 3978
|
|
45
46
|
```
|
|
46
47
|
|
|
47
48
|
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
|
|
|
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(
|
|
50
|
+
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(dangerously_allow_unauthenticated_requests: true)` disables inbound validation (never use it beyond local testing; the app logs a loud warning when you do). The `DANGEROUSLY_ALLOW_UNAUTHENTICATED_REQUESTS` environment variable sets the same option.
|
|
50
51
|
|
|
51
52
|
## Where to go next
|
|
52
53
|
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
# Running in Teams
|
|
2
2
|
|
|
3
|
-
Connect your locally running bot to a real Teams client.
|
|
3
|
+
Connect your locally running bot to a real Teams client with the [Teams CLI](https://www.npmjs.com/package/@microsoft/teams.cli).
|
|
4
4
|
|
|
5
|
-
## 1.
|
|
5
|
+
## 1. Install and sign in
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
The CLI requires Node.js 20 or newer:
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
```sh
|
|
10
|
+
npm install -g @microsoft/teams.cli
|
|
11
|
+
teams login
|
|
12
|
+
teams status
|
|
13
|
+
```
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
`teams status` should report that sideloading is enabled. If it is disabled, your tenant administrator must enable custom app upload before you can install the bot.
|
|
14
16
|
|
|
15
17
|
## 2. Start a tunnel
|
|
16
18
|
|
|
@@ -22,23 +24,44 @@ devtunnel port create teams-bot -p 3978
|
|
|
22
24
|
devtunnel host teams-bot
|
|
23
25
|
```
|
|
24
26
|
|
|
25
|
-
Note the tunnel URL,
|
|
27
|
+
Note the tunnel URL, such as `https://abc123-3978.euw.devtunnels.ms`. Dev Tunnels URLs are stable across restarts, so creation is a one-time step.
|
|
28
|
+
|
|
29
|
+
## 3. Register the app and bot
|
|
26
30
|
|
|
27
|
-
|
|
31
|
+
From your project directory, pass the public endpoint to the CLI:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
teams app create \
|
|
35
|
+
--name my-teams-ruby-bot \
|
|
36
|
+
--endpoint https://<your-tunnel>/api/messages
|
|
37
|
+
```
|
|
28
38
|
|
|
29
|
-
|
|
39
|
+
The CLI creates the app registration, manifest, bot, and Teams app. It prints the Teams App ID, an **Install in Teams** link, and the three values your Ruby app needs:
|
|
40
|
+
|
|
41
|
+
- `CLIENT_ID` — the bot's Microsoft App ID
|
|
42
|
+
- `CLIENT_SECRET` — the bot's client secret
|
|
43
|
+
- `TENANT_ID` — the Entra tenant ID
|
|
44
|
+
|
|
45
|
+
> If you plan to use [user authentication](../in-depth-guides/user-authentication.md), create an Azure-hosted bot with the CLI's `--azure`, `--subscription`, and `--resource-group` options. OAuth connection settings are not available for Teams-managed bots.
|
|
46
|
+
|
|
47
|
+
You can instead configure the same resources manually in the [Teams Developer Portal](https://dev.teams.microsoft.com/apps).
|
|
30
48
|
|
|
31
49
|
## 4. Run and install
|
|
32
50
|
|
|
51
|
+
Start the bot with the credentials printed by the CLI:
|
|
52
|
+
|
|
33
53
|
```sh
|
|
34
|
-
bundle exec
|
|
54
|
+
CLIENT_ID=... CLIENT_SECRET=... TENANT_ID=... bundle exec puma -p 3978
|
|
35
55
|
```
|
|
36
56
|
|
|
37
|
-
|
|
57
|
+
Open the **Install in Teams** link and send the bot a message. If you need the link again, run `teams app get <teams-app-id> --install-link`.
|
|
58
|
+
|
|
59
|
+
Inbound requests are JWT-validated with your real credentials; no `dangerously_allow_unauthenticated_requests` is needed behind the tunnel.
|
|
38
60
|
|
|
39
61
|
## Troubleshooting
|
|
40
62
|
|
|
41
|
-
- **No response in Teams**:
|
|
42
|
-
- **401s in your logs**: the requests are reaching you but failing validation
|
|
63
|
+
- **No response in Teams**: run `teams app doctor <teams-app-id>`, then check that the tunnel and server are running.
|
|
64
|
+
- **401s in your logs**: the requests are reaching you but failing validation, usually because the running app has credentials from a different registration.
|
|
65
|
+
- **Sideloading disabled**: ask your tenant administrator to enable custom app upload.
|
|
43
66
|
- **Teams caches aggressively**: after manifest changes, fully quit and reopen the Teams client.
|
|
44
67
|
- **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`.
|
|
@@ -10,5 +10,6 @@ Feature guides for building richer Teams apps:
|
|
|
10
10
|
- [Streaming](streaming.md) — chunked responses, informative updates, events, typed errors
|
|
11
11
|
- [User authentication](user-authentication.md) — OAuth sign-in, token exchange, Azure setup
|
|
12
12
|
- [Tabs and remote functions](tabs.md) — calling the bot backend from a tab with SSO
|
|
13
|
+
- [Agent 365](agent-365.md) — running as an agentic user (experimental)
|
|
13
14
|
- [Meeting events](meeting-events.md) — meeting start/end
|
|
14
15
|
- [Observability](observability.md) — logging and middleware
|
|
@@ -48,6 +48,23 @@ end
|
|
|
48
48
|
|
|
49
49
|
For richer action flows — opening a modal form from a card button — see [Dialogs](dialogs.md).
|
|
50
50
|
|
|
51
|
+
## Dynamic typeahead search
|
|
52
|
+
|
|
53
|
+
An `Input.ChoiceSet` with `choices.data` (`Data.Query`) fetches its choices from your bot as the user types. Teams sends an `application/search` invoke; answer it with `on_card_search`:
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
teams.on_card_search do |ctx|
|
|
57
|
+
query = ctx.activity.value.query_text
|
|
58
|
+
Teams::Api::SearchResponse.new(
|
|
59
|
+
CITIES.grep(/#{Regexp.escape(query)}/i).map do |city|
|
|
60
|
+
Teams::Api::SearchInvokeResult.new(title: city, value: city)
|
|
61
|
+
end
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`ctx.activity.value` also carries `dataset` (the `Data.Query` dataset id, for cards with several dynamic inputs) and `query_options` with `skip`/`top` for paging large result sets.
|
|
67
|
+
|
|
51
68
|
## Regenerating
|
|
52
69
|
|
|
53
70
|
The card classes are generated; don't hand-edit `lib/teams/cards/generated.rb`. To regenerate after an upstream card-model change:
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Agent 365 (experimental)
|
|
2
|
+
|
|
3
|
+
> Agent 365 support is experimental across the whole Teams SDK family. Expect
|
|
4
|
+
> changes.
|
|
5
|
+
|
|
6
|
+
Agent 365 gives an app a *stacked identity*: the app registration acts as an
|
|
7
|
+
**agentic app blueprint** (its client id *is* the blueprint id), a tenant can
|
|
8
|
+
instantiate **agentic apps** from it, and each app can carry **agentic users**
|
|
9
|
+
— user-shaped Entra identities that appear in Teams like people.
|
|
10
|
+
|
|
11
|
+
## Inbound
|
|
12
|
+
|
|
13
|
+
Nothing to configure. When Teams delivers an activity to an agentic user, the
|
|
14
|
+
identity arrives on the activity's `recipient` account and the request carries
|
|
15
|
+
an Entra-issued token (accepted automatically alongside classic Bot Framework
|
|
16
|
+
tokens). The SDK scopes the whole turn to that identity — `ctx.post`, replies,
|
|
17
|
+
and streaming authenticate as the agent, not the app:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
teams.on_message do |ctx|
|
|
21
|
+
identity = ctx.activity.recipient.agentic_identity
|
|
22
|
+
ctx.log.info "acting as agentic user #{identity.agentic_user_id}" if identity
|
|
23
|
+
ctx.reply "Hello from your agent"
|
|
24
|
+
end
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Lifecycle events
|
|
28
|
+
|
|
29
|
+
Teams reports agentic-user lifecycle changes as `agentLifecycle` events — see
|
|
30
|
+
[activity routing](../essentials/on-activity.md) for `on_agent_lifecycle` and
|
|
31
|
+
the per-variant routes.
|
|
32
|
+
|
|
33
|
+
## Proactive
|
|
34
|
+
|
|
35
|
+
Build the identity explicitly (the blueprint defaults to the app's client id,
|
|
36
|
+
the tenant to the configured credentials tenant) and pass it to the proactive
|
|
37
|
+
senders:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
identity = teams.agentic_identity(agentic_app_id: "...", agentic_user_id: "...")
|
|
41
|
+
teams.post(conversation_id, "Proactive hello from the agent", agentic_identity: identity)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Omitting `agentic_user_id` acts as the agentic app itself. There is no
|
|
45
|
+
fallback between identity kinds: if an agentic token cannot be acquired the
|
|
46
|
+
call fails rather than silently sending as the wrong identity.
|
|
@@ -31,7 +31,7 @@ teams.on_sign_in do |ctx, token_response|
|
|
|
31
31
|
end
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
`ctx.sign_in` returns the token when the user is already signed in; otherwise it sends an OAuth card
|
|
34
|
+
`ctx.sign_in` returns the token when the user is already signed in; otherwise it sends an OAuth card and returns `nil`. In group chats and channels the card is delivered in the conversation as a targeted message only the requesting user sees; in channels the card always uses the sign-in button (silent SSO is not supported in channel scope). The SDK's **default handlers** then complete the sign-in invokes automatically — both the interactive card path (`signin/verifyState`) and silent SSO (`signin/tokenExchange`) — and fire `on_sign_in`. You don't handle the invokes yourself.
|
|
35
35
|
|
|
36
36
|
`ctx.sign_out` clears the stored token.
|
|
37
37
|
|
data/lib/teams/activity.rb
CHANGED
|
@@ -7,12 +7,13 @@ module Teams
|
|
|
7
7
|
class ActivityContext
|
|
8
8
|
attr_reader :app, :activity, :conversation_reference, :extra, :stream
|
|
9
9
|
|
|
10
|
-
def initialize(app:, activity:, conversation_reference:, extra: {}, stream: nil)
|
|
10
|
+
def initialize(app:, activity:, conversation_reference:, extra: {}, stream: nil, api: nil)
|
|
11
11
|
@app = app
|
|
12
12
|
@activity = activity
|
|
13
13
|
@conversation_reference = conversation_reference
|
|
14
14
|
@extra = extra
|
|
15
15
|
@stream = stream
|
|
16
|
+
@api = api
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
def ref
|
|
@@ -31,15 +32,17 @@ module Teams
|
|
|
31
32
|
app.storage
|
|
32
33
|
end
|
|
33
34
|
|
|
35
|
+
# The turn's API client; scoped to the activity's agentic identity when
|
|
36
|
+
# the app runs as an Agent 365 agent.
|
|
34
37
|
def api
|
|
35
|
-
app.api
|
|
38
|
+
@api || app.api
|
|
36
39
|
end
|
|
37
40
|
|
|
38
41
|
# The TypeScript, Python, and .NET SDKs call this operation `send`.
|
|
39
42
|
# Ruby already defines Object#send for dynamic dispatch, so the public
|
|
40
43
|
# Ruby API uses `post` to avoid shadowing a core language method.
|
|
41
44
|
def post(activity_or_text)
|
|
42
|
-
app.send_activity(conversation_reference, apply_targeted_defaults(activity_or_text))
|
|
45
|
+
app.send_activity(conversation_reference, apply_targeted_defaults(activity_or_text), api:)
|
|
43
46
|
end
|
|
44
47
|
|
|
45
48
|
def reply(activity_or_text)
|
|
@@ -93,8 +96,10 @@ module Teams
|
|
|
93
96
|
|
|
94
97
|
# Starts the user sign-in flow: returns the token if the user is already
|
|
95
98
|
# signed in, otherwise sends an OAuth card and returns nil. In group
|
|
96
|
-
#
|
|
97
|
-
#
|
|
99
|
+
# chats and channels the card is a targeted message visible only to the
|
|
100
|
+
# requesting user; channels omit the token exchange resource because
|
|
101
|
+
# channel scope cannot do the silent SSO exchange, so the card renders
|
|
102
|
+
# the sign-in button instead.
|
|
98
103
|
def sign_in(connection_name: nil, oauth_card_text: "Please Sign In...", sign_in_button_text: "Sign In")
|
|
99
104
|
connection_name ||= app.default_connection_name
|
|
100
105
|
|
|
@@ -109,19 +114,6 @@ module Teams
|
|
|
109
114
|
# No token yet; continue with the OAuth card flow.
|
|
110
115
|
end
|
|
111
116
|
|
|
112
|
-
conversation_id = conversation_reference.conversation_id
|
|
113
|
-
if activity.conversation.is_group
|
|
114
|
-
one_on_one = api.conversations.create(
|
|
115
|
-
members: [activity.from.to_h],
|
|
116
|
-
tenant_id: activity.conversation.tenant_id
|
|
117
|
-
)
|
|
118
|
-
conversation_id = one_on_one.id
|
|
119
|
-
# Deliberately posts the plain text notice into the group (matching
|
|
120
|
-
# TypeScript and Python): group chats don't support SSO, so the card
|
|
121
|
-
# itself goes to the 1:1 below while the group sees only the notice.
|
|
122
|
-
post(oauth_card_text)
|
|
123
|
-
end
|
|
124
|
-
|
|
125
117
|
state = Base64.strict_encode64(JSON.generate(
|
|
126
118
|
"connectionName" => connection_name,
|
|
127
119
|
"conversation" => conversation_reference.to_h,
|
|
@@ -129,10 +121,14 @@ module Teams
|
|
|
129
121
|
))
|
|
130
122
|
resource = api.bots.sign_in.get_resource(state:)
|
|
131
123
|
|
|
124
|
+
is_channel = activity.conversation.conversation_type == "channel"
|
|
125
|
+
recipient = activity.from.to_h
|
|
126
|
+
recipient = recipient.merge("isTargeted" => true) if activity.conversation.is_group
|
|
127
|
+
|
|
132
128
|
card = {
|
|
133
129
|
"text" => oauth_card_text,
|
|
134
130
|
"connectionName" => connection_name,
|
|
135
|
-
"tokenExchangeResource" => resource.token_exchange_resource&.to_h,
|
|
131
|
+
"tokenExchangeResource" => (resource.token_exchange_resource&.to_h unless is_channel),
|
|
136
132
|
"tokenPostResource" => resource.token_post_resource&.to_h,
|
|
137
133
|
"buttons" => [
|
|
138
134
|
{ "type" => "signin", "title" => sign_in_button_text, "value" => resource.sign_in_link }
|
|
@@ -141,13 +137,13 @@ module Teams
|
|
|
141
137
|
|
|
142
138
|
payload = {
|
|
143
139
|
"type" => "message",
|
|
144
|
-
"recipient" =>
|
|
140
|
+
"recipient" => recipient,
|
|
145
141
|
"attachments" => [
|
|
146
142
|
{ "contentType" => "application/vnd.microsoft.card.oauth", "content" => card }
|
|
147
143
|
]
|
|
148
144
|
}
|
|
149
145
|
|
|
150
|
-
app.send_activity(
|
|
146
|
+
app.send_activity(conversation_reference, payload, api:)
|
|
151
147
|
nil
|
|
152
148
|
end
|
|
153
149
|
|
|
@@ -167,16 +163,6 @@ module Teams
|
|
|
167
163
|
|
|
168
164
|
private
|
|
169
165
|
|
|
170
|
-
def sign_in_reference(conversation_id)
|
|
171
|
-
return conversation_reference if conversation_id == conversation_reference.conversation_id
|
|
172
|
-
|
|
173
|
-
Api::ConversationReference.from_h(
|
|
174
|
-
conversation_reference.to_h.merge(
|
|
175
|
-
"conversation" => conversation_reference.conversation.to_h.merge("id" => conversation_id)
|
|
176
|
-
)
|
|
177
|
-
)
|
|
178
|
-
end
|
|
179
|
-
|
|
180
166
|
def incoming_targeted_sender
|
|
181
167
|
return nil unless activity.message?
|
|
182
168
|
return nil unless activity.recipient.is_targeted == true
|
data/lib/teams/api/account.rb
CHANGED
|
@@ -55,6 +55,36 @@ module Teams
|
|
|
55
55
|
read("tenantId", "tenant_id")
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
+
def agentic_user_id
|
|
59
|
+
read("agenticUserId", "agentic_user_id")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def agentic_app_id
|
|
63
|
+
read("agenticAppId", "agentic_app_id")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def agentic_app_blueprint_id
|
|
67
|
+
read("agenticAppBlueprintId", "agentic_app_blueprint_id")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def callback_uri
|
|
71
|
+
read("callbackUri", "callback_uri")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The Agent 365 identity scope this account carries (agentic accounts
|
|
75
|
+
# arrive with role "agenticUser" on the activity's recipient); nil
|
|
76
|
+
# unless the account belongs to an agentic app blueprint.
|
|
77
|
+
def agentic_identity
|
|
78
|
+
return nil unless agentic_app_blueprint_id
|
|
79
|
+
|
|
80
|
+
AgenticIdentity.new(
|
|
81
|
+
agentic_app_blueprint_id:,
|
|
82
|
+
agentic_app_id:,
|
|
83
|
+
agentic_user_id:,
|
|
84
|
+
tenant_id:
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
|
|
58
88
|
def to_h
|
|
59
89
|
body = raw.dup
|
|
60
90
|
body["id"] = id if id
|
|
@@ -70,6 +100,10 @@ module Teams
|
|
|
70
100
|
body["email"] = email if email
|
|
71
101
|
body["userPrincipalName"] = user_principal_name if user_principal_name
|
|
72
102
|
body["tenantId"] = tenant_id if tenant_id
|
|
103
|
+
body["agenticUserId"] = agentic_user_id if agentic_user_id
|
|
104
|
+
body["agenticAppId"] = agentic_app_id if agentic_app_id
|
|
105
|
+
body["agenticAppBlueprintId"] = agentic_app_blueprint_id if agentic_app_blueprint_id
|
|
106
|
+
body["callbackUri"] = callback_uri if callback_uri
|
|
73
107
|
remove_aliases(body)
|
|
74
108
|
end
|
|
75
109
|
|
|
@@ -83,6 +117,10 @@ module Teams
|
|
|
83
117
|
body.delete("given_name")
|
|
84
118
|
body.delete("user_principal_name")
|
|
85
119
|
body.delete("tenant_id")
|
|
120
|
+
body.delete("agentic_user_id")
|
|
121
|
+
body.delete("agentic_app_id")
|
|
122
|
+
body.delete("agentic_app_blueprint_id")
|
|
123
|
+
body.delete("callback_uri")
|
|
86
124
|
body
|
|
87
125
|
end
|
|
88
126
|
end
|
|
@@ -44,6 +44,98 @@ module Teams
|
|
|
44
44
|
def end_time
|
|
45
45
|
read("EndTime", "endTime", "end_time")
|
|
46
46
|
end
|
|
47
|
+
|
|
48
|
+
# application/search invoke fields (Adaptive Card dynamic typeahead
|
|
49
|
+
# Input.ChoiceSet queries via choices.data / Data.Query).
|
|
50
|
+
def query_text
|
|
51
|
+
read("queryText", "query_text")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Pagination options; skip and top read from the nested wrapper.
|
|
55
|
+
def query_options
|
|
56
|
+
value = read("queryOptions", "query_options")
|
|
57
|
+
value.is_a?(Hash) ? ActivityValue.new(value) : value
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def kind
|
|
61
|
+
read("kind")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The Data.Query dataset id authored on the Adaptive Card.
|
|
65
|
+
def dataset
|
|
66
|
+
read("dataset")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def skip
|
|
70
|
+
read("skip")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def top
|
|
74
|
+
read("top")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Agent 365 agentLifecycle event value fields. The service sends the
|
|
78
|
+
# agenticAppInstanceId / agentIdentityBlueprintId wire keys (not the
|
|
79
|
+
# account-style names); all SDKs preserve them.
|
|
80
|
+
def tenant_id
|
|
81
|
+
read("tenantId", "tenant_id")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def agentic_user_id
|
|
85
|
+
read("agenticUserId", "agentic_user_id")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def agentic_app_instance_id
|
|
89
|
+
read("agenticAppInstanceId", "agentic_app_instance_id")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def agent_identity_blueprint_id
|
|
93
|
+
read("agentIdentityBlueprintId", "agent_identity_blueprint_id")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def version
|
|
97
|
+
read("version")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Variant extras: manager (identity-created carries userId/email/
|
|
101
|
+
# displayName; manager-updated carries managerId), deletion reason,
|
|
102
|
+
# workload onboarding fields, and the created identity's expiry.
|
|
103
|
+
def manager
|
|
104
|
+
value = read("manager")
|
|
105
|
+
value.is_a?(Hash) ? ActivityValue.new(value) : value
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def user_id
|
|
109
|
+
read("userId", "user_id")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def email
|
|
113
|
+
read("email")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def display_name
|
|
117
|
+
read("displayName", "display_name")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def manager_id
|
|
121
|
+
read("managerId", "manager_id")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def deletion_reason
|
|
125
|
+
read("deletionReason", "deletion_reason")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def workload_name
|
|
129
|
+
read("workloadName", "workload_name")
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def workload_onboarding_state
|
|
133
|
+
read("workloadOnboardingState", "workload_onboarding_state")
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def expiration_date_time
|
|
137
|
+
read("expirationDateTime", "expiration_date_time")
|
|
138
|
+
end
|
|
47
139
|
end
|
|
48
140
|
end
|
|
49
141
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teams
|
|
4
|
+
module Api
|
|
5
|
+
# The Agent 365 identity scope for SDK operations: an agentic app
|
|
6
|
+
# blueprint (the app registration itself), optionally narrowed to an
|
|
7
|
+
# agentic app instance and an agentic user attached to that instance.
|
|
8
|
+
class AgenticIdentity
|
|
9
|
+
attr_reader :agentic_app_blueprint_id, :agentic_app_id, :agentic_user_id, :tenant_id
|
|
10
|
+
|
|
11
|
+
def initialize(agentic_app_blueprint_id:, agentic_app_id: nil, agentic_user_id: nil, tenant_id: nil)
|
|
12
|
+
@agentic_app_blueprint_id = agentic_app_blueprint_id
|
|
13
|
+
@agentic_app_id = agentic_app_id
|
|
14
|
+
@agentic_user_id = agentic_user_id
|
|
15
|
+
@tenant_id = tenant_id
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_h
|
|
19
|
+
body = { "agenticAppBlueprintId" => agentic_app_blueprint_id }
|
|
20
|
+
body["agenticAppId"] = agentic_app_id if agentic_app_id
|
|
21
|
+
body["agenticUserId"] = agentic_user_id if agentic_user_id
|
|
22
|
+
body["tenantId"] = tenant_id if tenant_id
|
|
23
|
+
body
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
module Teams
|
|
4
4
|
module Api
|
|
5
5
|
class ChannelData < Model
|
|
6
|
+
def app
|
|
7
|
+
wrap(read("app"), AppInfo)
|
|
8
|
+
end
|
|
9
|
+
|
|
6
10
|
def tenant
|
|
7
11
|
wrap(read("tenant"), TenantInfo)
|
|
8
12
|
end
|
|
@@ -55,6 +59,7 @@ module Teams
|
|
|
55
59
|
|
|
56
60
|
def to_h
|
|
57
61
|
body = raw.dup
|
|
62
|
+
body["app"] = app.to_h if app
|
|
58
63
|
body["tenant"] = tenant.to_h if tenant
|
|
59
64
|
body["team"] = team.to_h if team
|
|
60
65
|
body["channel"] = channel.to_h if channel
|