mirror of
https://github.com/JustArchiNET/ArchiSteamFarm.git
synced 2025-12-28 12:10:47 +00:00
Compare commits
93 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
661786adf2 | ||
|
|
6c63b6db68 | ||
|
|
dfdb0a22a0 | ||
|
|
41ecfb1d02 | ||
|
|
25b29cd56e | ||
|
|
ab5fb6dd1b | ||
|
|
75e4557da3 | ||
|
|
95c3658197 | ||
|
|
26e7f7deb5 | ||
|
|
f003dcda0b | ||
|
|
296318060f | ||
|
|
f80b114892 | ||
|
|
604652c03d | ||
|
|
2420d34b22 | ||
|
|
21a5793c45 | ||
|
|
888b45c919 | ||
|
|
35bf243f1a | ||
|
|
f4c1dededc | ||
|
|
d5f355a2bc | ||
|
|
eb9b5dd025 | ||
|
|
2cec35f911 | ||
|
|
7d4438b089 | ||
|
|
2782329549 | ||
|
|
14ac124e0c | ||
|
|
ec07d23cc4 | ||
|
|
3299ba8c10 | ||
|
|
4eb09f950d | ||
|
|
144a1d1574 | ||
|
|
05f3aada38 | ||
|
|
9240500e2c | ||
|
|
98768970a7 | ||
|
|
7e41e530e7 | ||
|
|
97198fd435 | ||
|
|
e8b83b8ad4 | ||
|
|
929a1dfd82 | ||
|
|
ea7bad2868 | ||
|
|
71f54a79f3 | ||
|
|
0cd7b10c9a | ||
|
|
42fb71f856 | ||
|
|
7b3ae25d58 | ||
|
|
2961975f05 | ||
|
|
06843ebf9f | ||
|
|
6ca395795c | ||
|
|
0e5490cc3a | ||
|
|
39cc6e6ea6 | ||
|
|
70544d1d76 | ||
|
|
b1e9a53adc | ||
|
|
72e59e7271 | ||
|
|
1ae3517374 | ||
|
|
975b3b89f3 | ||
|
|
a982520844 | ||
|
|
0ed4c7536a | ||
|
|
6e360b7e1a | ||
|
|
17c79904e4 | ||
|
|
0fcbc8c402 | ||
|
|
9e0d44bee2 | ||
|
|
a093f24a9d | ||
|
|
e6a51bae55 | ||
|
|
a992d0c3cd | ||
|
|
a0e5ae8f46 | ||
|
|
c4a46fbdde | ||
|
|
d899dbc18c | ||
|
|
04e14293ef | ||
|
|
18a1b0a883 | ||
|
|
5ca028ef47 | ||
|
|
43ec8f9566 | ||
|
|
32f5b3a1c5 | ||
|
|
196afbf276 | ||
|
|
9825f007c0 | ||
|
|
bc38ba478d | ||
|
|
2f22757fea | ||
|
|
3ff0468926 | ||
|
|
a6a973468c | ||
|
|
4aa1604dfb | ||
|
|
44c7fcd131 | ||
|
|
ce610ab24d | ||
|
|
c76f17c5c7 | ||
|
|
30b4e006dc | ||
|
|
39621ed46e | ||
|
|
e532b57369 | ||
|
|
b117c5164d | ||
|
|
be5a6bc27a | ||
|
|
0ecb04e62c | ||
|
|
0af9f99923 | ||
|
|
cd0078e83e | ||
|
|
10cedad0ee | ||
|
|
693f4edbe5 | ||
|
|
ed44ad030e | ||
|
|
d338477e5c | ||
|
|
053cb5fc03 | ||
|
|
a01ac6641e | ||
|
|
3deb560e5e | ||
|
|
1861add350 |
@@ -22,7 +22,6 @@ ArchiSteamFarm/logs
|
||||
# /_/ \_\____/|_| |____/ \___/ \___|_|\_\___|_|
|
||||
|
||||
# Additional folders that aren't used during image building:
|
||||
|
||||
**/.git*
|
||||
**/[Bb]in/
|
||||
**/[Oo]bj/
|
||||
@@ -31,6 +30,10 @@ ArchiSteamFarm.CustomPlugins.*
|
||||
ASF-ui/dist
|
||||
wiki
|
||||
|
||||
# Add exception for .git used in ASF-ui, it's used for calculating commit hash during build
|
||||
!.git/modules/ASF-ui
|
||||
!ASF-ui/.git
|
||||
|
||||
# _ _
|
||||
# | | (_) _ __ _ _ __ __
|
||||
# | | | || '_ \ | | | |\ \/ /
|
||||
|
||||
6
.github/renovate.json5
vendored
6
.github/renovate.json5
vendored
@@ -19,12 +19,6 @@
|
||||
"allowedVersions": "<= 3.1",
|
||||
"matchManagers": [ "nuget" ],
|
||||
"matchPackageNames": [ "Microsoft.Extensions.Configuration.Json", "Microsoft.Extensions.Logging.Configuration" ]
|
||||
},
|
||||
{
|
||||
// TODO: https://github.com/AngleSharp/AngleSharp.XPath/issues/36
|
||||
"allowedVersions": "< 2.0",
|
||||
"matchManagers": [ "nuget" ],
|
||||
"matchPackageNames": [ "AngleSharp.XPath" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
|
||||
- name: Upload latest strings for translation on Crowdin
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && matrix.configuration == 'Release' && startsWith(matrix.os, 'ubuntu-') }}
|
||||
uses: crowdin/github-action@1.4.9
|
||||
uses: crowdin/github-action@1.4.10
|
||||
with:
|
||||
crowdin_branch_name: main
|
||||
config: '.github/crowdin.yml'
|
||||
|
||||
2
.github/workflows/docker-ci.yml
vendored
2
.github/workflows/docker-ci.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v2.0.0
|
||||
|
||||
- name: Build ${{ matrix.configuration }} Docker image from ${{ matrix.file }}
|
||||
uses: docker/build-push-action@v3.0.0
|
||||
uses: docker/build-push-action@v3.1.0
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.file }}
|
||||
|
||||
2
.github/workflows/docker-publish-latest.yml
vendored
2
.github/workflows/docker-publish-latest.yml
vendored
@@ -55,7 +55,7 @@ jobs:
|
||||
echo "DH_REPOSITORY=$(echo ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build and publish Docker image from Dockerfile.Service
|
||||
uses: docker/build-push-action@v3.0.0
|
||||
uses: docker/build-push-action@v3.1.0
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.Service
|
||||
|
||||
2
.github/workflows/docker-publish-main.yml
vendored
2
.github/workflows/docker-publish-main.yml
vendored
@@ -55,7 +55,7 @@ jobs:
|
||||
echo "DH_REPOSITORY=$(echo ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build and publish Docker image from Dockerfile
|
||||
uses: docker/build-push-action@v3.0.0
|
||||
uses: docker/build-push-action@v3.1.0
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ env.PLATFORMS }}
|
||||
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
echo "DH_REPOSITORY=$(echo ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build and publish Docker image from Dockerfile
|
||||
uses: docker/build-push-action@v3.0.0
|
||||
uses: docker/build-push-action@v3.1.0
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ env.PLATFORMS }}
|
||||
|
||||
24
.github/workflows/publish.yml
vendored
24
.github/workflows/publish.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
run: dotnet --info
|
||||
|
||||
- name: Setup Node.js with npm
|
||||
uses: actions/setup-node@v3.3.0
|
||||
uses: actions/setup-node@v3.4.1
|
||||
with:
|
||||
check-latest: true
|
||||
node-version: ${{ env.NODE_JS_VERSION }}
|
||||
@@ -133,17 +133,6 @@ 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 any overlay for this variant, copy it to output directory
|
||||
variant_os="$(echo "$1" | cut -d '-' -f 1)"
|
||||
|
||||
if [ -d "ArchiSteamFarm/overlay/${variant_os}" ]; then
|
||||
cp -pR "ArchiSteamFarm/overlay/${variant_os}/"* "out/${1}"
|
||||
fi
|
||||
|
||||
if [ "$1" != "$variant_os" ] && [ -d "ArchiSteamFarm/overlay/${1}" ]; then
|
||||
cp -pR "ArchiSteamFarm/overlay/${1}/"* "out/${1}"
|
||||
fi
|
||||
|
||||
# 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}"
|
||||
@@ -254,17 +243,6 @@ jobs:
|
||||
throw "Last command failed."
|
||||
}
|
||||
|
||||
# If we're including any overlay for this variant, copy it to output directory
|
||||
$variant_os = $variant.Split('-', 2)[0];
|
||||
|
||||
if (Test-Path "ArchiSteamFarm\overlay\$variant_os" -PathType Container) {
|
||||
Copy-Item "ArchiSteamFarm\overlay\$variant_os\*" "out\$variant" -Recurse
|
||||
}
|
||||
|
||||
if (($variant -ne $variant_os) -and (Test-Path "ArchiSteamFarm\overlay\$variant" -PathType Container)) {
|
||||
Copy-Item "ArchiSteamFarm\overlay\$variant\*" "out\$variant" -Recurse
|
||||
}
|
||||
|
||||
# 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)) {
|
||||
|
||||
2
.github/workflows/translations.yml
vendored
2
.github/workflows/translations.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
git reset --hard origin/master
|
||||
|
||||
- name: Download latest translations from Crowdin
|
||||
uses: crowdin/github-action@1.4.9
|
||||
uses: crowdin/github-action@1.4.10
|
||||
with:
|
||||
upload_sources: false
|
||||
download_translations: true
|
||||
|
||||
2
ASF-ui
2
ASF-ui
Submodule ASF-ui updated: 99278781c3...60a692f2e0
@@ -20,11 +20,9 @@
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Web;
|
||||
using ArchiSteamFarm.Web.Responses;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin;
|
||||
|
||||
@@ -34,32 +32,13 @@ namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin;
|
||||
internal static class CatAPI {
|
||||
private const string URL = "https://aws.random.cat";
|
||||
|
||||
internal static async Task<string?> GetRandomCatURL(WebBrowser webBrowser) {
|
||||
internal static async Task<Uri?> GetRandomCatURL(WebBrowser webBrowser) {
|
||||
ArgumentNullException.ThrowIfNull(webBrowser);
|
||||
|
||||
Uri request = new($"{URL}/meow");
|
||||
|
||||
ObjectResponse<MeowResponse>? response = await webBrowser.UrlGetToJsonObject<MeowResponse>(request).ConfigureAwait(false);
|
||||
|
||||
if (response?.Content == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(response.Content.Link)) {
|
||||
throw new InvalidOperationException(nameof(response.Content.Link));
|
||||
}
|
||||
|
||||
return Uri.EscapeDataString(response.Content.Link);
|
||||
return response?.Content?.URL;
|
||||
}
|
||||
|
||||
#pragma warning disable CA1812 // False positive, the class is used during json deserialization
|
||||
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
|
||||
private sealed class MeowResponse {
|
||||
[JsonProperty("file", Required = Required.Always)]
|
||||
internal readonly string Link = "";
|
||||
|
||||
[JsonConstructor]
|
||||
private MeowResponse() { }
|
||||
}
|
||||
#pragma warning restore CA1812 // False positive, the class is used during json deserialization
|
||||
}
|
||||
|
||||
@@ -38,15 +38,15 @@ public sealed class CatController : ArchiController {
|
||||
/// Fetches URL of a random cat picture.
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(GenericResponse<string>), (int) HttpStatusCode.OK)]
|
||||
[ProducesResponseType(typeof(GenericResponse<Uri>), (int) HttpStatusCode.OK)]
|
||||
[ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.ServiceUnavailable)]
|
||||
public async Task<ActionResult<GenericResponse>> CatGet() {
|
||||
if (ASF.WebBrowser == null) {
|
||||
throw new InvalidOperationException(nameof(ASF.WebBrowser));
|
||||
}
|
||||
|
||||
string? link = await CatAPI.GetRandomCatURL(ASF.WebBrowser).ConfigureAwait(false);
|
||||
Uri? url = await CatAPI.GetRandomCatURL(ASF.WebBrowser).ConfigureAwait(false);
|
||||
|
||||
return !string.IsNullOrEmpty(link) ? Ok(new GenericResponse<string>(link)) : StatusCode((int) HttpStatusCode.ServiceUnavailable, new GenericResponse(false));
|
||||
return url != null ? Ok(new GenericResponse<Uri>(url)) : StatusCode((int) HttpStatusCode.ServiceUnavailable, new GenericResponse(false));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,9 +92,9 @@ internal sealed class ExamplePlugin : IASF, IBot, IBotCommand2, IBotConnection,
|
||||
case "CAT" when access >= EAccess.FamilySharing:
|
||||
// Notice how we can decide whether to use bot's AWH WebBrowser or ASF's one. For Steam-related requests, AWH's one should always be used, for third-party requests like those it doesn't really matter
|
||||
// Still, it makes sense to pass AWH's one, so in case you get some errors or alike, you know from which bot instance they come from. It's similar to using Bot's ArchiLogger compared to ASF's one
|
||||
string? randomCatURL = await CatAPI.GetRandomCatURL(bot.ArchiWebHandler.WebBrowser).ConfigureAwait(false);
|
||||
Uri? randomCatURL = await CatAPI.GetRandomCatURL(bot.ArchiWebHandler.WebBrowser).ConfigureAwait(false);
|
||||
|
||||
return !string.IsNullOrEmpty(randomCatURL) ? randomCatURL : "God damn it, we're out of cats, care to notify my master? Thanks!";
|
||||
return randomCatURL != null ? randomCatURL.ToString() : "God damn it, we're out of cats, care to notify my master? Thanks!";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
37
ArchiSteamFarm.CustomPlugins.ExamplePlugin/MeowResponse.cs
Normal file
37
ArchiSteamFarm.CustomPlugins.ExamplePlugin/MeowResponse.cs
Normal 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 System.Diagnostics.CodeAnalysis;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin;
|
||||
|
||||
#pragma warning disable CA1812 // False positive, the class is used during json deserialization
|
||||
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
|
||||
internal sealed class MeowResponse {
|
||||
[JsonProperty("file", Required = Required.Always)]
|
||||
internal readonly Uri URL = null!;
|
||||
|
||||
[JsonConstructor]
|
||||
private MeowResponse() { }
|
||||
}
|
||||
#pragma warning restore CA1812 // False positive, the class is used during json deserialization
|
||||
@@ -0,0 +1,180 @@
|
||||
<?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" xmlns="" id="root">
|
||||
<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="PluginDisabledMissingBuildToken" xml:space="preserve">
|
||||
<value>{0} быў адключаны з-за адсутнасці токена зборкі</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
<data name="PluginDisabledInConfig" xml:space="preserve">
|
||||
<value>{0} зараз адключаны ў адпаведнасці з вашай канфігурацыяй. Калі вы хочаце дапамагчы SteamDB у адпраўцы даных, калі ласка, зазірніце ў нашу вікі.</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
<data name="PluginInitializedAndEnabled" xml:space="preserve">
|
||||
<value>{0} быў паспяхова ініцыялізаваны, загадзя дзякуй за дапамогу. Першая адпраўка адбудзецца прыкладна праз {1}.</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin"), {1} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="FileCouldNotBeLoadedFreshInit" xml:space="preserve">
|
||||
<value>Не атрымалася загрузіць {0}, будзе ініцыялізаваны новы асобнік...</value>
|
||||
<comment>{0} will be replaced by the name of the file (e.g. "GlobalCache")</comment>
|
||||
</data>
|
||||
<data name="BotNoAppsToRefresh" xml:space="preserve">
|
||||
<value>На гэтым асобніку бота няма праграм, якія патрабуюць абнаўлення.</value>
|
||||
</data>
|
||||
<data name="BotRetrievingTotalAppAccessTokens" xml:space="preserve">
|
||||
<value>Атрыманне ў агульнай складанасці {0} токенаў доступу праграмы...</value>
|
||||
<comment>{0} will be replaced by the number (total count) of app access tokens being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingAppAccessTokens" xml:space="preserve">
|
||||
<value>Атрыманне {0} токенаў доступу праграмы...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app access tokens being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingAppAccessTokens" xml:space="preserve">
|
||||
<value>Скончана атрыманне {0} токенаў доступу праграмы.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app access tokens retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalAppAccessTokens" xml:space="preserve">
|
||||
<value>Завершана атрыманне {0} токенаў доступу праграмы.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of app access tokens retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Атрыманне ўсіх сховішч для агульнай колькасці праграм {0}...</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingAppInfos" xml:space="preserve">
|
||||
<value>Атрыманне інфармацыі пра праграму {0}...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingAppInfos" xml:space="preserve">
|
||||
<value>Завершана атрыманне інфармацыі пра праграму {0}.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Атрыманне {0} ключоў сховішча...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Завершана атрыманне {0} ключоў сховішча.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Завершана атрыманне ўсіх ключоў ключоў сховішча для {0} праграм.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
</data>
|
||||
<data name="SubmissionNoNewData" xml:space="preserve">
|
||||
<value>Новых дадзеных для адпраўкі няма, усё актуальна.</value>
|
||||
</data>
|
||||
<data name="SubmissionNoContributorSet" xml:space="preserve">
|
||||
<value>Памылка адпраўкі дадзеных: карэктны SteamID не быў прадстаўлены. Праверце правільнасць налады {0}.</value>
|
||||
<comment>{0} will be replaced by the name of the config property (e.g. "SteamOwnerID") that the user is expected to set</comment>
|
||||
</data>
|
||||
<data name="SubmissionInProgress" xml:space="preserve">
|
||||
<value>Адпраўка агульнай колькасці зарэгістраваных праграм/пакетаў/сховішч: {0}/{1}/{2}...</value>
|
||||
<comment>{0} will be replaced by the number of app access tokens being submitted, {1} will be replaced by the number of package access tokens being submitted, {2} will be replaced by the number of depot keys being submitted</comment>
|
||||
</data>
|
||||
<data name="SubmissionFailedTooManyRequests" xml:space="preserve">
|
||||
<value>Адпраўка не ўдалася з-за занадта вялікай колькасці адпраўленых запытаў. Мы паўтарым спробу прыкладна праз {0}.</value>
|
||||
<comment>{0} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessful" xml:space="preserve">
|
||||
<value>Дадзеныя былі паспяхова адпраўлены. Сервер зарэгістраваў агульную колькасць новых праграм/пакетаў/сховішч: {0} ({1} праверана)/{2} ({3} праверана)/{4} ({5} праверана).</value>
|
||||
<comment>{0} will be replaced by the number of new app access tokens that the server has registered, {1} will be replaced by the number of verified app access tokens that the server has registered, {2} will be replaced by the number of new package access tokens that the server has registered, {3} will be replaced by the number of verified package access tokens that the server has registered, {4} will be replaced by the number of new depot keys that the server has registered, {5} will be replaced by the number of verified depot keys that the server has registered</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulNewApps" xml:space="preserve">
|
||||
<value>Новыя праграмы: {0}</value>
|
||||
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulVerifiedApps" xml:space="preserve">
|
||||
<value>Правераныя праграмы: {0}</value>
|
||||
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulNewPackages" xml:space="preserve">
|
||||
<value>Новыя пакеты: {0}</value>
|
||||
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulVerifiedPackages" xml:space="preserve">
|
||||
<value>Правераныя пакеты: {0}</value>
|
||||
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulNewDepots" xml:space="preserve">
|
||||
<value>Новыя сховішчы: {0}</value>
|
||||
<comment>{0} will be replaced by list of the depots (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulVerifiedDepots" xml:space="preserve">
|
||||
<value>Правераныя сховішчы: {0}</value>
|
||||
<comment>{0} will be replaced by list of the depots (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="PluginSecretListInitialized" xml:space="preserve">
|
||||
<value>{0} ініцыялізаваны, убудова не працуе з ніводным з наступных: {1}.</value>
|
||||
<comment>{0} will be replaced by the name of the config property (e.g. "SecretPackageIDs"), {1} will be replaced by list of the objects (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="LoadingGlobalCache" xml:space="preserve">
|
||||
<value>Загрузка глабальнага кэша STD...</value>
|
||||
</data>
|
||||
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
|
||||
<value>Праверка цэласнасці глабальнага кэша STD...</value>
|
||||
</data>
|
||||
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
|
||||
<value>Не ўдалося праверыць цэласнасць глабальнага кэша STD. Гэта сведчыць аб магчымым пашкоджанні файла/памяці, замест гэтага будзе ініцыялізаваны новы асобнік.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -62,34 +62,119 @@
|
||||
PublicKeyToken=b77a5c561934e089
|
||||
</value>
|
||||
</resheader>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<data name="PluginDisabledMissingBuildToken" xml:space="preserve">
|
||||
<value>{0} е била деактивирана поради липсващ токън за изграждане</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
<data name="PluginDisabledInConfig" xml:space="preserve">
|
||||
<value>{0} в момента е деактивирана според вашата настройка. Ако искате да помогнете на SteamDB при подаването на данни, моля разгледайте нашата уикипедия.</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
<data name="PluginInitializedAndEnabled" xml:space="preserve">
|
||||
<value>{0} е инициализиранa успешно, благодаря Ви предварително за вашата помощ. Първото подаване на данни ще се случи приблизително след {1}.</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin"), {1} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="FileCouldNotBeLoadedFreshInit" xml:space="preserve">
|
||||
<value>{0} не беше успешно заредена, вместо това ще бъде стартирана нова инстанция...</value>
|
||||
<comment>{0} will be replaced by the name of the file (e.g. "GlobalCache")</comment>
|
||||
</data>
|
||||
<data name="BotNoAppsToRefresh" xml:space="preserve">
|
||||
<value>Няма нови игри или приложения, които да изискват презареждане на бота.</value>
|
||||
</data>
|
||||
<data name="BotRetrievingTotalAppAccessTokens" xml:space="preserve">
|
||||
<value>Събиране на общо {0} входящи токени за игри или приложения...</value>
|
||||
<comment>{0} will be replaced by the number (total count) of app access tokens being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingAppAccessTokens" xml:space="preserve">
|
||||
<value>Събиране на {0} входящи токени за игри или приложения...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app access tokens being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingAppAccessTokens" xml:space="preserve">
|
||||
<value>Приключи събирането на {0} входящи токени за игри или приложения.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app access tokens retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalAppAccessTokens" xml:space="preserve">
|
||||
<value>Приключи събиране на общо {0} входящи токени за игри или приложения.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of app access tokens retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Събиране на всички депа за общо {0} игри или проложения...</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingAppInfos" xml:space="preserve">
|
||||
<value>Събиране на {0} информация за играта или приложението...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingAppInfos" xml:space="preserve">
|
||||
<value>Приключи събирането на {0} информация за играта или приложението.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Събиране {0} ключове за депо...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Приключи събирането {0} ключове за депо.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Приключи събирането на всички ключове за депа за общо {0} игри или проложения.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
</data>
|
||||
<data name="SubmissionNoNewData" xml:space="preserve">
|
||||
<value>Няма нова информация за подаване, цялата информация е актуална.</value>
|
||||
</data>
|
||||
<data name="SubmissionNoContributorSet" xml:space="preserve">
|
||||
<value>Подаването на данните не може да се изпълни, защото няма валиден SteamID зададен като автор или сътрудник. Помислете да настроите {0} правилно.</value>
|
||||
<comment>{0} will be replaced by the name of the config property (e.g. "SteamOwnerID") that the user is expected to set</comment>
|
||||
</data>
|
||||
<data name="SubmissionInProgress" xml:space="preserve">
|
||||
<value>Подаване на общо регистрирани игри/приложения/пакети/депа: {0}/{1}/{2}...</value>
|
||||
<comment>{0} will be replaced by the number of app access tokens being submitted, {1} will be replaced by the number of package access tokens being submitted, {2} will be replaced by the number of depot keys being submitted</comment>
|
||||
</data>
|
||||
<data name="SubmissionFailedTooManyRequests" xml:space="preserve">
|
||||
<value>Подаването е неуспешно поради твърде много изпратени заявки, ще опитаме отново приблизително след {0}.</value>
|
||||
<comment>{0} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessful" xml:space="preserve">
|
||||
<value>Данните са изпратени успешно. Сървърът регистрира общо нови игри/приложения/пакети/депа: {0} ({1} потвърдени)/{2} ({3} потвърдени)/{4} ({5} потвърдени).</value>
|
||||
<comment>{0} will be replaced by the number of new app access tokens that the server has registered, {1} will be replaced by the number of verified app access tokens that the server has registered, {2} will be replaced by the number of new package access tokens that the server has registered, {3} will be replaced by the number of verified package access tokens that the server has registered, {4} will be replaced by the number of new depot keys that the server has registered, {5} will be replaced by the number of verified depot keys that the server has registered</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulNewApps" xml:space="preserve">
|
||||
<value>Нови игри / приложения: {0}</value>
|
||||
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulVerifiedApps" xml:space="preserve">
|
||||
<value>Потвърдени игри / приложения: {0}</value>
|
||||
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulNewPackages" xml:space="preserve">
|
||||
<value>Нови пакети: {0}</value>
|
||||
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulVerifiedPackages" xml:space="preserve">
|
||||
<value>Потвърдени пакети: {0}</value>
|
||||
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulNewDepots" xml:space="preserve">
|
||||
<value>Нови депа: {0}</value>
|
||||
<comment>{0} will be replaced by list of the depots (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulVerifiedDepots" xml:space="preserve">
|
||||
<value>Потвърдени депа: {0}</value>
|
||||
<comment>{0} will be replaced by list of the depots (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="PluginSecretListInitialized" xml:space="preserve">
|
||||
<value>{0} стартирана, плъгинът няма да разреши нито една от тези: {1}.</value>
|
||||
<comment>{0} will be replaced by the name of the config property (e.g. "SecretPackageIDs"), {1} will be replaced by list of the objects (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="LoadingGlobalCache" xml:space="preserve">
|
||||
<value>Зареждане на STD общ кеш...</value>
|
||||
</data>
|
||||
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
|
||||
<value>Потвърждаване на цялостта на STD общия кеш...</value>
|
||||
</data>
|
||||
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
|
||||
<value>Не успя да провери целостта на глобалния STD кеш. Това предполага потенциална грешка, вместо това ще бъде стартирана нова инстанция.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -88,9 +88,15 @@
|
||||
|
||||
|
||||
|
||||
<data name="SubmissionSuccessfulNewApps" xml:space="preserve">
|
||||
<value>Új appok: {0}</value>
|
||||
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
<data name="SubmissionSuccessfulNewPackages" xml:space="preserve">
|
||||
<value>Új csomagok: {0}</value>
|
||||
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -78,4 +78,20 @@
|
||||
<Link>www\%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="Exists($([System.IO.Path]::Combine('overlay', 'variant-base', $(ASFVariant.Split('-')[0]))))">
|
||||
<Content Include="overlay/variant-base/$(ASFVariant.Split('-')[0])/**/*.*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
<Link>%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="Exists($([System.IO.Path]::Combine('overlay', 'variant-specific', $(ASFVariant))))">
|
||||
<Content Include="overlay/variant-specific/$(ASFVariant)/**/*.*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
<Link>%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -31,7 +31,6 @@ using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Helpers;
|
||||
@@ -418,34 +417,18 @@ public static class ASF {
|
||||
}
|
||||
|
||||
private static async Task InitRateLimiters() {
|
||||
if (GlobalConfig == null) {
|
||||
throw new InvalidOperationException(nameof(GlobalConfig));
|
||||
}
|
||||
|
||||
// The only purpose of using hashing here is to cut on a potential size of the resource name - paths can be really long, and we almost certainly have some upper limit on the resource name we can allocate
|
||||
// At the same time it'd be the best if we avoided all special characters, such as '/' found e.g. in base64, as we can't be sure that it's not a prohibited character in regards to native OS implementation
|
||||
// Because of that, SHA256 is sufficient for our case, as it generates alphanumeric characters only, and is barely 256-bit long. We don't need any kind of complex cryptography or collision detection here, any hashing will do, and the shorter the better
|
||||
string networkGroupText = "";
|
||||
|
||||
if (!string.IsNullOrEmpty(Program.NetworkGroup)) {
|
||||
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
|
||||
networkGroupText = $"-{Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(Program.NetworkGroup!)))}";
|
||||
} else if (!string.IsNullOrEmpty(GlobalConfig.WebProxyText)) {
|
||||
networkGroupText = $"-{Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(GlobalConfig.WebProxyText!)))}";
|
||||
}
|
||||
|
||||
ConfirmationsSemaphore ??= await PluginsCore.GetCrossProcessSemaphore($"{nameof(ConfirmationsSemaphore)}{networkGroupText}").ConfigureAwait(false);
|
||||
GiftsSemaphore ??= await PluginsCore.GetCrossProcessSemaphore($"{nameof(GiftsSemaphore)}{networkGroupText}").ConfigureAwait(false);
|
||||
InventorySemaphore ??= await PluginsCore.GetCrossProcessSemaphore($"{nameof(InventorySemaphore)}{networkGroupText}").ConfigureAwait(false);
|
||||
LoginRateLimitingSemaphore ??= await PluginsCore.GetCrossProcessSemaphore($"{nameof(LoginRateLimitingSemaphore)}{networkGroupText}").ConfigureAwait(false);
|
||||
LoginSemaphore ??= await PluginsCore.GetCrossProcessSemaphore($"{nameof(LoginSemaphore)}{networkGroupText}").ConfigureAwait(false);
|
||||
RateLimitingSemaphore ??= await PluginsCore.GetCrossProcessSemaphore($"{nameof(RateLimitingSemaphore)}{networkGroupText}").ConfigureAwait(false);
|
||||
ConfirmationsSemaphore ??= await PluginsCore.GetCrossProcessSemaphore(nameof(ConfirmationsSemaphore)).ConfigureAwait(false);
|
||||
GiftsSemaphore ??= await PluginsCore.GetCrossProcessSemaphore(nameof(GiftsSemaphore)).ConfigureAwait(false);
|
||||
InventorySemaphore ??= await PluginsCore.GetCrossProcessSemaphore(nameof(InventorySemaphore)).ConfigureAwait(false);
|
||||
LoginRateLimitingSemaphore ??= await PluginsCore.GetCrossProcessSemaphore(nameof(LoginRateLimitingSemaphore)).ConfigureAwait(false);
|
||||
LoginSemaphore ??= await PluginsCore.GetCrossProcessSemaphore(nameof(LoginSemaphore)).ConfigureAwait(false);
|
||||
RateLimitingSemaphore ??= await PluginsCore.GetCrossProcessSemaphore(nameof(RateLimitingSemaphore)).ConfigureAwait(false);
|
||||
|
||||
WebLimitingSemaphores ??= new Dictionary<Uri, (ICrossProcessSemaphore RateLimitingSemaphore, SemaphoreSlim OpenConnectionsSemaphore)>(4) {
|
||||
{ ArchiWebHandler.SteamCommunityURL, (await PluginsCore.GetCrossProcessSemaphore($"{nameof(ArchiWebHandler)}{networkGroupText}-{nameof(ArchiWebHandler.SteamCommunityURL)}").ConfigureAwait(false), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) },
|
||||
{ ArchiWebHandler.SteamHelpURL, (await PluginsCore.GetCrossProcessSemaphore($"{nameof(ArchiWebHandler)}{networkGroupText}-{nameof(ArchiWebHandler.SteamHelpURL)}").ConfigureAwait(false), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) },
|
||||
{ ArchiWebHandler.SteamStoreURL, (await PluginsCore.GetCrossProcessSemaphore($"{nameof(ArchiWebHandler)}{networkGroupText}-{nameof(ArchiWebHandler.SteamStoreURL)}").ConfigureAwait(false), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) },
|
||||
{ WebAPI.DefaultBaseAddress, (await PluginsCore.GetCrossProcessSemaphore($"{nameof(ArchiWebHandler)}{networkGroupText}-{nameof(WebAPI)}").ConfigureAwait(false), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) }
|
||||
{ ArchiWebHandler.SteamCommunityURL, (await PluginsCore.GetCrossProcessSemaphore($"{nameof(ArchiWebHandler)}-{nameof(ArchiWebHandler.SteamCommunityURL)}").ConfigureAwait(false), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) },
|
||||
{ ArchiWebHandler.SteamHelpURL, (await PluginsCore.GetCrossProcessSemaphore($"{nameof(ArchiWebHandler)}-{nameof(ArchiWebHandler.SteamHelpURL)}").ConfigureAwait(false), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) },
|
||||
{ ArchiWebHandler.SteamStoreURL, (await PluginsCore.GetCrossProcessSemaphore($"{nameof(ArchiWebHandler)}-{nameof(ArchiWebHandler.SteamStoreURL)}").ConfigureAwait(false), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) },
|
||||
{ WebAPI.DefaultBaseAddress, (await PluginsCore.GetCrossProcessSemaphore($"{nameof(ArchiWebHandler)}-{nameof(WebAPI)}").ConfigureAwait(false), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) }
|
||||
}.ToImmutableDictionary();
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Core;
|
||||
using ArchiSteamFarm.IPC.Responses;
|
||||
using ArchiSteamFarm.Localization;
|
||||
using ArchiSteamFarm.NLog;
|
||||
using ArchiSteamFarm.NLog.Targets;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -43,6 +44,43 @@ namespace ArchiSteamFarm.IPC.Controllers.Api;
|
||||
public sealed class NLogController : ArchiController {
|
||||
private static readonly ConcurrentDictionary<WebSocket, (SemaphoreSlim Semaphore, CancellationToken CancellationToken)> ActiveLogWebSockets = new();
|
||||
|
||||
/// <summary>
|
||||
/// Fetches ASF log file, this works on assumption that the log file is in fact generated, as user could disable it through custom configuration.
|
||||
/// </summary>
|
||||
/// <param name="count">Maximum amount of lines from the log file returned. The respone naturally might have less amount than specified, if you've read whole file already.</param>
|
||||
/// <param name="lastAt">Ending index, used for pagination. Omit it for the first request, then initialize to TotalLines returned, and on every following request subtract count that you've used in the previous request from it until you hit 0 or less, which means you've read whole file already.</param>
|
||||
[HttpGet("File")]
|
||||
[ProducesResponseType(typeof(GenericResponse<GenericResponse<LogResponse>>), (int) HttpStatusCode.OK)]
|
||||
[ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.BadRequest)]
|
||||
[ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.ServiceUnavailable)]
|
||||
public async Task<ActionResult<GenericResponse>> FileGet(int count = 100, int lastAt = 0) {
|
||||
if (count <= 0) {
|
||||
return BadRequest(new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(count))));
|
||||
}
|
||||
|
||||
if (lastAt < 0) {
|
||||
return BadRequest(new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(lastAt))));
|
||||
}
|
||||
|
||||
if (!Logging.LogFileExists) {
|
||||
return BadRequest(new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(SharedInfo.LogFile))));
|
||||
}
|
||||
|
||||
string[]? lines = await Logging.ReadLogFileLines().ConfigureAwait(false);
|
||||
|
||||
if ((lines == null) || (lines.Length == 0)) {
|
||||
return BadRequest(new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(SharedInfo.LogFile))));
|
||||
}
|
||||
|
||||
if ((lastAt == 0) || (lastAt > lines.Length)) {
|
||||
lastAt = lines.Length;
|
||||
}
|
||||
|
||||
int startFrom = Math.Max(lastAt - count, 0);
|
||||
|
||||
return Ok(new GenericResponse<LogResponse>(new LogResponse(lines.Length, lines[startFrom..lastAt])));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches ASF log in realtime.
|
||||
/// </summary>
|
||||
@@ -52,7 +90,7 @@ public sealed class NLogController : ArchiController {
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(IEnumerable<GenericResponse<string>>), (int) HttpStatusCode.OK)]
|
||||
[ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.BadRequest)]
|
||||
public async Task<ActionResult> NLogGet(CancellationToken cancellationToken) {
|
||||
public async Task<ActionResult> Get(CancellationToken cancellationToken) {
|
||||
if (HttpContext == null) {
|
||||
throw new InvalidOperationException(nameof(HttpContext));
|
||||
}
|
||||
|
||||
52
ArchiSteamFarm/IPC/Responses/LogResponse.cs
Normal file
52
ArchiSteamFarm/IPC/Responses/LogResponse.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// |
|
||||
// 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.ComponentModel.DataAnnotations;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ArchiSteamFarm.IPC.Responses;
|
||||
|
||||
public sealed class LogResponse {
|
||||
/// <summary>
|
||||
/// Content of the log file which consists of lines read from it - in chronological order.
|
||||
/// </summary>
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
[Required]
|
||||
public IReadOnlyList<string> Content { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Total number of lines of the log file returned, can be used as an index for future requests.
|
||||
/// </summary>
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
[Required]
|
||||
public int TotalLines { get; private set; }
|
||||
|
||||
internal LogResponse(int totalLines, IReadOnlyList<string> content) {
|
||||
if (totalLines < 0) {
|
||||
throw new ArgumentOutOfRangeException(nameof(totalLines));
|
||||
}
|
||||
|
||||
TotalLines = totalLines;
|
||||
Content = content ?? throw new ArgumentNullException(nameof(content));
|
||||
}
|
||||
}
|
||||
460
ArchiSteamFarm/Localization/Strings.be-BY.resx
Normal file
460
ArchiSteamFarm/Localization/Strings.be-BY.resx
Normal file
@@ -0,0 +1,460 @@
|
||||
<?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" xmlns="" id="root">
|
||||
<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="AcceptingTrade" xml:space="preserve">
|
||||
<value>Прыняцце абмену: {0}</value>
|
||||
<comment>{0} will be replaced by trade number</comment>
|
||||
</data>
|
||||
<data name="AutoUpdateCheckInfo" xml:space="preserve">
|
||||
<value>ASF будзе аўтаматычна правяраць абнаўленні кожныя {0}.</value>
|
||||
<comment>{0} will be replaced by translated TimeSpan string (such as "24 hours")</comment>
|
||||
</data>
|
||||
<data name="Content" xml:space="preserve">
|
||||
<value>Змест:
|
||||
{0}</value>
|
||||
<comment>{0} will be replaced by content string. Please note that this string should include newline for formatting.</comment>
|
||||
</data>
|
||||
<data name="ErrorConfigPropertyInvalid" xml:space="preserve">
|
||||
<value>Сканфігураваная {0} уласцівасць няправільная {1}</value>
|
||||
<comment>{0} will be replaced by name of the configuration property, {1} will be replaced by invalid value</comment>
|
||||
</data>
|
||||
<data name="ErrorEarlyFatalExceptionInfo" xml:space="preserve">
|
||||
<value>ASF V{0} сутыкнуўся з фатальнай памылкай, перш чым модуль рэгістрацыі ядра змог запусціцца!</value>
|
||||
<comment>{0} will be replaced by version number</comment>
|
||||
</data>
|
||||
|
||||
<data name="ErrorExitingWithNonZeroErrorCode" xml:space="preserve">
|
||||
<value>Выхад з ненулявым кодам памылкі!</value>
|
||||
</data>
|
||||
<data name="ErrorFailingRequest" xml:space="preserve">
|
||||
<value>Памылка запыту да: {0}</value>
|
||||
<comment>{0} will be replaced by URL of the request</comment>
|
||||
</data>
|
||||
<data name="ErrorGlobalConfigNotLoaded" xml:space="preserve">
|
||||
<value>Не ўдалося загрузіць глабальную канфігурацыю. Пераканайцеся, што {0} існуе і з'яўляецца сапраўдным! Выконвайце інструкцыю па наладжванні на вікі, калі вы заблыталіся.</value>
|
||||
<comment>{0} will be replaced by file's path</comment>
|
||||
</data>
|
||||
<data name="ErrorIsInvalid" xml:space="preserve">
|
||||
<value>{0} няверны!</value>
|
||||
<comment>{0} will be replaced by object's name</comment>
|
||||
</data>
|
||||
<data name="ErrorNoBotsDefined" xml:space="preserve">
|
||||
<value>Боты не вызначаны. Можа вы забыліся наладзіць ASF? Выконвайце інструкцыю па наладжванні на вікі, калі вы заблыталіся.</value>
|
||||
</data>
|
||||
<data name="ErrorObjectIsNull" xml:space="preserve">
|
||||
<value>{0} мае значэнне нуль!</value>
|
||||
<comment>{0} will be replaced by object's name</comment>
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<data name="Exiting" xml:space="preserve">
|
||||
<value>Выхад...</value>
|
||||
</data>
|
||||
<data name="WarningFailed" xml:space="preserve">
|
||||
<value>Не атрымалася!</value>
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
<data name="LoggingIn" xml:space="preserve">
|
||||
<value>Уваход у {0}...</value>
|
||||
<comment>{0} will be replaced by service's name</comment>
|
||||
</data>
|
||||
<data name="NoBotsAreRunning" xml:space="preserve">
|
||||
<value>Няма працуючых ботаў, выхад...</value>
|
||||
</data>
|
||||
<data name="RefreshingOurSession" xml:space="preserve">
|
||||
<value>Абнаўленне нашай сесіі!</value>
|
||||
</data>
|
||||
<data name="RejectingTrade" xml:space="preserve">
|
||||
<value>Адхіленне абмену: {0}</value>
|
||||
<comment>{0} will be replaced by trade number</comment>
|
||||
</data>
|
||||
<data name="Restarting" xml:space="preserve">
|
||||
<value>Перазапуск...</value>
|
||||
</data>
|
||||
<data name="Starting" xml:space="preserve">
|
||||
<value>Запуск...</value>
|
||||
</data>
|
||||
<data name="Success" xml:space="preserve">
|
||||
<value>Паспяхова завершана!</value>
|
||||
</data>
|
||||
<data name="UnlockingParentalAccount" xml:space="preserve">
|
||||
<value>Разблакіраванне бацькоўскага кантролю...</value>
|
||||
</data>
|
||||
<data name="UpdateCheckingNewVersion" xml:space="preserve">
|
||||
<value>Ідзе праверка наяўнасці новай версіі...</value>
|
||||
</data>
|
||||
<data name="UpdateDownloadingNewVersion" xml:space="preserve">
|
||||
<value>Спампоўка новай версіі: {0} ({1} МБ)... Пакуль чакаеце, падумайце аб ахвяраванні, калі вы шануеце праробленую працу! :)</value>
|
||||
<comment>{0} will be replaced by version string, {1} will be replaced by update size (in megabytes)</comment>
|
||||
</data>
|
||||
<data name="UpdateFinished" xml:space="preserve">
|
||||
<value>Працэс абнаўлення завершаны!</value>
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<data name="Done" xml:space="preserve">
|
||||
<value>Гатова!</value>
|
||||
</data>
|
||||
<data name="GamesToIdle" xml:space="preserve">
|
||||
<value>Усяго ў нас засталося {0} гульняў ({1} карт) для фармавання (засталося ~{2})...</value>
|
||||
<comment>{0} will be replaced by number of games, {1} will be replaced by number of cards, {2} will be replaced by translated TimeSpan string (such as "1 day, 5 hours and 30 minutes")</comment>
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
|
||||
<data name="IdlingStopped" xml:space="preserve">
|
||||
<value>Фармаванне спынена!</value>
|
||||
</data>
|
||||
<data name="IgnoredPermanentPauseEnabled" xml:space="preserve">
|
||||
<value>Ігнараванне гэтага запыту, так як пастаянная паўза ўключана!</value>
|
||||
</data>
|
||||
<data name="NothingToIdle" xml:space="preserve">
|
||||
<value>Няма нічога да фармавання на гэтым акаўнце!</value>
|
||||
</data>
|
||||
<data name="NowIdling" xml:space="preserve">
|
||||
<value>Зараз фарміцца: {0} ({1})</value>
|
||||
<comment>{0} will be replaced by game's ID (number), {1} will be replaced by game's name</comment>
|
||||
</data>
|
||||
<data name="NowIdlingList" xml:space="preserve">
|
||||
<value>Зараз фарміцца: {0}</value>
|
||||
<comment>{0} will be replaced by list of the games (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="PlayingNotAvailable" xml:space="preserve">
|
||||
<value>Зараз запусціць гульню немагчыма, мы паспрабуем яшчэ раз пазней!</value>
|
||||
</data>
|
||||
<data name="StillIdling" xml:space="preserve">
|
||||
<value>Яшчэ фарміцца: {0} ({1})</value>
|
||||
<comment>{0} will be replaced by game's ID (number), {1} will be replaced by game's name</comment>
|
||||
</data>
|
||||
<data name="StillIdlingList" xml:space="preserve">
|
||||
<value>Яшчэ фарміцца: {0}</value>
|
||||
<comment>{0} will be replaced by list of the games (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="StoppedIdling" xml:space="preserve">
|
||||
<value>Спынена фармаванне: {0} ({1})</value>
|
||||
<comment>{0} will be replaced by game's ID (number), {1} will be replaced by game's name</comment>
|
||||
</data>
|
||||
<data name="StoppedIdlingList" xml:space="preserve">
|
||||
<value>Спынена фармаванне: {0}</value>
|
||||
<comment>{0} will be replaced by list of the games (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="UnknownCommand" xml:space="preserve">
|
||||
<value>Невядомая каманда!</value>
|
||||
</data>
|
||||
<data name="WarningCouldNotCheckBadges" xml:space="preserve">
|
||||
<value>Не ўдалося атрымаць інфармацыю аб значках, мы паспрабуем яшчэ раз пазней!</value>
|
||||
</data>
|
||||
<data name="WarningCouldNotCheckCardsStatus" xml:space="preserve">
|
||||
<value>Немагчыма праверыць стан картак для: {0} ({1}), мы паспрабуем яшчэ раз пазней!</value>
|
||||
<comment>{0} will be replaced by game's ID (number), {1} will be replaced by game's name</comment>
|
||||
</data>
|
||||
<data name="BotAcceptingGift" xml:space="preserve">
|
||||
<value>Прыняцце падарунка: {0}...</value>
|
||||
<comment>{0} will be replaced by giftID (number)</comment>
|
||||
</data>
|
||||
<data name="BotAccountLimited" xml:space="preserve">
|
||||
<value>Гэты ўліковы запіс абмежаваны, працэс фармавання недаступны, пакуль абмежаванне не будзе знята!</value>
|
||||
</data>
|
||||
<data name="BotAddLicense" xml:space="preserve">
|
||||
<value>ID: {0} | Статус: {1}</value>
|
||||
<comment>{0} will be replaced by game's ID (number), {1} will be replaced by status string</comment>
|
||||
</data>
|
||||
<data name="BotAddLicenseWithItems" xml:space="preserve">
|
||||
<value>ID: {0} | Статус: {1} | Тавары: {2}</value>
|
||||
<comment>{0} will be replaced by game's ID (number), {1} will be replaced by status string, {2} will be replaced by list of granted IDs (numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="BotAlreadyRunning" xml:space="preserve">
|
||||
<value>Гэты бот ужо працуе!</value>
|
||||
</data>
|
||||
<data name="BotAuthenticatorConverting" xml:space="preserve">
|
||||
<value>Пераўтварэнне .maFile у фармат ASF...</value>
|
||||
</data>
|
||||
<data name="BotAuthenticatorImportFinished" xml:space="preserve">
|
||||
<value>Паспяхова завершаны імпарт мабільнага аўтэнтыфікатара!</value>
|
||||
</data>
|
||||
<data name="BotAuthenticatorToken" xml:space="preserve">
|
||||
<value>2FA Токен: {0}</value>
|
||||
<comment>{0} will be replaced by generated 2FA token (string)</comment>
|
||||
</data>
|
||||
<data name="BotAutomaticIdlingNowPaused" xml:space="preserve">
|
||||
<value>Аўтаматычнае фарменне прыпынена!</value>
|
||||
</data>
|
||||
<data name="BotAutomaticIdlingNowResumed" xml:space="preserve">
|
||||
<value>Аўтаматычнае фарменне адноўлена!</value>
|
||||
</data>
|
||||
<data name="BotAutomaticIdlingPausedAlready" xml:space="preserve">
|
||||
<value>Аўтаматычнае фарменне ўжо прыпынена!</value>
|
||||
</data>
|
||||
<data name="BotAutomaticIdlingResumedAlready" xml:space="preserve">
|
||||
<value>Аўтаматычнае фарменне ўжо адноўлена!</value>
|
||||
</data>
|
||||
<data name="BotConnected" xml:space="preserve">
|
||||
<value>Падключана да Steam!</value>
|
||||
</data>
|
||||
<data name="BotDisconnected" xml:space="preserve">
|
||||
<value>Адключана ад Steam!</value>
|
||||
</data>
|
||||
<data name="BotDisconnecting" xml:space="preserve">
|
||||
<value>Адключэнне...</value>
|
||||
</data>
|
||||
<data name="BotInstanceNotStartingBecauseDisabled" xml:space="preserve">
|
||||
<value>Гэты бот не запушчаны, бо ён адключаны ў канфігурацыйным файле!</value>
|
||||
</data>
|
||||
<data name="BotInvalidAuthenticatorDuringLogin" xml:space="preserve">
|
||||
<value>Памылка TwoFactorCodeMismatch атрымана {0} разоў запар. Альбо вашыя ўліковыя даныя 2FA больш не дзейнічаюць, альбо вашы гадзіны не сінхранізуюцца, спыняю працу!</value>
|
||||
<comment>{0} will be replaced by maximum allowed number of failed 2FA attempts</comment>
|
||||
</data>
|
||||
<data name="BotLoggedOff" xml:space="preserve">
|
||||
<value>Выйшлі з Steam: {0}</value>
|
||||
<comment>{0} will be replaced by logging off reason (string)</comment>
|
||||
</data>
|
||||
<data name="BotLoggedOn" xml:space="preserve">
|
||||
<value>Паспяхова ўвайшлі як {0}.</value>
|
||||
<comment>{0} will be replaced by steam ID (number)</comment>
|
||||
</data>
|
||||
<data name="BotLoggingIn" xml:space="preserve">
|
||||
<value>Уваход...</value>
|
||||
</data>
|
||||
<data name="BotLogonSessionReplaced" xml:space="preserve">
|
||||
<value>Здаецца, што гэты акаўнт выкарыстоўваецца ў іншым экзэмпляры ASF, што з'яўляецца недапушчальным, спыняем працу!</value>
|
||||
</data>
|
||||
<data name="BotLootingFailed" xml:space="preserve">
|
||||
<value>Прапанова абмену не атрымалася!</value>
|
||||
</data>
|
||||
<data name="BotLootingMasterNotDefined" xml:space="preserve">
|
||||
<value>Прапанова абмену не можа быць адпраўлена, бо няма карыстальніка з правамі "master"!</value>
|
||||
</data>
|
||||
<data name="BotLootingSuccess" xml:space="preserve">
|
||||
<value>Прапанова абмену паспяхова адпраўлена!</value>
|
||||
</data>
|
||||
<data name="BotSendingTradeToYourself" xml:space="preserve">
|
||||
<value>Вы не можаце адправіць прапанову абмену да сябе!</value>
|
||||
</data>
|
||||
<data name="BotNoASFAuthenticator" xml:space="preserve">
|
||||
<value>У гэтага бота не ўключаны ASF 2FA! Ці не забыліся вы імпартаваць свой аўтэнтыфікатар да ASF 2FA?</value>
|
||||
</data>
|
||||
<data name="BotNotConnected" xml:space="preserve">
|
||||
<value>Гэты бот не падключаны!</value>
|
||||
</data>
|
||||
<data name="BotNotOwnedYet" xml:space="preserve">
|
||||
<value>Яшчэ не мае: {0}</value>
|
||||
<comment>{0} will be replaced by query (string)</comment>
|
||||
</data>
|
||||
<data name="BotOwnedAlreadyWithName" xml:space="preserve">
|
||||
<value>Ужо мае: {0} | {1}</value>
|
||||
<comment>{0} will be replaced by game's ID (number), {1} will be replaced by game's name</comment>
|
||||
</data>
|
||||
<data name="BotPointsBalance" xml:space="preserve">
|
||||
<value>Баланс ачкоў: {0}</value>
|
||||
<comment>{0} will be replaced by the points balance value (integer)</comment>
|
||||
</data>
|
||||
<data name="BotRateLimitExceeded" xml:space="preserve">
|
||||
<value>Перавышаны ліміт частаты запросаў, мы паўторым спробу пасля {0}...</value>
|
||||
<comment>{0} will be replaced by translated TimeSpan string (such as "25 minutes")</comment>
|
||||
</data>
|
||||
<data name="BotReconnecting" xml:space="preserve">
|
||||
<value>Паўторнае падключэнне...</value>
|
||||
</data>
|
||||
<data name="BotRedeem" xml:space="preserve">
|
||||
<value>Ключ: {0} | Статус: {1}</value>
|
||||
<comment>{0} will be replaced by cd-key (string), {1} will be replaced by status string</comment>
|
||||
</data>
|
||||
<data name="BotRedeemWithItems" xml:space="preserve">
|
||||
<value>Ключ: {0} | Статус: {1} | Тавары: {2}</value>
|
||||
<comment>{0} will be replaced by cd-key (string), {1} will be replaced by status string, {2} will be replaced by list of key-value pairs, separated by a comma</comment>
|
||||
</data>
|
||||
<data name="BotRemovedExpiredLoginKey" xml:space="preserve">
|
||||
<value>Выдалены пратэрмінаваны ключ для ўваходу!</value>
|
||||
</data>
|
||||
<data name="BotStatusNotIdling" xml:space="preserve">
|
||||
<value>Бот нічога не фарміць.</value>
|
||||
</data>
|
||||
<data name="BotStatusLimited" xml:space="preserve">
|
||||
<value>Бот абмежаваны і не можа атрымліваць карткі праз фарменне.</value>
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<data name="BotConnecting" xml:space="preserve">
|
||||
<value>Падключэнне...</value>
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<data name="PluginsWarning" xml:space="preserve">
|
||||
<value>Вы загрузілі адзін або некалькі карыстальніцкіх убудоў у ASF. Паколькі мы не можам прапанаваць падтрымку мадыфікаваных канфігурацый, калі ласка, звярніцеся да адпаведных распрацоўнікаў, убудовы якіх вы вырашылі выкарыстоўваць у выпадку ўзнікнення якіх-небудзь праблем.</value>
|
||||
</data>
|
||||
<data name="PleaseWait" xml:space="preserve">
|
||||
<value>Пачакайце, калі ласка...</value>
|
||||
</data>
|
||||
<data name="EnterCommand" xml:space="preserve">
|
||||
<value>Увядзіце каманду: </value>
|
||||
</data>
|
||||
<data name="Executing" xml:space="preserve">
|
||||
<value>Выкананне...</value>
|
||||
</data>
|
||||
<data name="InteractiveConsoleEnabled" xml:space="preserve">
|
||||
<value>Інтэрактыўная кансоль цяпер актыўная, націсніце 'c', каб увайсці ў рэжым каманд.</value>
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<data name="Result" xml:space="preserve">
|
||||
<value>Рэзультат: {0}</value>
|
||||
<comment>{0} will be replaced by generic result of various functions that use this string</comment>
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<data name="PatchingFiles" xml:space="preserve">
|
||||
<value>Выпраўленне файлаў ASF...</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -194,7 +194,10 @@ StackTrace :
|
||||
<value>Veuillez entrer votre code 2FA généré par votre application d'authentification de Steam : </value>
|
||||
<comment>Please note that this translation should end with space</comment>
|
||||
</data>
|
||||
|
||||
<data name="UserInputSteamGuard" xml:space="preserve">
|
||||
<value>Veuillez entrer le code d’authentification SteamGuard qui a été envoyé sur votre e-mail : </value>
|
||||
<comment>Please note that this translation should end with space</comment>
|
||||
</data>
|
||||
<data name="UserInputSteamLogin" xml:space="preserve">
|
||||
<value>Veuillez entrer votre nom d’utilisateur Steam : </value>
|
||||
<comment>Please note that this translation should end with space</comment>
|
||||
|
||||
@@ -696,7 +696,7 @@ Tempo de execução: {1}</value>
|
||||
<comment>{0} will be replaced by additional details about the password being considered weak</comment>
|
||||
</data>
|
||||
<data name="WarningWeakSteamPassword" xml:space="preserve">
|
||||
<value>A senha da conta Steam "{0}" é muito fraca. Escolha uma senha mais forte para aumentar a segurança. Detalhes: {0}</value>
|
||||
<value>A senha da conta Steam "{0}" é muito fraca. Escolha uma senha mais forte para aumentar a segurança. Detalhes: {1}</value>
|
||||
<comment>{0} will be replaced by the affected bot name, {1} will be replaced by additional details about the password being considered weak</comment>
|
||||
</data>
|
||||
<data name="WarningWeakCryptKey" xml:space="preserve">
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
<comment>{0} will be replaced by object's name</comment>
|
||||
</data>
|
||||
<data name="ErrorNoBotsDefined" xml:space="preserve">
|
||||
<value>Не налаштовано жодного боту. Можливо ви забули налаштувати ASF? Скористуйтеся розділом 'налаштування' у нашій wiki, якщо не знаєте, що робити.</value>
|
||||
<value>Не налаштовано жодного боту. Можливо ви забули налаштувати ASF? Скористуйтеся розділом 'setting up' у нашій wiki, якщо не знаєте, що робити.</value>
|
||||
</data>
|
||||
<data name="ErrorObjectIsNull" xml:space="preserve">
|
||||
<value>{0} має значення null!</value>
|
||||
@@ -162,7 +162,7 @@
|
||||
<comment>{0} will be replaced by trade number</comment>
|
||||
</data>
|
||||
<data name="Restarting" xml:space="preserve">
|
||||
<value>Перезавантаження...</value>
|
||||
<value>Перезапуск...</value>
|
||||
</data>
|
||||
<data name="Starting" xml:space="preserve">
|
||||
<value>Запуск...</value>
|
||||
@@ -187,7 +187,7 @@
|
||||
<value>Доступна нова версія ASF! Ви можете оновитися на неї самостійно!</value>
|
||||
</data>
|
||||
<data name="UpdateVersionInfo" xml:space="preserve">
|
||||
<value>Ваша версія: {0} |Остання версія: {1}</value>
|
||||
<value>Ваша версія: {0} | Остання версія: {1}</value>
|
||||
<comment>{0} will be replaced by current version, {1} will be replaced by remote version</comment>
|
||||
</data>
|
||||
<data name="UserInputSteam2FA" xml:space="preserve">
|
||||
@@ -199,7 +199,7 @@
|
||||
<comment>Please note that this translation should end with space</comment>
|
||||
</data>
|
||||
<data name="UserInputSteamLogin" xml:space="preserve">
|
||||
<value>Будь ласка, введіть свій Steam логін: </value>
|
||||
<value>Будь ласка, введіть свій логін Steam: </value>
|
||||
<comment>Please note that this translation should end with space</comment>
|
||||
</data>
|
||||
<data name="UserInputSteamParentalCode" xml:space="preserve">
|
||||
@@ -207,7 +207,7 @@
|
||||
<comment>Please note that this translation should end with space</comment>
|
||||
</data>
|
||||
<data name="UserInputSteamPassword" xml:space="preserve">
|
||||
<value>Будь ласка, введіть ваш Steam пароль: </value>
|
||||
<value>Будь ласка, введіть ваш пароль Steam: </value>
|
||||
<comment>Please note that this translation should end with space</comment>
|
||||
</data>
|
||||
<data name="WarningUnknownValuePleaseReport" xml:space="preserve">
|
||||
@@ -345,10 +345,18 @@
|
||||
<value>2FA токен: {0}</value>
|
||||
<comment>{0} will be replaced by generated 2FA token (string)</comment>
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
|
||||
<data name="BotAutomaticIdlingNowPaused" xml:space="preserve">
|
||||
<value>Автоматичний фарм призупинено!</value>
|
||||
</data>
|
||||
<data name="BotAutomaticIdlingNowResumed" xml:space="preserve">
|
||||
<value>Автоматичний фарм відновлено!</value>
|
||||
</data>
|
||||
<data name="BotAutomaticIdlingPausedAlready" xml:space="preserve">
|
||||
<value>Автоматичний фарм вже на паузі!</value>
|
||||
</data>
|
||||
<data name="BotAutomaticIdlingResumedAlready" xml:space="preserve">
|
||||
<value>Автоматичний фарм вже відновлено!</value>
|
||||
</data>
|
||||
<data name="BotConnected" xml:space="preserve">
|
||||
<value>Підключено до Steam!</value>
|
||||
</data>
|
||||
@@ -405,7 +413,10 @@
|
||||
<value>Вже має: {0} | {1}</value>
|
||||
<comment>{0} will be replaced by game's ID (number), {1} will be replaced by game's name</comment>
|
||||
</data>
|
||||
|
||||
<data name="BotPointsBalance" xml:space="preserve">
|
||||
<value>Поточний баланс: {0}</value>
|
||||
<comment>{0} will be replaced by the points balance value (integer)</comment>
|
||||
</data>
|
||||
<data name="BotRateLimitExceeded" xml:space="preserve">
|
||||
<value>Перевищений ліміт частоти запросів, ми спробуємо знову через {0} очікування...</value>
|
||||
<comment>{0} will be replaced by translated TimeSpan string (such as "25 minutes")</comment>
|
||||
@@ -424,8 +435,12 @@
|
||||
<data name="BotRemovedExpiredLoginKey" xml:space="preserve">
|
||||
<value>Видалений прострочений ключ авторизації!</value>
|
||||
</data>
|
||||
|
||||
|
||||
<data name="BotStatusNotIdling" xml:space="preserve">
|
||||
<value>Бот нічого не фармить.</value>
|
||||
</data>
|
||||
<data name="BotStatusLimited" xml:space="preserve">
|
||||
<value>Бот обмежений і не може отримувати карти від фарму.</value>
|
||||
</data>
|
||||
<data name="BotStatusConnecting" xml:space="preserve">
|
||||
<value>Бот підключається до мережі Steam.</value>
|
||||
</data>
|
||||
@@ -457,7 +472,9 @@
|
||||
<data name="BotConnectionLost" xml:space="preserve">
|
||||
<value>Підключення до мережі Steam втрачено. Підключаємось повторно...</value>
|
||||
</data>
|
||||
|
||||
<data name="BotAccountFree" xml:space="preserve">
|
||||
<value>Акаунт більше не зайнятий: фарм відновлено!</value>
|
||||
</data>
|
||||
|
||||
<data name="BotConnecting" xml:space="preserve">
|
||||
<value>Підключення...</value>
|
||||
@@ -666,10 +683,22 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<data name="WarningRunningInUnsupportedEnvironment" xml:space="preserve">
|
||||
<value>Ви запускаєте ASF у непідтримуваному середовищі, з допомогою параметру -ignore-unsupported-environment. Зверніть увагу, що ми не надаємо будь-якої допомоги у такій ситуації, і ви робите це цілком на власний ризик. Ми попереджали.</value>
|
||||
</data>
|
||||
<data name="FetchingChecksumFromRemoteServer" xml:space="preserve">
|
||||
<value>Отримуємо контрольну суму з віддаленого сервера...</value>
|
||||
</data>
|
||||
<data name="VerifyingChecksumWithRemoteServer" xml:space="preserve">
|
||||
<value>Перевіряємо контрольну суму завантаженого файлу з отриманою з віддаленого сервера...</value>
|
||||
</data>
|
||||
<data name="ChecksumMissing" xml:space="preserve">
|
||||
<value>Віддалений сервер нічого не знає про версію, на яку ми оновлюємось. Це можливо якщо ця версія нещодавно опублікована - відмовляємось від оновлення в якості додаткового заходу безпеки.</value>
|
||||
</data>
|
||||
<data name="ChecksumWrong" xml:space="preserve">
|
||||
<value>Віддалений сервер відповів з іншою контрольною сумою, це може свідчити про пошкоджене завантаження або атаку MITM, відмовляємось від процедури оновлення!</value>
|
||||
</data>
|
||||
<data name="PatchingFiles" xml:space="preserve">
|
||||
<value>Виправлення файлів ASF...</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
</value>
|
||||
</resheader>
|
||||
<data name="AcceptingTrade" xml:space="preserve">
|
||||
<value>Chấp nhận giao dịch: {0}</value>
|
||||
<value>Chấp nhận trao đổi: {0}</value>
|
||||
<comment>{0} will be replaced by trade number</comment>
|
||||
</data>
|
||||
<data name="AutoUpdateCheckInfo" xml:space="preserve">
|
||||
@@ -144,7 +144,7 @@ StackTrace:
|
||||
<value>Tập tin cấu hình chung đã được gỡ bỏ!</value>
|
||||
</data>
|
||||
<data name="IgnoringTrade" xml:space="preserve">
|
||||
<value>Bỏ qua giao dịch: {0}</value>
|
||||
<value>Bỏ qua trao đổi: {0}</value>
|
||||
<comment>{0} will be replaced by trade number</comment>
|
||||
</data>
|
||||
<data name="LoggingIn" xml:space="preserve">
|
||||
@@ -318,7 +318,7 @@ StackTrace:
|
||||
<comment>{0} will be replaced by game's ID (number), {1} will be replaced by game's name</comment>
|
||||
</data>
|
||||
<data name="BotAcceptingGift" xml:space="preserve">
|
||||
<value>Chấp nhận quà tặng: {0}...</value>
|
||||
<value>Đang chấp nhận quà tặng: {0}...</value>
|
||||
<comment>{0} will be replaced by giftID (number)</comment>
|
||||
</data>
|
||||
<data name="BotAccountLimited" xml:space="preserve">
|
||||
|
||||
@@ -47,6 +47,8 @@ internal static class Logging {
|
||||
private const string GeneralLayout = $@"${{date:format=yyyy-MM-dd HH\:mm\:ss}}|${{processname}}-${{processid}}|${{level:uppercase=true}}|{LayoutMessage}";
|
||||
private const string LayoutMessage = @"${logger}|${message}${onexception:inner= ${exception:format=toString,Data}}";
|
||||
|
||||
internal static bool LogFileExists => File.Exists(SharedInfo.LogFile);
|
||||
|
||||
private static readonly ConcurrentHashSet<LoggingRule> ConsoleLoggingRules = new();
|
||||
private static readonly SemaphoreSlim ConsoleSemaphore = new(1, 1);
|
||||
|
||||
@@ -181,6 +183,11 @@ internal static class Logging {
|
||||
CleanupFileName = false,
|
||||
DeleteOldFileOnStartup = true,
|
||||
FileName = Path.Combine("${currentdir}", SharedInfo.LogFile),
|
||||
|
||||
// Windows OS prevents other apps from reading file when actively holding exclusive (write) lock over it
|
||||
// We require read access for GET /Api/NLog/File ASF API usage, therefore we shouldn't keep the lock all the time
|
||||
KeepFileOpen = !OperatingSystem.IsWindows(),
|
||||
|
||||
Layout = GeneralLayout,
|
||||
MaxArchiveFiles = 10
|
||||
};
|
||||
@@ -235,6 +242,16 @@ internal static class Logging {
|
||||
ArchiKestrel.OnNewHistoryTarget(historyTarget);
|
||||
}
|
||||
|
||||
internal static async Task<string[]?> ReadLogFileLines() {
|
||||
try {
|
||||
return await File.ReadAllLinesAsync(SharedInfo.LogFile).ConfigureAwait(false);
|
||||
} catch (Exception e) {
|
||||
ASF.ArchiLogger.LogGenericException(e);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
internal static void StartInteractiveConsole() {
|
||||
Utilities.InBackground(HandleConsoleInteractively, true);
|
||||
ASF.ArchiLogger.LogGenericInfo(Strings.InteractiveConsoleEnabled);
|
||||
|
||||
@@ -31,6 +31,8 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Core;
|
||||
using ArchiSteamFarm.Helpers;
|
||||
@@ -40,17 +42,57 @@ using ArchiSteamFarm.Steam;
|
||||
using ArchiSteamFarm.Steam.Data;
|
||||
using ArchiSteamFarm.Steam.Exchange;
|
||||
using ArchiSteamFarm.Steam.Integration.Callbacks;
|
||||
using JetBrains.Annotations;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using SteamKit2;
|
||||
|
||||
namespace ArchiSteamFarm.Plugins;
|
||||
|
||||
internal static class PluginsCore {
|
||||
public static class PluginsCore {
|
||||
internal static bool HasCustomPluginsLoaded => ActivePlugins?.Any(static plugin => plugin is not OfficialPlugin officialPlugin || !officialPlugin.HasSameVersion()) == true;
|
||||
|
||||
[ImportMany]
|
||||
internal static ImmutableHashSet<IPlugin>? ActivePlugins { get; private set; }
|
||||
|
||||
[PublicAPI]
|
||||
public static async Task<ICrossProcessSemaphore> GetCrossProcessSemaphore(string objectName) {
|
||||
if (string.IsNullOrEmpty(objectName)) {
|
||||
throw new ArgumentNullException(nameof(objectName));
|
||||
}
|
||||
|
||||
if (ASF.GlobalConfig == null) {
|
||||
throw new InvalidOperationException(nameof(ASF.GlobalConfig));
|
||||
}
|
||||
|
||||
// The only purpose of using hashing here is to cut on a potential size of the resource name - paths can be really long, and we almost certainly have some upper limit on the resource name we can allocate
|
||||
// At the same time it'd be the best if we avoided all special characters, such as '/' found e.g. in base64, as we can't be sure that it's not a prohibited character in regards to native OS implementation
|
||||
// Because of that, SHA256 is sufficient for our case, as it generates alphanumeric characters only, and is barely 256-bit long. We don't need any kind of complex cryptography or collision detection here, any hashing will do, and the shorter the better
|
||||
if (!string.IsNullOrEmpty(Program.NetworkGroup)) {
|
||||
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
|
||||
objectName += $"-{Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(Program.NetworkGroup!)))}";
|
||||
} else if (!string.IsNullOrEmpty(ASF.GlobalConfig.WebProxyText)) {
|
||||
objectName += $"-{Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(ASF.GlobalConfig.WebProxyText!)))}";
|
||||
}
|
||||
|
||||
string resourceName = OS.GetOsResourceName(objectName);
|
||||
|
||||
if ((ActivePlugins == null) || (ActivePlugins.Count == 0)) {
|
||||
return new CrossProcessFileBasedSemaphore(resourceName);
|
||||
}
|
||||
|
||||
IList<ICrossProcessSemaphore?> responses;
|
||||
|
||||
try {
|
||||
responses = await Utilities.InParallel(ActivePlugins.OfType<ICrossProcessSemaphoreProvider>().Select(plugin => plugin.GetCrossProcessSemaphore(resourceName))).ConfigureAwait(false);
|
||||
} catch (Exception e) {
|
||||
ASF.ArchiLogger.LogGenericException(e);
|
||||
|
||||
return new CrossProcessFileBasedSemaphore(resourceName);
|
||||
}
|
||||
|
||||
return responses.FirstOrDefault(static response => response != null) ?? new CrossProcessFileBasedSemaphore(resourceName);
|
||||
}
|
||||
|
||||
internal static async Task<StringComparer> GetBotsComparer() {
|
||||
if (ActivePlugins == null) {
|
||||
return StringComparer.Ordinal;
|
||||
@@ -95,30 +137,6 @@ internal static class PluginsCore {
|
||||
return lastChangeNumber;
|
||||
}
|
||||
|
||||
internal static async Task<ICrossProcessSemaphore> GetCrossProcessSemaphore(string objectName) {
|
||||
if (string.IsNullOrEmpty(objectName)) {
|
||||
throw new ArgumentNullException(nameof(objectName));
|
||||
}
|
||||
|
||||
string resourceName = OS.GetOsResourceName(objectName);
|
||||
|
||||
if ((ActivePlugins == null) || (ActivePlugins.Count == 0)) {
|
||||
return new CrossProcessFileBasedSemaphore(resourceName);
|
||||
}
|
||||
|
||||
IList<ICrossProcessSemaphore?> responses;
|
||||
|
||||
try {
|
||||
responses = await Utilities.InParallel(ActivePlugins.OfType<ICrossProcessSemaphoreProvider>().Select(plugin => plugin.GetCrossProcessSemaphore(resourceName))).ConfigureAwait(false);
|
||||
} catch (Exception e) {
|
||||
ASF.ArchiLogger.LogGenericException(e);
|
||||
|
||||
return new CrossProcessFileBasedSemaphore(resourceName);
|
||||
}
|
||||
|
||||
return responses.FirstOrDefault(static response => response != null) ?? new CrossProcessFileBasedSemaphore(resourceName);
|
||||
}
|
||||
|
||||
internal static async Task<IMachineInfoProvider?> GetCustomMachineInfoProvider() {
|
||||
if (ActivePlugins == null) {
|
||||
return null;
|
||||
|
||||
@@ -52,6 +52,7 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
|
||||
private const string EconService = "IEconService";
|
||||
private const string LoyaltyRewardsService = "ILoyaltyRewardsService";
|
||||
private const byte MinimumSessionValidityInSeconds = 10;
|
||||
private const string SteamAppsService = "ISteamApps";
|
||||
private const string SteamUserAuthService = "ISteamUserAuth";
|
||||
private const string TwoFactorService = "ITwoFactorService";
|
||||
@@ -81,8 +82,8 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
|
||||
private bool Initialized;
|
||||
private DateTime LastSessionCheck;
|
||||
private DateTime LastSessionRefresh;
|
||||
private bool MarkingInventoryScheduled;
|
||||
private DateTime SessionValidUntil;
|
||||
private string? VanityURL;
|
||||
|
||||
internal ArchiWebHandler(Bot bot) {
|
||||
@@ -174,12 +175,12 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
|
||||
response = await UrlGetToJsonObjectWithSession<InventoryResponse>(request, requestOptions: WebBrowser.ERequestOptions.ReturnServerErrors, rateLimitingDelay: rateLimitingDelay).ConfigureAwait(false);
|
||||
|
||||
if (response?.Content == null) {
|
||||
if (response == null) {
|
||||
throw new HttpRequestException(string.Format(CultureInfo.CurrentCulture, Strings.ErrorObjectIsNull, nameof(response)));
|
||||
}
|
||||
|
||||
if (response.StatusCode.IsServerErrorCode()) {
|
||||
if (string.IsNullOrEmpty(response.Content.Error)) {
|
||||
if (string.IsNullOrEmpty(response.Content?.Error)) {
|
||||
// This is a generic server error without a reason, try again
|
||||
response = null;
|
||||
|
||||
@@ -187,7 +188,8 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
}
|
||||
|
||||
// This is actually client error with a reason, so it doesn't make sense to retry
|
||||
throw new HttpRequestException(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, response.Content.Error));
|
||||
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
|
||||
throw new HttpRequestException(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, response.Content!.Error));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
@@ -427,12 +429,12 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
for (byte i = 0; (i < WebBrowser.MaxTries) && (response == null); i++) {
|
||||
response = await UrlPostToJsonObjectWithSession<TradeOfferSendResponse>(request, data: data, referer: referer, requestOptions: WebBrowser.ERequestOptions.ReturnServerErrors).ConfigureAwait(false);
|
||||
|
||||
if (response?.Content == null) {
|
||||
if (response == null) {
|
||||
return (false, mobileTradeOfferIDs);
|
||||
}
|
||||
|
||||
if (response.StatusCode.IsServerErrorCode()) {
|
||||
if (string.IsNullOrEmpty(response.Content.ErrorText)) {
|
||||
if (string.IsNullOrEmpty(response.Content?.ErrorText)) {
|
||||
// This is a generic server error without a reason, try again
|
||||
response = null;
|
||||
|
||||
@@ -440,7 +442,8 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
}
|
||||
|
||||
// This is actually client error with a reason, so it doesn't make sense to retry
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, response.Content.ErrorText));
|
||||
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, response.Content!.ErrorText));
|
||||
|
||||
return (false, mobileTradeOfferIDs);
|
||||
}
|
||||
@@ -465,14 +468,11 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public async Task<HtmlDocumentResponse?> UrlGetToHtmlDocumentWithSession(Uri request, IReadOnlyCollection<KeyValuePair<string, string>>? headers = null, Uri? referer = null, WebBrowser.ERequestOptions requestOptions = WebBrowser.ERequestOptions.None, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries, int rateLimitingDelay = 0) {
|
||||
public async Task<HtmlDocumentResponse?> UrlGetToHtmlDocumentWithSession(Uri request, IReadOnlyCollection<KeyValuePair<string, string>>? headers = null, Uri? referer = null, WebBrowser.ERequestOptions requestOptions = WebBrowser.ERequestOptions.None, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries, int rateLimitingDelay = 0, bool allowSessionRefresh = true) {
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
if (maxTries == 0) {
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries));
|
||||
Bot.ArchiLogger.LogGenericDebug(string.Format(CultureInfo.CurrentCulture, Strings.ErrorFailingRequest, request));
|
||||
|
||||
return null;
|
||||
throw new ArgumentOutOfRangeException(nameof(maxTries));
|
||||
}
|
||||
|
||||
if (rateLimitingDelay < 0) {
|
||||
@@ -488,8 +488,8 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
bool? sessionExpired = await IsSessionExpired().ConfigureAwait(false);
|
||||
|
||||
if (sessionExpired.GetValueOrDefault(true)) {
|
||||
if (await RefreshSession().ConfigureAwait(false)) {
|
||||
return await UrlGetToHtmlDocumentWithSession(request, headers, referer, requestOptions, true, --maxTries, rateLimitingDelay).ConfigureAwait(false);
|
||||
if (allowSessionRefresh && await RefreshSession().ConfigureAwait(false)) {
|
||||
return await UrlGetToHtmlDocumentWithSession(request, headers, referer, requestOptions, true, maxTries, rateLimitingDelay, false).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
||||
@@ -520,15 +520,16 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
|
||||
Uri host = new(request.GetLeftPart(UriPartial.Authority));
|
||||
|
||||
HtmlDocumentResponse? response = await WebLimitRequest(host, async () => await WebBrowser.UrlGetToHtmlDocument(request, headers, referer, requestOptions, rateLimitingDelay: rateLimitingDelay).ConfigureAwait(false)).ConfigureAwait(false);
|
||||
// ReSharper disable once AccessToModifiedClosure - evaluated fully before returning
|
||||
HtmlDocumentResponse? response = await WebLimitRequest(host, async () => await WebBrowser.UrlGetToHtmlDocument(request, headers, referer, requestOptions, maxTries, rateLimitingDelay).ConfigureAwait(false)).ConfigureAwait(false);
|
||||
|
||||
if (response == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (IsSessionExpiredUri(response.FinalUri)) {
|
||||
if (await RefreshSession().ConfigureAwait(false)) {
|
||||
return await UrlGetToHtmlDocumentWithSession(request, headers, referer, requestOptions, checkSessionPreemptively, --maxTries, rateLimitingDelay).ConfigureAwait(false);
|
||||
if (allowSessionRefresh && await RefreshSession().ConfigureAwait(false)) {
|
||||
return await UrlGetToHtmlDocumentWithSession(request, headers, referer, requestOptions, checkSessionPreemptively, maxTries, rateLimitingDelay, false).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
||||
@@ -538,24 +539,28 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
}
|
||||
|
||||
// Under special brain-damaged circumstances, Steam might just return our own profile as a response to the request, for absolutely no reason whatsoever - just try again in this case
|
||||
if (!requestOptions.HasFlag(WebBrowser.ERequestOptions.ReturnRedirections) && await IsProfileUri(response.FinalUri).ConfigureAwait(false)) {
|
||||
if (!requestOptions.HasFlag(WebBrowser.ERequestOptions.ReturnRedirections) && await IsProfileUri(response.FinalUri).ConfigureAwait(false) && !await IsProfileUri(request).ConfigureAwait(false)) {
|
||||
Bot.ArchiLogger.LogGenericDebug(string.Format(CultureInfo.CurrentCulture, Strings.WarningWorkaroundTriggered, nameof(IsProfileUri)));
|
||||
|
||||
return await UrlGetToHtmlDocumentWithSession(request, headers, referer, requestOptions, checkSessionPreemptively, --maxTries, rateLimitingDelay).ConfigureAwait(false);
|
||||
if (--maxTries == 0) {
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries));
|
||||
Bot.ArchiLogger.LogGenericDebug(string.Format(CultureInfo.CurrentCulture, Strings.ErrorFailingRequest, request));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return await UrlGetToHtmlDocumentWithSession(request, headers, referer, requestOptions, checkSessionPreemptively, maxTries, rateLimitingDelay, allowSessionRefresh).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public async Task<ObjectResponse<T>?> UrlGetToJsonObjectWithSession<T>(Uri request, IReadOnlyCollection<KeyValuePair<string, string>>? headers = null, Uri? referer = null, WebBrowser.ERequestOptions requestOptions = WebBrowser.ERequestOptions.None, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries, int rateLimitingDelay = 0) {
|
||||
public async Task<ObjectResponse<T>?> UrlGetToJsonObjectWithSession<T>(Uri request, IReadOnlyCollection<KeyValuePair<string, string>>? headers = null, Uri? referer = null, WebBrowser.ERequestOptions requestOptions = WebBrowser.ERequestOptions.None, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries, int rateLimitingDelay = 0, bool allowSessionRefresh = true) {
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
if (maxTries == 0) {
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries));
|
||||
Bot.ArchiLogger.LogGenericDebug(string.Format(CultureInfo.CurrentCulture, Strings.ErrorFailingRequest, request));
|
||||
|
||||
return default(ObjectResponse<T>?);
|
||||
throw new ArgumentOutOfRangeException(nameof(maxTries));
|
||||
}
|
||||
|
||||
if (rateLimitingDelay < 0) {
|
||||
@@ -571,8 +576,8 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
bool? sessionExpired = await IsSessionExpired().ConfigureAwait(false);
|
||||
|
||||
if (sessionExpired.GetValueOrDefault(true)) {
|
||||
if (await RefreshSession().ConfigureAwait(false)) {
|
||||
return await UrlGetToJsonObjectWithSession<T>(request, headers, referer, requestOptions, true, --maxTries, rateLimitingDelay).ConfigureAwait(false);
|
||||
if (allowSessionRefresh && await RefreshSession().ConfigureAwait(false)) {
|
||||
return await UrlGetToJsonObjectWithSession<T>(request, headers, referer, requestOptions, true, maxTries, rateLimitingDelay, false).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
||||
@@ -603,15 +608,16 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
|
||||
Uri host = new(request.GetLeftPart(UriPartial.Authority));
|
||||
|
||||
ObjectResponse<T>? response = await WebLimitRequest(host, async () => await WebBrowser.UrlGetToJsonObject<T>(request, headers, referer, requestOptions, rateLimitingDelay: rateLimitingDelay).ConfigureAwait(false)).ConfigureAwait(false);
|
||||
// ReSharper disable once AccessToModifiedClosure - evaluated fully before returning
|
||||
ObjectResponse<T>? response = await WebLimitRequest(host, async () => await WebBrowser.UrlGetToJsonObject<T>(request, headers, referer, requestOptions, maxTries, rateLimitingDelay).ConfigureAwait(false)).ConfigureAwait(false);
|
||||
|
||||
if (response == null) {
|
||||
return default(ObjectResponse<T>?);
|
||||
}
|
||||
|
||||
if (IsSessionExpiredUri(response.FinalUri)) {
|
||||
if (await RefreshSession().ConfigureAwait(false)) {
|
||||
return await UrlGetToJsonObjectWithSession<T>(request, headers, referer, requestOptions, checkSessionPreemptively, --maxTries, rateLimitingDelay).ConfigureAwait(false);
|
||||
if (allowSessionRefresh && await RefreshSession().ConfigureAwait(false)) {
|
||||
return await UrlGetToJsonObjectWithSession<T>(request, headers, referer, requestOptions, checkSessionPreemptively, maxTries, rateLimitingDelay, false).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
||||
@@ -621,24 +627,28 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
}
|
||||
|
||||
// Under special brain-damaged circumstances, Steam might just return our own profile as a response to the request, for absolutely no reason whatsoever - just try again in this case
|
||||
if (!requestOptions.HasFlag(WebBrowser.ERequestOptions.ReturnRedirections) && await IsProfileUri(response.FinalUri).ConfigureAwait(false)) {
|
||||
if (!requestOptions.HasFlag(WebBrowser.ERequestOptions.ReturnRedirections) && await IsProfileUri(response.FinalUri).ConfigureAwait(false) && !await IsProfileUri(request).ConfigureAwait(false)) {
|
||||
Bot.ArchiLogger.LogGenericDebug(string.Format(CultureInfo.CurrentCulture, Strings.WarningWorkaroundTriggered, nameof(IsProfileUri)));
|
||||
|
||||
return await UrlGetToJsonObjectWithSession<T>(request, headers, referer, requestOptions, checkSessionPreemptively, --maxTries, rateLimitingDelay).ConfigureAwait(false);
|
||||
if (--maxTries == 0) {
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries));
|
||||
Bot.ArchiLogger.LogGenericDebug(string.Format(CultureInfo.CurrentCulture, Strings.ErrorFailingRequest, request));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return await UrlGetToJsonObjectWithSession<T>(request, headers, referer, requestOptions, checkSessionPreemptively, maxTries, rateLimitingDelay, allowSessionRefresh).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public async Task<bool> UrlHeadWithSession(Uri request, IReadOnlyCollection<KeyValuePair<string, string>>? headers = null, Uri? referer = null, WebBrowser.ERequestOptions requestOptions = WebBrowser.ERequestOptions.None, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries, int rateLimitingDelay = 0) {
|
||||
public async Task<bool> UrlHeadWithSession(Uri request, IReadOnlyCollection<KeyValuePair<string, string>>? headers = null, Uri? referer = null, WebBrowser.ERequestOptions requestOptions = WebBrowser.ERequestOptions.None, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries, int rateLimitingDelay = 0, bool allowSessionRefresh = true) {
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
if (maxTries == 0) {
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries));
|
||||
Bot.ArchiLogger.LogGenericDebug(string.Format(CultureInfo.CurrentCulture, Strings.ErrorFailingRequest, request));
|
||||
|
||||
return false;
|
||||
throw new ArgumentOutOfRangeException(nameof(maxTries));
|
||||
}
|
||||
|
||||
if (rateLimitingDelay < 0) {
|
||||
@@ -654,8 +664,8 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
bool? sessionExpired = await IsSessionExpired().ConfigureAwait(false);
|
||||
|
||||
if (sessionExpired.GetValueOrDefault(true)) {
|
||||
if (await RefreshSession().ConfigureAwait(false)) {
|
||||
return await UrlHeadWithSession(request, headers, referer, requestOptions, true, --maxTries, rateLimitingDelay).ConfigureAwait(false);
|
||||
if (allowSessionRefresh && await RefreshSession().ConfigureAwait(false)) {
|
||||
return await UrlHeadWithSession(request, headers, referer, requestOptions, true, maxTries, rateLimitingDelay, false).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
||||
@@ -686,15 +696,16 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
|
||||
Uri host = new(request.GetLeftPart(UriPartial.Authority));
|
||||
|
||||
BasicResponse? response = await WebLimitRequest(host, async () => await WebBrowser.UrlHead(request, headers, referer, requestOptions, rateLimitingDelay: rateLimitingDelay).ConfigureAwait(false)).ConfigureAwait(false);
|
||||
// ReSharper disable once AccessToModifiedClosure - evaluated fully before returning
|
||||
BasicResponse? response = await WebLimitRequest(host, async () => await WebBrowser.UrlHead(request, headers, referer, requestOptions, maxTries, rateLimitingDelay).ConfigureAwait(false)).ConfigureAwait(false);
|
||||
|
||||
if (response == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsSessionExpiredUri(response.FinalUri)) {
|
||||
if (await RefreshSession().ConfigureAwait(false)) {
|
||||
return await UrlHeadWithSession(request, headers, referer, requestOptions, checkSessionPreemptively, --maxTries, rateLimitingDelay).ConfigureAwait(false);
|
||||
if (allowSessionRefresh && await RefreshSession().ConfigureAwait(false)) {
|
||||
return await UrlHeadWithSession(request, headers, referer, requestOptions, checkSessionPreemptively, maxTries, rateLimitingDelay, false).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
||||
@@ -704,17 +715,24 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
}
|
||||
|
||||
// Under special brain-damaged circumstances, Steam might just return our own profile as a response to the request, for absolutely no reason whatsoever - just try again in this case
|
||||
if (!requestOptions.HasFlag(WebBrowser.ERequestOptions.ReturnRedirections) && await IsProfileUri(response.FinalUri).ConfigureAwait(false)) {
|
||||
if (!requestOptions.HasFlag(WebBrowser.ERequestOptions.ReturnRedirections) && await IsProfileUri(response.FinalUri).ConfigureAwait(false) && !await IsProfileUri(request).ConfigureAwait(false)) {
|
||||
Bot.ArchiLogger.LogGenericDebug(string.Format(CultureInfo.CurrentCulture, Strings.WarningWorkaroundTriggered, nameof(IsProfileUri)));
|
||||
|
||||
return await UrlHeadWithSession(request, headers, referer, requestOptions, checkSessionPreemptively, --maxTries, rateLimitingDelay).ConfigureAwait(false);
|
||||
if (--maxTries == 0) {
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries));
|
||||
Bot.ArchiLogger.LogGenericDebug(string.Format(CultureInfo.CurrentCulture, Strings.ErrorFailingRequest, request));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return await UrlHeadWithSession(request, headers, referer, requestOptions, checkSessionPreemptively, maxTries, rateLimitingDelay, allowSessionRefresh).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public async Task<HtmlDocumentResponse?> UrlPostToHtmlDocumentWithSession(Uri request, IReadOnlyCollection<KeyValuePair<string, string>>? headers = null, IDictionary<string, string>? data = null, Uri? referer = null, WebBrowser.ERequestOptions requestOptions = WebBrowser.ERequestOptions.None, ESession session = ESession.Lowercase, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries, int rateLimitingDelay = 0) {
|
||||
public async Task<HtmlDocumentResponse?> UrlPostToHtmlDocumentWithSession(Uri request, IReadOnlyCollection<KeyValuePair<string, string>>? headers = null, IDictionary<string, string>? data = null, Uri? referer = null, WebBrowser.ERequestOptions requestOptions = WebBrowser.ERequestOptions.None, ESession session = ESession.Lowercase, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries, int rateLimitingDelay = 0, bool allowSessionRefresh = true) {
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
if (!Enum.IsDefined(session)) {
|
||||
@@ -722,10 +740,7 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
}
|
||||
|
||||
if (maxTries == 0) {
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries));
|
||||
Bot.ArchiLogger.LogGenericDebug(string.Format(CultureInfo.CurrentCulture, Strings.ErrorFailingRequest, request));
|
||||
|
||||
return null;
|
||||
throw new ArgumentOutOfRangeException(nameof(maxTries));
|
||||
}
|
||||
|
||||
if (rateLimitingDelay < 0) {
|
||||
@@ -741,8 +756,8 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
bool? sessionExpired = await IsSessionExpired().ConfigureAwait(false);
|
||||
|
||||
if (sessionExpired.GetValueOrDefault(true)) {
|
||||
if (await RefreshSession().ConfigureAwait(false)) {
|
||||
return await UrlPostToHtmlDocumentWithSession(request, headers, data, referer, requestOptions, session, true, --maxTries, rateLimitingDelay).ConfigureAwait(false);
|
||||
if (allowSessionRefresh && await RefreshSession().ConfigureAwait(false)) {
|
||||
return await UrlPostToHtmlDocumentWithSession(request, headers, data, referer, requestOptions, session, true, maxTries, rateLimitingDelay, false).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
||||
@@ -798,15 +813,16 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
HtmlDocumentResponse? response = await WebLimitRequest(host, async () => await WebBrowser.UrlPostToHtmlDocument(request, headers, data, referer, requestOptions, rateLimitingDelay: rateLimitingDelay).ConfigureAwait(false)).ConfigureAwait(false);
|
||||
// ReSharper disable once AccessToModifiedClosure - evaluated fully before returning
|
||||
HtmlDocumentResponse? response = await WebLimitRequest(host, async () => await WebBrowser.UrlPostToHtmlDocument(request, headers, data, referer, requestOptions, maxTries, rateLimitingDelay).ConfigureAwait(false)).ConfigureAwait(false);
|
||||
|
||||
if (response == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (IsSessionExpiredUri(response.FinalUri)) {
|
||||
if (await RefreshSession().ConfigureAwait(false)) {
|
||||
return await UrlPostToHtmlDocumentWithSession(request, headers, data, referer, requestOptions, session, checkSessionPreemptively, --maxTries, rateLimitingDelay).ConfigureAwait(false);
|
||||
if (allowSessionRefresh && await RefreshSession().ConfigureAwait(false)) {
|
||||
return await UrlPostToHtmlDocumentWithSession(request, headers, data, referer, requestOptions, session, checkSessionPreemptively, maxTries, rateLimitingDelay, false).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
||||
@@ -816,17 +832,24 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
}
|
||||
|
||||
// Under special brain-damaged circumstances, Steam might just return our own profile as a response to the request, for absolutely no reason whatsoever - just try again in this case
|
||||
if (!requestOptions.HasFlag(WebBrowser.ERequestOptions.ReturnRedirections) && await IsProfileUri(response.FinalUri).ConfigureAwait(false)) {
|
||||
if (!requestOptions.HasFlag(WebBrowser.ERequestOptions.ReturnRedirections) && await IsProfileUri(response.FinalUri).ConfigureAwait(false) && !await IsProfileUri(request).ConfigureAwait(false)) {
|
||||
Bot.ArchiLogger.LogGenericDebug(string.Format(CultureInfo.CurrentCulture, Strings.WarningWorkaroundTriggered, nameof(IsProfileUri)));
|
||||
|
||||
return await UrlPostToHtmlDocumentWithSession(request, headers, data, referer, requestOptions, session, checkSessionPreemptively, --maxTries, rateLimitingDelay).ConfigureAwait(false);
|
||||
if (--maxTries == 0) {
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries));
|
||||
Bot.ArchiLogger.LogGenericDebug(string.Format(CultureInfo.CurrentCulture, Strings.ErrorFailingRequest, request));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return await UrlPostToHtmlDocumentWithSession(request, headers, data, referer, requestOptions, session, checkSessionPreemptively, maxTries, rateLimitingDelay, allowSessionRefresh).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public async Task<ObjectResponse<T>?> UrlPostToJsonObjectWithSession<T>(Uri request, IReadOnlyCollection<KeyValuePair<string, string>>? headers = null, IDictionary<string, string>? data = null, Uri? referer = null, WebBrowser.ERequestOptions requestOptions = WebBrowser.ERequestOptions.None, ESession session = ESession.Lowercase, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries, int rateLimitingDelay = 0) {
|
||||
public async Task<ObjectResponse<T>?> UrlPostToJsonObjectWithSession<T>(Uri request, IReadOnlyCollection<KeyValuePair<string, string>>? headers = null, IDictionary<string, string>? data = null, Uri? referer = null, WebBrowser.ERequestOptions requestOptions = WebBrowser.ERequestOptions.None, ESession session = ESession.Lowercase, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries, int rateLimitingDelay = 0, bool allowSessionRefresh = true) {
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
if (!Enum.IsDefined(session)) {
|
||||
@@ -834,10 +857,7 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
}
|
||||
|
||||
if (maxTries == 0) {
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries));
|
||||
Bot.ArchiLogger.LogGenericDebug(string.Format(CultureInfo.CurrentCulture, Strings.ErrorFailingRequest, request));
|
||||
|
||||
return null;
|
||||
throw new ArgumentOutOfRangeException(nameof(maxTries));
|
||||
}
|
||||
|
||||
if (rateLimitingDelay < 0) {
|
||||
@@ -853,8 +873,8 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
bool? sessionExpired = await IsSessionExpired().ConfigureAwait(false);
|
||||
|
||||
if (sessionExpired.GetValueOrDefault(true)) {
|
||||
if (await RefreshSession().ConfigureAwait(false)) {
|
||||
return await UrlPostToJsonObjectWithSession<T>(request, headers, data, referer, requestOptions, session, true, --maxTries, rateLimitingDelay).ConfigureAwait(false);
|
||||
if (allowSessionRefresh && await RefreshSession().ConfigureAwait(false)) {
|
||||
return await UrlPostToJsonObjectWithSession<T>(request, headers, data, referer, requestOptions, session, true, maxTries, rateLimitingDelay, false).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
||||
@@ -910,15 +930,16 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
ObjectResponse<T>? response = await WebLimitRequest(host, async () => await WebBrowser.UrlPostToJsonObject<T, IDictionary<string, string>>(request, headers, data, referer, requestOptions, rateLimitingDelay: rateLimitingDelay).ConfigureAwait(false)).ConfigureAwait(false);
|
||||
// ReSharper disable once AccessToModifiedClosure - evaluated fully before returning
|
||||
ObjectResponse<T>? response = await WebLimitRequest(host, async () => await WebBrowser.UrlPostToJsonObject<T, IDictionary<string, string>>(request, headers, data, referer, requestOptions, maxTries, rateLimitingDelay).ConfigureAwait(false)).ConfigureAwait(false);
|
||||
|
||||
if (response == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (IsSessionExpiredUri(response.FinalUri)) {
|
||||
if (await RefreshSession().ConfigureAwait(false)) {
|
||||
return await UrlPostToJsonObjectWithSession<T>(request, headers, data, referer, requestOptions, session, checkSessionPreemptively, --maxTries, rateLimitingDelay).ConfigureAwait(false);
|
||||
if (allowSessionRefresh && await RefreshSession().ConfigureAwait(false)) {
|
||||
return await UrlPostToJsonObjectWithSession<T>(request, headers, data, referer, requestOptions, session, checkSessionPreemptively, maxTries, rateLimitingDelay, false).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
||||
@@ -928,17 +949,24 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
}
|
||||
|
||||
// Under special brain-damaged circumstances, Steam might just return our own profile as a response to the request, for absolutely no reason whatsoever - just try again in this case
|
||||
if (!requestOptions.HasFlag(WebBrowser.ERequestOptions.ReturnRedirections) && await IsProfileUri(response.FinalUri).ConfigureAwait(false)) {
|
||||
if (!requestOptions.HasFlag(WebBrowser.ERequestOptions.ReturnRedirections) && await IsProfileUri(response.FinalUri).ConfigureAwait(false) && !await IsProfileUri(request).ConfigureAwait(false)) {
|
||||
Bot.ArchiLogger.LogGenericDebug(string.Format(CultureInfo.CurrentCulture, Strings.WarningWorkaroundTriggered, nameof(IsProfileUri)));
|
||||
|
||||
return await UrlPostToJsonObjectWithSession<T>(request, headers, data, referer, requestOptions, session, checkSessionPreemptively, --maxTries, rateLimitingDelay).ConfigureAwait(false);
|
||||
if (--maxTries == 0) {
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries));
|
||||
Bot.ArchiLogger.LogGenericDebug(string.Format(CultureInfo.CurrentCulture, Strings.ErrorFailingRequest, request));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return await UrlPostToJsonObjectWithSession<T>(request, headers, data, referer, requestOptions, session, checkSessionPreemptively, maxTries, rateLimitingDelay, allowSessionRefresh).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public async Task<ObjectResponse<T>?> UrlPostToJsonObjectWithSession<T>(Uri request, IReadOnlyCollection<KeyValuePair<string, string>>? headers = null, ICollection<KeyValuePair<string, string>>? data = null, Uri? referer = null, WebBrowser.ERequestOptions requestOptions = WebBrowser.ERequestOptions.None, ESession session = ESession.Lowercase, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries, int rateLimitingDelay = 0) {
|
||||
public async Task<ObjectResponse<T>?> UrlPostToJsonObjectWithSession<T>(Uri request, IReadOnlyCollection<KeyValuePair<string, string>>? headers = null, ICollection<KeyValuePair<string, string>>? data = null, Uri? referer = null, WebBrowser.ERequestOptions requestOptions = WebBrowser.ERequestOptions.None, ESession session = ESession.Lowercase, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries, int rateLimitingDelay = 0, bool allowSessionRefresh = true) {
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
if (!Enum.IsDefined(session)) {
|
||||
@@ -946,10 +974,7 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
}
|
||||
|
||||
if (maxTries == 0) {
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries));
|
||||
Bot.ArchiLogger.LogGenericDebug(string.Format(CultureInfo.CurrentCulture, Strings.ErrorFailingRequest, request));
|
||||
|
||||
return null;
|
||||
throw new ArgumentOutOfRangeException(nameof(maxTries));
|
||||
}
|
||||
|
||||
if (rateLimitingDelay < 0) {
|
||||
@@ -965,8 +990,8 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
bool? sessionExpired = await IsSessionExpired().ConfigureAwait(false);
|
||||
|
||||
if (sessionExpired.GetValueOrDefault(true)) {
|
||||
if (await RefreshSession().ConfigureAwait(false)) {
|
||||
return await UrlPostToJsonObjectWithSession<T>(request, headers, data, referer, requestOptions, session, true, --maxTries, rateLimitingDelay).ConfigureAwait(false);
|
||||
if (allowSessionRefresh && await RefreshSession().ConfigureAwait(false)) {
|
||||
return await UrlPostToJsonObjectWithSession<T>(request, headers, data, referer, requestOptions, session, true, maxTries, rateLimitingDelay, false).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
||||
@@ -1024,15 +1049,16 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
ObjectResponse<T>? response = await WebLimitRequest(host, async () => await WebBrowser.UrlPostToJsonObject<T, ICollection<KeyValuePair<string, string>>>(request, headers, data, referer, requestOptions, rateLimitingDelay: rateLimitingDelay).ConfigureAwait(false)).ConfigureAwait(false);
|
||||
// ReSharper disable once AccessToModifiedClosure - evaluated fully before returning
|
||||
ObjectResponse<T>? response = await WebLimitRequest(host, async () => await WebBrowser.UrlPostToJsonObject<T, ICollection<KeyValuePair<string, string>>>(request, headers, data, referer, requestOptions, maxTries, rateLimitingDelay).ConfigureAwait(false)).ConfigureAwait(false);
|
||||
|
||||
if (response == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (IsSessionExpiredUri(response.FinalUri)) {
|
||||
if (await RefreshSession().ConfigureAwait(false)) {
|
||||
return await UrlPostToJsonObjectWithSession<T>(request, headers, data, referer, requestOptions, session, checkSessionPreemptively, --maxTries, rateLimitingDelay).ConfigureAwait(false);
|
||||
if (allowSessionRefresh && await RefreshSession().ConfigureAwait(false)) {
|
||||
return await UrlPostToJsonObjectWithSession<T>(request, headers, data, referer, requestOptions, session, checkSessionPreemptively, maxTries, rateLimitingDelay, false).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
||||
@@ -1042,17 +1068,24 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
}
|
||||
|
||||
// Under special brain-damaged circumstances, Steam might just return our own profile as a response to the request, for absolutely no reason whatsoever - just try again in this case
|
||||
if (!requestOptions.HasFlag(WebBrowser.ERequestOptions.ReturnRedirections) && await IsProfileUri(response.FinalUri).ConfigureAwait(false)) {
|
||||
if (!requestOptions.HasFlag(WebBrowser.ERequestOptions.ReturnRedirections) && await IsProfileUri(response.FinalUri).ConfigureAwait(false) && !await IsProfileUri(request).ConfigureAwait(false)) {
|
||||
Bot.ArchiLogger.LogGenericDebug(string.Format(CultureInfo.CurrentCulture, Strings.WarningWorkaroundTriggered, nameof(IsProfileUri)));
|
||||
|
||||
return await UrlPostToJsonObjectWithSession<T>(request, headers, data, referer, requestOptions, session, checkSessionPreemptively, --maxTries, rateLimitingDelay).ConfigureAwait(false);
|
||||
if (--maxTries == 0) {
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries));
|
||||
Bot.ArchiLogger.LogGenericDebug(string.Format(CultureInfo.CurrentCulture, Strings.ErrorFailingRequest, request));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return await UrlPostToJsonObjectWithSession<T>(request, headers, data, referer, requestOptions, session, checkSessionPreemptively, maxTries, rateLimitingDelay, allowSessionRefresh).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public async Task<bool> UrlPostWithSession(Uri request, IReadOnlyCollection<KeyValuePair<string, string>>? headers = null, IDictionary<string, string>? data = null, Uri? referer = null, WebBrowser.ERequestOptions requestOptions = WebBrowser.ERequestOptions.None, ESession session = ESession.Lowercase, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries, int rateLimitingDelay = 0) {
|
||||
public async Task<bool> UrlPostWithSession(Uri request, IReadOnlyCollection<KeyValuePair<string, string>>? headers = null, IDictionary<string, string>? data = null, Uri? referer = null, WebBrowser.ERequestOptions requestOptions = WebBrowser.ERequestOptions.None, ESession session = ESession.Lowercase, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries, int rateLimitingDelay = 0, bool allowSessionRefresh = true) {
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
if (!Enum.IsDefined(session)) {
|
||||
@@ -1060,10 +1093,7 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
}
|
||||
|
||||
if (maxTries == 0) {
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries));
|
||||
Bot.ArchiLogger.LogGenericDebug(string.Format(CultureInfo.CurrentCulture, Strings.ErrorFailingRequest, request));
|
||||
|
||||
return false;
|
||||
throw new ArgumentOutOfRangeException(nameof(maxTries));
|
||||
}
|
||||
|
||||
if (rateLimitingDelay < 0) {
|
||||
@@ -1079,8 +1109,8 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
bool? sessionExpired = await IsSessionExpired().ConfigureAwait(false);
|
||||
|
||||
if (sessionExpired.GetValueOrDefault(true)) {
|
||||
if (await RefreshSession().ConfigureAwait(false)) {
|
||||
return await UrlPostWithSession(request, headers, data, referer, requestOptions, session, true, --maxTries, rateLimitingDelay).ConfigureAwait(false);
|
||||
if (allowSessionRefresh && await RefreshSession().ConfigureAwait(false)) {
|
||||
return await UrlPostWithSession(request, headers, data, referer, requestOptions, session, true, maxTries, rateLimitingDelay, false).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
||||
@@ -1136,15 +1166,16 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
BasicResponse? response = await WebLimitRequest(host, async () => await WebBrowser.UrlPost(request, headers, data, referer, requestOptions, rateLimitingDelay: rateLimitingDelay).ConfigureAwait(false)).ConfigureAwait(false);
|
||||
// ReSharper disable once AccessToModifiedClosure - evaluated fully before returning
|
||||
BasicResponse? response = await WebLimitRequest(host, async () => await WebBrowser.UrlPost(request, headers, data, referer, requestOptions, maxTries, rateLimitingDelay).ConfigureAwait(false)).ConfigureAwait(false);
|
||||
|
||||
if (response == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsSessionExpiredUri(response.FinalUri)) {
|
||||
if (await RefreshSession().ConfigureAwait(false)) {
|
||||
return await UrlPostWithSession(request, headers, data, referer, requestOptions, session, checkSessionPreemptively, --maxTries, rateLimitingDelay).ConfigureAwait(false);
|
||||
if (allowSessionRefresh && await RefreshSession().ConfigureAwait(false)) {
|
||||
return await UrlPostWithSession(request, headers, data, referer, requestOptions, session, checkSessionPreemptively, maxTries, rateLimitingDelay, false).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed);
|
||||
@@ -1154,10 +1185,17 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
}
|
||||
|
||||
// Under special brain-damaged circumstances, Steam might just return our own profile as a response to the request, for absolutely no reason whatsoever - just try again in this case
|
||||
if (!requestOptions.HasFlag(WebBrowser.ERequestOptions.ReturnRedirections) && await IsProfileUri(response.FinalUri).ConfigureAwait(false)) {
|
||||
if (!requestOptions.HasFlag(WebBrowser.ERequestOptions.ReturnRedirections) && await IsProfileUri(response.FinalUri).ConfigureAwait(false) && !await IsProfileUri(request).ConfigureAwait(false)) {
|
||||
Bot.ArchiLogger.LogGenericDebug(string.Format(CultureInfo.CurrentCulture, Strings.WarningWorkaroundTriggered, nameof(IsProfileUri)));
|
||||
|
||||
return await UrlPostWithSession(request, headers, data, referer, requestOptions, session, checkSessionPreemptively, --maxTries, rateLimitingDelay).ConfigureAwait(false);
|
||||
if (--maxTries == 0) {
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries));
|
||||
Bot.ArchiLogger.LogGenericDebug(string.Format(CultureInfo.CurrentCulture, Strings.ErrorFailingRequest, request));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return await UrlPostWithSession(request, headers, data, referer, requestOptions, session, checkSessionPreemptively, maxTries, rateLimitingDelay, allowSessionRefresh).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -1256,12 +1294,12 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
for (byte i = 0; (i < WebBrowser.MaxTries) && (response == null); i++) {
|
||||
response = await UrlPostToJsonObjectWithSession<TradeOfferAcceptResponse>(request, data: data, referer: referer, requestOptions: WebBrowser.ERequestOptions.ReturnServerErrors).ConfigureAwait(false);
|
||||
|
||||
if (response?.Content == null) {
|
||||
if (response == null) {
|
||||
return (false, false);
|
||||
}
|
||||
|
||||
if (response.StatusCode.IsServerErrorCode()) {
|
||||
if (string.IsNullOrEmpty(response.Content.ErrorText)) {
|
||||
if (string.IsNullOrEmpty(response.Content?.ErrorText)) {
|
||||
// This is a generic server error without a reason, try again
|
||||
response = null;
|
||||
|
||||
@@ -1269,7 +1307,8 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
}
|
||||
|
||||
// This is actually client error with a reason, so it doesn't make sense to retry
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, response.Content.ErrorText));
|
||||
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, response.Content!.ErrorText));
|
||||
|
||||
return (false, false);
|
||||
}
|
||||
@@ -1671,6 +1710,71 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
return result;
|
||||
}
|
||||
|
||||
internal async Task<byte?> GetCombinedTradeHoldDurationAgainstUser(ulong steamID, string? tradeToken = null) {
|
||||
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
|
||||
throw new ArgumentOutOfRangeException(nameof(steamID));
|
||||
}
|
||||
|
||||
(bool success, string? steamApiKey) = await CachedApiKey.GetValue().ConfigureAwait(false);
|
||||
|
||||
if (!success || string.IsNullOrEmpty(steamApiKey)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Dictionary<string, object?> arguments = new(!string.IsNullOrEmpty(tradeToken) ? 3 : 2, StringComparer.Ordinal) {
|
||||
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
|
||||
{ "key", steamApiKey! },
|
||||
|
||||
{ "steamid_target", steamID }
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(tradeToken)) {
|
||||
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
|
||||
arguments["trade_offer_access_token"] = tradeToken!;
|
||||
}
|
||||
|
||||
KeyValue? response = null;
|
||||
|
||||
for (byte i = 0; (i < WebBrowser.MaxTries) && (response == null); i++) {
|
||||
if ((i > 0) && (WebLimiterDelay > 0)) {
|
||||
await Task.Delay(WebLimiterDelay).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
using WebAPI.AsyncInterface econService = Bot.SteamConfiguration.GetAsyncWebAPIInterface(EconService);
|
||||
|
||||
econService.Timeout = WebBrowser.Timeout;
|
||||
|
||||
try {
|
||||
response = await WebLimitRequest(
|
||||
WebAPI.DefaultBaseAddress,
|
||||
|
||||
// ReSharper disable once AccessToDisposedClosure
|
||||
async () => await econService.CallAsync(HttpMethod.Get, "GetTradeHoldDurations", args: arguments).ConfigureAwait(false)
|
||||
).ConfigureAwait(false);
|
||||
} catch (TaskCanceledException e) {
|
||||
Bot.ArchiLogger.LogGenericDebuggingException(e);
|
||||
} catch (Exception e) {
|
||||
Bot.ArchiLogger.LogGenericWarningException(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (response == null) {
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
uint resultInSeconds = response["both_escrow"]["escrow_end_duration_seconds"].AsUnsignedInteger(uint.MaxValue);
|
||||
|
||||
if (resultInSeconds == uint.MaxValue) {
|
||||
Bot.ArchiLogger.LogNullError(resultInSeconds);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return resultInSeconds == 0 ? (byte) 0 : (byte) (resultInSeconds / 86400);
|
||||
}
|
||||
|
||||
internal async Task<IDocument?> GetConfirmationsPage(string deviceID, string confirmationHash, ulong time) {
|
||||
if (string.IsNullOrEmpty(deviceID)) {
|
||||
throw new ArgumentNullException(nameof(deviceID));
|
||||
@@ -1895,71 +1999,6 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
return result;
|
||||
}
|
||||
|
||||
internal async Task<byte?> GetCombinedTradeHoldDurationAgainstUser(ulong steamID, string? tradeToken = null) {
|
||||
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
|
||||
throw new ArgumentOutOfRangeException(nameof(steamID));
|
||||
}
|
||||
|
||||
(bool success, string? steamApiKey) = await CachedApiKey.GetValue().ConfigureAwait(false);
|
||||
|
||||
if (!success || string.IsNullOrEmpty(steamApiKey)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Dictionary<string, object?> arguments = new(!string.IsNullOrEmpty(tradeToken) ? 3 : 2, StringComparer.Ordinal) {
|
||||
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
|
||||
{ "key", steamApiKey! },
|
||||
|
||||
{ "steamid_target", steamID }
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(tradeToken)) {
|
||||
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
|
||||
arguments["trade_offer_access_token"] = tradeToken!;
|
||||
}
|
||||
|
||||
KeyValue? response = null;
|
||||
|
||||
for (byte i = 0; (i < WebBrowser.MaxTries) && (response == null); i++) {
|
||||
if ((i > 0) && (WebLimiterDelay > 0)) {
|
||||
await Task.Delay(WebLimiterDelay).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
using WebAPI.AsyncInterface econService = Bot.SteamConfiguration.GetAsyncWebAPIInterface(EconService);
|
||||
|
||||
econService.Timeout = WebBrowser.Timeout;
|
||||
|
||||
try {
|
||||
response = await WebLimitRequest(
|
||||
WebAPI.DefaultBaseAddress,
|
||||
|
||||
// ReSharper disable once AccessToDisposedClosure
|
||||
async () => await econService.CallAsync(HttpMethod.Get, "GetTradeHoldDurations", args: arguments).ConfigureAwait(false)
|
||||
).ConfigureAwait(false);
|
||||
} catch (TaskCanceledException e) {
|
||||
Bot.ArchiLogger.LogGenericDebuggingException(e);
|
||||
} catch (Exception e) {
|
||||
Bot.ArchiLogger.LogGenericWarningException(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (response == null) {
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
uint resultInSeconds = response["both_escrow"]["escrow_end_duration_seconds"].AsUnsignedInteger(uint.MaxValue);
|
||||
|
||||
if (resultInSeconds == uint.MaxValue) {
|
||||
Bot.ArchiLogger.LogNullError(resultInSeconds);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return resultInSeconds == 0 ? (byte) 0 : (byte) (resultInSeconds / 86400);
|
||||
}
|
||||
|
||||
internal async Task<bool?> HandleConfirmation(string deviceID, string confirmationHash, ulong time, ulong confirmationID, ulong confirmationKey, bool accept) {
|
||||
if (string.IsNullOrEmpty(deviceID)) {
|
||||
throw new ArgumentNullException(nameof(deviceID));
|
||||
@@ -2175,7 +2214,8 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
LastSessionCheck = LastSessionRefresh = DateTime.UtcNow;
|
||||
LastSessionCheck = DateTime.UtcNow;
|
||||
SessionValidUntil = LastSessionCheck.AddSeconds(MinimumSessionValidityInSeconds);
|
||||
Initialized = true;
|
||||
|
||||
return true;
|
||||
@@ -2389,15 +2429,22 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
private async Task<bool?> IsSessionExpired() {
|
||||
DateTime triggeredAt = DateTime.UtcNow;
|
||||
|
||||
if (triggeredAt <= LastSessionCheck) {
|
||||
return LastSessionCheck != LastSessionRefresh;
|
||||
if (triggeredAt <= SessionValidUntil) {
|
||||
// Assume session is still valid
|
||||
return false;
|
||||
}
|
||||
|
||||
await SessionSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try {
|
||||
if (triggeredAt <= SessionValidUntil) {
|
||||
// Other request already checked the session for us in the meantime, nice
|
||||
return false;
|
||||
}
|
||||
|
||||
if (triggeredAt <= LastSessionCheck) {
|
||||
return LastSessionCheck != LastSessionRefresh;
|
||||
// Other request already checked the session for us in the meantime and failed, pointless to try again
|
||||
return true;
|
||||
}
|
||||
|
||||
// Choosing proper URL to check against is actually much harder than it initially looks like, we must abide by several rules to make this function as lightweight and reliable as possible
|
||||
@@ -2420,8 +2467,9 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
|
||||
if (result) {
|
||||
Initialized = false;
|
||||
SessionValidUntil = DateTime.MinValue;
|
||||
} else {
|
||||
LastSessionRefresh = now;
|
||||
SessionValidUntil = now.AddSeconds(MinimumSessionValidityInSeconds);
|
||||
}
|
||||
|
||||
LastSessionCheck = now;
|
||||
@@ -2514,20 +2562,25 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
return false;
|
||||
}
|
||||
|
||||
DateTime triggeredAt = DateTime.UtcNow;
|
||||
DateTime previousSessionValidUntil = SessionValidUntil;
|
||||
|
||||
if (triggeredAt <= LastSessionCheck) {
|
||||
return LastSessionCheck == LastSessionRefresh;
|
||||
}
|
||||
DateTime triggeredAt = DateTime.UtcNow;
|
||||
|
||||
await SessionSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try {
|
||||
if ((triggeredAt <= SessionValidUntil) && (SessionValidUntil > previousSessionValidUntil)) {
|
||||
// Other request already refreshed the session for us in the meantime, nice
|
||||
return true;
|
||||
}
|
||||
|
||||
if (triggeredAt <= LastSessionCheck) {
|
||||
return LastSessionCheck == LastSessionRefresh;
|
||||
// Other request already checked the session for us in the meantime and failed, pointless to try again
|
||||
return false;
|
||||
}
|
||||
|
||||
Initialized = false;
|
||||
SessionValidUntil = DateTime.MinValue;
|
||||
|
||||
if (!Bot.IsConnectedAndLoggedOn) {
|
||||
return false;
|
||||
@@ -2539,7 +2592,7 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
DateTime now = DateTime.UtcNow;
|
||||
|
||||
if (result) {
|
||||
LastSessionRefresh = now;
|
||||
SessionValidUntil = now.AddSeconds(MinimumSessionValidityInSeconds);
|
||||
}
|
||||
|
||||
LastSessionCheck = now;
|
||||
|
||||
@@ -181,17 +181,19 @@ public sealed class Actions : IAsyncDisposable, IDisposable {
|
||||
handledConfirmations[confirmation.Creator] = confirmation;
|
||||
}
|
||||
|
||||
if (acceptedCreatorIDs?.Count > 0) {
|
||||
// Check if those are all that we were expected to confirm
|
||||
if ((handledConfirmations.Count >= acceptedCreatorIDs.Count) && acceptedCreatorIDs.All(handledConfirmations.ContainsKey)) {
|
||||
return (true, handledConfirmations.Values, string.Format(CultureInfo.CurrentCulture, Strings.BotHandledConfirmations, handledConfirmations.Count));
|
||||
}
|
||||
// We've accepted *something*, if caller didn't specify the IDs, that's enough for us
|
||||
if ((acceptedCreatorIDs == null) || (acceptedCreatorIDs.Count == 0)) {
|
||||
return (true, handledConfirmations.Values, string.Format(CultureInfo.CurrentCulture, Strings.BotHandledConfirmations, handledConfirmations.Count));
|
||||
}
|
||||
|
||||
// If he did, check if we've already found everything we were supposed to
|
||||
if ((handledConfirmations.Count >= acceptedCreatorIDs.Count) && acceptedCreatorIDs.All(handledConfirmations.ContainsKey)) {
|
||||
return (true, handledConfirmations.Values, string.Format(CultureInfo.CurrentCulture, Strings.BotHandledConfirmations, handledConfirmations.Count));
|
||||
}
|
||||
}
|
||||
|
||||
bool success = !waitIfNeeded || ((handledConfirmations?.Count > 0) && ((acceptedCreatorIDs == null) || (acceptedCreatorIDs.Count == 0)));
|
||||
|
||||
return (success, handledConfirmations?.Values, success ? string.Format(CultureInfo.CurrentCulture, Strings.BotHandledConfirmations, handledConfirmations?.Count ?? 0) : string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries));
|
||||
// If we've reached this point, then it's a failure for waitIfNeeded, and success otherwise
|
||||
return (!waitIfNeeded, handledConfirmations?.Values, !waitIfNeeded ? string.Format(CultureInfo.CurrentCulture, Strings.BotHandledConfirmations, handledConfirmations?.Count ?? 0) : string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries));
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>5.2.7.7</Version>
|
||||
<Version>5.2.8.2</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -33,6 +33,7 @@
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net48'">
|
||||
<PackageReference Include="JustArchiNET.Madness" />
|
||||
<PackageReference Include="TA.System.Runtime.CompilerServices.RuntimeHelpers.GetSubArray" />
|
||||
|
||||
<Using Include="JustArchiNET.Madness" />
|
||||
<Using Include="JustArchiNET.Madness.ArgumentNullExceptionMadness.ArgumentNullException" Alias="ArgumentNullException" />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="AngleSharp.XPath" Version="1.1.7" />
|
||||
<PackageVersion Include="AngleSharp.XPath" Version="2.0.1" />
|
||||
<PackageVersion Include="ConfigureAwaitChecker.Analyzer" Version="5.0.0.1" />
|
||||
<PackageVersion Include="CryptSharpStandard" Version="1.0.0" />
|
||||
<PackageVersion Include="Humanizer" Version="2.14.1" />
|
||||
@@ -11,11 +11,11 @@
|
||||
<PackageVersion Include="MSTest.TestFramework" Version="2.2.10" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageVersion Include="Nito.AsyncEx.Coordination" Version="5.1.2" />
|
||||
<PackageVersion Include="NLog.Web.AspNetCore" Version="5.0.0" />
|
||||
<PackageVersion Include="NLog.Web.AspNetCore" Version="5.1.0" />
|
||||
<PackageVersion Include="SteamKit2" Version="2.4.1" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.3.1" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.Annotations" Version="6.3.1" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.3.1" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.Annotations" Version="6.4.0" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.4.0" />
|
||||
<PackageVersion Include="System.Composition" Version="6.0.0" />
|
||||
<PackageVersion Include="System.Composition.AttributedModel" Version="6.0.0" />
|
||||
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
|
||||
@@ -27,7 +27,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net48'">
|
||||
<PackageVersion Include="JustArchiNET.Madness" Version="3.6.0" />
|
||||
<PackageVersion Include="JustArchiNET.Madness" Version="3.7.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Cors" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Diagnostics" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
|
||||
@@ -36,7 +36,8 @@
|
||||
<PackageVersion Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="3.1.26" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Configuration" Version="3.1.26" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="3.1.27" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Configuration" Version="3.1.27" />
|
||||
<PackageVersion Include="TA.System.Runtime.CompilerServices.RuntimeHelpers.GetSubArray" Version="1.0.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
ARG IMAGESUFFIX
|
||||
|
||||
FROM --platform=$BUILDPLATFORM node:lts${IMAGESUFFIX} AS build-node
|
||||
WORKDIR /app
|
||||
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 && \
|
||||
@@ -18,7 +19,7 @@ ENV DOTNET_NOLOGO true
|
||||
ENV NET_CORE_VERSION net6.0
|
||||
ENV STEAM_TOKEN_DUMPER_NAME ArchiSteamFarm.OfficialPlugins.SteamTokenDumper
|
||||
WORKDIR /app
|
||||
COPY --from=build-node /app/dist ASF-ui/dist
|
||||
COPY --from=build-node /app/ASF-ui/dist ASF-ui/dist
|
||||
COPY ArchiSteamFarm ArchiSteamFarm
|
||||
COPY ArchiSteamFarm.OfficialPlugins.SteamTokenDumper ArchiSteamFarm.OfficialPlugins.SteamTokenDumper
|
||||
COPY resources resources
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
ARG IMAGESUFFIX
|
||||
|
||||
FROM --platform=$BUILDPLATFORM node:lts${IMAGESUFFIX} AS build-node
|
||||
WORKDIR /app
|
||||
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 && \
|
||||
@@ -18,7 +19,7 @@ ENV DOTNET_NOLOGO true
|
||||
ENV NET_CORE_VERSION net6.0
|
||||
ENV STEAM_TOKEN_DUMPER_NAME ArchiSteamFarm.OfficialPlugins.SteamTokenDumper
|
||||
WORKDIR /app
|
||||
COPY --from=build-node /app/dist ASF-ui/dist
|
||||
COPY --from=build-node /app/ASF-ui/dist ASF-ui/dist
|
||||
COPY ArchiSteamFarm ArchiSteamFarm
|
||||
COPY ArchiSteamFarm.OfficialPlugins.SteamTokenDumper ArchiSteamFarm.OfficialPlugins.SteamTokenDumper
|
||||
COPY resources resources
|
||||
|
||||
2
wiki
2
wiki
Submodule wiki updated: 7dee92603f...e2ab1e7df2
Reference in New Issue
Block a user