Extract PublicListing and MatchActively to a plugin, resurrect MatchActively (#2759)

* Start work on extracting remote communication

* ok

* Dockerfile fixes

* More fixes

* Prepare /Api/Announce and /Api/HeartBeat

* Decrease publish race conditions

* OK

* Misc

* Misc

* Misc

* Move Steam group part back to ASF core

* Finally implement match actively v2 core

* Update RemoteCommunication.cs

* Use single round exclusively, report inventories more often

* Use randomization when asking others for assetIDs

* Add support for license and crowdin

* Kill dead code

* Fix return type of inventories

* Fix responses for good

* Unify old backend with new

* Report whole inventory, always

Helps with optimization on the backend side in terms of inventory fetching

* Update RemoteCommunication.cs

* Determine index of each asset and tell server about it

* Update AnnouncementRequest.cs

* Fix ASF screwing up with the order

* Fix warnings

* Misc rename

* Final logging touches
This commit is contained in:
Łukasz Domeradzki 2022-12-15 18:46:37 +01:00 committed by GitHub
parent fd517294d1
commit 98ef37e722
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 2292 additions and 2082 deletions

11
.github/crowdin.yml vendored
View file

@ -12,6 +12,17 @@
".zh-TW.resx": ".zh-Hant.resx"
}
},
{
"source": "/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Localization/Strings.resx",
"translation": "/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Localization/Strings.%locale%.resx",
"translation_replace": {
".lol-US.resx": ".qps-Ploc.resx",
".sr-CS.resx": ".sr-Latn.resx",
".zh-CN.resx": ".zh-Hans.resx",
".zh-HK.resx": ".zh-Hant-HK.resx",
".zh-TW.resx": ".zh-Hant.resx"
}
},
{
"source": "/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/Localization/Strings.resx",
"translation": "/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/Localization/Strings.%locale%.resx",

View file

@ -11,7 +11,7 @@ env:
NET_CORE_VERSION: net7.0
NET_FRAMEWORK_VERSION: net481
NODE_JS_VERSION: 'lts/*'
STEAM_TOKEN_DUMPER_NAME: ArchiSteamFarm.OfficialPlugins.SteamTokenDumper
PLUGINS: ArchiSteamFarm.OfficialPlugins.ItemsMatcher ArchiSteamFarm.OfficialPlugins.SteamTokenDumper
STEAM_TOKEN_DUMPER_TOKEN: ${{ secrets.STEAM_TOKEN_DUMPER_TOKEN }}
jobs:
@ -83,15 +83,35 @@ jobs:
}
}
- name: Prepare for publishing on Unix
if: startsWith(matrix.os, 'macos-') || startsWith(matrix.os, 'ubuntu-')
shell: bash
run: |
set -euo pipefail
dotnet restore
dotnet build ArchiSteamFarm -c "$CONFIGURATION" -p:ContinuousIntegrationBuild=true -p:TargetLatestRuntimePatch=false -p:UseAppHost=false --no-restore --nologo
- name: Prepare for publishing on Windows
if: startsWith(matrix.os, 'windows-')
shell: pwsh
run: |
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
dotnet restore
dotnet build ArchiSteamFarm -c "$env:CONFIGURATION" -p:ContinuousIntegrationBuild=true -p:TargetLatestRuntimePatch=false -p:UseAppHost=false --no-restore --nologo
- name: Prepare ArchiSteamFarm.OfficialPlugins.SteamTokenDumper on Unix
if: startsWith(matrix.os, 'macos-') || startsWith(matrix.os, 'ubuntu-')
shell: sh
run: |
set -eu
if [ -n "${STEAM_TOKEN_DUMPER_TOKEN-}" ] && [ -f "${STEAM_TOKEN_DUMPER_NAME}/SharedInfo.cs" ]; then
sed "s/STEAM_TOKEN_DUMPER_TOKEN/${STEAM_TOKEN_DUMPER_TOKEN}/g" "${STEAM_TOKEN_DUMPER_NAME}/SharedInfo.cs" > "${STEAM_TOKEN_DUMPER_NAME}/SharedInfo.cs.new"
mv "${STEAM_TOKEN_DUMPER_NAME}/SharedInfo.cs.new" "${STEAM_TOKEN_DUMPER_NAME}/SharedInfo.cs"
if [ -n "${STEAM_TOKEN_DUMPER_TOKEN-}" ] && [ -f "ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/SharedInfo.cs" ]; then
sed "s/STEAM_TOKEN_DUMPER_TOKEN/${STEAM_TOKEN_DUMPER_TOKEN}/g" "ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/SharedInfo.cs" > "ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/SharedInfo.cs.new"
mv "ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/SharedInfo.cs.new" "ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/SharedInfo.cs"
fi
- name: Prepare ArchiSteamFarm.OfficialPlugins.SteamTokenDumper on Windows
@ -102,19 +122,74 @@ jobs:
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
if ((Test-Path env:STEAM_TOKEN_DUMPER_TOKEN) -and ($env:STEAM_TOKEN_DUMPER_TOKEN) -and (Test-Path "$env:STEAM_TOKEN_DUMPER_NAME\SharedInfo.cs" -PathType Leaf)) {
(Get-Content "$env:STEAM_TOKEN_DUMPER_NAME\SharedInfo.cs").Replace('STEAM_TOKEN_DUMPER_TOKEN', "$env:STEAM_TOKEN_DUMPER_TOKEN") | Set-Content "$env:STEAM_TOKEN_DUMPER_NAME\SharedInfo.cs"
if ((Test-Path env:STEAM_TOKEN_DUMPER_TOKEN) -and ($env:STEAM_TOKEN_DUMPER_TOKEN) -and (Test-Path "ArchiSteamFarm.OfficialPlugins.SteamTokenDumper\SharedInfo.cs" -PathType Leaf)) {
(Get-Content "ArchiSteamFarm.OfficialPlugins.SteamTokenDumper\SharedInfo.cs").Replace('STEAM_TOKEN_DUMPER_TOKEN', "$env:STEAM_TOKEN_DUMPER_TOKEN") | Set-Content "ArchiSteamFarm.OfficialPlugins.SteamTokenDumper\SharedInfo.cs"
}
- name: Publish ArchiSteamFarm.OfficialPlugins.SteamTokenDumper for .NET Core
run: dotnet publish "${{ env.STEAM_TOKEN_DUMPER_NAME }}" -c "${{ env.CONFIGURATION }}" -f "${{ env.NET_CORE_VERSION }}" -o "out/${{ env.STEAM_TOKEN_DUMPER_NAME }}/${{ env.NET_CORE_VERSION }}" -p:ContinuousIntegrationBuild=true -p:TargetLatestRuntimePatch=false -p:UseAppHost=false --nologo
- name: Publish official plugins on Unix
if: startsWith(matrix.os, 'macos-') || startsWith(matrix.os, 'ubuntu-')
env:
MAX_JOBS: 3
shell: bash
run: |
set -euo pipefail
- name: Publish ArchiSteamFarm.OfficialPlugins.SteamTokenDumper for .NET Framework
publish() {
dotnet publish "$1" -c "$CONFIGURATION" -f "$NET_CORE_VERSION" -o "out/${1}/${NET_CORE_VERSION}" -p:ContinuousIntegrationBuild=true -p:TargetLatestRuntimePatch=false -p:UseAppHost=false --no-restore --nologo
}
for plugin in $PLUGINS; do
publish "$plugin" &
while [ "$(jobs -p | wc -l)" -ge "$MAX_JOBS" ]; do
sleep 1
done
done
wait
- name: Publish official plugins on Windows
if: startsWith(matrix.os, 'windows-')
run: dotnet publish "${{ env.STEAM_TOKEN_DUMPER_NAME }}" -c "${{ env.CONFIGURATION }}" -f "${{ env.NET_FRAMEWORK_VERSION }}" -o "out/${{ env.STEAM_TOKEN_DUMPER_NAME }}/${{ env.NET_FRAMEWORK_VERSION }}" -p:ContinuousIntegrationBuild=true -p:TargetLatestRuntimePatch=false -p:UseAppHost=false --nologo
env:
MAX_JOBS: 2
shell: pwsh
run: |
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
- name: Restore packages in preparation for ArchiSteamFarm publishing
run: dotnet restore ArchiSteamFarm -p:ContinuousIntegrationBuild=true --nologo
$PublishBlock = {
param($plugin, $framework)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
Set-Location "$env:GITHUB_WORKSPACE"
dotnet publish "$plugin" -c "$env:CONFIGURATION" -f "$framework" -o "out\$plugin\$framework" -p:ContinuousIntegrationBuild=true -p:TargetLatestRuntimePatch=false -p:UseAppHost=false --no-restore --nologo
if ($LastExitCode -ne 0) {
throw "Last command failed."
}
}
foreach ($plugin in $env:PLUGINS.Split([char[]] $null, [System.StringSplitOptions]::RemoveEmptyEntries)) {
foreach ($framework in "$env:NET_CORE_VERSION","$env:NET_FRAMEWORK_VERSION") {
Start-Job -Name "$plugin $framework" $PublishBlock -ArgumentList "$plugin","$framework"
# Limit active jobs in parallel to help with memory usage
$jobs = $(Get-Job -State Running)
while (@($jobs).Count -ge $env:MAX_JOBS) {
Wait-Job -Job $jobs -Any | Out-Null
$jobs = $(Get-Job -State Running)
}
}
}
Get-Job | Receive-Job -Wait
- name: Publish ArchiSteamFarm on Unix
if: startsWith(matrix.os, 'macos-') || startsWith(matrix.os, 'ubuntu-')
@ -134,11 +209,13 @@ jobs:
dotnet publish ArchiSteamFarm -c "$CONFIGURATION" -f "$NET_CORE_VERSION" -o "out/${1}" "-p:ASFVariant=$1" -p:ContinuousIntegrationBuild=true --no-restore --nologo $variantArgs
# If we're including SteamTokenDumper plugin for this framework, copy it to output directory
if [ -d "out/${STEAM_TOKEN_DUMPER_NAME}/${NET_CORE_VERSION}" ]; then
mkdir -p "out/${1}/plugins/${STEAM_TOKEN_DUMPER_NAME}"
cp -pR "out/${STEAM_TOKEN_DUMPER_NAME}/${NET_CORE_VERSION}/"* "out/${1}/plugins/${STEAM_TOKEN_DUMPER_NAME}"
fi
# If we're including official plugins for this framework, copy them to output directory
for plugin in $PLUGINS; do
if [ -d "out/${plugin}/${NET_CORE_VERSION}" ]; then
mkdir -p "out/${1}/plugins/${plugin}"
cp -pR "out/${plugin}/${NET_CORE_VERSION}/"* "out/${1}/plugins/${plugin}"
fi
done
# Include .ico file for all platforms, since only Windows script can bundle it inside the exe
cp "resources/ASF.ico" "out/${1}/ArchiSteamFarm.ico"
@ -244,13 +321,15 @@ jobs:
throw "Last command failed."
}
# If we're including SteamTokenDumper plugin for this framework, copy it to output directory
if (Test-Path "out\$env:STEAM_TOKEN_DUMPER_NAME\$targetFramework" -PathType Container) {
if (!(Test-Path "out\$variant\plugins\$env:STEAM_TOKEN_DUMPER_NAME" -PathType Container)) {
New-Item -ItemType Directory -Path "out\$variant\plugins\$env:STEAM_TOKEN_DUMPER_NAME" > $null
}
# If we're including official plugins for this framework, copy them to output directory
foreach ($plugin in $env:PLUGINS.Split([char[]] $null, [System.StringSplitOptions]::RemoveEmptyEntries)) {
if (Test-Path "out\$plugin\$targetFramework" -PathType Container) {
if (!(Test-Path "out\$variant\plugins\$plugin" -PathType Container)) {
New-Item -ItemType Directory -Path "out\$variant\plugins\$plugin" > $null
}
Copy-Item "out\$env:STEAM_TOKEN_DUMPER_NAME\$targetFramework\*" "out\$variant\plugins\$env:STEAM_TOKEN_DUMPER_NAME" -Recurse
Copy-Item "out\$plugin\$targetFramework\*" "out\$variant\plugins\$plugin" -Recurse
}
}
# Icon is available only in .NET Framework and .NET Core Windows build, we'll bundle the .ico file for other flavours

View file

@ -0,0 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ConfigureAwaitChecker.Analyzer" PrivateAssets="all" />
<PackageReference Include="JetBrains.Annotations" PrivateAssets="all" />
<PackageReference Include="Newtonsoft.Json" IncludeAssets="compile" />
<PackageReference Include="SteamKit2" IncludeAssets="compile" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" IncludeAssets="compile" />
<PackageReference Include="System.Composition.AttributedModel" IncludeAssets="compile" />
<PackageReference Include="System.Linq.Async" IncludeAssets="compile" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net481'">
<!-- Madness is already included in netf build of ASF, so we don't need to emit it ourselves -->
<PackageReference Update="JustArchiNET.Madness" IncludeAssets="compile" />
<Reference Include="System.Net.Http" HintPath="C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.8.1\System.Net.Http.dll" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ArchiSteamFarm\ArchiSteamFarm.csproj" ExcludeAssets="all" Private="false" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Localization\Strings.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Strings.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Compile Update="Localization\Strings.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Strings.resx</DependentUpon>
<DesignTime>True</DesignTime>
</Compile>
</ItemGroup>
</Project>

View file

@ -0,0 +1,24 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2022 Ł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;
[assembly: CLSCompliant(false)]

View file

@ -0,0 +1,120 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2022 Ł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.Net;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.IPC.Responses;
using ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Requests;
using ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Responses;
using ArchiSteamFarm.Steam;
using ArchiSteamFarm.Steam.Data;
using ArchiSteamFarm.Steam.Storage;
using ArchiSteamFarm.Storage;
using ArchiSteamFarm.Web;
using ArchiSteamFarm.Web.Responses;
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher;
internal static class Backend {
internal static async Task<HttpStatusCode?> AnnounceForListing(Bot bot, IReadOnlyList<Asset> inventory, IReadOnlyCollection<Asset.EType> acceptedMatchableTypes, string tradeToken, string? nickname = null, string? avatarHash = null) {
ArgumentNullException.ThrowIfNull(bot);
if ((inventory == null) || (inventory.Count == 0)) {
throw new ArgumentNullException(nameof(inventory));
}
if ((acceptedMatchableTypes == null) || (acceptedMatchableTypes.Count == 0)) {
throw new ArgumentNullException(nameof(acceptedMatchableTypes));
}
if (string.IsNullOrEmpty(tradeToken)) {
throw new ArgumentNullException(nameof(tradeToken));
}
if (tradeToken.Length != BotConfig.SteamTradeTokenLength) {
throw new ArgumentOutOfRangeException(nameof(tradeToken));
}
Uri request = new(ArchiNet.URL, "/Api/Listing/Announce");
AnnouncementRequest data = new(ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid(), bot.SteamID, tradeToken, inventory, acceptedMatchableTypes, bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchEverything), ASF.GlobalConfig?.MaxTradeHoldDuration ?? GlobalConfig.DefaultMaxTradeHoldDuration, nickname, avatarHash);
BasicResponse? response = await bot.ArchiWebHandler.WebBrowser.UrlPost(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors).ConfigureAwait(false);
return response?.StatusCode;
}
internal static async Task<(HttpStatusCode StatusCode, ImmutableHashSet<ListedUser> Users)?> GetListedUsersForMatching(Guid licenseID, Bot bot, IReadOnlyCollection<Asset> inventory, IReadOnlyCollection<Asset.EType> acceptedMatchableTypes, string tradeToken) {
if (licenseID == Guid.Empty) {
throw new ArgumentOutOfRangeException(nameof(licenseID));
}
ArgumentNullException.ThrowIfNull(bot);
if ((inventory == null) || (inventory.Count == 0)) {
throw new ArgumentNullException(nameof(inventory));
}
if ((acceptedMatchableTypes == null) || (acceptedMatchableTypes.Count == 0)) {
throw new ArgumentNullException(nameof(acceptedMatchableTypes));
}
if (string.IsNullOrEmpty(tradeToken)) {
throw new ArgumentNullException(nameof(tradeToken));
}
if (tradeToken.Length != BotConfig.SteamTradeTokenLength) {
throw new ArgumentOutOfRangeException(nameof(tradeToken));
}
Uri request = new(ArchiNet.URL, "/Api/Listing/Inventories");
Dictionary<string, string> headers = new(1, StringComparer.Ordinal) {
{ "X-License-Key", licenseID.ToString("N") }
};
InventoriesRequest data = new(ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid(), bot.SteamID, tradeToken, inventory, acceptedMatchableTypes, ASF.GlobalConfig?.MaxTradeHoldDuration ?? GlobalConfig.DefaultMaxTradeHoldDuration);
ObjectResponse<GenericResponse<ImmutableHashSet<ListedUser>>>? response = await bot.ArchiWebHandler.WebBrowser.UrlPostToJsonObject<GenericResponse<ImmutableHashSet<ListedUser>>, InventoriesRequest>(request, headers, data, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.AllowInvalidBodyOnErrors).ConfigureAwait(false);
if (response == null) {
return null;
}
return (response.StatusCode, response.Content?.Result ?? ImmutableHashSet<ListedUser>.Empty);
}
internal static async Task<HttpStatusCode?> HeartBeatForListing(Bot bot) {
ArgumentNullException.ThrowIfNull(bot);
Uri request = new(ArchiNet.URL, "/Api/Listing/HeartBeat");
HeartBeatRequest data = new(ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid(), bot.SteamID);
BasicResponse? response = await bot.ArchiWebHandler.WebBrowser.UrlPost(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors).ConfigureAwait(false);
return response?.StatusCode;
}
}

