Compare commits

..

162 Commits

Author SHA1 Message Date
Archi
10007e0752 Closes #2824 2023-02-26 13:43:25 +01:00
Łukasz Domeradzki
c89366b0a6 Closes #2827 (#2829) 2023-02-26 10:59:48 +01:00
ArchiBot
402867a025 Automatic translations update 2023-02-26 02:24:02 +00:00
ArchiBot
8223d09524 Automatic translations update 2023-02-25 02:21:51 +00:00
renovate[bot]
2a74778c32 Update github/codeql-action action to v2.2.5 2023-02-24 20:29:28 +00:00
ArchiBot
cb8dad153c Automatic translations update 2023-02-24 02:21:04 +00:00
Archi
f9efaed524 Misc 2023-02-23 17:20:08 +01:00
renovate[bot]
d38b4568eb Update ASF-ui digest to b30b3f5 2023-02-23 04:42:13 +00:00
ArchiBot
afd1c89ca8 Automatic translations update 2023-02-23 02:21:14 +00:00
ArchiBot
259700f899 Automatic translations update 2023-02-22 02:21:44 +00:00
renovate[bot]
fd86ee9823 Update dependency Microsoft.NET.Test.Sdk to v17.5.0 2023-02-21 10:35:08 +00:00
renovate[bot]
207c52e97f Update ASF-ui digest to 8a85633 2023-02-21 02:45:03 +00:00
ArchiBot
1b025481d5 Automatic translations update 2023-02-21 02:22:15 +00:00
ArchiBot
a871d283b0 Automatic translations update 2023-02-20 02:22:26 +00:00
ArchiBot
94f9d580f3 Automatic translations update 2023-02-19 02:22:10 +00:00
renovate[bot]
26193f62a9 Update dependency NLog.Web.AspNetCore to v5.2.2 2023-02-18 08:24:19 +00:00
renovate[bot]
372175b0c0 Update ASF-ui digest to 900dab6 2023-02-18 06:00:56 +00:00
ArchiBot
8322e39f37 Automatic translations update 2023-02-18 02:20:32 +00:00
renovate[bot]
0d4ba1b81d Update ASF-ui digest to 2c9d017 2023-02-17 08:23:08 +00:00
ArchiBot
a63a321072 Automatic translations update 2023-02-17 02:22:54 +00:00
renovate[bot]
a8ce7f2fdd Update ASF-ui digest to 77bbc49 2023-02-16 05:40:51 +00:00
ArchiBot
dfa967535b Automatic translations update 2023-02-16 02:21:45 +00:00
renovate[bot]
f92b77ac2e Update crowdin/github-action action to v1.7.0 2023-02-15 19:37:16 +00:00
renovate[bot]
ddf73e2089 Update JetBrains/qodana-action action to v2022.3.4 2023-02-15 16:10:38 +00:00
ArchiBot
722c79f1ac Automatic translations update 2023-02-15 02:22:15 +00:00
renovate[bot]
67194f6281 Update dependency System.Security.Cryptography.ProtectedData to v7.0.1 2023-02-14 15:24:48 +00:00
renovate[bot]
28289cb673 Update ASF-ui digest to 303ff51 2023-02-14 12:07:46 +00:00
ArchiBot
29d33c7796 Automatic translations update 2023-02-14 02:22:06 +00:00
renovate[bot]
aebb6a26a7 Update ASF-ui digest to 9b128b6 2023-02-13 20:22:52 +00:00
ArchiBot
f357092808 Automatic translations update 2023-02-13 02:22:09 +00:00
renovate[bot]
8cef49f258 Update wiki digest to 56e86e3 2023-02-12 21:01:03 +00:00
Archi
71b39570b3 Misc 2023-02-12 18:49:53 +01:00
Archi
64c5cec113 Misc 2023-02-12 18:48:03 +01:00
ArchiBot
5bdfab5fb2 Automatic translations update 2023-02-12 02:21:50 +00:00
Archi
e1fc57c19c Bump 2023-02-11 16:06:53 +01:00
Archi
f5812fc28a Good job archo 2023-02-11 16:06:23 +01:00
Archi
d7e8710333 Do not announce/match with limited accounts, lockdowns and trade bans, improve ArchiCacheable
We can totally make use of success previously more often
2023-02-11 15:58:15 +01:00
renovate[bot]
a0f521d4f4 Update ASF-ui digest to 4fb4d2d 2023-02-11 06:22:34 +00:00
ArchiBot
f82e79a41b Automatic translations update 2023-02-11 02:20:17 +00:00
renovate[bot]
5b3469cb1b Update github/codeql-action action to v2.2.4 2023-02-10 19:48:51 +00:00
renovate[bot]
dc2b7bc425 Update github/codeql-action action to v2.2.3 2023-02-10 06:47:59 +00:00
ArchiBot
0da63aba2b Automatic translations update 2023-02-10 02:48:29 +00:00
Archi
eee6457a7d Rename 2fasms to 2fafinalize 2023-02-10 00:39:28 +01:00
Łukasz Domeradzki
a12c11d334 Add ArchiSteamFarm.OfficialPlugins.MobileAuthenticator (#2822)
* Initial commit for 2FA plugin

* Shut up netf

* Actually import this authenticator right into ASF, and add safeguards

* Further fixes

* Render device_id in the resulting maFile
2023-02-10 00:37:26 +01:00
Archi
19349cc3c2 Bump 2023-02-09 20:06:46 +01:00
Archi
4b0a0157aa Bump 2023-02-09 17:22:51 +01:00
Archi
e211588915 CD: Publish improvements
Cuts on publishing time and actual work generated
2023-02-09 17:22:31 +01:00
Łukasz Domeradzki
a89cb28255 Add support for win-arm64 (#2821) 2023-02-09 14:08:39 +01:00
renovate[bot]
0b2f694fb0 Update ASF-ui digest to 36d0d00 2023-02-09 03:27:40 +00:00
ArchiBot
7e11e62f92 Automatic translations update 2023-02-09 02:42:36 +00:00
Archi
3236477702 Update qodana link 2023-02-09 03:32:40 +01:00
Archi
8008a04354 Code cleanups and improvements
- Make use of new UnixFileMode, always one native method we need to maintain less
- Add madness support for it, because new feature of course
- Add optional netstandard target and required compatibility for it, so I can test netf-oriented changes easier
2023-02-09 02:25:11 +01:00
Archi
b2c34694ae Misc code improvements 2023-02-08 21:18:20 +01:00
Archi
738a2c3508 Disable failTreshold until false positives are gone
e.g. https://youtrack.jetbrains.com/issue/QD-5167
2023-02-08 21:10:38 +01:00
Archi
06e10fa32a Add initial support for Qodana 2023-02-08 20:57:36 +01:00
Łukasz Domeradzki
d3490b4e92 Add --minimized command-line argument (#2817)
* Add experimental support for --minimized on Windows

* Simplify the code
2023-02-08 16:11:50 +01:00
renovate[bot]
573080e6c0 Update wiki digest to 02f4ac6 2023-02-08 07:03:48 +00:00
renovate[bot]
239cac2f62 Update ASF-ui digest to 07db676 2023-02-07 23:22:53 +00:00
ArchiBot
6db8a7b2b1 Automatic translations update 2023-02-07 02:34:06 +00:00
renovate[bot]
ea6fdafdbf Update docker/setup-buildx-action action to v2.4.1 2023-02-06 14:46:57 +00:00
ArchiBot
e864bd1d78 Automatic translations update 2023-02-06 02:18:32 +00:00
ArchiBot
b41a4ca572 Automatic translations update 2023-02-05 02:22:06 +00:00
Archi
dcdc3d4870 Misc 2023-02-04 20:23:32 +01:00
Archi
dc97558ad5 Misc 2023-02-04 16:02:28 +01:00
renovate[bot]
1cdb597acc Update ASF-ui digest to 56fb974 2023-02-04 05:18:50 +00:00
ArchiBot
1e328f8de8 Automatic translations update 2023-02-04 02:17:59 +00:00
renovate[bot]
bfc50f5861 Update wiki digest to f56cd3d 2023-02-03 13:59:53 +00:00
ArchiBot
04e338ace8 Automatic translations update 2023-02-03 02:22:04 +00:00
Archi
9f272ae490 Misc 2023-02-03 03:00:42 +01:00
Archi
e266b8ad31 Actually, this makes more sense 2023-02-03 02:32:17 +01:00
Archi
a8b28efe43 ASFB HttpClientHandler improvements
We should increase our security by checking against revoked SSL certificates, just in case.

When using proxy, pre-authenticating makes sense since if user provided the credentials, we can be pretty sure they're required and save a roundtrip.
2023-02-03 02:29:55 +01:00
renovate[bot]
6e8672cf84 Update ASF-ui digest to 54d5dc8 2023-02-02 06:11:01 +00:00
ArchiBot
8d7f68381b Automatic translations update 2023-02-02 02:21:18 +00:00
renovate[bot]
d7d67c11f9 Update wiki digest to 829f7e8 2023-02-01 15:58:17 +00:00
renovate[bot]
1f711f03d1 Update ASF-ui digest to 30f0bf0 2023-02-01 07:56:17 +00:00
ArchiBot
67e989b2a5 Automatic translations update 2023-02-01 02:24:04 +00:00
Archi
fd3ebe2819 Bump 2023-01-31 22:21:00 +01:00
Archi
17d9c9bc22 Prefer maximum compression on compressed requests
We have many clients and a single server, pushing additional work at the clients will typically degrade the effective speed with which the data can be transmitted, but will allow our server to handle more requests concurrently due to less bandwidth used, and will positively affect the bandwidth used for both server and the client.
2023-01-31 22:19:41 +01:00
renovate[bot]
d4efe537d9 Update docker/build-push-action action to v4 (#2813)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-31 15:07:31 +01:00
ArchiBot
95005565db Automatic translations update 2023-01-31 02:21:58 +00:00
renovate[bot]
c538eab00e Update docker/build-push-action action to v3.3.1 2023-01-30 21:56:28 +00:00
renovate[bot]
63ccf65ba0 Update docker/setup-buildx-action action to v2.4.0 2023-01-30 13:02:37 +00:00
ArchiBot
dcbd69f7e6 Automatic translations update 2023-01-30 02:18:12 +00:00
Archi
2b0c29d005 Oh right, this too 2023-01-29 23:02:48 +01:00
Archi
4b5a75eebc Satisfy netf, I'm too lazy to add madness support for it now 2023-01-29 23:00:35 +01:00
Archi
be8a441e2e Add ArchiSteamFarm.CustomPlugins.SignInWithSteam 2023-01-29 22:51:25 +01:00
renovate[bot]
aff62da00e Update ASF-ui digest to 22692a1 2023-01-29 05:44:06 +00:00
ArchiBot
f40600bccf Automatic translations update 2023-01-29 02:21:16 +00:00
renovate[bot]
6fc5de4105 Update docker/setup-buildx-action action to v2.3.0 2023-01-28 04:57:51 +00:00
ArchiBot
c7f35670bf Automatic translations update 2023-01-28 02:20:33 +00:00
renovate[bot]
36c9ace28e Update wiki digest to ec5b9ab 2023-01-27 21:30:43 +00:00
renovate[bot]
bab539ece3 Update ASF-ui digest to a3e6e1f 2023-01-27 05:04:18 +00:00
ArchiBot
1542efcda4 Automatic translations update 2023-01-27 02:20:39 +00:00
renovate[bot]
8129f1d707 Update ASF-ui digest to 5784179 2023-01-26 17:46:38 +00:00
renovate[bot]
142e574c5a Update crowdin/github-action action to v1.6.0 2023-01-26 09:47:49 +00:00
ArchiBot
b2871523c1 Automatic translations update 2023-01-26 02:19:40 +00:00
Sebastian Göls
0c125db118 Happy new year! (#2809) 2023-01-25 15:43:12 +01:00
renovate[bot]
dbcfef99be Update ASF-ui digest to 11f5159 2023-01-25 14:29:52 +00:00
ArchiBot
c49008400f Automatic translations update 2023-01-25 02:19:22 +00:00
Archi
ebd8cbf270 Remove no longer needed debugging leftover
Since interactive console is much more convenient to debug with, this no longer serves any purpose
2023-01-24 23:25:39 +01:00
Archi
b323a17fba Bump 2023-01-24 23:21:25 +01:00
Archi
4798b29bff Misc 2023-01-24 23:00:27 +01:00
Łukasz Domeradzki
df4f8d1e62 Improve fairness logic (#2807)
* Improve fairness logic

* Add unit test against abuse

* Further simplify the code

That first pass is not needed anymore, first loop covers it
2023-01-24 22:55:15 +01:00
Archi
00f7d2bfb9 Closes #2787 2023-01-24 22:49:41 +01:00
renovate[bot]
5ab1971018 Update ASF-ui digest to 94ce7b1 2023-01-24 05:32:33 +00:00
ArchiBot
841b80c297 Automatic translations update 2023-01-24 02:18:42 +00:00
Archi
10f2471332 CD: Upload sha512 sums first
So I can !asfapprove faster, lol
2023-01-24 02:27:37 +01:00
Archi
fccb455676 Bump 2023-01-24 01:40:27 +01:00
Archi
c9bc71462e Misc 2023-01-24 01:39:46 +01:00
Archi
e0f9fe3555 Skip empty nickname in self persona state callback 2023-01-24 01:29:55 +01:00
Archi
8bb48d829e STD: Add support for KnownDepotIDs 2023-01-23 12:08:30 +01:00
Archi
4c166102c5 Misc 2023-01-23 11:30:01 +01:00
Archi
a91a4aada6 Fetch main depot key only if we managed to fetch some other ones 2023-01-23 11:25:20 +01:00
renovate[bot]
e05139ea7f Update ASF-ui digest to a3cc476 2023-01-23 04:25:35 +00:00
ArchiBot
6119d33ab8 Automatic translations update 2023-01-23 02:18:51 +00:00
renovate[bot]
5905412d82 Update ASF-ui digest to b2dd529 2023-01-22 04:07:36 +00:00
ArchiBot
7596a89baa Automatic translations update 2023-01-22 02:20:35 +00:00
Archi
7e7fd3bab4 Bump 2023-01-21 23:28:23 +01:00
Archi
8ab6137ab1 Ensure we don't skip announcement if our trade token has changed
We don't care about nickname or avatar hash, even total amount of items is not that important, but trade token is crucial for matching
2023-01-21 23:23:08 +01:00
Archi
a01ea00873 STD: Add 500ms delay between depot key requests 2023-01-21 21:16:01 +01:00
Archi
4cb8244353 Move to announce endpoint v3
By using ordered list for json body, we can further minimize amount of data sent by getting rid of the index.

We still need previous asset ID, as we send only a subset of real data and server is unable to calculate it from the data sent.
2023-01-21 20:32:42 +01:00
renovate[bot]
41fef74e36 Update ASF-ui digest to 80e6bd5 2023-01-21 03:42:51 +00:00
ArchiBot
58376c0a10 Automatic translations update 2023-01-21 02:18:43 +00:00
Archi
628386859b Misc 2023-01-20 14:22:57 +01:00
Archi
1b0fe9cf45 Add desktop entry for generic and generic-netf too 2023-01-20 14:21:03 +01:00
Archi
946e88fb51 Remove unnecessary quotation 2023-01-20 13:56:33 +01:00
Archi
eed29fc330 Add desktop entry for linux 2023-01-20 13:48:31 +01:00
ArchiBot
da4ba2cda7 Automatic translations update 2023-01-20 02:22:00 +00:00
Archi
b9c75e73f0 Do a recovery of single depot tasks 2023-01-19 15:17:47 +01:00
Archi
cca21900a6 Allow STD requests to fail, and continue with other ones 2023-01-19 15:06:06 +01:00
renovate[bot]
a357953d0a Update ASF-ui digest to f813474 2023-01-19 04:37:00 +00:00
ArchiBot
bb58ec75f3 Automatic translations update 2023-01-19 02:36:34 +00:00
Archi
8aad8b6bcf Misc 2023-01-18 23:11:17 +01:00
Archi
1597786668 Bump 2023-01-18 23:01:39 +01:00
Archi
239d523513 Skip announcements during matching 2023-01-18 22:52:25 +01:00
Archi
3165bf62b4 Bump 2023-01-18 14:19:00 +01:00
Archi
bcfeb66ba4 Allow maximum of 10 pending to confirm trade offers at once 2023-01-18 14:16:35 +01:00
renovate[bot]
2e9c1042e1 Update ASF-ui digest to ecf6e1b 2023-01-18 07:22:03 +00:00
ArchiBot
1a26844cd8 Automatic translations update 2023-01-18 02:20:59 +00:00
Archi
7fb32effc1 Bump 2023-01-17 19:44:15 +01:00
Archi
28a3e27a5e Account for failures in a row when sending trade offers
We expect those to be occassional, but getting 5 in a row from 5 different users, that's extremely suspicious
2023-01-17 19:42:29 +01:00
Archi
27639b32d5 Accept all confirmations from ItemsMatcher at once
Previously we accepted those after each trade, because the overhead of loading other inventory was too big to leave those pending. Since we have all possible matches at once now, it makes sense to firstly schedule all trade offers, and then just confirm them all at once, especially since confirmations endpoint is horrific and very often problematic, on top of having 10-seconds rate-limiting.
2023-01-17 19:19:27 +01:00
Archi
5049f82dad Don't stop matching on occassional two factor failure 2023-01-17 19:00:31 +01:00
renovate[bot]
d0a0877a92 Update crowdin/github-action action to v1.5.3 2023-01-17 10:50:00 +00:00
renovate[bot]
70880a9f44 Update ASF-ui digest to 91a1d47 2023-01-17 02:41:35 +00:00
ArchiBot
ece3fb9c55 Automatic translations update 2023-01-17 02:19:39 +00:00
renovate[bot]
27b2061082 Update wiki digest to 2ab9176 2023-01-16 18:24:00 +00:00
ArchiBot
1d5ed1c080 Automatic translations update 2023-01-16 02:20:27 +00:00
Archi
f3593de457 Misc 2023-01-15 22:31:51 +01:00
Archi
8d34e4b798 Bump 2023-01-15 21:51:06 +01:00
Archi
d68b900055 Move http client compression to utilities
We don't really need standalone class, contrary to my first expectation
2023-01-15 21:46:58 +01:00
Archi
164b009a32 Enable response compression also for https in kestrel 2023-01-15 21:37:41 +01:00
Łukasz Domeradzki
ca9cccf5da Add support for request compression (#2805)
* Add support for brotli request compression

* Refactor and add support for netf

* Use fastest compression
2023-01-15 21:26:03 +01:00
renovate[bot]
508130c0bc Update ASF-ui digest to 09afe10 2023-01-15 11:44:34 +00:00
renovate[bot]
c2781fe9d2 Update wiki digest to 3507b52 2023-01-15 05:21:40 +00:00
renovate[bot]
bc8a08b9cc Update ASF-ui digest to 854f95f 2023-01-15 03:09:08 +00:00
ArchiBot
96277fb7a3 Automatic translations update 2023-01-15 02:20:45 +00:00
Archi
fc93f86060 Improve preferences of matching
We should try to match smallest bots first, but since assets are deduplicated exclusively for us, we should use total inventory count instead
2023-01-15 01:04:15 +01:00
Archi
7466db57cf Bump 2023-01-15 00:25:15 +01:00
Archi
e5ff2e9f02 Include TotalInventoryCount for the backend 2023-01-15 00:16:53 +01:00
Archi
88cec38df4 Decrease overhead for calculating tradable sets for announcement
We don't care about classIDs there, only amounts
2023-01-14 23:57:45 +01:00
Archi
d506cf8ed2 Bump 2023-01-14 23:48:57 +01:00
275 changed files with 3134 additions and 1495 deletions

View File

@@ -39,7 +39,7 @@ jobs:
- name: Upload latest strings for translation on Crowdin
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && matrix.configuration == 'Release' && startsWith(matrix.os, 'ubuntu-') }}
uses: crowdin/github-action@1.5.2
uses: crowdin/github-action@v1.7.0
with:
crowdin_branch_name: main
config: '.github/crowdin.yml'

25
.github/workflows/code-quality.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: ASF-code-quality
on: [push, pull_request]
env:
DOTNET_CLI_TELEMETRY_OPTOUT: true
DOTNET_NOLOGO: true
jobs:
main:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3.3.0
- name: Run Qodana scan
uses: JetBrains/qodana-action@v2022.3.4
env:
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
- name: Report Qodana results to GitHub
uses: github/codeql-action/upload-sarif@v2.2.5
with:
sarif_file: ${{ runner.temp }}/qodana/results/qodana.sarif.json

View File

@@ -22,10 +22,10 @@ jobs:
submodules: recursive
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.2.1
uses: docker/setup-buildx-action@v2.4.1
- name: Build ${{ matrix.configuration }} Docker image from ${{ matrix.file }}
uses: docker/build-push-action@v3.3.0
uses: docker/build-push-action@v4.0.0
with:
context: .
file: ${{ matrix.file }}

View File

@@ -20,7 +20,7 @@ jobs:
submodules: recursive
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.2.1
uses: docker/setup-buildx-action@v2.4.1
- name: Login to ghcr.io
uses: docker/login-action@v2.1.0
@@ -55,7 +55,7 @@ jobs:
echo "DH_REPOSITORY=$(echo ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV"
- name: Build and publish Docker image from Dockerfile.Service
uses: docker/build-push-action@v3.3.0
uses: docker/build-push-action@v4.0.0
with:
context: .
file: Dockerfile.Service

View File

@@ -21,7 +21,7 @@ jobs:
submodules: recursive
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.2.1
uses: docker/setup-buildx-action@v2.4.1
- name: Login to ghcr.io
uses: docker/login-action@v2.1.0
@@ -55,7 +55,7 @@ jobs:
echo "DH_REPOSITORY=$(echo ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV"
- name: Build and publish Docker image from Dockerfile
uses: docker/build-push-action@v3.3.0
uses: docker/build-push-action@v4.0.0
with:
context: .
platforms: ${{ env.PLATFORMS }}

View File

@@ -21,7 +21,7 @@ jobs:
submodules: recursive
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.2.1
uses: docker/setup-buildx-action@v2.4.1
- name: Login to ghcr.io
uses: docker/login-action@v2.1.0
@@ -56,7 +56,7 @@ jobs:
echo "DH_REPOSITORY=$(echo ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV"
- name: Build and publish Docker image from Dockerfile
uses: docker/build-push-action@v3.3.0
uses: docker/build-push-action@v4.0.0
with:
context: .
platforms: ${{ env.PLATFORMS }}

View File

@@ -3,25 +3,16 @@ name: ASF-publish
on: [push, pull_request]
env:
ASF_PRIVATE_SNK: ${{ secrets.ASF_PRIVATE_SNK }}
CONFIGURATION: Release
DOTNET_CLI_TELEMETRY_OPTOUT: true
DOTNET_NOLOGO: true
DOTNET_SDK_VERSION: 7.0.x
NET_CORE_VERSION: net7.0
NET_FRAMEWORK_VERSION: net481
NODE_JS_VERSION: 'lts/*'
PLUGINS: ArchiSteamFarm.OfficialPlugins.ItemsMatcher ArchiSteamFarm.OfficialPlugins.SteamTokenDumper
STEAM_TOKEN_DUMPER_TOKEN: ${{ secrets.STEAM_TOKEN_DUMPER_TOKEN }}
PLUGINS: ArchiSteamFarm.OfficialPlugins.ItemsMatcher ArchiSteamFarm.OfficialPlugins.MobileAuthenticator ArchiSteamFarm.OfficialPlugins.SteamTokenDumper
jobs:
publish:
strategy:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
publish-asf-ui:
runs-on: ubuntu-latest
steps:
- name: Checkout code
@@ -29,14 +20,6 @@ jobs:
with:
submodules: recursive
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.0.3
with:
dotnet-version: ${{ env.DOTNET_SDK_VERSION }}
- name: Verify .NET Core
run: dotnet --info
- name: Setup Node.js with npm
uses: actions/setup-node@v3.6.0
with:
@@ -55,8 +38,66 @@ jobs:
- name: Publish ASF-ui
run: npm run-script deploy --no-progress --prefix ASF-ui
- name: Upload ASF-ui
uses: actions/upload-artifact@v3.1.2
with:
name: ASF-ui
path: ASF-ui/dist
publish-asf:
needs: publish-asf-ui
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
variant: generic
- os: windows-latest
variant: generic-netf
- os: ubuntu-latest
variant: linux-arm
- os: ubuntu-latest
variant: linux-arm64
- os: ubuntu-latest
variant: linux-x64
- os: macos-latest
variant: osx-arm64
- os: macos-latest
variant: osx-x64
- os: windows-latest
variant: win-arm64
- os: windows-latest
variant: win-x64
runs-on: ${{ matrix.os }}
env:
DOTNET_CLI_TELEMETRY_OPTOUT: true
DOTNET_NOLOGO: true
steps:
- name: Checkout code
uses: actions/checkout@v3.3.0
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.0.3
with:
dotnet-version: ${{ env.DOTNET_SDK_VERSION }}
- name: Verify .NET Core
run: dotnet --info
- name: Download previously built ASF-ui
uses: actions/download-artifact@v3.0.2
with:
name: ASF-ui
path: ASF-ui/dist
- name: Prepare private key for signing on Unix
if: startsWith(matrix.os, 'macos-') || startsWith(matrix.os, 'ubuntu-')
env:
ASF_PRIVATE_SNK: ${{ secrets.ASF_PRIVATE_SNK }}
shell: sh
run: |
set -eu
@@ -67,6 +108,8 @@ jobs:
- name: Prepare private key for signing on Windows
if: startsWith(matrix.os, 'windows-')
env:
ASF_PRIVATE_SNK: ${{ secrets.ASF_PRIVATE_SNK }}
shell: pwsh
run: |
Set-StrictMode -Version Latest
@@ -85,15 +128,19 @@ jobs:
- name: Prepare for publishing on Unix
if: startsWith(matrix.os, 'macos-') || startsWith(matrix.os, 'ubuntu-')
env:
TARGET_FRAMEWORK: ${{ (endsWith(matrix.variant, '-netf') && env.NET_FRAMEWORK_VERSION) || env.NET_CORE_VERSION }}
shell: bash
run: |
set -euo pipefail
dotnet restore
dotnet build ArchiSteamFarm -c "$CONFIGURATION" -p:ContinuousIntegrationBuild=true -p:TargetLatestRuntimePatch=false -p:UseAppHost=false --no-restore --nologo
dotnet build ArchiSteamFarm -c "$CONFIGURATION" -f "$TARGET_FRAMEWORK" -p:ContinuousIntegrationBuild=true -p:TargetLatestRuntimePatch=false -p:UseAppHost=false --no-restore --nologo
- name: Prepare for publishing on Windows
if: startsWith(matrix.os, 'windows-')
env:
TARGET_FRAMEWORK: ${{ (endsWith(matrix.variant, '-netf') && env.NET_FRAMEWORK_VERSION) || env.NET_CORE_VERSION }}
shell: pwsh
run: |
Set-StrictMode -Version Latest
@@ -101,10 +148,12 @@ jobs:
$ProgressPreference = 'SilentlyContinue'
dotnet restore
dotnet build ArchiSteamFarm -c "$env:CONFIGURATION" -p:ContinuousIntegrationBuild=true -p:TargetLatestRuntimePatch=false -p:UseAppHost=false --no-restore --nologo
dotnet build ArchiSteamFarm -c "$env:CONFIGURATION" -f "$env:TARGET_FRAMEWORK" -p:ContinuousIntegrationBuild=true -p:TargetLatestRuntimePatch=false -p:UseAppHost=false --no-restore --nologo
- name: Prepare ArchiSteamFarm.OfficialPlugins.SteamTokenDumper on Unix
if: startsWith(matrix.os, 'macos-') || startsWith(matrix.os, 'ubuntu-')
env:
STEAM_TOKEN_DUMPER_TOKEN: ${{ secrets.STEAM_TOKEN_DUMPER_TOKEN }}
shell: sh
run: |
set -eu
@@ -116,6 +165,8 @@ jobs:
- name: Prepare ArchiSteamFarm.OfficialPlugins.SteamTokenDumper on Windows
if: startsWith(matrix.os, 'windows-')
env:
STEAM_TOKEN_DUMPER_TOKEN: ${{ secrets.STEAM_TOKEN_DUMPER_TOKEN }}
shell: pwsh
run: |
Set-StrictMode -Version Latest
@@ -129,21 +180,22 @@ jobs:
- name: Publish official plugins on Unix
if: startsWith(matrix.os, 'macos-') || startsWith(matrix.os, 'ubuntu-')
env:
MAX_JOBS: 3
MAX_JOBS: 4
TARGET_FRAMEWORK: ${{ (endsWith(matrix.variant, '-netf') && env.NET_FRAMEWORK_VERSION) || env.NET_CORE_VERSION }}
shell: bash
run: |
set -euo pipefail
publish() {
dotnet publish "$1" -c "$CONFIGURATION" -f "$NET_CORE_VERSION" -o "out/${1}/${NET_CORE_VERSION}" -p:ContinuousIntegrationBuild=true -p:TargetLatestRuntimePatch=false -p:UseAppHost=false --no-restore --nologo
dotnet publish "$1" -c "$CONFIGURATION" -f "$TARGET_FRAMEWORK" -o "out/${1}/${TARGET_FRAMEWORK}" -p:ContinuousIntegrationBuild=true -p:TargetLatestRuntimePatch=false -p:UseAppHost=false --no-restore --nologo
}
for plugin in $PLUGINS; do
publish "$plugin" &
while [ "$(jobs -p | wc -l)" -ge "$MAX_JOBS" ]; do
sleep 1
done
publish "$plugin" &
done
wait
@@ -151,7 +203,8 @@ jobs:
- name: Publish official plugins on Windows
if: startsWith(matrix.os, 'windows-')
env:
MAX_JOBS: 2
MAX_JOBS: 4
TARGET_FRAMEWORK: ${{ (endsWith(matrix.variant, '-netf') && env.NET_FRAMEWORK_VERSION) || env.NET_CORE_VERSION }}
shell: pwsh
run: |
Set-StrictMode -Version Latest
@@ -159,7 +212,7 @@ jobs:
$ProgressPreference = 'SilentlyContinue'
$PublishBlock = {
param($plugin, $framework)
param($plugin)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
@@ -167,7 +220,7 @@ jobs:
Set-Location "$env:GITHUB_WORKSPACE"
dotnet publish "$plugin" -c "$env:CONFIGURATION" -f "$framework" -o "out\$plugin\$framework" -p:ContinuousIntegrationBuild=true -p:TargetLatestRuntimePatch=false -p:UseAppHost=false --no-restore --nologo
dotnet publish "$plugin" -c "$env:CONFIGURATION" -f "$env:TARGET_FRAMEWORK" -o "out\$plugin\$env:TARGET_FRAMEWORK" -p:ContinuousIntegrationBuild=true -p:TargetLatestRuntimePatch=false -p:UseAppHost=false --no-restore --nologo
if ($LastExitCode -ne 0) {
throw "Last command failed."
@@ -175,215 +228,6 @@ jobs:
}
foreach ($plugin in $env:PLUGINS.Split([char[]] $null, [System.StringSplitOptions]::RemoveEmptyEntries)) {
foreach ($framework in "$env:NET_CORE_VERSION","$env:NET_FRAMEWORK_VERSION") {
Start-Job -Name "$plugin $framework" $PublishBlock -ArgumentList "$plugin","$framework"
# Limit active jobs in parallel to help with memory usage
$jobs = $(Get-Job -State Running)
while (@($jobs).Count -ge $env:MAX_JOBS) {
Wait-Job -Job $jobs -Any | Out-Null
$jobs = $(Get-Job -State Running)
}
}
}
Get-Job | Receive-Job -Wait
- name: Publish ArchiSteamFarm on Unix
if: startsWith(matrix.os, 'macos-') || startsWith(matrix.os, 'ubuntu-')
env:
MAX_JOBS: 3
VARIANTS: generic linux-arm linux-arm64 linux-x64 osx-arm64 osx-x64 win-x64 # NOTE: When modifying variants, don't forget to update ASF_VARIANT definitions in SharedInfo.cs!
shell: bash
run: |
set -euo pipefail
publish() {
if [ "$1" = 'generic' ]; then
variantArgs="-p:TargetLatestRuntimePatch=false -p:UseAppHost=false"
else
variantArgs="-p:PublishSingleFile=true -p:PublishTrimmed=true -r $1 --self-contained"
fi
dotnet publish ArchiSteamFarm -c "$CONFIGURATION" -f "$NET_CORE_VERSION" -o "out/${1}" "-p:ASFVariant=$1" -p:ContinuousIntegrationBuild=true --no-restore --nologo $variantArgs
# If we're including official plugins for this framework, copy them to output directory
for plugin in $PLUGINS; do
if [ -d "out/${plugin}/${NET_CORE_VERSION}" ]; then
mkdir -p "out/${1}/plugins/${plugin}"
cp -pR "out/${plugin}/${NET_CORE_VERSION}/"* "out/${1}/plugins/${plugin}"
fi
done
# Include .ico file for all platforms, since only Windows script can bundle it inside the exe
cp "resources/ASF.ico" "out/${1}/ArchiSteamFarm.ico"
# By default use fastest compression
seven_zip_args="-mx=1"
zip_args="-1"
# Include extra logic for builds marked for release
case "$GITHUB_REF" in
"refs/tags/"*)
# Tweak compression args for release publishing
seven_zip_args="-mx=9 -mfb=258 -mpass=15"
zip_args="-9"
# Update link in Changelog.html accordingly
if [ -f "out/${1}/Changelog.html" ]; then
tag="$(echo "$GITHUB_REF" | cut -c 11-)"
sed "s/ArchiSteamFarm\/commits\/main/ArchiSteamFarm\/releases\/tag\/${tag}/g" "out/${1}/Changelog.html" > "out/${1}/Changelog.html.new"
mv "out/${1}/Changelog.html.new" "out/${1}/Changelog.html"
fi
;;
esac
# Create the final zip file
case "$(uname -s)" in
"Darwin")
# We prefer to use zip on macOS as 7z implementation on that OS doesn't handle file permissions (chmod +x)
if command -v zip >/dev/null; then
(
cd "${GITHUB_WORKSPACE}/out/${1}"
zip -q -r $zip_args "../ASF-${1}.zip" .
)
elif command -v 7z >/dev/null; then
7z a -bd -slp -tzip -mm=Deflate $seven_zip_args "out/ASF-${1}.zip" "${GITHUB_WORKSPACE}/out/${1}/*"
else
echo "ERROR: No supported zip tool!"
return 1
fi
;;
*)
if command -v 7z >/dev/null; then
7z a -bd -slp -tzip -mm=Deflate $seven_zip_args "out/ASF-${1}.zip" "${GITHUB_WORKSPACE}/out/${1}/*"
elif command -v zip >/dev/null; then
(
cd "${GITHUB_WORKSPACE}/out/${1}"
zip -q -r $zip_args "../ASF-${1}.zip" .
)
else
echo "ERROR: No supported zip tool!"
return 1
fi
;;
esac
}
for variant in $VARIANTS; do
publish "$variant" &
while [ "$(jobs -p | wc -l)" -ge "$MAX_JOBS" ]; do
sleep 1
done
done
wait
- name: Publish ArchiSteamFarm on Windows
if: startsWith(matrix.os, 'windows-')
env:
MAX_JOBS: 2
VARIANTS: generic generic-netf linux-arm linux-arm64 linux-x64 osx-arm64 osx-x64 win-x64 # NOTE: When modifying variants, don't forget to update ASF_VARIANT definitions in SharedInfo.cs!
shell: pwsh
run: |
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
$PublishBlock = {
param($variant)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
Set-Location "$env:GITHUB_WORKSPACE"
if ($variant -like '*-netf') {
$targetFramework = $env:NET_FRAMEWORK_VERSION
} else {
$targetFramework = $env:NET_CORE_VERSION
}
if ($variant -like 'generic*') {
$variantArgs = '-p:TargetLatestRuntimePatch=false', '-p:UseAppHost=false'
} else {
$variantArgs = '-p:PublishSingleFile=true', '-p:PublishTrimmed=true', '-r', "$variant", '--self-contained'
}
dotnet publish ArchiSteamFarm -c "$env:CONFIGURATION" -f "$targetFramework" -o "out\$variant" "-p:ASFVariant=$variant" -p:ContinuousIntegrationBuild=true --no-restore --nologo $variantArgs
if ($LastExitCode -ne 0) {
throw "Last command failed."
}
# If we're including official plugins for this framework, copy them to output directory
foreach ($plugin in $env:PLUGINS.Split([char[]] $null, [System.StringSplitOptions]::RemoveEmptyEntries)) {
if (Test-Path "out\$plugin\$targetFramework" -PathType Container) {
if (!(Test-Path "out\$variant\plugins\$plugin" -PathType Container)) {
New-Item -ItemType Directory -Path "out\$variant\plugins\$plugin" > $null
}
Copy-Item "out\$plugin\$targetFramework\*" "out\$variant\plugins\$plugin" -Recurse
}
}
# Icon is available only in .NET Framework and .NET Core Windows build, we'll bundle the .ico file for other flavours
if (($targetFramework -eq "$env:NET_CORE_VERSION") -and !(Test-Path "out\$variant\ArchiSteamFarm.exe" -PathType Leaf)) {
Copy-Item 'resources\ASF.ico' "out\$variant\ArchiSteamFarm.ico"
}
# By default use fastest compression
$compressionArgs = '-mx=1'
# Include extra logic for builds marked for release
if ($env:GITHUB_REF -like 'refs/tags/*') {
# Tweak compression args for release publishing
$compressionArgs = '-mx=9', '-mfb=258', '-mpass=15'
# Update link in Changelog.html accordingly
if (Test-Path "out\$variant\Changelog.html" -PathType Leaf) {
$tag = $env:GITHUB_REF.Substring(10)
(Get-Content "out\$variant\Changelog.html").Replace('ArchiSteamFarm/commits/main', "ArchiSteamFarm/releases/tag/$tag") | Set-Content "out\$variant\Changelog.html"
}
}
# Create the final zip file
7z a -bd -slp -tzip -mm=Deflate $compressionArgs "out\ASF-$variant.zip" "$env:GITHUB_WORKSPACE\out\$variant\*"
if ($LastExitCode -ne 0) {
throw "Last command failed."
}
# We can aid non-windows users by adding chmod +x flag to appropriate executables directly in the zip file
# This is ALMOST a hack, but works reliably enough
if (Test-Path "tools\zip_exec\zip_exec.exe" -PathType Leaf) {
$executableFiles = @()
if ($variant -like 'generic*') {
$executableFiles += 'ArchiSteamFarm.sh', 'ArchiSteamFarm-Service.sh'
} elseif (($variant -like 'linux*') -or ($variant -like 'osx*')) {
$executableFiles += 'ArchiSteamFarm', 'ArchiSteamFarm-Service.sh'
}
foreach ($executableFile in $executableFiles) {
tools\zip_exec\zip_exec.exe "out\ASF-$variant.zip" "$executableFile"
if ($LastExitCode -ne 0) {
throw "Last command failed."
}
}
}
}
foreach ($variant in $env:VARIANTS.Split([char[]] $null, [System.StringSplitOptions]::RemoveEmptyEntries)) {
Start-Job -Name "$variant" $PublishBlock -ArgumentList "$variant"
# Limit active jobs in parallel to help with memory usage
$jobs = $(Get-Job -State Running)
@@ -392,62 +236,183 @@ jobs:
$jobs = $(Get-Job -State Running)
}
Start-Job -Name "$plugin" $PublishBlock -ArgumentList "$plugin"
}
Get-Job | Receive-Job -Wait
- name: Upload ASF-generic
uses: actions/upload-artifact@v3.1.2
with:
name: ${{ matrix.os }}_ASF-generic
path: out/ASF-generic.zip
- name: Publish ASF-${{ matrix.variant }} on Unix
if: startsWith(matrix.os, 'macos-') || startsWith(matrix.os, 'ubuntu-')
env:
TARGET_FRAMEWORK: ${{ (endsWith(matrix.variant, '-netf') && env.NET_FRAMEWORK_VERSION) || env.NET_CORE_VERSION }}
VARIANT: ${{ matrix.variant }}
shell: bash
run: |
set -euo pipefail
- name: Upload ASF-generic-netf
if [ "$VARIANT" = 'generic' ]; then
variantArgs="-p:TargetLatestRuntimePatch=false -p:UseAppHost=false"
else
variantArgs="-p:PublishSingleFile=true -p:PublishTrimmed=true -r $VARIANT --self-contained"
fi
dotnet publish ArchiSteamFarm -c "$CONFIGURATION" -f "$TARGET_FRAMEWORK" -o "out/${VARIANT}" "-p:ASFVariant=${VARIANT}" -p:ContinuousIntegrationBuild=true --no-restore --nologo $variantArgs
# If we're including official plugins for this framework, copy them to output directory
for plugin in $PLUGINS; do
if [ -d "out/${plugin}/${TARGET_FRAMEWORK}" ]; then
mkdir -p "out/${VARIANT}/plugins/${plugin}"
cp -pR "out/${plugin}/${TARGET_FRAMEWORK}/"* "out/${VARIANT}/plugins/${plugin}"
fi
done
# Include .ico file for all platforms, since only Windows script can bundle it inside the exe
cp "resources/ASF.ico" "out/${VARIANT}/ArchiSteamFarm.ico"
# By default use fastest compression
seven_zip_args="-mx=1"
zip_args="-1"
# Include extra logic for builds marked for release
case "$GITHUB_REF" in
"refs/tags/"*)
# Tweak compression args for release publishing
seven_zip_args="-mx=9 -mfb=258 -mpass=15"
zip_args="-9"
# Update link in Changelog.html accordingly
if [ -f "out/${VARIANT}/Changelog.html" ]; then
tag="$(echo "$GITHUB_REF" | cut -c 11-)"
sed "s/ArchiSteamFarm\/commits\/main/ArchiSteamFarm\/releases\/tag\/${tag}/g" "out/${VARIANT}/Changelog.html" > "out/${VARIANT}/Changelog.html.new"
mv "out/${VARIANT}/Changelog.html.new" "out/${VARIANT}/Changelog.html"
fi
;;
esac
# Create the final zip file
case "$(uname -s)" in
"Darwin")
# We prefer to use zip on macOS as 7z implementation on that OS doesn't handle file permissions (chmod +x)
if command -v zip >/dev/null; then
(
cd "${GITHUB_WORKSPACE}/out/${VARIANT}"
zip -q -r $zip_args "../ASF-${VARIANT}.zip" .
)
elif command -v 7z >/dev/null; then
7z a -bd -slp -tzip -mm=Deflate $seven_zip_args "out/ASF-${VARIANT}.zip" "${GITHUB_WORKSPACE}/out/${VARIANT}/*"
else
echo "ERROR: No supported zip tool!"
return 1
fi
;;
*)
if command -v 7z >/dev/null; then
7z a -bd -slp -tzip -mm=Deflate $seven_zip_args "out/ASF-${VARIANT}.zip" "${GITHUB_WORKSPACE}/out/${VARIANT}/*"
elif command -v zip >/dev/null; then
(
cd "${GITHUB_WORKSPACE}/out/${VARIANT}"
zip -q -r $zip_args "../ASF-${VARIANT}.zip" .
)
else
echo "ERROR: No supported zip tool!"
return 1
fi
;;
esac
- name: Publish ASF-${{ matrix.variant }} on Windows
if: startsWith(matrix.os, 'windows-')
uses: actions/upload-artifact@v3.1.2
with:
name: ${{ matrix.os }}_ASF-generic-netf
path: out/ASF-generic-netf.zip
env:
TARGET_FRAMEWORK: ${{ (endsWith(matrix.variant, '-netf') && env.NET_FRAMEWORK_VERSION) || env.NET_CORE_VERSION }}
VARIANT: ${{ matrix.variant }}
shell: pwsh
run: |
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
- name: Upload ASF-linux-arm
uses: actions/upload-artifact@v3.1.2
with:
name: ${{ matrix.os }}_ASF-linux-arm
path: out/ASF-linux-arm.zip
if ($env:VARIANT -like 'generic*') {
$variantArgs = '-p:TargetLatestRuntimePatch=false', '-p:UseAppHost=false'
} else {
$variantArgs = '-p:PublishSingleFile=true', '-p:PublishTrimmed=true', '-r', "$env:VARIANT", '--self-contained'
}
- name: Upload ASF-linux-arm64
uses: actions/upload-artifact@v3.1.2
with:
name: ${{ matrix.os }}_ASF-linux-arm64
path: out/ASF-linux-arm64.zip
dotnet publish ArchiSteamFarm -c "$env:CONFIGURATION" -f "$env:TARGET_FRAMEWORK" -o "out\$env:VARIANT" "-p:ASFVariant=$env:VARIANT" -p:ContinuousIntegrationBuild=true --no-restore --nologo $variantArgs
- name: Upload ASF-linux-x64
uses: actions/upload-artifact@v3.1.2
with:
name: ${{ matrix.os }}_ASF-linux-x64
path: out/ASF-linux-x64.zip
if ($LastExitCode -ne 0) {
throw "Last command failed."
}
- name: Upload ASF-osx-arm64
uses: actions/upload-artifact@v3.1.2
with:
name: ${{ matrix.os }}_ASF-osx-arm64
path: out/ASF-osx-arm64.zip
# If we're including official plugins for this framework, copy them to output directory
foreach ($plugin in $env:PLUGINS.Split([char[]] $null, [System.StringSplitOptions]::RemoveEmptyEntries)) {
if (Test-Path "out\$plugin\$env:TARGET_FRAMEWORK" -PathType Container) {
if (!(Test-Path "out\$env:VARIANT\plugins\$plugin" -PathType Container)) {
New-Item -ItemType Directory -Path "out\$env:VARIANT\plugins\$plugin" > $null
}
- name: Upload ASF-osx-x64
uses: actions/upload-artifact@v3.1.2
with:
name: ${{ matrix.os }}_ASF-osx-x64
path: out/ASF-osx-x64.zip
Copy-Item "out\$plugin\$env:TARGET_FRAMEWORK\*" "out\$env:VARIANT\plugins\$plugin" -Recurse
}
}
- name: Upload ASF-win-x64
# Icon is available only in .exe Windows builds, we'll bundle the .ico file for other flavours
if (!(Test-Path "out\$env:VARIANT\ArchiSteamFarm.exe" -PathType Leaf)) {
Copy-Item 'resources\ASF.ico' "out\$env:VARIANT\ArchiSteamFarm.ico"
}
# By default use fastest compression
$compressionArgs = '-mx=1'
# Include extra logic for builds marked for release
if ($env:GITHUB_REF -like 'refs/tags/*') {
# Tweak compression args for release publishing
$compressionArgs = '-mx=9', '-mfb=258', '-mpass=15'
# Update link in Changelog.html accordingly
if (Test-Path "out\$env:VARIANT\Changelog.html" -PathType Leaf) {
$tag = $env:GITHUB_REF.Substring(10)
(Get-Content "out\$env:VARIANT\Changelog.html").Replace('ArchiSteamFarm/commits/main', "ArchiSteamFarm/releases/tag/$tag") | Set-Content "out\$env:VARIANT\Changelog.html"
}
}
# Create the final zip file
7z a -bd -slp -tzip -mm=Deflate $compressionArgs "out\ASF-$env:VARIANT.zip" "$env:GITHUB_WORKSPACE\out\$env:VARIANT\*"
if ($LastExitCode -ne 0) {
throw "Last command failed."
}
# We can aid non-windows users by adding chmod +x flag to appropriate executables directly in the zip file
# This is ALMOST a hack, but works reliably enough
if (Test-Path "tools\zip_exec\zip_exec.exe" -PathType Leaf) {
$executableFiles = @()
if ($env:VARIANT -like 'generic*') {
$executableFiles += 'ArchiSteamFarm.sh', 'ArchiSteamFarm-Service.sh'
} elseif (($env:VARIANT -like 'linux*') -or ($env:VARIANT -like 'osx*')) {
$executableFiles += 'ArchiSteamFarm', 'ArchiSteamFarm-Service.sh'
}
foreach ($executableFile in $executableFiles) {
tools\zip_exec\zip_exec.exe "out\ASF-$env:VARIANT.zip" "$executableFile"
if ($LastExitCode -ne 0) {
throw "Last command failed."
}
}
}
- name: Upload ASF-${{ matrix.variant }}
uses: actions/upload-artifact@v3.1.2
with:
name: ${{ matrix.os }}_ASF-win-x64
path: out/ASF-win-x64.zip
name: ${{ matrix.os }}_ASF-${{ matrix.variant }}
path: out/ASF-${{ matrix.variant }}.zip
release:
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') }}
needs: publish
needs: publish-asf
runs-on: ubuntu-latest
steps:
@@ -496,6 +461,12 @@ jobs:
name: macos-latest_ASF-osx-x64
path: out
- name: Download ASF-win-arm64 artifact from windows-latest
uses: actions/download-artifact@v3.0.2
with:
name: windows-latest_ASF-win-arm64
path: out
- name: Download ASF-win-x64 artifact from windows-latest
uses: actions/download-artifact@v3.0.2
with:
@@ -533,111 +504,11 @@ jobs:
- name: Create ArchiSteamFarm GitHub release
id: github_release
uses: actions/create-release@v1.1.4
env:
GITHUB_TOKEN: ${{ secrets.ARCHIBOT_GITHUB_TOKEN }}
uses: ncipollo/release-action@v1.12.0
with:
tag_name: ${{ github.ref }}
release_name: ArchiSteamFarm V${{ github.ref }}
body_path: .github/RELEASE_TEMPLATE.md
artifacts: "out/*"
bodyFile: .github/RELEASE_TEMPLATE.md
makeLatest: false
name: ArchiSteamFarm V${{ github.ref_name }}
prerelease: true
- name: Upload ASF-generic to GitHub release
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.github_release.outputs.upload_url }}
asset_path: out/ASF-generic.zip
asset_name: ASF-generic.zip
asset_content_type: application/zip
- name: Upload ASF-generic-netf to GitHub release
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.github_release.outputs.upload_url }}
asset_path: out/ASF-generic-netf.zip
asset_name: ASF-generic-netf.zip
asset_content_type: application/zip
- name: Upload ASF-linux-arm to GitHub release
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.github_release.outputs.upload_url }}
asset_path: out/ASF-linux-arm.zip
asset_name: ASF-linux-arm.zip
asset_content_type: application/zip
- name: Upload ASF-linux-arm64 to GitHub release
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.github_release.outputs.upload_url }}
asset_path: out/ASF-linux-arm64.zip
asset_name: ASF-linux-arm64.zip
asset_content_type: application/zip
- name: Upload ASF-linux-x64 to GitHub release
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.github_release.outputs.upload_url }}
asset_path: out/ASF-linux-x64.zip
asset_name: ASF-linux-x64.zip
asset_content_type: application/zip
- name: Upload ASF-osx-arm64 to GitHub release
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.github_release.outputs.upload_url }}
asset_path: out/ASF-osx-arm64.zip
asset_name: ASF-osx-arm64.zip
asset_content_type: application/zip
- name: Upload ASF-osx-x64 to GitHub release
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.github_release.outputs.upload_url }}
asset_path: out/ASF-osx-x64.zip
asset_name: ASF-osx-x64.zip
asset_content_type: application/zip
- name: Upload ASF-win-x64 to GitHub release
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.github_release.outputs.upload_url }}
asset_path: out/ASF-win-x64.zip
asset_name: ASF-win-x64.zip
asset_content_type: application/zip
- name: Upload SHA512SUMS to GitHub release
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.github_release.outputs.upload_url }}
asset_path: out/SHA512SUMS
asset_name: SHA512SUMS
asset_content_type: text/plain
- name: Upload SHA512SUMS.sign to GitHub release
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.github_release.outputs.upload_url }}
asset_path: out/SHA512SUMS.sign
asset_name: SHA512SUMS.sign
asset_content_type: text/plain
token: ${{ secrets.ARCHIBOT_GITHUB_TOKEN }}

