Exemplo n.º 1
0
        /// <summary>
        /// This method can be called from inside a POST method on any Controller implementation.  If the activity is Not an Invoke, and
        /// DeliveryMode is Not ExpectReplies, and this is NOT a Get request to upgrade to WebSockets, then the activity will be enqueued
        /// to be processed on a background thread.
        /// </summary>
        /// <remarks>
        /// Note, this is an ImmediateAccept and BackgroundProcessing override of:
        /// Task IBotFrameworkHttpAdapter.ProcessAsync(HttpRequest httpRequest, HttpResponse httpResponse, IBot bot, CancellationToken cancellationToken = default);
        /// </remarks>
        /// <param name="httpRequest">The HTTP request object, typically in a POST handler by a Controller.</param>
        /// <param name="httpResponse">The HTTP response object.</param>
        /// <param name="bot">The bot implementation.</param>
        /// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive
        ///     notice of cancellation.</param>
        /// <returns>A task that represents the work queued to execute.</returns>
        async Task IBotFrameworkHttpAdapter.ProcessAsync(HttpRequest httpRequest, HttpResponse httpResponse, IBot bot, CancellationToken cancellationToken = default)
        {
            if (httpRequest == null)
            {
                throw new ArgumentNullException(nameof(httpRequest));
            }

            if (httpResponse == null)
            {
                throw new ArgumentNullException(nameof(httpResponse));
            }

            if (bot == null)
            {
                throw new ArgumentNullException(nameof(bot));
            }

            // Get is a socket exchange request, so should be processed by base BotFrameworkHttpAdapter
            if (httpRequest.Method == HttpMethods.Get)
            {
                await base.ProcessAsync(httpRequest, httpResponse, bot, cancellationToken);
            }
            else
            {
                // Deserialize the incoming Activity
                var activity = await HttpHelper.ReadRequestAsync <Activity>(httpRequest).ConfigureAwait(false);

                if (string.IsNullOrEmpty(activity?.Type))
                {
                    httpResponse.StatusCode = (int)HttpStatusCode.BadRequest;
                }
                else if (activity.Type == ActivityTypes.Invoke || activity.DeliveryMode == DeliveryModes.ExpectReplies)
                {
                    // NOTE: Invoke and ExpectReplies cannot be performed async, the response must be written before the calling thread is released.
                    await base.ProcessAsync(httpRequest, httpResponse, bot, cancellationToken);
                }
                else
                {
                    // Grab the auth header from the inbound http request
                    var authHeader = httpRequest.Headers["Authorization"];

                    try
                    {
                        // If authentication passes, queue a work item to process the inbound activity with the bot
                        var claimsIdentity = await JwtTokenValidation.AuthenticateRequest(activity, authHeader, CredentialProvider, ChannelProvider, HttpClient).ConfigureAwait(false);

                        // Queue the activity to be processed by the ActivityBackgroundService
                        _activityTaskQueue.QueueBackgroundActivity(claimsIdentity, activity);

                        // Activity has been queued to process, so return Ok immediately
                        httpResponse.StatusCode = (int)HttpStatusCode.OK;
                    }
                    catch (UnauthorizedAccessException)
                    {
                        // handle unauthorized here as this layer creates the http response
                        httpResponse.StatusCode = (int)HttpStatusCode.Unauthorized;
                    }
                }
            }
        }
Exemplo n.º 2
0
        public async Task <IActionResult> Post([FromBody] Activity activity)
        {
            // Validate Authorization Header. Should be a jwt token.
            var authHeader = this.Request.Headers["Authorization"].SingleOrDefault();

            try
            {
                await JwtTokenValidation.AuthenticateRequest(activity, authHeader, this.credentials);
            }
            catch (UnauthorizedAccessException)
            {
                return(this.Unauthorized());
            }

            // On message activity, reply with the same text
            if (activity.Type == ActivityTypes.Message)
            {
                var reply = activity.CreateReply($"You said: {activity.Text}");

                // Reply to Activity using Connector
                var connector = new ConnectorClient(
                    new Uri(activity.ServiceUrl, UriKind.Absolute),
                    new MicrosoftAppCredentials(this.credentials.AppId, this.credentials.Password));

                await connector.Conversations.ReplyToActivityAsync(reply);
            }

            return(this.Ok());
        }
