Recording replies in discord PMs - c#

I am currently working on making an application bot in discord, and made a quick test to try to find a way to detect a reply in a PM channel between a user and my bot. After fiddling around with varying google searches and such, my best attempt was this:
[Command("apply")]
public async Task ApplyAsync()
{
var user = Context.User as IGuildUser;
await user.SendMessageAsync("Test");
if (Context.Message.Content != null)
{
await user.SendMessageAsync($"You replied {Context.Message.Content}");
}
}
Still being somewhat new to c#, I figured this would take the message content of the users reply to the private message, but it just throws in ".apply", and I have't been able to figure out how to get the bot to actually detect a reply in a PM.

If you want to use the command in DMs, you have to change the line var user = Context.User as IGuildUser; to var user = Context.User as IUser; This is what caused the exception.

Related

The command to get avatar is not working correctly (Problem with SocketGuildUser maybe)

A very simple command to get the user's avatar. However, it works unstable and incorrectly (example in the screenshot). Bot does not respond to all users and not every time. What can this be related to? Maybe I don't understand something, but in my opinion, there is a problem with SocketGuildUser. Here is a simplified code snippet for clarity:
[Command("avatar")]
[Alias("getavatar")]
public async Task GetAvatar(ushort res, SocketGuildUser user = null)
{
if (user == null)
{
await Context.Channel.SendMessageAsync(Context.User.GetAvatarUrl(size: res));
}
else
{
await Context.Channel.SendMessageAsync(user.GetAvatarUrl(size: res));
}
}
Simply enable intents in the discord developer portal. Without it, users will not be cached and your user type reader will fail to parse any mentioned users
Edit: An alternate method would be to create your own custom type reader that will fallback to making a rest request if fetching the user from cache yields no results.

Dialog automatically continue when user closes or press a button in the webview on Botframework V4

I have a web app deployed in azure that opens in Messenger Webview. I made a empty text prompt for user to click for the user to be able to continue when they close the Webview. However, users sometimes forget to click the button. I read this doc but i can't manage to do it as i learn best seeing actual examples and codes. How can the dialog automatically continue when the user closes or press a button in the Webview? Thank you.
The web view is a set of questions and the answer of the users are save in Cosmos DB and when they close the web view the bot access their scores in Cosmos DB and calculate their scores.
This is my current code.
private static async Task<DialogTurnResult> ThirdStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken = default(CancellationToken))
{
Activity reply = stepContext.Context.Activity.CreateReply();
reply.ChannelData = JObject.FromObject(
new
{
attachment = new
{
type = "template",
payload = new
{
template_type = "generic",
elements = new[]
{
new
{
title = "<title>",
buttons = new object[]
{
new
{
type = "web_url",
title = "<title>",
url = webAppUrl,
messenger_extensions="true",
webview_height_ratio = "tall",
},
new
{
type = "postback",
title = "Done ✔️",
payload = "Done ✔️",
},
},
},
},
},
},
});
await stepContext.Context.SendActivityAsync(reply);
return await stepContext.PromptAsync(
nameof(TextPrompt),
new PromptOptions
{
Prompt = MessageFactory.Text(string.Empty),
RetryPrompt = MessageFactory.Text("Click Done to proceed."),
});
}
private static async Task<DialogTurnResult> FourthStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var result = stepContext.Result.ToString().ToLower();
if (result == "done ✔️" || result == "done")
{
return await stepContext.NextAsync();
}
await stepContext.Context.SendActivityAsync(
MessageFactory.Text(
$"Please press done to proceed, thank you!"));
return await stepContext.ReplaceDialogAsync(nameof(CalendarIncomeExpensesDialogV2));
}
update rafa:
Since you don't even know what kind of web app you're trying to create or what language you're writing the web app in or how you plan on hosting the web app, your first step is to figure all that out. If you need help with any part of that process then you'll need to ask a new very specific question about it and include what you've tried so far along with all the relevant code.
When you open the web app in your Facebook webview, you'll need to make sure the web app has all the information it needs to send an activity to the bot (I've been calling it a proactive message but those normally refer to bot-to-user messages). The needed credentials should already be built into the web app, so the only thing you'll need to send to the web app's endpoint is the user ID so that the web app can pass it along in the activity and the bot can identify which conversation the activity pertains to. The conventional way to send an activity to the bot is using Direct Line, though you may figure out a way to do it by just sending an HTTP request to the bot's endpoint or even using your Facebook app's callback URL.
There is absolutely no need to involve LUIS in this. LUIS should only be used to interpret messages from the user when you don't know what the user will say. Any time you're in control of the message that's getting sent to the bot, it doesn't make any sense to use LUIS. Keep in mind that there are 15+ activity types and your activity doesn't need to be a "message" activity. There are many ways you can identify the activity and respond accordingly in your bot. I recommend using an event activity.
Based on my understanding of your proficiency level, you will likely need to do a lot of research to accomplish what you're trying to accomplish. The documentation is a great place to start: https://learn.microsoft.com/en-us/azure/bot-service/