View File

@@ -26,7 +26,7 @@ jobs:
git reset --hard origin/master
- name: Download latest translations from Crowdin
uses: crowdin/github-action@1.5.2
uses: crowdin/github-action@v1.7.0
with:
upload_sources: false
download_translations: true

2
ASF-ui

Submodule ASF-ui updated: 80b91db274...b30b3f5bce

View File

@@ -11,7 +11,7 @@
<PackageReference Include="System.Composition.AttributedModel" IncludeAssets="compile" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net481'">
<ItemGroup Condition="'$(TargetFramework)' == 'net481' OR '$(TargetFramework)' == 'netstandard2.1'">
<!-- Madness is already included in netf build of ASF, so we don't need to emit it ourselves -->
<PackageReference Update="JustArchiNET.Madness" IncludeAssets="compile" />

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -9,7 +9,7 @@
<PackageReference Include="System.Composition.AttributedModel" IncludeAssets="compile" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net481'">
<ItemGroup Condition="'$(TargetFramework)' == 'net481' OR '$(TargetFramework)' == 'netstandard2.1'">
<!-- Madness is already included in netf build of ASF, so we don't need to emit it ourselves -->
<PackageReference Update="JustArchiNET.Madness" IncludeAssets="compile" />
</ItemGroup>

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AngleSharp.XPath" IncludeAssets="compile" />
<PackageReference Include="ConfigureAwaitChecker.Analyzer" PrivateAssets="all" />
<PackageReference Include="JetBrains.Annotations" PrivateAssets="all" />
<PackageReference Include="Newtonsoft.Json" IncludeAssets="compile" />
<PackageReference Include="System.Composition.AttributedModel" IncludeAssets="compile" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net481' OR '$(TargetFramework)' == 'netstandard2.1'">
<!-- Madness is already included in netf build of ASF, so we don't need to emit it ourselves -->
<PackageReference Update="JustArchiNET.Madness" IncludeAssets="compile" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" IncludeAssets="compile" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net481'">
<Reference Include="System.Net.Http" HintPath="C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.8.1\System.Net.Http.dll" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ArchiSteamFarm\ArchiSteamFarm.csproj" ExcludeAssets="all" Private="false" />
</ItemGroup>
</Project>

