mirror of
https://github.com/JustArchiNET/ArchiSteamFarm.git
synced 2026-01-01 22:20:52 +00:00
1815 lines
63 KiB
C#
1815 lines
63 KiB
C#
// _ _ _ ____ _ _____
|
|
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
|
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
|
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
|
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
|
//
|
|
// 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.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Xml;
|
|
using ArchiSteamFarm.Json;
|
|
using ArchiSteamFarm.Localization;
|
|
using HtmlAgilityPack;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
using SteamKit2;
|
|
using Formatting = Newtonsoft.Json.Formatting;
|
|
|
|
namespace ArchiSteamFarm {
|
|
internal sealed class ArchiWebHandler : IDisposable {
|
|
internal const byte MinSessionTTL = GlobalConfig.DefaultConnectionTimeout / 6; // Assume session is valid for at least that amount of seconds
|
|
|
|
private const string IEconService = "IEconService";
|
|
private const string IPlayerService = "IPlayerService";
|
|
private const string ISteamUserAuth = "ISteamUserAuth";
|
|
private const string ITwoFactorService = "ITwoFactorService";
|
|
|
|
// We must use HTTPS for SteamCommunity, as http would make certain POST requests failing (trades)
|
|
private const string SteamCommunityHost = "steamcommunity.com";
|
|
private const string SteamCommunityURL = "https://" + SteamCommunityHost;
|
|
|
|
// We could (and should) use HTTPS for SteamStore, but that would make certain POST requests failing
|
|
private const string SteamStoreHost = "store.steampowered.com";
|
|
private const string SteamStoreURL = "http://" + SteamStoreHost;
|
|
|
|
private static readonly SemaphoreSlim InventorySemaphore = new SemaphoreSlim(1, 1);
|
|
|
|
private readonly SemaphoreSlim ApiKeySemaphore = new SemaphoreSlim(1, 1);
|
|
private readonly Bot Bot;
|
|
private readonly SemaphoreSlim PublicInventorySemaphore = new SemaphoreSlim(1, 1);
|
|
private readonly SemaphoreSlim SessionSemaphore = new SemaphoreSlim(1, 1);
|
|
private readonly SemaphoreSlim TradeTokenSemaphore = new SemaphoreSlim(1, 1);
|
|
private readonly WebBrowser WebBrowser;
|
|
|
|
private string CachedApiKey;
|
|
private bool? CachedPublicInventory;
|
|
private string CachedTradeToken;
|
|
private DateTime LastSessionRefresh;
|
|
private bool MarkingInventoryScheduled;
|
|
private ulong SteamID;
|
|
private string VanityURL;
|
|
|
|
internal ArchiWebHandler(Bot bot) {
|
|
Bot = bot ?? throw new ArgumentNullException(nameof(bot));
|
|
WebBrowser = new WebBrowser(bot.ArchiLogger);
|
|
}
|
|
|
|
public void Dispose() {
|
|
ApiKeySemaphore.Dispose();
|
|
PublicInventorySemaphore.Dispose();
|
|
SessionSemaphore.Dispose();
|
|
TradeTokenSemaphore.Dispose();
|
|
WebBrowser.Dispose();
|
|
}
|
|
|
|
internal async Task<bool> AcceptTradeOffer(ulong tradeID) {
|
|
if (tradeID == 0) {
|
|
Bot.ArchiLogger.LogNullError(nameof(tradeID));
|
|
return false;
|
|
}
|
|
|
|
string request = "/tradeoffer/" + tradeID + "/accept";
|
|
string referer = SteamCommunityURL + "/tradeoffer/" + tradeID;
|
|
|
|
// Extra entry for sessionID
|
|
Dictionary<string, string> data = new Dictionary<string, string>(3) {
|
|
{ "serverid", "1" },
|
|
{ "tradeofferid", tradeID.ToString() }
|
|
};
|
|
|
|
return await UrlPostRetryWithSession(SteamCommunityURL, request, data, referer).ConfigureAwait(false);
|
|
}
|
|
|
|
internal async Task<bool> AddFreeLicense(uint subID) {
|
|
if (subID == 0) {
|
|
Bot.ArchiLogger.LogNullError(nameof(subID));
|
|
return false;
|
|
}
|
|
|
|
const string request = "/checkout/addfreelicense";
|
|
|
|
// Extra entry for sessionID
|
|
Dictionary<string, string> data = new Dictionary<string, string>(3) {
|
|
{ "subid", subID.ToString() },
|
|
{ "action", "add_to_cart" }
|
|
};
|
|
|
|
HtmlDocument htmlDocument = await UrlPostToHtmlDocumentRetryWithSession(SteamStoreURL, request, data).ConfigureAwait(false);
|
|
return htmlDocument?.DocumentNode.SelectSingleNode("//div[@class='add_free_content_success_area']") != null;
|
|
}
|
|
|
|
internal async Task<bool> ClearFromDiscoveryQueue(uint appID) {
|
|
if (appID == 0) {
|
|
Bot.ArchiLogger.LogNullError(nameof(appID));
|
|
return false;
|
|
}
|
|
|
|
string request = "/app/" + appID;
|
|
|
|
// Extra entry for sessionID
|
|
Dictionary<string, string> data = new Dictionary<string, string>(2) { { "appid_to_clear_from_queue", appID.ToString() } };
|
|
|
|
return await UrlPostRetryWithSession(SteamStoreURL, request, data).ConfigureAwait(false);
|
|
}
|
|
|
|
internal async Task DeclineTradeOffer(ulong tradeID) {
|
|
if (tradeID == 0) {
|
|
Bot.ArchiLogger.LogNullError(nameof(tradeID));
|
|
return;
|
|
}
|
|
|
|
string steamApiKey = await GetApiKey().ConfigureAwait(false);
|
|
if (string.IsNullOrEmpty(steamApiKey)) {
|
|
return;
|
|
}
|
|
|
|
KeyValue response = null;
|
|
for (byte i = 0; (i < WebBrowser.MaxTries) && (response == null); i++) {
|
|
using (dynamic iEconService = WebAPI.GetAsyncInterface(IEconService, steamApiKey)) {
|
|
iEconService.Timeout = WebBrowser.Timeout;
|
|
|
|
try {
|
|
response = await iEconService.DeclineTradeOffer(
|
|
tradeofferid: tradeID.ToString(),
|
|
method: WebRequestMethods.Http.Post,
|
|
secure: true
|
|
);
|
|
} catch (TaskCanceledException e) {
|
|
Bot.ArchiLogger.LogGenericDebuggingException(e);
|
|
} catch (Exception e) {
|
|
Bot.ArchiLogger.LogGenericWarningException(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (response == null) {
|
|
Bot.ArchiLogger.LogGenericWarning(string.Format(Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries));
|
|
}
|
|
}
|
|
|
|
internal async Task<HashSet<uint>> GenerateNewDiscoveryQueue() {
|
|
const string request = "/explore/generatenewdiscoveryqueue";
|
|
|
|
// Extra entry for sessionID
|
|
Dictionary<string, string> data = new Dictionary<string, string>(2) { { "queuetype", "0" } };
|
|
|
|
Steam.NewDiscoveryQueueResponse output = await UrlPostToObjectRetryWithSession<Steam.NewDiscoveryQueueResponse>(SteamStoreURL, request, data).ConfigureAwait(false);
|
|
return output?.Queue;
|
|
}
|
|
|
|
internal async Task<HashSet<Steam.TradeOffer>> GetActiveTradeOffers(IReadOnlyCollection<ulong> ignoredTradeOfferIDs = null) {
|
|
string steamApiKey = await GetApiKey().ConfigureAwait(false);
|
|
if (string.IsNullOrEmpty(steamApiKey)) {
|
|
return null;
|
|
}
|
|
|
|
KeyValue response = null;
|
|
for (byte i = 0; (i < WebBrowser.MaxTries) && (response == null); i++) {
|
|
using (dynamic iEconService = WebAPI.GetAsyncInterface(IEconService, steamApiKey)) {
|
|
iEconService.Timeout = WebBrowser.Timeout;
|
|
|
|
try {
|
|
response = await iEconService.GetTradeOffers(
|
|
active_only: 1,
|
|
get_descriptions: 1,
|
|
get_received_offers: 1,
|
|
secure: true,
|
|
time_historical_cutoff: uint.MaxValue
|
|
);
|
|
} catch (TaskCanceledException e) {
|
|
Bot.ArchiLogger.LogGenericDebuggingException(e);
|
|
} catch (Exception e) {
|
|
Bot.ArchiLogger.LogGenericWarningException(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (response == null) {
|
|
Bot.ArchiLogger.LogGenericWarning(string.Format(Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries));
|
|
return null;
|
|
}
|
|
|
|
Dictionary<ulong, (uint AppID, Steam.Asset.EType Type)> descriptions = new Dictionary<ulong, (uint AppID, Steam.Asset.EType Type)>();
|
|
foreach (KeyValue description in response["descriptions"].Children) {
|
|
ulong classID = description["classid"].AsUnsignedLong();
|
|
if (classID == 0) {
|
|
Bot.ArchiLogger.LogNullError(nameof(classID));
|
|
return null;
|
|
}
|
|
|
|
if (descriptions.ContainsKey(classID)) {
|
|
continue;
|
|
}
|
|
|
|
uint appID = 0;
|
|
|
|
string hashName = description["market_hash_name"].Value;
|
|
if (!string.IsNullOrEmpty(hashName)) {
|
|
appID = GetAppIDFromMarketHashName(hashName);
|
|
}
|
|
|
|
if (appID == 0) {
|
|
appID = description["appid"].AsUnsignedInteger();
|
|
}
|
|
|
|
Steam.Asset.EType type = Steam.Asset.EType.Unknown;
|
|
|
|
string descriptionType = description["type"].Value;
|
|
if (!string.IsNullOrEmpty(descriptionType)) {
|
|
type = GetItemType(descriptionType);
|
|
}
|
|
|
|
descriptions[classID] = (appID, type);
|
|
}
|
|
|
|
HashSet<Steam.TradeOffer> result = new HashSet<Steam.TradeOffer>();
|
|
foreach (KeyValue trade in response["trade_offers_received"].Children) {
|
|
Steam.TradeOffer.ETradeOfferState state = trade["trade_offer_state"].AsEnum<Steam.TradeOffer.ETradeOfferState>();
|
|
if (state == Steam.TradeOffer.ETradeOfferState.Unknown) {
|
|
Bot.ArchiLogger.LogNullError(nameof(state));
|
|
return null;
|
|
}
|
|
|
|
if (state != Steam.TradeOffer.ETradeOfferState.Active) {
|
|
continue;
|
|
}
|
|
|
|
ulong tradeOfferID = trade["tradeofferid"].AsUnsignedLong();
|
|
if (tradeOfferID == 0) {
|
|
Bot.ArchiLogger.LogNullError(nameof(tradeOfferID));
|
|
return null;
|
|
}
|
|
|
|
if (ignoredTradeOfferIDs?.Contains(tradeOfferID) == true) {
|
|
continue;
|
|
}
|
|
|
|
uint otherSteamID3 = trade["accountid_other"].AsUnsignedInteger();
|
|
if (otherSteamID3 == 0) {
|
|
Bot.ArchiLogger.LogNullError(nameof(otherSteamID3));
|
|
return null;
|
|
}
|
|
|
|
Steam.TradeOffer tradeOffer = new Steam.TradeOffer(tradeOfferID, otherSteamID3, state);
|
|
|
|
List<KeyValue> itemsToGive = trade["items_to_give"].Children;
|
|
if (itemsToGive.Count > 0) {
|
|
if (!ParseItems(descriptions, itemsToGive, tradeOffer.ItemsToGive)) {
|
|
Bot.ArchiLogger.LogGenericError(string.Format(Strings.ErrorParsingObject, nameof(itemsToGive)));
|
|
return null;
|
|
}
|
|
}
|
|
|
|
List<KeyValue> itemsToReceive = trade["items_to_receive"].Children;
|
|
if (itemsToReceive.Count > 0) {
|
|
if (!ParseItems(descriptions, itemsToReceive, tradeOffer.ItemsToReceive)) {
|
|
Bot.ArchiLogger.LogGenericError(string.Format(Strings.ErrorParsingObject, nameof(itemsToReceive)));
|
|
return null;
|
|
}
|
|
}
|
|
|
|
result.Add(tradeOffer);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
internal async Task<HtmlDocument> GetBadgePage(byte page) {
|
|
if (page == 0) {
|
|
Bot.ArchiLogger.LogNullError(nameof(page));
|
|
return null;
|
|
}
|
|
|
|
string request = "/my/badges?l=english&p=" + page;
|
|
return await UrlGetToHtmlDocumentRetryWithSession(SteamCommunityURL, request).ConfigureAwait(false);
|
|
}
|
|
|
|
internal async Task<Steam.ConfirmationDetails> GetConfirmationDetails(string deviceID, string confirmationHash, uint time, MobileAuthenticator.Confirmation confirmation) {
|
|
if (string.IsNullOrEmpty(deviceID) || string.IsNullOrEmpty(confirmationHash) || (time == 0) || (confirmation == null)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(deviceID) + " || " + nameof(confirmationHash) + " || " + nameof(time) + " || " + nameof(confirmation));
|
|
return null;
|
|
}
|
|
|
|
string request = "/mobileconf/details/" + confirmation.ID + "?l=english&p=" + deviceID + "&a=" + SteamID + "&k=" + WebUtility.UrlEncode(confirmationHash) + "&t=" + time + "&m=android&tag=conf";
|
|
|
|
Steam.ConfirmationDetails response = await UrlGetToObjectRetryWithSession<Steam.ConfirmationDetails>(SteamCommunityURL, request).ConfigureAwait(false);
|
|
if (response?.Success != true) {
|
|
return null;
|
|
}
|
|
|
|
response.Confirmation = confirmation;
|
|
return response;
|
|
}
|
|
|
|
internal async Task<HtmlDocument> GetConfirmations(string deviceID, string confirmationHash, uint time) {
|
|
if (string.IsNullOrEmpty(deviceID) || string.IsNullOrEmpty(confirmationHash) || (time == 0)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(deviceID) + " || " + nameof(confirmationHash) + " || " + nameof(time));
|
|
return null;
|
|
}
|
|
|
|
string request = "/mobileconf/conf?l=english&p=" + deviceID + "&a=" + SteamID + "&k=" + WebUtility.UrlEncode(confirmationHash) + "&t=" + time + "&m=android&tag=conf";
|
|
return await UrlGetToHtmlDocumentRetryWithSession(SteamCommunityURL, request).ConfigureAwait(false);
|
|
}
|
|
|
|
internal async Task<HtmlDocument> GetDiscoveryQueuePage() {
|
|
const string request = "/explore?l=english";
|
|
return await UrlGetToHtmlDocumentRetryWithSession(SteamStoreURL, request).ConfigureAwait(false);
|
|
}
|
|
|
|
internal async Task<HashSet<ulong>> GetFamilySharingSteamIDs() {
|
|
const string request = "/account/managedevices";
|
|
HtmlDocument htmlDocument = await UrlGetToHtmlDocumentRetryWithSession(SteamStoreURL, request).ConfigureAwait(false);
|
|
|
|
HtmlNodeCollection htmlNodes = htmlDocument?.DocumentNode.SelectNodes("(//table[@class='accountTable'])[last()]//a/@data-miniprofile");
|
|
if (htmlNodes == null) {
|
|
return null; // OK, no authorized steamIDs
|
|
}
|
|
|
|
HashSet<ulong> result = new HashSet<ulong>();
|
|
|
|
foreach (string miniProfile in htmlNodes.Select(htmlNode => htmlNode.GetAttributeValue("data-miniprofile", null))) {
|
|
if (string.IsNullOrEmpty(miniProfile)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(miniProfile));
|
|
return null;
|
|
}
|
|
|
|
if (!uint.TryParse(miniProfile, out uint steamID3) || (steamID3 == 0)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(steamID3));
|
|
return null;
|
|
}
|
|
|
|
ulong steamID = new SteamID(steamID3, EUniverse.Public, EAccountType.Individual);
|
|
result.Add(steamID);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
internal async Task<HtmlDocument> GetGameCardsPage(ulong appID) {
|
|
if (appID == 0) {
|
|
Bot.ArchiLogger.LogNullError(nameof(appID));
|
|
return null;
|
|
}
|
|
|
|
string request = "/my/gamecards/" + appID + "?l=english";
|
|
return await UrlGetToHtmlDocumentRetryWithSession(SteamCommunityURL, request).ConfigureAwait(false);
|
|
}
|
|
|
|
[SuppressMessage("ReSharper", "FunctionComplexityOverflow")]
|
|
internal async Task<HashSet<Steam.Asset>> GetMyInventory(bool tradableOnly = false, uint appID = Steam.Asset.SteamAppID, byte contextID = Steam.Asset.SteamCommunityContextID, IReadOnlyCollection<Steam.Asset.EType> wantedTypes = null, IReadOnlyCollection<uint> wantedRealAppIDs = null) {
|
|
if ((appID == 0) || (contextID == 0)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(appID) + " || " + nameof(contextID));
|
|
return null;
|
|
}
|
|
|
|
HashSet<Steam.Asset> result = new HashSet<Steam.Asset>();
|
|
|
|
// 5000 is maximum allowed count per single request
|
|
string request = "/inventory/" + SteamID + "/" + appID + "/" + contextID + "?l=english&count=5000";
|
|
ulong startAssetID = 0;
|
|
|
|
await InventorySemaphore.WaitAsync().ConfigureAwait(false);
|
|
|
|
try {
|
|
while (true) {
|
|
Steam.InventoryResponse response = await UrlGetToObjectRetryWithSession<Steam.InventoryResponse>(SteamCommunityURL, request + (startAssetID > 0 ? "&start_assetid=" + startAssetID : "")).ConfigureAwait(false);
|
|
|
|
if (response == null) {
|
|
return null;
|
|
}
|
|
|
|
if (!response.Success) {
|
|
Bot.ArchiLogger.LogGenericWarning(!string.IsNullOrEmpty(response.Error) ? string.Format(Strings.WarningFailedWithError, response.Error) : Strings.WarningFailed);
|
|
return null;
|
|
}
|
|
|
|
if (response.TotalInventoryCount == 0) {
|
|
// Empty inventory
|
|
return result;
|
|
}
|
|
|
|
if ((response.Assets == null) || (response.Assets.Count == 0) || (response.Descriptions == null) || (response.Descriptions.Count == 0)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(response.Assets) + " || " + nameof(response.Descriptions));
|
|
return null;
|
|
}
|
|
|
|
Dictionary<ulong, (uint RealAppID, Steam.Asset.EType Type, bool Tradable)> descriptionMap = new Dictionary<ulong, (uint RealAppID, Steam.Asset.EType Type, bool Tradable)>();
|
|
foreach (Steam.InventoryResponse.Description description in response.Descriptions.Where(description => description != null)) {
|
|
if (description.ClassID == 0) {
|
|
Bot.ArchiLogger.LogNullError(nameof(description.ClassID));
|
|
return null;
|
|
}
|
|
|
|
if (descriptionMap.ContainsKey(description.ClassID)) {
|
|
continue;
|
|
}
|
|
|
|
uint realAppID = 0;
|
|
|
|
if (!string.IsNullOrEmpty(description.MarketHashName)) {
|
|
realAppID = GetAppIDFromMarketHashName(description.MarketHashName);
|
|
}
|
|
|
|
if (realAppID == 0) {
|
|
realAppID = description.AppID;
|
|
}
|
|
|
|
Steam.Asset.EType type = Steam.Asset.EType.Unknown;
|
|
|
|
if (!string.IsNullOrEmpty(description.Type)) {
|
|
type = GetItemType(description.Type);
|
|
}
|
|
|
|
descriptionMap[description.ClassID] = (realAppID, type, description.Tradable);
|
|
}
|
|
|
|
foreach (Steam.Asset asset in response.Assets.Where(asset => asset != null)) {
|
|
if (descriptionMap.TryGetValue(asset.ClassID, out (uint RealAppID, Steam.Asset.EType Type, bool Tradable) description)) {
|
|
if (tradableOnly && !description.Tradable) {
|
|
continue;
|
|
}
|
|
|
|
asset.RealAppID = description.RealAppID;
|
|
asset.Type = description.Type;
|
|
}
|
|
|
|
if ((wantedTypes?.Contains(asset.Type) == false) || (wantedRealAppIDs?.Contains(asset.RealAppID) == false)) {
|
|
continue;
|
|
}
|
|
|
|
result.Add(asset);
|
|
}
|
|
|
|
if (!response.MoreItems) {
|
|
return result;
|
|
}
|
|
|
|
if (response.LastAssetID == 0) {
|
|
Bot.ArchiLogger.LogNullError(nameof(response.LastAssetID));
|
|
return null;
|
|
}
|
|
|
|
startAssetID = response.LastAssetID;
|
|
}
|
|
} finally {
|
|
if (Program.GlobalConfig.InventoryLimiterDelay == 0) {
|
|
InventorySemaphore.Release();
|
|
} else {
|
|
Utilities.InBackground(async () => {
|
|
await Task.Delay(Program.GlobalConfig.InventoryLimiterDelay * 1000).ConfigureAwait(false);
|
|
InventorySemaphore.Release();
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
internal async Task<Dictionary<uint, string>> GetMyOwnedGames() {
|
|
const string request = "/my/games/?xml=1";
|
|
|
|
XmlDocument response = await UrlGetToXmlDocumentRetryWithSession(SteamCommunityURL, request).ConfigureAwait(false);
|
|
|
|
XmlNodeList xmlNodeList = response?.SelectNodes("gamesList/games/game");
|
|
if ((xmlNodeList == null) || (xmlNodeList.Count == 0)) {
|
|
return null;
|
|
}
|
|
|
|
Dictionary<uint, string> result = new Dictionary<uint, string>(xmlNodeList.Count);
|
|
foreach (XmlNode xmlNode in xmlNodeList) {
|
|
XmlNode appNode = xmlNode.SelectSingleNode("appID");
|
|
if (appNode == null) {
|
|
Bot.ArchiLogger.LogNullError(nameof(appNode));
|
|
return null;
|
|
}
|
|
|
|
if (!uint.TryParse(appNode.InnerText, out uint appID)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(appID));
|
|
return null;
|
|
}
|
|
|
|
XmlNode nameNode = xmlNode.SelectSingleNode("name");
|
|
if (nameNode == null) {
|
|
Bot.ArchiLogger.LogNullError(nameof(nameNode));
|
|
return null;
|
|
}
|
|
|
|
result[appID] = nameNode.InnerText;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
internal async Task<Dictionary<uint, string>> GetOwnedGames(ulong steamID) {
|
|
if (steamID == 0) {
|
|
Bot.ArchiLogger.LogNullError(nameof(steamID));
|
|
return null;
|
|
}
|
|
|
|
string steamApiKey = await GetApiKey().ConfigureAwait(false);
|
|
if (string.IsNullOrEmpty(steamApiKey)) {
|
|
return null;
|
|
}
|
|
|
|
KeyValue response = null;
|
|
for (byte i = 0; (i < WebBrowser.MaxTries) && (response == null); i++) {
|
|
using (dynamic iPlayerService = WebAPI.GetAsyncInterface(IPlayerService, steamApiKey)) {
|
|
iPlayerService.Timeout = WebBrowser.Timeout;
|
|
|
|
try {
|
|
response = await iPlayerService.GetOwnedGames(
|
|
steamid: steamID,
|
|
include_appinfo: 1,
|
|
secure: true
|
|
);
|
|
} catch (TaskCanceledException e) {
|
|
Bot.ArchiLogger.LogGenericDebuggingException(e);
|
|
} catch (Exception e) {
|
|
Bot.ArchiLogger.LogGenericWarningException(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (response == null) {
|
|
Bot.ArchiLogger.LogGenericWarning(string.Format(Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries));
|
|
return null;
|
|
}
|
|
|
|
Dictionary<uint, string> result = new Dictionary<uint, string>(response["games"].Children.Count);
|
|
foreach (KeyValue game in response["games"].Children) {
|
|
uint appID = game["appid"].AsUnsignedInteger();
|
|
if (appID == 0) {
|
|
Bot.ArchiLogger.LogNullError(nameof(appID));
|
|
return null;
|
|
}
|
|
|
|
result[appID] = game["name"].Value;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
internal async Task<uint> GetServerTime() {
|
|
KeyValue response = null;
|
|
for (byte i = 0; (i < WebBrowser.MaxTries) && (response == null); i++) {
|
|
using (dynamic iTwoFactorService = WebAPI.GetAsyncInterface(ITwoFactorService)) {
|
|
iTwoFactorService.Timeout = WebBrowser.Timeout;
|
|
|
|
try {
|
|
response = await iTwoFactorService.QueryTime(
|
|
method: WebRequestMethods.Http.Post,
|
|
secure: true
|
|
);
|
|
} catch (TaskCanceledException e) {
|
|
Bot.ArchiLogger.LogGenericDebuggingException(e);
|
|
} catch (Exception e) {
|
|
Bot.ArchiLogger.LogGenericWarningException(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (response != null) {
|
|
return response["server_time"].AsUnsignedInteger();
|
|
}
|
|
|
|
Bot.ArchiLogger.LogGenericWarning(string.Format(Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries));
|
|
return 0;
|
|
}
|
|
|
|
internal async Task<HtmlDocument> GetSteamAwardsPage() {
|
|
const string request = "/SteamAwards?l=english";
|
|
return await UrlGetToHtmlDocumentRetryWithSession(SteamStoreURL, request).ConfigureAwait(false);
|
|
}
|
|
|
|
internal async Task<byte?> GetTradeHoldDurationForTrade(ulong tradeID) {
|
|
if (tradeID == 0) {
|
|
Bot.ArchiLogger.LogNullError(nameof(tradeID));
|
|
return null;
|
|
}
|
|
|
|
string request = "/tradeoffer/" + tradeID + "?l=english";
|
|
|
|
HtmlDocument htmlDocument = await UrlGetToHtmlDocumentRetryWithSession(SteamCommunityURL, request).ConfigureAwait(false);
|
|
|
|
HtmlNode htmlNode = htmlDocument?.DocumentNode.SelectSingleNode("//div[@class='pagecontent']/script");
|
|
if (htmlNode == null) {
|
|
// Trade can be no longer valid
|
|
return null;
|
|
}
|
|
|
|
string text = htmlNode.InnerText;
|
|
if (string.IsNullOrEmpty(text)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(text));
|
|
return null;
|
|
}
|
|
|
|
int index = text.IndexOf("g_daysTheirEscrow = ", StringComparison.Ordinal);
|
|
if (index < 0) {
|
|
Bot.ArchiLogger.LogNullError(nameof(index));
|
|
return null;
|
|
}
|
|
|
|
index += 20;
|
|
text = text.Substring(index);
|
|
|
|
index = text.IndexOf(';');
|
|
if (index < 0) {
|
|
Bot.ArchiLogger.LogNullError(nameof(index));
|
|
return null;
|
|
}
|
|
|
|
text = text.Substring(0, index);
|
|
|
|
if (byte.TryParse(text, out byte result)) {
|
|
return result;
|
|
}
|
|
|
|
Bot.ArchiLogger.LogNullError(nameof(result));
|
|
return null;
|
|
}
|
|
|
|
internal async Task<byte?> GetTradeHoldDurationForUser(ulong steamID, string tradeToken = null) {
|
|
if (steamID == 0) {
|
|
Bot.ArchiLogger.LogNullError(nameof(steamID));
|
|
return null;
|
|
}
|
|
|
|
string steamApiKey = await GetApiKey().ConfigureAwait(false);
|
|
if (string.IsNullOrEmpty(steamApiKey)) {
|
|
return null;
|
|
}
|
|
|
|
KeyValue response = null;
|
|
for (byte i = 0; (i < WebBrowser.MaxTries) && (response == null); i++) {
|
|
using (dynamic iEconService = WebAPI.GetAsyncInterface(IEconService, steamApiKey)) {
|
|
iEconService.Timeout = WebBrowser.Timeout;
|
|
|
|
try {
|
|
response = await iEconService.GetTradeHoldDurations(
|
|
secure: true,
|
|
steamid_target: steamID,
|
|
trade_offer_access_token: tradeToken ?? "" // TODO: Change me once https://github.com/SteamRE/SteamKit/pull/522 is merged
|
|
);
|
|
} catch (TaskCanceledException e) {
|
|
Bot.ArchiLogger.LogGenericDebuggingException(e);
|
|
} catch (Exception e) {
|
|
Bot.ArchiLogger.LogGenericWarningException(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (response == null) {
|
|
Bot.ArchiLogger.LogGenericWarning(string.Format(Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries));
|
|
return null;
|
|
}
|
|
|
|
uint resultInSeconds = response["their_escrow"]["escrow_end_duration_seconds"].AsUnsignedInteger(uint.MaxValue);
|
|
if (resultInSeconds == uint.MaxValue) {
|
|
Bot.ArchiLogger.LogNullError(nameof(resultInSeconds));
|
|
return null;
|
|
}
|
|
|
|
if (resultInSeconds == 0) {
|
|
return 0;
|
|
}
|
|
|
|
return (byte) (resultInSeconds / 86400);
|
|
}
|
|
|
|
internal async Task<string> GetTradeToken() {
|
|
if (CachedTradeToken != null) {
|
|
return CachedTradeToken;
|
|
}
|
|
|
|
await TradeTokenSemaphore.WaitAsync().ConfigureAwait(false);
|
|
|
|
try {
|
|
if (CachedTradeToken != null) {
|
|
return CachedTradeToken;
|
|
}
|
|
|
|
const string request = "/my/tradeoffers/privacy?l=english";
|
|
HtmlDocument htmlDocument = await UrlGetToHtmlDocumentRetryWithSession(SteamCommunityURL, request).ConfigureAwait(false);
|
|
|
|
if (htmlDocument == null) {
|
|
return null;
|
|
}
|
|
|
|
HtmlNode tokenNode = htmlDocument.DocumentNode.SelectSingleNode("//input[@class='trade_offer_access_url']");
|
|
if (tokenNode == null) {
|
|
Bot.ArchiLogger.LogNullError(nameof(tokenNode));
|
|
return null;
|
|
}
|
|
|
|
string value = tokenNode.GetAttributeValue("value", null);
|
|
if (string.IsNullOrEmpty(value)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(value));
|
|
return null;
|
|
}
|
|
|
|
int index = value.IndexOf("token=", StringComparison.Ordinal);
|
|
if (index < 0) {
|
|
Bot.ArchiLogger.LogNullError(nameof(index));
|
|
return null;
|
|
}
|
|
|
|
index += 6;
|
|
if (index + 8 < value.Length) {
|
|
Bot.ArchiLogger.LogNullError(nameof(index));
|
|
return null;
|
|
}
|
|
|
|
CachedTradeToken = value.Substring(index, 8);
|
|
return CachedTradeToken;
|
|
} finally {
|
|
TradeTokenSemaphore.Release();
|
|
}
|
|
}
|
|
|
|
internal async Task<bool?> HandleConfirmation(string deviceID, string confirmationHash, uint time, ulong confirmationID, ulong confirmationKey, bool accept) {
|
|
if (string.IsNullOrEmpty(deviceID) || string.IsNullOrEmpty(confirmationHash) || (time == 0) || (confirmationID == 0) || (confirmationKey == 0)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(deviceID) + " || " + nameof(confirmationHash) + " || " + nameof(time) + " || " + nameof(confirmationID) + " || " + nameof(confirmationKey));
|
|
return null;
|
|
}
|
|
|
|
string request = "/mobileconf/ajaxop?op=" + (accept ? "allow" : "cancel") + "&p=" + deviceID + "&a=" + SteamID + "&k=" + WebUtility.UrlEncode(confirmationHash) + "&t=" + time + "&m=android&tag=conf&cid=" + confirmationID + "&ck=" + confirmationKey;
|
|
|
|
Steam.ConfirmationResponse response = await UrlGetToObjectRetryWithSession<Steam.ConfirmationResponse>(SteamCommunityURL, request).ConfigureAwait(false);
|
|
return response?.Success;
|
|
}
|
|
|
|
internal async Task<bool?> HandleConfirmations(string deviceID, string confirmationHash, uint time, IReadOnlyCollection<MobileAuthenticator.Confirmation> confirmations, bool accept) {
|
|
if (string.IsNullOrEmpty(deviceID) || string.IsNullOrEmpty(confirmationHash) || (time == 0) || (confirmations == null) || (confirmations.Count == 0)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(deviceID) + " || " + nameof(confirmationHash) + " || " + nameof(time) + " || " + nameof(confirmations));
|
|
return null;
|
|
}
|
|
|
|
const string request = "/mobileconf/multiajaxop";
|
|
|
|
// Extra entry for sessionID
|
|
List<KeyValuePair<string, string>> data = new List<KeyValuePair<string, string>>(8 + confirmations.Count * 2) {
|
|
new KeyValuePair<string, string>("op", accept ? "allow" : "cancel"),
|
|
new KeyValuePair<string, string>("p", deviceID),
|
|
new KeyValuePair<string, string>("a", SteamID.ToString()),
|
|
new KeyValuePair<string, string>("k", confirmationHash),
|
|
new KeyValuePair<string, string>("t", time.ToString()),
|
|
new KeyValuePair<string, string>("m", "android"),
|
|
new KeyValuePair<string, string>("tag", "conf")
|
|
};
|
|
|
|
foreach (MobileAuthenticator.Confirmation confirmation in confirmations) {
|
|
data.Add(new KeyValuePair<string, string>("cid[]", confirmation.ID.ToString()));
|
|
data.Add(new KeyValuePair<string, string>("ck[]", confirmation.Key.ToString()));
|
|
}
|
|
|
|
Steam.ConfirmationResponse response = await UrlPostToObjectRetryWithSession<Steam.ConfirmationResponse>(SteamCommunityURL, request, data).ConfigureAwait(false);
|
|
return response?.Success;
|
|
}
|
|
|
|
internal async Task<bool> HasPublicInventory() {
|
|
if (CachedPublicInventory.HasValue) {
|
|
return CachedPublicInventory.Value;
|
|
}
|
|
|
|
// 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> Init(ulong steamID, EUniverse universe, string webAPIUserNonce, string parentalPin, string vanityURL = null) {
|
|
if ((steamID == 0) || (universe == EUniverse.Invalid) || string.IsNullOrEmpty(webAPIUserNonce) || string.IsNullOrEmpty(parentalPin)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(universe) + " || " + nameof(webAPIUserNonce) + " || " + nameof(parentalPin));
|
|
return false;
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(vanityURL)) {
|
|
VanityURL = vanityURL;
|
|
}
|
|
|
|
string sessionID = Convert.ToBase64String(Encoding.UTF8.GetBytes(steamID.ToString()));
|
|
|
|
// Generate an AES session key
|
|
byte[] sessionKey = SteamKit2.CryptoHelper.GenerateRandomBlock(32);
|
|
|
|
// RSA encrypt it with the public key for the universe we're on
|
|
byte[] cryptedSessionKey;
|
|
using (RSACrypto rsa = new RSACrypto(KeyDictionary.GetPublicKey(universe))) {
|
|
cryptedSessionKey = rsa.Encrypt(sessionKey);
|
|
}
|
|
|
|
// Copy our login key
|
|
byte[] loginKey = new byte[webAPIUserNonce.Length];
|
|
Array.Copy(Encoding.ASCII.GetBytes(webAPIUserNonce), loginKey, webAPIUserNonce.Length);
|
|
|
|
// AES encrypt the loginkey with our session key
|
|
byte[] cryptedLoginKey = SteamKit2.CryptoHelper.SymmetricEncrypt(loginKey, sessionKey);
|
|
|
|
// Do the magic
|
|
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.LoggingIn, ISteamUserAuth));
|
|
|
|
KeyValue authResult = null;
|
|
using (dynamic iSteamUserAuth = WebAPI.GetAsyncInterface(ISteamUserAuth)) {
|
|
iSteamUserAuth.Timeout = WebBrowser.Timeout;
|
|
|
|
try {
|
|
authResult = await iSteamUserAuth.AuthenticateUser(
|
|
steamid: steamID,
|
|
sessionkey: Encoding.ASCII.GetString(WebUtility.UrlEncodeToBytes(cryptedSessionKey, 0, cryptedSessionKey.Length)),
|
|
encrypted_loginkey: Encoding.ASCII.GetString(WebUtility.UrlEncodeToBytes(cryptedLoginKey, 0, cryptedLoginKey.Length)),
|
|
method: WebRequestMethods.Http.Post,
|
|
secure: true
|
|
);
|
|
} catch (TaskCanceledException e) {
|
|
Bot.ArchiLogger.LogGenericDebuggingException(e);
|
|
} catch (Exception e) {
|
|
Bot.ArchiLogger.LogGenericWarningException(e);
|
|
}
|
|
}
|
|
|
|
if (authResult == null) {
|
|
return false;
|
|
}
|
|
|
|
string steamLogin = authResult["token"].Value;
|
|
if (string.IsNullOrEmpty(steamLogin)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(steamLogin));
|
|
return false;
|
|
}
|
|
|
|
string steamLoginSecure = authResult["tokensecure"].Value;
|
|
if (string.IsNullOrEmpty(steamLoginSecure)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(steamLoginSecure));
|
|
return false;
|
|
}
|
|
|
|
WebBrowser.CookieContainer.Add(new Cookie("sessionid", sessionID, "/", "." + SteamCommunityHost));
|
|
WebBrowser.CookieContainer.Add(new Cookie("sessionid", sessionID, "/", "." + SteamStoreHost));
|
|
|
|
WebBrowser.CookieContainer.Add(new Cookie("steamLogin", steamLogin, "/", "." + SteamCommunityHost));
|
|
WebBrowser.CookieContainer.Add(new Cookie("steamLogin", steamLogin, "/", "." + SteamStoreHost));
|
|
|
|
WebBrowser.CookieContainer.Add(new Cookie("steamLoginSecure", steamLoginSecure, "/", "." + SteamCommunityHost));
|
|
WebBrowser.CookieContainer.Add(new Cookie("steamLoginSecure", steamLoginSecure, "/", "." + SteamStoreHost));
|
|
|
|
Bot.ArchiLogger.LogGenericInfo(Strings.Success);
|
|
|
|
// Unlock Steam Parental if needed
|
|
if (!parentalPin.Equals("0")) {
|
|
if (!await UnlockParentalAccount(parentalPin).ConfigureAwait(false)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
SteamID = steamID;
|
|
LastSessionRefresh = DateTime.UtcNow;
|
|
return true;
|
|
}
|
|
|
|
internal async Task<bool> JoinGroup(ulong groupID) {
|
|
if (groupID == 0) {
|
|
Bot.ArchiLogger.LogNullError(nameof(groupID));
|
|
return false;
|
|
}
|
|
|
|
string request = "/gid/" + groupID;
|
|
|
|
// Extra entry for sessionID
|
|
Dictionary<string, string> data = new Dictionary<string, string>(2) { { "action", "join" } };
|
|
|
|
return await UrlPostRetryWithSession(SteamCommunityURL, request, data).ConfigureAwait(false);
|
|
}
|
|
|
|
internal async Task MarkInventory() {
|
|
// We aim to have a maximum of 2 tasks, one already working, and one waiting in the queue
|
|
// This way we can call this function as many times as needed e.g. because of Steam events
|
|
lock (InventorySemaphore) {
|
|
if (MarkingInventoryScheduled) {
|
|
return;
|
|
}
|
|
|
|
MarkingInventoryScheduled = true;
|
|
}
|
|
|
|
await InventorySemaphore.WaitAsync().ConfigureAwait(false);
|
|
|
|
try {
|
|
lock (InventorySemaphore) {
|
|
MarkingInventoryScheduled = false;
|
|
}
|
|
|
|
const string request = "/my/inventory";
|
|
await UrlHeadRetryWithSession(SteamCommunityURL, request).ConfigureAwait(false);
|
|
} finally {
|
|
if (Program.GlobalConfig.InventoryLimiterDelay == 0) {
|
|
InventorySemaphore.Release();
|
|
} else {
|
|
Utilities.InBackground(async () => {
|
|
await Task.Delay(Program.GlobalConfig.InventoryLimiterDelay * 1000).ConfigureAwait(false);
|
|
InventorySemaphore.Release();
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
internal async Task<bool> MarkSentTrades() {
|
|
const string request = "/my/tradeoffers/sent";
|
|
return await UrlHeadRetryWithSession(SteamCommunityURL, request).ConfigureAwait(false);
|
|
}
|
|
|
|
internal void OnDisconnected() {
|
|
CachedApiKey = CachedTradeToken = null;
|
|
CachedPublicInventory = null;
|
|
SteamID = 0;
|
|
}
|
|
|
|
internal async Task<(EResult Result, EPurchaseResultDetail? PurchaseResult)?> RedeemWalletKey(string key) {
|
|
if (string.IsNullOrEmpty(key)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(key));
|
|
return null;
|
|
}
|
|
|
|
const string request = "/account/validatewalletcode";
|
|
|
|
// Extra entry for sessionID
|
|
Dictionary<string, string> data = new Dictionary<string, string>(2) { { "wallet_code", key } };
|
|
|
|
Steam.RedeemWalletResponse response = await UrlPostToObjectRetryWithSession<Steam.RedeemWalletResponse>(SteamStoreURL, request, data).ConfigureAwait(false);
|
|
if (response == null) {
|
|
return null;
|
|
}
|
|
|
|
return (response.Result, response.PurchaseResultDetail);
|
|
}
|
|
|
|
internal async Task<bool> SendTradeOffer(IReadOnlyCollection<Steam.Asset> inventory, ulong partnerID, string token = null) {
|
|
if ((inventory == null) || (inventory.Count == 0) || (partnerID == 0)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(inventory) + " || " + nameof(inventory.Count) + " || " + nameof(partnerID));
|
|
return false;
|
|
}
|
|
|
|
Steam.TradeOfferRequest singleTrade = new Steam.TradeOfferRequest();
|
|
HashSet<Steam.TradeOfferRequest> trades = new HashSet<Steam.TradeOfferRequest> { singleTrade };
|
|
|
|
foreach (Steam.Asset item in inventory) {
|
|
if (singleTrade.ItemsToGive.Assets.Count >= Trading.MaxItemsPerTrade) {
|
|
if (trades.Count >= Trading.MaxTradesPerAccount) {
|
|
break;
|
|
}
|
|
|
|
singleTrade = new Steam.TradeOfferRequest();
|
|
trades.Add(singleTrade);
|
|
}
|
|
|
|
singleTrade.ItemsToGive.Assets.Add(item);
|
|
}
|
|
|
|
const string request = "/tradeoffer/new/send";
|
|
const string referer = SteamCommunityURL + "/tradeoffer/new";
|
|
|
|
// Extra entry for sessionID
|
|
foreach (Dictionary<string, string> data in trades.Select(trade => new Dictionary<string, string>(6) {
|
|
{ "serverid", "1" },
|
|
{ "partner", partnerID.ToString() },
|
|
{ "tradeoffermessage", "Sent by " + SharedInfo.PublicIdentifier + "/" + SharedInfo.Version },
|
|
{ "json_tradeoffer", JsonConvert.SerializeObject(trade) },
|
|
{ "trade_offer_create_params", string.IsNullOrEmpty(token) ? "" : new JObject { { "trade_offer_access_token", token } }.ToString(Formatting.None) }
|
|
})) {
|
|
if (!await UrlPostRetryWithSession(SteamCommunityURL, request, data, referer).ConfigureAwait(false)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
internal async Task<bool> SteamAwardsVote(byte voteID, uint appID) {
|
|
if ((voteID == 0) || (appID == 0)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(voteID) + " || " + nameof(appID));
|
|
return false;
|
|
}
|
|
|
|
const string request = "/salevote";
|
|
|
|
// Extra entry for sessionID
|
|
Dictionary<string, string> data = new Dictionary<string, string>(3) {
|
|
{ "voteid", voteID.ToString() },
|
|
{ "appid", appID.ToString() }
|
|
};
|
|
|
|
return await UrlPostRetryWithSession(SteamStoreURL, request, data).ConfigureAwait(false);
|
|
}
|
|
|
|
internal async Task<bool> UnpackBooster(uint appID, ulong itemID) {
|
|
if ((appID == 0) || (itemID == 0)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(appID) + " || " + nameof(itemID));
|
|
return false;
|
|
}
|
|
|
|
string request = GetAbsoluteProfileURL() + "/ajaxunpackbooster";
|
|
|
|
// Extra entry for sessionID
|
|
Dictionary<string, string> data = new Dictionary<string, string>(3) {
|
|
{ "appid", appID.ToString() },
|
|
{ "communityitemid", itemID.ToString() }
|
|
};
|
|
|
|
Steam.GenericResponse response = await UrlPostToObjectRetryWithSession<Steam.GenericResponse>(SteamCommunityURL, request, data).ConfigureAwait(false);
|
|
return response?.Result == EResult.OK;
|
|
}
|
|
|
|
private string GetAbsoluteProfileURL() => !string.IsNullOrEmpty(VanityURL) ? "/id/" + VanityURL : "/profiles/" + SteamID;
|
|
|
|
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() {
|
|
const string request = "/dev/apikey?l=english";
|
|
HtmlDocument htmlDocument = await UrlGetToHtmlDocumentRetryWithSession(SteamCommunityURL, request).ConfigureAwait(false);
|
|
|
|
HtmlNode titleNode = htmlDocument?.DocumentNode.SelectSingleNode("//div[@id='mainContents']/h2");
|
|
if (titleNode == null) {
|
|
return (ESteamApiKeyState.Timeout, null);
|
|
}
|
|
|
|
string title = titleNode.InnerText;
|
|
if (string.IsNullOrEmpty(title)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(title));
|
|
return (ESteamApiKeyState.Error, null);
|
|
}
|
|
|
|
if (title.Contains("Access Denied")) {
|
|
return (ESteamApiKeyState.AccessDenied, null);
|
|
}
|
|
|
|
HtmlNode htmlNode = htmlDocument.DocumentNode.SelectSingleNode("//div[@id='bodyContents_ex']/p");
|
|
if (htmlNode == null) {
|
|
Bot.ArchiLogger.LogNullError(nameof(htmlNode));
|
|
return (ESteamApiKeyState.Error, null);
|
|
}
|
|
|
|
string text = htmlNode.InnerText;
|
|
if (string.IsNullOrEmpty(text)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(text));
|
|
return (ESteamApiKeyState.Error, null);
|
|
}
|
|
|
|
if (text.Contains("Registering for a Steam Web API Key")) {
|
|
return (ESteamApiKeyState.NotRegisteredYet, null);
|
|
}
|
|
|
|
int keyIndex = text.IndexOf("Key: ", StringComparison.Ordinal);
|
|
if (keyIndex < 0) {
|
|
Bot.ArchiLogger.LogNullError(nameof(keyIndex));
|
|
return (ESteamApiKeyState.Error, null);
|
|
}
|
|
|
|
keyIndex += 5;
|
|
|
|
if (text.Length <= keyIndex) {
|
|
Bot.ArchiLogger.LogNullError(nameof(text));
|
|
return (ESteamApiKeyState.Error, null);
|
|
}
|
|
|
|
text = text.Substring(keyIndex);
|
|
if (text.Length != 32) {
|
|
Bot.ArchiLogger.LogNullError(nameof(text));
|
|
return (ESteamApiKeyState.Error, null);
|
|
}
|
|
|
|
if (Utilities.IsValidHexadecimalString(text)) {
|
|
return (ESteamApiKeyState.Registered, text);
|
|
}
|
|
|
|
Bot.ArchiLogger.LogNullError(nameof(text));
|
|
return (ESteamApiKeyState.Error, null);
|
|
}
|
|
|
|
private static uint GetAppIDFromMarketHashName(string hashName) {
|
|
if (string.IsNullOrEmpty(hashName)) {
|
|
ASF.ArchiLogger.LogNullError(nameof(hashName));
|
|
return 0;
|
|
}
|
|
|
|
int index = hashName.IndexOf('-');
|
|
if (index <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
return uint.TryParse(hashName.Substring(0, index), out uint appID) ? appID : 0;
|
|
}
|
|
|
|
private static Steam.Asset.EType GetItemType(string name) {
|
|
if (string.IsNullOrEmpty(name)) {
|
|
ASF.ArchiLogger.LogNullError(nameof(name));
|
|
return Steam.Asset.EType.Unknown;
|
|
}
|
|
|
|
switch (name) {
|
|
case "Booster Pack":
|
|
return Steam.Asset.EType.BoosterPack;
|
|
case "Steam Gems":
|
|
return Steam.Asset.EType.SteamGems;
|
|
default:
|
|
if (name.EndsWith("Emoticon", StringComparison.Ordinal)) {
|
|
return Steam.Asset.EType.Emoticon;
|
|
}
|
|
|
|
if (name.EndsWith("Foil Trading Card", StringComparison.Ordinal)) {
|
|
return Steam.Asset.EType.FoilTradingCard;
|
|
}
|
|
|
|
if (name.EndsWith("Profile Background", StringComparison.Ordinal)) {
|
|
return Steam.Asset.EType.ProfileBackground;
|
|
}
|
|
|
|
return name.EndsWith("Trading Card", StringComparison.Ordinal) ? Steam.Asset.EType.TradingCard : Steam.Asset.EType.Unknown;
|
|
}
|
|
}
|
|
|
|
private async Task<bool?> IsInventoryPublic() {
|
|
const string request = "/my/edit/settings?l=english";
|
|
HtmlDocument htmlDocument = await UrlGetToHtmlDocumentRetryWithSession(SteamCommunityURL, request).ConfigureAwait(false);
|
|
|
|
HtmlNode htmlNode = htmlDocument?.DocumentNode.SelectSingleNode("//input[@id='inventoryPrivacySetting_public']");
|
|
if (htmlNode == null) {
|
|
return null;
|
|
}
|
|
|
|
// Notice: checked doesn't have a value - null is lack of attribute, "" is attribute existing
|
|
string state = htmlNode.GetAttributeValue("checked", null);
|
|
|
|
return state != null;
|
|
}
|
|
|
|
private static bool IsSessionExpiredUri(Uri uri) {
|
|
if (uri == null) {
|
|
ASF.ArchiLogger.LogNullError(nameof(uri));
|
|
return false;
|
|
}
|
|
|
|
bool result = uri.AbsolutePath.StartsWith("/login", StringComparison.Ordinal) || uri.Host.Equals("lostauth");
|
|
return result;
|
|
}
|
|
|
|
private static bool ParseItems(Dictionary<ulong, (uint AppID, Steam.Asset.EType Type)> descriptions, IReadOnlyCollection<KeyValue> input, ICollection<Steam.Asset> output) {
|
|
if ((descriptions == null) || (input == null) || (input.Count == 0) || (output == null)) {
|
|
ASF.ArchiLogger.LogNullError(nameof(descriptions) + " || " + nameof(input) + " || " + nameof(output));
|
|
return false;
|
|
}
|
|
|
|
foreach (KeyValue item in input) {
|
|
uint appID = item["appid"].AsUnsignedInteger();
|
|
if (appID == 0) {
|
|
ASF.ArchiLogger.LogNullError(nameof(appID));
|
|
return false;
|
|
}
|
|
|
|
ulong contextID = item["contextid"].AsUnsignedLong();
|
|
if (contextID == 0) {
|
|
ASF.ArchiLogger.LogNullError(nameof(contextID));
|
|
return false;
|
|
}
|
|
|
|
ulong classID = item["classid"].AsUnsignedLong();
|
|
if (classID == 0) {
|
|
ASF.ArchiLogger.LogNullError(nameof(classID));
|
|
return false;
|
|
}
|
|
|
|
uint amount = item["amount"].AsUnsignedInteger();
|
|
if (amount == 0) {
|
|
ASF.ArchiLogger.LogNullError(nameof(amount));
|
|
return false;
|
|
}
|
|
|
|
uint realAppID = appID;
|
|
Steam.Asset.EType type = Steam.Asset.EType.Unknown;
|
|
|
|
if (descriptions.TryGetValue(classID, out (uint AppID, Steam.Asset.EType Type) description)) {
|
|
realAppID = description.AppID;
|
|
type = description.Type;
|
|
}
|
|
|
|
Steam.Asset steamAsset = new Steam.Asset(appID, contextID, classID, amount, realAppID, type);
|
|
output.Add(steamAsset);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private async Task<bool> RefreshSession(string host) {
|
|
if (string.IsNullOrEmpty(host)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(host));
|
|
return false;
|
|
}
|
|
|
|
if (!Bot.IsConnectedAndLoggedOn) {
|
|
return false;
|
|
}
|
|
|
|
DateTime now = DateTime.UtcNow;
|
|
|
|
await SessionSemaphore.WaitAsync().ConfigureAwait(false);
|
|
|
|
try {
|
|
if (now < LastSessionRefresh) {
|
|
return true;
|
|
}
|
|
|
|
if (!Bot.IsConnectedAndLoggedOn) {
|
|
return false;
|
|
}
|
|
|
|
Bot.ArchiLogger.LogGenericInfo(Strings.RefreshingOurSession);
|
|
bool result = await Bot.RefreshSession().ConfigureAwait(false);
|
|
|
|
if (result) {
|
|
LastSessionRefresh = DateTime.UtcNow;
|
|
}
|
|
|
|
return result;
|
|
} finally {
|
|
SessionSemaphore.Release();
|
|
}
|
|
}
|
|
|
|
private async Task<bool> RegisterApiKey() {
|
|
const string request = "/dev/registerkey";
|
|
|
|
// Extra entry for sessionID
|
|
Dictionary<string, string> data = new Dictionary<string, string>(4) {
|
|
{ "domain", "localhost" },
|
|
{ "agreeToTerms", "agreed" },
|
|
{ "Submit", "Register" }
|
|
};
|
|
|
|
return await UrlPostRetryWithSession(SteamCommunityURL, request, data).ConfigureAwait(false);
|
|
}
|
|
|
|
private async Task<bool> UnlockParentalAccount(string parentalPin) {
|
|
if (string.IsNullOrEmpty(parentalPin)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(parentalPin));
|
|
return false;
|
|
}
|
|
|
|
Bot.ArchiLogger.LogGenericInfo(Strings.UnlockingParentalAccount);
|
|
|
|
if (!await UnlockParentalAccountForService(SteamCommunityURL, parentalPin).ConfigureAwait(false)) {
|
|
Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
|
return false;
|
|
}
|
|
|
|
if (!await UnlockParentalAccountForService(SteamStoreURL, parentalPin).ConfigureAwait(false)) {
|
|
Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
|
return false;
|
|
}
|
|
|
|
Bot.ArchiLogger.LogGenericInfo(Strings.Success);
|
|
return true;
|
|
}
|
|
|
|
private async Task<bool> UnlockParentalAccountForService(string serviceURL, string parentalPin) {
|
|
if (string.IsNullOrEmpty(serviceURL) || string.IsNullOrEmpty(parentalPin)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(serviceURL) + " || " + nameof(parentalPin));
|
|
return false;
|
|
}
|
|
|
|
// This request doesn't go through UrlPostRetryWithSession as we have no access to session refresh capability (this is in fact session initialization)
|
|
const string request = "/parental/ajaxunlock";
|
|
|
|
string sessionID = WebBrowser.CookieContainer.GetCookieValue(serviceURL, "sessionid");
|
|
if (string.IsNullOrEmpty(sessionID)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(sessionID));
|
|
return false;
|
|
}
|
|
|
|
Dictionary<string, string> data = new Dictionary<string, string>(2) {
|
|
{ "pin", parentalPin },
|
|
{ "sessionid", sessionID }
|
|
};
|
|
|
|
WebBrowser.BasicResponse response = await WebBrowser.UrlPostRetry(serviceURL + request, data, serviceURL).ConfigureAwait(false);
|
|
return (response != null) && !IsSessionExpiredUri(response.FinalUri);
|
|
}
|
|
|
|
private async Task<HtmlDocument> UrlGetToHtmlDocumentRetryWithSession(string host, string request, byte maxTries = WebBrowser.MaxTries) {
|
|
if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(request)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(host) + " || " + nameof(request));
|
|
return null;
|
|
}
|
|
|
|
if (maxTries == 0) {
|
|
Bot.ArchiLogger.LogGenericWarning(string.Format(Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries));
|
|
Bot.ArchiLogger.LogGenericDebug(string.Format(Strings.ErrorFailingRequest, host + request));
|
|
return null;
|
|
}
|
|
|
|
// If session refresh is already in progress, wait for it
|
|
await SessionSemaphore.WaitAsync().ConfigureAwait(false);
|
|
SessionSemaphore.Release();
|
|
|
|
for (byte i = 0; (i < Program.GlobalConfig.ConnectionTimeout) && (SteamID == 0) && Bot.IsConnectedAndLoggedOn; i++) {
|
|
await Task.Delay(1000).ConfigureAwait(false);
|
|
}
|
|
|
|
if (SteamID == 0) {
|
|
Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
|
Bot.ArchiLogger.LogGenericDebug(string.Format(Strings.ErrorFailingRequest, host + request));
|
|
return null;
|
|
}
|
|
|
|
WebBrowser.HtmlDocumentResponse response = await WebBrowser.UrlGetToHtmlDocumentRetry(host + request).ConfigureAwait(false);
|
|
if (response == null) {
|
|
return null;
|
|
}
|
|
|
|
if (!IsSessionExpiredUri(response.FinalUri)) {
|
|
return response.Content;
|
|
}
|
|
|
|
if (await RefreshSession(host).ConfigureAwait(false)) {
|
|
return await UrlGetToHtmlDocumentRetryWithSession(host, request, --maxTries).ConfigureAwait(false);
|
|
}
|
|
|
|
Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
|
Bot.ArchiLogger.LogGenericDebug(string.Format(Strings.ErrorFailingRequest, host + request));
|
|
return null;
|
|
}
|
|
|
|
private async Task<T> UrlGetToObjectRetryWithSession<T>(string host, string request, byte maxTries = WebBrowser.MaxTries) {
|
|
if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(request)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(host) + " || " + nameof(request));
|
|
return default;
|
|
}
|
|
|
|
if (maxTries == 0) {
|
|
Bot.ArchiLogger.LogGenericWarning(string.Format(Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries));
|
|
Bot.ArchiLogger.LogGenericDebug(string.Format(Strings.ErrorFailingRequest, host + request));
|
|
return default;
|
|
}
|
|
|
|
// If session refresh is already in progress, wait for it
|
|
await SessionSemaphore.WaitAsync().ConfigureAwait(false);
|
|
SessionSemaphore.Release();
|
|
|
|
for (byte i = 0; (i < Program.GlobalConfig.ConnectionTimeout) && (SteamID == 0) && Bot.IsConnectedAndLoggedOn; i++) {
|
|
await Task.Delay(1000).ConfigureAwait(false);
|
|
}
|
|
|
|
if (SteamID == 0) {
|
|
Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
|
Bot.ArchiLogger.LogGenericDebug(string.Format(Strings.ErrorFailingRequest, host + request));
|
|
return default;
|
|
}
|
|
|
|
WebBrowser.ObjectResponse<T> response = await WebBrowser.UrlGetToObjectRetry<T>(host + request).ConfigureAwait(false);
|
|
if (response == null) {
|
|
return default;
|
|
}
|
|
|
|
if (!IsSessionExpiredUri(response.FinalUri)) {
|
|
return response.Content;
|
|
}
|
|
|
|
if (await RefreshSession(host).ConfigureAwait(false)) {
|
|
return await UrlGetToObjectRetryWithSession<T>(host, request, --maxTries).ConfigureAwait(false);
|
|
}
|
|
|
|
Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
|
Bot.ArchiLogger.LogGenericDebug(string.Format(Strings.ErrorFailingRequest, host + request));
|
|
return default;
|
|
}
|
|
|
|
private async Task<XmlDocument> UrlGetToXmlDocumentRetryWithSession(string host, string request, byte maxTries = WebBrowser.MaxTries) {
|
|
if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(request)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(host) + " || " + nameof(request));
|
|
return null;
|
|
}
|
|
|
|
if (maxTries == 0) {
|
|
Bot.ArchiLogger.LogGenericWarning(string.Format(Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries));
|
|
Bot.ArchiLogger.LogGenericDebug(string.Format(Strings.ErrorFailingRequest, host + request));
|
|
return null;
|
|
}
|
|
|
|
// If session refresh is already in progress, wait for it
|
|
await SessionSemaphore.WaitAsync().ConfigureAwait(false);
|
|
SessionSemaphore.Release();
|
|
|
|
for (byte i = 0; (i < Program.GlobalConfig.ConnectionTimeout) && (SteamID == 0) && Bot.IsConnectedAndLoggedOn; i++) {
|
|
await Task.Delay(1000).ConfigureAwait(false);
|
|
}
|
|
|
|
if (SteamID == 0) {
|
|
Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
|
Bot.ArchiLogger.LogGenericDebug(string.Format(Strings.ErrorFailingRequest, host + request));
|
|
return null;
|
|
}
|
|
|
|
WebBrowser.XmlResponse response = await WebBrowser.UrlGetToXmlRetry(host + request).ConfigureAwait(false);
|
|
if (response == null) {
|
|
return null;
|
|
}
|
|
|
|
if (!IsSessionExpiredUri(response.FinalUri)) {
|
|
return response.Content;
|
|
}
|
|
|
|
if (await RefreshSession(host).ConfigureAwait(false)) {
|
|
return await UrlGetToXmlDocumentRetryWithSession(host, request, --maxTries).ConfigureAwait(false);
|
|
}
|
|
|
|
Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
|
Bot.ArchiLogger.LogGenericDebug(string.Format(Strings.ErrorFailingRequest, host + request));
|
|
return null;
|
|
}
|
|
|
|
private async Task<bool> UrlHeadRetryWithSession(string host, string request, byte maxTries = WebBrowser.MaxTries) {
|
|
if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(request)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(host) + " || " + nameof(request));
|
|
return false;
|
|
}
|
|
|
|
if (maxTries == 0) {
|
|
Bot.ArchiLogger.LogGenericWarning(string.Format(Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries));
|
|
Bot.ArchiLogger.LogGenericDebug(string.Format(Strings.ErrorFailingRequest, host + request));
|
|
return false;
|
|
}
|
|
|
|
// If session refresh is already in progress, wait for it
|
|
await SessionSemaphore.WaitAsync().ConfigureAwait(false);
|
|
SessionSemaphore.Release();
|
|
|
|
for (byte i = 0; (i < Program.GlobalConfig.ConnectionTimeout) && (SteamID == 0) && Bot.IsConnectedAndLoggedOn; i++) {
|
|
await Task.Delay(1000).ConfigureAwait(false);
|
|
}
|
|
|
|
if (SteamID == 0) {
|
|
Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
|
Bot.ArchiLogger.LogGenericDebug(string.Format(Strings.ErrorFailingRequest, host + request));
|
|
return false;
|
|
}
|
|
|
|
WebBrowser.BasicResponse response = await WebBrowser.UrlHeadRetry(host + request).ConfigureAwait(false);
|
|
if (response == null) {
|
|
return false;
|
|
}
|
|
|
|
if (!IsSessionExpiredUri(response.FinalUri)) {
|
|
return true;
|
|
}
|
|
|
|
if (await RefreshSession(host).ConfigureAwait(false)) {
|
|
return await UrlHeadRetryWithSession(host, request, --maxTries).ConfigureAwait(false);
|
|
}
|
|
|
|
Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
|
Bot.ArchiLogger.LogGenericDebug(string.Format(Strings.ErrorFailingRequest, host + request));
|
|
return false;
|
|
}
|
|
|
|
private async Task<bool> UrlPostRetryWithSession(string host, string request, Dictionary<string, string> data = null, string referer = null, bool includeSessionInData = true, byte maxTries = WebBrowser.MaxTries) {
|
|
if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(request)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(host) + " || " + nameof(request));
|
|
return false;
|
|
}
|
|
|
|
if (maxTries == 0) {
|
|
Bot.ArchiLogger.LogGenericWarning(string.Format(Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries));
|
|
Bot.ArchiLogger.LogGenericDebug(string.Format(Strings.ErrorFailingRequest, host + request));
|
|
return false;
|
|
}
|
|
|
|
// If session refresh is already in progress, wait for it
|
|
await SessionSemaphore.WaitAsync().ConfigureAwait(false);
|
|
SessionSemaphore.Release();
|
|
|
|
for (byte i = 0; (i < Program.GlobalConfig.ConnectionTimeout) && (SteamID == 0) && Bot.IsConnectedAndLoggedOn; i++) {
|
|
await Task.Delay(1000).ConfigureAwait(false);
|
|
}
|
|
|
|
if (SteamID == 0) {
|
|
Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
|
Bot.ArchiLogger.LogGenericDebug(string.Format(Strings.ErrorFailingRequest, host + request));
|
|
return false;
|
|
}
|
|
|
|
if (includeSessionInData) {
|
|
string sessionID = WebBrowser.CookieContainer.GetCookieValue(host, "sessionid");
|
|
|
|
if (string.IsNullOrEmpty(sessionID)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(sessionID));
|
|
return false;
|
|
}
|
|
|
|
if (data != null) {
|
|
data["sessionid"] = sessionID;
|
|
} else {
|
|
data = new Dictionary<string, string>(1) { { "sessionid", sessionID } };
|
|
}
|
|
}
|
|
|
|
WebBrowser.BasicResponse response = await WebBrowser.UrlPostRetry(host + request, data, referer).ConfigureAwait(false);
|
|
if (response == null) {
|
|
return false;
|
|
}
|
|
|
|
if (!IsSessionExpiredUri(response.FinalUri)) {
|
|
return true;
|
|
}
|
|
|
|
if (await RefreshSession(host).ConfigureAwait(false)) {
|
|
return await UrlPostRetryWithSession(host, request, data, referer, includeSessionInData, --maxTries).ConfigureAwait(false);
|
|
}
|
|
|
|
Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
|
Bot.ArchiLogger.LogGenericDebug(string.Format(Strings.ErrorFailingRequest, host + request));
|
|
return false;
|
|
}
|
|
|
|
private async Task<HtmlDocument> UrlPostToHtmlDocumentRetryWithSession(string host, string request, Dictionary<string, string> data = null, string referer = null, bool includeSessionInData = true, byte maxTries = WebBrowser.MaxTries) {
|
|
if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(request)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(host) + " || " + nameof(request));
|
|
return null;
|
|
}
|
|
|
|
if (maxTries == 0) {
|
|
Bot.ArchiLogger.LogGenericWarning(string.Format(Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries));
|
|
Bot.ArchiLogger.LogGenericDebug(string.Format(Strings.ErrorFailingRequest, host + request));
|
|
return null;
|
|
}
|
|
|
|
// If session refresh is already in progress, wait for it
|
|
await SessionSemaphore.WaitAsync().ConfigureAwait(false);
|
|
SessionSemaphore.Release();
|
|
|
|
for (byte i = 0; (i < Program.GlobalConfig.ConnectionTimeout) && (SteamID == 0) && Bot.IsConnectedAndLoggedOn; i++) {
|
|
await Task.Delay(1000).ConfigureAwait(false);
|
|
}
|
|
|
|
if (SteamID == 0) {
|
|
Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
|
Bot.ArchiLogger.LogGenericDebug(string.Format(Strings.ErrorFailingRequest, host + request));
|
|
return null;
|
|
}
|
|
|
|
if (includeSessionInData) {
|
|
string sessionID = WebBrowser.CookieContainer.GetCookieValue(host, "sessionid");
|
|
|
|
if (string.IsNullOrEmpty(sessionID)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(sessionID));
|
|
return null;
|
|
}
|
|
|
|
if (data != null) {
|
|
data["sessionid"] = sessionID;
|
|
} else {
|
|
data = new Dictionary<string, string>(1) { { "sessionid", sessionID } };
|
|
}
|
|
}
|
|
|
|
WebBrowser.HtmlDocumentResponse response = await WebBrowser.UrlPostToHtmlDocumentRetry(host + request, data, referer).ConfigureAwait(false);
|
|
if (response == null) {
|
|
return null;
|
|
}
|
|
|
|
if (!IsSessionExpiredUri(response.FinalUri)) {
|
|
return response.Content;
|
|
}
|
|
|
|
if (await RefreshSession(host).ConfigureAwait(false)) {
|
|
return await UrlPostToHtmlDocumentRetryWithSession(host, request, data, referer, includeSessionInData, --maxTries).ConfigureAwait(false);
|
|
}
|
|
|
|
Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
|
Bot.ArchiLogger.LogGenericDebug(string.Format(Strings.ErrorFailingRequest, host + request));
|
|
return null;
|
|
}
|
|
|
|
private async Task<T> UrlPostToObjectRetryWithSession<T>(string host, string request, Dictionary<string, string> data = null, string referer = null, bool includeSessionInData = true, byte maxTries = WebBrowser.MaxTries) {
|
|
if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(request)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(host) + " || " + nameof(request));
|
|
return default;
|
|
}
|
|
|
|
if (maxTries == 0) {
|
|
Bot.ArchiLogger.LogGenericWarning(string.Format(Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries));
|
|
Bot.ArchiLogger.LogGenericDebug(string.Format(Strings.ErrorFailingRequest, host + request));
|
|
return default;
|
|
}
|
|
|
|
// If session refresh is already in progress, wait for it
|
|
await SessionSemaphore.WaitAsync().ConfigureAwait(false);
|
|
SessionSemaphore.Release();
|
|
|
|
for (byte i = 0; (i < Program.GlobalConfig.ConnectionTimeout) && (SteamID == 0) && Bot.IsConnectedAndLoggedOn; i++) {
|
|
await Task.Delay(1000).ConfigureAwait(false);
|
|
}
|
|
|
|
if (SteamID == 0) {
|
|
Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
|
Bot.ArchiLogger.LogGenericDebug(string.Format(Strings.ErrorFailingRequest, host + request));
|
|
return default;
|
|
}
|
|
|
|
if (includeSessionInData) {
|
|
string sessionID = WebBrowser.CookieContainer.GetCookieValue(host, "sessionid");
|
|
|
|
if (string.IsNullOrEmpty(sessionID)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(sessionID));
|
|
return default;
|
|
}
|
|
|
|
if (data != null) {
|
|
data["sessionid"] = sessionID;
|
|
} else {
|
|
data = new Dictionary<string, string>(1) { { "sessionid", sessionID } };
|
|
}
|
|
}
|
|
|
|
WebBrowser.ObjectResponse<T> response = await WebBrowser.UrlPostToObjectRetry<T>(host + request, data, referer).ConfigureAwait(false);
|
|
if (response == null) {
|
|
return default;
|
|
}
|
|
|
|
if (!IsSessionExpiredUri(response.FinalUri)) {
|
|
return response.Content;
|
|
}
|
|
|
|
if (await RefreshSession(host).ConfigureAwait(false)) {
|
|
return await UrlPostToObjectRetryWithSession<T>(host, request, data, referer, includeSessionInData, --maxTries).ConfigureAwait(false);
|
|
}
|
|
|
|
Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
|
Bot.ArchiLogger.LogGenericDebug(string.Format(Strings.ErrorFailingRequest, host + request));
|
|
return default;
|
|
}
|
|
|
|
private async Task<T> UrlPostToObjectRetryWithSession<T>(string host, string request, List<KeyValuePair<string, string>> data = null, string referer = null, bool includeSessionInData = true, byte maxTries = WebBrowser.MaxTries) {
|
|
if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(request)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(host) + " || " + nameof(request));
|
|
return default;
|
|
}
|
|
|
|
if (maxTries == 0) {
|
|
Bot.ArchiLogger.LogGenericWarning(string.Format(Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries));
|
|
Bot.ArchiLogger.LogGenericDebug(string.Format(Strings.ErrorFailingRequest, host + request));
|
|
return default;
|
|
}
|
|
|
|
// If session refresh is already in progress, wait for it
|
|
await SessionSemaphore.WaitAsync().ConfigureAwait(false);
|
|
SessionSemaphore.Release();
|
|
|
|
for (byte i = 0; (i < Program.GlobalConfig.ConnectionTimeout) && (SteamID == 0) && Bot.IsConnectedAndLoggedOn; i++) {
|
|
await Task.Delay(1000).ConfigureAwait(false);
|
|
}
|
|
|
|
if (SteamID == 0) {
|
|
Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
|
Bot.ArchiLogger.LogGenericDebug(string.Format(Strings.ErrorFailingRequest, host + request));
|
|
return default;
|
|
}
|
|
|
|
if (includeSessionInData) {
|
|
string sessionID = WebBrowser.CookieContainer.GetCookieValue(host, "sessionid");
|
|
|
|
if (string.IsNullOrEmpty(sessionID)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(sessionID));
|
|
return default;
|
|
}
|
|
|
|
KeyValuePair<string, string> sessionValue = new KeyValuePair<string, string>("sessionid", sessionID);
|
|
|
|
if (data != null) {
|
|
data.Remove(sessionValue);
|
|
data.Add(sessionValue);
|
|
} else {
|
|
data = new List<KeyValuePair<string, string>>(1) { sessionValue };
|
|
}
|
|
}
|
|
|
|
WebBrowser.ObjectResponse<T> response = await WebBrowser.UrlPostToObjectRetry<T>(host + request, data, referer).ConfigureAwait(false);
|
|
if (response == null) {
|
|
return default;
|
|
}
|
|
|
|
if (!IsSessionExpiredUri(response.FinalUri)) {
|
|
return response.Content;
|
|
}
|
|
|
|
if (await RefreshSession(host).ConfigureAwait(false)) {
|
|
return await UrlPostToObjectRetryWithSession<T>(host, request, data, referer, includeSessionInData, --maxTries).ConfigureAwait(false);
|
|
}
|
|
|
|
Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
|
Bot.ArchiLogger.LogGenericDebug(string.Format(Strings.ErrorFailingRequest, host + request));
|
|
return default;
|
|
}
|
|
|
|
private enum ESteamApiKeyState : byte {
|
|
Error,
|
|
Timeout,
|
|
Registered,
|
|
NotRegisteredYet,
|
|
AccessDenied
|
|
}
|
|
}
|
|
} |