0
votes

I have looked through the documentation for botframework-webchat and have not been able to find any documentation on how conversations over 1 hour should be handled properly. This situation is most likely to occur if a web page is left idle in the background for an extended period of time.

The directline connection is maintained as long as the webchat remains active on a web page. The problem occurs after a page refresh.

The initial short term solution is to store the relevant conversation information in session storage, such as a token. The problem is that the token for the conversation is refreshed every 15 minutes. The refreshed token must be retrieved in order to maintain the conversation upon a page refresh.

I am sure a hacky work around exists for retrieving the refreshed token from the directline client object using an event callback.

Ideally, I am looking for a clean framework designed approach for handling this situation.

Though a working solution is better than no solution.

Relevant Link: https://github.com/microsoft/BotFramework-WebChat

Thanks.

3
Accepting / upvoting an answer serves the greater Stack Overflow community and anyone with a similar question. If you feel my answer was sufficient, please "accept" and upvote it. If not, let me know how else I can help!Steven Kanberg

3 Answers

1
votes

You can achieve this by implementing cookies in your client side. you can set cookies expiration time to 60 min and you can use watermark to make your chat persistent for one hour. Passing cookie to and from Bot Service.

0
votes

You can achieve this by setting up a "token" server. In the example below, I run this locally when I am developing/testing my bot.

You can use whatever package you want, however I landed on "restify" because I include it in the index.js file of my bot. I simply create a new server, separate from the bot's server, and assign it a port of it's own. Then, when I run the bot it runs automatically, as well. Put your appIds, appPasswords, and secrets in a .env file.

Then, in your web page that's hosting your bot, simply call the endpoint to fetch a token. You'll also notice that the code checks if a token already exists. If so, then it set's an interval with a timer for refreshing the token. The interval, at 1500000 ms, is set to run before the token would otherwise expire (1800000 ms). As such, the token is always getting refreshed. (Just popped in my head: may be smart to log the time remaining and the amount of time that passed, if the user navigated away, in order to set the interval to an accurate number so it refreshes when it should. Otherwise, the interval will reset with the expiration time being something much less.)

Also, I included some commented out code. This is if you want your conversations to persist beyond page refreshes or the user navigating away and returning. This way current conversations aren't lost and the token remains live. May not be necessary depending on your needs, but works well with the above.

Hope of help!


Token Server

/**
 * Creates token server
 */
const path = require('path');
const restify = require('restify');
const request = require('request');
const bodyParser = require('body-parser');

const ENV_FILE = path.join(__dirname, '.env');
require('dotenv').config({ path: ENV_FILE });

const corsToken = corsMiddleware({
  origins: [ '*' ]
});

// Create HTTP server.
let server = restify.createServer();
server.pre(cors.preflight);
server.use(cors.actual);
server.use(bodyParser.json({
  extended: false
}));

server.listen(process.env.port || process.env.PORT || 3500, function() {
  console.log(`\n${ server.name } listening to ${ server.url }.`);
});

// Listen for incoming requests.
server.post('/directline/token', (req, res) => {
  // userId must start with `dl_`
  const userId = (req.body && req.body.id) ? req.body.id : `dl_${ Date.now() + Math.random().toString(36) }`;
  const options = {
    method: 'POST',
    uri: 'https://directline.botframework.com/v3/directline/tokens/generate',
    headers: {
      'Authorization': `Bearer ${ process.env.directLineSecret }`
    },
    json: {
      user: {
        ID: userId
      }
    }
  };
  request.post(options, (error, response, body) => {
    // response.statusCode = 400;
    if (!error && response.statusCode < 300) {
      res.send(body);
      console.log('Someone requested a token...');
    } else if (response.statusCode === 400) {
      res.send(400);
    } else {
      res.status(500);
      res.send('Call to retrieve token from DirectLine failed');
    }
  });
});

// Listen for incoming requests.
server.post('/directline/refresh', (req, res) => {
  // userId must start with `dl_`
  const userId = (req.body && req.body.id) ? req.body.id : `dl_${ Date.now() + Math.random().toString(36) }`;
  const options = {
    method: 'POST',
    uri: 'https://directline.botframework.com/v3/directline/tokens/refresh',
    headers: {
      'Authorization': `Bearer ${ req.body.token }`,
      'Content-Type': 'application/json'
    },
    json: {
      user: {
        ID: userId
      }
    }
  };
  request.post(options, (error, response, body) => {
    if (!error && response.statusCode < 300) {
      res.send(body);
      console.log('Someone refreshed a token...');
    } else {
      res.status(500);
      res.send('Call to retrieve token from DirectLine failed');
    }
  });
});

webchat.html

<script>
  (async function () {
    let { token, conversationId } = sessionStorage;

    [...]

    if ( !token || errorCode === "TokenExpired" ) {
      let res = await fetch( 'http://localhost:3500/directline/token', { method: 'POST' } );

      const { token: directLineToken, conversationId, error } = await res.json();
      // sessionStorage[ 'token' ] = directLineToken;
      // sessionStorage[ 'conversationId' ] = conversationId;
      token = directLineToken;
    }

    if (token) {
      await setInterval(async () => {
        let res = await fetch( 'http://localhost:3500/directline/refresh', {
          method: 'POST',
          headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
          },
          body: JSON.stringify( { token: token } )
        } );
        const { token: directLineToken, conversationId } = await res.json();
        // sessionStorage[ 'token' ] = directLineToken;
        // sessionStorage[ 'conversationId' ] = conversationId;
        token = directLineToken;
      }, 1500000)
    }

    // if ( conversationId ) {
    //   let res = await fetch( `https://webchat.botframework.com/v3/directline/conversations/${ conversationId }`, {
    //     method: 'GET',
    //     headers: {
    //       'Authorization': `Bearer ${ token }`,
    //       'Content-Type': 'application/json'
    //     },
    //   } );

    //   const { conversationId: conversation_Id, error } = await res.json();
    //   if(error) {
    //     console.log(error.code)
    //     errorCode = error.code;
    //   }
    //   conversationId = conversation_Id;
    // }

    [...]

    window.ReactDOM.render(
      <ReactWebChat
        directLine={ window.WebChat.createDirectLine({ token });
      />
    ),
    document.getElementById( 'webchat' );
  });
</script>
0
votes

The solution involved storing the conversation id in session storage instead of the token. Upon a page refresh a new token will be retrieved.

https://github.com/microsoft/BotFramework-WebChat/issues/2899

https://github.com/microsoft/BotFramework-WebChat/issues/2396#issuecomment-530931579

This solution works but it is not optimal. A better solution would be to retrieve the active token in the directline object and store it in session storage. The problem is that a way to cleanly way to retrieve a refreshed token from a directline object does not exist at this point.