How can a bot start a thread in Slack - c#

I am trying to have my bot framework bot reply to a user by starting a thread. This way I can keep who the bot is talking to when in a channel with many people straight.
According to the slack documentation what I need to do is set the thread_ts property to the ts property sent to my bot. I have tried a few things and have been unable to accomplish this. This is the most concise example I have:
var reply = (Activity)activity;
reply = reply.CreateReply("reply");
reply.ChannelData = JObject.Parse($"{{thread_ts:'{ts}'}}");
await context.PostAsync(reply);
This is not working for me.

You will need to set the text in the ChannelData in order for your bot to reply in the thread. Right now you are setting it in your activity
reply = reply.CreateReply("reply");
All you need to do is this:
reply.ChannelData = JObject.Parse($"{{text:'reply', thread_ts:'{ts}'}}");
here is a full working method from a dialog:
public async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> argument)
{
var activity = await argument;
var ts = activity.ChannelData?.SlackMessage?.thread_ts
?? activity.ChannelData?.SlackMessage?.ts
?? activity.ChannelData?.SlackMessage["event"].thread_ts
?? activity.ChannelData?.SlackMessage["event"].ts;
var reply = (Activity)activity;
reply = reply.CreateReply();
reply.ChannelData = JObject.Parse($"{{text:'reply', thread_ts:'{ts}'}}");
await context.PostAsync(reply);
}

Related

MS Teams Bot Create Group Conversation

I have added my app to specific team in MS Teams and added my bot there. Now, I am trying to create a chat between specific persons who were added to the 'General' channel using bot to communicate with only certain persons. For now, I am testing it by typing a command to that channel and processing activity like:
public async Task Handle(CancellationToken cancellationToken)
{
// Get all members who were added to that channel
var members = new List<TeamsChannelAccount>();
string continuationToken = null;
do
{
var currentPage = await TeamsInfo.GetPagedMembersAsync(_turnContext, 100, continuationToken, cancellationToken);
continuationToken = currentPage.ContinuationToken;
members.AddRange(currentPage.Members);
}
while (continuationToken != null);
// Start a new conversation with all of them
var createdConversation = await _connectorClient.StartConversation(members,
_turnContext.Activity.ServiceUrl,
_turnContext.Activity.Conversation.TenantId,
MessageFactory.Text("test proactive mssages"),
_turnContext.Activity.Recipient);
}
where StartConversation is:
public async Task<ConversationResourceResponse> StartConversation(List<TeamsChannelAccount> members,
string serviceUrl,
string tenantId,
Activity activity,
ChannelAccount botAccount,
CancellationToken cancellationToken = default)
{
AppCredentials.TrustServiceUrl(serviceUrl, DateTime.MaxValue);
var connectorClient = new ConnectorClient(new Uri(serviceUrl), new MicrosoftAppCredentials(_botId, _botPassword));
try
{
var conversationParams = new ConversationParameters(
true,
botAccount,
members.Select(x => new ChannelAccount(x.Id, x.Name)).ToList(),
"Test proactive group message",
activity,
new TeamsChannelData()
{
Tenant = new TenantInfo(tenantId)
}
, tenantId);
return await connectorClient.Conversations.CreateConversationAsync(conversationParams, cancellationToken);
}
catch (Exception ex)
{
throw;
}
}
When I test this code, I receive the exception with the following message: Operation returned an invalid status code 'BadRequest' with Error Code "BadSyntax" and Message "Incorrect conversation creation parameters".
I'm not sure if that possible to create such conversations in MS Teams or not. The most confusing part for me is isGroup argument in ConversationParameters - doesn't that indicate that created conversation should be group chat? I've tried to create direct conversation with my account by setting isGroup to false and leaving only my TeamsChannelAccount in members and that worked
I am using Microsoft.Bot.Builder.Integration.AspNet.Core 4.14.1
Thanks for reading and help!
We tried it at our end and faced the same issue. Maybe you can try creating groups using deeplink https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/build-and-test/deep-links#deep-linking-to-a-chat
Thanks

