diff --git a/Asterion/Database/DataContext.cs b/Asterion/Database/DataContext.cs index c4d6b27..92e535c 100644 --- a/Asterion/Database/DataContext.cs +++ b/Asterion/Database/DataContext.cs @@ -1,4 +1,5 @@ -using Asterion.Database.Models; +using System.Text.Json; +using Asterion.Database.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Logging; @@ -52,9 +53,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity().Property(p => p.MessageStyle).HasDefaultValue(MessageStyle.Full); modelBuilder.Entity().Property(p => p.ChangelogStyle).HasDefaultValue(ChangelogStyle.PlainText); modelBuilder.Entity().Property(p => p.ChangeLogMaxLength).HasDefaultValue(2000); - + modelBuilder.Entity() .Property(e => e.ReleaseFilter) .HasDefaultValue(ReleaseType.Alpha | ReleaseType.Beta | ReleaseType.Release); + + // Ignore the LoaderFilter property + modelBuilder.Entity() + .Ignore(e => e.LoaderFilter); } + } \ No newline at end of file diff --git a/Asterion/Database/Models/ModrinthEntry.cs b/Asterion/Database/Models/ModrinthEntry.cs index 20cd4e8..677ab2e 100644 --- a/Asterion/Database/Models/ModrinthEntry.cs +++ b/Asterion/Database/Models/ModrinthEntry.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json; namespace Asterion.Database.Models; @@ -26,8 +27,22 @@ public class ModrinthEntry [Required] public DateTime Created { get; set; } [Required] public ReleaseType ReleaseFilter { get; set; } = ReleaseType.Alpha | ReleaseType.Beta | ReleaseType.Release; + + [NotMapped] + public string[]? LoaderFilter + { + get => SerializedLoaderFilter == null + ? null + : JsonSerializer.Deserialize(SerializedLoaderFilter); + set => SerializedLoaderFilter = value == null + ? null + : JsonSerializer.Serialize(value); + } + + public string? SerializedLoaderFilter { get; private set; } } + [Flags] public enum ReleaseType { diff --git a/Asterion/Interfaces/IDataService.cs b/Asterion/Interfaces/IDataService.cs index 344f1c1..a67670f 100644 --- a/Asterion/Interfaces/IDataService.cs +++ b/Asterion/Interfaces/IDataService.cs @@ -136,4 +136,5 @@ public Task UpdateModrinthProjectAsync(string projectId, string? newVersio public Task SetPingRoleAsync(ulong guildId, ulong? roleId, string? projectId = null); public Task GetPingRoleIdAsync(ulong guildId, string? projectId = null); public Task SetReleaseFilterAsync(ulong entryId, ReleaseType releaseType); + public Task SetLoaderFilterAsync(ulong entryEntryId, string[]? newLoaderFilter); } \ No newline at end of file diff --git a/Asterion/Migrations/20241123122729_addloadertype_entry.cs b/Asterion/Migrations/20241123122729_addloadertype_entry.cs new file mode 100644 index 0000000..2b8323c --- /dev/null +++ b/Asterion/Migrations/20241123122729_addloadertype_entry.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Asterion.Migrations +{ + /// + public partial class addloadertype_entry : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SerializedLoaderFilter", + table: "ModrinthEntries", + type: "TEXT", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "SerializedLoaderFilter", + table: "ModrinthEntries"); + } + } +} diff --git a/Asterion/Migrations/DataContextModelSnapshot.cs b/Asterion/Migrations/DataContextModelSnapshot.cs index 906ae55..f2bbde7 100644 --- a/Asterion/Migrations/DataContextModelSnapshot.cs +++ b/Asterion/Migrations/DataContextModelSnapshot.cs @@ -15,7 +15,7 @@ partial class DataContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); modelBuilder.Entity("Asterion.Database.Models.Array", b => { @@ -149,6 +149,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("INTEGER") .HasDefaultValue(7); + b.Property("SerializedLoaderFilter") + .HasColumnType("TEXT"); + b.HasKey("EntryId"); b.HasIndex("ArrayId"); diff --git a/Asterion/Modules/EntryInteractionModule.cs b/Asterion/Modules/EntryInteractionModule.cs index 4706a44..5a9b973 100644 --- a/Asterion/Modules/EntryInteractionModule.cs +++ b/Asterion/Modules/EntryInteractionModule.cs @@ -1,4 +1,6 @@ -using Asterion.Common; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using Asterion.Common; using Asterion.Interfaces; using Discord; using Discord.Interactions; @@ -6,12 +8,16 @@ using Asterion.Attributes; using Asterion.AutocompleteHandlers; using Asterion.Database.Models; +using Modrinth; namespace Asterion.Modules { [RequireUserPermission(GuildPermission.Administrator, Group = "ManageSubs")] [DoManageSubsRoleCheck(Group = "ManageSubs")] - public class EntryInteractionModule(ILocalizationService localizationService, IDataService dataService) + public class EntryInteractionModule( + ILocalizationService localizationService, + IDataService dataService, + IModrinthClient modrinthClient) : AsterionInteractionModuleBase(localizationService) { public static Embed CreateModrinthEntryEmbed(ModrinthEntry entry, ReleaseType releaseFilter) @@ -20,20 +26,24 @@ public static Embed CreateModrinthEntryEmbed(ModrinthEntry entry, ReleaseType re .WithTitle("Modrinth Entry Information") .WithDescription("Here is the information about the Modrinth entry:") .AddField("📂 Project ID", $"`{entry.ProjectId}`", false) - .AddField("📢 Custom Update Channel", - entry.CustomUpdateChannel.HasValue ? MentionUtils.MentionChannel(entry.CustomUpdateChannel.Value) : "None", + .AddField("📢 Custom Update Channel", + entry.CustomUpdateChannel.HasValue + ? MentionUtils.MentionChannel(entry.CustomUpdateChannel.Value) + : "None", true) - .AddField("📜 Custom Ping Role", - entry.CustomPingRole.HasValue ? MentionUtils.MentionRole(entry.CustomPingRole.Value) : "None", + .AddField("📜 Custom Ping Role", + entry.CustomPingRole.HasValue ? MentionUtils.MentionRole(entry.CustomPingRole.Value) : "None", false) .AddField("🔖 Release Filter", releaseFilter.ToString(), true) - .AddField("🕒 Created", TimestampTag.FromDateTime(entry.Created).ToString(), true) + .AddField("🔍 Loader Filter", + entry.LoaderFilter == null ? "None" : string.Join(", ", entry.LoaderFilter), true) + .AddField("🕒 Created", TimestampTag.FromDateTime(entry.Created).ToString(), false) .WithColor(Color.Blue) .WithCurrentTimestamp() .Build(); } - - private static MessageComponent CreateReleaseFilterComponent(string projectId, ReleaseType releaseFilter) + + private static SelectMenuBuilder CreateReleaseFilterComponent(string projectId, ReleaseType releaseFilter) { var selectMenu = new SelectMenuBuilder() .WithCustomId($"release_filter:{projectId}") @@ -47,17 +57,36 @@ private static MessageComponent CreateReleaseFilterComponent(string projectId, R .AddOption("Release", ((int)ReleaseType.Release).ToString(), "Include Release", isDefault: releaseFilter.HasFlag(ReleaseType.Release)); - return new ComponentBuilder() - .WithSelectMenu(selectMenu) - .Build(); + return selectMenu; } - + + private static SelectMenuBuilder CreateLoaderFilterComponent(string projectId, string[] supportedLoaders, + string[]? currentLoaderFilter) + { + var selectMenu = new SelectMenuBuilder() + .WithCustomId($"loader_filter:{projectId}") + .WithPlaceholder("Select loader(s) to filter - currently ALL") + .WithMinValues(0) + .WithMaxValues(supportedLoaders.Length); + + // Add options for each supported loader + foreach (var loader in supportedLoaders) + { + selectMenu.AddOption(loader, loader, $"Include {loader} loader", + isDefault: currentLoaderFilter != null && currentLoaderFilter.Contains(loader)); + } + + return selectMenu; + } + // Interaction command to display the entry info and provide a dropdown [SlashCommand("entry", "Displays entry information and allows selection of release type filters.")] - public async Task ShowEntryAsync([Summary("project_id")] [Autocomplete(typeof(SubscribedIdAutocompletionHandler))] string projectId) + public async Task ShowEntryAsync( + [Summary("project_id")] [Autocomplete(typeof(SubscribedIdAutocompletionHandler))] + string projectId) { await DeferAsync(); - + // Retrieve entry data using your custom connector var entry = await dataService.GetModrinthEntryAsync(Context.Guild.Id, projectId); if (entry == null) @@ -69,12 +98,19 @@ public async Task ShowEntryAsync([Summary("project_id")] [Autocomplete(typeof(Su // Create embed with entry details var embed = CreateModrinthEntryEmbed(entry, entry.ReleaseFilter); - + var components = new ComponentBuilder(); // Create a SelectMenu for ReleaseType filter - var component = CreateReleaseFilterComponent(projectId, entry.ReleaseFilter); + var releaseFilterComponent = CreateReleaseFilterComponent(projectId, entry.ReleaseFilter); + components.WithSelectMenu(releaseFilterComponent, 0); + + // Create a SelectMenu for Loader filter + var project = await modrinthClient.Project.GetAsync(projectId); + var supportedLoaders = project.Loaders; + var loaderFilterComponent = CreateLoaderFilterComponent(projectId, supportedLoaders, entry.LoaderFilter); + components.WithSelectMenu(loaderFilterComponent, 1); // Send the embed with the dropdown menu - await FollowupAsync(embed: embed, components: component); + await FollowupAsync(embed: embed, components: components.Build()); } [ComponentInteraction("release_filter:*")] @@ -113,16 +149,65 @@ public async Task HandleReleaseFilterSelectionAsync(string projectId, string[] s // Use the static method to generate the updated embed var updatedEmbed = CreateModrinthEntryEmbed(entry, newReleaseFilter); + var supportedLoaders = (await modrinthClient.Project.GetAsync(projectId)).Loaders; + // Update the original message with the new embed - await ModifyOriginalResponseAsync(msg => { + await ModifyOriginalResponseAsync(msg => + { msg.Embed = updatedEmbed; - msg.Components = CreateReleaseFilterComponent(projectId, newReleaseFilter); + msg.Components = new ComponentBuilder() + .WithSelectMenu(CreateReleaseFilterComponent(projectId, newReleaseFilter)) + .WithSelectMenu(CreateLoaderFilterComponent(projectId, supportedLoaders, entry.LoaderFilter)) + .Build(); }); // Optionally, acknowledge the selection change in a temporary message await FollowupAsync("Release filter updated successfully!", ephemeral: true); } + [ComponentInteraction("loader_filter:*")] + public async Task HandleLoaderFilterSelectionAsync(string projectId, string[] selectedValues) + { + // Defer the interaction to avoid timeout issues + await DeferAsync(ephemeral: true); + + // Retrieve the entry using projectId + var entry = await dataService.GetModrinthEntryAsync(Context.Guild.Id, projectId); + if (entry == null) + { + await FollowupAsync("Entry not found.", ephemeral: true); + return; + } + + // Convert selectedValues back into the Loader filter array + string[]? newLoaderFilter = null; + if (selectedValues.Length > 0) + { + newLoaderFilter = selectedValues; + } + + // Update the loader filter in the database + await dataService.SetLoaderFilterAsync(entry.EntryId, newLoaderFilter); + entry.LoaderFilter = newLoaderFilter; + // Use the static method to generate the updated embed + var updatedEmbed = CreateModrinthEntryEmbed(entry, entry.ReleaseFilter); + + // TODO: Maybe parse the supported loaders from the selected project instead of fetching it again + var supportedLoaders = (await modrinthClient.Project.GetAsync(projectId)).Loaders; + + // Update the original message with the new embed + await ModifyOriginalResponseAsync(msg => + { + msg.Embed = updatedEmbed; + msg.Components = new ComponentBuilder() + .WithSelectMenu(CreateReleaseFilterComponent(projectId, entry.ReleaseFilter)) + .WithSelectMenu(CreateLoaderFilterComponent(projectId, supportedLoaders, entry.LoaderFilter)) + .Build(); + }); + + // Optionally, acknowledge the selection change in a temporary message + await FollowupAsync("Loader filter updated successfully!", ephemeral: true); + } } -} +} \ No newline at end of file diff --git a/Asterion/Services/DataService.cs b/Asterion/Services/DataService.cs index 1d5e6a6..bc921d6 100644 --- a/Asterion/Services/DataService.cs +++ b/Asterion/Services/DataService.cs @@ -427,6 +427,22 @@ public async Task SetReleaseFilterAsync(ulong entryId, ReleaseType release return true; } + public async Task SetLoaderFilterAsync(ulong entryEntryId, string[]? newLoaderFilter) + { + using var scope = _services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + var entry = db.ModrinthEntries.FirstOrDefault(x => x.EntryId == entryEntryId); + + if (entry is null) return false; + + entry.LoaderFilter = newLoaderFilter; + + await db.SaveChangesAsync(); + + return true; + } + private async Task JoinGuild(SocketGuild guild) { await AddGuildAsync(guild.Id); diff --git a/Asterion/Services/Modrinth/SendDiscordNotificationJob.cs b/Asterion/Services/Modrinth/SendDiscordNotificationJob.cs index 75f4f2e..19317e9 100644 --- a/Asterion/Services/Modrinth/SendDiscordNotificationJob.cs +++ b/Asterion/Services/Modrinth/SendDiscordNotificationJob.cs @@ -122,6 +122,22 @@ private async Task SendNotifications(Project project, Version[] versions) _logger.LogDebug("Version {VersionId} of project {ProjectId} passed release filter", version.Id, project.Id); break; } + + // Now we check if there is a loader filter + if (entry.LoaderFilter is not null && entry.LoaderFilter.Length > 0) + { + var versionLoaders = version.Loaders; + + // Now we check, if any of the loaders are in the filter + if (!versionLoaders.Any(x => entry.LoaderFilter.Contains(x))) + { + _logger.LogDebug("Skipping version {VersionId} of project {ProjectId} due to loader filter", version.Id, project.Id); + continue; + + } + + _logger.LogDebug("Version {VersionId} of project {ProjectId} passed loader filter", version.Id, project.Id); + } _logger.LogDebug("Sending notification for version {VersionId} of project {ProjectId} to guild {GuildId}", version.Id, project.Id, guild.GuildId); var embed = ModrinthEmbedBuilder.VersionUpdateEmbed(guild.GuildSettings, project, version, team).Build();