Search code examples
botframeworkbotsdirect-line-botframeworkweb-chat

How to set the User avatar dynamically in BotFramework-WebChat based on logged in user using OAuthCard


I have developed a chat bot using Microsoft Bot Framework V4, and have used BotFramework-WebChat for providing the user to chat from website using DirectLine Token,

I am able to set the bot avatar and the user avatar by assigning the static public image URL. The problem is that I want to set the user avatar dynamically in the WebChat using below steps

  1. Fetch the user icon using the Microsoft graph API after OAuthCard login
  2. Set the signed in user image in the Webchat styleSetOptions dynamically.

I have gone through the Demo for setting the bot framework server and the webchat for the bot by following the samples provided

bot server == https://github.com/Microsoft/BotBuilder-Samples

webchat == https://github.com/Microsoft/BotFramework-WebChat

but there is no proper example or documentation on how to set the user image after the user has signed in. using the signed user object.

can any one point on the right direction on how can it be achieved.

Thanks in advance


Solution

  • You can achieve this by wrapping the Graph API call and result into the result of the AAD login process. The following code is based off of the BotBuilder-Samples 24.bot-authentication-msgraph sample and BotFramework-WebChat 17.chat-send-history sample using React.Component.

    (Please be aware that the Graph sample currently located in the master branch does not include obtaining the AAD login user's photo. I have a PR which adds this feature into the sample, however I have included it here, as well. I used the WebChat sample as a reference for building the below.)

    WebChat

    You will need these files from sample #17, followed by the App.js file that needs altering:

    • public [folder]
      • favicon.ico
      • index.html
      • manifest.json
    • src [folder]
      • App.js
      • index.css
      • index.js
    • .env
    • package.json

    App.js:

    Note: I generate the direct line token locally in a separate project. This assumes an AAD profile has a photo.

    import React from 'react';
    
    import ReactWebChat, { createDirectLine, createStore} from 'botframework-webchat';
    
    export default class extends React.Component {
      constructor(props) {
        super(props);
    
        this.state = {
          directLine: null,
          avatarState: false, // Sets value to false; Is updated to true after login
          // Sets the default styleOptions used during rendering
          styleOptions: {
              botAvatarImage: 'https://learn.microsoft.com/en-us/azure/bot-service/v4sdk/media/logo_bot.svg?view=azure-bot-service-4.0',
              botAvatarInitials: 'BF',
              userAvatarImage: 'https://github.com/compulim.png?size=64',
              userAvatarInitials: 'WC'
            }
        };
    
        // Creates the listener filtering for a new avatar image and applies to styleOptions
        this.store = createStore(
          {},
          () => next => action => {
            if (action.type === 'DIRECT_LINE/INCOMING_ACTIVITY') {
              }
            if (action.type === 'DIRECT_LINE/INCOMING_ACTIVITY' 
              && action.payload.activity.attachments 
              && 0 in action.payload.activity.attachments
              && this.state.avatarState === false) {
                let attachments = action.payload.activity.attachments;
                if ('content' in attachments[0] && 'images' in attachments[0].content) {
                  this.setState(() => ({
                      styleOptions: {
                        userAvatarImage: attachments[0].content.images[0].contentUrl
                      },
                      avatarState: true
                  }));
                }
            }
    
            return next(action);
          }
        )
      }
    
      componentDidMount() {
        this.fetchToken();
      }
    
      async fetchToken() {
        const res = await fetch('http://localhost:3979/directline/token', { method: 'POST' });
        const { token } = await res.json();
    
        this.setState(() => ({
          directLine: createDirectLine({ token })
        }));
      }
    
      render() {
        return (
          this.state.directLine ?
            <ReactWebChat
              className="chat"
              directLine={ this.state.directLine }
              styleOptions={ this.state.styleOptions }
              store={ this.store }
              { ...this.props }
            />
          :
            <div>Connecting to bot&hellip;</div>
        );
      }
    }
    

    package.json

    {
      "name": "change-avatar",
      "version": "0.1.0",
      "private": true,
      "homepage": "",
      "dependencies": {
        "botframework-webchat": ">= 0.0.0-0",
        "react": "^16.6.3",
        "react-dom": "^16.6.3",
        "react-scripts": "2.1.1"
      },
      "scripts": {
        "start": "react-scripts start",
        "build": "react-scripts build",
        "eject": "react-scripts eject"
      },
      "browserslist": [
        ">0.2%",
        "not dead",
        "not ie <= 11",
        "not op_mini all"
      ]
    }
    

    MS Graph Bot

    Update the following files in sample #24:

    bot.js:

    Replace async processStep with:

    async processStep(step) {
        // We do not need to store the token in the bot. When we need the token we can
        // send another prompt. If the token is valid the user will not need to log back in.
        // The token will be available in the Result property of the task.
        const tokenResponse = step.result;
    
        // If the user is authenticated the bot can use the token to make API calls.
        if (tokenResponse !== undefined) {
            let parts = await this.commandState.get(step.context);
            if (!parts) {
                parts = step.context.activity.text;
            }
            const command = parts.split(' ')[0].toLowerCase();
            if (command === 'me') {
                await OAuthHelpers.listMe(step.context, tokenResponse);
            } else if (command === 'send') {
                await OAuthHelpers.sendMail(step.context, tokenResponse, parts.split(' ')[1].toLowerCase());
            } else if (command === 'recent') {
                await OAuthHelpers.listRecentMail(step.context, tokenResponse);
            } else {
                let photoResponse = await OAuthHelpers.loginData(step.context, tokenResponse);
                const card = CardFactory.heroCard(
                    `Welcome ${ photoResponse.displayName }, you are now logged in.`,
                    [photoResponse],
                    []
                );
                const reply = ({ type: ActivityTypes.Message });
                reply.attachments = [card];
                await step.context.sendActivity(reply);
            }
        } else {
            // Ask the user to try logging in later as they are not logged in.
            await step.context.sendActivity(`We couldn't log you in. Please try again later.`);
        }
        return await step.endDialog();
    };
    

    oauth-helpers.js:

    Add static async loginData:

    /**
     * Displays information about the user in the bot.
     * @param {TurnContext} turnContext A TurnContext instance containing all the data needed for processing this conversation turn.
     * @param {TokenResponse} tokenResponse A response that includes a user token.
     */
    static async loginData(turnContext, tokenResponse) {
        if (!turnContext) {
            throw new Error('OAuthHelpers.loginData(): `turnContext` cannot be undefined.');
        }
        if (!tokenResponse) {
            throw new Error('OAuthHelpers.loginData(): `tokenResponse` cannot be undefined.');
        }
    
        try {
            // Pull in the data from Microsoft Graph.
            const client = new SimpleGraphClient(tokenResponse.token);
            const me = await client.getMe();
            const photoResponse = await client.getPhoto();
    
            // Attaches user's profile photo to the reply activity.
            if (photoResponse != null) {
                let replyAttachment;
                const base64 = Buffer.from(photoResponse, 'binary').toString('base64');
                replyAttachment = {
                    contentType: 'image/jpeg',
                    contentUrl: `data:image/jpeg;base64,${ base64 }`
                };
                replyAttachment.displayName = me.displayName;
                return (replyAttachment);
            }
        } catch (error) {
            throw error;
        }
    }
    

    simple-graph-client.js:

    Add async getPhoto:

    /**
     * Collects the user's photo.
     */
    async getPhoto() {
        return await this.graphClient
            .api('/me/photo/$value')
            .responseType('ArrayBuffer')
            .version('beta')
            .get()
            .then((res) => {
                return res;
            })
            .catch((err) => {
                console.log(err);
            });
    }
    

    package.json:

    Be sure the @microsoft/microsoft-graph-client installs version 1.0.0 due to breaking changes around AAD 'displayName' acquisition in subsequent versions.

    Once the above code was implemented, I was able to login which, upon success, immediately updated the user avatar.

    Hope of help!