C# Discord bot made using NetCord for keeping a TikTok-style streak
1using Microsoft.EntityFrameworkCore;
2using NetCord.Services;
3using StreakBot.Data;
4using StreakBot.Data.Entities;
5
6namespace StreakBot.Services;
7
8public class StreakService
9{
10 private readonly StreakDbContext _context;
11 private readonly ChannelService _channelService;
12
13 public StreakService(StreakDbContext context, ChannelService channelService)
14 {
15 _context = context;
16 _channelService = channelService;
17 }
18
19 public async Task ProcessMessageAsync(ulong userId, ulong serverId)
20 {
21 //Streaks are per server
22 var streak = await _context.Streaks
23 .Where(s => s.ServerId == serverId &&
24 (s.User1Id == userId || s.User2Id == userId))
25 .FirstOrDefaultAsync();
26
27 if (streak == null)
28 {
29 return;
30 }
31
32 ProcessStreakMessage(streak, userId);
33
34 if (_context.ChangeTracker.HasChanges())
35 {
36 await _context.SaveChangesAsync();
37
38 if (streak.User1MessageSent && streak.User2MessageSent)
39 {
40 await _channelService.UpdateStreakChannelAsync(serverId, streak.StreakNumber);
41 }
42 }
43 }
44
45 public async Task ProcessDmMessageAsync(ulong userId, ulong channelId)
46 {
47 var streaks = await _context.Streaks
48 .Where(s => s.ServerId == 0 &&
49 s.ChannelId == channelId &&
50 (s.User1Id == userId || s.User2Id == userId))
51 .ToListAsync();
52
53 foreach (var streak in streaks)
54 {
55 ProcessStreakMessage(streak, userId);
56 }
57
58 if (_context.ChangeTracker.HasChanges())
59 {
60 await _context.SaveChangesAsync();
61
62 // Update DM message for each affected streak
63 foreach (var streak in streaks)
64 {
65 var channel = await _context.Channels
66 .FirstOrDefaultAsync(c => c.ChannelId == channelId && c.ChannelType == Data.Entities.ChannelType.DmChannel);
67
68 if (channel?.MessageId != null)
69 {
70 await _channelService.UpdateDmStreakMessageAsync(channelId, channel.MessageId.Value, streak.StreakNumber, streak.CreatedDate.TimeOfDay);
71 }
72 }
73 }
74 }
75
76 private static void ProcessStreakMessage(Streak streak, ulong userId)
77 {
78 bool wasUser1 = streak.User1Id == userId;
79 bool wasUser2 = streak.User2Id == userId;
80
81 bool previousUser1Sent = streak.User1MessageSent;
82 bool previousUser2Sent = streak.User2MessageSent;
83
84 if (wasUser1 && !streak.User1MessageSent)
85 {
86 streak.User1MessageSent = true;
87 }
88 else if (wasUser2 && !streak.User2MessageSent)
89 {
90 streak.User2MessageSent = true;
91 }
92
93 if (streak.User1MessageSent && streak.User2MessageSent &&
94 !(previousUser1Sent && previousUser2Sent))
95 {
96 streak.StreakNumber++;
97 }
98 }
99
100 public async Task<Streak?> CreateStreakAsync(ulong user1Id, ulong user2Id, ulong serverId, ulong channelId = 0)
101 {
102 var streaks = serverId == 0
103 ? await _context.Streaks
104 .Where(s => s.ChannelId == channelId &&
105 (s.User1Id == user1Id || s.User2Id == user1Id))
106 .ToListAsync()
107 : await _context.Streaks
108 .Where(s => s.ServerId == serverId &&
109 (s.User1Id == user1Id || s.User2Id == user1Id))
110 .ToListAsync();
111
112 if (streaks.Any(c =>
113 (c.User1Id == user1Id && c.User2Id == user2Id) || (c.User1Id == user2Id && c.User2Id == user1Id)))
114 {
115 return null;
116 }
117
118 var now = DateTime.UtcNow;
119 var streak = new Streak
120 {
121 CreatedDate = now,
122 User1Id = user1Id,
123 User2Id = user2Id,
124 ServerId = serverId,
125 ChannelId = channelId,
126 User1MessageSent = false,
127 User2MessageSent = false,
128 StreakNumber = 0,
129 LastResetCheck = now.AddDays(1)
130 };
131
132 _context.Streaks.Add(streak);
133 await _context.SaveChangesAsync();
134
135 if (serverId == 0)
136 {
137 await _channelService.CreateDmStreakMessageAsync(channelId, streak.StreakNumber, streak.CreatedDate.TimeOfDay);
138 }
139 else
140 {
141 await _channelService.UpdateStreakChannelAsync(serverId, streak.StreakNumber);
142 await _channelService.UpdateTimeChannelAsync(serverId, streak.CreatedDate.TimeOfDay);
143 }
144
145 return streak;
146 }
147
148 public async Task<List<Streak>> CheckStreaksAsync(ulong user1Id)
149 {
150 var streaks = await _context.Streaks
151 .Where(s => s.User1Id == user1Id || s.User2Id == user1Id)
152 .ToListAsync();
153
154 return streaks;
155 }
156
157 public async Task<Streak?> GetStreakAsync(ulong user1Id, ulong user2Id)
158 {
159 return await _context.Streaks
160 .Where(s => (s.User1Id == user1Id && s.User2Id == user2Id) || (s.User1Id == user2Id && s.User2Id == user1Id))
161 .FirstOrDefaultAsync();
162 }
163
164 public async Task<string> RestoreStreakAsync(ulong user1Id, ulong user2Id)
165 {
166 var streak = await GetStreakAsync(user1Id, user2Id);
167
168 if (streak == null)
169 {
170 return $"No streak found between <@{user1Id}> and <@{user2Id}>.";
171 }
172
173 if (streak.MonthlyRestores <= 0)
174 {
175 return "No restores available this month. Restores reset at the start of each month.";
176 }
177
178 if (streak.StreakNumberToRestore == null || streak.StreakNumber >= streak.StreakNumberToRestore)
179 {
180 return "No broken streak to restore.";
181 }
182
183 var restoredNumber = streak.StreakNumberToRestore.Value;
184 if (streak.User1MessageSent && streak.User2MessageSent)
185 {
186 restoredNumber++;
187 }
188 streak.StreakNumber = restoredNumber;
189 streak.StreakNumberToRestore = null;
190 streak.MonthlyRestores--;
191
192 await _context.SaveChangesAsync();
193
194 if (streak.ServerId != 0)
195 {
196 await _channelService.UpdateStreakChannelAsync(streak.ServerId, streak.StreakNumber, !(streak.User1MessageSent && streak.User2MessageSent));
197 }
198 else
199 {
200 var channel = await _context.Channels
201 .FirstOrDefaultAsync(c => c.ChannelId == streak.ChannelId && c.ChannelType == ChannelType.DmChannel);
202
203 if (channel?.MessageId != null)
204 {
205 await _channelService.UpdateDmStreakMessageAsync(streak.ChannelId, channel.MessageId.Value, streak.StreakNumber, streak.CreatedDate.TimeOfDay);
206 }
207 }
208
209 return $"Streak restored to {restoredNumber}! You have {streak.MonthlyRestores} restore(s) remaining this month.";
210 }
211
212 public async Task<bool?> DeleteStreakAsync(ulong user1Id, ulong user2Id)
213 {
214 var streak = await GetStreakAsync(user1Id, user2Id);
215
216 if (streak == null)
217 {
218 return null;
219 }
220
221 var channels = await _context.Channels
222 .Where(c => c.ServerId == streak.ServerId)
223 .ToListAsync();
224
225 foreach (var channel in channels)
226 {
227 await _channelService.DeleteChannelAsync(channel.ChannelId);
228 }
229
230 return true;
231 }
232}