diff --git a/Hammer/Commands/MigrateDatabaseCommand.cs b/Hammer/Commands/MigrateDatabaseCommand.cs new file mode 100644 index 0000000..405a44e --- /dev/null +++ b/Hammer/Commands/MigrateDatabaseCommand.cs @@ -0,0 +1,59 @@ +using DSharpPlus; +using DSharpPlus.Entities; +using DSharpPlus.SlashCommands; +using DSharpPlus.SlashCommands.Attributes; +using Hammer.Services; +using Microsoft.Extensions.Logging; + +namespace Hammer.Commands; + +internal sealed class MigrateDatabaseCommand : ApplicationCommandModule +{ + private readonly ILogger _logger; + private readonly DatabaseService _databaseService; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The database service. + public MigrateDatabaseCommand(ILogger logger, DatabaseService databaseService) + { + _logger = logger; + _databaseService = databaseService; + } + + [SlashCommand("migratedb", "Migrates the SQLite database to MySQL/MariaDB.", false)] + [SlashRequireGuild] + public async Task MigrateDatabaseAsync(InteractionContext context) + { + var embed = new DiscordEmbedBuilder(); + embed.WithColor(DiscordColor.Orange); + embed.WithTitle("⏳ Migration in progress"); + embed.WithDescription("Please wait while the database is migrated..."); + + await context.CreateResponseAsync(embed); + try + { + int rows = await _databaseService.MigrateAsync(); + + var builder = new DiscordWebhookBuilder(); + embed.WithColor(DiscordColor.Green); + embed.WithTitle("✅ Migration complete"); + embed.WithDescription($"All data has been successfully migrated. {rows:N0} rows affected"); + builder.AddEmbed(embed); + await context.EditResponseAsync(builder); + } + catch (Exception exception) + { + _logger.LogError(exception, "Failed to migrate"); + var builder = new DiscordWebhookBuilder(); + embed.WithColor(DiscordColor.Red); + embed.WithTitle($"⚠️ Migration failed: {exception.GetType().Name}"); + embed.WithDescription($"{exception.GetType().Name} was thrown during migration: {exception.Message}\n\n" + + "View the log for more details"); + builder.AddEmbed(embed); + await context.EditResponseAsync(builder); + } + } +} diff --git a/Hammer/Data/HammerContext.cs b/Hammer/Data/HammerContext.cs index 8e093ab..96e2f56 100644 --- a/Hammer/Data/HammerContext.cs +++ b/Hammer/Data/HammerContext.cs @@ -9,7 +9,7 @@ namespace Hammer.Data; /// -/// Represents a session with the hammer.db database. +/// Represents a session with the Hammer database. /// internal sealed class HammerContext : DbContext { diff --git a/Hammer/Data/MigrationContext.cs b/Hammer/Data/MigrationContext.cs new file mode 100644 index 0000000..10896f6 --- /dev/null +++ b/Hammer/Data/MigrationContext.cs @@ -0,0 +1,116 @@ +using Hammer.Data.EntityConfigurations; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using MuteConfiguration = Hammer.Data.EntityConfigurations.MuteConfiguration; + +namespace Hammer.Data; + +/// +/// Represents a session with the hammer.db database. +/// +internal sealed class MigrationContext : DbContext +{ + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + public MigrationContext(ILogger logger) + { + _logger = logger; + } + + /// + /// Gets the set of alt accounts. + /// + /// The set of alt accounts. + public DbSet AltAccounts { get; private set; } = null!; + + /// + /// Gets the set of users who are blocked from making reports. + /// + /// The set of blocked reporters. + public DbSet BlockedReporters { get; private set; } = null!; + + /// + /// Gets the set of staff-deleted messages. + /// + /// The set of staff-deleted messages. + public DbSet DeletedMessages { get; private set; } = null!; + + /// + /// Gets the set of infractions. + /// + /// The set of infractions. + public DbSet Infractions { get; private set; } = null!; + + /// + /// Gets the set of member notes. + /// + /// The set of member notes. + public DbSet MemberNotes { get; private set; } = null!; + + /// + /// Gets the set of mutes. + /// + /// The set of mutes. + public DbSet Mutes { get; private set; } = null!; + + /// + /// Gets the set of reported messages. + /// + /// The set of reported messages. + public DbSet ReportedMessages { get; private set; } = null!; + + /// + /// Gets the set of rules. + /// + /// The set of rules. + public DbSet Rules { get; private set; } = null!; + + /// + /// Gets the set of staff messages. + /// + /// The set of staff messages. + public DbSet StaffMessages { get; private set; } = null!; + + /// + /// Gets the set of temporary bans. + /// + /// The set of temporary bans. + public DbSet TemporaryBans { get; private set; } = null!; + + /// + /// Gets the set of tracked messages. + /// + /// The set of tracked messages. + public DbSet TrackedMessages { get; private set; } = null!; + + /// + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + + _logger.LogDebug("Using SQLite database provider"); + optionsBuilder.UseSqlite("Data Source='data/hammer.db'"); + } + + /// + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.ApplyConfiguration(new AltAccountConfiguration(false)); + modelBuilder.ApplyConfiguration(new BlockedReporterConfiguration(false)); + modelBuilder.ApplyConfiguration(new DeletedMessageConfiguration(false)); + modelBuilder.ApplyConfiguration(new InfractionConfiguration(false)); + modelBuilder.ApplyConfiguration(new MemberNoteConfiguration(false)); + modelBuilder.ApplyConfiguration(new MuteConfiguration(false)); + modelBuilder.ApplyConfiguration(new StaffMessageConfiguration()); + modelBuilder.ApplyConfiguration(new ReportedMessageConfiguration()); + modelBuilder.ApplyConfiguration(new TemporaryBanConfiguration(false)); + modelBuilder.ApplyConfiguration(new TrackedMessageConfiguration(false)); + modelBuilder.ApplyConfiguration(new RuleConfiguration()); + } +} diff --git a/Hammer/Program.cs b/Hammer/Program.cs index 37413f6..822c86b 100644 --- a/Hammer/Program.cs +++ b/Hammer/Program.cs @@ -33,6 +33,7 @@ })); builder.Services.AddDbContextFactory(); +builder.Services.AddDbContextFactory(); builder.Services.AddHostedSingleton(); builder.Services.AddSingleton(); diff --git a/Hammer/Services/BotService.cs b/Hammer/Services/BotService.cs index 060d3e1..d2ba42e 100644 --- a/Hammer/Services/BotService.cs +++ b/Hammer/Services/BotService.cs @@ -84,6 +84,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) slashCommands.RegisterCommands(); slashCommands.RegisterCommands(); slashCommands.RegisterCommands(); + slashCommands.RegisterCommands(); slashCommands.RegisterCommands(); slashCommands.RegisterCommands(); slashCommands.RegisterCommands(); diff --git a/Hammer/Services/DatabaseService.cs b/Hammer/Services/DatabaseService.cs index 571a46a..e64c486 100644 --- a/Hammer/Services/DatabaseService.cs +++ b/Hammer/Services/DatabaseService.cs @@ -12,35 +12,87 @@ internal sealed class DatabaseService : BackgroundService { private readonly ILogger _logger; private readonly IDbContextFactory _dbContextFactory; + private readonly IDbContextFactory _migrationContextFactory; /// /// Initializes a new instance of the class. /// /// The logger. - /// The DbContext factory. - public DatabaseService(ILogger logger, IDbContextFactory dbContextFactory) + /// The factory. + /// The factory. + public DatabaseService(ILogger logger, + IDbContextFactory dbContextFactory, + IDbContextFactory migrationContextFactory) { _logger = logger; _dbContextFactory = dbContextFactory; + _migrationContextFactory = migrationContextFactory; + } + + /// + /// Migrates the database from one source to another. + /// + public async Task MigrateAsync() + { + await using HammerContext context = await _dbContextFactory.CreateDbContextAsync(); + await context.Database.EnsureCreatedAsync(); + + if (!context.IsMySql) + { + _logger.LogWarning("Cannot migrate from SQLite to SQLite. This operation will be skipped"); + return 0; + } + + await using MigrationContext migration = await _migrationContextFactory.CreateDbContextAsync(); + + _logger.LogInformation("Migrating database"); + context.AltAccounts.AddRange(migration.AltAccounts); + context.BlockedReporters.AddRange(migration.BlockedReporters); + context.DeletedMessages.AddRange(migration.DeletedMessages); + context.Infractions.AddRange(migration.Infractions); + context.MemberNotes.AddRange(migration.MemberNotes); + context.Mutes.AddRange(migration.Mutes); + context.ReportedMessages.AddRange(migration.ReportedMessages); + context.Rules.AddRange(migration.Rules); + context.StaffMessages.AddRange(migration.StaffMessages); + context.TemporaryBans.AddRange(migration.TemporaryBans); + context.TrackedMessages.AddRange(migration.TrackedMessages); + + _logger.LogDebug("Saving database"); + await context.SaveChangesAsync(); + + int count = 0; + count += context.AltAccounts.Count(); + count += context.BlockedReporters.Count(); + count += context.DeletedMessages.Count(); + count += context.Infractions.Count(); + count += context.MemberNotes.Count(); + count += context.Mutes.Count(); + count += context.ReportedMessages.Count(); + count += context.Rules.Count(); + count += context.StaffMessages.Count(); + count += context.TemporaryBans.Count(); + count += context.TrackedMessages.Count(); + return count; } /// protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - await CreateDatabaseAsync().ConfigureAwait(false); + await CreateDatabaseAsync(); } private async Task CreateDatabaseAsync() { - await using HammerContext context = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false); + await using HammerContext context = await _dbContextFactory.CreateDbContextAsync(); if (Environment.GetEnvironmentVariable("USE_MYSQL") != "1") { _logger.LogInformation("Creating database"); - await context.Database.EnsureCreatedAsync().ConfigureAwait(false); + await context.Database.EnsureCreatedAsync(); } _logger.LogInformation("Applying migrations"); - await context.Database.MigrateAsync().ConfigureAwait(false); + await context.Database.MigrateAsync(); } }