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.
UserIsAuthenticated
text is binded toUserAuthenticated
intent. In the worst case it would enter in the empty[LuisIntent("")]
intent. Instead it won't call any of those intents. – danyolgiax