mirror of
https://github.com/JustArchiNET/ArchiSteamFarm.git
synced 2025-12-16 06:20:34 +00:00
Compare commits
161 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e326cc94d0 | ||
|
|
66237dab24 | ||
|
|
53ec07a7f1 | ||
|
|
bbad80ae0e | ||
|
|
e6021a4f11 | ||
|
|
b3f792a2f3 | ||
|
|
7083a397b8 | ||
|
|
f98fd85fe9 | ||
|
|
f9f6e207d4 | ||
|
|
0c4e4e709f | ||
|
|
d7f9ad4916 | ||
|
|
b04f92ca3c | ||
|
|
16a0174cf1 | ||
|
|
c92d78e088 | ||
|
|
1f7b8d2416 | ||
|
|
7ca9b23aa3 | ||
|
|
f0acd8ddfd | ||
|
|
c5009dad31 | ||
|
|
9682d1d5ef | ||
|
|
a4e8fca784 | ||
|
|
8f25040bf5 | ||
|
|
34beec8d6b | ||
|
|
6c0fd638a5 | ||
|
|
0b94213af7 | ||
|
|
f874598fd8 | ||
|
|
a1adcd3a5a | ||
|
|
1596b98e25 | ||
|
|
9b0ad5c545 | ||
|
|
6a47b30987 | ||
|
|
79594a9d00 | ||
|
|
38a9818482 | ||
|
|
a407d538f5 | ||
|
|
44bea85296 | ||
|
|
915d26adfa | ||
|
|
3dd4248587 | ||
|
|
7969e58fbf | ||
|
|
7043ebda23 | ||
|
|
4c0a5b7553 | ||
|
|
533fbe0c2f | ||
|
|
437dfd5f02 | ||
|
|
5f4962ddcc | ||
|
|
7efa609a13 | ||
|
|
0d7049ea7c | ||
|
|
997e7f0420 | ||
|
|
96619b9565 | ||
|
|
4c2a786e54 | ||
|
|
469b1571e1 | ||
|
|
3bc66e0d27 | ||
|
|
9a4fac600a | ||
|
|
546ba8b68f | ||
|
|
ed29c0633f | ||
|
|
35b1135b50 | ||
|
|
90403c98c9 | ||
|
|
71a1c0574a | ||
|
|
ba64a50bb5 | ||
|
|
05131b2b76 | ||
|
|
0894eaee28 | ||
|
|
6f2fd4eccc | ||
|
|
04b534bda1 | ||
|
|
3620796d6d | ||
|
|
a321a38ccd | ||
|
|
ea965dfa85 | ||
|
|
f381106de2 | ||
|
|
b9ab3d6490 | ||
|
|
4cf1d1e08b | ||
|
|
cba6e4df64 | ||
|
|
0dd6f38748 | ||
|
|
970ba437a0 | ||
|
|
e0bbbe3894 | ||
|
|
504791b5b6 | ||
|
|
6a6d0b48b9 | ||
|
|
84ff83bbe2 | ||
|
|
787bcc3546 | ||
|
|
fd811d8cf4 | ||
|
|
3fa743f64b | ||
|
|
ec374c050a | ||
|
|
5a07f8a2a3 | ||
|
|
91b09dc43f | ||
|
|
8d6d355a3b | ||
|
|
da6fd69398 | ||
|
|
0e623cfd15 | ||
|
|
1c01d8f59f | ||
|
|
5723ee7b19 | ||
|
|
1d85292451 | ||
|
|
0d65f5174b | ||
|
|
b7f34f0d5d | ||
|
|
4ffcea72b0 | ||
|
|
9c5fade596 | ||
|
|
331ecc1cc8 | ||
|
|
6428e5abd1 | ||
|
|
b86f83a634 | ||
|
|
ff55e09783 | ||
|
|
d7d24d5e47 | ||
|
|
48a14136a9 | ||
|
|
c9acbb7bf2 | ||
|
|
f98a159799 | ||
|
|
184232995d | ||
|
|
aedede3ba4 | ||
|
|
c874779c0d | ||
|
|
4a2457d571 | ||
|
|
65fdd4196a | ||
|
|
19eefff525 | ||
|
|
626c18ba23 | ||
|
|
d9c8dc1e2d | ||
|
|
25fa5cabbb | ||
|
|
21c9dac593 | ||
|
|
617dccbd9a | ||
|
|
2f2665e2ad | ||
|
|
fcc0d70cd1 | ||
|
|
06b2cf4ff5 | ||
|
|
8642b0775e | ||
|
|
c193858973 | ||
|
|
362e921a27 | ||
|
|
e21fa45718 | ||
|
|
caae270ea8 | ||
|
|
4843d539f0 | ||
|
|
fa8e649288 | ||
|
|
20708ad900 | ||
|
|
5e0b9551da | ||
|
|
e43adf4de7 | ||
|
|
afe16b8eb1 | ||
|
|
e9f9663714 | ||
|
|
d514157903 | ||
|
|
56490f2d86 | ||
|
|
1fd6c8a477 | ||
|
|
e7bdd408be | ||
|
|
f73b4737b6 | ||
|
|
14c905b8ef | ||
|
|
fa7849460c | ||
|
|
4d12c9117f | ||
|
|
e2f08fe35b | ||
|
|
0089a87018 | ||
|
|
6325c454bc | ||
|
|
6b1f64579a | ||
|
|
14cfd61615 | ||
|
|
f2a8768f80 | ||
|
|
556f3fdac0 | ||
|
|
59124fcf68 | ||
|
|
71643446db | ||
|
|
a93ca58e9c | ||
|
|
f12e87c2f4 | ||
|
|
d48c96604b | ||
|
|
0976bbfd2d | ||
|
|
6f66518607 | ||
|
|
e4c20df4a8 | ||
|
|
5135677360 | ||
|
|
f6004f558b | ||
|
|
ddf08c4dc0 | ||
|
|
388eaf614d | ||
|
|
3a0768f9ef | ||
|
|
cb0e04c022 | ||
|
|
55cdac205d | ||
|
|
1bc0d6f16c | ||
|
|
f7377a7043 | ||
|
|
76f1ad45dd | ||
|
|
f21ffde803 | ||
|
|
e9c96f175f | ||
|
|
0f9a4c7c31 | ||
|
|
87451615e8 | ||
|
|
fa19aaae2e | ||
|
|
6b8280fceb |
1
.github/ISSUE_TEMPLATE/Bug-report.yml
vendored
1
.github/ISSUE_TEMPLATE/Bug-report.yml
vendored
@@ -149,6 +149,7 @@ body:
|
||||
|
||||
Ensure that your config has redacted (but NOT removed) potentially-sensitive properties, such as:
|
||||
- IPCPassword (recommended)
|
||||
- LicenseID (mandatory)
|
||||
- SteamOwnerID (optionally)
|
||||
- WebProxy (optionally, if exposing private details)
|
||||
- WebProxyPassword (optionally, if exposing private details)
|
||||
|
||||
2
.github/SUPPORT.md
vendored
2
.github/SUPPORT.md
vendored
@@ -2,6 +2,6 @@
|
||||
|
||||
Our **[wiki](https://github.com/JustArchiNET/ArchiSteamFarm/wiki)** is the official online documentation which covers at least a significant majority (if not all) of ASF subjects you could be interested in. We recommend to start with **[setting up](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Setting-up)**, **[configuration](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Configuration)** and our **[FAQ](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/FAQ)** which should help you with setting up ASF, configuring it, as well as answering the most common questions that you might have. For more advanced matters, as well as further elaboration, we have other pages available on our **[wiki](https://github.com/JustArchiNET/ArchiSteamFarm/wiki)** that you can visit.
|
||||
|
||||
We also have three independent support channels dedicated to our ASF users, in case you couldn't manage to solve the issue yourself. We answer all support and technical matters in our **[GitHub discussions](https://github.com/JustArchiNET/ArchiSteamFarm/discussions/categories/support)**, **[Steam group](https://steamcommunity.com/groups/archiasf/discussions/1)**, and on our **[Discord server](https://discord.gg/hSQgt8j)**. You're free to use the support channel that matches your preferences, although keep in mind that you have a higher chance solving your issue on the GitHub or Steam, where we're doing our best to answer all questions that couldn't be answered by our community itself (as opposed to Discord server where we're not active 24/7 and therefore not always able to answer).
|
||||
We also have three independent support channels dedicated to our ASF users, in case you couldn't manage to solve the issue yourself. We answer all support and technical matters in our **[GitHub discussions](https://github.com/JustArchiNET/ArchiSteamFarm/discussions/categories/support-english)**, **[Steam group](https://steamcommunity.com/groups/archiasf/discussions/1)**, and on our **[Discord server](https://discord.gg/hSQgt8j)**. You're free to use the support channel that matches your preferences, although keep in mind that you have a higher chance solving your issue on the GitHub or Steam, where we're doing our best to answer all questions that couldn't be answered by our community itself (as opposed to Discord server where we're not active 24/7 and therefore not always able to answer).
|
||||
|
||||
GitHub **issues** (unlike discussions), are being used solely for ASF development, especially in regards to bugs and enhancements. We have a very strict policy regarding that, as GitHub issues is **not** a general support channel, it's dedicated exclusively to ASF development and we're not answering common ASF matters there, as we have appropriate support channels (mentioned above) for that. Common matters include not only general questions or issues that are obviously related to program usage, but also users reporting "bugs" that are clearly considered intended behaviour coming for example (and mainly) from misconfiguration or lack of understanding how the program works. If you're not sure whether your matter relates to ASF development or not, especially if you're not sure if it's a bug or intended behaviour, we recommend to use a support channel instead, where we'll answer you in calm atmosphere and forward your matter as GitHub issue if deemed appropriate. Invalid GitHub issues will be closed immediately and won't be answered.
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
|
||||
- name: Upload latest strings for translation on Crowdin
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && matrix.configuration == 'Release' && startsWith(matrix.os, 'ubuntu-') }}
|
||||
uses: crowdin/github-action@v1.19.0
|
||||
uses: crowdin/github-action@v1.20.0
|
||||
with:
|
||||
crowdin_branch_name: main
|
||||
config: '.github/crowdin.yml'
|
||||
|
||||
4
.github/workflows/code-quality.yml
vendored
4
.github/workflows/code-quality.yml
vendored
@@ -33,13 +33,13 @@ jobs:
|
||||
show-progress: false
|
||||
|
||||
- name: Run Qodana scan
|
||||
uses: JetBrains/qodana-action@v2023.3.1
|
||||
uses: JetBrains/qodana-action@v2023.3.2
|
||||
with:
|
||||
args: --property=idea.headless.enable.statistics=false
|
||||
env:
|
||||
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
|
||||
|
||||
- name: Report Qodana results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@v3.24.4
|
||||
uses: github/codeql-action/upload-sarif@v3.24.9
|
||||
with:
|
||||
sarif_file: ${{ runner.temp }}/qodana/results/qodana.sarif.json
|
||||
|
||||
4
.github/workflows/docker-ci.yml
vendored
4
.github/workflows/docker-ci.yml
vendored
@@ -25,10 +25,10 @@ jobs:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
uses: docker/setup-buildx-action@v3.2.0
|
||||
|
||||
- name: Build ${{ matrix.configuration }} Docker image from ${{ matrix.file }}
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
uses: docker/build-push-action@v5.3.0
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.file }}
|
||||
|
||||
8
.github/workflows/docker-publish-latest.yml
vendored
8
.github/workflows/docker-publish-latest.yml
vendored
@@ -24,17 +24,17 @@ jobs:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
uses: docker/setup-buildx-action@v3.2.0
|
||||
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v3.0.0
|
||||
uses: docker/login-action@v3.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3.0.0
|
||||
uses: docker/login-action@v3.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -59,7 +59,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@v5.1.0
|
||||
uses: docker/build-push-action@v5.3.0
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.Service
|
||||
|
||||
8
.github/workflows/docker-publish-main.yml
vendored
8
.github/workflows/docker-publish-main.yml
vendored
@@ -25,17 +25,17 @@ jobs:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
uses: docker/setup-buildx-action@v3.2.0
|
||||
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v3.0.0
|
||||
uses: docker/login-action@v3.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3.0.0
|
||||
uses: docker/login-action@v3.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -59,7 +59,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@v5.1.0
|
||||
uses: docker/build-push-action@v5.3.0
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ env.PLATFORMS }}
|
||||
|
||||
@@ -25,17 +25,17 @@ jobs:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
uses: docker/setup-buildx-action@v3.2.0
|
||||
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v3.0.0
|
||||
uses: docker/login-action@v3.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3.0.0
|
||||
uses: docker/login-action@v3.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -60,7 +60,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@v5.1.0
|
||||
uses: docker/build-push-action@v5.3.0
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ env.PLATFORMS }}
|
||||
|
||||
104
.github/workflows/publish.yml
vendored
104
.github/workflows/publish.yml
vendored
@@ -8,7 +8,8 @@ env:
|
||||
DOTNET_NOLOGO: true
|
||||
DOTNET_SDK_VERSION: 8.0
|
||||
NODE_JS_VERSION: 'lts/*'
|
||||
PLUGINS: ArchiSteamFarm.OfficialPlugins.ItemsMatcher ArchiSteamFarm.OfficialPlugins.MobileAuthenticator ArchiSteamFarm.OfficialPlugins.SteamTokenDumper
|
||||
PLUGINS_BUNDLED: ArchiSteamFarm.OfficialPlugins.ItemsMatcher ArchiSteamFarm.OfficialPlugins.MobileAuthenticator ArchiSteamFarm.OfficialPlugins.SteamTokenDumper
|
||||
PLUGINS_INCLUDED: ArchiSteamFarm.OfficialPlugins.Monitoring # Apart from declaring them here, there is certain amount of hardcoding needed below for uploading
|
||||
|
||||
permissions: {}
|
||||
|
||||
@@ -88,7 +89,7 @@ jobs:
|
||||
run: dotnet --info
|
||||
|
||||
- name: Download previously built ASF-ui
|
||||
uses: actions/download-artifact@v4.1.2
|
||||
uses: actions/download-artifact@v4.1.4
|
||||
with:
|
||||
name: ASF-ui
|
||||
path: ASF-ui/dist
|
||||
@@ -181,7 +182,7 @@ jobs:
|
||||
(Get-Content "ArchiSteamFarm.OfficialPlugins.SteamTokenDumper\SharedInfo.cs").Replace('STEAM_TOKEN_DUMPER_TOKEN', "$env:STEAM_TOKEN_DUMPER_TOKEN") | Set-Content "ArchiSteamFarm.OfficialPlugins.SteamTokenDumper\SharedInfo.cs"
|
||||
}
|
||||
|
||||
- name: Publish official plugins on Unix
|
||||
- name: Publish bundled plugins on Unix
|
||||
if: startsWith(matrix.os, 'macos-') || startsWith(matrix.os, 'ubuntu-')
|
||||
env:
|
||||
MAX_JOBS: 4
|
||||
@@ -193,7 +194,7 @@ jobs:
|
||||
dotnet publish "$1" -c "$CONFIGURATION" -o "out/${1}" -p:ContinuousIntegrationBuild=true -p:TargetLatestRuntimePatch=false -p:UseAppHost=false --no-restore --nologo
|
||||
}
|
||||
|
||||
for plugin in $PLUGINS; do
|
||||
for plugin in $PLUGINS_BUNDLED; do
|
||||
while [ "$(jobs -p | wc -l)" -ge "$MAX_JOBS" ]; do
|
||||
sleep 1
|
||||
done
|
||||
@@ -203,7 +204,7 @@ jobs:
|
||||
|
||||
wait
|
||||
|
||||
- name: Publish official plugins on Windows
|
||||
- name: Publish bundled plugins on Windows
|
||||
if: startsWith(matrix.os, 'windows-')
|
||||
env:
|
||||
MAX_JOBS: 4
|
||||
@@ -229,7 +230,7 @@ jobs:
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($plugin in $env:PLUGINS.Split([char[]] $null, [System.StringSplitOptions]::RemoveEmptyEntries)) {
|
||||
foreach ($plugin in $env:PLUGINS_BUNDLED.Split([char[]] $null, [System.StringSplitOptions]::RemoveEmptyEntries)) {
|
||||
# Limit active jobs in parallel to help with memory usage
|
||||
$jobs = $(Get-Job -State Running)
|
||||
|
||||
@@ -244,6 +245,58 @@ jobs:
|
||||
|
||||
Get-Job | Receive-Job -Wait
|
||||
|
||||
- name: Publish included plugins on Unix
|
||||
if: ${{ matrix.os == 'ubuntu-latest' && matrix.variant == 'generic' }}
|
||||
env:
|
||||
MAX_JOBS: 4
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
publish() {
|
||||
dotnet publish "$1" -c "$CONFIGURATION" -o "out/${1}" -p:ContinuousIntegrationBuild=true -p:TargetLatestRuntimePatch=false -p:UseAppHost=false --no-restore --nologo
|
||||
|
||||
# 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"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Create the final zip file
|
||||
if command -v 7z >/dev/null; then
|
||||
7z a -bd -slp -tzip -mm=Deflate $seven_zip_args "out/${1}.zip" "${GITHUB_WORKSPACE}/out/${1}/*"
|
||||
else
|
||||
(
|
||||
cd "${GITHUB_WORKSPACE}/out/${1}"
|
||||
zip -q -r $zip_args "../${1}.zip" .
|
||||
)
|
||||
fi
|
||||
}
|
||||
|
||||
for plugin in $PLUGINS_INCLUDED; do
|
||||
while [ "$(jobs -p | wc -l)" -ge "$MAX_JOBS" ]; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
publish "$plugin" &
|
||||
done
|
||||
|
||||
wait
|
||||
|
||||
- name: Upload ArchiSteamFarm.OfficialPlugins.Monitoring
|
||||
if: ${{ matrix.os == 'ubuntu-latest' && matrix.variant == 'generic' }}
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
with:
|
||||
name: ArchiSteamFarm.OfficialPlugins.Monitoring
|
||||
path: out/ArchiSteamFarm.OfficialPlugins.Monitoring.zip
|
||||
|
||||
- name: Publish ASF-${{ matrix.variant }} on Unix
|
||||
if: startsWith(matrix.os, 'macos-') || startsWith(matrix.os, 'ubuntu-')
|
||||
env:
|
||||
@@ -261,7 +314,7 @@ jobs:
|
||||
dotnet publish ArchiSteamFarm -c "$CONFIGURATION" -o "out/${VARIANT}" "-p:ASFVariant=${VARIANT}" -p:ContinuousIntegrationBuild=true --nologo $variantArgs
|
||||
|
||||
# If we're including official plugins for this framework, copy them to output directory
|
||||
for plugin in $PLUGINS; do
|
||||
for plugin in $PLUGINS_BUNDLED; do
|
||||
if [ -d "out/${plugin}" ]; then
|
||||
mkdir -p "out/${VARIANT}/plugins/${plugin}"
|
||||
cp -pR "out/${plugin}/"* "out/${VARIANT}/plugins/${plugin}"
|
||||
@@ -301,24 +354,18 @@ jobs:
|
||||
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
|
||||
7z a -bd -slp -tzip -mm=Deflate $seven_zip_args "out/ASF-${VARIANT}.zip" "${GITHUB_WORKSPACE}/out/${VARIANT}/*"
|
||||
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
|
||||
else
|
||||
(
|
||||
cd "${GITHUB_WORKSPACE}/out/${VARIANT}"
|
||||
zip -q -r $zip_args "../ASF-${VARIANT}.zip" .
|
||||
)
|
||||
else
|
||||
echo "ERROR: No supported zip tool!"
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
@@ -346,7 +393,7 @@ jobs:
|
||||
}
|
||||
|
||||
# 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)) {
|
||||
foreach ($plugin in $env:PLUGINS_BUNDLED.Split([char[]] $null, [System.StringSplitOptions]::RemoveEmptyEntries)) {
|
||||
if (Test-Path "out\$plugin" -PathType Container) {
|
||||
if (!(Test-Path "out\$env:VARIANT\plugins\$plugin" -PathType Container)) {
|
||||
New-Item -ItemType Directory -Path "out\$env:VARIANT\plugins\$plugin" > $null
|
||||
@@ -424,50 +471,56 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Download ArchiSteamFarm.OfficialPlugins.Monitoring artifact
|
||||
uses: actions/download-artifact@v4.1.4
|
||||
with:
|
||||
name: ArchiSteamFarm.OfficialPlugins.Monitoring
|
||||
path: out
|
||||
|
||||
- name: Download ASF-generic artifact from ubuntu-latest
|
||||
uses: actions/download-artifact@v4.1.2
|
||||
uses: actions/download-artifact@v4.1.4
|
||||
with:
|
||||
name: ubuntu-latest_ASF-generic
|
||||
path: out
|
||||
|
||||
- name: Download ASF-linux-arm artifact from ubuntu-latest
|
||||
uses: actions/download-artifact@v4.1.2
|
||||
uses: actions/download-artifact@v4.1.4
|
||||
with:
|
||||
name: ubuntu-latest_ASF-linux-arm
|
||||
path: out
|
||||
|
||||
- name: Download ASF-linux-arm64 artifact from ubuntu-latest
|
||||
uses: actions/download-artifact@v4.1.2
|
||||
uses: actions/download-artifact@v4.1.4
|
||||
with:
|
||||
name: ubuntu-latest_ASF-linux-arm64
|
||||
path: out
|
||||
|
||||
- name: Download ASF-linux-x64 artifact from ubuntu-latest
|
||||
uses: actions/download-artifact@v4.1.2
|
||||
uses: actions/download-artifact@v4.1.4
|
||||
with:
|
||||
name: ubuntu-latest_ASF-linux-x64
|
||||
path: out
|
||||
|
||||
- name: Download ASF-osx-arm64 artifact from macos-latest
|
||||
uses: actions/download-artifact@v4.1.2
|
||||
uses: actions/download-artifact@v4.1.4
|
||||
with:
|
||||
name: macos-latest_ASF-osx-arm64
|
||||
path: out
|
||||
|
||||
- name: Download ASF-osx-x64 artifact from macos-latest
|
||||
uses: actions/download-artifact@v4.1.2
|
||||
uses: actions/download-artifact@v4.1.4
|
||||
with:
|
||||
name: macos-latest_ASF-osx-x64
|
||||
path: out
|
||||
|
||||
- name: Download ASF-win-arm64 artifact from windows-latest
|
||||
uses: actions/download-artifact@v4.1.2
|
||||
uses: actions/download-artifact@v4.1.4
|
||||
with:
|
||||
name: windows-latest_ASF-win-arm64
|
||||
path: out
|
||||
|
||||
- name: Download ASF-win-x64 artifact from windows-latest
|
||||
uses: actions/download-artifact@v4.1.2
|
||||
uses: actions/download-artifact@v4.1.4
|
||||
with:
|
||||
name: windows-latest_ASF-win-x64
|
||||
path: out
|
||||
@@ -504,9 +557,12 @@ jobs:
|
||||
- name: Create ArchiSteamFarm GitHub release
|
||||
uses: ncipollo/release-action@v1.14.0
|
||||
with:
|
||||
allowUpdates: true
|
||||
artifactErrorsFailBuild: true
|
||||
artifacts: "out/*"
|
||||
bodyFile: .github/RELEASE_TEMPLATE.md
|
||||
makeLatest: false
|
||||
name: ArchiSteamFarm V${{ github.ref_name }}
|
||||
prerelease: true
|
||||
token: ${{ secrets.ARCHIBOT_GITHUB_TOKEN }}
|
||||
updateOnlyUnreleased: true
|
||||
|
||||
3
.github/workflows/translations.yml
vendored
3
.github/workflows/translations.yml
vendored
@@ -3,6 +3,7 @@ name: ASF-translations
|
||||
on:
|
||||
schedule:
|
||||
- cron: '55 1 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -30,7 +31,7 @@ jobs:
|
||||
git reset --hard origin/master
|
||||
|
||||
- name: Download latest translations from Crowdin
|
||||
uses: crowdin/github-action@v1.19.0
|
||||
uses: crowdin/github-action@v1.20.0
|
||||
with:
|
||||
upload_sources: false
|
||||
download_translations: true
|
||||
|
||||
2
ASF-ui
2
ASF-ui
Submodule ASF-ui updated: cd1173a0d6...b57627e28c
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
@@ -48,7 +50,7 @@ internal sealed class ExamplePlugin : IASF, IBot, IBotCommand2, IBotConnection,
|
||||
// Please note that this property can have direct dependencies only on structures that were initialized by the constructor, as it's possible to be called before OnLoaded() takes place
|
||||
[JsonInclude]
|
||||
[Required]
|
||||
public string Name => nameof(ExamplePlugin);
|
||||
public string Name => typeof(ExamplePlugin).Assembly.GetName().Name ?? throw new InvalidOperationException(nameof(Name));
|
||||
|
||||
// This will be displayed to the user and written in the log file, typically you should point it to the version of your library, but alternatively you can do some more advanced logic if you'd like to
|
||||
// Please note that this property can have direct dependencies only on structures that were initialized by the constructor, as it's possible to be called before OnLoaded() takes place
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
@@ -42,7 +44,7 @@ internal sealed class PeriodicGCPlugin : IPlugin {
|
||||
|
||||
[JsonInclude]
|
||||
[Required]
|
||||
public string Name => nameof(PeriodicGCPlugin);
|
||||
public string Name => typeof(PeriodicGCPlugin).Assembly.GetName().Name ?? throw new InvalidOperationException(nameof(Name));
|
||||
|
||||
[JsonInclude]
|
||||
[Required]
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
@@ -35,7 +37,7 @@ namespace ArchiSteamFarm.CustomPlugins.SignInWithSteam;
|
||||
internal sealed class SignInWithSteamPlugin : IPlugin {
|
||||
[JsonInclude]
|
||||
[Required]
|
||||
public string Name => nameof(SignInWithSteamPlugin);
|
||||
public string Name => typeof(SignInWithSteamPlugin).Assembly.GetName().Name ?? throw new InvalidOperationException(nameof(Name));
|
||||
|
||||
[JsonInclude]
|
||||
[Required]
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
@@ -41,7 +43,7 @@ using SteamKit2;
|
||||
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher;
|
||||
|
||||
internal static class Backend {
|
||||
internal static async Task<ObjectResponse<GenericResponse<BackgroundTaskResponse>>?> AnnounceDiffForListing(WebBrowser webBrowser, ulong steamID, IReadOnlyCollection<AssetForListing> inventory, string inventoryChecksum, IReadOnlyCollection<Asset.EType> acceptedMatchableTypes, uint totalInventoryCount, bool matchEverything, string tradeToken, IReadOnlyCollection<AssetForListing> inventoryRemoved, string? previousInventoryChecksum, string? nickname = null, string? avatarHash = null) {
|
||||
internal static async Task<ObjectResponse<GenericResponse<BackgroundTaskResponse>>?> AnnounceDiffForListing(WebBrowser webBrowser, ulong steamID, IReadOnlyCollection<AssetForListing> inventory, string inventoryChecksum, IReadOnlyCollection<EAssetType> acceptedMatchableTypes, uint totalInventoryCount, bool matchEverything, string tradeToken, IReadOnlyCollection<AssetForListing> inventoryRemoved, string? previousInventoryChecksum, string? nickname = null, string? avatarHash = null) {
|
||||
ArgumentNullException.ThrowIfNull(webBrowser);
|
||||
|
||||
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
|
||||
@@ -72,7 +74,7 @@ internal static class Backend {
|
||||
return await webBrowser.UrlPostToJsonObject<GenericResponse<BackgroundTaskResponse>, AnnouncementDiffRequest>(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnRedirections | WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.AllowInvalidBodyOnErrors | WebBrowser.ERequestOptions.CompressRequest).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
internal static async Task<ObjectResponse<GenericResponse<BackgroundTaskResponse>>?> AnnounceForListing(WebBrowser webBrowser, ulong steamID, IReadOnlyCollection<AssetForListing> inventory, string inventoryChecksum, IReadOnlyCollection<Asset.EType> acceptedMatchableTypes, uint totalInventoryCount, bool matchEverything, string tradeToken, string? nickname = null, string? avatarHash = null) {
|
||||
internal static async Task<ObjectResponse<GenericResponse<BackgroundTaskResponse>>?> AnnounceForListing(WebBrowser webBrowser, ulong steamID, IReadOnlyCollection<AssetForListing> inventory, string inventoryChecksum, IReadOnlyCollection<EAssetType> acceptedMatchableTypes, uint totalInventoryCount, bool matchEverything, string tradeToken, string? nickname = null, string? avatarHash = null) {
|
||||
ArgumentNullException.ThrowIfNull(webBrowser);
|
||||
|
||||
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
|
||||
@@ -129,7 +131,7 @@ internal static class Backend {
|
||||
return response?.StatusCode;
|
||||
}
|
||||
|
||||
internal static async Task<(HttpStatusCode StatusCode, ImmutableHashSet<ListedUser> Users)?> GetListedUsersForMatching(Guid licenseID, Bot bot, WebBrowser webBrowser, IReadOnlyCollection<Asset> inventory, IReadOnlyCollection<Asset.EType> acceptedMatchableTypes) {
|
||||
internal static async Task<(HttpStatusCode StatusCode, ImmutableHashSet<ListedUser> Users)?> GetListedUsersForMatching(Guid licenseID, Bot bot, WebBrowser webBrowser, IReadOnlyCollection<Asset> inventory, IReadOnlyCollection<EAssetType> acceptedMatchableTypes) {
|
||||
ArgumentOutOfRangeException.ThrowIfEqual(licenseID, Guid.Empty);
|
||||
ArgumentNullException.ThrowIfNull(bot);
|
||||
ArgumentNullException.ThrowIfNull(webBrowser);
|
||||
@@ -156,10 +158,10 @@ internal static class Backend {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (response.StatusCode, response.Content?.Result ?? ImmutableHashSet<ListedUser>.Empty);
|
||||
return (response.StatusCode, response.Content?.Result ?? []);
|
||||
}
|
||||
|
||||
internal static async Task<ObjectResponse<GenericResponse<ImmutableHashSet<SetPart>>>?> GetSetParts(WebBrowser webBrowser, ulong steamID, IReadOnlyCollection<Asset.EType> matchableTypes, IReadOnlyCollection<uint> realAppIDs, CancellationToken cancellationToken = default) {
|
||||
internal static async Task<ObjectResponse<GenericResponse<ImmutableHashSet<SetPart>>>?> GetSetParts(WebBrowser webBrowser, ulong steamID, IReadOnlyCollection<EAssetType> matchableTypes, IReadOnlyCollection<uint> realAppIDs, CancellationToken cancellationToken = default) {
|
||||
ArgumentNullException.ThrowIfNull(webBrowser);
|
||||
|
||||
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
@@ -38,7 +40,7 @@ internal sealed class AnnouncementDiffRequest : AnnouncementRequest {
|
||||
[JsonRequired]
|
||||
private string PreviousInventoryChecksum { get; init; }
|
||||
|
||||
internal AnnouncementDiffRequest(Guid guid, ulong steamID, IReadOnlyCollection<AssetForListing> inventory, string inventoryChecksum, IReadOnlyCollection<Asset.EType> matchableTypes, uint totalInventoryCount, bool matchEverything, byte maxTradeHoldDuration, string tradeToken, IReadOnlyCollection<AssetForListing> inventoryRemoved, string previousInventoryChecksum, string? nickname = null, string? avatarHash = null) : base(guid, steamID, inventory, inventoryChecksum, matchableTypes, totalInventoryCount, matchEverything, maxTradeHoldDuration, tradeToken, nickname, avatarHash) {
|
||||
internal AnnouncementDiffRequest(Guid guid, ulong steamID, IReadOnlyCollection<AssetForListing> inventory, string inventoryChecksum, IReadOnlyCollection<EAssetType> matchableTypes, uint totalInventoryCount, bool matchEverything, byte maxTradeHoldDuration, string tradeToken, IReadOnlyCollection<AssetForListing> inventoryRemoved, string previousInventoryChecksum, string? nickname = null, string? avatarHash = null) : base(guid, steamID, inventory, inventoryChecksum, matchableTypes, totalInventoryCount, matchEverything, maxTradeHoldDuration, tradeToken, nickname, avatarHash) {
|
||||
ArgumentOutOfRangeException.ThrowIfEqual(guid, Guid.Empty);
|
||||
|
||||
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
@@ -48,7 +50,7 @@ internal class AnnouncementRequest {
|
||||
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
private ImmutableHashSet<Asset.EType> MatchableTypes { get; init; }
|
||||
private ImmutableHashSet<EAssetType> MatchableTypes { get; init; }
|
||||
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
@@ -73,7 +75,7 @@ internal class AnnouncementRequest {
|
||||
[JsonRequired]
|
||||
private string TradeToken { get; init; }
|
||||
|
||||
internal AnnouncementRequest(Guid guid, ulong steamID, IReadOnlyCollection<AssetForListing> inventory, string inventoryChecksum, IReadOnlyCollection<Asset.EType> matchableTypes, uint totalInventoryCount, bool matchEverything, byte maxTradeHoldDuration, string tradeToken, string? nickname = null, string? avatarHash = null) {
|
||||
internal AnnouncementRequest(Guid guid, ulong steamID, IReadOnlyCollection<AssetForListing> inventory, string inventoryChecksum, IReadOnlyCollection<EAssetType> matchableTypes, uint totalInventoryCount, bool matchEverything, byte maxTradeHoldDuration, string tradeToken, string? nickname = null, string? avatarHash = null) {
|
||||
ArgumentOutOfRangeException.ThrowIfEqual(guid, Guid.Empty);
|
||||
|
||||
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
@@ -39,7 +41,7 @@ internal class AssetForMatching {
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("r")]
|
||||
[JsonRequired]
|
||||
internal Asset.ERarity Rarity { get; private init; }
|
||||
internal EAssetRarity Rarity { get; private init; }
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("e")]
|
||||
@@ -54,7 +56,7 @@ internal class AssetForMatching {
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("p")]
|
||||
[JsonRequired]
|
||||
internal Asset.EType Type { get; private init; }
|
||||
internal EAssetType Type { get; private init; }
|
||||
|
||||
[JsonConstructor]
|
||||
protected AssetForMatching() { }
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
@@ -40,5 +42,5 @@ internal class AssetInInventory : AssetForMatching {
|
||||
AssetID = asset.AssetID;
|
||||
}
|
||||
|
||||
internal Asset ToAsset() => new(Asset.SteamAppID, Asset.SteamCommunityContextID, ClassID, Amount, tradable: Tradable, assetID: AssetID, realAppID: RealAppID, type: Type, rarity: Rarity);
|
||||
internal Asset ToAsset() => new(Asset.SteamAppID, Asset.SteamCommunityContextID, ClassID, Amount, new InventoryDescription(Asset.SteamAppID, ClassID, tradable: false, realAppID: RealAppID, type: Type, rarity: Rarity), AssetID);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
@@ -40,13 +42,13 @@ internal sealed class InventoriesRequest {
|
||||
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
internal ImmutableHashSet<Asset.EType> MatchableTypes { get; private init; }
|
||||
internal ImmutableHashSet<EAssetType> MatchableTypes { get; private init; }
|
||||
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
internal ulong SteamID { get; private init; }
|
||||
|
||||
internal InventoriesRequest(Guid guid, ulong steamID, IReadOnlyCollection<Asset> inventory, IReadOnlyCollection<Asset.EType> matchableTypes) {
|
||||
internal InventoriesRequest(Guid guid, ulong steamID, IReadOnlyCollection<Asset> inventory, IReadOnlyCollection<EAssetType> matchableTypes) {
|
||||
ArgumentOutOfRangeException.ThrowIfEqual(guid, Guid.Empty);
|
||||
|
||||
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
@@ -31,11 +33,11 @@ namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data;
|
||||
internal sealed class ListedUser {
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
internal ImmutableHashSet<AssetInInventory> Assets { get; private init; } = ImmutableHashSet<AssetInInventory>.Empty;
|
||||
internal ImmutableHashSet<AssetInInventory> Assets { get; private init; } = [];
|
||||
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
internal ImmutableHashSet<Asset.EType> MatchableTypes { get; private init; } = ImmutableHashSet<Asset.EType>.Empty;
|
||||
internal ImmutableHashSet<EAssetType> MatchableTypes { get; private init; } = [];
|
||||
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
@@ -36,7 +38,7 @@ internal sealed class SetPart {
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("r")]
|
||||
[JsonRequired]
|
||||
internal Asset.ERarity Rarity { get; private init; }
|
||||
internal EAssetRarity Rarity { get; private init; }
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("e")]
|
||||
@@ -46,7 +48,7 @@ internal sealed class SetPart {
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("p")]
|
||||
[JsonRequired]
|
||||
internal Asset.EType Type { get; private init; }
|
||||
internal EAssetType Type { get; private init; }
|
||||
|
||||
[JsonConstructor]
|
||||
private SetPart() { }
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
@@ -35,7 +37,7 @@ internal sealed class SetPartsRequest {
|
||||
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
internal ImmutableHashSet<Asset.EType> MatchableTypes { get; private init; }
|
||||
internal ImmutableHashSet<EAssetType> MatchableTypes { get; private init; }
|
||||
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
@@ -45,7 +47,7 @@ internal sealed class SetPartsRequest {
|
||||
[JsonRequired]
|
||||
internal ulong SteamID { get; private init; }
|
||||
|
||||
internal SetPartsRequest(Guid guid, ulong steamID, IReadOnlyCollection<Asset.EType> matchableTypes, IReadOnlyCollection<uint> realAppIDs) {
|
||||
internal SetPartsRequest(Guid guid, ulong steamID, IReadOnlyCollection<EAssetType> matchableTypes, IReadOnlyCollection<uint> realAppIDs) {
|
||||
ArgumentOutOfRangeException.ThrowIfEqual(guid, Guid.Empty);
|
||||
|
||||
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
@@ -46,7 +48,7 @@ internal sealed class ItemsMatcherPlugin : OfficialPlugin, IBot, IBotCommand2, I
|
||||
|
||||
[JsonInclude]
|
||||
[Required]
|
||||
public override string Name => nameof(ItemsMatcherPlugin);
|
||||
public override string Name => typeof(ItemsMatcherPlugin).Assembly.GetName().Name ?? throw new InvalidOperationException(nameof(Name));
|
||||
|
||||
[JsonInclude]
|
||||
[Required]
|
||||
|
||||
191
ArchiSteamFarm.OfficialPlugins.ItemsMatcher/MatchingUtilities.cs
Normal file
191
ArchiSteamFarm.OfficialPlugins.ItemsMatcher/MatchingUtilities.cs
Normal file
@@ -0,0 +1,191 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Ł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.Linq;
|
||||
using ArchiSteamFarm.Steam.Data;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher;
|
||||
|
||||
internal static class MatchingUtilities {
|
||||
internal static (Dictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), Dictionary<ulong, uint>> FullState, Dictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), Dictionary<ulong, uint>> TradableState) GetDividedInventoryState(IReadOnlyCollection<Asset> inventory) {
|
||||
if ((inventory == null) || (inventory.Count == 0)) {
|
||||
throw new ArgumentNullException(nameof(inventory));
|
||||
}
|
||||
|
||||
Dictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), Dictionary<ulong, uint>> fullState = new();
|
||||
Dictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), Dictionary<ulong, uint>> tradableState = new();
|
||||
|
||||
foreach (Asset item in inventory) {
|
||||
(uint RealAppID, EAssetType Type, EAssetRarity Rarity) key = (item.RealAppID, item.Type, item.Rarity);
|
||||
|
||||
if (fullState.TryGetValue(key, out Dictionary<ulong, uint>? fullSet)) {
|
||||
fullSet[item.ClassID] = fullSet.GetValueOrDefault(item.ClassID) + item.Amount;
|
||||
} else {
|
||||
fullState[key] = new Dictionary<ulong, uint> { { item.ClassID, item.Amount } };
|
||||
}
|
||||
|
||||
if (!item.Tradable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tradableState.TryGetValue(key, out Dictionary<ulong, uint>? tradableSet)) {
|
||||
tradableSet[item.ClassID] = tradableSet.GetValueOrDefault(item.ClassID) + item.Amount;
|
||||
} else {
|
||||
tradableState[key] = new Dictionary<ulong, uint> { { item.ClassID, item.Amount } };
|
||||
}
|
||||
}
|
||||
|
||||
return (fullState, tradableState);
|
||||
}
|
||||
|
||||
internal static Dictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), Dictionary<ulong, uint>> GetTradableInventoryState(IReadOnlyCollection<Asset> inventory) {
|
||||
if ((inventory == null) || (inventory.Count == 0)) {
|
||||
throw new ArgumentNullException(nameof(inventory));
|
||||
}
|
||||
|
||||
Dictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), Dictionary<ulong, uint>> tradableState = new();
|
||||
|
||||
foreach (Asset item in inventory.Where(static item => item.Tradable)) {
|
||||
(uint RealAppID, EAssetType Type, EAssetRarity Rarity) key = (item.RealAppID, item.Type, item.Rarity);
|
||||
|
||||
if (tradableState.TryGetValue(key, out Dictionary<ulong, uint>? tradableSet)) {
|
||||
tradableSet[item.ClassID] = tradableSet.GetValueOrDefault(item.ClassID) + item.Amount;
|
||||
} else {
|
||||
tradableState[key] = new Dictionary<ulong, uint> { { item.ClassID, item.Amount } };
|
||||
}
|
||||
}
|
||||
|
||||
return tradableState;
|
||||
}
|
||||
|
||||
internal static HashSet<Asset> GetTradableItemsFromInventory(IReadOnlyCollection<Asset> inventory, IReadOnlyDictionary<ulong, uint> classIDs, bool randomize = false) {
|
||||
if ((inventory == null) || (inventory.Count == 0)) {
|
||||
throw new ArgumentNullException(nameof(inventory));
|
||||
}
|
||||
|
||||
if ((classIDs == null) || (classIDs.Count == 0)) {
|
||||
throw new ArgumentNullException(nameof(classIDs));
|
||||
}
|
||||
|
||||
// We need a copy of classIDs passed since we're going to manipulate them
|
||||
Dictionary<ulong, uint> classIDsState = classIDs.ToDictionary();
|
||||
|
||||
HashSet<Asset> result = [];
|
||||
|
||||
IEnumerable<Asset> items = inventory.Where(static item => item.Tradable);
|
||||
|
||||
// Randomization helps to decrease "items no longer available" in regards to sending offers to other users
|
||||
if (randomize) {
|
||||
#pragma warning disable CA5394 // This call isn't used in a security-sensitive manner
|
||||
items = items.Where(item => classIDsState.ContainsKey(item.ClassID)).OrderBy(static _ => Random.Shared.Next());
|
||||
#pragma warning restore CA5394 // This call isn't used in a security-sensitive manner
|
||||
}
|
||||
|
||||
foreach (Asset item in items) {
|
||||
if (!classIDsState.TryGetValue(item.ClassID, out uint amount)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (amount >= item.Amount) {
|
||||
result.Add(item);
|
||||
|
||||
if (amount > item.Amount) {
|
||||
classIDsState[item.ClassID] = amount - item.Amount;
|
||||
} else {
|
||||
classIDsState.Remove(item.ClassID);
|
||||
|
||||
if (classIDsState.Count == 0) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Asset itemToAdd = item.DeepClone();
|
||||
|
||||
itemToAdd.Amount = amount;
|
||||
|
||||
result.Add(itemToAdd);
|
||||
|
||||
classIDsState.Remove(itemToAdd.ClassID);
|
||||
|
||||
if (classIDsState.Count == 0) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we got here it means we still have classIDs to match
|
||||
throw new InvalidOperationException(nameof(classIDs));
|
||||
}
|
||||
|
||||
internal static bool IsEmptyForMatching(IReadOnlyDictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), Dictionary<ulong, uint>> fullState, IReadOnlyDictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), Dictionary<ulong, uint>> tradableState) {
|
||||
ArgumentNullException.ThrowIfNull(fullState);
|
||||
ArgumentNullException.ThrowIfNull(tradableState);
|
||||
|
||||
foreach (((uint RealAppID, EAssetType Type, EAssetRarity Rarity) set, IReadOnlyDictionary<ulong, uint> state) in tradableState) {
|
||||
if (!fullState.TryGetValue(set, out Dictionary<ulong, uint>? fullSet) || (fullSet.Count == 0)) {
|
||||
throw new InvalidOperationException(nameof(fullSet));
|
||||
}
|
||||
|
||||
if (!IsEmptyForMatching(fullSet, state)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// We didn't find any matchable combinations, so this inventory is empty
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static bool IsEmptyForMatching(IReadOnlyDictionary<ulong, uint> fullSet, IReadOnlyDictionary<ulong, uint> tradableSet) {
|
||||
ArgumentNullException.ThrowIfNull(fullSet);
|
||||
ArgumentNullException.ThrowIfNull(tradableSet);
|
||||
|
||||
foreach ((ulong classID, uint amount) in tradableSet) {
|
||||
switch (amount) {
|
||||
case 0:
|
||||
// No tradable items, this should never happen, dictionary should not have this key to begin with
|
||||
throw new InvalidOperationException(nameof(amount));
|
||||
case 1:
|
||||
// Single tradable item, can be matchable or not depending on the rest of the inventory
|
||||
if (!fullSet.TryGetValue(classID, out uint fullAmount) || (fullAmount == 0)) {
|
||||
throw new InvalidOperationException(nameof(fullAmount));
|
||||
}
|
||||
|
||||
if (fullAmount > 1) {
|
||||
// If we have a single tradable item but more than 1 in total, this is matchable
|
||||
return false;
|
||||
}
|
||||
|
||||
// A single exclusive tradable item is not matchable, continue
|
||||
continue;
|
||||
default:
|
||||
// Any other combination of tradable items is always matchable
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// We didn't find any matchable combinations, so this inventory is empty
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
@@ -61,11 +63,11 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
private const byte MinimumSteamGuardEnabledDays = 15; // As imposed by Steam limits
|
||||
private const byte MinPersonaStateTTL = 5; // Minimum amount of minutes we must wait before requesting persona state update
|
||||
|
||||
private static readonly FrozenSet<Asset.EType> AcceptedMatchableTypes = new HashSet<Asset.EType>(4) {
|
||||
Asset.EType.Emoticon,
|
||||
Asset.EType.FoilTradingCard,
|
||||
Asset.EType.ProfileBackground,
|
||||
Asset.EType.TradingCard
|
||||
private static readonly FrozenSet<EAssetType> AcceptedMatchableTypes = new HashSet<EAssetType>(4) {
|
||||
EAssetType.Emoticon,
|
||||
EAssetType.FoilTradingCard,
|
||||
EAssetType.ProfileBackground,
|
||||
EAssetType.TradingCard
|
||||
}.ToFrozenSet();
|
||||
|
||||
private readonly Bot Bot;
|
||||
@@ -229,7 +231,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
return;
|
||||
}
|
||||
|
||||
HashSet<Asset.EType> acceptedMatchableTypes = Bot.BotConfig.MatchableTypes.Where(AcceptedMatchableTypes.Contains).ToHashSet();
|
||||
HashSet<EAssetType> acceptedMatchableTypes = Bot.BotConfig.MatchableTypes.Where(AcceptedMatchableTypes.Contains).ToHashSet();
|
||||
|
||||
if (acceptedMatchableTypes.Count == 0) {
|
||||
throw new InvalidOperationException(nameof(acceptedMatchableTypes));
|
||||
@@ -250,7 +252,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
List<Asset> inventory;
|
||||
|
||||
try {
|
||||
inventory = await Bot.ArchiWebHandler.GetInventoryAsync().ToListAsync().ConfigureAwait(false);
|
||||
inventory = await Bot.ArchiHandler.GetMyInventoryAsync().ToListAsync().ConfigureAwait(false);
|
||||
} catch (HttpRequestException e) {
|
||||
// This is actually a network failure, so we'll stop sending heartbeats but not record it as valid check
|
||||
ShouldSendHeartBeats = false;
|
||||
@@ -282,10 +284,10 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
|
||||
List<AssetForListing> assetsForListing = [];
|
||||
|
||||
Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), bool> tradableSets = new();
|
||||
Dictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), bool> tradableSets = new();
|
||||
|
||||
foreach (Asset item in inventory) {
|
||||
if (item is { AssetID: > 0, Amount: > 0, ClassID: > 0, RealAppID: > 0, Type: > Asset.EType.Unknown, Rarity: > Asset.ERarity.Unknown, IsSteamPointsShopItem: false } && acceptedMatchableTypes.Contains(item.Type)) {
|
||||
if (item is { AssetID: > 0, Amount: > 0, ClassID: > 0, RealAppID: > 0, Type: > EAssetType.Unknown, Rarity: > EAssetRarity.Unknown, IsSteamPointsShopItem: false } && acceptedMatchableTypes.Contains(item.Type)) {
|
||||
// Only tradable assets matter for MatchEverything bots
|
||||
if (!matchEverything || item.Tradable) {
|
||||
assetsForListing.Add(new AssetForListing(item, index, previousAssetID));
|
||||
@@ -293,7 +295,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
|
||||
// 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);
|
||||
(uint RealAppID, EAssetType Type, EAssetRarity Rarity) key = (item.RealAppID, item.Type, item.Rarity);
|
||||
|
||||
if (tradableSets.TryGetValue(key, out bool tradable)) {
|
||||
if (!tradable && item.Tradable) {
|
||||
@@ -375,15 +377,15 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
if (!matchEverything) {
|
||||
// We should deduplicate our sets before sending them to the server, for doing that we'll use ASFB set parts data
|
||||
HashSet<uint> realAppIDs = [];
|
||||
Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), Dictionary<ulong, uint>> state = new();
|
||||
Dictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), Dictionary<ulong, uint>> state = new();
|
||||
|
||||
foreach (AssetForListing asset in assetsForListing) {
|
||||
realAppIDs.Add(asset.RealAppID);
|
||||
|
||||
(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity) key = (asset.RealAppID, asset.Type, asset.Rarity);
|
||||
(uint RealAppID, EAssetType Type, EAssetRarity Rarity) key = (asset.RealAppID, asset.Type, asset.Rarity);
|
||||
|
||||
if (state.TryGetValue(key, out Dictionary<ulong, uint>? set)) {
|
||||
set[asset.ClassID] = set.TryGetValue(asset.ClassID, out uint amount) ? amount + asset.Amount : asset.Amount;
|
||||
set[asset.ClassID] = set.GetValueOrDefault(asset.ClassID) + asset.Amount;
|
||||
} else {
|
||||
state[key] = new Dictionary<ulong, uint> { { asset.ClassID, asset.Amount } };
|
||||
}
|
||||
@@ -446,11 +448,11 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
return;
|
||||
}
|
||||
|
||||
Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), HashSet<ulong>> databaseSets = setPartsResponse.Content.Result.GroupBy(static setPart => (setPart.RealAppID, setPart.Type, setPart.Rarity)).ToDictionary(static group => group.Key, static group => group.Select(static setPart => setPart.ClassID).ToHashSet());
|
||||
Dictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), HashSet<ulong>> databaseSets = setPartsResponse.Content.Result.GroupBy(static setPart => (setPart.RealAppID, setPart.Type, setPart.Rarity)).ToDictionary(static group => group.Key, static group => group.Select(static setPart => setPart.ClassID).ToHashSet());
|
||||
|
||||
Dictionary<ulong, uint> setCopy = [];
|
||||
|
||||
foreach (((uint RealAppID, Asset.EType Type, Asset.ERarity Rarity) key, Dictionary<ulong, uint> set) in state) {
|
||||
foreach (((uint RealAppID, EAssetType Type, EAssetRarity Rarity) key, Dictionary<ulong, uint> set) in state) {
|
||||
if (!databaseSets.TryGetValue(key, out HashSet<ulong>? databaseSet)) {
|
||||
// We have no clue about this set, we can't do any optimization
|
||||
continue;
|
||||
@@ -488,7 +490,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
HashSet<AssetForListing> assetsForListingFiltered = [];
|
||||
|
||||
foreach (AssetForListing asset in assetsForListing.Where(asset => state.TryGetValue((asset.RealAppID, asset.Type, asset.Rarity), out Dictionary<ulong, uint>? setState) && setState.TryGetValue(asset.ClassID, out uint targetAmount) && (targetAmount > 0)).OrderByDescending(static asset => asset.Tradable).ThenByDescending(static asset => asset.Index)) {
|
||||
(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity) key = (asset.RealAppID, asset.Type, asset.Rarity);
|
||||
(uint RealAppID, EAssetType Type, EAssetRarity Rarity) key = (asset.RealAppID, asset.Type, asset.Rarity);
|
||||
|
||||
if (!state.TryGetValue(key, out Dictionary<ulong, uint>? setState) || !setState.TryGetValue(asset.ClassID, out uint targetAmount) || (targetAmount == 0)) {
|
||||
// We're not interested in this combination
|
||||
@@ -901,7 +903,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
return;
|
||||
}
|
||||
|
||||
HashSet<Asset.EType> acceptedMatchableTypes = Bot.BotConfig.MatchableTypes.Where(AcceptedMatchableTypes.Contains).ToHashSet();
|
||||
HashSet<EAssetType> acceptedMatchableTypes = Bot.BotConfig.MatchableTypes.Where(AcceptedMatchableTypes.Contains).ToHashSet();
|
||||
|
||||
if (acceptedMatchableTypes.Count == 0) {
|
||||
Bot.ArchiLogger.LogNullError(acceptedMatchableTypes);
|
||||
@@ -937,7 +939,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
HashSet<Asset> assetsForMatching;
|
||||
|
||||
try {
|
||||
assetsForMatching = await Bot.ArchiWebHandler.GetInventoryAsync().Where(item => item is { AssetID: > 0, Amount: > 0, ClassID: > 0, RealAppID: > 0, Type: > Asset.EType.Unknown, Rarity: > Asset.ERarity.Unknown, IsSteamPointsShopItem: false } && acceptedMatchableTypes.Contains(item.Type) && !Bot.BotDatabase.MatchActivelyBlacklistAppIDs.Contains(item.RealAppID)).ToHashSetAsync().ConfigureAwait(false);
|
||||
assetsForMatching = await Bot.ArchiHandler.GetMyInventoryAsync().Where(item => item is { AssetID: > 0, Amount: > 0, ClassID: > 0, RealAppID: > 0, Type: > EAssetType.Unknown, Rarity: > EAssetRarity.Unknown, IsSteamPointsShopItem: false } && acceptedMatchableTypes.Contains(item.Type) && !Bot.BotDatabase.MatchActivelyBlacklistAppIDs.Contains(item.RealAppID)).ToHashSetAsync().ConfigureAwait(false);
|
||||
} catch (HttpRequestException e) {
|
||||
Bot.ArchiLogger.LogGenericWarningException(e);
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(assetsForMatching)));
|
||||
@@ -957,7 +959,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
}
|
||||
|
||||
// Remove from our inventory items that can't be possibly matched due to no dupes to offer available
|
||||
HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> setsToKeep = Trading.GetInventorySets(assetsForMatching).Where(static set => set.Value.Any(static amount => amount > 1)).Select(static set => set.Key).ToHashSet();
|
||||
HashSet<(uint RealAppID, EAssetType Type, EAssetRarity Rarity)> setsToKeep = Trading.GetInventorySets(assetsForMatching).Where(static set => set.Value.Any(static amount => amount > 1)).Select(static set => set.Key).ToHashSet();
|
||||
|
||||
if (assetsForMatching.RemoveWhere(item => !setsToKeep.Contains((item.RealAppID, item.Type, item.Rarity))) > 0) {
|
||||
if (assetsForMatching.Count == 0) {
|
||||
@@ -969,15 +971,15 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
|
||||
// We should deduplicate our sets before sending them to the server, for doing that we'll use ASFB set parts data
|
||||
HashSet<uint> realAppIDs = [];
|
||||
Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), Dictionary<ulong, uint>> setsState = new();
|
||||
Dictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), Dictionary<ulong, uint>> setsState = new();
|
||||
|
||||
foreach (Asset asset in assetsForMatching) {
|
||||
realAppIDs.Add(asset.RealAppID);
|
||||
|
||||
(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity) key = (asset.RealAppID, asset.Type, asset.Rarity);
|
||||
(uint RealAppID, EAssetType Type, EAssetRarity Rarity) key = (asset.RealAppID, asset.Type, asset.Rarity);
|
||||
|
||||
if (setsState.TryGetValue(key, out Dictionary<ulong, uint>? set)) {
|
||||
set[asset.ClassID] = set.TryGetValue(asset.ClassID, out uint amount) ? amount + asset.Amount : asset.Amount;
|
||||
set[asset.ClassID] = set.GetValueOrDefault(asset.ClassID) + asset.Amount;
|
||||
} else {
|
||||
setsState[key] = new Dictionary<ulong, uint> { { asset.ClassID, asset.Amount } };
|
||||
}
|
||||
@@ -1031,11 +1033,11 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
return;
|
||||
}
|
||||
|
||||
Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), HashSet<ulong>> databaseSets = setPartsResponse.Content.Result.GroupBy(static setPart => (setPart.RealAppID, setPart.Type, setPart.Rarity)).ToDictionary(static group => group.Key, static group => group.Select(static setPart => setPart.ClassID).ToHashSet());
|
||||
Dictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), HashSet<ulong>> databaseSets = setPartsResponse.Content.Result.GroupBy(static setPart => (setPart.RealAppID, setPart.Type, setPart.Rarity)).ToDictionary(static group => group.Key, static group => group.Select(static setPart => setPart.ClassID).ToHashSet());
|
||||
|
||||
Dictionary<ulong, uint> setCopy = [];
|
||||
|
||||
foreach (((uint RealAppID, Asset.EType Type, Asset.ERarity Rarity) key, Dictionary<ulong, uint> set) in setsState) {
|
||||
foreach (((uint RealAppID, EAssetType Type, EAssetRarity Rarity) key, Dictionary<ulong, uint> set) in setsState) {
|
||||
uint minimumAmount = uint.MaxValue;
|
||||
uint maximumAmount = uint.MinValue;
|
||||
|
||||
@@ -1094,7 +1096,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
HashSet<Asset> assetsForMatchingFiltered = [];
|
||||
|
||||
foreach (Asset asset in assetsForMatching.Where(asset => setsState.TryGetValue((asset.RealAppID, asset.Type, asset.Rarity), out Dictionary<ulong, uint>? setState) && setState.TryGetValue(asset.ClassID, out uint targetAmount) && (targetAmount > 0)).OrderByDescending(static asset => asset.Tradable)) {
|
||||
(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity) key = (asset.RealAppID, asset.Type, asset.Rarity);
|
||||
(uint RealAppID, EAssetType Type, EAssetRarity Rarity) key = (asset.RealAppID, asset.Type, asset.Rarity);
|
||||
|
||||
if (!setsState.TryGetValue(key, out Dictionary<ulong, uint>? setState) || !setState.TryGetValue(asset.ClassID, out uint targetAmount) || (targetAmount == 0)) {
|
||||
// We're not interested in this combination
|
||||
@@ -1165,7 +1167,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> MatchActively(IReadOnlyCollection<ListedUser> listedUsers, IReadOnlyCollection<Asset> ourAssets, IReadOnlyCollection<Asset.EType> acceptedMatchableTypes) {
|
||||
private async Task<bool> MatchActively(IReadOnlyCollection<ListedUser> listedUsers, IReadOnlyCollection<Asset> ourAssets, IReadOnlyCollection<EAssetType> acceptedMatchableTypes) {
|
||||
if ((listedUsers == null) || (listedUsers.Count == 0)) {
|
||||
throw new ArgumentNullException(nameof(listedUsers));
|
||||
}
|
||||
@@ -1178,9 +1180,9 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
throw new ArgumentNullException(nameof(acceptedMatchableTypes));
|
||||
}
|
||||
|
||||
(Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), Dictionary<ulong, uint>> ourFullState, Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), Dictionary<ulong, uint>> ourTradableState) = Trading.GetDividedInventoryState(ourAssets);
|
||||
(Dictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), Dictionary<ulong, uint>> ourFullState, Dictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), Dictionary<ulong, uint>> ourTradableState) = MatchingUtilities.GetDividedInventoryState(ourAssets);
|
||||
|
||||
if (Trading.IsEmptyForMatching(ourFullState, ourTradableState)) {
|
||||
if (MatchingUtilities.IsEmptyForMatching(ourFullState, ourTradableState)) {
|
||||
// User doesn't have any more dupes in the inventory
|
||||
Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, $"{nameof(ourFullState)} || {nameof(ourTradableState)}"));
|
||||
|
||||
@@ -1248,6 +1250,14 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
byte failuresInRow = 0;
|
||||
uint matchedSets = 0;
|
||||
|
||||
HashSet<(uint RealAppID, EAssetType Type, EAssetRarity Rarity)> skippedSetsThisUser = [];
|
||||
HashSet<(uint RealAppID, EAssetType Type, EAssetRarity Rarity)> skippedSetsThisTrade = [];
|
||||
|
||||
Dictionary<ulong, uint> classIDsToGive = new();
|
||||
Dictionary<ulong, uint> classIDsToReceive = new();
|
||||
Dictionary<ulong, uint> fairClassIDsToGive = new();
|
||||
Dictionary<ulong, uint> fairClassIDsToReceive = new();
|
||||
|
||||
foreach (ListedUser listedUser in listedUsers.Where(listedUser => (listedUser.SteamID != Bot.SteamID) && acceptedMatchableTypes.Any(listedUser.MatchableTypes.Contains) && !Bot.IsBlacklistedFromTrades(listedUser.SteamID)).OrderByDescending(listedUser => !deprioritizedSteamIDs.Contains(listedUser.SteamID)).ThenByDescending(static listedUser => listedUser.TotalGamesCount > 1).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}"));
|
||||
@@ -1261,7 +1271,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
break;
|
||||
}
|
||||
|
||||
HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> wantedSets = ourTradableState.Keys.Where(set => listedUser.MatchableTypes.Contains(set.Type)).ToHashSet();
|
||||
HashSet<(uint RealAppID, EAssetType Type, EAssetRarity Rarity)> wantedSets = ourTradableState.Keys.Where(set => listedUser.MatchableTypes.Contains(set.Type)).ToHashSet();
|
||||
|
||||
if (wantedSets.Count == 0) {
|
||||
continue;
|
||||
@@ -1282,26 +1292,27 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
continue;
|
||||
}
|
||||
|
||||
HashSet<Asset> theirInventory = listedUser.Assets.Where(item => (!listedUser.MatchEverything || item.Tradable) && wantedSets.Contains((item.RealAppID, item.Type, item.Rarity)) && ((tradeHoldDuration.Value == 0) || !(item.Type is Asset.EType.FoilTradingCard or Asset.EType.TradingCard && CardsFarmer.SalesBlacklist.Contains(item.RealAppID)))).Select(static asset => asset.ToAsset()).ToHashSet();
|
||||
HashSet<Asset> theirInventory = listedUser.Assets.Where(item => (!listedUser.MatchEverything || item.Tradable) && wantedSets.Contains((item.RealAppID, item.Type, item.Rarity)) && ((tradeHoldDuration.Value == 0) || !(item.Type is EAssetType.FoilTradingCard or EAssetType.TradingCard && CardsFarmer.SalesBlacklist.Contains(item.RealAppID)))).Select(static asset => asset.ToAsset()).ToHashSet();
|
||||
|
||||
if (theirInventory.Count == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> skippedSetsThisUser = [];
|
||||
skippedSetsThisUser.Clear();
|
||||
|
||||
Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), Dictionary<ulong, uint>> theirTradableState = Trading.GetTradableInventoryState(theirInventory);
|
||||
Dictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), Dictionary<ulong, uint>> theirTradableState = MatchingUtilities.GetTradableInventoryState(theirInventory);
|
||||
|
||||
for (byte i = 0; i < Trading.MaxTradesPerAccount; i++) {
|
||||
byte itemsInTrade = 0;
|
||||
HashSet<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity)> skippedSetsThisTrade = [];
|
||||
|
||||
Dictionary<ulong, uint> classIDsToGive = new();
|
||||
Dictionary<ulong, uint> classIDsToReceive = new();
|
||||
Dictionary<ulong, uint> fairClassIDsToGive = new();
|
||||
Dictionary<ulong, uint> fairClassIDsToReceive = new();
|
||||
skippedSetsThisTrade.Clear();
|
||||
|
||||
foreach (((uint RealAppID, Asset.EType Type, Asset.ERarity Rarity) set, Dictionary<ulong, uint> ourFullItems) in ourFullState.Where(set => !skippedSetsThisUser.Contains(set.Key) && listedUser.MatchableTypes.Contains(set.Key.Type) && set.Value.Values.Any(static count => count > 1))) {
|
||||
classIDsToGive.Clear();
|
||||
classIDsToReceive.Clear();
|
||||
fairClassIDsToGive.Clear();
|
||||
fairClassIDsToReceive.Clear();
|
||||
|
||||
foreach (((uint RealAppID, EAssetType Type, EAssetRarity Rarity) set, Dictionary<ulong, uint> ourFullItems) in ourFullState.Where(set => !skippedSetsThisUser.Contains(set.Key) && listedUser.MatchableTypes.Contains(set.Key.Type) && set.Value.Values.Any(static count => count > 1))) {
|
||||
if (!ourTradableState.TryGetValue(set, out Dictionary<ulong, uint>? ourTradableItems) || (ourTradableItems.Count == 0)) {
|
||||
// We may have no more tradable items from this set
|
||||
continue;
|
||||
@@ -1312,14 +1323,14 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Trading.IsEmptyForMatching(ourFullItems, ourTradableItems)) {
|
||||
if (MatchingUtilities.IsEmptyForMatching(ourFullItems, ourTradableItems)) {
|
||||
// We may have no more matchable items from this set
|
||||
continue;
|
||||
}
|
||||
|
||||
// Those 2 collections are on user-basis since we can't be sure that the trade passes through (and therefore we need to keep original state in case of a failure)
|
||||
Dictionary<ulong, uint> ourFullSet = new(ourFullItems);
|
||||
Dictionary<ulong, uint> ourTradableSet = new(ourTradableItems);
|
||||
Dictionary<ulong, uint> ourFullSet = ourFullItems.ToDictionary();
|
||||
Dictionary<ulong, uint> ourTradableSet = ourTradableItems.ToDictionary();
|
||||
|
||||
bool match;
|
||||
|
||||
@@ -1331,26 +1342,27 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ((ulong theirItem, uint theirTradableAmount) in theirTradableItems.OrderBy(item => ourFullSet.TryGetValue(item.Key, out uint ourAmountOfTheirItem) ? ourAmountOfTheirItem : 0)) {
|
||||
foreach ((ulong theirItem, uint theirTradableAmount) in theirTradableItems.OrderBy(item => ourFullSet.GetValueOrDefault(item.Key))) {
|
||||
if (ourFullSet.TryGetValue(theirItem, out uint ourAmountOfTheirItem) && (ourFullAmount <= ourAmountOfTheirItem + 1)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!listedUser.MatchEverything) {
|
||||
// We have a potential match, let's check fairness for them
|
||||
fairClassIDsToGive.TryGetValue(ourItem, out uint fairGivenAmount);
|
||||
fairClassIDsToReceive.TryGetValue(theirItem, out uint fairReceivedAmount);
|
||||
uint fairGivenAmount = fairClassIDsToGive.GetValueOrDefault(ourItem);
|
||||
uint fairReceivedAmount = fairClassIDsToReceive.GetValueOrDefault(theirItem);
|
||||
|
||||
fairClassIDsToGive[ourItem] = ++fairGivenAmount;
|
||||
fairClassIDsToReceive[theirItem] = ++fairReceivedAmount;
|
||||
|
||||
// Filter their inventory for the sets we're trading or have traded with this user
|
||||
HashSet<Asset> fairFiltered = theirInventory.Where(item => ((item.RealAppID == set.RealAppID) && (item.Type == set.Type) && (item.Rarity == set.Rarity)) || skippedSetsThisTrade.Contains((item.RealAppID, item.Type, item.Rarity))).Select(static item => item.CreateShallowCopy()).ToHashSet();
|
||||
HashSet<Asset> fairFiltered = theirInventory.Where(item => ((item.RealAppID == set.RealAppID) && (item.Type == set.Type) && (item.Rarity == set.Rarity)) || skippedSetsThisTrade.Contains((item.RealAppID, item.Type, item.Rarity))).ToHashSet();
|
||||
|
||||
// Copy list to HashSet<Steam.Asset>
|
||||
HashSet<Asset> fairItemsToGive = Trading.GetTradableItemsFromInventory(ourInventory.Values.Where(item => ((item.RealAppID == set.RealAppID) && (item.Type == set.Type) && (item.Rarity == set.Rarity)) || skippedSetsThisTrade.Contains((item.RealAppID, item.Type, item.Rarity))).Select(static item => item.CreateShallowCopy()).ToHashSet(), fairClassIDsToGive.ToDictionary(static classID => classID.Key, static classID => classID.Value));
|
||||
HashSet<Asset> fairItemsToReceive = Trading.GetTradableItemsFromInventory(fairFiltered.Select(static item => item.CreateShallowCopy()).ToHashSet(), fairClassIDsToReceive.ToDictionary(static classID => classID.Key, static classID => classID.Value));
|
||||
// Get tradable items from our and their inventory
|
||||
HashSet<Asset> fairItemsToGive = MatchingUtilities.GetTradableItemsFromInventory(ourInventory.Values.Where(item => ((item.RealAppID == set.RealAppID) && (item.Type == set.Type) && (item.Rarity == set.Rarity)) || skippedSetsThisTrade.Contains((item.RealAppID, item.Type, item.Rarity))).ToHashSet(), fairClassIDsToGive);
|
||||
HashSet<Asset> fairItemsToReceive = MatchingUtilities.GetTradableItemsFromInventory(fairFiltered, fairClassIDsToReceive);
|
||||
|
||||
// Actual check
|
||||
// Actual check, since we do this against remote user, we flip places for items
|
||||
if (!Trading.IsTradeNeutralOrBetter(fairFiltered, fairItemsToReceive, fairItemsToGive)) {
|
||||
// Revert the changes
|
||||
if (fairGivenAmount > 1) {
|
||||
@@ -1373,11 +1385,11 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
skippedSetsThisTrade.Add(set);
|
||||
|
||||
// Update our state based on given items
|
||||
classIDsToGive[ourItem] = classIDsToGive.TryGetValue(ourItem, out uint ourGivenAmount) ? ourGivenAmount + 1 : 1;
|
||||
classIDsToGive[ourItem] = classIDsToGive.GetValueOrDefault(ourItem) + 1;
|
||||
ourFullSet[ourItem] = ourFullAmount - 1; // We don't need to remove anything here because we can guarantee that ourItem.Value is at least 2
|
||||
|
||||
// Update our state based on received items
|
||||
classIDsToReceive[theirItem] = classIDsToReceive.TryGetValue(theirItem, out uint ourReceivedAmount) ? ourReceivedAmount + 1 : 1;
|
||||
classIDsToReceive[theirItem] = classIDsToReceive.GetValueOrDefault(theirItem) + 1;
|
||||
ourFullSet[theirItem] = ourAmountOfTheirItem + 1;
|
||||
|
||||
if (ourTradableAmount > 1) {
|
||||
@@ -1418,8 +1430,8 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
}
|
||||
|
||||
// Remove the items from inventories
|
||||
HashSet<Asset> itemsToGive = Trading.GetTradableItemsFromInventory(ourInventory.Values, classIDsToGive);
|
||||
HashSet<Asset> itemsToReceive = Trading.GetTradableItemsFromInventory(theirInventory, classIDsToReceive, true);
|
||||
HashSet<Asset> itemsToGive = MatchingUtilities.GetTradableItemsFromInventory(ourInventory.Values, classIDsToGive);
|
||||
HashSet<Asset> itemsToReceive = MatchingUtilities.GetTradableItemsFromInventory(theirInventory, classIDsToReceive, true);
|
||||
|
||||
if ((itemsToGive.Count != itemsToReceive.Count) || !Trading.IsFairExchange(itemsToGive, itemsToReceive)) {
|
||||
// Failsafe
|
||||
@@ -1503,10 +1515,14 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
// However, since this is only an assumption, we must mark newly acquired items as untradable so we're sure that they're not considered for trading, only for matching
|
||||
foreach (Asset itemToReceive in itemsToReceive) {
|
||||
if (ourInventory.TryGetValue(itemToReceive.AssetID, out Asset? item)) {
|
||||
item.Tradable = false;
|
||||
item.Description ??= new InventoryDescription(itemToReceive.AppID, itemToReceive.ClassID, itemToReceive.InstanceID, realAppID: itemToReceive.RealAppID, type: itemToReceive.Type, rarity: itemToReceive.Rarity);
|
||||
|
||||
item.Description.Body.tradable = false;
|
||||
item.Amount += itemToReceive.Amount;
|
||||
} else {
|
||||
itemToReceive.Tradable = false;
|
||||
itemToReceive.Description ??= new InventoryDescription(itemToReceive.AppID, itemToReceive.ClassID, itemToReceive.InstanceID, realAppID: itemToReceive.RealAppID, type: itemToReceive.Type, rarity: itemToReceive.Rarity);
|
||||
|
||||
itemToReceive.Description.Body.tradable = false;
|
||||
ourInventory[itemToReceive.AssetID] = itemToReceive;
|
||||
}
|
||||
|
||||
@@ -1515,11 +1531,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
throw new InvalidOperationException(nameof(fullAmounts));
|
||||
}
|
||||
|
||||
if (!fullAmounts.TryGetValue(itemToReceive.ClassID, out uint fullAmount)) {
|
||||
fullAmount = 0;
|
||||
}
|
||||
|
||||
fullAmounts[itemToReceive.ClassID] = itemToReceive.Amount + fullAmount;
|
||||
fullAmounts[itemToReceive.ClassID] = fullAmounts.GetValueOrDefault(itemToReceive.ClassID) + itemToReceive.Amount;
|
||||
}
|
||||
|
||||
skippedSetsThisUser.UnionWith(skippedSetsThisTrade);
|
||||
@@ -1531,7 +1543,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
|
||||
matchedSets += (uint) skippedSetsThisUser.Count;
|
||||
|
||||
if (Trading.IsEmptyForMatching(ourFullState, ourTradableState)) {
|
||||
if (MatchingUtilities.IsEmptyForMatching(ourFullState, ourTradableState)) {
|
||||
// User doesn't have any more dupes in the inventory
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
@@ -36,8 +38,6 @@ 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);
|
||||
|
||||
@@ -144,43 +144,22 @@ internal static class Commands {
|
||||
|
||||
ulong steamTime = await mobileAuthenticator.GetSteamTime().ConfigureAwait(false);
|
||||
|
||||
bool successFinalizing = false;
|
||||
string? code = mobileAuthenticator.GenerateTokenForTime(steamTime);
|
||||
|
||||
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)));
|
||||
}
|
||||
|
||||
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 (string.IsNullOrEmpty(code)) {
|
||||
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(mobileAuthenticator.GenerateTokenForTime)));
|
||||
}
|
||||
|
||||
if (!successFinalizing) {
|
||||
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, MaxFinalizationAttempts));
|
||||
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.success) {
|
||||
EResult result = (EResult) response.status;
|
||||
|
||||
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, result));
|
||||
}
|
||||
|
||||
if (!bot.TryImportAuthenticator(mobileAuthenticator)) {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
@@ -61,7 +63,6 @@ internal sealed class MobileAuthenticatorHandler : ClientMsgHandler {
|
||||
authenticator_type = 1,
|
||||
authenticator_time = Utilities.GetUnixTime(),
|
||||
device_identifier = deviceID,
|
||||
sms_phone_id = "1",
|
||||
steamid = steamID
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
@@ -41,7 +43,7 @@ namespace ArchiSteamFarm.OfficialPlugins.MobileAuthenticator;
|
||||
internal sealed class MobileAuthenticatorPlugin : OfficialPlugin, IBotCommand2, IBotSteamClient {
|
||||
[JsonInclude]
|
||||
[Required]
|
||||
public override string Name => nameof(MobileAuthenticatorPlugin);
|
||||
public override string Name => typeof(MobileAuthenticatorPlugin).Assembly.GetName().Name ?? throw new InvalidOperationException(nameof(Name));
|
||||
|
||||
[JsonInclude]
|
||||
[Required]
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<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="OpenTelemetry.Exporter.Prometheus.AspNetCore" />
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" />
|
||||
<PackageReference Include="SteamKit2" IncludeAssets="compile" />
|
||||
<PackageReference Include="System.Composition.AttributedModel" IncludeAssets="compile" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ArchiSteamFarm\ArchiSteamFarm.csproj" ExcludeAssets="all" Private="false" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
26
ArchiSteamFarm.OfficialPlugins.Monitoring/AssemblyInfo.cs
Normal file
26
ArchiSteamFarm.OfficialPlugins.Monitoring/AssemblyInfo.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Ł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(true)]
|
||||
189
ArchiSteamFarm.OfficialPlugins.Monitoring/MonitoringPlugin.cs
Normal file
189
ArchiSteamFarm.OfficialPlugins.Monitoring/MonitoringPlugin.cs
Normal file
@@ -0,0 +1,189 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// |
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// |
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Composition;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Core;
|
||||
using ArchiSteamFarm.IPC.Integration;
|
||||
using ArchiSteamFarm.Plugins;
|
||||
using ArchiSteamFarm.Plugins.Interfaces;
|
||||
using ArchiSteamFarm.Steam;
|
||||
using ArchiSteamFarm.Storage;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using OpenTelemetry.Metrics;
|
||||
using SteamKit2;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.Monitoring;
|
||||
|
||||
[Export(typeof(IPlugin))]
|
||||
[SuppressMessage("ReSharper", "MemberCanBeFileLocal")]
|
||||
internal sealed class MonitoringPlugin : OfficialPlugin, IWebServiceProvider, IGitHubPluginUpdates, IDisposable {
|
||||
private const string MeterName = SharedInfo.AssemblyName;
|
||||
|
||||
private const string MetricNamePrefix = "asf";
|
||||
|
||||
private static bool Enabled => ASF.GlobalConfig?.IPC ?? GlobalConfig.DefaultIPC;
|
||||
|
||||
[JsonInclude]
|
||||
[Required]
|
||||
public override string Name => typeof(MonitoringPlugin).Assembly.GetName().Name ?? throw new InvalidOperationException(nameof(Name));
|
||||
|
||||
public string RepositoryName => SharedInfo.GithubRepo;
|
||||
|
||||
[JsonInclude]
|
||||
[Required]
|
||||
public override Version Version => typeof(MonitoringPlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version));
|
||||
|
||||
private Meter? Meter;
|
||||
|
||||
public void Dispose() => Meter?.Dispose();
|
||||
|
||||
public void OnConfiguringEndpoints(IApplicationBuilder app) {
|
||||
ArgumentNullException.ThrowIfNull(app);
|
||||
|
||||
if (!Enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
app.UseEndpoints(static builder => builder.MapPrometheusScrapingEndpoint());
|
||||
}
|
||||
|
||||
public void OnConfiguringServices(IServiceCollection services) {
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
|
||||
if (!Enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
InitializeMeter();
|
||||
|
||||
services.AddOpenTelemetry().WithMetrics(
|
||||
builder => {
|
||||
builder.AddPrometheusExporter(static config => config.ScrapeEndpointPath = "/Api/metrics");
|
||||
builder.AddRuntimeInstrumentation();
|
||||
builder.AddAspNetCoreInstrumentation();
|
||||
builder.AddHttpClientInstrumentation();
|
||||
builder.AddMeter(Meter.Name);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public override Task OnLoaded() => Task.CompletedTask;
|
||||
|
||||
[MemberNotNull(nameof(Meter))]
|
||||
private void InitializeMeter() {
|
||||
if (Meter != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Meter = new Meter(MeterName, Version.ToString());
|
||||
|
||||
Meter.CreateObservableGauge(
|
||||
$"{MetricNamePrefix}_ipc_banned_ips",
|
||||
static () => ApiAuthenticationMiddleware.GetCurrentlyBannedIPs().Count(),
|
||||
description: "Number of IP addresses currently banned by ASFs IPC module"
|
||||
);
|
||||
|
||||
Meter.CreateObservableGauge(
|
||||
$"{MetricNamePrefix}_active_plugins",
|
||||
static () => PluginsCore.ActivePluginsCount,
|
||||
description: "Number of plugins currently loaded in ASF"
|
||||
);
|
||||
|
||||
Meter.CreateObservableGauge(
|
||||
$"{MetricNamePrefix}_bots", static () => {
|
||||
ICollection<Bot> bots = Bot.Bots?.Values ?? Array.Empty<Bot>();
|
||||
|
||||
return new List<Measurement<int>>(4) {
|
||||
new(bots.Count, new KeyValuePair<string, object?>(TagNames.BotState, "configured")),
|
||||
new(bots.Count(static bot => bot.IsConnectedAndLoggedOn), new KeyValuePair<string, object?>(TagNames.BotState, "online")),
|
||||
new(bots.Count(static bot => !bot.IsConnectedAndLoggedOn), new KeyValuePair<string, object?>(TagNames.BotState, "offline")),
|
||||
new(bots.Count(static bot => bot.CardsFarmer.NowFarming), new KeyValuePair<string, object?>(TagNames.BotState, "farming"))
|
||||
};
|
||||
},
|
||||
description: "Number of bots that are currently loaded in ASF"
|
||||
);
|
||||
|
||||
Meter.CreateObservableGauge(
|
||||
$"{MetricNamePrefix}_bot_friends", static () => {
|
||||
ICollection<Bot> bots = Bot.Bots?.Values ?? Array.Empty<Bot>();
|
||||
|
||||
return bots.Where(static bot => bot.IsConnectedAndLoggedOn).Select(static bot => new Measurement<int>(bot.SteamFriends.GetFriendCount(), new KeyValuePair<string, object?>(TagNames.BotName, bot.BotName), new KeyValuePair<string, object?>(TagNames.SteamID, bot.SteamID)));
|
||||
},
|
||||
description: "Number of friends each bot has on Steam"
|
||||
);
|
||||
|
||||
Meter.CreateObservableGauge(
|
||||
$"{MetricNamePrefix}_bot_clans", static () => {
|
||||
ICollection<Bot> bots = Bot.Bots?.Values ?? Array.Empty<Bot>();
|
||||
|
||||
return bots.Where(static bot => bot.IsConnectedAndLoggedOn).Select(static bot => new Measurement<int>(bot.SteamFriends.GetClanCount(), new KeyValuePair<string, object?>(TagNames.BotName, bot.BotName), new KeyValuePair<string, object?>(TagNames.SteamID, bot.SteamID)));
|
||||
},
|
||||
description: "Number of Steam groups each bot is in"
|
||||
);
|
||||
|
||||
Meter.CreateObservableGauge(
|
||||
$"{MetricNamePrefix}_bot_farming_minutes_remaining", static () => {
|
||||
ICollection<Bot> bots = Bot.Bots?.Values ?? Array.Empty<Bot>();
|
||||
|
||||
return bots.Select(static bot => new Measurement<double>(bot.CardsFarmer.TimeRemaining.TotalMinutes, new KeyValuePair<string, object?>(TagNames.BotName, bot.BotName), new KeyValuePair<string, object?>(TagNames.SteamID, bot.SteamID)));
|
||||
},
|
||||
description: "Approximate number of minutes remaining until each bot has finished farming all cards"
|
||||
);
|
||||
|
||||
Meter.CreateObservableGauge(
|
||||
$"{MetricNamePrefix}_bot_heartbeat_failures", static () => {
|
||||
ICollection<Bot> bots = Bot.Bots?.Values ?? Array.Empty<Bot>();
|
||||
|
||||
return bots.Select(static bot => new Measurement<byte>(bot.HeartBeatFailures, new KeyValuePair<string, object?>(TagNames.BotName, bot.BotName), new KeyValuePair<string, object?>(TagNames.SteamID, bot.SteamID)));
|
||||
},
|
||||
description: "Number of times a bot has failed to reach Steam servers"
|
||||
);
|
||||
|
||||
Meter.CreateObservableGauge(
|
||||
$"{MetricNamePrefix}_bot_wallet_balance", static () => {
|
||||
ICollection<Bot> bots = Bot.Bots?.Values ?? Array.Empty<Bot>();
|
||||
|
||||
return bots.Where(static bot => bot.WalletCurrency != ECurrencyCode.Invalid).Select(static bot => new Measurement<long>(bot.WalletBalance, new KeyValuePair<string, object?>(TagNames.BotName, bot.BotName), new KeyValuePair<string, object?>(TagNames.SteamID, bot.SteamID), new KeyValuePair<string, object?>(TagNames.CurrencyCode, bot.WalletCurrency.ToString())));
|
||||
},
|
||||
description: "Current Steam wallet balance of each bot"
|
||||
);
|
||||
|
||||
Meter.CreateObservableGauge(
|
||||
$"{MetricNamePrefix}_bot_bgr_keys_remaining", static () => {
|
||||
ICollection<Bot> bots = Bot.Bots?.Values ?? Array.Empty<Bot>();
|
||||
|
||||
return bots.Select(static bot => new Measurement<long>(bot.GamesToRedeemInBackgroundCount, new KeyValuePair<string, object?>(TagNames.BotName, bot.BotName), new KeyValuePair<string, object?>(TagNames.SteamID, bot.SteamID)));
|
||||
},
|
||||
description: "Remaining games to redeem in background per bot"
|
||||
);
|
||||
}
|
||||
}
|
||||
31
ArchiSteamFarm.OfficialPlugins.Monitoring/TagNames.cs
Normal file
31
ArchiSteamFarm.OfficialPlugins.Monitoring/TagNames.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Ł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.
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.Monitoring;
|
||||
|
||||
internal static class TagNames {
|
||||
internal const string BotName = "bot";
|
||||
internal const string BotState = "state";
|
||||
internal const string CurrencyCode = "currency";
|
||||
internal const string SteamID = "steamid";
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
@@ -21,21 +23,17 @@
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json.Serialization;
|
||||
using ArchiSteamFarm.Steam.Data;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.Data;
|
||||
|
||||
#pragma warning disable CA1812 // False positive, the class is used during json deserialization
|
||||
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
|
||||
internal sealed class SubmitResponse {
|
||||
internal sealed class SubmitResponse : BooleanResponse {
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("data")]
|
||||
internal SubmitResponseData? Data { get; private init; }
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("success")]
|
||||
[JsonRequired]
|
||||
internal bool Success { get; private init; }
|
||||
|
||||
[JsonConstructor]
|
||||
private SubmitResponse() { }
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
@@ -29,31 +31,31 @@ internal sealed class SubmitResponseData {
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("new_apps")]
|
||||
[JsonRequired]
|
||||
internal ImmutableHashSet<uint> NewApps { get; private init; } = ImmutableHashSet<uint>.Empty;
|
||||
internal ImmutableHashSet<uint> NewApps { get; private init; } = [];
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("new_depots")]
|
||||
[JsonRequired]
|
||||
internal ImmutableHashSet<uint> NewDepots { get; private init; } = ImmutableHashSet<uint>.Empty;
|
||||
internal ImmutableHashSet<uint> NewDepots { get; private init; } = [];
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("new_subs")]
|
||||
[JsonRequired]
|
||||
internal ImmutableHashSet<uint> NewPackages { get; private init; } = ImmutableHashSet<uint>.Empty;
|
||||
internal ImmutableHashSet<uint> NewPackages { get; private init; } = [];
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("verified_apps")]
|
||||
[JsonRequired]
|
||||
internal ImmutableHashSet<uint> VerifiedApps { get; private init; } = ImmutableHashSet<uint>.Empty;
|
||||
internal ImmutableHashSet<uint> VerifiedApps { get; private init; } = [];
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("verified_depots")]
|
||||
[JsonRequired]
|
||||
internal ImmutableHashSet<uint> VerifiedDepots { get; private init; } = ImmutableHashSet<uint>.Empty;
|
||||
internal ImmutableHashSet<uint> VerifiedDepots { get; private init; } = [];
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("verified_subs")]
|
||||
[JsonRequired]
|
||||
internal ImmutableHashSet<uint> VerifiedPackages { get; private init; } = ImmutableHashSet<uint>.Empty;
|
||||
internal ImmutableHashSet<uint> VerifiedPackages { get; private init; } = [];
|
||||
}
|
||||
#pragma warning restore CA1812 // False positive, the class is used during json deserialization
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>{0} de {1} chaves do depósito recuperadas com sucesso.</value>
|
||||
<value>{0} de {1} chaves de depots 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">
|
||||
|
||||
@@ -168,9 +168,9 @@
|
||||
<value>STD genel önbelleği yükleniyor...</value>
|
||||
</data>
|
||||
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
|
||||
<value>STD genel önbellek bütünlüğünü doğrulama...</value>
|
||||
<value>STD genel önbellek bütünlüğünü doğruluyor...</value>
|
||||
</data>
|
||||
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
|
||||
<value>STD genel önbellek bütünlüğü doğrulanamadı. Bu, olası bir dosya / bellek bozulmasına işaret eder, bunun yerine yeni bir örnek başlatılır.</value>
|
||||
<value>STD genel önbellek bütünlüğü doğrulanamadı. Bu, olası bir dosya/bellek bozulmasına işaret eder, bunun yerine yeni bir örnek başlatılır.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -97,24 +97,80 @@
|
||||
<value>Закінчено отримання всього {0} токенів доступу.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of app access tokens retrieved</comment>
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<data name="BotRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Отримання всіх депо для загалом {0} програм...</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingAppInfos" xml:space="preserve">
|
||||
<value>Отримання {0} інформації про програму...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingAppInfos" xml:space="preserve">
|
||||
<value>Завершено отримання {0} інформації про програму.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
|
||||
<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>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
</data>
|
||||
<data name="SubmissionNoNewData" xml:space="preserve">
|
||||
<value>Ніяких нових даних подавати не потрібно, все актуально.</value>
|
||||
</data>
|
||||
<data name="SubmissionNoContributorSet" xml:space="preserve">
|
||||
<value>Не вдалося надіслати дані, оскільки немає дійсного набору SteamID, який ми могли б класифікувати як дописувача. Подумайте про встановлення властивості {0}.</value>
|
||||
<comment>{0} will be replaced by the name of the config property (e.g. "SteamOwnerID") that the user is expected to set</comment>
|
||||
</data>
|
||||
<data name="SubmissionInProgress" xml:space="preserve">
|
||||
<value>Надіслати загальну кількість зареєстрованих програм/пакетів/депо: {0}/{1}/{2}...</value>
|
||||
<comment>{0} will be replaced by the number of app access tokens being submitted, {1} will be replaced by the number of package access tokens being submitted, {2} will be replaced by the number of depot keys being submitted</comment>
|
||||
</data>
|
||||
<data name="SubmissionFailedTooManyRequests" xml:space="preserve">
|
||||
<value>Відправлення не вдалося через занадто велику кількість надісланих запитів, ми спробуємо ще раз приблизно через {0} відтепер.</value>
|
||||
<comment>{0} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessful" xml:space="preserve">
|
||||
<value>Дані успішно надіслано. На сервері зареєстровано всього нових програм/пакунків/сховищ: {0} ({1} перевірено)/{2} ({3} перевірено)/{4} ({5} перевірено).</value>
|
||||
<comment>{0} will be replaced by the number of new app access tokens that the server has registered, {1} will be replaced by the number of verified app access tokens that the server has registered, {2} will be replaced by the number of new package access tokens that the server has registered, {3} will be replaced by the number of verified package access tokens that the server has registered, {4} will be replaced by the number of new depot keys that the server has registered, {5} will be replaced by the number of verified depot keys that the server has registered</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulNewApps" xml:space="preserve">
|
||||
<value>Нові програми: {0}</value>
|
||||
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulVerifiedApps" xml:space="preserve">
|
||||
<value>Перевірених програм: {0}</value>
|
||||
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulNewPackages" xml:space="preserve">
|
||||
<value>Нові пакунки: {0}</value>
|
||||
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulVerifiedPackages" xml:space="preserve">
|
||||
<value>Перевірені пакунки: {0}</value>
|
||||
<comment>{0} will be replaced by list of the packages (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulNewDepots" xml:space="preserve">
|
||||
<value>Нові склади: {0}</value>
|
||||
<comment>{0} will be replaced by list of the depots (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="SubmissionSuccessfulVerifiedDepots" xml:space="preserve">
|
||||
<value>Перевірені склади: {0}</value>
|
||||
<comment>{0} will be replaced by list of the depots (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="PluginSecretListInitialized" xml:space="preserve">
|
||||
<value>{0} ініціалізовано, плагін не буде вирішувати жодного з них: {1}.</value>
|
||||
<comment>{0} will be replaced by the name of the config property (e.g. "SecretPackageIDs"), {1} will be replaced by list of the objects (IDs, numbers), separated by a comma</comment>
|
||||
</data>
|
||||
<data name="LoadingGlobalCache" xml:space="preserve">
|
||||
<value>Завантаження глобального кешу STD...</value>
|
||||
</data>
|
||||
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
|
||||
<value>Перевірка цілісності глобального кешу STD...</value>
|
||||
</data>
|
||||
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
|
||||
<value>Не вдалося перевірити цілісність глобального кешу STD. Це свідчить про можливе пошкодження файлу/пам'яті, замість нього буде ініціалізовано новий екземпляр.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
@@ -33,17 +35,17 @@ public sealed class SteamTokenDumperConfig {
|
||||
[JsonDisallowNull]
|
||||
[JsonInclude]
|
||||
[SwaggerItemsMinMax(MinimumUint = 1, MaximumUint = uint.MaxValue)]
|
||||
public ImmutableHashSet<uint> SecretAppIDs { get; private init; } = ImmutableHashSet<uint>.Empty;
|
||||
public ImmutableHashSet<uint> SecretAppIDs { get; private init; } = [];
|
||||
|
||||
[JsonDisallowNull]
|
||||
[JsonInclude]
|
||||
[SwaggerItemsMinMax(MinimumUint = 1, MaximumUint = uint.MaxValue)]
|
||||
public ImmutableHashSet<uint> SecretDepotIDs { get; private init; } = ImmutableHashSet<uint>.Empty;
|
||||
public ImmutableHashSet<uint> SecretDepotIDs { get; private init; } = [];
|
||||
|
||||
[JsonDisallowNull]
|
||||
[JsonInclude]
|
||||
[SwaggerItemsMinMax(MinimumUint = 1, MaximumUint = uint.MaxValue)]
|
||||
public ImmutableHashSet<uint> SecretPackageIDs { get; private init; } = ImmutableHashSet<uint>.Empty;
|
||||
public ImmutableHashSet<uint> SecretPackageIDs { get; private init; } = [];
|
||||
|
||||
[JsonInclude]
|
||||
public bool SkipAutoGrantPackages { get; private init; } = true;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
@@ -43,7 +45,7 @@ public sealed class Bot {
|
||||
|
||||
foreach ((uint appID, byte cards) in itemsPerSet) {
|
||||
for (byte i = 1; i <= cards; i++) {
|
||||
items.Add(CreateCard(i, appID));
|
||||
items.Add(CreateCard(i, realAppID: appID));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,8 +61,8 @@ public sealed class Bot {
|
||||
const uint appID = 42;
|
||||
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, appID),
|
||||
CreateCard(2, appID)
|
||||
CreateCard(1, realAppID: appID),
|
||||
CreateCard(2, realAppID: appID)
|
||||
];
|
||||
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => GetItemsForFullBadge(items, 2, appID, MinCardsPerBadge - 1));
|
||||
@@ -71,10 +73,10 @@ public sealed class Bot {
|
||||
const uint appID = 42;
|
||||
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, appID),
|
||||
CreateCard(1, appID),
|
||||
CreateCard(2, appID),
|
||||
CreateCard(3, appID)
|
||||
CreateCard(1, realAppID: appID),
|
||||
CreateCard(1, realAppID: appID),
|
||||
CreateCard(2, realAppID: appID),
|
||||
CreateCard(3, realAppID: appID)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 3, appID);
|
||||
@@ -93,10 +95,10 @@ public sealed class Bot {
|
||||
const uint appID = 42;
|
||||
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, appID),
|
||||
CreateCard(1, appID),
|
||||
CreateCard(2, appID),
|
||||
CreateCard(2, appID)
|
||||
CreateCard(1, realAppID: appID),
|
||||
CreateCard(1, realAppID: appID),
|
||||
CreateCard(2, realAppID: appID),
|
||||
CreateCard(2, realAppID: appID)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
|
||||
@@ -114,9 +116,9 @@ public sealed class Bot {
|
||||
const uint appID = 42;
|
||||
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, appID, 2),
|
||||
CreateCard(2, appID),
|
||||
CreateCard(2, appID)
|
||||
CreateCard(1, amount: 2, realAppID: appID),
|
||||
CreateCard(2, realAppID: appID),
|
||||
CreateCard(2, realAppID: appID)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
|
||||
@@ -134,27 +136,27 @@ public sealed class Bot {
|
||||
const uint appID = 42;
|
||||
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, appID, type: Asset.EType.TradingCard, rarity: Asset.ERarity.Common),
|
||||
CreateCard(2, appID, type: Asset.EType.TradingCard, rarity: Asset.ERarity.Common),
|
||||
CreateCard(1, realAppID: appID, type: EAssetType.TradingCard, rarity: EAssetRarity.Common),
|
||||
CreateCard(2, realAppID: appID, type: EAssetType.TradingCard, rarity: EAssetRarity.Common),
|
||||
|
||||
CreateCard(1, appID, type: Asset.EType.FoilTradingCard, rarity: Asset.ERarity.Uncommon),
|
||||
CreateCard(2, appID, type: Asset.EType.FoilTradingCard, rarity: Asset.ERarity.Uncommon),
|
||||
CreateCard(1, realAppID: appID, type: EAssetType.FoilTradingCard, rarity: EAssetRarity.Uncommon),
|
||||
CreateCard(2, realAppID: appID, type: EAssetType.FoilTradingCard, rarity: EAssetRarity.Uncommon),
|
||||
|
||||
CreateCard(1, appID, type: Asset.EType.FoilTradingCard, rarity: Asset.ERarity.Rare),
|
||||
CreateCard(2, appID, type: Asset.EType.FoilTradingCard, rarity: Asset.ERarity.Rare),
|
||||
CreateCard(1, realAppID: appID, type: EAssetType.FoilTradingCard, rarity: EAssetRarity.Rare),
|
||||
CreateCard(2, realAppID: appID, type: EAssetType.FoilTradingCard, rarity: EAssetRarity.Rare),
|
||||
|
||||
// for better readability and easier verification when thinking about this test the items that shall be selected for sending are the ones below this comment
|
||||
CreateCard(1, appID, type: Asset.EType.TradingCard, rarity: Asset.ERarity.Uncommon),
|
||||
CreateCard(2, appID, type: Asset.EType.TradingCard, rarity: Asset.ERarity.Uncommon),
|
||||
CreateCard(3, appID, type: Asset.EType.TradingCard, rarity: Asset.ERarity.Uncommon),
|
||||
CreateCard(1, realAppID: appID, type: EAssetType.TradingCard, rarity: EAssetRarity.Uncommon),
|
||||
CreateCard(2, realAppID: appID, type: EAssetType.TradingCard, rarity: EAssetRarity.Uncommon),
|
||||
CreateCard(3, realAppID: appID, type: EAssetType.TradingCard, rarity: EAssetRarity.Uncommon),
|
||||
|
||||
CreateCard(1, appID, type: Asset.EType.FoilTradingCard, rarity: Asset.ERarity.Common),
|
||||
CreateCard(3, appID, type: Asset.EType.FoilTradingCard, rarity: Asset.ERarity.Common),
|
||||
CreateCard(7, appID, type: Asset.EType.FoilTradingCard, rarity: Asset.ERarity.Common),
|
||||
CreateCard(1, realAppID: appID, type: EAssetType.FoilTradingCard, rarity: EAssetRarity.Common),
|
||||
CreateCard(3, realAppID: appID, type: EAssetType.FoilTradingCard, rarity: EAssetRarity.Common),
|
||||
CreateCard(7, realAppID: appID, type: EAssetType.FoilTradingCard, rarity: EAssetRarity.Common),
|
||||
|
||||
CreateCard(2, appID, type: Asset.EType.Unknown, rarity: Asset.ERarity.Rare),
|
||||
CreateCard(3, appID, type: Asset.EType.Unknown, rarity: Asset.ERarity.Rare),
|
||||
CreateCard(4, appID, type: Asset.EType.Unknown, rarity: Asset.ERarity.Rare)
|
||||
CreateCard(2, realAppID: appID, type: EAssetType.Unknown, rarity: EAssetRarity.Rare),
|
||||
CreateCard(3, realAppID: appID, type: EAssetType.Unknown, rarity: EAssetRarity.Rare),
|
||||
CreateCard(4, realAppID: appID, type: EAssetType.Unknown, rarity: EAssetRarity.Rare)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 3, appID);
|
||||
@@ -175,8 +177,8 @@ public sealed class Bot {
|
||||
const uint appID = 42;
|
||||
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, appID),
|
||||
CreateCard(2, appID)
|
||||
CreateCard(1, realAppID: appID),
|
||||
CreateCard(2, realAppID: appID)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 3, appID);
|
||||
@@ -190,8 +192,8 @@ public sealed class Bot {
|
||||
const uint appID = 42;
|
||||
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, appID),
|
||||
CreateCard(2, appID)
|
||||
CreateCard(1, realAppID: appID),
|
||||
CreateCard(2, realAppID: appID)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
|
||||
@@ -210,8 +212,8 @@ public sealed class Bot {
|
||||
const uint appID1 = 43;
|
||||
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, appID0),
|
||||
CreateCard(1, appID1)
|
||||
CreateCard(1, realAppID: appID0),
|
||||
CreateCard(1, realAppID: appID1)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(
|
||||
@@ -235,8 +237,8 @@ public sealed class Bot {
|
||||
const uint appID1 = 43;
|
||||
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, appID0),
|
||||
CreateCard(1, appID1)
|
||||
CreateCard(1, realAppID: appID0),
|
||||
CreateCard(1, realAppID: appID1)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(
|
||||
@@ -258,12 +260,12 @@ public sealed class Bot {
|
||||
const uint appID2 = 44;
|
||||
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, appID0),
|
||||
CreateCard(2, appID0),
|
||||
CreateCard(1, realAppID: appID0),
|
||||
CreateCard(2, realAppID: appID0),
|
||||
|
||||
CreateCard(1, appID1),
|
||||
CreateCard(2, appID1),
|
||||
CreateCard(3, appID1)
|
||||
CreateCard(1, realAppID: appID1),
|
||||
CreateCard(2, realAppID: appID1),
|
||||
CreateCard(3, realAppID: appID1)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(
|
||||
@@ -288,8 +290,8 @@ public sealed class Bot {
|
||||
const uint appID = 42;
|
||||
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, appID, rarity: Asset.ERarity.Common),
|
||||
CreateCard(1, appID, rarity: Asset.ERarity.Rare)
|
||||
CreateCard(1, realAppID: appID, rarity: EAssetRarity.Common),
|
||||
CreateCard(1, realAppID: appID, rarity: EAssetRarity.Rare)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 1, appID);
|
||||
@@ -306,8 +308,8 @@ public sealed class Bot {
|
||||
const uint appID = 42;
|
||||
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, appID, rarity: Asset.ERarity.Common),
|
||||
CreateCard(1, appID, rarity: Asset.ERarity.Rare)
|
||||
CreateCard(1, realAppID: appID, rarity: EAssetRarity.Common),
|
||||
CreateCard(1, realAppID: appID, rarity: EAssetRarity.Rare)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
|
||||
@@ -322,11 +324,11 @@ public sealed class Bot {
|
||||
const uint appID = 42;
|
||||
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, appID, rarity: Asset.ERarity.Common),
|
||||
CreateCard(2, appID, rarity: Asset.ERarity.Common),
|
||||
CreateCard(1, appID, rarity: Asset.ERarity.Uncommon),
|
||||
CreateCard(2, appID, rarity: Asset.ERarity.Uncommon),
|
||||
CreateCard(3, appID, rarity: Asset.ERarity.Uncommon)
|
||||
CreateCard(1, realAppID: appID, rarity: EAssetRarity.Common),
|
||||
CreateCard(2, realAppID: appID, rarity: EAssetRarity.Common),
|
||||
CreateCard(1, realAppID: appID, rarity: EAssetRarity.Uncommon),
|
||||
CreateCard(2, realAppID: appID, rarity: EAssetRarity.Uncommon),
|
||||
CreateCard(3, realAppID: appID, rarity: EAssetRarity.Uncommon)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 3, appID);
|
||||
@@ -345,8 +347,8 @@ public sealed class Bot {
|
||||
const uint appID = 42;
|
||||
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, appID, type: Asset.EType.TradingCard),
|
||||
CreateCard(1, appID, type: Asset.EType.FoilTradingCard)
|
||||
CreateCard(1, realAppID: appID, type: EAssetType.TradingCard),
|
||||
CreateCard(1, realAppID: appID, type: EAssetType.FoilTradingCard)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 1, appID);
|
||||
@@ -363,8 +365,8 @@ public sealed class Bot {
|
||||
const uint appID = 42;
|
||||
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, appID, type: Asset.EType.TradingCard),
|
||||
CreateCard(1, appID, type: Asset.EType.FoilTradingCard)
|
||||
CreateCard(1, realAppID: appID, type: EAssetType.TradingCard),
|
||||
CreateCard(1, realAppID: appID, type: EAssetType.FoilTradingCard)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
|
||||
@@ -379,11 +381,11 @@ public sealed class Bot {
|
||||
const uint appID = 42;
|
||||
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, appID, type: Asset.EType.TradingCard),
|
||||
CreateCard(2, appID, type: Asset.EType.TradingCard),
|
||||
CreateCard(1, appID, type: Asset.EType.FoilTradingCard),
|
||||
CreateCard(2, appID, type: Asset.EType.FoilTradingCard),
|
||||
CreateCard(3, appID, type: Asset.EType.FoilTradingCard)
|
||||
CreateCard(1, realAppID: appID, type: EAssetType.TradingCard),
|
||||
CreateCard(2, realAppID: appID, type: EAssetType.TradingCard),
|
||||
CreateCard(1, realAppID: appID, type: EAssetType.FoilTradingCard),
|
||||
CreateCard(2, realAppID: appID, type: EAssetType.FoilTradingCard),
|
||||
CreateCard(3, realAppID: appID, type: EAssetType.FoilTradingCard)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 3, appID);
|
||||
@@ -402,8 +404,8 @@ public sealed class Bot {
|
||||
const uint appID0 = 42;
|
||||
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, appID0, 2),
|
||||
CreateCard(2, appID0)
|
||||
CreateCard(1, amount: 2, realAppID: appID0),
|
||||
CreateCard(2, realAppID: appID0)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID0);
|
||||
@@ -423,8 +425,8 @@ public sealed class Bot {
|
||||
HashSet<Asset> items = [];
|
||||
|
||||
for (byte i = 0; i < Steam.Exchange.Trading.MaxItemsPerTrade; i++) {
|
||||
items.Add(CreateCard(1, appID));
|
||||
items.Add(CreateCard(2, appID));
|
||||
items.Add(CreateCard(1, realAppID: appID));
|
||||
items.Add(CreateCard(2, realAppID: appID));
|
||||
}
|
||||
|
||||
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
|
||||
@@ -440,10 +442,10 @@ public sealed class Bot {
|
||||
HashSet<Asset> items = [];
|
||||
|
||||
for (byte i = 0; i < 100; i++) {
|
||||
items.Add(CreateCard(1, appID0));
|
||||
items.Add(CreateCard(2, appID0));
|
||||
items.Add(CreateCard(1, appID1));
|
||||
items.Add(CreateCard(2, appID1));
|
||||
items.Add(CreateCard(1, realAppID: appID0));
|
||||
items.Add(CreateCard(2, realAppID: appID0));
|
||||
items.Add(CreateCard(1, realAppID: appID1));
|
||||
items.Add(CreateCard(2, realAppID: appID1));
|
||||
}
|
||||
|
||||
Dictionary<uint, byte> itemsPerSet = new() {
|
||||
@@ -463,10 +465,10 @@ public sealed class Bot {
|
||||
const uint appID2 = 44;
|
||||
|
||||
HashSet<Asset> items = [
|
||||
CreateCard(1, appID0),
|
||||
CreateCard(2, appID0),
|
||||
CreateCard(3, appID0),
|
||||
CreateCard(4, appID0)
|
||||
CreateCard(1, realAppID: appID0),
|
||||
CreateCard(2, realAppID: appID0),
|
||||
CreateCard(3, realAppID: appID0),
|
||||
CreateCard(4, realAppID: appID0)
|
||||
];
|
||||
|
||||
Assert.ThrowsException<InvalidOperationException>(
|
||||
@@ -489,12 +491,12 @@ public sealed class Bot {
|
||||
Assert.IsTrue(expectedResult.All(expectation => realResult.TryGetValue(expectation.Key, out long reality) && (expectation.Value == reality)));
|
||||
}
|
||||
|
||||
private static Asset CreateCard(ulong classID, uint realAppID, uint amount = 1, Asset.EType type = Asset.EType.TradingCard, Asset.ERarity rarity = Asset.ERarity.Common) => new(Asset.SteamAppID, Asset.SteamCommunityContextID, classID, amount, realAppID: realAppID, type: type, rarity: rarity);
|
||||
private static Asset CreateCard(ulong classID, ulong instanceID = 0, uint amount = 1, bool marketable = false, bool tradable = false, uint realAppID = Asset.SteamAppID, EAssetType type = EAssetType.TradingCard, EAssetRarity rarity = EAssetRarity.Common) => new(Asset.SteamAppID, Asset.SteamCommunityContextID, classID, amount, new InventoryDescription(Asset.SteamAppID, classID, instanceID, marketable, tradable, realAppID, type, rarity));
|
||||
|
||||
private static HashSet<Asset> GetItemsForFullBadge(IReadOnlyCollection<Asset> inventory, byte cardsPerSet, uint appID, ushort maxItems = Steam.Exchange.Trading.MaxItemsPerTrade) => GetItemsForFullBadge(inventory, new Dictionary<uint, byte> { { appID, cardsPerSet } }, maxItems);
|
||||
|
||||
private static HashSet<Asset> GetItemsForFullBadge(IReadOnlyCollection<Asset> inventory, IDictionary<uint, byte> cardsPerSet, ushort maxItems = Steam.Exchange.Trading.MaxItemsPerTrade) {
|
||||
Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), List<uint>> inventorySets = Steam.Exchange.Trading.GetInventorySets(inventory);
|
||||
Dictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), List<uint>> inventorySets = Steam.Exchange.Trading.GetInventorySets(inventory);
|
||||
|
||||
return GetItemsForFullSets(inventory, inventorySets.ToDictionary(static kv => kv.Key, kv => (SetsToExtract: inventorySets[kv.Key][0], cardsPerSet[kv.Key.RealAppID])), maxItems).ToHashSet();
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
@@ -31,18 +33,18 @@ public sealed class Trading {
|
||||
[TestMethod]
|
||||
public void ExploitingNewSetsIsFairButNotNeutral() {
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1, 40),
|
||||
CreateItem(2, 10),
|
||||
CreateItem(3, 10)
|
||||
CreateItem(1, amount: 40),
|
||||
CreateItem(2, amount: 10),
|
||||
CreateItem(3, amount: 10)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(2, 5),
|
||||
CreateItem(3, 5)
|
||||
CreateItem(2, amount: 5),
|
||||
CreateItem(3, amount: 5)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(1, 9),
|
||||
CreateItem(1, amount: 9),
|
||||
CreateItem(4)
|
||||
];
|
||||
|
||||
@@ -52,24 +54,39 @@ public sealed class Trading {
|
||||
|
||||
[TestMethod]
|
||||
public void MismatchRarityIsNotFair() {
|
||||
HashSet<Asset> itemsToGive = [CreateItem(1, rarity: Asset.ERarity.Rare)];
|
||||
HashSet<Asset> itemsToReceive = [CreateItem(2)];
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(1, rarity: EAssetRarity.Rare)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(2)
|
||||
];
|
||||
|
||||
Assert.IsFalse(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MismatchRealAppIDsIsNotFair() {
|
||||
HashSet<Asset> itemsToGive = [CreateItem(1, realAppID: 570)];
|
||||
HashSet<Asset> itemsToReceive = [CreateItem(2)];
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(1, realAppID: 570)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(2)
|
||||
];
|
||||
|
||||
Assert.IsFalse(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MismatchTypesIsNotFair() {
|
||||
HashSet<Asset> itemsToGive = [CreateItem(1, type: Asset.EType.Emoticon)];
|
||||
HashSet<Asset> itemsToReceive = [CreateItem(2)];
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(1, type: EAssetType.Emoticon)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(2)
|
||||
];
|
||||
|
||||
Assert.IsFalse(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
}
|
||||
@@ -77,19 +94,19 @@ public sealed class Trading {
|
||||
[TestMethod]
|
||||
public void MultiGameMultiTypeBadReject() {
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1, 9),
|
||||
CreateItem(3, 9, 730, Asset.EType.Emoticon),
|
||||
CreateItem(4, realAppID: 730, type: Asset.EType.Emoticon)
|
||||
CreateItem(1, amount: 9),
|
||||
CreateItem(3, amount: 9, realAppID: 730, type: EAssetType.Emoticon),
|
||||
CreateItem(4, realAppID: 730, type: EAssetType.Emoticon)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(1),
|
||||
CreateItem(4, realAppID: 730, type: Asset.EType.Emoticon)
|
||||
CreateItem(4, realAppID: 730, type: EAssetType.Emoticon)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(2),
|
||||
CreateItem(3, realAppID: 730, type: Asset.EType.Emoticon)
|
||||
CreateItem(3, realAppID: 730, type: EAssetType.Emoticon)
|
||||
];
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
@@ -99,18 +116,18 @@ public sealed class Trading {
|
||||
[TestMethod]
|
||||
public void MultiGameMultiTypeNeutralAccept() {
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1, 9),
|
||||
CreateItem(3, realAppID: 730, type: Asset.EType.Emoticon)
|
||||
CreateItem(1, amount: 9),
|
||||
CreateItem(3, realAppID: 730, type: EAssetType.Emoticon)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(1),
|
||||
CreateItem(3, realAppID: 730, type: Asset.EType.Emoticon)
|
||||
CreateItem(3, realAppID: 730, type: EAssetType.Emoticon)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(2),
|
||||
CreateItem(4, realAppID: 730, type: Asset.EType.Emoticon)
|
||||
CreateItem(4, realAppID: 730, type: EAssetType.Emoticon)
|
||||
];
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
@@ -120,7 +137,7 @@ public sealed class Trading {
|
||||
[TestMethod]
|
||||
public void MultiGameSingleTypeBadReject() {
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1, 9),
|
||||
CreateItem(1, amount: 9),
|
||||
CreateItem(3, realAppID: 730),
|
||||
CreateItem(4, realAppID: 730)
|
||||
];
|
||||
@@ -142,7 +159,7 @@ public sealed class Trading {
|
||||
[TestMethod]
|
||||
public void MultiGameSingleTypeNeutralAccept() {
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1, 2),
|
||||
CreateItem(1, amount: 2),
|
||||
CreateItem(3, realAppID: 730)
|
||||
];
|
||||
|
||||
@@ -164,15 +181,19 @@ public sealed class Trading {
|
||||
public void SingleGameAbrynosWasWrongNeutralAccept() {
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1),
|
||||
CreateItem(2, 2),
|
||||
CreateItem(2, amount: 2),
|
||||
CreateItem(3),
|
||||
CreateItem(4),
|
||||
CreateItem(5)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = [CreateItem(2)];
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(2)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = [CreateItem(3)];
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(3)
|
||||
];
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
|
||||
@@ -180,13 +201,17 @@ public sealed class Trading {
|
||||
|
||||
[TestMethod]
|
||||
public void SingleGameDonationAccept() {
|
||||
HashSet<Asset> inventory = [CreateItem(1)];
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = [CreateItem(1)];
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(1)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(2),
|
||||
CreateItem(3, type: Asset.EType.SteamGems)
|
||||
CreateItem(3, type: EAssetType.SteamGems)
|
||||
];
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
@@ -196,19 +221,19 @@ public sealed class Trading {
|
||||
[TestMethod]
|
||||
public void SingleGameMultiTypeBadReject() {
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1, 9),
|
||||
CreateItem(3, 9, type: Asset.EType.Emoticon),
|
||||
CreateItem(4, type: Asset.EType.Emoticon)
|
||||
CreateItem(1, amount: 9),
|
||||
CreateItem(3, amount: 9, type: EAssetType.Emoticon),
|
||||
CreateItem(4, type: EAssetType.Emoticon)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(1),
|
||||
CreateItem(4, type: Asset.EType.Emoticon)
|
||||
CreateItem(4, type: EAssetType.Emoticon)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(2),
|
||||
CreateItem(3, type: Asset.EType.Emoticon)
|
||||
CreateItem(3, type: EAssetType.Emoticon)
|
||||
];
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
@@ -218,18 +243,18 @@ public sealed class Trading {
|
||||
[TestMethod]
|
||||
public void SingleGameMultiTypeNeutralAccept() {
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1, 9),
|
||||
CreateItem(3, type: Asset.EType.Emoticon)
|
||||
CreateItem(1, amount: 9),
|
||||
CreateItem(3, type: EAssetType.Emoticon)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(1),
|
||||
CreateItem(3, type: Asset.EType.Emoticon)
|
||||
CreateItem(3, type: EAssetType.Emoticon)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(2),
|
||||
CreateItem(4, type: Asset.EType.Emoticon)
|
||||
CreateItem(4, type: EAssetType.Emoticon)
|
||||
];
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
@@ -250,7 +275,9 @@ public sealed class Trading {
|
||||
CreateItem(3)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = [CreateItem(4, 3)];
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(4, amount: 3)
|
||||
];
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
|
||||
@@ -260,15 +287,17 @@ public sealed class Trading {
|
||||
public void SingleGameQuantityBadReject2() {
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1),
|
||||
CreateItem(2, 2)
|
||||
CreateItem(2, amount: 2)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(1),
|
||||
CreateItem(2, 2)
|
||||
CreateItem(2, amount: 2)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = [CreateItem(3, 3)];
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(3, amount: 3)
|
||||
];
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
|
||||
@@ -277,7 +306,7 @@ public sealed class Trading {
|
||||
[TestMethod]
|
||||
public void SingleGameQuantityNeutralAccept() {
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1, 2),
|
||||
CreateItem(1, amount: 2),
|
||||
CreateItem(2)
|
||||
];
|
||||
|
||||
@@ -286,7 +315,9 @@ public sealed class Trading {
|
||||
CreateItem(2)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = [CreateItem(3, 2)];
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(3, amount: 2)
|
||||
];
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
|
||||
@@ -299,8 +330,13 @@ public sealed class Trading {
|
||||
CreateItem(2)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = [CreateItem(1)];
|
||||
HashSet<Asset> itemsToReceive = [CreateItem(2)];
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(1)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(2)
|
||||
];
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
|
||||
@@ -309,12 +345,14 @@ public sealed class Trading {
|
||||
[TestMethod]
|
||||
public void SingleGameSingleTypeBadWithOverpayingReject() {
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1, 2),
|
||||
CreateItem(2, 2),
|
||||
CreateItem(3, 2)
|
||||
CreateItem(1, amount: 2),
|
||||
CreateItem(2, amount: 2),
|
||||
CreateItem(3, amount: 2)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = [CreateItem(2)];
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(2)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(1),
|
||||
@@ -329,12 +367,17 @@ public sealed class Trading {
|
||||
public void SingleGameSingleTypeBigDifferenceAccept() {
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1),
|
||||
CreateItem(2, 5),
|
||||
CreateItem(2, amount: 5),
|
||||
CreateItem(3)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = [CreateItem(2)];
|
||||
HashSet<Asset> itemsToReceive = [CreateItem(3)];
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(2)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(3)
|
||||
];
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
|
||||
@@ -344,10 +387,10 @@ public sealed class Trading {
|
||||
public void SingleGameSingleTypeBigDifferenceReject() {
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1),
|
||||
CreateItem(2, 2),
|
||||
CreateItem(3, 2),
|
||||
CreateItem(4, 3),
|
||||
CreateItem(5, 10)
|
||||
CreateItem(2, amount: 2),
|
||||
CreateItem(3, amount: 2),
|
||||
CreateItem(4, amount: 3),
|
||||
CreateItem(5, amount: 10)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = [
|
||||
@@ -366,9 +409,17 @@ public sealed class Trading {
|
||||
|
||||
[TestMethod]
|
||||
public void SingleGameSingleTypeGoodAccept() {
|
||||
HashSet<Asset> inventory = [CreateItem(1, 2)];
|
||||
HashSet<Asset> itemsToGive = [CreateItem(1)];
|
||||
HashSet<Asset> itemsToReceive = [CreateItem(2)];
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1, amount: 2)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(1)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(2)
|
||||
];
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
|
||||
@@ -376,9 +427,17 @@ public sealed class Trading {
|
||||
|
||||
[TestMethod]
|
||||
public void SingleGameSingleTypeNeutralAccept() {
|
||||
HashSet<Asset> inventory = [CreateItem(1)];
|
||||
HashSet<Asset> itemsToGive = [CreateItem(1)];
|
||||
HashSet<Asset> itemsToReceive = [CreateItem(2)];
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(1)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(2)
|
||||
];
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
|
||||
@@ -387,11 +446,13 @@ public sealed class Trading {
|
||||
[TestMethod]
|
||||
public void SingleGameSingleTypeNeutralWithOverpayingAccept() {
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1, 2),
|
||||
CreateItem(2, 2)
|
||||
CreateItem(1, amount: 2),
|
||||
CreateItem(2, amount: 2)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = [CreateItem(2)];
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(2)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(1),
|
||||
@@ -405,26 +466,28 @@ public sealed class Trading {
|
||||
[TestMethod]
|
||||
public void TakingExcessiveAmountOfSingleCardCanStillBeFairAndNeutral() {
|
||||
HashSet<Asset> inventory = [
|
||||
CreateItem(1, 52),
|
||||
CreateItem(2, 73),
|
||||
CreateItem(3, 52),
|
||||
CreateItem(4, 47),
|
||||
CreateItem(1, amount: 52),
|
||||
CreateItem(2, amount: 73),
|
||||
CreateItem(3, amount: 52),
|
||||
CreateItem(4, amount: 47),
|
||||
CreateItem(5)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToGive = [CreateItem(2, 73)];
|
||||
HashSet<Asset> itemsToGive = [
|
||||
CreateItem(2, amount: 73)
|
||||
];
|
||||
|
||||
HashSet<Asset> itemsToReceive = [
|
||||
CreateItem(1, 9),
|
||||
CreateItem(3, 9),
|
||||
CreateItem(4, 8),
|
||||
CreateItem(5, 24),
|
||||
CreateItem(6, 23)
|
||||
CreateItem(1, amount: 9),
|
||||
CreateItem(3, amount: 9),
|
||||
CreateItem(4, amount: 8),
|
||||
CreateItem(5, amount: 24),
|
||||
CreateItem(6, amount: 23)
|
||||
];
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
|
||||
}
|
||||
|
||||
private static Asset CreateItem(ulong classID, uint amount = 1, uint realAppID = Asset.SteamAppID, Asset.EType type = Asset.EType.TradingCard, Asset.ERarity rarity = Asset.ERarity.Common) => new(Asset.SteamAppID, Asset.SteamCommunityContextID, classID, amount, realAppID: realAppID, type: type, rarity: rarity);
|
||||
private static Asset CreateItem(ulong classID, ulong instanceID = 0, uint amount = 1, bool marketable = false, bool tradable = false, uint realAppID = Asset.SteamAppID, EAssetType type = EAssetType.TradingCard, EAssetRarity rarity = EAssetRarity.Common) => new(Asset.SteamAppID, Asset.SteamCommunityContextID, classID, amount, new InventoryDescription(Asset.SteamAppID, classID, instanceID, marketable, tradable, realAppID, type, rarity));
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// _ _ _ ____ _ _____
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -19,6 +19,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArchiSteamFarm.CustomPlugin
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArchiSteamFarm.OfficialPlugins.MobileAuthenticator", "ArchiSteamFarm.OfficialPlugins.MobileAuthenticator\ArchiSteamFarm.OfficialPlugins.MobileAuthenticator.csproj", "{8D85BCCA-4DE6-4CC0-B015-E2E89E8E8AA3}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArchiSteamFarm.OfficialPlugins.Monitoring", "ArchiSteamFarm.OfficialPlugins.Monitoring\ArchiSteamFarm.OfficialPlugins.Monitoring.csproj", "{0213FBF7-C06E-4E76-9F10-D58D5CB99339}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -77,5 +79,11 @@ Global
|
||||
{8D85BCCA-4DE6-4CC0-B015-E2E89E8E8AA3}.DebugFast|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8D85BCCA-4DE6-4CC0-B015-E2E89E8E8AA3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8D85BCCA-4DE6-4CC0-B015-E2E89E8E8AA3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0213FBF7-C06E-4E76-9F10-D58D5CB99339}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0213FBF7-C06E-4E76-9F10-D58D5CB99339}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0213FBF7-C06E-4E76-9F10-D58D5CB99339}.DebugFast|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0213FBF7-C06E-4E76-9F10-D58D5CB99339}.DebugFast|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0213FBF7-C06E-4E76-9F10-D58D5CB99339}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0213FBF7-C06E-4E76-9F10-D58D5CB99339}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@@ -712,11 +712,13 @@
|
||||
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForSimpleTypes/@EntryValue">UseExplicitType</s:String>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/EditorConfig/EnableClangFormatSupport/@EntryValue">False</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/EditorConfig/EnableEditorConfigSupport/@EntryValue">False</s:Boolean>
|
||||
<s:String x:Key="/Default/CodeStyle/FileHeader/FileHeaderText/@EntryValue"> _ _ _ ____ _ _____
|
||||
<s:String x:Key="/Default/CodeStyle/FileHeader/FileHeaderText/@EntryValue">----------------------------------------------------------------------------------------------
|
||||
_ _ _ ____ _ _____
|
||||
/ \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
/ _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
/ ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
/_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
----------------------------------------------------------------------------------------------
|
||||
|
|
||||
Copyright 2015-${CurrentDate.Year} Łukasz "JustArchi" Domeradzki
|
||||
Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
@@ -28,10 +30,12 @@ using System.Runtime.CompilerServices;
|
||||
[assembly: InternalsVisibleTo("ArchiSteamFarm.Tests, PublicKey=002400000480000014020000060200000024000052534131001000000100010099f0e5961ec7497fd7de1cba2b8c5eff3b18c1faf3d7a8d56e063359c7f928b54b14eae24d23d9d3c1a5db7ceca82edb6956d43e8ea2a0b7223e6e6836c0b809de43fde69bf33fba73cf669e71449284d477333d4b6e54fb69f7b6c4b4811b8fe26e88975e593cffc0e321490a50500865c01e50ab87c8a943b2a788af47dc20f2b860062b7b6df25477e471a744485a286b435cea2df3953cbb66febd8db73f3ccb4588886373141d200f749ba40bb11926b668cc15f328412dd0b0b835909229985336eb4a34f47925558dc6dc3910ea09c1aad5c744833f26ad9de727559d393526a7a29b3383de87802a034ead8ecc2d37340a5fa9b406774446256337d77e3c9e8486b5e732097e238312deaf5b4efcc04df8ecb986d90ee12b4a8a9a00319cc25cb91fd3e36a3cc39e501f83d14eb1e1a6fa6a1365483d99f4cefad1ea5dec204dad958e2a9a93add19781a8aa7bac71747b11d156711eafd1e873e19836eb573fa5cde284739df09b658ed40c56c7b5a7596840774a7065864e6c2af7b5a8bf7a2d238de83d77891d98ef5a4a58248c655a1c7c97c99e01d9928dc60c629eeb523356dc3686e3f9a1a30ffcd0268cd03718292f21d839fce741f4c1163001ab5b654c37d862998962a05e8028e061c611384772777ef6a49b00ebb4f228308e61b2afe408b33db2d82c4f385e26d7438ec0a183c64eeca4138cbc3dc2")]
|
||||
[assembly: InternalsVisibleTo("ArchiSteamFarm.OfficialPlugins.ItemsMatcher, PublicKey=002400000480000014020000060200000024000052534131001000000100010099f0e5961ec7497fd7de1cba2b8c5eff3b18c1faf3d7a8d56e063359c7f928b54b14eae24d23d9d3c1a5db7ceca82edb6956d43e8ea2a0b7223e6e6836c0b809de43fde69bf33fba73cf669e71449284d477333d4b6e54fb69f7b6c4b4811b8fe26e88975e593cffc0e321490a50500865c01e50ab87c8a943b2a788af47dc20f2b860062b7b6df25477e471a744485a286b435cea2df3953cbb66febd8db73f3ccb4588886373141d200f749ba40bb11926b668cc15f328412dd0b0b835909229985336eb4a34f47925558dc6dc3910ea09c1aad5c744833f26ad9de727559d393526a7a29b3383de87802a034ead8ecc2d37340a5fa9b406774446256337d77e3c9e8486b5e732097e238312deaf5b4efcc04df8ecb986d90ee12b4a8a9a00319cc25cb91fd3e36a3cc39e501f83d14eb1e1a6fa6a1365483d99f4cefad1ea5dec204dad958e2a9a93add19781a8aa7bac71747b11d156711eafd1e873e19836eb573fa5cde284739df09b658ed40c56c7b5a7596840774a7065864e6c2af7b5a8bf7a2d238de83d77891d98ef5a4a58248c655a1c7c97c99e01d9928dc60c629eeb523356dc3686e3f9a1a30ffcd0268cd03718292f21d839fce741f4c1163001ab5b654c37d862998962a05e8028e061c611384772777ef6a49b00ebb4f228308e61b2afe408b33db2d82c4f385e26d7438ec0a183c64eeca4138cbc3dc2")]
|
||||
[assembly: InternalsVisibleTo("ArchiSteamFarm.OfficialPlugins.MobileAuthenticator, PublicKey=002400000480000014020000060200000024000052534131001000000100010099f0e5961ec7497fd7de1cba2b8c5eff3b18c1faf3d7a8d56e063359c7f928b54b14eae24d23d9d3c1a5db7ceca82edb6956d43e8ea2a0b7223e6e6836c0b809de43fde69bf33fba73cf669e71449284d477333d4b6e54fb69f7b6c4b4811b8fe26e88975e593cffc0e321490a50500865c01e50ab87c8a943b2a788af47dc20f2b860062b7b6df25477e471a744485a286b435cea2df3953cbb66febd8db73f3ccb4588886373141d200f749ba40bb11926b668cc15f328412dd0b0b835909229985336eb4a34f47925558dc6dc3910ea09c1aad5c744833f26ad9de727559d393526a7a29b3383de87802a034ead8ecc2d37340a5fa9b406774446256337d77e3c9e8486b5e732097e238312deaf5b4efcc04df8ecb986d90ee12b4a8a9a00319cc25cb91fd3e36a3cc39e501f83d14eb1e1a6fa6a1365483d99f4cefad1ea5dec204dad958e2a9a93add19781a8aa7bac71747b11d156711eafd1e873e19836eb573fa5cde284739df09b658ed40c56c7b5a7596840774a7065864e6c2af7b5a8bf7a2d238de83d77891d98ef5a4a58248c655a1c7c97c99e01d9928dc60c629eeb523356dc3686e3f9a1a30ffcd0268cd03718292f21d839fce741f4c1163001ab5b654c37d862998962a05e8028e061c611384772777ef6a49b00ebb4f228308e61b2afe408b33db2d82c4f385e26d7438ec0a183c64eeca4138cbc3dc2")]
|
||||
[assembly: InternalsVisibleTo("ArchiSteamFarm.OfficialPlugins.Monitoring, PublicKey=002400000480000014020000060200000024000052534131001000000100010099f0e5961ec7497fd7de1cba2b8c5eff3b18c1faf3d7a8d56e063359c7f928b54b14eae24d23d9d3c1a5db7ceca82edb6956d43e8ea2a0b7223e6e6836c0b809de43fde69bf33fba73cf669e71449284d477333d4b6e54fb69f7b6c4b4811b8fe26e88975e593cffc0e321490a50500865c01e50ab87c8a943b2a788af47dc20f2b860062b7b6df25477e471a744485a286b435cea2df3953cbb66febd8db73f3ccb4588886373141d200f749ba40bb11926b668cc15f328412dd0b0b835909229985336eb4a34f47925558dc6dc3910ea09c1aad5c744833f26ad9de727559d393526a7a29b3383de87802a034ead8ecc2d37340a5fa9b406774446256337d77e3c9e8486b5e732097e238312deaf5b4efcc04df8ecb986d90ee12b4a8a9a00319cc25cb91fd3e36a3cc39e501f83d14eb1e1a6fa6a1365483d99f4cefad1ea5dec204dad958e2a9a93add19781a8aa7bac71747b11d156711eafd1e873e19836eb573fa5cde284739df09b658ed40c56c7b5a7596840774a7065864e6c2af7b5a8bf7a2d238de83d77891d98ef5a4a58248c655a1c7c97c99e01d9928dc60c629eeb523356dc3686e3f9a1a30ffcd0268cd03718292f21d839fce741f4c1163001ab5b654c37d862998962a05e8028e061c611384772777ef6a49b00ebb4f228308e61b2afe408b33db2d82c4f385e26d7438ec0a183c64eeca4138cbc3dc2")]
|
||||
[assembly: InternalsVisibleTo("ArchiSteamFarm.OfficialPlugins.SteamTokenDumper, PublicKey=002400000480000014020000060200000024000052534131001000000100010099f0e5961ec7497fd7de1cba2b8c5eff3b18c1faf3d7a8d56e063359c7f928b54b14eae24d23d9d3c1a5db7ceca82edb6956d43e8ea2a0b7223e6e6836c0b809de43fde69bf33fba73cf669e71449284d477333d4b6e54fb69f7b6c4b4811b8fe26e88975e593cffc0e321490a50500865c01e50ab87c8a943b2a788af47dc20f2b860062b7b6df25477e471a744485a286b435cea2df3953cbb66febd8db73f3ccb4588886373141d200f749ba40bb11926b668cc15f328412dd0b0b835909229985336eb4a34f47925558dc6dc3910ea09c1aad5c744833f26ad9de727559d393526a7a29b3383de87802a034ead8ecc2d37340a5fa9b406774446256337d77e3c9e8486b5e732097e238312deaf5b4efcc04df8ecb986d90ee12b4a8a9a00319cc25cb91fd3e36a3cc39e501f83d14eb1e1a6fa6a1365483d99f4cefad1ea5dec204dad958e2a9a93add19781a8aa7bac71747b11d156711eafd1e873e19836eb573fa5cde284739df09b658ed40c56c7b5a7596840774a7065864e6c2af7b5a8bf7a2d238de83d77891d98ef5a4a58248c655a1c7c97c99e01d9928dc60c629eeb523356dc3686e3f9a1a30ffcd0268cd03718292f21d839fce741f4c1163001ab5b654c37d862998962a05e8028e061c611384772777ef6a49b00ebb4f228308e61b2afe408b33db2d82c4f385e26d7438ec0a183c64eeca4138cbc3dc2")]
|
||||
#else
|
||||
[assembly: InternalsVisibleTo("ArchiSteamFarm.Tests")]
|
||||
[assembly: InternalsVisibleTo("ArchiSteamFarm.OfficialPlugins.ItemsMatcher")]
|
||||
[assembly: InternalsVisibleTo("ArchiSteamFarm.OfficialPlugins.MobileAuthenticator")]
|
||||
[assembly: InternalsVisibleTo("ArchiSteamFarm.OfficialPlugins.Monitoring")]
|
||||
[assembly: InternalsVisibleTo("ArchiSteamFarm.OfficialPlugins.SteamTokenDumper")]
|
||||
#endif
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
@@ -98,7 +100,9 @@ public sealed class ObservableConcurrentDictionary<TKey, TValue> : IDictionary<T
|
||||
public void Add(TKey key, TValue value) {
|
||||
ArgumentNullException.ThrowIfNull(key);
|
||||
|
||||
TryAdd(key, value);
|
||||
if (!TryAdd(key, value)) {
|
||||
throw new ArgumentException($"An item with the same key has already been added. Key: {key}");
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear() {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
@@ -41,6 +43,8 @@ using ArchiSteamFarm.Steam;
|
||||
using ArchiSteamFarm.Steam.Integration;
|
||||
using ArchiSteamFarm.Storage;
|
||||
using ArchiSteamFarm.Web;
|
||||
using ArchiSteamFarm.Web.GitHub;
|
||||
using ArchiSteamFarm.Web.GitHub.Data;
|
||||
using ArchiSteamFarm.Web.Responses;
|
||||
using JetBrains.Annotations;
|
||||
using SteamKit2;
|
||||
@@ -113,6 +117,10 @@ public static class ASF {
|
||||
|
||||
WebBrowser = new WebBrowser(ArchiLogger, GlobalConfig.WebProxy, true);
|
||||
|
||||
if (!await PluginsCore.InitPlugins().ConfigureAwait(false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await UpdateAndRestart().ConfigureAwait(false);
|
||||
|
||||
if (!Program.IgnoreUnsupportedEnvironment && !await ProtectAgainstCrashes().ConfigureAwait(false)) {
|
||||
@@ -123,10 +131,6 @@ public static class ASF {
|
||||
|
||||
Program.AllowCrashFileRemoval = true;
|
||||
|
||||
if (!await PluginsCore.InitPlugins().ConfigureAwait(false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await PluginsCore.OnASFInitModules(GlobalConfig.AdditionalProperties).ConfigureAwait(false);
|
||||
await InitRateLimiters().ConfigureAwait(false);
|
||||
|
||||
@@ -185,212 +189,23 @@ public static class ASF {
|
||||
}
|
||||
}
|
||||
|
||||
internal static async Task<Version?> Update(GlobalConfig.EUpdateChannel? channel = null, bool updateOverride = false) {
|
||||
if (channel.HasValue && !Enum.IsDefined(channel.Value)) {
|
||||
throw new InvalidEnumArgumentException(nameof(channel), (int) channel, typeof(GlobalConfig.EUpdateChannel));
|
||||
internal static async Task<(bool Updated, Version? NewVersion)> Update(GlobalConfig.EUpdateChannel? updateChannel = null, bool updateOverride = false, bool forced = false) {
|
||||
if (updateChannel.HasValue && !Enum.IsDefined(updateChannel.Value)) {
|
||||
throw new InvalidEnumArgumentException(nameof(updateChannel), (int) updateChannel, typeof(GlobalConfig.EUpdateChannel));
|
||||
}
|
||||
|
||||
if (GlobalConfig == null) {
|
||||
throw new InvalidOperationException(nameof(GlobalConfig));
|
||||
}
|
||||
|
||||
if (WebBrowser == null) {
|
||||
throw new InvalidOperationException(nameof(WebBrowser));
|
||||
(bool updated, Version? newVersion) = await UpdateASF(updateChannel, updateOverride, forced).ConfigureAwait(false);
|
||||
|
||||
if (!updated) {
|
||||
// ASF wasn't updated as part of the process, update the plugins alone
|
||||
updated = await PluginsCore.UpdatePlugins(SharedInfo.Version, false, updateChannel, updateOverride, forced).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
channel ??= GlobalConfig.UpdateChannel;
|
||||
|
||||
if (!SharedInfo.BuildInfo.CanUpdate || (channel == GlobalConfig.EUpdateChannel.None)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await UpdateSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try {
|
||||
// If backup directory from previous update exists, it's a good idea to purge it now
|
||||
string backupDirectory = Path.Combine(SharedInfo.HomeDirectory, SharedInfo.UpdateDirectory);
|
||||
|
||||
if (Directory.Exists(backupDirectory)) {
|
||||
ArchiLogger.LogGenericInfo(Strings.UpdateCleanup);
|
||||
|
||||
for (byte i = 0; (i < WebBrowser.MaxTries) && Directory.Exists(backupDirectory); i++) {
|
||||
if (i > 0) {
|
||||
// It's entirely possible that old process is still running, wait a short moment for eventual cleanup
|
||||
await Task.Delay(5000).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
try {
|
||||
Directory.Delete(backupDirectory, true);
|
||||
} catch (Exception e) {
|
||||
ArchiLogger.LogGenericDebuggingException(e);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (Directory.Exists(backupDirectory)) {
|
||||
ArchiLogger.LogGenericError(Strings.WarningFailed);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericInfo(Strings.Done);
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericInfo(Strings.UpdateCheckingNewVersion);
|
||||
|
||||
GitHub.ReleaseResponse? releaseResponse = await GitHub.GetLatestRelease(channel == GlobalConfig.EUpdateChannel.Stable).ConfigureAwait(false);
|
||||
|
||||
if (releaseResponse == null) {
|
||||
ArchiLogger.LogGenericWarning(Strings.ErrorUpdateCheckFailed);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(releaseResponse.Tag)) {
|
||||
ArchiLogger.LogGenericWarning(Strings.ErrorUpdateCheckFailed);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Version newVersion = new(releaseResponse.Tag);
|
||||
|
||||
ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.UpdateVersionInfo, SharedInfo.Version, newVersion));
|
||||
|
||||
if (SharedInfo.Version >= newVersion) {
|
||||
return newVersion;
|
||||
}
|
||||
|
||||
if (!updateOverride && (GlobalConfig.UpdatePeriod == 0)) {
|
||||
ArchiLogger.LogGenericInfo(Strings.UpdateNewVersionAvailable);
|
||||
await Task.Delay(SharedInfo.ShortInformationDelay).ConfigureAwait(false);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Auto update logic starts here
|
||||
if (releaseResponse.Assets.IsEmpty) {
|
||||
ArchiLogger.LogGenericWarning(Strings.ErrorUpdateNoAssets);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
string targetFile = $"{SharedInfo.ASF}-{SharedInfo.BuildInfo.Variant}.zip";
|
||||
GitHub.ReleaseResponse.Asset? binaryAsset = releaseResponse.Assets.FirstOrDefault(asset => !string.IsNullOrEmpty(asset.Name) && asset.Name.Equals(targetFile, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (binaryAsset == null) {
|
||||
ArchiLogger.LogGenericWarning(Strings.ErrorUpdateNoAssetForThisVersion);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (binaryAsset.DownloadURL == null) {
|
||||
ArchiLogger.LogNullError(binaryAsset.DownloadURL);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericInfo(Strings.FetchingChecksumFromRemoteServer);
|
||||
|
||||
string? remoteChecksum = await ArchiNet.FetchBuildChecksum(newVersion, SharedInfo.BuildInfo.Variant).ConfigureAwait(false);
|
||||
|
||||
switch (remoteChecksum) {
|
||||
case null:
|
||||
// Timeout or error, refuse to update as a security measure
|
||||
return null;
|
||||
case "":
|
||||
// Unknown checksum, release too new or actual malicious build published, no need to scare the user as it's 99.99% the first
|
||||
ArchiLogger.LogGenericWarning(Strings.ChecksumMissing);
|
||||
|
||||
return SharedInfo.Version;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(releaseResponse.ChangelogPlainText)) {
|
||||
ArchiLogger.LogGenericInfo(releaseResponse.ChangelogPlainText);
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.UpdateDownloadingNewVersion, newVersion, binaryAsset.Size / 1024 / 1024));
|
||||
|
||||
Progress<byte> progressReporter = new();
|
||||
|
||||
progressReporter.ProgressChanged += OnProgressChanged;
|
||||
|
||||
BinaryResponse? response;
|
||||
|
||||
try {
|
||||
response = await WebBrowser.UrlGetToBinary(binaryAsset.DownloadURL, progressReporter: progressReporter).ConfigureAwait(false);
|
||||
} finally {
|
||||
progressReporter.ProgressChanged -= OnProgressChanged;
|
||||
}
|
||||
|
||||
if (response?.Content == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericInfo(Strings.VerifyingChecksumWithRemoteServer);
|
||||
|
||||
byte[] responseBytes = response.Content as byte[] ?? response.Content.ToArray();
|
||||
|
||||
string checksum = Utilities.GenerateChecksumFor(responseBytes);
|
||||
|
||||
if (!checksum.Equals(remoteChecksum, StringComparison.OrdinalIgnoreCase)) {
|
||||
ArchiLogger.LogGenericError(Strings.ChecksumWrong);
|
||||
|
||||
return SharedInfo.Version;
|
||||
}
|
||||
|
||||
await PluginsCore.OnUpdateProceeding(newVersion).ConfigureAwait(false);
|
||||
|
||||
bool kestrelWasRunning = ArchiKestrel.IsRunning;
|
||||
|
||||
if (kestrelWasRunning) {
|
||||
// We disable ArchiKestrel here as the update process moves the core files and might result in IPC crash
|
||||
// TODO: It might fail if the update was triggered from the API, this should be something to improve in the future, by changing the structure into request -> return response -> finish update
|
||||
try {
|
||||
await ArchiKestrel.Stop().ConfigureAwait(false);
|
||||
} catch (Exception e) {
|
||||
ArchiLogger.LogGenericWarningException(e);
|
||||
}
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericInfo(Strings.PatchingFiles);
|
||||
|
||||
MemoryStream ms = new(responseBytes);
|
||||
|
||||
try {
|
||||
await using (ms.ConfigureAwait(false)) {
|
||||
using ZipArchive zipArchive = new(ms);
|
||||
|
||||
if (!UpdateFromArchive(zipArchive, SharedInfo.HomeDirectory)) {
|
||||
ArchiLogger.LogGenericError(Strings.WarningFailed);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
ArchiLogger.LogGenericException(e);
|
||||
|
||||
if (kestrelWasRunning) {
|
||||
// We've temporarily disabled ArchiKestrel but the update has failed, let's bring it back up
|
||||
// We can't even be sure if it's possible to bring it back up in this state, but it's worth trying anyway
|
||||
try {
|
||||
await ArchiKestrel.Start().ConfigureAwait(false);
|
||||
} catch (Exception ex) {
|
||||
ArchiLogger.LogGenericWarningException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericInfo(Strings.UpdateFinished);
|
||||
|
||||
await PluginsCore.OnUpdateFinished(newVersion).ConfigureAwait(false);
|
||||
|
||||
return newVersion;
|
||||
} finally {
|
||||
UpdateSemaphore.Release();
|
||||
}
|
||||
return (updated, newVersion);
|
||||
}
|
||||
|
||||
private static async Task<bool> CanHandleWriteEvent(string filePath) {
|
||||
@@ -800,16 +615,6 @@ public static class ASF {
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnProgressChanged(object? sender, byte progressPercentage) {
|
||||
const byte printEveryPercentage = 10;
|
||||
|
||||
if (progressPercentage % printEveryPercentage != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericDebug($"{progressPercentage}%...");
|
||||
}
|
||||
|
||||
private static async void OnRenamed(object sender, RenamedEventArgs e) {
|
||||
// This function can be called with a possibility of OldName or (new) Name being null, we have to take it into account
|
||||
ArgumentNullException.ThrowIfNull(sender);
|
||||
@@ -915,7 +720,7 @@ public static class ASF {
|
||||
throw new InvalidOperationException(nameof(GlobalConfig));
|
||||
}
|
||||
|
||||
if (!SharedInfo.BuildInfo.CanUpdate || (GlobalConfig.UpdateChannel == GlobalConfig.EUpdateChannel.None)) {
|
||||
if (GlobalConfig.UpdateChannel == GlobalConfig.EUpdateChannel.None) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -932,14 +737,11 @@ public static class ASF {
|
||||
ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.AutoUpdateCheckInfo, autoUpdatePeriod.ToHumanReadable()));
|
||||
}
|
||||
|
||||
Version? newVersion = await Update().ConfigureAwait(false);
|
||||
(bool updated, Version? newVersion) = await Update().ConfigureAwait(false);
|
||||
|
||||
if (newVersion == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (SharedInfo.Version >= newVersion) {
|
||||
if (SharedInfo.Version > newVersion) {
|
||||
if (!updated) {
|
||||
if ((newVersion != null) && (SharedInfo.Version > newVersion)) {
|
||||
// User is running version newer than their channel allows
|
||||
ArchiLogger.LogGenericWarning(Strings.WarningPreReleaseVersion);
|
||||
await Task.Delay(SharedInfo.InformationDelay).ConfigureAwait(false);
|
||||
}
|
||||
@@ -953,9 +755,224 @@ public static class ASF {
|
||||
await RestartOrExit().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static bool UpdateFromArchive(ZipArchive archive, string targetDirectory) {
|
||||
ArgumentNullException.ThrowIfNull(archive);
|
||||
ArgumentException.ThrowIfNullOrEmpty(targetDirectory);
|
||||
private static async Task<(bool Updated, Version? NewVersion)> UpdateASF(GlobalConfig.EUpdateChannel? channel = null, bool updateOverride = false, bool forced = false) {
|
||||
if (channel.HasValue && !Enum.IsDefined(channel.Value)) {
|
||||
throw new InvalidEnumArgumentException(nameof(channel), (int) channel, typeof(GlobalConfig.EUpdateChannel));
|
||||
}
|
||||
|
||||
if (GlobalConfig == null) {
|
||||
throw new InvalidOperationException(nameof(GlobalConfig));
|
||||
}
|
||||
|
||||
if (WebBrowser == null) {
|
||||
throw new InvalidOperationException(nameof(WebBrowser));
|
||||
}
|
||||
|
||||
channel ??= GlobalConfig.UpdateChannel;
|
||||
|
||||
if (!SharedInfo.BuildInfo.CanUpdate || (channel == GlobalConfig.EUpdateChannel.None)) {
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
string targetFile;
|
||||
|
||||
await UpdateSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try {
|
||||
// If backup directory from previous update exists, it's a good idea to purge it now
|
||||
string backupDirectory = Path.Combine(SharedInfo.HomeDirectory, SharedInfo.UpdateDirectory);
|
||||
|
||||
if (Directory.Exists(backupDirectory)) {
|
||||
ArchiLogger.LogGenericInfo(Strings.UpdateCleanup);
|
||||
|
||||
for (byte i = 0; (i < WebBrowser.MaxTries) && Directory.Exists(backupDirectory); i++) {
|
||||
if (i > 0) {
|
||||
// It's entirely possible that old process is still running, wait a short moment for eventual cleanup
|
||||
await Task.Delay(5000).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
try {
|
||||
Directory.Delete(backupDirectory, true);
|
||||
} catch (Exception e) {
|
||||
ArchiLogger.LogGenericDebuggingException(e);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (Directory.Exists(backupDirectory)) {
|
||||
ArchiLogger.LogGenericError(Strings.WarningFailed);
|
||||
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericInfo(Strings.Done);
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericInfo(Strings.UpdateCheckingNewVersion);
|
||||
|
||||
ReleaseResponse? releaseResponse = await GitHubService.GetLatestRelease(SharedInfo.GithubRepo, channel == GlobalConfig.EUpdateChannel.Stable).ConfigureAwait(false);
|
||||
|
||||
if (releaseResponse == null) {
|
||||
ArchiLogger.LogGenericWarning(Strings.ErrorUpdateCheckFailed);
|
||||
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(releaseResponse.Tag)) {
|
||||
ArchiLogger.LogGenericWarning(Strings.ErrorUpdateCheckFailed);
|
||||
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
Version newVersion = new(releaseResponse.Tag);
|
||||
|
||||
ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.UpdateVersionInfo, SharedInfo.Version, newVersion));
|
||||
|
||||
if (!forced && (SharedInfo.Version >= newVersion)) {
|
||||
return (false, newVersion);
|
||||
}
|
||||
|
||||
if (!updateOverride && (GlobalConfig.UpdatePeriod == 0)) {
|
||||
ArchiLogger.LogGenericInfo(Strings.UpdateNewVersionAvailable);
|
||||
await Task.Delay(SharedInfo.ShortInformationDelay).ConfigureAwait(false);
|
||||
|
||||
return (false, newVersion);
|
||||
}
|
||||
|
||||
// Auto update logic starts here
|
||||
if (releaseResponse.Assets.IsEmpty) {
|
||||
ArchiLogger.LogGenericWarning(Strings.ErrorUpdateNoAssets);
|
||||
|
||||
return (false, newVersion);
|
||||
}
|
||||
|
||||
targetFile = $"{SharedInfo.ASF}-{SharedInfo.BuildInfo.Variant}.zip";
|
||||
ReleaseAsset? binaryAsset = releaseResponse.Assets.FirstOrDefault(asset => !string.IsNullOrEmpty(asset.Name) && asset.Name.Equals(targetFile, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (binaryAsset == null) {
|
||||
ArchiLogger.LogGenericWarning(Strings.ErrorUpdateNoAssetForThisVersion);
|
||||
|
||||
return (false, newVersion);
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericInfo(Strings.FetchingChecksumFromRemoteServer);
|
||||
|
||||
string? remoteChecksum = await ArchiNet.FetchBuildChecksum(newVersion, SharedInfo.BuildInfo.Variant).ConfigureAwait(false);
|
||||
|
||||
switch (remoteChecksum) {
|
||||
case null:
|
||||
// Timeout or error, refuse to update as a security measure
|
||||
return (false, newVersion);
|
||||
case "":
|
||||
// Unknown checksum, release too new or actual malicious build published, no need to scare the user as it's 99.99% the first
|
||||
ArchiLogger.LogGenericWarning(Strings.ChecksumMissing);
|
||||
|
||||
return (false, newVersion);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(releaseResponse.ChangelogPlainText)) {
|
||||
ArchiLogger.LogGenericInfo(releaseResponse.ChangelogPlainText);
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.UpdateDownloadingNewVersion, newVersion, binaryAsset.Size / 1024 / 1024));
|
||||
|
||||
Progress<byte> progressReporter = new();
|
||||
|
||||
progressReporter.ProgressChanged += onProgressChanged;
|
||||
|
||||
BinaryResponse? response;
|
||||
|
||||
try {
|
||||
response = await WebBrowser.UrlGetToBinary(binaryAsset.DownloadURL, progressReporter: progressReporter).ConfigureAwait(false);
|
||||
} finally {
|
||||
progressReporter.ProgressChanged -= onProgressChanged;
|
||||
}
|
||||
|
||||
if (response?.Content == null) {
|
||||
return (false, newVersion);
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericInfo(Strings.VerifyingChecksumWithRemoteServer);
|
||||
|
||||
byte[] responseBytes = response.Content as byte[] ?? response.Content.ToArray();
|
||||
|
||||
string checksum = Utilities.GenerateChecksumFor(responseBytes);
|
||||
|
||||
if (!checksum.Equals(remoteChecksum, StringComparison.OrdinalIgnoreCase)) {
|
||||
ArchiLogger.LogGenericError(Strings.ChecksumWrong);
|
||||
|
||||
return (false, newVersion);
|
||||
}
|
||||
|
||||
await PluginsCore.OnUpdateProceeding(newVersion).ConfigureAwait(false);
|
||||
|
||||
bool kestrelWasRunning = ArchiKestrel.IsRunning;
|
||||
|
||||
if (kestrelWasRunning) {
|
||||
// We disable ArchiKestrel here as the update process moves the core files and might result in IPC crash
|
||||
// TODO: It might fail if the update was triggered from the API, this should be something to improve in the future, by changing the structure into request -> return response -> finish update
|
||||
try {
|
||||
await ArchiKestrel.Stop().ConfigureAwait(false);
|
||||
} catch (Exception e) {
|
||||
ArchiLogger.LogGenericWarningException(e);
|
||||
}
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericInfo(Strings.PatchingFiles);
|
||||
|
||||
try {
|
||||
MemoryStream memoryStream = new(responseBytes);
|
||||
|
||||
await using (memoryStream.ConfigureAwait(false)) {
|
||||
using ZipArchive zipArchive = new(memoryStream);
|
||||
|
||||
if (!await UpdateFromArchive(newVersion, channel.Value, updateOverride, forced, zipArchive).ConfigureAwait(false)) {
|
||||
ArchiLogger.LogGenericError(Strings.WarningFailed);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
ArchiLogger.LogGenericException(e);
|
||||
|
||||
if (kestrelWasRunning) {
|
||||
// We've temporarily disabled ArchiKestrel but the update has failed, let's bring it back up
|
||||
// We can't even be sure if it's possible to bring it back up in this state, but it's worth trying anyway
|
||||
try {
|
||||
await ArchiKestrel.Start().ConfigureAwait(false);
|
||||
} catch (Exception ex) {
|
||||
ArchiLogger.LogGenericWarningException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
return (false, newVersion);
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericInfo(Strings.UpdateFinished);
|
||||
|
||||
await PluginsCore.OnUpdateFinished(newVersion).ConfigureAwait(false);
|
||||
|
||||
return (true, newVersion);
|
||||
} finally {
|
||||
UpdateSemaphore.Release();
|
||||
}
|
||||
|
||||
void onProgressChanged(object? sender, byte progressPercentage) {
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThan(progressPercentage, 100);
|
||||
|
||||
Utilities.OnProgressChanged(targetFile, progressPercentage);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<bool> UpdateFromArchive(Version newVersion, GlobalConfig.EUpdateChannel updateChannel, bool updateOverride, bool forced, ZipArchive zipArchive) {
|
||||
ArgumentNullException.ThrowIfNull(newVersion);
|
||||
|
||||
if (!Enum.IsDefined(updateChannel)) {
|
||||
throw new InvalidEnumArgumentException(nameof(updateChannel), (int) updateChannel, typeof(GlobalConfig.EUpdateChannel));
|
||||
}
|
||||
|
||||
ArgumentNullException.ThrowIfNull(zipArchive);
|
||||
|
||||
if (SharedInfo.HomeDirectory == AppContext.BaseDirectory) {
|
||||
// We're running a build that includes our dependencies in ASF's home
|
||||
@@ -968,114 +985,10 @@ public static class ASF {
|
||||
LoadAssembliesNeededBeforeUpdate();
|
||||
}
|
||||
|
||||
// Firstly we'll move all our existing files to a backup directory
|
||||
string backupDirectory = Path.Combine(targetDirectory, SharedInfo.UpdateDirectory);
|
||||
// We're ready to start update process, handle any plugin updates ready for new version
|
||||
await PluginsCore.UpdatePlugins(newVersion, true, updateChannel, updateOverride, forced).ConfigureAwait(false);
|
||||
|
||||
foreach (string file in Directory.EnumerateFiles(targetDirectory, "*", SearchOption.AllDirectories)) {
|
||||
string fileName = Path.GetFileName(file);
|
||||
|
||||
if (string.IsNullOrEmpty(fileName)) {
|
||||
ArchiLogger.LogNullError(fileName);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
string relativeFilePath = Path.GetRelativePath(targetDirectory, file);
|
||||
|
||||
if (string.IsNullOrEmpty(relativeFilePath)) {
|
||||
ArchiLogger.LogNullError(relativeFilePath);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
string? relativeDirectoryName = Path.GetDirectoryName(relativeFilePath);
|
||||
|
||||
switch (relativeDirectoryName) {
|
||||
case null:
|
||||
ArchiLogger.LogNullError(relativeDirectoryName);
|
||||
|
||||
return false;
|
||||
case "":
|
||||
// No directory, root folder
|
||||
switch (fileName) {
|
||||
case Logging.NLogConfigurationFile:
|
||||
case SharedInfo.LogFile:
|
||||
// Files with those names in root directory we want to keep
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
case SharedInfo.ArchivalLogsDirectory:
|
||||
case SharedInfo.ConfigDirectory:
|
||||
case SharedInfo.DebugDirectory:
|
||||
case SharedInfo.PluginsDirectory:
|
||||
case SharedInfo.UpdateDirectory:
|
||||
// Files in those directories we want to keep in their current place
|
||||
continue;
|
||||
default:
|
||||
// Files in subdirectories of those directories we want to keep as well
|
||||
if (Utilities.RelativeDirectoryStartsWith(relativeDirectoryName, SharedInfo.ArchivalLogsDirectory, SharedInfo.ConfigDirectory, SharedInfo.DebugDirectory, SharedInfo.PluginsDirectory, SharedInfo.UpdateDirectory)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
string targetBackupDirectory = relativeDirectoryName.Length > 0 ? Path.Combine(backupDirectory, relativeDirectoryName) : backupDirectory;
|
||||
Directory.CreateDirectory(targetBackupDirectory);
|
||||
|
||||
string targetBackupFile = Path.Combine(targetBackupDirectory, fileName);
|
||||
|
||||
File.Move(file, targetBackupFile, true);
|
||||
}
|
||||
|
||||
// We can now get rid of directories that are empty
|
||||
Utilities.DeleteEmptyDirectoriesRecursively(targetDirectory);
|
||||
|
||||
if (!Directory.Exists(targetDirectory)) {
|
||||
Directory.CreateDirectory(targetDirectory);
|
||||
}
|
||||
|
||||
// Now enumerate over files in the zip archive, skip directory entries that we're not interested in (we can create them ourselves if needed)
|
||||
foreach (ZipArchiveEntry zipFile in archive.Entries.Where(static zipFile => !string.IsNullOrEmpty(zipFile.Name))) {
|
||||
string file = Path.GetFullPath(Path.Combine(targetDirectory, zipFile.FullName));
|
||||
|
||||
if (!file.StartsWith(targetDirectory, StringComparison.Ordinal)) {
|
||||
throw new InvalidOperationException(nameof(file));
|
||||
}
|
||||
|
||||
if (File.Exists(file)) {
|
||||
// This is possible only with files that we decided to leave in place during our backup function
|
||||
string targetBackupFile = $"{file}.bak";
|
||||
|
||||
File.Move(file, targetBackupFile, true);
|
||||
}
|
||||
|
||||
// Check if this file requires its own folder
|
||||
if (zipFile.Name != zipFile.FullName) {
|
||||
string? directory = Path.GetDirectoryName(file);
|
||||
|
||||
if (string.IsNullOrEmpty(directory)) {
|
||||
ArchiLogger.LogNullError(directory);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Directory.Exists(directory)) {
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
// We're not interested in extracting placeholder files (but we still want directories created for them, done above)
|
||||
switch (zipFile.Name) {
|
||||
case ".gitkeep":
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
zipFile.ExtractToFile(file);
|
||||
}
|
||||
|
||||
return true;
|
||||
return Utilities.UpdateFromArchive(zipArchive, SharedInfo.HomeDirectory);
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// _ _ _ ____ _ _____
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
@@ -24,21 +26,17 @@ using System.Text.RegularExpressions;
|
||||
namespace ArchiSteamFarm.Core;
|
||||
|
||||
internal static partial class GeneratedRegexes {
|
||||
private const string CdKeyPattern = @"^[0-9A-Z]{4,7}-[0-9A-Z]{4,7}-[0-9A-Z]{4,7}(?:(?:-[0-9A-Z]{4,7})?(?:-[0-9A-Z]{4,7}))?$";
|
||||
private const string DecimalPattern = @"[0-9\.,]+";
|
||||
private const RegexOptions DefaultOptions = RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase;
|
||||
private const string DigitsPattern = @"\d+";
|
||||
private const string NonAsciiPattern = @"[^\u0000-\u007F]+";
|
||||
|
||||
[GeneratedRegex(CdKeyPattern, DefaultOptions)]
|
||||
[GeneratedRegex(@"^[0-9A-Z]{4,7}-[0-9A-Z]{4,7}-[0-9A-Z]{4,7}(?:(?:-[0-9A-Z]{4,7})?(?:-[0-9A-Z]{4,7}))?$", DefaultOptions)]
|
||||
internal static partial Regex CdKey();
|
||||
|
||||
[GeneratedRegex(DecimalPattern, DefaultOptions)]
|
||||
[GeneratedRegex(@"[0-9\.,]+", DefaultOptions)]
|
||||
internal static partial Regex Decimal();
|
||||
|
||||
[GeneratedRegex(DigitsPattern, DefaultOptions)]
|
||||
[GeneratedRegex(@"\d+", DefaultOptions)]
|
||||
internal static partial Regex Digits();
|
||||
|
||||
[GeneratedRegex(NonAsciiPattern, DefaultOptions)]
|
||||
[GeneratedRegex(@"[^\u0000-\u007F]+", DefaultOptions)]
|
||||
internal static partial Regex NonAscii();
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
@@ -27,38 +29,44 @@ namespace ArchiSteamFarm.Core;
|
||||
|
||||
internal static partial class NativeMethods {
|
||||
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
|
||||
[LibraryImport("user32.dll")]
|
||||
[SupportedOSPlatform("Windows")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial void FlashWindowEx(ref FlashWindowInfo pwfi);
|
||||
|
||||
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
|
||||
[LibraryImport("kernel32.dll")]
|
||||
[SupportedOSPlatform("Windows")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool GetConsoleMode(nint hConsoleHandle, out EConsoleMode lpMode);
|
||||
|
||||
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
|
||||
[LibraryImport("libc", EntryPoint = "geteuid", SetLastError = true)]
|
||||
[SupportedOSPlatform("FreeBSD")]
|
||||
[SupportedOSPlatform("Linux")]
|
||||
[SupportedOSPlatform("MacOS")]
|
||||
[LibraryImport("libc", EntryPoint = "geteuid", SetLastError = true)]
|
||||
internal static partial uint GetEuid();
|
||||
|
||||
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
|
||||
[SupportedOSPlatform("Windows")]
|
||||
[LibraryImport("kernel32.dll")]
|
||||
[SupportedOSPlatform("Windows")]
|
||||
internal static partial nint GetStdHandle(EStandardHandle nStdHandle);
|
||||
|
||||
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
|
||||
[LibraryImport("kernel32.dll")]
|
||||
[SupportedOSPlatform("Windows")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
[LibraryImport("kernel32.dll")]
|
||||
internal static partial bool SetConsoleMode(nint hConsoleHandle, EConsoleMode dwMode);
|
||||
|
||||
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
|
||||
[SupportedOSPlatform("Windows")]
|
||||
[LibraryImport("kernel32.dll")]
|
||||
[SupportedOSPlatform("Windows")]
|
||||
internal static partial EExecutionState SetThreadExecutionState(EExecutionState executionState);
|
||||
|
||||
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
|
||||
[LibraryImport("user32.dll")]
|
||||
[SupportedOSPlatform("Windows")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
[LibraryImport("user32.dll")]
|
||||
internal static partial void ShowWindow(nint hWnd, EShowWindow nCmdShow);
|
||||
|
||||
[Flags]
|
||||
@@ -77,6 +85,16 @@ internal static partial class NativeMethods {
|
||||
Awake = SystemRequired | AwayModeRequired | Continuous
|
||||
}
|
||||
|
||||
[Flags]
|
||||
[SupportedOSPlatform("Windows")]
|
||||
internal enum EFlashFlags : uint {
|
||||
Stop = 0,
|
||||
Caption = 1,
|
||||
Tray = 2,
|
||||
All = Caption | Tray,
|
||||
Timer = 4
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("Windows")]
|
||||
internal enum EShowWindow : uint {
|
||||
Minimize = 6
|
||||
@@ -86,4 +104,16 @@ internal static partial class NativeMethods {
|
||||
internal enum EStandardHandle {
|
||||
Input = -10
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
[SupportedOSPlatform("Windows")]
|
||||
internal struct FlashWindowInfo {
|
||||
#pragma warning disable Reordering // TODO: This silly pragma doesn't do anything, but it stops Rider from reordering, we may be able to get rid of it later
|
||||
public uint StructSize;
|
||||
public nint WindowHandle;
|
||||
public EFlashFlags Flags;
|
||||
public uint Count;
|
||||
public uint TimeoutBetweenFlashes;
|
||||
#pragma warning restore Reordering // TODO: This silly pragma doesn't do anything, but it stops Rider from reordering, we may be able to get rid of it later
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
@@ -84,11 +86,11 @@ internal static class OS {
|
||||
private static Mutex? SingleInstance;
|
||||
|
||||
internal static void CoreInit(bool minimized, bool systemRequired) {
|
||||
if (OperatingSystem.IsWindows()) {
|
||||
if (minimized) {
|
||||
WindowsMinimizeConsoleWindow();
|
||||
}
|
||||
if (minimized) {
|
||||
MinimizeConsoleWindow();
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsWindows()) {
|
||||
if (systemRequired) {
|
||||
WindowsKeepSystemActive();
|
||||
}
|
||||
@@ -227,6 +229,73 @@ internal static class OS {
|
||||
return false;
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("Windows")]
|
||||
internal static void WindowsStartFlashingConsoleWindow() {
|
||||
if (!OperatingSystem.IsWindows()) {
|
||||
throw new PlatformNotSupportedException();
|
||||
}
|
||||
|
||||
using Process currentProcess = Process.GetCurrentProcess();
|
||||
|
||||
nint handle = currentProcess.MainWindowHandle;
|
||||
|
||||
if (handle == nint.Zero) {
|
||||
return;
|
||||
}
|
||||
|
||||
NativeMethods.FlashWindowInfo flashInfo = new() {
|
||||
StructSize = (uint) Marshal.SizeOf<NativeMethods.FlashWindowInfo>(),
|
||||
Flags = NativeMethods.EFlashFlags.All | NativeMethods.EFlashFlags.Timer,
|
||||
WindowHandle = handle,
|
||||
Count = uint.MaxValue
|
||||
};
|
||||
|
||||
NativeMethods.FlashWindowEx(ref flashInfo);
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("Windows")]
|
||||
internal static void WindowsStopFlashingConsoleWindow() {
|
||||
if (!OperatingSystem.IsWindows()) {
|
||||
throw new PlatformNotSupportedException();
|
||||
}
|
||||
|
||||
using Process currentProcess = Process.GetCurrentProcess();
|
||||
nint handle = currentProcess.MainWindowHandle;
|
||||
|
||||
if (handle == nint.Zero) {
|
||||
return;
|
||||
}
|
||||
|
||||
NativeMethods.FlashWindowInfo flashInfo = new() {
|
||||
StructSize = (uint) Marshal.SizeOf<NativeMethods.FlashWindowInfo>(),
|
||||
Flags = NativeMethods.EFlashFlags.Stop,
|
||||
WindowHandle = handle
|
||||
};
|
||||
|
||||
NativeMethods.FlashWindowEx(ref flashInfo);
|
||||
}
|
||||
|
||||
private static void MinimizeConsoleWindow() {
|
||||
(_, int top) = Console.GetCursorPosition();
|
||||
|
||||
// Will work if the terminal supports XTWINOPS "iconify" escape sequence, reference: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
||||
Console.Write('\x1b' + @"[2;0;0t");
|
||||
|
||||
// Reset cursor position if terminal outputs escape sequences as-is
|
||||
Console.SetCursorPosition(0, top);
|
||||
|
||||
// Fallback if we're using conhost on Windows
|
||||
if (OperatingSystem.IsWindows()) {
|
||||
using Process process = Process.GetCurrentProcess();
|
||||
|
||||
nint windowHandle = process.MainWindowHandle;
|
||||
|
||||
if (windowHandle != nint.Zero) {
|
||||
NativeMethods.ShowWindow(windowHandle, NativeMethods.EShowWindow.Minimize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("Windows")]
|
||||
private static void WindowsDisableQuickEditMode() {
|
||||
if (!OperatingSystem.IsWindows()) {
|
||||
@@ -264,15 +333,4 @@ internal static class OS {
|
||||
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, result));
|
||||
}
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("Windows")]
|
||||
private static void WindowsMinimizeConsoleWindow() {
|
||||
if (!OperatingSystem.IsWindows()) {
|
||||
throw new PlatformNotSupportedException();
|
||||
}
|
||||
|
||||
using Process process = Process.GetCurrentProcess();
|
||||
|
||||
NativeMethods.ShowWindow(process.MainWindowHandle, NativeMethods.EShowWindow.Minimize);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
@@ -26,6 +28,7 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Resources;
|
||||
@@ -35,6 +38,7 @@ using System.Threading.Tasks;
|
||||
using AngleSharp.Dom;
|
||||
using AngleSharp.XPath;
|
||||
using ArchiSteamFarm.Localization;
|
||||
using ArchiSteamFarm.NLog;
|
||||
using ArchiSteamFarm.Storage;
|
||||
using Humanizer;
|
||||
using Humanizer.Localisation;
|
||||
@@ -263,26 +267,6 @@ public static class Utilities {
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static void DeleteEmptyDirectoriesRecursively(string directory) {
|
||||
ArgumentException.ThrowIfNullOrEmpty(directory);
|
||||
|
||||
if (!Directory.Exists(directory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
foreach (string subDirectory in Directory.EnumerateDirectories(directory)) {
|
||||
DeleteEmptyDirectoriesRecursively(subDirectory);
|
||||
}
|
||||
|
||||
if (!Directory.EnumerateFileSystemEntries(directory).Any()) {
|
||||
Directory.Delete(directory);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
ASF.ArchiLogger.LogGenericException(e);
|
||||
}
|
||||
}
|
||||
|
||||
internal static ulong MathAdd(ulong first, int second) {
|
||||
if (second >= 0) {
|
||||
return first + (uint) second;
|
||||
@@ -291,16 +275,17 @@ public static class Utilities {
|
||||
return first - (uint) -second;
|
||||
}
|
||||
|
||||
internal static bool RelativeDirectoryStartsWith(string directory, params string[] prefixes) {
|
||||
ArgumentException.ThrowIfNullOrEmpty(directory);
|
||||
internal static void OnProgressChanged(string fileName, byte progressPercentage) {
|
||||
ArgumentException.ThrowIfNullOrEmpty(fileName);
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThan(progressPercentage, 100);
|
||||
|
||||
#pragma warning disable CA1508 // False positive, params could be null when explicitly set
|
||||
if ((prefixes == null) || (prefixes.Length == 0)) {
|
||||
#pragma warning restore CA1508 // False positive, params could be null when explicitly set
|
||||
throw new ArgumentNullException(nameof(prefixes));
|
||||
const byte printEveryPercentage = 10;
|
||||
|
||||
if (progressPercentage % printEveryPercentage != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (from prefix in prefixes where directory.Length > prefix.Length let pathSeparator = directory[prefix.Length] where (pathSeparator == Path.DirectorySeparatorChar) || (pathSeparator == Path.AltDirectorySeparatorChar) select prefix).Any(prefix => directory.StartsWith(prefix, StringComparison.Ordinal));
|
||||
ASF.ArchiLogger.LogGenericDebug($"{fileName} {progressPercentage}%...");
|
||||
}
|
||||
|
||||
internal static (bool IsWeak, string? Reason) TestPasswordStrength(string password, ISet<string>? additionallyForbiddenPhrases = null) {
|
||||
@@ -337,6 +322,120 @@ public static class Utilities {
|
||||
return (result.Score < 4, suggestions is { Count: > 0 } ? string.Join(' ', suggestions.Where(static suggestion => suggestion.Length > 0)) : null);
|
||||
}
|
||||
|
||||
internal static bool UpdateFromArchive(ZipArchive zipArchive, string targetDirectory) {
|
||||
ArgumentNullException.ThrowIfNull(zipArchive);
|
||||
ArgumentException.ThrowIfNullOrEmpty(targetDirectory);
|
||||
|
||||
// Firstly we'll move all our existing files to a backup directory
|
||||
string backupDirectory = Path.Combine(targetDirectory, SharedInfo.UpdateDirectory);
|
||||
|
||||
foreach (string file in Directory.EnumerateFiles(targetDirectory, "*", SearchOption.AllDirectories)) {
|
||||
string fileName = Path.GetFileName(file);
|
||||
|
||||
if (string.IsNullOrEmpty(fileName)) {
|
||||
ASF.ArchiLogger.LogNullError(fileName);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
string relativeFilePath = Path.GetRelativePath(targetDirectory, file);
|
||||
|
||||
if (string.IsNullOrEmpty(relativeFilePath)) {
|
||||
ASF.ArchiLogger.LogNullError(relativeFilePath);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
string? relativeDirectoryName = Path.GetDirectoryName(relativeFilePath);
|
||||
|
||||
switch (relativeDirectoryName) {
|
||||
case null:
|
||||
ASF.ArchiLogger.LogNullError(relativeDirectoryName);
|
||||
|
||||
return false;
|
||||
case "":
|
||||
// No directory, root folder
|
||||
switch (fileName) {
|
||||
case Logging.NLogConfigurationFile:
|
||||
case SharedInfo.LogFile:
|
||||
// Files with those names in root directory we want to keep
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
case SharedInfo.ArchivalLogsDirectory:
|
||||
case SharedInfo.ConfigDirectory:
|
||||
case SharedInfo.DebugDirectory:
|
||||
case SharedInfo.PluginsDirectory:
|
||||
case SharedInfo.UpdateDirectory:
|
||||
// Files in those directories we want to keep in their current place
|
||||
continue;
|
||||
default:
|
||||
// Files in subdirectories of those directories we want to keep as well
|
||||
if (RelativeDirectoryStartsWith(relativeDirectoryName, SharedInfo.ArchivalLogsDirectory, SharedInfo.ConfigDirectory, SharedInfo.DebugDirectory, SharedInfo.PluginsDirectory, SharedInfo.UpdateDirectory)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
string targetBackupDirectory = relativeDirectoryName.Length > 0 ? Path.Combine(backupDirectory, relativeDirectoryName) : backupDirectory;
|
||||
Directory.CreateDirectory(targetBackupDirectory);
|
||||
|
||||
string targetBackupFile = Path.Combine(targetBackupDirectory, fileName);
|
||||
|
||||
File.Move(file, targetBackupFile, true);
|
||||
}
|
||||
|
||||
// We can now get rid of directories that are empty
|
||||
DeleteEmptyDirectoriesRecursively(targetDirectory);
|
||||
|
||||
if (!Directory.Exists(targetDirectory)) {
|
||||
Directory.CreateDirectory(targetDirectory);
|
||||
}
|
||||
|
||||
// Now enumerate over files in the zip archive, skip directory entries that we're not interested in (we can create them ourselves if needed)
|
||||
foreach (ZipArchiveEntry zipFile in zipArchive.Entries.Where(static zipFile => !string.IsNullOrEmpty(zipFile.Name))) {
|
||||
string file = Path.GetFullPath(Path.Combine(targetDirectory, zipFile.FullName));
|
||||
|
||||
if (!file.StartsWith(targetDirectory, StringComparison.Ordinal)) {
|
||||
throw new InvalidOperationException(nameof(file));
|
||||
}
|
||||
|
||||
if (File.Exists(file)) {
|
||||
// This is possible only with files that we decided to leave in place during our backup function
|
||||
string targetBackupFile = $"{file}.bak";
|
||||
|
||||
File.Move(file, targetBackupFile, true);
|
||||
}
|
||||
|
||||
// Check if this file requires its own folder
|
||||
if (zipFile.Name != zipFile.FullName) {
|
||||
string? directory = Path.GetDirectoryName(file);
|
||||
|
||||
if (string.IsNullOrEmpty(directory)) {
|
||||
ASF.ArchiLogger.LogNullError(directory);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Directory.Exists(directory)) {
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
// We're not interested in extracting placeholder files (but we still want directories created for them, done above)
|
||||
switch (zipFile.Name) {
|
||||
case ".gitkeep":
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
zipFile.ExtractToFile(file);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static void WarnAboutIncompleteTranslation(ResourceManager resourceManager) {
|
||||
ArgumentNullException.ThrowIfNull(resourceManager);
|
||||
|
||||
@@ -391,4 +490,38 @@ public static class Utilities {
|
||||
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.TranslationIncomplete, $"{CultureInfo.CurrentUICulture.Name} ({CultureInfo.CurrentUICulture.EnglishName})", translationCompleteness.ToString("P1", CultureInfo.CurrentCulture)));
|
||||
}
|
||||
}
|
||||
|
||||
private static void DeleteEmptyDirectoriesRecursively(string directory) {
|
||||
ArgumentException.ThrowIfNullOrEmpty(directory);
|
||||
|
||||
if (!Directory.Exists(directory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
foreach (string subDirectory in Directory.EnumerateDirectories(directory)) {
|
||||
DeleteEmptyDirectoriesRecursively(subDirectory);
|
||||
}
|
||||
|
||||
if (!Directory.EnumerateFileSystemEntries(directory).Any()) {
|
||||
Directory.Delete(directory);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
ASF.ArchiLogger.LogGenericException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool RelativeDirectoryStartsWith(string directory, params string[] prefixes) {
|
||||
ArgumentException.ThrowIfNullOrEmpty(directory);
|
||||
|
||||
#pragma warning disable CA1508 // False positive, params could be null when explicitly set
|
||||
if ((prefixes == null) || (prefixes.Length == 0)) {
|
||||
#pragma warning restore CA1508 // False positive, params could be null when explicitly set
|
||||
throw new ArgumentNullException(nameof(prefixes));
|
||||
}
|
||||
|
||||
HashSet<char> separators = [Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar];
|
||||
|
||||
return prefixes.Where(prefix => (directory.Length > prefix.Length) && separators.Contains(directory[prefix.Length])).Any(prefix => directory.StartsWith(prefix, StringComparison.Ordinal));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
@@ -64,7 +66,7 @@ public sealed class ArchiCacheable<T> : IDisposable {
|
||||
} catch (OperationCanceledException e) {
|
||||
ASF.ArchiLogger.LogGenericDebuggingException(e);
|
||||
|
||||
return ReturnFailedValueFor(cacheFallback);
|
||||
return GetFailedValueFor(cacheFallback);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -75,7 +77,7 @@ public sealed class ArchiCacheable<T> : IDisposable {
|
||||
(bool success, T? result) = await ResolveFunction(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!success) {
|
||||
return ReturnFailedValueFor(cacheFallback, result);
|
||||
return GetFailedValueFor(cacheFallback, result);
|
||||
}
|
||||
|
||||
InitializedValue = result;
|
||||
@@ -85,7 +87,7 @@ public sealed class ArchiCacheable<T> : IDisposable {
|
||||
} catch (OperationCanceledException e) {
|
||||
ASF.ArchiLogger.LogGenericDebuggingException(e);
|
||||
|
||||
return ReturnFailedValueFor(cacheFallback);
|
||||
return GetFailedValueFor(cacheFallback);
|
||||
} finally {
|
||||
InitSemaphore.Release();
|
||||
}
|
||||
@@ -110,7 +112,7 @@ public sealed class ArchiCacheable<T> : IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
private (bool Success, T? Result) ReturnFailedValueFor(ECacheFallback cacheFallback, T? result = default) {
|
||||
private (bool Success, T? Result) GetFailedValueFor(ECacheFallback cacheFallback, T? result = default) {
|
||||
if (!Enum.IsDefined(cacheFallback)) {
|
||||
throw new InvalidEnumArgumentException(nameof(cacheFallback), (int) cacheFallback, typeof(ECacheFallback));
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
46
ArchiSteamFarm/Helpers/Json/BooleanNumberConverter.cs
Normal file
46
ArchiSteamFarm/Helpers/Json/BooleanNumberConverter.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Ł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.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace ArchiSteamFarm.Helpers.Json;
|
||||
|
||||
[PublicAPI]
|
||||
public sealed class BooleanNumberConverter : JsonConverter<bool> {
|
||||
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
|
||||
reader.TokenType switch {
|
||||
JsonTokenType.True => true,
|
||||
JsonTokenType.False => false,
|
||||
JsonTokenType.Number => reader.GetByte() == 1,
|
||||
_ => throw new JsonException()
|
||||
};
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options) {
|
||||
ArgumentNullException.ThrowIfNull(writer);
|
||||
|
||||
writer.WriteNumberValue(value ? 1 : 0);
|
||||
}
|
||||
}
|
||||
58
ArchiSteamFarm/Helpers/Json/GuidJsonConverter.cs
Normal file
58
ArchiSteamFarm/Helpers/Json/GuidJsonConverter.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Ł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.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace ArchiSteamFarm.Helpers.Json;
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// TODO: This class exists purely because STJ can't deserialize Guid in other formats than default, at least for now
|
||||
/// https://github.com/dotnet/runtime/issues/30692
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public sealed class GuidJsonConverter : JsonConverter<Guid> {
|
||||
public override Guid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
|
||||
if (reader.TryGetGuid(out Guid result)) {
|
||||
// Great, we can work with it
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
// Try again using more flexible implementation, sigh
|
||||
return Guid.Parse(reader.GetString()!);
|
||||
} catch {
|
||||
// Throw JsonException instead, which will be converted into standard message by STJ
|
||||
throw new JsonException();
|
||||
}
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, Guid value, JsonSerializerOptions options) {
|
||||
ArgumentNullException.ThrowIfNull(writer);
|
||||
|
||||
writer.WriteStringValue(value.ToString());
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
@@ -42,6 +44,7 @@ public static class JsonUtilities {
|
||||
public static readonly JsonSerializerOptions IndentedJsonSerialierOptions = CreateDefaultJsonSerializerOptions(true);
|
||||
|
||||
[PublicAPI]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")]
|
||||
public static JsonElement ToJsonElement<T>(this T obj) where T : notnull {
|
||||
ArgumentNullException.ThrowIfNull(obj);
|
||||
|
||||
@@ -49,9 +52,11 @@ public static class JsonUtilities {
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public static T? ToJsonObject<T>(this JsonElement jsonElement, CancellationToken cancellationToken = default) => jsonElement.Deserialize<T>(DefaultJsonSerialierOptions);
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")]
|
||||
public static T? ToJsonObject<T>(this JsonElement jsonElement) => jsonElement.Deserialize<T>(DefaultJsonSerialierOptions);
|
||||
|
||||
[PublicAPI]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")]
|
||||
public static async ValueTask<T?> ToJsonObject<T>(this Stream stream, CancellationToken cancellationToken = default) {
|
||||
ArgumentNullException.ThrowIfNull(stream);
|
||||
|
||||
@@ -59,6 +64,7 @@ public static class JsonUtilities {
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")]
|
||||
public static T? ToJsonObject<T>([StringSyntax(StringSyntaxAttribute.Json)] this string json) {
|
||||
ArgumentException.ThrowIfNullOrEmpty(json);
|
||||
|
||||
@@ -66,6 +72,7 @@ public static class JsonUtilities {
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")]
|
||||
public static string ToJsonText<T>(this T obj, bool writeIndented = false) => JsonSerializer.Serialize(obj, writeIndented ? IndentedJsonSerialierOptions : DefaultJsonSerialierOptions);
|
||||
|
||||
private static void ApplyCustomModifiers(JsonTypeInfo jsonTypeInfo) {
|
||||
@@ -102,6 +109,7 @@ public static class JsonUtilities {
|
||||
}
|
||||
}
|
||||
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")]
|
||||
private static JsonSerializerOptions CreateDefaultJsonSerializerOptions(bool writeIndented = false) =>
|
||||
new() {
|
||||
AllowTrailingCommas = true,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
@@ -65,7 +67,7 @@ public abstract class SerializableFile : IDisposable {
|
||||
ArgumentNullException.ThrowIfNull(serializableFile);
|
||||
|
||||
if (string.IsNullOrEmpty(serializableFile.FilePath)) {
|
||||
throw new InvalidOperationException(nameof(FilePath));
|
||||
throw new InvalidOperationException(nameof(serializableFile.FilePath));
|
||||
}
|
||||
|
||||
if (serializableFile.ReadOnly) {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
@@ -20,29 +22,49 @@
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Core;
|
||||
using ArchiSteamFarm.Helpers.Json;
|
||||
using ArchiSteamFarm.IPC.Controllers.Api;
|
||||
using ArchiSteamFarm.IPC.Integration;
|
||||
using ArchiSteamFarm.Localization;
|
||||
using ArchiSteamFarm.NLog;
|
||||
using ArchiSteamFarm.NLog.Targets;
|
||||
using ArchiSteamFarm.Plugins;
|
||||
using ArchiSteamFarm.Plugins.Interfaces;
|
||||
using ArchiSteamFarm.Storage;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Headers;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using NLog.Web;
|
||||
using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;
|
||||
|
||||
namespace ArchiSteamFarm.IPC;
|
||||
|
||||
internal static class ArchiKestrel {
|
||||
internal static bool IsRunning => KestrelWebHost != null;
|
||||
internal static bool IsRunning => WebApplication != null;
|
||||
|
||||
internal static HistoryTarget? HistoryTarget { get; private set; }
|
||||
|
||||
private static IHost? KestrelWebHost;
|
||||
private static WebApplication? WebApplication;
|
||||
|
||||
internal static void OnNewHistoryTarget(HistoryTarget? historyTarget = null) {
|
||||
if (HistoryTarget != null) {
|
||||
@@ -57,102 +79,443 @@ internal static class ArchiKestrel {
|
||||
}
|
||||
|
||||
internal static async Task Start() {
|
||||
if (KestrelWebHost != null) {
|
||||
if (WebApplication != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ASF.ArchiLogger.LogGenericInfo(Strings.IPCStarting);
|
||||
|
||||
// The order of dependency injection matters, pay attention to it
|
||||
HostBuilder builder = new();
|
||||
|
||||
string customDirectory = Path.Combine(Directory.GetCurrentDirectory(), SharedInfo.WebsiteDirectory);
|
||||
string websiteDirectory = Directory.Exists(customDirectory) ? customDirectory : Path.Combine(AppContext.BaseDirectory, SharedInfo.WebsiteDirectory);
|
||||
|
||||
// Set default content root
|
||||
builder.UseContentRoot(SharedInfo.HomeDirectory);
|
||||
|
||||
// Check if custom config is available
|
||||
string absoluteConfigDirectory = Path.Combine(Directory.GetCurrentDirectory(), SharedInfo.ConfigDirectory);
|
||||
string customConfigPath = Path.Combine(absoluteConfigDirectory, SharedInfo.IPCConfigFile);
|
||||
|
||||
bool customConfigExists = File.Exists(customConfigPath);
|
||||
|
||||
if (customConfigExists && Debugging.IsDebugConfigured) {
|
||||
try {
|
||||
string json = await File.ReadAllTextAsync(customConfigPath).ConfigureAwait(false);
|
||||
|
||||
if (!string.IsNullOrEmpty(json)) {
|
||||
JsonNode? jsonNode = JsonNode.Parse(json);
|
||||
|
||||
ASF.ArchiLogger.LogGenericDebug($"{SharedInfo.IPCConfigFile}: {jsonNode?.ToJsonText(true) ?? "null"}");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
ASF.ArchiLogger.LogGenericException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Enable NLog integration for logging
|
||||
builder.ConfigureLogging(
|
||||
static logging => {
|
||||
logging.ClearProviders();
|
||||
logging.SetMinimumLevel(Debugging.IsUserDebugging ? LogLevel.Trace : LogLevel.Warning);
|
||||
}
|
||||
);
|
||||
|
||||
builder.UseNLog(new NLogAspNetCoreOptions { ShutdownOnDispose = false });
|
||||
|
||||
builder.ConfigureWebHostDefaults(
|
||||
webBuilder => {
|
||||
// Set default web root
|
||||
if (Directory.Exists(websiteDirectory)) {
|
||||
webBuilder.UseWebRoot(websiteDirectory);
|
||||
}
|
||||
|
||||
// Now conditionally initialize settings that are not possible to override
|
||||
if (customConfigExists) {
|
||||
// Set up custom config to be used
|
||||
webBuilder.UseConfiguration(new ConfigurationBuilder().SetBasePath(absoluteConfigDirectory).AddJsonFile(SharedInfo.IPCConfigFile, false, Program.ConfigWatch).Build());
|
||||
|
||||
// Use custom config for Kestrel configuration
|
||||
webBuilder.UseKestrel(static (builderContext, options) => options.Configure(builderContext.Configuration.GetSection("Kestrel")));
|
||||
} else {
|
||||
// Use ASF defaults for Kestrel
|
||||
webBuilder.UseKestrel(static options => options.ListenLocalhost(1242));
|
||||
}
|
||||
|
||||
// Specify Startup class for IPC
|
||||
webBuilder.UseStartup<Startup>();
|
||||
}
|
||||
);
|
||||
|
||||
// Init history logger for /Api/Log usage
|
||||
Logging.InitHistoryLogger();
|
||||
|
||||
// Start the server
|
||||
IHost? kestrelWebHost = null;
|
||||
WebApplication webApplication = await CreateWebApplication().ConfigureAwait(false);
|
||||
|
||||
try {
|
||||
kestrelWebHost = builder.Build();
|
||||
await kestrelWebHost.StartAsync().ConfigureAwait(false);
|
||||
// Start the server
|
||||
await webApplication.StartAsync().ConfigureAwait(false);
|
||||
} catch (Exception e) {
|
||||
ASF.ArchiLogger.LogGenericException(e);
|
||||
kestrelWebHost?.Dispose();
|
||||
|
||||
await webApplication.DisposeAsync().ConfigureAwait(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
KestrelWebHost = kestrelWebHost;
|
||||
WebApplication = webApplication;
|
||||
|
||||
ASF.ArchiLogger.LogGenericInfo(Strings.IPCReady);
|
||||
}
|
||||
|
||||
internal static async Task Stop() {
|
||||
if (KestrelWebHost == null) {
|
||||
if (WebApplication == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await KestrelWebHost.StopAsync().ConfigureAwait(false);
|
||||
KestrelWebHost.Dispose();
|
||||
KestrelWebHost = null;
|
||||
await WebApplication.StopAsync().ConfigureAwait(false);
|
||||
await WebApplication.DisposeAsync().ConfigureAwait(false);
|
||||
|
||||
WebApplication = null;
|
||||
}
|
||||
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "PathString is a primitive, it's unlikely to be trimmed to the best of our knowledge")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3000", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")]
|
||||
private static void ConfigureApp([SuppressMessage("ReSharper", "SuggestBaseTypeForParameter")] ConfigurationManager configuration, IApplicationBuilder app) {
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
ArgumentNullException.ThrowIfNull(app);
|
||||
|
||||
// The order of dependency injection is super important, doing things in wrong order will break everything
|
||||
// https://docs.microsoft.com/aspnet/core/fundamentals/middleware
|
||||
|
||||
// This one is easy, it's always in the beginning
|
||||
if (Debugging.IsUserDebugging) {
|
||||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
|
||||
// Add support for proxies, this one comes usually after developer exception page, but could be before
|
||||
app.UseForwardedHeaders();
|
||||
|
||||
if (ASF.GlobalConfig?.OptimizationMode != GlobalConfig.EOptimizationMode.MinMemoryUsage) {
|
||||
// Add support for response caching - must be called before static files as we want to cache those as well
|
||||
app.UseResponseCaching();
|
||||
}
|
||||
|
||||
// Add support for response compression - must be called before static files as we want to compress those as well
|
||||
app.UseResponseCompression();
|
||||
|
||||
// It's not apparent when UsePathBase() should be called, but definitely before we get down to static files
|
||||
// TODO: Maybe eventually we can get rid of this, https://github.com/aspnet/AspNetCore/issues/5898
|
||||
PathString pathBase = configuration.GetSection("Kestrel").GetValue<PathString>("PathBase");
|
||||
|
||||
if (!string.IsNullOrEmpty(pathBase) && (pathBase != "/")) {
|
||||
app.UsePathBase(pathBase);
|
||||
}
|
||||
|
||||
// The default HTML file (usually index.html) is responsible for IPC GUI routing, so re-execute all non-API calls on /
|
||||
// This must be called before default files, because we don't know the exact file name that will be used for index page
|
||||
app.UseWhen(static context => !context.Request.Path.StartsWithSegments("/Api", StringComparison.OrdinalIgnoreCase), static appBuilder => appBuilder.UseStatusCodePagesWithReExecute("/"));
|
||||
|
||||
// Add support for default root path redirection (GET / -> GET /index.html), must come before static files
|
||||
app.UseDefaultFiles();
|
||||
|
||||
Dictionary<string, string> pluginPaths = new(StringComparer.Ordinal);
|
||||
|
||||
if (PluginsCore.ActivePlugins.Count > 0) {
|
||||
foreach (IWebInterface plugin in PluginsCore.ActivePlugins.OfType<IWebInterface>()) {
|
||||
if (string.IsNullOrEmpty(plugin.PhysicalPath) || string.IsNullOrEmpty(plugin.WebPath)) {
|
||||
// Invalid path provided
|
||||
continue;
|
||||
}
|
||||
|
||||
string physicalPath = plugin.PhysicalPath;
|
||||
|
||||
if (!Path.IsPathRooted(physicalPath)) {
|
||||
// Relative path
|
||||
string? assemblyDirectory = Path.GetDirectoryName(plugin.GetType().Assembly.Location);
|
||||
|
||||
if (string.IsNullOrEmpty(assemblyDirectory)) {
|
||||
throw new InvalidOperationException(nameof(assemblyDirectory));
|
||||
}
|
||||
|
||||
physicalPath = Path.Combine(assemblyDirectory, plugin.PhysicalPath);
|
||||
}
|
||||
|
||||
if (!Directory.Exists(physicalPath)) {
|
||||
// Non-existing path provided
|
||||
continue;
|
||||
}
|
||||
|
||||
pluginPaths[physicalPath] = plugin.WebPath;
|
||||
|
||||
if (plugin.WebPath != "/") {
|
||||
app.UseDefaultFiles(plugin.WebPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add support for static files from custom plugins (e.g. HTML, CSS and JS)
|
||||
foreach ((string physicalPath, string webPath) in pluginPaths) {
|
||||
app.UseStaticFiles(
|
||||
new StaticFileOptions {
|
||||
FileProvider = new PhysicalFileProvider(physicalPath),
|
||||
OnPrepareResponse = OnPrepareResponse,
|
||||
RequestPath = webPath
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Add support for static files (e.g. HTML, CSS and JS from IPC GUI)
|
||||
app.UseStaticFiles(
|
||||
new StaticFileOptions {
|
||||
OnPrepareResponse = OnPrepareResponse
|
||||
}
|
||||
);
|
||||
|
||||
// Use routing for our API controllers, this should be called once we're done with all the static files mess
|
||||
app.UseRouting();
|
||||
|
||||
// We want to protect our API with IPCPassword and additional security, this should be called after routing, so the middleware won't have to deal with API endpoints that do not exist
|
||||
app.UseWhen(static context => context.Request.Path.StartsWithSegments("/Api", StringComparison.OrdinalIgnoreCase), static appBuilder => appBuilder.UseMiddleware<ApiAuthenticationMiddleware>());
|
||||
|
||||
string? ipcPassword = ASF.GlobalConfig != null ? ASF.GlobalConfig.IPCPassword : GlobalConfig.DefaultIPCPassword;
|
||||
|
||||
if (!string.IsNullOrEmpty(ipcPassword)) {
|
||||
// We want to apply CORS policy in order to allow userscripts and other third-party integrations to communicate with ASF API, this should be called before response compression, but can't be due to how our flow works
|
||||
// We apply CORS policy only with IPCPassword set as an extra authentication measure
|
||||
app.UseCors();
|
||||
}
|
||||
|
||||
// Add support for websockets that we use e.g. in /Api/NLog
|
||||
app.UseWebSockets();
|
||||
|
||||
// Finally register proper API endpoints once we're done with routing
|
||||
app.UseEndpoints(static endpoints => endpoints.MapControllers());
|
||||
|
||||
if (PluginsCore.ActivePlugins.Count > 0) {
|
||||
foreach (IWebServiceProvider plugin in PluginsCore.ActivePlugins.OfType<IWebServiceProvider>()) {
|
||||
try {
|
||||
plugin.OnConfiguringEndpoints(app);
|
||||
} catch (Exception e) {
|
||||
ASF.ArchiLogger.LogGenericException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add support for swagger, responsible for automatic API documentation generation, this should be on the end, once we're done with API
|
||||
app.UseSwagger();
|
||||
|
||||
// Add support for swagger UI, this should be after swagger, obviously
|
||||
app.UseSwaggerUI(
|
||||
static options => {
|
||||
options.DisplayRequestDuration();
|
||||
options.EnableDeepLinking();
|
||||
options.ShowExtensions();
|
||||
options.SwaggerEndpoint($"{SharedInfo.ASF}/swagger.json", $"{SharedInfo.ASF} API");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")]
|
||||
private static void ConfigureServices([SuppressMessage("ReSharper", "SuggestBaseTypeForParameter")] ConfigurationManager configuration, IServiceCollection services) {
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
|
||||
// The order of dependency injection is super important, doing things in wrong order will break everything
|
||||
// Order in Configure() method is a good start
|
||||
|
||||
// Prepare knownNetworks that we'll use in a second
|
||||
HashSet<string>? knownNetworksTexts = configuration.GetSection("Kestrel:KnownNetworks").Get<HashSet<string>>();
|
||||
|
||||
HashSet<IPNetwork>? knownNetworks = null;
|
||||
|
||||
if (knownNetworksTexts?.Count > 0) {
|
||||
// Use specified known networks
|
||||
knownNetworks = new HashSet<IPNetwork>();
|
||||
|
||||
foreach (string knownNetworkText in knownNetworksTexts) {
|
||||
string[] addressParts = knownNetworkText.Split('/', 3, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if ((addressParts.Length != 2) || !IPAddress.TryParse(addressParts[0], out IPAddress? ipAddress) || !byte.TryParse(addressParts[1], out byte prefixLength)) {
|
||||
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(knownNetworkText)));
|
||||
ASF.ArchiLogger.LogGenericDebug($"{nameof(knownNetworkText)}: {knownNetworkText}");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
knownNetworks.Add(new IPNetwork(ipAddress, prefixLength));
|
||||
}
|
||||
}
|
||||
|
||||
// Add support for proxies
|
||||
services.Configure<ForwardedHeadersOptions>(
|
||||
options => {
|
||||
options.ForwardedHeaders = ForwardedHeaders.All;
|
||||
|
||||
if (knownNetworks != null) {
|
||||
foreach (IPNetwork knownNetwork in knownNetworks) {
|
||||
options.KnownNetworks.Add(knownNetwork);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (ASF.GlobalConfig?.OptimizationMode != GlobalConfig.EOptimizationMode.MinMemoryUsage) {
|
||||
// Add support for response caching
|
||||
services.AddResponseCaching();
|
||||
}
|
||||
|
||||
// Add support for response compression
|
||||
services.AddResponseCompression(static options => options.EnableForHttps = true);
|
||||
|
||||
string? ipcPassword = ASF.GlobalConfig != null ? ASF.GlobalConfig.IPCPassword : GlobalConfig.DefaultIPCPassword;
|
||||
|
||||
if (!string.IsNullOrEmpty(ipcPassword)) {
|
||||
// We want to apply CORS policy in order to allow userscripts and other third-party integrations to communicate with ASF API
|
||||
// We apply CORS policy only with IPCPassword set as an extra authentication measure
|
||||
services.AddCors(static options => options.AddDefaultPolicy(static policyBuilder => policyBuilder.AllowAnyOrigin()));
|
||||
}
|
||||
|
||||
// Add support for swagger, responsible for automatic API documentation generation
|
||||
services.AddSwaggerGen(
|
||||
static options => {
|
||||
options.AddSecurityDefinition(
|
||||
nameof(GlobalConfig.IPCPassword), new OpenApiSecurityScheme {
|
||||
Description = $"{nameof(GlobalConfig.IPCPassword)} authentication using request headers. Check {SharedInfo.ProjectURL}/wiki/IPC#authentication for more info.",
|
||||
In = ParameterLocation.Header,
|
||||
Name = ApiAuthenticationMiddleware.HeadersField,
|
||||
Type = SecuritySchemeType.ApiKey
|
||||
}
|
||||
);
|
||||
|
||||
options.AddSecurityRequirement(
|
||||
new OpenApiSecurityRequirement {
|
||||
{
|
||||
new OpenApiSecurityScheme {
|
||||
Reference = new OpenApiReference {
|
||||
Id = nameof(GlobalConfig.IPCPassword),
|
||||
Type = ReferenceType.SecurityScheme
|
||||
}
|
||||
},
|
||||
|
||||
Array.Empty<string>()
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// We require custom schema IDs due to conflicting type names, choosing the proper one is tricky as there is no good answer and any kind of convention has a potential to create conflict
|
||||
// FullName and Name both do, ToString() for unknown to me reason doesn't, and I don't have courage to call our WebUtilities.GetUnifiedName() better than what .NET ships with (because it isn't)
|
||||
// Let's use ToString() until we find a good enough reason to change it, also, the name must pass ^[a-zA-Z0-9.-_]+$ regex
|
||||
options.CustomSchemaIds(static type => type.ToString().Replace('+', '-'));
|
||||
|
||||
options.EnableAnnotations(true, true);
|
||||
|
||||
options.SchemaFilter<CustomAttributesSchemaFilter>();
|
||||
options.SchemaFilter<EnumSchemaFilter>();
|
||||
options.SchemaFilter<ReadOnlyFixesSchemaFilter>();
|
||||
|
||||
options.SwaggerDoc(
|
||||
SharedInfo.ASF, new OpenApiInfo {
|
||||
Contact = new OpenApiContact {
|
||||
Name = SharedInfo.GithubRepo,
|
||||
Url = new Uri(SharedInfo.ProjectURL)
|
||||
},
|
||||
|
||||
License = new OpenApiLicense {
|
||||
Name = SharedInfo.LicenseName,
|
||||
Url = new Uri(SharedInfo.LicenseURL)
|
||||
},
|
||||
|
||||
Title = $"{SharedInfo.AssemblyName} API",
|
||||
Version = SharedInfo.Version.ToString()
|
||||
}
|
||||
);
|
||||
|
||||
string xmlDocumentationFile = Path.Combine(AppContext.BaseDirectory, SharedInfo.AssemblyDocumentation);
|
||||
|
||||
if (File.Exists(xmlDocumentationFile)) {
|
||||
options.IncludeXmlComments(xmlDocumentationFile);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// We need MVC for /Api, but we're going to use only a small subset of all available features
|
||||
IMvcBuilder mvc = services.AddControllers();
|
||||
|
||||
// Add support for controllers declared in custom plugins
|
||||
if (PluginsCore.ActivePlugins.Count > 0) {
|
||||
HashSet<Assembly>? assemblies = PluginsCore.LoadAssemblies();
|
||||
|
||||
if (assemblies != null) {
|
||||
foreach (Assembly assembly in assemblies) {
|
||||
mvc.AddApplicationPart(assembly);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (IWebServiceProvider plugin in PluginsCore.ActivePlugins.OfType<IWebServiceProvider>()) {
|
||||
try {
|
||||
plugin.OnConfiguringServices(services);
|
||||
} catch (Exception e) {
|
||||
ASF.ArchiLogger.LogGenericException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mvc.AddControllersAsServices();
|
||||
|
||||
mvc.AddJsonOptions(
|
||||
static options => {
|
||||
JsonSerializerOptions jsonSerializerOptions = Debugging.IsUserDebugging ? JsonUtilities.IndentedJsonSerialierOptions : JsonUtilities.DefaultJsonSerialierOptions;
|
||||
|
||||
options.JsonSerializerOptions.PropertyNamingPolicy = jsonSerializerOptions.PropertyNamingPolicy;
|
||||
options.JsonSerializerOptions.TypeInfoResolver = jsonSerializerOptions.TypeInfoResolver;
|
||||
options.JsonSerializerOptions.WriteIndented = jsonSerializerOptions.WriteIndented;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static async Task<WebApplication> CreateWebApplication() {
|
||||
string customDirectory = Path.Combine(Directory.GetCurrentDirectory(), SharedInfo.WebsiteDirectory);
|
||||
string websiteDirectory = Directory.Exists(customDirectory) ? customDirectory : Path.Combine(AppContext.BaseDirectory, SharedInfo.WebsiteDirectory);
|
||||
|
||||
// The order of dependency injection matters, pay attention to it
|
||||
WebApplicationBuilder builder = WebApplication.CreateEmptyBuilder(
|
||||
new WebApplicationOptions {
|
||||
ApplicationName = SharedInfo.AssemblyName,
|
||||
ContentRootPath = SharedInfo.HomeDirectory,
|
||||
WebRootPath = websiteDirectory
|
||||
}
|
||||
);
|
||||
|
||||
// Enable NLog integration for logging
|
||||
builder.Logging.SetMinimumLevel(Debugging.IsUserDebugging ? LogLevel.Trace : LogLevel.Warning);
|
||||
builder.Logging.AddNLogWeb(new NLogAspNetCoreOptions { ShutdownOnDispose = false });
|
||||
|
||||
// Check if custom config is available
|
||||
string absoluteConfigDirectory = Path.Combine(Directory.GetCurrentDirectory(), SharedInfo.ConfigDirectory);
|
||||
string customConfigPath = Path.Combine(absoluteConfigDirectory, SharedInfo.IPCConfigFile);
|
||||
bool customConfigExists = File.Exists(customConfigPath);
|
||||
|
||||
if (customConfigExists) {
|
||||
if (Debugging.IsDebugConfigured) {
|
||||
try {
|
||||
string json = await File.ReadAllTextAsync(customConfigPath).ConfigureAwait(false);
|
||||
|
||||
if (!string.IsNullOrEmpty(json)) {
|
||||
JsonNode? jsonNode = JsonNode.Parse(json);
|
||||
|
||||
ASF.ArchiLogger.LogGenericDebug($"{SharedInfo.IPCConfigFile}: {jsonNode?.ToJsonText(true) ?? "null"}");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
ASF.ArchiLogger.LogGenericException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Set up custom config to be used
|
||||
builder.WebHost.UseConfiguration(new ConfigurationBuilder().SetBasePath(absoluteConfigDirectory).AddJsonFile(SharedInfo.IPCConfigFile, false, true).Build());
|
||||
}
|
||||
|
||||
builder.WebHost.ConfigureKestrel(
|
||||
options => {
|
||||
options.AddServerHeader = false;
|
||||
|
||||
if (customConfigExists) {
|
||||
// Use custom config for Kestrel configuration
|
||||
options.Configure(builder.Configuration.GetSection("Kestrel"));
|
||||
} else {
|
||||
// Use ASFB defaults for Kestrel
|
||||
options.ListenLocalhost(1242);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (customConfigExists) {
|
||||
// User might be using HTTPS when providing custom config, use full implementation of Kestrel for that scenario
|
||||
builder.WebHost.UseKestrel();
|
||||
} else {
|
||||
// We don't need extra features when not using custom config
|
||||
builder.WebHost.UseKestrelCore();
|
||||
}
|
||||
|
||||
ConfigureServices(builder.Configuration, builder.Services);
|
||||
|
||||
WebApplication result = builder.Build();
|
||||
|
||||
ConfigureApp(builder.Configuration, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void OnPrepareResponse(StaticFileResponseContext context) {
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
if (context.File is not { Exists: true, IsDirectory: false } || string.IsNullOrEmpty(context.File.Name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
string extension = Path.GetExtension(context.File.Name);
|
||||
|
||||
CacheControlHeaderValue cacheControl = new();
|
||||
|
||||
switch (extension.ToUpperInvariant()) {
|
||||
case ".CSS" or ".JS":
|
||||
// Add support for SRI-protected static files
|
||||
// SRI requires from us to notify the caller (especially proxy) to avoid modifying the data
|
||||
cacheControl.NoTransform = true;
|
||||
|
||||
goto default;
|
||||
default:
|
||||
// Instruct the caller to always ask us first about every file it requests
|
||||
// Contrary to the name, this doesn't prevent client from caching, but rather informs it that it must verify with us first that their cache is still up-to-date
|
||||
// This is used to handle ASF and user updates to WWW root, we don't want the client to ever use outdated scripts
|
||||
cacheControl.NoCache = true;
|
||||
|
||||
// All static files are public by definition, we don't have any authorization here
|
||||
cacheControl.Public = true;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
ResponseHeaders headers = context.Context.Response.GetTypedHeaders();
|
||||
|
||||
headers.CacheControl = cacheControl;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
@@ -180,7 +182,7 @@ public sealed class ASFController : ArchiController {
|
||||
return BadRequest(new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(request.Channel))));
|
||||
}
|
||||
|
||||
(bool success, string? message, Version? version) = await Actions.Update(request.Channel).ConfigureAwait(false);
|
||||
(bool success, string? message, Version? version) = await Actions.Update(request.Channel, request.Forced).ConfigureAwait(false);
|
||||
|
||||
if (string.IsNullOrEmpty(message)) {
|
||||
message = success ? Strings.Success : Strings.WarningFailed;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// |
|
||||
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user