From 1595452e31e59f13cbc890a77d0fab77b61a574d Mon Sep 17 00:00:00 2001 From: vycdev2 Date: Sat, 1 Aug 2026 12:45:42 +0000 Subject: [PATCH] fix: preserve activity level precision --- ...4127_RecalculateActivityLevels.Designer.cs | 1388 +++++++++++++++++ ...0260801124127_RecalculateActivityLevels.cs | 32 + Morpheus.Tests/ActivityLevelServiceTests.cs | 8 + Services/ActivityLevelService.cs | 2 +- 4 files changed, 1429 insertions(+), 1 deletion(-) create mode 100644 Migrations/20260801124127_RecalculateActivityLevels.Designer.cs create mode 100644 Migrations/20260801124127_RecalculateActivityLevels.cs diff --git a/Migrations/20260801124127_RecalculateActivityLevels.Designer.cs b/Migrations/20260801124127_RecalculateActivityLevels.Designer.cs new file mode 100644 index 0000000..afde4a5 --- /dev/null +++ b/Migrations/20260801124127_RecalculateActivityLevels.Designer.cs @@ -0,0 +1,1388 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Morpheus.Database; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Morpheus.Migrations +{ + [DbContext(typeof(DB))] + [Migration("20260801124127_RecalculateActivityLevels")] + partial class RecalculateActivityLevels + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Morpheus.Database.Models.BotSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("BotSettings"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.ButtonGamePress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("GuildId") + .HasColumnType("integer"); + + b.Property("InsertDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Score") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("InsertDate"); + + b.HasIndex("GuildId", "InsertDate"); + + b.HasIndex("UserId", "InsertDate"); + + b.ToTable("ButtonGamePresses"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.Channel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DiscordId") + .HasColumnType("numeric(20,0)"); + + b.Property("InsertDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("DiscordId") + .IsUnique(); + + b.ToTable("Channels"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.Guild", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DiscordId") + .HasColumnType("numeric(20,0)"); + + b.Property("HoneypotChannelId") + .HasColumnType("numeric(20,0)"); + + b.Property("InsertDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LevelUpMessages") + .HasColumnType("boolean"); + + b.Property("LevelUpMessagesChannelId") + .HasColumnType("numeric(20,0)"); + + b.Property("LevelUpQuotes") + .HasColumnType("boolean"); + + b.Property("LevelUpQuotesChannelId") + .HasColumnType("numeric(20,0)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinsChannelId") + .HasColumnType("numeric(20,0)"); + + b.Property("Prefix") + .IsRequired() + .HasColumnType("text"); + + b.Property("QuoteAddRequiredApprovals") + .HasColumnType("integer"); + + b.Property("QuoteRemoveRequiredApprovals") + .HasColumnType("integer"); + + b.Property("QuotesApprovalChannelId") + .HasColumnType("numeric(20,0)"); + + b.Property("SendHoneypotMessages") + .HasColumnType("boolean"); + + b.Property("UseActivityRoles") + .HasColumnType("boolean"); + + b.Property("UseGlobalQuotes") + .HasColumnType("boolean"); + + b.Property("WelcomeChannelId") + .HasColumnType("numeric(20,0)"); + + b.Property("WelcomeMessages") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("DiscordId") + .IsUnique(); + + b.ToTable("Guilds"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.Log", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("InsertDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Severity") + .HasColumnType("integer"); + + b.Property("Version") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("InsertDate", "Severity"); + + b.ToTable("Logs"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.Quote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Approved") + .HasColumnType("boolean"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuildId") + .HasColumnType("integer"); + + b.Property("InsertDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Removed") + .HasColumnType("boolean"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GuildId"); + + b.HasIndex("UserId"); + + b.HasIndex("InsertDate", "Approved", "Removed"); + + b.ToTable("Quotes"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.QuoteApproval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("InsertDate") + .HasColumnType("timestamp with time zone"); + + b.Property("QuoteApprovalMessageId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("numeric(20,0)"); + + b.HasKey("Id"); + + b.HasIndex("QuoteApprovalMessageId", "UserId") + .IsUnique(); + + b.ToTable("QuoteApprovals"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.QuoteApprovalMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApprovalMessageId") + .HasColumnType("numeric(20,0)"); + + b.Property("Approved") + .HasColumnType("boolean"); + + b.Property("InsertDate") + .HasColumnType("timestamp with time zone"); + + b.Property("QuoteId") + .HasColumnType("integer"); + + b.Property("Score") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("QuoteId"); + + b.HasIndex("Approved", "InsertDate"); + + b.ToTable("QuoteApprovalMessages"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.QuoteScore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("InsertDate") + .HasColumnType("timestamp with time zone"); + + b.Property("QuoteId") + .HasColumnType("integer"); + + b.Property("Score") + .HasColumnType("integer"); + + b.Property("UpdateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("QuoteId", "UserId") + .IsUnique(); + + b.ToTable("QuoteScores"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.ReactionRoleItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CustomId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Emoji") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReactionRoleMessageId") + .HasColumnType("integer"); + + b.Property("RoleId") + .HasColumnType("numeric(20,0)"); + + b.HasKey("Id"); + + b.HasIndex("ReactionRoleMessageId", "Emoji") + .IsUnique(); + + b.HasIndex("ReactionRoleMessageId", "RoleId") + .IsUnique(); + + b.ToTable("ReactionRoleItems"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.ReactionRoleMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelId") + .HasColumnType("numeric(20,0)"); + + b.Property("GuildId") + .HasColumnType("integer"); + + b.Property("InsertDate") + .HasColumnType("timestamp with time zone"); + + b.Property("MessageId") + .HasColumnType("numeric(20,0)"); + + b.Property("UseButtons") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("GuildId"); + + b.HasIndex("MessageId") + .IsUnique(); + + b.ToTable("ReactionRoleMessages"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.Reminder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelId") + .HasColumnType("numeric(20,0)"); + + b.Property("DeliveryFailureCount") + .HasColumnType("integer"); + + b.Property("DueDate") + .HasColumnType("timestamp with time zone"); + + b.Property("FirstDeliveryFailureAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuildId") + .HasColumnType("integer"); + + b.Property("InsertDate") + .HasColumnType("timestamp with time zone"); + + b.Property("NextDeliveryAttemptAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GuildId"); + + b.HasIndex("UserId"); + + b.ToTable("Reminders"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("GuildId") + .HasColumnType("integer"); + + b.Property("RoleId") + .HasColumnType("numeric(20,0)"); + + b.Property("RoleType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GuildId"); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.RssSeenEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("EntryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("FeedUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("SeenAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("FeedUrl", "EntryId") + .IsUnique(); + + b.ToTable("RssSeenEntries"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.RssSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AvatarUrl") + .HasColumnType("text"); + + b.Property("ChannelDiscordId") + .HasColumnType("numeric(20,0)"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("text"); + + b.Property("FeedUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuildDiscordId") + .HasColumnType("numeric(20,0)"); + + b.Property("InsertDate") + .HasColumnType("timestamp with time zone"); + + b.Property("WebhookId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("FeedUrl"); + + b.HasIndex("WebhookId"); + + b.HasIndex("ChannelDiscordId", "FeedUrl") + .IsUnique(); + + b.ToTable("RssSubscriptions"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.Stock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DailyChangePercent") + .HasColumnType("decimal(18,4)"); + + b.Property("EntityId") + .HasColumnType("integer"); + + b.Property("EntityType") + .HasColumnType("integer"); + + b.Property("InsertDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastUpdatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PreviousPrice") + .HasColumnType("decimal(18,4)"); + + b.Property("Price") + .HasColumnType("decimal(18,4)"); + + b.Property("UpdateTimeMinutes") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EntityType", "EntityId") + .IsUnique(); + + b.HasIndex("LastUpdatedDate", "UpdateTimeMinutes"); + + b.ToTable("Stocks"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.StockHolding", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("InsertDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Shares") + .HasColumnType("decimal(18,6)"); + + b.Property("StockId") + .HasColumnType("integer"); + + b.Property("TotalInvested") + .HasColumnType("decimal(18,4)"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("StockId"); + + b.HasIndex("UserId", "StockId") + .IsUnique(); + + b.ToTable("StockHoldings"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.StockTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("decimal(18,4)"); + + b.Property("Fee") + .HasColumnType("decimal(18,4)"); + + b.Property("InsertDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PriceAtTransaction") + .HasColumnType("decimal(18,4)"); + + b.Property("Shares") + .HasColumnType("decimal(18,6)"); + + b.Property("StockId") + .HasColumnType("integer"); + + b.Property("TargetUserId") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("InsertDate"); + + b.HasIndex("TargetUserId"); + + b.HasIndex("StockId", "InsertDate"); + + b.HasIndex("UserId", "InsertDate"); + + b.ToTable("StockTransactions"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.TemporaryBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuildId") + .HasColumnType("numeric(20,0)"); + + b.Property("InsertDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("UnbannedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("numeric(20,0)"); + + b.HasKey("Id"); + + b.HasIndex("ExpiresAt", "UnbannedAt"); + + b.HasIndex("GuildId", "UserId"); + + b.ToTable("TemporaryBans"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.TwitchSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AvatarUrl") + .HasColumnType("text"); + + b.Property("ChannelDiscordId") + .HasColumnType("numeric(20,0)"); + + b.Property("GuildDiscordId") + .HasColumnType("numeric(20,0)"); + + b.Property("InsertDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsLive") + .HasColumnType("boolean"); + + b.Property("LastAnnouncedStreamId") + .HasColumnType("text"); + + b.Property("TwitchDisplayName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TwitchLogin") + .IsRequired() + .HasColumnType("text"); + + b.Property("TwitchUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("WebhookId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TwitchUserId"); + + b.HasIndex("WebhookId"); + + b.HasIndex("ChannelDiscordId", "TwitchUserId") + .IsUnique(); + + b.ToTable("TwitchSubscriptions"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Balance") + .ValueGeneratedOnAdd() + .HasColumnType("decimal(18,4)") + .HasDefaultValue(1000.00m); + + b.Property("DiscordId") + .HasColumnType("numeric(20,0)"); + + b.Property("InsertDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastRobberyAttempt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSuccessfullyRobbed") + .HasColumnType("timestamp with time zone"); + + b.Property("LastUsernameCheck") + .HasColumnType("timestamp with time zone"); + + b.Property("LevelUpMessages") + .HasColumnType("boolean"); + + b.Property("LevelUpQuotes") + .HasColumnType("boolean"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("DiscordId") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.UserActivity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DiscordChannelId") + .HasColumnType("numeric(20,0)"); + + b.Property("DiscordMessageId") + .HasColumnType("numeric(20,0)"); + + b.Property("GuildAverageMessageLength") + .HasColumnType("double precision"); + + b.Property("GuildId") + .HasColumnType("integer"); + + b.Property("GuildMessageCount") + .HasColumnType("integer"); + + b.Property("InsertDate") + .HasColumnType("timestamp with time zone"); + + b.Property("MessageHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("MessageLength") + .HasColumnType("integer"); + + b.Property("MessageSimHash") + .HasColumnType("numeric(20,0)"); + + b.Property("NormalizedLength") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("XpGained") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("InsertDate"); + + b.HasIndex("DiscordChannelId", "InsertDate"); + + b.HasIndex("GuildId", "InsertDate"); + + b.HasIndex("InsertDate", "DiscordChannelId"); + + b.HasIndex("InsertDate", "GuildId"); + + b.HasIndex("InsertDate", "UserId"); + + b.HasIndex("UserId", "GuildId", "InsertDate"); + + b.ToTable("UserActivity"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.UserLevels", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("GuildId") + .HasColumnType("integer"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("TotalXp") + .HasColumnType("integer"); + + b.Property("UserAverageMessageLength") + .HasColumnType("double precision"); + + b.Property("UserAverageMessageLengthEma") + .HasColumnType("double precision"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("UserMessageCount") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GuildId"); + + b.HasIndex("UserId", "GuildId") + .IsUnique(); + + b.ToTable("UserLevels"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.Webhook", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelDiscordId") + .HasColumnType("numeric(20,0)"); + + b.Property("GuildDiscordId") + .HasColumnType("numeric(20,0)"); + + b.Property("InsertDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastCheckedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.Property("WebhookId") + .HasColumnType("numeric(20,0)"); + + b.HasKey("Id"); + + b.HasIndex("ChannelDiscordId") + .IsUnique(); + + b.ToTable("Webhooks"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.XkcdDeliveryRetry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AttemptCount") + .HasColumnType("integer"); + + b.Property("LastAttemptAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Link") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Link") + .IsUnique(); + + b.ToTable("XkcdDeliveryRetries"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.XkcdSeen", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Link") + .IsRequired() + .HasColumnType("text"); + + b.Property("SeenAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Link") + .IsUnique(); + + b.ToTable("XkcdSeen"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.XkcdSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelDiscordId") + .HasColumnType("numeric(20,0)"); + + b.Property("GuildDiscordId") + .HasColumnType("numeric(20,0)"); + + b.Property("InsertDate") + .HasColumnType("timestamp with time zone"); + + b.Property("WebhookId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ChannelDiscordId") + .IsUnique(); + + b.HasIndex("WebhookId"); + + b.ToTable("XkcdSubscriptions"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.YoutubeSeenVideo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("SeenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VideoId") + .IsRequired() + .HasColumnType("text"); + + b.Property("YoutubeChannelId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("VideoId") + .IsUnique(); + + b.ToTable("YoutubeSeenVideos"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.YoutubeSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelDiscordId") + .HasColumnType("numeric(20,0)"); + + b.Property("GuildDiscordId") + .HasColumnType("numeric(20,0)"); + + b.Property("InsertDate") + .HasColumnType("timestamp with time zone"); + + b.Property("WebhookId") + .HasColumnType("integer"); + + b.Property("YoutubeAvatarUrl") + .HasColumnType("text"); + + b.Property("YoutubeChannelId") + .IsRequired() + .HasColumnType("text"); + + b.Property("YoutubeChannelTitle") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("WebhookId"); + + b.HasIndex("YoutubeChannelId"); + + b.HasIndex("ChannelDiscordId", "YoutubeChannelId") + .IsUnique(); + + b.ToTable("YoutubeSubscriptions"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.ButtonGamePress", b => + { + b.HasOne("Morpheus.Database.Models.Guild", "Guild") + .WithMany("ButtonGamePresses") + .HasForeignKey("GuildId"); + + b.HasOne("Morpheus.Database.Models.User", "User") + .WithMany("ButtonGamePresses") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Guild"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.Quote", b => + { + b.HasOne("Morpheus.Database.Models.Guild", "Guild") + .WithMany("Quotes") + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Morpheus.Database.Models.User", "User") + .WithMany("Quotes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Guild"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.QuoteApproval", b => + { + b.HasOne("Morpheus.Database.Models.QuoteApprovalMessage", "ApprovalMessage") + .WithMany() + .HasForeignKey("QuoteApprovalMessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApprovalMessage"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.QuoteApprovalMessage", b => + { + b.HasOne("Morpheus.Database.Models.Quote", "Quote") + .WithMany() + .HasForeignKey("QuoteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Quote"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.QuoteScore", b => + { + b.HasOne("Morpheus.Database.Models.Quote", "Quote") + .WithMany("Scores") + .HasForeignKey("QuoteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Morpheus.Database.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Quote"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.ReactionRoleItem", b => + { + b.HasOne("Morpheus.Database.Models.ReactionRoleMessage", "ReactionRoleMessage") + .WithMany("Items") + .HasForeignKey("ReactionRoleMessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ReactionRoleMessage"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.ReactionRoleMessage", b => + { + b.HasOne("Morpheus.Database.Models.Guild", "Guild") + .WithMany() + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Guild"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.Reminder", b => + { + b.HasOne("Morpheus.Database.Models.Guild", "Guild") + .WithMany() + .HasForeignKey("GuildId"); + + b.HasOne("Morpheus.Database.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Guild"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.Role", b => + { + b.HasOne("Morpheus.Database.Models.Guild", "Guild") + .WithMany("Roles") + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Guild"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.RssSubscription", b => + { + b.HasOne("Morpheus.Database.Models.Webhook", "Webhook") + .WithMany() + .HasForeignKey("WebhookId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Webhook"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.StockHolding", b => + { + b.HasOne("Morpheus.Database.Models.Stock", "Stock") + .WithMany("StockHoldings") + .HasForeignKey("StockId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Morpheus.Database.Models.User", "User") + .WithMany("StockHoldings") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Stock"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.StockTransaction", b => + { + b.HasOne("Morpheus.Database.Models.Stock", "Stock") + .WithMany() + .HasForeignKey("StockId"); + + b.HasOne("Morpheus.Database.Models.User", "TargetUser") + .WithMany() + .HasForeignKey("TargetUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Morpheus.Database.Models.User", "User") + .WithMany("StockTransactions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Stock"); + + b.Navigation("TargetUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.TwitchSubscription", b => + { + b.HasOne("Morpheus.Database.Models.Webhook", "Webhook") + .WithMany() + .HasForeignKey("WebhookId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Webhook"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.UserActivity", b => + { + b.HasOne("Morpheus.Database.Models.Guild", "Guild") + .WithMany("UserActivity") + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Morpheus.Database.Models.User", "User") + .WithMany("UserActivity") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Guild"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.UserLevels", b => + { + b.HasOne("Morpheus.Database.Models.Guild", "Guild") + .WithMany("UserLevels") + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Morpheus.Database.Models.User", "User") + .WithMany("UserLevels") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Guild"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.XkcdSubscription", b => + { + b.HasOne("Morpheus.Database.Models.Webhook", "Webhook") + .WithMany() + .HasForeignKey("WebhookId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Webhook"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.YoutubeSubscription", b => + { + b.HasOne("Morpheus.Database.Models.Webhook", "Webhook") + .WithMany() + .HasForeignKey("WebhookId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Webhook"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.Guild", b => + { + b.Navigation("ButtonGamePresses"); + + b.Navigation("Quotes"); + + b.Navigation("Roles"); + + b.Navigation("UserActivity"); + + b.Navigation("UserLevels"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.Quote", b => + { + b.Navigation("Scores"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.ReactionRoleMessage", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.Stock", b => + { + b.Navigation("StockHoldings"); + }); + + modelBuilder.Entity("Morpheus.Database.Models.User", b => + { + b.Navigation("ButtonGamePresses"); + + b.Navigation("Quotes"); + + b.Navigation("StockHoldings"); + + b.Navigation("StockTransactions"); + + b.Navigation("UserActivity"); + + b.Navigation("UserLevels"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20260801124127_RecalculateActivityLevels.cs b/Migrations/20260801124127_RecalculateActivityLevels.cs new file mode 100644 index 0000000..8ea4209 --- /dev/null +++ b/Migrations/20260801124127_RecalculateActivityLevels.cs @@ -0,0 +1,32 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Morpheus.Migrations +{ + /// + public partial class RecalculateActivityLevels : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(""" + UPDATE "UserLevels" + SET "Level" = FLOOR(POWER(LOG(("TotalXp"::double precision + 111.0) / 111.0), 5.0243))::integer + WHERE "TotalXp" >= 0 + AND "Level" IS DISTINCT FROM FLOOR(POWER(LOG(("TotalXp"::double precision + 111.0) / 111.0), 5.0243))::integer; + """); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(""" + UPDATE "UserLevels" + SET "Level" = FLOOR(POWER(LOG(FLOOR(("TotalXp"::double precision + 111.0) / 111.0)), 5.0243))::integer + WHERE "TotalXp" >= 0 + AND "Level" IS DISTINCT FROM FLOOR(POWER(LOG(FLOOR(("TotalXp"::double precision + 111.0) / 111.0)), 5.0243))::integer; + """); + } + } +} diff --git a/Morpheus.Tests/ActivityLevelServiceTests.cs b/Morpheus.Tests/ActivityLevelServiceTests.cs index 28bea74..d7c64e4 100644 --- a/Morpheus.Tests/ActivityLevelServiceTests.cs +++ b/Morpheus.Tests/ActivityLevelServiceTests.cs @@ -9,11 +9,19 @@ public class ActivityLevelServiceTests [InlineData(998, 0)] [InlineData(999, 1)] [InlineData(1000, 1)] + [InlineData(1449, 1)] + [InlineData(1450, 2)] public void CalculateLevel_ReturnsExpectedBoundaryLevels(long xp, int expectedLevel) { Assert.Equal(expectedLevel, ActivityLevelService.CalculateLevel(xp)); } + [Fact] + public void CalculateLevel_HandlesMaximumXp() + { + Assert.True(ActivityLevelService.CalculateLevel(long.MaxValue) > 0); + } + [Theory] [InlineData(1)] [InlineData(2)] diff --git a/Services/ActivityLevelService.cs b/Services/ActivityLevelService.cs index ed990b6..e6da49c 100644 --- a/Services/ActivityLevelService.cs +++ b/Services/ActivityLevelService.cs @@ -68,7 +68,7 @@ FOR UPDATE public static int CalculateLevel(long xp) { - return (int)Math.Pow(Math.Log10((xp + 111) / 111), 5.0243); + return (int)Math.Pow(Math.Log10((xp + 111d) / 111d), 5.0243); } public static int CalculateXp(int level)