Search code examples
c#botframeworkmicrosoft-teams

Proactively sending message through bot using a web app


Solved

Thanks to user MarkMoose I realized that my database table was not storing the full IDs. Future debuggers: If that does not help you solve your problem please look at my conversation with MarkMoose, they walked me through very useful troubleshooting steps.

I am trying to create a Microsoft Teams bot hosted on Azure using Microsoft Bot SDK Version 4.0.

The flow is as follows

  1. Web app alarm triggers. Sends a POST request to the bot that contains (all this data gathered from a previous message from the user)

    • To ID (of recipient user)
    • To Name (of recipient user)
    • From ID (my bot)
    • From Name (my bot)
    • channel ID
    • conversation ID
    • Service URL (of recipient user)
  2. Bot extracts that info from JSON forms new message activity

  3. Bot sends activity to the user

Problem: When the bot attempts to create a ConversationAccount object using the credentials listed above it throws the following error:

Exception caught: Microsoft.Bot.Schema.ErrorResponseException: Operation returned an invalid status code 'BadRequest'

Here is the part of the relevant part of the code.

Note specifically the following lines:

var account = new MicrosoftAppCredentials(botCreds["App ID"], botCreds["App Password"]);
var jwtToken = await account.GetTokenAsync();
ConnectorClient connector = new ConnectorClient(new System.Uri(serviceURL), account);

These are the lines that slightly differ when I found other peoples solutions to my problem. In my current version of the code I am using this posts solution. I have also tried the DelegatingHandler class he created but it throws the same error.

/// <summary>
/// Sends a message to a user or group chat.
/// </summary>
/// <param name="forwardContext">JSON object containing credentials for destination chat.</param>
/// <param name="messageToSend">The message to forward.</param>
/// <returns></returns>
private async Task ForwardMessage(JToken forwardContext, string messageToSend)
{
    // Collect data from JSON input
    var restCmd = forwardContext;
    var toId = (string) restCmd["toId"];
    var toName = (string) restCmd["toName"];
    var fromId = (string) restCmd["fromId"];
    var fromName = (string) restCmd["fromName"];
    var channelId = (string) restCmd["channel"];
    var serviceURL = (string) restCmd["serviceURL"];
    var conversationId = (string) restCmd["conversation"];
    var cred_str = $@"toId: {toId}
    toName: {toName}
    fromId: {fromId}
    fromName: {fromName}
    channelId: {channelId}
    serviceURL: {serviceURL}
    conversationId: {conversationId}";
    _logger.LogInformation(cred_str);
    _logger.LogInformation($"Forwarding the following message to {toName}: {messageToSend}");

    Dictionary<string, string> botCreds = GetBotCredentials();

    // Create relevant accounts
    ChannelAccount userAccount = new ChannelAccount(name: toName, id: toId);
    ChannelAccount botAccount = new ChannelAccount(name: fromName, id: fromId);
    if (!MicrosoftAppCredentials.IsTrustedServiceUrl(serviceURL))
    {
        _logger.LogInformation($"Adding to trusted service urls: {serviceURL}");

        // Register the service URL as trusted
        MicrosoftAppCredentials.TrustServiceUrl(serviceURL);
    }
    MicrosoftAppCredentials.TrustServiceUrl(serviceURL);
    var account = new MicrosoftAppCredentials(botCreds["App ID"], botCreds["App Password"]);
    var jwtToken = await account.GetTokenAsync();
    ConnectorClient connector = new ConnectorClient(new System.Uri(serviceURL), account);

    // Create a new message activity
    IMessageActivity message = Activity.CreateMessageActivity();

    conversationId = (
        await connector
        .Conversations
        .CreateDirectConversationAsync(botAccount, userAccount)).Id;

    // Set relevant message details
    message.From = botAccount;
    message.Recipient = userAccount;
    message.Text = messageToSend;
    message.Locale = "en-Us";
    message.ChannelId = channelId;

    // Create a new converstaion and add it to the message.
    message.Conversation = new ConversationAccount(id: conversationId);
    await connector.Conversations.SendToConversationAsync((Activity) message);
}

