Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion core/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
launchSettings.json
appsettings.Development.json
*.runsettings
*.runsettings

# Web build output and dependencies
**/Web/bin/
**/Web/node_modules/
1 change: 1 addition & 0 deletions core/core.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<Project Path="samples/MeetingsBot/MeetingsBot.csproj" />
<Project Path="samples/MessageExtensionBot/MessageExtensionBot.csproj" />
<Project Path="samples/PABot/PABot.csproj" Id="ef8f29ef-fe59-4edf-8a50-6e7ab6699a45" />
<Project Path="samples/TabApp/TabApp.csproj" />
<Project Path="samples/TeamsBot/TeamsBot.csproj" Id="94a35050-6826-446f-9b29-863f2bbc75b7" />
<Project Path="samples/TeamsChannelBot/TeamsChannelBot.csproj" />
</Folder>
Expand Down
13 changes: 13 additions & 0 deletions core/samples/TabApp/Body.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace TabApp;

public class PostToChatBody
{
public required string Message { get; set; }
public string? ChatId { get; set; }
public string? ChannelId { get; set; }
}

public record PostToChatResult(bool Ok);
92 changes: 92 additions & 0 deletions core/samples/TabApp/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Identity.Web;
using Microsoft.Teams.Bot.Apps.Schema;
using Microsoft.Teams.Bot.Core;
using Microsoft.Teams.Bot.Core.Hosting;
using TabApp;

WebApplicationBuilder builder = WebApplication.CreateSlimBuilder(args);
builder.Services.AddBotAuthorization();
builder.Services.AddConversationClient();
WebApplication app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

// ==================== TABS ====================

var contentTypes = new FileExtensionContentTypeProvider();
app.MapGet("/tabs/test/{*path}", (string? path) =>
{
var root = Path.Combine(Directory.GetCurrentDirectory(), "Web", "bin");
var full = Path.Combine(root, path ?? "index.html");
contentTypes.TryGetContentType(full, out var ct);
return Results.File(File.OpenRead(full), ct ?? "text/html");
});

// ==================== SERVER FUNCTIONS ====================

app.MapPost("/functions/post-to-chat", async (
PostToChatBody body,
HttpContext httpCtx,
ConversationClient conversations,
IConfiguration config,
IMemoryCache cache,
ILogger<Program> logger,
CancellationToken ct) =>
{
logger.LogInformation("post-to-chat called");

var serviceUrl = new Uri("https://smba.trafficmanager.net/teams");
string conversationId;

if (body.ChatId is not null)
{
// group chat or 1:1 chat tab — chat ID is the conversation ID
conversationId = body.ChatId;
}
else if (body.ChannelId is not null)
{
// channel tab — post to the channel directly
conversationId = body.ChannelId;
}
else
{
// personal tab — create or reuse a 1:1 conversation
string userId = httpCtx.User.GetObjectId() ?? throw new InvalidOperationException("User object ID claim not found.");

if (!cache.TryGetValue($"conv:{userId}", out string? cached))
{
string botId = config["AzureAd:ClientId"] ?? throw new InvalidOperationException("Bot client ID not configured.");
string tenantId = httpCtx.User.GetTenantId() ?? throw new InvalidOperationException("Tenant ID claim not found.");

CreateConversationResponse res = await conversations.CreateConversationAsync(new ConversationParameters
{
IsGroup = false,
TenantId = tenantId,
Members = [new TeamsConversationAccount { Id = userId }]
}, serviceUrl, cancellationToken: ct);

cached = res.Id ?? throw new InvalidOperationException("CreateConversation returned no ID.");
cache.Set($"conv:{userId}", cached);
}

conversationId = cached!;
}

TeamsActivity activity = TeamsActivity.CreateBuilder()
.WithType(TeamsActivityType.Message)
.WithText("Hello from the tab!")
.WithServiceUrl(serviceUrl)
.WithConversation(new TeamsConversation { Id = conversationId! })
.Build();
await conversations.SendActivityAsync(activity, cancellationToken: ct);

return Results.Json(new PostToChatResult(Ok: true));
}).RequireAuthorization();

app.Run();
109 changes: 109 additions & 0 deletions core/samples/TabApp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# TabApp

A sample demonstrating a React/Vite tab served by the bot, with server functions and client-side Graph calls.

| Feature | How it works |
|---|---|
| **Static tab** | Bot serves `Web/bin` via `app.WithTab("test", "./Web/bin")` at `/tabs/test` |
| **Teams Context** | Reads the raw Teams context via the Teams JS SDK |
| **Post to Chat** | Tab calls `POST /functions/post-to-chat` → bot sends a proactive message |
| **Who Am I** | Acquires a Graph token via MSAL and calls `GET /me` |
| **Toggle Presence** | Acquires a Graph token with `Presence.ReadWrite` and calls `POST /me/presence/setUserPreferredPresence` |

---

## Azure App Registration

### 1. Application ID URI

Under **Expose an API → Application ID URI**, set it to:

```
api://{YOUR_CLIENT_ID}
```

Then add a scope named `access_as_user` and pre-authorize the Teams client IDs:

| Client ID | App |
|---|---|
| `1fec8e78-bce4-4aaf-ab1b-5451cc387264` | Teams desktop / mobile |
| `5e3ce6c0-2b1f-4285-8d4b-75ee78787346` | Teams web |

### 2. Redirect URI

Under **Authentication → Add a platform → Single-page application**, add:

```
https://{YOUR_DOMAIN}/tabs/test
```
and
```
brk-multihub://{your_domain}
```

### 3. API permissions

Under **API permissions → Add a permission → Microsoft Graph → Delegated**:

| Permission | Required for |
|---|---|
| `User.Read` | Who Am I |
| `Presence.ReadWrite` | Toggle Presence |

---

## Manifest

**`webApplicationInfo`** — required for SSO (`authentication.getAuthToken()` and MSAL silent auth):

```json
"webApplicationInfo": {
"id": "{YOUR_CLIENT_ID}",
"resource": "api://{YOUR_CLIENT_ID}"
}
```

**`staticTabs`**:

```json
"staticTabs": [
{
"entityId": "tab",
"name": "Tab",
"contentUrl": "https://{YOUR_DOMAIN}/tabs/test",
"websiteUrl": "https://{YOUR_DOMAIN}/tabs/test",
"scopes": ["personal"]
}
]
```

---

## Configuration

**`launchSettings.json`** (or environment variables):

```json
"AzureAD__TenantId": "{YOUR_TENANT_ID}",
"AzureAD__ClientId": "{YOUR_CLIENT_ID}",
"AzureAD__ClientCredentials__0__SourceType": "ClientSecret",
"AzureAd__ClientCredentials__0__ClientSecret": "{YOUR_CLIENT_SECRET}"
```

**`Web/.env`**:

```
VITE_CLIENT_ID={YOUR_CLIENT_ID}
```

---

## Build & Run

```bash
# Build the React app
cd Web && npm install && npm run build

# Run the bot
dotnet run
```
18 changes: 18 additions & 0 deletions core/samples/TabApp/TabApp.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Teams.Bot.Apps\Microsoft.Teams.Bot.Apps.csproj" />
</ItemGroup>

<!-- Copy the built React/Vite output so it is available at runtime -->
<ItemGroup>
<Content Include="Web\bin\**" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

</Project>
12 changes: 12 additions & 0 deletions core/samples/TabApp/Web/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Teams Tab</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Loading