View File

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

View File

@@ -0,0 +1,30 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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;
using Newtonsoft.Json;
namespace ArchiSteamFarm.CustomPlugins.SignInWithSteam.Data;
public sealed class SignInWithSteamRequest {
[JsonProperty(Required = Required.Always)]
public Uri RedirectURL { get; private set; } = null!;
}

View File

@@ -0,0 +1,32 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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;
using Newtonsoft.Json;
namespace ArchiSteamFarm.CustomPlugins.SignInWithSteam.Data;
public sealed class SignInWithSteamResponse {
[JsonProperty(Required = Required.Always)]
public Uri ReturnURL { get; private set; }
internal SignInWithSteamResponse(Uri returnURL) => ReturnURL = returnURL ?? throw new ArgumentNullException(nameof(returnURL));
}

View File

@@ -0,0 +1,122 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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;
using System.Globalization;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using AngleSharp.Dom;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.CustomPlugins.SignInWithSteam.Data;
using ArchiSteamFarm.IPC.Controllers.Api;
using ArchiSteamFarm.IPC.Responses;
using ArchiSteamFarm.Localization;
using ArchiSteamFarm.Steam;
using ArchiSteamFarm.Steam.Integration;
using ArchiSteamFarm.Web;
using ArchiSteamFarm.Web.Responses;
using Microsoft.AspNetCore.Mvc;
namespace ArchiSteamFarm.CustomPlugins.SignInWithSteam;
[Route("/Api/Bot/{botName:required}/SignInWithSteam")]
public sealed class SignInWithSteamController : ArchiController {
[HttpPost]
[ProducesResponseType(typeof(GenericResponse<SignInWithSteamResponse>), (int) HttpStatusCode.OK)]
[ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.BadRequest)]
[ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.ServiceUnavailable)]
public async Task<ActionResult<GenericResponse>> Post(string botName, [FromBody] SignInWithSteamRequest request) {
if (string.IsNullOrEmpty(botName)) {
throw new ArgumentNullException(nameof(botName));
}
ArgumentNullException.ThrowIfNull(request);
Bot? bot = Bot.GetBot(botName);
if (bot == null) {
return BadRequest(new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botName)));
}
if (!bot.IsConnectedAndLoggedOn) {
return StatusCode((int) HttpStatusCode.ServiceUnavailable, new GenericResponse(false, Strings.BotNotConnected));
}
// We've got a redirection, initiate OpenID procedure by following it
using HtmlDocumentResponse? challengeResponse = await bot.ArchiWebHandler.UrlGetToHtmlDocumentWithSession(request.RedirectURL).ConfigureAwait(false);
if (challengeResponse?.Content == null) {
return StatusCode((int) HttpStatusCode.ServiceUnavailable, new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries)));
}
IAttr? paramsNode = challengeResponse.Content.SelectSingleNode<IAttr>("//input[@name='openidparams']/@value");
if (paramsNode == null) {
ASF.ArchiLogger.LogNullError(paramsNode);
return StatusCode((int) HttpStatusCode.InternalServerError, new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorObjectIsNull, nameof(paramsNode))));
}
string paramsValue = paramsNode.Value;
if (string.IsNullOrEmpty(paramsValue)) {
ASF.ArchiLogger.LogNullError(paramsValue);
return StatusCode((int) HttpStatusCode.InternalServerError, new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorObjectIsNull, nameof(paramsValue))));
}
IAttr? nonceNode = challengeResponse.Content.SelectSingleNode<IAttr>("//input[@name='nonce']/@value");
if (nonceNode == null) {
ASF.ArchiLogger.LogNullError(nonceNode);
return StatusCode((int) HttpStatusCode.InternalServerError, new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorObjectIsNull, nameof(nonceNode))));
}
string nonceValue = nonceNode.Value;
if (string.IsNullOrEmpty(nonceValue)) {
ASF.ArchiLogger.LogNullError(nonceValue);
return StatusCode((int) HttpStatusCode.InternalServerError, new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorObjectIsNull, nameof(nonceValue))));
}
Uri loginRequest = new(ArchiWebHandler.SteamCommunityURL, "/openid/login");
using StringContent actionContent = new("steam_openid_login");
using StringContent modeContent = new("checkid_setup");
using StringContent paramsContent = new(paramsValue);
using StringContent nonceContent = new(nonceValue);
using MultipartFormDataContent data = new();
data.Add(actionContent, "action");
data.Add(modeContent, "openid.mode");
data.Add(paramsContent, "openidparams");
data.Add(nonceContent, "nonce");
// Accept OpenID request presented and follow redirection back to the data we initially expected
BasicResponse? loginResponse = await bot.ArchiWebHandler.WebBrowser.UrlPost(loginRequest, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnRedirections).ConfigureAwait(false);
return loginResponse != null ? Ok(new GenericResponse<SignInWithSteamResponse>(new SignInWithSteamResponse(loginResponse.FinalUri))) : StatusCode((int) HttpStatusCode.ServiceUnavailable, new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries)));
}
}

