// ////////////////////////////////////////////////////////////
        // Pull Request Reviews
        // ////////////////////////////////////////////////////////////

        private async Task <HttpResponseMessage> ProxyPullRequestReview(HttpRequestMessage request, CancellationToken cancellationToken, string owner, string repo, int number)
        {
            var response = await ProxyRequest(request, cancellationToken);

            if (response.IsSuccessStatusCode)
            {
                var user = RequestContext.Principal as ShipHubPrincipal;
                try {
                    await response.Content.LoadIntoBufferAsync();

                    var review = await response.Content.ReadAsAsync <Review>(GitHubSerialization.MediaTypeFormatters, cancellationToken);

                    using (var context = new dm.ShipHubContext()) {
                        var updater  = new DataUpdater(context, _mapper);
                        var repoName = $"{owner}/{repo}";
                        var ids      = await context.Issues
                                       .AsNoTracking()
                                       .Where(x => x.Repository.FullName == repoName)
                                       .Where(x => x.Number == number)
                                       .Select(x => new { IssueId = x.Id, RepositoryId = x.RepositoryId })
                                       .SingleAsync();

                        await updater.UpdateReviews(ids.RepositoryId, ids.IssueId, response.Headers.Date ?? DateTimeOffset.UtcNow, new[] { review }, user.UserId);

                        await updater.Changes.Submit(_queueClient);
                    }
                } catch (Exception e) {
                    // swallow db exceptions, since if we're here github has created the resource.
                    // we'll probably get it fixed in our db sooner or later, but for now we need to give the client its data.
                    e.Report($"request: {request.RequestUri} response: {response} user: {user.DebugIdentifier}", user.DebugIdentifier);
                }
            }

            return(response);
        }
        public async Task <HttpResponseMessage> DeleteRequestedReviewer(HttpRequestMessage request, CancellationToken cancellationToken, string owner, string repo, int issueNumber)
        {
            // https://developer.github.com/v3/pulls/review_requests/#delete-a-review-request
            var response = await ProxyRequest(request, cancellationToken);

            if (response.StatusCode == HttpStatusCode.OK)
            {
                var user = RequestContext.Principal as ShipHubPrincipal;
                try {
                    await response.Content.LoadIntoBufferAsync();

                    var data = await response.Content.ReadAsAsync <JToken>(GitHubSerialization.MediaTypeFormatters, cancellationToken);

                    var removed = data.Value <IEnumerable <string> >("reviewers");

                    using (var context = new dm.ShipHubContext()) {
                        var changes = await context.DeleteReviewers($"{owner}/{repo}", issueNumber, removed);

                        await changes.Submit(_queueClient);
                    }
                } catch (Exception e) {
                    e.Report($"request: {request.RequestUri} response: {response} user: {user.DebugIdentifier}", user.DebugIdentifier);
                }
            }

            return(response);
        }
        public async Task <HttpResponseMessage> IssueMilestoneDelete(HttpRequestMessage request, CancellationToken cancellationToken, string owner, string repo, long number)
        {
            var response = await ProxyRequest(request, cancellationToken);

            if (response.StatusCode == HttpStatusCode.NoContent)
            {
                var user = RequestContext.Principal as ShipHubPrincipal;
                try {
                    using (var context = new dm.ShipHubContext()) {
                        // Eww
                        var repoFullName = $"{owner}/{repo}";
                        var milestoneId  = await context.Milestones.AsNoTracking()
                                           .Where(x => x.Repository.FullName == repoFullName)
                                           .Where(x => x.Number == number)
                                           .Select(x => (long?)x.Id)
                                           .SingleOrDefaultAsync();

                        if (milestoneId != null)
                        {
                            var changes = await context.DeleteMilestone(milestoneId.Value);

                            await changes.Submit(_queueClient);
                        }
                    }
                } catch (Exception e) {
                    e.Report($"request: {request.RequestUri} response: {response} user: {user.DebugIdentifier}", user.DebugIdentifier);
                }
            }

            return(response);
        }
        // ////////////////////////////////////////////////////////////
        // Issue Labels
        // ////////////////////////////////////////////////////////////

        private async Task <HttpResponseMessage> ProxyLabel(HttpRequestMessage request, CancellationToken cancellationToken, string owner, string repo)
        {
            var response = await ProxyRequest(request, cancellationToken);

            if (response.IsSuccessStatusCode)
            {
                var user = RequestContext.Principal as ShipHubPrincipal;
                try {
                    await response.Content.LoadIntoBufferAsync();

                    var label = await response.Content.ReadAsAsync <Label>(GitHubSerialization.MediaTypeFormatters, cancellationToken);

                    using (var context = new dm.ShipHubContext()) {
                        var updater  = new DataUpdater(context, _mapper);
                        var repoName = $"{owner}/{repo}";
                        var repoId   = await context.Repositories
                                       .AsNoTracking()
                                       .Where(x => x.FullName == repoName)
                                       .Select(x => x.Id)
                                       .SingleAsync();

                        await updater.UpdateLabels(repoId, new[] { label });

                        await updater.Changes.Submit(_queueClient);
                    }
                } catch (Exception e) {
                    // swallow db exceptions, since if we're here github has created the resource.
                    // we'll probably get it fixed in our db sooner or later, but for now we need to give the client its data.
                    e.Report($"request: {request.RequestUri} response: {response} user: {user.DebugIdentifier}", user.DebugIdentifier);
                }
            }

            return(response);
        }
        public async Task <IHttpActionResult> Logout()
        {
            // User wants to log out.
            using (var context = new d.ShipHubContext()) {
                var hookDetails = await context.GetLogoutWebhooks(ShipHubUser.UserId);

                var github = await _grainFactory.GetGrain <IGitHubActor>(ShipHubUser.UserId);

                var tasks = new List <Task>();

                // Delete all repo hooks where they're the only user
                tasks.AddRange(hookDetails.RepositoryHooks.Select(x => github.DeleteRepositoryWebhook(x.Name, x.HookId, RequestPriority.Interactive)));
                // Delete all org hooks where they're the only user
                tasks.AddRange(hookDetails.OrganizationHooks.Select(x => github.DeleteOrganizationWebhook(x.Name, x.HookId, RequestPriority.Interactive)));

                // Wait and log errors.
                var userInfo = $"{ShipHubUser.Login} ({ShipHubUser.UserId})";
                try {
                    // Ensure requests complete before we revoke our access below
                    await Task.WhenAll(tasks);

                    foreach (var task in tasks)
                    {
                        task.LogFailure(userInfo);
                    }
                } catch {
                    // They're logging out. We had our chance.
                }

                var tokens = await context.Tokens
                             .Where(x => x.UserId == ShipHubUser.UserId)
                             .Select(x => x.Token)
                             .ToArrayAsync();

                // Try all the tokens.
                foreach (var token in tokens)
                {
                    RevokeGrant(token).LogFailure(userInfo);
                }

                // Invalidate their token with ShipHub
                await context.RevokeAccessTokens(ShipHubUser.UserId);
            }

            return(Ok());
        }
        private async Task <(g.Account UserInfo, IHttpActionResult ErrorResult)> LoginCommon(string token, CancellationToken cancellationToken)
        {
            // This would really be a great place for F# discriminated unions.
            // Return the user info or an error.

            var userResponse = await GitHubUser(token, cancellationToken);

            if (!userResponse.IsOk)
            {
                return(UserInfo : null, ErrorResult : Error("Unable to determine account from token.", HttpStatusCode.InternalServerError, userResponse.Error));
            }

            var userInfo = userResponse.Result;

            if (userInfo.Type != g.GitHubAccountType.User)
            {
                return(UserInfo : null, ErrorResult : Error("Token must be for a user.", HttpStatusCode.BadRequest));
            }

            // Check scopes (currently a duplicate check here, I know).
            var scopesOk = _validScopesCollection.Any(x => x.IsSubsetOf(userResponse.Scopes));

            if (!scopesOk)
            {
                return(
                    UserInfo : null,
                    ErrorResult : Error("Insufficient scopes granted.", HttpStatusCode.Unauthorized, new {
                    Granted = userResponse.Scopes,
                })
                    );
            }

            using (var context = new d.ShipHubContext()) {
                // Create account using stored procedure
                // This ensures it exists (simpler logic) and also won't collide with sync.
                await context.BulkUpdateAccounts(userResponse.Date, new[] { _mapper.Map <AccountTableType>(userInfo) });

                // Save user access details
                await context.SetUserAccessToken(userInfo.Id, string.Join(",", userResponse.Scopes), userResponse.RateLimit);
            }

            return(UserInfo : userInfo, ErrorResult : null);
        }
        public async Task <HttpResponseMessage> PullRequestReviewCommentDelete(HttpRequestMessage request, CancellationToken cancellationToken, long commentId)
        {
            // https://developer.github.com/v3/pulls/comments/#delete-a-comment
            var response = await ProxyRequest(request, cancellationToken);

            if (response.StatusCode == HttpStatusCode.NoContent)
            {
                var user = RequestContext.Principal as ShipHubPrincipal;
                try {
                    using (var context = new dm.ShipHubContext()) {
                        var changes = await context.DeletePullRequestComment(commentId, null);

                        await changes.Submit(_queueClient);
                    }
                } catch (Exception e) {
                    e.Report($"request: {request.RequestUri} response: {response} user: {user.DebugIdentifier}", user.DebugIdentifier);
                }
            }

            return(response);
        }
        public async Task <HttpResponseMessage> PullRequestReviewDelete(HttpRequestMessage request, CancellationToken cancellationToken, long reviewId)
        {
            // https://developer.github.com/v3/pulls/reviews/#delete-a-pending-review
            var response = await ProxyRequest(request, cancellationToken);

            // WTF DELETE Review returns 200 OK
            // https://developer.github.com/v3/pulls/reviews/#delete-a-pending-review
            if (response.IsSuccessStatusCode)
            {
                var user = RequestContext.Principal as ShipHubPrincipal;
                try {
                    using (var context = new dm.ShipHubContext()) {
                        var changes = await context.DeleteReview(reviewId);

                        await changes.Submit(_queueClient);
                    }
                } catch (Exception e) {
                    e.Report($"request: {request.RequestUri} response: {response} user: {user.DebugIdentifier}", user.DebugIdentifier);
                }
            }

            return(response);
        }
        public async Task <HttpResponseMessage> PullRequestReviewCommentEdit(HttpRequestMessage request, CancellationToken cancellationToken, long commentId)
        {
            // https://developer.github.com/v3/pulls/comments/#edit-a-comment
            var response = await ProxyRequest(request, cancellationToken);

            if (response.IsSuccessStatusCode)
            {
                var user = RequestContext.Principal as ShipHubPrincipal;
                try {
                    await response.Content.LoadIntoBufferAsync();

                    var comment = await response.Content.ReadAsAsync <PullRequestComment>(GitHubSerialization.MediaTypeFormatters, cancellationToken);

                    using (var context = new dm.ShipHubContext()) {
                        var updater = new DataUpdater(context, _mapper);
                        var ids     = await context.PullRequestComments
                                      .AsNoTracking()
                                      .Where(x => x.Id == commentId)
                                      .Select(x => new { IssueId = x.IssueId, RepositoryId = x.RepositoryId })
                                      .SingleOrDefaultAsync();

                        if (ids != null)
                        {
                            await updater.UpdatePullRequestComments(ids.RepositoryId, ids.IssueId, response.Headers.Date ?? DateTimeOffset.UtcNow, new[] { comment });

                            await updater.Changes.Submit(_queueClient);
                        }
                    }
                } catch (Exception e) {
                    // swallow db exceptions, since if we're here github has created the resource.
                    // we'll probably get it fixed in our db sooner or later, but for now we need to give the client its data.
                    e.Report($"request: {request.RequestUri} response: {response} user: {user.DebugIdentifier}", user.DebugIdentifier);
                }
            }

            return(response);
        }
        public async Task <IHttpActionResult> Login([FromBody] LoginRequest request, CancellationToken cancellationToken)
        {
            if ((request?.AccessToken).IsNullOrWhiteSpace())
            {
                return(BadRequest($"{nameof(request.AccessToken)} is required."));
            }
            if (request.ClientName.IsNullOrWhiteSpace())
            {
                return(BadRequest($"{nameof(request.ClientName)} is required."));
            }

            d.User            user = null;
            g.Account         userInfo;
            IHttpActionResult errorResult = null;

            // Newer clients will have already authenticated via lambda_legacy
            // They don't send the authorization header though, so let's check
            // if we recognize this token.
            using (var context = new d.ShipHubContext()) {
                user = await context.Tokens
                       .AsNoTracking()
                       .Where(x => x.Token == request.AccessToken)
                       .Select(x => x.User)
                       .SingleOrDefaultAsync();
            }

            if (user != null)
            {
                // We know this user already.
                userInfo = new g.Account()
                {
                    Id    = user.Id,
                    Login = user.Login,
                    Name  = user.Name,
                    Type  = g.GitHubAccountType.User,
                };
            }
            else // End goal is to make everything below obsolete.
            {
                var login = await LoginCommon(request.AccessToken, cancellationToken);

                userInfo    = login.UserInfo;
                errorResult = login.ErrorResult;
            }

            if (errorResult != null)
            {
                // Abort early on error
                return(errorResult);
            }

            // Save settings if sent
            if (request.SyncSettings != null)
            {
                using (var context = new d.ShipHubContext()) {
                    await context.SetAccountSettings(userInfo.Id, request.SyncSettings);
                }
            }

            // Start sync
            var userGrain = await _grainFactory.GetGrain <IUserActor>(userInfo.Id);

            userGrain.Sync().LogFailure($"{userInfo.Login} ({userInfo.Id})");

            return(Ok(userInfo));
        }