We are trying to save a simple serializable object in PrivateConversationData in a Dialog and access it from state in MessagesController
For some reason, after we do Context.Done in the dialog, we are not getting back data stored in the state
public static async Task SetUserAsync<T>(IActivity activity, T botUser) where T : IBotUser
{
if (botUser != null)
{
using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, activity.AsMessageActivity()))
{
var botDataStore = scope.Resolve<IBotDataStore<BotData>>();
var key = new AddressKey()
{
BotId = activity.Recipient.Id,
ChannelId = activity.ChannelId,
UserId = activity.From.Id,
ConversationId = activity.Conversation.Id,
ServiceUrl = activity.ServiceUrl
};
var privateData = await botDataStore.LoadAsync(key, BotStoreType.BotPrivateConversationData, CancellationToken.None);
privateData.SetProperty<T>(Keys.CacheBotUserKey, botUser);
await botDataStore.SaveAsync(key, BotStoreType.BotPrivateConversationData, privateData, CancellationToken.None);
await botDataStore.FlushAsync(key, CancellationToken.None);
}
}
}
The dialog code is as simple as
public override async Task ProcessMessageAsync(IDialogContext context, IAwaitable<IMessageActivity> result)
{
BotUser user = new BotUser { UserId = "user1" };
await StateHelper.SetUserAsync(context.Activity, user);
var userFromState = await StateHelper.GetUserAsync<BotUser>(context.Activity);
Debug.WriteLine("Within dialog (after set) >" + userFromState?.UserId);
context.Done<object>(null);
}
and in MessagesController.cs we are simply calling the
await Conversation.SendAsync(activity, () => new DummyDialog()).ConfigureAwait(false);
var user = await StateHelper.GetUserAsync<BotUser>(activity);
System.Diagnostics.Debug.WriteLine("Within MC (end) >" + user?.UserId);
In this case, we get below output
Within dialog (after set) > user1
Within MC (end) >
Is there anything wrong?
When a dialog loads, the state is loaded with it and can be accessed/saved by using the methods on the Context object. When the dialog completes, the state is persisted by the SDK. If you create a new scope, nested within a dialog, and attempt to load/persist the state: then the dialog state will overwrite it. To work around this, you can add a method your StateHelper that accepts an IDialogContext, and use that while within a Dialog.
Related
I created a bot with a command, that allows the user to configure some sort of 'feed' to their channel.
This feed is supposed to send a message, save guild, channel and message id. And in a stand-alone update cycle, try to update the message with new information.
This all works fairly well, as long as it is within the same session.
Say the bot losses it's connection due to a discord outage, and re-connects x amount of time later, the bot no longer seems to be able to find, and thus update the message anymore.
In particular, it seems to be unable to retrieve the message by id
var message = await channel.GetMessageAsync(playtimeFeed.MessageId) as SocketUserMessage;
It's worth to note that I make use of _settings which is persisted in json format, and is loaded again upon bot reboot.
I also confirmed that the message still exists in the server at the channel, with the same message id. And that the bot has permissions to view the message history of the channel.
Thus my question, how come the GetMessageAsync is unable to retrieve a previously posted message after reconnecting?
Initialy invoked command
public async Task BindPlaytimeFeedAsync(ICommandContext context)
{
var builder = await _scumService.GetTop25PlaytimeByDate(new DateTime(), DateTime.Now);
var message = await context.Channel.SendMessageAsync(null, false, builder.Build());
_settings.PlaytimeFeed = new MessageInfo()
{
GuildId = context.Guild.Id,
ChannelId = context.Channel.Id,
MessageId = message.Id,
};
var ptFeedMessage = await context.Channel.SendMessageAsync("Playtime feed is now bound to this channel (this message self-destructs in 5 seconds)");
await Task.Delay(5000);
await ptFeedMessage.DeleteAsync();
}
The refresh interval of the feed is defined alongside the bot itself using a timer as seen below.
...
_client = new DiscordSocketClient(
new DiscordSocketConfig
{
LogLevel = LogSeverity.Verbose,
AlwaysDownloadUsers = true, // Start the cache off with updated information.
MessageCacheSize = 1000
}
);
_service = ConfigureServices();
_feedInterval = new Timer(async (e) =>
{
Console.WriteLine("doing feed stuff");
await HandleFeedsAsync();
}, null, 15000, 300000);
CmdHandler = new CommandHandler(_service, state);
...
private async Task HandleFeedsAsync()
{
var botSettings = _service.GetService<ISettings>() as BotSettings;
await HandleKdFeedAsync(botSettings.KdFeed);
await HandlePlaytimeFeedAsync(botSettings.PlaytimeFeed);
await HandleWeeklyPlaytimeFeed(botSettings.WeeklyPlaytimeFeed);
await HandleAdminFeed(botSettings);
}
And ultimately the message is overwritten using the below snippet.
private async Task HandlePlaytimeFeedAsync(MessageInfo playtimeFeed)
{
if (playtimeFeed == null)
return;
var scumService = _service.GetService<ScumService>();
var guild = _client.GetGuild(playtimeFeed.GuildId);
var channel = guild.GetTextChannel(playtimeFeed.ChannelId);
var message = await channel.GetMessageAsync(playtimeFeed.MessageId) as SocketUserMessage;
if (message == null)
return;
var builder = await scumService.GetTop25PlaytimeByDate(new DateTime(), DateTime.Now);
await message.ModifyAsync(prop =>
{
prop.Embed = builder.Build();
});
}
var message = await channel.GetMessageAsync(playtimeFeed.MessageId) as SocketUserMessage;
The GetMessageAsync method attempts to retrieve a message from cache as a SocketUserMessage, if however the message is not found in cache, a rest request is performed which would return a RestUserMessge. By performing a soft cast on the result of GetMessageAsync, you can get null if/when a RestUserMessage is returned.
When the possibility exists that the message you are dealing with can be either a Socket entity or Rest entity, simply use the interface to interact with it -- IUserMessage.
I'm trying to replace the InMemory storage with Cosmos storage provided by Azure.
I'm storing some information within the conversation data, using it within my dialogs and resetting it from my message controller if a certain command was sent.
The way I access my conversation data within a dialog is :
context.ConversationData.GetValueOrDefault<String>("varName", "");
The way I'm resetting my data from within the messageContoller is :
StateClient stateClient = activity.GetStateClient();
BotData userData = await stateClient.BotState.GetConversationDataAsync(activity.ChannelId, activity.Conversation.Id);
userData.RemoveProperty("varName");
await stateClient.BotState.SetConversationDataAsync(activity.ChannelId,
activity.Conversation.Id, userData);
The previous line of codes are working properly if I used InMemory. as soon as I switch to cosmos the resetting part of code fails. While debugging the issue I found that the conversation data object returned is never the same as the one returned from within the dialog and I was unable to reset the variables.
This is the way I'm connecting to cosmos database:
var uri = new Uri(ConfigurationManager.AppSettings["DocumentDbUrl"]);
var key = ConfigurationManager.AppSettings["DocumentDbKey"];
var store = new DocumentDbBotDataStore(uri, key);
Conversation.UpdateContainer(
builder = >{
builder.Register(c = >store).Keyed < IBotDataStore < BotData >> (AzureModule.Key_DataStore).AsSelf().SingleInstance();
builder.Register(c = >new CachingBotDataStore(store, CachingBotDataStoreConsistencyPolicy.ETagBasedConsistency)).As < IBotDataStore < BotData >> ().AsSelf().InstancePerLifetimeScope();
});
Any idea why this is happening ?
Edit:
When using the im memory storage this code works just fine, but replacing the storage with the cosmos storage fails to retrieve the conversation data outside the dialog (the dialog gets/sets the conversation data correctly but the StateClents fails to retrieve the data correctly it returns an empty object but the weird part is that is has the same conversation ID as the one returned from the dialog)
While debugging the issue I found that the conversation data object returned is never the same as the one returned from within the dialog and I was unable to reset the variables.
Please make sure you are using same conversation when you do saving data and resetting data operations.
Besides, I do a test using the following sample code, I can save and reset conversation data as expected.
In message controller:
public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
if (activity.Type == ActivityTypes.Message)
{
if (activity.Text=="reset")
{
var message = activity as IMessageActivity;
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.BotConversationData, CancellationToken.None);
//var varName = userData.GetProperty<string>("varName");
userData.SetProperty<object>("varName", null);
await botDataStore.SaveAsync(key, BotStoreType.BotConversationData, userData, CancellationToken.None);
await botDataStore.FlushAsync(key, CancellationToken.None);
}
}
await Conversation.SendAsync(activity, () => new Dialogs.RootDialog());
}
else
{
HandleSystemMessage(activity);
}
var response = Request.CreateResponse(HttpStatusCode.OK);
return response;
}
public class AddressKey : IAddress
{
public string BotId { get; set; }
public string ChannelId { get; set; }
public string ConversationId { get; set; }
public string ServiceUrl { get; set; }
public string UserId { get; set; }
}
In dialog:
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
{
var activity = await result as Activity;
// calculate something for us to return
int length = (activity.Text ?? string.Empty).Length;
var varName = "";
if (activity.Text.ToLower().Contains("hello"))
{
context.ConversationData.SetValue<string>("varName", activity.Text);
}
if (activity.Text.ToLower().Contains("getval"))
{
varName = context.ConversationData.GetValueOrDefault<string>("varName", "");
activity.Text = $"{varName} form cosmos";
}
if (activity.Text.ToLower().Contains("remove"))
{
activity.Text = "varName is removed";
}
// return our reply to the user
await context.PostAsync($"{activity.Text}");
context.Wait(MessageReceivedAsync);
}
Test steps:
After enter hello bot, can find it saved as conversation data in Cosmosdb.
After enter “reset”, can find the value of varName is reset to null.
activity.GetStateClient() is deprecated: https://github.com/Microsoft/BotBuilder/blob/a6b9ec56393d6e5a4be74b324f722b5ca8840b4a/CSharp/Library/Microsoft.Bot.Connector.Shared/ActivityEx.cs#L329
It only uses the default state service. If you are using BotBuilder-Azure for state, then your CosmosDb implementation will not be retrieved using .GetStateClient(). Please refer to #Fei's answer for how to manipulate state using DialogModule.BeginLifetimeScope or dialog.Context methods.
In a dialog within my bot, I store a flag value in the ConversationData like so:
context.ConversationData.SetValue("SomeFlag", true);
Later, I need to check that flag in my MessagesController, before the message is dispatched to a dialog. As per this previous question I tried retrieving the ConversationData in via the StateClient like this:
public async Task<HttpResponseMessage> Post([FromBody] Activity incomingMessage)
{
StateClient stateClient = incomingMessage.GetStateClient();
BotData userData = await stateClient.BotState.GetConversationDataAsync(message.ChannelId, message.Conversation.Id);
bool finishedQuote = userData.GetProperty<bool>("SomeFlag");
//...
// do conditional logic, then dispatch to a dialog as normal
}
However, at runtime, the userData variable holds a BotData object where userData.Data is null, and I'm unable to retrieve any stored flags via GetProperty. I don't see anything in the relevant documentation that helps shed light on this issue - what might I be doing wrong here? Is there something I'm misunderstanding?
The following should work for what you need:
if (activity.Type == ActivityTypes.Message)
{
var message = activity as IMessageActivity;
using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, message))
{
var botDataStore = scope.Resolve<IBotDataStore<BotData>>();
var key = Address.FromActivity(message);
ConversationReference r = new ConversationReference();
var userData = await botDataStore.LoadAsync(key, BotStoreType.BotUserData, CancellationToken.None);
//you can get/set UserData, ConversationData, or PrivateConversationData like below
//set state data
userData.SetProperty("key 1", "value1");
userData.SetProperty("key 2", "value2");
//get state data
userData.GetProperty<string>("key 1");
userData.GetProperty<string>("key 2");
await botDataStore.SaveAsync(key, BotStoreType.BotUserData, userData, CancellationToken.None);
await botDataStore.FlushAsync(key, CancellationToken.None);
}
await Conversation.SendAsync(activity, () => new Dialogs.RootDialog());
}
Initialize BotState object with StateClient as below. Try the below code
public static T GetStateData<T>(Activity activity, string key)
{
BotState botState = new BotState(activity.GetStateClient());
BotData botData = botState.GetConversationData(activity.ChannelId, activity.Conversation.Id);
return botData.GetProperty<T>(key);
}
In MessagesController.cs, following code is executed in Post method,
if (activity.Text.ToLowerInvariant().StartsWith("code:"))
{
var stateClient = activity.GetStateClient();
var botData = await stateClient.BotState.GetUserDataAsync(activity.ChannelId, activity.From.Id);
var token = botData.GetProperty<string>("AccessToken");
BotUserModel botUser = CreateNewUser(token);
var privateData = await stateClient.BotState.GetPrivateConversationDataAsync(activity.ChannelId, activity.Conversation.Id, activity.From.Id);
privateData.SetProperty<BotUserModel>("botUser", botUser);
}
else
{
await Conversation.SendAsync(activity, () => new LuisDialog());
}
This is saving botUser into PrivateConversationData dictionary
Inside the LUIS Dialog,
[LuisIntent("DoSomething")]
public async Task DoSomething(IDialogContext context, LuisResult result)
{
BotUserModel botUser;
context.PrivateConversationData.TryGetValue<BotUserModel>("botUser", out botUser);
// Just to test
context.PrivateConversationData.SetValue<BotUserModel>("TestValue", new BotUserModel());
}
Here, I'm getting an exception KeyNotFoundException:botUser
BotUserModel is marked [Serializable] and has few public properties - all with get/set. I checked the IBotBag (i.e. PrivateConversationData) and its empty
[LuisIntent("DoSomethingNew")]
public async Task DoSomethingNew(IDialogContext context, LuisResult result)
{
// Assuming DoSomething intent is invoked first
BotUserModel botUser;
context.PrivateConversationData.TryGetValue<BotUserModel>("TestValue", out botUser);
// Here, no exception!
}
Now, here I get the value of TestValue set in LUIS Dialog in DoSomething method.
So essentially, any data set to PrivateConversationData or UserData inside LUIS Intent is accessible by other LUIS intents; whereas, data set in MessageController.cs (before LUIS is called) is not accessible within LUIS.
Tried with UserData as well.
Am I missing anything?
You are forgetting to set the private data store back into the state client. This should make it work.
var privateData = await stateClient.BotState.GetPrivateConversationDataAsync(activity.ChannelId, activity.Conversation.Id, activity.From.Id);
privateData.SetProperty<BotUserModel>("botUser", botUser);
await stateClient.BotState.SetPrivateConversationDataAsync(activity.ChannelId, activity.Conversation.Id, activity.From.Id, privateData);
Check out the documentation on the state client.
I am refactoring my ASP MVC code in session_start in Global.asax.cs with an async call to external service. I either get a white page with endless spinning in IE, or execution immediately returns to the calling thread. In the Session_start() when I tried .Result, I got white page with spinning IE icon. When I tried .ContinueWith(), the execution return to the next line which depends on the result from the async. Thus authResult is always null. Can someone help? Thanks.
This is from the Session_Start()
if (Session["userProfile"] == null) {
//call into an async method
//authResult = uc.checkUserViaWebApi(networkLogin[userLoginIdx]).Result;
var userProfileTask = uc.checkUserViaWebApi(networkLogin[userLoginIdx])
.ContinueWith(result => {
if (result.IsCompleted) {
authResult = result.Result;
}
});
Task.WhenAll(userProfileTask);
if (authResult.Result == enumAuthenticationResult.Authorized) {
This is from User_Controller class
public async Task < AuthResult > checkUserViaWebApi(string networkName) {
UserProfile _thisProfile = await VhaHelpersLib.WebApiBroker.Get < UserProfile > (
System.Configuration.ConfigurationManager.AppSettings["userWebApiEndpoint"], "User/Profile/" + networkName);
AuthResult authenticationResult = new AuthResult();
if (_thisProfile == null) /*no user profile*/ {
authenticationResult.Result = enumAuthenticationResult.NoLSV;
authenticationResult.Controller = "AccessRequest";
authenticationResult.Action = "LSVInstruction";
}
This is helper class that does the actual call using HttpClient
public static async Task<T> Get<T>(string baseUrl, string urlSegment)
{
string content = string.Empty;
using(HttpClient client = GetClient(baseUrl))
{
HttpResponseMessage response = await client.GetAsync(urlSegment.TrimStart('/')).ConfigureAwait(false);
if(response.IsSuccessStatusCode)
{
content = await response.Content.ReadAsStringAsync();
}
return JsonConvert.DeserializeObject<T>(content);
}
It doesn't make sense to call User_Controller from Session_Start.
You want to call VhaHelpersLib directly inside Session_Start, if VhaHelpersLib doesn't have any dependencies.
Since Session_Start is not async, you want to use Result.
var setting = ConfigurationManager.AppSettings["userWebApiEndpoint"];
UserProfile profile = await VhaHelpersLib.WebApiBroker.Get<UserProfile>(
setting, "User/Profile/" + networkName).Result;
if (profile == enumAuthenticationResult.Authorized) {
...
}