Compare commits

...

404 Commits

Author SHA1 Message Date
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
Archi
7a13895429 Fix false positives 2024-02-23 02:49:03 +01:00
Archi
75668ea099 Bump 2024-02-23 02:43:06 +01:00
Archi
d3aa881f55 Fix STD serialization after STJ changes 2024-02-23 02:42:18 +01:00
Archi
33a1fdf556 Misc 2024-02-23 01:18:09 +01:00
Archi
0c848ec366 Misc 2024-02-23 01:15:35 +01:00
Archi
21d4e46b81 Bump 2024-02-22 23:45:13 +01:00
Archi
7a21c7bc45 Don't copy all variant-specific files if ASFVariant is not declared 2024-02-22 23:44:06 +01:00
renovate[bot]
9358aa5d1f chore(deps): update github/codeql-action action to v3.24.4 2024-02-22 15:09:20 +00:00
Archi
6c9e9da740 Modernize unit tests 2024-02-22 16:08:54 +01:00
renovate[bot]
a78a607f73 chore(deps): update asf-ui digest to cd1173a 2024-02-22 03:20:14 +00:00
ArchiBot
879323ed20 Automatic translations update 2024-02-22 02:04:27 +00:00
renovate[bot]
4e081b26a1 chore(deps): update asf-ui digest to e055bd1 2024-02-21 13:02:17 +00:00
Archi
6cd3459dd4 Misc 2024-02-21 13:20:26 +01:00
Archi
482576f16d Add extra constructors also to other public collections 2024-02-21 13:18:23 +01:00
Archi
f63dbffee3 Add extra constructors 2024-02-21 13:08:32 +01:00
renovate[bot]
d9cdc806fe chore(deps): update wiki digest to 03614e1 2024-02-21 04:05:58 +00:00
Archi
36a78b55a4 Good idea 2024-02-21 03:46:25 +01:00
Archi
ab983099cc Misc 2024-02-21 03:32:14 +01:00
Archi
aab8f5f0b5 BIG BUMP 2024-02-21 03:11:32 +01:00
Łukasz Domeradzki
6b0bf0f9c1 Closes #3061 (#3145)
* Good start

* Misc

* Make ApiAuthenticationMiddleware use new json

* Remove first newtonsoft dependency

* Pull latest ASFB json enhancements

* Start reimplementing newtonsoft!

* One thing at a time

* Keep doing all kind of breaking changes which need to be tested later

* Add back ShouldSerialize() support

* Misc

* Eradicate remaining parts of newtonsoft

* WIP

* Workaround STJ stupidity in regards to derived types

STJ can't serialize derived type properties by default, so we'll use another approach in our serializable file function

* Make CI happy

* Bunch of further fixes

* Fix AddFreeLicense() after rewrite

* Add full support for JsonDisallowNullAttribute

* Optimize our json utilities even further

* Misc

* Add support for fields in disallow null

* Misc optimization

* Fix deserialization of GlobalCache in STD

* Fix non-public [JsonExtensionData]

* Fix IM missing method exception, correct db storage helpers

* Fix saving into generic databases

Thanks STJ

* Make Save() function abstract to force inheritors to implement it properly

* Correct ShouldSerializeAdditionalProperties to be a method

* Misc cleanup

* Code review

* Allow JSON comments in configs, among other

* Allow trailing commas in configs

Users very often add them accidentally, no reason to throw on them

* Fix confirmation ID

Probably needs further fixes, will need to check later

* Correct confirmations deserialization

* Use JsonNumberHandling

* Misc

* Misc

* [JsonDisallowNull] corrections

* Forbid [JsonDisallowNull] on non-nullable structs

* Not really but okay

* Add and use ToJson() helpers

* Misc

