Dejan Janjušević
Dejan Janjušević

Reputation: 3230

Operation returned an invalid status code 'Forbidden' when calling GetConversationMembersAsync for 1:1 private conversation

Using the "Hello World" Microsoft Teams application sample from here: https://github.com/OfficeDev/msteams-samples-hello-world-csharp

Trying to get a list of participants in a personal Microsoft Teams 1:1 chat after an action command from a messaging extension is invoked. Specifically, I need the e-mail address of the other participant, where I am the first participant.

This is the code from the messages controller:

[BotAuthentication]
public class MessagesController : ApiController
{
    [HttpPost]
    public async Task<HttpResponseMessage> Post([FromBody] Activity activity)
    {
        using (var connector = new ConnectorClient(new Uri(activity.ServiceUrl)))
        {
            if (activity.IsComposeExtensionQuery())
            {
                // Invoke the command handler
                var response = await MessageExtension.HandleMessageExtensionQuery(connector, activity).ConfigureAwait(false);
                return response != null
                    ? Request.CreateResponse<ComposeExtensionResponse>(response)
                    : new HttpResponseMessage(HttpStatusCode.OK);
            }
            else
            {
                await EchoBot.EchoMessage(connector, activity);
                return new HttpResponseMessage(HttpStatusCode.Accepted);
            }
        }
    }
}

The code from MessageExtension.HandleMessageExtensionQuery is as follows:

    public static async Task<ComposeExtensionResponse> HandleMessageExtensionQuery(ConnectorClient connector, Activity activity)
    {
        var query = activity.GetComposeExtensionQueryData();
        if (query == null)
        {
            return null;
        }

        // Exception thrown here - error 403, there is no additional data except "Operation returned an invalid status code 'Forbidden'"
        var members = await connector.Conversations.GetConversationMembersAsync(activity.Conversation.Id);

        var handler = GetCommandHandler(query.CommandId); // Gets a handler based on the command, irrelevant for this question
        if (handler == null)
        {
            return null;
        }

        return await handler.HandleCommand(query, members); // Should handle the command, but never comes here if we are in a 1:1 conversation
    }

The call to GetConversationMembersAsync fails with the following message: Operation returned an invalid status code 'Forbidden' if the command is invoked from a 1:1 personal conversation between two people.

The call doesn't fail if invoked from a group channel.

How to get the list of participants of a 1:1 personal conversation? Do I have to authenticate my user through the bot in order to do that, or do I have to grant my bot some particular permissions? Does my account need to have some particular permissions in order to do this?

EDIT - Added the app manifest

{
    "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json",
    "manifestVersion": "1.5",
    "version": "1.0.0",
    "id": "1ce95960-0417-4469-ab77-5052758a4e7e",
    "packageName": "com.contoso.helloworld",
    "developer": {
        "name": "Contoso",
        "websiteUrl": "https://8112abe3.ngrok.io",
        "privacyUrl": "https://8112abe3.ngrok.io/privacy-policy",
        "termsOfUseUrl": "https://8112abe3.ngrok.io/terms-service"
    },
    "icons": {
        "color": "color.png",
        "outline": "outline.png"
    },
    "name": {
        "short": "Hello World",
        "full": "Hello World App"
    },
    "description": {
        "short": "Hello World App for Microsoft Teams",
        "full": "This sample app provides a very simple app. You can extend this to add more content and capabilities."
    },
    "accentColor": "#60A18E",
    "configurableTabs": [
        {
            "configurationUrl": "https://526d7c43.ngrok.io/configure",
            "canUpdateConfiguration": true,
            "scopes": [
                "team",
                "groupchat"
            ]
        }
    ],
    "staticTabs": [
        {
            "entityId": "com.contoso.helloworld.hellotab",
            "name": "Hello Tab",
            "contentUrl": "https://8112abe3.ngrok.io/hello",
            "websiteUrl": "https://8112abe3.ngrok.io/hello",
            "scopes": [
                "personal"
            ]
        }
    ],
    "bots": [
        {
            "botId": "bfbcb607-5c29-4438-85a5-15e63fb0b273",
            "scopes": [
                "personal",
                "team",
                "groupchat"
            ],
            "supportsFiles": false,
            "isNotificationOnly": false
        }
    ],
    "composeExtensions": [
        {
            "botId": "bfbcb607-5c29-4438-85a5-15e63fb0b273",
            "canUpdateConfiguration": true,
            "commands": [
                {
                    "id": "getRandomText",
                    "type": "query",
                    "title": "Get random text",
                    "description": "",
                    "initialRun": true,
                    "fetchTask": false,
                    "context": [
                        "commandBox",
                        "compose",
                        "message"
                    ],
                    "parameters": [
                        {
                            "name": "cardTitle",
                            "title": "Subject",
                            "description": "",
                            "inputType": "text"
                        }
                    ]
                }
            ]
        }
    ],
    "permissions": [
        "identity",
        "messageTeamMembers"
    ],
    "validDomains": [
        "8112abe3.ngrok.io"
    ]
}

