The end goal is this:
Ms botduilder sdk, chatbot for Slack.
When the user is added to group chat with bot (ActivityTypes.ConversationUpdate and activity.MembersAdded.Count != 0), I would like to immediately start from collecting data from him using formflow dialog via private messages. But i can't find a way to do that from this point, seems like you need to have some message from user already.
Is that correct and the only workaround is to ask user for some text typed first(or button "lets start")?
I've also tried this solution with resolving dialog stack from proactive examples:
else if (activity.Type == ActivityTypes.ConversationUpdate)
{
if (activity.MembersAdded.Count != 0)
{
using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, activity))
{
var botData = scope.Resolve<IBotData>();
await botData.LoadAsync(CancellationToken.None);
//This is our dialog stack
var stack = scope.Resolve<IDialogStack>();
//interrupt the stack. This means that we're stopping whatever conversation that is currently happening with the user
//Then adding this stack to run and once it's finished, we will be back to the original conversation
var questions = new WelcomePoll();
var myform = new FormDialog<WelcomePoll>(questions, WelcomePoll.BuildForm, FormOptions.PromptInStart, null);
stack.Call(myform, Resume); //GOT "STACK IS EMPTY" EXCEPTION
return new HttpResponseMessage(System.Net.HttpStatusCode.Accepted);
}
}
}
But got System.InvalidOperationException: 'Stack is empty' on stack.Call line. Maybe i need to create stack first but i couldn't find correct method. Thank you.
Disclaimer: my answer a bit more generic than your question. See the launch of a Dialog from ConversationUpdate at the end of the answer
Sending "real" proactive messages is possible in Microsoft Bot Framework but not in every channel.
Important point: when I say "real proactive messages", it is to make a distinction from what Microsoft is calling proactive message
in the documentation (which is sending messages from the bot but to someone it already talks to in the past): here I am talking about sending a message to a customer that never talks to the bot before.
The main pain for sending proactive messages is knowing the ID of the 2 ChannelAccount (= users, that is to say your bot and your end-user) that you will set in the Conversation object.
Those ChannelAccount objects got an ID that depend on the channel you are currently using. For example from what I previously analysed during my work on Bot Framework:
If you want to know how to start a Conversation once you got those IDs, jump to the section at the end.
The advantage of Slack is the possibility to get all the necessary parameters to start a conversation by only knowing a few information and requesting their API. For example you can start a conversation by knowing only:
Then you can use Slack's API to get what the bot really needs.
How to get Slack data?
1 method to call the service, 1 using it to get the right data:
public class SlackService
{
internal static async Task<SlackMembersRootObject> GetSlackUsersList(string slackApiToken, CancellationToken ct)
{
using (var client = new HttpClient())
{
using (var response = await client.GetAsync($"https://slack.com/api/users.list?token=" + $"{slackApiToken}", ct).ConfigureAwait(false))
{
var jsonString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
var rootObject = Newtonsoft.Json.JsonConvert.DeserializeObject<SlackMembersRootObject>(jsonString);
return rootObject;
}
}
}
}
public static async Task StartSlackDirectMessage(string msAppId, string msAppPassword, string slackApiToken, string message, string botSlackUsername, string botName, string destEmailAddress, string destName, string lang, CancellationToken ct)
{
// Getting Slack user (and bot) list
var slackUsersList = await SlackService.GetSlackUsersList(slackApiToken, CancellationToken.None);
// Get Bot SlackId by searching its "username", you can also search by other criteria
var slackBotUser = slackUsersList.Members.FirstOrDefault(x => x.Name == botSlackUsername);
if (slackBotUser == null)
{
throw new Exception($"Slack API : no bot found for name '{botSlackUsername}'");
}
// Get User SlackId by email address
var slackTargetedUser = slackUsersList.Members.FirstOrDefault(x => x.Profile.Email == destEmailAddress);
if (slackTargetedUser != null)
{
// if found, starting the conversation by passing the right IDs
await StartDirectMessage(msAppId, msAppPassword, "https://slack.botframework.com/", "slack", $"{slackBotUser.Profile.Bot_id}:{slackBotUser.Team_id}", botName, $"{slackTargetedUser.Id}:{slackTargetedUser.Team_id}", destName, message, lang, ct);
}
else
{
throw new Exception($"Slack API : no user found for email '{destEmailAddress}'");
}
}
How to start a conversation? Once you know the relevant information about your bot and user (which depend on the channel, once again), you can start your conversation.
Sample method:
private static async Task StartDirectMessage(string msAppId, string msAppPassword, string connectorClientUrl, string channelId, string fromId, string fromName, string recipientId, string recipientName, string message, string locale, CancellationToken token)
{
// Init connector
MicrosoftAppCredentials.TrustServiceUrl(connectorClientUrl, DateTime.Now.AddDays(7));
var account = new MicrosoftAppCredentials(msAppId, msAppPassword);
var connector = new ConnectorClient(new Uri(connectorClientUrl), account);
// Init conversation members
ChannelAccount channelFrom = new ChannelAccount(fromId, fromName);
ChannelAccount channelTo = new ChannelAccount(recipientId, recipientName);
// Create Conversation
var conversation = await connector.Conversations.CreateDirectConversationAsync(channelFrom, channelTo);
// Create message Activity and send it to Conversation
IMessageActivity newMessage = Activity.CreateMessageActivity();
newMessage.Type = ActivityTypes.Message;
newMessage.From = channelFrom;
newMessage.Recipient = channelTo;
newMessage.Locale = (locale ?? "en-US");
newMessage.ChannelId = channelId;
newMessage.Conversation = new ConversationAccount(id: conversation.Id);
newMessage.Text = message;
await connector.Conversations.SendToConversationAsync((Activity)newMessage);
}
So back to your context:
(ActivityTypes.ConversationUpdate and activity.MembersAdded.Count != 0)
: in this ConversationUpdate, you can get the userId and your botId. Then you start a new Conversation (using code above), then create a fake message that you don't send and get the IDialogTask from it: you will be able to launch your DialogExample:
if (activity.MembersAdded.Count != 0)
{
// We have to create a new conversation between Bot and AddedUser
#region Conversation creation
// Connector init
MicrosoftAppCredentials.TrustServiceUrl(activity.ServiceUrl, DateTime.Now.AddDays(7));
var account = new MicrosoftAppCredentials("yourMsAppId", "yourMsAppPassword");
var connector = new ConnectorClient(new Uri(activity.ServiceUrl), account);
// From the conversationUpdate message, Recipient = bot and From = User
ChannelAccount botChannelAccount = new ChannelAccount(activity.Recipient.Id, activity.Recipient.Name);
ChannelAccount userChannelAccount = new ChannelAccount(activity.From.Id, activity.From.Name);
// Create Conversation
var conversation = await connector.Conversations.CreateDirectConversationAsync(botChannelAccount, userChannelAccount);
#endregion
// Then we prepare a fake message from bot to user, mandatory to get the working IDialogTask. This message MUST be from User to Bot, if you want the following Dialog to be from Bot to User
IMessageActivity fakeMessage = Activity.CreateMessageActivity();
fakeMessage.From = userChannelAccount;
fakeMessage.Recipient = botChannelAccount;
fakeMessage.ChannelId = activity.ChannelId;
fakeMessage.Conversation = new ConversationAccount(id: conversation.Id);
fakeMessage.ServiceUrl = activity.ServiceUrl;
fakeMessage.Id = Guid.NewGuid().ToString();
// Then use this fake message to launch the new dialog
using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, fakeMessage))
{
var botData = scope.Resolve<IBotData>();
await botData.LoadAsync(CancellationToken.None);
//This is our dialog stack
var task = scope.Resolve<IDialogTask>();
//interrupt the stack. This means that we're stopping whatever conversation that is currently happening with the user
//Then adding this stack to run and once it's finished, we will be back to the original conversation
var dialog = new DemoDialog();
task.Call(dialog.Void<object, IMessageActivity>(), null);
await task.PollAsync(CancellationToken.None);
//flush dialog stack
await botData.FlushAsync(CancellationToken.None);
}
}