Exemplo n.º 3
0
        /// <summary>
        /// Creates a turn context and runs the middleware pipeline for an incoming activity.
        /// </summary>
        /// <param name="authHeader">The HTTP authentication header of the request.</param>
        /// <param name="activity">The incoming activity.</param>
        /// <param name="callback">The code to run at the end of the adapter's middleware
        /// pipeline.</param>
        /// <returns>A task that represents the work queued to execute. If the activity type
        /// was 'Invoke' and the corresponding key (channelId + activityId) was found
        /// then an InvokeResponse is returned, otherwise null is returned.</returns>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="activity"/> is <c>null</c>.</exception>
        /// <exception cref="UnauthorizedAccessException">
        /// authentication failed.</exception>
        /// <remarks>Call this method to reactively send a message to a conversation.
        /// <para>This method registers the following services for the turn.<list type="bullet">
        /// <item><see cref="IIdentity"/> (key = "BotIdentity"), a claims identity for the bot.</item>
        /// <item><see cref="IConnectorClient"/>, the channel connector client to use this turn.</item>
        /// </list></para>
        /// </remarks>
        /// <seealso cref="ContinueConversation(string, ConversationReference, Func{ITurnContext, Task})"/>
        /// <seealso cref="BotAdapter.RunPipeline(ITurnContext, Func{ITurnContext, Task}, System.Threading.CancellationTokenSource)"/>
        public async Task <InvokeResponse> ProcessActivity(string authHeader, Activity activity, Func <ITurnContext, Task> callback)
        {
            BotAssert.ActivityNotNull(activity);
            var claimsIdentity = await JwtTokenValidation.AuthenticateRequest(activity, authHeader, _credentialProvider, _httpClient);

            using (var context = new TurnContext(this, activity))
            {
                context.Services.Add <IIdentity>("BotIdentity", claimsIdentity);
                var connectorClient = await this.CreateConnectorClientAsync(activity.ServiceUrl, claimsIdentity);

                context.Services.Add <IConnectorClient>(connectorClient);
                await base.RunPipeline(context, callback).ConfigureAwait(false);

                // Handle Invoke scenarios, which deviate from the request/response model in that
                // the Bot will return a specific body and return code.
                if (activity.Type == ActivityTypes.Invoke)
                {
                    Activity invokeResponse = context.Services.Get <Activity>(InvokeReponseKey);
                    if (invokeResponse == null)
                    {
                        // ToDo: Trace Here
                        throw new InvalidOperationException("Bot failed to return a valid 'invokeResponse' activity.");
                    }
                    else
                    {
                        return((InvokeResponse)invokeResponse.Value);
                    }
                }

                // For all non-invoke scenarios, the HTTP layers above don't have to mess
                // withthe Body and return codes.
                return(null);
            }
        }
Exemplo n.º 4
0
        public static async Task <IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req,
            ILogger logger,
            ExecutionContext context)
        {
            logger.LogInformation("Messages function received a request.");

            ISettings            settings            = new Settings(logger, context);
            IClippySetRepository clippySetRepository = new ClippySetRepository(logger, settings);
            IClippySetIndexer    clippySetIndexer    = new ClippySetIndexer(logger);
            ICredentialProvider  credentialProvider  = new SimpleCredentialProvider(settings.MicrosoftAppId, null);
            IChannelProvider     channelProvider     = new SimpleChannelProvider();

            Activity activity;

            try
            {
                var authorizationHeader = GetAuthorizationHeader(req);
                activity = await ParseRequestBody(req);

                await JwtTokenValidation.AuthenticateRequest(activity, authorizationHeader, credentialProvider, channelProvider);
            }
            catch (JsonReaderException e)
            {
                logger.LogDebug(e, "JSON parser failed to parse request payload.");
                return(new BadRequestResult());
            }
            catch (UnauthorizedAccessException e)
            {
                logger.LogDebug(e, "Request was not propertly authorized.");
                return(new UnauthorizedResult());
            }

            if (!activity.IsComposeExtensionQuery())
            {
                logger.LogDebug("Request payload was not a compose extension query.");
                return(new BadRequestObjectResult($"Clippy only supports compose extension query activity types."));
            }

            var queryValue = JObject.FromObject(activity.Value).ToObject <ComposeExtensionValue>();
            var query      = queryValue.GetParameterValue();

            var clippySet = await clippySetRepository.FetchClippySetAsync();

            await clippySetIndexer.IndexClippySetAsync(clippySet);

            var clippies = await clippySetIndexer.FindClippiesByQuery(query);

            var result = new ComposeExtensionResponse
            {
                ComposeExtensionResult = new ComposeExtensionResult
                {
                    Type             = "result",
                    AttachmentLayout = "grid",
                    Attachments      = clippies.Select(clippy => new ClippyComposeExtensionCard(clippy).ToAttachment()).ToArray()
                }
            };

            return(new OkObjectResult(result));
        }
