Compare commits

...

161 Commits

Author SHA1 Message Date
Łukasz Domeradzki
e326cc94d0 Misc 2024-03-26 01:43:15 +01:00
Łukasz Domeradzki
66237dab24 Back to .15... 2024-03-26 01:16:18 +01:00
Łukasz Domeradzki
53ec07a7f1 Correct plugin names we distribute outselves 2024-03-26 01:15:17 +01:00
Łukasz Domeradzki
bbad80ae0e Bump 2024-03-26 00:39:32 +01:00
Łukasz Domeradzki
e6021a4f11 Misc 2024-03-26 00:36:04 +01:00
Łukasz Domeradzki
b3f792a2f3 Misc 2024-03-26 00:31:35 +01:00
Łukasz Domeradzki
7083a397b8 Squashed commit of the following:
commit ea03c2dab0fe884ed2c2db1acc6ab003ef71a684
Author: Łukasz Domeradzki <JustArchi@JustArchi.net>
Date:   Mon Mar 25 23:59:50 2024 +0100

    Attempt 2

commit cb9e67d0d94df76f49ad58f225b35a2661b44100
Author: Łukasz Domeradzki <JustArchi@JustArchi.net>
Date:   Mon Mar 25 23:45:13 2024 +0100

    Ship monitoring plugin attempt 1
