3

I implemented an external login for my BOT. When external site calls Bot CallBack method I need to set token and username in PrivateConversationData and then resume chat with a message like "Welcome back [username]!".

To display this message I send a MessageActivity but this activity never connects to my chat and won't fire the appropriate [LuisIntent("UserIsAuthenticated")].

Other intents, out of login-flow, works as expected.

This is the callback method:

public class OAuthCallbackController : ApiController
{
    [HttpGet]
    [Route("api/OAuthCallback")]
    public async Task OAuthCallback([FromUri] string userId, [FromUri] string botId, [FromUri] string conversationId,
        [FromUri] string channelId, [FromUri] string serviceUrl, [FromUri] string locale,
        [FromUri] CancellationToken cancellationToken, [FromUri] string accessToken, [FromUri] string username)
    {
        var resumptionCookie = new ResumptionCookie(TokenDecoder(userId), TokenDecoder(botId),
            TokenDecoder(conversationId), channelId, TokenDecoder(serviceUrl), locale);

            var container = WebApiApplication.FindContainer();

            var message = resumptionCookie.GetMessage();
            message.Text = "UserIsAuthenticated";

            using (var scope = DialogModule.BeginLifetimeScope(container, message))
            {
                var botData = scope.Resolve<IBotData>();
                await botData.LoadAsync(cancellationToken);

                botData.PrivateConversationData.SetValue("accessToken", accessToken);
                botData.PrivateConversationData.SetValue("username", username);

                ResumptionCookie pending;
                if (botData.PrivateConversationData.TryGetValue("persistedCookie", out pending))
                {
                    botData.PrivateConversationData.RemoveValue("persistedCookie");
                    await botData.FlushAsync(cancellationToken);
                }

                var stack = scope.Resolve<IDialogStack>();
                var child = scope.Resolve<MainDialog>(TypedParameter.From(message));
                var interruption = child.Void<object, IMessageActivity>();

                try
                {
                    stack.Call(interruption, null);

                    await stack.PollAsync(cancellationToken);
                }
                finally
                {
                    await botData.FlushAsync(cancellationToken);
                }
            }
        }
    }   

    public static string TokenDecoder(string token)
    {
        return Encoding.UTF8.GetString(HttpServerUtility.UrlTokenDecode(token));
    }
}

This is the controller:

public class MessagesController : ApiController
{
    private readonly ILifetimeScope scope;

    public MessagesController(ILifetimeScope scope)
    {
        SetField.NotNull(out this.scope, nameof(scope), scope);
    }

    public async Task<HttpResponseMessage> Post([FromBody] Activity activity, CancellationToken token)
    {
        if (activity != null)
        {
            switch (activity.GetActivityType())
            {
                case ActivityTypes.Message:
                    using (var scope = DialogModule.BeginLifetimeScope(this.scope, activity))
                    {
                        var postToBot = scope.Resolve<IPostToBot>();
                        await postToBot.PostAsync(activity, token);
                    }
                    break;
            }
        }

        return new HttpResponseMessage(HttpStatusCode.Accepted);
    }
}

This is how I registered components:

protected override void Load(ContainerBuilder builder)
    {
        base.Load(builder);

        builder.Register(
            c => new LuisModelAttribute("myId", "SubscriptionKey"))
            .AsSelf()
            .AsImplementedInterfaces()
            .SingleInstance();

        builder.RegisterType<MainDialog>().AsSelf().As<IDialog<object>>().InstancePerDependency();

        builder.RegisterType<LuisService>()
            .Keyed<ILuisService>(FiberModule.Key_DoNotSerialize)
            .AsImplementedInterfaces()
            .SingleInstance();
    }

This is the dialog:

[Serializable]
public sealed class MainDialog : LuisDialog<object>
{
    public static readonly string AuthTokenKey = "TestToken";
    public readonly ResumptionCookie ResumptionCookie;
    public static readonly Uri CloudocOauthCallback = new Uri("http://localhost:3980/api/OAuthCallback");

    public MainDialog(IMessageActivity activity, ILuisService luis)
        : base(luis)
    {
        ResumptionCookie = new ResumptionCookie(activity);
    }

    [LuisIntent("")]
    public async Task None(IDialogContext context, LuisResult result)
    {
        await context.PostAsync("Sorry cannot understand!");
        context.Wait(MessageReceived);
    }

