diff --git a/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs b/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs index 03ff63c0b..10c31be39 100644 --- a/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs +++ b/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs @@ -34,7 +34,7 @@ using ArchiSteamFarm.Steam; using ArchiSteamFarm.Steam.Storage; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json.Linq; -using SteamKit2; +using SteamKit2.Internal; namespace ArchiSteamFarm.IPC.Controllers.Api; @@ -313,7 +313,7 @@ public sealed class BotController : ArchiController { /// [Consumes("application/json")] [HttpPost("{botNames:required}/Redeem")] - [ProducesResponseType(typeof(GenericResponse>>), (int) HttpStatusCode.OK)] + [ProducesResponseType(typeof(GenericResponse>>), (int) HttpStatusCode.OK)] [ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.BadRequest)] public async Task> RedeemPost(string botNames, [FromBody] BotRedeemRequest request) { if (string.IsNullOrEmpty(botNames)) { @@ -332,14 +332,14 @@ public sealed class BotController : ArchiController { return BadRequest(new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames))); } - IList results = await Utilities.InParallel(bots.Select(bot => request.KeysToRedeem.Select(key => bot.Actions.RedeemKey(key))).SelectMany(static task => task)).ConfigureAwait(false); + IList results = await Utilities.InParallel(bots.Select(bot => request.KeysToRedeem.Select(key => bot.Actions.RedeemKey(key))).SelectMany(static task => task)).ConfigureAwait(false); - Dictionary> result = new(bots.Count, Bot.BotsComparer); + Dictionary> result = new(bots.Count, Bot.BotsComparer); int count = 0; foreach (Bot bot in bots) { - Dictionary responses = new(request.KeysToRedeem.Count, StringComparer.Ordinal); + Dictionary responses = new(request.KeysToRedeem.Count, StringComparer.Ordinal); result[bot.BotName] = responses; foreach (string key in request.KeysToRedeem) { @@ -347,7 +347,7 @@ public sealed class BotController : ArchiController { } } - return Ok(new GenericResponse>>(result.Values.SelectMany(static responses => responses.Values).All(static value => value != null), result)); + return Ok(new GenericResponse>>(result.Values.SelectMany(static responses => responses.Values).All(static value => value != null), result)); } /// diff --git a/ArchiSteamFarm/Steam/Bot.cs b/ArchiSteamFarm/Steam/Bot.cs index 6e113f488..2415aa473 100644 --- a/ArchiSteamFarm/Steam/Bot.cs +++ b/ArchiSteamFarm/Steam/Bot.cs @@ -3441,37 +3441,40 @@ public sealed class Bot : IAsyncDisposable, IDisposable { } // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework - SteamApps.PurchaseResponseCallback? result = await Actions.RedeemKey(key!).ConfigureAwait(false); + CStore_RegisterCDKey_Response? response = await Actions.RedeemKey(key!).ConfigureAwait(false); - if (result == null) { + if (response == null) { continue; } + EResult result = (EResult) response.purchase_receipt_info.purchase_status; + EPurchaseResultDetail purchaseResultDetail = (EPurchaseResultDetail) response.purchase_result_details; + string? balanceText = null; - if ((result.PurchaseResultDetail == EPurchaseResultDetail.CannotRedeemCodeFromClient) || ((result.PurchaseResultDetail == EPurchaseResultDetail.BadActivationCode) && assumeWalletKeyOnBadActivationCode)) { + if ((purchaseResultDetail == EPurchaseResultDetail.CannotRedeemCodeFromClient) || ((purchaseResultDetail == EPurchaseResultDetail.BadActivationCode) && assumeWalletKeyOnBadActivationCode)) { // If it's a wallet code, we try to redeem it first, then handle the inner result as our primary one // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework (EResult Result, EPurchaseResultDetail? PurchaseResult, string? BalanceText)? walletResult = await ArchiWebHandler.RedeemWalletKey(key!).ConfigureAwait(false); if (walletResult != null) { - result.Result = walletResult.Value.Result; - result.PurchaseResultDetail = walletResult.Value.PurchaseResult.GetValueOrDefault(walletResult.Value.Result == EResult.OK ? EPurchaseResultDetail.NoDetail : EPurchaseResultDetail.BadActivationCode); // BadActivationCode is our smart guess in this case + result = walletResult.Value.Result; + purchaseResultDetail = walletResult.Value.PurchaseResult.GetValueOrDefault(walletResult.Value.Result == EResult.OK ? EPurchaseResultDetail.NoDetail : EPurchaseResultDetail.BadActivationCode); // BadActivationCode is our smart guess in this case balanceText = walletResult.Value.BalanceText; } else { - result.Result = EResult.Timeout; - result.PurchaseResultDetail = EPurchaseResultDetail.Timeout; + result = EResult.Timeout; + purchaseResultDetail = EPurchaseResultDetail.Timeout; } } - Dictionary? items = result.ParseItems(); + Dictionary? items = response.purchase_receipt_info.line_items.Count > 0 ? response.purchase_receipt_info.line_items.ToDictionary(static lineItem => lineItem.packageid, static lineItem => lineItem.line_item_description) : null; - ArchiLogger.LogGenericDebug(items?.Count > 0 ? string.Format(CultureInfo.CurrentCulture, Strings.BotRedeemWithItems, key, $"{result.Result}/{result.PurchaseResultDetail}{(!string.IsNullOrEmpty(balanceText) ? $"/{balanceText}" : "")}", string.Join(", ", items)) : string.Format(CultureInfo.CurrentCulture, Strings.BotRedeem, key, $"{result.Result}/{result.PurchaseResultDetail}{(!string.IsNullOrEmpty(balanceText) ? $"/{balanceText}" : "")}")); + ArchiLogger.LogGenericDebug(items?.Count > 0 ? string.Format(CultureInfo.CurrentCulture, Strings.BotRedeemWithItems, key, $"{result}/{purchaseResultDetail}{(!string.IsNullOrEmpty(balanceText) ? $"/{balanceText}" : "")}", string.Join(", ", items)) : string.Format(CultureInfo.CurrentCulture, Strings.BotRedeem, key, $"{result}/{purchaseResultDetail}{(!string.IsNullOrEmpty(balanceText) ? $"/{balanceText}" : "")}")); bool rateLimited = false; bool redeemed = false; - switch (result.PurchaseResultDetail) { + switch (purchaseResultDetail) { case EPurchaseResultDetail.AccountLocked: case EPurchaseResultDetail.AlreadyPurchased: case EPurchaseResultDetail.CannotRedeemCodeFromClient: @@ -3491,7 +3494,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable { break; default: - ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(result.PurchaseResultDetail), result.PurchaseResultDetail)); + ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(purchaseResultDetail), purchaseResultDetail)); break; } @@ -3509,7 +3512,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable { name = string.Join(", ", items.Values); } - string logEntry = $"{name}{DefaultBackgroundKeysRedeemerSeparator}[{result.PurchaseResultDetail}]{(items?.Count > 0 ? $"{DefaultBackgroundKeysRedeemerSeparator}{string.Join(", ", items)}" : "")}{DefaultBackgroundKeysRedeemerSeparator}{key}"; + string logEntry = $"{name}{DefaultBackgroundKeysRedeemerSeparator}[{purchaseResultDetail}]{(items?.Count > 0 ? $"{DefaultBackgroundKeysRedeemerSeparator}{string.Join(", ", items)}" : "")}{DefaultBackgroundKeysRedeemerSeparator}{key}"; string filePath = GetFilePath(redeemed ? EFileType.KeysToRedeemUsed : EFileType.KeysToRedeemUnused); diff --git a/ArchiSteamFarm/Steam/Integration/ArchiHandler.cs b/ArchiSteamFarm/Steam/Integration/ArchiHandler.cs index d4d66b0a3..a0229c83c 100644 --- a/ArchiSteamFarm/Steam/Integration/ArchiHandler.cs +++ b/ArchiSteamFarm/Steam/Integration/ArchiHandler.cs @@ -45,6 +45,7 @@ public sealed class ArchiHandler : ClientMsgHandler { private readonly SteamUnifiedMessages.UnifiedService UnifiedEconService; private readonly SteamUnifiedMessages.UnifiedService UnifiedFriendMessagesService; private readonly SteamUnifiedMessages.UnifiedService UnifiedPlayerService; + private readonly SteamUnifiedMessages.UnifiedService UnifiedStoreService; private readonly SteamUnifiedMessages.UnifiedService UnifiedTwoFactorService; internal DateTime LastPacketReceived { get; private set; } @@ -59,6 +60,7 @@ public sealed class ArchiHandler : ClientMsgHandler { UnifiedEconService = steamUnifiedMessages.CreateService(); UnifiedFriendMessagesService = steamUnifiedMessages.CreateService(); UnifiedPlayerService = steamUnifiedMessages.CreateService(); + UnifiedStoreService = steamUnifiedMessages.CreateService(); UnifiedTwoFactorService = steamUnifiedMessages.CreateService(); } @@ -631,7 +633,7 @@ public sealed class ArchiHandler : ClientMsgHandler { } } - internal async Task RedeemKey(string key) { + internal async Task RedeemKey(string key) { if (string.IsNullOrEmpty(key)) { throw new ArgumentNullException(nameof(key)); } @@ -644,20 +646,23 @@ public sealed class ArchiHandler : ClientMsgHandler { return null; } - ClientMsgProtobuf request = new(EMsg.ClientRegisterKey) { - SourceJobID = Client.GetNextJobID(), - Body = { key = key } + CStore_RegisterCDKey_Request request = new() { + activation_code = key, + is_request_from_client = true }; - Client.Send(request); + SteamUnifiedMessages.ServiceMethodResponse response; try { - return await new AsyncJob(Client, request.SourceJobID).ToLongRunningTask().ConfigureAwait(false); + response = await UnifiedStoreService.SendMessage(x => x.RegisterCDKey(request)).ToLongRunningTask().ConfigureAwait(false); } catch (Exception e) { - ArchiLogger.LogGenericException(e); + ArchiLogger.LogGenericWarningException(e); return null; } + + // We want to deserialize the response even with failed EResult + return response.GetDeserializedResponse(); } internal void RequestItemAnnouncements() { diff --git a/ArchiSteamFarm/Steam/Integration/SteamUtilities.cs b/ArchiSteamFarm/Steam/Integration/SteamUtilities.cs index edc4565e1..c1585856e 100644 --- a/ArchiSteamFarm/Steam/Integration/SteamUtilities.cs +++ b/ArchiSteamFarm/Steam/Integration/SteamUtilities.cs @@ -20,7 +20,6 @@ // limitations under the License. using System; -using System.Collections.Generic; using System.Globalization; using ArchiSteamFarm.Core; using ArchiSteamFarm.Localization; @@ -70,46 +69,4 @@ internal static class SteamUtilities { return result; } - - internal static Dictionary? ParseItems(this SteamApps.PurchaseResponseCallback callback) { - ArgumentNullException.ThrowIfNull(callback); - - List lineItems = callback.PurchaseReceiptInfo["lineitems"].Children; - - if (lineItems.Count == 0) { - return null; - } - - Dictionary result = new(lineItems.Count); - - foreach (KeyValue lineItem in lineItems) { - uint packageID = lineItem["PackageID"].AsUnsignedInteger(); - - if (packageID == 0) { - // Coupons have PackageID of -1 (don't ask me why) - // We'll use ItemAppID in this case - packageID = lineItem["ItemAppID"].AsUnsignedInteger(); - - if (packageID == 0) { - ASF.ArchiLogger.LogNullError(packageID); - - return null; - } - } - - string? gameName = lineItem["ItemDescription"].AsString(); - - if (string.IsNullOrEmpty(gameName)) { - ASF.ArchiLogger.LogNullError(gameName); - - return null; - } - - // Apparently steam expects client to decode sent HTML - gameName = Uri.UnescapeDataString(gameName); - result[packageID] = gameName; - } - - return result; - } } diff --git a/ArchiSteamFarm/Steam/Interaction/Actions.cs b/ArchiSteamFarm/Steam/Interaction/Actions.cs index 77c90d7dc..9a45484e1 100644 --- a/ArchiSteamFarm/Steam/Interaction/Actions.cs +++ b/ArchiSteamFarm/Steam/Interaction/Actions.cs @@ -39,6 +39,7 @@ using ArchiSteamFarm.Storage; using ArchiSteamFarm.Web; using JetBrains.Annotations; using SteamKit2; +using SteamKit2.Internal; namespace ArchiSteamFarm.Steam.Interaction; @@ -278,7 +279,7 @@ public sealed class Actions : IAsyncDisposable, IDisposable { } [PublicAPI] - public async Task RedeemKey(string key) { + public async Task RedeemKey(string key) { await LimitGiftsRequestsAsync().ConfigureAwait(false); return await Bot.ArchiHandler.RedeemKey(key).ConfigureAwait(false); diff --git a/ArchiSteamFarm/Steam/Interaction/Commands.cs b/ArchiSteamFarm/Steam/Interaction/Commands.cs index 98afaf705..50f3326c6 100644 --- a/ArchiSteamFarm/Steam/Interaction/Commands.cs +++ b/ArchiSteamFarm/Steam/Interaction/Commands.cs @@ -39,6 +39,7 @@ using ArchiSteamFarm.Steam.Storage; using ArchiSteamFarm.Storage; using JetBrains.Annotations; using SteamKit2; +using SteamKit2.Internal; namespace ArchiSteamFarm.Steam.Interaction; @@ -2534,11 +2535,19 @@ public sealed class Commands { if (!skipRequest) { // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework - SteamApps.PurchaseResponseCallback? redeemResult = await currentBot.Actions.RedeemKey(key!).ConfigureAwait(false); + CStore_RegisterCDKey_Response? redeemResult = await currentBot.Actions.RedeemKey(key!).ConfigureAwait(false); - result = redeemResult?.Result ?? EResult.Timeout; - purchaseResultDetail = redeemResult?.PurchaseResultDetail ?? EPurchaseResultDetail.Timeout; - items = redeemResult?.ParseItems(); + if (redeemResult != null) { + result = (EResult) redeemResult.purchase_receipt_info.purchase_status; + purchaseResultDetail = (EPurchaseResultDetail) redeemResult.purchase_result_details; + + if (redeemResult.purchase_receipt_info.line_items.Count > 0) { + items = redeemResult.purchase_receipt_info.line_items.ToDictionary(static lineItem => lineItem.packageid, static lineItem => lineItem.line_item_description); + } + } else { + result = EResult.Timeout; + purchaseResultDetail = EPurchaseResultDetail.Timeout; + } } if ((result == EResult.Timeout) || (purchaseResultDetail == EPurchaseResultDetail.Timeout)) { @@ -2618,9 +2627,9 @@ public sealed class Commands { foreach (Bot innerBot in Bot.Bots.Where(bot => (bot.Value != currentBot) && (!redeemFlags.HasFlag(ERedeemFlags.SkipInitial) || (bot.Value != Bot)) && !triedBots.Contains(bot.Value) && !rateLimitedBots.Contains(bot.Value) && bot.Value.IsConnectedAndLoggedOn && ((access >= EAccess.Owner) || ((steamID != 0) && (bot.Value.GetAccess(steamID) >= EAccess.Operator))) && ((items.Count == 0) || items.Keys.Any(packageID => !bot.Value.OwnedPackageIDs.ContainsKey(packageID)))).OrderBy(static bot => bot.Key, Bot.BotsComparer).Select(static bot => bot.Value)) { // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework - SteamApps.PurchaseResponseCallback? redeemResult = await innerBot.Actions.RedeemKey(key!).ConfigureAwait(false); + CStore_RegisterCDKey_Response? redeemResponse = await innerBot.Actions.RedeemKey(key!).ConfigureAwait(false); - if (redeemResult == null) { + if (redeemResponse == null) { response.AppendLine(FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotRedeem, key, $"{EResult.Timeout}/{EPurchaseResultDetail.Timeout}"), innerBot.BotName)); continue; @@ -2628,7 +2637,10 @@ public sealed class Commands { triedBots.Add(innerBot); - switch (redeemResult.PurchaseResultDetail) { + EResult redeemResult = (EResult) redeemResponse.purchase_receipt_info.purchase_status; + EPurchaseResultDetail redeemPurchaseResult = (EPurchaseResultDetail) redeemResponse.purchase_result_details; + + switch (redeemPurchaseResult) { case EPurchaseResultDetail.BadActivationCode: case EPurchaseResultDetail.DuplicateActivationCode: case EPurchaseResultDetail.NoDetail: // OK @@ -2645,9 +2657,9 @@ public sealed class Commands { break; } - Dictionary? redeemItems = redeemResult.ParseItems(); + Dictionary? redeemItems = redeemResponse.purchase_receipt_info.line_items.Count > 0 ? redeemResponse.purchase_receipt_info.line_items.ToDictionary(static lineItem => lineItem.packageid, static lineItem => lineItem.line_item_description) : null; - response.AppendLine(FormatBotResponse(redeemItems?.Count > 0 ? string.Format(CultureInfo.CurrentCulture, Strings.BotRedeemWithItems, key, $"{redeemResult.Result}/{redeemResult.PurchaseResultDetail}", string.Join(", ", redeemItems)) : string.Format(CultureInfo.CurrentCulture, Strings.BotRedeem, key, $"{redeemResult.Result}/{redeemResult.PurchaseResultDetail}"), innerBot.BotName)); + response.AppendLine(FormatBotResponse(redeemItems?.Count > 0 ? string.Format(CultureInfo.CurrentCulture, Strings.BotRedeemWithItems, key, $"{redeemResult}/{redeemPurchaseResult}", string.Join(", ", redeemItems)) : string.Format(CultureInfo.CurrentCulture, Strings.BotRedeem, key, $"{redeemResult}/{redeemPurchaseResult}"), innerBot.BotName)); if (alreadyHandled) { break;