OfficeDev / teams-toolkit

Developer tools for building Teams apps
Other
454 stars 183 forks source link

Receiving unhandled error DialogContextError: Cannot find command: {0} on after Auth Prompt #12253

Closed NWH-SAmin5 closed 3 days ago

NWH-SAmin5 commented 3 weeks ago

Describe the bug I have command bot using this version, I am receiving unhandled error DialogContextError: Cannot find command: {0} right after TeamsBotSsoPrompt or OAuthPrompt I tried both. It looks like OAuthPrompt from my custom dialog is not coming back to my custom dialog and its going to BotSsoExecutionDialog.

        "@microsoft/teamsfx": "^2.3.2",
        "botbuilder": "^4.22.3",
        "botbuilder-dialogs": "^4.22.3",

Full error

[onTurnError] unhandled error DialogContextError: Cannot find command: {0}
    at <removed>\node_modules\botbuilder-dialogs\src\dialogContext.ts:31:19
    at Generator.throw (<anonymous>)
    at rejected (<removed>\node_modules\botbuilder-dialogs\lib\dialogContext.js:6:65)
    at processTicksAndRejections (node:internal/process/task_queues:95:5) {
  error: ErrorWithCode.CannotFindCommand: Cannot find command: {0}
      at BotSsoExecutionDialog.<anonymous> (<removed>\node_modules\@microsoft\teamsfx\src\conversation\sso\botSsoExecutionDialog.ts:216:11)
      at Generator.next (<anonymous>)
      at <removed>\node_modules\@microsoft\teamsfx\dist\index.node.cjs.js:206:71
      at new Promise (<anonymous>)
      at __awaiter (<removed>\node_modules\@microsoft\teamsfx\dist\index.node.cjs.js:202:12)
      at BotSsoExecutionDialog.commandRouteStep (<removed>\node_modules\@microsoft\teamsfx\dist\index.node.cjs.js:3471:16)
      at WaterfallDialog.<anonymous> (<removed>\node_modules\botbuilder-dialogs\src\waterfallDialog.ts:247:44)
      at Generator.next (<anonymous>)
      at <removed>\node_modules\botbuilder-dialogs\lib\waterfallDialog.js:8:71
      at new Promise (<anonymous>) {
    code: 'CannotFindCommand'
  },
  dialogContext: {
    activeDialog: 'CommandRouteDialog',
    parent: 'BotSsoExecutionDialog',
    stack: [ [Object] ]
  }
}

Method 1

/LoginCmd.ts (TeamsBotSsoPrompt)

export class LoginCmd implements TeamsFxBotCommandHandler {
    triggerPatterns: TriggerPatterns = [
        /^login?:?\s?(.*?)$/i
    ];

    private _conversationState: ConversationState;
    private _dialogState: StatePropertyAccessor<DialogState>;
    private readonly DIALOG_STATE = "DialogState";
    private _dialogs: DialogSet;

    constructor() {
        this._conversationState = new ConversationState(new MemoryStorage());
        this._dialogState = this._conversationState.createProperty(this.DIALOG_STATE);
        this._dialogs = new DialogSet(this._dialogState);

        const TeamsBotSsoPromptId = "TEAMS_BOT_SSO_PROMPT";

        const settings: TeamsBotSsoPromptSettings = {
            scopes: ["User.Read"],
            timeout: 900000,
            endOnInvalidMessage: true,
        };

        const authConfig: OnBehalfOfCredentialAuthConfig = oboAuthConfig;
        const loginUrl = process.env.INITIATE_LOGIN_ENDPOINT;
        const ssoPrompt = new TeamsBotSsoPrompt(authConfig, loginUrl, TeamsBotSsoPromptId, settings);
        this._dialogs.add(ssoPrompt);
        this._dialogs.add(
            new WaterfallDialog("taskNeedingLogin", [
                async (step) => {
                  return await step.beginDialog(TeamsBotSsoPromptId);
                },
                async (step) => {
                  const token = step.result;
                  if (token) {
                    // ... continue with task needing access token ...
                    await step.context.sendActivity(`Here is your token ${token.token}`);
                    return await step.endDialog();
                  } else {
                    await step.context.sendActivity(`Sorry... We couldn't log you in. Try again later.`);
                    return await step.endDialog();
                  }
                },
              ])
        );
    }

    async handleCommandReceived(context: TurnContext, message: CommandMessage): Promise<string | Partial<Activity> | void> {
        console.log(`App received message: ${message.text}`);
        await context.sendActivities([{ type: ActivityTypes.Typing }]);

        try {
            const dc = await this._dialogs.createContext(context);
            await dc.beginDialog("taskNeedingLogin");
        } catch (error) {
            console.log(error);
        }
    }
}