View File

@@ -0,0 +1,43 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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;
using System.Composition;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Plugins.Interfaces;
using JetBrains.Annotations;
namespace ArchiSteamFarm.CustomPlugins.SignInWithSteam;
[Export(typeof(IPlugin))]
[UsedImplicitly]
internal sealed class SignInWithSteamPlugin : IPlugin {
public string Name => nameof(SignInWithSteamPlugin);
public Version Version => typeof(SignInWithSteamPlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version));
public Task OnLoaded() {
ASF.ArchiLogger.LogGenericInfo("Loaded!");
return Task.CompletedTask;
}
}

View File

@@ -13,10 +13,12 @@
<PackageReference Include="System.Linq.Async" IncludeAssets="compile" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net481'">
<ItemGroup Condition="'$(TargetFramework)' == 'net481' OR '$(TargetFramework)' == 'netstandard2.1'">
<!-- Madness is already included in netf build of ASF, so we don't need to emit it ourselves -->
<PackageReference Update="JustArchiNET.Madness" IncludeAssets="compile" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net481'">
<Reference Include="System.Net.Http" HintPath="C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.8.1\System.Net.Http.dll" />
</ItemGroup>

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -38,7 +38,7 @@ using SteamKit2;
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher;
internal static class Backend {
internal static async Task<BasicResponse?> AnnounceForListing(ulong steamID, WebBrowser webBrowser, IReadOnlyCollection<AssetForListing> inventory, IReadOnlyCollection<Asset.EType> acceptedMatchableTypes, bool matchEverything, string tradeToken, string? nickname = null, string? avatarHash = null) {
internal static async Task<BasicResponse?> AnnounceForListing(ulong steamID, WebBrowser webBrowser, IReadOnlyList<AssetForListing> inventory, IReadOnlyCollection<Asset.EType> acceptedMatchableTypes, uint totalInventoryCount, bool matchEverything, string tradeToken, string? nickname = null, string? avatarHash = null) {
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
throw new ArgumentOutOfRangeException(nameof(steamID));
}
@@ -53,6 +53,10 @@ internal static class Backend {
throw new ArgumentNullException(nameof(acceptedMatchableTypes));
}
if (totalInventoryCount == 0) {
throw new ArgumentOutOfRangeException(nameof(totalInventoryCount));
}
if (string.IsNullOrEmpty(tradeToken)) {
throw new ArgumentNullException(nameof(tradeToken));
}
@@ -61,11 +65,11 @@ internal static class Backend {
throw new ArgumentOutOfRangeException(nameof(tradeToken));
}
Uri request = new(ArchiNet.URL, "/Api/Listing/Announce/v2");
Uri request = new(ArchiNet.URL, "/Api/Listing/Announce/v3");
AnnouncementRequest data = new(ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid(), steamID, tradeToken, inventory, acceptedMatchableTypes, matchEverything, ASF.GlobalConfig?.MaxTradeHoldDuration ?? GlobalConfig.DefaultMaxTradeHoldDuration, nickname, avatarHash);
AnnouncementRequest data = new(ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid(), steamID, tradeToken, inventory, acceptedMatchableTypes, totalInventoryCount, matchEverything, ASF.GlobalConfig?.MaxTradeHoldDuration ?? GlobalConfig.DefaultMaxTradeHoldDuration, nickname, avatarHash);
return await webBrowser.UrlPost(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnRedirections | WebBrowser.ERequestOptions.ReturnClientErrors).ConfigureAwait(false);
return await webBrowser.UrlPost(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnRedirections | WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.CompressRequest).ConfigureAwait(false);
}
internal static async Task<(HttpStatusCode StatusCode, ImmutableHashSet<ListedUser> Users)?> GetListedUsersForMatching(Guid licenseID, Bot bot, WebBrowser webBrowser, IReadOnlyCollection<Asset> inventory, IReadOnlyCollection<Asset.EType> acceptedMatchableTypes) {
@@ -92,7 +96,7 @@ internal static class Backend {
InventoriesRequest data = new(ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid(), bot.SteamID, inventory, acceptedMatchableTypes);
ObjectResponse<GenericResponse<ImmutableHashSet<ListedUser>>>? response = await webBrowser.UrlPostToJsonObject<GenericResponse<ImmutableHashSet<ListedUser>>, InventoriesRequest>(request, headers, data, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.AllowInvalidBodyOnErrors).ConfigureAwait(false);
ObjectResponse<GenericResponse<ImmutableHashSet<ListedUser>>>? response = await webBrowser.UrlPostToJsonObject<GenericResponse<ImmutableHashSet<ListedUser>>, InventoriesRequest>(request, headers, data, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.AllowInvalidBodyOnErrors | WebBrowser.ERequestOptions.CompressRequest).ConfigureAwait(false);
if (response == null) {
return null;
@@ -109,6 +113,6 @@ internal static class Backend {
HeartBeatRequest data = new(ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid(), bot.SteamID);
return await webBrowser.UrlPost(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnRedirections | WebBrowser.ERequestOptions.ReturnClientErrors).ConfigureAwait(false);
return await webBrowser.UrlPost(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnRedirections | WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.CompressRequest).ConfigureAwait(false);
}
}

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -38,7 +38,7 @@ internal sealed class AnnouncementRequest {
private readonly Guid Guid;
[JsonProperty(Required = Required.Always)]
private readonly ImmutableHashSet<AssetForListing> Inventory;
private readonly ImmutableList<AssetForListing> Inventory;
[JsonProperty(Required = Required.Always)]
private readonly ImmutableHashSet<Asset.EType> MatchableTypes;
@@ -55,10 +55,13 @@ internal sealed class AnnouncementRequest {
[JsonProperty(Required = Required.Always)]
private readonly ulong SteamID;
[JsonProperty(Required = Required.Always)]
private readonly uint TotalInventoryCount;
[JsonProperty(Required = Required.Always)]
private readonly string TradeToken;
internal AnnouncementRequest(Guid guid, ulong steamID, string tradeToken, IReadOnlyCollection<AssetForListing> inventory, IReadOnlyCollection<Asset.EType> matchableTypes, bool matchEverything, byte maxTradeHoldDuration, string? nickname = null, string? avatarHash = null) {
internal AnnouncementRequest(Guid guid, ulong steamID, string tradeToken, IReadOnlyList<AssetForListing> inventory, IReadOnlyCollection<Asset.EType> matchableTypes, uint totalInventoryCount, bool matchEverything, byte maxTradeHoldDuration, string? nickname = null, string? avatarHash = null) {
if (guid == Guid.Empty) {
throw new ArgumentOutOfRangeException(nameof(guid));
}
@@ -83,13 +86,18 @@ internal sealed class AnnouncementRequest {
throw new ArgumentNullException(nameof(matchableTypes));
}
if (totalInventoryCount == 0) {
throw new ArgumentOutOfRangeException(nameof(totalInventoryCount));
}
Guid = guid;
SteamID = steamID;
TradeToken = tradeToken;
Inventory = inventory.ToImmutableHashSet();
Inventory = inventory.ToImmutableList();
MatchableTypes = matchableTypes.ToImmutableHashSet();
MatchEverything = matchEverything;
MaxTradeHoldDuration = maxTradeHoldDuration;
TotalInventoryCount = totalInventoryCount;
Nickname = nickname;
AvatarHash = avatarHash;

View File

@@ -26,16 +26,12 @@ using Newtonsoft.Json;
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data;
internal sealed class AssetForListing : AssetInInventory {
[JsonProperty("i", Required = Required.Always)]
internal readonly uint Index;
[JsonProperty("l", Required = Required.Always)]
internal readonly ulong PreviousAssetID;
internal AssetForListing(Asset asset, uint index, ulong previousAssetID) : base(asset) {
internal AssetForListing(Asset asset, ulong previousAssetID) : base(asset) {
ArgumentNullException.ThrowIfNull(asset);
Index = index;
PreviousAssetID = previousAssetID;
}
}

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -55,6 +55,11 @@ internal sealed class ListedUser {
internal readonly ulong SteamID;
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
[JsonProperty(Required = Required.Always)]
internal readonly uint TotalInventoryCount;
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
[JsonProperty(Required = Required.Always)]
internal readonly string TradeToken = "";

View File

@@ -68,5 +68,11 @@ namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Localization {
return ResourceManager.GetString("TradeOfferFailed", resourceCulture);
}
}
internal static string ActivelyMatchingSomeConfirmationsFailed {
get {
return ResourceManager.GetString("ActivelyMatchingSomeConfirmationsFailed", resourceCulture);
}
}
}
}

View File

@@ -78,4 +78,8 @@
<value>Неуспешно изпращане на трейд оферта до бот {0} ({1}), продължаваме...</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>Някои потвърждения са неуспешни, приблизително {0} от {1} от размените бяха изпратени успешно.</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

@@ -78,4 +78,8 @@
<value>Nepodařilo se odeslat obchodní nabídku pro bota {0} ({1}), pokračuji...</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>Některá potvrzení se nezdařila, úspěšně bylo odesláno přibližně {0} z {1} obchodů.</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

@@ -78,4 +78,8 @@
<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>
<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

@@ -78,4 +78,8 @@
<value>Error al enviar la oferta de intercambio al bot {0} ({1}), continuando...</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>Algunas confirmaciones han fallado, aproximadamente {0} de {1} intercambios fueron enviados exitosamente.</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

@@ -66,4 +66,20 @@
<value>Deze ronde zijn in totaal {0} sets gematcht.</value>
<comment>{0} will be replaced by number of sets traded</comment>
</data>
<data name="ListingAnnouncing" xml:space="preserve">
<value>Mededeling van {0} ({1}) met een inventaris gemaakt van in totaal {2} items op de lijst...</value>
<comment>{0} will be replaced by steam ID (number), {1} will be replaced by user's nickname, {2} will be replaced with number of items in the inventory</comment>
</data>
<data name="MatchingFound" xml:space="preserve">
<value>In totaal {0} items gekoppeld aan bot {1} ({2}), handelsaanbieding verzonden...</value>
<comment>{0} will be replaced by number of items matched, {1} will be replaced by steam ID (number), {2} will be replaced by user's nickname</comment>
</data>
<data name="TradeOfferFailed" xml:space="preserve">
<value>Fout bij het verzenden van een transactie aanbod aan bot {0} ({1}), verder gaan...</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>Sommige bevestigingen zijn mislukt, ongeveer {0} van de {1} transacties zijn succesvol verzonden.</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

@@ -78,4 +78,8 @@
<value>Nie udało się wysłać oferty wymiany do bota {0} ({1}), przechodzenie dalej...</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>Niektóre potwierdzenia nie powiodły się, około {0} z {1} transakcji zostało wysłanych pomyślnie.</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

@@ -78,4 +78,8 @@
<value>Falha ao enviar uma oferta de troca para o bot {0} ({1}), prosseguindo...</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>Algumas confirmações falharam, cerca de {0} de {1} operações foram enviadas com sucesso.</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

@@ -78,4 +78,8 @@
<value>FAILD 2 SEND TRADE OFFR 2 BOT {0} ({1}), MOVIN ON...</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>SUM CONFIRMASHUNS HAS FAILD, APPROXIMATELY {0} OUT OV {1} TRADEZ WUZ SENT SUCCESFULLY.</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

@@ -78,4 +78,8 @@
<value>Failed to send a trade offer to bot {0} ({1}), moving on...</value>
<comment>{0} will be replaced by steam ID (number), {1} will be replaced by user's nickname'</comment>
</data>
<data name="ActivelyMatchingSomeConfirmationsFailed" xml:space="preserve">
<value>Some confirmations have failed, approximately {0} out of {1} trades were sent successfully.</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

@@ -78,4 +78,8 @@
<value>Не удалось отправить предложение о сделке боту {0} ({1}), переходим в...</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>Некоторые подтверждения не удались - примерно {0} трейдов из {1} были успешно отправлены.</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

@@ -63,7 +63,23 @@
</value>
</resheader>
<data name="ActivelyMatchingItemsRound" xml:space="preserve">
<value>За цей раунд зібрано {0} наборів карток.</value>
<value>Загальна кількість наборів у цьому раунді: {0}.</value>
<comment>{0} will be replaced by number of sets traded</comment>
</data>
<data name="ListingAnnouncing" xml:space="preserve">
<value>Оголошення від {0} ({1}) із загальною кількістю товарів у списку: {2}...</value>
<comment>{0} will be replaced by steam ID (number), {1} will be replaced by user's nickname, {2} will be replaced with number of items in the inventory</comment>
</data>
<data name="MatchingFound" xml:space="preserve">
<value>Всього зібрано {0} предметів за допомогою бота {1} ({2}), надсилаю пропозицію обміну...</value>
<comment>{0} will be replaced by number of items matched, {1} will be replaced by steam ID (number), {2} will be replaced by user's nickname</comment>
</data>
<data name="TradeOfferFailed" xml:space="preserve">
<value>Не вдалося відправити пропозицію обміну боту {0} ({1}), йдемо далі...</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>Деякі підтвердження завершилися невдачею, приблизно {0} з {1} угод були відправлені успішно.</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

@@ -78,4 +78,8 @@
<value>Thất bại khi gửi đề nghị trao đổi tới bot {0} ({1}), đang tiếp tục...</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>Một số xác nhận thất bại, khoảng {0} trong số {1} giao dịch đã được gửi thành công.</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

@@ -78,4 +78,8 @@
<value>向机器人 {0}{1})发送交易报价失败,正在继续…</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>部分确认失败,{1} 个交易中约有 {0} 个发送成功。</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

@@ -78,4 +78,8 @@
<value>無法向 Bot {0}{1})發送交易提案,正在繼續…</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>部分交易失敗,{1} 個交易中約 {0} 個發送成功。</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

@@ -29,6 +29,7 @@ using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Helpers;
using ArchiSteamFarm.Localization;
using ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data;
using ArchiSteamFarm.Steam;
@@ -41,11 +42,15 @@ using ArchiSteamFarm.Steam.Storage;
using ArchiSteamFarm.Storage;
using ArchiSteamFarm.Web;
using ArchiSteamFarm.Web.Responses;
using Newtonsoft.Json.Linq;
using SteamKit2;
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher;
internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
private const string MatchActivelyTradeOfferIDsStorageKey = $"{nameof(ItemsMatcher)}-{nameof(MatchActively)}-TradeOfferIDs";
private const byte MaxAnnouncementTTL = 60; // Maximum amount of minutes we can wait if the next announcement doesn't happen naturally
private const byte MaxTradeOffersActive = 10; // The actual upper limit is 30, but we should use lower amount to allow some bots to react before we hit the maximum allowed
private const byte MinAnnouncementTTL = 5; // Minimum amount of minutes we must wait before the next Announcement
private const byte MinHeartBeatTTL = 10; // Minimum amount of minutes we must wait before sending next HeartBeat
private const byte MinPersonaStateTTL = 5; // Minimum amount of minutes we must wait before requesting persona state update
@@ -57,16 +62,18 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
Asset.EType.TradingCard
);
// We access this collection only within a semaphore, therefore there is no need for concurrent access
private readonly Dictionary<ulong, uint> AnnouncedItems = new();
private readonly Bot Bot;
private readonly Timer? HeartBeatTimer;
// We access this collection only within a semaphore, therefore there is no need for concurrent access
private readonly Dictionary<ulong, uint> LastAnnouncedItems = new();
private readonly SemaphoreSlim MatchActivelySemaphore = new(1, 1);
private readonly Timer? MatchActivelyTimer;
private readonly SemaphoreSlim RequestsSemaphore = new(1, 1);
private readonly WebBrowser? WebBrowser;
private readonly WebBrowser WebBrowser;
private string? LastAnnouncedTradeToken;
private DateTime LastAnnouncement;
private DateTime LastHeartBeat;
private DateTime LastPersonaStateRequest;
@@ -78,14 +85,9 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
ArgumentNullException.ThrowIfNull(bot);
Bot = bot;
if (!Bot.BotConfig.RemoteCommunication.HasFlag(BotConfig.ERemoteCommunication.PublicListing) && !Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchActively)) {
return;
}
WebBrowser = new WebBrowser(bot.ArchiLogger, ASF.GlobalConfig?.WebProxy, true);
if (Bot.BotConfig.RemoteCommunication.HasFlag(BotConfig.ERemoteCommunication.PublicListing)) {
if (Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.SteamTradeMatcher) && Bot.BotConfig.RemoteCommunication.HasFlag(BotConfig.ERemoteCommunication.PublicListing)) {
HeartBeatTimer = new Timer(
HeartBeat,
null,
@@ -112,6 +114,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
// Those are objects that are always being created if constructor doesn't throw exception
MatchActivelySemaphore.Dispose();
RequestsSemaphore.Dispose();
WebBrowser.Dispose();
// Those are objects that might be null and the check should be in-place
HeartBeatTimer?.Dispose();
@@ -122,16 +125,10 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
MatchActivelyTimer.Dispose();
}
}
WebBrowser?.Dispose();
}
public async ValueTask DisposeAsync() {
// Those are objects that are always being created if constructor doesn't throw exception
MatchActivelySemaphore.Dispose();
RequestsSemaphore.Dispose();
// Those are objects that might be null and the check should be in-place
// Dispose timers first so we won't launch new events
if (HeartBeatTimer != null) {
await HeartBeatTimer.DisposeAsync().ConfigureAwait(false);
}
@@ -143,7 +140,13 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
}
}
WebBrowser?.Dispose();
// Ensure the semaphores are closed, then dispose the rest
await MatchActivelySemaphore.WaitAsync().ConfigureAwait(false);
await RequestsSemaphore.WaitAsync().ConfigureAwait(false);
MatchActivelySemaphore.Dispose();
RequestsSemaphore.Dispose();
WebBrowser.Dispose();
}
internal void OnNewItemsNotification() => ShouldSendAnnouncementEarlier = true;
@@ -153,11 +156,12 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
return;
}
if (WebBrowser == null) {
throw new InvalidOperationException(nameof(WebBrowser));
if ((DateTime.UtcNow < LastAnnouncement.AddMinutes(ShouldSendAnnouncementEarlier ? MinAnnouncementTTL : MaxAnnouncementTTL)) && ShouldSendHeartBeats) {
return;
}
if ((DateTime.UtcNow < LastAnnouncement.AddMinutes(ShouldSendAnnouncementEarlier ? MinAnnouncementTTL : MaxAnnouncementTTL)) && ShouldSendHeartBeats) {
if (MatchActivelySemaphore.CurrentCount == 0) {
// We shouldn't bother with announcements while we're matching, it can wait until we're done
return;
}
@@ -168,6 +172,11 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
return;
}
if (MatchActivelySemaphore.CurrentCount == 0) {
// We shouldn't bother with announcements while we're matching, it can wait until we're done
return;
}
// Don't announce if we don't meet conditions
bool? eligible = await IsEligibleForListing().ConfigureAwait(false);
@@ -238,28 +247,29 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
bool matchEverything = Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchEverything);
uint index = 0;
ulong previousAssetID = 0;
HashSet<AssetForListing> assetsForListing = new();
List<AssetForListing> assetsForListing = new();
Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), Dictionary<ulong, uint>> tradableState = new();
Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), bool> tradableSets = new();
foreach (Asset item in inventory) {
if (acceptedMatchableTypes.Contains(item.Type)) {
// Only tradable assets matter for MatchEverything bots
if (!matchEverything || item.Tradable) {
assetsForListing.Add(new AssetForListing(item, index++, previousAssetID));
assetsForListing.Add(new AssetForListing(item, previousAssetID));
}
// But even for Fair bots, we should track and skip sets where we don't have any item to trade with
if (!matchEverything) {
(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity) key = (item.RealAppID, item.Type, item.Rarity);
if (tradableState.TryGetValue(key, out Dictionary<ulong, uint>? set)) {
set[item.ClassID] = set.TryGetValue(item.ClassID, out uint tradableAmount) ? tradableAmount + (item.Tradable ? item.Amount : 0) : item.Tradable ? item.Amount : 0;
if (tradableSets.TryGetValue(key, out bool tradable)) {
if (!tradable && item.Tradable) {
tradableSets[key] = true;
}
} else {
tradableState[key] = new Dictionary<ulong, uint> { { item.ClassID, item.Tradable ? item.Amount : 0 } };
tradableSets[key] = item.Tradable;
}
}
}
@@ -277,22 +287,18 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
// We can now skip sets where we don't have any item to trade with, MatchEverything bots are already filtered to tradable only
if (!matchEverything) {
HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> setsToRemove = tradableState.Where(static set => set.Value.Values.All(static amount => amount == 0)).Select(static set => set.Key).ToHashSet();
assetsForListing.RemoveAll(item => tradableSets.TryGetValue((item.RealAppID, item.Type, item.Rarity), out bool tradable) && !tradable);
if (setsToRemove.Count > 0) {
assetsForListing.RemoveWhere(item => setsToRemove.Contains((item.RealAppID, item.Type, item.Rarity)));
if (assetsForListing.Count == 0) {
// We're not eligible, record this as a valid check
LastAnnouncement = DateTime.UtcNow;
ShouldSendAnnouncementEarlier = ShouldSendHeartBeats = false;
if (assetsForListing.Count == 0) {
// We're not eligible, record this as a valid check
LastAnnouncement = DateTime.UtcNow;
ShouldSendAnnouncementEarlier = ShouldSendHeartBeats = false;
return;
}
return;
}
}
if (ShouldSendHeartBeats && (assetsForListing.Count == AnnouncedItems.Count) && assetsForListing.All(item => AnnouncedItems.TryGetValue(item.AssetID, out uint amount) && (item.Amount == amount))) {
if (ShouldSendHeartBeats && (tradeToken == LastAnnouncedTradeToken) && (assetsForListing.Count == LastAnnouncedItems.Count) && assetsForListing.All(item => LastAnnouncedItems.TryGetValue(item.AssetID, out uint amount) && (item.Amount == amount))) {
// There is nothing new to announce, this is fine, skip the request
LastAnnouncement = DateTime.UtcNow;
ShouldSendAnnouncementEarlier = false;
@@ -321,10 +327,10 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
SignedInWithSteam = true;
}
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Localization.Strings.ListingAnnouncing, Bot.SteamID, nickname, assetsForListing.Count));
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Localization.Strings.ListingAnnouncing, Bot.SteamID, nickname ?? Bot.SteamID.ToString(CultureInfo.InvariantCulture), assetsForListing.Count));
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
BasicResponse? response = await Backend.AnnounceForListing(Bot.SteamID, WebBrowser, assetsForListing, acceptedMatchableTypes, matchEverything, tradeToken!, nickname, avatarHash).ConfigureAwait(false);
BasicResponse? response = await Backend.AnnounceForListing(Bot.SteamID, WebBrowser, assetsForListing, acceptedMatchableTypes, (uint) inventory.Count, matchEverything, tradeToken!, nickname, avatarHash).ConfigureAwait(false);
if (response == null) {
// This is actually a network failure, so we'll stop sending heartbeats but not record it as valid check
@@ -364,7 +370,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
LastAnnouncement = DateTime.UtcNow.AddYears(1);
return;
#if NETFRAMEWORK
#if NETFRAMEWORK || NETSTANDARD
case (HttpStatusCode) 429:
#else
case HttpStatusCode.TooManyRequests:
@@ -386,13 +392,14 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
ShouldSendAnnouncementEarlier = false;
ShouldSendHeartBeats = true;
AnnouncedItems.Clear();
LastAnnouncedTradeToken = tradeToken;
LastAnnouncedItems.Clear();
foreach (AssetForListing item in assetsForListing) {
AnnouncedItems[item.AssetID] = item.Amount;
LastAnnouncedItems[item.AssetID] = item.Amount;
}
AnnouncedItems.TrimExcess();
LastAnnouncedItems.TrimExcess();
} finally {
RequestsSemaphore.Release();
}
@@ -412,10 +419,6 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
}
private async void HeartBeat(object? state = null) {
if (WebBrowser == null) {
throw new InvalidOperationException(nameof(WebBrowser));
}
if (!Bot.IsConnectedAndLoggedOn || (Bot.HeartBeatFailures > 0)) {
return;
}
@@ -494,6 +497,20 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
}
private async Task<bool?> IsEligibleForMatching() {
// Bot can't be limited
if (Bot.IsAccountLimited) {
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(Bot.IsAccountLimited)}: {Bot.IsAccountLimited}"));
return false;
}
// Bot can't be on lockdown
if (Bot.IsAccountLocked) {
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(Bot.IsAccountLocked)}: {Bot.IsAccountLocked}"));
return false;
}
// Bot must have ASF 2FA
if (!Bot.HasMobileAuthenticator) {
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(Bot.HasMobileAuthenticator)}: {Bot.HasMobileAuthenticator}"));
@@ -514,17 +531,22 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
if (hasValidApiKey != true) {
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(Bot.ArchiWebHandler.HasValidApiKey)}: {hasValidApiKey?.ToString() ?? "null"}"));
return hasValidApiKey;
return hasValidApiKey.HasValue ? false : null;
}
// Bot can't be trade banned
(bool _, bool? Result) economyBan = await Bot.ArchiWebHandler.CachedEconomyBan.GetValue(ECacheFallback.SuccessPreviously).ConfigureAwait(false);
if (economyBan.Result != false) {
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(economyBan)}: {economyBan.Result?.ToString() ?? "null"}"));
return economyBan.Result.HasValue ? false : null;
}
return true;
}
private async void MatchActively(object? state = null) {
if (WebBrowser == null) {
throw new InvalidOperationException(nameof(WebBrowser));
}
if (ASF.GlobalConfig == null) {
throw new InvalidOperationException(nameof(ASF.GlobalConfig));
}
@@ -561,6 +583,8 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
return;
}
bool tradesSent;
try {
Bot.ArchiLogger.LogGenericInfo(Strings.Starting);
@@ -626,16 +650,22 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
using (await Bot.Actions.GetTradingLock().ConfigureAwait(false)) {
#pragma warning restore CA2000 // False positive, we're actually wrapping it in the using clause below exactly for that purpose
Bot.ArchiLogger.LogGenericInfo(Strings.Starting);
await MatchActively(response.Value.Users, ourInventory, acceptedMatchableTypes).ConfigureAwait(false);
tradesSent = await MatchActively(response.Value.Users, ourInventory, acceptedMatchableTypes).ConfigureAwait(false);
}
Bot.ArchiLogger.LogGenericInfo(Strings.Done);
} finally {
MatchActivelySemaphore.Release();
}
if (tradesSent && ShouldSendHeartBeats && (DateTime.UtcNow > LastAnnouncement.AddMinutes(ShouldSendAnnouncementEarlier ? MinAnnouncementTTL : MaxAnnouncementTTL))) {
// If we're announced, it makes sense to update our state now, at least once
Bot.RequestPersonaStateUpdate();
}
}
private async Task MatchActively(IReadOnlyCollection<ListedUser> listedUsers, Dictionary<ulong, Asset> ourInventory, IReadOnlyCollection<Asset.EType> acceptedMatchableTypes) {
private async Task<bool> MatchActively(IReadOnlyCollection<ListedUser> listedUsers, Dictionary<ulong, Asset> ourInventory, IReadOnlyCollection<Asset.EType> acceptedMatchableTypes) {
if ((listedUsers == null) || (listedUsers.Count == 0)) {
throw new ArgumentNullException(nameof(listedUsers));
}
@@ -654,14 +684,67 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
// User doesn't have any more dupes in the inventory
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, $"{nameof(ourFullState)} || {nameof(ourTradableState)}"));
return;
return false;
}
// Cancel previous trade offers sent and deprioritize SteamIDs that didn't answer us in this round
HashSet<ulong>? matchActivelyTradeOfferIDs = null;
JToken? matchActivelyTradeOfferIDsToken = Bot.BotDatabase.LoadFromJsonStorage(MatchActivelyTradeOfferIDsStorageKey);
if (matchActivelyTradeOfferIDsToken != null) {
try {
matchActivelyTradeOfferIDs = matchActivelyTradeOfferIDsToken.ToObject<HashSet<ulong>>();
} catch (Exception e) {
Bot.ArchiLogger.LogGenericWarningException(e);
}
}
matchActivelyTradeOfferIDs ??= new HashSet<ulong>();
HashSet<ulong> deprioritizedSteamIDs = new();
if (matchActivelyTradeOfferIDs.Count > 0) {
// This is not a mandatory step, we allow it to fail
HashSet<TradeOffer>? sentTradeOffers = await Bot.ArchiWebHandler.GetTradeOffers(true, false, true, false).ConfigureAwait(false);
if (sentTradeOffers != null) {
HashSet<ulong> activeTradeOfferIDs = new();
foreach (TradeOffer tradeOffer in sentTradeOffers.Where(tradeOffer => (tradeOffer.State == ETradeOfferState.Active) && matchActivelyTradeOfferIDs.Contains(tradeOffer.TradeOfferID))) {
deprioritizedSteamIDs.Add(tradeOffer.OtherSteamID64);
if (!await Bot.ArchiWebHandler.CancelTradeOffer(tradeOffer.TradeOfferID).ConfigureAwait(false)) {
activeTradeOfferIDs.Add(tradeOffer.TradeOfferID);
}
}
if (!matchActivelyTradeOfferIDs.SetEquals(activeTradeOfferIDs)) {
matchActivelyTradeOfferIDs = activeTradeOfferIDs;
if (matchActivelyTradeOfferIDs.Count > 0) {
Bot.BotDatabase.SaveToJsonStorage(MatchActivelyTradeOfferIDsStorageKey, JToken.FromObject(matchActivelyTradeOfferIDs));
} else {
Bot.BotDatabase.DeleteFromJsonStorage(MatchActivelyTradeOfferIDsStorageKey);
}
}
}
}
HashSet<ulong> pendingMobileTradeOfferIDs = new();
byte maxTradeHoldDuration = ASF.GlobalConfig?.MaxTradeHoldDuration ?? GlobalConfig.DefaultMaxTradeHoldDuration;
byte failuresInRow = 0;
uint matchedSets = 0;
foreach (ListedUser listedUser in listedUsers.Where(listedUser => (listedUser.SteamID != Bot.SteamID) && acceptedMatchableTypes.Any(listedUser.MatchableTypes.Contains) && !Bot.IsBlacklistedFromTrades(listedUser.SteamID)).OrderByDescending(static listedUser => listedUser.MatchEverything).ThenBy(static listedUser => listedUser.Assets.Count)) {
foreach (ListedUser listedUser in listedUsers.Where(listedUser => (listedUser.SteamID != Bot.SteamID) && acceptedMatchableTypes.Any(listedUser.MatchableTypes.Contains) && !Bot.IsBlacklistedFromTrades(listedUser.SteamID)).OrderBy(listedUser => deprioritizedSteamIDs.Contains(listedUser.SteamID)).ThenByDescending(static listedUser => listedUser.MatchEverything).ThenBy(static listedUser => listedUser.TotalInventoryCount)) {
if (failuresInRow >= WebBrowser.MaxTries) {
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(failuresInRow)} >= {WebBrowser.MaxTries}"));
break;
}
HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> wantedSets = ourTradableState.Keys.Where(set => listedUser.MatchableTypes.Contains(set.Type)).ToHashSet();
if (wantedSets.Count == 0) {
@@ -824,33 +907,46 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
if ((itemsToGive.Count != itemsToReceive.Count) || !Trading.IsFairExchange(itemsToGive, itemsToReceive)) {
// Failsafe
Bot.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, Strings.ErrorAborted));
return;
throw new InvalidOperationException($"{nameof(itemsToGive)} && {nameof(itemsToReceive)}");
}
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Localization.Strings.MatchingFound, itemsToReceive.Count, listedUser.SteamID, listedUser.Nickname));
Bot.ArchiLogger.LogGenericTrace($"{Bot.SteamID} <- {string.Join(", ", itemsToReceive.Select(static item => $"{item.RealAppID}/{item.Type}/{item.Rarity}/{item.ClassID} #{item.Amount}"))} | {string.Join(", ", itemsToGive.Select(static item => $"{item.RealAppID}/{item.Type}/{item.Rarity}/{item.ClassID} #{item.Amount}"))} -> {listedUser.SteamID}");
(bool success, HashSet<ulong>? mobileTradeOfferIDs) = await Bot.ArchiWebHandler.SendTradeOffer(listedUser.SteamID, itemsToGive, itemsToReceive, listedUser.TradeToken, true).ConfigureAwait(false);
(bool success, HashSet<ulong>? tradeOfferIDs, HashSet<ulong>? mobileTradeOfferIDs) = await Bot.ArchiWebHandler.SendTradeOffer(listedUser.SteamID, itemsToGive, itemsToReceive, listedUser.TradeToken, true).ConfigureAwait(false);
if ((mobileTradeOfferIDs?.Count > 0) && Bot.HasMobileAuthenticator) {
(bool twoFactorSuccess, _, _) = await Bot.Actions.HandleTwoFactorAuthenticationConfirmations(true, Confirmation.EType.Trade, mobileTradeOfferIDs, true).ConfigureAwait(false);
if (tradeOfferIDs?.Count > 0) {
matchActivelyTradeOfferIDs.UnionWith(tradeOfferIDs);
if (!twoFactorSuccess) {
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(twoFactorSuccess)));
Bot.BotDatabase.SaveToJsonStorage(MatchActivelyTradeOfferIDsStorageKey, JToken.FromObject(matchActivelyTradeOfferIDs));
}
return;
if (mobileTradeOfferIDs?.Count > 0) {
pendingMobileTradeOfferIDs.UnionWith(mobileTradeOfferIDs);
if (pendingMobileTradeOfferIDs.Count >= MaxTradeOffersActive) {
(bool twoFactorSuccess, IReadOnlyCollection<Confirmation>? handledConfirmations, _) = await Bot.Actions.HandleTwoFactorAuthenticationConfirmations(true, Confirmation.EType.Trade, pendingMobileTradeOfferIDs, true).ConfigureAwait(false);
if (!twoFactorSuccess) {
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Localization.Strings.ActivelyMatchingSomeConfirmationsFailed, handledConfirmations?.Count ?? 0, pendingMobileTradeOfferIDs.Count));
}
pendingMobileTradeOfferIDs.Clear();
}
}
if (!success) {
// The user likely no longer has the items we need, this is fine, we can continue the matching with other ones
failuresInRow++;
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Localization.Strings.TradeOfferFailed, listedUser.SteamID, listedUser.Nickname));
break;
}
failuresInRow = 0;
Bot.ArchiLogger.LogGenericInfo(Strings.Success);
// Assume the trade offer has went through and was accepted, this will allow us to keep matching the same set with different users as if we've got what we wanted
@@ -925,6 +1021,16 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
}
}
if (pendingMobileTradeOfferIDs.Count > 0) {
(bool twoFactorSuccess, IReadOnlyCollection<Confirmation>? handledConfirmations, _) = await Bot.Actions.HandleTwoFactorAuthenticationConfirmations(true, Confirmation.EType.Trade, pendingMobileTradeOfferIDs, true).ConfigureAwait(false);
if (!twoFactorSuccess) {
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Localization.Strings.ActivelyMatchingSomeConfirmationsFailed, handledConfirmations?.Count ?? 0, pendingMobileTradeOfferIDs.Count));
}
}
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Localization.Strings.ActivelyMatchingItemsRound, matchedSets));
return matchedSets > 0;
}
}