View file

@ -0,0 +1,88 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2022 Ł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.Concurrent;
using System.Composition;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Localization;
using ArchiSteamFarm.Plugins;
using ArchiSteamFarm.Plugins.Interfaces;
using ArchiSteamFarm.Steam;
using ArchiSteamFarm.Steam.Storage;
using Newtonsoft.Json;
using SteamKit2;
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher;
[Export(typeof(IPlugin))]
internal sealed class ItemsMatcherPlugin : OfficialPlugin, IBot, IBotIdentity {
private static readonly ConcurrentDictionary<Bot, RemoteCommunication> RemoteCommunications = new();
[JsonProperty]
public override string Name => nameof(ItemsMatcherPlugin);
[JsonProperty]
public override Version Version => typeof(ItemsMatcherPlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version));
public async Task OnBotDestroy(Bot bot) {
ArgumentNullException.ThrowIfNull(bot);
if (RemoteCommunications.TryRemove(bot, out RemoteCommunication? remoteCommunications)) {
await remoteCommunications.DisposeAsync().ConfigureAwait(false);
}
}
public async Task OnBotInit(Bot bot) {
ArgumentNullException.ThrowIfNull(bot);
if (RemoteCommunications.TryRemove(bot, out RemoteCommunication? remoteCommunications)) {
await remoteCommunications.DisposeAsync().ConfigureAwait(false);
}
if (bot.BotConfig.RemoteCommunication == BotConfig.ERemoteCommunication.None) {
return;
}
RemoteCommunication remoteCommunication = new(bot);
if (!RemoteCommunications.TryAdd(bot, remoteCommunication)) {
await remoteCommunication.DisposeAsync().ConfigureAwait(false);
}
}
public override Task OnLoaded() {
Utilities.WarnAboutIncompleteTranslation(Strings.ResourceManager);
return Task.CompletedTask;
}
public async Task OnSelfPersonaState(Bot bot, SteamFriends.PersonaStateCallback data, string? nickname, string? avatarHash) {
ArgumentNullException.ThrowIfNull(bot);
if (!RemoteCommunications.TryGetValue(bot, out RemoteCommunication? remoteCommunication)) {
return;
}
await remoteCommunication.OnPersonaState(nickname, avatarHash).ConfigureAwait(false);
}
}

View file

@ -0,0 +1,3 @@
This directory contains ASF strings for display and localization purposes.
All strings used by ASF can be found in main `Strings.resx` file, and that's also the only `resx` file that should be modified - all other `resx` files are managed automatically and should not be touched. Please visit **[localization](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Localization)** section on the wiki if you want to improve translation of other files.

View file