* Misc
2024-02-21 03:09:36 +01:00
ArchiBot
3968130e15 Automatic translations update 2024-02-21 02:04:51 +00:00
ArchiBot
dd488a1c9c Automatic translations update 2024-02-20 02:03:29 +00:00
renovate[bot]
de3c332803 chore(deps): update asf-ui digest to 0e9cf35 2024-02-19 22:19:08 +00:00
renovate[bot]
684b7b6e28 chore(deps): update asf-ui digest to 7b31664 2024-02-19 16:19:48 +00:00
ArchiBot
d36f16e205 Automatic translations update 2024-02-18 02:05:50 +00:00
renovate[bot]
c3ab3d767f chore(deps): update asf-ui digest to 82412dc 2024-02-17 22:55:28 +00:00
renovate[bot]
a790c34976 chore(deps): update dependency markdig.signed to v0.35.0 2024-02-17 06:16:26 +00:00
ArchiBot
be208eab92 Automatic translations update 2024-02-17 02:04:16 +00:00
renovate[bot]
c9598709d6 chore(deps): update asf-ui digest to 6eca08a 2024-02-16 09:31:51 +00:00
ArchiBot
367e7f841c Automatic translations update 2024-02-16 02:05:09 +00:00
renovate[bot]
8c988cfed6 chore(deps): update asf-ui digest to eebc86f 2024-02-15 21:33:19 +00:00
renovate[bot]
5b15535661 chore(deps): update github/codeql-action action to v3.24.3 2024-02-15 14:21:59 +00:00
renovate[bot]
e0c692e0ab chore(deps): update wiki digest to 5ea73d0 2024-02-15 04:18:35 +00:00
Archi
1ddf6b34e2 Misc lang correction 2024-02-15 03:50:27 +01:00
ArchiBot
8476c8c221 Automatic translations update 2024-02-14 02:04:50 +00:00
renovate[bot]
24ec938565 chore(deps): update mstest monorepo to v3.2.1 2024-02-13 22:23:04 +00:00
renovate[bot]
0dbfba275e chore(deps): update asf-ui digest to 661a7d1 2024-02-13 19:08:50 +00:00
renovate[bot]
55c5c70b52 chore(deps): update github/codeql-action action to v3.24.1 2024-02-13 15:20:35 +00:00
renovate[bot]
17aa8297da chore(deps): update crowdin/github-action action to v1.19.0 2024-02-13 10:47:17 +00:00
ArchiBot
b0aa2a9104 Automatic translations update 2024-02-13 02:04:51 +00:00
Archi
898c402dfc Misc 2024-02-12 19:20:53 +01:00
Archi
9258819c84 Misc 2024-02-12 15:24:41 +01:00
ArchiBot
b5db0b511c Automatic translations update 2024-02-12 02:05:33 +00:00
renovate[bot]
7c6804449c chore(deps): update wiki digest to e004808 2024-02-11 21:50:56 +00:00
ArchiBot
ff28e2bf8f Automatic translations update 2024-02-11 02:06:02 +00:00
Archi
3510cd1be0 Bump 2024-02-10 18:31:26 +01:00
Archi
5320e8d9cc Misc refactor after #3142 2024-02-10 18:03:32 +01:00
LRFLEW
6ab9e2958d Use FixedTimeEquals for IPC Password testing (#3142) 2024-02-10 17:42:45 +01:00
ArchiBot
e47f286bda Automatic translations update 2024-02-10 02:03:17 +00:00
ArchiBot
84b1599ca6 Automatic translations update 2024-02-09 02:04:10 +00:00
Archi
c5396a8ec8 Update publish.yml 2024-02-08 12:43:42 +01:00
Archi
9a9f18184b Misc 2024-02-08 12:37:50 +01:00
Archi
10d97e16e3 Closes #3140 2024-02-08 12:31:43 +01:00
renovate[bot]
5ece500396 chore(deps): update asf-ui digest to 7406f71 2024-02-08 04:42:12 +00:00
ArchiBot
1a311513ca Automatic translations update 2024-02-08 02:04:11 +00:00
renovate[bot]
6e0e4835d1 chore(deps): update ncipollo/release-action action to v1.14.0 2024-02-07 16:15:35 +00:00
renovate[bot]
ae4a784c3a chore(deps): update asf-ui digest to 558b879 2024-02-07 13:01:40 +00:00
renovate[bot]
287be65e7f chore(deps): update crowdin/github-action action to v1.18.0 2024-02-07 10:34:04 +00:00
renovate[bot]
476653a6cc chore(deps): update actions/setup-node action to v4.0.2 2024-02-07 07:24:21 +00:00
renovate[bot]
407496efd6 chore(deps): update dependency microsoft.net.test.sdk to v17.9.0 2024-02-07 04:18:59 +00:00
ArchiBot
9995b807f9 Automatic translations update 2024-02-07 02:04:09 +00:00
Archi
a6f4692b75 Update TrimmerRoots.xml 2024-02-07 00:16:45 +01:00
Archi
024b7ff824 Bump 2024-02-07 00:16:11 +01:00
Archi
68e46096ad Closes #3137 2024-02-07 00:15:50 +01:00
renovate[bot]
d9f5f60854 chore(deps): update wiki digest to 8f35971 2024-02-06 19:30:47 +00:00
Archi
82ccd4ddce Use different text for private apps 2024-02-06 20:08:21 +01:00
renovate[bot]
5f69b337a6 chore(deps): update actions/upload-artifact action to v4.3.1 2024-02-06 13:05:46 +00:00
Archi
dbbb6802d4 Misc 2024-02-06 12:16:16 +01:00
renovate[bot]
459cb44ff4 chore(deps): update actions/download-artifact action to v4.1.2 2024-02-06 07:31:32 +00:00
renovate[bot]
c1ebc813d5 chore(deps): update asf-ui digest to 630dbb8 2024-02-06 03:42:11 +00:00
ArchiBot
948c42055b Automatic translations update 2024-02-06 02:04:37 +00:00
Archi
06d049d882 Bump 2024-02-06 02:15:36 +01:00
Archi
04f5e91e92 Closes #3109 2024-02-06 02:14:54 +01:00
renovate[bot]
02a479ba13 chore(deps): update wiki digest to 024c6b4 2024-02-06 00:19:11 +00:00
Archi
01b99d20f6 Try to bulletproof against #3119 a bit 2024-02-05 20:49:34 +01:00
Archi
9c6cd7f692 Refactor FarmingResetSemaphore into FarmingResetEvent 2024-02-05 20:41:24 +01:00
Archi
7899829dc7 Refactor docker containers to use /asf
In order to keep compatibility with existing containers, we'll use the same paths for user-related overrides, that is, /app/config, /app/logs or /app/plugins. Instead, the only thing we'll do is moving ASF away from /app to new /asf directory, which will hopefully limit amount of screwups that users are doing within existing /app directory.

Also while at it, add symlink for a bit better integration.
2024-02-05 16:26:54 +01:00
ArchiBot
d0693d362a Automatic translations update 2024-02-05 02:07:18 +00:00
Archi
227066f355 Bump 2024-02-04 22:31:10 +01:00
Archi
348c43b259 Skip spamming ASFB with requests from unlicensed users
Check license prior to fetching inventory and sending data to ASFB, will also limit traffic on Steam side.
2024-02-04 22:28:59 +01:00
ArchiBot
a23b7e1594 Automatic translations update 2024-02-04 02:06:40 +00:00
Archi
1c7952f8dd Bump 2024-02-03 22:02:57 +01:00
Archi
f0ef4c6ba6 Implement basic crash protection
- On start, we create/load crash file, if previous startup was less than 5 minutes ago, we also increase the counter
- If counter reaches 5, we abort the process, that is, freeze the process by not loading anything other than auto-updates
- In order for user to recover from above, he needs to manually remove ASF.crash file
- If process exits normally without reaching counter of 5 yet, we remove crash file (reset the counter), normal exit includes SIGTERM, SIGINT, clicking [X], !exit, !restart, and anything else that allows ASF to gracefully quit
- If process exits abnormally, that includes SIGKILL, unhandled exceptions, as well as power outages and anything that prevents ASF from graceful quit, we keep crash file around
- Update procedure, as an exception, allows crash file removal even with counter of 5. This should allow crash file recovery for people that crashed before, without a need of manual removal.
2024-02-03 21:18:47 +01:00
renovate[bot]
eb71b640c5 chore(deps): update dependency microsoft.identitymodel.jsonwebtokens to v7.3.1 2024-02-03 08:28:40 +00:00
renovate[bot]
6b7a0ff1ce chore(deps): update github/codeql-action action to v3.24.0 2024-02-02 19:35:45 +00:00
Archi
d08740b6d7 Schedule refresh of licenses for STD plugin with delay
This addresses two things:
- It allows for better load-balancing, as STD refresh can be postponed for a short while after bot logs in - it has more important matters to handle right away, and STD is optional/supportive plugin.
- It helps @xPaw sleep better at night working around fools with their ASFs crashing thirty times per second due to third-party plugins.
2024-02-02 13:17:55 +01:00
ArchiBot
70e3649e60 Automatic translations update 2024-02-02 02:04:28 +00:00
renovate[bot]
24d79f9b20 chore(deps): update asf-ui digest to 6ca9093 2024-02-01 22:02:19 +00:00
Archi
43c2ae6746 Remove obsolete stuff 2024-02-01 14:27:56 +01:00
Archi
4bd2a3ec7f Bump 2024-02-01 14:22:53 +01:00
renovate[bot]
a90b375a72 chore(deps): update asf-ui digest to e3fd680 2024-02-01 03:36:01 +00:00
renovate[bot]
9f1e562a19 chore(deps): update asf-ui digest to 7b4a361 2024-01-31 18:14:11 +00:00
renovate[bot]
4edfcff2e0 chore(deps): update asf-ui digest to 9607b07 2024-01-31 14:06:00 +00:00
renovate[bot]
d4b48ab235 chore(deps): update crowdin/github-action action to v1.17.0 2024-01-31 08:04:58 +00:00
ArchiBot
f1e5c31110 Automatic translations update 2024-01-31 02:04:51 +00:00
renovate[bot]
32c4522b47 chore(deps): update asf-ui digest to b207bca 2024-01-30 23:15:29 +00:00
Archi
fc760e1a84 Misc 2024-01-30 14:30:52 +01:00
Archi
2a92bf4dec Bump 2024-01-30 13:51:18 +01:00
Archi
716b253a04 Move from System.IdentityModel.Tokens.Jwt to Microsoft.IdentityModel.JsonWebTokens
> As of IdentityModel 7x, this is a legacy tool that should be replaced with Microsoft.IdentityModel.JsonWebTokens.

> This is a newer, faster version of System.IdentityModel.Tokens.Jwt that has additional functionality
2024-01-30 13:26:32 +01:00
renovate[bot]
0384365315 chore(deps): update dependency system.identitymodel.tokens.jwt to v7.3.0 2024-01-30 06:46:07 +00:00
renovate[bot]
45dc910e01 chore(deps): update asf-ui digest to b341e7f 2024-01-30 04:38:00 +00:00
ArchiBot
f7e57e7d39 Automatic translations update 2024-01-30 02:04:51 +00:00
Archi
ace4151bbc Remove dead code 2024-01-29 19:05:14 +01:00
Archi
088c3a35ed Bump 2024-01-29 18:57:54 +01:00
Archi
608bece8dc Misc 2024-01-29 18:49:29 +01:00
Archi
119caebfa8 Deprecate CachedAccessToken, move to Bot.AccessToken instead
Thanks to @xPaw findings, it seems that access token we get on logon can be used for all functionality we require in ASF. This means we no longer need to fetch the one from points shop in AWH and can safely remove that.

Since access token in AWH is public API, this commit:
- Makes Bot.AccessToken public API.
- Deprecates ArchiWebHandler.CachedAccessToken with intention of removal in the next version. Until then, it resolves to Bot.AccessToken internally so all plugins can keep working during transition period.
- Deprecates Utilities.ReadJwtToken(), probably nobody else than me used it, just switch over to Utilities.TryReadJwtToken(), much better design.
- Reverts ArchiCacheable parts back to stable API, as we no longer need the breaking change done in #3133
2024-01-29 18:42:21 +01:00
Archi
2e0771b8d9 Closes #3133
After investigation, it turns out that the token actually has correct scope (THANK GOD), it's the fact that Valve started issuing those on much shorter notice than our cache.

Up until now, we played it smartly by assuming cached access token should be valid for at least 6 hours, since every time we visited the page, we got a new token that was valid for 24h since issuing. This however is no longer the case and Valve seems to recycle the same token for every request now, probably until we get close to its expiration. This also means that with unlucky timing, we might be trying to use access token that has expired already even for up to 6 more hours, which is unwanted and causes all kind of issues, with 403 in trade offers being one of them.

I could make stupid solution and cache token for shorter, e.g. 1 minute, but instead I did 200 IQ move and rewrote the functionality in a way to actually parse that token, its validity, and set the cache to be valid for a brief moment before the token actually expires. This way, we're not only more efficient (we can cache the token even for 24h if needed), but we're also invalidating it as soon as it goes out of the scope.
2024-01-29 17:53:46 +01:00
Archi
a08080a2ce Do not treat NU190x as error
Selected NU190x warnings can happen retroactively when given library is found with vulnerabilities. While this is important for development and for building, we should not retroactively cause selected git tags fail to build purely because a package we references was found to be vulnerable - warning during build is sufficient.

Resolves https://aur.archlinux.org/packages/asf and other sources trying to build older tag such as 5.5.1.4 as of today. Will apply from future release naturally.
2024-01-28 16:05:10 +01:00
renovate[bot]
d4bcdfde3e chore(deps): update asf-ui digest to abebbe3 2024-01-28 03:59:05 +00:00
ArchiBot
249ebfb590 Automatic translations update 2024-01-28 02:06:03 +00:00
ArchiBot
56fc50e5ad Automatic translations update 2024-01-27 02:03:38 +00:00
renovate[bot]
5d8db36753 chore(deps): update github/codeql-action action to v3.23.2 2024-01-26 17:09:43 +00:00
renovate[bot]
54f493467e chore(deps): update asf-ui digest to 2111058 2024-01-26 02:15:52 +00:00
ArchiBot
7ed39db953 Automatic translations update 2024-01-26 02:05:48 +00:00
Archi
cc28d7520e Misc 2024-01-25 23:34:58 +01:00
renovate[bot]
109b307d0f chore(deps): update peter-evans/dockerhub-description action to v4 (#3132)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-25 22:11:27 +01:00
renovate[bot]
178ecc2e4a chore(deps): update asf-ui digest to 89945a1 2024-01-25 20:38:29 +00:00
renovate[bot]
ddfa7220d8 chore(deps): update mstest monorepo to v3.2.0 2024-01-25 06:39:34 +00:00
ArchiBot
812523baef Automatic translations update 2024-01-25 02:10:26 +00:00
renovate[bot]
c5fd423a78 chore(deps): update wiki digest to 1324210 2024-01-24 19:28:33 +00:00
Archi
0b6c308d2e Bump 2024-01-24 16:29:27 +01:00
renovate[bot]
d6ef5e5404 chore(deps): update actions/upload-artifact action to v4.3.0 2024-01-24 13:06:56 +00:00
Archi
9c304b8965 Actually make --ignore-unsupported-environment ignore the issue 2024-01-24 12:45:46 +01:00
Archi
05c5a7fc30 Closes #3128 2024-01-24 12:43:36 +01:00
renovate[bot]
2a2d4f09f1 chore(deps): update asf-ui digest to 97c6b06 2024-01-24 04:57:59 +00:00
ArchiBot
ce5cd7bc8f Automatic translations update 2024-01-24 02:09:10 +00:00
renovate[bot]
a072a7b7d6 chore(deps): update wiki digest to d8d3cd4 2024-01-23 22:59:22 +00:00
Archi
8a52f4fbbb Closes #3126
Smartly putting it in the middle making breaking change, since nobody managed to reference this public API yet! \o/
2024-01-23 23:01:22 +01:00
Archi
ba07405d9a Refactor selected boolean bot config properties
All of them are common enough to be contained into a single flags property, this will vastly improve readability of the bot config, among being ready to add more properties in the future without polluting it.

Also hooray for 6 bytes less of memory usage of each bot, glorious.
2024-01-23 22:49:33 +01:00
Archi
6239457f01 Closes #3127 2024-01-23 21:23:06 +01:00
renovate[bot]
584fe4bd37 chore(deps): update wiki digest to 01a1155 2024-01-23 06:32:47 +00:00
renovate[bot]
1f4fa2ed90 chore(deps): update asf-ui digest to 80c331a 2024-01-23 04:45:51 +00:00
renovate[bot]
d24731999a chore(deps): update jetbrains/qodana-action action to v2023.3.1 2024-01-22 23:04:28 +00:00
ArchiBot
1a3e82a1b0 Automatic translations update 2024-01-22 02:11:05 +00:00
ArchiBot
f45b10f2ff Automatic translations update 2024-01-21 02:11:05 +00:00
ArchiBot
2097fea6a0 Automatic translations update 2024-01-20 02:07:31 +00:00
renovate[bot]
c4bdb39c6d chore(deps): update crowdin/github-action action to v1.16.1 2024-01-19 15:58:09 +00:00
renovate[bot]
2f2b411293 chore(deps): update asf-ui digest to 9105e9b 2024-01-19 11:09:53 +00:00
renovate[bot]
860b979afb chore(deps): update actions/upload-artifact action to v4.2.0 2024-01-19 00:18:38 +00:00
renovate[bot]
401d3f65f5 chore(deps): update asf-ui digest to e77a557 2024-01-18 22:35:27 +00:00
renovate[bot]
051cf043f3 chore(deps): update asf-ui digest to d8b90fb 2024-01-18 07:09:47 +00:00
renovate[bot]
4ebb4cfd9e chore(deps): update github/codeql-action action to v3.23.1 2024-01-18 02:20:11 +00:00
ArchiBot
d58a2d2717 Automatic translations update 2024-01-18 02:08:26 +00:00
renovate[bot]
51b764a39f chore(deps): update asf-ui digest to 38f6759 2024-01-17 20:20:07 +00:00
renovate[bot]
0f94a05a69 chore(deps): update asf-ui digest to cd3a947 2024-01-17 06:54:52 +00:00
ArchiBot
16fd3c067f Automatic translations update 2024-01-17 02:08:36 +00:00
ArchiBot
e13881763c Automatic translations update 2024-01-16 02:08:17 +00:00
renovate[bot]
90241c6076 chore(deps): update asf-ui digest to 344cfcf 2024-01-15 04:06:46 +00:00
ArchiBot
d020a97209 Automatic translations update 2024-01-15 02:09:55 +00:00
renovate[bot]
d9ceea448f chore(deps): update asf-ui digest to 255221f 2024-01-14 11:09:25 +00:00
renovate[bot]
ec01846653 chore(deps): update asf-ui digest to 9919655 2024-01-13 10:20:06 +00:00
renovate[bot]
7d3c0a9d13 chore(deps): update asf-ui digest to e1b0524 2024-01-13 05:36:41 +00:00
ArchiBot
de88e3072b Automatic translations update 2024-01-13 02:07:46 +00:00
renovate[bot]
5e2ad8eb19 chore(deps): update actions/upload-artifact action to v4.1.0 2024-01-12 18:29:45 +00:00
renovate[bot]
127107a96c chore(deps): update asf-ui digest to fa12667 2024-01-12 07:29:26 +00:00
Archi
1587c6facb Bump 2024-01-11 16:51:05 +01:00
Archi
042fadca28 Merge branch 'main' of https://github.com/JustArchiNET/ArchiSteamFarm 2024-01-11 16:46:48 +01:00
Archi
4a9e6f6cc6 Deprioritize bots with 1-game inventory
Those are usually stash accounts, and while we still want to match them, we can leave them only as a last resort if no other bots are available.

This decreases chance of hitting a bot that was just recently turned off or had its items traded away, as what usually happens with such accounts.
2024-01-11 16:46:45 +01:00
renovate[bot]
b7ac24eb7b chore(deps): update asf-ui digest to 392b585 2024-01-11 06:54:53 +00:00
renovate[bot]
c78d26a701 chore(deps): update dependency system.identitymodel.tokens.jwt to v7.2.0 2024-01-11 00:47:47 +00:00
renovate[bot]
53614661c1 chore(deps): update asf-ui digest to cd30e8d 2024-01-10 21:09:00 +00:00
renovate[bot]
ea8c300a1a chore(deps): update actions/download-artifact action to v4.1.1 2024-01-10 19:29:00 +00:00
renovate[bot]
bb91bf3918 chore(deps): update dependency system.identitymodel.tokens.jwt to v7.1.2 2024-01-09 18:38:55 +00:00
renovate[bot]
b9f72c293d chore(deps): update asf-ui digest to 701562c 2024-01-09 03:29:49 +00:00
renovate[bot]
7a14a394c2 chore(deps): update github/codeql-action action to v3.23.0 (#3122)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-08 18:31:49 +01:00
Sebastian Göls
dbf7148fbe Happy new year! (#3121)
Co-authored-by: Sebastian Göls <sebastian.goels@salvagninigroup.com>
2024-01-08 11:33:28 +01:00
ArchiBot
59d51d1b15 Automatic translations update 2024-01-08 02:08:23 +00:00
renovate[bot]
acff4602cd chore(deps): update asf-ui digest to 8a350cd 2024-01-07 03:34:22 +00:00
ArchiBot
44f135eb14 Automatic translations update 2024-01-07 02:10:04 +00:00
renovate[bot]
f4945c024e chore(deps): update asf-ui digest to f30f6ea 2024-01-06 04:22:52 +00:00
ArchiBot
a329e2a3da Automatic translations update 2024-01-06 02:06:55 +00:00
renovate[bot]
608edf2569 chore(deps): update asf-ui digest to e2a9bdb 2024-01-05 04:04:42 +00:00
ArchiBot
f203e02a45 Automatic translations update 2024-01-05 02:07:53 +00:00
renovate[bot]
bdcc00af1f chore(deps): update asf-ui digest to ede4734 2024-01-04 21:04:35 +00:00
renovate[bot]
21d004fb26 chore(deps): update crowdin/github-action action to v1.16.0 2024-01-04 13:47:19 +00:00
renovate[bot]
d4ae307676 chore(deps): update asf-ui digest to 6ce634f 2024-01-04 04:05:08 +00:00
ArchiBot
0a9993e85a Automatic translations update 2024-01-04 02:07:12 +00:00
Archi
1f2269dcf2 Misc code syntax 2024-01-03 15:06:08 +01:00
Archi
bb73916af0 Misc optimizations 2024-01-03 14:57:49 +01:00
Archi
12c4b7e924 Apply frozen collections optimizations 2024-01-03 13:46:54 +01:00
renovate[bot]
c6b78b118c chore(deps): update asf-ui digest to 8a2b86b 2024-01-03 07:37:18 +00:00
Archi
aaa6b6674a Bump 2024-01-03 00:34:34 +01:00
Archi
be2e173404 Misc followup 2024-01-03 00:34:12 +01:00
Archi
bfb189d55b Bump 2024-01-03 00:29:41 +01:00
Archi
3d503ed5ee Fix invalid heartbeats from inactive STM accounts
It was possible before if the inventory state was the same as previously announced, even if server purged the info long time ago. Also, add required logic for recovery if that happens regardless.
2024-01-03 00:23:27 +01:00
Archi
ab01733860 Bump 2024-01-02 01:21:00 +01:00
Archi
dd0949b58d Fix enum pointer failures in swagger schema 2024-01-02 01:19:28 +01:00
Archi
d825f74489 Bump 2024-01-01 23:25:50 +01:00
Archi
7f1ecdd585 Misc 2024-01-01 23:22:19 +01:00
Archi
01b2e205be Make std command return before refresh finishes 2024-01-01 23:16:52 +01:00
Archi
d398e84f25 Misc 2024-01-01 23:00:58 +01:00
Archi
ac427ed1ec Misc match improvements 2024-01-01 22:58:54 +01:00
ArchiBot
0118ccb614 Automatic translations update 2023-12-31 02:08:18 +00:00
renovate[bot]
697e059b66 chore(deps): update asf-ui digest to d9c38ab 2023-12-30 04:15:17 +00:00
renovate[bot]
45539018f5 chore(deps): update dependency nlog.web.aspnetcore to v5.3.8 2023-12-29 22:50:02 +00:00
renovate[bot]
1ebf2b6272 chore(deps): update asf-ui digest to 99254ec 2023-12-29 19:06:24 +00:00
Archi
800dae280a Bump 2023-12-29 13:24:44 +01:00
Archi
bccdf269f0 Closes #3115 2023-12-29 13:23:38 +01:00
renovate[bot]
757072cb01 chore(deps): update asf-ui digest to e52146a 2023-12-29 03:25:23 +00:00
renovate[bot]
b601d31d5c chore(deps): update asf-ui digest to 43ce08a 2023-12-28 18:49:37 +00:00
renovate[bot]
86ae501ce5 chore(deps): update asf-ui digest to bb4c28c 2023-12-27 04:47:07 +00:00
ArchiBot
c22e5c146f Automatic translations update 2023-12-27 02:06:17 +00:00
renovate[bot]
4a710b4ffe chore(deps): update crazy-max/ghaction-import-gpg action to v6.1.0 2023-12-26 20:02:14 +00:00
renovate[bot]
f6ad3747f4 chore(deps): update asf-ui digest to 3eb41f0 2023-12-26 04:04:41 +00:00
ArchiBot
ec205bb7a2 Automatic translations update 2023-12-26 02:06:22 +00:00
renovate[bot]
464edddacf chore(deps): update asf-ui digest to 2e91573 2023-12-25 04:12:10 +00:00
ArchiBot
e6c6bce8a7 Automatic translations update 2023-12-25 02:07:40 +00:00
renovate[bot]
6a413b4d29 chore(deps): update asf-ui digest to 2dbdfaa 2023-12-24 19:43:31 +00:00
ArchiBot
4f30e2e3c7 Automatic translations update 2023-12-24 02:08:02 +00:00
Archi
2bef94e3b4 Misc 2023-12-23 23:37:29 +01:00
Archi
cf94c417d2 Misc 2023-12-23 23:16:44 +01:00
ArchiBot
e82100308c Automatic translations update 2023-12-23 02:05:36 +00:00
renovate[bot]
ada67a0f97 chore(deps): update github/codeql-action action to v3.22.12 2023-12-22 06:50:53 +00:00
renovate[bot]
1bd20f7144 chore(deps): update asf-ui digest to 88a332c 2023-12-22 04:51:36 +00:00
ArchiBot
9b295ad85e Automatic translations update 2023-12-22 02:06:52 +00:00
Archi
d1953215e8 Bump 2023-12-22 00:21:25 +01:00
Archi
e480aca8b2 Use inventories items deduplication logic aligned with ASFB 2023-12-22 00:18:52 +01:00
Archi
2befe20f76 Use set parts also as inventories request optimization 2023-12-21 23:46:42 +01:00
ArchiBot
c16485ad0b Automatic translations update 2023-12-21 02:07:13 +00:00
Archi
f036e99450 Misc
Specifying target framework should no longer be needed, as we have only one left
2023-12-20 18:46:19 +01:00
renovate[bot]
2804a36920 chore(deps): update asf-ui digest to 295cb26 2023-12-20 04:08:32 +00:00
ArchiBot
30e62813c7 Automatic translations update 2023-12-20 01:57:07 +00:00
renovate[bot]
621ce390c2 chore(deps): update asf-ui digest to 53e75ab 2023-12-19 23:06:09 +00:00
renovate[bot]
8d40423d9d chore(deps): update asf-ui digest to 71014eb 2023-12-19 15:49:29 +00:00
renovate[bot]
1cf8959b92 chore(deps): update actions/download-artifact action to v4.1.0 2023-12-19 04:26:50 +00:00
ArchiBot
56aafe3374 Automatic translations update 2023-12-19 02:07:52 +00:00
renovate[bot]
5570bd2999 chore(deps): update actions/setup-node action to v4.0.1 2023-12-18 14:02:50 +00:00
ArchiBot
9bec394436 Automatic translations update 2023-12-18 02:08:05 +00:00
ArchiBot
3ae1a7ccfd Automatic translations update 2023-12-17 02:08:56 +00:00
renovate[bot]
e0b1c4c16f chore(deps): update asf-ui digest to 68c799a 2023-12-16 19:31:03 +00:00
Archi
eb5bc560a4 Misc 2023-12-16 14:33:32 +01:00
Archi
f9309b7c54 Bump 2023-12-16 14:28:13 +01:00
renovate[bot]
3c2a154b39 chore(deps): update asf-ui digest to b924e3b 2023-12-16 07:42:41 +00:00
ArchiBot
23d07eb43e Automatic translations update 2023-12-16 02:06:58 +00:00
Archi
20af0edd4d Misc 2023-12-15 14:22:22 +01:00
Archi
4b29daabd4 Fix possible NRE 2023-12-15 14:20:58 +01:00
renovate[bot]
a60513e998 chore(deps): update asf-ui digest to 531e8ec 2023-12-15 03:03:17 +00:00
ArchiBot
188b96b951 Automatic translations update 2023-12-15 02:08:24 +00:00
renovate[bot]
b8e9dca6d3 chore(deps): update dependency markdig.signed to v0.34.0 2023-12-14 21:15:35 +00:00
Archi
157537c6ec Bump 2023-12-14 22:15:07 +01:00
renovate[bot]
a363b92075 chore(deps): update actions/download-artifact action to v4 (#3099)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-14 21:44:05 +01:00
renovate[bot]
f993d3d365 chore(deps): update actions/upload-artifact action to v4 (#3100)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-14 21:43:56 +01:00
Jack Nolddor
3f4520edf3 chore: blacklist Winter Sale 2023 appid (#3098) 2023-12-14 21:09:56 +01:00
Archi
6e7041d8c5 Fix possible unhandled exception crash
We should throw ONLY when caller asked us to cancel, not when httpclient's timeout is reached.

Thanks @nolddor
2023-12-14 21:07:48 +01:00
Archi
e6cf5971a6 Update ArchiSteamFarm.sln.DotSettings 2023-12-14 14:22:53 +01:00
Archi
845af42080 Update qodana.yaml 2023-12-14 14:18:16 +01:00
Archi
c74481590b Update qodana.yaml 2023-12-14 14:10:14 +01:00
Archi
0ac5447198 Update qodana.yaml 2023-12-14 14:03:19 +01:00
ArchiBot
e0428f8a91 Automatic translations update 2023-12-14 02:07:39 +00:00
renovate[bot]
cc0d2cb1d4 Update wiki digest to e1c80a5 2023-12-13 23:16:06 +00:00
Archi
a0769eaf9a Bump 2023-12-14 00:15:37 +01:00
renovate[bot]
dc35545043 Update github/codeql-action action to v3 (#3097)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-13 18:15:41 +01:00
Archi
890709429c Misc 2023-12-13 10:02:19 +01:00
Archi
6f6a561b9e Update code-quality.yml 2023-12-13 09:46:20 +01:00
ArchiBot
91d314c861 Automatic translations update 2023-12-13 02:08:27 +00:00
renovate[bot]
8abae9d4be Update github/codeql-action action to v2.22.10 2023-12-12 18:36:05 +00:00
Archi
26a390760e Merge branch 'main' of https://github.com/JustArchiNET/ArchiSteamFarm 2023-12-12 14:34:54 +01:00
Archi
87933a2c92 Update ArchiSteamFarm.sln.DotSettings 2023-12-12 14:34:50 +01:00
ArchiBot
1f843bb5d6 Automatic translations update 2023-12-12 02:08:22 +00:00
renovate[bot]
e87e78a372 Update ASF-ui digest to f84a296 2023-12-11 23:25:18 +00:00
Archi
91c82302bb Disable stack trace support in trimmed builds 2023-12-12 00:24:59 +01:00
Archi
d5a41dce1d Misc 2023-12-12 00:05:19 +01:00
Archi
40ab1d848c .NET 8 code enhancements 2023-12-11 23:55:13 +01:00
renovate[bot]
cc3a0a4144 Update JetBrains/qodana-action action to v2023.3.0 2023-12-11 16:31:06 +00:00
renovate[bot]
5448403f43 Update wiki digest to 15af6e4 2023-12-11 13:19:51 +00:00
Archi
636dd139c2 Misc 2023-12-11 11:42:19 +01:00
Archi
e7ad69be26 Closes #3093 2023-12-11 11:38:37 +01:00
ArchiBot
2f56b6dc3a Automatic translations update 2023-12-11 02:08:21 +00:00
renovate[bot]
ba7073df98 Update github/codeql-action action to v2.22.9 2023-12-10 12:22:37 +00:00
renovate[bot]
457bf6dfbb Update dependency NLog.Web.AspNetCore to v5.3.7 2023-12-10 11:22:34 +00:00
renovate[bot]
1e6279e1ca Update ASF-ui digest to 44cbd70 2023-12-10 08:40:06 +00:00
ArchiBot
6e455d0eba Automatic translations update 2023-12-09 02:06:46 +00:00
ArchiBot
9c9a74b448 Automatic translations update 2023-12-08 02:08:17 +00:00
ArchiBot
b7790961fc Automatic translations update 2023-12-07 02:08:13 +00:00
renovate[bot]
5ecc050b12 Update wiki digest to d849fb2 2023-12-06 12:31:12 +00:00
Sebastian Göls
c9819bde7f Add duplicate-check to issue templates (#3092)
* Update Bug-report.yml

* Update Enhancement-idea.yml

* Update Enhancement-idea.yml

* Update Bug-report.yml
2023-12-06 13:30:54 +01:00
ArchiBot
12660449ed Automatic translations update 2023-12-06 02:08:19 +00:00
renovate[bot]
7237e0affc Update wiki digest to 05c3655 2023-12-05 13:17:54 +00:00
ArchiBot
85bb68825b Automatic translations update 2023-12-05 02:08:13 +00:00
Archi
48b8a28c7a Bump 2023-12-05 01:55:23 +01:00
296 changed files with 9128 additions and 4577 deletions

View File

@@ -36,7 +36,7 @@ csharp_prefer_simple_default_expression = true:warning
csharp_prefer_simple_using_statement = true:warning
csharp_prefer_static_local_function = true:warning
csharp_preferred_modifier_order = public, protected, internal, private, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async:warning
csharp_preferred_modifier_order = public, protected, internal, private, file, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, required, volatile, async:warning
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = false
@@ -61,7 +61,6 @@ csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = none
csharp_space_between_square_brackets = false
csharp_style_conditional_delegate_call = true:warning
@@ -79,15 +78,27 @@ csharp_style_expression_bodied_properties = true:warning
csharp_style_implicit_object_creation_when_type_is_apparent = true:warning
csharp_style_inlined_variable_declaration = true:warning
csharp_style_pattern_local_over_anonymous_function = true:warning
csharp_style_namespace_declarations = file_scoped:warning
csharp_style_pattern_matching_over_as_with_null_check = true:warning
csharp_style_pattern_matching_over_is_with_cast_check = true:warning
csharp_style_prefer_extended_property_pattern = true:warning
dotnet_style_prefer_foreach_explicit_cast_in_source = always:warning
csharp_style_prefer_index_operator = true:warning
csharp_style_prefer_local_over_anonymous_function = true:warning
csharp_style_prefer_method_group_conversion = true:warning
csharp_style_prefer_not_pattern = true:warning
csharp_style_prefer_null_check_over_type_check = true:warning
csharp_style_prefer_pattern_matching = true:warning
csharp_style_prefer_primary_constructors = true:warning
csharp_style_prefer_range_operator = true:warning
csharp_style_prefer_readonly_struct = true:warning
csharp_style_prefer_readonly_struct_member = true:warning
csharp_style_prefer_switch_expression = true:warning
csharp_style_prefer_top_level_statements = false:warning
csharp_style_prefer_tuple_swap = true:warning
csharp_style_prefer_utf8_string_literals = true:warning
csharp_style_throw_expression = true:warning
@@ -98,13 +109,12 @@ csharp_style_var_elsewhere = false:warning
csharp_style_var_for_built_in_types = false:warning
csharp_style_var_when_type_is_apparent = false:warning
csharp_using_directive_placement = outside_namespace
csharp_using_directive_placement = outside_namespace:warning
###############################
# .NET Coding Conventions #
###############################
[*.{cs,vb}]
dotnet_analyzer_diagnostic.severity = warning
dotnet_code_quality.ca3003.excluded_symbol_names = BotController
@@ -186,6 +196,7 @@ dotnet_sort_system_directives_first = true
dotnet_style_coalesce_expression = true:warning
dotnet_style_collection_initializer = true:warning
dotnet_style_explicit_tuple_names = true:warning
dotnet_style_namespace_match_folder = true:warning
dotnet_style_null_propagation = true:warning
dotnet_style_object_initializer = true:warning
@@ -219,7 +230,7 @@ dotnet_style_require_accessibility_modifiers = always:warning
# JetBrains, IntelliJ/Rider #
###############################
[*.{csproj,props,xml}]
[*.{csproj,props,resx,xml}]
ij_xml_keep_blank_lines = 1
ij_xml_keep_line_breaks = false
ij_xml_keep_line_breaks_in_text = false

View File

@@ -12,6 +12,8 @@ body:
required: true
- label: I also read **[Setting-up](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Setting-up)** and **[FAQ](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/FAQ)**, I don't need **[help](https://github.com/JustArchiNET/ArchiSteamFarm/blob/main/.github/SUPPORT.md)**, this is a bug report
required: true
- label: This is not a **[duplicate](https://github.com/JustArchiNET/ArchiSteamFarm/issues?q=is%3Aissue)** of an existing issue
required: true
- label: I don't own more than **[10 accounts in total](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/FAQ#how-many-bots-can-i-run-with-asf)**
required: true
- label: I'm not using **[custom plugins](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Plugins)**
@@ -147,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)

View File

@@ -12,6 +12,8 @@ body:
required: true
- label: I also read **[Setting-up](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Setting-up)** and **[FAQ](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/FAQ)**, I don't need **[help](https://github.com/JustArchiNET/ArchiSteamFarm/blob/main/.github/SUPPORT.md)**, this is an enhancement idea
required: true
- label: This is not a **[duplicate](https://github.com/JustArchiNET/ArchiSteamFarm/issues?q=is%3Aissue)** of an existing issue
required: true
- label: My idea doesn't duplicate existing ASF functionality described on the **[wiki](https://github.com/JustArchiNET/ArchiSteamFarm/wiki)**
required: true
- label: I believe that my idea falls into ASF's scope and should be offered as part of ASF built-in functionality

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.15.2
uses: crowdin/github-action@v1.19.0
with:
crowdin_branch_name: main
config: '.github/crowdin.yml'

View File

@@ -33,15 +33,13 @@ jobs:
show-progress: false
- name: Run Qodana scan
if: false # TODO: Enable GitHub results after qodana starts reporting meaningful stuff
uses: JetBrains/qodana-action@v2023.2.9
uses: JetBrains/qodana-action@v2023.3.1
with:
args: --property=idea.headless.enable.statistics=false
env:
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
- name: Report Qodana results to GitHub
if: false # TODO: Enable GitHub results after qodana starts reporting meaningful stuff
uses: github/codeql-action/upload-sarif@v2.22.8
uses: github/codeql-action/upload-sarif@v3.24.8
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 }}
@@ -74,7 +74,7 @@ jobs:
push: true
- name: Update DockerHub repository description
uses: peter-evans/dockerhub-description@v3.4.2
uses: peter-evans/dockerhub-description@v4.0.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}

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

@@ -24,7 +24,7 @@ jobs:
submodules: recursive
- name: Setup Node.js with npm
uses: actions/setup-node@v4.0.0
uses: actions/setup-node@v4.0.2
with:
check-latest: true
node-version: ${{ env.NODE_JS_VERSION }}
@@ -42,7 +42,7 @@ jobs:
run: npm run-script deploy --no-progress --prefix ASF-ui
- name: Upload ASF-ui
uses: actions/upload-artifact@v3.1.3
uses: actions/upload-artifact@v4.3.1
with:
name: ASF-ui
path: ASF-ui/dist
@@ -88,7 +88,7 @@ jobs:
run: dotnet --info
- name: Download previously built ASF-ui
uses: actions/download-artifact@v3.0.2
uses: actions/download-artifact@v4.1.4
with:
name: ASF-ui
path: ASF-ui/dist
@@ -143,8 +143,17 @@ jobs:
$ProgressPreference = 'SilentlyContinue'
dotnet restore
if ($LastExitCode -ne 0) {
throw "Last command failed."
}
dotnet build ArchiSteamFarm -c "$env:CONFIGURATION" -p:ContinuousIntegrationBuild=true -p:TargetLatestRuntimePatch=false -p:UseAppHost=false --nologo
if ($LastExitCode -ne 0) {
throw "Last command failed."
}
- name: Prepare ArchiSteamFarm.OfficialPlugins.SteamTokenDumper on Unix
if: startsWith(matrix.os, 'macos-') || startsWith(matrix.os, 'ubuntu-')
env:
@@ -396,7 +405,7 @@ jobs:
}
- name: Upload ASF-${{ matrix.variant }}
uses: actions/upload-artifact@v3.1.3
uses: actions/upload-artifact@v4.3.1
with:
name: ${{ matrix.os }}_ASF-${{ matrix.variant }}
path: out/ASF-${{ matrix.variant }}.zip
@@ -416,55 +425,55 @@ jobs:
show-progress: false
- name: Download ASF-generic artifact from ubuntu-latest
uses: actions/download-artifact@v3.0.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@v3.0.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@v3.0.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@v3.0.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@v3.0.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@v3.0.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@v3.0.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@v3.0.2
uses: actions/download-artifact@v4.1.4
with:
name: windows-latest_ASF-win-x64
path: out
- name: Import GPG key for signing
uses: crazy-max/ghaction-import-gpg@v6.0.0
uses: crazy-max/ghaction-import-gpg@v6.1.0
with:
gpg_private_key: ${{ secrets.ARCHIBOT_GPG_PRIVATE_KEY }}
@@ -481,23 +490,26 @@ jobs:
)
- name: Upload SHA512SUMS
uses: actions/upload-artifact@v3.1.3
uses: actions/upload-artifact@v4.3.1
with:
name: SHA512SUMS
path: out/SHA512SUMS
- name: Upload SHA512SUMS.sign
uses: actions/upload-artifact@v3.1.3
uses: actions/upload-artifact@v4.3.1
with:
name: SHA512SUMS.sign
path: out/SHA512SUMS.sign
- name: Create ArchiSteamFarm GitHub release
uses: ncipollo/release-action@v1.13.0
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

@@ -30,7 +30,7 @@ jobs:
git reset --hard origin/master
- name: Download latest translations from Crowdin
uses: crowdin/github-action@v1.15.2
uses: crowdin/github-action@v1.19.0
with:
upload_sources: false
download_translations: true
@@ -42,7 +42,7 @@ jobs:
token: ${{ secrets.ASF_CROWDIN_API_TOKEN }}
- name: Import GPG key for signing
uses: crazy-max/ghaction-import-gpg@v6.0.0
uses: crazy-max/ghaction-import-gpg@v6.1.0
with:
gpg_private_key: ${{ secrets.ARCHIBOT_GPG_PRIVATE_KEY }}
git_config_global: true

2
ASF-ui

Submodule ASF-ui updated: 62a5d46a03...25ce8efeb9

View File

@@ -6,7 +6,6 @@
<ItemGroup>
<PackageReference Include="ConfigureAwaitChecker.Analyzer" PrivateAssets="all" />
<PackageReference Include="JetBrains.Annotations" PrivateAssets="all" />
<PackageReference Include="Newtonsoft.Json" IncludeAssets="compile" />
<PackageReference Include="SteamKit2" IncludeAssets="compile" />
<PackageReference Include="System.Composition.AttributedModel" IncludeAssets="compile" />
</ItemGroup>

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -21,16 +23,17 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Plugins.Interfaces;
using ArchiSteamFarm.Steam;
using ArchiSteamFarm.Steam.Data;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SteamKit2;
namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin;
@@ -45,31 +48,35 @@ namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin;
internal sealed class ExamplePlugin : IASF, IBot, IBotCommand2, IBotConnection, IBotFriendRequest, IBotMessage, IBotModules, IBotTradeOffer {
// This is used for identification purposes, typically you want to use a friendly name of your plugin here, such as the name of your main class
// Please note that this property can have direct dependencies only on structures that were initialized by the constructor, as it's possible to be called before OnLoaded() takes place
[JsonInclude]
[Required]
public string Name => nameof(ExamplePlugin);
// 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
[JsonInclude]
[Required]
public Version Version => typeof(ExamplePlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version));
// Plugins can expose custom properties for our GET /Api/Plugins API call, simply annotate them with [JsonProperty] (or keep public)
[JsonProperty]
public bool CustomIsEnabledField { get; private set; } = true;
[JsonInclude]
[Required]
public bool CustomIsEnabledField { get; private init; } = true;
// This method, apart from being called before any bot initialization takes place, allows you to read custom global config properties that are not recognized by ASF
// Thanks to that, you can extend default ASF config with your own stuff, then parse it here in order to customize your plugin during runtime
// Keep in mind that, as noted in the interface, additionalConfigProperties can be null if no custom, unrecognized properties are found by ASF, you should handle that case appropriately
// In addition to that, this method also guarantees that all plugins were already OnLoaded(), which allows cross-plugins-communication to be possible
public Task OnASFInit(IReadOnlyDictionary<string, JToken>? additionalConfigProperties = null) {
public Task OnASFInit(IReadOnlyDictionary<string, JsonElement>? additionalConfigProperties = null) {
if (additionalConfigProperties == null) {
return Task.CompletedTask;
}
foreach ((string configProperty, JToken configValue) in additionalConfigProperties) {
foreach ((string configProperty, JsonElement configValue) in additionalConfigProperties) {
// It's a good idea to prefix your custom properties with the name of your plugin, so there will be no possible conflict of ASF or other plugins using the same name, neither now or in the future
switch (configProperty) {
case $"{nameof(ExamplePlugin)}TestProperty" when configValue.Type == JTokenType.Boolean:
bool exampleBooleanValue = configValue.Value<bool>();
ASF.ArchiLogger.LogGenericInfo($"{nameof(ExamplePlugin)}TestProperty boolean property has been found with a value of: {exampleBooleanValue}");
case $"{nameof(ExamplePlugin)}TestProperty" when configValue.ValueKind == JsonValueKind.True:
ASF.ArchiLogger.LogGenericInfo($"{nameof(ExamplePlugin)}TestProperty boolean property has been found with a value of true");
break;
}
@@ -135,7 +142,7 @@ internal sealed class ExamplePlugin : IASF, IBot, IBotCommand2, IBotConnection,
// Keep in mind that, as noted in the interface, additionalConfigProperties can be null if no custom, unrecognized properties are found by ASF, you should handle that case appropriately
// Also keep in mind that this function can be called multiple times, e.g. when user edits their bot configs during runtime
// Take a look at OnASFInit() for example parsing code
public async Task OnBotInitModules(Bot bot, IReadOnlyDictionary<string, JToken>? additionalConfigProperties = null) {
public async Task OnBotInitModules(Bot bot, IReadOnlyDictionary<string, JsonElement>? additionalConfigProperties = null) {
// For example, we'll ensure that every bot starts paused regardless of Paused property, in order to do this, we'll just call Pause here in InitModules()
// Thanks to the fact that this method is called with each bot config reload, we'll ensure that our bot stays paused even if it'd get unpaused otherwise
bot.ArchiLogger.LogGenericInfo("Pausing this bot as asked from the plugin");

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -21,15 +23,17 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Newtonsoft.Json;
using System.Text.Json.Serialization;
namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin;
#pragma warning disable CA1812 // False positive, the class is used during json deserialization
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
internal sealed class MeowResponse {
[JsonProperty("url", Required = Required.Always)]
internal readonly Uri URL = null!;
[JsonInclude]
[JsonPropertyName("url")]
[JsonRequired]
internal Uri URL { get; private init; } = null!;
[JsonConstructor]
private MeowResponse() { }

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -20,8 +22,10 @@
// limitations under the License.
using System;
using System.ComponentModel.DataAnnotations;
using System.Composition;
using System.Runtime;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
@@ -38,8 +42,12 @@ internal sealed class PeriodicGCPlugin : IPlugin {
private static readonly object LockObject = new();
private static readonly Timer PeriodicGCTimer = new(PerformGC);
[JsonInclude]
[Required]
public string Name => nameof(PeriodicGCPlugin);
[JsonInclude]
[Required]
public Version Version => typeof(PeriodicGCPlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version));
public Task OnLoaded() {

View File

@@ -7,7 +7,6 @@
<PackageReference Include="AngleSharp.XPath" IncludeAssets="compile" />
<PackageReference Include="ConfigureAwaitChecker.Analyzer" PrivateAssets="all" />
<PackageReference Include="JetBrains.Annotations" PrivateAssets="all" />
<PackageReference Include="Newtonsoft.Json" IncludeAssets="compile" />
<PackageReference Include="System.Composition.AttributedModel" IncludeAssets="compile" />
</ItemGroup>

View File

@@ -1,18 +1,20 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// ----------------------------------------------------------------------------------------------
//
// 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.

View File

@@ -1,18 +1,20 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// ----------------------------------------------------------------------------------------------
//
// 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.
@@ -20,11 +22,12 @@
// limitations under the License.
using System;
using Newtonsoft.Json;
using System.Text.Json.Serialization;
namespace ArchiSteamFarm.CustomPlugins.SignInWithSteam.Data;
public sealed class SignInWithSteamRequest {
[JsonProperty(Required = Required.Always)]
public Uri RedirectURL { get; private set; } = null!;
[JsonInclude]
[JsonRequired]
public Uri RedirectURL { get; private init; } = null!;
}

View File

@@ -1,18 +1,20 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// ----------------------------------------------------------------------------------------------
//
// 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.
@@ -20,13 +22,14 @@
// limitations under the License.
using System;
using Newtonsoft.Json;
using System.Text.Json.Serialization;
namespace ArchiSteamFarm.CustomPlugins.SignInWithSteam.Data;
public sealed class SignInWithSteamResponse {
[JsonProperty(Required = Required.Always)]
public Uri ReturnURL { get; private set; }
[JsonInclude]
[JsonRequired]
public Uri ReturnURL { get; private init; }
internal SignInWithSteamResponse(Uri returnURL) {
ArgumentNullException.ThrowIfNull(returnURL);

View File

@@ -1,18 +1,20 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// ----------------------------------------------------------------------------------------------
//
// 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.

View File

@@ -1,18 +1,20 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// ----------------------------------------------------------------------------------------------
//
// 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.
@@ -20,7 +22,9 @@
// limitations under the License.
using System;
using System.ComponentModel.DataAnnotations;
using System.Composition;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Plugins.Interfaces;
@@ -31,8 +35,12 @@ namespace ArchiSteamFarm.CustomPlugins.SignInWithSteam;
[Export(typeof(IPlugin))]
[UsedImplicitly]
internal sealed class SignInWithSteamPlugin : IPlugin {
[JsonInclude]
[Required]
public string Name => nameof(SignInWithSteamPlugin);
[JsonInclude]
[Required]
public Version Version => typeof(SignInWithSteamPlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version));
public Task OnLoaded() {

View File

@@ -6,7 +6,6 @@
<ItemGroup>
<PackageReference Include="ConfigureAwaitChecker.Analyzer" PrivateAssets="all" />
<PackageReference Include="JetBrains.Annotations" PrivateAssets="all" />
<PackageReference Include="Newtonsoft.Json" IncludeAssets="compile" />
<PackageReference Include="SteamKit2" IncludeAssets="compile" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" IncludeAssets="compile" />
<PackageReference Include="System.Composition.AttributedModel" IncludeAssets="compile" />

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -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) {
@@ -114,7 +116,22 @@ internal static class Backend {
return Utilities.GenerateChecksumFor(bytes);
}
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?> GetLicenseStatus(Guid licenseID, WebBrowser webBrowser) {
ArgumentOutOfRangeException.ThrowIfEqual(licenseID, Guid.Empty);
ArgumentNullException.ThrowIfNull(webBrowser);
Uri request = new(ArchiNet.URL, "/Api/Licenses/Status");
Dictionary<string, string> headers = new(1, StringComparer.Ordinal) {
{ "X-License-Key", licenseID.ToString("N") }
};
ObjectResponse<GenericResponse>? response = await webBrowser.UrlGetToJsonObject<GenericResponse>(request, headers, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.AllowInvalidBodyOnErrors).ConfigureAwait(false);
return response?.StatusCode;
}
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);
@@ -141,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,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -22,20 +24,22 @@
using System;
using System.Globalization;
using System.IO;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using ArchiSteamFarm.Collections;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Helpers;
using ArchiSteamFarm.Helpers.Json;
using ArchiSteamFarm.Localization;
using ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data;
using JetBrains.Annotations;
using Newtonsoft.Json;
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher;
internal sealed class BotCache : SerializableFile {
[JsonProperty(Required = Required.DisallowNull)]
internal readonly ConcurrentList<AssetForListing> LastAnnouncedAssetsForListing = new();
[JsonDisallowNull]
[JsonInclude]
internal ConcurrentList<AssetForListing> LastAnnouncedAssetsForListing { get; private init; } = [];
internal string? LastAnnouncedTradeToken {
get => BackingLastAnnouncedTradeToken;
@@ -63,11 +67,27 @@ internal sealed class BotCache : SerializableFile {
}
}
[JsonProperty]
private string? BackingLastAnnouncedTradeToken;
internal DateTime? LastRequestAt {
get => BackingLastRequestAt;
[JsonProperty]
private string? BackingLastInventoryChecksumBeforeDeduplication;
set {
if (BackingLastRequestAt == value) {
return;
}
BackingLastRequestAt = value;
Utilities.InBackground(Save);
}
}
[JsonInclude]
private string? BackingLastAnnouncedTradeToken { get; set; }
[JsonInclude]
private string? BackingLastInventoryChecksumBeforeDeduplication { get; set; }
[JsonInclude]
private DateTime? BackingLastRequestAt { get; set; }
private BotCache(string filePath) : this() {
ArgumentException.ThrowIfNullOrEmpty(filePath);
@@ -84,6 +104,9 @@ internal sealed class BotCache : SerializableFile {
[UsedImplicitly]
public bool ShouldSerializeBackingLastInventoryChecksumBeforeDeduplication() => !string.IsNullOrEmpty(BackingLastInventoryChecksumBeforeDeduplication);
[UsedImplicitly]
public bool ShouldSerializeBackingLastRequestAt() => BackingLastRequestAt.HasValue;
[UsedImplicitly]
public bool ShouldSerializeLastAnnouncedAssetsForListing() => LastAnnouncedAssetsForListing.Count > 0;
@@ -97,6 +120,8 @@ internal sealed class BotCache : SerializableFile {
base.Dispose(disposing);
}
protected override Task Save() => Save(this);
internal static async Task<BotCache> CreateOrLoad(string filePath) {
ArgumentException.ThrowIfNullOrEmpty(filePath);
@@ -115,7 +140,7 @@ internal sealed class BotCache : SerializableFile {
return new BotCache(filePath);
}
botCache = JsonConvert.DeserializeObject<BotCache>(json);
botCache = json.ToJsonObject<BotCache>();
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -80,12 +82,20 @@ internal static class Commands {
return access > EAccess.None ? bot.Commands.FormatBotResponse(Strings.ErrorAccessDenied) : null;
}
if (!bot.IsConnectedAndLoggedOn) {
return bot.Commands.FormatBotResponse(Strings.BotNotConnected);
}
if (bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchEverything)) {
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(BotConfig.ETradingPreferences.MatchEverything)));
}
if ((ASF.GlobalConfig?.LicenseID == null) || (ASF.GlobalConfig.LicenseID == Guid.Empty)) {
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(ASF.GlobalConfig.LicenseID)));
}
if (!bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchActively) || !ItemsMatcherPlugin.RemoteCommunications.TryGetValue(bot, out RemoteCommunication? remoteCommunication)) {
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(BotConfig.ETradingPreferences.MatchActively)));
if (!ItemsMatcherPlugin.RemoteCommunications.TryGetValue(bot, out RemoteCommunication? remoteCommunication)) {
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(remoteCommunication)));
}
remoteCommunication.TriggerMatchActivelyEarlier();
@@ -112,7 +122,7 @@ internal static class Commands {
IList<string?> results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => ResponseMatch(Steam.Interaction.Commands.GetProxyAccess(bot, access, steamID), bot)))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
List<string> responses = [..results.Where(static result => !string.IsNullOrEmpty(result))];
return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null;
}

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -22,21 +24,23 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Text.Json.Serialization;
using ArchiSteamFarm.Steam.Data;
using ArchiSteamFarm.Steam.Storage;
using Newtonsoft.Json;
using SteamKit2;
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data;
internal sealed class AnnouncementDiffRequest : AnnouncementRequest {
[JsonProperty(Required = Required.Always)]
private readonly ImmutableHashSet<AssetForListing> InventoryRemoved;
[JsonInclude]
[JsonRequired]
private ImmutableHashSet<AssetForListing> InventoryRemoved { get; init; }
[JsonProperty(Required = Required.Always)]
private readonly string PreviousInventoryChecksum;
[JsonInclude]
[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,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -22,49 +24,58 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Text.Json.Serialization;
using ArchiSteamFarm.Steam.Data;
using ArchiSteamFarm.Steam.Storage;
using JetBrains.Annotations;
using Newtonsoft.Json;
using SteamKit2;
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data;
internal class AnnouncementRequest {
[JsonProperty]
private readonly string? AvatarHash;
[JsonInclude]
private string? AvatarHash { get; init; }
[JsonProperty(Required = Required.Always)]
private readonly Guid Guid;
[JsonInclude]
[JsonRequired]
private Guid Guid { get; init; }
[JsonProperty(Required = Required.Always)]
private readonly ImmutableHashSet<AssetForListing> Inventory;
[JsonInclude]
[JsonRequired]
private ImmutableHashSet<AssetForListing> Inventory { get; init; }
[JsonProperty(Required = Required.Always)]
private readonly string InventoryChecksum;
[JsonInclude]
[JsonRequired]
private string InventoryChecksum { get; init; }
[JsonProperty(Required = Required.Always)]
private readonly ImmutableHashSet<Asset.EType> MatchableTypes;
[JsonInclude]
[JsonRequired]
private ImmutableHashSet<EAssetType> MatchableTypes { get; init; }
[JsonProperty(Required = Required.Always)]
private readonly bool MatchEverything;
[JsonInclude]
[JsonRequired]
private bool MatchEverything { get; init; }
[JsonProperty(Required = Required.Always)]
private readonly byte MaxTradeHoldDuration;
[JsonInclude]
[JsonRequired]
private byte MaxTradeHoldDuration { get; init; }
[JsonProperty]
private readonly string? Nickname;
[JsonInclude]
private string? Nickname { get; init; }
[JsonProperty(Required = Required.Always)]
private readonly ulong SteamID;
[JsonInclude]
[JsonRequired]
private ulong SteamID { get; init; }
[JsonProperty(Required = Required.Always)]
private readonly uint TotalInventoryCount;
[JsonInclude]
[JsonRequired]
private uint TotalInventoryCount { get; init; }
[JsonProperty(Required = Required.Always)]
private readonly string TradeToken;
[JsonInclude]
[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,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -20,19 +22,23 @@
// limitations under the License.
using System;
using System.Text.Json.Serialization;
using ArchiSteamFarm.Steam.Data;
using Newtonsoft.Json;
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data;
internal sealed class AssetForListing : AssetInInventory {
[JsonProperty("i", Required = Required.Always)]
internal readonly uint Index;
internal string BackendHashCode => $"{Index}-{PreviousAssetID}-{AssetID}-{ClassID}-{Rarity}-{RealAppID}-{Tradable}-{Type}-{Amount}";
[JsonProperty("l", Required = Required.Always)]
internal readonly ulong PreviousAssetID;
[JsonInclude]
[JsonPropertyName("i")]
[JsonRequired]
internal uint Index { get; private init; }
internal string BackendHashCode => Index + "-" + PreviousAssetID + "-" + AssetID + "-" + ClassID + "-" + Rarity + "-" + RealAppID + "-" + Tradable + "-" + Type + "-" + Amount;
[JsonInclude]
[JsonPropertyName("l")]
[JsonRequired]
internal ulong PreviousAssetID { get; private init; }
internal AssetForListing(Asset asset, uint index, ulong previousAssetID) : base(asset) {
ArgumentNullException.ThrowIfNull(asset);

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -20,30 +22,42 @@
// limitations under the License.
using System;
using System.Text.Json.Serialization;
using ArchiSteamFarm.Steam.Data;
using Newtonsoft.Json;
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data;
internal class AssetForMatching {
[JsonProperty("c", Required = Required.Always)]
internal readonly ulong ClassID;
[JsonProperty("r", Required = Required.Always)]
internal readonly Asset.ERarity Rarity;
[JsonProperty("e", Required = Required.Always)]
internal readonly uint RealAppID;
[JsonProperty("t", Required = Required.Always)]
internal readonly bool Tradable;
[JsonProperty("p", Required = Required.Always)]
internal readonly Asset.EType Type;
[JsonProperty("a", Required = Required.Always)]
[JsonInclude]
[JsonPropertyName("a")]
[JsonRequired]
internal uint Amount { get; set; }
[JsonInclude]
[JsonPropertyName("c")]
[JsonRequired]
internal ulong ClassID { get; private init; }
[JsonInclude]
[JsonPropertyName("r")]
[JsonRequired]
internal EAssetRarity Rarity { get; private init; }
[JsonInclude]
[JsonPropertyName("e")]
[JsonRequired]
internal uint RealAppID { get; private init; }
[JsonInclude]
[JsonPropertyName("t")]
[JsonRequired]
internal bool Tradable { get; private init; }
[JsonInclude]
[JsonPropertyName("p")]
[JsonRequired]
internal EAssetType Type { get; private init; }
[JsonConstructor]
protected AssetForMatching() { }

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -20,14 +22,16 @@
// limitations under the License.
using System;
using System.Text.Json.Serialization;
using ArchiSteamFarm.Steam.Data;
using Newtonsoft.Json;
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data;
internal class AssetInInventory : AssetForMatching {
[JsonProperty("d", Required = Required.Always)]
internal readonly ulong AssetID;
[JsonInclude]
[JsonPropertyName("d")]
[JsonRequired]
internal ulong AssetID { get; private init; }
[JsonConstructor]
protected AssetInInventory() { }
@@ -38,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,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -21,22 +23,20 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Newtonsoft.Json;
using System.Text.Json.Serialization;
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data;
#pragma warning disable CA1812 // False positive, the class is used during json deserialization
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
internal sealed class BackgroundTaskResponse {
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
[JsonProperty(Required = Required.Always)]
internal readonly bool Finished;
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
[JsonInclude]
[JsonRequired]
internal bool Finished { get; private init; }
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
[JsonProperty(Required = Required.Always)]
internal readonly Guid RequestID;
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
[JsonInclude]
[JsonRequired]
internal Guid RequestID { get; private init; }
[JsonConstructor]
private BackgroundTaskResponse() { }

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -20,17 +22,19 @@
// limitations under the License.
using System;
using Newtonsoft.Json;
using System.Text.Json.Serialization;
using SteamKit2;
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data;
internal sealed class HeartBeatRequest {
[JsonProperty(Required = Required.Always)]
internal readonly Guid Guid;
[JsonInclude]
[JsonRequired]
internal Guid Guid { get; private init; }
[JsonProperty(Required = Required.Always)]
internal readonly ulong SteamID;
[JsonInclude]
[JsonRequired]
internal ulong SteamID { get; private init; }
internal HeartBeatRequest(Guid guid, ulong steamID) {
ArgumentOutOfRangeException.ThrowIfEqual(guid, Guid.Empty);

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -23,26 +25,30 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text.Json.Serialization;
using ArchiSteamFarm.Steam.Data;
using Newtonsoft.Json;
using SteamKit2;
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data;
internal sealed class InventoriesRequest {
[JsonProperty(Required = Required.Always)]
internal readonly Guid Guid;
[JsonInclude]
[JsonRequired]
internal Guid Guid { get; private init; }
[JsonProperty(Required = Required.Always)]
internal readonly ImmutableHashSet<AssetForMatching> Inventory;
[JsonInclude]
[JsonRequired]
internal ImmutableHashSet<AssetForMatching> Inventory { get; private init; }
[JsonProperty(Required = Required.Always)]
internal readonly ImmutableHashSet<Asset.EType> MatchableTypes;
[JsonInclude]
[JsonRequired]
internal ImmutableHashSet<EAssetType> MatchableTypes { get; private init; }
[JsonProperty(Required = Required.Always)]
internal readonly ulong SteamID;
[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,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -21,47 +23,48 @@
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using ArchiSteamFarm.Steam.Data;
using Newtonsoft.Json;
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data;
#pragma warning disable CA1812 // False positive, the class is used during json deserialization
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
internal sealed class ListedUser {
[JsonProperty(Required = Required.Always)]
internal readonly ImmutableHashSet<AssetInInventory> Assets = ImmutableHashSet<AssetInInventory>.Empty;
[JsonInclude]
[JsonRequired]
internal ImmutableHashSet<AssetInInventory> Assets { get; private init; } = [];
[JsonProperty(Required = Required.Always)]
internal readonly ImmutableHashSet<Asset.EType> MatchableTypes = ImmutableHashSet<Asset.EType>.Empty;
[JsonInclude]
[JsonRequired]
internal ImmutableHashSet<EAssetType> MatchableTypes { get; private init; } = [];
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
[JsonProperty(Required = Required.Always)]
internal readonly bool MatchEverything;
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
[JsonInclude]
[JsonRequired]
internal bool MatchEverything { get; private init; }
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
[JsonProperty(Required = Required.Always)]
internal readonly byte MaxTradeHoldDuration;
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
[JsonInclude]
[JsonRequired]
internal byte MaxTradeHoldDuration { get; private init; }
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
[JsonProperty(Required = Required.AllowNull)]
internal readonly string? Nickname;
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
[JsonInclude]
internal string? Nickname { get; private init; }
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
[JsonProperty(Required = Required.Always)]
internal readonly ulong SteamID;
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
[JsonInclude]
[JsonRequired]
internal ulong SteamID { get; private init; }
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
[JsonProperty(Required = Required.Always)]
internal readonly uint TotalInventoryCount;
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
[JsonInclude]
[JsonRequired]
internal uint TotalGamesCount { get; private init; }
[JsonProperty(Required = Required.Always)]
internal readonly string TradeToken = "";
[JsonInclude]
[JsonRequired]
internal uint TotalInventoryCount { get; private init; }
[JsonInclude]
[JsonRequired]
internal string TradeToken { get; private init; } = "";
[JsonConstructor]
private ListedUser() { }

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -20,33 +22,33 @@
// limitations under the License.
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using ArchiSteamFarm.Steam.Data;
using Newtonsoft.Json;
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data;
#pragma warning disable CA1812 // False positive, the class is used during json deserialization
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
internal sealed class SetPart {
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
[JsonProperty("c", Required = Required.Always)]
internal readonly ulong ClassID;
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
[JsonInclude]
[JsonPropertyName("c")]
[JsonRequired]
internal ulong ClassID { get; private init; }
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
[JsonProperty("r", Required = Required.Always)]
internal readonly Asset.ERarity Rarity;
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
[JsonInclude]
[JsonPropertyName("r")]
[JsonRequired]
internal EAssetRarity Rarity { get; private init; }
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
[JsonProperty("e", Required = Required.Always)]
internal readonly uint RealAppID;
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
[JsonInclude]
[JsonPropertyName("e")]
[JsonRequired]
internal uint RealAppID { get; private init; }
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
[JsonProperty("p", Required = Required.Always)]
internal readonly Asset.EType Type;
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
[JsonInclude]
[JsonPropertyName("p")]
[JsonRequired]
internal EAssetType Type { get; private init; }
[JsonConstructor]
private SetPart() { }

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -22,26 +24,30 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Text.Json.Serialization;
using ArchiSteamFarm.Steam.Data;
using Newtonsoft.Json;
using SteamKit2;
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data;
internal sealed class SetPartsRequest {
[JsonProperty(Required = Required.Always)]
internal readonly Guid Guid;
[JsonInclude]
[JsonRequired]
internal Guid Guid { get; private init; }
[JsonProperty(Required = Required.Always)]
internal readonly ImmutableHashSet<Asset.EType> MatchableTypes;
[JsonInclude]
[JsonRequired]
internal ImmutableHashSet<EAssetType> MatchableTypes { get; private init; }
[JsonProperty(Required = Required.Always)]
internal readonly ImmutableHashSet<uint> RealAppIDs;
[JsonInclude]
[JsonRequired]
internal ImmutableHashSet<uint> RealAppIDs { get; private init; }
[JsonProperty(Required = Required.Always)]
internal readonly ulong SteamID;
[JsonInclude]
[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,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -23,8 +25,11 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Composition;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Localization;
@@ -33,8 +38,6 @@ using ArchiSteamFarm.Plugins.Interfaces;
using ArchiSteamFarm.Steam;
using ArchiSteamFarm.Steam.Exchange;
using ArchiSteamFarm.Steam.Integration.Callbacks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SteamKit2;
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher;
@@ -43,10 +46,12 @@ namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher;
internal sealed class ItemsMatcherPlugin : OfficialPlugin, IBot, IBotCommand2, IBotIdentity, IBotModules, IBotTradeOfferResults, IBotUserNotifications {
internal static readonly ConcurrentDictionary<Bot, RemoteCommunication> RemoteCommunications = new();
[JsonProperty]
[JsonInclude]
[Required]
public override string Name => nameof(ItemsMatcherPlugin);
[JsonProperty]
[JsonInclude]
[Required]
public override Version Version => typeof(ItemsMatcherPlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version));
public async Task<string?> OnBotCommand(Bot bot, EAccess access, string message, string[] args, ulong steamID = 0) {
@@ -83,7 +88,7 @@ internal sealed class ItemsMatcherPlugin : OfficialPlugin, IBot, IBotCommand2, I
return Task.CompletedTask;
}
public async Task OnBotInitModules(Bot bot, IReadOnlyDictionary<string, JToken>? additionalConfigProperties = null) {
public async Task OnBotInitModules(Bot bot, IReadOnlyDictionary<string, JsonElement>? additionalConfigProperties = null) {
ArgumentNullException.ThrowIfNull(bot);
if (RemoteCommunications.TryRemove(bot, out RemoteCommunication? remoteCommunication)) {

View File

@@ -82,7 +82,4 @@
<value>Einige Bestätigungen sind fehlgeschlagen. Lediglich {0} von {1} Transaktionen wurden erfolgreich versendet.</value>
<comment>{0} will be replaced by amount of the trade offers that succeeded (number), {1} will be replaced by amount of the trade offers that were supposed to be sent in total (number)</comment>
</data>
<data name="PluginDisabledCustomBuild" xml:space="preserve">
<value>ItemsMatcherPlugin wird in benutzerdefinierten ASF-Versionen nicht unterstützt, die Funktionalität ist deaktiviert.</value>
</data>
</root>

View File

@@ -82,7 +82,4 @@
<value>Algunas confirmaciones han fallado, aproximadamente {0} de {1} intercambios fueron enviados exitosamente.</value>
<comment>{0} will be replaced by amount of the trade offers that succeeded (number), {1} will be replaced by amount of the trade offers that were supposed to be sent in total (number)</comment>
</data>
<data name="PluginDisabledCustomBuild" xml:space="preserve">
<value>ItemsMatcherPlugin no está soportado en compilaciones personalizadas de ASF, la función está deshabilitada</value>
</data>
</root>

View File

@@ -66,4 +66,8 @@
<value>{0} sets ont été matché pendant ce round.</value>
<comment>{0} will be replaced by number of sets traded</comment>
</data>
<data name="ActivelyMatchingSomeConfirmationsFailed" xml:space="preserve">
<value>Certaines confirmations ont échoué, environ {0} sur les transactions {1} ont été envoyées avec succès.</value>
<comment>{0} will be replaced by amount of the trade offers that succeeded (number), {1} will be replaced by amount of the trade offers that were supposed to be sent in total (number)</comment>
</data>
</root>

View File

@@ -66,4 +66,16 @@
<value>Abbinati un totale di {0} set questo round.</value>
<comment>{0} will be replaced by number of sets traded</comment>
</data>
<data name="MatchingFound" xml:space="preserve">
<value>Abbinato un totale di {0} elementi tramite bot {1} ({2}), inviando un'offerta commerciale...</value>
<comment>{0} will be replaced by number of items matched, {1} will be replaced by steam ID (number), {2} will be replaced by user's nickname</comment>
</data>
<data name="TradeOfferFailed" xml:space="preserve">
<value>Impossibile inviare un'offerta di scambio al bot {0} ({1}), proseguendo...</value>
<comment>{0} will be replaced by steam ID (number), {1} will be replaced by user's nickname'</comment>
</data>
<data name="ActivelyMatchingSomeConfirmationsFailed" xml:space="preserve">
<value>Alcune conferme sono fallite, circa {0} su {1} sono state inviate con successo.</value>
<comment>{0} will be replaced by amount of the trade offers that succeeded (number), {1} will be replaced by amount of the trade offers that were supposed to be sent in total (number)</comment>
</data>
</root>

View File

@@ -82,7 +82,4 @@
<value>Niektóre potwierdzenia nie powiodły się, około {0} z {1} transakcji zostało wysłanych pomyślnie.</value>
<comment>{0} will be replaced by amount of the trade offers that succeeded (number), {1} will be replaced by amount of the trade offers that were supposed to be sent in total (number)</comment>
</data>
<data name="PluginDisabledCustomBuild" xml:space="preserve">
<value>Wtyczka ItemsMatcherPlugin nie jest obsługiwana w niestandardowych kompilacjach ASF, funkcjonalność jest wyłączona.</value>
</data>
</root>

View File

@@ -66,4 +66,20 @@
<value>Celkovo porovnaných {0} sad karet.</value>
<comment>{0} will be replaced by number of sets traded</comment>
</data>
<data name="ListingAnnouncing" xml:space="preserve">
<value>Oznámenie {0} ({1}) s inventárom v ktorom je {2} položiek na zozname...</value>
<comment>{0} will be replaced by steam ID (number), {1} will be replaced by user's nickname, {2} will be replaced with number of items in the inventory</comment>
</data>
<data name="MatchingFound" xml:space="preserve">
<value>Zhodné s celkom {0} položiek s botom {1} ({2}), posielanie obchodnej ponuky...</value>
<comment>{0} will be replaced by number of items matched, {1} will be replaced by steam ID (number), {2} will be replaced by user's nickname</comment>
</data>
<data name="TradeOfferFailed" xml:space="preserve">
<value>Nepodarilo se odoslať obchodnú ponuku pre bota {0} ({1}), pokračujem...</value>
<comment>{0} will be replaced by steam ID (number), {1} will be replaced by user's nickname'</comment>
</data>
<data name="ActivelyMatchingSomeConfirmationsFailed" xml:space="preserve">
<value>Niektoré potvrdenia sa nepodarili, úspešne bolo poslaných približne {0} z {1} obchodov.</value>
<comment>{0} will be replaced by amount of the trade offers that succeeded (number), {1} will be replaced by amount of the trade offers that were supposed to be sent in total (number)</comment>
</data>
</root>

View File

@@ -82,7 +82,4 @@
<value>部分确认失败,{1} 个交易中约有 {0} 个发送成功。</value>
<comment>{0} will be replaced by amount of the trade offers that succeeded (number), {1} will be replaced by amount of the trade offers that were supposed to be sent in total (number)</comment>
</data>
<data name="PluginDisabledCustomBuild" xml:space="preserve">
<value>ItemsMatcherPlugin 不支持自定义的 ASF 构建,此功能已禁用。</value>
</data>
</root>

View File

@@ -82,7 +82,4 @@
<value>部分交易失敗,{1} 個交易中約 {0} 個發送成功。</value>
<comment>{0} will be replaced by amount of the trade offers that succeeded (number), {1} will be replaced by amount of the trade offers that were supposed to be sent in total (number)</comment>
</data>
<data name="PluginDisabledCustomBuild" xml:space="preserve">
<value>ItemsMatcherPlugin 在自訂的 ASF 組建中不受支援,已停止功能。</value>
</data>
</root>

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,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -20,6 +22,7 @@
// limitations under the License.
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
@@ -27,6 +30,7 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
@@ -42,7 +46,6 @@ using ArchiSteamFarm.Steam.Storage;
using ArchiSteamFarm.Storage;
using ArchiSteamFarm.Web;
using ArchiSteamFarm.Web.Responses;
using Newtonsoft.Json.Linq;
using SteamKit2;
using SteamKit2.Internal;
@@ -51,6 +54,7 @@ namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher;
internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
private const string MatchActivelyTradeOfferIDsStorageKey = $"{nameof(ItemsMatcher)}-{nameof(MatchActively)}-TradeOfferIDs";
private const byte MaxAnnouncementTTL = 60; // Maximum amount of minutes we can wait if the next announcement doesn't happen naturally
private const byte MaxInactivityDays = 14; // How long the server is willing to keep information about us for
private const uint MaxItemsCount = 500000; // Server is unwilling to accept more items than this
private const byte MaxTradeOffersActive = 5; // The actual upper limit is 30, but we should use lower amount to allow some bots to react before we hit the maximum allowed
private const byte MinAnnouncementTTL = 5; // Minimum amount of minutes we must wait before the next Announcement
@@ -59,12 +63,12 @@ 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 ImmutableHashSet<Asset.EType> AcceptedMatchableTypes = ImmutableHashSet.Create(
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;
private readonly Timer? HeartBeatTimer;
@@ -227,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));
@@ -248,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;
@@ -278,12 +282,12 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
uint index = 0;
ulong previousAssetID = 0;
List<AssetForListing> assetsForListing = new();
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));
@@ -291,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) {
@@ -332,7 +336,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
string inventoryChecksumBeforeDeduplication = Backend.GenerateChecksumFor(assetsForListing);
if ((tradeToken == BotCache.LastAnnouncedTradeToken) && !string.IsNullOrEmpty(BotCache.LastInventoryChecksumBeforeDeduplication)) {
if (BotCache.LastRequestAt.HasValue && (DateTime.UtcNow.Subtract(BotCache.LastRequestAt.Value).TotalDays < MaxInactivityDays) && (tradeToken == BotCache.LastAnnouncedTradeToken) && !string.IsNullOrEmpty(BotCache.LastInventoryChecksumBeforeDeduplication)) {
if (inventoryChecksumBeforeDeduplication == BotCache.LastInventoryChecksumBeforeDeduplication) {
// We've determined our state to be the same, we can skip announce entirely and start sending heartbeats exclusively
bool triggerImmediately = !ShouldSendHeartBeats;
@@ -372,16 +376,16 @@ 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 = new();
Dictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), Dictionary<ulong, uint>> state = new();
HashSet<uint> realAppIDs = [];
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 } };
}
@@ -444,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());
HashSet<(ulong ClassID, uint Amount)> setCopy = new();
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;
@@ -469,7 +473,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
minimumAmount = amount;
}
setCopy.Add((classID, amount));
setCopy[classID] = amount;
}
foreach ((ulong classID, uint amount) in setCopy) {
@@ -483,10 +487,10 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
}
}
HashSet<AssetForListing> assetsForListingFiltered = new();
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
@@ -536,7 +540,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
string checksum = Backend.GenerateChecksumFor(assetsForListing);
string? previousChecksum = BotCache.LastAnnouncedAssetsForListing.Count > 0 ? Backend.GenerateChecksumFor(BotCache.LastAnnouncedAssetsForListing) : null;
if ((tradeToken == BotCache.LastAnnouncedTradeToken) && (checksum == previousChecksum)) {
if (BotCache.LastRequestAt.HasValue && (DateTime.UtcNow.Subtract(BotCache.LastRequestAt.Value).TotalDays < MaxInactivityDays) && (tradeToken == BotCache.LastAnnouncedTradeToken) && (checksum == previousChecksum)) {
// We've determined our state to be the same, we can skip announce entirely and start sending heartbeats exclusively
bool triggerImmediately = !ShouldSendHeartBeats;
@@ -654,9 +658,11 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
LastAnnouncement = LastHeartBeat = DateTime.UtcNow;
ShouldSendAnnouncementEarlier = false;
ShouldSendHeartBeats = true;
BotCache.LastAnnouncedAssetsForListing.ReplaceWith(assetsForListing);
BotCache.LastAnnouncedTradeToken = tradeToken;
BotCache.LastInventoryChecksumBeforeDeduplication = inventoryChecksumBeforeDeduplication;
BotCache.LastRequestAt = LastHeartBeat;
return;
}
@@ -757,9 +763,11 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
LastAnnouncement = LastHeartBeat = DateTime.UtcNow;
ShouldSendAnnouncementEarlier = false;
ShouldSendHeartBeats = true;
BotCache.LastAnnouncedAssetsForListing.ReplaceWith(assetsForListing);
BotCache.LastAnnouncedTradeToken = tradeToken;
BotCache.LastInventoryChecksumBeforeDeduplication = inventoryChecksumBeforeDeduplication;
BotCache.LastRequestAt = LastHeartBeat;
return;
}
@@ -773,12 +781,12 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
internal void TriggerMatchActivelyEarlier() {
if (MatchActivelyTimer == null) {
throw new InvalidOperationException(nameof(MatchActivelyTimer));
}
// ReSharper disable once SuspiciousLockOverSynchronizationPrimitive - this is not a mistake, we need extra synchronization, and we can re-use the semaphore object for that
lock (MatchActivelySemaphore) {
MatchActivelyTimer.Change(TimeSpan.Zero, TimeSpan.FromHours(6));
Utilities.InBackground(() => MatchActively());
} else {
// ReSharper disable once SuspiciousLockOverSynchronizationPrimitive - this is not a mistake, we need extra synchronization, and we can re-use the semaphore object for that
lock (MatchActivelySemaphore) {
MatchActivelyTimer.Change(TimeSpan.Zero, TimeSpan.FromHours(6));
}
}
}
@@ -881,7 +889,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
throw new InvalidOperationException(nameof(ASF.GlobalConfig.LicenseID));
}
if (!Bot.IsConnectedAndLoggedOn || Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchEverything) || !Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchActively)) {
if (!Bot.IsConnectedAndLoggedOn || Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchEverything)) {
Bot.ArchiLogger.LogGenericTrace(Strings.ErrorAborted);
return;
@@ -895,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);
@@ -914,50 +922,215 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
try {
Bot.ArchiLogger.LogGenericInfo(Strings.Starting);
Dictionary<ulong, Asset> ourInventory;
HttpStatusCode? licenseStatus = await Backend.GetLicenseStatus(ASF.GlobalConfig.LicenseID.Value, WebBrowser).ConfigureAwait(false);
try {
ourInventory = 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)).ToDictionaryAsync(static item => item.AssetID).ConfigureAwait(false);
} catch (HttpRequestException e) {
Bot.ArchiLogger.LogGenericWarningException(e);
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(ourInventory)));
return;
} catch (Exception e) {
Bot.ArchiLogger.LogGenericException(e);
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(ourInventory)));
if (licenseStatus == null) {
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(licenseStatus)));
return;
}
if (ourInventory.Count == 0) {
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(ourInventory)));
if (!licenseStatus.Value.IsSuccessCode()) {
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, licenseStatus.Value));
return;
}
HashSet<Asset> assetsForMatching;
try {
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)));
return;
} catch (Exception e) {
Bot.ArchiLogger.LogGenericException(e);
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(assetsForMatching)));
return;
}
if (assetsForMatching.Count == 0) {
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(assetsForMatching)));
return;
}
// 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(ourInventory.Values).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();
HashSet<ulong> assetIDsToRemove = ourInventory.Where(item => !setsToKeep.Contains((item.Value.RealAppID, item.Value.Type, item.Value.Rarity))).Select(static item => item.Key).ToHashSet();
if (assetsForMatching.RemoveWhere(item => !setsToKeep.Contains((item.RealAppID, item.Type, item.Rarity))) > 0) {
if (assetsForMatching.Count == 0) {
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(assetsForMatching)));
foreach (ulong assetIDToRemove in assetIDsToRemove) {
ourInventory.Remove(assetIDToRemove);
return;
}
}
if (ourInventory.Count == 0) {
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(ourInventory)));
// 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, EAssetType Type, EAssetRarity Rarity), Dictionary<ulong, uint>> setsState = new();
foreach (Asset asset in assetsForMatching) {
realAppIDs.Add(asset.RealAppID);
(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.GetValueOrDefault(asset.ClassID) + asset.Amount;
} else {
setsState[key] = new Dictionary<ulong, uint> { { asset.ClassID, asset.Amount } };
}
}
if (!SignedInWithSteam) {
HttpStatusCode? signInWithSteam = await ArchiNet.SignInWithSteam(Bot, WebBrowser).ConfigureAwait(false);
if ((signInWithSteam == null) || !signInWithSteam.Value.IsSuccessCode()) {
// This is actually a network failure
return;
}
SignedInWithSteam = true;
}
ObjectResponse<GenericResponse<ImmutableHashSet<SetPart>>>? setPartsResponse = await Backend.GetSetParts(WebBrowser, Bot.SteamID, acceptedMatchableTypes, realAppIDs).ConfigureAwait(false);
if (setPartsResponse == null) {
// This is actually a network failure
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorObjectIsNull, nameof(setPartsResponse)));
return;
}
if (ourInventory.Count > MaxItemsCount) {
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(ourInventory)} > {MaxItemsCount}"));
if (setPartsResponse.StatusCode.IsRedirectionCode()) {
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, setPartsResponse.StatusCode));
if (setPartsResponse.FinalUri.Host != ArchiWebHandler.SteamCommunityURL.Host) {
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(setPartsResponse.FinalUri), setPartsResponse.FinalUri));
return;
}
// We've expected the result, not the redirection to the sign in, we need to authenticate again
SignedInWithSteam = false;
return;
}
(HttpStatusCode StatusCode, ImmutableHashSet<ListedUser> Users)? response = await Backend.GetListedUsersForMatching(ASF.GlobalConfig.LicenseID.Value, Bot, WebBrowser, ourInventory.Values, acceptedMatchableTypes).ConfigureAwait(false);
if (!setPartsResponse.StatusCode.IsSuccessCode()) {
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, setPartsResponse.StatusCode));
return;
}
if (setPartsResponse.Content?.Result == null) {
// This should never happen if we got the correct response
Bot.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(setPartsResponse), setPartsResponse.Content?.Result));
return;
}
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, EAssetType Type, EAssetRarity Rarity) key, Dictionary<ulong, uint> set) in setsState) {
uint minimumAmount = uint.MaxValue;
uint maximumAmount = uint.MinValue;
foreach (uint amount in set.Values) {
if (amount < minimumAmount) {
minimumAmount = amount;
}
if (amount > maximumAmount) {
maximumAmount = amount;
}
}
if (maximumAmount < 2) {
// We don't have anything to swap with, remove all entries from this set
set.Clear();
continue;
}
if (!databaseSets.TryGetValue(key, out HashSet<ulong>? databaseSet)) {
// We have no clue about this set, we can't do any optimization
continue;
}
if ((databaseSet.Count != set.Count) || !databaseSet.SetEquals(set.Keys)) {
// User either has more or less classIDs than we know about, we can't optimize this
continue;
}
if (maximumAmount - minimumAmount < 2) {
// We don't have anything to swap with, remove all entries from this set
set.Clear();
continue;
}
// User has all classIDs we know about, we can deduplicate his items based on lowest count
setCopy.Clear();
foreach ((ulong classID, uint amount) in set) {
setCopy[classID] = amount;
}
foreach ((ulong classID, uint amount) in setCopy) {
if (minimumAmount >= amount) {
set.Remove(classID);
continue;
}
set[classID] = amount - minimumAmount;
}
}
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, 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
continue;
}
if (asset.Amount >= targetAmount) {
asset.Amount = targetAmount;
if (setState.Remove(asset.ClassID) && (setState.Count == 0)) {
setsState.Remove(key);
}
} else {
setState[asset.ClassID] = targetAmount - asset.Amount;
}
assetsForMatchingFiltered.Add(asset);
}
assetsForMatching = assetsForMatchingFiltered;
if (assetsForMatching.Count == 0) {
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(assetsForMatching)));
return;
}
if (assetsForMatching.Count > MaxItemsCount) {
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(assetsForMatching)} > {MaxItemsCount}"));
return;
}
(HttpStatusCode StatusCode, ImmutableHashSet<ListedUser> Users)? response = await Backend.GetListedUsersForMatching(ASF.GlobalConfig.LicenseID.Value, Bot, WebBrowser, assetsForMatching, acceptedMatchableTypes).ConfigureAwait(false);
if (response == null) {
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(response)));
@@ -980,9 +1153,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
#pragma warning disable CA2000 // False positive, we're actually wrapping it in the using clause below exactly for that purpose
using (await Bot.Actions.GetTradingLock().ConfigureAwait(false)) {
#pragma warning restore CA2000 // False positive, we're actually wrapping it in the using clause below exactly for that purpose
Bot.ArchiLogger.LogGenericInfo(Strings.Starting);
tradesSent = await MatchActively(response.Value.Users, ourInventory, acceptedMatchableTypes).ConfigureAwait(false);
tradesSent = await MatchActively(response.Value.Users, assetsForMatching, acceptedMatchableTypes).ConfigureAwait(false);
}
Bot.ArchiLogger.LogGenericInfo(Strings.Done);
@@ -996,22 +1167,22 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
}
}
private async Task<bool> MatchActively(IReadOnlyCollection<ListedUser> listedUsers, Dictionary<ulong, Asset> ourInventory, 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));
}
if ((ourInventory == null) || (ourInventory.Count == 0)) {
throw new ArgumentNullException(nameof(ourInventory));
if ((ourAssets == null) || (ourAssets.Count == 0)) {
throw new ArgumentNullException(nameof(ourAssets));
}
if ((acceptedMatchableTypes == null) || (acceptedMatchableTypes.Count == 0)) {
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(ourInventory.Values);
(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)}"));
@@ -1021,26 +1192,34 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
// Cancel previous trade offers sent and deprioritize SteamIDs that didn't answer us in this round
HashSet<ulong>? matchActivelyTradeOfferIDs = null;
JToken? matchActivelyTradeOfferIDsToken = Bot.BotDatabase.LoadFromJsonStorage(MatchActivelyTradeOfferIDsStorageKey);
JsonElement matchActivelyTradeOfferIDsToken = Bot.BotDatabase.LoadFromJsonStorage(MatchActivelyTradeOfferIDsStorageKey);
if (matchActivelyTradeOfferIDsToken != null) {
if (matchActivelyTradeOfferIDsToken.ValueKind == JsonValueKind.Array) {
try {
matchActivelyTradeOfferIDs = matchActivelyTradeOfferIDsToken.ToObject<HashSet<ulong>>();
matchActivelyTradeOfferIDs = new HashSet<ulong>(matchActivelyTradeOfferIDsToken.GetArrayLength());
foreach (JsonElement tradeIDElement in matchActivelyTradeOfferIDsToken.EnumerateArray()) {
if (!tradeIDElement.TryGetUInt64(out ulong tradeID)) {
continue;
}
matchActivelyTradeOfferIDs.Add(tradeID);
}
} catch (Exception e) {
Bot.ArchiLogger.LogGenericWarningException(e);
}
}
matchActivelyTradeOfferIDs ??= new HashSet<ulong>();
matchActivelyTradeOfferIDs ??= [];
HashSet<ulong> deprioritizedSteamIDs = new();
HashSet<ulong> deprioritizedSteamIDs = [];
if (matchActivelyTradeOfferIDs.Count > 0) {
// This is not a mandatory step, we allow it to fail
HashSet<TradeOffer>? sentTradeOffers = await Bot.ArchiWebHandler.GetTradeOffers(true, false, true, false).ConfigureAwait(false);
if (sentTradeOffers != null) {
HashSet<ulong> activeTradeOfferIDs = new();
HashSet<ulong> activeTradeOfferIDs = [];
foreach (TradeOffer tradeOffer in sentTradeOffers.Where(tradeOffer => (tradeOffer.State == ETradeOfferState.Active) && matchActivelyTradeOfferIDs.Contains(tradeOffer.TradeOfferID))) {
deprioritizedSteamIDs.Add(tradeOffer.OtherSteamID64);
@@ -1054,7 +1233,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
matchActivelyTradeOfferIDs = activeTradeOfferIDs;
if (matchActivelyTradeOfferIDs.Count > 0) {
Bot.BotDatabase.SaveToJsonStorage(MatchActivelyTradeOfferIDsStorageKey, JToken.FromObject(matchActivelyTradeOfferIDs));
Bot.BotDatabase.SaveToJsonStorage(MatchActivelyTradeOfferIDsStorageKey, matchActivelyTradeOfferIDs);
} else {
Bot.BotDatabase.DeleteFromJsonStorage(MatchActivelyTradeOfferIDsStorageKey);
}
@@ -1062,14 +1241,24 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
}
}
HashSet<ulong> pendingMobileTradeOfferIDs = new();
Dictionary<ulong, Asset> ourInventory = ourAssets.ToDictionary(static asset => asset.AssetID);
HashSet<ulong> pendingMobileTradeOfferIDs = [];
byte maxTradeHoldDuration = ASF.GlobalConfig?.MaxTradeHoldDuration ?? GlobalConfig.DefaultMaxTradeHoldDuration;
byte failuresInRow = 0;
uint matchedSets = 0;
foreach (ListedUser listedUser in listedUsers.Where(listedUser => (listedUser.SteamID != Bot.SteamID) && acceptedMatchableTypes.Any(listedUser.MatchableTypes.Contains) && !Bot.IsBlacklistedFromTrades(listedUser.SteamID)).OrderBy(listedUser => deprioritizedSteamIDs.Contains(listedUser.SteamID)).ThenByDescending(static listedUser => listedUser.MatchEverything).ThenBy(static listedUser => listedUser.TotalInventoryCount)) {
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}"));
@@ -1082,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;
@@ -1103,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 = new();
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 = new();
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;
@@ -1133,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;
@@ -1152,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) {
@@ -1194,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) {
@@ -1239,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
@@ -1256,7 +1447,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
if (tradeOfferIDs?.Count > 0) {
matchActivelyTradeOfferIDs.UnionWith(tradeOfferIDs);
Bot.BotDatabase.SaveToJsonStorage(MatchActivelyTradeOfferIDsStorageKey, JToken.FromObject(matchActivelyTradeOfferIDs));
Bot.BotDatabase.SaveToJsonStorage(MatchActivelyTradeOfferIDsStorageKey, matchActivelyTradeOfferIDs);
}
if (mobileTradeOfferIDs?.Count > 0) {
@@ -1324,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;
}
@@ -1336,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);
@@ -1352,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;
}
@@ -1436,15 +1627,42 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
return;
}
if (response.StatusCode.IsClientErrorCode()) {
BotCache ??= await BotCache.CreateOrLoad(BotCacheFilePath).ConfigureAwait(false);
if (!response.StatusCode.IsSuccessCode()) {
ShouldSendHeartBeats = false;
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, response.StatusCode));
return;
switch (response.StatusCode) {
case HttpStatusCode.Conflict:
// ArchiNet told us to that we need to announce again
LastAnnouncement = DateTime.MinValue;
BotCache.LastAnnouncedAssetsForListing.Clear();
BotCache.LastInventoryChecksumBeforeDeduplication = BotCache.LastAnnouncedTradeToken = null;
BotCache.LastRequestAt = null;
return;
case HttpStatusCode.Forbidden:
// ArchiNet told us to stop submitting data for now
LastAnnouncement = DateTime.UtcNow.AddYears(1);
return;
case HttpStatusCode.TooManyRequests:
// ArchiNet told us to try again later
LastAnnouncement = DateTime.UtcNow.AddDays(1);
return;
default:
// There is something wrong with our payload or the server, we shouldn't retry for at least several hours
LastAnnouncement = DateTime.UtcNow.AddHours(6);
return;
}
}
LastHeartBeat = DateTime.UtcNow;
BotCache.LastRequestAt = LastHeartBeat = DateTime.UtcNow;
} finally {
RequestsSemaphore.Release();
}

