Skip to content

Commit

Permalink
Closes #3327
Browse files Browse the repository at this point in the history
  • Loading branch information
JustArchi committed Nov 6, 2024
1 parent b66d883 commit 7c481de
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 10 deletions.
4 changes: 2 additions & 2 deletions ArchiSteamFarm/IPC/Controllers/Api/BotController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ public async Task<ActionResult<GenericResponse>> PausePost(string botNames, [Fro
[HttpPost("{botNames:required}/RedeemPoints/{definitionID:required}")]
[ProducesResponseType<GenericResponse<IReadOnlyDictionary<string, EResult>>>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]
public async Task<ActionResult<GenericResponse>> RedeemPointsPost(string botNames, uint definitionID) {
public async Task<ActionResult<GenericResponse>> RedeemPointsPost(string botNames, uint definitionID, [FromQuery] bool forced = false) {
ArgumentException.ThrowIfNullOrEmpty(botNames);
ArgumentOutOfRangeException.ThrowIfZero(definitionID);

Expand All @@ -335,7 +335,7 @@ public async Task<ActionResult<GenericResponse>> RedeemPointsPost(string botName
return BadRequest(new GenericResponse(false, Strings.FormatBotNotFound(botNames)));
}

IList<EResult> results = await Utilities.InParallel(bots.Select(bot => bot.Actions.RedeemPoints(definitionID))).ConfigureAwait(false);
IList<EResult> results = await Utilities.InParallel(bots.Select(bot => bot.Actions.RedeemPoints(definitionID, forced))).ConfigureAwait(false);

Dictionary<string, EResult> result = new(bots.Count, Bot.BotsComparer);

Expand Down
52 changes: 52 additions & 0 deletions ArchiSteamFarm/Steam/Integration/ArchiHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,58 @@ public async IAsyncEnumerable<Asset> GetMyInventoryAsync(uint appID = Asset.Stea
return response.Result == EResult.OK ? response.Body.summary?.points : null;
}

[PublicAPI]
public async Task<Dictionary<uint, LoyaltyRewardDefinition>?> GetRewardItems(IReadOnlyCollection<uint> definitionIDs) {
if ((definitionIDs == null) || (definitionIDs.Count == 0)) {
throw new ArgumentNullException(nameof(definitionIDs));
}

if (Client == null) {
throw new InvalidOperationException(nameof(Client));
}

if (!Client.IsConnected) {
return null;
}

CLoyaltyRewards_QueryRewardItems_Request request = new();

request.definitionids.AddRange(definitionIDs is IReadOnlySet<uint> or ISet<uint> ? definitionIDs : definitionIDs.Distinct());

Dictionary<uint, LoyaltyRewardDefinition>? result = null;

while (true) {
SteamUnifiedMessages.ServiceMethodResponse<CLoyaltyRewards_QueryRewardItems_Response> response;

try {
response = await UnifiedLoyaltyRewards.QueryRewardItems(request).ToLongRunningTask().ConfigureAwait(false);
} catch (Exception e) {
ArchiLogger.LogGenericWarningException(e);

return null;
}

if (response.Result != EResult.OK) {
return null;
}

result ??= new Dictionary<uint, LoyaltyRewardDefinition>(response.Body.total_count);

bool added = false;

foreach (LoyaltyRewardDefinition _ in response.Body.definitions.Where(entry => result.TryAdd(entry.defid, entry))) {
added = true;
}

// Normally it should be enough to compare counts exclusively, but we're going to use additional bulletproofing against infinite loops just in case
if (!added || (result.Count >= response.Body.total_count) || string.IsNullOrEmpty(response.Body.next_cursor) || (request.cursor == response.Body.next_cursor)) {
return result;
}

request.cursor = response.Body.next_cursor;
}
}

[PublicAPI]
public async Task<CCredentials_GetSteamGuardDetails_Response?> GetSteamGuardStatus() {
if (Client == null) {
Expand Down
28 changes: 27 additions & 1 deletion ArchiSteamFarm/Steam/Interaction/Actions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
using JetBrains.Annotations;
using SteamKit2;
using SteamKit2.Internal;
using SteamKit2.WebUI.Internal;

namespace ArchiSteamFarm.Steam.Interaction;

Expand Down Expand Up @@ -171,6 +172,15 @@ public ulong GetFirstSteamMasterID() {
return (steamOwnerID > 0) && new SteamID(steamOwnerID).IsIndividualAccount ? steamOwnerID : 0;
}

[PublicAPI]
public async Task<Dictionary<uint, LoyaltyRewardDefinition>?> GetRewardItems(IReadOnlyCollection<uint> definitionIDs) {
if ((definitionIDs == null) || (definitionIDs.Count == 0)) {
throw new ArgumentNullException(nameof(definitionIDs));
}

return await Bot.ArchiHandler.GetRewardItems(definitionIDs).ConfigureAwait(false);
}

[MustDisposeResource]
[PublicAPI]
public async Task<IDisposable> GetTradingLock() {
Expand Down Expand Up @@ -314,9 +324,25 @@ public static string Hash(ArchiCryptoHelper.EHashingMethod hashingMethod, string
}

[PublicAPI]
public async Task<EResult> RedeemPoints(uint definitionID) {
public async Task<EResult> RedeemPoints(uint definitionID, bool forced = false) {
ArgumentOutOfRangeException.ThrowIfZero(definitionID);

if (!forced) {
Dictionary<uint, LoyaltyRewardDefinition>? definitions = await Bot.Actions.GetRewardItems(new HashSet<uint>(1) { definitionID }).ConfigureAwait(false);

if (definitions == null) {
return EResult.Timeout;
}

if (!definitions.TryGetValue(definitionID, out LoyaltyRewardDefinition? definition)) {
return EResult.InvalidParam;
}

if (definition.point_cost > 0) {
return EResult.InvalidState;
}
}

return await Bot.ArchiHandler.RedeemPoints(definitionID).ConfigureAwait(false);
}

Expand Down
46 changes: 40 additions & 6 deletions ArchiSteamFarm/Steam/Interaction/Commands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
using JetBrains.Annotations;
using SteamKit2;
using SteamKit2.Internal;
using SteamKit2.WebUI.Internal;

namespace ArchiSteamFarm.Steam.Interaction;

Expand Down Expand Up @@ -1028,6 +1029,10 @@ internal void OnNewLicenseList() {
ArgumentOutOfRangeException.ThrowIfZero(contextID);
ArgumentNullException.ThrowIfNull(targetBot);

if ((assetRarities == null) || (assetRarities.Count == 0)) {
throw new ArgumentNullException(nameof(assetRarities));
}

if (access < EAccess.Master) {
return null;
}
Expand Down Expand Up @@ -2767,7 +2772,7 @@ internal void OnNewLicenseList() {
return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null;
}

private async Task<string?> ResponseRedeemPoints(EAccess access, HashSet<uint> definitionIDs) {
private async Task<string?> ResponseRedeemPoints(EAccess access, Dictionary<uint, bool> definitionIDs) {
if (!Enum.IsDefined(access)) {
throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess));
}
Expand All @@ -2784,13 +2789,34 @@ internal void OnNewLicenseList() {
return FormatBotResponse(Strings.BotNotConnected);
}

IList<EResult> results = await Utilities.InParallel(definitionIDs.Select(Bot.Actions.RedeemPoints)).ConfigureAwait(false);
HashSet<uint> definitionIDsToCheck = definitionIDs.Where(static entry => !entry.Value).Select(static entry => entry.Key).ToHashSet();

if (definitionIDsToCheck.Count > 0) {
Dictionary<uint, LoyaltyRewardDefinition>? definitions = await Bot.Actions.GetRewardItems(definitionIDsToCheck).ConfigureAwait(false);

if (definitions == null) {
return FormatBotResponse(Strings.FormatWarningFailedWithError(nameof(Bot.Actions.GetRewardItems)));
}

foreach (uint definitionID in definitionIDsToCheck) {
if (!definitions.TryGetValue(definitionID, out LoyaltyRewardDefinition? definition)) {
return FormatBotResponse(Strings.FormatWarningFailedWithError(definitionID));
}

if (definition.point_cost > 0) {
return FormatBotResponse(Strings.FormatWarningFailedWithError($"{definitionID} {nameof(definition.point_cost)} ({definition.point_cost}) > 0"));
}
}
}

// We already did more optimized check, therefore we can skip the one in actions
IList<EResult> results = await Utilities.InParallel(definitionIDs.Keys.Select(definitionID => Bot.Actions.RedeemPoints(definitionID, true))).ConfigureAwait(false);

int i = 0;

StringBuilder response = new();

foreach (uint definitionID in definitionIDs) {
foreach (uint definitionID in definitionIDs.Keys) {
response.AppendLine(FormatBotResponse(Strings.FormatBotAddLicense(definitionID, results[i++])));
}

Expand Down Expand Up @@ -2818,14 +2844,22 @@ internal void OnNewLicenseList() {
return FormatBotResponse(Strings.FormatErrorIsEmpty(nameof(definitions)));
}

HashSet<uint> definitionIDs = new(definitions.Length);
Dictionary<uint, bool> definitionIDs = new(definitions.Length);

foreach (string definition in definitions) {
if (!uint.TryParse(definition, out uint definitionID) || (definitionID == 0)) {
bool forced = false;
string definitionToParse = definition;

if (definitionToParse.EndsWith('!')) {
forced = true;
definitionToParse = definitionToParse[..^1];
}

if (!uint.TryParse(definitionToParse, out uint definitionID) || (definitionID == 0)) {
return FormatBotResponse(Strings.FormatErrorIsInvalid(nameof(definition)));
}

definitionIDs.Add(definitionID);
definitionIDs[definitionID] = forced;
}

return await ResponseRedeemPoints(access, definitionIDs).ConfigureAwait(false);
Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>6.0.8.8</Version>
<Version>6.0.9.0</Version>
</PropertyGroup>

<PropertyGroup>
Expand Down

0 comments on commit 7c481de

Please sign in to comment.