Compare commits

..

63 Commits

Author SHA1 Message Date
JustArchi
9f4ed24704 And copy it... 2015-12-18 07:01:03 +01:00
JustArchi
015c6b7bdf Add minimal.xml to VS project 2015-12-18 06:59:47 +01:00
JustArchi
a17c1fc35a VS please 2015-12-18 06:56:33 +01:00
JustArchi
19e46ce78d Misc 2015-12-18 06:54:53 +01:00
JustArchi
cf6ee3b60d Switch to new async methods 2015-12-17 22:04:13 +01:00
JustArchi
488003993f Do not wait longer than 5 sec 2015-12-17 22:00:24 +01:00
JustArchi
c64a6fabbc SteamAuth sync 2015-12-17 21:59:32 +01:00
JustArchi
eb2751861d Don't forget to reset status back to false after the test, closes #34 2015-12-17 20:57:03 +01:00
JustArchi
48a1cf1189 Handle expired StartFarming too 2015-12-16 22:14:53 +01:00
JustArchi
98e1d51a48 Handle expired sessionIDs for main accounts 2015-12-16 22:05:42 +01:00
JustArchi
5e22c832a2 Do not reject friend invites, group invites, and trades, by default, closes #33 2015-12-16 20:09:31 +01:00
JustArchi
6523b30f10 Always use LoginID, closes #32 2015-12-16 20:01:40 +01:00
JustArchi
8fefa7c5af Use random UniqueID for LoginID 2015-12-16 10:21:13 +01:00
JustArchi
b6a5b8942c Actually do not wait 25 minutes after expired login keys 2015-12-16 09:39:03 +01:00
JustArchi
cd622989d6 SteamAuth sync 2015-12-16 01:30:02 +01:00
JustArchi
8251274bf8 Version bump 2015-12-16 01:23:56 +01:00
JustArchi
dd63c97ced Clean it up a little 2015-12-16 01:20:54 +01:00
JustArchi
b17960096e Internal improvements 2015-12-16 01:18:58 +01:00
JustArchi
83934af041 Bump to 0.9.1 2015-12-13 21:06:08 +01:00
JustArchi
0421da30cd Fix derp, closes #30 2015-12-13 21:05:01 +01:00
Łukasz Domeradzki
bf084a1ffb Update README.md 2015-12-13 15:56:29 +01:00
JustArchi
f3868b9acc Misc 2015-12-13 15:28:03 +01:00
JustArchi
7c3d195fd4 Bugfixes, closes #28 2015-12-13 15:25:00 +01:00
JustArchi
4d7294feac Change a bit the logic of ASF 2FA to handle GeneralFailures, closes #27 2015-12-13 13:51:58 +01:00
JustArchi
6877abed6e Add workaround for expired login keys, closes #26 2015-12-13 13:00:23 +01:00
JustArchi
5353d755a0 Misc cleanup 2015-12-12 08:04:41 +01:00
JustArchi
d2dff449d1 Periodically check for new games to farm 2015-12-12 07:54:14 +01:00
JustArchi
d526c6c1ce Fix SMS problems, resync SteamAuth 2015-12-12 06:54:25 +01:00
JustArchi
49fc908f0f ToRevert: Add hacked LogOn, revert when pull request is accepted 2015-12-12 06:05:10 +01:00
JustArchi
b3162ce390 Squashed commit of ASF 2FA
Check https://github.com/JustArchi/ArchiSteamFarm/wiki/Escrow for more info
2015-12-11 22:53:28 +01:00
JustArchi
9a2a37f2fc Misc 2015-12-09 21:11:49 +01:00
JustArchi
a90b3afcdd Add some delay for LoggedInElsewhere 2015-12-09 20:55:43 +01:00
Łukasz Domeradzki
6c903b2433 Update README.md 2015-12-09 19:56:57 +01:00
JustArchi
00558441fe Misc 2015-12-06 20:12:44 +01:00
JustArchi
7e10e6ba81 Push 2FA requests ahead of queue 2015-12-06 19:38:09 +01:00
JustArchi
3995b3a083 Correct anti-captcha delay 2015-12-04 16:47:53 +01:00
JustArchi
e6c9c3f0a9 One more fix to #22 2015-12-02 17:17:47 +01:00
JustArchi
c8fc25111e Further optimize new web browser 2015-12-01 01:34:05 +01:00
JustArchi
6b3cf5e788 Add missing license notice 2015-11-29 00:13:03 +01:00
JustArchi
afbd8d12b4 Version bump 2015-11-28 20:47:32 +01:00
JustArchi
e8e393b320 Correct anti-captcha delay 2015-11-27 16:25:03 +01:00
JustArchi
84db370516 Try to implement some anty-captcha, #22 2015-11-26 01:22:56 +01:00
JustArchi
173148ce25 Add support for longer cd-keys 2015-11-25 16:49:01 +01:00
JustArchi
0fd0528fd0 Use global timeout 2015-11-25 16:35:09 +01:00
JustArchi
664a0e0758 Use new ArchiBoT's WebBrowser 2015-11-25 16:31:39 +01:00
JustArchi
6454e3ee12 Packages update 2015-11-25 05:35:14 +01:00
Łukasz Domeradzki
33f4a82dd5 Sometimes Git SHA1 hash is everything you need 2015-11-22 19:42:00 +01:00
Łukasz Domeradzki
9bd6532411 Update README.md 2015-11-22 01:50:17 +01:00
JustArchi
b89d2977f8 10 hours should be enough 2015-11-22 01:45:58 +01:00
JustArchi
25b557d5d1 Misc 2015-11-22 01:40:44 +01:00
JustArchi
3e34fabfaf Avoid possible steam network fuckups 2015-11-22 01:22:45 +01:00
Łukasz Domeradzki
fc49e8ffa7 Update README.md 2015-11-22 01:08:12 +01:00
JustArchi
48a5eeec29 Misc 2015-11-21 21:30:49 +01:00
JustArchi
7fa176ef7b Fix current status to match actual situation 2015-11-21 21:28:34 +01:00
JustArchi
bfaa99a8a9 Revolution is here 2015-11-21 21:00:46 +01:00
JustArchi
1ff4ed026e Prepare configs for future cards farm algorithms 2015-11-21 18:27:58 +01:00
JustArchi
1703bbfb68 Version bump 2015-11-20 19:08:55 +01:00
JustArchi
a6a2e1605d Misc 2015-11-20 14:47:16 +01:00
JustArchi
c0c61913c2 Fix crash related to trading 2015-11-19 18:34:18 +01:00
JustArchi
fb2437d75d Code cleanup & improvements of previous pull request 2015-11-18 22:10:24 +01:00
Łukasz Domeradzki
499e55c473 Merge pull request #15 from Ryzhehvost/statusplus
added extended !status syntax
2015-11-18 21:59:40 +01:00
jogger
dfd4513b16 added extended !status syntax 2015-11-18 22:15:10 +02:00
JustArchi
f873da7236 Packages update 2015-11-18 14:49:31 +01:00
54 changed files with 2885 additions and 380 deletions

3
.gitignore vendored
View File

