mirror of
https://github.com/JustArchiNET/ArchiSteamFarm.git
synced 2025-12-31 13:40:46 +00:00
Compare commits
168 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22177f1fe7 | ||
|
|
ab1edae9c7 | ||
|
|
53d80345d3 | ||
|
|
d571cd9580 | ||
|
|
4106c5f41a | ||
|
|
71fa7560f7 | ||
|
|
32196a53cd | ||
|
|
5d467aca9a | ||
|
|
52eacaf577 | ||
|
|
d44c57a48f | ||
|
|
629a750df6 | ||
|
|
21b1a319ca | ||
|
|
bb1f02e788 | ||
|
|
5d8826b71b | ||
|
|
829e5ac4fb | ||
|
|
feec46da32 | ||
|
|
4288f35e99 | ||
|
|
69004d20ce | ||
|
|
91ce53d283 | ||
|
|
400ea64cfd | ||
|
|
1b047dfbff | ||
|
|
f9cd805304 | ||
|
|
74e2ad2f40 | ||
|
|
0242ed9f10 | ||
|
|
b31c8266eb | ||
|
|
ddd55fd4d9 | ||
|
|
a324a69371 | ||
|
|
de67195185 | ||
|
|
290f16b3fe | ||
|
|
324d208416 | ||
|
|
5db85aa6f1 | ||
|
|
4d46c4c4b1 | ||
|
|
f93a10bcd0 | ||
|
|
56c6246a20 | ||
|
|
2f4c82b563 | ||
|
|
8bd3c8b20d | ||
|
|
05c93aa82b | ||
|
|
e8babfb329 | ||
|
|
fd2d1baa1f | ||
|
|
4a5b8bbf3c | ||
|
|
5c8ebf7ec1 | ||
|
|
d972c93072 | ||
|
|
aa75f4204e | ||
|
|
5ad1815ba0 | ||
|
|
0518a35fb4 | ||
|
|
4dbd9720f2 | ||
|
|
4d06104306 | ||
|
|
5c6229da1b | ||
|
|
8b661874da | ||
|
|
24b4c9a2f1 | ||
|
|
9dd1dd227f | ||
|
|
d3b1213fc5 | ||
|
|
603663a43c | ||
|
|
57cb519c44 | ||
|
|
3eb6cbf491 | ||
|
|
0396687f50 | ||
|
|
714cc5ae9d | ||
|
|
4f478d829d | ||
|
|
d81144d549 | ||
|
|
f2563c582c | ||
|
|
927715188d | ||
|
|
03beeb97dd | ||
|
|
a3dcb252c7 | ||
|
|
52d4b6702c | ||
|
|
4e5cd2e380 | ||
|
|
af6eb07607 | ||
|
|
b5ecd31666 | ||
|
|
a2e5968a49 | ||
|
|
23da14e77e | ||
|
|
b2a5daad2c | ||
|
|
6c9142132c | ||
|
|
cd9a939fb4 | ||
|
|
4f8b1a542a | ||
|
|
230332ca79 | ||
|
|
b082024d0b | ||
|
|
af4a64b99e | ||
|
|
9163b57c3e | ||
|
|
69aaca3bad | ||
|
|
8272a2b929 | ||
|
|
4d0ba1dbcc | ||
|
|
bf46c29ff6 | ||
|
|
5fb3f88a52 | ||
|
|
74a193d3fe | ||
|
|
283bb789bb | ||
|
|
515f96ea8e | ||
|
|
b9308810d6 | ||
|
|
68be2d2442 | ||
|
|
d110259388 | ||
|
|
b03cf66882 | ||
|
|
480abe95d1 | ||
|
|
9408a16a5c | ||
|
|
a8c632f98d | ||
|
|
0016fc43a9 | ||
|
|
05006f0ba1 | ||
|
|
be0682e0f5 | ||
|
|
4b6c657ffc | ||
|
|
a8bef43a30 | ||
|
|
6767c879c6 | ||
|
|
67fdaa8314 | ||
|
|
2aed6d6143 | ||
|
|
94a18ed8dd | ||
|
|
3c9bb23957 | ||
|
|
ca418b5570 | ||
|
|
954e811584 | ||
|
|
6beda3f404 | ||
|
|
0cc6f4c40a | ||
|
|
2fcc22768c | ||
|
|
29f70918c2 | ||
|
|
3f5d21758c | ||
|
|
705e17c9bb | ||
|
|
2afc59f01a | ||
|
|
741df3c70f | ||
|
|
6c4d6c1e75 | ||
|
|
52aa398ec7 | ||
|
|
72c14c3180 | ||
|
|
78d2554e80 | ||
|
|
a46ec87252 | ||
|
|
4a5c1c15ce | ||
|
|
7fc609653a | ||
|
|
48077466ab | ||
|
|
4a76f84e3a | ||
|
|
3ab68cb574 | ||
|
|
daf616c0b3 | ||
|
|
2ea8c89037 | ||
|
|
bf91dc77e8 | ||
|
|
7778618b75 | ||
|
|
6cc78e805e | ||
|
|
f5b4383cdf | ||
|
|
ab5e950972 | ||
|
|
a3829f84b4 | ||
|
|
4e5fdabe0b | ||
|
|
1e786bdea3 | ||
|
|
27297f6f26 | ||
|
|
cfebe7bd50 | ||
|
|
8a2ee4e22b | ||
|
|
c66260954c | ||
|
|
adaa1416bb | ||
|
|
f3bc0162ef | ||
|
|
7a87131651 | ||
|
|
c945aca1b7 | ||
|
|
874b66d3af | ||
|
|
142347ad20 | ||
|
|
32dca1ce33 | ||
|
|
36e15eccf1 | ||
|
|
b00b297def | ||
|
|
0fbf31db89 | ||
|
|
5d7a1e33b6 | ||
|
|
3bcf09d42e | ||
|
|
7d19111b42 | ||
|
|
3667b62bee | ||
|
|
d9ff9d7a97 | ||
|
|
7ae60bdb3c | ||
|
|
ecbbca0215 | ||
|
|
534f84b181 | ||
|
|
894587b965 | ||
|
|
8f335f4182 | ||
|
|
750c56bf08 | ||
|
|
c84366f9ba | ||
|
|
837e4bde8f | ||
|
|
525a6f2d19 | ||
|
|
8719a2b98b | ||
|
|
a768ec43a5 | ||
|
|
385ea566aa | ||
|
|
df878185c2 | ||
|
|
f834ffe612 | ||
|
|
4e032fa129 | ||
|
|
6b116b5163 | ||
|
|
7ee92cb5b9 |
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
@@ -19,8 +19,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
show-progress: false
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup .NET Core
|
||||
@@ -39,7 +40,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@v1.12.0
|
||||
uses: crowdin/github-action@v1.13.1
|
||||
with:
|
||||
crowdin_branch_name: main
|
||||
config: '.github/crowdin.yml'
|
||||
|
||||
23
.github/workflows/code-quality.yml
vendored
23
.github/workflows/code-quality.yml
vendored
@@ -10,18 +10,35 @@ jobs:
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
checks: write
|
||||
contents: write
|
||||
pull-requests: write
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3.5.3
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Checkout code (for PR)
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
fetch-depth: 100 # History is required for pull request analysis
|
||||
ref: ${{ github.event.pull_request.head.sha }} # To check out the actual pull request commit, not the merge commit
|
||||
show-progress: false
|
||||
|
||||
- name: Run Qodana scan
|
||||
uses: JetBrains/qodana-action@v2023.2.1
|
||||
uses: JetBrains/qodana-action@v2023.2.8
|
||||
with:
|
||||
args: --property=idea.headless.enable.statistics=false
|
||||
env:
|
||||
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
|
||||
|
||||
- name: Report Qodana results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@v2.21.2
|
||||
uses: github/codeql-action/upload-sarif@v2.22.3
|
||||
with:
|
||||
sarif_file: ${{ runner.temp }}/qodana/results/qodana.sarif.json
|
||||
|
||||
7
.github/workflows/docker-ci.yml
vendored
7
.github/workflows/docker-ci.yml
vendored
@@ -17,15 +17,16 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
show-progress: false
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.9.1
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
|
||||
- name: Build ${{ matrix.configuration }} Docker image from ${{ matrix.file }}
|
||||
uses: docker/build-push-action@v4.1.1
|
||||
uses: docker/build-push-action@v5.0.0
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.file }}
|
||||
|
||||
11
.github/workflows/docker-publish-latest.yml
vendored
11
.github/workflows/docker-publish-latest.yml
vendored
@@ -15,22 +15,23 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
show-progress: false
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.9.1
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -55,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.Service
|
||||
uses: docker/build-push-action@v4.1.1
|
||||
uses: docker/build-push-action@v5.0.0
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.Service
|
||||
|
||||
11
.github/workflows/docker-publish-main.yml
vendored
11
.github/workflows/docker-publish-main.yml
vendored
@@ -16,22 +16,23 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
show-progress: false
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.9.1
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -55,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@v4.1.1
|
||||
uses: docker/build-push-action@v5.0.0
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ env.PLATFORMS }}
|
||||
|
||||
11
.github/workflows/docker-publish-released.yml
vendored
11
.github/workflows/docker-publish-released.yml
vendored
@@ -16,22 +16,23 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
show-progress: false
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.9.1
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2.2.0
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -56,7 +57,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@v4.1.1
|
||||
uses: docker/build-push-action@v5.0.0
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ env.PLATFORMS }}
|
||||
|
||||
25
.github/workflows/publish.yml
vendored
25
.github/workflows/publish.yml
vendored
@@ -16,12 +16,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
show-progress: false
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup Node.js with npm
|
||||
uses: actions/setup-node@v3.7.0
|
||||
uses: actions/setup-node@v3.8.1
|
||||
with:
|
||||
check-latest: true
|
||||
node-version: ${{ env.NODE_JS_VERSION }}
|
||||
@@ -39,7 +40,7 @@ jobs:
|
||||
run: npm run-script deploy --no-progress --prefix ASF-ui
|
||||
|
||||
- name: Upload ASF-ui
|
||||
uses: actions/upload-artifact@v3.1.2
|
||||
uses: actions/upload-artifact@v3.1.3
|
||||
with:
|
||||
name: ASF-ui
|
||||
path: ASF-ui/dist
|
||||
@@ -78,7 +79,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v3.2.0
|
||||
@@ -405,7 +408,7 @@ jobs:
|
||||
}
|
||||
|
||||
- name: Upload ASF-${{ matrix.variant }}
|
||||
uses: actions/upload-artifact@v3.1.2
|
||||
uses: actions/upload-artifact@v3.1.3
|
||||
with:
|
||||
name: ${{ matrix.os }}_ASF-${{ matrix.variant }}
|
||||
path: out/ASF-${{ matrix.variant }}.zip
|
||||
@@ -417,7 +420,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Download ASF-generic artifact from ubuntu-latest
|
||||
uses: actions/download-artifact@v3.0.2
|
||||
@@ -474,7 +479,7 @@ jobs:
|
||||
path: out
|
||||
|
||||
- name: Import GPG key for signing
|
||||
uses: crazy-max/ghaction-import-gpg@v5.3.0
|
||||
uses: crazy-max/ghaction-import-gpg@v6.0.0
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.ARCHIBOT_GPG_PRIVATE_KEY }}
|
||||
|
||||
@@ -491,19 +496,19 @@ jobs:
|
||||
)
|
||||
|
||||
- name: Upload SHA512SUMS
|
||||
uses: actions/upload-artifact@v3.1.2
|
||||
uses: actions/upload-artifact@v3.1.3
|
||||
with:
|
||||
name: SHA512SUMS
|
||||
path: out/SHA512SUMS
|
||||
|
||||
- name: Upload SHA512SUMS.sign
|
||||
uses: actions/upload-artifact@v3.1.2
|
||||
uses: actions/upload-artifact@v3.1.3
|
||||
with:
|
||||
name: SHA512SUMS.sign
|
||||
path: out/SHA512SUMS.sign
|
||||
|
||||
- name: Create ArchiSteamFarm GitHub release
|
||||
uses: ncipollo/release-action@v1.12.0
|
||||
uses: ncipollo/release-action@v1.13.0
|
||||
with:
|
||||
artifacts: "out/*"
|
||||
bodyFile: .github/RELEASE_TEMPLATE.md
|
||||
|
||||
11
.github/workflows/translations.yml
vendored
11
.github/workflows/translations.yml
vendored
@@ -10,8 +10,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
show-progress: false
|
||||
submodules: recursive
|
||||
token: ${{ secrets.ARCHIBOT_GITHUB_TOKEN }}
|
||||
|
||||
@@ -26,7 +27,7 @@ jobs:
|
||||
git reset --hard origin/master
|
||||
|
||||
- name: Download latest translations from Crowdin
|
||||
uses: crowdin/github-action@v1.12.0
|
||||
uses: crowdin/github-action@v1.13.1
|
||||
with:
|
||||
upload_sources: false
|
||||
download_translations: true
|
||||
@@ -38,7 +39,7 @@ jobs:
|
||||
token: ${{ secrets.ASF_CROWDIN_API_TOKEN }}
|
||||
|
||||
- name: Import GPG key for signing
|
||||
uses: crazy-max/ghaction-import-gpg@v5.3.0
|
||||
uses: crazy-max/ghaction-import-gpg@v6.0.0
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.ARCHIBOT_GPG_PRIVATE_KEY }}
|
||||
git_config_global: true
|
||||
@@ -59,7 +60,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Push changes to wiki
|
||||
uses: ad-m/github-push-action@v0.6.0
|
||||
uses: ad-m/github-push-action@v0.8.0
|
||||
with:
|
||||
github_token: ${{ secrets.ARCHIBOT_GITHUB_TOKEN }}
|
||||
branch: master
|
||||
@@ -78,7 +79,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Push changes to ASF
|
||||
uses: ad-m/github-push-action@v0.6.0
|
||||
uses: ad-m/github-push-action@v0.8.0
|
||||
with:
|
||||
github_token: ${{ secrets.ARCHIBOT_GITHUB_TOKEN }}
|
||||
branch: ${{ github.ref }}
|
||||
|
||||
2
ASF-ui
2
ASF-ui
Submodule ASF-ui updated: e01b29e635...1447dbe0e5
@@ -20,6 +20,8 @@
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Web;
|
||||
using ArchiSteamFarm.Web.Responses;
|
||||
@@ -30,15 +32,15 @@ namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin;
|
||||
// You've always wanted from your ASF to post cats, right? Now is your chance!
|
||||
// P.S. The code is almost 1:1 copy from the one I use in ArchiBot, you can thank me later
|
||||
internal static class CatAPI {
|
||||
private const string URL = "https://aws.random.cat";
|
||||
private const string URL = "https://api.thecatapi.com";
|
||||
|
||||
internal static async Task<Uri?> GetRandomCatURL(WebBrowser webBrowser) {
|
||||
ArgumentNullException.ThrowIfNull(webBrowser);
|
||||
|
||||
Uri request = new($"{URL}/meow");
|
||||
Uri request = new($"{URL}/v1/images/search");
|
||||
|
||||
ObjectResponse<MeowResponse>? response = await webBrowser.UrlGetToJsonObject<MeowResponse>(request).ConfigureAwait(false);
|
||||
ObjectResponse<ImmutableList<MeowResponse>>? response = await webBrowser.UrlGetToJsonObject<ImmutableList<MeowResponse>>(request).ConfigureAwait(false);
|
||||
|
||||
return response?.Content?.URL;
|
||||
return response?.Content?.FirstOrDefault()?.URL;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Composition;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Core;
|
||||
@@ -40,6 +41,7 @@ namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin;
|
||||
// Your plugin class should inherit the plugin interfaces it wants to handle
|
||||
// If you do not want to handle a particular action (e.g. OnBotMessage that is offered in IBotMessage), it's the best idea to not inherit it at all
|
||||
// This will keep your code compact, efficient and less dependent. You can always add additional interfaces when you'll need them, this example project will inherit quite a bit of them to show you potential usage
|
||||
[SuppressMessage("ReSharper", "MemberCanBeFileLocal")]
|
||||
internal sealed class ExamplePlugin : IASF, IBot, IBotCommand2, IBotConnection, IBotFriendRequest, IBotMessage, IBotModules, IBotTradeOffer {
|
||||
// This is used for identification purposes, typically you want to use a friendly name of your plugin here, such as the name of your main class
|
||||
// Please note that this property can have direct dependencies only on structures that were initialized by the constructor, as it's possible to be called before OnLoaded() takes place
|
||||
|
||||
@@ -28,7 +28,7 @@ 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)]
|
||||
[JsonProperty("url", Required = Required.Always)]
|
||||
internal readonly Uri URL = null!;
|
||||
|
||||
[JsonConstructor]
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
<comment>{0} will be replaced by number of sets traded</comment>
|
||||
</data>
|
||||
<data name="ListingAnnouncing" xml:space="preserve">
|
||||
<value>Gebe Benutzerkonto {0} ({1}) mit insgesamt aus {2} Gegenständen bestehendem Inventar bekannt...</value>
|
||||
<value>Benutzerkonto {0} ({1}) mit aus insgesamt {2} Gegenständen bestehendem Inventar wird angekündigt...</value>
|
||||
<comment>{0} will be replaced by steam ID (number), {1} will be replaced by user's nickname, {2} will be replaced with number of items in the inventory</comment>
|
||||
</data>
|
||||
<data name="MatchingFound" xml:space="preserve">
|
||||
@@ -75,11 +75,11 @@
|
||||
<comment>{0} will be replaced by number of items matched, {1} will be replaced by steam ID (number), {2} will be replaced by user's nickname</comment>
|
||||
</data>
|
||||
<data name="TradeOfferFailed" xml:space="preserve">
|
||||
<value>Fehler beim Senden eines Handelsangebots an Bot {0} ({1}), überspringe...</value>
|
||||
<value>Fehler beim Senden eines Handelsangebots an Bot {0} ({1}); überspringe...</value>
|
||||
<comment>{0} will be replaced by steam ID (number), {1} will be replaced by user's nickname'</comment>
|
||||
</data>
|
||||
<data name="ActivelyMatchingSomeConfirmationsFailed" xml:space="preserve">
|
||||
<value>Einige Bestätigungen sind fehlgeschlagen, etwa {0} von {1} Transaktionen wurden erfolgreich versendet.</value>
|
||||
<value>Einige Bestätigungen sind fehlgeschlagen. Lediglich {0} von {1} Transaktionen wurden erfolgreich versendet.</value>
|
||||
<comment>{0} will be replaced by amount of the trade offers that succeeded (number), {1} will be replaced by amount of the trade offers that were supposed to be sent in total (number)</comment>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
<comment>{0} will be replaced by steam ID (number), {1} will be replaced by user's nickname, {2} will be replaced with number of items in the inventory</comment>
|
||||
</data>
|
||||
<data name="MatchingFound" xml:space="preserve">
|
||||
<value>與 Bot {1}({2})匹配到共 {0} 個物品,正在發送交易提案…</value>
|
||||
<value>與 Bot {1}({2})比對到共 {0} 個物品,正在發送交易提案…</value>
|
||||
<comment>{0} will be replaced by number of items matched, {1} will be replaced by steam ID (number), {2} will be replaced by user's nickname</comment>
|
||||
</data>
|
||||
<data name="TradeOfferFailed" xml:space="preserve">
|
||||
|
||||
@@ -23,6 +23,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Composition;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Core;
|
||||
using ArchiSteamFarm.OfficialPlugins.MobileAuthenticator.Localization;
|
||||
@@ -35,6 +36,7 @@ using SteamKit2;
|
||||
namespace ArchiSteamFarm.OfficialPlugins.MobileAuthenticator;
|
||||
|
||||
[Export(typeof(IPlugin))]
|
||||
[SuppressMessage("ReSharper", "MemberCanBeFileLocal")]
|
||||
internal sealed class MobileAuthenticatorPlugin : OfficialPlugin, IBotCommand2, IBotSteamClient {
|
||||
[JsonProperty]
|
||||
public override string Name => nameof(MobileAuthenticatorPlugin);
|
||||
|
||||
@@ -67,11 +67,11 @@
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
<data name="PluginDisabledInConfig" xml:space="preserve">
|
||||
<value>{0} ist gemäß Ihrer Konfiguration derzeit deaktiviert. Wenn Sie SteamDB bei der Daten-Sammlung helfen möchten, sehen Sie sich bitte unser Wiki an.</value>
|
||||
<value>{0} ist gemäß ihrer Konfiguration derzeit deaktiviert. Wenn Sie SteamDB bei der Daten-Sammlung helfen möchten, sehen Sie sich bitte unser Wiki an.</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} wurde erfolgreich initialisiert. Wir danken Ihnen im Voraus für Ihre Hilfe. Die erste Übermittlung wird in etwa {1} ab jetzt erfolgen.</value>
|
||||
<value>{0} wurde erfolgreich initialisiert. Wir danken ihnen im Voraus für ihre Hilfe. Die erste Übermittlung wird in etwa {1} ab jetzt erfolgen.</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">
|
||||
|
||||
@@ -109,7 +109,10 @@
|
||||
<value>Hai completato il recupero di {0} informazioni app.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
|
||||
</data>
|
||||
|
||||
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Recuperate {0} chiavi depot su {1} con successo.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys that were successfully retrieved, {1} will be replaced by the number (count this batch) of depot keys that were supposed to be retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Finito il recupero di tutte le chiavi del deposito per un totale di {0} applicazioni.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
|
||||
@@ -93,7 +93,10 @@
|
||||
<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>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -227,6 +227,7 @@
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MissingIndent/@EntryIndexedValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MissingLinebreak/@EntryIndexedValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MissingSpace/@EntryIndexedValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MoveLocalFunctionAfterJumpStatement/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MoveToExistingPositionalDeconstructionPattern/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MultipleSpaces/@EntryIndexedValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MultipleStatementsOnOneLine/@EntryIndexedValue">WARNING</s:String>
|
||||
@@ -246,6 +247,7 @@
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=PatternAlwaysOfType/@EntryIndexedValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=PlaceAssignmentExpressionIntoBlock/@EntryIndexedValue">DO_NOT_SHOW</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=PropertyNotResolved/@EntryIndexedValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RawStringCanBeSimplified/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantArrayCreationExpression/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantAttributeParentheses/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantBlankLines/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
@@ -279,6 +281,7 @@
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RouteTemplates_002EParameterConstraintCanBeSpecified/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RouteTemplates_002ERouteParameterIsNotPassedToMethod/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=SeparateControlTransferStatement/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=SeparateLocalFunctionsWithJumpStatement/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=SimilarAnonymousTypeNearby/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=SpecifyStringComparison/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=StringEndsWithIsCultureSpecific/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
@@ -314,9 +317,11 @@
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseNullPropagation/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseNullPropagationWhenPossible/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UsePositionalDeconstructionPattern/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseRawString/@EntryIndexedValue">DO_NOT_SHOW</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseStringInterpolationWhenPossible/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseThrowIfNullMethod/@EntryIndexedValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseVerbatimString/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UsingStatementResourceInitializationExpression/@EntryIndexedValue">SUGGESTION</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=WrongIndentSize/@EntryIndexedValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=WrongMetadataUse/@EntryIndexedValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=Xaml_002EIgnoredPathHighlighting/@EntryIndexedValue">WARNING</s:String>
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" />
|
||||
<PackageReference Include="System.Composition" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" />
|
||||
<PackageReference Include="System.Linq.Async" />
|
||||
<PackageReference Include="zxcvbn-core" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -39,6 +39,9 @@ public sealed class ObservableConcurrentDictionary<TKey, TValue> : IDictionary<T
|
||||
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
[PublicAPI]
|
||||
public ICollection<TKey> Keys => BackingDictionary.Keys;
|
||||
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
private readonly ConcurrentDictionary<TKey, TValue> BackingDictionary = new();
|
||||
|
||||
|
||||
@@ -580,6 +580,28 @@ public static class ASF {
|
||||
await OnCreatedKeysFile(name, fullPath).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async Task OnConfigChanged() {
|
||||
string globalConfigFile = GetFilePath(EFileType.Config);
|
||||
|
||||
if (string.IsNullOrEmpty(globalConfigFile)) {
|
||||
throw new InvalidOperationException(nameof(globalConfigFile));
|
||||
}
|
||||
|
||||
(GlobalConfig? globalConfig, _) = await GlobalConfig.Load(globalConfigFile).ConfigureAwait(false);
|
||||
|
||||
if (globalConfig == null) {
|
||||
// Invalid config file, we allow user to fix it without destroying the ASF instance right away
|
||||
return;
|
||||
}
|
||||
|
||||
if (globalConfig == GlobalConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericInfo(Strings.GlobalConfigChanged);
|
||||
await RestartOrExit().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async void OnCreated(object sender, FileSystemEventArgs e) {
|
||||
ArgumentNullException.ThrowIfNull(sender);
|
||||
ArgumentNullException.ThrowIfNull(e);
|
||||
@@ -666,8 +688,7 @@ public static class ASF {
|
||||
}
|
||||
|
||||
if (botName.Equals(SharedInfo.ASF, StringComparison.OrdinalIgnoreCase)) {
|
||||
ArchiLogger.LogGenericInfo(Strings.GlobalConfigChanged);
|
||||
await RestartOrExit().ConfigureAwait(false);
|
||||
await OnConfigChanged().ConfigureAwait(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
@@ -48,6 +49,8 @@ public static class Utilities {
|
||||
// normally we'd just use words like "steam" and "farm", but the library we're currently using is a bit iffy about banned words, so we need to also add combinations such as "steamfarm"
|
||||
private static readonly ImmutableHashSet<string> ForbiddenPasswordPhrases = ImmutableHashSet.Create(StringComparer.InvariantCultureIgnoreCase, "archisteamfarm", "archi", "steam", "farm", "archisteam", "archifarm", "steamfarm", "asf", "asffarm", "password");
|
||||
|
||||
private static readonly JwtSecurityTokenHandler JwtSecurityTokenHandler = new();
|
||||
|
||||
[PublicAPI]
|
||||
public static string GetArgsAsText(string[] args, byte argsToSkip, string delimiter) {
|
||||
ArgumentNullException.ThrowIfNull(args);
|
||||
@@ -112,7 +115,7 @@ public static class Utilities {
|
||||
public static void InBackground<T>(Func<T> function, bool longRunning = false) {
|
||||
ArgumentNullException.ThrowIfNull(function);
|
||||
|
||||
InBackground(void() => function(), longRunning);
|
||||
InBackground(void () => function(), longRunning);
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
@@ -187,6 +190,21 @@ public static class Utilities {
|
||||
return (text.Length % 2 == 0) && text.All(Uri.IsHexDigit);
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public static JwtSecurityToken? ReadJwtToken(string token) {
|
||||
if (string.IsNullOrEmpty(token)) {
|
||||
throw new ArgumentNullException(nameof(token));
|
||||
}
|
||||
|
||||
try {
|
||||
return JwtSecurityTokenHandler.ReadJwtToken(token);
|
||||
} catch (Exception e) {
|
||||
ASF.ArchiLogger.LogGenericException(e);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public static IList<INode> SelectNodes(this IDocument document, string xpath) {
|
||||
ArgumentNullException.ThrowIfNull(document);
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Core;
|
||||
@@ -54,7 +53,7 @@ public sealed class CommandController : ArchiController {
|
||||
return BadRequest(new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(request.Command))));
|
||||
}
|
||||
|
||||
Bot? targetBot = Bot.Bots?.OrderBy(static bot => bot.Key, Bot.BotsComparer).Select(static bot => bot.Value).FirstOrDefault();
|
||||
Bot? targetBot = Bot.GetDefaultBot();
|
||||
|
||||
if (targetBot == null) {
|
||||
return BadRequest(new GenericResponse(false, Strings.ErrorNoBotsDefined));
|
||||
|
||||
@@ -195,23 +195,23 @@ StackTrace:
|
||||
<comment>Please note that this translation should end with space</comment>
|
||||
</data>
|
||||
<data name="UserInputSteam2FA" xml:space="preserve">
|
||||
<value>Bitte geben Sie Ihren 2FA-Code aus Ihrer Steam-Authentifizierungsapp ein: </value>
|
||||
<value>Bitte geben Sie ihren 2FA-Code aus ihrer Steam-Authentifizierungsapp ein: </value>
|
||||
<comment>Please note that this translation should end with space</comment>
|
||||
</data>
|
||||
<data name="UserInputSteamGuard" xml:space="preserve">
|
||||
<value>Bitte geben Sie den SteamGuard Authentifizierungstoken ein, der an Ihre E-Mail Adresse geschickt wurde: </value>
|
||||
<value>Bitte geben Sie den SteamGuard Authentifizierungstoken ein, der an ihre E-Mail Adresse geschickt wurde: </value>
|
||||
<comment>Please note that this translation should end with space</comment>
|
||||
</data>
|
||||
<data name="UserInputSteamLogin" xml:space="preserve">
|
||||
<value>Bitte geben Sie Ihren Steam-Benutzernamen ein: </value>
|
||||
<value>Bitte geben Sie ihren Steam-Benutzernamen ein: </value>
|
||||
<comment>Please note that this translation should end with space</comment>
|
||||
</data>
|
||||
<data name="UserInputSteamParentalCode" xml:space="preserve">
|
||||
<value>Bitte geben Sie Ihre Steam-Familienansicht-PIN ein: </value>
|
||||
<value>Bitte geben Sie ihre Steam-Familienansicht-PIN ein: </value>
|
||||
<comment>Please note that this translation should end with space</comment>
|
||||
</data>
|
||||
<data name="UserInputSteamPassword" xml:space="preserve">
|
||||
<value>Bitte geben Sie Ihr Steam-Passwort ein: </value>
|
||||
<value>Bitte geben Sie ihr Steam-Passwort ein: </value>
|
||||
<comment>Please note that this translation should end with space</comment>
|
||||
</data>
|
||||
<data name="WarningUnknownValuePleaseReport" xml:space="preserve">
|
||||
@@ -374,7 +374,7 @@ StackTrace:
|
||||
<value>Bot-Instanz nicht gestartet, weil diese in der Konfigurationsdatei deaktiviert ist!</value>
|
||||
</data>
|
||||
<data name="BotInvalidAuthenticatorDuringLogin" xml:space="preserve">
|
||||
<value>Der TwoFactorCodeMismatch-Fehlercode wurde {0} Mal in Folge empfangen. Entweder sind Ihre 2FA-Anmedeinformationen nicht mehr gültig oder die Systemuhr ist nicht synchronisiert. Der Vorgang wird abgebrochen!</value>
|
||||
<value>Der TwoFactorCodeMismatch-Fehlercode wurde {0} Mal in Folge empfangen. Entweder sind ihre 2FA-Anmedeinformationen nicht mehr gültig oder die Systemuhr ist nicht synchronisiert. Der Vorgang wird abgebrochen!</value>
|
||||
<comment>{0} will be replaced by maximum allowed number of failed 2FA attempts</comment>
|
||||
</data>
|
||||
<data name="BotLoggedOff" xml:space="preserve">
|
||||
@@ -404,7 +404,7 @@ StackTrace:
|
||||
<value>Sie können nicht mit sich selbst handeln!</value>
|
||||
</data>
|
||||
<data name="BotNoASFAuthenticator" xml:space="preserve">
|
||||
<value>Dieser Bot hat ASF-2FA nicht aktiviert! Haben Sie vergessen, Ihren Authentifikator als ASF-2FA zu importieren?</value>
|
||||
<value>Dieser Bot hat ASF-2FA nicht aktiviert! Haben Sie vergessen, ihren Authentifikator als ASF-2FA zu importieren?</value>
|
||||
</data>
|
||||
<data name="BotNotConnected" xml:space="preserve">
|
||||
<value>Diese Bot-Instanz ist nicht verbunden!</value>
|
||||
@@ -516,7 +516,7 @@ StackTrace:
|
||||
<value>Ihre angegebene CurrentCulture ist ungültig, ASF wird weiterhin mit dem Standard ausgeführt!</value>
|
||||
</data>
|
||||
<data name="TranslationIncomplete" xml:space="preserve">
|
||||
<value>ASF wird versuchen, Ihre bevorzugte Sprache {0} zu verwenden, jedoch wurde die Übersetzung in dieser Sprache nur zu {1} abgeschlossen. Können Sie uns vielleicht helfen, die ASF-Übersetzung in Ihrer Sprache zu verbessern?</value>
|
||||
<value>ASF wird versuchen, ihre bevorzugte Sprache {0} zu verwenden, jedoch wurde die Übersetzung in dieser Sprache nur zu {1} abgeschlossen. Können Sie uns vielleicht helfen, die ASF-Übersetzung in ihrer Sprache zu verbessern?</value>
|
||||
<comment>{0} will be replaced by culture code, such as "en-US", {1} will be replaced by completeness percentage, such as "78.5%"</comment>
|
||||
</data>
|
||||
<data name="IdlingGameNotPossible" xml:space="preserve">
|
||||
@@ -548,7 +548,7 @@ StackTrace:
|
||||
<value>Zugriff verweigert!</value>
|
||||
</data>
|
||||
<data name="WarningPreReleaseVersion" xml:space="preserve">
|
||||
<value>Sie verwenden eine Version, die neuer ist als die zuletzt veröffentlichte Version Ihres Aktualisierungskanals. Bitte bedenken Sie, dass Vorabversionen nur für Benutzer gedacht sind, die wissen wie man Fehler meldet, mit Problemen umgeht und Rückmeldung gibt - es wird keine technische Unterstützung geben.</value>
|
||||
<value>Sie verwenden eine Version, die neuer ist als die zuletzt veröffentlichte Version ihres Aktualisierungskanals. Bitte bedenken Sie, dass Vorabversionen nur für Benutzer gedacht sind, die wissen wie man Fehler meldet, mit Problemen umgeht und Rückmeldung gibt - es wird keine technische Unterstützung geben.</value>
|
||||
</data>
|
||||
<data name="BotStats" xml:space="preserve">
|
||||
<value>Aktuelle Speichernutzung: {0} MB.
|
||||
@@ -666,7 +666,7 @@ Prozesslaufzeit: {1}</value>
|
||||
<comment>{0} will be replaced by trade offer ID (number), {1} will be replaced by internal ASF enum name, {2} will be replaced by technical reason why the trade was determined to be in this state</comment>
|
||||
</data>
|
||||
<data name="BotInvalidPasswordDuringLogin" xml:space="preserve">
|
||||
<value>Fehlercode InvalidPassword {0} mal hintereinander erhalten. Ihr Passwort für dieses Konto ist höchstwahrscheinlich falsch. Abbruch!</value>
|
||||
<value>Fehlercode InvalidPassword {0} mal hintereinander erhalten. ihr Passwort für dieses Konto ist höchstwahrscheinlich falsch. Abbruch!</value>
|
||||
<comment>{0} will be replaced by maximum allowed number of failed login attempts</comment>
|
||||
</data>
|
||||
<data name="Result" xml:space="preserve">
|
||||
@@ -716,10 +716,10 @@ Prozesslaufzeit: {1}</value>
|
||||
<comment>{0} will be replaced by the name of a particular setting (e.g. "AES"), {1} will be replaced by the name of the property (e.g. "SteamPassword")</comment>
|
||||
</data>
|
||||
<data name="WarningRunningAsRoot" xml:space="preserve">
|
||||
<value>Sie versuchen ASF als Administrator (root) auszuführen. Dies stellt ein signifikantes Sicherheitsrisiko für Ihr Gerät dar und da ASF diese Rechte nicht benötigt, unterstützen wir dieses Szenario nicht. Verwenden Sie das Kommandozeilenargument --ignore-unsupported-environment, wenn Sie wirklich wissen, was Sie tun.</value>
|
||||
<value>Sie versuchen ASF als Administrator (root) auszuführen. Dies stellt ein signifikantes Sicherheitsrisiko für ihr Gerät dar und da ASF diese Rechte nicht benötigt, unterstützen wir dieses Szenario nicht. Verwenden Sie das Kommandozeilenargument --ignore-unsupported-environment, wenn Sie wirklich wissen, was Sie tun.</value>
|
||||
</data>
|
||||
<data name="WarningRunningInUnsupportedEnvironment" xml:space="preserve">
|
||||
<value>Sie nutzen ASF in einer nicht unterstützten Umgebung und verwenden das Argument --ignore-unsupported-environment. Bitte beachten Sie, dass wir für dieses Szenario keinerlei Unterstützung anbieten und Sie das Risiko vollständig bei Ihnen liegt. Sie wurden gewarnt.</value>
|
||||
<value>Sie nutzen ASF in einer nicht unterstützten Umgebung und verwenden das Argument --ignore-unsupported-environment. Bitte beachten Sie, dass wir für dieses Szenario keinerlei Unterstützung anbieten und Sie das Risiko vollständig bei ihnen liegt. Sie wurden gewarnt.</value>
|
||||
</data>
|
||||
<data name="FetchingChecksumFromRemoteServer" xml:space="preserve">
|
||||
<value>Rufe Prüfsumme vom Server ab...</value>
|
||||
@@ -737,7 +737,7 @@ Prozesslaufzeit: {1}</value>
|
||||
<value>Aktualisiere ASF-Dateien...</value>
|
||||
</data>
|
||||
<data name="UserInputCryptkey" xml:space="preserve">
|
||||
<value>Bitte gib deinen kryptographischen Schlüssel ein: </value>
|
||||
<value>Bitte gib deinen kryptografiischen Schlüssel ein: </value>
|
||||
<comment>Please note that this translation should end with space</comment>
|
||||
</data>
|
||||
<data name="ErrorIPNotBanned" xml:space="preserve">
|
||||
@@ -745,7 +745,7 @@ Prozesslaufzeit: {1}</value>
|
||||
<comment>{0} will be replaced by an IP address which was requested to be unbanned from using IPC</comment>
|
||||
</data>
|
||||
<data name="WarningNoLicense" xml:space="preserve">
|
||||
<value>Sie haben versucht, die kostenpflichtige Funktion {0} zu verwenden, aber Sie haben keine gültige Lizenz-ID in der globalen ASF Konfiguration gesetzt. Bitte überprüfen Sie Ihre Konfiguration, da die Funktionalität ohne zusätzliche Details nicht funktioniert.</value>
|
||||
<value>Sie haben versucht, die kostenpflichtige Funktion {0} zu verwenden, aber Sie haben keine gültige Lizenz-ID in der globalen ASF Konfiguration gesetzt. Bitte überprüfen Sie ihre Konfiguration, da die Funktionalität ohne zusätzliche Details nicht funktioniert.</value>
|
||||
<comment>{0} will be replaced by feature name (e.g. MatchActively)</comment>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -188,7 +188,10 @@
|
||||
<value>Versione locale: {0} | Versione remota: {1}</value>
|
||||
<comment>{0} will be replaced by current version, {1} will be replaced by remote version</comment>
|
||||
</data>
|
||||
|
||||
<data name="UserInputDeviceConfirmation" xml:space="preserve">
|
||||
<value>Controlla la tua app mobile Steam, potresti aver ricevuto una notifica di approvazione per il login. Premi Y se hai ricevuto e approvato la notifica, N se vuoi fornire il codice: </value>
|
||||
<comment>Please note that this translation should end with space</comment>
|
||||
</data>
|
||||
<data name="UserInputSteam2FA" xml:space="preserve">
|
||||
<value>Inserisci il codice 2FA dell'autenticatore mobile di Steam: </value>
|
||||
<comment>Please note that this translation should end with space</comment>
|
||||
@@ -731,7 +734,16 @@ Tempo di attività: {1}</value>
|
||||
<data name="PatchingFiles" xml:space="preserve">
|
||||
<value>Patching dei file ASF...</value>
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
<data name="UserInputCryptkey" xml:space="preserve">
|
||||
<value>Inserisci la tua cryptkey: </value>
|
||||
<comment>Please note that this translation should end with space</comment>
|
||||
</data>
|
||||
<data name="ErrorIPNotBanned" xml:space="preserve">
|
||||
<value>L'indirizzo IP {0} non è bannato!</value>
|
||||
<comment>{0} will be replaced by an IP address which was requested to be unbanned from using IPC</comment>
|
||||
</data>
|
||||
<data name="WarningNoLicense" xml:space="preserve">
|
||||
<value>Hai tentato di utilizzare la funzione a pagamento {0} ma non hai un valido LicenseID impostato nella configurazione globale di ASF. Controlla la tua configurazione, poiché la funzionalità non funzionerà senza ulteriori dettagli.</value>
|
||||
<comment>{0} will be replaced by feature name (e.g. MatchActively)</comment>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -104,7 +104,9 @@ StackTrace:
|
||||
<value>{0} är ogiltig!</value>
|
||||
<comment>{0} will be replaced by object's name</comment>
|
||||
</data>
|
||||
|
||||
<data name="ErrorNoBotsDefined" xml:space="preserve">
|
||||
<value>Inga bottar är definierade. Glömde du att konfigurera ASF? Följ 'setting up' guiden på wikin om du är förvirrad.</value>
|
||||
</data>
|
||||
<data name="ErrorObjectIsNull" xml:space="preserve">
|
||||
<value>{0} är null!</value>
|
||||
<comment>{0} will be replaced by object's name</comment>
|
||||
@@ -189,17 +191,26 @@ StackTrace:
|
||||
<value>Lokal version: {0} | Senaste version: {1}</value>
|
||||
<comment>{0} will be replaced by current version, {1} will be replaced by remote version</comment>
|
||||
</data>
|
||||
|
||||
<data name="UserInputDeviceConfirmation" xml:space="preserve">
|
||||
<value>Vänligen kontrollera din Steam mobilapp, du bör ha mottagit ett inloggningsgodkännande. Skriv Y ifall du har fått och godkänt notifikationen, och skriv N ifall du vill ange koden istället: </value>
|
||||
<comment>Please note that this translation should end with space</comment>
|
||||
</data>
|
||||
<data name="UserInputSteam2FA" xml:space="preserve">
|
||||
<value>Vänligen ange din 2FA kod från din Steam authenticator-app: </value>
|
||||
<comment>Please note that this translation should end with space</comment>
|
||||
</data>
|
||||
|
||||
<data name="UserInputSteamGuard" xml:space="preserve">
|
||||
<value>Vänligen ange SteamGuard-koden som skickades till din e-post: </value>
|
||||
<comment>Please note that this translation should end with space</comment>
|
||||
</data>
|
||||
<data name="UserInputSteamLogin" xml:space="preserve">
|
||||
<value>Vänligen ange din Steam-inloggning: </value>
|
||||
<comment>Please note that this translation should end with space</comment>
|
||||
</data>
|
||||
|
||||
<data name="UserInputSteamParentalCode" xml:space="preserve">
|
||||
<value>Vänligen ange Steam Familjevy-PIN: </value>
|
||||
<comment>Please note that this translation should end with space</comment>
|
||||
</data>
|
||||
<data name="UserInputSteamPassword" xml:space="preserve">
|
||||
<value>Vänligen ange ditt Steam lösenord: </value>
|
||||
<comment>Please note that this translation should end with space</comment>
|
||||
@@ -211,7 +222,9 @@ StackTrace:
|
||||
<data name="IPCReady" xml:space="preserve">
|
||||
<value>IPC servern är redo!</value>
|
||||
</data>
|
||||
|
||||
<data name="IPCStarting" xml:space="preserve">
|
||||
<value>Startar IPC servern på...</value>
|
||||
</data>
|
||||
<data name="BotAlreadyStopped" xml:space="preserve">
|
||||
<value>Bot-instansen har redan stoppats!</value>
|
||||
</data>
|
||||
@@ -219,25 +232,53 @@ StackTrace:
|
||||
<value>Kunde inte hitta någon bot vid namn {0}!</value>
|
||||
<comment>{0} will be replaced by bot's name query (string)</comment>
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
<data name="BotStatusOverview" xml:space="preserve">
|
||||
<value>{0}/{1} bottar körs för tillfället, med totalt {2} spel ({3} kort) kvar.</value>
|
||||
<comment>{0} will be replaced by number of active bots, {1} will be replaced by total number of bots, {2} will be replaced by total number of games left to farm, {3} will be replaced by total number of cards left to farm</comment>
|
||||
</data>
|
||||
<data name="BotStatusIdling" xml:space="preserve">
|
||||
<value>Bot farmar spel: {0} ({1}, {2} kort-drops kvar) från totalt {3} spel ({4} kort) kvar att farma (~{5} kvar).</value>
|
||||
<comment>{0} will be replaced by game's ID (number), {1} will be replaced by game's name, {2} will be replaced by number of cards left to farm, {3} will be replaced by total number of games to farm, {4} will be replaced by total number of cards to farm, {5} will be replaced by translated TimeSpan string (such as "1 day, 5 hours and 30 minutes")</comment>
|
||||
</data>
|
||||
<data name="BotStatusIdlingList" xml:space="preserve">
|
||||
<value>Bot farmar spel: {0} från totalt {1} spel ({2} kort) kvar att farma (~{3} kvar).</value>
|
||||
<comment>{0} will be replaced by list of the games (IDs, numbers), {1} will be replaced by total number of games to farm, {2} will be replaced by total number of cards to farm, {3} will be replaced by translated TimeSpan string (such as "1 day, 5 hours and 30 minutes")</comment>
|
||||
</data>
|
||||
<data name="CheckingFirstBadgePage" xml:space="preserve">
|
||||
<value>Kollar första märkessidan...</value>
|
||||
</data>
|
||||
<data name="CheckingOtherBadgePages" xml:space="preserve">
|
||||
<value>Kollar andra märkessidor...</value>
|
||||
</data>
|
||||
|
||||
<data name="ChosenFarmingAlgorithm" xml:space="preserve">
|
||||
<value>Vald farmnings algoritm: {0}</value>
|
||||
<comment>{0} will be replaced by the name of chosen farming algorithm</comment>
|
||||
</data>
|
||||
<data name="Done" xml:space="preserve">
|
||||
<value>Klart!</value>
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<data name="GamesToIdle" xml:space="preserve">
|
||||
<value>Vi har totalt {0} spel ({1} kort) kvar att farma (~{2} kvar)...</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="IdlingFinished" xml:space="preserve">
|
||||
<value>Farmande färdigt!</value>
|
||||
</data>
|
||||
<data name="IdlingFinishedForGame" xml:space="preserve">
|
||||
<value>Farmandet slutförd: {0} ({1}) efter {2} speltid!</value>
|
||||
<comment>{0} will be replaced by game's ID (number), {1} will be replaced by game's name, {2} will be replaced by translated TimeSpan string (such as "1 day, 5 hours and 30 minutes")</comment>
|
||||
</data>
|
||||
<data name="IdlingFinishedForGames" xml:space="preserve">
|
||||
<value>Färdig med att farma spel: {0}</value>
|
||||
<comment>{0} will be replaced by list of the games (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="IdlingStatusForGame" xml:space="preserve">
|
||||
<value>Farmstatus för {0} ({1}): {2} kort kvar</value>
|
||||
<comment>{0} will be replaced by game's ID (number), {1} will be replaced by game's name, {2} will be replaced by number of cards left to farm</comment>
|
||||
</data>
|
||||
<data name="IdlingStopped" xml:space="preserve">
|
||||
<value>Farmandet stoppad!</value>
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -190,7 +190,10 @@
|
||||
<value>Ваша версія: {0} | Остання версія: {1}</value>
|
||||
<comment>{0} will be replaced by current version, {1} will be replaced by remote version</comment>
|
||||
</data>
|
||||
|
||||
<data name="UserInputDeviceConfirmation" xml:space="preserve">
|
||||
<value>Будь ласка, перевірте свій мобільний додаток Steam, ви мали отримати сповіщення про підтвердження входу. Введіть Y, якщо ви отримали та схвалили сповіщення, N, якщо замість цього хочете надати код: </value>
|
||||
<comment>Please note that this translation should end with space</comment>
|
||||
</data>
|
||||
<data name="UserInputSteam2FA" xml:space="preserve">
|
||||
<value>Будь ласка, введіть ваш код 2FA з додатку для автентифікації у Steam: </value>
|
||||
<comment>Please note that this translation should end with space</comment>
|
||||
|
||||
@@ -71,25 +71,26 @@
|
||||
<comment>{0} will be replaced by translated TimeSpan string (such as "24 hours")</comment>
|
||||
</data>
|
||||
<data name="Content" xml:space="preserve">
|
||||
<value>內容︰{0}</value>
|
||||
<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>
|
||||
<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>
|
||||
<value>ASF V{0} 在能初始化核心紀錄模組之前就遇到嚴重錯誤!</value>
|
||||
<comment>{0} will be replaced by version number</comment>
|
||||
</data>
|
||||
<data name="ErrorEarlyFatalExceptionPrint" xml:space="preserve">
|
||||
<value>例外錯誤:{0}() {1}
|
||||
堆疊追蹤:
|
||||
堆疊追踪:
|
||||
{2}</value>
|
||||
<comment>{0} will be replaced by function name, {1} will be replaced by exception message, {2} will be replaced by entire stack trace. Please note that this string should include newlines for formatting.</comment>
|
||||
</data>
|
||||
<data name="ErrorExitingWithNonZeroErrorCode" xml:space="preserve">
|
||||
<value>以非零錯誤代碼退出!</value>
|
||||
<value>以非零錯誤碼退出!</value>
|
||||
</data>
|
||||
<data name="ErrorFailingRequest" xml:space="preserve">
|
||||
<value>請求失敗︰ {0}</value>
|
||||
@@ -104,14 +105,14 @@
|
||||
<comment>{0} will be replaced by object's name</comment>
|
||||
</data>
|
||||
<data name="ErrorNoBotsDefined" xml:space="preserve">
|
||||
<value>沒有設定任何 Bot。您是否忘記設定 ASF 了?如果您不明白發生了什麼,請閱讀 Wiki 上的「設定指南」。</value>
|
||||
<value>沒有設定任何 Bot。您是否忘記設定 ASF 了?如果您不明白發生了什麼,請閱讀 Wiki 上的「新手上路」。</value>
|
||||
</data>
|
||||
<data name="ErrorObjectIsNull" xml:space="preserve">
|
||||
<value>{0} 為空值!</value>
|
||||
<comment>{0} will be replaced by object's name</comment>
|
||||
</data>
|
||||
<data name="ErrorParsingObject" xml:space="preserve">
|
||||
<value>剖析 {0} 失敗!</value>
|
||||
<value>無法剖析 {0}!</value>
|
||||
<comment>{0} will be replaced by object's name</comment>
|
||||
</data>
|
||||
<data name="ErrorRequestFailedTooManyTimes" xml:space="preserve">
|
||||
@@ -128,7 +129,7 @@
|
||||
<value>無法繼續進行更新,因為該版本並未提供任何資源檔案!</value>
|
||||
</data>
|
||||
<data name="ErrorUserInputRunningInHeadlessMode" xml:space="preserve">
|
||||
<value>收到一個使用者輸入請求,但進程目前正以無介面模式執行!</value>
|
||||
<value>收到一個使用者輸入請求,但程序目前正以無頭模式執行!</value>
|
||||
</data>
|
||||
<data name="Exiting" xml:space="preserve">
|
||||
<value>正在退出…</value>
|
||||
@@ -137,7 +138,7 @@
|
||||
<value>失敗!</value>
|
||||
</data>
|
||||
<data name="GlobalConfigChanged" xml:space="preserve">
|
||||
<value>已變更全域設定檔!</value>
|
||||
<value>已修改全域設定檔!</value>
|
||||
</data>
|
||||
<data name="ErrorGlobalConfigRemoved" xml:space="preserve">
|
||||
<value>已刪除全域設定檔!</value>
|
||||
@@ -154,7 +155,7 @@
|
||||
<value>沒有執行中的 Bot,正在退出…</value>
|
||||
</data>
|
||||
<data name="RefreshingOurSession" xml:space="preserve">
|
||||
<value>更新工作階段!</value>
|
||||
<value>正在更新工作階段!</value>
|
||||
</data>
|
||||
<data name="RejectingTrade" xml:space="preserve">
|
||||
<value>正在拒絕交易︰{0}</value>
|
||||
@@ -176,7 +177,7 @@
|
||||
<value>正在檢查新版本…</value>
|
||||
</data>
|
||||
<data name="UpdateDownloadingNewVersion" xml:space="preserve">
|
||||
<value>正在下載新版本:{0}({1} MB)…等待期間,若您喜歡這個軟體,請考慮捐助 ASF!:)</value>
|
||||
<value>正在下載新版本:{0} ({1} MB)…等待期間,若您喜歡這個軟體,請考慮贊助 ASF!:)</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">
|
||||
@@ -194,23 +195,23 @@
|
||||
<comment>Please note that this translation should end with space</comment>
|
||||
</data>
|
||||
<data name="UserInputSteam2FA" xml:space="preserve">
|
||||
<value>請輸入您的 Steam Guard 行動驗證器上的雙重驗證代碼: </value>
|
||||
<value>請輸入您的 Steam 行動驗證器上的雙重驗證代碼:</value>
|
||||
<comment>Please note that this translation should end with space</comment>
|
||||
</data>
|
||||
<data name="UserInputSteamGuard" xml:space="preserve">
|
||||
<value>請輸入寄送至您的電子信箱的 Steam Guard 驗證代碼: </value>
|
||||
<value>請輸入寄送至您的電子郵件的 Steam Guard 驗證碼:</value>
|
||||
<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">
|
||||
<value>請輸入 Steam 家庭監護碼: </value>
|
||||
<value>請輸入 Steam 家庭監護 PIN 碼:</value>
|
||||
<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">
|
||||
@@ -292,7 +293,7 @@
|
||||
<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>
|
||||
<value>目前無法執行掛卡,我們將稍後再試!</value>
|
||||
</data>
|
||||
<data name="StillIdling" xml:space="preserve">
|
||||
<value>仍在掛卡:{0}({1})</value>
|
||||
@@ -352,13 +353,13 @@
|
||||
<value>已暫停自動掛卡!</value>
|
||||
</data>
|
||||
<data name="BotAutomaticIdlingNowResumed" xml:space="preserve">
|
||||
<value>已恢復自動掛卡!</value>
|
||||
<value>已繼續自動掛卡!</value>
|
||||
</data>
|
||||
<data name="BotAutomaticIdlingPausedAlready" xml:space="preserve">
|
||||
<value>已經暫停自動掛卡!</value>
|
||||
</data>
|
||||
<data name="BotAutomaticIdlingResumedAlready" xml:space="preserve">
|
||||
<value>已經恢復自動掛卡!</value>
|
||||
<value>已經繼續自動掛卡!</value>
|
||||
</data>
|
||||
<data name="BotConnected" xml:space="preserve">
|
||||
<value>已連線至 Steam!</value>
|
||||
@@ -373,7 +374,7 @@
|
||||
<value>這個 Bot 將不會啟動,因為它在設定檔中被停用!</value>
|
||||
</data>
|
||||
<data name="BotInvalidAuthenticatorDuringLogin" xml:space="preserve">
|
||||
<value>已連續收到 {0} 次 TwoFactorCodeMismatch 錯誤訊息。您的雙重驗證憑證可能已失效,或者時間不同步,正在中止!</value>
|
||||
<value>已連續收到 {0} 次 TwoFactorCodeMismatch 錯誤碼。您的雙重驗證憑證可能已失效,或者時間不同步,正在中止!</value>
|
||||
<comment>{0} will be replaced by maximum allowed number of failed 2FA attempts</comment>
|
||||
</data>
|
||||
<data name="BotLoggedOff" xml:space="preserve">
|
||||
@@ -381,7 +382,7 @@
|
||||
<comment>{0} will be replaced by logging off reason (string)</comment>
|
||||
</data>
|
||||
<data name="BotLoggedOn" xml:space="preserve">
|
||||
<value>已成功登入 {0}。</value>
|
||||
<value>已成功登入帳號 {0}。</value>
|
||||
<comment>{0} will be replaced by steam ID (number)</comment>
|
||||
</data>
|
||||
<data name="BotLoggingIn" xml:space="preserve">
|
||||
@@ -394,13 +395,13 @@
|
||||
<value>交易提案失敗!</value>
|
||||
</data>
|
||||
<data name="BotLootingMasterNotDefined" xml:space="preserve">
|
||||
<value>無法發送交易提案,因為沒有帳號設有 master 權限!</value>
|
||||
<value>無法發送交易提案,因為沒有帳號設有 Master 權限!</value>
|
||||
</data>
|
||||
<data name="BotLootingSuccess" xml:space="preserve">
|
||||
<value>交易提案發送成功!</value>
|
||||
</data>
|
||||
<data name="BotSendingTradeToYourself" xml:space="preserve">
|
||||
<value>您無法對自己發送交易請求!</value>
|
||||
<value>您無法對自己發送交易提案!</value>
|
||||
</data>
|
||||
<data name="BotNoASFAuthenticator" xml:space="preserve">
|
||||
<value>這個 Bot 並未啟用 ASF 雙重驗證!您是否忘記將雙重驗證匯入至 ASF?</value>
|
||||
@@ -445,7 +446,7 @@
|
||||
<value>Bot 為受限狀態,無法透過掛卡得到卡片。</value>
|
||||
</data>
|
||||
<data name="BotStatusConnecting" xml:space="preserve">
|
||||
<value>Bot 正在連線到 Steam 網路。</value>
|
||||
<value>Bot 正在連線至 Steam 網路。</value>
|
||||
</data>
|
||||
<data name="BotStatusNotRunning" xml:space="preserve">
|
||||
<value>Bot 未在執行。</value>
|
||||
@@ -512,7 +513,7 @@
|
||||
<value>看來這是您首次使用本程式,歡迎!</value>
|
||||
</data>
|
||||
<data name="ErrorInvalidCurrentCulture" xml:space="preserve">
|
||||
<value>您提供的 CurrentCulture 無效,ASF 將以預設值繼續執行!</value>
|
||||
<value>您所提供的 CurrentCulture 無效,ASF 將以預設值繼續執行!</value>
|
||||
</data>
|
||||
<data name="TranslationIncomplete" xml:space="preserve">
|
||||
<value>ASF 將使用您的偏好語系 {0},但該語言的翻譯只完成了 {1}。或許您能協助我們完善 ASF 的翻譯。</value>
|
||||
@@ -537,7 +538,7 @@
|
||||
<value>Bot 已被封鎖,無法透過掛卡得到卡片。</value>
|
||||
</data>
|
||||
<data name="ErrorFunctionOnlyInHeadlessMode" xml:space="preserve">
|
||||
<value>此功能僅能在無介面模式下使用!</value>
|
||||
<value>此功能只能在無頭模式下使用!</value>
|
||||
</data>
|
||||
<data name="BotOwnedAlready" xml:space="preserve">
|
||||
<value>已擁有:{0}</value>
|
||||
@@ -567,7 +568,7 @@
|
||||
<comment>{0} will be replaced by number of bots that already own particular game being checked, {1} will be replaced by total number of bots that were checked during the process, {2} will be replaced by game's ID (number)</comment>
|
||||
</data>
|
||||
<data name="BotRefreshingPackagesData" xml:space="preserve">
|
||||
<value>更新套件資料中…</value>
|
||||
<value>正在更新軟體套件資料…</value>
|
||||
</data>
|
||||
<data name="WarningDeprecated" xml:space="preserve">
|
||||
<value>{0} 的用法已被棄用,並且將在未來的版本中移除,請改用 {1}。</value>
|
||||
@@ -607,7 +608,7 @@
|
||||
<value>已中止!</value>
|
||||
</data>
|
||||
<data name="WarningExcessiveBotsCount" xml:space="preserve">
|
||||
<value>您執行的個人 Bot 帳號數量超過我們的建議上限({0})。請留意,此設定不受支援,且可能會造成各種 Steam 相關問題,包括帳號停權。請參閱常見問答了解詳情。</value>
|
||||
<value>您執行的個人 Bot 帳號數量超過我們的建議上限({0})。請留意,此設定不受支援,且可能會造成各種 Steam 相關問題,包括帳號停權。請參閱常見問題以了解詳情。</value>
|
||||
<comment>{0} will be replaced by our maximum recommended bots count (number)</comment>
|
||||
</data>
|
||||
<data name="PluginLoaded" xml:space="preserve">
|
||||
@@ -628,23 +629,23 @@
|
||||
<value>請稍候…</value>
|
||||
</data>
|
||||
<data name="EnterCommand" xml:space="preserve">
|
||||
<value>輸入指令: </value>
|
||||
<value>輸入指令:</value>
|
||||
</data>
|
||||
<data name="Executing" xml:space="preserve">
|
||||
<value>正在執行…</value>
|
||||
</data>
|
||||
<data name="InteractiveConsoleEnabled" xml:space="preserve">
|
||||
<value>已開啟互動式主控台,按「C」進入指令模式。</value>
|
||||
<value>已開啟互動式控制台,按「C」進入指令模式。</value>
|
||||
</data>
|
||||
<data name="BotGamesToRedeemInBackgroundCount" xml:space="preserve">
|
||||
<value>Bot 的背景佇列中剩下 {0} 個遊戲。</value>
|
||||
<value>Bot 的背景佇列中剩餘 {0} 個遊戲。</value>
|
||||
<comment>{0} will be replaced by remaining number of games in BGR's queue</comment>
|
||||
</data>
|
||||
<data name="ErrorSingleInstanceRequired" xml:space="preserve">
|
||||
<value>ASF 程序已執行於此工作目錄,正在中止!</value>
|
||||
<value>ASF 程序已執行於當前的資料夾中,正在中止!</value>
|
||||
</data>
|
||||
<data name="BotHandledConfirmations" xml:space="preserve">
|
||||
<value>成功處理 {0} 個確認!</value>
|
||||
<value>成功處理 {0} 個交易確認!</value>
|
||||
<comment>{0} will be replaced by number of confirmations</comment>
|
||||
</data>
|
||||
<data name="BotExtraIdlingCooldown" xml:space="preserve">
|
||||
@@ -655,7 +656,7 @@
|
||||
<value>正在清理更新後的過時檔案…</value>
|
||||
</data>
|
||||
<data name="BotGeneratingSteamParentalCode" xml:space="preserve">
|
||||
<value>正在產生 Steam 家庭監護碼,這會需要一段時間,請考慮將它寫入設定檔中…</value>
|
||||
<value>正在產生 Steam 家庭監護 PIN 碼,這會需要一段時間,請考慮將它寫入設定檔中…</value>
|
||||
</data>
|
||||
<data name="IPCConfigChanged" xml:space="preserve">
|
||||
<value>IPC 設定檔已變更!</value>
|
||||
@@ -665,7 +666,7 @@
|
||||
<comment>{0} will be replaced by trade offer ID (number), {1} will be replaced by internal ASF enum name, {2} will be replaced by technical reason why the trade was determined to be in this state</comment>
|
||||
</data>
|
||||
<data name="BotInvalidPasswordDuringLogin" xml:space="preserve">
|
||||
<value>連續收到 InvalidPassword 錯誤代碼 {0} 次。您的帳號密碼大概是錯的,正在中止!</value>
|
||||
<value>連續收到 InvalidPassword 錯誤碼 {0} 次。您的帳號密碼大概是錯的,正在中止!</value>
|
||||
<comment>{0} will be replaced by maximum allowed number of failed login attempts</comment>
|
||||
</data>
|
||||
<data name="Result" xml:space="preserve">
|
||||
@@ -673,14 +674,14 @@
|
||||
<comment>{0} will be replaced by generic result of various functions that use this string</comment>
|
||||
</data>
|
||||
<data name="WarningUnsupportedEnvironment" xml:space="preserve">
|
||||
<value>您正嘗試執行 {0} 不同的 ASF 於不支援的環境中:{1}。如果您真的知道您在做什麼的話,請加上 --ignore-unsupported-environment 引數。</value>
|
||||
<value>您正嘗試於不支援的環境({1})中執行 {0} 版本的 ASF。如果您真的知道您在做什麼的話,請加上 --ignore-unsupported-environment 引數。</value>
|
||||
</data>
|
||||
<data name="WarningUnknownCommandLineArgument" xml:space="preserve">
|
||||
<value>未知的命令列引數:{0}</value>
|
||||
<comment>{0} will be replaced by unrecognized command that has been provided</comment>
|
||||
</data>
|
||||
<data name="ErrorConfigDirectoryNotFound" xml:space="preserve">
|
||||
<value>無法找到設定檔所在目錄,正在中止!</value>
|
||||
<value>無法找到設定檔所在的資料夾,正在中止!</value>
|
||||
</data>
|
||||
<data name="BotIdlingSelectedGames" xml:space="preserve">
|
||||
<value>正在掛 {0} 指定的遊玩時數:{1}</value>
|
||||
@@ -707,15 +708,15 @@
|
||||
<comment>{0} will be replaced by the number of bytes (characters) recommended</comment>
|
||||
</data>
|
||||
<data name="WarningDefaultCryptKeyUsedForHashing" xml:space="preserve">
|
||||
<value>您正在使用 {0} 設定 {1} 屬性。但您沒有提供一個自定義的 --cryptkey。您應該提供自定義 --cryptkey 以提高安全性。</value>
|
||||
<value>您正在使用 {0} 設定 {1} 屬性。但您沒有提供自訂的 --cryptkey。您應提供自訂 --cryptkey 以提高安全性。</value>
|
||||
<comment>{0} will be replaced by the name of a particular setting (e.g. "SCrypt"), {1} will be replaced by the name of the property (e.g. "IPCPassword")</comment>
|
||||
</data>
|
||||
<data name="WarningDefaultCryptKeyUsedForEncryption" xml:space="preserve">
|
||||
<value>您正在使用 {0} 設定 {1} 屬性。但您沒有提供一個自定義的 --cryptkey。這完全破壞了保護,因為 ASF 被迫使用自己的(已知)金鑰。您應該提供自定義 --cryptkey 用於使用此設定提供的安全優勢。</value>
|
||||
<value>您正在使用 {0} 設定 {1} 屬性。但您沒有提供自訂的 --cryptkey。這完全破壞了保護,因為 ASF 被迫使用自己的(已知)金鑰。您應提供自訂 --cryptkey 以使用本設定所提供的安全優勢。</value>
|
||||
<comment>{0} will be replaced by the name of a particular setting (e.g. "AES"), {1} will be replaced by the name of the property (e.g. "SteamPassword")</comment>
|
||||
</data>
|
||||
<data name="WarningRunningAsRoot" xml:space="preserve">
|
||||
<value>您正在以管理員權限(Root)執行ASF。這會給您的設備帶來重大的安全風險,且由於ASF的操作不需要管理員權限,我們建議盡可能以非管理員使用者身份執行它。</value>
|
||||
<value>您正以管理員權限(Root)執行 ASF。這會給您的設備帶來重大的安全風險,且由於 ASF 的操作不需要管理員權限,我們建議盡可能以非管理員使用者身份執行它。</value>
|
||||
</data>
|
||||
<data name="WarningRunningInUnsupportedEnvironment" xml:space="preserve">
|
||||
<value>您在不受支援的環境中執行 ASF,並提供 --ignore-unsupported-environment 引數。請注意,我們不對這種情形提供任何形式的支援,您完全需要自行承擔風險。您已經被警告過了。</value>
|
||||
@@ -727,16 +728,16 @@
|
||||
<value>正在驗證已下載的二進制檔案與來自遠端伺服器的核對和…</value>
|
||||
</data>
|
||||
<data name="ChecksumMissing" xml:space="preserve">
|
||||
<value>遠端伺服器對我們要更新到的版本一無所知。若該版本是最近發布的,則可能出現這種情形⸺立刻拒絕進行更新程序作為額外的安全措施。</value>
|
||||
<value>遠端伺服器對我們要更新到的版本一無所知。若該版本是最近發布的,則可能出現這種情形⸺作為額外的安全措施,已立刻拒絕進行更新程序。</value>
|
||||
</data>
|
||||
<data name="ChecksumWrong" xml:space="preserve">
|
||||
<value>遠端伺服器回覆了不同的核對和,這可能意味著下載檔案損毀或遭受中間人攻擊,拒絕繼續更新程序!</value>
|
||||
<value>遠端伺服器回覆了不同的核對和,這可能代表下載的檔案已損壞或遭到了中間人攻擊,拒絕繼續更新程序!</value>
|
||||
</data>
|
||||
<data name="PatchingFiles" xml:space="preserve">
|
||||
<value>正在修補 ASF 檔案…</value>
|
||||
<value>正在更新 ASF 檔案…</value>
|
||||
</data>
|
||||
<data name="UserInputCryptkey" xml:space="preserve">
|
||||
<value>請輸入您的 cryptkey: </value>
|
||||
<value>請輸入您的 cryptkey:</value>
|
||||
<comment>Please note that this translation should end with space</comment>
|
||||
</data>
|
||||
<data name="ErrorIPNotBanned" xml:space="preserve">
|
||||
@@ -744,7 +745,7 @@
|
||||
<comment>{0} will be replaced by an IP address which was requested to be unbanned from using IPC</comment>
|
||||
</data>
|
||||
<data name="WarningNoLicense" xml:space="preserve">
|
||||
<value>您正在嘗試使用付費功能 {0},但您尚未在 ASF 全域設定中設定有效的 LicenseID。請檢查您的設定,因為如果沒有其他詳細資訊,該功能將會無法運作。</value>
|
||||
<value>您正在嘗試使用付費功能 {0},但您尚未在 ASF 全域設定中設定有效的 LicenseID。請檢查您的設定,因為如果沒有額外資訊,該功能將會無法運作。</value>
|
||||
<comment>{0} will be replaced by feature name (e.g. MatchActively)</comment>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -384,7 +384,7 @@ internal static class Logging {
|
||||
command = command[commandPrefix.Length..];
|
||||
}
|
||||
|
||||
Bot? targetBot = Bot.Bots?.OrderBy(static bot => bot.Key, Bot.BotsComparer).Select(static bot => bot.Value).FirstOrDefault();
|
||||
Bot? targetBot = Bot.GetDefaultBot();
|
||||
|
||||
if (targetBot == null) {
|
||||
Console.WriteLine($@"<< {Strings.ErrorNoBotsDefined}");
|
||||
|
||||
@@ -584,8 +584,6 @@ internal static class Program {
|
||||
bool networkGroupNext = false;
|
||||
bool pathNext = false;
|
||||
|
||||
bool noArgumentValueNext() => !cryptKeyNext && !cryptKeyFileNext && !networkGroupNext && !pathNext;
|
||||
|
||||
foreach (string arg in args) {
|
||||
switch (arg.ToUpperInvariant()) {
|
||||
case "--CRYPTKEY" when noArgumentValueNext():
|
||||
@@ -697,6 +695,8 @@ internal static class Program {
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
bool noArgumentValueNext() => !cryptKeyNext && !cryptKeyFileNext && !networkGroupNext && !pathNext;
|
||||
}
|
||||
|
||||
private static async Task<bool> ParseEnvironmentVariables() {
|
||||
|
||||
@@ -27,6 +27,7 @@ using System.Collections.Immutable;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
@@ -67,6 +68,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
||||
private const byte LoginCooldownInMinutes = 25; // Captcha disappears after around 20 minutes, so we make it 25
|
||||
private const uint LoginID = 1242; // This must be the same for all ASF bots and all ASF processes
|
||||
private const byte MaxLoginFailures = WebBrowser.MaxTries; // Max login failures in a row before we determine that our credentials are invalid (because Steam wrongly returns those, of course)course)
|
||||
private const byte MinimumAccessTokenValidityMinutes = 10;
|
||||
private const byte RedeemCooldownInHours = 1; // 1 hour since first redeem attempt, this is a limitation enforced by Steam
|
||||
|
||||
[PublicAPI]
|
||||
@@ -134,6 +136,10 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
||||
[PublicAPI]
|
||||
public bool IsPlayingPossible => !PlayingBlocked && !LibraryLocked;
|
||||
|
||||
[JsonProperty]
|
||||
[PublicAPI]
|
||||
public string? PublicIP => SteamClient.PublicIP?.ToString();
|
||||
|
||||
[JsonIgnore]
|
||||
[PublicAPI]
|
||||
public SteamApps SteamApps { get; }
|
||||
@@ -225,11 +231,13 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
||||
internal bool PlayingBlocked { get; private set; }
|
||||
internal bool PlayingWasBlocked { get; private set; }
|
||||
|
||||
private DateTime? AccessTokenValidUntil;
|
||||
private string? AuthCode;
|
||||
|
||||
[JsonProperty]
|
||||
private string? AvatarHash;
|
||||
|
||||
private string? BackingAccessToken;
|
||||
private Timer? ConnectionFailureTimer;
|
||||
private bool FirstTradeSent;
|
||||
private Timer? GamesRedeemerInBackgroundTimer;
|
||||
@@ -240,6 +248,8 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
||||
private ulong MasterChatGroupID;
|
||||
private Timer? PlayingWasBlockedTimer;
|
||||
private bool ReconnectOnUserInitiated;
|
||||
private string? RefreshToken;
|
||||
private Timer? RefreshTokensTimer;
|
||||
private bool SendCompleteTypesScheduled;
|
||||
private Timer? SendItemsTimer;
|
||||
private bool SteamParentalActive;
|
||||
@@ -247,6 +257,30 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
||||
private Timer? TradeCheckTimer;
|
||||
private string? TwoFactorCode;
|
||||
|
||||
private string? AccessToken {
|
||||
get => BackingAccessToken;
|
||||
|
||||
set {
|
||||
AccessTokenValidUntil = null;
|
||||
BackingAccessToken = value;
|
||||
|
||||
if (string.IsNullOrEmpty(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
|
||||
JwtSecurityToken? jwtToken = Utilities.ReadJwtToken(value!);
|
||||
|
||||
if (jwtToken == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (jwtToken.ValidTo > DateTime.MinValue) {
|
||||
AccessTokenValidUntil = jwtToken.ValidTo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Bot(string botName, BotConfig botConfig, BotDatabase botDatabase) {
|
||||
BotName = !string.IsNullOrEmpty(botName) ? botName : throw new ArgumentNullException(nameof(botName));
|
||||
BotConfig = botConfig ?? throw new ArgumentNullException(nameof(botConfig));
|
||||
@@ -353,6 +387,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
||||
ConnectionFailureTimer?.Dispose();
|
||||
GamesRedeemerInBackgroundTimer?.Dispose();
|
||||
PlayingWasBlockedTimer?.Dispose();
|
||||
RefreshTokensTimer?.Dispose();
|
||||
SendItemsTimer?.Dispose();
|
||||
SteamSaleEvent?.Dispose();
|
||||
TradeCheckTimer?.Dispose();
|
||||
@@ -386,6 +421,10 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
||||
await PlayingWasBlockedTimer.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (RefreshTokensTimer != null) {
|
||||
await RefreshTokensTimer.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (SendItemsTimer != null) {
|
||||
await SendItemsTimer.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
@@ -479,6 +518,10 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
||||
throw new InvalidOperationException(nameof(Bots));
|
||||
}
|
||||
|
||||
if (BotsComparer == null) {
|
||||
throw new InvalidOperationException(nameof(BotsComparer));
|
||||
}
|
||||
|
||||
string[] botNames = args.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
HashSet<Bot> result = new();
|
||||
@@ -491,26 +534,41 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (botName.Contains("..", StringComparison.Ordinal)) {
|
||||
if ((botName.Length > 2) && botName.Contains("..", StringComparison.Ordinal)) {
|
||||
string[] botRange = botName.Split(new[] { ".." }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (botRange.Length == 2) {
|
||||
Bot? firstBot = GetBot(botRange[0]);
|
||||
Bot? firstBot = GetBot(botRange[0]);
|
||||
|
||||
if (firstBot != null) {
|
||||
Bot? lastBot = GetBot(botRange[1]);
|
||||
if (firstBot != null) {
|
||||
switch (botRange.Length) {
|
||||
case 1:
|
||||
// Either bot.. or ..bot
|
||||
IEnumerable<Bot> query = Bots.OrderBy(static bot => bot.Key, BotsComparer).Select(static bot => bot.Value);
|
||||
|
||||
if (lastBot != null) {
|
||||
foreach (Bot bot in Bots.OrderBy(static bot => bot.Key, BotsComparer).Select(static bot => bot.Value).SkipWhile(bot => bot != firstBot)) {
|
||||
query = botName.StartsWith("..", StringComparison.Ordinal) ? query.TakeWhile(bot => bot != firstBot) : query.SkipWhile(bot => bot != firstBot);
|
||||
|
||||
foreach (Bot bot in query) {
|
||||
result.Add(bot);
|
||||
|
||||
if (bot == lastBot) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(firstBot);
|
||||
|
||||
continue;
|
||||
}
|
||||
case 2:
|
||||
// firstBot..lastBot
|
||||
Bot? lastBot = GetBot(botRange[1]);
|
||||
|
||||
if ((lastBot != null) && (BotsComparer.Compare(firstBot.BotName, lastBot.BotName) <= 0)) {
|
||||
foreach (Bot bot in Bots.OrderBy(static bot => bot.Key, BotsComparer).Select(static bot => bot.Value).SkipWhile(bot => bot != firstBot).TakeWhile(bot => bot != lastBot)) {
|
||||
result.Add(bot);
|
||||
}
|
||||
|
||||
result.Add(lastBot);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1182,6 +1240,19 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
||||
return (productInfoResultSet is { Complete: true, Failed: false } || optimisticDiscovery ? appID : 0, DateTime.MinValue, true);
|
||||
}
|
||||
|
||||
internal static Bot? GetDefaultBot() {
|
||||
if ((Bots == null) || Bots.IsEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
|
||||
if (!string.IsNullOrEmpty(ASF.GlobalConfig?.DefaultBot) && Bots.TryGetValue(ASF.GlobalConfig!.DefaultBot!, out Bot? targetBot)) {
|
||||
return targetBot;
|
||||
}
|
||||
|
||||
return Bots.OrderBy(static bot => bot.Key, BotsComparer).Select(static bot => bot.Value).FirstOrDefault();
|
||||
}
|
||||
|
||||
internal Task<HashSet<uint>?> GetMarketableAppIDs() => ArchiWebHandler.GetAppList();
|
||||
|
||||
internal async Task<Dictionary<uint, PackageData>?> GetPackagesData(IReadOnlyCollection<uint> packageIDs) {
|
||||
@@ -1402,16 +1473,13 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
||||
string configFile = GetFilePath(EFileType.Config);
|
||||
|
||||
if (string.IsNullOrEmpty(configFile)) {
|
||||
ArchiLogger.LogNullError(configFile);
|
||||
|
||||
return;
|
||||
throw new InvalidOperationException(nameof(configFile));
|
||||
}
|
||||
|
||||
(BotConfig? botConfig, _) = await BotConfig.Load(configFile, BotName).ConfigureAwait(false);
|
||||
|
||||
if (botConfig == null) {
|
||||
await Destroy().ConfigureAwait(false);
|
||||
|
||||
// Invalid config file, we allow user to fix it without destroying the bot right away
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1459,32 +1527,55 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
||||
await PluginsCore.OnBotFarmingStopped(this).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
internal async Task<bool> RefreshSession() {
|
||||
internal async Task<bool> RefreshWebSession(bool force = false) {
|
||||
if (!IsConnectedAndLoggedOn) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SteamUser.WebAPIUserNonceCallback callback;
|
||||
DateTime now = DateTime.UtcNow;
|
||||
|
||||
try {
|
||||
callback = await SteamUser.RequestWebAPIUserNonce().ToLongRunningTask().ConfigureAwait(false);
|
||||
} catch (Exception e) {
|
||||
ArchiLogger.LogGenericWarningException(e);
|
||||
if (!force && !string.IsNullOrEmpty(AccessToken) && AccessTokenValidUntil.HasValue && (AccessTokenValidUntil.Value > now.AddMinutes(MinimumAccessTokenValidityMinutes))) {
|
||||
// We can use the tokens we already have
|
||||
if (await ArchiWebHandler.Init(SteamID, SteamClient.Universe, AccessToken!, SteamParentalActive ? BotConfig.SteamParentalCode : null).ConfigureAwait(false)) {
|
||||
InitRefreshTokensTimer(AccessTokenValidUntil.Value);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// We need to refresh our session, access token is no longer valid
|
||||
BotDatabase.AccessToken = AccessToken = null;
|
||||
|
||||
if (string.IsNullOrEmpty(RefreshToken)) {
|
||||
// Without refresh token we can't get fresh access tokens, relog needed
|
||||
await Connect(true).ConfigureAwait(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(callback.Nonce)) {
|
||||
CAuthentication_AccessToken_GenerateForApp_Response? response = await ArchiHandler.GenerateAccessTokens(RefreshToken!).ConfigureAwait(false);
|
||||
|
||||
if (response == null) {
|
||||
// The request has failed, in almost all cases this means our refresh token is no longer valid, relog needed
|
||||
BotDatabase.RefreshToken = RefreshToken = null;
|
||||
|
||||
await Connect(true).ConfigureAwait(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (await ArchiWebHandler.Init(SteamID, SteamClient.Universe, callback.Nonce, SteamParentalActive ? BotConfig.SteamParentalCode : null).ConfigureAwait(false)) {
|
||||
// TODO: Handle update of refresh token with next SK2 release
|
||||
UpdateTokens(response.access_token, RefreshToken!);
|
||||
|
||||
if (await ArchiWebHandler.Init(SteamID, SteamClient.Universe, response.access_token, SteamParentalActive ? BotConfig.SteamParentalCode : null).ConfigureAwait(false)) {
|
||||
InitRefreshTokensTimer(AccessTokenValidUntil ?? now.AddDays(1));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// We got the tokens, but failed to authorize? Purge them just to be sure and reconnect
|
||||
BotDatabase.AccessToken = AccessToken = null;
|
||||
|
||||
await Connect(true).ConfigureAwait(false);
|
||||
|
||||
return false;
|
||||
@@ -1552,6 +1643,8 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
||||
ASF.ArchiLogger.LogGenericDebug($"{databaseFilePath}: {JsonConvert.SerializeObject(botDatabase, Formatting.Indented)}");
|
||||
}
|
||||
|
||||
botDatabase.PerformMaintenance();
|
||||
|
||||
Bot bot;
|
||||
|
||||
await BotsSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
@@ -2058,7 +2151,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
||||
case EResult.AccountLoginDeniedNeedTwoFactor:
|
||||
case EResult.AccountLoginDeniedThrottle:
|
||||
case EResult.DuplicateRequest: // This will happen if user reacts to popup and tries to use the code afterwards, we have the code saved in ASF, we just need to try again
|
||||
case EResult.Expired: // Same as Timeout
|
||||
case EResult.Expired: // Refresh token expired
|
||||
case EResult.FileNotFound: // User denied approval despite telling us that he accepted it, just try again
|
||||
case EResult.InvalidPassword:
|
||||
case EResult.NoConnection:
|
||||
@@ -2084,8 +2177,8 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
||||
}
|
||||
|
||||
break;
|
||||
case EResult.AccessDenied when string.IsNullOrEmpty(BotDatabase.RefreshToken) && (++LoginFailures >= MaxLoginFailures):
|
||||
case EResult.InvalidPassword when string.IsNullOrEmpty(BotDatabase.RefreshToken) && (++LoginFailures >= MaxLoginFailures):
|
||||
case EResult.AccessDenied when string.IsNullOrEmpty(RefreshToken) && (++LoginFailures >= MaxLoginFailures):
|
||||
case EResult.InvalidPassword when string.IsNullOrEmpty(RefreshToken) && (++LoginFailures >= MaxLoginFailures):
|
||||
LoginFailures = 0;
|
||||
ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.BotInvalidPasswordDuringLogin, MaxLoginFailures));
|
||||
Stop();
|
||||
@@ -2239,6 +2332,19 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
||||
WalletBalance = 0;
|
||||
WalletCurrency = ECurrencyCode.Invalid;
|
||||
|
||||
AccessToken = BotDatabase.AccessToken;
|
||||
RefreshToken = BotDatabase.RefreshToken;
|
||||
|
||||
if (BotConfig.PasswordFormat.HasTransformation()) {
|
||||
if (!string.IsNullOrEmpty(AccessToken)) {
|
||||
AccessToken = await ArchiCryptoHelper.Decrypt(BotConfig.PasswordFormat, AccessToken!).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(RefreshToken)) {
|
||||
AccessToken = await ArchiCryptoHelper.Decrypt(BotConfig.PasswordFormat, RefreshToken!).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
CardsFarmer.SetInitialState(BotConfig.Paused);
|
||||
|
||||
if (SendItemsTimer != null) {
|
||||
@@ -2309,6 +2415,42 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
||||
);
|
||||
}
|
||||
|
||||
private void InitRefreshTokensTimer(DateTime validUntil) {
|
||||
if (validUntil == DateTime.MinValue) {
|
||||
throw new ArgumentOutOfRangeException(nameof(validUntil));
|
||||
}
|
||||
|
||||
if (validUntil == DateTime.MaxValue) {
|
||||
// OK, tokens do not require refreshing
|
||||
StopRefreshTokensTimer();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
TimeSpan delay = validUntil - DateTime.UtcNow;
|
||||
|
||||
// Start refreshing token before it's invalid
|
||||
if (delay.TotalMinutes > MinimumAccessTokenValidityMinutes) {
|
||||
delay -= TimeSpan.FromMinutes(MinimumAccessTokenValidityMinutes);
|
||||
} else {
|
||||
delay = TimeSpan.Zero;
|
||||
}
|
||||
|
||||
// Timer can accept only dueTimes up to 2^32 - 2
|
||||
uint dueTime = (uint) Math.Min(uint.MaxValue - 1, (ulong) delay.TotalMilliseconds);
|
||||
|
||||
if (RefreshTokensTimer == null) {
|
||||
RefreshTokensTimer = new Timer(
|
||||
OnRefreshTokensTimer,
|
||||
null,
|
||||
TimeSpan.FromMilliseconds(dueTime), // Delay
|
||||
TimeSpan.FromMinutes(1) // Period
|
||||
);
|
||||
} else {
|
||||
RefreshTokensTimer.Change(TimeSpan.FromMilliseconds(dueTime), TimeSpan.FromMinutes(1));
|
||||
}
|
||||
}
|
||||
|
||||
private void InitStart() {
|
||||
if (!BotConfig.Enabled) {
|
||||
ArchiLogger.LogGenericWarning(Strings.BotInstanceNotStartingBecauseDisabled);
|
||||
@@ -2447,17 +2589,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
string? refreshToken = BotDatabase.RefreshToken;
|
||||
|
||||
if (!string.IsNullOrEmpty(refreshToken)) {
|
||||
// Decrypt refreshToken if needed
|
||||
if (BotConfig.PasswordFormat.HasTransformation()) {
|
||||
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
|
||||
refreshToken = await ArchiCryptoHelper.Decrypt(BotConfig.PasswordFormat, refreshToken!).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!await InitLoginAndPassword(string.IsNullOrEmpty(refreshToken)).ConfigureAwait(false)) {
|
||||
if (!await InitLoginAndPassword(string.IsNullOrEmpty(RefreshToken)).ConfigureAwait(false)) {
|
||||
Stop();
|
||||
|
||||
return;
|
||||
@@ -2502,7 +2634,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
||||
|
||||
InitConnectionFailureTimer();
|
||||
|
||||
if (string.IsNullOrEmpty(refreshToken)) {
|
||||
if (string.IsNullOrEmpty(RefreshToken)) {
|
||||
AuthPollResult pollResult;
|
||||
|
||||
try {
|
||||
@@ -2534,19 +2666,15 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
||||
return;
|
||||
}
|
||||
|
||||
refreshToken = pollResult.RefreshToken;
|
||||
|
||||
if (BotConfig.UseLoginKeys) {
|
||||
BotDatabase.RefreshToken = BotConfig.PasswordFormat.HasTransformation() ? ArchiCryptoHelper.Encrypt(BotConfig.PasswordFormat, refreshToken) : refreshToken;
|
||||
|
||||
if (!string.IsNullOrEmpty(pollResult.NewGuardData)) {
|
||||
BotDatabase.SteamGuardData = pollResult.NewGuardData;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(pollResult.NewGuardData) && BotConfig.UseLoginKeys) {
|
||||
BotDatabase.SteamGuardData = pollResult.NewGuardData;
|
||||
}
|
||||
|
||||
UpdateTokens(pollResult.AccessToken, pollResult.RefreshToken);
|
||||
}
|
||||
|
||||
SteamUser.LogOnDetails logOnDetails = new() {
|
||||
AccessToken = refreshToken,
|
||||
AccessToken = RefreshToken,
|
||||
CellID = ASF.GlobalDatabase?.CellID,
|
||||
LoginID = LoginID,
|
||||
SentryFileHash = sentryFileHash,
|
||||
@@ -2571,6 +2699,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
||||
HeartBeatFailures = 0;
|
||||
StopConnectionFailureTimer();
|
||||
StopPlayingWasBlockedTimer();
|
||||
StopRefreshTokensTimer();
|
||||
|
||||
ArchiLogger.LogGenericInfo(Strings.BotDisconnected);
|
||||
|
||||
@@ -2605,10 +2734,11 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
||||
case EResult.AccountDisabled:
|
||||
// Do not attempt to reconnect, those failures are permanent
|
||||
return;
|
||||
case EResult.AccessDenied when !string.IsNullOrEmpty(BotDatabase.RefreshToken):
|
||||
case EResult.InvalidPassword when !string.IsNullOrEmpty(BotDatabase.RefreshToken):
|
||||
case EResult.AccessDenied when !string.IsNullOrEmpty(RefreshToken):
|
||||
case EResult.Expired when !string.IsNullOrEmpty(RefreshToken):
|
||||
case EResult.InvalidPassword when !string.IsNullOrEmpty(RefreshToken):
|
||||
// We can retry immediately
|
||||
BotDatabase.RefreshToken = null;
|
||||
BotDatabase.RefreshToken = RefreshToken = null;
|
||||
ArchiLogger.LogGenericInfo(Strings.BotRemovedExpiredLoginKey);
|
||||
|
||||
break;
|
||||
@@ -2639,6 +2769,15 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait with reconnection until we're done with the prompt, not earlier
|
||||
while (RequiredInput != ASF.EUserInputType.None) {
|
||||
await Task.Delay(1000).ConfigureAwait(false);
|
||||
|
||||
if (!KeepRunning || SteamClient.IsConnected) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericInfo(Strings.BotReconnecting);
|
||||
await Connect().ConfigureAwait(false);
|
||||
}
|
||||
@@ -3042,10 +3181,9 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
||||
|
||||
ArchiWebHandler.OnVanityURLChanged(callback.VanityURL);
|
||||
|
||||
if (!await ArchiWebHandler.Init(SteamID, SteamClient.Universe, callback.WebAPIUserNonce ?? throw new InvalidOperationException(nameof(callback.WebAPIUserNonce)), SteamParentalActive ? BotConfig.SteamParentalCode : null).ConfigureAwait(false)) {
|
||||
if (!await RefreshSession().ConfigureAwait(false)) {
|
||||
return;
|
||||
}
|
||||
// Establish web session
|
||||
if (!await RefreshWebSession().ConfigureAwait(false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pre-fetch API key for future usage if possible
|
||||
@@ -3190,6 +3328,15 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
||||
await CheckOccupationStatus().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async void OnRefreshTokensTimer(object? state = null) {
|
||||
if (AccessTokenValidUntil.HasValue && (AccessTokenValidUntil.Value > DateTime.UtcNow.AddMinutes(MinimumAccessTokenValidityMinutes))) {
|
||||
// We don't need to refresh just yet
|
||||
InitRefreshTokensTimer(AccessTokenValidUntil.Value);
|
||||
}
|
||||
|
||||
await RefreshWebSession().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async void OnSendItemsTimer(object? state = null) => await Actions.SendInventory(filterFunction: item => BotConfig.LootableTypes.Contains(item.Type)).ConfigureAwait(false);
|
||||
|
||||
private async void OnServiceMethod(SteamUnifiedMessages.ServiceMethodNotification notification) {
|
||||
@@ -3662,6 +3809,38 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
||||
PlayingWasBlockedTimer = null;
|
||||
}
|
||||
|
||||
private void StopRefreshTokensTimer() {
|
||||
if (RefreshTokensTimer == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
RefreshTokensTimer.Dispose();
|
||||
RefreshTokensTimer = null;
|
||||
}
|
||||
|
||||
private void UpdateTokens(string accessToken, string refreshToken) {
|
||||
if (string.IsNullOrEmpty(accessToken)) {
|
||||
throw new ArgumentNullException(nameof(accessToken));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(refreshToken)) {
|
||||
throw new ArgumentNullException(nameof(refreshToken));
|
||||
}
|
||||
|
||||
AccessToken = accessToken;
|
||||
RefreshToken = refreshToken;
|
||||
|
||||
if (BotConfig.UseLoginKeys) {
|
||||
if (BotConfig.PasswordFormat.HasTransformation()) {
|
||||
BotDatabase.AccessToken = ArchiCryptoHelper.Encrypt(BotConfig.PasswordFormat, accessToken);
|
||||
BotDatabase.RefreshToken = ArchiCryptoHelper.Encrypt(BotConfig.PasswordFormat, refreshToken);
|
||||
} else {
|
||||
BotDatabase.AccessToken = accessToken;
|
||||
BotDatabase.RefreshToken = refreshToken;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private (bool IsSteamParentalEnabled, string? SteamParentalCode) ValidateSteamParental(ParentalSettings settings, string? steamParentalCode = null, bool allowGeneration = true) {
|
||||
ArgumentNullException.ThrowIfNull(settings);
|
||||
|
||||
|
||||
@@ -34,9 +34,11 @@ using ArchiSteamFarm.Collections;
|
||||
using ArchiSteamFarm.Core;
|
||||
using ArchiSteamFarm.Localization;
|
||||
using ArchiSteamFarm.Plugins;
|
||||
using ArchiSteamFarm.Steam.Data;
|
||||
using ArchiSteamFarm.Steam.Integration;
|
||||
using ArchiSteamFarm.Steam.Storage;
|
||||
using ArchiSteamFarm.Storage;
|
||||
using ArchiSteamFarm.Web;
|
||||
using JetBrains.Annotations;
|
||||
using Newtonsoft.Json;
|
||||
using SteamKit2;
|
||||
@@ -47,6 +49,7 @@ public sealed class CardsFarmer : IAsyncDisposable, IDisposable {
|
||||
internal const byte DaysForRefund = 14; // In how many days since payment we're allowed to refund
|
||||
internal const byte HoursForRefund = 2; // Up to how many hours we're allowed to play for refund
|
||||
|
||||
private const byte DaysToIgnoreRiskyAppIDs = 14; // How many days since determining that game is not candidate for idling, we assume that to still be the case, in risky approach
|
||||
private const byte ExtraFarmingDelaySeconds = 10; // In seconds, how much time to add on top of FarmingDelay (helps fighting misc time differences of Steam network)
|
||||
private const byte HoursToIgnore = 1; // How many hours we ignore unreleased appIDs and don't bother checking them again
|
||||
|
||||
@@ -142,6 +145,7 @@ public sealed class CardsFarmer : IAsyncDisposable, IDisposable {
|
||||
private bool ParsingScheduled;
|
||||
private bool PermanentlyPaused;
|
||||
private bool ShouldResumeFarming;
|
||||
private bool ShouldSkipNewGamesIfPossible;
|
||||
|
||||
internal CardsFarmer(Bot bot) {
|
||||
Bot = bot ?? throw new ArgumentNullException(nameof(bot));
|
||||
@@ -223,7 +227,7 @@ public sealed class CardsFarmer : IAsyncDisposable, IDisposable {
|
||||
// We should restart the farming if the order or efficiency of the farming could be affected by the newly-activated product
|
||||
// The order is affected when user uses farming order that isn't independent of the game data (it could alter the order in deterministic way if the game was considered in current queue)
|
||||
// The efficiency is affected only in complex algorithm (entirely), as it depends on hours order that is not independent (as specified above)
|
||||
if ((Bot.BotConfig.HoursUntilCardDrops > 0) || ((Bot.BotConfig.FarmingOrders.Count > 0) && Bot.BotConfig.FarmingOrders.Any(static farmingOrder => (farmingOrder != BotConfig.EFarmingOrder.Unordered) && (farmingOrder != BotConfig.EFarmingOrder.Random)))) {
|
||||
if (!ShouldSkipNewGamesIfPossible && ((Bot.BotConfig.HoursUntilCardDrops > 0) || ((Bot.BotConfig.FarmingOrders.Count > 0) && Bot.BotConfig.FarmingOrders.Any(static farmingOrder => (farmingOrder != BotConfig.EFarmingOrder.Unordered) && (farmingOrder != BotConfig.EFarmingOrder.Random))))) {
|
||||
await StopFarming().ConfigureAwait(false);
|
||||
await StartFarming().ConfigureAwait(false);
|
||||
}
|
||||
@@ -298,7 +302,7 @@ public sealed class CardsFarmer : IAsyncDisposable, IDisposable {
|
||||
|
||||
internal void SetInitialState(bool paused) {
|
||||
PermanentlyPaused = Paused = paused;
|
||||
ShouldResumeFarming = false;
|
||||
ShouldResumeFarming = ShouldSkipNewGamesIfPossible = false;
|
||||
}
|
||||
|
||||
internal async Task StartFarming() {
|
||||
@@ -424,19 +428,18 @@ public sealed class CardsFarmer : IAsyncDisposable, IDisposable {
|
||||
throw new ArgumentOutOfRangeException(nameof(hours));
|
||||
}
|
||||
|
||||
ushort? cardsRemaining = await GetCardsRemaining(appID).ConfigureAwait(false);
|
||||
Game? game = await GetGameCardsInfo(appID).ConfigureAwait(false);
|
||||
|
||||
switch (cardsRemaining) {
|
||||
case null:
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningCouldNotCheckCardsStatus, appID, name));
|
||||
if (game == null) {
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningCouldNotCheckCardsStatus, appID, name));
|
||||
|
||||
return;
|
||||
case 0:
|
||||
return;
|
||||
default:
|
||||
GamesToFarm.Add(new Game(appID, name, hours, cardsRemaining.Value, badgeLevel));
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
if (game.CardsRemaining > 0) {
|
||||
Bot.BotDatabase.FarmingRiskyPrioritizedAppIDs.Add(appID);
|
||||
|
||||
GamesToFarm.Add(new Game(appID, name, hours, game.CardsRemaining, badgeLevel));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,30 +498,8 @@ public sealed class CardsFarmer : IAsyncDisposable, IDisposable {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (SalesBlacklist.Contains(appID) || (ASF.GlobalConfig?.Blacklist.Contains(appID) == true) || Bot.IsBlacklistedFromIdling(appID) || (Bot.BotConfig.FarmPriorityQueueOnly && !Bot.IsPriorityIdling(appID))) {
|
||||
// We're configured to ignore this appID, so skip it
|
||||
continue;
|
||||
}
|
||||
|
||||
bool ignored = false;
|
||||
|
||||
foreach (ConcurrentDictionary<uint, DateTime> sourceOfIgnoredAppIDs in SourcesOfIgnoredAppIDs) {
|
||||
if (!sourceOfIgnoredAppIDs.TryGetValue(appID, out DateTime ignoredUntil)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ignoredUntil > DateTime.UtcNow) {
|
||||
// This game is still ignored
|
||||
ignored = true;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// This game served its time as being ignored
|
||||
sourceOfIgnoredAppIDs.TryRemove(appID, out _);
|
||||
}
|
||||
|
||||
if (ignored) {
|
||||
if (!ShouldIdle(appID)) {
|
||||
// No point in evaluating further if we can determine that on appID alone
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -737,6 +718,8 @@ public sealed class CardsFarmer : IAsyncDisposable, IDisposable {
|
||||
// Either we have decent info about appID, name, hours, cardsRemaining (cardsRemaining > 0) and level
|
||||
// OR we strongly believe that Steam lied to us, in this case we will need to check game individually (cardsRemaining == 0)
|
||||
if (cardsRemaining > 0) {
|
||||
Bot.BotDatabase.FarmingRiskyPrioritizedAppIDs.Add(appID);
|
||||
|
||||
GamesToFarm.Add(new Game(appID, name, hours, cardsRemaining, badgeLevel));
|
||||
} else {
|
||||
Task task = CheckGame(appID, name, hours, badgeLevel);
|
||||
@@ -762,7 +745,7 @@ public sealed class CardsFarmer : IAsyncDisposable, IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CheckPage(byte page, ISet<uint> parsedAppIDs) {
|
||||
private async Task<bool> CheckPage(byte page, ISet<uint> parsedAppIDs) {
|
||||
if (page == 0) {
|
||||
throw new ArgumentOutOfRangeException(nameof(page));
|
||||
}
|
||||
@@ -772,10 +755,12 @@ public sealed class CardsFarmer : IAsyncDisposable, IDisposable {
|
||||
using IDocument? htmlDocument = await Bot.ArchiWebHandler.GetBadgePage(page).ConfigureAwait(false);
|
||||
|
||||
if (htmlDocument == null) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
await CheckPage(htmlDocument, parsedAppIDs).ConfigureAwait(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task Farm() {
|
||||
@@ -1003,16 +988,58 @@ public sealed class CardsFarmer : IAsyncDisposable, IDisposable {
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<ushort?> GetCardsRemaining(uint appID) {
|
||||
private async Task<Game?> GetGameCardsInfo(uint appID) {
|
||||
if (appID == 0) {
|
||||
throw new ArgumentOutOfRangeException(nameof(appID));
|
||||
}
|
||||
|
||||
using IDocument? htmlDocument = await Bot.ArchiWebHandler.GetGameCardsPage(appID).ConfigureAwait(false);
|
||||
|
||||
INode? progressNode = htmlDocument?.SelectSingleNode("//span[@class='progress_info_bold']");
|
||||
if (htmlDocument == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
INode? nameNode = htmlDocument.SelectSingleNode("(//span[@class='profile_small_header_location'])[last()]");
|
||||
|
||||
if (nameNode == null) {
|
||||
Bot.ArchiLogger.LogNullError(nameNode);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
string name = nameNode.TextContent;
|
||||
|
||||
if (string.IsNullOrEmpty(name)) {
|
||||
Bot.ArchiLogger.LogNullError(name);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
INode? hoursNode = htmlDocument.SelectSingleNode("//div[@class='badge_title_stats_playtime']");
|
||||
|
||||
if (hoursNode == null) {
|
||||
Bot.ArchiLogger.LogNullError(hoursNode);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
float hours = 0.0F;
|
||||
Match hoursMatch = GeneratedRegexes.Decimal().Match(hoursNode.TextContent);
|
||||
|
||||
// This might fail if we have exactly 0.0 hours played, as it's not printed in that case - that's fine
|
||||
if (hoursMatch.Success) {
|
||||
if (!float.TryParse(hoursMatch.Value, NumberStyles.Number, CultureInfo.InvariantCulture, out hours) || (hours <= 0.0F)) {
|
||||
Bot.ArchiLogger.LogNullError(hours);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
INode? progressNode = htmlDocument.SelectSingleNode("//span[@class='progress_info_bold']");
|
||||
|
||||
if (progressNode == null) {
|
||||
Bot.ArchiLogger.LogNullError(progressNode);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1024,33 +1051,86 @@ public sealed class CardsFarmer : IAsyncDisposable, IDisposable {
|
||||
return null;
|
||||
}
|
||||
|
||||
ushort cardsRemaining = 0;
|
||||
|
||||
Match match = GeneratedRegexes.Digits().Match(progress);
|
||||
|
||||
if (!match.Success) {
|
||||
return 0;
|
||||
if (match.Success) {
|
||||
if (!ushort.TryParse(match.Value, out cardsRemaining) || (cardsRemaining == 0)) {
|
||||
Bot.ArchiLogger.LogNullError(cardsRemaining);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ushort.TryParse(match.Value, out ushort cardsRemaining) || (cardsRemaining == 0)) {
|
||||
Bot.ArchiLogger.LogNullError(cardsRemaining);
|
||||
byte badgeLevel = 0;
|
||||
|
||||
return null;
|
||||
INode? levelNode = htmlDocument.SelectSingleNode("//div[@class='badge_info_description']/div[2]");
|
||||
|
||||
// There is no levelNode if we didn't craft that badge yet (level 0)
|
||||
if (levelNode != null) {
|
||||
string levelText = levelNode.TextContent;
|
||||
|
||||
if (string.IsNullOrEmpty(levelText)) {
|
||||
Bot.ArchiLogger.LogNullError(levelText);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
int levelStartIndex = levelText.IndexOf("Level ", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (levelStartIndex < 0) {
|
||||
Bot.ArchiLogger.LogNullError(levelStartIndex);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
levelStartIndex += 6;
|
||||
|
||||
if (levelText.Length <= levelStartIndex) {
|
||||
Bot.ArchiLogger.LogNullError(levelStartIndex);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
int levelEndIndex = levelText.IndexOf(',', levelStartIndex);
|
||||
|
||||
if (levelEndIndex <= levelStartIndex) {
|
||||
Bot.ArchiLogger.LogNullError(levelEndIndex);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
levelText = levelText[levelStartIndex..levelEndIndex];
|
||||
|
||||
if (!byte.TryParse(levelText, out badgeLevel) || badgeLevel is 0 or > 5) {
|
||||
Bot.ArchiLogger.LogNullError(badgeLevel);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return cardsRemaining;
|
||||
return new Game(appID, name, hours, cardsRemaining, badgeLevel);
|
||||
}
|
||||
|
||||
private async Task<bool?> IsAnythingToFarm() {
|
||||
// Find the number of badge pages
|
||||
Bot.ArchiLogger.LogGenericInfo(Strings.CheckingFirstBadgePage);
|
||||
|
||||
using IDocument? htmlDocument = await Bot.ArchiWebHandler.GetBadgePage(1).ConfigureAwait(false);
|
||||
using IDocument? htmlDocument = await Bot.ArchiWebHandler.GetBadgePage(1, Bot.BotConfig.EnableRiskyCardsDiscovery ? (byte) 2 : WebBrowser.MaxTries).ConfigureAwait(false);
|
||||
|
||||
if (htmlDocument == null) {
|
||||
Bot.ArchiLogger.LogGenericWarning(Strings.WarningCouldNotCheckBadges);
|
||||
|
||||
return null;
|
||||
if (!Bot.BotConfig.EnableRiskyCardsDiscovery) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await IsAnythingToFarmRisky().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
ShouldSkipNewGamesIfPossible = false;
|
||||
|
||||
byte maxPages = 1;
|
||||
|
||||
INode? htmlNode = htmlDocument.SelectSingleNode("(//a[@class='pagelink'])[last()]");
|
||||
@@ -1077,6 +1157,8 @@ public sealed class CardsFarmer : IAsyncDisposable, IDisposable {
|
||||
|
||||
Task mainTask = CheckPage(htmlDocument, parsedAppIDs);
|
||||
|
||||
bool allTasksSucceeded = true;
|
||||
|
||||
switch (ASF.GlobalConfig?.OptimizationMode) {
|
||||
case GlobalConfig.EOptimizationMode.MinMemoryUsage:
|
||||
await mainTask.ConfigureAwait(false);
|
||||
@@ -1085,33 +1167,50 @@ public sealed class CardsFarmer : IAsyncDisposable, IDisposable {
|
||||
Bot.ArchiLogger.LogGenericInfo(Strings.CheckingOtherBadgePages);
|
||||
|
||||
for (byte page = 2; page <= maxPages; page++) {
|
||||
await CheckPage(page, parsedAppIDs).ConfigureAwait(false);
|
||||
if (!await CheckPage(page, parsedAppIDs).ConfigureAwait(false)) {
|
||||
allTasksSucceeded = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
HashSet<Task> tasks = new(maxPages) { mainTask };
|
||||
|
||||
if (maxPages > 1) {
|
||||
Bot.ArchiLogger.LogGenericInfo(Strings.CheckingOtherBadgePages);
|
||||
|
||||
HashSet<Task<bool>> tasks = new(maxPages - 1);
|
||||
|
||||
for (byte page = 2; page <= maxPages; page++) {
|
||||
// ReSharper disable once InlineTemporaryVariable - we need a copy of variable being passed when in for loops, as loop will proceed before our task is launched
|
||||
byte currentPage = page;
|
||||
tasks.Add(CheckPage(currentPage, parsedAppIDs));
|
||||
}
|
||||
|
||||
bool[] taskResults = await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
if (taskResults.Any(static result => !result)) {
|
||||
allTasksSucceeded = false;
|
||||
}
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
await mainTask.ConfigureAwait(false);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (allTasksSucceeded) {
|
||||
Bot.BotDatabase.FarmingRiskyPrioritizedAppIDs.IntersectWith(GamesToFarm.Select(static game => game.AppID));
|
||||
}
|
||||
|
||||
if (GamesToFarm.Count == 0) {
|
||||
ShouldResumeFarming = false;
|
||||
|
||||
return false;
|
||||
// Allow changing to risky algorithm only if we failed at least some badge pages and we have the prop enabled
|
||||
if (allTasksSucceeded || !Bot.BotConfig.EnableRiskyCardsDiscovery) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return await IsAnythingToFarmRisky().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
ShouldResumeFarming = true;
|
||||
@@ -1120,6 +1219,92 @@ public sealed class CardsFarmer : IAsyncDisposable, IDisposable {
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<bool?> IsAnythingToFarmRisky() {
|
||||
Task<ImmutableHashSet<BoosterCreatorEntry>?> boosterCreatorEntriesTask = Bot.ArchiWebHandler.GetBoosterCreatorEntries();
|
||||
|
||||
ImmutableHashSet<uint>? boosterElibility = await Bot.ArchiWebHandler.GetBoosterEligibility().ConfigureAwait(false);
|
||||
|
||||
if (boosterElibility == null) {
|
||||
Bot.ArchiLogger.LogGenericWarning(Strings.WarningCouldNotCheckBadges);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
ImmutableHashSet<BoosterCreatorEntry>? boosterCreatorEntries = await boosterCreatorEntriesTask.ConfigureAwait(false);
|
||||
|
||||
if (boosterCreatorEntries == null) {
|
||||
Bot.ArchiLogger.LogGenericWarning(Strings.WarningCouldNotCheckBadges);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
GamesToFarm.Clear();
|
||||
|
||||
DateTime now = DateTime.UtcNow;
|
||||
|
||||
byte failuresInRow = 0;
|
||||
|
||||
// Normally we apply ordering after GamesToFarm are already found, but since this method is risky and greedy, we do as much as possible to allow user to optimize it
|
||||
// In particular, firstly we give priority to appIDs that we already found out before, either rule them out, or prioritize
|
||||
// Next, we apply farm priority queue right away, by both considering apps (if FarmPriorityQueueOnly) as well as giving priority to those that user specified
|
||||
// Lastly, we forcefully apply random order to those considered the same in value, as we can't really afford massive amount of misses in a row
|
||||
HashSet<uint> gamesToFarm = boosterCreatorEntries.Select(static entry => entry.AppID).Where(appID => !boosterElibility.Contains(appID) && (!Bot.BotDatabase.FarmingRiskyIgnoredAppIDs.TryGetValue(appID, out DateTime ignoredUntil) || (ignoredUntil < now)) && ShouldIdle(appID)).ToHashSet();
|
||||
|
||||
foreach (uint appID in Bot.BotDatabase.FarmingRiskyIgnoredAppIDs.Keys.Where(appID => !gamesToFarm.Contains(appID))) {
|
||||
Bot.BotDatabase.FarmingRiskyIgnoredAppIDs.Remove(appID);
|
||||
}
|
||||
|
||||
Bot.BotDatabase.FarmingRiskyPrioritizedAppIDs.IntersectWith(gamesToFarm);
|
||||
|
||||
#pragma warning disable CA5394 // This call isn't used in a security-sensitive manner
|
||||
IOrderedEnumerable<uint> gamesToFarmOrdered = gamesToFarm.OrderByDescending(Bot.BotDatabase.FarmingRiskyPrioritizedAppIDs.Contains).ThenByDescending(Bot.IsPriorityIdling).ThenBy(static _ => Random.Shared.Next());
|
||||
#pragma warning restore CA5394 // This call isn't used in a security-sensitive manner
|
||||
|
||||
DateTime ignoredUntil = now.AddDays(DaysToIgnoreRiskyAppIDs);
|
||||
|
||||
foreach (uint appID in gamesToFarmOrdered) {
|
||||
Game? game = await GetGameCardsInfo(appID).ConfigureAwait(false);
|
||||
|
||||
if (game == null) {
|
||||
if (++failuresInRow >= WebBrowser.MaxTries) {
|
||||
// We're not going to check further
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
failuresInRow = 0;
|
||||
|
||||
if (game.CardsRemaining == 0) {
|
||||
Bot.BotDatabase.FarmingRiskyIgnoredAppIDs[appID] = ignoredUntil;
|
||||
Bot.BotDatabase.FarmingRiskyPrioritizedAppIDs.Remove(appID);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
Bot.BotDatabase.FarmingRiskyPrioritizedAppIDs.Add(appID);
|
||||
|
||||
GamesToFarm.Add(game);
|
||||
|
||||
if ((game.HoursPlayed >= Bot.BotConfig.HoursUntilCardDrops) || (GamesToFarm.Count >= ArchiHandler.MaxGamesPlayedConcurrently)) {
|
||||
// Avoid further parsing in this risky method, we have enough for now
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (GamesToFarm.Count == 0) {
|
||||
ShouldResumeFarming = ShouldSkipNewGamesIfPossible = false;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
ShouldResumeFarming = ShouldSkipNewGamesIfPossible = true;
|
||||
await SortGamesToFarm().ConfigureAwait(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<bool> IsPlayableGame(Game game) {
|
||||
ArgumentNullException.ThrowIfNull(game);
|
||||
|
||||
@@ -1142,19 +1327,53 @@ public sealed class CardsFarmer : IAsyncDisposable, IDisposable {
|
||||
private async Task<bool?> ShouldFarm(Game game) {
|
||||
ArgumentNullException.ThrowIfNull(game);
|
||||
|
||||
ushort? cardsRemaining = await GetCardsRemaining(game.AppID).ConfigureAwait(false);
|
||||
Game? latestGameData = await GetGameCardsInfo(game.AppID).ConfigureAwait(false);
|
||||
|
||||
if (!cardsRemaining.HasValue) {
|
||||
if (latestGameData == null) {
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningCouldNotCheckCardsStatus, game.AppID, game.GameName));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
game.CardsRemaining = cardsRemaining.Value;
|
||||
game.CardsRemaining = latestGameData.CardsRemaining;
|
||||
|
||||
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.IdlingStatusForGame, game.AppID, game.GameName, game.CardsRemaining));
|
||||
|
||||
return game.CardsRemaining > 0;
|
||||
if (game.CardsRemaining == 0) {
|
||||
Bot.BotDatabase.FarmingRiskyIgnoredAppIDs[game.AppID] = DateTime.UtcNow.AddDays(DaysToIgnoreRiskyAppIDs);
|
||||
Bot.BotDatabase.FarmingRiskyPrioritizedAppIDs.Remove(game.AppID);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool ShouldIdle(uint appID) {
|
||||
if (appID == 0) {
|
||||
throw new ArgumentOutOfRangeException(nameof(appID));
|
||||
}
|
||||
|
||||
if (SalesBlacklist.Contains(appID) || (ASF.GlobalConfig?.Blacklist.Contains(appID) == true) || Bot.IsBlacklistedFromIdling(appID) || (Bot.BotConfig.FarmPriorityQueueOnly && !Bot.IsPriorityIdling(appID))) {
|
||||
// We're configured to ignore this appID, so skip it
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (ConcurrentDictionary<uint, DateTime> sourceOfIgnoredAppIDs in SourcesOfIgnoredAppIDs) {
|
||||
if (!sourceOfIgnoredAppIDs.TryGetValue(appID, out DateTime ignoredUntil)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ignoredUntil > DateTime.UtcNow) {
|
||||
// This game is still ignored
|
||||
return false;
|
||||
}
|
||||
|
||||
// This game served its time as being ignored
|
||||
sourceOfIgnoredAppIDs.TryRemove(appID, out _);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task SortGamesToFarm() {
|
||||
|
||||
@@ -45,7 +45,7 @@ public sealed class Game : IEquatable<Game> {
|
||||
AppID = appID > 0 ? appID : throw new ArgumentOutOfRangeException(nameof(appID));
|
||||
GameName = !string.IsNullOrEmpty(gameName) ? gameName : throw new ArgumentNullException(nameof(gameName));
|
||||
HoursPlayed = hoursPlayed >= 0 ? hoursPlayed : throw new ArgumentOutOfRangeException(nameof(hoursPlayed));
|
||||
CardsRemaining = cardsRemaining > 0 ? cardsRemaining : throw new ArgumentOutOfRangeException(nameof(cardsRemaining));
|
||||
CardsRemaining = cardsRemaining;
|
||||
BadgeLevel = badgeLevel;
|
||||
|
||||
PlayableAppID = appID;
|
||||
|
||||
34
ArchiSteamFarm/Steam/Data/BoosterCreatorEntry.cs
Normal file
34
ArchiSteamFarm/Steam/Data/BoosterCreatorEntry.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// |
|
||||
// Copyright 2015-2023 Ł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.Diagnostics.CodeAnalysis;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ArchiSteamFarm.Steam.Data;
|
||||
|
||||
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
|
||||
public sealed class BoosterCreatorEntry {
|
||||
[JsonProperty("appid", Required = Required.Always)]
|
||||
public uint AppID { get; private set; }
|
||||
|
||||
[JsonConstructor]
|
||||
private BoosterCreatorEntry() { }
|
||||
}
|
||||
@@ -43,6 +43,9 @@ public sealed class Confirmation {
|
||||
[JsonConstructor]
|
||||
private Confirmation() { }
|
||||
|
||||
[UsedImplicitly]
|
||||
public static bool ShouldSerializeNonce() => false;
|
||||
|
||||
[PublicAPI]
|
||||
public enum EConfirmationType : byte {
|
||||
Unknown,
|
||||
|
||||
@@ -39,6 +39,7 @@ public sealed class ArchiHandler : ClientMsgHandler {
|
||||
internal const byte MaxGamesPlayedConcurrently = 32; // This is limit introduced by Steam Network
|
||||
|
||||
private readonly ArchiLogger ArchiLogger;
|
||||
private readonly SteamUnifiedMessages.UnifiedService<IAuthentication> UnifiedAuthenticationService;
|
||||
private readonly SteamUnifiedMessages.UnifiedService<IChatRoom> UnifiedChatRoomService;
|
||||
private readonly SteamUnifiedMessages.UnifiedService<IClanChatRooms> UnifiedClanChatRoomsService;
|
||||
private readonly SteamUnifiedMessages.UnifiedService<ICredentials> UnifiedCredentialsService;
|
||||
@@ -53,6 +54,7 @@ public sealed class ArchiHandler : ClientMsgHandler {
|
||||
ArgumentNullException.ThrowIfNull(steamUnifiedMessages);
|
||||
|
||||
ArchiLogger = archiLogger ?? throw new ArgumentNullException(nameof(archiLogger));
|
||||
UnifiedAuthenticationService = steamUnifiedMessages.CreateService<IAuthentication>();
|
||||
UnifiedChatRoomService = steamUnifiedMessages.CreateService<IChatRoom>();
|
||||
UnifiedClanChatRoomsService = steamUnifiedMessages.CreateService<IClanChatRooms>();
|
||||
UnifiedCredentialsService = steamUnifiedMessages.CreateService<ICredentials>();
|
||||
@@ -358,6 +360,37 @@ public sealed class ArchiHandler : ClientMsgHandler {
|
||||
Client.Send(request);
|
||||
}
|
||||
|
||||
internal async Task<CAuthentication_AccessToken_GenerateForApp_Response?> GenerateAccessTokens(string refreshToken) {
|
||||
if (string.IsNullOrEmpty(refreshToken)) {
|
||||
throw new ArgumentNullException(nameof(refreshToken));
|
||||
}
|
||||
|
||||
if (Client == null) {
|
||||
throw new InvalidOperationException(nameof(Client));
|
||||
}
|
||||
|
||||
if (!Client.IsConnected || (Client.SteamID == null)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
CAuthentication_AccessToken_GenerateForApp_Request request = new() {
|
||||
refresh_token = refreshToken,
|
||||
steamid = Client.SteamID
|
||||
};
|
||||
|
||||
SteamUnifiedMessages.ServiceMethodResponse response;
|
||||
|
||||
try {
|
||||
response = await UnifiedAuthenticationService.SendMessage(x => x.GenerateAccessTokenForApp(request)).ToLongRunningTask().ConfigureAwait(false);
|
||||
} catch (Exception e) {
|
||||
ArchiLogger.LogGenericWarningException(e);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.Result == EResult.OK ? response.GetDeserializedResponse<CAuthentication_AccessToken_GenerateForApp_Response>() : null;
|
||||
}
|
||||
|
||||
internal async Task<ulong> GetClanChatGroupID(ulong steamID) {
|
||||
if ((steamID == 0) || !new SteamID(steamID).IsClanAccount) {
|
||||
throw new ArgumentOutOfRangeException(nameof(steamID));
|
||||
@@ -585,10 +618,10 @@ public sealed class ArchiHandler : ClientMsgHandler {
|
||||
|
||||
if (gameIDs.Count > 0) {
|
||||
#pragma warning disable CA1508 // False positive, not every IReadOnlyCollection is ISet
|
||||
IEnumerable<uint> uniqueValidGameIDs = (gameIDs as ISet<uint> ?? gameIDs.Distinct()).Where(static gameID => gameID > 0);
|
||||
ISet<uint> uniqueGameIDs = gameIDs as ISet<uint> ?? gameIDs.ToHashSet();
|
||||
#pragma warning restore CA1508 // False positive, not every IReadOnlyCollection is ISet
|
||||
|
||||
foreach (uint gameID in uniqueValidGameIDs) {
|
||||
foreach (uint gameID in uniqueGameIDs.Where(static gameID => gameID > 0)) {
|
||||
if (request.Body.games_played.Count >= MaxGamesPlayedConcurrently) {
|
||||
if (string.IsNullOrEmpty(gameName)) {
|
||||
throw new ArgumentOutOfRangeException(nameof(gameIDs));
|
||||
|
||||
@@ -52,7 +52,6 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
private const ushort MaxItemsInSingleInventoryRequest = 5000;
|
||||
private const byte MinimumSessionValidityInSeconds = 10;
|
||||
private const string SteamAppsService = "ISteamApps";
|
||||
private const string SteamUserAuthService = "ISteamUserAuth";
|
||||
private const string SteamUserService = "ISteamUser";
|
||||
private const string TwoFactorService = "ITwoFactorService";
|
||||
|
||||
@@ -139,6 +138,108 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
return string.IsNullOrEmpty(VanityURL) ? $"/profiles/{Bot.SteamID}" : $"/id/{VanityURL}";
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public async Task<ImmutableHashSet<BoosterCreatorEntry>?> GetBoosterCreatorEntries() {
|
||||
Uri request = new(SteamCommunityURL, "/tradingcards/boostercreator");
|
||||
|
||||
using HtmlDocumentResponse? response = await UrlGetToHtmlDocumentWithSession(request, checkSessionPreemptively: false).ConfigureAwait(false);
|
||||
|
||||
if (response?.Content == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
IList<INode> scriptNodes = response.Content.SelectNodes("//script[@type='text/javascript']");
|
||||
|
||||
if (scriptNodes.Count == 0) {
|
||||
Bot.ArchiLogger.LogNullError(scriptNodes);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
ImmutableHashSet<BoosterCreatorEntry>? result = null;
|
||||
|
||||
foreach (INode scriptNode in scriptNodes) {
|
||||
int startIndex = scriptNode.TextContent.IndexOf("CBoosterCreatorPage.Init(", StringComparison.Ordinal);
|
||||
|
||||
if (startIndex < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
startIndex += 25;
|
||||
|
||||
int endIndex = scriptNode.TextContent.IndexOf("],", startIndex, StringComparison.Ordinal);
|
||||
|
||||
if (endIndex <= startIndex) {
|
||||
Bot.ArchiLogger.LogNullError(endIndex);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
string json = scriptNode.TextContent[startIndex..(endIndex + 1)];
|
||||
|
||||
try {
|
||||
result = JsonConvert.DeserializeObject<ImmutableHashSet<BoosterCreatorEntry>>(json);
|
||||
} catch (Exception e) {
|
||||
Bot.ArchiLogger.LogGenericException(e);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (result == null) {
|
||||
Bot.ArchiLogger.LogNullError(result);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public async Task<ImmutableHashSet<uint>?> GetBoosterEligibility() {
|
||||
Uri request = new(SteamCommunityURL, "/my/ajaxgetboostereligibility");
|
||||
|
||||
using HtmlDocumentResponse? response = await UrlGetToHtmlDocumentWithSession(request, checkSessionPreemptively: false).ConfigureAwait(false);
|
||||
|
||||
if (response?.Content == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
HashSet<uint> result = new();
|
||||
|
||||
IEnumerable<IAttr> linkNodes = response.Content.SelectNodes<IAttr>("//li[@class='booster_eligibility_game']/a/@href");
|
||||
|
||||
foreach (string hrefText in linkNodes.Select(static linkNode => linkNode.Value)) {
|
||||
if (string.IsNullOrEmpty(hrefText)) {
|
||||
Bot.ArchiLogger.LogNullError(hrefText);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
int index = hrefText.LastIndexOf('/');
|
||||
|
||||
if ((index <= 0) || (hrefText.Length <= index + 2)) {
|
||||
Bot.ArchiLogger.LogNullError(index);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
string appIDText = hrefText[(index + 1)..];
|
||||
|
||||
if (string.IsNullOrEmpty(appIDText) || !uint.TryParse(appIDText, out uint appID) || (appID == 0)) {
|
||||
Bot.ArchiLogger.LogNullError(appIDText);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
result.Add(appID);
|
||||
}
|
||||
|
||||
return result.ToImmutableHashSet();
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public async IAsyncEnumerable<Asset> GetInventoryAsync(ulong steamID = 0, uint appID = Asset.SteamAppID, ulong contextID = Asset.SteamCommunityContextID) {
|
||||
if (appID == 0) {
|
||||
@@ -1561,7 +1662,7 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
throw new ArgumentOutOfRangeException(nameof(subID));
|
||||
}
|
||||
|
||||
Uri request = new(SteamCheckoutURL, $"/checkout/addfreelicense/{subID}");
|
||||
Uri request = new(SteamStoreURL, $"/freelicense/addfreelicense/{subID}");
|
||||
|
||||
// Extra entry for sessionID
|
||||
Dictionary<string, string> data = new(2, StringComparer.Ordinal) {
|
||||
@@ -1746,14 +1847,18 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
return result;
|
||||
}
|
||||
|
||||
internal async Task<IDocument?> GetBadgePage(byte page) {
|
||||
internal async Task<IDocument?> GetBadgePage(byte page, byte maxTries = WebBrowser.MaxTries) {
|
||||
if (page == 0) {
|
||||
throw new ArgumentOutOfRangeException(nameof(page));
|
||||
}
|
||||
|
||||
if (maxTries == 0) {
|
||||
throw new ArgumentOutOfRangeException(nameof(maxTries));
|
||||
}
|
||||
|
||||
Uri request = new(SteamCommunityURL, $"/my/badges?l=english&p={page}");
|
||||
|
||||
HtmlDocumentResponse? response = await UrlGetToHtmlDocumentWithSession(request, checkSessionPreemptively: false).ConfigureAwait(false);
|
||||
HtmlDocumentResponse? response = await UrlGetToHtmlDocumentWithSession(request, checkSessionPreemptively: false, maxTries: maxTries).ConfigureAwait(false);
|
||||
|
||||
return response?.Content;
|
||||
}
|
||||
@@ -2184,7 +2289,7 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
return response?.Content?.Success;
|
||||
}
|
||||
|
||||
internal async Task<bool> Init(ulong steamID, EUniverse universe, string webAPIUserNonce, string? parentalCode = null) {
|
||||
internal async Task<bool> Init(ulong steamID, EUniverse universe, string accessToken, string? parentalCode = null) {
|
||||
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
|
||||
throw new ArgumentOutOfRangeException(nameof(steamID));
|
||||
}
|
||||
@@ -2193,83 +2298,8 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
throw new InvalidEnumArgumentException(nameof(universe), (int) universe, typeof(EUniverse));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(webAPIUserNonce)) {
|
||||
throw new ArgumentNullException(nameof(webAPIUserNonce));
|
||||
}
|
||||
|
||||
byte[]? publicKey = KeyDictionary.GetPublicKey(universe);
|
||||
|
||||
if ((publicKey == null) || (publicKey.Length == 0)) {
|
||||
Bot.ArchiLogger.LogNullError(publicKey);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Generate a random 32-byte session key
|
||||
byte[] sessionKey = CryptoHelper.GenerateRandomBlock(32);
|
||||
|
||||
// RSA encrypt our session key with the public key for the universe we're on
|
||||
byte[] encryptedSessionKey;
|
||||
|
||||
using (RSACrypto rsa = new(publicKey)) {
|
||||
encryptedSessionKey = rsa.Encrypt(sessionKey);
|
||||
}
|
||||
|
||||
// Generate login key from the user nonce that we've received from Steam network
|
||||
byte[] loginKey = Encoding.UTF8.GetBytes(webAPIUserNonce);
|
||||
|
||||
// AES encrypt our login key with our session key
|
||||
byte[] encryptedLoginKey = CryptoHelper.SymmetricEncrypt(loginKey, sessionKey);
|
||||
|
||||
Dictionary<string, object?> arguments = new(3, StringComparer.Ordinal) {
|
||||
{ "encrypted_loginkey", encryptedLoginKey },
|
||||
{ "sessionkey", encryptedSessionKey },
|
||||
{ "steamid", steamID }
|
||||
};
|
||||
|
||||
// We're now ready to send the data to Steam API
|
||||
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.LoggingIn, SteamUserAuthService));
|
||||
|
||||
KeyValue? response;
|
||||
|
||||
// We do not use usual retry pattern here as webAPIUserNonce is valid only for a single request
|
||||
// Even during timeout, webAPIUserNonce is most likely already invalid
|
||||
// Instead, the caller is supposed to ask for new webAPIUserNonce and call Init() again on failure
|
||||
using (WebAPI.AsyncInterface steamUserAuthService = Bot.SteamConfiguration.GetAsyncWebAPIInterface(SteamUserAuthService)) {
|
||||
steamUserAuthService.Timeout = WebBrowser.Timeout;
|
||||
|
||||
try {
|
||||
response = await WebLimitRequest(
|
||||
WebAPI.DefaultBaseAddress,
|
||||
|
||||
// ReSharper disable once AccessToDisposedClosure
|
||||
async () => await steamUserAuthService.CallAsync(HttpMethod.Post, "AuthenticateUser", args: arguments).ConfigureAwait(false)
|
||||
).ConfigureAwait(false);
|
||||
} catch (TaskCanceledException e) {
|
||||
Bot.ArchiLogger.LogGenericDebuggingException(e);
|
||||
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
Bot.ArchiLogger.LogGenericWarningException(e);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
string? steamLogin = response["token"].AsString();
|
||||
|
||||
if (string.IsNullOrEmpty(steamLogin)) {
|
||||
Bot.ArchiLogger.LogNullError(steamLogin);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
string? steamLoginSecure = response["tokensecure"].AsString();
|
||||
|
||||
if (string.IsNullOrEmpty(steamLoginSecure)) {
|
||||
Bot.ArchiLogger.LogNullError(steamLoginSecure);
|
||||
|
||||
return false;
|
||||
if (string.IsNullOrEmpty(accessToken)) {
|
||||
throw new ArgumentNullException(nameof(accessToken));
|
||||
}
|
||||
|
||||
string sessionID = Convert.ToBase64String(Encoding.UTF8.GetBytes(steamID.ToString(CultureInfo.InvariantCulture)));
|
||||
@@ -2279,10 +2309,7 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
WebBrowser.CookieContainer.Add(new Cookie("sessionid", sessionID, "/", $".{SteamHelpURL.Host}"));
|
||||
WebBrowser.CookieContainer.Add(new Cookie("sessionid", sessionID, "/", $".{SteamStoreURL.Host}"));
|
||||
|
||||
WebBrowser.CookieContainer.Add(new Cookie("steamLogin", steamLogin, "/", $".{SteamCheckoutURL.Host}"));
|
||||
WebBrowser.CookieContainer.Add(new Cookie("steamLogin", steamLogin, "/", $".{SteamCommunityURL.Host}"));
|
||||
WebBrowser.CookieContainer.Add(new Cookie("steamLogin", steamLogin, "/", $".{SteamHelpURL.Host}"));
|
||||
WebBrowser.CookieContainer.Add(new Cookie("steamLogin", steamLogin, "/", $".{SteamStoreURL.Host}"));
|
||||
string steamLoginSecure = $"{steamID}||{accessToken}";
|
||||
|
||||
WebBrowser.CookieContainer.Add(new Cookie("steamLoginSecure", steamLoginSecure, "/", $".{SteamCheckoutURL.Host}"));
|
||||
WebBrowser.CookieContainer.Add(new Cookie("steamLoginSecure", steamLoginSecure, "/", $".{SteamCommunityURL.Host}"));
|
||||
@@ -2676,7 +2703,7 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
}
|
||||
|
||||
Bot.ArchiLogger.LogGenericInfo(Strings.RefreshingOurSession);
|
||||
bool result = await Bot.RefreshSession().ConfigureAwait(false);
|
||||
bool result = await Bot.RefreshWebSession(true).ConfigureAwait(false);
|
||||
|
||||
DateTime now = DateTime.UtcNow;
|
||||
|
||||
|
||||
@@ -62,6 +62,9 @@ public sealed class BotConfig {
|
||||
[PublicAPI]
|
||||
public const bool DefaultEnabled = false;
|
||||
|
||||
[PublicAPI]
|
||||
public const bool DefaultEnableRiskyCardsDiscovery = false;
|
||||
|
||||
[PublicAPI]
|
||||
public const bool DefaultFarmPriorityQueueOnly = false;
|
||||
|
||||
@@ -171,6 +174,9 @@ public sealed class BotConfig {
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
public bool Enabled { get; private set; } = DefaultEnabled;
|
||||
|
||||
[JsonProperty]
|
||||
public bool EnableRiskyCardsDiscovery { get; private set; } = DefaultEnableRiskyCardsDiscovery;
|
||||
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
public ImmutableList<EFarmingOrder> FarmingOrders { get; private set; } = DefaultFarmingOrders;
|
||||
|
||||
@@ -343,6 +349,9 @@ public sealed class BotConfig {
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeEnabled() => !Saving || (Enabled != DefaultEnabled);
|
||||
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeEnableRiskyCardsDiscovery() => !Saving || (EnableRiskyCardsDiscovery != DefaultEnableRiskyCardsDiscovery);
|
||||
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeFarmingOrders() => !Saving || ((FarmingOrders != DefaultFarmingOrders) && !FarmingOrders.SequenceEqual(DefaultFarmingOrders));
|
||||
|
||||
|
||||
@@ -43,6 +43,12 @@ public sealed class BotDatabase : GenericDatabase {
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
internal readonly ConcurrentHashSet<uint> FarmingPriorityQueueAppIDs = new();
|
||||
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
internal readonly ObservableConcurrentDictionary<uint, DateTime> FarmingRiskyIgnoredAppIDs = new();
|
||||
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
internal readonly ConcurrentHashSet<uint> FarmingRiskyPrioritizedAppIDs = new();
|
||||
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
internal readonly ConcurrentHashSet<uint> MatchActivelyBlacklistAppIDs = new();
|
||||
|
||||
@@ -62,6 +68,19 @@ public sealed class BotDatabase : GenericDatabase {
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
private readonly OrderedDictionary GamesToRedeemInBackground = new();
|
||||
|
||||
internal string? AccessToken {
|
||||
get => BackingAccessToken;
|
||||
|
||||
set {
|
||||
if (BackingAccessToken == value) {
|
||||
return;
|
||||
}
|
||||
|
||||
BackingAccessToken = value;
|
||||
Utilities.InBackground(Save);
|
||||
}
|
||||
}
|
||||
|
||||
internal MobileAuthenticator? MobileAuthenticator {
|
||||
get => BackingMobileAuthenticator;
|
||||
|
||||
@@ -101,6 +120,9 @@ public sealed class BotDatabase : GenericDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
[JsonProperty]
|
||||
private string? BackingAccessToken;
|
||||
|
||||
[JsonProperty($"_{nameof(MobileAuthenticator)}")]
|
||||
private MobileAuthenticator? BackingMobileAuthenticator;
|
||||
|
||||
@@ -122,10 +144,15 @@ public sealed class BotDatabase : GenericDatabase {
|
||||
private BotDatabase() {
|
||||
FarmingBlacklistAppIDs.OnModified += OnObjectModified;
|
||||
FarmingPriorityQueueAppIDs.OnModified += OnObjectModified;
|
||||
FarmingRiskyIgnoredAppIDs.OnModified += OnObjectModified;
|
||||
FarmingRiskyPrioritizedAppIDs.OnModified += OnObjectModified;
|
||||
MatchActivelyBlacklistAppIDs.OnModified += OnObjectModified;
|
||||
TradingBlacklistSteamIDs.OnModified += OnObjectModified;
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeBackingAccessToken() => !string.IsNullOrEmpty(BackingAccessToken);
|
||||
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeBackingMobileAuthenticator() => BackingMobileAuthenticator != null;
|
||||
|
||||
@@ -141,6 +168,12 @@ public sealed class BotDatabase : GenericDatabase {
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeFarmingPriorityQueueAppIDs() => FarmingPriorityQueueAppIDs.Count > 0;
|
||||
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeFarmingRiskyIgnoredAppIDs() => !FarmingRiskyIgnoredAppIDs.IsEmpty;
|
||||
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeFarmingRiskyPrioritizedAppIDs() => FarmingRiskyPrioritizedAppIDs.Count > 0;
|
||||
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeGamesToRedeemInBackground() => HasGamesToRedeemInBackground;
|
||||
|
||||
@@ -155,6 +188,8 @@ public sealed class BotDatabase : GenericDatabase {
|
||||
// Events we registered
|
||||
FarmingBlacklistAppIDs.OnModified -= OnObjectModified;
|
||||
FarmingPriorityQueueAppIDs.OnModified -= OnObjectModified;
|
||||
FarmingRiskyIgnoredAppIDs.OnModified -= OnObjectModified;
|
||||
FarmingRiskyPrioritizedAppIDs.OnModified -= OnObjectModified;
|
||||
MatchActivelyBlacklistAppIDs.OnModified -= OnObjectModified;
|
||||
TradingBlacklistSteamIDs.OnModified -= OnObjectModified;
|
||||
|
||||
@@ -233,6 +268,14 @@ public sealed class BotDatabase : GenericDatabase {
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
internal void PerformMaintenance() {
|
||||
DateTime now = DateTime.UtcNow;
|
||||
|
||||
foreach (uint appID in FarmingRiskyIgnoredAppIDs.Where(entry => entry.Value < now).Select(static entry => entry.Key)) {
|
||||
FarmingRiskyIgnoredAppIDs.Remove(appID);
|
||||
}
|
||||
}
|
||||
|
||||
internal void RemoveGameToRedeemInBackground(string key) {
|
||||
if (string.IsNullOrEmpty(key)) {
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
|
||||
@@ -60,6 +60,9 @@ public sealed class GlobalConfig {
|
||||
[PublicAPI]
|
||||
public const bool DefaultDebug = false;
|
||||
|
||||
[PublicAPI]
|
||||
public const string? DefaultDefaultBot = null;
|
||||
|
||||
[PublicAPI]
|
||||
public const byte DefaultFarmingDelay = 15;
|
||||
|
||||
@@ -208,6 +211,9 @@ public sealed class GlobalConfig {
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
public bool Debug { get; private set; } = DefaultDebug;
|
||||
|
||||
[JsonProperty]
|
||||
public string? DefaultBot { get; private set; } = DefaultDefaultBot;
|
||||
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
[Range(1, byte.MaxValue)]
|
||||
public byte FarmingDelay { get; private set; } = DefaultFarmingDelay;
|
||||
@@ -363,6 +369,9 @@ public sealed class GlobalConfig {
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeDebug() => !Saving || (Debug != DefaultDebug);
|
||||
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeDefaultBot() => !Saving || (DefaultBot != DefaultDefaultBot);
|
||||
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeFarmingDelay() => !Saving || (FarmingDelay != DefaultFarmingDelay);
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ internal static class GitHub {
|
||||
MarkdownDocument markdownDocument = Markdown.Parse(markdownText);
|
||||
MarkdownDocument result = new();
|
||||
|
||||
foreach (Block block in markdownDocument.SkipWhile(static block => block is not HeadingBlock { Inline.FirstChild: LiteralInline literalInline } || !literalInline.Content.ToString().Equals("Changelog", StringComparison.OrdinalIgnoreCase)).Skip(1).TakeWhile(static block => block is not ThematicBreakBlock).ToList()) {
|
||||
foreach (Block block in markdownDocument.SkipWhile(static block => block is not HeadingBlock { Inline.FirstChild: LiteralInline literalInline } || (literalInline.Content.ToString()?.Equals("Changelog", StringComparison.OrdinalIgnoreCase) != true)).Skip(1).TakeWhile(static block => block is not ThematicBreakBlock).ToList()) {
|
||||
// All blocks that we're interested in must be removed from original markdownDocument firstly
|
||||
markdownDocument.Remove(block);
|
||||
result.Add(block);
|
||||
|
||||
@@ -30,7 +30,7 @@ ProtectProc=invisible
|
||||
ProtectSystem=strict
|
||||
ReadWritePaths=/home/%i/ArchiSteamFarm /tmp
|
||||
RemoveIPC=yes
|
||||
RestrictAddressFamilies=AF_INET AF_INET6
|
||||
RestrictAddressFamilies=AF_INET AF_INET6 AF_NETLINK AF_UNIX
|
||||
RestrictNamespaces=yes
|
||||
RestrictRealtime=yes
|
||||
RestrictSUIDSGID=yes
|
||||
|
||||
@@ -30,7 +30,7 @@ ProtectProc=invisible
|
||||
ProtectSystem=strict
|
||||
ReadWritePaths=/home/%i/ArchiSteamFarm /tmp
|
||||
RemoveIPC=yes
|
||||
RestrictAddressFamilies=AF_INET AF_INET6
|
||||
RestrictAddressFamilies=AF_INET AF_INET6 AF_NETLINK AF_UNIX
|
||||
RestrictNamespaces=yes
|
||||
RestrictRealtime=yes
|
||||
RestrictSUIDSGID=yes
|
||||
|
||||
@@ -30,7 +30,7 @@ ProtectProc=invisible
|
||||
ProtectSystem=strict
|
||||
ReadWritePaths=/home/%i/ArchiSteamFarm /tmp
|
||||
RemoveIPC=yes
|
||||
RestrictAddressFamilies=AF_INET AF_INET6
|
||||
RestrictAddressFamilies=AF_INET AF_INET6 AF_NETLINK AF_UNIX
|
||||
RestrictNamespaces=yes
|
||||
RestrictRealtime=yes
|
||||
RestrictSUIDSGID=yes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>5.4.9.2</Version>
|
||||
<Version>5.4.12.2</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
@@ -5,19 +5,20 @@
|
||||
<PackageVersion Include="CryptSharpStandard" Version="1.0.0" />
|
||||
<PackageVersion Include="Humanizer" Version="2.14.1" />
|
||||
<PackageVersion Include="JetBrains.Annotations" Version="2023.2.0" />
|
||||
<PackageVersion Include="Markdig.Signed" Version="0.32.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.7.0" />
|
||||
<PackageVersion Include="Markdig.Signed" Version="0.33.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||
<PackageVersion Include="MSTest.TestAdapter" Version="3.1.1" />
|
||||
<PackageVersion Include="MSTest.TestFramework" Version="3.1.1" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageVersion Include="Nito.AsyncEx.Coordination" Version="5.1.2" />
|
||||
<PackageVersion Include="NLog.Web.AspNetCore" Version="5.3.3" />
|
||||
<PackageVersion Include="NLog.Web.AspNetCore" Version="5.3.5" />
|
||||
<PackageVersion Include="SteamKit2" Version="2.5.0-Beta.1" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.Annotations" Version="6.5.0" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.5.0" />
|
||||
<PackageVersion Include="System.Composition" Version="7.0.0" />
|
||||
<PackageVersion Include="System.Composition.AttributedModel" Version="7.0.0" />
|
||||
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="7.0.3" />
|
||||
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
|
||||
<PackageVersion Include="zxcvbn-core" Version="7.0.92" />
|
||||
</ItemGroup>
|
||||
@@ -27,7 +28,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net481' OR '$(TargetFramework)' == 'netstandard2.1'">
|
||||
<PackageVersion Include="JustArchiNET.Madness" Version="3.15.0" />
|
||||
<PackageVersion Include="JustArchiNET.Madness" Version="3.16.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" />
|
||||
|
||||
2
wiki
2
wiki
Submodule wiki updated: 3edaba1ddc...7cf972f6e8
Reference in New Issue
Block a user