I am exploring the ASPBoilerplate framework and what interests me more is its real time notification capability. However, I am having this error.
Here is my Notification publisher class:
public class Publisher : Hub, ITransientDependency
{
private readonly INotificationPublisher _notificationPublisher;
public Publisher(INotificationPublisher notificationPublisher)
{
_notificationPublisher = notificationPublisher;
}
//Send a general notification to all subscribed users in current tenant (tenant in the session)
public async Task Publish_Announcement(string announcementMessage)
{
//Example "LowDiskWarningMessage" content for English -> "Attention! Only {remainingDiskInMb} MBs left on the disk!"
var data = new MessageNotificationData(announcementMessage);
await _notificationPublisher.PublishAsync("abp.notifications.received", data, severity: NotificationSeverity.Info);
}
}
And I am testing if it will notify all online users whenever a new user was created.
public override async Task<UserDto> Create(CreateUserDto input)
{
CheckCreatePermission();
var user = ObjectMapper.Map<User>(input);
user.TenantId = AbpSession.TenantId;
user.IsEmailConfirmed = true;
await _userManager.InitializeOptionsAsync(AbpSession.TenantId);
CheckErrors(await _userManager.CreateAsync(user, input.Password));
if (input.RoleNames != null)
{
CheckErrors(await _userManager.SetRoles(user, input.RoleNames));
}
CurrentUnitOfWork.SaveChanges();
//I cannot call the publisher class since it has dependencies in its contructor.
new Publisher().Publish_Announcement("Hi");
return MapToEntityDto(user);
}
Or am I just doing it wrong?
Please help. Thanks!
Basically, the Notifications is based on Publisher / Subsriber pattern and while the notifications are published through the system, the users who have subscribed to that notification will receive it.
Publish Notifications
public class MyService : ITransientDependency
{
private readonly INotificationPublisher _notificationPublisher;
public MyService(INotificationPublisher notificationPublisher)
{
_notificationPublisher = notificationPublisher;
}
//Send a general notification to a specific user
public async Task Publish_SentFriendshipRequest(string senderUserName, string friendshipMessage, UserIdentifier targetUserId)
{
await _notificationPublisher.PublishAsync("SentFriendshipRequest", new SentFriendshipRequestNotificationData(senderUserName, friendshipMessage), userIds: new[] { targetUserId });
}
//Send an entity notification to a specific user
public async Task Publish_CommentPhoto(string commenterUserName, string comment, Guid photoId, UserIdentifier photoOwnerUserId)
{
await _notificationPublisher.PublishAsync("CommentPhoto", new CommentPhotoNotificationData(commenterUserName, comment), new EntityIdentifier(typeof(Photo), photoId), userIds: new[] { photoOwnerUserId });
}
//Send a general notification to all subscribed users in current tenant (tenant in the session)
public async Task Publish_LowDisk(int remainingDiskInMb)
{
//Example "LowDiskWarningMessage" content for English -> "Attention! Only {remainingDiskInMb} MBs left on the disk!"
var data = new LocalizableMessageNotificationData(new LocalizableString("LowDiskWarningMessage", "MyLocalizationSourceName"));
data["remainingDiskInMb"] = remainingDiskInMb;
await _notificationPublisher.PublishAsync("System.LowDisk", data, severity: NotificationSeverity.Warn);
}
}
Subscribe to Notifications
public class MyService : ITransientDependency
{
private readonly INotificationSubscriptionManager _notificationSubscriptionManager;
public MyService(INotificationSubscriptionManager notificationSubscriptionManager)
{
_notificationSubscriptionManager = notificationSubscriptionManager;
}
//Subscribe to a general notification
public async Task Subscribe_SentFriendshipRequest(int? tenantId, long userId)
{
await _notificationSubscriptionManager.SubscribeAsync(new UserIdentifier(tenantId, userId), "SentFriendshipRequest");
}
//Subscribe to an entity notification
public async Task Subscribe_CommentPhoto(int? tenantId, long userId, Guid photoId)
{
await _notificationSubscriptionManager.SubscribeAsync(new UserIdentifier(tenantId, userId), "CommentPhoto", new EntityIdentifier(typeof(Photo), photoId));
}
}
Client-Side
This is done by signalr and you need to capture the event once it's triggered.
abp.event.on('abp.notifications.received', function (userNotification) {
if (userNotification.notification.data.type === 'Abp.Notifications.LocalizableMessageNotificationData') {
var localizedText = abp.localization.localize(
userNotification.notification.data.message.name,
userNotification.notification.data.message.sourceName
);
$.each(userNotification.notification.data.properties, function (key, value) {
localizedText = localizedText.replace('{' + key + '}', value);
});
alert('New localized notification: ' + localizedText);
} else if (userNotification.notification.data.type === 'Abp.Notifications.MessageNotificationData') {
alert('New simple notification: ' + userNotification.notification.data.message);
}
});
You can find more information here
Related
I'm using TranscriptLoggerMiddleware to log transcript to Azure blobs.
Now, I want to add additional information to the activity, for example, account ID.
Ideally I want the account ID to be the top level folder when creating the blobs so one can easily locate all conversations for a given account.
The logger is only passed the activity without any context. So I'm looking at the Entities activity property which I can potentially use for storing my account ID.
Is this a valid approach?
Any other ideas on how to implement this?
Answering my own question.
This worked for me:
public class SetEntityMiddleware : IMiddleware
{
private readonly BotState _userState;
private readonly IStatePropertyAccessor<UserProfileState> _userProfileState;
public SetEntityMiddleware(UserState userState)
{
_userState = userState;
_userProfileState = userState.CreateProperty<UserProfileState>(nameof(UserProfileState));
}
public async Task OnTurnAsync(ITurnContext turnContext, NextDelegate next, CancellationToken cancellationToken = default)
{
var userProfile = await _userProfileState.GetAsync(turnContext, () => new UserProfileState(), cancellationToken);
this.SetEntity(turnContext.Activity, userProfile);
turnContext.OnSendActivities(async (ctx, activities, nextSend) =>
{
var userProfile = await _userProfileState.GetAsync(ctx, () => new UserProfileState(), cancellationToken);
foreach (var activity in activities)
{
this.SetEntity(activity, userProfile);
}
return await nextSend().ConfigureAwait(false);
});
await next(cancellationToken).ConfigureAwait(false);
}
private void SetEntity(Activity activity, UserProfileState userProfile)
{
if (activity.Type == ActivityTypes.Message &&
!string.IsNullOrEmpty(userProfile.AccountNumber))
{
var entity = new Entity();
entity.Type = "userProfile";
entity.Properties["accountNumber"] = userProfile.AccountNumber;
if (activity.Entities == null)
{
activity.Entities = new List<Entity>();
}
activity.Entities.Add(entity);
}
}
}
I have a scorable dialog that will check if the incoming message is equal to "resetconversation". When true, the dialog resets the dialog stack in the PostAsync method.
When the postasync method is hit, the dialog stack resets as expected but when I again send a message the stack reappears and the conversation proceeds as if it was never reset.
[note]: I have noticed that this issue happens when a PromptDialog.Choice() is expecting an input.
Below is the scorable registration and the scorable dialog class.
Thanks!
Registering my scorable dialog:
Conversation.UpdateContainer(
builder =>
{
builder.RegisterModule(new AzureModule(Assembly.GetExecutingAssembly()));
string conn = ConfigurationManager.ConnectionStrings["BotDataContextConnectionString"].ConnectionString;
var store = new SqlBotDataStore(conn);
builder.Register(c => store)
.Keyed<IBotDataStore<BotData>>(AzureModule.Key_DataStore)
.AsSelf()
.SingleInstance();
//Scorable to check if dialog stack reset has been requested
builder.RegisterType<HandleResetRequestScorable>()
.AsSelf()
.As<IScorable<IActivity, double>>();
});
Scorable Dialog:
[Serializable]
public class HandleResetRequestScorable : ScorableBase<IActivity, bool, double>
{
private readonly IBotToUser _botToUser;
private readonly IDialogStack _dialogStack;
private bool _isMessageType { get; set; }
public HandleResetRequestScorable(IBotToUser botToUser, IDialogStack dialogTask)
{
SetField.NotNull(out _botToUser, nameof(_botToUser), botToUser);
//SetField.NotNull(out _dialogStack, nameof(_dialogStack), dialogTask);
}
protected override async Task<bool> PrepareAsync(IActivity activity, CancellationToken token)
{
string message = string.Empty;
if (activity.Type == ActivityTypes.Event)
{
_isMessageType = false;
message = activity.AsEventActivity().Name.ToLower();
}
else
{
_isMessageType = true;
message = activity.AsMessageActivity().Text.ToLower();
}
if (message == "resetconversation")
return true;
return false;
}
protected override bool HasScore(IActivity item, bool state)
{
return state;
}
protected override double GetScore(IActivity item, bool state)
{
return state ? 1.0 : 0;
}
protected override async Task PostAsync(IActivity activity, bool state, CancellationToken token)
{
try
{
var message = activity as IMessageActivity;
string[] data = message.Value.ToString().Split('|');
using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, activity as Activity))
{
//get the conversation related data
var botData = scope.Resolve<IBotData>();
await botData.LoadAsync(default(CancellationToken));
//get the dialog stack
var convoStack = scope.Resolve<IDialogStack>();
convoStack.Reset();
if (_isMessageType)
{
await _botToUser.PostAsync("Conversation reset complete.");
await _botToUser.PostAsync("Hi");
}
await botData.FlushAsync(default(CancellationToken));
}
}
catch
{
throw;
}
}
protected override Task DoneAsync(IActivity item, bool state, CancellationToken token)
{
//throw new NotImplementedException();
return Task.CompletedTask;
}
}
}
Is there a way to create a hero card that has buttons that keep working when it is forwarded to another user?
We want to build a chatbot that enables you to share things with someone else. So first you talk to the chatbot (mainly via buttons) to setup a thing and then you forward that to one of your contacts so that they can participate. With Facebook Messenger we can directly call a share action that pops up the share dialog with which the user can forward a card to a contact. Using a m.me/123?ref=456 URL in a button of the card the receiver can open a conversation with the chatbot in the correct context. I managed to do something similar for Telegram.
I try to replicate this with other services. With Skype there is no way to explicitly call the share action, but I can write a message "Please forward the next card to the user you want to share this with:" [and then I display that card]. Now I give that card a button with a postBack action that should open the correct context, but that button seems to be non functional. When tapping it no post back is sent to the chatbot.
Is there a way to implement something like what I described for Facebook Messenger with Skype? Or is there a way to deep link to a chatbot passing some reference?
(I'd rather have the later for a second use case anyway, where a user puts a share button on their website that then opens Skype in a conversation with the chatbot, passing the reference. I have that for Messenger and Telegram.)
Edit: What I would like to do would basically be the following. I use nodejs, but only the ChatConnector class and not the UniversalBot class.
connector.send([
{
type: 'message',
address,
textFomrat: 'plain',
text: 'Forward the next message to people you want to share THING with:',
},
{
type: 'message',
address,
attachments: [{
contentType: 'application/vnd.microsoft.card.hero',
content: {
title: name,
images: [{
url: image_url
}],
buttons: [{
type: 'postBack',
title: 'Chat with this bot about THING',
value: 'open:' + thing_id,
}]
}
}],
}
], callback);
I'm not sure how you're sending cards to the other user, but here's an example in .net where the buttons sent will function as expected for the other user:
using Microsoft.Bot.Builder.Dialogs;
using System;
using System.Threading.Tasks;
using Microsoft.Bot.Connector;
using Microsoft.Bot.Builder.FormFlow;
using System.Collections.Generic;
using System.Collections.Concurrent;
using Microsoft.Bot.Builder.ConnectorEx;
using Newtonsoft.Json;
namespace CardsBot
{
[Serializable]
public class SendToUserDialog : IDialog<object>
{
static ConcurrentDictionary<string, ConversationReference> _cachedConversationReferences = new ConcurrentDictionary<string, ConversationReference>();
private const string HelpString = "Enter 'Send Card' to send a card to another user. 'Send Message' to send a json message to another user. Or 'List Users' to see who is ready to receive messages.";
public async Task StartAsync(IDialogContext context)
{
context.Wait(this.MessageReceivedAsync);
}
public async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> argument)
{
var message = await argument;
string upperText = message.Text.Replace(" ", "").ToUpper();
if (upperText == "SENDCARD")
{
IFormDialog<SendCardForm> form = new FormDialog<SendCardForm>(new SendCardForm(), SendCardForm.BuildForm, FormOptions.PromptInStart);
context.Call(form, AfterSendCardComplete);
}
else if (upperText == "SENDMESSAGE")
{
IFormDialog<SendMessageForm> form = new FormDialog<SendMessageForm>(new SendMessageForm(), SendMessageForm.BuildForm, FormOptions.PromptInStart);
context.Call(form, AfterSendMessageFormComplete);
}
else if (upperText == "LISTUSERS")
{
var names = String.Join(", ", _cachedConversationReferences.Keys);
await context.PostAsync($"Users : {names}");
}
else
{
if (!context.UserData.ContainsKey("name"))
{
var getNamePrompt = new PromptDialog.PromptString("What is your name?", "Please enter your name.", 3);
context.Call(getNamePrompt, AfterNamePrompt);
}
else
{
await context.PostAsync($"You said: {message.Text} ({HelpString})");
context.Wait(MessageReceivedAsync);
}
}
}
private async Task AfterSendMessageFormComplete(IDialogContext context, IAwaitable<SendMessageForm> result)
{
var sendMessageForm = await result;
if (string.IsNullOrEmpty(sendMessageForm.RecipientName))
{
await context.PostAsync("A recipient name was not provided. Cannot send the message to nobody.");
}
else
{
ConversationReference conversation = null;
if (_cachedConversationReferences.TryGetValue(sendMessageForm.RecipientName, out conversation))
{
var originalReply = conversation.GetPostToBotMessage().CreateReply();
var replyMessage = GetMessage(originalReply, sendMessageForm.MessageJson);
var connector = new ConnectorClient(new Uri(originalReply.ServiceUrl));
await connector.Conversations.ReplyToActivityAsync(replyMessage);
await context.PostAsync($"Message was sent to {sendMessageForm.RecipientName} on {replyMessage.ChannelId} channel.");
}
else
{
await context.PostAsync($"No recipient found matching the name {sendMessageForm.RecipientName}");
}
}
}
private async Task AfterNamePrompt(IDialogContext context, IAwaitable<string> result)
{
var name = await result;
context.UserData.SetValue("name", name);
await context.PostAsync($"Thanks. What would you like to do now {name}? {HelpString}");
var conversationReference = context.Activity.ToConversationReference();
_cachedConversationReferences.AddOrUpdate(name, conversationReference, (oldVal, newVal) => conversationReference);
}
public Activity GetMessage(Activity originalReply, string messageJson)
{
var reply = JsonConvert.DeserializeObject<Activity>(messageJson);
reply.ChannelId = originalReply.ChannelId;
reply.Timestamp = originalReply.Timestamp;
reply.From = originalReply.From;
reply.Conversation = originalReply.Conversation;
reply.Recipient = originalReply.Recipient;
reply.Id = originalReply.Id;
reply.ReplyToId = originalReply.ReplyToId;
reply.ServiceUrl = originalReply.ServiceUrl;
return reply;
}
private async Task AfterSendCardComplete(IDialogContext context, IAwaitable<SendCardForm> result)
{
var SendCardForm = await result;
if (string.IsNullOrEmpty(SendCardForm.RecipientName))
{
await context.PostAsync("A recipient name was not provided. Cannot send a card to nobody.");
}
else
{
ConversationReference conversation = null;
if (_cachedConversationReferences.TryGetValue(SendCardForm.RecipientName, out conversation))
{
var reply = conversation.GetPostToBotMessage().CreateReply();
reply.Attachments.Add(GetCard(SendCardForm.Text, SendCardForm.Buttons.Value));
var connector = new ConnectorClient(new Uri(reply.ServiceUrl));
await connector.Conversations.ReplyToActivityAsync(reply);
}
else
{
await context.PostAsync($"No recipient found matching the name {SendCardForm.RecipientName}");
}
}
}
public Attachment GetCard(string text, int numberOfButtons)
{
var card = new HeroCard(text);
card.Buttons = new List<CardAction>(GetCardActions(numberOfButtons));
return card.ToAttachment();
}
private IEnumerable<CardAction> GetCardActions(int numberOfButtons)
{
for (int counter = 1; counter < numberOfButtons; counter++)
{
yield return new CardAction()
{
Title = $"button{counter}",
Type = ActionTypes.ImBack,
Value = $"button{counter}"
};
}
}
}
[Serializable]
public class SendMessageForm
{
[Prompt("Who would you like to send this message to (enter the name of the recipient)?")]
public string RecipientName { get; set; }
[Prompt("Paste in the .json of the message you would like to Send.")]
public string MessageJson { get; set; }
public static IForm<SendMessageForm> BuildForm()
{
return new FormBuilder<SendMessageForm>()
.AddRemainingFields()
.Confirm("Is this information correct?{*}")
.Build();
}
}
[Serializable]
public class SendCardForm
{
[Prompt("What would you like for the card text?")]
public string Text { get; set; }
[Prompt("How many buttons?")]
public int? Buttons { get; set; }
[Prompt("Who would you like to send the card to?")]
public string RecipientName { get; set; }
public static IForm<SendCardForm> BuildForm()
{
return new FormBuilder<SendCardForm>()
.AddRemainingFields()
.Confirm("Is this information correct?{*}")
.Build();
}
}
}
I am implementing a simple login screen with MvvmCross Forms and Refit.
The start of the authentication task is started by pressing the button that has the command 'LogInCommand'
<Button x:Name="loginButton" Text = "Login" HorizontalOptions = "Center"
VerticalOptions = "StartAndExpand" FontSize="24" Command="{ Binding LogInCommand }"/>
This will execute the following command in the AuthenticationViewModel:
#region commands
async Task LogInTaskAsync()
{
var errors = _validator.Validate(this);
if (!errors.IsValid)
{
_toastService.DisplayErrors(errors); //Display errors here.
}
else
{
using (new Busy(this))
{
try
{
Status = AuthenticationStatusEnum.LOADING;
// get access token
string accessToken = await _authenticationService.LogIn(_email, _password);
Debug.WriteLine(String.Format("Access Token:t {0} ", accessToken));
// save token on preferences.
Settings.AccessToken = accessToken;
Status = AuthenticationStatusEnum.LOGIN_SUCESS;
}
catch (TaskCanceledException ex) {
Debug.WriteLine("Timeout Operation ...");
}
catch (Exception ex)
{
MessagingCenter.Send(new object(), EventTypeName.EXCEPTION_OCCURRED, ex);
Status = AuthenticationStatusEnum.LOGIN_FAILED;
}
}
}
}
IMvxCommand _logInCommand;
public IMvxCommand LogInCommand =>
_logInCommand ?? (_logInCommand = new MvxCommand(async () => await LogInTaskAsync()));
An instance of the AuthenticationService that is working with refit is injected into the view model.
async public Task<string> LogIn(string email, string password)
{
Debug.WriteLine(String.Format("Login with {0}/{1}", email, password));
return await _authenticationRestService.getAuthorizationToken(new JwtAuthenticationRequestDTO()
{
Email = email,
Password = password
}).ContinueWith(t => t.Result.Data.Token, TaskContinuationOptions.OnlyOnRanToCompletion);
}
The Refit service specification is as follows:
[Headers("Accept: application/json")]
public interface IAuthenticationRestService
{
[Post("/")]
Task<APIResponse<JwtAuthenticationResponseDTO>> getAuthorizationToken([Body] JwtAuthenticationRequestDTO authorizationRequest);
}
The problem is that the task is always canceled, the TaskCanceledException exception is always thrown.
I also expose CoreApp where I configure context instances.
public class CoreApp : MvvmCross.Core.ViewModels.MvxApplication
{
public override void Initialize()
{
var httpClient = new HttpClient(new HttpLoggingHandler())
{
BaseAddress = new Uri(SharedConfig.BASE_API_URL),
Timeout = TimeSpan.FromMinutes(SharedConfig.TIMEOUT_OPERATION_MINUTES)
};
// Register REST services
Mvx.RegisterSingleton<IAuthenticationRestService>(() => RestServiceFactory.getService<IAuthenticationRestService>(httpClient));
Mvx.RegisterSingleton<IParentsRestService>(() => RestServiceFactory.getService<IParentsRestService>(httpClient));
Mvx.RegisterSingleton<IChildrenRestService>(() => RestServiceFactory.getService<IChildrenRestService>(httpClient));
CreatableTypes()
.InNamespace("Bullytect.Core.Services")
.EndingWith("ServiceImpl")
.AsInterfaces()
.RegisterAsLazySingleton();
Mapper.Initialize(cfg => {
cfg.CreateMap<ParentDTO, ParentEntity>();
cfg.CreateMap<SonDTO, SonEntity>();
});
Mvx.RegisterType<IValidator, Validator>();
RegisterAppStart(new CustomAppStart());
}
}
Someone knows how I can fix this. Thanks in advance!!
I have a bot where I am doing something like this:
1) A new Activity (message) arrives.
2) I dispatch the message to a RootDialog.
3) Depending on some Logic, RootDialog can:
a) Call a LuisDialog (handling natural language)
b) Call a CustomDialog (handles some business logic).
But when the user state is reset, and the flow leads to an intention inside the LuisDialog, it calls the intention method twice. Just the first time the state is empty, then it works fine.
Let me show you the code:
MessagesController:
public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
if (activity.Type == ActivityTypes.Message)
{
ConnectorClient connector = new ConnectorClient(new Uri(activity.ServiceUrl));
try
{
await Conversation.SendAsync(activity, () => new RootDialog());
}
catch (HttpRequestException e)
{
...
}
}
RootDialog:
public class RootDialog : IDialog<object>
{
public async Task StartAsync(IDialogContext context)
{
await MessageReceivedAsync(context, Awaitable.FromItem(context.Activity.AsMessageActivity()));
}
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> awaitable)
{
bool value = DoSomeCustomLogic();
if (value)
{
string message = DoSomething();
await context.PostAsync(message);
} else {
bool value2 = DoSomeCustomLogic2();
if (value2)
{
var answerValidationDialog = new ValidateAnswerWithUserDialog();
context.Call(answerValidationDialog, ValidateAnswerWithUserDialogCompletedCallback);
} else {
var luisDialog = new LuisDialog();
await context.Forward(luisDialog,LuisDialogCompletedCallback, context.Activity, CancellationToken.None);
}
}
}
Callbacks only do context.Done(true);
And LuisDialog has an Intention which goes like this:
[LuisIntent(LuisUtils.INTENT_MENU_SALUTE)]
public async Task SaluteOrMenu(IDialogContext context, LuisResult result)
{
if (LuisUtils.IntentScoreIsHighEnough(result))
{
string userName = context.Activity.From.Name;
ContextHelper helper = new ContextHelper(MessageReceived);
await helper.AskUserToDoSomeInitialAction(context, saluteWord, userName);
context.Done(true);
}
else
{
await None(context, result);
}
}
And finally class ContextHelper:
public class ContextHelper
{
private Func<IDialogContext, IAwaitable<IMessageActivity>, Task> MessageReceived;
public ContextHelper(Func<IDialogContext, IAwaitable<IMessageActivity>, Task> messageReceived)
{
MessageReceived = messageReceived;
}
public async Task AskUserToDoSomeInitialAction(IDialogContext context, string saluteWord, string userName)
{
var reply = context.MakeMessage();
List<CardAction> buttons = BuildInitialOptionActions();
List<CardImage> images = BuildInitialOptionImages();
string initialText = $"Hi stranger!"
var card = new HeroCard
{
Title = "Hello!"
Text = initialText,
Buttons = buttons,
Images = images
};
reply.Attachments = new List<Attachment> { card.ToAttachment() };
await context.PostAsync(reply);
context.Wait(AfterUserChoseOptionInSalute);
}
private async Task AfterUserChoseOptionInSalute(IDialogContext context, IAwaitable<IMessageActivity> result)
{
await ReDispatchMessageReceivedToDialog(context);
}
private async Task ReDispatchMessageReceivedToDialog(IDialogContext context)
{
await MessageReceived(context, Awaitable.FromItem(context.Activity.AsMessageActivity()));
}
}
The SaluteOrMenu Intention gets called twice (only the first time I interact with the bot or when I delete the state. After Debugging I saw that after doing context.Wait(AfterUserChoseOptionInSalute);, the bot calls that function (instead of waiting for an event to call it)
Any ideas?
Thanks in advance.
I found the line that was wrong. It was on the first dialog (the RootDialog):
public async Task StartAsync(IDialogContext context)
{
await MessageReceivedAsync(context, Awaitable.FromItem(context.Activity.AsMessageActivity()));
}
That is a line that re dispatches a message with the incoming activity. I had it somewhere in some chunk of code and (don't know why), thought it was a good idea to use it on StartAsync. So, because of that, two calls were occurring.
Dumb me.
I just changed it to this and worked:
public async Task StartAsync(IDialogContext context)
{
context.Wait(MessageReceivedAsync);
}