2024-03-26 00:29:23 +01:00
Łukasz Domeradzki
f98fd85fe9 Misc 2024-03-25 23:03:49 +01:00
Sebastian Göls
f9f6e207d4 Add official monitoring plugin (#3160)
* Add Monitoring plugin

* Prepare pipeline

* Fix Rider stupidity

* Fix Windows build

* Remove translation files

* Apply feedback

* Add steam id as additional tag to metrics

* Apply feedback

* Add runtime metrics

* Fix my brain not braining

* Use extension methods to add instrumentation and Add monitoring for outbound HTTP traffic

* Upgrade OpenTelemetry.Extensions.Hosting to prerelease due to runtime exception

* Remove config and add file that was supposed to be committed yesterday to fix the runtime exception

* Revert changes to publish.yml

* Remove localization

* Apply feedback

* Apply feedback

* Fix version number

* Revert use of property in Kestrel (even tho it's an outside caller to the source class)
2024-03-25 22:58:03 +01:00
Sebastian Göls
0c4e4e709f Add POST /Api/Plugins/Update (#3173) 2024-03-25 22:57:47 +01:00
renovate[bot]
d7f9ad4916 chore(deps): update asf-ui digest to b57627e 2024-03-25 17:40:33 +00:00
renovate[bot]
b04f92ca3c chore(deps): update jetbrains/qodana-action action to v2023.3.2 2024-03-25 09:55:54 +00:00
ArchiBot
16a0174cf1 Automatic translations update 2024-03-24 02:06:44 +00:00
renovate[bot]
c92d78e088 chore(deps): update asf-ui digest to bdb3808 2024-03-23 03:19:40 +00:00
ArchiBot
1f7b8d2416 Automatic translations update 2024-03-23 02:04:54 +00:00
renovate[bot]
7ca9b23aa3 chore(deps): update github/codeql-action action to v3.24.9 2024-03-22 16:16:04 +00:00
Archi
f0acd8ddfd Bump 2024-03-22 17:15:35 +01:00
Archi
c5009dad31 Decrease itemsCountPerRequest to 5000, optimize performance a bit, closes #3171 2024-03-22 16:34:46 +01:00
Archi
9682d1d5ef Down to 5000? 2024-03-22 16:16:38 +01:00
Archi
a4e8fca784 Keep decreasing itemsCountPerRequest 2024-03-22 15:50:29 +01:00
Archi
8f25040bf5 Decrease itemsCountPerRequest to help resolve #3171 2024-03-22 15:17:28 +01:00
renovate[bot]
34beec8d6b chore(deps): update asf-ui digest to 2f7a98e 2024-03-22 07:39:05 +00:00
ArchiBot
6c0fd638a5 Automatic translations update 2024-03-22 02:04:31 +00:00
Archi
0b94213af7 Misc
We originally had ifdefs here for netf, so const made sense, they don't anymore.
2024-03-22 01:10:39 +01:00
Archi
f874598fd8 Misc 2024-03-21 22:37:09 +01:00
Archi
a1adcd3a5a Misc refactor 2024-03-21 22:32:15 +01:00
Archi
1596b98e25 Misc 2024-03-21 21:55:46 +01:00
renovate[bot]
9b0ad5c545 chore(deps): update wiki digest to 4869495 2024-03-21 07:57:02 +00:00
renovate[bot]
6a47b30987 chore(deps): update asf-ui digest to 2b813a2 2024-03-21 03:31:32 +00:00
renovate[bot]
79594a9d00 chore(deps): update crowdin/github-action action to v1.20.0 2024-03-20 22:10:52 +00:00
renovate[bot]
38a9818482 chore(deps): update asf-ui digest to e91b9a0 2024-03-20 20:52:25 +00:00
Archi
a407d538f5 Bump 2024-03-20 21:52:03 +01:00
Archi
44bea85296 Fix and derequire type text in confirmation
Even if we have it always available, we don't need use it 99.9% of time, and even in 0.1% it's only supportive attribute for debugging. Make it optional, will help with robustness.
2024-03-20 21:51:28 +01:00
renovate[bot]
915d26adfa chore(deps): update wiki digest to 5e52590 2024-03-20 12:52:53 +00:00
Archi
3dd4248587 Bump 2024-03-20 13:52:15 +01:00
Archi
7969e58fbf Remove wrong failsafe 2024-03-20 13:51:43 +01:00
Archi
7043ebda23 Bump 2024-03-20 11:58:17 +01:00
Archi
4c0a5b7553 Make it possible to call updateplugins without args 2024-03-20 11:50:51 +01:00
Archi
533fbe0c2f Respect updateOverride when updating plugins 2024-03-20 11:36:14 +01:00
Archi
437dfd5f02 Allow forced plugin updates as well 2024-03-20 11:13:10 +01:00
Archi
5f4962ddcc Update IPluginUpdates.cs 2024-03-20 09:49:52 +01:00
Archi
7efa609a13 Update IPluginUpdates.cs 2024-03-20 04:47:12 +01:00
Archi
0d7049ea7c Misc 2024-03-20 04:46:09 +01:00
Archi
997e7f0420 Add bool asfUpdate to IPluginUpdates 2024-03-20 04:42:18 +01:00
Archi
96619b9565 Bump 2024-03-20 04:20:05 +01:00
Archi
4c2a786e54 Add downgrade possibility in update command 2024-03-20 04:16:22 +01:00
Archi
469b1571e1 Misc refactor of login results 2024-03-20 03:44:21 +01:00
Vita Chumakova
3bc66e0d27 Add family join confirmation type (#3166) 2024-03-20 03:20:23 +01:00
ArchiBot
9a4fac600a Automatic translations update 2024-03-20 02:04:23 +00:00
renovate[bot]
546ba8b68f chore(deps): update asf-ui digest to 25ce8ef 2024-03-19 19:28:12 +00:00
Archi
ed29c0633f Bump 2024-03-19 15:05:56 +01:00
Archi
35b1135b50 Update Bot.cs 2024-03-19 14:50:22 +01:00
Archi
90403c98c9 Bump 2024-03-19 13:55:34 +01:00
Archi
71a1c0574a Of course I had to forget about something important 2024-03-19 13:55:09 +01:00
Archi
ba64a50bb5 Bump 2024-03-19 13:28:52 +01:00
Archi
05131b2b76 Misc 2024-03-19 13:26:44 +01:00
Archi
0894eaee28 Closes #3162 2024-03-19 13:24:24 +01:00
Archi
6f2fd4eccc Closes #3163 2024-03-19 13:03:09 +01:00
Archi
04b534bda1 SK2 3.0 2024-03-19 12:40:54 +01:00
Archi
3620796d6d Misc 2024-03-19 11:07:50 +01:00
Archi
a321a38ccd Update publish.yml 2024-03-19 10:29:45 +01:00
Archi
ea965dfa85 Bump 2024-03-19 09:58:40 +01:00
Archi
f381106de2 Misc enhancements 2024-03-19 09:53:04 +01:00
Citrinate
b9ab3d6490 Fix GetTradeOffer exception (#3164) 2024-03-19 09:35:05 +01:00
renovate[bot]
4cf1d1e08b chore(deps): update asf-ui digest to a0ab4b2 2024-03-19 04:33:21 +00:00
ArchiBot
cba6e4df64 Automatic translations update 2024-03-19 02:05:23 +00:00
Archi
0dd6f38748 Move GetServerTime() from AWH to AH 2024-03-18 23:48:30 +01:00
renovate[bot]
970ba437a0 chore(deps): update github/codeql-action action to v3.24.8 2024-03-18 14:46:04 +00:00
Archi
e0bbbe3894 Update publish.yml 2024-03-18 15:45:37 +01:00
Archi
504791b5b6 Misc 2024-03-18 15:44:36 +01:00
Archi
6a6d0b48b9 Bump 2024-03-18 14:18:23 +01:00
Archi
84ff83bbe2 Improve performance when matching multiple users 2024-03-18 13:52:12 +01:00
Archi
787bcc3546 Extract ItemsMatcher-exclusively parts out of ASF core, decrease dependency on DeepClone() 2024-03-18 13:45:13 +01:00
Archi
fd811d8cf4 Implement DeepClone() for asset and description 2024-03-18 12:44:29 +01:00
Archi
3fa743f64b Add body for asset
It makes sense to expose entire underlying asset to the callers, as underlying body might have features they like, such as currencyid or est_usd - values that do not exist in json and we're not making use of them, but we still want to keep if provided e.g. by ArchiHandler.
2024-03-18 12:22:07 +01:00
Archi
ec374c050a Misc 2024-03-18 11:54:42 +01:00
Archi
5a07f8a2a3 Make descriptions optional, open constructors for plugins
In rare occurances, we might not have a description assigned to the item. This is most notable in inactive trade offers, but we permit this to happen even in inventory fetches.

Assigning "default" description is unwanted if caller wants to have a way to determine that description wasn't there to begin with. It makes more sense to make it nullable and *expect* it to be null, then caller can do appropriate checking and decide what they want to do with that.

Also open constructors for plugins usage in case they'd like to construct assets manually, e.g. for sending.
2024-03-18 11:53:14 +01:00
renovate[bot]
91b09dc43f chore(deps): update asf-ui digest to b112518 2024-03-18 04:33:16 +00:00
ArchiBot
8d6d355a3b Automatic translations update 2024-03-18 02:04:51 +00:00
renovate[bot]
da6fd69398 chore(deps): update wiki digest to be281d8 2024-03-17 21:07:32 +00:00
Archi
0e623cfd15 Bump 2024-03-17 22:06:55 +01:00
Archi
1c01d8f59f Fix tradableOnly/marketableOnly not working properly 2024-03-17 22:06:30 +01:00
Archi
5723ee7b19 Misc 2024-03-17 17:16:45 +01:00
Archi
1d85292451 Bump 2024-03-17 16:03:58 +01:00
renovate[bot]
0d65f5174b chore(deps): update asf-ui digest to b241705 2024-03-17 07:14:25 +00:00
Archi
b7f34f0d5d Misc 2024-03-17 03:47:08 +01:00
Archi
4ffcea72b0 Misc 2024-03-17 03:40:28 +01:00
Archi
9c5fade596 Final touches 2024-03-17 03:36:02 +01:00
Archi
331ecc1cc8 Add edge cases compatibility for plugins 2024-03-17 03:11:22 +01:00
ArchiBot
6428e5abd1 Automatic translations update 2024-03-17 02:05:53 +00:00
Archi
b86f83a634 Misc 2024-03-17 02:54:28 +01:00
Archi
ff55e09783 Apply guid json converter only where we need it 2024-03-17 02:44:49 +01:00
Archi
d7d24d5e47 Misc fix 2024-03-17 02:44:40 +01:00
Archi
48a14136a9 Update all file headers, again 2024-03-17 02:35:40 +01:00
Archi
c9acbb7bf2 Big post-PR cleanup 2024-03-17 02:29:04 +01:00
Archi
f98a159799 File header update 2024-03-17 00:06:13 +01:00
Vita Chumakova
184232995d Inventory fetching through CM (#3155)
* New inventory fetching

* use new method everywhere

* Store description in the asset, add protobuf body as a backing field for InventoryDescription, add properties to description

* parse trade offers as json, stub descriptions, fix build

* formatting, misc fixes

* fix pragma comments

* fix passing tradable property

* fix convesion of assets, add compatibility method

* fix fetching tradeoffers

* use 40k as default count per request

* throw an exception instead of silencing the error
2024-03-16 23:57:25 +01:00
Łukasz Domeradzki
aedede3ba4 Implement plugin updates with IPluginUpdates interface (#3151)
* Initial implementation of plugin updates

* Update PluginsCore.cs

* Update IPluginUpdates.cs

* Update PluginsCore.cs

* Make it work

* Misc

* Revert "Misc"

This reverts commit bccd1bb2b8.

* Proper fix

* Make plugin updates independent of GitHub

* Final touches

* Misc

* Allow plugin creators for more flexibility in picking from GitHub releases

* Misc rename

* Make changelog internal again

This is ASF implementation detail, make body available instead and let people implement changelogs themselves

* Misc

* Add missing localization

* Add a way to disable plugin updates

* Update PluginsCore.cs

* Update PluginsCore.cs

* Misc

* Update IGitHubPluginUpdates.cs

* Update IGitHubPluginUpdates.cs

* Update IGitHubPluginUpdates.cs

* Update IGitHubPluginUpdates.cs

* Make zip selection ignore case

* Update ArchiSteamFarm/Core/Utilities.cs

Co-authored-by: Vita Chumakova <me@ezhevita.dev>

* Misc error notify

* Add commands and finally call it a day

* Misc progress percentages text

* Misc

* Flip DefaultPluginsUpdateMode as per the voting

* Misc

---------

Co-authored-by: Vita Chumakova <me@ezhevita.dev>
2024-03-16 23:56:57 +01:00
renovate[bot]
c874779c0d chore(deps): update dependency microsoft.identitymodel.jsonwebtokens to v7.4.1 2024-03-16 00:47:06 +00:00
renovate[bot]
4a2457d571 chore(deps): update asf-ui digest to 8b1b833 2024-03-15 23:20:40 +00:00
ArchiBot
65fdd4196a Automatic translations update 2024-03-15 02:05:20 +00:00
renovate[bot]
19eefff525 chore(deps): update dependency markdig.signed to v0.36.2 2024-03-14 19:16:09 +00:00
renovate[bot]
626c18ba23 chore(deps): update docker/setup-buildx-action action to v3.2.0 2024-03-14 16:43:54 +00:00
renovate[bot]
d9c8dc1e2d chore(deps): update docker/build-push-action action to v5.3.0 2024-03-14 14:39:15 +00:00
renovate[bot]
25fa5cabbb chore(deps): update dependency markdig.signed to v0.36.0 2024-03-14 11:13:36 +00:00
Vita Chumakova
21c9dac593 Misc minimize fixes after #3158 (#3159)
* Misc minimize fixes after #3158

* only "iconify" escape sequence support is needed
2024-03-14 12:13:16 +01:00
renovate[bot]
617dccbd9a chore(deps): update asf-ui digest to 49f688e 2024-03-14 04:39:59 +00:00
ArchiBot
2f2665e2ad Automatic translations update 2024-03-14 02:04:37 +00:00
Archi
fcc0d70cd1 Fix build 2024-03-14 01:23:29 +01:00
Archi
06b2cf4ff5 Misc 2024-03-14 01:11:49 +01:00
Vita Chumakova
8642b0775e Flash console window on input request on Windows (#3158)
* Flash console window on input request

* Use BELL character instead of Beep, fix flash struct, add support for minimizing and flashing with Windows Terminal

* cross-platform minimization, use alert char instead of number, fix struct again

* remove console window

* formatting

* use MainWindowHandle if it's set (fix flashing winterm if ASF is launched in conhost)

* fix build

* remove support for flashing winterm
2024-03-14 01:08:00 +01:00
renovate[bot]
c193858973 chore(deps): update docker/login-action action to v3.1.0 2024-03-13 17:41:19 +00:00
renovate[bot]
362e921a27 chore(deps): update asf-ui digest to 9faaefe 2024-03-13 03:57:24 +00:00
ArchiBot
e21fa45718 Automatic translations update 2024-03-13 02:05:37 +00:00
renovate[bot]
caae270ea8 chore(deps): update github/codeql-action action to v3.24.7 2024-03-12 19:43:24 +00:00
ArchiBot
4843d539f0 Automatic translations update 2024-03-12 02:04:54 +00:00
Archi
fa8e649288 Fix JB header
Don't update all files just yet, wait for ongoing PRs to finish
2024-03-12 00:05:34 +01:00
Archi
20708ad900 Open ToSteamClientLanguage() for plugins usage 2024-03-11 23:56:47 +01:00
ArchiBot
5e0b9551da Automatic translations update 2024-03-11 02:04:50 +00:00
ArchiBot
e43adf4de7 Automatic translations update 2024-03-10 02:06:54 +00:00
Archi
afe16b8eb1 Bump 2024-03-09 22:15:22 +01:00
Archi
e9f9663714 Fix https crash for kestrel core 2024-03-09 21:54:14 +01:00
renovate[bot]
d514157903 chore(deps): update wiki digest to c156e89 2024-03-09 17:37:26 +00:00
Archi
56490f2d86 Bump 2024-03-09 18:36:48 +01:00
Archi
1fd6c8a477 Minimize dependencies for starting IPC server
Previously WebApplication didn't offer any advantages over generic Host, but with release of .NET 8 there is now slim and empty builders, which limit amount of initialized dependencies and allow us to skip some unnecessary features in default pipeline.
2024-03-09 18:24:15 +01:00
Archi
e7bdd408be Bump, misc 2024-03-09 16:59:05 +01:00
renovate[bot]
f73b4737b6 chore(deps): update docker/build-push-action action to v5.2.0 2024-03-08 10:22:24 +00:00
ArchiBot
14c905b8ef Automatic translations update 2024-03-07 01:56:16 +00:00
Łukasz Domeradzki
fa7849460c Update Bug-report.yml 2024-03-06 00:23:41 +01:00
renovate[bot]
4d12c9117f chore(deps): update actions/download-artifact action to v4.1.4 2024-03-02 04:14:53 +00:00
ArchiBot
e2f08fe35b Automatic translations update 2024-03-02 02:03:09 +00:00
Archi
0089a87018 Misc 2024-03-02 01:22:47 +01:00
ArchiBot
6325c454bc Automatic translations update 2024-03-01 02:06:50 +00:00
renovate[bot]
6b1f64579a chore(deps): update github/codeql-action action to v3.24.6 2024-02-29 17:05:50 +00:00
renovate[bot]
14cfd61615 chore(deps): update asf-ui digest to 3c96528 2024-02-29 04:11:33 +00:00
ArchiBot
f2a8768f80 Automatic translations update 2024-02-29 02:03:36 +00:00
Archi
556f3fdac0 Misc 2024-02-28 21:40:54 +01:00
renovate[bot]
59124fcf68 chore(deps): update asf-ui digest to bec199f 2024-02-28 17:47:01 +00:00
ArchiBot
71643446db Automatic translations update 2024-02-28 02:04:29 +00:00
renovate[bot]
a93ca58e9c chore(deps): update dependency microsoft.identitymodel.jsonwebtokens to v7.4.0 2024-02-27 23:03:52 +00:00
Archi
f12e87c2f4 Update SUPPORT.md 2024-02-27 23:58:35 +01:00
renovate[bot]
d48c96604b chore(deps): update docker/setup-buildx-action action to v3.1.0 2024-02-27 10:07:20 +00:00
renovate[bot]
0976bbfd2d chore(deps): update actions/download-artifact action to v4.1.3 2024-02-27 00:46:20 +00:00
Archi
6f66518607 Bump 2024-02-27 01:45:51 +01:00
Archi
e4c20df4a8 Misc 2024-02-27 01:44:21 +01:00
Archi
5135677360 Add back missing STJ feature of non-standard Guid deserialization 2024-02-27 01:34:56 +01:00
Archi
f6004f558b Closes #3147 2024-02-27 00:42:32 +01:00
renovate[bot]
ddf08c4dc0 chore(deps): update asf-ui digest to f916fb3 2024-02-26 19:35:51 +00:00
ArchiBot
388eaf614d Automatic translations update 2024-02-26 02:05:51 +00:00
renovate[bot]
3a0768f9ef chore(deps): update asf-ui digest to 05de7b9 2024-02-25 21:40:17 +00:00
ArchiBot
cb0e04c022 Automatic translations update 2024-02-25 02:05:54 +00:00
Archi
55cdac205d Merge branch 'main' of https://github.com/JustArchiNET/ArchiSteamFarm 2024-02-24 18:39:16 +01:00
Archi
1bc0d6f16c Archi had too much coding lately 2024-02-24 18:39:09 +01:00
renovate[bot]
f7377a7043 chore(deps): update asf-ui digest to d0d90a5 2024-02-24 04:29:58 +00:00
ArchiBot
76f1ad45dd Automatic translations update 2024-02-24 02:03:45 +00:00
renovate[bot]
f21ffde803 chore(deps): update github/codeql-action action to v3.24.5 2024-02-23 16:55:14 +00:00
renovate[bot]
e9c96f175f chore(deps): update asf-ui digest to f2ef756 2024-02-23 14:02:04 +00:00
Archi
0f9a4c7c31 Update Commands.cs 2024-02-23 14:19:09 +01:00
Archi
87451615e8 Extract addlicense logic to actions 2024-02-23 14:14:16 +01:00
ArchiBot
fa19aaae2e Automatic translations update 2024-02-23 02:08:38 +00:00
Archi
6b8280fceb Bump 2024-02-23 02:49:20 +01:00
278 changed files with 6006 additions and 2457 deletions

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Submodule ASF-ui updated: cd1173a0d6...b57627e28c

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

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

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

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

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

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

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

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

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

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

View File

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

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

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

View File

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

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

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

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

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

View File

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

View File

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

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

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

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

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

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

View File

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

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

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

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

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

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

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

View File

@@ -1,8 +1,10 @@
// _ _ _ ____ _ _____
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

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

View File

@@ -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"> _ _ _ ____ _ _____&#xD;
<s:String x:Key="/Default/CodeStyle/FileHeader/FileHeaderText/@EntryValue">----------------------------------------------------------------------------------------------&#xD;
_ _ _ ____ _ _____&#xD;
/ \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___&#xD;
/ _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \&#xD;
/ ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |&#xD;
/_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|&#xD;
----------------------------------------------------------------------------------------------&#xD;
|&#xD;
Copyright 2015-${CurrentDate.Year} Łukasz "JustArchi" Domeradzki&#xD;
Contact: JustArchi@JustArchi.net&#xD;

View File

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

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

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

View File

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

View File

@@ -1,8 +1,10 @@
// _ _ _ ____ _ _____
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

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

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

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

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

View File

@@ -1,8 +1,10 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net

View File

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

View File

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

View File

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

View File

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