Exemplo n.º 5
0
        public async Task <IActionResult> BotMessages(string instanceId, int routeCounter)
        {
            if (routeCounter > 1)
            {
                // Node is gone. Return 404, bot should queue messages until the connection is reestablished.
                return(NotFound());
            }

            IActionResult result;

            if (instanceId == _instanceId)
            {
                // This is the node with the web socket, send the message
                var activity = await HttpHelper.ReadRequestAsync <Activity>(Request);

                var authHeader     = Request.Headers["Authorization"];
                var claimsIdentity = await JwtTokenValidation.AuthenticateRequest(activity, authHeader, _credentialProvider, _channelProvider);

                var resultTcs = new TaskCompletionSource <ResourceResponse>();
                if (!_replyBuffer.Post(Tuple.Create(activity, resultTcs)))
                {
                    return(new InternalServerErrorResult());
                }
                var receiveResponse = await resultTcs.Task;
                result = Ok();
            }
            else
            {
                // The websocket is on another node, route to it
                result = await RerouteAsync(instanceId, routeCounter);
            }

            return(result);
        }
        /// <summary>
        /// Creates a turn context and runs the middleware pipeline for an incoming activity.
        /// </summary>
        /// <param name="authHeader">The HTTP authentication header of the request.</param>
        /// <param name="activity">The incoming activity.</param>
        /// <param name="callback">The code to run at the end of the adapter's middleware pipeline.</param>
        /// <param name="cancellationToken">A cancellation token that can be used by other objects
        /// or threads to receive notice of cancellation.</param>
        /// <returns>A task that represents the work queued to execute. If the activity type
        /// was 'Invoke' and the corresponding key (channelId + activityId) was found
        /// then an InvokeResponse is returned, otherwise null is returned.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="activity"/> is <c>null</c>.</exception>
        /// <exception cref="UnauthorizedAccessException">authentication failed.</exception>
        /// <remarks>Call this method to reactively send a message to a conversation.
        /// If the task completes successfully, then if the activity's <see cref="Activity.Type"/>
        /// is <see cref="ActivityTypes.Invoke"/> and the corresponding key
        /// (<see cref="Activity.ChannelId"/> + <see cref="Activity.Id"/>) is found
        /// then an <see cref="InvokeResponse"/> is returned, otherwise null is returned.
        /// <para>This method registers the following services for the turn.<list type="bullet">
        /// <item><see cref="IIdentity"/> (key = "BotIdentity"), a claims identity for the bot.</item>
        /// <item><see cref="IConnectorClient"/>, the channel connector client to use this turn.</item>
        /// </list></para>
        /// </remarks>
        /// <seealso cref="ContinueConversationAsync(string, ConversationReference, BotCallbackHandler, CancellationToken)"/>
        /// <seealso cref="BotAdapter.RunPipelineAsync(ITurnContext, BotCallbackHandler, CancellationToken)"/>
        public async Task <InvokeResponse> ProcessActivityAsync(string authHeader, Activity activity, BotCallbackHandler callback, CancellationToken cancellationToken)
        {
            BotAssert.ActivityNotNull(activity);

            var claimsIdentity = await JwtTokenValidation.AuthenticateRequest(activity, authHeader, _credentialProvider, _channelProvider, _httpClient).ConfigureAwait(false);

            return(await ProcessActivityAsync(claimsIdentity, activity, callback, cancellationToken).ConfigureAwait(false));
        }
        /// <summary>
        /// Creates a turn context and runs the middleware pipeline for an incoming activity.
        /// </summary>
        /// <param name="authHeader">The HTTP authentication header of the request.</param>
        /// <param name="activity">The incoming activity.</param>
        /// <param name="callback">The code to run at the end of the adapter's middleware
        /// pipeline.</param>
        /// <returns>A task that represents the work queued to execute. If the activity type
        /// was 'Invoke' and the corresponding key (channelId + activityId) was found
        /// then an InvokeResponse is returned, otherwise null is returned.</returns>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="activity"/> is <c>null</c>.</exception>
        /// <exception cref="UnauthorizedAccessException">
        /// authentication failed.</exception>
        /// <remarks>Call this method to reactively send a message to a conversation.
        /// <para>This method registers the following services for the turn.<list type="bullet">
        /// <item><see cref="IIdentity"/> (key = "BotIdentity"), a claims identity for the bot.</item>
        /// <item><see cref="IConnectorClient"/>, the channel connector client to use this turn.</item>
        /// </list></para>
        /// </remarks>
        /// <seealso cref="ContinueConversation(string, ConversationReference, Func{ITurnContext, Task})"/>
        /// <seealso cref="BotAdapter.RunPipeline(ITurnContext, Func{ITurnContext, Task})"/>
        public async Task <InvokeResponse> ProcessActivity(string authHeader, Activity activity, Func <ITurnContext, Task> callback)
        {
            BotAssert.ActivityNotNull(activity);

            var claimsIdentity = await JwtTokenValidation.AuthenticateRequest(activity, authHeader, _credentialProvider, _httpClient).ConfigureAwait(false);

            return(await ProcessActivity(claimsIdentity, activity, callback).ConfigureAwait(false));
        }
