C# Discord bot made using NetCord for keeping a TikTok-style streak
1using Microsoft.EntityFrameworkCore;
2using Microsoft.Extensions.DependencyInjection;
3using Microsoft.Extensions.Hosting;
4using Microsoft.Extensions.Logging;
5using StreakBot.Data;
6using StreakBot.Data.Entities;
7
8namespace StreakBot.Services;
9
10public class StreakResetBackgroundService : BackgroundService
11{
12 private readonly IServiceScopeFactory _scopeFactory;
13 private readonly ILogger<StreakResetBackgroundService> _logger;
14 private readonly TimeSpan _checkInterval = TimeSpan.FromMinutes(1);
15
16 public StreakResetBackgroundService(
17 IServiceScopeFactory scopeFactory,
18 ILogger<StreakResetBackgroundService> logger)
19 {
20 _scopeFactory = scopeFactory;
21 _logger = logger;
22 }
23
24 protected override async Task ExecuteAsync(CancellationToken stoppingToken)
25 {
26 while (!stoppingToken.IsCancellationRequested)
27 {
28 try
29 {
30 await CheckAllStreaksAsync();
31 }
32 catch (Exception ex)
33 {
34 _logger.LogError(ex, "Error checking streaks for reset");
35 }
36
37 await Task.Delay(_checkInterval, stoppingToken);
38 }
39 }
40
41 private async Task CheckAllStreaksAsync()
42 {
43 using var scope = _scopeFactory.CreateScope();
44 var context = scope.ServiceProvider.GetRequiredService<StreakDbContext>();
45 var channelService = scope.ServiceProvider.GetRequiredService<ChannelService>();
46
47 var streaks = await context.Streaks.ToListAsync();
48 var affectedStreaks = new List<Streak>();
49
50 foreach (var streak in streaks)
51 {
52 var result = await CheckAndResetIfNeededAsync(streak, channelService);
53
54 if (result != null)
55 {
56 affectedStreaks.Add(result);
57 }
58 }
59
60 if (context.ChangeTracker.HasChanges())
61 {
62 var count = await context.SaveChangesAsync();
63 _logger.LogInformation("Reset {Count} streak(s)", count);
64 }
65
66 foreach (var streak in affectedStreaks.Where(s => s.ServerId != 0))
67 {
68 await channelService.UpdateStreakChannelAsync(streak.ServerId, streak.StreakNumber, true);
69 }
70
71 await UpdateAllTimeChannelsAsync(streaks, channelService);
72 await UpdateAllDmMessagesAsync(streaks, channelService, context);
73 }
74
75 private async Task<Streak?> CheckAndResetIfNeededAsync(Streak streak, ChannelService channelService)
76 {
77 var now = DateTime.UtcNow;
78 var timeUntilReset = streak.LastResetCheck - now;
79
80 // Check if we need to send a reminder (less than 1 hour until reset and not both have sent)
81 if (timeUntilReset > TimeSpan.Zero && timeUntilReset <= TimeSpan.FromHours(1))
82 {
83 if ((!streak.User1MessageSent || !streak.User2MessageSent) && !streak.ReminderSent)
84 {
85 await channelService.SendStreakReminderAsync(
86 streak.User1Id,
87 streak.User2Id,
88 !streak.User1MessageSent,
89 !streak.User2MessageSent,
90 streak.ServerId);
91
92 streak.ReminderSent = true;
93 }
94 }
95
96 if (now <= streak.LastResetCheck)
97 {
98 return null;
99 }
100
101 if (!streak.User1MessageSent || !streak.User2MessageSent)
102 {
103 streak.StreakNumberToRestore = streak.StreakNumber;
104 streak.StreakNumber = 0;
105 _logger.LogInformation("Resetting streak between {0} and {1}", streak.User1Id, streak.User2Id);
106 }
107
108 streak.User1MessageSent = false;
109 streak.User2MessageSent = false;
110 streak.ReminderSent = false;
111
112 var daysToAdd = (int)Math.Ceiling((now - streak.LastResetCheck).TotalDays);
113 streak.LastResetCheck = streak.LastResetCheck.AddDays(daysToAdd);
114
115 return streak;
116 }
117
118 private async Task UpdateAllTimeChannelsAsync(List<Streak> streaks, ChannelService channelService)
119 {
120 var serverStreaks = streaks
121 .Where(s => s.ServerId != 0)
122 .DistinctBy(s => s.ServerId)
123 .ToList();
124
125 foreach (var streak in serverStreaks)
126 {
127 await channelService.UpdateTimeChannelAsync(streak.ServerId, streak.CreatedDate.TimeOfDay);
128 }
129 }
130
131 private async Task UpdateAllDmMessagesAsync(List<Streak> streaks, ChannelService channelService, StreakDbContext context)
132 {
133 var dmStreaks = streaks.Where(s => s.ServerId == 0).ToList();
134
135 foreach (var streak in dmStreaks)
136 {
137 var channel = await context.Channels
138 .FirstOrDefaultAsync(c => c.ChannelId == streak.ChannelId && c.ChannelType == ChannelType.DmChannel);
139
140 if (channel?.MessageId != null)
141 {
142 await channelService.UpdateDmStreakMessageAsync(channel.ChannelId, channel.MessageId.Value, streak.StreakNumber, streak.CreatedDate.TimeOfDay);
143 }
144 }
145 }
146}