View File

@@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ConfigureAwaitChecker.Analyzer" PrivateAssets="all" />
<PackageReference Include="JetBrains.Annotations" PrivateAssets="all" />
<PackageReference Include="Newtonsoft.Json" IncludeAssets="compile" />
<PackageReference Include="SteamKit2" IncludeAssets="compile" />
<PackageReference Include="System.Composition.AttributedModel" IncludeAssets="compile" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net481' OR '$(TargetFramework)' == 'netstandard2.1'">
<!-- Madness is already included in netf build of ASF, so we don't need to emit it ourselves -->
<PackageReference Update="JustArchiNET.Madness" IncludeAssets="compile" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ArchiSteamFarm\ArchiSteamFarm.csproj" ExcludeAssets="all" Private="false" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Localization\Strings.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Strings.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Compile Update="Localization\Strings.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Strings.resx</DependentUpon>
<DesignTime>True</DesignTime>
</Compile>
</ItemGroup>
</Project>

View File

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

View File

@@ -0,0 +1,311 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Localization;
using ArchiSteamFarm.Steam;
using Newtonsoft.Json;
using SteamKit2;
using SteamKit2.Internal;
namespace ArchiSteamFarm.OfficialPlugins.MobileAuthenticator;
internal static class Commands {
private const byte MaxFinalizationAttempts = 900 / Steam.Security.MobileAuthenticator.CodeInterval;
internal static async Task<string?> OnBotCommand(Bot bot, EAccess access, string message, string[] args, ulong steamID = 0) {
ArgumentNullException.ThrowIfNull(bot);
if (!Enum.IsDefined(access)) {
throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess));
}
if (string.IsNullOrEmpty(message)) {
throw new ArgumentNullException(nameof(message));
}
if ((args == null) || (args.Length == 0)) {
throw new ArgumentNullException(nameof(args));
}
if ((steamID != 0) && !new SteamID(steamID).IsIndividualAccount) {
throw new ArgumentOutOfRangeException(nameof(steamID));
}
switch (args.Length) {
case 1:
switch (args[0].ToUpperInvariant()) {
case "2FAINIT":
return await ResponseTwoFactorInit(access, bot).ConfigureAwait(false);
}
break;
default:
switch (args[0].ToUpperInvariant()) {
case "2FAFINALIZE" when args.Length > 2:
return await ResponseTwoFactorFinalize(access, args[1], Utilities.GetArgsAsText(message, 2), steamID).ConfigureAwait(false);
case "2FAFINALIZE":
return await ResponseTwoFactorFinalize(access, bot, args[1]).ConfigureAwait(false);
case "2FAINIT":
return await ResponseTwoFactorInit(access, Utilities.GetArgsAsText(args, 1, ","), steamID).ConfigureAwait(false);
}
break;
}
return null;
}
private static async Task<string?> ResponseTwoFactorFinalize(EAccess access, Bot bot, string activationCode) {
if (!Enum.IsDefined(access)) {
throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess));
}
ArgumentNullException.ThrowIfNull(bot);
if (string.IsNullOrEmpty(activationCode)) {
throw new ArgumentNullException(nameof(activationCode));
}
if (access < EAccess.Master) {
return access > EAccess.None ? bot.Commands.FormatBotResponse(Strings.ErrorAccessDenied) : null;
}
if (bot.HasMobileAuthenticator) {
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(bot.HasMobileAuthenticator)));
}
if (!bot.IsConnectedAndLoggedOn) {
return bot.Commands.FormatBotResponse(Strings.BotNotConnected);
}
string maFilePath = bot.GetFilePath(Bot.EFileType.MobileAuthenticator);
string maFilePendingPath = $"{maFilePath}.PENDING";
if (!File.Exists(maFilePendingPath)) {
return bot.Commands.FormatBotResponse(Strings.NothingFound);
}
string json;
try {
json = await File.ReadAllTextAsync(maFilePendingPath).ConfigureAwait(false);
} catch (Exception e) {
bot.ArchiLogger.LogGenericException(e);
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, e.Message));
}
if (string.IsNullOrEmpty(json)) {
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(json)));
}
Steam.Security.MobileAuthenticator? mobileAuthenticator = JsonConvert.DeserializeObject<Steam.Security.MobileAuthenticator>(json);
if (mobileAuthenticator == null) {
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(json)));
}
mobileAuthenticator.Init(bot);
MobileAuthenticatorHandler? mobileAuthenticatorHandler = bot.GetHandler<MobileAuthenticatorHandler>();
if (mobileAuthenticatorHandler == null) {
throw new InvalidOperationException(nameof(mobileAuthenticatorHandler));
}
ulong steamTime = await mobileAuthenticator.GetSteamTime().ConfigureAwait(false);
bool successFinalizing = false;
for (byte i = 0; i < MaxFinalizationAttempts; i++) {
if (i > 0) {
steamTime += Steam.Security.MobileAuthenticator.CodeInterval;
}
string? code = mobileAuthenticator.GenerateTokenForTime(steamTime);
if (string.IsNullOrEmpty(code)) {
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(mobileAuthenticator.GenerateTokenForTime)));
}
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
CTwoFactor_FinalizeAddAuthenticator_Response? response = await mobileAuthenticatorHandler.FinalizeAuthenticator(bot.SteamID, activationCode, code!, steamTime).ConfigureAwait(false);
if (response == null) {
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(mobileAuthenticatorHandler.FinalizeAuthenticator)));
}
if (response.want_more) {
// OK, whatever
continue;
}
if (!response.success) {
EResult result = (EResult) response.status;
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, result));
}
successFinalizing = true;
break;
}
if (!successFinalizing) {
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, MaxFinalizationAttempts));
}
if (!bot.TryImportAuthenticator(mobileAuthenticator)) {
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(bot.TryImportAuthenticator)));
}
string maFileFinishedPath = $"{maFilePath}.NEW";
try {
File.Move(maFilePendingPath, maFileFinishedPath, true);
} catch (Exception e) {
bot.ArchiLogger.LogGenericException(e);
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, e.Message));
}
return bot.Commands.FormatBotResponse(Strings.Done);
}
private static async Task<string?> ResponseTwoFactorFinalize(EAccess access, string botNames, string activationCode, ulong steamID = 0) {
if (!Enum.IsDefined(access)) {
throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess));
}
if (string.IsNullOrEmpty(botNames)) {
throw new ArgumentNullException(nameof(botNames));
}
if (string.IsNullOrEmpty(activationCode)) {
throw new ArgumentNullException(nameof(activationCode));
}
if ((steamID != 0) && !new SteamID(steamID).IsIndividualAccount) {
throw new ArgumentOutOfRangeException(nameof(steamID));
}
HashSet<Bot>? bots = Bot.GetBots(botNames);
if ((bots == null) || (bots.Count == 0)) {
return access >= EAccess.Owner ? Steam.Interaction.Commands.FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => ResponseTwoFactorFinalize(Steam.Interaction.Commands.GetProxyAccess(bot, access, steamID), bot, activationCode))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null;
}
private static async Task<string?> ResponseTwoFactorInit(EAccess access, Bot bot) {
if (!Enum.IsDefined(access)) {
throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess));
}
ArgumentNullException.ThrowIfNull(bot);
if (access < EAccess.Master) {
return access > EAccess.None ? bot.Commands.FormatBotResponse(Strings.ErrorAccessDenied) : null;
}
if (bot.HasMobileAuthenticator) {
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(bot.HasMobileAuthenticator)));
}
if (!bot.IsConnectedAndLoggedOn) {
return bot.Commands.FormatBotResponse(Strings.BotNotConnected);
}
MobileAuthenticatorHandler? mobileAuthenticatorHandler = bot.GetHandler<MobileAuthenticatorHandler>();
if (mobileAuthenticatorHandler == null) {
throw new InvalidOperationException(nameof(mobileAuthenticatorHandler));
}
string deviceID = $"android:{Guid.NewGuid()}";
CTwoFactor_AddAuthenticator_Response? response = await mobileAuthenticatorHandler.AddAuthenticator(bot.SteamID, deviceID).ConfigureAwait(false);
if (response == null) {
return bot.Commands.FormatBotResponse(Strings.WarningFailed);
}
EResult result = (EResult) response.status;
if (result != EResult.OK) {
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, result));
}
MaFileData maFileData = new(response, deviceID);
string maFilePendingPath = $"{bot.GetFilePath(Bot.EFileType.MobileAuthenticator)}.PENDING";
string json = JsonConvert.SerializeObject(maFileData, Formatting.Indented);
try {
await File.WriteAllTextAsync(maFilePendingPath, json).ConfigureAwait(false);
} catch (Exception e) {
bot.ArchiLogger.LogGenericException(e);
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, e.Message));
}
return bot.Commands.FormatBotResponse(Strings.Done);
}
private static async Task<string?> ResponseTwoFactorInit(EAccess access, string botNames, ulong steamID = 0) {
if (!Enum.IsDefined(access)) {
throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess));
}
if (string.IsNullOrEmpty(botNames)) {
throw new ArgumentNullException(nameof(botNames));
}
if ((steamID != 0) && !new SteamID(steamID).IsIndividualAccount) {
throw new ArgumentOutOfRangeException(nameof(steamID));
}
HashSet<Bot>? bots = Bot.GetBots(botNames);
if ((bots == null) || (bots.Count == 0)) {
return access >= EAccess.Owner ? Steam.Interaction.Commands.FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null;
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => ResponseTwoFactorInit(Steam.Interaction.Commands.GetProxyAccess(bot, access, steamID), bot))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root" xmlns="">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
</root>

