C# Discord bot made using NetCord for keeping a TikTok-style streak
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: adds restore command

also adds a logging change

Minito 857fc1a8 2e92de73

+314 -2
+36
Commands/RestoreCommand.cs
··· 1 + using NetCord; 2 + using NetCord.Services.ApplicationCommands; 3 + using StreakBot.Services; 4 + 5 + namespace StreakBot.Commands; 6 + 7 + public class RestoreCommand : ApplicationCommandModule<ApplicationCommandContext> 8 + { 9 + private readonly StreakService _streakService; 10 + 11 + public RestoreCommand(StreakService streakService) 12 + { 13 + _streakService = streakService; 14 + } 15 + 16 + [SlashCommand("restore", "Restores your streak with another user after it has been reset. Limited to 5 restores per month.")] 17 + public async Task<string> Restore( 18 + [SlashCommandParameter(Name = "user", Description = "The user to restore your streak with")] 19 + User user) 20 + { 21 + var initiatorId = Context.User.Id; 22 + var targetId = user.Id; 23 + 24 + if (initiatorId == targetId) 25 + { 26 + return "You can't restore a streak with yourself!"; 27 + } 28 + 29 + if (user.IsBot) 30 + { 31 + return "You can't restore a streak with a bot!"; 32 + } 33 + 34 + return await _streakService.RestoreStreakAsync(initiatorId, targetId); 35 + } 36 + }
+2
Data/Entities/Streak.cs
··· 13 13 public int StreakNumber { get; set; } 14 14 public DateTime LastResetCheck { get; set; } 15 15 public bool ReminderSent { get; set; } 16 + public int? StreakNumberToRestore { get; set; } 17 + public int MonthlyRestores { get; set; } = 5; 16 18 }
+95
Migrations/20260318000000_AddStreakRestore.Designer.cs
··· 1 + // <auto-generated /> 2 + using System; 3 + using Microsoft.EntityFrameworkCore; 4 + using Microsoft.EntityFrameworkCore.Infrastructure; 5 + using Microsoft.EntityFrameworkCore.Migrations; 6 + using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 + using StreakBot.Data; 8 + 9 + #nullable disable 10 + 11 + namespace StreakBot.Migrations 12 + { 13 + [DbContext(typeof(StreakDbContext))] 14 + [Migration("20260318000000_AddStreakRestore")] 15 + partial class AddStreakRestore 16 + { 17 + /// <inheritdoc /> 18 + protected override void BuildTargetModel(ModelBuilder modelBuilder) 19 + { 20 + #pragma warning disable 612, 618 21 + modelBuilder.HasAnnotation("ProductVersion", "10.0.0"); 22 + 23 + modelBuilder.Entity("StreakBot.Data.Entities.Channel", b => 24 + { 25 + b.Property<int>("Id") 26 + .ValueGeneratedOnAdd() 27 + .HasColumnType("INTEGER"); 28 + 29 + b.Property<ulong>("ChannelId") 30 + .HasColumnType("INTEGER"); 31 + 32 + b.Property<int>("ChannelType") 33 + .HasColumnType("INTEGER"); 34 + 35 + b.Property<ulong?>("MessageId") 36 + .HasColumnType("INTEGER"); 37 + 38 + b.Property<ulong>("ServerId") 39 + .HasColumnType("INTEGER"); 40 + 41 + b.HasKey("Id"); 42 + 43 + b.ToTable("Channels"); 44 + }); 45 + 46 + modelBuilder.Entity("StreakBot.Data.Entities.Streak", b => 47 + { 48 + b.Property<int>("Id") 49 + .ValueGeneratedOnAdd() 50 + .HasColumnType("INTEGER"); 51 + 52 + b.Property<ulong>("ChannelId") 53 + .HasColumnType("INTEGER"); 54 + 55 + b.Property<DateTime>("CreatedDate") 56 + .HasColumnType("TEXT"); 57 + 58 + b.Property<DateTime>("LastResetCheck") 59 + .HasColumnType("TEXT"); 60 + 61 + b.Property<int>("MonthlyRestores") 62 + .HasColumnType("INTEGER"); 63 + 64 + b.Property<bool>("ReminderSent") 65 + .HasColumnType("INTEGER"); 66 + 67 + b.Property<ulong>("ServerId") 68 + .HasColumnType("INTEGER"); 69 + 70 + b.Property<int>("StreakNumber") 71 + .HasColumnType("INTEGER"); 72 + 73 + b.Property<int?>("StreakNumberToRestore") 74 + .HasColumnType("INTEGER"); 75 + 76 + b.Property<ulong>("User1Id") 77 + .HasColumnType("INTEGER"); 78 + 79 + b.Property<bool>("User1MessageSent") 80 + .HasColumnType("INTEGER"); 81 + 82 + b.Property<ulong>("User2Id") 83 + .HasColumnType("INTEGER"); 84 + 85 + b.Property<bool>("User2MessageSent") 86 + .HasColumnType("INTEGER"); 87 + 88 + b.HasKey("Id"); 89 + 90 + b.ToTable("Streaks"); 91 + }); 92 + #pragma warning restore 612, 618 93 + } 94 + } 95 + }
+39
Migrations/20260318000000_AddStreakRestore.cs
··· 1 + using Microsoft.EntityFrameworkCore.Migrations; 2 + 3 + #nullable disable 4 + 5 + namespace StreakBot.Migrations 6 + { 7 + /// <inheritdoc /> 8 + public partial class AddStreakRestore : Migration 9 + { 10 + /// <inheritdoc /> 11 + protected override void Up(MigrationBuilder migrationBuilder) 12 + { 13 + migrationBuilder.AddColumn<int>( 14 + name: "StreakNumberToRestore", 15 + table: "Streaks", 16 + type: "INTEGER", 17 + nullable: true); 18 + 19 + migrationBuilder.AddColumn<int>( 20 + name: "MonthlyRestores", 21 + table: "Streaks", 22 + type: "INTEGER", 23 + nullable: false, 24 + defaultValue: 5); 25 + } 26 + 27 + /// <inheritdoc /> 28 + protected override void Down(MigrationBuilder migrationBuilder) 29 + { 30 + migrationBuilder.DropColumn( 31 + name: "StreakNumberToRestore", 32 + table: "Streaks"); 33 + 34 + migrationBuilder.DropColumn( 35 + name: "MonthlyRestores", 36 + table: "Streaks"); 37 + } 38 + } 39 + }
+6
Migrations/StreakDbContextModelSnapshot.cs
··· 61 61 b.Property<ulong>("ServerId") 62 62 .HasColumnType("INTEGER"); 63 63 64 + b.Property<int>("MonthlyRestores") 65 + .HasColumnType("INTEGER"); 66 + 64 67 b.Property<int>("StreakNumber") 68 + .HasColumnType("INTEGER"); 69 + 70 + b.Property<int?>("StreakNumberToRestore") 65 71 .HasColumnType("INTEGER"); 66 72 67 73 b.Property<ulong>("User1Id")
+1
Program.cs
··· 25 25 services.AddScoped<ChannelService>(); 26 26 services.AddScoped<StreakService>(); 27 27 services.AddHostedService<StreakResetBackgroundService>(); 28 + services.AddHostedService<MonthlyRestoreBackgroundService>(); 28 29 services.AddGatewayHandlers(typeof(Program).Assembly); 29 30 }) 30 31 .UseDiscordGateway(options =>
+78
Services/MonthlyRestoreBackgroundService.cs
··· 1 + using Microsoft.EntityFrameworkCore; 2 + using Microsoft.Extensions.DependencyInjection; 3 + using Microsoft.Extensions.Hosting; 4 + using Microsoft.Extensions.Logging; 5 + using StreakBot.Data; 6 + 7 + namespace StreakBot.Services; 8 + 9 + public class MonthlyRestoreBackgroundService : BackgroundService 10 + { 11 + private readonly IServiceScopeFactory _scopeFactory; 12 + private readonly ILogger<MonthlyRestoreBackgroundService> _logger; 13 + private readonly TimeSpan _checkInterval = TimeSpan.FromHours(24); 14 + private int _lastResetMonth = -1; 15 + 16 + public MonthlyRestoreBackgroundService( 17 + IServiceScopeFactory scopeFactory, 18 + ILogger<MonthlyRestoreBackgroundService> 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 + if (_lastResetMonth == -1) 31 + { 32 + var now = DateTime.UtcNow; 33 + 34 + _lastResetMonth = now.Month; 35 + } 36 + else 37 + { 38 + await CheckAndResetMonthlyRestoresAsync(); 39 + } 40 + } 41 + catch (Exception ex) 42 + { 43 + _logger.LogError(ex, "Error checking monthly restore reset"); 44 + } 45 + 46 + await Task.Delay(_checkInterval, stoppingToken); 47 + } 48 + } 49 + 50 + private async Task CheckAndResetMonthlyRestoresAsync() 51 + { 52 + var now = DateTime.UtcNow; 53 + 54 + var currentMonth = now.Month; 55 + 56 + // Only reset once per month (when the month changes) 57 + if (_lastResetMonth != currentMonth) 58 + { 59 + using var scope = _scopeFactory.CreateScope(); 60 + var context = scope.ServiceProvider.GetRequiredService<StreakDbContext>(); 61 + 62 + var streaks = await context.Streaks.ToListAsync(); 63 + 64 + foreach (var streak in streaks) 65 + { 66 + streak.MonthlyRestores = 5; 67 + } 68 + 69 + if (context.ChangeTracker.HasChanges()) 70 + { 71 + var count = await context.SaveChangesAsync(); 72 + _logger.LogInformation("Reset monthly restores for {Count} streak(s)", count); 73 + } 74 + 75 + _lastResetMonth = currentMonth; 76 + } 77 + } 78 + }
+2
Services/StreakResetBackgroundService.cs
··· 100 100 101 101 if (!streak.User1MessageSent || !streak.User2MessageSent) 102 102 { 103 + streak.StreakNumberToRestore = streak.StreakNumber; 103 104 streak.StreakNumber = 0; 105 + _logger.LogInformation("Resetting streak between {0} and {1}", streak.User1Id, streak.User2Id); 104 106 } 105 107 106 108 streak.User1MessageSent = false;
+55 -2
Services/StreakService.cs
··· 154 154 return streaks; 155 155 } 156 156 157 - public async Task<bool?> DeleteStreakAsync(ulong user1Id, ulong user2Id) 157 + public async Task<Streak?> GetStreakAsync(ulong user1Id, ulong user2Id) 158 158 { 159 - var streak = await _context.Streaks 159 + return await _context.Streaks 160 160 .Where(s => (s.User1Id == user1Id && s.User2Id == user2Id) || (s.User1Id == user2Id && s.User2Id == user1Id)) 161 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); 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); 162 215 163 216 if (streak == null) 164 217 {