@ -0,0 +1,72 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Localization {
using System;
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Strings {
private static System.Resources.ResourceManager resourceMan;
private static System.Globalization.CultureInfo resourceCulture;
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Strings() {
}
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
internal static System.Resources.ResourceManager ResourceManager {
get {
if (object.Equals(null, resourceMan)) {
System.Resources.ResourceManager temp = new System.Resources.ResourceManager("ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Localization.Strings", typeof(Strings).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
internal static System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
internal static string ActivelyMatchingItemsRound {
get {
return ResourceManager.GetString("ActivelyMatchingItemsRound", resourceCulture);
}
}
internal static string ListingAnnouncing {
get {
return ResourceManager.GetString("ListingAnnouncing", resourceCulture);
}
}
internal static string MatchingFound {
get {
return ResourceManager.GetString("MatchingFound", resourceCulture);
}
}
internal static string TradeOfferFailed {
get {
return ResourceManager.GetString("TradeOfferFailed", resourceCulture);
}
}
}
}

View file

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root" xmlns="">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
<data name="ActivelyMatchingItemsRound" xml:space="preserve">
<value>Matched a total of {0} sets this round.</value>
<comment>{0} will be replaced by number of sets traded</comment>
</data>
<data name="ListingAnnouncing" xml:space="preserve">
<value>Announcing {0} ({1}) with inventory made out of {2} items in total on the listing...</value>
<comment>{0} will be replaced by steam ID (number), {1} will be replaced by user's nickname, {2} will be replaced with number of items in the inventory</comment>
</data>
<data name="MatchingFound" xml:space="preserve">
<value>Matched a total of {0} items with bot {1} ({2}), sending trade offer...</value>
<comment>{0} will be replaced by number of items matched, {1} will be replaced by steam ID (number), {2} will be replaced by user's nickname</comment>
</data>
<data name="TradeOfferFailed" xml:space="preserve">
<value>Failed to send a trade offer to bot {0} ({1}), moving on...</value>
<comment>{0} will be replaced by steam ID (number), {1} will be replaced by user's nickname'</comment>
</data>
</root>

View file

@ -28,27 +28,24 @@ using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Localization;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Localization;
using ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Responses;
using ArchiSteamFarm.Steam;
using ArchiSteamFarm.Steam.Cards;
using ArchiSteamFarm.Steam.Data;
using ArchiSteamFarm.Steam.Exchange;
using ArchiSteamFarm.Steam.Integration;
using ArchiSteamFarm.Steam.Security;
using ArchiSteamFarm.Steam.Storage;
using ArchiSteamFarm.Storage;
using ArchiSteamFarm.Web;
namespace ArchiSteamFarm.Core;
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher;
internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
private const ushort MaxItemsForFairBots = ArchiWebHandler.MaxItemsInSingleInventoryRequest * WebBrowser.MaxTries; // Determines which fair bots we'll deprioritize when matching due to excessive number of inventory requests they need to make, which are likely to fail in the process or cause excessive delays
private const byte MaxMatchedBotsHard = 40; // Determines how many bots we can attempt to match in total, where match attempt is equal to analyzing bot's inventory
private const byte MaxMatchingRounds = 10; // Determines maximum amount of matching rounds we're going to consider before leaving the rest of work for the next batch
private const byte MinAnnouncementCheckTTL = 6; // Minimum amount of hours we must wait before checking eligibility for Announcement, should be lower than MinPersonaStateTTL
private const byte MinAnnouncementCheckTTL = 45; // Minimum amount of minutes we must wait before checking eligibility for Announcement, should be lower than MinPersonaStateTTL
private const byte MinHeartBeatTTL = 10; // Minimum amount of minutes we must wait before sending next HeartBeat
private const byte MinItemsCount = 100; // Minimum amount of items to be eligible for public listing
private const byte MinPersonaStateTTL = 8; // Minimum amount of hours we must wait before requesting persona state update
private const byte MinPersonaStateTTL = 60; // Minimum amount of minutes we must wait before requesting persona state update
private static readonly ImmutableHashSet<Asset.EType> AcceptedMatchableTypes = ImmutableHashSet.Create(
Asset.EType.Emoticon,
@ -59,6 +56,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
private readonly Bot Bot;
private readonly SemaphoreSlim MatchActivelySemaphore = new(1, 1);
private readonly Timer? HeartBeatTimer;
private readonly Timer? MatchActivelyTimer;
private readonly SemaphoreSlim RequestsSemaphore = new(1, 1);
@ -68,15 +66,30 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
private bool ShouldSendHeartBeats;
internal RemoteCommunication(Bot bot) {
Bot = bot ?? throw new ArgumentNullException(nameof(bot));
ArgumentNullException.ThrowIfNull(bot);
Bot = bot;
if (Bot.BotConfig.RemoteCommunication.HasFlag(BotConfig.ERemoteCommunication.PublicListing)) {
HeartBeatTimer = new Timer(
HeartBeat,
null,
TimeSpan.FromMinutes(MinHeartBeatTTL) + TimeSpan.FromSeconds(ASF.LoadBalancingDelay * Bot.Bots?.Count ?? 0),
TimeSpan.FromMinutes(1)
);
}
if (Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchActively)) {
MatchActivelyTimer = new Timer(
MatchActively,
null,
TimeSpan.FromHours(1) + TimeSpan.FromSeconds(ASF.LoadBalancingDelay * Bot.Bots?.Count ?? 0), // Delay
TimeSpan.FromHours(8) // Period
);
if ((ASF.GlobalConfig?.LicenseID != null) && (ASF.GlobalConfig.LicenseID != Guid.Empty)) {
MatchActivelyTimer = new Timer(
MatchActively,
null,
TimeSpan.FromHours(1) + TimeSpan.FromSeconds(ASF.LoadBalancingDelay * Bot.Bots?.Count ?? 0),
TimeSpan.FromHours(6)
);
} else {
bot.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.WarningNoLicense, nameof(BotConfig.ETradingPreferences.MatchActively)));
}
}
}
@ -86,6 +99,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
RequestsSemaphore.Dispose();
// Those are objects that might be null and the check should be in-place
HeartBeatTimer?.Dispose();
MatchActivelyTimer?.Dispose();
}
@ -95,18 +109,22 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
RequestsSemaphore.Dispose();
// Those are objects that might be null and the check should be in-place
if (HeartBeatTimer != null) {
await HeartBeatTimer.DisposeAsync().ConfigureAwait(false);
}
if (MatchActivelyTimer != null) {
await MatchActivelyTimer.DisposeAsync().ConfigureAwait(false);
}
}
internal async Task OnHeartBeat() {
if (!Bot.BotConfig.RemoteCommunication.HasFlag(BotConfig.ERemoteCommunication.PublicListing)) {
private async void HeartBeat(object? state = null) {
if (!Bot.IsConnectedAndLoggedOn || (Bot.HeartBeatFailures > 0)) {
return;
}
// Request persona update if needed
if ((DateTime.UtcNow > LastPersonaStateRequest.AddHours(MinPersonaStateTTL)) && (DateTime.UtcNow > LastAnnouncementCheck.AddHours(MinAnnouncementCheckTTL))) {
if ((DateTime.UtcNow > LastPersonaStateRequest.AddMinutes(MinPersonaStateTTL)) && (DateTime.UtcNow > LastAnnouncementCheck.AddMinutes(MinAnnouncementCheckTTL))) {
LastPersonaStateRequest = DateTime.UtcNow;
Bot.RequestPersonaStateUpdate();
}
@ -120,7 +138,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
}
try {
HttpStatusCode? response = await ArchiNet.HeartBeatForListing(Bot).ConfigureAwait(false);
HttpStatusCode? response = await Backend.HeartBeatForListing(Bot).ConfigureAwait(false);
if (!response.HasValue) {
return;
@ -139,29 +157,19 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
}
}
internal async Task OnLoggedOn() {
if (!Bot.BotConfig.RemoteCommunication.HasFlag(BotConfig.ERemoteCommunication.SteamGroup)) {
return;
}
if (!await Bot.ArchiWebHandler.JoinGroup(SharedInfo.ASFGroupSteamID).ConfigureAwait(false)) {
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(ArchiWebHandler.JoinGroup)));
}
}
internal async Task OnPersonaState(string? nickname = null, string? avatarHash = null) {
if (!Bot.BotConfig.RemoteCommunication.HasFlag(BotConfig.ERemoteCommunication.PublicListing)) {
return;
}
if ((DateTime.UtcNow < LastAnnouncementCheck.AddHours(MinAnnouncementCheckTTL)) && (ShouldSendHeartBeats || (LastHeartBeat == DateTime.MinValue))) {
if ((DateTime.UtcNow < LastAnnouncementCheck.AddMinutes(MinAnnouncementCheckTTL)) && (ShouldSendHeartBeats || (LastHeartBeat == DateTime.MinValue))) {
return;
}
await RequestsSemaphore.WaitAsync().ConfigureAwait(false);
try {
if ((DateTime.UtcNow < LastAnnouncementCheck.AddHours(MinAnnouncementCheckTTL)) && (ShouldSendHeartBeats || (LastHeartBeat == DateTime.MinValue))) {
if ((DateTime.UtcNow < LastAnnouncementCheck.AddMinutes(MinAnnouncementCheckTTL)) && (ShouldSendHeartBeats || (LastHeartBeat == DateTime.MinValue))) {
return;
}
@ -188,6 +196,8 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
// This is actually network failure, so we'll stop sending heartbeats but not record it as valid check
ShouldSendHeartBeats = false;
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.WarningFailedWithError, nameof(tradeToken)));
return;
}
@ -201,10 +211,10 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
return;
}
HashSet<Asset> inventory;
List<Asset> inventory;
try {
inventory = await Bot.ArchiWebHandler.GetInventoryAsync().Where(item => item.Tradable && acceptedMatchableTypes.Contains(item.Type)).ToHashSetAsync().ConfigureAwait(false);
inventory = await Bot.ArchiWebHandler.GetInventoryAsync().ToListAsync().ConfigureAwait(false);
} catch (HttpRequestException e) {
Bot.ArchiLogger.LogGenericWarningException(e);
@ -224,28 +234,52 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
LastAnnouncementCheck = DateTime.UtcNow;
// This is actual inventory
if (inventory.Count < MinItemsCount) {
if (inventory.Count(item => item.Tradable && acceptedMatchableTypes.Contains(item.Type)) < MinItemsCount) {
ShouldSendHeartBeats = false;
return;
}
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ListingAnnouncing, Bot.SteamID, nickname, inventory.Count));
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
HttpStatusCode? response = await ArchiNet.AnnounceForListing(Bot, inventory, acceptedMatchableTypes, tradeToken!, nickname, avatarHash).ConfigureAwait(false);
HttpStatusCode? response = await Backend.AnnounceForListing(Bot, inventory, acceptedMatchableTypes, tradeToken!, nickname, avatarHash).ConfigureAwait(false);
if (!response.HasValue) {
return;
}
if (response.Value.IsClientErrorCode()) {
ASF.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.WarningFailedWithError, response));
LastHeartBeat = DateTime.MinValue;
ShouldSendHeartBeats = false;
switch (response) {
case HttpStatusCode.Forbidden:
// ArchiNet told us to stop submitting data for now
LastAnnouncementCheck = DateTime.UtcNow.AddYears(1);
break;
#if NETFRAMEWORK
case (HttpStatusCode) 429:
#else
case HttpStatusCode.TooManyRequests:
#endif
// ArchiNet told us to try again later
LastAnnouncementCheck = DateTime.UtcNow.AddDays(1);
break;
}
return;
}
LastHeartBeat = DateTime.UtcNow;
ShouldSendHeartBeats = true;
Bot.ArchiLogger.LogGenericInfo(ArchiSteamFarm.Localization.Strings.Success);
} finally {
RequestsSemaphore.Release();
}
@ -261,7 +295,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
// Bot must have STM enabled in TradingPreferences
if (!Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.SteamTradeMatcher)) {
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(Bot.BotConfig.TradingPreferences)}: {Bot.BotConfig.TradingPreferences}"));
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.WarningFailedWithError, $"{nameof(Bot.BotConfig.TradingPreferences)}: {Bot.BotConfig.TradingPreferences}"));
return false;
}
@ -270,7 +304,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
bool? hasPublicInventory = await Bot.HasPublicInventory().ConfigureAwait(false);
if (hasPublicInventory != true) {
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(Bot.HasPublicInventory)}: {hasPublicInventory?.ToString() ?? "null"}"));
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.WarningFailedWithError, $"{nameof(Bot.HasPublicInventory)}: {hasPublicInventory?.ToString() ?? "null"}"));
return hasPublicInventory;
}
@ -281,14 +315,14 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
private async Task<bool?> IsEligibleForMatching() {
// Bot must have ASF 2FA
if (!Bot.HasMobileAuthenticator) {
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(Bot.HasMobileAuthenticator)}: {Bot.HasMobileAuthenticator}"));
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.WarningFailedWithError, $"{nameof(Bot.HasMobileAuthenticator)}: {Bot.HasMobileAuthenticator}"));
return false;
}
// Bot must have at least one accepted matchable type set
if ((Bot.BotConfig.MatchableTypes.Count == 0) || Bot.BotConfig.MatchableTypes.All(static type => !AcceptedMatchableTypes.Contains(type))) {
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(Bot.BotConfig.MatchableTypes)}: {string.Join(", ", Bot.BotConfig.MatchableTypes)}"));
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.WarningFailedWithError, $"{nameof(Bot.BotConfig.MatchableTypes)}: {string.Join(", ", Bot.BotConfig.MatchableTypes)}"));
return false;
}
@ -297,7 +331,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
bool? hasValidApiKey = await Bot.ArchiWebHandler.HasValidApiKey().ConfigureAwait(false);
if (hasValidApiKey != true) {
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(Bot.ArchiWebHandler.HasValidApiKey)}: {hasValidApiKey?.ToString() ?? "null"}"));
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.WarningFailedWithError, $"{nameof(Bot.ArchiWebHandler.HasValidApiKey)}: {hasValidApiKey?.ToString() ?? "null"}"));
return hasValidApiKey;
}
@ -306,8 +340,16 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
}
private async void MatchActively(object? state = null) {
if (ASF.GlobalConfig == null) {
throw new InvalidOperationException(nameof(ASF.GlobalConfig));
}
if (!ASF.GlobalConfig.LicenseID.HasValue || (ASF.GlobalConfig.LicenseID == Guid.Empty)) {
throw new InvalidOperationException(nameof(ASF.GlobalConfig.LicenseID));
}
if (!Bot.IsConnectedAndLoggedOn || Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchEverything) || !Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchActively) || (await IsEligibleForMatching().ConfigureAwait(false) != true)) {
Bot.ArchiLogger.LogGenericTrace(Strings.ErrorAborted);
Bot.ArchiLogger.LogGenericTrace(ArchiSteamFarm.Localization.Strings.ErrorAborted);
return;
}
@ -315,120 +357,125 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
HashSet<Asset.EType> acceptedMatchableTypes = Bot.BotConfig.MatchableTypes.Where(AcceptedMatchableTypes.Contains).ToHashSet();
if (acceptedMatchableTypes.Count == 0) {
Bot.ArchiLogger.LogGenericTrace(Strings.ErrorAborted);
Bot.ArchiLogger.LogNullError(acceptedMatchableTypes);
return;
}
if (!await MatchActivelySemaphore.WaitAsync(0).ConfigureAwait(false)) {
Bot.ArchiLogger.LogGenericTrace(Strings.ErrorAborted);
Bot.ArchiLogger.LogGenericTrace(ArchiSteamFarm.Localization.Strings.ErrorAborted);
return;
}
try {
Bot.ArchiLogger.LogGenericTrace(Strings.Starting);
Bot.ArchiLogger.LogGenericInfo(ArchiSteamFarm.Localization.Strings.Starting);
Dictionary<ulong, (byte Tries, ISet<ulong>? GivenAssetIDs, ISet<ulong>? ReceivedAssetIDs)> triedSteamIDs = new();
string? tradeToken = await Bot.ArchiHandler.GetTradeToken().ConfigureAwait(false);
bool shouldContinueMatching = true;
bool tradedSomething = false;
if (string.IsNullOrEmpty(tradeToken)) {
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.WarningFailedWithError, nameof(tradeToken)));
for (byte i = 0; (i < MaxMatchingRounds) && shouldContinueMatching; i++) {
if ((i > 0) && tradedSomething) {
// After each round we wait at least 5 minutes for all bots to react
await Task.Delay(5 * 60 * 1000).ConfigureAwait(false);
}
if (!Bot.IsConnectedAndLoggedOn || Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchEverything) || !Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchActively) || (await IsEligibleForMatching().ConfigureAwait(false) != true)) {
Bot.ArchiLogger.LogGenericTrace(Strings.ErrorAborted);
break;
}
#pragma warning disable CA2000 // False positive, we're actually wrapping it in the using clause below exactly for that purpose
using (await Bot.Actions.GetTradingLock().ConfigureAwait(false)) {
#pragma warning restore CA2000 // False positive, we're actually wrapping it in the using clause below exactly for that purpose
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.ActivelyMatchingItems, i));
(shouldContinueMatching, tradedSomething) = await MatchActivelyRound(acceptedMatchableTypes, triedSteamIDs).ConfigureAwait(false);
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.DoneActivelyMatchingItems, i));
}
return;
}
Bot.ArchiLogger.LogGenericTrace(Strings.Done);
HashSet<Asset> ourInventory;
try {
ourInventory = await Bot.ArchiWebHandler.GetInventoryAsync().Where(item => acceptedMatchableTypes.Contains(item.Type) && !Bot.BotDatabase.MatchActivelyBlacklistAppIDs.Contains(item.RealAppID)).ToHashSetAsync().ConfigureAwait(false);
} catch (HttpRequestException e) {
Bot.ArchiLogger.LogGenericWarningException(e);
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.WarningFailedWithError, nameof(ourInventory)));
return;
} catch (Exception e) {
Bot.ArchiLogger.LogGenericException(e);
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.WarningFailedWithError, nameof(ourInventory)));
return;
}
if (ourInventory.Count == 0) {
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.ErrorIsEmpty, nameof(ourInventory)));
return;
}
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
(HttpStatusCode StatusCode, ImmutableHashSet<ListedUser> Users)? response = await Backend.GetListedUsersForMatching(ASF.GlobalConfig.LicenseID.Value, Bot, ourInventory, acceptedMatchableTypes, tradeToken!).ConfigureAwait(false);
if (response == null) {
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.WarningFailedWithError, nameof(response)));
return;
}
if (!response.Value.StatusCode.IsSuccessCode()) {
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.WarningFailedWithError, response.Value.StatusCode));
return;
}
if (response.Value.Users.IsEmpty) {
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.WarningFailedWithError, nameof(response.Value.Users)));
return;
}
#pragma warning disable CA2000 // False positive, we're actually wrapping it in the using clause below exactly for that purpose
using (await Bot.Actions.GetTradingLock().ConfigureAwait(false)) {
#pragma warning restore CA2000 // False positive, we're actually wrapping it in the using clause below exactly for that purpose
Bot.ArchiLogger.LogGenericInfo(ArchiSteamFarm.Localization.Strings.Starting);
await MatchActivelyRound(response.Value.Users, ourInventory, acceptedMatchableTypes).ConfigureAwait(false);
}
Bot.ArchiLogger.LogGenericInfo(ArchiSteamFarm.Localization.Strings.Done);
} finally {
MatchActivelySemaphore.Release();
}
}
private async Task<(bool ShouldContinueMatching, bool TradedSomething)> MatchActivelyRound(IReadOnlyCollection<Asset.EType> acceptedMatchableTypes, IDictionary<ulong, (byte Tries, ISet<ulong>? GivenAssetIDs, ISet<ulong>? ReceivedAssetIDs)> triedSteamIDs) {
private async Task MatchActivelyRound(IReadOnlyCollection<ListedUser> listedUsers, IReadOnlyCollection<Asset> ourInventory, IReadOnlyCollection<Asset.EType> acceptedMatchableTypes) {
if ((listedUsers == null) || (listedUsers.Count == 0)) {
throw new ArgumentNullException(nameof(listedUsers));
}
if ((ourInventory == null) || (ourInventory.Count == 0)) {
throw new ArgumentNullException(nameof(ourInventory));
}
if ((acceptedMatchableTypes == null) || (acceptedMatchableTypes.Count == 0)) {
throw new ArgumentNullException(nameof(acceptedMatchableTypes));
}
ArgumentNullException.ThrowIfNull(triedSteamIDs);
HashSet<Asset> ourInventory;
try {
ourInventory = await Bot.ArchiWebHandler.GetInventoryAsync().Where(item => acceptedMatchableTypes.Contains(item.Type) && !Bot.BotDatabase.MatchActivelyBlacklistAppIDs.Contains(item.RealAppID)).ToHashSetAsync().ConfigureAwait(false);
} catch (HttpRequestException e) {
Bot.ArchiLogger.LogGenericWarningException(e);
return (false, false);
} catch (Exception e) {
Bot.ArchiLogger.LogGenericException(e);
return (false, false);
}
if (ourInventory.Count == 0) {
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(ourInventory)));
return (false, false);
}
(Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), Dictionary<ulong, uint>> ourFullState, Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), Dictionary<ulong, uint>> ourTradableState) = Trading.GetDividedInventoryState(ourInventory);
if (Trading.IsEmptyForMatching(ourFullState, ourTradableState)) {
// User doesn't have any more dupes in the inventory
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, $"{nameof(ourFullState)} || {nameof(ourTradableState)}"));
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.ErrorIsEmpty, $"{nameof(ourFullState)} || {nameof(ourTradableState)}"));
return (false, false);
return;
}
ImmutableHashSet<ArchiNet.ListedUser>? listedUsers = await ArchiNet.GetListedUsers(Bot).ConfigureAwait(false);
if ((listedUsers == null) || (listedUsers.Count == 0)) {
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(listedUsers)));
return (false, false);
}
byte maxTradeHoldDuration = ASF.GlobalConfig?.MaxTradeHoldDuration ?? GlobalConfig.DefaultMaxTradeHoldDuration;
byte totalMatches = 0;
bool rateLimited = false;
HashSet<ulong> triedSteamIDs = new();
HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> skippedSetsThisRound = new();
foreach (ArchiNet.ListedUser? listedUser in listedUsers.Where(listedUser => (listedUser.SteamID != Bot.SteamID) && acceptedMatchableTypes.Any(listedUser.MatchableTypes.Contains) && (!triedSteamIDs.TryGetValue(listedUser.SteamID, out (byte Tries, ISet<ulong>? GivenAssetIDs, ISet<ulong>? ReceivedAssetIDs) attempt) || (attempt.Tries < byte.MaxValue)) && !Bot.IsBlacklistedFromTrades(listedUser.SteamID)).OrderBy(listedUser => triedSteamIDs.TryGetValue(listedUser.SteamID, out (byte Tries, ISet<ulong>? GivenAssetIDs, ISet<ulong>? ReceivedAssetIDs) attempt) ? attempt.Tries : 0).ThenByDescending(static listedUser => listedUser.MatchEverything).ThenByDescending(static listedUser => listedUser.MatchEverything || (listedUser.ItemsCount < MaxItemsForFairBots)).ThenByDescending(static listedUser => listedUser.Score)) {
byte maxTradeHoldDuration = ASF.GlobalConfig?.MaxTradeHoldDuration ?? GlobalConfig.DefaultMaxTradeHoldDuration;
foreach (ListedUser listedUser in listedUsers.Where(listedUser => (listedUser.SteamID != Bot.SteamID) && !triedSteamIDs.Contains(listedUser.SteamID) && acceptedMatchableTypes.Any(listedUser.MatchableTypes.Contains) && !Bot.IsBlacklistedFromTrades(listedUser.SteamID)).OrderByDescending(static listedUser => listedUser.MatchEverything).ThenBy(static listedUser => listedUser.Assets.Count)) {
HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> wantedSets = ourTradableState.Keys.Where(set => !skippedSetsThisRound.Contains(set) && listedUser.MatchableTypes.Contains(set.Type)).ToHashSet();
if (wantedSets.Count == 0) {
continue;
}
if (++totalMatches > MaxMatchedBotsHard) {
break;
}
Bot.ArchiLogger.LogGenericTrace($"{listedUser.SteamID}...");
byte? tradeHoldDuration = await Bot.ArchiWebHandler.GetCombinedTradeHoldDurationAgainstUser(listedUser.SteamID, listedUser.TradeToken).ConfigureAwait(false);
switch (tradeHoldDuration) {
case null:
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(tradeHoldDuration)));
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.ErrorIsEmpty, nameof(tradeHoldDuration)));
continue;
case > 0 when (tradeHoldDuration.Value > maxTradeHoldDuration) || (tradeHoldDuration.Value > listedUser.MaxTradeHoldDuration):
@ -437,35 +484,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
continue;
}
HashSet<Asset> theirInventory;
try {
theirInventory = await Bot.ArchiWebHandler.GetInventoryAsync(listedUser.SteamID).Where(item => (!listedUser.MatchEverything || item.Tradable) && wantedSets.Contains((item.RealAppID, item.Type, item.Rarity)) && ((tradeHoldDuration.Value == 0) || !(item.Type is Asset.EType.FoilTradingCard or Asset.EType.TradingCard && CardsFarmer.SalesBlacklist.Contains(item.RealAppID)))).ToHashSetAsync().ConfigureAwait(false);
} catch (HttpRequestException e) {
Bot.ArchiLogger.LogGenericWarningException(e);
#if NETFRAMEWORK
if (e.StatusCode == (HttpStatusCode) 429) {
#else
if (e.StatusCode == HttpStatusCode.TooManyRequests) {
#endif
rateLimited = true;
break;
}
continue;
} catch (Exception e) {
Bot.ArchiLogger.LogGenericException(e);
continue;
}
if (theirInventory.Count == 0) {
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(theirInventory)));
continue;
}
HashSet<Asset> theirInventory = listedUser.Assets.Where(item => (!listedUser.MatchEverything || item.Tradable) && wantedSets.Contains((item.RealAppID, item.Type, item.Rarity)) && ((tradeHoldDuration.Value == 0) || !(item.Type is Asset.EType.FoilTradingCard or Asset.EType.TradingCard && CardsFarmer.SalesBlacklist.Contains(item.RealAppID)))).Select(static asset => asset.ToAsset()).ToHashSet();
HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> skippedSetsThisUser = new();
@ -500,7 +519,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
if (!ourFullSet.TryGetValue(classID, out uint fullAmount) || (fullAmount == 0) || (fullAmount < amount)) {
Bot.ArchiLogger.LogNullError(fullAmount);
return (false, skippedSetsThisRound.Count > 0);
return;
}
if (fullAmount > amount) {
@ -512,7 +531,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
if (!ourTradableSet.TryGetValue(classID, out uint tradableAmount) || (tradableAmount == 0) || (tradableAmount < amount)) {
Bot.ArchiLogger.LogNullError(tradableAmount);
return (false, skippedSetsThisRound.Count > 0);
return;
}
if (fullAmount > amount) {
@ -625,40 +644,27 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
}
if (skippedSetsThisTrade.Count == 0) {
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(skippedSetsThisTrade)));
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.ErrorIsEmpty, nameof(skippedSetsThisTrade)));
break;
}
// Remove the items from inventories
HashSet<Asset> itemsToGive = Trading.GetTradableItemsFromInventory(ourInventory, classIDsToGive);
HashSet<Asset> itemsToReceive = Trading.GetTradableItemsFromInventory(theirInventory, classIDsToReceive);
HashSet<Asset> itemsToReceive = Trading.GetTradableItemsFromInventory(theirInventory, classIDsToReceive, true);
if ((itemsToGive.Count != itemsToReceive.Count) || !Trading.IsFairExchange(itemsToGive, itemsToReceive)) {
// Failsafe
Bot.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, Strings.ErrorAborted));
Bot.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.WarningFailedWithError, ArchiSteamFarm.Localization.Strings.ErrorAborted));
return (false, skippedSetsThisRound.Count > 0);
return;
}
if (triedSteamIDs.TryGetValue(listedUser.SteamID, out (byte Tries, ISet<ulong>? GivenAssetIDs, ISet<ulong>? ReceivedAssetIDs) previousAttempt)) {
if ((previousAttempt.GivenAssetIDs == null) || (previousAttempt.ReceivedAssetIDs == null) || (itemsToGive.Select(static item => item.AssetID).All(previousAttempt.GivenAssetIDs.Contains) && itemsToReceive.Select(static item => item.AssetID).All(previousAttempt.ReceivedAssetIDs.Contains))) {
// This user didn't respond in our previous round, avoid him for remaining ones
triedSteamIDs[listedUser.SteamID] = (byte.MaxValue, previousAttempt.GivenAssetIDs, previousAttempt.ReceivedAssetIDs);
triedSteamIDs.Add(listedUser.SteamID);
break;
}
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.MatchingFound, itemsToReceive.Count, listedUser.SteamID, listedUser.Nickname));
previousAttempt.GivenAssetIDs.UnionWith(itemsToGive.Select(static item => item.AssetID));
previousAttempt.ReceivedAssetIDs.UnionWith(itemsToReceive.Select(static item => item.AssetID));
} else {
previousAttempt.GivenAssetIDs = new HashSet<ulong>(itemsToGive.Select(static item => item.AssetID));
previousAttempt.ReceivedAssetIDs = new HashSet<ulong>(itemsToReceive.Select(static item => item.AssetID));
}
triedSteamIDs[listedUser.SteamID] = (++previousAttempt.Tries, previousAttempt.GivenAssetIDs, previousAttempt.ReceivedAssetIDs);
Bot.ArchiLogger.LogGenericTrace($"{Bot.SteamID} <- {string.Join(", ", itemsToReceive.Select(static item => $"{item.RealAppID}/{item.Type}-{item.ClassID} #{item.Amount}"))} | {string.Join(", ", itemsToGive.Select(static item => $"{item.RealAppID}/{item.Type}-{item.ClassID} #{item.Amount}"))} -> {listedUser.SteamID}");
Bot.ArchiLogger.LogGenericTrace($"{Bot.SteamID} <- {string.Join(", ", itemsToReceive.Select(static item => $"{item.RealAppID}/{item.Type}/{item.Rarity}/{item.ClassID} #{item.Amount}"))} | {string.Join(", ", itemsToGive.Select(static item => $"{item.RealAppID}/{item.Type}/{item.Rarity}/{item.ClassID} #{item.Amount}"))} -> {listedUser.SteamID}");
(bool success, HashSet<ulong>? mobileTradeOfferIDs) = await Bot.ArchiWebHandler.SendTradeOffer(listedUser.SteamID, itemsToGive, itemsToReceive, listedUser.TradeToken, true).ConfigureAwait(false);
@ -666,14 +672,14 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
(bool twoFactorSuccess, _, _) = await Bot.Actions.HandleTwoFactorAuthenticationConfirmations(true, Confirmation.EType.Trade, mobileTradeOfferIDs, true).ConfigureAwait(false);
if (!twoFactorSuccess) {
Bot.ArchiLogger.LogGenericTrace(Strings.WarningFailed);
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.WarningFailedWithError, nameof(twoFactorSuccess)));
return (false, skippedSetsThisRound.Count > 0);
return;
}
}
if (!success) {
Bot.ArchiLogger.LogGenericTrace(Strings.WarningFailed);
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.TradeOfferFailed, listedUser.SteamID, listedUser.Nickname));
break;
}
@ -682,15 +688,10 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
theirInventory.UnionWith(itemsToGive);
skippedSetsThisUser.UnionWith(skippedSetsThisTrade);
Bot.ArchiLogger.LogGenericTrace(Strings.Success);
Bot.ArchiLogger.LogGenericInfo(ArchiSteamFarm.Localization.Strings.Success);
}
if (skippedSetsThisUser.Count == 0) {
if (skippedSetsThisRound.Count == 0) {
// If we didn't find any match on clean round, this user isn't going to have anything interesting for us anytime soon
triedSteamIDs[listedUser.SteamID] = (byte.MaxValue, null, null);
}
continue;
}
@ -711,8 +712,5 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
}
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.ActivelyMatchingItemsRound, skippedSetsThisRound.Count));
// Keep matching when we either traded something this round (so it makes sense for a refresh) or if we didn't try all available bots yet (so it makes sense to keep going)
return (!rateLimited && (totalMatches > 0) && ((skippedSetsThisRound.Count > 0) || triedSteamIDs.Values.All(static data => data.Tries < 2)), skippedSetsThisRound.Count > 0);
}
}

