This commit is contained in:
Archi
2023-11-05 23:22:15 +01:00
parent b46e936ac6
commit 3a9e9de596
4 changed files with 87 additions and 12 deletions

View File

@@ -1214,5 +1214,11 @@ namespace ArchiSteamFarm.Localization {
return ResourceManager.GetString("WarningNoLicense", resourceCulture); return ResourceManager.GetString("WarningNoLicense", resourceCulture);
} }
} }
public static string WarningRegionRestrictedPackage {
get {
return ResourceManager.GetString("WarningRegionRestrictedPackage", resourceCulture);
}
}
} }
} }

View File

@@ -748,4 +748,8 @@ Process uptime: {1}</value>
<value>You've attempted to use paid feature {0} but you don't have a valid LicenseID set in the ASF global config. Please review your configuration, as the functionality won't work without additional details.</value> <value>You've attempted to use paid feature {0} but you don't have a valid LicenseID set in the ASF global config. Please review your configuration, as the functionality won't work without additional details.</value>
<comment>{0} will be replaced by feature name (e.g. MatchActively)</comment> <comment>{0} will be replaced by feature name (e.g. MatchActively)</comment>
</data> </data>
<data name="WarningRegionRestrictedPackage" xml:space="preserve">
<value>ASF is unable to play app {0} as it has region-related restriction for {1} country that lasts until {2}.</value>
<comment>{0} will be replaced by app ID (number), {1} will be replaced by short country code (string, such as "PL"), {2} will be replaced by human-readable date (string).</comment>
</data>
</root> </root>

View File

@@ -69,6 +69,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
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) 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)
private const byte MinimumAccessTokenValidityMinutes = 10; private const byte MinimumAccessTokenValidityMinutes = 10;
private const byte RedeemCooldownInHours = 1; // 1 hour since first redeem attempt, this is a limitation enforced by Steam private const byte RedeemCooldownInHours = 1; // 1 hour since first redeem attempt, this is a limitation enforced by Steam
private const byte RegionRestrictionPlayableBlockMonths = 3;
[PublicAPI] [PublicAPI]
public static IReadOnlyDictionary<string, Bot>? BotsReadOnly => Bots; public static IReadOnlyDictionary<string, Bot>? BotsReadOnly => Bots;
@@ -241,6 +242,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
private Timer? ConnectionFailureTimer; private Timer? ConnectionFailureTimer;
private bool FirstTradeSent; private bool FirstTradeSent;
private Timer? GamesRedeemerInBackgroundTimer; private Timer? GamesRedeemerInBackgroundTimer;
private string? IPCountryCode;
private EResult LastLogOnResult; private EResult LastLogOnResult;
private DateTime LastLogonSessionReplaced; private DateTime LastLogonSessionReplaced;
private bool LibraryLocked; private bool LibraryLocked;
@@ -1120,6 +1122,56 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
} }
} }
// Check region restrictions
if (!string.IsNullOrEmpty(IPCountryCode)) {
DateTime? regionRestrictedUntil = null;
DateTime safePlayableBefore = DateTime.UtcNow.AddMonths(-RegionRestrictionPlayableBlockMonths);
foreach (uint packageID in packageIDs) {
if (!OwnedPackageIDs.TryGetValue(packageID, out (EPaymentMethod PaymentMethod, DateTime TimeCreated) ownedPackageData)) {
// We don't own that packageID, keep checking
continue;
}
if (ownedPackageData.TimeCreated < safePlayableBefore) {
// Our package is older than required, this is playable
regionRestrictedUntil = null;
break;
}
// We've got a package that was activated recently, we should check if we have any playable restrictions on it
if ((ASF.GlobalDatabase == null) || !ASF.GlobalDatabase.PackagesDataReadOnly.TryGetValue(packageID, out PackageData? packageData)) {
// No information about that package, try again later
return (0, DateTime.MaxValue, true);
}
if ((packageData.ProhibitRunInCountries == null) || packageData.ProhibitRunInCountries.IsEmpty) {
// No restrictions, we're good to go
regionRestrictedUntil = null;
break;
}
if (packageData.ProhibitRunInCountries.Contains(IPCountryCode)) {
// We are restricted by this package, we can only be saved by another package that is not restricted
DateTime regionRestrictedUntilPackage = ownedPackageData.TimeCreated.AddMonths(RegionRestrictionPlayableBlockMonths);
if (!regionRestrictedUntil.HasValue || (regionRestrictedUntilPackage < regionRestrictedUntil.Value)) {
regionRestrictedUntil = regionRestrictedUntilPackage;
}
}
}
if (regionRestrictedUntil.HasValue) {
// We can't play this game for now
ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningRegionRestrictedPackage, appID, IPCountryCode, regionRestrictedUntil.Value));
return (0, regionRestrictedUntil.Value, false);
}
}
SteamApps.PICSTokensCallback? tokenCallback = null; SteamApps.PICSTokensCallback? tokenCallback = null;
for (byte i = 0; (i < WebBrowser.MaxTries) && (tokenCallback == null) && IsConnectedAndLoggedOn; i++) { for (byte i = 0; (i < WebBrowser.MaxTries) && (tokenCallback == null) && IsConnectedAndLoggedOn; i++) {
@@ -1300,33 +1352,38 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
if (productInfo.KeyValues == KeyValue.Invalid) { if (productInfo.KeyValues == KeyValue.Invalid) {
ArchiLogger.LogNullError(productInfo); ArchiLogger.LogNullError(productInfo);
return null; continue;
} }
uint changeNumber = productInfo.ChangeNumber; uint changeNumber = productInfo.ChangeNumber;
HashSet<uint>? appIDs = null; HashSet<uint>? appIDs = null;
try { KeyValue appIDsKv = productInfo.KeyValues["appids"];
KeyValue appIDsKv = productInfo.KeyValues["appids"];
if (appIDsKv == KeyValue.Invalid) {
continue;
}
if (appIDsKv != KeyValue.Invalid) {
appIDs = new HashSet<uint>(appIDsKv.Children.Count); appIDs = new HashSet<uint>(appIDsKv.Children.Count);
foreach (string? appIDText in appIDsKv.Children.Select(static app => app.Value)) { foreach (string? appIDText in appIDsKv.Children.Select(static app => app.Value)) {
if (!uint.TryParse(appIDText, out uint appID) || (appID == 0)) { if (!uint.TryParse(appIDText, out uint appID) || (appID == 0)) {
ArchiLogger.LogNullError(appID); ArchiLogger.LogNullError(appID);
return null; continue;
} }
appIDs.Add(appID); appIDs.Add(appID);
} }
} finally {
result[productInfo.ID] = new PackageData(changeNumber, validUntil, appIDs?.ToImmutableHashSet());
} }
string[]? prohibitRunInCountries = null;
string? prohibitRunInCountriesText = productInfo.KeyValues["extended"]["prohibitrunincountries"].AsString();
if (!string.IsNullOrEmpty(prohibitRunInCountriesText)) {
prohibitRunInCountries = prohibitRunInCountriesText.Split(' ', StringSplitOptions.RemoveEmptyEntries);
}
result[productInfo.ID] = new PackageData(changeNumber, validUntil, appIDs?.ToImmutableHashSet(), prohibitRunInCountries?.ToImmutableHashSet(StringComparer.Ordinal));
} }
return result; return result;
@@ -2350,7 +2407,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
} }
AccountFlags = EAccountFlags.NormalUser; AccountFlags = EAccountFlags.NormalUser;
AvatarHash = Nickname = null; AvatarHash = IPCountryCode = Nickname = null;
MasterChatGroupID = 0; MasterChatGroupID = 0;
RequiredInput = ASF.EUserInputType.None; RequiredInput = ASF.EUserInputType.None;
WalletBalance = 0; WalletBalance = 0;
@@ -3131,6 +3188,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
} }
AccountFlags = callback.AccountFlags; AccountFlags = callback.AccountFlags;
IPCountryCode = callback.IPCountryCode;
SteamID = callback.ClientSteamID ?? throw new InvalidOperationException(nameof(callback.ClientSteamID)); SteamID = callback.ClientSteamID ?? throw new InvalidOperationException(nameof(callback.ClientSteamID));
ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.BotLoggedOn, $"{SteamID}{(!string.IsNullOrEmpty(callback.VanityURL) ? $"/{callback.VanityURL}" : "")}")); ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.BotLoggedOn, $"{SteamID}{(!string.IsNullOrEmpty(callback.VanityURL) ? $"/{callback.VanityURL}" : "")}"));

