* Closes #3415

* Misc

* Refresh tokens always for non-listed packages
This commit is contained in:
Łukasz Domeradzki
2025-05-11 21:36:16 +02:00
committed by GitHub
parent a19611c3ae
commit 10abfb847f
7 changed files with 152 additions and 9 deletions

View File

@@ -25,7 +25,6 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper;
internal static class SharedInfo {
internal const byte ApiVersion = 2;
internal const byte AppInfosPerSingleRequest = byte.MaxValue;
internal const byte HoursBetweenUploads = 24;
internal const byte MaximumHoursBetweenRefresh = 8; // Per single bot account, makes sense to be 2 or 3 times less than MinimumHoursBetweenUploads
internal const byte MaximumMinutesBeforeFirstUpload = 60; // Must be greater or equal to MinimumMinutesBeforeFirstUpload

View File

@@ -360,7 +360,7 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
bot.ArchiLogger.LogGenericInfo(Strings.FormatBotRetrievingTotalAppAccessTokens(appIDsToRefresh.Count));
HashSet<uint> appIDsThisRound = new(Math.Min(appIDsToRefresh.Count, SharedInfo.AppInfosPerSingleRequest));
HashSet<uint> appIDsThisRound = new(Math.Min(appIDsToRefresh.Count, Bot.EntriesPerSinglePICSRequest));
using (HashSet<uint>.Enumerator enumerator = appIDsToRefresh.GetEnumerator()) {
while (true) {
@@ -368,7 +368,7 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
return;
}
while ((appIDsThisRound.Count < SharedInfo.AppInfosPerSingleRequest) && enumerator.MoveNext()) {
while ((appIDsThisRound.Count < Bot.EntriesPerSinglePICSRequest) && enumerator.MoveNext()) {
appIDsThisRound.Add(enumerator.Current);
}
@@ -409,7 +409,7 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
return;
}
while ((appIDsThisRound.Count < SharedInfo.AppInfosPerSingleRequest) && enumerator.MoveNext()) {
while ((appIDsThisRound.Count < Bot.EntriesPerSinglePICSRequest) && enumerator.MoveNext()) {
appIDsThisRound.Add(enumerator.Current);
}

View File

@@ -30,6 +30,7 @@ using System.Collections.Immutable;
using System.Collections.Specialized;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -65,9 +66,11 @@ namespace ArchiSteamFarm.Steam;
public sealed class Bot : IAsyncDisposable, IDisposable {
internal const ushort CallbackSleep = 500; // In milliseconds
internal const byte EntriesPerSinglePICSRequest = byte.MaxValue;
internal const byte MinCardsPerBadge = 5;
private const char DefaultBackgroundKeysRedeemerSeparator = '\t';
private const byte ExtraStorePackagesValidForDays = 7;
private const byte LoginCooldownInMinutes = 25; // Captcha disappears after around 20 minutes, so we make it 25
private const uint LoginID = 1242; // This must be the same for all ASF bots and all ASF processes
private const byte MaxLoginFailures = WebBrowser.MaxTries; // Max login failures in a row before we determine that our credentials are invalid (because Steam wrongly returns those, of course)course)
@@ -2100,6 +2103,24 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
UnpackBoosterPacksSemaphore.Dispose();
}
private async Task ExtendWithStoreData([SuppressMessage("ReSharper", "SuggestBaseTypeForParameter")] Dictionary<uint, LicenseData> ownedPackages, HashSet<uint> allPackages, Dictionary<uint, uint> packagesToRefresh) {
ArgumentNullException.ThrowIfNull(ownedPackages);
ArgumentNullException.ThrowIfNull(allPackages);
ArgumentNullException.ThrowIfNull(packagesToRefresh);
if (BotDatabase.ExtraStorePackagesRefreshedAt.AddDays(ExtraStorePackagesValidForDays) < DateTime.UtcNow) {
await RefreshStoreData(allPackages, packagesToRefresh).ConfigureAwait(false);
}
foreach (uint packageID in BotDatabase.ExtraStorePackages) {
ownedPackages[packageID] = new LicenseData {
PackageID = packageID,
PaymentMethod = EPaymentMethod.None,
TimeCreated = DateTime.UnixEpoch
};
}
}
private async Task<Dictionary<string, string>?> GetKeysFromFile(string filePath) {
ArgumentException.ThrowIfNullOrEmpty(filePath);
@@ -3171,15 +3192,22 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
Commands.OnNewLicenseList();
Dictionary<uint, LicenseData> ownedPackages = new();
Dictionary<uint, LicenseData> ownedPackages = [];
HashSet<uint> allPackages = [];
Dictionary<uint, ulong> packageAccessTokens = new();
Dictionary<uint, uint> packagesToRefresh = new();
Dictionary<uint, ulong> packageAccessTokens = [];
Dictionary<uint, uint> packagesToRefresh = [];
bool hasNewEntries = false;
// We want to record only the most relevant entry from non-borrowed games, therefore we also apply ordering here
foreach (SteamApps.LicenseListCallback.License license in callback.LicenseList.Where(static license => !license.LicenseFlags.HasFlag(ELicenseFlags.Borrowed)).OrderByDescending(static license => license.TimeCreated).Where(license => !ownedPackages.ContainsKey(license.PackageID))) {
foreach (SteamApps.LicenseListCallback.License license in callback.LicenseList.OrderByDescending(static license => license.TimeCreated)) {
allPackages.Add(license.PackageID);
if (license.LicenseFlags.HasFlag(ELicenseFlags.Borrowed) || ownedPackages.ContainsKey(license.PackageID)) {
continue;
}
ownedPackages[license.PackageID] = new LicenseData {
PackageID = license.PackageID,
PaymentMethod = license.PaymentMethod,
@@ -3200,6 +3228,8 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
}
}
await ExtendWithStoreData(ownedPackages, allPackages, packagesToRefresh).ConfigureAwait(false);
OwnedPackages = ownedPackages.ToFrozenDictionary();
if (packageAccessTokens.Count > 0) {
@@ -3687,6 +3717,45 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
}
}
private async Task RefreshStoreData([SuppressMessage("ReSharper", "SuggestBaseTypeForParameter")] HashSet<uint> allPackages, [SuppressMessage("ReSharper", "SuggestBaseTypeForParameter")] Dictionary<uint, uint> packagesToRefresh) {
ArgumentNullException.ThrowIfNull(allPackages);
ArgumentNullException.ThrowIfNull(packagesToRefresh);
if (ASF.GlobalDatabase == null) {
throw new InvalidOperationException(nameof(ASF.GlobalDatabase));
}
StoreUserData? storeData = await ArchiWebHandler.GetStoreUserData().ConfigureAwait(false);
if (storeData == null) {
return;
}
BotDatabase.ExtraStorePackages.ReplaceWith(storeData.OwnedPackages.Where(packageID => !allPackages.Contains(packageID)));
BotDatabase.ExtraStorePackagesRefreshedAt = DateTime.UtcNow;
foreach (uint[] packageIDs in BotDatabase.ExtraStorePackages.Chunk(EntriesPerSinglePICSRequest)) {
try {
SteamApps.PICSTokensCallback accessTokens = await SteamApps.PICSGetAccessTokens([], packageIDs);
if (accessTokens.PackageTokens.Count > 0) {
ASF.GlobalDatabase.RefreshPackageAccessTokens(accessTokens.PackageTokens);
}
} catch (Exception e) {
ArchiLogger.LogGenericWarningException(e);
}
}
// Wait up to 5 seconds for initialization, we can work with any change number, although non-zero is preferred
for (byte i = 0; (i < WebBrowser.MaxTries) && (SteamPICSChanges.LastChangeNumber == 0); i++) {
await Task.Delay(1000).ConfigureAwait(false);
}
foreach (uint packageID in BotDatabase.ExtraStorePackages) {
packagesToRefresh.Add(packageID, SteamPICSChanges.LastChangeNumber);
}
}
private async Task ResetGamesPlayed() {
if (!IsConnectedAndLoggedOn || CardsFarmer.NowFarming) {
return;

View File

@@ -0,0 +1,39 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2025 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// |
// http://www.apache.org/licenses/LICENSE-2.0
// |
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
namespace ArchiSteamFarm.Steam.Data;
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
internal sealed class StoreUserData {
[JsonInclude]
[JsonPropertyName("rgOwnedPackages")]
[JsonRequired]
internal ImmutableHashSet<uint> OwnedPackages { get; private init; } = ImmutableHashSet<uint>.Empty;
[JsonConstructor]
private StoreUserData() { }
}

View File

@@ -1875,6 +1875,14 @@ public sealed class ArchiWebHandler : IDisposable {
return response?.Content;
}
internal async Task<StoreUserData?> GetStoreUserData() {
Uri request = new(SteamStoreURL, "/dynamicstore/userdata?l=english");
ObjectResponse<StoreUserData>? response = await UrlGetToJsonObjectWithSession<StoreUserData>(request).ConfigureAwait(false);
return response?.Content;
}
internal async Task<byte?> GetTradeHoldDurationForTrade(ulong tradeID) {
ArgumentOutOfRangeException.ThrowIfZero(tradeID);

View File

@@ -35,12 +35,12 @@ namespace ArchiSteamFarm.Steam.Integration;
internal static class SteamPICSChanges {
private const byte RefreshTimerInMinutes = 5;
internal static uint LastChangeNumber { get; private set; }
internal static bool LiveUpdate { get; private set; }
private static readonly SemaphoreSlim RefreshSemaphore = new(1, 1);
private static readonly Timer RefreshTimer = new(RefreshChanges);
private static uint LastChangeNumber;
private static bool TimerAlreadySet;
internal static void Init(uint changeNumberToStartFrom) => LastChangeNumber = changeNumberToStartFrom;

View File

@@ -63,6 +63,23 @@ public sealed class BotDatabase : GenericDatabase {
}
}
[JsonDisallowNull]
[JsonInclude]
internal ConcurrentHashSet<uint> ExtraStorePackages { get; private init; } = [];
internal DateTime ExtraStorePackagesRefreshedAt {
get => BackingExtraStorePackagesRefreshedAt;
set {
if (BackingExtraStorePackagesRefreshedAt == value) {
return;
}
BackingExtraStorePackagesRefreshedAt = value;
Utilities.InBackground(Save);
}
}
[JsonDisallowNull]
[JsonInclude]
internal ConcurrentHashSet<uint> FarmingBlacklistAppIDs { get; private init; } = [];
@@ -129,6 +146,9 @@ public sealed class BotDatabase : GenericDatabase {
[JsonInclude]
private string? BackingAccessToken { get; set; }
[JsonInclude]
private DateTime BackingExtraStorePackagesRefreshedAt { get; set; }
[JsonInclude]
[JsonPropertyName($"_{nameof(MobileAuthenticator)}")]
private MobileAuthenticator? BackingMobileAuthenticator { get; set; }
@@ -151,6 +171,7 @@ public sealed class BotDatabase : GenericDatabase {
[JsonConstructor]
private BotDatabase() {
ExtraStorePackages.OnModified += OnObjectModified;
FarmingBlacklistAppIDs.OnModified += OnObjectModified;
FarmingPriorityQueueAppIDs.OnModified += OnObjectModified;
FarmingRiskyIgnoredAppIDs.OnModified += OnObjectModified;
@@ -188,6 +209,9 @@ public sealed class BotDatabase : GenericDatabase {
[UsedImplicitly]
public bool ShouldSerializeBackingAccessToken() => !string.IsNullOrEmpty(BackingAccessToken);
[UsedImplicitly]
public bool ShouldSerializeBackingExtraStorePackagesRefreshedAt() => BackingExtraStorePackagesRefreshedAt > DateTime.MinValue;
[UsedImplicitly]
public bool ShouldSerializeBackingMobileAuthenticator() => BackingMobileAuthenticator != null;
@@ -197,6 +221,9 @@ public sealed class BotDatabase : GenericDatabase {
[UsedImplicitly]
public bool ShouldSerializeBackingSteamGuardData() => !string.IsNullOrEmpty(BackingSteamGuardData);
[UsedImplicitly]
public bool ShouldSerializeExtraStorePackages() => ExtraStorePackages.Count > 0;
[UsedImplicitly]
public bool ShouldSerializeFarmingBlacklistAppIDs() => FarmingBlacklistAppIDs.Count > 0;
@@ -221,6 +248,7 @@ public sealed class BotDatabase : GenericDatabase {
protected override void Dispose(bool disposing) {
if (disposing) {
// Events we registered
ExtraStorePackages.OnModified -= OnObjectModified;
FarmingBlacklistAppIDs.OnModified -= OnObjectModified;
FarmingPriorityQueueAppIDs.OnModified -= OnObjectModified;
FarmingRiskyIgnoredAppIDs.OnModified -= OnObjectModified;