@@ -2,9 +2,10 @@
## ArchiSteamFarm
#################
# Ignore all config files, apart from example.xml
# Ignore all config files, apart from ones we want to include
ArchiSteamFarm/config/*
!ArchiSteamFarm/config/example.xml
!ArchiSteamFarm/config/minimal.xml
#################
## Eclipse

View File

@@ -5,6 +5,8 @@ VisualStudioVersion = 14.0.23107.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArchiSteamFarm", "ArchiSteamFarm\ArchiSteamFarm.csproj", "{35AF7887-08B9-40E8-A5EA-797D8B60B30C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SteamAuth", "SteamAuth\SteamAuth.csproj", "{5AD0934E-F6C4-4AE5-83AF-C788313B2A87}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -15,6 +17,10 @@ Global
{35AF7887-08B9-40E8-A5EA-797D8B60B30C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{35AF7887-08B9-40E8-A5EA-797D8B60B30C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{35AF7887-08B9-40E8-A5EA-797D8B60B30C}.Release|Any CPU.Build.0 = Release|Any CPU
{5AD0934E-F6C4-4AE5-83AF-C788313B2A87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5AD0934E-F6C4-4AE5-83AF-C788313B2A87}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5AD0934E-F6C4-4AE5-83AF-C788313B2A87}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5AD0934E-F6C4-4AE5-83AF-C788313B2A87}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2"/>
</startup>
</configuration>
</configuration>

View File

@@ -24,6 +24,7 @@
using SteamKit2;
using SteamKit2.Internal;
using System;
using System.Collections.Generic;
using System.IO;
@@ -90,9 +91,23 @@ namespace ArchiSteamFarm {
Client.Send(request);
}
internal void PlayGames(params ulong[] gameIDs) {
internal void PlayGames(params uint[] gameIDs) {
var request = new ClientMsgProtobuf<CMsgClientGamesPlayed>(EMsg.ClientGamesPlayed);
foreach (ulong gameID in gameIDs) {
foreach (uint gameID in gameIDs) {
if (gameID == 0) {
continue;
}
request.Body.games_played.Add(new CMsgClientGamesPlayed.GamePlayed {
game_id = new GameID(gameID),
});
}
Client.Send(request);
}
internal void PlayGames(ICollection<uint> gameIDs) {
var request = new ClientMsgProtobuf<CMsgClientGamesPlayed>(EMsg.ClientGamesPlayed);
foreach (uint gameID in gameIDs) {
if (gameID == 0) {
continue;
}
@@ -137,5 +152,60 @@ namespace ArchiSteamFarm {
Client.PostCallback(new NotificationCallback(notification));
}
}
// TODO: Please remove me entirely once https://github.com/SteamRE/SteamKit/pull/217 gets merged
internal void HackedLogOn(uint id, SteamUser.LogOnDetails details) {
if (details == null) {
throw new ArgumentNullException("details");
}
if (string.IsNullOrEmpty(details.Username) || (string.IsNullOrEmpty(details.Password) && string.IsNullOrEmpty(details.LoginKey))) {
throw new ArgumentException("LogOn requires a username and password to be set in 'details'.");
}
if (!string.IsNullOrEmpty(details.LoginKey) && !details.ShouldRememberPassword) {
// Prevent consumers from screwing this up.
// If should_remember_password is false, the login_key is ignored server-side.
// The inverse is not applicable (you can log in with should_remember_password and no login_key).
throw new ArgumentException("ShouldRememberPassword is required to be set to true in order to use LoginKey.");
}
if (!Client.IsConnected) {
return;
}
var logon = new ClientMsgProtobuf<CMsgClientLogon>(EMsg.ClientLogon);
SteamID steamId = new SteamID(details.AccountID, details.AccountInstance, Client.ConnectedUniverse, EAccountType.Individual);
logon.ProtoHeader.client_sessionid = 0;
logon.ProtoHeader.steamid = steamId.ConvertToUInt64();
logon.Body.obfustucated_private_ip = id;
logon.Body.account_name = details.Username;
logon.Body.password = details.Password;
logon.Body.should_remember_password = details.ShouldRememberPassword;
logon.Body.protocol_version = MsgClientLogon.CurrentProtocol;
logon.Body.client_os_type = (uint) details.ClientOSType;
logon.Body.client_language = details.ClientLanguage;
logon.Body.cell_id = details.CellID;
logon.Body.steam2_ticket_request = details.RequestSteam2Ticket;
logon.Body.client_package_version = 1771;
// steam guard
logon.Body.auth_code = details.AuthCode;
logon.Body.two_factor_code = details.TwoFactorCode;
logon.Body.login_key = details.LoginKey;
logon.Body.sha_sentryfile = details.SentryFileHash;
logon.Body.eresult_sentryfile = (int) (details.SentryFileHash != null ? EResult.OK : EResult.FileNotFound);
Client.Send(logon);
}
}
}

View File

@@ -9,7 +9,7 @@
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>ArchiSteamFarm</RootNamespace>
<AssemblyName>ArchiSteamFarm</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<IsWebBootstrapper>false</IsWebBootstrapper>
<PublishUrl>publish\</PublishUrl>
@@ -26,6 +26,7 @@
<ApplicationVersion>1.0.0.%2a</ApplicationVersion>
<UseApplicationTrust>false</UseApplicationTrust>
<BootstrapperEnabled>true</BootstrapperEnabled>
<TargetFrameworkProfile />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
@@ -50,6 +51,8 @@
<DocumentationFile>
</DocumentationFile>
<Prefer32Bit>true</Prefer32Bit>
<GenerateSerializationAssemblies>Auto</GenerateSerializationAssemblies>
<UseVSHostingProcess>false</UseVSHostingProcess>
</PropertyGroup>
<PropertyGroup>
<ApplicationIcon>cirno.ico</ApplicationIcon>
@@ -63,7 +66,7 @@
<Private>True</Private>
</Reference>
<Reference Include="Newtonsoft.Json, Version=8.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.8.0.1-beta1\lib\net45\Newtonsoft.Json.dll</HintPath>
<HintPath>..\packages\Newtonsoft.Json.8.0.1-beta3\lib\net45\Newtonsoft.Json.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="protobuf-net, Version=2.0.0.668, Culture=neutral, PublicKeyToken=257b51d87d2e4d67, processorArchitecture=MSIL">
@@ -97,6 +100,7 @@
<Compile Include="SteamTradeOffer.cs" />
<Compile Include="Trading.cs" />
<Compile Include="Utilities.cs" />
<Compile Include="WebBrowser.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
@@ -119,6 +123,15 @@
<Content Include="config\example.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="config\minimal.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SteamAuth\SteamAuth.csproj">
<Project>{5ad0934e-f6c4-4ae5-83af-c788313b2a87}</Project>
<Name>SteamAuth</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
@@ -129,6 +142,7 @@
<PostBuildEvent Condition=" '$(OS)' != 'Unix' ">if $(ConfigurationName) == Release (
mkdir "$(TargetDir)out" "$(TargetDir)out\config"
copy "$(TargetDir)config\example.xml" "$(TargetDir)out\config"
copy "$(TargetDir)config\minimal.xml" "$(TargetDir)out\config"
"$(SolutionDir)tools\ILMerge.exe" /out:"$(TargetDir)out\ASF.exe" "$(TargetDir)$(TargetName).exe" "$(TargetDir)*.dll" /target:exe /targetplatform:v4,C:\Windows\Microsoft.NET\Framework64\v4.0.30319 /wildcards
del "$(TargetDir)out\ASF.pdb"
)</PostBuildEvent>

View File

@@ -33,14 +33,15 @@ using System.Text;
using System.Threading.Tasks;
namespace ArchiSteamFarm {
internal class ArchiWebHandler {
private const int Timeout = 1000 * 15; // In miliseconds
internal sealed class ArchiWebHandler {
private const int Timeout = 1000 * WebBrowser.HttpTimeout; // In miliseconds
private readonly Bot Bot;
private readonly string ApiKey;
private readonly Dictionary<string, string> SteamCookieDictionary = new Dictionary<string, string>();
private ulong SteamID;
private string VanityURL;
private readonly Dictionary<string, string> SteamCookieDictionary = new Dictionary<string, string>();
// This is required because home_process request must be done on final URL
private string GetHomeProcess() {
@@ -130,7 +131,7 @@ namespace ArchiSteamFarm {
{"pin", parentalPin}
};
HttpResponseMessage response = await Utilities.UrlPostRequestWithResponse("https://steamcommunity.com/parental/ajaxunlock", postData, SteamCookieDictionary, "https://steamcommunity.com/").ConfigureAwait(false);
HttpResponseMessage response = await WebBrowser.UrlPost("https://steamcommunity.com/parental/ajaxunlock", postData, SteamCookieDictionary, "https://steamcommunity.com/").ConfigureAwait(false);
if (response != null && response.IsSuccessStatusCode) {
Logging.LogGenericInfo(Bot.BotName, "Success!");
@@ -147,8 +148,31 @@ namespace ArchiSteamFarm {
Logging.LogGenericInfo(Bot.BotName, "Failed!");
}
}
}
Bot.Trading.CheckTrades();
internal async Task<bool?> IsLoggedIn() {
if (SteamID == 0) {
return false;
}
HtmlDocument htmlDocument = await WebBrowser.UrlGetToHtmlDocument("http://steamcommunity.com/my/profile", SteamCookieDictionary).ConfigureAwait(false);
if (htmlDocument == null) {
return null;
}
HtmlNode htmlNode = htmlDocument.DocumentNode.SelectSingleNode("//span[@id='account_pulldown']");
return htmlNode != null;
}
internal async Task<bool> ReconnectIfNeeded() {
bool? isLoggedIn = await IsLoggedIn().ConfigureAwait(false);
if (isLoggedIn.HasValue && !isLoggedIn.Value) {
Logging.LogGenericInfo(Bot.BotName, "Reconnecting because our sessionID expired!");
Bot.SteamClient.Disconnect(); // Bot will handle reconnect
return true;
}
return false;
}
internal List<SteamTradeOffer> GetTradeOffers() {
@@ -191,7 +215,9 @@ namespace ArchiSteamFarm {
is_our_offer = trade["is_our_offer"].AsBoolean(),
time_created = trade["time_created"].AsInteger(),
time_updated = trade["time_updated"].AsInteger(),
from_real_time_trade = trade["from_real_time_trade"].AsBoolean()
from_real_time_trade = trade["from_real_time_trade"].AsBoolean(),
escrow_end_date = trade["escrow_end_date"].AsInteger(),
confirmation_method = (SteamTradeOffer.ETradeOfferConfirmationMethod) trade["confirmation_method"].AsInteger()
};
foreach (KeyValue item in trade["items_to_give"].Children) {
tradeOffer.items_to_give.Add(new SteamItem {
@@ -240,7 +266,7 @@ namespace ArchiSteamFarm {
{"action", "join"}
};
await Utilities.UrlPostRequest(request, postData, SteamCookieDictionary).ConfigureAwait(false);
await WebBrowser.UrlPost(request, postData, SteamCookieDictionary).ConfigureAwait(false);
}
internal async Task LeaveClan(ulong clanID) {
@@ -259,7 +285,8 @@ namespace ArchiSteamFarm {
{"action", "leaveGroup"},
{"groupId", clanID.ToString()}
};
await Utilities.UrlPostRequest(request, postData, SteamCookieDictionary).ConfigureAwait(false);
await WebBrowser.UrlPost(request, postData, SteamCookieDictionary).ConfigureAwait(false);
}
internal async Task<bool> AcceptTradeOffer(ulong tradeID) {
@@ -281,7 +308,11 @@ namespace ArchiSteamFarm {
{"tradeofferid", tradeID.ToString()}
};
HttpResponseMessage result = await Utilities.UrlPostRequestWithResponse(request, postData, SteamCookieDictionary, referer).ConfigureAwait(false);
HttpResponseMessage result = await WebBrowser.UrlPost(request, postData, SteamCookieDictionary, referer).ConfigureAwait(false);
if (result == null) {
return false;
}
bool success = result.IsSuccessStatusCode;
if (!success) {
@@ -333,7 +364,7 @@ namespace ArchiSteamFarm {
return null;
}
return await Utilities.UrlToHtmlDocument("http://steamcommunity.com/profiles/" + SteamID + "/badges?p=" + page, SteamCookieDictionary).ConfigureAwait(false);
return await WebBrowser.UrlGetToHtmlDocument("http://steamcommunity.com/profiles/" + SteamID + "/badges?p=" + page, SteamCookieDictionary).ConfigureAwait(false);
}
internal async Task<HtmlDocument> GetGameCardsPage(ulong appID) {
@@ -341,7 +372,7 @@ namespace ArchiSteamFarm {
return null;
}
return await Utilities.UrlToHtmlDocument("http://steamcommunity.com/profiles/" + SteamID + "/gamecards/" + appID, SteamCookieDictionary).ConfigureAwait(false);
return await WebBrowser.UrlGetToHtmlDocument("http://steamcommunity.com/profiles/" + SteamID + "/gamecards/" + appID, SteamCookieDictionary).ConfigureAwait(false);
}
}
}

391
ArchiSteamFarm/Bot.cs Normal file → Executable file
View File

@@ -22,6 +22,8 @@
*/
using Newtonsoft.Json;
using SteamAuth;
using SteamKit2;
using System;
using System.Collections.Concurrent;
@@ -32,25 +34,26 @@ using System.Threading.Tasks;
using System.Xml;
namespace ArchiSteamFarm {
internal class Bot {
internal sealed class Bot {
private const ushort CallbackSleep = 500; // In miliseconds
private static readonly ConcurrentDictionary<string, Bot> Bots = new ConcurrentDictionary<string, Bot>();
private readonly string ConfigFile;
private readonly string SentryFile;
private bool IsRunning = false;
private string AuthCode, TwoFactorAuth;
private readonly string ConfigFile, LoginKeyFile, MobileAuthenticatorFile, SentryFile;
internal readonly string BotName;
private bool LoggedInElsewhere = false;
private bool IsRunning = false;
private string AuthCode, LoginKey, TwoFactorAuth;
internal ArchiHandler ArchiHandler { get; private set; }
internal ArchiWebHandler ArchiWebHandler { get; private set; }
internal CallbackManager CallbackManager { get; private set; }
internal CardsFarmer CardsFarmer { get; private set; }
internal SteamClient SteamClient { get; private set; }
internal SteamFriends SteamFriends { get; private set; }
internal SteamGuardAccount SteamGuardAccount { get; private set; }
internal SteamUser SteamUser { get; private set; }
internal Trading Trading { get; private set; }
@@ -63,10 +66,30 @@ namespace ArchiSteamFarm {
internal string SteamParentalPIN { get; private set; } = "0";
internal ulong SteamMasterID { get; private set; } = 0;
internal ulong SteamMasterClanID { get; private set; } = 0;
internal bool CardDropsRestricted { get; private set; } = false;
internal bool UseAsfAsMobileAuthenticator { get; private set; } = false;
internal bool ShutdownOnFarmingFinished { get; private set; } = false;
internal HashSet<uint> Blacklist { get; private set; } = new HashSet<uint> { 303700, 335590, 368020 };
internal HashSet<uint> Blacklist { get; private set; } = new HashSet<uint> { 303700, 335590, 368020, 425280 };
internal bool Statistics { get; private set; } = true;
private static bool IsValidCdKey(string key) {
if (string.IsNullOrEmpty(key)) {
return false;
}
if (key.Length != 17 && key.Length != 29) {
return false;
}
for (byte i = 5; i < key.Length; i += 6) {
if (key[i] != '-') {
return false;
}
}
return true;
}
internal static int GetRunningBotsCount() {
return Bots.Count;
}
@@ -87,6 +110,8 @@ namespace ArchiSteamFarm {
BotName = botName;
ConfigFile = Path.Combine(Program.ConfigDirectoryPath, BotName + ".xml");
LoginKeyFile = Path.Combine(Program.ConfigDirectoryPath, BotName + ".key");
MobileAuthenticatorFile = Path.Combine(Program.ConfigDirectoryPath, BotName + ".auth");
SentryFile = Path.Combine(Program.ConfigDirectoryPath, BotName + ".bin");
if (!ReadConfig()) {
@@ -112,12 +137,16 @@ namespace ArchiSteamFarm {
SteamFriends = SteamClient.GetHandler<SteamFriends>();
CallbackManager.Subscribe<SteamFriends.FriendsListCallback>(OnFriendsList);
CallbackManager.Subscribe<SteamFriends.FriendMsgCallback>(OnFriendMsg);
CallbackManager.Subscribe<SteamFriends.PersonaStateCallback>(OnPersonaState);
if (UseAsfAsMobileAuthenticator && File.Exists(MobileAuthenticatorFile)) {
SteamGuardAccount = JsonConvert.DeserializeObject<SteamGuardAccount>(File.ReadAllText(MobileAuthenticatorFile));
}
SteamUser = SteamClient.GetHandler<SteamUser>();
CallbackManager.Subscribe<SteamUser.AccountInfoCallback>(OnAccountInfo);
CallbackManager.Subscribe<SteamUser.LoggedOffCallback>(OnLoggedOff);
CallbackManager.Subscribe<SteamUser.LoggedOnCallback>(OnLoggedOn);
CallbackManager.Subscribe<SteamUser.LoginKeyCallback>(OnLoginKey);
CallbackManager.Subscribe<SteamUser.UpdateMachineAuthCallback>(OnMachineAuth);
CallbackManager.Subscribe<ArchiHandler.NotificationCallback>(OnNotification);
@@ -131,6 +160,94 @@ namespace ArchiSteamFarm {
var fireAndForget = Task.Run(async () => await Start().ConfigureAwait(false));
}
internal async Task AcceptAllConfirmations() {
if (SteamGuardAccount == null) {
return;
}
await SteamGuardAccount.RefreshSessionAsync().ConfigureAwait(false);
try {
foreach (Confirmation confirmation in await SteamGuardAccount.FetchConfirmationsAsync().ConfigureAwait(false)) {
if (SteamGuardAccount.AcceptConfirmation(confirmation)) {
Logging.LogGenericInfo(BotName, "Accepting confirmation: Success!");
} else {
Logging.LogGenericWarning(BotName, "Accepting confirmation: Failed!");
}
}
} catch (SteamGuardAccount.WGTokenInvalidException) {
Logging.LogGenericWarning(BotName, "Accepting confirmation: Failed!");
Logging.LogGenericWarning(BotName, "Confirmation could not be accepted because of invalid token exception");
Logging.LogGenericWarning(BotName, "If issue persists, consider removing and readding ASF 2FA");
}
}
private bool LinkMobileAuthenticator() {
if (SteamGuardAccount != null) {
return false;
}
Logging.LogGenericNotice(BotName, "Linking new ASF MobileAuthenticator...");
UserLogin userLogin = new UserLogin(SteamLogin, SteamPassword);
LoginResult loginResult;
while ((loginResult = userLogin.DoLogin()) != LoginResult.LoginOkay) {
switch (loginResult) {
case LoginResult.NeedEmail:
userLogin.EmailCode = Program.GetUserInput(BotName, Program.EUserInputType.SteamGuard);
break;
default:
Logging.LogGenericError(BotName, "Unhandled situation: " + loginResult);
return false;
}
}
AuthenticatorLinker authenticatorLinker = new AuthenticatorLinker(userLogin.Session);
AuthenticatorLinker.LinkResult linkResult;
while ((linkResult = authenticatorLinker.AddAuthenticator()) != AuthenticatorLinker.LinkResult.AwaitingFinalization) {
switch (linkResult) {
case AuthenticatorLinker.LinkResult.MustProvidePhoneNumber:
authenticatorLinker.PhoneNumber = Program.GetUserInput(BotName, Program.EUserInputType.PhoneNumber);
break;
default:
Logging.LogGenericError(BotName, "Unhandled situation: " + linkResult);
return false;
}
}
SteamGuardAccount = authenticatorLinker.LinkedAccount;
try {
File.WriteAllText(MobileAuthenticatorFile, JsonConvert.SerializeObject(SteamGuardAccount));
} catch (Exception e) {
Logging.LogGenericException(BotName, e);
return false;
}
AuthenticatorLinker.FinalizeResult finalizeResult = authenticatorLinker.FinalizeAddAuthenticator(Program.GetUserInput(BotName, Program.EUserInputType.SMS));
if (finalizeResult != AuthenticatorLinker.FinalizeResult.Success) {
Logging.LogGenericError(BotName, "Unhandled situation: " + finalizeResult);
DelinkMobileAuthenticator();
return false;
}
Logging.LogGenericInfo(BotName, "Successfully linked ASF as new mobile authenticator for this account!");
Program.GetUserInput(BotName, Program.EUserInputType.RevocationCode, SteamGuardAccount.RevocationCode);
return true;
}
private bool DelinkMobileAuthenticator() {
if (SteamGuardAccount == null) {
return false;
}
bool result = SteamGuardAccount.DeactivateAuthenticator();
SteamGuardAccount = null;
File.Delete(MobileAuthenticatorFile);
return result;
}
private bool ReadConfig() {
if (!File.Exists(ConfigFile)) {
return false;
@@ -178,6 +295,12 @@ namespace ArchiSteamFarm {
case "SteamMasterClanID":
SteamMasterClanID = ulong.Parse(value);
break;
case "UseAsfAsMobileAuthenticator":
UseAsfAsMobileAuthenticator = bool.Parse(value);
break;
case "CardDropsRestricted":
CardDropsRestricted = bool.Parse(value);
break;
case "ShutdownOnFarmingFinished":
ShutdownOnFarmingFinished = bool.Parse(value);
break;
@@ -213,7 +336,12 @@ namespace ArchiSteamFarm {
IsRunning = true;
Logging.LogGenericInfo(BotName, "Starting...");
await Program.LimitSteamRequestsAsync().ConfigureAwait(false);
// 2FA tokens are expiring soon, use limiter only when we don't have any pending
if (TwoFactorAuth == null) {
await Program.LimitSteamRequestsAsync().ConfigureAwait(false);
}
SteamClient.Connect();
var fireAndForget = Task.Run(() => HandleCallbacks());
@@ -229,33 +357,30 @@ namespace ArchiSteamFarm {
SteamClient.Disconnect();
}
private async Task<bool> Shutdown(string botNameToShutdown) {
Bot botToShutdown;
if (!Bots.TryGetValue(botNameToShutdown, out botToShutdown)) {
return false;
internal async Task<bool> Shutdown(string botName = null) {
Bot bot;
if (string.IsNullOrEmpty(botName)) {
bot = this;
} else {
if (!Bots.TryGetValue(botName, out bot)) {
return false;
}
}
await botToShutdown.Stop().ConfigureAwait(false);
Bots.TryRemove(botNameToShutdown, out botToShutdown);
await bot.Stop().ConfigureAwait(false);
Bots.TryRemove(bot.BotName, out bot);
Program.OnBotShutdown(botToShutdown);
Program.OnBotShutdown();
return true;
}
internal async Task<bool> Shutdown() {
return await Shutdown(BotName).ConfigureAwait(false);
}
internal async Task OnFarmingFinished() {
if (ShutdownOnFarmingFinished) {
await Shutdown().ConfigureAwait(false);
}
}
internal void PlayGame(params ulong[] gameIDs) {
ArchiHandler.PlayGames(gameIDs);
}
private void HandleCallbacks() {
TimeSpan timeSpan = TimeSpan.FromMilliseconds(CallbackSleep);
while (IsRunning) {
@@ -272,44 +397,110 @@ namespace ArchiSteamFarm {
}
private void ResponseStatus(ulong steamID) {
private void ResponseStatus(ulong steamID, string botName = null) {
if (steamID == 0) {
return;
}
Bot bot;
if (string.IsNullOrEmpty(botName)) {
bot = this;
} else {
if (!Bots.TryGetValue(botName, out bot)) {
SendMessageToUser(steamID, "Couldn't find any bot named " + botName + "!");
return;
}
}
if (bot.CardsFarmer.CurrentGamesFarming.Count > 0) {
SendMessageToUser(steamID, "Bot " + bot.BotName + " is currently farming appIDs: " + string.Join(", ", bot.CardsFarmer.CurrentGamesFarming) + " and has a total of " + bot.CardsFarmer.GamesToFarm.Count + " games left to farm");
}
SendMessageToUser(steamID, "Currently " + Bots.Count + " bots are running");
}
private void ResponseStart(ulong steamID, string botNameToStart) {
if (steamID == 0 || string.IsNullOrEmpty(botNameToStart)) {
private void Response2FA(ulong steamID, string botName = null) {
if (steamID == 0) {
return;
}
if (Bots.ContainsKey(botNameToStart)) {
Bot bot;
if (string.IsNullOrEmpty(botName)) {
bot = this;
} else {
if (!Bots.TryGetValue(botName, out bot)) {
SendMessageToUser(steamID, "Couldn't find any bot named " + botName + "!");
return;
}
}
if (bot.SteamGuardAccount == null) {
SendMessageToUser(steamID, "That bot doesn't have ASF 2FA enabled!");
return;
}
long timeLeft = 30 - TimeAligner.GetSteamTime() % 30;
SendMessageToUser(steamID, "2FA Token: " + bot.SteamGuardAccount.GenerateSteamGuardCode() + " (expires in " + timeLeft + " seconds)");
}
private void Response2FAOff(ulong steamID, string botName = null) {
if (steamID == 0) {
return;
}
Bot bot;
if (string.IsNullOrEmpty(botName)) {
bot = this;
} else {
if (!Bots.TryGetValue(botName, out bot)) {
SendMessageToUser(steamID, "Couldn't find any bot named " + botName + "!");
return;
}
}
if (bot.SteamGuardAccount == null) {
SendMessageToUser(steamID, "That bot doesn't have ASF 2FA enabled!");
return;
}
if (bot.DelinkMobileAuthenticator()) {
SendMessageToUser(steamID, "Done! Bot is no longer using ASF 2FA");
} else {
SendMessageToUser(steamID, "Something went wrong!");
}
}
private void ResponseStart(ulong steamID, string botName) {
if (steamID == 0 || string.IsNullOrEmpty(botName)) {
return;
}
if (Bots.ContainsKey(botName)) {
SendMessageToUser(steamID, "That bot instance is already running!");
return;
}
new Bot(botNameToStart);
if (Bots.ContainsKey(botNameToStart)) {
new Bot(botName);
if (Bots.ContainsKey(botName)) {
SendMessageToUser(steamID, "Done!");
} else {
SendMessageToUser(steamID, "That bot instance failed to start, make sure that " + botNameToStart + ".xml config exists and bot is active!");
SendMessageToUser(steamID, "That bot instance failed to start, make sure that XML config exists and bot is active!");
}
}
private async Task ResponseStop(ulong steamID, string botNameToShutdown) {
if (steamID == 0 || string.IsNullOrEmpty(botNameToShutdown)) {
private async Task ResponseStop(ulong steamID, string botName) {
if (steamID == 0 || string.IsNullOrEmpty(botName)) {
return;
}
if (!Bots.ContainsKey(botNameToShutdown)) {
if (!Bots.ContainsKey(botName)) {
SendMessageToUser(steamID, "That bot instance is already inactive!");
return;
}
if (await Shutdown(botNameToShutdown).ConfigureAwait(false)) {
if (await Shutdown(botName).ConfigureAwait(false)) {
SendMessageToUser(steamID, "Done!");
} else {
SendMessageToUser(steamID, "That bot instance failed to shutdown!");
@@ -332,6 +523,10 @@ namespace ArchiSteamFarm {
Logging.LogGenericInfo(BotName, "Connected to Steam!");
if (File.Exists(LoginKeyFile)) {
LoginKey = File.ReadAllText(LoginKeyFile);
}
byte[] sentryHash = null;
if (File.Exists(SentryFile)) {
byte[] sentryFileContent = File.ReadAllBytes(SentryFile);
@@ -342,16 +537,19 @@ namespace ArchiSteamFarm {
SteamLogin = Program.GetUserInput(BotName, Program.EUserInputType.Login);
}
if (SteamPassword.Equals("null")) {
if (SteamPassword.Equals("null") && string.IsNullOrEmpty(LoginKey)) {
SteamPassword = Program.GetUserInput(BotName, Program.EUserInputType.Password);
}
SteamUser.LogOn(new SteamUser.LogOnDetails {
// TODO: We should use SteamUser.LogOn with proper LoginID once https://github.com/SteamRE/SteamKit/pull/217 gets merged
ArchiHandler.HackedLogOn(Program.UniqueID, new SteamUser.LogOnDetails {
Username = SteamLogin,
Password = SteamPassword,
AuthCode = AuthCode,
LoginKey = LoginKey,
TwoFactorCode = TwoFactorAuth,
SentryFileHash = sentryHash
SentryFileHash = sentryHash,
ShouldRememberPassword = true
});
}
@@ -368,8 +566,21 @@ namespace ArchiSteamFarm {
return;
}
await CardsFarmer.StopFarming().ConfigureAwait(false);
Logging.LogGenericWarning(BotName, "Disconnected from Steam, reconnecting...");
await Program.LimitSteamRequestsAsync().ConfigureAwait(false);
// 2FA tokens are expiring soon, use limiter only when we don't have any pending
if (TwoFactorAuth == null) {
await Program.LimitSteamRequestsAsync().ConfigureAwait(false);
}
if (LoggedInElsewhere) {
LoggedInElsewhere = false;
Logging.LogGenericWarning(BotName, "Account is being used elsewhere, will try reconnecting in 5 minutes...");
await Utilities.SleepAsync(5 * 60 * 1000).ConfigureAwait(false);
}
SteamClient.Connect();
}
@@ -386,13 +597,11 @@ namespace ArchiSteamFarm {
SteamID steamID = friend.SteamID;
switch (steamID.AccountType) {
case EAccountType.Clan:
//ArchiHandler.AcceptClanInvite(steamID);
// TODO: Accept clan invites from master?
break;
default:
if (steamID == SteamMasterID) {
SteamFriends.AddFriend(steamID);
} else {
SteamFriends.RemoveFriend(steamID);
}
break;
}
@@ -418,7 +627,7 @@ namespace ArchiSteamFarm {
return;
}
if (message.Length == 17 && message[5] == '-' && message[11] == '-') {
if (IsValidCdKey(message)) {
ArchiHandler.RedeemKey(message);
return;
}
@@ -429,10 +638,17 @@ namespace ArchiSteamFarm {
if (!message.Contains(" ")) {
switch (message) {
case "!2fa":
Response2FA(steamID);
break;
case "!2faoff":
Response2FAOff(steamID);
break;
case "!exit":
await ShutdownAllBots().ConfigureAwait(false);
break;
case "!farm":
SendMessageToUser(steamID, "Please wait...");
await CardsFarmer.StartFarming().ConfigureAwait(false);
SendMessageToUser(steamID, "Done!");
break;
@@ -449,28 +665,28 @@ namespace ArchiSteamFarm {
} else {
string[] args = message.Split(' ');
switch (args[0]) {
case "!2fa":
Response2FA(steamID, args[1]);
break;
case "!2faoff":
Response2FAOff(steamID, args[1]);
break;
case "!redeem":
ArchiHandler.RedeemKey(args[1]);
break;
case "!start":
ResponseStart(steamID, args[1]);
break;
case "!stop":
await ResponseStop(steamID, args[1]).ConfigureAwait(false);
break;
case "!status":
ResponseStatus(steamID, args[1]);
break;
}
}
}
private void OnPersonaState(SteamFriends.PersonaStateCallback callback) {
if (callback == null) {
return;
}
SteamID steamID = callback.FriendID;
SteamID sourceSteamID = callback.SourceSteamID;
string steamNickname = callback.Name;
EPersonaState personaState = callback.State;
EClanRank clanRank = (EClanRank) callback.ClanRank;
}
private void OnAccountInfo(SteamUser.AccountInfoCallback callback) {
if (callback == null) {
return;
@@ -485,6 +701,14 @@ namespace ArchiSteamFarm {
}
Logging.LogGenericInfo(BotName, "Logged off of Steam: " + callback.Result);
switch (callback.Result) {
case EResult.AlreadyLoggedInElsewhere:
case EResult.LoggedInElsewhere:
case EResult.LogonSessionReplaced:
LoggedInElsewhere = true;
break;
}
}
private async void OnLoggedOn(SteamUser.LoggedOnCallback callback) {
@@ -498,11 +722,40 @@ namespace ArchiSteamFarm {
AuthCode = Program.GetUserInput(SteamLogin, Program.EUserInputType.SteamGuard);
break;
case EResult.AccountLoginDeniedNeedTwoFactor:
TwoFactorAuth = Program.GetUserInput(SteamLogin, Program.EUserInputType.TwoFactorAuthentication);
if (SteamGuardAccount == null) {
TwoFactorAuth = Program.GetUserInput(SteamLogin, Program.EUserInputType.TwoFactorAuthentication);
} else {
TwoFactorAuth = SteamGuardAccount.GenerateSteamGuardCode();
}
break;
case EResult.InvalidPassword:
Logging.LogGenericWarning(BotName, "Unable to login to Steam: " + result);
await Stop().ConfigureAwait(false);
// InvalidPassword means usually that login key has expired, if we used it
if (!string.IsNullOrEmpty(LoginKey)) {
LoginKey = null;
File.Delete(LoginKeyFile);
Logging.LogGenericInfo(BotName, "Removed expired login key, reconnecting...");
} else { // If we didn't use login key, InvalidPassword usually means we got captcha or other network-based throttling
Logging.LogGenericInfo(BotName, "Will retry after 25 minutes...");
await Utilities.SleepAsync(25 * 60 * 1000).ConfigureAwait(false); // Captcha disappears after around 20 minutes, so we make it 25
}
// After all of that, try again
await Start().ConfigureAwait(false);
break;
case EResult.OK:
Logging.LogGenericInfo(BotName, "Successfully logged on!");
if (UseAsfAsMobileAuthenticator && TwoFactorAuth == null && SteamGuardAccount == null) {
LinkMobileAuthenticator();
}
// Reset one-time-only access tokens
AuthCode = null;
TwoFactorAuth = null;
if (!SteamNickname.Equals("null")) {
SteamFriends.SetPersonaName(SteamNickname);
}
@@ -513,31 +766,43 @@ namespace ArchiSteamFarm {
await ArchiWebHandler.Init(SteamClient, callback.WebAPIUserNonce, callback.VanityURL, SteamParentalPIN).ConfigureAwait(false);
ulong clanID = SteamMasterClanID;
if (clanID != 0) {
SteamFriends.JoinChat(clanID);
if (SteamMasterClanID != 0) {
await ArchiWebHandler.JoinClan(SteamMasterClanID).ConfigureAwait(false);
SteamFriends.JoinChat(SteamMasterClanID);
}
if (Statistics) {
SteamFriends.JoinChat(Program.ArchiSCFarmGroup);
await ArchiWebHandler.JoinClan(Program.ArchiSCFarmGroup).ConfigureAwait(false);
SteamFriends.JoinChat(Program.ArchiSCFarmGroup);
}
Trading.CheckTrades();
await CardsFarmer.StartFarming().ConfigureAwait(false);
break;
case EResult.ServiceUnavailable:
case EResult.Timeout:
case EResult.TryAnotherCM:
Logging.LogGenericWarning(BotName, "Unable to login to Steam: " + callback.Result + " / " + callback.ExtendedResult + ", retrying...");
Logging.LogGenericWarning(BotName, "Unable to login to Steam: " + result + ", retrying...");
await Stop().ConfigureAwait(false);
await Start().ConfigureAwait(false);
break;
default:
Logging.LogGenericWarning(BotName, "Unable to login to Steam: " + callback.Result + " / " + callback.ExtendedResult);
Logging.LogGenericWarning(BotName, "Unable to login to Steam: " + result);
await Shutdown().ConfigureAwait(false);
break;
}
}
private void OnLoginKey(SteamUser.LoginKeyCallback callback) {
if (callback == null) {
return;
}
File.WriteAllText(LoginKeyFile, callback.LoginKey);
SteamUser.AcceptNewLoginKey(callback);
}
private void OnMachineAuth(SteamUser.UpdateMachineAuthCallback callback) {
if (callback == null) {
return;

View File

@@ -27,28 +27,21 @@ using SteamKit2.Internal;
using System.IO;
namespace ArchiSteamFarm {
/// <summary>
/// Message used to Accept or Decline a group(clan) invite.
/// </summary>
internal sealed class CMsgClientClanInviteAction : ISteamSerializableMessage, ISteamSerializable {
EMsg ISteamSerializableMessage.GetEMsg() {
return EMsg.ClientAcknowledgeClanInvite;
}
public CMsgClientClanInviteAction() {
}
/// <summary>
/// Group invited to.
/// </summary>
internal ulong GroupID = 0;
/// <summary>
/// To accept or decline the invite.
/// </summary>
internal bool AcceptInvite = true;
public CMsgClientClanInviteAction() { }
void ISteamSerializable.Serialize(Stream stream) {
if (stream == null) {
return;
}
try {
BinaryWriter binaryWriter = new BinaryWriter(stream);
binaryWriter.Write(GroupID);
@@ -59,6 +52,10 @@ namespace ArchiSteamFarm {
}
void ISteamSerializable.Deserialize(Stream stream) {
if (stream == null) {
return;
}
try {
BinaryReader binaryReader = new BinaryReader(stream);
GroupID = binaryReader.ReadUInt64();

282
ArchiSteamFarm/CardsFarmer.cs Normal file → Executable file
View File

@@ -23,22 +23,114 @@
*/
using HtmlAgilityPack;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
namespace ArchiSteamFarm {
internal class CardsFarmer {
internal sealed class CardsFarmer {
private const byte StatusCheckSleep = 5; // In minutes, how long to wait before checking the appID again
private const ushort MaxFarmingTime = 600; // In minutes, how long ASF is allowed to farm one game in solo mode
private readonly ManualResetEvent FarmResetEvent = new ManualResetEvent(false);
private readonly SemaphoreSlim Semaphore = new SemaphoreSlim(1);
private readonly Bot Bot;
private readonly Timer Timer;
internal readonly ConcurrentDictionary<uint, double> GamesToFarm = new ConcurrentDictionary<uint, double>();
internal readonly List<uint> CurrentGamesFarming = new List<uint>();
private volatile bool NowFarming = false;
internal CardsFarmer(Bot bot) {
Bot = bot;
Timer = new Timer(
async e => await CheckGamesForFarming().ConfigureAwait(false),
null,
TimeSpan.FromMinutes(15), // Delay
TimeSpan.FromMinutes(15) // Period
);
}
internal static List<uint> GetGamesToFarmSolo(ConcurrentDictionary<uint, double> gamesToFarm) {
if (gamesToFarm == null) {
return null;
}
List<uint> result = new List<uint>();
foreach (KeyValuePair<uint, double> keyValue in gamesToFarm) {
if (keyValue.Value >= 2) {
result.Add(keyValue.Key);
}
}
return result;
}
internal static uint GetAnyGameToFarm(ConcurrentDictionary<uint, double> gamesToFarm) {
if (gamesToFarm == null) {
return 0;
}
foreach (uint appID in gamesToFarm.Keys) {
return appID;
}
return 0;
}
internal bool FarmMultiple() {
if (GamesToFarm.Count == 0) {
return true;
}
double maxHour = -1;
foreach (double hour in GamesToFarm.Values) {
if (hour > maxHour) {
maxHour = hour;
}
}
CurrentGamesFarming.Clear();
foreach (uint appID in GamesToFarm.Keys) {
CurrentGamesFarming.Add(appID);
}
Logging.LogGenericInfo(Bot.BotName, "Now farming: " + string.Join(", ", GamesToFarm.Keys));
if (Farm(maxHour, GamesToFarm.Keys)) {
return true;
} else {
CurrentGamesFarming.Clear();
NowFarming = false;
return false;
}
}
internal async Task<bool> FarmSolo(uint appID) {
if (appID == 0) {
return true;
}
CurrentGamesFarming.Clear();
CurrentGamesFarming.Add(appID);
Logging.LogGenericInfo(Bot.BotName, "Now farming: " + appID);
if (await Farm(appID).ConfigureAwait(false)) {
double hours;
GamesToFarm.TryRemove(appID, out hours);
return true;
} else {
CurrentGamesFarming.Clear();
NowFarming = false;
return false;
}
}
internal async Task StartFarming() {
@@ -51,6 +143,28 @@ namespace ArchiSteamFarm {
return;
}
// Check if farming is possible
Logging.LogGenericInfo(Bot.BotName, "Checking possibility to farm...");
NowFarming = true;
Semaphore.Release();
Bot.ArchiHandler.PlayGames(1337);
// We'll now either receive OnLoggedOff() with LoggedInElsewhere, or nothing happens
if (await Task.Run(() => FarmResetEvent.WaitOne(5000)).ConfigureAwait(false)) { // If LoggedInElsewhere happens in 5 seconds from now, abort farming
NowFarming = false;
return;
}
NowFarming = false;
Logging.LogGenericInfo(Bot.BotName, "Farming is possible!");
await Semaphore.WaitAsync().ConfigureAwait(false);
if (await Bot.ArchiWebHandler.ReconnectIfNeeded().ConfigureAwait(false)) {
Semaphore.Release();
return;
}
Logging.LogGenericInfo(Bot.BotName, "Checking badges...");
// Find the number of badge pages
@@ -68,7 +182,6 @@ namespace ArchiSteamFarm {
}
// Find APPIDs we need to farm
List<uint> appIDs = new List<uint>();
for (var page = 1; page <= maxPages; page++) {
Logging.LogGenericInfo(Bot.BotName, "Checking page: " + page + "/" + maxPages);
@@ -84,16 +197,15 @@ namespace ArchiSteamFarm {
continue;
}
GamesToFarm.Clear();
foreach (HtmlNode badgesPageNode in badgesPageNodes) {
string steamLink = badgesPageNode.GetAttributeValue("href", null);
if (steamLink == null) {
Logging.LogGenericError(Bot.BotName, "Couldn't get steamLink for one of the games: " + badgesPageNode.OuterHtml);
continue;
}
uint appID = (uint) Utilities.OnlyNumbers(steamLink);
if (appID == 0) {
Logging.LogGenericError(Bot.BotName, "Couldn't get appID for one of the games: " + badgesPageNode.OuterHtml);
continue;
}
@@ -101,26 +213,89 @@ namespace ArchiSteamFarm {
continue;
}
appIDs.Add(appID);
// We assume that every game has at least 2 hours played, until we actually check them
GamesToFarm.AddOrUpdate(appID, 2, (key, value) => 2);
}
}
// If we have restricted card drops, actually do check all games that are left to farm
if (Bot.CardDropsRestricted) {
foreach (uint appID in GamesToFarm.Keys) {
Logging.LogGenericInfo(Bot.BotName, "Checking hours of appID: " + appID);
HtmlDocument appPage = await Bot.ArchiWebHandler.GetGameCardsPage(appID).ConfigureAwait(false);
if (appPage == null) {
continue;
}
HtmlNode appNode = appPage.DocumentNode.SelectSingleNode("//div[@class='badge_title_stats_playtime']");
if (appNode == null) {
continue;
}
string hoursString = appNode.InnerText;
if (string.IsNullOrEmpty(hoursString)) {
continue;
}
hoursString = Regex.Match(hoursString, @"[0-9\.,]+").Value;
double hours;
if (string.IsNullOrEmpty(hoursString)) {
hours = 0;
} else {
hours = double.Parse(hoursString, CultureInfo.InvariantCulture);
}
GamesToFarm[appID] = hours;
}
}
Logging.LogGenericInfo(Bot.BotName, "Farming in progress...");
NowFarming = appIDs.Count > 0;
NowFarming = GamesToFarm.Count > 0;
Semaphore.Release();
// Start farming
while (appIDs.Count > 0) {
uint appID = appIDs[0];
Logging.LogGenericInfo(Bot.BotName, "Now farming: " + appID);
if (await Farm(appID).ConfigureAwait(false)) {
appIDs.Remove(appID);
} else {
NowFarming = false;
return;
// Now the algorithm used for farming depends on whether account is restricted or not
if (Bot.CardDropsRestricted) {
// If we have restricted card drops, we use complex algorithm, which prioritizes farming solo titles >= 2 hours, then all at once, until any game hits mentioned 2 hours
Logging.LogGenericInfo(Bot.BotName, "Chosen farming algorithm: Complex");
while (GamesToFarm.Count > 0) {
List<uint> gamesToFarmSolo = GetGamesToFarmSolo(GamesToFarm);
if (gamesToFarmSolo.Count > 0) {
while (gamesToFarmSolo.Count > 0) {
uint appID = gamesToFarmSolo[0];
bool success = await FarmSolo(appID).ConfigureAwait(false);
if (success) {
Logging.LogGenericInfo(Bot.BotName, "Done farming: " + appID);
gamesToFarmSolo.Remove(appID);
} else {
return;
}
}
} else {
bool success = FarmMultiple();
if (success) {
Logging.LogGenericInfo(Bot.BotName, "Done farming: " + string.Join(", ", GamesToFarm.Keys));
} else {
return;
}
}
}
} else {
// If we have unrestricted card drops, we use simple algorithm and farm cards one-by-one
Logging.LogGenericInfo(Bot.BotName, "Chosen farming algorithm: Simple");
while (GamesToFarm.Count > 0) {
uint appID = GetAnyGameToFarm(GamesToFarm);
bool success = await FarmSolo(appID).ConfigureAwait(false);
if (success) {
Logging.LogGenericInfo(Bot.BotName, "Done farming: " + appID);
} else {
return;
}
}
}
CurrentGamesFarming.Clear();
NowFarming = false;
Logging.LogGenericInfo(Bot.BotName, "Farming finished!");
await Bot.OnFarmingFinished().ConfigureAwait(false);
@@ -136,7 +311,7 @@ namespace ArchiSteamFarm {
Logging.LogGenericInfo(Bot.BotName, "Sending signal to stop farming");
FarmResetEvent.Set();
while (NowFarming) {
for (var i = 0; i < 5 && NowFarming; i++) {
Logging.LogGenericInfo(Bot.BotName, "Waiting for reaction...");
await Utilities.SleepAsync(1000).ConfigureAwait(false);
}
@@ -145,24 +320,40 @@ namespace ArchiSteamFarm {
Semaphore.Release();
}
private async Task<bool?> ShouldFarm(ulong appID) {
bool? result = null;
HtmlDocument gamePageDocument = await Bot.ArchiWebHandler.GetGameCardsPage(appID).ConfigureAwait(false);
if (gamePageDocument != null) {
HtmlNode gamePageNode = gamePageDocument.DocumentNode.SelectSingleNode("//span[@class='progress_info_bold']");
if (gamePageNode != null) {
result = !gamePageNode.InnerText.Contains("No card drops");
}
private async Task CheckGamesForFarming() {
if (NowFarming || GamesToFarm.Count > 0) {
return;
}
return result;
await StartFarming().ConfigureAwait(false);
}
private async Task<bool> Farm(ulong appID) {
Bot.PlayGame(appID);
private async Task<bool?> ShouldFarm(ulong appID) {
if (appID == 0) {
return false;
}
HtmlDocument htmlDocument = await Bot.ArchiWebHandler.GetGameCardsPage(appID).ConfigureAwait(false);
if (htmlDocument == null) {
return null;
}
HtmlNode htmlNode = htmlDocument.DocumentNode.SelectSingleNode("//span[@class='progress_info_bold']");
if (htmlNode == null) {
await Bot.ArchiWebHandler.ReconnectIfNeeded().ConfigureAwait(false);
return null;
}
return !htmlNode.InnerText.Contains("No card drops");
}
private async Task<bool> Farm(uint appID) {
Bot.ArchiHandler.PlayGames(appID);
bool success = true;
bool? keepFarming = await ShouldFarm(appID).ConfigureAwait(false);
while (keepFarming == null || keepFarming.Value) {
for (ushort farmingTime = 0; farmingTime <= MaxFarmingTime && (!keepFarming.HasValue || keepFarming.Value); farmingTime += StatusCheckSleep) {
Logging.LogGenericInfo(Bot.BotName, "Still farming: " + appID);
if (FarmResetEvent.WaitOne(1000 * 60 * StatusCheckSleep)) {
success = false;
@@ -171,9 +362,42 @@ namespace ArchiSteamFarm {
keepFarming = await ShouldFarm(appID).ConfigureAwait(false);
}
Bot.PlayGame(0);
Bot.ArchiHandler.PlayGames(0);
Logging.LogGenericInfo(Bot.BotName, "Stopped farming: " + appID);
return success;
}
private bool Farm(double maxHour, ICollection<uint> appIDs) {
if (maxHour >= 2) {
return true;
}
Bot.ArchiHandler.PlayGames(appIDs);
bool success = true;
while (maxHour < 2) {
Logging.LogGenericInfo(Bot.BotName, "Still farming: " + string.Join(", ", appIDs));
if (FarmResetEvent.WaitOne(1000 * 60 * StatusCheckSleep)) {
success = false;
break;
}
// Don't forget to update our GamesToFarm hours
double timePlayed = StatusCheckSleep / 60.0;
foreach (KeyValuePair<uint, double> keyValue in GamesToFarm) {
if (!appIDs.Contains(keyValue.Key)) {
continue;
}
GamesToFarm[keyValue.Key] = keyValue.Value + timePlayed;
}
maxHour += timePlayed;
}
Bot.ArchiHandler.PlayGames(0);
Logging.LogGenericInfo(Bot.BotName, "Stopped farming: " + string.Join(", ", appIDs));
return success;
}
}
}

View File

@@ -29,9 +29,11 @@ using System.Runtime.CompilerServices;
namespace ArchiSteamFarm {
internal static class Logging {
private static void Log(string message) {
lock (Program.ConsoleLock) {
Console.WriteLine(DateTime.Now + " " + message);
if (Program.ConsoleIsBusy) {
return;
}
Console.WriteLine(DateTime.Now + " " + message);
}
internal static void LogGenericError(string botName, string message, [CallerMemberName] string previousMethodName = "") {

View File

@@ -34,8 +34,11 @@ namespace ArchiSteamFarm {
internal enum EUserInputType {
Login,
Password,
PhoneNumber,
SMS,
SteamGuard,
SteamParentalPIN,
RevocationCode,
TwoFactorAuthentication,
}
@@ -49,13 +52,16 @@ namespace ArchiSteamFarm {
private static readonly Assembly Assembly = Assembly.GetExecutingAssembly();
private static readonly string ExecutablePath = Assembly.Location;
private static readonly AssemblyName AssemblyName = Assembly.GetName();
private static readonly object ConsoleLock = new object();
//private static readonly string ExeName = AssemblyName.Name + ".exe";
private static readonly string Version = AssemblyName.Version.ToString();
internal static readonly object ConsoleLock = new object();
internal static readonly uint UniqueID = (uint) Utilities.Random.Next();
internal static readonly string Version = AssemblyName.Version.ToString();
internal static bool ConsoleIsBusy = false;
private static async Task CheckForUpdate() {
JObject response = await Utilities.UrlToJObject(LatestGithubReleaseURL).ConfigureAwait(false);
JObject response = await WebBrowser.UrlGetToJObject(LatestGithubReleaseURL).ConfigureAwait(false);
if (response == null) {
return;
}
@@ -98,9 +104,10 @@ namespace ArchiSteamFarm {
SteamSemaphore.Release();
}
internal static string GetUserInput(string botLogin, EUserInputType userInputType) {
internal static string GetUserInput(string botLogin, EUserInputType userInputType, string extraInformation = null) {
string result;
lock (ConsoleLock) {
ConsoleIsBusy = true;
switch (userInputType) {
case EUserInputType.Login:
Console.Write("<" + botLogin + "> Please enter your login: ");
@@ -108,24 +115,36 @@ namespace ArchiSteamFarm {
case EUserInputType.Password:
Console.Write("<" + botLogin + "> Please enter your password: ");
break;
case EUserInputType.PhoneNumber:
Console.Write("<" + botLogin + "> Please enter your full phone number (e.g. +1234567890): ");
break;
case EUserInputType.SMS:
Console.Write("<" + botLogin + "> Please enter SMS code sent on your mobile: ");
break;
case EUserInputType.SteamGuard:
Console.Write("<" + botLogin + "> Please enter the auth code sent to your email: ");
break;
case EUserInputType.SteamParentalPIN:
Console.Write("<" + botLogin + "> Please enter steam parental PIN: ");
break;
case EUserInputType.RevocationCode:
Console.WriteLine("<" + botLogin + "> PLEASE WRITE DOWN YOUR REVOCATION CODE: " + extraInformation);
Console.WriteLine("<" + botLogin + "> THIS IS THE ONLY WAY TO NOT GET LOCKED OUT OF YOUR ACCOUNT!");
Console.Write("<" + botLogin + "> Hit enter once ready...");
break;
case EUserInputType.TwoFactorAuthentication:
Console.Write("<" + botLogin + "> Please enter your 2 factor auth code from your authenticator app: ");
break;
}
result = Console.ReadLine();
Console.Clear(); // For security purposes
ConsoleIsBusy = false;
}
result = result.Trim(); // Get rid of all whitespace characters
return result;
return result.Trim(); // Get rid of all whitespace characters
}
internal static async void OnBotShutdown(Bot bot) {
internal static async void OnBotShutdown() {
if (Bot.GetRunningBotsCount() == 0) {
Logging.LogGenericInfo("Main", "No bots are running, exiting");
await Utilities.SleepAsync(5000).ConfigureAwait(false); // This might be the only message user gets, consider giving him some time
@@ -133,9 +152,15 @@ namespace ArchiSteamFarm {
}
}
private static void InitServices() {
WebBrowser.Init();
}
private static void Main(string[] args) {
Logging.LogGenericInfo("Main", "Archi's Steam Farm, version " + Version);
InitServices();
Task.Run(async () => await CheckForUpdate().ConfigureAwait(false)).Wait();
// Allow loading configs from source tree if it's a debug build
@@ -163,7 +188,7 @@ namespace ArchiSteamFarm {
}
// Check if we got any bots running
OnBotShutdown(null);
OnBotShutdown();
ShutdownResetEvent.WaitOne();
}

View File

@@ -32,5 +32,5 @@ using System.Runtime.InteropServices;
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("0.7.0.0")]
[assembly: AssemblyFileVersion("0.7.0.0")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@@ -39,7 +39,14 @@ namespace ArchiSteamFarm {
Declined,
InvalidItems,
EmailPending,
EmailCanceled
EmailCanceled,
OnHold
}
internal enum ETradeOfferConfirmationMethod {
Invalid,
Email,
MobileApp
}
internal string tradeofferid { get; set; }
@@ -53,6 +60,8 @@ namespace ArchiSteamFarm {
internal int time_created { get; set; }
internal int time_updated { get; set; }
internal bool from_real_time_trade { get; set; }
internal int escrow_end_date { get; set; }
internal ETradeOfferConfirmationMethod confirmation_method { get; set; }
// Extra
private ulong _OtherSteamID64 = 0;

View File

@@ -62,6 +62,7 @@ namespace ArchiSteamFarm {
}
await Task.WhenAll(tasks).ConfigureAwait(false);
await Bot.AcceptAllConfirmations().ConfigureAwait(false);
}
private async Task ParseTrade(SteamTradeOffer tradeOffer) {
@@ -74,19 +75,11 @@ namespace ArchiSteamFarm {
return;
}
ulong otherSteamID = tradeOffer.OtherSteamID64;
bool success = false;
bool tradeAccepted = false;
if (tradeOffer.items_to_give.Count == 0 || otherSteamID == Bot.SteamMasterID) {
tradeAccepted = true;
success = await Bot.ArchiWebHandler.AcceptTradeOffer(tradeID).ConfigureAwait(false);
if (tradeOffer.items_to_give.Count == 0 || tradeOffer.OtherSteamID64 == Bot.SteamMasterID) {
Logging.LogGenericInfo(Bot.BotName, "Accepting trade: " + tradeID);
await Bot.ArchiWebHandler.AcceptTradeOffer(tradeID).ConfigureAwait(false);
} else {
success = Bot.ArchiWebHandler.DeclineTradeOffer(tradeID);
}
if (tradeAccepted && success) {
// Do whatever we want with success
Logging.LogGenericInfo(Bot.BotName, "Ignoring trade: " + tradeID);
}
}
}

View File

@@ -22,20 +22,13 @@
*/
using HtmlAgilityPack;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace ArchiSteamFarm {
internal static class Utilities {
private static readonly Random Random = new Random();
internal static readonly Random Random = new Random();
internal static async Task SleepAsync(int miliseconds) {
await Task.Delay(miliseconds).ConfigureAwait(false);
@@ -46,172 +39,25 @@ namespace ArchiSteamFarm {
return 0;
}
string resultString;
try {
Regex regexObj = new Regex(@"[^\d]");
resultString = regexObj.Replace(inputString, "");
} catch (ArgumentException e) {
Logging.LogGenericException("Utilities", e);
string resultString = OnlyNumbersString(inputString);
if (string.IsNullOrEmpty(resultString)) {
return 0;
}
return ulong.Parse(resultString, CultureInfo.InvariantCulture);
}
internal static async Task<HttpResponseMessage> UrlToHttpResponse(string websiteAddress, Dictionary<string, string> cookieVariables = null) {
if (string.IsNullOrEmpty(websiteAddress)) {
return null;
}
HttpResponseMessage result = null;
try {
using (HttpClientHandler clientHandler = new HttpClientHandler { UseCookies = false }) {
using (HttpClient client = new HttpClient(clientHandler)) {
client.Timeout = TimeSpan.FromSeconds(30);
client.DefaultRequestHeaders.UserAgent.ParseAdd("ArchiSteamFarm/1.0");
HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Get, websiteAddress);
if (cookieVariables != null) {
StringBuilder cookie = new StringBuilder();
foreach (KeyValuePair<string, string> cookieVariable in cookieVariables) {
cookie.Append(cookieVariable.Key + "=" + cookieVariable.Value + ";");
}
requestMessage.Headers.Add("Cookie", cookie.ToString());
}
HttpResponseMessage responseMessage = await client.SendAsync(requestMessage).ConfigureAwait(false);
if (responseMessage != null) {
responseMessage.EnsureSuccessStatusCode();
result = responseMessage;
}
}
}
} catch (Exception e) {
Logging.LogGenericException("Utilities", e);
ulong result;
if (!ulong.TryParse(resultString, out result)) {
return 0;
}
return result;
}
internal static async Task<HtmlDocument> UrlToHtmlDocument(string websiteAddress, Dictionary<string, string> cookieVariables = null) {
if (string.IsNullOrEmpty(websiteAddress)) {
internal static string OnlyNumbersString(string text) {
if (string.IsNullOrEmpty(text)) {
return null;
}
HtmlDocument result = null;
try {
HttpResponseMessage responseMessage = await UrlToHttpResponse(websiteAddress, cookieVariables).ConfigureAwait(false);
if (responseMessage != null) {
string source = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
if (!string.IsNullOrEmpty(source)) {
source = WebUtility.HtmlDecode(source);
result = new HtmlDocument();
result.LoadHtml(source);
}
}
} catch (Exception e) {
Logging.LogGenericException("Utilities", e);
}
return result;
}
internal static async Task<bool> UrlPostRequest(string request, Dictionary<string, string> postData, Dictionary<string, string> cookieVariables = null, string referer = null) {
if (string.IsNullOrEmpty(request) || postData == null) {
return false;
}
bool result = false;
try {
using (HttpClientHandler clientHandler = new HttpClientHandler { UseCookies = false }) {
using (HttpClient client = new HttpClient(clientHandler)) {
client.Timeout = TimeSpan.FromSeconds(30);
client.DefaultRequestHeaders.UserAgent.ParseAdd("ArchiSteamFarm/1.0");
HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post, request);
requestMessage.Content = new FormUrlEncodedContent(postData);
if (cookieVariables != null && cookieVariables.Count > 0) {
StringBuilder cookie = new StringBuilder();
foreach (KeyValuePair<string, string> cookieVariable in cookieVariables) {
cookie.Append(cookieVariable.Key + "=" + cookieVariable.Value + ";");
}
requestMessage.Headers.Add("Cookie", cookie.ToString());
}
if (referer != null) {
requestMessage.Headers.Referrer = new Uri(referer);
}
HttpResponseMessage responseMessage = await client.SendAsync(requestMessage).ConfigureAwait(false);
if (responseMessage != null) {
result = responseMessage.IsSuccessStatusCode;
}
}
}
} catch (Exception e) {
Logging.LogGenericException("Utilities", e);
}
return result;
}
internal static async Task<HttpResponseMessage> UrlPostRequestWithResponse(string request, Dictionary<string, string> postData, Dictionary<string, string> cookieVariables = null, string referer = null) {
if (string.IsNullOrEmpty(request) || postData == null) {
return null;
}
HttpResponseMessage result = null;
try {
using (HttpClientHandler clientHandler = new HttpClientHandler { UseCookies = false }) {
using (HttpClient client = new HttpClient(clientHandler)) {
client.Timeout = TimeSpan.FromSeconds(30);
client.DefaultRequestHeaders.UserAgent.ParseAdd("ArchiSteamFarm/1.0");
HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post, request);
requestMessage.Content = new FormUrlEncodedContent(postData);
if (cookieVariables != null && cookieVariables.Count > 0) {
StringBuilder cookie = new StringBuilder();
foreach (KeyValuePair<string, string> cookieVariable in cookieVariables) {
cookie.Append(cookieVariable.Key + "=" + cookieVariable.Value + ";");
}
requestMessage.Headers.Add("Cookie", cookie.ToString());
}
if (referer != null) {
requestMessage.Headers.Referrer = new Uri(referer);
}
HttpResponseMessage responseMessage = await client.SendAsync(requestMessage).ConfigureAwait(false);
if (responseMessage != null) {
result = responseMessage;
}
}
}
} catch (Exception e) {
Logging.LogGenericException("Utilities", e);
}
return result;
}
internal static async Task<JObject> UrlToJObject(string request) {
if (string.IsNullOrEmpty(request)) {
return null;
}
HttpResponseMessage httpResponseMessage = await UrlToHttpResponse(request, null).ConfigureAwait(false);
if (httpResponseMessage == null) {
return null;
}
string source = await httpResponseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
if (string.IsNullOrEmpty(source)) {
return null;
}
JObject result = null;
try {
result = JObject.Parse(source);
} catch {
}
return result;
return Regex.Replace(text, @"[^\d]", "");
}
}
}

View File

@@ -0,0 +1,247 @@
/*
_ _ _ ____ _ _____
/ \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
/ _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
/ ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
/_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
Copyright 2015 Ł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 HtmlAgilityPack;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
namespace ArchiSteamFarm {
internal static class WebBrowser {
internal const byte HttpTimeout = 180; // In seconds
private static readonly HttpClientHandler HttpClientHandler = new HttpClientHandler { UseCookies = false };
private static readonly HttpClient HttpClient = new HttpClient(HttpClientHandler) { Timeout = TimeSpan.FromSeconds(HttpTimeout) };
internal static void Init() {
HttpClient.DefaultRequestHeaders.UserAgent.ParseAdd("ArchiSteamFarm/" + Program.Version);
// Don't limit maximum number of allowed concurrent connections
// It's application's responsibility to handle that stuff
ServicePointManager.DefaultConnectionLimit = int.MaxValue;
// Don't use Expect100Continue, we don't need to do that
ServicePointManager.Expect100Continue = false;
}
private static async Task<HttpResponseMessage> UrlRequest(string request, HttpMethod httpMethod, Dictionary<string, string> data = null, Dictionary<string, string> cookies = null, string referer = null) {
if (string.IsNullOrEmpty(request) || httpMethod == null) {
return null;
}
HttpRequestMessage requestMessage = new HttpRequestMessage(httpMethod, request);
if (httpMethod == HttpMethod.Post && data != null) {
requestMessage.Content = new FormUrlEncodedContent(data);
}
if (cookies != null && cookies.Count > 0) {
StringBuilder cookieHeader = new StringBuilder();
foreach (KeyValuePair<string, string> cookie in cookies) {
cookieHeader.Append(cookie.Key + "=" + cookie.Value + ";");
}
requestMessage.Headers.Add("Cookie", cookieHeader.ToString());
}
if (referer != null) {
requestMessage.Headers.Referrer = new Uri(referer);
}
HttpResponseMessage responseMessage;
try {
responseMessage = await HttpClient.SendAsync(requestMessage).ConfigureAwait(false);
} catch { // Request failed, we don't need to know the exact reason, swallow exception
return null;
}
if (responseMessage == null || !responseMessage.IsSuccessStatusCode) {
return null;
}
return responseMessage;
}
internal static async Task<HttpResponseMessage> UrlGet(string request, Dictionary<string, string> cookies = null, string referer = null) {
if (string.IsNullOrEmpty(request)) {
return null;
}
return await UrlRequest(request, HttpMethod.Get, null, cookies, referer).ConfigureAwait(false);
}
internal static async Task<HttpResponseMessage> UrlPost(string request, Dictionary<string, string> postData = null, Dictionary<string, string> cookies = null, string referer = null) {
if (string.IsNullOrEmpty(request)) {
return null;
}
return await UrlRequest(request, HttpMethod.Post, postData, cookies, referer).ConfigureAwait(false);
}
internal static async Task<HtmlDocument> HttpResponseToHtmlDocument(HttpResponseMessage httpResponse) {
if (httpResponse == null || httpResponse.Content == null) {
return null;
}
string content = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
if (string.IsNullOrEmpty(content)) {
return null;
}
content = WebUtility.HtmlDecode(content);
HtmlDocument htmlDocument = new HtmlDocument();
htmlDocument.LoadHtml(content);
return htmlDocument;
}
internal static async Task<string> UrlGetToContent(string request, Dictionary<string, string> cookies = null, string referer = null) {
if (string.IsNullOrEmpty(request)) {
return null;
}
HttpResponseMessage responseMessage = await UrlGet(request, cookies, referer).ConfigureAwait(false);
if (responseMessage == null || responseMessage.Content == null) {
return null;
}
return await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
}
internal static async Task<string> UrlPostToContent(string request, Dictionary<string, string> postData = null, Dictionary<string, string> cookies = null, string referer = null) {
if (string.IsNullOrEmpty(request)) {
return null;
}
HttpResponseMessage responseMessage = await UrlPost(request, postData, cookies, referer).ConfigureAwait(false);
if (responseMessage == null || responseMessage.Content == null) {
return null;
}
return await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
}
internal static async Task<string> UrlGetToTitle(string request, Dictionary<string, string> cookies = null, string referer = null) {
if (string.IsNullOrEmpty(request)) {
return null;
}
HtmlDocument htmlDocument = await UrlGetToHtmlDocument(request, cookies, referer).ConfigureAwait(false);
if (htmlDocument == null) {
return null;
}
HtmlNode htmlNode = htmlDocument.DocumentNode.SelectSingleNode("//head/title");
if (htmlNode == null) {
return null;
}
return htmlNode.InnerText;
}
internal static async Task<HtmlDocument> UrlGetToHtmlDocument(string request, Dictionary<string, string> cookies = null, string referer = null) {
if (string.IsNullOrEmpty(request)) {
return null;
}
HttpResponseMessage httpResponse = await UrlGet(request, cookies, referer).ConfigureAwait(false);
if (httpResponse == null) {
return null;
}
return await HttpResponseToHtmlDocument(httpResponse).ConfigureAwait(false);
}
internal static async Task<JArray> UrlGetToJArray(string request, Dictionary<string, string> cookies = null, string referer = null) {
if (string.IsNullOrEmpty(request)) {
return null;
}
string content = await UrlGetToContent(request, cookies, referer).ConfigureAwait(false);
if (string.IsNullOrEmpty(content)) {
return null;
}
JArray jArray;
try {
jArray = JArray.Parse(content);
} catch (Exception e) {
Logging.LogGenericException("WebBrowser", e);
return null;
}
return jArray;
}
internal static async Task<JObject> UrlGetToJObject(string request, Dictionary<string, string> cookies = null, string referer = null) {
if (string.IsNullOrEmpty(request)) {
return null;
}
string content = await UrlGetToContent(request, cookies, referer).ConfigureAwait(false);
if (string.IsNullOrEmpty(content)) {
return null;
}
JObject jObject;
try {
jObject = JObject.Parse(content);
} catch (Exception e) {
Logging.LogGenericException("WebBrowser", e);
return null;
}
return jObject;
}
internal static async Task<XmlDocument> UrlGetToXML(string request, Dictionary<string, string> cookies = null, string referer = null) {
if (string.IsNullOrEmpty(request)) {
return null;
}
string content = await UrlGetToContent(request, cookies, referer).ConfigureAwait(false);
if (string.IsNullOrEmpty(content)) {
return null;
}
XmlDocument xmlDocument = new XmlDocument();
try {
xmlDocument.LoadXml(content);
} catch (XmlException e) {
Logging.LogGenericException("WebBrowser", e);
return null;
}
return xmlDocument;
}
}
}

View File

@@ -1,11 +1,19 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<!-- Every bot should have it's own unique .xml configuration file, this is example on which you can base on -->
<!-- This is full-fledged example config, you may be also interested in minimal.xml for bare minimum one -->
<!-- Default values used in config match default ASF values when given config property is not found -->
<!-- Notice, if you use special characters reserved for XML, you should escape them -->
<!-- Escape table: [& - &amp;] | [" - &quot;] | [' - &apos;] | [< - &lt;] | [> - &gt;] -->
<!-- So e.g. if your SteamPassword is "foo&" you should write value="foo&amp;" -->
<!-- Type of the property is a tip for you that defines what values you can use -->
<!-- bool - Boolean value that can be only "true" or "false" -->
<!-- string - Any sequence of characters, unless stated otherwise (keep in mind escape table above), with special treatment of "null" value -->
<!-- uint - 32-bit unsigned integer, used mostly for steam appID -->
<!-- ulong - 64-bit unsigned (long) integer, used mostly for representing steamID64 -->
<!-- HashSet(uint) - Comma-separated list of unique 32-bit unsigned integers -->
<!-- Master switch to turn account on and off, set to "true" after you're done -->
<!-- TIP: This bot instance won't run unless below switch is set to "true" -->
<Enabled type="bool" value="false"/>
@@ -30,19 +38,31 @@
<!-- TIP: Most likely you don't want to change it. You can use "null" if you wish to enter PIN on every startup, 0 means there is no PIN -->
<SteamParentalPIN type="string" value="0"/>
<!-- This is steamID64 of the bot-master - you, [Example: 76561198006963719] -->
<!-- This is steamID64 of the bot-master - you, for example "76561198006963719" -->
<!-- You can get one e.g. by logging in to http://steamrep.com/ -->
<!-- TIP: You can use "0", but bot won't accept steam cd-keys or trades from anybody" -->
<SteamMasterID type="ulong" value="0"/>
<!-- This defines clan of the master, bot will join chatroom of that clan automatically after logging in if set -->
<!-- SteamMasterClanID could be found by visiting your group-page -->
<!-- [Example: http://steamcommunity.com/groups/hellokitty/] -->
<!-- Adding "memberslistxml/?xml=1" to the end of this url so it looks like -->
<!-- [Example: http://steamcommunity.com/groups/hellokitty/memberslistxml/?xml=1] -->
<!-- Finally replace the SteamMasterClanID value with groupID64 you got from your groups xml-file -->
<!-- TIP: Most likely you don't want to change it -->
<!-- This is steamID64 of the master clan (group). If defined, bot will join the group and the chat automatically after logging in -->
<!-- The easiest way to get one is to check groups on your profile: http://steamcommunity.com/my/profile" -->
<!-- Then copying the URL of "Leave group" link, it will look like this: javascript:leaveGroupPrompt('103582791440160998','Archi\'s SC Farm') -->
<!-- The steamID64 we're looking for is there, in above example: "103582791440160998" -->
<!-- TIP: If you don't have your own farming group, most likely you don't want to change it, 0 means there is no master group defined -->
<SteamMasterClanID type="ulong" value="0"/>
<!-- This switch defines if you want to use built-in ASF two-factor-authentication (ASF 2FA) for this account -->
<!-- The one and only purpose for this function is automating steam logins and steam trades, so you won't need to enter 2FA codes all the time -->
<!-- This however defeats the whole purpose of two-factor-auth, and should be used on alt accounts ONLY -->
<!-- You should read full documentation, along with explanation here: https://github.com/JustArchi/ArchiSteamFarm/wiki/Escrow -->
<!-- WARNING, this option can potentially LOCK OUT YOUR ACCOUNT! If you don't fully understand this feature, DON'T USE IT! -->
<UseAsfAsMobileAuthenticator type="bool" value="false"/>
<!-- This switch defines if the account has card drops restricted -->
<!-- Restricted card drops means that the account doesn't receive any steam cards until it plays the game for at least 2 hours -->
<!-- As there is no magical way to detect it by ASF, I made this option config-based switch -->
<!-- TIP: Based on this parameter, ASF will try to choose the most optimal cards farming algorithm for this account -->
<CardDropsRestricted type="bool" value="false"/>
<!-- This switch defines if bot should disconnect once farming is finished -->
<!-- When no bots are active, ASF will shutdown as well -->
<!-- Some people may want to keep their bots 24/7, other disconnect them after job is done -->
@@ -50,11 +70,13 @@
<ShutdownOnFarmingFinished type="bool" value="false"/>
<!-- Comma-separated list of IDs that should not be considered for farming -->
<!-- Default value includes appIDs that are wrongly appearing on the profile, e.g. Monster Summer Sale -->
<!-- TIP: Most likely you don't want to change it -->
<Blacklist type="HashSet(uint)" value="303700,335590,368020"/>
<Blacklist type="HashSet(uint)" value="303700,335590,368020,425280"/>
<!-- This enables statistics for me - bot will join Archi's SC Farm group and chat -->
<!-- This switch enables statistics for me - bot will join Archi's SC Farm group and chat -->
<!-- Consider leaving it at "true", this way I can check how many running bots are active -->
<!-- That directly affects my willings to work on the project, as I can see how many users are actually using it -->
<!-- TIP: Group link is http://steamcommunity.com/groups/ascfarm -->
<Statistics type="bool" value="true"/>
</configuration>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<!-- This is minimalistic config to "just make ASF work" -->
<!-- For full-fledged config, please take a look at example.xml -->
<Enabled type="bool" value="false"/>
<SteamLogin type="string" value="null"/>
<SteamPassword type="string" value="null"/>
</configuration>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="HtmlAgilityPack" version="1.4.9" targetFramework="net45" />
<package id="Newtonsoft.Json" version="8.0.1-beta1" targetFramework="net45" />
<package id="Newtonsoft.Json" version="8.0.1-beta3" targetFramework="net45" />
<package id="protobuf-net" version="2.0.0.668" targetFramework="net45" />
<package id="SteamKit2" version="1.6.5" targetFramework="net45" />
</packages>

View File

@@ -1,42 +1,58 @@
ArchiSteamFarm
===================
Big work-in-progress. This bot allows you to farm steam cards using multiple accounts simultaneously. Each account is defined by it's own XML config in `config` directory and you don´t need any steam-client running in the background.
ASF is a C# application that allows you to farm steam cards using multiple steam accounts simultaneously. Unlike idle master which works only on one account at given time, requires steam client running in background, and launches additional processes imitiating "game playing" status, ASF doesn't require any steam client running in the background, doesn't launch any additional processes and is made to handle unlimited steam accounts at once. In addition to that, it's meant to be run on servers or other desktop-less machines, and features full Mono support, which makes it possible to launch on any Mono-supported operating system, such as Windows, Linux or OS X. ASF is based on, and possible, thanks to [SteamKit2](https://github.com/SteamRE/SteamKit).
**Current functions:**
ASF doesn't require and doesn't interfere in any way with Steam client. In addition to that, it no longer requires exclusive access to given account, which means that you can use your main account in Steam client, and use ASF for farming the same account at the same time. If you decide to launch a game, ASF will get disconnected, and resume farming once you finish playing your game, being as transparent as possible.
- Automatically farm steam cards using any number of active accounts
- Automatically accept friend requests sent from master
- Automatically accept all trades coming from master
- Automatically accept all steam cd-keys sent via chat from master
- SteamGuard / 2-factor-authentication support
- Full Mono support, tested on Debian "9.0" Stretch (testing)
**Core features:**
- Automatically farm available games using any number of active accounts
- Automatically accept friend requests sent from master
- Automatically accept all trades coming from master
- Automatically accept all steam cd-keys sent via chat from master
- Possibility to choose the most efficient cards farming algorithm, based on given account
- SteamGuard / SteamParental / 2FA support
- Unique ASF 2FA mechanism allowing ASF to act as mobile authenticator (if needed)
- ASF update notifications
- Full Mono support, cross-OS compatibility
**Setting up:**
Each ASF bot is defined in it's own XML config file in `config` directory. ASF comes with included ```example.xml``` config file, on which you should base all of your bots. Simply copy ```example.xml``` to a new file, and edit properties inside. Don't forget to switch ```Enabled``` property to ```true``` once you're done, as this is the master switch which enables configured bot to launch. The most minimalistic setup to make ASF working is changing only ```Enabled```, ```SteamLogin``` and ```SteamPassword``` properties, everything else is more or less optional to enable additional features.
After you set up all your bots (their configs), you should launch ```ASF.exe```. If your accounts require additional steps to unlock, such as Steam guard code, you'll need to enter those too after ASF tries to launch given bot. If everything ended properly, you should notice in the console output, as well as on your Steam, that all of your bots automatically started cards farming.
ASF doesn't require and doesn't interfere in any way with Steam client, which means that you can be logged in to Steam client as your primary account, and launch ASF at the same time, for any number of accounts, including your main one (if needed).
**Current Commands:**
- `!exit` Stops whole ASF
- `!farm` Restarts the bot and starts card-farming (again)
- `!start <BOT>` Starts given bot instance
- `!status` Prints current status of ASF
- `!stop <BOT>` Stops given bot instance
- `!2fa` Generates temporary 2FA token for current bot instance
- `!2fa <BOT>` Generates temporary 2FA token for given bot instance
- `!2faoff` Deactivates 2FA for current bot instance
- `!2faoff <BOT>` Deactivates 2FA for given bot instance
- `!exit` Stops whole ASF
- `!farm` Restarts cards farming module. ASF automatically executes that if any cd-key is successfully claimed
- `!redeem <KEY>` Redeems cd-key on current bot instance. You can also paste cd-key directly to the chat
- `!start <BOT>` Starts given bot instance
- `!status` Prints current status of ASF
- `!stop` Stops current bot instance
- `!stop <BOT>` Stops given bot instance
> You can use chat-commands in group-chat or private-chat with your bot.
> The MasterID has to be set for this specific bot / config-file.
> Commands can be executed via a private chat with your bot.
> Remember that bot accepts commands only from ```SteamMasterID```. That property can be configured in the config.
**Supported / Tested Operating-Systems:**
- Windows 10 Enterprise Edition
- Windows 10 Professional/Enterprise Edition (Native)
- Windows 8.1 Professional (Native)
- Windows 7 Ultimate (Native)
- Debian 9.0 Stretch (Mono)
- Debian 8.1 Jessie (Mono)
- OS X 10.11.1 (Mono)
However, any operating system [listed here](http://www.mono-project.com/docs/about-mono/supported-platforms/) should run ASF flawlessly.
**TODO**:
**Need help or more info?**
- Smart Multi-Game-Farming
- Possible integration with SteamTradeMatcher, bot can detect dupes and trade them automatically. Backend-code is already here, just missing actual implementation.
- Automatic sending of steamtrades to master(after game is fully farmed)
- Probably much more
> This is big WIP, so feel free to send pull requests if you wish. I'll
> release some releases later, when everything is tested and code
> cleaned up.
Head over to our [wiki](https://github.com/JustArchi/ArchiSteamFarm/wiki) then.

12
SteamAuth/APIEndpoints.cs Normal file
View File

@@ -0,0 +1,12 @@
namespace SteamAuth
{
public static class APIEndpoints
{
public const string STEAMAPI_BASE = "https://api.steampowered.com";
public const string COMMUNITY_BASE = "https://steamcommunity.com";
public const string MOBILEAUTH_BASE = STEAMAPI_BASE + "/IMobileAuthService/%s/v0001";
public static string MOBILEAUTH_GETWGTOKEN = MOBILEAUTH_BASE.Replace("%s", "GetWGToken");
public const string TWO_FACTOR_BASE = STEAMAPI_BASE + "/ITwoFactorService/%s/v0001";
public static string TWO_FACTOR_TIME_QUERY = TWO_FACTOR_BASE.Replace("%s", "QueryTime");
}
}

View File

@@ -0,0 +1,277 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace SteamAuth
{
/// <summary>
/// Handles the linking process for a new mobile authenticator.
/// </summary>
public class AuthenticatorLinker
{
/// <summary>
/// Set to register a new phone number when linking. If a phone number is not set on the account, this must be set. If a phone number is set on the account, this must be null.
/// </summary>
public string PhoneNumber = null;
/// <summary>
/// Randomly-generated device ID. Should only be generated once per linker.
/// </summary>
public string DeviceID { get; private set; }
/// <summary>
/// After the initial link step, if successful, this will be the SteamGuard data for the account. PLEASE save this somewhere after generating it; it's vital data.
/// </summary>
public SteamGuardAccount LinkedAccount { get; private set; }
/// <summary>
/// True if the authenticator has been fully finalized.
/// </summary>
public bool Finalized = false;
private SessionData _session;
private CookieContainer _cookies;
public AuthenticatorLinker(SessionData session)
{
this._session = session;
this.DeviceID = GenerateDeviceID();
this._cookies = new CookieContainer();
session.AddCookies(_cookies);
}
public LinkResult AddAuthenticator()
{
bool hasPhone = _hasPhoneAttached();
if (hasPhone && PhoneNumber != null)
return LinkResult.MustRemovePhoneNumber;
if (!hasPhone && PhoneNumber == null)
return LinkResult.MustProvidePhoneNumber;
if (!hasPhone)
{
if (!_addPhoneNumber())
{
return LinkResult.GeneralFailure;
}
}
var postData = new NameValueCollection();
postData.Add("access_token", _session.OAuthToken);
postData.Add("steamid", _session.SteamID.ToString());
postData.Add("authenticator_type", "1");
postData.Add("device_identifier", this.DeviceID);
postData.Add("sms_phone_id", "1");
string response = SteamWeb.MobileLoginRequest(APIEndpoints.STEAMAPI_BASE + "/ITwoFactorService/AddAuthenticator/v0001", "POST", postData);
if (response == null) return LinkResult.GeneralFailure;
var addAuthenticatorResponse = JsonConvert.DeserializeObject<AddAuthenticatorResponse>(response);
if (addAuthenticatorResponse == null || addAuthenticatorResponse.Response == null)
{
return LinkResult.GeneralFailure;
}
if (addAuthenticatorResponse.Response.Status == 29)
{
return LinkResult.AuthenticatorPresent;
}
if (addAuthenticatorResponse.Response.Status != 1)
{
return LinkResult.GeneralFailure;
}
this.LinkedAccount = addAuthenticatorResponse.Response;
LinkedAccount.Session = this._session;
LinkedAccount.DeviceID = this.DeviceID;
return LinkResult.AwaitingFinalization;
}
public FinalizeResult FinalizeAddAuthenticator(string smsCode)
{
bool smsCodeGood = false;
var postData = new NameValueCollection();
postData.Add("steamid", _session.SteamID.ToString());
postData.Add("access_token", _session.OAuthToken);
postData.Add("activation_code", smsCode);
postData.Add("authenticator_code", "");
int tries = 0;
while (tries <= 30)
{
postData.Set("authenticator_code", tries == 0 ? "" : LinkedAccount.GenerateSteamGuardCode());
postData.Add("authenticator_time", TimeAligner.GetSteamTime().ToString());
if(smsCodeGood)
postData.Set("activation_code", "");
string response = SteamWeb.MobileLoginRequest(APIEndpoints.STEAMAPI_BASE + "/ITwoFactorService/FinalizeAddAuthenticator/v0001", "POST", postData);
if (response == null) return FinalizeResult.GeneralFailure;
var finalizeResponse = JsonConvert.DeserializeObject<FinalizeAuthenticatorResponse>(response);
if (finalizeResponse == null || finalizeResponse.Response == null)
{
return FinalizeResult.GeneralFailure;
}
if(finalizeResponse.Response.Status == 89)
{
return FinalizeResult.BadSMSCode;
}
if(finalizeResponse.Response.Status == 88)
{
if(tries >= 30)
{
return FinalizeResult.UnableToGenerateCorrectCodes;
}
}
if (!finalizeResponse.Response.Success)
{
return FinalizeResult.GeneralFailure;
}
if (finalizeResponse.Response.WantMore)
{
smsCodeGood = true;
tries++;
continue;
}
this.LinkedAccount.FullyEnrolled = true;
return FinalizeResult.Success;
}
return FinalizeResult.GeneralFailure;
}
private bool _addPhoneNumber()
{
var postData = new NameValueCollection();
postData.Add("op", "add_phone_number");
postData.Add("arg", PhoneNumber);
postData.Add("sessionid", _session.SessionID);
string response = SteamWeb.Request(APIEndpoints.COMMUNITY_BASE + "/steamguard/phoneajax", "POST", postData, _cookies);
if (response == null) return false;
var addPhoneNumberResponse = JsonConvert.DeserializeObject<AddPhoneResponse>(response);
return addPhoneNumberResponse.Success;
}
private bool _hasPhoneAttached()
{
var postData = new NameValueCollection();
postData.Add("op", "has_phone");
postData.Add("arg", "null");
postData.Add("sessionid", _session.SessionID);
string response = SteamWeb.Request(APIEndpoints.COMMUNITY_BASE + "/steamguard/phoneajax", "POST", postData, _cookies);
if (response == null) return false;
var hasPhoneResponse = JsonConvert.DeserializeObject<HasPhoneResponse>(response);
return hasPhoneResponse.HasPhone;
}
public enum LinkResult
{
MustProvidePhoneNumber, //No phone number on the account
MustRemovePhoneNumber, //A phone number is already on the account
AwaitingFinalization, //Must provide an SMS code
GeneralFailure, //General failure (really now!)
AuthenticatorPresent
}
public enum FinalizeResult
{
BadSMSCode,
UnableToGenerateCorrectCodes,
Success,
GeneralFailure
}
private class AddAuthenticatorResponse
{
[JsonProperty("response")]
public SteamGuardAccount Response { get; set; }
}
private class FinalizeAuthenticatorResponse
{
[JsonProperty("response")]
public FinalizeAuthenticatorInternalResponse Response { get; set; }
internal class FinalizeAuthenticatorInternalResponse
{
[JsonProperty("status")]
public int Status { get; set; }
[JsonProperty("server_time")]
public long ServerTime { get; set; }
[JsonProperty("want_more")]
public bool WantMore { get; set; }
[JsonProperty("success")]
public bool Success { get; set; }
}
}
private class HasPhoneResponse
{
[JsonProperty("has_phone")]
public bool HasPhone { get; set; }
}
private class AddPhoneResponse
{
[JsonProperty("success")]
public bool Success { get; set; }
}
public static string GenerateDeviceID()
{
using (var sha1 = new SHA1Managed())
{
RNGCryptoServiceProvider secureRandom = new RNGCryptoServiceProvider();
byte[] randomBytes = new byte[8];
secureRandom.GetBytes(randomBytes);
byte[] hashedBytes = sha1.ComputeHash(randomBytes);
string random32 = BitConverter.ToString(hashedBytes).Replace("-", "").Substring(0, 32).ToLower();
return "android:" + SplitOnRatios(random32, new[] { 8, 4, 4, 4, 12 }, "-");
}
}
private static string SplitOnRatios(string str, int[] ratios, string intermediate)
{
string result = "";
int pos = 0;
for (int index = 0; index < ratios.Length; index++)
{
result += str.Substring(pos, ratios[index]);
pos = ratios[index];
if (index < ratios.Length - 1)
result += intermediate;
}
return result;
}
}
}

15
SteamAuth/Confirmation.cs Normal file
View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SteamAuth
{
public class Confirmation
{
public string ConfirmationID;
public string ConfirmationKey;
public string ConfirmationDescription;
}
}

View File

@@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("SteamAuth")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("SteamAuth")]
[assembly: AssemblyCopyright("Copyright © 2015")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("5ad0934e-f6c4-4ae5-83af-c788313b2a87")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

40
SteamAuth/SessionData.cs Normal file
View File

@@ -0,0 +1,40 @@
using System.Net;
namespace SteamAuth
{
public class SessionData
{
public string SessionID { get; set; }
public string SteamLogin { get; set; }
public string SteamLoginSecure { get; set; }
public string WebCookie { get; set; }
public string OAuthToken { get; set; }
public ulong SteamID { get; set; }
public void AddCookies(CookieContainer cookies)
{
cookies.Add(new Cookie("mobileClientVersion", "0 (2.1.3)", "/", ".steamcommunity.com"));
cookies.Add(new Cookie("mobileClient", "android", "/", ".steamcommunity.com"));
cookies.Add(new Cookie("steamid", SteamID.ToString(), "/", ".steamcommunity.com"));
cookies.Add(new Cookie("steamLogin", SteamLogin, "/", ".steamcommunity.com")
{
HttpOnly = true
});
cookies.Add(new Cookie("steamLoginSecure", SteamLoginSecure, "/", ".steamcommunity.com")
{
HttpOnly = true,
Secure = true
});
cookies.Add(new Cookie("Steam_Language", "english", "/", ".steamcommunity.com"));
cookies.Add(new Cookie("dob", "", "/", ".steamcommunity.com"));
cookies.Add(new Cookie("sessionid", this.SessionID, "/", ".steamcommunity.com"));
}
}
}

View File

@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{5AD0934E-F6C4-4AE5-83AF-C788313B2A87}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>SteamAuth</RootNamespace>
<AssemblyName>SteamAuth</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Newtonsoft.Json, Version=8.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.8.0.1-beta3\lib\net45\Newtonsoft.Json.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="APIEndpoints.cs" />
<Compile Include="AuthenticatorLinker.cs" />
<Compile Include="Confirmation.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SessionData.cs" />
<Compile Include="SteamGuardAccount.cs" />
<Compile Include="SteamWeb.cs" />
<Compile Include="TimeAligner.cs" />
<Compile Include="UserLogin.cs" />
<Compile Include="Util.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

28
SteamAuth/SteamAuth.sln Normal file
View File

@@ -0,0 +1,28 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.23107.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SteamAuth", "SteamAuth.csproj", "{5AD0934E-F6C4-4AE5-83AF-C788313B2A87}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestBed", "..\TestBed\TestBed.csproj", "{8A732227-C090-4011-9F0A-51180CFE6271}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{5AD0934E-F6C4-4AE5-83AF-C788313B2A87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5AD0934E-F6C4-4AE5-83AF-C788313B2A87}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5AD0934E-F6C4-4AE5-83AF-C788313B2A87}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5AD0934E-F6C4-4AE5-83AF-C788313B2A87}.Release|Any CPU.Build.0 = Release|Any CPU
{8A732227-C090-4011-9F0A-51180CFE6271}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8A732227-C090-4011-9F0A-51180CFE6271}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8A732227-C090-4011-9F0A-51180CFE6271}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8A732227-C090-4011-9F0A-51180CFE6271}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,419 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace SteamAuth
{
public class SteamGuardAccount
{
[JsonProperty("shared_secret")]
public string SharedSecret { get; set; }
[JsonProperty("serial_number")]
public string SerialNumber { get; set; }
[JsonProperty("revocation_code")]
public string RevocationCode { get; set; }
[JsonProperty("uri")]
public string URI { get; set; }
[JsonProperty("server_time")]
public long ServerTime { get; set; }
[JsonProperty("account_name")]
public string AccountName { get; set; }
[JsonProperty("token_gid")]
public string TokenGID { get; set; }
[JsonProperty("identity_secret")]
public string IdentitySecret { get; set; }
[JsonProperty("secret_1")]
public string Secret1 { get; set; }
[JsonProperty("status")]
public int Status { get; set; }
[JsonProperty("device_id")]
public string DeviceID { get; set; }
/// <summary>
/// Set to true if the authenticator has actually been applied to the account.
/// </summary>
[JsonProperty("fully_enrolled")]
public bool FullyEnrolled { get; set; }
public SessionData Session { get; set; }
private static byte[] steamGuardCodeTranslations = new byte[] { 50, 51, 52, 53, 54, 55, 56, 57, 66, 67, 68, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 86, 87, 88, 89 };
public bool DeactivateAuthenticator(int scheme = 2)
{
var postData = new NameValueCollection();
postData.Add("steamid", this.Session.SteamID.ToString());
postData.Add("steamguard_scheme", scheme.ToString());
postData.Add("revocation_code", this.RevocationCode);
postData.Add("access_token", this.Session.OAuthToken);
try
{
string response = SteamWeb.MobileLoginRequest(APIEndpoints.STEAMAPI_BASE + "/ITwoFactorService/RemoveAuthenticator/v0001", "POST", postData);
var removeResponse = JsonConvert.DeserializeObject<RemoveAuthenticatorResponse>(response);
if (removeResponse == null || removeResponse.Response == null || !removeResponse.Response.Success) return false;
return true;
}
catch (Exception e)
{
return false;
}
}
public string GenerateSteamGuardCode()
{
return GenerateSteamGuardCodeForTime(TimeAligner.GetSteamTime());
}
public string GenerateSteamGuardCodeForTime(long time)
{
if (this.SharedSecret == null || this.SharedSecret.Length == 0)
{
return "";
}
byte[] sharedSecretArray = Convert.FromBase64String(this.SharedSecret);
byte[] timeArray = new byte[8];
time /= 30L;
for (int i = 8; i > 0; i--)
{
timeArray[i - 1] = (byte)time;
time >>= 8;
}
HMACSHA1 hmacGenerator = new HMACSHA1();
hmacGenerator.Key = sharedSecretArray;
byte[] hashedData = hmacGenerator.ComputeHash(timeArray);
byte[] codeArray = new byte[5];
try
{
byte b = (byte)(hashedData[19] & 0xF);
int codePoint = (hashedData[b] & 0x7F) << 24 | (hashedData[b + 1] & 0xFF) << 16 | (hashedData[b + 2] & 0xFF) << 8 | (hashedData[b + 3] & 0xFF);
for (int i = 0; i < 5; ++i)
{
codeArray[i] = steamGuardCodeTranslations[codePoint % steamGuardCodeTranslations.Length];
codePoint /= steamGuardCodeTranslations.Length;
}
}
catch (Exception e)
{
return null; //Change later, catch-alls are bad!
}
return Encoding.UTF8.GetString(codeArray);
}
public Confirmation[] FetchConfirmations()
{
string url = this.GenerateConfirmationURL();
CookieContainer cookies = new CookieContainer();
this.Session.AddCookies(cookies);
string response = SteamWeb.Request(url, "GET", null, cookies);
/*So you're going to see this abomination and you're going to be upset.
It's understandable. But the thing is, regex for HTML -- while awful -- makes this way faster than parsing a DOM, plus we don't need another library.
And because the data is always in the same place and same format... It's not as if we're trying to naturally understand HTML here. Just extract strings.
I'm sorry. */
Regex confIDRegex = new Regex("data-confid=\"(\\d+)\"");
Regex confKeyRegex = new Regex("data-key=\"(\\d+)\"");
Regex confDescRegex = new Regex("<div>((Confirm|Trade with|Sell -) .+)</div>");
if (response == null || !(confIDRegex.IsMatch(response) && confKeyRegex.IsMatch(response) && confDescRegex.IsMatch(response)))
{
if (response == null || !response.Contains("<div>Nothing to confirm</div>"))
{
throw new WGTokenInvalidException();
}
return new Confirmation[0];
}
MatchCollection confIDs = confIDRegex.Matches(response);
MatchCollection confKeys = confKeyRegex.Matches(response);
MatchCollection confDescs = confDescRegex.Matches(response);
List<Confirmation> ret = new List<Confirmation>();
for (int i = 0; i < confIDs.Count; i++)
{
string confID = confIDs[i].Groups[1].Value;
string confKey = confKeys[i].Groups[1].Value;
string confDesc = confDescs[i].Groups[1].Value;
Confirmation conf = new Confirmation()
{
ConfirmationDescription = confDesc,
ConfirmationID = confID,
ConfirmationKey = confKey
};
ret.Add(conf);
}
return ret.ToArray();
}
public async Task<Confirmation[]> FetchConfirmationsAsync()
{
string url = this.GenerateConfirmationURL();
CookieContainer cookies = new CookieContainer();
this.Session.AddCookies(cookies);
string response = await SteamWeb.RequestAsync(url, "GET", null, cookies);
/*So you're going to see this abomination and you're going to be upset.
It's understandable. But the thing is, regex for HTML -- while awful -- makes this way faster than parsing a DOM, plus we don't need another library.
And because the data is always in the same place and same format... It's not as if we're trying to naturally understand HTML here. Just extract strings.
I'm sorry. */
Regex confIDRegex = new Regex("data-confid=\"(\\d+)\"");
Regex confKeyRegex = new Regex("data-key=\"(\\d+)\"");
Regex confDescRegex = new Regex("<div>((Confirm|Trade with|Sell -) .+)</div>");
if (response == null || !(confIDRegex.IsMatch(response) && confKeyRegex.IsMatch(response) && confDescRegex.IsMatch(response)))
{
if (response == null || !response.Contains("<div>Nothing to confirm</div>"))
{
throw new WGTokenInvalidException();
}
return new Confirmation[0];
}
MatchCollection confIDs = confIDRegex.Matches(response);
MatchCollection confKeys = confKeyRegex.Matches(response);
MatchCollection confDescs = confDescRegex.Matches(response);
List<Confirmation> ret = new List<Confirmation>();
for (int i = 0; i < confIDs.Count; i++)
{
string confID = confIDs[i].Groups[1].Value;
string confKey = confKeys[i].Groups[1].Value;
string confDesc = confDescs[i].Groups[1].Value;
Confirmation conf = new Confirmation()
{
ConfirmationDescription = confDesc,
ConfirmationID = confID,
ConfirmationKey = confKey
};
ret.Add(conf);
}
return ret.ToArray();
}
public bool AcceptConfirmation(Confirmation conf)
{
return _sendConfirmationAjax(conf, "allow");
}
public bool DenyConfirmation(Confirmation conf)
{
return _sendConfirmationAjax(conf, "cancel");
}
/// <summary>
/// Refreshes the Steam session. Necessary to perform confirmations if your session has expired or changed.
/// </summary>
/// <returns></returns>
public bool RefreshSession()
{
string url = APIEndpoints.MOBILEAUTH_GETWGTOKEN;
NameValueCollection postData = new NameValueCollection();
postData.Add("access_token", this.Session.OAuthToken);
string response = SteamWeb.Request(url, "POST", postData);
if (response == null) return false;
try
{
var refreshResponse = JsonConvert.DeserializeObject<RefreshSessionDataResponse>(response);
if (refreshResponse == null || refreshResponse.Response == null || String.IsNullOrEmpty(refreshResponse.Response.Token))
return false;
string token = this.Session.SteamID + "%7C%7C" + refreshResponse.Response.Token;
string tokenSecure = this.Session.SteamID + "%7C%7C" + refreshResponse.Response.TokenSecure;
this.Session.SteamLogin = token;
this.Session.SteamLoginSecure = tokenSecure;
return true;
}
catch (Exception e)
{
return false;
}
}
/// <summary>
/// Refreshes the Steam session. Necessary to perform confirmations if your session has expired or changed.
/// </summary>
/// <returns></returns>
public async Task<bool> RefreshSessionAsync()
{
string url = APIEndpoints.MOBILEAUTH_GETWGTOKEN;
NameValueCollection postData = new NameValueCollection();
postData.Add("access_token", this.Session.OAuthToken);
string response = await SteamWeb.RequestAsync(url, "POST", postData);
if (response == null) return false;
try
{
var refreshResponse = JsonConvert.DeserializeObject<RefreshSessionDataResponse>(response);
if (refreshResponse == null || refreshResponse.Response == null || String.IsNullOrEmpty(refreshResponse.Response.Token))
return false;
string token = this.Session.SteamID + "%7C%7C" + refreshResponse.Response.Token;
string tokenSecure = this.Session.SteamID + "%7C%7C" + refreshResponse.Response.TokenSecure;
this.Session.SteamLogin = token;
this.Session.SteamLoginSecure = tokenSecure;
return true;
}
catch (Exception e)
{
return false;
}
}
private bool _sendConfirmationAjax(Confirmation conf, string op)
{
string url = APIEndpoints.COMMUNITY_BASE + "/mobileconf/ajaxop";
string queryString = "?op=" + op + "&";
queryString += GenerateConfirmationQueryParams(op);
queryString += "&cid=" + conf.ConfirmationID + "&ck=" + conf.ConfirmationKey;
url += queryString;
CookieContainer cookies = new CookieContainer();
this.Session.AddCookies(cookies);
string referer = GenerateConfirmationURL();
string response = SteamWeb.Request(url, "GET", null, cookies, null);
if (response == null) return false;
SendConfirmationResponse confResponse = JsonConvert.DeserializeObject<SendConfirmationResponse>(response);
return confResponse.Success;
}
public string GenerateConfirmationURL(string tag = "conf")
{
string endpoint = APIEndpoints.COMMUNITY_BASE + "/mobileconf/conf?";
string queryString = GenerateConfirmationQueryParams(tag);
return endpoint + queryString;
}
public string GenerateConfirmationQueryParams(string tag)
{
if (String.IsNullOrEmpty(DeviceID))
DeviceID = AuthenticatorLinker.GenerateDeviceID();
long time = TimeAligner.GetSteamTime();
return "p=" + this.DeviceID + "&a=" + this.Session.SteamID.ToString() + "&k=" + _generateConfirmationHashForTime(time, tag) + "&t=" + time + "&m=android&tag=" + tag;
}
private string _generateConfirmationHashForTime(long time, string tag)
{
byte[] decode = Convert.FromBase64String(this.IdentitySecret);
int n2 = 8;
if (tag != null)
{
if (tag.Length > 32)
{
n2 = 8 + 32;
}
else
{
n2 = 8 + tag.Length;
}
}
byte[] array = new byte[n2];
int n3 = 8;
while (true)
{
int n4 = n3 - 1;
if (n3 <= 0)
{
break;
}
array[n4] = (byte)time;
time >>= 8;
n3 = n4;
}
if (tag != null)
{
Array.Copy(Encoding.UTF8.GetBytes(tag), 0, array, 8, n2 - 8);
}
try
{
HMACSHA1 hmacGenerator = new HMACSHA1();
hmacGenerator.Key = decode;
byte[] hashedData = hmacGenerator.ComputeHash(array);
string encodedData = Convert.ToBase64String(hashedData, Base64FormattingOptions.None);
string hash = WebUtility.UrlEncode(encodedData);
return hash;
}
catch (Exception e)
{
return null; //Fix soon: catch-all is BAD!
}
}
//TODO: Determine how to detect an invalid session.
public class WGTokenInvalidException : Exception
{
}
private class RefreshSessionDataResponse
{
[JsonProperty("response")]
public RefreshSessionDataInternalResponse Response { get; set; }
internal class RefreshSessionDataInternalResponse
{
[JsonProperty("token")]
public string Token { get; set; }
[JsonProperty("token_secure")]
public string TokenSecure { get; set; }
}
}
private class RemoveAuthenticatorResponse
{
[JsonProperty("response")]
public RemoveAuthenticatorInternalResponse Response { get; set; }
internal class RemoveAuthenticatorInternalResponse
{
[JsonProperty("success")]
public bool Success { get; set; }
}
}
private class SendConfirmationResponse
{
[JsonProperty("success")]
public bool Success { get; set; }
}
}
}

137
SteamAuth/SteamWeb.cs Normal file
View File

@@ -0,0 +1,137 @@
using System;
using System.Collections.Specialized;
using System.IO;
using System.Net;
using System.Threading.Tasks;
namespace SteamAuth
{
public class SteamWeb
{
/// <summary>
/// Perform a mobile login request
/// </summary>
/// <param name="url">API url</param>
/// <param name="method">GET or POST</param>
/// <param name="data">Name-data pairs</param>
/// <param name="cookies">current cookie container</param>
/// <returns>response body</returns>
public static string MobileLoginRequest(string url, string method, NameValueCollection data = null, CookieContainer cookies = null, NameValueCollection headers = null)
{
return Request(url, method, data, cookies, headers, APIEndpoints.COMMUNITY_BASE + "/mobilelogin?oauth_client_id=DE45CD61&oauth_scope=read_profile%20write_profile%20read_client%20write_client");
}
public static string Request(string url, string method, NameValueCollection data = null, CookieContainer cookies = null, NameValueCollection headers = null, string referer = APIEndpoints.COMMUNITY_BASE)
{
string query = (data == null ? string.Empty : string.Join("&", Array.ConvertAll(data.AllKeys, key => String.Format("{0}={1}", WebUtility.UrlEncode(key), WebUtility.UrlEncode(data[key])))));
if (method == "GET")
{
url += (url.Contains("?") ? "&" : "?") + query;
}
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.Method = method;
request.Accept = "text/javascript, text/html, application/xml, text/xml, */*";
request.UserAgent = "Mozilla/5.0 (Linux; U; Android 4.1.1; en-us; Google Nexus 4 - 4.1.1 - API 16 - 768x1280 Build/JRO03S) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30";
request.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;
request.Referer = referer;
if (headers != null)
{
request.Headers.Add(headers);
}
if (cookies != null)
{
request.CookieContainer = cookies;
}
if (method == "POST")
{
request.ContentType = "application/x-www-form-urlencoded; charset=UTF-8";
request.ContentLength = query.Length;
StreamWriter requestStream = new StreamWriter(request.GetRequestStream());
requestStream.Write(query);
requestStream.Close();
}
try
{
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
{
if (response.StatusCode != HttpStatusCode.OK)
{
return null;
}
using (StreamReader responseStream = new StreamReader(response.GetResponseStream()))
{
string responseData = responseStream.ReadToEnd();
return responseData;
}
}
}
catch (WebException)
{
return null;
}
}
public static async Task<string> RequestAsync(string url, string method, NameValueCollection data = null, CookieContainer cookies = null, NameValueCollection headers = null, string referer = APIEndpoints.COMMUNITY_BASE)
{
string query = (data == null ? string.Empty : string.Join("&", Array.ConvertAll(data.AllKeys, key => String.Format("{0}={1}", WebUtility.UrlEncode(key), WebUtility.UrlEncode(data[key])))));
if (method == "GET")
{
url += (url.Contains("?") ? "&" : "?") + query;
}
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.Method = method;
request.Accept = "text/javascript, text/html, application/xml, text/xml, */*";
request.UserAgent = "Mozilla/5.0 (Linux; U; Android 4.1.1; en-us; Google Nexus 4 - 4.1.1 - API 16 - 768x1280 Build/JRO03S) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30";
request.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;
request.Referer = referer;
if (headers != null)
{
request.Headers.Add(headers);
}
if (cookies != null)
{
request.CookieContainer = cookies;
}
if (method == "POST")
{
request.ContentType = "application/x-www-form-urlencoded; charset=UTF-8";
request.ContentLength = query.Length;
StreamWriter requestStream = new StreamWriter(request.GetRequestStream());
requestStream.Write(query);
requestStream.Close();
}
try
{
HttpWebResponse response = (HttpWebResponse) await request.GetResponseAsync();
if (response.StatusCode != HttpStatusCode.OK)
{
return null;
}
using (StreamReader responseStream = new StreamReader(response.GetResponseStream()))
{
string responseData = responseStream.ReadToEnd();
return responseData;
}
}
catch (WebException)
{
return null;
}
}
}
}

84
SteamAuth/TimeAligner.cs Normal file
View File

@@ -0,0 +1,84 @@
using System;
using System.Threading.Tasks;
using System.Net;
using Newtonsoft.Json;
namespace SteamAuth
{
/// <summary>
/// Class to help align system time with the Steam server time. Not super advanced; probably not taking some things into account that it should.
/// Necessary to generate up-to-date codes. In general, this will have an error of less than a second, assuming Steam is operational.
/// </summary>
public class TimeAligner
{
private static bool _aligned = false;
private static int _timeDifference = 0;
public static long GetSteamTime()
{
if (!TimeAligner._aligned)
{
TimeAligner.AlignTime();
}
return Util.GetSystemUnixTime() + _timeDifference;
}
public static async Task<long> GetSteamTimeAsync()
{
if (!TimeAligner._aligned)
{
await TimeAligner.AlignTimeAsync();
}
return Util.GetSystemUnixTime() + _timeDifference;
}
public static void AlignTime()
{
long currentTime = Util.GetSystemUnixTime();
using (WebClient client = new WebClient())
{
try
{
string response = client.UploadString(APIEndpoints.TWO_FACTOR_TIME_QUERY, "steamid=0");
TimeQuery query = JsonConvert.DeserializeObject<TimeQuery>(response);
TimeAligner._timeDifference = (int)(query.Response.ServerTime - currentTime);
TimeAligner._aligned = true;
}
catch (WebException)
{
return;
}
}
}
public static async Task AlignTimeAsync()
{
long currentTime = Util.GetSystemUnixTime();
WebClient client = new WebClient();
try
{
string response = await client.UploadStringTaskAsync(new Uri(APIEndpoints.TWO_FACTOR_TIME_QUERY), "steamid=0");
TimeQuery query = JsonConvert.DeserializeObject<TimeQuery>(response);
TimeAligner._timeDifference = (int)(query.Response.ServerTime - currentTime);
TimeAligner._aligned = true;
}
catch (WebException)
{
return;
}
}
internal class TimeQuery
{
[JsonProperty("response")]
internal TimeQueryResponse Response { get; set; }
internal class TimeQueryResponse
{
[JsonProperty("server_time")]
public long ServerTime { get; set; }
}
}
}
}

254
SteamAuth/UserLogin.cs Normal file
View File

@@ -0,0 +1,254 @@
using Newtonsoft.Json;
using System;
using System.Collections.Specialized;
using System.Net;
using System.Security.Cryptography;
using System.Text;
namespace SteamAuth
{
/// <summary>
/// Handles logging the user into the mobile Steam website. Necessary to generate OAuth token and session cookies.
/// </summary>
public class UserLogin
{
public string Username;
public string Password;
public ulong SteamID;
public bool RequiresCaptcha;
public string CaptchaGID = null;
public string CaptchaText = null;
public bool RequiresEmail;
public string EmailDomain = null;
public string EmailCode = null;
public bool Requires2FA;
public string TwoFactorCode = null;
public SessionData Session = null;
public bool LoggedIn = false;
private CookieContainer _cookies = new CookieContainer();
public UserLogin(string username, string password)
{
this.Username = username;
this.Password = password;
}
public LoginResult DoLogin()
{
var postData = new NameValueCollection();
var cookies = _cookies;
string response = null;
if (cookies.Count == 0)
{
//Generate a SessionID
cookies.Add(new Cookie("mobileClientVersion", "0 (2.1.3)", "/", ".steamcommunity.com"));
cookies.Add(new Cookie("mobileClient", "android", "/", ".steamcommunity.com"));
cookies.Add(new Cookie("Steam_Language", "english", "/", ".steamcommunity.com"));
NameValueCollection headers = new NameValueCollection();
headers.Add("X-Requested-With", "com.valvesoftware.android.steam.community");
SteamWeb.MobileLoginRequest("https://steamcommunity.com/login?oauth_client_id=DE45CD61&oauth_scope=read_profile%20write_profile%20read_client%20write_client", "GET", null, cookies, headers);
}
postData.Add("username", this.Username);
response = SteamWeb.MobileLoginRequest(APIEndpoints.COMMUNITY_BASE + "/login/getrsakey", "POST", postData, cookies);
if (response == null || response.Contains("<BODY>\nAn error occurred while processing your request.")) return LoginResult.GeneralFailure;
var rsaResponse = JsonConvert.DeserializeObject<RSAResponse>(response);
if (!rsaResponse.Success)
{
return LoginResult.BadRSA;
}
RNGCryptoServiceProvider secureRandom = new RNGCryptoServiceProvider();
byte[] encryptedPasswordBytes;
using (var rsaEncryptor = new RSACryptoServiceProvider())
{
var passwordBytes = Encoding.ASCII.GetBytes(this.Password);
var rsaParameters = rsaEncryptor.ExportParameters(false);
rsaParameters.Exponent = Util.HexStringToByteArray(rsaResponse.Exponent);
rsaParameters.Modulus = Util.HexStringToByteArray(rsaResponse.Modulus);
rsaEncryptor.ImportParameters(rsaParameters);
encryptedPasswordBytes = rsaEncryptor.Encrypt(passwordBytes, false);
}
string encryptedPassword = Convert.ToBase64String(encryptedPasswordBytes);
postData.Clear();
postData.Add("username", this.Username);
postData.Add("password", encryptedPassword);
postData.Add("twofactorcode", this.TwoFactorCode ?? "");
postData.Add("captchagid", this.RequiresCaptcha ? this.CaptchaGID : "-1");
postData.Add("captcha_text", this.RequiresCaptcha ? this.CaptchaText : "");
postData.Add("emailsteamid", (this.Requires2FA || this.RequiresEmail) ? this.SteamID.ToString() : "");
postData.Add("emailauth", this.RequiresEmail ? this.EmailCode : "");
postData.Add("rsatimestamp", rsaResponse.Timestamp);
postData.Add("remember_login", "false");
postData.Add("oauth_client_id", "DE45CD61");
postData.Add("oauth_scope", "read_profile write_profile read_client write_client");
postData.Add("loginfriendlyname", "#login_emailauth_friendlyname_mobile");
postData.Add("donotcache", Util.GetSystemUnixTime().ToString());
response = SteamWeb.MobileLoginRequest(APIEndpoints.COMMUNITY_BASE + "/login/dologin", "POST", postData, cookies);
if (response == null) return LoginResult.GeneralFailure;
var loginResponse = JsonConvert.DeserializeObject<LoginResponse>(response);
if (loginResponse.Message != null && loginResponse.Message.Contains("Incorrect login"))
{
return LoginResult.BadCredentials;
}
if (loginResponse.CaptchaNeeded)
{
this.RequiresCaptcha = true;
this.CaptchaGID = loginResponse.CaptchaGID;
return LoginResult.NeedCaptcha;
}
if (loginResponse.EmailAuthNeeded)
{
this.RequiresEmail = true;
this.SteamID = loginResponse.EmailSteamID;
return LoginResult.NeedEmail;
}
if (loginResponse.TwoFactorNeeded && !loginResponse.Success)
{
this.Requires2FA = true;
return LoginResult.Need2FA;
}
if (loginResponse.Message != null && loginResponse.Message.Contains("too many login failures"))
{
return LoginResult.TooManyFailedLogins;
}
if (loginResponse.OAuthData == null || loginResponse.OAuthData.OAuthToken == null || loginResponse.OAuthData.OAuthToken.Length == 0)
{
return LoginResult.GeneralFailure;
}
if (!loginResponse.LoginComplete)
{
return LoginResult.BadCredentials;
}
else
{
var readableCookies = cookies.GetCookies(new Uri("https://steamcommunity.com"));
var oAuthData = loginResponse.OAuthData;
SessionData session = new SessionData();
session.OAuthToken = oAuthData.OAuthToken;
session.SteamID = oAuthData.SteamID;
session.SteamLogin = session.SteamID + "%7C%7C" + oAuthData.SteamLogin;
session.SteamLoginSecure = session.SteamID + "%7C%7C" + oAuthData.SteamLoginSecure;
session.WebCookie = oAuthData.Webcookie;
session.SessionID = readableCookies["sessionid"].Value;
this.Session = session;
this.LoggedIn = true;
return LoginResult.LoginOkay;
}
return LoginResult.GeneralFailure;
}
private class LoginResponse
{
[JsonProperty("success")]
public bool Success { get; set; }
[JsonProperty("login_complete")]
public bool LoginComplete { get; set; }
[JsonProperty("oauth")]
public string OAuthDataString { get; set; }
public OAuth OAuthData
{
get
{
return OAuthDataString != null ? JsonConvert.DeserializeObject<OAuth>(OAuthDataString) : null;
}
}
[JsonProperty("captcha_needed")]
public bool CaptchaNeeded { get; set; }
[JsonProperty("captcha_gid")]
public string CaptchaGID { get; set; }
[JsonProperty("emailsteamid")]
public ulong EmailSteamID { get; set; }
[JsonProperty("emailauth_needed")]
public bool EmailAuthNeeded { get; set; }
[JsonProperty("requires_twofactor")]
public bool TwoFactorNeeded { get; set; }
[JsonProperty("message")]
public string Message { get; set; }
internal class OAuth
{
[JsonProperty("steamid")]
public ulong SteamID { get; set; }
[JsonProperty("oauth_token")]
public string OAuthToken { get; set; }
[JsonProperty("wgtoken")]
public string SteamLogin { get; set; }
[JsonProperty("wgtoken_secure")]
public string SteamLoginSecure { get; set; }
[JsonProperty("webcookie")]
public string Webcookie { get; set; }
}
}
private class RSAResponse
{
[JsonProperty("success")]
public bool Success { get; set; }
[JsonProperty("publickey_exp")]
public string Exponent { get; set; }
[JsonProperty("publickey_mod")]
public string Modulus { get; set; }
[JsonProperty("timestamp")]
public string Timestamp { get; set; }
[JsonProperty("steamid")]
public ulong SteamID { get; set; }
}
}
public enum LoginResult
{
LoginOkay,
GeneralFailure,
BadRSA,
BadCredentials,
NeedCaptcha,
Need2FA,
NeedEmail,
TooManyFailedLogins,
}
}

24
SteamAuth/Util.cs Normal file
View File

@@ -0,0 +1,24 @@
using System;
using System.Net;
namespace SteamAuth
{
public class Util
{
public static long GetSystemUnixTime()
{
return (long)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds;
}
public static byte[] HexStringToByteArray(string hex)
{
int hexLen = hex.Length;
byte[] ret = new byte[hexLen / 2];
for (int i = 0; i < hexLen; i += 2)
{
ret[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
}
return ret;
}
}
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Newtonsoft.Json" version="8.0.1-beta3" targetFramework="net452" />
</packages>

View File

@@ -106,7 +106,7 @@
<summary>
Reads the next JSON token from the stream as a <see cref="T:System.Nullable`1"/>.
</summary>
<returns>A <see cref="T:System.String"/>. This method will return <c>null</c> at the end of an array.</returns>
<returns>A <see cref="T:System.Nullable`1"/>. This method will return <c>null</c> at the end of an array.</returns>
</member>
<member name="M:Newtonsoft.Json.Bson.BsonReader.Read">
<summary>
@@ -926,6 +926,25 @@
Causes child objects to be indented according to the <see cref="P:Newtonsoft.Json.JsonTextWriter.Indentation"/> and <see cref="P:Newtonsoft.Json.JsonTextWriter.IndentChar"/> settings.
</summary>
</member>
<member name="T:Newtonsoft.Json.IJsonBufferPool`1">
<summary>
Provides an interface for using pooled buffers.
</summary>
<typeparam name="T">The buffer type content.</typeparam>
</member>
<member name="M:Newtonsoft.Json.IJsonBufferPool`1.RentBuffer(System.Int32)">
<summary>
Rent a buffer from the pool. This buffer must be returned when it is no longer needed.
</summary>
<param name="minSize">The minimum required size of the buffer. The returned buffer may be larger.</param>
<returns>The rented buffer from the pool.</returns>
</member>
<member name="M:Newtonsoft.Json.IJsonBufferPool`1.ReturnBuffer(`0[]@)">
<summary>
Return a buffer to the pool.
</summary>
<param name="buffer">The buffer that is being returned.</param>
</member>
<member name="T:Newtonsoft.Json.JsonConstructorAttribute">
<summary>
Instructs the <see cref="T:Newtonsoft.Json.JsonSerializer"/> to use the specified constructor when deserializing that object.
@@ -1028,6 +1047,21 @@
Load comments as a <see cref="T:Newtonsoft.Json.Linq.JValue"/> with type <see cref="F:Newtonsoft.Json.Linq.JTokenType.Comment"/>.
</summary>
</member>
<member name="T:Newtonsoft.Json.Linq.LineInfoHandling">
<summary>
Specifies how line information is handled when loading JSON.
</summary>
</member>
<member name="F:Newtonsoft.Json.Linq.LineInfoHandling.Ignore">
<summary>
Ignore line information.
</summary>
</member>
<member name="F:Newtonsoft.Json.Linq.LineInfoHandling.Load">
<summary>
Load line information.
</summary>
</member>
<member name="T:Newtonsoft.Json.Linq.JsonLoadSettings">
<summary>
Specifies the settings used when loading JSON.
@@ -1039,6 +1073,12 @@
</summary>
<value>The JSON comment handling.</value>
</member>
<member name="P:Newtonsoft.Json.Linq.JsonLoadSettings.LineInfoHandling">
<summary>
Gets or sets how JSON line info is handled when loading JSON.
</summary>
<value>The JSON line info handling.</value>
</member>
<member name="T:Newtonsoft.Json.Linq.JsonMergeSettings">
<summary>
Specifies the settings used when merging JSON.
@@ -2167,7 +2207,7 @@
<summary>
Reads the next JSON token from the stream as a <see cref="T:System.Nullable`1"/>.
</summary>
<returns>A <see cref="T:System.String"/>. This method will return <c>null</c> at the end of an array.</returns>
<returns>A <see cref="T:System.Nullable`1"/>. This method will return <c>null</c> at the end of an array.</returns>
</member>
<member name="M:Newtonsoft.Json.Linq.JTokenReader.Read">
<summary>
@@ -6074,6 +6114,11 @@
The property must be defined in JSON and cannot be a null value.
</summary>
</member>
<member name="F:Newtonsoft.Json.Required.DisallowNull">
<summary>
The property is not required but it cannot be a null value.
</summary>
</member>
<member name="T:Newtonsoft.Json.PreserveReferencesHandling">
<summary>
Specifies reference handling options for the <see cref="T:Newtonsoft.Json.JsonSerializer"/>.
@@ -6620,7 +6665,7 @@
<summary>
Reads the next JSON token from the stream as a <see cref="T:System.Nullable`1"/>.
</summary>
<returns>A <see cref="T:System.String"/>. This method will return <c>null</c> at the end of an array.</returns>
<returns>A <see cref="T:System.Nullable`1"/>. This method will return <c>null</c> at the end of an array.</returns>
</member>
<member name="M:Newtonsoft.Json.JsonValidatingReader.Read">
<summary>
@@ -6685,6 +6730,11 @@
</summary>
<param name="reader">The <c>TextReader</c> containing the XML data to read.</param>
</member>
<member name="P:Newtonsoft.Json.JsonTextReader.BufferPool">
<summary>
Gets or sets the reader's character buffer pool.
</summary>
</member>
<member name="M:Newtonsoft.Json.JsonTextReader.Read">
<summary>
Reads the next JSON token from the stream.
@@ -6723,7 +6773,7 @@
<summary>
Reads the next JSON token from the stream as a <see cref="T:System.Nullable`1"/>.
</summary>
<returns>A <see cref="T:System.String"/>. This method will return <c>null</c> at the end of an array.</returns>
<returns>A <see cref="T:System.Nullable`1"/>. This method will return <c>null</c> at the end of an array.</returns>
</member>
<member name="M:Newtonsoft.Json.JsonTextReader.Close">
<summary>
@@ -6871,6 +6921,11 @@
Represents a writer that provides a fast, non-cached, forward-only way of generating JSON data.
</summary>
</member>
<member name="P:Newtonsoft.Json.JsonTextWriter.BufferPool">
<summary>
Gets or sets the writer's character buffer pool.
</summary>
</member>
<member name="P:Newtonsoft.Json.JsonTextWriter.Indentation">
<summary>
Gets or sets how many IndentChars to write for each level in the hierarchy when <see cref="T:Newtonsoft.Json.Formatting"/> is set to <c>Formatting.Indented</c>.
@@ -7470,7 +7525,7 @@
<summary>
Reads the next JSON token from the stream as a <see cref="T:System.Nullable`1"/>.
</summary>
<returns>A <see cref="T:System.String"/>. This method will return <c>null</c> at the end of an array.</returns>
<returns>A <see cref="T:System.Nullable`1"/>. This method will return <c>null</c> at the end of an array.</returns>
</member>
<member name="M:Newtonsoft.Json.JsonReader.Skip">
<summary>

View File

@@ -89,7 +89,7 @@
<summary>
Reads the next JSON token from the stream as a <see cref="T:System.Nullable`1"/>.
</summary>
<returns>A <see cref="T:System.String"/>. This method will return <c>null</c> at the end of an array.</returns>
<returns>A <see cref="T:System.Nullable`1"/>. This method will return <c>null</c> at the end of an array.</returns>
</member>
<member name="M:Newtonsoft.Json.Bson.BsonReader.ReadAsDateTimeOffset">
<summary>
@@ -977,6 +977,25 @@
Causes child objects to be indented according to the <see cref="P:Newtonsoft.Json.JsonTextWriter.Indentation"/> and <see cref="P:Newtonsoft.Json.JsonTextWriter.IndentChar"/> settings.
</summary>
</member>
<member name="T:Newtonsoft.Json.IJsonBufferPool`1">
<summary>
Provides an interface for using pooled buffers.
</summary>
<typeparam name="T">The buffer type content.</typeparam>
</member>
<member name="M:Newtonsoft.Json.IJsonBufferPool`1.RentBuffer(System.Int32)">
<summary>
Rent a buffer from the pool. This buffer must be returned when it is no longer needed.
</summary>
<param name="minSize">The minimum required size of the buffer. The returned buffer may be larger.</param>
<returns>The rented buffer from the pool.</returns>
</member>
<member name="M:Newtonsoft.Json.IJsonBufferPool`1.ReturnBuffer(`0[]@)">
<summary>
Return a buffer to the pool.
</summary>
<param name="buffer">The buffer that is being returned.</param>
</member>
<member name="T:Newtonsoft.Json.JsonConstructorAttribute">
<summary>
Instructs the <see cref="T:Newtonsoft.Json.JsonSerializer"/> to use the specified constructor when deserializing that object.
@@ -1079,6 +1098,21 @@
Load comments as a <see cref="T:Newtonsoft.Json.Linq.JValue"/> with type <see cref="F:Newtonsoft.Json.Linq.JTokenType.Comment"/>.
</summary>
</member>
<member name="T:Newtonsoft.Json.Linq.LineInfoHandling">
<summary>
Specifies how line information is handled when loading JSON.
</summary>
</member>
<member name="F:Newtonsoft.Json.Linq.LineInfoHandling.Ignore">
<summary>
Ignore line information.
</summary>
</member>
<member name="F:Newtonsoft.Json.Linq.LineInfoHandling.Load">
<summary>
Load line information.
</summary>
</member>
<member name="T:Newtonsoft.Json.Linq.JPropertyDescriptor">
<summary>
Represents a view of a <see cref="T:Newtonsoft.Json.Linq.JProperty"/>.
@@ -1179,6 +1213,12 @@
</summary>
<value>The JSON comment handling.</value>
</member>
<member name="P:Newtonsoft.Json.Linq.JsonLoadSettings.LineInfoHandling">
<summary>
Gets or sets how JSON line info is handled when loading JSON.
</summary>
<value>The JSON line info handling.</value>
</member>
<member name="T:Newtonsoft.Json.Linq.JsonMergeSettings">
<summary>
Specifies the settings used when merging JSON.
@@ -2229,7 +2269,7 @@
<summary>
Reads the next JSON token from the stream as a <see cref="T:System.Nullable`1"/>.
</summary>
<returns>A <see cref="T:System.String"/>. This method will return <c>null</c> at the end of an array.</returns>
<returns>A <see cref="T:System.Nullable`1"/>. This method will return <c>null</c> at the end of an array.</returns>
</member>
<member name="M:Newtonsoft.Json.Linq.JTokenReader.ReadAsDateTimeOffset">
<summary>
@@ -5124,6 +5164,11 @@
The property must be defined in JSON and cannot be a null value.
</summary>
</member>
<member name="F:Newtonsoft.Json.Required.DisallowNull">
<summary>
The property is not required but it cannot be a null value.
</summary>
</member>
<member name="T:Newtonsoft.Json.PreserveReferencesHandling">
<summary>
Specifies reference handling options for the <see cref="T:Newtonsoft.Json.JsonSerializer"/>.
@@ -5670,7 +5715,7 @@
<summary>
Reads the next JSON token from the stream as a <see cref="T:System.Nullable`1"/>.
</summary>
<returns>A <see cref="T:System.String"/>. This method will return <c>null</c> at the end of an array.</returns>
<returns>A <see cref="T:System.Nullable`1"/>. This method will return <c>null</c> at the end of an array.</returns>
</member>
<member name="M:Newtonsoft.Json.JsonValidatingReader.ReadAsDateTimeOffset">
<summary>
@@ -5741,6 +5786,11 @@
</summary>
<param name="reader">The <c>TextReader</c> containing the XML data to read.</param>
</member>
<member name="P:Newtonsoft.Json.JsonTextReader.BufferPool">
<summary>
Gets or sets the reader's character buffer pool.
</summary>
</member>
<member name="M:Newtonsoft.Json.JsonTextReader.Read">
<summary>
Reads the next JSON token from the stream.
@@ -5779,7 +5829,7 @@
<summary>
Reads the next JSON token from the stream as a <see cref="T:System.Nullable`1"/>.
</summary>
<returns>A <see cref="T:System.String"/>. This method will return <c>null</c> at the end of an array.</returns>
<returns>A <see cref="T:System.Nullable`1"/>. This method will return <c>null</c> at the end of an array.</returns>
</member>
<member name="M:Newtonsoft.Json.JsonTextReader.ReadAsDateTimeOffset">
<summary>
@@ -5933,6 +5983,11 @@
Represents a writer that provides a fast, non-cached, forward-only way of generating JSON data.
</summary>
</member>
<member name="P:Newtonsoft.Json.JsonTextWriter.BufferPool">
<summary>
Gets or sets the writer's character buffer pool.
</summary>
</member>
<member name="P:Newtonsoft.Json.JsonTextWriter.Indentation">
<summary>
Gets or sets how many IndentChars to write for each level in the hierarchy when <see cref="T:Newtonsoft.Json.Formatting"/> is set to <c>Formatting.Indented</c>.
@@ -6538,7 +6593,7 @@
<summary>
Reads the next JSON token from the stream as a <see cref="T:System.Nullable`1"/>.
</summary>
<returns>A <see cref="T:System.String"/>. This method will return <c>null</c> at the end of an array.</returns>
<returns>A <see cref="T:System.Nullable`1"/>. This method will return <c>null</c> at the end of an array.</returns>
</member>
<member name="M:Newtonsoft.Json.JsonReader.ReadAsDateTimeOffset">
<summary>

View File

@@ -89,7 +89,7 @@
<summary>
Reads the next JSON token from the stream as a <see cref="T:System.Nullable`1"/>.
</summary>
<returns>A <see cref="T:System.String"/>. This method will return <c>null</c> at the end of an array.</returns>
<returns>A <see cref="T:System.Nullable`1"/>. This method will return <c>null</c> at the end of an array.</returns>
</member>
<member name="M:Newtonsoft.Json.Bson.BsonReader.ReadAsDateTimeOffset">
<summary>
@@ -973,6 +973,25 @@
Floating point numbers are parsed to <see cref="F:Newtonsoft.Json.FloatParseHandling.Decimal"/>.
</summary>
</member>
<member name="T:Newtonsoft.Json.IJsonBufferPool`1">
<summary>
Provides an interface for using pooled buffers.
</summary>
<typeparam name="T">The buffer type content.</typeparam>
</member>
<member name="M:Newtonsoft.Json.IJsonBufferPool`1.RentBuffer(System.Int32)">
<summary>
Rent a buffer from the pool. This buffer must be returned when it is no longer needed.
</summary>
<param name="minSize">The minimum required size of the buffer. The returned buffer may be larger.</param>
<returns>The rented buffer from the pool.</returns>
</member>
<member name="M:Newtonsoft.Json.IJsonBufferPool`1.ReturnBuffer(`0[]@)">
<summary>
Return a buffer to the pool.
</summary>
<param name="buffer">The buffer that is being returned.</param>
</member>
<member name="T:Newtonsoft.Json.JsonDictionaryAttribute">
<summary>
Instructs the <see cref="T:Newtonsoft.Json.JsonSerializer"/> how to serialize the collection.
@@ -1151,6 +1170,21 @@
Load comments as a <see cref="T:Newtonsoft.Json.Linq.JValue"/> with type <see cref="F:Newtonsoft.Json.Linq.JTokenType.Comment"/>.
</summary>
</member>
<member name="T:Newtonsoft.Json.Linq.LineInfoHandling">
<summary>
Specifies how line information is handled when loading JSON.
</summary>
</member>
<member name="F:Newtonsoft.Json.Linq.LineInfoHandling.Ignore">
<summary>
Ignore line information.
</summary>
</member>
<member name="F:Newtonsoft.Json.Linq.LineInfoHandling.Load">
<summary>
Load line information.
</summary>
</member>
<member name="T:Newtonsoft.Json.Linq.JsonLoadSettings">
<summary>
Specifies the settings used when loading JSON.
@@ -1162,6 +1196,12 @@
</summary>
<value>The JSON comment handling.</value>
</member>
<member name="P:Newtonsoft.Json.Linq.JsonLoadSettings.LineInfoHandling">
<summary>
Gets or sets how JSON line info is handled when loading JSON.
</summary>
<value>The JSON line info handling.</value>
</member>
<member name="T:Newtonsoft.Json.Linq.JsonMergeSettings">
<summary>
Specifies the settings used when merging JSON.
@@ -2321,7 +2361,7 @@
<summary>
Reads the next JSON token from the stream as a <see cref="T:System.Nullable`1"/>.
</summary>
<returns>A <see cref="T:System.String"/>. This method will return <c>null</c> at the end of an array.</returns>
<returns>A <see cref="T:System.Nullable`1"/>. This method will return <c>null</c> at the end of an array.</returns>
</member>
<member name="M:Newtonsoft.Json.Linq.JTokenReader.ReadAsDateTimeOffset">
<summary>
@@ -5332,6 +5372,11 @@
The property must be defined in JSON and cannot be a null value.
</summary>
</member>
<member name="F:Newtonsoft.Json.Required.DisallowNull">
<summary>
The property is not required but it cannot be a null value.
</summary>
</member>
<member name="T:Newtonsoft.Json.PreserveReferencesHandling">
<summary>
Specifies reference handling options for the <see cref="T:Newtonsoft.Json.JsonSerializer"/>.
@@ -5878,7 +5923,7 @@
<summary>
Reads the next JSON token from the stream as a <see cref="T:System.Nullable`1"/>.
</summary>
<returns>A <see cref="T:System.String"/>. This method will return <c>null</c> at the end of an array.</returns>
<returns>A <see cref="T:System.Nullable`1"/>. This method will return <c>null</c> at the end of an array.</returns>
</member>
<member name="M:Newtonsoft.Json.JsonValidatingReader.ReadAsDateTimeOffset">
<summary>
@@ -5949,6 +5994,11 @@
</summary>
<param name="reader">The <c>TextReader</c> containing the XML data to read.</param>
</member>
<member name="P:Newtonsoft.Json.JsonTextReader.BufferPool">
<summary>
Gets or sets the reader's character buffer pool.
</summary>
</member>
<member name="M:Newtonsoft.Json.JsonTextReader.Read">
<summary>
Reads the next JSON token from the stream.
@@ -5987,7 +6037,7 @@
<summary>
Reads the next JSON token from the stream as a <see cref="T:System.Nullable`1"/>.
</summary>
<returns>A <see cref="T:System.String"/>. This method will return <c>null</c> at the end of an array.</returns>
<returns>A <see cref="T:System.Nullable`1"/>. This method will return <c>null</c> at the end of an array.</returns>
</member>
<member name="M:Newtonsoft.Json.JsonTextReader.ReadAsDateTimeOffset">
<summary>
@@ -6141,6 +6191,11 @@
Represents a writer that provides a fast, non-cached, forward-only way of generating JSON data.
</summary>
</member>
<member name="P:Newtonsoft.Json.JsonTextWriter.BufferPool">
<summary>
Gets or sets the writer's character buffer pool.
</summary>
</member>
<member name="P:Newtonsoft.Json.JsonTextWriter.Indentation">
<summary>
Gets or sets how many IndentChars to write for each level in the hierarchy when <see cref="T:Newtonsoft.Json.Formatting"/> is set to <c>Formatting.Indented</c>.
@@ -6746,7 +6801,7 @@
<summary>
Reads the next JSON token from the stream as a <see cref="T:System.Nullable`1"/>.
</summary>
<returns>A <see cref="T:System.String"/>. This method will return <c>null</c> at the end of an array.</returns>
<returns>A <see cref="T:System.Nullable`1"/>. This method will return <c>null</c> at the end of an array.</returns>
</member>
<member name="M:Newtonsoft.Json.JsonReader.ReadAsDateTimeOffset">
<summary>

View File

@@ -106,7 +106,7 @@
<summary>
Reads the next JSON token from the stream as a <see cref="T:System.Nullable`1"/>.
</summary>
<returns>A <see cref="T:System.String"/>. This method will return <c>null</c> at the end of an array.</returns>
<returns>A <see cref="T:System.Nullable`1"/>. This method will return <c>null</c> at the end of an array.</returns>
</member>
<member name="M:Newtonsoft.Json.Bson.BsonReader.ReadAsDateTimeOffset">
<summary>
@@ -1084,6 +1084,25 @@
Causes child objects to be indented according to the <see cref="P:Newtonsoft.Json.JsonTextWriter.Indentation"/> and <see cref="P:Newtonsoft.Json.JsonTextWriter.IndentChar"/> settings.
</summary>
</member>
<member name="T:Newtonsoft.Json.IJsonBufferPool`1">
<summary>
Provides an interface for using pooled buffers.
</summary>
<typeparam name="T">The buffer type content.</typeparam>
</member>
<member name="M:Newtonsoft.Json.IJsonBufferPool`1.RentBuffer(System.Int32)">
<summary>
Rent a buffer from the pool. This buffer must be returned when it is no longer needed.
</summary>
<param name="minSize">The minimum required size of the buffer. The returned buffer may be larger.</param>
<returns>The rented buffer from the pool.</returns>
</member>
<member name="M:Newtonsoft.Json.IJsonBufferPool`1.ReturnBuffer(`0[]@)">
<summary>
Return a buffer to the pool.
</summary>
<param name="buffer">The buffer that is being returned.</param>
</member>
<member name="T:Newtonsoft.Json.IJsonLineInfo">
<summary>
Provides an interface to enable a class to return line and position information.
@@ -2386,7 +2405,7 @@
<summary>
Reads the next JSON token from the stream as a <see cref="T:System.Nullable`1"/>.
</summary>
<returns>A <see cref="T:System.String"/>. This method will return <c>null</c> at the end of an array.</returns>
<returns>A <see cref="T:System.Nullable`1"/>. This method will return <c>null</c> at the end of an array.</returns>
</member>
<member name="M:Newtonsoft.Json.JsonReader.ReadAsDateTimeOffset">
<summary>
@@ -3030,6 +3049,11 @@
</summary>
<param name="reader">The <c>TextReader</c> containing the XML data to read.</param>
</member>
<member name="P:Newtonsoft.Json.JsonTextReader.BufferPool">
<summary>
Gets or sets the reader's character buffer pool.
</summary>
</member>
<member name="M:Newtonsoft.Json.JsonTextReader.Read">
<summary>
Reads the next JSON token from the stream.
@@ -3068,7 +3092,7 @@
<summary>
Reads the next JSON token from the stream as a <see cref="T:System.Nullable`1"/>.
</summary>
<returns>A <see cref="T:System.String"/>. This method will return <c>null</c> at the end of an array.</returns>
<returns>A <see cref="T:System.Nullable`1"/>. This method will return <c>null</c> at the end of an array.</returns>
</member>
<member name="M:Newtonsoft.Json.JsonTextReader.ReadAsDateTimeOffset">
<summary>
@@ -3110,6 +3134,11 @@
Represents a writer that provides a fast, non-cached, forward-only way of generating JSON data.
</summary>
</member>
<member name="P:Newtonsoft.Json.JsonTextWriter.BufferPool">
<summary>
Gets or sets the writer's character buffer pool.
</summary>
</member>
<member name="P:Newtonsoft.Json.JsonTextWriter.Indentation">
<summary>
Gets or sets how many IndentChars to write for each level in the hierarchy when <see cref="T:Newtonsoft.Json.Formatting"/> is set to <c>Formatting.Indented</c>.
@@ -3557,7 +3586,7 @@
<summary>
Reads the next JSON token from the stream as a <see cref="T:System.Nullable`1"/>.
</summary>
<returns>A <see cref="T:System.String"/>. This method will return <c>null</c> at the end of an array.</returns>
<returns>A <see cref="T:System.Nullable`1"/>. This method will return <c>null</c> at the end of an array.</returns>
</member>
<member name="M:Newtonsoft.Json.JsonValidatingReader.ReadAsDateTimeOffset">
<summary>
@@ -4080,6 +4109,21 @@
Load comments as a <see cref="T:Newtonsoft.Json.Linq.JValue"/> with type <see cref="F:Newtonsoft.Json.Linq.JTokenType.Comment"/>.
</summary>
</member>
<member name="T:Newtonsoft.Json.Linq.LineInfoHandling">
<summary>
Specifies how line information is handled when loading JSON.
</summary>
</member>
<member name="F:Newtonsoft.Json.Linq.LineInfoHandling.Ignore">
<summary>
Ignore line information.
</summary>
</member>
<member name="F:Newtonsoft.Json.Linq.LineInfoHandling.Load">
<summary>
Load line information.
</summary>
</member>
<member name="T:Newtonsoft.Json.Linq.Extensions">
<summary>
Contains the LINQ to JSON extension methods.
@@ -6199,7 +6243,7 @@
<summary>
Reads the next JSON token from the stream as a <see cref="T:System.Nullable`1"/>.
</summary>
<returns>A <see cref="T:System.String"/>. This method will return <c>null</c> at the end of an array.</returns>
<returns>A <see cref="T:System.Nullable`1"/>. This method will return <c>null</c> at the end of an array.</returns>
</member>
<member name="M:Newtonsoft.Json.Linq.JTokenReader.ReadAsDateTimeOffset">
<summary>
@@ -6782,6 +6826,12 @@
</summary>
<value>The JSON comment handling.</value>
</member>
<member name="P:Newtonsoft.Json.Linq.JsonLoadSettings.LineInfoHandling">
<summary>
Gets or sets how JSON line info is handled when loading JSON.
</summary>
<value>The JSON line info handling.</value>
</member>
<member name="T:Newtonsoft.Json.Linq.MergeArrayHandling">
<summary>
Specifies how JSON arrays are merged together.
@@ -6966,6 +7016,11 @@
The property must be defined in JSON and cannot be a null value.
</summary>
</member>
<member name="F:Newtonsoft.Json.Required.DisallowNull">
<summary>
The property is not required but it cannot be a null value.
</summary>
</member>
<member name="T:Newtonsoft.Json.Schema.Extensions">
<summary>
<para>

View File

@@ -106,7 +106,7 @@
<summary>
Reads the next JSON token from the stream as a <see cref="T:System.Nullable`1"/>.
</summary>
<returns>A <see cref="T:System.String"/>. This method will return <c>null</c> at the end of an array.</returns>
<returns>A <see cref="T:System.Nullable`1"/>. This method will return <c>null</c> at the end of an array.</returns>
</member>
<member name="M:Newtonsoft.Json.Bson.BsonReader.ReadAsDateTimeOffset">
<summary>
@@ -857,6 +857,25 @@
Causes child objects to be indented according to the <see cref="P:Newtonsoft.Json.JsonTextWriter.Indentation"/> and <see cref="P:Newtonsoft.Json.JsonTextWriter.IndentChar"/> settings.
</summary>
</member>
<member name="T:Newtonsoft.Json.IJsonBufferPool`1">
<summary>
Provides an interface for using pooled buffers.
</summary>
<typeparam name="T">The buffer type content.</typeparam>
</member>
<member name="M:Newtonsoft.Json.IJsonBufferPool`1.RentBuffer(System.Int32)">
<summary>
Rent a buffer from the pool. This buffer must be returned when it is no longer needed.
</summary>
<param name="minSize">The minimum required size of the buffer. The returned buffer may be larger.</param>
<returns>The rented buffer from the pool.</returns>
</member>
<member name="M:Newtonsoft.Json.IJsonBufferPool`1.ReturnBuffer(`0[]@)">
<summary>
Return a buffer to the pool.
</summary>
<param name="buffer">The buffer that is being returned.</param>
</member>
<member name="T:Newtonsoft.Json.IJsonLineInfo">
<summary>
Provides an interface to enable a class to return line and position information.
@@ -1947,7 +1966,7 @@
<summary>
Reads the next JSON token from the stream as a <see cref="T:System.Nullable`1"/>.
</summary>
<returns>A <see cref="T:System.String"/>. This method will return <c>null</c> at the end of an array.</returns>
<returns>A <see cref="T:System.Nullable`1"/>. This method will return <c>null</c> at the end of an array.</returns>
</member>
<member name="M:Newtonsoft.Json.JsonReader.ReadAsDateTimeOffset">
<summary>
@@ -2573,6 +2592,11 @@
</summary>
<param name="reader">The <c>TextReader</c> containing the XML data to read.</param>
</member>
<member name="P:Newtonsoft.Json.JsonTextReader.BufferPool">
<summary>
Gets or sets the reader's character buffer pool.
</summary>
</member>
<member name="M:Newtonsoft.Json.JsonTextReader.Read">
<summary>
Reads the next JSON token from the stream.
@@ -2611,7 +2635,7 @@
<summary>
Reads the next JSON token from the stream as a <see cref="T:System.Nullable`1"/>.
</summary>
<returns>A <see cref="T:System.String"/>. This method will return <c>null</c> at the end of an array.</returns>
<returns>A <see cref="T:System.Nullable`1"/>. This method will return <c>null</c> at the end of an array.</returns>
</member>
<member name="M:Newtonsoft.Json.JsonTextReader.ReadAsDateTimeOffset">
<summary>
@@ -2653,6 +2677,11 @@
Represents a writer that provides a fast, non-cached, forward-only way of generating JSON data.
</summary>
</member>
<member name="P:Newtonsoft.Json.JsonTextWriter.BufferPool">
<summary>
Gets or sets the writer's character buffer pool.
</summary>
</member>
<member name="P:Newtonsoft.Json.JsonTextWriter.Indentation">
<summary>
Gets or sets how many IndentChars to write for each level in the hierarchy when <see cref="T:Newtonsoft.Json.Formatting"/> is set to <c>Formatting.Indented</c>.
@@ -3100,7 +3129,7 @@
<summary>
Reads the next JSON token from the stream as a <see cref="T:System.Nullable`1"/>.
</summary>
<returns>A <see cref="T:System.String"/>. This method will return <c>null</c> at the end of an array.</returns>
<returns>A <see cref="T:System.Nullable`1"/>. This method will return <c>null</c> at the end of an array.</returns>
</member>
<member name="M:Newtonsoft.Json.JsonValidatingReader.ReadAsDateTimeOffset">
<summary>
@@ -3614,6 +3643,21 @@
Load comments as a <see cref="T:Newtonsoft.Json.Linq.JValue"/> with type <see cref="F:Newtonsoft.Json.Linq.JTokenType.Comment"/>.
</summary>
</member>
<member name="T:Newtonsoft.Json.Linq.LineInfoHandling">
<summary>
Specifies how line information is handled when loading JSON.
</summary>
</member>
<member name="F:Newtonsoft.Json.Linq.LineInfoHandling.Ignore">
<summary>
Ignore line information.
</summary>
</member>
<member name="F:Newtonsoft.Json.Linq.LineInfoHandling.Load">
<summary>
Load line information.
</summary>
</member>
<member name="T:Newtonsoft.Json.Linq.Extensions">
<summary>
Contains the LINQ to JSON extension methods.
@@ -4517,6 +4561,12 @@
</summary>
<value>The JSON comment handling.</value>
</member>
<member name="P:Newtonsoft.Json.Linq.JsonLoadSettings.LineInfoHandling">
<summary>
Gets or sets how JSON line info is handled when loading JSON.
</summary>
<value>The JSON line info handling.</value>
</member>
<member name="T:Newtonsoft.Json.Linq.JsonMergeSettings">
<summary>
Specifies the settings used when merging JSON.
@@ -5484,7 +5534,7 @@
<summary>
Reads the next JSON token from the stream as a <see cref="T:System.Nullable`1"/>.
</summary>
<returns>A <see cref="T:System.String"/>. This method will return <c>null</c> at the end of an array.</returns>
<returns>A <see cref="T:System.Nullable`1"/>. This method will return <c>null</c> at the end of an array.</returns>
</member>
<member name="M:Newtonsoft.Json.Linq.JTokenReader.ReadAsDateTimeOffset">
<summary>
@@ -6231,6 +6281,11 @@
The property must be defined in JSON and cannot be a null value.
</summary>
</member>
<member name="F:Newtonsoft.Json.Required.DisallowNull">
<summary>
The property is not required but it cannot be a null value.
</summary>
</member>
<member name="T:Newtonsoft.Json.Schema.Extensions">
<summary>
<para>

View File

@@ -106,7 +106,7 @@
<summary>
Reads the next JSON token from the stream as a <see cref="T:System.Nullable`1"/>.
</summary>
<returns>A <see cref="T:System.String"/>. This method will return <c>null</c> at the end of an array.</returns>
<returns>A <see cref="T:System.Nullable`1"/>. This method will return <c>null</c> at the end of an array.</returns>
</member>
<member name="M:Newtonsoft.Json.Bson.BsonReader.ReadAsDateTimeOffset">
<summary>
@@ -956,6 +956,25 @@
Causes child objects to be indented according to the <see cref="P:Newtonsoft.Json.JsonTextWriter.Indentation"/> and <see cref="P:Newtonsoft.Json.JsonTextWriter.IndentChar"/> settings.
</summary>
</member>
<member name="T:Newtonsoft.Json.IJsonBufferPool`1">
<summary>
Provides an interface for using pooled buffers.
</summary>
<typeparam name="T">The buffer type content.</typeparam>
</member>
<member name="M:Newtonsoft.Json.IJsonBufferPool`1.RentBuffer(System.Int32)">
<summary>
Rent a buffer from the pool. This buffer must be returned when it is no longer needed.
</summary>
<param name="minSize">The minimum required size of the buffer. The returned buffer may be larger.</param>
<returns>The rented buffer from the pool.</returns>
</member>
<member name="M:Newtonsoft.Json.IJsonBufferPool`1.ReturnBuffer(`0[]@)">
<summary>
Return a buffer to the pool.
</summary>
<param name="buffer">The buffer that is being returned.</param>
</member>
<member name="T:Newtonsoft.Json.IJsonLineInfo">
<summary>
Provides an interface to enable a class to return line and position information.
@@ -2197,7 +2216,7 @@
<summary>
Reads the next JSON token from the stream as a <see cref="T:System.Nullable`1"/>.
</summary>
<returns>A <see cref="T:System.String"/>. This method will return <c>null</c> at the end of an array.</returns>
<returns>A <see cref="T:System.Nullable`1"/>. This method will return <c>null</c> at the end of an array.</returns>
</member>
<member name="M:Newtonsoft.Json.JsonReader.ReadAsDateTimeOffset">
<summary>
@@ -2823,6 +2842,11 @@
</summary>
<param name="reader">The <c>TextReader</c> containing the XML data to read.</param>
</member>
<member name="P:Newtonsoft.Json.JsonTextReader.BufferPool">
<summary>
Gets or sets the reader's character buffer pool.
</summary>
</member>
<member name="M:Newtonsoft.Json.JsonTextReader.Read">
<summary>
Reads the next JSON token from the stream.
@@ -2861,7 +2885,7 @@
<summary>
Reads the next JSON token from the stream as a <see cref="T:System.Nullable`1"/>.
</summary>
<returns>A <see cref="T:System.String"/>. This method will return <c>null</c> at the end of an array.</returns>
<returns>A <see cref="T:System.Nullable`1"/>. This method will return <c>null</c> at the end of an array.</returns>
</member>
<member name="M:Newtonsoft.Json.JsonTextReader.ReadAsDateTimeOffset">
<summary>
@@ -2903,6 +2927,11 @@
Represents a writer that provides a fast, non-cached, forward-only way of generating JSON data.
</summary>
</member>
<member name="P:Newtonsoft.Json.JsonTextWriter.BufferPool">
<summary>
Gets or sets the writer's character buffer pool.
</summary>
</member>
<member name="P:Newtonsoft.Json.JsonTextWriter.Indentation">
<summary>
Gets or sets how many IndentChars to write for each level in the hierarchy when <see cref="T:Newtonsoft.Json.Formatting"/> is set to <c>Formatting.Indented</c>.
@@ -3350,7 +3379,7 @@
<summary>
Reads the next JSON token from the stream as a <see cref="T:System.Nullable`1"/>.
</summary>
<returns>A <see cref="T:System.String"/>. This method will return <c>null</c> at the end of an array.</returns>
<returns>A <see cref="T:System.Nullable`1"/>. This method will return <c>null</c> at the end of an array.</returns>
</member>
<member name="M:Newtonsoft.Json.JsonValidatingReader.ReadAsDateTimeOffset">
<summary>
@@ -3864,6 +3893,21 @@
Load comments as a <see cref="T:Newtonsoft.Json.Linq.JValue"/> with type <see cref="F:Newtonsoft.Json.Linq.JTokenType.Comment"/>.
</summary>
</member>
<member name="T:Newtonsoft.Json.Linq.LineInfoHandling">
<summary>
Specifies how line information is handled when loading JSON.
</summary>
</member>
<member name="F:Newtonsoft.Json.Linq.LineInfoHandling.Ignore">
<summary>
Ignore line information.
</summary>
</member>
<member name="F:Newtonsoft.Json.Linq.LineInfoHandling.Load">
<summary>
Load line information.
</summary>
</member>
<member name="T:Newtonsoft.Json.Linq.Extensions">
<summary>
Contains the LINQ to JSON extension methods.
@@ -4787,6 +4831,12 @@
</summary>
<value>The JSON comment handling.</value>
</member>
<member name="P:Newtonsoft.Json.Linq.JsonLoadSettings.LineInfoHandling">
<summary>
Gets or sets how JSON line info is handled when loading JSON.
</summary>
<value>The JSON line info handling.</value>
</member>
<member name="T:Newtonsoft.Json.Linq.JsonMergeSettings">
<summary>
Specifies the settings used when merging JSON.
@@ -5772,7 +5822,7 @@
<summary>
Reads the next JSON token from the stream as a <see cref="T:System.Nullable`1"/>.
</summary>
<returns>A <see cref="T:System.String"/>. This method will return <c>null</c> at the end of an array.</returns>
<returns>A <see cref="T:System.Nullable`1"/>. This method will return <c>null</c> at the end of an array.</returns>
</member>
<member name="M:Newtonsoft.Json.Linq.JTokenReader.ReadAsDateTimeOffset">
<summary>
@@ -6528,6 +6578,11 @@
The property must be defined in JSON and cannot be a null value.
</summary>
</member>
<member name="F:Newtonsoft.Json.Required.DisallowNull">
<summary>
The property is not required but it cannot be a null value.
</summary>
</member>
<member name="T:Newtonsoft.Json.Schema.Extensions">
<summary>
<para>