View File

@@ -0,0 +1,81 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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;
using Newtonsoft.Json;
using SteamKit2.Internal;
namespace ArchiSteamFarm.OfficialPlugins.MobileAuthenticator;
internal sealed class MaFileData {
[JsonProperty("account_name", Required = Required.Always)]
internal readonly string AccountName;
[JsonProperty("device_id", Required = Required.Always)]
internal readonly string DeviceID;
[JsonProperty("identity_secret", Required = Required.Always)]
internal readonly string IdentitySecret;
[JsonProperty("revocation_code", Required = Required.Always)]
internal readonly string RevocationCode;
[JsonProperty("secret_1", Required = Required.Always)]
internal readonly string Secret1;
[JsonProperty("serial_number", Required = Required.Always)]
internal readonly ulong SerialNumber;
[JsonProperty("server_time", Required = Required.Always)]
internal readonly ulong ServerTime;
[JsonProperty("shared_secret", Required = Required.Always)]
internal readonly string SharedSecret;
[JsonProperty("status", Required = Required.Always)]
internal readonly int Status;
[JsonProperty("token_gid", Required = Required.Always)]
internal readonly string TokenGid;
[JsonProperty("uri", Required = Required.Always)]
internal readonly string Uri;
internal MaFileData(CTwoFactor_AddAuthenticator_Response data, string deviceID) {
ArgumentNullException.ThrowIfNull(data);
if (string.IsNullOrEmpty(deviceID)) {
throw new ArgumentNullException(nameof(deviceID));
}
AccountName = data.account_name;
DeviceID = deviceID;
IdentitySecret = Convert.ToBase64String(data.identity_secret);
RevocationCode = data.revocation_code;
Secret1 = Convert.ToBase64String(data.secret_1);
SerialNumber = data.serial_number;
ServerTime = data.server_time;
SharedSecret = Convert.ToBase64String(data.shared_secret);
Status = data.status;
TokenGid = data.token_gid;
Uri = data.uri;
}
}

