Compare commits

..

93 Commits

Author SHA1 Message Date
JustArchi
661786adf2 Resolve AngleSharp.XPath issue 2022-07-20 22:17:08 +02:00
ArchiBot
6c63b6db68 Automatic translations update 2022-07-20 02:41:09 +00:00
renovate[bot]
dfdb0a22a0 Update swashbuckle-aspnetcore monorepo to v6.4.0 2022-07-19 21:28:18 +00:00
Sebastian Göls
41ecfb1d02 Move copying of overlay files to ArchiSteamFarm.csproj (#2650)
* Move copying of overlay files to ArchiSteamFarm.csproj

* Fix build on Windows

* Try to make it more reliable

* Update ArchiSteamFarm.csproj

* Update ArchiSteamFarm.csproj

* Revert "Update ArchiSteamFarm.csproj"

This reverts commit ba41b2e3c1.

* Rename

Co-authored-by: JustArchi <JustArchi@JustArchi.net>
2022-07-19 22:20:18 +02:00
renovate[bot]
25b29cd56e Update docker/build-push-action action to v3.1.0 2022-07-19 15:59:00 +00:00
renovate[bot]
ab5fb6dd1b Update ASF-ui digest to 60a692f 2022-07-19 04:27:13 +00:00
renovate[bot]
75e4557da3 Update dependency NLog.Web.AspNetCore to v5.1.0 2022-07-18 20:41:52 +00:00
renovate[bot]
95c3658197 Update ASF-ui digest to 7ec7898 2022-07-18 14:56:43 +00:00
ArchiBot
26e7f7deb5 Automatic translations update 2022-07-18 02:43:39 +00:00
renovate[bot]
f003dcda0b Update wiki digest to 5974dbb 2022-07-17 13:04:41 +00:00
renovate[bot]
296318060f Update ASF-ui digest to 06d59c3 2022-07-17 01:05:02 +00:00
renovate[bot]
f80b114892 Update ASF-ui digest to 463b788 2022-07-16 19:05:04 +00:00
JustArchi
604652c03d Make PluginsCore public 2022-07-16 18:00:42 +02:00
renovate[bot]
2420d34b22 Update ASF-ui digest to 4e98e21 2022-07-15 19:46:13 +00:00
JustArchi
21a5793c45 Take into account that git is special snowflake 2022-07-15 21:45:18 +02:00
JustArchi
888b45c919 Include commit hash for docker builds of ASF-ui
Originally spotted at https://github.com/JustArchiNET/ASF-ui/issues/1589
2022-07-15 21:33:28 +02:00
renovate[bot]
35bf243f1a Update ASF-ui digest to c694963 2022-07-15 06:56:42 +00:00
ArchiBot
f4c1dededc Automatic translations update 2022-07-15 02:47:02 +00:00
renovate[bot]
d5f355a2bc Update actions/setup-node action to v3.4.1 2022-07-14 14:49:14 +00:00
ArchiBot
eb9b5dd025 Automatic translations update 2022-07-14 02:43:58 +00:00
renovate[bot]
2cec35f911 Update swashbuckle-aspnetcore monorepo to v6.3.2 2022-07-13 22:50:35 +00:00
renovate[bot]
7d4438b089 Update ASF-ui digest to 60d2fe0 2022-07-13 12:15:56 +00:00
ArchiBot
2782329549 Automatic translations update 2022-07-13 02:41:49 +00:00
renovate[bot]
14ac124e0c Update dotnet monorepo to v3.1.27 2022-07-12 14:35:31 +00:00
renovate[bot]
ec07d23cc4 Update ASF-ui digest to e7e7192 2022-07-12 05:32:17 +00:00
ArchiBot
3299ba8c10 Automatic translations update 2022-07-12 02:46:54 +00:00
renovate[bot]
4eb09f950d Update ASF-ui digest to c9ca06e 2022-07-11 21:20:43 +00:00
JustArchi
144a1d1574 Do not trigger ProfileUri workaround if request was for profile 2022-07-11 23:15:29 +02:00
renovate[bot]
05f3aada38 Update actions/setup-node action to v3.4.0 2022-07-11 16:08:49 +00:00
JustArchi
9240500e2c Bump 2022-07-11 18:07:03 +02:00
renovate[bot]
98768970a7 Update ASF-ui digest to b76040f 2022-07-11 05:27:44 +00:00
ArchiBot
7e41e530e7 Automatic translations update 2022-07-11 02:39:31 +00:00
renovate[bot]
97198fd435 Update ASF-ui digest to 8d93cf5 2022-07-10 11:01:21 +00:00
renovate[bot]
e8b83b8ad4 Update ASF-ui digest to 5b632ba 2022-07-10 03:47:31 +00:00
ArchiBot
929a1dfd82 Automatic translations update 2022-07-10 02:43:57 +00:00
renovate[bot]
ea7bad2868 Update ASF-ui digest to 23585b6 2022-07-09 22:06:59 +00:00
renovate[bot]
71f54a79f3 Update ASF-ui digest to 5409364 2022-07-09 13:28:26 +00:00
JustArchi
0cd7b10c9a Rewrite AWH requests public API
Breaking change that needs recompilation but doesn't need code edits, actually make maxTries work as they should and are expected to, with session refresh being controlled by a new boolean instead
2022-07-09 00:51:52 +02:00
renovate[bot]
42fb71f856 Update ASF-ui digest to 8b7e253 2022-07-08 18:49:30 +00:00
JustArchi
7b3ae25d58 Misc 2022-07-08 19:20:29 +02:00
JustArchi
2961975f05 Misc 2022-07-08 19:17:43 +02:00
JustArchi
06843ebf9f Misc 2022-07-08 19:16:58 +02:00
JustArchi
6ca395795c Move network group logic into plugins core
This will allow plugin creators to make use of network groups
2022-07-08 19:16:29 +02:00
JustArchi
0e5490cc3a Allow plugin creators to initialize their own limiters 2022-07-08 19:11:27 +02:00
renovate[bot]
39cc6e6ea6 Update crowdin/github-action action to v1.4.10 2022-07-08 15:08:45 +00:00
renovate[bot]
70544d1d76 Update ASF-ui digest to aed6536 2022-07-08 12:16:11 +00:00
ArchiBot
b1e9a53adc Automatic translations update 2022-07-08 02:42:04 +00:00
renovate[bot]
72e59e7271 Update ASF-ui digest to 64548c6 2022-07-07 23:37:04 +00:00
renovate[bot]
1ae3517374 Update ASF-ui digest to 5dd4507 2022-07-07 05:28:04 +00:00
ArchiBot
975b3b89f3 Automatic translations update 2022-07-07 02:45:33 +00:00
renovate[bot]
a982520844 Update ASF-ui digest to dafec4e 2022-07-06 14:07:46 +00:00
renovate[bot]
0ed4c7536a Update ASF-ui digest to 360b16c 2022-07-05 11:12:49 +00:00
renovate[bot]
6e360b7e1a Update ASF-ui digest to 782c965 2022-07-04 17:27:25 +00:00
renovate[bot]
17c79904e4 Update ASF-ui digest to 745af45 2022-07-04 04:05:38 +00:00
ArchiBot
0fcbc8c402 Automatic translations update 2022-07-04 02:45:38 +00:00
renovate[bot]
9e0d44bee2 Update ASF-ui digest to 1310fcd 2022-07-03 23:35:20 +00:00
renovate[bot]
a093f24a9d Update ASF-ui digest to 0709f06 2022-07-03 14:19:14 +00:00
renovate[bot]
e6a51bae55 Update ASF-ui digest to 15c0ccf 2022-07-03 05:10:36 +00:00
ArchiBot
a992d0c3cd Automatic translations update 2022-07-03 02:40:51 +00:00
JustArchi
a0e5ae8f46 Bump 2022-07-03 01:22:24 +02:00
JustArchi
c4a46fbdde Misc 2022-07-03 01:22:02 +02:00
Łukasz Domeradzki
d899dbc18c Add NLog/File endpoint (#2639)
* Add log endpoint

* Update LogController.cs

* Address netf breaking

* Fixes & feedback

* THIS IS MADNESS

* Revert "THIS IS MADNESS"

This reverts commit 8359960314.

* Solve netf madness differently
2022-07-03 01:20:43 +02:00
renovate[bot]
04e14293ef Update wiki digest to 55fc787 2022-07-02 18:37:09 +00:00
renovate[bot]
18a1b0a883 Update ASF-ui digest to bcd2d66 2022-07-02 10:09:30 +00:00
ArchiBot
5ca028ef47 Automatic translations update 2022-07-02 02:40:27 +00:00
renovate[bot]
43ec8f9566 Update wiki digest to d78047f 2022-07-01 23:27:03 +00:00
JustArchi
32f5b3a1c5 Bump 2022-07-02 00:37:42 +02:00
JustArchi
196afbf276 Merge branch 'fix' 2022-07-02 00:32:45 +02:00
renovate[bot]
9825f007c0 Update ASF-ui digest to 319696a 2022-07-01 15:40:47 +00:00
JustArchi
bc38ba478d Fix Archi brain damage 2022-07-01 13:29:47 +02:00
JustArchi
2f22757fea Be more optimistic about session checks
We can't check for session with every request, allow at least 10 seconds of optimistic assumption as otherwise we're spamming the servers too much
2022-07-01 13:15:45 +02:00
JustArchi
3ff0468926 Avoid excessive 2FA delays when waitIfNeeded without specifying IDs 2022-07-01 13:13:39 +02:00
ArchiBot
a6a973468c Automatic translations update 2022-07-01 02:47:18 +00:00
renovate[bot]
4aa1604dfb Update ASF-ui digest to 8c7498a 2022-06-30 05:49:32 +00:00
ArchiBot
44c7fcd131 Automatic translations update 2022-06-30 02:41:51 +00:00
ArchiBot
ce610ab24d Automatic translations update 2022-06-29 02:42:10 +00:00
renovate[bot]
c76f17c5c7 Update ASF-ui digest to 955afdf 2022-06-28 06:05:35 +00:00
renovate[bot]
30b4e006dc Update ASF-ui digest to e519f42 2022-06-28 00:48:08 +00:00
renovate[bot]
39621ed46e Update ASF-ui digest to 70fe63b 2022-06-27 17:45:00 +00:00
JustArchi
e532b57369 Thanks netf 2022-06-27 14:32:50 +02:00
JustArchi
b117c5164d Misc 2022-06-27 14:17:38 +02:00
JustArchi
be5a6bc27a Misc 2022-06-27 12:28:06 +02:00
ArchiBot
0ecb04e62c Automatic translations update 2022-06-27 02:40:39 +00:00
renovate[bot]
0af9f99923 Update ASF-ui digest to 0992da0 2022-06-26 05:13:28 +00:00
ArchiBot
cd0078e83e Automatic translations update 2022-06-26 02:42:45 +00:00
renovate[bot]
10cedad0ee Update ASF-ui digest to a116352 2022-06-25 05:12:08 +00:00
ArchiBot
693f4edbe5 Automatic translations update 2022-06-25 02:41:53 +00:00
renovate[bot]
ed44ad030e Update ASF-ui digest to 3b53132 2022-06-24 22:04:31 +00:00
JustArchi
d338477e5c Very important CatAPI fixes 2022-06-24 23:19:31 +02:00
renovate[bot]
053cb5fc03 Update wiki digest to be4f1cc 2022-06-24 15:50:12 +00:00
renovate[bot]
a01ac6641e Update ASF-ui digest to e5cf327 2022-06-24 12:59:57 +00:00
renovate[bot]
3deb560e5e Update ASF-ui digest to b11aacc 2022-06-23 21:45:29 +00:00
JustArchi
1861add350 Bump 2022-06-23 10:45:51 +02:00
45 changed files with 1303 additions and 366 deletions

View File

@@ -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
# _ _
# | | (_) _ __ _ _ __ __
# | | | || '_ \ | | | |\ \/ /

View File

@@ -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" ]
}
]
}

View File

@@ -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'

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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 }}

View File

@@ -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 }}

View File

@@ -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)) {

View File

@@ -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

Submodule ASF-ui updated: 99278781c3...60a692f2e0

View File

@@ -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
}

View File

@@ -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));
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,37 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// |
// http://www.apache.org/licenses/LICENSE-2.0
// |
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
using 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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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();
}

View File

@@ -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));
}

View 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));
}
}

View 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>

View File

@@ -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 dauthentification 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 dutilisateur Steam : </value>
<comment>Please note that this translation should end with space</comment>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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">

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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]

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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

View File

@@ -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

Submodule wiki updated: 7dee92603f...e2ab1e7df2