View file

@ -0,0 +1,106 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2022 Ł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.Linq;
using ArchiSteamFarm.Steam.Data;
using ArchiSteamFarm.Steam.Storage;
using JetBrains.Annotations;
using Newtonsoft.Json;
using SteamKit2;
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Requests;
internal sealed class AnnouncementRequest {
[JsonProperty]
internal readonly string? AvatarHash;
[JsonProperty(Required = Required.Always)]
internal readonly Guid Guid;
[JsonProperty(Required = Required.Always)]
internal readonly ImmutableHashSet<AssetForListing> Inventory;
[JsonProperty(Required = Required.Always)]
internal readonly ImmutableHashSet<Asset.EType> MatchableTypes;
[JsonProperty(Required = Required.Always)]
internal readonly bool MatchEverything;
[JsonProperty(Required = Required.Always)]
internal readonly byte MaxTradeHoldDuration;
[JsonProperty]
internal readonly string? Nickname;
[JsonProperty(Required = Required.Always)]
internal readonly ulong SteamID;
[JsonProperty(Required = Required.Always)]
internal readonly string TradeToken;
internal AnnouncementRequest(Guid guid, ulong steamID, string tradeToken, IReadOnlyList<Asset> inventory, IReadOnlyCollection<Asset.EType> matchableTypes, bool matchEverything, byte maxTradeHoldDuration, string? nickname = null, string? avatarHash = null) {
if (guid == Guid.Empty) {
throw new ArgumentOutOfRangeException(nameof(guid));
}
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
throw new ArgumentOutOfRangeException(nameof(steamID));
}
if (string.IsNullOrEmpty(tradeToken)) {
throw new ArgumentNullException(nameof(tradeToken));
}
if (tradeToken.Length != BotConfig.SteamTradeTokenLength) {
throw new ArgumentOutOfRangeException(nameof(tradeToken));
}
if ((inventory == null) || (inventory.Count == 0)) {
throw new ArgumentNullException(nameof(inventory));
}
if ((matchableTypes == null) || (matchableTypes.Count == 0)) {
throw new ArgumentNullException(nameof(matchableTypes));
}
uint index = 0;
Guid = guid;
SteamID = steamID;
TradeToken = tradeToken;
Inventory = inventory.Select(asset => new AssetForListing(asset, index++)).ToImmutableHashSet();
MatchableTypes = matchableTypes.ToImmutableHashSet();
MatchEverything = matchEverything;
MaxTradeHoldDuration = maxTradeHoldDuration;
Nickname = nickname;
AvatarHash = avatarHash;
}
[UsedImplicitly]
public bool ShouldSerializeAvatarHash() => !string.IsNullOrEmpty(AvatarHash);
[UsedImplicitly]
public bool ShouldSerializeNickname() => !string.IsNullOrEmpty(Nickname);
}

