Skip to content

Commit

Permalink
Closes #3058
Browse files Browse the repository at this point in the history
  • Loading branch information
JustArchi committed Nov 1, 2023
1 parent 09804a5 commit ad4c81a
Show file tree
Hide file tree
Showing 6 changed files with 56 additions and 78 deletions.
12 changes: 6 additions & 6 deletions ArchiSteamFarm/IPC/Controllers/Api/BotController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
using ArchiSteamFarm.Steam.Storage;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
using SteamKit2;
using SteamKit2.Internal;

namespace ArchiSteamFarm.IPC.Controllers.Api;

Expand Down Expand Up @@ -313,7 +313,7 @@ public async Task<ActionResult<GenericResponse>> PausePost(string botNames, [Fro
/// </remarks>
[Consumes("application/json")]
[HttpPost("{botNames:required}/Redeem")]
[ProducesResponseType(typeof(GenericResponse<IReadOnlyDictionary<string, IReadOnlyDictionary<string, SteamApps.PurchaseResponseCallback>>>), (int) HttpStatusCode.OK)]
[ProducesResponseType(typeof(GenericResponse<IReadOnlyDictionary<string, IReadOnlyDictionary<string, CStore_RegisterCDKey_Response>>>), (int) HttpStatusCode.OK)]
[ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.BadRequest)]
public async Task<ActionResult<GenericResponse>> RedeemPost(string botNames, [FromBody] BotRedeemRequest request) {
if (string.IsNullOrEmpty(botNames)) {
Expand All @@ -332,22 +332,22 @@ public async Task<ActionResult<GenericResponse>> RedeemPost(string botNames, [Fr
return BadRequest(new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)));
}

IList<SteamApps.PurchaseResponseCallback?> results = await Utilities.InParallel(bots.Select(bot => request.KeysToRedeem.Select(key => bot.Actions.RedeemKey(key))).SelectMany(static task => task)).ConfigureAwait(false);
IList<CStore_RegisterCDKey_Response?> results = await Utilities.InParallel(bots.Select(bot => request.KeysToRedeem.Select(key => bot.Actions.RedeemKey(key))).SelectMany(static task => task)).ConfigureAwait(false);

Dictionary<string, IReadOnlyDictionary<string, SteamApps.PurchaseResponseCallback?>> result = new(bots.Count, Bot.BotsComparer);
Dictionary<string, IReadOnlyDictionary<string, CStore_RegisterCDKey_Response?>> result = new(bots.Count, Bot.BotsComparer);

int count = 0;

foreach (Bot bot in bots) {
Dictionary<string, SteamApps.PurchaseResponseCallback?> responses = new(request.KeysToRedeem.Count, StringComparer.Ordinal);
Dictionary<string, CStore_RegisterCDKey_Response?> responses = new(request.KeysToRedeem.Count, StringComparer.Ordinal);
result[bot.BotName] = responses;

foreach (string key in request.KeysToRedeem) {
responses[key] = results[count++];
}
}

return Ok(new GenericResponse<IReadOnlyDictionary<string, IReadOnlyDictionary<string, SteamApps.PurchaseResponseCallback?>>>(result.Values.SelectMany(static responses => responses.Values).All(static value => value != null), result));
return Ok(new GenericResponse<IReadOnlyDictionary<string, IReadOnlyDictionary<string, CStore_RegisterCDKey_Response?>>>(result.Values.SelectMany(static responses => responses.Values).All(static value => value != null), result));
}

/// <summary>
Expand Down
27 changes: 15 additions & 12 deletions ArchiSteamFarm/Steam/Bot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3441,37 +3441,40 @@ private async void RedeemGamesInBackground(object? state = null) {
}

// 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<uint, string>? items = result.ParseItems();
Dictionary<uint, string>? 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:
Expand All @@ -3491,7 +3494,7 @@ private async void RedeemGamesInBackground(object? state = null) {

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;
}
Expand All @@ -3509,7 +3512,7 @@ private async void RedeemGamesInBackground(object? state = null) {
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);

Expand Down
19 changes: 12 additions & 7 deletions ArchiSteamFarm/Steam/Integration/ArchiHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public sealed class ArchiHandler : ClientMsgHandler {
private readonly SteamUnifiedMessages.UnifiedService<IEcon> UnifiedEconService;
private readonly SteamUnifiedMessages.UnifiedService<IFriendMessages> UnifiedFriendMessagesService;
private readonly SteamUnifiedMessages.UnifiedService<IPlayer> UnifiedPlayerService;
private readonly SteamUnifiedMessages.UnifiedService<IStore> UnifiedStoreService;
private readonly SteamUnifiedMessages.UnifiedService<ITwoFactor> UnifiedTwoFactorService;

internal DateTime LastPacketReceived { get; private set; }
Expand All @@ -59,6 +60,7 @@ internal ArchiHandler(ArchiLogger archiLogger, SteamUnifiedMessages steamUnified
UnifiedEconService = steamUnifiedMessages.CreateService<IEcon>();
UnifiedFriendMessagesService = steamUnifiedMessages.CreateService<IFriendMessages>();
UnifiedPlayerService = steamUnifiedMessages.CreateService<IPlayer>();
UnifiedStoreService = steamUnifiedMessages.CreateService<IStore>();
UnifiedTwoFactorService = steamUnifiedMessages.CreateService<ITwoFactor>();
}

Expand Down Expand Up @@ -631,7 +633,7 @@ internal async Task PlayGames(IReadOnlyCollection<uint> gameIDs, string? gameNam
}
}

internal async Task<SteamApps.PurchaseResponseCallback?> RedeemKey(string key) {
internal async Task<CStore_RegisterCDKey_Response?> RedeemKey(string key) {
if (string.IsNullOrEmpty(key)) {
throw new ArgumentNullException(nameof(key));
}
Expand All @@ -644,20 +646,23 @@ internal async Task PlayGames(IReadOnlyCollection<uint> gameIDs, string? gameNam
return null;
}

ClientMsgProtobuf<CMsgClientRegisterKey> 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<SteamApps.PurchaseResponseCallback>(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<CStore_RegisterCDKey_Response>();
}

internal void RequestItemAnnouncements() {
Expand Down
43 changes: 0 additions & 43 deletions ArchiSteamFarm/Steam/Integration/SteamUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
// limitations under the License.

using System;
using System.Collections.Generic;
using System.Globalization;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Localization;
Expand Down Expand Up @@ -70,46 +69,4 @@ internal static class SteamUtilities {

return result;
}

internal static Dictionary<uint, string>? ParseItems(this SteamApps.PurchaseResponseCallback callback) {
ArgumentNullException.ThrowIfNull(callback);

List<KeyValue> lineItems = callback.PurchaseReceiptInfo["lineitems"].Children;

if (lineItems.Count == 0) {
return null;
}

Dictionary<uint, string> 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;
}
}
3 changes: 2 additions & 1 deletion ArchiSteamFarm/Steam/Interaction/Actions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
using ArchiSteamFarm.Web;
using JetBrains.Annotations;
using SteamKit2;
using SteamKit2.Internal;

namespace ArchiSteamFarm.Steam.Interaction;

Expand Down Expand Up @@ -278,7 +279,7 @@ public static string Hash(ArchiCryptoHelper.EHashingMethod hashingMethod, string
}

[PublicAPI]
public async Task<SteamApps.PurchaseResponseCallback?> RedeemKey(string key) {
public async Task<CStore_RegisterCDKey_Response?> RedeemKey(string key) {
await LimitGiftsRequestsAsync().ConfigureAwait(false);

return await Bot.ArchiHandler.RedeemKey(key).ConfigureAwait(false);
Expand Down
30 changes: 21 additions & 9 deletions ArchiSteamFarm/Steam/Interaction/Commands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
using ArchiSteamFarm.Storage;
using JetBrains.Annotations;
using SteamKit2;
using SteamKit2.Internal;

namespace ArchiSteamFarm.Steam.Interaction;

Expand Down Expand Up @@ -2534,11 +2535,19 @@ internal void OnNewLicenseList() {

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)) {
Expand Down Expand Up @@ -2618,17 +2627,20 @@ internal void OnNewLicenseList() {

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;
}

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
Expand All @@ -2645,9 +2657,9 @@ internal void OnNewLicenseList() {
break;
}

Dictionary<uint, string>? redeemItems = redeemResult.ParseItems();
Dictionary<uint, string>? 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;
Expand Down

0 comments on commit ad4c81a

Please sign in to comment.