Rewrite AWH bits into ArchiCachable

This commit is contained in:
JustArchi
2018-12-12 22:19:52 +01:00
parent ec72962992
commit c7ef612bee
3 changed files with 257 additions and 155 deletions

View File

@@ -28,6 +28,7 @@ using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml; using System.Xml;
using ArchiSteamFarm.Helpers;
using ArchiSteamFarm.Json; using ArchiSteamFarm.Json;
using ArchiSteamFarm.Localization; using ArchiSteamFarm.Localization;
using HtmlAgilityPack; using HtmlAgilityPack;
@@ -58,14 +59,12 @@ namespace ArchiSteamFarm {
{ WebAPI.DefaultBaseAddress.Host, (new SemaphoreSlim(1, 1), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) } { WebAPI.DefaultBaseAddress.Host, (new SemaphoreSlim(1, 1), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) }
}; };
private readonly SemaphoreSlim ApiKeySemaphore = new SemaphoreSlim(1, 1);
private readonly Bot Bot; private readonly Bot Bot;
private readonly SemaphoreSlim PublicInventorySemaphore = new SemaphoreSlim(1, 1); private readonly ArchiCachable<string> CachedApiKey;
private readonly ArchiCachable<bool> CachedPublicInventory;
private readonly SemaphoreSlim SessionSemaphore = new SemaphoreSlim(1, 1); private readonly SemaphoreSlim SessionSemaphore = new SemaphoreSlim(1, 1);
private readonly WebBrowser WebBrowser; private readonly WebBrowser WebBrowser;
private string CachedApiKey;
private bool? CachedPublicInventory;
private DateTime LastSessionCheck; private DateTime LastSessionCheck;
private DateTime LastSessionRefresh; private DateTime LastSessionRefresh;
private bool MarkingInventoryScheduled; private bool MarkingInventoryScheduled;
@@ -74,12 +73,15 @@ namespace ArchiSteamFarm {
internal ArchiWebHandler(Bot bot) { internal ArchiWebHandler(Bot bot) {
Bot = bot ?? throw new ArgumentNullException(nameof(bot)); Bot = bot ?? throw new ArgumentNullException(nameof(bot));
CachedApiKey = new ArchiCachable<string>(ResolveApiKey, TimeSpan.FromHours(1));
CachedPublicInventory = new ArchiCachable<bool>(ResolvePublicInventory, TimeSpan.FromHours(1));
WebBrowser = new WebBrowser(bot.ArchiLogger, Program.GlobalConfig.WebProxy); WebBrowser = new WebBrowser(bot.ArchiLogger, Program.GlobalConfig.WebProxy);
} }
public void Dispose() { public void Dispose() {
ApiKeySemaphore.Dispose(); CachedApiKey.Dispose();
PublicInventorySemaphore.Dispose(); CachedPublicInventory.Dispose();
SessionSemaphore.Dispose(); SessionSemaphore.Dispose();
WebBrowser.Dispose(); WebBrowser.Dispose();
} }
@@ -192,8 +194,8 @@ namespace ArchiSteamFarm {
return false; return false;
} }
string steamApiKey = await GetApiKey().ConfigureAwait(false); (bool success, string steamApiKey) = await CachedApiKey.GetValue().ConfigureAwait(false);
if (string.IsNullOrEmpty(steamApiKey)) { if (!success || string.IsNullOrEmpty(steamApiKey)) {
return false; return false;
} }
@@ -239,8 +241,8 @@ namespace ArchiSteamFarm {
} }
internal async Task<HashSet<Steam.TradeOffer>> GetActiveTradeOffers() { internal async Task<HashSet<Steam.TradeOffer>> GetActiveTradeOffers() {
string steamApiKey = await GetApiKey().ConfigureAwait(false); (bool success, string steamApiKey) = await CachedApiKey.GetValue().ConfigureAwait(false);
if (string.IsNullOrEmpty(steamApiKey)) { if (!success || string.IsNullOrEmpty(steamApiKey)) {
return null; return null;
} }
@@ -699,8 +701,8 @@ namespace ArchiSteamFarm {
return null; return null;
} }
string steamApiKey = await GetApiKey().ConfigureAwait(false); (bool success, string steamApiKey) = await CachedApiKey.GetValue().ConfigureAwait(false);
if (string.IsNullOrEmpty(steamApiKey)) { if (!success || string.IsNullOrEmpty(steamApiKey)) {
return null; return null;
} }
@@ -841,8 +843,8 @@ namespace ArchiSteamFarm {
return null; return null;
} }
string steamApiKey = await GetApiKey().ConfigureAwait(false); (bool success, string steamApiKey) = await CachedApiKey.GetValue().ConfigureAwait(false);
if (string.IsNullOrEmpty(steamApiKey)) { if (!success || string.IsNullOrEmpty(steamApiKey)) {
return null; return null;
} }
@@ -946,31 +948,14 @@ namespace ArchiSteamFarm {
} }
internal async Task<bool> HasPublicInventory() { internal async Task<bool> HasPublicInventory() {
if (CachedPublicInventory.HasValue) { (bool success, bool hasPublicInventory) = await CachedPublicInventory.GetValue().ConfigureAwait(false);
return CachedPublicInventory.Value; return success && hasPublicInventory;
}
// We didn't fetch state yet
await PublicInventorySemaphore.WaitAsync().ConfigureAwait(false);
try {
if (CachedPublicInventory.HasValue) {
return CachedPublicInventory.Value;
}
bool? isInventoryPublic = await IsInventoryPublic().ConfigureAwait(false);
if (!isInventoryPublic.HasValue) {
return false;
}
CachedPublicInventory = isInventoryPublic.Value;
return isInventoryPublic.Value;
} finally {
PublicInventorySemaphore.Release();
}
} }
internal async Task<bool> HasValidApiKey() => !string.IsNullOrEmpty(await GetApiKey().ConfigureAwait(false)); internal async Task<bool> HasValidApiKey() {
(bool success, string steamApiKey) = await CachedApiKey.GetValue().ConfigureAwait(false);
return success && !string.IsNullOrEmpty(steamApiKey);
}
internal async Task<bool> Init(ulong steamID, EUniverse universe, string webAPIUserNonce, string parentalCode = null) { internal async Task<bool> Init(ulong steamID, EUniverse universe, string webAPIUserNonce, string parentalCode = null) {
if ((steamID == 0) || (universe == EUniverse.Invalid) || string.IsNullOrEmpty(webAPIUserNonce)) { if ((steamID == 0) || (universe == EUniverse.Invalid) || string.IsNullOrEmpty(webAPIUserNonce)) {
@@ -1121,9 +1106,9 @@ namespace ArchiSteamFarm {
} }
internal void OnDisconnected() { internal void OnDisconnected() {
CachedApiKey = null;
CachedPublicInventory = null;
SteamID = 0; SteamID = 0;
Utilities.InBackground(CachedApiKey.Reset);
Utilities.InBackground(CachedPublicInventory.Reset);
} }
internal void OnVanityURLChanged(string vanityURL = null) => VanityURL = string.IsNullOrEmpty(vanityURL) ? null : vanityURL; internal void OnVanityURLChanged(string vanityURL = null) => VanityURL = string.IsNullOrEmpty(vanityURL) ? null : vanityURL;
@@ -1307,70 +1292,6 @@ namespace ArchiSteamFarm {
return string.IsNullOrEmpty(VanityURL) ? "/profiles/" + SteamID : "/id/" + VanityURL; return string.IsNullOrEmpty(VanityURL) ? "/profiles/" + SteamID : "/id/" + VanityURL;
} }
private async Task<string> GetApiKey() {
if (CachedApiKey != null) {
// We fetched API key already, and either got valid one, or permanent AccessDenied
// In any case, this is our final result
return CachedApiKey;
}
if (Bot.IsAccountLimited) {
// API key is permanently unavailable for limited accounts
return null;
}
// We didn't fetch API key yet
await ApiKeySemaphore.WaitAsync().ConfigureAwait(false);
try {
if (CachedApiKey != null) {
return CachedApiKey;
}
(ESteamApiKeyState State, string Key) result = await GetApiKeyState().ConfigureAwait(false);
switch (result.State) {
case ESteamApiKeyState.AccessDenied:
// We succeeded in fetching API key, but it resulted in access denied
// Cache the result as empty, API key is unavailable permanently
CachedApiKey = string.Empty;
break;
case ESteamApiKeyState.NotRegisteredYet:
// We succeeded in fetching API key, and it resulted in no key registered yet
// Let's try to register a new key
if (!await RegisterApiKey().ConfigureAwait(false)) {
// Request timed out, bad luck, we'll try again later
return null;
}
// We should have the key ready, so let's fetch it again
result = await GetApiKeyState().ConfigureAwait(false);
if (result.State != ESteamApiKeyState.Registered) {
// Something went wrong, bad luck, we'll try again later
return null;
}
goto case ESteamApiKeyState.Registered;
case ESteamApiKeyState.Registered:
// We succeeded in fetching API key, and it resulted in registered key
// Cache the result, this is the API key we want
CachedApiKey = result.Key;
break;
case ESteamApiKeyState.Timeout:
// Request timed out, bad luck, we'll try again later
return null;
default:
// We got an unhandled error, this should never happen
Bot.ArchiLogger.LogGenericError(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(result.State), result.State));
return null;
}
return CachedApiKey;
} finally {
ApiKeySemaphore.Release();
}
}
private async Task<(ESteamApiKeyState State, string Key)> GetApiKeyState() { private async Task<(ESteamApiKeyState State, string Key)> GetApiKeyState() {
const string request = "/dev/apikey?l=english"; const string request = "/dev/apikey?l=english";
HtmlDocument htmlDocument = await UrlGetToHtmlDocumentWithSession(SteamCommunityURL, request).ConfigureAwait(false); HtmlDocument htmlDocument = await UrlGetToHtmlDocumentWithSession(SteamCommunityURL, request).ConfigureAwait(false);
@@ -1466,55 +1387,6 @@ namespace ArchiSteamFarm {
} }
} }
private async Task<bool?> IsInventoryPublic() {
const string request = "/my/edit/settings?l=english";
HtmlDocument htmlDocument = await UrlGetToHtmlDocumentWithSession(SteamCommunityURL, request, false).ConfigureAwait(false);
if (htmlDocument == null) {
return null;
}
HtmlNode htmlNode = htmlDocument.DocumentNode.SelectSingleNode("//div[@data-component='ProfilePrivacySettings']/@data-privacysettings");
if (htmlNode == null) {
Bot.ArchiLogger.LogNullError(nameof(htmlNode));
return null;
}
string json = htmlNode.GetAttributeValue("data-privacysettings", null);
if (string.IsNullOrEmpty(json)) {
Bot.ArchiLogger.LogNullError(nameof(json));
return null;
}
// This json is encoded as html attribute, don't forget to decode it
json = WebUtility.HtmlDecode(json);
Steam.UserPrivacy userPrivacy;
try {
userPrivacy = JsonConvert.DeserializeObject<Steam.UserPrivacy>(json);
} catch (JsonException e) {
Bot.ArchiLogger.LogGenericException(e);
return null;
}
if (userPrivacy == null) {
Bot.ArchiLogger.LogNullError(nameof(userPrivacy));
return null;
}
switch (userPrivacy.Settings.Inventory) {
case Steam.UserPrivacy.PrivacySettings.EPrivacySetting.FriendsOnly:
case Steam.UserPrivacy.PrivacySettings.EPrivacySetting.Private:
return false;
case Steam.UserPrivacy.PrivacySettings.EPrivacySetting.Public:
return true;
default:
Bot.ArchiLogger.LogGenericError(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(userPrivacy.Settings.Inventory), userPrivacy.Settings.Inventory));
return null;
}
}
private async Task<bool> IsProfileUri(Uri uri, bool waitForInitialization = true) { private async Task<bool> IsProfileUri(Uri uri, bool waitForInitialization = true) {
if (uri == null) { if (uri == null) {
ASF.ArchiLogger.LogNullError(nameof(uri)); ASF.ArchiLogger.LogNullError(nameof(uri));
@@ -1667,6 +1539,98 @@ namespace ArchiSteamFarm {
return await UrlPostWithSession(SteamCommunityURL, request, data).ConfigureAwait(false); return await UrlPostWithSession(SteamCommunityURL, request, data).ConfigureAwait(false);
} }
private async Task<(bool Success, string Result)> ResolveApiKey() {
if (Bot.IsAccountLimited) {
// API key is permanently unavailable for limited accounts
return (true, null);
}
(ESteamApiKeyState State, string Key) result = await GetApiKeyState().ConfigureAwait(false);
switch (result.State) {
case ESteamApiKeyState.AccessDenied:
// We succeeded in fetching API key, but it resulted in access denied
// Return empty result, API key is unavailable permanently
return (true, "");
case ESteamApiKeyState.NotRegisteredYet:
// We succeeded in fetching API key, and it resulted in no key registered yet
// Let's try to register a new key
if (!await RegisterApiKey().ConfigureAwait(false)) {
// Request timed out, bad luck, we'll try again later
return (false, null);
}
// We should have the key ready, so let's fetch it again
result = await GetApiKeyState().ConfigureAwait(false);
if (result.State != ESteamApiKeyState.Registered) {
// Something went wrong, bad luck, we'll try again later
goto case ESteamApiKeyState.Timeout;
}
goto case ESteamApiKeyState.Registered;
case ESteamApiKeyState.Registered:
// We succeeded in fetching API key, and it resulted in registered key
// Cache the result, this is the API key we want
return (true, result.Key);
case ESteamApiKeyState.Timeout:
// Request timed out, bad luck, we'll try again later
return (false, null);
default:
// We got an unhandled error, this should never happen
Bot.ArchiLogger.LogGenericError(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(result.State), result.State));
return (false, null);
}
}
private async Task<(bool Success, bool Result)> ResolvePublicInventory() {
const string request = "/my/edit/settings?l=english";
HtmlDocument htmlDocument = await UrlGetToHtmlDocumentWithSession(SteamCommunityURL, request, false).ConfigureAwait(false);
if (htmlDocument == null) {
return (false, false);
}
HtmlNode htmlNode = htmlDocument.DocumentNode.SelectSingleNode("//div[@data-component='ProfilePrivacySettings']/@data-privacysettings");
if (htmlNode == null) {
Bot.ArchiLogger.LogNullError(nameof(htmlNode));
return (false, false);
}
string json = htmlNode.GetAttributeValue("data-privacysettings", null);
if (string.IsNullOrEmpty(json)) {
Bot.ArchiLogger.LogNullError(nameof(json));
return (false, false);
}
// This json is encoded as html attribute, don't forget to decode it
json = WebUtility.HtmlDecode(json);
Steam.UserPrivacy userPrivacy;
try {
userPrivacy = JsonConvert.DeserializeObject<Steam.UserPrivacy>(json);
} catch (JsonException e) {
Bot.ArchiLogger.LogGenericException(e);
return (false, false);
}
if (userPrivacy == null) {
Bot.ArchiLogger.LogNullError(nameof(userPrivacy));
return (false, false);
}
switch (userPrivacy.Settings.Inventory) {
case Steam.UserPrivacy.PrivacySettings.EPrivacySetting.FriendsOnly:
case Steam.UserPrivacy.PrivacySettings.EPrivacySetting.Private:
return (true, false);
case Steam.UserPrivacy.PrivacySettings.EPrivacySetting.Public:
return (true, true);
default:
Bot.ArchiLogger.LogGenericError(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(userPrivacy.Settings.Inventory), userPrivacy.Settings.Inventory));
return (false, false);
}
}
private async Task<bool> UnlockParentalAccount(string parentalCode) { private async Task<bool> UnlockParentalAccount(string parentalCode) {
if (string.IsNullOrEmpty(parentalCode)) { if (string.IsNullOrEmpty(parentalCode)) {
Bot.ArchiLogger.LogNullError(nameof(parentalCode)); Bot.ArchiLogger.LogNullError(nameof(parentalCode));

View File

@@ -2023,9 +2023,6 @@ namespace ArchiSteamFarm {
// Sometimes Steam won't send us our own PersonaStateCallback, so request it explicitly // Sometimes Steam won't send us our own PersonaStateCallback, so request it explicitly
RequestPersonaStateUpdate(); RequestPersonaStateUpdate();
// This will pre-cache API key for eventual further usage
Utilities.InBackground(ArchiWebHandler.HasValidApiKey);
Utilities.InBackground(InitializeFamilySharing); Utilities.InBackground(InitializeFamilySharing);
if (Statistics != null) { if (Statistics != null) {

View File

@@ -0,0 +1,141 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
//
// Copyright 2015-2018 Ł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;
using System.Threading;
using System.Threading.Tasks;
namespace ArchiSteamFarm.Helpers {
internal sealed class ArchiCachable<T> : IDisposable {
private readonly TimeSpan CacheLifetime;
private readonly SemaphoreSlim InitSemaphore = new SemaphoreSlim(1, 1);
private readonly Func<Task<(bool Success, T Result)>> ResolveFunction;
private bool IsInitialized => InitializedAt > DateTime.MinValue;
private bool IsPermanentCache => CacheLifetime == Timeout.InfiniteTimeSpan;
private bool IsRecent => IsPermanentCache || (DateTime.UtcNow.Subtract(InitializedAt) < CacheLifetime);
// Purge should happen slightly after lifetime, to allow eventual refresh if the property is still used
private TimeSpan PurgeLifetime => CacheLifetime + TimeSpan.FromMinutes(1);
private DateTime InitializedAt;
private T InitializedValue;
private Timer MaintenanceTimer;
internal ArchiCachable(Func<Task<(bool Success, T Result)>> resolveFunction, TimeSpan? cacheLifetime = null) {
ResolveFunction = resolveFunction ?? throw new ArgumentNullException(nameof(resolveFunction));
CacheLifetime = cacheLifetime ?? Timeout.InfiniteTimeSpan;
}
public void Dispose() {
// Those are objects that are always being created if constructor doesn't throw exception
InitSemaphore.Dispose();
// Those are objects that might be null and the check should be in-place
MaintenanceTimer?.Dispose();
}
internal async Task<(bool Success, T Result)> GetValue() {
if (IsInitialized && IsRecent) {
return (true, InitializedValue);
}
await InitSemaphore.WaitAsync().ConfigureAwait(false);
try {
if (IsInitialized && IsRecent) {
return (true, InitializedValue);
}
(bool success, T result) = await ResolveFunction().ConfigureAwait(false);
if (!success) {
return (false, InitializedValue);
}
InitializedValue = result;
InitializedAt = DateTime.UtcNow;
if (!IsPermanentCache) {
if (MaintenanceTimer == null) {
MaintenanceTimer = new Timer(
async e => await SoftReset().ConfigureAwait(false),
null,
PurgeLifetime, // Delay
Timeout.InfiniteTimeSpan // Period
);
} else {
MaintenanceTimer.Change(PurgeLifetime, Timeout.InfiniteTimeSpan);
}
}
return (true, result);
} finally {
InitSemaphore.Release();
}
}
internal async Task Reset() {
if (!IsInitialized) {
return;
}
await InitSemaphore.WaitAsync().ConfigureAwait(false);
try {
if (!IsInitialized) {
return;
}
HardReset();
} finally {
InitSemaphore.Release();
}
}
private void HardReset() {
InitializedAt = DateTime.MinValue;
InitializedValue = default;
if (MaintenanceTimer != null) {
MaintenanceTimer.Dispose();
MaintenanceTimer = null;
}
}
private async Task SoftReset() {
if (!IsInitialized || IsRecent) {
return;
}
await InitSemaphore.WaitAsync().ConfigureAwait(false);
try {
if (!IsInitialized || IsRecent) {
return;
}
HardReset();
} finally {
InitSemaphore.Release();
}
}
}
}