3
votes

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.

2
Can you add your MainDialog?Ezequiel Jadib
Sure. I added it!danyolgiax
No typo here? Using two different words: UserIsAuthenticated and UserAuthenticateduser6269864
In LUIS UserIsAuthenticated text is binded to UserAuthenticated intent. In the worst case it would enter in the empty [LuisIntent("")] intent. Instead it won't call any of those intents.danyolgiax

2 Answers

0
votes

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.

0
votes

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);
    }