    [LuisIntent("UserAuthenticated")]
    public async Task UserAuthenticated(IDialogContext context, LuisResult result)
    {
        string username;
        context.PrivateConversationData.TryGetValue("username", out username);

        await context.PostAsync($"Welcome back {username}!");
        context.Wait(MessageReceived);
    }

    [LuisIntent("Login")]
    private async Task LogIn(IDialogContext context, LuisResult result)
    {
        string token;
        if (!context.PrivateConversationData.TryGetValue(AuthTokenKey, out token))
        {
            context.PrivateConversationData.SetValue("persistedCookie", ResumptionCookie);

            var loginUrl = CloudocHelpers.GetLoginURL(ResumptionCookie, OauthCallback.ToString());

            var reply = context.MakeMessage();

            var cardButtons = new List<CardAction>();
            var plButton = new CardAction
            {
                Value = loginUrl,
                Type = ActionTypes.Signin,
                Title = "Connetti a Cloudoc"
            };
            cardButtons.Add(plButton);
            var plCard = new SigninCard("Connect", cardButtons);

            reply.Attachments = new List<Attachment>
            {
                plCard.ToAttachment()
            };

            await context.PostAsync(reply);
            context.Wait(MessageReceived);
        }
        else
        {
            context.Done(token);
        }
    }
}

What I miss?

Update

Also tried with ResumeAsync in callback method:

var container = WebApiApplication.FindContainer();

var message = resumptionCookie.GetMessage();
message.Text = "UserIsAuthenticated";

using (var scope = DialogModule.BeginLifetimeScope(container, message))
{
     var botData = scope.Resolve<IBotData>();
     await botData.LoadAsync(cancellationToken);

     botData.PrivateConversationData.SetValue("accessToken", accessToken);
     botData.PrivateConversationData.SetValue("username", username);

     ResumptionCookie pending;
     if (botData.PrivateConversationData.TryGetValue("persistedCookie", out pending))
     {
         botData.PrivateConversationData.RemoveValue("persistedCookie");
         await botData.FlushAsync(cancellationToken);
     }

     await Conversation.ResumeAsync(resumptionCookie, message, cancellationToken);
 }

but it give me the error Operation is not valid due to the current state of the object.

Update 2

Following Ezequiel idea I changed my code this way:

    [HttpGet]
    [Route("api/OAuthCallback")]
    public async Task OAuthCallback(string state, [FromUri] string accessToken, [FromUri] string username)
    {
        var resumptionCookie = ResumptionCookie.GZipDeserialize(state);
        var message = resumptionCookie.GetMessage();
        message.Text = "UserIsAuthenticated";

        await Conversation.ResumeAsync(resumptionCookie, message);
    }

resumptionCookie seems to be ok:

enter image description here

but await Conversation.ResumeAsync(resumptionCookie, message); continue to give me the error Operation is not valid due to the current state of the object.

Community
  • 1
  • 1
danyolgiax
  • 12,798
  • 10
  • 65
  • 116

2 Answers2

0

You need to resume the conversation with the bot that's why the message is likely not arriving.

Instead of using the dialog stack, try using

await Conversation.ResumeAsync(resumptionCookie, message);

Depending on your auth needs, you might want to consider AuthBot. You can also take a look to the logic on the OAuthCallback controller of the library to get an idea of how they are resuming the conversation with the Bot after auth.

The ContosoFlowers example, is also using the resume conversation mechanism. Not for auth purposes, but for showing how to handle a hypotethical credit card payment.

