Skip to content

Commit

Permalink
Merge pull request #74 from maxisoft/dev
Browse files Browse the repository at this point in the history
Enhanced Error Handling
  • Loading branch information
maxisoft authored May 22, 2024
2 parents fd19f93 + a3a5226 commit 2185314
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 42 deletions.
52 changes: 32 additions & 20 deletions ASFFreeGames/Commands/CommandDispatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, IBotCommand> Commands;
private readonly Dictionary<string, IBotCommand> 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<string, IBotCommand>(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<string?> 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
}
}
}
18 changes: 17 additions & 1 deletion ASFFreeGames/Commands/FreeGamesCommand.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -202,7 +204,21 @@ private async Task<int> CollectGames(IEnumerable<Bot> bots, ECollectGameRequestS
int res = 0;

try {
ICollection<RedditGameEntry> games = await RedditHelper.GetGames(cancellationToken).ConfigureAwait(false);
ICollection<RedditGameEntry> 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);

Expand Down
48 changes: 27 additions & 21 deletions ASFFreeGames/Reddit/RedditHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,7 @@ public static async ValueTask<ICollection<RedditGameEntry>> GetGames(Cancellatio
return result;
}

JsonNode jsonPayload = await GetPayload(webBrowser, cancellationToken).ConfigureAwait(false) ?? JsonNode.Parse("{}")!;

try {
if ((jsonPayload["kind"]?.GetValue<string>() != "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"];

Expand Down Expand Up @@ -171,28 +159,46 @@ internal static RedditGameEntry[] LoadMessages(JsonNode children) {
/// <returns>A JSON object response or null if failed.</returns>
/// <exception cref="RedditServerException">Thrown when Reddit returns a server error.</exception>
/// <remarks>This method is based on this GitHub issue: https://github.com/maxisoft/ASFFreeGames/issues/28</remarks>
private static async ValueTask<JsonNode?> GetPayload(WebBrowser webBrowser, CancellationToken cancellationToken, uint retry = 5) {
private static async ValueTask<JsonNode> 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<string>() != "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) {
Expand All @@ -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("{}")!;
}

/// <summary>
Expand Down

0 comments on commit 2185314

Please sign in to comment.