Botframework v4 resume conversation outside of the waterfall dialog flow

Botframework V4 webchat
My bot displays a SigninCard to the user, which when clicked user is redirected to an external website where the user will enter login credentials. This external website will perform a service call to a separate endpoint in my bot controller with a token. Now I want to make the bot to display a message to the user, and let the user follow through the rest of the conversation.
For all intents and purposes, it needs to be as same as displaying a proactive message to the user, and jumping to the next waterfall step afterwards.
This is my code so far.
1st approach
Conversation reference information is passed from here.
private async Task<DialogTurnResult> SigninStepStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var convRef = stepContext.Context.Activity.GetConversationReference();
var stateCol = System.Web.HttpUtility.ParseQueryString(string.Empty);
stateCol["userId"] = convRef.User.Id;
stateCol["botId"] = convRef.Bot.Id;
stateCol["conversationId"] = convRef.Conversation.Id;
stateCol["serviceUrl"] = convRef.ServiceUrl;
stateCol["channelId"] = convRef.ChannelId;
SigninCard signinCard = new SigninCard
{
Text = "Please sign-in to continue",
Buttons = new List<CardAction> {
new CardAction(ActionTypes.Signin, "Sign-in",
value: "http://some-external-url?state="+ System.Web.HttpUtility.UrlEncode(stateCol.ToString())) },
};
var reply = stepContext.Context.Activity.CreateReply();
reply.Attachments = new List<Attachment>
{
signinCard.ToAttachment(),
};
return await stepContext.PromptAsync(
"testprompt",
new PromptOptions
{
Prompt = reply,
},
cancellationToken);
}
Conversation reference information is received here. This endpoint is invoked by an external website.
[HttpGet]
public async Task<HttpResponseMessage> Callback(string state)
{
// Resume conversation
var stateCol = System.Web.HttpUtility.ParseQueryString(state);
ConnectorClient connector = new ConnectorClient(new Uri(stateCol["serviceUrl"]));
IMessageActivity newMessage = Activity.CreateMessageActivity();
newMessage.From = new ChannelAccount(stateCol["userId"]);
newMessage.Conversation = new ConversationAccount(id: stateCol["conversationId"]);
newMessage.Recipient = new ChannelAccount(stateCol["botId"]);
newMessage.ChannelId = stateCol["channelId"];
newMessage.Text = "hello user";
newMessage.ReplyToId = stateCol["botId"];
await connector.Conversations.SendToConversationAsync((Activity)newMessage);
//...
}
This piece of code correctly posts "Hello user" message to conversation flow, as a message coming from the bot. I tried connector.Conversations.ReplyTo method, also tried swapping botid and userid values to make it the user replying to the bot. But none of the cases made the waterfall step to jump to the next step in the flow like manually entering a reply text through emulator.
I followed the Idea 2 scenario of this question : https://stackoverflow.com/a/48977832 which is on botfraemwork v3. In botframework v3, it has a resume method as indicated in this answer. But I cannot find this method or anything that makes the conversation to resume, in botframework v4.
Conversation.ResumeAsync(reference, message);
Wondering if there is any special kind of activity that I can send, end the current prompt and make it start the next waterfall step.
2nd approach
On the side, I also tried to follow the ProactiveBot sample code in Botframework 4, to see if that approach will cause the waterfall flow to jump to the next step in the line.
[HttpGet]
public async Task<HttpResponseMessage> Callback(string code, string state, string session_state = null)
{
// Resume conversation
var stateCol = System.Web.HttpUtility.ParseQueryString(state);
MicrosoftAppCredentials.TrustServiceUrl(stateCol["serviceUrl"]);
ConversationReference cr = new ConversationReference();
cr.Bot = new ChannelAccount(stateCol["botId"]);
cr.User = new ChannelAccount(stateCol["userId"]);
cr.ChannelId = stateCol["channelId"];
cr.Conversation = new ConversationAccount(id: stateCol["conversationId"]);
cr.ServiceUrl = stateCol["serviceUrl"];
var msg = cr.GetContinuationActivity();
msg.Text = "login_succeed";
await ((BotAdapter)_adapter).ContinueConversationAsync(_appId, cr, BotCallback, default(CancellationToken));
//..
}
But at ContinueConversationAsync it threw below error, even though I am not using any clientSecret parameter anywhere. Not sure if this is because of using the Signin card to invoke the external web site url in the first code block above.
An unhandled exception occurred while processing the request.
ArgumentNullException: Value cannot be null. (Parameter 'clientSecret')
Microsoft.IdentityModel.Clients.ActiveDirectory.ClientCredential..ctor(string clientId, string clientSecret)