Ezequiel Jadib
  • 14,767
  • 2
  • 38
  • 43
  • If I try with `ResumeAsync` instead dialog stack I receive the following error: `Operation is not valid due to the current state of the object.` – danyolgiax Nov 08 '16 at 14:36
  • Well hard to know where the issue is happening; though the way to do it is using the ResumeAsync as shown in the examples I provided. Try commenting out all the code around the state and just leave the resumptionCookie.GetMessage and see what happens. Are you sure the values you are using for recreating the cookie are the expected ones? Usually it's better to serialize the cookie and deserialize it (see the samples) – Ezequiel Jadib Nov 08 '16 at 15:13
  • Updated the question... no luck! :( – danyolgiax Nov 09 '16 at 10:43
  • My recommendation here would be to start simplifying the code to nail down the problem. Start without DI for example, or even without a LuisDialog. And once you have the resumption mechanism working, start to add the rest of the pieces. Instead of creating the resumption cookie in the constructor, create it in the login method. Are u using the latest version of the BotBuilder package? Why you are injecting the ILuisService? – Ezequiel Jadib Nov 09 '16 at 11:46
  • I found a scope problem registering Autofac components. I added the working answer! Thanks a lot! – danyolgiax Nov 09 '16 at 11:49
  • Great! Combination of things; wrong scope in the message controller and also not using the Conversation.ResumeAsync. Re reading the code it seems you were using part of the code of the CreateNewConversation sample. If you are using that approach, with the LifetimeScope, you need to do this with the Resumption Cookie (https://github.com/Microsoft/BotBuilder-Samples/blob/master/CSharp/core-CreateNewConversation/Controllers/MessagesController.cs#L54) and then you should inject it like https://github.com/Microsoft/BotBuilder-Samples/blob/master/CSharp/core-CreateNewConversation/SurveyService.cs – Ezequiel Jadib Nov 09 '16 at 12:27
0

I found how to make it works.

Controller:

public class MessagesController : ApiController
{
    public async Task<HttpResponseMessage> Post([FromBody] Activity activity, CancellationToken token)
    {
        if (activity != null)
        {
            switch (activity.GetActivityType())
            {
                case ActivityTypes.Message:

                    var container = WebApiApplication.FindContainer();

                    using (var scope = DialogModule.BeginLifetimeScope(container, activity))
                    {
                        await Conversation.SendAsync(activity, () => scope.Resolve<IDialog<object>>(), token);
                    }
                    break;
            }
        }
        return new HttpResponseMessage(HttpStatusCode.Accepted);
    }
}

Global.asax

public class WebApiApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        GlobalConfiguration.Configure(WebApiConfig.Register);

        var builder = new ContainerBuilder();

        builder.RegisterModule(new DialogModule());

        builder.RegisterModule(new MyModule());

        var config = GlobalConfiguration.Configuration;

        builder.RegisterApiControllers(Assembly.GetExecutingAssembly());

        builder.RegisterWebApiFilterProvider(config);

        var container = builder.Build();
        config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
    }

    public static ILifetimeScope FindContainer()
    {
        var config = GlobalConfiguration.Configuration;
        var resolver = (AutofacWebApiDependencyResolver)config.DependencyResolver;
        return resolver.Container;
    }
}

MyModule:

public sealed class MyModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        base.Load(builder);

        builder.Register(
            c => new LuisModelAttribute("MyId", "SubId"))
            .AsSelf()
            .AsImplementedInterfaces()
            .SingleInstance();

        builder.RegisterType<MainDialog>().AsSelf().As<IDialog<object>>().InstancePerDependency();

        builder.RegisterType<LuisService>()
            .Keyed<ILuisService>(FiberModule.Key_DoNotSerialize)
            .AsImplementedInterfaces()
            .SingleInstance();
    }
}

Callback method:

public class OAuthCallbackController : ApiController
{

    [HttpGet]
    [Route("api/OAuthCallback")]
    public async Task OAuthCallback(string state, [FromUri] CancellationToken cancellationToken, [FromUri] string accessToken, [FromUri] string username)
    {
        var resumptionCookie = ResumptionCookie.GZipDeserialize(state);
        var message = resumptionCookie.GetMessage();
        message.Text = "UserIsAuthenticated";

        using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, message))
        {
            var dataBag = scope.Resolve<IBotData>();
            await dataBag.LoadAsync(cancellationToken);

            dataBag.PrivateConversationData.SetValue("accessToken", accessToken);
            dataBag.PrivateConversationData.SetValue("username", username);

            ResumptionCookie pending;
            if (dataBag.PrivateConversationData.TryGetValue("persistedCookie", out pending))
            {
                dataBag.PrivateConversationData.RemoveValue("persistedCookie");
                await dataBag.FlushAsync(cancellationToken);
            }
        }

        await Conversation.ResumeAsync(resumptionCookie, message, cancellationToken);
    }
danyolgiax
  • 12,798
  • 10
  • 65
  • 116