View file

@ -0,0 +1,37 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2022 Ł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.Steam.Data;
using Newtonsoft.Json;
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Requests;
internal sealed class AssetForListing : AssetInInventory {
[JsonProperty(Required = Required.Always)]
internal readonly uint Index;
internal AssetForListing(Asset asset, uint index) : base(asset) {
ArgumentNullException.ThrowIfNull(asset);
Index = index;
}
}

View file

@ -0,0 +1,68 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2022 Ł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.Steam.Data;
using Newtonsoft.Json;
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Requests;
internal class AssetInInventory {
[JsonProperty(Required = Required.Always)]
internal readonly uint Amount;
[JsonProperty(Required = Required.Always)]
internal readonly ulong AssetID;
[JsonProperty(Required = Required.Always)]
internal readonly ulong ClassID;
[JsonProperty(Required = Required.Always)]
internal readonly Asset.ERarity Rarity;
[JsonProperty(Required = Required.Always)]
internal readonly uint RealAppID;
[JsonProperty(Required = Required.Always)]
internal readonly bool Tradable;
[JsonProperty(Required = Required.Always)]
internal readonly Asset.EType Type;
internal AssetInInventory(Asset asset) {
ArgumentNullException.ThrowIfNull(asset);
AssetID = asset.AssetID;
Amount = asset.Amount;
ClassID = asset.ClassID;
Tradable = asset.Tradable;
RealAppID = asset.RealAppID;
Type = asset.Type;
Rarity = asset.Rarity;
}
[JsonConstructor]
private AssetInInventory() { }
internal Asset ToAsset() => new(Asset.SteamAppID, Asset.SteamCommunityContextID, ClassID, Amount, tradable: Tradable, assetID: AssetID, realAppID: RealAppID, type: Type, rarity: Rarity);
}

View file

@ -0,0 +1,47 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2022 Ł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 Newtonsoft.Json;
using SteamKit2;
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Requests;
internal sealed class HeartBeatRequest {
[JsonProperty(Required = Required.Always)]
internal readonly Guid Guid;
[JsonProperty(Required = Required.Always)]
internal readonly ulong SteamID;
internal HeartBeatRequest(Guid guid, ulong steamID) {
if (guid == Guid.Empty) {
throw new ArgumentOutOfRangeException(nameof(guid));
}
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
throw new ArgumentOutOfRangeException(nameof(steamID));
}
Guid = guid;
SteamID = steamID;
}
}

View file

@ -0,0 +1,84 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2022 Ł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.Linq;
using ArchiSteamFarm.Steam.Data;
using ArchiSteamFarm.Steam.Storage;
using Newtonsoft.Json;
using SteamKit2;
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Requests;
internal sealed class InventoriesRequest {
[JsonProperty(Required = Required.Always)]
internal readonly Guid Guid;
[JsonProperty(Required = Required.Always)]
internal readonly ImmutableHashSet<AssetInInventory> Inventory;
[JsonProperty(Required = Required.Always)]
internal readonly ImmutableHashSet<Asset.EType> MatchableTypes;
[JsonProperty(Required = Required.Always)]
internal readonly byte MaxTradeHoldDuration;
[JsonProperty(Required = Required.Always)]
internal readonly ulong SteamID;
[JsonProperty(Required = Required.Always)]
internal readonly string TradeToken;
internal InventoriesRequest(Guid guid, ulong steamID, string tradeToken, IReadOnlyCollection<Asset> inventory, IReadOnlyCollection<Asset.EType> matchableTypes, byte maxTradeHoldDuration) {
if (guid == Guid.Empty) {
throw new ArgumentOutOfRangeException(nameof(guid));
}
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
throw new ArgumentOutOfRangeException(nameof(steamID));
}
if (string.IsNullOrEmpty(tradeToken)) {
throw new ArgumentNullException(nameof(tradeToken));
}
if (tradeToken.Length != BotConfig.SteamTradeTokenLength) {
throw new ArgumentOutOfRangeException(nameof(tradeToken));
}
if ((inventory == null) || (inventory.Count == 0)) {
throw new ArgumentNullException(nameof(inventory));
}
if ((matchableTypes == null) || (matchableTypes.Count == 0)) {
throw new ArgumentNullException(nameof(matchableTypes));
}
Guid = guid;
SteamID = steamID;
TradeToken = tradeToken;
Inventory = inventory.Select(static asset => new AssetInInventory(asset)).ToImmutableHashSet();
MatchableTypes = matchableTypes.ToImmutableHashSet();
MaxTradeHoldDuration = maxTradeHoldDuration;
}
}

View file

@ -0,0 +1,65 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2022 Ł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.Immutable;
using System.Diagnostics.CodeAnalysis;
using ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Requests;
using ArchiSteamFarm.Steam.Data;
using Newtonsoft.Json;
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Responses;
#pragma warning disable CA1812 // False positive, the class is used during json deserialization
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
internal sealed class ListedUser {
[JsonProperty(Required = Required.Always)]
internal readonly ImmutableHashSet<AssetInInventory> Assets = ImmutableHashSet<AssetInInventory>.Empty;
[JsonProperty(Required = Required.Always)]
internal readonly ImmutableHashSet<Asset.EType> MatchableTypes = ImmutableHashSet<Asset.EType>.Empty;
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
[JsonProperty(Required = Required.Always)]
internal readonly bool MatchEverything;
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
[JsonProperty(Required = Required.Always)]
internal readonly byte MaxTradeHoldDuration;
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
[JsonProperty(Required = Required.AllowNull)]
internal readonly string? Nickname;
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
[JsonProperty(Required = Required.Always)]
internal readonly ulong SteamID;
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
[JsonProperty(Required = Required.Always)]
internal readonly string TradeToken = "";
[JsonConstructor]
private ListedUser() { }
}
#pragma warning restore CA1812 // False positive, the class is used during json deserialization

View file

