Teams bot Activity.CreateReply throwing NullReferenceException - c#

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.

Related

How to migrate 1:1 proactive messages functionality in Teams with Bot Framework V4 C# SDK

I have a Bot Framework V3 SDK implementation (working fine) that can initiate a 1:1 private chat with any user in a specific Team using a Microsoft Teams Bot (installed on the same team).
I am having problems trying to migrate it to the V4 SDK.
I read on various articles and other questions that it's not possible to do it unless the user contacts the the bot first (to avoid spamming to users), but this is not true with the V3 version and not an option for the functionality that I need.
The original solution uses the "CreateOrGetDirectConversation" extension method from the Microsoft.Bot.Connector.Teams assembly, but it's not available in the V4 version of the assembly.
I tried using the CreateDirectConversation/CreateDirectConversationAsync methods but with no success (they always result in "Bad Request" errors).
This is the code actually working using the V3 libraries:
// Create 1:1 conversation
var conversation = connectorClient.Conversations.CreateOrGetDirectConversation(botAccount, user, tenantId);
// Send message to the user
var message = Activity.CreateMessageActivity();
message.Type = ActivityTypes.Message;
message.Text = "My message";
connectorClient.Conversations.SendToConversation((Activity)message, conversation.Id);
And I am finding it very hard to migrate. Am I missing something?
Per the docs, Proactive messaging for bots:
Bots can create new conversations with an individual Microsoft Teams user as long as your bot has user information obtained through previous addition in a personal, groupChat or team scope. This information enables your bot to proactively notify them. For instance, if your bot was added to a team, it could query the team roster and send users individual messages in personal chats, or a user could #mention another user to trigger the bot to send that user a direct message.
Note: The Microsoft.Bot.Builder.Teams extension is still in Prerelease for V4, which is why samples and code are kind of hard to find for it.
Adding the Middleware
In Startup.cs:
var credentials = new SimpleCredentialProvider(Configuration["MicrosoftAppId"], Configuration["MicrosoftAppPassword"]);
services.AddSingleton(credentials);
[...]
services.AddBot<YourBot>(options =>
{
options.CredentialProvider = credentials;
options.Middleware.Add(
new TeamsMiddleware(
new ConfigurationCredentialProvider(this.Configuration)));
[...]
Prepping Your Bot
In your main <YourBot>.cs:
private readonly SimpleCredentialProvider _credentialProvider;
[...]
public <YourBot>(ConversationState conversationState, SimpleCredentialProvider CredentialProvider)
{
_credentialProvider = CredentialProvider;
[...]
Sending the Message
var teamConversationData = turnContext.Activity.GetChannelData<TeamsChannelData>();
var connectorClient = new ConnectorClient(new Uri(activity.ServiceUrl), _credentialProvider.AppId, _credentialProvider.Password);
var userId = <UserIdToSendTo>;
var tenantId = teamConversationData.Tenant.Id;
var parameters = new ConversationParameters
{
Members = new[] { new ChannelAccount(userId) },
ChannelData = new TeamsChannelData
{
Tenant = new TenantInfo(tenantId),
},
};
var conversationResource = await connectorClient.Conversations.CreateConversationAsync(parameters);
var message = Activity.CreateMessageActivity();
message.Text = "This is a proactive message.";
await connectorClient.Conversations.SendToConversationAsync(conversationResource.Id, (Activity)message);
Note: If you need to get user ID's, you can use:
var members = (await turnContext.TurnState.Get<IConnectorClient>().Conversations.GetConversationMembersAsync(
turnContext.Activity.GetChannelData<TeamsChannelData>().Team.Id).ConfigureAwait(false)).ToList();
Also, I didn't need this in my testing, but if you get 401 errors, you may need to trust the Teams ServiceUrl:
MicrosoftAppCredentials.TrustServiceUrl(turnContext.Activity.ServiceUrl);
Resources
Teams Extension on MyGet
Teams Extension MyGet Package Repo
Samples using the extension
Proactive Teams Sample
Useful, unofficial blog post

Recording replies in discord PMs

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.

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!

Microsoft teams bot - could not parse tenant id

I'm working on a bot for MS Teams, and running into an issue. When trying to initiate a conversation from the bot, I get this error:
Microsoft.Rest.HttpOperationException: Could not parse tenant id
I haven't been able to find anywhere in the docs that mentions a required Tenant ID, and I never set one up in the application. How can I specify this, or is the root cause something else?
Below is the code I am using that returns the error (strings obfuscated).
private ConversationResourceResponse GetConversation(IActivity activity)
{
var connector = new ConnectorClient(new Uri(activity.ServiceUrl));
var userAccount = new ChannelAccount("user#domain.com");
var botAccount = new ChannelAccount("#botHandle", "botName");
var conversationId = connector.Conversations.CreateDirectConversation(botAccount, userAccount);
return conversationId;
}
Thank you!
There is a special behaviour in MS Teams when you want to create a conversation, so you have to use a specific method provided by MS Teams NuGet package:
// Create or get existing chat conversation with user
var response = client.Conversations.CreateOrGetDirectConversation(activity.Recipient, activity.From, activity.GetTenantId());
You can see that the method has the tenantId in parameter.
The NuGet package is called Microsoft.Bot.Connector.Teams and is available here.
More details on the MS Teams documentation (it's not detailed on Bot framework side):
https://learn.microsoft.com/en-us/microsoftteams/platform/scenarios/bots-personal-conversations#starting-a-11-conversation

Bot framework get the ServiceUrl of embedded chat control page

I want to embed the chat control to many websites and I want to get the Url of the website that I've embedded in order to my bot can get the Data matching with the Website URL. However, when I embed the iframe generated from WebChat, I always get the same ServiceUrl and that's https://webchat.botframework.com/, it isn't the Url of the website, so how can I embed the chat control to any website and my bot can get the website Url not the Url of the WebChat or DirectLine.
Here's what I've tried:Direct-Line chat control
Here's the result I've tested with my published bot:
I've noticed that, when I've tested my bot with the Bot Framework Emulator, it always return the exact Url of the sender (in case of local testingm, it will return http://localhost:XXXX/). So how can we do like this?
I think a way to achieve this would be by using BackChannel, which adds the ability for a bot to communicate with a page that embeds the bot through WebChat. It will a allow you to:
Send events to a page that hosts an instance of a WebChat
Listen for events from the page that hosts an instance of a WebChat
The first piece is, of course, the HTML page, which will contain what you put together, plus the logic to send/listen to events. The sample page with the basic logic can be found here and below is the image with the events related code.
Now, you need to prepare your bot to listen and send events. There is a sample in Node.js, that shows how to do that.
Porting that in C#, is as easy as listen and sending to activities of type Event. A sample code for that (using the events of the HTML page mentioned before):
public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
if (activity.Type == ActivityTypes.Event &&
string.Equals(activity.Name, "buttonClicked", StringComparison.InvariantCultureIgnoreCase))
{
ConnectorClient connector = new ConnectorClient(new Uri(activity.ServiceUrl));
// return our reply to the user
Activity reply = activity.CreateReply("I see that you just pushed that button");
await connector.Conversations.ReplyToActivityAsync(reply);
}
if (activity.Type == ActivityTypes.Message)
{
ConnectorClient connector = new ConnectorClient(new Uri(activity.ServiceUrl));
// return our reply to the user
var reply = activity.CreateReply();
reply.Type = ActivityTypes.Event;
reply.Name = "changeBackground";
reply.Value = activity.Text;
await connector.Conversations.ReplyToActivityAsync(reply);
}
else
{
HandleSystemMessage(activity);
}
var response = Request.CreateResponse(HttpStatusCode.OK);
return response;
}
Bottom line, in your HTML page you will have to send an event to the bot, with the page URL and the bot will have to listen to that event to get the value

Categories