Sending a Proactive Message as a result of a dialog prompt choice

I'm pretty new to Microsoft Bot Framework. I have a bot that receives messages from one user and can be viewed by another. I want to add a feature where the viewer can then reply to the sender. I figured the best way to do this is to send a proactive message to the original sender. However, I'm having trouble understanding the documentation Microsoft provides and other sources are pretty dated.
Right now this is what I have.
MessageDetails.RelatesTo contains the ConversationReference:
private async Task<DialogTurnResult> FinalStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var messageDetails = (MessageDetails)stepContext.Options;
var replyText = (string)stepContext.Result;
var messageactivity = messageDetails.RelatesTo.GetContinuationActivity();
await stepContext.Context.SendActivityAsync($"{messageDetails.RelatesTo}");
var client = new ConnectorClient(new Uri(messageactivity.ServiceUrl));
if (messageDetails.IsTrustedServiceUrl)
{
MicrosoftAppCredentials.TrustServiceUrl(messageactivity.ServiceUrl);
}
var triggerReply = messageactivity.CreateReply();
triggerReply.Text = $"NoReply from {stepContext.Context.Activity.Name}: {replyText}";
await client.Conversations.ReplyToActivityAsync(triggerReply);
await stepContext.Context.SendActivityAsync($"Your reply has been sent to {messageDetails.RelatesTo.User.Name}.");
return await stepContext.EndDialogAsync(messageDetails, cancellationToken);
}
This code doesn't work and I'm not entirely sure why. (I would also appreciate any advice on how I might troubleshoot the problem). I realize that this doesn't make use of the controller shown in the sample code provided by Microsoft. Honestly, I don't understand how the notify controller works. So if a solution involves that, it would be great to get an explanation on some of the details.
Fixing Your Problem
My guess is that you're getting this error:
System.ArgumentNullException: 'Value cannot be null. Parameter name: clientId'
This is because you're not specifying the appId in new ConnectorClient. You can disregard that, as you're better off doing something more like:
await stepContext.Context.Adapter.ContinueConversationAsync("<yourAppId>", messageDetails.RelatesTo, async (ITurnContext turnContext, CancellationToken cancel) =>
{
await turnContext.SendActivityAsync(triggerReply);
}, cancellationToken);
The key is making sure you have a ConversationReference for the user that you need to send the proactive message to. If you don't have one, you should be able to CreateConversation to establish one
Note that you also have:
if (messageDetails.IsTrustedServiceUrl)
{
MicrosoftAppCredentials.TrustServiceUrl(messageactivity.ServiceUrl);
}
This is basically saying, "if we already trust the serviceUrl, trust it again". Instead, you need: if (!messageDetails.IsTrustedServiceUrl)
How Proactive Bot Works
Here's a brief explanation of how the proactive sample works. I recommend downloading it and playing around with it to get a better understanding.
The bot establishes listeners on api/messages (all bots do this) and api/notify (only this bot does this).
When a user messages the bot, it goes through api/messages and is processed through the ActivityHandler.
On any message, the bot saves the conversation reference
When somebody visits <theBotUrl>/api/notify, it loops through each saved conversation reference, calls ContinueConversation, and sends the message to all of the users in the saved conversation references

Resume Bot Framework dialog when triggered by external service