And here is my code for gathering the information used above. This function gets called when the user first interacts with the bot.

/// <summary>
    /// Called only when the !setup command is sent to the bot.
    /// Updates the chats info in the DB.
    /// </summary>
    /// <param name="activity">Activity of the message the "!setup" command was sent in.</param>
    /// <returns>True if the update query executed fine.</returns>
    private bool SetupCommand(Activity activity)
    {
        // Connect to the database
        this.database = new DBConnection(serverIP, databaseName, userName, password, _logger);
        this.database.Connect();
        var tableName = "ms_teams_chats";

        // Data gathered from Activity for database.

        // User ID
        string toId = activity.From.Id;

        // User Name
        string toName = activity.From.Name;

        // Bot ID
        string fromId = activity.Recipient.Id;

        // Bot Name
        string fromName = activity.Recipient.Name;

        // Users service URL
        string serviceURL = activity.ServiceUrl;

        // The platform the message came from. Example: 'skype'
        string channelId = activity.ChannelId;
        string conversationID = activity.Conversation.Id;

        string conversationName = activity.Conversation.Name;
        bool isGroupChat = activity.Conversation.IsGroup ?? false;

        string upsertQuery = string.Empty;
        upsertQuery = $@"
            INSERT INTO {tableName} 
                (user_id, user_name, assoc_bot_id, assoc_bot_name, service_url, channel_id, conversation_id, is_group_chat)
            VALUES (
                '{toId}', '{toName}', '{fromId}', '{fromName}', '{serviceURL}', '{channelId}', '{conversationID}', {isGroupChat}
            )
            ON DUPLICATE KEY UPDATE
            user_id = '{toId}',
            user_name = '{toName}',
            assoc_bot_id = '{fromId}',
            assoc_bot_name = '{fromName}',
            service_url = '{serviceURL}',
            channel_id = '{channelId}',
            conversation_id = '{conversationID}',
            is_group_chat = {isGroupChat}
        ";
        try
        {
            this.database.ExecuteNonQuery(upsertQuery);
        }
        catch (System.Exception e)
        {
            _logger.LogError($"Could not update users information. \nError:{e.ToString()}");
            return false;
        }

        return true;
    }

Solution

  • It seems you are struggling with the same issue I had last week. it seems that the CreateDirectConversationAsync does not work in MS Teams as Teams also needs a tennantId. I found a statement about this here: https://github.com/Microsoft/BotBuilder/issues/2944

    the answer mentions a nuget package (Microsoft.Bot.Connector.Teams) that is no longer available in V4 of the SDK. but as I see that you already got a conversationId from your JSON input, this should not be a problem. just use the conversationId you passed in the JSON. if you would do this, your code could look something like:

    private static async Task SendProActiveMessgae()private async Task ForwardMessage(JToken forwardContext, string messageToSend)
    {
        // Collect data from JSON input
        var restCmd = forwardContext;
        var toId = (string) restCmd["toId"];
        var toName = (string) restCmd["toName"];
        var fromId = (string) restCmd["fromId"];
        var fromName = (string) restCmd["fromName"];
        var serviceURL = (string) restCmd["serviceURL"]
        var conversationId = (string) restCmd["conversation"];
    
        var uri = new Uri(serviceURL);
        var appId = "APP ID";
        var appSecret = "APP PASSWORD";
        ConnectorClient connector = new ConnectorClient(uri, appId, appSecret);
    
        var activity = new Activity()
        {
            Type = ActivityTypes.Message,
            From = new ChannelAccount(fromId, fromName),
            Recipient = new ChannelAccount(toId, toName),
            Conversation = new ConversationAccount(false, "personal", conversationId),
            Text = messageToSend
        };
        try
        {
            MicrosoftAppCredentials.TrustServiceUrl(serviceURL);
            await connector.Conversations.SendToConversationAsync(conversationId, activity);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }