Implement plugin system (#1020)

* Implement basic plugin system

* The dawn of new era

* Add plugins warning

* Move more members to PublicAPI

* Open commands for the plugins

* Add IBotHackNewChat

* Run plugin events in parallel

* Use properties in IPlugin

* Hook our custom plugin into CI to ensure it compiles

* Fix dotnet brain damage

* Add IBotsComparer

* Add code documentation

* Add IBotTradeOffer

* Add IBotTradeOffer example

* Add IBotTradeOfferResults

* Final bulletproofing

* Final renaming
This commit is contained in:
Łukasz Domeradzki 2019-01-10 22:33:07 +01:00 committed by GitHub
parent 62a770479e
commit 0f2a816b92
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 3025 additions and 1653 deletions

2
.gitignore vendored
View file

@ -15,6 +15,8 @@ ArchiSteamFarm/debug
# Ignore out folders for publishing
out
ArchiSteamFarm/out
ArchiSteamFarm.CustomPlugins.Example/out
ArchiSteamFarm.Tests/out
# Ignore crowdin CLI secret
tools/crowdin-cli/crowdin_identity.yml

View file

@ -48,6 +48,7 @@ script:
fi
dotnet build ArchiSteamFarm -c "$CONFIGURATION" -f "$NET_CORE_VERSION" -o 'out/source' /nologo
dotnet build ArchiSteamFarm.CustomPlugins.ExamplePlugin -c "$CONFIGURATION" -f "$NET_CORE_VERSION" -o 'out/source' /nologo
dotnet test ArchiSteamFarm.Tests -c "$CONFIGURATION" -f "$NET_CORE_VERSION" -o 'out/source' /nologo
publish() {

View file

@ -0,0 +1,38 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Authors>JustArchi</Authors>
<Company>JustArchi</Company>
<Copyright>Copyright © ArchiSteamFarm 2015-2019</Copyright>
<DefaultItemExcludes>$(DefaultItemExcludes);debug/**;out/**</DefaultItemExcludes>
<ErrorReport>none</ErrorReport>
<LangVersion>latest</LangVersion>
<NoWarn />
<OutputType>Library</OutputType>
<PackageIconUrl>https://github.com/JustArchiNET/ArchiSteamFarm/raw/master/resources/ASF.ico</PackageIconUrl>
<PackageLicenseUrl>http://www.apache.org/licenses/LICENSE-2.0</PackageLicenseUrl>
<PackageProjectUrl>https://github.com/JustArchiNET/ArchiSteamFarm</PackageProjectUrl>
<RepositoryType>Git</RepositoryType>
<RepositoryUrl>https://github.com/JustArchi/ArchiSteamFarm.git</RepositoryUrl>
<TargetFrameworks>netcoreapp2.2;net472</TargetFrameworks>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<DebugSymbols>false</DebugSymbols>
<DebugType>none</DebugType>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsAsErrors />
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ConfigureAwaitChecker.Analyzer" Version="2.0.0" />
<PackageReference Include="SteamKit2" Version="2.1.0" />
<PackageReference Include="System.Composition.AttributedModel" Version="1.3.0-preview.18571.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ArchiSteamFarm\ArchiSteamFarm.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,62 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
//
// Copyright 2015-2019 Ł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.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin {
// This is example class that shows how you can call third-party services within your plugin
// You always wanted from your ASF to post cats, right? Now is your chance!
// P.S. The code is almost 1:1 copy from the one I use in ArchiBoT, you can thank me later
internal static class CatAPI {
private const string URL = "https://aws.random.cat";
internal static async Task<string> GetRandomCatURL(WebBrowser webBrowser) {
const string request = URL + "/meow";
WebBrowser.ObjectResponse<MeowResponse> response = await webBrowser.UrlGetToJsonObject<MeowResponse>(request).ConfigureAwait(false);
if (response?.Content == null) {
return null;
}
if (string.IsNullOrEmpty(response.Content.Link)) {
ASF.ArchiLogger.LogNullError(nameof(response.Content.Link));
return null;
}
return Uri.EscapeUriString(response.Content.Link);
}
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
private sealed class MeowResponse {
#pragma warning disable 649
[JsonProperty(PropertyName = "file", Required = Required.Always)]
internal readonly string Link;
#pragma warning restore 649
private MeowResponse() { }
}
}
}

View file

@ -0,0 +1,158 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
//
// Copyright 2015-2019 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
using System.Collections.Generic;
using System.Composition;
using System.Threading.Tasks;
using ArchiSteamFarm.Json;
using ArchiSteamFarm.Plugins;
using Newtonsoft.Json.Linq;
using SteamKit2;
namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin {
// In order for your plugin to work, it must export generic ASF's IPlugin interface
[Export(typeof(IPlugin))]
// Your plugin class should inherit the plugin interfaces it wants to handle
// If you do not want to handle a particular action (e.g. OnBotMessage that is offered in IBotMessage), it's the best idea to not inherit it at all
// This will keep your code compact, efficient and less dependent. You can always add additional interfaces when you'll need them, this example project will inherit quite a bit of them to show you potential usage
internal sealed class ExamplePlugin : IASF, IBot, IBotCommand, IBotConnection, IBotMessage, IBotModules, IBotTradeOffer {
// This is used for identification purposes, typically you want to use a friendly name of your plugin here, such as the name of your main class
// Please note that this property can have direct dependencies only on structures that were initialized by the constructor, as it's possible to be called before OnLoaded() takes place
public string Name => nameof(ExamplePlugin);
// This will be displayed to the user and written in the log file, typically you should point it to the version of your library, but alternatively you can do some more advanced logic if you'd like to
// Please note that this property can have direct dependencies only on structures that were initialized by the constructor, as it's possible to be called before OnLoaded() takes place
public Version Version => typeof(ExamplePlugin).Assembly.GetName().Version;
// This method, apart from being called before any bot initialization takes place, allows you to read custom global config properties that are not recognized by ASF
// Thanks to that, you can extend default ASF config with your own stuff, then parse it here in order to customize your plugin during runtime
// Keep in mind that, as noted in the interface, additionalConfigProperties can be null if no custom, unrecognized properties are found by ASF, you should handle that case appropriately
// In addition to that, this method also guarantees that all plugins were already OnLoaded(), which allows cross-plugins-communication to be possible
public void OnASFInit(IReadOnlyDictionary<string, JToken> additionalConfigProperties = null) {
if (additionalConfigProperties == null) {
return;
}
foreach (KeyValuePair<string, JToken> configProperty in additionalConfigProperties) {
switch (configProperty.Key) {
// It's a good idea to prefix your custom properties with the name of your plugin, so there will be no possible conflict of ASF or other plugins using the same name, neither now or in the future
case nameof(ExamplePlugin) + "TestProperty" when configProperty.Value.Type == JTokenType.Boolean:
bool exampleBooleanValue = configProperty.Value.Value<bool>();
ASF.ArchiLogger.LogGenericInfo(nameof(ExamplePlugin) + "TestProperty boolean property has been found with a value of: " + exampleBooleanValue);
break;
}
}
}
// This method is called when unknown command is received (starting with CommandPrefix)
// This allows you to recognize the command yourself and implement custom commands
// Keep in mind that there is no guarantee what is the actual access of steamID, so you should do the appropriate access checking yourself
// You can use either ASF's default functions for that (using the Bot.Access), or implement your own logic as you please
// Since ASF already had to do initial parsing in order to determine that the command is unknown, args[] are splitted using standard ASF delimiters
// If by any chance you want to handle message in its raw format, you also have it available, although for usual ASF pattern you can most likely stick with args[] exclusively. The message has CommandPrefix already stripped for your convenience
// If you do not recognize the command, just return null/empty and allow ASF to gracefully return "unknown command" to user on usual basis
public async Task<string> OnBotCommand(Bot bot, ulong steamID, string message, string[] args) {
// In comparison with OnBotMessage(), we're using asynchronous CatAPI call here, so we declare our method as async and return the message as usual
switch (args[0].ToUpperInvariant()) {
// Notice how we handle access here as well, it'll work only for FamilySharing+
case "CAT" when bot.Access.IsFamilySharing(steamID):
// Notice how we can decide whether to use bot's AWH WebBrowser or ASF's one. For Steam-related requests, AWH's one should always be used, for third-party requests like those it doesn't really matter
// Still, it makes sense to pass AWH's one, so in case you get some errors or alike, you know from which bot instance they come from. It's similar to using Bot's ArchiLogger compared to ASF's one
string randomCatURL = await CatAPI.GetRandomCatURL(bot.ArchiWebHandler.WebBrowser).ConfigureAwait(false);
if (string.IsNullOrEmpty(randomCatURL)) {
return "God damn it, we're out of cats, care to notify my master? Thanks!";
}
return randomCatURL;
default:
return null;
}
}
// This method is called when bot is destroyed, e.g. on config removal
// You should ensure that all of your references to this bot instance are cleared - most of the time this is anything you created in OnBotInit(), including deep roots in your custom modules
// This doesn't have to be done immediately (e.g. no need to cancel existing work), but it should be done in timely manner when everything is finished
// Doing so will allow the garbage collector to dispose the bot afterwards, refraining from doing so will create a "memory leak" by keeping the reference alive
public void OnBotDestroy(Bot bot) { }
// This method is called when bot is disconnected from Steam network, you may want to use this info in some kind of way, or not
// ASF tries its best to provide logical reason why the disconnection has happened, and will use EResult.OK if the disconnection was initiated by us (e.g. as part of a command)
// Still, you should take anything other than EResult.OK with a grain of salt, unless you want to assume that Steam knows why it disconnected us (hehe, you bet)
public void OnBotDisconnected(Bot bot, EResult reason) { }
// This method is called at the end of Bot's constructor
// You can initialize all your per-bot structures here
// In general you should do that only when you have a particular need of custom modules or alike, since ASF's plugin system will always provide bot to you as a function argument
public void OnBotInit(Bot bot) {
// Apart of those two that are already provided by ASF, you can also initialize your own logger with your plugin's name, if needed
bot.ArchiLogger.LogGenericInfo("Our bot named " + bot.BotName + " has been initialized, and we're letting you know about it from our " + nameof(ExamplePlugin) + "!");
ASF.ArchiLogger.LogGenericWarning("In case we won't have a bot reference or have something process-wide to log, we can also use ASF's logger!");
}
// This method, apart from being called during bot modules initialization, allows you to read custom bot config properties that are not recognized by ASF
// Thanks to that, you can extend default bot config with your own stuff, then parse it here in order to customize your plugin during runtime
// Keep in mind that, as noted in the interface, additionalConfigProperties can be null if no custom, unrecognized properties are found by ASF, you should handle that case appropriately
// Also keep in mind that this function can be called multiple times, e.g. when user edits his bot configs during runtime
// Take a look at OnASFInit() for example parsing code
public async void OnBotInitModules(Bot bot, IReadOnlyDictionary<string, JToken> additionalConfigProperties = null) {
// ASF marked this message as synchronous, in case we have async code to execute, we can just use async void return
// For example, we'll ensure that every bot starts paused regardless of Paused property, in order to do this, we'll just call Pause here in InitModules()
// Thanks to the fact that this method is called with each bot config reload, we'll ensure that our bot stays paused even if it'd get unpaused otherwise
await bot.Actions.Pause(true).ConfigureAwait(false);
}
// This method is called when the bot is successfully connected to Steam network and it's a good place to schedule any on-connected tasks, as AWH is also expected to be available shortly
public void OnBotLoggedOn(Bot bot) { }
// This method is called when bot receives a message that is NOT a command (in other words, a message that doesn't start with CommandPrefix)
// Normally ASF entirely ignores such messages as the program should not respond to something that isn't recognized
// Therefore this function allows you to catch all such messages and handle them yourself
// Keep in mind that there is no guarantee what is the actual access of steamID, so you should do the appropriate access checking yourself
// You can use either ASF's default functions for that (using the Bot.Access), or implement your own logic as you please
// If you do not intend to return any response to user, just return null/empty and ASF will proceed with the silence as usual
public Task<string> OnBotMessage(Bot bot, ulong steamID, string message) {
// Normally ASF will expect from you async-capable responses, such as Task<string>. This allows you to make your code fully asynchronous which is a core fundament on which ASF is built upon
// Since in this method we're not doing any async stuff, instead of defining this method as async (pointless), we just need to wrap our responses in Task.FromResult<>()
bot.ArchiLogger.LogGenericTrace("Hey boss, we got some unknown message here!");
return Task.FromResult("I didn't get that, did you mean to use a command?");
}
// This method is called when bot receives a trade offer that ASF isn't willing to accept (ignored and rejected trades)
// It allows you not only to analyze such trades, but generate a response whether ASF should accept it (true), or proceed like usual (false)
// Thanks to that, you can implement custom rules for all trades that aren't handled by ASF, for example cross-set trading on your own custom rules
// You'd implement your own logic here, as an example we'll allow all trades to be accepted if the bot's name starts from "TrashBot"
public Task<bool> OnBotTradeOffer(Bot bot, Steam.TradeOffer tradeOffer) => Task.FromResult(bot.BotName.StartsWith("TrashBot", StringComparison.OrdinalIgnoreCase));
// This is the earliest method that will be called, right after loading the plugin, long before any bot initialization takes place
// It's a good place to initialize all potential (non-bot-specific) structures that you will need across lifetime of your plugin, such as global timers, concurrent dictionaries and alike
// If you do not have any global structures to initialize, you can leave this function empty
// At this point you can access core ASF's functionality, such as logging or a web browser
// Once all plugins execute their OnLoaded() methods, OnASFInit() will be called next
public void OnLoaded() { }
}
}

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<config>
<add key="dependencyversion" value="Highest" />
</config>
<packageSources>
<add key="dotnet-core" value="https://dotnet.myget.org/F/dotnet-core/api/v3/index.json" />
</packageSources>
</configuration>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

View file

@ -1,7 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<ApplicationIcon>ASF.ico</ApplicationIcon>
<Authors>JustArchi</Authors>
<Company>JustArchi</Company>
<Copyright>Copyright © ArchiSteamFarm 2015-2019</Copyright>
@ -11,11 +10,11 @@
<LangVersion>latest</LangVersion>
<NoWarn />
<OutputType>Library</OutputType>
<PackageIconUrl>https://github.com/JustArchi/ArchiSteamFarm/raw/master/resources/ASF.ico</PackageIconUrl>
<PackageIconUrl>https://github.com/JustArchiNET/ArchiSteamFarm/raw/master/resources/ASF.ico</PackageIconUrl>
<PackageLicenseUrl>http://www.apache.org/licenses/LICENSE-2.0</PackageLicenseUrl>
<PackageProjectUrl>https://github.com/JustArchi/ArchiSteamFarm</PackageProjectUrl>
<PackageProjectUrl>https://github.com/JustArchiNET/ArchiSteamFarm</PackageProjectUrl>
<RepositoryType>Git</RepositoryType>
<RepositoryUrl>https://github.com/JustArchi/ArchiSteamFarm.git</RepositoryUrl>
<RepositoryUrl>https://github.com/JustArchiNET/ArchiSteamFarm.git</RepositoryUrl>
<TargetFrameworks>netcoreapp2.2;net472</TargetFrameworks>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
</PropertyGroup>

View file

@ -5,7 +5,9 @@ VisualStudioVersion = 15.0.26621.2
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ArchiSteamFarm", "ArchiSteamFarm\ArchiSteamFarm.csproj", "{CF84911C-2C4C-4195-8AF3-ABBB6D3DE9AA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArchiSteamFarm.Tests", "ArchiSteamFarm.Tests\ArchiSteamFarm.Tests.csproj", "{91DC4C4F-3C23-4716-8CB2-BC7EAB65D759}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ArchiSteamFarm.Tests", "ArchiSteamFarm.Tests\ArchiSteamFarm.Tests.csproj", "{91DC4C4F-3C23-4716-8CB2-BC7EAB65D759}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ArchiSteamFarm.CustomPlugins.ExamplePlugin", "ArchiSteamFarm.CustomPlugins.ExamplePlugin\ArchiSteamFarm.CustomPlugins.ExamplePlugin.csproj", "{2E2C26B6-7C1D-4BAF-BCF9-79286DA08F82}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -21,6 +23,10 @@ Global
{91DC4C4F-3C23-4716-8CB2-BC7EAB65D759}.Debug|Any CPU.Build.0 = Debug|Any CPU
{91DC4C4F-3C23-4716-8CB2-BC7EAB65D759}.Release|Any CPU.ActiveCfg = Release|Any CPU
{91DC4C4F-3C23-4716-8CB2-BC7EAB65D759}.Release|Any CPU.Build.0 = Release|Any CPU
{2E2C26B6-7C1D-4BAF-BCF9-79286DA08F82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2E2C26B6-7C1D-4BAF-BCF9-79286DA08F82}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2E2C26B6-7C1D-4BAF-BCF9-79286DA08F82}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2E2C26B6-7C1D-4BAF-BCF9-79286DA08F82}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View file

@ -27,17 +27,24 @@ using System.IO.Compression;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.IPC;
using ArchiSteamFarm.Localization;
using ArchiSteamFarm.NLog;
using ArchiSteamFarm.Plugins;
using JetBrains.Annotations;
using SteamKit2;
using SteamKit2.Discovery;
namespace ArchiSteamFarm {
internal static class ASF {
public static class ASF {
// This is based on internal Valve guidelines, we're not using it as a hard limit
private const byte MaximumRecommendedBotsCount = 10;
internal static readonly ArchiLogger ArchiLogger = new ArchiLogger(SharedInfo.ASF);
[PublicAPI]
public static readonly ArchiLogger ArchiLogger = new ArchiLogger(SharedInfo.ASF);
[PublicAPI]
public static WebBrowser WebBrowser { get; internal set; }
private static readonly ConcurrentDictionary<string, object> LastWriteEvents = new ConcurrentDictionary<string, object>();
private static readonly SemaphoreSlim UpdateSemaphore = new SemaphoreSlim(1, 1);
@ -45,68 +52,8 @@ namespace ArchiSteamFarm {
private static Timer AutoUpdatesTimer;
private static FileSystemWatcher FileSystemWatcher;
internal static async Task InitBots() {
if (Bot.Bots.Count != 0) {
return;
}
// Ensure that we ask for a list of servers if we don't have any saved servers available
IEnumerable<ServerRecord> servers = await Program.GlobalDatabase.ServerListProvider.FetchServerListAsync().ConfigureAwait(false);
if (servers?.Any() != true) {
ArchiLogger.LogGenericInfo(string.Format(Strings.Initializing, nameof(SteamDirectory)));
SteamConfiguration steamConfiguration = SteamConfiguration.Create(builder => builder.WithProtocolTypes(Program.GlobalConfig.SteamProtocols).WithCellID(Program.GlobalDatabase.CellID).WithServerListProvider(Program.GlobalDatabase.ServerListProvider).WithHttpClientFactory(() => Program.WebBrowser.GenerateDisposableHttpClient()));
try {
await SteamDirectory.LoadAsync(steamConfiguration).ConfigureAwait(false);
ArchiLogger.LogGenericInfo(Strings.Success);
} catch {
ArchiLogger.LogGenericWarning(Strings.BotSteamDirectoryInitializationFailed);
await Task.Delay(5000).ConfigureAwait(false);
}
}
HashSet<string> botNames;
try {
botNames = Directory.EnumerateFiles(SharedInfo.ConfigDirectory, "*" + SharedInfo.ConfigExtension).Select(Path.GetFileNameWithoutExtension).Where(botName => !string.IsNullOrEmpty(botName) && IsValidBotName(botName)).ToHashSet();
} catch (Exception e) {
ArchiLogger.LogGenericException(e);
return;
}
if (botNames.Count == 0) {
ArchiLogger.LogGenericWarning(Strings.ErrorNoBotsDefined);
return;
}
if (botNames.Count > MaximumRecommendedBotsCount) {
ArchiLogger.LogGenericWarning(string.Format(Strings.WarningExcessiveBotsCount, MaximumRecommendedBotsCount));
await Task.Delay(10000).ConfigureAwait(false);
}
await Utilities.InParallel(botNames.OrderBy(botName => botName).Select(Bot.RegisterBot)).ConfigureAwait(false);
}
internal static void InitEvents() {
if (FileSystemWatcher != null) {
return;
}
FileSystemWatcher = new FileSystemWatcher(SharedInfo.ConfigDirectory) { NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite };
FileSystemWatcher.Changed += OnChanged;
FileSystemWatcher.Created += OnCreated;
FileSystemWatcher.Deleted += OnDeleted;
FileSystemWatcher.Renamed += OnRenamed;
FileSystemWatcher.EnableRaisingEvents = true;
}
internal static bool IsOwner(ulong steamID) {
[PublicAPI]
public static bool IsOwner(ulong steamID) {
if (steamID == 0) {
ArchiLogger.LogNullError(nameof(steamID));
@ -116,6 +63,26 @@ namespace ArchiSteamFarm {
return (steamID == Program.GlobalConfig.SteamOwnerID) || (Debugging.IsDebugBuild && (steamID == SharedInfo.ArchiSteamID));
}
internal static async Task Init() {
WebBrowser = new WebBrowser(ArchiLogger, Program.GlobalConfig.WebProxy, true);
if (Program.GlobalConfig.IPC) {
await ArchiKestrel.Start().ConfigureAwait(false);
}
await UpdateAndRestart().ConfigureAwait(false);
if (!Core.InitPlugins()) {
await Task.Delay(10000).ConfigureAwait(false);
}
await Core.OnASFInitModules(Program.GlobalConfig.AdditionalProperties).ConfigureAwait(false);
await InitBots().ConfigureAwait(false);
InitEvents();
}
internal static async Task RestartOrExit() {
if (Program.RestartAllowed && Program.GlobalConfig.AutoRestart) {
ArchiLogger.LogGenericInfo(Strings.Restarting);
@ -218,7 +185,7 @@ namespace ArchiSteamFarm {
ArchiLogger.LogGenericInfo(string.Format(Strings.UpdateDownloadingNewVersion, newVersion, binaryAsset.Size / 1024 / 1024));
WebBrowser.BinaryResponse response = await Program.WebBrowser.UrlGetToBinaryWithProgress(binaryAsset.DownloadURL).ConfigureAwait(false);
WebBrowser.BinaryResponse response = await WebBrowser.UrlGetToBinaryWithProgress(binaryAsset.DownloadURL).ConfigureAwait(false);
if (response?.Content == null) {
return null;
@ -252,33 +219,6 @@ namespace ArchiSteamFarm {
}
}
internal static async Task UpdateAndRestart() {
if (!SharedInfo.BuildInfo.CanUpdate || (Program.GlobalConfig.UpdateChannel == GlobalConfig.EUpdateChannel.None)) {
return;
}
if ((AutoUpdatesTimer == null) && (Program.GlobalConfig.UpdatePeriod > 0)) {
TimeSpan autoUpdatePeriod = TimeSpan.FromHours(Program.GlobalConfig.UpdatePeriod);
AutoUpdatesTimer = new Timer(
async e => await UpdateAndRestart().ConfigureAwait(false),
null,
autoUpdatePeriod, // Delay
autoUpdatePeriod // Period
);
ArchiLogger.LogGenericInfo(string.Format(Strings.AutoUpdateCheckInfo, autoUpdatePeriod.ToHumanReadable()));
}
Version newVersion = await Update().ConfigureAwait(false);
if ((newVersion == null) || (newVersion <= SharedInfo.Version)) {
return;
}
await RestartOrExit().ConfigureAwait(false);
}
private static async Task<bool> CanHandleWriteEvent(string name) {
if (string.IsNullOrEmpty(name)) {
ArchiLogger.LogNullError(nameof(name));
@ -297,6 +237,77 @@ namespace ArchiSteamFarm {
return LastWriteEvents.TryGetValue(name, out object savedWriteEvent) && (currentWriteEvent == savedWriteEvent) && LastWriteEvents.TryRemove(name, out _);
}
private static async Task InitBots() {
if (Bot.Bots != null) {
return;
}
StringComparer botsComparer = await Core.GetBotsComparer().ConfigureAwait(false);
if (botsComparer == null) {
ArchiLogger.LogNullError(nameof(botsComparer));
return;
}
Bot.Init(botsComparer);
// Ensure that we ask for a list of servers if we don't have any saved servers available
IEnumerable<ServerRecord> servers = await Program.GlobalDatabase.ServerListProvider.FetchServerListAsync().ConfigureAwait(false);
if (servers?.Any() != true) {
ArchiLogger.LogGenericInfo(string.Format(Strings.Initializing, nameof(SteamDirectory)));
SteamConfiguration steamConfiguration = SteamConfiguration.Create(builder => builder.WithProtocolTypes(Program.GlobalConfig.SteamProtocols).WithCellID(Program.GlobalDatabase.CellID).WithServerListProvider(Program.GlobalDatabase.ServerListProvider).WithHttpClientFactory(() => WebBrowser.GenerateDisposableHttpClient()));
try {
await SteamDirectory.LoadAsync(steamConfiguration).ConfigureAwait(false);
ArchiLogger.LogGenericInfo(Strings.Success);
} catch {
ArchiLogger.LogGenericWarning(Strings.BotSteamDirectoryInitializationFailed);
await Task.Delay(5000).ConfigureAwait(false);
}
}
HashSet<string> botNames;
try {
botNames = Directory.EnumerateFiles(SharedInfo.ConfigDirectory, "*" + SharedInfo.ConfigExtension).Select(Path.GetFileNameWithoutExtension).Where(botName => !string.IsNullOrEmpty(botName) && IsValidBotName(botName)).ToHashSet(botsComparer);
} catch (Exception e) {
ArchiLogger.LogGenericException(e);
return;
}
if (botNames.Count == 0) {
ArchiLogger.LogGenericWarning(Strings.ErrorNoBotsDefined);
return;
}
if (botNames.Count > MaximumRecommendedBotsCount) {
ArchiLogger.LogGenericWarning(string.Format(Strings.WarningExcessiveBotsCount, MaximumRecommendedBotsCount));
await Task.Delay(10000).ConfigureAwait(false);
}
await Utilities.InParallel(botNames.OrderBy(botName => botName).Select(Bot.RegisterBot)).ConfigureAwait(false);
}
private static void InitEvents() {
if (FileSystemWatcher != null) {
return;
}
FileSystemWatcher = new FileSystemWatcher(SharedInfo.ConfigDirectory) { NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite };
FileSystemWatcher.Changed += OnChanged;
FileSystemWatcher.Created += OnCreated;
FileSystemWatcher.Deleted += OnDeleted;
FileSystemWatcher.Renamed += OnRenamed;
FileSystemWatcher.EnableRaisingEvents = true;
}
private static bool IsValidBotName(string botName) {
if (string.IsNullOrEmpty(botName)) {
ArchiLogger.LogNullError(nameof(botName));
@ -540,6 +551,33 @@ namespace ArchiSteamFarm {
await OnCreatedFile(e.Name, e.FullPath).ConfigureAwait(false);
}
private static async Task UpdateAndRestart() {
if (!SharedInfo.BuildInfo.CanUpdate || (Program.GlobalConfig.UpdateChannel == GlobalConfig.EUpdateChannel.None)) {
return;
}
if ((AutoUpdatesTimer == null) && (Program.GlobalConfig.UpdatePeriod > 0)) {
TimeSpan autoUpdatePeriod = TimeSpan.FromHours(Program.GlobalConfig.UpdatePeriod);
AutoUpdatesTimer = new Timer(
async e => await UpdateAndRestart().ConfigureAwait(false),
null,
autoUpdatePeriod, // Delay
autoUpdatePeriod // Period
);
ArchiLogger.LogGenericInfo(string.Format(Strings.AutoUpdateCheckInfo, autoUpdatePeriod.ToHumanReadable()));
}
Version newVersion = await Update().ConfigureAwait(false);
if ((newVersion == null) || (newVersion <= SharedInfo.Version)) {
return;
}
await RestartOrExit().ConfigureAwait(false);
}
private static bool UpdateFromArchive(ZipArchive archive, string targetDirectory) {
if ((archive == null) || string.IsNullOrEmpty(targetDirectory)) {
ArchiLogger.LogNullError(nameof(archive) + " || " + nameof(targetDirectory));
@ -573,6 +611,7 @@ namespace ArchiSteamFarm {
switch (relativeDirectoryName) {
// Files in those directories we want to keep in their current place
case SharedInfo.ConfigDirectory:
case SharedInfo.PluginsDirectory:
continue;
case "":

77
ArchiSteamFarm/Access.cs Normal file
View file

@ -0,0 +1,77 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
//
// Copyright 2015-2019 Ł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 ArchiSteamFarm.Collections;
using JetBrains.Annotations;
namespace ArchiSteamFarm {
public sealed class Access {
internal readonly ConcurrentHashSet<ulong> SteamFamilySharingIDs = new ConcurrentHashSet<ulong>();
private readonly Bot Bot;
internal Access(Bot bot) => Bot = bot ?? throw new ArgumentNullException(nameof(bot));
[PublicAPI]
public bool IsFamilySharing(ulong steamID) {
if (steamID == 0) {
Bot.ArchiLogger.LogNullError(nameof(steamID));
return false;
}
return ASF.IsOwner(steamID) || SteamFamilySharingIDs.Contains(steamID) || (GetSteamUserPermission(steamID) >= BotConfig.EPermission.FamilySharing);
}
[PublicAPI]
public bool IsMaster(ulong steamID) {
if (steamID == 0) {
Bot.ArchiLogger.LogNullError(nameof(steamID));
return false;
}
return ASF.IsOwner(steamID) || (GetSteamUserPermission(steamID) >= BotConfig.EPermission.Master);
}
[PublicAPI]
public bool IsOperator(ulong steamID) {
if (steamID == 0) {
Bot.ArchiLogger.LogNullError(nameof(steamID));
return false;
}
return ASF.IsOwner(steamID) || (GetSteamUserPermission(steamID) >= BotConfig.EPermission.Operator);
}
private BotConfig.EPermission GetSteamUserPermission(ulong steamID) {
if (steamID == 0) {
Bot.ArchiLogger.LogNullError(nameof(steamID));
return BotConfig.EPermission.None;
}
return Bot.BotConfig.SteamUserPermissions.TryGetValue(steamID, out BotConfig.EPermission permission) ? permission : BotConfig.EPermission.None;
}
}
}

View file

@ -28,10 +28,11 @@ using ArchiSteamFarm.Collections;
using ArchiSteamFarm.Helpers;
using ArchiSteamFarm.Json;
using ArchiSteamFarm.Localization;
using JetBrains.Annotations;
using SteamKit2;
namespace ArchiSteamFarm {
internal sealed class Actions : IDisposable {
public sealed class Actions : IDisposable {
private static readonly SemaphoreSlim GiftCardsSemaphore = new SemaphoreSlim(1, 1);
private static readonly SemaphoreSlim GiftsSemaphore = new SemaphoreSlim(1, 1);
@ -55,7 +56,8 @@ namespace ArchiSteamFarm {
CardsFarmerResumeTimer?.Dispose();
}
internal async Task<bool> AcceptConfirmations(bool accept, Steam.ConfirmationDetails.EType acceptedType = Steam.ConfirmationDetails.EType.Unknown, IReadOnlyCollection<ulong> acceptedTradeOfferIDs = null, bool waitIfNeeded = false) {
[PublicAPI]
public async Task<bool> AcceptConfirmations(bool accept, Steam.ConfirmationDetails.EType acceptedType = Steam.ConfirmationDetails.EType.Unknown, IReadOnlyCollection<ulong> acceptedTradeOfferIDs = null, bool waitIfNeeded = false) {
if (!Bot.HasMobileAuthenticator) {
return false;
}
@ -109,6 +111,190 @@ namespace ArchiSteamFarm {
return !waitIfNeeded;
}
[PublicAPI]
public static (bool Success, string Output) Exit() {
// Schedule the task after some time so user can receive response
Utilities.InBackground(
async () => {
await Task.Delay(1000).ConfigureAwait(false);
await Program.Exit().ConfigureAwait(false);
}
);
return (true, Strings.Done);
}
[PublicAPI]
public async Task<(bool Success, string Output)> Pause(bool permanent, ushort resumeInSeconds = 0) {
if (Bot.CardsFarmer.Paused) {
return (false, Strings.BotAutomaticIdlingPausedAlready);
}
await Bot.CardsFarmer.Pause(permanent).ConfigureAwait(false);
if (!permanent && (Bot.BotConfig.GamesPlayedWhileIdle.Count > 0)) {
// We want to let family sharing users access our library, and in this case we must also stop GamesPlayedWhileIdle
// We add extra delay because OnFarmingStopped() also executes PlayGames()
// Despite of proper order on our end, Steam network might not respect it
await Task.Delay(Bot.CallbackSleep).ConfigureAwait(false);
await Bot.ArchiHandler.PlayGames(Enumerable.Empty<uint>(), Bot.BotConfig.CustomGamePlayedWhileIdle).ConfigureAwait(false);
}
if (resumeInSeconds > 0) {
if (CardsFarmerResumeTimer != null) {
CardsFarmerResumeTimer.Dispose();
CardsFarmerResumeTimer = null;
}
CardsFarmerResumeTimer = new Timer(
e => Resume(),
null,
TimeSpan.FromSeconds(resumeInSeconds), // Delay
Timeout.InfiniteTimeSpan // Period
);
}
return (true, Strings.BotAutomaticIdlingNowPaused);
}
[PublicAPI]
public async Task<ArchiHandler.PurchaseResponseCallback> RedeemKey(string key) {
await LimitGiftsRequestsAsync().ConfigureAwait(false);
return await Bot.ArchiHandler.RedeemKey(key).ConfigureAwait(false);
}
[PublicAPI]
public static (bool Success, string Output) Restart() {
// Schedule the task after some time so user can receive response
Utilities.InBackground(
async () => {
await Task.Delay(1000).ConfigureAwait(false);
await Program.Restart().ConfigureAwait(false);
}
);
return (true, Strings.Done);
}
[PublicAPI]
public (bool Success, string Output) Resume() {
if (!Bot.CardsFarmer.Paused) {
return (false, Strings.BotAutomaticIdlingResumedAlready);
}
Utilities.InBackground(() => Bot.CardsFarmer.Resume(true));
return (true, Strings.BotAutomaticIdlingNowResumed);
}
[PublicAPI]
public async Task<(bool Success, string Output)> SendTradeOffer(uint appID = Steam.Asset.SteamAppID, uint contextID = Steam.Asset.SteamCommunityContextID, ulong targetSteamID = 0, IReadOnlyCollection<Steam.Asset.EType> wantedTypes = null, IReadOnlyCollection<uint> wantedRealAppIDs = null) {
if ((appID == 0) || (contextID == 0)) {
Bot.ArchiLogger.LogNullError(nameof(appID) + " || " + nameof(contextID));
return (false, string.Format(Strings.ErrorObjectIsNull, nameof(targetSteamID) + " || " + nameof(appID) + " || " + nameof(contextID)));
}
if (!Bot.IsConnectedAndLoggedOn) {
return (false, Strings.BotNotConnected);
}
if (targetSteamID == 0) {
targetSteamID = GetFirstSteamMasterID();
if (targetSteamID == 0) {
return (false, Strings.BotLootingMasterNotDefined);
}
}
if (targetSteamID == Bot.SteamID) {
return (false, Strings.BotSendingTradeToYourself);
}
lock (TradingSemaphore) {
if (TradingScheduled) {
return (false, Strings.ErrorAborted);
}
TradingScheduled = true;
}
await TradingSemaphore.WaitAsync().ConfigureAwait(false);
try {
lock (TradingSemaphore) {
TradingScheduled = false;
}
HashSet<Steam.Asset> inventory = await Bot.ArchiWebHandler.GetInventory(Bot.SteamID, appID, contextID, true, wantedTypes, wantedRealAppIDs).ConfigureAwait(false);
if ((inventory == null) || (inventory.Count == 0)) {
return (false, string.Format(Strings.ErrorIsEmpty, nameof(inventory)));
}
if (!await Bot.ArchiWebHandler.MarkSentTrades().ConfigureAwait(false)) {
return (false, Strings.BotLootingFailed);
}
(bool success, HashSet<ulong> mobileTradeOfferIDs) = await Bot.ArchiWebHandler.SendTradeOffer(targetSteamID, inventory, token: Bot.BotConfig.SteamTradeToken).ConfigureAwait(false);
if ((mobileTradeOfferIDs != null) && (mobileTradeOfferIDs.Count > 0) && Bot.HasMobileAuthenticator) {
if (!await AcceptConfirmations(true, Steam.ConfirmationDetails.EType.Trade, mobileTradeOfferIDs, true).ConfigureAwait(false)) {
return (false, Strings.BotLootingFailed);
}
}
if (!success) {
return (false, Strings.BotLootingFailed);
}
} finally {
TradingSemaphore.Release();
}
return (true, Strings.BotLootingSuccess);
}
[PublicAPI]
public (bool Success, string Output) Start() {
if (Bot.KeepRunning) {
return (false, Strings.BotAlreadyRunning);
}
SkipFirstShutdown = true;
Utilities.InBackground(Bot.Start);
return (true, Strings.Done);
}
[PublicAPI]
public (bool Success, string Output) Stop() {
if (!Bot.KeepRunning) {
return (false, Strings.BotAlreadyStopped);
}
Bot.Stop();
return (true, Strings.Done);
}
[PublicAPI]
public static async Task<(bool Success, string Message)> Update() {
Version version = await ASF.Update(true).ConfigureAwait(false);
if (version == null) {
return (false, null);
}
if (SharedInfo.Version >= version) {
return (false, "V" + SharedInfo.Version + " ≥ V" + version);
}
Utilities.InBackground(ASF.RestartOrExit);
return (true, version.ToString());
}
internal async Task AcceptDigitalGiftCards() {
lock (GiftCardsSemaphore) {
if (ProcessingGiftsScheduled) {
@ -177,18 +363,6 @@ namespace ArchiSteamFarm {
}
}
internal static (bool Success, string Output) Exit() {
// Schedule the task after some time so user can receive response
Utilities.InBackground(
async () => {
await Task.Delay(1000).ConfigureAwait(false);
await Program.Exit().ConfigureAwait(false);
}
);
return (true, Strings.Done);
}
internal async Task<SemaphoreLock> GetTradingLock() {
await TradingSemaphore.WaitAsync().ConfigureAwait(false);
@ -197,177 +371,6 @@ namespace ArchiSteamFarm {
internal void OnDisconnected() => HandledGifts.Clear();
internal async Task<(bool Success, string Output)> Pause(bool permanent, ushort resumeInSeconds = 0) {
if (!Bot.IsConnectedAndLoggedOn) {
return (false, Strings.BotNotConnected);
}
if (Bot.CardsFarmer.Paused) {
return (false, Strings.BotAutomaticIdlingPausedAlready);
}
await Bot.CardsFarmer.Pause(permanent).ConfigureAwait(false);
if (!permanent && (Bot.BotConfig.GamesPlayedWhileIdle.Count > 0)) {
// We want to let family sharing users access our library, and in this case we must also stop GamesPlayedWhileIdle
// We add extra delay because OnFarmingStopped() also executes PlayGames()
// Despite of proper order on our end, Steam network might not respect it
await Task.Delay(Bot.CallbackSleep).ConfigureAwait(false);
await Bot.ArchiHandler.PlayGames(Enumerable.Empty<uint>(), Bot.BotConfig.CustomGamePlayedWhileIdle).ConfigureAwait(false);
}
if (resumeInSeconds > 0) {
if (CardsFarmerResumeTimer != null) {
CardsFarmerResumeTimer.Dispose();
CardsFarmerResumeTimer = null;
}
CardsFarmerResumeTimer = new Timer(
e => Resume(),
null,
TimeSpan.FromSeconds(resumeInSeconds), // Delay
Timeout.InfiniteTimeSpan // Period
);
}
return (true, Strings.BotAutomaticIdlingNowPaused);
}
internal async Task<ArchiHandler.PurchaseResponseCallback> RedeemKey(string key) {
await LimitGiftsRequestsAsync().ConfigureAwait(false);
return await Bot.ArchiHandler.RedeemKey(key).ConfigureAwait(false);
}
internal static (bool Success, string Output) Restart() {
// Schedule the task after some time so user can receive response
Utilities.InBackground(
async () => {
await Task.Delay(1000).ConfigureAwait(false);
await Program.Restart().ConfigureAwait(false);
}
);
return (true, Strings.Done);
}
internal (bool Success, string Output) Resume() {
if (!Bot.IsConnectedAndLoggedOn) {
return (false, Strings.BotNotConnected);
}
if (!Bot.CardsFarmer.Paused) {
return (false, Strings.BotAutomaticIdlingResumedAlready);
}
Utilities.InBackground(() => Bot.CardsFarmer.Resume(true));
return (true, Strings.BotAutomaticIdlingNowResumed);
}
internal async Task<(bool Success, string Output)> SendTradeOffer(uint appID = Steam.Asset.SteamAppID, uint contextID = Steam.Asset.SteamCommunityContextID, ulong targetSteamID = 0, IReadOnlyCollection<Steam.Asset.EType> wantedTypes = null, IReadOnlyCollection<uint> wantedRealAppIDs = null) {
if ((appID == 0) || (contextID == 0)) {
Bot.ArchiLogger.LogNullError(nameof(appID) + " || " + nameof(contextID));
return (false, string.Format(Strings.ErrorObjectIsNull, nameof(targetSteamID) + " || " + nameof(appID) + " || " + nameof(contextID)));
}
if (!Bot.IsConnectedAndLoggedOn) {
return (false, Strings.BotNotConnected);
}
if (targetSteamID == 0) {
targetSteamID = GetFirstSteamMasterID();
if (targetSteamID == 0) {
return (false, Strings.BotLootingMasterNotDefined);
}
}
if (targetSteamID == Bot.SteamID) {
return (false, Strings.BotSendingTradeToYourself);
}
lock (TradingSemaphore) {
if (TradingScheduled) {
return (false, Strings.ErrorAborted);
}
TradingScheduled = true;
}
await TradingSemaphore.WaitAsync().ConfigureAwait(false);
try {
lock (TradingSemaphore) {
TradingScheduled = false;
}
HashSet<Steam.Asset> inventory = await Bot.ArchiWebHandler.GetInventory(Bot.SteamID, appID, contextID, true, wantedTypes, wantedRealAppIDs).ConfigureAwait(false);
if ((inventory == null) || (inventory.Count == 0)) {
return (false, string.Format(Strings.ErrorIsEmpty, nameof(inventory)));
}
if (!await Bot.ArchiWebHandler.MarkSentTrades().ConfigureAwait(false)) {
return (false, Strings.BotLootingFailed);
}
(bool success, HashSet<ulong> mobileTradeOfferIDs) = await Bot.ArchiWebHandler.SendTradeOffer(targetSteamID, inventory, token: Bot.BotConfig.SteamTradeToken).ConfigureAwait(false);
if ((mobileTradeOfferIDs != null) && (mobileTradeOfferIDs.Count > 0) && Bot.HasMobileAuthenticator) {
if (!await AcceptConfirmations(true, Steam.ConfirmationDetails.EType.Trade, mobileTradeOfferIDs, true).ConfigureAwait(false)) {
return (false, Strings.BotLootingFailed);
}
}
if (!success) {
return (false, Strings.BotLootingFailed);
}
} finally {
TradingSemaphore.Release();
}
return (true, Strings.BotLootingSuccess);
}
internal (bool Success, string Output) Start() {
if (Bot.KeepRunning) {
return (false, Strings.BotAlreadyRunning);
}
SkipFirstShutdown = true;
Utilities.InBackground(Bot.Start);
return (true, Strings.Done);
}
internal (bool Success, string Output) Stop() {
if (!Bot.KeepRunning) {
return (false, Strings.BotAlreadyStopped);
}
Bot.Stop();
return (true, Strings.Done);
}
internal static async Task<(bool Success, string Message)> Update() {
Version version = await ASF.Update(true).ConfigureAwait(false);
if (version == null) {
return (false, null);
}
if (SharedInfo.Version >= version) {
return (false, "V" + SharedInfo.Version + " ≥ V" + version);
}
Utilities.InBackground(ASF.RestartOrExit);
return (true, version.ToString());
}
private ulong GetFirstSteamMasterID() => Bot.BotConfig.SteamUserPermissions.Where(kv => (kv.Key != 0) && (kv.Value == BotConfig.EPermission.Master)).Select(kv => kv.Key).OrderByDescending(steamID => steamID != Bot.SteamID).ThenBy(steamID => steamID).FirstOrDefault();
private static async Task LimitGiftsRequestsAsync() {

View file

@ -29,6 +29,7 @@ using System.Threading.Tasks;
using ArchiSteamFarm.CMsgs;
using ArchiSteamFarm.Localization;
using ArchiSteamFarm.NLog;
using JetBrains.Annotations;
using SteamKit2;
using SteamKit2.Internal;
using SteamKit2.Unified.Internal;
@ -832,7 +833,7 @@ namespace ArchiSteamFarm {
Notifications = new Dictionary<EUserNotification, uint>(1) { { EUserNotification.Items, msg.count_new_items } };
}
[SuppressMessage("ReSharper", "UnusedMember.Global")]
[PublicAPI]
internal enum EUserNotification : byte {
Unknown,
Trading,

View file

@ -2,7 +2,7 @@
<PropertyGroup>
<ApplicationIcon>ASF.ico</ApplicationIcon>
<AssemblyVersion>3.4.2.3</AssemblyVersion>
<AssemblyVersion>4.0.0.0</AssemblyVersion>
<Authors>JustArchi</Authors>
<Company>JustArchi</Company>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
@ -10,16 +10,16 @@
<DefaultItemExcludes>$(DefaultItemExcludes);debug/**;out/**</DefaultItemExcludes>
<Description>ASF is an application that allows you to farm steam cards using multiple steam accounts simultaneously.</Description>
<ErrorReport>none</ErrorReport>
<FileVersion>3.4.2.3</FileVersion>
<FileVersion>4.0.0.0</FileVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<LangVersion>latest</LangVersion>
<NoWarn>1591</NoWarn>
<OutputType>Exe</OutputType>
<PackageIconUrl>https://github.com/JustArchi/ArchiSteamFarm/raw/master/resources/ASF.ico</PackageIconUrl>
<PackageIconUrl>https://github.com/JustArchiNET/ArchiSteamFarm/raw/master/resources/ASF.ico</PackageIconUrl>
<PackageLicenseUrl>http://www.apache.org/licenses/LICENSE-2.0</PackageLicenseUrl>
<PackageProjectUrl>https://github.com/JustArchi/ArchiSteamFarm</PackageProjectUrl>
<PackageProjectUrl>https://github.com/JustArchiNET/ArchiSteamFarm</PackageProjectUrl>
<RepositoryType>Git</RepositoryType>
<RepositoryUrl>https://github.com/JustArchi/ArchiSteamFarm.git</RepositoryUrl>
<RepositoryUrl>https://github.com/JustArchiNET/ArchiSteamFarm.git</RepositoryUrl>
<RuntimeIdentifiers>linux-arm;linux-x64;osx-x64;win-x64</RuntimeIdentifiers>
<ServerGarbageCollection>false</ServerGarbageCollection>
<TargetFrameworks>netcoreapp2.2;net472</TargetFrameworks>
@ -41,6 +41,7 @@
<PackageReference Include="ConfigureAwaitChecker.Analyzer" Version="2.0.0" />
<PackageReference Include="HtmlAgilityPack" Version="1.8.11" />
<PackageReference Include="Humanizer" Version="2.5.16" />
<PackageReference Include="JetBrains.Annotations" Version="2018.2.1" />
<PackageReference Include="Markdig.Signed" Version="0.15.6" />
<PackageReference Include="Microsoft.AspNetCore.Cors" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="2.2.0" />
@ -58,6 +59,7 @@
<PackageReference Include="protobuf-net" Version="3.0.0-alpha.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-beta" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="5.0.0-beta" />
<PackageReference Include="System.Composition" Version="1.3.0-preview.18571.3" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp2.2'">

File diff suppressed because it is too large Load diff

View file

@ -31,9 +31,10 @@ 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.Unified.Internal;
@ -52,24 +53,46 @@ namespace ArchiSteamFarm {
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 …
internal static readonly ConcurrentDictionary<string, Bot> Bots = new ConcurrentDictionary<string, Bot>();
[PublicAPI]
public static IReadOnlyDictionary<string, Bot> BotsReadOnly => Bots;
internal static ConcurrentDictionary<string, Bot> Bots { get; private set; }
private static readonly SemaphoreSlim BotsSemaphore = new SemaphoreSlim(1, 1);
private static readonly SemaphoreSlim LoginSemaphore = new SemaphoreSlim(1, 1);
internal readonly Actions Actions;
internal readonly ArchiHandler ArchiHandler;
internal readonly ArchiLogger ArchiLogger;
internal readonly ArchiWebHandler ArchiWebHandler;
internal readonly BotDatabase BotDatabase;
private static RegexOptions BotsRegex;
[PublicAPI]
public readonly Access Access;
[PublicAPI]
public readonly Actions Actions;
[PublicAPI]
public readonly ArchiLogger ArchiLogger;
[PublicAPI]
public readonly ArchiWebHandler ArchiWebHandler;
[JsonProperty]
internal readonly string BotName;
public readonly string BotName;
[PublicAPI]
public readonly Commands Commands;
[JsonProperty]
public bool IsConnectedAndLoggedOn => SteamClient?.SteamID != null;
[JsonProperty]
public bool IsPlayingPossible => !PlayingBlocked && (LibraryLockedBySteamID == 0);
internal readonly ArchiHandler ArchiHandler;
internal readonly BotDatabase BotDatabase;
[JsonProperty]
internal readonly CardsFarmer CardsFarmer;
internal readonly Commands Commands;
internal readonly ConcurrentDictionary<uint, (EPaymentMethod PaymentMethod, DateTime TimeCreated)> OwnedPackageIDs = new ConcurrentDictionary<uint, (EPaymentMethod PaymentMethod, DateTime TimeCreated)>();
internal readonly SteamApps SteamApps;
internal readonly SteamFriends SteamFriends;
@ -79,12 +102,6 @@ namespace ArchiSteamFarm {
internal bool IsAccountLimited => AccountFlags.HasFlag(EAccountFlags.LimitedUser) || AccountFlags.HasFlag(EAccountFlags.LimitedUserForce);
internal bool IsAccountLocked => AccountFlags.HasFlag(EAccountFlags.Lockdown);
[JsonProperty]
internal bool IsConnectedAndLoggedOn => SteamClient?.SteamID != null;
[JsonProperty]
internal bool IsPlayingPossible => !PlayingBlocked && (LibraryLockedBySteamID == 0);
private readonly CallbackManager CallbackManager;
private readonly SemaphoreSlim CallbackSemaphore = new SemaphoreSlim(1, 1);
private readonly SemaphoreSlim GamesRedeemerInBackgroundSemaphore = new SemaphoreSlim(1, 1);
@ -94,7 +111,6 @@ namespace ArchiSteamFarm {
private readonly SemaphoreSlim PICSSemaphore = new SemaphoreSlim(1, 1);
private readonly Statistics Statistics;
private readonly SteamClient SteamClient;
private readonly ConcurrentHashSet<ulong> SteamFamilySharingIDs = new ConcurrentHashSet<ulong>();
private readonly SteamUser SteamUser;
private readonly Trading Trading;
@ -111,10 +127,16 @@ namespace ArchiSteamFarm {
private string SSteamID => SteamID.ToString();
[JsonProperty]
internal BotConfig BotConfig { get; private set; }
public EAccountFlags AccountFlags { get; private set; }
[JsonProperty]
internal bool KeepRunning { get; private set; }
public bool KeepRunning { get; private set; }
[JsonProperty]
public string Nickname { get; private set; }
[JsonProperty]
internal BotConfig BotConfig { get; private set; }
internal bool PlayingBlocked { get; private set; }
internal bool PlayingWasBlocked { get; private set; }
@ -123,9 +145,6 @@ namespace ArchiSteamFarm {
internal uint WalletBalance { get; private set; }
internal ECurrencyCode WalletCurrency { get; private set; }
[JsonProperty]
private EAccountFlags AccountFlags;
private string AuthCode;
[JsonProperty]
@ -143,9 +162,6 @@ namespace ArchiSteamFarm {
private ulong LibraryLockedBySteamID;
private ulong MasterChatGroupID;
[JsonProperty]
private string Nickname;
private Timer PlayingWasBlockedTimer;
private bool ReconnectOnUserInitiated;
private Timer SendItemsTimer;
@ -227,6 +243,7 @@ namespace ArchiSteamFarm {
CallbackManager.Subscribe<ArchiHandler.UserNotificationsCallback>(OnUserNotifications);
CallbackManager.Subscribe<ArchiHandler.VanityURLChangedCallback>(OnVanityURLChangedCallback);
Access = new Access(this);
Actions = new Actions(this);
CardsFarmer = new CardsFarmer(this);
Commands = new Commands(this);
@ -236,8 +253,6 @@ namespace ArchiSteamFarm {
Statistics = new Statistics(this);
}
InitModules();
HeartBeatTimer = new Timer(
async e => await HeartBeat().ConfigureAwait(false),
null,
@ -554,7 +569,7 @@ namespace ArchiSteamFarm {
string botPattern = botName.Substring(2);
try {
IEnumerable<Bot> regexMatches = Bots.Where(kvp => Regex.IsMatch(kvp.Key, botPattern, RegexOptions.CultureInvariant)).Select(kvp => kvp.Value);
IEnumerable<Bot> regexMatches = Bots.Where(kvp => Regex.IsMatch(kvp.Key, botPattern, BotsRegex)).Select(kvp => kvp.Value);
result.UnionWith(regexMatches);
} catch (ArgumentException e) {
ASF.ArchiLogger.LogGenericWarningException(e);
@ -637,16 +652,6 @@ namespace ArchiSteamFarm {
return result;
}
internal BotConfig.EPermission GetSteamUserPermission(ulong steamID) {
if (steamID == 0) {
ArchiLogger.LogNullError(nameof(steamID));
return BotConfig.EPermission.None;
}
return BotConfig.SteamUserPermissions.TryGetValue(steamID, out BotConfig.EPermission permission) ? permission : BotConfig.EPermission.None;
}
internal async Task<byte?> GetTradeHoldDuration(ulong steamID, ulong tradeID) {
if ((steamID == 0) || (tradeID == 0)) {
ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(tradeID));
@ -748,6 +753,28 @@ namespace ArchiSteamFarm {
}
}
internal static void Init(StringComparer botsComparer) {
if (botsComparer == null) {
ASF.ArchiLogger.LogNullError(nameof(botsComparer));
return;
}
if (Bots != null) {
ASF.ArchiLogger.LogGenericError(Strings.WarningFailed);
return;
}
Bots = new ConcurrentDictionary<string, Bot>(botsComparer);
if ((botsComparer == StringComparer.InvariantCulture) || (botsComparer == StringComparer.Ordinal)) {
BotsRegex |= RegexOptions.CultureInvariant;
} else if ((botsComparer == StringComparer.InvariantCultureIgnoreCase) || (botsComparer == StringComparer.OrdinalIgnoreCase)) {
BotsRegex |= RegexOptions.CultureInvariant | RegexOptions.IgnoreCase;
}
}
internal bool IsBlacklistedFromIdling(uint appID) {
if (appID == 0) {
ArchiLogger.LogNullError(nameof(appID));
@ -768,26 +795,6 @@ namespace ArchiSteamFarm {
return BotDatabase.IsBlacklistedFromTrades(steamID);
}
internal bool IsFamilySharing(ulong steamID) {
if (steamID == 0) {
ArchiLogger.LogNullError(nameof(steamID));
return false;
}
return ASF.IsOwner(steamID) || SteamFamilySharingIDs.Contains(steamID) || (GetSteamUserPermission(steamID) >= BotConfig.EPermission.FamilySharing);
}
internal bool IsMaster(ulong steamID) {
if (steamID == 0) {
ArchiLogger.LogNullError(nameof(steamID));
return false;
}
return ASF.IsOwner(steamID) || (GetSteamUserPermission(steamID) >= BotConfig.EPermission.Master);
}
internal bool IsPriorityIdling(uint appID) {
if (appID == 0) {
ArchiLogger.LogNullError(nameof(appID));
@ -800,7 +807,7 @@ namespace ArchiSteamFarm {
internal async Task OnConfigChanged(bool deleted) {
if (deleted) {
Destroy();
await Destroy().ConfigureAwait(false);
return;
}
@ -808,7 +815,7 @@ namespace ArchiSteamFarm {
BotConfig botConfig = await BotConfig.Load(ConfigFilePath).ConfigureAwait(false);
if (botConfig == null) {
Destroy();
await Destroy().ConfigureAwait(false);
return;
}
@ -827,7 +834,7 @@ namespace ArchiSteamFarm {
Stop(botConfig.Enabled);
BotConfig = botConfig;
InitModules();
await InitModules().ConfigureAwait(false);
InitStart();
} finally {
InitializationSemaphore.Release();
@ -946,6 +953,10 @@ namespace ArchiSteamFarm {
BotsSemaphore.Release();
}
await Core.OnBotInit(bot).ConfigureAwait(false);
await bot.InitModules().ConfigureAwait(false);
bot.InitStart();
}
@ -1255,7 +1266,7 @@ namespace ArchiSteamFarm {
SteamClient.Connect();
}
private void Destroy(bool force = false) {
private async Task Destroy(bool force = false) {
if (!force) {
Stop();
} else {
@ -1264,6 +1275,7 @@ namespace ArchiSteamFarm {
}
Bots.TryRemove(BotName, out _);
await Core.OnBotDestroy(this).ConfigureAwait(false);
}
private void Disconnect() {
@ -1496,7 +1508,7 @@ namespace ArchiSteamFarm {
return;
}
SteamFamilySharingIDs.ReplaceIfNeededWith(steamIDs);
Access.SteamFamilySharingIDs.ReplaceIfNeededWith(steamIDs);
}
private bool InitLoginAndPassword(bool requiresPassword) {
@ -1523,7 +1535,7 @@ namespace ArchiSteamFarm {
return true;
}
private void InitModules() {
private async Task InitModules() {
CardsFarmer.SetInitialState(BotConfig.Paused);
if (SendItemsTimer != null) {
@ -1548,6 +1560,8 @@ namespace ArchiSteamFarm {
if (BotConfig.AutoSteamSaleEvent) {
SteamSaleEvent = new SteamSaleEvent(this);
}
await Core.OnBotInitModules(this, BotConfig.AdditionalProperties).ConfigureAwait(false);
}
private async Task InitPermanentConnectionFailure() {
@ -1556,7 +1570,7 @@ namespace ArchiSteamFarm {
}
ArchiLogger.LogGenericWarning(Strings.BotHeartBeatFailed);
Destroy(true);
await Destroy(true).ConfigureAwait(false);
await RegisterBot(BotName).ConfigureAwait(false);
}
@ -1758,6 +1772,8 @@ namespace ArchiSteamFarm {
FirstTradeSent = false;
await Core.OnBotDisconnected(this, callback.UserInitiated ? EResult.OK : lastLogOnResult).ConfigureAwait(false);
// If we initiated disconnect, do not attempt to reconnect
if (callback.UserInitiated && !ReconnectOnUserInitiated) {
return;
@ -1831,7 +1847,7 @@ namespace ArchiSteamFarm {
break;
default:
if (IsFamilySharing(friend.SteamID)) {
if (Access.IsFamilySharing(friend.SteamID)) {
await ArchiHandler.AddFriend(friend.SteamID).ConfigureAwait(false);
} else if (BotConfig.BotBehaviour.HasFlag(BotConfig.EBotBehaviour.RejectInvalidFriendInvites)) {
await ArchiHandler.RemoveFriend(friend.SteamID).ConfigureAwait(false);
@ -2145,7 +2161,10 @@ namespace ArchiSteamFarm {
Utilities.InBackground(RedeemGamesInBackground);
}
ArchiHandler.SetCurrentMode(2);
if (Core.BotUsesNewChat(this)) {
ArchiHandler.SetCurrentMode(2);
}
ArchiHandler.RequestItemAnnouncements();
// Sometimes Steam won't send us our own PersonaStateCallback, so request it explicitly
@ -2170,6 +2189,8 @@ namespace ArchiSteamFarm {
);
}
await Core.OnBotLoggedOn(this).ConfigureAwait(false);
break;
case EResult.InvalidPassword:
case EResult.NoConnection:

View file

@ -20,6 +20,7 @@
// limitations under the License.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.IO;
@ -28,7 +29,9 @@ using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Json;
using ArchiSteamFarm.Localization;
using JetBrains.Annotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SteamKit2;
namespace ArchiSteamFarm {
@ -142,6 +145,14 @@ namespace ArchiSteamFarm {
[JsonProperty(Required = Required.DisallowNull)]
internal readonly bool UseLoginKeys = DefaultUseLoginKeys;
[JsonExtensionData]
internal Dictionary<string, JToken> AdditionalProperties {
get;
[UsedImplicitly]
private set;
}
internal string DecryptedSteamPassword {
get {
if (string.IsNullOrEmpty(SteamPassword)) {
@ -360,9 +371,7 @@ namespace ArchiSteamFarm {
[Flags]
internal enum EBotBehaviour : byte {
[SuppressMessage("ReSharper", "UnusedMember.Global")]
None = 0,
RejectInvalidFriendInvites = 1,
RejectInvalidTrades = 2,
RejectInvalidGroupInvites = 4,
@ -399,9 +408,7 @@ namespace ArchiSteamFarm {
[Flags]
internal enum ERedeemingPreferences : byte {
[SuppressMessage("ReSharper", "UnusedMember.Global")]
None = 0,
Forwarding = 1,
Distributing = 2,
KeepMissingGames = 4,
@ -410,9 +417,7 @@ namespace ArchiSteamFarm {
[Flags]
internal enum ETradingPreferences : byte {
[SuppressMessage("ReSharper", "UnusedMember.Global")]
None = 0,
AcceptDonations = 1,
SteamTradeMatcher = 2,
MatchEverything = 4,

View file

@ -23,9 +23,10 @@ using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
namespace ArchiSteamFarm.Collections {
internal sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ISet<T> {
public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ISet<T> {
public int Count => BackingCollection.Count;
public bool IsReadOnly => false;
@ -114,12 +115,15 @@ namespace ArchiSteamFarm.Collections {
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
// We use Count() and not Any() because we must ensure full loop pass
internal bool AddRange(IEnumerable<T> items) => items.Count(Add) > 0;
[PublicAPI]
public bool AddRange(IEnumerable<T> items) => items.Count(Add) > 0;
// We use Count() and not Any() because we must ensure full loop pass
internal bool RemoveRange(IEnumerable<T> items) => items.Count(Remove) > 0;
[PublicAPI]
public bool RemoveRange(IEnumerable<T> items) => items.Count(Remove) > 0;
internal bool ReplaceIfNeededWith(IReadOnlyCollection<T> other) {
[PublicAPI]
public bool ReplaceIfNeededWith(IReadOnlyCollection<T> other) {
if (SetEquals(other)) {
return false;
}
@ -129,7 +133,8 @@ namespace ArchiSteamFarm.Collections {
return true;
}
internal void ReplaceWith(IEnumerable<T> other) {
[PublicAPI]
public void ReplaceWith(IEnumerable<T> other) {
BackingCollection.Clear();
foreach (T item in other) {

View file

@ -27,23 +27,52 @@ using System.Text;
using System.Threading.Tasks;
using ArchiSteamFarm.Json;
using ArchiSteamFarm.Localization;
using ArchiSteamFarm.Plugins;
using JetBrains.Annotations;
using SteamKit2;
namespace ArchiSteamFarm {
internal sealed class Commands {
public sealed class Commands {
private readonly Bot Bot;
private readonly Dictionary<uint, string> CachedGamesOwned = new Dictionary<uint, string>();
internal Commands(Bot bot) => Bot = bot ?? throw new ArgumentNullException(nameof(bot));
internal void OnNewLicenseList() {
lock (CachedGamesOwned) {
CachedGamesOwned.Clear();
CachedGamesOwned.TrimExcess();
[PublicAPI]
public 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<string> Response(ulong steamID, string message) {
[PublicAPI]
public string FormatBotResponse(string response) {
if (string.IsNullOrEmpty(response)) {
ASF.ArchiLogger.LogNullError(nameof(response));
return null;
}
return "<" + Bot.BotName + "> " + response;
}
[PublicAPI]
public static string FormatStaticResponse(string response) {
if (string.IsNullOrEmpty(response)) {
ASF.ArchiLogger.LogNullError(nameof(response));
return null;
}
return "<" + SharedInfo.ASF + "> " + response;
}
[PublicAPI]
public async Task<string> Response(ulong steamID, string message) {
if ((steamID == 0) || string.IsNullOrEmpty(message)) {
Bot.ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(message));
@ -52,7 +81,9 @@ namespace ArchiSteamFarm {
if (!string.IsNullOrEmpty(Program.GlobalConfig.CommandPrefix)) {
if (!message.StartsWith(Program.GlobalConfig.CommandPrefix, StringComparison.Ordinal)) {
return null;
string pluginsResponse = await Core.OnBotMessage(Bot, steamID, message).ConfigureAwait(false);
return !string.IsNullOrEmpty(pluginsResponse) ? pluginsResponse : null;
}
message = message.Substring(Program.GlobalConfig.CommandPrefix.Length);
@ -144,8 +175,9 @@ namespace ArchiSteamFarm {
return ResponseVersion(steamID);
default:
string pluginsResponse = await Core.OnBotCommand(Bot, steamID, message, args).ConfigureAwait(false);
return ResponseUnknown(steamID);
return !string.IsNullOrEmpty(pluginsResponse) ? pluginsResponse : ResponseUnknown(steamID);
}
default:
@ -332,50 +364,18 @@ namespace ArchiSteamFarm {
return await ResponseUnpackBoosters(steamID, Utilities.GetArgsAsText(args, 1, ",")).ConfigureAwait(false);
default:
string pluginsResponse = await Core.OnBotCommand(Bot, steamID, message, args).ConfigureAwait(false);
return ResponseUnknown(steamID);
return !string.IsNullOrEmpty(pluginsResponse) ? pluginsResponse : ResponseUnknown(steamID);
}
}
}
private static string FormatBotResponse(string response, string botName) {
if (string.IsNullOrEmpty(response) || string.IsNullOrEmpty(botName)) {
ASF.ArchiLogger.LogNullError(nameof(response) + " || " + nameof(botName));
return null;
internal void OnNewLicenseList() {
lock (CachedGamesOwned) {
CachedGamesOwned.Clear();
CachedGamesOwned.TrimExcess();
}
return Environment.NewLine + "<" + botName + "> " + response;
}
private string FormatBotResponse(string response) {
if (string.IsNullOrEmpty(response)) {
ASF.ArchiLogger.LogNullError(nameof(response));
return null;
}
return "<" + Bot.BotName + "> " + response;
}
private static string FormatStaticResponse(string response) {
if (string.IsNullOrEmpty(response)) {
ASF.ArchiLogger.LogNullError(nameof(response));
return null;
}
return "<" + SharedInfo.ASF + "> " + response;
}
private bool IsOperator(ulong steamID) {
if (steamID == 0) {
Bot.ArchiLogger.LogNullError(nameof(steamID));
return false;
}
return ASF.IsOwner(steamID) || (Bot.GetSteamUserPermission(steamID) >= BotConfig.EPermission.Operator);
}
private async Task<string> Response2FA(ulong steamID) {
@ -385,7 +385,7 @@ namespace ArchiSteamFarm {
return null;
}
if (!Bot.IsMaster(steamID)) {
if (!Bot.Access.IsMaster(steamID)) {
return null;
}
@ -425,7 +425,7 @@ namespace ArchiSteamFarm {
return null;
}
if (!Bot.IsMaster(steamID)) {
if (!Bot.Access.IsMaster(steamID)) {
return null;
}
@ -469,7 +469,7 @@ namespace ArchiSteamFarm {
return null;
}
if (!IsOperator(steamID)) {
if (!Bot.Access.IsOperator(steamID)) {
return null;
}
@ -516,7 +516,7 @@ namespace ArchiSteamFarm {
return null;
}
if (!IsOperator(steamID)) {
if (!Bot.Access.IsOperator(steamID)) {
return null;
}
@ -570,7 +570,7 @@ namespace ArchiSteamFarm {
return null;
}
if (!Bot.IsMaster(steamID)) {
if (!Bot.Access.IsMaster(steamID)) {
return null;
}
@ -618,7 +618,7 @@ namespace ArchiSteamFarm {
return null;
}
if (!IsOperator(steamID)) {
if (!Bot.Access.IsOperator(steamID)) {
return null;
}
@ -700,7 +700,7 @@ namespace ArchiSteamFarm {
return null;
}
if (!Bot.IsMaster(steamID)) {
if (!Bot.Access.IsMaster(steamID)) {
return null;
}
@ -778,7 +778,7 @@ namespace ArchiSteamFarm {
return null;
}
if (!Bot.IsMaster(steamID)) {
if (!Bot.Access.IsMaster(steamID)) {
return null;
}
@ -814,7 +814,7 @@ namespace ArchiSteamFarm {
return null;
}
if (!Bot.IsMaster(steamID)) {
if (!Bot.Access.IsMaster(steamID)) {
return null;
}
@ -866,7 +866,7 @@ namespace ArchiSteamFarm {
return null;
}
if (!Bot.IsMaster(steamID)) {
if (!Bot.Access.IsMaster(steamID)) {
return null;
}
@ -934,7 +934,7 @@ namespace ArchiSteamFarm {
return null;
}
if (!Bot.IsMaster(steamID)) {
if (!Bot.Access.IsMaster(steamID)) {
return null;
}
@ -978,7 +978,7 @@ namespace ArchiSteamFarm {
return null;
}
return Bot.IsFamilySharing(steamID) ? FormatBotResponse(SharedInfo.ProjectURL + "/wiki/Commands") : null;
return Bot.Access.IsFamilySharing(steamID) ? FormatBotResponse(SharedInfo.ProjectURL + "/wiki/Commands") : null;
}
private string ResponseIdleBlacklist(ulong steamID) {
@ -988,7 +988,7 @@ namespace ArchiSteamFarm {
return null;
}
if (!Bot.IsMaster(steamID)) {
if (!Bot.Access.IsMaster(steamID)) {
return null;
}
@ -1024,7 +1024,7 @@ namespace ArchiSteamFarm {
return null;
}
if (!Bot.IsMaster(steamID)) {
if (!Bot.Access.IsMaster(steamID)) {
return null;
}
@ -1076,7 +1076,7 @@ namespace ArchiSteamFarm {
return null;
}
if (!Bot.IsMaster(steamID)) {
if (!Bot.Access.IsMaster(steamID)) {
return null;
}
@ -1128,7 +1128,7 @@ namespace ArchiSteamFarm {
return null;
}
if (!Bot.IsMaster(steamID)) {
if (!Bot.Access.IsMaster(steamID)) {
return null;
}
@ -1164,7 +1164,7 @@ namespace ArchiSteamFarm {
return null;
}
if (!Bot.IsMaster(steamID)) {
if (!Bot.Access.IsMaster(steamID)) {
return null;
}
@ -1216,7 +1216,7 @@ namespace ArchiSteamFarm {
return null;
}
if (!Bot.IsMaster(steamID)) {
if (!Bot.Access.IsMaster(steamID)) {
return null;
}
@ -1268,7 +1268,7 @@ namespace ArchiSteamFarm {
return null;
}
if (!Bot.IsMaster(steamID)) {
if (!Bot.Access.IsMaster(steamID)) {
return null;
}
@ -1316,7 +1316,7 @@ namespace ArchiSteamFarm {
return FormatBotResponse(Strings.BotNotConnected);
}
if (!Bot.IsMaster(steamID)) {
if (!Bot.Access.IsMaster(steamID)) {
return null;
}
@ -1352,7 +1352,7 @@ namespace ArchiSteamFarm {
return null;
}
if (!Bot.IsMaster(steamID)) {
if (!Bot.Access.IsMaster(steamID)) {
return null;
}
@ -1396,7 +1396,7 @@ namespace ArchiSteamFarm {
return null;
}
if (!Bot.IsMaster(steamID)) {
if (!Bot.Access.IsMaster(steamID)) {
return null;
}
@ -1456,7 +1456,7 @@ namespace ArchiSteamFarm {
return null;
}
if (!Bot.IsMaster(steamID)) {
if (!Bot.Access.IsMaster(steamID)) {
return null;
}
@ -1496,7 +1496,7 @@ namespace ArchiSteamFarm {
return (null, null);
}
if (!IsOperator(steamID)) {
if (!Bot.Access.IsOperator(steamID)) {
return (null, null);
}
@ -1615,7 +1615,7 @@ namespace ArchiSteamFarm {
return null;
}
if (!Bot.IsMaster(steamID)) {
if (!Bot.Access.IsMaster(steamID)) {
return null;
}
@ -1658,11 +1658,11 @@ namespace ArchiSteamFarm {
return null;
}
if (!Bot.IsFamilySharing(steamID)) {
if (!Bot.Access.IsFamilySharing(steamID)) {
return null;
}
if (permanent && !IsOperator(steamID)) {
if (permanent && !Bot.Access.IsOperator(steamID)) {
return FormatBotResponse(Strings.ErrorAccessDenied);
}
@ -1704,7 +1704,7 @@ namespace ArchiSteamFarm {
return null;
}
if (!Bot.IsMaster(steamID)) {
if (!Bot.Access.IsMaster(steamID)) {
return null;
}
@ -1728,7 +1728,7 @@ namespace ArchiSteamFarm {
return null;
}
if (!Bot.IsMaster(steamID)) {
if (!Bot.Access.IsMaster(steamID)) {
return null;
}
@ -1789,7 +1789,7 @@ namespace ArchiSteamFarm {
return null;
}
if (!Bot.IsMaster(steamID)) {
if (!Bot.Access.IsMaster(steamID)) {
return null;
}
@ -1940,7 +1940,7 @@ namespace ArchiSteamFarm {
return null;
}
if (!IsOperator(steamID)) {
if (!Bot.Access.IsOperator(steamID)) {
return null;
}
@ -1970,7 +1970,7 @@ namespace ArchiSteamFarm {
while (!string.IsNullOrEmpty(key)) {
string startingKey = key;
using (IEnumerator<Bot> botsEnumerator = Bot.Bots.Where(bot => (bot.Value != Bot) && !rateLimitedBots.Contains(bot.Value) && bot.Value.IsConnectedAndLoggedOn && bot.Value.Commands.IsOperator(steamID)).OrderBy(bot => bot.Key).Select(bot => bot.Value).GetEnumerator()) {
using (IEnumerator<Bot> botsEnumerator = Bot.Bots.Where(bot => (bot.Value != Bot) && !rateLimitedBots.Contains(bot.Value) && bot.Value.IsConnectedAndLoggedOn && bot.Value.Commands.Bot.Access.IsOperator(steamID)).OrderBy(bot => bot.Key).Select(bot => bot.Value).GetEnumerator()) {
Bot currentBot = Bot;
while (!string.IsNullOrEmpty(key) && (currentBot != null)) {
@ -2051,7 +2051,7 @@ namespace ArchiSteamFarm {
bool alreadyHandled = false;
foreach (Bot innerBot in Bot.Bots.Where(bot => (bot.Value != currentBot) && (!redeemFlags.HasFlag(ERedeemFlags.SkipInitial) || (bot.Value != Bot)) && !rateLimitedBots.Contains(bot.Value) && bot.Value.IsConnectedAndLoggedOn && bot.Value.Commands.IsOperator(steamID) && ((items.Count == 0) || items.Keys.Any(packageID => !bot.Value.OwnedPackageIDs.ContainsKey(packageID)))).OrderBy(bot => bot.Key).Select(bot => bot.Value)) {
foreach (Bot innerBot in Bot.Bots.Where(bot => (bot.Value != currentBot) && (!redeemFlags.HasFlag(ERedeemFlags.SkipInitial) || (bot.Value != Bot)) && !rateLimitedBots.Contains(bot.Value) && bot.Value.IsConnectedAndLoggedOn && bot.Value.Commands.Bot.Access.IsOperator(steamID) && ((items.Count == 0) || items.Keys.Any(packageID => !bot.Value.OwnedPackageIDs.ContainsKey(packageID)))).OrderBy(bot => bot.Key).Select(bot => bot.Value)) {
ArchiHandler.PurchaseResponseCallback otherResult = await innerBot.Actions.RedeemKey(key).ConfigureAwait(false);
if (otherResult == null) {
@ -2183,7 +2183,7 @@ namespace ArchiSteamFarm {
return null;
}
if (!Bot.IsFamilySharing(steamID)) {
if (!Bot.Access.IsFamilySharing(steamID)) {
return null;
}
@ -2219,7 +2219,7 @@ namespace ArchiSteamFarm {
return null;
}
if (!Bot.IsMaster(steamID)) {
if (!Bot.Access.IsMaster(steamID)) {
return null;
}
@ -2271,7 +2271,7 @@ namespace ArchiSteamFarm {
return (null, Bot);
}
if (!Bot.IsFamilySharing(steamID)) {
if (!Bot.Access.IsFamilySharing(steamID)) {
return (null, Bot);
}
@ -2343,7 +2343,7 @@ namespace ArchiSteamFarm {
return null;
}
if (!Bot.IsMaster(steamID)) {
if (!Bot.Access.IsMaster(steamID)) {
return null;
}
@ -2379,7 +2379,7 @@ namespace ArchiSteamFarm {
return null;
}
if (!Bot.IsMaster(steamID)) {
if (!Bot.Access.IsMaster(steamID)) {
return null;
}
@ -2435,7 +2435,7 @@ namespace ArchiSteamFarm {
return null;
}
if (!Bot.IsMaster(steamID)) {
if (!Bot.Access.IsMaster(steamID)) {
return null;
}
@ -2467,7 +2467,7 @@ namespace ArchiSteamFarm {
return null;
}
if (!Bot.IsMaster(steamID)) {
if (!Bot.Access.IsMaster(steamID)) {
return null;
}
@ -2541,7 +2541,7 @@ namespace ArchiSteamFarm {
return null;
}
return IsOperator(steamID) ? FormatBotResponse(Strings.UnknownCommand) : null;
return Bot.Access.IsOperator(steamID) ? FormatBotResponse(Strings.UnknownCommand) : null;
}
private async Task<string> ResponseUnpackBoosters(ulong steamID) {
@ -2551,7 +2551,7 @@ namespace ArchiSteamFarm {
return null;
}
if (!Bot.IsMaster(steamID)) {
if (!Bot.Access.IsMaster(steamID)) {
return null;
}
@ -2617,7 +2617,7 @@ namespace ArchiSteamFarm {
return null;
}
return IsOperator(steamID) ? FormatBotResponse(string.Format(Strings.BotVersion, SharedInfo.ASF, SharedInfo.Version)) : null;
return Bot.Access.IsOperator(steamID) ? FormatBotResponse(string.Format(Strings.BotVersion, SharedInfo.ASF, SharedInfo.Version)) : null;
}
private string ResponseWalletBalance(ulong steamID) {
@ -2627,7 +2627,7 @@ namespace ArchiSteamFarm {
return null;
}
if (!Bot.IsMaster(steamID)) {
if (!Bot.Access.IsMaster(steamID)) {
return null;
}

View file

@ -93,7 +93,7 @@ namespace ArchiSteamFarm {
return null;
}
WebBrowser.ObjectResponse<ReleaseResponse> objectResponse = await Program.WebBrowser.UrlGetToJsonObject<ReleaseResponse>(releaseURL).ConfigureAwait(false);
WebBrowser.ObjectResponse<ReleaseResponse> objectResponse = await ASF.WebBrowser.UrlGetToJsonObject<ReleaseResponse>(releaseURL).ConfigureAwait(false);
return objectResponse?.Content;
}
@ -105,7 +105,7 @@ namespace ArchiSteamFarm {
return null;
}
WebBrowser.ObjectResponse<List<ReleaseResponse>> objectResponse = await Program.WebBrowser.UrlGetToJsonObject<List<ReleaseResponse>>(releaseURL).ConfigureAwait(false);
WebBrowser.ObjectResponse<List<ReleaseResponse>> objectResponse = await ASF.WebBrowser.UrlGetToJsonObject<List<ReleaseResponse>>(releaseURL).ConfigureAwait(false);
return objectResponse?.Content;
}

View file

@ -20,6 +20,7 @@
// limitations under the License.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.IO;
@ -27,7 +28,9 @@ using System.Net;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Localization;
using JetBrains.Annotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SteamKit2;
namespace ArchiSteamFarm {
@ -183,6 +186,14 @@ namespace ArchiSteamFarm {
}
}
[JsonExtensionData]
[PublicAPI]
internal Dictionary<string, JToken> AdditionalProperties {
get;
[UsedImplicitly]
private set;
}
internal bool IsWebProxyPasswordSet { get; private set; }
internal bool ShouldSerializeEverything { private get; set; } = true;
internal bool ShouldSerializeHelperProperties { private get; set; } = true;
@ -333,11 +344,10 @@ namespace ArchiSteamFarm {
MinMemoryUsage
}
[PublicAPI]
internal enum EUpdateChannel : byte {
None,
Stable,
[SuppressMessage("ReSharper", "UnusedMember.Global")]
Experimental
}

View file

@ -22,9 +22,10 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
namespace ArchiSteamFarm.Helpers {
internal sealed class ArchiCacheable<T> : IDisposable {
public sealed class ArchiCacheable<T> : IDisposable {
private readonly TimeSpan CacheLifetime;
private readonly SemaphoreSlim InitSemaphore = new SemaphoreSlim(1, 1);
private readonly Func<Task<(bool Success, T Result)>> ResolveFunction;
@ -53,7 +54,8 @@ namespace ArchiSteamFarm.Helpers {
MaintenanceTimer?.Dispose();
}
internal async Task<(bool Success, T Result)> GetValue() {
[PublicAPI]
public async Task<(bool Success, T Result)> GetValue() {
if (IsInitialized && IsRecent) {
return (true, InitializedValue);
}

View file

@ -132,7 +132,7 @@ namespace ArchiSteamFarm.IPC.Controllers.Api {
return BadRequest(new GenericResponse<string>(false, string.Format(Strings.ErrorIsInvalid, nameof(request.URL))));
}
WebBrowser.StringResponse urlResponse = await Program.WebBrowser.UrlGetToString(request.URL).ConfigureAwait(false);
WebBrowser.StringResponse urlResponse = await ASF.WebBrowser.UrlGetToString(request.URL).ConfigureAwait(false);
if (urlResponse?.Content == null) {
return BadRequest(new GenericResponse<string>(false, string.Format(Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries)));

View file

@ -26,6 +26,7 @@ using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
@ -53,7 +54,7 @@ namespace ArchiSteamFarm.IPC.Middleware {
public ApiAuthenticationMiddleware(RequestDelegate next) => Next = next ?? throw new ArgumentNullException(nameof(next));
[SuppressMessage("ReSharper", "UnusedMember.Global")]
[PublicAPI]
public async Task InvokeAsync(HttpContext context) {
if (context == null) {
ASF.ArchiLogger.LogNullError(nameof(context));

View file

@ -25,27 +25,43 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using ArchiSteamFarm.Localization;
using HtmlAgilityPack;
using JetBrains.Annotations;
using Newtonsoft.Json;
using SteamKit2;
namespace ArchiSteamFarm.Json {
internal static class Steam {
public static class Steam {
// REF: https://developer.valvesoftware.com/wiki/Steam_Web_API/IEconService#CEcon_Asset
internal sealed class Asset {
internal const uint SteamAppID = 753;
internal const uint SteamCommunityContextID = 6;
public sealed class Asset {
[PublicAPI]
public const uint SteamAppID = 753;
internal uint Amount { get; set; }
[PublicAPI]
public const uint SteamCommunityContextID = 6;
[PublicAPI]
public uint Amount { get; internal set; }
[JsonProperty(PropertyName = "appid", Required = Required.DisallowNull)]
internal uint AppID { get; private set; }
public uint AppID { get; private set; }
internal ulong AssetID { get; private set; }
internal ulong ClassID { get; private set; }
internal ulong ContextID { get; private set; }
internal uint RealAppID { get; set; }
internal bool Tradable { get; set; }
internal EType Type { get; set; }
[PublicAPI]
public ulong AssetID { get; private set; }
[PublicAPI]
public ulong ClassID { get; private set; }
[PublicAPI]
public ulong ContextID { get; private set; }
[PublicAPI]
public uint RealAppID { get; internal set; }
[PublicAPI]
public bool Tradable { get; internal set; }
[PublicAPI]
public EType Type { get; internal set; }
[JsonProperty(PropertyName = "amount", Required = Required.Always)]
private string AmountText {
@ -135,8 +151,8 @@ namespace ArchiSteamFarm.Json {
set => AssetIDText = value;
}
// Constructed from trades being received
internal Asset(uint appID, ulong contextID, ulong classID, uint amount, uint realAppID, EType type = EType.Unknown) {
// Constructed from trades being received or plugins
public Asset(uint appID, ulong contextID, ulong classID, uint amount, uint realAppID, EType type = EType.Unknown) {
if ((appID == 0) || (contextID == 0) || (classID == 0) || (amount == 0) || (realAppID == 0)) {
throw new ArgumentNullException(nameof(appID) + " || " + nameof(contextID) + " || " + nameof(classID) + " || " + nameof(amount) + " || " + nameof(realAppID));
}
@ -152,7 +168,7 @@ namespace ArchiSteamFarm.Json {
// Deserialized from JSON
private Asset() { }
internal enum EType : byte {
public enum EType : byte {
Unknown,
BoosterPack,
Emoticon,
@ -164,16 +180,16 @@ namespace ArchiSteamFarm.Json {
}
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
internal class BooleanResponse {
public class BooleanResponse {
[JsonProperty(PropertyName = "success", Required = Required.Always)]
internal readonly bool Success;
public readonly bool Success;
// Deserialized from JSON
protected BooleanResponse() { }
}
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
internal sealed class ConfirmationDetails : BooleanResponse {
public sealed class ConfirmationDetails : BooleanResponse {
internal MobileAuthenticator.Confirmation Confirmation { get; set; }
internal ulong TradeOfferID { get; private set; }
internal EType Type { get; private set; }
@ -252,8 +268,8 @@ namespace ArchiSteamFarm.Json {
private ConfirmationDetails() { }
// REF: Internal documentation
[SuppressMessage("ReSharper", "UnusedMember.Global")]
internal enum EType : byte {
[PublicAPI]
public enum EType : byte {
Unknown,
Generic,
Trade,
@ -265,14 +281,101 @@ namespace ArchiSteamFarm.Json {
}
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
internal class EResultResponse {
public class EResultResponse {
[JsonProperty(PropertyName = "success", Required = Required.Always)]
internal readonly EResult Result;
public readonly EResult Result;
// Deserialized from JSON
protected EResultResponse() { }
}
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
public class NumberResponse {
[PublicAPI]
public bool Success { get; private set; }
[JsonProperty(PropertyName = "success", Required = Required.Always)]
private byte SuccessNumber {
set {
switch (value) {
case 0:
Success = false;
break;
case 1:
Success = true;
break;
default:
ASF.ArchiLogger.LogGenericError(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(value), value));
return;
}
}
}
// Deserialized from JSON
protected NumberResponse() { }
}
// REF: https://developer.valvesoftware.com/wiki/Steam_Web_API/IEconService#CEcon_TradeOffer
public sealed class TradeOffer {
[PublicAPI]
public readonly ulong OtherSteamID64;
[PublicAPI]
public readonly ETradeOfferState State;
[PublicAPI]
public readonly ulong TradeOfferID;
[PublicAPI]
public IReadOnlyCollection<Asset> ItemsToGiveReadOnly => ItemsToGive;
[PublicAPI]
public IReadOnlyCollection<Asset> ItemsToReceiveReadOnly => ItemsToReceive;
internal readonly HashSet<Asset> ItemsToGive = new HashSet<Asset>();
internal readonly HashSet<Asset> ItemsToReceive = new HashSet<Asset>();
// Constructed from trades being received
internal TradeOffer(ulong tradeOfferID, uint otherSteamID3, ETradeOfferState state) {
if ((tradeOfferID == 0) || (otherSteamID3 == 0) || (state == ETradeOfferState.Unknown)) {
throw new ArgumentNullException(nameof(tradeOfferID) + " || " + nameof(otherSteamID3) + " || " + nameof(state));
}
TradeOfferID = tradeOfferID;
OtherSteamID64 = new SteamID(otherSteamID3, EUniverse.Public, EAccountType.Individual);
State = state;
}
internal bool IsValidSteamItemsRequest(IReadOnlyCollection<Asset.EType> acceptedTypes) {
if ((acceptedTypes == null) || (acceptedTypes.Count == 0)) {
ASF.ArchiLogger.LogNullError(nameof(acceptedTypes));
return false;
}
return ItemsToGive.All(item => (item.AppID == Asset.SteamAppID) && (item.ContextID == Asset.SteamCommunityContextID) && acceptedTypes.Contains(item.Type));
}
[PublicAPI]
public enum ETradeOfferState : byte {
Unknown,
Invalid,
Active,
Accepted,
Countered,
Expired,
Canceled,
Declined,
InvalidItems,
EmailPending,
EmailCanceled,
OnHold
}
}
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
internal sealed class InventoryResponse : NumberResponse {
[JsonProperty(PropertyName = "assets", Required = Required.DisallowNull)]
@ -368,34 +471,6 @@ namespace ArchiSteamFarm.Json {
private NewDiscoveryQueueResponse() { }
}
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
internal class NumberResponse {
internal bool Success { get; private set; }
[JsonProperty(PropertyName = "success", Required = Required.Always)]
private byte SuccessNumber {
set {
switch (value) {
case 0:
Success = false;
break;
case 1:
Success = true;
break;
default:
ASF.ArchiLogger.LogGenericError(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(value), value));
return;
}
}
}
// Deserialized from JSON
protected NumberResponse() { }
}
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
internal sealed class RedeemWalletResponse : EResultResponse {
[JsonProperty(PropertyName = "wallet", Required = Required.DisallowNull)]
@ -419,52 +494,6 @@ namespace ArchiSteamFarm.Json {
}
}
// REF: https://developer.valvesoftware.com/wiki/Steam_Web_API/IEconService#CEcon_TradeOffer
internal sealed class TradeOffer {
internal readonly HashSet<Asset> ItemsToGive = new HashSet<Asset>();
internal readonly HashSet<Asset> ItemsToReceive = new HashSet<Asset>();
internal readonly ulong OtherSteamID64;
internal readonly ETradeOfferState State;
internal readonly ulong TradeOfferID;
// Constructed from trades being received
internal TradeOffer(ulong tradeOfferID, uint otherSteamID3, ETradeOfferState state) {
if ((tradeOfferID == 0) || (otherSteamID3 == 0) || (state == ETradeOfferState.Unknown)) {
throw new ArgumentNullException(nameof(tradeOfferID) + " || " + nameof(otherSteamID3) + " || " + nameof(state));
}
TradeOfferID = tradeOfferID;
OtherSteamID64 = new SteamID(otherSteamID3, EUniverse.Public, EAccountType.Individual);
State = state;
}
internal bool IsValidSteamItemsRequest(IReadOnlyCollection<Asset.EType> acceptedTypes) {
if ((acceptedTypes == null) || (acceptedTypes.Count == 0)) {
ASF.ArchiLogger.LogNullError(nameof(acceptedTypes));
return false;
}
return ItemsToGive.All(item => (item.AppID == Asset.SteamAppID) && (item.ContextID == Asset.SteamCommunityContextID) && acceptedTypes.Contains(item.Type));
}
[SuppressMessage("ReSharper", "UnusedMember.Global")]
internal enum ETradeOfferState : byte {
Unknown,
Invalid,
Active,
Accepted,
Countered,
Expired,
Canceled,
Declined,
InvalidItems,
EmailPending,
EmailCanceled,
OnHold
}
}
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
internal sealed class TradeOfferAcceptResponse {
[JsonProperty(PropertyName = "needs_mobile_confirmation", Required = Required.DisallowNull)]

View file

@ -1125,6 +1125,15 @@ namespace ArchiSteamFarm.Localization {
}
}
/// <summary>
/// Wyszukuje zlokalizowany ciąg podobny do ciągu Nothing found!.
/// </summary>
internal static string NothingFound {
get {
return ResourceManager.GetString("NothingFound", resourceCulture);
}
}
/// <summary>
/// Wyszukuje zlokalizowany ciąg podobny do ciągu We don&apos;t have anything to idle on this account!.
/// </summary>
@ -1161,6 +1170,33 @@ namespace ArchiSteamFarm.Localization {
}
}
/// <summary>
/// Wyszukuje zlokalizowany ciąg podobny do ciągu {0} has been loaded successfully!.
/// </summary>
internal static string PluginLoaded {
get {
return ResourceManager.GetString("PluginLoaded", resourceCulture);
}
}
/// <summary>
/// Wyszukuje zlokalizowany ciąg podobny do ciągu Loading {0} V{1}....
/// </summary>
internal static string PluginLoading {
get {
return ResourceManager.GetString("PluginLoading", resourceCulture);
}
}
/// <summary>
/// Wyszukuje zlokalizowany ciąg podobny do ciągu You&apos;ve loaded one or more of custom plugins into the ASF. Since we&apos;re unable to offer a support for modded setups, please reach the appropriate developers of the plugins that you decided to use in case of any issues..
/// </summary>
internal static string PluginsWarning {
get {
return ResourceManager.GetString("PluginsWarning", resourceCulture);
}
}
/// <summary>
/// Wyszukuje zlokalizowany ciąg podobny do ciągu Refreshing our session!.
/// </summary>

View file

@ -679,4 +679,18 @@ StackTrace:
<value>You're running more personal bot accounts than our upper recommended limit ({0}). Be advised that this setup is not supported and might cause various Steam-related issues, including account suspensions. Check out the FAQ for more details.</value>
<comment>{0} will be replaced by our maximum recommended bots count (number)</comment>
</data>
<data name="PluginLoaded" xml:space="preserve">
<value>{0} has been loaded successfully!</value>
<comment>{0} will be replaced by the name of the custom ASF plugin</comment>
</data>
<data name="PluginLoading" xml:space="preserve">
<value>Loading {0} V{1}...</value>
<comment>{0} will be replaced by the name of the custom ASF plugin, {1} will be replaced by its version</comment>
</data>
<data name="NothingFound" xml:space="preserve">
<value>Nothing found!</value>
</data>
<data name="PluginsWarning" xml:space="preserve">
<value>You've loaded one or more of custom plugins into the ASF. Since we're unable to offer a support for modded setups, please reach the appropriate developers of the plugins that you decided to use in case of any issues.</value>
</data>
</root>

View file

@ -24,13 +24,14 @@ using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using ArchiSteamFarm.Localization;
using JetBrains.Annotations;
using NLog;
namespace ArchiSteamFarm.NLog {
internal sealed class ArchiLogger {
public sealed class ArchiLogger {
private readonly Logger Logger;
internal ArchiLogger(string name) {
public ArchiLogger(string name) {
if (string.IsNullOrEmpty(name)) {
throw new ArgumentNullException(nameof(name));
}
@ -38,6 +39,107 @@ namespace ArchiSteamFarm.NLog {
Logger = LogManager.GetLogger(name);
}
[PublicAPI]
public void LogGenericDebug(string message, [CallerMemberName] string previousMethodName = null) {
if (string.IsNullOrEmpty(message)) {
LogNullError(nameof(message));
return;
}
Logger.Debug($"{previousMethodName}() {message}");
}
[PublicAPI]
public void LogGenericDebuggingException(Exception exception, [CallerMemberName] string previousMethodName = null) {
if (exception == null) {
LogNullError(nameof(exception));
return;
}
if (!Debugging.IsUserDebugging) {
return;
}
Logger.Debug(exception, $"{previousMethodName}()");
}
[PublicAPI]
public void LogGenericError(string message, [CallerMemberName] string previousMethodName = null) {
if (string.IsNullOrEmpty(message)) {
LogNullError(nameof(message));
return;
}
Logger.Error($"{previousMethodName}() {message}");
}
[PublicAPI]
public void LogGenericException(Exception exception, [CallerMemberName] string previousMethodName = null) {
if (exception == null) {
LogNullError(nameof(exception));
return;
}
Logger.Error(exception, $"{previousMethodName}()");
}
[PublicAPI]
public void LogGenericInfo(string message, [CallerMemberName] string previousMethodName = null) {
if (string.IsNullOrEmpty(message)) {
LogNullError(nameof(message));
return;
}
Logger.Info($"{previousMethodName}() {message}");
}
[PublicAPI]
public void LogGenericTrace(string message, [CallerMemberName] string previousMethodName = null) {
if (string.IsNullOrEmpty(message)) {
LogNullError(nameof(message));
return;
}
Logger.Trace($"{previousMethodName}() {message}");
}
[PublicAPI]
public void LogGenericWarning(string message, [CallerMemberName] string previousMethodName = null) {
if (string.IsNullOrEmpty(message)) {
LogNullError(nameof(message));
return;
}
Logger.Warn($"{previousMethodName}() {message}");
}
[PublicAPI]
public void LogGenericWarningException(Exception exception, [CallerMemberName] string previousMethodName = null) {
if (exception == null) {
LogNullError(nameof(exception));
return;
}
Logger.Warn(exception, $"{previousMethodName}()");
}
[PublicAPI]
public void LogNullError(string nullObjectName, [CallerMemberName] string previousMethodName = null) {
if (string.IsNullOrEmpty(nullObjectName)) {
return;
}
LogGenericError(string.Format(Strings.ErrorObjectIsNull, nullObjectName), previousMethodName);
}
internal void LogChatMessage(bool echo, string message, ulong chatGroupID = 0, ulong chatID = 0, ulong steamID = 0, [CallerMemberName] string previousMethodName = null) {
if (string.IsNullOrEmpty(message) || (((chatGroupID == 0) || (chatID == 0)) && (steamID == 0))) {
LogNullError(nameof(message) + " || " + "((" + nameof(chatGroupID) + " || " + nameof(chatID) + ") && " + nameof(steamID) + ")");
@ -120,97 +222,5 @@ namespace ArchiSteamFarm.NLog {
break;
}
}
internal void LogGenericDebug(string message, [CallerMemberName] string previousMethodName = null) {
if (string.IsNullOrEmpty(message)) {
LogNullError(nameof(message));
return;
}
Logger.Debug($"{previousMethodName}() {message}");
}
internal void LogGenericDebuggingException(Exception exception, [CallerMemberName] string previousMethodName = null) {
if (exception == null) {
LogNullError(nameof(exception));
return;
}
if (!Debugging.IsUserDebugging) {
return;
}
Logger.Debug(exception, $"{previousMethodName}()");
}
internal void LogGenericError(string message, [CallerMemberName] string previousMethodName = null) {
if (string.IsNullOrEmpty(message)) {
LogNullError(nameof(message));
return;
}
Logger.Error($"{previousMethodName}() {message}");
}
internal void LogGenericException(Exception exception, [CallerMemberName] string previousMethodName = null) {
if (exception == null) {
LogNullError(nameof(exception));
return;
}
Logger.Error(exception, $"{previousMethodName}()");
}
internal void LogGenericInfo(string message, [CallerMemberName] string previousMethodName = null) {
if (string.IsNullOrEmpty(message)) {
LogNullError(nameof(message));
return;
}
Logger.Info($"{previousMethodName}() {message}");
}
internal void LogGenericTrace(string message, [CallerMemberName] string previousMethodName = null) {
if (string.IsNullOrEmpty(message)) {
LogNullError(nameof(message));
return;
}
Logger.Trace($"{previousMethodName}() {message}");
}
internal void LogGenericWarning(string message, [CallerMemberName] string previousMethodName = null) {
if (string.IsNullOrEmpty(message)) {
LogNullError(nameof(message));
return;
}
Logger.Warn($"{previousMethodName}() {message}");
}
internal void LogGenericWarningException(Exception exception, [CallerMemberName] string previousMethodName = null) {
if (exception == null) {
LogNullError(nameof(exception));
return;
}
Logger.Warn(exception, $"{previousMethodName}()");
}
internal void LogNullError(string nullObjectName, [CallerMemberName] string previousMethodName = null) {
if (string.IsNullOrEmpty(nullObjectName)) {
return;
}
LogGenericError(string.Format(Strings.ErrorObjectIsNull, nullObjectName), previousMethodName);
}
}
}

View file

@ -21,8 +21,8 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using ArchiSteamFarm.Collections;
using JetBrains.Annotations;
using NLog;
using NLog.Targets;
@ -38,7 +38,7 @@ namespace ArchiSteamFarm.NLog {
private readonly FixedSizeConcurrentQueue<string> HistoryQueue = new FixedSizeConcurrentQueue<string>(DefaultMaxCount);
// This is NLog config property, it must have public get() and set() capabilities
[SuppressMessage("ReSharper", "UnusedMember.Global")]
[PublicAPI]
public byte MaxCount {
get => HistoryQueue.MaxCount;
@ -55,7 +55,7 @@ namespace ArchiSteamFarm.NLog {
// This parameter-less constructor is intentionally public, as NLog uses it for creating targets
// It must stay like this as we want to have our targets defined in our NLog.config
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
[PublicAPI]
public HistoryTarget() { }
internal HistoryTarget(string name) : this() => Name = name;

View file

@ -22,6 +22,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using JetBrains.Annotations;
using NLog;
using NLog.Config;
using NLog.Targets;
@ -33,18 +34,15 @@ namespace ArchiSteamFarm.NLog {
internal const string TargetName = "Steam";
// This is NLog config property, it must have public get() and set() capabilities
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")]
[PublicAPI]
public string BotName { get; set; }
// This is NLog config property, it must have public get() and set() capabilities
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")]
[PublicAPI]
public ulong ChatGroupID { get; set; }
// This is NLog config property, it must have public get() and set() capabilities
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")]
[PublicAPI]
[RequiredParameter]
public ulong SteamID { get; set; }

View file

@ -0,0 +1,361 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
//
// Copyright 2015-2019 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Composition.Convention;
using System.Composition.Hosting;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using ArchiSteamFarm.Json;
using ArchiSteamFarm.Localization;
using Newtonsoft.Json.Linq;
using SteamKit2;
namespace ArchiSteamFarm.Plugins {
internal static class Core {
[ImportMany]
private static ImmutableHashSet<IPlugin> ActivePlugins { get; set; }
internal static bool BotUsesNewChat(Bot bot) {
if (bot == null) {
ASF.ArchiLogger.LogNullError(nameof(bot));
return false;
}
if ((ActivePlugins == null) || (ActivePlugins.Count == 0)) {
return true;
}
foreach (IBotHackNewChat plugin in ActivePlugins.OfType<IBotHackNewChat>()) {
try {
if (!plugin.BotUsesNewChat(bot)) {
return false;
}
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
}
}
return true;
}
internal static async Task<StringComparer> GetBotsComparer() {
if ((ActivePlugins == null) || (ActivePlugins.Count == 0)) {
return StringComparer.Ordinal;
}
IList<StringComparer> results;
try {
results = await Utilities.InParallel(ActivePlugins.OfType<IBotsComparer>().Select(plugin => Task.Run(() => plugin.BotsComparer))).ConfigureAwait(false);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
return StringComparer.Ordinal;
}
StringComparer result = results.FirstOrDefault(comparer => comparer != null);
return result ?? StringComparer.Ordinal;
}
internal static bool InitPlugins() {
string pluginsPath = Path.Combine(SharedInfo.HomeDirectory, SharedInfo.PluginsDirectory);
if (!Directory.Exists(pluginsPath)) {
ASF.ArchiLogger.LogGenericTrace(Strings.NothingFound);
return true;
}
ASF.ArchiLogger.LogGenericInfo(string.Format(Strings.Initializing, nameof(Plugins)));
HashSet<Assembly> assemblies = new HashSet<Assembly>();
try {
foreach (string assemblyPath in Directory.EnumerateFiles(pluginsPath, "*.dll")) {
Assembly assembly;
try {
assembly = Assembly.LoadFrom(assemblyPath);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
continue;
}
assemblies.Add(assembly);
}
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
return false;
}
if (assemblies.Count == 0) {
ASF.ArchiLogger.LogGenericInfo(Strings.NothingFound);
return true;
}
ConventionBuilder conventions = new ConventionBuilder();
conventions.ForTypesDerivedFrom<IPlugin>().Export<IPlugin>();
ContainerConfiguration configuration = new ContainerConfiguration().WithAssemblies(assemblies, conventions);
try {
using (CompositionHost container = configuration.CreateContainer()) {
ActivePlugins = container.GetExports<IPlugin>().ToImmutableHashSet();
}
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
return false;
}
HashSet<IPlugin> invalidPlugins = new HashSet<IPlugin>();
foreach (IPlugin plugin in ActivePlugins) {
try {
string pluginName = plugin.Name;
ASF.ArchiLogger.LogGenericInfo(string.Format(Strings.PluginLoading, pluginName, plugin.Version));
plugin.OnLoaded();
ASF.ArchiLogger.LogGenericInfo(string.Format(Strings.PluginLoaded, pluginName));
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
invalidPlugins.Add(plugin);
}
}
ImmutableHashSet<IPlugin> activePlugins = ActivePlugins.Except(invalidPlugins);
if (activePlugins.Count == 0) {
ActivePlugins = null;
return false;
}
ActivePlugins = activePlugins;
ASF.ArchiLogger.LogGenericInfo(Strings.PluginsWarning);
return invalidPlugins.Count == 0;
}
internal static async Task OnASFInitModules(IReadOnlyDictionary<string, JToken> additionalConfigProperties = null) {
if ((ActivePlugins == null) || (ActivePlugins.Count == 0)) {
return;
}
try {
await Utilities.InParallel(ActivePlugins.OfType<IASF>().Select(plugin => Task.Run(() => plugin.OnASFInit(additionalConfigProperties)))).ConfigureAwait(false);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
}
}
internal static async Task<string> OnBotCommand(Bot bot, ulong steamID, string message, string[] args) {
if ((bot == null) || (steamID == 0) || string.IsNullOrEmpty(message) || (args == null) || (args.Length == 0)) {
ASF.ArchiLogger.LogNullError(nameof(bot) + " || " + nameof(args));
return null;
}
if ((ActivePlugins == null) || (ActivePlugins.Count == 0)) {
return null;
}
IList<string> responses;
try {
responses = await Utilities.InParallel(ActivePlugins.OfType<IBotCommand>().Select(plugin => plugin.OnBotCommand(bot, steamID, message, args))).ConfigureAwait(false);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
return null;
}
return string.Join(Environment.NewLine, responses.Where(response => !string.IsNullOrEmpty(response)));
}
internal static async Task OnBotDestroy(Bot bot) {
if (bot == null) {
ASF.ArchiLogger.LogNullError(nameof(bot));
return;
}
if ((ActivePlugins == null) || (ActivePlugins.Count == 0)) {
return;
}
try {
await Utilities.InParallel(ActivePlugins.OfType<IBot>().Select(plugin => Task.Run(() => plugin.OnBotDestroy(bot)))).ConfigureAwait(false);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
}
}
internal static async Task OnBotDisconnected(Bot bot, EResult reason) {
if (bot == null) {
ASF.ArchiLogger.LogNullError(nameof(bot));
return;
}
if ((ActivePlugins == null) || (ActivePlugins.Count == 0)) {
return;
}
try {
await Utilities.InParallel(ActivePlugins.OfType<IBotConnection>().Select(plugin => Task.Run(() => plugin.OnBotDisconnected(bot, reason)))).ConfigureAwait(false);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
}
}
internal static async Task OnBotInit(Bot bot) {
if (bot == null) {
ASF.ArchiLogger.LogNullError(nameof(bot));
return;
}
if ((ActivePlugins == null) || (ActivePlugins.Count == 0)) {
return;
}
try {
await Utilities.InParallel(ActivePlugins.OfType<IBot>().Select(plugin => Task.Run(() => plugin.OnBotInit(bot)))).ConfigureAwait(false);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
}
}
internal static async Task OnBotInitModules(Bot bot, IReadOnlyDictionary<string, JToken> additionalConfigProperties = null) {
if (bot == null) {
ASF.ArchiLogger.LogNullError(nameof(bot));
return;
}
if ((ActivePlugins == null) || (ActivePlugins.Count == 0)) {
return;
}
try {
await Utilities.InParallel(ActivePlugins.OfType<IBotModules>().Select(plugin => Task.Run(() => plugin.OnBotInitModules(bot, additionalConfigProperties)))).ConfigureAwait(false);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
}
}
internal static async Task OnBotLoggedOn(Bot bot) {
if (bot == null) {
ASF.ArchiLogger.LogNullError(nameof(bot));
return;
}
if ((ActivePlugins == null) || (ActivePlugins.Count == 0)) {
return;
}
try {
await Utilities.InParallel(ActivePlugins.OfType<IBotConnection>().Select(plugin => Task.Run(() => plugin.OnBotLoggedOn(bot)))).ConfigureAwait(false);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
}
}
internal static async Task<string> OnBotMessage(Bot bot, ulong steamID, string message) {
if ((bot == null) || (steamID == 0) || string.IsNullOrEmpty(message)) {
ASF.ArchiLogger.LogNullError(nameof(bot) + " || " + nameof(message));
return null;
}
if ((ActivePlugins == null) || (ActivePlugins.Count == 0)) {
return null;
}
IList<string> responses;
try {
responses = await Utilities.InParallel(ActivePlugins.OfType<IBotMessage>().Select(plugin => plugin.OnBotMessage(bot, steamID, message))).ConfigureAwait(false);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
return null;
}
return string.Join(Environment.NewLine, responses.Where(response => !string.IsNullOrEmpty(response)));
}
internal static async Task<bool> OnBotTradeOffer(Bot bot, Steam.TradeOffer tradeOffer) {
if ((bot == null) || (tradeOffer == null)) {
ASF.ArchiLogger.LogNullError(nameof(bot) + " || " + nameof(tradeOffer));
return false;
}
if ((ActivePlugins == null) || (ActivePlugins.Count == 0)) {
return false;
}
IList<bool> responses;
try {
responses = await Utilities.InParallel(ActivePlugins.OfType<IBotTradeOffer>().Select(plugin => plugin.OnBotTradeOffer(bot, tradeOffer))).ConfigureAwait(false);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
return false;
}
return responses.Any(response => response);
}
internal static async Task OnBotTradeOfferResults(Bot bot, IReadOnlyCollection<Trading.ParseTradeResult> tradeResults) {
if ((bot == null) || (tradeResults == null) || (tradeResults.Count == 0)) {
ASF.ArchiLogger.LogNullError(nameof(bot) + " || " + nameof(tradeResults));
return;
}
if ((ActivePlugins == null) || (ActivePlugins.Count == 0)) {
return;
}
try {
await Utilities.InParallel(ActivePlugins.OfType<IBotTradeOfferResults>().Select(plugin => Task.Run(() => plugin.OnBotTradeOfferResults(bot, tradeResults)))).ConfigureAwait(false);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
}
}
}
}

View file

@ -0,0 +1,36 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
//
// Copyright 2015-2019 Ł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.Collections.Generic;
using JetBrains.Annotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace ArchiSteamFarm.Plugins {
[PublicAPI]
public interface IASF : IPlugin {
/// <summary>
/// ASF will call this method right after global config initialization.
/// </summary>
/// <param name="additionalConfigProperties">Extra config properties made out of <see cref="JsonExtensionDataAttribute" />. Can be null if no extra properties are found.</param>
void OnASFInit([CanBeNull] IReadOnlyDictionary<string, JToken> additionalConfigProperties = null);
}
}

View file

@ -0,0 +1,42 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
//
// Copyright 2015-2019 Ł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 JetBrains.Annotations;
namespace ArchiSteamFarm.Plugins {
[PublicAPI]
public interface IBot : IPlugin {
/// <summary>
/// ASF will call this method after removing its own references from it, e.g. after config removal.
/// You should ensure that you'll remove any of your own references to this bot instance in timely manner.
/// Doing so will allow the garbage collector to dispose the bot afterwards, refraining from doing so will create a "memory leak" by keeping the reference alive.
/// </summary>
/// <param name="bot">Bot object related to this callback.</param>
void OnBotDestroy([NotNull] Bot bot);
/// <summary>
/// ASF will call this method after creating the bot object, e.g. after config creation.
/// Bot config is not yet available at this stage. This function will execute only once for every bot object.
/// </summary>
/// <param name="bot">Bot object related to this callback.</param>
void OnBotInit([NotNull] Bot bot);
}
}

View file

@ -0,0 +1,39 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
//
// Copyright 2015-2019 Ł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.Threading.Tasks;
using JetBrains.Annotations;
namespace ArchiSteamFarm.Plugins {
[PublicAPI]
public interface IBotCommand : IPlugin {
/// <summary>
/// ASF will call this method for unrecognized commands.
/// </summary>
/// <param name="bot">Bot object related to this callback.</param>
/// <param name="steamID">64-bit long unsigned integer of steamID executing the command.</param>
/// <param name="message">Command message in its raw format, stripped of <see cref="GlobalConfig.CommandPrefix" />.</param>
/// <param name="args">Pre-parsed message using standard ASF delimiters.</param>
/// <returns>Response to the command, or null/empty (as the task value) if the command isn't handled by this plugin.</returns>
[NotNull]
Task<string> OnBotCommand([NotNull] Bot bot, ulong steamID, [NotNull] string message, [NotNull] string[] args);
}
}

View file

@ -0,0 +1,41 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
//
// Copyright 2015-2019 Ł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 JetBrains.Annotations;
using SteamKit2;
namespace ArchiSteamFarm.Plugins {
[PublicAPI]
public interface IBotConnection : IPlugin {
/// <summary>
/// ASF will call this method when bot gets disconnected from Steam network.
/// </summary>
/// <param name="bot">Bot object related to this callback.</param>
/// <param name="reason">Reason for disconnection, or <see cref="EResult.OK" /> if the disconnection was initiated by ASF (e.g. as a result of a command).</param>
void OnBotDisconnected([NotNull] Bot bot, EResult reason);
/// <summary>
/// ASF will call this method when bot successfully connects to Steam network.
/// </summary>
/// <param name="bot">Bot object related to this callback.</param>
void OnBotLoggedOn([NotNull] Bot bot);
}
}

View file

@ -0,0 +1,35 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
//
// Copyright 2015-2019 Ł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 JetBrains.Annotations;
namespace ArchiSteamFarm.Plugins {
[PublicAPI]
public interface IBotHackNewChat : IPlugin {
/// <summary>
/// ASF will use this property for determining whether the bot should use new chat system.
/// Unless you know what you're doing, you should not implement this property yourself and let ASF decide.
/// </summary>
/// <param name="bot">Bot object related to this callback.</param>
/// <returns>Boolean indicating whether the bot should use new chat system.</returns>
bool BotUsesNewChat([NotNull] Bot bot);
}
}

View file

@ -0,0 +1,38 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
//
// Copyright 2015-2019 Ł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.Threading.Tasks;
using JetBrains.Annotations;
namespace ArchiSteamFarm.Plugins {
[PublicAPI]
public interface IBotMessage : IPlugin {
/// <summary>
/// ASF will call this method for messages that are not commands, so ones that do not start from <see cref="GlobalConfig.CommandPrefix" />.
/// </summary>
/// <param name="bot">Bot object related to this callback.</param>
/// <param name="steamID">64-bit long unsigned integer of steamID executing the command.</param>
/// <param name="message">Message in its raw format.</param>
/// <returns>Response to the message, or null/empty (as the task value) for silence.</returns>
[NotNull]
Task<string> OnBotMessage([NotNull] Bot bot, ulong steamID, [NotNull] string message);
}
}

View file

@ -0,0 +1,37 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
//
// Copyright 2015-2019 Ł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.Collections.Generic;
using JetBrains.Annotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace ArchiSteamFarm.Plugins {
[PublicAPI]
public interface IBotModules : IPlugin {
/// <summary>
/// ASF will call this method right after bot config initialization.
/// </summary>
/// <param name="bot">Bot object related to this callback.</param>
/// <param name="additionalConfigProperties">Extra config properties made out of <see cref="JsonExtensionDataAttribute" />. Can be null if no extra properties are found.</param>
void OnBotInitModules([NotNull] Bot bot, [CanBeNull] IReadOnlyDictionary<string, JToken> additionalConfigProperties = null);
}
}

View file

@ -0,0 +1,38 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
//
// Copyright 2015-2019 Ł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.Threading.Tasks;
using ArchiSteamFarm.Json;
using JetBrains.Annotations;
namespace ArchiSteamFarm.Plugins {
[PublicAPI]
public interface IBotTradeOffer : IPlugin {
/// <summary>
/// ASF will call this method for unhandled (ignored and rejected) trade offers received by the bot.
/// </summary>
/// <param name="bot">Bot object related to this callback.</param>
/// <param name="tradeOffer">Trade offer related to this callback.</param>
/// <returns>True if the trade offer should be accepted as part of this plugin, false otherwise.</returns>
[NotNull]
Task<bool> OnBotTradeOffer([NotNull] Bot bot, [NotNull] Steam.TradeOffer tradeOffer);
}
}

View file

@ -0,0 +1,35 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
//
// Copyright 2015-2019 Ł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.Collections.Generic;
using JetBrains.Annotations;
namespace ArchiSteamFarm.Plugins {
[PublicAPI]
public interface IBotTradeOfferResults : IPlugin {
/// <summary>
/// ASF will call this method for notifying you about the result of each received trade offer being handled. The method is executed for each batch that can contain 1 or more trade offers.
/// </summary>
/// <param name="bot">Bot object related to this callback.</param>
/// <param name="tradeResults">Trade results related to this callback.</param>
void OnBotTradeOfferResults([NotNull] Bot bot, [NotNull] IReadOnlyCollection<Trading.ParseTradeResult> tradeResults);
}
}

View file

@ -0,0 +1,36 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
//
// Copyright 2015-2019 Ł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 JetBrains.Annotations;
namespace ArchiSteamFarm.Plugins {
[PublicAPI]
public interface IBotsComparer : IPlugin {
/// <summary>
/// ASF will use this property for determining the comparer for the bots.
/// Unless you know what you're doing, you should not implement this property yourself and let ASF decide.
/// </summary>
/// <returns>Comparer that will be used for the bots, as well as bot regexes.</returns>
[NotNull]
StringComparer BotsComparer { get; }
}
}

View file

@ -0,0 +1,48 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
//
// Copyright 2015-2019 Ł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 JetBrains.Annotations;
namespace ArchiSteamFarm.Plugins {
[PublicAPI]
public interface IPlugin {
/// <summary>
/// ASF will use this property as general plugin identifier for the user.
/// </summary>
/// <returns>String that will be used as the name of this plugin.</returns>
[NotNull]
string Name { get; }
/// <summary>
/// ASF will use this property as version indicator of your plugin to the user.
/// You have a freedom in deciding what versionining you want to use, this is for identification purposes only.
/// </summary>
/// <returns>Version that will be shown to the user when plugin is loaded.</returns>
[NotNull]
Version Version { get; }
/// <summary>
/// ASF will call this method right after plugin initialization.
/// </summary>
void OnLoaded();
}
}

View file

@ -44,7 +44,6 @@ namespace ArchiSteamFarm {
internal static GlobalDatabase GlobalDatabase { get; private set; }
internal static bool ProcessRequired { get; private set; }
internal static bool RestartAllowed { get; private set; } = true;
internal static WebBrowser WebBrowser { get; private set; }
private static readonly object ConsoleLock = new object();
@ -215,11 +214,7 @@ namespace ArchiSteamFarm {
OS.Init(SystemRequired, GlobalConfig.OptimizationMode);
await InitGlobalDatabaseAndServices().ConfigureAwait(false);
await ASF.UpdateAndRestart().ConfigureAwait(false);
await ASF.InitBots().ConfigureAwait(false);
ASF.InitEvents();
await ASF.Init().ConfigureAwait(false);
}
private static void InitCore(IReadOnlyCollection<string> args) {
@ -370,11 +365,6 @@ namespace ArchiSteamFarm {
}
WebBrowser.Init();
WebBrowser = new WebBrowser(ASF.ArchiLogger, GlobalConfig.WebProxy, true);
if (GlobalConfig.IPC) {
await ArchiKestrel.Start().ConfigureAwait(false);
}
}
private static async Task<bool> InitShutdownSequence() {

View file

@ -24,6 +24,7 @@ using System.Diagnostics;
using System.Threading.Tasks;
#if NETFRAMEWORK
using JetBrains.Annotations;
using System.Collections.Generic;
using System.Net.WebSockets;
using System.Threading;
@ -100,6 +101,7 @@ namespace ArchiSteamFarm {
}
string result = path.Substring(relativeTo.Length);
return (result[0] == System.IO.Path.DirectorySeparatorChar) || (result[0] == System.IO.Path.AltDirectorySeparatorChar) ? result.Substring(1) : result;
#else
#pragma warning disable IDE0022
@ -113,6 +115,8 @@ namespace ArchiSteamFarm {
internal static async Task<WebSocketReceiveResult> ReceiveAsync(this WebSocket webSocket, byte[] buffer, CancellationToken cancellationToken) => await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), cancellationToken).ConfigureAwait(false);
internal static async Task SendAsync(this WebSocket webSocket, byte[] buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) => await webSocket.SendAsync(new ArraySegment<byte>(buffer), messageType, endOfMessage, cancellationToken).ConfigureAwait(false);
internal static string[] Split(this string text, char separator, StringSplitOptions options = StringSplitOptions.None) => text.Split(new[] { separator }, options);
[PublicAPI]
internal static void TrimExcess<T1, T2>(this Dictionary<T1, T2> _) { } // no-op
#endif
}

View file

@ -46,6 +46,7 @@ namespace ArchiSteamFarm {
internal const string LicenseURL = "http://www.apache.org/licenses/LICENSE-2.0";
internal const string LogFile = "log.txt";
internal const string MobileAuthenticatorExtension = ".maFile";
internal const string PluginsDirectory = "plugins";
internal const string ProjectURL = "https://github.com/" + GithubRepo;
internal const string SentryHashExtension = ".bin";
internal const string StatisticsServer = "asf.justarchi.net";

View file

@ -101,7 +101,7 @@ namespace ArchiSteamFarm {
};
// Listing is free to deny our announce request, hence we don't retry
if (await Program.WebBrowser.UrlPost(request, data, maxTries: 1).ConfigureAwait(false) != null) {
if (await Bot.ArchiWebHandler.WebBrowser.UrlPost(request, data, maxTries: 1).ConfigureAwait(false) != null) {
LastHeartBeat = DateTime.UtcNow;
}
} finally {
@ -176,16 +176,16 @@ namespace ArchiSteamFarm {
};
// Listing is free to deny our announce request, hence we don't retry
ShouldSendHeartBeats = await Program.WebBrowser.UrlPost(request, data, maxTries: 1).ConfigureAwait(false) != null;
ShouldSendHeartBeats = await Bot.ArchiWebHandler.WebBrowser.UrlPost(request, data, maxTries: 1).ConfigureAwait(false) != null;
} finally {
RequestsSemaphore.Release();
}
}
private static async Task<HashSet<ListedUser>> GetListedUsers() {
private async Task<HashSet<ListedUser>> GetListedUsers() {
const string request = URL + "/Api/Bots";
WebBrowser.ObjectResponse<HashSet<ListedUser>> objectResponse = await Program.WebBrowser.UrlGetToJsonObject<HashSet<ListedUser>>(request).ConfigureAwait(false);
WebBrowser.ObjectResponse<HashSet<ListedUser>> objectResponse = await Bot.ArchiWebHandler.WebBrowser.UrlGetToJsonObject<HashSet<ListedUser>>(request).ConfigureAwait(false);
return objectResponse?.Content;
}

View file

@ -27,9 +27,11 @@ using System.Threading.Tasks;
using ArchiSteamFarm.Collections;
using ArchiSteamFarm.Json;
using ArchiSteamFarm.Localization;
using ArchiSteamFarm.Plugins;
using JetBrains.Annotations;
namespace ArchiSteamFarm {
internal sealed class Trading : IDisposable {
public sealed class Trading : IDisposable {
internal const byte MaxItemsPerTrade = byte.MaxValue; // This is due to limit on POST size in WebBrowser
internal const byte MaxTradesPerAccount = 5; // This is limit introduced by Valve
@ -427,6 +429,8 @@ namespace ArchiSteamFarm {
// If we finished a trade, perform a loot if user wants to do so
await Bot.Actions.SendTradeOffer(wantedTypes: Bot.BotConfig.LootableTypes).ConfigureAwait(false);
}
await Core.OnBotTradeOfferResults(Bot, results.Select(result => result.TradeResult).ToHashSet()).ConfigureAwait(false);
}
private async Task<(ParseTradeResult TradeResult, bool RequiresMobileConfirmation)> ParseTrade(Steam.TradeOffer tradeOffer) {
@ -457,6 +461,18 @@ namespace ArchiSteamFarm {
return (null, false);
}
switch (result.Result) {
case ParseTradeResult.EResult.Ignored:
case ParseTradeResult.EResult.Rejected:
bool accept = await Core.OnBotTradeOffer(Bot, tradeOffer).ConfigureAwait(false);
if (accept) {
result.Result = ParseTradeResult.EResult.Accepted;
}
break;
}
switch (result.Result) {
case ParseTradeResult.EResult.Accepted:
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.AcceptingTrade, tradeOffer.TradeOfferID));
@ -510,7 +526,7 @@ namespace ArchiSteamFarm {
if (tradeOffer.OtherSteamID64 != 0) {
// Always accept trades from SteamMasterID
if (Bot.IsMaster(tradeOffer.OtherSteamID64)) {
if (Bot.Access.IsMaster(tradeOffer.OtherSteamID64)) {
return new ParseTradeResult(tradeOffer.TradeOfferID, ParseTradeResult.EResult.Accepted, tradeOffer.ItemsToReceive);
}
@ -609,11 +625,14 @@ namespace ArchiSteamFarm {
return new ParseTradeResult(tradeOffer.TradeOfferID, accept ? ParseTradeResult.EResult.Accepted : ParseTradeResult.EResult.Rejected, tradeOffer.ItemsToReceive);
}
private sealed class ParseTradeResult {
internal readonly HashSet<Steam.Asset.EType> ReceivingItemTypes;
internal readonly ulong TradeOfferID;
public sealed class ParseTradeResult {
[PublicAPI]
public readonly ulong TradeOfferID;
internal EResult Result { get; set; }
internal readonly HashSet<Steam.Asset.EType> ReceivingItemTypes;
[PublicAPI]
public EResult Result { get; internal set; }
internal ParseTradeResult(ulong tradeOfferID, EResult result, IReadOnlyCollection<Steam.Asset> itemsToReceive = null) {
if ((tradeOfferID == 0) || (result == EResult.Unknown)) {
@ -628,7 +647,7 @@ namespace ArchiSteamFarm {
}
}
internal enum EResult : byte {
public enum EResult : byte {
Unknown,
Accepted,
Blacklisted,

View file

@ -29,10 +29,11 @@ using System.Xml;
using ArchiSteamFarm.Localization;
using ArchiSteamFarm.NLog;
using HtmlAgilityPack;
using JetBrains.Annotations;
using Newtonsoft.Json;
namespace ArchiSteamFarm {
internal sealed class WebBrowser : IDisposable {
public sealed class WebBrowser : IDisposable {
internal const byte MaxConnections = 10; // Defines maximum number of connections per ServicePoint. Be careful, as it also defines maximum number of sockets in CLOSE_WAIT state
internal const byte MaxTries = 5; // Defines maximum number of recommended tries for a single request
@ -73,6 +74,200 @@ namespace ArchiSteamFarm {
HttpClientHandler.Dispose();
}
[PublicAPI]
public async Task<HtmlDocumentResponse> UrlGetToHtmlDocument(string request, string referer = null, byte maxTries = MaxTries) {
if (string.IsNullOrEmpty(request) || (maxTries == 0)) {
ArchiLogger.LogNullError(nameof(request) + " || " + nameof(maxTries));
return null;
}
StringResponse response = await UrlGetToString(request, referer, maxTries).ConfigureAwait(false);
return response != null ? new HtmlDocumentResponse(response) : null;
}
[PublicAPI]
public async Task<ObjectResponse<T>> UrlGetToJsonObject<T>(string request, string referer = null, byte maxTries = MaxTries) where T : class {
if (string.IsNullOrEmpty(request) || (maxTries == 0)) {
ArchiLogger.LogNullError(nameof(request) + " || " + nameof(maxTries));
return null;
}
for (byte i = 0; i < maxTries; i++) {
StringResponse response = await UrlGetToString(request, referer, 1).ConfigureAwait(false);
if (string.IsNullOrEmpty(response?.Content)) {
continue;
}
T obj;
try {
obj = JsonConvert.DeserializeObject<T>(response.Content);
} catch (JsonException e) {
ArchiLogger.LogGenericWarningException(e);
if (Debugging.IsUserDebugging) {
ArchiLogger.LogGenericDebug(string.Format(Strings.Content, response.Content));
}
continue;
}
return new ObjectResponse<T>(response, obj);
}
if (maxTries > 1) {
ArchiLogger.LogGenericWarning(string.Format(Strings.ErrorRequestFailedTooManyTimes, maxTries));
ArchiLogger.LogGenericDebug(string.Format(Strings.ErrorFailingRequest, request));
}
return null;
}
[PublicAPI]
public async Task<XmlDocumentResponse> UrlGetToXmlDocument(string request, string referer = null, byte maxTries = MaxTries) {
if (string.IsNullOrEmpty(request) || (maxTries == 0)) {
ArchiLogger.LogNullError(nameof(request) + " || " + nameof(maxTries));
return null;
}
for (byte i = 0; i < maxTries; i++) {
StringResponse response = await UrlGetToString(request, referer, 1).ConfigureAwait(false);
if (string.IsNullOrEmpty(response?.Content)) {
continue;
}
XmlDocument xmlDocument = new XmlDocument();
try {
xmlDocument.LoadXml(response.Content);
} catch (XmlException e) {
ArchiLogger.LogGenericWarningException(e);
continue;
}
return new XmlDocumentResponse(response, xmlDocument);
}
if (maxTries > 1) {
ArchiLogger.LogGenericWarning(string.Format(Strings.ErrorRequestFailedTooManyTimes, maxTries));
ArchiLogger.LogGenericDebug(string.Format(Strings.ErrorFailingRequest, request));
}
return null;
}
[PublicAPI]
public async Task<BasicResponse> UrlHead(string request, string referer = null, byte maxTries = MaxTries) {
if (string.IsNullOrEmpty(request) || (maxTries == 0)) {
ArchiLogger.LogNullError(nameof(request) + " || " + nameof(maxTries));
return null;
}
for (byte i = 0; i < maxTries; i++) {
using (HttpResponseMessage response = await InternalHead(request, referer).ConfigureAwait(false)) {
if (response == null) {
continue;
}
return new BasicResponse(response);
}
}
if (maxTries > 1) {
ArchiLogger.LogGenericWarning(string.Format(Strings.ErrorRequestFailedTooManyTimes, maxTries));
ArchiLogger.LogGenericDebug(string.Format(Strings.ErrorFailingRequest, request));
}
return null;
}
[PublicAPI]
public async Task<BasicResponse> UrlPost(string request, IReadOnlyCollection<KeyValuePair<string, string>> data = null, string referer = null, byte maxTries = MaxTries) {
if (string.IsNullOrEmpty(request) || (maxTries == 0)) {
ArchiLogger.LogNullError(nameof(request) + " || " + nameof(maxTries));
return null;
}
for (byte i = 0; i < maxTries; i++) {
using (HttpResponseMessage response = await InternalPost(request, data, referer).ConfigureAwait(false)) {
if (response == null) {
continue;
}
return new BasicResponse(response);
}
}
if (maxTries > 1) {
ArchiLogger.LogGenericWarning(string.Format(Strings.ErrorRequestFailedTooManyTimes, maxTries));
ArchiLogger.LogGenericDebug(string.Format(Strings.ErrorFailingRequest, request));
}
return null;
}
[PublicAPI]
public async Task<HtmlDocumentResponse> UrlPostToHtmlDocument(string request, IReadOnlyCollection<KeyValuePair<string, string>> data = null, string referer = null, byte maxTries = MaxTries) {
if (string.IsNullOrEmpty(request) || (maxTries == 0)) {
ArchiLogger.LogNullError(nameof(request) + " || " + nameof(maxTries));
return null;
}
StringResponse response = await UrlPostToString(request, data, referer, maxTries).ConfigureAwait(false);
return response != null ? new HtmlDocumentResponse(response) : null;
}
[PublicAPI]
public async Task<ObjectResponse<T>> UrlPostToJsonObject<T>(string request, IReadOnlyCollection<KeyValuePair<string, string>> data = null, string referer = null, byte maxTries = MaxTries) where T : class {
if (string.IsNullOrEmpty(request) || (maxTries == 0)) {
ArchiLogger.LogNullError(nameof(request) + " || " + nameof(maxTries));
return null;
}
for (byte i = 0; i < maxTries; i++) {
StringResponse response = await UrlPostToString(request, data, referer, maxTries).ConfigureAwait(false);
if (string.IsNullOrEmpty(response?.Content)) {
continue;
}
T obj;
try {
obj = JsonConvert.DeserializeObject<T>(response.Content);
} catch (JsonException e) {
ArchiLogger.LogGenericWarningException(e);
if (Debugging.IsUserDebugging) {
ArchiLogger.LogGenericDebug(string.Format(Strings.Content, response.Content));
}
continue;
}
return new ObjectResponse<T>(response, obj);
}
if (maxTries > 1) {
ArchiLogger.LogGenericWarning(string.Format(Strings.ErrorRequestFailedTooManyTimes, maxTries));
ArchiLogger.LogGenericDebug(string.Format(Strings.ErrorFailingRequest, request));
}
return null;
}
internal HttpClient GenerateDisposableHttpClient(bool extendedTimeout = false) {
HttpClient result = new HttpClient(HttpClientHandler) {
Timeout = TimeSpan.FromSeconds(extendedTimeout ? ExtendedTimeoutMultiplier * Program.GlobalConfig.ConnectionTimeout : Program.GlobalConfig.ConnectionTimeout)
@ -185,57 +380,6 @@ namespace ArchiSteamFarm {
return null;
}
internal async Task<HtmlDocumentResponse> UrlGetToHtmlDocument(string request, string referer = null, byte maxTries = MaxTries) {
if (string.IsNullOrEmpty(request) || (maxTries == 0)) {
ArchiLogger.LogNullError(nameof(request) + " || " + nameof(maxTries));
return null;
}
StringResponse response = await UrlGetToString(request, referer, maxTries).ConfigureAwait(false);
return response != null ? new HtmlDocumentResponse(response) : null;
}
internal async Task<ObjectResponse<T>> UrlGetToJsonObject<T>(string request, string referer = null, byte maxTries = MaxTries) where T : class {
if (string.IsNullOrEmpty(request) || (maxTries == 0)) {
ArchiLogger.LogNullError(nameof(request) + " || " + nameof(maxTries));
return null;
}
for (byte i = 0; i < maxTries; i++) {
StringResponse response = await UrlGetToString(request, referer, 1).ConfigureAwait(false);
if (string.IsNullOrEmpty(response?.Content)) {
continue;
}
T obj;
try {
obj = JsonConvert.DeserializeObject<T>(response.Content);
} catch (JsonException e) {
ArchiLogger.LogGenericWarningException(e);
if (Debugging.IsUserDebugging) {
ArchiLogger.LogGenericDebug(string.Format(Strings.Content, response.Content));
}
continue;
}
return new ObjectResponse<T>(response, obj);
}
if (maxTries > 1) {
ArchiLogger.LogGenericWarning(string.Format(Strings.ErrorRequestFailedTooManyTimes, maxTries));
ArchiLogger.LogGenericDebug(string.Format(Strings.ErrorFailingRequest, request));
}
return null;
}
internal async Task<StringResponse> UrlGetToString(string request, string referer = null, byte maxTries = MaxTries) {
if (string.IsNullOrEmpty(request) || (maxTries == 0)) {
ArchiLogger.LogNullError(nameof(request) + " || " + nameof(maxTries));
@ -261,142 +405,6 @@ namespace ArchiSteamFarm {
return null;
}
internal async Task<XmlDocumentResponse> UrlGetToXmlDocument(string request, string referer = null, byte maxTries = MaxTries) {
if (string.IsNullOrEmpty(request) || (maxTries == 0)) {
ArchiLogger.LogNullError(nameof(request) + " || " + nameof(maxTries));
return null;
}
for (byte i = 0; i < maxTries; i++) {
StringResponse response = await UrlGetToString(request, referer, 1).ConfigureAwait(false);
if (string.IsNullOrEmpty(response?.Content)) {
continue;
}
XmlDocument xmlDocument = new XmlDocument();
try {
xmlDocument.LoadXml(response.Content);
} catch (XmlException e) {
ArchiLogger.LogGenericWarningException(e);
continue;
}
return new XmlDocumentResponse(response, xmlDocument);
}
if (maxTries > 1) {
ArchiLogger.LogGenericWarning(string.Format(Strings.ErrorRequestFailedTooManyTimes, maxTries));
ArchiLogger.LogGenericDebug(string.Format(Strings.ErrorFailingRequest, request));
}
return null;
}
internal async Task<BasicResponse> UrlHead(string request, string referer = null, byte maxTries = MaxTries) {
if (string.IsNullOrEmpty(request) || (maxTries == 0)) {
ArchiLogger.LogNullError(nameof(request) + " || " + nameof(maxTries));
return null;
}
for (byte i = 0; i < maxTries; i++) {
using (HttpResponseMessage response = await InternalHead(request, referer).ConfigureAwait(false)) {
if (response == null) {
continue;
}
return new BasicResponse(response);
}
}
if (maxTries > 1) {
ArchiLogger.LogGenericWarning(string.Format(Strings.ErrorRequestFailedTooManyTimes, maxTries));
ArchiLogger.LogGenericDebug(string.Format(Strings.ErrorFailingRequest, request));
}
return null;
}
internal async Task<BasicResponse> UrlPost(string request, IReadOnlyCollection<KeyValuePair<string, string>> data = null, string referer = null, byte maxTries = MaxTries) {
if (string.IsNullOrEmpty(request) || (maxTries == 0)) {
ArchiLogger.LogNullError(nameof(request) + " || " + nameof(maxTries));
return null;
}
for (byte i = 0; i < maxTries; i++) {
using (HttpResponseMessage response = await InternalPost(request, data, referer).ConfigureAwait(false)) {
if (response == null) {
continue;
}
return new BasicResponse(response);
}
}
if (maxTries > 1) {
ArchiLogger.LogGenericWarning(string.Format(Strings.ErrorRequestFailedTooManyTimes, maxTries));
ArchiLogger.LogGenericDebug(string.Format(Strings.ErrorFailingRequest, request));
}
return null;
}
internal async Task<HtmlDocumentResponse> UrlPostToHtmlDocument(string request, IReadOnlyCollection<KeyValuePair<string, string>> data = null, string referer = null, byte maxTries = MaxTries) {
if (string.IsNullOrEmpty(request) || (maxTries == 0)) {
ArchiLogger.LogNullError(nameof(request) + " || " + nameof(maxTries));
return null;
}
StringResponse response = await UrlPostToString(request, data, referer, maxTries).ConfigureAwait(false);
return response != null ? new HtmlDocumentResponse(response) : null;
}
internal async Task<ObjectResponse<T>> UrlPostToJsonObject<T>(string request, IReadOnlyCollection<KeyValuePair<string, string>> data = null, string referer = null, byte maxTries = MaxTries) where T : class {
if (string.IsNullOrEmpty(request) || (maxTries == 0)) {
ArchiLogger.LogNullError(nameof(request) + " || " + nameof(maxTries));
return null;
}
for (byte i = 0; i < maxTries; i++) {
StringResponse response = await UrlPostToString(request, data, referer, maxTries).ConfigureAwait(false);
if (string.IsNullOrEmpty(response?.Content)) {
continue;
}
T obj;
try {
obj = JsonConvert.DeserializeObject<T>(response.Content);
} catch (JsonException e) {
ArchiLogger.LogGenericWarningException(e);
if (Debugging.IsUserDebugging) {
ArchiLogger.LogGenericDebug(string.Format(Strings.Content, response.Content));
}
continue;
}
return new ObjectResponse<T>(response, obj);
}
if (maxTries > 1) {
ArchiLogger.LogGenericWarning(string.Format(Strings.ErrorRequestFailedTooManyTimes, maxTries));
ArchiLogger.LogGenericDebug(string.Format(Strings.ErrorFailingRequest, request));
}
return null;
}
private async Task<HttpResponseMessage> InternalGet(string request, string referer = null, HttpCompletionOption httpCompletionOptions = HttpCompletionOption.ResponseContentRead) {
if (string.IsNullOrEmpty(request)) {
ArchiLogger.LogNullError(nameof(request));
@ -563,7 +571,7 @@ namespace ArchiSteamFarm {
return null;
}
internal class BasicResponse {
public class BasicResponse {
internal readonly Uri FinalUri;
internal BasicResponse(HttpResponseMessage httpResponseMessage) {
@ -583,20 +591,9 @@ namespace ArchiSteamFarm {
}
}
internal sealed class BinaryResponse : BasicResponse {
internal readonly byte[] Content;
internal BinaryResponse(HttpResponseMessage httpResponseMessage, byte[] content) : base(httpResponseMessage) {
if ((httpResponseMessage == null) || (content == null)) {
throw new ArgumentNullException(nameof(httpResponseMessage) + " || " + nameof(content));
}
Content = content;
}
}
internal sealed class HtmlDocumentResponse : BasicResponse {
internal readonly HtmlDocument Content;
public sealed class HtmlDocumentResponse : BasicResponse {
[PublicAPI]
public readonly HtmlDocument Content;
internal HtmlDocumentResponse(StringResponse stringResponse) : base(stringResponse) {
if (stringResponse == null) {
@ -607,8 +604,9 @@ namespace ArchiSteamFarm {
}
}
internal sealed class ObjectResponse<T> : BasicResponse {
internal readonly T Content;
public sealed class ObjectResponse<T> : BasicResponse {
[PublicAPI]
public readonly T Content;
internal ObjectResponse(StringResponse stringResponse, T content) : base(stringResponse) {
if (stringResponse == null) {
@ -619,6 +617,31 @@ namespace ArchiSteamFarm {
}
}
public sealed class XmlDocumentResponse : BasicResponse {
[PublicAPI]
public readonly XmlDocument Content;
internal XmlDocumentResponse(StringResponse stringResponse, XmlDocument content) : base(stringResponse) {
if (stringResponse == null) {
throw new ArgumentNullException(nameof(stringResponse));
}
Content = content;
}
}
internal sealed class BinaryResponse : BasicResponse {
internal readonly byte[] Content;
internal BinaryResponse(HttpResponseMessage httpResponseMessage, byte[] content) : base(httpResponseMessage) {
if ((httpResponseMessage == null) || (content == null)) {
throw new ArgumentNullException(nameof(httpResponseMessage) + " || " + nameof(content));
}
Content = content;
}
}
internal sealed class StringResponse : BasicResponse {
internal readonly string Content;
@ -630,17 +653,5 @@ namespace ArchiSteamFarm {
Content = content;
}
}
internal sealed class XmlDocumentResponse : BasicResponse {
internal readonly XmlDocument Content;
internal XmlDocumentResponse(StringResponse stringResponse, XmlDocument content) : base(stringResponse) {
if (stringResponse == null) {
throw new ArgumentNullException(nameof(stringResponse));
}
Content = content;
}
}
}
}

View file

@ -101,6 +101,14 @@ build_script:
dotnet build ArchiSteamFarm -c "$env:CONFIGURATION" -f "$env:NET_CORE_VERSION" -o 'out\source' /nologo
if ($LastExitCode -ne 0) {
throw "Last command failed."
}
dotnet build ArchiSteamFarm.CustomPlugins.ExamplePlugin -c "$env:CONFIGURATION" -f "$env:NET_CORE_VERSION" -o 'out\source' /nologo
if ($LastExitCode -ne 0) {
throw "Last command failed."
}