View File

@@ -33,10 +33,13 @@ public sealed class PackageData {
[JsonProperty(Required = Required.Always)] [JsonProperty(Required = Required.Always)]
public uint ChangeNumber { get; private set; } public uint ChangeNumber { get; private set; }
[JsonProperty]
public ImmutableHashSet<string>? ProhibitRunInCountries { get; private set; }
[JsonProperty(Required = Required.Always)] [JsonProperty(Required = Required.Always)]
public DateTime ValidUntil { get; private set; } public DateTime ValidUntil { get; private set; }
internal PackageData(uint changeNumber, DateTime validUntil, ImmutableHashSet<uint>? appIDs = null) { internal PackageData(uint changeNumber, DateTime validUntil, ImmutableHashSet<uint>? appIDs = null, ImmutableHashSet<string>? prohibitRunInCountries = null) {
if (changeNumber == 0) { if (changeNumber == 0) {
throw new ArgumentOutOfRangeException(nameof(changeNumber)); throw new ArgumentOutOfRangeException(nameof(changeNumber));
} }
@@ -48,6 +51,7 @@ public sealed class PackageData {
ChangeNumber = changeNumber; ChangeNumber = changeNumber;
ValidUntil = validUntil; ValidUntil = validUntil;
AppIDs = appIDs; AppIDs = appIDs;
ProhibitRunInCountries = prohibitRunInCountries;
} }
[JsonConstructor] [JsonConstructor]
@@ -55,4 +59,7 @@ public sealed class PackageData {
[UsedImplicitly] [UsedImplicitly]
public bool ShouldSerializeAppIDs() => AppIDs is { IsEmpty: false }; public bool ShouldSerializeAppIDs() => AppIDs is { IsEmpty: false };
[UsedImplicitly]
public bool ShouldSerializeProhibitRunInCountries() => ProhibitRunInCountries is { IsEmpty: false };
} }