@ -545,27 +545,26 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
ASF.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.WarningFailedWithError, response.StatusCode));
switch (response.StatusCode) {
// SteamDB told us to stop submitting data for now
case HttpStatusCode.Forbidden:
// SteamDB told us to stop submitting data for now
// ReSharper disable once SuspiciousLockOverSynchronizationPrimitive - this is not a mistake, we need extra synchronization, and we can re-use the semaphore object for that
lock (SubmissionSemaphore) {
SubmissionTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
}
break;
// SteamDB told us to reset our cache
case HttpStatusCode.Conflict:
// SteamDB told us to reset our cache
GlobalCache.Reset(true);
break;
// SteamDB told us to try again later
#if NETFRAMEWORK
case (HttpStatusCode) 429:
#else
case HttpStatusCode.TooManyRequests:
#endif
// SteamDB told us to try again later
#pragma warning disable CA5394 // This call isn't used in a security-sensitive manner
TimeSpan startIn = TimeSpan.FromMinutes(Random.Shared.Next(SharedInfo.MinimumMinutesBeforeFirstUpload, SharedInfo.MaximumMinutesBeforeFirstUpload));
#pragma warning restore CA5394 // This call isn't used in a security-sensitive manner

View file

@ -1,4 +1,4 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.6.30114.105
@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArchiSteamFarm.CustomPlugin
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArchiSteamFarm.OfficialPlugins.SteamTokenDumper", "ArchiSteamFarm.OfficialPlugins.SteamTokenDumper\ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.csproj", "{324E42B1-3C5D-421D-A600-1BEEE7AF401A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArchiSteamFarm.OfficialPlugins.ItemsMatcher", "ArchiSteamFarm.OfficialPlugins.ItemsMatcher\ArchiSteamFarm.OfficialPlugins.ItemsMatcher.csproj", "{A8BA3425-2EE4-4333-9905-8867EDFE327F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -53,5 +55,11 @@ Global
{324E42B1-3C5D-421D-A600-1BEEE7AF401A}.DebugFast|Any CPU.Build.0 = DebugFast|Any CPU
{324E42B1-3C5D-421D-A600-1BEEE7AF401A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{324E42B1-3C5D-421D-A600-1BEEE7AF401A}.Release|Any CPU.Build.0 = Release|Any CPU
{A8BA3425-2EE4-4333-9905-8867EDFE327F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A8BA3425-2EE4-4333-9905-8867EDFE327F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A8BA3425-2EE4-4333-9905-8867EDFE327F}.DebugFast|Any CPU.ActiveCfg = Debug|Any CPU
{A8BA3425-2EE4-4333-9905-8867EDFE327F}.DebugFast|Any CPU.Build.0 = Debug|Any CPU
{A8BA3425-2EE4-4333-9905-8867EDFE327F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A8BA3425-2EE4-4333-9905-8867EDFE327F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View file

@ -26,8 +26,10 @@ using System.Runtime.CompilerServices;
#if ASF_SIGNED_BUILD
[assembly: InternalsVisibleTo("ArchiSteamFarm.Tests, PublicKey=002400000480000014020000060200000024000052534131001000000100010099f0e5961ec7497fd7de1cba2b8c5eff3b18c1faf3d7a8d56e063359c7f928b54b14eae24d23d9d3c1a5db7ceca82edb6956d43e8ea2a0b7223e6e6836c0b809de43fde69bf33fba73cf669e71449284d477333d4b6e54fb69f7b6c4b4811b8fe26e88975e593cffc0e321490a50500865c01e50ab87c8a943b2a788af47dc20f2b860062b7b6df25477e471a744485a286b435cea2df3953cbb66febd8db73f3ccb4588886373141d200f749ba40bb11926b668cc15f328412dd0b0b835909229985336eb4a34f47925558dc6dc3910ea09c1aad5c744833f26ad9de727559d393526a7a29b3383de87802a034ead8ecc2d37340a5fa9b406774446256337d77e3c9e8486b5e732097e238312deaf5b4efcc04df8ecb986d90ee12b4a8a9a00319cc25cb91fd3e36a3cc39e501f83d14eb1e1a6fa6a1365483d99f4cefad1ea5dec204dad958e2a9a93add19781a8aa7bac71747b11d156711eafd1e873e19836eb573fa5cde284739df09b658ed40c56c7b5a7596840774a7065864e6c2af7b5a8bf7a2d238de83d77891d98ef5a4a58248c655a1c7c97c99e01d9928dc60c629eeb523356dc3686e3f9a1a30ffcd0268cd03718292f21d839fce741f4c1163001ab5b654c37d862998962a05e8028e061c611384772777ef6a49b00ebb4f228308e61b2afe408b33db2d82c4f385e26d7438ec0a183c64eeca4138cbc3dc2")]
[assembly: InternalsVisibleTo("ArchiSteamFarm.OfficialPlugins.ItemsMatcher, PublicKey=002400000480000014020000060200000024000052534131001000000100010099f0e5961ec7497fd7de1cba2b8c5eff3b18c1faf3d7a8d56e063359c7f928b54b14eae24d23d9d3c1a5db7ceca82edb6956d43e8ea2a0b7223e6e6836c0b809de43fde69bf33fba73cf669e71449284d477333d4b6e54fb69f7b6c4b4811b8fe26e88975e593cffc0e321490a50500865c01e50ab87c8a943b2a788af47dc20f2b860062b7b6df25477e471a744485a286b435cea2df3953cbb66febd8db73f3ccb4588886373141d200f749ba40bb11926b668cc15f328412dd0b0b835909229985336eb4a34f47925558dc6dc3910ea09c1aad5c744833f26ad9de727559d393526a7a29b3383de87802a034ead8ecc2d37340a5fa9b406774446256337d77e3c9e8486b5e732097e238312deaf5b4efcc04df8ecb986d90ee12b4a8a9a00319cc25cb91fd3e36a3cc39e501f83d14eb1e1a6fa6a1365483d99f4cefad1ea5dec204dad958e2a9a93add19781a8aa7bac71747b11d156711eafd1e873e19836eb573fa5cde284739df09b658ed40c56c7b5a7596840774a7065864e6c2af7b5a8bf7a2d238de83d77891d98ef5a4a58248c655a1c7c97c99e01d9928dc60c629eeb523356dc3686e3f9a1a30ffcd0268cd03718292f21d839fce741f4c1163001ab5b654c37d862998962a05e8028e061c611384772777ef6a49b00ebb4f228308e61b2afe408b33db2d82c4f385e26d7438ec0a183c64eeca4138cbc3dc2")]
[assembly: InternalsVisibleTo("ArchiSteamFarm.OfficialPlugins.SteamTokenDumper, PublicKey=002400000480000014020000060200000024000052534131001000000100010099f0e5961ec7497fd7de1cba2b8c5eff3b18c1faf3d7a8d56e063359c7f928b54b14eae24d23d9d3c1a5db7ceca82edb6956d43e8ea2a0b7223e6e6836c0b809de43fde69bf33fba73cf669e71449284d477333d4b6e54fb69f7b6c4b4811b8fe26e88975e593cffc0e321490a50500865c01e50ab87c8a943b2a788af47dc20f2b860062b7b6df25477e471a744485a286b435cea2df3953cbb66febd8db73f3ccb4588886373141d200f749ba40bb11926b668cc15f328412dd0b0b835909229985336eb4a34f47925558dc6dc3910ea09c1aad5c744833f26ad9de727559d393526a7a29b3383de87802a034ead8ecc2d37340a5fa9b406774446256337d77e3c9e8486b5e732097e238312deaf5b4efcc04df8ecb986d90ee12b4a8a9a00319cc25cb91fd3e36a3cc39e501f83d14eb1e1a6fa6a1365483d99f4cefad1ea5dec204dad958e2a9a93add19781a8aa7bac71747b11d156711eafd1e873e19836eb573fa5cde284739df09b658ed40c56c7b5a7596840774a7065864e6c2af7b5a8bf7a2d238de83d77891d98ef5a4a58248c655a1c7c97c99e01d9928dc60c629eeb523356dc3686e3f9a1a30ffcd0268cd03718292f21d839fce741f4c1163001ab5b654c37d862998962a05e8028e061c611384772777ef6a49b00ebb4f228308e61b2afe408b33db2d82c4f385e26d7438ec0a183c64eeca4138cbc3dc2")]
#else
[assembly: InternalsVisibleTo("ArchiSteamFarm.Tests")]
[assembly: InternalsVisibleTo("ArchiSteamFarm.OfficialPlugins.ItemsMatcher")]
[assembly: InternalsVisibleTo("ArchiSteamFarm.OfficialPlugins.SteamTokenDumper")]
#endif

View file

@ -20,65 +20,14 @@
// limitations under the License.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using ArchiSteamFarm.Localization;
using ArchiSteamFarm.Steam;
using ArchiSteamFarm.Steam.Data;
using ArchiSteamFarm.Steam.Storage;
using ArchiSteamFarm.Storage;
using ArchiSteamFarm.Web;
using ArchiSteamFarm.IPC.Responses;
using ArchiSteamFarm.Web.Responses;
using Newtonsoft.Json;
namespace ArchiSteamFarm.Core;
internal static class ArchiNet {
private static Uri URL => new("https://asf.JustArchi.net");
internal static async Task<HttpStatusCode?> AnnounceForListing(Bot bot, IReadOnlyCollection<Asset> inventory, IReadOnlyCollection<Asset.EType> acceptedMatchableTypes, string tradeToken, string? nickname = null, string? avatarHash = null) {
ArgumentNullException.ThrowIfNull(bot);
if ((inventory == null) || (inventory.Count == 0)) {
throw new ArgumentNullException(nameof(inventory));
}
if ((acceptedMatchableTypes == null) || (acceptedMatchableTypes.Count == 0)) {
throw new ArgumentNullException(nameof(acceptedMatchableTypes));
}
if (string.IsNullOrEmpty(tradeToken)) {
throw new ArgumentNullException(nameof(tradeToken));
}
if (tradeToken.Length != BotConfig.SteamTradeTokenLength) {
throw new ArgumentOutOfRangeException(nameof(tradeToken));
}
Uri request = new(URL, "/Api/Announce");
Dictionary<string, string> data = new(10, StringComparer.Ordinal) {
{ "AvatarHash", avatarHash ?? "" },
{ "GamesCount", inventory.Select(static item => item.RealAppID).Distinct().Count().ToString(CultureInfo.InvariantCulture) },
{ "Guid", (ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid()).ToString("N") },
{ "ItemsCount", inventory.Count.ToString(CultureInfo.InvariantCulture) },
{ "MatchableTypes", JsonConvert.SerializeObject(acceptedMatchableTypes) },
{ "MatchEverything", bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchEverything) ? "1" : "0" },
{ "MaxTradeHoldDuration", (ASF.GlobalConfig?.MaxTradeHoldDuration ?? GlobalConfig.DefaultMaxTradeHoldDuration).ToString(CultureInfo.InvariantCulture) },
{ "Nickname", nickname ?? "" },
{ "SteamID", bot.SteamID.ToString(CultureInfo.InvariantCulture) },
{ "TradeToken", tradeToken }
};
BasicResponse? response = await bot.ArchiWebHandler.WebBrowser.UrlPost(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors).ConfigureAwait(false);
return response?.StatusCode;
}
internal static Uri URL => new("https://asf.JustArchi.net");
internal static async Task<string?> FetchBuildChecksum(Version version, string variant) {
ArgumentNullException.ThrowIfNull(version);
@ -93,177 +42,12 @@ internal static class ArchiNet {
Uri request = new(URL, $"/Api/Checksum/{version}/{variant}");
ObjectResponse<ChecksumResponse>? response = await ASF.WebBrowser.UrlGetToJsonObject<ChecksumResponse>(request).ConfigureAwait(false);
ObjectResponse<GenericResponse<string>>? response = await ASF.WebBrowser.UrlGetToJsonObject<GenericResponse<string>>(request).ConfigureAwait(false);
if (response?.Content == null) {
return null;
}
return response.Content.Checksum ?? "";
}
internal static async Task<ImmutableHashSet<ListedUser>?> GetListedUsers(Bot bot) {
ArgumentNullException.ThrowIfNull(bot);
Uri request = new(URL, "/Api/Bots");
ObjectResponse<ImmutableHashSet<ListedUser>>? response = await bot.ArchiWebHandler.WebBrowser.UrlGetToJsonObject<ImmutableHashSet<ListedUser>>(request).ConfigureAwait(false);
return response?.Content;
}
internal static async Task<HttpStatusCode?> HeartBeatForListing(Bot bot) {
ArgumentNullException.ThrowIfNull(bot);
Uri request = new(URL, "/Api/HeartBeat");
Dictionary<string, string> data = new(2, StringComparer.Ordinal) {
{ "Guid", (ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid()).ToString("N") },
{ "SteamID", bot.SteamID.ToString(CultureInfo.InvariantCulture) }
};
BasicResponse? response = await bot.ArchiWebHandler.WebBrowser.UrlPost(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors).ConfigureAwait(false);
return response?.StatusCode;
}
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
internal sealed class ListedUser {
[JsonProperty("items_count", Required = Required.Always)]
internal readonly ushort ItemsCount;
internal readonly HashSet<Asset.EType> MatchableTypes = new();
[JsonProperty("max_trade_hold_duration", Required = Required.Always)]
internal readonly byte MaxTradeHoldDuration;
[JsonProperty("steam_id", Required = Required.Always)]
internal readonly ulong SteamID;
[JsonProperty("trade_token", Required = Required.Always)]
internal readonly string TradeToken = "";
internal float Score => GamesCount / (float) ItemsCount;
#pragma warning disable CS0649 // False positive, it's a field set during json deserialization
[JsonProperty("games_count", Required = Required.Always)]
private readonly ushort GamesCount;
#pragma warning restore CS0649 // False positive, it's a field set during json deserialization
internal bool MatchEverything { get; private set; }
[JsonProperty("matchable_backgrounds", Required = Required.Always)]
private byte MatchableBackgroundsNumber {
set {
switch (value) {
case 0:
MatchableTypes.Remove(Asset.EType.ProfileBackground);
break;
case 1:
MatchableTypes.Add(Asset.EType.ProfileBackground);
break;
default:
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value));
return;
}
}
}
[JsonProperty("matchable_cards", Required = Required.Always)]
private byte MatchableCardsNumber {
set {
switch (value) {
case 0:
MatchableTypes.Remove(Asset.EType.TradingCard);
break;
case 1:
MatchableTypes.Add(Asset.EType.TradingCard);
break;
default:
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value));
return;
}
}
}
[JsonProperty("matchable_emoticons", Required = Required.Always)]
private byte MatchableEmoticonsNumber {
set {
switch (value) {
case 0:
MatchableTypes.Remove(Asset.EType.Emoticon);
break;
case 1:
MatchableTypes.Add(Asset.EType.Emoticon);
break;
default:
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value));
return;
}
}
}
[JsonProperty("matchable_foil_cards", Required = Required.Always)]
private byte MatchableFoilCardsNumber {
set {
switch (value) {
case 0:
MatchableTypes.Remove(Asset.EType.FoilTradingCard);
break;
case 1:
MatchableTypes.Add(Asset.EType.FoilTradingCard);
break;
default:
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value));
return;
}
}
}
[JsonProperty("match_everything", Required = Required.Always)]
private byte MatchEverythingNumber {
set {
switch (value) {
case 0:
MatchEverything = false;
break;
case 1:
MatchEverything = true;
break;
default:
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(value), value));
return;
}
}
}
[JsonConstructor]
private ListedUser() { }
}
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
private sealed class ChecksumResponse {
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
[JsonProperty("checksum", Required = Required.AllowNull)]
internal readonly string? Checksum;
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
[JsonConstructor]
private ChecksumResponse() { }
return response.Content.Result ?? "";
}
}

View file

