Microsoft Bot Framework WebChat: Disable AdaptiveCards submit buttons of previous message - c#

How to disable input/submit button actions in the previous conversation of BotChat - AdaptiveCards in the Microsoft Bot Framework (C#)

I'm imagining you want to display a card to the user that's meant to be used only once, such as a calendar reminder like the one seen in this example.
Bots are mostly meant to have the same kind of access to a channel that a human would, so they can't go back and modify the messages that have already been sent (unless the specific channel allows edits like Slack does). While you can't disable a button in a card that's already part of the conversation history, you can change the way your bot responds to the messages that are generated by that card. What you'll want to do is keep track of whether a button has been clicked and then respond differently when the button is clicked subsequent times.
Here's a basic example of some Dialog code that can respond to messages in three ways. If you type any message and send it to the bot, it will display a card with a button on it. If you click the button, it will say "You did it!" along with the ID of the button you clicked. If you click the same button again, it will say "You already did that!" again attaching the ID.
/// <summary>
/// You'll want a label like this to identify the activity
/// that gets generated in response to your submit button.
/// </summary>
private const string DO_SOMETHING = "DoSomething";
/// <summary>
/// This is passed into context.Wait() in your StartAsync method.
/// </summary>
private async Task MessageReceivedAsync(IDialogContext context,
IAwaitable<IMessageActivity> result)
{
var msg = await result;
if (!string.IsNullOrWhiteSpace(msg.Text))
{
// If msg.Text isn't null or white space then that means the user
// actually typed something, and we're responding to that with a card.
var reply = context.MakeMessage();
var attachment = MakeAdaptiveCardAttachment();
reply.Attachments.Add(attachment);
await context.PostAsync(reply);
}
else
{
// If the user didn't type anything then this could be an activity
// that was generated by your submit button. But we want to make sure
// it is by checking msg.Value.
dynamic value = msg.Value;
try
{
// If value doesn't have a type then this will throw a RuntimeBinderException
if (value != null && value.type == DO_SOMETHING)
{
string id = value.id;
// Check the ID to see if that particular card has been clicked before.
if (!context.PrivateConversationData.ContainsKey(id))
{
// This is how your bot will keep track of what's been clicked.
context.PrivateConversationData.SetValue(id, true);
await context.PostAsync("You did it! " + id);
}
else
{
await context.PostAsync("You already did that! " + id);
}
}
}
catch (Microsoft.CSharp.RuntimeBinder.RuntimeBinderException)
{
// Respond to messages that don't have values with a type (or id).
}
}
context.Wait(MessageReceivedAsync);
}
private static Attachment MakeAdaptiveCardAttachment()
{
var card = new AdaptiveCard();
// We need to identify this specific card if we want to allow multiple
// instances of the card to be clicked.
// A timestamp could work but a GUID will do.
var cardId = Guid.NewGuid().ToString();
card.Body.Add(new TextBlock() { Text = cardId });
card.Actions.Add(new SubmitAction()
{
Title = "Do something",
// The data we put inside this action will persist.
// I've found setting DataJson to be more reliable than using the Data property.
// Note that if your WebApiConfig.cs has a CamelCasePropertyNamesContractResolver
// (which is a default) and you use capitalized (Pascal case) identifiers,
// they may be converted to camel case and you won't be able to retrieve
// the data with the same identifiers.
DataJson = JsonConvert.SerializeObject(new
{
// We need a type to differentiate this action from other actions.
type = DO_SOMETHING,
// We need an id to differentiate this card from other cards.
id = cardId,
}),
});
return new Attachment()
{
ContentType = AdaptiveCard.ContentType,
Content = card,
};
}
Here's what it looks like in Bot Framework Emulator. Note that even after you've clicked one card and can't get the first response from that card, you can still get the first response from the other card.

Related

Azure SignalR frequently not receiving messages

We're having some issues where the Azure hosted SignalR frequently does not receive client-sent messages.
When testing locally with a self-hosted SignalR, we never have any issues.
What we want to achieve is the following:
Whenever a users opens a 'case'-page, we want to add that user to this specific case group.
In the front-end (React) we do the following:
// When the 'Case'-page gets rendered
useEffect(() => {
if (caseId && currentTeam?.id) {
dispatch(joinCaseGroup(caseId, currentTeam.id));
}
return () => {
const cleanup = () => {
dispatch(leaveCaseGroup(caseId));
};
cleanup();
};
}, [caseId, currentTeam.id, dispatch]);
// Function triggered by `joinCaseGroup`
connection.invoke('JoinCaseGroup', caseId, teamId))
This gets handled in our Hub:
public class EventsClientHub : Hub
{
/// <summary>
/// Contains all the users that are in a case group
/// </summary>
private static readonly Dictionary<Guid, List<string>> UserCaseGroups = new();
public async Task JoinCaseGroup(string caseId, string team)
{
// Here we check if the team has access to the case
if (Guid.TryParse(caseId, out var parsedCaseId) && Guid.TryParse(team, out var teamId) && await _caseQueryHandler.TeamHasAccessToCase(teamId, parsedCaseId))
{
// Remove from previous case groups, because a user can only be in 1 case group
await RemoveUserFromCaseGroups();
await AddUserToCaseGroup(parsedCaseId);
}
}
private async Task AddUserToCaseGroup(Guid caseId)
{
// Keep track of all users in a case
string userId = Context.User!.FindFirst("sub")!.Value;
if (!UserCaseGroups.ContainsKey(caseId))
{
UserCaseGroups.TryAdd(caseId, new List<string>());
}
UserCaseGroups[caseId].Add(userId);
// Add the user to the case group
await Groups.AddToGroupAsync(Context.ConnectionId, $"case_{caseId}");
}
}
Now, when debugging this, we see that the client does send the request and gets a response:
But when we check the live trace logs on Azure, we only see that the user gets added to his team group, but not to the case group:
The weird thing is, when we refresh a couple of times, the user sometimes does get added to the group and can receive live updates.
Any idea what the issue could be here?
Thanks in advance!