Exemplo n.º 8
0
        public async Task ProcessActivity(string authHeader, Activity activity, Func <IBotContext, Task> callback)
        {
            BotAssert.ActivityNotNull(activity);
            ClaimsIdentity claimsIdentity = await JwtTokenValidation.AuthenticateRequest(activity, authHeader, _credentialProvider, _httpClient);

            // For requests from channel App Id is in Audience claim of JWT token. For emulator it is in AppId claim. For
            // unauthenticated requests we have anonymouse identity provided auth is disabled.
            string botAppId = GetBotId(claimsIdentity);
            var    context  = new BotFrameworkBotContext(botAppId, this, activity);
            await base.RunPipeline(context, callback).ConfigureAwait(false);
        }
        /// <summary>
        /// Creates a turn context and runs the middleware pipeline for an incoming activity.
        /// </summary>
        /// <param name="authHeader">The HTTP authentication header of the request.</param>
        /// <param name="activity">The incoming activity.</param>
        /// <param name="callback">The code to run at the end of the adapter's middleware
        /// pipeline.</param>
        /// <returns>A task that represents the work queued to execute.</returns>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="activity"/> is <c>null</c>.</exception>
        /// <exception cref="UnauthorizedAccessException">
        /// authentication failed.</exception>
        /// <remarks>Call this method to reactively send a message to a conversation.
        /// <para>This method registers the following services for the turn.<list type="bullet">
        /// <item><see cref="IIdentity"/> (key = "BotIdentity"), a claims identity for the bot.</item>
        /// <item><see cref="IConnectorClient"/>, the channel connector client to use this turn.</item>
        /// </list></para>
        /// </remarks>
        /// <seealso cref="ContinueConversation(string, ConversationReference, Func{ITurnContext, Task})"/>
        /// <seealso cref="BotAdapter.RunPipeline(ITurnContext, Func{ITurnContext, Task}, System.Threading.CancellationTokenSource)"/>
        public async Task ProcessActivity(string authHeader, Activity activity, Func <ITurnContext, Task> callback)
        {
            BotAssertSlack.ActivityNotNull(activity);
            var claimsIdentity = await JwtTokenValidation.AuthenticateRequest(activity, authHeader, _credentialProvider, _httpClient);

            var context = new TurnContext(this, activity);

            context.Services.Add <IIdentity>("BotIdentity", claimsIdentity);
            var connectorClient = await this.CreateConnectorClientAsync(activity.ServiceUrl, claimsIdentity);

            context.Services.Add <IConnectorClient>(connectorClient);
            await base.RunPipeline(context, callback).ConfigureAwait(false);
        }
        public async void Channel_MsaHeader_Valid_ServiceUrlShouldBeTrusted()
        {
            string header      = $"Bearer {await new MicrosoftAppCredentials("2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F").GetTokenAsync()}";
            var    credentials = new SimpleCredentialProvider("2cd87869-38a0-4182-9251-d056e8f0ac24", "");

            await JwtTokenValidation.AuthenticateRequest(
                new Activity { ServiceUrl = "https://smba.trafficmanager.net/amer-client-ss.msg/" },
                header,
                credentials,
                emptyClient);

            Assert.True(MicrosoftAppCredentials.IsTrustedServiceUrl("https://smba.trafficmanager.net/amer-client-ss.msg/"));
        }
        public async void Channel_AuthenticationDisabled_ServiceUrlShouldNotBeTrusted()
        {
            var header      = "";
            var credentials = new SimpleCredentialProvider();

            var claimsPrincipal = await JwtTokenValidation.AuthenticateRequest(
                new Activity { ServiceUrl = "https://webchat.botframework.com/" },
                header,
                credentials,
                emptyClient);

            Assert.False(MicrosoftAppCredentials.IsTrustedServiceUrl("https://webchat.botframework.com/"));
        }
        public async void Channel_AuthenticationDisabled_ShouldBeAnonymous()
        {
            var header      = "";
            var credentials = new SimpleCredentialProvider();

            var claimsPrincipal = await JwtTokenValidation.AuthenticateRequest(
                new Activity { ServiceUrl = "https://webchat.botframework.com/" },
                header,
                credentials,
                emptyClient);

            Assert.Equal("anonymous", claimsPrincipal.AuthenticationType);
        }
        public async void Channel_MsaHeader_Valid_ServiceUrlShouldBeTrusted()
        {
            var header      = "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6ImI0eXNPV0l0RDEzaVFmTExlQkZYOWxSUER0ayIsInR5cCI6IkpXVCIsIng1dCI6ImI0eXNPV0l0RDEzaVFmTExlQkZYOWxSUER0ayJ9.eyJzZXJ2aWNldXJsIjoiaHR0cHM6Ly9zbWJhLnRyYWZmaWNtYW5hZ2VyLm5ldC9hbWVyLWNsaWVudC1zcy5tc2cvIiwibmJmIjoxNTE5Njk3OTQ0LCJleHAiOjE1MTk3MDE1NDQsImlzcyI6Imh0dHBzOi8vYXBpLmJvdGZyYW1ld29yay5jb20iLCJhdWQiOiI3Zjc0NTEzZS02Zjk2LTRkYmMtYmU5ZC05YTgxZmVhMjJiODgifQ.wjApM-MBhEIHSRHJGmivfpyFg0-SrTFh6Xta2RrKlZT4urACPX7kdZAb6oGOeDIm0NU16BPcpEqtCm9nBPmwoKKRbLCQ4Q3DGcB_LY15VCYfiiAnaevNNcvq7j_Hu-oyTmKOqpjfzu8qMIsjySClf1qZFucUrqzccePtlb63DAVfv-nF3bp-sm-zFG7RBX32cCygBMvpVENBroAq3ANfUQCmixkExcGr5npV3dFihSE0H9ntLMGseBdW7dRe5xOXDIgCtcCJPid-A6Vz-DxWGabyy2mVXLwYYuDxP4L5aruGwJIl_Z2-_MjhrWVszoeCRoOlx9-LNtbdSYGWmXWSbg";
            var credentials = new SimpleCredentialProvider("7f74513e-6f96-4dbc-be9d-9a81fea22b88", "");

            await JwtTokenValidation.AuthenticateRequest(
                new Activity { ServiceUrl = "https://smba.trafficmanager.net/amer-client-ss.msg/" },
                header,
                credentials,
                emptyClient);

            Assert.True(MicrosoftAppCredentials.IsTrustedServiceUrl("https://smba.trafficmanager.net/amer-client-ss.msg/"));
        }
        public async void Channel_MsaHeader_Invalid_ServiceUrlShouldNotBeTrusted()
        {
            string header      = $"Bearer {await new MicrosoftAppCredentials("2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F").GetTokenAsync()}";
            var    credentials = new SimpleCredentialProvider("7f74513e-6f96-4dbc-be9d-9a81fea22b88", "");

            await Assert.ThrowsAsync <UnauthorizedAccessException>(
                async() => await JwtTokenValidation.AuthenticateRequest(
                    new Activity {
                ServiceUrl = "https://webchat.botframework.com/"
            },
                    header,
                    credentials,
                    emptyClient));

            Assert.False(MicrosoftAppCredentials.IsTrustedServiceUrl("https://webchat.botframework.com/"));
        }
        public async void Channel_AuthenticationDisabledAndSkill_ShouldBeAnonymous()
        {
            var header      = string.Empty;
            var credentials = new SimpleCredentialProvider();

            var claimsPrincipal = await JwtTokenValidation.AuthenticateRequest(
                new Activity
            {
                ChannelId  = Channels.Emulator,
                ServiceUrl = "https://webchat.botframework.com/",
                RelatesTo  = new ConversationReference(),
                Recipient  = new ChannelAccount {
                    Role = RoleTypes.Skill
                }
            },
                header,
                credentials,
                new SimpleChannelProvider(),
                emptyClient);

            Assert.Equal(AuthenticationConstants.AnonymousAuthType, claimsPrincipal.AuthenticationType);
            Assert.Equal(AuthenticationConstants.AnonymousSkillAppId, JwtTokenValidation.GetAppIdFromClaims(claimsPrincipal.Claims));
        }
        public async Task <IActionResult> Run([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req, ILogger logger, ExecutionContext context)
        {
            if (logger == null)
            {
                throw new ArgumentNullException(nameof(logger));
            }

            using (var scope = logger.BeginScope($"{nameof(MessagesHttpFunction.Run)}")) {
                if (req == null)
                {
                    throw new ArgumentNullException(nameof(req));
                }

                if (context == null)
                {
                    throw new ArgumentNullException(nameof(context));
                }

                logger.LogInformation("Messages function received a request.");

                // Use the configured service for tests or create ones to use.
                ISettings             settings             = this.settings ?? new Settings(logger, context);
                IStickerSetRepository stickerSetRepository = this.stickerSetRepository ?? new StickerSetRepository(logger, settings);
                IStickerSetIndexer    stickerSetIndexer    = this.stickerSetIndexer ?? new StickerSetIndexer(logger);
                ICredentialProvider   credentialProvider   = this.credentialProvider ?? new SimpleCredentialProvider(settings.MicrosoftAppId, null);
                IChannelProvider      channelProvider      = this.channelProvider ?? new SimpleChannelProvider();

                // Parse the incoming activity and authenticate the request
                Activity activity;
                try {
                    var authorizationHeader = GetAuthorizationHeader(req);
                    activity = await ParseRequestBody(req);

                    await JwtTokenValidation.AuthenticateRequest(activity, authorizationHeader, credentialProvider, channelProvider);
                } catch (JsonReaderException e) {
                    logger.LogDebug(e, "JSON parser failed to parse request payload.");
                    return(new BadRequestResult());
                } catch (UnauthorizedAccessException e) {
                    logger.LogDebug(e, "Request was not propertly authorized.");
                    return(new UnauthorizedResult());
                }

                // Log telemetry about the activity
                try {
                    this.LogActivityTelemetry(activity);
                } catch (Exception ex) {
                    logger.LogWarning(ex, "Error sending user activity telemetry");
                }

                // Reject all activity types other than those related to messaging extensions
                if (!activity.IsComposeExtensionQuery())
                {
                    logger.LogDebug("Request payload was not a messaging extension query.");
                    return(new BadRequestObjectResult($"App only supports messaging extension query activity types."));
                }

                // Get the query string. We expect exactly 1 parameter, so we take the first parameter, regardless of the name.
                var skip  = 0;
                var count = 25;
                var query = string.Empty;
                if (activity.Value != null)
                {
                    var queryValue = JObject.FromObject(activity.Value).ToObject <ComposeExtensionValue>();
                    query = queryValue.GetParameterValue();

                    if (queryValue?.QueryOptions != null)
                    {
                        skip  = queryValue.QueryOptions.Skip;
                        count = queryValue.QueryOptions.Count;
                    }
                }

                // Find matching stickers
                var stickerSet = await stickerSetRepository.FetchStickerSetAsync();

                await stickerSetIndexer.IndexStickerSetAsync(stickerSet);

                var stickers = await stickerSetIndexer.FindStickersByQuery(query, skip, count);

                var result = new ComposeExtensionResponse {
                    ComposeExtensionResult = new ComposeExtensionResult {
                        Type             = "result",
                        AttachmentLayout = "grid",
                        Attachments      = stickers.Select(sticker => new StickerComposeExtensionCard(sticker).ToAttachment()).ToArray()
                    }
                };

                return(new OkObjectResult(result));
            }
        }