diff --git a/CompatBot/CompatBot.csproj b/CompatBot/CompatBot.csproj index 0b0450f1..32eccab1 100644 --- a/CompatBot/CompatBot.csproj +++ b/CompatBot/CompatBot.csproj @@ -43,9 +43,7 @@ - - diff --git a/CompatBot/Config.cs b/CompatBot/Config.cs index 6c5156b1..528e7b1f 100644 --- a/CompatBot/Config.cs +++ b/CompatBot/Config.cs @@ -85,6 +85,7 @@ namespace CompatBot public static string IrdCachePath => config.GetValue(nameof(IrdCachePath), "./ird/"); public static double GameTitleMatchThreshold => config.GetValue(nameof(GameTitleMatchThreshold), 0.57); public static byte[] CryptoSalt => Convert.FromBase64String(config.GetValue(nameof(CryptoSalt), "")); + public static string RenameNameSuffix => config.GetValue(nameof(RenameNameSuffix), " (Rule 7)"); internal static class AllowedMentions { diff --git a/CompatBot/Database/DbImporter.cs b/CompatBot/Database/DbImporter.cs index 01a438d6..9930b1d2 100644 --- a/CompatBot/Database/DbImporter.cs +++ b/CompatBot/Database/DbImporter.cs @@ -1,5 +1,9 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; using System.Threading; using System.Threading.Tasks; using CompatBot.Database.Migrations; @@ -7,6 +11,7 @@ using CompatBot.Utils; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Migrations.Internal; +using PsnClient.Utils; namespace CompatBot.Database { @@ -146,5 +151,99 @@ namespace CompatBot.Database } return dbPath; } + + public static async Task ImportNamesPool(ThumbnailDb db, CancellationToken cancellationToken) + { + Config.Log.Debug("Importing name pool..."); + var rootDir = Environment.CurrentDirectory; + while (rootDir is not null && !Directory.EnumerateFiles(rootDir, "names_*.txt", SearchOption.TopDirectoryOnly).Any()) + rootDir = Path.GetDirectoryName(rootDir); + if (rootDir is null) + { + Config.Log.Error("Couldn't find any name sources"); + return db.NamePool.Any(); + } + + var resources = Directory.GetFiles(rootDir, "names_*.txt", SearchOption.TopDirectoryOnly) + .OrderBy(f => f) + .ToList(); + if (resources.Count == 0) + { + Config.Log.Error("Couldn't find any name sources (???)"); + return db.NamePool.Any(); + } + + var timestamp = -1L; + using (var sha256 = System.Security.Cryptography.SHA256.Create()) + { + byte[] buf; + foreach (var path in resources) + { + var fileInfo = new FileInfo(path); + buf = BitConverter.GetBytes(fileInfo.Length); + sha256.TransformBlock(buf, 0, buf.Length, null, 0); + } + buf = Encoding.UTF8.GetBytes(Config.RenameNameSuffix); + buf = sha256.TransformFinalBlock(buf, 0, buf.Length); + timestamp = BitConverter.ToInt64(buf, 0); + } + + const string renameStateKey = "rename-name-pool"; + var stateEntry = db.State.FirstOrDefault(n => n.Locale == renameStateKey); + if (stateEntry?.Timestamp == timestamp) + { + Config.Log.Info("Name pool is up-to-date"); + return true; + } + + Config.Log.Info("Updating name pool..."); + try + { + var names = new HashSet(); + foreach (var resourcePath in resources) + { + await using var stream = File.Open(resourcePath, FileMode.Open, FileAccess.Read, FileShare.Read); + using var reader = new StreamReader(stream); + while (await reader.ReadLineAsync().ConfigureAwait(false) is string line) + { + if (line.Length < 2 || line.StartsWith("#")) + continue; + + var commentPos = line.IndexOf(" ("); + if (commentPos > 1) + line = line.Substring(0, commentPos); + line = line.Trim() + .Replace(" ", " ") + .Replace('`', '\'') // consider ’ + .Replace("\"", "\\\""); + if (line.Length + Config.RenameNameSuffix.Length > 32) + continue; + + if (line.Contains('@') + || line.Contains('#') + || line.Contains(':')) + continue; + + names.Add(line); + } + } + await using var tx = await db.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); + db.NamePool.RemoveRange(db.NamePool); + foreach (var name in names) + await db.NamePool.AddAsync(new() {Name = name}, cancellationToken).ConfigureAwait(false); + if (stateEntry is null) + await db.State.AddAsync(new() {Locale = renameStateKey, Timestamp = timestamp}, cancellationToken).ConfigureAwait(false); + else + stateEntry.Timestamp = timestamp; + await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + await tx.CommitAsync(cancellationToken).ConfigureAwait(false); + return names.Count > 0; + } + catch (Exception e) + { + Config.Log.Error(e); + return false; + } + } } } \ No newline at end of file diff --git a/CompatBot/Database/Migrations/ThumbnailDb/20210309212939_AddUserNamePool.Designer.cs b/CompatBot/Database/Migrations/ThumbnailDb/20210309212939_AddUserNamePool.Designer.cs new file mode 100644 index 00000000..e974dcc1 --- /dev/null +++ b/CompatBot/Database/Migrations/ThumbnailDb/20210309212939_AddUserNamePool.Designer.cs @@ -0,0 +1,266 @@ +// +using System; +using CompatBot.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace CompatBot.Migrations +{ + [DbContext(typeof(ThumbnailDb))] + [Migration("20210309212939_AddUserNamePool")] + partial class AddUserNamePool + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.3"); + + modelBuilder.Entity("CompatBot.Database.Fortune", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("content"); + + b.HasKey("Id") + .HasName("id"); + + b.ToTable("fortune"); + }); + + modelBuilder.Entity("CompatBot.Database.Metacritic", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("CriticScore") + .HasColumnType("INTEGER") + .HasColumnName("critic_score"); + + b.Property("Notes") + .HasColumnType("TEXT") + .HasColumnName("notes"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("title"); + + b.Property("UserScore") + .HasColumnType("INTEGER") + .HasColumnName("user_score"); + + b.HasKey("Id") + .HasName("id"); + + b.ToTable("metacritic"); + }); + + modelBuilder.Entity("CompatBot.Database.NamePool", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("id"); + + b.ToTable("name_pool"); + }); + + modelBuilder.Entity("CompatBot.Database.State", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Locale") + .HasColumnType("TEXT") + .HasColumnName("locale"); + + b.Property("Timestamp") + .HasColumnType("INTEGER") + .HasColumnName("timestamp"); + + b.HasKey("Id") + .HasName("id"); + + b.HasIndex("Locale") + .IsUnique() + .HasDatabaseName("state_locale"); + + b.HasIndex("Timestamp") + .HasDatabaseName("state_timestamp"); + + b.ToTable("state"); + }); + + modelBuilder.Entity("CompatBot.Database.SyscallInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Function") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("function"); + + b.HasKey("Id") + .HasName("id"); + + b.HasIndex("Function") + .HasDatabaseName("syscall_info_function"); + + b.ToTable("syscall_info"); + }); + + modelBuilder.Entity("CompatBot.Database.SyscallToProductMap", b => + { + b.Property("ProductId") + .HasColumnType("INTEGER") + .HasColumnName("product_id"); + + b.Property("SyscallInfoId") + .HasColumnType("INTEGER") + .HasColumnName("syscall_info_id"); + + b.HasKey("ProductId", "SyscallInfoId") + .HasName("id"); + + b.HasIndex("SyscallInfoId") + .HasDatabaseName("ix_syscall_to_product_map_syscall_info_id"); + + b.ToTable("syscall_to_product_map"); + }); + + modelBuilder.Entity("CompatBot.Database.Thumbnail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("CompatibilityChangeDate") + .HasColumnType("INTEGER") + .HasColumnName("compatibility_change_date"); + + b.Property("CompatibilityStatus") + .HasColumnType("INTEGER") + .HasColumnName("compatibility_status"); + + b.Property("ContentId") + .HasColumnType("TEXT") + .HasColumnName("content_id"); + + b.Property("EmbedColor") + .HasColumnType("INTEGER") + .HasColumnName("embed_color"); + + b.Property("EmbeddableUrl") + .HasColumnType("TEXT") + .HasColumnName("embeddable_url"); + + b.Property("MetacriticId") + .HasColumnType("INTEGER") + .HasColumnName("metacritic_id"); + + b.Property("Name") + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.Property("ProductCode") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("product_code"); + + b.Property("Timestamp") + .HasColumnType("INTEGER") + .HasColumnName("timestamp"); + + b.Property("Url") + .HasColumnType("TEXT") + .HasColumnName("url"); + + b.HasKey("Id") + .HasName("id"); + + b.HasIndex("ContentId") + .IsUnique() + .HasDatabaseName("thumbnail_content_id"); + + b.HasIndex("MetacriticId") + .HasDatabaseName("ix_thumbnail_metacritic_id"); + + b.HasIndex("ProductCode") + .IsUnique() + .HasDatabaseName("thumbnail_product_code"); + + b.HasIndex("Timestamp") + .HasDatabaseName("thumbnail_timestamp"); + + b.ToTable("thumbnail"); + }); + + modelBuilder.Entity("CompatBot.Database.SyscallToProductMap", b => + { + b.HasOne("CompatBot.Database.Thumbnail", "Product") + .WithMany("SyscallToProductMap") + .HasForeignKey("ProductId") + .HasConstraintName("fk_syscall_to_product_map__thumbnail_product_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CompatBot.Database.SyscallInfo", "SyscallInfo") + .WithMany("SyscallToProductMap") + .HasForeignKey("SyscallInfoId") + .HasConstraintName("fk_syscall_to_product_map_syscall_info_syscall_info_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("SyscallInfo"); + }); + + modelBuilder.Entity("CompatBot.Database.Thumbnail", b => + { + b.HasOne("CompatBot.Database.Metacritic", "Metacritic") + .WithMany() + .HasForeignKey("MetacriticId") + .HasConstraintName("fk_thumbnail_metacritic_metacritic_id"); + + b.Navigation("Metacritic"); + }); + + modelBuilder.Entity("CompatBot.Database.SyscallInfo", b => + { + b.Navigation("SyscallToProductMap"); + }); + + modelBuilder.Entity("CompatBot.Database.Thumbnail", b => + { + b.Navigation("SyscallToProductMap"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CompatBot/Database/Migrations/ThumbnailDb/20210309212939_AddUserNamePool.cs b/CompatBot/Database/Migrations/ThumbnailDb/20210309212939_AddUserNamePool.cs new file mode 100644 index 00000000..eb51eb02 --- /dev/null +++ b/CompatBot/Database/Migrations/ThumbnailDb/20210309212939_AddUserNamePool.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace CompatBot.Migrations +{ + public partial class AddUserNamePool : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "name_pool", + columns: table => new + { + id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + name = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("id", x => x.id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "name_pool"); + } + } +} diff --git a/CompatBot/Database/Migrations/ThumbnailDb/ThumbnailDbModelSnapshot.cs b/CompatBot/Database/Migrations/ThumbnailDb/ThumbnailDbModelSnapshot.cs index cadbf2b4..eff642e1 100644 --- a/CompatBot/Database/Migrations/ThumbnailDb/ThumbnailDbModelSnapshot.cs +++ b/CompatBot/Database/Migrations/ThumbnailDb/ThumbnailDbModelSnapshot.cs @@ -64,6 +64,24 @@ namespace CompatBot.Migrations b.ToTable("metacritic"); }); + modelBuilder.Entity("CompatBot.Database.NamePool", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("id"); + + b.ToTable("name_pool"); + }); + modelBuilder.Entity("CompatBot.Database.State", b => { b.Property("Id") diff --git a/CompatBot/Database/ThumbnailDb.cs b/CompatBot/Database/ThumbnailDb.cs index add4a818..34e5abde 100644 --- a/CompatBot/Database/ThumbnailDb.cs +++ b/CompatBot/Database/ThumbnailDb.cs @@ -14,6 +14,7 @@ namespace CompatBot.Database public DbSet SyscallToProductMap { get; set; } = null!; public DbSet Metacritic { get; set; } = null!; public DbSet Fortune { get; set; } = null!; + public DbSet NamePool { get; set; } = null!; protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { @@ -35,6 +36,7 @@ namespace CompatBot.Database modelBuilder.Entity().HasIndex(sci => sci.Function).HasDatabaseName("syscall_info_function"); modelBuilder.Entity().HasKey(m => new {m.ProductId, m.SyscallInfoId}); modelBuilder.Entity(); + modelBuilder.Entity(); //configure default policy of Id being the primary key modelBuilder.ConfigureDefaultPkConvention(); @@ -126,4 +128,11 @@ namespace CompatBot.Database [Required] public string Content { get; set; } = null!; } + + internal class NamePool + { + public int Id { get; set; } + [Required] + public string Name { get; set; } = null!; + } } diff --git a/CompatBot/EventHandlers/UsernameZalgoMonitor.cs b/CompatBot/EventHandlers/UsernameZalgoMonitor.cs index 06eed17b..d5d658ee 100644 --- a/CompatBot/EventHandlers/UsernameZalgoMonitor.cs +++ b/CompatBot/EventHandlers/UsernameZalgoMonitor.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Text; using System.Threading.Tasks; using CompatApiClient.Utils; +using CompatBot.Database; using CompatBot.Utils; using DSharpPlus; using DSharpPlus.Entities; @@ -166,8 +168,10 @@ namespace CompatBot.EventHandlers { var hash = userId.GetHashCode(); var rng = new Random(hash); - var name = NamesPool.List[rng.Next(NamesPool.NameCount)]; - return name + NamesPool.NameSuffix; + using var db = new ThumbnailDb(); + var count = db.NamePool.Count(); + var name = db.NamePool.Skip(rng.Next(count)).First().Name; + return name + Config.RenameNameSuffix; } } } diff --git a/CompatBot/Program.cs b/CompatBot/Program.cs index ebf20a23..37a5985c 100644 --- a/CompatBot/Program.cs +++ b/CompatBot/Program.cs @@ -101,9 +101,14 @@ namespace CompatBot return; await using (var db = new ThumbnailDb()) + { if (!await DbImporter.UpgradeAsync(db, Config.Cts.Token)) return; + if (!await DbImporter.ImportNamesPool(db, Config.Cts.Token)) + return; + } + await SqlConfiguration.RestoreAsync().ConfigureAwait(false); Config.Log.Debug("Restored configuration variables from persistent storage"); diff --git a/README.md b/README.md index 4f871f76..e2e30de4 100644 --- a/README.md +++ b/README.md @@ -65,5 +65,4 @@ External resources that need manual updates ------------------------------------------- * [Unicode Confusables](http://www.unicode.org/Public/security/latest/confusables.txt), for Homoglyph checks * [Windows Error Codes](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/), for error decoding on non-Windows host -* Optionally [Redump disc key database](http://redump.org/downloads/) in text format (requires membership) * Optionally pool of names (one name per line), files named as `names_.txt` diff --git a/SourceGenerators/NamesSourceGenerator.cs b/SourceGenerators/NamesSourceGenerator.cs deleted file mode 100644 index 465fb99e..00000000 --- a/SourceGenerators/NamesSourceGenerator.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; - -namespace SourceGenerators -{ - [Generator] - public class NamesSourceGenerator : ISourceGenerator - { - private const string Indent = " "; - private const string NameSuffix = " (Rule 7)"; - //private const int DiscordUsernameLengthLimit = 32-10; //" #12345678" - private const int DiscordUsernameLengthLimit = 32; - - public void Initialize(GeneratorInitializationContext context) - { - } - - public void Execute(GeneratorExecutionContext context) - { - var resources = context.AdditionalFiles - .Where(f => Path.GetFileName(f.Path).ToLower().StartsWith("names_") && f.Path.ToLower().EndsWith(".txt")) - .OrderBy(f => f.Path) - .ToList(); - if (resources.Count == 0) - return; - - var names = new HashSet(); - foreach (var resource in resources) - { - using var stream = File.Open(resource.Path, FileMode.Open, FileAccess.Read, FileShare.Read); - using var reader = new StreamReader(stream); - while (reader.ReadLine() is string line) - { - if (line.Length < 2 || line.StartsWith("#")) - continue; - - var commentPos = line.IndexOf(" ("); - if (commentPos > 1) - line = line.Substring(0, commentPos); - line = line.Trim() - .Replace(" ", " ") - .Replace('`', '\'') // consider ’ - .Replace("\"", "\\\""); - //if (line.Length + NameSuffix.Length > DiscordUsernameLengthLimit) - // line = line.Split(' ')[0]; - if (line.Length + NameSuffix.Length > DiscordUsernameLengthLimit) - continue; - - if (line.Contains('@') - || line.Contains('#') - || line.Contains(':')) - continue; - - names.Add(line); - //if (line.Contains(' ')) - // names.Add(line.Split(' ')[0]); - } - } - - if (!context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.RootNamespace", out var ns)) - ns = context.Compilation.AssemblyName; - var cn = "NamesPool"; - var result = new StringBuilder() - .AppendLine("using System.Collections.Generic;") - .AppendLine() - .AppendLine($"namespace {ns}") - .AppendLine("{") - .AppendLine($"{Indent}public static class {cn}") - .AppendLine($"{Indent}{{") - .AppendLine($"{Indent}{Indent}public const string NameSuffix = \"{NameSuffix}\";") - .AppendLine() - .AppendLine($"{Indent}{Indent}public const int NameCount = {names.Count};") - .AppendLine() - .AppendLine($"{Indent}{Indent}public static readonly List List = new()") - .AppendLine($"{Indent}{Indent}{{"); - foreach (var name in names.OrderBy(n => n)) - result.AppendLine($"{Indent}{Indent}{Indent}\"{name}\","); - result.AppendLine($"{Indent}{Indent}}};") - .AppendLine($"{Indent}}}") - .AppendLine("}"); - - context.AddSource($"{cn}.Generated.cs", SourceText.From(result.ToString(), Encoding.UTF8)); - - } - } -} \ No newline at end of file