Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhanced Error Handling #74

Merged
merged 3 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading