Skip to content

Commit

Permalink
Integrate Redlib for free game discovery, add configurations, strateg…
Browse files Browse the repository at this point in the history
…ies, error handling, tests and update build process

This commit introduces Redlib integration into the ASFFreeGames project, allowing it to fetch free games from Redlib instances. Key changes include:

* **Redlib Integration:**
    * Added support for Redlib as a source for finding free games.
    * Implemented `RedlibListFreeGamesStrategy` to fetch games from Redlib instances.
    * Introduced configurations for Redlib proxy and instance URL.
    * Updated `ListFreeGamesMainStrategy` to handle fetching from Redlib as a fallback strategy.
* **Code refactoring:**
    * Introduced `EListFreeGamesStrategy` enum to represent supported free game listing sources (Reddit, Redlib).
    * Improved logic for handling successful and failed attempts in `ListFreeGamesMainStrategy`.
    * Added exception handling for Redlib related issues.
* **Testing:**
    * Added a new unit test (`RedlibInstanceListTests.Test`) to verify Redlib instance listing functionality.
    * Updated `FreeGamesCommand.Test` to handle Redlib strategy.
* **Build:**
    * Added `Resouces` folder to the project.
    * Included `redlib_instances.json` as an embedded resource to store Redlib instances.
  • Loading branch information
maxisoft committed Sep 10, 2024
1 parent 78b042a commit 2791023
Show file tree
Hide file tree
Showing 32 changed files with 995 additions and 38 deletions.
3 changes: 2 additions & 1 deletion ASFFreeGames.Tests/Redlib/RedlibHtmlParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Text;
using System.Threading.Tasks;
using Maxisoft.ASF.Redlib;
using Maxisoft.ASF.Redlib.Html;
using Xunit;

namespace Maxisoft.ASF.Tests.Redlib;
Expand All @@ -14,7 +15,7 @@ public async void Test() {
string html = await LoadHtml().ConfigureAwait(false);

// ReSharper disable once ArgumentsStyleLiteral
IReadOnlyCollection<GameEntry> result = RedlibHtmlParser.ParseGamesFromHtml(html, dedup: false);
IReadOnlyCollection<RedlibGameEntry> result = RedlibHtmlParser.ParseGamesFromHtml(html, dedup: false);
Assert.NotEmpty(result);
Assert.Equal(25, result.Count);

Expand Down
24 changes: 24 additions & 0 deletions ASFFreeGames.Tests/Redlib/RedlibInstancesListTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using ASFFreeGames.Configurations;
using Maxisoft.ASF.Redlib;
using Maxisoft.ASF.Redlib.Html;
using Maxisoft.ASF.Redlib.Instances;
using Xunit;

namespace Maxisoft.ASF.Tests.Redlib;

public class RedlibInstanceListTests {
[Fact]
public async void Test() {
RedlibInstanceList lister = new(new ASFFreeGamesOptions());
List<Uri> uris = await RedlibInstanceList.ListFromEmbedded(default(CancellationToken)).ConfigureAwait(false);

Assert.NotEmpty(uris);
}
}
9 changes: 9 additions & 0 deletions ASFFreeGames/ASFFreeGames.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,13 @@
<Link>Directory.Build.props</Link>
</Content>
</ItemGroup>

<ItemGroup>
<Folder Include="Resouces\" />
</ItemGroup>

<ItemGroup>
<None Remove="Resouces\redlib_instances.json" />
<EmbeddedResource Include="Resouces\redlib_instances.json" />
</ItemGroup>
</Project>
36 changes: 29 additions & 7 deletions ASFFreeGames/Commands/FreeGamesCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using Maxisoft.ASF;
using Maxisoft.ASF.ASFExtentions;
using Maxisoft.ASF.Configurations;
using Maxisoft.ASF.FreeGames.Strategies;
using Maxisoft.ASF.HttpClientSimple;
using Maxisoft.ASF.Reddit;
using Maxisoft.ASF.Utils;
Expand All @@ -23,6 +24,8 @@ namespace ASFFreeGames.Commands {
// Implement the IBotCommand interface
internal sealed class FreeGamesCommand(ASFFreeGamesOptions options) : IBotCommand, IDisposable {
public void Dispose() {
Strategy.Dispose();

if (HttpFactory.IsValueCreated) {
HttpFactory.Value.Dispose();
}
Expand All @@ -40,6 +43,9 @@ public void Dispose() {

private readonly Lazy<SimpleHttpClientFactory> HttpFactory = new(() => new SimpleHttpClientFactory(options));

public IListFreeGamesStrategy Strategy { get; internal set; } = new ListFreeGamesMainStrategy();
public EListFreeGamesStrategy PreviousSucessfulStrategy { get; private set; } = EListFreeGamesStrategy.Reddit | EListFreeGamesStrategy.Redlib;

// Define a constructor that takes an plugin options instance as a parameter

/// <inheritdoc />
Expand Down Expand Up @@ -218,23 +224,39 @@ private async Task<int> CollectGames(IEnumerable<Bot> bots, ECollectGameRequestS
try {
IReadOnlyCollection<RedditGameEntry> games;

ListFreeGamesContext strategyContext = new(Options, new Lazy<SimpleHttpClient>(() => HttpFactory.Value.CreateGeneric())) {
Strategy = Strategy,
HttpClientFactory = HttpFactory.Value,
PreviousSucessfulStrategy = PreviousSucessfulStrategy
};

try {
#pragma warning disable CA2000
games = await RedditHelper.GetGames(HttpFactory.Value.CreateForReddit(), cancellationToken).ConfigureAwait(false);
games = await Strategy.GetGames(strategyContext, cancellationToken).ConfigureAwait(false);
#pragma warning restore CA2000
}
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}");
ArchiSteamFarm.Core.ASF.ArchiLogger.LogGenericError($"Unable to get and load json {e.GetType().Name}: {e.Message}");
}

return 0;
}
finally {
PreviousSucessfulStrategy = strategyContext.PreviousSucessfulStrategy;

if (Options.VerboseLog ?? false) {
ArchiSteamFarm.Core.ASF.ArchiLogger.LogGenericInfo($"PreviousSucessfulStrategy = {PreviousSucessfulStrategy}");
}
}

LogNewGameCount(games, VerboseLog || requestSource is ECollectGameRequestSource.RequestedByUser);
#pragma warning disable CA1308
string remote = strategyContext.PreviousSucessfulStrategy.ToString().ToLowerInvariant();
#pragma warning restore CA1308
LogNewGameCount(games, remote, VerboseLog || requestSource is ECollectGameRequestSource.RequestedByUser);

foreach (Bot bot in bots) {
if (cancellationToken.IsCancellationRequested) {
Expand Down Expand Up @@ -348,7 +370,7 @@ private async Task<int> CollectGames(IEnumerable<Bot> bots, ECollectGameRequestS
return res;
}

private void LogNewGameCount(IReadOnlyCollection<RedditGameEntry> games, bool logZero = false) {
private void LogNewGameCount(IReadOnlyCollection<RedditGameEntry> games, string remote, bool logZero = false) {
int totalAppIdCounter = PreviouslySeenAppIds.Count;
int newGameCounter = 0;

Expand All @@ -359,13 +381,13 @@ private void LogNewGameCount(IReadOnlyCollection<RedditGameEntry> games, bool lo
}

if ((totalAppIdCounter == 0) && (games.Count > 0)) {
ArchiSteamFarm.Core.ASF.ArchiLogger.LogGenericInfo($"[FreeGames] found potentially {games.Count} free games on reddit", nameof(CollectGames));
ArchiSteamFarm.Core.ASF.ArchiLogger.LogGenericInfo($"[FreeGames] found potentially {games.Count} free games on {remote}", nameof(CollectGames));
}
else if (newGameCounter > 0) {
ArchiSteamFarm.Core.ASF.ArchiLogger.LogGenericInfo($"[FreeGames] found {newGameCounter} fresh free game(s) on reddit", nameof(CollectGames));
ArchiSteamFarm.Core.ASF.ArchiLogger.LogGenericInfo($"[FreeGames] found {newGameCounter} fresh free game(s) on {remote}", nameof(CollectGames));
}
else if ((newGameCounter == 0) && logZero) {
ArchiSteamFarm.Core.ASF.ArchiLogger.LogGenericInfo($"[FreeGames] found 0 new game out of {games.Count} free games on reddit", nameof(CollectGames));
ArchiSteamFarm.Core.ASF.ArchiLogger.LogGenericInfo($"[FreeGames] found 0 new game out of {games.Count} free games on {remote}", nameof(CollectGames));
}
}

Expand Down
8 changes: 8 additions & 0 deletions ASFFreeGames/Configurations/ASFFreeGamesOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,13 @@ public bool IsBlacklisted(in GameIdentifier gid) {

[JsonPropertyName("redditProxy")]
public string? RedditProxy { get; set; }

[JsonPropertyName("redlibProxy")]
public string? RedlibProxy { get; set; }
#endregion

[JsonPropertyName("redlibInstanceUrl")]
#pragma warning disable CA1056
public string? RedlibInstanceUrl { get; set; } = "https://raw.githubusercontent.com/redlib-org/redlib-instances/main/instances.json";
#pragma warning restore CA1056
}
2 changes: 2 additions & 0 deletions ASFFreeGames/Configurations/ASFFreeGamesOptionsLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public static void Bind(ref ASFFreeGamesOptions options) {
options.RandomizeRecheckInterval = configurationRoot.GetValue("RandomizeRecheckInterval", options.RandomizeRecheckInterval);
options.Proxy = configurationRoot.GetValue("Proxy", options.Proxy);
options.RedditProxy = configurationRoot.GetValue("RedditProxy", options.RedditProxy);
options.RedlibProxy = configurationRoot.GetValue("RedlibProxy", options.RedlibProxy);
options.RedlibInstanceUrl = configurationRoot.GetValue("RedlibInstanceUrl", options.RedlibInstanceUrl);
}
finally {
Semaphore.Release();
Expand Down
3 changes: 2 additions & 1 deletion ASFFreeGames/Configurations/ASFFreeGamesOptionsSaver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,12 @@ internal static int CreateOptionsBuffer(ASFFreeGamesOptions options, IMemoryOwne
written += WriteNameAndProperty("verboseLog"u8, options.VerboseLog, buffer, written);
written += WriteNameAndProperty("proxy"u8, options.Proxy, buffer, written);
written += WriteNameAndProperty("redditProxy"u8, options.RedditProxy, buffer, written);
written += WriteNameAndProperty("redlibProxy"u8, options.RedlibProxy, buffer, written);
written += WriteNameAndProperty("redlibInstanceUrl"u8, options.RedlibInstanceUrl, buffer, written);
RemoveTrailingCommaAndLineReturn(buffer, ref written);

written += WriteJsonString("\n}"u8, buffer, written);

// Resize buffer if needed
if (written >= buffer.Length) {
throw new InvalidOperationException("Buffer overflow while saving options");
}
Expand Down
11 changes: 11 additions & 0 deletions ASFFreeGames/FreeGames/Strategies/EListFreeGamesStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System;

// ReSharper disable once CheckNamespace
namespace Maxisoft.ASF.FreeGames.Strategies;

[Flags]
public enum EListFreeGamesStrategy {
None = 0,
Reddit = 1 << 0,
Redlib = 1 << 1
}
15 changes: 15 additions & 0 deletions ASFFreeGames/FreeGames/Strategies/HttpRequestRedlibException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;
using System.Net;
using Maxisoft.ASF.Redlib;

// ReSharper disable once CheckNamespace
namespace Maxisoft.ASF.FreeGames.Strategies;

public class HttpRequestRedlibException : RedlibException {
public required HttpStatusCode? StatusCode { get; init; }
public required Uri? Uri { get; init; }

public HttpRequestRedlibException() { }
public HttpRequestRedlibException(string message) : base(message) { }
public HttpRequestRedlibException(string message, Exception inner) : base(message, inner) { }
}
26 changes: 26 additions & 0 deletions ASFFreeGames/FreeGames/Strategies/IListFreeGamesStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Maxisoft.ASF.Reddit;

// ReSharper disable once CheckNamespace
namespace Maxisoft.ASF.FreeGames.Strategies;

[SuppressMessage("ReSharper", "RedundantNullableFlowAttribute")]
public interface IListFreeGamesStrategy : IDisposable {
Task<IReadOnlyCollection<RedditGameEntry>> GetGames([NotNull] ListFreeGamesContext context, CancellationToken cancellationToken);

public static Exception ExceptionFromTask<T>([NotNull] Task<T> task) {
if (task is { IsFaulted: true, Exception: not null }) {
return task.Exception.InnerExceptions.Count == 1 ? task.Exception.InnerExceptions[0] : task.Exception;
}

if (task.IsCanceled) {
return new TaskCanceledException();
}

throw new InvalidOperationException("Unknown task state");
}
}
13 changes: 13 additions & 0 deletions ASFFreeGames/FreeGames/Strategies/ListFreeGamesContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;
using ASFFreeGames.Configurations;
using Maxisoft.ASF.HttpClientSimple;

// ReSharper disable once CheckNamespace
namespace Maxisoft.ASF.FreeGames.Strategies;

public sealed record ListFreeGamesContext(ASFFreeGamesOptions Options, Lazy<SimpleHttpClient> HttpClient, uint Retry = 5) {
public required SimpleHttpClientFactory HttpClientFactory { get; init; }
public EListFreeGamesStrategy PreviousSucessfulStrategy { get; set; }

public required IListFreeGamesStrategy Strategy { get; init; }
}
Loading

0 comments on commit 2791023

Please sign in to comment.