The Scenario
I have a bot built using the Bot Framework with a series of dialogs. One of these dialogs gives the user the option of inputting some complex data via a web page by presenting a button to them. Clicking the button they are then taken to the site, fill out the data, save and are then directed back to the bot.
I want my bot to pause the dialog until it receives an event from my web page telling me the user has saved the data and then continue asking the user questions.
Before
I had a version implemented whereby I would store a ConversationReference before the user clicked the button and then when the external event happened I would send the cards and next messages I wanted to show (not in a dialog) from a webhook, that was fine but it got quite complicated/messy - I'd rather keep the whole app in one continuous dialog.
Idea 1: Use DirectLine API
I did some research and many people were suggesting using the DirectLine API. So I implemented this:
public async Task SendEventAsync(InternalEventMessage message, ConversationReference reference) {
var client = new DirectLineClient(!String.IsNullOrEmpty(_settings.DirectLineSecret) ? _settings.DirectLineSecret : null);
if (_settings.SiteUrl.Contains("localhost")) {
client.BaseUri = new Uri(_settings.DirectLineServiceUrl);
}
var eventMessage = Activity.CreateEventActivity();
//Wrong way round?!?
eventMessage.From = reference.Bot;
eventMessage.Type = ActivityTypes.Event;
eventMessage.Value = message;
var conversation = await client.Conversations.PostActivityAsync(reference.Conversation.Id, eventMessage as Activity);
}
This uses the DirectLine client to send an event message to the serviceUrl using a stored ConversationReference, basically imitating a user (bot and user seem to be the wrong way round in the SDK). Checking for localhost was so that the DirectLine library pointed at the emulator server rather than https://directline.botframework.com.
In my dialog I call:
//method above shows input button and links to web page
context.Wait(WaitForAddressInput);
}
private async Task WaitForAddressInput(IDialogContext context, IAwaitable<IActivity> result) {
var message = await result;
switch (message.Type) {
case ActivityTypes.Message:
//TODO: Add response
break;
case ActivityTypes.Event:
var eventMessage = message as IEventActivity;
if (((JObject)eventMessage.Value).ToObject<InternalEventMessage>().Type == EventType.AddressInputComplete) {
_addressResult = (await _tableService.ReadOrderById(Order.OrderId)).Address;
await context.PostAsync($"Great}");
context.Done(_addressResult);
}
break;
}
}
This waits for any message from the user after the button has been shown and if our event matches then we proceed with the dialog.
This works locally using the emulator but, frustratingly, doesn't live. It fails to recognise channels created via webchat or Messenger. That is explained here: Microsoft Bot Framework DirectLine Can't Access Conversations
For security reasons, you can't use DirectLine to spy on messages from
another conversation.
So I can't access a channel that I haven't created using DirectLine.
Idea 2: BotConnector
So I thought I'd try the BotConnector using similar code:
public async Task SendEventAsync(InternalEventMessage message, Microsoft.Bot.Connector.DirectLine.ConversationReference reference) {
var botAccount = new ChannelAccount(reference.User.Id, reference.User.Name);
var userAccount = new ChannelAccount(reference.Bot.Id, reference.Bot.Name);
MicrosoftAppCredentials.TrustServiceUrl(reference.ServiceUrl);
var connector = new ConnectorClient(new Uri(reference.ServiceUrl), new MicrosoftAppCredentials("xxxxxxxxxxxxxxxxxxxxxxxx", "xxxxxxxxxxxxxxxxxxxxxxxx"));
connector.Credentials.InitializeServiceClient();
var eventMessage = Activity.CreateMessageActivity();
eventMessage.Recipient = botAccount;
eventMessage.From = userAccount;
eventMessage.Type = ActivityTypes.Event;
eventMessage.Conversation = new ConversationAccount(id: reference.Conversation.Id);
eventMessage.ServiceUrl = reference.ServiceUrl;
eventMessage.Timestamp = DateTimeOffset.UtcNow;
eventMessage.LocalTimestamp = DateTime.Now;
eventMessage.ChannelId = reference.ChannelId;
var result = await connector.Conversations.SendToConversationAsync(eventMessage as Microsoft.Bot.Connector.Activity);
}
This doesn't crash and I can see the event appear in the emulator request console but nothing happens, it seems to be ignored!
Idea 3: Try to imitate the bot service calling my bot
I haven't tried this yet because I think it might be the most time consuming but I was reading here about the service authentication and wondered if it would be possible to imitate the hosted bot service sending a message and send my event that way with the required data?
This seems like a fairly common scenario so I'm surprised I haven't come across a way to do this yet. If anyone has any other ideas on how I can send an event message to my bot from an external service then I'd love to hear it.
Update:
See my answer below Eric's to see what I did.
Idea 1:
DirectLine is a channel, not a library to use in order to connect to channels. (For instance: you would not use Facebook Messenger to connect to Skype) DirectLineClient is useful for creating a client application that connects to the DirectLine channel through the Direct Line connector service.
Idea 2:
This method should work. In fact, the BotAuth library uses this method for the MagicNumber login flow within the CallbackController: https://github.com/MicrosoftDX/botauth/blob/9a0a9f1b665f4aa95b6d60d09346dda90d8b314e/CSharp/BotAuth/Controllers/CallbackController.cs
For your scenario, you should be able to construct a CardAction of type ActionTypes.OpenUrl that contains a value with the ConversationReference encoded in the url. Clicking the button will call an mvc controller that displays a page (saving the ConversationReference in a cookie or something) and when the user finishes adding the address on the page, use the ConversationReference to send an event to the bot (similar to how BotAuth resumes the conversation in the CallbackController).
Idea 3:
This would bypass the connector services, and is not a supported scenario. The link you shared explains the details of how authentication works in the Bot Framework, not how to bypass the connector services.
Eric's answer led me to solve the issue using the BotAuth example but, for completeness, here is what I did using Idea 2.
I created a CallbackController on my Bot Framework endpoint and then used the following code to send an event back to the awaiting dialog:
MicrosoftAppCredentials.TrustServiceUrl(reference.ServiceUrl);
var message = reference.GetPostToBotMessage();
message.Value = new InternalEventMessage(type);
message.Type = ActivityTypes.Event;
await Conversation.ResumeAsync(reference, message);
The dialog awaits with this code and continues:
context.Wait(WaitForAddressInput);
}
private async Task WaitForAddressInput(IDialogContext context,
IAwaitable<IActivity> result)
{
var message = await result;
switch (message.Type)
{
case ActivityTypes.Message:
//TODO: Add response
break;
case ActivityTypes.Event:
//Process event and continue!
break;
}
}
This is the most complicated issue I've had with the Bot Framework and I found the docs a little lacking. Hope this helps someone!

