diff --git a/Asterion.Test/Asterion.Test.csproj b/Asterion.Test/Asterion.Test.csproj index ee95fc0..e8f2553 100644 --- a/Asterion.Test/Asterion.Test.csproj +++ b/Asterion.Test/Asterion.Test.csproj @@ -15,7 +15,7 @@ - + diff --git a/Asterion/Asterion.csproj b/Asterion/Asterion.csproj index ef48e1e..3dbaae7 100644 --- a/Asterion/Asterion.csproj +++ b/Asterion/Asterion.csproj @@ -15,7 +15,7 @@ - + diff --git a/Asterion/Database/DataContext.cs b/Asterion/Database/DataContext.cs index 7d0a00b..c4d6b27 100644 --- a/Asterion/Database/DataContext.cs +++ b/Asterion/Database/DataContext.cs @@ -52,5 +52,9 @@ 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); } } \ No newline at end of file diff --git a/Asterion/Database/Models/ModrinthEntry.cs b/Asterion/Database/Models/ModrinthEntry.cs index 40494ab..20cd4e8 100644 --- a/Asterion/Database/Models/ModrinthEntry.cs +++ b/Asterion/Database/Models/ModrinthEntry.cs @@ -24,4 +24,14 @@ public class ModrinthEntry public virtual Guild Guild { get; set; } = null!; [Required] public DateTime Created { get; set; } -} \ No newline at end of file + + [Required] public ReleaseType ReleaseFilter { get; set; } = ReleaseType.Alpha | ReleaseType.Beta | ReleaseType.Release; +} + +[Flags] +public enum ReleaseType +{ + Alpha = 1, + Beta = 2, + Release = 4 +} diff --git a/Asterion/Interfaces/IDataService.cs b/Asterion/Interfaces/IDataService.cs index 04336ef..344f1c1 100644 --- a/Asterion/Interfaces/IDataService.cs +++ b/Asterion/Interfaces/IDataService.cs @@ -135,4 +135,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); } \ No newline at end of file diff --git a/Asterion/Migrations/20240926184037_add_releasetype.Designer.cs b/Asterion/Migrations/20240926184037_add_releasetype.Designer.cs new file mode 100644 index 0000000..6612244 --- /dev/null +++ b/Asterion/Migrations/20240926184037_add_releasetype.Designer.cs @@ -0,0 +1,317 @@ +// +using System; +using Asterion.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Asterion.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20240926184037_add_releasetype")] + partial class add_releasetype + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + modelBuilder.Entity("Asterion.Database.Models.Array", b => + { + b.Property("ArrayId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("ArrayId"); + + b.ToTable("Arrays"); + }); + + modelBuilder.Entity("Asterion.Database.Models.Guild", b => + { + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("Active") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("GuildSettingsId") + .HasColumnType("INTEGER"); + + b.Property("ManageRole") + .HasColumnType("INTEGER"); + + b.Property("ModrinthArrayId") + .HasColumnType("INTEGER"); + + b.Property("PingRole") + .HasColumnType("INTEGER"); + + b.HasKey("GuildId"); + + b.HasIndex("ModrinthArrayId") + .IsUnique(); + + b.ToTable("Guilds"); + }); + + modelBuilder.Entity("Asterion.Database.Models.GuildSettings", b => + { + b.Property("GuildSettingsId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChangeLogMaxLength") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(2000L); + + b.Property("ChangelogStyle") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("CheckMessagesForModrinthLink") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("MessageStyle") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("RemoveOnLeave") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("ShowChannelSelection") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("ShowSubscribeButton") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.HasKey("GuildSettingsId"); + + b.HasIndex("GuildId") + .IsUnique(); + + b.ToTable("GuildSettings"); + }); + + modelBuilder.Entity("Asterion.Database.Models.ModrinthEntry", b => + { + b.Property("EntryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArrayId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CustomPingRole") + .HasColumnType("INTEGER"); + + b.Property("CustomUpdateChannel") + .HasColumnType("INTEGER"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ReleaseFilter") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(7); + + b.HasKey("EntryId"); + + b.HasIndex("ArrayId"); + + b.HasIndex("GuildId"); + + b.HasIndex("ProjectId"); + + b.ToTable("ModrinthEntries"); + }); + + modelBuilder.Entity("Asterion.Database.Models.ModrinthInstanceStatistics", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Authors") + .HasColumnType("INTEGER"); + + b.Property("Files") + .HasColumnType("INTEGER"); + + b.Property("Projects") + .HasColumnType("INTEGER"); + + b.Property("Versions") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ModrinthInstanceStatistics"); + }); + + modelBuilder.Entity("Asterion.Database.Models.ModrinthProject", b => + { + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastCheckVersion") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastUpdated") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("ProjectId"); + + b.ToTable("ModrinthProjects"); + }); + + modelBuilder.Entity("Asterion.Database.Models.TotalDownloads", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Downloads") + .HasColumnType("INTEGER"); + + b.Property("Followers") + .HasColumnType("INTEGER"); + + b.Property("ProjectId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Timestamp") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("TotalDownloads"); + }); + + modelBuilder.Entity("Asterion.Database.Models.Guild", b => + { + b.HasOne("Asterion.Database.Models.Array", "ModrinthArray") + .WithOne("Guild") + .HasForeignKey("Asterion.Database.Models.Guild", "ModrinthArrayId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ModrinthArray"); + }); + + modelBuilder.Entity("Asterion.Database.Models.GuildSettings", b => + { + b.HasOne("Asterion.Database.Models.Guild", "Guild") + .WithOne("GuildSettings") + .HasForeignKey("Asterion.Database.Models.GuildSettings", "GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Guild"); + }); + + modelBuilder.Entity("Asterion.Database.Models.ModrinthEntry", b => + { + b.HasOne("Asterion.Database.Models.Array", "Array") + .WithMany() + .HasForeignKey("ArrayId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Asterion.Database.Models.Guild", "Guild") + .WithMany() + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Asterion.Database.Models.ModrinthProject", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Array"); + + b.Navigation("Guild"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("Asterion.Database.Models.TotalDownloads", b => + { + b.HasOne("Asterion.Database.Models.ModrinthProject", "Project") + .WithMany("TotalDownloads") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("Asterion.Database.Models.Array", b => + { + b.Navigation("Guild") + .IsRequired(); + }); + + modelBuilder.Entity("Asterion.Database.Models.Guild", b => + { + b.Navigation("GuildSettings") + .IsRequired(); + }); + + modelBuilder.Entity("Asterion.Database.Models.ModrinthProject", b => + { + b.Navigation("TotalDownloads"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Asterion/Migrations/20240926184037_add_releasetype.cs b/Asterion/Migrations/20240926184037_add_releasetype.cs new file mode 100644 index 0000000..9c0e46d --- /dev/null +++ b/Asterion/Migrations/20240926184037_add_releasetype.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Asterion.Migrations +{ + /// + public partial class add_releasetype : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ReleaseFilter", + table: "ModrinthEntries", + type: "INTEGER", + nullable: false, + defaultValue: 7); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ReleaseFilter", + table: "ModrinthEntries"); + } + } +} diff --git a/Asterion/Migrations/DataContextModelSnapshot.cs b/Asterion/Migrations/DataContextModelSnapshot.cs index 65753d3..906ae55 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.4"); + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); modelBuilder.Entity("Asterion.Database.Models.Array", b => { @@ -144,6 +144,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("TEXT"); + b.Property("ReleaseFilter") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(7); + b.HasKey("EntryId"); b.HasIndex("ArrayId"); diff --git a/Asterion/Modules/EntryInteractionModule.cs b/Asterion/Modules/EntryInteractionModule.cs new file mode 100644 index 0000000..32deac6 --- /dev/null +++ b/Asterion/Modules/EntryInteractionModule.cs @@ -0,0 +1,125 @@ +using Asterion.Common; +using Asterion.Interfaces; +using Discord; +using Discord.Interactions; +using System.Threading.Tasks; +using Asterion.AutocompleteHandlers; +using Asterion.Database.Models; + +namespace Asterion.Modules +{ + public class EntryInteractionModule(ILocalizationService localizationService, IDataService dataService) + : AsterionInteractionModuleBase(localizationService) + { + public static Embed CreateModrinthEntryEmbed(ModrinthEntry entry, ReleaseType releaseFilter) + { + return new EmbedBuilder() + .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", + true) + .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) + .WithColor(Color.Blue) + .WithCurrentTimestamp() + .Build(); + } + + private static MessageComponent CreateReleaseFilterComponent(string projectId, ReleaseType releaseFilter) + { + var selectMenu = new SelectMenuBuilder() + .WithCustomId($"release_filter:{projectId}") + .WithPlaceholder("Select release type(s)") + .WithMinValues(1) + .WithMaxValues(3) + .AddOption("Alpha", ((int)ReleaseType.Alpha).ToString(), "Include Alpha releases", + isDefault: releaseFilter.HasFlag(ReleaseType.Alpha)) + .AddOption("Beta", ((int)ReleaseType.Beta).ToString(), "Include Beta releases", + isDefault: releaseFilter.HasFlag(ReleaseType.Beta)) + .AddOption("Release", ((int)ReleaseType.Release).ToString(), "Include Release", + isDefault: releaseFilter.HasFlag(ReleaseType.Release)); + + return new ComponentBuilder() + .WithSelectMenu(selectMenu) + .Build(); + } + + // 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) + { + await DeferAsync(); + + // Retrieve entry data using your custom connector + var entry = await dataService.GetModrinthEntryAsync(Context.Guild.Id, projectId); + if (entry == null) + { + await RespondAsync("Entry not found."); + return; + } + + // Create embed with entry details + var embed = CreateModrinthEntryEmbed(entry, entry.ReleaseFilter); + + + // Create a SelectMenu for ReleaseType filter + var component = CreateReleaseFilterComponent(projectId, entry.ReleaseFilter); + + // Send the embed with the dropdown menu + await FollowupAsync(embed: embed, components: component); + } + + [ComponentInteraction("release_filter:*")] + public async Task HandleReleaseFilterSelectionAsync(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 ReleaseType enum + ReleaseType newReleaseFilter = 0; + foreach (var value in selectedValues) + { + if (int.TryParse(value, out var releaseTypeValue)) + { + newReleaseFilter |= (ReleaseType)releaseTypeValue; + } + } + + if (newReleaseFilter == 0) + { + await FollowupAsync("Please select at least one release type.", ephemeral: true); + return; + } + + // Update the release filter in the database + await dataService.SetReleaseFilterAsync(entry.EntryId, newReleaseFilter); + + // Use the static method to generate the updated embed + var updatedEmbed = CreateModrinthEntryEmbed(entry, newReleaseFilter); + + // Update the original message with the new embed + await ModifyOriginalResponseAsync(msg => { + msg.Embed = updatedEmbed; + msg.Components = CreateReleaseFilterComponent(projectId, newReleaseFilter); + }); + + // Optionally, acknowledge the selection change in a temporary message + await FollowupAsync("Release filter updated successfully!", ephemeral: true); + } + + + } +} diff --git a/Asterion/Services/DataService.cs b/Asterion/Services/DataService.cs index 648204b..1d5e6a6 100644 --- a/Asterion/Services/DataService.cs +++ b/Asterion/Services/DataService.cs @@ -411,6 +411,22 @@ public async Task SetPingRoleAsync(ulong guildId, ulong? roleId, string? p } } + public async Task SetReleaseFilterAsync(ulong entryId, ReleaseType releaseType) + { + using var scope = _services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + var entry = await db.ModrinthEntries.FirstOrDefaultAsync(x => x.EntryId == entryId); + + if (entry is null) return false; + + entry.ReleaseFilter = releaseType; + + 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 8754aa5..75f4f2e 100644 --- a/Asterion/Services/Modrinth/SendDiscordNotificationJob.cs +++ b/Asterion/Services/Modrinth/SendDiscordNotificationJob.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging; using Modrinth; using Modrinth.Models; +using Modrinth.Models.Enums.Project; using Quartz; using Version = Modrinth.Models.Version; @@ -108,6 +109,20 @@ private async Task SendNotifications(Project project, Version[] versions) foreach (var version in versions.OrderBy(x => x.DatePublished)) { + var releaseFilter = entry.ReleaseFilter; + + switch (version.ProjectVersionType) + { + case ProjectVersionType.Alpha when !releaseFilter.HasFlag(ReleaseType.Alpha): + case ProjectVersionType.Beta when !releaseFilter.HasFlag(ReleaseType.Beta): + case ProjectVersionType.Release when !releaseFilter.HasFlag(ReleaseType.Release): + _logger.LogDebug("Skipping version {VersionId} of project {ProjectId} due to release filter", version.Id, project.Id); + continue; + default: + _logger.LogDebug("Version {VersionId} of project {ProjectId} passed release filter", version.Id, project.Id); + break; + } + _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(); var buttons = new ComponentBuilder().WithButton(ModrinthComponentBuilder.GetVersionUrlButton(project, version)).Build();