From 2fc8a7480ca084b58280af153bd2a064d4f8b7ca Mon Sep 17 00:00:00 2001 From: Zechiax <106590288+Zechiax@users.noreply.github.com> Date: Mon, 12 Jun 2023 11:33:52 +0200 Subject: [PATCH 01/30] ping command for debugging only --- Asterion/Modules/BotCommands.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Asterion/Modules/BotCommands.cs b/Asterion/Modules/BotCommands.cs index d0a6418..487d8e3 100644 --- a/Asterion/Modules/BotCommands.cs +++ b/Asterion/Modules/BotCommands.cs @@ -4,10 +4,12 @@ namespace Asterion.Modules; public class BotCommands : InteractionModuleBase { +#if DEBUG [SlashCommand("ping", "Pings the bot", runMode: RunMode.Async)] public async Task Ping() { await RespondAsync($"Pong :ping_pong: It took me {Context.Client.Latency}ms to respond to you", ephemeral: true); } +#endif } \ No newline at end of file From f8f55e0d7d05a795b47977965d58162e781438e8 Mon Sep 17 00:00:00 2001 From: Zechiax <106590288+Zechiax@users.noreply.github.com> Date: Mon, 12 Jun 2023 13:49:09 +0200 Subject: [PATCH 02/30] inherit from base context --- .../Common/AsterionInteractionModuleBase.cs | 19 +++++++++++++++++++ Asterion/Modules/BotCommands.cs | 17 +++++++++++++---- Asterion/Modules/BotManagement.cs | 3 ++- Asterion/Modules/ChartModule.cs | 3 ++- Asterion/Modules/GuildManagement.cs | 3 ++- Asterion/Modules/ModrinthInteractionModule.cs | 9 +++++---- Asterion/Modules/ModrinthModule.cs | 5 +++-- 7 files changed, 46 insertions(+), 13 deletions(-) create mode 100644 Asterion/Common/AsterionInteractionModuleBase.cs diff --git a/Asterion/Common/AsterionInteractionModuleBase.cs b/Asterion/Common/AsterionInteractionModuleBase.cs new file mode 100644 index 0000000..c8c04b7 --- /dev/null +++ b/Asterion/Common/AsterionInteractionModuleBase.cs @@ -0,0 +1,19 @@ +using System.Globalization; +using Discord; +using Discord.Interactions; + +namespace Asterion.Common; + +public class AsterionInteractionModuleBase : InteractionModuleBase +{ + protected CultureInfo? CommandCultureInfo { get; set; } + + + public override void BeforeExecute(ICommandInfo cmd) + { + // We currently set US culture for all commands + CommandCultureInfo = CultureInfo.GetCultureInfo("en-US"); + + base.BeforeExecute(cmd); + } +} \ No newline at end of file diff --git a/Asterion/Modules/BotCommands.cs b/Asterion/Modules/BotCommands.cs index 487d8e3..7905d9b 100644 --- a/Asterion/Modules/BotCommands.cs +++ b/Asterion/Modules/BotCommands.cs @@ -1,15 +1,24 @@ -using Discord.Interactions; +using Asterion.Common; +using Discord.Interactions; +using Discord.WebSocket; namespace Asterion.Modules; -public class BotCommands : InteractionModuleBase +public class BotCommands : AsterionInteractionModuleBase { #if DEBUG [SlashCommand("ping", "Pings the bot", runMode: RunMode.Async)] public async Task Ping() { - await RespondAsync($"Pong :ping_pong: It took me {Context.Client.Latency}ms to respond to you", - ephemeral: true); + if (Context.Client is BaseSocketClient socketClient) + { + var latency = socketClient.Latency; + await RespondAsync($"Pong! Latency: {latency}ms"); + } + else + { + await RespondAsync("Unable to determine latency."); + } } #endif } \ No newline at end of file diff --git a/Asterion/Modules/BotManagement.cs b/Asterion/Modules/BotManagement.cs index 99d1407..b60ca96 100644 --- a/Asterion/Modules/BotManagement.cs +++ b/Asterion/Modules/BotManagement.cs @@ -1,4 +1,5 @@ using Asterion.AutocompleteHandlers; +using Asterion.Common; using Asterion.Interfaces; using Discord.Interactions; using Microsoft.Extensions.DependencyInjection; @@ -6,7 +7,7 @@ namespace Asterion.Modules; [RequireOwner] -public class BotManagement : InteractionModuleBase +public class BotManagement : AsterionInteractionModuleBase { private readonly IDataService _dataService; diff --git a/Asterion/Modules/ChartModule.cs b/Asterion/Modules/ChartModule.cs index 0eef7ad..8745de0 100644 --- a/Asterion/Modules/ChartModule.cs +++ b/Asterion/Modules/ChartModule.cs @@ -1,4 +1,5 @@ using Asterion.AutocompleteHandlers; +using Asterion.Common; using Asterion.Services; using Asterion.Services.Modrinth; using Discord; @@ -12,7 +13,7 @@ namespace Asterion.Modules; -public class ChartModule : InteractionModuleBase +public class ChartModule : AsterionInteractionModuleBase { private readonly ILogger _logger; private readonly ModrinthService _modrinthService; diff --git a/Asterion/Modules/GuildManagement.cs b/Asterion/Modules/GuildManagement.cs index c03529d..cd88b12 100644 --- a/Asterion/Modules/GuildManagement.cs +++ b/Asterion/Modules/GuildManagement.cs @@ -1,5 +1,6 @@ using System.Text; using Asterion.Attributes; +using Asterion.Common; using Asterion.Interfaces; using Discord; using Discord.Interactions; @@ -7,7 +8,7 @@ namespace Asterion.Modules; [EnabledInDm(false)] -public class GuildManagement : InteractionModuleBase +public class GuildManagement : AsterionInteractionModuleBase { private readonly IDataService _dataService; diff --git a/Asterion/Modules/ModrinthInteractionModule.cs b/Asterion/Modules/ModrinthInteractionModule.cs index 7398773..96747ef 100644 --- a/Asterion/Modules/ModrinthInteractionModule.cs +++ b/Asterion/Modules/ModrinthInteractionModule.cs @@ -1,4 +1,5 @@ using Asterion.Attributes; +using Asterion.Common; using Asterion.ComponentBuilders; using Asterion.Database.Models; using Asterion.EmbedBuilders; @@ -14,7 +15,7 @@ namespace Asterion.Modules; [EnabledInDm(false)] -public class ModrinthInteractionModule : InteractionModuleBase +public class ModrinthInteractionModule : AsterionInteractionModuleBase { private const string RequestError = "Sorry, there was an error processing your request, try again later"; private readonly IDataService _dataService; @@ -40,7 +41,7 @@ private ComponentBuilder GetButtons(Project project, GuildSettings guildSettings components.WithButton(ModrinthComponentBuilder.GetProjectLinkButton(project)) .WithButton(ModrinthComponentBuilder.GetUserToViewButton(Context.User.Id, team.GetOwner()?.User.Id, project.Id)); - + return components; } @@ -52,7 +53,7 @@ public async Task SubProject(string userId, string projectId) await DeferAsync(); var guildId = Context.Guild.Id; - var channel = await Context.Guild.GetTextChannelAsync(Context.Channel.Id); + var channel = Context.Guild.GetTextChannel(Context.Channel.Id); var subscribed = await _dataService.IsGuildSubscribedToProjectAsync(guildId, projectId); @@ -106,7 +107,7 @@ await FollowupAsync( ephemeral: true); - var guildChannels = await Context.Guild.GetTextChannelsAsync(); + var guildChannels = Context.Guild.TextChannels; var options = new SelectMenuBuilder { diff --git a/Asterion/Modules/ModrinthModule.cs b/Asterion/Modules/ModrinthModule.cs index 2ab0ef1..20c05a0 100644 --- a/Asterion/Modules/ModrinthModule.cs +++ b/Asterion/Modules/ModrinthModule.cs @@ -1,5 +1,6 @@ using Asterion.Attributes; using Asterion.AutocompleteHandlers; +using Asterion.Common; using Asterion.ComponentBuilders; using Asterion.Database.Models; using Asterion.EmbedBuilders; @@ -22,7 +23,7 @@ namespace Asterion.Modules; [EnabledInDm(false)] [RequireContext(ContextType.Guild)] // ReSharper disable once ClassNeverInstantiated.Global -public class ModrinthModule : InteractionModuleBase +public class ModrinthModule : AsterionInteractionModuleBase { private readonly DiscordSocketClient _client; private readonly IDataService _dataService; @@ -51,7 +52,7 @@ public async Task FindUser([Summary("Query", "ID or username")] [MaxLength(60)] _logger.LogDebug("Search for user '{Query}", query); var searchResult = await _modrinthService.FindUser(query); _logger.LogDebug("Search status: {SearchStatus}", searchResult.SearchStatus); - + switch (searchResult.SearchStatus) { case SearchStatus.ApiDown: From 5055dfdb4c2a6be88458d3f7ca4d64ee76fe2abe Mon Sep 17 00:00:00 2001 From: Zechiax <106590288+Zechiax@users.noreply.github.com> Date: Mon, 12 Jun 2023 13:49:27 +0200 Subject: [PATCH 03/30] add figgle text at startup because why not --- Asterion/Asterion.cs | 5 ++++- Asterion/Asterion.csproj | 1 + Asterion/Program.cs | 5 ++++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Asterion/Asterion.cs b/Asterion/Asterion.cs index 7896c02..152ab04 100644 --- a/Asterion/Asterion.cs +++ b/Asterion/Asterion.cs @@ -19,9 +19,12 @@ namespace Asterion; public class Asterion { private readonly IConfiguration _config; + private int _shardId; - public Asterion() + public Asterion(int shardId) { + _shardId = shardId; + _config = new ConfigurationBuilder() .SetBasePath(AppContext.BaseDirectory) .AddJsonFile("config.json", false, true) diff --git a/Asterion/Asterion.csproj b/Asterion/Asterion.csproj index 6de5ce3..f794ee0 100644 --- a/Asterion/Asterion.csproj +++ b/Asterion/Asterion.csproj @@ -16,6 +16,7 @@ + diff --git a/Asterion/Program.cs b/Asterion/Program.cs index ee16846..a36febe 100644 --- a/Asterion/Program.cs +++ b/Asterion/Program.cs @@ -1,4 +1,5 @@ using System.Globalization; +using Figgle; using Serilog; namespace Asterion; @@ -9,6 +10,8 @@ private static void Main(string[] args) { if (args.Length > 0 && args[0] == "migration") return; + Console.WriteLine(FiggleFonts.Slant.Render("Asterion v3")); + Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo("en-US"); Log.Logger = new LoggerConfiguration() @@ -24,6 +27,6 @@ private static void Main(string[] args) .WriteTo.Console() .CreateLogger(); - new Asterion().MainAsync().GetAwaiter().GetResult(); + new Asterion(0).MainAsync().GetAwaiter().GetResult(); } } \ No newline at end of file From d511d4d83be20e173ead089f482e7ea0d4ff1c2e Mon Sep 17 00:00:00 2001 From: Zechiax <106590288+Zechiax@users.noreply.github.com> Date: Mon, 12 Jun 2023 13:53:15 +0200 Subject: [PATCH 04/30] make settings module inherit from base --- Asterion/Modules/SettingsModule.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Asterion/Modules/SettingsModule.cs b/Asterion/Modules/SettingsModule.cs index e9df792..f944713 100644 --- a/Asterion/Modules/SettingsModule.cs +++ b/Asterion/Modules/SettingsModule.cs @@ -1,4 +1,5 @@ using Asterion.Attributes; +using Asterion.Common; using Asterion.ComponentBuilders; using Asterion.Database.Models; using Asterion.EmbedBuilders; @@ -12,7 +13,7 @@ namespace Asterion.Modules; [RequireUserPermission(GuildPermission.Administrator)] [RequireContext(ContextType.Guild)] -public class SettingsModule : InteractionModuleBase +public class SettingsModule : AsterionInteractionModuleBase { private readonly IDataService _dataService; private readonly ILogger _logger; From dcb2566502675ecb4fcec7310c17edc5d5c75692 Mon Sep 17 00:00:00 2001 From: Zechiax <106590288+Zechiax@users.noreply.github.com> Date: Mon, 12 Jun 2023 14:19:10 +0200 Subject: [PATCH 05/30] License to AGPL --- LICENSE | 149 ++++++++++++++++++++++++++------------------------------ 1 file changed, 68 insertions(+), 81 deletions(-) diff --git a/LICENSE b/LICENSE index f288702..0ad25db 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies @@ -7,17 +7,15 @@ Preamble - The GNU General Public License is a free, copyleft license for -software and other kinds of works. + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to +our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. +software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you @@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. The precise terms and conditions for copying, distribution and modification follow. @@ -72,7 +60,7 @@ modification follow. 0. Definitions. - "This License" refers to version 3 of the GNU General Public License. + "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. @@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. - 13. Use with the GNU Affero General Public License. + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single +under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General +Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published +GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's +versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. @@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. + GNU Affero General Public License for more details. - You should have received a copy of the GNU General Public License + You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see +For more information on this, and how to apply and follow the GNU AGPL, see . - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. From 49a1ce062ba31de9872800d04a8f5582016c9b13 Mon Sep 17 00:00:00 2001 From: Zechiax <106590288+Zechiax@users.noreply.github.com> Date: Mon, 12 Jun 2023 15:25:12 +0200 Subject: [PATCH 06/30] add localization service --- Asterion/Asterion.cs | 5 ++ Asterion/Asterion.csproj | 15 +++++ Asterion/Modules/BotCommands.cs | 10 +-- .../Resources/responses.en-US.Designer.cs | 62 +++++++++++++++++++ Asterion/Resources/responses.en-US.resx | 21 +++++++ 5 files changed, 104 insertions(+), 9 deletions(-) create mode 100644 Asterion/Resources/responses.en-US.Designer.cs create mode 100644 Asterion/Resources/responses.en-US.resx diff --git a/Asterion/Asterion.cs b/Asterion/Asterion.cs index 152ab04..5f1bf24 100644 --- a/Asterion/Asterion.cs +++ b/Asterion/Asterion.cs @@ -146,6 +146,11 @@ private ServiceProvider ConfigureServices() .AddMemoryCache() .AddLogging(configure => configure.AddSerilog(dispose: true)); + services.AddLocalization(options => + { + options.ResourcesPath = "Resources"; + }); + if (IsDebug()) services.Configure(options => options.MinLevel = LogLevel.Debug); else diff --git a/Asterion/Asterion.csproj b/Asterion/Asterion.csproj index f794ee0..327897c 100644 --- a/Asterion/Asterion.csproj +++ b/Asterion/Asterion.csproj @@ -41,4 +41,19 @@ + + + ResXFileCodeGenerator + responses.en-US.Designer.cs + + + + + + True + True + responses.en-US.resx + + + diff --git a/Asterion/Modules/BotCommands.cs b/Asterion/Modules/BotCommands.cs index 7905d9b..7d814cd 100644 --- a/Asterion/Modules/BotCommands.cs +++ b/Asterion/Modules/BotCommands.cs @@ -10,15 +10,7 @@ public class BotCommands : AsterionInteractionModuleBase [SlashCommand("ping", "Pings the bot", runMode: RunMode.Async)] public async Task Ping() { - if (Context.Client is BaseSocketClient socketClient) - { - var latency = socketClient.Latency; - await RespondAsync($"Pong! Latency: {latency}ms"); - } - else - { - await RespondAsync("Unable to determine latency."); - } + await RespondAsync($"Pong! Latency: {Context.Client.Latency}ms"); } #endif } \ No newline at end of file diff --git a/Asterion/Resources/responses.en-US.Designer.cs b/Asterion/Resources/responses.en-US.Designer.cs new file mode 100644 index 0000000..378ae96 --- /dev/null +++ b/Asterion/Resources/responses.en-US.Designer.cs @@ -0,0 +1,62 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Asterion.Resources { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class responses_en_US { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal responses_en_US() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Asterion.Resources.responses.en-US", typeof(responses_en_US).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + } +} diff --git a/Asterion/Resources/responses.en-US.resx b/Asterion/Resources/responses.en-US.resx new file mode 100644 index 0000000..4ba99c0 --- /dev/null +++ b/Asterion/Resources/responses.en-US.resx @@ -0,0 +1,21 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file From fab51523805e4ff8a319051e96bb4c3403c69ad3 Mon Sep 17 00:00:00 2001 From: Zechiax <106590288+Zechiax@users.noreply.github.com> Date: Mon, 12 Jun 2023 15:53:37 +0200 Subject: [PATCH 07/30] base for translations --- Asterion/Asterion.cs | 1 + Asterion/Interfaces/ILocalizationService.cs | 10 +++++++ Asterion/Modules/BotCommands.cs | 9 +++++- .../Resources/responses.en-US.Designer.cs | 9 ++++++ Asterion/Resources/responses.en-US.resx | 3 ++ Asterion/Services/LocalizationService.cs | 30 +++++++++++++++++++ 6 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 Asterion/Interfaces/ILocalizationService.cs create mode 100644 Asterion/Services/LocalizationService.cs diff --git a/Asterion/Asterion.cs b/Asterion/Asterion.cs index 5f1bf24..e85e80d 100644 --- a/Asterion/Asterion.cs +++ b/Asterion/Asterion.cs @@ -143,6 +143,7 @@ private ServiceProvider ConfigureServices() .AddHttpClient() .AddDbContext() .AddSingleton() + .AddSingleton() .AddMemoryCache() .AddLogging(configure => configure.AddSerilog(dispose: true)); diff --git a/Asterion/Interfaces/ILocalizationService.cs b/Asterion/Interfaces/ILocalizationService.cs new file mode 100644 index 0000000..a43f43e --- /dev/null +++ b/Asterion/Interfaces/ILocalizationService.cs @@ -0,0 +1,10 @@ +using System.Globalization; +using Discord.Interactions; + +namespace Asterion.Interfaces; + +public interface ILocalizationService +{ + string Get(string key); + string Get(string key, CultureInfo cultureInfo); +} \ No newline at end of file diff --git a/Asterion/Modules/BotCommands.cs b/Asterion/Modules/BotCommands.cs index 7d814cd..555f58a 100644 --- a/Asterion/Modules/BotCommands.cs +++ b/Asterion/Modules/BotCommands.cs @@ -1,4 +1,5 @@ using Asterion.Common; +using Asterion.Interfaces; using Discord.Interactions; using Discord.WebSocket; @@ -6,11 +7,17 @@ namespace Asterion.Modules; public class BotCommands : AsterionInteractionModuleBase { + private readonly ILocalizationService _localizationService; + public BotCommands(ILocalizationService localizationService) + { + _localizationService = localizationService; + } + #if DEBUG [SlashCommand("ping", "Pings the bot", runMode: RunMode.Async)] public async Task Ping() { - await RespondAsync($"Pong! Latency: {Context.Client.Latency}ms"); + await RespondAsync($"{_localizationService.Get("HelloWorld")} Pong! Latency: {Context.Client.Latency}ms"); } #endif } \ No newline at end of file diff --git a/Asterion/Resources/responses.en-US.Designer.cs b/Asterion/Resources/responses.en-US.Designer.cs index 378ae96..087b5aa 100644 --- a/Asterion/Resources/responses.en-US.Designer.cs +++ b/Asterion/Resources/responses.en-US.Designer.cs @@ -58,5 +58,14 @@ internal responses_en_US() { resourceCulture = value; } } + + /// + /// Looks up a localized string similar to Hello World!. + /// + internal static string HelloWorld { + get { + return ResourceManager.GetString("HelloWorld", resourceCulture); + } + } } } diff --git a/Asterion/Resources/responses.en-US.resx b/Asterion/Resources/responses.en-US.resx index 4ba99c0..acb170b 100644 --- a/Asterion/Resources/responses.en-US.resx +++ b/Asterion/Resources/responses.en-US.resx @@ -18,4 +18,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Hello World! + \ No newline at end of file diff --git a/Asterion/Services/LocalizationService.cs b/Asterion/Services/LocalizationService.cs new file mode 100644 index 0000000..2a68793 --- /dev/null +++ b/Asterion/Services/LocalizationService.cs @@ -0,0 +1,30 @@ +using System.Globalization; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; + +namespace Asterion.Interfaces; + +public class LocalizationService : ILocalizationService +{ + private readonly IStringLocalizer _localizer; + private readonly ILogger _logger; + + public LocalizationService(IStringLocalizerFactory localizer, ILogger logger) + { + _localizer = localizer.Create("responses", "Asterion"); + _logger = logger; + } + + public string Get(string key) + { + _logger.LogDebug("Getting localized string for key {Key}", key); + var localizedString = _localizer[key]; + _logger.LogDebug("Localized string for key {Key} is {LocalizedString}", key, localizedString); + return localizedString; + } + + public string Get(string key, CultureInfo cultureInfo) + { + return _localizer[key, cultureInfo]; + } +} \ No newline at end of file From edacadb64c82fa7a1149ef9e1ce54709f0d8ebea Mon Sep 17 00:00:00 2001 From: Zechiax <106590288+Zechiax@users.noreply.github.com> Date: Mon, 12 Jun 2023 17:24:53 +0200 Subject: [PATCH 08/30] use more localization --- Asterion/Interfaces/ILocalizationService.cs | 2 ++ Asterion/Modules/BotCommands.cs | 8 +---- Asterion/Modules/ModrinthModule.cs | 4 ++- .../Resources/responses.en-US.Designer.cs | 33 +++++++++++++++++-- Asterion/Resources/responses.en-US.resx | 13 ++++++-- Asterion/Services/LocalizationService.cs | 22 +++++++++++-- 6 files changed, 66 insertions(+), 16 deletions(-) diff --git a/Asterion/Interfaces/ILocalizationService.cs b/Asterion/Interfaces/ILocalizationService.cs index a43f43e..f562cc6 100644 --- a/Asterion/Interfaces/ILocalizationService.cs +++ b/Asterion/Interfaces/ILocalizationService.cs @@ -7,4 +7,6 @@ public interface ILocalizationService { string Get(string key); string Get(string key, CultureInfo cultureInfo); + string Get(string key, object[] parameters); + string Get(string key, CultureInfo cultureInfo, object[] parameters); } \ No newline at end of file diff --git a/Asterion/Modules/BotCommands.cs b/Asterion/Modules/BotCommands.cs index 555f58a..80c0bbf 100644 --- a/Asterion/Modules/BotCommands.cs +++ b/Asterion/Modules/BotCommands.cs @@ -7,17 +7,11 @@ namespace Asterion.Modules; public class BotCommands : AsterionInteractionModuleBase { - private readonly ILocalizationService _localizationService; - public BotCommands(ILocalizationService localizationService) - { - _localizationService = localizationService; - } - #if DEBUG [SlashCommand("ping", "Pings the bot", runMode: RunMode.Async)] public async Task Ping() { - await RespondAsync($"{_localizationService.Get("HelloWorld")} Pong! Latency: {Context.Client.Latency}ms"); + await RespondAsync($"Pong! Latency: {Context.Client.Latency}ms"); } #endif } \ No newline at end of file diff --git a/Asterion/Modules/ModrinthModule.cs b/Asterion/Modules/ModrinthModule.cs index 20c05a0..2319811 100644 --- a/Asterion/Modules/ModrinthModule.cs +++ b/Asterion/Modules/ModrinthModule.cs @@ -30,9 +30,11 @@ public class ModrinthModule : AsterionInteractionModuleBase private readonly InteractiveService _interactive; private readonly ILogger _logger; private readonly ModrinthService _modrinthService; + private readonly ILocalizationService _localizationService; public ModrinthModule(IServiceProvider serviceProvider) { + _localizationService = serviceProvider.GetRequiredService(); _dataService = serviceProvider.GetRequiredService(); _modrinthService = serviceProvider.GetRequiredService(); _interactive = serviceProvider.GetRequiredService(); @@ -98,7 +100,7 @@ await ModifyOriginalResponseAsync(x => }); return; case SearchStatus.NoResult: - await ModifyOriginalResponseAsync(x => { x.Content = $"No result for query '{query}'"; }); + await ModifyOriginalResponseAsync(x => { x.Content = _localizationService.Get("Modrinth_Search_NoResult", new object[]{query}); }); return; case SearchStatus.UnknownError: await ModifyOriginalResponseAsync(x => { x.Content = "Unknown error, please try again later"; }); diff --git a/Asterion/Resources/responses.en-US.Designer.cs b/Asterion/Resources/responses.en-US.Designer.cs index 087b5aa..810b251 100644 --- a/Asterion/Resources/responses.en-US.Designer.cs +++ b/Asterion/Resources/responses.en-US.Designer.cs @@ -60,11 +60,38 @@ internal responses_en_US() { } /// - /// Looks up a localized string similar to Hello World!. + /// Looks up a localized string similar to The Modrinth API is currently unavailable.. /// - internal static string HelloWorld { + internal static string Error_ModrinthApiUnavailable { get { - return ResourceManager.GetString("HelloWorld", resourceCulture); + return ResourceManager.GetString("Error_ModrinthApiUnavailable", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please try again later.. + /// + internal static string Error_TryAgainLater { + get { + return ResourceManager.GetString("Error_TryAgainLater", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unknown error. + /// + internal static string Error_Unknown { + get { + return ResourceManager.GetString("Error_Unknown", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No results for query '{0}'. + /// + internal static string Modrinth_Search_NoResult { + get { + return ResourceManager.GetString("Modrinth_Search_NoResult", resourceCulture); } } } diff --git a/Asterion/Resources/responses.en-US.resx b/Asterion/Resources/responses.en-US.resx index acb170b..cf097a6 100644 --- a/Asterion/Resources/responses.en-US.resx +++ b/Asterion/Resources/responses.en-US.resx @@ -18,7 +18,16 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Hello World! + + The Modrinth API is currently unavailable. + + + Please try again later. + + + Unknown error + + + No results for query '{0}' \ No newline at end of file diff --git a/Asterion/Services/LocalizationService.cs b/Asterion/Services/LocalizationService.cs index 2a68793..c0171dd 100644 --- a/Asterion/Services/LocalizationService.cs +++ b/Asterion/Services/LocalizationService.cs @@ -1,8 +1,9 @@ using System.Globalization; +using Asterion.Interfaces; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; -namespace Asterion.Interfaces; +namespace Asterion.Services; public class LocalizationService : ILocalizationService { @@ -17,9 +18,14 @@ public LocalizationService(IStringLocalizerFactory localizer, ILogger Date: Wed, 14 Jun 2023 22:40:55 +0200 Subject: [PATCH 09/30] localization of search errors and centralize them --- Asterion/Asterion.csproj | 4 +- .../Common/AsterionInteractionModuleBase.cs | 47 ++++++++++++++++++- Asterion/Interfaces/ILocalizationService.cs | 9 ++-- Asterion/Modules/BotCommands.cs | 3 ++ Asterion/Modules/BotManagement.cs | 2 +- Asterion/Modules/ChartModule.cs | 7 ++- Asterion/Modules/GuildManagement.cs | 3 +- Asterion/Modules/ModrinthInteractionModule.cs | 3 +- Asterion/Modules/ModrinthModule.cs | 25 ++-------- Asterion/Modules/SettingsModule.cs | 2 +- Asterion/Resources/responses.en-US.resx | 9 ++-- Asterion/Services/LocalizationService.cs | 26 +++++++--- Asterion/Services/Modrinth/ModrinthService.cs | 20 ++++---- Asterion/Services/Modrinth/SearchResult.cs | 5 +- 14 files changed, 109 insertions(+), 56 deletions(-) diff --git a/Asterion/Asterion.csproj b/Asterion/Asterion.csproj index 327897c..facc945 100644 --- a/Asterion/Asterion.csproj +++ b/Asterion/Asterion.csproj @@ -42,14 +42,14 @@ - + ResXFileCodeGenerator responses.en-US.Designer.cs - + True True responses.en-US.resx diff --git a/Asterion/Common/AsterionInteractionModuleBase.cs b/Asterion/Common/AsterionInteractionModuleBase.cs index c8c04b7..7fa6d26 100644 --- a/Asterion/Common/AsterionInteractionModuleBase.cs +++ b/Asterion/Common/AsterionInteractionModuleBase.cs @@ -1,14 +1,57 @@ using System.Globalization; +using Asterion.Interfaces; using Discord; using Discord.Interactions; +using Color = Discord.Color; namespace Asterion.Common; public class AsterionInteractionModuleBase : InteractionModuleBase { protected CultureInfo? CommandCultureInfo { get; set; } - - + protected ILocalizationService LocalizationService { get; } + public AsterionInteractionModuleBase(ILocalizationService localizationService) + { + LocalizationService = localizationService; + } + + protected async Task FollowupWithSearchResultErrorAsync(Services.Modrinth.SearchResult status) + { + if (status.Success) + { + throw new ArgumentException("SearchResult was successful, but was expected to be an error"); + } + + string title = LocalizationService.Get("Modrinth_Search_Unsuccessful", CommandCultureInfo); + + string? description; + switch (status.SearchStatus) + { + case Services.Modrinth.SearchStatus.ApiDown: + description = LocalizationService.Get("Error_ModrinthApiUnavailable", CommandCultureInfo); + title += ". " + LocalizationService.Get("Error_TryAgainLater", CommandCultureInfo); + break; + case Services.Modrinth.SearchStatus.NoResult: + description = LocalizationService.Get("Modrinth_Search_NoResult_WithQuery", CommandCultureInfo, new object[] {status.Query}); + break; + case Services.Modrinth.SearchStatus.UnknownError: + description = LocalizationService.Get("Error_Unknown", CommandCultureInfo); + title += ". " + LocalizationService.Get("Error_TryAgainLater", CommandCultureInfo); + break; + default: + throw new ArgumentOutOfRangeException(); + } + + var embed = new EmbedBuilder() + .WithTitle(title) + .WithDescription(description) + .WithColor(Color.Red) + .WithCurrentTimestamp() + .Build(); + + await FollowupAsync(embeds: new[] {embed}); + } + public override void BeforeExecute(ICommandInfo cmd) { // We currently set US culture for all commands diff --git a/Asterion/Interfaces/ILocalizationService.cs b/Asterion/Interfaces/ILocalizationService.cs index f562cc6..9eef949 100644 --- a/Asterion/Interfaces/ILocalizationService.cs +++ b/Asterion/Interfaces/ILocalizationService.cs @@ -1,12 +1,13 @@ using System.Globalization; using Discord.Interactions; +using Microsoft.Extensions.Localization; namespace Asterion.Interfaces; public interface ILocalizationService { - string Get(string key); - string Get(string key, CultureInfo cultureInfo); - string Get(string key, object[] parameters); - string Get(string key, CultureInfo cultureInfo, object[] parameters); + LocalizedString Get(string key); + LocalizedString Get(string key, CultureInfo? cultureInfo); + LocalizedString Get(string key, params object[] parameters); + LocalizedString Get(string key, CultureInfo? cultureInfo, params object[] parameters); } \ No newline at end of file diff --git a/Asterion/Modules/BotCommands.cs b/Asterion/Modules/BotCommands.cs index 80c0bbf..35b0dd8 100644 --- a/Asterion/Modules/BotCommands.cs +++ b/Asterion/Modules/BotCommands.cs @@ -14,4 +14,7 @@ public async Task Ping() await RespondAsync($"Pong! Latency: {Context.Client.Latency}ms"); } #endif + public BotCommands(ILocalizationService localizationService) : base(localizationService) + { + } } \ No newline at end of file diff --git a/Asterion/Modules/BotManagement.cs b/Asterion/Modules/BotManagement.cs index b60ca96..8148557 100644 --- a/Asterion/Modules/BotManagement.cs +++ b/Asterion/Modules/BotManagement.cs @@ -11,7 +11,7 @@ public class BotManagement : AsterionInteractionModuleBase { private readonly IDataService _dataService; - public BotManagement(IServiceProvider serviceProvider) + public BotManagement(IServiceProvider serviceProvider, ILocalizationService localizationService) : base(localizationService) { _dataService = serviceProvider.GetRequiredService(); } diff --git a/Asterion/Modules/ChartModule.cs b/Asterion/Modules/ChartModule.cs index 8745de0..e8bbfd9 100644 --- a/Asterion/Modules/ChartModule.cs +++ b/Asterion/Modules/ChartModule.cs @@ -1,5 +1,6 @@ using Asterion.AutocompleteHandlers; using Asterion.Common; +using Asterion.Interfaces; using Asterion.Services; using Asterion.Services.Modrinth; using Discord; @@ -19,7 +20,8 @@ public class ChartModule : AsterionInteractionModuleBase private readonly ModrinthService _modrinthService; private readonly ProjectStatisticsManager _projectStatisticsManager; - public ChartModule(IServiceProvider serviceProvider) + public ChartModule(IServiceProvider serviceProvider, ILocalizationService localizationService) : base( + localizationService) { _projectStatisticsManager = serviceProvider.GetRequiredService(); _modrinthService = serviceProvider.GetRequiredService(); @@ -30,7 +32,8 @@ public ChartModule(IServiceProvider serviceProvider) [Group("chart", "Creates charts of different statistics")] public class ChartType : ChartModule { - public ChartType(IServiceProvider serviceProvider) : base(serviceProvider) + public ChartType(IServiceProvider serviceProvider, ILocalizationService localizationService) : base( + serviceProvider, localizationService) { } diff --git a/Asterion/Modules/GuildManagement.cs b/Asterion/Modules/GuildManagement.cs index cd88b12..e4cd412 100644 --- a/Asterion/Modules/GuildManagement.cs +++ b/Asterion/Modules/GuildManagement.cs @@ -12,7 +12,8 @@ public class GuildManagement : AsterionInteractionModuleBase { private readonly IDataService _dataService; - public GuildManagement(IDataService dataService) + public GuildManagement(IDataService dataService, ILocalizationService localizationService) : base( + localizationService) { _dataService = dataService; } diff --git a/Asterion/Modules/ModrinthInteractionModule.cs b/Asterion/Modules/ModrinthInteractionModule.cs index 96747ef..393e109 100644 --- a/Asterion/Modules/ModrinthInteractionModule.cs +++ b/Asterion/Modules/ModrinthInteractionModule.cs @@ -22,7 +22,8 @@ public class ModrinthInteractionModule : AsterionInteractionModuleBase private readonly ILogger _logger; private readonly ModrinthService _modrinthService; - public ModrinthInteractionModule(IServiceProvider serviceProvider) + public ModrinthInteractionModule(IServiceProvider serviceProvider, ILocalizationService localizationService) : + base(localizationService) { _dataService = serviceProvider.GetRequiredService(); _modrinthService = serviceProvider.GetRequiredService(); diff --git a/Asterion/Modules/ModrinthModule.cs b/Asterion/Modules/ModrinthModule.cs index 2319811..1634201 100644 --- a/Asterion/Modules/ModrinthModule.cs +++ b/Asterion/Modules/ModrinthModule.cs @@ -32,7 +32,7 @@ public class ModrinthModule : AsterionInteractionModuleBase private readonly ModrinthService _modrinthService; private readonly ILocalizationService _localizationService; - public ModrinthModule(IServiceProvider serviceProvider) + public ModrinthModule(IServiceProvider serviceProvider, ILocalizationService localizationService) : base(localizationService) { _localizationService = serviceProvider.GetRequiredService(); _dataService = serviceProvider.GetRequiredService(); @@ -91,26 +91,11 @@ public async Task SearchProject([Summary("Query", "Query, ID or slug")] [MaxLeng _logger.LogDebug("Search for query '{Query}'", query); var searchResult = await _modrinthService.FindProject(query); _logger.LogDebug("Search status: {SearchStatus}", searchResult.SearchStatus); - switch (searchResult.SearchStatus) + + if (searchResult.Success == false) { - case SearchStatus.ApiDown: - await ModifyOriginalResponseAsync(x => - { - x.Content = "Modrinth API is probably down, please try again later"; - }); - return; - case SearchStatus.NoResult: - await ModifyOriginalResponseAsync(x => { x.Content = _localizationService.Get("Modrinth_Search_NoResult", new object[]{query}); }); - return; - case SearchStatus.UnknownError: - await ModifyOriginalResponseAsync(x => { x.Content = "Unknown error, please try again later"; }); - return; - case SearchStatus.FoundById: - break; - case SearchStatus.FoundBySearch: - break; - default: - throw new ArgumentOutOfRangeException(); + await FollowupWithSearchResultErrorAsync(searchResult); + return; } var projectDto = searchResult.Payload; diff --git a/Asterion/Modules/SettingsModule.cs b/Asterion/Modules/SettingsModule.cs index f944713..0ae83ef 100644 --- a/Asterion/Modules/SettingsModule.cs +++ b/Asterion/Modules/SettingsModule.cs @@ -18,7 +18,7 @@ public class SettingsModule : AsterionInteractionModuleBase private readonly IDataService _dataService; private readonly ILogger _logger; - public SettingsModule(IServiceProvider serviceProvider) + public SettingsModule(ILocalizationService localizationService, IServiceProvider serviceProvider) : base(localizationService) { _logger = serviceProvider.GetRequiredService>(); _dataService = serviceProvider.GetRequiredService(); diff --git a/Asterion/Resources/responses.en-US.resx b/Asterion/Resources/responses.en-US.resx index cf097a6..5cceae4 100644 --- a/Asterion/Resources/responses.en-US.resx +++ b/Asterion/Resources/responses.en-US.resx @@ -19,15 +19,18 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - The Modrinth API is currently unavailable. + The Modrinth API is currently unavailable - Please try again later. + Please try again later Unknown error - + No results for query '{0}' + + Search was not successful + \ No newline at end of file diff --git a/Asterion/Services/LocalizationService.cs b/Asterion/Services/LocalizationService.cs index c0171dd..786fabb 100644 --- a/Asterion/Services/LocalizationService.cs +++ b/Asterion/Services/LocalizationService.cs @@ -9,6 +9,7 @@ public class LocalizationService : ILocalizationService { private readonly IStringLocalizer _localizer; private readonly ILogger _logger; + private readonly CultureInfo _defaultCultureInfo = CultureInfo.GetCultureInfo("en-US"); public LocalizationService(IStringLocalizerFactory localizer, ILogger logger) { @@ -16,31 +17,42 @@ public LocalizationService(IStringLocalizerFactory localizer, ILogger> FindProject(string query) catch (ModrinthApiException e) { _logger.LogDebug(e, "Error while searching for project '{Query}'", query); - return new SearchResult(new ProjectDto(), SearchStatus.ApiDown); + return new SearchResult(new ProjectDto(), SearchStatus.ApiDown, query); } catch (Exception e) { _logger.LogError(e, "Error while searching for project '{Query}'", query); - return new SearchResult(new ProjectDto(), SearchStatus.ApiDown); + return new SearchResult(new ProjectDto(), SearchStatus.ApiDown, query); } if (projectFoundById && project is not null) @@ -347,7 +347,7 @@ public async Task> FindProject(string query) { Project = project, SearchResponse = searchResponse - }, SearchStatus.FoundById); + }, SearchStatus.FoundById, query); SetSearchResultToCache(result, query); @@ -361,7 +361,7 @@ public async Task> FindProject(string query) // No search results if (searchResponse.TotalHits <= 0) - return new SearchResult(new ProjectDto(), SearchStatus.NoResult); + return new SearchResult(new ProjectDto(), SearchStatus.NoResult, query); // Return first result project = await Api.Project.GetAsync(searchResponse.Hits[0].ProjectId); @@ -370,7 +370,7 @@ public async Task> FindProject(string query) { Project = project, SearchResponse = searchResponse - }, SearchStatus.FoundBySearch); + }, SearchStatus.FoundBySearch, query); SetSearchResultToCache(result, query); @@ -380,7 +380,7 @@ public async Task> FindProject(string query) { _logger.LogWarning("Could not get project information for query '{Query}', exception: {Message}", query, e.Message); - return new SearchResult(new ProjectDto(), SearchStatus.ApiDown); + return new SearchResult(new ProjectDto(), SearchStatus.ApiDown, query); } } @@ -412,11 +412,11 @@ public async Task> FindUser(string query) { // Project not found by slug or id _logger.LogDebug("User not found '{Query}'", query); - return new SearchResult(new UserDto(), SearchStatus.NoResult); + return new SearchResult(new UserDto(), SearchStatus.NoResult, query); } catch (Exception) { - return new SearchResult(new UserDto(), SearchStatus.ApiDown); + return new SearchResult(new UserDto(), SearchStatus.ApiDown, query); } // User can't be null from here @@ -429,7 +429,7 @@ public async Task> FindUser(string query) User = user, Projects = projects, MajorColor = (await httpClient.GetMajorColorFromImageUrl(user.AvatarUrl)).ToDiscordColor() - }, SearchStatus.FoundBySearch); + }, SearchStatus.FoundBySearch, query); _cache.Set($"user-query:{user.Id}", searchResult, TimeSpan.FromMinutes(60)); _cache.Set($"user-query:{query}", searchResult, TimeSpan.FromMinutes(60)); @@ -438,7 +438,7 @@ public async Task> FindUser(string query) } catch (Exception) { - return new SearchResult(new UserDto(), SearchStatus.ApiDown); + return new SearchResult(new UserDto(), SearchStatus.ApiDown, query); } } } \ No newline at end of file diff --git a/Asterion/Services/Modrinth/SearchResult.cs b/Asterion/Services/Modrinth/SearchResult.cs index cb8ca52..b6aebf6 100644 --- a/Asterion/Services/Modrinth/SearchResult.cs +++ b/Asterion/Services/Modrinth/SearchResult.cs @@ -5,18 +5,19 @@ namespace Asterion.Services.Modrinth; public class SearchResult { - public SearchResult(T? payload, SearchStatus searchStatus) + public SearchResult(T? payload, SearchStatus searchStatus, string query) { Payload = payload; SearchStatus = searchStatus; SearchTime = DateTime.UtcNow; + Query = query; } /// /// Success in getting results /// public bool Success => SearchStatus is SearchStatus.FoundById or SearchStatus.FoundBySearch; - + public string Query { get; } public DateTime SearchTime { get; private set; } public SearchStatus SearchStatus { get; } public T? Payload { get; private set; } From 518d0cadbd83523abc86e939e061c787f4eb0c91 Mon Sep 17 00:00:00 2001 From: Zechiax <106590288+Zechiax@users.noreply.github.com> Date: Wed, 14 Jun 2023 23:04:05 +0200 Subject: [PATCH 10/30] add string --- Asterion/Resources/responses.en-US.resx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Asterion/Resources/responses.en-US.resx b/Asterion/Resources/responses.en-US.resx index 5cceae4..40b726f 100644 --- a/Asterion/Resources/responses.en-US.resx +++ b/Asterion/Resources/responses.en-US.resx @@ -33,4 +33,10 @@ Search was not successful + + Settings + + + Notification Style + \ No newline at end of file From a21418a7aa36398a107c39fc2675a324b3c7f7f8 Mon Sep 17 00:00:00 2001 From: Zechiax <106590288+Zechiax@users.noreply.github.com> Date: Wed, 14 Jun 2023 23:32:30 +0200 Subject: [PATCH 11/30] use Quartz for recurring client task --- Asterion/Asterion.cs | 16 +++++++++ Asterion/Asterion.csproj | 3 ++ Asterion/Services/ClientService.cs | 52 +++++++++++++++++++++--------- 3 files changed, 55 insertions(+), 16 deletions(-) diff --git a/Asterion/Asterion.cs b/Asterion/Asterion.cs index e85e80d..4b745e2 100644 --- a/Asterion/Asterion.cs +++ b/Asterion/Asterion.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Modrinth; +using Quartz; using Serilog; using RunMode = Discord.Commands.RunMode; @@ -102,6 +103,11 @@ public async Task MainAsync() // so that we can get the correct guild count services.GetRequiredService().Initialize(); + // We start the scheduler after the client has been logged in + // so that we can get the correct guild count + var scheduler = await services.GetRequiredService().GetScheduler(); + await scheduler.Start(); + await Task.Delay(Timeout.Infinite); } @@ -147,6 +153,16 @@ private ServiceProvider ConfigureServices() .AddMemoryCache() .AddLogging(configure => configure.AddSerilog(dispose: true)); + services.AddQuartz(q => + { + q.UseInMemoryStore(); + q.UseMicrosoftDependencyInjectionJobFactory(); + }); + services.AddQuartzHostedService(options => + { + options.WaitForJobsToComplete = true; + }); + services.AddLocalization(options => { options.ResourcesPath = "Resources"; diff --git a/Asterion/Asterion.csproj b/Asterion/Asterion.csproj index facc945..bf5c248 100644 --- a/Asterion/Asterion.csproj +++ b/Asterion/Asterion.csproj @@ -33,6 +33,9 @@ + + + diff --git a/Asterion/Services/ClientService.cs b/Asterion/Services/ClientService.cs index d9d5149..3ba913e 100644 --- a/Asterion/Services/ClientService.cs +++ b/Asterion/Services/ClientService.cs @@ -3,6 +3,7 @@ using Asterion.Interfaces; using Discord.WebSocket; using Microsoft.Extensions.DependencyInjection; +using Quartz; using Timer = System.Timers.Timer; namespace Asterion.Services; @@ -11,35 +12,38 @@ public class ClientService { private readonly DiscordSocketClient _client; private readonly IDataService _data; - private readonly BackgroundWorker _refreshWorker; + private readonly ISchedulerFactory _schedulerFactory; public ClientService(IServiceProvider serviceProvider) { _client = serviceProvider.GetRequiredService(); _data = serviceProvider.GetRequiredService(); - - _refreshWorker = new BackgroundWorker(); - _refreshWorker.DoWork += RefreshAsync; - - // Refresh status every 15 minutes - var checkTimer = new Timer(TimeSpan.FromMinutes(15).TotalMilliseconds); - checkTimer.Elapsed += checkTimer_Elapsed; - checkTimer.Start(); + _schedulerFactory = serviceProvider.GetRequiredService(); } public void Initialize() { _client.Ready += SetGameAsync; + ScheduleRefreshJob(); } - private async void RefreshAsync(object? sender, DoWorkEventArgs e) + private async void ScheduleRefreshJob() { - await SetGameAsync(); - } + var scheduler = await _schedulerFactory.GetScheduler(); + + IJobDetail job = JobBuilder.Create() + .WithIdentity("RefreshJob", "group1") + .Build(); - private void checkTimer_Elapsed(object? sender, ElapsedEventArgs e) - { - if (!_refreshWorker.IsBusy) _refreshWorker.RunWorkerAsync(); + ITrigger trigger = TriggerBuilder.Create() + .WithIdentity("RefreshTrigger", "group1") + .StartNow() + .WithSimpleSchedule(x => x + .WithIntervalInSeconds(10) // Every 15 minutes + .RepeatForever()) + .Build(); + + await scheduler.ScheduleJob(job, trigger); } public async Task SetGameAsync() @@ -49,4 +53,20 @@ public async Task SetGameAsync() await _client.SetGameAsync( $"Monitoring {count} project{(count == 1 ? null : 's')} for updates in {_client.Guilds.Count} servers"); } -} \ No newline at end of file + + // Define your Quartz job class as an inner class of ClientService + private class RefreshJob : IJob + { + private readonly ClientService _clientService; + + public RefreshJob(ClientService clientService) + { + _clientService = clientService; + } + + public async Task Execute(IJobExecutionContext context) + { + await _clientService.SetGameAsync(); + } + } +} From 9856056710d7de42c68bea169a86081595edfaf2 Mon Sep 17 00:00:00 2001 From: Zechiax <106590288+Zechiax@users.noreply.github.com> Date: Thu, 15 Jun 2023 00:03:33 +0200 Subject: [PATCH 12/30] stop the scheduler on shutdown --- Asterion/Asterion.cs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/Asterion/Asterion.cs b/Asterion/Asterion.cs index 4b745e2..9807c12 100644 --- a/Asterion/Asterion.cs +++ b/Asterion/Asterion.cs @@ -82,6 +82,15 @@ public async Task MainAsync() await client.LoginAsync(TokenType.Bot, _config.GetValue("token")); await client.StartAsync(); + // We start the stats service after the client has been logged in + // so that we can get the correct guild count + services.GetRequiredService().Initialize(); + + // We start the scheduler after the client has been logged in + // so that we can get the correct guild count + var scheduler = await services.GetRequiredService().GetScheduler(); + await scheduler.Start(); + // Disconnect from Discord when pressing Ctrl+C Console.CancelKeyPress += (_, args) => { @@ -92,21 +101,15 @@ public async Task MainAsync() client.LogoutAsync().Wait(); logger.LogInformation("Stopping the client"); client.StopAsync().Wait(); + + logger.LogInformation("Stopping the scheduler"); + scheduler.Shutdown().Wait(); logger.LogInformation("Disposing services"); services.DisposeAsync().GetAwaiter().GetResult(); args.Cancel = false; }; - - // We start the stats service after the client has been logged in - // so that we can get the correct guild count - services.GetRequiredService().Initialize(); - - // We start the scheduler after the client has been logged in - // so that we can get the correct guild count - var scheduler = await services.GetRequiredService().GetScheduler(); - await scheduler.Start(); await Task.Delay(Timeout.Infinite); } From efbed7184da09e69e989c190b039fb13890fc618 Mon Sep 17 00:00:00 2001 From: Zechiax <106590288+Zechiax@users.noreply.github.com> Date: Thu, 15 Jun 2023 14:11:21 +0200 Subject: [PATCH 13/30] add base for update job --- Asterion.Test/SplitExtensionTests.cs | 53 ++++++++ Asterion/Extensions/ArrayExtensions.cs | 16 +++ .../Services/Modrinth/SearchUpdatesJob.cs | 114 ++++++++++++++++++ 3 files changed, 183 insertions(+) create mode 100644 Asterion.Test/SplitExtensionTests.cs create mode 100644 Asterion/Extensions/ArrayExtensions.cs create mode 100644 Asterion/Services/Modrinth/SearchUpdatesJob.cs diff --git a/Asterion.Test/SplitExtensionTests.cs b/Asterion.Test/SplitExtensionTests.cs new file mode 100644 index 0000000..278a8bc --- /dev/null +++ b/Asterion.Test/SplitExtensionTests.cs @@ -0,0 +1,53 @@ +using Asterion.Extensions; + +namespace Asterion.Test; + +[TestFixture] +public class SplitExtensionTests +{ + [Test] + public void TestSliceOnEmptyArray() + { + var array = Array.Empty(); + + var result = array.Split(10).ToArray(); + + Assert.That(result, Is.Empty); + } + + [Test] + public void TestSplitEvenArray() + { + int[] numbers = {1, 2, 3, 4, 5, 6}; + var segments = numbers.Split(2).ToArray(); + + Assert.Multiple(() => + { + Assert.That(segments, Has.Length.EqualTo(3)); + Assert.That(segments[0], Has.Count.EqualTo(2)); + Assert.That(segments[0].Array, Is.Not.Null); + Assert.That(segments[0].Array![segments[0].Offset], Is.EqualTo(1)); + Assert.That(segments[0].Array![segments[0].Offset + 1], Is.EqualTo(2)); + }); + } + + [Test] + public void TestSplitOddArray() + { + int[] numbers = {1, 2, 3, 4, 5, 6, 7}; + var segments = numbers.Split(3).ToArray(); + + Assert.Multiple(() => + { + Assert.That(segments, Has.Length.EqualTo(3)); + Assert.That(segments[0], Has.Count.EqualTo(3)); + Assert.That(segments[0].Array, Is.Not.Null); + Assert.That(segments[0].Array![segments[0].Offset], Is.EqualTo(1)); + Assert.That(segments[0].Array![segments[0].Offset + 1], Is.EqualTo(2)); + Assert.That(segments[0].Array![segments[0].Offset + 2], Is.EqualTo(3)); + + // Last segment should have 1 element + Assert.That(segments[2], Has.Count.EqualTo(1)); + }); + } +} \ No newline at end of file diff --git a/Asterion/Extensions/ArrayExtensions.cs b/Asterion/Extensions/ArrayExtensions.cs new file mode 100644 index 0000000..947647f --- /dev/null +++ b/Asterion/Extensions/ArrayExtensions.cs @@ -0,0 +1,16 @@ +namespace Asterion.Extensions; + +public static class ArrayExtensions +{ + public static IEnumerable> Split(this T[] array, int blockSize) + { + var offset = 0; + while (offset < array.Length) + { + var remaining = array.Length - offset; + var blockSizeToUse = Math.Min(remaining, blockSize); + yield return new ArraySegment(array, offset, blockSizeToUse); + offset += blockSizeToUse; + } + } +} \ No newline at end of file diff --git a/Asterion/Services/Modrinth/SearchUpdatesJob.cs b/Asterion/Services/Modrinth/SearchUpdatesJob.cs new file mode 100644 index 0000000..f9c9c6d --- /dev/null +++ b/Asterion/Services/Modrinth/SearchUpdatesJob.cs @@ -0,0 +1,114 @@ +using Asterion.Database.Models; +using Asterion.Extensions; +using Asterion.Interfaces; +using Microsoft.Extensions.Logging; +using Modrinth; +using Modrinth.Models; +using Quartz; +using Version = Modrinth.Models.Version; + +namespace Asterion.Services.Modrinth; + +public class SearchUpdatesJob : IJob +{ + private readonly IModrinthClient _client; + private readonly IDataService _dataService; + private readonly ILogger _logger; + private readonly ProjectStatisticsManager _projectStatisticsManager; + + private const int SplitSize = 100; + + public SearchUpdatesJob(IModrinthClient client, IDataService dataService, ILogger logger, ProjectStatisticsManager projectStatisticsManager) + { + _client = client; + _dataService = dataService; + _logger = logger; + _projectStatisticsManager = projectStatisticsManager; + } + + public async Task Execute(IJobExecutionContext context) + { + _logger.LogInformation("Update search job started"); + var time = DateTime.UtcNow; + + try + { + await DoExecutionWork(); + } + // We should only catch exception that we know could be thrown, as Quartz will reschedule the job if we throw + finally + { + + } + + } + + private async Task DoExecutionWork() + { + // We won't catch any exceptions here, it's not this methods responsibility to handle them + var projectsDto = await GetProjectsAsync(); + var projectIds = projectsDto.Select(p => p.ProjectId); + + var projects = await _client.Project.GetMultipleAsync(projectIds); + + // We let the statistics update run in the background, as it's not critical to the job + var copy = projects.ToList(); + var updateStatsTask = UpdateStatisticsData(copy); + + var updatedProjects = FilterUpdatedProjects(projectsDto, projects); + var versionsIds = updatedProjects.SelectMany(p => p.Versions).ToArray(); + + var versions = await GetAllVersionsAsync(versionsIds); + + + // Let's await all the background tasks we started + await updateStatsTask; + } + + private async Task> GetAllVersionsAsync(string[] versionIds) + { + var versionSegments = versionIds.Split(SplitSize).ToList(); + var versions = new List(); + + foreach (var segment in versionSegments) + { + var segmentVersions = await _client.Version.GetMultipleAsync(segment); + versions.AddRange(segmentVersions); + } + + return versions; + } + + private async Task UpdateStatisticsData(IEnumerable projects) + { + var projectList = projects.ToList(); + _logger.LogDebug("Updating statistics for {Count} projects", projectList.Count); + foreach (var project in projectList) + { + await _projectStatisticsManager.UpdateDownloadsAsync(project); + } + _logger.LogDebug("Statistics update finished"); + } + + private static IEnumerable FilterUpdatedProjects(IList projectsDto, IEnumerable projects) + { + var updatedProjects = new List(); + foreach (var project in projects) + { + var projectDto = projectsDto.First(p => p.ProjectId == project.Id); + if (projectDto.LastUpdated < project.Updated) + { + updatedProjects.Add(project); + } + } + + return updatedProjects; + } + + private async Task> GetProjectsAsync() + { + var projects = await _dataService.GetAllModrinthProjectsAsync(); + _logger.LogDebug("Found {Count} projects", projects.Count); + return projects; + } +} \ No newline at end of file From 95424ce144c000f864faf9391480f7f87967c524 Mon Sep 17 00:00:00 2001 From: Zechiax <106590288+Zechiax@users.noreply.github.com> Date: Thu, 15 Jun 2023 23:41:28 +0200 Subject: [PATCH 14/30] make SearchUpdatesJob almost complete --- .../Services/Modrinth/SearchUpdatesJob.cs | 66 ++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/Asterion/Services/Modrinth/SearchUpdatesJob.cs b/Asterion/Services/Modrinth/SearchUpdatesJob.cs index f9c9c6d..eaae3a7 100644 --- a/Asterion/Services/Modrinth/SearchUpdatesJob.cs +++ b/Asterion/Services/Modrinth/SearchUpdatesJob.cs @@ -40,7 +40,6 @@ public async Task Execute(IJobExecutionContext context) { } - } private async Task DoExecutionWork() @@ -55,15 +54,78 @@ private async Task DoExecutionWork() var copy = projects.ToList(); var updateStatsTask = UpdateStatisticsData(copy); - var updatedProjects = FilterUpdatedProjects(projectsDto, projects); + var updatedProjects = FilterUpdatedProjects(projectsDto, projects).ToArray(); var versionsIds = updatedProjects.SelectMany(p => p.Versions).ToArray(); var versions = await GetAllVersionsAsync(versionsIds); + + Dictionary> projectVersions = new(); + foreach (var project in updatedProjects) + { + var projectVersionsList = versions.Where(v => v.ProjectId == project.Id) + .OrderByDescending(v => v.DatePublished) + .ToList(); + projectVersions.Add(project, projectVersionsList); + } + + // This will modify the projectVersions dictionary in-place and only keep the projects and versions that have updates + await CheckForUpdatesAsync(projectVersions); + + // TODO: Create DiscordNotifyJob // Let's await all the background tasks we started await updateStatsTask; } + + private async Task CheckForUpdatesAsync(Dictionary> projectVersions) + { + // This list will hold the keys to remove from the projectVersions dictionary + var keysToRemove = new List(); + + // Loop through the projects in the dictionary + foreach (var (project, versions) in projectVersions) + { + var latestVersion = versions.First(); + var dbProject = await _dataService.GetModrinthProjectByIdAsync(project.Id); + if (dbProject == null) + { + _logger.LogError("Failed to find project {ProjectId} in the database", project.Id); + keysToRemove.Add(project); + continue; + } + + if (dbProject.LastCheckVersion == latestVersion.Id) + { + keysToRemove.Add(project); + continue; + } + + _logger.LogInformation("Found update for project {ProjectId}", project.Id); + var success = await _dataService.UpdateModrinthProjectAsync(dbProject.ProjectId, latestVersion.Id, project.Title, + DateTime.UtcNow); + + if (!success) + { + _logger.LogError("Failed to update project {ProjectId} in the database", project.Id); + keysToRemove.Add(project); + continue; + } + + // Remove versions from the list that are the same or newer than the latest version + // This modifies the list associated with the project in the dictionary + var keepVersions = versions.Where(v => v.DatePublished >= latestVersion.DatePublished).ToList(); + projectVersions[project] = keepVersions; + } + + // Now remove the projects from the dictionary that were added to the keysToRemove list + // This modifies the dictionary in-place + foreach (var key in keysToRemove) + { + projectVersions.Remove(key); + } + } + private async Task> GetAllVersionsAsync(string[] versionIds) { From 2120d7293037f0d97cd088d6f65bd3bceda0c1ad Mon Sep 17 00:00:00 2001 From: Zechiax <106590288+Zechiax@users.noreply.github.com> Date: Fri, 16 Jun 2023 16:14:08 +0200 Subject: [PATCH 15/30] base for Discord notification job --- .../Services/Modrinth/SearchUpdatesJob.cs | 28 +++++- .../Modrinth/SendDiscordNotificationJob.cs | 93 +++++++++++++++++++ 2 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 Asterion/Services/Modrinth/SendDiscordNotificationJob.cs diff --git a/Asterion/Services/Modrinth/SearchUpdatesJob.cs b/Asterion/Services/Modrinth/SearchUpdatesJob.cs index eaae3a7..24eea39 100644 --- a/Asterion/Services/Modrinth/SearchUpdatesJob.cs +++ b/Asterion/Services/Modrinth/SearchUpdatesJob.cs @@ -1,4 +1,5 @@ -using Asterion.Database.Models; +using System.Text.Json; +using Asterion.Database.Models; using Asterion.Extensions; using Asterion.Interfaces; using Microsoft.Extensions.Logging; @@ -15,11 +16,13 @@ public class SearchUpdatesJob : IJob private readonly IDataService _dataService; private readonly ILogger _logger; private readonly ProjectStatisticsManager _projectStatisticsManager; + private readonly IScheduler _scheduler; private const int SplitSize = 100; - public SearchUpdatesJob(IModrinthClient client, IDataService dataService, ILogger logger, ProjectStatisticsManager projectStatisticsManager) + public SearchUpdatesJob(IScheduler scheduler, IModrinthClient client, IDataService dataService, ILogger logger, ProjectStatisticsManager projectStatisticsManager) { + _scheduler = scheduler; _client = client; _dataService = dataService; _logger = logger; @@ -57,13 +60,13 @@ private async Task DoExecutionWork() var updatedProjects = FilterUpdatedProjects(projectsDto, projects).ToArray(); var versionsIds = updatedProjects.SelectMany(p => p.Versions).ToArray(); - var versions = await GetAllVersionsAsync(versionsIds); + var versionsList = await GetAllVersionsAsync(versionsIds); Dictionary> projectVersions = new(); foreach (var project in updatedProjects) { - var projectVersionsList = versions.Where(v => v.ProjectId == project.Id) + var projectVersionsList = versionsList.Where(v => v.ProjectId == project.Id) .OrderByDescending(v => v.DatePublished) .ToList(); projectVersions.Add(project, projectVersionsList); @@ -72,7 +75,22 @@ private async Task DoExecutionWork() // This will modify the projectVersions dictionary in-place and only keep the projects and versions that have updates await CheckForUpdatesAsync(projectVersions); - // TODO: Create DiscordNotifyJob + // For each project, we'll create a Discord Notification job and pass it the information it needs + foreach (var (project, versions) in projectVersions) + { + var job = JobBuilder.Create() + //.WithIdentity($"discord-notification-{project.Id}", "modrinth") + .UsingJobData("Project", JsonSerializer.Serialize(project)) + .UsingJobData("Versions", JsonSerializer.Serialize(versions)) + .Build(); + + var trigger = TriggerBuilder.Create() + //.WithIdentity($"discord-notification-{project.Id}", "modrinth") + .StartNow() + .Build(); + + await _scheduler.ScheduleJob(job, trigger); + } // Let's await all the background tasks we started await updateStatsTask; diff --git a/Asterion/Services/Modrinth/SendDiscordNotificationJob.cs b/Asterion/Services/Modrinth/SendDiscordNotificationJob.cs new file mode 100644 index 0000000..534cdee --- /dev/null +++ b/Asterion/Services/Modrinth/SendDiscordNotificationJob.cs @@ -0,0 +1,93 @@ +using System.Text.Json; +using Asterion.Interfaces; +using Discord.WebSocket; +using Microsoft.Extensions.Logging; +using Modrinth; +using Modrinth.Models; +using Quartz; +using Version = Modrinth.Models.Version; + +namespace Asterion.Services.Modrinth; + +public class SendDiscordNotificationJob : IJob +{ + private readonly ILogger _logger; + private readonly IDataService _dataService; + private readonly DiscordSocketClient _client; + + private JobKey _jobKey; + + public SendDiscordNotificationJob(ILogger logger, IDataService dataService, DiscordSocketClient client) + { + _client = client; + _logger = logger; + _dataService = dataService; + } + + public async Task Execute(IJobExecutionContext context) + { + _jobKey = context.JobDetail.Key; + + // We currently store the data there as JSON, so we need to deserialize it + var projectJson = context.JobDetail.JobDataMap.GetString("project"); + var versionListJson = context.JobDetail.JobDataMap.GetString("versionList"); + + if (projectJson is null || versionListJson is null) + { + _logger.LogError("Project or version list was null"); + LogAbortJob(); + return; + } + + var project = JsonSerializer.Deserialize(projectJson); + var versionList = JsonSerializer.Deserialize(versionListJson); + + if (project is null || versionList is null) + { + _logger.LogError("Project or version list was null"); + LogAbortJob(); + return; + } + + _logger.LogInformation("Starting to send notifications for {ProjectName}", project.Title); + await SendNotifications(project, versionList); + } + + private void LogAbortJob() + { + _logger.LogWarning("Aborting job ID {JobId}", _jobKey); + } + + private async Task SendNotifications(Project project, Version[] projects) + { + var guilds = await _dataService.GetAllGuildsSubscribedToProject(project.Id); + + if (guilds.Count <= 0) + { + _logger.LogWarning("No guilds subscribed to {ProjectName}, aborting job ID {JobId}", project.Title, _jobKey.Name); + return; + } + + foreach (var guild in guilds) + { + var entry = await _dataService.GetModrinthEntryAsync(guild.GuildId, project.Id); + + if (entry is null) + { + _logger.LogWarning( + "No entry found for guild {GuildId} and project {ProjectId} (might have unsubscribed)", + guild.GuildId, project.Id); + continue; + } + + var channel = _client.GetGuild(guild.GuildId)?.GetTextChannel((ulong) entry.CustomUpdateChannel); + + if (channel is null) + { + _logger.LogWarning("Channel {ChannelId} not found in guild {GuildId}", entry.CustomUpdateChannel, guild.GuildId); + // TODO: Maybe notify the user that the channel was not found and remove the entry? + continue; + } + } + } +} \ No newline at end of file From b13c9b84d1956f6f82ed7c13c586eca26380d65a Mon Sep 17 00:00:00 2001 From: Zechiax <106590288+Zechiax@users.noreply.github.com> Date: Sat, 17 Jun 2023 00:08:24 +0200 Subject: [PATCH 16/30] maybe complete the discord and search job --- .../Services/Modrinth/SearchUpdatesJob.cs | 4 +-- .../Modrinth/SendDiscordNotificationJob.cs | 33 ++++++++++++++++++- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/Asterion/Services/Modrinth/SearchUpdatesJob.cs b/Asterion/Services/Modrinth/SearchUpdatesJob.cs index 24eea39..26eb0cc 100644 --- a/Asterion/Services/Modrinth/SearchUpdatesJob.cs +++ b/Asterion/Services/Modrinth/SearchUpdatesJob.cs @@ -20,9 +20,9 @@ public class SearchUpdatesJob : IJob private const int SplitSize = 100; - public SearchUpdatesJob(IScheduler scheduler, IModrinthClient client, IDataService dataService, ILogger logger, ProjectStatisticsManager projectStatisticsManager) + public SearchUpdatesJob(ISchedulerFactory scheduler, IModrinthClient client, IDataService dataService, ILogger logger, ProjectStatisticsManager projectStatisticsManager) { - _scheduler = scheduler; + _scheduler = scheduler.GetScheduler().GetAwaiter().GetResult(); _client = client; _dataService = dataService; _logger = logger; diff --git a/Asterion/Services/Modrinth/SendDiscordNotificationJob.cs b/Asterion/Services/Modrinth/SendDiscordNotificationJob.cs index 534cdee..9a7b2ae 100644 --- a/Asterion/Services/Modrinth/SendDiscordNotificationJob.cs +++ b/Asterion/Services/Modrinth/SendDiscordNotificationJob.cs @@ -1,5 +1,9 @@ using System.Text.Json; +using Asterion.ComponentBuilders; +using Asterion.Database.Models; +using Asterion.EmbedBuilders; using Asterion.Interfaces; +using Discord; using Discord.WebSocket; using Microsoft.Extensions.Logging; using Modrinth; @@ -14,11 +18,13 @@ public class SendDiscordNotificationJob : IJob private readonly ILogger _logger; private readonly IDataService _dataService; private readonly DiscordSocketClient _client; + private readonly IModrinthClient _modrinthClient; private JobKey _jobKey; - public SendDiscordNotificationJob(ILogger logger, IDataService dataService, DiscordSocketClient client) + public SendDiscordNotificationJob(ILogger logger, IDataService dataService, DiscordSocketClient client, IModrinthClient modrinthClient) { + _modrinthClient = modrinthClient; _client = client; _logger = logger; _dataService = dataService; @@ -68,6 +74,9 @@ private async Task SendNotifications(Project project, Version[] projects) return; } + // If the request fail, the job will be rescheduled so it should be fine to just throw here + var team = await _modrinthClient.Team.GetAsync(project.Team); + foreach (var guild in guilds) { var entry = await _dataService.GetModrinthEntryAsync(guild.GuildId, project.Id); @@ -88,6 +97,28 @@ private async Task SendNotifications(Project project, Version[] projects) // TODO: Maybe notify the user that the channel was not found and remove the entry? continue; } + + var pingRole = guild.PingRole is null ? null : channel.Guild.GetRole((ulong) guild.PingRole); + + foreach (var version in projects) + { + var embed = ModrinthEmbedBuilder.VersionUpdateEmbed(guild.GuildSettings, project, version, team).Build(); + var buttons = new ComponentBuilder().WithButton(ModrinthComponentBuilder.GetVersionUrlButton(project, version)).Build(); + + await SendUpdateEmbedToChannel(channel, pingRole?.Mention ?? string.Empty, embed, buttons); + } + } + } + + private async Task SendUpdateEmbedToChannel(ISocketMessageChannel channel, string message, Embed embed, MessageComponent buttons) + { + try + { + await channel.SendMessageAsync(message, embed: embed, components: buttons); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to send message to channel {ChannelId}", channel.Id); } } } \ No newline at end of file From 85def8225f502b3f0c095dc15cc29b0f1932e519 Mon Sep 17 00:00:00 2001 From: Zechiax <106590288+Zechiax@users.noreply.github.com> Date: Sat, 17 Jun 2023 11:45:43 +0200 Subject: [PATCH 17/30] set status every 15 minutes --- Asterion/Services/ClientService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Asterion/Services/ClientService.cs b/Asterion/Services/ClientService.cs index 3ba913e..ebffd6c 100644 --- a/Asterion/Services/ClientService.cs +++ b/Asterion/Services/ClientService.cs @@ -39,7 +39,7 @@ private async void ScheduleRefreshJob() .WithIdentity("RefreshTrigger", "group1") .StartNow() .WithSimpleSchedule(x => x - .WithIntervalInSeconds(10) // Every 15 minutes + .WithIntervalInSeconds(15 * 60) // Every 15 minutes .RepeatForever()) .Build(); From 4d17eb493ce61205e284e55bd84be60a23f87c6e Mon Sep 17 00:00:00 2001 From: Zechiax <106590288+Zechiax@users.noreply.github.com> Date: Sat, 17 Jun 2023 12:03:10 +0200 Subject: [PATCH 18/30] complete base for update checking using Quartz.NET --- Asterion/Modules/ModrinthModule.cs | 12 +- Asterion/Program.cs | 3 +- Asterion/Services/Modrinth/ModrinthService.cs | 267 ++---------------- .../Services/Modrinth/SearchUpdatesJob.cs | 24 +- .../Modrinth/SendDiscordNotificationJob.cs | 9 +- 5 files changed, 54 insertions(+), 261 deletions(-) diff --git a/Asterion/Modules/ModrinthModule.cs b/Asterion/Modules/ModrinthModule.cs index 1634201..2986e78 100644 --- a/Asterion/Modules/ModrinthModule.cs +++ b/Asterion/Modules/ModrinthModule.cs @@ -13,6 +13,7 @@ using Fergun.Interactive; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Modrinth; using Modrinth.Exceptions; using Modrinth.Models; @@ -31,9 +32,11 @@ public class ModrinthModule : AsterionInteractionModuleBase private readonly ILogger _logger; private readonly ModrinthService _modrinthService; private readonly ILocalizationService _localizationService; + private readonly IModrinthClient _modrinthClient; - public ModrinthModule(IServiceProvider serviceProvider, ILocalizationService localizationService) : base(localizationService) + public ModrinthModule(IModrinthClient modrinthClient, IServiceProvider serviceProvider, ILocalizationService localizationService) : base(localizationService) { + _modrinthClient = modrinthClient; _localizationService = serviceProvider.GetRequiredService(); _dataService = serviceProvider.GetRequiredService(); _modrinthService = serviceProvider.GetRequiredService(); @@ -364,14 +367,13 @@ await ModifyOriginalResponseAsync(x => public async Task GetRandomProject() { await DeferAsync(); - - var api = _modrinthService.Api; + Project? randomProject; try { // Count to 70, as Modrinth currently returns less than 70 projects // If it's set to 1, it will sometimes return 0 projects - randomProject = (await api.Project.GetRandomAsync(70)).FirstOrDefault(); + randomProject = (await _modrinthClient.Project.GetRandomAsync(70)).FirstOrDefault(); } catch (ModrinthApiException e) { @@ -381,7 +383,7 @@ public async Task GetRandomProject() if (randomProject == null) throw new Exception("No projects found"); - var team = await api.Team.GetAsync(randomProject.Team); + var team = await _modrinthClient.Team.GetAsync(randomProject.Team); var embed = ModrinthEmbedBuilder.GetProjectEmbed(randomProject, team); diff --git a/Asterion/Program.cs b/Asterion/Program.cs index a36febe..04435b2 100644 --- a/Asterion/Program.cs +++ b/Asterion/Program.cs @@ -21,8 +21,9 @@ private static void Main(string[] args) .MinimumLevel.Debug() #else .MinimumLevel.Information() - .MinimumLevel.Override("Microsoft.EntityFrameworkCore.Database.Command", Serilog.Events.LogEventLevel.Warning) #endif + .MinimumLevel.Override("Microsoft.EntityFrameworkCore", Serilog.Events.LogEventLevel.Information) // Set the minimum log level for EF Core messages + .MinimumLevel.Override("Microsoft.EntityFrameworkCore.Database.Command", Serilog.Events.LogEventLevel.Warning) // Set the minimum log level for EF Core command messages .Enrich.FromLogContext() .WriteTo.Console() .CreateLogger(); diff --git a/Asterion/Services/Modrinth/ModrinthService.cs b/Asterion/Services/Modrinth/ModrinthService.cs index d146504..89df388 100644 --- a/Asterion/Services/Modrinth/ModrinthService.cs +++ b/Asterion/Services/Modrinth/ModrinthService.cs @@ -1,12 +1,7 @@ using System.ComponentModel; using System.Net; -using System.Timers; -using Asterion.ComponentBuilders; -using Asterion.Database.Models; -using Asterion.EmbedBuilders; using Asterion.Extensions; using Asterion.Interfaces; -using Discord; using Discord.WebSocket; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; @@ -14,9 +9,7 @@ using Modrinth; using Modrinth.Exceptions; using Modrinth.Models; -using Array = System.Array; -using Timer = System.Timers.Timer; -using Version = Modrinth.Models.Version; +using Quartz; namespace Asterion.Services.Modrinth; @@ -30,8 +23,11 @@ public partial class ModrinthService private readonly ILogger _logger; private readonly ProjectStatisticsManager _projectStatisticsManager; private readonly BackgroundWorker _updateWorker; + private readonly IScheduler _scheduler; + private readonly JobKey _jobKey; + protected IModrinthClient Api { get; } - public ModrinthService(IServiceProvider serviceProvider, IHttpClientFactory httpClientFactory) + public ModrinthService(IServiceProvider serviceProvider, IHttpClientFactory httpClientFactory, ISchedulerFactory scheduler) { _httpClientFactory = httpClientFactory; Api = serviceProvider.GetRequiredService(); @@ -40,259 +36,44 @@ public ModrinthService(IServiceProvider serviceProvider, IHttpClientFactory http _dataService = serviceProvider.GetRequiredService(); _client = serviceProvider.GetRequiredService(); _projectStatisticsManager = serviceProvider.GetRequiredService(); + _scheduler = scheduler.GetScheduler().GetAwaiter().GetResult(); _cacheEntryOptions = new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15) }; - _updateWorker = new BackgroundWorker(); - _updateWorker.DoWork += CheckUpdates; - - - var checkTimer = new Timer(MinutesToMilliseconds(10)); - checkTimer.Elapsed += checkTimer_Elapsed; - checkTimer.Start(); + var job = JobBuilder.Create() + .WithIdentity("SearchUpdatesJob", "Modrinth") + .Build(); + + _jobKey = job.Key; + + // Every 10 minutes + var trigger = TriggerBuilder.Create() + .WithIdentity("SearchUpdatesTrigger", "Modrinth") + .StartAt(DateBuilder.FutureDate(10, IntervalUnit.Minute)) // Start 10 minutes from now + .WithSimpleSchedule(x => x + .WithIntervalInMinutes(10) + .RepeatForever()) + .Build(); + + _scheduler.ScheduleJob(job, trigger); _logger.LogInformation("Modrinth service initialized"); } - public IModrinthClient Api { get; } - - private static double MinutesToMilliseconds(int minutes) - { - return TimeSpan.FromMinutes(minutes).TotalMilliseconds; - } - - /// - /// Checks updates for every project stored in database, sends notification to every guild who has subscribed for - /// updates - /// - /// - /// - private async void CheckUpdates(object? sender, DoWorkEventArgs e) - { - try - { - _logger.LogInformation("Running update check"); - - var databaseProjects = await _dataService.GetAllModrinthProjectsAsync(); - - var projectIds = databaseProjects.Select(x => x.ProjectId); - _logger.LogDebug("Getting multiple projects ({Count}) from Modrinth", databaseProjects.Count); - var apiProjects = await GetMultipleProjects(projectIds); - - if (apiProjects is null) - { - _logger.LogWarning("Could not get information from API, update search interrupted"); - return; - } - - // Update downloads data in database - before we remove projects which are not updated - _logger.LogDebug("Updating downloads in database"); - foreach (var project in apiProjects) - { - await _projectStatisticsManager.UpdateDownloadsAsync(project); - } - - // Remove projects which are not updated (last update timestamp is less or equal to the last update timestamp in database) - apiProjects = apiProjects.Where(x => databaseProjects.Any(y => y.ProjectId == x.Id && y.LastUpdated < x.Updated)).ToArray(); - - _logger.LogDebug("Got {Count} projects", apiProjects.Length); - - var versions = apiProjects.SelectMany(p => p.Versions).ToArray(); - - - const int splitBy = 100; - _logger.LogDebug("Getting multiple versions ({Count}) from Modrinth", versions.Length); - - // Make multiple requests to get all versions - we don't want to get 1500+ versions in one request - // We make sure to split the requests into chunks of 500 versions - var apiVersions = new List(); - // Split the array into chunks of 500, we use ArraySegment - var versionChunks = Array.Empty>(); - - for (var i = 0; i < versions.Length; i += splitBy) - { - _logger.LogDebug("Appending versions {Start} to {End}", i, Math.Min(splitBy, versions.Length - i)); - versionChunks = versionChunks - .Append(new ArraySegment(versions, i, Math.Min(splitBy, versions.Length - i))).ToArray(); - } - - foreach (var chunk in versionChunks) - { - _logger.LogDebug("Getting versions {Start} to {End}", chunk.Offset, chunk.Offset + chunk.Count); - var versionsChunk = await GetMultipleVersionsAsync(chunk); - if (versionsChunk is null) - { - _logger.LogWarning("Could not get information from API, update search interrupted"); - return; - } - - apiVersions.AddRange(versionsChunk); - } - - _logger.LogDebug("Got {Count} versions", apiVersions.Count); - - foreach (var project in apiProjects) - { - _logger.LogDebug("Checking new versions for project {Title} ID {ProjectId}", project.Title, project.Id); - var versionList = apiVersions.Where(x => x.ProjectId == project.Id).ToList(); - - var newVersions = await GetNewVersions(versionList, project.Id); - - if (newVersions is null) - { - _logger.LogWarning("There was an error while finding new versions for project ID {ID}, skipping...", - project.Id); - continue; - } - - if (newVersions.Length == 0) - { - _logger.LogDebug("No new versions for project {Title} ID {ID}", project.Title, project.Id); - continue; - } - - _logger.LogInformation("Found {Count} new versions for project {Title} ID {ID}", newVersions.Length, - project.Title, project.Id); - - // Update data in database - _logger.LogDebug("Updating data in database"); - await _dataService.UpdateModrinthProjectAsync(project.Id, newVersions[0].Id, project.Title, - project.Updated); - - var team = await GetProjectsTeamMembersAsync(project.Id); - - var guilds = await _dataService.GetAllGuildsSubscribedToProject(project.Id); - - await CheckGuilds(newVersions, project, guilds, team); - } - - _logger.LogInformation("Update check ended"); - } - catch (Exception exception) - { - _logger.LogCritical("Exception while checking for updates: {Exception} \n\nStackTrace: {StackTrace}", - exception.Message, exception.StackTrace); - } - } - - private async Task GetNewVersions(IEnumerable versionList, string projectId) - { - var dbProject = await _dataService.GetModrinthProjectByIdAsync(projectId); - - if (dbProject is null) - { - _logger.LogWarning("Project ID {ID} not found in database", projectId); - return null; - } - - // Ensures the data is chronologically ordered - var orderedVersions = versionList.OrderByDescending(x => x.DatePublished); - - // Take new versions from the latest to the one we already checked - var newVersions = orderedVersions.TakeWhile(version => - version.Id != dbProject.LastCheckVersion && version.DatePublished > dbProject.LastUpdated).ToArray(); - - return newVersions; - } - - /// - /// Will load guild's channel from custom field of entries in database and send updates - /// - /// - /// - /// - /// - private async Task CheckGuilds(Version[] versions, Project project, IEnumerable guilds, - TeamMember[]? teamMembers = null) - { - foreach (var guild in guilds) - { - var entry = await _dataService.GetModrinthEntryAsync(guild.GuildId, project.Id); - - // Channel is not set, skip sending updates to this guild - if (entry!.CustomUpdateChannel is null) - { - _logger.LogInformation( - "Guild ID {GuildID} has not yet set default update channel or custom channel for this project", - guild.GuildId); - continue; - } - - var channel = _client.GetGuild(guild.GuildId).GetTextChannel((ulong) entry.CustomUpdateChannel); - - _logger.LogInformation("Sending updates to guild ID {Id} and channel ID {Channel}", guild.GuildId, - channel.Id); - - // None of these can be null, everything is checked beforehand - await SendUpdatesToChannel(channel, project, versions, teamMembers, guild.GuildSettings); - } - } - - /// - /// Sends update information about every new version to specified Text Channel - /// - /// - /// - /// - /// - /// - private async Task SendUpdatesToChannel(SocketTextChannel textChannel, Project currentProject, - IEnumerable newVersions, TeamMember[]? team, GuildSettings guildSettings) - { - // Iterate versions - they are ordered from latest to oldest, we want to sent them chronologically - foreach (var version in newVersions.Reverse()) - { - var embed = ModrinthEmbedBuilder.VersionUpdateEmbed(guildSettings, currentProject, version, team); - var buttons = - new ComponentBuilder().WithButton( - ModrinthComponentBuilder.GetVersionUrlButton(currentProject, version)); - try - { - var pingRoleId = await _dataService.GetPingRoleIdAsync(textChannel.Guild.Id); - - SocketRole? pingRole = null; - if (pingRoleId is not null) pingRole = textChannel.Guild.GetRole((ulong) pingRoleId); - - await textChannel.SendMessageAsync(pingRole?.Mention, embed: embed.Build(), - components: buttons.Build()); - } - catch (Exception ex) - { - _logger.LogCritical("Error while sending message to guild {Guild}: {Exception}", textChannel.Guild.Id, - ex.Message); - } - } - } - /// /// Force check for updates, used for debugging /// /// False if worker is busy public bool ForceUpdate() { - if (_updateWorker.IsBusy) return false; - - _updateWorker.RunWorkerAsync(); - + _scheduler.TriggerJob(_jobKey); + return true; } - - private void checkTimer_Elapsed(object? sender, ElapsedEventArgs e) - { - try - { - if (!_updateWorker.IsBusy) _updateWorker.RunWorkerAsync(); - } - catch (Exception ex) - { - _logger.LogCritical("Error while checking for updates: {Exception}", ex.Message); - } - } - /// /// Tries to find project on Modrinth /// diff --git a/Asterion/Services/Modrinth/SearchUpdatesJob.cs b/Asterion/Services/Modrinth/SearchUpdatesJob.cs index 26eb0cc..f468527 100644 --- a/Asterion/Services/Modrinth/SearchUpdatesJob.cs +++ b/Asterion/Services/Modrinth/SearchUpdatesJob.cs @@ -53,9 +53,9 @@ private async Task DoExecutionWork() var projects = await _client.Project.GetMultipleAsync(projectIds); - // We let the statistics update run in the background, as it's not critical to the job + // We let the statistics update var copy = projects.ToList(); - var updateStatsTask = UpdateStatisticsData(copy); + await UpdateStatisticsData(copy); var updatedProjects = FilterUpdatedProjects(projectsDto, projects).ToArray(); var versionsIds = updatedProjects.SelectMany(p => p.Versions).ToArray(); @@ -75,13 +75,24 @@ private async Task DoExecutionWork() // This will modify the projectVersions dictionary in-place and only keep the projects and versions that have updates await CheckForUpdatesAsync(projectVersions); + if (projectVersions.Count > 0) + { + _logger.LogInformation("Found {Count} projects with updates", projectVersions.Count); + } + else + { + _logger.LogInformation("No projects with updates found"); + } + // For each project, we'll create a Discord Notification job and pass it the information it needs foreach (var (project, versions) in projectVersions) { + _logger.LogInformation("Scheduling Discord notification for project {ProjectId} with {NewVersionsCount} new versions", project.Id, versions.Count); + var job = JobBuilder.Create() //.WithIdentity($"discord-notification-{project.Id}", "modrinth") - .UsingJobData("Project", JsonSerializer.Serialize(project)) - .UsingJobData("Versions", JsonSerializer.Serialize(versions)) + .UsingJobData("project", JsonSerializer.Serialize(project)) + .UsingJobData("versions", JsonSerializer.Serialize(versions.ToArray())) .Build(); var trigger = TriggerBuilder.Create() @@ -91,9 +102,6 @@ private async Task DoExecutionWork() await _scheduler.ScheduleJob(job, trigger); } - - // Let's await all the background tasks we started - await updateStatsTask; } private async Task CheckForUpdatesAsync(Dictionary> projectVersions) @@ -132,7 +140,7 @@ private async Task CheckForUpdatesAsync(Dictionary> proj // Remove versions from the list that are the same or newer than the latest version // This modifies the list associated with the project in the dictionary - var keepVersions = versions.Where(v => v.DatePublished >= latestVersion.DatePublished).ToList(); + var keepVersions = versions.Where(v => v.DatePublished <= latestVersion.DatePublished && v.DatePublished > dbProject.LastUpdated).ToList(); projectVersions[project] = keepVersions; } diff --git a/Asterion/Services/Modrinth/SendDiscordNotificationJob.cs b/Asterion/Services/Modrinth/SendDiscordNotificationJob.cs index 9a7b2ae..054ea9c 100644 --- a/Asterion/Services/Modrinth/SendDiscordNotificationJob.cs +++ b/Asterion/Services/Modrinth/SendDiscordNotificationJob.cs @@ -36,7 +36,7 @@ public async Task Execute(IJobExecutionContext context) // We currently store the data there as JSON, so we need to deserialize it var projectJson = context.JobDetail.JobDataMap.GetString("project"); - var versionListJson = context.JobDetail.JobDataMap.GetString("versionList"); + var versionListJson = context.JobDetail.JobDataMap.GetString("versions"); if (projectJson is null || versionListJson is null) { @@ -55,7 +55,7 @@ public async Task Execute(IJobExecutionContext context) return; } - _logger.LogInformation("Starting to send notifications for {ProjectName}", project.Title); + _logger.LogInformation("Starting to send notifications for {ProjectName} ({ProjectID}) with {NewVersionsCount} new versions", project.Title, project.Id, versionList.Length); await SendNotifications(project, versionList); } @@ -64,7 +64,7 @@ private void LogAbortJob() _logger.LogWarning("Aborting job ID {JobId}", _jobKey); } - private async Task SendNotifications(Project project, Version[] projects) + private async Task SendNotifications(Project project, Version[] versions) { var guilds = await _dataService.GetAllGuildsSubscribedToProject(project.Id); @@ -100,8 +100,9 @@ private async Task SendNotifications(Project project, Version[] projects) var pingRole = guild.PingRole is null ? null : channel.Guild.GetRole((ulong) guild.PingRole); - foreach (var version in projects) + foreach (var version in versions) { + _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(); From ec10c95ac172e72b0047b8ba1f245b4b7906ae8f Mon Sep 17 00:00:00 2001 From: Zechiax <106590288+Zechiax@users.noreply.github.com> Date: Wed, 21 Jun 2023 11:47:13 +0200 Subject: [PATCH 19/30] updates --- Asterion.Test/Asterion.Test.csproj | 4 ++-- Asterion/Asterion.cs | 7 ++++--- Asterion/Asterion.csproj | 12 ++++++------ Asterion/Services/Modrinth/SearchUpdatesJob.cs | 3 +-- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Asterion.Test/Asterion.Test.csproj b/Asterion.Test/Asterion.Test.csproj index d265ac3..8389d17 100644 --- a/Asterion.Test/Asterion.Test.csproj +++ b/Asterion.Test/Asterion.Test.csproj @@ -15,10 +15,10 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Asterion/Asterion.cs b/Asterion/Asterion.cs index 9807c12..aba6953 100644 --- a/Asterion/Asterion.cs +++ b/Asterion/Asterion.cs @@ -95,15 +95,16 @@ public async Task MainAsync() Console.CancelKeyPress += (_, args) => { args.Cancel = true; + logger.LogInformation("{Key} pressed, exiting bot", args.SpecialKey); + logger.LogInformation("Stopping the scheduler"); + scheduler.Shutdown(true).Wait(); + logger.LogInformation("Logging out from Discord"); client.LogoutAsync().Wait(); logger.LogInformation("Stopping the client"); client.StopAsync().Wait(); - - logger.LogInformation("Stopping the scheduler"); - scheduler.Shutdown().Wait(); logger.LogInformation("Disposing services"); services.DisposeAsync().GetAwaiter().GetResult(); diff --git a/Asterion/Asterion.csproj b/Asterion/Asterion.csproj index bf5c248..a216184 100644 --- a/Asterion/Asterion.csproj +++ b/Asterion/Asterion.csproj @@ -15,15 +15,15 @@ - + - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -41,7 +41,7 @@ - + diff --git a/Asterion/Services/Modrinth/SearchUpdatesJob.cs b/Asterion/Services/Modrinth/SearchUpdatesJob.cs index f468527..0c70018 100644 --- a/Asterion/Services/Modrinth/SearchUpdatesJob.cs +++ b/Asterion/Services/Modrinth/SearchUpdatesJob.cs @@ -71,7 +71,7 @@ private async Task DoExecutionWork() .ToList(); projectVersions.Add(project, projectVersionsList); } - + // This will modify the projectVersions dictionary in-place and only keep the projects and versions that have updates await CheckForUpdatesAsync(projectVersions); @@ -88,7 +88,6 @@ private async Task DoExecutionWork() foreach (var (project, versions) in projectVersions) { _logger.LogInformation("Scheduling Discord notification for project {ProjectId} with {NewVersionsCount} new versions", project.Id, versions.Count); - var job = JobBuilder.Create() //.WithIdentity($"discord-notification-{project.Id}", "modrinth") .UsingJobData("project", JsonSerializer.Serialize(project)) From e8f01d9a375e0b121d85428fab491299d4c48122 Mon Sep 17 00:00:00 2001 From: Zechiax <106590288+Zechiax@users.noreply.github.com> Date: Wed, 26 Jul 2023 16:15:38 +0200 Subject: [PATCH 20/30] upgrade packages --- Asterion.Test/Asterion.Test.csproj | 4 ++-- Asterion/Asterion.csproj | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Asterion.Test/Asterion.Test.csproj b/Asterion.Test/Asterion.Test.csproj index 8389d17..d132d78 100644 --- a/Asterion.Test/Asterion.Test.csproj +++ b/Asterion.Test/Asterion.Test.csproj @@ -15,8 +15,8 @@ - - + + diff --git a/Asterion/Asterion.csproj b/Asterion/Asterion.csproj index a216184..7825543 100644 --- a/Asterion/Asterion.csproj +++ b/Asterion/Asterion.csproj @@ -13,17 +13,17 @@ - - + + - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -32,11 +32,11 @@ - - - - - + + + + + From 8e18bebf80d2c4edd7b041363634983347c8a9cd Mon Sep 17 00:00:00 2001 From: Zechiax <106590288+Zechiax@users.noreply.github.com> Date: Mon, 21 Aug 2023 18:50:38 +0200 Subject: [PATCH 21/30] add common embed builder --- .../Common/AsterionInteractionModuleBase.cs | 8 +--- Asterion/EmbedBuilders/CommonEmbedBuilder.cs | 48 +++++++++++++++++++ 2 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 Asterion/EmbedBuilders/CommonEmbedBuilder.cs diff --git a/Asterion/Common/AsterionInteractionModuleBase.cs b/Asterion/Common/AsterionInteractionModuleBase.cs index 7fa6d26..77ff347 100644 --- a/Asterion/Common/AsterionInteractionModuleBase.cs +++ b/Asterion/Common/AsterionInteractionModuleBase.cs @@ -1,4 +1,5 @@ using System.Globalization; +using Asterion.EmbedBuilders; using Asterion.Interfaces; using Discord; using Discord.Interactions; @@ -42,12 +43,7 @@ protected async Task FollowupWithSearchResultErrorAsync(Services.Modrinth.Sea throw new ArgumentOutOfRangeException(); } - var embed = new EmbedBuilder() - .WithTitle(title) - .WithDescription(description) - .WithColor(Color.Red) - .WithCurrentTimestamp() - .Build(); + var embed = CommonEmbedBuilder.GetErrorEmbedBuilder(title, description).Build(); await FollowupAsync(embeds: new[] {embed}); } diff --git a/Asterion/EmbedBuilders/CommonEmbedBuilder.cs b/Asterion/EmbedBuilders/CommonEmbedBuilder.cs new file mode 100644 index 0000000..2ac44f8 --- /dev/null +++ b/Asterion/EmbedBuilders/CommonEmbedBuilder.cs @@ -0,0 +1,48 @@ +using Discord; +using Color = Discord.Color; + +namespace Asterion.EmbedBuilders; + +public static class CommonEmbedBuilder +{ + private static Color _errorColor = Color.Red; + private static Color _warningColor = Color.Orange; + private static Color _infoColor = Color.Blue; + private static Color _successColor = Color.Green; + + public static EmbedBuilder GetSuccessEmbedBuilder(string title, string description) + { + return new EmbedBuilder() + .WithTitle(title) + .WithDescription(description) + .WithColor(_successColor) + .WithCurrentTimestamp(); + } + + public static EmbedBuilder GetErrorEmbedBuilder(string title, string description) + { + return new EmbedBuilder() + .WithTitle(title) + .WithDescription(description) + .WithColor(_errorColor) + .WithCurrentTimestamp(); + } + + public static EmbedBuilder GetInfoEmbedBuilder(string title, string description) + { + return new EmbedBuilder() + .WithTitle(title) + .WithDescription(description) + .WithColor(_infoColor) + .WithCurrentTimestamp(); + } + + public static EmbedBuilder GetWarningEmbedBuilder(string title, string description) + { + return new EmbedBuilder() + .WithTitle(title) + .WithDescription(description) + .WithColor(_warningColor) + .WithCurrentTimestamp(); + } +} \ No newline at end of file From a5196de002ddca130be3b638e63dfbb4ec909fa9 Mon Sep 17 00:00:00 2001 From: Zechiax <106590288+Zechiax@users.noreply.github.com> Date: Tue, 10 Oct 2023 20:01:05 +0200 Subject: [PATCH 22/30] update packages --- Asterion.Test/Asterion.Test.csproj | 6 +++--- Asterion/Asterion.csproj | 26 +++++++++++++------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Asterion.Test/Asterion.Test.csproj b/Asterion.Test/Asterion.Test.csproj index d132d78..4d87100 100644 --- a/Asterion.Test/Asterion.Test.csproj +++ b/Asterion.Test/Asterion.Test.csproj @@ -15,11 +15,11 @@ - - + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Asterion/Asterion.csproj b/Asterion/Asterion.csproj index 7825543..dfff130 100644 --- a/Asterion/Asterion.csproj +++ b/Asterion/Asterion.csproj @@ -13,17 +13,17 @@ - - - + + + - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -32,15 +32,15 @@ - - - - + + + + - - + + From afdda45cef2d14e61df03c8d73d9bf9ecd99cfe5 Mon Sep 17 00:00:00 2001 From: Zechiax <106590288+Zechiax@users.noreply.github.com> Date: Tue, 10 Oct 2023 20:03:01 +0200 Subject: [PATCH 23/30] small changes --- Asterion/Services/BotStatsService.cs | 14 ++++++-------- Asterion/Services/ClientService.cs | 3 +-- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/Asterion/Services/BotStatsService.cs b/Asterion/Services/BotStatsService.cs index 1eef8fd..e3f554a 100644 --- a/Asterion/Services/BotStatsService.cs +++ b/Asterion/Services/BotStatsService.cs @@ -44,15 +44,13 @@ public async Task PublishToTopGg() _logger.LogInformation("Publishing bot stats to top.gg, server count: {ServerCount}", _discordClient.Guilds.Count); using var request = new HttpRequestMessage(HttpMethod.Post, - $"https://top.gg/api/bots/{_discordClient.CurrentUser.Id}/stats") + $"https://top.gg/api/bots/{_discordClient.CurrentUser.Id}/stats"); + request.Content = new StringContent(JsonConvert.SerializeObject(new { - Content = new StringContent(JsonConvert.SerializeObject(new - { - server_count = _discordClient.Guilds.Count, - // shard_count = 1, - shards = Array.Empty() - }), Encoding.UTF8, "application/json") - }; + server_count = _discordClient.Guilds.Count, + // shard_count = 1, + shards = Array.Empty() + }), Encoding.UTF8, "application/json"); request.Headers.Add("Authorization", _topGgToken); diff --git a/Asterion/Services/ClientService.cs b/Asterion/Services/ClientService.cs index ebffd6c..20cd4db 100644 --- a/Asterion/Services/ClientService.cs +++ b/Asterion/Services/ClientService.cs @@ -53,8 +53,7 @@ public async Task SetGameAsync() await _client.SetGameAsync( $"Monitoring {count} project{(count == 1 ? null : 's')} for updates in {_client.Guilds.Count} servers"); } - - // Define your Quartz job class as an inner class of ClientService + private class RefreshJob : IJob { private readonly ClientService _clientService; From ce48acb7b65e17d7b1f4352c0961caeb51be5de7 Mon Sep 17 00:00:00 2001 From: Zechiax <106590288+Zechiax@users.noreply.github.com> Date: Tue, 10 Oct 2023 20:15:47 +0200 Subject: [PATCH 24/30] update packages --- Asterion.Test/Asterion.Test.csproj | 2 +- Asterion/Asterion.csproj | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Asterion.Test/Asterion.Test.csproj b/Asterion.Test/Asterion.Test.csproj index 4d87100..73f0a5f 100644 --- a/Asterion.Test/Asterion.Test.csproj +++ b/Asterion.Test/Asterion.Test.csproj @@ -16,7 +16,7 @@ - + diff --git a/Asterion/Asterion.csproj b/Asterion/Asterion.csproj index dfff130..f6c2740 100644 --- a/Asterion/Asterion.csproj +++ b/Asterion/Asterion.csproj @@ -19,7 +19,7 @@ - + @@ -32,7 +32,7 @@ - + From 2ee7374d5db85379494494f400e462b1f4a6841f Mon Sep 17 00:00:00 2001 From: Zechiax <106590288+Zechiax@users.noreply.github.com> Date: Tue, 10 Oct 2023 20:30:44 +0200 Subject: [PATCH 25/30] cleanup --- .../Services/Modrinth/ModrinthService.Api.cs | 14 +++++------ Asterion/Services/Modrinth/ModrinthService.cs | 23 +++++++------------ 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/Asterion/Services/Modrinth/ModrinthService.Api.cs b/Asterion/Services/Modrinth/ModrinthService.Api.cs index 7b3ca45..1ac613b 100644 --- a/Asterion/Services/Modrinth/ModrinthService.Api.cs +++ b/Asterion/Services/Modrinth/ModrinthService.Api.cs @@ -27,7 +27,7 @@ public partial class ModrinthService Project? p; try { - p = await Api.Project.GetAsync(slugOrId); + p = await _api.Project.GetAsync(slugOrId); } catch (Exception e) { @@ -45,7 +45,7 @@ public partial class ModrinthService { try { - var searchResponse = await Api.Version.GetProjectVersionListAsync(slugOrId); + var searchResponse = await _api.Version.GetProjectVersionListAsync(slugOrId); return searchResponse; } catch (Exception e) @@ -60,7 +60,7 @@ public partial class ModrinthService { try { - var searchResponse = await Api.Version.GetMultipleAsync(versions); + var searchResponse = await _api.Version.GetMultipleAsync(versions); return searchResponse; } catch (ModrinthApiException e) @@ -86,7 +86,7 @@ public partial class ModrinthService try { _logger.LogDebug("Team members for project ID {ProjectId} are not in cache", projectId); - var team = await Api.Team.GetProjectTeamAsync(projectId); + var team = await _api.Team.GetProjectTeamAsync(projectId); _cache.Set($"project-team-members:{projectId}", team, TimeSpan.FromMinutes(30)); _logger.LogDebug("Saving team members for project ID {ProjectId} to cache", projectId); @@ -104,7 +104,7 @@ public partial class ModrinthService { try { - var searchResponse = await Api.Project.SearchAsync(query); + var searchResponse = await _api.Project.SearchAsync(query); return searchResponse; } catch (Exception e) @@ -119,7 +119,7 @@ public partial class ModrinthService { try { - var searchResponse = await Api.Project.GetMultipleAsync(projectIds); + var searchResponse = await _api.Project.GetMultipleAsync(projectIds); return searchResponse; } catch (Exception e) @@ -156,7 +156,7 @@ public partial class ModrinthService try { _logger.LogDebug("Game versions not in cache, fetching from api..."); - var gameVersions = await Api.Tag.GetGameVersionsAsync(); + var gameVersions = await _api.Tag.GetGameVersionsAsync(); _cache.Set("gameVersions", gameVersions, TimeSpan.FromHours(12)); diff --git a/Asterion/Services/Modrinth/ModrinthService.cs b/Asterion/Services/Modrinth/ModrinthService.cs index 89df388..2bfdbb9 100644 --- a/Asterion/Services/Modrinth/ModrinthService.cs +++ b/Asterion/Services/Modrinth/ModrinthService.cs @@ -17,25 +17,18 @@ public partial class ModrinthService { private readonly IMemoryCache _cache; private readonly MemoryCacheEntryOptions _cacheEntryOptions; - private readonly DiscordSocketClient _client; - private readonly IDataService _dataService; private readonly IHttpClientFactory _httpClientFactory; private readonly ILogger _logger; - private readonly ProjectStatisticsManager _projectStatisticsManager; - private readonly BackgroundWorker _updateWorker; private readonly IScheduler _scheduler; private readonly JobKey _jobKey; - protected IModrinthClient Api { get; } + private readonly IModrinthClient _api; public ModrinthService(IServiceProvider serviceProvider, IHttpClientFactory httpClientFactory, ISchedulerFactory scheduler) { _httpClientFactory = httpClientFactory; - Api = serviceProvider.GetRequiredService(); + _api = serviceProvider.GetRequiredService(); _logger = serviceProvider.GetRequiredService>(); _cache = serviceProvider.GetRequiredService(); - _dataService = serviceProvider.GetRequiredService(); - _client = serviceProvider.GetRequiredService(); - _projectStatisticsManager = serviceProvider.GetRequiredService(); _scheduler = scheduler.GetScheduler().GetAwaiter().GetResult(); _cacheEntryOptions = new MemoryCacheEntryOptions @@ -99,9 +92,9 @@ public async Task> FindProject(string query) var projectFoundById = false; try { - project = await Api.Project.GetAsync(query); + project = await _api.Project.GetAsync(query); - searchResponse = await Api.Project.SearchAsync(query); + searchResponse = await _api.Project.SearchAsync(query); // Won't be set if exception is thrown projectFoundById = true; } @@ -138,14 +131,14 @@ public async Task> FindProject(string query) try { - searchResponse = await Api.Project.SearchAsync(query); + searchResponse = await _api.Project.SearchAsync(query); // No search results if (searchResponse.TotalHits <= 0) return new SearchResult(new ProjectDto(), SearchStatus.NoResult, query); // Return first result - project = await Api.Project.GetAsync(searchResponse.Hits[0].ProjectId); + project = await _api.Project.GetAsync(searchResponse.Hits[0].ProjectId); var result = new SearchResult(new ProjectDto { @@ -186,7 +179,7 @@ public async Task> FindUser(string query) try { - user = await Api.User.GetAsync(query); + user = await _api.User.GetAsync(query); _logger.LogDebug("User query '{Query}' found", query); } catch (ModrinthApiException e) when (e.Response.StatusCode == HttpStatusCode.NotFound) @@ -203,7 +196,7 @@ public async Task> FindUser(string query) // User can't be null from here try { - var projects = await Api.User.GetProjectsAsync(user.Id); + var projects = await _api.User.GetProjectsAsync(user.Id); var searchResult = new SearchResult(new UserDto { From 00ce73a66a0a4436dcded23995a5f29e5140808b Mon Sep 17 00:00:00 2001 From: Zechiax <106590288+Zechiax@users.noreply.github.com> Date: Tue, 10 Oct 2023 20:42:53 +0200 Subject: [PATCH 26/30] fix #100 --- Asterion/EmbedBuilders/ListEmbedBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Asterion/EmbedBuilders/ListEmbedBuilder.cs b/Asterion/EmbedBuilders/ListEmbedBuilder.cs index 11c620b..e4a0b5b 100644 --- a/Asterion/EmbedBuilders/ListEmbedBuilder.cs +++ b/Asterion/EmbedBuilders/ListEmbedBuilder.cs @@ -16,7 +16,7 @@ public static List CreateListEmbed(IList entries) var currentEntry = 0; - var numberOfEmbeds = entries.Count / 25 + 1; + var numberOfEmbeds = entries.Count / 25 + (entries.Count % 25 > 0 ? 1 : 0); for (var i = 0; i < numberOfEmbeds; i++) { From 7e9095d69d630e7f2682205ed0f7a6b83c1a1ecb Mon Sep 17 00:00:00 2001 From: Zechiax <106590288+Zechiax@users.noreply.github.com> Date: Sun, 12 Nov 2023 11:06:10 +0100 Subject: [PATCH 27/30] fix #104 --- Asterion/Modules/ModrinthInteractionModule.cs | 4 ++-- Asterion/Modules/ModrinthModule.cs | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Asterion/Modules/ModrinthInteractionModule.cs b/Asterion/Modules/ModrinthInteractionModule.cs index 393e109..a2e955b 100644 --- a/Asterion/Modules/ModrinthInteractionModule.cs +++ b/Asterion/Modules/ModrinthInteractionModule.cs @@ -185,8 +185,6 @@ public async Task UnsubProject(string userId, string projectId) var guildId = Context.Guild.Id; - var subscribed = await _dataService.IsGuildSubscribedToProjectAsync(guildId, projectId); - var project = await _modrinthService.GetProject(projectId); var guild = await _dataService.GetGuildByIdAsync(guildId); @@ -203,6 +201,8 @@ public async Task UnsubProject(string userId, string projectId) await FollowupAsync(RequestError, ephemeral: true); return; } + + var subscribed = await _dataService.IsGuildSubscribedToProjectAsync(guildId, project.Id); var team = await _modrinthService.GetProjectsTeamMembersAsync(project.Id); diff --git a/Asterion/Modules/ModrinthModule.cs b/Asterion/Modules/ModrinthModule.cs index 2986e78..d7277eb 100644 --- a/Asterion/Modules/ModrinthModule.cs +++ b/Asterion/Modules/ModrinthModule.cs @@ -167,6 +167,17 @@ await ModifyOriginalResponseAsync(x => }); return; } + + var subscribed = await _dataService.IsGuildSubscribedToProjectAsync(Context.Guild.Id, project.Id); + + if (subscribed) + { + await ModifyOriginalResponseAsync(x => + { + x.Content = $"You're already subscribed to project ID {project.Id}"; + }); + return; + } var versions = await _modrinthService.GetVersionListAsync(project.Id); if (versions == null) From 5eab99b6e224b330f818a0815c8a1d3f3fe30d45 Mon Sep 17 00:00:00 2001 From: Zechiax <106590288+Zechiax@users.noreply.github.com> Date: Sun, 24 Dec 2023 12:34:28 +0100 Subject: [PATCH 28/30] upgrade to .net 8 --- Asterion.Test/Asterion.Test.csproj | 12 ++--- Asterion/Asterion.cs | 1 - Asterion/Asterion.csproj | 44 +++++++++---------- Asterion/Extensions/Rgb24Extensions.cs | 3 +- Asterion/Modules/ModrinthModule.cs | 9 +++- Asterion/Services/ImageService.cs | 5 ++- .../Modrinth/SendDiscordNotificationJob.cs | 6 +-- 7 files changed, 45 insertions(+), 35 deletions(-) diff --git a/Asterion.Test/Asterion.Test.csproj b/Asterion.Test/Asterion.Test.csproj index 73f0a5f..90e89b2 100644 --- a/Asterion.Test/Asterion.Test.csproj +++ b/Asterion.Test/Asterion.Test.csproj @@ -1,13 +1,13 @@ - net7.0 + net8.0 enable enable false - 11 + 12 Asterion.Test @@ -15,11 +15,11 @@ - - - + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Asterion/Asterion.cs b/Asterion/Asterion.cs index aba6953..325d570 100644 --- a/Asterion/Asterion.cs +++ b/Asterion/Asterion.cs @@ -160,7 +160,6 @@ private ServiceProvider ConfigureServices() services.AddQuartz(q => { q.UseInMemoryStore(); - q.UseMicrosoftDependencyInjectionJobFactory(); }); services.AddQuartzHostedService(options => { diff --git a/Asterion/Asterion.csproj b/Asterion/Asterion.csproj index f6c2740..81b027d 100644 --- a/Asterion/Asterion.csproj +++ b/Asterion/Asterion.csproj @@ -2,44 +2,44 @@ Exe - net7.0 + net8.0 enable enable Linux false - 11 + 12 2.0.1 - - + + - + - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/Asterion/Extensions/Rgb24Extensions.cs b/Asterion/Extensions/Rgb24Extensions.cs index e62115f..b8d1a9d 100644 --- a/Asterion/Extensions/Rgb24Extensions.cs +++ b/Asterion/Extensions/Rgb24Extensions.cs @@ -1,4 +1,5 @@ -using Color = Discord.Color; +using SixLabors.ImageSharp.PixelFormats; +using Color = Discord.Color; namespace Asterion.Extensions; diff --git a/Asterion/Modules/ModrinthModule.cs b/Asterion/Modules/ModrinthModule.cs index d7277eb..6b12ea4 100644 --- a/Asterion/Modules/ModrinthModule.cs +++ b/Asterion/Modules/ModrinthModule.cs @@ -361,7 +361,14 @@ public async Task LatestRelease(string slugOrId, MessageStyle style = MessageSty var guild = await _dataService.GetGuildByIdAsync(Context.Guild.Id); - var embed = ModrinthEmbedBuilder.VersionUpdateEmbed(guild?.GuildSettings, project, latestVersion, team); + if (guild is null) + { + // Try again later + await ModifyOriginalResponseAsync(x => { x.Content = "Internal error, please try again later"; }); + return; + } + + var embed = ModrinthEmbedBuilder.VersionUpdateEmbed(guild.GuildSettings, project, latestVersion, team); var buttons = new ComponentBuilder().WithButton( diff --git a/Asterion/Services/ImageService.cs b/Asterion/Services/ImageService.cs index 5e60371..f73a0da 100644 --- a/Asterion/Services/ImageService.cs +++ b/Asterion/Services/ImageService.cs @@ -1,4 +1,7 @@ -using SixLabors.ImageSharp.Processing.Processors.Quantization; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace Asterion.Services; diff --git a/Asterion/Services/Modrinth/SendDiscordNotificationJob.cs b/Asterion/Services/Modrinth/SendDiscordNotificationJob.cs index 054ea9c..1f21314 100644 --- a/Asterion/Services/Modrinth/SendDiscordNotificationJob.cs +++ b/Asterion/Services/Modrinth/SendDiscordNotificationJob.cs @@ -20,7 +20,7 @@ public class SendDiscordNotificationJob : IJob private readonly DiscordSocketClient _client; private readonly IModrinthClient _modrinthClient; - private JobKey _jobKey; + private JobKey _jobKey = null!; public SendDiscordNotificationJob(ILogger logger, IDataService dataService, DiscordSocketClient client, IModrinthClient modrinthClient) { @@ -89,7 +89,7 @@ private async Task SendNotifications(Project project, Version[] versions) continue; } - var channel = _client.GetGuild(guild.GuildId)?.GetTextChannel((ulong) entry.CustomUpdateChannel); + var channel = _client.GetGuild(guild.GuildId)?.GetTextChannel((ulong) entry.CustomUpdateChannel!); if (channel is null) { @@ -100,7 +100,7 @@ private async Task SendNotifications(Project project, Version[] versions) var pingRole = guild.PingRole is null ? null : channel.Guild.GetRole((ulong) guild.PingRole); - foreach (var version in versions) + foreach (var version in versions.OrderBy(x => x.DatePublished)) { _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(); From ceb243b4112fe008354b50d281f096bf6fcb757d Mon Sep 17 00:00:00 2001 From: Zechiax <106590288+Zechiax@users.noreply.github.com> Date: Sun, 24 Dec 2023 12:34:52 +0100 Subject: [PATCH 29/30] upgrade livecharts --- Asterion/Asterion.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Asterion/Asterion.csproj b/Asterion/Asterion.csproj index 81b027d..6856b9d 100644 --- a/Asterion/Asterion.csproj +++ b/Asterion/Asterion.csproj @@ -19,7 +19,7 @@ - + From 0df47bde58e2d361cebd6c6a21734e63683c8027 Mon Sep 17 00:00:00 2001 From: Zechiax <106590288+Zechiax@users.noreply.github.com> Date: Sun, 24 Dec 2023 12:35:22 +0100 Subject: [PATCH 30/30] bump dotnet version in dotnet.yml --- .github/workflows/dotnet.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 4bab0ed..b4a83fe 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -16,7 +16,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v2 with: - dotnet-version: 7.0.x + dotnet-version: 8.0.x - name: Restore dependencies run: dotnet restore - name: Build