Method 2

/MainDialog.ts

const MAIN_DIALOG = 'MainDialog';
const OAUTH_PROMPT = 'OAuthPrompt';
const CONFIRM_PROMPT = 'ConfirmPrompt';
const MAIN_WATERFALL_DIALOG = 'MainWaterfallDialog';

export class MainDialog extends MainInnerDialog {
    constructor() {
        super(MAIN_DIALOG, "local-oauthconnection");

        this.addDialog(new OAuthPrompt(OAUTH_PROMPT, {
            connectionName: "local-oauthconnection",
            text: 'Please Sign In to your another account',
            title: 'Sign In',
            timeout: 300000
        }));
        this.addDialog(new ConfirmPrompt(CONFIRM_PROMPT));
        this.addDialog(new WaterfallDialog(MAIN_WATERFALL_DIALOG, [
            this.loginPromptStep.bind(this),
            this.loginCompleteStep.bind(this),
            this.ensureOAuth.bind(this),
            this.displayToken.bind(this)
        ]));

        this.initialDialogId = MAIN_WATERFALL_DIALOG;
    }

    /**
     * The run method handles the incoming activity (in the form of a DialogContext) and passes it through the dialog system.
     * If no dialog is active, it will start the default dialog.
     * @param context 
     * @param accessor 
     */
    public async run(context: TurnContext, accessor: StatePropertyAccessor) {
        console.log("run");

        const dialogSet = new DialogSet(accessor);
        dialogSet.add(this);

        const dialogContext = await dialogSet.createContext(context);
        const results = await dialogContext.continueDialog();
        if (results.status === DialogTurnStatus.empty) {
            console.log(this.id);
            await dialogContext.beginDialog(this.id);
        }
    }

    private async loginPromptStep(stepContext: WaterfallStepContext) {
        console.log("loginPromptStep");
        const result = await stepContext.beginDialog(OAUTH_PROMPT);
        return result
    }

    private async loginCompleteStep(stepContext: WaterfallStepContext) {
        console.log("loginCompleteStep");

        // Get the token from the previous step. Note that we could also have gotten the
        // token directly from the prompt itself. There is an example of this in the next method.
        const tokenResponse = stepContext.result;
        if (tokenResponse) {
            await stepContext.context.sendActivity('You are now logged in.');
            return await stepContext.prompt(CONFIRM_PROMPT, 'Would you like to view your token?');
        } else {
            return await stepContext.context.sendActivity('Login was not successful please try again.');
        }
    }

    async ensureOAuth(stepContext: WaterfallStepContext) {
        await stepContext.context.sendActivity('Thank you.');

        const result = stepContext.result;
        if (result) {
            // Call the prompt again because we need the token. The reasons for this are:
            // 1. If the user is already logged in we do not need to store the token locally in the bot and worry
            // about refreshing it. We can always just call the prompt again to get the token.
            // 2. We never know how long it will take a user to respond. By the time the
            // user responds the token may have expired. The user would then be prompted to login again.
            //
            // There is no reason to store the token locally in the bot because we can always just call
            // the OAuth prompt to get the token or get a new token if needed.
            return await stepContext.beginDialog(OAUTH_PROMPT);
        }
        return await stepContext.endDialog();
    }

    private async displayToken(stepContext: WaterfallStepContext) {
        console.log("displayToken");
        const tokenResponse = stepContext.result;
        if (tokenResponse) {
            await stepContext.context.sendActivity(`Here is your token ${ tokenResponse.token }`);
        }
        return await stepContext.endDialog();
    }
SLdragon commented 3 weeks ago

Hi, @NWH-SAmin5 , the command bot is used to receive user's command, and then send back message to user, you cannot start a dialog from command handler. If you just want to get the token, and then start a new dialog, I suggest you refer this sample project, which uses ssoDialog to achieve this:

https://github.com/OfficeDev/teams-toolkit-samples/tree/dev/bot-sso

WaseekSenju commented 2 weeks ago

image image

I am getting this error on the sample project as well.

SLdragon commented 2 weeks ago

Hi @WaseekSenju , which sample project are you using? Please share more details so that we can take a look, thank you~

microsoft-github-policy-service[bot] commented 1 week ago

This issue has been automatically marked as stale because it has been marked as requiring author feedback but has not had any activity for 7 days. It will be closed if no further activity occurs within 3 days of this comment. If it is closed, feel free to comment when you are able to provide the additional information and we will re-investigate.

microsoft-github-policy-service[bot] commented 3 days ago

Due to lack of details for further investigation, we will archive the issue for now. In case you still have following-up questions on this issue, please always feel free to reopen the issue by clicking ‘reopen issue’ button below the comment box. We will get back to you as soon as possible.