fix: route Teams sign-in invokes to OAuth dialog + use UserTokenClient for sign-out
All checks were successful
Build and Deploy Teams Planner Bot / build-and-run (push) Successful in 30s

This commit is contained in:
윤정민 2026-05-15 17:33:39 +09:00
parent fd504738eb
commit e42b3d7c43
5 changed files with 45 additions and 10 deletions

BIN
appPackage/color.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -1,8 +1,8 @@
{
"$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.16/MicrosoftTeams.schema.json",
"manifestVersion": "1.16",
"version": "0.1.0",
"id": "REPLACE_WITH_YOUR_BOT_APP_ID",
"version": "1.0.0",
"id": "1d0c2212-0bae-46af-babd-6c5223d2ee4c",
"packageName": "com.example.teamsplannerbot",
"developer": {
"name": "Internal",
@ -25,7 +25,7 @@
"accentColor": "#4F6BED",
"bots": [
{
"botId": "REPLACE_WITH_YOUR_BOT_APP_ID",
"botId": "1d0c2212-0bae-46af-babd-6c5223d2ee4c",
"scopes": ["personal", "team", "groupchat"],
"supportsFiles": false,
"isNotificationOnly": false,
@ -42,7 +42,7 @@
"permissions": ["identity", "messageTeamMembers"],
"validDomains": ["token.botframework.com"],
"webApplicationInfo": {
"id": "REPLACE_WITH_YOUR_BOT_APP_ID",
"id": "1d0c2212-0bae-46af-babd-6c5223d2ee4c",
"resource": "https://graph.microsoft.com/"
}
}

BIN
appPackage/outline.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -1,17 +1,34 @@
import {
ActivityHandler,
CardFactory,
CloudAdapter,
ConversationState,
StatePropertyAccessor,
TeamsActivityHandler,
TurnContext,
UserState,
} from "botbuilder";
import { UserTokenClient } from "botframework-connector";
import { OAuthPrompt, DialogSet, DialogTurnStatus, WaterfallDialog } from "botbuilder-dialogs";
import { createGraphClient } from "../graph/graphClientFactory";
import { PlannerClient } from "../graph/plannerClient";
import { LlmClassifier } from "../llm/types";
import { buildResultCard } from "../cards/confirmationCard";
async function signOutUser(context: TurnContext, connectionName: string): Promise<void> {
const adapter = context.adapter as CloudAdapter;
const userTokenClient = context.turnState.get<UserTokenClient>(
(adapter as unknown as { UserTokenClientKey: symbol }).UserTokenClientKey,
);
if (!userTokenClient) {
throw new Error("UserTokenClient not available on this adapter");
}
await userTokenClient.signOutUser(
context.activity.from.id,
connectionName,
context.activity.channelId,
);
}
const OAUTH_PROMPT = "graphOAuthPrompt";
const MAIN_DIALOG = "mainDialog";
@ -22,12 +39,13 @@ export interface PlannerBotDeps {
connectionName: string; // Azure Bot OAuth connection name
}
export class PlannerBot extends ActivityHandler {
export class PlannerBot extends TeamsActivityHandler {
private readonly conversationState: ConversationState;
private readonly userState: UserState;
private readonly classifier: LlmClassifier;
private readonly dialogs: DialogSet;
private readonly dialogStateAccessor: StatePropertyAccessor;
private readonly connectionName: string;
constructor(deps: PlannerBotDeps) {
super();
@ -35,6 +53,7 @@ export class PlannerBot extends ActivityHandler {
this.userState = deps.userState;
this.classifier = deps.classifier;
this.connectionName = deps.connectionName;
this.dialogStateAccessor = this.conversationState.createProperty("DialogState");
this.dialogs = new DialogSet(this.dialogStateAccessor);
@ -67,10 +86,7 @@ export class PlannerBot extends ActivityHandler {
const text = (context.activity.text ?? "").trim();
if (text.toLowerCase() === "logout") {
const adapter = context.adapter as unknown as {
signOutUser: (ctx: TurnContext, connName: string) => Promise<void>;
};
await adapter.signOutUser(context, deps.connectionName);
await signOutUser(context, this.connectionName);
await context.sendActivity("로그아웃 완료.");
await next();
return;
@ -89,6 +105,15 @@ export class PlannerBot extends ActivityHandler {
await next();
});
// Teams sends an invoke activity ("signin/verifyState" or "signin/tokenExchange")
// after the user completes the OAuth flow. We must route it back into the
// active dialog so OAuthPrompt can finish.
this.onTokenResponseEvent(async (context, next) => {
const dialogContext = await this.dialogs.createContext(context);
await dialogContext.continueDialog();
await next();
});
this.onMembersAdded(async (context, next) => {
const greeting =
"안녕하세요! 작업 진행 상황을 평소 말로 알려주시면 Planner에 자동으로 정리해 드릴게요.\n" +
@ -108,6 +133,16 @@ export class PlannerBot extends ActivityHandler {
await this.userState.saveChanges(context, false);
}
protected async handleTeamsSigninVerifyState(context: TurnContext): Promise<void> {
const dialogContext = await this.dialogs.createContext(context);
await dialogContext.continueDialog();
}
protected async handleTeamsSigninTokenExchange(context: TurnContext): Promise<void> {
const dialogContext = await this.dialogs.createContext(context);
await dialogContext.continueDialog();
}
private async handleUtterance(
context: TurnContext,
token: string,

BIN
teams-planner-bot.zip Normal file

Binary file not shown.