Teams bot Activity.CreateReply throwing NullReferenceException

I'm working on a bot for Microsoft Teams. I am using the custom bot feature. I got the bot working as a sideloaded package, but due to the constraints of my network, I need to keep the bot internal and use the custom bot feature. I am currently testing it by using ngrok to tunnel to my localhost.
I am now running into an issue when I try to create my reply. Whenever I call this:
var reply = activity.CreateReply(message.ReadToEnd());
I get a NullReferenceException saying that the "Object reference not set to an instance of an object". message is an open .txt file. I get this error every time I call activity.CreateReply(). The part that I don't understand is that everything works as intended in the Bot Framework Emulator and when the bot is a sideloaded package, but not when the bot is a custom bot.
Here's my full Post method:
public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
var connector = new ConnectorClient(new Uri(activity.ServiceUrl));
if (activity.Type == ActivityTypes.Message)
{
// Commands:
// Retrieve TFS Work Item(s)
if (new Regex(#"\but\s?\d{5}\b").IsMatch(activity.Text.ToLower()))
{
var reply = new RetrieveWorkItem();
await connector.Conversations.ReplyToActivityAsync(reply.Response(activity));
}
// Help
else if (activity.Text.ToLower().Contains("help"))
{
var message = File.OpenText($"{System.AppDomain.CurrentDomain.BaseDirectory}/Messages/HelpMessage.txt");
var reply = activity.CreateReply(message.ReadToEnd());
await connector.Conversations.ReplyToActivityAsync(reply);
}
// Not Recognized
else
{
var reply = activity.CreateReply("Command not recognized. Type \"#Keller Bot Help\" for a list of commands.");
await connector.Conversations.ReplyToActivityAsync(reply);
}
}
else
{
HandleSystemMessage(activity, connector);
}
var response = Request.CreateResponse(HttpStatusCode.OK);
return response;
}
Here's the full error and StackTrace sent by the bot: https://pastebin.com/ZSrjrA9z
You say you're implementing this as a custom bot, per the instructions here. The issue is that it appears as if you're using the Bot Framework messaging calls (e.g. CreateReply()), which won't work since you're not dealing with a registered BF bot when you go through the custom bot process.
Instead, you can just create a new Activity() and return that in response to the HttpPost request.
We do have a sample you can check out, in case that helps.

Categories