View File

@@ -0,0 +1,138 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.NLog;
using SteamKit2;
using SteamKit2.Internal;
namespace ArchiSteamFarm.OfficialPlugins.MobileAuthenticator;
internal sealed class MobileAuthenticatorHandler : ClientMsgHandler {
private readonly ArchiLogger ArchiLogger;
private readonly SteamUnifiedMessages.UnifiedService<ITwoFactor> UnifiedTwoFactorService;
internal MobileAuthenticatorHandler(ArchiLogger archiLogger, SteamUnifiedMessages steamUnifiedMessages) {
ArgumentNullException.ThrowIfNull(steamUnifiedMessages);
ArchiLogger = archiLogger ?? throw new ArgumentNullException(nameof(archiLogger));
UnifiedTwoFactorService = steamUnifiedMessages.CreateService<ITwoFactor>();
}
public override void HandleMsg(IPacketMsg packetMsg) => ArgumentNullException.ThrowIfNull(packetMsg);
internal async Task<CTwoFactor_AddAuthenticator_Response?> AddAuthenticator(ulong steamID, string deviceID) {
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
throw new ArgumentOutOfRangeException(nameof(steamID));
}
if (string.IsNullOrEmpty(deviceID)) {
throw new ArgumentNullException(nameof(deviceID));
}
if (Client == null) {
throw new InvalidOperationException(nameof(Client));
}
if (!Client.IsConnected) {
return null;
}
CTwoFactor_AddAuthenticator_Request request = new() {
authenticator_type = 1,
authenticator_time = Utilities.GetUnixTime(),
device_identifier = deviceID,
sms_phone_id = "1",
steamid = steamID
};
SteamUnifiedMessages.ServiceMethodResponse response;
try {
response = await UnifiedTwoFactorService.SendMessage(x => x.AddAuthenticator(request)).ToLongRunningTask().ConfigureAwait(false);
} catch (Exception e) {
ArchiLogger.LogGenericWarningException(e);
return null;
}
if (response.Result != EResult.OK) {
return null;
}
CTwoFactor_AddAuthenticator_Response body = response.GetDeserializedResponse<CTwoFactor_AddAuthenticator_Response>();
return body;
}
internal async Task<CTwoFactor_FinalizeAddAuthenticator_Response?> FinalizeAuthenticator(ulong steamID, string activationCode, string authenticatorCode, ulong authenticatorTime) {
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
throw new ArgumentOutOfRangeException(nameof(steamID));
}
if (string.IsNullOrEmpty(activationCode)) {
throw new ArgumentNullException(nameof(activationCode));
}
if (string.IsNullOrEmpty(authenticatorCode)) {
throw new ArgumentNullException(nameof(authenticatorCode));
}
if (authenticatorTime <= 0) {
throw new ArgumentOutOfRangeException(nameof(authenticatorTime));
}
if (Client == null) {
throw new InvalidOperationException(nameof(Client));
}
if (!Client.IsConnected) {
return null;
}
CTwoFactor_FinalizeAddAuthenticator_Request request = new() {
activation_code = activationCode,
authenticator_code = authenticatorCode,
authenticator_time = authenticatorTime,
steamid = steamID
};
SteamUnifiedMessages.ServiceMethodResponse response;
try {
response = await UnifiedTwoFactorService.SendMessage(x => x.FinalizeAddAuthenticator(request)).ToLongRunningTask().ConfigureAwait(false);
} catch (Exception e) {
ArchiLogger.LogGenericWarningException(e);
return null;
}
if (response.Result != EResult.OK) {
return null;
}
CTwoFactor_FinalizeAddAuthenticator_Response body = response.GetDeserializedResponse<CTwoFactor_FinalizeAddAuthenticator_Response>();
return body;
}
}

View File

@@ -0,0 +1,91 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// 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;
using System.Collections.Generic;
using System.ComponentModel;
using System.Composition;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.OfficialPlugins.MobileAuthenticator.Localization;
using ArchiSteamFarm.Plugins;
using ArchiSteamFarm.Plugins.Interfaces;
using ArchiSteamFarm.Steam;
using Newtonsoft.Json;
using SteamKit2;
namespace ArchiSteamFarm.OfficialPlugins.MobileAuthenticator;
[Export(typeof(IPlugin))]
internal sealed class MobileAuthenticatorPlugin : OfficialPlugin, IBotCommand2, IBotSteamClient {
[JsonProperty]
public override string Name => nameof(MobileAuthenticatorPlugin);
[JsonProperty]
public override Version Version => typeof(MobileAuthenticatorPlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version));
public async Task<string?> OnBotCommand(Bot bot, EAccess access, string message, string[] args, ulong steamID = 0) {
ArgumentNullException.ThrowIfNull(bot);
if (!Enum.IsDefined(access)) {
throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess));
}
if (string.IsNullOrEmpty(message)) {
throw new ArgumentNullException(nameof(message));
}
if ((args == null) || (args.Length == 0)) {
throw new ArgumentNullException(nameof(args));
}
if ((steamID != 0) && !new SteamID(steamID).IsIndividualAccount) {
throw new ArgumentOutOfRangeException(nameof(steamID));
}
return await Commands.OnBotCommand(bot, access, message, args, steamID).ConfigureAwait(false);
}
public Task OnBotSteamCallbacksInit(Bot bot, CallbackManager callbackManager) {
ArgumentNullException.ThrowIfNull(bot);
ArgumentNullException.ThrowIfNull(callbackManager);
return Task.CompletedTask;
}
public Task<IReadOnlyCollection<ClientMsgHandler>?> OnBotSteamHandlersInit(Bot bot) {
ArgumentNullException.ThrowIfNull(bot);
SteamUnifiedMessages? steamUnifiedMessages = bot.GetHandler<SteamUnifiedMessages>();
if (steamUnifiedMessages == null) {
throw new InvalidOperationException(nameof(steamUnifiedMessages));
}
return Task.FromResult<IReadOnlyCollection<ClientMsgHandler>?>(new HashSet<ClientMsgHandler>(1) { new MobileAuthenticatorHandler(bot.ArchiLogger, steamUnifiedMessages) });
}
public override Task OnLoaded() {
Utilities.WarnAboutIncompleteTranslation(Strings.ResourceManager);
return Task.CompletedTask;
}
}

View File

