I implemented an external login for my BOT. When external site calls Bot CallBack method I need to set token and username in PrivateConversationData and then resume chat with a message like "Welcome back [username]!".
To display this message I send a MessageActivity but this activity never connects to my chat and won't fire the appropriate [LuisIntent("UserIsAuthenticated")].
Other intents, out of login-flow, works as expected.
This is the callback method:
public class OAuthCallbackController : ApiController
{
[HttpGet]
[Route("api/OAuthCallback")]
public async Task OAuthCallback([FromUri] string userId, [FromUri] string botId, [FromUri] string conversationId,
[FromUri] string channelId, [FromUri] string serviceUrl, [FromUri] string locale,
[FromUri] CancellationToken cancellationToken, [FromUri] string accessToken, [FromUri] string username)
{
var resumptionCookie = new ResumptionCookie(TokenDecoder(userId), TokenDecoder(botId),
TokenDecoder(conversationId), channelId, TokenDecoder(serviceUrl), locale);
var container = WebApiApplication.FindContainer();
var message = resumptionCookie.GetMessage();
message.Text = "UserIsAuthenticated";
using (var scope = DialogModule.BeginLifetimeScope(container, message))
{
var botData = scope.Resolve<IBotData>();
await botData.LoadAsync(cancellationToken);
botData.PrivateConversationData.SetValue("accessToken", accessToken);
botData.PrivateConversationData.SetValue("username", username);
ResumptionCookie pending;
if (botData.PrivateConversationData.TryGetValue("persistedCookie", out pending))
{
botData.PrivateConversationData.RemoveValue("persistedCookie");
await botData.FlushAsync(cancellationToken);
}
var stack = scope.Resolve<IDialogStack>();
var child = scope.Resolve<MainDialog>(TypedParameter.From(message));
var interruption = child.Void<object, IMessageActivity>();
try
{
stack.Call(interruption, null);
await stack.PollAsync(cancellationToken);
}
finally
{
await botData.FlushAsync(cancellationToken);
}
}
}
}
public static string TokenDecoder(string token)
{
return Encoding.UTF8.GetString(HttpServerUtility.UrlTokenDecode(token));
}
}
This is the controller:
public class MessagesController : ApiController
{
private readonly ILifetimeScope scope;
public MessagesController(ILifetimeScope scope)
{
SetField.NotNull(out this.scope, nameof(scope), scope);
}
public async Task<HttpResponseMessage> Post([FromBody] Activity activity, CancellationToken token)
{
if (activity != null)
{
switch (activity.GetActivityType())
{
case ActivityTypes.Message:
using (var scope = DialogModule.BeginLifetimeScope(this.scope, activity))
{
var postToBot = scope.Resolve<IPostToBot>();
await postToBot.PostAsync(activity, token);
}
break;
}
}
return new HttpResponseMessage(HttpStatusCode.Accepted);
}
}
This is how I registered components:
protected override void Load(ContainerBuilder builder)
{
base.Load(builder);
builder.Register(
c => new LuisModelAttribute("myId", "SubscriptionKey"))
.AsSelf()
.AsImplementedInterfaces()
.SingleInstance();
builder.RegisterType<MainDialog>().AsSelf().As<IDialog<object>>().InstancePerDependency();
builder.RegisterType<LuisService>()
.Keyed<ILuisService>(FiberModule.Key_DoNotSerialize)
.AsImplementedInterfaces()
.SingleInstance();
}
This is the dialog:
[Serializable]
public sealed class MainDialog : LuisDialog<object>
{
public static readonly string AuthTokenKey = "TestToken";
public readonly ResumptionCookie ResumptionCookie;
public static readonly Uri CloudocOauthCallback = new Uri("http://localhost:3980/api/OAuthCallback");
public MainDialog(IMessageActivity activity, ILuisService luis)
: base(luis)
{
ResumptionCookie = new ResumptionCookie(activity);
}
[LuisIntent("")]
public async Task None(IDialogContext context, LuisResult result)
{
await context.PostAsync("Sorry cannot understand!");
context.Wait(MessageReceived);
}
[LuisIntent("UserAuthenticated")]
public async Task UserAuthenticated(IDialogContext context, LuisResult result)
{
string username;
context.PrivateConversationData.TryGetValue("username", out username);
await context.PostAsync($"Welcome back {username}!");
context.Wait(MessageReceived);
}
[LuisIntent("Login")]
private async Task LogIn(IDialogContext context, LuisResult result)
{
string token;
if (!context.PrivateConversationData.TryGetValue(AuthTokenKey, out token))
{
context.PrivateConversationData.SetValue("persistedCookie", ResumptionCookie);
var loginUrl = CloudocHelpers.GetLoginURL(ResumptionCookie, OauthCallback.ToString());
var reply = context.MakeMessage();
var cardButtons = new List<CardAction>();
var plButton = new CardAction
{
Value = loginUrl,
Type = ActionTypes.Signin,
Title = "Connetti a Cloudoc"
};
cardButtons.Add(plButton);
var plCard = new SigninCard("Connect", cardButtons);
reply.Attachments = new List<Attachment>
{
plCard.ToAttachment()
};
await context.PostAsync(reply);
context.Wait(MessageReceived);
}
else
{
context.Done(token);
}
}
}
What I miss?
Update
Also tried with ResumeAsync in callback method:
var container = WebApiApplication.FindContainer();
var message = resumptionCookie.GetMessage();
message.Text = "UserIsAuthenticated";
using (var scope = DialogModule.BeginLifetimeScope(container, message))
{
var botData = scope.Resolve<IBotData>();
await botData.LoadAsync(cancellationToken);
botData.PrivateConversationData.SetValue("accessToken", accessToken);
botData.PrivateConversationData.SetValue("username", username);
ResumptionCookie pending;
if (botData.PrivateConversationData.TryGetValue("persistedCookie", out pending))
{
botData.PrivateConversationData.RemoveValue("persistedCookie");
await botData.FlushAsync(cancellationToken);
}
await Conversation.ResumeAsync(resumptionCookie, message, cancellationToken);
}
but it give me the error Operation is not valid due to the current state of the object.
Update 2
Following Ezequiel idea I changed my code this way:
[HttpGet]
[Route("api/OAuthCallback")]
public async Task OAuthCallback(string state, [FromUri] string accessToken, [FromUri] string username)
{
var resumptionCookie = ResumptionCookie.GZipDeserialize(state);
var message = resumptionCookie.GetMessage();
message.Text = "UserIsAuthenticated";
await Conversation.ResumeAsync(resumptionCookie, message);
}
resumptionCookie seems to be ok:
but await Conversation.ResumeAsync(resumptionCookie, message); continue to give me the error Operation is not valid due to the current state of the object.
You need to resume the conversation with the bot that's why the message is likely not arriving.
Instead of using the dialog stack, try using
await Conversation.ResumeAsync(resumptionCookie, message);
Depending on your auth needs, you might want to consider AuthBot. You can also take a look to the logic on the OAuthCallback controller of the library to get an idea of how they are resuming the conversation with the Bot after auth.
The ContosoFlowers example, is also using the resume conversation mechanism. Not for auth purposes, but for showing how to handle a hypotethical credit card payment.
I found how to make it works.
Controller:
public class MessagesController : ApiController
{
public async Task<HttpResponseMessage> Post([FromBody] Activity activity, CancellationToken token)
{
if (activity != null)
{
switch (activity.GetActivityType())
{
case ActivityTypes.Message:
var container = WebApiApplication.FindContainer();
using (var scope = DialogModule.BeginLifetimeScope(container, activity))
{
await Conversation.SendAsync(activity, () => scope.Resolve<IDialog<object>>(), token);
}
break;
}
}
return new HttpResponseMessage(HttpStatusCode.Accepted);
}
}
Global.asax
public class WebApiApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
GlobalConfiguration.Configure(WebApiConfig.Register);
var builder = new ContainerBuilder();
builder.RegisterModule(new DialogModule());
builder.RegisterModule(new MyModule());
var config = GlobalConfiguration.Configuration;
builder.RegisterApiControllers(Assembly.GetExecutingAssembly());
builder.RegisterWebApiFilterProvider(config);
var container = builder.Build();
config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
}
public static ILifetimeScope FindContainer()
{
var config = GlobalConfiguration.Configuration;
var resolver = (AutofacWebApiDependencyResolver)config.DependencyResolver;
return resolver.Container;
}
}
MyModule:
public sealed class MyModule : Module
{
protected override void Load(ContainerBuilder builder)
{
base.Load(builder);
builder.Register(
c => new LuisModelAttribute("MyId", "SubId"))
.AsSelf()
.AsImplementedInterfaces()
.SingleInstance();
builder.RegisterType<MainDialog>().AsSelf().As<IDialog<object>>().InstancePerDependency();
builder.RegisterType<LuisService>()
.Keyed<ILuisService>(FiberModule.Key_DoNotSerialize)
.AsImplementedInterfaces()
.SingleInstance();
}
}
Callback method:
public class OAuthCallbackController : ApiController
{
[HttpGet]
[Route("api/OAuthCallback")]
public async Task OAuthCallback(string state, [FromUri] CancellationToken cancellationToken, [FromUri] string accessToken, [FromUri] string username)
{
var resumptionCookie = ResumptionCookie.GZipDeserialize(state);
var message = resumptionCookie.GetMessage();
message.Text = "UserIsAuthenticated";
using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, message))
{
var dataBag = scope.Resolve<IBotData>();
await dataBag.LoadAsync(cancellationToken);
dataBag.PrivateConversationData.SetValue("accessToken", accessToken);
dataBag.PrivateConversationData.SetValue("username", username);
ResumptionCookie pending;
if (dataBag.PrivateConversationData.TryGetValue("persistedCookie", out pending))
{
dataBag.PrivateConversationData.RemoveValue("persistedCookie");
await dataBag.FlushAsync(cancellationToken);
}
}
await Conversation.ResumeAsync(resumptionCookie, message, cancellationToken);
}
Related
I'm using the packages "FastEndpoints" & "FastEndpoints.Security" for creating the RESTApi.
This is my Endpoint:
public class LoginEndpoint : Endpoint<LoginRequest, LoginResponse>
{
IUserService _userSvc;
public LoginEndpoint(IUserService users)
{
_userSvc = users;
}
public override void Configure()
{
Verbs(Http.GET);
Routes("/api/login");
AllowAnonymous();
}
public override async Task HandleAsync(LoginRequest req, CancellationToken ct)
{
if (_userSvc.LoginValidByName(req.Name, req.Password))
{
var user = _userSvc.GetFromName(req.Name);
var expiresAt = DateTime.UtcNow.AddDays(1);
var token = JWTBearer.CreateToken(
GlobalSettings.TOKEN_SIGNING_KEY,
expiresAt,
user.Permissions.Select(p => p.Name));
await SendAsync(
new LoginResponse()
{
Token = token,
ExpiresAt = expiresAt
});
}
else
{
await SendUnauthorizedAsync();
}
}
}
Using Postman, the endpoints works as expected:
But when using RestSharp (and mind you, I'm very new to the whole RESTApi world), I get an error 'Request ended prematurely'.
This is my simple call:
public class ApiClient
{
private RestClient _restClient;
public ApiClient(string baseUrl)
{
_restClient = new RestClient(baseUrl);
//ServicePointManager.ServerCertificateValidationCallback += (s, c, ch, p) => true;
}
public async Task<bool> UserValid(string username, string password)
{
var request = new RestRequest("/api/login", Method.Get);
request.AddParameter("name", username);
request.AddParameter("password", password);
var result = await _restClient.GetAsync(request);
if (result.StatusCode == HttpStatusCode.OK)
return true;
else
return false;
}
}
Can someone fill me in?
Since it works with Postman, I suspect my call being bad.
Is:
_userSvc.LoginValidByName
Or any other function missing an await by chance?
Requirement
FormStateModel already contains FIRST input that users types.
Code
Simply I want to put the string that is in activity.Text inside FormStateModel:
private IDialog<FormStateModel> MakeRootDialog(string input)
{
return Chain.From(() => new FormDialog<FormStateModel>(
new FormStateModel() { Question = input },
ContactDetailsForm.BuildForm,
FormOptions.None));
}
=
public async Task<HttpResponseMessage> Post([FromBody] Activity activity)
{
if (activity.Type == ActivityTypes.Message)
{
await Conversation.SendAsync(
toBot: activity,
MakeRoot: () => this.MakeRootDialog(activity.Text));
}
else
{
await HandleSystemMessageAsync(activity);
}
var response = this.Request.CreateResponse(HttpStatusCode.OK);
return response;
}
On ConversationUpdate I start conversation simply by asking "Please type your Question:"
private static async Task<Activity> HandleSystemMessageAsync(Activity message)
{
switch (message.Type)
{
case ActivityTypes.DeleteUserData:
break;
case ActivityTypes.ConversationUpdate:
await Welcome(message);
break;
(...)
In that way:
private static async Task Welcome(Activity activity)
{
(...)
reply.Text = string.Format("Hello, how can we help you today? Please type your Question:");
await client.Conversations.ReplyToActivityAsync(reply);
(...)
}
But I can not find a way how to pass it. In this case this exception occurs:
anonymous method closures that capture the environment are not serializable, consider removing environment capture or using a reflection serialization surrogate:
Is there any way around that to populate state model at this step?
Solved by calling RootDialog inside MessagesController, then Calling new FormDialog by context.Call(form, (...));
public async Task<HttpResponseMessage> Post([FromBody] Activity activity)
{
await Conversation.SendAsync(activity, () => new LayerDialog());
}
LayerDialog:
[Serializable]
public class LayerDialog: IDialog<IMessageActivity>
{
public async Task StartAsync(IDialogContext context)
{
context.Wait(this.OnMessageReceivedAsync);
}
private async Task OnMessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> result)
{
var awaited = await result;
FormStateModel model = new FormStateModel();
model.Value = awaited.Text;
var form = new FormDialog<FormStateModel >(model ,
BuildForm , FormOptions.PromptInStart);
context.Call(form , this.AfterResume);
}
I'm currently making a chatbot with Microsoft's Bot Framework. In my flow I have a final dialog that lets the user know, that they are participating in the competition. There is also an error-handling method for unknown input. The two methods are seen here:
[Serializable]
public class ConcertCityDialog : AbstractBasicDialog<DialogResult>
{
private static FacebookService FacebookService => new FacebookService(new FacebookClient());
[LuisIntent("ConcertCity")]
public async Task ConcertCityIntent(IDialogContext context, LuisResult result)
{
var fbAccount = await FacebookService.GetAccountAsync(context.Activity.From.Id);
var selectedCityName = result.Entities.FirstOrDefault()?.Entity;
concert_city selectedCity;
using (var concertCityService = new ConcertCityService())
{
selectedCity = concertCityService.FindConcertCity(selectedCityName);
}
if (selectedCity == null)
{
await NoneIntent(context, result);
return;
}
user_interaction latestInteraction;
using (var userService = new MessengerUserService())
{
var user = userService.FindByFacebookIdIncludeInteractions(context.Activity.From.Id);
latestInteraction = user.user_interaction.MaxBy(e => e.created_at);
}
latestInteraction.preferred_city_id = selectedCity.id;
latestInteraction.gif_created = true;
using (var userInteractionService = new UserInteractionService())
{
userInteractionService.UpdateUserInteraction(latestInteraction);
}
var shareIntroReply = context.MakeMessage();
shareIntroReply.Text = "Great choice! You are now participating in the competition. If you dare then pass your message \uD83D\uDE0E";
await context.PostAsync(shareIntroReply);
var reply = await MessageUtility.MakeShareMessageCard(context, fbAccount, latestInteraction, false);
await context.PostAsync(reply);
context.Done(DialogResult.Done);
}
[LuisIntent("")]
[LuisIntent("None")]
public async Task NoneIntent(IDialogContext context, LuisResult result)
{
messenger_user user;
using (var userService = new MessengerUserService())
{
user = userService.FindByFacebookId(context.Activity.From.Id);
}
var phrase = CreateMisunderstoodPhrase(user, result.Query);
using (var misunderstoodPhraseService = new MisunderstoodPhraseService())
{
misunderstoodPhraseService.CreatePhrase(phrase);
}
List<concert_city> concertCities;
using (var concertCityService = new ConcertCityService())
{
concertCities = concertCityService.GetUpcomingConcertCities().ToList();
}
// Prompt city
var reply = context.MakeMessage();
reply.Text = "I'm not sure what you mean \uD83E\uDD14<br/>Which Grøn Koncert would you like to attend?";
reply.SuggestedActions = new SuggestedActions
{
Actions = concertCities.Select(e => MessageUtility.MakeQuickAnswer(e.name)).ToList()
};
await context.PostAsync(reply);
context.Wait(MessageReceived);
}
protected override void OnDeserializedCustom(StreamingContext context)
{
}
}
And here is the AbstractBasicDialog implementation:
[Serializable]
public abstract class AbstractBasicDialog<T> : LuisDialog<T>
{
protected AbstractBasicDialog() : base(new LuisService(new LuisModelAttribute(
ConfigurationManager.AppSettings["LuisAppId"],
ConfigurationManager.AppSettings["LuisAPIKey"],
domain: ConfigurationManager.AppSettings["LuisAPIHostName"])))
{
}
[LuisIntent("Cancel")]
public virtual async Task CancelIntent(IDialogContext context, LuisResult result)
{
var randomQuotes = new List<string>
{
"If you say so, I'll leave you alone for now",
"alright then, I'll leave you alone",
"Okay then, I won't bother you anymore"
};
await context.PostAsync(MessageUtility.RandAnswer(randomQuotes));
context.Done(DialogResult.Cancel);
}
[LuisIntent("Start")]
public virtual async Task StartIntent(IDialogContext context, LuisResult result)
{
context.Done(DialogResult.Restart);
}
[LuisIntent("CustomerSupport")]
public async Task CustomerSupportIntent(IDialogContext context, LuisResult result)
{
using (var userService = new MessengerUserService())
{
var user = userService.FindByFacebookId(context.Activity.From.Id);
if (user != null)
{
user.receiving_support = true;
userService.UpdateUser(user);
}
}
await context.PostAsync("I'll let customer service know, that you want to talk to them. They will get back to you within 24 hours.<br/>If at any time you want to return to me, and start passing a message, just type \"Stop customer support\".");
context.Call(new CustomerSupportDialog(), ResumeAfterCustomerSupport);
}
private async Task ResumeAfterCustomerSupport(IDialogContext context, IAwaitable<DialogResult> result)
{
context.Done(await result);
}
protected misunderstood_phrase CreateMisunderstoodPhrase(messenger_user user, string phrase)
{
return new misunderstood_phrase
{
phrase = phrase,
dialog = GetType().Name,
messenger_user_id = user.id
};
}
[OnDeserialized]
private void OnDeserialized(StreamingContext context)
{
OnDeserializedCustom(context);
}
protected abstract void OnDeserializedCustom(StreamingContext context);
}
The call chain starts at this dialog:
[Serializable]
public class BasicLuisDialog : LuisDialog<DialogResult>
{
private static FacebookService FacebookService => new FacebookService(new FacebookClient());
public BasicLuisDialog() : base(new LuisService(new LuisModelAttribute(
ConfigurationManager.AppSettings["LuisAppId"],
ConfigurationManager.AppSettings["LuisAPIKey"],
domain: ConfigurationManager.AppSettings["LuisAPIHostName"])))
{
}
[LuisIntent("")]
[LuisIntent("None")]
public async Task NoneIntent(IDialogContext context, LuisResult result)
{
var facebookAccount = await FacebookService.GetAccountAsync(context.Activity.From.Id);
RegisterUser(facebookAccount, null, out var user);
var phrase = CreateMisunderstoodPhrase(user, result.Query);
using (var misunderstoodPhraseService = new MisunderstoodPhraseService())
{
misunderstoodPhraseService.CreatePhrase(phrase);
}
var reply = context.MakeMessage();
reply.SuggestedActions = new SuggestedActions
{
Actions = new List<CardAction>
{
new CardAction { Title = "Get started", Type = ActionTypes.ImBack, Value = "Get started" },
new CardAction { Title = "Customer support", Type = ActionTypes.ImBack, Value = "Customer support" }
}
};
var name = string.IsNullOrEmpty(facebookAccount.FirstName) ? "" : $"{facebookAccount.FirstName} ";
reply.Text = $"Hm, I'm not sure what you mean {name} \uD83E\uDD14 Here are some ways you can interact with me:";
await context.PostAsync(reply);
context.Wait(MessageReceived);
}
[LuisIntent("Greeting")]
[LuisIntent("Positive")]
[LuisIntent("Start")]
public async Task GreetingIntent(IDialogContext context, LuisResult result)
{
var rnd = new Random();
var facebookAccount = await FacebookService.GetAccountAsync(context.Activity.From.Id);
// Initial Greeting
var greetings = new List<string>
{
"Well hello there",
"Hi there"
};
if (!string.IsNullOrEmpty(facebookAccount.FirstName))
{
greetings.Add("Hi {0}");
greetings.Add("Hello {0}");
greetings.Add("Welcome {0}");
}
if (facebookAccount.Gender == "male")
greetings.Add("Hey handsome");
else if (facebookAccount.Gender == "female")
greetings.Add("Hi gorgeous");
var randIndex = rnd.Next(greetings.Count);
var greeting = string.Format(greetings[randIndex], facebookAccount.FirstName);
await context.PostAsync(greeting);
await MessageUtility.StartTyping(context, 300);
country country;
using (var countryService = new CountryService())
{
country = countryService.FindCountry(facebookAccount.Locale);
}
var userHasCountry = RegisterUser(facebookAccount, country, out var user);
// If user contry not found prompt for answer
if (!userHasCountry)
{
var countryReply = context.MakeMessage();
countryReply.Text = "You are hard to keep track of - where are you from?";
countryReply.SuggestedActions = new SuggestedActions
{
Actions = new List<CardAction>
{
MessageUtility.MakeQuickAnswer("Denmark"),
MessageUtility.MakeQuickAnswer("Norway"),
MessageUtility.MakeQuickAnswer("Sweden"),
MessageUtility.MakeQuickAnswer("Other")
}
};
await context.PostAsync(countryReply);
context.Call(new CountryDialog(), AfterCountryDialog);
}
else
{
await FunPrompt(context, country);
}
}
private async Task AfterCountryDialog(IDialogContext countryContext, IAwaitable<country> countryAwaitable)
{
var country = await countryAwaitable;
var facebookAccount = await FacebookService.GetAccountAsync(countryContext.Activity.From.Id);
using (var userService = new MessengerUserService())
{
var user = userService.FindByFacebookId(facebookAccount.Id);
user.country = country;
userService.UpdateUser(user);
}
var reply = countryContext.MakeMessage();
reply.Text = "That's cool \uD83D\uDE0E";
await countryContext.PostAsync(reply);
await MessageUtility.StartTyping(countryContext, 350);
await FunPrompt(countryContext, country);
}
private async Task FunPrompt(IDialogContext context, country country)
{
if (country?.name == "norway" && DateTime.Now < new DateTime(2018, 8, 13))
{
var reply = context.MakeMessage();
reply.Text = "Unfortunately the competition isn't open in Norway yet. You can still talk to customer support if you want to";
reply.SuggestedActions = new SuggestedActions
{
Actions = new List<CardAction>
{
MessageUtility.MakeQuickAnswer("Customer support")
}
};
await context.PostAsync(reply);
context.Wait(MessageReceived);
}
else if ((country?.name == "denmark" && DateTime.Now >= new DateTime(2018, 7, 29)) ||
(country?.name == "norway" && DateTime.Now >= new DateTime(2018, 10, 21)))
{
var reply = context.MakeMessage();
reply.Text = "The competition has ended. You can still talk to customer support if you want to";
reply.SuggestedActions = new SuggestedActions
{
Actions = new List<CardAction>
{
MessageUtility.MakeQuickAnswer("Customer support")
}
};
await context.PostAsync(reply);
context.Wait(MessageReceived);
}
else
{
await context.PostAsync("Are you up for some fun?");
context.Call(new IntroductionDialog(), ResumeAfterDialog);
}
}
[LuisIntent("CustomerSupport")]
public async Task CustomerSupportIntent(IDialogContext context, LuisResult result)
{
using (var userService = new MessengerUserService())
{
var user = userService.FindByFacebookId(context.Activity.From.Id);
if (user != null)
{
user.receiving_support = true;
userService.UpdateUser(user);
}
}
await context.PostAsync("I'll let customer support know, that you want to talk to them. They should be messaging you shortly.<br/>You can end your conversation with customer support at any time by typing \"Stop customer support\".");
context.Call(new CustomerSupportDialog(), ResumeAfterDialog);
}
private async Task ResumeAfterDialog(IDialogContext context, IAwaitable<DialogResult> result)
{
var resultState = await result;
if (resultState == DialogResult.Restart)
await GreetingIntent(context, null);
else if (resultState == DialogResult.CustomerSupport)
await ResumeAfterCustomerSupport(context);
else if (resultState == DialogResult.Done || resultState == DialogResult.Cancel)
context.Done(resultState);
else
context.Wait(MessageReceived);
}
private async Task ResumeAfterCustomerSupport(IDialogContext context)
{
using (var userService = new MessengerUserService())
{
var user = userService.FindByFacebookId(context.Activity.From.Id);
if (user != null)
{
user.receiving_support = false;
userService.UpdateUser(user);
}
}
await context.PostAsync("I hope you got the help you needed. Would you like to pass a message to a friend?");
context.Call(new IntroductionDialog(), ResumeAfterDialog);
}
private bool RegisterUser(FacebookAccount fbAccount, country country, out messenger_user user)
{
if (string.IsNullOrEmpty(fbAccount?.Id))
{
user = null;
return false;
}
using (var userService = new MessengerUserService())
{
user = userService.FindByFacebookId(fbAccount.Id);
if (user != null)
return user.country != null;
user = new messenger_user
{
id = fbAccount.Id,
country = country
};
userService.CreateUser(user);
return user.country != null;
}
}
protected misunderstood_phrase CreateMisunderstoodPhrase(messenger_user user, string phrase)
{
return new misunderstood_phrase
{
phrase = phrase,
dialog = GetType().Name,
messenger_user_id = user.id
};
}
}
This works most of the time. The user is told that their registration was a success and the flow exits with the context.Done() call. Sometimes however the chatbot doesn't register the dialog as being exited, as seen here:
As you can see the chatbot is still in the same Dialog even though I have called the Done() method. This is a general problem in my chatbot, as it happens sometimes in all my dialogs.
Do you have any input as to what could be wrong?
EDIT:
When debugging this I've added breakpoints every time it calls context.Call. When my issue arises it stops hitting these breakpoints afterwards. Could this be a side-effect of some DI or something? This is my DI code:
Conversation.UpdateContainer(builder =>
{
builder.RegisterModule(new DialogModule());
builder.RegisterModule(new ReflectionSurrogateModule());
builder.RegisterModule(new DialogModule_MakeRoot());
builder.RegisterModule(new AzureModule(Assembly.GetExecutingAssembly()));
var store = new TableBotDataStore(ConfigurationManager.ConnectionStrings["StorageConnectionString"].ConnectionString);
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();
builder.RegisterType<BasicLuisDialog>().As<LuisDialog<DialogResult>>().InstancePerDependency();
});
I think I finally found the problem. In my code I had implemented a helper method in a static class that would send a typing response and wait a certain amount of time. Seeing as the context was passed into this static method it seems that this was causing some issues.
After changing the method to an extension method of the LuisDialog I no longer have this issue.
I would appreciate if anyone can expand on why this might have been a problem.
EDIT: The method in question:
public static async Task StartTyping(IDialogContext context, int sleep)
{
var typingMsg = context.MakeMessage();
typingMsg.Type = ActivityTypes.Typing;
await context.PostAsync(typingMsg);
await Task.Delay(sleep);
}
I faced a very similar issue and while moving the typing sending into a base class from a static helper class as Frederik did help to highly reduce the number of times the problem occured, the final solution was this: https://github.com/Microsoft/BotBuilder/issues/4477
In short, I had to downgrade the bot-related NuGet packages (Microsoft.Bot.Builder, Microsoft.Bot.Builder.History, Microsoft.Bot.Connector) to 3.13.1 and the issue disappeared.
since in [LuisIntent("ConcertCity")] you are using context.Done() so the current dialog gets exit from the stack. This is why the next message is being handled by the previous dialog or the message controller where the 'None' intent is being called and you are getting this response
reply.Text = "I'm not sure what you mean \uD83E\uDD14<br/>Which Grøn Koncert would you like to attend?";
You should not do context.Done() every places, this should only be called when you have to go to the previous dialog on the stack.
I am having issues with testing Login Controller using IdentityServer4. It throws the following error:
{System.Net.Http.WinHttpException (0x80072EFD): A connection with the server could not be established
I am trying to generate the access Token using ResourceOwnerPassword, for which I have implemented IResourceOwnerPasswordValidator. I get the error in UserAccessToken.cs class when I call the RequestResourcePasswordAsync.
I am pretty sure it is because of the handler. Because if I use a handler in my test class and call the TokenClient with that handler I do get access Token but then I cannot test my Login Controller.
LoginController.cs
[HttpPost]
public async Task<IActionResult> Login([FromBody]LoginViewModel user)
{
var accessToken = await UserAccessToken.GenerateTokenAsync(user.Username, user.Password);
var loginToken = JsonConvert.DeserializeObject(accessToken);
return Ok(loginToken);
}
UserAccessToken.cs
public async Task<string> GenerateTokenAsync(string username, string password)
{
var tokenUrl = "http://localhost:5000/connect/token";
var tokenClient = new TokenClient(tokenUrl,"ClientId","ClientPassword");
var tokenResponse = await tokenClient.RequestResourceOwnerPasswordAsync(username, password, SecurityConfig.PublicApiResourceId);
if (tokenResponse.IsError)
{
throw new AuthenticationFailedException(tokenResponse.Error);
}
return tokenResponse.Json.ToString();
}
TestClass.cs
[Fact]
public async Task Login()
{
var client = _identityServer.CreateClient();
var data = new StringContent(JsonConvert.SerializeObject(new LoginViewModel { Username = "1206", Password = "5m{F?Hk92/Qj}n7Lp6" }), Encoding.UTF8, "application/json");
var dd = await client.PostAsync("http://localhost:5000/login", data);
var ss = dd;
}
IdentityServerSetup.cs //Integration Test Setup
public class IdentityServerSetup
{
private TestServer _identityServer;
private const string TokenEndpoint = "http://localhost:5000/connect/token";
public HttpMessageHandler _handler;
//IF I use this code I do get a AccessToken
public async Task<string> GetAccessTokenForUser(string userName, string password, string clientId, string clientSecret, string apiName = "integrapay.api.public")
{
var client = new TokenClient(TokenEndpoint, clientId, clientSecret, innerHttpMessageHandler: _handler);
var response = await client.RequestResourceOwnerPasswordAsync(userName, password, apiName);
return response.AccessToken;
}
}
Well, you have already answered the question yourself: The problem is with the HttpHandler the TokenClient uses. It should use the one provided by the TestServer to successfully communicate with it instead of doing actual requests to localhost.
Right now, UserAccessToken requires a TokenClient. This is a dependency of your class, so you should refactor the code to pass in a TokenClient instead of generating it yourself. This pattern is called Dependency Injection and is ideal for cases like yours, where you might have different requirements in your tests than in your production setup.
You could make the code look like this:
UserAccessToken.cs
public class UserAccessToken
{
private readonly TokenClient _tokenClient;
public UserAccessToken(TokenClient tokenClient)
{
_tokenClient = tokenClient;
}
public async Task<string> GenerateTokenAsync(string username, string password)
{
var tokenUrl = "http://localhost:5000/connect/token";
var tokenResponse = await _tokenClient.RequestResourceOwnerPasswordAsync(username, password, SecurityConfig.PublicApiResourceId);
if (tokenResponse.IsError)
{
throw new AuthenticationFailedException(tokenResponse.Error);
}
return tokenResponse.Json.ToString();
}
}
TestHelpers.cs
public static class TestHelpers
{
private static TestServer _testServer;
private static readonly object _initializationLock = new object();
public static TestServer GetTestServer()
{
if (_testServer == null)
{
InitializeTestServer();
}
return _testServer;
}
private static void InitializeTestServer()
{
lock (_initializationLock)
{
if (_testServer != null)
{
return;
}
var webHostBuilder = new WebHostBuilder()
.UseStartup<IntegrationTestsStartup>();
var testServer = new TestServer(webHostBuilder);
var initializationTask = InitializeDatabase(testServer);
initializationTask.ConfigureAwait(false);
initializationTask.Wait();
testServer.BaseAddress = new Uri("http://localhost");
_testServer = testServer;
}
}
}
IntegrationTestsStartup.cs
public class IntegrationTestsStartup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<TokenClient>(() =>
{
var handler = TestUtilities.GetTestServer().CreateHandler();
var client = new TokenClient(TokenEndpoint, clientId, clientSecret, innerHttpMessageHandler: handler);
return client;
};
services.AddTransient<UserAccessToken>();
}
}
LoginController.cs
public class LoginController : Controller
{
private readonly UserAccessToken _userAccessToken;
public LoginController(UserAccessToken userAccessToken)
{
_userAccessToken = userAccessToken;
}
[HttpPost]
public async Task<IActionResult> Login([FromBody]LoginViewModel user)
{
var accessToken = await _userAccessToken .GenerateTokenAsync(user.Username, user.Password);
var loginToken = JsonConvert.DeserializeObject(accessToken);
return Ok(loginToken);
}
}
Here's one of my GitHub projects that makes use of the TestServer class and shows how I'm using it. It's not using IdentityServer4, though.
I am using Moq in .net core(1.1) and having a bit of torrid time understanding this behavior as all the examples on interweb points to the fact the this should work with no issues.
I have already tried with:
Returns(Task.FromResult(...)
Returns(Task.FromResult(...)
ReturnsAsync(...)
Mocking a IHttpClient interface to wrap PostAsync, PutAsync and GetAsync. All of these return an ApiResponse object.
var mockClient = new Mock<IHttpClient>();
Does not work:
mockClient.Setup(x => x.PostAsync(url, JsonConvert.SerializeObject(body), null))
.Returns(Task.FromResult(new ApiResponse()));
PostSync definition:
public async Task<ApiResponse> PostAsync(string url, string body, string authToken = null)
Does work:
mockClient.Setup(x => x.PostAsync(url, JsonConvert.SerializeObject(body), null))
.Returns(Task.FromResult(bool));
PostSync definition:
public async Task<bool> PostAsync(string url, string body, string authToken = null)
Usage:
var api = new ApiService(mockClient.Object);
var response = api.LoginAsync(body.Username, body.Password);
UPDATE
[Fact]
public async void TestLogin()
{
var mockClient = new Mock<IHttpClient>();
mockClient.Setup(x => x.PostAsync(url, JsonConvert.SerializeObject(body), null)).Returns(Task.FromResult(new ApiResponse()));
var api = new ApiService(mockClient.Object);
var response = await api.LoginAsync(body.Username, body.Password);
Assert.IsTrue(response);
}
Return Type:
public class ApiResponse
{
public string Content { get; set; }
public HttpStatusCode StatusCode { get; set; }
public string Reason { get; set; }
}
LoginAsync:
public async Task<bool> LoginAsync(string user, string password)
{
var body = new { Username = user, Password = password };
try
{
var response = await _http.PostAsync(_login_url, JsonConvert.SerializeObject(body), null);
return response .State == 1;
}
catch (Exception ex)
{
Logger.Error(ex);
return false;
}
}
PostAsync:
public async Task<object> PostAsync(string url, string body, string authToken = null)
{
var client = new HttpClient();
var content = new StringContent(body, Encoding.UTF8, "application/json");
var response = await client.PostAsync(new Uri(url), content);
var resp = await response.Result.Content.ReadAsStringAsync();
return new ApiResponse
{
Content = resp,
StatusCode = response.Result.StatusCode,
Reason = response.Result.ReasonPhrase
};
}
Assuming a simple method under test like this based on minimal example provided above.
public class ApiService {
private IHttpClient _http;
private string _login_url;
public ApiService(IHttpClient httpClient) {
this._http = httpClient;
}
public async Task<bool> LoginAsync(string user, string password) {
var body = new { Username = user, Password = password };
try {
var response = await _http.PostAsync(_login_url, JsonConvert.SerializeObject(body), null);
return response.StatusCode == HttpStatusCode.OK;
} catch (Exception ex) {
//Logger.Error(ex);
return false;
}
}
}
The following test works when configured correctly
[Fact]
public async Task Login_Should_Return_True() { //<-- note the Task and not void
//Arrange
var mockClient = new Mock<IHttpClient>();
mockClient
.Setup(x => x.PostAsync(It.IsAny<string>(), It.IsAny<string>(), null))
.ReturnsAsync(new ApiResponse() { StatusCode = HttpStatusCode.OK });
var api = new ApiService(mockClient.Object);
//Act
var response = await api.LoginAsync("", "");
//Assert
Assert.IsTrue(response);
}
The above is just for demonstrative purposes only to show that it can work provided the test is configured properly and exercised based on the expected behavior.
Take some time and review the Moq quick start to get a better understanding of how to use the framework.