mirror of
https://github.com/JustArchiNET/ArchiSteamFarm.git
synced 2026-01-16 08:25:28 +00:00
Works exactly the same as TwoFactorCodeMismatch, apparently it's too hard for Valve to tell the client "hey, our password validator module is currently down and we're unable to log you in", no, "INVALID PASSWORD" is the right answer.
3198 lines
100 KiB
C#
Executable File
3198 lines
100 KiB
C#
Executable File
// _ _ _ ____ _ _____
|
|
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
|
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
|
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
|
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
|
// |
|
|
// Copyright 2015-2020 Łukasz "JustArchi" Domeradzki
|
|
// Contact: JustArchi@JustArchi.net
|
|
// |
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
// |
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
// |
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Immutable;
|
|
using System.Collections.Specialized;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Security.Cryptography;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using ArchiSteamFarm.Collections;
|
|
using ArchiSteamFarm.Localization;
|
|
using ArchiSteamFarm.NLog;
|
|
using ArchiSteamFarm.Plugins;
|
|
using JetBrains.Annotations;
|
|
using Newtonsoft.Json;
|
|
using SteamKit2;
|
|
using SteamKit2.Internal;
|
|
|
|
namespace ArchiSteamFarm {
|
|
public sealed class Bot : IAsyncDisposable {
|
|
internal const ushort CallbackSleep = 500; // In milliseconds
|
|
internal const ushort MaxMessagePrefixLength = MaxMessageLength - ReservedMessageLength - 2; // 2 for a minimum of 2 characters (escape one and real one)
|
|
internal const byte MinPlayingBlockedTTL = 60; // Delay in seconds added when account was occupied during our disconnect, to not disconnect other Steam client session too soon
|
|
|
|
private const char DefaultBackgroundKeysRedeemerSeparator = '\t';
|
|
private const byte LoginCooldownInMinutes = 25; // Captcha disappears after around 20 minutes, so we make it 25
|
|
private const uint LoginID = 1242; // This must be the same for all ASF bots and all ASF processes
|
|
private const byte MaxInvalidPasswordFailures = WebBrowser.MaxTries; // Max InvalidPassword failures in a row before we determine that our password is invalid (because Steam wrongly returns those, of course)
|
|
private const ushort MaxMessageLength = 5000; // This is a limitation enforced by Steam
|
|
private const byte MaxTwoFactorCodeFailures = WebBrowser.MaxTries; // Max TwoFactorCodeMismatch failures in a row before we determine that our 2FA credentials are invalid (because Steam wrongly returns those, of course)
|
|
private const byte RedeemCooldownInHours = 1; // 1 hour since first redeem attempt, this is a limitation enforced by Steam
|
|
private const byte ReservedMessageLength = 2; // 2 for 2x optional …
|
|
|
|
[PublicAPI]
|
|
public static IReadOnlyDictionary<string, Bot> BotsReadOnly => Bots;
|
|
|
|
internal static ConcurrentDictionary<string, Bot> Bots { get; private set; }
|
|
internal static StringComparer BotsComparer { get; private set; }
|
|
internal static EOSType OSType { get; private set; } = EOSType.Unknown;
|
|
|
|
private static readonly SemaphoreSlim BotsSemaphore = new SemaphoreSlim(1, 1);
|
|
|
|
[JsonIgnore]
|
|
[PublicAPI]
|
|
public readonly Actions Actions;
|
|
|
|
[JsonIgnore]
|
|
[PublicAPI]
|
|
public readonly ArchiLogger ArchiLogger;
|
|
|
|
[JsonIgnore]
|
|
[PublicAPI]
|
|
public readonly ArchiWebHandler ArchiWebHandler;
|
|
|
|
[JsonProperty]
|
|
[PublicAPI]
|
|
public readonly string BotName;
|
|
|
|
[JsonProperty]
|
|
[PublicAPI]
|
|
public readonly CardsFarmer CardsFarmer;
|
|
|
|
[JsonIgnore]
|
|
[PublicAPI]
|
|
public readonly Commands Commands;
|
|
|
|
[JsonIgnore]
|
|
[PublicAPI]
|
|
public readonly SteamApps SteamApps;
|
|
|
|
[JsonIgnore]
|
|
[PublicAPI]
|
|
public readonly SteamConfiguration SteamConfiguration;
|
|
|
|
[JsonIgnore]
|
|
[PublicAPI]
|
|
public readonly SteamFriends SteamFriends;
|
|
|
|
[JsonProperty]
|
|
[PublicAPI]
|
|
public uint GamesToRedeemInBackgroundCount => BotDatabase?.GamesToRedeemInBackgroundCount ?? 0;
|
|
|
|
[JsonProperty]
|
|
[PublicAPI]
|
|
public bool HasMobileAuthenticator => BotDatabase?.MobileAuthenticator != null;
|
|
|
|
[JsonProperty]
|
|
[PublicAPI]
|
|
public bool IsConnectedAndLoggedOn => SteamClient?.SteamID != null;
|
|
|
|
[JsonProperty]
|
|
[PublicAPI]
|
|
public bool IsPlayingPossible => !PlayingBlocked && !LibraryLocked;
|
|
|
|
[NotNull]
|
|
[JsonIgnore]
|
|
[PublicAPI]
|
|
public IReadOnlyCollection<uint> OwnedPackageIDsReadOnly => OwnedPackageIDs.Keys.ToHashSet();
|
|
|
|
internal readonly ArchiHandler ArchiHandler;
|
|
internal readonly BotDatabase BotDatabase;
|
|
|
|
internal bool CanReceiveSteamCards => !IsAccountLimited && !IsAccountLocked;
|
|
internal bool IsAccountLimited => AccountFlags.HasFlag(EAccountFlags.LimitedUser) || AccountFlags.HasFlag(EAccountFlags.LimitedUserForce);
|
|
internal bool IsAccountLocked => AccountFlags.HasFlag(EAccountFlags.Lockdown);
|
|
|
|
private readonly CallbackManager CallbackManager;
|
|
private readonly SemaphoreSlim CallbackSemaphore = new SemaphoreSlim(1, 1);
|
|
private readonly SemaphoreSlim GamesRedeemerInBackgroundSemaphore = new SemaphoreSlim(1, 1);
|
|
private readonly Timer HeartBeatTimer;
|
|
private readonly SemaphoreSlim InitializationSemaphore = new SemaphoreSlim(1, 1);
|
|
private readonly SemaphoreSlim MessagingSemaphore = new SemaphoreSlim(1, 1);
|
|
private readonly ConcurrentDictionary<ArchiHandler.UserNotificationsCallback.EUserNotification, uint> PastNotifications = new ConcurrentDictionary<ArchiHandler.UserNotificationsCallback.EUserNotification, uint>();
|
|
private readonly Statistics Statistics;
|
|
private readonly SteamClient SteamClient;
|
|
private readonly ConcurrentHashSet<ulong> SteamFamilySharingIDs = new ConcurrentHashSet<ulong>();
|
|
private readonly SteamUser SteamUser;
|
|
private readonly Trading Trading;
|
|
|
|
private IEnumerable<(string FilePath, EFileType FileType)> RelatedFiles {
|
|
get {
|
|
foreach (EFileType fileType in Enum.GetValues(typeof(EFileType))) {
|
|
string filePath = GetFilePath(fileType);
|
|
|
|
if (string.IsNullOrEmpty(filePath)) {
|
|
ArchiLogger.LogNullError(nameof(filePath));
|
|
|
|
yield break;
|
|
}
|
|
|
|
yield return (filePath, fileType);
|
|
}
|
|
}
|
|
}
|
|
|
|
#pragma warning disable IDE0051
|
|
[JsonProperty(PropertyName = SharedInfo.UlongCompatibilityStringPrefix + nameof(SteamID))]
|
|
[NotNull]
|
|
private string SSteamID => SteamID.ToString();
|
|
#pragma warning restore IDE0051
|
|
|
|
[JsonProperty]
|
|
[PublicAPI]
|
|
public EAccountFlags AccountFlags { get; private set; }
|
|
|
|
[JsonProperty]
|
|
[PublicAPI]
|
|
public BotConfig BotConfig { get; private set; }
|
|
|
|
[JsonProperty]
|
|
[PublicAPI]
|
|
public bool KeepRunning { get; private set; }
|
|
|
|
[JsonProperty]
|
|
[PublicAPI]
|
|
public string Nickname { get; private set; }
|
|
|
|
[JsonProperty]
|
|
[PublicAPI]
|
|
public ASF.EUserInputType RequiredInput { get; private set; }
|
|
|
|
[JsonProperty]
|
|
[PublicAPI]
|
|
public ulong SteamID { get; private set; }
|
|
|
|
[JsonProperty]
|
|
[PublicAPI]
|
|
public long WalletBalance { get; private set; }
|
|
|
|
[JsonProperty]
|
|
[PublicAPI]
|
|
public ECurrencyCode WalletCurrency { get; private set; }
|
|
|
|
internal ImmutableDictionary<uint, (EPaymentMethod PaymentMethod, DateTime TimeCreated)> OwnedPackageIDs { get; private set; } = ImmutableDictionary<uint, (EPaymentMethod PaymentMethod, DateTime TimeCreated)>.Empty;
|
|
internal bool PlayingBlocked { get; private set; }
|
|
internal bool PlayingWasBlocked { get; private set; }
|
|
|
|
private string AuthCode;
|
|
|
|
#pragma warning disable IDE0052
|
|
[JsonProperty]
|
|
private string AvatarHash;
|
|
#pragma warning restore IDE0052
|
|
|
|
private Timer ConnectionFailureTimer;
|
|
private bool FirstTradeSent;
|
|
private Timer GamesRedeemerInBackgroundTimer;
|
|
private byte HeartBeatFailures;
|
|
private byte InvalidPasswordFailures;
|
|
private EResult LastLogOnResult;
|
|
private DateTime LastLogonSessionReplaced;
|
|
private bool LibraryLocked;
|
|
private ulong MasterChatGroupID;
|
|
private Timer PlayingWasBlockedTimer;
|
|
private bool ReconnectOnUserInitiated;
|
|
private Timer SendItemsTimer;
|
|
private bool SteamParentalActive = true;
|
|
private SteamSaleEvent SteamSaleEvent;
|
|
private string TwoFactorCode;
|
|
private byte TwoFactorCodeFailures;
|
|
|
|
private Bot([NotNull] string botName, [NotNull] BotConfig botConfig, [NotNull] BotDatabase botDatabase) {
|
|
if (string.IsNullOrEmpty(botName) || (botConfig == null) || (botDatabase == null)) {
|
|
throw new ArgumentNullException(nameof(botName) + " || " + nameof(botConfig) + " || " + nameof(botDatabase));
|
|
}
|
|
|
|
BotName = botName;
|
|
BotConfig = botConfig;
|
|
BotDatabase = botDatabase;
|
|
|
|
ArchiLogger = new ArchiLogger(botName);
|
|
|
|
if (HasMobileAuthenticator) {
|
|
BotDatabase.MobileAuthenticator.Init(this);
|
|
}
|
|
|
|
ArchiWebHandler = new ArchiWebHandler(this);
|
|
|
|
SteamConfiguration = SteamConfiguration.Create(builder => builder.WithProtocolTypes(ASF.GlobalConfig.SteamProtocols).WithCellID(ASF.GlobalDatabase.CellID).WithServerListProvider(ASF.GlobalDatabase.ServerListProvider).WithHttpClientFactory(ArchiWebHandler.GenerateDisposableHttpClient));
|
|
|
|
// Initialize
|
|
SteamClient = new SteamClient(SteamConfiguration, botName);
|
|
|
|
if (Debugging.IsDebugConfigured && Directory.Exists(SharedInfo.DebugDirectory)) {
|
|
string debugListenerPath = Path.Combine(SharedInfo.DebugDirectory, botName);
|
|
|
|
try {
|
|
Directory.CreateDirectory(debugListenerPath);
|
|
SteamClient.DebugNetworkListener = new NetHookNetworkListener(debugListenerPath, SteamClient);
|
|
} catch (Exception e) {
|
|
ArchiLogger.LogGenericException(e);
|
|
}
|
|
}
|
|
|
|
SteamUnifiedMessages steamUnifiedMessages = SteamClient.GetHandler<SteamUnifiedMessages>();
|
|
|
|
ArchiHandler = new ArchiHandler(ArchiLogger, steamUnifiedMessages ?? throw new ArgumentNullException(nameof(steamUnifiedMessages)));
|
|
SteamClient.AddHandler(ArchiHandler);
|
|
|
|
CallbackManager = new CallbackManager(SteamClient);
|
|
CallbackManager.Subscribe<SteamClient.ConnectedCallback>(OnConnected);
|
|
CallbackManager.Subscribe<SteamClient.DisconnectedCallback>(OnDisconnected);
|
|
|
|
SteamApps = SteamClient.GetHandler<SteamApps>();
|
|
CallbackManager.Subscribe<SteamApps.GuestPassListCallback>(OnGuestPassList);
|
|
CallbackManager.Subscribe<SteamApps.LicenseListCallback>(OnLicenseList);
|
|
|
|
SteamFriends = SteamClient.GetHandler<SteamFriends>();
|
|
CallbackManager.Subscribe<SteamFriends.FriendsListCallback>(OnFriendsList);
|
|
CallbackManager.Subscribe<SteamFriends.PersonaStateCallback>(OnPersonaState);
|
|
|
|
CallbackManager.Subscribe<SteamUnifiedMessages.ServiceMethodNotification>(OnServiceMethod);
|
|
|
|
SteamUser = SteamClient.GetHandler<SteamUser>();
|
|
CallbackManager.Subscribe<SteamUser.LoggedOffCallback>(OnLoggedOff);
|
|
CallbackManager.Subscribe<SteamUser.LoggedOnCallback>(OnLoggedOn);
|
|
CallbackManager.Subscribe<SteamUser.LoginKeyCallback>(OnLoginKey);
|
|
CallbackManager.Subscribe<SteamUser.UpdateMachineAuthCallback>(OnMachineAuth);
|
|
CallbackManager.Subscribe<SteamUser.WalletInfoCallback>(OnWalletUpdate);
|
|
|
|
CallbackManager.Subscribe<ArchiHandler.PlayingSessionStateCallback>(OnPlayingSessionState);
|
|
CallbackManager.Subscribe<ArchiHandler.SharedLibraryLockStatusCallback>(OnSharedLibraryLockStatus);
|
|
CallbackManager.Subscribe<ArchiHandler.UserNotificationsCallback>(OnUserNotifications);
|
|
CallbackManager.Subscribe<ArchiHandler.VanityURLChangedCallback>(OnVanityURLChangedCallback);
|
|
|
|
Actions = new Actions(this);
|
|
CardsFarmer = new CardsFarmer(this);
|
|
Commands = new Commands(this);
|
|
Trading = new Trading(this);
|
|
|
|
if (!Debugging.IsDebugBuild && ASF.GlobalConfig.Statistics) {
|
|
Statistics = new Statistics(this);
|
|
}
|
|
|
|
HeartBeatTimer = new Timer(
|
|
async e => await HeartBeat().ConfigureAwait(false),
|
|
null,
|
|
TimeSpan.FromMinutes(1) + TimeSpan.FromSeconds(ASF.LoadBalancingDelay * Bots.Count), // Delay
|
|
TimeSpan.FromMinutes(1) // Period
|
|
);
|
|
}
|
|
|
|
public async ValueTask DisposeAsync() {
|
|
// Those are objects that are always being created if constructor doesn't throw exception
|
|
ArchiWebHandler.Dispose();
|
|
CallbackSemaphore.Dispose();
|
|
GamesRedeemerInBackgroundSemaphore.Dispose();
|
|
InitializationSemaphore.Dispose();
|
|
MessagingSemaphore.Dispose();
|
|
Trading.Dispose();
|
|
|
|
await Actions.DisposeAsync().ConfigureAwait(false);
|
|
await CardsFarmer.DisposeAsync().ConfigureAwait(false);
|
|
await HeartBeatTimer.DisposeAsync().ConfigureAwait(false);
|
|
|
|
// Those are objects that might be null and the check should be in-place
|
|
BotDatabase?.Dispose();
|
|
|
|
if (ConnectionFailureTimer != null) {
|
|
await ConnectionFailureTimer.DisposeAsync().ConfigureAwait(false);
|
|
}
|
|
|
|
if (GamesRedeemerInBackgroundTimer != null) {
|
|
await GamesRedeemerInBackgroundTimer.DisposeAsync().ConfigureAwait(false);
|
|
}
|
|
|
|
if (PlayingWasBlockedTimer != null) {
|
|
await PlayingWasBlockedTimer.DisposeAsync().ConfigureAwait(false);
|
|
}
|
|
|
|
if (SendItemsTimer != null) {
|
|
await SendItemsTimer.DisposeAsync().ConfigureAwait(false);
|
|
}
|
|
|
|
if (Statistics != null) {
|
|
await Statistics.DisposeAsync().ConfigureAwait(false);
|
|
}
|
|
|
|
if (SteamSaleEvent != null) {
|
|
await SteamSaleEvent.DisposeAsync().ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
[PublicAPI]
|
|
public static Bot GetBot(string botName) {
|
|
if (string.IsNullOrEmpty(botName)) {
|
|
ASF.ArchiLogger.LogNullError(nameof(botName));
|
|
|
|
return null;
|
|
}
|
|
|
|
if (Bots.TryGetValue(botName, out Bot targetBot)) {
|
|
return targetBot;
|
|
}
|
|
|
|
if (!ulong.TryParse(botName, out ulong steamID) || (steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
|
|
return null;
|
|
}
|
|
|
|
return Bots.Values.FirstOrDefault(bot => bot.SteamID == steamID);
|
|
}
|
|
|
|
[PublicAPI]
|
|
public static HashSet<Bot> GetBots(string args) {
|
|
if (string.IsNullOrEmpty(args)) {
|
|
ASF.ArchiLogger.LogNullError(nameof(args));
|
|
|
|
return null;
|
|
}
|
|
|
|
string[] botNames = args.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
HashSet<Bot> result = new HashSet<Bot>();
|
|
|
|
foreach (string botName in botNames) {
|
|
if (botName.Equals(SharedInfo.ASF, StringComparison.OrdinalIgnoreCase)) {
|
|
IEnumerable<Bot> allBots = Bots.OrderBy(bot => bot.Key, BotsComparer).Select(bot => bot.Value);
|
|
result.UnionWith(allBots);
|
|
|
|
return result;
|
|
}
|
|
|
|
if (botName.Contains("..")) {
|
|
string[] botRange = botName.Split(new[] { ".." }, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
if (botRange.Length == 2) {
|
|
Bot firstBot = GetBot(botRange[0]);
|
|
|
|
if (firstBot != null) {
|
|
Bot lastBot = GetBot(botRange[1]);
|
|
|
|
if (lastBot != null) {
|
|
foreach (Bot bot in Bots.OrderBy(bot => bot.Key, BotsComparer).Select(bot => bot.Value).SkipWhile(bot => bot != firstBot)) {
|
|
result.Add(bot);
|
|
|
|
if (bot == lastBot) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (botName.StartsWith("r!", StringComparison.OrdinalIgnoreCase)) {
|
|
string botsPattern = botName.Substring(2);
|
|
|
|
RegexOptions botsRegex = RegexOptions.None;
|
|
|
|
if ((BotsComparer == StringComparer.InvariantCulture) || (BotsComparer == StringComparer.Ordinal)) {
|
|
botsRegex |= RegexOptions.CultureInvariant;
|
|
} else if ((BotsComparer == StringComparer.InvariantCultureIgnoreCase) || (BotsComparer == StringComparer.OrdinalIgnoreCase)) {
|
|
botsRegex |= RegexOptions.CultureInvariant | RegexOptions.IgnoreCase;
|
|
}
|
|
|
|
Regex regex;
|
|
|
|
try {
|
|
regex = new Regex(botsPattern, botsRegex);
|
|
} catch (ArgumentException e) {
|
|
ASF.ArchiLogger.LogGenericWarningException(e);
|
|
|
|
return null;
|
|
}
|
|
|
|
IEnumerable<Bot> regexMatches = Bots.Where(kvp => regex.IsMatch(kvp.Key)).Select(kvp => kvp.Value);
|
|
result.UnionWith(regexMatches);
|
|
|
|
continue;
|
|
}
|
|
|
|
Bot singleBot = GetBot(botName);
|
|
|
|
if (singleBot == null) {
|
|
continue;
|
|
}
|
|
|
|
result.Add(singleBot);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
[PublicAPI]
|
|
public async Task<byte?> GetTradeHoldDuration(ulong steamID, ulong tradeID) {
|
|
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount || (tradeID == 0)) {
|
|
ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(tradeID));
|
|
|
|
return null;
|
|
}
|
|
|
|
if (SteamFriends.GetFriendRelationship(steamID) == EFriendRelationship.Friend) {
|
|
byte? tradeHoldDurationForUser = await ArchiWebHandler.GetTradeHoldDurationForUser(steamID).ConfigureAwait(false);
|
|
|
|
if (tradeHoldDurationForUser.HasValue) {
|
|
return tradeHoldDurationForUser;
|
|
}
|
|
}
|
|
|
|
Bot targetBot = Bots.Values.FirstOrDefault(bot => bot.SteamID == steamID);
|
|
|
|
if (targetBot?.IsConnectedAndLoggedOn == true) {
|
|
string targetTradeToken = await targetBot.ArchiHandler.GetTradeToken().ConfigureAwait(false);
|
|
|
|
if (!string.IsNullOrEmpty(targetTradeToken)) {
|
|
byte? tradeHoldDurationForUser = await ArchiWebHandler.GetTradeHoldDurationForUser(steamID, targetTradeToken).ConfigureAwait(false);
|
|
|
|
if (tradeHoldDurationForUser.HasValue) {
|
|
return tradeHoldDurationForUser;
|
|
}
|
|
}
|
|
}
|
|
|
|
return await ArchiWebHandler.GetTradeHoldDurationForTrade(tradeID).ConfigureAwait(false);
|
|
}
|
|
|
|
[PublicAPI]
|
|
public bool HasPermission(ulong steamID, BotConfig.EPermission permission) {
|
|
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount || (permission == BotConfig.EPermission.None)) {
|
|
ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(permission));
|
|
|
|
return false;
|
|
}
|
|
|
|
if (ASF.IsOwner(steamID)) {
|
|
return true;
|
|
}
|
|
|
|
return permission switch {
|
|
BotConfig.EPermission.FamilySharing when SteamFamilySharingIDs.Contains(steamID) => true,
|
|
_ => BotConfig.SteamUserPermissions.TryGetValue(steamID, out BotConfig.EPermission realPermission) && (realPermission >= permission)
|
|
};
|
|
}
|
|
|
|
[PublicAPI]
|
|
public bool SetUserInput(ASF.EUserInputType inputType, string inputValue) {
|
|
if ((inputType == ASF.EUserInputType.None) || !Enum.IsDefined(typeof(ASF.EUserInputType), inputType) || string.IsNullOrEmpty(inputValue)) {
|
|
ArchiLogger.LogNullError(nameof(inputType) + " || " + nameof(inputValue));
|
|
|
|
return false;
|
|
}
|
|
|
|
// This switch should cover ONLY bot properties
|
|
switch (inputType) {
|
|
case ASF.EUserInputType.Login:
|
|
if (BotConfig == null) {
|
|
return false;
|
|
}
|
|
|
|
BotConfig.SteamLogin = inputValue;
|
|
|
|
break;
|
|
case ASF.EUserInputType.Password:
|
|
if (BotConfig == null) {
|
|
return false;
|
|
}
|
|
|
|
BotConfig.DecryptedSteamPassword = inputValue;
|
|
|
|
break;
|
|
case ASF.EUserInputType.SteamGuard:
|
|
if (inputValue.Length != 5) {
|
|
return false;
|
|
}
|
|
|
|
AuthCode = inputValue;
|
|
|
|
break;
|
|
case ASF.EUserInputType.SteamParentalCode:
|
|
if ((BotConfig == null) || (inputValue.Length != BotConfig.SteamParentalCodeLength)) {
|
|
return false;
|
|
}
|
|
|
|
BotConfig.SteamParentalCode = inputValue;
|
|
|
|
break;
|
|
case ASF.EUserInputType.TwoFactorAuthentication:
|
|
if (((inputValue.Length != MobileAuthenticator.CodeDigits) && (inputValue.Length != MobileAuthenticator.BackupCodeDigits)) || inputValue.Any(character => !MobileAuthenticator.CodeCharacters.Contains(character))) {
|
|
return false;
|
|
}
|
|
|
|
TwoFactorCode = inputValue;
|
|
|
|
break;
|
|
default:
|
|
ASF.ArchiLogger.LogGenericError(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(inputType), inputType));
|
|
|
|
break;
|
|
}
|
|
|
|
if (RequiredInput == inputType) {
|
|
RequiredInput = ASF.EUserInputType.None;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
internal void AddGamesToRedeemInBackground(IOrderedDictionary gamesToRedeemInBackground) {
|
|
if ((gamesToRedeemInBackground == null) || (gamesToRedeemInBackground.Count == 0)) {
|
|
ArchiLogger.LogNullError(nameof(gamesToRedeemInBackground));
|
|
|
|
return;
|
|
}
|
|
|
|
BotDatabase.AddGamesToRedeemInBackground(gamesToRedeemInBackground);
|
|
|
|
if ((GamesRedeemerInBackgroundTimer == null) && BotDatabase.HasGamesToRedeemInBackground && IsConnectedAndLoggedOn) {
|
|
Utilities.InBackground(RedeemGamesInBackground);
|
|
}
|
|
}
|
|
|
|
internal async Task<bool> DeleteAllRelatedFiles() {
|
|
await BotDatabase.MakeReadOnly().ConfigureAwait(false);
|
|
|
|
foreach (string filePath in RelatedFiles.Select(file => file.FilePath).Where(File.Exists)) {
|
|
try {
|
|
File.Delete(filePath);
|
|
} catch (Exception e) {
|
|
ArchiLogger.LogGenericException(e);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
internal bool DeleteRedeemedKeysFiles() {
|
|
string unusedKeysFilePath = GetFilePath(EFileType.KeysToRedeemUnused);
|
|
|
|
if (string.IsNullOrEmpty(unusedKeysFilePath)) {
|
|
ASF.ArchiLogger.LogNullError(nameof(unusedKeysFilePath));
|
|
|
|
return false;
|
|
}
|
|
|
|
if (File.Exists(unusedKeysFilePath)) {
|
|
try {
|
|
File.Delete(unusedKeysFilePath);
|
|
} catch (Exception e) {
|
|
ArchiLogger.LogGenericException(e);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
string usedKeysFilePath = GetFilePath(EFileType.KeysToRedeemUsed);
|
|
|
|
if (string.IsNullOrEmpty(usedKeysFilePath)) {
|
|
ASF.ArchiLogger.LogNullError(nameof(usedKeysFilePath));
|
|
|
|
return false;
|
|
}
|
|
|
|
if (File.Exists(usedKeysFilePath)) {
|
|
try {
|
|
File.Delete(usedKeysFilePath);
|
|
} catch (Exception e) {
|
|
ArchiLogger.LogGenericException(e);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
internal static string FormatBotResponse(string response, string botName) {
|
|
if (string.IsNullOrEmpty(response) || string.IsNullOrEmpty(botName)) {
|
|
ASF.ArchiLogger.LogNullError(nameof(response) + " || " + nameof(botName));
|
|
|
|
return null;
|
|
}
|
|
|
|
return Environment.NewLine + "<" + botName + "> " + response;
|
|
}
|
|
|
|
internal async Task<(uint PlayableAppID, DateTime IgnoredUntil, bool IgnoredGlobally)> GetAppDataForIdling(uint appID, float hoursPlayed, bool allowRecursiveDiscovery = true, bool optimisticDiscovery = true) {
|
|
if ((appID == 0) || (hoursPlayed < 0)) {
|
|
ArchiLogger.LogNullError(nameof(appID) + " || " + nameof(hoursPlayed));
|
|
|
|
return (0, DateTime.MaxValue, true);
|
|
}
|
|
|
|
HashSet<uint> packageIDs = ASF.GlobalDatabase.GetPackageIDs(appID, OwnedPackageIDs.Keys);
|
|
|
|
if ((packageIDs == null) || (packageIDs.Count == 0)) {
|
|
return (0, DateTime.MaxValue, true);
|
|
}
|
|
|
|
if ((hoursPlayed < CardsFarmer.HoursForRefund) && !BotConfig.IdleRefundableGames) {
|
|
DateTime mostRecent = DateTime.MinValue;
|
|
|
|
foreach (uint packageID in packageIDs) {
|
|
if (!OwnedPackageIDs.TryGetValue(packageID, out (EPaymentMethod PaymentMethod, DateTime TimeCreated) packageData)) {
|
|
continue;
|
|
}
|
|
|
|
if (IsRefundable(packageData.PaymentMethod) && (packageData.TimeCreated > mostRecent)) {
|
|
mostRecent = packageData.TimeCreated;
|
|
}
|
|
}
|
|
|
|
if (mostRecent > DateTime.MinValue) {
|
|
DateTime playableIn = mostRecent.AddDays(CardsFarmer.DaysForRefund);
|
|
|
|
if (playableIn > DateTime.UtcNow) {
|
|
return (0, playableIn, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
SteamApps.PICSTokensCallback tokenCallback = null;
|
|
|
|
for (byte i = 0; (i < WebBrowser.MaxTries) && (tokenCallback == null) && IsConnectedAndLoggedOn; i++) {
|
|
try {
|
|
tokenCallback = await SteamApps.PICSGetAccessTokens(appID, null).ToLongRunningTask().ConfigureAwait(false);
|
|
} catch (Exception e) {
|
|
ArchiLogger.LogGenericWarningException(e);
|
|
}
|
|
}
|
|
|
|
if (tokenCallback == null) {
|
|
return (optimisticDiscovery ? appID : 0, DateTime.MinValue, true);
|
|
}
|
|
|
|
SteamApps.PICSRequest request = new SteamApps.PICSRequest(appID, tokenCallback.AppTokens.TryGetValue(appID, out ulong accessToken) ? accessToken : 0, false);
|
|
|
|
AsyncJobMultiple<SteamApps.PICSProductInfoCallback>.ResultSet productInfoResultSet = null;
|
|
|
|
for (byte i = 0; (i < WebBrowser.MaxTries) && (productInfoResultSet == null) && IsConnectedAndLoggedOn; i++) {
|
|
try {
|
|
productInfoResultSet = await SteamApps.PICSGetProductInfo(request.ToEnumerable(), Enumerable.Empty<SteamApps.PICSRequest>()).ToLongRunningTask().ConfigureAwait(false);
|
|
} catch (Exception e) {
|
|
ArchiLogger.LogGenericWarningException(e);
|
|
}
|
|
}
|
|
|
|
if (productInfoResultSet?.Results == null) {
|
|
return (optimisticDiscovery ? appID : 0, DateTime.MinValue, true);
|
|
}
|
|
|
|
foreach (Dictionary<uint, SteamApps.PICSProductInfoCallback.PICSProductInfo> productInfoApps in productInfoResultSet.Results.Select(result => result.Apps)) {
|
|
if (!productInfoApps.TryGetValue(appID, out SteamApps.PICSProductInfoCallback.PICSProductInfo productInfoApp)) {
|
|
continue;
|
|
}
|
|
|
|
KeyValue productInfo = productInfoApp.KeyValues;
|
|
|
|
if (productInfo == KeyValue.Invalid) {
|
|
ArchiLogger.LogNullError(nameof(productInfo));
|
|
|
|
break;
|
|
}
|
|
|
|
KeyValue commonProductInfo = productInfo["common"];
|
|
|
|
if (commonProductInfo == KeyValue.Invalid) {
|
|
continue;
|
|
}
|
|
|
|
string releaseState = commonProductInfo["ReleaseState"].Value;
|
|
|
|
if (!string.IsNullOrEmpty(releaseState)) {
|
|
// We must convert this to uppercase, since Valve doesn't stick to any convention and we can have a case mismatch
|
|
switch (releaseState.ToUpperInvariant()) {
|
|
case "RELEASED":
|
|
break;
|
|
case "PRELOADONLY":
|
|
case "PRERELEASE":
|
|
return (0, DateTime.MaxValue, true);
|
|
default:
|
|
ArchiLogger.LogGenericError(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(releaseState), releaseState));
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
string type = commonProductInfo["type"].Value;
|
|
|
|
if (string.IsNullOrEmpty(type)) {
|
|
return (appID, DateTime.MinValue, true);
|
|
}
|
|
|
|
// We must convert this to uppercase, since Valve doesn't stick to any convention and we can have a case mismatch
|
|
switch (type.ToUpperInvariant()) {
|
|
case "APPLICATION":
|
|
case "EPISODE":
|
|
case "GAME":
|
|
case "MOD":
|
|
case "MOVIE":
|
|
case "SERIES":
|
|
case "TOOL":
|
|
case "VIDEO":
|
|
// Types that can be idled
|
|
return (appID, DateTime.MinValue, true);
|
|
case "ADVERTISING":
|
|
case "DEMO":
|
|
case "DLC":
|
|
case "GUIDE":
|
|
case "HARDWARE":
|
|
case "MUSIC":
|
|
// Types that can't be idled
|
|
break;
|
|
default:
|
|
ArchiLogger.LogGenericError(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(type), type));
|
|
|
|
break;
|
|
}
|
|
|
|
if (!allowRecursiveDiscovery) {
|
|
return (0, DateTime.MinValue, true);
|
|
}
|
|
|
|
string listOfDlc = productInfo["extended"]["listofdlc"].Value;
|
|
|
|
if (string.IsNullOrEmpty(listOfDlc)) {
|
|
return (appID, DateTime.MinValue, true);
|
|
}
|
|
|
|
string[] dlcAppIDsTexts = listOfDlc.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
foreach (string dlcAppIDsText in dlcAppIDsTexts) {
|
|
if (!uint.TryParse(dlcAppIDsText, out uint dlcAppID) || (dlcAppID == 0)) {
|
|
ArchiLogger.LogNullError(nameof(dlcAppID));
|
|
|
|
break;
|
|
}
|
|
|
|
(uint playableAppID, _, _) = await GetAppDataForIdling(dlcAppID, hoursPlayed, false, false).ConfigureAwait(false);
|
|
|
|
if (playableAppID != 0) {
|
|
return (playableAppID, DateTime.MinValue, true);
|
|
}
|
|
}
|
|
|
|
return (appID, DateTime.MinValue, true);
|
|
}
|
|
|
|
return ((productInfoResultSet.Complete && !productInfoResultSet.Failed) || optimisticDiscovery ? appID : 0, DateTime.MinValue, true);
|
|
}
|
|
|
|
internal static string GetFilePath(string botName, EFileType fileType) {
|
|
if (string.IsNullOrEmpty(botName) || !Enum.IsDefined(typeof(EFileType), fileType)) {
|
|
ASF.ArchiLogger.LogNullError(nameof(botName) + " || " + nameof(fileType));
|
|
|
|
return null;
|
|
}
|
|
|
|
string botPath = Path.Combine(SharedInfo.ConfigDirectory, botName);
|
|
|
|
switch (fileType) {
|
|
case EFileType.Config:
|
|
return botPath + SharedInfo.JsonConfigExtension;
|
|
case EFileType.Database:
|
|
return botPath + SharedInfo.DatabaseExtension;
|
|
case EFileType.KeysToRedeem:
|
|
return botPath + SharedInfo.KeysExtension;
|
|
case EFileType.KeysToRedeemUnused:
|
|
return botPath + SharedInfo.KeysExtension + SharedInfo.KeysUnusedExtension;
|
|
case EFileType.KeysToRedeemUsed:
|
|
return botPath + SharedInfo.KeysExtension + SharedInfo.KeysUsedExtension;
|
|
case EFileType.MobileAuthenticator:
|
|
return botPath + SharedInfo.MobileAuthenticatorExtension;
|
|
case EFileType.SentryFile:
|
|
return botPath + SharedInfo.SentryHashExtension;
|
|
default:
|
|
ASF.ArchiLogger.LogGenericError(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(fileType), fileType));
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
[ItemCanBeNull]
|
|
internal async Task<HashSet<uint>> GetMarketableAppIDs() => await ArchiWebHandler.GetAppList().ConfigureAwait(false);
|
|
|
|
[ItemCanBeNull]
|
|
internal async Task<Dictionary<uint, (uint ChangeNumber, HashSet<uint> AppIDs)>> GetPackagesData(IReadOnlyCollection<uint> packageIDs) {
|
|
if ((packageIDs == null) || (packageIDs.Count == 0)) {
|
|
ArchiLogger.LogNullError(nameof(packageIDs));
|
|
|
|
return null;
|
|
}
|
|
|
|
HashSet<SteamApps.PICSRequest> packageRequests = new HashSet<SteamApps.PICSRequest>();
|
|
|
|
foreach (uint packageID in packageIDs) {
|
|
if (!ASF.GlobalDatabase.PackageAccessTokensReadOnly.TryGetValue(packageID, out ulong packageAccessToken)) {
|
|
continue;
|
|
}
|
|
|
|
packageRequests.Add(new SteamApps.PICSRequest(packageID, packageAccessToken, false));
|
|
}
|
|
|
|
if (packageRequests.Count == 0) {
|
|
return new Dictionary<uint, (uint ChangeNumber, HashSet<uint> AppIDs)>(0);
|
|
}
|
|
|
|
AsyncJobMultiple<SteamApps.PICSProductInfoCallback>.ResultSet productInfoResultSet = null;
|
|
|
|
for (byte i = 0; (i < WebBrowser.MaxTries) && (productInfoResultSet == null) && IsConnectedAndLoggedOn; i++) {
|
|
try {
|
|
productInfoResultSet = await SteamApps.PICSGetProductInfo(Enumerable.Empty<SteamApps.PICSRequest>(), packageRequests).ToLongRunningTask().ConfigureAwait(false);
|
|
} catch (Exception e) {
|
|
ArchiLogger.LogGenericWarningException(e);
|
|
}
|
|
}
|
|
|
|
if (productInfoResultSet?.Results == null) {
|
|
return null;
|
|
}
|
|
|
|
Dictionary<uint, (uint ChangeNumber, HashSet<uint> AppIDs)> result = new Dictionary<uint, (uint ChangeNumber, HashSet<uint> AppIDs)>();
|
|
|
|
foreach (SteamApps.PICSProductInfoCallback.PICSProductInfo productInfo in productInfoResultSet.Results.SelectMany(productInfoResult => productInfoResult.Packages).Where(productInfoPackages => productInfoPackages.Key != 0).Select(productInfoPackages => productInfoPackages.Value)) {
|
|
if (productInfo.KeyValues == KeyValue.Invalid) {
|
|
ArchiLogger.LogNullError(nameof(productInfo));
|
|
|
|
return null;
|
|
}
|
|
|
|
(uint ChangeNumber, HashSet<uint> AppIDs) value = (productInfo.ChangeNumber, null);
|
|
|
|
try {
|
|
KeyValue appIDs = productInfo.KeyValues["appids"];
|
|
|
|
if (appIDs == KeyValue.Invalid) {
|
|
continue;
|
|
}
|
|
|
|
value.AppIDs = new HashSet<uint>(appIDs.Children.Count);
|
|
|
|
foreach (string appIDText in appIDs.Children.Select(app => app.Value)) {
|
|
if (!uint.TryParse(appIDText, out uint appID) || (appID == 0)) {
|
|
ArchiLogger.LogNullError(nameof(appID));
|
|
|
|
return null;
|
|
}
|
|
|
|
value.AppIDs.Add(appID);
|
|
}
|
|
} finally {
|
|
result[productInfo.ID] = value;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
internal async Task<(Dictionary<string, string> UnusedKeys, Dictionary<string, string> UsedKeys)> GetUsedAndUnusedKeys() {
|
|
string unusedKeysFilePath = GetFilePath(EFileType.KeysToRedeemUnused);
|
|
|
|
if (string.IsNullOrEmpty(unusedKeysFilePath)) {
|
|
ASF.ArchiLogger.LogNullError(nameof(unusedKeysFilePath));
|
|
|
|
return (null, null);
|
|
}
|
|
|
|
string usedKeysFilePath = GetFilePath(EFileType.KeysToRedeemUsed);
|
|
|
|
if (string.IsNullOrEmpty(usedKeysFilePath)) {
|
|
ASF.ArchiLogger.LogNullError(nameof(usedKeysFilePath));
|
|
|
|
return (null, null);
|
|
}
|
|
|
|
string[] files = { unusedKeysFilePath, usedKeysFilePath };
|
|
|
|
IList<Dictionary<string, string>> results = await Utilities.InParallel(files.Select(GetKeysFromFile)).ConfigureAwait(false);
|
|
|
|
return (results[0], results[1]);
|
|
}
|
|
|
|
internal async Task<bool?> HasPublicInventory() {
|
|
if (!IsConnectedAndLoggedOn) {
|
|
return null;
|
|
}
|
|
|
|
CPrivacySettings privacySettings = await ArchiHandler.GetPrivacySettings().ConfigureAwait(false);
|
|
|
|
if (privacySettings == null) {
|
|
ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
|
|
|
return null;
|
|
}
|
|
|
|
return ((ArchiHandler.EPrivacySetting) privacySettings.privacy_state == ArchiHandler.EPrivacySetting.Public) && ((ArchiHandler.EPrivacySetting) privacySettings.privacy_state_inventory == ArchiHandler.EPrivacySetting.Public);
|
|
}
|
|
|
|
internal async Task IdleGame(CardsFarmer.Game game) {
|
|
if (game == null) {
|
|
ArchiLogger.LogNullError(nameof(game));
|
|
|
|
return;
|
|
}
|
|
|
|
string gameName = null;
|
|
|
|
if (!string.IsNullOrEmpty(BotConfig.CustomGamePlayedWhileFarming)) {
|
|
gameName = string.Format(BotConfig.CustomGamePlayedWhileFarming, game.AppID, game.GameName);
|
|
}
|
|
|
|
await ArchiHandler.PlayGames(game.PlayableAppID.ToEnumerable(), gameName).ConfigureAwait(false);
|
|
}
|
|
|
|
internal async Task IdleGames(IReadOnlyCollection<CardsFarmer.Game> games) {
|
|
if ((games == null) || (games.Count == 0)) {
|
|
ArchiLogger.LogNullError(nameof(games));
|
|
|
|
return;
|
|
}
|
|
|
|
string gameName = null;
|
|
|
|
if (!string.IsNullOrEmpty(BotConfig.CustomGamePlayedWhileFarming)) {
|
|
gameName = string.Format(BotConfig.CustomGamePlayedWhileFarming, string.Join(", ", games.Select(game => game.AppID)), string.Join(", ", games.Select(game => game.GameName)));
|
|
}
|
|
|
|
await ArchiHandler.PlayGames(games.Select(game => game.PlayableAppID), gameName).ConfigureAwait(false);
|
|
}
|
|
|
|
internal async Task ImportKeysToRedeem(string filePath) {
|
|
if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath)) {
|
|
ArchiLogger.LogNullError(nameof(filePath));
|
|
|
|
return;
|
|
}
|
|
|
|
try {
|
|
OrderedDictionary gamesToRedeemInBackground = new OrderedDictionary();
|
|
|
|
using (StreamReader reader = new StreamReader(filePath)) {
|
|
string line;
|
|
|
|
while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null) {
|
|
if (line.Length == 0) {
|
|
continue;
|
|
}
|
|
|
|
// Valid formats:
|
|
// Key (name will be the same as key and replaced from redemption result, if possible)
|
|
// Name + Key (user provides both, if name is equal to key, above logic is used, otherwise name is kept)
|
|
// Name + <Ignored> + Key (BGR output format, we include extra properties in the middle, those are ignored during import)
|
|
string[] parsedArgs = line.Split(DefaultBackgroundKeysRedeemerSeparator, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
if (parsedArgs.Length < 1) {
|
|
ArchiLogger.LogGenericWarning(string.Format(Strings.ErrorIsInvalid, line));
|
|
|
|
continue;
|
|
}
|
|
|
|
string name = parsedArgs[0];
|
|
string key = parsedArgs[^1];
|
|
|
|
gamesToRedeemInBackground[key] = name;
|
|
}
|
|
}
|
|
|
|
if (gamesToRedeemInBackground.Count > 0) {
|
|
IOrderedDictionary validGamesToRedeemInBackground = ValidateGamesToRedeemInBackground(gamesToRedeemInBackground);
|
|
|
|
if ((validGamesToRedeemInBackground != null) && (validGamesToRedeemInBackground.Count > 0)) {
|
|
AddGamesToRedeemInBackground(validGamesToRedeemInBackground);
|
|
}
|
|
}
|
|
|
|
File.Delete(filePath);
|
|
} catch (Exception e) {
|
|
ArchiLogger.LogGenericException(e);
|
|
}
|
|
}
|
|
|
|
internal static void Init(StringComparer botsComparer) {
|
|
if (botsComparer == null) {
|
|
ASF.ArchiLogger.LogNullError(nameof(botsComparer));
|
|
|
|
return;
|
|
}
|
|
|
|
if (Bots != null) {
|
|
ASF.ArchiLogger.LogGenericError(Strings.WarningFailed);
|
|
|
|
return;
|
|
}
|
|
|
|
BotsComparer = botsComparer;
|
|
Bots = new ConcurrentDictionary<string, Bot>(botsComparer);
|
|
}
|
|
|
|
internal bool IsBlacklistedFromIdling(uint appID) {
|
|
if (appID == 0) {
|
|
ArchiLogger.LogNullError(nameof(appID));
|
|
|
|
return false;
|
|
}
|
|
|
|
return BotDatabase.IsBlacklistedFromIdling(appID);
|
|
}
|
|
|
|
internal bool IsBlacklistedFromTrades(ulong steamID) {
|
|
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
|
|
ArchiLogger.LogNullError(nameof(steamID));
|
|
|
|
return false;
|
|
}
|
|
|
|
return BotDatabase.IsBlacklistedFromTrades(steamID);
|
|
}
|
|
|
|
internal bool IsPriorityIdling(uint appID) {
|
|
if (appID == 0) {
|
|
ArchiLogger.LogNullError(nameof(appID));
|
|
|
|
return false;
|
|
}
|
|
|
|
return BotDatabase.IsPriorityIdling(appID);
|
|
}
|
|
|
|
internal async Task OnConfigChanged(bool deleted) {
|
|
if (deleted) {
|
|
await Destroy().ConfigureAwait(false);
|
|
|
|
return;
|
|
}
|
|
|
|
string configFile = GetFilePath(EFileType.Config);
|
|
|
|
if (string.IsNullOrEmpty(configFile)) {
|
|
ArchiLogger.LogNullError(nameof(configFile));
|
|
|
|
return;
|
|
}
|
|
|
|
BotConfig botConfig = await BotConfig.Load(configFile).ConfigureAwait(false);
|
|
|
|
if (botConfig == null) {
|
|
await Destroy().ConfigureAwait(false);
|
|
|
|
return;
|
|
}
|
|
|
|
if (botConfig == BotConfig) {
|
|
return;
|
|
}
|
|
|
|
await InitializationSemaphore.WaitAsync().ConfigureAwait(false);
|
|
|
|
try {
|
|
if (botConfig == BotConfig) {
|
|
return;
|
|
}
|
|
|
|
Stop(botConfig.Enabled);
|
|
BotConfig = botConfig;
|
|
|
|
await InitModules().ConfigureAwait(false);
|
|
InitStart();
|
|
} finally {
|
|
InitializationSemaphore.Release();
|
|
}
|
|
}
|
|
|
|
internal async Task OnFarmingFinished(bool farmedSomething) {
|
|
await OnFarmingStopped().ConfigureAwait(false);
|
|
|
|
if (BotConfig.SendOnFarmingFinished && (BotConfig.LootableTypes.Count > 0) && (farmedSomething || !FirstTradeSent)) {
|
|
FirstTradeSent = true;
|
|
|
|
await Actions.SendInventory(filterFunction: item => BotConfig.LootableTypes.Contains(item.Type)).ConfigureAwait(false);
|
|
}
|
|
|
|
if (BotConfig.ShutdownOnFarmingFinished) {
|
|
Stop();
|
|
}
|
|
|
|
await PluginsCore.OnBotFarmingFinished(this, farmedSomething).ConfigureAwait(false);
|
|
}
|
|
|
|
internal async Task OnFarmingStopped() {
|
|
await ResetGamesPlayed().ConfigureAwait(false);
|
|
await PluginsCore.OnBotFarmingStopped(this).ConfigureAwait(false);
|
|
}
|
|
|
|
internal async Task<bool> RefreshSession() {
|
|
if (!IsConnectedAndLoggedOn) {
|
|
return false;
|
|
}
|
|
|
|
SteamUser.WebAPIUserNonceCallback callback;
|
|
|
|
try {
|
|
callback = await SteamUser.RequestWebAPIUserNonce().ToLongRunningTask().ConfigureAwait(false);
|
|
} catch (Exception e) {
|
|
ArchiLogger.LogGenericWarningException(e);
|
|
await Connect(true).ConfigureAwait(false);
|
|
|
|
return false;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(callback.Nonce)) {
|
|
await Connect(true).ConfigureAwait(false);
|
|
|
|
return false;
|
|
}
|
|
|
|
if (await ArchiWebHandler.Init(SteamID, SteamClient.Universe, callback.Nonce, SteamParentalActive ? BotConfig.SteamParentalCode : null).ConfigureAwait(false)) {
|
|
return true;
|
|
}
|
|
|
|
await Connect(true).ConfigureAwait(false);
|
|
|
|
return false;
|
|
}
|
|
|
|
internal static async Task RegisterBot(string botName) {
|
|
if (string.IsNullOrEmpty(botName)) {
|
|
ASF.ArchiLogger.LogNullError(nameof(botName));
|
|
|
|
return;
|
|
}
|
|
|
|
if (Bots.ContainsKey(botName)) {
|
|
return;
|
|
}
|
|
|
|
string configFilePath = GetFilePath(botName, EFileType.Config);
|
|
|
|
if (string.IsNullOrEmpty(configFilePath)) {
|
|
ASF.ArchiLogger.LogNullError(nameof(configFilePath));
|
|
|
|
return;
|
|
}
|
|
|
|
BotConfig botConfig = await BotConfig.Load(configFilePath).ConfigureAwait(false);
|
|
|
|
if (botConfig == null) {
|
|
ASF.ArchiLogger.LogGenericError(string.Format(Strings.ErrorBotConfigInvalid, configFilePath));
|
|
|
|
return;
|
|
}
|
|
|
|
if (Debugging.IsDebugConfigured) {
|
|
ASF.ArchiLogger.LogGenericDebug(configFilePath + ": " + JsonConvert.SerializeObject(botConfig, Formatting.Indented));
|
|
}
|
|
|
|
string databaseFilePath = GetFilePath(botName, EFileType.Database);
|
|
|
|
if (string.IsNullOrEmpty(databaseFilePath)) {
|
|
ASF.ArchiLogger.LogNullError(nameof(databaseFilePath));
|
|
|
|
return;
|
|
}
|
|
|
|
BotDatabase botDatabase = await BotDatabase.CreateOrLoad(databaseFilePath).ConfigureAwait(false);
|
|
|
|
if (botDatabase == null) {
|
|
ASF.ArchiLogger.LogGenericError(string.Format(Strings.ErrorDatabaseInvalid, databaseFilePath));
|
|
|
|
return;
|
|
}
|
|
|
|
if (Debugging.IsDebugConfigured) {
|
|
ASF.ArchiLogger.LogGenericDebug(databaseFilePath + ": " + JsonConvert.SerializeObject(botDatabase, Formatting.Indented));
|
|
}
|
|
|
|
Bot bot;
|
|
|
|
await BotsSemaphore.WaitAsync().ConfigureAwait(false);
|
|
|
|
try {
|
|
if (Bots.ContainsKey(botName)) {
|
|
return;
|
|
}
|
|
|
|
bot = new Bot(botName, botConfig, botDatabase);
|
|
|
|
if (!Bots.TryAdd(botName, bot)) {
|
|
ASF.ArchiLogger.LogNullError(nameof(bot));
|
|
|
|
await bot.DisposeAsync().ConfigureAwait(false);
|
|
|
|
return;
|
|
}
|
|
} finally {
|
|
BotsSemaphore.Release();
|
|
}
|
|
|
|
await PluginsCore.OnBotInit(bot).ConfigureAwait(false);
|
|
|
|
HashSet<ClientMsgHandler> customHandlers = await PluginsCore.OnBotSteamHandlersInit(bot).ConfigureAwait(false);
|
|
|
|
if ((customHandlers != null) && (customHandlers.Count > 0)) {
|
|
foreach (ClientMsgHandler customHandler in customHandlers) {
|
|
bot.SteamClient.AddHandler(customHandler);
|
|
}
|
|
}
|
|
|
|
await PluginsCore.OnBotSteamCallbacksInit(bot, bot.CallbackManager).ConfigureAwait(false);
|
|
|
|
await bot.InitModules().ConfigureAwait(false);
|
|
|
|
bot.InitStart();
|
|
}
|
|
|
|
internal async Task<bool> Rename(string newBotName) {
|
|
if (string.IsNullOrEmpty(newBotName)) {
|
|
ArchiLogger.LogNullError(nameof(newBotName));
|
|
|
|
return false;
|
|
}
|
|
|
|
if (newBotName.Equals(SharedInfo.ASF) || Bots.ContainsKey(newBotName)) {
|
|
return false;
|
|
}
|
|
|
|
if (KeepRunning) {
|
|
Stop(true);
|
|
}
|
|
|
|
await BotDatabase.MakeReadOnly().ConfigureAwait(false);
|
|
|
|
// We handle the config file last as it'll trigger new bot creation
|
|
foreach ((string filePath, EFileType fileType) in RelatedFiles.Where(file => File.Exists(file.FilePath)).OrderByDescending(file => file.FileType != EFileType.Config)) {
|
|
string newFilePath = GetFilePath(newBotName, fileType);
|
|
|
|
if (string.IsNullOrEmpty(newFilePath)) {
|
|
ArchiLogger.LogNullError(nameof(newFilePath));
|
|
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
File.Move(filePath, newFilePath);
|
|
} catch (Exception e) {
|
|
ArchiLogger.LogGenericException(e);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
internal void RequestPersonaStateUpdate() {
|
|
if (!IsConnectedAndLoggedOn) {
|
|
return;
|
|
}
|
|
|
|
SteamFriends.RequestFriendInfo(SteamID, EClientPersonaStateFlag.PlayerName | EClientPersonaStateFlag.Presence);
|
|
}
|
|
|
|
internal async Task<bool> SendMessage(ulong steamID, string message) {
|
|
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount || string.IsNullOrEmpty(message)) {
|
|
ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(message));
|
|
|
|
return false;
|
|
}
|
|
|
|
if (!IsConnectedAndLoggedOn) {
|
|
return false;
|
|
}
|
|
|
|
ArchiLogger.LogChatMessage(true, message, steamID: steamID);
|
|
|
|
ushort maxMessageLength = (ushort) (MaxMessageLength - ReservedMessageLength - (ASF.GlobalConfig.SteamMessagePrefix?.Length ?? 0));
|
|
|
|
// We must escape our message prior to sending it
|
|
message = Escape(message);
|
|
|
|
int i = 0;
|
|
|
|
while (i < message.Length) {
|
|
int partLength;
|
|
bool copyNewline = false;
|
|
|
|
// ReSharper disable ArrangeMissingParentheses - conflict with Roslyn
|
|
if (message.Length - i > maxMessageLength) {
|
|
int lastNewLine = message.LastIndexOf(Environment.NewLine, i + maxMessageLength - Environment.NewLine.Length, maxMessageLength - Environment.NewLine.Length, StringComparison.Ordinal);
|
|
|
|
if (lastNewLine > i) {
|
|
partLength = lastNewLine - i + Environment.NewLine.Length;
|
|
copyNewline = true;
|
|
} else {
|
|
partLength = maxMessageLength;
|
|
}
|
|
} else {
|
|
partLength = message.Length - i;
|
|
}
|
|
|
|
// If our message is of max length and ends with a single '\' then we can't split it here, it escapes the next character
|
|
if ((partLength >= maxMessageLength) && (message[i + partLength - 1] == '\\') && (message[i + partLength - 2] != '\\')) {
|
|
// Instead, we'll cut this message one char short and include the rest in next iteration
|
|
partLength--;
|
|
}
|
|
|
|
// ReSharper restore ArrangeMissingParentheses
|
|
string messagePart = message.Substring(i, partLength);
|
|
|
|
messagePart = ASF.GlobalConfig.SteamMessagePrefix + (i > 0 ? "…" : "") + messagePart + (maxMessageLength < message.Length - i ? "…" : "");
|
|
|
|
await MessagingSemaphore.WaitAsync().ConfigureAwait(false);
|
|
|
|
try {
|
|
bool sent = false;
|
|
|
|
for (byte j = 0; (j < WebBrowser.MaxTries) && !sent && IsConnectedAndLoggedOn; j++) {
|
|
// We add a one-second delay here to avoid Steam screwup in form of a ghost notification
|
|
// The exact cause is unknown, but the theory is that Steam is confused when dealing with more than 1 message per second from the same user
|
|
await Task.Delay(1000).ConfigureAwait(false);
|
|
|
|
EResult result = await ArchiHandler.SendMessage(steamID, messagePart).ConfigureAwait(false);
|
|
|
|
switch (result) {
|
|
case EResult.Busy:
|
|
case EResult.Fail:
|
|
case EResult.RateLimitExceeded:
|
|
case EResult.ServiceUnavailable:
|
|
case EResult.Timeout:
|
|
await Task.Delay(5000).ConfigureAwait(false);
|
|
|
|
continue;
|
|
case EResult.OK:
|
|
sent = true;
|
|
|
|
break;
|
|
default:
|
|
ArchiLogger.LogGenericError(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(result), result));
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!sent) {
|
|
ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
|
|
|
return false;
|
|
}
|
|
} finally {
|
|
MessagingSemaphore.Release();
|
|
}
|
|
|
|
i += partLength - (copyNewline ? Environment.NewLine.Length : 0);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
internal async Task<bool> SendMessage(ulong chatGroupID, ulong chatID, string message) {
|
|
if ((chatGroupID == 0) || (chatID == 0) || string.IsNullOrEmpty(message)) {
|
|
ArchiLogger.LogNullError(nameof(chatGroupID) + " || " + nameof(chatID) + " || " + nameof(message));
|
|
|
|
return false;
|
|
}
|
|
|
|
if (!IsConnectedAndLoggedOn) {
|
|
return false;
|
|
}
|
|
|
|
ArchiLogger.LogChatMessage(true, message, chatGroupID, chatID);
|
|
|
|
ushort maxMessageLength = (ushort) (MaxMessageLength - ReservedMessageLength - (ASF.GlobalConfig.SteamMessagePrefix?.Length ?? 0));
|
|
|
|
// We must escape our message prior to sending it
|
|
message = Escape(message);
|
|
|
|
int i = 0;
|
|
|
|
// ReSharper disable ArrangeMissingParentheses - conflict with Roslyn
|
|
while (i < message.Length) {
|
|
int partLength;
|
|
bool copyNewline = false;
|
|
|
|
if (message.Length - i > maxMessageLength) {
|
|
int lastNewLine = message.LastIndexOf(Environment.NewLine, i + maxMessageLength - Environment.NewLine.Length, maxMessageLength - Environment.NewLine.Length, StringComparison.Ordinal);
|
|
|
|
if (lastNewLine > i) {
|
|
partLength = lastNewLine - i + Environment.NewLine.Length;
|
|
copyNewline = true;
|
|
} else {
|
|
partLength = maxMessageLength;
|
|
}
|
|
} else {
|
|
partLength = message.Length - i;
|
|
}
|
|
|
|
// If our message is of max length and ends with a single '\' then we can't split it here, it escapes the next character
|
|
if ((partLength >= maxMessageLength) && (message[i + partLength - 1] == '\\') && (message[i + partLength - 2] != '\\')) {
|
|
// Instead, we'll cut this message one char short and include the rest in next iteration
|
|
partLength--;
|
|
}
|
|
|
|
// ReSharper restore ArrangeMissingParentheses
|
|
string messagePart = message.Substring(i, partLength);
|
|
|
|
messagePart = ASF.GlobalConfig.SteamMessagePrefix + (i > 0 ? "…" : "") + messagePart + (maxMessageLength < message.Length - i ? "…" : "");
|
|
|
|
await MessagingSemaphore.WaitAsync().ConfigureAwait(false);
|
|
|
|
try {
|
|
bool sent = false;
|
|
|
|
for (byte j = 0; (j < WebBrowser.MaxTries) && !sent && IsConnectedAndLoggedOn; j++) {
|
|
EResult result = await ArchiHandler.SendMessage(chatGroupID, chatID, messagePart).ConfigureAwait(false);
|
|
|
|
switch (result) {
|
|
case EResult.Busy:
|
|
case EResult.Fail:
|
|
case EResult.RateLimitExceeded:
|
|
case EResult.ServiceUnavailable:
|
|
case EResult.Timeout:
|
|
await Task.Delay(5000).ConfigureAwait(false);
|
|
|
|
continue;
|
|
case EResult.OK:
|
|
sent = true;
|
|
|
|
break;
|
|
default:
|
|
ArchiLogger.LogGenericError(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(result), result));
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!sent) {
|
|
ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
|
|
|
return false;
|
|
}
|
|
} finally {
|
|
MessagingSemaphore.Release();
|
|
}
|
|
|
|
i += partLength - (copyNewline ? Environment.NewLine.Length : 0);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
internal async Task<bool> SendTypingMessage(ulong steamID) {
|
|
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
|
|
ArchiLogger.LogNullError(nameof(steamID));
|
|
|
|
return false;
|
|
}
|
|
|
|
if (!IsConnectedAndLoggedOn) {
|
|
return false;
|
|
}
|
|
|
|
return await ArchiHandler.SendTypingStatus(steamID).ConfigureAwait(false) == EResult.OK;
|
|
}
|
|
|
|
internal async Task Start() {
|
|
if (KeepRunning) {
|
|
return;
|
|
}
|
|
|
|
KeepRunning = true;
|
|
Utilities.InBackground(HandleCallbacks, true);
|
|
ArchiLogger.LogGenericInfo(Strings.Starting);
|
|
|
|
// Support and convert 2FA files
|
|
if (!HasMobileAuthenticator) {
|
|
string mobileAuthenticatorFilePath = GetFilePath(EFileType.MobileAuthenticator);
|
|
|
|
if (string.IsNullOrEmpty(mobileAuthenticatorFilePath)) {
|
|
ArchiLogger.LogNullError(nameof(mobileAuthenticatorFilePath));
|
|
|
|
return;
|
|
}
|
|
|
|
if (File.Exists(mobileAuthenticatorFilePath)) {
|
|
await ImportAuthenticator(mobileAuthenticatorFilePath).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
string keysToRedeemFilePath = GetFilePath(EFileType.KeysToRedeem);
|
|
|
|
if (string.IsNullOrEmpty(keysToRedeemFilePath)) {
|
|
ArchiLogger.LogNullError(nameof(keysToRedeemFilePath));
|
|
|
|
return;
|
|
}
|
|
|
|
if (File.Exists(keysToRedeemFilePath)) {
|
|
await ImportKeysToRedeem(keysToRedeemFilePath).ConfigureAwait(false);
|
|
}
|
|
|
|
await Connect().ConfigureAwait(false);
|
|
}
|
|
|
|
internal void Stop(bool skipShutdownEvent = false) {
|
|
if (!KeepRunning) {
|
|
return;
|
|
}
|
|
|
|
KeepRunning = false;
|
|
ArchiLogger.LogGenericInfo(Strings.BotStopping);
|
|
|
|
if (SteamClient.IsConnected) {
|
|
Disconnect();
|
|
}
|
|
|
|
if (!skipShutdownEvent) {
|
|
Utilities.InBackground(Events.OnBotShutdown);
|
|
}
|
|
}
|
|
|
|
internal static IOrderedDictionary ValidateGamesToRedeemInBackground(IOrderedDictionary gamesToRedeemInBackground) {
|
|
if ((gamesToRedeemInBackground == null) || (gamesToRedeemInBackground.Count == 0)) {
|
|
ASF.ArchiLogger.LogNullError(nameof(gamesToRedeemInBackground));
|
|
|
|
return null;
|
|
}
|
|
|
|
HashSet<object> invalidKeys = new HashSet<object>();
|
|
|
|
foreach (DictionaryEntry game in gamesToRedeemInBackground) {
|
|
bool invalid = false;
|
|
|
|
string key = game.Key as string;
|
|
|
|
if (string.IsNullOrEmpty(key)) {
|
|
invalid = true;
|
|
ASF.ArchiLogger.LogGenericWarning(string.Format(Strings.ErrorIsInvalid, nameof(key)));
|
|
} else if (!Utilities.IsValidCdKey(key)) {
|
|
invalid = true;
|
|
ASF.ArchiLogger.LogGenericWarning(string.Format(Strings.ErrorIsInvalid, key));
|
|
}
|
|
|
|
string name = game.Value as string;
|
|
|
|
if (string.IsNullOrEmpty(name)) {
|
|
invalid = true;
|
|
ASF.ArchiLogger.LogGenericWarning(string.Format(Strings.ErrorIsInvalid, nameof(name)));
|
|
}
|
|
|
|
if (invalid) {
|
|
invalidKeys.Add(game.Key);
|
|
}
|
|
}
|
|
|
|
if (invalidKeys.Count > 0) {
|
|
foreach (string invalidKey in invalidKeys) {
|
|
gamesToRedeemInBackground.Remove(invalidKey);
|
|
}
|
|
}
|
|
|
|
return gamesToRedeemInBackground;
|
|
}
|
|
|
|
private async Task CheckOccupationStatus() {
|
|
StopPlayingWasBlockedTimer();
|
|
|
|
if (!IsPlayingPossible) {
|
|
PlayingWasBlocked = true;
|
|
ArchiLogger.LogGenericInfo(Strings.BotAccountOccupied);
|
|
|
|
return;
|
|
}
|
|
|
|
if (PlayingWasBlocked && (PlayingWasBlockedTimer == null)) {
|
|
InitPlayingWasBlockedTimer();
|
|
}
|
|
|
|
ArchiLogger.LogGenericInfo(Strings.BotAccountFree);
|
|
|
|
if (!await CardsFarmer.Resume(false).ConfigureAwait(false)) {
|
|
await ResetGamesPlayed().ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
private async Task Connect(bool force = false) {
|
|
if (!force && (!KeepRunning || SteamClient.IsConnected)) {
|
|
return;
|
|
}
|
|
|
|
await LimitLoginRequestsAsync().ConfigureAwait(false);
|
|
|
|
if (!force && (!KeepRunning || SteamClient.IsConnected)) {
|
|
return;
|
|
}
|
|
|
|
ArchiLogger.LogGenericInfo(Strings.BotConnecting);
|
|
InitConnectionFailureTimer();
|
|
SteamClient.Connect();
|
|
}
|
|
|
|
private async Task Destroy(bool force = false) {
|
|
if (KeepRunning) {
|
|
if (!force) {
|
|
Stop();
|
|
} else {
|
|
// Stop() will most likely block due to connection freeze, don't wait for it
|
|
Utilities.InBackground(() => Stop());
|
|
}
|
|
}
|
|
|
|
Bots.TryRemove(BotName, out _);
|
|
await PluginsCore.OnBotDestroy(this).ConfigureAwait(false);
|
|
}
|
|
|
|
private void Disconnect() {
|
|
StopConnectionFailureTimer();
|
|
SteamClient.Disconnect();
|
|
}
|
|
|
|
private static string Escape(string message) {
|
|
if (string.IsNullOrEmpty(message)) {
|
|
ASF.ArchiLogger.LogNullError(nameof(message));
|
|
|
|
return null;
|
|
}
|
|
|
|
return message.Replace("\\", "\\\\").Replace("[", "\\[");
|
|
}
|
|
|
|
private string GetFilePath(EFileType fileType) {
|
|
if (!Enum.IsDefined(typeof(EFileType), fileType)) {
|
|
ASF.ArchiLogger.LogNullError(nameof(fileType));
|
|
|
|
return null;
|
|
}
|
|
|
|
return GetFilePath(BotName, fileType);
|
|
}
|
|
|
|
[ItemCanBeNull]
|
|
private async Task<Dictionary<string, string>> GetKeysFromFile(string filePath) {
|
|
if (string.IsNullOrEmpty(filePath)) {
|
|
ArchiLogger.LogNullError(nameof(filePath));
|
|
|
|
return null;
|
|
}
|
|
|
|
if (!File.Exists(filePath)) {
|
|
return new Dictionary<string, string>(0, StringComparer.Ordinal);
|
|
}
|
|
|
|
Dictionary<string, string> keys = new Dictionary<string, string>(StringComparer.Ordinal);
|
|
|
|
try {
|
|
using StreamReader reader = new StreamReader(filePath);
|
|
|
|
string line;
|
|
|
|
while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null) {
|
|
if (line.Length == 0) {
|
|
continue;
|
|
}
|
|
|
|
string[] parsedArgs = line.Split(DefaultBackgroundKeysRedeemerSeparator, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
if (parsedArgs.Length < 3) {
|
|
ArchiLogger.LogGenericWarning(string.Format(Strings.ErrorIsInvalid, line));
|
|
|
|
continue;
|
|
}
|
|
|
|
string key = parsedArgs[^1];
|
|
|
|
if (!Utilities.IsValidCdKey(key)) {
|
|
ArchiLogger.LogGenericWarning(string.Format(Strings.ErrorIsInvalid, key));
|
|
|
|
continue;
|
|
}
|
|
|
|
string name = parsedArgs[0];
|
|
keys[key] = name;
|
|
}
|
|
} catch (Exception e) {
|
|
ArchiLogger.LogGenericException(e);
|
|
|
|
return null;
|
|
}
|
|
|
|
return keys;
|
|
}
|
|
|
|
private void HandleCallbacks() {
|
|
TimeSpan timeSpan = TimeSpan.FromMilliseconds(CallbackSleep);
|
|
|
|
while (KeepRunning || SteamClient.IsConnected) {
|
|
if (!CallbackSemaphore.Wait(0)) {
|
|
if (Debugging.IsUserDebugging) {
|
|
ArchiLogger.LogGenericDebug(string.Format(Strings.WarningFailedWithError, nameof(CallbackSemaphore)));
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
try {
|
|
CallbackManager.RunWaitAllCallbacks(timeSpan);
|
|
} catch (Exception e) {
|
|
ArchiLogger.LogGenericException(e);
|
|
} finally {
|
|
CallbackSemaphore.Release();
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task HeartBeat() {
|
|
if (!KeepRunning || !IsConnectedAndLoggedOn || (HeartBeatFailures == byte.MaxValue)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (DateTime.UtcNow.Subtract(ArchiHandler.LastPacketReceived).TotalSeconds > ASF.GlobalConfig.ConnectionTimeout) {
|
|
await SteamFriends.RequestProfileInfo(SteamID).ToLongRunningTask().ConfigureAwait(false);
|
|
}
|
|
|
|
HeartBeatFailures = 0;
|
|
|
|
if (Statistics != null) {
|
|
Utilities.InBackground(Statistics.OnHeartBeat);
|
|
}
|
|
} catch (Exception e) {
|
|
ArchiLogger.LogGenericDebuggingException(e);
|
|
|
|
if (!KeepRunning || !IsConnectedAndLoggedOn || (HeartBeatFailures == byte.MaxValue)) {
|
|
return;
|
|
}
|
|
|
|
if (++HeartBeatFailures >= (byte) Math.Ceiling(ASF.GlobalConfig.ConnectionTimeout / 10.0)) {
|
|
HeartBeatFailures = byte.MaxValue;
|
|
ArchiLogger.LogGenericWarning(Strings.BotConnectionLost);
|
|
Utilities.InBackground(() => Connect(true));
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task ImportAuthenticator(string maFilePath) {
|
|
if (HasMobileAuthenticator || !File.Exists(maFilePath)) {
|
|
return;
|
|
}
|
|
|
|
ArchiLogger.LogGenericInfo(Strings.BotAuthenticatorConverting);
|
|
|
|
try {
|
|
string json = await RuntimeCompatibility.File.ReadAllTextAsync(maFilePath).ConfigureAwait(false);
|
|
|
|
if (string.IsNullOrEmpty(json)) {
|
|
ArchiLogger.LogGenericError(string.Format(Strings.ErrorIsEmpty, nameof(json)));
|
|
|
|
return;
|
|
}
|
|
|
|
MobileAuthenticator authenticator = JsonConvert.DeserializeObject<MobileAuthenticator>(json);
|
|
|
|
if (authenticator == null) {
|
|
ArchiLogger.LogNullError(nameof(authenticator));
|
|
|
|
return;
|
|
}
|
|
|
|
authenticator.Init(this);
|
|
BotDatabase.MobileAuthenticator = authenticator;
|
|
|
|
File.Delete(maFilePath);
|
|
} catch (Exception e) {
|
|
ArchiLogger.LogGenericException(e);
|
|
|
|
return;
|
|
}
|
|
|
|
ArchiLogger.LogGenericInfo(Strings.BotAuthenticatorImportFinished);
|
|
}
|
|
|
|
private void InitConnectionFailureTimer() {
|
|
if (ConnectionFailureTimer != null) {
|
|
return;
|
|
}
|
|
|
|
ConnectionFailureTimer = new Timer(
|
|
async e => await InitPermanentConnectionFailure().ConfigureAwait(false),
|
|
null,
|
|
TimeSpan.FromMinutes(Math.Ceiling(ASF.GlobalConfig.ConnectionTimeout / 30.0)), // Delay
|
|
Timeout.InfiniteTimeSpan // Period
|
|
);
|
|
}
|
|
|
|
private async Task InitializeFamilySharing() {
|
|
HashSet<ulong> steamIDs = await ArchiWebHandler.GetFamilySharingSteamIDs().ConfigureAwait(false);
|
|
|
|
if (steamIDs == null) {
|
|
return;
|
|
}
|
|
|
|
SteamFamilySharingIDs.ReplaceWith(steamIDs);
|
|
}
|
|
|
|
private async Task<bool> InitLoginAndPassword(bool requiresPassword) {
|
|
if (string.IsNullOrEmpty(BotConfig.SteamLogin)) {
|
|
RequiredInput = ASF.EUserInputType.Login;
|
|
|
|
string steamLogin = await Logging.GetUserInput(ASF.EUserInputType.Login, BotName).ConfigureAwait(false);
|
|
|
|
if (string.IsNullOrEmpty(steamLogin) || !SetUserInput(ASF.EUserInputType.Login, steamLogin)) {
|
|
ArchiLogger.LogGenericError(string.Format(Strings.ErrorIsInvalid, nameof(steamLogin)));
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (requiresPassword && string.IsNullOrEmpty(BotConfig.DecryptedSteamPassword)) {
|
|
RequiredInput = ASF.EUserInputType.Password;
|
|
|
|
string steamPassword = await Logging.GetUserInput(ASF.EUserInputType.Password, BotName).ConfigureAwait(false);
|
|
|
|
if (string.IsNullOrEmpty(steamPassword) || !SetUserInput(ASF.EUserInputType.Password, steamPassword)) {
|
|
ArchiLogger.LogGenericError(string.Format(Strings.ErrorIsInvalid, nameof(steamPassword)));
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private async Task InitModules() {
|
|
AccountFlags = EAccountFlags.NormalUser;
|
|
AvatarHash = Nickname = null;
|
|
MasterChatGroupID = 0;
|
|
RequiredInput = ASF.EUserInputType.None;
|
|
WalletBalance = 0;
|
|
WalletCurrency = ECurrencyCode.Invalid;
|
|
|
|
CardsFarmer.SetInitialState(BotConfig.Paused);
|
|
|
|
if (SendItemsTimer != null) {
|
|
await SendItemsTimer.DisposeAsync().ConfigureAwait(false);
|
|
|
|
SendItemsTimer = null;
|
|
}
|
|
|
|
if ((BotConfig.SendTradePeriod > 0) && (BotConfig.LootableTypes.Count > 0) && BotConfig.SteamUserPermissions.Values.Any(permission => permission >= BotConfig.EPermission.Master)) {
|
|
SendItemsTimer = new Timer(
|
|
async e => await Actions.SendInventory(filterFunction: item => BotConfig.LootableTypes.Contains(item.Type)).ConfigureAwait(false),
|
|
null,
|
|
TimeSpan.FromHours(BotConfig.SendTradePeriod) + TimeSpan.FromSeconds(ASF.LoadBalancingDelay * Bots.Count), // Delay
|
|
TimeSpan.FromHours(BotConfig.SendTradePeriod) // Period
|
|
);
|
|
}
|
|
|
|
if (SteamSaleEvent != null) {
|
|
await SteamSaleEvent.DisposeAsync().ConfigureAwait(false);
|
|
|
|
SteamSaleEvent = null;
|
|
}
|
|
|
|
if (BotConfig.AutoSteamSaleEvent) {
|
|
SteamSaleEvent = new SteamSaleEvent(this);
|
|
}
|
|
|
|
await PluginsCore.OnBotInitModules(this, BotConfig.AdditionalProperties).ConfigureAwait(false);
|
|
}
|
|
|
|
private async Task InitPermanentConnectionFailure() {
|
|
if (!KeepRunning) {
|
|
return;
|
|
}
|
|
|
|
ArchiLogger.LogGenericWarning(Strings.BotHeartBeatFailed);
|
|
await Destroy(true).ConfigureAwait(false);
|
|
await RegisterBot(BotName).ConfigureAwait(false);
|
|
}
|
|
|
|
private void InitPlayingWasBlockedTimer() {
|
|
if (PlayingWasBlockedTimer != null) {
|
|
return;
|
|
}
|
|
|
|
PlayingWasBlockedTimer = new Timer(
|
|
e => ResetPlayingWasBlockedWithTimer(),
|
|
null,
|
|
TimeSpan.FromSeconds(MinPlayingBlockedTTL), // Delay
|
|
Timeout.InfiniteTimeSpan // Period
|
|
);
|
|
}
|
|
|
|
private void InitStart() {
|
|
if (!BotConfig.Enabled) {
|
|
ArchiLogger.LogGenericInfo(Strings.BotInstanceNotStartingBecauseDisabled);
|
|
|
|
return;
|
|
}
|
|
|
|
// Start
|
|
Utilities.InBackground(Start);
|
|
}
|
|
|
|
private bool IsMasterClanID(ulong steamID) {
|
|
if ((steamID == 0) || !new SteamID(steamID).IsClanAccount) {
|
|
ArchiLogger.LogNullError(nameof(steamID));
|
|
|
|
return false;
|
|
}
|
|
|
|
return steamID == BotConfig.SteamMasterClanID;
|
|
}
|
|
|
|
private static bool IsRefundable(EPaymentMethod paymentMethod) {
|
|
if (paymentMethod == EPaymentMethod.None) {
|
|
ASF.ArchiLogger.LogNullError(nameof(paymentMethod));
|
|
|
|
return false;
|
|
}
|
|
|
|
// Complimentary is also a flag
|
|
return paymentMethod switch {
|
|
EPaymentMethod.ActivationCode => false,
|
|
EPaymentMethod.Complimentary => false,
|
|
EPaymentMethod.GuestPass => false,
|
|
EPaymentMethod.HardwarePromo => false,
|
|
_ => !paymentMethod.HasFlag(EPaymentMethod.Complimentary)
|
|
};
|
|
}
|
|
|
|
private async Task JoinMasterChatGroupID() {
|
|
if ((BotConfig.SteamMasterClanID == 0) || IsAccountLimited) {
|
|
return;
|
|
}
|
|
|
|
if (MasterChatGroupID == 0) {
|
|
ulong chatGroupID = await ArchiHandler.GetClanChatGroupID(BotConfig.SteamMasterClanID).ConfigureAwait(false);
|
|
|
|
if (chatGroupID == 0) {
|
|
return;
|
|
}
|
|
|
|
MasterChatGroupID = chatGroupID;
|
|
}
|
|
|
|
HashSet<ulong> chatGroupIDs = await ArchiHandler.GetMyChatGroupIDs().ConfigureAwait(false);
|
|
|
|
if (chatGroupIDs?.Contains(MasterChatGroupID) != false) {
|
|
return;
|
|
}
|
|
|
|
if (!await ArchiHandler.JoinChatRoomGroup(MasterChatGroupID).ConfigureAwait(false)) {
|
|
ArchiLogger.LogGenericWarning(string.Format(Strings.WarningFailedWithError, nameof(ArchiHandler.JoinChatRoomGroup)));
|
|
}
|
|
}
|
|
|
|
private static async Task LimitLoginRequestsAsync() {
|
|
if ((ASF.LoginSemaphore == null) || (ASF.LoginRateLimitingSemaphore == null)) {
|
|
ASF.ArchiLogger.LogNullError(nameof(ASF.LoginSemaphore) + " || " + nameof(ASF.LoginRateLimitingSemaphore));
|
|
|
|
return;
|
|
}
|
|
|
|
if (ASF.GlobalConfig.LoginLimiterDelay == 0) {
|
|
await ASF.LoginRateLimitingSemaphore.WaitAsync().ConfigureAwait(false);
|
|
ASF.LoginRateLimitingSemaphore.Release();
|
|
|
|
return;
|
|
}
|
|
|
|
await ASF.LoginSemaphore.WaitAsync().ConfigureAwait(false);
|
|
|
|
try {
|
|
await ASF.LoginRateLimitingSemaphore.WaitAsync().ConfigureAwait(false);
|
|
ASF.LoginRateLimitingSemaphore.Release();
|
|
} finally {
|
|
Utilities.InBackground(
|
|
async () => {
|
|
await Task.Delay(ASF.GlobalConfig.LoginLimiterDelay * 1000).ConfigureAwait(false);
|
|
ASF.LoginSemaphore.Release();
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
private async void OnConnected(SteamClient.ConnectedCallback callback) {
|
|
if (callback == null) {
|
|
ArchiLogger.LogNullError(nameof(callback));
|
|
|
|
return;
|
|
}
|
|
|
|
HeartBeatFailures = 0;
|
|
ReconnectOnUserInitiated = false;
|
|
StopConnectionFailureTimer();
|
|
|
|
ArchiLogger.LogGenericInfo(Strings.BotConnected);
|
|
|
|
if (!KeepRunning) {
|
|
ArchiLogger.LogGenericInfo(Strings.BotDisconnecting);
|
|
Disconnect();
|
|
|
|
return;
|
|
}
|
|
|
|
string sentryFilePath = GetFilePath(EFileType.SentryFile);
|
|
|
|
if (string.IsNullOrEmpty(sentryFilePath)) {
|
|
ArchiLogger.LogNullError(nameof(sentryFilePath));
|
|
|
|
return;
|
|
}
|
|
|
|
byte[] sentryFileHash = null;
|
|
|
|
if (File.Exists(sentryFilePath)) {
|
|
try {
|
|
byte[] sentryFileContent = await RuntimeCompatibility.File.ReadAllBytesAsync(sentryFilePath).ConfigureAwait(false);
|
|
sentryFileHash = CryptoHelper.SHAHash(sentryFileContent);
|
|
} catch (Exception e) {
|
|
ArchiLogger.LogGenericException(e);
|
|
|
|
try {
|
|
File.Delete(sentryFilePath);
|
|
} catch {
|
|
// Ignored, we can only try to delete faulted file at best
|
|
}
|
|
}
|
|
}
|
|
|
|
string loginKey = null;
|
|
|
|
if (BotConfig.UseLoginKeys) {
|
|
// Login keys are not guaranteed to be valid, we should use them only if we don't have full details available from the user
|
|
if (string.IsNullOrEmpty(BotConfig.DecryptedSteamPassword) || (string.IsNullOrEmpty(AuthCode) && string.IsNullOrEmpty(TwoFactorCode) && !HasMobileAuthenticator)) {
|
|
loginKey = BotDatabase.LoginKey;
|
|
|
|
// Decrypt login key if needed
|
|
if (!string.IsNullOrEmpty(loginKey) && (loginKey.Length > 19) && (BotConfig.PasswordFormat != ArchiCryptoHelper.ECryptoMethod.PlainText)) {
|
|
loginKey = ArchiCryptoHelper.Decrypt(BotConfig.PasswordFormat, loginKey);
|
|
}
|
|
}
|
|
} else {
|
|
// If we're not using login keys, ensure we don't have any saved
|
|
BotDatabase.LoginKey = null;
|
|
}
|
|
|
|
if (!await InitLoginAndPassword(string.IsNullOrEmpty(loginKey)).ConfigureAwait(false)) {
|
|
Stop();
|
|
|
|
return;
|
|
}
|
|
|
|
// Steam login and password fields can contain ASCII characters only, including spaces
|
|
const string nonAsciiPattern = @"[^\u0000-\u007F]+";
|
|
|
|
string username = Regex.Replace(BotConfig.SteamLogin, nonAsciiPattern, "", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
|
|
string password = BotConfig.DecryptedSteamPassword;
|
|
|
|
if (!string.IsNullOrEmpty(password)) {
|
|
password = Regex.Replace(password, nonAsciiPattern, "", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
|
|
}
|
|
|
|
ArchiLogger.LogGenericInfo(Strings.BotLoggingIn);
|
|
|
|
if (string.IsNullOrEmpty(TwoFactorCode) && HasMobileAuthenticator) {
|
|
// We should always include 2FA token, even if it's not required
|
|
TwoFactorCode = await BotDatabase.MobileAuthenticator.GenerateToken().ConfigureAwait(false);
|
|
}
|
|
|
|
InitConnectionFailureTimer();
|
|
|
|
SteamUser.LogOnDetails logOnDetails = new SteamUser.LogOnDetails {
|
|
AuthCode = AuthCode,
|
|
CellID = ASF.GlobalDatabase.CellID,
|
|
LoginID = LoginID,
|
|
LoginKey = loginKey,
|
|
Password = password,
|
|
SentryFileHash = sentryFileHash,
|
|
ShouldRememberPassword = BotConfig.UseLoginKeys,
|
|
TwoFactorCode = TwoFactorCode,
|
|
Username = username
|
|
};
|
|
|
|
if (OSType == EOSType.Unknown) {
|
|
OSType = logOnDetails.ClientOSType;
|
|
}
|
|
|
|
SteamUser.LogOn(logOnDetails);
|
|
}
|
|
|
|
private async void OnDisconnected(SteamClient.DisconnectedCallback callback) {
|
|
if (callback == null) {
|
|
ArchiLogger.LogNullError(nameof(callback));
|
|
|
|
return;
|
|
}
|
|
|
|
if (ASF.LoginRateLimitingSemaphore == null) {
|
|
ASF.ArchiLogger.LogNullError(nameof(ASF.LoginRateLimitingSemaphore));
|
|
|
|
return;
|
|
}
|
|
|
|
EResult lastLogOnResult = LastLogOnResult;
|
|
LastLogOnResult = EResult.Invalid;
|
|
HeartBeatFailures = 0;
|
|
SteamParentalActive = true;
|
|
StopConnectionFailureTimer();
|
|
StopPlayingWasBlockedTimer();
|
|
|
|
ArchiLogger.LogGenericInfo(Strings.BotDisconnected);
|
|
|
|
PastNotifications.Clear();
|
|
|
|
Actions.OnDisconnected();
|
|
ArchiWebHandler.OnDisconnected();
|
|
CardsFarmer.OnDisconnected();
|
|
Trading.OnDisconnected();
|
|
|
|
FirstTradeSent = false;
|
|
|
|
await PluginsCore.OnBotDisconnected(this, callback.UserInitiated ? EResult.OK : lastLogOnResult).ConfigureAwait(false);
|
|
|
|
// If we initiated disconnect, do not attempt to reconnect
|
|
if (callback.UserInitiated && !ReconnectOnUserInitiated) {
|
|
return;
|
|
}
|
|
|
|
switch (lastLogOnResult) {
|
|
case EResult.AccountDisabled:
|
|
// Do not attempt to reconnect, those failures are permanent
|
|
return;
|
|
case EResult.InvalidPassword when !string.IsNullOrEmpty(BotDatabase.LoginKey):
|
|
BotDatabase.LoginKey = null;
|
|
ArchiLogger.LogGenericInfo(Strings.BotRemovedExpiredLoginKey);
|
|
|
|
break;
|
|
case EResult.InvalidPassword:
|
|
case EResult.NoConnection:
|
|
case EResult.ServiceUnavailable:
|
|
case EResult.Timeout:
|
|
case EResult.TryAnotherCM:
|
|
case EResult.TwoFactorCodeMismatch:
|
|
await Task.Delay(5000).ConfigureAwait(false);
|
|
|
|
break;
|
|
case EResult.RateLimitExceeded:
|
|
ArchiLogger.LogGenericInfo(string.Format(Strings.BotRateLimitExceeded, TimeSpan.FromMinutes(LoginCooldownInMinutes).ToHumanReadable()));
|
|
|
|
if (!await ASF.LoginRateLimitingSemaphore.WaitAsync(1000 * WebBrowser.MaxTries).ConfigureAwait(false)) {
|
|
break;
|
|
}
|
|
|
|
try {
|
|
await Task.Delay(LoginCooldownInMinutes * 60 * 1000).ConfigureAwait(false);
|
|
} finally {
|
|
ASF.LoginRateLimitingSemaphore.Release();
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
if (!KeepRunning || SteamClient.IsConnected) {
|
|
return;
|
|
}
|
|
|
|
ArchiLogger.LogGenericInfo(Strings.BotReconnecting);
|
|
await Connect().ConfigureAwait(false);
|
|
}
|
|
|
|
private async void OnFriendsList(SteamFriends.FriendsListCallback callback) {
|
|
if (callback?.FriendList == null) {
|
|
ArchiLogger.LogNullError(nameof(callback) + " || " + nameof(callback.FriendList));
|
|
|
|
return;
|
|
}
|
|
|
|
foreach (SteamFriends.FriendsListCallback.Friend friend in callback.FriendList.Where(friend => friend.Relationship == EFriendRelationship.RequestRecipient)) {
|
|
switch (friend.SteamID.AccountType) {
|
|
case EAccountType.Clan when IsMasterClanID(friend.SteamID):
|
|
ArchiHandler.AcknowledgeClanInvite(friend.SteamID, true);
|
|
await JoinMasterChatGroupID().ConfigureAwait(false);
|
|
|
|
break;
|
|
case EAccountType.Clan:
|
|
bool acceptGroupRequest = await PluginsCore.OnBotFriendRequest(this, friend.SteamID).ConfigureAwait(false);
|
|
|
|
if (acceptGroupRequest) {
|
|
ArchiHandler.AcknowledgeClanInvite(friend.SteamID, true);
|
|
await JoinMasterChatGroupID().ConfigureAwait(false);
|
|
|
|
break;
|
|
}
|
|
|
|
if (BotConfig.BotBehaviour.HasFlag(BotConfig.EBotBehaviour.RejectInvalidGroupInvites)) {
|
|
ArchiHandler.AcknowledgeClanInvite(friend.SteamID, false);
|
|
}
|
|
|
|
break;
|
|
default:
|
|
if (HasPermission(friend.SteamID, BotConfig.EPermission.FamilySharing)) {
|
|
if (!await ArchiHandler.AddFriend(friend.SteamID).ConfigureAwait(false)) {
|
|
ArchiLogger.LogGenericWarning(string.Format(Strings.WarningFailedWithError, nameof(ArchiHandler.AddFriend)));
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
bool acceptFriendRequest = await PluginsCore.OnBotFriendRequest(this, friend.SteamID).ConfigureAwait(false);
|
|
|
|
if (acceptFriendRequest) {
|
|
if (!await ArchiHandler.AddFriend(friend.SteamID).ConfigureAwait(false)) {
|
|
ArchiLogger.LogGenericWarning(string.Format(Strings.WarningFailedWithError, nameof(ArchiHandler.AddFriend)));
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
if (BotConfig.BotBehaviour.HasFlag(BotConfig.EBotBehaviour.RejectInvalidFriendInvites)) {
|
|
if (!await ArchiHandler.RemoveFriend(friend.SteamID).ConfigureAwait(false)) {
|
|
ArchiLogger.LogGenericWarning(string.Format(Strings.WarningFailedWithError, nameof(ArchiHandler.RemoveFriend)));
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private async void OnGuestPassList(SteamApps.GuestPassListCallback callback) {
|
|
if (callback?.GuestPasses == null) {
|
|
ArchiLogger.LogNullError(nameof(callback) + " || " + nameof(callback.GuestPasses));
|
|
|
|
return;
|
|
}
|
|
|
|
if ((callback.CountGuestPassesToRedeem == 0) || (callback.GuestPasses.Count == 0) || !BotConfig.AcceptGifts) {
|
|
return;
|
|
}
|
|
|
|
HashSet<ulong> guestPassIDs = callback.GuestPasses.Select(guestPass => guestPass["gid"].AsUnsignedLong()).Where(gid => gid != 0).ToHashSet();
|
|
|
|
if (guestPassIDs.Count == 0) {
|
|
return;
|
|
}
|
|
|
|
await Actions.AcceptGuestPasses(guestPassIDs).ConfigureAwait(false);
|
|
}
|
|
|
|
private async Task OnIncomingChatMessage(CChatRoom_IncomingChatMessage_Notification notification) {
|
|
if (notification == null) {
|
|
ArchiLogger.LogNullError(nameof(notification));
|
|
|
|
return;
|
|
}
|
|
|
|
// Under normal circumstances, timestamp must always be greater than 0, but Steam already proved that it's capable of going against the logic
|
|
if ((notification.steamid_sender != SteamID) && (notification.timestamp > 0)) {
|
|
if (ShouldAckChatMessage(notification.steamid_sender)) {
|
|
Utilities.InBackground(() => ArchiHandler.AckChatMessage(notification.chat_group_id, notification.chat_id, notification.timestamp));
|
|
}
|
|
}
|
|
|
|
string message;
|
|
|
|
// Prefer to use message without bbcode, but only if it's available
|
|
if (!string.IsNullOrEmpty(notification.message_no_bbcode)) {
|
|
message = notification.message_no_bbcode;
|
|
} else if (!string.IsNullOrEmpty(notification.message)) {
|
|
message = UnEscape(notification.message);
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
ArchiLogger.LogChatMessage(false, message, notification.chat_group_id, notification.chat_id, notification.steamid_sender);
|
|
|
|
// Steam network broadcasts chat events also when we don't explicitly sign into Steam community
|
|
// We'll explicitly ignore those messages when using offline mode, as it was done in the first version of Steam chat when no messages were broadcasted at all before signing in
|
|
// Handling messages will still work correctly in invisible mode, which is how it should work in the first place
|
|
// This goes in addition to usual logic that ignores irrelevant messages from being parsed further
|
|
if ((notification.chat_group_id != MasterChatGroupID) || (BotConfig.OnlineStatus == EPersonaState.Offline)) {
|
|
return;
|
|
}
|
|
|
|
await Commands.HandleMessage(notification.chat_group_id, notification.chat_id, notification.steamid_sender, message).ConfigureAwait(false);
|
|
}
|
|
|
|
private async Task OnIncomingMessage(CFriendMessages_IncomingMessage_Notification notification) {
|
|
if (notification == null) {
|
|
ArchiLogger.LogNullError(nameof(notification));
|
|
|
|
return;
|
|
}
|
|
|
|
if ((EChatEntryType) notification.chat_entry_type != EChatEntryType.ChatMsg) {
|
|
return;
|
|
}
|
|
|
|
// Under normal circumstances, timestamp must always be greater than 0, but Steam already proved that it's capable of going against the logic
|
|
if (!notification.local_echo && (notification.rtime32_server_timestamp > 0)) {
|
|
if (ShouldAckChatMessage(notification.steamid_friend)) {
|
|
Utilities.InBackground(() => ArchiHandler.AckMessage(notification.steamid_friend, notification.rtime32_server_timestamp));
|
|
}
|
|
}
|
|
|
|
string message;
|
|
|
|
// Prefer to use message without bbcode, but only if it's available
|
|
if (!string.IsNullOrEmpty(notification.message_no_bbcode)) {
|
|
message = notification.message_no_bbcode;
|
|
} else if (!string.IsNullOrEmpty(notification.message)) {
|
|
message = UnEscape(notification.message);
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
ArchiLogger.LogChatMessage(notification.local_echo, message, steamID: notification.steamid_friend);
|
|
|
|
// Steam network broadcasts chat events also when we don't explicitly sign into Steam community
|
|
// We'll explicitly ignore those messages when using offline mode, as it was done in the first version of Steam chat when no messages were broadcasted at all before signing in
|
|
// Handling messages will still work correctly in invisible mode, which is how it should work in the first place
|
|
// This goes in addition to usual logic that ignores irrelevant messages from being parsed further
|
|
if (notification.local_echo || (BotConfig.OnlineStatus == EPersonaState.Offline)) {
|
|
return;
|
|
}
|
|
|
|
await Commands.HandleMessage(notification.steamid_friend, message).ConfigureAwait(false);
|
|
}
|
|
|
|
private async void OnLicenseList(SteamApps.LicenseListCallback callback) {
|
|
if (callback?.LicenseList == null) {
|
|
ArchiLogger.LogNullError(nameof(callback) + " || " + nameof(callback.LicenseList));
|
|
|
|
return;
|
|
}
|
|
|
|
if (callback.LicenseList.Count == 0) {
|
|
ArchiLogger.LogGenericError(string.Format(Strings.ErrorIsEmpty, nameof(callback.LicenseList)));
|
|
|
|
return;
|
|
}
|
|
|
|
Commands.OnNewLicenseList();
|
|
|
|
Dictionary<uint, (EPaymentMethod PaymentMethod, DateTime TimeCreated)> ownedPackageIDs = new Dictionary<uint, (EPaymentMethod PaymentMethod, DateTime TimeCreated)>();
|
|
|
|
Dictionary<uint, ulong> packageAccessTokens = new Dictionary<uint, ulong>();
|
|
Dictionary<uint, uint> packagesToRefresh = new Dictionary<uint, uint>();
|
|
|
|
foreach (SteamApps.LicenseListCallback.License license in callback.LicenseList.GroupBy(license => license.PackageID, (packageID, licenses) => licenses.OrderByDescending(license => license.TimeCreated).First())) {
|
|
ownedPackageIDs[license.PackageID] = (license.PaymentMethod, license.TimeCreated);
|
|
|
|
if (!ASF.GlobalDatabase.PackageAccessTokensReadOnly.TryGetValue(license.PackageID, out ulong packageAccessToken) || (packageAccessToken != license.AccessToken)) {
|
|
packageAccessTokens[license.PackageID] = license.AccessToken;
|
|
|
|
// Package is always due to refresh with access token change
|
|
packagesToRefresh[license.PackageID] = (uint) license.LastChangeNumber;
|
|
} else if (!ASF.GlobalDatabase.PackagesDataReadOnly.TryGetValue(license.PackageID, out (uint ChangeNumber, HashSet<uint> AppIDs) packageData) || (packageData.ChangeNumber < license.LastChangeNumber)) {
|
|
packagesToRefresh[license.PackageID] = (uint) license.LastChangeNumber;
|
|
}
|
|
}
|
|
|
|
OwnedPackageIDs = ownedPackageIDs.ToImmutableDictionary();
|
|
|
|
if (packageAccessTokens.Count > 0) {
|
|
ASF.GlobalDatabase.RefreshPackageAccessTokens(packageAccessTokens);
|
|
}
|
|
|
|
if (packagesToRefresh.Count > 0) {
|
|
ArchiLogger.LogGenericTrace(Strings.BotRefreshingPackagesData);
|
|
await ASF.GlobalDatabase.RefreshPackages(this, packagesToRefresh).ConfigureAwait(false);
|
|
ArchiLogger.LogGenericTrace(Strings.Done);
|
|
}
|
|
|
|
await CardsFarmer.OnNewGameAdded().ConfigureAwait(false);
|
|
}
|
|
|
|
private void OnLoggedOff(SteamUser.LoggedOffCallback callback) {
|
|
if (callback == null) {
|
|
ArchiLogger.LogNullError(nameof(callback));
|
|
|
|
return;
|
|
}
|
|
|
|
LastLogOnResult = callback.Result;
|
|
|
|
ArchiLogger.LogGenericInfo(string.Format(Strings.BotLoggedOff, callback.Result));
|
|
|
|
switch (callback.Result) {
|
|
case EResult.LoggedInElsewhere:
|
|
// This result directly indicates that playing was blocked when we got (forcefully) disconnected
|
|
PlayingWasBlocked = true;
|
|
|
|
break;
|
|
case EResult.LogonSessionReplaced:
|
|
DateTime now = DateTime.UtcNow;
|
|
|
|
if (now.Subtract(LastLogonSessionReplaced).TotalHours < 1) {
|
|
ArchiLogger.LogGenericError(Strings.BotLogonSessionReplaced);
|
|
Stop();
|
|
|
|
return;
|
|
}
|
|
|
|
LastLogonSessionReplaced = now;
|
|
|
|
break;
|
|
}
|
|
|
|
ReconnectOnUserInitiated = true;
|
|
SteamClient.Disconnect();
|
|
}
|
|
|
|
private async void OnLoggedOn(SteamUser.LoggedOnCallback callback) {
|
|
if (callback == null) {
|
|
ArchiLogger.LogNullError(nameof(callback));
|
|
|
|
return;
|
|
}
|
|
|
|
// Always reset one-time-only access tokens when we get OnLoggedOn() response
|
|
AuthCode = TwoFactorCode = null;
|
|
|
|
// Keep LastLogOnResult for OnDisconnected()
|
|
LastLogOnResult = callback.Result;
|
|
|
|
HeartBeatFailures = 0;
|
|
StopConnectionFailureTimer();
|
|
|
|
switch (callback.Result) {
|
|
case EResult.AccountDisabled:
|
|
case EResult.InvalidPassword when string.IsNullOrEmpty(BotDatabase.LoginKey):
|
|
// Those failures are permanent, we should Stop() the bot if any of those happen
|
|
ArchiLogger.LogGenericWarning(string.Format(Strings.BotUnableToLogin, callback.Result, callback.ExtendedResult));
|
|
Stop();
|
|
|
|
break;
|
|
case EResult.AccountLogonDenied:
|
|
RequiredInput = ASF.EUserInputType.SteamGuard;
|
|
|
|
string authCode = await Logging.GetUserInput(ASF.EUserInputType.SteamGuard, BotName).ConfigureAwait(false);
|
|
|
|
if (string.IsNullOrEmpty(authCode) || !SetUserInput(ASF.EUserInputType.SteamGuard, authCode)) {
|
|
ArchiLogger.LogGenericError(string.Format(Strings.ErrorIsInvalid, nameof(authCode)));
|
|
|
|
Stop();
|
|
}
|
|
|
|
break;
|
|
case EResult.AccountLoginDeniedNeedTwoFactor:
|
|
if (!HasMobileAuthenticator) {
|
|
RequiredInput = ASF.EUserInputType.TwoFactorAuthentication;
|
|
|
|
string twoFactorCode = await Logging.GetUserInput(ASF.EUserInputType.TwoFactorAuthentication, BotName).ConfigureAwait(false);
|
|
|
|
if (string.IsNullOrEmpty(twoFactorCode) || !SetUserInput(ASF.EUserInputType.TwoFactorAuthentication, twoFactorCode)) {
|
|
ArchiLogger.LogGenericError(string.Format(Strings.ErrorIsInvalid, nameof(twoFactorCode)));
|
|
|
|
Stop();
|
|
}
|
|
}
|
|
|
|
break;
|
|
case EResult.OK:
|
|
AccountFlags = callback.AccountFlags;
|
|
SteamID = callback.ClientSteamID;
|
|
|
|
ArchiLogger.LogGenericInfo(string.Format(Strings.BotLoggedOn, SteamID + (!string.IsNullOrEmpty(callback.VanityURL) ? "/" + callback.VanityURL : "")));
|
|
|
|
// Old status for these doesn't matter, we'll update them if needed
|
|
InvalidPasswordFailures = TwoFactorCodeFailures = 0;
|
|
LibraryLocked = PlayingBlocked = false;
|
|
|
|
if (PlayingWasBlocked && (PlayingWasBlockedTimer == null)) {
|
|
InitPlayingWasBlockedTimer();
|
|
}
|
|
|
|
if (IsAccountLimited) {
|
|
ArchiLogger.LogGenericWarning(Strings.BotAccountLimited);
|
|
}
|
|
|
|
if (IsAccountLocked) {
|
|
ArchiLogger.LogGenericWarning(Strings.BotAccountLocked);
|
|
}
|
|
|
|
if ((callback.CellID != 0) && (callback.CellID != ASF.GlobalDatabase.CellID)) {
|
|
ASF.GlobalDatabase.CellID = callback.CellID;
|
|
}
|
|
|
|
// Handle steamID-based maFile
|
|
if (!HasMobileAuthenticator) {
|
|
string maFilePath = Path.Combine(SharedInfo.ConfigDirectory, SteamID + SharedInfo.MobileAuthenticatorExtension);
|
|
|
|
if (File.Exists(maFilePath)) {
|
|
await ImportAuthenticator(maFilePath).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
if (callback.ParentalSettings != null) {
|
|
(bool isSteamParentalEnabled, string steamParentalCode) = ValidateSteamParental(callback.ParentalSettings, BotConfig.SteamParentalCode);
|
|
|
|
if (isSteamParentalEnabled) {
|
|
SteamParentalActive = true;
|
|
|
|
if (!string.IsNullOrEmpty(steamParentalCode)) {
|
|
if (BotConfig.SteamParentalCode != steamParentalCode) {
|
|
if (!SetUserInput(ASF.EUserInputType.SteamParentalCode, steamParentalCode)) {
|
|
ArchiLogger.LogGenericError(string.Format(Strings.ErrorIsInvalid, nameof(steamParentalCode)));
|
|
|
|
Stop();
|
|
|
|
break;
|
|
}
|
|
}
|
|
} else if (string.IsNullOrEmpty(BotConfig.SteamParentalCode) || (BotConfig.SteamParentalCode.Length != BotConfig.SteamParentalCodeLength)) {
|
|
RequiredInput = ASF.EUserInputType.SteamParentalCode;
|
|
|
|
steamParentalCode = await Logging.GetUserInput(ASF.EUserInputType.SteamParentalCode, BotName).ConfigureAwait(false);
|
|
|
|
if (string.IsNullOrEmpty(steamParentalCode) || !SetUserInput(ASF.EUserInputType.SteamParentalCode, steamParentalCode)) {
|
|
ArchiLogger.LogGenericError(string.Format(Strings.ErrorIsInvalid, nameof(steamParentalCode)));
|
|
|
|
Stop();
|
|
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
SteamParentalActive = false;
|
|
}
|
|
} else if (SteamParentalActive && !string.IsNullOrEmpty(BotConfig.SteamParentalCode) && (BotConfig.SteamParentalCode.Length != BotConfig.SteamParentalCodeLength)) {
|
|
RequiredInput = ASF.EUserInputType.SteamParentalCode;
|
|
|
|
string steamParentalCode = await Logging.GetUserInput(ASF.EUserInputType.SteamParentalCode, BotName).ConfigureAwait(false);
|
|
|
|
if (string.IsNullOrEmpty(steamParentalCode) || !SetUserInput(ASF.EUserInputType.SteamParentalCode, steamParentalCode)) {
|
|
ArchiLogger.LogGenericError(string.Format(Strings.ErrorIsInvalid, nameof(steamParentalCode)));
|
|
|
|
Stop();
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
ArchiWebHandler.OnVanityURLChanged(callback.VanityURL);
|
|
|
|
if (!await ArchiWebHandler.Init(SteamID, SteamClient.Universe, callback.WebAPIUserNonce, SteamParentalActive ? BotConfig.SteamParentalCode : null).ConfigureAwait(false)) {
|
|
if (!await RefreshSession().ConfigureAwait(false)) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Pre-fetch API key for future usage if possible
|
|
Utilities.InBackground(ArchiWebHandler.HasValidApiKey);
|
|
|
|
if ((GamesRedeemerInBackgroundTimer == null) && BotDatabase.HasGamesToRedeemInBackground) {
|
|
Utilities.InBackground(RedeemGamesInBackground);
|
|
}
|
|
|
|
ArchiHandler.SetCurrentMode(2);
|
|
ArchiHandler.RequestItemAnnouncements();
|
|
|
|
// Sometimes Steam won't send us our own PersonaStateCallback, so request it explicitly
|
|
RequestPersonaStateUpdate();
|
|
|
|
Utilities.InBackground(InitializeFamilySharing);
|
|
|
|
if (Statistics != null) {
|
|
Utilities.InBackground(Statistics.OnLoggedOn);
|
|
}
|
|
|
|
if (BotConfig.OnlineStatus != EPersonaState.Offline) {
|
|
SteamFriends.SetPersonaState(BotConfig.OnlineStatus);
|
|
}
|
|
|
|
if (BotConfig.SteamMasterClanID != 0) {
|
|
Utilities.InBackground(
|
|
async () => {
|
|
if (!await ArchiWebHandler.JoinGroup(BotConfig.SteamMasterClanID).ConfigureAwait(false)) {
|
|
ArchiLogger.LogGenericWarning(string.Format(Strings.WarningFailedWithError, nameof(ArchiWebHandler.JoinGroup)));
|
|
}
|
|
|
|
await JoinMasterChatGroupID().ConfigureAwait(false);
|
|
}
|
|
);
|
|
}
|
|
|
|
if (CardsFarmer.Paused) {
|
|
// Emit initial game playing status in this case
|
|
Utilities.InBackground(ResetGamesPlayed);
|
|
}
|
|
|
|
SteamPICSChanges.OnBotLoggedOn();
|
|
|
|
await PluginsCore.OnBotLoggedOn(this).ConfigureAwait(false);
|
|
|
|
break;
|
|
case EResult.InvalidPassword:
|
|
case EResult.NoConnection:
|
|
case EResult.PasswordRequiredToKickSession: // Not sure about this one, it seems to be just generic "try again"? #694
|
|
case EResult.RateLimitExceeded:
|
|
case EResult.ServiceUnavailable:
|
|
case EResult.Timeout:
|
|
case EResult.TryAnotherCM:
|
|
case EResult.TwoFactorCodeMismatch:
|
|
ArchiLogger.LogGenericWarning(string.Format(Strings.BotUnableToLogin, callback.Result, callback.ExtendedResult));
|
|
|
|
switch (callback.Result) {
|
|
case EResult.InvalidPassword when string.IsNullOrEmpty(BotDatabase.LoginKey) && (++InvalidPasswordFailures >= MaxInvalidPasswordFailures):
|
|
InvalidPasswordFailures = 0;
|
|
ArchiLogger.LogGenericError(string.Format(Strings.BotInvalidPasswordDuringLogin, MaxInvalidPasswordFailures));
|
|
Stop();
|
|
|
|
break;
|
|
case EResult.TwoFactorCodeMismatch when HasMobileAuthenticator && (++TwoFactorCodeFailures >= MaxTwoFactorCodeFailures):
|
|
TwoFactorCodeFailures = 0;
|
|
ArchiLogger.LogGenericError(string.Format(Strings.BotInvalidAuthenticatorDuringLogin, MaxTwoFactorCodeFailures));
|
|
Stop();
|
|
|
|
break;
|
|
}
|
|
|
|
break;
|
|
default:
|
|
// Unexpected result, shutdown immediately
|
|
ArchiLogger.LogGenericError(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(callback.Result), callback.Result));
|
|
ArchiLogger.LogGenericError(string.Format(Strings.BotUnableToLogin, callback.Result, callback.ExtendedResult));
|
|
Stop();
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void OnLoginKey(SteamUser.LoginKeyCallback callback) {
|
|
if (callback == null) {
|
|
ArchiLogger.LogNullError(nameof(callback));
|
|
|
|
return;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(callback.LoginKey)) {
|
|
ArchiLogger.LogNullError(nameof(callback.LoginKey));
|
|
|
|
return;
|
|
}
|
|
|
|
if (!BotConfig.UseLoginKeys) {
|
|
return;
|
|
}
|
|
|
|
string loginKey = callback.LoginKey;
|
|
|
|
if (BotConfig.PasswordFormat != ArchiCryptoHelper.ECryptoMethod.PlainText) {
|
|
loginKey = ArchiCryptoHelper.Encrypt(BotConfig.PasswordFormat, loginKey);
|
|
}
|
|
|
|
BotDatabase.LoginKey = loginKey;
|
|
SteamUser.AcceptNewLoginKey(callback);
|
|
}
|
|
|
|
private async void OnMachineAuth(SteamUser.UpdateMachineAuthCallback callback) {
|
|
if (callback == null) {
|
|
ArchiLogger.LogNullError(nameof(callback));
|
|
|
|
return;
|
|
}
|
|
|
|
string sentryFilePath = GetFilePath(EFileType.SentryFile);
|
|
|
|
if (string.IsNullOrEmpty(sentryFilePath)) {
|
|
ArchiLogger.LogNullError(nameof(sentryFilePath));
|
|
|
|
return;
|
|
}
|
|
|
|
long fileSize;
|
|
byte[] sentryHash;
|
|
|
|
try {
|
|
#if !NETFRAMEWORK
|
|
await
|
|
#endif
|
|
using FileStream fileStream = File.Open(sentryFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite);
|
|
|
|
fileStream.Seek(callback.Offset, SeekOrigin.Begin);
|
|
|
|
await fileStream.WriteAsync(callback.Data, 0, callback.BytesToWrite).ConfigureAwait(false);
|
|
|
|
fileSize = fileStream.Length;
|
|
fileStream.Seek(0, SeekOrigin.Begin);
|
|
|
|
using SHA1CryptoServiceProvider sha = new SHA1CryptoServiceProvider();
|
|
|
|
sentryHash = sha.ComputeHash(fileStream);
|
|
} catch (Exception e) {
|
|
ArchiLogger.LogGenericException(e);
|
|
|
|
try {
|
|
File.Delete(sentryFilePath);
|
|
} catch {
|
|
// Ignored, we can only try to delete faulted file at best
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Inform the steam servers that we're accepting this sentry file
|
|
SteamUser.SendMachineAuthResponse(
|
|
new SteamUser.MachineAuthDetails {
|
|
BytesWritten = callback.BytesToWrite,
|
|
FileName = callback.FileName,
|
|
FileSize = (int) fileSize,
|
|
JobID = callback.JobID,
|
|
LastError = 0,
|
|
Offset = callback.Offset,
|
|
OneTimePassword = callback.OneTimePassword,
|
|
Result = EResult.OK,
|
|
SentryFileHash = sentryHash
|
|
}
|
|
);
|
|
}
|
|
|
|
private void OnPersonaState(SteamFriends.PersonaStateCallback callback) {
|
|
if (callback == null) {
|
|
ArchiLogger.LogNullError(nameof(callback));
|
|
|
|
return;
|
|
}
|
|
|
|
if (callback.FriendID != SteamID) {
|
|
return;
|
|
}
|
|
|
|
string avatarHash = null;
|
|
|
|
if ((callback.AvatarHash.Length > 0) && callback.AvatarHash.Any(singleByte => singleByte != 0)) {
|
|
avatarHash = BitConverter.ToString(callback.AvatarHash).Replace("-", "").ToLowerInvariant();
|
|
|
|
if (string.IsNullOrEmpty(avatarHash) || avatarHash.All(singleChar => singleChar == '0')) {
|
|
avatarHash = null;
|
|
}
|
|
}
|
|
|
|
AvatarHash = avatarHash;
|
|
Nickname = callback.Name;
|
|
|
|
if (Statistics != null) {
|
|
Utilities.InBackground(() => Statistics.OnPersonaState(callback.Name, avatarHash));
|
|
}
|
|
}
|
|
|
|
private async void OnPlayingSessionState(ArchiHandler.PlayingSessionStateCallback callback) {
|
|
if (callback == null) {
|
|
ArchiLogger.LogNullError(nameof(callback));
|
|
|
|
return;
|
|
}
|
|
|
|
if (callback.PlayingBlocked == PlayingBlocked) {
|
|
return; // No status update, we're not interested
|
|
}
|
|
|
|
PlayingBlocked = callback.PlayingBlocked;
|
|
await CheckOccupationStatus().ConfigureAwait(false);
|
|
}
|
|
|
|
private async void OnServiceMethod(SteamUnifiedMessages.ServiceMethodNotification notification) {
|
|
if (notification == null) {
|
|
ArchiLogger.LogNullError(nameof(notification));
|
|
|
|
return;
|
|
}
|
|
|
|
switch (notification.MethodName) {
|
|
case "ChatRoomClient.NotifyIncomingChatMessage#1":
|
|
await OnIncomingChatMessage((CChatRoom_IncomingChatMessage_Notification) notification.Body).ConfigureAwait(false);
|
|
|
|
break;
|
|
case "FriendMessagesClient.IncomingMessage#1":
|
|
await OnIncomingMessage((CFriendMessages_IncomingMessage_Notification) notification.Body).ConfigureAwait(false);
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
private async void OnSharedLibraryLockStatus(ArchiHandler.SharedLibraryLockStatusCallback callback) {
|
|
if (callback == null) {
|
|
ArchiLogger.LogNullError(nameof(callback));
|
|
|
|
return;
|
|
}
|
|
|
|
// Ignore no status updates
|
|
if (LibraryLocked) {
|
|
if ((callback.LibraryLockedBySteamID != 0) && (callback.LibraryLockedBySteamID != SteamID)) {
|
|
return;
|
|
}
|
|
|
|
LibraryLocked = false;
|
|
} else {
|
|
if ((callback.LibraryLockedBySteamID == 0) || (callback.LibraryLockedBySteamID == SteamID)) {
|
|
return;
|
|
}
|
|
|
|
LibraryLocked = true;
|
|
}
|
|
|
|
await CheckOccupationStatus().ConfigureAwait(false);
|
|
}
|
|
|
|
private void OnUserNotifications(ArchiHandler.UserNotificationsCallback callback) {
|
|
if (callback == null) {
|
|
ArchiLogger.LogNullError(nameof(callback));
|
|
|
|
return;
|
|
}
|
|
|
|
if ((callback.Notifications == null) || (callback.Notifications.Count == 0)) {
|
|
return;
|
|
}
|
|
|
|
HashSet<ArchiHandler.UserNotificationsCallback.EUserNotification> newPluginNotifications = new HashSet<ArchiHandler.UserNotificationsCallback.EUserNotification>();
|
|
|
|
foreach ((ArchiHandler.UserNotificationsCallback.EUserNotification notification, uint count) in callback.Notifications) {
|
|
bool newNotification;
|
|
|
|
if (count > 0) {
|
|
newNotification = !PastNotifications.TryGetValue(notification, out uint previousCount) || (count > previousCount);
|
|
PastNotifications[notification] = count;
|
|
|
|
if (newNotification) {
|
|
newPluginNotifications.Add(notification);
|
|
}
|
|
} else {
|
|
newNotification = false;
|
|
PastNotifications.TryRemove(notification, out _);
|
|
}
|
|
|
|
ArchiLogger.LogGenericTrace(notification + " = " + count);
|
|
|
|
switch (notification) {
|
|
case ArchiHandler.UserNotificationsCallback.EUserNotification.Gifts when newNotification && BotConfig.AcceptGifts:
|
|
Utilities.InBackground(Actions.AcceptDigitalGiftCards);
|
|
|
|
break;
|
|
case ArchiHandler.UserNotificationsCallback.EUserNotification.Items when newNotification:
|
|
Utilities.InBackground(CardsFarmer.OnNewItemsNotification);
|
|
|
|
if (BotConfig.BotBehaviour.HasFlag(BotConfig.EBotBehaviour.DismissInventoryNotifications)) {
|
|
Utilities.InBackground(ArchiWebHandler.MarkInventory);
|
|
}
|
|
|
|
break;
|
|
case ArchiHandler.UserNotificationsCallback.EUserNotification.Trading when newNotification:
|
|
Utilities.InBackground(Trading.OnNewTrade);
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (newPluginNotifications.Count > 0) {
|
|
Utilities.InBackground(() => PluginsCore.OnBotUserNotifications(this, newPluginNotifications));
|
|
}
|
|
}
|
|
|
|
private void OnVanityURLChangedCallback(ArchiHandler.VanityURLChangedCallback callback) {
|
|
if (callback == null) {
|
|
ArchiLogger.LogNullError(nameof(callback));
|
|
|
|
return;
|
|
}
|
|
|
|
ArchiWebHandler.OnVanityURLChanged(callback.VanityURL);
|
|
}
|
|
|
|
private void OnWalletUpdate(SteamUser.WalletInfoCallback callback) {
|
|
if (callback == null) {
|
|
ArchiLogger.LogNullError(nameof(callback));
|
|
|
|
return;
|
|
}
|
|
|
|
WalletBalance = callback.LongBalance;
|
|
WalletCurrency = callback.Currency;
|
|
}
|
|
|
|
private async Task RedeemGamesInBackground() {
|
|
if (!await GamesRedeemerInBackgroundSemaphore.WaitAsync(0).ConfigureAwait(false)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (GamesRedeemerInBackgroundTimer != null) {
|
|
await GamesRedeemerInBackgroundTimer.DisposeAsync().ConfigureAwait(false);
|
|
|
|
GamesRedeemerInBackgroundTimer = null;
|
|
}
|
|
|
|
ArchiLogger.LogGenericInfo(Strings.Starting);
|
|
|
|
bool assumeWalletKeyOnBadActivationCode = BotConfig.RedeemingPreferences.HasFlag(BotConfig.ERedeemingPreferences.AssumeWalletKeyOnBadActivationCode);
|
|
|
|
while (IsConnectedAndLoggedOn && BotDatabase.HasGamesToRedeemInBackground) {
|
|
(string key, string name) = BotDatabase.GetGameToRedeemInBackground();
|
|
|
|
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(name)) {
|
|
ArchiLogger.LogNullError(nameof(key) + " || " + nameof(name));
|
|
|
|
break;
|
|
}
|
|
|
|
ArchiHandler.PurchaseResponseCallback result = await Actions.RedeemKey(key).ConfigureAwait(false);
|
|
|
|
if (result == null) {
|
|
continue;
|
|
}
|
|
|
|
if (((result.PurchaseResultDetail == EPurchaseResultDetail.CannotRedeemCodeFromClient) || ((result.PurchaseResultDetail == EPurchaseResultDetail.BadActivationCode) && assumeWalletKeyOnBadActivationCode)) && (WalletCurrency != ECurrencyCode.Invalid)) {
|
|
// If it's a wallet code, we try to redeem it first, then handle the inner result as our primary one
|
|
(EResult Result, EPurchaseResultDetail? PurchaseResult)? walletResult = await ArchiWebHandler.RedeemWalletKey(key).ConfigureAwait(false);
|
|
|
|
if (walletResult != null) {
|
|
result.Result = walletResult.Value.Result;
|
|
result.PurchaseResultDetail = walletResult.Value.PurchaseResult.GetValueOrDefault(walletResult.Value.Result == EResult.OK ? EPurchaseResultDetail.NoDetail : EPurchaseResultDetail.BadActivationCode); // BadActivationCode is our smart guess in this case
|
|
} else {
|
|
result.Result = EResult.Timeout;
|
|
result.PurchaseResultDetail = EPurchaseResultDetail.Timeout;
|
|
}
|
|
}
|
|
|
|
ArchiLogger.LogGenericDebug(string.Format(Strings.BotRedeem, key, result.Result + "/" + result.PurchaseResultDetail));
|
|
|
|
bool rateLimited = false;
|
|
bool redeemed = false;
|
|
|
|
switch (result.PurchaseResultDetail) {
|
|
case EPurchaseResultDetail.AccountLocked:
|
|
case EPurchaseResultDetail.AlreadyPurchased:
|
|
case EPurchaseResultDetail.CannotRedeemCodeFromClient:
|
|
case EPurchaseResultDetail.DoesNotOwnRequiredApp:
|
|
case EPurchaseResultDetail.RestrictedCountry:
|
|
case EPurchaseResultDetail.Timeout:
|
|
break;
|
|
case EPurchaseResultDetail.BadActivationCode:
|
|
case EPurchaseResultDetail.DuplicateActivationCode:
|
|
case EPurchaseResultDetail.NoDetail: // OK
|
|
redeemed = true;
|
|
|
|
break;
|
|
case EPurchaseResultDetail.RateLimited:
|
|
rateLimited = true;
|
|
|
|
break;
|
|
default:
|
|
ASF.ArchiLogger.LogGenericError(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(result.PurchaseResultDetail), result.PurchaseResultDetail));
|
|
|
|
break;
|
|
}
|
|
|
|
if (rateLimited) {
|
|
break;
|
|
}
|
|
|
|
BotDatabase.RemoveGameToRedeemInBackground(key);
|
|
|
|
// If user omitted the name or intentionally provided the same name as key, replace it with the Steam result
|
|
if (name.Equals(key) && (result.Items != null) && (result.Items.Count > 0)) {
|
|
name = string.Join(", ", result.Items.Values);
|
|
}
|
|
|
|
string logEntry = name + DefaultBackgroundKeysRedeemerSeparator + "[" + result.PurchaseResultDetail + "]" + ((result.Items != null) && (result.Items.Count > 0) ? DefaultBackgroundKeysRedeemerSeparator + string.Join(", ", result.Items) : "") + DefaultBackgroundKeysRedeemerSeparator + key;
|
|
|
|
string filePath = GetFilePath(redeemed ? EFileType.KeysToRedeemUsed : EFileType.KeysToRedeemUnused);
|
|
|
|
if (string.IsNullOrEmpty(filePath)) {
|
|
ArchiLogger.LogNullError(nameof(filePath));
|
|
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await RuntimeCompatibility.File.AppendAllTextAsync(filePath, logEntry + Environment.NewLine).ConfigureAwait(false);
|
|
} catch (Exception e) {
|
|
ArchiLogger.LogGenericException(e);
|
|
ArchiLogger.LogGenericError(string.Format(Strings.Content, logEntry));
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (IsConnectedAndLoggedOn && BotDatabase.HasGamesToRedeemInBackground) {
|
|
ArchiLogger.LogGenericInfo(string.Format(Strings.BotRateLimitExceeded, TimeSpan.FromHours(RedeemCooldownInHours).ToHumanReadable()));
|
|
|
|
GamesRedeemerInBackgroundTimer = new Timer(
|
|
async e => await RedeemGamesInBackground().ConfigureAwait(false),
|
|
null,
|
|
TimeSpan.FromHours(RedeemCooldownInHours), // Delay
|
|
Timeout.InfiniteTimeSpan // Period
|
|
);
|
|
}
|
|
|
|
ArchiLogger.LogGenericInfo(Strings.Done);
|
|
} finally {
|
|
GamesRedeemerInBackgroundSemaphore.Release();
|
|
}
|
|
}
|
|
|
|
private async Task ResetGamesPlayed() {
|
|
if (CardsFarmer.NowFarming) {
|
|
return;
|
|
}
|
|
|
|
if (BotConfig.GamesPlayedWhileIdle.Count > 0) {
|
|
if (!IsPlayingPossible) {
|
|
return;
|
|
}
|
|
|
|
// This function might be executed before PlayingSessionStateCallback/SharedLibraryLockStatusCallback, ensure proper delay in this case
|
|
await Task.Delay(2000).ConfigureAwait(false);
|
|
|
|
if (CardsFarmer.NowFarming || !IsPlayingPossible) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
await ArchiHandler.PlayGames(BotConfig.GamesPlayedWhileIdle, BotConfig.CustomGamePlayedWhileIdle).ConfigureAwait(false);
|
|
}
|
|
|
|
private void ResetPlayingWasBlockedWithTimer() {
|
|
PlayingWasBlocked = false;
|
|
StopPlayingWasBlockedTimer();
|
|
}
|
|
|
|
private bool ShouldAckChatMessage(ulong steamID) {
|
|
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
|
|
ArchiLogger.LogNullError(nameof(steamID));
|
|
|
|
return false;
|
|
}
|
|
|
|
if (BotConfig.BotBehaviour.HasFlag(BotConfig.EBotBehaviour.MarkReceivedMessagesAsRead)) {
|
|
return true;
|
|
}
|
|
|
|
return BotConfig.BotBehaviour.HasFlag(BotConfig.EBotBehaviour.MarkBotMessagesAsRead) && Bots.Values.Any(bot => bot.SteamID == steamID);
|
|
}
|
|
|
|
private void StopConnectionFailureTimer() {
|
|
if (ConnectionFailureTimer == null) {
|
|
return;
|
|
}
|
|
|
|
ConnectionFailureTimer.Dispose();
|
|
ConnectionFailureTimer = null;
|
|
}
|
|
|
|
private void StopPlayingWasBlockedTimer() {
|
|
if (PlayingWasBlockedTimer == null) {
|
|
return;
|
|
}
|
|
|
|
PlayingWasBlockedTimer.Dispose();
|
|
PlayingWasBlockedTimer = null;
|
|
}
|
|
|
|
private static string UnEscape(string message) {
|
|
if (string.IsNullOrEmpty(message)) {
|
|
ASF.ArchiLogger.LogNullError(nameof(message));
|
|
|
|
return null;
|
|
}
|
|
|
|
return message.Replace("\\[", "[").Replace("\\\\", "\\");
|
|
}
|
|
|
|
private (bool IsSteamParentalEnabled, string SteamParentalCode) ValidateSteamParental(ParentalSettings settings, string steamParentalCode = null) {
|
|
if (settings == null) {
|
|
ArchiLogger.LogNullError(nameof(settings));
|
|
|
|
return (false, null);
|
|
}
|
|
|
|
if (!settings.is_enabled) {
|
|
return (false, null);
|
|
}
|
|
|
|
ArchiCryptoHelper.ESteamParentalAlgorithm steamParentalAlgorithm;
|
|
|
|
switch (settings.passwordhashtype) {
|
|
case 4:
|
|
steamParentalAlgorithm = ArchiCryptoHelper.ESteamParentalAlgorithm.Pbkdf2;
|
|
|
|
break;
|
|
case 6:
|
|
steamParentalAlgorithm = ArchiCryptoHelper.ESteamParentalAlgorithm.SCrypt;
|
|
|
|
break;
|
|
default:
|
|
ArchiLogger.LogGenericError(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(settings.passwordhashtype), settings.passwordhashtype));
|
|
|
|
return (true, null);
|
|
}
|
|
|
|
if ((steamParentalCode != null) && (steamParentalCode.Length == BotConfig.SteamParentalCodeLength)) {
|
|
byte i = 0;
|
|
byte[] password = new byte[steamParentalCode.Length];
|
|
|
|
foreach (char character in steamParentalCode.TakeWhile(character => (character >= '0') && (character <= '9'))) {
|
|
password[i++] = (byte) character;
|
|
}
|
|
|
|
if (i >= steamParentalCode.Length) {
|
|
IEnumerable<byte> passwordHash = ArchiCryptoHelper.GenerateSteamParentalHash(password, settings.salt, (byte) settings.passwordhash.Length, steamParentalAlgorithm);
|
|
|
|
if (passwordHash?.SequenceEqual(settings.passwordhash) == true) {
|
|
return (true, steamParentalCode);
|
|
}
|
|
}
|
|
}
|
|
|
|
ArchiLogger.LogGenericInfo(Strings.BotGeneratingSteamParentalCode);
|
|
|
|
steamParentalCode = ArchiCryptoHelper.RecoverSteamParentalCode(settings.passwordhash, settings.salt, steamParentalAlgorithm);
|
|
|
|
ArchiLogger.LogGenericInfo(Strings.Done);
|
|
|
|
return (true, steamParentalCode);
|
|
}
|
|
|
|
internal enum EFileType : byte {
|
|
Config,
|
|
Database,
|
|
KeysToRedeem,
|
|
KeysToRedeemUnused,
|
|
KeysToRedeemUsed,
|
|
MobileAuthenticator,
|
|
SentryFile
|
|
}
|
|
}
|
|
}
|