How can I proactively trigger a dialog from a bot without a turnContext?

I have a Teams bot that can send proactive messages to a user via webAPI. I can get a ConnectorClient from the Microsoft.Bot.Connector namespace, and then from there I can identify the relevant conversation, and call SendToConversationAsync to message a user.
If I want to use this to initiate a dialog though, the challenge seems to be that I don't have a TurnContext to reference. I found a post here that seemed promising, but that still depends on having a dialog turn running. Ideally, I'd like to be able to do this via the ConnectorClient reference that I already have, and trying with a null TurnContext doesn't seem to work. For example, trying this:
var dialogState = _accessors.ConversationState.CreateProperty<DialogState>(nameof(DialogState));
var dialogSet = new DialogSet(dialogState);
dialogSet.Add(new MyDialog());
DialogContext dc = await dialogSet.CreateContextAsync(turnContext, cancellationToken);
var turnResult = await dc.BeginDialogAsync("MyDialog");
Throws an exception if turnContext is null.
This answer here has some promising ideas too, suggesting faking an incoming event, but I can do something like CreateInvokeActivity(), but sending that to the conversation throws an exception. I'm also not sure how to trigger the pipeline to get the message through in the same process without going as far up as using an HTTPCLient to POST the raw message (which requires getting a token I believe). The bot already has a 1:1 conversation with the user, but I'd like to have this initiate a dialog if possible. Is there a way to have the ConnectorClient begin a dialog proactively, or trigger an invoke to the bot pipeline programmatically to allow it to kick off there?
I managed to figure out a way to do this, but it's probably not an ideal scenario. I wanted to start a dialog from the API, specifically an authentication dialog that gets a user's OAuth token for accessing graph. If the user is signed in, the token is returned immediately, and if not, they get a sign in prompt. I have something like this in my bot code (edited for brevity):
public static async Task<string> GetTokenAsync(ITurnContext turnContext, CancellationToken cancellationToken)
{
var dialogState = _accessors.ConversationState.CreateProperty<DialogState>(nameof(DialogState));
var dialogSet = new DialogSet(dialogState);
dialogSet.Add(new AuthDialog());
DialogContext dc = await dialogSet.CreateContextAsync(turnContext, cancellationToken);
var turnResult = await dc.BeginDialogAsync("AuthDialog");
await _accessors.ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
if(turnResult.Status== DialogTurnStatus.Waiting)
{
_log.Debug("Got login request for user-waiting for response");
return string.Empty;
}
else if(turnResult.Result is TokenResponse)
{
return ((TokenResponse)turnResult.Result).Token;
}
return null;
}
This creates the dialog and, if possible, returns the token. In my webAPI, I have something like this to invoke it proactively:
string conversationID = "CONV_ID_FROM_STATE";
var members = await m_client.Conversations.GetConversationMembersAsync(conversationID);
BotFrameworkAdapter b = new BotFrameworkAdapter(new SimpleCredentialProvider("BOT ID", "BOT_SECRET"));
var message = Activity.CreateMessageActivity();
message.Text = "login";
message.From = new ChannelAccount(members[0].Id);
message.Conversation = new ConversationAccount(id: conversationID, conversationType: "personal", tenantId: :BOT_TENANT_ID);
message.ChannelId = "msteams";
TurnContext t = new TurnContext(b, (Activity)message);
ClaimsIdentity id = new ClaimsIdentity();
id.AddClaim(new Claim("aud", "BOT_ID"));
t.TurnState.Add("BotIdentity", id);
t.TurnState.Add("Microsoft.Bot.Builder.BotAdapter.OAuthScope", "https://api.botframework.com");
t.TurnState.Add("Microsoft.Bot.Connector.IConnectorClient", m_client);
string token = await myBot<AuthDialog>.GetTokenAsync(t, default);
At this point, if the token is an empty string, the user hasn't signed in, but otherwise it should be a valid token to make graph calls with. I've tested this with a few new accounts, and it seems to work, so I'm calling that a win for now. If there's something that's fundamentally busted here though, please comment.
You can create a turn context from a conversion reference using ContinueConversationAsync. Please refer to the docs and the sample for more information.