View File

@@ -6,7 +6,6 @@
<ItemGroup>
<PackageReference Include="ConfigureAwaitChecker.Analyzer" PrivateAssets="all" />
<PackageReference Include="JetBrains.Annotations" PrivateAssets="all" />
<PackageReference Include="Newtonsoft.Json" IncludeAssets="compile" />
<PackageReference Include="SteamKit2" IncludeAssets="compile" />
<PackageReference Include="System.Composition.AttributedModel" IncludeAssets="compile" />
</ItemGroup>

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -27,17 +29,15 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Helpers.Json;
using ArchiSteamFarm.Localization;
using ArchiSteamFarm.Steam;
using Newtonsoft.Json;
using SteamKit2;
using SteamKit2.Internal;
namespace ArchiSteamFarm.OfficialPlugins.MobileAuthenticator;
internal static class Commands {
private const byte MaxFinalizationAttempts = 900 / Steam.Security.MobileAuthenticator.CodeInterval;
internal static async Task<string?> OnBotCommand(Bot bot, EAccess access, string message, string[] args, ulong steamID = 0) {
ArgumentNullException.ThrowIfNull(bot);
@@ -128,7 +128,7 @@ internal static class Commands {
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(json)));
}
Steam.Security.MobileAuthenticator? mobileAuthenticator = JsonConvert.DeserializeObject<Steam.Security.MobileAuthenticator>(json);
Steam.Security.MobileAuthenticator? mobileAuthenticator = json.ToJsonObject<Steam.Security.MobileAuthenticator>();
if (mobileAuthenticator == null) {
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(json)));
@@ -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)) {
@@ -220,7 +199,7 @@ internal static class Commands {
IList<string?> results = await Utilities.InParallel(bots.Select(bot => ResponseTwoFactorFinalize(Steam.Interaction.Commands.GetProxyAccess(bot, access, steamID), bot, activationCode))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
List<string> responses = [..results.Where(static result => !string.IsNullOrEmpty(result))];
return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null;
}
@@ -261,7 +240,7 @@ internal static class Commands {
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(json)));
}
Steam.Security.MobileAuthenticator? mobileAuthenticator = JsonConvert.DeserializeObject<Steam.Security.MobileAuthenticator>(json);
Steam.Security.MobileAuthenticator? mobileAuthenticator = json.ToJsonObject<Steam.Security.MobileAuthenticator>();
if (mobileAuthenticator == null) {
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(json)));
@@ -313,7 +292,7 @@ internal static class Commands {
IList<string?> results = await Utilities.InParallel(bots.Select(bot => ResponseTwoFactorFinalized(Steam.Interaction.Commands.GetProxyAccess(bot, access, steamID), bot, activationCode))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
List<string> responses = [..results.Where(static result => !string.IsNullOrEmpty(result))];
return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null;
}
@@ -360,7 +339,7 @@ internal static class Commands {
MaFileData maFileData = new(response, bot.SteamID, deviceID);
string maFilePendingPath = $"{bot.GetFilePath(Bot.EFileType.MobileAuthenticator)}.PENDING";
string json = JsonConvert.SerializeObject(maFileData, Formatting.Indented);
string json = maFileData.ToJsonText(true);
try {
await File.WriteAllTextAsync(maFilePendingPath, json).ConfigureAwait(false);
@@ -392,7 +371,7 @@ internal static class Commands {
IList<string?> results = await Utilities.InParallel(bots.Select(bot => ResponseTwoFactorInit(Steam.Interaction.Commands.GetProxyAccess(bot, access, steamID), bot))).ConfigureAwait(false);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
List<string> responses = [..results.Where(static result => !string.IsNullOrEmpty(result))];
return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null;
}

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -20,48 +22,71 @@
// limitations under the License.
using System;
using Newtonsoft.Json;
using System.Text.Json.Serialization;
using SteamKit2;
using SteamKit2.Internal;
namespace ArchiSteamFarm.OfficialPlugins.MobileAuthenticator;
internal sealed class MaFileData {
[JsonProperty("account_name", Required = Required.Always)]
internal readonly string AccountName;
[JsonInclude]
[JsonPropertyName("account_name")]
[JsonRequired]
internal string AccountName { get; private init; }
[JsonProperty("device_id", Required = Required.Always)]
internal readonly string DeviceID;
[JsonInclude]
[JsonPropertyName("device_id")]
[JsonRequired]
internal string DeviceID { get; private init; }
[JsonProperty("identity_secret", Required = Required.Always)]
internal readonly string IdentitySecret;
[JsonInclude]
[JsonPropertyName("identity_secret")]
[JsonRequired]
internal string IdentitySecret { get; private init; }
[JsonProperty("revocation_code", Required = Required.Always)]
internal readonly string RevocationCode;
[JsonInclude]
[JsonPropertyName("revocation_code")]
[JsonRequired]
internal string RevocationCode { get; private init; }
[JsonProperty("secret_1", Required = Required.Always)]
internal readonly string Secret1;
[JsonInclude]
[JsonPropertyName("secret_1")]
[JsonRequired]
internal string Secret1 { get; private init; }
[JsonProperty("serial_number", Required = Required.Always)]
internal readonly ulong SerialNumber;
[JsonInclude]
[JsonPropertyName("serial_number")]
[JsonRequired]
internal ulong SerialNumber { get; private init; }
[JsonProperty("server_time", Required = Required.Always)]
internal readonly ulong ServerTime;
[JsonInclude]
[JsonPropertyName("server_time")]
[JsonRequired]
internal ulong ServerTime { get; private init; }
[JsonProperty(Required = Required.Always)]
internal readonly MaFileSessionData Session;
[JsonInclude]
[JsonRequired]
internal MaFileSessionData Session { get; private init; }
[JsonProperty("shared_secret", Required = Required.Always)]
internal readonly string SharedSecret;
[JsonInclude]
[JsonPropertyName("shared_secret")]
[JsonRequired]
internal string SharedSecret { get; private init; }
[JsonProperty("status", Required = Required.Always)]
internal readonly int Status;
[JsonInclude]
[JsonPropertyName("status")]
[JsonRequired]
internal int Status { get; private init; }
[JsonProperty("token_gid", Required = Required.Always)]
internal readonly string TokenGid;
[JsonInclude]
[JsonPropertyName("token_gid")]
[JsonRequired]
internal string TokenGid { get; private init; }
[JsonProperty("uri", Required = Required.Always)]
internal readonly string Uri;
[JsonInclude]
[JsonPropertyName("uri")]
[JsonRequired]
internal string Uri { get; private init; }
internal MaFileData(CTwoFactor_AddAuthenticator_Response data, ulong steamID, string deviceID) {
ArgumentNullException.ThrowIfNull(data);

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -20,14 +22,15 @@
// limitations under the License.
using System;
using Newtonsoft.Json;
using System.Text.Json.Serialization;
using SteamKit2;
namespace ArchiSteamFarm.OfficialPlugins.MobileAuthenticator;
internal sealed class MaFileSessionData {
[JsonProperty(Required = Required.Always)]
internal readonly ulong SteamID;
[JsonInclude]
[JsonRequired]
internal ulong SteamID { get; private init; }
internal MaFileSessionData(ulong steamID) {
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -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,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -22,15 +24,16 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.OfficialPlugins.MobileAuthenticator.Localization;
using ArchiSteamFarm.Plugins;
using ArchiSteamFarm.Plugins.Interfaces;
using ArchiSteamFarm.Steam;
using Newtonsoft.Json;
using SteamKit2;
namespace ArchiSteamFarm.OfficialPlugins.MobileAuthenticator;
@@ -38,10 +41,12 @@ namespace ArchiSteamFarm.OfficialPlugins.MobileAuthenticator;
[Export(typeof(IPlugin))]
[SuppressMessage("ReSharper", "MemberCanBeFileLocal")]
internal sealed class MobileAuthenticatorPlugin : OfficialPlugin, IBotCommand2, IBotSteamClient {
[JsonProperty]
[JsonInclude]
[Required]
public override string Name => nameof(MobileAuthenticatorPlugin);
[JsonProperty]
[JsonInclude]
[Required]
public override Version Version => typeof(MobileAuthenticatorPlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version));
public async Task<string?> OnBotCommand(Bot bot, EAccess access, string message, string[] args, ulong steamID = 0) {

View File

@@ -6,7 +6,6 @@
<ItemGroup>
<PackageReference Include="ConfigureAwaitChecker.Analyzer" PrivateAssets="all" />
<PackageReference Include="JetBrains.Annotations" PrivateAssets="all" />
<PackageReference Include="Newtonsoft.Json" IncludeAssets="compile" />
<PackageReference Include="SteamKit2" IncludeAssets="compile" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" IncludeAssets="compile" />
<PackageReference Include="System.Composition.AttributedModel" IncludeAssets="compile" />

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -23,36 +25,52 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Text.Json.Serialization;
using ArchiSteamFarm.Core;
using Newtonsoft.Json;
using SteamKit2;
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.Data;
internal sealed class SubmitRequest {
[JsonProperty("guid", Required = Required.Always)]
private static string Guid => ASF.GlobalDatabase?.Identifier.ToString("N") ?? throw new InvalidOperationException(nameof(ASF.GlobalDatabase.Identifier));
[JsonProperty("token", Required = Required.Always)]
private static string Token => SharedInfo.Token;
[JsonProperty("v", Required = Required.Always)]
private static byte Version => SharedInfo.ApiVersion;
[JsonProperty("apps", Required = Required.Always)]
private readonly ImmutableDictionary<string, string> Apps;
[JsonProperty("depots", Required = Required.Always)]
private readonly ImmutableDictionary<string, string> Depots;
private readonly ulong SteamID;
[JsonProperty("subs", Required = Required.Always)]
private readonly ImmutableDictionary<string, string> Subs;
#pragma warning disable CA1822 // We can't make it static, STJ doesn't serialize it otherwise
[JsonInclude]
[JsonPropertyName("guid")]
private string Guid => ASF.GlobalDatabase?.Identifier.ToString("N") ?? throw new InvalidOperationException(nameof(ASF.GlobalDatabase.Identifier));
#pragma warning restore CA1822 // We can't make it static, STJ doesn't serialize it otherwise
[JsonProperty("steamid", Required = Required.Always)]
[JsonInclude]
[JsonPropertyName("steamid")]
private string SteamIDText => new SteamID(SteamID).Render();
#pragma warning disable CA1822 // We can't make it static, STJ doesn't serialize it otherwise
[JsonInclude]
[JsonPropertyName("token")]
private string Token => SharedInfo.Token;
#pragma warning restore CA1822 // We can't make it static, STJ doesn't serialize it otherwise
#pragma warning disable CA1822 // We can't make it static, STJ doesn't serialize it otherwise
[JsonInclude]
[JsonPropertyName("v")]
private byte Version => SharedInfo.ApiVersion;
#pragma warning restore CA1822 // We can't make it static, STJ doesn't serialize it otherwise
[JsonInclude]
[JsonPropertyName("apps")]
[JsonRequired]
private ImmutableDictionary<string, string> Apps { get; init; }
[JsonInclude]
[JsonPropertyName("depots")]
[JsonRequired]
private ImmutableDictionary<string, string> Depots { get; init; }
[JsonInclude]
[JsonPropertyName("subs")]
[JsonRequired]
private ImmutableDictionary<string, string> Subs { get; init; }
internal SubmitRequest(ulong steamID, IReadOnlyCollection<KeyValuePair<uint, ulong>> apps, IReadOnlyCollection<KeyValuePair<uint, ulong>> accessTokens, IReadOnlyCollection<KeyValuePair<uint, string>> depots) {
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
throw new ArgumentOutOfRangeException(nameof(steamID));

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -20,22 +22,21 @@
// limitations under the License.
using System.Diagnostics.CodeAnalysis;
using Newtonsoft.Json;
using System.Text.Json.Serialization;
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 {
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
[JsonProperty("data", Required = Required.DisallowNull)]
internal readonly SubmitResponseData? Data;
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
[JsonInclude]
[JsonPropertyName("data")]
internal SubmitResponseData? Data { get; private init; }
#pragma warning disable CS0649 // False positive, the field is used during json deserialization
[JsonProperty("success", Required = Required.Always)]
internal readonly bool Success;
#pragma warning restore CS0649 // False positive, the field is used during json deserialization
[JsonInclude]
[JsonPropertyName("success")]
[JsonRequired]
internal bool Success { get; private init; }
[JsonConstructor]
private SubmitResponse() { }

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -20,28 +22,40 @@
// limitations under the License.
using System.Collections.Immutable;
using Newtonsoft.Json;
using System.Text.Json.Serialization;
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.Data;
#pragma warning disable CA1812 // False positive, the class is used during json deserialization
internal sealed class SubmitResponseData {
[JsonProperty("new_apps", Required = Required.Always)]
internal readonly ImmutableHashSet<uint> NewApps = ImmutableHashSet<uint>.Empty;
[JsonInclude]
[JsonPropertyName("new_apps")]
[JsonRequired]
internal ImmutableHashSet<uint> NewApps { get; private init; } = [];
[JsonProperty("new_depots", Required = Required.Always)]
internal readonly ImmutableHashSet<uint> NewDepots = ImmutableHashSet<uint>.Empty;
[JsonInclude]
[JsonPropertyName("new_depots")]
[JsonRequired]
internal ImmutableHashSet<uint> NewDepots { get; private init; } = [];
[JsonProperty("new_subs", Required = Required.Always)]
internal readonly ImmutableHashSet<uint> NewPackages = ImmutableHashSet<uint>.Empty;
[JsonInclude]
[JsonPropertyName("new_subs")]
[JsonRequired]
internal ImmutableHashSet<uint> NewPackages { get; private init; } = [];
[JsonProperty("verified_apps", Required = Required.Always)]
internal readonly ImmutableHashSet<uint> VerifiedApps = ImmutableHashSet<uint>.Empty;
[JsonInclude]
[JsonPropertyName("verified_apps")]
[JsonRequired]
internal ImmutableHashSet<uint> VerifiedApps { get; private init; } = [];
[JsonProperty("verified_depots", Required = Required.Always)]
internal readonly ImmutableHashSet<uint> VerifiedDepots = ImmutableHashSet<uint>.Empty;
[JsonInclude]
[JsonPropertyName("verified_depots")]
[JsonRequired]
internal ImmutableHashSet<uint> VerifiedDepots { get; private init; } = [];
[JsonProperty("verified_subs", Required = Required.Always)]
internal readonly ImmutableHashSet<uint> VerifiedPackages = ImmutableHashSet<uint>.Empty;
[JsonInclude]
[JsonPropertyName("verified_subs")]
[JsonRequired]
internal ImmutableHashSet<uint> VerifiedPackages { get; private init; } = [];
}
#pragma warning restore CA1812 // False positive, the class is used during json deserialization

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -21,49 +23,57 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Helpers;
using ArchiSteamFarm.Helpers.Json;
using ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.Localization;
using ArchiSteamFarm.Web.Responses;
using JetBrains.Annotations;
using Newtonsoft.Json;
using SteamKit2;
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper;
internal sealed class GlobalCache : SerializableFile {
internal static readonly ArchiCacheable<ImmutableHashSet<uint>> KnownDepotIDs = new(ResolveKnownDepotIDs, TimeSpan.FromDays(7));
internal static readonly ArchiCacheable<FrozenSet<uint>> KnownDepotIDs = new(ResolveKnownDepotIDs, TimeSpan.FromDays(7));
private static string SharedFilePath => Path.Combine(ArchiSteamFarm.SharedInfo.ConfigDirectory, $"{nameof(SteamTokenDumper)}.cache");
[JsonProperty(Required = Required.DisallowNull)]
private readonly ConcurrentDictionary<uint, uint> AppChangeNumbers = new();
[JsonProperty(Required = Required.DisallowNull)]
private readonly ConcurrentDictionary<uint, ulong> AppTokens = new();
[JsonProperty(Required = Required.DisallowNull)]
private readonly ConcurrentDictionary<uint, string> DepotKeys = new();
[JsonProperty(Required = Required.DisallowNull)]
private readonly ConcurrentDictionary<uint, ulong> SubmittedApps = new();
[JsonProperty(Required = Required.DisallowNull)]
private readonly ConcurrentDictionary<uint, string> SubmittedDepots = new();
[JsonProperty(Required = Required.DisallowNull)]
private readonly ConcurrentDictionary<uint, ulong> SubmittedPackages = new();
[JsonProperty(Required = Required.DisallowNull)]
[JsonInclude]
internal uint LastChangeNumber { get; private set; }
[JsonDisallowNull]
[JsonInclude]
private ConcurrentDictionary<uint, uint> AppChangeNumbers { get; init; } = new();
[JsonDisallowNull]
[JsonInclude]
private ConcurrentDictionary<uint, ulong> AppTokens { get; init; } = new();
[JsonDisallowNull]
[JsonInclude]
private ConcurrentDictionary<uint, string> DepotKeys { get; init; } = new();
[JsonDisallowNull]
[JsonInclude]
private ConcurrentDictionary<uint, ulong> SubmittedApps { get; init; } = new();
[JsonDisallowNull]
[JsonInclude]
private ConcurrentDictionary<uint, string> SubmittedDepots { get; init; } = new();
[JsonDisallowNull]
[JsonInclude]
private ConcurrentDictionary<uint, ulong> SubmittedPackages { get; init; } = new();
[JsonConstructor]
internal GlobalCache() => FilePath = SharedFilePath;
[UsedImplicitly]
@@ -87,6 +97,8 @@ internal sealed class GlobalCache : SerializableFile {
[UsedImplicitly]
public bool ShouldSerializeSubmittedPackages() => !SubmittedPackages.IsEmpty;
protected override Task Save() => Save(this);
internal ulong GetAppToken(uint appID) => AppTokens[appID];
internal Dictionary<uint, ulong> GetAppTokensForSubmission() => AppTokens.Where(appToken => (SteamTokenDumperPlugin.Config?.SecretAppIDs.Contains(appToken.Key) != true) && (appToken.Value > 0) && (!SubmittedApps.TryGetValue(appToken.Key, out ulong token) || (appToken.Value != token))).ToDictionary(static appToken => appToken.Key, static appToken => appToken.Value);
@@ -118,7 +130,7 @@ internal sealed class GlobalCache : SerializableFile {
return null;
}
globalCache = JsonConvert.DeserializeObject<GlobalCache>(json);
globalCache = json.ToJsonObject<GlobalCache>();
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
@@ -306,7 +318,7 @@ internal sealed class GlobalCache : SerializableFile {
return (depotKey.Length == 64) && Utilities.IsValidHexadecimalText(depotKey);
}
private static async Task<(bool Success, ImmutableHashSet<uint>? Result)> ResolveKnownDepotIDs(CancellationToken cancellationToken = default) {
private static async Task<(bool Success, FrozenSet<uint>? Result)> ResolveKnownDepotIDs(CancellationToken cancellationToken = default) {
if (ASF.WebBrowser == null) {
throw new InvalidOperationException(nameof(ASF.WebBrowser));
}
@@ -343,7 +355,7 @@ internal sealed class GlobalCache : SerializableFile {
result.Add(depotID);
}
return (result.Count > 0, result.ToImmutableHashSet());
return (result.Count > 0, result.ToFrozenSet());
} catch (Exception e) {
ASF.ArchiLogger.LogGenericWarningException(e);

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -19,16 +21,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.
using Newtonsoft.Json;
using System.Text.Json.Serialization;
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper;
public sealed class GlobalConfigExtension {
[JsonProperty]
public SteamTokenDumperConfig? SteamTokenDumperPlugin { get; private set; }
[JsonInclude]
public SteamTokenDumperConfig? SteamTokenDumperPlugin { get; private init; }
[JsonProperty(Required = Required.DisallowNull)]
public bool SteamTokenDumperPluginEnabled { get; private set; }
[JsonInclude]
public bool SteamTokenDumperPluginEnabled { get; private init; }
[JsonConstructor]
internal GlobalConfigExtension() { }

View File

@@ -97,7 +97,10 @@
<value>Pabeidza iegūt kopumā {0} aplikāciju piekļuves marķierus.</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>Notiek visu depot atslēgu izgūšana, kopā {0} lietotnēm...</value>
<comment>{0} will be replaced by the number (total count) of apps being retrieved</comment>
</data>
<data name="BotRetrievingAppInfos" xml:space="preserve">
<value>Iegūst {0} aplikāciju informāciju...</value>
<comment>{0} will be replaced by the number (count this batch) of app infos being retrieved</comment>
@@ -106,13 +109,33 @@
<value>Pabeidza iegūt {0} aplikāciju informāciju.</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>Veiksmīgi izgūtas {0} no {1} depot atslēgām.</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>Pabeigta visu depot atslēgu izgūšana, kopā {0} lietotnēm.</value>
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
</data>
<data name="SubmissionNoNewData" xml:space="preserve">
<value>Nav jaunu datu, ko iesniegt, viss ir atjaunināts.</value>
</data>
<data name="SubmissionNoContributorSet" xml:space="preserve">
<value>Nevarēja iesniegt datus, jo nav derīgas SteamID kopas, ko mēs varētu klasificēt kā atbalstītāju. Apsveriet iespēju iestatīt {0} rekvizītu.</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>Iesniedz reģistrētās lietotnes/pakotnes/depot, kopumā: {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>Iesniegšana neizdevās, jo tika nosūtīti pārāk daudz pieprasījumi. Mēs mēģināsim vēlreiz pēc aptuveni {0} no šī brīža.</value>
<comment>{0} will be replaced by translated TimeSpan string (such as "53 minutes")</comment>
</data>
<data name="SubmissionSuccessful" xml:space="preserve">
<value>Dati ir veiksmīgi iesniegti. Serveris ir reģistrējis pavisam jaunas lietotnes/pakotnes/depots: {0} ({1} verificēts)/{2} ({3} verificēts)/{4} ({5} verificēts).</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>Jaunas aplikācijas: {0}</value>
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
@@ -129,10 +152,25 @@
<value>Pārbaudītas pakas: {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>Jaunas aplikācijas: {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>Pārbaudītas aplikācijas: {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} inicializēts, spraudnis neatrisinās nevienu no šiem: {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>Notiek STD globālās kešatmiņas ielāde...</value>
</data>
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
<value>Notiek STD globālās kešatmiņas integritātes apstiprināšana...</value>
</data>
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
<value>Neizdevās pārbaudīt STD globālās kešatmiņas integritāti. Tas liecina par iespējamu faila/atmiņas bojājumu, tā vietā tiks inicializēta jauna instance.</value>
</data>
</root>

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

@@ -109,7 +109,10 @@
<value>Dokončené získavanie informácií {0} aplikácií.</value>
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
</data>
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
<value>Úspešne načítaných {0} z {1} depot kľúčov.</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>Dokončené získanie všetkých kľúčov položiek {0} aplikácií.</value>
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
@@ -161,7 +164,13 @@
<value>{0} inicializovaných, modul nebude pracovať so žiadnym: {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>Načítanie globálnej vyrovnávacej pamäti STD...</value>
</data>
<data name="ValidatingGlobalCacheIntegrity" xml:space="preserve">
<value>Overovanie globálnej vyrovnávacej pamäti STD...</value>
</data>
<data name="GlobalCacheIntegrityValidationFailed" xml:space="preserve">
<value>Overovanie globálnej vyrovnávacej pamäti STD sa nepodarilo. To naznačuje, že mohlo dôjsť k poškodeniu súboru/pamäti, miesto toho bude inicializovaná nová inštancia.</value>
</data>
</root>

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,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -20,29 +22,33 @@
// limitations under the License.
using System.Collections.Immutable;
using System.Text.Json.Serialization;
using ArchiSteamFarm.Helpers.Json;
using ArchiSteamFarm.IPC.Integration;
using Newtonsoft.Json;
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper;
public sealed class SteamTokenDumperConfig {
[JsonProperty(Required = Required.DisallowNull)]
[JsonInclude]
public bool Enabled { get; internal set; }
[JsonProperty(Required = Required.DisallowNull)]
[JsonDisallowNull]
[JsonInclude]
[SwaggerItemsMinMax(MinimumUint = 1, MaximumUint = uint.MaxValue)]
public ImmutableHashSet<uint> SecretAppIDs { get; private set; } = ImmutableHashSet<uint>.Empty;
public ImmutableHashSet<uint> SecretAppIDs { get; private init; } = [];
[JsonProperty(Required = Required.DisallowNull)]
[JsonDisallowNull]
[JsonInclude]
[SwaggerItemsMinMax(MinimumUint = 1, MaximumUint = uint.MaxValue)]
public ImmutableHashSet<uint> SecretDepotIDs { get; private set; } = ImmutableHashSet<uint>.Empty;
public ImmutableHashSet<uint> SecretDepotIDs { get; private init; } = [];
[JsonProperty(Required = Required.DisallowNull)]
[JsonDisallowNull]
[JsonInclude]
[SwaggerItemsMinMax(MinimumUint = 1, MaximumUint = uint.MaxValue)]
public ImmutableHashSet<uint> SecretPackageIDs { get; private set; } = ImmutableHashSet<uint>.Empty;
public ImmutableHashSet<uint> SecretPackageIDs { get; private init; } = [];
[JsonProperty(Required = Required.DisallowNull)]
public bool SkipAutoGrantPackages { get; private set; } = true;
[JsonInclude]
public bool SkipAutoGrantPackages { get; private init; } = true;
[JsonConstructor]
internal SteamTokenDumperConfig() { }

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -30,6 +32,6 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper;
public sealed class SteamTokenDumperController : ArchiController {
[HttpGet(nameof(GlobalConfigExtension))]
[ProducesResponseType<GlobalConfigExtension>((int) HttpStatusCode.OK)]
[SwaggerOperation(Tags = new[] { nameof(GlobalConfigExtension) })]
[SwaggerOperation(Tags = [nameof(GlobalConfigExtension)])]
public ActionResult<GlobalConfigExtension> Get() => Ok(new GlobalConfigExtension());
}

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -21,17 +23,21 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Composition;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Helpers;
using ArchiSteamFarm.Helpers.Json;
using ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.Data;
using ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.Localization;
using ArchiSteamFarm.Plugins;
@@ -41,8 +47,6 @@ using ArchiSteamFarm.Steam.Interaction;
using ArchiSteamFarm.Storage;
using ArchiSteamFarm.Web;
using ArchiSteamFarm.Web.Responses;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SteamKit2;
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper;
@@ -51,7 +55,6 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper;
internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotCommand2, IBotSteamClient, ISteamPICSChanges {
private const ushort DepotsRateLimitingDelay = 500;
[JsonProperty]
internal static SteamTokenDumperConfig? Config { get; private set; }
private static readonly ConcurrentDictionary<Bot, IDisposable> BotSubscriptions = new();
@@ -62,15 +65,17 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
private static GlobalCache? GlobalCache;
private static DateTimeOffset LastUploadAt = DateTimeOffset.MinValue;
[JsonProperty]
[JsonInclude]
[Required]
public override string Name => nameof(SteamTokenDumperPlugin);
[JsonProperty]
[JsonInclude]
[Required]
public override Version Version => typeof(SteamTokenDumperPlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version));
public Task<uint> GetPreferredChangeNumberToStartFrom() => Task.FromResult(GlobalCache?.LastChangeNumber ?? 0);
public async Task OnASFInit(IReadOnlyDictionary<string, JToken>? additionalConfigProperties = null) {
public async Task OnASFInit(IReadOnlyDictionary<string, JsonElement>? additionalConfigProperties = null) {
if (!SharedInfo.HasValidToken) {
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.PluginDisabledMissingBuildToken, nameof(SteamTokenDumperPlugin)));
@@ -81,15 +86,19 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
SteamTokenDumperConfig? config = null;
if (additionalConfigProperties != null) {
foreach ((string configProperty, JToken configValue) in additionalConfigProperties) {
foreach ((string configProperty, JsonElement configValue) in additionalConfigProperties) {
try {
switch (configProperty) {
case nameof(GlobalConfigExtension.SteamTokenDumperPlugin):
config = configValue.ToObject<SteamTokenDumperConfig>();
config = configValue.ToJsonObject<SteamTokenDumperConfig>();
break;
case nameof(GlobalConfigExtension.SteamTokenDumperPluginEnabled):
isEnabled = configValue.Value<bool>();
case nameof(GlobalConfigExtension.SteamTokenDumperPluginEnabled) when configValue.ValueKind == JsonValueKind.False:
isEnabled = false;
break;
case nameof(GlobalConfigExtension.SteamTokenDumperPluginEnabled) when configValue.ValueKind == JsonValueKind.True:
isEnabled = true;
break;
}
@@ -160,7 +169,7 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginInitializedAndEnabled, nameof(SteamTokenDumperPlugin), startIn.ToHumanReadable()));
}
public async Task<string?> OnBotCommand(Bot bot, EAccess access, string message, string[] args, ulong steamID = 0) {
public Task<string?> OnBotCommand(Bot bot, EAccess access, string message, string[] args, ulong steamID = 0) {
ArgumentNullException.ThrowIfNull(bot);
if (!Enum.IsDefined(access)) {
@@ -175,20 +184,20 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
case 1:
switch (args[0].ToUpperInvariant()) {
case "STD":
return await ResponseRefreshManually(access, bot).ConfigureAwait(false);
return Task.FromResult(ResponseRefreshManually(access, bot));
}
break;
default:
switch (args[0].ToUpperInvariant()) {
case "STD":
return await ResponseRefreshManually(access, Utilities.GetArgsAsText(args, 1, ","), steamID).ConfigureAwait(false);
return Task.FromResult(ResponseRefreshManually(access, Utilities.GetArgsAsText(args, 1, ","), steamID));
}
break;
}
return null;
return Task.FromResult((string?) null);
}
public async Task OnBotDestroy(Bot bot) {
@@ -289,14 +298,26 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
return;
}
HashSet<uint> packageIDs = callback.LicenseList.Where(static license => !Config.SecretPackageIDs.Contains(license.PackageID) && ((license.PaymentMethod != EPaymentMethod.AutoGrant) || !Config.SkipAutoGrantPackages)).Select(static license => license.PackageID).ToHashSet();
// Schedule a refresh in a while from now
if (!BotSynchronizations.TryGetValue(bot, out (SemaphoreSlim RefreshSemaphore, Timer RefreshTimer) synchronization)) {
return;
}
await Refresh(bot, packageIDs).ConfigureAwait(false);
if (!await synchronization.RefreshSemaphore.WaitAsync(0).ConfigureAwait(false)) {
// Another refresh is in progress, skip the refresh for now
return;
}
try {
synchronization.RefreshTimer.Change(TimeSpan.FromMinutes(1), TimeSpan.FromHours(SharedInfo.MaximumHoursBetweenRefresh));
} finally {
synchronization.RefreshSemaphore.Release();
}
}
private static async void OnSubmissionTimer(object? state = null) => await SubmitData().ConfigureAwait(false);
private static async Task Refresh(Bot bot, IReadOnlyCollection<uint>? packageIDs = null) {
private static async Task Refresh(Bot bot) {
ArgumentNullException.ThrowIfNull(bot);
if (GlobalCache == null) {
@@ -322,9 +343,9 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
return;
}
packageIDs ??= bot.OwnedPackageIDs.Where(static package => (Config?.SecretPackageIDs.Contains(package.Key) != true) && ((package.Value.PaymentMethod != EPaymentMethod.AutoGrant) || (Config?.SkipAutoGrantPackages == false))).Select(static package => package.Key).ToHashSet();
HashSet<uint> packageIDs = bot.OwnedPackageIDs.Where(static package => (Config?.SecretPackageIDs.Contains(package.Key) != true) && ((package.Value.PaymentMethod != EPaymentMethod.AutoGrant) || (Config?.SkipAutoGrantPackages == false))).Select(static package => package.Key).ToHashSet();
HashSet<uint> appIDsToRefresh = new();
HashSet<uint> appIDsToRefresh = [];
foreach (uint packageID in packageIDs.Where(static packageID => Config?.SecretPackageIDs.Contains(packageID) != true)) {
if (!ASF.GlobalDatabase.PackagesDataReadOnly.TryGetValue(packageID, out PackageData? packageData) || (packageData.AppIDs == null)) {
@@ -384,7 +405,7 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.BotFinishedRetrievingTotalAppAccessTokens, appIDsToRefresh.Count));
bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.BotRetrievingTotalDepots, appIDsToRefresh.Count));
(_, ImmutableHashSet<uint>? knownDepotIDs) = await GlobalCache.KnownDepotIDs.GetValue(ECacheFallback.SuccessPreviously).ConfigureAwait(false);
(_, FrozenSet<uint>? knownDepotIDs) = await GlobalCache.KnownDepotIDs.GetValue(ECacheFallback.SuccessPreviously).ConfigureAwait(false);
using (HashSet<uint>.Enumerator enumerator = appIDsToRefresh.GetEnumerator()) {
while (true) {
@@ -500,7 +521,9 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
}
}
bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.BotFinishedRetrievingDepotKeys, depotKeysSuccessful, depotKeysTotal));
if (depotKeysTotal > 0) {
bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.BotFinishedRetrievingDepotKeys, depotKeysSuccessful, depotKeysTotal));
}
if (depotKeysSuccessful < depotKeysTotal) {
// We're not going to record app change numbers, as we didn't fetch all the depot keys we wanted
@@ -527,7 +550,7 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
}
}
private static async Task<string?> ResponseRefreshManually(EAccess access, Bot bot, bool submit = true) {
private static string? ResponseRefreshManually(EAccess access, Bot bot) {
if (!Enum.IsDefined(access)) {
throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess));
}
@@ -542,16 +565,17 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.WarningFailedWithError, nameof(GlobalCache)));
}
await Refresh(bot).ConfigureAwait(false);
if (submit) {
Utilities.InBackground(static () => SubmitData());
}
Utilities.InBackground(
async () => {
await Refresh(bot).ConfigureAwait(false);
await SubmitData().ConfigureAwait(false);
}
);
return bot.Commands.FormatBotResponse(ArchiSteamFarm.Localization.Strings.Done);
}
private static async Task<string?> ResponseRefreshManually(EAccess access, string botNames, ulong steamID = 0) {
private static string? ResponseRefreshManually(EAccess access, string botNames, ulong steamID = 0) {
if (!Enum.IsDefined(access)) {
throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess));
}
@@ -568,17 +592,25 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
return access >= EAccess.Owner ? Commands.FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.BotNotFound, botNames)) : null;
}
if (bots.RemoveWhere(bot => Commands.GetProxyAccess(bot, access, steamID) < EAccess.Master) > 0) {
if (bots.Count == 0) {
return access >= EAccess.Owner ? Commands.FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.BotNotFound, botNames)) : null;
}
}
if (GlobalCache == null) {
return Commands.FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.WarningFailedWithError, nameof(GlobalCache)));
}
IList<string?> results = await Utilities.InParallel(bots.Select(bot => ResponseRefreshManually(Commands.GetProxyAccess(bot, access, steamID), bot, false))).ConfigureAwait(false);
Utilities.InBackground(
async () => {
await Utilities.InParallel(bots.Select(static bot => Refresh(bot))).ConfigureAwait(false);
Utilities.InBackground(static () => SubmitData());
await SubmitData().ConfigureAwait(false);
}
);
List<string> responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!);
return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null;
return Commands.FormatStaticResponse(ArchiSteamFarm.Localization.Strings.Done);
}
private static async Task SubmitData(CancellationToken cancellationToken = default) {
@@ -680,7 +712,7 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
}
if (response.Content.Data == null) {
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.ErrorIsInvalid), nameof(response.Content.Data));
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.ErrorIsInvalid, nameof(response.Content.Data)));
return;
}

View File

@@ -6,9 +6,7 @@
<ItemGroup>
<PackageReference Include="ConfigureAwaitChecker.Analyzer" PrivateAssets="all" />
<PackageReference Include="JetBrains.Annotations" PrivateAssets="all" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="MSTest.TestAdapter" />
<PackageReference Include="MSTest.TestFramework" />
<PackageReference Include="MSTest" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -39,11 +41,11 @@ public sealed class Bot {
{ 43, MinCardsPerBadge + 1 }
};
HashSet<Asset> items = new();
HashSet<Asset> items = [];
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));
}
}
@@ -55,30 +57,27 @@ public sealed class Bot {
}
[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void MaxItemsTooSmall() {
const uint appID = 42;
HashSet<Asset> items = new() {
CreateCard(1, appID),
CreateCard(2, appID)
};
HashSet<Asset> items = [
CreateCard(1, realAppID: appID),
CreateCard(2, realAppID: appID)
];
GetItemsForFullBadge(items, 2, appID, MinCardsPerBadge - 1);
Assert.Fail();
Assert.ThrowsException<ArgumentOutOfRangeException>(() => GetItemsForFullBadge(items, 2, appID, MinCardsPerBadge - 1));
}
[TestMethod]
public void MoreCardsThanNeeded() {
const uint appID = 42;
HashSet<Asset> items = new() {
CreateCard(1, appID),
CreateCard(1, appID),
CreateCard(2, appID),
CreateCard(3, appID)
};
HashSet<Asset> items = [
CreateCard(1, realAppID: appID),
CreateCard(1, realAppID: appID),
CreateCard(2, realAppID: appID),
CreateCard(3, realAppID: appID)
];
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 3, appID);
@@ -95,12 +94,12 @@ public sealed class Bot {
public void MultipleSets() {
const uint appID = 42;
HashSet<Asset> items = new() {
CreateCard(1, appID),
CreateCard(1, appID),
CreateCard(2, appID),
CreateCard(2, appID)
};
HashSet<Asset> items = [
CreateCard(1, realAppID: appID),
CreateCard(1, realAppID: appID),
CreateCard(2, realAppID: appID),
CreateCard(2, realAppID: appID)
];
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
@@ -116,11 +115,11 @@ public sealed class Bot {
public void MultipleSetsDifferentAmount() {
const uint appID = 42;
HashSet<Asset> items = new() {
CreateCard(1, appID, 2),
CreateCard(2, appID),
CreateCard(2, appID)
};
HashSet<Asset> items = [
CreateCard(1, amount: 2, realAppID: appID),
CreateCard(2, realAppID: appID),
CreateCard(2, realAppID: appID)
];
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
@@ -136,29 +135,29 @@ public sealed class Bot {
public void MutliRarityAndType() {
const uint appID = 42;
HashSet<Asset> items = new() {
CreateCard(1, appID, type: Asset.EType.TradingCard, rarity: Asset.ERarity.Common),
CreateCard(2, appID, type: Asset.EType.TradingCard, rarity: Asset.ERarity.Common),
HashSet<Asset> items = [
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);
@@ -177,10 +176,10 @@ public sealed class Bot {
public void NotAllCardsPresent() {
const uint appID = 42;
HashSet<Asset> items = new() {
CreateCard(1, appID),
CreateCard(2, appID)
};
HashSet<Asset> items = [
CreateCard(1, realAppID: appID),
CreateCard(2, realAppID: appID)
];
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 3, appID);
@@ -192,10 +191,10 @@ public sealed class Bot {
public void OneSet() {
const uint appID = 42;
HashSet<Asset> items = new() {
CreateCard(1, appID),
CreateCard(2, appID)
};
HashSet<Asset> items = [
CreateCard(1, realAppID: appID),
CreateCard(2, realAppID: appID)
];
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
@@ -212,10 +211,10 @@ public sealed class Bot {
const uint appID0 = 42;
const uint appID1 = 43;
HashSet<Asset> items = new() {
CreateCard(1, appID0),
CreateCard(1, appID1)
};
HashSet<Asset> items = [
CreateCard(1, realAppID: appID0),
CreateCard(1, realAppID: appID1)
];
HashSet<Asset> itemsToSend = GetItemsForFullBadge(
items, new Dictionary<uint, byte> {
@@ -237,10 +236,10 @@ public sealed class Bot {
const uint appID0 = 42;
const uint appID1 = 43;
HashSet<Asset> items = new() {
CreateCard(1, appID0),
CreateCard(1, appID1)
};
HashSet<Asset> items = [
CreateCard(1, realAppID: appID0),
CreateCard(1, realAppID: appID1)
];
HashSet<Asset> itemsToSend = GetItemsForFullBadge(
items, new Dictionary<uint, byte> {
@@ -260,14 +259,14 @@ public sealed class Bot {
const uint appID1 = 43;
const uint appID2 = 44;
HashSet<Asset> items = new() {
CreateCard(1, appID0),
CreateCard(2, appID0),
HashSet<Asset> items = [
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(
items, new Dictionary<uint, byte> {
@@ -290,10 +289,10 @@ public sealed class Bot {
public void OtherRarityFullSets() {
const uint appID = 42;
HashSet<Asset> items = new() {
CreateCard(1, appID, rarity: Asset.ERarity.Common),
CreateCard(1, appID, rarity: Asset.ERarity.Rare)
};
HashSet<Asset> items = [
CreateCard(1, realAppID: appID, rarity: EAssetRarity.Common),
CreateCard(1, realAppID: appID, rarity: EAssetRarity.Rare)
];
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 1, appID);
@@ -308,10 +307,10 @@ public sealed class Bot {
public void OtherRarityNoSets() {
const uint appID = 42;
HashSet<Asset> items = new() {
CreateCard(1, appID, rarity: Asset.ERarity.Common),
CreateCard(1, appID, rarity: Asset.ERarity.Rare)
};
HashSet<Asset> items = [
CreateCard(1, realAppID: appID, rarity: EAssetRarity.Common),
CreateCard(1, realAppID: appID, rarity: EAssetRarity.Rare)
];
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
@@ -324,13 +323,13 @@ public sealed class Bot {
public void OtherRarityOneSet() {
const uint appID = 42;
HashSet<Asset> items = new() {
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)
};
HashSet<Asset> items = [
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);
@@ -347,10 +346,10 @@ public sealed class Bot {
public void OtherTypeFullSets() {
const uint appID = 42;
HashSet<Asset> items = new() {
CreateCard(1, appID, type: Asset.EType.TradingCard),
CreateCard(1, appID, type: Asset.EType.FoilTradingCard)
};
HashSet<Asset> items = [
CreateCard(1, realAppID: appID, type: EAssetType.TradingCard),
CreateCard(1, realAppID: appID, type: EAssetType.FoilTradingCard)
];
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 1, appID);
@@ -365,10 +364,10 @@ public sealed class Bot {
public void OtherTypeNoSets() {
const uint appID = 42;
HashSet<Asset> items = new() {
CreateCard(1, appID, type: Asset.EType.TradingCard),
CreateCard(1, appID, type: Asset.EType.FoilTradingCard)
};
HashSet<Asset> items = [
CreateCard(1, realAppID: appID, type: EAssetType.TradingCard),
CreateCard(1, realAppID: appID, type: EAssetType.FoilTradingCard)
];
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
@@ -381,13 +380,13 @@ public sealed class Bot {
public void OtherTypeOneSet() {
const uint appID = 42;
HashSet<Asset> items = new() {
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)
};
HashSet<Asset> items = [
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);
@@ -404,10 +403,10 @@ public sealed class Bot {
public void TooHighAmount() {
const uint appID0 = 42;
HashSet<Asset> items = new() {
CreateCard(1, appID0, 2),
CreateCard(2, appID0)
};
HashSet<Asset> items = [
CreateCard(1, amount: 2, realAppID: appID0),
CreateCard(2, realAppID: appID0)
];
HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID0);
@@ -423,11 +422,11 @@ public sealed class Bot {
public void TooManyCardsForSingleTrade() {
const uint appID = 42;
HashSet<Asset> items = new();
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,13 +439,13 @@ public sealed class Bot {
const uint appID0 = 42;
const uint appID1 = 43;
HashSet<Asset> items = new();
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() {
@@ -460,28 +459,27 @@ public sealed class Bot {
}
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void TooManyCardsPerSet() {
const uint appID0 = 42;
const uint appID1 = 43;
const uint appID2 = 44;
HashSet<Asset> items = new() {
CreateCard(1, appID0),
CreateCard(2, appID0),
CreateCard(3, appID0),
CreateCard(4, appID0)
};
HashSet<Asset> items = [
CreateCard(1, realAppID: appID0),
CreateCard(2, realAppID: appID0),
CreateCard(3, realAppID: appID0),
CreateCard(4, realAppID: appID0)
];
GetItemsForFullBadge(
items, new Dictionary<uint, byte> {
{ appID0, 3 },
{ appID1, 3 },
{ appID2, 3 }
}
Assert.ThrowsException<InvalidOperationException>(
() => GetItemsForFullBadge(
items, new Dictionary<uint, byte> {
{ appID0, 3 },
{ appID1, 3 },
{ appID2, 3 }
}
)
);
Assert.Fail();
}
private static void AssertResultMatchesExpectation(IReadOnlyDictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult, IReadOnlyCollection<Asset> itemsToSend) {
@@ -493,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,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -200,60 +202,62 @@ public sealed class SteamChatMessage {
public async Task RyzhehvostInitialTestForSplitting() {
const string prefix = "/me ";
const string message = @"<XLimited5> Уже имеет: app/1493800 | Aircraft Carrier Survival: Prolouge
<XLimited5> Уже имеет: app/349520 | Armillo
<XLimited5> Уже имеет: app/346330 | BrainBread 2
<XLimited5> Уже имеет: app/1086690 | C-War 2
<XLimited5> Уже имеет: app/730 | Counter-Strike: Global Offensive
<XLimited5> Уже имеет: app/838380 | DEAD OR ALIVE 6
<XLimited5> Уже имеет: app/582890 | Estranged: The Departure
<XLimited5> Уже имеет: app/331470 | Everlasting Summer
<XLimited5> Уже имеет: app/1078000 | Gamecraft
<XLimited5> Уже имеет: app/266310 | GameGuru
<XLimited5> Уже имеет: app/275390 | Guacamelee! Super Turbo Championship Edition
<XLimited5> Уже имеет: app/627690 | Idle Champions of the Forgotten Realms
<XLimited5> Уже имеет: app/1048540 | Kao the Kangaroo: Round 2
<XLimited5> Уже имеет: app/370910 | Kathy Rain
<XLimited5> Уже имеет: app/343710 | KHOLAT
<XLimited5> Уже имеет: app/253900 | Knights and Merchants
<XLimited5> Уже имеет: app/224260 | No More Room in Hell
<XLimited5> Уже имеет: app/343360 | Particula
<XLimited5> Уже имеет: app/237870 | Planet Explorers
<XLimited5> Уже имеет: app/684680 | Polygoneer
<XLimited5> Уже имеет: app/1089130 | Quake II RTX
<XLimited5> Уже имеет: app/755790 | Ring of Elysium
<XLimited5> Уже имеет: app/1258080 | Shop Titans
<XLimited5> Уже имеет: app/759530 | Struckd - 3D Game Creator
<XLimited5> Уже имеет: app/269710 | Tumblestone
<XLimited5> Уже имеет: app/304930 | Unturned
<XLimited5> Уже имеет: app/1019250 | WWII TCG - World War 2: The Card Game
const string message = """
<XLimited5> Уже имеет: app/1493800 | Aircraft Carrier Survival: Prolouge
<XLimited5> Уже имеет: app/349520 | Armillo
<XLimited5> Уже имеет: app/346330 | BrainBread 2
<XLimited5> Уже имеет: app/1086690 | C-War 2
<XLimited5> Уже имеет: app/730 | Counter-Strike: Global Offensive
<XLimited5> Уже имеет: app/838380 | DEAD OR ALIVE 6
<XLimited5> Уже имеет: app/582890 | Estranged: The Departure
<XLimited5> Уже имеет: app/331470 | Everlasting Summer
<XLimited5> Уже имеет: app/1078000 | Gamecraft
<XLimited5> Уже имеет: app/266310 | GameGuru
<XLimited5> Уже имеет: app/275390 | Guacamelee! Super Turbo Championship Edition
<XLimited5> Уже имеет: app/627690 | Idle Champions of the Forgotten Realms
<XLimited5> Уже имеет: app/1048540 | Kao the Kangaroo: Round 2
<XLimited5> Уже имеет: app/370910 | Kathy Rain
<XLimited5> Уже имеет: app/343710 | KHOLAT
<XLimited5> Уже имеет: app/253900 | Knights and Merchants
<XLimited5> Уже имеет: app/224260 | No More Room in Hell
<XLimited5> Уже имеет: app/343360 | Particula
<XLimited5> Уже имеет: app/237870 | Planet Explorers
<XLimited5> Уже имеет: app/684680 | Polygoneer
<XLimited5> Уже имеет: app/1089130 | Quake II RTX
<XLimited5> Уже имеет: app/755790 | Ring of Elysium
<XLimited5> Уже имеет: app/1258080 | Shop Titans
<XLimited5> Уже имеет: app/759530 | Struckd - 3D Game Creator
<XLimited5> Уже имеет: app/269710 | Tumblestone
<XLimited5> Уже имеет: app/304930 | Unturned
<XLimited5> Уже имеет: app/1019250 | WWII TCG - World War 2: The Card Game
<ASF> 1/1 ботов уже имеют игру app/1493800 | Aircraft Carrier Survival: Prolouge.
<ASF> 1/1 ботов уже имеют игру app/349520 | Armillo.
<ASF> 1/1 ботов уже имеют игру app/346330 | BrainBread 2.
<ASF> 1/1 ботов уже имеют игру app/1086690 | C-War 2.
<ASF> 1/1 ботов уже имеют игру app/730 | Counter-Strike: Global Offensive.
<ASF> 1/1 ботов уже имеют игру app/838380 | DEAD OR ALIVE 6.
<ASF> 1/1 ботов уже имеют игру app/582890 | Estranged: The Departure.
<ASF> 1/1 ботов уже имеют игру app/331470 | Everlasting Summer.
<ASF> 1/1 ботов уже имеют игру app/1078000 | Gamecraft.
<ASF> 1/1 ботов уже имеют игру app/266310 | GameGuru.
<ASF> 1/1 ботов уже имеют игру app/275390 | Guacamelee! Super Turbo Championship Edition.
<ASF> 1/1 ботов уже имеют игру app/627690 | Idle Champions of the Forgotten Realms.
<ASF> 1/1 ботов уже имеют игру app/1048540 | Kao the Kangaroo: Round 2.
<ASF> 1/1 ботов уже имеют игру app/370910 | Kathy Rain.
<ASF> 1/1 ботов уже имеют игру app/343710 | KHOLAT.
<ASF> 1/1 ботов уже имеют игру app/253900 | Knights and Merchants.
<ASF> 1/1 ботов уже имеют игру app/224260 | No More Room in Hell.
<ASF> 1/1 ботов уже имеют игру app/343360 | Particula.
<ASF> 1/1 ботов уже имеют игру app/237870 | Planet Explorers.
<ASF> 1/1 ботов уже имеют игру app/684680 | Polygoneer.
<ASF> 1/1 ботов уже имеют игру app/1089130 | Quake II RTX.
<ASF> 1/1 ботов уже имеют игру app/755790 | Ring of Elysium.
<ASF> 1/1 ботов уже имеют игру app/1258080 | Shop Titans.
<ASF> 1/1 ботов уже имеют игру app/759530 | Struckd - 3D Game Creator.
<ASF> 1/1 ботов уже имеют игру app/269710 | Tumblestone.
<ASF> 1/1 ботов уже имеют игру app/304930 | Unturned.";
<ASF> 1/1 ботов уже имеют игру app/1493800 | Aircraft Carrier Survival: Prolouge.
<ASF> 1/1 ботов уже имеют игру app/349520 | Armillo.
<ASF> 1/1 ботов уже имеют игру app/346330 | BrainBread 2.
<ASF> 1/1 ботов уже имеют игру app/1086690 | C-War 2.
<ASF> 1/1 ботов уже имеют игру app/730 | Counter-Strike: Global Offensive.
<ASF> 1/1 ботов уже имеют игру app/838380 | DEAD OR ALIVE 6.
<ASF> 1/1 ботов уже имеют игру app/582890 | Estranged: The Departure.
<ASF> 1/1 ботов уже имеют игру app/331470 | Everlasting Summer.
<ASF> 1/1 ботов уже имеют игру app/1078000 | Gamecraft.
<ASF> 1/1 ботов уже имеют игру app/266310 | GameGuru.
<ASF> 1/1 ботов уже имеют игру app/275390 | Guacamelee! Super Turbo Championship Edition.
<ASF> 1/1 ботов уже имеют игру app/627690 | Idle Champions of the Forgotten Realms.
<ASF> 1/1 ботов уже имеют игру app/1048540 | Kao the Kangaroo: Round 2.
<ASF> 1/1 ботов уже имеют игру app/370910 | Kathy Rain.
<ASF> 1/1 ботов уже имеют игру app/343710 | KHOLAT.
<ASF> 1/1 ботов уже имеют игру app/253900 | Knights and Merchants.
<ASF> 1/1 ботов уже имеют игру app/224260 | No More Room in Hell.
<ASF> 1/1 ботов уже имеют игру app/343360 | Particula.
<ASF> 1/1 ботов уже имеют игру app/237870 | Planet Explorers.
<ASF> 1/1 ботов уже имеют игру app/684680 | Polygoneer.
<ASF> 1/1 ботов уже имеют игру app/1089130 | Quake II RTX.
<ASF> 1/1 ботов уже имеют игру app/755790 | Ring of Elysium.
<ASF> 1/1 ботов уже имеют игру app/1258080 | Shop Titans.
<ASF> 1/1 ботов уже имеют игру app/759530 | Struckd - 3D Game Creator.
<ASF> 1/1 ботов уже имеют игру app/269710 | Tumblestone.
<ASF> 1/1 ботов уже имеют игру app/304930 | Unturned.
""";
List<string> output = await GetMessageParts(message, prefix).ToListAsync().ConfigureAwait(false);
@@ -309,27 +313,21 @@ public sealed class SteamChatMessage {
Assert.AreEqual(newlinePart, output[3]);
}
[ExpectedException(typeof(ArgumentOutOfRangeException))]
[TestMethod]
public async Task ThrowsOnTooLongNewlinesPrefix() {
string prefix = new('\n', (MaxMessagePrefixBytes / NewlineWeight) + 1);
const string message = "asdf";
await GetMessageParts(message, prefix).ToListAsync().ConfigureAwait(false);
Assert.Fail();
await Assert.ThrowsExceptionAsync<ArgumentOutOfRangeException>(async () => await GetMessageParts(message, prefix).ToListAsync().ConfigureAwait(false)).ConfigureAwait(false);
}
[ExpectedException(typeof(ArgumentOutOfRangeException))]
[TestMethod]
public async Task ThrowsOnTooLongPrefix() {
string prefix = new('x', MaxMessagePrefixBytes + 1);
const string message = "asdf";
await GetMessageParts(message, prefix).ToListAsync().ConfigureAwait(false);
Assert.Fail();
await Assert.ThrowsExceptionAsync<ArgumentOutOfRangeException>(async () => await GetMessageParts(message, prefix).ToListAsync().ConfigureAwait(false)).ConfigureAwait(false);
}
}

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -30,21 +32,21 @@ namespace ArchiSteamFarm.Tests;
public sealed class Trading {
[TestMethod]
public void ExploitingNewSetsIsFairButNotNeutral() {
HashSet<Asset> inventory = new() {
CreateItem(1, 40),
CreateItem(2, 10),
CreateItem(3, 10)
};
HashSet<Asset> inventory = [
CreateItem(1, amount: 40),
CreateItem(2, amount: 10),
CreateItem(3, amount: 10)
];
HashSet<Asset> itemsToGive = new() {
CreateItem(2, 5),
CreateItem(3, 5)
};
HashSet<Asset> itemsToGive = [
CreateItem(2, amount: 5),
CreateItem(3, amount: 5)
];
HashSet<Asset> itemsToReceive = new() {
CreateItem(1, 9),
HashSet<Asset> itemsToReceive = [
CreateItem(1, amount: 9),
CreateItem(4)
};
];
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
@@ -52,45 +54,60 @@ public sealed class Trading {
[TestMethod]
public void MismatchRarityIsNotFair() {
HashSet<Asset> itemsToGive = new() { CreateItem(1, rarity: Asset.ERarity.Rare) };
HashSet<Asset> itemsToReceive = new() { 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 = new() { CreateItem(1, realAppID: 570) };
HashSet<Asset> itemsToReceive = new() { 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 = new() { CreateItem(1, type: Asset.EType.Emoticon) };
HashSet<Asset> itemsToReceive = new() { CreateItem(2) };
HashSet<Asset> itemsToGive = [
CreateItem(1, type: EAssetType.Emoticon)
];
HashSet<Asset> itemsToReceive = [
CreateItem(2)
];
Assert.IsFalse(IsFairExchange(itemsToGive, itemsToReceive));
}
[TestMethod]
public void MultiGameMultiTypeBadReject() {
HashSet<Asset> inventory = new() {
CreateItem(1, 9),
CreateItem(3, 9, 730, Asset.EType.Emoticon),
CreateItem(4, realAppID: 730, type: Asset.EType.Emoticon)
};
HashSet<Asset> inventory = [
CreateItem(1, amount: 9),
CreateItem(3, amount: 9, realAppID: 730, type: EAssetType.Emoticon),
CreateItem(4, realAppID: 730, type: EAssetType.Emoticon)
];
HashSet<Asset> itemsToGive = new() {
HashSet<Asset> itemsToGive = [
CreateItem(1),
CreateItem(4, realAppID: 730, type: Asset.EType.Emoticon)
};
CreateItem(4, realAppID: 730, type: EAssetType.Emoticon)
];
HashSet<Asset> itemsToReceive = new() {
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));
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
@@ -98,20 +115,20 @@ public sealed class Trading {
[TestMethod]
public void MultiGameMultiTypeNeutralAccept() {
HashSet<Asset> inventory = new() {
CreateItem(1, 9),
CreateItem(3, realAppID: 730, type: Asset.EType.Emoticon)
};
HashSet<Asset> inventory = [
CreateItem(1, amount: 9),
CreateItem(3, realAppID: 730, type: EAssetType.Emoticon)
];
HashSet<Asset> itemsToGive = new() {
HashSet<Asset> itemsToGive = [
CreateItem(1),
CreateItem(3, realAppID: 730, type: Asset.EType.Emoticon)
};
CreateItem(3, realAppID: 730, type: EAssetType.Emoticon)
];
HashSet<Asset> itemsToReceive = new() {
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));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
@@ -119,21 +136,21 @@ public sealed class Trading {
[TestMethod]
public void MultiGameSingleTypeBadReject() {
HashSet<Asset> inventory = new() {
CreateItem(1, 9),
HashSet<Asset> inventory = [
CreateItem(1, amount: 9),
CreateItem(3, realAppID: 730),
CreateItem(4, realAppID: 730)
};
];
HashSet<Asset> itemsToGive = new() {
HashSet<Asset> itemsToGive = [
CreateItem(1),
CreateItem(3, realAppID: 730)
};
];
HashSet<Asset> itemsToReceive = new() {
HashSet<Asset> itemsToReceive = [
CreateItem(2),
CreateItem(4, realAppID: 730)
};
];
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
@@ -141,20 +158,20 @@ public sealed class Trading {
[TestMethod]
public void MultiGameSingleTypeNeutralAccept() {
HashSet<Asset> inventory = new() {
CreateItem(1, 2),
HashSet<Asset> inventory = [
CreateItem(1, amount: 2),
CreateItem(3, realAppID: 730)
};
];
HashSet<Asset> itemsToGive = new() {
HashSet<Asset> itemsToGive = [
CreateItem(1),
CreateItem(3, realAppID: 730)
};
];
HashSet<Asset> itemsToReceive = new() {
HashSet<Asset> itemsToReceive = [
CreateItem(2),
CreateItem(4, realAppID: 730)
};
];
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
@@ -162,21 +179,21 @@ public sealed class Trading {
[TestMethod]
public void SingleGameAbrynosWasWrongNeutralAccept() {
HashSet<Asset> inventory = new() {
HashSet<Asset> inventory = [
CreateItem(1),
CreateItem(2, 2),
CreateItem(2, amount: 2),
CreateItem(3),
CreateItem(4),
CreateItem(5)
};
];
HashSet<Asset> itemsToGive = new() {
HashSet<Asset> itemsToGive = [
CreateItem(2)
};
];
HashSet<Asset> itemsToReceive = new() {
HashSet<Asset> itemsToReceive = [
CreateItem(3)
};
];
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
@@ -184,18 +201,18 @@ public sealed class Trading {
[TestMethod]
public void SingleGameDonationAccept() {
HashSet<Asset> inventory = new() {
HashSet<Asset> inventory = [
CreateItem(1)
};
];
HashSet<Asset> itemsToGive = new() {
HashSet<Asset> itemsToGive = [
CreateItem(1)
};
];
HashSet<Asset> itemsToReceive = new() {
HashSet<Asset> itemsToReceive = [
CreateItem(2),
CreateItem(3, type: Asset.EType.SteamGems)
};
CreateItem(3, type: EAssetType.SteamGems)
];
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
@@ -203,21 +220,21 @@ public sealed class Trading {
[TestMethod]
public void SingleGameMultiTypeBadReject() {
HashSet<Asset> inventory = new() {
CreateItem(1, 9),
CreateItem(3, 9, type: Asset.EType.Emoticon),
CreateItem(4, type: Asset.EType.Emoticon)
};
HashSet<Asset> inventory = [
CreateItem(1, amount: 9),
CreateItem(3, amount: 9, type: EAssetType.Emoticon),
CreateItem(4, type: EAssetType.Emoticon)
];
HashSet<Asset> itemsToGive = new() {
HashSet<Asset> itemsToGive = [
CreateItem(1),
CreateItem(4, type: Asset.EType.Emoticon)
};
CreateItem(4, type: EAssetType.Emoticon)
];
HashSet<Asset> itemsToReceive = new() {
HashSet<Asset> itemsToReceive = [
CreateItem(2),
CreateItem(3, type: Asset.EType.Emoticon)
};
CreateItem(3, type: EAssetType.Emoticon)
];
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
@@ -225,20 +242,20 @@ public sealed class Trading {
[TestMethod]
public void SingleGameMultiTypeNeutralAccept() {
HashSet<Asset> inventory = new() {
CreateItem(1, 9),
CreateItem(3, type: Asset.EType.Emoticon)
};
HashSet<Asset> inventory = [
CreateItem(1, amount: 9),
CreateItem(3, type: EAssetType.Emoticon)
];
HashSet<Asset> itemsToGive = new() {
HashSet<Asset> itemsToGive = [
CreateItem(1),
CreateItem(3, type: Asset.EType.Emoticon)
};
CreateItem(3, type: EAssetType.Emoticon)
];
HashSet<Asset> itemsToReceive = new() {
HashSet<Asset> itemsToReceive = [
CreateItem(2),
CreateItem(4, type: Asset.EType.Emoticon)
};
CreateItem(4, type: EAssetType.Emoticon)
];
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
@@ -246,19 +263,21 @@ public sealed class Trading {
[TestMethod]
public void SingleGameQuantityBadReject() {
HashSet<Asset> inventory = new() {
HashSet<Asset> inventory = [
CreateItem(1),
CreateItem(2),
CreateItem(3)
};
];
HashSet<Asset> itemsToGive = new() {
HashSet<Asset> itemsToGive = [
CreateItem(1),
CreateItem(2),
CreateItem(3)
};
];
HashSet<Asset> itemsToReceive = new() { CreateItem(4, 3) };
HashSet<Asset> itemsToReceive = [
CreateItem(4, amount: 3)
];
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
@@ -266,17 +285,19 @@ public sealed class Trading {
[TestMethod]
public void SingleGameQuantityBadReject2() {
HashSet<Asset> inventory = new() {
HashSet<Asset> inventory = [
CreateItem(1),
CreateItem(2, 2)
};
CreateItem(2, amount: 2)
];
HashSet<Asset> itemsToGive = new() {
HashSet<Asset> itemsToGive = [
CreateItem(1),
CreateItem(2, 2)
};
CreateItem(2, amount: 2)
];
HashSet<Asset> itemsToReceive = new() { CreateItem(3, 3) };
HashSet<Asset> itemsToReceive = [
CreateItem(3, amount: 3)
];
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
@@ -284,17 +305,19 @@ public sealed class Trading {
[TestMethod]
public void SingleGameQuantityNeutralAccept() {
HashSet<Asset> inventory = new() {
CreateItem(1, 2),
HashSet<Asset> inventory = [
CreateItem(1, amount: 2),
CreateItem(2)
};
];
HashSet<Asset> itemsToGive = new() {
HashSet<Asset> itemsToGive = [
CreateItem(1),
CreateItem(2)
};
];
HashSet<Asset> itemsToReceive = new() { CreateItem(3, 2) };
HashSet<Asset> itemsToReceive = [
CreateItem(3, amount: 2)
];
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
@@ -302,13 +325,18 @@ public sealed class Trading {
[TestMethod]
public void SingleGameSingleTypeBadReject() {
HashSet<Asset> inventory = new() {
HashSet<Asset> inventory = [
CreateItem(1),
CreateItem(2)
};
];
HashSet<Asset> itemsToGive = new() { CreateItem(1) };
HashSet<Asset> itemsToReceive = new() { CreateItem(2) };
HashSet<Asset> itemsToGive = [
CreateItem(1)
];
HashSet<Asset> itemsToReceive = [
CreateItem(2)
];
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
@@ -316,18 +344,20 @@ public sealed class Trading {
[TestMethod]
public void SingleGameSingleTypeBadWithOverpayingReject() {
HashSet<Asset> inventory = new() {
CreateItem(1, 2),
CreateItem(2, 2),
CreateItem(3, 2)
};
HashSet<Asset> inventory = [
CreateItem(1, amount: 2),
CreateItem(2, amount: 2),
CreateItem(3, amount: 2)
];
HashSet<Asset> itemsToGive = new() { CreateItem(2) };
HashSet<Asset> itemsToGive = [
CreateItem(2)
];
HashSet<Asset> itemsToReceive = new() {
HashSet<Asset> itemsToReceive = [
CreateItem(1),
CreateItem(3)
};
];
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
@@ -335,14 +365,19 @@ public sealed class Trading {
[TestMethod]
public void SingleGameSingleTypeBigDifferenceAccept() {
HashSet<Asset> inventory = new() {
HashSet<Asset> inventory = [
CreateItem(1),
CreateItem(2, 5),
CreateItem(2, amount: 5),
CreateItem(3)
};
];
HashSet<Asset> itemsToGive = new() { CreateItem(2) };
HashSet<Asset> itemsToReceive = new() { CreateItem(3) };
HashSet<Asset> itemsToGive = [
CreateItem(2)
];
HashSet<Asset> itemsToReceive = [
CreateItem(3)
];
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
@@ -350,23 +385,23 @@ public sealed class Trading {
[TestMethod]
public void SingleGameSingleTypeBigDifferenceReject() {
HashSet<Asset> inventory = new() {
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 = new() {
HashSet<Asset> itemsToGive = [
CreateItem(2),
CreateItem(5)
};
];
HashSet<Asset> itemsToReceive = new() {
HashSet<Asset> itemsToReceive = [
CreateItem(3),
CreateItem(4)
};
];
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
@@ -374,9 +409,17 @@ public sealed class Trading {
[TestMethod]
public void SingleGameSingleTypeGoodAccept() {
HashSet<Asset> inventory = new() { CreateItem(1, 2) };
HashSet<Asset> itemsToGive = new() { CreateItem(1) };
HashSet<Asset> itemsToReceive = new() { 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));
@@ -384,9 +427,17 @@ public sealed class Trading {
[TestMethod]
public void SingleGameSingleTypeNeutralAccept() {
HashSet<Asset> inventory = new() { CreateItem(1) };
HashSet<Asset> itemsToGive = new() { CreateItem(1) };
HashSet<Asset> itemsToReceive = new() { 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));
@@ -394,17 +445,19 @@ public sealed class Trading {
[TestMethod]
public void SingleGameSingleTypeNeutralWithOverpayingAccept() {
HashSet<Asset> inventory = new() {
CreateItem(1, 2),
CreateItem(2, 2)
};
HashSet<Asset> inventory = [
CreateItem(1, amount: 2),
CreateItem(2, amount: 2)
];
HashSet<Asset> itemsToGive = new() { CreateItem(2) };
HashSet<Asset> itemsToGive = [
CreateItem(2)
];
HashSet<Asset> itemsToReceive = new() {
HashSet<Asset> itemsToReceive = [
CreateItem(1),
CreateItem(3)
};
];
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
@@ -412,27 +465,29 @@ public sealed class Trading {
[TestMethod]
public void TakingExcessiveAmountOfSingleCardCanStillBeFairAndNeutral() {
HashSet<Asset> inventory = new() {
CreateItem(1, 52),
CreateItem(2, 73),
CreateItem(3, 52),
CreateItem(4, 47),
HashSet<Asset> inventory = [
CreateItem(1, amount: 52),
CreateItem(2, amount: 73),
CreateItem(3, amount: 52),
CreateItem(4, amount: 47),
CreateItem(5)
};
];
HashSet<Asset> itemsToGive = new() { CreateItem(2, 73) };
HashSet<Asset> itemsToGive = [
CreateItem(2, amount: 73)
];
HashSet<Asset> itemsToReceive = new() {
CreateItem(1, 9),
CreateItem(3, 9),
CreateItem(4, 8),
CreateItem(5, 24),
CreateItem(6, 23)
};
HashSet<Asset> itemsToReceive = [
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,10 +1,12 @@
// _ _ _ ____ _ _____
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

File diff suppressed because one or more lines are too long

View File

@@ -4,6 +4,9 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<OpenApiGenerateDocuments>false</OpenApiGenerateDocuments>
<OutputType>Exe</OutputType>
<!-- TODO: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2550 -->
<JsonSerializerIsReflectionEnabledByDefault>true</JsonSerializerIsReflectionEnabledByDefault>
</PropertyGroup>
<ItemGroup>
@@ -13,15 +16,13 @@
<PackageReference Include="Humanizer" />
<PackageReference Include="JetBrains.Annotations" PrivateAssets="all" />
<PackageReference Include="Markdig.Signed" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" />
<PackageReference Include="Nito.AsyncEx.Coordination" />
<PackageReference Include="NLog.Web.AspNetCore" />
<PackageReference Include="SteamKit2" />
<PackageReference Include="Swashbuckle.AspNetCore" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" />
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" />
<PackageReference Include="System.Composition" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" />
<PackageReference Include="System.Linq.Async" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" />
<PackageReference Include="zxcvbn-core" />
@@ -63,7 +64,7 @@
</Content>
</ItemGroup>
<ItemGroup Condition="Exists($([System.IO.Path]::Combine('overlay', 'variant-base', $(ASFVariant.Split('-')[0]))))">
<ItemGroup Condition="'$(ASFVariant)' != '' AND Exists($([System.IO.Path]::Combine('overlay', 'variant-base', $(ASFVariant.Split('-')[0]))))">
<Content Include="overlay/variant-base/$(ASFVariant.Split('-')[0])/**/*.*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
@@ -71,7 +72,7 @@
</Content>
</ItemGroup>
<ItemGroup Condition="Exists($([System.IO.Path]::Combine('overlay', 'variant-specific', $(ASFVariant))))">
<ItemGroup Condition="'$(ASFVariant)' != '' AND Exists($([System.IO.Path]::Combine('overlay', 'variant-specific', $(ASFVariant))))">
<Content Include="overlay/variant-specific/$(ASFVariant)/**/*.*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -24,11 +26,12 @@ using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using JetBrains.Annotations;
namespace ArchiSteamFarm.Collections;
public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ISet<T> where T : notnull {
public sealed class ConcurrentHashSet<T> : IReadOnlySet<T>, ISet<T> where T : notnull {
[PublicAPI]
public event EventHandler? OnModified;
@@ -37,15 +40,31 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ISet<T> where
private readonly ConcurrentDictionary<T, bool> BackingCollection;
[JsonConstructor]
public ConcurrentHashSet() => BackingCollection = new ConcurrentDictionary<T, bool>();
public ConcurrentHashSet(IEnumerable<T> collection) {
ArgumentNullException.ThrowIfNull(collection);
BackingCollection = new ConcurrentDictionary<T, bool>(collection.Select(static item => new KeyValuePair<T, bool>(item, true)));
}
public ConcurrentHashSet(IEqualityComparer<T> comparer) {
ArgumentNullException.ThrowIfNull(comparer);
BackingCollection = new ConcurrentDictionary<T, bool>(comparer);
}
public ConcurrentHashSet(IEnumerable<T> collection, IEqualityComparer<T> comparer) {
ArgumentNullException.ThrowIfNull(collection);
ArgumentNullException.ThrowIfNull(comparer);
BackingCollection = new ConcurrentDictionary<T, bool>(collection.Select(static item => new KeyValuePair<T, bool>(item, true)), comparer);
}
public bool Add(T item) {
ArgumentNullException.ThrowIfNull(item);
if (!BackingCollection.TryAdd(item, true)) {
return false;
}
@@ -65,9 +84,18 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ISet<T> where
OnModified?.Invoke(this, EventArgs.Empty);
}
public bool Contains(T item) => BackingCollection.ContainsKey(item);
public bool Contains(T item) {
ArgumentNullException.ThrowIfNull(item);
public void CopyTo(T[] array, int arrayIndex) => BackingCollection.Keys.CopyTo(array, arrayIndex);
return BackingCollection.ContainsKey(item);
}
public void CopyTo(T[] array, int arrayIndex) {
ArgumentNullException.ThrowIfNull(array);
ArgumentOutOfRangeException.ThrowIfNegative(arrayIndex);
BackingCollection.Keys.CopyTo(array, arrayIndex);
}
public void ExceptWith(IEnumerable<T> other) {
ArgumentNullException.ThrowIfNull(other);
@@ -80,6 +108,8 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ISet<T> where
public IEnumerator<T> GetEnumerator() => BackingCollection.Keys.GetEnumerator();
public void IntersectWith(IEnumerable<T> other) {
ArgumentNullException.ThrowIfNull(other);
ISet<T> otherSet = other as ISet<T> ?? other.ToHashSet();
foreach (T item in this.Where(item => !otherSet.Contains(item))) {
@@ -88,36 +118,48 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ISet<T> where
}
public bool IsProperSubsetOf(IEnumerable<T> other) {
ArgumentNullException.ThrowIfNull(other);
ISet<T> otherSet = other as ISet<T> ?? other.ToHashSet();
return (otherSet.Count > Count) && IsSubsetOf(otherSet);
}
public bool IsProperSupersetOf(IEnumerable<T> other) {
ArgumentNullException.ThrowIfNull(other);
ISet<T> otherSet = other as ISet<T> ?? other.ToHashSet();
return (otherSet.Count < Count) && IsSupersetOf(otherSet);
}
public bool IsSubsetOf(IEnumerable<T> other) {
ArgumentNullException.ThrowIfNull(other);
ISet<T> otherSet = other as ISet<T> ?? other.ToHashSet();
return this.All(otherSet.Contains);
}
public bool IsSupersetOf(IEnumerable<T> other) {
ArgumentNullException.ThrowIfNull(other);
ISet<T> otherSet = other as ISet<T> ?? other.ToHashSet();
return otherSet.All(Contains);
}
public bool Overlaps(IEnumerable<T> other) {
ArgumentNullException.ThrowIfNull(other);
ISet<T> otherSet = other as ISet<T> ?? other.ToHashSet();
return otherSet.Any(Contains);
}
public bool Remove(T item) {
ArgumentNullException.ThrowIfNull(item);
if (!BackingCollection.TryRemove(item, out _)) {
return false;
}
@@ -128,14 +170,18 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ISet<T> where
}
public bool SetEquals(IEnumerable<T> other) {
ArgumentNullException.ThrowIfNull(other);
ISet<T> otherSet = other as ISet<T> ?? other.ToHashSet();
return (otherSet.Count == Count) && otherSet.All(Contains);
}
public void SymmetricExceptWith(IEnumerable<T> other) {
ArgumentNullException.ThrowIfNull(other);
ISet<T> otherSet = other as ISet<T> ?? other.ToHashSet();
HashSet<T> removed = new();
HashSet<T> removed = [];
foreach (T item in otherSet.Where(Contains)) {
removed.Add(item);
@@ -155,12 +201,18 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ISet<T> where
}
}
void ICollection<T>.Add(T item) => Add(item);
void ICollection<T>.Add(T item) {
ArgumentNullException.ThrowIfNull(item);
Add(item);
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
[PublicAPI]
public bool AddRange(IEnumerable<T> items) {
ArgumentNullException.ThrowIfNull(items);
bool result = false;
foreach (T _ in items.Where(Add)) {
@@ -172,6 +224,8 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ISet<T> where
[PublicAPI]
public bool RemoveRange(IEnumerable<T> items) {
ArgumentNullException.ThrowIfNull(items);
bool result = false;
foreach (T _ in items.Where(Remove)) {
@@ -181,8 +235,17 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ISet<T> where
return result;
}
[PublicAPI]
public int RemoveWhere(Predicate<T> match) {
ArgumentNullException.ThrowIfNull(match);
return BackingCollection.Keys.Where(match.Invoke).Count(key => BackingCollection.TryRemove(key, out _));
}
[PublicAPI]
public bool ReplaceIfNeededWith(IReadOnlyCollection<T> other) {
ArgumentNullException.ThrowIfNull(other);
if (SetEquals(other)) {
return false;
}
@@ -194,6 +257,8 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ISet<T> where
[PublicAPI]
public void ReplaceWith(IEnumerable<T> other) {
ArgumentNullException.ThrowIfNull(other);
Clear();
UnionWith(other);
}

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -22,18 +24,18 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using JetBrains.Annotations;
using Nito.AsyncEx;
namespace ArchiSteamFarm.Collections;
internal sealed class ConcurrentList<T> : IList<T>, IReadOnlyList<T> {
public sealed class ConcurrentList<T> : IList<T>, IReadOnlyList<T> where T : notnull {
[PublicAPI]
public event EventHandler? OnModified;
public bool IsReadOnly => false;
internal int Count {
[PublicAPI]
public int Count {
get {
using (Lock.ReaderLock()) {
return BackingCollection.Count;
@@ -41,7 +43,9 @@ internal sealed class ConcurrentList<T> : IList<T>, IReadOnlyList<T> {
}
}
private readonly List<T> BackingCollection = new();
public bool IsReadOnly => false;
private readonly List<T> BackingCollection;
private readonly AsyncReaderWriterLock Lock = new();
int ICollection<T>.Count => Count;
@@ -55,6 +59,8 @@ internal sealed class ConcurrentList<T> : IList<T>, IReadOnlyList<T> {
}
set {
ArgumentNullException.ThrowIfNull(value);
using (Lock.WriterLock()) {
BackingCollection[index] = value;
}
@@ -63,7 +69,18 @@ internal sealed class ConcurrentList<T> : IList<T>, IReadOnlyList<T> {
}
}
[JsonConstructor]
public ConcurrentList() => BackingCollection = [];
public ConcurrentList(IEnumerable<T> collection) {
ArgumentNullException.ThrowIfNull(collection);
BackingCollection = [..collection];
}
public void Add(T item) {
ArgumentNullException.ThrowIfNull(item);
using (Lock.WriterLock()) {
BackingCollection.Add(item);
}
@@ -80,12 +97,17 @@ internal sealed class ConcurrentList<T> : IList<T>, IReadOnlyList<T> {
}
public bool Contains(T item) {
ArgumentNullException.ThrowIfNull(item);
using (Lock.ReaderLock()) {
return BackingCollection.Contains(item);
}
}
public void CopyTo(T[] array, int arrayIndex) {
ArgumentNullException.ThrowIfNull(array);
ArgumentOutOfRangeException.ThrowIfNegative(arrayIndex);
using (Lock.ReaderLock()) {
BackingCollection.CopyTo(array, arrayIndex);
}
@@ -94,12 +116,17 @@ internal sealed class ConcurrentList<T> : IList<T>, IReadOnlyList<T> {
public IEnumerator<T> GetEnumerator() => new ConcurrentEnumerator<T>(BackingCollection, Lock.ReaderLock());
public int IndexOf(T item) {
ArgumentNullException.ThrowIfNull(item);
using (Lock.ReaderLock()) {
return BackingCollection.IndexOf(item);
}
}
public void Insert(int index, T item) {
ArgumentOutOfRangeException.ThrowIfNegative(index);
ArgumentNullException.ThrowIfNull(item);
using (Lock.WriterLock()) {
BackingCollection.Insert(index, item);
}
@@ -108,6 +135,8 @@ internal sealed class ConcurrentList<T> : IList<T>, IReadOnlyList<T> {
}
public bool Remove(T item) {
ArgumentNullException.ThrowIfNull(item);
using (Lock.WriterLock()) {
if (!BackingCollection.Remove(item)) {
return false;
@@ -120,6 +149,8 @@ internal sealed class ConcurrentList<T> : IList<T>, IReadOnlyList<T> {
}
public void RemoveAt(int index) {
ArgumentOutOfRangeException.ThrowIfNegative(index);
using (Lock.WriterLock()) {
BackingCollection.RemoveAt(index);
}
@@ -129,7 +160,10 @@ internal sealed class ConcurrentList<T> : IList<T>, IReadOnlyList<T> {
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
internal void ReplaceWith(IEnumerable<T> collection) {
[PublicAPI]
public void ReplaceWith(IEnumerable<T> collection) {
ArgumentNullException.ThrowIfNull(collection);
using (Lock.WriterLock()) {
BackingCollection.Clear();
BackingCollection.AddRange(collection);

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -23,22 +25,17 @@ using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using ArchiSteamFarm.Core;
namespace ArchiSteamFarm.Collections;
internal sealed class FixedSizeConcurrentQueue<T> : IEnumerable<T> {
internal sealed class FixedSizeConcurrentQueue<T> : IEnumerable<T> where T : notnull {
private readonly ConcurrentQueue<T> BackingQueue = new();
internal byte MaxCount {
get => BackingMaxCount;
set {
if (value == 0) {
ASF.ArchiLogger.LogNullError(value);
return;
}
ArgumentOutOfRangeException.ThrowIfZero(value);
BackingMaxCount = value;
@@ -58,6 +55,8 @@ internal sealed class FixedSizeConcurrentQueue<T> : IEnumerable<T> {
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
internal void Enqueue(T obj) {
ArgumentNullException.ThrowIfNull(obj);
BackingQueue.Enqueue(obj);
Resize();

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -23,12 +25,13 @@ using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using JetBrains.Annotations;
using Newtonsoft.Json;
namespace ArchiSteamFarm.Collections;
public sealed class ObservableConcurrentDictionary<TKey, TValue> : IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue> where TKey : notnull {
[PublicAPI]
public event EventHandler? OnModified;
[PublicAPI]
@@ -42,8 +45,7 @@ public sealed class ObservableConcurrentDictionary<TKey, TValue> : IDictionary<T
[PublicAPI]
public ICollection<TKey> Keys => BackingDictionary.Keys;
[JsonProperty(Required = Required.DisallowNull)]
private readonly ConcurrentDictionary<TKey, TValue> BackingDictionary = new();
private readonly ConcurrentDictionary<TKey, TValue> BackingDictionary;
int ICollection<KeyValuePair<TKey, TValue>>.Count => BackingDictionary.Count;
int IReadOnlyCollection<KeyValuePair<TKey, TValue>>.Count => BackingDictionary.Count;
@@ -54,7 +56,10 @@ public sealed class ObservableConcurrentDictionary<TKey, TValue> : IDictionary<T
public TValue this[TKey key] {
get => BackingDictionary[key];
set {
ArgumentNullException.ThrowIfNull(value);
if (BackingDictionary.TryGetValue(key, out TValue? savedValue) && EqualityComparer<TValue>.Default.Equals(savedValue, value)) {
return;
}
@@ -64,13 +69,41 @@ public sealed class ObservableConcurrentDictionary<TKey, TValue> : IDictionary<T
}
}
[JsonConstructor]
public ObservableConcurrentDictionary() => BackingDictionary = new ConcurrentDictionary<TKey, TValue>();
public ObservableConcurrentDictionary(IEnumerable<KeyValuePair<TKey, TValue>> collection) {
ArgumentNullException.ThrowIfNull(collection);
BackingDictionary = new ConcurrentDictionary<TKey, TValue>(collection);
}
public ObservableConcurrentDictionary(IEqualityComparer<TKey> comparer) {
ArgumentNullException.ThrowIfNull(comparer);
BackingDictionary = new ConcurrentDictionary<TKey, TValue>(comparer);
}
public ObservableConcurrentDictionary(IEnumerable<KeyValuePair<TKey, TValue>> collection, IEqualityComparer<TKey> comparer) {
ArgumentNullException.ThrowIfNull(collection);
ArgumentNullException.ThrowIfNull(comparer);
BackingDictionary = new ConcurrentDictionary<TKey, TValue>(collection, comparer);
}
public void Add(KeyValuePair<TKey, TValue> item) {
(TKey key, TValue value) = item;
Add(key, value);
}
public void Add(TKey key, TValue value) => TryAdd(key, value);
public void Add(TKey key, TValue value) {
ArgumentNullException.ThrowIfNull(key);
if (!TryAdd(key, value)) {
throw new ArgumentException($"An item with the same key has already been added. Key: {key}");
}
}
public void Clear() {
if (BackingDictionary.IsEmpty) {
@@ -82,7 +115,14 @@ public sealed class ObservableConcurrentDictionary<TKey, TValue> : IDictionary<T
}
public bool Contains(KeyValuePair<TKey, TValue> item) => ((ICollection<KeyValuePair<TKey, TValue>>) BackingDictionary).Contains(item);
public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex) => ((ICollection<KeyValuePair<TKey, TValue>>) BackingDictionary).CopyTo(array, arrayIndex);
public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex) {
ArgumentNullException.ThrowIfNull(array);
ArgumentOutOfRangeException.ThrowIfNegative(arrayIndex);
((ICollection<KeyValuePair<TKey, TValue>>) BackingDictionary).CopyTo(array, arrayIndex);
}
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() => BackingDictionary.GetEnumerator();
public bool Remove(KeyValuePair<TKey, TValue> item) {
@@ -98,6 +138,8 @@ public sealed class ObservableConcurrentDictionary<TKey, TValue> : IDictionary<T
}
public bool Remove(TKey key) {
ArgumentNullException.ThrowIfNull(key);
if (!BackingDictionary.TryRemove(key, out _)) {
return false;
}
@@ -107,14 +149,36 @@ public sealed class ObservableConcurrentDictionary<TKey, TValue> : IDictionary<T
return true;
}
bool IDictionary<TKey, TValue>.ContainsKey(TKey key) => BackingDictionary.ContainsKey(key);
bool IReadOnlyDictionary<TKey, TValue>.ContainsKey(TKey key) => BackingDictionary.ContainsKey(key);
bool IDictionary<TKey, TValue>.ContainsKey(TKey key) {
ArgumentNullException.ThrowIfNull(key);
return BackingDictionary.ContainsKey(key);
}
bool IReadOnlyDictionary<TKey, TValue>.ContainsKey(TKey key) {
ArgumentNullException.ThrowIfNull(key);
return BackingDictionary.ContainsKey(key);
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
bool IReadOnlyDictionary<TKey, TValue>.TryGetValue(TKey key, out TValue value) => BackingDictionary.TryGetValue(key, out value!);
bool IDictionary<TKey, TValue>.TryGetValue(TKey key, out TValue value) => BackingDictionary.TryGetValue(key, out value!);
bool IReadOnlyDictionary<TKey, TValue>.TryGetValue(TKey key, out TValue value) {
ArgumentNullException.ThrowIfNull(key);
return BackingDictionary.TryGetValue(key, out value!);
}
bool IDictionary<TKey, TValue>.TryGetValue(TKey key, out TValue value) {
ArgumentNullException.ThrowIfNull(key);
return BackingDictionary.TryGetValue(key, out value!);
}
[PublicAPI]
public bool TryAdd(TKey key, TValue value) {
ArgumentNullException.ThrowIfNull(key);
if (!BackingDictionary.TryAdd(key, value)) {
return false;
}
@@ -125,5 +189,9 @@ public sealed class ObservableConcurrentDictionary<TKey, TValue> : IDictionary<T
}
[PublicAPI]
public bool TryGetValue(TKey key, out TValue? value) => BackingDictionary.TryGetValue(key, out value);
public bool TryGetValue(TKey key, out TValue? value) {
ArgumentNullException.ThrowIfNull(key);
return BackingDictionary.TryGetValue(key, out value);
}
}

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -21,8 +23,8 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
@@ -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;
@@ -75,9 +79,9 @@ public static class ASF {
internal static ICrossProcessSemaphore? LoginRateLimitingSemaphore { get; private set; }
internal static ICrossProcessSemaphore? LoginSemaphore { get; private set; }
internal static ICrossProcessSemaphore? RateLimitingSemaphore { get; private set; }
internal static ImmutableDictionary<Uri, (ICrossProcessSemaphore RateLimitingSemaphore, SemaphoreSlim OpenConnectionsSemaphore)>? WebLimitingSemaphores { get; private set; }
internal static FrozenDictionary<Uri, (ICrossProcessSemaphore RateLimitingSemaphore, SemaphoreSlim OpenConnectionsSemaphore)>? WebLimitingSemaphores { get; private set; }
private static readonly ImmutableHashSet<string> AssembliesNeededBeforeUpdate = ImmutableHashSet.Create(StringComparer.Ordinal, "System.IO.Pipes");
private static readonly FrozenSet<string> AssembliesNeededBeforeUpdate = new HashSet<string>(1, StringComparer.Ordinal) { "System.IO.Pipes" }.ToFrozenSet(StringComparer.Ordinal);
private static readonly SemaphoreSlim UpdateSemaphore = new(1, 1);
private static Timer? AutoUpdatesTimer;
@@ -101,23 +105,32 @@ public static class ASF {
return fileType switch {
EFileType.Config => Path.Combine(SharedInfo.ConfigDirectory, SharedInfo.GlobalConfigFileName),
EFileType.Database => Path.Combine(SharedInfo.ConfigDirectory, SharedInfo.GlobalDatabaseFileName),
EFileType.Crash => Path.Combine(SharedInfo.ConfigDirectory, SharedInfo.GlobalCrashFileName),
_ => throw new InvalidOperationException(nameof(fileType))
};
}
internal static async Task Init() {
internal static async Task<bool> Init() {
if (GlobalConfig == null) {
throw new InvalidOperationException(nameof(GlobalConfig));
}
if (!PluginsCore.InitPlugins()) {
await Task.Delay(SharedInfo.InformationDelay).ConfigureAwait(false);
}
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)) {
ArchiLogger.LogFatalError(Strings.ErrorTooManyCrashes);
return true;
}
Program.AllowCrashFileRemoval = true;
await PluginsCore.OnASFInitModules(GlobalConfig.AdditionalProperties).ConfigureAwait(false);
await InitRateLimiters().ConfigureAwait(false);
@@ -142,6 +155,8 @@ public static class ASF {
if (Program.ConfigWatch) {
InitConfigWatchEvents();
}
return true;
}
internal static bool IsValidBotName(string botName) {
@@ -174,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) {
@@ -441,7 +267,7 @@ public static class ASF {
{ ArchiWebHandler.SteamHelpURL, (await PluginsCore.GetCrossProcessSemaphore($"{nameof(ArchiWebHandler)}-{nameof(ArchiWebHandler.SteamHelpURL)}").ConfigureAwait(false), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) },
{ ArchiWebHandler.SteamStoreURL, (await PluginsCore.GetCrossProcessSemaphore($"{nameof(ArchiWebHandler)}-{nameof(ArchiWebHandler.SteamStoreURL)}").ConfigureAwait(false), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) },
{ WebAPI.DefaultBaseAddress, (await PluginsCore.GetCrossProcessSemaphore($"{nameof(ArchiWebHandler)}-{nameof(WebAPI)}").ConfigureAwait(false), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) }
}.ToImmutableDictionary();
}.ToFrozenDictionary();
}
private static void LoadAllAssemblies() {
@@ -789,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);
@@ -813,6 +629,37 @@ public static class ASF {
}
}
private static async Task<bool> ProtectAgainstCrashes() {
if (Debugging.IsDebugBuild) {
// Allow debug builds to run unconditionally, we expect to crash a lot in those
return true;
}
string crashFilePath = GetFilePath(EFileType.Crash);
CrashFile crashFile = await CrashFile.CreateOrLoad(crashFilePath).ConfigureAwait(false);
if (crashFile.StartupCount >= WebBrowser.MaxTries) {
// We've reached maximum allowed count of recent crashes, return failure
return false;
}
DateTime now = DateTime.UtcNow;
if (now - crashFile.LastStartup > TimeSpan.FromMinutes(5)) {
// Last crash was long ago, restart counter
crashFile.StartupCount = 1;
} else if (++crashFile.StartupCount >= WebBrowser.MaxTries) {
// We've reached maximum allowed count of recent crashes, return failure
return false;
}
crashFile.LastStartup = now;
// We're allowing this run to proceed
return true;
}
private static async Task RegisterBots() {
if (GlobalConfig == null) {
throw new InvalidOperationException(nameof(GlobalConfig));
@@ -873,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;
}
@@ -890,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);
}
@@ -905,12 +749,230 @@ public static class ASF {
return;
}
// Allow crash file recovery, if needed
Program.AllowCrashFileRemoval = true;
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
@@ -923,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]
@@ -1047,6 +1005,7 @@ public static class ASF {
internal enum EFileType : byte {
Config,
Database
Database,
Crash
}
}

View File

@@ -1,10 +1,12 @@
// _ _ _ ____ _ _____
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -1,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -26,6 +28,12 @@ using System.Runtime.Versioning;
namespace ArchiSteamFarm.Core;
internal static partial class NativeMethods {
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
[SupportedOSPlatform("Windows")]
[return: MarshalAs(UnmanagedType.Bool)]
[LibraryImport("user32.dll")]
internal static partial void FlashWindowEx(ref FlashWindowInfo pwfi);
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
[SupportedOSPlatform("Windows")]
[return: MarshalAs(UnmanagedType.Bool)]
@@ -77,6 +85,16 @@ internal static partial class NativeMethods {
Awake = SystemRequired | AwayModeRequired | Continuous
}
[SupportedOSPlatform("Windows")]
[Flags]
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,15 @@ internal static partial class NativeMethods {
internal enum EStandardHandle {
Input = -10
}
[StructLayout(LayoutKind.Sequential)]
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,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -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,10 +1,12 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -21,11 +23,12 @@
using System;
using System.Collections;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IdentityModel.Tokens.Jwt;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Resources;
@@ -35,10 +38,12 @@ using System.Threading.Tasks;
using AngleSharp.Dom;
using AngleSharp.XPath;
using ArchiSteamFarm.Localization;
using ArchiSteamFarm.NLog;
using ArchiSteamFarm.Storage;
using Humanizer;
using Humanizer.Localisation;
using JetBrains.Annotations;
using Microsoft.IdentityModel.JsonWebTokens;
using SteamKit2;
using Zxcvbn;
@@ -48,9 +53,7 @@ public static class Utilities {
private const byte TimeoutForLongRunningTasksInSeconds = 60;
// normally we'd just use words like "steam" and "farm", but the library we're currently using is a bit iffy about banned words, so we need to also add combinations such as "steamfarm"
private static readonly ImmutableHashSet<string> ForbiddenPasswordPhrases = ImmutableHashSet.Create(StringComparer.InvariantCultureIgnoreCase, "archisteamfarm", "archi", "steam", "farm", "archisteam", "archifarm", "steamfarm", "asf", "asffarm", "password");
private static readonly JwtSecurityTokenHandler JwtSecurityTokenHandler = new();
private static readonly FrozenSet<string> ForbiddenPasswordPhrases = new HashSet<string>(10, StringComparer.InvariantCultureIgnoreCase) { "archisteamfarm", "archi", "steam", "farm", "archisteam", "archifarm", "steamfarm", "asf", "asffarm", "password" }.ToFrozenSet(StringComparer.InvariantCultureIgnoreCase);
[PublicAPI]
public static string GenerateChecksumFor(byte[] source) {
@@ -123,7 +126,7 @@ public static class Utilities {
switch (ASF.GlobalConfig?.OptimizationMode) {
case GlobalConfig.EOptimizationMode.MinMemoryUsage:
List<T> results = new();
List<T> results = [];
foreach (Task<T> task in tasks) {
results.Add(await task.ConfigureAwait(false));
@@ -179,19 +182,6 @@ public static class Utilities {
return (text.Length % 2 == 0) && text.All(Uri.IsHexDigit);
}
[PublicAPI]
public static JwtSecurityToken? ReadJwtToken(string token) {
ArgumentException.ThrowIfNullOrEmpty(token);
try {
return JwtSecurityTokenHandler.ReadJwtToken(token);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
return null;
}
}
[PublicAPI]
public static IList<INode> SelectNodes(this IDocument document, string xpath) {
ArgumentNullException.ThrowIfNull(document);
@@ -260,24 +250,21 @@ public static class Utilities {
return job.ToTask();
}
internal static void DeleteEmptyDirectoriesRecursively(string directory) {
ArgumentException.ThrowIfNullOrEmpty(directory);
if (!Directory.Exists(directory)) {
return;
}
[PublicAPI]
public static bool TryReadJsonWebToken(string token, [NotNullWhen(true)] out JsonWebToken? result) {
ArgumentException.ThrowIfNullOrEmpty(token);
try {
foreach (string subDirectory in Directory.EnumerateDirectories(directory)) {
DeleteEmptyDirectoriesRecursively(subDirectory);
}
if (!Directory.EnumerateFileSystemEntries(directory).Any()) {
Directory.Delete(directory);
}
result = new JsonWebToken(token);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
ASF.ArchiLogger.LogGenericDebuggingException(e);
result = null;
return false;
}
return true;
}
internal static ulong MathAdd(ulong first, int second) {
@@ -288,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) {
@@ -334,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);
@@ -388,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));
}
}

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