public async Task Execute(CancellationToken cancellationToken)
        {
            // TODO: if we ever have more patrons (unlikely) than can be kept in memory, this needs a different
            // approach

            var patrons = await database.Patrons.ToListAsync(cancellationToken);

            patrons.ForEach(p => p.Marked = false);

            foreach (var settings in await database.PatreonSettings.ToListAsync(cancellationToken))
            {
                if (settings.Active == false)
                {
                    continue;
                }

                var api = new PatreonCreatorAPI(settings);

                foreach (var actualPatron in await api.GetPatrons(settings, cancellationToken))
                {
                    await PatreonGroupHandler.HandlePatreonPledgeObject(actualPatron.Pledge, actualPatron.User,
                                                                        actualPatron.Reward?.Id, database, jobClient);

                    if (cancellationToken.IsCancellationRequested)
                    {
                        throw new TaskCanceledException();
                    }
                }

                settings.LastRefreshed = DateTime.UtcNow;
            }

            foreach (var toDelete in patrons.Where(p => p.Marked == false))
            {
                await database.LogEntries.AddAsync(new LogEntry()
                {
                    Message = $"Destroying patron ({toDelete.Id}) because it is unmarked " +
                              "(wasn't found from fresh data from Patreon)"
                }, cancellationToken);

                logger.LogInformation("Deleted unmarked Patron {Id}", toDelete.Id);
                database.Patrons.Remove(toDelete);
            }

            await database.SaveChangesAsync(cancellationToken);

            jobClient.Enqueue <ApplyPatronForumGroupsJob>(x => x.Execute(CancellationToken.None));
        }
Пример #2
0
        private async Task HandlePatrons(CancellationToken cancellationToken)
        {
            if (Settings == null)
            {
                throw new InvalidOperationException("Patreon settings haven't been loaded");
            }

            // TODO: might need to change this to working in batches
            var allPatrons = await Database.Patrons.ToListAsync(cancellationToken);

            foreach (var patron in allPatrons)
            {
                // Skip patrons who shouldn't have a forum group, check_unmarked will find them
                if (PatreonGroupHandler.ShouldBeInGroupForPatron(patron, Settings) ==
                    PatreonGroupHandler.RewardGroup.None)
                {
                    continue;
                }

                // Also skip suspended who should have their groups revoked as long as they are suspended
                if (patron.Suspended == true)
                {
                    continue;
                }

                // TODO: alias implementation
                var forumUser = await DiscourseAPI.FindUserByEmail(patron.Email, cancellationToken);

                if (cancellationToken.IsCancellationRequested)
                {
                    return;
                }

                if (forumUser == null)
                {
                    logger.LogTrace("Patron ({Username}) is missing a forum account, can't apply groups",
                                    patron.Username);
                    patron.HasForumAccount = false;
                }
                else
                {
                    patron.HasForumAccount = true;
                    HandlePatron(patron, forumUser, logger);
                }
            }
        }
        public async Task <IActionResult> PostWebhook(
            [Required][MaxLength(200)][Bind(Prefix = "webhook_id")] string webhookId)
        {
            var type = GetEventType();

            var settings =
                await database.PatreonSettings.FirstOrDefaultAsync(s => s.WebhookId == webhookId && s.Active);

            if (settings == null)
            {
                return(this.WorkingForbid("Invalid hook id"));
            }

            var verifiedPayload = await CheckSignature(settings);

            logger.LogTrace("Got patreon payload: {VerifiedPayload}", verifiedPayload);

            PatreonAPIObjectResponse data;

            try
            {
                data = JsonSerializer.Deserialize <PatreonAPIObjectResponse>(verifiedPayload,
                                                                             new JsonSerializerOptions(JsonSerializerDefaults.Web)) ?? throw new NullDecodedJsonException();
            }
            catch (Exception e)
            {
                logger.LogWarning("Failed to parse JSON in patreon webhook body: {@E}", e);
                throw new HttpResponseException()
                      {
                          Value = new BasicJSONErrorResult("Invalid request", "Invalid JSON").ToString()
                      };
            }

            var pledge = data.Data;

            if (pledge.Type != "pledge")
            {
                throw new HttpResponseException()
                      {
                          Value = new BasicJSONErrorResult("Bad data", "Expected pledge object").ToString()
                      };
            }

            var patronHookData = pledge.Relationships?.Patron?.Data;

            if (patronHookData == null)
            {
                throw new HttpResponseException()
                      {
                          Value = new BasicJSONErrorResult("Bad data", "Associated patron data not included").ToString()
                      };
            }

            var userData = data.FindIncludedObject(patronHookData.Id);

            if (userData == null)
            {
                throw new HttpResponseException()
                      {
                          Value = new BasicJSONErrorResult("Bad data", "Included objects didn't contain relevant user object")
                                  .ToString()
                      };
            }

            var email = userData.Attributes.Email;

            if (string.IsNullOrEmpty(email))
            {
                throw new HttpResponseException()
                      {
                          Value = new BasicJSONErrorResult("Bad data", "User object is missing email")
                                  .ToString()
                      };
            }

            switch (type)
            {
            case EventType.Create:
            case EventType.Update:
            {
                string rewardId;

                try
                {
                    // This was what the old code did, no clue why it would be necessary to unnecessarily
                    // look up the reward object...
                    // rewardId = data.FindIncludedObject(pledge.Relationships["reward"].Data.Id).Id;
                    rewardId = pledge.Relationships?.Reward?.Data?.Id ??
                               throw new Exception("Required relationship/property not included");
                }
                catch (Exception e)
                {
                    logger.LogWarning("Couldn't find reward ID in patreon webhook: {@E}", e);
                    rewardId = "Unknown";
                }

                await PatreonGroupHandler.HandlePatreonPledgeObject(pledge, userData, rewardId, database,
                                                                    jobClient);

                break;
            }

            case EventType.Delete:
            {
                // Find relevant patron object and delete it
                var patron = await database.Patrons.FirstOrDefaultAsync(p => p.Email == email);

                if (patron != null)
                {
                    database.Patrons.Remove(patron);
                    jobClient.Schedule <CheckSSOUserSuspensionJob>(x => x.Execute(email, CancellationToken.None),
                                                                   TimeSpan.FromSeconds(15));
                }
                else
                {
                    logger.LogWarning("Could not find patron to delete by email: {Email}", email);
                }

                break;
            }

            default:
                throw new ArgumentOutOfRangeException();
            }

            settings.LastWebhook = DateTime.UtcNow;

            await database.SaveChangesAsync();

            // Queue a job to update the status for the relevant user
            jobClient.Enqueue <ApplySinglePatronGroupsJob>(x => x.Execute(email, CancellationToken.None));

            return(Ok());
        }