diff --git a/ASFFreeGames/Commands/CommandDispatcher.cs b/ASFFreeGames/Commands/CommandDispatcher.cs index 491ac83..67bde6c 100644 --- a/ASFFreeGames/Commands/CommandDispatcher.cs +++ b/ASFFreeGames/Commands/CommandDispatcher.cs @@ -5,39 +5,51 @@ using ArchiSteamFarm.Steam; using ASFFreeGames.Commands.GetIp; using ASFFreeGames.Configurations; -using Maxisoft.ASF; namespace ASFFreeGames.Commands { // Implement the IBotCommand interface - internal sealed class CommandDispatcher : IBotCommand { + internal sealed class CommandDispatcher(ASFFreeGamesOptions options) : IBotCommand { // Declare a private field for the plugin options instance - private readonly ASFFreeGamesOptions Options; + private readonly ASFFreeGamesOptions Options = options ?? throw new ArgumentNullException(nameof(options)); // Declare a private field for the dictionary that maps command names to IBotCommand instances - private readonly Dictionary Commands; + private readonly Dictionary Commands = new(StringComparer.OrdinalIgnoreCase) { + { "GETIP", new GetIPCommand() }, + { "FREEGAMES", new FreeGamesCommand(options) } + }; // Define a constructor that takes an plugin options instance as a parameter - public CommandDispatcher(ASFFreeGamesOptions options) { - Options = options ?? throw new ArgumentNullException(nameof(options)); + // Initialize the commands dictionary with instances of GetIPCommand and FreeGamesCommand - // Initialize the commands dictionary with instances of GetIPCommand and FreeGamesCommand - Commands = new Dictionary(StringComparer.OrdinalIgnoreCase) { - { "GETIP", new GetIPCommand() }, - { "FREEGAMES", new FreeGamesCommand(options) } - }; - } - - // Define a method named Execute that takes the bot, message, args, steamID, and cancellationToken parameters and returns a string response public async Task Execute(Bot? bot, string message, string[] args, ulong steamID = 0, CancellationToken cancellationToken = default) { - if (args is { Length: > 0 }) { - // Try to get the corresponding IBotCommand instance from the commands dictionary based on the first argument - if (Commands.TryGetValue(args[0], out IBotCommand? command)) { - // Delegate the command execution to the IBotCommand instance, passing the bot and other parameters - return await command.Execute(bot, message, args, steamID, cancellationToken).ConfigureAwait(false); + try { + if (args is { Length: > 0 }) { + // Try to get the corresponding IBotCommand instance from the commands dictionary based on the first argument + if (Commands.TryGetValue(args[0], out IBotCommand? command)) { + // Delegate the command execution to the IBotCommand instance, passing the bot and other parameters + return await command.Execute(bot, message, args, steamID, cancellationToken).ConfigureAwait(false); + } + } + } + catch (Exception ex) { + // Check if verbose logging is enabled or if the build is in debug mode + // ReSharper disable once RedundantAssignment + bool verboseLogging = Options.VerboseLog ?? false; +#if DEBUG + verboseLogging = true; // Enable verbose logging in debug mode +#endif + + if (verboseLogging) { + // Log the detailed stack trace and full description of the exception + ArchiSteamFarm.Core.ASF.ArchiLogger.LogGenericException(ex); + } + else { + // Log a compact error message + ArchiSteamFarm.Core.ASF.ArchiLogger.LogGenericError($"An error occurred: {ex.GetType().Name} {ex.Message}"); } } - return null; + return null; // Return null if an exception occurs or if no command is found } } } diff --git a/ASFFreeGames/Commands/FreeGamesCommand.cs b/ASFFreeGames/Commands/FreeGamesCommand.cs index 113f8ad..816efc8 100644 --- a/ASFFreeGames/Commands/FreeGamesCommand.cs +++ b/ASFFreeGames/Commands/FreeGamesCommand.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; +using System.Text.Json; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -202,7 +204,21 @@ private async Task CollectGames(IEnumerable bots, ECollectGameRequestS int res = 0; try { - ICollection games = await RedditHelper.GetGames(cancellationToken).ConfigureAwait(false); + ICollection games; + + try { + games = await RedditHelper.GetGames(cancellationToken).ConfigureAwait(false); + } + catch (Exception e) when (e is InvalidOperationException or JsonException or IOException or RedditServerException) { + if (Options.VerboseLog ?? false) { + ArchiSteamFarm.Core.ASF.ArchiLogger.LogGenericException(e); + } + else { + ArchiSteamFarm.Core.ASF.ArchiLogger.LogGenericError($"Unable to load json from reddit {e.GetType().Name}: {e.Message}"); + } + + return 0; + } LogNewGameCount(games, VerboseLog || requestSource is ECollectGameRequestSource.RequestedByUser); diff --git a/ASFFreeGames/Reddit/RedditHelper.cs b/ASFFreeGames/Reddit/RedditHelper.cs index be5c0b9..68f324c 100644 --- a/ASFFreeGames/Reddit/RedditHelper.cs +++ b/ASFFreeGames/Reddit/RedditHelper.cs @@ -38,19 +38,7 @@ public static async ValueTask> GetGames(Cancellatio return result; } - JsonNode jsonPayload = await GetPayload(webBrowser, cancellationToken).ConfigureAwait(false) ?? JsonNode.Parse("{}")!; - - try { - if ((jsonPayload["kind"]?.GetValue() != "Listing") || - jsonPayload["data"] is null) { - return result; - } - } - catch (Exception e) when (e is FormatException or InvalidOperationException) { - ArchiSteamFarm.Core.ASF.ArchiLogger.LogGenericInfo("invalid json"); - - return result; - } + JsonNode? jsonPayload = await GetPayload(webBrowser, cancellationToken).ConfigureAwait(false); JsonNode? childrenElement = jsonPayload["data"]?["children"]; @@ -171,28 +159,46 @@ internal static RedditGameEntry[] LoadMessages(JsonNode children) { /// A JSON object response or null if failed. /// Thrown when Reddit returns a server error. /// This method is based on this GitHub issue: https://github.com/maxisoft/ASFFreeGames/issues/28 - private static async ValueTask GetPayload(WebBrowser webBrowser, CancellationToken cancellationToken, uint retry = 5) { + private static async ValueTask GetPayload(WebBrowser webBrowser, CancellationToken cancellationToken, uint retry = 5) { StreamResponse? stream = null; for (int t = 0; t < retry; t++) { try { - stream = await webBrowser.UrlGetToStream(GetUrl(), rateLimitingDelay: 500, cancellationToken: cancellationToken).ConfigureAwait(false); + stream = await webBrowser.UrlGetToStream(GetUrl(), rateLimitingDelay: 500, maxTries: 1, cancellationToken: cancellationToken).ConfigureAwait(false); if (stream?.Content is null) { - throw new RedditServerException("Reddit server error: content is null", stream?.StatusCode ?? HttpStatusCode.InternalServerError); + throw new RedditServerException("content is null", stream?.StatusCode ?? HttpStatusCode.InternalServerError); } if (stream.StatusCode.IsServerErrorCode()) { - throw new RedditServerException($"Reddit server error: {stream.StatusCode}", stream.StatusCode); + throw new RedditServerException($"server error code is {stream.StatusCode}", stream.StatusCode); } - return await ParseJsonNode(stream, cancellationToken).ConfigureAwait(false); + JsonNode? res = await ParseJsonNode(stream, cancellationToken).ConfigureAwait(false); + + if (res is null) { + throw new RedditServerException("empty response", stream.StatusCode); + } + + try { + if ((res["kind"]?.GetValue() != "Listing") || + res["data"] is null) { + throw new RedditServerException("invalid response", stream.StatusCode); + } + } + catch (Exception e) when (e is FormatException or InvalidOperationException) { + throw new RedditServerException("invalid response", stream.StatusCode); + } + + return res; } catch (Exception e) when (e is JsonException or IOException or RedditServerException or HttpRequestException) { - // If no RedditServerException was thrown, re-throw the original Exception + // If it's the last retry, re-throw the original Exception if (t + 1 == retry) { throw; } + + cancellationToken.ThrowIfCancellationRequested(); } finally { if (stream is not null) { @@ -202,11 +208,11 @@ internal static RedditGameEntry[] LoadMessages(JsonNode children) { stream = null; } - await Task.Delay((2 << t) * 100, cancellationToken).ConfigureAwait(false); + await Task.Delay((2 << (t + 1)) * 100, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); } - return JsonNode.Parse("{}"); + return JsonNode.Parse("{}")!; } ///