EDIT 2 - After trying, based on sample 51 - TeamsMessagingExtensionsAction

As suggested, I tried with the sample 51 called "TeamsMessagingExtensionsAction" The code is:

MicrosoftAppCredentials.TrustServiceUrl(turnContext.Activity.ServiceUrl);
var members = (await turnContext.TurnState.Get<IConnectorClient>().Conversations.GetConversationMembersAsync(
    turnContext.Activity.Conversation.Id).ConfigureAwait(false)).ToList();

The exception along with the stack trace:

Microsoft.Bot.Schema.ErrorResponseException: Operation returned an invalid status code 'Forbidden'
   at Microsoft.Bot.Connector.Conversations.GetConversationMembersWithHttpMessagesAsync(String conversationId, Dictionary`2 customHeaders, CancellationToken cancellationToken) in d:\a\1\s\libraries\Microsoft.Bot.Connector\Conversations.cs:line 1462
   at Microsoft.BotBuilderSamples.Bots.TeamsMessagingExtensionsActionBot.ShareMessageCommand(ITurnContext`1 turnContext, MessagingExtensionAction action) in D:\Visual Studio Projects\botbuilder-samples\samples\csharp_dotnetcore\51.teams-messaging-extensions-action\Bots\TeamsMessagingExtensionsActionBot.cs:line 68
   at Microsoft.BotBuilderSamples.Bots.TeamsMessagingExtensionsActionBot.OnTeamsMessagingExtensionSubmitActionAsync(ITurnContext`1 turnContext, MessagingExtensionAction action, CancellationToken cancellationToken) in D:\Visual Studio Projects\botbuilder-samples\samples\csharp_dotnetcore\51.teams-messaging-extensions-action\Bots\TeamsMessagingExtensionsActionBot.cs:line 29
   at Microsoft.Bot.Builder.Teams.TeamsActivityHandler.OnTeamsMessagingExtensionSubmitActionDispatchAsync(ITurnContext`1 turnContext, MessagingExtensionAction action, CancellationToken cancellationToken) in d:\a\1\s\libraries\Microsoft.Bot.Builder\Teams\TeamsActivityHandler.cs:line 201
   at Microsoft.Bot.Builder.Teams.TeamsActivityHandler.OnInvokeActivityAsync(ITurnContext`1 turnContext, CancellationToken cancellationToken) in d:\a\1\s\libraries\Microsoft.Bot.Builder\Teams\TeamsActivityHandler.cs:line 88
   at Microsoft.Bot.Builder.Teams.TeamsActivityHandler.OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken) in d:\a\1\s\libraries\Microsoft.Bot.Builder\Teams\TeamsActivityHandler.cs:line 39
   at Microsoft.Bot.Builder.BotFrameworkAdapter.TenantIdWorkaroundForTeamsMiddleware.OnTurnAsync(ITurnContext turnContext, NextDelegate next, CancellationToken cancellationToken) in d:\a\1\s\libraries\Microsoft.Bot.Builder\BotFrameworkAdapter.cs:line 1158
   at Microsoft.Bot.Builder.MiddlewareSet.ReceiveActivityWithStatusAsync(ITurnContext turnContext, BotCallbackHandler callback, CancellationToken cancellationToken) in d:\a\1\s\libraries\Microsoft.Bot.Builder\MiddlewareSet.cs:line 55
   at Microsoft.Bot.Builder.BotAdapter.RunPipelineAsync(ITurnContext turnContext, BotCallbackHandler callback, CancellationToken cancellationToken) in d:\a\1\s\libraries\Microsoft.Bot.Builder\BotAdapter.cs:line 182

EDIT 3 - Tried with the sample 57

So I have started with the sample 57 called "TeamsConversationBot" and TLDR; works in channel, doesn't work in a private conversation.

Here are the steps performed:
Note: ngrok was already started using the command ngrok http -host-header=rewrite 3978 and my existing bot registration was already configured to listen on the given URL

  1. Updated the appsettings.json configuration for the project to use the Microsoft App Id and App Password from the Bot Framework registration.
  2. Edited the manifest.json contained in the TeamsAppManifest with the required GUIDs, zipped it along with PNG icons into manifest.zip and uploaded to Teams App Studio using "Import an existing app", which resulted in a new application called "TeamsConversationBot"
  3. In Teams App Studio, opened the new application for editing, went to "Test and distribute", clicked "Install" and then "Add" on the next screen.
  4. Navigated to the above screen again, but instead of "Add" I chose "Add to a team" from the drop down, where I added the bot to the channel of the Team I am the owner of.
  5. In Visual Studio, went to TeamsConversationsBot.cs and set a breakpoint in MessageAllMembersAsync method, on a line which says var members = await TeamsInfo.GetMembersAsync(turnContext, cancellationToken);
  6. Started the project, went to my channel in Teams, sent the "MessageAllMembers" to my bot, waited for the above mentioned breakpoint to be hit and observed that the said call succeeds, i.e. returns a list of members. So far so good!
  7. Went back to Teams App Builder to edit my "TeamsConversationsBot" app.
  8. In manifest editor, went to "Messaging extensions" tab and set up my existing bot to listen for messaging extension invocations.
  9. Added a new action-based command with a defined number of parameters. Gave it an Id, named a parameter and input the other mandatory fields, doesn't really matter. For "select the context in which the compose extension should work" I chose "Command Box" and "Compose Box", ticked "Initial run" and clicked "Save".
  10. Installed the app again using "Test and distribute" -> "Install" and then "Add" button.
  11. In Visual Studio, added the following method to TeamsConversationBot.cs:

    protected override async Task<InvokeResponse> OnInvokeActivityAsync(ITurnContext<IInvokeActivity> turnContext, CancellationToken cancellationToken)
    {
        var connector = turnContext.TurnState.Get<IConnectorClient>();
        var conversation = turnContext.Activity.Conversation;
        var members = await connector.Conversations.GetConversationMembersAsync(conversation.Id, cancellationToken);
        return await base.OnInvokeActivityAsync(turnContext, cancellationToken);
    }
    
  12. Set a breakpoint on the line which says var members = await connector.Conversations.GetConversationMembersAsync(conversation.Id, cancellationToken); and start the project

  13. Went to the channel, selected messaging extensions under the compose box and invoked the newly added command. This triggered the above breakpoint to be hit. Performed a step over and observed that members variable contained all members of the channel. So that works too!
  14. Went to a private conversation between me and the other team member (where me and them both have sent messages to each other previously, to ensure the conversation is not empty), selected messaging extensions under the compose box and invoked the newly added command. This again triggered the above mentioned breakpoint to be hit. Performed a step over and bam! The call has caused an unhandled exception Microsoft.Bot.Schema.ErrorResponseException: Operation returned an invalid status code 'Forbidden'

Upvotes: 1

Views: 3244

Answers (2)

Dejan Janjušević
Dejan Janjušević

Reputation: 3230

Actually, there is a way to get the list of participants in a 1:1 private conversation between two people when invoking an action-based messaging extension. The trick is to get the bot into the private conversation too.

I thought about that solution in the beginning but didn't know how, and since no one suggested it, I thought it wasn't possible in one go. But turns out I was wrong.

So in case anyone ever stumbles upon this kind of problem. here it is.

Originally I did this using a FetchTask action messaging extension (the action-based messaging extension where you don't supply the predefined list of parameters, but fetch the parameters using the bot).

But it can be done using a regular action with static parameters too, so let's make an example using the method OnInvokeActivityAsync that I used in the question. See comments in the code below for an explanation.

protected override async Task<InvokeResponse> OnInvokeActivityAsync(ITurnContext<IInvokeActivity> turnContext, CancellationToken cancellationToken)
{
    var connector = turnContext.TurnState.Get<IConnectorClient>();
    var conversation = turnContext.Activity.Conversation;
    IList<ChannelAccount> members;

    try
    {
        members = await connector.Conversations.GetConversationMembersAsync(conversation.Id, cancellationToken);
    }
    catch (ErrorResponseException ex)
    {
        // If the ErrorResponseException contains the response with status code 403, that means our bot is not a member of this conversation.
        // In that case, return an adaptive card containing the prompt to add the bot to the current conversation.
        // After accepting the prompt, the bot will be added to the conversation and we will be able to obtain the list of conversation participants.
        if (ex.Response.StatusCode == HttpStatusCode.Forbidden)
        {
            return new InvokeResponse
            {
                Status = 200,
                Body = AddBotToConversation()
            };
        }

        throw;
    }

    // At this point, we have the list of conversation members
    var otherMember = members.FirstOrDefault(x => x.Id != turnContext.Activity.From.Id);

    return new InvokeResponse
    {
        Status = 200,
        Body = await DoSomethingWithOtherMemberInformationAndReturnACard(otherMember, cancellationToken)
    };
}

The AddBotToConversation can be defined like this:

private MessagingExtensionActionResponse AddBotToConversation()
{
    var card = new AdaptiveCard(new AdaptiveSchemaVersion(1, 0))
    {
        Body = new List<AdaptiveElement>()
        {
            new AdaptiveTextBlock("We need to add the bot to this conversation in order to perform the requested action"),
        },
        Actions = new List<AdaptiveAction>()
        {
            new AdaptiveSubmitAction
            {
                Title = "Continue",
                Data = new Dictionary<string, object>
                {
                    // The magic happens here. This tells Teams to add this bot to the current conversation
                    ["msteams"] = new Dictionary<string, bool>
                    {
                        ["justInTimeInstall"] = true,
                    }
                }
            }
        }
    };

    var invokeResponse = new MessagingExtensionActionResponse
    {
        Task = new TaskModuleContinueResponse
        {
            Value = new TaskModuleTaskInfo
            {
                Card = new Attachment
                {
                    ContentType = AdaptiveCard.ContentType,
                    Content = card
                }
            }
        }
    };

    return invokeResponse;
}

Just make sure the messaging extension is action-based, not search-based. Search extensions will not support this approach.

EDIT Last but not least, don't forget to add the "groupchat" scope to the "scopes" collection of your bot under the "bots" collection in the Teams App manifest.json file, or else you won't be able to add your bot to a private conversation.

Upvotes: 1

mdrichardson
mdrichardson

Reputation: 7241

Response to Edit 3

Sorry, it looks like I misunderstood what you were trying to do. You cannot do this with the Bot Framework. Technically, the bot is not part of a Personal conversation between you and another user. Therefore, it doesn't have permissions to get information about that conversation, even though it's available as a Messaging Extension.

You may be able to use this Graph API call, although you'll have to deal with getting the the auth token on your own:

https://graph.microsoft.com/beta/chats/<conversationId>/members

Making this an answer because it's too long to comment and I think it should accomplish what you need. Please let me know if this works or not and I can edit this


I didn't see anything on the backend by looking into your appId, so I'm not sure of what the actual cause of the problem is.

You shouldn't need any permissions set at all -- those are only for OAuth, which GetConversationMembersAsync doesn't need. You should only really need the scopes set in your manifest.json, which appear to be set just fine.

However, for your App Registration, you may need to ensure that this is checked, under Authentication:

enter image description here

If it isn't you can manually adjust your App Registration's Manifest, by setting this:

"signInAudience": "AzureADandPersonalMicrosoftAccount",

If none of that works:

Please try Sample 57, which has a GetMembersAsync function that accomplishes the same thing, but with the current BotFramework SDK. Just be sure to follow the README and set everything up as specified.

Let me know how it goes. If you need additional help, can you try explaining your reproduction steps in as much detail as possible? I'd like to try to reproduce this and want to make sure our steps match--it may also help me call out where you might be going wrong.

Upvotes: 1

Related Questions