Implement update check on startup
[ArchiSteamFarm.git] / ArchiSteamFarm / Bot.cs
blobed1448d980c854c1bde87113234d0dfc2c8812c1
1 /*
2 _ _ _ ____ _ _____
3 / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
4 / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
5 / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
6 /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
8 Copyright 2015 Łukasz "JustArchi" Domeradzki
9 Contact: JustArchi@JustArchi.net
11 Licensed under the Apache License, Version 2.0 (the "License");
12 you may not use this file except in compliance with the License.
13 You may obtain a copy of the License at
15 http://www.apache.org/licenses/LICENSE-2.0
17 Unless required by applicable law or agreed to in writing, software
18 distributed under the License is distributed on an "AS IS" BASIS,
19 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 See the License for the specific language governing permissions and
21 limitations under the License.
25 using SteamKit2;
26 using System;
27 using System.Collections.Generic;
28 using System.IO;
29 using System.Security.Cryptography;
30 using System.Threading;
31 using System.Threading.Tasks;
32 using System.Xml;
34 namespace ArchiSteamFarm {
35 internal class Bot {
36 private const ushort CallbackSleep = 500; // In miliseconds
38 private readonly Dictionary<string, string> Config = new Dictionary<string, string>();
40 internal readonly string BotName;
41 private readonly string ConfigFile;
42 private readonly string SentryFile;
44 private readonly CardsFarmer CardsFarmer;
46 internal ulong BotID { get; private set; }
47 private string AuthCode, TwoFactorAuth;
49 internal ArchiHandler ArchiHandler { get; private set; }
50 internal ArchiWebHandler ArchiWebHandler { get; private set; }
51 internal CallbackManager CallbackManager { get; private set; }
52 internal SteamClient SteamClient { get; private set; }
53 internal SteamFriends SteamFriends { get; private set; }
54 internal SteamUser SteamUser { get; private set; }
55 internal Trading Trading { get; private set; }
57 // Config variables
58 internal bool Enabled { get { return bool.Parse(Config["Enabled"]); } }
59 private string SteamLogin { get { return Config["SteamLogin"]; } }
60 private string SteamPassword { get { return Config["SteamPassword"]; } }
61 private string SteamNickname { get { return Config["SteamNickname"]; } }
62 private string SteamApiKey { get { return Config["SteamApiKey"]; } }
63 private string SteamParentalPIN { get { return Config["SteamParentalPIN"]; } }
64 internal ulong SteamMasterID { get { return ulong.Parse(Config["SteamMasterID"]); } }
65 private ulong SteamMasterClanID { get { return ulong.Parse(Config["SteamMasterClanID"]); } }
66 internal HashSet<uint> Blacklist { get; } = new HashSet<uint>();
67 internal bool Statistics { get { return bool.Parse(Config["Statistics"]); } }
69 internal Bot(string botName) {
70 BotName = botName;
71 CardsFarmer = new CardsFarmer(this);
73 ConfigFile = Path.Combine(Program.ConfigDirectoryPath, BotName + ".xml");
74 SentryFile = Path.Combine(Program.ConfigDirectoryPath, BotName + ".bin");
76 ReadConfig();
78 if (!Enabled) {
79 return;
82 Start();
85 private void ReadConfig() {
86 using (XmlReader reader = XmlReader.Create(ConfigFile)) {
87 while (reader.Read()) {
88 if (reader.NodeType != XmlNodeType.Element) {
89 continue;
92 string key = reader.Name;
93 if (string.IsNullOrEmpty(key)) {
94 continue;
97 string value = reader.GetAttribute("value");
98 if (string.IsNullOrEmpty(value)) {
99 continue;
102 Config.Add(key, value);
104 switch (key) {
105 case "Blacklist":
106 foreach (string appID in value.Split(',')) {
107 Blacklist.Add(uint.Parse(appID));
109 break;
115 internal void Start() {
116 if (SteamClient != null) {
117 return;
120 SteamClient = new SteamClient();
122 ArchiHandler = new ArchiHandler();
123 SteamClient.AddHandler(ArchiHandler);
125 CallbackManager = new CallbackManager(SteamClient);
126 CallbackManager.Subscribe<SteamClient.ConnectedCallback>(OnConnected);
127 CallbackManager.Subscribe<SteamClient.DisconnectedCallback>(OnDisconnected);
129 SteamFriends = SteamClient.GetHandler<SteamFriends>();
130 CallbackManager.Subscribe<SteamFriends.FriendsListCallback>(OnFriendsList);
131 CallbackManager.Subscribe<SteamFriends.FriendMsgCallback>(OnFriendMsg);
132 CallbackManager.Subscribe<SteamFriends.PersonaStateCallback>(OnPersonaState);
134 SteamUser = SteamClient.GetHandler<SteamUser>();
135 CallbackManager.Subscribe<SteamUser.AccountInfoCallback>(OnAccountInfo);
136 CallbackManager.Subscribe<SteamUser.LoggedOffCallback>(OnLoggedOff);
137 CallbackManager.Subscribe<SteamUser.LoggedOnCallback>(OnLoggedOn);
138 CallbackManager.Subscribe<SteamUser.UpdateMachineAuthCallback>(OnMachineAuth);
140 CallbackManager.Subscribe<ArchiHandler.NotificationCallback>(OnNotification);
141 CallbackManager.Subscribe<ArchiHandler.PurchaseResponseCallback>(OnPurchaseResponse);
143 ArchiWebHandler = new ArchiWebHandler(this, SteamApiKey);
144 Trading = new Trading(this);
146 SteamClient.Connect();
147 Task.Run(() => HandleCallbacks());
150 internal void Stop() {
151 if (SteamClient == null) {
152 return;
155 SteamClient.Disconnect();
156 SteamClient = null;
157 CallbackManager = null;
160 internal void PlayGame(params ulong[] gameIDs) {
161 ArchiHandler.PlayGames(gameIDs);
164 private void HandleCallbacks() {
165 TimeSpan timeSpan = TimeSpan.FromMilliseconds(CallbackSleep);
166 while (CallbackManager != null) {
167 CallbackManager.RunWaitCallbacks(timeSpan);
171 private void OnConnected(SteamClient.ConnectedCallback callback) {
172 if (callback == null) {
173 return;
176 if (callback.Result != EResult.OK) {
177 Logging.LogGenericError(BotName, "Unable to connect to Steam: " + callback.Result);
178 return;
181 Logging.LogGenericInfo(BotName, "Connected to Steam!");
183 byte[] sentryHash = null;
184 if (File.Exists(SentryFile)) {
185 byte[] sentryFileContent = File.ReadAllBytes(SentryFile);
186 sentryHash = CryptoHelper.SHAHash(sentryFileContent);
189 string steamLogin = SteamLogin;
190 if (string.IsNullOrEmpty(steamLogin) || steamLogin.Equals("null")) {
191 steamLogin = Program.GetUserInput(BotName, Program.EUserInputType.Login);
192 Config["SteamLogin"] = steamLogin;
195 string steamPassword = SteamPassword;
196 if (string.IsNullOrEmpty(steamPassword) || steamPassword.Equals("null")) {
197 steamPassword = Program.GetUserInput(BotName, Program.EUserInputType.Password);
198 Config["SteamPassword"] = steamPassword;
201 SteamUser.LogOn(new SteamUser.LogOnDetails {
202 Username = steamLogin,
203 Password = steamPassword,
204 AuthCode = AuthCode,
205 TwoFactorCode = TwoFactorAuth,
206 SentryFileHash = sentryHash
210 private void OnDisconnected(SteamClient.DisconnectedCallback callback) {
211 if (callback == null) {
212 return;
215 if (SteamClient == null) {
216 return;
219 Logging.LogGenericWarning(BotName, "Disconnected from Steam, reconnecting...");
220 Thread.Sleep(TimeSpan.FromMilliseconds(CallbackSleep));
221 SteamClient.Connect();
224 private void OnFriendsList(SteamFriends.FriendsListCallback callback) {
225 if (callback == null) {
226 return;
229 foreach (var friend in callback.FriendList) {
230 if (friend.Relationship != EFriendRelationship.RequestRecipient) {
231 continue;
234 SteamID steamID = friend.SteamID;
235 switch (steamID.AccountType) {
236 case EAccountType.Clan:
237 //ArchiHandler.AcceptClanInvite(steamID);
238 break;
239 default:
240 if (steamID == SteamMasterID) {
241 SteamFriends.AddFriend(steamID);
242 } else {
243 SteamFriends.RemoveFriend(steamID);
245 break;
250 private void OnFriendMsg(SteamFriends.FriendMsgCallback callback) {
251 if (callback == null) {
252 return;
255 if (callback.EntryType != EChatEntryType.ChatMsg) {
256 return;
259 ulong steamID = callback.Sender;
260 if (steamID != SteamMasterID) {
261 return;
264 string message = callback.Message;
265 if (string.IsNullOrEmpty(message)) {
266 return;
269 if (message.Length == 17 && message[5] == '-' && message[11] == '-') {
270 ArchiHandler.RedeemKey(message);
274 private void OnPersonaState(SteamFriends.PersonaStateCallback callback) {
275 if (callback == null) {
276 return;
279 SteamID steamID = callback.FriendID;
280 SteamID sourceSteamID = callback.SourceSteamID;
281 string steamNickname = callback.Name;
282 EPersonaState personaState = callback.State;
283 EClanRank clanRank = (EClanRank) callback.ClanRank;
286 private void OnAccountInfo(SteamUser.AccountInfoCallback callback) {
287 if (callback == null) {
288 return;
291 SteamFriends.SetPersonaState(EPersonaState.Online);
294 private void OnLoggedOff(SteamUser.LoggedOffCallback callback) {
295 if (callback == null) {
296 return;
299 Logging.LogGenericInfo(BotName, "Logged off of Steam: " + callback.Result);
302 private async void OnLoggedOn(SteamUser.LoggedOnCallback callback) {
303 if (callback == null) {
304 return;
307 if (callback.ClientSteamID != 0) {
308 BotID = callback.ClientSteamID;
311 EResult result = callback.Result;
312 switch (result) {
313 case EResult.AccountLogonDenied:
314 AuthCode = Program.GetUserInput(SteamLogin, Program.EUserInputType.SteamGuard);
315 break;
316 case EResult.AccountLoginDeniedNeedTwoFactor:
317 TwoFactorAuth = Program.GetUserInput(SteamLogin, Program.EUserInputType.TwoFactorAuthentication);
318 break;
319 case EResult.OK:
320 Logging.LogGenericInfo(BotName, "Successfully logged on!");
322 string steamNickname = SteamNickname;
323 if (!string.IsNullOrEmpty(steamNickname) && !steamNickname.Equals("null")) {
324 SteamFriends.SetPersonaName(steamNickname);
327 string steamParentalPIN = SteamParentalPIN;
328 if (string.IsNullOrEmpty(steamParentalPIN) || steamParentalPIN.Equals("null")) {
329 steamParentalPIN = Program.GetUserInput(BotName, Program.EUserInputType.SteamParentalPIN);
330 Config["SteamParentalPIN"] = steamParentalPIN;
333 await ArchiWebHandler.Init(SteamClient, callback.WebAPIUserNonce, callback.VanityURL, steamParentalPIN).ConfigureAwait(false);
335 ulong clanID = SteamMasterClanID;
336 if (clanID != 0) {
337 SteamFriends.JoinChat(clanID);
340 if (Statistics) {
341 SteamFriends.JoinChat(Program.ArchiSCFarmGroup);
342 await ArchiWebHandler.JoinClan(Program.ArchiSCFarmGroup).ConfigureAwait(false);
345 await CardsFarmer.StartFarming().ConfigureAwait(false);
346 break;
347 case EResult.Timeout:
348 case EResult.TryAnotherCM:
349 Logging.LogGenericWarning(BotName, "Unable to login to Steam: " + callback.Result + " / " + callback.ExtendedResult + ", retrying...");
350 Stop();
351 Thread.Sleep(5000);
352 Start();
353 break;
354 default:
355 Logging.LogGenericWarning(BotName, "Unable to login to Steam: " + callback.Result + " / " + callback.ExtendedResult);
356 Stop();
357 break;
361 private void OnMachineAuth(SteamUser.UpdateMachineAuthCallback callback) {
362 if (callback == null) {
363 return;
366 Logging.LogGenericInfo(BotName, "Updating sentryfile...");
368 int fileSize;
369 byte[] sentryHash;
371 using (FileStream fileStream = File.Open(SentryFile, FileMode.OpenOrCreate, FileAccess.ReadWrite)) {
372 fileStream.Seek(callback.Offset, SeekOrigin.Begin);
373 fileStream.Write(callback.Data, 0, callback.BytesToWrite);
374 fileSize = (int) fileStream.Length;
376 fileStream.Seek(0, SeekOrigin.Begin);
377 using (SHA1CryptoServiceProvider sha = new SHA1CryptoServiceProvider()) {
378 sentryHash = sha.ComputeHash(fileStream);
383 // Inform the steam servers that we're accepting this sentry file
384 SteamUser.SendMachineAuthResponse(new SteamUser.MachineAuthDetails {
385 JobID = callback.JobID,
386 FileName = callback.FileName,
387 BytesWritten = callback.BytesToWrite,
388 FileSize = fileSize,
389 Offset = callback.Offset,
390 Result = EResult.OK,
391 LastError = 0,
392 OneTimePassword = callback.OneTimePassword,
393 SentryFileHash = sentryHash,
396 Logging.LogGenericInfo(BotName, "Sentryfile updated successfully!");
399 private void OnNotification(ArchiHandler.NotificationCallback callback) {
400 if (callback == null) {
401 return;
404 switch (callback.NotificationType) {
405 case ArchiHandler.NotificationCallback.ENotificationType.Trading:
406 Trading.CheckTrades();
407 break;
411 private async void OnPurchaseResponse(ArchiHandler.PurchaseResponseCallback callback) {
412 if (callback == null) {
413 return;
416 var purchaseResult = callback.PurchaseResult;
417 SteamFriends.SendChatMessage(SteamMasterID, EChatEntryType.ChatMsg, "Status: " + purchaseResult);
419 if (purchaseResult == ArchiHandler.PurchaseResponseCallback.EPurchaseResult.OK) {
420 await CardsFarmer.StartFarming().ConfigureAwait(false);