Compare commits

...

168 Commits

Author SHA1 Message Date
Archi
22177f1fe7 Fix RefreshToken handling for existing setups 2023-10-19 14:36:05 +02:00
Archi
ab1edae9c7 Bump 2023-10-19 13:43:58 +02:00
Archi
53d80345d3 Bump 2023-10-19 13:43:42 +02:00
Łukasz Domeradzki
d571cd9580 Closes #3043 (#3044)
* Implement support for access tokens

A bit more work and testing is needed

* Make ValidUntil computed, fix netf, among others

* netf fixes as always

* Allow AWH to forcefully refresh session

* Unify access token lifetime
2023-10-19 13:38:39 +02:00
ArchiBot
4106c5f41a Automatic translations update 2023-10-19 02:05:44 +00:00
Łukasz Domeradzki
71fa7560f7 Fix qodana for PRs (#3040)
* Fix qodana for PRs

* Update code-quality.yml
2023-10-18 23:51:35 +02:00
Łukasz Domeradzki
32196a53cd Closes #3039 (#3041) 2023-10-18 23:27:26 +02:00
Łukasz Domeradzki
5d467aca9a Add default bot implementation (#3038)
* Add default bot implementation

* Thought so

* Brain damage for netf
2023-10-18 22:57:58 +02:00
renovate[bot]
52eacaf577 Update ASF-ui digest to 1447dbe 2023-10-18 19:35:28 +00:00
Archi
d44c57a48f Misc 2023-10-18 10:56:07 +02:00
Archi
629a750df6 Bump 2023-10-18 10:47:42 +02:00
Archi
21b1a319ca Fix netf with new code 2023-10-18 10:35:44 +02:00
Archi
bb1f02e788 Closes #3029 2023-10-18 10:27:50 +02:00
renovate[bot]
5d8826b71b Update ASF-ui digest to 1d748b6 2023-10-18 03:48:39 +00:00
ArchiBot
829e5ac4fb Automatic translations update 2023-10-18 02:06:16 +00:00
renovate[bot]
feec46da32 Update wiki digest to bd2c085 2023-10-17 22:58:10 +00:00
renovate[bot]
4288f35e99 Update actions/checkout action to v4.1.1 2023-10-17 17:26:45 +00:00
ArchiBot
69004d20ce Automatic translations update 2023-10-17 02:06:30 +00:00
renovate[bot]
91ce53d283 Update wiki digest to 3408c8a 2023-10-16 23:00:59 +00:00
renovate[bot]
400ea64cfd Update dependency NLog.Web.AspNetCore to v5.3.5 2023-10-15 20:01:50 +00:00
ArchiBot
1b047dfbff Automatic translations update 2023-10-15 02:06:55 +00:00
renovate[bot]
f9cd805304 Update ASF-ui digest to 6fab369 2023-10-14 04:22:54 +00:00
renovate[bot]
74e2ad2f40 Update github/codeql-action action to v2.22.3 2023-10-13 16:10:15 +00:00
ArchiBot
0242ed9f10 Automatic translations update 2023-10-13 02:06:53 +00:00
renovate[bot]
b31c8266eb Update github/codeql-action action to v2.22.2 2023-10-12 10:41:00 +00:00
renovate[bot]
ddd55fd4d9 Update ASF-ui digest to b7537c4 2023-10-12 07:43:41 +00:00
ArchiBot
a324a69371 Automatic translations update 2023-10-12 02:05:09 +00:00
renovate[bot]
de67195185 Update wiki digest to 944296e 2023-10-11 19:10:30 +00:00
Archi
290f16b3fe Bump 2023-10-11 19:24:27 +02:00
Archi
324d208416 Kill old refresh token when getting Expired/Expired 2023-10-11 19:22:40 +02:00
renovate[bot]
5db85aa6f1 Update ASF-ui digest to be9b550 2023-10-11 03:03:04 +00:00
ArchiBot
4d46c4c4b1 Automatic translations update 2023-10-11 02:05:26 +00:00
renovate[bot]
f93a10bcd0 Update ASF-ui digest to 184ffad 2023-10-10 13:07:08 +00:00
renovate[bot]
56c6246a20 Update ad-m/github-push-action action to v0.8.0 2023-10-10 06:05:28 +00:00
renovate[bot]
2f4c82b563 Update ASF-ui digest to 0134a94 2023-10-10 03:45:05 +00:00
ArchiBot
8bd3c8b20d Automatic translations update 2023-10-10 02:05:49 +00:00
renovate[bot]
05c93aa82b Update JetBrains/qodana-action action to v2023.2.8 2023-10-09 16:48:38 +00:00
renovate[bot]
e8babfb329 Update github/codeql-action action to v2.22.1 2023-10-09 11:28:12 +00:00
renovate[bot]
fd2d1baa1f Update ASF-ui digest to a3f5b60 2023-10-09 03:24:00 +00:00
ArchiBot
4a5b8bbf3c Automatic translations update 2023-10-09 02:05:39 +00:00
renovate[bot]
5c8ebf7ec1 Update wiki digest to b40fb89 2023-10-08 12:02:33 +00:00
renovate[bot]
d972c93072 Update ASF-ui digest to 137b040 2023-10-07 04:46:42 +00:00
ArchiBot
aa75f4204e Automatic translations update 2023-10-07 02:05:01 +00:00
renovate[bot]
5ad1815ba0 Update github/codeql-action action to v2.22.0 2023-10-06 12:32:23 +00:00
renovate[bot]
0518a35fb4 Update ASF-ui digest to cf7f1a0 2023-10-06 07:20:40 +00:00
ArchiBot
4dbd9720f2 Automatic translations update 2023-10-06 02:05:38 +00:00
renovate[bot]
4d06104306 Update ASF-ui digest to 50cc9c7 2023-10-05 18:01:57 +00:00
ArchiBot
5c6229da1b Automatic translations update 2023-10-05 02:06:46 +00:00
ArchiBot
8b661874da Automatic translations update 2023-10-04 02:06:12 +00:00
Archi
24b4c9a2f1 Bump 2023-10-03 21:19:23 +02:00
Archi
9dd1dd227f Closes #3020 2023-10-03 21:18:59 +02:00
ArchiBot
d3b1213fc5 Automatic translations update 2023-10-03 02:05:07 +00:00
Archi
603663a43c Bump 2023-10-03 02:00:57 +02:00
Archi
57cb519c44 Misc 2023-10-02 20:37:25 +02:00
Łukasz Domeradzki
3eb6cbf491 Closes #3017 (#3018)
* Closes #3017

* Allow risky discovery for non-first badge page failures

* Skip excessive risky check once game is fully farmed

* Misc
2023-10-02 20:31:59 +02:00
Archi
0396687f50 Bump 2023-10-02 16:16:46 +02:00
Archi
714cc5ae9d Closes #3019 2023-10-02 16:16:19 +02:00
renovate[bot]
4f478d829d Update github/codeql-action action to v2.21.9 2023-09-27 15:32:46 +00:00
Archi
d81144d549 Bump 2023-09-27 13:50:59 +02:00
Archi
f2563c582c Closes #3014
AF_UNIX is apparently needed on arch
AF_NETLINK will be mandatory since .NET 8, but based on my research even .NET 7 uses it in some conditions, so it makes sense to patch it right away as our previous settings were too restrictive, even if it did in fact work
2023-09-27 13:48:00 +02:00
renovate[bot]
927715188d Update ASF-ui digest to e510667 2023-09-27 03:32:16 +00:00
renovate[bot]
03beeb97dd Update ASF-ui digest to 6bb8251 2023-09-25 17:15:56 +00:00
Archi
a3dcb252c7 Bump 2023-09-25 12:11:31 +02:00
ArchiBot
52d4b6702c Automatic translations update 2023-09-25 02:05:43 +00:00
ArchiBot
4e5cd2e380 Automatic translations update 2023-09-24 02:05:33 +00:00
Archi
af6eb07607 Add support for bot.. and ..bot syntax 2023-09-24 02:45:06 +02:00
renovate[bot]
b5ecd31666 Update ASF-ui digest to 7a96c9c 2023-09-23 05:03:47 +00:00
ArchiBot
a2e5968a49 Automatic translations update 2023-09-23 02:03:42 +00:00
renovate[bot]
23da14e77e Update actions/checkout action to v4.1.0 2023-09-22 19:24:26 +00:00
Archi
b2a5daad2c Bump 2023-09-22 14:27:19 +02:00
renovate[bot]
6c9142132c Update ASF-ui digest to 79963aa 2023-09-22 03:31:51 +00:00
ArchiBot
cd9a939fb4 Automatic translations update 2023-09-22 02:05:14 +00:00
renovate[bot]
4f8b1a542a Update ASF-ui digest to 2676e37 2023-09-21 06:47:20 +00:00
ArchiBot
230332ca79 Automatic translations update 2023-09-21 02:05:17 +00:00
renovate[bot]
b082024d0b Update wiki digest to 4cc5e13 2023-09-20 11:53:36 +00:00
Łukasz Domeradzki
af4a64b99e Closes #3007 (#3008) 2023-09-20 13:53:15 +02:00
Rudokhvist
9163b57c3e Switch to different cat api in example plugin (#3006)
* switch to different cat api

* damn forgot json constructor

* check for empty array in responce

* revert meowresponse.cs

* use firstordefault()
2023-09-20 12:40:41 +02:00
renovate[bot]
69aaca3bad Update ASF-ui digest to 5875d1f 2023-09-20 03:45:24 +00:00
ArchiBot
8272a2b929 Automatic translations update 2023-09-20 02:05:25 +00:00
renovate[bot]
4d0ba1dbcc Update ASF-ui digest to 81b18fe 2023-09-19 22:01:00 +00:00
renovate[bot]
bf46c29ff6 Update github/codeql-action action to v2.21.8 2023-09-19 11:05:21 +00:00
ArchiBot
5fb3f88a52 Automatic translations update 2023-09-19 02:05:49 +00:00
renovate[bot]
74a193d3fe Update ASF-ui digest to 8cd89bc 2023-09-18 17:37:06 +00:00
renovate[bot]
283bb789bb Update crowdin/github-action action to v1.13.1 2023-09-18 07:48:42 +00:00
ArchiBot
515f96ea8e Automatic translations update 2023-09-18 02:05:11 +00:00
renovate[bot]
b9308810d6 Update ASF-ui digest to f77258e 2023-09-17 21:27:00 +00:00
ArchiBot
68be2d2442 Automatic translations update 2023-09-17 02:05:42 +00:00
renovate[bot]
d110259388 Update ASF-ui digest to 73cc7ef 2023-09-16 22:34:09 +00:00
ArchiBot
b03cf66882 Automatic translations update 2023-09-16 02:03:57 +00:00
ArchiBot
480abe95d1 Automatic translations update 2023-09-15 02:05:37 +00:00
renovate[bot]
9408a16a5c Update ASF-ui digest to 7b71bc2 2023-09-14 22:53:42 +00:00
renovate[bot]
a8c632f98d Update github/codeql-action action to v2.21.7 2023-09-14 17:36:10 +00:00
renovate[bot]
0016fc43a9 Update ASF-ui digest to c7922a4 2023-09-14 04:51:45 +00:00
ArchiBot
05006f0ba1 Automatic translations update 2023-09-14 02:04:43 +00:00
renovate[bot]
be0682e0f5 Update dependency JustArchiNET.Madness to v3.16.0 2023-09-13 20:25:32 +00:00
renovate[bot]
4b6c657ffc Update github/codeql-action action to v2.21.6 2023-09-13 17:26:15 +00:00
ArchiBot
a8bef43a30 Automatic translations update 2023-09-13 02:05:33 +00:00
renovate[bot]
6767c879c6 Update ASF-ui digest to ce59413 2023-09-12 15:26:45 +00:00
renovate[bot]
67fdaa8314 Update docker/setup-buildx-action action to v3 (#3004)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-12 13:52:14 +02:00
renovate[bot]
2aed6d6143 Update docker/login-action action to v3 (#3003)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-12 13:51:56 +02:00
renovate[bot]
94a18ed8dd Update docker/build-push-action action to v5 (#3002)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-12 13:51:29 +02:00
renovate[bot]
3c9bb23957 Update crowdin/github-action action to v1.13.0 2023-09-12 07:00:30 +00:00
ArchiBot
ca418b5570 Automatic translations update 2023-09-12 02:04:17 +00:00
renovate[bot]
954e811584 Update ASF-ui digest to 70d4a77 2023-09-10 18:47:35 +00:00
renovate[bot]
6beda3f404 Update crazy-max/ghaction-import-gpg action to v6 (#3001)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-10 20:47:19 +02:00
ArchiBot
0cc6f4c40a Automatic translations update 2023-09-10 02:05:42 +00:00
renovate[bot]
2fcc22768c Update ASF-ui digest to a949754 2023-09-09 02:14:09 +00:00
ArchiBot
29f70918c2 Automatic translations update 2023-09-09 02:03:21 +00:00
renovate[bot]
3f5d21758c Update docker/build-push-action action to v4.2.1 2023-09-08 16:32:25 +00:00
renovate[bot]
705e17c9bb Update docker/build-push-action action to v4.2.0 2023-09-08 11:21:02 +00:00
ArchiBot
2afc59f01a Automatic translations update 2023-09-08 02:05:04 +00:00
renovate[bot]
741df3c70f Update ASF-ui digest to ea8cb61 2023-09-07 04:09:56 +00:00
ArchiBot
6c4d6c1e75 Automatic translations update 2023-09-07 02:04:47 +00:00
renovate[bot]
52aa398ec7 Update dependency NLog.Web.AspNetCore to v5.3.4 2023-09-06 22:49:15 +00:00
renovate[bot]
72c14c3180 Update actions/upload-artifact action to v3.1.3 2023-09-06 20:40:38 +00:00
ArchiBot
78d2554e80 Automatic translations update 2023-09-05 02:04:09 +00:00
renovate[bot]
a46ec87252 Update ASF-ui digest to 81fcdcd 2023-09-04 17:20:07 +00:00
Archi
4a5c1c15ce Misc 2023-09-04 19:19:48 +02:00
renovate[bot]
7fc609653a Update actions/checkout action to v4 (#2996)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-04 19:16:07 +02:00
renovate[bot]
48077466ab Update JetBrains/qodana-action action to v2023.2.6 2023-09-04 12:52:26 +00:00
ArchiBot
4a76f84e3a Automatic translations update 2023-09-04 02:05:10 +00:00
renovate[bot]
3ab68cb574 Update ASF-ui digest to d5893d2 2023-09-03 20:33:29 +00:00
renovate[bot]
daf616c0b3 Update crazy-max/ghaction-import-gpg action to v5.4.0 2023-09-03 14:36:40 +00:00
ArchiBot
2ea8c89037 Automatic translations update 2023-09-03 02:05:37 +00:00
Archi
bf91dc77e8 Bump 2023-09-02 23:36:42 +02:00
Archi
7778618b75 Bump 2023-09-02 23:36:20 +02:00
Archi
6cc78e805e Closes #2993 2023-09-02 23:35:00 +02:00
ArchiBot
f5b4383cdf Automatic translations update 2023-09-02 02:02:58 +00:00
ArchiBot
ab5e950972 Automatic translations update 2023-09-01 02:06:45 +00:00
renovate[bot]
a3829f84b4 Update ASF-ui digest to 3ef126f 2023-08-31 02:02:29 +00:00
Archi
4e5fdabe0b Misc 2023-08-30 14:17:59 +02:00
renovate[bot]
1e786bdea3 Update dependency Markdig.Signed to v0.33.0 2023-08-30 06:14:25 +00:00
ArchiBot
27297f6f26 Automatic translations update 2023-08-30 02:04:44 +00:00
renovate[bot]
cfebe7bd50 Update dependency Microsoft.NET.Test.Sdk to v17.7.2 2023-08-29 09:45:50 +00:00
ArchiBot
8a2ee4e22b Automatic translations update 2023-08-29 02:34:32 +00:00
renovate[bot]
c66260954c Update github/codeql-action action to v2.21.5 2023-08-28 19:26:41 +00:00
renovate[bot]
adaa1416bb Update docker/setup-buildx-action action to v2.10.0 2023-08-28 08:04:20 +00:00
renovate[bot]
f3bc0162ef Update ASF-ui digest to 5d9525f 2023-08-27 03:26:06 +00:00
renovate[bot]
7a87131651 Update ASF-ui digest to 07b1fe2 2023-08-26 00:57:47 +00:00
renovate[bot]
c945aca1b7 Update ncipollo/release-action action to v1.13.0 2023-08-24 20:17:48 +00:00
renovate[bot]
874b66d3af Update actions/checkout action to v3.6.0 2023-08-24 15:29:46 +00:00
renovate[bot]
142347ad20 Update ASF-ui digest to f31aaee 2023-08-23 17:29:17 +00:00
ArchiBot
32dca1ce33 Automatic translations update 2023-08-21 02:03:31 +00:00
renovate[bot]
36e15eccf1 Update ASF-ui digest to 7b543ae 2023-08-19 07:31:10 +00:00
renovate[bot]
b00b297def Update ASF-ui digest to 6efaee8 2023-08-17 23:01:22 +00:00
renovate[bot]
0fbf31db89 Update actions/setup-node action to v3.8.1 2023-08-17 14:30:36 +00:00
renovate[bot]
5d7a1e33b6 Update dependency Microsoft.NET.Test.Sdk to v17.7.1 2023-08-16 15:35:29 +00:00
ArchiBot
3bcf09d42e Automatic translations update 2023-08-16 02:03:55 +00:00
ArchiBot
7d19111b42 Automatic translations update 2023-08-15 02:03:44 +00:00
renovate[bot]
3667b62bee Update ASF-ui digest to 135bbb3 2023-08-14 20:30:17 +00:00
renovate[bot]
d9ff9d7a97 Update github/codeql-action action to v2.21.4 2023-08-14 17:04:43 +00:00
renovate[bot]
7ae60bdb3c Update actions/setup-node action to v3.8.0 2023-08-14 13:32:22 +00:00
ArchiBot
ecbbca0215 Automatic translations update 2023-08-14 02:04:33 +00:00
renovate[bot]
534f84b181 Update wiki digest to e1c5a2a 2023-08-13 21:18:17 +00:00
ArchiBot
894587b965 Automatic translations update 2023-08-13 02:03:57 +00:00
renovate[bot]
8f335f4182 Update ASF-ui digest to 0964964 2023-08-11 19:19:30 +00:00
ArchiBot
750c56bf08 Automatic translations update 2023-08-11 02:03:06 +00:00
Archi
c84366f9ba Misc syntax improvements 2023-08-10 21:36:17 +02:00
renovate[bot]
837e4bde8f Update ASF-ui digest to bb92516 2023-08-10 05:09:49 +00:00
renovate[bot]
525a6f2d19 Update ASF-ui digest to dd93453 2023-08-09 21:07:13 +00:00
Archi
8719a2b98b Bump 2023-08-09 23:06:53 +02:00
Vita Chumakova
a768ec43a5 Fix addlicense (#2983) 2023-08-09 23:06:11 +02:00
renovate[bot]
385ea566aa Update ASF-ui digest to 0b812a7 2023-08-09 12:06:50 +00:00
renovate[bot]
df878185c2 Update github/codeql-action action to v2.21.3 2023-08-08 14:29:55 +00:00
ArchiBot
f834ffe612 Automatic translations update 2023-08-08 02:09:46 +00:00
renovate[bot]
4e032fa129 Update ASF-ui digest to 05ff07a 2023-08-07 23:23:13 +00:00
Archi
6b116b5163 Misc 2023-08-07 18:12:50 +02:00
Archi
7ee92cb5b9 Bump 2023-08-07 17:23:53 +02:00
48 changed files with 1053 additions and 352 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Submodule ASF-ui updated: e01b29e635...1447dbe0e5

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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() { }
}

View File

@@ -43,6 +43,9 @@ public sealed class Confirmation {
[JsonConstructor]
private Confirmation() { }
[UsedImplicitly]
public static bool ShouldSerializeNonce() => false;
[PublicAPI]
public enum EConfirmationType : byte {
Unknown,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>5.4.9.2</Version>
<Version>5.4.12.2</Version>
</PropertyGroup>
<PropertyGroup>

View File

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

Submodule wiki updated: 3edaba1ddc...7cf972f6e8