diff --git a/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs b/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs index d1e321dec..7e9c3b49b 100644 --- a/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs +++ b/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs @@ -325,7 +325,7 @@ public sealed class BotController : ArchiController { [HttpPost("{botNames:required}/RedeemPoints/{definitionID:required}")] [ProducesResponseType>>((int) HttpStatusCode.OK)] [ProducesResponseType((int) HttpStatusCode.BadRequest)] - public async Task> RedeemPointsPost(string botNames, uint definitionID) { + public async Task> RedeemPointsPost(string botNames, uint definitionID, [FromQuery] bool forced = false) { ArgumentException.ThrowIfNullOrEmpty(botNames); ArgumentOutOfRangeException.ThrowIfZero(definitionID); @@ -335,7 +335,7 @@ public sealed class BotController : ArchiController { return BadRequest(new GenericResponse(false, Strings.FormatBotNotFound(botNames))); } - IList results = await Utilities.InParallel(bots.Select(bot => bot.Actions.RedeemPoints(definitionID))).ConfigureAwait(false); + IList results = await Utilities.InParallel(bots.Select(bot => bot.Actions.RedeemPoints(definitionID, forced))).ConfigureAwait(false); Dictionary result = new(bots.Count, Bot.BotsComparer); diff --git a/ArchiSteamFarm/Steam/Integration/ArchiHandler.cs b/ArchiSteamFarm/Steam/Integration/ArchiHandler.cs index de50f36f8..02f482ba8 100644 --- a/ArchiSteamFarm/Steam/Integration/ArchiHandler.cs +++ b/ArchiSteamFarm/Steam/Integration/ArchiHandler.cs @@ -386,6 +386,58 @@ public sealed class ArchiHandler : ClientMsgHandler { return response.Result == EResult.OK ? response.Body.summary?.points : null; } + [PublicAPI] + public async Task?> GetRewardItems(IReadOnlyCollection 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 or ISet ? definitionIDs : definitionIDs.Distinct()); + + Dictionary? result = null; + + while (true) { + SteamUnifiedMessages.ServiceMethodResponse 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(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 GetSteamGuardStatus() { if (Client == null) { diff --git a/ArchiSteamFarm/Steam/Interaction/Actions.cs b/ArchiSteamFarm/Steam/Interaction/Actions.cs index 17fd26c88..0ec277723 100644 --- a/ArchiSteamFarm/Steam/Interaction/Actions.cs +++ b/ArchiSteamFarm/Steam/Interaction/Actions.cs @@ -42,6 +42,7 @@ using ArchiSteamFarm.Web; using JetBrains.Annotations; using SteamKit2; using SteamKit2.Internal; +using SteamKit2.WebUI.Internal; namespace ArchiSteamFarm.Steam.Interaction; @@ -171,6 +172,15 @@ public sealed class Actions : IAsyncDisposable, IDisposable { return (steamOwnerID > 0) && new SteamID(steamOwnerID).IsIndividualAccount ? steamOwnerID : 0; } + [PublicAPI] + public async Task?> GetRewardItems(IReadOnlyCollection 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 GetTradingLock() { @@ -314,9 +324,25 @@ public sealed class Actions : IAsyncDisposable, IDisposable { } [PublicAPI] - public async Task RedeemPoints(uint definitionID) { + public async Task RedeemPoints(uint definitionID, bool forced = false) { ArgumentOutOfRangeException.ThrowIfZero(definitionID); + if (!forced) { + Dictionary? definitions = await Bot.Actions.GetRewardItems(new HashSet(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); } diff --git a/ArchiSteamFarm/Steam/Interaction/Commands.cs b/ArchiSteamFarm/Steam/Interaction/Commands.cs index f9b4a6394..7e9e9e90a 100644 --- a/ArchiSteamFarm/Steam/Interaction/Commands.cs +++ b/ArchiSteamFarm/Steam/Interaction/Commands.cs @@ -41,6 +41,7 @@ using ArchiSteamFarm.Storage; using JetBrains.Annotations; using SteamKit2; using SteamKit2.Internal; +using SteamKit2.WebUI.Internal; namespace ArchiSteamFarm.Steam.Interaction; @@ -1028,6 +1029,10 @@ public sealed class Commands { ArgumentOutOfRangeException.ThrowIfZero(contextID); ArgumentNullException.ThrowIfNull(targetBot); + if ((assetRarities == null) || (assetRarities.Count == 0)) { + throw new ArgumentNullException(nameof(assetRarities)); + } + if (access < EAccess.Master) { return null; } @@ -2767,7 +2772,7 @@ public sealed class Commands { return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null; } - private async Task ResponseRedeemPoints(EAccess access, HashSet definitionIDs) { + private async Task ResponseRedeemPoints(EAccess access, Dictionary definitionIDs) { if (!Enum.IsDefined(access)) { throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess)); } @@ -2784,13 +2789,34 @@ public sealed class Commands { return FormatBotResponse(Strings.BotNotConnected); } - IList results = await Utilities.InParallel(definitionIDs.Select(Bot.Actions.RedeemPoints)).ConfigureAwait(false); + HashSet definitionIDsToCheck = definitionIDs.Where(static entry => !entry.Value).Select(static entry => entry.Key).ToHashSet(); + + if (definitionIDsToCheck.Count > 0) { + Dictionary? 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 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++]))); } @@ -2818,14 +2844,22 @@ public sealed class Commands { return FormatBotResponse(Strings.FormatErrorIsEmpty(nameof(definitions))); } - HashSet definitionIDs = new(definitions.Length); + Dictionary 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); diff --git a/Directory.Build.props b/Directory.Build.props index 24f4c0af2..c88f6d9b6 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 6.0.8.8 + 6.0.9.0