@@ -12,7 +12,7 @@
<PackageReference Include="System.Composition.AttributedModel" IncludeAssets="compile" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net481'">
<ItemGroup Condition="'$(TargetFramework)' == 'net481' OR '$(TargetFramework)' == 'netstandard2.1'">
<!-- Madness is already included in netf build of ASF, so we don't need to emit it ourselves -->
<PackageReference Update="JustArchiNET.Madness" IncludeAssets="compile" />
</ItemGroup>

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -22,6 +22,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -29,6 +30,7 @@ using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Helpers;
using ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.Localization;
using ArchiSteamFarm.Web.Responses;
using JetBrains.Annotations;
using Newtonsoft.Json;
using SteamKit2;
@@ -36,6 +38,8 @@ using SteamKit2;
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper;
internal sealed class GlobalCache : SerializableFile {
internal static readonly ArchiCacheable<ImmutableHashSet<uint>> KnownDepotIDs = new(ResolveKnownDepotIDs, TimeSpan.FromDays(7));
private static string SharedFilePath => Path.Combine(ArchiSteamFarm.SharedInfo.ConfigDirectory, $"{nameof(SteamTokenDumper)}.cache");
[JsonProperty(Required = Required.DisallowNull)]
@@ -237,35 +241,28 @@ internal sealed class GlobalCache : SerializableFile {
}
}
internal void UpdateDepotKeys(ICollection<SteamApps.DepotKeyCallback> depotKeyResults) {
ArgumentNullException.ThrowIfNull(depotKeyResults);
internal void UpdateDepotKey(SteamApps.DepotKeyCallback depotKeyResult) {
ArgumentNullException.ThrowIfNull(depotKeyResult);
bool save = false;
foreach (SteamApps.DepotKeyCallback depotKeyResult in depotKeyResults) {
if (depotKeyResult.Result != EResult.OK) {
continue;
}
string depotKey = Convert.ToHexString(depotKeyResult.DepotKey);
if (!IsValidDepotKey(depotKey)) {
ASF.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.ErrorIsInvalid, nameof(depotKey)));
continue;
}
if (DepotKeys.TryGetValue(depotKeyResult.DepotID, out string? previousDepotKey) && (previousDepotKey == depotKey)) {
continue;
}
DepotKeys[depotKeyResult.DepotID] = depotKey;
save = true;
if (depotKeyResult.Result != EResult.OK) {
return;
}
if (save) {
Utilities.InBackground(Save);
string depotKey = Convert.ToHexString(depotKeyResult.DepotKey);
if (!IsValidDepotKey(depotKey)) {
ASF.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.ErrorIsInvalid, nameof(depotKey)));
return;
}
if (DepotKeys.TryGetValue(depotKeyResult.DepotID, out string? previousDepotKey) && (previousDepotKey == depotKey)) {
return;
}
DepotKeys[depotKeyResult.DepotID] = depotKey;
Utilities.InBackground(Save);
}
internal void UpdatePackageTokens(IReadOnlyCollection<KeyValuePair<uint, ulong>> packageTokens) {
@@ -333,4 +330,50 @@ internal sealed class GlobalCache : SerializableFile {
return (depotKey.Length == 64) && Utilities.IsValidHexadecimalText(depotKey);
}
private static async Task<(bool Success, ImmutableHashSet<uint>? Result)> ResolveKnownDepotIDs() {
if (ASF.WebBrowser == null) {
throw new InvalidOperationException(nameof(ASF.WebBrowser));
}
Uri request = new($"{SharedInfo.ServerURL}/knowndepots.csv");
StreamResponse? response = await ASF.WebBrowser.UrlGetToStream(request).ConfigureAwait(false);
if (response?.Content == null) {
return (false, null);
}
await using (response.ConfigureAwait(false)) {
try {
using StreamReader reader = new(response.Content);
string? countText = await reader.ReadLineAsync().ConfigureAwait(false);
if (string.IsNullOrEmpty(countText) || !int.TryParse(countText, out int count) || (count <= 0)) {
ASF.ArchiLogger.LogNullError(nameof(countText));
return (false, null);
}
HashSet<uint> result = new(count);
while (await reader.ReadLineAsync().ConfigureAwait(false) is { Length: > 0 } line) {
if (!uint.TryParse(line, out uint depotID) || (depotID == 0)) {
ASF.ArchiLogger.LogNullError(nameof(depotID));
continue;
}
result.Add(depotID);
}
return (result.Count > 0, result.ToImmutableHashSet());
} catch (Exception e) {
ASF.ArchiLogger.LogGenericWarningException(e);
return (false, null);
}
}
}
}

View File

@@ -4,7 +4,7 @@
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -117,12 +117,6 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.Localization {
}
}
internal static string BotRetrievingDepotKeys {
get {
return ResourceManager.GetString("BotRetrievingDepotKeys", resourceCulture);
}
}
internal static string BotFinishedRetrievingDepotKeys {
get {
return ResourceManager.GetString("BotFinishedRetrievingDepotKeys", resourceCulture);

View File

@@ -109,14 +109,7 @@
<value>Завершана атрыманне інфармацыі пра праграму {0}.</value>
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
</data>
<data name="BotRetrievingDepotKeys" xml:space="preserve">
<value>Атрыманне {0} ключоў сховішча...</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
</data>
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
<value>Завершана атрыманне {0} ключоў сховішча.</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
</data>
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
<value>Завершана атрыманне ўсіх ключоў ключоў сховішча для {0} праграм.</value>
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>

View File

@@ -109,14 +109,7 @@
<value>Приключи събирането на {0} информация за играта или приложението.</value>
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
</data>
<data name="BotRetrievingDepotKeys" xml:space="preserve">
<value>Събиране {0} ключове за депо...</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
</data>
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
<value>Приключи събирането {0} ключове за депо.</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
</data>
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
<value>Приключи събирането на всички ключове за депа за общо {0} игри или проложения.</value>
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>

View File

@@ -109,13 +109,9 @@
<value>Načítání informací o aplikaci {0} bylo dokončeno.</value>
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
</data>
<data name="BotRetrievingDepotKeys" xml:space="preserve">
<value>Získávání {0} tokenů úložišť...</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
</data>
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
<value>Načítání {0} přístupových tokenů bylo dokončeno.</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
<value>Úspěšně načteno {0} z {1} depot klíčů.</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>Načítání všech tokenbů úložišť, celkem z {0} aplikací bylo dokončeno.</value>

View File

@@ -109,13 +109,9 @@
<value>Abruf von {0} App-Infos abgeschlossen.</value>
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
</data>
<data name="BotRetrievingDepotKeys" xml:space="preserve">
<value>Rufe {0} Depotschlüssel ab...</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
</data>
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
<value>Abruf von {0} Depotschlüsseln abgeschlossen.</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
<value>Erfolgreich {0} von {1} Depot-Schlüsseln abgerufen.</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>Abruf aller Depotschlüssel für insgesamt {0} Apps abgeschlossen.</value>

View File

@@ -109,14 +109,7 @@
<value>Ολοκληρώθηκε η ανάκτηση {0} πληροφοριών εφαρμογών.</value>
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
</data>
<data name="BotRetrievingDepotKeys" xml:space="preserve">
<value>Ανάκτηση {0} κλειδιών αποθήκης...</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
</data>
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
<value>Ολοκληρώθηκε η ανάκτηση {0} κλειδιών αποθήκευσης.</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
</data>
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
<value>Ολοκληρώθηκε η ανάκτηση όλων των κλειδιών αποθηκών για συνολικά {0} εφαρμογές.</value>
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>

View File

@@ -109,13 +109,9 @@
<value>Se han recuperado {0} datos de aplicación.</value>
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
</data>
<data name="BotRetrievingDepotKeys" xml:space="preserve">
<value>Recuperando {0} claves de depósito...</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
</data>
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
<value>Se han recuperado {0} claves de depósito.</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
<value>Se recuperaron exitosamente {0} de {1} claves de depósito.</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>Se han recuperado todas las claves de depósito para un total de {0} aplicaciones.</value>

View File

@@ -109,14 +109,7 @@
<value>Saatiin haettua {0} sovelluksen tiedot.</value>
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
</data>
<data name="BotRetrievingDepotKeys" xml:space="preserve">
<value>Haetaan {0} depot-avainta...</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
</data>
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
<value>Saatiin haettua {0} depot-avainta.</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
</data>
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
<value>Saatiin haettua kaikki depot-avaimet yhteensä {0} sovellukselle.</value>
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>

View File

@@ -109,14 +109,7 @@
<value>Récupération de {0} infos d'application terminée.</value>
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
</data>
<data name="BotRetrievingDepotKeys" xml:space="preserve">
<value>Récupération de {0} clés de dépôts...</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
</data>
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
<value>Récupération de {0} clés de dépôts terminée.</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
</data>
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
<value>Fin de la récupération de toutes les clés de dépôts pour un total de {0} applications.</value>
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>

View File

@@ -86,14 +86,7 @@
<data name="BotRetrievingDepotKeys" xml:space="preserve">
<value>מאחזר {0} מפתחות מחסן...</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
</data>
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
<value>סיים לאחזר {0} מפתחות מחסן.</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
</data>
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
<value>סיים לאחזר את כל מפתחות המחסן עבור סך של {0} אפליקציות.</value>
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>

View File

@@ -108,7 +108,6 @@
</data>
<data name="SubmissionNoNewData" xml:space="preserve">
<value>Nincs új beküldendő adat, minden naprakész.</value>
</data>

View File

@@ -109,14 +109,7 @@
<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="BotRetrievingDepotKeys" xml:space="preserve">
<value>Recupero {0} chiavi...</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
</data>
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
<value>Completato il recupero di {0} chiavi.</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
</data>
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
<value>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

@@ -88,7 +88,6 @@
</data>
<data name="SubmissionNoNewData" xml:space="preserve">
<value>새로 등록할 데이터가 없습니다.</value>
</data>

View File

@@ -105,7 +105,6 @@
</data>
<data name="SubmissionNoNewData" xml:space="preserve">
<value>Nėra jokių naujų duomenų, kuriuos būtų galima pateikti, viskas jau atnaujinta.</value>
</data>

View File

@@ -81,7 +81,6 @@
<data name="SubmissionSuccessfulNewApps" xml:space="preserve">
<value>Jaunas aplikācijas: {0}</value>
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>

View File

@@ -62,30 +62,79 @@
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
<data name="PluginDisabledMissingBuildToken" xml:space="preserve">
<value>{0} is uitgeschakeld vanwege een ontbrekende build-token</value>
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
</data>
<data name="PluginDisabledInConfig" xml:space="preserve">
<value>{0} is momenteel uitgeschakeld volgens uw configuratie. Als je SteamDB wilt helpen bij het indienen van gegevens, bekijk dan onze wiki.</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} is succesvol geïnitialiseerd. Alvast bedankt voor uw hulp. De eerste inzending vindt vanaf nu plaats in ongeveer {1}.</value>
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin"), {1} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
</data>
<data name="FileCouldNotBeLoadedFreshInit" xml:space="preserve">
<value>{0} kon niet worden geladen, een nieuwe instantie zal worden geïnitialiseerd...</value>
<comment>{0} will be replaced by the name of the file (e.g. "GlobalCache")</comment>
</data>
<data name="BotNoAppsToRefresh" xml:space="preserve">
<value>Er zijn geen apps die een vernieuwing vereisen op deze bot instantie.</value>
</data>
<data name="BotRetrievingTotalAppAccessTokens" xml:space="preserve">
<value>In totaal {0} app-toegangstokens ophalen...</value>
<comment>{0} will be replaced by the number (total count) of app access tokens being retrieved</comment>
</data>
<data name="BotRetrievingAppAccessTokens" xml:space="preserve">
<value>{0} app-toegangstokens ophalen...</value>
<comment>{0} will be replaced by the number (count this batch) of app access tokens being retrieved</comment>
</data>
<data name="BotFinishedRetrievingAppAccessTokens" xml:space="preserve">
<value>Ophalen van {0} app-toegangstokens voltooid.</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>Het ophalen van in totaal {0} app-toegangstokens is voltooid.</value>
<comment>{0} will be replaced by the number (total count) of app access tokens retrieved</comment>
</data>
<data name="BotRetrievingTotalDepots" xml:space="preserve">
<value>Alle depots ophalen voor een totaal van {0} apps...</value>
<comment>{0} will be replaced by the number (total count) of apps being retrieved</comment>
</data>
<data name="BotRetrievingAppInfos" xml:space="preserve">
<value>{0} app-infos ophalen...</value>
<comment>{0} will be replaced by the number (count this batch) of app infos being retrieved</comment>
</data>
<data name="BotFinishedRetrievingAppInfos" xml:space="preserve">
<value>Ophalen van {0} app-infos voltooid.</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>Succesvol {0} van de {1} depotsleutels opgehaald.</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="SubmissionSuccessfulNewApps" xml:space="preserve">
<value>Nieuwe apps: {0}</value>
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
</data>
<data name="SubmissionSuccessfulVerifiedApps" xml:space="preserve">
<value>Geverifieerde apps: {0}</value>
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
</data>
<data name="SubmissionSuccessfulNewPackages" xml:space="preserve">
<value>Nieuwe pakketten: {0}</value>
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
</data>
<data name="SubmissionSuccessfulVerifiedPackages" xml:space="preserve">
<value>Geverifieerde pakketten: {0}</value>
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
</data>

View File

@@ -109,13 +109,9 @@
<value>Zakończono pobieranie {0} informacji o aplikacji.</value>
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
</data>
<data name="BotRetrievingDepotKeys" xml:space="preserve">
<value>Pobieranie {0} kluczy magazynu...</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
</data>
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
<value>Zakończono pobieranie {0} kluczy magazynu.</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
<value>Pomyślnie pobrano {0} z {1} kluczy magazynu.</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>Zakończono pobieranie wszystkich kluczy magazynu dla {0} aplikacji.</value>

View File

@@ -109,13 +109,9 @@
<value>Recuperamos um total de {0} informações de aplicativos.</value>
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
</data>
<data name="BotRetrievingDepotKeys" xml:space="preserve">
<value>Recuperando {0} códigos de depots...</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
</data>
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
<value>Recuperamos códigos para {0} depots.</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
<value>{0} de {1} chaves do depósito recuperadas com sucesso.</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>Recuperamos códigos de acesso para {0} aplicativos.</value>

View File

@@ -109,13 +109,9 @@
<value>FINISHD RETRIEVIN {0} APP INFOS.</value>
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
</data>
<data name="BotRetrievingDepotKeys" xml:space="preserve">
<value>RETRIEVIN {0} DEPOT KEYS...</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
</data>
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
<value>FINISHD RETRIEVIN {0} DEPOT KEYS.</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
<value>SUCCESFULLY RETRIEVD {0} OUT OV {1} DEPOT KEYS.</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>FINISHD RETRIEVIN ALL DEPOT KEYS 4 TOTAL OV {0} APPS.</value>

View File

@@ -109,13 +109,9 @@
<value>Finished retrieving {0} app infos.</value>
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
</data>
<data name="BotRetrievingDepotKeys" xml:space="preserve">
<value>Retrieving {0} depot keys...</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
</data>
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
<value>Finished retrieving {0} depot keys.</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
<value>Successfully retrieved {0} out of {1} depot keys.</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>Finished retrieving all depot keys for a total of {0} apps.</value>

View File

@@ -109,13 +109,9 @@
<value>Завершено получение информации {0} приложений.</value>
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
</data>
<data name="BotRetrievingDepotKeys" xml:space="preserve">
<value>Получение {0} ключей хранилища...</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
</data>
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
<value>Получение {0} ключей хранилища завершено.</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
<value>Успешно извлечено {0} ключей хранилища из {1}.</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>Получение всех ключей хранилища {0} приложений завершено.</value>

View File

@@ -109,14 +109,7 @@
<value>Dokončené získavanie informácií {0} aplikácií.</value>
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
</data>
<data name="BotRetrievingDepotKeys" xml:space="preserve">
<value>Získavam {0} kľúčov položiek...</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
</data>
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
<value>Dokončené získavanie {0} kľúčov položiek.</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
</data>
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
<value>Dokončené získanie všetkých kľúčov položiek {0} aplikácií.</value>
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>

View File

@@ -109,13 +109,9 @@
<value>{0} uygulama bilgisinin alınması tamamlandı.</value>
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
</data>
<data name="BotRetrievingDepotKeys" xml:space="preserve">
<value>{0} depo anahtarı alınıyor...</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
</data>
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
<value>{0} depo anahtarının alınması tamamlandı.</value>
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
<value>{0}/{1} depo anahtarı başarıyla alındı.</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>Toplam {0} uygulama için tüm depo anahtarlarının alınması tamamlandı.</value>

Some files were not shown because too many files have changed in this diff Show More