@ -39,6 +39,9 @@ public sealed class GenericResponse<T> : GenericResponse where T : class {
public GenericResponse(bool success, string? message) : base(success, message) { }
public GenericResponse(bool success, T? result) : base(success) => Result = result;
public GenericResponse(bool success, string? message, T? result) : base(success, message) => Result = result;
[JsonConstructor]
private GenericResponse() { }
}
public class GenericResponse {
@ -49,7 +52,7 @@ public class GenericResponse {
/// This property will provide exact reason for majority of expected failures.
/// </remarks>
[JsonProperty]
public string Message { get; private set; }
public string? Message { get; private set; }
/// <summary>
/// Boolean type that specifies if the request has succeeded.
@ -64,4 +67,7 @@ public class GenericResponse {
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
Message = !string.IsNullOrEmpty(message) ? message! : success ? "OK" : Strings.WarningFailed;
}
[JsonConstructor]
protected GenericResponse() { }
}

File diff suppressed because it is too large Load diff

View file

@ -603,10 +603,6 @@ Process uptime: {1}</value>
<data name="ErrorAborted" xml:space="preserve">
<value>Aborted!</value>
</data>
<data name="ActivelyMatchingItemsRound" xml:space="preserve">
<value>Matched a total of {0} sets this round.</value>
<comment>{0} will be replaced by number of sets traded</comment>
</data>
<data name="WarningExcessiveBotsCount" xml:space="preserve">
<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>
@ -744,4 +740,8 @@ Process uptime: {1}</value>
<value>The IP address {0} is not banned!</value>
<comment>{0} will be replaced by an IP address which was requested to be unbanned from using IPC</comment>
</data>
<data name="WarningNoLicense" xml:space="preserve">
<value>You've attempted to use paid feature {0} but you don't have a valid LicenseID set in the ASF global config. Please review your configuration, as the functionality won't work without additional details.</value>
<comment>{0} will be replaced by feature name (e.g. MatchActively)</comment>
</data>
</root>

View file

@ -0,0 +1,39 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2022 Ł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.Steam;
using JetBrains.Annotations;
using SteamKit2;
namespace ArchiSteamFarm.Plugins.Interfaces;
[PublicAPI]
public interface IBotIdentity : IPlugin {
/// <summary>
/// ASF will call this method when bot receives its own identity information.
/// </summary>
/// <param name="bot">Bot object related to this callback.</param>
/// <param name="data">Full data received by ASF in the callback that relates to this bot (Steam) account.</param>
/// <param name="nickname">Parsed nickname set for this bot (Steam) account.</param>
/// <param name="avatarHash">Parsed hash of the avatar of this bot (Steam) account, as hex.</param>
Task OnSelfPersonaState(Bot bot, SteamFriends.PersonaStateCallback data, string? nickname, string? avatarHash);
}

View file

@ -453,6 +453,21 @@ public static class PluginsCore {
}
}
internal static async Task OnSelfPersonaState(Bot bot, SteamFriends.PersonaStateCallback data, string? nickname, string? avatarHash) {
ArgumentNullException.ThrowIfNull(bot);
ArgumentNullException.ThrowIfNull(data);
if ((ActivePlugins == null) || (ActivePlugins.Count == 0)) {
return;
}
try {
await Utilities.InParallel(ActivePlugins.OfType<IBotIdentity>().Select(plugin => plugin.OnSelfPersonaState(bot, data, nickname, avatarHash))).ConfigureAwait(false);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
}
}
internal static async Task<string?> OnBotMessage(Bot bot, ulong steamID, string message) {
ArgumentNullException.ThrowIfNull(bot);

View file

@ -214,6 +214,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
[PublicAPI]
public ECurrencyCode WalletCurrency { get; private set; }
internal byte HeartBeatFailures { get; private set; }
internal bool PlayingBlocked { get; private set; }
internal bool PlayingWasBlocked { get; private set; }
@ -225,7 +226,6 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
private Timer? ConnectionFailureTimer;
private bool FirstTradeSent;
private Timer? GamesRedeemerInBackgroundTimer;
private byte HeartBeatFailures;
private EResult LastLogOnResult;
private DateTime LastLogonSessionReplaced;
private bool LibraryLocked;
@ -233,7 +233,6 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
private ulong MasterChatGroupID;
private Timer? PlayingWasBlockedTimer;
private bool ReconnectOnUserInitiated;
private RemoteCommunication? RemoteCommunication;
private bool SendCompleteTypesScheduled;
private Timer? SendItemsTimer;
private bool SteamParentalActive;
@ -345,7 +344,6 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
ConnectionFailureTimer?.Dispose();
GamesRedeemerInBackgroundTimer?.Dispose();
PlayingWasBlockedTimer?.Dispose();
RemoteCommunication?.Dispose();
SendItemsTimer?.Dispose();
SteamSaleEvent?.Dispose();
}
@ -382,10 +380,6 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
await SendItemsTimer.DisposeAsync().ConfigureAwait(false);
}
if (RemoteCommunication != null) {
await RemoteCommunication.DisposeAsync().ConfigureAwait(false);
}
if (SteamSaleEvent != null) {
await SteamSaleEvent.DisposeAsync().ConfigureAwait(false);
}
@ -1946,10 +1940,6 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
}
HeartBeatFailures = 0;
if (RemoteCommunication != null) {
Utilities.InBackground(RemoteCommunication.OnHeartBeat);
}
} catch (Exception e) {
ArchiLogger.LogGenericDebuggingException(e);
@ -2097,16 +2087,6 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
SteamSaleEvent = new SteamSaleEvent(this);
}
if (RemoteCommunication != null) {
await RemoteCommunication.DisposeAsync().ConfigureAwait(false);
RemoteCommunication = null;
}
if (BotConfig.RemoteCommunication > BotConfig.ERemoteCommunication.None) {
RemoteCommunication = new RemoteCommunication(this);
}
await PluginsCore.OnBotInitModules(this, BotConfig.AdditionalProperties).ConfigureAwait(false);
}
@ -2862,10 +2842,6 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
Utilities.InBackground(InitializeFamilySharing);
if (RemoteCommunication != null) {
Utilities.InBackground(RemoteCommunication.OnLoggedOn);
}
ResetPersonaState();
if (BotConfig.SteamMasterClanID != 0) {
@ -2880,6 +2856,10 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
);
}
if (BotConfig.RemoteCommunication.HasFlag(BotConfig.ERemoteCommunication.SteamGroup)) {
Utilities.InBackground(() => ArchiWebHandler.JoinGroup(SharedInfo.ASFGroupSteamID));
}
if (CardsFarmer.Paused) {
// Emit initial game playing status in this case
Utilities.InBackground(ResetGamesPlayed);
@ -3013,13 +2993,15 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
);
}
private void OnPersonaState(SteamFriends.PersonaStateCallback callback) {
private async void OnPersonaState(SteamFriends.PersonaStateCallback callback) {
ArgumentNullException.ThrowIfNull(callback);
if (callback.FriendID != SteamID) {
return;
}
Nickname = callback.Name;
string? avatarHash = null;
if ((callback.AvatarHash?.Length > 0) && callback.AvatarHash.Any(static singleByte => singleByte != 0)) {
@ -3033,11 +3015,8 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
}
AvatarHash = avatarHash;
Nickname = callback.Name;
if (RemoteCommunication != null) {
Utilities.InBackground(() => RemoteCommunication.OnPersonaState(callback.Name, avatarHash));
}
await PluginsCore.OnSelfPersonaState(this, callback, Nickname, AvatarHash).ConfigureAwait(false);
}
private async void OnPlayingSessionState(ArchiHandler.PlayingSessionStateCallback callback) {

View file

@ -37,7 +37,7 @@ namespace ArchiSteamFarm.Steam.Data;
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
internal sealed class InventoryResponse : OptionalResultResponse {
[JsonProperty("assets", Required = Required.DisallowNull)]
internal readonly ImmutableHashSet<Asset> Assets = ImmutableHashSet<Asset>.Empty;
internal readonly ImmutableList<Asset> Assets = ImmutableList<Asset>.Empty;
[JsonProperty("descriptions", Required = Required.DisallowNull)]
internal readonly ImmutableHashSet<Description> Descriptions = ImmutableHashSet<Description>.Empty;

View file

@ -262,7 +262,7 @@ public sealed class Trading : IDisposable {
return tradableState;
}
internal static HashSet<Asset> GetTradableItemsFromInventory(IReadOnlyCollection<Asset> inventory, IDictionary<ulong, uint> classIDs) {
internal static HashSet<Asset> GetTradableItemsFromInventory(IReadOnlyCollection<Asset> inventory, IDictionary<ulong, uint> classIDs, bool randomize = false) {
if ((inventory == null) || (inventory.Count == 0)) {
throw new ArgumentNullException(nameof(inventory));
}
@ -273,7 +273,16 @@ public sealed class Trading : IDisposable {
HashSet<Asset> result = new();
foreach (Asset item in inventory.Where(static item => item.Tradable)) {
IEnumerable<Asset> items = inventory;
// Randomization helps to decrease "items no longer available" in regards to sending offers to other users
if (randomize) {
#pragma warning disable CA5394 // This call isn't used in a security-sensitive manner
items = items.OrderBy(static _ => Random.Shared.Next());
#pragma warning restore CA5394 // This call isn't used in a security-sensitive manner
}
foreach (Asset item in items.Where(static item => item.Tradable)) {
if (!classIDs.TryGetValue(item.ClassID, out uint amount)) {
continue;
}

View file

@ -48,10 +48,9 @@ using SteamKit2;
namespace ArchiSteamFarm.Steam.Integration;
public sealed class ArchiWebHandler : IDisposable {
internal const ushort MaxItemsInSingleInventoryRequest = 5000;
private const string EconService = "IEconService";
private const string LoyaltyRewardsService = "ILoyaltyRewardsService";
private const ushort MaxItemsInSingleInventoryRequest = 5000;
private const byte MinimumSessionValidityInSeconds = 10;
private const string SteamAppsService = "ISteamApps";
private const string SteamUserAuthService = "ISteamUserAuth";

View file

@ -129,6 +129,9 @@ public sealed class GlobalConfig {
[PublicAPI]
public static readonly ImmutableHashSet<uint> DefaultBlacklist = ImmutableHashSet<uint>.Empty;
[PublicAPI]
public static readonly Guid? DefaultLicenseID;
private static readonly ImmutableHashSet<string> ForbiddenIPCPasswordPhrases = ImmutableHashSet.Create(StringComparer.InvariantCultureIgnoreCase, "ipc", "api", "gui", "asf-ui", "asf-gui");
[JsonIgnore]
@ -237,6 +240,9 @@ public sealed class GlobalConfig {
[JsonProperty(Required = Required.DisallowNull)]
public ArchiCryptoHelper.EHashingMethod IPCPasswordFormat { get; private set; } = DefaultIPCPasswordFormat;
[JsonProperty]
public Guid? LicenseID { get; private set; } = DefaultLicenseID;
[JsonProperty(Required = Required.DisallowNull)]
[Range(byte.MinValue, byte.MaxValue)]
public byte LoginLimiterDelay { get; private set; } = DefaultLoginLimiterDelay;
@ -375,6 +381,9 @@ public sealed class GlobalConfig {
[UsedImplicitly]
public bool ShouldSerializeIPCPasswordFormat() => !Saving || (IPCPasswordFormat != DefaultIPCPasswordFormat);
[UsedImplicitly]
public bool ShouldSerializeLicenseID() => !Saving || ((LicenseID != DefaultLicenseID) && (LicenseID != Guid.Empty));
[UsedImplicitly]
public bool ShouldSerializeLoginLimiterDelay() => !Saving || (LoginLimiterDelay != DefaultLoginLimiterDelay);

View file

@ -4,9 +4,10 @@ FROM --platform=$BUILDPLATFORM node:lts${IMAGESUFFIX} AS build-node
WORKDIR /app/ASF-ui
COPY ASF-ui .
COPY .git/modules/ASF-ui /app/.git/modules/ASF-ui
RUN echo "node: $(node --version)" && \
echo "npm: $(npm --version)" && \
npm ci --no-progress && \
RUN set -eu; \
echo "node: $(node --version)"; \
echo "npm: $(npm --version)"; \
npm ci --no-progress; \
npm run deploy --no-progress
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:7.0${IMAGESUFFIX} AS build-dotnet
@ -17,30 +18,43 @@ ARG TARGETOS
ENV DOTNET_CLI_TELEMETRY_OPTOUT true
ENV DOTNET_NOLOGO true
ENV NET_CORE_VERSION net7.0
ENV PLUGINS ArchiSteamFarm.OfficialPlugins.ItemsMatcher
ENV STEAM_TOKEN_DUMPER_NAME ArchiSteamFarm.OfficialPlugins.SteamTokenDumper
WORKDIR /app
COPY --from=build-node /app/ASF-ui/dist ASF-ui/dist
COPY ArchiSteamFarm ArchiSteamFarm
COPY ArchiSteamFarm.OfficialPlugins.ItemsMatcher ArchiSteamFarm.OfficialPlugins.ItemsMatcher
COPY ArchiSteamFarm.OfficialPlugins.SteamTokenDumper ArchiSteamFarm.OfficialPlugins.SteamTokenDumper
COPY resources resources
COPY .editorconfig .editorconfig
COPY Directory.Build.props Directory.Build.props
COPY Directory.Packages.props Directory.Packages.props
COPY LICENSE.txt LICENSE.txt
RUN dotnet --info && \
RUN set -eu; \
dotnet --info; \
\
case "$TARGETOS" in \
"linux") ;; \
*) echo "ERROR: Unsupported OS: ${TARGETOS}"; exit 1 ;; \
esac && \
esac; \
\
case "$TARGETARCH" in \
"amd64") asf_variant="${TARGETOS}-x64" ;; \
"arm") asf_variant="${TARGETOS}-${TARGETARCH}" ;; \
"arm64") asf_variant="${TARGETOS}-${TARGETARCH}" ;; \
*) echo "ERROR: Unsupported CPU architecture: ${TARGETARCH}"; exit 1 ;; \
esac && \
if [ -n "${STEAM_TOKEN_DUMPER_TOKEN-}" ] && [ -f "${STEAM_TOKEN_DUMPER_NAME}/SharedInfo.cs" ]; then sed -i "s/STEAM_TOKEN_DUMPER_TOKEN/${STEAM_TOKEN_DUMPER_TOKEN}/g" "${STEAM_TOKEN_DUMPER_NAME}/SharedInfo.cs"; dotnet publish "${STEAM_TOKEN_DUMPER_NAME}" -c "$CONFIGURATION" -f "$NET_CORE_VERSION" -o "out/${STEAM_TOKEN_DUMPER_NAME}/${NET_CORE_VERSION}" -p:ASFVariant=docker -p:ContinuousIntegrationBuild=true -p:UseAppHost=false -r "$asf_variant" --nologo --no-self-contained; fi && \
dotnet publish ArchiSteamFarm -c "$CONFIGURATION" -f "$NET_CORE_VERSION" -o "out/result" -p:ASFVariant=docker -p:ContinuousIntegrationBuild=true -p:UseAppHost=false -r "$asf_variant" --nologo --no-self-contained && \
if [ -d "out/${STEAM_TOKEN_DUMPER_NAME}/${NET_CORE_VERSION}" ]; then mkdir -p "out/result/plugins/${STEAM_TOKEN_DUMPER_NAME}"; cp -pR "out/${STEAM_TOKEN_DUMPER_NAME}/${NET_CORE_VERSION}/"* "out/result/plugins/${STEAM_TOKEN_DUMPER_NAME}"; fi
esac; \
\
dotnet publish ArchiSteamFarm -c "$CONFIGURATION" -f "$NET_CORE_VERSION" -o "out/result" -p:ASFVariant=docker -p:ContinuousIntegrationBuild=true -p:UseAppHost=false -r "$asf_variant" --nologo --no-self-contained; \
\
if [ -n "${STEAM_TOKEN_DUMPER_TOKEN-}" ] && [ -f "${STEAM_TOKEN_DUMPER_NAME}/SharedInfo.cs" ]; then \
sed -i "s/STEAM_TOKEN_DUMPER_TOKEN/${STEAM_TOKEN_DUMPER_TOKEN}/g" "${STEAM_TOKEN_DUMPER_NAME}/SharedInfo.cs"; \
dotnet publish "${STEAM_TOKEN_DUMPER_NAME}" -c "$CONFIGURATION" -f "$NET_CORE_VERSION" -o "out/result/plugins/${STEAM_TOKEN_DUMPER_NAME}" -p:ASFVariant=docker -p:ContinuousIntegrationBuild=true -p:UseAppHost=false -r "$asf_variant" --nologo --no-self-contained; \
fi; \
\
for plugin in $PLUGINS; do \
dotnet publish "$plugin" -c "$CONFIGURATION" -f "$NET_CORE_VERSION" -o "out/result/plugins/$plugin" -p:ASFVariant=docker -p:ContinuousIntegrationBuild=true -p:UseAppHost=false -r "$asf_variant" --nologo --no-self-contained; \
done
FROM --platform=$TARGETPLATFORM mcr.microsoft.com/dotnet/aspnet:7.0${IMAGESUFFIX} AS runtime
ENV ASF_USER asf
@ -62,8 +76,9 @@ EXPOSE 1242
WORKDIR /app
COPY --from=build-dotnet /app/out/result .
RUN groupadd -r -g 1000 asf && \
useradd -r -d /app -g 1000 -u 1000 asf && \
RUN set -eu; \
groupadd -r -g 1000 asf; \
useradd -r -d /app -g 1000 -u 1000 asf; \
chown -hR asf:asf /app
VOLUME ["/app/config", "/app/logs"]

View file

@ -4,9 +4,10 @@ FROM --platform=$BUILDPLATFORM node:lts${IMAGESUFFIX} AS build-node
WORKDIR /app/ASF-ui
COPY ASF-ui .
COPY .git/modules/ASF-ui /app/.git/modules/ASF-ui
RUN echo "node: $(node --version)" && \
echo "npm: $(npm --version)" && \
npm ci --no-progress && \
RUN set -eu; \
echo "node: $(node --version)"; \
echo "npm: $(npm --version)"; \
npm ci --no-progress; \
npm run deploy --no-progress
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:7.0${IMAGESUFFIX} AS build-dotnet
@ -17,30 +18,43 @@ ARG TARGETOS
ENV DOTNET_CLI_TELEMETRY_OPTOUT true
ENV DOTNET_NOLOGO true
ENV NET_CORE_VERSION net7.0
ENV PLUGINS ArchiSteamFarm.OfficialPlugins.ItemsMatcher
ENV STEAM_TOKEN_DUMPER_NAME ArchiSteamFarm.OfficialPlugins.SteamTokenDumper
WORKDIR /app
COPY --from=build-node /app/ASF-ui/dist ASF-ui/dist
COPY ArchiSteamFarm ArchiSteamFarm
COPY ArchiSteamFarm.OfficialPlugins.ItemsMatcher ArchiSteamFarm.OfficialPlugins.ItemsMatcher
COPY ArchiSteamFarm.OfficialPlugins.SteamTokenDumper ArchiSteamFarm.OfficialPlugins.SteamTokenDumper
COPY resources resources
COPY .editorconfig .editorconfig
COPY Directory.Build.props Directory.Build.props
COPY Directory.Packages.props Directory.Packages.props
COPY LICENSE.txt LICENSE.txt
RUN dotnet --info && \
RUN set -eu; \
dotnet --info; \
\
case "$TARGETOS" in \
"linux") ;; \
*) echo "ERROR: Unsupported OS: ${TARGETOS}"; exit 1 ;; \
esac && \
esac; \
\
case "$TARGETARCH" in \
"amd64") asf_variant="${TARGETOS}-x64" ;; \
"arm") asf_variant="${TARGETOS}-${TARGETARCH}" ;; \
"arm64") asf_variant="${TARGETOS}-${TARGETARCH}" ;; \
*) echo "ERROR: Unsupported CPU architecture: ${TARGETARCH}"; exit 1 ;; \
esac && \
if [ -n "${STEAM_TOKEN_DUMPER_TOKEN-}" ] && [ -f "${STEAM_TOKEN_DUMPER_NAME}/SharedInfo.cs" ]; then sed -i "s/STEAM_TOKEN_DUMPER_TOKEN/${STEAM_TOKEN_DUMPER_TOKEN}/g" "${STEAM_TOKEN_DUMPER_NAME}/SharedInfo.cs"; dotnet publish "${STEAM_TOKEN_DUMPER_NAME}" -c "$CONFIGURATION" -f "$NET_CORE_VERSION" -o "out/${STEAM_TOKEN_DUMPER_NAME}/${NET_CORE_VERSION}" "-p:ASFVariant=${asf_variant}" -p:ContinuousIntegrationBuild=true -p:UseAppHost=false -r "$asf_variant" --nologo --no-self-contained; fi && \
dotnet publish ArchiSteamFarm -c "$CONFIGURATION" -f "$NET_CORE_VERSION" -o "out/result" "-p:ASFVariant=${asf_variant}" -p:ContinuousIntegrationBuild=true -p:PublishSingleFile=true -p:PublishTrimmed=true -r "$asf_variant" --nologo --self-contained && \
if [ -d "out/${STEAM_TOKEN_DUMPER_NAME}/${NET_CORE_VERSION}" ]; then mkdir -p "out/result/plugins/${STEAM_TOKEN_DUMPER_NAME}"; cp -pR "out/${STEAM_TOKEN_DUMPER_NAME}/${NET_CORE_VERSION}/"* "out/result/plugins/${STEAM_TOKEN_DUMPER_NAME}"; fi
esac; \
\
dotnet publish ArchiSteamFarm -c "$CONFIGURATION" -f "$NET_CORE_VERSION" -o "out/result" "-p:ASFVariant=${asf_variant}" -p:ContinuousIntegrationBuild=true -p:PublishSingleFile=true -p:PublishTrimmed=true -r "$asf_variant" --nologo --self-contained; \
\
if [ -n "${STEAM_TOKEN_DUMPER_TOKEN-}" ] && [ -f "${STEAM_TOKEN_DUMPER_NAME}/SharedInfo.cs" ]; then \
sed -i "s/STEAM_TOKEN_DUMPER_TOKEN/${STEAM_TOKEN_DUMPER_TOKEN}/g" "${STEAM_TOKEN_DUMPER_NAME}/SharedInfo.cs"; \
dotnet publish "${STEAM_TOKEN_DUMPER_NAME}" -c "$CONFIGURATION" -f "$NET_CORE_VERSION" -o "out/result/plugins/${STEAM_TOKEN_DUMPER_NAME}" "-p:ASFVariant=${asf_variant}" -p:ContinuousIntegrationBuild=true -p:UseAppHost=false -r "$asf_variant" --nologo --no-self-contained; \
fi; \
\
for plugin in $PLUGINS; do \
dotnet publish "$plugin" -c "$CONFIGURATION" -f "$NET_CORE_VERSION" -o "out/result/plugins/$plugin" "-p:ASFVariant=${asf_variant}" -p:ContinuousIntegrationBuild=true -p:UseAppHost=false -r "$asf_variant" --nologo --no-self-contained; \
done
FROM --platform=$TARGETPLATFORM mcr.microsoft.com/dotnet/runtime-deps:7.0${IMAGESUFFIX} AS runtime
ENV ASF_USER asf
@ -62,8 +76,9 @@ EXPOSE 1242
WORKDIR /app
COPY --from=build-dotnet /app/out/result .
RUN groupadd -r -g 1000 asf && \
useradd -r -d /app -g 1000 -u 1000 asf && \
RUN set -eu; \
groupadd -r -g 1000 asf; \
useradd -r -d /app -g 1000 -u 1000 asf; \
chown -hR asf:asf /app
VOLUME ["/app/config", "/app/logs"]