c# - Cannot get Skype Bot to respond to the proper event

I am using Visual Studio to create a Skype bot based on their Bot Builder SDK and using the Skype emulator. (I am using this page) I successfully got the bot to connect and receive regular text messages, and it responds properly with:
You sent [message] which was [length] characters
However, I tried adding an event to fire when a user is added, and it should just send "Welcome".
public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
if (activity.Type == ActivityTypes.Message)
{
ConnectorClient connector = new ConnectorClient(new System.Uri(activity.ServiceUrl));
int length = (activity.Text ?? string.Empty).Length;
Activity reply = activity.CreateReply($"You sent {activity.Text} which was {length} characters");
await Conversation.SendAsync(activity, () => new Dialogs.RootDialog());
}
else
{
HandleSystemMessage(activity);
}
var response = Request.CreateResponse(HttpStatusCode.OK);
return response;
}
private Activity HandleSystemMessage(Activity message)
{
//The other if statements are the rest of the activity types
if (message.Type == ActivityTypes.ConversationUpdate)
{
// Handle conversation state changes, like members being added and removed
// Use Activity.MembersAdded and Activity.MembersRemoved and Activity.Action for info
// Not available in all channels
Activity reply = message.CreateReply("Welcome");
}
This is the code I have currently. I tried adding the same Connector and await lines to the else statement on line 17, but that just makes the bot respond with
You sent which was 0 characters
If any other info is needed to fix this I'll be happy to provide it.
Any help is appreciated!
EDIT: The code I currently have does not respond with anything. It sees the ConversationUpdate event and does nothing with it
I've never used the Skype SDK but I imagine it would look something like this if you did it properly:
public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
var connector = new ConnectorClient(new Uri(activity.ServiceUrl));
Activity reply = null;
if (activity.Type == ActivityTypes.Message)
{
int length = (activity.Text ?? string.Empty).Length;
reply = activity.CreateReply($"You sent {activity.Text} which was {length} characters");
}
else if (activity.Type == ActivityTypes.ConversationUpdate)
{
reply = message.CreateReply("Welcome");
}
if (reply != null)
{
await connector.Conversations.SendToConversationAsync((Activity)reply);
}
}
I'd suggest you take a tutorial in C# and maybe read the skype documentation?
https://learn.microsoft.com/en-us/bot-framework/dotnet/bot-builder-dotnet-connector
The above still may not work, if you take a look at the docs link I posted you may need to start a conversation if one does not already exist using the CreateDirectConversationAsync method.

Microsoft DirectLine API to bot does not work

