Files
ArchiSteamFarm/ArchiSteamFarm/Bot.cs

651 lines
18 KiB
C#
Raw Normal View History

2015-10-28 19:21:27 +01:00
/*
_ _ _ ____ _ _____
/ \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
/ _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
/ ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
/_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
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 SteamKit2;
2015-10-25 06:16:50 +01:00
using System;
2015-11-18 14:28:46 +01:00
using System.Collections.Concurrent;
2015-10-25 06:16:50 +01:00
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Threading.Tasks;
using System.Xml;
namespace ArchiSteamFarm {
2015-11-27 16:25:03 +01:00
internal sealed class Bot {
2015-10-28 19:21:27 +01:00
private const ushort CallbackSleep = 500; // In miliseconds
2015-10-25 06:16:50 +01:00
2015-11-18 14:28:46 +01:00
private static readonly ConcurrentDictionary<string, Bot> Bots = new ConcurrentDictionary<string, Bot>();
2015-10-25 06:16:50 +01:00
2015-11-20 14:47:16 +01:00
private readonly string ConfigFile, SentryFile;
internal readonly string BotName;
2015-10-25 06:16:50 +01:00
private bool IsRunning = false;
2015-10-25 06:16:50 +01:00
private string AuthCode, 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; }
2015-10-25 06:16:50 +01:00
internal SteamClient SteamClient { get; private set; }
internal SteamFriends SteamFriends { get; private set; }
internal SteamUser SteamUser { get; private set; }
internal Trading Trading { get; private set; }
// Config variables
2015-10-31 09:17:56 +01:00
internal bool Enabled { get; private set; } = false;
internal string SteamLogin { get; private set; } = "null";
internal string SteamPassword { get; private set; } = "null";
internal string SteamNickname { get; private set; } = "null";
internal string SteamApiKey { get; private set; } = "null";
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;
2015-10-31 06:09:03 +01:00
internal bool ShutdownOnFarmingFinished { get; private set; } = false;
internal HashSet<uint> Blacklist { get; private set; } = new HashSet<uint> { 303700, 335590, 368020 };
internal bool Statistics { get; private set; } = true;
2015-11-25 16:49:01 +01:00
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() {
2015-11-18 14:28:46 +01:00
return Bots.Count;
2015-11-18 14:30:05 +01:00
}
2015-10-31 05:27:30 +01:00
internal static async Task ShutdownAllBots() {
List<Task> tasks = new List<Task>();
2015-11-18 14:28:46 +01:00
foreach (Bot bot in Bots.Values) {
tasks.Add(Task.Run(async () => await bot.Shutdown().ConfigureAwait(false)));
}
2015-10-31 05:27:30 +01:00
await Task.WhenAll(tasks).ConfigureAwait(false);
}
2015-10-25 06:16:50 +01:00
2015-10-28 19:21:27 +01:00
internal Bot(string botName) {
2015-11-01 02:04:44 +01:00
if (Bots.ContainsKey(botName)) {
return;
}
2015-10-25 06:16:50 +01:00
BotName = botName;
ConfigFile = Path.Combine(Program.ConfigDirectoryPath, BotName + ".xml");
SentryFile = Path.Combine(Program.ConfigDirectoryPath, BotName + ".bin");
2015-11-01 02:04:44 +01:00
if (!ReadConfig()) {
return;
}
2015-10-25 06:16:50 +01:00
if (!Enabled) {
return;
}
2015-10-28 19:21:27 +01:00
2015-11-18 14:28:46 +01:00
Bots.AddOrUpdate(BotName, this, (key, value) => this);
// Initialize
SteamClient = new SteamClient();
ArchiHandler = new ArchiHandler();
SteamClient.AddHandler(ArchiHandler);
CallbackManager = new CallbackManager(SteamClient);
CallbackManager.Subscribe<SteamClient.ConnectedCallback>(OnConnected);
CallbackManager.Subscribe<SteamClient.DisconnectedCallback>(OnDisconnected);
SteamFriends = SteamClient.GetHandler<SteamFriends>();
CallbackManager.Subscribe<SteamFriends.FriendsListCallback>(OnFriendsList);
CallbackManager.Subscribe<SteamFriends.FriendMsgCallback>(OnFriendMsg);
SteamUser = SteamClient.GetHandler<SteamUser>();
CallbackManager.Subscribe<SteamUser.AccountInfoCallback>(OnAccountInfo);
CallbackManager.Subscribe<SteamUser.LoggedOffCallback>(OnLoggedOff);
CallbackManager.Subscribe<SteamUser.LoggedOnCallback>(OnLoggedOn);
CallbackManager.Subscribe<SteamUser.UpdateMachineAuthCallback>(OnMachineAuth);
CallbackManager.Subscribe<ArchiHandler.NotificationCallback>(OnNotification);
CallbackManager.Subscribe<ArchiHandler.PurchaseResponseCallback>(OnPurchaseResponse);
ArchiWebHandler = new ArchiWebHandler(this, SteamApiKey);
CardsFarmer = new CardsFarmer(this);
Trading = new Trading(this);
// Start
var fireAndForget = Task.Run(async () => await Start().ConfigureAwait(false));
2015-10-25 06:16:50 +01:00
}
2015-11-01 02:04:44 +01:00
private bool ReadConfig() {
if (!File.Exists(ConfigFile)) {
return false;
}
2015-11-04 04:51:52 +01:00
try {
using (XmlReader reader = XmlReader.Create(ConfigFile)) {
while (reader.Read()) {
if (reader.NodeType != XmlNodeType.Element) {
continue;
}
string key = reader.Name;
if (string.IsNullOrEmpty(key)) {
continue;
}
string value = reader.GetAttribute("value");
if (string.IsNullOrEmpty(value)) {
continue;
}
switch (key) {
case "Enabled":
Enabled = bool.Parse(value);
break;
case "SteamLogin":
SteamLogin = value;
break;
case "SteamPassword":
SteamPassword = value;
break;
case "SteamNickname":
SteamNickname = value;
break;
case "SteamApiKey":
SteamApiKey = value;
break;
case "SteamParentalPIN":
SteamParentalPIN = value;
break;
case "SteamMasterID":
SteamMasterID = ulong.Parse(value);
break;
case "SteamMasterClanID":
SteamMasterClanID = ulong.Parse(value);
break;
case "CardDropsRestricted":
CardDropsRestricted = bool.Parse(value);
break;
2015-11-04 04:51:52 +01:00
case "ShutdownOnFarmingFinished":
ShutdownOnFarmingFinished = bool.Parse(value);
break;
case "Blacklist":
2015-11-04 04:55:00 +01:00
Blacklist.Clear();
2015-11-04 04:51:52 +01:00
foreach (string appID in value.Split(',')) {
Blacklist.Add(uint.Parse(appID));
}
break;
case "Statistics":
Statistics = bool.Parse(value);
break;
default:
Logging.LogGenericWarning(BotName, "Unrecognized config value: " + key + "=" + value);
break;
}
}
2015-10-25 06:16:50 +01:00
}
2015-11-18 14:28:46 +01:00
} catch (Exception e) {
2015-11-04 04:51:52 +01:00
Logging.LogGenericException(BotName, e);
Logging.LogGenericError(BotName, "Your config for this bot instance is invalid, it won't run!");
return false;
2015-10-25 06:16:50 +01:00
}
2015-11-01 02:04:44 +01:00
return true;
2015-10-25 06:16:50 +01:00
}
internal async Task Start() {
if (IsRunning) {
2015-10-25 06:16:50 +01:00
return;
}
IsRunning = true;
Logging.LogGenericInfo(BotName, "Starting...");
2015-12-06 19:38:09 +01:00
// 2FA tokens are expiring soon, use limiter only when we don't have any pending
if (TwoFactorAuth == null) {
await Program.LimitSteamRequestsAsync().ConfigureAwait(false);
}
2015-11-04 04:31:27 +01:00
SteamClient.Connect();
var fireAndForget = Task.Run(() => HandleCallbacks());
2015-10-25 06:16:50 +01:00
}
2015-10-31 05:27:30 +01:00
internal async Task Stop() {
if (!IsRunning) {
2015-10-25 06:16:50 +01:00
return;
}
2015-10-31 05:27:30 +01:00
await CardsFarmer.StopFarming().ConfigureAwait(false);
IsRunning = false;
2015-11-01 02:08:41 +01:00
SteamClient.Disconnect();
2015-10-31 03:50:08 +01:00
}
2015-11-01 02:04:44 +01:00
private async Task<bool> Shutdown(string botNameToShutdown) {
Bot botToShutdown;
if (!Bots.TryGetValue(botNameToShutdown, out botToShutdown)) {
return false;
}
await botToShutdown.Stop().ConfigureAwait(false);
2015-11-18 14:28:46 +01:00
Bots.TryRemove(botNameToShutdown, out botToShutdown);
2015-11-01 02:04:44 +01:00
Program.OnBotShutdown(botToShutdown);
return true;
}
internal async Task<bool> Shutdown() {
return await Shutdown(BotName).ConfigureAwait(false);
}
2015-10-31 05:27:30 +01:00
internal async Task OnFarmingFinished() {
if (ShutdownOnFarmingFinished) {
2015-10-31 05:27:30 +01:00
await Shutdown().ConfigureAwait(false);
}
2015-10-25 06:16:50 +01:00
}
private void HandleCallbacks() {
TimeSpan timeSpan = TimeSpan.FromMilliseconds(CallbackSleep);
while (IsRunning) {
2015-10-25 06:16:50 +01:00
CallbackManager.RunWaitCallbacks(timeSpan);
}
}
2015-11-01 02:04:44 +01:00
private void SendMessageToUser(ulong steamID, string message) {
if (steamID == 0 || string.IsNullOrEmpty(message)) {
return;
}
SteamFriends.SendChatMessage(steamID, EChatEntryType.ChatMsg, message);
}
private void ResponseStatus(ulong steamID, string botName = null) {
2015-11-01 02:04:44 +01:00
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) {
2015-11-21 21:30:49 +01:00
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");
2015-11-01 02:04:44 +01:00
}
private void ResponseStart(ulong steamID, string botNameToStart) {
2015-11-01 02:04:44 +01:00
if (steamID == 0 || string.IsNullOrEmpty(botNameToStart)) {
return;
}
if (Bots.ContainsKey(botNameToStart)) {
SendMessageToUser(steamID, "That bot instance is already running!");
return;
}
new Bot(botNameToStart);
if (Bots.ContainsKey(botNameToStart)) {
SendMessageToUser(steamID, "Done!");
} else {
SendMessageToUser(steamID, "That bot instance failed to start, make sure that " + botNameToStart + ".xml config exists and bot is active!");
}
}
private async Task ResponseStop(ulong steamID, string botNameToShutdown) {
if (steamID == 0 || string.IsNullOrEmpty(botNameToShutdown)) {
return;
}
if (!Bots.ContainsKey(botNameToShutdown)) {
SendMessageToUser(steamID, "That bot instance is already inactive!");
return;
}
if (await Shutdown(botNameToShutdown).ConfigureAwait(false)) {
SendMessageToUser(steamID, "Done!");
} else {
SendMessageToUser(steamID, "That bot instance failed to shutdown!");
}
}
2015-10-25 06:16:50 +01:00
private void OnConnected(SteamClient.ConnectedCallback callback) {
if (callback == null) {
return;
}
if (callback.Result != EResult.OK) {
Logging.LogGenericError(BotName, "Unable to connect to Steam: " + callback.Result);
return;
}
Logging.LogGenericInfo(BotName, "Connected to Steam!");
byte[] sentryHash = null;
if (File.Exists(SentryFile)) {
byte[] sentryFileContent = File.ReadAllBytes(SentryFile);
sentryHash = CryptoHelper.SHAHash(sentryFileContent);
}
if (SteamLogin.Equals("null")) {
SteamLogin = Program.GetUserInput(BotName, Program.EUserInputType.Login);
}
if (SteamPassword.Equals("null")) {
SteamPassword = Program.GetUserInput(BotName, Program.EUserInputType.Password);
}
2015-10-25 06:16:50 +01:00
SteamUser.LogOn(new SteamUser.LogOnDetails {
Username = SteamLogin,
Password = SteamPassword,
2015-10-25 06:16:50 +01:00
AuthCode = AuthCode,
TwoFactorCode = TwoFactorAuth,
SentryFileHash = sentryHash
});
}
2015-11-01 02:04:44 +01:00
private async void OnDisconnected(SteamClient.DisconnectedCallback callback) {
2015-10-25 06:16:50 +01:00
if (callback == null) {
return;
}
2015-11-01 02:08:41 +01:00
if (!IsRunning) {
2015-11-01 02:04:44 +01:00
return;
}
2015-10-28 20:01:43 +01:00
if (SteamClient == null) {
return;
}
2015-10-25 06:16:50 +01:00
Logging.LogGenericWarning(BotName, "Disconnected from Steam, reconnecting...");
2015-12-06 19:38:09 +01:00
// 2FA tokens are expiring soon, use limiter only when we don't have any pending
if (TwoFactorAuth == null) {
await Program.LimitSteamRequestsAsync().ConfigureAwait(false);
}
2015-10-25 06:16:50 +01:00
SteamClient.Connect();
}
private void OnFriendsList(SteamFriends.FriendsListCallback callback) {
if (callback == null) {
return;
}
foreach (var friend in callback.FriendList) {
if (friend.Relationship != EFriendRelationship.RequestRecipient) {
continue;
}
SteamID steamID = friend.SteamID;
switch (steamID.AccountType) {
case EAccountType.Clan:
2015-11-20 14:47:16 +01:00
ArchiHandler.DeclineClanInvite(steamID);
2015-10-25 06:16:50 +01:00
break;
default:
if (steamID == SteamMasterID) {
SteamFriends.AddFriend(steamID);
} else {
SteamFriends.RemoveFriend(steamID);
}
break;
}
}
}
2015-10-31 05:27:30 +01:00
private async void OnFriendMsg(SteamFriends.FriendMsgCallback callback) {
2015-10-25 06:16:50 +01:00
if (callback == null) {
return;
}
if (callback.EntryType != EChatEntryType.ChatMsg) {
return;
}
ulong steamID = callback.Sender;
if (steamID != SteamMasterID) {
return;
}
string message = callback.Message;
if (string.IsNullOrEmpty(message)) {
return;
}
2015-11-25 16:49:01 +01:00
if (IsValidCdKey(message)) {
2015-10-25 06:16:50 +01:00
ArchiHandler.RedeemKey(message);
2015-11-01 02:04:44 +01:00
return;
2015-10-25 06:16:50 +01:00
}
2015-10-31 05:27:30 +01:00
2015-11-01 02:04:44 +01:00
if (!message.StartsWith("!")) {
return;
}
if (!message.Contains(" ")) {
switch (message) {
case "!exit":
await ShutdownAllBots().ConfigureAwait(false);
break;
case "!farm":
2015-11-20 14:47:16 +01:00
SendMessageToUser(steamID, "Please wait...");
2015-11-01 02:04:44 +01:00
await CardsFarmer.StartFarming().ConfigureAwait(false);
SendMessageToUser(steamID, "Done!");
break;
case "!restart":
await Program.Restart().ConfigureAwait(false);
break;
case "!status":
ResponseStatus(steamID);
break;
case "!stop":
await Shutdown().ConfigureAwait(false);
break;
}
} else {
string[] args = message.Split(' ');
switch (args[0]) {
2015-11-20 14:47:16 +01:00
case "!redeem":
ArchiHandler.RedeemKey(args[1]);
break;
2015-11-01 02:04:44 +01:00
case "!start":
ResponseStart(steamID, args[1]);
break;
case "!stop":
await ResponseStop(steamID, args[1]).ConfigureAwait(false);
break;
case "!status":
ResponseStatus(steamID, args[1]);
break;
}
2015-10-31 05:27:30 +01:00
}
2015-10-25 06:16:50 +01:00
}
private void OnAccountInfo(SteamUser.AccountInfoCallback callback) {
if (callback == null) {
return;
}
SteamFriends.SetPersonaState(EPersonaState.Online);
}
private void OnLoggedOff(SteamUser.LoggedOffCallback callback) {
if (callback == null) {
return;
}
Logging.LogGenericInfo(BotName, "Logged off of Steam: " + callback.Result);
}
private async void OnLoggedOn(SteamUser.LoggedOnCallback callback) {
if (callback == null) {
return;
}
EResult result = callback.Result;
switch (result) {
case EResult.AccountLogonDenied:
AuthCode = Program.GetUserInput(SteamLogin, Program.EUserInputType.SteamGuard);
2015-10-25 06:16:50 +01:00
break;
case EResult.AccountLoginDeniedNeedTwoFactor:
TwoFactorAuth = Program.GetUserInput(SteamLogin, Program.EUserInputType.TwoFactorAuthentication);
2015-10-25 06:16:50 +01:00
break;
case EResult.InvalidPassword:
Logging.LogGenericWarning(BotName, "Unable to login to Steam: " + result + ", will retry after a longer while");
await Stop().ConfigureAwait(false);
2015-12-04 16:47:53 +01:00
await Utilities.SleepAsync(25 * 60 * 1000).ConfigureAwait(false); // Steam removes requirement of captcha after around 20 minutes
await Start().ConfigureAwait(false);
break;
2015-10-25 06:16:50 +01:00
case EResult.OK:
Logging.LogGenericInfo(BotName, "Successfully logged on!");
2015-10-28 20:01:43 +01:00
2015-12-06 19:38:09 +01:00
// Reset one-time-only access tokens
AuthCode = null;
TwoFactorAuth = null;
if (!SteamNickname.Equals("null")) {
SteamFriends.SetPersonaName(SteamNickname);
2015-10-28 20:01:43 +01:00
}
if (SteamParentalPIN.Equals("null")) {
SteamParentalPIN = Program.GetUserInput(BotName, Program.EUserInputType.SteamParentalPIN);
}
await ArchiWebHandler.Init(SteamClient, callback.WebAPIUserNonce, callback.VanityURL, SteamParentalPIN).ConfigureAwait(false);
2015-10-25 06:16:50 +01:00
2015-12-06 20:12:44 +01:00
if (SteamMasterClanID != 0) {
await ArchiWebHandler.JoinClan(SteamMasterClanID).ConfigureAwait(false);
SteamFriends.JoinChat(SteamMasterClanID);
2015-10-28 19:21:27 +01:00
}
2015-10-25 06:16:50 +01:00
2015-10-29 16:38:16 +01:00
if (Statistics) {
await ArchiWebHandler.JoinClan(Program.ArchiSCFarmGroup).ConfigureAwait(false);
2015-12-06 20:12:44 +01:00
SteamFriends.JoinChat(Program.ArchiSCFarmGroup);
2015-10-29 16:38:16 +01:00
}
2015-10-25 06:16:50 +01:00
await CardsFarmer.StartFarming().ConfigureAwait(false);
break;
2015-12-02 17:17:47 +01:00
case EResult.ServiceUnavailable:
2015-10-28 20:01:43 +01:00
case EResult.Timeout:
case EResult.TryAnotherCM:
Logging.LogGenericWarning(BotName, "Unable to login to Steam: " + result + ", retrying...");
2015-10-31 05:27:30 +01:00
await Stop().ConfigureAwait(false);
await Start().ConfigureAwait(false);
2015-10-25 06:16:50 +01:00
break;
2015-10-28 20:01:43 +01:00
default:
Logging.LogGenericWarning(BotName, "Unable to login to Steam: " + result);
2015-10-31 05:27:30 +01:00
await Shutdown().ConfigureAwait(false);
2015-10-28 20:01:43 +01:00
break;
2015-10-25 06:16:50 +01:00
}
}
private void OnMachineAuth(SteamUser.UpdateMachineAuthCallback callback) {
if (callback == null) {
return;
}
Logging.LogGenericInfo(BotName, "Updating sentryfile...");
int fileSize;
byte[] sentryHash;
using (FileStream fileStream = File.Open(SentryFile, FileMode.OpenOrCreate, FileAccess.ReadWrite)) {
fileStream.Seek(callback.Offset, SeekOrigin.Begin);
fileStream.Write(callback.Data, 0, callback.BytesToWrite);
fileSize = (int) fileStream.Length;
fileStream.Seek(0, SeekOrigin.Begin);
using (SHA1CryptoServiceProvider sha = new SHA1CryptoServiceProvider()) {
sentryHash = sha.ComputeHash(fileStream);
}
}
// Inform the steam servers that we're accepting this sentry file
SteamUser.SendMachineAuthResponse(new SteamUser.MachineAuthDetails {
JobID = callback.JobID,
FileName = callback.FileName,
BytesWritten = callback.BytesToWrite,
FileSize = fileSize,
Offset = callback.Offset,
Result = EResult.OK,
LastError = 0,
OneTimePassword = callback.OneTimePassword,
SentryFileHash = sentryHash,
});
2015-10-28 20:09:16 +01:00
Logging.LogGenericInfo(BotName, "Sentryfile updated successfully!");
2015-10-25 06:16:50 +01:00
}
private void OnNotification(ArchiHandler.NotificationCallback callback) {
if (callback == null) {
return;
}
switch (callback.NotificationType) {
case ArchiHandler.NotificationCallback.ENotificationType.Trading:
Trading.CheckTrades();
break;
}
}
private async void OnPurchaseResponse(ArchiHandler.PurchaseResponseCallback callback) {
if (callback == null) {
return;
}
var purchaseResult = callback.PurchaseResult;
2015-11-01 16:46:02 +01:00
var items = callback.Items;
2015-11-01 16:53:08 +01:00
SendMessageToUser(SteamMasterID, "Status: " + purchaseResult + " | Items: " + string.Join("", items));
2015-10-25 06:16:50 +01:00
if (purchaseResult == ArchiHandler.PurchaseResponseCallback.EPurchaseResult.OK) {
await CardsFarmer.StartFarming().ConfigureAwait(false);
}
}
}
}