How do I check if a user exists in Dsharplus (C# discord bot)

Basically I want if someone types "whois (mention user)" the bot to send a message like "(user mentioned in first message) is (random message from array)"
I just need to know 2 things
How do I only send a message if it's an actual mentioned user
and How do I set a variable something only if a certain user (my disc account) is mention
[Description("Mention someone after and it will tell you all about them")]
public async Task WhoIs(CommandContext ctx, [Description("Mention a User")] string name)
{ string[] descriptions = {"", "", "", "", ""};
Random rng = new Random();
int rngOk = rng.Next(descriptions.Length);
//if (name == "a certain user")
//{
// rngOk = 3;
//}
await ctx.Channel.SendMessageAsync(name + descriptions[rngOk]).ConfigureAwait(false);
}
This is my code so far
If you need any additional information, just ask
Any help would be appreciated
I don't know precisely what you are trying to ask, your question is very vague but I will do my best to answer it
First Question
For the first part of your question "How do I only send a message if it's an actual mentioned user", you need to have a DiscordUser parameter in your command like this
[Command("test")]
public async Task MentionUserExample(CommandContext ctx, DiscordUser user)
{
if (user.Id == "Put the ID of the user here")
{
//Write whatever you are trying to do here. This will only trigger if the
//user you mention, matches the ID of the one you set here
}
else
{
//Show an error message or something similar
}
}
By doing this, when a user does "!help #RandomUser", the if statement will compare the ID of the mentioned user, to the ID of the user you set in the if statement
Second Question
Your second question "How do I set a variable something only if a certain user (my disc account) is mention", I don't understand properly. What are you trying to achieve here.
If we are mentioning your account as an example, why do you want to store it in a variable, if you are using it for something else it makes sense but once again your question is very vague
Once again you need the DiscordUser parameter in your command
[Command("test")]
public async Task MentionUserExample(CommandContext ctx, DiscordUser user)
{
if (user.Id = "The ID of the user you want to store")
{
DiscordUser UserToStore = user;
}
}
From here on you can use this variable and access many different properties as you would normally like Id, Username etc

botFramework v4 how to handle dialog response after LUIS call

I have a bot written in C# that is using LUIS to determine intents. I have a method that makes a call to the LUIS service and then looks for an 'Open_Case' intent. The model has a CaseNumber entity defined which may or may not be included in the response from the LUIS service.
If the response doesn't have a case number entity I start a dialog to ask the user for the case number.
Once I have a case number I then want to return a card with case information.
Here's the code I have:-
/// <summary>
/// Dispatches the turn to the requested LUIS model.
/// </summary>
private async Task DispatchToLuisModelAsync(ITurnContext context, string appName, DialogContext dc, CancellationToken cancellationToken =
default (CancellationToken)) {
var result = await botServices.LuisServices[appName].RecognizeAsync(context, cancellationToken);
var intent = result.Intents ? .FirstOrDefault();
string caseNumber = null;
if (intent ? .Key == "Open_Case") {
if (!result.Entities.ContainsKey("Case_CaseNumber")) {
var dialogResult = await dc.BeginDialogAsync(CaseNumberDialogId, null, cancellationToken);
} else {
caseNumber = (string)((Newtonsoft.Json.Linq.JValue) result.Entities["Case_CaseNumber"].First).Value;
var cardAttachment = botServices.CaseInfoServices.LookupCase(caseNumber);
var reply = context.Activity.CreateReply();
reply.Attachments = new List < Attachment > () {
cardAttachment
};
await context.SendActivityAsync(reply, cancellationToken);
}
}
}
What I'm struggling with is where the code send the card response should sit.
In the code I currently have I send the card if the number was returned in the LUIS response, but if there was no number and I start the dialog then I only get access to the number either in the final step of the dialog or in the dialog result in the root turn handler. I've currently duplicated the reply inside the final step in the dialog, but it feels wrong and inelegant.
I'm sure there must be a way that I can collect the number from LUIS or the dialog and THEN send the response from a single place instead of duplicating code.
Any suggestions gratefully received...
I came to the conclusion that I need to put the code that displays the card into a method on the bot class, then call it from the else in code snippet and also from the turn handler when the dialogTurnStatus is equal to Complete

Branching dialogs/forms based on response in MS Bot Framework

We're experimenting with the MS Bot Framework and haven't quite worked out how to do this scenario:
We have a LUIS Dialog (type <object>), which is working correctly and is trained properly. To use the common sandwich example, the basics of what LUIS intent is looking for is the user asking for the status of an order. If the order number was provided in the question ("What is the status of order 1234?"), then the LUIS dialog does the lookup and reports the status directly (which is all currently working).
However, if the user just triggers the intent without providing the order number ("I'd like to look up the status of an order."), I'd like to launch another dialog/form to ask the user if they'd like to look up the order by address or order number, and then do the appropriate DB lookup based on how they answer.
I'm just not sure how to configure the Form/Dialog (or even which is best in this case) to do a different lookup based on if they choose address or number lookup.
Here's the intent so far:
private readonly BuildFormDelegate<OrderStatusDialog> OrderStatusDelegate;
[LuisIntent(nameof(LuisIntents.OrderStatus))]
public async Task OrderStatus(IDialogContext context, LuisResult result)
{
// Order number(s) were provided
if (result.Entities.Any(Entity => Entity.Type == nameof(LuisEntityTypes.OrderNumber)))
{
// Loop in case they asked about multiple orders
foreach (var entity in result.Entities.Where(Entity => Entity.Type == nameof(LuisEntityTypes.OrderNumber)))
{
var orderNum = entity.Entity;
// Call webservice to check status
var request = new RestRequest(Properties.Settings.Default.GetOrderByNum, Method.GET);
request.AddUrlSegment("num", orderNum);
var response = await RestHelper.SendRestRequestAsync(request);
var parsedResponse = JObject.Parse(response);
if ((bool)parsedResponse["errored"])
{
await context.PostAsync((string)parsedResponse["errMsg"]);
continue;
}
// Grab status from returned JSON
var status = parsedResponse["orderStatus"].ToString();
await context.PostAsync($"The status of order {orderNum} is {status}");
}
context.Wait(MessageReceived);
}
// Order number was not provided
else
{
var orderStatusForm = new FormDialog<OrderStatusDialog>(new OrderStatusDialog(), OrderStatusDelegate,
FormOptions.PromptInStart);
context.Call<OrderStatusDialog>(orderStatusForm, CallBack);
}
}
private async Task CallBack(IDialogContext context, IAwaitable<object> result)
{
context.Wait(MessageReceived);
}
And the form:
public enum OrderStatusLookupOptions
{
Address,
OrderNumber
}
[Serializable]
public class OrderStatusDialog
{
public OrderStatusLookupOptions? LookupOption;
public static IForm<OrderStatusDialog> BuildForm()
{
return new FormBuilder<OrderStatusDialog>()
.Message("In order to look up the status of a order, we will first need either the order number or your delivery address.")
.Build();
}
}
The FormFlow route is a valid option. What is missing in your form flow is asking for the address/order number after the lookup option is selected.
What you can do in that case is adding two more fields to the OrderStatusDialog class: OrderNumber and DeliveryAddress.
Then you need to use the selected OrderStatusLookupOptions to activate/deactivate the next field.
The code, from the top of my head, would be something like:
[Serializable]
public class OrderStatusDialog
{
public OrderStatusLookupOptions? LookupOption;
public int OrderNumber;
public string DeliveryAddress
public static IForm<OrderStatusDialog> BuildForm()
{
return new FormBuilder<OrderStatusDialog>()
.Message("In order to look up the status of a order, we will first need either the order number or your delivery address.")
.Field(nameof(OrderStatusDialog.LookupOption))
.Field(new FieldReflector<OrderStatusDialog>(nameof(OrderStatusDialog.OrderNumber))
.SetActive(state => state.LookupOption == OrderStatusLookupOptions.OrderNumber))
.Field(new FieldReflector<OrderStatusDialog>(nameof(OrderStatusDialog.DeliveryAddress))
.SetActive(state => state.LookupOption == OrderStatusLookupOptions.Address))
.Build();
}
}
Then on your Callback method you will receive the form filled and you can do the DB lookup.
Alternatively, you can just use PromptDialogs and guide the user through the same experience. Take a look to the MultiDialogs sample to see the different alternatives.
I added a working sample on this here.

Android Activity destroyed when calling ZXing barcode scanner. How to recover?

First of all, I'm using, Xamarin with MvvmCross.
In my ViewModel, I'm using the ZXing MobileBarcodeScanner class to scan a barcode when the user clicks a button:
var scanner = new MobileBarcodeScanner();
var result = await scanner.Scan();
if (result != null)
{
CodigoProduto = result.Text;
InternalPesquisarProduto();
}
After the scan, I run the InternalPesquisarProduto void, that search for data on a remote server, based of course, on the barcode that was read. This method, also display some loading message while the data is fetched:
Ui.DisplayLoading("Searching...", "Searching data");
// Code that fetches the data
Ui.DismissLoading();
The Ui is a property on my ViewModel defined like this:
protected IUiInteractor Ui { get; set; }
I receive it by dependency injection. Here is the relevant code from the implementation being used in this scenario:
public class AndroidUiInteractor : IUiInteractor
{
private IMvxAndroidCurrentTopActivity _mvxCurrentTopActivity;
public AndroidUiInteractor(IMvxAndroidCurrentTopActivity mvxCurrentTopActivity)
{
_mvxCurrentTopActivity = mvxCurrentTopActivity;
}
public void DisplayLoading(string title, string message)
{
_mvxCurrentTopActivity.Activity.RunOnUiThread(() =>
{
_progressDlg = new ProgressDialog(_mvxCurrentTopActivity.Activity);
// Configuring the title and the message
_progressDlg.Show();
});
}
}
The problem is that when the scanner.Scan is called, my caller activity is destroyed, so when I call the Ui.DisplayLoading, the _mvxCurrentTopActivity.Activity is null.
What is most weird about this case, is that I have two Samsungs with Android 5.0 API 21 that I use in my tests, and this problem only happens in one of them, on the other, the activity is not destroyed when calling the scanner.Scan.
Note: I'm sorry for anything wrong in the code, but because of company policies, I can only access the internet by Terminal Service, and the Ctrl + V is disabled on it.
It turns out the problem was in the device. After reseting it's configurations it worked properly.
This might not be a definitive solution for everyone that faces that problem, but in my scenario it could be done.

Categories

Resources