So to explain my problem, I have to give you the context.
I got a Bot built with microsoft bot framework deployed on slack. Now it can happen these "events" on my backend that the bot communicates with. When a event occurs, I want to notify my bot of it and then let it send a message to all of it's conversations that something has happend. So basicly:
Backend>Microserivce>Bot>users
To do this I have to store all conversations in my backend, which I do in a database there. When a event happends, the backend will post an activity to the bot with all the conversations(basicly their id's) and the event it should show them.
So in essence my backend need to post a message to my bot.
For doing this I found the microsoft directline api which acts as a middleman here, a more abstract way to talk with the bot. The problem is that I don't know how to do it. I followed microsofts own tutorial but it doesn't seem to work for me:
This is the endpoint that my backend uses to notify the bot. "content" contains conversations and events as a json formated string.
[HttpPost]
[Route("conversationsEvents")]
public HttpResponseMessage PostConversationsEvents([FromBody]string content)
{
NotifyBot.Notify(content);
return Request.CreateResponse(HttpStatusCode.NoContent );
}
NotifyBot.Notify(content) looks like this:
private static async Task StartBotConversation( string contents)
{
string directLineSecret = "secret";
string fromUser = "microserviceNotifyEndpoint";
Activity activity = new Activity
{
From = new ChannelAccount(fromUser),
Text = contents,
Type = ActivityTypes.Event
};
DirectLineClient client = new DirectLineClient(directLineSecret);
var conversation = await client.Conversations.StartConversationAsync();
await client.Conversations.PostActivityAsync(conversation.ConversationId, activity);
}
Basicly the execution get's stuck at var conversation = await client.Conversations.StartConversationAsync(); , it just waits forever.
I tried changing it to var conversation = await client.Conversations.StartConversationAsync().ConfigureAwait(continueOnCapturedContext: false);´the execution goes on but the activity doesn't seem to get posted.
I'm not sure why the call to .StartConversationAsync() would freeze in your case. Maybe you haven't enabled the Direct Line channel on dev.botframework.com/bots? Nonetheless, as pointed out by Sergey, the Direct Line is a Channel and not a means for communicating with your bot on other channels.
Check out the Connector Client: bot-builder-dotnet-connector
Here is a static example of using it to proactively send a message to a user from a bot: MicrosoftDX/botFramework-proactiveMessages - sample: ConversationStarter.cs
pertinent code from sample:
public static async Task Resume(string conversationId,string channelId)
{
var userAccount = new ChannelAccount(toId,toName);
var botAccount = new ChannelAccount(fromId, fromName);
var connector = new ConnectorClient(new Uri(serviceUrl));
IMessageActivity message = Activity.CreateMessageActivity();
if (!string.IsNullOrEmpty(conversationId) && !string.IsNullOrEmpty(channelId))
{
message.ChannelId = channelId;
}
else
{
conversationId = (await connector.Conversations.CreateDirectConversationAsync( botAccount, userAccount)).Id;
}
message.From = botAccount;
message.Recipient = userAccount;
message.Conversation = new ConversationAccount(id: conversationId);
message.Text = "Hello, this is a notification";
message.Locale = "en-Us";
await connector.Conversations.SendToConversationAsync((Activity)message);
}
The serviceUrl, the channelId, conversationId, toId, fromId, etc are cached from previous communication by the user to the bot (these are statically stored in this example, so only work for one user). This example shows how it is possible to proactively send a message to a user from a bot. The Direct Line api is not required.
You don't need to use DirectLine, it is designed for creating alternative bot UIs.
To implementing what your want, you may try the following:
First, you need to store users addresses to whom you want to send the messages. It my be done by storing the ResumptionCookie of a user last message in your backend database.
var state = new ResumptionCookie(message).GZipSerialize();
When your PostConversationsEvents is called, you may resume the conversation at the latest point with each users.
var resumptionCookie = ResumptionCookie.GZipDeserialize(state);
var message = resumptionCookie.GetMessage();
message.Text = content;
await Conversation.ResumeAsync(resumptionCookie, message);
It is not the only solution. As I said, in this case you just resumed the conversation with the user at the latest point. Another solution is to save the user address (user the same ResumptionCookie class) but start the conversation when you need to:
var resumptionCookie = ResumptionCookie.GZipDeserialize(state);
var message = cookie.GetMessage();
ConnectorClient client = new ConnectorClient(new Uri(message.ServiceUrl));
var conversation = await
client.Conversations.CreateDirectConversationAsync(message.Recipient, message.From);
message.Conversation.Id = conversation.Id;
var newMessage = message.CreateReply();
newMessage.Text = content;
await client.Conversations.SendToConversationAsync(newMessage);
See more details on BotFramework documentation.

Categories