Search code examples
c#azurebotframeworkbotnet

Access to user data via the StateClient for a localhost bot that isn't registered without an activity


Hi all i'm trying to save some data from a login controller to the users data store.

[HttpGet, Route("api/{channelId}/{userId}/authorize")]
public async System.Threading.Tasks.Task<HttpResponseMessage> Authorize(string channelId, string userId, string code)
{
    string protocalAndDomain = HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority);

    AuthenticationContext ac = new AuthenticationContext(Constants.AD_AUTH_CONTEXT);
    ClientCredential cc = new ClientCredential(Constants.AD_CLIENT_ID, Constants.AD_CLIENT_SECRET);
    AuthenticationResult ar = await ac.AcquireTokenByAuthorizationCodeAsync(code, new Uri(protocalAndDomain + "/api/" + channelId + "/" + userId + "/authorize"), cc);
    MicrosoftAppCredentials.TrustServiceUrl(protocalAndDomain, DateTime.Now.AddHours(1));

    if (!String.IsNullOrEmpty(ar.AccessToken))
    {
        // Store access token & User Id to bot state
        //var botCred = new MicrosoftAppCredentials(Constants.MS_APP_ID, Constants.MS_APP_PASSWORD);
        //https://state.botframework.com

        using (var sc = new StateClient(new Uri("http://localhost:3979/")))
            if (sc != null)
            {
                var botData = new BotData(data: null, eTag: "*");
                botData.SetProperty("accessToken", ar.AccessToken);
                botData.SetProperty("userEmail", ar.UserInfo.DisplayableId);

                //i get a 401 response here
                await sc.BotState.SetUserDataAsync(channelId, userId, botData);
            }


        var response = Request.CreateResponse(HttpStatusCode.Moved);
        response.Headers.Location = new Uri("/loggedin.html", UriKind.Relative);
        return response;

    }
    else
        return Request.CreateResponse(HttpStatusCode.Unauthorized);
}

I've seen examples in where you can use the AppId an appPassword to access the bot state, but to my understanding those aren't available until your bot is published/regested in the azuer application portal which i currently can't do.

or that you can access it via the activity which again i don't have access to.

this is actually just a temporary solution my plan is to eventually save the user data to Azure table storage, however i would like a temporary solution in the mean time; I'm considering serializing and deserializing a dictionary to local text file, but that seems like overkill for now and it seems silly that i can't locally save to the user data without having my app registered in azure.

cheers any help is much appreciated.


Solution

  • With this line:

    var sc = new StateClient(new Uri("http://localhost:3979/"))
    

    you are instructing the BotBuilder to use a State Service at http://localhost:3979/ But there is no State Service at that endpoint.

    If you want to have a temporary solution, until you add Azure Table Storage, you can use the InMemoryDataStore:

    protected void Application_Start()
    {
        Conversation.UpdateContainer(
            builder =>
                {
                    builder.RegisterModule(new AzureModule(Assembly.GetExecutingAssembly()));
    
                    var store = new InMemoryDataStore(); // volatile in-memory store
    
                    builder.Register(c => store)
                        .Keyed<IBotDataStore<BotData>>(AzureModule.Key_DataStore)
                        .AsSelf()
                        .SingleInstance();
    
    
                });
    
        GlobalConfiguration.Configure(WebApiConfig.Register);
    }
    

    Note: this requires the Azure Extensions nuget package https://www.nuget.org/packages/Microsoft.Bot.Builder.Azure/

    Once the InMemoryDataStore is registered, you can access it using something like:

    var message = new Activity()
                    {
                        ChannelId = ChannelIds.Directline,
                        From = new ChannelAccount(userId, userName),
                        Recipient = new ChannelAccount(botId, botName),
                        Conversation = new ConversationAccount(id: conversationId),
                        ServiceUrl = serviceUrl
                    }.AsMessageActivity();
    
    using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, message))
    {
        var botDataStore = scope.Resolve<IBotDataStore<BotData>>();
        var key = new AddressKey()
        {
            BotId = message.Recipient.Id,
            ChannelId = message.ChannelId,
            UserId = message.From.Id,
            ConversationId = message.Conversation.Id,
            ServiceUrl = message.ServiceUrl
        };
        var userData = await botDataStore.LoadAsync(key, BotStoreType.BotUserData, CancellationToken.None);
    
        userData.SetProperty("key 1", "value1");
        userData.SetProperty("key 2", "value2");
    
        await botDataStore.SaveAsync(key, BotStoreType.BotUserData, userData, CancellationToken.None);
        await botDataStore.FlushAsync(key, CancellationToken.None);
    }