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)); }
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()); }