17
cc.sh
View file

@ -11,6 +11,7 @@ CONFIGURATION="Release"
OUT="out"
OUT_ASF="${OUT}/result"
OUT_STD="${OUT}/${STEAM_TOKEN_DUMPER_NAME}"
PLUGINS="${MAIN_PROJECT}.OfficialPlugins.ItemsMatcher"
ANALYSIS=1
ASF_UI=1
@ -132,6 +133,8 @@ if [ "$TEST" -eq 1 ]; then
dotnet test "$TESTS_PROJECT" $DOTNET_FLAGS
fi
echo "INFO: Building ${MAIN_PROJECT}..."
dotnet publish "$MAIN_PROJECT" -o "$OUT_ASF" $DOTNET_FLAGS $PUBLISH_FLAGS
if [ -n "${STEAM_TOKEN_DUMPER_TOKEN-}" ] && [ -f "${STEAM_TOKEN_DUMPER_NAME}/SharedInfo.cs" ] && command -v git >/dev/null; then
@ -139,15 +142,19 @@ if [ -n "${STEAM_TOKEN_DUMPER_TOKEN-}" ] && [ -f "${STEAM_TOKEN_DUMPER_NAME}/Sha
sed "s/STEAM_TOKEN_DUMPER_TOKEN/${STEAM_TOKEN_DUMPER_TOKEN}/g" "${STEAM_TOKEN_DUMPER_NAME}/SharedInfo.cs" > "${STEAM_TOKEN_DUMPER_NAME}/SharedInfo.cs.new";
mv "${STEAM_TOKEN_DUMPER_NAME}/SharedInfo.cs.new" "${STEAM_TOKEN_DUMPER_NAME}/SharedInfo.cs"
dotnet publish "$STEAM_TOKEN_DUMPER_NAME" -o "$OUT_STD" $DOTNET_FLAGS $PUBLISH_FLAGS
git checkout -- "${STEAM_TOKEN_DUMPER_NAME}/SharedInfo.cs"
echo "INFO: Building ${STEAM_TOKEN_DUMPER_NAME}..."
rm -rf "${OUT_ASF}/plugins/${STEAM_TOKEN_DUMPER_NAME}"
mkdir -p "${OUT_ASF}/plugins/${STEAM_TOKEN_DUMPER_NAME}"
cp -pR "${OUT_STD}/"* "${OUT_ASF}/plugins/${STEAM_TOKEN_DUMPER_NAME}"
dotnet publish "$STEAM_TOKEN_DUMPER_NAME" -o "${OUT_ASF}/plugins/${STEAM_TOKEN_DUMPER_NAME}" $DOTNET_FLAGS $PUBLISH_FLAGS
git checkout -- "${STEAM_TOKEN_DUMPER_NAME}/SharedInfo.cs"
else
echo "WARNING: ${STEAM_TOKEN_DUMPER_NAME} dependencies are missing, skipping build of ${STEAM_TOKEN_DUMPER_NAME}..."
fi
for plugin in $PLUGINS; do
echo "INFO: Building ${plugin}..."
dotnet publish "$plugin" -o "${OUT_ASF}/plugins/${plugin}" $DOTNET_FLAGS $PUBLISH_FLAGS
done
echo
echo "SUCCESS: Compilation finished successfully! :)"